- Entity.Target is one slot of TargetRef, read-only from outside
(IReadOnlyReactiveProperty); Entity.SetTarget is its one writer. A chunk is
taken only while the entity can see it, so a rock in darkness cannot be
picked. Every handler, agent, turret and test writes through SetTarget.
- Entity.ChunkInfo (Q14 A): the value the per-tick sensor rule settles at,
computed on demand through Sensor.PassiveRate/Sensor.Gain -- sum of the
active sensors' passive rates over (sensors x TargetInfoDecay), capped at 1.
Stored nowhere; no per-tick loop over chunks. ChunkVisible is the detection
threshold through the chunk path.
- The lost-track rule reads through the chunk path: Activate's reconciliation
and a per-tick check of the one slot drop a chunk target that has gone dark.
- Entity.VisibleChunksInReach (Q13 A): visible chunks within the longest range
among active weapons -- the one reach rule for cycling and programmatic picks.
- Entity-only readers take .Entity: TargetedBy/TargetedByCount, target items,
the stance gate (a chunk has no stance), lock (Q12 A: never on a chunk),
turrets, Combat, Minion, FireControl (a chunk fires untargeted until Cut 4).
TargetRange and the arc gate read a chunk's planar position.
- ActionGameManager: the reticle and next/previous offer visible ships and
visible chunks in reach; nearest stays enemies-only; the target panel,
indicator and fire-control debug take TargetRef. EntityInstance reads .Entity.
- MiningCut3Tests pin parity, darkness, monotone brightness, wear and break,
reach, the chunk query, target survival and loss, TargetRef equality, lock,
and field-kind assignment at generation and first load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SensorGainExtractionChangesNothing drives a fixed scene (two reflecting ships at
different ranges and bearings, a ping part way through) and pins the observer's
EntityInfoGathered trace, bit for bit, by hash. The golden value is recorded
against the pre-extraction Sensor.Execute.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in Zone.cs: kept master's TryGetShot, kept Cut 1's deletion of the BeltUpdates field.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every mutant of the Cut 1-2 chunk surface (Zone.ChunkExists, ChunkRadius,
Wear, EvaluateBelt, ChunkPose, PackZone/unpack, AsteroidBelt.Pose and its
sizes, ChunkId) was run against MiningCut1Tests and MiningCut2Tests. Six
survived; five are behaviour and get a test each:
- ChunkExists is false for a body that is not a debris field (a planet) and
for a key naming no body.
- A hit on the exact respawn instant lands on the live chunk (Wear agrees
with ChunkExists and ChunkRadius about the boundary).
- A chunk worn after respawning packs with no BrokenUntil.
- ChunkPose carries the worn size, and zero for a broken chunk.
- Chunk rotation wraps at one turn (the fixture never reached one before).
The Cut 2 fixture gains a non-belt body, and its settings the three curves
that body's constructor reads. The sixth survivor, a ChunkId hash that
ignores the index, changes no observable behaviour.
Tests only; no production change.
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>
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>
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>
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>
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>
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>
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>
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>
Self's review found that commit 4e6495dd's fix for F1 replaced the per-lane
forward-arithmetic defect with a second, still-independent membership
computation: Lane's own half-open admission (Lo <= s < Hi, from a cell's own
Extent) versus Lanes' ceil((cell.Lo - centreLateral) / laneSpacing) lane
index. The two agree only when a cell's own (Hi - Lo) equals the externally
supplied laneSpacing exactly in float -- the identical two-derivations trap
Cut 12.2's own fix batch already found and removed from Lane itself,
reintroduced one layer up. Confirmed reachable: on commit 4e6495dd, solid5
b=(0,1) s=0.49999997 n=1 leaves the ceil-based centre lane EMPTY while Lane(s)
itself finds all 5 cells of the row -- breaking R3 ("the centre lane always
meets metal", Commit's own guard) and the 12.2 placement contract.
FireControl.Lanes is now built entirely out of FireControl.Lane: it calls
Lane(hull, b, centreLateral + k * laneSpacing, ...) once per lane index, so
Lane(s) is literally this function's n = 0 case rather than a parallel
re-implementation of its rule. Commit's placement and Apply's damage split
now read one function. The narrower question left -- can two individually
correct Lane() calls, at two different lines, still walk the same cell? --
is resolved by ownership (the lane closest to the centre keeps a contested
cell; the centre lane, k = 0, never loses one), not by a competing formula,
so the centre lane's final cell set is always exactly Lane(centreLateral)'s
own output.
Lane() itself is unchanged (still called directly by Commit and by
FireControlCut12Tests' edge-seed and interval-edge tests, all still green:
HitsLandOnTheFacingEdge, LaneSeamIsHalfOpen, LaneNeverEmptiesAtAnIntervalEdge,
Cut12-edge-68442709, Cut12-edge-64078070).
Adds LanesQueryIsDisjointMatchesLaneAtCentreAndNeverEmptyOnHull: now that
Lanes is a public, pure geometric query, the boundary sweep is a behavioural
test of its own contract, no reflection -- over every cell's own shadow edge
and float neighbours, at exact-axis, 45-degree, |bx| > |by| bearings and
Soul's own two reproduction cases, spread 1-2: (a) no cell is walked by two
lanes, (b) the centre lane equals a plain Lane(s) call, (c) the centre lane
is never empty when the centre lateral is inside the hull's own shadow. Hand-
confirmed to fail on 4e6495dd's ceil-based Lanes (property (b), the same
input above) and to pass on this commit (fc123fix2-mut3.log).
Adds CommittedCellIsTheFirstCellTheCentreLaneDamages: an end-to-end check
through Fire -> Commit -> Apply (damageSpread 0, a single lane) that the
committed outcome.Cell is the first cell Apply damages.
Full suite: 286 tests, identical across 3 runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F3 (medium, confirmed): NoCellIsStruckByTwoLanesAtAnAngledBearing used
penetration 0 (impact cells only) and one fixed-seed shot per bearing, at
only two bearings, so it survived R1e (one-cell spacing when |bx| > |by|),
R1h (spacing 1/max(|bx|,|by|)), R1c (spacing * 0.9), and R1b (spacing *
0.99). Its own comment also falsely claimed a plain revert "fails this at
both bearings" without ever checking a real revert.
Reworked as NoCellIsStruckByTwoLanesInOneShotAtAnyBearing: seven bearings
(exact axis, both 45-degree diagonals, |bx| > |by| and |bx| < |by| on both
sides, all within the fixture's own FiringArc), 300 hits sampled per bearing
at a wide targeting spread (so the committed Lateral's draw visits many
points along the hull's shadow), and ample penetration.
Hand-verified against fc123fix2-mut.log: reverting to one-cell spacing (R0)
and R1c/R1e/R1h are all caught by the full suite -- but not by this test.
F1's structural fix (this branch's earlier commit, FireControl.Lanes) assigns
each cell to exactly one lane index by a single computation, so a wrong
laneSpacing can misassign or drop a cell but can no longer make two lanes
claim the same one; R0/R1c/R1e/R1h are instead caught by the pre-existing
damage-shape tests (DirectHitAtAnAngledBearingMatchesAnIndependentModel,
FootprintWidensAtAnAngledBearingComparedToAxisAligned, and others). R1b (a
1% narrowing) survives the whole 97-test FireControl suite under any of
those tests, including this one, even at 5000 hits per bearing -- recorded
here as an open survivor for Soul's own mutation pass, not claimed dead.
This test's own job is now a standing regression guard on the disjointness
invariant through the real Fire/Commit/Apply pipeline, not a killer of
spacing-magnitude mutants (that job now belongs to the damage-shape tests).
F4: deletes MirrorImageShotsProduceStatisticallyMirroredHullDamage. Soul
showed it cannot fail on spacing -- it survives the full revert and every
spacing mutant, because no per-shot mirror pair is reachable through Fire
(LateralDraw's distribution reverses under mirroring, so a shot and its
"mirror" are independently drawn samples, not a matched pair). It was meant
to pin lane processing order under a shared-cell race; that question is
folded into the F2 disjointness rule (a shared cell can no longer arise) and
the pending F2 ruling on multi-cell item lane order, which remains on hold
per this fix batch's brief.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zone.Wear did not check whether the chunk was already broken. A later hit
read wear.Damage starting from 0 (the value ChunkWear carries while
BrokenUntil is in the future), stayed under hitpoints, and wrote
BrokenUntil = null -- un-breaking a chunk mid-respawn.
Damage delivered to a chunk that is currently broken (zone time <
BrokenUntil) now changes nothing: Wear returns false (this hit did not
break it -- it was already broken) and BrokenUntil, and Damage, are left
untouched. The chunk stays absent until its original respawn time, then
resumes wear from zero as before.
Adds two behavioural tests: a re-hit inside the respawn window, both
below and above hitpoints, must not un-break the chunk or move its
respawn time; and the same rule survives a save/reload round trip.
Confirmed by reverting the fix and watching both new tests fail with the
chunk reappearing early, then restoring the fix byte-exactly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MirrorImageShotsProduceStatisticallyMirroredDamage compared per-cell armour
damage SUMS between a bearing and its mirror, using heavy (1000) uniform
armour. That armour never depleted, so no cell's absorption ever competed
with another lane's, and the test could not see the one thing the operator's
ruling is about: two lanes racing for one cell's finite armour under the old
one-cell spacing. Renamed to MirrorImageShotsProduceStatisticallyMirroredHullDamage,
it now resets a small, finite armour value before every individual shot (so
the race is live on every trial) and compares aggregate HULL damage, where a
lost race's leftover actually lands.
Finding, reported rather than hidden: even with this fix, the test does not
fail under the reverted one-cell spacing. Reasoning checked against measured
behaviour: FireControl.Apply always processes lanes in a fixed index order
(li ascending, i.e. lateral increasing), never based on the bearing's sign.
Over many INDEPENDENTLY random lateral draws, a bearing and its mirror sample
statistically equivalent distributions of "which side's lane goes first" --
the fixed-index tie-break rule respects the mirror symmetry in aggregate, so
independently-drawn trials wash out the order-dependent bias that Soul's own
LaneOrderDemo probe found (that probe compares ascending vs descending
processing of the SAME committed shot, not independently-drawn mirrored
bearings). A production-level test that reliably fails under the old spacing
for this specific property would need to force an exact, deterministic
lateral for two geometrically mirrored shots, which is not reachable through
Fire/Commit without reflection (Commit's lateral draw is randomised, and
Apply is private). NoCellIsStruckByTwoLanesAtAnAngledBearing and
FootprintWidensAtAnAngledBearingComparedToAxisAligned remain the two tests
that actually discriminate the ruling behaviourally (confirmed failing under
a hand-reverted one-cell spacing). The mirror test stays in the suite as a
true, currently-passing invariant and useful regression coverage, not as a
defense of this specific ruling.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F1: no existing test through Fire commits at a non-axis bearing.
DirectHitAtAnAngledBearingMatchesAnIndependentModel checks every cell's
armour, every item's durability and the hull's own total against an
independent double-precision ray/box model (ported from the Soul probe that
verified this cut's arithmetic to 14,984/15,000 shots), at 45 degrees, a
shallow angle and a bearing with |bx| > |by|.
DiagonalDirectHitReachesEveryCornerCellInOrderThroughFireCommitApply pins the
spec's own diagonal case (Probe1's corner-crossing lane) through the full
Fire -> Commit -> Apply path rather than a direct Lane call: the lane reaches
(0,0), then (1,1), then (2,2), in that order (a bearing this close to 45
degrees also admits the diagonal's row/column neighbours, so the walked lane
is usually five cells, not three -- the spec only requires the three corner
cells present and in order, the same standard the existing direct-Lane test
already pins).
F2 (operator ruling, 2026-09-25: spread lanes should diffuse sideways rather
than double up): three behavioural tests, deliberately not asserting the
spacing value itself (operator: "test behavior, not shape").
NoCellIsStruckByTwoLanesAtAnAngledBearing fires at 45 degrees and at a bearing
with |bx| > |by|, spread 2, heavy uniform armour, and checks no cell fires
ArmorDamage twice. MirrorImageShotsProduceStatisticallyMirroredDamage compares
300 hits at a bearing against 300 at its mirror image, and asserts the
per-cell damage totals are mirrored within tolerance.
FootprintWidensAtAnAngledBearingComparedToAxisAligned compares the lateral
span of hit cells for the same spread at 45 degrees against axis-aligned, and
asserts the angled footprint is wider.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>