Aetheria/docs/aetheria-release-map.md
Meta Vens 1b2e9c1258 Map: allocator translation cost is linear (core r2 probes, tolerance, chosen constants)
Imagination pass, aetheria-release, session self-2026-10-09-morning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:37:59 +02:00

496 KiB
Raw Blame History

Aetheria Release: Map

Date: 2026-10-03

This map holds body facts, the model page and rationale for campaign aetheria-release. It has no progress section, no per-cut sections, no rulings and no ledger: those are typed documents in the Eureka mind. Ends live in docs/aetheria-release-target.md. The Eyes inventory this pass started from is F:\Projects\aetheria-release-inventory.md, which is outside the repo.

Every anchor below is against origin/master f1dee184 and was read in a sparse scratch worktree on 2026-10-03, unless the line or its section says otherwise. The section "Demo scope" holds the 2026-10-06 re-scope: the release this campaign plans is the demo.

Body facts

B1. No impact event carries a world point.

  • Entity.IncomingHit (ServerShared/Entity.cs:138) carries only the source.
  • ArmorDamage (:139) carries a schematic cell. ItemDamage (:140) carries the item. HullDamage (:143) carries the amount.
  • Zone.ShotCommitted and ShotResolved (Zone.cs:41-42) carry a ShotOutcome (FireControl.cs:1902-1930), which holds a schematic cell, bearing and lateral offset but no world point.
  • The world point of a direct hit is never computed. ApplyDirectHit (FireControl.cs:944-998) walks lanes in the schematic frame. A contact or delayed blast computes it at :1030 and :1043 and passes it to Detonate. Detonate (:1341-1414) has the world point and the share each entity took, and publishes neither.
  • So nothing can show a hit at the place it landed without recomputing FireControl's geometry.

B2. Weapon hits on a shield show nothing. ShieldManager.ShowHit (Gameplay/ShieldManager.cs:88) has one caller, ship-on-ship collision (:80). FireControl calls Shield.TakeHit and Break (FireControl.cs:952-956, :1378-1385) with no presentation hook. The inventory's "shield impact: present" refers to the shield-panel rig, which FieldTester drives in FieldShieldTest.unity. It does not run in the game.

B3. A capability event contract already exists for this. CapabilityEvents (ServerShared/CapabilityEvents.cs) defines AbsorbEvent (position, direction, magnitude, damage type) and one Subject per event kind. CapabilityPresentationBinder (Gameplay/CapabilityPresentationBinder.cs) wires IAbsorbPresenter components on an object and its children. The contract's own comment says the simulation will own and publish these events, and presentations will subscribe and decide nothing (ruling fork L, 2026-09-17). Today its only publisher is FieldTester. AbsorbEvent has no way to tell a shield absorb from a hull absorb.

B4. The weapons already announce what they do.

  • InstantWeapon.OnFire(shotId) (Behaviors/InstantWeapon.cs:272) fires only for a round that was not refused.
  • ChargedWeapon has OnStartCharging, OnStopCharging, OnCharged and OnFailed (ChargedWeapon.cs:87-90).
  • ConstantWeapon has OnStartFiring and OnStopFiring.
  • EntityInstance.SetEntity subscribes the effect managers to all of these (Gameplay/EntityInstance.cs:209-240).

B5. The Wwise-shaped audio path is dead at its source.

  • EquippedItem.SoundBank (Entity.cs:1353) is never assigned outside two tests (FireControlPerWeaponTests.cs:424, :672).
  • So every FireAudioEvent and SetAudioParameter call returns at its null check. The calls sit in InstantWeapon.cs:275, ChargedWeapon.cs:137,154,169,185, Reactor.cs:97, Thruster.cs:96 and AetherDrive.cs:170-172.
  • No production code subscribes to EquippedItem.AudioEvents (:1375).
  • WwiseSoundBinding and WwiseLoopingSoundBinding (WwiseSoundBinding.cs) have zero use sites.
  • GameData/SoundbanksInfo.json (2,648 lines) is read by no code. Only docs/build-delivery-cut.md:231 mentions copying it.
  • EntityInstance.cs:243-251 reads item.Data.SoundBank and does nothing with it.
  • Catalog fields that hold Wwise ids:
    • EquippableItemData.SoundBank (ItemData.cs:403);
    • AetherDriveData.RpmAudioParameter and TorqueRatioAudioParameter (AetherDrive.cs:44-48, keys 11 and 12);
    • Faction.OverworldMusic, CombatMusic and BossMusic (Corporations.cs:52-59, keys 13 to 15).

B6. Unity audio is switched off and has no listener. ProjectSettings/AudioManager.asset has m_DisableAudio: 1. A YAML grep found AudioListener: count 0 in ARPG.unity and in Main Menu.unity; the two test scenes have one each. The editor is Unity 6000.3.24f1 (ProjectSettings/ProjectVersion.txt).

B7. The 2021 sound library is on disk and usable.

  • The Wwise project is Aetheria-Economy_WwiseProject.wproj, WwiseVersion="v2021.1.1" and SchemaVersion="103".
  • Its 225 .wav are in LFS and present in the operator's checkout: Originals/SFX holds 731 MB.
  • The SFX include fire, hit and miss for cannon, ioncannon, lightingGun, machinegun and railgun. They also include charge and charge-fail, ship destroyed (two), and shield up, down and active. Overheat alarm, docking, undocking, wormhole, reactor states and sensor ping are there too.
  • Interface sounds are blip, equip, mouseover, pickup, putdown, and trade buy and sell.
  • Music: menu, Gen overworld and combat, Metal overworld, combat and boss, Daya overworld, Kawaii Future Bass. There are also 30 ambience layers.
  • The 20 Originals/Plugins/SoundSeed Grain sources feed a Wwise plugin, and their procedural behaviour does not carry over outside Wwise.
  • *.wav is an LFS pattern in .gitattributes.

B8. The camera has the tooling for shake; nothing uses it. com.unity.cinemachine 2.10.7 is in Packages/manifest.json. It ships CinemachineImpulseSource and CinemachineImpulseListener. A grep found no Impulse and no shake in Assets/Scripts. The follow camera is ActionGameManager.FollowCamera (ActionGameManager.cs:122).

B9. Graphics settings are not saved.

  • MainMenu.ShowGraphicsSettings's Back button (UI/MainMenu.cs:258-263) never calls ActionGameManager.SavePlayerSettings().
  • Gameplay's Back button does call it (:236-241).
  • PlayerGraphicsSettings (ServerShared/PlayerSettings.cs:63-67) holds keys 0 and 1.
  • PropertiesPanel.AddField(name, float read, write, min, max) exists (UI/Properties Panel/PropertiesPanel.cs:290).

B10. The lanes overlap the feedback files in these hunks (git diff origin/master...origin/<lane> hunk headers):

  • mining (83d8371e):
    • FireControl.cs at :72 and :522;
    • Entity.cs at :43, :198-240, :295-395 and :1075;
    • EntityInstance.cs at :236 and :410;
    • ActionGameManager.cs across :362-1432, including :1076-1100;
    • ZoneRenderer.cs and Zone.cs.
  • scenarios (aa3baf12):
    • Entity.cs at :204, :601 and :786-1003;
    • ActionGameManager.cs at :583, :732 and :801.
  • moddable-ships (a93625c2):
    • ActionGameManager.cs at :42-70;
    • ZoneRenderer.cs;
    • Packages/manifest.json, which adds glTFast.

feedback-1's edits are disjoint from these: FireControl.cs:944-1045 and :1375-1412, Entity.cs:138-150, and EntityInstance.cs:159-170.

B11. The headless tests compile only ServerShared. That covers Aetheria.Shared/Aetheria.Shared.csproj:20 and tests/Aetheria.Shared.Tests. A rule that must be pinned headless must live in ServerShared.

Model page

Kind Identity Lifecycle Authority
Impact (AbsorbEvent with a layer) Transient. The receiving entity's Capabilities stream, plus the shot. It is not stored. Published once per landed hit, before the hull damage is applied. It is never replayed or persisted. FireControl alone, in ApplyDirectHit and Detonate. Presenters only read it.
Impact cue Derived from one impact and the receiver. Computed at presentation time and discarded. Feedback (engine-free, ServerShared). Visual and audio presenters render it and do not re-derive it.
Player feedback preferences (shake, volumes) PlayerSettings global, player store, keys under GraphicsSettings (shake) and a new AudioSettings (volumes). Edited in the settings menu and saved on Back. A new key defaults on load. The player, through the menu. No runtime writer.
Presentation tuning (effect prefab, intensity scale, impulse shape, sound bank) A Unity component or asset in Assets/Content. Authored in the editor and versioned with the scene. The operator. It stays Unity-serialized by fork M (a).
Sound assets The file under Assets/Audio, copied from the Wwise Originals. Provenance is recorded per file. Added per cut. The Wwise project stays as the source archive until audio-route and audio-source-rights are ruled. The operator, on rights (audio-source-rights).
Ship content (ShipAuthoring with GLB) aetheria.ship_authoring id. GLB nodes carry an aetheria.id extra. Adopted by this campaign (ruling adopt-moddable-ships). Rows for the package, its anchors and the derived catalog are in "Content: the moddable-ships lane" below. The moddable-ships rulings (MQ1-MQ5) and this campaign's content strand.

No cell is empty for the two mapped cuts. The content rows are mapped in their own section below.

Rationale

Why the impact is published by FireControl and not derived in Unity. B1 shows that the world point exists only inside FireControl's lane walk and blast resolution. If a presenter derived it, it would have to recompute lanes from the ShotOutcome. That rebuilds the second hit authority that fire control Cuts 3 to 12.4(b) deleted. So the owner of where a round landed publishes where it landed.

Why reuse CapabilityEvents rather than a new subject. The contract was ruled for exactly this (B3): the simulation publishes absorbs and presenters subscribe. Its binder already exists. Reusing it adds one field (the layer) instead of a second event family. It also brings the shield-panel presenters within reach of real hits later, with no new wiring.

Why the cue derivation lives in ServerShared. Intensity and shake are rules ("in proportion to the damage", "zero at zero preference"). B11 says only ServerShared is under headless test and Stryker.NET. Keeping the function engine-free puts the rules where mutation testing reaches them. Audio then renders the same cue (target: one cue, two channels).

Why native Unity audio is recommended (question audio-route):

  • The simulation already announces fire, charge, impact and death (B4 and feedback-1).

  • The Wwise path is dead at its source (B5).

  • Unity audio is only switched off (B6).

  • Of the Wwise project, what carries over is the wav files (B7). The 2021.1 project, its events, banks and SoundSeed plugin sources would need migrating to a Wwise release that supports Unity 6.

  • Wwise adds three costs:

    • a native plugin per platform;
    • a second authoring application;
    • a sound-bank build step. Its authoring console runs on Windows and macOS, not on Yggdrasil's Linux.

    Agents cannot easily author Wwise work units, and the operator's AquaSynth renders would have to pass through it.

  • Licensing is not the obstacle. Wwise is free under a $250K development budget, with no asset limit (Audiokinetic pricing page and indie-license blog). FMOD is free under $200K yearly revenue and a $500K budget.

  • What the middleware would buy is interactive music and parameter-driven mixing, authored by a sound designer. The release bar asks for neither yet.

  • Unity's limits are known: 32 real voices by default (AudioManager.asset m_RealVoiceCount), no authored containers or RTPC graph, and mixer assets that cannot be created from script.

  • Unity 6's AudioRandomContainer has no public scripting API for its clip list. The first cut therefore uses a small ScriptableObject sound bank with random pick and jitter.

Confidence: medium-high. It falls to medium if the operator expects a contracted sound designer working in Wwise, or adaptive music with transitions and stingers before release.

Why content runs in parallel. The release quota is met at the operator's pace, one hull a day, and that clock starts only when the first Tripo mesh flies. The steps in front of it are the operator's owed S2 play checks, the moddable-ships merge, and the add-on's package action. Each is cheap and none depends on feedback or audio. Serializing them behind audio would idle the slowest resource in the project.

Sources consulted for the audio fork:

Two claims were not verified from primary sources on 2026-10-03: which Wwise release first supports Unity 6000.x, and the scripting surface of AudioRandomContainer.

Demo scope

Re-scope of 2026-10-06 (rulings first-release-is-a-demo, demo-cut-fixed-region, demo-cast, miss-terris-belongs-to-emily-r3, ship-authors-need-no-unity). The release this campaign plans is the demo: one gate, one region, one boss, a fixed cast. The design review that prompted it is F:\Projects\aetheria-game-design-review-2026-10-05.md, outside the repo. Anchors in this section are against origin/master df7c44f2 (the mining merge), read with git show and git grep from the Aetheria object store on 2026-10-06.

Body facts

  • D1. A fixed-cast generator already exists: the prelude. Galaxy.cs:150-262 builds a galaxy from named factions: protagonist, antagonist, buffer, quest and neutrals, resolved by name prefix (ResolveFaction, :141-144). It places homes so the antagonist is far from the protagonist and the buffer between them, and sets the entrance near the protagonist's home. Its settings are Assets/Resources/Settings.asset TutorialGenerationSettings (an LFS object, read with git lfs smudge): protagonist Miss, antagonist Zhe, buffer Luc, neutrals Aero and Finch, quest Adras, 64 zones. TutorialGalaxy stages it. The main galaxy (Galaxy.cs:96-131) draws MegaCount 12 factions at random from the catalog, places BossCount 3 boss zones, and has 128 zones. A prelude galaxy guarantees a station at its entrance (ZoneGenerator.cs:223-224) and lets generation use any product (LoadoutGenerator.cs:184).
  • D2. Boss zones and the exit exist only in the main galaxy. PlaceFactionsMain (Galaxy.cs:265-309) fills BossZones on chokepoints; the main constructor sets Exit to the most isolated zone (:123). The prelude sets neither. Both persist (SavedGame keys 3 and 5).
  • D3. Nothing spawns a boss. Faction.BossHull is key 10 (Corporations.cs:43-44) and is unset on all 12 catalog factions. Outside the field and Galaxy.BossZones, BossHull is read only by tools/AetherDb. ZoneGenerator generates stations, turrets, faction ships and wanderers (:280-365) and no boss.
  • D4. Wormholes are built in Unity, and the exit does nothing. ZoneRenderer.cs:225-236 adds one Wormhole { Position, Target } per adjacent zone. Interact (ActionGameManager.cs:321-327) enters any wormhole in range; EnterWormhole (:660) populates the target zone. Nothing checks Galaxy.Exit.
  • D5. Station services are buy-only and credits are not saved. TradeMenu has Buy(CraftedItemInstance) (:366) and Buy(SimpleCommodity, int) (:405) and no sell. "Repair" appears in no gameplay or UI script (only AetherDb comments). Credits are public int Credits = 15000000 on ActionGameManager (:114); SavedGame (keys 0-12) does not carry them, so Continue resets them. The hull branch of Buy checks GetPrice (:369-370) and charges data.Price (:378). Durability is per instance (ItemInstance.cs:53) against the design's (ItemData.cs:371), and falls under fire (Entity.cs:645-664).
  • D6. The catalog has no Pirates and the demo cast sells no ship hull. Decoded from git show df7c44f2:GameData/Aetheria.cc | git lfs smudge with Python msgpack, using the store's own schema table for slot names. Factions (12): Zhestokost, Finch Cybernetics, Lightsail Express, NiteLife Energy, Death Monkey Explosives, Aeronautics Unlimited, Lucent Media, Alakrita, Ewan Hart Inc, Rossum & Douglas, Adrasteia, Miss Terri's. Hulls (5): Longinus and LonginusX (Alakrita, ship), Djinni (Rossum & Douglas, ship), Zenith (AU, station), Turret (Zhestokost, turret). 64 products; by maker AU 11, Lightsail 9, NiteLife 9, Lucent 7, Death Monkey 6, Zhestokost 6, Rossum & Douglas 6, Alakrita 5, Finch 2, Miss Terri's 1, Adrasteia 1, Ewan Hart 1.

Why the demo is a prelude galaxy

D1 is already the demo's shape: named factions in fixed roles, a small galaxy, an entrance station. The demo needs a different cast from the tutorial (the Pirates as the ally, not Miss Terri's), a boss zone and an exit. So the demo is a compiled scenario whose script carries its cast and calls the prelude constructor, then places the gate at the antagonist's home (cut-demo-galaxy). The tutorial's settings and Miss Terri's prelude role are untouched. A demo mode flag on the galaxy was rejected: the scenario script is the owner the scenario rulings already name.

The gate is in the boss zone itself: an exit wormhole there opens when the zone's boss is dead (cut-boss-gate). Locking every wormhole out of a boss zone, as Three Gates item 2 describes, is the full game's three-section rule; the demo needs one gate and lets the player retreat.

Scope sort

Every cut spec and follow-up in force under root aetheria-release on 2026-10-06, after this pass's admissions. "Landed" means the spec has a report; it stays in force as a record.

Kind Id (local) Scope Reason
spec demo-galaxy demo The demo scenario, its cast and the gate's placement.
spec boss-gate demo The one boss and the sealed gate; the win.
spec station-services demo Sell, repair and saved credits: the run pays.
spec feedback-1 demo Every hit seen where it lands (target line 5).
spec audio-1 demo Every action makes a sound (line 4).
spec faction-play-1 demo Doctrine and the flight for the cast; needs r2 (follow-up faction-play-reanchor-demo).
spec faction-play-2 demo Lucent's duel is the featured neutral's spectacle; needs r2.
spec faction-play-3 demo Support and anchors; the played proof moves here for the cast; needs r2.
spec faction-play-4 full game The tender loop on real rounds is depth past the demo's proof.
spec loot-1 demo Floating items in the simulation; loot is what the player sells.
spec loot-2 demo Pickup as a capability.
spec loot-3 demo The grab presented in Unity; merges the loot branch.
spec ballistic-ammo demo Ruled for everyone and unblocked; ammunition is a cargo good in the Pirates' game. Needs a balance pass.
spec ships-player demo A built player loads package hulls: every demo hull needs it.
spec ships-addon-frame demo The Blender step from a Tripo mesh to a hull, no Unity. Stale against ship-data-all-in-cc if demo-ship-cc-timing rules before-hulls.
spec ships-addon-mounts demo Mount helpers for demo hulls. Same caveat.
spec ships-hull-material demo One hull shader painted from a mask: the demo's material under demo-material-scope's recommended option.
spec ships-addon-paint demo The mask authored in Blender for that shader.
spec ships-hull-livery demo Faction liveries for the cast; its Faction key collides (follow-up livery-key-collision).
spec scenarios-smoke demo Test designs and smoke scenarios: the verification path.
spec mining-merge-unity demo The Unity half of the merge that landed.
spec material-graph full game Graph record and judge; not demo-bearing under demo-material-scope (recommended).
spec material-library-export full game Same.
spec material-lowering full game Same.
spec hull-bakes full game Same.
spec ships-studio-schematic full game Authoring comfort in Studio; the demo's few hulls do not need it.
spec ships-addon-gizmos full game Same.
spec mining-index-tree full game Sublinear index: wanted, not critical (ruling).
spec mining-belt-cells full game Same.
spec ships-merge, ships-mounts, ships-addon-package, ships-bind-path, cut-ship-render-fixes demo, landed The package path the demo hulls use.
spec scenarios-adopt, scenarios-menu demo, landed Scenarios: the demo is one.
spec mining-index, mining-target-queries, mining-index-pins, mining-merge demo, landed Targeting through the index; the belt freeze is gone.
follow-up pirates-catalog-record demo The ally needs a Faction record and products.
follow-up zhestokost-boss-hull demo The boss hull and BossHull.
follow-up demo-cast-hulls demo A hull per cast faction.
follow-up faction-play-reanchor-demo demo Re-anchor faction play, add the Pirates, play the cast.
follow-up livery-key-collision demo Two specs claim Faction key 16.
follow-up demo-ux-ledger demo The bar's UX polish pass has no owner.
follow-up demo-difficulty-onramp demo "Game is hard"; the region needs a ramp.
follow-up vault-docs-sweep demo Stale vault notes mislead the faction-play revision.
follow-up controls-facing-and-aim demo Under demo-controls-rebuild's recommended option.
follow-up release-target-is-a-demo demo This pass; Self closes it with target r3.
follow-up release-map-not-on-master demo Land this map and the target doc on master.
follow-up wwise-catalog-fields demo The audio sequel removes the Wwise ids; the boss music field is among them.
follow-up scenarios-batch4-minors demo Inventory drag and station-reactor quality: UX ledger items.
follow-up hull-damage-wear demo Damage visible on the hull (line 6).
follow-up mod-ship-single-existence-gate demo A skipped package throws on spawn; demo hulls are packages.
follow-up ambiguous-hardpoint-origin demo Save correctness for package hulls.
follow-up remove-sbsar-files demo Clean break from Substance before a public build.
follow-up delaunay-exact-predicates demo Galaxy links; a silently unlinked zone could strand a demo run.
follow-up failed-start-scene-halts demo The New Game failure path the demo's missing-cast refusal uses.
follow-up stale-docs-after-scenarios-menu demo Docs that describe the old New Game path.
follow-up scratch-carry-drafts demo The ship .cc draft bears on demo-ship-cc-timing.
follow-up faction-lore-notes-r3 full game Vault setting notes; no demo cut reads them.
follow-up mining-impacts-through-contract full game Mining Cut 4 is parked.
follow-up mining-cuts-4-5-7 full game Yield and content; no buyer for ore in the demo.
follow-up mining-target-pin-gaps full game Low test pin gaps in landed code.
follow-up belt-render-reads-index full game Belt rendering through the index.
follow-up c4-ship-element-ids full game Variants campaign.
follow-up variants-python-raw-push full game Variants campaign.
follow-up mod-hull-changed-under-run full game Mod edits under a run; demo hulls are first-party.
follow-up ship-lods full game Only if a profile shows cost.
follow-up hull-decals full game Insignia and damage decals.
follow-up hull-player-paint full game Player liveries.
follow-up blender-pin-operator full game Pins Blender for the material judge.
follow-up material-mod-graph-kernels full game Mod material kernels.
follow-up material-map-rescope full game Re-scope of the material map, after demo-material-scope.
follow-up bodies-entity-and-mines full game Bodies for entities and mines.
follow-up cultmath-mixed-seed full game CultLib hygiene.
follow-up launcher-ammunition full game Missiles and drones campaign.
follow-up drones-munitions-substrate full game Same.
follow-up articulated-mounts full game Links with arc and traverse.
follow-up colosseum-npc-lab full game Its own campaign.
follow-up heat-flow-schematic-particles full game Presentation depth past line 6.
follow-up scenarios-verification-ledger full game Deferred by ruling.
follow-up authored-missions full game Authored scenes and missions.
follow-up volumetrics-backport-map full game Visual quality with no demo line.
follow-up ghostlight-readme-drift full game Ghostlight, narrative.
follow-up faction-play-reanchor, faction-lore-notes obsolete Superseded on 2026-10-06 by faction-play-reanchor-demo and faction-lore-notes-r3.

Counts: demo 52 (31 specs, 11 of them landed, and 21 follow-ups); full game 34 (9 specs, 25 follow-ups); obsolete 2 follow-ups. No in-force spec lost its purpose in both scopes, so none was withdrawn.

Demo hulls, the Pirates and the boss

Anchors are against origin/master 4d664b53, unless marked @2a5fc00b, the tip of the unmerged eureka/aetheria-release-boss-gate branch (which contains demo-galaxy). Read by cut-pirates-record, cut-package-product, cut-boss-hull-derived and cut-demo-cast-hulls; question pirates-record-content ruled the record's content (see "The Pirates record" below).

  • H1. The catalog at 4d664b53 still has no Pirates, and no faction has a boss hull. Probe: git show 4d664b53:GameData/Aetheria.cc | git lfs smudge, decoded with Python msgpack using the store's own schema table. Result: 12 aetheria.faction records, the same twelve as D6, with no Pirates. BossHull (slot 10) is the empty key '' on all twelve. Each faction's Allegiance holds 11 entries, one for every other faction. Influence runs from 2 to 6. There are 12 NameFile records: Australia, Congo, Germany, India, Iran, Japan, Mexico, Russia, Swahili, Thailand, USA and pleiades. There are 5 hulls: LonginusX, Longinus and Djinni (HullType 0, ship), Zenith (1, station) and Turret (2, turret). Their prices are 7.5M, 7.5M, 10M, 10M and 0.5M. There are 64 products. The catalog changed once after df7c44f2: 3d77f2e0 (evasion-catalog tracking).

  • H2. "Pirates" appears only on the unmerged demo branches. git grep -i pirate over *.cs at 4d664b53 returns nothing. At @2a5fc00b, DemoTerminus.cs:10 sets ProtagonistFaction = "Pirates", which Galaxy.ResolveFaction resolves by name prefix. The test partial RunStartTests copies GameData/Aetheria.cc into a scratch directory (RunStartTests.cs:36-46 @2a5fc00b). AddPirates() (DemoTerminusTests.cs:20-28 @2a5fc00b) upserts a stand-in Pirates record whose allegiance covers every faction. It is called 21 times: DemoTerminusTests 9, RunGoalTests 10, ScenarioTests 2. Test DemoTerminus_missing_cast_refused (DemoTerminusTests.cs:86-95 @2a5fc00b) asserts that the shipped catalog has no Pirates. Once the record lands, both the stand-in and that assertion are wrong: the stand-in duplicates the record and the assertion fails.

  • H3. demo-galaxy and boss-gate are held but unmerged. boss-gate contains demo-galaxy; its tip is 2a5fc00b, 17 commits ahead of master and 94 behind. Verdicts cut-demo-galaxy.s4 and cut-boss-gate.s2 hold. Finding cut-boss-gate.s1.boss-hull-is-a-strangers records that the demo cannot be generated on the bare catalog, because the Pirates record is missing.

  • H4. A package holds exactly two records, and no product. ShipAuthoringStore.LoadRecords (ShipAuthoring.cs:155-167) refuses any count other than one hull and one visual. ship_cc.py:85-86 refuses any file that does not hold exactly two envelopes. ShipModCatalog.Compose (ShipModCatalog.cs:48-133) writes only those two records (:113-115). Generation, station stock and the boss all choose from FactionProductData (M5). A package hull is therefore reachable only through the console's give command. Ruling package-carries-product is not implemented on master.

  • H5. Mod keys. IsModKey (ShipModCatalog.cs:25-26) recognises mod-hull: and mod-ship:. Its comment states the rule that the shipped catalog never references a mod key, because a mod may be uninstalled. A shipped Faction.BossHull therefore cannot name a package hull.

  • H6. Package hulls and top speed. ship-authoring create without --like (ShipAuthoringCommands.cs:45-66) writes a hull with no behaviours and Price 0. HullLike copies the reference's members, VelocityLimit and Price included (ShipAuthoring.cs:91-106). Validate (ShipAuthoring.cs:186-293) has no rule about top speed or price. This is R6, still true. Generation silently skips price-0 products (M5).

  • H7. Where boss and hull type are read. At @2a5fc00b:

    • LoadoutGenerator.GenerateBossLoadout (:178-196) uses Faction.BossHull when it is set and sold. Otherwise it falls back to the priciest ship hull on offer.
    • RandomHull (:131-136) is the one hull chooser. GenerateShipLoadout reaches it at :37.
    • The station stock filter (:112) admits ship hulls only.
    • Galaxy.PlaceFactionsMain picks its boss factions by BossHull.IsSet() (:296-299).
    • AetherDb's factions command prints BossHull (Program.cs:662-672), and dangling mentions it (:544).

    No other reader exists. HullType is Ship | Station | Turret (Enums.cs:140-145). Thrusters carry EntityTypeRestriction(HullType.Ship), so a boss must be a Ship, not a new HullType.

  • H8. HullData's free key. Keys in use: ItemData 1, 2, 4-8; CraftedItemData 9; EquippableItemData 10-16, 18-22, 30, 31; HullData 23-29, 32. Key 33 is free on HullData. evasion-term's key 33 is on WeaponData, a sibling type.

  • H9. Emoji in names. The TextMesh Pro default sprite asset (Assets/TextMesh Pro/Sprites/EmojiOne.json) holds 16 face glyphs (1f600-1f923, 263a, 2639) and no others. The fire emoji in the Pirate Coalition's equipment copy would not render without a new emoji asset (see "Full emoji").

  • H10. Pirates lore that the record reads from. Sources:

    • The vault note Worldbuilding/Pre-Elysium/Factions/Powers/Major/Pirate Coalition.md: a loose network, no central command.
    • Brainstorming/Faction Flavor and Visual Identity.md:526-557. Its points: stolen and adapted equipment gives the faction its range rather than an exclusive technology; products are named in emoji; hulls show several surviving finishes interrupted by primer, with one crew accent colour.
    • Game Design/Faction Play.md:395 and :416: pirates buy everyone's gear.
    • Corporate Roster: the pirate name is rolled per run. The demo's record is fixed.
  • H11. Packages in git and in builds. At 4d664b53, .gitattributes has no *.glb rule, and GameData/*.cc does not match GameData/Mods/<id>/ship.cc (M11 still true). There is no player build entry point; ships-player owns staging. ActionGameManager composes from GameData/Mods (:49, :63), so editor play reads packages from the repo.

The Pirates record

Draft for question pirates-record-content.

Field Draft Why
Name, ShortName Pirates, Pirates DemoTerminus's cast field and ruling demo-cast both say "Pirates"
Description "A loose coalition of crews and hidden docks moving people and goods outside official control. No command, no common law: possession has a maintenance schedule." Vault note; Faction Play's operating logic
PrimaryColor (0.42, 0.40, 0.37), primer grey "finishes interrupted by primer"
SecondaryColor (1.00, 0.45, 0.05), the crew's accent colour "a crew's chosen accent colour"
GeonameFile pleiades Star names suit hidden docks; it is not one of the cast's files
InfluenceDistance 3 Scattered bases, no continuous territory
Allegiance 1.0 to each of the 12 other factions; no other faction's allegiance changes Pirates buy everyone's gear. Only the Pirates field Pirates-made products, including their hull
Personality, music, BossHull Empty or unset Doctrine arrives with faction-play r2; the Pirates are not a boss

Products: stolen-and-adapted rebrands over other makers' designs. The Pirates own no design; their range is what they took (Faction Flavor :532). Each product's roles: one ProductRole per role the design declares, mean .45, sd .22 (middling and inconsistent, against makers' .15). Names are emoji; the text gloss is for the operator's review and is not stored.

Product name Gloss Design (current maker) Description
🫳🔫 finders keepers Autocannon (Zhestokost) "Serial number filed off. Still shoots."
🪦🤠 second owner Earp (Aeronautics Unlimited) "The previous owner had no further use for it."
🧾🚫 no receipt FastBlast+- (NiteLife Energy) "Warranty void where prohibited, which is everywhere."
🎨🙈 overpainted 6k Shooter (Zhestokost) "The logo underneath is somebody else's problem."
🔥 the fire scorched void policy (Death Monkey Explosives) "Turns out we did start the fire after all." (the roster's line, Corporate Roster and Item Wishlist.md:85)
👀🎯 borrowed eyes Targeting Computer (generic) "Somebody else's eyes. They still work."
🔑🏃💨 hotwired Small Drive (Lightsail Express) "Hotwired. Do not ask about the ignition."
📦🤫 not as declared Store-All Plus (Lightsail Express) "Contents not as declared."

None is on a Miss Terri's design. Each (Pirates, design) pair is new, so Brand stays unambiguous (CG4). The fire product moves to the flamethrower design if follow-up pirates-flamethrower lands one.

The record's data does not depend on the emoji font: the simulation owns facts and the font is a presenter (invariant sim-owns-facts). So the record waits on boss-gate (H2) and not on the emoji cuts, and the demo's Pirates (finding boss-hull-is-a-strangers) are not held behind a Unity presentation cut. Until the emoji font lands, editor play shows the names as missing-glyph boxes.

The boss hull is derived, not named

Follow-up zhestokost-boss-hull left this map to choose between two options. One was an AetherDb command that names the package hull from Faction.BossHull. The other was a package that declares which faction it is the boss of. The first is ruled out by H5: the shipped catalog would name a mod key, and uninstalling the package would leave the reference dangling.

The second option, built from parts that already exist:

  • HullData.Boss (key 33, bool) marks a hull that only bosses fly.
  • The package's own products name the faction as manufacturer (package-carries-product).
  • A faction's boss hull is then the Boss hull it manufactures.
  • Faction.BossHull has no writer in the catalog (H1), so it is deleted. Key 10 is retired, as ShipAuthoring key 1 was.

Starsector is the prior art: a mod puts its ships into an existing faction by shipping a faction file that the engine merges. The package therefore carries its own availability (map "Why first-party hulls through the package path").

The consequence for the product: a Boss hull is never station stock, never ordinary traffic, and never offered by RandomHull unless the caller asks for a boss. That keeps the boss unique. If a faction makes no Boss hull, boss-gate's logged fallback stands.

Why top speed is refused at Validate. Validate is the one owner of top speed (follow-up package-hull-top-speed): compose then excludes a speedless package with its name, and the dock intro never sees one. Handling the absence at the intro was rejected because it would let a hull fly with no cap.

Ruling demo-content-bar also needs NPC role hulls (haulers, tenders, miners) and a dockable station; follow-up demo-role-hulls owns them.

Full emoji

Anchors are against origin/master 4d664b53. Read by cut-emoji-font and cut-emoji-sequences; question emoji-art-set asks which pinned font input and fetch step the font build uses. Rulings this rests on: pirates-record-content (emoji-names) and the operator's "I totally want full emoji support".

  • E1. TextMesh Pro is the one inside com.unity.ugui 2.0.0, on Unity 6000.3.24f1. ProjectSettings/ProjectVersion.txt; Packages/manifest.json:19 ("com.unity.ugui": "2.0.0"); there is no separate com.unity.textmeshpro entry. Source read from F:\Projects\Aetheria\Library\PackageCache\com.unity.ugui@b996e7548785\Runtime\TMP (read only).
  • E2. TMP Settings today (Assets/TextMesh Pro/Resources/TMP Settings.asset, an LFS file): m_ActiveFontFeatures: 00000000 (:18), m_fallbackFontAssets: [] (:36), m_defaultSpriteAsset = EmojiOne (c41005c1…, :39-40), m_enableEmojiSupport: 1 (:42), m_EmojiFallbackTextAssets: [] (:44). Default font asset is LiberationSans SDF (:27).
  • E3. EmojiOne is a 16-face sample and nothing uses it. Assets/TextMesh Pro/Sprites/EmojiOne.json holds 16 faces (H9). git grep "<sprite" outside the TMP folder: nothing. EmojiOne's guid is referenced only by its own meta and TMP Settings; no prefab or scene sets m_spriteAsset. Its attribution file says only "review their licensing terms" (EmojiOne/JoyPixels art is not openly licensed).
  • E4. TMP's emoji path, by source read.
    • Lookup: TextMeshProUGUI.cs:1932-1944 (and TextMeshPro.cs:1619): when a code point is an emoji-presentation form not followed by U+FE0E, or an emoji followed by U+FE0F, TMP searches TMP_Settings.emojiFallbackTextAssets first, through TMP_FontAssetUtilities.GetTextElementFromTextAssets. That list takes font assets.
    • Sprite assets resolve one code point each (TMP_SpriteAsset.GetSpriteIndexFromUnicode, :216, a Dictionary<uint, TMP_SpriteCharacter>), so a sprite asset cannot match a ZWJ, skin-tone, flag or keycap sequence. Sequences resolve only through a font asset's GSUB ligature records.
    • Ligatures: TextMeshProUGUI.cs:2050-2118. Gated by bool ligature = m_ActiveFontFeatures.Contains(OTL_FeatureTag.liga) (:1824), per component. Matching skips ZWJ and VS1-VS16 when they are not the expected component (TMP_TextParsingUtilities.IsIgnorableForLigature, :172-178), consumes a trailing variation selector, and collapses the span into one glyph. The ligature table read is the current font asset's, which is the emoji font when the fallback found the first code point (:2021-2023).
    • Records come from FontEngine.GetLigatureSubstitutionRecords as glyphs are added to a dynamic font asset (TMP_FontAsset.cs:3097-3131).
    • Colour glyphs: a font asset in GlyphRenderMode.COLOR gets an RGBA32 atlas and the TextMeshPro/Sprite shader (TMP_FontAsset.cs:606-625); its vertices are forced white, so a <color> tag around an emoji does not tint it (TMP_Text.cs:5376-5385).
    • TMP_Text.OnMissingCharacter (TMP_Text.cs:1389) is a static event raised for every missing code point: the smoke's probe.
  • E5. No text component has ligatures on. 154 TMP components in 50 files (48 prefabs, 2 scenes), none legacy UnityEngine.UI.Text. Serialized feature lists: 24 x 00000000 (legacy: converted on load to [kern] when m_enableKerning, TMP_Text.cs:6082-6088) and 38 x 6e72656b (kern), all in the two scenes (ARPG.unity 64 lines with the field, FieldShieldTest.unity 1; grep -rc "m_ActiveFontFeatures: 6e72656b$" over prefabs and scenes = 38). The 48 prefabs' components have no field and take the same legacy path. One component is created at runtime (ActionGameManager.cs:301, the FACE AIM mark).
  • E6. Turning liga on changes no Latin text. All ten project font assets (Assets/Fonts/*/*.asset, LiberationSans SDF) are static (m_AtlasPopulationMode: 0) with zero ligature records. Only the emoji font would supply ligatures.
  • E7. LFS. .gitattributes:33 *.ttf and :50 *.asset are LFS; TMP Settings.asset, EmojiOne.asset and EmojiOne.png are LFS objects today.
  • E8. Editor batch pattern. Assets/Scripts/Editor/EngineAssetCheck.cs:35-50: a MenuItem plus -executeMethod EngineAssetCheck.Run, exit 1 on failure in batchmode. The emoji builder and smoke follow it.
  • E9. Prior art (web, 2026-10-08).
    • Unity's documented path for colour emoji is a colour font asset in TMP Settings' Emoji Fallback Text Assets (TMP 3.2/4.0 manual "Color emojis"; Unity 6 manual UIE-color-emojis). Unsupported: SVG colour glyphs, AAT fonts, COLRv1 (the manual names "Noto Color Emoji" as the COLRv1 case), and chain-context and single-substitution features. Ligature substitution (GSUB type 4), which carries ZWJ, skin-tone and flag sequences, is supported. COLRv0 is supported (the manual's Android note).
    • Unity forum thread 932305: the CBDT NotoColorEmoji.ttf from the googlefonts repo renders through a Color font asset; the Google Fonts download (COLRv1) does not.
    • Sets: Twemoji (jdecked/twemoji, community fork, Emoji 17.0, graphics CC-BY 4.0; distro packages build COLRv0/COLRv1/CBDT TTFs from its SVGs with nanoemoji, version 17.0.2). Noto Emoji (googlefonts/noto-emoji: fonts OFL 1.1, images Apache 2.0; the prebuilt NotoColorEmoji.ttf is CBDT, bitmaps from png/128). OpenMoji (CC-BY-SA 4.0; COLRv0 font shipped; outlined line-art).
    • Not verified here: the exact jdecked release assets (its release page lists two source archives and no TTF), so a Twemoji TTF is built, not downloaded.
  • E10. Visual identity. AetheriaLore Brainstorming/Faction Flavor and Visual Identity.md:532, 541, 548: Pirates' products are named in emoji, a crew vocabulary; hulls carry "large emoji marks". Ship Design Language.md:222-226: one large emoji crew emblem painted three times. Corporate Roster and Item Wishlist.md:85: "Named entirely in emoji. The flamethrower is" the fire emoji. Game Design/Visual and Sensory Direction.md says nothing about an emoji art set; its interface register is "should feel like equipment" (:43).

Why one colour emoji font asset. One owner: a colour emoji font asset generated from a pinned font file, set as TMP's emoji fallback and appended to TMP's global fallback list. Every TMP text renders emoji through it with no per-component wiring, because TMP itself routes emoji code points there (E4).

Rejected: a generated TMP sprite asset (the brief's first guess). Sprite lookup is one code point per sprite (E4), so full emoji would also need a text preprocessor that rewrites every sequence to a private-use code point, installed on every one of 154 components and every runtime text. That is more code doing what TMP's font path already does.

Rejected: a runtime-built font asset (TMP_FontAsset.CreateFontAsset at boot). It needs Shader.Find("TextMeshPro/Sprite") in a player (TMP_FontAsset.cs:625), which a build strips unless the shader is always-included; an editor-built asset references its material and shader.

Sequences need liga on the rendering component (E4, E5). There is no global TMP switch, so the sequences cut sets it on every serialized component through one editor command and on TMP Settings' defaults for new components. It changes no Latin text (E6).

The atlas is dynamic and multi-atlas: glyphs rasterise on first use, so nothing is hand-made and nothing is pre-baked. m_ClearDynamicDataOnBuild: 1 (TMP Settings) already clears dynamic atlases at build. The pinned font file is the only binary input, and its provenance is a lock file (source, version, sha256 in and out). emoji-art-set decides only which pinned input and which fetch step; the asset, settings, smoke and deletion are the same for every option. The font build itself (option twemoji: nanoemoji) runs under py -3 on Yggdrasil, not Starfire; the Unity batch run is Starfire's. The sequences command touches only the m_ActiveFontFeatures line of each of 48 prefabs and 2 scenes, so it wants the freshest master and a quick merge.

Retiring the prelude and the tutorial

Body: origin/master 4d664b53; "merge tree M" as in the faction-play re-anchor (BS-probes), unless noted. Read by cut-retire-prelude and cut-gear-without-presence; question dev-scenarios-cast asks which galaxy the six test scenarios use once the authored cast goes. D1 and "Why the demo is a prelude galaxy" describe the code before this retirement.

  • PR1. Galaxy.IsPrelude (Galaxy.cs:30) is written by three constructors: the save constructor from SavedGame.IsTutorial (:53), the main constructor false (:107), the prelude constructor true (:175). Its readers (grep over Assets, tests, tools): InfluenceOf (:158-161, halves influence rounding up), LoadoutGenerator.IsAvailable (:211-214, every product on offer), RunStart.GenerateArena (RunStart.cs:29) and ActionGameManager.PopulateLevel (ActionGameManager.cs:721-727) as ZoneGenerator's isTutorial, RunSave.Capture (SavedGame.cs:77), and tests (RunGoalTests.cs:22, RunStartTests.cs:146, ScenarioTests.cs:137, :146, :554-557).
  • PR2. InfluenceOf feeds both generation and later zone generation: Galaxy.cs:214-247 (cast homes), :314-324 (main homes), :342-349 (influence), and ZoneGenerator.cs:212 (factionPresence, which sets station and enemy counts). A restored galaxy generates unvisited zones through PopulateLevel, so the halving must persist with the run, today through SavedGame.IsTutorial (key 11).
  • PR3. ZoneGenerator.GenerateZone(..., bool isTutorial = false, bool ambient = true) (:40-46): the entrance station floor at :223-224, the widened orbit pool at :238 and :246-248, and the throw at :327-329 all apply only to isTutorial && galaxyZone == galaxy.Entrance. Source: operator 2026-09-25 "Tutorial station" (docs/locomotion-cut.md:375), scoped to the tutorial level.
  • PR4. The cast type is TutorialGenerationSettings (Settings.cs:96-106): five faction names, LinkDensity, ZoneCount. Readers: GameSettings.cs:30 (field, plus TutorialBackgroundSettings :31), GalaxyStage.cs:16, :29, :71-74 (Prelude() and Prelude(cast)), RunStart.cs:36-37, ScenarioStage.cs:21-35 (read only by TutorialGalaxy.cs:13), MainMenu.cs:178-179, ActionGameManager.cs:848, DemoTerminus.cs:8 (the demo's cast), tools/AetherDb/Program.cs:489-533 (settings and settings-dump).
  • PR5. Assets/Resources/Settings.asset (LFS, smudged at 4d664b53) :43-52: the authored cast is protagonist Miss, antagonist Zhe, buffer Luc, neutrals Aero and Finch, quest Adras, 64 zones; TutorialBackgroundSettings at :53.
  • PR6. Six test scenarios generate stage.Prelude(), the authored cast: Arcs, DjinniShakedown, Duel, LauncherAngles, LongHaul, StarvedReactor (Scenarios/*.cs, one line each). Scenarios.Development is { TutorialGalaxy, MainGalaxy } (Scenarios.cs:8). Their galaxy includes Miss Terri's as protagonist.
  • PR7. The prelude constructor's playerSettings and narrativeDirectory parameters feed only the StoryProcessor lines commented out at Galaxy.cs:258-260 (dead since ce1a0a46, 2021, per locomotion-cut.md:375).
  • PR8. LoadoutGenerator.IsAvailable (:211-214) and ManufacturerPreference (:217-226): availability requires Galaxy.ContainsFaction(manufacturer) unless the galaxy is a prelude; an absent manufacturer has no home zone, so ManufacturerPreference gives it distance 0, the highest weight its allegiance allows. Galaxy.ContainsFaction (:453-457, cache _containedFactions :36) has one game reader (IsAvailable); other readers are FireControlCut6cTests.cs:253 and the AetherDb loadout exclude-sellers mode (Program.cs:745-790), which builds a galaxy without named factions precisely to exercise the presence gate. ScenarioStage.cs:29 builds its availability with Faction null, which today means every present maker.
  • PR9. SavedGame (SavedGame.cs:9-52) is an int-keyed MessagePack document aetheria.savedgame v1, keys 0-13; IsTutorial is key 11.
  • PR10. Tests touching the concept (grep -c "tutorial|prelude", case-insensitive): RestoredHullsTests 65, ScenarioTests 30, DemoTerminusTests 29, RunStartTests 16, MiningCut3Tests 3, RunGoalTests 1. Most are the TutorialGalaxySettings() fixture family in RestoredHullsTests and the isTutorial: argument.
  • PR11. Open boss-gate finding test-reads-isprelude (verdict cut-boss-gate.s1) is RunGoalTests.cs:22; retiring the flag removes it by construction.

Why two cuts. The follow-up asks to absorb gear-availability-without-presence and to keep the subtraction separate from behaviour. Removing the presence gate changes what every galaxy generates, so it is its own cut (gear-without-presence), landed first; retire-prelude then deletes the flag with no behaviour change except where named.

Why IsPrelude is split by rule, not renamed. It bundles four unrelated rules, and each goes to its owner:

  • influence halving -> the cast's own InfluenceScale (the demo's cast sets .5, so its galaxy is unchanged), carried by the Galaxy and the save (key 14) because later zones read it (PR2);
  • the entrance station -> Scenario.EntranceStation, a declaration beside Ambient, default true, false for MainGalaxy, so every scenario's arena is unchanged; RunStart.GenerateArena applies it;
  • availability -> the general rule of gear-without-presence;
  • the save flag -> deleted (key 11 retired; ruling no-save-compatibility-before-players).

A renamed flag on Galaxy was rejected: it would keep one switch deciding four things.

Absent manufacturers. Preference weights by distance to the maker's headquarters, and an absent maker has none. The cut weighs it one jump beyond the farthest zone. Prior art: the gravity model of trade (Tinbergen 1962; Anderson and van Wincoop 2003) weights flows by distance decay, and a supplier with no local presence carries the largest trade cost, so it is reachable but least preferred. Zero distance (today's accident) would make absent makers the most preferred, inverting the rule.

Old saves. Key 14 absent decodes as 0 and the save constructor refuses a non-positive scale by name. Per ruling no-save-compatibility-before-players nothing reads key 11.

Model page rows

Kind What names it Over time Who decides
Pirates Faction Catalog record, name Pirates Authored once by AetherDb pirates-faction apply; doctrine is added by faction-play r2 Ruling pirates-record-content; the operator sees the dry run
Package product mod-product:<id>:<maker key> in the package's ship.cc Written by AetherDb ship-authoring product; copied verbatim by compose; Blender never edits it The package author
Boss hull HullData.Boss (key 33) Set by ship-authoring boss; a faction's boss hulls are derived per generation from the products it makes The package author; BossHullsOf derives
Faction.BossHull (key 10) Retired Deleted by boss-hull-derived none
Pirates products 8 FactionProductData, emoji names, over other makers' designs Authored with the faction in one commit Ruling pirates-record-content
Package hull price HullData.Price Written by ship-authoring price (or create --like) The package author
Emoji font file Assets/Fonts/Emoji/<set>.ttf + emoji-font.lock Rebuilt only by tools/emoji at a new pinned version The operator's emoji-art-set ruling
Emoji font asset Assets/Fonts/Emoji/Emoji.asset (dynamic, COLOR, multi-atlas) Regenerated by EmojiFont.Build; atlas filled at runtime, cleared at build EmojiFont.Build
TMP emoji routing TMP Settings m_EmojiFallbackTextAssets and m_fallbackFontAssets Set by EmojiFont.Build EmojiFont.Build; no prefab overrides it
Ligatures per text Each TMP component's fontFeatures liga added once by EmojiFont.EnableLigatures; new components inherit TMP Settings EmojiFont.EnableLigatures, then the component
EmojiOne sample none Deleted by emoji-font none
Galaxy cast (replaces TutorialGenerationSettings) A scenario's code constant (DemoTerminus.Cast) Compiled with the game; no authored settings copy The scenario
Galaxy.InfluenceScale Its galaxy Set by the constructor (the cast's value, 1 for main), saved as SavedGame key 14 The cast
Scenario.EntranceStation Its scenario Compiled The scenario; RunStart applies it
SavedGame key 11 (IsTutorial) Retired Not read, not written none
Galaxy.IsPrelude, TutorialGalaxy, TutorialGenerationSettings, GameSettings.TutorialGenerationSettings Deleted by retire-prelude none

Faction play: the NPC scripting

The operator ruled on 2026-10-03 that the faction dynamics may elaborate the NPC scripting through the extension points she built into it (ruling npc-scripting-may-grow). Legacy-first still forbids CultMesh and daemon work. This section maps those extension points, names the smallest set of primitives that carries every dynamic in Game Design/Faction Play.md (AetheriaLore f3e0e6d), and gives the reasons for the four proof cuts (faction-play-1 to -4), which are typed specs in the mind. Anchors are against origin/master f1dee184, read with git show on 2026-10-03, unless the line names a lane.

Body facts

F1. NPC behaviour is a code-composed state graph, not a tree, a DSL or assets.

  • Agent (ServerShared/Agents/Agent.cs:14-80) holds one current BaseState. Each tick, Update (:47-56) runs the state, then takes the first StateTransition whose Condition() holds (:82-94: target state, condition closure, optional on-transition action).
  • BaseState.AddTransition(..., includeChildren, ignoreStates) (States/BaseState.cs:19-58) copies a transition onto every state reachable from the source. That is the interrupt mechanism: one call makes "a target appeared" fire from anywhere in the graph. It is the same idea as Halo 2's impulses.
  • Minion (Minion.cs:5-22) is the only Agent subclass. It composes root, patrol and combat in its constructor. Behaviour is authored in C#; there is no data-driven layer.
  • codex/scenarios makes AddTransition's walk state per call, because zones are built on several threads (its BaseState.cs hunk at :12-58).

F2. Tasks are the command slot, and only one task kind is wired.

  • Agent.Task (Agent.cs:22) holds one AgentTask (Tasks/AgentTask.cs:13-22). Minion enters patrol when Task is PatrolOrbitsTask (Minion.cs:10-12).
  • HaulingTask, Mining, StationTowing and Survey are data with no state that reads them. AgentTask.Priority and Reserved have no reader; they are the remains of a task board that is no longer in the tree (WanderTask and MatchVelocity were deleted in cc7f2c88).
  • PatrolOrbitsState (Tasks/PatrolOrbits.cs:10-31) and MoveToOrbitState (States/MoveTo.cs:31-37) are the only movement states. No state follows an entity, holds a standoff, or flees.
  • No task state returns to root when Task changes. Only combat's interrupt leaves patrol.

F3. Agents are made in one place and ticked before entities.

  • Zone.CreateAgent (Zone.cs:137-144) gives every non-player ship a Minion with a patrol over four random orbits. Zone.Update ticks agents (:185-186), then entities (:188), then FireControl.Step (:193).
  • codex/scenarios adds Zone.Admit(entity, piloted) as the only writer of Entities.Add and the only creator of agents (construction, run staging, warp arrival, undock, spawned turrets).
  • Agents are not persisted (AgentTask.cs:11). A loaded zone rebuilds them.

F4. Each ship perceives alone.

  • Entity holds VisibleEntities, VisibleEnemies, VisibleFriendlies, EntityHostility (Entity.cs:34-37) and EntityInfoGathered (:64).
  • Sensor.Execute is the only writer of EntityInfoGathered (Sensor.cs:157-189). Sensor.Ping (:110-125) boosts it and adds the pinger's own visibility.
  • Crossing TargetDetectionInfoThreshold moves an entity into the visible lists (Entity.cs:237-254).
  • FireControl.Designated requires source.VisibleEntities.Contains(target) (FireControl.cs:232-239). IsRevealed reads the observer's own EntityInfoGathered (:130-149). A ship cannot fire at, or aim at gear on, something its own sensors do not hold.
  • No group, squad or faction-level knowledge exists. Ship.HomeEntity (Ship.cs:26) has no reader.

F5. Hostility has a derived rule, a per-entity override and a grudge.

  • IsHostileTo (Entity.cs:535-553): an override decides outright. Otherwise a zone owner is hostile to trespassers (presence not permitted), and anyone is hostile to whoever is hostile to them. // TODO: Inter-faction hostility is at :547.
  • GetFactionRelationship (:556-563) answers Beloved for the own faction and Neutral for every other, except for the player's ship, which reads Galaxy.FactionRelationships. Ruling faction-relations-field fills this.
  • SetIff (:529-533) writes the runtime override. WatchForGrudge (:452-481) sets a sticky hostile override back on anything that turns hostile while detected. Leaving the zone clears overrides (:207). The override map is private: nothing outside Entity can ask whether a grudge is held.

F6. Target selection has four writers.

  • Minion.cs:14: the first enemy to become visible, when the slot is empty.
  • TurretController.cs:96: the first visible enemy ship.
  • The player's input (ActionGameManager.cs:377-403).
  • Entity clears it when the target leaves the zone or visibility (Entity.cs:201, :227, :298-299).
  • codex/mining turns Target into a TargetRef (asteroid chunks can be targets) written through SetTarget. It edits Minion.cs:14-21 and Combat.cs:28.

F7. Combat tuning is global.

  • CombatState.SampleDps biases range by GameplaySettings.AgentRangeExponent (Combat.cs:158, Settings.cs:242). Movement reads AgentForwardLerp and AgentMaxForwardDistance (Combat.cs:95, Settings.cs:243-244).
  • FireControl.AgentFires (FireControl.cs:93-99) compares hit probability with the global AgentMinHitProbability (Settings.cs:260). Its callers are Combat.cs:121 and TurretController.cs:81.
  • CombatState aims at the target's most dangerous revealed weapon through TrySelectTargetItem (Combat.cs:40-53).

F8. Faction.Personality is economy vocabulary, not combat doctrine.

  • Faction.Personality (Corporations.cs:28-29) maps PersonalityAttribute records (ItemData.cs:902-914: a name with low and high pole names) to floats.
  • The same records key CompoundCommodityData.DemandProfile (ItemData.cs:334-335), which is what a population with those traits wants to buy.
  • Neither map has a reader. A string probe of the working-copy store found the schema and no faction values; that probe is not proof.
  • So the attributes are population traits shared with demand. Encoding engage_on or target_priority as float bands in them, as Faction Play's lever table proposed, would give one record two meanings.

F9. Generation spawns individuals, not groups.

  • ZoneGenerator adds enemyCount ships of the nearest faction one at a time (ZoneGenerator.cs:334-340), plus neutral wanderers of one other faction (:342-356).
  • LoadoutGenerator.GenerateShipLoadout(hullFilter) (LoadoutGenerator.cs:35-50) fills every hardpoint (:189-231), so no generated ship is unarmed. Nothing marks a ship as a tender or a hauler.

F10. Hits carry their source only on the hull. IncomingHit (Entity.cs:138) fires with the shooter at FireControl.cs:962 and :1009, after the shield branches have returned, so a shot a shield absorbs reports no source. feedback-1 adds Capabilities.Absorb for both layers, without a source.

F11. Cargo moves between entities; jettison has no owner. Entity.TryTransferItems(target, item, quantity) (Entity.cs:693-725) moves commodities between cargo bays, and weapons draw ammunition from cargo through AmmoType (InstantWeapon.cs:187-192). Dropped items exist only as Unity pickups spawned on death (EntityInstance.cs:304-320), and the mining lane keeps pickups Unity-only (its pickups question, recommendation A). The simulation has no floating cargo to jettison into. The floating-item substrate is mapped as cuts loot-1 to loot-3 (section "Lanes: scenarios, mining and the loot move", L6-L8).

F12. Barks and pings already have a channel. Entity.SetMessage (Entity.cs:1275-1278) shows a line for MessageDuration. Entity.Sensor.Ping() is callable by anything that holds the entity.

F13. Agents are testable headless. IffAndCombatTests builds a Zone with ships and drives detection by writing EntityInfoGathered (tests/Aetheria.Shared.Tests/IffAndCombatTests.cs:51-92). Agents/ is under ServerShared, so the whole graph compiles into Aetheria.Shared (B11).

F14. Ammunition is real for three weapons, and nobody is stocked.

  • Only DeathCluster, FastBlast+- and pretty pretty bang bang carry an AmmoType; their refs were repointed in 515859cf (docs/cultcache-migration-cut.md:161-163). Every other weapon, Zhestokost ballistics included, reloads for free.
  • LoadoutGenerator puts no rounds in any cargo, and station stock is drawn from EquippableItemData only (LoadoutGenerator.cs:98-99), so no station sells rounds either. An NPC carrying one of the three ammunition weapons fires its first magazine and then never reloads (InstantWeapon.cs:186-197).
  • So Zhestokost's "magazines run low, back to the tender" has no substrate until its weapons draw a commodity and generation stocks it.

What the extension points can carry

Extension point Carries Cannot carry
State graph with interrupt transitions (F1) Every per-ship motor behaviour: follow at a standoff, hold, flee, return, drift, sweep. Anything that needs more than one ship's knowledge: who may engage, shared tracks, duels.
Agent.Task (F2) The order a ship is executing. The patrol precedent already maps a task type to a state. Deciding the order. Nothing writes tasks except CreateAgent.
Zone.CreateAgent, then Zone.Admit (F3) Where a ship joins whatever owns its orders, and where doctrine is applied. Group state, which must outlive one admission.
Perception lists and EntityInfoGathered (F4) A ship's own view, which FireControl trusts. Shared knowledge. A second writer would let a ship aim at gear its own sensors never resolved (IsRevealed), breaking instruments-bound and fire control's single authority.
SetIff and grudges (F5) Hostility memory, and spreading it: one call per member. Faction-to-faction stance; that is the relations field.
Ship.Target (F6) The engagement itself: CombatState runs while it is set. Coordination, while four writers race for it.
Faction.Personality (F8) Nothing in this strand; it belongs to demand. Typed enums and per-role values.
SetMessage, Sensor.Ping (F12) Hails and ping cadence. Nothing needed here.

The graph, the task slot and the factory were built to be extended, and they carry the individual ship. What is missing is one layer: an owner of a group's intent. Every cross-ship dynamic in Faction Play needs it.

The primitives

Seven primitives carry the proof factions and leave room for the rest. Each says where it plugs in, what it reads and which quirks it enables.

1. Doctrine (data). A typed FactionDoctrine on Faction (key 17; key 16 is Relations by ruling), with flight-level fields and a RoleDoctrine per role (combatant, support). Flight-level fields: EngageOn (detection, identified, provoked, trespass, never), Grace, Hail, ComplySpeed, Pack, StepInDelay, PingInterval. Per role: BreakOffHull, BreakOffAmmo, RangeExponent, MinHitProbability, HoldStandoff, Anchor, Leash. A faction with no doctrine gets one built from today's globals, so it fights exactly as Minion does now. Inputs: catalog only. Enables the tuning half of every quirk (R&D range, Zhestokost spray, Cryonix discipline). Why not Personality: F8. The shape is question faction-doctrine-shape.

2. Flight (the group's intent owner). One runtime object per faction per zone, holding that faction's piloted ships. It is formed at admission and rebuilt on load, never persisted (F3). It is the only writer of each member's Target and Task; the agent graph only executes them. It reads members' perception, hull fraction, cargo and grudges, the doctrine, and the zone's stations. It ticks in Zone.Update before the agents. This is RimWorld's Lord (a group state machine that assigns duties to pawns) and Halo 3's objectives (squads fill prioritised tasks with capacities), at the size this game needs. Why a new owner: no extension point holds more than one ship's knowledge (table above).

3. Engagement rule with grace and hail (in the flight). Per hostile track, the flight decides when it becomes engageable (EngageOn over the union of members' views), hails once through the nearest member's SetMessage, holds for Grace, then engages, unless the track has slowed below ComplySpeed, which counts as compliance. A grudge on the track skips the grace. Enables Zhestokost's inspection hail, Sol Dominion's "never before identification" (identified: any member's info at the gear tier), Miss Terri's and Aya's "provoked", Megiddo's trespass, and Odla's "never". F.E.A.R.'s squad dialogue is the precedent: the bark speaks a decision already made.

4. Engagement slots and the duel (in the flight). Pack engagers per track; the rest hold at HoldStandoff (default: just outside their own longest weapon range). An engager that breaks off frees its slot after StepInDelay. With Pack 1 the slot is a duel. It is offered only to a challenger with no other hostile track near it, and it breaks when anyone lands a hit on a holding member, or anyone but the challenger lands one on the headliner. Broken, every slot opens and the pack engages whoever broke the form. This carries ruling lucent-duel-bait whole: honour the duel and pick the pack apart, or stage an interloper. The signal is a landed hit with its source (F10), so the cut adds Source to feedback-1's absorb. Prior art: attack tokens (the "kung fu circle" of brawlers) and Halo 3's task capacity.

5. Anchor and leash (in the flight). Each role's doctrine names an anchor: the flight's support ship, the nearest own station, the patrol site, or none. An engager past Leash from its anchor is ordered back, with hysteresis. Enables the Zhestokost column that will not pass its tender, AU escorts bound to the worksite, NiteLife guards that never leave the station, Dominion's jurisdiction, Megiddo's perimeter, and Aya refuge across a border.

6. Roles, break-off and rearm (in the flight). A member with no weapon is support; the rest are combatants. Doctrine says how many support ships generation adds (LoadoutGenerator gains an unarmed option, F9). Break-off orders a flee to the nearest own station, or away from the threat. It triggers on hull fraction (Lucent headliners, AU haulers, Alakrita, Cryonix), or on ammunition fraction against the count at admission. An ammunition break-off returns to the support ship, which transfers rounds through TryTransferItems (F11) until the baseline is restored; then the member rejoins. This carries the Zhestokost tender loop, once Zhestokost weapons draw a commodity and generation stocks it (F14, question zhestokost-ammunition). Support ships use their own role doctrine: the tender follows the column's rear and flees early, and the column, anchored to it, follows it home.

7. Motor states (in the agent graph). FollowState (an entity at a standoff, matching its velocity) and FleeState (toward a refuge, or away from a threat). They are new BaseStates on the existing graph, entered from tasks, and every task state returns to root when the task changes. Follow serves escort, hold, return, rearm and shadow; flee serves every break-off. They are two of the tutorial's four shared states; drift-cold and sweep stay with the tutorial.

Beyond the proof, three more primitives use the same owners and need no new layer:

  • Relations (ruling faction-relations-field). GetFactionRelationship reads Faction.Relations, and IsHostileTo treats Hated and Hostile as hostile at the TODO in F5. Every cross-faction pair starts here.
  • Picture sharing between flights. A flight pushes track entries and grudges to other flights in the zone by a ShareTrack policy: own faction, allies by relation, or every flight hostile to the track (Finch). A shared entry lets a recipient engage and steer; adopted grudges become SetIff on its own members. Sharing never writes EntityInfoGathered, so a recipient fires only once its own sensors hold the target. This is question shared-track-cueing.
  • Calls. A zone-level distress subject (caller, attacker, position), raised by a flight when a member is hit, or as a bark with a false caller (pirates). Flights with AnswerCall in range retask members. Navigator escorts pulled off a Lightsail convoy, Aya cover and the corridor edge come from this plus relations.

Target priority (drives, cargo, structures) extends CombatState's aim-item choice by doctrine and the flight's track choice. Jettison is ruled (jettison-shared-floating-items) and builds on Zone.Release from loot-1.

Model page rows

Kind Identity Lifecycle Authority
Doctrine Faction.Doctrine, catalog, key 17. Authored through an AetherDb catalog command, as the targeting migrations were. Null reads as the default doctrine built from GameplaySettings. The catalog author. Read when a ship joins a flight.
Agent (Zone.Agents) Its ship. Created by Zone.Admit, removed on the Entities removal path (it was never removed before). Zone.
Flight Runtime: zone plus faction. Created on first join, rebuilt on load; members leave on removal or death, and it goes with the zone. Never persisted. Itself alone, for members' Target, Task, hails and pings.
Track (flight picture) Runtime: flight plus hostile entity. Lives while any member sees it, or a sharer lends it. The flight, from members' perception. Never written into EntityInfoGathered.
Duel Runtime: flight plus challenger. Offered, then held or broken. Ends when the challenger leaves the picture or dies. The flight, from landed hits with their source.
Ammunition baseline Runtime: member. Counted at admission. The flight. Rearm moves real cargo through TryTransferItems.

Rationale

Why the flight owns intent and the agent only executes. Faction Play's dynamics are decisions about several ships at once: who engages, who holds, who knows what, who goes home. Spreading them across agents would keep four writers on Target (F6), add more on Task, and make agents read each other's private state. The RimWorld Lord and Halo 3's objectives both keep one group-level decider over simple individual executors, and Starsector varies one AI by parameters. The demotions: Minion no longer chooses a target. Zone.CreateAgent no longer assigns a patrol; the flight does. The global combat tunables become the defaults of a faction with no doctrine. TurretController keeps its own target writer, because turrets are not piloted and belong to no flight; giving stations a flight is a later step.

Why one flight per faction per zone. It needs no persistence and no grouping in generation, and a zone rarely holds two separate groups of one faction. The cost: two such groups would act as one. When generation learns groups, the key widens to a group id assigned in ZoneGenerator; nothing else changes.

Why shared tracks are cues, not fire-control data (the recommendation in shared-track-cueing). Fire control was rebuilt so that one owner decides a hit from the shooter's own sensor data (Designated, IsRevealed, PSensor). A remote track that fed EntityInfoGathered would undo that, and would show the player hits from ships that never saw them. As cues, sharing still delivers the dynamics Faction Play names: the Zhestokost column needs no identification and finds the hot target the Dominion pointed it at; the Finch shadow is a countdown because the hunters come, not because they fire from beyond their sensors. The US Navy's Cooperative Engagement Capability fires on remote tracks, and it needs the fused track picture that the target rules out of scope.

Why the tutorial director goes through the flight. The tutorial sketch's director sets agents' tasks and targets directly. With the flight as their only writer, the director becomes an input to the flight (a scripted order for named members that outranks doctrine), not a second writer. The tutorial cut owns that hook; this map records the boundary.

Why the proof is four cuts. One cut would be about 2,500 lines with tests, well over a Hands budget, so it is split along the primitives:

  • faction-play-1 lands the owner: doctrine, the flight as the only writer of Target and Task, the engagement rule with grace, hail and compliance, the follow state, and per-doctrine range and fire threshold. Zhestokost already hails before it sprays, Lucent announces itself and fires only on good odds, and AU escorts shoot the first contact.
  • faction-play-2 adds slots and the duel, hull break-off with the flee state, and ping cadence: Lucent whole. It needs feedback-1's absorb.
  • faction-play-3 adds roles, support generation, anchor and leash: the tender the column will not leave, and the AU haulers that run while their escorts stay close.
  • faction-play-4 makes ammunition real for Zhestokost and closes the tender loop (question zhestokost-ammunition, F14). It ends with the operator's playtest of the three factions.

Each is sized at roughly 550 to 720 lines with tests, under 200k Hands tokens. All four wait for codex/mining (it changes Target's type and edits Minion.cs) and codex/scenarios (it rewrites admission in Zone.cs) to merge, because the first cut rewrites exactly those lines.

Re-anchor on master 4d664b53 (2026-10-08)

Body: origin/master 4d664b537f76029fbaee94392da3d8a122f81339, read in a detached scratch worktree. Where a probe says "merge tree M", it read git merge-tree --write-tree origin/master origin/eureka/aetheria-release-boss-gate = tree 8387b47214ef6e44cae28a4e6a2b9b91d51464d9 (boss-gate 2a5fc00b contains demo-galaxy 85d0a148; the merge is clean, no conflicts) with git show <tree>:<path>. No commit was made. Read by the faction-play cuts, r2 and later.

  • BS1. Branch state. git merge-base --is-ancestor: codex/mining, codex/scenarios, eureka/aetheria-release-mining and -scenarios are ancestors of master. eureka/aetheria-release-demo-galaxy (85d0a148) and -boss-gate (2a5fc00b) are NOT. demo-galaxy's merge base with master is 228f241e; master is 94 commits ahead of it. -retire-longinusx on origin equals master (4d664b53): its Hands had not pushed.
  • BS2. git merge-tree --write-tree --name-only origin/master <branch>: demo-galaxy and boss-gate each merge into master with no conflicting path (trees f3f69e84 and 8387b472).
  • BS3. feedback-1, ballistic-ammo, controls-mount-aim, controls-ai-bearing, autofire-threshold and ai-autofire had in-force specs and no report (specs-with-no-report query, as of ordinal 753). CapabilityEvents.PublishAbsorb has no caller in Assets/Scripts/ServerShared (grep): feedback-1's absorb publish sites do not exist yet.
  • FP1. Minion.cs:7-22: constructor Minion(Ship ship); :14 is the one VisibleEnemies subscription that writes Ship.SetTarget(add.Value) when Target.Value.IsNone; combat transitions read Ship.Target.Value.Entity (:18, :21).
  • FP2. Zone.cs: Agents :40; Admit :167-172 is the only Agents.Add in Assets (grep) and the comment names it the one admission; CreateAgent :174-181 draws the circuit at :178 and assigns agent.Task at :179; the agent loop :221-222; the only Entities subscription is the death subscription at :107 (ObserveAdd -> Death -> Remove + Deactivate).
  • FP3. Nothing ever removes an agent. Agents.Remove/Clear appear nowhere in Assets or tests (grep). A dead or departed ship's Minion keeps running every tick. Removal from Zone.Entities happens at Zone.cs:107 (death), Entity.cs:1268 (docking) and ActionGameManager.cs:725 (the player's zone transfer).
  • FP4. Entity.cs: _iffOverrides :53; Target :60 (TargetRef, one writer SetTarget :358); SetIff :811-815; IsHostileTo :817-836; GetFactionRelationship :838-845 reads Galaxy.FactionRelationships only for the player's ship, so NPC-vs-NPC hostility is still only trespass in an owned zone plus reciprocity ("TODO: Inter-faction hostility"). Consequence for the demo: the allied Pirates and the antagonist Zhestokost do not fight each other; the Pirates' doctrine is seen only against trespassers in Pirates zones. Relations (key 16) remain unmapped.
  • FP5. FireControl.cs:104-112 AgentFires(weapon, shooter, target) reads GameplaySettings.AgentMinHitProbability at :109. Callers: Combat.cs:121 and TurretController.cs:83 (grep). Combat.cs:159 reads AgentRangeExponent; _optimumRange :13. Settings.cs:251 and :274 hold the two globals. Ten FireControl test fixtures set AgentMinHitProbability = 0f in their settings.
  • FP6. Agent.cs: Task :22, Settings :25, constructor :28-36, Update :47-59 zeroes Ship.Turn each tick (controls-helm), Accelerate :61-84. TaskType AgentTask.cs:24-33 has no Follow or Flee. States live in Agents/States (BaseState, Combat, MoveTo); PatrolOrbitsState is in Agents/Tasks/PatrolOrbits.cs:10.
  • FP7. Corporations.cs: Faction keys run to BossMusic Key(15) at :58-59; keys 16 and 17 are free.
  • FP8. tools/AetherDb/Program.cs command switch :23-40; TargetingCatalog(bool apply) at :1096 is the dry-run/apply precedent.
  • FP9. Tests that construct new Minion(ship) directly: FireControlPerWeaponTests.cs:276, :291, :580, :597 and FireControlTests.cs:214; RunStartTests.cs:100 and SafeAssert.cs:44 read zone.Agents.
  • FP10. LoadoutGenerator.cs: GenerateShipLoadout(Predicate<HullData>) :35-50, EquipHardpoints :220-263, FillInterior :265. ZoneGenerator.cs: faction ships :342-348, wanderers :350-364, return pack :366. boss-gate adds a boss block to both files (+38 and +18 lines), so anchors in them move by symbol after it merges.
  • FP11. Entity.TryTransferItems(Entity, CultRecordKey, int) at Entity.cs:977. No GeneratedMagazines or AmmoType exists in LoadoutGenerator.cs or Settings.cs: ballistic-ammo has not landed.
  • FP12. Entity.Sensor :142; Sensor.Ping() Sensor.cs:110.
  • FP13. Catalog GameData/Aetheria.cc at 4d664b53 (LFS, smudged in the worktree): the string "Pirates" occurs only inside geoname lists, no Faction record (H1). Follow-up pirates-catalog-record is the owner.
  • FP14. Lore source for the Pirates' doctrine: AetheriaLore Aetheria/Game Design/Faction Play.md section "Pirate Coalition" (:333-338) and the summary row :386: crews want cargo, not death; demand a drop, hit drives, leave fights they are losing. Of these, the flight primitives express the demand (hail and grace) and leaving a losing fight (break-off). Demanding a drop as compliance and aiming for drives need jettison (map item 10) and subsystem aim, which no cut builds yet.

What changed from the first anchor. The first faction-play pass anchored on f1dee184 and waited for the mining and scenarios lanes. Both are merged (BS1), so the later specs branch from master and anchor on 4d664b53. Target is a TargetRef written only through SetTarget, and Zone.Admit is the only creator of agents, as the follow-up says.

Agents are also removed on entity removal (FP3). A flight whose membership follows removal while the agent list does not would leave two answers to "who is in this zone"; one subscription now decides both.

The Pirates' doctrine is authored in the doctrine-catalog command's table. While the catalog lacks the Pirates record (FP13), the command names the missing faction and writes the rest; once the record lands, re-running apply writes it. No doctrine, barks or levers for Miss Terri's.

The played proof sits in the DemoTerminus scenario with the demo cast, because the third cut completes the behaviours the demo cast shows (Zhestokost's tender column, Lucent's duel, AU's haulers); the fourth keeps the tender loop as full-game scope with no proof.

The ping cadence asks Sensor.Ping() and nothing more. Follow-up sensor-stat-set-cut is a separate, larger mapping (per-sensor power, beam width, passive gear, shared tracks) and was not mapped here; whatever Ping becomes, the flight only decides when to ask.

Key layout on Faction: Relations 16 (reserved, ruling faction-relations-field), Doctrine 17 (ruling faction-doctrine-typed). ships-hull-livery's Livery at 16 must move to 18 (follow-up livery-key-collision, owner ships-hull-livery's next revision).

Collision: ai-autofire rewrites Combat's and TurretController's fire decision "at the agent threshold". After faction-play-1 the agent threshold is the member's RoleDoctrine.MinHitProbability (turrets keep the global). Whichever lands second re-anchors; the next ai-autofire revision should read _agent.Doctrine.MinHitProbability.

Prior art

Content: the moddable-ships lane

The operator ruled on 2026-10-03 that this campaign adopts codex/moddable-ships whole (ruling adopt-moddable-ships). This section maps what the adoption takes: S2's owed checks, the merge, the Blender package action and S3. S5 stays with the variants campaign. The cuts are typed specs in the mind (ships-merge, ships-mounts, ships-addon-package, ships-addon-frame, ships-addon-mounts, ships-player), and the forks are questions (ships-merge-gate, release-hull-home, s3-player-build). Two rulings of 2026-10-03 reshape the add-on and player cuts and add a material strand: hull-runtime-materials and thrusters-radiators-are-meshes. They are mapped in "Hull materials, mount meshes and per-hull prep" below, with the cuts ships-hull-material, ships-hull-livery and ships-addon-paint and the questions hull-paint-mask (ruled: hull-paint-mask-texture) and hull-livery-scope.

Anchors are against the lane tip a93625c2, read with git show on 2026-10-03. For files the lane does not touch, they hold on origin/master f1dee184 too. The lane's own docs (docs/moddable-ships-cut.md, docs/moddable-ship-authoring.md) are body facts from here on.

Body facts

M1. The merge into master is trivial. All 21 commits on origin/master since the lane's base 65c63495 are Scenarios map: doc commits. git merge-tree --write-tree origin/master origin/codex/moddable-ships is clean (tree 7e034e04). No file is changed on both sides. Fire control and the stack reached the lane through its own merge 18e12d81 on 2026-09-30.

M2. The lane conflicts with the two other open lanes, one line each.

  • codex/mining (83d8371e): AetheriaStores.cs:9 (CatalogTypes; mining adds FieldKindData, ships add ShipAuthoring) and tools/AetherDb/Program.cs:39-41 (a case line and the usage string).
  • codex/scenarios (aa3baf12): AetheriaStores.cs:9 (scenarios add Scenario). Program.cs, ItemData.cs, Loadout.cs and LoadoutTests.cs merge on their own.
  • The resolution is a union in both: keep every type and every command.
  • Probe: git merge-tree --write-tree origin/<lane> origin/codex/moddable-ships.

M3. The cuts already mapped in this campaign meet the lane in one file.

  • The lane edits UI/MainMenu.cs in ShowMain and inserts EnterGame and Refuse just before ShowSettings (lane :195-218).
  • audio-1 edits ShowSettings (master :186-200), so expect an adjacent-hunk conflict there. Resolve it by keeping both.
  • feedback-1 edits ShowGraphicsSettings, which the lane does not touch.
  • Neither cut touches ActionGameManager.cs, ZoneRenderer.cs or the lane's new files.

M4. The mount ruling of 2026-09-30 is in the docs, not the code.

  • a93625c2 records it: a mount anchor exists only for weapon, radiator and thruster hardpoints.
  • The validator still demands an anchor for every hardpoint (ShipAuthoring.cs:222-230), and a weapon's mount must have the role articulation.
  • ShipModPlan copies every mount into Equipment (ShipModPlan.cs:13-14,56). ShipModShips fills EquipmentHardpoints from it (ShipModShips.cs:152).
  • The only reader of EquipmentHardpoints is the dead SoundBank block (EntityInstance.cs:244-253), which audio-1 also deletes. So the array has no consumer.
  • Role-versus-type checks are split. The validator checks thruster and radiator mounts. ShipModPlan.Build checks that an emitter is a mount (:30-35) and that muzzles hang off weapons (:38-48).

M5. A package can reach the game only through the console.

  • Generation, station stock and the starting ship all choose from FactionProductData (LoadoutGenerator.RandomProducts; RandomHull at LoadoutGenerator.cs:113).
  • A product names a design and a manufacturer Faction (FactionProduct.cs:15-31). Generation also requires Price > 0.
  • A package holds only HullData and ShipAuthoring (ShipAuthoring.cs:126-140 refuses a third record; ship_cc.py:81-82 does the same).
  • So the only way into a zone is the give <hull name> console command while docked (ActionGameManager.cs:527-556). It commissions a bare hull at the station.
  • A grep of ConsoleController and ConsoleView found no debug-build gate. It is assumed live in a player; ships-player checks it.
  • ship-authoring create writes a HullData with default stats and Price 0 (ShipAuthoringCommands.cs:38-56).
  • Question release-hull-home asks how a release hull gets its manufacturer and price.

M6. The model must sit on the schematic grid.

  • Entity.ToWorldPoint (Entity.cs:426-433) gives world = (cell − Shape.CenterOfMass) × SchematicCellSize (2 m, Settings.cs:255), with schematic +x to starboard and +y forward.
  • Fire control decides hits in that frame. feedback-1 places impact sparks at ToWorldPoint of the lane entry.
  • A GLB whose origin is not the grid's centre of mass, or whose scale is not 2 m per cell, shows hits beside the hull.
  • Nothing checks this today.

M7. Axes, probed.

  • Blender 5.2.2 (--background --factory-startup, a cube ship exported with export_format='GLB', use_active_collection=True, export_extras=True, export_yup=True):
    • An object custom property aetheria.id lands in the node's extras.
    • Objects outside the active collection are left out.
    • The GLB has a scene.
    • Blender (0, 1.5, 0) exports as glTF (0, 0, -1.5).
    • An empty's scale (2, 3, 1) exports as (2, 1, 3).
  • With glTFast's handedness flip, Unity sees (-x, z, -y) (ShipModVisual.BuildLineMesh, ShipModVisual.cs:98).
  • So the nose of a ship points along Blender −Y, Blender's own Front, and the ship's starboard side is Blender −X.

M8. The operator's two Tripo hulls are already in one .blend. A read-only probe of a copy of C:\Users\Meta\Desktop\Quiet.blend (Blender 5.2.2):

Collection Object Triangles Size (Blender units) Material
Headliner ff611c02… (and original) 50,008 0.68 × 1.00 × 0.35 one; a 4096² JPG base colour
Quiet Dexter Quiet 9,614 0.98 × 1.05 × 0.41 one; a 4096² JPG base colour
Quiet Sinister Quiet 10,880 1.07 × 1.05 × 0.41 the same
  • Each collection also holds a Grease Pencil LineArt object. Quiet holds a scaled icosphere, a hand-made shield ellipsoid.
  • Tripo meshes arrive at about 1 unit long, one material, base colour only: no normal, roughness or emissive map. The textures came from FBX imports (D:\Downloads\tripo_texture_*.fbm) and are packed into the .blend.
  • Schematic sketches sit beside the .blend: HeadlinerSchematic.png, DexterQuietSchematic.png and SinisterQuietSchematic.png.

M9. The template lacks a radiator material.

  • EntityInstance heats radiators with material.SetFloat("_Emission", …) (EntityInstance.cs:407-410). Only Aetheria shaders define _Emission (Assets/Materials/Radiators.mat).
  • ShipModTemplate (ShipModTemplate.cs:12-16) carries shield, tractor, ping, destroy effect, invisible and map icon, but no radiator material.
  • A mod radiator therefore keeps its glTF material and never glows.
  • Thruster emitters are particle emission shapes (ShipInstance.cs:71, particlesShape.meshRenderer). Any mesh works.

M10. A built player has never loaded a mod ship. Every proof so far ran in the Editor:

  • ShipModPlaySmoke and ShipModPreview.Smoke passed in Unity 6000.3.24f1 (moddable-ships-cut.md S2 status).
  • glTFast's manual says a Built-In pipeline player must carry glTFast's shader variants: a ShaderVariantCollection in Graphics' Preloaded Shaders, or placeholder materials in Resources. This is from the docs, not a probe (https://docs.unity3d.com/Packages/com.unity.cloud.gltfast@6.0/manual/ProjectSetup.html).
  • Whether glTFast keeps meshes readable in a player is also unprobed. ShipModShips.Assemble throws if the collider mesh is not readable (ShipModShips.cs:103-104).
  • origin/master has no player build entry point. docs/build-delivery-cut.md Cut 1 maps one (Assets/Editor/AetheriaBuild.cs, copying GameData/Aetheria.cc). It does not copy GameData/Mods, and nobody owns it.

M11. Git does not yet track packages well. .gitattributes has no *.glb rule. GameData/*.cc covers only direct children of GameData, so GameData/Mods/<id>/ship.cc would be stored as a plain blob.

M12. The add-on reads CultLib from wherever Brokkr points. _libraries puts <cultlib_py_src>/cultcache-py/src on sys.path (ship_cc.py:46-52). Brokkr's preference is a working-tree path. Nothing pins the revision the add-on runs against. The variants campaign is rewriting Python's write path (spec variants:cut_spec:cut-parity-py.r1).

S2's owed checks: what each one checks, and who can prove it

The lane owes six operator checks (moddable-ships-cut.md, S2 status). Each check is a rule decided in one place, plus the Unity wiring that shows it. The rule is agent-provable. The wiring is agent-provable unless it needs clicks through scenes.

Check Rule, and where it is decided Agent proof Needs the operator
C1. The menu names excluded mods ShipModCatalog.Compose and ResolveCatalog exclude and name a bad or colliding package, and boot continues. Already headless: ABadPackageIsExcludedAndNamedWhileTheGoodOnesCompose, ACollisionExcludesEveryPackageInvolvedAndNoneOfTheRest, TheBootCatalogSurvivesBadPackages, TheBootCatalogSurvivesAComposeThatFails. Built-player boot smoke (ships-player): with one bad package staged, Player.log holds Some mods were not loaded: <dir>: <reason>. MainMenu.Refuse logs it at MainMenu.cs:213. No.
C2. New Game with a mod installed EnterGame waits on ShipModShips.Loading, refuses a faulted preload, then loads ARPG (MainMenu.cs:197-209). The preload half: the player smoke's SHIP_MOD_PRELOAD line, with no glTFast shader error. Yes: clicking New Game, and the prototypes surviving the Main Menu → ARPG scene load (DontDestroyOnLoad, ShipModShips.cs:39). No mod hull appears on New Game anyway (M5).
C3. Continue with a mod The run stores mod-hull:<id> keys, and the recomposed catalog keeps them. Headless: ComposeAtBootIsDeterministic. New sim test AModShipSurvivesSaveRecomposeAndReopen (ships-player). A Continue click in the S3 session.
C4. Zone entry LoadEntity branches on Visual (ZoneRenderer.cs:292-294). EntityInstance.SetEntity binds barrels, radiators and thruster emitters by mount name. ShipModPlaySmoke covers instantiation. Extending it to call SetEntity over an outfitted mod ship is a probe in ships-player. If SetEntity needs the live scene, record it as not yet reached. Yes: the ship looks right, thrusters emit at the nozzles, guns fire from the muzzles, the shield fits, the map icon shows.
C5. Wormhole Prototypes outlive zone changes, and LoadEntity clones again. Instantiating twice from one prototype in the smoke. Yes: one transit.
C6. Continue after removal RunSave.RequireDesigns refuses and names the missing mod ids. Headless: RunReferencingAMissingModRefusesContinue and its siblings. Covered by the rule tests. A glance: the dialog appears on Continue.

All the operator rows fit in the S3 session, on a built player with the first real hull: about fifteen minutes, done once. With no package installed the mod path is inert:

  • ResolveCatalog returns the shipped catalog (TheBootCatalogIsTheShippedOneUntilAModIsInstalled).
  • Preload returns at once (ShipModShips.cs:31).
  • Loading is a completed task.

Merging before the operator session therefore changes nothing a no-mod player sees. Question ships-merge-gate asks the operator to confirm that.

The merge

  1. Hands creates eureka/aetheria-release-ships at a93625c2. The Codex branch is never pushed to.

  2. Hands runs git merge --no-ff origin/master, which is clean (M1), and verifies:

    • the headless suite on Yggdrasil (684 at S2; master added no tests);
    • the Unity compile on Starfire;
    • both batch smokes against the generated skiff fixture.

    This is cut ships-merge.

  3. Soul passes the merge: a narrow pass, because no file changed on both sides.

  4. Self merges the branch into master with --no-ff, gated by ships-merge-gate.

  5. After that, every content cut branches from master in its own worktree, on eureka/aetheria-release-ships-<cut>. Anchors stay valid, because the master merge changes no lane file.

  6. Mining and scenarios inherit the union resolutions in M2 when they merge (follow-up lane-merge-union-catalogtypes).

The stale local ref codex/moddable-ships (375d6bd4) in the Codex worktree is left alone.

The Blender package action

The GLB export and ModelAsset below are superseded by "Ship data all in the .cc": Package writes ShipModel into ship.cc.

The add-on today binds a collection, captures LineArt, and edits the grid and hardpoints. It writes no anchors and exports no GLB (moddable-ship-authoring.md proof gate 2). The design aims at one thing: the operator's time per hull, at one hull a day. Everything that can be derived from the mesh is generated. Everything that needs her eyes takes one click at the 3D cursor.

Per hull, after the add-on cuts:

  1. Tripo: image to model, textured. Export GLB. Tripo's smart low-poly option is worth its 10 credits above about 60k triangles.
  2. Blender: import the GLB into a new collection. Press New Ship:
    • Enter the id, the name, a reference hull such as Djinni, and the length in cells.
    • AetherDb creates GameData/Mods/<id>/ship.cc with the reference hull's stats (create --like). C# owns the schema, and Python never builds a HullData.
    • The add-on binds the collection, turns the long axis to −Y, scales the mesh to length × 2 m, rasterises a draft grid from the top-down silhouette, and shows the grid as an overlay.
  3. Rebuild, unwrap, rebake and mask: see "Hull materials, mount meshes and per-hull prep" below (steps P3 to P7).
  4. Mounts:
    • Thrusters and radiators are meshes (ruling thrusters-radiators-are-meshes). Select the nozzle-exit or radiator faces in Edit Mode and press Add Thruster or Add Radiator, or tag a selected mesh object.
    • Weapons stay points: with the 3D cursor on each gun, press Add Weapon (Energy, Ballistic or Launcher).
    • Each also creates the hardpoint row on the cell under it, with a 1×1 footprint.
  5. Panel: toggle cells, add the internal hardpoints (reactor, shield, sensors and so on, which carry no anchor), and set footprints.
  6. Package, one button:
    • Recentre the ship on the grid's centre of mass (M6).
    • Regenerate the structural anchors unless she has marked one as kept:
      • hull-collider: a convex hull of the render meshes, at most 255 faces;
      • map-icon: a flat top-down outline;
      • shield: an ellipsoid empty over the bounds with a margin;
      • tractor: an empty at the nose.
    • Cap textures at 2048 by default.
    • Export ship.glb with extras.
    • Write ModelAsset, Anchors, the layout, and the lines captured from the collection's LineArt.
    • Run AetherDb ship-authoring validate, and show the C# validator's errors by anchor and hardpoint name.
  7. Unity Editor play: dock, give <name>, equip, undock, fly.

Steps 2, 4 and 6 replace the hand work that M8 shows she has started. The aetheria.id of every node is its anchor id, so ModelNodeId equals the anchor Id. Node ids are written by the add-on and never typed by her.

Material slots. The material owners are in "Hull materials, mount meshes and per-hull prep" below. Ruling hull-runtime-materials superseded the deferral of livery recorded here before.

Colliders. Hits are fire control's (R8). The hull collider serves only ship-on-ship contact and is convex (ShipModShips.cs:110), so a generated convex hull is the whole job. Unity cooks at most 255 polygons for a convex collider, and the generator emits no more.

Split into three cuts.

  • ships-addon-package is the core: anchors from role-tagged objects, GLB export, create --like, validate.
  • ships-addon-frame: New Ship, fit, rasterise, the grid, recentring, structural anchors.
  • ships-addon-mounts: the mount helpers, texture cap, triangle warning, CultLib revision guard, and the per-hull checklist in the authoring doc.

Each fits well under 200k Hands tokens. The core alone already makes a valid package from hand-placed objects. Revision 2 of each (2026-10-03) carries the two new rulings; the section below says what changed.

Hull materials, mount meshes and per-hull prep

Three operator rulings of 2026-10-03 drive this section.

  • hull-runtime-materials: "I expect I'll want to do quite some cleanup and prep for the meshes. The geometry is still not ideal, and we want more runtime material control than just a single baked map. See how MechWarrior does it, for example."
  • thrusters-radiators-are-meshes: "Note that both thrusters and radiators should be meshes, the former doesn't get rendered but is used as an emission surface".
  • hull-paint-mask-texture, answering question hull-paint-mask with option texture-sidecar: "I want a mask map per ship for livery, I'll be rebuilding the nonsense geometry and unwrapping anyway, I want these to stand up to my own authoring standards even if tripo does a lot of the leg work."

The prior-art survey is F:\Projects\aetheria-hull-materials-prior-art.md (Eyes, 2026-10-03). Anchors below are against 80de88f2, the tip of the in-flight eureka/aetheria-release-ships-mounts. The add-on is unchanged there since a93625c2.

Body facts

M13. Mod hulls render outside the game's ship lighting and fade.

  • ShipModVisual.LoadAsync builds new GltfImport(...) with no material generator (ShipModVisual.cs:45). glTFast's default Built-In generator therefore picks glTF/PbrMetallicRoughness for every hull material.
  • Prefab hulls paint with Aetheria/GlowFade (Assets/Shaders/GlowFade.shader; Car Paint White.mat and Cockpit.mat use it). It does two things a glTF shader does not:
    • it lights by the volumetric ambient (noambient, VolumeSampleColorSimple in vert, :35 and :67-72);
    • it dithers in and out by _Fade (:84-88).
  • EntityInstance.Awake gives each material with _Fade a per-entity instance it fades. Every other submesh is swapped to InvisibleMaterial and only shown by ShowUnfadedElements (EntityInstance.cs:86-99, :120-131). So a mod hull pops where a prefab hull dissolves, and it is lit differently.
  • Nothing tints any ship today. Faction.PrimaryColor and SecondaryColor (Corporations.cs:35-38) are read only by the sector map (SectorMap.cs).

M14. glTFast keeps meshes readable and exposes every core material slot. Read from the package source (com.unity.cloud.gltfast@11ddc2436f97):

  • MeshGenerator.cs:539 calls UploadMeshData(false), so meshes stay readable. The collider (ShipModShips.cs:103) and a particle mesh shape need that, so M10's readability doubt is closed for the importer. ships-player still checks it in a built player.
  • IMaterialGenerator.GenerateMaterial(MaterialBase, IGltfReadable, bool) sees every core slot: PbrMetallicRoughness.BaseColorTexture and MetallicRoughnessTexture, NormalTexture, OcclusionTexture, EmissiveTexture, each with index and texCoord (Schema/Material.cs:48-77, TextureInfo.cs:42-49). IGltfReadable.GetTexture(int) returns the texture.
  • glTFast decodes images only as parts of a glTF. It has no entry point for a loose PNG.

M15. Blender exports a named colour attribute exactly (probed for the rejected vertex-colour option, kept as a fact).

  • A FLOAT_COLOR corner attribute exported with export_vertex_color='NAME' becomes one exact COLOR_0.
  • 'ACTIVE' wrote every attribute, and its COLOR_0 was all ones.

M16. The Tripo hulls, measured. A read-only probe of a copy of Quiet.blend:

Hull Triangles Face-connected parts Largest part's share of faces Parts under 2% of hull length Non-manifold edges UV islands
Headliner 50,008 189 33% 23 (647 faces) 6,145 6,237
Dexter Quiet 9,614 12 87% 8 (124 faces) 524 1,386
Sinister Quiet 10,880 12 88% 7 (114 faces) 356 1,525
  • The UV atlases are confetti: 1,400 to 6,200 islands in one 4096² image. Nothing is authored in that space. The operator's rebuild gives each hull a clean UV0, and the Tripo mesh becomes a bake source.
  • K-means (CIELAB, a 512² sample) of the albedos:
    • Headliner's is colourful. With k = 6, the clusters are dark navy (43% of the texels), mid grey (21%), beige (13%), lavender (11%), orange (7%) and blue (4%).
    • Quiet's is near-greyscale (a* and b* within ±6): light panels (44% at L 69 to 79), darks and teal accents.
    • Within-cluster lightness spread is 3 to 12 L. That spread is baked shading and detail.
  • Probe scripts: kmeans.py, meshstats.py, parts.py, vcol.py and maskpng.py in this pass's scratch. Their numbers are quoted here.

M17. Thruster and radiator mounts are already meshes at runtime.

  • ShipModShips.Assemble requires a MeshRenderer under both (ShipModShips.cs:87-92, :128-137).
  • On 80de88f2, ships-mounts hides the thruster's mesh behind the template's Invisible material (:130, commit da2a299b).
  • ShipInstance.SetEntity hands that renderer to the exhaust particle system as its shape (ShipInstance.cs:68-71). Unity's documentation says a mesh shape emits along the surface normal.
  • The GLB rule is not checked before Unity. ShipModCatalog.Bind checks only that each anchor's node exists (ShipModCatalog.cs:176-180), and ship-authoring validate reads the records alone (ShipAuthoringCommands.cs:32-37).

M18. Blender saves a byte mask raw. Probe, Blender 5.2.2: a 4×4 RGBA byte image whose pixels are (0.5, 0.25, 1.0, 0.5), saved as PNG.

  • Tagged Non-Color or sRGB, it is written as 8-bit RGBA (colour type 6) with bytes 128, 64, 255, 128. The values are raw, with no transfer applied.
  • A float image saves as 16-bit.
  • The .gitattributes rule *.png filter=lfs already covers package masks.

The hull material model

Superseded in part by "Hull materials as owned graphs" below: the mask semantics, HullPaint, the shader rules and the livery resolution. Kept as the record of revision 2.

What a package carries.

  • The GLB's core PBR slots, baked by the operator onto her clean UV0:

    • base colour;
    • metallic-roughness;
    • normal;
    • occlusion;
    • emissive texture and factor.

    Missing slots fall back to factors.

  • ship.mask.png, the livery mask: RGBA8 on UV0, one per hull.

    • R, G and B are the weights of the primary, secondary and accent paint. Black is bare: the GLB's own surface shows.
    • A is wear and grime: 0.5 is neutral, toward 1 the paint wears to the base surface, toward 0 grime gathers.
    • She authors it to her standard. The add-on offers starting points: a region mask baked from albedo clusters, and a wear alpha baked from curvature and AO.
  • Blender material slots become glTF materials. These are surface kinds, such as bare metal, glass and lights, each with its own factors.

  • ShipAuthoring.Paint (key 5, a HullPaint), typed in the package:

    • Mask, the mask's relative path;
    • the factory Livery: three colours, a metallic and smoothness per slot, wear and grime;
    • RegionLuminance, each region's mean albedo luminance under the mask, derived at Package.

    Null means an unpainted hull. It renders with the GLB's look, and still fades and is lit like a prefab hull.

What the game owns.

  • One shader, Aetheria/Hull, built on GlowFade's fade, dither and volumetric ambient. It samples the mask on UV0. Its rules:
    • Painted albedo is the slot colour × saturate(lum(base) / region luminance).
    • Wear: where alpha is above 0.5 and a tiling noise is under _Wear, paint chips to the base.
    • Grime: below 0.5, _Grime darkens and roughens.
    • Painted metallic and smoothness come from the slot's finish.
    • It has no shader_feature keywords, so build stripping cannot drop a variant.
  • Mask import: linear, DXT5, with mips.
    • Linear, because the channels are weights and not colours: an sRGB decode would turn a 0.5 wear value into 0.21. M18 shows Blender writes the values raw, so the decode must not apply a transfer either.
    • DXT5 (BC3) is a quarter of RGBA32's memory: a 2048² mask with mips is about 5.3 MB instead of 21 MB.
    • Its alpha block is the best-kept channel, and the alpha carries wear. Region edges lose only within a 4×4 block, about 2 cm on a 40 m hull at 2048.
  • ShipModShips.PreloadAsync reads the PNG with File.ReadAllBytes and ImageConversion.LoadImage into a linear texture with mips, compresses it, and makes it non-readable.
  • HullMaterialGenerator, a glTFast IMaterialGenerator with one instance per package, clones ShipModTemplate.Hull with the GLB's slots and the hull's mask. No glTF shader is needed at runtime.
  • ShipModShips.ApplyPaint sets the resolved livery through a MaterialPropertyBlock in ShipInstance.SetEntity. Property blocks do not survive Instantiate.
  • Livery resolution is one pure function, HullPaint.Resolve: the owner faction's Livery (Faction key 16, cut ships-hull-livery) when set, else the factory livery. The mask and region luminance are always the hull's.

Role materials and the two mount meshes (ruling thrusters-radiators-are-meshes):

  • Thruster: a mesh carrying the invisible material. It is the exhaust's emission shape, and its normals point aft.
  • Radiator: a mesh rendered with the template's Radiator material, which EntityInstance heats.
  • Weapons stay point anchors.

Out of release scope, as follow-ups:

  • hull-decals. With a mask texture, painted-in logos can live in the mask; only separate insignia and damage decals remain.
  • hull-damage-wear.
  • hull-player-paint, unless hull-livery-scope rules otherwise.

Per-hull steps

Ruling hull-paint-mask-texture sets the time budget as hers. These are the steps and who does each.

Step The operator The add-on
P1 Tripo export Generate; try PBR and delight once (below) —
P2 New Ship id, name, reference hull, length create --like; the Tripo mesh moved into Source and oriented and scaled; a draft grid rasterised from it
P3 Rebuild Retopologise the nonsense geometry under Ship Root, at its final scale, with her own tools Rasterise again from her meshes
P4 Unwrap A clean UV0 Package refuses a mesh without UV0 or with UV0 outside [0, 1]
P5 Rebake Check and fix the result Bake From Source: Selected to Active from Source, albedo (colour only) and normal, wired into her material
P6 Mask Paint the mask to her standard New Mask; Suggest Regions; Bake Starting Mask; Bake Wear (curvature and AO into alpha); factory colours from the clusters
P7 Surface kinds Material slots for glass and lights —
P8 Mounts The faces for each thruster and radiator, the cursor for each gun Emitter normals aft; hardpoint rows on the cells beneath
P9 Layout Internal hardpoints, cell touch-ups —
P10 Package — Recentring, collider, map icon, shield, tractor, texture cap, GLB, ship.mask.png, region luminance, records, validation
P11 Look Dock, give, fly —

Dropped with the rebuild:

  • Select Debris and the mesh report: her new geometry replaces the debris.
  • The triangle-budget warning: her topology is her standard.

The texture cap stays because it bounds runtime memory.

P1, the Tripo export check: a step for the operator to try.

  • Tripo's API defaults to pbr=true, which yields base colour, metallic, roughness and normal maps. Texture version v3.5-20260815 adds delight (default true).
  • The hulls in Quiet.blend arrived with base colour only, and why is unknown.
  • A delit, PBR source makes Bake From Source's albedo cleaner and gives a metallic-roughness map to bake too.
  • Try one hull on the site and through the API. Nothing waits on it.

Cuts

Revised by "Hull materials as owned graphs" below: ships-hull-material r3, ships-addon-paint r3 and ships-hull-livery r2 replace the revisions named here.

  • ships-hull-material r2: the shader, the generator, HullPaint with Mask, the mask's PNG checks in Bind, the linear DXT5 load and ApplyPaint.
  • ships-hull-livery r1 (unchanged): Faction.Livery. It waits on hull-livery-scope.
  • ships-addon-paint r2:
    • the mask slot and New Mask;
    • Suggest Regions and Bake Starting Mask;
    • Bake Wear;
    • the UV0 and mask checks;
    • the mask export;
    • ship-authoring paint --mask.
  • ships-addon-package r2 (unchanged): validate reads the GLB; Bind refuses a thruster or radiator node without a mesh.
  • ships-addon-frame r3: Source, rasterising from her meshes, the export leaving out Source, and Bake From Source. No debris tools.
  • ships-addon-mounts r3: face-based thruster and radiator mounts on her meshes, and no triangle warning.
  • ships-player r2 (unchanged): the no-glTF/-shader check, radiator material, emitter probe. *.png is already LFS.

Order: ships-mounts, then ships-addon-package, then ships-addon-frame, then ships-addon-mounts. ships-hull-material runs beside the add-on cuts after ships-mounts. ships-addon-paint needs both lines; ships-hull-livery and ships-player need ships-hull-material.

Model page rows

Kind Identity Lifecycle Authority
Hull paint ShipAuthoring.Paint in the package Mask path and factory livery written at Package from the panel; region luminance re-derived every Package ship-authoring paint writes it; ShipAuthoringStore.Validate judges it
Livery mask ship.mask.png in the package, named by Paint.Mask Authored by the operator in Blender; capped copy exported every Package The operator; Bind judges the PNG header
Bake source The Source collection in the .blend Imported once from Tripo; never exported Input to Bake From Source and to the first rasterise only
Faction livery Faction.Livery in Aetheria.cc Authored in the inspector; null keeps factory paint The catalog
Hull material One clone of ShipModTemplate.Hull per glTF material Built at preload with the hull's mask; cache only HullMaterialGenerator

Rationale

Why a sidecar PNG loaded by ImageConversion, not a texture inside the GLB.

  • Inside the GLB, the mask would have to be referenced from extras. That means a post-export rewrite of the GLB (Blender cannot write a texture index into extras), glTFast's Newtonsoft ImportAddon to read it, and load-bearing JSON where the package's other bindings are typed. Binding by image name instead is the name-as-authority the assembler is forbidden.
  • glTFast has no loose-image entry point (M14). ImageConversion is built into the engine, so the sidecar costs one typed field, one header check and about a dozen lines of load code.
  • Blender saves the image directly (M18).

Why a generator instead of post-import material swaps.

  • glTFast calls the generator once per glTF material, with the slots already parsed.
  • Swapping afterwards would leave glTF shaders as build inputs and would decide materials by renderer.

Why the tint still divides by region luminance.

  • Her rebaked albedo may keep Tripo's colours under painted areas.
  • Dividing by each region's mean keeps panel shading at any livery colour without asking her to author a greyscale albedo. The factory livery's colours, suggested from the cluster means, reproduce the baked look.

Why the bake helpers are offered at all. Selected-to-Active needs a selection order, a cage or ray distance, the colour-only contributions, new images in the right colour space and node wiring, and that setup is repeated per hull and per map. Clustering and the curvature-and-AO alpha are starting points she paints over. Each ships-addon-* operator check asks which helper earned its place.

Why thrusters duplicate faces but radiators separate them.

  • A thruster's mesh is never drawn, so the hull keeps its nozzle.
  • A radiator's faces are drawn with the radiator material. Leaving them in the hull would draw the surface twice.

Hull materials as owned graphs

This section designs the hull material system as one piece. It replaces the mask semantics, the HullPaint shape, the shader rules and the livery resolution of "The hull material model" above. M13 to M18 and the per-hull steps stand, except P6, which is restated below.

The operator's words, in order:

  1. hull-runtime-materials: "we want more runtime material control than just a single baked map. See how MechWarrior does it".
  2. hull-paint-mask-texture: "I want a mask map per ship for livery, I'll be rebuilding the nonsense geometry and unwrapping anyway".
  3. mask-channels-place-materials: "If each channel determines placement of one material, then factions can fly their colors by setting parameters on those hull materials (primary, secondary, trim) and we'd ship eventual faction skins as their own material map... If we also want procedural wear and buildup, we can bake input maps for that, too."
  4. clean-break-from-substance: "Let's make a clean break from substance, it's dead".
  5. Not yet a ruling: "Authoring materials as graphs in blender is pretty intuitive, if we're owning material generation, could we not set things up so that we can evaluate the same graph?"
  6. Not yet a ruling: "We may not be allowed to pull blenders math into our runtime, but nothing prevents us from putting our math in blender".
  7. materials-baked-per-ship-incremental: "triggering texture bakes per ship only when they spawn or change, and then also only recomputing the parts of the graph that change, which would be just wear for most dynamic changes".
  8. shaders-own-every-pixel: "our shaders own every pixel".

Surveys: F:\Projects\blender-graph-runtime-prior-art.md, F:\Projects\aetheria-texture-graph-prior-art.md and F:\Projects\aetheria-hull-materials-prior-art.md (Eyes, 2026-10-03). Anchors are against Aetheria 239f3599 (origin/master, which contains ships-mounts and the render fixes) and CultLib a7966142. Probe scripts are in this pass's scratch (matprobe/probe_bake.py, probe_dump.py, bench/Program.cs).

Body facts

M19. The operator's Blender is 5.2.2 LTS from Steam, and Steam updates it.

  • C:\Program Files (x86)\Steam\steamapps\common\Blender\blender.exe reports 5.2.2 LTS, build hash d13f752e3b9c, built 2026-09-15. It is the only Blender in the uninstall registry. A stale 5.0.1 sits in D:\Steam\SteamApps\common\Blender.
  • Steam replaces the build when a new one ships. Node semantics changed in 5.0 (Voronoi hashing), so a silent update can move her previews away from the game.

M20. A Cycles Emit bake is an exact point oracle. Probe probe_bake.py, Blender 5.2.2, a 64×64 float input image of coordinates read with Closest interpolation into a graph, baked by Emit into a 64×64 float image (margin 0):

Probe CPU OptiX (GTX 1070)
P1 identity: the input texel reads back 4096/4096 exact 4096/4096 exact
P2 a*b + c in two Math nodes 4096/4096 equal float32 (a*b)+c; fused would differ at 1011 same
P3 CultMath's float-only mod289((x*34+10)*x) as Math nodes 4096/4096 exact 4096/4096 exact
P4 Noise Texture (3D, detail 0): two bakes bit-identical; 0.5 at integer points bit-identical
P5 an OSL Script node (In[0]*3+1) in the bake 4096/4096 within 1e-6 no result: killed after 4.5 minutes, apparently compiling OSL
  • CPU and OptiX noise agree at 2,764 of 4,096 texels, at most 1.19e-7 apart (1 ulp near 1). Arithmetic agrees bit for bit. So the oracle is CPU Cycles, and noise parity is a ULP tolerance, never bit equality, once a GPU is in the chain.
  • Cycles' CPU kernels do not contract multiply-add (P2), so float-only arithmetic is reproducible from C#.

M21. The add-on can dump a node group faithfully. Probe probe_dump.py, Blender 5.2.2:

  • Group interfaces have panels. A socket reports its panel, type, default, min, max and a stable identifier (Socket_3). Panels are typed structure, so a socket's scope (Livery, Ship, Maps) needs no name parsing.
  • ColorRamp.evaluate(t) and CurveMapping.evaluate(curve, t) run in Python, so ramps and curves export as sampled tables in Blender's own evaluation (a B-spline ramp gives 0.1667, not 0, at t = 0).
  • A group instance is ShaderNodeGroup with node_tree. Mix exposes data_type, blend_type and both clamps. Noise exposes dimensions, type and normalize. Math has 41 operations.
  • Shader nodes have no Bit Math or Integer Math in 5.2.2. FunctionNodeBitMath exists for geometry nodes only. PCG hashes cannot be built in shader nodes.

M22. A C# tape interpreter is cheap for arithmetic and slow for noise. Probe bench, .NET 10 RyuJIT on Starfire (Ryzen 3 3100, 4 cores), a scalar SSA tape interpreted over 256-texel batches at 2048²:

  • 154 arithmetic instructions: 788 ms on one thread (1.2 ns per instruction-texel).
  • Adding 24 CultMath snoise(float3) calls: 23.5 s on one thread, 7.1 s on four (about 230 ns per noise call per texel).
  • Unity runs Mono, which is slower than RyuJIT. That was not measured.

A full-resolution noise graph costs seconds per bake on the CPU. Aetheria already dispatches compute shaders at runtime (M23), where the same work takes milliseconds.

M23. Aetheria's compute and CultMath substrate.

  • Runtime compute already ships: ShieldPanel.cs:293-439 dispatches ShieldSim.compute; Stardust, Slime and Lightning have compute kernels under Assets/Shaders/Compute/.
  • Packages/manifest.json:57 pins org.gamecult.cultmath at cultmath-unity-v0.2.4. CultLib has tagged cultmath-unity-v0.3.0. Aetheria.Shared.csproj:27 compiles CultMath from source.
  • No Shader Graph or SRP package is in the manifest.

M24. Faction key 16 is taken twice. ships-hull-livery r1 puts Livery at Faction key 16. Rulings faction-relations-field and faction-doctrine-typed put Relations at 16 and Doctrine at 17. Corporations.cs uses keys up to 15. The livery takes key 18.

M25. CultMath's seams for new shader code.

  • CultMath.hlsl:509-510 includes Phacelle and Interval.
  • GlslLowering.SeparateFiles (GlslLowering.cs:19-24) lowers a separately-licensed include into its own GLSL file. Its one entry is MPL-2.0 Phacelle.
  • C# has asuint and asfloat (math.cs:390-391) but no uint2/3/4 types. Cycles' hashes are scalar uint, so they port without new vector types.

The system in brief

Blender (author, preview)        AetherDb (judge)                 Aetheria.cc
 node group "Hull Standard"  -->  material-graph put  ---------->  MaterialGraph record
 + per-hull wrapper material      whitelist, contract, cones        (truth)
                                        |
                                        v  lowering (Aetheria.Shared)
                                  CultMath expression IR  --> HLSL kernels: pair, frontier, wear
                                        |                     (generated, committed, dxc-checked)
                                        v  reference evaluator (C#, tests only)
Unity: MaterialBakes  -- dispatch per (hull, livery) and per wear set --> baked textures
       Aetheria/Hull  -- samples Surface, Worn, Wear, plus the GLB's normal, occlusion, emission

A material graph is one per-texel function:

  • Inputs:
    • image roles sampled on UV0 from two package PNGs:
      • ship.mask.png: R, G, B place the primary, secondary and trim materials; A is an authored wear hint.
      • ship.maps.png: R ambient occlusion, G curvature (0.5 flat), B cavity, A grunge.
    • parameters in two scopes. Livery parameters are set per faction, with a factory default per hull. Ship parameters are set per ship, such as wear level.
  • Outputs, a closed vocabulary:
    • Surface Color, Surface Metallic and Surface Roughness: the finished livery;
    • Worn Color, Worn Metallic and Worn Roughness: what wear reveals;
    • Wear, from 0 to 1.

Authoring in Blender.

  • A library .blend holds node groups built from whitelisted nodes. The interface panels are Livery, Ship and Maps. A Maps socket's name is a role from the closed vocabulary.
  • The per-hull material her add-on sets up is a wrapper:
    • the group instance;
    • Image Texture nodes for her mask and maps;
    • a preview that mixes Surface and Worn by Wear into a Principled BSDF, the same combination the game's shader makes.
  • The wrapper is display only. Only the group is exported.

The release whitelist is Blender's primitive and converter nodes, with Cycles semantics ported into CultMath:

  • Math (all 41 operations), Vector Math (component-wise operations), Clamp, Map Range (Float, all four modes);
  • Mix (Float, Vector, and Color with every blend mode);
  • Color Ramp and Float Curve, as tables sampled in Python;
  • Separate and Combine XYZ and Color (RGB mode), RGB to BW, Invert;
  • Value, RGB, Reroute, Frame, Group Input and Output, and nested groups (flattened).

Procedural textures (Noise, White Noise, Voronoi, Wave, Gradient) follow the release, as question release-procedural-noise proposes. Per ruling 3, release wear comes from baked maps. Grunge is baked into ship.maps.png A.

Bakes, dirty sets and the memory budget.

  • The judge computes each output's cone (the nodes it reads) and each node's scope: hull (reads only images), livery, or ship. Surface and Worn cones may not read a Ship parameter; the judge refuses that. So every per-ship cost is the Wear output alone.
  • The frontier is the set of values in Wear's cone that read no Ship parameter but feed one that does. At most four scalars, or the judge refuses the graph, so they fit one RGBA16F texture.
  • Three kernels per graph, generated from the record:
    • pair: Surface at R and Worn at R/2, for each (hull, skin, livery). R is 2048 by default and 1024 at low quality.
    • frontier: the frontier values at wear resolution, once per pair.
    • wear: only the ship-scope nodes of Wear's cone, reading the frontier, per distinct set of Ship parameters.
  • A Ship parameter change re-runs only the wear kernel. That is ruling 7: cost in proportion to the nodes that changed. A livery change is a different pair.
  • Caches are content-addressed. A pair key hashes the graph record, both PNGs, the skin, the livery bindings and R. A wear key hashes the pair key and the Ship parameter values, quantised to 1/64. Ships with identical inputs share every texture. Entries are refcounted by live bodies and released at zero.
  • Budget, at R = 2048, with BC1 (DXT1) through Texture2D.Compress after an async readback:
Texture Per Size with mips
Surface Color and Surface MR (R metal, G roughness) pair 2 × 2.7 MiB
Worn Color and Worn MR at 1024² pair 2 × 0.67 MiB
Frontier, 512² RGBA16F, no mips pair 2 MiB
Wear, 512² R8 wear set 0.33 MiB
  • That is about 8.7 MiB per pair and 0.33 MiB per ship. At R = 1024 a pair is about 2.6 MiB.
  • A zone with 20 ships in 6 (hull, livery) pairs holds about 59 MiB. Per-ship full bakes would hold about 320 MiB.
  • Each bake also needs about 70 MiB of transient upload and render textures, released once its readback completes.
  • Until the compressed copy exists, the ship samples the render textures directly.

The runtime shader.

  • Aetheria/Hull is GlowFade's surface shader: the same noambient volumetric ambient, the same dither fade by _Fade and the same edge colour.
  • The albedo, metallic and roughness are lerp(Surface, Worn, Wear). The normal, occlusion and emission come from the GLB's slots.
  • It has no paint logic, no keywords beyond GlowFade's own, and nothing evaluated per frame but that one lerp. The lerp is fixed by the output contract.

The livery.

  • HullPaint (ShipAuthoring key 5) names:
    • the graph;
    • the two PNGs;
    • the factory livery: bindings of the graph's Livery parameters.
  • Faction.Livery (key 18) holds bindings of the same parameters and wins over the factory livery when that faction flies the hull. HullPaint.Resolve is the one resolution.
  • A binding names a parameter by its interface identifier. The catalog test checks every binding against the graph: it must exist, be in scope and be in range.
  • Faction skins (another mask per hull) are a later key on HullPaint, chosen by the livery.

Authority map

  • Owner. The MaterialGraph record in Aetheria.cc owns what a hull material computes. CultMath owns what each node means: Cycles semantics ported once, in HLSL with a C# mirror. MaterialBakes owns when and at what resolution a graph is evaluated. HullPaint.Resolve owns which livery a body shows. Aetheria/Hull owns how the three outputs and the GLB slots become a pixel.
  • Inputs.
    • The judge reads the Blender dump.
    • The lowering reads the record.
    • MaterialBakes reads:
      • the generated kernels;
      • the package PNGs;
      • the resolved livery;
      • the body's Ship parameters.
  • Outputs. The record; the generated kernels; the baked textures per pair and per wear set; one property block per hull renderer.
  • Derived state.
    • The kernels are derived from the record and committed. A test fails when they differ from the lowering.
    • Baked textures are cache only.
    • The Blender wrapper and its preview are display only.
    • Cones, scopes and the frontier are recomputed by every judge and lowering.
  • Forbidden writers.
    • Unity Shader Graph, Mixture and any glTF/ shader on a mod hull.
    • Any per-frame graph evaluation in the hull shader.
    • Python encoding a MaterialGraph or HullPaint.
    • Blender's GPL EEVEE (gpu_shader_material_*) or blenlib noise code in any CultLib or Aetheria file.
    • A material, role or output chosen by a name outside the closed vocabularies.
    • A second livery resolution.
    • Faction.PrimaryColor and SecondaryColor, which stay map colours.
  • Shared paths. Editor play, the preview smoke, the play smoke and the built player all bake through MaterialBakes from the same kernels. Every AetherDb path (put, check, the catalog test) judges through one MaterialGraphJudge.
  • Deletion line.
    • r2's region tint (_PaintStrength, RegionLuminance, a tiling _WearNoise) and its Livery of three colours are never built.
    • Revision 1's vertex mask is gone.
    • The 41 .sbsar files go with follow-up remove-sbsar-files.

Parity

  1. Node semantics, in CultMath.
    • Each ported function's HLSL mirrors its C# with bit equality, through the existing mirror tests.
    • It compiles under dxc and lowers to GLSL through the existing GlslLowering. Each new licence file gets a SeparateFiles entry.
    • The C# matches a Cycles oracle fixture. A GPL-headed tool script, run only under the pinned Blender (5.2.2 LTS, hash d13f752e3b9c), bakes each family on the CPU the M20 way. It refuses any other version. The fixture records the version and hash.
    • Tolerance is per family: exact for arithmetic (M20 P2, P3), and a stated ULP bound wherever Cycles uses library transcendentals.
  2. Lowering, in Aetheria.
    • A test graph uses every whitelisted node and blend mode. The add-on's test script dumps it and bakes all seven outputs on a 64² fixture under the pinned Blender. The fixture (dump, inputs, outputs) is committed.
    • The test lowers the dump and runs CultMath's reference evaluator, then compares per output within the family tolerances.
    • This pins the node-to-function mapping. Neither side's unit tests can see that mapping.
  3. Kernels.
    • The generated HLSL is committed and must equal the lowering's output.
    • Each kernel compiles under dxc.
    • A Unity batch smoke on Starfire dispatches the fixture graph on the GPU and reads back. It compares with the reference evaluator within 2 ulp plus the family tolerance. M20 shows a GPU and a CPU Cycles already differ by 1 ulp.
  4. Drift.
    • The add-on stamps the Blender version into each dump.
    • The judge refuses a dump from a different major and minor version than the pinned semantics, naming both.
    • Updating Blender is a deliberate cut: regenerate the oracle fixtures, read the diff, and move the pin.
    • Before then, the operator stops Steam updating Blender, or keeps a pinned portable 5.2.2 for authoring.

The tape

The material evaluator is not a second tape if it owns no evaluation semantics.

  • It lowers into CultMath's scalar expression IR: SSA values, operations that are CultMath functions, named inputs, parameters and several outputs, with each instruction's input-dependence mask.
  • Two backends exist now: an HLSL emitter, and a reference evaluator for tests.
  • The tape target's step 2 adds bytecode with point, interval and gradient evaluators over the same IR. Step 4 adds an HLSL interpreter of that bytecode. Neither is built now. Ruling tape-target-unparks-when-asura-stable stands for both.
  • What lands now is the IR the tape will consume. Its point evaluator is only a test reference, not a runtime path.

A minimal evaluator is needed now, because ruling 7 requires bakes at spawn and at change, and the release hulls need faction colours (content-bar-one-per-concept).

Mod-authored graphs need the tape's GPU interpreter, because Unity cannot compile a shader at runtime. Until then a mod package uses the shipped graphs with its own PNGs and livery values: the MWO model, per the operator's ruling 3.

Question material-ir-in-cultmath asks whether this IR may land in CultMath while the tape stays parked.

Cuts

CultLib, campaign cultmath-tapes:

  • cycles-converters. The Cycles oracle tool and fixture harness. Ports of Cycles' Math, Map Range, Clamp, Mix (every blend mode), RGB to BW and ramp-table lookup, in an Apache-2.0 file pair.
  • expr-ir. The scalar expression IR, dependence masks, cones and frontier, the reference evaluator and the HLSL emitter.
  • cycles-noise. Ports of Cycles' hash, Perlin, fBM, the Noise Texture node and White Noise (BSD-3 and Apache file pairs). This follows the release.
  • The CultMath Unity release that ships the new HLSL is a release cut (follow-up cultmath-unity-material-release). Aetheria's Unity pin must move to it before hull-bakes, whose kernels include those files.

Aetheria, campaign aetheria-release:

  • material-graph. The record, the dump, MaterialGraphJudge (whitelist, vocabularies, scopes, the contract, the version pin), and AetherDb material-graph put|check.
  • material-lowering. Lowering to the IR, the generated kernels and their equality test, and the whole-graph Blender oracle.
  • material-library-export. The add-on's dumper, Export and Check calling AetherDb, and the library .blend convention.
  • hull-bakes. MaterialBakes: dispatch, the content-addressed tiers, refcounts, readback and compression, and a GPU parity smoke.
  • ships-hull-material r3. HullPaint reshaped, the PNG checks in Bind, Aetheria/Hull, the generator and ApplyBakes from SetEntity.
  • ships-addon-paint r3. The maps bake into ship.maps.png, the mask tools, Set Up Hull Material (the wrapper), and Package writing both PNGs and Paint.
  • ships-hull-livery r2. Faction.Livery at key 18, as bindings checked against the graph.

Order:

  • cycles-converters, then expr-ir, then the CultMath Unity release.
  • Then material-graph (it may start at once; it needs neither CultLib cut), material-lowering, hull-bakes, ships-hull-material r3 and ships-hull-livery r2.
  • material-library-export follows material-graph.
  • ships-addon-paint r3 needs ships-addon-mounts, material-library-export and ships-hull-material r3.
  • ships-player keeps its dependency on ships-hull-material.

What blocks the release hulls: every cut above except cycles-noise.

What can follow:

  • cycles-noise and Voronoi, Wave and Gradient;
  • CultMath-native nodes in Blender;
  • the tape's GPU interpreter for mod graphs;
  • faction skins;
  • follow-ups hull-damage-wear (the first real Ship parameter), hull-decals and hull-player-paint;
  • detail normals per material;
  • a GPU BC encoder, if the readback-and-compress hitch measures badly.

Model page rows

Kind Identity Lifecycle Authority
Material graph MaterialGraph in Aetheria.cc, keyed by its stable id (the group's name lowered, as ship ids are) Exported from the library .blend by material-graph put. Re-exported whole on change, never edited in place. The kernels are regenerated in the same command MaterialGraphJudge admits it. The record is truth and the kernels are derived
Hull paint ShipAuthoring.Paint (key 5) in the package Written at Package. A graph id, two PNG paths and the factory livery bindings ship-authoring paint; Validate, and the catalog test for bindings
Mask and maps PNGs ship.mask.png, ship.maps.png in the package, named by Paint Authored and baked in Blender; exported every Package The operator. Bind judges the headers
Faction livery Faction.Livery (key 18) Authored in the inspector; null keeps the factory livery The catalog; checked against the hull's graph
Pair bake Content hash of graph, PNGs, skin, livery and R Baked when the first body needs it; released when the last is gone MaterialBakes. Cache only
Wear bake Content hash of the pair key and quantised Ship parameters Re-baked when a body's Ship parameters change MaterialBakes. Cache only
Cycles oracle fixture A file per family in CultMath tests, stamped with the Blender version Regenerated only by a pin-moving cut The tool script under the pinned Blender

Rationale

Why generated compute kernels and not a CPU interpreter. M22 puts a noise-bearing full-resolution bake at seconds on the operator's CPU, under a faster JIT than Unity's. Aetheria already ships runtime compute (M23). The graphs are game-owned, so kernels can be generated ahead of time and compiled into the build, where a runtime shader compile is impossible. The cost is that mods cannot bring graphs until the tape's GPU interpreter exists. That is question material-evaluator-backend.

Why the per-ship cost is the Wear output alone. Prior art bounds memory one of two ways:

  • a shared base with a small per-instance mask, as in Destiny's gearstack wear mask, MW5's wear masks and Star Citizen's vertex-alpha wear;
  • a cap on instance count.

Making it a rule of the output contract, enforced by the judge, makes the budget structural rather than a convention. The lerp in the shader is the contract's one fixed line, so the shader owns no material meaning.

Why the frontier is cached at wear resolution. Ruling 7 asks that a wear change recompute only what depends on wear. Caching every node at full resolution is Mixture's documented failure: memory linear in node count. One RGBA16F texture at 512² per pair costs 2 MiB and makes a wear bake a few dozen instructions over 262k texels.

Why Cycles semantics are ported, not reimplemented from the manual.

  • The manual leaves Mix's blend modes, Map Range's smooth modes, ramp interpolation and Smooth Min unspecified (Eyes §3d).
  • The Cycles kernel files are Apache-2.0, and noise.h is BSD-3.
  • Porting them into licence-headed CultMath files, as Phacelle is kept under MPL-2.0, gives the exact semantics with provenance.
  • Ramps and curves need no port: Python samples Blender's own evaluation into tables (M21).

Why the oracle is a CPU bake. It is exact for arithmetic and reproducible (M20). A GPU bake differs by an ulp on noise, and OSL on OptiX did not finish.

Why the record holds the authored graph, not the IR. The lowering will change as ports are added and the tape arrives. The authored graph is what she made, and the IR is a cache of one lowering of it.

Rejected.

  • Unity Shader Graph: its Lit target adds Unity's ambient, and ruling 8 makes our shaders own every pixel.
  • Mixture: it adds Shader Graph and SRP Core, and its memory grows with node count.
  • Material Maker: a second authoring application with no runtime parameter path.
  • MaterialX: it has no HLSL generator, Blender's export is partial, and its noise is not Blender's.
  • OSL as an authoring route: Cycles only, no EEVEE preview, and no result on OptiX (M20 P5).
  • Region-luminance tint (r2): every channel now places a whole material.

Ship data all in the .cc

The operator, 2026-10-03, admitted as ruling ship-data-all-in-cc: "I want all the data to live in the .cc. there's no reason our schemas can't hold everything we need and it means we benefit from the cross runtime guarantees CultLib offers". It restates MQ1 (2026-09-30), "a ship .cc holds everything", which the GLB path contradicted.

This section moves the hull's geometry, node tree, surfaces and images out of ship.glb and the planned PNG sidecars into one typed record in ship.cc. It changes the package format, the Package action, the validator, the runtime loader and five specs. Anchors are against Aetheria origin/master 4895c752 and CultLib main a7966142, read on 2026-10-03.

Body facts

M26. The GLB is read in three places, and only for its scene graph and meshes.

  • The add-on exports it at Package with export_extras and export_yup (aetheria_ships/__init__.py:343-366, called at :385). The anchors and the layout already go into the .cc (:386, ship_cc.py:116-140).
  • ShipModCatalog.Bind reads only the JSON chunk's node extras and whether each node names a mesh (ShipModCatalog.cs:168-218, through Newtonsoft). AetherDb validate reaches it through ReadPackage when the file exists (ShipAuthoringCommands.cs:34-45).
  • ShipModVisual.LoadAsync imports it with glTFast and maps anchors to node indices (ShipModVisual.cs:33-60). The assembler then reads three things per node (ShipModShips.cs:85-120):
    • the MeshRenderer, for the map icon, the thrusters and the radiators;
    • the readable MeshFilter mesh, for the hull collider;
    • the shield marker's local scale.
  • glTFast has no other consumer:
    • ShipModVisual.cs is its only using GLTFast, and Packages/manifest.json:9 is its only pin.
    • ShipFixture.cs:29-85 hand-builds a GLB for the tests.
    • ShipModPreview.cs:28,125 goes through LoadAsync.

M27. No package is committed.

  • git ls-tree of 4895c752 holds no ship.cc, .glb or GameData/Mods path. Only the operator's local drafts exist, and Package landed today (7d649d5c).
  • .gitattributes puts GameData/*.cc and *.png in LFS. GameData/Mods/<id>/ship.cc matches neither rule.

M28. CultCache sets no record size limit, but the single-file store works on the whole file.

  • C#: SingleFileBackingStore reads the snapshot whole and deserializes every record (CultCache.cs:3290-3320). It writes through a temp file and File.Replace. It refuses a schema that is not registered (ToStoredDocument, :3209). So every reader of a ship file must register every record type in it.
  • Python: the MessagePack store's push pulls the whole store, replaces one envelope and rewrites the file (stores.py:103-121). A record it did not touch is carried as raw bytes.
  • The binding limits are MessagePack's bin32 (4 GiB per value) and .NET's 2 GiB array. A ship file at tens of MiB is far inside both. The real cost is that every Save Layout rewrites the whole file.
  • When Python writes a record kind first, it gets _default_catalog_entry: an empty member list (stores.py:356-368). So Python may only replace payloads of records whose catalog entry C# wrote. That matches the standing rule that Python never builds a HullData.

M29. CultLib's typed mesh is a chunk payload, not a model.

  • GameCult.Geometry has CultGeometryTriangleMesh: positions, normals, UVs, indices and a material per triangle (CultGeometryDocuments.cs:370-395).
  • It has no tangents, no submesh ranges, no node tree and no images.
  • Its fingerprint keys the chunk_artifact record, so a field added to it changes chunk keys.
  • It has no Python, TypeScript or Rust mirror.
  • GameCult.Geometry.dll is not in the CultLib Unity package (unity/org.gamecult.cultlib/Runtime/Plugins).

M30. Blender's conventions already line up with the runtime.

  • ShipPolyline stores Blender's right-handed Z-up space, and the runtime converts with (−x, z, −y) (ShipModVisual.cs:85-112).
  • Blender's UV origin and Image.pixels row order are bottom-left, and so are Unity's mesh UVs and Texture2D.LoadRawTextureData. glTF is top-left, which is why the exporter and glTFast each flip V.
  • Both facts come from the documentation. A probe in ships-cc-model proves them.

The design in brief

Blender collection                    ship.cc (one file, the only truth)
 objects, evaluated meshes  --+       HullData        mod-hull:<id>    (C# writes; Python edits layout)
 material slots, images       +-->    ShipAuthoring   mod-ship:<id>    (anchors, lines, later Paint)
 (Package, Python, raw slots) |       ShipModel       mod-model:<id>   (nodes, meshes, surfaces, images)
                              |
AetherDb ship-authoring       +-- seed-model (C# writes the record's catalog entry), validate
Unity preload: ShipModel -> GameObjects, Mesh, Texture2D (no importer)
Compose: HullData + ShipAuthoring into the derived catalog; ShipModel stays in the package

ShipModel is [CultDocument("aetheria.ship_model", "1")], stored at mod-model:<id>.

  • One record holds the whole body.
  • Its space is Blender's: right-handed, Z up, metres, UV origin bottom-left.
Key Member Type Meaning
0 Id string, [CultName] The ship id
1 Nodes List<ShipNode> Parents before children
2 Meshes List<ShipMesh> Indexed by ShipNode.Mesh
3 Surfaces List<ShipSurface> Indexed by ShipSubmesh.Surface
4 Images List<ShipImage> Indexed by the surface and paint slots
  • ShipNode:

    • Id: the node's aetheria.id, or null for an unanchored render node;
    • Parent: an index, or −1 for the root;
    • Translation (float[3]), Rotation (float[4], xyzw) and Scale (float[3]);
    • Mesh: an index, or −1 for none.
  • ShipMesh:

    • one typed array per attribute: Positions (float[3n]), Normals (float[3n]), Tangents (float[4n], w is the bitangent sign) and Uv0 (float[2n]);
    • Indices (uint[]);
    • Submeshes, each with IndexStart, IndexCount and Surface.

    Triangles only. Bounds are not stored, because every runtime computes them.

  • ShipSurface:

    • Name: the Blender material's name, for display;
    • BaseColor: an image index, or −1;
    • BaseColorFactor (float[4]).

    ships-hull-material r3 appends the slots its shader reads, such as the normal map and emission.

  • ShipImage:

    • Width and Height;
    • Format, an enum open to later members: Rgba8Srgb and Rgba8Linear now, plus the block formats if question ship-image-encoding rules them;
    • Levels (List<byte[]>): mip 0 first, rows bottom to top, tightly packed.

    An image has no role of its own: the slot that names it gives it one.

  • ships-addon-paint r3 appends Mask and Maps to ShipModel as image indices. They are not PNG files.

ShipAuthoring.

  • Key 2 (ModelAsset) is retired and never reused, as key 1 was.
  • Nothing in it names the model. The model sits at its deterministic key beside the hull and the visual, and LoadRecords already enforces the keys of those two.
  • A file that still carries key 2 loads with no model record, so it is a draft. Nothing is lost silently, because the GLB beside it was never the truth.

Why the model stays out of the derived catalog.

  • Compose rewrites Aetheria.modded.cc every boot (M28). Copying ten hulls at 30 to 80 MiB each into it would rewrite hundreds of MiB per boot.
  • So Compose keeps writing only the two semantic records.
  • PackageOf opens <modsRoot>/<id>/ship.cc read-only and takes ShipModel from it. The game reads the body from the same file the operator authored.

Writer: Package, with no GLB.

  1. AetherDb ship-authoring seed-model <ship.cc> adds an empty ShipModel at mod-model:<id> when none is there.
    • It is idempotent.
    • C# writes the catalog entry, so Python only ever replaces a payload (M28).
    • Drafts made before this cut take the same path, so no migration branch is needed.
  2. Python packs the body by slot in ship_cc.replace_model, pinned like the anchor and hardpoint slots, and pushes it. The add-on builds the body:
    • Nodes come from the bound collection, leaving out Source and Grease Pencil. A node's TRS is relative to its nearest ancestor in the collection.
    • Meshes come from the evaluated depsgraph, with modifiers applied:
      • numpy, which Blender bundles, splits corners into vertices by position, normal, tangent and UV;
      • calc_tangents runs on UV0;
      • each material slot becomes a submesh.
    • Surfaces come from the material slots, and images from the images those slots use:
      • capped at 2048 (the cap moves here from ships-addon-mounts);
      • read with pixels.foreach_get;
      • written as RGBA8 in the image's colour space.
    • The body is packed with msgpack.packb(..., use_single_float=True). Without that flag each float costs 9 bytes instead of 5.
  3. replace_visual writes the anchors only.
  4. AetherDb ship-authoring validate judges all three records.

Reader: Unity builds from records. ShipModVisual.Load becomes synchronous.

  • It makes a GameObject per node and converts TRS with the line mesh's (−x, z, −y). That conversion moves into one ShipSpace helper shared by lines and meshes.
  • For each mesh it builds a Mesh:
    • UInt32 indices above 65,535 vertices;
    • positions, normals and tangents converted the same way;
    • tangent w negated and each triangle's winding reversed, because the map is a reflection;
    • UVs passed through;
    • the mesh kept readable for the collider.
  • For each image it builds a Texture2D with LoadRawTextureData, generates mips, runs Compress, and makes the texture non-readable.
  • Until ships-hull-material r3, every surface takes one interim template material with its base colour. r3 replaces it with Aetheria/Hull.
  • These go: glTFast, UninterruptedDeferAgent, the Nodes instantiator subclass, NodeIndices and ModelPath.

Validation moves onto records. ShipAuthoringStore.Validate(hull, ship, model) is the one judge. model is null for a draft. It checks:

  • node ids are unique, and every anchor's ModelNodeId names one;
  • parents come before children, and every index is in range;
  • each thruster-emitter, radiator-mesh, map-icon or hull-collider node has a mesh. Today the first two are checked at Bind and the last two only in Unity;
  • the streams:
    • their lengths match the vertex count;
    • every float is finite;
    • every index is below the vertex count;
    • submesh ranges lie inside the index buffer;
  • the images:
    • each level is sized exactly for its format;
    • no side is over 2048;
    • each image is in a colour space its slot allows.

Bind keeps the directory and refs checks, and loses ReadNodeIds. AetherDb validate always reads records, and says "draft, no model yet" when the model is missing.

Sizes. These are estimates for Headliner (50,008 triangles). ships-cc-package measures them.

Part Raw RGBA8, mip 0 Block formats with mips
Mesh: ~30k vertices × 12 floats at 5 bytes, plus indices ~2.3 MB (9.5 MB if every triangle were split) same
One 2048² image 16 MiB 5.3 MiB (BC7 or BC5)
First cut: base colour only ~19 MiB ~8 MiB
After paint and r3: base, normal, emission, mask, maps ~83 MiB ~29 MiB
  • No CultCache limit binds here (M28).
  • What matters is that every Save Layout rewrites the file. At 80 MiB that is roughly a second in Python, against milliseconds today. That is one input to question ship-image-encoding.
  • GameData/Mods/*/ship.cc needs an LFS rule (M27). ships-cc-package adds it, so ships-player no longer has to.

Authority map

  • Owner.
    • ship.cc owns everything about a mod ship.
    • ShipModel owns its body: nodes, meshes, surfaces and images.
    • ShipAuthoringStore.Validate is the one judge of all three records.
    • C# owns every schema and catalog entry. Python (ship_cc) is a pinned writer of payloads that C# seeded.
  • Inputs.
    • Package reads the bound collection's evaluated meshes, materials and images.
    • The validator reads the three records.
    • PackageOf reads the package's ship.cc.
    • ShipModVisual reads the ShipModel record.
  • Outputs.
    • The records.
    • The prototypes' GameObjects, Mesh and Texture2D. These are cache only and rebuilt every boot.
  • Derived state.
    • Bounds, the Unity-space conversion, mips and compressed textures are derived at load.
    • The .blend is the operator's source. It never decides a semantic.
  • Forbidden writers.
    • Any GLB, glTF or PNG file in a package.
    • glTFast, or any other importer, on the mod-ship path.
    • Newtonsoft reading a package.
    • Python writing a record kind that C# has not seeded.
    • A second validator, in Python or in Unity.
    • Compose copying ShipModel into the derived catalog.
  • Shared paths. Each of these reads one ShipModel through one Validate and one ShipModVisual.Load:
    • the add-on's Package;
    • AetherDb validate and compose;
    • the Editor preview and smoke;
    • the play smoke;
    • the built player.
  • Deletion line, before the reader lands:
    • _export_glb and MODEL_ASSET;
    • ModelAsset and MODEL_ASSET_SLOT;
    • ReadNodeIds and its Newtonsoft use;
    • Package.ModelPath and NodeIndices;
    • the GltfImport path and the glTFast pin;
    • ShipFixture.Glb and GlbJson, and the GLB-shape tests (ShipModCatalogTests.cs:107-140).

Questions

ship-model-owner: who owns the model schema?

  • a. Aetheria, now, as aetheria.ship_model. It is shaped after glTF so that a later lift into CultLib is a schema move with a migration. The lift happens when a second consumer needs it (follow-up cultlib-model-schema).
  • b. CultLib, now: GameCult.Geometry gains gamecult.geometry.model, ships it in the CultLib Unity package, and Aetheria wraps it.
  • c. Extend CultGeometryTriangleMesh. That changes chunk keys (M29) and still has no node tree or images. Not recommended.
  • Recommendation: a, with medium confidence.
    • The guarantees the ruling names are CultCache's: typed envelopes, the schema catalog, slot parity across runtimes, and Python writing pinned slots. They hold for an Aetheria record exactly as they do for a CultLib one.
    • A CultLib model type would have to serve a general consumer. That means more UV sets, colours, skinning, morph targets and instancing: a glTF-sized surface, mirrored in every runtime. Aetheria needs about a sixth of it.
    • It would also put a CultLib cut and a Unity release in front of the release hulls.
  • Prior art. No engine adopts a neutral interchange format as its runtime format. Each owns a binary form fitted to its loader.
    • glTF 2.0 keeps one typed accessor per attribute and one primitive per material.
    • Unity's Mesh takes one array per attribute, and SubMeshDescriptor ranges over one index buffer.
    • Godot's ArrayMesh keeps per-surface arrays.
    • Bevy's Mesh keeps an attribute map, with U16 or U32 indices.
    • Unreal splits positions from tangents and UVs into separate vertex buffers.
    • ShipMesh takes their common subset: one array per attribute, which MessagePack types element by element, and submesh ranges.
    • These are from the engines' documentation and were not re-fetched for this pass.

ship-image-encoding: how are images stored?

  • a. RGBA8, mip 0 only. Unity generates mips and compresses at load (Texture2D.Compress, DXT5).
  • b. Block formats with mips, encoded at Package: BC7 for colour, mask and maps, BC5 for normals.
    • Unity loads them with LoadRawTextureData, with no decode and no compress hitch.
    • This needs a native encoder in Blender's Python, supplied by Brokkr: bc7enc (MIT or public domain), etcpak (BSD-3) or Intel's ISPC Texture Compressor (MIT).
  • c. PNG bytes inside the record, decoded by ImageConversion.
  • Recommendation: raw-then-bc. Format and Levels go in the schema from day one; a for the first cut; b as the release encoding, in cut ships-cc-image-bc. Confidence is medium-high. The other options are bc-now, raw-only and png.
    • b makes the file about a third the size, so every save writes less.
    • b matches the VRAM budget the material plan already assumes (BC).
    • b removes the load-time compress, which costs hundreds of milliseconds per 2048² image.
    • c puts a xenos codec inside a typed record. Its meaning becomes "whatever a PNG decoder says", not a layout every runtime reads the same way. It still pays the decode and the compress at load.
    • KTX2 is the prior art for b's shape: a format, a size and an index of level byte ranges, with block formats as ordinary formats. Unity's Texture2D and Godot's .ctex store the same thing.
  • If she rules b first, ships-cc-package takes the encoder and ships-cc-image-bc disappears.

Cuts

Cut Repo Depends on Hands budget What
ships-cc-model r1 Aetheria — ~130k The schema; seed-model; Validate on records; PackageOf reading the package; ShipModVisual.Load from records; glTFast removed; fixtures as records; the coordinate probe; the doc
ships-cc-package r1 Aetheria ships-cc-model ~110k ship_cc.replace_model; Package without a GLB; the image cap; Blender tests and the headless smoke; the LFS rule; sizes measured
ships-cc-image-bc r1 Aetheria ships-cc-package, ship-image-encoding ~90k BC7 and BC5 with mips at Package; LoadRawTextureData

The smallest first cut. The smallest step that gets her authoring onto the .cc alone is ships-cc-model followed by ships-cc-package, merged to master together.

  • ships-cc-model alone would leave the add-on writing a GLB that nothing reads.
  • No package is committed (M27), so the pair breaks nothing in the repo. The operator's drafts go through seed-model on their next Package.

Spec and question drafts for admission, bounds-checked: F:\Projects\eureka-scratch-carry\ship-cc\ (model.json, package.json, questions.json).

Specs this changes. The Superseded resolutions are Self's to admit.

  • ships-hull-material r2, superseded by r3. r3 folds this section into "Hull materials as owned graphs":
    • no glTFast generator: ShipModVisual builds materials from ShipSurface;
    • HullPaint keeps the graph and the livery, and drops the PNG paths;
    • Bind's PNG checks become image checks in Validate;
    • surfaces gain the normal and emission slots.
  • ships-addon-paint r2, superseded by r3. Package writes the mask and maps as ShipModel images. There is no PNG export and no paint --mask <path>.
  • ships-player r2, superseded by r3:
    • "glTFast's shader variants in the player" and the no-glTF/-shader check are dropped;
    • the LFS rule moves to ships-cc-package;
    • the preload log line sums image memory from the records;
    • it depends on ships-cc-package.
  • ships-addon-frame r3, superseded by r4. "The export leaving out Source" becomes "the model writer leaves out Source".
  • ships-addon-mounts r3, superseded by r4. The texture cap leaves it.
  • The material-system plan, which is not admitted:
    • hull-bakes' pair key hashes the mask and maps image bytes instead of PNG files;
    • "the GLB's slots" becomes "the model's surfaces".
  • ships-addon-package r2 needs no revision, because it has landed. ships-cc-package deletes its GLB export, and ships-cc-model deletes its GLB read in validate.

Order:

  • ships-cc-model can start now.
  • ships-addon-frame and ships-addon-mounts touch __init__.py, as ships-cc-package does. Land the pair before those two.
  • ships-hull-material r3 and ships-addon-paint r3 then build on records from the start.
  • Follow-up variants-python-raw-push gains a consumer: replace_model is a raw push, like the other two.

Model page rows

Kind Identity Lifecycle Authority
Ship model mod-model:<id> in the package's ship.cc Seeded by seed-model; rewritten whole by every Package Package writes it and Validate judges it. It is never copied into the derived catalog
Ship image An entry of ShipModel.Images, named by a surface or paint slot Captured from Blender at Package, capped at 2048 The operator's images; Validate judges size and format
Mod body prototype Hull record key Built at boot from ShipModel ShipModVisual.Load. Cache only

Rationale

Why one record and not a record per image.

  • Separate image records would need typed refs from ShipModel and from HullPaint, plus an orphan check.
  • HullPaint sits in the derived catalog and ShipModel does not, so a ref from one to the other would dangle there.
  • Every Package writes all the images anyway, and the C# store decodes every registered record either way.
  • One record has no refs to keep and nothing to orphan.

Why Python packs the body instead of handing it to C#. Any handoff from Blender to AetherDb needs a transport. A temporary file in another format would be the GLB again. Python writing slots pinned by ShipSchemaPinTests is the contract the hull layout and the anchors already use, and C# stays the one judge.

Why not keep the GLB as a cache beside the record. A second copy of the body, with its own reader, is a second owner. Nothing in the game needs glTF. The loader from records is about the size of the glTFast glue it replaces.

Ship authoring across Blender and Studio

The operator, 2026-10-03, ruling studio-is-the-authoring-pane: "I think bridging the data to a place we already control the UI is the smart compromise. Studio will need to update its schematic rendering though since we'll be shipping the stroke paths directly now." It answers her earlier ask for "a dedicated CultCache Studio pane in Blender", after the Eyes pass (F:\Projects\blender-tooling-surface-prior-art.md) found that Python cannot register an editor type. Later the same day she added: "Honestly, I don't love the fact that CultCache Studio runs in Unity. We have all this UI lowering infra (Eve, now Thing) and our best inspection tool lives in, of all things, IMGUI". Question ship-authoring-host puts that to her.

Rulings also in force here: ship-data-all-in-cc, ship-model-aetheria-owned and thrusters-radiators-are-meshes. Anchors are against Aetheria origin/master 4895c752, CultLib main a7966142 and Eve main 8e270d6, all read on 2026-10-03.

Body facts

M31. Studio draws a hull's schematic as a texture under a checkbox grid, and a mod ship has no texture.

  • InspectableSchematicShapeDrawer (Assets/Scripts/Editor/CultCacheDrawers.cs:93-146) loads EquippableItemData.Schematic as an asset GUID and draws it with EditorGUI.DrawPreviewTexture under one GUILayout.Toggle per cell. The grid's height is locked to the texture's aspect, and hardpoint cells are tinted.
  • ShipAuthoringStore sets hull.Schematic = null for a mod hull (ShipAuthoring.cs:103). So Studio shows a mod hull's cells over nothing.
  • The strokes live in ShipAuthoring.SchematicLines (key 4, ShipPolyline: points in Blender space, radii, opacities, colour; ShipAuthoring.cs:19,40-48) at mod-ship:<id>, in the same ship.cc as the hull at mod-hull:<id>.
  • A drawer can see the sibling record. CultInspector.Records is every stored document in the open store, and CultInspector.Record is a read-only copy of the one being drawn (CultLib CultCacheStudioDrawers.cs:46-49). A drawer returns a new value only for the member it draws.
  • A hardpoint's footprint is a plain Shape (ItemData.cs:563). It carries no drawer attribute, so Studio shows it with the default drawing.

M32. Neither writer guards the file against the other, and they write differently.

  • Python push pulls the whole store, replaces one envelope and rewrites the file. Records it did not touch are carried as raw bytes (M28). The add-on's replace_layout also checks a hash of the hull body (ship_cc.py:155-161). So a Blender write is safe record by record.
  • Studio's Save is _cache.FlushAsync() (CultCacheStudioWindow.cs:423-425). It rewrites every record from memory. Its only refresh is a manual Reload button (:94). There is no file watch and no stale-file check, and CultCache keeps no file version (CultCache.cs: no revision or timestamp check on the single-file store).
  • So if Studio has ship.cc open while Package writes the model, anchors and lines, its next Save silently restores the old ones.

M33. Blender already turns a Grease Pencil object into the strokes, and Line Art can make that object from the mesh.

  • Package captures the collection's one Grease Pencil object, evaluated by default, into SchematicLines (__init__.py:387-391, ship_cc.capture_grease_pencil :198).
  • Blender's Line Art modifier makes Grease Pencil strokes from mesh feature lines: contour, silhouette, crease, material border, edge marks and intersections, seen from a camera. It is evaluated through the depsgraph like any modifier. So capture_grease_pencil(..., evaluated=True) reads its output unchanged.

M34. Thing cannot carry the ship authoring surface yet. Thing is the rename of Eve (Eve/docs/thing-campaign.md). That campaign renames and publishes; it adds no primitives.

  • Component kinds (Eve/docs/surface-contract-v1.md:96-106,309-333):
    • layout: surface, grid, panel, card and similar;
    • text, image.*, graph, tree and inspector.kv;
    • controls: button, toggle, slider, stepper, segmented, colour, select and input;
    • inventory.grid, inventory.item and inventory.drag_session. These are spatial items with footprints and rotation, and drop operations whose fit is left to the provider.
  • There is no kind for vector strokes or polylines. The browser lowering's only SVG is for graph diagrams (eve-browser-lowering/src/index.ts:1653-1685).
  • There is no cell-painting kind. Occupancy can be drawn as a grid of control.toggle, which means 1,024 bindings for a 32 x 32 hull.
  • Provider side:
    • C# GameCult.Eve.Surface with CultMesh OperationBinding and state bindings (CultLib src/GameCult.Mesh/docs/getting-started/03-publish-an-eve-surface.md);
    • cultmesh-browser, a WebSocket client.
  • Lowerings (Eve/docs/renderer-parity.md:32-41):
    • The web reference renders fixtures and local advertisements. Its stated gap is "live Odin/CultMesh provider feed and command round-trip tests".
    • Flutter (Windows, Linux, Android) and iOS are screenshot parity targets on fixtures.
    • EveUnity has a live CultMesh provider path.
  • CultInspectorModel is engine-free C#, but nothing projects it into an Eve surface. The Unity window is its only lowering.

The design in brief

Blender (add-on)                         ship.cc (one file, the only truth)          Studio (host per ship-authoring-host)
 model, materials, UVs                    HullData      mod-hull:<id>  <-- cells, hardpoint values, stats   (Studio edits)
 Bake Schematic: Line Art GP  --Package-> ShipAuthoring mod-ship:<id>  <-- anchors, SchematicLines         (Blender writes)
 mount gizmos, cell overlay   --Package-> ShipModel     mod-model:<id> <-- nodes, meshes, surfaces, images (Blender writes)
 Rasterise (proposes cells)   --replace_layout, revision-checked--> HullData.Shape
 mount helpers (paired rows)  --replace_layout--> HullData.Hardpoints (type, Transform, Position)
 overlay reads mod-hull on file change (bpy.app.timers mtime poll)          Studio reloads on file change; refuses a stale Save

Who owns what.

  • Blender owns the body and everything spatial:
    • the model;
    • the strokes, baked from the model;
    • anchors and mount meshes;
    • the Position of a hardpoint that has a mount object.
  • Studio owns the hull's semantics:
    • cell occupancy after any Rasterise;
    • each hardpoint's type, footprint, rotation, armour and firing arc;
    • internal hardpoints and stats.
  • Each record has one writer, except two narrow, revision-checked Blender writes into HullData:
    • Rasterise proposes Shape, only when she presses it;
    • the mount helpers create and remove paired rows and write the mounted Position.

Strokes: Line Art, then Package's existing capture.

  • Bake Schematic creates or refreshes a Grease Pencil object Schematic in Generated. It carries a Line Art modifier whose source is the render meshes, so Source and Generated are excluded.
  • The edge types are contour, crease, material border, edge marks and intersection, seen from a top-down orthographic camera that the bake also places in Generated.
  • Package already captures the one evaluated Grease Pencil object (M33). So the bake adds no write path.
  • Two ways to change the result:
    • Edge marks: she marks a feature with Mark Freestyle Edge and rebakes.
    • Freeze Schematic: it applies the modifier so she can redraw strokes by hand. This is the Grease Pencil-assisted path, and a frozen object is marked aetheria.keep.
  • Silhouette alone loses panel lines. A hand-drawn-only path costs her time on every hull. Line Art gives the feature lines with the hand edit as a fallback.

Studio: strokes under the grid, footprints painted.

  • ShipSchematicProjection (ServerShared, engine-free) maps SchematicLines into cell space:

    • cell = Blender XY / 2 m, mirrored on both axes, plus Shape.CenterOfMass;
    • schematic +x is Blender −X, and schematic +y is Blender −Y.

    This is ships-addon-frame's grid rule, run backwards.

  • The shape drawer finds mod-ship:<id> in inspector.Records when the hull has no texture. It draws the projected strokes with Handles.DrawAAPolyLine, using each line's colour and radius, and unlocks the height.

  • A new InspectableFootprint drawer paints a hardpoint's footprint as a toggle grid with the hull's occupied cells shown around it.

  • The rules live in ServerShared, so a Thing host reuses them and replaces only the drawers.

The bridge: the shared file, with a stale-write guard and a file watch.

  • Studio's window gets a FileSystemWatcher on the open path:
    • when the store is not dirty, a change on disk reloads it on the next editor tick;
    • when it is dirty, Save compares the file's SHA-256 with the one recorded at the last load or flush. If they differ, Save refuses and offers Reload, naming the file.
  • The guard sits in Studio, not in CultCache. The operator's CultCache scope restraint (2026-10-02) keeps CultCache from growing into a database, and Studio is the only writer that flushes whole stores.
  • The add-on polls the file's mtime from a bpy.app.timers callback. Blender forbids threads, so there is no watcher thread. On a change it rereads mod-hull for its overlay.
  • Prior art:
    • Send2UE ships exported files and remote commands, not live state.
    • Multi-User replicates Blender datablocks for many editors at once. That is the wrong shape for one operator whose two tools own disjoint records.
    • CultCache's own write rules are already per record on the Python side (M32).
  • A CultMesh live link earns its place only when a provider process holds the store, which is the Thing host. Then Blender connects through cultmesh-py and stops writing the file. Until then it adds a transport and no invariant. This is not a separate fork: it follows ship-authoring-host.

Gizmos and the overlay in Blender. These go in a WorkSpaceTool "Ship Mounts" with a GizmoGroup, and replace N-panel text entry.

  • Overlay: a gpu and blf draw handler in the 3D view, at Ship Root's placement:

    • the cell grid, with occupied cells filled;
    • each hardpoint's footprint, tinted by type;
    • each weapon's firing arc as a fan.

    It is read-only and replaces ships-addon-frame r3's generated Grid object, so there is nothing left to exclude from export.

  • Mount gizmos: one per mount object (weapon empty, muzzle, thruster mesh, radiator mesh), coloured by role.

    • Clicking selects the object and names its hardpoint row in the tool header.
    • Dragging a weapon snaps it to cell centres. On release its row's Position is written through replace_layout.
    • Package re-derives every mounted Position from the object (the cell under its bounds centre). So the r3 warning becomes a derivation.
  • Studio's side: it shows Position read-only on a row whose Transform names a mount object.

  • Not built:

    • a Sprytile-style cell painter in Blender (Studio owns cells);
    • a firing-arc dial gizmo (Studio owns the arc; the overlay only shows it).
  • Deleted: the N-panel hull editor:

    • the cell checkbox grid and the hardpoint boxes;
    • AETHERIA_PG_cell, AETHERIA_PG_hardpoint and AETHERIA_PG_layout;
    • Load Layout, Resize Grid, Add Hardpoint, Remove Hardpoint and Save Layout.

Authority map

  • Owner.
    • ship.cc is the only truth.
    • HullData semantics belong to Studio's edits, judged by ShipAuthoringStore.Validate.
    • ShipAuthoring and ShipModel belong to Package.
    • Spatial placement belongs to Blender's objects.
  • Inputs.
    • Blender reads its scene and the file's mod-hull (for the overlay and the revision).
    • Studio reads the whole store, and the sibling mod-ship for strokes.
  • Outputs.
    • Blender writes mod-ship, mod-model, a proposed Shape, paired hardpoint rows and mounted Positions.
    • Studio writes mod-hull.
  • Derived state.
    • The Blender overlay is display-only.
    • Studio's stroke underlay is display-only.
    • A mounted hardpoint's Position is derived from its object.
    • The Schematic Grease Pencil object is derived from the meshes unless frozen.
  • Forbidden writers.
    • The N-panel layout editor, which is deleted.
    • A Studio Save over a file that changed on disk since it loaded.
    • Studio editing a mounted Position.
    • Blender editing a hardpoint's type, footprint, rotation, armour or arc.
    • Python computing CenterOfMass.
  • Shared paths.
    • Every Blender write to HullData goes through replace_layout and its revision check.
    • Rasterise, the mount helpers and the gizmo release write through that one path.
    • Package writes the other two records.
  • Deletion line. The N-panel layout editor is cut in ships-addon-frame r4, before any overlay or gizmo is added.

Questions

ship-authoring-host: where does the hull editor and stroke view run?

Context:

  • Thing cannot carry it today (M34). It has no stroke kind and no cell-painting kind.
  • No lowering has proven the live CultMesh round trip.
  • Nothing projects CultInspectorModel into a surface.

Prior art: the Language Server Protocol and Jupyter's kernel and front-end split. In both, one process owns the model and the editors are interchangeable clients. This is the target shape doctrine names. Both reached it after a working single-host editor existed.

  • a. thin-imgui-now-thing-next.
    • The Studio drawers above, kept thin: every rule lives in engine-free ServerShared (ShipSchematicProjection, the footprint rule) and in CultInspectorModel.
    • The Studio disk guard.
    • A Thing host for Studio as its own campaign after the release, with the missing pieces named here: a stroke kind, a cell-paint kind, the browser live loop, and a CultInspectorModel-to-surface projection.
    • About 160k Hands tokens now, in two cuts. The IMGUI added is about 150 lines of drawer.
  • b. thing-now.
    • An Aetheria provider: AetherDb, which already owns ship-authoring, holds ship.cc and publishes a surface over CultMesh.
    • New contract kinds schematic.strokes and cells.paint, in the contract, the browser lowering and conformance fixtures.
    • The browser live-loop proof.
    • A CultInspectorModel projection limited to the field kinds HullData uses.
    • Blender connects through cultmesh-py.
    • About 5 cuts, roughly 600-800k Hands tokens, across Eve, CultLib and Aetheria. Hull authoring waits on all of it, ahead of the release.
  • c. thing-now-minimal.
    • As b, but no new kinds:
      • the provider renders the strokes server-side as an image asset;
      • cells are a grid of control.toggle.
    • About 3 cuts, roughly 300-400k Hands tokens.
    • It leaves 1,024 bindings per hull and a stroke view that cannot zoom crisply. The Thing campaign would then rebuild that surface.

Recommendation: a, confidence medium-high.

  • The release needs hull authoring now. Studio already edits every HullData field.
  • The IMGUI cost is held to one drawer file, because the rules sit where a Thing host will read them.
  • b is the right destination, but it would put three unproven pieces (two new kinds and the live loop) on the release's critical path.
  • c builds a surface that b would then throw away.

Depends on: nothing. The Blender cuts are independent of this answer.

Cuts

Spec Repo Depends on Hands budget What it does
ships-studio-schematic r1 Aetheria ship-authoring-host ~90k ShipSchematicProjection; strokes under the grid; the InspectableFootprint drawer; tests on the projection
studio-disk-guard r1 CultLib ship-authoring-host ~70k File watch, auto-reload when clean, stale-Save refusal by SHA-256, in the Studio window; a CultLib release
ships-addon-schematic-bake r1 Aetheria — ~70k Bake Schematic (Line Art and the ortho camera in Generated), Freeze Schematic, its own module; smoke
ships-addon-frame r4 Aetheria ships-cc-package ~150k r3 with the .cc change, the N-panel layout editor cut first, the Grid object replaced by the cell overlay, Rasterise kept
ships-addon-mounts r4 Aetheria ships-addon-frame ~110k r3 without the texture cap and without internal-hardpoint editing; rows are pairs only
ships-addon-gizmos r1 Aetheria ships-addon-mounts ~120k The Ship Mounts tool, mount gizmos, the footprint and arc overlay, mounted Position derived on release and at Package, Studio's read-only Position

Admitted on 2026-10-03, along with question ship-authoring-host: ships-studio-schematic r1, ships-addon-schematic-bake r1 and ships-addon-gizmos r1. Two batches wait on Self, bounds-checked, in F:\Projects\eureka-scratch-carry\studio-pane\:

  • pending-after-ships-cc-package.json: ships-addon-frame r4, ships-addon-mounts r4 and their Superseded resolutions of r3. Admission refuses r4 until ships-cc-package, which it depends on, is admitted.
  • pending-after-cultlib-in-campaign.json: studio-disk-guard r1. Admission refuses it because GameCult/CultLib is not a repo of the campaign. Self adds the repo (a campaign revision) or routes the cut to a CultLib campaign.

Specs this changes.

  • ships-addon-frame r3, superseded by r4.
    • It folds in the .cc section's change: the model writer leaves out Source.
    • The Grid object becomes the overlay, and the layout editor is cut first.
  • ships-addon-mounts r3, superseded by r4.
    • It folds in the .cc section's change: the texture cap leaves.
    • "The layout panel stays the editor" becomes "Studio is the editor".
    • Add and Remove Hardpoint for internal types leave Blender.
  • ships-cc-model (draft). It should mark ShipModel's bulk arrays [CultInspectorHidden] and show their counts read-only. Otherwise Studio, opening ship.cc, tries to draw millions of floats in IMGUI. Self amends the draft before admitting it.

Order against the .cc move on 2026-10-06.

  • Before the 6th:
    • ships-studio-schematic and studio-disk-guard, once ship-authoring-host is ruled. They read only HullData and ShipAuthoring.SchematicLines, both on master.
    • ships-addon-schematic-bake. It lives in its own module with a one-line registration, so its merge with ships-cc-package's __init__.py edits is trivial. It feeds Package's existing capture.
  • After ships-cc-package lands: ships-addon-frame r4, then ships-addon-mounts r4, then ships-addon-gizmos. Each builds on ShipModel-in-.cc, not on the GLB.
  • If she rules b or c, ships-studio-schematic and studio-disk-guard are withdrawn and the Thing cuts are mapped then. The Blender cuts do not change.

Rationale

Why Studio owns semantics and Blender owns space.

  • The operator's ruling puts editing where GameCult controls the UI.
  • What Blender knows better than any grid editor is where things are on the mesh. So placement is the one hull value Blender writes, and it is derived from objects, not typed.

Why Line Art and not silhouette edges or a custom edge extractor.

  • Line Art is Blender's own feature-line generator. Its output is the Grease Pencil object that Package already captures.
  • A Python edge walker would re-implement occlusion and chaining, and own them forever.

Why the guard sits in Studio, not in CultCache.

  • Python already writes safely record by record.
  • Only a whole-store flush from memory can clobber. The editor that does that flush owns the check.

Rejected.

  • A Blender fork with a native editor (the Eyes pass, section 2).
  • A Sprytile-style cell painter in Blender: a second editor of cells.
  • A CultMesh link before a provider holds the store.

S3: the first playable mod ship in a built player

  • First hull: Headliner.
    • It is a single hull. Quiet comes as two variants (Dexter and Sinister), and MQ2 A makes them two full packages.
    • Its 50k triangles exercise the heavy path: collider generation and texture memory.
    • It is Lucent's duel ship (ruling lucent-duel-bait), so faction-play-2 will want it in Lucent flights.
    • Its mesh and texture are already in Quiet.blend, so no new GLB is needed.
    • The operator authors it with the add-on, because placing the mounts needs her eyes. She commits the package with the add-on cuts' tooling.
  • ships-player does the agent-side work, and does not wait on the hull:
    • the template radiator material;
    • glTFast's shader variants in the player;
    • one preload log line with hull count, shader names and texture memory;
    • LFS rules for packages (M11);
    • GameData/Mods staged into the player;
    • the C3 sim test;
    • the SetEntity probe;
    • the built-player boot smoke on Starfire, with the skiff fixture and a bad package.
  • What it needs to build a player is question s3-player-build (M10).
  • The operator's S3 session is gate 4, gate 5 (S4) and C2 to C6: Headliner in a built player, about fifteen minutes.

S5 and the variants campaign: what this strand needs, and when

  • Nothing before release for S5 itself.
    • The bar is 3 shipped prefab hulls plus 10 package hulls (content-bar-one-per-concept). The prefab hulls keep working through the Prefab branch.
    • Packages are complete hulls (MQ2 A), so no family needs variants.
    • S5, which migrates the shipped hulls, retires the FBX builder and collapses families, waits on variants C3 and C4, and stays theirs.
  • Before the operator's CultLib checkout moves past the C2a merge:
    • Python must still write a raw record into a single-file store. ship_cc pushes HullData and ShipAuthoring envelopes (ship_cc.py:108,163), and parity-py rewrites that write path.
    • Follow-up variants-python-raw-push asks parity-py to keep that path or name its replacement, with tools/blender/tests as its consumer check.
    • ships-addon-mounts adds a guard: the add-on refuses an untested cultcache_py revision (M12).
  • When C4 brings element ids to Aetheria, they arrive on ShipAnchor, HardpointData and ShipPolyline.
    • ship_cc already carries hardpoint slots past FiringArc, keyed by mount id. ships-addon-package does the same for anchors, keyed by anchor id.
    • Lines are replaced whole, which Q7 allows.
    • Key 32 (HullData.Visual) is taken, and C4 must not reuse it.
    • The lane's own S5 text puts hull families "in Aetheria.cc". MQ1's confirmed reading puts a family in its ship's .cc. S5's owner should resolve which one holds before C4 maps it.
    • This is follow-up c4-ship-element-ids.

Model page rows

Kind Identity Lifecycle Authority
Ship package GameData/Mods/<id>/; <id> is ShipAuthoring.Id, lower-case and stable across renames. Created by ship-authoring create, edited daily in Blender, validated by C#. Removing it makes Continue refuse runs that used it (MQ4). The package's two records. The .blend in Asset Sources/ is the operator's source and never the owner of semantics.
Hull record mod-hull:<id> in the package. Stats seeded from a reference hull at create; layout written by Blender's Save and Package. HullData in the package: cells, hardpoints, stats. How it gets a manufacturer is question release-hull-home.
Anchor ShipAnchor.Id, equal to the mount id for mounts and to the GLB node's aetheria.id. Written whole by Package from role-tagged objects. Unknown tail slots are carried by id. Blender objects tagged aetheria.role propose; the C# validator decides.
GLB Retired by "Ship data all in the .cc": the body is ShipModel at mod-model:<id>. Was re-exported by every Package; ships-cc-package deletes the export. None. ShipAuthoring key 2 is retired.
Derived catalog Aetheria.modded.cc in the persistent data path. Recomposed every boot, and disposable. ShipModCatalog.Compose. Never authored.
Mod prototypes Hull record key. Built at boot, kept across scenes, gone at exit. ShipModShips.Preload. Cache only.

Rationale

Why the operator checks move to the S3 session.

  • They need a mod ship in a zone, which today means the console and a docked player (M5). Done in the Editor with the skiff fixture, they prove the wiring on a tetrahedron.
  • Done once in a built player with Headliner, the same fifteen minutes also give gates 4 and 5, which are owed anyway.
  • The merge is safe without them because the path is inert with no package installed.

Why mounts before the add-on. The add-on writes anchors. If it learned articulation as the weapon role, the first real package would need migrating when the ruling's code landed. ships-mounts also collapses the role checks into the validator (M4), so that the add-on's Validate button and compose refuse the same things.

Why the add-on calls AetherDb instead of growing a Python validator. The doc's shared-path claim is true only for C# (moddable-ship-authoring.md, correction 4). A second validator in Python would be a second semantic authority. A subprocess call costs a few seconds per Package and keeps one owner. The same reasoning makes create --like a C# command: HullData has some 30 slots that Python would otherwise copy by hand.

Why generated structural anchors. The collider, map icon, shield and tractor are functions of the mesh's bounds and silhouette. Hand-making them per hull is the work that M8's hand-scaled icosphere already shows. Generating them, with a "keep mine" flag, removes about half of the per-hull steps without taking a decision from her.

Why first-party hulls through the package path (the recommendation in release-hull-home). In KSP, RimWorld, Factorio and Starsector, the base game's content uses the same format and the same loading path as mods:

  • KSP stock parts live in GameData/Squad, beside mod folders.
  • Factorio's base game is the base mod, read before every other mod and merged into one prototype table.
  • Starsector mods put their ships into an existing faction's fleets and markets with their own .faction file. The engine merges that file into the core faction's known-hull lists, so the package carries its own availability.
  • The RimWorld claim is from memory and was not re-fetched.

Dogfooding the mod path means the release exercises the path players' mods will take. A package that carries its own product needs no edit to Aetheria.cc.

Sources:

Lanes: scenarios, mining and the loot move

The operator ruled on 2026-10-03 that this campaign adopts codex/scenarios and codex/mining from where they stand ("Adopt mining and scenarios", ruling adopt-mining-and-scenarios). This section maps:

  • both adoptions;
  • the loot move into ServerShared, which jettison rests on (ruling jettison-shared-floating-items);
  • the merge order of every lane and the faction-play cuts;
  • the re-map of faction-play-4 to ruling ballistics-real-ammo.

The cuts are typed specs in the mind: scenarios-adopt, mining-index, mining-index-pins, mining-target-queries, mining-merge, mining-index-tree, mining-belt-cells, loot-1 to loot-3, ballistic-ammo and faction-play-4 r2. The forks are questions fastblast-ammo, reticle-exactness and belt-size-tiers.

The lane tips were read with git show in a scratch clone on 2026-10-03:

  • codex/scenarios is at aa3baf12: 30 ahead of master and 20 behind, base 702b454b.
  • codex/mining is at 83d8371e: 32 ahead and 21 behind, base 65c63495.
  • Everything master gained since either base is Scenarios map: doc commits.

The operator's records for both lanes live in docs/scenarios-cut.md on master: the Soul passes on scenarios batches 2 to 4 (lines 72-140), and the mining Cut 3 Soul and belt-freeze rulings (lines 830-876).

Body facts

L1. The scenarios blocker is real, latent, and wider than one lookup.

  • Soul's batch-4 probe P30 exists only in a dead session scratchpad (...\F--Projects-CultLib\2a6aec4d-...\scratchpad\soul5\SoulScen5Probes.cs, log probesC.log). At aa3baf12 it returned:

    • (a) An L-shaped gun in an L-shaped hardpoint equips at origin (1, 1). Hardpoints[origin] is NULL, while the hardpoint found by the item's cells is the right one. ArcFor returns the default 120 instead of the authored 360.
    • (b) A design with an empty leading column: ItemFits(-1, 4) is true but TryFindSpace is false, and ArcFor throws IndexOutOfRangeException.
    • (c) Over the catalog with that design, 380 placements are accepted. In 9 the origin is outside every hardpoint, in 7 it is inside another hardpoint (a radiator), and 14 offsets are never tried by TryFindSpace.
  • 18 readers take an item's hardpoint from its origin cell (Hardpoints[item.Position.x, item.Position.y]):

    • FireControl.ArcFor (:38);
    • eleven Unity weapon managers;
    • EntityInstance.cs:245,260 and ShipInstance.cs:70;
    • ActionGameManager.cs:1044,1048,1049.

    This is a grep at aa3baf12. The pattern in scenarios-adopt's negative check matches exactly these 18.

  • Entity.HardpointAt (Entity.cs:820-835) already finds the right hardpoint at equip. TryEquip (:904-935) throws that answer away.

  • The defect is latent. A catalog probe at aa3baf12 with catalog 70a7b0a9 (an xUnit probe run through ygg-verify, job aeth@aa3baf12e4, 2026-10-03) found:

    • every catalog hardpoint is a full rectangle;
    • the only gear designs whose shape lacks its origin cell are the Vulcan reactor and the Industrial Thermostatic Heater (Tool).

    So nothing in the shipped catalog trips it today. The first non-rectangular hardpoint or mount would.

  • The "held fix" was never written. The fix batch was queued and not dispatched under the 2026-10-01 drain (scenarios-cut.md:105-106). P30's harness survives only in that scratchpad, so scenarios-adopt commits its fixtures as tests.

L2. Merge-tree probes between the lane tips (git merge-tree --write-tree --name-only, 2026-10-03):

Pair Conflicts
master x scenarios none
master x mining none
scenarios x moddable-ships AetheriaStores.cs (CatalogTypes, line 9)
mining x moddable-ships AetheriaStores.cs, tools/AetherDb/Program.cs
scenarios x mining AetheriaStores.cs; Zone.cs (one region: scenarios' MineAsteroid against mining's deletion); GameData/Aetheria.cc; FireControlCut124Tests.cs
  • The scenarios x mining set matches the merge rule recorded in scenarios-cut.md S4 (lines 133-140).
  • Only mining changes Settings.asset (347752dd to b2e346f5, MiningDifficulty removed), so it merges without a conflict.

L3. The belt freeze's cause is mapped (mining Cut 3 Soul F1). Historical: the scan, ChunksNear and ActionGameManager.TargetCandidates were deleted by mining-index and mining-target-queries; see "Mining index: as built".

  • Every reticle, next or previous press builds ActionGameManager.TargetCandidates (:1174 at 83d8371e).
  • That calls Entity.VisibleChunksInReach (Entity.cs:377-385), which calls Zone.ChunksNear (Zone.cs:326-339).
  • ChunksNear loops over every rock of every belt whose annulus reaches, posing each rock and then testing its visibility.
  • The handlers then sort the whole list by distance and search it with Array.IndexOf (:393-407).
  • Soul measured 63 ms at 30k rocks, 449 ms at 300k and 4.3 s at 3M. At 3M, 633k rocks are in reach and 2,240 are visible.

L4. The operator's index ruling, and the visibility bound.

  • The operator, verbatim: "Just don't specialize the indexing too much towards asteroid belts, because we'll want a bunch of subsystems feeding targeting data" (scenarios-cut.md:871-875).
  • Detection of a rock is the settled sensor rule (Q14 A): ChunkInfo = saturate(sum_i v * s_i * curve_i(angle) / d / (n k)). The sources are Sensor.Gain (Sensor.cs:187-199) and Entity.ChunkInfo (Entity.cs:355-368).
  • The rule is linear in visibility v and falls as 1/d. A Bezier curve lies inside the hull of its control points. So for a region with a known maximum visibility and a known nearest distance, an upper bound on ChunkInfo is cheap to compute.
  • Rock visibility is CrossSection * pi * cells^2 * light (Zone.ChunkVisibility, :314-321).
  • Together these let a provider prune whole regions without deciding detection.

L5. Hands' unbuilt work in progress (superseded by Targeting/ on master; the patch was not adopted) is F:\Projects\HANDOFF-mining-index-wip-2026-10-01.patch: 257 lines against 83d8371e's Zone.cs.

  • It groups rocks into bands of 256 by orbit distance, each sorted by orbital phase at a key time.
  • It widens the query arc by the band's rate spread, re-keys at most 8 bands per query, bounds the light per band, and counts ChunksExamined.
  • Its structure is belt-shaped (ChunksNear(position, range, distancePerVisibility) on Zone), and its queries still build eager lists.
  • Kept: the kinetic bands. Not kept: the belt-shaped API and the eager queries.

L6. Nothing of the loot move has landed, on master or on any lane.

  • git grep for Zone.Loot, TryPickUp and LootDrop over every local and remote ref found nothing in code.
  • On master f1dee184, the loot path is all Unity:
    • the roll uses UnityEngine.Random.value and onUnitSphere (EntityInstance.cs:302-321);
    • pickups are GridObjects, moved by Unity (GridObject.cs);
    • collection is ShieldManager.OnCollisionEnter (ShieldManager.cs:40-55);
    • the tractor beam pulls by Physics.SphereCastAll (TractorBeam.cs:14-30).
  • No lane touches these hunks. Mining edits EntityInstance.cs:236,410 and ZoneRenderer.cs:72-483, away from the loot lines. Moddable-ships edits ZoneRenderer.cs:289-304.

L7. Today's loot tuning, read from the assets.

  • GameSettings.cs:14-16: PickupLifetime 30, LootDropProbability .25, LootDropVelocity 25.
  • All five pickup prefabs (Assets/Prefabs/RPG/Pickups/*.prefab) carry the same GridObject values: Drag .05, LaunchDrag .2, Gravity 1, GridAttraction 10, RotationSpeed 1.
  • Tractor Beam.prefab: Radius 25, Traction 25, Distance 75.
  • GridObject also moves mines (Mine.cs:12, MineManager.cs:21-32).

L8. Fork L already decides the pickup, and the Tractor Beam has no capability yet.

  • Fork L was ruled by the operator on 2026-09-17 (headless-playground-cut.md:924-970):

    • pickup is a simulation-owned timed grab: extend, envelop, pull;
    • no Unity collision decides a pickup;
    • the tractor beam is a generic Tool item that provides only pickup;
    • the simulation owns the grabbed object and its pose;
    • GrabEvent names the object by a typed simulation identity;
    • loot is a body plus an item instance.
  • Two readings are therefore not the design:

    • option (a) of that section, "contact detection stays Unity collision", which was recommended before the ruling;
    • the brief's "Zone.TryPickUp from ShieldManager".

    The jettison ruling's own words agree: jettisoned cargo "can be picked up without unity's involvement".

  • Scenarios rulings of 2026-09-30 (scenarios-cut.md:46-56): picking up requires a pickup behaviour, which the Tractor Beam carries, and every generated ship and the player's starting ship carry one.

  • The catalog probe (L1's job) found the Tractor Beam as GearData on a Sensors hardpoint, with no behaviours and one product. Nothing in the simulation can grab today.

  • The presentation seam exists, but grabs nothing real:

    • GrabEvent carries an int TargetHandle (CapabilityEvents.cs:53-67);
    • CapabilityPresentationBinder resolves it to a live Transform through delegates (:16-45), and only FieldTester supplies them.

L9. The ammunition substrate, by catalog probe (same job, catalog 70a7b0a9):

Hardpoint Weapon Behaviour Magazine Ammo Energy
Ballistic 6k Shooter Instant 6 none 5
Ballistic Autocannon Auto 50 Ammo 5
Ballistic ClearPath Auto 0 none 1..0.5
Ballistic DeathCluster Auto 8 Ammo 25..5
Ballistic Earp Instant 6 none 5
Ballistic pretty pretty bang bang Charged 8 Ammo 25..50
Energy FastBlast+- Auto 12 Ammo 0
Energy ChargeBlast SG, ChargeBlast+-, ColdFire, CShot RainbowLite Lazer, plight, Spectra mixed 0 or 1 none 0 to 150
Launcher GT 3K, LRMM72, pswarm, scorched void policy, SRMM72 mixed 2 to 288 none 0.1 to 50
  • The catalog has one ammunition commodity, Ammo, at price 1000.

  • One cargo unit is one magazine. InstantWeapon.UseAmmo removes 1 unit per reload and refills MagazineSize rounds (InstantWeapon.cs:176-202). ConstantWeapon does the same (:129-160). So faction-play-4 r1's "AmmoMagazines x MagazineSize rounds" over-stocked by a factor of MagazineSize.

  • UseAmmo returns true at once when MagazineSize <= 1 (:178), so a one-round gun fires free even with an AmmoType.

  • Firing energy reaches a weapon through the power bus:

    • PowerBus.cs:122 takes every active IPowerConsumer;
    • an item's PowerSupply is its tier's grant ratio (:196-205), which is 0 below a starved tier;
    • so a weapon that requests nothing still reads its tier's ratio, and a brownout still reaches it.

    That is why ballistic-ammo takes ammo-fed weapons off the bus instead of zeroing their request.

  • ChargedWeapon's charge is time-based (_charge += dt / ChargeTime). Only firing spends the capacitor, so bypassing the spend does not stall a charge.

Model page rows

Kind Identity Lifecycle Authority
Equipped item's hardpoint EquippedItem.Hardpoint, a reference into the hull's Hardpoints list; null for Tool gear. Set once at equip from the placement rule, and derived again on every load (load re-equips). Never stored. Entity.HardpointAt, through TryEquip. No reader derives it from the origin cell.
Targeting index Zone.Targets, one per zone. Built with the zone. Providers register as belts are built and entities admitted. Rebuilt on load; never saved. TargetingIndex for candidate sets and bounds; each provider for its own motion; Entity for detection.
Belt band (provider region) Belt key plus band index; arc segments by key order. Derived from AsteroidBeltData, PlanetSettings and time. Re-keyed lazily, a bounded number per query. Cache only. mining-belt-cells replaces it with rings and cells. BeltTargets.
Belt ring and cell (after mining-belt-cells) Belt key, ring index, cell index. A ring is a run of rocks by orbit distance; its cells are equal sectors of a frame turning at the ring's mean rate. Derived when the belt is built, never saved. A ring is refreshed (its rocks re-bucketed) when its drift passes one cell, within a rock budget per query. BeltTargets.
Target choice The TargetRef a key press writes. Made per press; no list is kept between presses. Entity (reticle, next, previous, nearest), through SetTarget.
Floating item FloatingItemId: a zone counter, never reused in a zone. Released at death (and later by jettison); drifts; held during a grab; removed into cargo by TryPickUp, or at FloatingItemLifetime. Not saved: gone when the zone unloads, as Unity loot is today. Zone: release, step, expiry and commit.
Loot dice Zone CombatSeed, loot stream, death ordinal. One generator per death, discarded after the roll. Zone.DropLoot, on SimulationDice. Never UnityEngine.Random, never a shared stream.
Grab Holder entity plus FloatingItemId. Started, Extend, Envelop, Pull, then Completed or Cancelled. One holder per item, one grab per Pickup. The Pickup behaviour owns the timeline; Zone owns the held pose and the commit.
Pickup capability PickupData (BehaviorData union key 40) on the Tractor Beam design. Authored by an AetherDb one-shot, then edited in the catalog. Every generated ship carries one. The catalog author.
Ammunition An AmmoType commodity; one unit is one magazine. Stocked at generation and in station inventories; consumed one unit per reload; moved by TryTransferItems when rearming. Cargo. WeaponData.DrawsRounds, derived from AmmoType, decides reactor or magazine.

Merge order

Each lane becomes a eureka/aetheria-release-* branch; nothing is pushed to codex/*. In order of landing on master:

  1. ships-merge. Its spec exists; ruling ships-merge-on-agent-proof.
  2. scenarios-adopt.
    • Master (with ships) is merged into scenarios first. The only conflict is AetheriaStores.cs:9. The blocker fix follows.
    • Scenarios goes before mining for two reasons: its remaining work is one small cut, and mining's merge rule was written to resolve scenarios' side.
  3. mining-index, then mining-index-pins, then mining-target-queries, on the mining branch. They touch only mining's own code, so they run in parallel with steps 1 and 2. The pins land first, so the same tests guard Best when mining-target-queries gives it callers.
  4. mining-merge. Master (with ships and scenarios) is merged into mining under the recorded rule, and the field kinds are applied again to scenarios' catalog.
  5. feedback-1 and audio-1 may land at any time: their hunks are disjoint from all three lanes (B10). feedback-1 must land before loot-2.
  6. loot-1 to loot-3, on one branch from master after step 4, merged together. They wait for step 4 because mining's belt code rewrites Zone.cs, and the loot cuts edit Zone's constructor and Update.
  7. ballistic-ammo, after step 4: it needs scenarios' LoadoutGenerator and mining's Weapon.cs. It is independent of the loot cuts.
  8. faction-play-1 to -3, re-anchored first (follow-up faction-play-reanchor). faction-play-1 rewrites lines both lanes changed, so it starts only after step 4:
    • mining's targeting lines: Minion.cs:14-21, Combat.cs:28, and Target as a TargetRef written through SetTarget;
    • scenarios' admission lines: Zone.Admit.
  9. faction-play-4 r2, after faction-play-3 and ballistic-ammo.
  10. Jettison, then pirate collection, on Zone.Release and the grab, after step 6.
  11. mining-index-tree, then mining-belt-cells, at any time after step 4, on a branch from master. They gate nothing, neither the merge nor the release (ruling targeting-sublinear-wanted-not-critical). They inherit the tests of mining-index-pins and mining-target-queries, and tighten the cost pins.

Mining index: as built (master df7c44f2)

Status, read this before L3 to L5 and the cuts above. They are the plan and the pre-cut Body. eureka/aetheria-release-mining merged to master as df7c44f2. Landed: mining-index (cut-mining-index.h1), mining-index-pins (cut-mining-index-pins.h1), mining-target-queries (cut-mining-target-queries.h1 and .h2) and mining-merge (cut-mining-merge.h1). Not landed: mining-index-tree and mining-belt-cells. L3's scan, L5's patch and the belt task machine no longer exist.

Owner, by question.

  • Which things can lie in a region, and how visible at most: TargetingIndex (ServerShared/Targeting/TargetingIndex.cs), one per zone as Zone.Targets. It names no belt, rock or chunk. It owns region ordering and reach.
  • Where rocks are and which regions bound them: BeltTargets, one provider per belt, registered in Zone's constructor beside EntityTargets.
  • Is this target detected, and which one does a press pick: Entity (ChunkVisible, VisibleEntities, TargetUnderReticle, TargetNext, TargetPrevious, TargetNearestEnemy), through PickTarget and Eligible.
  • Who writes the choice: Entity.SetTarget, the one writer.
  • Rock wear and respawn: Zone's wear store (ChunkWear, BrokenUntil). Belts have no task, timer list or update thread: EvaluateBelt computes poses on demand for rendering, and BeltTargets derives bounds from time.

Inputs and outputs.

  • TargetSearch in: searcher position, reach, optional observer, and ReachPerVisibility (Entity.DetectionReachPerVisibility, from sensor gain bounds and the curve's hull).
  • Providers out: TargetRegions (distance interval, bearing interval, visibility ceiling) and TargetCandidates (a TargetRef and a planar position).
  • Index out: Within returns every candidate within exact reach whose region can be detected, unordered. Best takes an ITargetKey (Bound, Key, Precedes) and returns the least-key eligible candidate: regions are sorted by bound and opened until a bound exceeds the best key.
  • Entity presses: AngleKey (reticle: least planar angle to the look direction) and DistanceKey (next and previous: the successor or predecessor in (distance, target) order, wrapping, with the held target as pivot). Ties order a ship before a rock, then by chunk field and index.
  • Entity.VisibleChunksInReach keeps its contract (visible chunks within mining reach, by index then field) on Within.

Derived state, never saved. The index and every provider's bands, keys and Rekeys count are derived from AsteroidBeltData, PlanetSettings and zone time and rebuilt with the zone. Bands re-key lazily, at most 8 per query. Examined is a diagnostic counter, not state. No candidate list survives a press. The only persisted belt state is ZonePack key 6 (ChunkWear).

Forbidden writers.

  • Unity code picks no target. ActionGameManager binds four input actions to the four Entity methods and does nothing else (ActionGameManager.cs:389-392). It builds no candidate list, sorts nothing and calls neither Within nor Best.
  • Nothing enumerates a belt's rocks to answer a targeting question. The only loop over a belt's rocks outside BeltTargets is Zone.EvaluateBelt (rendering).
  • The index decides reach and nothing about detection. A provider's bounds must hold for every thing in the region; a provider that loosens them costs speed, one that tightens them past a real rock drops targets.
  • Only SetTarget writes Target; presses return false and keep the target when no candidate exists.
  • No UnityEngine.Random or shared stream in target choice (it has none).

Shared paths. Reticle, next, previous and nearest-enemy presses; VisibleChunksInReach; fire control's use of Target (TargetRef); the AI's Minion and Combat target writers (F6, still SetTarget); load (Zone rebuilds providers).

Verification layer. tests/Aetheria.Shared.Tests/MiningIndexTests.cs: index equals brute force (WithinIsExactlyTheBruteForceSet, BestIsTheBruteForceNearest), region bounds hold per rock (BeltRegionBoundsHoldForEveryRock, ACurveStaysWithinItsHullRange), no visible rock dropped, cost pins (Examined, DarkBandsAreSkippedWhole, ReKeyingIsBoundedPerQuery, RegionsStayNearTheSearchArc, VisibilityBoundPrunesADimBelt) and ARockExactlyAtReachIsFound. MiningTargetQueriesTests.cs: press rules against a brute-force order, perception (ShipsTheObserverHasNotDetectedAreNeverPicked), tie orders, empty presses, and per-press cost ceilings (AKeyPressDoesNotGrowWithTheBelt, PressPruningKeepsEachPressNearItsMeasuredCost). Both are partial MiningCut3Tests and compile only ServerShared (B11). The Unity side was compiled separately: a Unity 6000.3.24f1 batchmode compile at a83aa54e on 2026-10-06 (mining-merge-unity) exited 0 with no error CS in any of the five assemblies. Both ships smokes passed, and it needed no fix commit, so the cut has no report. Stryker and tools/mutation-table.sh covered Entity.cs spans fully; survivors in BeltTargets and TargetingIndex are equivalent or float boundaries.

Live seams and debt.

  • EntityTargets is registered on every zone but no production query reaches it with an observer: Entity.PickTarget offers VisibleEntities itself, and MiningSearch carries no observer. The index owns no ship answer today. Finding orphaned-index-surfaces, deferred to mining-index-tree.
  • Entity.VisibleChunksInReach has tests as its only callers; Within has no production caller.
  • A press still costs O(rocks in a fixed belt): Best enumerates and sorts the flat region set. Deferred to mining-index-tree (hierarchy, heap, Accepts split) and mining-belt-cells (rings and cells); findings cut-mining-index-tree.r1 and cut-mining-belt-cells.r1 carry them. Neither gates the release.
  • The index comment headers (TargetingIndex.cs:10, Zone.cs:26, both test files) cite docs/aetheria-release-map.md, which exists only on eureka/aetheria-release-target, not on master.

Why the press still grows. At a4e8e147, after mining-target-queries, a press is one Best query. Its cost has two linear terms in the rock count of a fixed belt:

  • Region enumeration. Best asks every provider for its whole flat region set, bounds each region and sorts the set, before opening one. A band holds a fixed 256 rocks, so at 3M rocks a band is 0.05 units wide and a full ring. A 400-unit search crosses about 8,000 of them, and each contributes its arc segments: 30k-64k regions, 38-61 ms.
  • Sliver regions. A segment of 32 rocks in such a band is a 0.05 x 470-unit sliver. A sliver near the searcher has a small bound but holds rocks spread along hundreds of units, so best-first opens many slivers for few useful rocks.

The floor. The press queries are exact: the smallest angle to the look direction (reticle), and the successor in (distance, target) order (next, previous). In linear space these are thin-cone and thin-ring range queries. Any tree of cells must open every cell that the cone's ray or the ring's circle crosses: about the boundary's length over the cell size. That is O(sqrt(n)) for n rocks in reach, the kd-tree line-query bound (Lee and Wong 1977), and no linear-space structure beats it for simplex ranges (Chazelle 1989). The target is therefore sqrt(n), not flat. A flat cost needs an approximate answer (question reticle-exactness).

Target growth. In a fixed belt, regions opened plus rocks examined per press grow as the square root of the rock count: at most 4.5x per tenfold rocks (sqrt(10) is 3.16), against 10x at a4e8e147. Visibility tests per press (ChunkVisible, the expensive part) stay within 2x per tenfold. Building stays O(n) per belt, and a refresh costs O(ring).

Proof by measurement. Soul's F1 fixture: rocks 300..900 about the sun, light radius 1000, kind cross-section 5, sensor Eye, guns 150 and 400.

  • Run it at 30k, 300k and 3M rocks, with the eye at chunk 0 + (5, 5), at (310, 0) and at (600, 0).
  • Presses: five TargetNext from no target, one TargetPrevious from no target, and TargetUnderReticle looking along the belt and across it.
  • Record Opened, Examined, Tested and time. Repeat after +300 s (stale rings) and after +1e7 s.
  • The suite pins 30k against 300k. The 3M point is a Soul probe, too slow for the suite.
  • Expected at 3M, with 16-rock cells about 3.5 units across: low thousands examined and hundreds of regions opened per press. Today it is 37k-108k examined and 30k-64k regions.

The design. The index owns search order. The belt owns rock motion and cell bounds.

  • mining-index-tree (index only). The provider seam becomes a hierarchy, Roots and Open. Opening a region yields child regions, candidates, or both.
    • Best keeps a binary heap of regions by their bound (netstandard2.1 has no PriorityQueue). It stops when the next bound exceeds the best key. This is Hjaltason and Samet's incremental distance browsing, now over a tree.
    • Within walks the tree depth-first, pruning with CanHold.
    • ITargetKey splits into a cheap geometric Key and an expensive Accepts (detection). Accepts runs only for a candidate that would beat the best so far.
    • New diagnostics beside Examined: Opened (regions opened) and Tested (Accepts calls).
    • BeltTargets and EntityTargets are adapted mechanically: today's flat regions become roots that open into candidates. Answers and costs do not change.
  • mining-belt-cells (belt only). BeltTargets is rebuilt as rings of square cells.
    • Rocks sorted by orbit distance are cut into rings. A ring closes once its width reaches its cell arc (2 pi r x 16 / count), so cells are about square at any density. A ring also closes at a cap of 8,192 rocks, which bounds one refresh.
    • Each ring turns at its mean rate. Its cells are equal sectors of that turning frame, about 16 rocks each, held as one index array with cell offsets.
    • A rock drifts against its frame by at most the ring's rate half-spread times the time since the ring was refreshed. That drift widens every cell's sector, so the bounds stay sound without touching a rock.
    • When the drift passes one cell, the ring is refreshed: every rock is re-bucketed by its current turn in one O(ring) counting pass, with no sort.
    • A refresh happens only when a ring node is opened, before any of its cells exist in the query, so a query never sees a rock twice.
    • Each query refreshes at most 16,384 rocks. A ring left stale is searched through its wider sectors, which stays correct.
    • The tree is implicit: a balanced range tree over ring indices (annulus bounds), whose ring leaves open into halving cell ranges (sector bounds), down to single cells. Today's Sector bounds and light bound carry over.
  • Drift in cell units grows at about pi r |d rate / d r| per second, whatever the density.
    • With period = distance, a ring goes stale every r / pi seconds: about 190 s at r = 600.
    • Refresh work per unit time is O(n) per belt, amortised over the presses that touch it.

Prior art, cited from memory and not re-fetched:

  • Hjaltason and Samet, "Distance browsing in spatial databases" (ACM TODS 1999): the heap-ordered search that Best becomes.
  • Šaltenis, Jensen, Leutenegger and Lopez, "Indexing the positions of continuously moving objects" (SIGMOD 2000), the TPR-tree. Its bounds widen with elapsed time by a velocity bound and are tightened on update. Here that is the drift-widened sector and the refresh.
  • Basch, Guibas and Hershberger, "Data structures for mobile data" (SODA 1997). An event-driven kinetic order is rejected: every pair of rocks with different rates eventually swaps, so events grow as the square of a ring's size.
  • Arya, Mount, Netanyahu, Silverman and Wu, "An optimal algorithm for approximate nearest neighbor searching in fixed dimensions" (JACM 1998). A cost flat in density is reachable only for an approximate answer (question reticle-exactness).
  • Space games: EVE's belts are static, and Elite Dangerous generates ring rocks per cell around the player. Neither simulates per-rock orbital shear, so they bear on the visual budget, not on this index.

Forks.

  • reticle-exactness: keep the reticle exact at sqrt(n), or accept an angle tolerance for a flat cost. Next and previous stay exact either way, because cycling must visit every target once.
  • belt-size-tiers: give each size class its own provider, so small rocks are pruned at their shorter detection reach. This is a constant factor on Tested, not a growth fix.

Rationale

Why every item carries its hardpoint, rather than keeping the origin inside it. Soul named both fixes.

  • Keeping the origin inside the hardpoint forbids legal placements. An L-shaped gun could never fill an L-shaped hardpoint whose corner is empty, because the gun's own origin cell is that corner (P30 (a)).
  • Looking the hardpoint up by the item's cells at every reader would put the placement rule in 18 places.

The placement rule already knows the answer at equip. Keeping that answer deletes 18 re-derivations; it adds nothing.

Why the index is generic and the belt is a provider.

  • The operator ruled it (L4).
  • The seam is the smallest that carries both providers in the tree today and the ones the operator named, EW and microfauna. A provider yields regions with a distance interval, a bearing interval and a visibility ceiling, and the candidates for a region.
  • The index owns no motion model. A provider for orbiting rocks, one for moving ships and one for a slime field each keep their own.
  • Prior art, cited from memory and not re-fetched:
    • best-first incremental search over a region hierarchy: Hjaltason and Samet, "Distance browsing in spatial databases" (ACM TODS 1999);
    • kinetic sorted orders for moving points: Basch, Guibas and Hershberger, "Data structures for mobile data" (SODA 1997).

Why the index and the key-press rules are two cuts.

  • The index alone removes the 633k-rock scan, but leaves the handlers sorting every visible rock (2,240 at 3M) on each press.
  • The operator's rule is that a press must not grow with belt size. Only best-first answers meet it. After Soul's first pass on the index, the operator accepted the remaining growth with rock density (see below).
  • Together the two are about 900 lines with tests, over one Hands budget. The seam between them, the index's query API, is where Soul can falsify each.

Why the index fix before the merge is only pins, and the cells come after.

  • Soul (verdict cut-mining-index.s1) measured the index at a4e8e147. It is correct, and the 3M-rock press fell from 4.3 s to 150-600 ms.
  • Press cost still grows linearly with rock count in a fixed belt (finding f1-press-linear-in-density).
  • The operator ruled that this is not critical, but that the improvement is wanted (ruling targeting-sublinear-wanted-not-critical, which supersedes targeting-density-scaling-accepted): "O(sqrt(n)) is way better than O(n), my ruling says it's not critical, not that it's undesired".
  • So the merge waits only for mining-index-pins: three tests for findings cost-loosening-unpinned and exact-reach-unpinned, which the suite could not see.
    • The share of regions past reach catches a widened arc window or a loosened Nearest.
    • Examined against an exact per-rock bound catches a loosened visibility bound.
    • A rock exactly at reach, searched from beyond its belt's outer edge too, catches a slack sign slip.
  • mining-target-queries r1 stands as specced. Its AKeyPressDoesNotGrowWithTheBelt scales area at constant density, which both rulings leave in force.
  • The sublinear structure is the section "Mining index: rings, cells and best-first search" above, as two cuts after mining-merge.

Why loot is three cuts on one branch, merged together.

  • The ruled design (L8) needs three things:
    • floating items in the simulation (loot-1);
    • the grab as a capability, with its catalog entry and generation (loot-2);
    • the presenters (loot-3).
  • Together they are about 1,000 lines.
  • Merging after loot-1 alone would ship a game where loot cannot be collected, because loot-1 deletes the Unity collection that the ruling retires.

One branch keeps master playable, and three cuts keep each Soul pass small.

Why floating items are not saved. Today's Unity loot is not saved either, and a floating item lives 30 seconds. Saving them would add a ZonePack field and a migration for a window shorter than a save cycle. Jettison may want longer lifetimes; its cut can revisit this with a reason.

Why the loot dice are the zone's, keyed by death ordinal.

  • Fire control's Cut 6b rule is that a roll belongs to the thing rolled for, not to a shared stream, so a UI or another system drawing first cannot change it.
  • The loot roll follows the same rule with its own stream, so it cannot disturb shot dice either (test LootDiceDoNotTouchShotDice).
  • The mixing function moves to one owner (SimulationDice) instead of being copied. The owner-level fix is CultMath's (follow-up cultmath-mixed-seed).

Why ammo-fed weapons leave the power bus. By L9, a weapon on the bus reads its tier's grant ratio even when it requests nothing. A request of zero would still let a brownout silence a gun whose energy comes from its rounds. Off the bus, its supply is 1 and the reactor cannot touch it. That makes the ruling's trade-off literal: energy weapons compete for reactor power, and ballistic weapons compete for cargo.

Why one ammunition commodity.

  • The catalog has one (Ammo).
  • The mechanism is data (AmmoType per weapon), so a split by calibre is a catalog edit that needs no code.
  • A tender carrying one commodity is also the simplest rearm loop to prove.

This is a default, not a ruling. The operator can ask for calibres at any time.

Why mining's Q6a should be asked again. Mining ruled that ore is lost when a hold is full (Q6a A), because pickups were Unity-only (mining-cut.md:286-287). After loot-1, overflow could float through Zone.Release like any other loot. Follow-up mining-cuts-4-5-7 carries the question to that map.

Controls

The control rebuild of ruling facing-separate-from-aim, which ruling demo-controls-rebuild puts before the demo. The pilot steers the hull's facing with its own input, the cursor aims, fixed mounts fire when the aim is inside their arc and turrets track. Articulated mounts (follow-up articulated-mounts, ruling arc-and-traverse-on-the-link) are full game; this section keeps every arc read behind the seam they will plug into. Anchors are against origin/master 228f241e, read in a detached scratch worktree on 2026-10-06.

Prior art

Sources are marked: primary (the game's own source, manual or developer post, read in full), wiki (a community wiki page read in full), snippet (a search-result summary only, not read at source; treat as unverified).

  • Starsector (wiki: the fandom Piloting page, read through the MediaWiki API; the Fractal Softworks manual PDF could not be fetched). WASD is ship-relative: A and D turn, Ctrl-A and Ctrl-D strafe. Holding Shift makes the ship "automatically point towards the mouse"; a setting makes that the default. The mouse aims the selected weapon group, which tracks the cursor and fires on the left button. Shift+1-5 puts a group on autofire: those weapons pick and attack their own targets, and the HUD marks the group with a filled square. R designates the ship under the mouse, and weapons "preferentially fire at the target". The wiki's own advice is to put everything except missiles on autofire so that the mouse governs only shields. That advice is the community's answer to the core failure of a cursor-aimed, arc-limited model: holding the cursor inside a narrow arc while manoeuvring is hard (snippet, forum). Vanilla draws weapon arcs, and a popular mod redraws them (snippet).
  • Endless Sky (primary: source/Preferences.cpp and PreferencesPanel.cpp on master). Fixed guns fire along the hull; turrets track. Player options: "Automatic aiming" off / always on / when firing (default when firing), "Automatic firing" off / on / guns only / turrets only (default off), "Aim turrets with mouse", "Turrets focus fire" (default on), "Control ship with mouse", and "Turret overlays" off / always on / blindspots only (default blindspots only). So the arc display defaults to showing only where the guns cannot reach.
  • Escape Velocity Nova (primary: the Nova Bible, weapon Guidance field). Keyboard turning; fixed guns fire ahead. Guidance 7 is a "front-quadrant turret" that can fire plus or minus 45 degrees off the nose and "fires straight ahead if no target"; guidance 8 is the rear quadrant. A designated target pulls a limited-arc turret's rounds onto it, and without one the mount fires down its axis. This is the closest precedent for Aetheria's designation-based fire control.
  • Cosmoteer (primary: developer blog, "Cosmoteer 0.14.11 - Arcade Style Ship Controls"). Direct Control: W and S thrust, A and D rotate, Q and E strafe; the mouse aims and fires. Turreted weapons fire on the left button and fixed weapons on the right by default, rebindable per weapon. Rotating to face the cursor is an opt-in setting.
  • Nebulous: Fleet Command (snippet). Weapons engage designated targets on their own within their arcs; ships auto-orient to bring mounts to bear. Its known failures are arc failures: narrow-arc guns idle while the ship searches for an angle that brings every turret to bear, and turrets report "target masked by hull".
  • Battlestar Galactica Deadlock (snippet). Broadside, bow and stern batteries have about 90-degree arcs; dorsal and ventral guns cover 360 degrees with limited elevation. Arcs show when a turret is clicked in the radial menu, and a key toggles them on the selected ship. Players asked for more visible arcs (a Steam thread title).
  • SubSpace and Continuum (snippet). Arrow keys rotate, up thrusts, guns fire along the nose: the pure nose-aim model this ruling retires.
  • World of Warships (snippet), added because Aetheria's camera is a chase camera, not top-down (body fact C7). The mouse orbits the camera and aims; the keyboard is the helm. Guns traverse toward the aim at a turret traverse speed. The classic complaint is "the guns won't fire though the view looks on target": the turret has not finished traversing or cannot bear, and the reticle's gun markers carry that state.

Failure modes to design against.

  1. "My guns don't fire": the aim or target is outside a mount's arc, or a turret has not traversed yet, and nothing on screen says so (Starsector forum, Nebulous, World of Warships). Answer: the HUD shows each group's state where the eye already is (the group crosshair and the arc fan), and per-group autofire exists for players who would rather manoeuvre.
  2. Holding the cursor inside a narrow arc while also flying the ship is hard (Starsector). Answer: a hold-to-face modifier for nose guns, and autofire for side guns.
  3. Over-eager auto-orientation fights the player (Nebulous ships hunting for an angle). Answer: the player's hull never turns itself; only an AI hull turns to bring a group to bear, and it chooses one group.
  4. Arcs drawn everywhere become clutter. Answer: draw only arcs below 360 degrees, the Endless Sky "blindspots only" default.

Body facts

  • C1. The input layout. Assets/Resources/Aetheria.inputactions, map Player: Move (Vector2: W/S as y, A/D as x, gamepad left stick), Look (pointer delta, right stick), and Turn (1D axis, Q negative, E positive), which no script reads. Fire is the action bar: ActionGameManager.cs:101-103 binds the left, right and middle mouse buttons to action-bar slots, and ActionBarWeaponGroupBinding.Activate (ActionBarSlot.cs:189-195) activates every weapon in its group. No Shift binding exists in the Player or Global maps.
  • C2. The player's steering is the aim. ActionGameManager.cs:1277-1281: the pointer delta (cursor locked, :648) integrates _entityYawPitch, which becomes _viewDirection, which is written to CurrentEntity.LookDirection. :1290 writes Move to ship.MovementDirection (x strafe, y thrust).
  • C3. LookDirection is the facing command. Ship.Update (Ship.cs:278-291) sets deltaRot = dot(look, right), shapes it sqrt(|d|) * sign(d), feeds it to the rotation thrusters and the aether drives' Axis.z, and below |d| < .01 writes Direction = lerp(Direction, look, ...) directly. So aim and facing are one field.
  • C4. Turning is kinematic. No angular velocity exists. A thruster rotates Entity.Direction by input * Torque * Thrust * TorqueMultiplier / Mass * dt (Thruster.cs:106-107); an aether drive by force.z * axis.z * AetherTorqueMultiplier / Mass (AetherDrive.cs:154-155). Settings.asset (LFS): TorqueFloor 0.5, TorqueMultiplier 0.1, AetherTorqueMultiplier 0.1. Ship.TurnTime (Ship.cs:61-66) estimates a turn. Turn rate is therefore torque over mass times one global multiplier.
  • C5. FireControl's arc. ArcFor (FireControl.cs:36-41) is HardpointData.FiringArc (ItemData.cs:569, key 6) when above zero, else GameplaySettings.FiringArc (Settings.cs:214, initializer 120; the field is absent from Settings.asset, so 120 holds). InArc (:50-58) compares a planar bearing with MountDirection (hull Direction rotated by the item's rotation); 360 or more passes, and a bearing under 1e-6 passes. AimDirection (:116-126) clamps LookDirection to the arc's nearer edge. The Turret hull's hardpoints author 360 (AetherDb firing-arc-migrate, Program.cs:1037).
  • C6. The trigger gate reads the target, not the aim. ArcPermitsFire (:76-88): with no target it passes; with a target it tests the target's bearing, and a fused weapon passes anyway (operator ruling 2026-09-30, fire-control-cut.md:2628-2640). PFire (:256-270) prices a shot at zero out of arc. Solve (:440-473) flies a round to the target's intercept when it is engaged (TravelDirection, :396-400), and along AimDirection otherwise. So the cursor today decides only facing, reticle targeting (Entity.TargetUnderReticle, Entity.cs:407-412), no-lock fused flight, the lock cone (LockWeapon.cs:100), the tractor (ShipInstance.cs:116), guided rounds' aim point (GuidedProjectileManager.cs:79) and loot's view direction (ZoneRenderer.cs:440).
  • C7. The camera is a chase camera that looks at the aim. FollowCamera follows the ship and looks at EntityInstance.LookAtPoint, set each frame to ship position plus LookDirection times the target range, or 10,000 with no target (EntityInstance.cs:401-402, ActionGameManager.cs:1039-1040). The scene's two framing transposers (ARPG.unity:4963, :21276) position the camera and do not rotate it, so the camera's heading comes from the aim and survives the split. Which of the two belongs to FollowCamera was not resolved.
  • C8. What the HUD draws. UpdateTargetIndicators (ActionGameManager.cs:1390-1422): ViewDot at LookAtPoint; one crosshair per articulation group at the mean of its barrels' forward rays (_articulationGroups, :1041-1057, grouped by the barrel's ArticulationPoint.Group); the target indicator; lock indicators. Update fills the target panel's bars and UpdateFireControlDebug (:1319-1380) prints the fire-control gates as text, including arc {d.InArc}. No arc, no lead marker and no predicted path is drawn.
  • C9. The barrel picture has its own arc. ArticulationPoint (Unity) slews a barrel toward Target (every point's target is LookAtPoint, EntityInstance.cs:278-283) at Speed and clamps yaw to YawMin..YawMax, authored by ShipPrefabAuthoring.cs:141-148 and unrelated to FiringArc. Package hulls have none (ShipModShips.cs:164): their weapons are points, so every package-hull weapon lands in group -1 and one crosshair.
  • C10. AI writers treat LookDirection as facing. Agent.Accelerate (Agent.cs:59-79) writes it to turn toward the velocity error. Combat (:98-128) calls Accelerate(noTurn: true) and then writes LookDirection = toTarget (the intercept), which turns the nose to the target. MoveTo.cs:26 writes it. TurretController.cs:74-77 writes it; a turret hull has no rotation thrusters, so it only aims. Faction-play r1's new FollowState "looks at the anchor" and FleeState flies through Accelerate, so they inherit the same assumption; their r2 (follow-up faction-play-reanchor-demo) must use the helm port below.
  • C11. The Arcs scenario assumes nose aim. Scenarios/Arcs.cs: forward mounts, a bare hull off the bow and one off the stern, a hostile 360 turret. Its check "nothing fires at the stern one through the ship" could only be reached by turning, because turning the view turned the hull. Checklist item 11 (merge-to-master-checklist.md:49) also wants side mounts firing abeam.
  • C12. Weapon groups persist. Entity.WeaponGroups (Entity.cs:69) is saved as EntityPack key 16 (EntitySerializer.cs:189) and Loadout key 3 (Loadout.cs:19).

Authority map

  • Owner. Ship owns facing: Ship.Update turns the hull from one demand, Ship.Turn (-1..1, positive clockwise), and from nothing else. Entity.Aim (LookDirection renamed) owns where the pilot points. FireControl owns every mount question: its arc (ArcFor), its axis (MountDirection), whether its trigger is free (ArcPermitsFire), and the one direction its next round flies (MountAim, new).
  • Inputs. The player's Turn axis and hold-to-face button, the pointer delta, the AI states' chosen headings, the hardpoint's FiringArc and item rotation, the designated target.
  • Outputs. Hull rotation through the thrusters and drives; the trigger gate; each round's flight direction; the facts the HUD draws.
  • Derived state. Steering.Toward(ship, heading) derives a turn demand from a heading (the C3 law, moved). The barrel picture, the group crosshairs, the arc fans, the lead marker and the predicted path are display-only derivations of FireControl and Ship.Coast.
  • Demotions. LookDirection is no longer an owner of facing; it is renamed Aim and steers nothing. ArticulationPoint is no longer an owner of arcs; its yaw derives from MountAim. The barrel transforms are no longer the source of the group crosshair.
  • Forbidden writers. Any write of Entity.Direction outside the thrusters, the aether drives, wormhole exit, load and staging (the lerp snap in Ship.Update goes). Any read of Aim in Ship.Update. Any read of FiringArc outside FireControl.ArcFor, hardpoint validation and AetherDb. Any arc, bearing or intercept arithmetic in Assets/Scripts/Gameplay or UI.
  • Shared paths. The player and every AI state reach the hull only through Ship.Turn; player and AI rounds leave through the same MountAim; the HUD, the barrel picture and Solve read the same MountAim.
  • Deletion line. Before any new behaviour: the facing block of Ship.Update (Ship.cs:278-291) is replaced, the LookDirection field is renamed so that every old writer fails to compile until it chooses aim or helm, and ArticulationPoint's yaw clamp is deleted.

Model page rows

Kind Identity Lifecycle Authority
Turn demand (Ship.Turn) Per ship, runtime only. Written every frame by the player input or the ship's agent; never saved. The pilot (input or agent), through Steering.Toward for headings.
Aim (Entity.Aim) Per entity, runtime only. Written every frame by the player's view or the agent; never saved (as LookDirection was not). The pilot.
Mount aim Derived per weapon from aim, target, arc and mount. Computed on read. FireControl.MountAim.
Gun solution Derived per weapon from its subject, arc, mount and aim. Computed on read. FireControl.Solution.
Face-the-aim state Per player session, runtime only. Held by Left Shift, latched by Caps Lock; never saved. HelmInput.
Arc-display preference PlayerSettings player store. Edited by a toggle; persists. The player.

Rationale

Why rename LookDirection. A rename makes every old writer stop compiling until someone decides whether it meant aim or helm. Keeping the name would leave AI states that mean "turn there" compiling as "aim there", silently, which is exactly the split-brain the rebuild removes. The Gameplay and test call sites are mechanical.

Why a turn demand rather than a desired heading. The player's keys command a rate; the AI and the hold-to-face button command a heading. A demand is the narrower port: a heading converts to it through one function (Steering.Toward), and a rate needs no conversion. A heading port would force the keys to invent a far-off heading.

Why the cursor gates and the target lands. The ruling's words make the aim the trigger gate: a fixed mount fires when the aim is inside its arc. FireControl's lock model, its hit pricing and its tests are built on a designated target. Question controls-target-pulls-rounds asks whether a designated target inside the arc pulls the rounds and turrets onto it (Escape Velocity's front-quadrant turret), or whether rounds always fly along the clamped aim, which would reopen FireControl's pricing.

Why traverse is not simulated in the demo. Traverse speed is a stat of the moving part (ruling arc-and-traverse-on-the-link), and that model is full game. MountAim is stateless now; articulated mounts make it stateful behind the same signature, and every caller (Solve, the trigger, the HUD, the barrel) already reads it, so nothing else moves.

Why the predicted path is a coast. It answers "where does this hull go if I let go", which is the question gravity wells and slower turns make hard. It reuses the hull's own drag and gravity step (extracted to Ship.Coast), so the line cannot disagree with the simulation. Thrust is not projected: it changes every frame.

Cut order. controls-helm first: the split itself, and the rename forces every writer to be ported in the same cut. controls-mount-aim second: the trigger and the one round direction (revised by "Firing on solutions" below into the per-gun solution). Then controls-hud and controls-ai-bearing, which are independent of each other. controls-autofire was withdrawn under ruling fire-on-solutions.

Firing on solutions

Revision of 2026-10-06 after the operator's rulings on this section's questions: controls-target-pulls-rounds (target-pulls), controls-facing-input (A/D turn, Q/E strafe, Left Shift held or Caps Lock toggled faces the aim), controls-turn-inertia (kinematic, tuned) and the direction fire-on-solutions: "Fire control should surface a firing solution for any gun that's in arc, and the player should be able to fire on any solution without aiming." The aim stops being the only trigger gate. Question controls-autofire was withdrawn with it. Anchors are against origin/master e1296f1f, whose code is unchanged from 228f241e.

Prior art: firing without manual aim

Source marks as above.

  • Elite Dangerous (wiki: the fandom Category:Weapons page, read in full through the MediaWiki API). Three mounts of every weapon trade effort for output. Fixed mounts are the strongest and shoot straight ahead with a few degrees of convergence. Gimballed mounts track a locked target inside a cone and "act as a Fixed mount", firing straight ahead, when nothing is locked. Turrets cover 360 degrees and take one global mode: Forward Fire (manual, as fixed), Target Only (track and fire on the locked target inside their arcs once the trigger is pulled) and Fire at Will (any hostile that is firing at you). Chaff breaks gimbal and turret tracking but not fixed mounts. So assisted fire is paid for in damage and is deniable by countermeasures, and manual aim keeps its value.
  • MechWarrior Online and 5 (snippet). Weapons sit in numbered groups on triggers; a group can chain-fire. Lock-on weapons (LRMs) cannot fire without a lock, which a teammate or your own sensors must hold; Artemis shortens the lock.
  • Nebulous: Fleet Command (snippet). Weapons are tasked on designated targets; ships turn to bear. WCON "Free" lets untasked weapons return fire on any contact that attacks the ship; "Tight" fires only on command; Hold Fire keeps the orders. Shift groups like weapons into batteries such as fore and rear.
  • Battlestar Galactica Deadlock (snippet, earlier pass). Batteries fire on ordered targets within their arcs; arcs show on demand.
  • Freelancer (snippet). The reticle leads the target, and fixed and turret guns are all slaved to the mouse crosshair; the lead cross is the aim aid.
  • Starsector (wiki, earlier pass). Autofire groups pick and attack their own targets, preferring the designated one.

Failure modes.

  1. Solutions nobody can read in a fight: a marker per gun per contact swamps the screen (Elite's Fire at Will turrets spraying at whatever shoots, Starsector's autofire picking its own targets). Answer: solutions are drawn per weapon group, against one subject, on the group crosshair the player already reads.
  2. No skill left: if every gun fires itself on everything, the player only steers. Answer: the player still designates, chooses which group to fire and when, and steers the arc onto the target. A solution needs targeting data (FireControl's Designated and reveal tiers), so a contact you cannot see well gives no solution and free aim keeps its value, as Elite's chaff keeps fixed mounts valuable.
  3. Guns that silently refuse (the old "my guns don't fire"): a group whose trigger is held and does nothing. Answer: each group crosshair shows one of three states, and the arc fans use the same colours.

Body facts

  • C13. A solution type already exists. FireSolution (FireControl.cs:1809-1824) is Solve's per-shot answer: outcome, PFire, Designated, the engaged entity and the travel direction. HitProbability (:316-324) is the one live price, and Inspect (:331) is its presentation-only breakdown. PFire (:256-270) holds the two gates a solution needs: Designated (visible, in range, locked) and InArc of the target.
  • C14. A target is one entity per shooter. Entity.Target holds one TargetRef (an entity or a chunk); reticle, nearest, next and previous are its only player writers (ActionGameManager.cs:389-392).

The design

  • A solution is per gun and has one owner. FireControl.Solution(Weapon, Entity shooter) answers, for one gun: the subject it is drawn against, whether it bears (the subject is Designated and inside this mount's arc, the same two gates PFire already applies, now one extracted function Bears that PFire also calls), and the direction its round would fly (MountAim: the subject's intercept when it bears, else the arc-clamped aim). The price stays HitProbability; the HUD asks it, never a copy.
  • What a solution is drawn against is question controls-solution-subject. The specs are written for the recommended option, the designated target.
  • The trigger. A gun fires when its trigger is held and one of three holds, checked in this order by one gate (ArcPermitsFire): it is fused (the 2026-09-30 exemption); it has a solution, and fires on it without the aim; or the aim is inside its arc (free fire along the aim). Otherwise it holds. Free fire with no solution is the facing-separate-from-aim rule unchanged: a fixed mount fires along the aim when the aim is inside its arc.
  • How the player fires on a solution is question controls-solution-trigger. The specs are written for the recommended option: the existing group triggers, with no new input.
  • The HUD. One crosshair per weapon group, in one of three states: on solution (it sits on the subject's intercept, since MountAim is the intercept, and shows how many of the group's guns bear and the best HitProbability among them); free (it sits on the aim ray); holding (dimmed at the arc edge nearest the aim). Arc fans take the same colours per gun. The separate lead marker of controls-hud r1 is dropped: a group crosshair on solution is the lead.
  • Autofire is not mapped. Its job was to let side guns fire without the aim; a solution now does that on the group trigger. Firing with no input held is not ruled, so cut-controls-autofire is withdrawn.
  • The AI is unchanged by this. Agents already fire on what is in arc and worth it (AgentFires over HitProbability), and their aim is their intercept, so Solution and AgentFires agree for them. cut-controls-ai-bearing still holds.
  • The face-the-aim state has one owner, HelmInput (engine-free, ServerShared): a held flag fed by Left Shift, a latched flag toggled by Caps Lock, and FaceAim as their union. While it is on, the hull turns toward the aim and the A/D axis is ignored. The latch is the game's own state, not the keyboard's Caps Lock light, so the HUD shows a small "face aim" mark while it is latched.

Authority map changes

  • FireControl.Solution is the one owner of a gun's solution; MountAim is its direction and is no longer a separate decision. PFire and Solution share Bears.
  • ArcPermitsFire is no longer the aim's gate alone; it is the one trigger gate, in the order above.
  • HelmInput owns face-the-aim. Shift and Caps Lock are its inputs; ActionGameManager reads HelmInput.Demand and decides nothing.
  • Forbidden: a second hit price or arc test in Gameplay or UI (the HUD reads Solution and HitProbability); a face-aim flag outside HelmInput.

Missiles and point defense

Ruling missiles-sim-side-light: "We need missiles sim side, and we can cut that so as to enable PDC without requiring full entities. Kind of like the asteroids." Ruling thresholded-autofire: "I think we need thresholded autofire as an option, otherwise PDC would require too much micro." Ruling missiles-fuel-and-seekers: "a missile knows where it's going when it's fired but relies on its own sensors for terminal maneuvering. Maneuvering which should expend limited fuel. A missile that spends fuel evading PD might not have enough left to catch up to a target that is itself also dodging." Ruling missile-stats-now-gear-later: "For now these stats will belong to the missile, later they will be derived from gear." Full munition entities stay later (ruling drones-and-munitions-are-entities, follow-up drones-munitions-substrate); nothing here may foreclose them.

The third pass answers four more rulings. missiles-resolution-model, geometry-sets-odds: "I think geometry should set the odds, though. We're not simulating realistic scale and collisions. Going sideways really fast can help you evade both guns and missiles, but the extent should be based on stats. If it's only geometry then the missile's terminal maneuvering fuel, gathered targeting data and piloting skill don't exist at all as far as hit resolution is concerned." missile-cognition: "I actually did mean the skill of the missile itself. Cognition is a commodity in Aetheria, and a smarter missile can better spend its delta v". manoeuvre-penalises-solutions: "There is currently no firing solution penalty for target maneuvering afaik, which is a real gap for the speed demon hot doctrine glass cannon play style." evasion-not-only-flight-time: "Deviation from predicted intercept means you have to spend your delta as the projectile is in flight. Doesn't help you dodge a laser at all." missiles-seeker-retarget, hostile-in-cone: "I like the idea of letting them reacquire targets. Makes turning invisible and firing decoys a plausible strategy."

Anchors are against origin/master 7f51d46d (nothing under Assets/Scripts or tools changed since c7432f39, where the second pass read them), read in a detached scratch worktree on 2026-10-06. Cuts: missile-stats (r2), evasion-term (r1), missile-records (r3, flight only), missile-odds (r1), missile-presenter (r2), munition-shots (r3), autofire-threshold (r3), ai-autofire (r1, unchanged).

Prior art

Source marks as in Controls.

  • Nebulous: Fleet Command (wiki: the community mechanics:missiles page, read in full). Missiles are tracked contacts with hit points by template (10 to 150) and armour as wall thickness, and a radar cross-section that varies with aspect. Point-defense turrets and defensive missiles engage them automatically; decoys soak PD attention (turrets take decoys first). Launch rate is bounded by programming channels, which caps salvo density. Defensive missiles cannot engage other defensive missiles, so two ships cannot dump their magazines at each other's interceptors (snippet, glossary search).
  • Starsector (wiki: Weapon_data.csv, read in full). A missile is a projectile with "proj hitpoints". Weapon hints decide targeting, not whether a round can physically connect: PD targets missiles but prefers missiles in range over ships, PD_ONLY targets only missiles (and fighters with ANTI_FTR), PD_ALSO takes missiles only when nothing else is available. Autofire runs per-weapon target AI (snippet, mod README). Forum consensus (snippet): missile power is non-linear, weak until it saturates PD, then strong until magazines run dry.
  • FreeSpace 2 (snippet, hard-light wiki Weapons.tbl and Ai_profiles.tbl). Bombs are "interceptable": weapons with hit points and an enlarged collision radius. Turrets shoot bombs; AI profiles stop turrets targeting bombs beyond their own weapon range; bombs are invulnerable for a short delay after launch.
  • Children of a Dead Earth (snippet, forum threads). PD is a saturation contest: either saturate it with many small rounds or send few heavily armoured missiles; small lasers are enough for PD. The counterplay is economic, not a hard counter.
  • Homeworld (snippet). Flak and defense-field frigates are the anti-missile and anti-strike roles; no technical detail found.

Fuel, seekers and terminal guidance (second pass, for ruling missiles-fuel-and-seekers).

  • Nebulous: Fleet Command (wiki: the community mechanics:missiles page, read in full). A missile's engine trades burn duration, top speed and manoeuvrability in one fuel-mix editor; engine size adds fuel and range only. During the burn it accelerates and turns at its G rating; after the burn it coasts at its final velocity and cannot manoeuvre. Terminal manoeuvres (weave, corkscrew) start when the seeker finds the target and exist to survive point defense. Launch on a track plots an intercept from the reported position and velocity; the seeker then re-selects its target every 0.5 s by signal strength, and a command-guided missile that loses its track cruises to the last known position. Seekers have a range and a cone (active radar 2 km, 50 degrees). Decoys draw PD.
  • Children of a Dead Earth (forum: the missile construction and tuning guide, read in full). Delta-v is split into boost, midcourse and terminal phases; against fast or evasive targets 50-60% goes to boost, against slow ones 70-80%. A missile whose terminal delta-v runs out against a manoeuvring target cannot close; high acceleration lowers the terminal delta-v needed. Guidance is proportional navigation or augmented PN, and a target that burns briefly then coasts makes APN waste delta-v chasing an acceleration that stopped.
  • DCS World (simtuts defence guide, read in full; Falcon BMS not fetched). A missile's motor burns out and it then bleeds energy, so a long shot arrives slow and is defeated by basic manoeuvring; turning cold makes it fly farther; a hard break 2-3 s before impact exploits proportional navigation's turn limit; beaming (notching) breaks a Doppler seeker's lock. The lethal envelope is smaller than maximum range.
  • Starsector (wiki: Harpoon MRM and ECCM Package pages). Missiles have a flight time (Harpoon 12.5 s) and hit points (Harpoon 150); light PD and flak intercept them; a nimble frigate can evade a first run, and ECCM (more acceleration and turn rate) is the answer. Missiles that run out of flight time still collide (snippet).
  • HighFleet (gameplay.tips guide, snippet). A blind-fired missile flies ballistic and switches on its own radar 100 km before the aim point, a 75 km, 90-degree cone; it locks the first fleet, friend or foe, that enters the cone and guides to impact.
  • Homeworld (fandom wiki, snippet). Missile stats are per missile type (1,250 m/s, 1,650 m/s^2, 120 HP for the missile destroyer's round); no fuel model found.
  • Guidance law (Cranfield, "Zero-Effort-Miss Shaping Guidance Laws", snippet). PN commands lateral acceleration proportional to the line-of-sight rate; the zero-effort miss over time-to-go is the acceleration a missile still needs. PN needs only the bearing and its rate, which is what ruling shared-track-bearing-only allows a missile in flight.

Odds from geometry and stats (third pass, for rulings missiles-resolution-model, manoeuvre-penalises-solutions and evasion-not-only-flight-time; recalled, not fetched this pass).

  • EVE Online. A turret's hit chance falls with the target's angular velocity against the turret's tracking stat and with its signature against the turret's resolution, whatever the round's flight time; a missile's damage falls with the target's speed against the missile's explosion velocity. Going sideways fast helps against both, by an amount the stats set: the operator's sentence, shipped as a tracking term rather than as flight-time dodging.
  • Zero-effort miss (Cranfield, above). The miss a guided round would make if neither side manoeuvred from now on is the standard terminal quantity; what the missile can still correct is its lateral acceleration and remaining delta-v over the time to go. The odds below price that endgame instead of asking a coarse simulation to decide contact.

Evasion from agility and unpredictability (fourth pass, ruling evasion-is-unpredictability, fetched 2026-10-06).

  • Singer's manoeuvre model (Singer 1970, as summarised in the tracking literature, e.g. ResearchGate "Comparison of several maneuvering target tracking models"). Target acceleration is a zero-mean first-order Markov process set by two parameters: its variance (bounded by the target's maximum acceleration) and the manoeuvre frequency, the inverse of its time constant. Long time constants make the target a constant-velocity target, short ones a constant-acceleration one. The split this pass uses is the same: what the target could do (capability) and how much it is changing its acceleration (behaviour).
  • Naval fire control against zigzags (Pacific War Online Encyclopedia, "Fire Control"; Wikipedia "Ship gun fire-control system"). Analogue fire-control computers assumed a steady course, so a course change during the time of flight defeated the solution. A target on a steady course or a steady turn is predictable; a target that keeps changing its vector is not. "Danger space" (NavWeaps definitions) is the along-range interval a ship can occupy and still be hit: the target's extent, not its motion.
  • Weaving targets in missile guidance (Zarchan, "Proportional navigation and weaving targets"; RMS miss against a sinusoidal target). A weave at the right frequency maximises a PN missile's miss; a weave much faster than the guidance loop averages out. High-frequency dithering is not evasion. Hence the behaviour statistic below is low-passed.
  • EVE Online (EVE University wiki, "Turret mechanics", fetched). Hit chance is 0.5 ^ ((angular velocity * 40000 / (tracking * signature))^2 + falloff term): doubling tracking, doubling signature or halving angular velocity are equivalent. A stationary target is always hit. EVE prices transversal speed because its turrets slew. Aetheria's lead cancels a constant sideways speed as a prediction problem (M20), but a mount must still follow the line of sight, so evasion-term r4 adopts EVE's angular form for the mount (below, "Tracking per gun").
  • Starsector (wiki, "Movement" and "Auxiliary Thrusters"). Manoeuvrability is acceleration, deceleration and turn rate together; the hull mod that buys it raises all three. Dodging is done by turning and burning, not by a strafe stat.
  • Elite Dangerous (player guides, Steam discussions). Pitching the main drive onto a new vector beats lateral thrusters for evasion; lateral thrust matters most on ships built around it. Gimbal jitter makes evasive targets hard to hold even for hitscan lasers.

Angular velocity against a mount's rate (fifth pass, ruling evasion-angular-velocity; Eyes pass of 2026-10-07, source marks as in Controls; the EVE formula is the player wiki's, not CCP's).

  • EVE turrets (EVE University wiki, "Turret mechanics" and "Tracking"). The tracking part of the hit chance is 0.5 ^ ((omega * sigRes / (tracking * sigRadius))^2), with omega the transversal velocity over range in rad/s and tracking in rad/s; radial velocity contributes nothing, and at omega = tracking (equal signatures) half the shots hit. The range part sits in the same exponent, so the price is a product of per-term factors. Smaller turrets track faster. The wiki states the failure mode: a close orbit raises omega until big guns cannot hit, and the counters lie outside the gun (webifiers, target painters, tracking computers, smaller guns, drones). CCP's 2004 balance note made signature matter so frigates survive guns built for larger ships. EVE has no term for changes of vector; manoeuvre counts only through the angular velocity it makes.
  • Naval directors (Naval Ordnance and Gunnery vol. 2, chapters 19 and 24 to 26, transcribed at eugeneleeslover.com). The deflection lead is the line-of-sight angular rate times the time of flight, and the computer assumes constant rates; a change of course or speed during the flight spoils the solution. A steady crosser is a rate problem the director solves; a manoeuvre is the unmodelled term. Two errors, two owners.
  • Kerrison predictor (Wikipedia). Low crossing aircraft meant high angular rates and short engagements; the hard part was mount slew, not ballistics. Its straight-line assumption fails against a manoeuvring target.
  • CIWS (Wikipedia, Phalanx and Goalkeeper). Point-defense mounts slew at 80 to 115 degrees per second (sources disagree); a battleship turret trains at a few degrees per second. The authored table below spans the low part of that range: a PD-class gun near 40 degrees per second (none is authored yet), the catalog's guns from 12 down to 1.5.
  • Servo tracking (arXiv 2408.16870). A proportional-derivative mount following a constant-rate target keeps a steady-state error. A mount's lag grows with the rate it is asked to follow, which is the shape PMount gives.

Failure modes to design against.

  1. PD trivialises missiles: perfect interception makes launchers dead weight. Answer: a PD round is priced and rolled like any shot (spread against a small target, range, sensor info), a round that cannot arrive before the missile does has no solution, and heat and ammunition bound how much a group can spend. Saturation then beats PD, as in Starsector and CDE.
  2. PD that never works: interception left to luck the player cannot read. Answer: the HUD marks detected inbound munitions and the ones an autofire gun is engaging, and an intercept shows an effect at the munition's simulated position.
  3. Unreadable salvos: thirty rounds and thirty tracers. Answer: markers only for detected hostile munitions within weapon reach, one shape, no per-gun lines.
  4. Cost blowups: per-missile physics engines, per-missile agents, per-gun O(contacts) scans every frame. Answer: a missile is a few floats on its pending shot stepped by one pure function (a PN command, a fuel debit, one settled seeker reading and one closest-approach test against its own target); reacquisition queries the targeting index only on a seeker interval; an autofire gun keeps its subject while it stays worth firing. The per-missile step cost is measured and pinned (missile-records).
  5. Recursion: PD missiles shooting PD missiles (Nebulous's explicit rule). Answer: a munition weapon never takes a munition as its subject.
  6. Unreadable missile states (burning, coasting dry, seeker blind). Answer: the presenter's flame follows the live throttle and goes dark when the tank is empty; the HUD diamond is filled while a munition can still manoeuvre and hollow once it coasts; a missile that self-destructs at the end of its life shows a fizzle, not a hit.
  7. PD-or-nothing balance: evasion that is free makes PD useless; no evasion makes PD decisive. Answer: evasion and pursuit draw on the same fuel, so PD pressure that a missile survives still costs it terminal authority downrange (the operator's sentence), and a dodging target spends that authority faster.
  8. Two hit models drifting apart. Answer: one roll and one price owner for every shot. A gun round is priced from its frozen shooter data, the target's realized deviation in flight and the tracking term; a guided round flies until its terminal gate and is priced there from its own seeker data, its zero-effort miss beyond reach, the same tracking term and its terminal correction. Both commit through Commit and apply through Apply, read the one PDeviation shape and the one Evasion term, and nothing after the roll decides again.
  9. Determinism and replay. The zone steps by the frame's delta time (M15), so a stepped missile is exactly as replayable as a ship's motion and no less. Answer: no shared random stream (the evasion phase comes from the shot id, as the roll's seed does), and the terminal gate tests closest approach of straight motions, so a long frame cannot step past it.

Body facts

  • M1. A guided round is an ordinary shot in the simulation. LauncherData (Behaviors/Launcher.cs:10, a LockWeaponData, so a LockWeapon) and GuidedWeaponData (:37, an InstantWeapon) fire through InstantWeapon.Execute into FireControl.Fire (InstantWeapon.cs:271) like any gun. Flight time is FlightDistance / weapon.Velocity (FireControl.cs:559); CommitTime is ArrivalTime - CommitHorizon (0.5 s, Settings.cs:250), or the fire time for a fused round with no target (:588); Commit rolls at that horizon (:707) and Step applies at arrival (:645). No ServerShared code reads MissileVelocity, Thrust, DodgeFrequency or the three curves.

  • M2. The flight is Unity's and decides nothing. GuidedProjectile.Update (Gameplay/Weapons/GuidedProjectile.cs:127-212) homes with first_order_intercept, a noise dodge and the guidance, lift and thrust curves. GuidedProjectileManager.Bind (GuidedProjectileManager.cs:14-26) subscribes to ShotCommitted (retarget to the burst point) and ShotResolved (burst or fade). The split branch (GuidedProjectile.cs:166-194) spawns visual children that no shot owns. Between fire and arrival a missile is a PendingShot with no position and a Unity transform nothing simulated can reach.

  • M3. The GuidedWeaponData branch homes on the aim (GuidedProjectileManager.cs:62-80, the aim at :79, LookDirection, renamed by controls-helm). An unfused round with no engaged target has FlightDistance 0 (FireControl.cs:494) and so resolves the tick it fires.

  • M4. The pending shot. PendingShot (FireControl.cs:1748-1798) is a struct in Zone.PendingShots (Zone.cs:46), never saved; TryGetShot is a linear scan (Zone.cs:55-65). Its Target is an Entity. Step resolves a shot whose target left the zone as a fresh Miss, at any stage, without rewriting the committed outcome (:619-629): the precedent an intercepted missile follows. ShotResult is Miss, Hit, Burst (:1944-1949).

  • M5. Mines are not simulation records. Mine moves by a Unity GridObject, arms by Physics.OverlapSphere and damages through FireControl.Detonate (Gameplay/Weapons/Mine.cs, MineManager.cs). Detonate (FireControl.cs:1350) iterates zone.Entities only. Follow-up bodies-entity-and-mines owns mines; no cut here moves them.

  • M6. No point defense exists. A case-insensitive search for point defen, PDC, \bPD\b and anti-?missile over Assets/Scripts and tests returns nothing. Nothing can take a shot as its target.

  • M7. Targets. TargetRef (Zone.cs:595-621) is an Entity or a ChunkId. The zone registers EntityTargets and one BeltTargets per belt (Zone.cs:101, :119). Shots at chunks are not routed yet (FireControl.cs:533, mining Cut 4).

  • M8. A weapon's only subject is its entity's one target. Fire reads source.Target (FireControl.cs:534), Refuses reads it (:94), and Weapon.StanceAllowsFire reads it (Weapon.cs:107).

  • M9. AI fire is per weapon. Combat (Agents/States/Combat.cs:119-124) and TurretController (Behaviors/TurretController.cs:83-96) activate each weapon when FireControl.AgentFires (FireControl.cs:101-109) passes: HitProbability >= AgentMinHitProbability (0.2, Settings.cs:259) for an unfused weapon, designated and not refused for a fused one.

  • M10. Presenters fly along the barrel. ProjectileManager.Fire launches along barrel.forward, not the shot's frozen TravelDirection; package hulls have no articulation (C9), so their rounds leave along the hull whatever the simulation decided.

  • M11. EntityPack keys. The base EntityPack uses keys 0 to 16; ShipPack uses 17 to 19 and OrbitalEntityPack 17 to 20 (EntitySerializer.cs:153-189). The next free base key is 21.

  • M12. Shift is already an action-bar slot. GetDefaultPlayerSettings (ActionGameManager.cs:95-105) binds <Keyboard>/leftShift to action-bar slot 0, which controls-helm r2 also binds to Face Aim. Reported to Self; not this section's cut.

  • M13. Launcher content. GT 3K, pswarm and scorched void policy carry guidance-system, thruster and warhead roles; LRMM72 and SRMM72 are flat (docs/stats-power-cut7-roles.md:139-146). The Launcher Angles scenario fits a GT 3K and a pswarm (Scenarios/LauncherAngles.cs).

  • M14. Cost. The first pass's derived pose cost nothing per tick. A missile that spends fuel by events cannot be a closed form of its shot and time (what it spent depends on what PD and its target did), so the revision steps it: per live munition per tick, one PN command, one fuel debit, one settled seeker reading (Sensor.Gain, no stored info), one closest-approach test against its own target, and, while it has no lock, one index query per seeker interval. No ServerShared scratch build was run for this pass; missile-records r3 measures the step and pins it.

  • M15. Zone time is the frame's delta time. ActionGameManager calls Zone.Update(Time.deltaTime) (Gameplay/ActionGameManager.cs:1308); Zone.Update (Zone.cs:202-221) advances time, orbits, agents and entities by that dt, then FireControl.Step. There is no fixed tick. Rolls are seeded per (zone, shot id) (FireControl.cs:707 onward), so an outcome does not depend on what else drew, but no zone state is frame-exact replayable today. Headless tests step with a fixed dt.

  • M16. What a seeker can reuse. Sensor.Gain (Behaviors/Sensor.cs:187-190) is the one gain rule: a passive tick adds visibility * sensitivity * response * dt / distance. Entities hold stepped info (Sensor.Execute, EntityInfoGathered, :137-180); a chunk's info is the settled value saturate(sum(rate) / (n * TargetInfoDecay)) computed when asked (Entity.ChunkInfo, Entity.cs:357-370). Detection is info above TargetDetectionInfoThreshold for entities (Entity.cs:245-256) and chunks (:374). A target's brightness is Entity.Visibility, the sum of its VisibilitySources (Entity.cs:139). A seeker therefore needs no stored info: its reading of its target is the settled value from its own sensitivity and cone, the chunk rule with one sensor.

  • M17. Launcher data. LauncherData keys 26-31 and GuidedWeaponData keys 21-26 hold, in order, GuidanceCurve, ThrustCurve, LiftCurve, Thrust, DodgeFrequency, MissileVelocity (Behaviors/Launcher.cs:10-61). LockWeaponData's LockSpeed, SensorImpact and LockAngle are the launcher's lock-on, the shooter's sensors, not the round's (Behaviors/LockWeapon.cs:13-19, :104). The warhead is WeaponItemData's Damage, DamageSpread, DamageType, BlastRadius (key 29) and Fuse (key 32) (ItemData.cs:477-506), read through FireControl.FuseOf (FireControl.cs:63-68). The catalog is GameData/Aetheria.cc; each catalog change has its own AetherDb *-migrate apply command (tools/AetherDb/Program.cs:32-40). The roles doc assigns Thrust, MissileVelocity and Velocity to the thruster role (docs/stats-power-cut7-roles.md:139-146).

  • M18. What the flight fields meant. GuidedProjectile.Update (GuidedProjectile.cs:127-212) aims at first_order_intercept at TopSpeed (MissileVelocity), turns its velocity toward the desired one at Thrust * ThrustCurve(progress) per second, and blends a noise dodge at DodgeFrequency by GuidanceCurve(progress). There is no fuel, no seeker, and the round fades past Range.

  • M19. Blasts. Apply detonates a Burst or Proximity round through Detonate at its BurstPoint (FireControl.cs:920-930); Detonate overlaps the disc with the cells of every entity in the zone (:1350), the path mines use (M5). HullEntry (:839) gives the distance along a line to an entity's first metal cell.

  • M20. No manoeuvre enters the price, and none reaches an instant weapon (probe for rulings manoeuvre-penalises-solutions and evasion-not-only-flight-time, source read at 7f51d46d on 2026-10-06). HitProbability (FireControl.cs:316-323) is PFire * PSpread * POnHull: shooter accuracy and info, barrel spread, silhouette. The target's velocity enters only the lead (PredictedIntercept, :202-208), which cancels a constant sideways speed exactly. The one motion term is the roll's DeviationProbability (:425-431): the target's realized departure from the fire-time straight-line prediction, forgiven by the shooter's Tracking, measured at CommitTime. Fire sets CommitTime to now + max(0, flightTime - CommitHorizon) (:588, CommitHorizon 0.5 s), so a round whose flight is under 0.5 s commits at fire and measures nothing. Beams fire through Fire per BeamResolveInterval with flight time 0 (Behaviors/ConstantWeapon.cs:176-188), as instant weapons do (InstantWeapon.cs:271). Result: the solution the HUD, AgentFires and autofire read carries no manoeuvre penalty; at the roll only long-flight rounds see manoeuvres, only ones made in flight, and never a sideways speed. A laser cannot be dodged at all. The operator's reading holds.

  • M21. What agility exists. No Agility stat exists anywhere. Ship aggregates LeftStrafeThrust and RightStrafeThrust from its active lateral thrusters and aether drives (Ship.cs:52-53, :181-220); a thruster accelerates its entity by Thrust / Mass (Behaviors/Thruster.cs:104). A hull's strafe acceleration is therefore min(LeftStrafeThrust, RightStrafeThrust) / Mass, derived from fitted gear; an entity that is not a Ship has none. No entity stores its acceleration. The shooter's tracking is TargetingSystem.Tracking (Behaviors/TargetingSystem.cs:43, :80), falling back to UnaidedTracking (10, Settings.cs:228) through FireControl.Tracking (:190): metres of deviation forgiven.

  • M22. The shot pipeline can carry a guided round. Step (FireControl.cs:609-656) commits when now >= CommitTime and applies once committed and now >= ArrivalTime; Commit rolls CommitProbability with the per-shot seed (:707-760). A munition whose CommitTime stays infinite until its terminal gate, which then sets it to the gate's time and ArrivalTime to the closest approach, goes through the same commit, events and application as a gun round.

  • M23. What the ships can actually reach in the window (probe for ruling evasion-is-unpredictability: tests/Aetheria.Shared.Tests/EvasionEnvelopeProbe.cs, a scratch file never merged, run headless on Yggdrasil through ygg-verify on probe commit 7ef47d72 over 646057f2, 2026-10-06, the shipped catalog and authored settings, fixture seed 1). Each ship was fitted by the loadout generator, settled for 2 s, then flown from rest for 0.5 s in the real Ship.Update (dt 1/60) under fifteen control policies (strafe, forward, reverse, corner, turn-and-burn at 45, 90, 135 and 180 degrees, each mirrored). The reach is the largest displacement from the idle run along each axis; the half-width is the mean of the two sides.

    Fit Mass Thrust (N) and turn Nose-on half-width Broadside half-width
    Longinus (generated) 3,835 2 Large Drive forward 250,000 each; 2 Talaria on the flanks at lever 0.998, which cancel to zero net strafe and only turn; no reverse. Forward 130.4 m/s^2, turn 1.95 rad/s by Thruster.Execute's arithmetic 7.50 m 8.15 m
    Djinni (generated) 13,835 8 Talaria (3 forward, 1 reverse, 4 lateral at lever 0.73 and 0.87): forward 16.3, reverse 5.4, strafe 10.8 m/s^2 each way, turn 0.86 rad/s 2.00 m 2.19 m
    LonginusX (generated) 3,810 one Traction aether drive, omnidirectional: measured 109 to 154 m/s^2 on the four axes 16.81 m 18.06 m

    A second generated fit of each agreed within 30% (Longinus 9.77 / 8.13, Djinni 1.65 / 1.80, LonginusX 16.50 / 17.71); the Duel's LonginusX fit gave 11.62 / 12.97. The catalog has three ship hulls (Djinni, Longinus, LonginusX); there is no Gemini, and the LonginusX with its aether drive is the catalog's omnidirectional ship. On the Longinus, turning right also pushes it right: the turning Talaria translate the hull 2.4 m in 0.5 s while turning 59.5 degrees.

  • M24. Ship's thrust aggregates are not an envelope. Ship.LeftStrafeThrust and RightStrafeThrust count the Longinus's two flank Talaria (75,000 N each, 19.6 m/s^2), yet a pure strafe input moves it 0.000 m: the torque compensation in Ship.Update (Ship.cs:257-275) cancels a lone off-axis strafer completely. Its lateral push exists only while it turns. r1's min(LeftStrafeThrust, RightStrafeThrust) / Mass would have rated the Longinus at 19.6 m/s^2 of agility and the Djinni at 10.8, the operator's ships in roughly the wrong proportion for the wrong reason. Aether drives add drive.Thrust (AetherDrive.cs:140), a per-update impulse times mass, into the same sums as thruster newtons, so ForwardThrust / Mass reads 1.9 m/s^2 for a LonginusX that accelerates at about 150. ClockwiseTorque sums lever arms without thrust (Ship.cs:225-245), so TurnTime (Ship.cs:61) is not in the units Thruster.Execute turns by (Behaviors/Thruster.cs:106-107: input * Torque * Thrust * TorqueMultiplier / Mass rad/s).

  • M25. The Djinni moves about 1.6 times faster than its stats say. Full forward for 0.5 s moved the Djinni 3.27 m where ForwardThrust / Mass (16.3 m/s^2) predicts 2.03 m; strafe gave 2.00 m against 1.36, and it turned 40.8 degrees against 24.7 by the Execute arithmetic. The Longinus matches its stats (15.48 m against 16.3 predicted, the gap being drag). Not explained by this pass: thruster Thrust read 75,000 after settling and Mass 13,835. Whatever the cause, an envelope that reads the stats would misprice the Djinni by that factor, so evasion-term r2 pins the envelope against motion measured in Ship.Update and stops on a mismatch.

  • M26. A cheap reach is exact enough. For a body-frame envelope (forward, reverse, left and right acceleration, a turn rate each way) under kinematic turning (ruling controls-turn-inertia), the largest displacement along a direction in a window, taken over eight faces (the four thrust axes and the four corners of the thrust box), each turned toward the direction at full rate while thrusting whenever it points within 90 degrees, then held, matched a brute-force search over turn strategies (one-degree steps, 400-step integration) to within 0.1% on four test envelopes, at 0, 45 and 90 degrees (scratch script, 2026-10-06). Against the real Ship.Update, with the Longinus's flank push counted as strafe, it gave 7.31 m nose-on (probe 7.50) and 8.61 m broadside (probe 8.15). Cost: one box support and eight faces of two trigonometric calls each, per side.

  • M27. Tracking is one number per ship, read in three places (source read at 46b28501, the evasion-term head Soul held, 2026-10-07, for ruling tracking-per-gun). FireControl.Tracking(Entity) (FireControl.cs:190-194) returns the active TargetingSystem.Tracking (Behaviors/TargetingSystem.cs:43, :80), else UnaidedTracking (10, Settings.cs:228). Its readers are HitProbability (:327, PDeviation(evasion, Tracking(source))), Inspect (:344, and PEvasion at :368) and Fire, which freezes it into PendingShot.Tracking (:676, field :1868). DeviationProbability (:444) forgives realized plus evasion deviation with that frozen value, so the in-flight drift and the evasion term share one tolerance. Nothing outside FireControl calls Tracking(; the debug HUD only prints shot.Tracking and d.Tracking (Gameplay/ActionGameManager.cs:1356, :1380). No weapon field carries tracking.

  • M28. The catalog's weapons by size class (probe: cultcache_py SingleFileMessagePackBackingStore over GameData/Aetheria.cc at 46b28501, payloads decoded by MessagePack key, 2026-10-07; the Sep 30 and Oct 3 AetherDb builds refuse that catalog on the aetheria.fieldkinddata schema). 18 WeaponItemData records; WeaponItemData.WeaponCaliber (ItemData.cs:483, key 25: Small, Medium, Large, ExtraLarge) is the only authored size class. Ballistic: Small 6k Shooter, ClearPath, Earp; Medium Autocannon, DeathCluster, pretty pretty bang bang. Energy: Small CShot RainbowLite Lazer, ColdFire, FastBlast+-, Spectra; Medium ChargeBlast SG, ChargeBlast+-; Large plight. Launcher: Medium GT 3K, pswarm, scorched void policy; Large LRMM72; ExtraLarge SRMM72. No ballistic or beam gun is Large or ExtraLarge except plight, so in the demo the heavy guns are Medium. The two targeting designs author Tracking 15-25 (Targeting Computer) and 30-45 (Fire Control Array).

  • M29. Where a weapon stat can live. WeaponData (Behaviors/Weapon.cs:20) holds the per-weapon PerformanceStats at keys 1-16 (Spread key 15, Velocity key 16), evaluated in Weapon.UpdateStats (:131-140) through the item's heat, durability and quality terms. Subclasses use keys 17 to 32 (InstantWeaponData 17-20; ChargedWeaponData to 32; LockWeaponData 21-25; LauncherData 26-31; GuidedWeaponData 21-26), so 33 is the first key free across the whole hierarchy. CultCache opens a record missing a local slot as defaulted_missing_slot and uses the local default (CultLib src/GameCult.Caching/CultCache.cs, schema comparison), so a field initializer is what unmigrated records and code-built fixtures read. A PerformanceStat with no terms resolves to Max, and a bare new PerformanceStat() resolves to 0.

  • M30. A weapon's footprint is its item Shape (source read and catalog probe at 46b28501, 2026-10-07; method as M28). Footprint is evidence about a gun, not a key to its tracking (ruling weapon-tracking-authored, which superseded weapon-tracking-defaults). ItemData.Shape (ItemData.cs:288-289, key 5, [InspectableSchematicShape], the cells Studio paints) holds a bool[,]; Shape.Coordinates (:84-95) lists its true cells, so the cell count is Shape.Coordinates.Length. A hardpoint's HardpointData.Shape (:563, key 2) is the slot, not the gun: Takes (:580-581) fits any design of the slot's type whose shape lies within it. new Shape() is one true cell (:25-29). The catalog's weapons by cell count:

    Cells Weapons (hardpoint, caliber)
    1 plight (Energy, Large), pswarm (Launcher, Medium)
    2 6k Shooter, ClearPath, Earp (Ballistic, Small); ColdFire, FastBlast+-, Spectra (Energy, Small)
    3 GT 3K, scorched void policy (Launcher, Medium)
    4 Autocannon, DeathCluster, pretty pretty bang bang (Ballistic, Medium); ChargeBlast SG, ChargeBlast+- (Energy, Medium)
    6 LRMM72 (Launcher, Large), SRMM72 (Launcher, ExtraLarge)
    9 CShot RainbowLite Lazer (Energy, Small)

    plight's one cell is authored, not a default, and it is a content error (ruling plight-not-one-cell; its provenance is M35). Caliber and footprint disagree in both directions (plight Large in one cell, CShot Small in nine). The hulls' weapon hardpoints: Longinus and LonginusX two Energy 1x2 and two Launcher 1x3; Djinni two Ballistic 1x2, two Launcher 1x3 and two Launcher 3x2; the station Turret two Ballistic 2x4 at arc 360. No weapon hardpoint is one cell or 3x3, so CShot fits no hull.

  • M31. Nothing in the price reads velocity across the line of sight, and nothing models a mount's rate (source read at 46b28501). Target velocity enters the lead (PredictedIntercept, FireControl.cs:202-208, without the shooter's velocity) and the frozen FireTargetVelocity that DeviationProbability (:439-445) projects; Evasion (:452-470) reads envelope, heading, track, line of sight and window, never a velocity. A steady crosser therefore costs only the in-flight drift, and an instant weapon nothing. No weapon or hardpoint field carries a slew rate: HardpointData.FiringArc (ItemData.cs:567-569) is an arc in degrees, and the targeting gear's Tracking is metres (M27). PendingShot.PFire (:1867) and Tracking (:1868) are the shooter facts Fire freezes (:663-677); CommitProbability (:792-806) multiplies shot.PFire * pDeviation, then spread and hull.

  • M32. Top speed is a hull behaviour: the Longinus and the Djinni carry one, the LonginusX does not (source read and catalog probe at 46b28501, method as M28, 2026-10-07; corrects an earlier M32 that probed only gear and weapons and concluded top speed was drag's). VelocityLimitData (union 11, Behaviors/VelocityLimit.cs, one PerformanceStat TopSpeed at key 1) is a behaviour on HullData.Behaviors (EquippableItemData, key 11). Its Execute clamps Entity.Velocity to TopSpeed and books the removed part in Entity.LimitClamp, which Entity.Acceleration puts back so the clamp is not a manoeuvre (2cf980e3, Soul finding limit-clamp-evasion, fixed at 46b28501). The catalog's five hulls: Longinus TopSpeed 100..100 (terms heat 1), Djinni 50..50 (terms heat 1, quality 1.5; Min == Max, so both evaluate flat), LonginusX, Zenith and Turret none. History (git log -S VelocityLimit; docs/locomotion-cut.md lines 75-81 and 440-460): the code has carried the behaviour since 2020; the Longinus and Djinni records were absent from the catalog until locomotion Cut 1's restore-hulls (3cb6b154, 2026-09-25, deleted after landing in 819bd6b8) rebuilt them from the decoded 2021-04-14 legacy record, each with its own legacy VelocityLimitData. The legacy LonginusX carried none, and neither does today's, so nothing was dropped that is still missing. Readers: Agent.TopSpeed (Agents/Agent.cs:26, falling back to 100 for a hull without one, so AI LonginusX steer at 100), and the dock intro (Gameplay/ActionGameManager.cs:849), which dereferences GetBehavior<VelocityLimit>() with no null check and so throws for a LonginusX player. The cap is the hull's own behaviour, gated by the hull's Active (locomotion map, Soul hazard 2026-09-26). The LonginusX's ceiling is physics: drag (Ship.cs:307-310, steady thrust a settles near a / Drag, about 1,300 m/s at 130 m/s^2 and drag 0.1) and its aether drive, whose push falls to zero as speed along an axis reaches the rotor speed (AetherDrive.cs:140-148; forward rotor speed is Rpm * 3 / 100, 1,200 to 2,400 m/s at the Traction drive's 40,000 to 80,000 maximum rpm). Together they put it several hundred m/s up; arithmetic, not a flight probe.

  • M33. The table fit (probe: a scratch script over the catalog decode of M30 and M34, not committed, 2026-10-07, for evasion-term r5). It evaluates the mount factor 0.5 ^ ((omega / rate)^2) and the vector factor PDeviation(E, Tracking) for each authored rate under three targeting fits (unaided 10, Targeting Computer 20 and Fire Control Array 37.5, the designs' midpoints, M28), at the geometries of the table in "Tracking per gun", with crossing speeds at the hulls' real top speeds (M32). omega comes from speed and range; E is the window-0.75 figure the r2 pass measured (the table under "Evasion is how unpredictably", and the speed demon's 9.63 m). The results are in that section.

  • M34. The catalog's guns by role (catalog probe at 46b28501, method as M28, 2026-10-07; the weapon behaviour's evaluated range of Damage key 2, Range key 6 and the shot interval Cooldown key 19, with Count key 17 pellets per shot):

    Weapon Kind, caliber, cells Mass Damage x count Interval (s) Range (m)
    ClearPath Ballistic auto, Small, 2 50 2-6 0.1-0.033 250-600
    Spectra Energy beam, Small, 2 25 20-85 0.5-0.25 800-1,750
    FastBlast+- Energy auto, Small, 2 125 20-80 x 12 0.33-0.1 500-1,250
    Earp Ballistic single, Small, 2 75 5-25 trigger 1,000-2,500
    6k Shooter Ballistic single, Small, 2 95 4-30 trigger 1,000-2,000
    ColdFire Energy beam, Small, 2 150 40-160 2.5-1 750-2,250
    Autocannon Ballistic auto, Medium, 4 75 3 0.5 1,000
    ChargeBlast+- Energy charged, Medium, 4 225 25-110 (x3 charged) 0.75-0.4 800-1,900
    DeathCluster Ballistic auto, Medium, 4 250 100-250 x 12-15 1-0.25 500-1,000
    ChargeBlast SG Energy charged, Medium, 4 250 50-175 x 5 0.75-0.4 450-950
    pretty pretty bang bang Ballistic charged, Medium, 4 250 64-256 x 5 0.67-0.33 500-1,250
    plight Electrostatic charged, Large, 1 (M35) 100 75-400 (x3 charged) 4-2, after a 3-2 s charge 500-2,000
    CShot RainbowLite Lazer Energy beam, Small, 9 500 100-600 3-1 1,000-3,000
    GT 3K, pswarm, scorched void policy, LRMM72, SRMM72 Launcher or guided 100

    "Trigger" is an InstantWeaponData whose cooldown is 0.01 s: it fires as fast as the trigger. plight is sold (product DragOnBreath, Lucent Media), so it spawns in generated loadouts.

  • M35. plight has been one cell since it was created (git history and vault read, 2026-10-07). It first appears in GameData/WeaponItemData/plight.json at 85483319 (2021-06-23, "Modular Database Upgrade") with "shape":{"name":[[true]]}, caliber Large, the TaranisLG effect prefab, and no other shape in any later revision; the 2021-03 AetherDB.msgpack has no plight record. docs/content-batch-one.md:822 copied the catalog's 1x1. The AetheriaLore vault names it only as Lucent's "TaranisLG lightning gun with vape tie-in" (Corporate Roster and Item Wishlist.md, Faction Flavor and Visual Identity.md) and gives no size; Weapon Category Codes.md defines the ESD lightning-gun code and size letters but no TaranisLG label. Every Energy hardpoint on a flyable hull is 1x2 (M30): a 1x2 plight fits the Longinus and LonginusX as it does today, and any larger shape fits no hull, like CShot. Nothing in the record settles its intended shape; question plight-shape asks it.

Body facts: evasion core, mount term and autofire seams (2026-10-08)

Body: origin/master 4d664b53, read in a detached worktree. The in-flight branch origin/eureka/aetheria-release-controls-mount-aim (e103b90a, 538b11b4) was read with git diff 4d664b53 <branch>, never checked out. Read by cut-munition-shots, cut-munition-jink, cut-autofire-threshold and cut-autofire-controls.

  • MA1. Master's evasion is two factors in one price (source read, FireControl.cs). HitProbability (:350-357) is PFire * PMount(AngularVelocity(source, target), TrackingRate(weapon, source)) * PSpread * POnHull * PDeviation(evasion, Tracking(source)). Forecast (:326) computes evasion = Evasion(target, TravelDirection). The mount factor reads the gun's rate times the ship's gear (TrackingRate, :221-224); the jink factor reads the ship's gear alone in metres (Tracking, :195-199), as rulings tracking-per-gun and jink-per-ship say.
  • MA2. The evasion core is already subject-free (FireControl.cs:496-516). Evasion(Entity, los) returns 0 for a non-Ship and otherwise calls the pure Evasion(in ManoeuvreEnvelope, float2 heading, in ManoeuvreTrack, float2 los, float window), whose comment calls it "the one call guns and munitions make". A munition can answer it with its own envelope, heading and track without a second evasion function.
  • MA3. The envelope and track types (FireControl.cs:2049-2115). ManoeuvreEnvelope is six accelerations (Forward, Reverse, Left, Right in m/s^2 along body axes; Clockwise, CounterClockwise turn rates). ManoeuvreTrack.Observe(float2 acceleration, float dt, float window) keeps trend, innovation and second moments and forgets a window after the last change of vector. Reach (:521-546) with both turn rates zero reduces to the box's no-turn support 0.5 * (face along) * T^2 plus faces whose turn is zero (ReachFace returns 0 for a turned face when its rate is below 1e-4, :552). So a point-mass munition with no turn rate is priced by its box alone, which is what a vector-commanded munition can do.
  • MA4. Who writes a ship's track (grep over Assets/Scripts/ServerShared). One writer: Entity.Update at Entity.cs:1478 (Manoeuvre.Observe(Acceleration, delta, SolutionWindow)), acceleration being the base update's velocity change with the limit clamp added back (:1477). Thrusters supply the envelope (Thruster.Manoeuvre(), Thruster.cs:98); Ship.Envelope sums them (Ship.cs:153-159).
  • MA5. Fire freezes the mount for an entity subject only (FireControl.cs:688). pFire = solution.PFire * (solution.Engaged != null ? PMount(AngularVelocity(source, solution.Engaged), TrackingRate(weapon, source)) : 1f). CommitProbability (:864-878) multiplies the frozen PFire by DeviationProbability (:483-489), which adds Evasion(shot.Target, ...) to the realised drift. A munition subject needs both: the mount frozen at fire from its own position and velocity, and the evasion at commit from its own track.
  • MA6. AngularVelocity has a planar overload (FireControl.cs:209-213): AngularVelocity(float2 offset, float2 relativeVelocity), range floored at 1 m. A munition's offset is Flight.Position - source.Position.xz and its relative velocity Flight.Velocity - source.Velocity; no new geometry is needed.
  • MA7. No munition code is on master (ls Assets/Scripts/ServerShared, grep MunitionId). Munitions.cs, MunitionFlight and MunitionId do not exist; TargetRef (Zone.cs:646-670) has only Entity and Chunk. missile-stats, missile-records and missile-odds had no report (specs-with-no-report query, as of ordinal 750). munition-shots' anchors into that code are by name.
  • MA8. controls-mount-aim, in execution, changes the seams these cuts touch (diff of 4d664b53..origin/eureka/aetheria-release-controls-mount-aim, FireControl.cs and Weapon.cs): adds Solution(Weapon, Entity shooter, TargetRef subject) after AimDirection returning GunSolution { Entity Subject; bool Bears; bool Free; float2 Direction }; adds Bears(weapon, source, Entity target, out range) and routes PFire through it; rewrites ArcPermitsFire to fused || mine layer || Solution(weapon, shooter, shooter.Target.Value) bears or free; Solve's travel direction becomes Solution(weapon, source, target).Direction. The branch dropped the Broadside scenario (538b11b4: no catalog hull has side-rotated gun hardpoints).
  • MA9. PendingShot construction sites (grep new PendingShot over Assets/Scripts and tests): two, FireControl.Fire and FireControlCut9Tests.cs. Readers of shot.Target in FireControl: :487-488, :808, :867-876, :884, :920-950, :1132-1140, :1190, :1483; outside: EntityInstance.cs:331-333.
  • MA10. Autofire seams on master (source reads): Entity.WeaponGroups Entity.cs:82, sized :791-793, removal path :1050; PowerBus.Step :1453 then the equipment loop :1455-1458 (the only place equipped behaviours execute in Entity.Update); Weapon.Activate/Deactivate set _firing only (Weapon.cs:156-164), so a weapon draws no power by being triggered. StanceAllowsFire Weapon.cs:113, ArcAllowsFire :123. Refuses FireControl.cs:93-95, AgentFires :104-112 (its first branch is the mine layer's: worth it when Solve says Designated). Fire reads source.Target.Value.Entity at :683. EntityPack keys 0-16 on the base class, subclasses use 17-20, key 21 unused (EntitySerializer.cs). ActionBarWeaponGroupBinding ActionBarSlot.cs:177-210; slot input actions are created in ActionGameManager.createBinding (:434-442, started activates, canceled deactivates). ControlsHud.cs does not exist on master (controls-hud creates it). Zone.Targets.Within(in TargetSearch, List<TargetCandidate> into) (TargetingIndex.cs:99) fills a caller's list. Scenarios.Tests (Scenarios.cs:7-10) ends with LauncherAngles, LongHaul.
  • MA11. The close-crosser fixture already exists (tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs). EvMountRange(gunRate, laserRate) sets two guns on one shooter to authored rates through EvSetRate (:23-29), and LightAndHeavyGunsSplitOnAFastCloseCrosser (:139-160) with rates 40 and 3 and EvCross(range, jinking: false, float2(100, 0)) at 200 m measures PMount >= 0.85 and <= 0.01. The steady crosser's evasion is zero (EvCross settles the track), so the split is the mount term alone. The fixture places the target with ScenarioStance.Neutral (default of ScenarioStage.Place, ScenarioStage.cs:109); Entity.SetIff(other, true) (Entity.cs:810) or ScenarioStance.Hostile makes it hostile.
  • MA12. No PD-class gun is in the catalog (tracking table). The highest authored rates are ClearPath 12 and Spectra 10 deg/s; launchers are +infinity. Against inbound missiles the angular velocity is small (mostly radial approach), so these suffice to engage salvos in a scenario; against a fast close crosser ClearPath prices 0.37 / 0.76 (TC / FCA, map table). The catalog growth section's PD guns (CG-probes) are the content that closes this.

The design

The resolution model is ruled: missiles-resolution-model, geometry-sets-odds. The second pass recommended pure geometry (a fuze decides contact) and rejected both roll variants as two answers to one question. The operator overturned it for a reason that pass did not weigh: the simulation is not at a scale where contact means anything, so a fuze makes terminal fuel, targeting data and the missile's own skill irrelevant to the hit. The split-brain worry is answered by one commit point, after which the flight obeys the roll, which is the gun's own contract (M22):

  • The flight steps as before (PN, fuel, seeker) and decides whether and when a missile reaches its terminal gate, and with what left. A missile that runs dry, loses its seeker or is outmanoeuvred arrives with a large miss, or never arrives.
  • At the gate the roll decides whether it connects, once, at a price FireControl owns. After the roll the flight presents the outcome and decides nothing.

Evasion is how unpredictably the target can and does change its motion (rulings manoeuvre-penalises-solutions, evasion-not-only-flight-time and evasion-is-unpredictability). M20 shows the price has no manoeuvre term and a laser cannot be dodged. r1 answered with one strafe figure; the operator ruled that incomplete: "A ship like Longinus has almost zero strafe thrust but should still be plenty agile while the Djinni has dedicated strafing thrusters but is too heavy to get very far with them. Unpredictability of motion should matter, too." M24 adds that the strafe figure is not even what the hull can do. The term (evasion-term r2) is the product of two parts, each with one owner, and it adds to the in-flight deviation rather than replacing it:

  • Capability: the reachable spread. Ship.Envelope is a ManoeuvreEnvelope: body-frame acceleration forward, reverse, left and right (m/s^2) and turn rate clockwise and counter-clockwise (rad/s), summed from what each live propulsor reports with the arithmetic its own Execute uses (a Thruster its thrust over mass along its mount and its lever times thrust times TorqueMultiplier over mass). An aether drive reports nothing: the drive is parked out of the demo (ruling retire-longinusx), and evasion-term r6 deletes the AetherDrive.Manoeuvre report that r2 landed (see "Retiring the LonginusX and parking the aether drive"). A thruster that only turns counts as lateral push, because it pushes whenever it turns (M23). Not a Ship, no envelope. FireControl.Reach(envelope, heading, direction, window) is the largest displacement from the coasting path along direction in window, by the eight-face form of M26. The capability across a line of sight is the half-width of the reachable interval, C = (Reach(+n) + Reach(-n)) / 2, with n perpendicular to the line of sight. An omnidirectional hull has the same C from any bearing; a Longinus has its largest crossing the line of sight and a smaller one nose-on, because it must turn first.
  • Behaviour: how much it is changing its vector. Entity.Manoeuvre is a ManoeuvreTrack, written once per Entity.Update from the measured acceleration (the velocity change across the base update, so thrust and not drag or gravity): a trend a_bar (exponential average, time constant SolutionWindow), the innovation a - a_bar low-passed with time constant SolutionWindow / 4, and the exponential average of that innovation's outer product (three floats). LateralRms(n) is the root of its projection on n. A steady burn is predictable (its innovation decays to zero), coasting is predictable, a weave at the window's scale is not, and dithering much faster than the window averages out (Zarchan, above).
  • The term. FireControl.Evasion(target, lineOfSight) reads the target's envelope, heading, track and SolutionWindow T: u = 0.5 * LateralRms(n) * T^2, zero when u is zero (no envelope work for a coasting target), else min(C, u). Behaviour realises the evasion, capability caps it. A coasting ship evades nothing whatever it could do; a jinking ship evades up to what its envelope reaches across that line of sight. Sideways speed does not enter this term: the lead cancels it as a prediction problem (M20). It enters the mount's term (ruling evasion-angular-velocity, "Tracking per gun" below), which Evasion does not compute, so a missile's gate price never sees it.
  • Pure core, one entity entry. Reach and the pure Evasion(envelope, heading, track, lineOfSight, window) are public and allocation-free; the entity overload is what guns and missiles call. PDeviation(deviation, tolerance) stays the one deviation shape. DeviationProbability adds Evasion(target, TravelDirection) at the commit to the realized deviation; HitProbability multiplies PDeviation(Evasion(...), Tracking(source)). A laser commits at fire, so its whole vector penalty is this term. A target moving at constant velocity has Evasion 0.

Measured with this form (scratch model of Reach and ManoeuvreTrack at dt 1/60, each ship's best of a fixed jink set: lateral square wave, a new random direction, and a turn-and-burn weave of plus or minus 60 degrees, half-periods 0.5 and 1 s, read over the last 4 s of 8):

Ship, stats Window 0.5 s 0.75 s 1.0 s Coasting
Gemini-like test hull: the Longinus's 130.4 m/s^2 on all four axes, its turn 11.0 m 20.9 m 31.2 m 0
Longinus (catalog, M23, flank push as strafe) 6.4 m 14.7 m 24.9 m 0
Djinni (catalog stats, M23) 0.9 m 1.7 m 2.6 m 0

The Gemini-like hull is most evasive from every bearing; the Longinus is about seven times the Djinni, mostly by turning its drive (nose-on 6.4 m against a strafe-only jink's 1.7); the Djinni is limited by its mass. The Longinus crossing the line of sight and only weaving reads 1.5 m, and pulsing its drive instead reads about 5.5: its evasion is in its throttle and its turn, not its strafe.

The speed demon's counterplay (ruling speed-demon-counterplay): "Decent chance that unless you specced specifically to counter them, the only gun you have that'll hit a speed demon is stuff you intended to use for PD. That's what they're counting on, since they sacrificed all their armor for that mobility." The vector term delivers part of that: the shooter's Tracking forgives the evasion, and the target's envelope is large exactly when it carries little mass. The per-gun part is the mount's term in "Tracking per gun" below. Re-measured without r1's speed share: a speed demon with 60 m/s^2 on every axis, jinking a lateral square wave at half-period 0.5 s nose-on, opens 5.07 m at window 0.5 s, so a shooter with Tracking 10 keeps 0.49 and one with Tracking 50 keeps 0.90; a heavy hull with 10 m/s^2 opens 0.84 m (0.92 and 0.98). At window 0.75 s the speed demon opens 9.63 m (0.04 and 0.81). r1's numbers (22.5 m, 0 and 0.55) came mostly from the 150 m/s crossing speed this revision no longer counts. Window 0.75 s is what makes heavy guns "mostly miss" a well-flown speed demon while PD-class tracking still lands, so evasion-term r2 authors SolutionWindow 0.75; the catalog's shooters track 17 to 38 (M23 probe), and the play check owns the final value. The autofire chooser lets PD guns divert: with no worth-firing munition, an autofire gun takes the designated target or the best-priced hostile when its price clears the threshold. A worth-firing hostile munition preempts an entity subject even while that subject stays worth firing (autofire-threshold r3), so a PD group that diverted onto a speed demon still turns back to the missiles.

Tracking per gun: a mount's rate against line-of-sight angular velocity (evasion-term r4, revised by r5). Ruling tracking-per-gun: "gun times ship". Ruling evasion-angular-velocity, which replaces evasion-speed: "now that I think about it, relative velocity should be an evasion term, shouldn't it? ... yes, angular velocity is the term I was reaching for". Ruling weapon-tracking-authored, which supersedes weapon-tracking-defaults: "I never said footprint directly determines tracking." Each gun's rate is authored, on a steep scale: PD-class guns hit a speed demon reliably, the current smallest guns struggle, big guns rarely hit something fast. Ruling jink-per-ship: "Jink per ship is fine, the point is to confuse attacker's fire control after all." Soul's finding cut-evasion-term.s1.tracking-per-ship is the gap this closes: with one Tracking per ship (M27), a light and a heavy gun on one hull were forgiven alike.

Evasion has two terms, each failing the shot for a different reason, each with one owner:

  • Changes of vector defeat the prediction. This is r2's term, unchanged: Evasion(target, lineOfSight) in metres, forgiven by the ship's targeting gear Tracking(source) in metres through PDeviation, and frozen as PendingShot.Tracking, which also forgives the in-flight drift. Predicting where a target will be is the fire-control computer's job, so this tolerance stays per ship.
  • Line-of-sight angular velocity defeats the mount. FireControl.AngularVelocity(offset, relativeVelocity) is degrees(|offset x relativeVelocity| / max(|offset|, 1)^2), planar, in degrees per second; the entity overload passes the target's position minus the shooter's and the target's velocity minus the shooter's. Radial velocity contributes nothing and a shooter matching the target's velocity sees none. The shot's tracking rate is FireControl.TrackingRate(weapon, source) = weapon.Tracking * Tracking(source) / UnaidedTracking: the gun's own rate in degrees per second (WeaponData.Tracking, key 33, a PerformanceStat evaluated like Spread), times the targeting gear's Tracking as a multiple of unaided (1 unaided, 1.5 to 2.5 for the Targeting Computer, 3 to 4.5 for the Fire Control Array). That is ruling tracking-per-gun's product, and EVE's tracking computers work the same way. The factor is PMount(omega, rate) = 0.5 ^ ((omega / rate)^2): 1 for a still line of sight, one half at the mount's rate, a sixteenth at twice it, never a cliff.

HitProbability is PFire * PSpread * POnHull * PDeviation(Evasion, Tracking(source)) * PMount(AngularVelocity(source, target), TrackingRate(weapon, source)). Fire freezes the mount factor into the shot's shooter-side probability, PendingShot.PFire = solution.PFire * PMount: the mount follows the target until the round leaves, a fired round cannot be re-laid, and the mount is the shooter's, like accuracy and sensor data (M31). CommitProbability is unchanged and the HUD's estimate at fire is still the commit price. No new shot field: the one hand-built PendingShot (FireControlCut9Tests.cs:160) and every test that sets PFire keep their meaning. A beam fires through Fire every resolve interval, so it is priced afresh each time. A round fired at nothing has nothing to follow and multiplies by 1. Inspect reports AngularVelocity, TrackingRate and PMount, and the debug HUD prints them beside Tracking.

Evasion never computes the mount term. A guided round's gate price (missile-odds r2) reads Evasion(subject, line of sight) and its own correction, so the angular term does not touch it: a missile steers itself, and no mount follows it. The table authors every launcher and guided weapon at positive infinity on purpose: no mount follows a missile. Until missile-records and missile-odds land, launchers fire as ordinary shots (M1), and their price is exactly what it is at 46b28501.

The table. Each weapon's rate is authored on its record (WeaponData.Tracking, degrees per second, Min == Max, no terms), and nothing derives it: not footprint, caliber or hardpoint type (ruling weapon-tracking-authored). The initial values are written once by a one-shot AetherDb migration that carries the table below as a literal list keyed by record, then is deleted, as locomotion's restore-hulls was (M32), so afterwards the catalog is the table's only copy. The scale is steep and anchored on the ruled outcomes, with each gun placed by its role (M34): weight, damage per shot, rate of fire and size. Footprint is evidence that agrees with most of it, never the key.

Rate (deg/s) Weapons Why there
about 40 none yet: PD class, content to author Hits a speed demon reliably: 0.82 with the Targeting Computer at 150 m/s crossing 200 m.
12 ClearPath Lightest ballistic (50 kg), the lowest damage (2-6) at the highest rate (10-30 shots a second), 600 m reach: a cargo-defence hose, the catalog's nearest thing to PD, still a struggling small gun.
10 Spectra Lightest weapon (25 kg), a light beam (20-85) at 2-4 shots a second.
9 FastBlast+- "Do less damage, faster": 12-pellet bursts of 20-80 at 3-10 a second, but 125 kg.
8 Earp Single shots (5-25) as fast as the trigger, 75 kg, reach to 2,500 m: a precision gun, not a hose.
7 6k Shooter Earp's role, heavier (95 kg) and shorter.
6 ColdFire Heaviest small gun (150 kg), a 40-160 laser every 1 to 2.5 s to 2,250 m: a small sniper.
4 Autocannon Medium and four cells but light (75 kg) with 3 damage a shot: the quickest of the medium guns.
3.5 ChargeBlast+- 225 kg, one charged knockout shot (25-110, tripled when charged) to 1,900 m.
3 DeathCluster, ChargeBlast SG, pretty pretty bang bang 250 kg each, heavy volleys of 5 to 15 pellets of 50-256: the medium shotguns.
2 plight Large charged lightning, 75-400 (tripled when charged), one shot every 2 to 4 s after a 2 to 3 s charge: a heavy gun (ruling plight-not-one-cell).
1.5 CShot RainbowLite Lazer The heaviest gun (500 kg, nine cells), 100-600 to 3,000 m; fits no hull today.
infinity GT 3K, pswarm, scorched void policy, LRMM72, SRMM72 No mount follows a missile; the round steers itself (missile-odds).

A field left unauthored defaults to positive infinity, a mount that follows anything, so code-built fixtures price exactly as before. That default is harmless only while every shipped gun carries a finite rate, which a test over the catalog pins; a weapon authored later without a rate would follow anything, and that test is what catches it. Fitted by M33 at the TC / FCA midpoints, with crossing speeds at the hulls' real top speeds (M32):

Geometry (degrees per second) PD 40 12 8 6 4 3 2
Fast crosser close: Longinus at its 100 m/s top speed, 200 m (28.6) 0.91 / 0.98 0.37 / 0.76 0.11 / 0.53 0.02 / 0.33 0.00 / 0.08 0.00 / 0.01 0 / 0
The same at range: 100 m/s at 1,000 m (5.7) 1.00 / 1.00 0.96 / 0.99 0.91 / 0.98 0.85 / 0.96 0.70 / 0.90 0.53 / 0.84 0.24 / 0.67
Heavy ship at range: Djinni at its 50 m/s top speed, 1,000 m (2.9) 1.00 / 1.00 0.99 / 1.00 0.98 / 0.99 0.96 / 0.99 0.91 / 0.98 0.85 / 0.96 0.70 / 0.90
Heavy ship close: Djinni at 50 m/s, 200 m (14.3) 0.98 / 0.99 0.78 / 0.93 0.57 / 0.85 0.37 / 0.76 0.11 / 0.53 0.02 / 0.33 0.00 / 0.08
Jinking target: Longinus weaving (14.7 m), 60 m/s at 500 m (6.9) 0.26 / 0.61 0.25 / 0.60 0.23 / 0.59 0.21 / 0.57 0.16 / 0.53 0.11 / 0.47 0.03 / 0.34
Speed demon (no hull authored): 150 m/s at 200 m (43.0) 0.82 / 0.94 0.11 / 0.53 0.01 / 0.24 0.00 / 0.08 0 / 0 0 / 0 0 / 0
Speed demon jinking close: 9.63 m, 150 m/s at 200 m (43.0) 0.42 / 0.70 0.06 / 0.40 0.00 / 0.18 0.00 / 0.06 0 / 0 0 / 0 0 / 0
Uncapped LonginusX: 800 m/s at 1,000 m (45.8) 0.80 / 0.94 0.08 / 0.49 0.00 / 0.20 0.00 / 0.06 0 / 0 0 / 0 0 / 0

Each cell is PDeviation(E, Tracking) * PMount, the whole evasion share of the price. Unaided (Tracking 10), the fast crosser close leaves a PD gun 0.70 and ClearPath 0.02. Crossing at 200 m with the Targeting Computer, by speed, for PD, 12 and 4: 50 m/s gives 0.98, 0.78 and 0.11; 100 m/s gives 0.91, 0.37 and 0; 150 m/s gives 0.82, 0.11 and 0; 300 m/s gives 0.45, 0 and 0. The ruled outcomes hold: a PD-class gun hits a fast hull close reliably (0.82 to 0.91 with the common gear), the smallest guns struggle (0.37 at best, 0.02 to 0.11 for most), the big guns rarely hit something fast (0.08 at most), and the big guns hit a big ship at range (0.70 to 0.91). Against a jinking target at middle range the guns barely differ, because the jink is a prediction problem the ship's gear forgives (ruling jink-per-ship), not the mount; they part company when the target is close and fast. The individual rates are the play check's levers; the anchors are the ruled outcomes.

Not imported from EVE. Signature resolution and signature radius: the probe meets every ruled outcome without them, and a target's size already enters the price through its silhouette (PSpread, POnHull). Falloff: range is priced by PSpread and the weapon's range gates.

Counters to the close orbit. EVE's documented failure mode, the small fast ship orbiting close while big guns go useless, is the play shape ruling speed-demon-counterplay asks for, so the counters must exist and lie outside the big gun:

  • PD-class guns, authored near 40 deg/s. The catalog has none yet; they are content to author. plight, the one-cell gun, is a heavy gun whose footprint is a content error (M35).
  • Gear: the Fire Control Array multiplies every mount by 3 to 4.5, so ClearPath on a dedicated fit keeps 0.76 against the fast crosser close.
  • Range: angular velocity falls with range, so a ship that holds the crosser off sees it at a fifth of the rate at 1,000 m; the fast ship has to come close to use its own guns, which face the same term.
  • Velocity: a shooter that matches the target's velocity sees no angular velocity, so a fast hull of one's own is a counter.
  • Missiles: a guided round has no mount (missile-odds), so launchers counter it as drones do in EVE.
  • Armour: the speed demon gave its up, so the rounds that land hurt.

Top speed is a hull stat, and so an evasion stat (M32). The Longinus is capped at 100 m/s and the Djinni at 50 by their own VelocityLimit behaviour, and the table above is fitted at those speeds. The uncapped LonginusX row records what an uncapped hull would do; the hull itself is retired (ruling retire-longinusx), question longinusx-top-speed is withdrawn as moot, and after the retirement every ship hull in the shipped catalog carries a cap ("The dock intro", below).

When articulated mounts arrive (ruling arc-and-traverse-on-the-link, follow-up articulated-mounts), the link's traverse speed and the gun's tracking compose by the slower of the two inside TrackingRate: a heavy gun on a fast link is still slow, and a pod of small guns on a slow link slews slowly, as that ruling says. Until then a fixed mount follows at the gun's own rate within its arc.

The missile's price (missile-odds). At the gate, the same model with the missile's inputs:

Gun factor Missile factor
PFire = shooter Accuracy x PSensor(shooter info) data: locked, PSensor(settings, 1, seeker reading), its own gathered data; unlocked, the frozen launch PFire
realized deviation in flight zero-effort miss beyond reach, max(0, ZEM - reach), with reach = max(BlastRadius, SchematicCellSize / 2) plus half the target's silhouette span across the line of sight
Evasion against the gun's bearing Evasion(target, line of sight): the same entity call a gun makes
tolerance Tracking correction: locked, Cognition * Closable(Lateral, DeltaV, time to go); unlocked, 0
PSpread, POnHull 1: a guided round has no barrel spread and steers onto metal

Closable(a, dv, t) is the sideways distance a missile can still cover in t with acceleration a and delta-v dv: 0.5 * a * t^2 when dv >= a * t, else dv * t - dv^2 / (2a). Terminal fuel, lateral authority and cognition widen the tolerance; seeker data scales the whole; the target's motion and agility and the flight's own miss eat into it. An unguided rocket (no seeker, no lateral authority) has tolerance 0, so geometry alone decides it: it connects only when its straight line passes within reach.

Cognition (ruling missile-cognition) is Control.Cognition, 0 to 1, authored per missile and later derived from guidance gear: the missile's skill at spending delta-v, in exactly four places. (1) Efficiency: the debit is |command| * dt * (2 - Cognition), so a mindless missile wastes as much as it uses on corrections. (2) Reserve: before the gate, pursuit and evasion may not spend below Cognition * Lateral * CommitHorizon, the delta-v a full-authority endgame needs, so a smart missile arrives with its terminal and a dumb one may arrive dry. (3) When to jink: evasion against inbound PD draws only on fuel above the reserve, so a dumb missile (reserve 0) jinks itself dry while a smart one stops jinking to keep its terminal. (4) The price: the correction scales with it.

The missile's stats (ruling missile-stats-now-gear-later). One MunitionData object on LauncherData and on GuidedWeaponData, grouped by the gear each group will later come from, so the entity cut replaces a group with a derivation from that gear without reshaping anything that reads it:

Group (gear later) Fields (PerformanceStat, evaluated through the item) Replaces
Hull (missile hull) Durability, Radius, Signature the first pass's global settings
Thrusters (missile thruster) Acceleration (main motor), Lateral (manoeuvre authority: the sideways acceleration cap), MaxSpeed Thrust, MissileVelocity
Sensors (seeker) Sensitivity, ConeHalfAngle, Range nothing (new)
Control (guidance, its cognition) Navigation (PN constant), Evasion (share of Lateral spent jinking), EvasionFrequency, Lifetime, Cognition (skill at spending delta-v, 0 to 1, ruling missile-cognition) DodgeFrequency, GuidanceCurve
Fuel (tank) DeltaV (m/s) nothing (new)
Payload (warhead) no new field: Munitions.Payload(item) reads the weapon's existing Damage, DamageSpread, DamageType, BlastRadius, Fuse nothing; one reader

Payload is an accessor, not copied fields: the warhead already has one owner on WeaponItemData (M17), and a copy would be a second. LiftCurve stays presentation outside the groups. ThrustCurve dies once the presenter reads the live throttle. The missile-stats cut lands the object, migrates the catalog (Thrust to Thrusters.Acceleration, MissileVelocity to Thrusters.MaxSpeed, DodgeFrequency to Control.EvasionFrequency), authors the new fields for GT 3K, pswarm, scorched void policy, LRMM72 and SRMM72 (M13; the flat dumbfire pair get no seeker and no lateral authority, so they fly as rockets), and deletes the three migrated fields before anything reads the stats. missile-presenter deletes GuidanceCurve and ThrustCurve once the presenter stops reading them. Together they close follow-up launcher-flight-fields.

A missile is still its pending shot. A shot is a munition when its weapon data is LauncherData or GuidedWeaponData (Munitions.IsMunition, the one test). Its identity is its ShotId; its flight state is one inline struct on the shot, MunitionFlight: planar Position and Velocity, DeltaV remaining, Command (the last step's commanded acceleration; the presenter's throttle is its length over Acceleration, and its sideways part is the munition's acceleration as a target), Wear, its Seeker subject (an Entity, null when it has none), Locked, and the source's faction frozen at fire. No second list, dictionary or class of missiles exists beside Zone.PendingShots. This is still the asteroid shape: a light record with a little state, simulated where the simulation lives, no Entity, gear or agent behind it.

Launch. Fire freezes, as today, the launch data the shooter's own sensors gave it (FireTargetPosition, FireTargetVelocity; a LauncherData round still needs the launcher's lock, M17). The flight starts at the fire origin with the shooter's velocity plus weapon.Velocity along the mount (LaunchDirection), full DeltaV, and the shot's target as its seeker subject, unlocked (none for a round fired along the aim, whose launch data is the aim point or burst position). A munition shot's CommitTime is infinite until its terminal gate opens; ArrivalTime is FireTime + Control.Lifetime, its self-destruct time, until the gate replaces it.

The step. FireControl.Step advances each live munition once per tick, before its resolution tests, through one function Munitions.Advance(Zone zone, ref PendingShot shot, float dt):

  1. Seek. The seeker's reading of its subject is the settled sensor value (M16) with one sensor: Sensor.Gain(subject.Visibility, Sensitivity, cone(angle off the velocity), distance, 1) over TargetInfoDecay, saturated; zero beyond Sensors.Range or outside ConeHalfAngle. Locked while it exceeds TargetDetectionInfoThreshold. Nothing else is stored. The settled-value arithmetic moves out of Entity.ChunkInfo into one function both call.
  2. Aim point. Locked: the subject's live position and velocity (terminal guidance on its own sensor). Unlocked: the launch track extrapolated, FireTargetPosition + FireTargetVelocity * (now - FireTime) (midcourse: it knows where it was sent). A missile in flight is told nothing else (ruling shared-track-bearing-only).
  3. Command. Proportional navigation at the aim point: lateral acceleration Navigation * closing speed * line-of-sight rate, clamped to Thrusters.Lateral, plus along-track acceleration toward MaxSpeed, clamped to Acceleration. PN needs only the bearing and its rate, so terminal guidance uses no data the ruling forbids.
  4. Evade. While PD rounds are inbound at it (InboundRounds > 0, which arrives with munition-shots, the cut that makes it shootable), it adds a jink of Evasion * Lateral whose sign follows a phase seeded from its shot id at EvasionFrequency, drawing only on fuel above its reserve (cognition, above). Evasion and pursuit share the clamp and the tank.
  5. Pay. DeltaV -= |command| * dt * (2 - Cognition). Before the gate a command that would take DeltaV below the reserve Cognition * Lateral * CommitHorizon is scaled down to what the reserve allows; after the gate the whole tank is usable. A dry missile commands nothing and coasts on its velocity (Nebulous).
  6. Move and gate. Integrate position. Munitions.Terminal holds when the missile has a seeker subject and the time to its closest approach with that subject (both straight motions) is within CommitHorizon and before its lifetime ends. missile-odds opens the gate there (CommitTime = now, ArrivalTime = now + time to go) and the generic Commit rolls. Until missile-odds lands nothing opens the gate, and every missile ends at its lifetime.

Resolution. At the gate the generic Commit rolls the munition price with the per-shot seed and publishes ShotCommitted: Hit or Miss. Between commit and arrival (at most CommitHorizon) the flight presents the outcome and decides nothing: after a hit it closes on the target's live position with whatever sideways acceleration the closure needs, without debit; after a miss it commands nothing and coasts past. At ArrivalTime the generic Apply resolves it. A hit detonates through Detonate at the hull entry point on the missile's line to the target (HullEntry, M19), or at the target's position when the line finds no cell, with radius max(BlastRadius, SchematicCellSize / 2). A miss damages nothing: a fused missile's near miss is the presenter's flash, not a blast, so the roll is the only thing that decides the target's damage. A munition that reaches its lifetime without a gate resolves Miss at its position (a fizzle). Interception keeps the first pass's rule at every stage, committed included: wear at Hull.Durability resolves a fresh Intercepted outcome, no damage. A seeker subject that leaves the zone before the gate makes the missile blind (retargeting, below); after the gate it resolves a fresh Miss, the target-gone precedent (M4).

Retargeting is ruled: missiles-seeker-retarget, hostile-in-cone. A missile with no subject, or one outside its cone, queries the targeting index every GameplaySettings.SeekerInterval (0.5 s, Nebulous's re-selection period) for entities hostile to the frozen faction, within Sensors.Range and the cone, and takes the brightest reading above threshold. Turning invisible works through the same reading: a target whose visibility falls below what the seeker needs is lost, and the missile seeks again. Decoys need something a seeker can read that is not a hostile ship; no cut here makes one (follow-up missile-decoys-signature).

Seen and shot like a rock. A munition's detectability uses the passive balance with its own Hull.Signature; the index provider MunitionTargets offers every live munition at its stepped position; TargetRef gains MunitionId; Entity.SetTarget refuses a munition. PD rounds at a munition are priced and rolled by FireControl as the first pass said; lead now reads the missile's real velocity, and the deviation term reads its real stepped position against the frozen prediction, so jinking is what beats PD. As a target a munition takes the shared Evasion term with its own envelope (Lateral each side while it has delta-v, no turn) and a ManoeuvreTrack fed its commanded acceleration (munition-shots must be revised to this interface), so a jinking missile is harder to hold and a dry one is easy prey. The "round arrives too late" gate reads Munitions.TimeToGo (distance to its aim point over closing speed) instead of a fixed arrival.

The presenter. GuidedProjectile places itself at the stepped position (height from LiftCurve over its life), faces the velocity, and drives its flame from the length of Command, dark when dry. Between commit and arrival it follows the record's post-commit flight. It plays a hit at a Hit (at the burst point), a kill at Intercepted, a near-miss flash at a committed Miss and a fizzle at a lifetime Miss.

Thresholded autofire. A weapon group can be set to autofire with a threshold (default AgentMinHitProbability, 0.2). Each tick, before behaviours, Autofire.Update gives each weapon of an autofire group a subject from FireControl.ChooseSubject and activates it when one is chosen, deactivating it otherwise. ChooseSubject keeps the current subject while it is still worth firing, except that a worth-firing hostile munition preempts an entity subject; otherwise it takes, in order, the most urgent detected hostile munition in reach (least Munitions.TimeToGo), then the designated target, then the detected hostile entity in reach with the highest price, each only if worth firing. "Worth firing" is one function, FireControl.Worth(weapon, shooter, subject, threshold): the price against the threshold for an unfused weapon, designated and not refused for a fused one. AgentFires becomes Worth at the agent threshold. FireControl.SubjectOf(weapon, shooter) is the one answer to "what does this weapon fire at": its autofire subject in an autofire group, else the entity's designated target. Fire, Refuses, Solution, ArcPermitsFire and the stance gate read it.

What makes a gun point defense is ruled: pd-who-engages, any-gun ("anything can be PD"). Nothing in data; any autofire gun can engage a munition, and the price decides: a fast, tight, short-flight gun clears a threshold against a small fast target, a heavy gun does not. Heat is the waste guard.

Manual fire against autofire is ruled: controls-solution-subject, designated ("manual guns fire only on selected target when solution is available, autofire guns seek their own firing solutions"). Manual fire is controls-mount-aim r2 unchanged (SubjectOf returns the designated target); autofire's own choice is ChooseSubject. The trigger is ruled too: controls-solution-trigger, group-trigger ("Group triggers only"): holding a group's trigger fires each gun on its solution, else along the aim when the aim is in its arc, else holds. A launcher fired along the aim with no solution launches at the aim point as its launch data and its seeker may acquire what it finds there (HighFleet's blind fire).

The AI. Agents fire through the same chooser (ai-autofire): Combat and TurretController treat every group as autofire at the agent threshold, with their chosen target as the designated one. One owner of "which subject, and is it worth it" for player and AI alike, and AI point defense against the player's missiles falls out of it. The revision leaves ai-autofire r1 as it is: it reads only ChooseSubject and Worth.

The HUD. An autofire group's action-bar slot shows a filled mark and its threshold. ControlsHud draws a small diamond on each detected hostile munition within the ship's longest weapon range: filled while it can still manoeuvre, hollow once it coasts dry, ringed while one of your autofire guns has it as subject.

Authority map

  • Owner. FireControl owns the one hit price for every shot, gun, beam or guided (HitProbability, CommitProbability, Evasion, PDeviation, Reach), every munition transition (fire, gate, commit, application, interception, lifetime, resolution) and every subject question (SubjectOf, ChooseSubject, Worth). Munitions owns the pure facts and the flight arithmetic (IsMunition, Payload, Advance, TimeToGo, Terminal, Reserve, the seeker reading); FireControl.Step is its only caller that writes a shot. MunitionData owns a missile type's stats, cognition included, until gear replaces each group. Entity.Update owns Entity.Manoeuvre (and the acceleration it observes); Ship owns Ship.Envelope, summed from what each propulsor reports. The player owns which groups are autofire and their thresholds.
  • Inputs. The frozen launch data, the flight state, zone time and dt, the seeker subject's live position, velocity, acceleration and visibility, the munition's stats, the count of PD rounds inbound; the weapon's stats and the shooter's sensors, accuracy and tracking; the target's strafe thrust and mass; Entity.Autofire.
  • Outputs. Munition positions, velocities and commands for the index, the HUD and the presenter; Hit, Miss and Intercepted outcomes; blasts through Detonate; prices that carry the tracking term for the HUD, the AI and autofire; weapon activation for autofire groups; the saved autofire settings.
  • Derived state. Seeker reading (every step, never stored beyond Locked), time to go, the terminal fact, reserve, zero-effort miss, correction, agility, evasion, throttle (from Command), detectability, the index's munition region.
  • Demotions. The first pass's derived pose is gone: a munition's position is stepped state, written only in Munitions.Advance under FireControl.Step. The second pass's geometric fuze never lands: the flight decides only whether and with what a missile reaches its gate; the roll at the gate decides contact, and the post-commit flight only presents it. Flight time is no longer the only way a manoeuvre reaches a price: the tracking term reaches every shot. GuidedProjectile owns only how a round looks. Thrust, MissileVelocity, DodgeFrequency, GuidanceCurve and ThrustCurve no longer exist as loose launcher fields. Entity.Target is the designated target, the subject of manual fire only. PendingShot.Target derives from Subject.
  • Forbidden writers. Any write of a MunitionFlight field outside Munitions.Advance and Fire, except Wear (only FireControl.DamageMunition) and InboundRounds (only FireControl's shot transitions). Any contact, damage or dice in Munitions. A commit point for a munition other than its terminal gate; a second price for guided rounds beside CommitProbability's munition branch. An evasion, reach or deviation-shape formula outside FireControl.Evasion, Reach and PDeviation. Any write of Entity.Manoeuvre outside Entity.Update. Homing, intercept or guidance arithmetic in Gameplay or UI. A copy of warhead fields inside MunitionData. A global setting standing in for a per-missile stat. Any read of Entity.Target as a weapon's subject outside SubjectOf. A second worth-it test beside Worth. A second record type, list or dictionary of missiles beside Zone.PendingShots.
  • Shared paths. Guns, beams, instant weapons, PD rounds at munitions and guided rounds all price through PDeviation and Evasion, commit through Commit and apply through Apply. Missile hits, mines and fused rounds all damage through Detonate. The seeker and Entity.ChunkInfo read the same settled-sensor function over Sensor.Gain. Player autofire, Combat and TurretController choose through ChooseSubject; the presenter, the HUD and the index read the same flight state.
  • Deletion line. Before new behaviour: the loose launcher flight fields go in missile-stats (migrated, then deleted); DeviationProbability's inline saturate(1 - deviation / Tracking) becomes PDeviation in evasion-term before the term is added; GuidedProjectile's homing, dodge and split code and the two curves go in missile-presenter; the stored PendingShot.Target field becomes Subject in munition-shots.

Model page rows

Kind Identity Lifecycle Authority
Munition (light record) Its PendingShot's ShotId, as MunitionId in a TargetRef. From fire through its terminal gate (commit) to arrival, or until interception or lifetime; never saved, as pending shots are not. FireControl for transitions and the roll; Munitions.Advance for flight.
Munition flight PendingShot.Flight (position, velocity, delta-v, command, wear, seeker, lock, faction, inbound rounds, Track). Stepped every tick; gone with its shot. Flight.Track observes the command Advance applied each step; it is runtime, never saved, and discarded with the shot. Munitions.Advance under FireControl.Step, the one writer of the track (as Entity.Update is for a ship); Munitions.Envelope derives what it can reach from its stats and fuel, and FireControl prices it through the same evasion core as a ship; wear by DamageMunition.
Entity manoeuvre track Entity.Manoeuvre (ManoeuvreTrack: trend, filtered innovation, its second moment). Observed every update from the velocity change across the base update; never saved. Entity.Update.
Ship envelope Ship.Envelope (ManoeuvreEnvelope: four body-frame accelerations, two turn rates). Recomputed every update from the live propulsors; never saved. Ship, from each propulsor's report.
Missile stats MunitionData on LauncherData / GuidedWeaponData, six groups. Catalog data in Aetheria.cc. The catalog now; gear once munitions are entities.
Autofire setting Per weapon group, Entity.Autofire (on, threshold). Set by the player; saved in EntityPack key 21. The player.
Autofire subject Per weapon, Weapon.AutofireSubject, runtime only. Re-picked every tick, and when the current one stops being worth firing. FireControl.ChooseSubject.

Rationale

Why stepping now. The first pass refused stepping because a stepped missile would hold a second clock of arrival beside FireControl's frozen timing. Ruling missiles-fuel-and-seekers makes the outcome depend on what happens in flight, which no frozen time can know; so the frozen timing and the roll leave guided rounds and the flight becomes their one clock. Stepping costs a few dozen operations per missile per tick, pinned by measurement in missile-records.

Why PN on a bearing. Ruling shared-track-bearing-only lets a missile in flight hold a bearing and nothing more. Proportional navigation is the textbook guidance law and needs only the line-of-sight angle and its rate, so the law and the ruling agree, and the textbook's failure modes (a late hard break, burn-and-coast against augmented PN, low energy at long range) become the dodging the operator asked for.

Why odds at one gate. Ruling missiles-resolution-model reverses the second pass's geometry-only recommendation, and the reason is the operator's: at this simulation's scale a fuze is not physics, it is a coarse proxy that throws away terminal fuel, seeker data and cognition. The second pass's objection to rolls (flight and dice answering one question twice) is met by placing the roll where guns place theirs, at a commit before arrival, and making the flight after it presentation. The flight still matters: it is what spends fuel, loses seekers and builds the miss the price reads. The entity cut keeps this shape: gear replaces the stats the price reads, not the price.

Why evasion is envelope times unpredictability. Ruling evasion-not-only-flight-time: in-flight deviation only charges a dodge the round's flight time allows, so a laser cannot be dodged (M20); a tracking term charges every weapon. Ruling evasion-is-unpredictability says what the term measures: "The Gemini is evasive because it can floor it in any direction at any time (with each new vector fouling the targeting solution), whereas the Longinus needs to aim itself where it wants to go". That is Singer's split, capability and manoeuvre frequency, and the naval lesson that fire control holds a steady course and loses a changing one. The reachable spread is the capability because it is the only measure that sees turn time, mass and every thruster at once (M24 shows the strafe figure sees none of them correctly). The innovation statistic is the behaviour because a steady burn and a coast are both predictable and dithering is not evasion. min is the product of a saturated use fraction and the capability, written without a tuning constant. Rejected: r1's strafe-over-mass agility (M24); transversal speed inside this term (the lead cancels it as prediction; ruling evasion-angular-velocity prices it in the mount's own term instead); instantaneous jerk (frame-dependent, rewards dithering); a per-hull agility stat in data (the envelope derives from fitted gear, so refits and damage change evasion with no authoring).

Why the two evasion terms multiply. Ruling evasion-angular-velocity keeps changes of vector and adds the line of sight's angular velocity. They fail a shot for different reasons with different owners: a perfect predictor on a slow mount still lags a fast crosser, and a fast mount still loses a jink it could not predict. The naval directors split them the same way (the rate term solved, the manoeuvre unmodelled), and EVE's exponent sum is itself a product of per-term factors. The price was already a product of independent factors (PFire, PSpread, POnHull, PDeviation), so the mount's factor joins it with no tuning constant between the two terms. Rejected: turning the angular velocity into metres (omega * range * window) and adding it to the deviation. That makes one tolerance answer two questions, couples range into the vector term, and would let a mount forgive in-flight drift, which no mount can re-lay once the round has left. Rejected: PDeviation(omega, rate), the linear shape. It reaches zero at the mount's rate, so a gun goes from fine to never within a factor of two in speed, and "rarely" becomes "never". The Gaussian keeps a tail, and its rate reads as the angular velocity at which the gun hits half its shots, which a designer can author in the inspector.

Why the vector tolerance stays per ship and the ship multiplies the mount. Ruling tracking-per-gun asked that a shot be forgiven per gun, "gun times ship", when evasion had one term. With two terms, the per-gun part belongs where the gun's mount is the limit: the angular term, forgiven by the gun's rate times the gear. The jink is a prediction error, which is the targeting gear's job (Singer's split, the naval directors), so its tolerance stays the gear's metres, and r3's dimensionless multiplier on those metres, never built, is dropped. A heavy gun therefore loses to a fast close crosser, not to a weave it cannot predict any worse than a light gun can. The gear keeps both of its effects: metres of prediction forgiven, and as a multiple of unaided, the rate every mount on the ship follows, as EVE's tracking computers multiply turret tracking. Rejected: a per-gun multiplier on the vector tolerance as well, which needs a reference rate constant to turn degrees per second into a dimensionless factor, and charges the mount for the director's error.

Why reactive evasion. The operator's sentence is "a missile that spends fuel evading PD". Evasion triggered by inbound rounds makes PD fire cost the missile fuel even when it misses, so a saturating salvo and a well-placed PD group both change what arrives. A constant terminal weave (Nebulous) would charge every missile the same and make PD irrelevant to the fuel story.

Why stats per missile type, in gear-shaped groups. Ruling missile-stats-now-gear-later. The first pass's global durability, radius and visibility are replaced. Each group is what one gear category will supply, so the entity cut swaps a group's source and nothing that reads Munitions changes.

Why the player cannot designate a missile. Presses that land on a swarm of rounds instead of the ship behind them are a classic irritation, and the operator asked for autofire precisely so that PD needs no micro.

Cut order. missile-stats (r2) first: the stat object with Cognition and the catalog migration, no behaviour change. evasion-term (r1) needs nothing and may run beside it: the tracking term in every gun price. missile-records (r3) after missile-stats: flight, seeker, reacquisition, cognition's efficiency and reserve, the terminal fact, lifetime, interception, the provider and the cost pin, headless; missiles fly but never hit. missile-odds (r1) after missile-records and evasion-term: the gate, the price, the commit, the post-commit flight and the blast. missile-presenter (r2) after missile-odds and controls-helm. munition-shots (r3) after missile-odds and controls-mount-aim. autofire-threshold (r3) after it and controls-hud; ai-autofire r1 last, after controls-ai-bearing. Splitting the old missile-records keeps each Hands pass inside its budget: the flight alone sat near the ceiling, and the price is a second subject with its own tests.

Left for later. Full munition entities (follow-up drones-munitions-substrate), whose gear replaces MunitionData group by group; midcourse bearing updates from the launcher over a datalink (ruling shared-track-bearing-only allows a bearing; nothing sends one yet); seeker kinds, decoys (follow-up missile-decoys-signature: a decoy needs a signature the seeker's reading can take), jamming and soft kill; boost and midcourse fuel planning; mines as records (bodies-entity-and-mines); whether launchers draw rounds (launcher-ammunition). Whether the demo cast fields launchers for the player's PD to meet is content: the cuts add a Point Defense scenario for the operator check.

Why a munition's evasion goes through the ship's core. Follow-up munition-shots-evasion-interface (absorbed by cut-munition-shots) asked that a munition answer FireControl.Evasion "with its own manoeuvre envelope (from its missile-stats thruster group) and its own course-change record, like ships do". The earlier price went through a direct agility (Stats.Lateral while fuelled) and the command vector, which is the strafe-only reading ruling evasion-is-unpredictability overturned for ships. The munition now gives the core the two inputs it reads: Munitions.Envelope(flight) (faces equal to Advance's own command clamps; no turn rates, because a munition commands any planar vector at once, MA3) and Flight.Track (observed by Advance, MA4's pattern). Then Evasion(flight, los) is one line over the shared core, a jinking missile is evasive because its track sees changes of vector, and a dry one is not because its envelope is zero. A missile pursuing a weaving target also shows innovation from its PN commands; that is evasion by the ruling's own measure ("how much the ship is actually changing its vector"), not a defect.

Why the mount term is the munition's own geometry. Follow-up munition-shots-mount-term: PMount(AngularVelocity(Flight.Position - source.Position.xz, Flight.Velocity - source.Velocity), TrackingRate(weapon, source)) (MA6), frozen at fire like an entity's (MA5), with the jink forgiven per ship by Tracking(source) (ruling jink-per-ship). Inbound missiles approach mostly radially, so their angular velocity is small and PD guns hold them; a missile crossing close is hard to follow, which is the CIWS geometry and EVE's tracking formula 0.5^((omega/rate)^2) the mount term already is.

Why the box support is enough. Reach's no-turn support assumes the lateral and along-track faces fire independently (a box), while Advance clamps the total command to Stats.Acceleration (a disc with a lateral cap). Across the line of sight the binding limit is the lateral face, which the box and the disc share; the corners overstate reach only on oblique lines of sight, and the observed track caps the evasion before the envelope does in every case the tests build. A disc support would be a second reach function for one corner case; not bought.

Why munition-shots is split. The earlier estimate was 465 lines and 12 tests across Solve, Fire, Step, Commit, Apply, Detonate, Solution and Advance, and the envelope, track and mount term add to it. The evasion-term cut of similar reach took 190 calls and overran (memory hands-batch-sizing-and-stryker-offsets). The jink (inbound-round count, Advance step 4 and the fuel-spend tests) is a separate behaviour with its own rulings (missiles-fuel-and-seekers, missile-cognition) and its own failure modes, and it reaches the PD price only through the track. So munition-shots is the subject, price and resolution (about 380 lines) and munition-jink is the jink (about 170 lines), each falsifiable alone.

Why the autofire divert test uses a steady close crosser. Under ruling evasion-angular-velocity-r2 a steady fast crosser close in evades through the mount term even with no jink, and that is exactly where ruling speed-demon-counterplay wants light guns to hold a ship heavy guns cannot. MA11's fixture gives the split (PMount >= 0.85 against <= 0.01) with zero evasion, so the test proves the chooser reads the per-gun price and nothing else. The threshold is set inside the interval between the heavy gun's price and the lower of the light gun's price and the heavy gun's price without the mount, so a Worth that drops the mount, or reads the ship's gear in place of the gun's rate, lets the heavy gun fire and fails.

Why autofire-threshold is split too. The earlier version (about 520 lines) mixed the headless chooser, Worth, SubjectOf, the saved settings and 13 tests with Unity work (action-bar modifiers, HUD diamonds, a new scenario) whose every loop needs a Starfire compile. autofire-threshold keeps the sim half (about 360 lines) and gives the settings one writer, Entity.SetAutofire, so the presentation half, autofire-controls (about 190 lines), writes nothing else.

Unowned checks.

  • missile-odds was written against evasion-term. It prices a missile's hit on a ship with the target's evasion; whether a missile as shooter takes any mount term was never ruled (the tracking table lists launchers at +infinity: "the round steers itself"). Worth a check before missile-odds is dispatched, if no parallel pass already owns it.
  • No PD-class gun existed (MA12). The Point Defense scenario and the speed-demon play check could run on ClearPath and Spectra, but the ruled outcome "PD-class guns hit a speed demon reliably" has no catalog gun to show it until one is authored (see "Catalog growth").

Catalog growth: generic designs, branded products

Ruling catalog-grows-generic-designs-branded-products (operator, 2026-10-08): designs are generic and branding lives in products. A design is expected to carry several products. Title is the one owner of an item's displayed title (ItemManager.Title, landed by product-titles); there is no DisplayName. Body: GameCult/Aetheria origin/master 4d664b537f76029fbaee94392da3d8a122f81339, unless a fact names another anchor (LP facts read the product-titles head 7f700d3c). Read by cut-pd-gear, cut-lot-product, cut-generic-design-names, cut-product-lines-weapons, cut-product-titles and cut-pirates-record; question minor-power-brands and follow-up gear-product-lines own the rest of the gear.

Body facts

Catalog decode method for CG1-CG4: cultcache_py SingleFileMessagePackBackingStore. pull_all() over GameData/Aetheria.cc at 4d664b53 (LFS pulled into the worktree), each payload msgpack.unpackbed and read by MessagePack key (the M28 method); the store is cultcache.store.v1 with 13 schemas and 217 records. Scratch scripts were not committed.

  • CG1. The catalog's record census at 4d664b53. 64 FactionProductData, 51 CompoundCommodityData, 32 GearData, 18 WeaponItemData, 13 SimpleCommodityData, 12 Faction, 12 NameFile, 5 HullData, 4 CargoBayData, 1 FieldKindData, 1 DockingBayData, 1 InputLayout, 0 Loadout. Factions (short names): AU, Adrasteia, Alakrita, DME, Ewan Hart, Finch, Lightsail, Lucent, Miss Terri's, NiteLife, R&D, Zhestokost. No Pirates record yet (H1).

  • CG2. The data model already separates design from product; the content does not (source read plus CG1). FactionProductData (ServerShared/FactionProduct.cs:14-31: Name 1, Description 2, Design 3 CultRecordRef<CraftedItemData>, Manufacturer 4 CultRecordRef<Faction>, Roles 5 ProductRole{Role, Mean, StandardDeviation}) is a manufacturer's branded variant of a design; its own comment says a market segment is "a second product with a role pumped up". Price is on the design, not the product (HullData.Price, H1). Generation and station stock pick products, not designs (LoadoutGenerator.RandomProducts, :142-171; GenerateStationLoadout stocks 16 random products, :107-114). Presets build a design by its first available product in record-key order (Loadout.Resolve, Loadout.cs:160-191). But of the 59 sold designs only Targeting Computer and Fire Control Array carry more than one product (3 each: Panopticon Prime by Finch, ClapBack Ultra by Lucent, LockOn Pro by NiteLife on the array, and Panopticon, ClapBack, LockOn on the computer); every other design has exactly one product, and in 49 of them the design's own name is the brand (product name equals design name: ClearPath, Spectra, Earp, Manhattan, Store-All Plus ...). Three designs already have generic names under a brand product (Core Power -> Steadfast, plight -> DragOnBreath, pswarm -> Leonid); seven generic-named designs sell a product of the same generic name (Autocannon, Cockpit 2x2, Turret Control Module, Industrial Thermostatic Heater, Medium Drive, Small Drive, Medium Docking Bay). Most designs' Description is the brand's voice ("Defend your precious cargo and have fun doing it"); 20 products carry an empty or [TODO] description. 64 products over 60 designs; 0 duplicate (design, maker) pairs.

  • CG3. Where the player sees a name (source read, master). PropertiesPanel.GetTitle (UI/Properties Panel/PropertiesPanel.cs:431-437, data.Name) and the trade list (UI/Menu/TradeMenu.cs:193-197, the Name column, tier-coloured) and buy dialog (:355) show the design Name. The product name and description appear only as extra property lines after Brand (PropertiesPanel.cs:370-380); Inspect(ItemData) shows the design as Type (:530). So today the brand reaches the headline only because design names are brand names, and a product named in emoji would be listed under its design's name. product-titles moves the headline to the product (see LP3).

  • CG4. A lot does not record its product (source read, master). Lot (ServerShared/Provenance.cs:64-70) holds Design 0, Origin 1, Quality 2, Roles 3. ItemManager.CreateLot(product) (ItemManager.cs:134-155) writes the maker into Origin and drops the product. ItemManager.Brand (:206-222) re-derives the product as the lowest-key FactionProductData with that maker and design. Two products of one design by one maker (the market segment ProductRole's comment designs for) are therefore indistinguishable after creation: the player sees the lower-key product's name and text whatever was bought. Today no maker sells two products of one design (CG2), so nothing shows it yet.

  • CG5. Weapon fields and the reference small guns (source read Behaviors/Weapon.cs:20-72, Behaviors/InstantWeapon.cs:10-20, ItemData.cs:477-509; catalog decode). WeaponData keys: DamageType 1, Damage 2, Penetration 3, DamageSpread 4, MinRange 5, Range 6, DamageCurve 7, EffectPrefab 8, Energy 9, Heat 10, Visibility 11, AmmoType 12, MagazineSize 13, ReloadTime 14, Spread 15, Velocity 16, Tracking 33; InstantWeaponData Count 17, BurstTime 18, Cooldown 19; WeaponItemData WeaponRange 24, Caliber 25, WeaponType 26, FireTypes 27, Modifiers 28, BlastRadius 29, Fuse 32; GearData Hardpoint 23 (enum order Hull..AetherDrive: Energy 9, Ballistic 10, Launcher 11). Shape is [width, height, cells]. Evaluated (Min-Max):

    Design HP Mass Price Dmg Range Cooldown Count Spread Velocity Energy Ammo/Mag Tracking
    ClearPath Ballistic 2 cells 50 50,000 2-6 250-600 0.1-0.033 1 1.25-0.25 700-1250 1-0.5 none / 0 12
    Spectra Energy 2 25 125,000 20-85 800-1750 0.5-0.25 1 0.1-0 0 (beam) 20-40 none 10
    FastBlast+- Energy 2 125 150,000 20-80 500-1250 0.33-0.1 12 3-1 400-900 0 cells / 12 9
    Autocannon Ballistic 4 75 250,000 3 1000 0.5 1 0.5 500 5 Ammo / 50 4
    DeathCluster Ballistic 4 250 450,000 100-250 500-1000 1-0.25 12-15 3-0.5 750-1500 25-5 Ammo / 8 3

    Damage, Heat and Energy are divided by the burst count per pellet (InstantWeapon.cs:128-134). The ammunition commodity is the SimpleCommodity that Autocannon's AmmoType names (3085fc8a...).

  • CG6. Homes for one-cell guns (source read LoadoutGenerator.HasHome, :173-176; HardpointData.Takes, M30). A design is offered only if some catalog hull has a hardpoint that Takes it, which is any design of the slot's type whose shape lies within the slot. Longinus has two Energy 1x2 hardpoints, Djinni two Ballistic 1x2, the station Turret two Ballistic 2x4. So a 1x1 Ballistic or Energy gun and a 1x2 Ballistic gun all have homes on master.

  • CG7. Munitions on master. missile-records, missile-stats, munition-shots, autofire-threshold and autofire-controls had no report: master has no munition record yet (MA7). missile-stats authors munition Durability 20. Detonate is the one blast path and damages entities, not munitions (missile-odds, "Detonate is the one blast path").

  • CG8. A shipped weapon may not carry a fuse today. FireControlCut124Tests.ShippedCatalogIsNeutralOnBlastRadiusAndFuse (tests/Aetheria.Shared.Tests/FireControlCut124Tests.cs:2591-2610) asserts every shipped weapon's BlastRadius and Fuse are null. An airburst flak gun would break it; the flak below is a pellet gun.

  • CG9. The catalog tracking test (tests/Aetheria.Shared.Tests/EvasionTrackingTests.cs:290-313, EveryCatalogGunTracks) requires every non-launcher gun's Tracking finite, Min = Max > 0. New guns must author it.

  • CG10. Design names in code at 4d664b53 (git grep -F over *.cs for the quoted names of the generic-names table). 61 hits in 12 files: scenarios Arcs.cs 14, Duel.cs 11, LauncherAngles.cs 8, StarvedReactor.cs 6; tests EvasionTermTests 10, BrownoutTests, EvasionTrackingTests, PowerCurveTests, RestoredHullsTests, RunStartTests, ScenarioTests 1 each; tools/AetherDb/Program.cs 6. Loadout and product references are by record key, unaffected by a rename.

  • CG11. Behaviour index 31 is CapacitorData (ServerShared/Behaviors/Behaviors.cs:178), so PotaT+- is NiteLife's capacitor. The wishlist's "capacitors: no products" line is stale.

  • CG12. Vault sources read (AetheriaLore 33cd06a): Aetheria/Game Design/Faction Play.md (loadout and quirk lines 118-266, 307-357), Aetheria/Brainstorming/Faction Flavor and Visual Identity.md (Aya point defense and interceptors :458, Megiddo decoys and interception :566), Aetheria/Brainstorming/Corporate Roster and Item Wishlist.md (item ideas :57-87, gaps :97-146; its "What this is for" names the design/product text split). Zhestokost's loadout names "flak and shrapnel (MFer, DeathCluster)" against missiles; Lightsail's names ClearPath as its machine gun and Cetacean Navigators' Waykeeper escorts carry point defense with Lightsail haulers; Aya and Megiddo are the heavy-PD and decoy factions but are not catalog factions. Ship Play Concepts.md was not found in either repo at these heads.

  • LP1. Lot keys (Assets/Scripts/ServerShared/Provenance.cs:63-80, at 7f700d3c). Keys 0-3 (Design, Origin, Quality, Roles); key 4 is free. Lots and origins already carry CultRecordRef<ItemData|Faction|SimpleCommodityData> through MessagePack (:66, :101, :108, :119), so a fourth CultRecordRef<T> needs no formatter work.

  • LP2. ItemManager.Brand (ItemManager.cs:206-222) derives the product at :217-220 with GetAll<FactionProductData>().Where(maker and design).OrderBy(key).FirstOrDefault(). It is the only such derivation in Assets/ (git grep GetAll<FactionProductData>: ItemManager :217, plus Loadout :194 and LoadoutGenerator :145, which choose a product to mint, not read an item's).

  • LP3. Title (ItemManager.cs:226-227) reads Brand(crafted).Product?.Name. Brand's readers: Title, PropertiesPanel.cs:374 (Manufacturer and description), tests LoadoutTests :336/:359/:395/:844 and RunStartTests :136/:678. Nothing else reads an item's brand, so fixing Brand fixes every reader.

  • LP4. Lot writers in Assets/: only CreateLot(product) (:136-158) and CreateLot(design, maker, quality) (:161-170). Production callers of the second: ActionGameManager.cs:544/:548 (console give, no maker) and Loadout.cs:182 (unbranded preset, no maker). No new Produced in Assets/ or tools/; Produced lots exist only in tests.

  • LP5. Behaviour change: a lot from CreateLot(design, maker, q) whose maker sells that design used to brand as the maker's key-first product; once the lot records its product it brands (maker, null) and titles by design. No production path mints such a lot (LP4).

  • LP6. Tests pinning the derivation: LoadoutTests BrandPicksFirstProductInKeyOrderForSameMakerAndDesign (:375-397) pins the tie-break that goes away; its comment says the census's duplicate detector exists because of it. Stale comment in LoadoutTests :315-317 ("Brand() requires at most one product per (maker, design)").

  • LP7. AetherDb census (tools/AetherDb/Program.cs:124-138) prints "(maker, design) pairs with more than one product" as a fault, justified by Brand's tie-break. Under the ruling a design carries several products, and a maker may sell two of one design; with Lot.Product the pair is no longer ambiguous.

  • LP8. Run save: RunSave.Commit (SavedGame.cs:138) writes the reachable ledger; RunSave.Lots(cache) (:132) reads it back. RunSaveTests :382-400 shows the fixture shape (a ledger plus a SavedZone whose pack references the lot).

  • LP9. ItemTitleTests (tests/Aetheria.Shared.Tests/ItemTitleTests.cs) builds Array with products Panopticon Prime (Finch, key array-finch) and ClapBack Ultra (Lucent); Soul's probe added a second Finch product of Array and saw Title return the first (finding title-key-first-product).

Content tables

Drafted content that specs cite. Names are drafts the operator may rename.

Maker profiles (one Mean / StandardDeviation applied to every role of the design, from Faction Play's loadout lines and the existing products' roles):

Maker Mean Dev Why
Zhestokost .50 .08 armour and consistency, no finesse
AU .50 .12 "mid-quality, consistent: the all-rounder"
Lightsail .55 .10 freight reliability
R&D .60 .15 compute, contractor quality
Lucent .60 .15 premium, loud
NiteLife .55 .12 energy specialist
Finch .65 .20 "high variance in role quality"
Alakrita .60 .14 light, fragile, excellent
DME .55 .20 runs hot, uneven
Adrasteia .60 .10 exacting

No product is authored for Miss Terri's (ruling miss-terris-belongs-to-emily-r3) or the Pirates (the Pirates record owns their products, ruling pirates-record-content).

PD products (pd-gear; the three new designs are the Point Defense Gun, Point Defense Laser and Flak Gun, whose stats follow under the rationale):

Design Product Maker Description
Point Defense Gun Waykeeper Lightsail Freight first, questions later. Keeps a hauler's paint unscratched through a full salvo.
Point Defense Gun Rampart Zhestokost A column does not dodge. It does not need to.
Point Defense Gun Swatter AU Keeps the rocks and the rockets off your back while you work.
Point Defense Gun Catch-All R&D Guaranteed to stop the missile!* *Missile defined as whatever it stops.
Point Defense Laser Glare Lucent Every missile gets its moment in the spotlight. A short one.
Point Defense Laser Nightlight NiteLife Lights out for anything with a warhead. Shares the bus with your blasters, so mind the meter.
Point Defense Laser Wren Finch Small, quiet and quicker than it looks, most days.
Point Defense Laser Gnat Alakrita Weighs less than your conscience and keeps the fast pass fast.
Flak Gun MFer Zhestokost Our shrapnel turns your enemies into scrapnel. (the operator's own line, Item Wishlist :57)
Flak Gun Hailstorm AU A wall of steel for crews who would rather not count missiles.
Flak Gun sky full of teeth DME light it up/ shake it loose/ nothing gets through tonight

Generic design names (generic-design-names; hulls, Traction and already generic names excluded: hull names are the model names other passes are revising, retire-longinusx deletes Traction):

Old design name Generic design Functional description
ClearPath Light Machine Gun Light ballistic hose: many small rounds at short range.
6k Shooter Slug Rifle Single heavy slugs, fired as fast as the trigger.
Earp Long Slug Rifle A lighter slug rifle with the longest ballistic reach.
DeathCluster Cluster Cannon Medium shotgun: a heavy volley of pellets.
pretty pretty bang bang Charged Scattergun Ballistic shotgun that charges for a heavier volley.
Spectra Light Laser The lightest beam: low damage, quick cycle, long reach.
FastBlast+- Cell Blaster Rapid energy bursts fed by cells that cut its reactor draw.
ColdFire Long Laser Heavy small beam: slow, hard-hitting, long reach.
CShot RainbowLite Lazer Heavy Beam Laser The heaviest beam: nine cells, enormous damage and reach.
ChargeBlast+- Charged Blaster One charged knockout shot.
ChargeBlast SG Charged Scatter Blaster Charged energy shotgun.
plight Lightning Projector Charged chain lightning.
GT 3K Guided Missile Launcher Fires guided missiles.
LRMM72 Long-Range Missile Rack Large missile rack for long engagements.
SRMM72 Short-Range Missile Rack Extra-large rack of short-range missiles.
pswarm Swarm Rocket Pod Dumbfire rockets in a swarm.
scorched void policy Incendiary Rocket Pod Incendiary dumbfire rockets.
Arctica Adaptive Radiator Radiator whose output scales widely with conditions.
Iapyx Radiator Standard two-cell radiator.
Skiron Long Radiator Four-cell radiator with a large surface.
OK Disperser Heat Pump Moves heat to where it can be shed.
cold like my heart Cryogenic Heat Pump Heavy heat pump that drives temperatures far down.
Core Power Compact Reactor Small reactor.
Manhattan Heavy Reactor Large three-by-three reactor.
Notorious High-Yield Reactor Large reactor with a wide output range.
Vulcan Ring Reactor Ring-shaped reactor, four by four.
MoveOnPro Muon Fusion Reactor Compact fusion reactor that draws a reservoir.
Core Power Station Reactor Station Reactor A reactor sized for a station's core.
Manhattan Station Reactor Heavy Station Reactor A heavy reactor sized for a station's core.
Notorious Station Reactor High-Yield Station Reactor A high-yield reactor sized for a station's core.
Vulcan Station Reactor Ring Station Reactor A ring reactor sized for a station's core.
MoveOnPro Station Reactor Muon Fusion Station Reactor A muon fusion reactor sized for a station's core.
RevvITup 2.0 High-Thrust Drive Large drive: huge thrust, little torque.
deep space burnout Burn Drive Large drive that runs very hot.
Talaria Rugged Drive Two-cell working drive.
Victoire Racing Drive Feather-light two-cell drive with extreme response.
The Bat Sensor Array Two-by-two long-range sensor array.
not if i see you first Compact Sensor One-cell sensor.
PotaT+- Capacitor Stores charge for burst draw.
SafeStuff II Secure Cargo Bay Three-by-three secured hold.
Store-All Enterprise Edition Large Cargo Bay Six-by-six hold.
Store-All Plus Cargo Bay Two-by-two hold.
Store-All Plus Mini Small Cargo Bay One-cell hold.

The product keeps the old name, so the brand survives as the product. Products whose name equals a generic design get a brand: Autocannon (Zhestokost) -> Hammerfall; Cockpit 2x2 (Lightsail) -> Wheelhouse; Industrial Thermostatic Heater (Lightsail) -> Hearth; Medium Drive (Lightsail) -> Steady Haul; Small Drive (Lightsail) -> Short Hop; Turret Control Module (AU) -> Watchtower; Medium Docking Bay (AU) -> Open Berth. The Bat's product is Miss Terri's: its record is not touched; only the generic design under it is renamed. Description move: the design's old text goes to its product when the product's is empty, [TODO] or identical; otherwise the product keeps its own and the dropped design text is printed in the dry run and the commit message.

Weapon product lines (product-lines-weapons):

Design New product Maker Description
Light Machine Gun Rattler AU Cheap, loud, and there when the rocks start moving.
Slug Rifle SM-6 Marksman R&D Standardised, certified, guaranteed to hit the target.*
Long Slug Rifle Longshot Finch Light, quiet and dead accurate, when it feels like it.
Autocannon Workhorse AU Feeds from anything, fires at anything.
Cluster Cannon everything at once DME why aim/ when you can be everywhere
Charged Scattergun Grapeshot Zhestokost The old answer to boarders, sized for the void.
Light Laser Sparrow Finch Barely registers on the scale, or on their sensors.
Light Laser Spotlight Lucent Make sure they know who got them.
Cell Blaster Strobe Lucent Fast, bright, and very hard to ignore.
Long Laser Kestrel Finch Hovers at the edge of range, then stoops.
Heavy Beam Laser Afterglow NiteLife Keeps glowing long after the fight is over.
Charged Blaster Encore Lucent One more, for the fans.
Charged Scatter Blaster nobody leaves early DME doors are locked/ lights are low/ stay for the finale
Lightning Projector Static Shock NiteLife A jolt for every ship in reach.
Guided Missile Launcher Homer Finch Finds its way, mostly.
Long-Range Missile Rack Long Haul AU Delivers to the far side of the zone.
Short-Range Missile Rack Short Fuse AU For problems already too close.
Swarm Rocket Pod Confetti Lucent Every launch is a celebration.
Incendiary Rocket Pod Trebuchet Zhestokost Siege fire for a column that does not stop.

Model page rows

Kind What names it Over time Who decides
FactionProductData (catalog) Its record Every sold design carries at least one product whose name differs from the design's, and is expected to carry several The catalog author: the brand a player sees
Item title ItemManager.Title(item) Derived on each read from the lot's brand; the design name stays as Type ItemManager.Title, the one owner; there is no DisplayName
Lot (run save) Lot.Product (key 4) Written once by CreateLot(product); a lot made without a product (console give, extraction) has none and shows its maker only. A product that no longer resolves reads as unbranded CreateLot; Brand reads it and nothing re-derives a product from maker and design
WeaponItemData (catalog) Three new generic designs: Point Defense Gun, Point Defense Laser, Flak Gun Authored once by pd-gear The catalog author

Rationale

Titles name the product. One engine-free derivation in ServerShared, ItemManager.Title( CraftedItemInstance): the brand's product name, else the design name. Every title site reads it. This is what makes "branding lives in products" visible, and what shows the Pirates' emoji names. lot-product moves the brand's authority from a catalog query (LP2) into the lot, so that two products of one design by one maker stay distinct (CG4, LP9). A first plan for it added a DisplayName over the same hunks as Title; Soul verdict cut-product-titles.s1 found two title owners in force (title-vs-displayname-split) and Self ruled Title the one owner. A lot whose product no longer resolves (a removed mod's product) reads as unbranded: Brand returns (maker, null) and Title falls back to the design; RequireDesigns is not widened (ruling no-save-compatibility-before-players). The census duplicate-brand block (LP7) goes away.

Why three PD designs and not five. The missile-defence rulings in force are pd-who-engages (any gun, priced by its own stats), missiles-sim-side-light (missiles are lightweight records PD can engage), weapon-tracking-authored (a PD class near 40 deg/s exists to be authored) and speed-demon-counterplay. They need guns with PD stats, which the catalog lacks: the PD Gun (ballistic hose, the Djinni's slots), the PD Laser (energy, the Longinus's slots, so the demo's starting hull can fit PD) and the Flak Gun (Zhestokost's flak doctrine as a pellet gun, which needs no new rule: each pellet rolls). An interceptor launcher needs missiles that take munition subjects and a decoy launcher needs a signature the seeker can read that is not a ship; both are new mechanics, which the target's not-in-scope line excludes until ruled. They are follow-ups interceptor-launcher and missile-decoys-signature, not catalog content. An airburst flak (a blast that wears munitions in its disc) is follow-up flak-airburst-at-munitions.

Derivation of the PD stats (from CG5 and the tracking table). Tracking 40 is the table's PD anchor (0.82 / 0.94 against the speed demon at TC / FCA). PD Gun is ClearPath's role pushed to its extreme: one cell, lighter (40 kg), less damage per round (1.5-4) at a higher rate (0.06-0.025 s) and shorter reach (300-550 m), because a munition has 20 durability (CG7): 5 to 14 rounds kill one. It draws ammunition (ballistics-real-ammo) and a small cycling energy, 0.4-0.2 (ballistics-cycle-power). PD Laser is Spectra's role: instant beam, 5-12 per shot every 0.2-0.1 s to 400-750 m, energy-limited at 6-10 a shot, 2 to 4 hits a missile. Flak Gun is DeathCluster's shotgun at small calibre, 1x2: 6-8 pellets sharing 30-70 damage a volley, 350-800 m, Tracking 20 (between PD and ClearPath, because pellets cover what tracking misses).

Retiring the LonginusX and parking the aether drive

Ruling retire-longinusx, in the operator's words: "I want to retire the LonginusX, the aether drive concept needs a rework and it needn't land in the demo." Ruling aether-drive-niche keeps the design intent for the rework: "Like, it has a decent niche. It's the only zero-visibility mobility option besides velocity conversion (also esoteric gear), and the rotor mechanic limits its mobility to bursts, so it's currently pretty trivial to outmaneuver an all aether drive ship if you get it to spend its momentum. I think we want to establish the thruster meta before seeing where it fits in, though." Follow-up aether-drive-rework-rotor carries why it is frustrating to fly today: the rotors spin down after the first manoeuvre, so that manoeuvre is the only one a pilot gets.

Two specs carry it: evasion-term r6 takes the drive out of the unbuilt evasion work, and retire-longinusx r1 deletes the hull, the drive and every consumer from master. The drive's code and data are kept at the tag parked/aether-drive, not on any live path.

Body facts

  • R1. Catalog consumers (catalog decode of GameData/Aetheria.cc at master 516af3e5, 2026-10-07: Python msgpack over the store's own schema table, rows decoded by slot; 217 documents in 13 schemas). Exactly four records carry the hull or the drive: HullData LonginusX (445d911a-9951-40a3-874e-0b7ce8b35129, Ship, price 7,500,000; hardpoints Control, AetherDrive (2,3) 2x2, Energy x2, Launcher x2, Radiator x2, Reactor (2,1), Sensors; behaviours Reflector only, no VelocityLimit), its FactionProductData (maker Alakrita), GearData Traction (42d1ffaa-f764-4a65-ba03-43cc4e6fd2bc, the catalog's only AetherDriveData, union 12, plus Wear; hardpoint AetherDrive; price 250,000; its Particles is the GUID of Assets/Content/Prefabs/Thrusters/Aether Drive.prefab), and its FactionProductData (007ed2b1-8fdf-473d-9ebf-e55fcd1a6903, maker Aeronautics Unlimited). A byte scan of every row for the two record ids and for the strings LonginusX and Traction finds only those four. No faction's BossHull is set; no station stock record names either, because stations stock from products. No other hull has an AetherDrive hardpoint and no other gear has union 12.
  • R2. The LonginusX is the Longinus with a drive. Both hulls share the 6x17 Shape, mass 2,500, prefab 4a3db609... and schematic 51702555...; Control (2,6), Energy (1,8) and (4,8), Launcher (0,5) and (5,5) and Sensors (3,10) sit in the same cells. The Longinus has four Thruster hardpoints ((1,0) and (3,0) 2x2, (2,14) and (3,14) 1x2), its Reactor at (2,4) and Radiators at (2,2) and (3,2), and a VelocityLimit of 100. A bare LonginusX and a bare Longinus differ only by that cap, so a bare-hull target moves to the Longinus unchanged. A fitted LonginusX's Fire Control Array at (2,5) collides with the Longinus's Reactor (2,4)-(3,5), so fits are relaid.
  • R3. Code consumers on master (source read at 516af3e5). Simulation: Behaviors/AetherDrive.cs (184 lines), Behaviors.cs:160 union 12, Enums.cs:53 HardpointType.AetherDrive (last member, so removing it shifts nothing), Ship.cs (the _aetherDrives sets, RemoveAetherDrive, six thrust aggregates and the drive.Axis write in Update), ItemData.cs:895-899 (five request-field registry rows), Settings.cs:211-212 (AetherTorqueMultiplier, AetherHeatMultiplier, read only by the drive), comments in PowerTiers.cs:16 and Entity.cs:1803,1819. Presentation: Gameplay/ShipInstance.cs (AetherDriveInstance, drive particles), UI/HUD/SchematicDisplay.cs (AetherDriveUi and the three RPM label and fill pairs), Gameplay/ActionGameManager.cs:934 (the undock refusal's drive clause), Scenes/ARPG.unity (the RPM Display object 1735948311, active by default, so it shows for every ship once nothing hides it), the Aether Drive.prefab and its only material Dithered Particle Trail AetherDrive.mat. Tools: tools/AetherDb/Program.cs AetherDriveRoles in roles-migrate, the AetherDriveData.Torque row in brownout-migrate, comments at :938 and :1409; tools/blender/aetheria_ships/ship_cc.py:36 HARDPOINT_TYPE_NAMES. Settings data: Assets/Resources/Settings.asset line 15 StartingHullName: LonginusX (read by the Tutorial and Main Galaxy modes through ActionGameManager.cs:818; GameSettings.cs:13 defaults to Longinus) and lines 310-311.
  • R4. Scenario and test consumers. Scenarios Duel, Arcs, Launcher Angles and Starved Reactor fit a LonginusX with Traction and place bare LonginusX targets. Tests: BrownoutTests (three drive tests and RunAetherDrive), ShipAuthoringTests:254 and ShipValidationTests:36 (AetherDrive as an internal hardpoint type), FireControlCut124Tests:475 (a LonginusX row), FireControlCut7Tests:215, RunStartTests:673, ScenarioTests:132 (asserts the starting hull is LonginusX) and :288-340, tests/mutation_tests_addressables_cut.py (a LonginusX record as the known-good mutant), and comments in FireControlCut9Tests:363, FireControlCut12Tests:1327 and ConditionRatioTests:15. Docs that describe the live game: docs/merge-to-master-checklist.md lines 36, 47, 53 and 59.
  • R5. Open branches (read through origin/* refs from C:\ar-retire-map, 2026-10-07). eureka/aetheria-release-evasion-term (tip e4944549, fix batch B on top of 46b28501) adds AetherDrive.Manoeuvre (24 lines, da2957b8), sums it into Ship.Envelope (RecalculateEnvelope), and flies a LonginusX with Traction (EvGemini) as the "gemini" in EvFleet and the gunner and target fixture. Batch B's 5f404d50 adds TheDrivesReportReadsItsStateAndTheShipsSpeed, pinning the drive half of finding envelope-inputs-unpinned (offline gate and speed efficiency). controls-helm (tip 8c5eb5b7) rewrites Ship.Update's turn block, including the drive.Axis line, and ScenarioTests:317-322. demo-galaxy and boss-gate stage the demo player as Generated("Longinus") and touch ScenarioTests only at lines 44, 96 and 507; the boss fallback picks the priciest ship hull, the Djinni at 10,000,000, so the retirement changes no boss.
  • R6. Package hulls and top speed. ShipAuthoringStore.HullLike copies every member of a shipped reference hull, VelocityLimit included, for ship-authoring create --like <hull>; create without --like writes a hull with no behaviours, and nothing in ShipModCatalog or validation requires one. The dock intro (ActionGameManager.cs:849) dereferences GetBehavior<VelocityLimit>() unguarded.

What goes, what is parked

Deleted, because nothing that ships consumes it once the hull is gone: the four catalog records (R1), the drive behaviour, union 12 (retired with a do-not-reuse comment, as union 26 is), HardpointType.AetherDrive, the drive's settings, presenters, HUD, prefab, material and scene object, every test and tool row of R3 and R4, and the StartingHullName value. The Longinus takes the LonginusX's place in every scenario and test, because R2 makes it the same frame without the drive. Velocity conversion (union 10) is separate gear and stays.

Parked: the annotated tag parked/aether-drive on the retirement's base, the last master commit that holds the drive, the Traction record and the drive scenarios. Its message is the park note:

parked/aether-drive: the aether drive and the LonginusX, retired from the demo
(ruling aetheria-release:ruling:retire-longinusx).

Design intent for the rework (ruling aetheria-release:ruling:aether-drive-niche, the operator,
2026-10-07): "Like, it has a decent niche. It's the only zero-visibility mobility option besides
velocity conversion (also esoteric gear), and the rotor mechanic limits its mobility to bursts, so
it's currently pretty trivial to outmaneuver an all aether drive ship if you get it to spend its
momentum. I think we want to establish the thruster meta before seeing where it fits in, though."

Why it frustrates today (follow-up aetheria-release:follow_up:aether-drive-rework-rotor): the
rotors spin down after the first manoeuvre, so the first manoeuvre is the only one you get; judge
the rework against the game's overall pace, which the operator also finds too fast.

What is here: Assets/Scripts/ServerShared/Behaviors/AetherDrive.cs (rotors, coupling, torque,
heat), its union 12 and HardpointType.AetherDrive, Ship's drive aggregates, ShipInstance's drive
particles, SchematicDisplay's RPM display, Aether Drive.prefab, GameplaySettings
AetherTorqueMultiplier and AetherHeatMultiplier, the LonginusX hull and the Traction drive in
GameData/Aetheria.cc (LFS), and the Duel, Arcs, Launcher Angles and Starved Reactor fits.
The drive's evasion envelope report (AetherDrive.Manoeuvre) is on the evasion-term branch at
da2957b8, tested by 5f404d50; evasion-term r6 removed it before merge.
Return after the thruster meta is established.

Ordering

evasion-term r5 kept r2's envelope as landed, drive included, and named the LonginusX three times: plight's fit, the evasion-catalog step that would have capped it, and the test TheLonginusXTopSpeedIsAsRuled. Its Hands would build and test code this retirement deletes, so r6 supersedes it now: the drive's report and its sum leave the envelope, the gemini fixtures fly a Djinni (the catalog's only ship that strafes, M24 and M25) or a fitted Longinus, and the LonginusX steps are gone. r6 does not touch AetherDrive itself, Ship's thrust aggregates or the scenarios: those are master's, and the retirement deletes them whole.

Between r6 merging and the retirement merging, a LonginusX on master has an empty envelope and so no evasion. Nothing ships in that window, and the retirement is the next subtraction, so the transient is harmless; it is cheaper than teaching r6 a drive that dies a cut later.

retire-longinusx depends on evasion-term (its RecalculateEnvelope sits beside the aggregates the retirement deletes, and both rewrite the LFS catalog, which does not merge) and on controls-helm (it rewrites the drive.Axis line and ScenarioTests:317-322, next to the retirement's StanceAndPilot edit). It need not wait for demo-galaxy or boss-gate (R5). The catalog is rewritten by a one-shot command run on master's catalog as it stands when the branch is last rebased, never merged as a binary.

The retirement is a pure subtraction and adds no rule except the catalog test of R6's first half. The behaviour change that the drive's absence makes in evasion belongs to evasion-term r6, so Soul can falsify each alone.

The dock intro

After the retirement every ship hull in the shipped catalog, the Longinus (100) and the Djinni (50), carries a VelocityLimit, so for the shipped catalog follow-up dock-intro-velocitylimit-null reduces to a test, "every ship hull in the shipped catalog has a top speed", which the retirement adds. Package hulls (R6) keep the follow-up alive: a hull created without --like has no cap and throws at the intro. What is left is one owner for package hulls, either ship-authoring validation refusing a ship hull with no VelocityLimit or the intro handling its absence; the demo cast hulls (demo-cast-hulls) meet it first.

Model page rows

Kind What names it Over time Who decides
parked/aether-drive tag The tag name; its message is the park note. Fixed at the retirement's base; read by the rework, never merged back. The operator's rework, after the thruster meta.

Sim speed

Rulings sim-speed-knob, sim-speed-presentation, sim-speed-surface, sim-speed-tenth, tick-rate-follows-design-speed and unity-presents-only-r3, for the pace named in pace-after-primitives. The operator: "We don't want Unity having any authority over the sim whatsoever. The sim is authoritative and Unity should only be presenting." Anchors are against origin/master 6d427b7a, read on 2026-10-07 in the read-only worktree C:\ar-simspeed-map. controls-helm r2 is in flight on ActionGameManager.cs, EntityInstance.cs, ShipInstance.cs and Ship.cs, so line numbers there move when it lands. None of loot-1..loot-3, missile-records, missile-odds, missile-presenter or munition-shots has landed at this base.

The cuts: sim-speed r2 (the fixed step and drawing ahead, at speed 1), the census cuts ballistic-flight, sim-contact, retire-unity-mines, loot-2 r2, missile-presenter r3 and munition-shots r4, and sim-speed-scale r2 (the design speed, the tick rate and the player's factor), which waits on all of them.

Body facts

  • T1. One caller steps the simulation, by the frame's dt. The only Zone.Update call in Assets/Scripts is Zone.Update(Time.deltaTime) at the end of ActionGameManager.Update (Gameplay/ActionGameManager.cs:1311), inside if(!_paused) (:1244). Zone.Update (ServerShared/Zone.cs:202-222) adds dt to _time (a double, :35), recomputes orbits and divides their displacement by dt (:210, so a zero dt gives an infinite velocity), steps agents, then entities, then FireControl.Step. ServerShared reads no Unity clock: its only clock reads are DateTime.Now seeds (ItemManager.cs:20, Galaxy.cs:111,167, GalaxyStage.cs:49). There is no fixed tick (M15), and sim history depends on the frame rate today.
  • T2. Nothing touches Unity's time scale in the game. The one Time.timeScale write is the Time Scale field of UI/FieldTester.cs:80, the shield-field lab scene. ProjectSettings/TimeManager.asset has Fixed Timestep 0.02 and Maximum Allowed Timestep 0.1; no script has a FixedUpdate.
  • T3. Unity physics writes sim velocity in two places. HullCollider.OnCollisionEnter (Gameplay/HullCollider.cs:14-17) adds impulse / Time.deltaTime to Entity.Velocity; the impulses come from non-kinematic loot and mine bodies. ShieldManager.OnCollisionEnter (Gameplay/ShieldManager.cs:32-81) decides three facts: an item pickup (TryStore on collision, :42-56), a mine detonation (mine.Explode(), :57-60), and the ship-on-ship bounce, an elastic exchange of normal velocity written to Entity.Velocity (:70-79, with PostCollisionVelocity at :83-86).
  • T4. Unity-owned world objects keep their own clocks. Loot drift and attraction (GridObject.cs:37-49), the tractor pull (TractorBeam.cs:21-27, a Physics.SphereCastAll that pushes GridObject.Velocity), the player's tractor ramp (ActionGameManager.cs:1307-1309, Time.deltaTime * 2 onto Entity.TractorPower), the loot roll on death (EntityInstance.cs:293-308, UnityEngine.Random), and the mine (Weapons/Mine.cs:46-100: real-time arming, Physics.OverlapSphere proximity, range from the source transform, lifetime, blast delay, then FireControl.Detonate called from Unity).
  • T5. The drawn rounds fly themselves. Projectile.Update (Weapons/Projectile.cs:43-50) integrates gravity, drag and position by Time.deltaTime and dies at a Unity distance from its start; ProjectileManager.Fire (:16-28) picks the drawn direction from barrel.forward with UnityEngine.Random spread, offsets the spawn by Random.value * Time.deltaTime and adds the shooter's velocity. The simulation's round (PendingShot, FireControl.cs:563-590, :1748-1798) flies straight from FireOrigin along TravelDirection at the weapon's velocity, inheriting nothing, and arrives at ArrivalTime. FireControl decides every hit (fire control Cuts 3-4); the two flights disagree, and Unity ends its own at a distance it chose.
  • T6. The guided round flies itself and times its binding in real time. GuidedProjectile integrates thrust and position by Time.deltaTime (:143,205,210), dodges with UnityEngine.Random (:118,173), and ends its binding when Time.time - _spawnTime passes BindingLifetime (:116,232), which GuidedProjectileManager.cs:24 sets from shot.ArrivalTime - zone.Time + 1: a sim interval compared against real time.
  • T7. Presenters that read sim state and animate in real time. Charge effects, beams and hitscan flashes (ConstantLaser.cs, ConstantLightning.cs, Laser.cs, HitscanEffect.cs: start and stop on sim events, no physics query), shield panels (ShieldInterceptor.cs:63-146), fades (EntityInstance.cs:363,381), mount slew (ArticulationPoint.cs:36-47), field ripples (FieldDriver.cs:231-264), sun, volumes, wave scroll, HUD blinks, the hit marker, the death transition and every menu tween. The gas giant's wave phase reads Zone.Time (ZoneRenderer.cs:503). FieldDriver.cs:307-323 moves a grabbed transform itself (loot-3 removes it).
  • T8. Input and commands. Each frame ActionGameManager.Update writes the view, Aim (LookDirection before controls-helm), MovementDirection and, after controls-helm, Ship.Turn. Weapon slots call Activate and Deactivate from input callbacks (:424-425). UI/ClickRaycaster.cs:26 picks drawn objects with Physics.Raycast: input choosing a target, not physics deciding a fact. The console's give-item command (ActionGameManager.cs:531) is an operator tool.
  • T9. Pause and saving. _paused (ActionGameManager.cs:187) gates the whole of Update, the step included. Zone.Pack saves Time (Zone.cs:184). PlayerGameplaySettings (ServerShared/PlayerSettings.cs:56) is saved in player.cc; designer tuning lives in GameplaySettings (ServerShared/Settings.cs:159, values in Assets/Resources/Settings.asset).
  • T10. Headless tests step zones directly with chosen dts, 0f included (FireAuthorityTests.cs:192, FireControlCut11Tests.cs:192,225,411); three fixtures hard-code 1/60 (MiningCut1Tests.cs, RestoredHullsTests.cs) and choose their own dt.
  • T11. Kinematic contact pairs are on. ProjectSettings/DynamicsManager.asset m_ContactPairsMode: 1 enables kinematic-kinematic pairs, so the kinematic shield colliders (Content/Prefabs/Shield.prefab, m_IsKinematic: 1) do raise OnCollisionEnter: T3's ship-on-ship bounce is live.
  • T12. Which effect prefabs the catalog fires. A byte search of GameData/Aetheria.cc for each effect prefab's GUID (as hex text and as raw bytes) finds ProjectileManager prefabs AC5, 6k Shooter, DeathCluster, prettypretty, ClearPath, ChargeBlast SG, ChargeBlast+- and FastBlast+-, and GuidedProjectileManager prefabs Micromissile, Regular Missile, SRM and scorchedvoid.
  • T13. Nothing fires the mine. The same search finds the Mine Launcher prefab (GUID 24eddacc819f20542962f2395930f261, the only prefab carrying MineManager) zero times. IntroCutscene (ActionGameManager.cs:839-866), which writes a ship's position, direction and velocity from a real-time coroutine, is never started (its call at :791 is commented out).
  • T14. What depends on step count rather than sim seconds. Measured by source read for the design-speed step. Zone._time is a double, so time does not drift with more steps. Entity.Position is a float3 and zones reach ZoneRadius 10000 (Settings.asset), where a float's spacing is about 0.001 units: a per-step displacement of v * Step rounds to it, so slow movement far from the origin loses precision as the step shrinks (at Step 1/300, 5 u/s is about 17 spacings per step, a few percent of error; at 1/60 it was 85). The manoeuvre track (ManoeuvreTrack.Observe, evasion-term branch) measures acceleration as the per-step velocity change over dt and calls it changed above ChangeThreshold 0.01; float rounding of that difference grows as 1/dt (about 0.005 at 100 u/s and dt 1/600). Its exponentials and its quiet timer use dt and are exact. Cooldowns charge the firing tick (InstantWeapon.cs:254) and drop the remainder, so a gun's real period rounds up to whole steps: a smaller step brings it nearer the authored cadence. Agents decide every step (Agent.Update), and Combat draws one jittered range sample per step (Combat.cs:145); a smaller step means more decisions per sim second, the same per real second. No ruling fixes a per-step count.

Census

Every Unity-side place that integrates, times or decides a sim fact at 6d427b7a, and the cut that moves it. Presenters that only read sim state (T7) and input (T8) are not entries.

Row Site Fact Unity decides Owner
C1 ActionGameManager.cs:1311 The step's length (frame dt) sim-speed r2
C2 ActionGameManager.cs:839-866 (dead) A ship's position, direction, velocity sim-speed r2 deletes it
C3 HullCollider.cs:14-17 Velocity from physics impulses sim-contact
C4 ShieldManager.cs:70-86 Ship-on-ship bounce sim-contact (new rule in Zone)
C5 GuidedProjectile.cs, GuidedProjectileManager.cs:24-83 A missile's flight; when its binding ends missile-records, missile-odds, missile-presenter r3
C6 ActionGameManager.cs:1307-1309 Tractor spin-up loot-2 r2
C7 Projectile.cs:43-50, ProjectileManager.cs:16-28 A round's drawn flight and end ballistic-flight (new)
C8 Mine.cs, MineManager.cs, ShieldManager.cs:57-60 Mine flight, arming, lifetime, detonation retire-unity-mines r2 deletes it (unreachable per T13); sim-mines brings it back as a sim body (section 'Mines in the sim')
C9 GridObject.cs, EntityInstance.cs:293-308, ZoneRenderer loot, ShieldManager.cs:42-56 Loot drop, drift, expiry, pickup loot-1
C10 TractorBeam.cs:21-27, FieldDriver.cs:307-323, binder resolvers Tractor pull, grabbed pose loot-3
C11 ActionGameManager.cs:705-711 Executes a zone transfer the sim decided follow-up zone-transfer-owner
C12 EntityInstance.cs:403, ShipInstance.cs:141 None (draws the latest pose, so a fixed step hops) sim-speed r2 draws ahead

sim-speed-scale runs the census patterns as negative checks before any code, so the setting cannot ship while a row is open.

The design

One clock, SimClock (ServerShared, engine-free), owns the step and its pacing.

  • Step is fixed for the clock's life. sim-speed builds it at 1/60 (speed 1); sim-speed-scale builds it at SimSpeed / TickRate from game data.
  • StepsPerRealSecond is TickRate times the player's factor (sim-speed-scale; 60 before it). The factor never reaches Step, so one set of game data gives one history at any player setting.
  • Advance(realSeconds, step) adds the clamped real seconds (at most 0.1) to an accumulator and calls step(Step) each time it holds one tick. A hitch slows the world instead of spiralling.
  • Lead is the sim seconds since the last step (below one Step); Steps counts steps. There is no SimDelta: nothing in Unity integrates on the clock.

ActionGameManager calls Clock.Advance(_paused ? 0 : Time.deltaTime, Zone.Update) every frame, outside the pause gate. Pause stays the menu's.

Presenters draw the latest sim state ahead by Lead, through DrawAhead (ServerShared, pure, never called by a rule): an entity's position plus velocity times Lead, a ship's rotation turned by Entity.TurnRate times Lead (the sim writes TurnRate from the actuators each step; staging and wormhole writes leave it zero), and Zone.Time + Lead for presenters that evaluate a sim function of time. A ballistic round is drawn at FireControl.RoundAt(shot, Zone.Time + Lead), the line the simulation flies it; a missile at its record's position plus velocity times Lead. The presenter API carries the latest state and Lead; nothing a presenter receives can feed back.

Presenter Reads Clock
Entity pose (EntityInstance, ShipInstance) DrawAhead Lead
Ballistic round (Projectile) its PendingShot copy, RoundAt, ShotResolved Zone.Time + Lead; trail fade real
Guided round (GuidedProjectile) its munition record, ShotResolved Lead; lift curve on sim seconds; fades real
Floating item (ItemPickup, loot-1) its body latest state (ahead by Lead when loot-1 lands after sim-speed)
Charge effects, beams, flashes, shields, fades, slew, ripples, sun, volumes, HUD, menus sim events and state real time (ruling sim-speed-presentation)
Thruster flames, particles, audio pitch current state real time

Authority map

  • Owner. SimClock owns sim time's relation to real time. Zone owns sim time and the step. Game data owns SimSpeed and TickRate; the player owns the factor.
  • Inputs. Real frame seconds and pause; SimSpeed, TickRate, SimSpeedFactor.
  • Outputs. Zone.Update(Step) calls; Lead and Steps for presenters.
  • Derived state. Lead, Steps and every drawn pose are presentation-only. Entity.TurnRate is sim-written and read only by DrawAhead. Sim seconds per real second (SimSpeed * SimSpeedFactor) is derived.
  • Forbidden writers. Any other Zone.Update caller; Time.timeScale outside FieldTester; any Unity code integrating, timing or deciding a census fact; a presenter given a sim-time delta; the player's factor reaching Step.
  • Shared paths. The game and every scenario reach the simulation through the one Advance; every presenter draws with the same Lead.
  • Deletion line. Each census cut deletes its Unity owner before adding the sim owner; sim-speed-scale adds no code until every census pattern is empty.

Model page rows

Kind Named by Over time Decided by
Design speed, tick rate GameplaySettings.SimSpeed, TickRate Set by designers in game data game data; read by SimClock at zone entry
Player slow-down PlayerGameplaySettings.SimSpeedFactor Set by the player, saved in player.cc the settings menu
Zone time Zone.Time, saved in ZonePack.Time Advances by Step per step Zone.Update, called only by SimClock
Clock accumulator none Under one tick, dropped on load and zone change SimClock
Turn rate Entity.TurnRate Rewritten every step the rotation actuators
Ship contact Zone.Contacted events Published at each closing contact Contacts.Step

Prior art

From documentation and talks already read, not re-fetched for this pass.

  • Gaffer On Games, "Fix Your Timestep!" (Glenn Fiedler, 2004). A fixed dt fed from an accumulator of real time, a cap on frame time against the spiral of death, and drawing between steps with alpha = accumulator / dt. The design is this article, with the factor applied where real time enters the accumulator.
  • Unity Rigidbody.interpolation. Interpolate draws one physics step behind; Extrapolate draws ahead from velocity and can show a body briefly past a collider before it is corrected. Unity recommends Interpolate for the camera's subject.
  • Client-side prediction (Quake 3, Source). Remote entities interpolate a little in the past; the player's own avatar is drawn ahead because input lag on it is what players feel.
  • Factorio, Paradox, StarCraft II replays. Speed changes the tick rate, never the tick, because lockstep and replays need identical histories. Supreme Commander simulates at 10 Hz and draws in between; Source servers tick at 66 Hz, Overwatch at about 63.
  • Celeste Assist Mode. Game speed from 50 to 100 percent, a saved accessibility option; the precedent for the factor and for its 50 percent floor.

Rationale

Why the step follows the design speed. Ruling tick-rate-follows-design-speed. At a design speed of 0.1 and a 1/60 step the world ticks at 6 Hz; drawing ahead hides the motion, but not the input: a trigger or a turn waits up to a tick (167 ms) to enter the simulation. With Step = SimSpeed / TickRate the real tick rate is a chosen constant, and the player's factor alone changes ticks per real second, so APlayerFactorIsTheSameHistory holds for one set of game data. A design-speed change is a balance change: it changes the step, so the pace pass measures at the shipped values. Per T14 no ruled behaviour depends on a step count; what moves is closer to the continuous model (cooldowns nearer their authored cadence, agents deciding more often per sim second). The two costs are float precision and the manoeuvre track's threshold, both pinned as measurements in sim-speed-scale.

The tick rate: 30 Hz at design speed, with the factor floored at 0.5. Recommendation, moderate confidence; TickRate is game data, so the play check can move it without code.

Tick rate at design speed 20 Hz 30 Hz 60 Hz
Step at design 0.1 1/200 sim s 1/300 sim s 1/600 sim s
Input latency, factor 1 (worst) 50 ms 33 ms 17 ms
Input latency, factor 0.5 (worst) 100 ms 67 ms 33 ms
Steps per real second vs today about a third about half today's at 60 fps
Float spacings per step at 5 u/s, 9000 units out 25 17 8

Today's game steps once per frame, so 30 Hz costs about half of today's simulation per real second at 60 fps and less at higher frame rates. 60 Hz buys latency nobody needs in a game slowed tenfold and halves the float headroom (T14). 20 Hz puts the slowed factor at 10 Hz, near the rate the operator called noticeable. A factor floor of 0.25 at 30 Hz would tick at 7.5 Hz, so the floor is 0.5 (Celeste's), keeping the slowest tick at 15 Hz.

Drawing between steps: extrapolate everything from the latest state. Interpolation shows only states the simulation produced, but draws one step behind; extrapolation has no lag but can overshoot a turn or a contact and snap at the next step. At 30 Hz and design speed 0.1:

Factor 1 (30 Hz) Factor 0.5 (15 Hz)
Interpolation lag 33 ms real (one step) 67 ms real
Extrapolation snap, a turn stopping at 3 rad/s 0.57 degrees 0.57 degrees, half as often
Extrapolation snap, a contact reversing 100 u/s 0.33 units 0.33 units
Extrapolation error, steady acceleration of 100 u/s2 under 0.001 units under 0.001 units

The player's ship is extrapolated (the operator's acceptance, and where lag is felt). The rest are extrapolated too, for three reasons: one draw time keeps the geometry the player aims at consistent with their own ship (interpolating the others would set them a step behind it, up to 0.67 units at 200 u/s closing); the analytic presenters (RoundAt, a missile's record) are exact under it; and it needs no previous-state buffer, so no presentation store has to be owned or kept out of the simulation's reach. If the play check shows visible snaps on contact, the fallback is interpolating non-player entities through a presentation-only buffer written by the step wrapper, not a change to the simulation.

Why rounds are drawn on the sim line. FireControl already owns every hit. A drawn round that inherits velocity, jitters, drops under gravity and dies at its own distance is a second flight that disagrees with the first; at a slowed design speed the disagreement is visible for seconds. RoundAt states the simulation's line; the presenter blends from the barrel into it in the first tenth of a sim second and ends where the outcome says.

Why contact moves into the simulation instead of being deleted. The bounce is live (T11) and the game has it today; deleting it would be a product change no ruling asks for. The rule moves verbatim (the same elastic exchange, once per contact, no push-out) onto a disc that bounds the hull, so ships may come a little closer than the shield ellipsoid let them.

Why the Unity mine is deleted, not moved. No catalog item fires it (T13) and it owns its whole life in Unity; moving it would keep Unity deciding. It returns as a simulation body (ruling mines-back-in-sim-asap; section 'Mines in the sim').

Why the split. sim-speed lands the fixed step at speed 1 now: at speed 1 the integrators still on Time.deltaTime run at the sim's average rate, so nothing desyncs, and replays become possible (M15). The setting waits for the census, because any open row runs at real speed in a slowed world. Ordering: sim-speed (after controls-helm); then ballistic-flight and sim-contact (independent, small); missile-presenter r3 after missile-odds; the loot branch (loot-1 r2, loot-2 r2, loot-3); retire-unity-mines r2 at any time (it no longer waits on loot-3); sim-speed-scale last, before the combat-pace-pass.

Why the clock does not own pause. Pause is the menu's; the menu feeds the clock zero real time.

Mines in the sim

Rulings mines-back-in-sim-asap ("yes delete the mines but I want them back in the sim asap"), mines-float-like-loot ("Mines are free floating just like loot") and unity-presents-only-r3. Anchors are against origin/master 35ea0da4, read on 2026-10-07 in the read-only worktree C:\ar-mines-map. None of loot-1..loot-3, retire-unity-mines, sim-speed, sim-contact, missile-stats or missile-records has landed at this base; mining-merge has (the Targeting/ providers are on master).

The cuts: floating-bodies (the drifting body loot and mines share), retire-unity-mines r2 (the Unity mine deleted now, no longer waiting on the loot branch), sim-mines (the mine in Zone, its gear and the catalog item, headless) and sim-mines-presenter (Unity draws it). loot-1 r2 rebuilds on floating-bodies. Questions mine-trigger-iff and mine-owner-leash gate sim-mines.

Body facts

  • N1. How a mine is fired. MineManager (Gameplay/Weapons/MineManager.cs:13-32) is an InstantWeaponEffectManager, so the launcher is an InstantWeaponData item: InstantWeapon pays cooldown, ammo, energy, heat and wear and calls FireControl.Fire, which queues a PendingShot (InstantWeapon.cs:271); OnFire then makes MineManager.Fire spawn the mine at the barrel transform. Launch velocity: barrel.forward * weapon.Velocity turned by UnityEngine.Random angles in +-Spread/2 on all three axes, plus the ship's velocity (InheritVelocity: 1 in the prefab). The mine copies weapon.Damage, weapon.Range and WeaponData.DamageType. So one trigger pull also queued an ordinary shot at the target.
  • N2. Flight and drag. GridObject (Gameplay/GridObject.cs; prefab values at Mine Launcher.prefab:173-178): the launch velocity decays by 1 - LaunchDrag*dt per frame (LaunchDrag 0.2); a separate drift velocity is accelerated down the zone's gravity well by normalize(f) * GravityStrength * (1/(1-|f|^2) - 1) * Gravity with no dt, so it is frame-rate dependent (Gravity 1), and decays by Drag 0.05; a vertical spring (GridAttraction 10) holds it at the grid height. The pickup prefabs carry the same Drag, LaunchDrag and Gravity (map L7): loot and mines already fly one body.
  • N3. Arming. Mine.Update (Mine.cs:53-87) checks nothing until ActivationDelay 2 s of Time.time after the mine is enabled (Mine Launcher.prefab:158).
  • N4. Triggering. Once armed and not yet counting down, Physics.OverlapSphere(position, BlastRange 25, layer mask 1): any collider carrying HullCollider starts a countdown of BlastDelay 2 s, which never cancels. There is no faction test: the launcher's own hull and its allies trigger it. Mask 1 is the Default layer. The shipped hulls and the turret carry HullCollider on layer 0 (Longinus.prefab, Djinni.prefab, Turret.prefab), but ShipPrefabAuthoring.cs:161 puts mod-package hulls on Combat, and station prefabs have no HullCollider, so mod ships and stations never trigger a mine. That exclusion is a layer accident, not a rule.
  • N5. Shield contact is dead. ShieldManager.cs:57-60 explodes a mine on shield contact, but the mine object (layer 21, GridObjects) has no collider in the prefab, so the branch never runs.
  • N6. Ends. Once armed, the mine explodes when its countdown ends, when Lifetime 30 s has passed, or when it is farther than weapon.Range from its launcher's transform (Mine.cs:79-84). Every end is an explosion; none fizzles. A destroyed launcher makes Source.transform throw.
  • N7. The blast. Mine.Explode calls FireControl.Detonate(zone, position.xz, BlastRange, Damage, DamageType) (Mine.cs:97-104), spawns HitEffect (BigExplosion) and returns to the pool. Detonate (FireControl.cs:1350-1422) spreads Damage over the disc's area: each occupied hull cell takes damage * overlap / (pi r^2), so an entity takes damage in proportion to how much of its hull the disc covers, with no radial falloff; an active shield takes the entity's whole share or breaks; armour, then item pools, absorb per cell. It reads no source or faction, so everyone in the disc is harmed, the launcher included. The trigger radius and the blast radius are the one BlastRange.
  • N8. Catalog. T13 holds at 35ea0da4: no catalog record names the Mine Launcher prefab. The item existed in the 2021 catalog (79b0c036 "Mines and Flamethrowers", 2021-02-06); the daemon-era 8c71a637 recovered it from GameData/Legacy/AetherDB.2021-03-05.msgpack, an LFS object, and pinned its 2 s activation and 30 s lifetime. WeaponType.Mine exists (Enums.cs:66). WeaponItemData carries BlastRadius (key 29) and Fuse (key 32), read only through FireControl.FuseOf. The behaviour union's next free index is 40 (Behaviors.cs:188).
  • N9. Presentation. Mine pulses emission on submesh 2 by EmissionCurve: none before arming, ActiveEmission on a 1 s cycle when armed, ArmedEmission on a 0.25 s cycle while counting down. GridObject spins it by sin and cos of time. All of it presents the arming and countdown facts.

Prior art

A short check, for the two forks only. EVE Online's bombs are unguided and untargeted, drift along the launch line, and hit every ship in the area, the launcher's fleet and the launcher included (EVE University, Bombs). Starsector's Stinger mines use a friend-or-foe trigger and detonate when a target comes within range (Starsector wiki, Mine Strike). Freelancer's mines seek hostile ships, and FreeSpace minefields hit anything (from memory, unchecked). The common shape is an IFF trigger with a blast that does not ask whose it is. No reference leashes a mine to its launcher.

The design

What a mine is. A free-floating body (ruling mines-float-like-loot) with a frozen payload: the KinematicBody loot drifts on, plus the time it was laid, its arming delay, fuse delay and lifetime, its blast radius, damage and damage type, and its layer's identity (entity and faction, frozen at lay). It is not a munition record: it has no seeker, thrust, fuel or shot id, and nothing prices or rolls it. It is not an Entity.

What is shared with loot, and what is not. floating-bodies lands what both need: FloatingBodyId (one zone counter for every free body), KinematicBody and its Step, the three motion settings both already use (N2), and SimulationDice (moved out of FireControl, as loot-1 r1 planned). Each kind keeps its own collection: Zone.FloatingItems (loot, loot-1 r2) and Zone.Mines. One collection with a payload switch would let TryPickUp see a mine and make every reader branch on kind; two typed collections make "a tractor grabs a mine" unrepresentable instead of forbidden. Neither kind gets a targeting provider: nothing detects or shoots them today, and one detection interface for bodies stays with the follow-up.

Laying. MineLayerData : InstantWeaponData carries the mine's own stats (arming delay, fuse delay, lifetime), as missile stats belong to the missile until gear supplies them (ruling missile-stats-now-gear-later). InstantWeapon keeps paying for the round. FireControl.Fire and FireControl.Refuses route a mine layer to FireControl.Lay before Solve: no target, no arc gate, no PendingShot. Lay freezes the payload and has Zone.Lay add the mine at the shooter's position with velocity shooter velocity + rotate(MountDirection.xz, a dice angle in +-Spread/2) * Velocity, the dice drawn from SimulationDice.For(CombatSeed, MineStream, id). This also closes N1's double fire.

Arming, triggering and the blast. Zone.Update steps mines after the entities and before FireControl.Step, in lay order. A mine is armed at LaidAt + ArmingDelay. While armed and not triggered, it triggers on the first eligible entity whose hull the blast disc touches, through FireControl.Touches(entity, point, radius), the coverage test extracted from Detonate, so what triggers a mine and what its blast reaches are one geometry and every hull counts (N4's layer accident is not kept). Which entities are eligible is question mine-trigger-iff. A triggered mine detonates at TriggeredAt + FuseDelay whatever moves; an untriggered one at LaidAt + Lifetime; the leash is question mine-owner-leash. Detonation calls FireControl.Detonate unchanged with the frozen payload and then removes the mine, so every removal of a mine is its blast, and that is the fact the presenter draws.

Presentation. MineInstance replaces Mine and GridObject on a mine prefab. ZoneRenderer creates one per Zone.Mines add and plays the blast on remove. It draws the body at the grid height (ahead by the sim clock's lead once sim-speed has landed), spins as GridObject did, and pulses from Armed and Triggered in real time (ruling sim-speed-presentation). A mine layer has no fire effect manager: the mine it lays is its presentation.

Authority map

  • Owner: Zone owns every mine's existence (Lay the one creator, the mine step the one remover). FireControl owns laying's payload (Lay) and the blast (Detonate, Touches). KinematicBody owns the drift arithmetic for loot and mines alike.
  • Inputs: the mine layer's evaluated stats at the fire step, the item's BlastRadius, the shooter's position, velocity, mount and identity, zone time and force, the zone's entities and hulls.
  • Outputs: Zone.Mines add and remove; Detonate's damage.
  • Derived: Armed and Triggered are read by presenters; the drawn pose and the pulse are presentation only.
  • Forbidden writers: any MonoBehaviour moving, arming, timing or detonating a mine; a creator of mines besides Zone.Lay; a blast path besides Detonate; TryPickUp reaching a mine; a PendingShot for a mine layer.
  • Shared paths: player, AI and scenario mine layers all lay through InstantWeapon and FireControl.Fire; loot and mines drift through one KinematicBody.Step.
  • Deletion line: Mine, MineManager, the Mine Launcher prefab and ShieldManager's mine branch go first (retire-unity-mines r2), before any simulated mine exists.

Model page rows

Kind Named by Over time Decides
Mine FloatingBodyId in its zone, never reused; not saved Laid, drifts, arms, triggers, detonates, removed Zone (life), FireControl (lay, blast)
KinematicBody Its floating item or mine Stepped each sim step under zone force and drag KinematicBody.Step
MineLayerData A behaviour on a WeaponType.Mine catalog item Authored Catalog

Rationale

Why mines first, and what "asap" costs either way. After the whole of loot-1, the mine waits for the loot branch to merge: loot-1, loot-2 r2 (which also waits on feedback-1, unreported) and loot-3 merge together, because between them the player cannot pick anything up. That is three or four cuts nobody has started. With floating-bodies split out (about 200 headless lines, no Unity change, no behaviour change), the mine waits only for that: retire-unity-mines r2 lands now in parallel, then sim-mines, then sim-mines-presenter. The price is one more small cut and a revision of loot-1 (r2 takes the body from floating-bodies and inherits the deletion of GridObject). The body's shape is fixed before loot uses it, a small risk, because loot and mines already fly the same constants (N2).

Why retire-unity-mines no longer waits on loot-3. r1 waited so it could delete GridObject once the pickups were off it. r2 leaves GridObject to loot-1 r2, whose pickups are its last users once the mine is gone.

Why the trigger shares Detonate's geometry. The old trigger asked Unity's colliders and the blast asked the hull cells, so a mine could trigger on a hull its blast then missed, or the reverse. One coverage test makes "triggered by" and "harmed by" the same disc.

Why no targeting provider or point defense against mines. The old mine was neither detectable nor shootable, and ruling pd-who-engages prices munitions, which a mine is not. Detection of floating bodies stays with follow-up bodies-detection.

Catalog breadth: the weapon matrix, roles backfill and stat-boosting gear

Pass: Imagination imagination-catalog-breadth, session self-2026-10-08b. Self merges this into docs/aetheria-release-map.md; nothing here is committed. Body: GameCult/Aetheria origin/master a8f71d463bf1160de0b5f23caa56dc7957252345 (fetched 2026-10-08), detached worktree in the session scratchpad, removed at the end. Rulings applied: catalog-breadth-weapons-gear-consumables, catalog-grows-generic-designs-branded-products, tech-lineage-per-component-role, weapon-tracking-authored, plight-not-one-cell, plight-shape. Operator, 2026-10-08, relayed by Self: "All this comes with roles and components too, right?" (not yet admitted as a ruling; Self should admit it so specs can cite it by id).

Part (c), consumables, is not mapped here. The component-role rule (CB-R1) and the stacking rule (question boost-stacking) are written so that pass reuses them unchanged.

Body facts

Decode method: cultcache_py SingleFileMessagePackBackingStore.pull_all() over GameData/Aetheria.cc at a8f71d46 (LFS pulled into the worktree), payloads msgpack.unpackbed and read by MessagePack key. Scratch scripts cat.py, wm.py, stats.py, not committed.

  • CB1. Census at a8f71d46. 74 FactionProductData, 51 CompoundCommodityData, 31 GearData, 22 WeaponItemData, 13 SimpleCommodityData, 12 Faction, 12 NameFile, 4 CargoBayData, 4 HullData, 3 PersonalityAttribute, 1 FieldKindData, 1 DockingBayData, 1 InputLayout. pd-gear and the Mine Launcher are on master; the Pirates (pirates-record 8e51016f), lot-product (ec081b9b), generic-design-names, product-lines-weapons, maker-only-faction (5dce222b), mods-in-tree (cc83f5ad) and faction-play-1 (b3d1e929) are not. Design names are still the branded ones.

  • CB2. The catalog already has a category axis and a size axis on weapons (source read ItemData.cs:477-487, Enums.cs:55-83). WeaponItemData.WeaponType (key 26: ElectromagneticallyPropelled, ExplosivelyPropelled, Laser, Electrostatic, ParticleProjection, Missile, MicroMissile, SplitMissile, Mine, Jet) and WeaponCaliber (key 25: Small, Medium, Large, ExtraLarge). The operator's vault note AetheriaLore Aetheria/Worldbuilding/Post-Elysium/Reference/Weapon Category Codes.md (33cd06a) defines the same taxonomy: caliber S/M/L/XL, weapon types EMPS, XPS, L, PL, ESD, PPC, launchers M, MM, MIRV with guidance D/G/S. The enum lacks PL (pulse laser) and adds Mine and Jet. The only code readers are the action-bar icon (Gameplay/ActionBarSlot.cs:154, GameSettings.cs:52-54).

  • CB3. The matrix at a8f71d46 (type x authored caliber, with cell footprint):

    Type Small Medium Large ExtraLarge
    XPS (Ballistic) ClearPath 2, Earp 2, 6k Shooter 2, PD Gun 1 Autocannon 4, DeathCluster 4, pretty pretty bang bang 4, Flak Gun 2 - -
    EMPS - - - -
    Laser (Energy) Spectra 2, ColdFire 2, PD Laser 1, CShot RainbowLite Lazer 9 - - -
    ESD (Energy) - - plight 2 -
    PPC (Energy) FastBlast+- 2 ChargeBlast+- 4, ChargeBlast SG 4 - -
    Missile (Launcher) - GT 3K 3 - -
    MicroMissile (Launcher) - scorched void policy 3, pswarm 1 LRMM72 6 SRMM72 6
    SplitMissile - - - -
    Mine (Launcher) - Mine Launcher 1 - -
    Jet - - - -

    Caliber and footprint disagree in six designs: CShot (Small, 9 cells), plight (Large, 2), pswarm (Medium, 1), SRMM72 (ExtraLarge, 6 like LRMM72's Large), Mine Launcher (Medium, 1), Flak Gun (Medium, 2). Map M30 already recorded CShot and plight.

  • CB4. Weapon homes on master (HullData.Hardpoints, key 23). Longinus: Energy 1x2 x2, Launcher 1x3 x2. Djinni: Ballistic 1x2 x2, Launcher 1x3 x2, Launcher 3x2 x2. Turret (station): Ballistic 2x4 x2. Zenith: none. LoadoutGenerator.HasHome (:201) offers a design only if some hull's hardpoint Takes it, so a Large or ExtraLarge Energy gun, an ExtraLarge launcher and a 2x2 Energy gun have no home until a hull carries such a mount. No mod package is in the tree yet.

  • CB5. Roles and components today (source read ItemData.cs:315-331, FactionProduct.cs:14-40, Provenance.cs:73, ItemData.cs:640-720). A design declares Roles (ItemRole, a name only; its comment: "the part itself earns a record when crafting needs one to exist"). A product authors ProductRole{Role, Mean, StandardDeviation} per role; ItemManager.CreateLot (ItemManager.cs:149) rolls each role's quality into the lot; a stat reads it through a StatTerm{Source: Quality, Role}. No record type for a component item exists; the 51 CompoundCommodityData (Circuit Board, Reaction Chamber, Heat Sink Tile, Trigger ...) carry no roles and nothing links a role to them. Lineage (tech-lineage-per-component-role) has no field.

  • CB6. Roles are live only where a stat reads them. AetherDb roles-migrate (tools/AetherDb/Program.cs:255-378) assigned roles per kind from fixed field maps (:156-216) and skipped flat stats (Min == Max) and kinds with no quality-reading behaviour. The census prints, but no test fails on, products that author no spread for a role (Program.cs:110-127).

  • CB7. The audit of in-flight content for roles (decode plus pirates-record diff read):

    • pd-gear (merged): all three designs declare their kind's roles, each read by non-flat stats; every product authors ProductRole for each. No gap.
    • Pirates (pirates-record 8e51016f, Program.cs diff lines 66-82): every rebrand mirrors its design's roles at apply time (mean .45, sd .22). It inherits the design's gaps: its Autocannon rebrand carries no roles because Autocannon declares none.
    • Mine Launcher (merged): no roles, every MineLayerData stat flat with no terms at all; its DME product authors none.
    • Also role-less with flat stats: Autocannon, LRMM72, SRMM72 (every stat Min == Max, Quality term with no role). Large Drive reads Quality with no role and declares none. Cargo bays, docking bay, Cockpit 2x2, Turret Control Module and Tractor Beam carry no quality-reading stat.
  • CB8. Stat modifiers exist and work for passive gear (source read Behaviors/StatModifier.cs, StatResolver.cs:108-145, Entity.cs:1960-1980). StatModifierData (behaviour union 4) names a target StatReference{Target type name, Stat field}, a magnitude PerformanceStat (so per-role quality works), Constant or Multiplier, and an optional RequireBehavior. While the gear is Active its behaviour groups execute in order and stop at the first that fails (Entity.cs:1969-1976), so an EnergyDraw before the modifier gates it on power; the modifier attaches to every matching stat on the same entity and detaches when it stops executing. PerformanceStat.Evaluate applies lerp * Scale + Constant, then the power factor (ItemData.cs:700-722). The sim reads every boosted stat through Evaluate (Thruster.cs:43, Sensor.cs:168, Radiator.cs:118, Weapon.cs:133-143, VelocityLimit.cs:35, TargetingSystem.cs:80). The catalog's one modifier: MoveOnPro (both sizes) scales CapacitorData.Capacity by 1.1-1.25.

  • CB9. Gap: a modifier cannot target a base behaviour type. TargetsOf (StatModifier.cs:91-114) matches GetType() == targetType at :98, :102 and :104, while ResolveStatField resolves any BehaviorData subtype by name, abstract ones included (ItemData.cs:785-799, Extensions.cs:36-44). A modifier on WeaponData.Damage validates and then attaches to nothing, because every gun's behaviour is a concrete subtype (AutoWeaponData, InstantWeaponData, ChargedWeaponData, ConstantWeaponData, LauncherData, MineLayerData).

  • CB10. Stacking today is unbounded. StatResolver.ScaleModifier multiplies every attached scale and ConstantModifier sums every constant (StatResolver.cs:108-122). Ten 1.15 multipliers compound to 4.0x. Consumable effects attach through the same resolver (Entity.cs:857-881, StatModifier.cs:63), so the stacking rule is shared with part (c).

  • CB11. Where stat gear lives. Tool gear has no hardpoint and fits any free interior cell (Entity.cs:1076-1090); Targeting Computer (Tool 1x1: TargetingSystem, EnergyDraw 2, Heat 15) is the closest template for a powered booster.

  • CB12. Consumables already have a sim mechanism (for part (c), not mapped here): ConsumableItemData (ItemData.cs, keys 10-14: Behaviors, Stackable, Duration, Icon, Effectiveness) is a CraftedItemData, so it declares roles; Entity.TryActivateConsumable (:872) and ConsumableItemEffect (:1582) run its behaviours, including StatModifier, with a ConsumableProgress stat source. The catalog holds no consumable record.

Model page row changes

  • WeaponItemData (catalog): who decides gains "the matrix cell": WeaponType and WeaponCaliber are the authored category and size (question weapon-matrix-axes); footprint lies in its caliber's band (Small 1-2 cells, Medium 3-4, Large 5-6, ExtraLarge 7-9).
  • CraftedItemData.Roles / FactionProductData.Roles: a design declares a role only if a non-flat stat reads it; every product authors one ProductRole per role (rule CB-R1).
  • StatModifierData: targets a behaviour type and its subtypes (after modifier-subtypes); the resolver's stacking rule is the one for gear and consumables alike (question boost-stacking).

Rules (cited by the specs)

  • CB-R1, roles and components (the whole ruling, consumables included). A crafted design (weapon, gear, consumable) declares roles iff it has a stat that varies with quality. Each declared role is read by at least one non-flat stat's Quality term; each non-flat stat reads exactly one declared role; every product that sells the design authors one ProductRole per role, from its maker's profile (catalog-growth addenda table; Pirates .45/.22 per pirates-record). Roles come from the kind's field map in Program.cs:156-216, extended for new kinds as named in each spec. A consumable's roles govern its effect magnitude stats the same way. Under the recommended option of question component-items, the component is the (design role, product ProductRole) pair: no component item record until crafting reads one, and the lineage mark of tech-lineage-per-component-role lands on ProductRole when the first aethertech product does.
  • CB-R2, authoring a new design. Copy a template by record key (never by name, so it holds across generic-design-names), set the shape, caliber, type and name, set every authored stat by the ladder below as a Min-Max range of value x (0.8, 1.2) (benefit stats rise with quality, cost stats fall), keep the template's non-quality terms, point each Quality term at its role, author Tracking per weapon with no terms (weapon-tracking-authored), carry no BlastRadius or Fuse (FireControlCut124Tests.cs:2589), no AmmoType (the ballistic-ammo revision owns rounds), and a description with no maker name. Two or more products from distinct makers, never Miss Terri's or the Pirates, product name never the design name.
  • CB-R3, caliber ladder (per step from the template's caliber): Damage per shot x2.0, Range x1.2, Mass x2.0, Price x2.0, Energy and Heat x1.8, Cooldown x1.25, Spread x0.85, Visibility x1.5. Tracking (authored, steep): 2-cell Small 10-12, Medium 5-6, Large 3, ExtraLarge 1.5; flame guns double those (the cone covers what tracking misses).
  • CB-R4, footprints for new designs. Small 1x2, Medium 2x2 (launchers 1x3), Large 2x3 (launchers 3x2), ExtraLarge 2x4 (fits the station Turret's Ballistic 2x4).
  • CB-R5, catalog blob. Each content cut lands through one idempotent AetherDb command; under follow-up catalog-blob-merge-order its merge takes master's catalog, reruns the command's apply, commits the regenerated blob in its own LFS commit with the dry-run text and reruns the suite. No cut picks a side's blob.

Design tables

Product names are drafts in each maker's voice (profiles: catalog-growth addenda); Hands writes the one-line descriptions in that voice.

matrix-coilguns (EMPS, Ballistic hardpoint, template ClearPath 0271bb97 for Small and Autocannon 13116166 above; EMPS vs XPS at one caliber: Velocity x2.5, Penetration authored high, Energy x3 per ballistics-cycle-power, Damage x1.2, Cooldown x1.6; roles barrel, feed mechanism):

Design Caliber Shape Products (maker)
Light Coilgun Small 1x2 RC-1 Needle (R&D), Pin (Finch)
Coilgun Medium 2x2 RC-4 Spindle (R&D), Plumb Line (Adrasteia)
Railgun Large 2x3 True Meridian (Adrasteia), RX-9 Lancer (R&D), Gavel (Zhestokost)
Heavy Railgun ExtraLarge 2x4 Axis (Adrasteia), Long Reach (AU)

matrix-cannons (XPS, template Autocannon, AutoWeaponData; barrel, feed mechanism):

Heavy Autocannon Large 2x3 Bulwark (Zhestokost), Foreman (AU)
Siege Cannon ExtraLarge 2x4 Last Word (Zhestokost), big red problem (DME)

matrix-beams (Energy; Laser from ColdFire 2933c4ce, PPC from ChargeBlast+- 499f7891; focusing array, power coupling):

Medium Beam Laser Medium 2x2 Filament (Alakrita), Limelight (Lucent)
Large Beam Laser Large 2x3 Marquee (Lucent), Clear Sight (Adrasteia)
Heavy Ion Cannon Large 2x3 Blackout (NiteLife), short circuit (DME)
Ion Lance ExtraLarge 2x4 Lights Out (NiteLife), IX-12 Breaker (R&D)

matrix-lightning (ESD, ChargedWeaponData from plight 632420a6; focusing array, power coupling):

Arc Thrower Small 1x2 Sparkler (Lucent), Live Wire (NiteLife)
Storm Projector Medium 2x2 Thunderhead (NiteLife), everybody dance (DME)
Tempest Projector ExtraLarge 2x4 Grand Finale (Lucent), Mains (NiteLife)

matrix-missiles (Launcher; Missile from GT 3K 5ee2ff03, MicroMissile from LRMM72 32157982; guidance system, thruster, warhead):

Light Missile Launcher Small 1x2 Fledgling (Finch), Courier (AU)
Heavy Missile Launcher Large 3x2 GT 6K (R&D), Freight Forward (AU)
Torpedo Launcher ExtraLarge 2x4 Battering Ram (Zhestokost), GT 12K (R&D)
Micromissile Battery ExtraLarge 2x4 the whole show (DME), Fireworks (Lucent)

matrix-mines (Launcher, MineLayerData from Mine Launcher 1b4a2bb3; new role map: warhead reads Damage, Penetration, DamageSpread; dispenser reads Range, Cooldown, Spread, Velocity, Energy, Heat, Visibility; ArmingDelay, FuseDelay and Lifetime stay authored per design, not ladder-scaled):

Mine Rack Medium 1x3 party favours (DME), Caltrop (Zhestokost)
Heavy Mine Layer Large 3x2 surprise party (DME), Wake Warden (Lightsail)
Minefield Projector ExtraLarge 2x4 Exclusion Zone (Zhestokost), do not enter (DME)

matrix-flamers (Jet, Ballistic hardpoint, InstantWeaponData pellet gun from Flak Gun 6af6446e: DamageType Thermal, Count 8-12, Spread 25-35 deg, Range 150-300 at Small, Velocity 300; roles nozzle (Range, Spread, Velocity) and fuel pump (Damage, Cooldown, Energy, Heat, Visibility)). Absorbs follow-up pirates-flamethrower's design; the Pirates' own 🔥 product moves in their own record, not here.

Light Flamethrower Small 1x2 hot take (DME), Scorch (Zhestokost)
Flamethrower Medium 2x2 keep it lit (DME), Brushfire (AU)
Heavy Flamethrower Large 2x3 Purifier (Zhestokost), burn the boats (DME)
Inferno Projector ExtraLarge 2x4 Ashfall (Zhestokost), Afterparty (NiteLife)

After these, every cell of CB3 is filled except the SplitMissile row (follow-up mirv-split-munition): 24 new designs, 49 new products.

Stat boosters (Tool 1x1, template Targeting Computer c07ea205's EnergyDraw and Heat, then a StatModifierData Multiplier in the same group so power gates it; roles amplifier (the magnitude) and regulator (EnergyDraw, Heat); magnitude per question boost-strength). Superseded 2026-10-09 by ruling boost-strength-tiered-by-size: each stat gets a modest 1x1 and a strong 2x2 design, tabled in 'Spec refresh 2026-10-09' below):

Design Targets Products (maker) Cut
Thrust Amplifier ThrusterData.Thrust Tailwind (Finch), Second Wind (Lightsail) boost-gear-systems
Signal Amplifier SensorData.Sensitivity Earful (Finch), Lookout (AU) boost-gear-systems
Emissivity Booster RadiatorData.Emissivity Cold Read (Adrasteia), Cool Head (Lightsail) boost-gear-systems
Reactor Overclock ReactorData.Charge Afterhours (NiteLife), redline (DME) boost-gear-systems
Damage Amplifier WeaponData.Damage Showstopper (Lucent), Heavy Hand (Zhestokost) boost-gear-combat
Range Extender WeaponData.Range Far Sight (Adrasteia), Long Look (Finch) boost-gear-combat
Shield Booster ShieldData.Capacity Thick Skin (AU), SB-2 Bastion (R&D) boost-gear-combat

Rationale

Why the category is WeaponType and the size is WeaponCaliber. The operator's own taxonomy (CB2) names exactly these two axes, and the catalog already carries both fields. Footprint is mount space, which the brief's "sizes (cell footprints)" reads as the size; the two disagree in six records (CB3). Recommended: caliber is the one authored size, and a catalog test holds every footprint inside its caliber's band, so neither can drift; the six are fixed as content. plight is the exception the operator holds open (ruling plight-shape, Defaulted, revisit after play): it stays Large, and the band test names that ruling as its one exemption. Prior art: Starsector mounts are type x size (small/medium/large; ballistic/energy/missile) and every type exists at every size; Elite's hardpoints are class 1-4 with weapon families not filling every class.

Why MIRV waits. A split missile with no split is a mislabelled missile. Missiles become stepped munition records only after missile-records (r3, no report); splitting belongs on that record.

Why components are the role and its ProductRole. No system reads a part record (crafting is not in the demo; the target's not-in-scope line excludes new mechanics), so a component item now would be inert data. X4 and EVE give components item records because their economies produce and consume them; the role-quality pair already carries what the operator's examples need (a laser better at what its focusing role governs). Question component-items asks the operator.

Why the stacking question recommends diminishing returns. Interior cells are cheap compared with hardpoints, so unbounded multiplication (CB10) makes a hold full of amplifiers the dominant fit. EVE University, Stacking penalties (fetched 2026-10-08): the n-th percentage modifier on one attribute is multiplied by S(n-1), S(u) = e^-(u/2.67)^2, strongest first, so 100%, 86.9%, 57.1%, 28.3%, 10.6%, 3.0%; absolute effects are never penalized; positive and negative effects are penalized separately. Starsector hullmods are on or off per ship and priced in ordnance points (starsector.wiki.gg/wiki/Hullmods, fetched 2026-10-08: Unstable Injector raises speed and cuts weapon range 25%); the exact bonus values were not on that page. Nebulous jammers stack on a similar diminishing curve (consumables prior-art file, "Stacking and exclusivity"). Because consumables attach through the same resolver, whichever rule the operator picks covers part (c) too.

Cut order. roles-backfill and modifier-subtypes are independent of everything here. The seven matrix cuts are independent of one another and of generic-design-names (templates by key, new designs born generic); they wait only on the axis ruling. caliber-bands lands last: it recalibers six records and adds the band and coverage tests that would fail before the matrix fills. product-lines-weapons r2 adds roles-backfill to r1's dependencies so its new products of Autocannon, LRMM72, SRMM72 and Mine Launcher author ProductRole for real roles. product-lines-gear turns follow-up gear-product-lines into a spec (Self withdraws the follow-up as absorbed when it admits nothing else over it). boost-gear-combat waits on modifier-subtypes; modifier-stacking and both boost cuts wait on the operator.

Record keys. No cut here adds a MessagePack key to any type, so none touches the Faction key collision (maker-only-faction 19, faction-play-1 Doctrine, livery 18).

Admitted (mind receipts 2026-10-08 19:48-19:50 UTC)

Questions: weapon-matrix-axes (raised in matrix-coilguns, blocks every matrix cut and caliber-bands), component-items (raised in nothing: the specs follow its recommended option and stay valid under the others), boost-strength (raised in boost-gear-systems), boost-stacking (raised in modifier-stacking). Cut specs r1: roles-backfill, modifier-subtypes, modifier-stacking, boost-gear-systems, boost-gear-combat, matrix-coilguns, matrix-cannons, matrix-beams, matrix-lightning, matrix-missiles, matrix-mines, matrix-flamers, caliber-bands, product-lines-gear; product-lines-weapons r2 with resolution cut_spec.cut-product-lines-weapons.r1.n1 (Superseded). Follow-ups: mirv-split-munition, homeless-calibers, matrix-rounds. Left for Self: follow-up gear-product-lines is absorbed by product-lines-gear (close it when that merges); follow-up pirates-flamethrower's design half is absorbed by matrix-flamers (its product move stays the Pirates' record's).

Consumables: the host fix, supply, throttle lock and vapour cloud

Pass: Imagination imagination-consumables, session self-2026-10-08b. Self merges this into docs/aetheria-release-map.md; nothing here is committed. Body: GameCult/Aetheria origin/master a8f71d463bf1160de0b5f23caa56dc7957252345 (fetched 2026-10-08 ~20:00 UTC; the pirates-record merge had not landed), detached worktree in the session scratchpad, removed at the end. Catalog: LFS blob GameData/Aetheria.cc sha256 5ecf375f...a011, decoded with cultcache_py SingleFileMessagePackBackingStore.pull_all() and msgpack (payloads are arrays indexed by MessagePack key). Scratch probes q1_thrust_speed.py, q2_templates.py, not committed.

Rulings applied: catalog-breadth-weapons-gear-consumables (operator: "consumables (goofy stuff like juicing your thrusters for a few seconds but locking the throttle at max, or a consumable coolant that turns into a cloud of vapor to break locks)"), catalog-breadth-roles-and-components ("All this comes with roles and components too, right?"), catalog-grows-generic-designs-branded-products, sensor-stat-set. Reused unchanged from parts (a) and (b) (F:\Projects\aetheria-map-addenda-catalog-breadth-2026-10-08.md): CB-R1 (roles), CB-R2 (authoring), CB-R5 (catalog blob), and the stacking rule of question boost-stacking through cut modifier-stacking. Prior art: F:\Projects\aetheria-consumables-prior-art-2026-10-08.md (cited below as PA section n).

Body facts

  • CC1. The consumable mechanism exists end to end in code, and no record uses it. Census: no aetheria.consumableitemdata record in the catalog (decode). ConsumableItemData : CraftedItemData (ItemData.cs:342-358, keys 10 Behaviors, 11 Stackable, 12 Duration, 13 Icon, 14 Effectiveness; Roles at key 9 from CraftedItemData). Instance ConsumableItem (ItemInstance.cs:65, union 3 and 2). Activation: Entity.TryActivateConsumable (Entity.cs:872-884) refuses a second active effect of a non-Stackable design (CanActivateConsumable, :867-870), takes the first instance of that design from cargo, builds a ConsumableItemEffect and removes the instance. Each tick (Entity.cs:1443-1454) the effect updates its behaviours in order, stopping at the first that fails (ConsumableItemEffect.Update, :1604-1619), and at RemainingDuration < 0 is dropped after Resolver.Forget(effect). Catalog load validates a consumable's modifiers and role usage (AetheriaStores.cs:77-80).
  • CC2. Player input exists; AI input does not. The player drags a consumable from cargo onto an action-bar slot (ActionGameManager.cs:484-485), the slot calls TryActivateConsumable (ActionBarSlot.cs:118-121), shows the count in cargo and the remaining-duration fill (:128-134), and the binding is saved (SavedGame.cs:221,230). Nothing in Agents/ calls it (grep: the only caller is ActionBarSlot.cs:120).
  • CC3. Defect: a consumable's stat modifier never reaches anything, and would throw. StatModifier.Initialize (StatModifier.cs:82-89) is what computes _targets; the only caller of IInitializableBehavior.Initialize is Entity.Activate over Equipment (Entity.cs:202-207). ActivateConsumable (Entity.cs:857-860) never calls it, so _targets stays null. The effect's Update runs IAlwaysUpdatedBehavior.Update before Execute; on the second tick _executed && !_applied calls ApplyModifier (:224-230), which iterates the null _targets: a NullReferenceException. Source read; no test covers it (the expiry test StatResolverTests.cs:363 uses a consumable with no behaviours). Body fact CB10's claim that consumable effects attach through the resolver holds for the resolver's API, not for this host path.
  • CC4. Defect: an expired consumable's modifiers would outlive it. Attachments are keyed by the target item as owner and the StatModifier instance as modifier key (StatModifier.cs:224-237). Expiry calls Resolver.Forget(effect) (Entity.cs:1451), which removes only entries whose owner is the effect (StatResolver.cs:154-170); nothing calls StatModifier.Dispose (:245-249). Once CC3 is fixed, a boost would stay attached to the thrusters forever.
  • CC5. Defect: a bought or generated consumable is not a ConsumableItem. ItemManager.CreateInstance(int lot) (ItemManager.cs:173-189) returns EquippableItem or else CompoundCommodity; TryActivateConsumable then casts the cargo instance to ConsumableItem (Entity.cs:880), an InvalidCastException. The tests build ConsumableItem by hand for this reason (LoadoutTests.cs:787-791).
  • CC6. Nobody sells consumables, and nothing restocks. Station stock is RandomProducts<EquippableItemData>(16, 1, ...) into the station's cargo at generation (LoadoutGenerator.cs:110-119); RandomProducts and AvailableProducts are constrained to EquippableItemData (:142, :154) because the availability rule includes HasHome (:164). Stations are generated once per zone (ZoneGenerator.cs:303,324); no code restocks a station (grep Restock, GenerateStationLoadout). The trade menu sells out of station cargo (StationServices.TryBuy, StationServices.cs:73; BuyPrice prices any CraftedItemInstance, :13-24).
  • CC7. Cargo is the inventory. A crafted instance occupies its design's Shape cells in a cargo bay (Entity.cs:2187-2207); there is no item stack for crafted items. A consumable design therefore needs a Shape (ItemData key 5); CompoundCommodity records have none (decode: Shape None, Price 0), so they are not templates. Targeting Computer c07ea205 is the 1x1 template for mass, specific heat and conductivity (CB11).
  • CC8. How locks are decided. Per observer, EntityInfoGathered[target] rises by Sensor.Gain (Sensor.cs:187-189: passive visibility x sensitivity x curve(bearing) x dt / distance, ping visibility x sensitivity x PingBoost x distance once) and decays by TargetInfoDecay each tick (Sensor.cs:155-178). Crossing TargetDetectionInfoThreshold adds or removes the target from VisibleEnemies (Entity.cs:261-277); leaving VisibleEnemies clears the observer's target (Entity.cs:248-250), which zeroes every LockWeapon lock (target changed, LockWeapon.cs:85-90). Lock rate is LockSpeed x info^SensorImpact inside LockAngle (:98-105); launchers are LockWeapons (Launcher.cs:7, CreateInstance returns LockWeapon). FireControl's sensor factor also reads info (FireControl.cs:439-442). Minions retarget when an enemy re-enters VisibleEnemies (Minion.cs:13). A missile already in flight is a Unity GuidedProjectile that follows its Target transform (GuidedProjectile.cs:44,142); the simulation does not re-decide it. The same Gain serves chunk detection through PassiveRate (Sensor.cs:194-199, Entity.cs:384), and MiningCut3Tests.cs:254 pins the entity info trace bit for bit.
  • CC9. How thrust and the throttle work. Ship.MovementDirection (Ship.cs:27) has two writers, the player's input (ActionGameManager.cs:1339) and the agent (Agent.cs:70-80); Ship.Update (Ship.cs:231-257) turns it into each thruster's Axis (strafe at :241-252, forward and reverse at :255-256) and Turn into rotation. A thruster pushes by Thrust and adds heat and visibility proportional to its input, not to thrust (Thruster.cs:116-137), so a Thrust multiplier alone costs no heat. Top speed is a hull's VelocityLimitData.TopSpeed (decode: Djinni 50, Longinus 100), clamped in VelocityLimit.Execute; the hull is in Equipment (Entity.cs:886, MapEntity), so a modifier on VelocityLimitData.TopSpeed reaches it. Catalog thrusters: Small Drive 75k, Talaria 75k-250k, Medium Drive 100k-300k, deep space burnout and RevvITup 250k-1M, Victoire 200k-1.5M (Thrust Min-Max).
  • CC10. Zone bodies. A mine is a runtime-only KinematicBody in Zone.Mines (Mines.cs, Zone.cs:22), added only by Zone.Lay (:235-239), stepped and removed only by StepMines (:245-260) after the entity loop, never saved, and drawn by one ZoneRenderer subscription (ZoneRenderer.cs:225-264, MinePrefab :47). This is the pattern a vapour cloud follows.
  • CC11. A consumable's own heat does nothing. Behavior.AddHeat is a no-op for a consumable host (Behaviors.cs:60, "TODO: Heat for Consumables"). The first set takes its heat cost through the thrusters' own Heat stat instead.
  • CC12. Record keys. BehaviorData union ids run to 40 (MineLayerData, Behaviors.cs:189); 12 and 26 are retired. No remote branch (71 refs, git show <ref>:.../Behaviors.cs) claims 41 or above. This pass claims 41 ThrottleLockData and 42 VapourDumpData. No key is added to Faction, ConsumableItemData or any saved type; the Faction 16-19 collision is untouched. VapourCloud is runtime only, like Mine.

Model page row changes

  • ConsumableItemData (catalog): new live row. Named by record key; a design with roles (CB-R1), Shape 1x1, a Price, Stackable false, a Duration, and behaviours. Decided by its AetherDb command.
  • ConsumableItem (run save, cargo): created by ItemManager.CreateInstance for a consumable design; lives in a cargo bay; consumed by TryActivateConsumable.
  • ConsumableItemEffect (runtime): one per activation; owns its behaviours' lifetime: initialised at activation, disposed at expiry, then forgotten by the resolver. Not saved.
  • Entity.ThrottleLocked (derived): true while any active effect carries a ThrottleLock. Nobody writes it. Ship.Update is the one place that turns intent (MovementDirection) into thruster axes, and it reads the lock there.
  • VapourCloud (runtime, Zone.Clouds): added only by Zone.Vent, stepped and removed only by Zone.StepClouds. Zone.Obscuration(a, b) is derived from the live clouds; Sensor.Gain is its one reader.

Rules (cited by the specs)

  • CS-R1, the consumable mechanic. A consumable is a cargo item, single use, activated from the action bar (player) or by an agent rule (AI). Its effect is timed (Duration) and carries its own drawback in the same record. A non-Stackable design allows one active effect at a time, so its Duration is also its cooldown; no second cooldown mechanism. Boosts from a consumable multiply with gear under the one resolver rule (ruling boost-stacking-multiply; pinned by cut modifier-stacking r3).
  • CS-R2, roles. Per CB-R1, every non-flat effect stat reads one declared role and every product authors one ProductRole per role. Benefit magnitudes rise with quality, cost magnitudes fall.
  • CS-R3, the throttle lock. While locked, the effective movement intent is full forward, no strafe, no reverse; turning stays with the pilot (question overdrive-steering). Intent writers keep writing; they are not owners of the thruster axes.
  • CS-R4, obscuration. A cloud is a disc with an opacity that fades linearly to 0 over its lifetime. Every sensor gain (passive, ping and chunk) is multiplied by the product of (1 - opacity) over the clouds the sight line crosses, endpoints inside counting as crossing; with no cloud crossed no multiplication happens, so the info trace is bit-identical. Symmetric: the venting ship sees out no better than others see in. Lock loss is the existing rule (CC8): info decays below the detection threshold and the target is dropped. A sensor strong or close enough still burns through.

Design tables

Thruster Overdrive (cut consumables-first-set; ConsumableItemData; Shape 1x1; Mass 10; Price 6,000; Stackable false; Duration 5 s; roles propellant, regulator; behaviours in order: ThrottleLock, then four StatModifierData Multipliers):

Target stat Magnitude Min-Max Role
ThrusterData.Thrust 2.0-3.0 propellant
VelocityLimitData.TopSpeed 1.5-2.0 propellant
ThrusterData.Heat 4.0-2.5 regulator
ThrusterData.Visibility 4.0-2.5 regulator

Products: send it (Death Monkey Explosives), Updraft (Finch Cybernetics). The heat and plume cost is real because thrusters heat by input, not thrust (CC9): a locked throttle runs every forward thruster at full input for 5 s at up to 4x heat and plume. Prior art: Starsector Burn Drive (+200 speed for 5 s, no steering, PA 1), Plasma Jets (+375% acceleration, 3 s, PA 1), EVE overheating (+50% propulsion, heat damage, PA 5).

Coolant Vent (same cut; ConsumableItemData; Shape 1x1; Mass 15; Price 4,000; Stackable false; Duration 12 s; roles nozzle, coolant; behaviours in order: VapourDump, then one StatModifierData Multiplier):

Stat Min-Max Role
VapourDumpData.Radius authored so the cloud encloses the largest ship hull (Djinni, 14x17 cells) at Min; Max 1.5x Min nozzle
VapourDumpData.Opacity 0.80-0.95 nozzle
VapourDumpData.Lifetime 10-14 s coolant
RadiatorData.Emissivity (penalty, the drawback: the coolant is gone) 0.4-0.6 coolant

Products: Smoke Machine (NiteLife Energy), Morning Fog (Lightsail Express). Prior art: Nebulous jamming against seeker return (PA 8), Elite heat sink and silent running (PA 3), Star Citizen chaff as a field effect (PA 9), EVE ECM chance as strength against sensor strength (PA 5).

Rationale

Fix the host before adding content. The mechanism the brief names (CB12) exists but is broken in three places (CC3, CC4, CC5): a consumable with a stat modifier throws on its second tick, would leak its boost if it did not, and can never be bought. consumable-host is the smallest cut that makes the existing path true; no new owner. Cut modifier-stacking's test AConsumableBoostStacksWithGear cannot pass before it, so its r2 adds the dependency and nothing else.

Why the throttle lock is derived, not latched. Two writers already set MovementDirection (CC9). A third writer (the consumable) would race them; a latch set by a behaviour and cleared each tick is a second owner of time. Entity.ThrottleLocked derived from the active effects, read once in Ship.Update, cannot outlive its effect and needs no reset path.

Why obscuration and not a flat break. The sim already decides locks by information against a threshold (CC8), and lock loss already clears targets and zeroes locks. Scaling gain along the sight line makes the cloud a contest of signal against obscurant, the Nebulous shape (PA 8: a seeker's return must exceed the jamming it feels; burnthrough exists), and EVE's lesson that a guaranteed area break is unreliable or oppressive is avoided (PA 5). A flat break would add a second rule that drops targets, beside the one that exists. The cloud rides Sensor.Gain, which survives the sensor-stat-set rebuild (every emitter still produces a gain). In-flight missiles are not re-decided by the sim (CC8): the cloud denies the lock before launch; seekers in flight are follow-up cloud-vs-missiles-in-flight.

Why the cloud is a zone body that inherits velocity. Vented gas keeps the ship's velocity (KinematicBody, CC10), so a coasting ship stays inside it and a ship that burns leaves it: the two consumables combine (vent, then overdrive out of the cloud) and their drawbacks bite each other (the overdrive plume is bright; leaving the cloud ends the cover). The symmetric rule makes hiding blind.

Hoarding (written for the abundant option; the operator ruled consumables-scarce on 2026-10-09 and accepted the hoarding risk, so the cheap, everywhere-stocked half of this paragraph no longer holds: see 'Spec refresh 2026-10-09'). The elixir literature (PA 10) names scarcity, unlimited inventory and unclear purpose as causes, and caps, abundance and situational need as cures. Here: cargo cells and mass are the cap (CC7), both designs are cheap (6,000 and 4,000 against 25,000-250,000 for a drive) and stocked at every station under question consumable-supply, AI pilots use them (cut consumable-ai) so the player sees what they do, and each solves a moment (being locked, needing distance) rather than adding a stat the player could save for a boss. Neither is a permanent advantage: each carries its drawback inside the effect. X4's lesson (PA 9: flares strong in AI hands, worthless in the player's) is answered by giving the player and the AI the same rule and the same information (a painted target always knows, sensor-stat-set).

Magnitudes are authored ranges grounded in the prior art above; follow-up combat-pace-pass measures time-to-kill and engagement distance and tunes them with everything else. No question is raised on them.

Cut order. consumable-host, throttle-lock and vapour-cloud are independent. consumable-supply needs the host (instances). vapour-cloud-presenter needs the cloud. consumables-first-set needs the host, the lock and the cloud. consumable-ai needs all of those and supply. modifier-stacking r2 now waits on the host.

Admitted

Receipt mind-commit-06b3cb76... (2026-10-08 20:05 UTC). Questions: consumable-supply (raised in consumable-supply), vapour-cloud-model (raised in vapour-cloud), overdrive-steering (raised in throttle-lock). Cut specs r1: consumable-host, consumable-supply, throttle-lock, vapour-cloud, vapour-cloud-presenter, consumables-first-set, consumable-ai; modifier-stacking r2 (adds depends_on consumable-host) with resolution cut_spec.cut-modifier-stacking.r1.n1 (Superseded). Follow-ups: cloud-vs-missiles-in-flight, modifier-magnitude-frozen-at-attach, consumable-heat-noop, station-restock. Claimed keys: BehaviorData union 41 ThrottleLockData, 42 VapourDumpData (keys 1-3).

Ships add-on frame r5: authoring without the repo

Imagination, agent imagination-addon-frame, session self-2026-10-08b. Campaign aetheria-release. Builds on F:\Projects\aetheria-map-addenda-addon-refresh-2026-10-08.md (A1 to A14); A14 recorded the tension this page resolves. Numbering continues at B1.

Pinned heads: Aetheria 07dcb580 (branch eureka/aetheria-release-ships-addon-frame-r4, local and on origin; the brief's name eureka/aetheria-release-ships-addon-frame does not exist), CultLib 05e67014 (main), CultLib 45c2f400 (Aetheria's pinned CultLibRevision, Directory.Build.props:6). Every line number below is at 07dcb580.

Body facts

B1. What New Ship needs from the repo (source read). _new_ship (tools/blender/aetheria_ships/__init__.py:556-605) finds the repo by walking up for tools/AetherDb (_aetheria_repo, 314-325), writes to <repo>/GameData/Mods/<id>/ship.cc, and runs dotnet run --project <repo>/tools/AetherDb -- ship-authoring create (_aetherdb, 328-336). create (tools/AetherDb/ShipAuthoringCommands.cs:47-67) reads the reference hull from GameData/Aetheria.cc through AetherDb.Open(), whose FindRoot (tools/AetherDb/AetherDb.cs:26-32) keys on Aetheria.Shared/Aetheria.Shared.csproj, and builds the hull with ShipAuthoringStore.HullLike (Assets/Scripts/ServerShared/ShipAuthoring.cs:95-106): copy every member, reset Name, Shape (new Shape()), Hardpoints, Prefab, Schematic and Visual. create does not run the ship-ID rule; only Validate (ShipAuthoring.cs:189-192) does.

The same _aetherdb port also carries Package's validate (__init__.py:650), and the specs in force add centre (package-frame r1) and paint (paint r3). So the repo and dotnet dependency is the port's, not New Ship's alone: Package already needs both today.

B2. Where an author's machine gets the shipped catalog (source read). The game reads <install>/GameData/Aetheria.cc and <install>/GameData/Mods (Assets/Scripts/Gameplay/ActionGameManager.cs:40,48-49), with GameData beside the player's data folder. A ship author who has the game has both. No build or release script in the repo copies GameData into a player build (searched Assets/Scripts/Editor, tools, workflow files): the runtime contract is the only evidence that a release carries it.

B3. Studio needs Unity (source read). CultCache Studio is CultCacheStudioWindow : EditorWindow (CultLib src/GameCult.Unity/Assets/Caching/Editor/ CultCacheStudioWindow.cs), opened from a Unity menu item. Ruling authoring-host-thin-imgui-then-thing keeps it an IMGUI skin until a Thing host after the release. Studio therefore cannot be the repo-free and Unity-free creator before then.

B4. The shipped catalog's schema catalog (probe probes/p1_catalog.py on the 07dcb580 LFS object 9e75d9a4...). 13 catalog entries, 219 records, 5 hulldata records (LonginusX, Zenith, Turret, Djinni, Longinus; Djinni, Longinus and LonginusX are HullType 0, Ship). It carries an aetheria.hulldata entry and no aetheria.ship_authoring entry: the C# store writes catalog entries only for types it holds records of.

B5. cultcache-py cannot derive a C# catalog identity (probe p3_schema_id.py and source read). For all 13 C#-written entries, content_hash == sha256(canonical_schema_json) but schema_id != sha256(canonical_schema_json): C# derives the id from a semantic fingerprint over its member descriptors (CultLib src/GameCult.Caching/CultCache.cs: 655-658), with type names from CultSchemaTypeNames.FromType. cultcache-py's DocumentDefinition.catalog_entry (packages/cultcache-py/src/cultcache_py/documents.py: 105-143) defaults the schema id and content hash to the type name, and its store's default entry for an unregistered envelope is a stub with no members (stores.py:356-368). Python can carry a C#-written entry through a write; it cannot author one.

B6. Encode and decode probe on Yggdrasil (ygg-verify.sh, image mcr.microsoft.com/dotnet/sdk:10.0, Aetheria 07dcb580, CultLib 45c2f400 staged, logs scratchpad/probes/ygg-create-1.log and ygg-publish-2.log). Reference: dotnet run ... ship-authoring create /tmp/o/dn/ship.cc probe.a "Probe A" --like Djinni.

Candidate catalog hulldata catalog ship_authoring hull record ship record C# inspect
cultcache-py, no template (py_create.py) identical (copied from the shipped catalog) differs: stub, schema id aetheria.ship_authoring, version aetheria.ship_authoring.v1, members [] equal values identical bytes loads: probe.a: Probe A, 0 hardpoints
cultcache-py with the C# file's ship_authoring entry identical identical equal values, different bytes: 20 float32 (0xca) slots re-encoded float64 (0xcb), 204 to 284 bytes identical loads
published AetherDb (B7) identical identical identical identical loads

stored_at is ignored throughout. new Shape() encodes [[1, 1, [true]]]. The C# reader accepted the stub catalog by schema name (compatible drift, CultCache.cs:518-522, 803-830), reporting every slot as defaulted_missing_slot while still decoding the payload's values: that warning text and the observed behaviour disagree, so a file that relies on it relies on a CultLib behaviour nobody owns. The float widening is the same in today's ship_cc.replace_layout, which re-packs the whole hull body on every Save; C# reads float64 into float (smoke_frame's Package validated after a Rasterise write), so it is a wire-parity gap, not a corruption (follow-up py-float-width-parity).

B7. A published AetherDb needs neither dotnet nor the repo (Yggdrasil, log ygg-nodotnet-5.log). dotnet publish tools/AetherDb -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true gave one 77,287,607-byte executable (75 MB folder with pdbs). Run in debian:bookworm-slim with no dotnet (command -v dotnet: none), from a folder holding only GameData/Aetheria.cc plus an empty Aetheria.Shared/Aetheria.Shared.csproj (the current FindRoot marker), create ... --like Djinni took 1.1 s and wrote a file whose catalog and both records are byte-identical to the dotnet-run reference. The first run aborted with No usable version of libssl was found; with libssl3 and libicu72 installed it ran. Windows needs neither package; the win-x64 publish was not probed.

B8. The reserved-name gap is the owner's (same log). The published tool created /out/con/ship.cc for ID con and inspect loaded it: the C# rule (ShipAuthoring.cs: 190-192) admits Windows device names, and create does not run it at all (B1).

B9. Downstream specs read (mind, 2026-10-08). package-frame r1 says "after frame r4 has merged" and its recentre test edits cells and Packages with no Save; mounts r4's Package writes derived Positions straight to the .cc through replace_layout; gizmos r2's drag release writes through replace_layout from an interactive (undoable) tool. bake r1, guards r1, mesh-mounts r1, mask-bakes r1 and paint r3 do not depend on what r5 changes (paint's paint command goes through the _aetherdb port, which the tool cut moves).

The owner answer

What can create and write a ship .cc on an author's machine without the repo or dotnet? Two things, by probe: cultcache-py, if a C#-written aetheria.ship_authoring catalog entry is handed to it (B5, B6); or AetherDb itself, published self-contained (B7). Studio cannot before the Thing host (B3).

Recommendation: the published AetherDb (question authoring-tool-without-repo, option published-aetherdb). It keeps C# the single owner of every ship-package rule the add-on calls (HullLike, the ID rule, Validate, CenterOfMass, Paint) and removes the repo and the SDK from all four commands at once, which the Python route does not: Package, centre and paint would still need the repo. The Python route needs a second copy of HullLike's reset list and a pinned catalog entry, and widens floats until CultLib fixes B6. The repo-free reference hull is the author's own game install (GameData/Aetheria.cc, B2).

Prior art (from knowledge; no Eyes pass was run in this budget, so confidence is medium): game modding kits that keep one rule owner ship the game's own code compiled to modders rather than re-implementing it in the content tool: Valve's Source SDK ships compiled studiomdl, vbsp and vrad; Bethesda's Creation Kit is built on the engine's record code; games that ship no tool (Factorio, KSP, Minecraft data packs) validate content at game load, which Aetheria also does (ShipModCatalog.ResolveCatalog excludes a bad package and names it). Blender add-ons that drive external compilers (the Source Engine tools add-on, io_scene_valvesource, calling studiomdl) take the executable's path as a preference.

Authority map, r5

  • Owner of the .cc on the author's machine: two explicit, non-undo writers. Save Layout owns cells and hardpoints (and the first Save of a new ship creates the file through the published AetherDb create). Package owns the visual (model asset, anchors, lines) and, from mounts r5, saves the layout buffer through Save's writer. C# (through the published AetherDb) owns HullLike, the ship-ID rule, Validate, CenterOfMass and Paint.
  • Owner of the frame: the .blend. The layout buffer (scene.aetheria_layout), aetheria.grid_origin with the grid size it was computed for, Ship Root and the objects under it all live in the .blend, so Blender's undo moves them together.
  • Demotions. Rasterise and Flip Nose are no longer .cc writers; they propose cells into the buffer. New Ship is no longer a .cc writer; it binds a pending ship. The repo is no longer an input; the game data folder is. dotnet run is no longer a path.
  • Package and an unsaved buffer. Package refuses while the bound collection's buffer differs from the .cc ("Save Layout first"), so it never packages cells the author has not saved. The comparison is ship_cc's own layout revision of the buffer against the revision Load or Save recorded. Mounts r5 derives Positions into the buffer after that check and saves them through Save's writer.
  • One chooser decides which collection is the ship; Bind and New Ship accept an unbound collection through it, everything else requires a bound one.
  • One frame predicate: the ship's meshes are Ship Root's descendants outside Source and Generated with no aetheria.role. Rasterise reads them; Flip Nose turns Ship Root's children; package-frame's recentre moves Ship Root. A mesh in the bound collection that is not under Ship Root is refused by Rasterise, Flip Nose and Package with a count, never silently left out.

Rationale

Why Save, not the UNDO operators, writes the .cc. Blender's memfile undo restores the .blend and cannot restore a file on disk. Any undoable operator that writes the .cc splits the grid's truth (finding undo-splits-origin-from-cells), and dropping the UNDO flag does not help: the next undo push still restores the scene to the step before. So no undoable operator writes the disk, and the .cc changes only on an explicit Save or Package. This is the editor-buffer pattern Blender itself follows for external files (images are edited in memory and saved with Image > Save; the .blend is saved explicitly).

Why New Ship creates nothing on disk (new-ship-orphans-cc). Creation is deferred to the first Save, which creates and fills the file in one operator: create, then the buffer through replace_layout, and on any failure after create it removes the file and the folder it made. Nothing persists unless the ship is valid enough to save, and an undone New Ship leaves nothing to orphan.

Grid placement against outside edits. Studio can resize the grid. grid_origin is stored with the width and height it was computed for, and the Grid is drawn only while the buffer has that size; otherwise the panel asks for Rasterise again. This keeps a stale origin from placing cells, without moving the origin into the .cc (the runtime derives alignment from the centre of mass after package-frame and never needs it).

Errors (create-failure-echoes-inputs). Operator reports carry fixed text per failure; AetherDb's own output goes to the system console, not the report.

Ship IDs (ship-id-allows-reserved-names). The rule stays C#'s (ShipAuthoringStore), gains the Windows device names (con, prn, aux, nul, com1-9, lpt1-9, alone or before a dot) and is run by create. New Ship checks it before anything moves through ship_cc.valid_ship_id, whose constants ShipSchemaPinTests pins to the C# rule, as it already pins the slot numbers.

Rejected. Studio owning creation (needs Unity until the Thing host, B3). A local encoder or catalog entry in the add-on derived by hand (B5: Python cannot derive C#'s schema id; the brief forbids it). Relying on cultcache-py's stub catalog entry (B6: the C# reader's behaviour contradicts its own warning). A framework-dependent publish (it needs the .NET runtime installed, which is the install the ruling removes).

Model page rows (changed)

Kind Named by Over time Decides
Ship .cc GameData/Mods/<id>/ship.cc under the author's game data folder created by the first Save of a pending ship; changed by Save, Package, Capture Lines Save (layout), Package (visual), C# rules through the published AetherDb
Layout buffer scene.aetheria_layout filled by Load or New Ship, edited by Rasterise, Flip Nose, Resize and hardpoint ops, undoable the author; persisted only by Save
Grid placement aetheria.grid_origin = [x, y, width, height] on the collection written by Rasterise only, undoable Rasterise
Pending ship aetheria.pending = [name, reference] on the collection set by New Ship, cleared by the Save that creates the file New Ship, then Save

Flight control allocates thrust

Pass: Imagination, session self-2026-10-09-morning. Body: GameCult/Aetheria origin/master 80d2c7f3, detached worktree, removed at the end. Ruling: flight-control-allocates-thrust (operator words in its operator_quote). Prior art: F:\Projects\aetheria-thrust-allocation-prior-art-2026-10-09.md (Eyes; cited as PA). This section supersedes the body facts, consumer audit and Cuts 2-3 of docs/locomotion-cut.md (2026-09-22), which predate the aether drive's deletion (2cfdc728) and the LookDirection-to-Turn change. That file's Cut 4 (AI heading planner) and Cut 5 (combat facing) are carried as follow-ups, not as specs.

Body facts

  • TA1. The plant. 2D translation plus kinematic yaw. Thruster is the only propulsor (Ship.cs:138-139). Per unit throttle a thruster adds -push * Thrust / Mass to velocity along its mount and turns Direction by Torque * Thrust * TorqueMultiplier / Mass rad/s (Thruster.cs:127-130). Torque is the sine of the moment arm (Thruster.cs:65-70). Axis saturates to [0,1] (Thruster.cs:46-50). Authored TorqueFloor 0.5, TorqueMultiplier 0.1 (Settings.asset, read by the probe through AuthoredSettings).

  • TA2. The mixer and its satellites (Ship.cs at 80d2c7f3): bucket sets :33-40, built in Activate :97-116 from mount rotation and from Torque against TorqueFloor; aggregates :51-60; TurnTime :62-67 (no caller); ItemDestroyed pruning :121, :124-134; RecalculateThrust and six Recalculate* :150-227; the mixer :236-259 (strafe trim with an unguarded division by the trim count at :237, :246). Readers outside Ship.cs: the aggregates only in EvasionTermTests.cs:708-709 and :997-1056; TurnTime none; IAnalogBehavior (Behaviors.cs:96-99) only Thruster.cs:40 and a comment at Entity.cs:2343. Thruster.Manoeuvre (Thruster.cs:98-114) is the envelope's per-thruster term and the only other TorqueFloor reader in source (:109); tests read the floor at EvasionTermTests.cs:331-332,383-384 and SteeringTests.cs:304-305.

  • TA3. The Duel ship, probed (scratch test in RunStartTests, Yggdrasil job, CultMath 614fd445, 2026-10-09). Longinus, mass 4235, four thrusters. Per unit throttle (starboard m/s^2, forward m/s^2, clockwise deg/s):

    Thruster Cell, mount Thrust Column
    Large Drive (1,0) Reversed 228441 (0, 53.94, +49.24)
    Large Drive (3,0) Reversed 259158 (0, 61.19, -55.86)
    Talaria (2,14) CounterClockwise 123144 (+29.08, 0, +166.26)
    Talaria (3,14) Clockwise 92312 (-21.80, 0, -124.63)

    The two drives share a design and a lot quality (0.300) and still differ by 13% in live thrust, so held forward thrust is a 6.6 deg/s turn. The two Talarias have equal torque-to-sideways ratios, so together they cannot make a pure torque: any turn on this hull pushes it sideways.

  • TA4. Allocation probe (same job; player.Update stepped alone at 1/60 s; a scratch hook replaced the mixer with a BoundedLeastSquares solve; rows normalised per axis by the box extreme, ridge 1e-3; "prio" is a yaw row weight of 1000; heading in degrees, clockwise positive; axes are the four throttles in the table's order):

    Case mixer WLS w=1 WLS w=3 prio
    forward, intact, 2 s heading -12.09 -6.41 -1.38 0.017
    forward, strong drive off, 2 s 97.41 59.06 14.24 -0.014 (axes .92 0 0 .36)
    forward, weak drive off, 2 s -109.65 -56.22 -11.47 0.026 (axes 0 .90 .29 0)
    forward, nose Talaria (2,14) off, 2 s -12.09 -8.83 -2.97 0.012
    strafe right, 2 s 0 (no strafe at all) 181.39 39.29 0.023 (axes 0 .85 .28 0)
    forward + Turn 1, mean deg/s 159.9 94.2 189.0 214.9 (axes 1 0 1 0)
    Turn 1 in place, mean deg/s 166.1 (axes 0 0 1 0) 97.8 (.57 0 .42 0) 188.9 (1 0 .84 0) 214.9 (1 0 1 0)
    Turn -1 in place -124.6 -81.8 -158.8 -179.6
    Turn 1, Talaria (2,14) off 0 (stuck) 12.4 36.8 48.8 (drive only)
    intent zero 0 0 0 0

    Box extremes: clockwise 215.5 deg/s, counter-clockwise 180.5 deg/s, forward 115.1 m/s^2, starboard 29.1, port 21.8. A yaw-first solve holds heading to hundredths of a degree whatever is lost; a weighted solve at the old map's default (w = 3) leaves a drift the ruling forbids. Solve cost: 14.2 us mean (including per-call allocation, n = 4), at most 11 active-set iterations over 2000 random intents.

  • TA5. The Execute gate drops small balancing throttles. Thruster.Execute and PowerRequest act only above throttle .01 (Thruster.cs:87,124). Probe: weighted solve, full forward, 10 s: heading -7.49 deg with the gate, -4.56 without; forward 0.3, 10 s: -0.213 deg with, -0.028 without. A balancing throttle below .01 on a 166 deg/s Talaria leaves up to 1.7 deg/s unbalanced.

  • TA6. A power-gated column deadlocks. PowerRequest is proportional to the throttle (Thruster.cs:87) and PowerBus.Step grants after Ship.Update (Entity.cs:1463), so a column zeroed for PowerSupply <= 1e-4 gets throttle 0, requests nothing, and is never granted. The column's thrust is therefore EvaluateNominalPower(Thrust) (full grant; heat, durability, quality and modifiers live, Entity.cs:1856) and its liveness is Item.Active only. A browned-out thruster still under-delivers after the solve (follow-up allocator-brownout-columns).

  • TA7. The Duel arena moves the player by itself. With forward thrust the player's velocity swings between about +28 and -28 m/s along the nose every ~60 ticks with no VelocityLimit clamp (limit 100, drag 0.1), while intent zero leaves it at rest. Translation measured in the Duel is not thrust; tests that measure translation use RestoredHullsTests.BuildThrustedShip. Cause not traced (the zone's field at the spawn is the first suspect).

  • TA8. CultMath 0.3.0 re-rolls generated content. Full suite at 80d2c7f3 with only CultMathRevision moved to 614fd445 (tag cultmath-unity-v0.3.0): 1057 pass, 5 fail. TutorialGalaxy_unchanged (faction homes differ), three RestoredHullsTests that pick zones by name (RestoredHullsTests.cs:624,740,807: "EAC-7089", "EAC-2733" no longer exist) and ACoastingAgileShipIsEasierToHitThanTheSameShipJinking (hits 95 jinking against 78 coasting; its target is stage.Generated("Djinni"), EvasionTermTests.cs:520, so the target's fit follows the galaxy). The cause is 0.3.0's breaking snoise kernel (its CHANGELOG), read in C# by Settings.cs:72-73 (galaxy cloud density), GlobalData.cs:30 and Environment.cs:132,144. Shaders use the GPU Noise plugin, not CultMath (Background.shader:39), so nothing on the GPU moves. Every other DemoTerminus test passes. Between the tags CultMath also adds phacelle (MPL-2.0 files), cellular, smin_grad, the *_grad noises and BoundedLeastSquares; no existing signature changes.

  • TA9. The pin guard. Directory.Build.targets:21 conditions VerifyCultLibRevision on a backslash-spelled project identity; docs/mining-cut-refresh.md:155-157 infers it never fires on Linux. Yggdrasil's ~/eureka-verify/pins holds cultlib-45c2f40 and cultlib-6d5e209 only; ~/eureka-verify/repos/CultLib.git contains 614fd445.

Authority map

  • Owner: ThrustAllocator (new, one per Ship, stepped in Ship.Update) turns intent into every thruster's throttle.
  • Inputs: intent (MovementDirection, or full forward while Entity.ThrottleLocked; Turn), and one column per thruster: its body push and turn per unit throttle at full grant, zero when the item is not Active.
  • Outputs: Thruster.Axis for the tick, read only by Execute, PowerRequest and presentation.
  • Derived: the box extremes (what intent is a fraction of) are derived from the columns each tick; the envelope becomes the same extremes (cut envelope-from-columns). The warm start is command state, never truth.
  • Demoted or dead: the bucket sets, aggregates, strafe trim, TurnTime, RemoveThruster pruning and IAnalogBehavior are deleted. TorqueFloor is no longer an owner of anything; it dies with Thruster.Manoeuvre. A thruster's capability is no longer classified by mount or by a floor; it is its column.
  • Forbidden writers: any Axis write outside Ship.Update; any rule that classifies a thruster as "for" rotation or translation; any second solve or fallback mixer; any liveness set kept across ticks.
  • Shared paths: player helm, agents, the throttle lock and tests all reach the throttles only through intent.

Model page rows (changed)

Kind Named by Over time Decides
Thruster column (runtime) the Thruster behaviour rebuilt every tick from live stats and Item.Active ThrustAllocator reads; item state stays EquippedItem's and PowerBus's
Allocator warm start (runtime) the ship's ThrustAllocator last tick's throttles; reset when the thruster count changes ThrustAllocator; never saved
GameplaySettings.TorqueFloor (authored) field name deleted by envelope-from-columns; the Settings.asset line is left for Unity to drop (it lands in AuthoredSettings.Unplaced) nobody after the cut

Rationale

  • Yaw before translation for a hold. The ruling forbids yaw from asymmetry, and TA4 shows a weighted solve cannot deliver that at any weight that also lets a turn trade: holding is exact only when the yaw row dominates. Prior art agrees for spacecraft-like plants: MechJeb weights torque 200 times translation, and the Space Engineers allocation proposal serves rotation authority first (PA A3). Multirotors do the opposite (PX4 and ArduPilot sacrifice yaw, PA A2) because yaw is not their stability axis. Translation first was rejected: it reproduces the mixer's drift. What gives under saturation is translation, so a damaged ship crabs rather than spins; the operator expected exactly that ("crab walking just became optimal", docs/locomotion-cut.md:25-29 at 80d2c7f3).
  • What a full turn spends is the operator's call (question turn-authority), because the operator's 2026-09-22 words on rotation demand (docs/locomotion-cut.md:82-87 at 80d2c7f3) and TA4's numbers pull different ways.
  • One solve with a large yaw weight, not two sequential solves. The large-weight form approximates sequential least squares (PA A1, Schofield) with one call, deterministic and allocation-free; a yaw weight of 1000 against a ridge of 1e-4 keeps the conditioning near 1e5, inside the range the solver documents as exact (BoundedLeastSquares.cs remarks).
  • The ridge makes the optimum unique, so the warm start never changes the answer and two equal thrusters share a demand. It costs 0.01% of full thrust.
  • No actuator interface. One kind of actuator exists. The allocator takes columns and returns throttles, so a vectored row (follow-up vectored-thruster-rows) is more columns: a row tilting within +-theta becomes an axial variable in [0,1] and a lateral variable in [-sin theta, sin theta], and its slew rate narrows the lateral variable's bounds each tick. Bounds stay boxes, which is what the solver takes; the row maps its two solved variables back to a throttle and an angle.
  • The pin bump is its own cut. The solver exists only from 0.3.0, and 0.3.0 re-rolls generated content (TA8). Backporting the solver to a 0.2.5 was rejected: CultLib releases from main, and a branch release would carry the old noise kernel's seam bug forward. no-save-compatibility-before-players covers the re-rolled galaxy.
  • Order. cultmath-0-3-pin, then thrust-allocator-core; throttle-lock lands on the mixer as specified, then thrust-allocator deletes the mixer, keeps throttle-lock's one intent line as the allocator's input and rewrites throttle-lock's two mixer-shaped tests in allocator terms. One owner of Ship.Update's thruster block at every step.

Revision: translation error costs linearly (core r2)

Pass: Imagination, session self-2026-10-09-morning, after Hands stopped on cut-thrust-allocator-core.r1 (question allocator-translation-cost-l2-has-no-onset, ruling allocator-l1-translation-cost, operator ruling turn-authority-full). Probes ran the real CultMath.BoundedLeastSquares (CultLib 614fd445, the pinned 0.3.0) in a scratch console on Yggdrasil, logs l1probe1.log to l1probe3.log in the session scratchpad (imag-allocator-r2). Column sets: TA3 (Duel Longinus); TA3 with the clockwise Talaria zeroed; a symmetric Djinni-like hull (mains (0,50,+-20), fore laterals (+-20,0,+-80), aft laterals (+-20,0,-+60), bow (0,-30,0)); drives 0.3% apart for the tiny-imbalance hold.

  • TA10. A squared translation cost has no onset (Hands' probe, confirmed): r1's TA-R2 mixes the drive into a turn from Turn 0 for any weights, so it cannot meet turn-authority-full.

  • TA11. The split form works only when the solver resolves it. Variables: throttles and four error variables e+x, e-x, e+y, e-y in [0,2]; rows H*(B_r u / s_r - e+ + e-) = H d_r / s_r, the yaw row Wy * B_z u / s_z = Wy d_z / s_z, a cost row we = -wkappa per error variable (linear slope L = 2 w^2 kappa, quadratic part at most e/(2 kappa) of it), a ridge row rho*u per throttle. At the solver's fixed KKT tolerance (1e-6 of the gradient at the origin) every one of 84 settings in two grids failed somewhere: Hands' (H 300, Wy 1, w .01, kappa 2, rho .01) and the first choice (H 100, Wy 10, w .01, kappa 10, rho 1e-3) gave all-zero throttles for move (0,1) and for a strafe, because the cost signal L is below the tolerance against the H^2 and Wy^2 terms; larger L settings served translation but gave throttles that depended on the warm start by up to 0.62 (ties the ridge cannot resolve below the tolerance). The chain the form needs, tolerance << rho^2 << L << H^2, Wy^2, with L/H^2 about 1e-4 for a sharp onset and L/Wy^2 about 1e-6 for the hold, spans more than the 1e6 that tolerance leaves.

  • TA12. With the tolerance at 1e-12 everything holds (l1probe3.log, same grid, scratch change of the constant): warm-start difference 0 in every setting, IterationLimit never reached, at most 10 iterations. Chosen H 100, Wy 1000, w .3, kappa 10, rho .1:

    Check Result
    Duel onset, clockwise / counter-clockwise .772 / .691 (columns: 166.26/215.50 = .7715, 124.63/180.49 = .6905)
    Duel, clockwise Talaria lost drive from Turn > 0, Turn .1 gives drive .0999; Turn 1 gives 49.23 deg/s
    Symmetric hull no translation up to Turn .88 (pure-torque pairs, then mains cancelled by the bow); strafe .95 of the extreme
    Hold, forward, yaw rate (deg/s) intact -.0003, strong drive off .0003, weak drive off -.0002, drives .3% apart .0003
    Forward served, move (0,1) 1.000 of the forward extreme
    Largest throttle step per .001 of Turn over [-1,1] .0133 (the steepest column ratio; Duel's drive slope is .0044)
    Largest yaw residual in the sweeps .0006 deg/s
    cond(A), estimated from A^T A 1.0e4; the solver documents exact results through 3e5

    Neighbouring settings fail one check each: H 30 leaks the drive at Turn .001; w 1 or 3 leaks it at .0014 throttle before the onset (L/H^2 too large); rho .03 makes throttles jump up to .12 per .001 of Turn (near-ties resolved too steeply); Wy 300 lets the hold drift .003 deg/s.

  • TA13. The Duel Longinus does not strafe. Under the linear cost a starboard strafe would need the counter-turning drive at full to hold heading, which costs more forward error than the strafe gains, so move (1,0) gives no throttle. Today's mixer gives none either (TA4). The symmetric hull strafes.

Rationale changes:

  • The solve keeps one call; the tolerance is the solver owner's to expose. CultMath's BoundedLeastSquares.Solve gets an optional relative KKT tolerance (default unchanged, 1e-6), filled in CultLib, never worked around in Aetheria; the allocator passes 1e-12 and relies on the solver's progress rule to stop at its noise floor. Follow-up cultmath-bls-kkt-tolerance carries it; Aetheria takes it by a pin bump in the core cut's first commit.
  • The onset is an ordering, not a label. Under a linear cost the actuator with the least normalised translation per unit of turn serves first and the next joins only when it saturates; on the Duel that is the Talaria, so the onset is the Talaria's share of the turn extreme and moves with the columns (damage, heat, brownout). Nothing classifies a thruster as attitude or drive.
  • The bullets "One solve with a large yaw weight" and "The ridge" above describe r1 and are superseded by this revision: the yaw weight is 1000 against a ridge of .1, and uniqueness of the answer depends on the tolerance as well as the ridge.

Spec refresh 2026-10-09: scarce consumables, the fog bank, multiply, tiered boosters, the shipped AetherDb

Pass: Imagination imagination-spec-refresh, session self-2026-10-09-morning, follow-up rulings-2026-10-09-spec-refresh. Body: GameCult/Aetheria origin/master 439d488a (and 07dcb580, the frame-r4 branch, for the authoring tool), detached worktree C:\aeth-wt\imag-refresh, removed at the end. Rulings applied (read their operator_quote in the mind): consumables-scarce, vapour-cloud-obscuration, boost-stacking-multiply, boost-strength-tiered-by-size, components-are-roles-for-demo, published-aetherdb-with-game, overdrive-keep-steering; composed with flight-control-allocates-thrust and turn-authority-full (section 'Flight control allocates thrust' above). No question is raised: every choice below is a project default or authored content that follow-up combat-pace-pass tunes.

Body facts

  • RF1. The cloud patch layer (scene and source read). ARPG.unity holds a camera named Patch (GameObject 559840914, local y 10 under the fog camera parent, culling mask 2048 = layer 11 Patch in ProjectSettings/TagManager.asset) whose target is Assets/Resources/Fog Patch.renderTexture (guid c618a6b2...). VolumeSampling.cs:69 publishes it as _NebulaPatch; Volumetric.cginc:95-98 reads it as the patch's vertical thickness: saturate((-abs(y + disp - FloorOffset) + patch) / PatchBlend) * PatchDensity. The one environment is Settings.DefaultEnvironment (ActionGameManager.cs:216-222): PatchDensity .35, FloorOffset -20, PatchBlend 25 (Assets/Resources/Settings.asset:389-392). What paints the layer today: two quads under SectorBrushes, Depth Micro and Depth Macro, with Sector Patch Depth Small/Large.mat on Brushes/Simplex Brush (additive Blend One One, world-space noise, _Depth 10 and 20). A fog-bank brush already exists and nothing references it: Assets/Prefabs/RPG/Fog Patch.prefab (layer 11, a quad rotated to face up, Fog Brush.mat on Brushes/Power Brush, _Depth 80, _Power 16, _Cutoff 1; prefab guid 4084aa3d..., grep of every scene, prefab and asset: no reference). A quad of side 2R carries the brush's pulse out to radius R (PowerBrush.shader: dist = length(uv - .5) * 2).
  • RF2. Drawing a zone body ahead. DrawAhead.cs:15-18 draws an entity at Position + Velocity * Lead; KinematicBody.TotalVelocity (FloatingBodies.cs:31-32) is commented "What a presenter draws ahead with". MineInstance.Place draws the latest body with no lead. The mine presenter's hooks at 439d488a: ZoneRenderer.cs:47 (MinePrefab), :97 (_mines), :227-235 (load and subscribe), :259-264 (LoadMine), :289-291 (ClearZone).
  • RF3. Generation draws (source read). LoadoutGenerator holds its own copy of the caller's Random (LoadoutGenerator.cs:27), and the wanderer generator is reused across ships (ZoneGenerator.cs:378), so one extra draw in GenerateShipLoadout shifts every later wanderer. ItemManager.CreateInstance rolls lot quality on ItemManager.Random (ItemManager.cs:123), so every extra instance shifts later lots. The catalog at 439d488a holds no consumable record (CC1), so a consumable step that draws only when a consumable is on offer leaves every current golden untouched.
  • RF4. Death drops carry. Today EntityInstance.cs:296-316 drops each cargo item on death; cut loot-1 r2 moves this into Zone.DropLoot ("every cargo stack drops"). A consumable an AI ship carries is therefore loot when it dies, through the owner that already decides loot.
  • RF5. Multiply is the rule and nothing pins it. StatResolver.ScaleModifier (StatResolver.cs:108-114) multiplies every attached scale; ConstantModifier (:116-122) sums. StatResolverTests.cs (455 lines) has no test with two scales on one stat. No spec in force names modifier-stacking in depends_on (specs-with-no-report recipe, 2026-10-09).
  • RF6. No player build entry point exists. No BuildPipeline, BuildPlayerOptions or IPostprocessBuild anywhere in Assets/Scripts or Assets/Editor (which holds only Epiphany/EpiphanyEditorBridge.cs). Ruling adopt-build-cut-1 (2026-10-03) ordered docs/build-delivery-cut.md Cut 1 mapped as one cut; none was admitted, and ships-player r2 adds its Mods staging to "the committed entry point". The game reads <dataPath>/../GameData (ActionGameManager.cs:37-41, :48-49). GameData at 439d488a holds Aetheria.cc, Narrative/ (read by MainMenu.cs:183) and SoundbanksInfo.json, which no C# file reads (grep) and which audio-1 retires with Wwise. Addressables: m_BuildAddressablesWithPlayerBuild: 0 (AddressableAssetSettings.asset:61, PreferencesValue), so whether content builds with the player is a per-machine preference today. Build-delivery 0.2 (probe, 2026-09-17) found a read-only AetheriaStores.Open of a missing catalog silent; not re-probed at 439d488a (AetheriaStores.cs:16-31 is unchanged in kind), so its test is written first and must fail first.
  • RF7. The install's tool folder cannot be Tools. The repo has tools/; Windows paths are case-insensitive, so a developer publishing AetherDb into <repo>/Tools would write into tools/. The install folder is ModTools/, gitignored at the repo root.
  • RF8. The add-on anchors at 07dcb580 are r1's (__init__.py:29 timeout, :302-311 preference, :314-325 _aetheria_repo, :328-336 _aetherdb, :576-577 New Ship's target; AetherDb.cs:26-32 FindRoot, identical on 439d488a).

Model page rows (changed)

Kind Named by Over time Decides
Consumable stock (runtime, station cargo) the station rolled once at generation: a station stocks with chance StationConsumableChance, then one product, 1 to StationConsumableUnits units LoadoutGenerator, through AvailableProducts
Carried consumable (runtime, ship cargo) the ship rolled once at generation with chance ShipConsumableChance, one unit; spent by consumable-ai or dropped as loot on death LoadoutGenerator; then the consumable host or Zone.DropLoot
Fog bank (presentation) its VapourCloud one patch brush per cloud, depth = authored depth x the cloud's opacity fraction, destroyed when the cloud leaves Zone.Clouds ZoneRenderer's one subscription; writes nothing to the sim
Player directory the build output root made by one AetheriaBuild run: player, GameData/Aetheria.cc, GameData/Narrative/**, GameData/Mods/**, ModTools/AetherDb.exe AetheriaBuild alone
Ship author's game folder the add-on's one preference the folder whose GameData holds Aetheria.cc; ModTools/AetherDb is found under it the author

Design tables

Stat boosters, tiered by size (ruling boost-strength-tiered-by-size). Shape and template per CB-R2 and the r1 table: Tool gear, Targeting Computer c07ea205's EnergyDraw and Heat, then a StatModifierData Multiplier in the same group; roles amplifier (magnitude) and regulator (EnergyDraw, Heat). The 1x1 keeps the template's EnergyDraw, Heat, Mass and Price; the 2x2 takes 3x EnergyDraw and Heat and 4x Mass and Price. Magnitude Min-Max (rises with amplifier quality): 1x1 1.03-1.08, 2x2 1.20-1.40. Both designs on one stat multiply (ruling boost-stacking-multiply), so a 1x1 and a 2x2 at best quality give 1.512.

Stat 1x1 design: products 2x2 design: products Cut
ThrusterData.Thrust Thrust Amplifier: Tailwind (Finch), Second Wind (Lightsail) Thrust Amplifier Array: Jetstream (Finch), Trade Wind (Lightsail) boost-gear-systems
SensorData.Sensitivity Signal Amplifier: Earful (Finch), Lookout (AU) Signal Amplifier Array: Big Ears (Finch), Watchtower (AU) boost-gear-systems
RadiatorData.Emissivity Emissivity Booster: Cold Read (Adrasteia), Cool Head (Lightsail) Emissivity Booster Array: Dead Calm (Adrasteia), Cold Storage (Lightsail) boost-gear-systems
ReactorData.Charge Reactor Overclock: Afterhours (NiteLife), redline (DME) Reactor Overclock Array: Last Call (NiteLife), meltdown (DME) boost-gear-systems
WeaponData.Damage Damage Amplifier: Showstopper (Lucent), Heavy Hand (Zhestokost) Damage Amplifier Array: Encore (Lucent), Iron Fist (Zhestokost) boost-gear-combat
WeaponData.Range Range Extender: Far Sight (Adrasteia), Long Look (Finch) Range Extender Array: Horizon Line (Adrasteia), Far Flight (Finch) boost-gear-combat
ShieldData.Capacity Shield Booster: Thick Skin (AU), SB-2 Bastion (R&D) Shield Booster Array: Hard Shell (AU), SB-8 Rampart (R&D) boost-gear-combat

Both boost cuts are held behind follow-up role-targeted-adjacent-boosts: if the operator rules that boosters address a role (and adjacency), the Target column changes and these specs get another revision.

The first consumables, strengthened (ruling consumables-scarce: rare, stronger). Changes from the 'Consumables' tables above; everything not named is unchanged.

Design Field Was Now
Thruster Overdrive Duration 5 s 6 s
Price 6,000 18,000
Thrust (propellant) 2.0-3.0 3.0-4.5
TopSpeed (propellant) 1.5-2.0 2.0-3.0
Thruster Heat, Visibility (regulator, falling) 4.0-2.5 5.0-3.0
Coolant Vent Duration 12 s 15 s
Price 4,000 12,000
Radius Min half the Djinni diagonal x 1.25 x 1.5 (Max stays 1.5x Min)
Opacity (nozzle) .80-.95 .90-.98
Lifetime (coolant) 10-14 s 14-20 s
Radiator Emissivity penalty (coolant) .4-.6 .3-.5

Supply, scarce. StationConsumableChance .25, StationConsumableUnits 2 (one product, 1-2 units), ShipConsumableChance .10, ShipConsumableUnits 1. Named constants in LoadoutGenerator beside the gear stock's literal 16, as r1 had them.

Rationale

Scarcity through the owners that exist. "Rare loot and a few stations" needs no loot table: a ship that carries a consumable drops it on death through the one loot owner (RF4), and only a quarter of stations stock any. The same roll makes AI use rare, so when the player meets a vent or an overdrive in an AI's hands it is an event, and killing that ship before it fires is a reason to loot it. Stronger effects at a higher price follow the ruling; the hoarding risk is the operator's, accepted. The consumable step draws nothing when no consumable is on offer (RF3), so this cut re-rolls no current fixture; once consumables-first-set lands the generated galaxy re-rolls once, which no-save-compatibility-before-players covers.

The fog bank is a patch brush. The operator's words in vapour-cloud-obscuration: painting into the patch layer is the presenter. The volumetric pass already turns patch thickness into fog the ship flies inside (RF1), so a cloud is one more additive brush on layer Patch, and Fog Patch.prefab is that brush, already authored. Its depth is the authored depth times OpacityAt / Opacity, so the fog thins exactly as the sim's obscuration does and is gone when the cloud is removed (opacity reaches 0 at Lifetime). r1's particle system is dropped: a second renderer for a thing the nebula already draws.

Multiply needs a test, not code. The ruling keeps today's arithmetic (RF5), so modifier-stacking shrinks to the tests that pin it, gear with gear and consumable with gear. A rule nobody pins is a rule a later "balance" patch removes silently.

The boost tiers. A strong 2x2 and a modest 1x1 per stat lets cell budget, not a stacking curve, bound a ship: four cells buy x1.20-1.40 from one array or x1.13-1.36 from four 1x1s (1.03^4 to 1.08^4) at 4x the power and heat, so at equal quality the array is the better use of cells and power and a hold full of smalls is the expensive fit. Under multiply nothing else bounds stacking, so these two ranges are the bound; combat-pace-pass tunes them.

How the boosts and the overdrive compose with flight control. A Thrust multiplier (Thrust Amplifier, Thruster Overdrive) changes only the resolved ThrusterData.Thrust. From thrust-allocator on, the allocator's columns are the thrusters' EvaluateNominalPower(Thrust) each tick (TA6), so a boosted drive is a stronger column and the yaw-first solve still holds heading when one drive is boosted and its twin is not; the evasion envelope reads the same columns (envelope-from-columns). The overdrive's lock is intent (throttle-lock), not throttles: under the allocator "throttle at max" means full forward intent, and a drive may be trimmed below 1 to hold heading. So no booster or consumable spec writes an Axis, touches Ship.Update or Thruster.cs, or asserts per-thruster throttles; their tests observe resolved stats, ThrottleLocked and motion, which hold before and after the allocator lands.

The shipped tool and the player build are two owners, in order. ships-authoring-tool owns where the add-on finds AetherDb (the game folder's ModTools/) and what AetherDb treats as its root (a folder whose GameData holds Aetheria.cc). player-build owns what a player directory contains, the published AetherDb included. A self-contained single-file publish was probed at 77 MB for linux-x64 (B7); the Windows player carries the win-x64 one. One preference replaces r1's two: the tool's path is derived from the game folder, so the two cannot disagree. ships-player r3 drops its own Mods staging line and builds through player-build, so one cut decides the staging list.

Component records are owed after the demo cut (ruling components-are-roles-for-demo); follow-up component-records-after-demo carries the mapping so it outlives this refresh.

Admitted

Receipts mind-commit-d41d568c..., mind-commit-dc5c5a2a..., mind-commit-c2edfe6d... (2026-10-09 13:24-13:28 UTC). Revisions, each with the resolution superseding its predecessor: consumable-supply r2, vapour-cloud-presenter r2, modifier-stacking r3 (shrunk to tests), consumables-first-set r2 (not named in the follow-up; reshaped by consumables-scarce), boost-gear-systems r2 and boost-gear-combat r2 (held), ships-authoring-tool r2, ships-player r3. New: cut player-build r1, follow-up component-records-after-demo. No question raised.