Commit graph Aetheria/tests
Author SHA1 Message Date
Meta Vens
d0782a7d14 Every new run is a scenario: the script harness, the tutorial and main galaxies as scenarios, the New Game submenu and six test arenas
A Scenario is an abstract C# class compiled with the game: Generate picks the galaxy through GalaxyStage (Main or
Prelude, at the scenario's seed, the background noise drawn from the same seed), Stage places the run's first
entities through ScenarioStage (Fit, Bare, Generated, Preset, Player, Place, Cargo), which collects named failures
and admits nothing. RunStart generates the galaxy, the arena under Ambient and admits everything through Zone.Admit,
all or nothing. The aetheria.scenario record, ScenarioShip, ScenarioEntity, RunStart.Check and the null-scenario
branch are cut.

New Game opens a submenu: Tutorial Galaxy and Main Galaxy, then (editor and development builds) Djinni Shakedown,
Starved Reactor, Arcs, Duel, Launcher Angles and Long Haul, each with its brief. MainMenu.Launch is the one way into a
new run; ActionGameManager.PendingScenario carries the scenario to StartGame once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:33:55 +02:00
Meta Vens
8a0a29e0de Galaxy.IsPrelude is the one owner of whether a run is the prelude: ActionGameManager.IsTutorial and PlayerSettings.TutorialPassed are cut
IsTutorial duplicated Galaxy.IsPrelude; its readers read CurrentGalaxy.IsPrelude, and RunSave.Capture writes
SavedGame.IsTutorial from galaxy.IsPrelude with no isTutorial argument. TutorialPassed was never written and its only
reader was New Game's unreachable main-galaxy branch, which goes with it (S2 brings the main galaxy back as a
scenario). spawnturret's two identical generator branches collapse to one. Old player.cc files still load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 20:26:30 +02:00
Meta Vens
aca1d53e6f ship_cc: replace_visual writes ShipAuthoring's model asset and anchors, pinned to the C# keys
MODEL_ASSET_SLOT, ANCHORS_SLOT and ANCHOR_MEMBERS sit beside the other slot constants and ShipSchemaPinTests binds them
to ShipAuthoring's and ShipAnchor's [Key]s. replace_visual carries an anchor's slots past Order by Id, refuses a blank
or duplicate id, a wrong-length row and a changed ship id, and leaves the hull record and the lines alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:07:10 +02:00
Meta Vens
f98be434f0 AetherDb ship-authoring: create --like a shipped hull, and validate reads the GLB when present
ShipAuthoringStore.HullLike copies a reference hull through its MessagePack round trip and resets Name, Shape,
Hardpoints, Prefab, Schematic and Visual. 'create ... --like <name>' opens the shipped catalog read-only and refuses an
unknown or non-ship hull. 'validate' runs ReadPackage when ModelAsset names an existing file and says which it judged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:07:09 +02:00
Meta Vens
f5743cbad2 Ship packages: a thruster or radiator anchor on a mesh-less GLB node is refused at bind
ReadNodeIds is the one GLB parse and now records, per aetheria.id node, whether it carries a mesh. Bind refuses a
thruster-emitter or radiator-mesh anchor whose node has none, naming the ship and anchor (ruling
thrusters-radiators-are-meshes), so Compose and validate refuse such a package before the catalog sees it. AetherDb
'ship-authoring validate' judges the GLB when it exists (next commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:07:08 +02:00
Meta Vens
ed15d34b35 Tests: generated loadouts give every reader its hardpoint (Soul's headless probe)
Soul's scenarios-adopt probe S4 as a committed test: on generated ships,
stations and turrets every weapon, thruster and radiator holds a non-null
Hardpoint containing all its cells, and Tool gear and the hull hold none.
It pins the input the muzzle, arc, thruster-emitter and radiator readers
dereference (finding headless-reader-input).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 16:14:10 +02:00
Meta Vens
cfd9884a33 Merge master into eureka/aetheria-release-scenarios: take the ship-mounts lane
The one conflict is EntityInstance's SoundBank block, which this branch had
moved onto item.Hardpoint and ship-mounts deleted: it stays deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:41:54 +02:00
Meta Vens
9787df6f4b Tests: a full hardpoint offers no space (Stryker survivor at TryFindSpace's last return)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:35:51 +02:00
Meta Vens
bba169cac5 Tests: an equipped item knows its hardpoint (Soul's P30 fixtures)
Soul's scenarios batch-4 probe P30 lived only in a session scratchpad. Its
fixtures are now tests: an L gun in an L hardpoint whose origin lies outside
it, an edge gun whose origin lies in a neighbouring radiator hardpoint,
TryFindSpace finding what ItemFits accepts (unrotated and turned), reload into
the same hardpoint, and Tool gear holding none. Three batch-4 minors are
pinned too: IsFilledBy against a cell oracle written in the test, the last-row
placement search, and billing independent of heater equip order.

The file is a part of RunStartTests, so it shares that fixture's scratch-hull
and catalog helpers instead of a second copy of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 15:00:00 +02:00
Meta Vens
80de88f267 Ship mounts: each weapon needs a muzzle of its own
Kills the Stryker survivor that armed every weapon with any muzzle on the ship
(the && in the per-weapon muzzle check turned into ||): a second weapon mount
with no muzzle is refused even when the first has one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:54:03 +02:00
Meta Vens
2d39c83504 Merge master into eureka/aetheria-release-scenarios: adopt the scenarios lane onto master's base
The one conflict is AetheriaStores.CatalogTypes: keep ShipAuthoring from the
ships lane and Scenario from the scenarios lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:53:29 +02:00
Meta Vens
976b711905 Ship mounts: anchors only for visible hardpoints, every role rule in Validate
ShipAuthoringStore.Validate now alone decides which hardpoints carry anchors
and which role each plays. A thruster, radiator, energy, ballistic or launcher
hardpoint needs an anchor of its mount id with role thruster-emitter,
radiator-mesh or the new weapon-mount; every other hardpoint is internal and
no anchor may take its id. A mount-role anchor must be a hardpoint's mount, a
muzzle must hang off a weapon mount, and each weapon needs a muzzle; these
checks move out of ShipModPlan.Build, which now only maps a validated pair to
slots and throws nothing itself. 'articulation' stays in the vocabulary for
pivots and is never a mount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:39:08 +02:00
Meta Vens
23976aada7 Ship mounts: delete EquipmentHardpoints and the plan's Equipment list
The array's only reader was the dead SoundBank block in EntityInstance, which
looked up a hardpoint transform and did nothing with it. ShipModPlan.Equipment,
ShipModShips' assignment, the preview check, the FBX builder's HP.Equipment
case and its smoke assertion all fed that one dead reader. An HP.Equipment node
is now refused by the builder as an unknown hardpoint kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:34:05 +02:00
Meta Vens
2d30403ab2 Tests: pin generation's fill preference, lot reuse and thermostat billing
Soul's S6. Generation always gives Zenith's 16-cell Reactor hardpoint a reactor
that fills it though smaller ones fit; a generated Djinni's eight thrusters
share one lot; across 5 K either side of a heater's target the bus bills it
exactly when its thermostat is open; and a thermostat gates only its own
behaviour group (a two-group fixture: the closed group's 10 is not billed, the
other group's 7 is).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:13:38 +02:00
Meta Vens
ff2be8e26f One placement rule for hardpoint gear
Soul's S3 and the S2 ruling ("B is the design intent, saving slots in a
hardpoint can be a valid tradeoff for crowded ships if you really need to fit
an extra tool"). Entity.HardpointAt is the one placement rule: at an origin,
the item lies within a hardpoint of its type at that hardpoint's rotation
(HardpointData.TakesAt), its own cells are free, and that hardpoint holds no
other hardpoint item. The origin no longer has to be a hardpoint cell, and
TryFindSpace tries every offset inside a hardpoint, the offsets the fit rule
tries, so an L-shaped hardpoint takes its L design instead of generation
throwing. Leftover hardpoint cells may hold general gear, and a hardpoint still
holds one hardpoint item.

Tests: an L hardpoint takes its L design by hand and in generation (Soul's
P11); a turret's gun goes back beside the tool gear in its leftover cells
(P21); a second gun in a roomy hardpoint is refused; a saved entity reloads to
the same loadout with its items in reverse order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:13:37 +02:00
Meta Vens
d50fd1b973 A faction reaches its own gear; allegiance names only other factions
Operator ruling, 2026-09-30: "removing the 11 self-entries was my intention".
IsAvailable, the one reach rule, now says a faction always reaches its own
manufacturer's gear, and a transient `AetherDb allegiance-self [apply]` drops
the self-entries from the eleven factions that had one (Rossum & Douglas never
did, which is why its stations could not carry its own gear).

Tests: in a main-sector galaxy every faction is offered all it makes and no
allegiance map names its own faction; and, through a fixture faction outside
the galaxy with no self-entry, its own product is on offer to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:13:34 +02:00
Meta Vens
f458206c76 Content: a station reactor for every reactor maker
Operator ruling, 2026-09-30: "Keep the variety, not every galaxy will have
Zhestokost in it." A transient `AetherDb station-reactors [apply]` gives each
manufacturer that makes reactors a design for Zenith's 16-cell Reactor
hardpoint, scaled by cells from its largest reactor with every other value,
stat term, role, temperature and sound bank its own, and a product by it:

  Vulcan Station Reactor     (AU, from Vulcan 12 cells x1.333)  charge 66.7..266.7, mass 666.7, price 1,550,000
  Notorious Station Reactor  (Lucent, from Notorious 9 x1.778)  charge 711.1..2133.3, mass 1333.3, price 1,800,000
  MoveOnPro Station Reactor  (NiteLife, from MoveOnPro 4 x4)    charge 100..500, mass 1200, price 3,600,000
  Core Power Station Reactor (R&D, from Core Power 4 x4)        charge 400, mass 200, price 1,000,000
  products: Vulcan Station, Notorious Station, MoveOnPro Station, Steadfast Station

The Manhattan-derived one is renamed "Manhattan Station Reactor" (product
"Manhattan Station").

Reach is read through the one rule, LoadoutGenerator.IsAvailable. Test: a
faction allied only to AU, Lightsail and NiteLife is offered none of
Zhestokost's products and AU's, its stations carry nothing it cannot reach and
nothing by Zhestokost, and they are powered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:13:31 +02:00
Meta Vens
09050ae920 Tests: drop the flaky failed-belt-task test
AFailedBeltTaskSurfacesOnce swapped a belt's asteroids while that belt's task
could still be running, so it failed in 5 of 8 runs including unmutated ones
(Soul S1). It tests belt-task machinery that codex/mining deletes (Cut 1,
22a54ccb), so it goes rather than being reworked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 23:13:27 +02:00
Meta Vens
c4af127280 Stations carry only gear their faction can reach
Operator ruling B, 2026-09-30: "a station with no access to Zhestokost gear
should not have Zhestokost gear." Test: in a main-sector galaxy (not the
prelude, which offers every product), every faction's generated stations carry
a heater and a reactor, and nothing, equipped or stocked, whose manufacturer is
outside the galaxy or the faction's allegiance.

With today's catalog every faction's allegiance names every faction (weights
only bias preference) except Rossum & Douglas, which leaves out itself; the
test killed a reach that ignores allegiance through an R&D station carrying
R&D reactors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:46:32 +02:00
Meta Vens
f879527158 Zone: every writer of what the belt tasks read settles them first
Soul's F5 and F6. The asteroid-belt tasks read the clock, the orbits and each
belt's damage and respawn state. Update advanced the clock before waiting on
last tick's tasks (Soul measured 242k transforms placed for the next tick in
576 of 3000 ticks), MineAsteroid wrote damage and respawn state under a
running task, CreateOrbit and AddOrbit wrote the orbits, and a task that threw
stayed in the list, so Wait rethrew it on every tick after.

SettleBelts waits for the tasks and clears the list even when one failed; Update
settles before it advances the clock, and MineAsteroid, CreateOrbit and
AddOrbit settle before they write.

Tests: over 3000 ticks every asteroid of a belt is placed for the time of the
tick that started its task; mining every asteroid of a belt, chipping and
breaking each four times a tick for 1000 ticks, never races the task; a task
that fails surfaces once and later ticks run clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:46:31 +02:00
Meta Vens
74d2cf8fc4 Power bus: a consumer its thermostat has closed is not billed
The bus billed every active IPowerConsumer, but a behaviour group stops at its
Thermotoggle: a heater above its target never runs its EnergyDraw, yet its
full draw (1000-2500/s) was charged every tick. On an idle station that kept
the reactor in permanent deficit, overloading at overload efficiency. The bus
now skips a consumer whose group has a closed thermostat ahead of it, the same
rule it already applies to inactive items; Thermotoggle.Open is the one
statement of when a thermostat is open, and Execute returns it.

Tests (Soul's F2; S17 survived before): from a hull at one uniform temperature,
one tick below the heater's target raises its cells and one above does not, so
the heater's own contribution is measured with no reactor heat in it; and the
bus bills the heater's draw below its target and not above.

This does not keep an idle station warm (Soul's F1, reported separately): the
reactor's throttled idle heat alone still drives it past its plateau.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:46:30 +02:00
Meta Vens
072a56f0b4 One loose fit rule for hardpoint gear
Operator ruling, 2026-09-30: "Hardpoint fit is loose: nobody's gonna stop you
from putting a small reactor in a large reactor's hardpoint." There is one rule,
HardpointData.Takes: gear of the hardpoint's type fits when its shape, at the
hardpoint's rotation, lies within the hardpoint's cells. TakesAt is the same
rule at one offset, and Entity.ItemFits (equipping, save load, presets) now
asks it instead of repeating the containment test. Generation's hardpoint
picks, HasHome (so station stock and loadouts), product reuse and `AetherDb
hardpoint-fit` all read Takes.

Generation keeps its own placement preference, named IsFilledBy and not a fit
rule: it offers gear that fills the hardpoint when any is on offer and falls
back to anything that fits only when none is, so loadouts change only where a
hardpoint used to stay empty. RestoredHullsTests pick fillers through
IsFilledBy.

Consequence: the Autocannon, Vulcan, scorched void policy, Notorious, pretty
pretty bang bang, Manhattan, plight, DeathCluster, Small Drive and pswarm now
have a home and are offered. Still homeless: CShot RainbowLite Lazer,
ChargeBlast SG, ChargeBlast+-, OK Disperser.

Tests: generation offers exactly the sold hardpoint gear some hull fits; a 7x7
design no hull fits is offered only once a hull that takes it exists; a 5x5
Reactor hardpoint nothing fills still gets a reactor; equipping and the rule
agree for every hull, hardpoint and design; the homeless list is stated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:46:28 +02:00
Meta Vens
87fae6e4f0 Content: damage curves for the five sold weapons that had none
Soul's F4: LRMM72, SRMM72, Autocannon, pswarm and plight had DamageCurve ==
null, and once they had products NPCs were generated with them and
CombatState.SampleDps threw at InstantWeapon.cs:64 / ChargedWeapon.cs:81.
A transient `AetherDb damage-curves [apply]` copies each one's curve from its
nearest comparable weapon, same class first, then closest range and damage:

  Autocannon <- DeathCluster (auto-fire Ballistic, 4 cells, range reaching 1000)
  LRMM72     <- GT 3K (the only other unguided launcher)
  SRMM72     <- scorched void policy (guided; 750 range and 128 damage inside its 500-1250, 128-512)
  pswarm     <- scorched void policy (guided; identical damage, range, cooldown)
  plight     <- ChargeBlast+- (charged Energy; nearest range)

Every curve in the catalog is the same one, keys (0, 1, 0, 0) and
(1, 0, -1.7656243, -1.7656243): full damage at minimum range falling to none
at maximum. Test: every weapon a product sells has a damage curve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:46:27 +02:00
Meta Vens
d91ab8daf9 Generation never offers gear no hull can mount
Operator ruling, 2026-09-30, on the Autocannon: "Leave the autocannon unsold,
we'll need to author a bunch more hulls before all the gear variety in the game
has a home." The fit rule gets one owner, HardpointData.Takes (the gear's type,
laid in at the hardpoint's rotation, filling every cell); LoadoutGenerator's
hardpoint picks, its reuse of a previous product and `AetherDb hardpoint-fit`
all read it. RandomProducts, which both station stock and NPC loadouts draw
through, offers only gear with a home: Tool gear, hulls, or hardpoint gear some
hull in the catalog takes. The Autocannon keeps its product and price.

Test: the Autocannon is never drawn in 2000 draws of its hardpoint type; after
a scratch hull whose hardpoint takes it is authored, it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:01:35 +02:00
Meta Vens
5c3cd599ac Zone: wait for last tick's belt tasks before clearing the orbit cache
Update cleared `_updatedOrbits` and only then waited on the previous tick's
asteroid-belt tasks, which read the cache through GetOrbitPosition. A task
still running could see the set mid-clear ("concurrent update ... corrupted its
state") or recompute an orbit into last tick's position. The wait now comes
first.

Test (Soul's orbit counter, committed): 3000 back-to-back 0.02 s ticks of a
quiet arena, and no orbit stands still against its parent. Clearing first gave
500+ stalls or the corruption exception. Measured against the parent because a
moon can genuinely pause in zone space: one such reading (tick 1321, a moon
whose motion cancels its planet's) is real motion, not the race.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:01:34 +02:00
Meta Vens
b3e7995ceb Stations: a station reactor, so the heater has power
Operator ruling, 2026-09-30: "Yep, we need a station reactor." Zenith's 16-cell
Reactor hardpoint had no design to fill it, so generated stations had no power.
A transient `AetherDb station-reactor [apply]` authored it from Manhattan, the
largest and most industrial reactor, scaled by cells (16/9):

  Station Reactor (GearData, Reactor hardpoint, full 4x4)
    mass 500 -> 888.9, price 750,000 -> 1,350,000 (to the nearest 50,000),
    charge 500..1000 -> 888.9..1777.8; every other value, the efficiency,
    overload and throttling stats and all stat terms, the roles (core,
    regulator), temperatures, durability and sound bank are Manhattan's.
  Product "Station Reactor" by Zhestokost (Manhattan's maker), default role
  quality.

Station generation fills the slot through its ordinary hardpoint pass. Tests:
every generated station carries one heater and a reactor; an idle station (a
quiet arena, nothing else running) holds its heater's cells above freezing over
the second five of ten minutes (unpowered they fell to ~247 K); a station hull
with no room for a heater fails generation by name instead of skipping it
(Soul's S11 survived before).

The station's border cells still sit near 265 K with the heater and reactor
running (the reactor core reaches ~433 K); how warm the whole hull should run
is left as a tuning question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 18:01:32 +02:00
Meta Vens
89ef80d298 Stations: every generated station carries a heater
Operator ruling, 2026-09-30: an idle station has no heat source and cools
toward freezing and invisibility, so station generation (LoadoutGenerator, the
one owner of station loadouts) equips one heater after the docking bay, from
the products whose design is Tool gear with a low-pass Thermotoggle and Heat,
the cockpit's thermostat composition. It is required, like the docking bay.

Not yet warm: generated stations have no reactor (no reactor product fits
Zenith's 16-cell Reactor hardpoint), so the heater's EnergyDraw gets no supply
and the station still cools (280 K to ~247 K over ten idle minutes). The test
that an idle station stays above freezing waits on that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:45:32 +02:00
Meta Vens
f4703e8288 Scenarios Cut 1: every unsold design is a scenario test design
Q3 with the 2026-09-30 ruling: a design no product sells is a scenario test
design and nothing else. A test design is recognised as one a scenario places:
its preset's hull or a slot of it, or cargo it carries. The census holds for
the shipped catalog (today there is no unsold design at all), and in a scratch
copy a new unsold design is flagged until a scenario places it, once through a
preset slot and once as an entity's cargo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:45:30 +02:00
Meta Vens
653ba490a7 Scenarios content: delete five industrial placeholders; products for four weapons and the heater
Operator rulings, 2026-09-30 (docs/scenarios-cut.md, "the ten real designs with
no product"), landed through a transient `AetherDb product-gap [apply]`:

- Deleted, nothing referencing them: Assembly Line, Deep Ore Extractor, Surface
  Ore Extractor, Refinery, Shipyard (RTS-era placeholders, price and durability
  0, three with unreadable behaviour entries).
- Products, maker taken from the nearest analogue's product:
    Autocannon  -> Autocannon by Zhestokost (DeathCluster: Ballistic, 4 cells, AutoWeapon)
    LRMM72      -> LRMM72 by Rossum & Douglas (GT 3K: the only other unguided launcher)
    SRMM72      -> SRMM72 by Death Monkey Explosives (scorched void policy: guided, nearest size and price)
    Tractor Beam -> Space Tractor by Ewan Hart Inc (no analogue; the wishlist's
                   "Space Tractor: It's a tractor. In space.", content-batch-one section 10)
    Industrial Thermostatic Heater -> same name by Lightsail Express (Cockpit 2x2,
                   the same Thermotoggle/EnergyDraw/Heat group)
  Each product carries its design's roles at the default quality (0.5 +- 0.15).
- The heater design kept (operator), with Price 0 -> 100,000 (between Cockpit
  2x2 and Fire Control Array) and Durability 0 -> 100 (Cockpit 2x2's): at zero
  price generation never offers it, at zero durability it never comes online.

The rewrite stores every weapon record at the current schema, so
FireControlCut124's shipped-catalog check now expects an Exact migration
report instead of drift with slot 32 defaulted. `AetherDb census` reports no
unsold design. Tractor Beam keeps price 0, durability 0 and no behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:45:18 +02:00
Meta Vens
27598a464a BaseState: an includeChildren walk keeps its state in locals
AddTransition's graph walk kept its visited, leaf and ignore sets in static
lists, so two Minions built at once on different threads (zones are built in
parallel) walked and cleared each other's state. Soul hit it as "collection
was modified" during parallel test runs. The sets are now the call's own.

BaseStateTests starts sixteen walks together behind a barrier, fifty rounds,
over prebuilt 200-state chains; with the statics restored every run failed
(several hundred of 800 walks wrong or throwing); with locals, clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:12:41 +02:00
Meta Vens
ff75b8ea34 Scenarios Cut 1: a stance survives docking; it goes only with an entity gone for good
Every entity's zone-removal subscription dropped its IFF override for whatever
left the zone, and TryDock removes the docking ship, so a scenario stance was
lost one way (the other entity forgot; the docked ship, deactivated, kept its
own) as soon as the player docked. The removal event already tells the two
apart: TryDock parents the ship to its dock before removing it, and every
leaving-for-good path (death, warp, unload) leaves with no parent. The override
is now dropped only for an entity that leaves unparented.

Tests: a hostile stance holds both ways, including the live hostility each side
tracks, across dock and undock; a destroyed entity takes the player's stance
toward it with it. UndockingAdmitsWithNoAgent commits Soul's probe: undock
admits unpiloted, active, with no agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:09:12 +02:00
Meta Vens
929020e7df Scenarios Cut 1: a quiet arena drops a story station's turrets too
The quiet-arena test ran with an empty Narrative directory, so the story-station
branch of ZoneGenerator never ran under Ambient: false and keeping its turrets
went unnoticed (Soul's S06). The test now places a story station in the entrance,
as the narrative loader would, and checks it is generated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 15:00:44 +02:00
Meta Vens
178897ce7a Scenarios Cut 1: a failed staging reports what it admitted, not a crash
"Zone unchanged on failure" was defended only by xunit formatting an entity
sequence until the test host overflowed its stack. SafeAssert.Unchanged compares
the zone's entities and agents by identity and order and names what changed by
hull name; every Assert.Contains over zone.Entities in RunStartTests goes through
SafeAssert.In for the same reason.

AMissingDesignFailsNamedAndChangesNothing gains the partial-admission case: a
player and a piloted entity that build cleanly, then a later entity whose preset
is missing. Admitting the built player before the all-or-nothing return now fails
by name ("the zone's entities changed, 5 -> 6: added [Djinni]").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:59:50 +02:00
Meta Vens
7e7c08bc73 Scenarios Cut 1: pin that admission activates
An admitted entity that was never activated tracks no hostility, so
targeting and grudges never see the stance. The stance test now also reads
each side's live EntityHostility, which only activation populates; dropping
Activate from Zone.Admit survived the suite before this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:59:43 +02:00
Meta Vens
2724a0d8a7 Scenarios Cut 1: observe an unpiloted ship's steering, not its position
The zone's gravity moves every ship, so an unpiloted scenario ship drifts
(about 14 m over 5 s at the test's spot) with nothing flying it. The rule is
"no agent steers it": the test now checks its movement and look inputs stay
untouched every tick, and that it still has no agent afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:10:03 +02:00
Meta Vens
85ebe036e1 Scenarios Cut 1: RunStart stages a new run; the Scenario catalog type
RunStart (ServerShared, no Unity type) owns what a new run starts with
(docs/scenarios-cut.md, 2.2):
- GenerateArena: the galaxy's entrance pack, generated under the scenario's
  Ambient rule. ZoneGenerator.GenerateZone gains `ambient`; false keeps
  planets, orbits and stations and generates no ship, turret or wanderer.
- Check: every failure staging a scenario would hit, found with no zone,
  touching no store.
- Stage: all-or-nothing. With no scenario, the generated starting-hull ship
  at the origin, as StartGame built it. With one, the player preset and the
  entities at their authored place and heading with cargo, stance set both
  ways through SetIff, a Minion for each piloted one; all through Zone.Admit.

Scenario (aetheria.scenario v1) joins AetheriaStores.CatalogTypes.
RunStartTests stage into a real prelude arena from the shipped catalog and
authored settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:35:18 +02:00
Meta Vens
5706b657e3 Scenarios Cut 1: Materialize builds product-less designs unbranded; the hull picks the entity
Two rules on Loadouts.Materialize (docs/scenarios-cut.md, 2.2, Q3):
- A design no product makes is outside the economy and is built unbranded,
  with no maker, at Loadouts.UnbrandedQuality (.95, the console give's).
  A design whose products are all unavailable still fails, as before.
- The hull's type picks the entity: a ship hull builds a Ship, a turret hull
  an OrbitalEntity with no orbit; a station hull is refused. Materialize now
  returns Entity.

Loadouts.Resolve is the one rule for how a design is built; RunStart uses it
for scenario cargo. LoadoutTests.MissingDesignReports no longer uses the
product-less "Orphan" as a failure (it now builds); two tests pin the Q3 rule
and the unavailable-product contract it does not loosen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:33:00 +02:00
Meta Vens
4f6bbbb8da Ship S2 smoke fixture: the node index test names the weapon nodes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:54:02 +02:00
Meta Vens
5b58b947bd Ship S2 smoke fixture: the weapon mount takes the hull other cell instead of moving the thruster off the schematic
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 10:53:26 +02:00
Meta Vens
54dcf5214c Ship S2 smoke fixture: the generated GLB carries a tetrahedron mesh on the map, collider and thruster anchors, plus a weapon mount and muzzle
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>
2026-09-30 10:52:47 +02:00
Meta Vens
34319b0df1 Ship S2 fixes: RequireDesigns refuses nulls explicitly, and a failing Compose falls back to the shipped catalog
- 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>
2026-09-30 10:06:16 +02:00
Meta Vens
8f8b72be3c Ship S2 fixes: the edge-of-range hardpoint test re-roles its anchor with its type
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:34:54 +02:00
Meta Vens
5ed3b759c4 Ship S2 fixes: the RunSave designs fixture resolves its gear ref
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 08:31:24 +02:00
Meta Vens
54917bb39d Ship S2 fixes: structural anchors are not mounts, mod presets refused, one bad mod never bricks the boot
- 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>
2026-09-30 08:30:52 +02:00
Meta Vens
86318ec014 Ship S2 tests: muzzle order against ids, every weapon type, PackageOf validates
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 06:46:17 +02:00
Meta Vens
460bb85ed0 Ship S2: the game composes and loads mod ships
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>
2026-09-30 06:43:52 +02:00
Meta Vens
f470c1b775 Ship S1 fixes: refuse the retired embedded hull, one body per hull, closed validator gaps
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>
2026-09-30 06:18:20 +02:00
Meta Vens
1702e0549f Ship S1: one hull record, and a hull names its visual
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>
2026-09-30 03:06:43 +02:00
Meta Vens
18e12d81ce Merge master (fire control 12) into codex/moddable-ships 2026-09-30 02:21:09 +02:00
Meta Vens
fe38f28865 Close the gaps a mutation pass found in the new ship tests (S-Soul F1-F3)
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>
2026-09-30 02:14:10 +02:00