Probes B1-B9, the owner answer (published AetherDb), the r5 authority map and rationale from the imagination-addon-frame pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6915 lines
465 KiB
Markdown
6915 lines
465 KiB
Markdown
# 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:**
|
||
- https://www.audiokinetic.com/en/wwise/pricing/
|
||
- https://www.audiokinetic.com/en/community/blog/free-wwise-for-indie-developers/
|
||
- https://www.gamedeveloper.com/audio/small-developers-and-creators-can-now-use-fmod-studio-for-free
|
||
- https://gamefromscratch.com/fmod-studio-now-free-for-indie-game-developers/
|
||
|
||
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 `BaseState`s 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
|
||
|
||
- Damián Isla, "Handling Complexity in the Halo 2 AI", GDC 2005
|
||
(https://www.gamedeveloper.com/programming/gdc-2005-proceeding-handling-complexity-in-the-i-halo-2-i-ai):
|
||
a prioritised behaviour DAG, and impulses that raise a behaviour's priority from
|
||
anywhere. `includeChildren` is the legacy tree's impulse.
|
||
- Damián Isla, "Building a Better Battle: HALO 3 AI Objectives", GDC 2008
|
||
(https://gdcvault.com/play/497/Building-a-Better-Battle-HALO, slides at
|
||
https://web.cs.wpi.edu/~rich/courses/imgd4000-d09/lectures/halo3.pdf): designers
|
||
author tasks with priorities and capacities, and squads fill them; inside a task
|
||
the AI is autonomous. This is the slot model for the pack.
|
||
- Jeff Orkin, "Three States and a Plan: The A.I. of F.E.A.R.", GDC 2006
|
||
(https://www.gamedevs.org/uploads/three-states-plan-ai-of-fear.pdf): three FSM
|
||
states and a planner per agent, and a global coordinator that clusters agents
|
||
into squads by proximity and runs one squad behaviour at a time. Squad dialogue
|
||
voices decisions already made. A planner is more than these dynamics need; the
|
||
coordinator and the barks transfer.
|
||
- RimWorld's Lord, LordJob and LordToil (decompiled source,
|
||
https://github.com/josh-m/RW-Decompile/blob/master/Verse.AI.Group/LordMaker.cs):
|
||
a group controller with its own state machine assigns a duty to each pawn, and
|
||
each pawn's think tree runs its duty. That is the flight and its tasks.
|
||
- Starsector officer personalities (https://starsector.wiki.gg/wiki/Officer): timid
|
||
to reckless, each changing engagement range and retreat over one combat AI.
|
||
Faction doctrine and fleet assignments (patrol, escort, defend) are cited from
|
||
memory, not re-fetched. Lesson: parameters on one AI, as Faction Play already
|
||
says.
|
||
- Attack tokens, the "kung fu circle" of brawler games: only N enemies attack at
|
||
once while the rest posture. From memory; no primary source fetched.
|
||
- The US Navy's Cooperative Engagement Capability, which engages on remote fused
|
||
tracks. From memory; cited as the path not taken.
|
||
|
||
## 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 `Position`s.
|
||
- 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:
|
||
- https://forum.kerbalspaceprogram.com/topic/144858-squad-folder/
|
||
- https://forums.factorio.com/viewtopic.php?t=12564
|
||
- https://spmatlas.com/guides/mods-explained/
|
||
- https://fractalsoftworks.com/forum/index.php?topic=12970.0
|
||
- https://docs.blender.org/manual/en/latest/addons/scene_gltf2.html
|
||
|
||
## 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 `GridObject`s, 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: `TargetRegion`s (distance interval, bearing interval, visibility
|
||
ceiling) and `TargetCandidate`s (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.
|
||
|
||
### Mining index: rings, cells and best-first search
|
||
|
||
**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 `PerformanceStat`s 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.unpackb`ed 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](https://wiki.eveuniversity.org/Bombs)). Starsector's Stinger
|
||
mines use a friend-or-foe trigger and detonate when a target comes within range
|
||
([Starsector wiki](https://starsector.fandom.com/wiki/Stinger-class_Proximity_Mine),
|
||
[Mine Strike](https://starsector.wiki.gg/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.unpackb`ed 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`):
|
||
|
||
| 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 stack with gear under the one resolver
|
||
rule (`modifier-stacking`).
|
||
- **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.** 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 |
|