# Aetheria Codex assimilation: refreshed cut maps (mining, moddable ships) Date 2026-09-29. Imagination output for Self. It re-anchors two Codex lanes to the Body as it will stand after `codex/fire-control-12` merges to `master`. The Codex maps are raw material, not authority. Where this file and a Codex map disagree, this file is the proposal and the Codex map is the input. ## How this was established - **Branch tips read, 2026-09-29, after `git fetch`.** - `origin/master` `9b85211f` - `origin/codex/fire-control-12` `b66ba524` - `origin/codex/mining` `283ce7dc` - `origin/codex/moddable-ships` `375d6bd4` - Nothing in `F:\Projects\Aetheria`'s working tree was read or switched. Every read is `git show` or `git grep` against a SHA. - **The post-fire-control Body is a synthetic merge.** `git merge-tree --write-tree origin/master origin/codex/fire-control-12` is clean, tree `91576243`. I wrapped it in an unreferenced local commit, `8a2db490` (no ref, no push to origin), so it can be built. The fire-control checklist (`docs/merge-to-master-checklist.md:7,107` on `b66ba524`) predicts the same clean merge. - **Anchors.** Each `file:line` names a real SHA, never the synthetic one: - `b66ba524` for files only fire control touched; - `283ce7dc` for files the mining branch touched; - `375d6bd4` for files the ships branch touched. The three file sets are disjoint. I checked with `git diff --name-only` over each branch's own range, and none of the three pairs shares a file. So each anchored line is byte-identical in the merged tree. - **Tests ran on Yggdrasil, against synthetic merges** (section 0). CultLib `45c2f40` and CultMath `6d5e209` are the pins in `Directory.Build.props`. The two LFS files the tests read (`GameData/Aetheria.cc`, `Assets/Resources/Settings.asset`) were staged beside the job, because the verify mirror carries no LFS. - **Catalog facts** come from a probe that opens the merged tree's `GameData/Aetheria.cc` through `AetheriaStores.Open` (section 0). ## 0. Probe results **Test suite on the synthetic merges**, Yggdrasil, `dotnet test tests/Aetheria.Shared.Tests`, CultLib `45c2f40`, CultMath `6d5e209`, the `dotnet` image: | Tree | Commit (local, unreferenced) | Result | |---|---|---| | master + fire-control-12 + mining | `396d19e5` | **379 passed, 0 failed** | | master + fire-control-12 + ships | `0036204f` | **364 passed, 0 failed** | | master + fire-control-12 + mining + ships | `474c35c3` | **381 passed, 0 failed** | Fire control closed 12.4 at 362 tests. 379 = 362 + 17 mining tests, 364 = 362 + 2 ship tests, and 381 is both together. Nothing on either branch breaks a fire-control test, and nothing in fire control breaks a branch test. This proves the headless simulation only. No Unity compile ran. **Catalog probe** (`scratchpad/probe/Program.cs`: it opens `GameData/Aetheria.cc` through `AetheriaStores.Open` in the merged tree; the catalog oid `2aa79c0a` is the same in all three merges and at `b66ba524`): - 124 items: 32 `GearData`, 18 `WeaponItemData`, 13 `SimpleCommodityData`, 5 `HullData`, 51 compound commodities, 4 cargo bays, 1 docking bay. - **Weapon damage types:** Kinetic 9, Electric 3, Optical 3, Thermal 3. There is none of Corrosive or Ionizing, though the `DamageType` enum has all six. The mining map said Kinetic 10. - **Penetration > 0:** only Autocannon, LRMM72 and SRMM72, each at 0.25. - **DamageSpread > 0:** GT 3K 3.25, plight 3.3, pswarm 1.5, scorched void policy 1.5, and SRMM72 0.5. The map listed four and missed SRMM72. - **No weapon has `BlastRadius` or `Fuse`.** Blasts are dormant in shipped content until F12-2 authors them. - **Simple commodities:** the 13 the map named. Every one is priced 0 **except Ammo, at 1000**. The map's "every live simple commodity has Price 0" is off by one. - **No live item carries a scanner or mining behaviour**, on fire control or on mining merged. - **Pre-existing, on `b66ba524` as well as on the merges:** four `GearData` records (Refinery, Deep Ore Extractor, Assembly Line, Shipyard) hold a **null element** in `Behaviors` (index 0 of 1). It does not come from mining's union-26 retirement: the same four are null on fire control alone. This matters to variants C4 (A.6). - **Shipped hull mount ids are not unique:** | Hull | Hardpoints | Distinct `Transform`s | Empty `Transform`s | |---|---|---|---| | Djinni | 22 | 20 | 3 | | Longinus | 13 | 12 | 2 | | LonginusX | 10 | 9 | 2 | | Turret | 7 | 3 | 5 | | Zenith | 5 | 1 | 5 | All five hulls use `Prefab`. This matters to ship migration (S5). ## Shared precondition for both campaigns Both campaigns wait on one event: `codex/fire-control-12` merging into `master` with `--no-ff`, which Self does after the operator signs off (`docs/merge-to-master-checklist.md:105-113` on `b66ba524`). Until then, neither campaign has a base to merge onto except the fire-control branch itself. After it: ``` git switch codex/mining && git merge --no-ff master # in a worktree, never the operator's tree git switch codex/moddable-ships && git merge --no-ff master ``` **Merge, do not rebase.** Both maps and their Soul receipts cite landed SHAs: mining cites 11 of its 13 commits, and the ships doc cites its commits' results. A rebase rewrites every one of those SHAs, which strands the maps' evidence. The merge has no conflicts (below), so it costs one merge commit per branch. --- ## Rulings (operator, 2026-09-30) - **MQ1: a ship `.cc` is self-contained.** Operator: "The idea is for the new moddable ships to eventually take over from the existing system. A ship .cc should contain everything needed to instantiate and render a ship." The confirmed reading: the hull is its own `HullData` record and the visual is its own record, both in the one ship `.cc`, side by side. The hull is not embedded in the visual. A hull family such as Longinus and LonginusX lives together in one `.cc`, so variants resolve within one store. S1 follows option B, with both records inside the ship's own store. - **MQ2 A for now.** There are no cross-file variants of shipped hulls in this lane. **Operator note, to be carried into CultLib's variants target:** "we'll want cross-store variant support eventually; modders should be able to add variants of existing items without modifying the original". - **MQ3 A.** The FBX prefab builder is scaffolding. The typed `.cc` lane replaces the existing system, and the builder dies at S5. - **MQ4 A.** Continue refuses loudly and names the missing mod ids. - **MQ5 A.** The game composes the modded catalog at boot, from `GameData/Mods`, into a disposable file. - **Soul (2026-09-30) on `375d6bd4`** found the Blender↔C# `.cc` round trip real, with no vendored codec. Before any cut builds on it, the S-Soul fix slot must fix: - F1: 8 of 9 validator mutations survive, including the `..` path guard and the prefab refusal. - F2: `Compose`, `ReadPackage` and `ReadNodeIds` are untested. - F3: there are no Python tests; slot numbers and enums are duplicated by position. - F6: `CatalogTypes` includes `ShipAuthoring`. Check name collisions. - F7: a drive-by `using` removal. - F5 is settled by MQ1. - **Pivots own firing arcs (operator, 2026-09-30, from fire control).** A turret's arc belongs to its pivot, not its hardpoints. When this campaign adds pivots to `HullData`, the arc moves from the hardpoint to the pivot, and the interim "intersection of hardpoint arcs per pivot" rule in fire control is deleted. Unity's `Pivot......` node-name parsing (`ShipPrefabAuthoring.cs:144`) must not survive as a second arc authority. # Part B. Moddable ships ## B.1 Status header (verified against git) Branch `codex/moddable-ships`, tip `375d6bd4`: 4 commits on `4b594e11`. `4b594e11` is an ancestor of fire control, which has since moved 8 commits to `b66ba524`. Neither side touches the other's files. | SHA | What landed | |---|---| | `42eb5214` | `ShipAuthoring` record and validator; `AetherDb ship-authoring create/inspect/validate`; Blender LineArt capture; 2 xunit tests | | `4ba4bbaf` | `ShipModCatalog.Compose` (derived catalog) and GLB node-id reader | | `24c29640` | Unity `ShipModVisual` (glTFast runtime import, line mesh) and Editor preview; `com.unity.cloud.gltfast` 6.20.0 added to `Packages/manifest.json` | | `375d6bd4` | Blender layout editing (hull grid, hardpoints) through `ship_cc.py` | The delta is +511 C# (including `tools/AetherDb`), +494 Python, +119 tests, and one new Unity package dependency. It also carries a stray fix in `Assets/Scripts/UI/Menu/TradeMenu.cs` (an unused `using System.Runtime.Remoting.Contexts`). Self's ruling: it stays on this branch as cleanup. Master compiled clean in Unity with the using present, so master does not need it. **Test counts.** The two ship tests pass on the merged tree: 364/364 (section 0). **S-Soul status:** F1, F2, F3 and F6 fixed at `fe38f288` (479 tests). F4 has no spec entry. The Python tests (`tools/blender/tests/test_ship_cc.py`, 32) have no runner yet; run them by hand. **S0 status (2026-09-30):** master `65c63495` (fire control 12, `dcd7bbc5`) merged into `codex/moddable-ships` with `--no-ff` at `18e12d81`, no conflicts. Corrections 1-4 applied to `docs/moddable-ship-authoring.md`. Yggdrasil full suite on the merge (`dotnet test tests/Aetheria.Shared.Tests`, CultLib `45c2f40`, CultMath `6d5e209`): **615 passed, 0 failed**. Self's steps, 2026-09-30 05:15 CEST: the Unity 6000.3.24f1 batchmode compile of `01e9db1c` has 0 `error CS` and 0 `warning CS`, and `ShipModPreview.Smoke` passed (`SHIP_MOD_VISUAL_SMOKE anchors=5 points=2 segments=1`, its first recorded run). **The branch has no committed fixture package:** the smoke ran against one generated from `ShipAuthoringTests.Fixture()` with a hand-added glTF `scene` (the test GLB has nodes but no scene; whether glTFast instantiates a scene-less GLB is unchecked). S1 should commit a scene-bearing fixture package so the smoke is reproducible. **Anchors re-taken against the merged tree (`18e12d81`).** - Unchanged: `ShipAuthoring.cs:15` (`Hull`, key 1), `:84`, `:111-136` (the hardpoint checks are `:112-131`), `ShipModCatalog.cs:20-21` (keys), `ItemData.cs:512` (`Hardpoints` `Key(23)`), `:515` (`Prefab` `Key(24)`), `Loadout.cs:74`, `EntityInstance.cs:15-27`, `ZoneRenderer.cs:289-310` (`LoadEntity`), `AetheriaStores.cs:9`. - Moved: the second upsert in `ShipModCatalog.Compose` is `:68-69` (was `:60-61`; the F1-F3 fix added the name-collision checks above it). `ship_cc.py`: slot constants are `:23-25`, `replace_layout` is `:96-124`, the `body[HULL_SLOT]` reads are `:84,101`. `ActionGameManager.cs`: the catalog opens at `:55-59` (`AetheriaStores.Open`, not `:45-60`); `capturepreset` is `:621-662`. - S1's key 32 for `HullData.Visual` holds: `EquippableItemData` owns 30 and 31, `HullData` uses 23-29, and `WeaponItemData.Fuse` at 32 (`ItemData.cs:505`) is a sibling class, not an ancestor. **Proof-gate status against the doc's own gates (`docs/moddable-ship-authoring.md:149-164` @`375d6bd4`):** | Gate | Doc implies | Verified in git | |---|---|---| | 1. C#/Python round trip preserving unknown fields | met | **C# only.** `StandaloneShipRoundTripsWithoutTouchingTheCatalog` pins C#. The Python half and "preserving unknown future fields" rest on the Quiet probe, whose output `Build/quiet.cc` is git-ignored. No repository test exercises `ship_cc.py`. | | 2. Blender edits cells and hardpoints | met, except node-id authoring | **Code exists, unpinned.** `ship_cc.replace_layout` (`ship_cc.py:75-106`) has no test. | | 3. Validator rejects malformed records, changes nothing on failure | met, and compose checks it | **Validator pinned.** `InvalidSchematicOrDanglingMountCannotBePublished` covers 5 cases. **Compose unpinned:** no test covers key collision, the atomic replace, a missing GLB node, or "the previous derived file is unchanged on failure" (`ShipModCatalog.cs:23-73`). | | 4. Loads in a built player | outstanding | not started | | 5. Play checks | outstanding | not started | **Corrections to `docs/moddable-ship-authoring.md` at `375d6bd4`:** 1. **`:9-16` and `:132-136` report the Quiet probe's numbers as facts.** They include 1,040 polylines and "206 shipped records became 208". Nothing in git reproduces them. Label them as an unrecorded probe, or commit a fixture that pins them. 2. **The composed catalog holds two copies of every mod hull.** - `:114-118` says compose preserves one owner per ship. - `ShipModCatalog.cs:60-61` upserts `ship.Hull` as a `HullData` at `mod-hull:`, **and** the whole `ShipAuthoring`, whose key 1 is the same `HullData`, at `mod-ship:`. - Gameplay can read only the first. The second is a dead copy of the same truth inside a derived file. Q1 decides the fix. 3. **`AetheriaStores.CatalogTypes` now routes `ShipAuthoring` to the shipped catalog** (`AetheriaStores.cs:9` @`375d6bd4`), while the doc says the shipped catalog "contains no copy of that ship". Routing a type is not holding a record, so this is harmless today. It stays correct only while nothing writes a `ShipAuthoring` through a catalog-writable cache on `Aetheria.cc`. Name that invariant in the doc. 4. **`:98-106` "the same semantic validator" on every path.** The Blender side validates nothing semantic: it checks grid bounds and footprint shape (`ship_cc.py:83-97`). "Shared path" is true for the C# callers only. ## B.2 Merge path onto the fire-control-12 lineage | Probe | Result | |---|---| | `git merge-tree --write-tree origin/codex/fire-control-12 origin/codex/moddable-ships` | clean, tree `97711bcd` | | `git merge-tree --write-tree 8a2db490 origin/codex/moddable-ships` (master+fc12) | clean, tree `88672977` | | on top of mining merged into master+fc12 | clean, tree `a7410174` | | files shared with fire control `4b594e11..b66ba524` (6 files: `Entity.cs`, `FireControl.cs`, two test files, two docs) | none | The merged tree passes the full headless suite: 364/364 alone, and 381/381 with mining (section 0). The only cross-lane seam is `Packages/manifest.json` (glTFast). It merges textually, but it needs Unity to resolve the package on first open, like the CultMath tag already does (`docs/merge-to-master-checklist.md:14`). ## B.3 Ordered cuts | Cut | Nature | Blocked by | |---|---|---| | S0 | Placement: merge master, correct the doc | fire-control-12 on master | | S-Soul | Fix batch from the parallel Soul pass | Soul's report | | S1 | One hull record, and a hull names its visual | MQ1 | | S2 | The game composes and loads mod ships (`ShipInstance` wiring, boot preload) | S1; MQ4, MQ5 | | S3 | Gate 4: a built player loads a mod ship | S2; a playable mod ship (content) | | S4 | Gate 5: play checks | S3 | | S5 | Migration of shipped hulls, onto variants; FBX builder retirement | S4; MQ2, MQ3; variants C4 | ### S0. Placement and doc correction - **Repo/branch:** Aetheria, `codex/moddable-ships`, in a worktree. - **Deletes first:** none. - **Keeps/moves/adds:** `git merge --no-ff master`. Apply corrections 1-4 to the doc. - **Verification:** - Yggdrasil full suite on the merge (the M0 command). - Unity batchmode compile of the merged tree with glTFast resolved. This is Self's step, and it needs network for the package. - The Editor smoke already in the branch: `-executeMethod ShipModPreview.Smoke -shipModPath ` (`ShipModPreview.cs:8` @`375d6bd4`), run against a committed fixture package. It is the first recorded run of that smoke. - **Subtraction:** docs only. ### S-Soul. Fix slot for the parallel Soul pass - **Repo/branch:** `codex/moddable-ships`, after S0. - **Content:** Soul's findings. Two areas this map expects Soul to own, and does not duplicate: - Compose's unpinned failure paths (B.1, gate 3). - The hand-copied schema on the Python side: - `ship_cc.py:17-21` slot constants (`HULL_SLOT 1`, `HULL_SHAPE_SLOT 5`, `HULL_HARDPOINTS_SLOT 23`); - `ship_cc.py:88-98` hardpoint slots 0-6; - `__init__.py:18-21`, a copy of the `HardpointType` and `ItemRotation` enums. All of these match C# today: `ItemData.cs:287` `Shape` is `Key(5)`, `HullData.Hardpoints` is `Key(23)` at `:512`, and `HardpointData` is `:553-563`, all @`b66ba524`. `Enums.cs:38` matches too. The hazard is drift: fire control added slot 6 (`FiringArc`) to `HardpointData` in this same lineage. The memory `honor-invariants-over-workarounds` names hand-copied schemas as a known failure. The coherent fix reads the published schema catalog. Soul decides whether it is in scope. ### S1. One hull record, and a hull names its visual (written for MQ1 = B) - **Repo/branch:** `codex/moddable-ships`, after S-Soul. - **Deletes first:** - `ShipAuthoring.Hull` (`ShipAuthoring.cs:15`, key 1) becomes a retired-key comment. - The embedded-hull branch of the validator (`ShipAuthoring.cs:84-133`) moves; it does not duplicate. - The second upsert in `ShipModCatalog.Compose` (`ShipModCatalog.cs:60-61`) becomes a verbatim copy of the mod store's records. - `ship_cc.py`'s `body[HULL_SLOT]` indirection (`:63-66`, `:80-100`). - The only existing `.cc` with the old shape is the git-ignored `Build/quiet.cc` probe. No committed content migrates. - **Adds:** - A mod `.cc` holds two records: a `HullData` at the deterministic key `mod-hull:` (`ShipModCatalog.cs:20`) and the `ShipAuthoring` visual package (`Id`, `ModelAsset`, `Anchors`, `SchematicLines`). - `HullData` gains a nullable `CultRecordRef Visual` at the next free key on its ancestry. `EquippableItemData` owns keys up to 31 (the comment at `ItemData.cs` `WeaponItemData.Fuse`), so it is 32, unless C4 claims that slot first. - The validator refuses a hull with both `Prefab` and `Visual` set, or neither, when it is a ship. - The validator is one function over (hull, visual). It checks every `Hardpoints[].Transform` against the visual's anchor ids, the checks that are at `ShipAuthoring.cs:111-136` today. - **Authority map:** - Owner: the `HullData` record owns hull semantics (cells, hardpoints, stats). The `ShipAuthoring` record owns the visual package (model path, anchor-to-node map, lines). - Inputs: the mod `.cc`, and the GLB node extras. - Outputs: records copied verbatim into the derived catalog. - Demotion: `ShipAuthoring` is no longer an owner of hull data. `mod-ship:` in the derived catalog is the same record as in the mod store. - Forbidden writers: compose transforming records; Blender writing hull slots inside `ShipAuthoring`; any lookup from hull to visual by key-prefix convention. The typed `Visual` ref is the only binding. - Shared paths: `ship-authoring validate`, compose, the Editor preview and the runtime loader all call the one `(hull, visual)` validator. - Deletion line: after S1, `grep -n "\.Hull\b" ShipAuthoring.cs ShipModCatalog.cs` finds no embedded hull. - **Verification** (Yggdrasil, `--filter FullyQualifiedName~ShipAuthoring`, then the full suite): - `ComposedCatalogHoldsOneHullPerModShip`: the negative for correction 2. It fails today. - `HullNamesExactlyOneVisual`: Prefab xor Visual. - `HardpointsMustResolveToTheVisualsAnchors`: moved from the old validator. - `ComposeRefusesAKeyCollisionAndLeavesThePreviousFile` and `ComposeRefusesAMissingGlbNode`: the gate 3 negatives, unless S-Soul already added them. - `AHullVariantInheritsItsVisual`: runs only once Aetheria is on variants C4. It is the reason for B, and it is recorded as pending until then. - A Python check, in the Blender-free `python:3.12-slim` image with `pip install msgpack -e /packages/cultcache-py`: `ship_cc.replace_layout` on a C#-written fixture preserves every untouched slot, then C# reopens it and it validates. This pins gate 1's Python half for the first time. - Operator: in Blender, load a layout, change a mount, save, then `ship-authoring validate`. - **Subtraction estimate:** about 40 removed (the embedded-hull paths and the duplicate upsert), about 35 added (`Visual`, the joint validator's plumbing), and about 25 in the Python path. ### S2. The game composes and loads mod ships - **Repo/branch:** `codex/moddable-ships`, after S1. - **Deletes first:** none. This cut is additive: it is the construction boundary the doc names at `:141-145`. - **Adds, per MQ4 and MQ5:** - **Compose at boot** (MQ5 = A): `ActionGameManager` opens its catalog at `ActionGameManager.cs:45-60` (`b66ba524`). When `GameData/Mods` holds packages, it opens the derived file instead, composed at boot into the persistent data path. `capturepreset` keeps writing the shipped catalog (`:605-620`, and the `Loadout.cs:74` writable open). - **Boot preload:** after the catalog opens and before the first zone loads, every hull with `Visual` set imports its GLB through `ShipModVisual.LoadAsync` into an inactive prototype keyed by hull key. `LoadEntity` then instantiates synchronously, as it does for prefabs. - **Assembly:** a `ShipInstance` template prefab (Addressable) carries the shared effect prototypes: shield, tractor, thruster particles, radiator material. The loader parents the GLB under it and fills `EntityInstance`'s arrays (`EntityInstance.cs:15-27` @`b66ba524`: `MapIcon`, `Shield`, `HullColliders`, `EquipmentHardpoints`, `RadiatorHardpoints`, `ThrusterHardpoints`, `WeaponHardpoints`, `ArticulationPoints`, and `ShipInstance.TractorBeam`) from anchors by role. The name matches that `EntityInstance.cs:245,260,271` and `ShipInstance.cs:69-70` do against `hp.Transform` keep working, because anchor ids are the mount ids. - **`ZoneRenderer.LoadEntity`** (`ZoneRenderer.cs:289-310` @`b66ba524`) branches on `hullData.Prefab` or `hullData.Visual`. That is one branch point, owned by the hull record. - **Authority map:** - Owner: the hull's `Visual` ref decides the visual. The anchor role decides the component slot. - Inputs: the derived catalog snapshot, the mod package directory, and the template prefab. - Outputs: a `ShipInstance` indistinguishable to the simulation from a prefab one. - Derived state: the prototypes (cache-only, rebuilt each boot) and the derived catalog (disposable). - Forbidden writers: prefab arrays hand-wired for a mod ship; any async load during `LoadEntity`; any gameplay read of a mod package outside the derived catalog. - Shared paths: new game, Continue, zone entry, and wormhole transit all go through `LoadEntity`. - **Verification:** - Yggdrasil: `ComposeAtBootIsDeterministic`. The same packages give byte-identical derived records, which pins run-store references across relaunches. - `RunReferencingAMissingModRefusesContinue`, per MQ4. - Unity batchmode (Self): an Editor play-mode smoke loads a fixture mod ship into a zone and asserts `ThrusterHardpoints`, `WeaponHardpoints` and the collider are populated. - Operator: none yet (S3 and S4). - **Subtraction estimate:** 0 removed, about 150 added (loader, template, preload). This is the capability gate 4 buys. **S2 status (2026-09-30, Self):** landed on `codex/moddable-ships` through `48cabc5f`. Soul passes on S2 and its fixes (bad packages excluded and named, never fatal; mount anchors typed by hardpoint; `capturepreset` refuses mod designs; explicit null checks at Continue; compose failure falls back to the shipped catalog). Headless 684/684. Unity 6000.3.24f1: compile clean; `ShipModPlaySmoke.Run` (edit mode, real preload and `Instantiate`) prints `SHIP_MOD_PLAY_SMOKE thrusters=1 weapons=1 collider=ok mapicon=ok`; `ShipModPreview.Smoke -assemble` passes against the generated mesh fixture. Owed: the operator's play-mode checks (menu dialog for excluded mods, New Game/Continue with a mod, zone entry, wormhole, Continue after removal). Recorded, not fixed: two processes sharing `Aetheria.modded.cc`; preload running inside the `CultCache` getter; a mod updated in place that changes a hull passes the gate. The interim ruling that a non-thruster/radiator/weapon mount must be an `articulation` anchor is superseded by the ruling below. **Mount-point ruling (operator, 2026-09-30):** "Regarding mount points, they're only really needed for hardpoints that are externally visible in some way. That's just weapons, radiators, thrusters right now." So a mount anchor exists only for the `weapon`, `radiator` and `thruster` roles. There is no `equipment` role and no plain mount. Other hardpoints are internal and carry no anchor. Articulation points (turret pivots, `ArticulationPoint.Group`) are a separate concept and are not mounts. They stay. Follow-up for S3 prep: find out whether `EquipmentHardpoints` (line 314) has a consumer outside anchor wiring. If it has none, delete it, and have the validator refuse a mount with any other role. Also: the operator said "Missing design refusal on catalog entries is good": keep it. ### S3. Gate 4: a built player A player build loads a composed mod ship without the Editor or Blender. Only Self (the batchmode build) and the operator (launch) can do this. It also requires a playable mod ship: a complete package with hardpoints, not the Quiet probe, which has four anchors and zero hardpoints (`docs/moddable-ship-authoring.md:134-136`). That is content, and it is the operator's to author or commission. ### S4. Gate 5: play checks The operator only: spawn, equip, thrust, fire, shield, damage, save and reload, and the schematic UI for the mod ship. It runs on master's post-fire-control rules, so the checklist's steps 11-15 (arcs, rolled impacts, blasts) apply to the mod hull too. ### S5. Migration onto variants After gate 5, and after MQ2 and MQ3. - Shipped hulls move to `Visual` one at a time, and each migration deletes that hull's `Prefab`. - Hull families collapse into variants in `Aetheria.cc`. The variants scan found Longinus/LonginusX, differing in `Behaviors`, `Hardpoints` and two cosmetic members (`CultLib docs/document-variants-cut.md` §6). - It waits on variants C4 (Aetheria adoption at `caching-unity-v1.5.0`). - **Mount ids come first.** The typed lane requires unique, non-empty mount ids (`ShipAuthoring.cs:115-117` @`375d6bd4`). No shipped hull has them: Djinni has 3 empty `Transform`s and one duplicate, and Zenith has 5 hardpoints with one distinct `Transform` (section 0). Each migration first assigns unique mount ids and repoints the prefab-era name matches. `Transform` is also not usable as a C2 element id, so C4's minted id and the mount id stay two members: one is the identity for variants, and the other names the model node. - **Deletion line:** when the last hull migrates, `HullData.Prefab` (key 24) retires, and `ShipPrefabAuthoring.cs` goes (MQ3). ## B.4 Subtraction ledger (estimate) | Cut | Removed | Added | Formats, targets, dependencies | |---|---|---|---| | landed | 1 | ~511 C#, ~494 Py, 119 test | `aetheria.ship_authoring` document type; glTFast package | | S1 | ~40 | ~35 C#, ~25 Py | `ShipAuthoring` key 1 retired; `HullData` `Visual` slot added | | S2 | 0 | ~150 | one template prefab; no new target | | S5 | per hull: its prefab; finally `ShipPrefabAuthoring.cs` (252) plus its smoke (80) and `HullData.Prefab` | ~0 | FBX builder retired | The lane is net additive until S5, and S5 is where the old authority dies. Keeping two ship-authoring paths alive indefinitely is the liability MQ3 exists to close. ## B.5 Operator questions (moddable ships), one fork each **MQ1. Where does a mod ship's hull live, and how does a hull find its model?** (Blocks S1.) *Context.* - Today a mod ship is one `ShipAuthoring` record with the `HullData` embedded inside it. - Compose writes that hull twice into the derived catalog: once as a `HullData`, and once inside the `ShipAuthoring` copy. - The runtime has no binding yet from a hull to its model; the doc lists it as outstanding. - You want variants to kill duplicate data entry for ships. A variant is a whole record whose base is another record of the same type in the same store. - "LonginusX is Longinus with another hardpoint layout" is therefore a `HullData` variant. An embedded hull inside another document cannot be one. *Options.* - **A:** keep the embedded hull. One record per mod, but a hull family must be full copies, and the derived catalog keeps a dead second copy. - **B:** split it. The hull is a plain `HullData` record, and it names its visual through a `Visual` ref, the way it names a Unity prefab today. `ShipAuthoring` becomes the visual package only. A hull variant inherits its base's visual through normal resolution. - **C:** split it the other way. `ShipAuthoring` references its hull. Then a hull variant has no visual unless something reads the variant's stored base key, and no consumer may read deltas (variants target, "Readers see complete documents"). *Recommend B.* It is the only shape where variants reach ships without either copying or delta-reading. It costs little now, because no committed content uses the old shape. **MQ2. Can a mod ship be a variant of a shipped hull?** (Blocks S5's plan for mods; does not block S1-S4.) *Context.* - Your variants ruling 3 says a variant and its base live in the same store. A mod's `.cc` is its own store, so "my Longinus with a turret" cannot name the shipped Longinus as its base. - Separately, Blender reads mod stores through the Python runtime, and variants ruling 5 has Python refuse any store holding a variant until Python resolution is built (it is parked). - So even a family within one mod's store is blocked in Blender today. *Options.* - **A:** no. A mod ships complete hulls, and families inside a mod wait for Python resolution. - **B:** yes, through compose. Compose would resolve a mod-declared base against the shipped catalog, which is a second resolver outside the cache. - **C:** yes, by reopening ruling 3 for a mod layer. *Recommend A.* B is exactly the bespoke inheritance the variants campaign exists to prevent. C is a CultLib decision bigger than this lane. **MQ3. What is the FBX prefab builder for, now that the typed `.cc` lane exists?** (Blocks S5.) *Context.* - `ShipPrefabAuthoring.cs` (252 lines) and its smoke (80 lines) landed on fire-control-12 at `4b594e11`, so they reach master in this merge. - They build Unity prefabs from FBX collections by naming convention (`docs/ship-collection-authoring.md`). - The typed lane builds the same `ShipInstance` from a `.cc` record and a GLB. - The ships doc keeps the FBX builder "available to existing content", with retirement "following migration". - As things stand, the game has two ship-authoring authorities, and no ruling says which one new ships use. *The question.* Is the FBX builder a permanent path, for example for artists who will never use Blender with Brokkr? Or is it scaffolding until the typed lane passes gate 5? *Options.* - **A:** scaffolding. New ships use the typed lane from gate 5 on, and the builder dies at S5. - **B:** permanent. Both paths stay, and S2's loader keeps the `Prefab` branch forever. - **C:** retire it now, and author no new ships until gate 5. *Recommend A.* Two permanent authoring authorities for one concept is the split this doctrine cuts. C stalls content for no gain. **MQ4. What happens to a saved run when a mod it used is removed?** (Blocks S2's Continue path.) *Context.* - The run store references designs by key, and a mod hull's key is `mod-hull:`. - Remove the mod, and those references dangle. - The fire-control checklist already records that an old run store throws on its first lot lookup (`docs/merge-to-master-checklist.md:15`). *Options.* - **A:** Continue refuses loudly and names the missing mod ids. - **B:** the missing ships are stripped from the run on load. - **C:** the run loads with placeholder hulls. *Recommend A.* A bad mod is rejected before any scene is built (the doc's own shared-path rule). B silently edits the player's save. **MQ5. Who composes the derived catalog?** (Blocks S2.) *Context.* The doc's own objective is "a mod package can be installed without opening the Unity Editor". Gate 4 says "in a built Unity player without Unity Editor or Blender". A built player ships no `AetherDb`, and today only `AetherDb ship-authoring compose` composes. *Options.* - **A:** the game composes at boot, from `GameData/Mods`, into a disposable file in the persistent data path. - **B:** an installer or `AetherDb` step composes, and the game opens `Aetheria.modded.cc` when it exists. *Recommend A.* Compose already lives in `ServerShared` (`ShipModCatalog.cs`), so the game can call it. B makes "is the derived file stale?" a new failure the player owns. ## B.6 Dependencies (moddable ships) - **Fire-control-12 on master:** S0. - **Unity package resolution:** glTFast 6.20.0 needs network on first open (S0 compile). - **Document variants:** - **C2 (element ids):** "ids everywhere" puts an element-id member on `ShipAnchor`, `ShipPolyline`, `HardpointData`, `ItemRole` and `BehaviorData` (every object-list element these records reach). Consequences I read from the code, not ran: - **Hardpoint ids survive a Blender layout save** while the mount id is unchanged. `ship_cc.py:88-98` keeps each hardpoint's slots from 7 on, keyed by `Transform`, so an id slot at 7 or later rides along. Renaming a mount makes a new element, which is the ruling's own rule ("a moved element is a new element"). A hardpoint created in Blender has no id until the next C# write mints one. - **Line ids churn on every capture.** `replace_lines` replaces `SchematicLines` whole (`ship_cc.py:51`). This is harmless: a variant would override lines whole anyway (variants Q7). - **The Python side writes stores C2's registry will then require ids for.** C2 mints them on the first C# write, and Python carries the id as an ordinary member (variants Q3 rollout). So mod stores need no migration step. - **Python refusal:** mod stores stay v1 (no variants) while Blender reads them through `cultcache-py` (MQ2 = A). - **C3 (Studio):** it edits hull variants in `Aetheria.cc`. It is a prerequisite for S5, not for S1-S4. - **C4 (Aetheria adoption, `caching-unity-v1.5.0`):** S5, and the pending test `AHullVariantInheritsItsVisual`. Slot coordination: S1 claims `HullData` key 32 for `Visual`, and C4's id slots must not collide with it. - **Brokkr:** the Blender add-on depends on Brokkr's `brokkr_bridge` preferences for the CultLib Python path (`__init__.py:182-186`). It is an operator-machine dependency with no pin in this repo.