Commit graph Aetheria/tests
Author SHA1 Message Date
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
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
Meta Vens
1f8c0ccd6d Give the Blender side tests and pin its hand-copied schema to C# (S-Soul F3)
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>
2026-09-29 23:55:46 +02:00
Meta Vens
1987009ba1 Pin Compose, ReadPackage and ReadNodeIds; refuse name collisions (S-Soul F2, F6)
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>
2026-09-29 23:47:13 +02:00
Meta Vens
5df72ba363 Pin every refusal in ShipAuthoringStore.Validate and Read (S-Soul F1)
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>
2026-09-29 23:46:44 +02:00
Meta Vens
742cb23f5b Gate a charged weapon's Activate on CanFire, and start a burst's cooldown with the tick it flew in
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>
2026-09-29 22:15:51 +02:00
Meta Vens
819b282346 Pin that Combat picks no group whose weapon cannot reach the target
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:54:36 +02:00
Meta Vens
c0fb26fca1 Pin that a triggered burst is not ready before its first round
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:39:18 +02:00
Meta Vens
1c5872e7a8 Give the stance-test gun a burst and a cooldown
The default InstantWeaponData has Count 0, so the fixture gun never had a round to fire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:35:53 +02:00
Meta Vens
c4b628fad7 Resolve the stance-test gun before triggering it
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>
2026-09-29 21:32:31 +02:00
Meta Vens
020a42fcb6 Execute the weapon directly in the stance tests
The stance fixture has a shooter the zone cannot tick; the weapon runs on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:31:38 +02:00
Meta Vens
7c1b1cd3d8 Tick the zone before reading the cooldown in the stance tests
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>
2026-09-29 21:27:28 +02:00
Meta Vens
0e73bbac72 Read the pending burst, not Progress, as the mark that Trigger ran; sound the charged control by its own event
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>
2026-09-29 21:23:04 +02:00
Meta Vens
136fddf15a Start the cooldown at a burst's first unrefused round; refused charged weapons never charge
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>
2026-09-29 21:16:52 +02:00