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>
Re-anchored to the post-fire-control-12 Body (synthetic merge tested on
Yggdrasil: 379/364/381 green). Codex's map is raw material; this is the
proposal. Operator questions open.
Co-Authored-By: Claude Opus 5.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>
F3: the throw fired on selectedStationOrbits.Length == 0, which is also true
whenever stationCount rolls 0 -- an unrelated variable. It now checks
lagrangeCandidates.Length == 0 directly, the actual "no candidate orbit"
case, and the message says so.
F4: CreateLagrangeOrbit's FixedPosition copy and its comment described a
fallback orbit that no longer exists (widening always returns a real,
parented planet orbit). Deleted; nothing reads FixedPosition off a
lagrange/turret orbit.
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>
Two writers decided the tutorial entrance's station orbit: the floor
(stationCount < 1 -> 1) and a synthesized fallback orbit, each masking
bugs in the other (removing either alone still passed the entrance
test). Collapse to one rule: the floor still guarantees stationCount
>= 1, but its orbit always comes from the same Lagrange selection. When
that selection has no candidate (a zone that is entirely one rosette),
widen it to every parented planet orbit ordinary placement draws from
before the rosette filter, instead of inventing an unparented,
zero-distance orbit -- which gave PlaceTurret's phase a division by
zero and left an orphan orbit record. If even the widened set is empty,
throw loudly instead of silently omitting the entrance's station.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WeaponItemData.AirburstRange (slot 29) becomes BlastRadius, same slot and
type so CultCache opens the catalog as compatible drift. A new nullable
slot 32 (30 and 31 belong to EquippableItemData) carries WeaponFuse?
Fuse. FireControl.Fire now decides "detonates" once, from BlastRadius
and Fuse directly, and freezes Fuse only when BlastRadius > 0 -- the
WeaponModifiers.Airburst label no longer decides anything (Q12-6).
PendingShot.BurstRadius is renamed BlastRadius and gains a Fuse field
that nothing reads yet; Step's Splash/Apply dispatch is unchanged.
No shipped weapon carries a blast radius or the Airburst label (Cut 12's
Probe4, reconfirmed against GameData/Aetheria.cc), so this is
behaviour-neutral on shipped content. Apply's fuse switch and deleting
Splash are Cut 12.4(b).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Imagination pass: every anchor is re-probed; a disc runs armour per cell, then pools
each item through ItemAbsorb; Entity.Absorb dies with Splash; the penetrator's
fuse point reads the same Reach owner as the direct-hit walk; the ledger now
carries actuals for 12.1-12.3; Q12-9 (the host's shield for a blast) is opened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FixedPointItemResolution now throws "the model never meets a cycle" if a pool
is still open after its fixed point settles, instead of silently
force-settling in the reverse of first-encountered order. The sweep already
gates its own draws (LanesReaching) so a genuine cross-item cycle is never
actually presented to the model; a thrown exception here means the gating
missed a real case, not a numbers question the model should answer with its
own competing tie-break. The stale "opposite tie-break" comment describing
the deleted behaviour is gone with it.
Three of Soul's five probes (soul125-FireControlCut123Tests.probe.cs) are
committed here, ported from reflection onto the public Fire/Commit/Apply path
this file's own tests already use -- FireCentre/FireUntilHit retries until
the committed hit lands on the expected column, the same idiom
ZeroDepositDoesNotStall and PooledPathHonoursTheItemThresholdLikeTheSoloPath
already use, never a direct call into the private Apply:
- ZeroDepositContributorFiresNoItemDamageEvent (P3): a zero-deposit
contributor must not fire a phantom ItemDamage event. Kills Entity.cs's
`incoming[i] > 0f` boundary flipped to `>=`, and a swap that reports the
post-split share instead of the incoming one (F2).
- ClippedLaneDoesNotHoldThePool (P4): an item beyond every lane's own
penetration clip must never be waited on.
- OwnRepeatVisitDoesNotHoldThePool (P5): a lane's own later visit to an item
must not hold that item's pool open -- only other lanes are future
reachers. Kills FireControl.cs's `alreadyContributed` guard being deleted
(F1).
The other two probes (Soul5_AngledMirrorAndConservationWithSharedItems,
Soul5_AngledMirrorCycleShapes) are NOT committed. They drive Apply directly
through reflection with a hand-built PendingShot.Outcome so they can sweep
1400 exact, mirrored (bearing, lateral) pairs per test and compare against an
independent model -- Commit's own bearing is reachable through the public
path (shooter/target positioning), but its lateral is a random draw from the
per-shot RNG, not a value either test can set or predict without either
control of that RNG's seed and Silhouette's exact sigma, or a decoy Apply
entry point. Reaching them without reflection needs one of:
(a) `[InternalsVisibleTo]` for Aetheria.Shared.Tests on FireControl.Apply, or
(b) a public/internal test seam that takes a pre-built ShotOutcome (bearing,
lateral) and runs Apply's own walk against it.
Neither is authorized by this fix batch's map; this is reported rather than
silently reflected. The Soul5_Alloc benchmark (also reflection-based, and not
one of P1-P5) is likewise left uncommitted.
Mutation verification (docs/fire-control-cut.md, Cut 12.3, one-apply-path):
F1 and both F2 mutants confirmed killed by the new tests; the earlier
fix batch's own forced-sequential, resolve-one-pool-only, and cycle-throws
mutants (Soul's M3/M4/M6 family) still die against CycleTieBreakIsMirrorSymmetric
and friends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entity.ItemAbsorb's `item == null` guard is unreachable: both callers (Absorb,
and FireControl.ApplyPooled's Resolve) already guarantee a non-null item
before calling in. Its `total > 0f ? ... : 0f` divide guard is unreachable
too -- the function already returned above whenever total <= 0.1f, so the
divide never sees zero. Both deleted; the surrounding comment now says why
instead of describing a guard that no longer exists.
ArmorAbsorb's own comment was stale from before the "one code path" ruling:
it described the item phase as deferred only for a cell shared with another
lane, when every occupied cell now defers to ItemAbsorb's pool, a lane that
turns out to be the sole contributor included (a pool of one, not a separate
solo path).
Soul's findings (docs/fire-control-cut.md, Cut 12.3): F4, F6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ApplyPooled.Resolve no longer calls FinishLane directly -- AdvanceLane is now
the single place that checks pointer >= walked and finishes a lane, so lane
completion has one writer instead of two. StillReachableByAnotherLane's own
`blocked ? pointer+1 : pointer` collapses to plain `pointer[li2]`: the
alreadyContributed check just above it already excludes a lane that is
blocked on the very item being asked about, so `pointer[li2]` is never that
item's own cell by the time the +1 would have mattered.
Soul's findings (docs/fire-control-cut.md, Cut 12.3): F4.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four tests against the real shipped catalog and the tutorial construction path, each pinning one
of this campaign's rules:
- EveryAssetReferenceResolvesToAMetaGuid: every hull's Prefab, every ThrusterData.ParticlesPrefab,
and every non-empty Schematic names a GUID an Assets/**/*.meta actually carries. Adapted from
Soul's own probe (AssetGuidsResolve); it is what caught Djinni's fabricated Prefab GUID and the
five restored thrusters' null ParticlesPrefab.
- EntranceZoneAlwaysGetsADockedStationAcrossSeeds: replaces
TutorialZoneGenerationCanProduceADockedStation, which never used the tutorial construction path
and only claimed a docking bay shows up somewhere in 25 zone-name variants. This builds the real
tutorial Galaxy with Assets/Resources/Settings.asset's own values and generates the Entrance zone
the way ActionGameManager.StartGame/PopulateLevel does, requiring a docked station in every run
that gets that far. Galaxy zone-graph generation has its own pre-existing, seed-dependent
flakiness (a disconnected zone graph throws inside the Galaxy constructor on roughly 1 seed in
15) confirmed unrelated to this cut by reproducing identically against the pre-fix-batch catalog;
that failure belongs to Galaxy.cs and is skipped rather than asserted on, with a floor requiring
at least 15 of 25 seeds to actually produce a Galaxy.
- LonginusNeverExceedsItsRestoredTopSpeedUnderFullThrust: straight-line full-thrust run against the
restored VelocityLimitData cap (100).
- LoadoutGeneratorFillsEveryThrusterHardpointAcrossSeeds: LoadoutGenerator, filtered to each
restored hull, fills every thruster hardpoint across ten seeds.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator, 2026-09-25 (docs/locomotion-cut.md): the tutorial's entrance zone always gets a
generated faction station with a docking bay, deterministically. Structurally, three defects made
that false on every seed: Galaxy.cs halves influence and picks the lowest-influence unowned zone
for Entrance, so stationCount rolled 0; a Lagrange orbit is only available when a planet has an
orbiting sibling, which a boring or empty system never has; and a null station loadout silently
skipped the slot.
GenerateZone now identifies the entrance zone by reference against the galaxy parameter it already
takes (isTutorial && galaxyZone == galaxy.Entrance), never by name:
- stationCount is floored to 1 for the entrance zone, after the roll that produced it.
- When no Lagrange orbit exists, a fallback orbit fixed at half the zone radius from centre is used
instead of leaving selectedStationOrbits empty. CreateLagrangeOrbit now carries FixedPosition
through so a Parent-less base orbit (the fallback) still positions correctly.
- A null station loadout throws for the entrance zone instead of continuing past it.
- The enemy count is based on the pre-override station count, so forcing the entrance's station
doesn't also hand it a bonus enemy.
The story-station path (Locations/LocationStory) is not used: it isn't saved, and
EntitySerializer.cs:77 would break Continue, per the map's own ruling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same pattern as restore-hulls itself: the one-shot command's carrying cost is
zero once it has run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Landed via `dotnet run --project tools/AetherDb -- fix-loco1 apply`, then deleted the command in
the same batch (Cut 9/Cut 1 precedent). Fixes, each against the decoded 2021-04-14 legacy record
or a named catalog precedent:
- Djinni.Prefab was a fabricated/truncated GUID; corrected to Djinni.prefab.meta's own GUID.
- All five restored thrusters' ParticlesPrefab (null) -> Thruster 1.prefab's GUID, the same one
Large Drive uses (legacy record names the same prefab path for all of them).
- All five thrusters' invented OptimalTemperature/PlateauWidth (300/100) -> Large Drive's own
200-400 precedent, 251/24 (docs/stats-power-cut1-migration.md).
- ThermalResilience: Medium Drive/Small Drive -> 10, Victoire/Talaria/RevvITup 2.0 -> 15 (legacy
record). Victoire's Durability 100 -> 30 (legacy). Victoire and Medium Drive's legacy
descriptions restored (Talaria/RevvITup 2.0's legacy descriptions are the literal placeholder
"[TODO]" and are not).
- Djinni.Schematic (null) -> djinni_schematic_transparent.png's GUID, already in the tree, unused.
- Djinni's OptimalTemperature 250 -> 251, correcting a rounding slip in restore-hulls's own
"apply LonginusX's optimal-fraction to Djinni's band" derivation ((275.15-173.15)/400 = .255 of
band applied to Djinni's 200-unit band rounds to 251, not 250).
- All seven restored records (Longinus, Djinni, five thrusters) re-keyed from freshly minted GUIDs
to their own legacy GUIDs, decoded directly from GameData/Legacy/AetherDB.2021-04-14.msgpack,
matching the Cut 9 import precedent (tools/AetherDb/Import.cs, git history). Each record's own
FactionProductData.Design ref repointed at the new key.
Verified: dry run then apply, second apply refused (Longinus already carries its legacy key),
`dangling` reports 0, `census` still names both hulls, `loadout` generates every hull cleanly
across seeds 1-5.
Re-keying changed GetAll<GearData> enumeration order, which
RestoredHullMovesAndTurnsUnderMovementAndLookDirection's design-selection (thrusterDesigns.First())
depended on implicitly: it started landing every Djinni thruster hardpoint on Victoire, the one
restored thruster with a real EnergyUsage draw, and the fixture ship carries no reactor. Fixed by
equipping the fixture ship's own Reactor hardpoint, which every restored hull actually has, instead
of relying on incidental unpowered-item ordering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the shared-item model sweep instead of leaving it a follow-up:
the earlier ModelWithSharedItems copied FireControl.Apply's own
survey-then-walk shape closely enough that reverting production to a
plain sequential per-lane absorb (no pooling at all) still passed it.
FixedPointItemResolution settles items in a different order from
production: it batches every lane's own armour-only walk into full
passes, then checks every currently open pool for readiness once per
whole pass (a dynamic, re-derived-each-time check, not a table built
once up front -- a precomputed-once version deadlocked on a non-convex
item crossed twice by the same lane). Lane geometry is read from the
real FireControl.Lanes instead of a second hand-rolled walk, removing
a class of disagreement that had nothing to do with absorption order.
Durabilities are now drawn below a lane's own share so every shared hit
overflows, hull damage is compared alongside armour and items, and the
tolerance is tightened now that lane geometry can no longer drift.
Also fixes: rejected attempts in the retry loop were mutating real
state without resetting it, corrupting the "before" snapshot; the
model was missing Entity.ItemAbsorb's own pooled .1f threshold.
Reran M0 (control, green) through M4: M1 (forced sequential absorb)
now kills the sweep as well as the mirror/proportional tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Cut 1 restore-hulls command deletion also dropped a blank line separating
FitCommand from the dangling-refs command block. Restore it so the file is
byte-identical to base (git diff against pre-Cut-1 now empty).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commit Soul's probes as behaviour tests against the pooled walk:
CycleTieBreakIsMirrorSymmetric (a genuine two-item cyclic dependency,
mirror-symmetric and damage-conserving by construction),
PooledPathHonoursTheItemThresholdLikeTheSoloPath (the .1f threshold is
decided once on the pool total, never per contribution, and pins the
pooled ItemDamage event count), and ZeroDepositDoesNotStall (a lane
whose armour ate its whole share still deposits and never blocks the
pool it belongs to). Adds the Custom fixture hook (an arbitrary named
item shape) the cycle test needs to build two crossing non-convex items.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the survey-then-branch (ApplySequential vs. ApplyWithSharedItems)
with a single pooled walk. Lanes are clipped to penetration once, right
after Lanes returns; every item -- single-cell or multi-cell -- pools
whatever lanes reach it and resolves the moment no other unfinished lane
can still reach it down its own remaining clipped walk, with no separate
reachability survey. A cyclic dependency between two non-convex items
resolves every currently open pool at once, order-independent by
construction. Entity.ItemAbsorb becomes the one absorption function,
gated on the pooled total; Absorb (Splash's only remaining caller) now
funnels through it with a one-contribution pool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Same contract Cut 10 applied to the Cut 9 importer: a one-shot restore
carries zero ongoing cost once its records are in the catalog. The
authored-choices review list this command printed lives in the cut's
report, not in source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read-only against GameData/Aetheria.cc: each hull exists with the
expected thruster hardpoint count (Longinus 4, Djinni 8), every thruster
hardpoint accepts a catalog thruster design through TryEquip at its own
position, and each hull moves and turns for a few ticks under
MovementDirection/LookDirection on the pre-Cut-3 mixer -- content proof,
not controller proof.
Also covers the operator's tutorial-station addition (2026-09-25):
Zenith (the live Station hull) is confirmed working already -- station-fit,
hardpoint-fit and `loadout` all pass for it, and the legacy record's only
station design ("Zenith Station") is that same hull, so nothing needed
restoring. ZoneGenerator's story-station path is dead code today (Galaxy's
tutorial constructor has its StoryProcessor call commented out), so
TutorialZoneGenerationCanProduceADockedStation drives the background
station path directly (real ZoneGenerator.GenerateZone call, real
catalog, a minimal SavedGame/Galaxy fixture) across 25 zone/seed
attempts and asserts at least one produces a station with a docking bay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Landed via `dotnet run --project tools/AetherDb -- restore-hulls apply`.
Longinus (13 hardpoints, Alakrita) and Djinni (22 hardpoints, Rossum &
Douglas), plus Medium Drive, Small Drive, Victoire, Talaria and RevvITup
2.0 (approved 2026-09-22), each with a FactionProductData. Verified:
census names both hulls with makers, dangling reports nothing new,
LoadoutGenerator/`loadout` generates every hull in the catalog cleanly
across seeds 1-5, second apply refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One-shot restore of Longinus, Djinni and their five thruster designs from
the decoded 2021-04-14 legacy record, in the shape of the existing
*-migrate [apply] commands: dry run by default, writes every record in one
Commit, refuses a second apply. Longinus's scalar fields and ReflectorData
come from LonginusX (confirmed byte-identical against the decoded legacy
record); its hardpoint layout is authored directly from the legacy decode,
since LonginusX's own interior was rearranged to fit its AetherDrive.
Djinni is authored entirely from the decode (no live twin exists).
Every legacy PerformanceStat's [Min, Max, HeatExp, DurabilityExp,
QualityExp] is translated into today's Min/Max/Terms shape, never copied.
Authored post-breach fields (roles, FactionProductData, PowerSupply terms,
FiringArc) are covered in the command's own header comment for operator
review. Deleted in a later commit of this same cut once landed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F8 tests, through the real Fire/Commit/Apply pipeline (no reflection):
- ProportionalAbsorptionSplitsTheLeftoverBetweenTheLanes /
...LeavesNothingWhenTheItemAbsorbsEverything: the ruling's own literal case
(two lanes bringing D1=25/D2=15 onto one 2x1 item), both the partial- and
full-absorption branches.
- MirrorSymmetryHoldsWithASharedMultiCellItem: a symmetric hull and its own
mirror-image fixture must produce the same sorted leftover pair. Fails on
the pre-fix-batch code (commit d1cc047a) at itemDurability 20 ({0,20} vs
{5,15}); a durability equal to one of the two D values turned out
degenerate and was rejected first (see the test's own comment).
- EveryAxisAlignedSpreadShotStrikesExactlyTwoNPlusOneFacingCells: Soul's own
owed regression guard on lane spacing, independent of item sharing. Killed
by hand-mutating FireControl.cs's laneSpacing by *.99f (R1b): 28/400
interior-column hits drop a facing column.
- ModelSweepAgreesWithProductionIncludingSharedMultiCellItems: an independent
model (armour-then-item-then-hull per lane, extended with the same
survey/sum-then-split shape production uses) checked against production
over a sweep of two hull sizes, three bearings and both 2x1 bars and
non-convex L-shaped items (Build gains lBars/LShape for this).
Tool-hardpoint items (Bar, Marker) can only occupy interior cells
(Entity.ItemFits), so every fixture above needed a hull at least 3-4 cells
deep -- shallower hulls (used in an earlier draft) fail marker/bar equip
outright.
Hand-verified against the M13 "lane_order_reversed" mutant Soul recorded as
surviving on the pre-fix code (FireControl.cs, Lanes' own `var k = li - n`):
MirrorSymmetryHoldsWithASharedMultiCellItem fails on the pre-fix code under
that mutant and passes on this cut's code under the same mutant, confirming
the new resolution is order-independent of which physical lane a buffer index
happens to hold, not only of Apply's own loop order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Operator ruling (2026-09-25): "proportional absorption; go". A multi-cell item
struck by several lanes of one shot now absorbs their combined incoming damage
(up to its own remaining durability) and each lane's leftover is in proportion
to what it brought, replacing the old behaviour where whichever lane Apply
happened to visit first drained the item and the next lane inherited whatever
was left.
Entity.Absorb is split into ArmorAbsorb (per-cell, never shared) and ItemAbsorb
(the single-lane path), so FireControl.Apply can call armour immediately and
defer item resolution only for a cell whose item is reachable by more than one
lane this shot. FireControl.Apply gains a geometric survey (which items are
reachable by >1 lane, decided before any damage is applied) and takes the
untouched sequential walk (ApplySequential) whenever nothing is shared, or a
new coordinated walk (ApplyWithSharedItems) that deposits each lane's
post-armour remainder into a shared item's pool and resolves it once every
contributing lane has arrived, splitting the leftover proportionally
(sum-then-split, order-independent by construction). A deterministic tie-break
(smallest first-contact Entry) breaks the rare "opposite order" deadlock two
non-convex shared items can create between two lanes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>