The fixture package now has real vertices (BIN chunk written by the generator, no committed bytes), an energy hardpoint "gun" with a
muzzle anchor, and the thruster moved outside the schematic, so ShipModShips.Assemble accepts it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- RequireDesigns names the null (entity, item, zone record, lot, lot ledger) instead of catching NullReferenceException.
- ResolveCatalog reports a Compose-level failure as an exclusion covering all mods and boots the shipped catalog.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Validate: a hardpoint's anchor role is fixed by its type (thruster-emitter, radiator-mesh, else articulation); ShipModPlan's
duplicate role check is deleted.
- Loadouts.Commit refuses a preset naming a mod design (mod-hull:/mod-ship:), so the shipped catalog never references a mod.
- Compose quarantines each package: a bad package is excluded and named, a collision excludes every package involved; the
game boots on the rest (or the shipped catalog) and the main menu lists the exclusions.
- RequireDesigns refuses a save with a null item or lot instead of throwing NullReferenceException.
- Tests: per structural role, muzzle id ties, every slot/zone/lot in RequireDesigns, listing order, exclusion cases.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Boot opens the derived catalog when GameData/Mods holds packages
(ShipModCatalog.ResolveCatalog), preloads each hull's GLB into an assembled
inactive prototype (ShipModShips), and LoadEntity clones it when the hull names
a Visual. ShipModPlan decides which anchor fills which ShipInstance slot; a
ShipModTemplate prefab carries the shared effects. Continue refuses a run that
names a design the catalog no longer holds (RunSave.RequireDesigns).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Load reads the raw ship authoring payload and refuses a non-nil retired key 1 (the
pre-S1 embedded hull) naming the old shape, so a stale hull is no longer silently
ignored; ship_cc.py refuses the same file on read and therefore on every save.
A hull naming both a Unity prefab and a visual is refused by one predicate
(ShipAuthoringStore.RequireOneBody), called by Validate and by Compose over every hull
in the derived catalog, shipped ones included; a whitespace-only Prefab is unset. This
replaces the mod-ship-only "cannot name a Unity prefab" check.
The validator also refuses non-finite or negative hardpoint Armor and FiringArc, an
undefined HardpointType or ItemRotation, and an anchor Role outside the eight the
schema documents.
Load wraps MessagePack decode failures with the file path. Python replace_layout
refuses non-boolean layout and hardpoint-footprint cells.
Tests: exact Visual key comparison, HullData.Visual at key 32, a save refreshing
stored_at, plus one case per new refusal. HullData.Visual is now [Inspectable] like
Prefab (Unity editor only; not compiled here).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A mod ship file now holds a HullData at mod-hull:<id> and the ShipAuthoring
visual package at mod-ship:<id>. HullData.Visual (key 32) is the typed ref
between them; ShipAuthoring.Hull (key 1) is retired.
- One joint validator over (hull, visual) is the only semantic check; Load
owns the file structure (one hull, one visual, deterministic keys).
- Compose copies the mod store's records verbatim, so the derived catalog
holds one hull per mod ship.
- ship_cc.py edits the hull record's slots directly; the body[HULL_SLOT]
indirection is gone.
- The package fixture is generated from ShipAuthoringTests.Fixture() (glTF
scene included) and exported when AETHERIA_SHIP_FIXTURE_DIR is set.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A pass of 137 hand-written mutations over ShipAuthoring.cs, ShipModCatalog.cs
and ship_cc.py, run against the tests from the last three commits, left six
surviving or unrunnable. Each is resolved:
- Validator: the hull Width/Height < 1 guards and the opacity IsInfinity
guard could not change any outcome (an empty grid has no occupied cell;
+Infinity exceeds 1 and -Infinity is below 0). Deleted.
- Two accepted shapes had no test: a hardpoint footprint with empty cells
beside occupied ones, and an empty ParentId meaning no parent. Added.
- The GLB header length was pinned only from above. Added the shorter case.
- Compose's working-directory cleanup was invisible because the assertion
listed files, not directory entries. It now lists both.
- ReadPackage runs the catalog's item validation on the hull (the derived
catalog must reopen); that call now has a test.
- Python: a single non-ship record, a non-array payload long enough to fill
every slot, and a hardpoint-only change to the layout revision each had a
surviving mutation. Tests added.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ship_cc.py edited a ShipAuthoring record by bare slot numbers and hardpoint
positions (hp[3], hp[7:], raw[0..6]), and __init__.py copied the
HardpointType and ItemRotation enums by position. Nothing tested either
file and nothing tied the numbers to the C# [Key] attributes that own them.
- ship_cc.py names its schema facts once: the four record slots (each
commented with the C# member it mirrors), HARDPOINT_MEMBERS (HardpointData
member names in slot order), and the two enum name tuples. Hardpoint rows
are built and read through encode_hardpoint/decode_hardpoint; replace_layout
and the Blender operators no longer index a row by literal. __init__.py
builds its enum items from the tuples. The rotation label
"Counterclockwise" becomes "CounterClockwise", matching the C# member.
- tools/blender/tests/test_ship_cc.py (30 unittest cases, bpy-free, needs
CultLib's cultcache-py and msgpack): read, replace_lines, read_layout,
replace_layout (untouched slots and later-schema hardpoint slots preserved
by mount id, schema catalog preserved, every refusal leaves the file
byte-identical), the row codec, and capture_grease_pencil against stand-in
objects.
- ShipSchemaPinTests (C#, 7 cases) reads ship_cc.py and fails when a slot
constant, a HARDPOINT_MEMBERS name or position, or an enum name/value
disagrees with the C# types, and when a serialized ship no longer has the
nested array shapes the Python indexes (schematic [[w, h, cells]], cells
x-major, hardpoint transform and footprint positions).
Reading the slots from the file's schema catalog instead was rejected: the
catalog lists only document members, and Hull, Shape and HardpointData are
nested types it does not describe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compose, ReadPackage and ReadNodeIds had no test. ShipModCatalogTests now
covers the derived catalog, the atomic replace of an existing one, every
failure path leaving the previous derived file byte-identical, the unsafe
source/output refusals, package binding (directory name, GLB existence and
extension, anchor node presence), and the GLB reader (each header guard, the
JSON chunk boundary, node index accounting, duplicate ids).
Writing them exposed three defects in Compose, fixed here:
- F6: AetheriaStores.CatalogTypes routes ShipAuthoring to the shipped
catalog, and CultCache indexes names per concrete type with last writer
winning while GetByName over a base type throws on a cross-subtype match.
Compose checked keys only, so a mod hull named like a shipped item, or like
another mod's hull, silently shadowed it or broke lookups. It now refuses
a hull name matching any catalog ItemData and a ship ID matching an
existing ShipAuthoring, naming the ship.
- The backing store writes a .lock file beside every file it opens, so each
Compose left <output>.<guid>.tmp.lock behind, success or failure, once per
boot. The working copy now lives in its own directory that is deleted whole.
- Deleted: the duplicate-ID check (ReadPackage already pins ID to directory
name, so it could not fire) and ReadPackage's containment StartsWith
(Validate already refuses rooted and `..` model paths on the same Read).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul's mutation pass on 375d6bd4 found 8 of 9 validator mutations
surviving, including the `..` model-path guard and the prefab refusal. The
validator had five hand-picked cases. ShipValidationTests now carries one
case per guard: each starts from the valid fixture, breaks exactly one
thing, and names the message that guard alone produces. It also pins the ID
alphabet at both ends of each range, the accepted model-asset spellings
beside the refused ones, the range edges of point attributes, an anchor
naming its hardpoint as parent, and Read's exactly-one-record and
validate-on-load behaviour.
ShipAuthoringTests.Fixture becomes internal so the new file shares it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A released charge has a burst pending and no cooldown until its first round, so a second Activate in that
window started a second charge; Activate now asks CanFire. The round that starts a burst's cooldown does so
after the tick's decrement, which made every cooldown one tick long; it now charges that tick to the cooldown.
Pins both, and that a burst triggered after a fired one is not ready before its first round.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A burst is sized from stats the first Execute resolves, so the untouched fixture gun fired nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BurstCount is set by the first Execute, so a trigger pulled with no tick has no burst pending. The
tests now run one tick and read whether the cooldown started.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cooldown now starts at a burst's first fired round, so Progress no longer moves at the trigger.
IffAndCombatTests read CanFire (false while a burst is pending) instead, and the charged-weapon
control uses the charge-start event name the sound bank resolves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A burst whose every round is refused fires nothing and leaves no cooldown: the cooldown moves from
Trigger to the first round Execute does not refuse, and CanFire holds a triggered burst that has not
yet fired. ChargedWeapon.Activate asks FireControl before it starts charging, so a refused weapon pays
no charge heat or audio. InReach is deleted: Designated carries the one range test and the lock gate,
and Combat reads it. GuidedProjectile answers Resolve while alive and keeps its binding through the
shot's flight time, so a faded round still shows the simulation's detonation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mutation pass on the fuse fixes left one survivor: a no-lock round whose
aim ends inside the blast radius (a designated target out of arc) flew only to
its aimed point instead of the arming distance, and nothing noticed. Its arrival
is now pinned to the arming distance, beside the targeted case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The trigger's own refusal check stopped a short-Range round before Execute
ever reached its refusal, so wear, heat, visibility and sound left ahead of
Execute's `continue` survived every mutation. The cost theory now also
authorises a burst directly, the way a trigger that passed leaves it, and the
separate ammo-only test folds into it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator ruling 2026-09-30. A fused round's burst is at least BlastRadius
beyond the far edge of the shooter's own hull, measured along the flight line
against the hull as it stood at fire (HullAhead's exit). A close burst -- a
target inside that distance, in or out of arc -- is pushed out along the aim
and carries no target; a contact round is floored to it too.
- The F3 trigger gate is deleted. Fire refuses a round (returns 0) whose
arming distance exceeds Range, per round, where Range is read after Execute
refreshed it; InstantWeapon and ConstantWeapon skip a refused round.
- PFire's range, Inspect's range and the range clamp are planar (R7).
- HullEntryDistance becomes HullAhead (entry and exit); the max(...,0) clamp
on entry is gone, the arming floor covers it.
- ShotOutcome.HasBurstPoint names the rounds that have a frozen burst point.
- SafeAssert: the tests no longer hand xunit an Entity, PendingShot or
ShotOutcome to format; a failing assertion had been overflowing the test
host's stack.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator ruling 2026-09-30: a fused weapon with a selected but out-of-arc
target fires. Its round flies along the arc-clamped aim and bursts at the
target's range. Non-fused weapons still refuse.
- FireControl.ArcPermitsFire is the one trigger arc gate (fused weapons are
exempt); Fire reads the same InArc through PFire's new designated/inArc
outs, which replace `bears`. No valid targeting data still bursts at Range.
- F3: a fused weapon whose effective Range does not exceed its BlastRadius
stays safed at the trigger (FuseCanArm, read from the runtime Range). The
catalog check on Range.Min is deleted; it read the authored floor, which
Evaluate can scale below.
- F4: a targeted proximity burst is pulled back along its line to Range.
- F8: a proximity round that detonates commits as Burst (or Hit); Apply's
Proximity case collapses into the Burst path via Outcome.BurstPoint.
- Tests: arc-only and LockWeapon gates, out-of-arc burst, trigger gate,
runtime-Range refusal, Range clamp, Burst typing, commit horizon, and
Soul's adopted probes (aim clamp edges, contact point, target leaves,
shooter destroyed).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Assert.Null(shot.Target) and Assert.Same(committed, resolved) format the
entity graph when they fail, which overflows the xunit stack and crashes the
host instead of reporting a red. Use Assert.True with a message.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator rulings 2026-09-30 after Soul's pass on 8c002637:
- A no-lock round flies along Entity.LookDirection restricted by the mount's
arc (FireControl.AimDirection: inside InArc's cone as is, outside it turned
to the cone's edge; no aim falls back to the mount). The simulation had no
aim clamp before; the old code used the hull-fixed mount direction.
- An invalid target (out of range, out of arc, not visible, not locked) counts
as no lock for a fused round. PFire's gate is the one decider and now also
reports `bears`; Fire reads it. The round carries no target afterwards.
- A no-lock Contact round bursts at the first hull its aim line crosses
before max range, read through Lane's own cell admission (CellsOnLine,
split out of Lane). Proximity and Delayed keep the max-range burst. Such a
round commits at Fire, since it has no target facing to read late, and
arrives when it has flown to the burst point. The seam for mining chunks is
FireControl.ContactDistance.
- A round outlives its shooter: Step no longer cancels a shot whose source
left the zone. Resolution reads the zone and the target; the shooter is an
identity a shot carries. PendingShot.FireOrigin freezes where it left from.
- ShotOutcome.Result (Miss, Hit, Burst) replaces the stored Hit bool, which
is now derived. A burst without a target is no longer reported as a miss,
and the debug HUD labels it BURST.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Soul: a no-lock round bursts at Range, so Range <= BlastRadius (or a zero
Range) lands the blast on the shooter. Made structurally impossible at the
catalog rather than policed at runtime: StatValidation.ValidateFuse joins
the checks CultRecordRefs.Validate runs on every Upsert, at the weakest value
the Range stat can take. Only a weapon that will detonate (a Fuse and a
BlastRadius above zero) is held to it.
AetheriaStores.Open now calls that same Validate for every equippable and
consumable document instead of carrying its own copy of the three checks, so
Upsert and Open can no longer disagree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Proximity, contact and delayed shots fired with no target burst at Range
along a non-axis aim (2,1) and leave the shooter undamaged; the shot flies
there at the weapon's Velocity. A targeted proximity shot keeps the burst
point and the damage value measured at b66ba524 (5.65884256).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Assert.Contains on a Ship formats the whole entity graph on failure and overflowed the stack, aborting
the test run instead of reporting red; the new tests check membership with a boolean and a message. The
cockpit-kill test now gives the target a hull the blast also kills, the case where the double Death
actually occurred.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Entity.Death, the owner of the stream, now ends after its first cause. A blast that destroyed a
cockpit fired Death, which removed and deactivated the entity, and the blast's own DamageHull then
raised a second Death to every subscriber but Zone's idempotent handler. Every damage path gets the
guard because none of them raise Death themselves.
New pins: one blast killing three ships (a break after the first kill survives the old suite), a
shieldless ship outside the disc receiving nothing, a cockpit-kill blast and a direct hull kill each
raising Death exactly once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer failed in 1 of 12 full-suite
runs (1232 bytes over 1000 Lane calls) and in 0 of 100 runs of the test alone
or its class. The allocation is a one-off, not a per-call cost: under suite
load the runtime's background tier-up is delayed by other tests' JIT activity,
so first-use allocation lands inside the single measured window.
The rule stays: Lane and buffered Silhouette allocate nothing. The test now
measures up to 20 rounds of 1000 calls and requires the smallest round to be
zero. A per-call allocation appears in every round and still fails it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each rule below was protected only by a test a mutant passed. New or
strengthened tests, with the mutant each one kills:
- AContactOrDelayedBlastOnAMissDoesNothing: ApplyBlastHit's miss guard.
- ToWorldPointIsTheInverseOfToSchematicPointAtANonAxisFacing and
AContactBlastLandsOnTheCellItHitAtANonAxisFacing: ToWorldPoint's right axis,
asserted per cell at facing (2,1); TurningAfterCommit now compares per cell.
- ProximityBurstPositionIsThePredictedInterceptForAMovingTarget: a moving
target and an intercept solved in the test, not Durability <= before.
- AnUntouchedEntitysShieldIsNotConsulted: Detonate's skip of untouched
entities.
- TheFusePointStaysOnMetalAcrossAConcaveGap: the delayed point clamps at the
first gap (the lane's walk ends there).
- ShieldPaysForWhatReachesIt asserts the reserve dropped by the covered share;
AShieldRaisedBetweenCommitAndArrivalAbsorbsTheBlast and
AShieldDroppedBetweenCommitAndArrivalDoesNotAbsorbTheBlast replace the
outcome-flags-only check with the shield state flipped between commit and
arrival.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
F1: RectDiskOverlap counted a whole square when a cell edge sat exactly r from
the disc centre (`h < yhi`, `-h > ylo`). The comparisons are inclusive now, so
the chord bounds the piece. A proximity burst on an integer centre of mass
delivered 127.32 of 100 at BlastRadius 1 and 100.80 at 3.
F2: delete Detonate's distance prefilter and the radius <= 0 guard. The
prefilter measured from the centre of mass with the bounding box's
half-diagonal, which is not a bound on how far a hull's cells reach; it culled
real overlap on an L-shaped hull and on the shipped Longinus corners.
RectDiskOverlap is the single decider. An untouched entity is still skipped
(its covered list is never allocated).
F3: Detonate iterates a snapshot of zone.Entities, as Zone.Update does. A
lethal blast removed the entity from the live collection and the next MoveNext
threw, leaving the shot pending so it detonated again the next tick.
Tests: tangent discs through Fire and Detonate, an L-shaped hull, grazing
blasts at the farthest corner of Longinus and LonginusX (expected values from
a numeric overlap), and a lethal blast that must resolve once.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TutorialEntranceWithNoLagrangeCandidateStillSeatsAStationWithFiniteNearbyTurrets
asserted a turret orbit's Distance matched its station's -- true unconditionally,
since PlaceTurret sets Distance from orbit.Distance directly the same way
CreateLagrangeOrbit does for the station itself, so the check could never
fail regardless of placement. Replaced with a bound on PlaceTurrets' actual
"near their station" claim: the Phase offset (20 * distanceMultiplier /
orbit.Distance), generous over PlaceTurrets' own max distanceMultiplier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F5: committed ThrowReachableWithAuthoredSettings (from Soul's SoulL3Census.cs)
against a real seed-1 tutorial entrance -- RosetteProbability 0 and MassFloor
1e9 leave every planet a parentless root, so even the widened candidate set
(F3's fix) is empty and the throw fires through public construction, not a
hypothetical.
F6/N1: EntranceStationSitsOnAGenuineLagrangeOrbitWheneverOneExists strengthens
the existing across-seeds test with a per-seed pin: whenever a seed's entrance
actually has a genuine (non-rosette) Lagrange candidate, its station must sit
on one, not the widened fallback.
F6/N3: MultiRootEntranceStationOrbitsAreAlwaysParentedWithPositiveDistance
authors a fragmented, heavily-rosette entrance (RosetteProbability 1, a fixed
SubZoneCount of 8) and pins that every resulting station orbit is a real,
positioned orbit -- a set Parent and a positive Distance -- regardless of
which candidate selection produced it.
F6/N9: EntranceStationPhaseDiffersFromEveryPlanetAtItsOwnDistanceAcrossSeeds
pins CreateLagrangeOrbit's own Phase offset: a docked station's Phase must
differ from every planet's Phase at the station's own (Parent, Distance),
catching a mutant that seats a station directly on its source planet's orbit
object instead of an offset copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F6/N2: added WideningDoesNotLeakToANonEntranceNoLagrangeZoneThatRollsAStation.
Seed 3's "EAC-7089" (the same no-Lagrange fixture the widened-candidate test
uses) naturally has no genuine Lagrange candidate; boosting one faction's
presence there via the same public SavedGame round trip
BuildGalaxyWithForcedEntrance uses makes its station-count roll nonzero
without touching ZoneGenerator's private RNG. Verified directly: dropping
ZoneGenerator's isTutorialEntrance guard on the widening condition turns
this fixture's station count from 0 into 2, so the fixture genuinely
exercises the leak path this test pins closed.
F7: fixed the leak test's comment (the widened-candidate fixture is seed 3,
not seed 1, so it no longer claims to share a fixture with a test that
doesn't). ForcedTutorialEntranceStationDoesNotInflateEnemyCount asserted
shipCount == 2, which was pinning TutorialZoneSettings' own default
NeutralWandererCount (2), not the enemy roll baseStationCount gates -- it
now filters ShipPacks by the same nearest-faction ZoneGenerator computes
internally (EntityPack.Faction), asserting the real enemy count (0) instead.
Also (Soul's finding): IsRosetteMember's planet-orbit count
(pack.Orbits.Count minus the OrbitalEntityPack count) over-counts whenever
a station or turret loadout generator returns null, since
CreateLagrangeOrbit already appended that orbit before the null check
skips adding the entity. It now derives the set from pack.Planets' own
Orbit refs directly (Census.cs's own approach), which sidesteps the bug
entirely.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RestoredHullsTests.Settings() hand-set thermal radiation/conduction at
their bare-class defaults (1/1/1) against the shipped 3 / 1e-8 / 0.01
(Assets/Resources/Settings.asset), so equipped parts froze toward 0 K on
real physics and every test built on that fixture ran on thermal behaviour
nobody ships. It now loads the real GameplaySettings through
AuthoredSettings (tools/AetherDb/AuthoredSettings.cs), the same headless
YAML reader tools/AetherDb already uses to inspect Settings.asset outside
Unity -- the tests project now references AetherDb.csproj to reuse it
instead of hand-copying the same fields a second time.
LonginusNeverExceedsItsRestoredTopSpeedUnderFullThrust no longer needs
OverrideShutdown to keep the ship flying (that was compensating for the
broken fixture shutting equipment down within ~1s); it now drives a
generated Longinus (seed-pinned, including ItemManager.Random, which
defaults to a DateTime.Now seed and was making item quality -- and so
the measured speed -- non-deterministic) for 5s, the window Soul measured
before this loadout's thrusters cross 386 K and shut down. The tolerance
is now asymmetric instead of the old +-10: +4 on the ceiling (the mixer's
own tick order settles real, unforced flight at the cap plus one tick's
acceleration, measured at ~103.3 for this loadout) and -3 on the floor
(tight enough to catch a nerfed TopSpeed catalog value, which +-10 or a
wider ceiling would not).
Every other test in the file now runs on real settings instead of the
frozen fixture; all 15 tests in RestoredHullsTests pass unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Geometry, absorption order, symmetry/frame, shields and fuse tests from the
cut doc's own verification list, plus Soul's three fold-in probes (a
radius-without-a-fuse damage-outcome test, a moving-target proximity-burst
pin, and Detonate's nonpositive-radius guard) and the two catalog probes
(shipped-catalog neutrality on BlastRadius/Fuse, and a WeaponFuse/BlastRadius
round-trip through a written and reopened catalog copy).
Fixture: SchematicCellSize 2, target off the origin, hull centre of mass not
an integer cell, one facing with |fx| > |fy| -- the rules the cut doc names
for every blast test. Geometry expectations come from exact half-split
symmetry where possible (TheShareOffTheHullIsLost, ShieldPaysForWhatReachesIt)
and from an independent numeric area integral otherwise (EachCellTakesItsShare
OfTheDisc, TheFusePointStaysOnMetal), never from the SUT's own overlap code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FireControl.Apply now switches on the frozen shot.Fuse: a null fuse keeps
12.3's lane path (ApplyDirectHit), a proximity fuse detonates at arrival with
no roll, and a contact or delayed fuse detonates only on a committed hit
(ApplyBlastHit), locating P through Lane and the new Reach owner (moved
verbatim from Apply's own inline penetration clip; the delayed fuse point
reads the identical rule). Step has no damage branch left.
Detonate (FireControl.cs) is the one owner of blast damage: candidates by
centre-plus-half-diagonal, exact circle/cell overlap shares computed once,
every blast's shield decided live per Q12-9 = A (including the host of a
contact/delayed blast -- Commit now gates its own shield decision on
shot.Fuse == null), armour absorbed per cell, and each item's covered cells
pooled into one ItemAbsorb call. Entity.Absorb and FireControl.Splash are
deleted; Entity gains ToSchematicPoint/ToWorldPoint for the point conversion
Detonate and the delayed fuse point need. Mine.Explode now calls Detonate.
Existing Splash/Absorb-era tests across Cut4/5/6/11/123 are rewritten or
retargeted to the new public owners; behavioural coverage for the blast
model itself lands separately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1 (speed cap): assert against Longinus's legacy TopSpeed literal (100)
instead of reading the bound back out of the same catalog value being
tested -- the old assertion passed under TopSpeed=1000 because the
bound moved with the mutation. Also fixes the false "drag/thrust
balance" comment: there is no such balance near 100. At full throttle
Longinus's own thrusters heat their hull past ThermalOnline's shutdown
threshold within ~1s, and once the hull's own cell goes offline the
same way, VelocityLimit (a hull behaviour, gated identically) shuts off
with it, so the cap was only ever exercised for the first ~15 ticks.
OverrideShutdown -- a real, first-class field (the cockpit's "Override
Shutdown" control) -- forces the hull and both firing thrusters to keep
running for the whole 12s scenario, so the cap is what actually holds
speed down throughout, not an unreached ceiling.
F2 (entrance station): EntranceZoneAlwaysGetsADockedStationAcrossSeeds
now also requires the normal case to land on a genuine (non-rosette)
Lagrange orbit. Adds TutorialEntranceWithNoLagrangeCandidateStillSeats...,
built via a public SavedGame/SavedZone round trip (no reflection) that
re-targets a real galaxy's Entrance to a real zone already known to
have no Lagrange candidate (seed 3's "EAC-7089": four planets, one
rosette, no captured satellites) -- pins a docked station whose turrets
land on a real, finite, nearby orbit. Adds the leak checks (override
must not reach other tutorial zones or a non-tutorial game's own
entrance) and the enemy-count check (the override must not also raise
the enemy roll, which stays keyed to the pre-override station count).
F3 (asset identity): EveryAssetReferenceResolvesToAMetaGuid only checks
that a referenced GUID exists somewhere under Assets/, so it passed
when Djinni's Prefab was swapped for Longinus's. The new test compares
each reference against the specific legacy file's own GUID (Djinni's
own .prefab.meta; Thruster 1.prefab.meta for every restored thruster's
ParticlesPrefab).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>