Aetheria/docs/moddable-ships-cut.md
Meta Vens a93625c2f3 Ships map: mount anchors only for weapons, radiators and thrusters
Operator ruling 2026-09-30: mount points exist only for externally
visible hardpoints. No equipment role, no plain mount. Articulation
points stay; they are not mounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 12:14:27 +02:00

516 lines
34 KiB
Markdown

# 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.<group>.<yawMin>.<yawMax>...` 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:<id>`, **and** the whole
`ShipAuthoring`, whose key 1 is the same `HullData`, at `mod-ship:<id>`.
- 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 <package/ship.cc>`
(`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:<id>` (`ShipModCatalog.cs:20`)
and the `ShipAuthoring` visual package (`Id`, `ModelAsset`, `Anchors`, `SchematicLines`).
- `HullData` gains a nullable `CultRecordRef<ShipAuthoring> 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:<id>` 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 <CultLib>/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:<id>`.
- 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.