Rewrites the release target for the demo cut (one gate, one region, one boss, a fixed cast of Pirates, Zhestokost, Lucent and AU, called a demo). Adds the map's Demo scope section with body facts D1-D6 and the scope table, and sweeps four stale docs the design review listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1202 lines
71 KiB
Markdown
1202 lines
71 KiB
Markdown
# Headless Playground: Run Lifecycle Cut Map
|
|
|
|
Date: 2026-09-17
|
|
|
|
Status: Imagination pass, cut map. Nothing here has landed. Anchors are against
|
|
`codex/item-provenance` HEAD `1e647953`. Mechanism claims marked **(probe)** were run
|
|
headless against the real catalog `GameData/Aetheria.cc` (read-only) and the authored
|
|
`Assets/Resources/Settings.asset`. Run and player stores went to a temp directory. The
|
|
probe was built with `--artifacts-path` in scratch, so this tree got no `bin`/`obj`
|
|
writes. CultLib was a temporary detached worktree at `a0813c6`, since removed. The probe
|
|
source is not kept; §2.8 records what it did.
|
|
|
|
## 0. Target
|
|
|
|
Operator, 2026-09-17: player inputs "map directly to the same entity inputs the AI
|
|
uses". An agent should be able to "spin up a playground ... generate galaxies, puppeteer
|
|
an entity around yourself using the existing agent routines", and "This should be
|
|
possible without involving Unity".
|
|
|
|
Ends:
|
|
- An agent (Hands or Soul) runs a scripted session headless. The script can:
|
|
- start a new game (galaxy, entrance zone, player ship);
|
|
- drive the pilot by hand-set entity inputs or by an existing `Agent`;
|
|
- dock, undock, warp between zones, take hits, die, and save;
|
|
- reopen the stores and Continue.
|
|
- The session emits a compact trace. It exits non-zero when an expectation fails or an
|
|
exception escapes.
|
|
- First consumers:
|
|
- the headless half of the CultCache-migration play smoke
|
|
(`docs/cultcache-migration-cut.md:2739-2765`);
|
|
- the headless half of the provenance Cut C operator checks
|
|
(`docs/item-provenance-cut.md` §6, "Operator-only checks after Cut C");
|
|
- the reopen defect in §2.6.
|
|
|
|
Invariants:
|
|
- **One owner of the run lifecycle.** The playground and `ActionGameManager` both call
|
|
the same ServerShared owner. Neither re-implements new game, zone entry, warp, dock,
|
|
save, Continue or death.
|
|
- **One input surface.** Manual input, agent routines and playground commands write the
|
|
same entity fields and call the same behavior methods. The playground has no private
|
|
input path and writes no entity state the player cannot reach. Exceptions are named
|
|
commands that call the same owner the Unity console calls.
|
|
- **Determinism.** A scenario with a given seed produces the same trace twice. The
|
|
probe's two runs matched line for line.
|
|
- **The playground never touches `GameData/run.cc` or `GameData/player.cc`.** It opens
|
|
the catalog read-only and keeps its run and player stores in a directory it is given.
|
|
|
|
Non-goals:
|
|
- No rendering, no Unity, no new daemon, no CultMesh surface.
|
|
- No headless ballistics. Fire control does not detect hits; it rolls them, headless
|
|
(fork D ruling, Cut 2). Unity flies the shot to the outcome.
|
|
- No trade menu, no action-bar UI, no input devices.
|
|
- Tier colour, properties panel, input screen and CultCache Studio rendering stay
|
|
operator checks.
|
|
|
|
## 1. Verdict on the operator's claim
|
|
|
|
**Mostly true for movement and firing. False for how combat resolves, and the AI's
|
|
combat state cannot run without Unity.**
|
|
|
|
- True:
|
|
- Player movement and AI movement write the same `Ship.MovementDirection`
|
|
(`Ship.cs:27`). Player: `ActionGameManager.cs:1201`. AI: `Agent.cs:67,73,77`.
|
|
- Both write the same `Entity.LookDirection` (`Entity.cs:42`). Player:
|
|
`ActionGameManager.cs:1189-1192`. AI: `MoveTo.cs:26`, `Combat.cs:111`,
|
|
`Agent.cs:66`.
|
|
- Both write the same `Entity.Target` (`Entity.cs:40`). Player:
|
|
`ActionGameManager.cs:366-397`. AI: `Minion.cs:14`.
|
|
- Both fire through the same `Weapon.Activate/Deactivate`. Player:
|
|
`ActionBarSlot.cs:190-204`, via the action bar at `ActionGameManager.cs:411-412`.
|
|
AI: `Combat.cs:56,106,108`.
|
|
- Player-only inputs, all plain entity writes that an agent could make:
|
|
- `TractorPower` (`ActionGameManager.cs:1214-1216`);
|
|
- `OverrideShutdown`, `HeatsinksEnabled`, shield `Enabled`, `Sensor.Ping` (`:339-362`);
|
|
- gear `IActivatedBehavior` (`ActionBarSlot.cs:159-168`);
|
|
- consumables (`ActionBarSlot.cs:119-122`, `Entity.TryActivateConsumable`).
|
|
- Player-only lifecycle actions. These are the orchestration this map moves:
|
|
- interact, which means warp, dock or undock (`ActionGameManager.cs:298-320`);
|
|
- tow (`:889-913`);
|
|
- boarding another docked ship (`InventoryPanel.cs:117-118`).
|
|
- **False: combat resolves in Unity.**
|
|
- `InstantWeapon.OnFire` only raises an event (`InstantWeapon.cs:183`). The projectile
|
|
is a Unity effect manager (`EntityInstance.cs:203-204`).
|
|
- Hits are physics colliders (`HullCollider.cs:18`, `GuidedProjectile.cs:170`).
|
|
- Turning a hit into armor, item and hull damage is `EntityInstance.DamageSchematic`
|
|
(`EntityInstance.cs:276-314`), with hit-shape and penetration at `:330-395`.
|
|
- NPC death removes the entity from the zone at `:437`. Loot drops in the same place
|
|
(`:405-435`), rolled with `UnityEngine.Random`.
|
|
- Pickup is a collision trigger (`ShieldManager.cs:29`).
|
|
- **(probe)** Weapon groups fired 6 `OnFire` events at an enemy 50 units ahead. Enemy
|
|
hull stayed 500.0 -> 500.0.
|
|
- **False: `CombatState` needs Unity-written data.** It indexes
|
|
`Ship.HardpointTransforms` (`Combat.cs:101`). Only `EntityInstance.cs:516` writes that
|
|
dictionary, from barrel transforms.
|
|
- **(probe)** A `Minion` with a target throws `KeyNotFoundException: 'Energy Hardpoint '`
|
|
on its first combat step.
|
|
- Every NPC agent that acquires a target would throw the same way headless.
|
|
- The weapons' own direction reader already has a fallback (`Behaviors.cs:24-43`).
|
|
`CombatState` does not use it.
|
|
|
|
## 2. Body findings
|
|
|
|
### 2.1 Where the run lifecycle lives today
|
|
|
|
All of this is Unity code:
|
|
|
|
| Concern | Anchor | Unity dependencies |
|
|
|---|---|---|
|
|
| Store composition | `ActionGameManager.cs:43-63` (static, `Application.dataPath`) | path only |
|
|
| Handoff across scene load | statics `CurrentGalaxy`, `IsTutorial` (`:96-97`), written by `MainMenu.cs:104-105,126,138,152,166`, read by `SectorMap.cs:114-310` | `SceneManager.LoadScene` (`MainMenu.cs:106,139,167`) |
|
|
| New game: clear | `MainMenu.cs:112` | none |
|
|
| New game: galaxy | `MainMenu.cs:125,148` (`UnityEngine.Random.value` background noise), `:127-141,153-169` (`Task.Run`, then `Observable.NextFrame`) | Random, UniRx main-thread scheduler |
|
|
| ItemManager | `ActionGameManager.cs:264`; RNG clock-seeded at `ItemManager.cs:20` | `Debug.Log` |
|
|
| Start / Continue | `StartGame` `:725-770`: spawn player ship `:733-744`; Continue resolves the docked child `:751-762`; action bar restore `:764-767` | renderer binding, `SectorMap` reveal |
|
|
| Zone entry | `PopulateLevel` `:639-675`: generate/unpack `:643-654`; move pilot `:659-666` | `ZoneRenderer.LoadZone`, `Debug.Log` |
|
|
| Wormhole placement (a game rule) | `ZoneRenderer.cs:222-231` (`dir * Pack.Radius * GameSettings.WormholeDistanceRatio`) | lives in a MonoBehaviour |
|
|
| Warp | detect `ActionGameManager.cs:312-316`; `EnterWormhole` `:615-637`, which saves on arrival (`:635`) | `ZoneRenderer.WormholeInstances` as the wormhole list |
|
|
| Dock / undock / tow | `:801-823`, `DoDock` `:825-843` (cameras, menus), `:845-887` (dialogs), `:889-913` | cameras, dialogs |
|
|
| Board a docked ship | `InventoryPanel.cs:117-118` | UI button |
|
|
| Save | `SaveRun` `:241-249`, called at quit `:236` and warp `:635` | action-bar slots (`:246`) |
|
|
| Death | `BindToEntity` subscribes `Death` -> `Die` (`:1053`); `Die` `:1080-1115` clears the run (`:1088`) | `Time.time`, `Observable.EveryUpdate` post-fx |
|
|
| Tick | `Update` `:1151-1220`: input `:1189-1216`, then `Zone.Update(Time.deltaTime)` `:1218` | `Time.deltaTime`, pause flag |
|
|
| Dead code | `IntroCutscene` `:772-799`; its only call is commented out (`:760`) | coroutine, `Time.time` |
|
|
|
|
### 2.2 Already headless
|
|
|
|
- `Aetheria.Shared/Aetheria.Shared.csproj` compiles `Assets/Scripts/ServerShared/**`
|
|
plus UniRx (`UniRxLibrary`) and Ink with no Unity defines. It is the
|
|
Unity-independence check.
|
|
- Galaxy generation, zone generation, `Zone`, entities, behaviors, agents, `RunSave`,
|
|
`AetheriaStores` and `ItemManager` all run under it. The probe measured:
|
|
- galaxy seed 42: 196 zones;
|
|
- the first zone with ships and stations: 7 entities and 4 agents;
|
|
- a player `LonginusX` spawned through `LoadoutGenerator`;
|
|
- thrust by hand-set `MovementDirection` reached 142 m/s in 5 s;
|
|
- a `Minion` patrol moved the player ship 642 units in 10 s;
|
|
- `TryDock` and `TryUndock` succeeded (`Entity.cs:721-755`);
|
|
- `Ship.EnterWormhole` followed by stepping fired `OnEnteredWormhole` (`Ship.cs:82-90,372`);
|
|
- `Death` fired on hull damage (`Entity.cs:277-280`);
|
|
- `RunSave.Capture` and `RunSave.Commit` wrote a 489 KB `run.cc`.
|
|
- Settings: `GameSettings` is a ScriptableObject (`GameSettings.cs:11`) holding
|
|
ServerShared classes.
|
|
- `tools/AetherDb/AuthoredSettings.cs` reads them from the asset YAML. The probe
|
|
placed every field (0 unplaced).
|
|
- Two lifecycle inputs are Unity-only `GameSettings` fields: `StartingHullName`
|
|
(`GameSettings.cs:17`, authored `LonginusX`) and `WormholeDistanceRatio` (`:23`,
|
|
authored 0.75).
|
|
- Randomness:
|
|
- `ItemManager.Random` is clock-seeded (`ItemManager.cs:20`).
|
|
- `Galaxy` is clock-seeded only when `seed == 0` (`Galaxy.cs:108,158`).
|
|
- `Zone` seeds from the zone name (`Zone.cs:54`).
|
|
- `Zone.Update` runs belt updates on `Task.Run` (`Zone.cs:144`). It is still
|
|
deterministic (probe: identical traces).
|
|
|
|
### 2.3 Agents
|
|
|
|
- The agent API is `Agent.Update(dt)` (`Agent.cs:47-56`) over a `BaseState` graph.
|
|
- `Minion` (`Minion.cs:7-21`) patrols (`PatrolOrbits.cs`), auto-targets the first visible
|
|
enemy (`:14`), and enters `CombatState`.
|
|
- Agents are created only for packed non-player ships at zone construction
|
|
(`Zone.cs:88-93,99-106`) and are updated before entities (`:147-150`).
|
|
- Nothing ever removes one from `Zone.Agents`.
|
|
- The player ship never gets an agent.
|
|
- An agent held in a zone's list would stay behind when its ship warps (`PopulateLevel`
|
|
moves the entity, not agents).
|
|
- There is no "fly to a point" state. `MoveToState` is abstract and its only concrete
|
|
state is `MoveToOrbitState` (`MoveTo.cs:7-37`).
|
|
|
|
### 2.4 AetherDb
|
|
|
|
- Commands: `census`, `station-fit`, `hardpoint-fit`, `loadout [seed]`, `save`,
|
|
`factions`, `dangling`, `settings`, `settings-dump` (`Program.cs:18-35`).
|
|
- Stores open through `AetherDb.Open`, which calls `AetheriaStores.Open` with
|
|
`GameData/run.cc` hard-wired (`AetherDb.cs`). `save` (`Program.cs:349-410`) is the only
|
|
played-save reader.
|
|
- It already owns `AuthoredSettings` (YamlDotNet) and references `Aetheria.Shared`, so it
|
|
has everything the playground needs.
|
|
|
|
### 2.5 Tests at HEAD
|
|
|
|
**(probe)** `dotnet test tests/Aetheria.Shared.Tests` at `1e647953` with CultLib
|
|
`a0813c6`: **32 passed, 2 failed.** Both failures are
|
|
`MessagePackSerializationException: Failed to deserialize SavedZone value`:
|
|
- `LoadoutTests.MaterializedLotsSurviveSaveAndReload`
|
|
- `RunSaveTests.CommitKeepsOnlyReachableLots`
|
|
|
|
This contradicts the "34 tests green" verification that `docs/item-provenance-cut.md`
|
|
sets for Cut B.
|
|
|
|
### 2.6 The reopen defect, confirmed
|
|
|
|
**(probe)** Reopening a run store that holds a `SavedZone` with any entity throws:
|
|
|
|
```
|
|
MessagePackSerializationException: Failed to deserialize SavedZone value.
|
|
TypeAccessException: No hash-resistant equality comparer available for type: CultMath.int2
|
|
```
|
|
|
|
- The failing field is `EntityPack.PersistedBehaviors`, a
|
|
`Dictionary<int2, PersistentBehaviorData[]>` (`EntitySerializer.cs:200`), written at
|
|
`:35-40` and read at `:126-130`.
|
|
- The player ship's dictionary was **empty** (0 persisted behaviors) and it still threw.
|
|
MessagePack builds the dictionary's comparer before reading any entry.
|
|
- `CultMessagePackSecurity` runs `UntrustedData` and adds a comparer only for
|
|
`CultRecordRef<T>` keys.
|
|
- No other run-store type has a struct dictionary key. `SavedGame` uses `int` keys, the
|
|
ledger uses `int`, and `PlayerSettings` uses `string`.
|
|
- Consequence: **Continue has been unable to load any run that visited a zone** since
|
|
entities entered the run store under this security. Migration smoke step 6 cannot
|
|
pass. Unity reads through the same CultLib serializer, which is inferred, not run
|
|
here.
|
|
|
|
### 2.7 Asides found in passing (not this cut's to fix)
|
|
|
|
- `TargetNearest` picks the **farthest** visible enemy: `MaxBy(length(...))`
|
|
(`ActionGameManager.cs:378-379`).
|
|
- The interact loop enters every wormhole in range, then calls `Dock()` anyway
|
|
(`:312-317`).
|
|
- `SaveRun` passes `DockedEntity ?? CurrentEntity` (`:245`), but `BindToEntity` nulls
|
|
`DockedEntity` (`:946`). The order is fragile, not wrong today.
|
|
|
|
### 2.8 The probe (discarded)
|
|
|
|
A scratch console referencing `Aetheria.Shared` and `AuthoredSettings.cs`. It:
|
|
1. Loaded authored settings.
|
|
2. Opened the stores (catalog read-only; run and player in temp).
|
|
3. Generated a galaxy with seed 42 and set `ItemManager.Random` to seed 7.
|
|
4. Generated zones outward from the entrance until one held ships and stations.
|
|
5. Spawned the player ship, drove it by hand, then by a `Minion`.
|
|
6. Forced a target, fired groups by hand, then docked, undocked and entered a wormhole.
|
|
7. Killed an NPC by hull damage, then saved, disposed and reopened.
|
|
|
|
Traces from two runs were identical.
|
|
|
|
## 3. Authority map
|
|
|
|
**Owner: `Run`, a new ServerShared class (`Assets/Scripts/ServerShared/Run.cs`).** It
|
|
owns one live run's lifecycle: start, zone entry, pilot, dock state, warp, save,
|
|
Continue and death. There is one instance per run. It holds no Unity type.
|
|
|
|
- **Inputs:**
|
|
- the `CultCache` from `AetheriaStores.Open`, composed by the caller;
|
|
- `GameplaySettings`, `ZoneGenerationSettings`, `PlanetSettings`;
|
|
- a `uint seed` for `ItemManager`;
|
|
- `Action<string> log`;
|
|
- for a new run, a caller-built `Galaxy` and `isTutorial`;
|
|
- `dt` per step;
|
|
- commands from either lowering;
|
|
- `SavedActionBarBinding[]` at save, owned by the Unity action bar (the playground
|
|
passes empty).
|
|
- **Outputs:**
|
|
- state: `Galaxy`, `ItemManager`, `Zone`, `Pilot` (the piloted `Entity`), `Station`
|
|
and `Bay` when docked, `TowingStation`, `Ended`;
|
|
- events: `ZoneEntered(Zone)`, `PilotChanged(Entity)`, `Docked(Entity, EquippedDockingBay)`,
|
|
`Undocked`, `WarpArrived(GalaxyZone)`, `Saved`, `Died(CauseOfDeath)`;
|
|
- store writes through `RunSave.Commit` and `RunSave.Clear` only.
|
|
- **Derived state:**
|
|
- `ActionGameManager.CurrentEntity`, `.Zone`, `.ItemManager`, `.DockedEntity`,
|
|
`.DockingBay` and `.TowingStation` become read-only projections of `Run`, for
|
|
display only.
|
|
- `ZoneRenderer.WormholeInstances` becomes render-only: its keys are
|
|
`Zone.Wormholes`, and it is no longer the wormhole list gameplay reads.
|
|
- `SectorMap` reads `Run.Galaxy` for display.
|
|
- **Forbidden writers after the cut:**
|
|
- `ActionGameManager` may not call `RunSave.Clear`, `RunSave.Capture`, `RunSave.Commit`,
|
|
`ZoneGenerator.GenerateZone` or `new Zone`, and may not add or remove the pilot in
|
|
`Zone.Entities`.
|
|
- It may not construct `ItemManager`, spawn the player ship, subscribe `Death` to end
|
|
the run, or set `CurrentEntity`, `DockedEntity`, `DockingBay` or `TowingStation`.
|
|
- `MainMenu` may not call `RunSave.Clear` or construct `Galaxy(cache, saved)`.
|
|
- `InventoryPanel` may not write `CurrentEntity` or `DockingBay.DockedShip`.
|
|
- `ZoneRenderer` may not create `Wormhole` objects.
|
|
- The playground may not write `Position`, `Velocity`, `Hull.Durability` or zone
|
|
membership directly. It has no `hit` command; damage comes only from fire control
|
|
(fork D ruling).
|
|
- **Fire control (Cut 2).** `FireControl` (ServerShared) owns hit, cell and damage; `Zone`
|
|
owns death removal, loot drops and the pickup commit.
|
|
- `Entity.TargetItem` is a command, written only through `TrySelectTargetItem`. The
|
|
aimed item is derived from it plus reveal.
|
|
- Unity weapon effects and `HullCollider` are presentation of `ShotOutcome`.
|
|
- Forbidden deciders of hits or damage: Unity weapon effects, `HullCollider`, any
|
|
`Physics.*` query, `EntityInstance`, `ShieldManager` (it may only call
|
|
`Zone.TryPickUp`), the playground, and `Entity.HardpointTransforms` (deleted).
|
|
- Firing arc: the mount direction is `Behavior.Direction`, derived from the ship's
|
|
`Direction` and the mounted item's `ItemRotation` (itself `HardpointData.Rotation`,
|
|
`Entity.cs:559`); the arc width is `GameplaySettings.FiringArc` (fork W for overrides).
|
|
Prefab `WeaponHardpoint.FiringPoint` transforms and `ArticulationPoint` limits are
|
|
presentation: they aim barrels and place effects, and decide no shot.
|
|
- **Shared paths:**
|
|
- Manual input and agent input write the same entity fields (§1), and both go before
|
|
`Run.Step`. Unity writes input in `Update`, then calls `run.Step(Time.deltaTime)`.
|
|
The playground updates its pilot agent inside `Run.Step`, before `Zone.Update`, so
|
|
the order matches.
|
|
- Direct interaction (Unity's Interact key), programmatic interaction (playground
|
|
`dock`/`warp`) and Continue's docked landing all commit through `Run.TryDock`,
|
|
`Run.TryEnterWormhole` and `Run.Land`.
|
|
- A new run and Continue share `Run.EnterZone`.
|
|
- Warp arrival and quit share `Run.Save`.
|
|
- **Pilot agent:**
|
|
- `Run.PilotAgent` (nullable) is owned by `Run`, not by `Zone.Agents`, so it follows
|
|
the pilot across warps.
|
|
- Unity leaves it null. It may never be non-null while Unity input is enabled.
|
|
- `Zone.Agents` stays the owner of NPC agents.
|
|
- **Deletion line:**
|
|
- `ActionGameManager`: bodies of `SaveRun`, `EnterWormhole`, `PopulateLevel`'s
|
|
simulation half, `StartGame`, `Dock`, `DoDock`'s state half, `Undock`'s checks,
|
|
`TowShip`, `Die`'s clear, and `IntroCutscene` (entirely);
|
|
- statics `CurrentGalaxy` and `IsTutorial`;
|
|
- `MainMenu.cs:104-105,112`;
|
|
- `ZoneRenderer.cs:222-231`'s wormhole construction;
|
|
- `ItemManager.cs:20`'s clock seed.
|
|
|
|
All of these go in the same cut that adds `Run`. None survives as a fallback.
|
|
|
|
Unity dependencies, each injected or cut:
|
|
|
|
| Dependency | Where | Disposition |
|
|
|---|---|---|
|
|
| `Time.deltaTime` | `:1218` | injected: `Run.Step(float dt)` |
|
|
| `Time.time` | `IntroCutscene`, `Die` post-fx | cutscene deleted; post-fx stays in Unity, on `Died` |
|
|
| `UnityEngine.Random` | `MainMenu.cs:125,148` (background noise), `EntityInstance.cs:413-428` (loot) | noise stays in the menu as a galaxy-generation input; loot roll moves to `Zone` on `ItemManager.Random` (Cut 2); effect spread jitter (`ProjectileManager.cs:20-26`) stays visual |
|
|
| Clock RNG | `ItemManager.cs:20` | cut; the seed is a constructor argument, and Unity passes a clock-derived one |
|
|
| Coroutines | `IntroCutscene` | deleted (dead) |
|
|
| UniRx | `Observable.NextFrame` in the menu, `EveryUpdate` in `Die` | stay in Unity; `Run` uses C# events, and `Entity.Death` (UniRx, headless-compiled) is what `Run` subscribes to |
|
|
| `Debug.Log` | `:264`, `:657`, `MainMenu` | injected `Action<string>` |
|
|
| Scene load | `MainMenu` | stays; the handoff static becomes `ActionGameManager.NewRun : (Galaxy, bool)?`, command-only, consumed once in `Start`; null means Continue |
|
|
| MonoBehaviour state | `_currentEntity`, `DockedEntity`, `DockingBay`, `TowingStation`, `Zone`, `ItemManager` | projections of `Run` |
|
|
| Barrel transforms | `EntityInstance.cs:516` -> `HardpointTransforms` | `HardpointTransforms` deleted (Cut 2); the arc is item rotation plus `FiringArc` (fork A); barrels and `ArticulationPoint` are presentation only, `GetBarrel` |
|
|
| `Physics.*` hit queries | `Projectile`, `GuidedProjectile`, `Laser`, `Lightning`, `HullCollider` subjects | deleted from damage paths; `FireControl` rolls (Cut 2) |
|
|
|
|
## 4. Cuts
|
|
|
|
Build budget for every cut:
|
|
- Packages: `Aetheria.Shared` (netstandard2.1), `tools/AetherDb` and
|
|
`tests/Aetheria.Shared.Tests` (net10.0), Debug only.
|
|
- CultLib at `a0813c6`, unmodified, under fork R (a).
|
|
- No new project, target, package or schema store.
|
|
- Build host: this Windows workstation, which is also the target for the headless
|
|
checks.
|
|
- `Assembly-CSharp` is proven only by Unity batchmode with the editor closed, per
|
|
`docs/item-provenance-cut.md` §6. This is the operator's call.
|
|
- Headless commands use `--artifacts-path <scratch>` while another agent is reading the
|
|
tree.
|
|
|
|
### Cut 0: make a run with entities reopenable (fork R)
|
|
|
|
**Ruled (operator, 2026-09-17):** fork R is the pairs array. It lands with the provenance
|
|
Cut B fix batch; see `docs/item-provenance-cut.md`.
|
|
|
|
|
|
- `EntitySerializer.cs:200`: `PersistedBehaviors` becomes
|
|
`(int2 position, PersistentBehaviorData[] data)[]`, matching `Equipment` at `:197-199`.
|
|
- Key 5 is kept. The wire shape changes from a map to an array; saves are discarded
|
|
and no `GameData/run.cc` exists.
|
|
- `:35-40` packs pairs.
|
|
- `:126-130` builds a local lookup at unpack.
|
|
- `RunSaveTests.cs` `BarePack` initializer: `Array.Empty<...>()`.
|
|
- Verification:
|
|
- The two HEAD-red tests go green: 34 passed.
|
|
- Mutation: restore the `Dictionary<int2,…>` type. Both tests go red with the
|
|
`int2` comparer message.
|
|
- Add `ReopenedPackKeepsPersistedBehaviors` to `RunSaveTests`. A pack with two
|
|
positions, each with one persisted behavior, commits and reopens, and the positions
|
|
and data are equal.
|
|
- Mutation: pack drops `data`, so the test goes red.
|
|
- Mutation: unpack ignores the lookup, so the test goes red.
|
|
- Negative: `rg -n "Dictionary<int2" Assets/Scripts/ServerShared` is empty.
|
|
- Ledger: about ±6 C#, +25 test.
|
|
|
|
### Cut 1: `Run` becomes the single lifecycle owner; Unity lowers it
|
|
|
|
This is subtraction first, then one new file.
|
|
|
|
- Deletes and moves:
|
|
- `ActionGameManager.cs`:
|
|
- `IntroCutscene` `:772-799` (dead).
|
|
- Statics `CurrentGalaxy` and `IsTutorial` (`:96-97`).
|
|
- `SaveRun` body `:243-248` becomes `Run?.Save(actionBar)`. Quit (`:236`) calls it.
|
|
- `EnterWormhole` `:615-637` and the detect loop `:312-316` become
|
|
`Run.TryEnterWormhole()`. `WarpArrived` drives `SectorMap.QueueZoneReveal`.
|
|
- `PopulateLevel` `:639-675`: the simulation half moves into `Run.EnterZone`. What
|
|
stays is a `ZoneEntered` handler: `ZoneRenderer.LoadZone`, `PlayMusic`, rebind.
|
|
- `StartGame` `:725-770` becomes `Run.New` or `Run.Continue` in `Start`. It keeps
|
|
the sector reveal and action-bar restore in Unity. Restore reads
|
|
`min(slots, saved.Length)`, so a playground-written save continues in Unity.
|
|
- `Dock` `:801-823` becomes `Run.TryDock()`. `DoDock` `:825-843` keeps cameras and
|
|
menus on `Docked`; its state writes `:828,830,840-841` go to `Run`.
|
|
- `Undock` `:845-887` becomes `Run.Undock()`, returning `UndockResult`. The dialog
|
|
text maps from the result.
|
|
- `TowShip` `:889-913` becomes `Run.Tow()`.
|
|
- `Die` `:1080-1088` becomes a `Died` handler: unbind, menus, post-fx,
|
|
`SavePlayerSettings`. The `RunSave.Clear` at `:1088` goes. `:1053` subscribes
|
|
`Run.Died`.
|
|
- `:264` constructs no `ItemManager`.
|
|
- `Update` `:1218` becomes `Run.Step(Time.deltaTime)`.
|
|
- `MainMenu.cs`:
|
|
- Continue (`:102-107`) sets `ActionGameManager.NewRun = null` and loads the scene.
|
|
- New Game drops `:112` and sets `NewRun = (sector, tutorial)` at `:138,166`.
|
|
- `SectorMap.cs:114-310` reads `ActionGameManager.Instance.Run.Galaxy`.
|
|
- `InventoryPanel.cs:117-118` becomes `GameManager.Run.Board(ship)`.
|
|
- `ZoneRenderer.cs:222-231` iterates `zone.Wormholes`.
|
|
- `ItemManager.cs:20`: the seed becomes a constructor argument. Call sites: 11 in
|
|
tests, 1 in `AetherDb`, and `Run`.
|
|
- Settings (fork S (a)):
|
|
- `StartingHullName` and `WormholeDistanceRatio` move from `GameSettings.cs:17,23` to
|
|
`GameplaySettings` (`Settings.cs:155`).
|
|
- The asset keys move under `GameplaySettings:` in `Settings.asset` by text edit, with
|
|
Unity closed.
|
|
- Readers `ActionGameManager.cs:738` and `ZoneRenderer` go through
|
|
`ItemManager.GameplaySettings`.
|
|
- `Zone.cs`: `public Wormhole[] Wormholes`, built in the constructor from
|
|
`GalaxyZone.AdjacentZones` with the rule from `ZoneRenderer.cs:226-230`. Null galaxy
|
|
zone gives an empty array.
|
|
- New `Run.cs`: types and rules, no bodies here.
|
|
- `static Run New(CultCache, GameplaySettings, ZoneGenerationSettings, PlanetSettings, Galaxy, bool isTutorial, uint seed, Action<string> log)`
|
|
1. `RunSave.Clear`.
|
|
2. `ItemManager` over `RunSave.Lots`, seeded.
|
|
3. `EnterZone(Entrance)`.
|
|
4. Spawn the player ship per `:734-743`.
|
|
5. Set the pilot. Returns a live run.
|
|
- `static Run Continue(CultCache, …, uint seed, Action<string> log)`
|
|
- Returns null when there is no `SavedGame`.
|
|
- Otherwise `Galaxy(cache, saved, log)`, then `EnterZone(saved zone)`, then
|
|
`Land(saved entity)`.
|
|
- `Land` handles an orbital entity per `:753-756`: pilot is the `IsPlayerShip`
|
|
child, and the run is docked there.
|
|
- `void Step(float dt)`
|
|
- Throws if `Ended`.
|
|
- Updates `PilotAgent`, then `Zone.Update(dt)`.
|
|
- `void EnterZone(GalaxyZone)`: the rule from `:643-666`. It raises `ZoneEntered`.
|
|
- `bool TryEnterWormhole()`
|
|
- Picks the nearest `Zone.Wormholes` entry within
|
|
`GameplaySettings.WormholeExitRadius`. Requires the pilot to be a `Ship`, not
|
|
mid-animation, and undocked.
|
|
- On `OnEnteredWormhole`:
|
|
1. `EnterZone(target)`.
|
|
2. Discover adjacent zones.
|
|
3. `ExitWormhole` at the back-link wormhole with
|
|
`WormholeExitVelocity * ItemManager.Random.NextFloat2Direction()`.
|
|
4. `Save(lastActionBar)`.
|
|
5. Raise `WarpArrived`.
|
|
- `EquippedDockingBay TryDock()`: the rule from `:803-821`, within `DockingDistance`.
|
|
- `UndockResult Undock()`: `Ok`, `NotDocked`, `MissingCockpit`, `MissingThruster`,
|
|
`MissingReactor`, `BayNotEmpty`. Check order as `:850-885`.
|
|
- `bool Board(Ship)`: docked only; the ship must be the station's `IsPlayerShip`
|
|
child; updates `Bay.DockedShip`.
|
|
- `void Tow()`: the rule from `:891-911`.
|
|
- `void Save(SavedActionBarBinding[] actionBar)`
|
|
- `RunSave.Capture(cache, Galaxy, Zone, Station ?? Pilot, isTutorial, actionBar)`,
|
|
then `Commit(…, ItemManager.Lots)`.
|
|
- No-op when `Ended`.
|
|
- Remembers `actionBar` for warp saves.
|
|
- Death: `Run` subscribes `Pilot.Death` whenever the pilot changes. On death:
|
|
1. `Ended = true`.
|
|
2. `RunSave.Clear`.
|
|
3. Dispose subscriptions.
|
|
4. Raise `Died(cause)`.
|
|
- `Agent PilotAgent { get; set; }`: setting it while `Ended` throws.
|
|
- Tests (in `RunTests`, same project, fixture catalog as `LoadoutTests`). Each is paired
|
|
with the mutation that must kill it:
|
|
|
|
| Test | Asserts | Mutation that must kill it |
|
|
|---|---|---|
|
|
| `NewRunSpawnsPilotInEntrance` | pilot is a `Ship` with `IsPlayerShip`, in `Zone.Entities`, `Zone.GalaxyZone == Galaxy.Entrance`; the run store is cleared of a prior `SavedGame` | `New` skips `RunSave.Clear`; spawn omits `Zones.Entities.Add` |
|
|
| `WarpMovesPilotAndSaves` | stepping after `TryEnterWormhole` lands the pilot in the target zone, out of the old zone's entities, with `SavedGame.CurrentZone` equal to the target index after reopen | arrival skips `Save`; `EnterZone` leaves the pilot in the old zone |
|
|
| `ContinueLandsDocked` | save while docked, reopen, `Continue`: `Station` is the saved orbital, `Pilot` is its player child | `Land` binds to the orbital itself |
|
|
| `DeathEndsAndClears` | hull death clears every run record; `Step` throws; `Save` writes nothing | `Died` handler omits `Clear`; `Ended` not set |
|
|
| `PilotAgentFollowsWarp` | a `Minion` set as `PilotAgent` still updates the pilot (position changes) after a warp; `Zone.Agents` of both zones exclude it | agent added to `Zone.Agents` |
|
|
| `WormholesAreZoneOwned` | `Zone.Wormholes.Length == AdjacentZones.Count` and positions follow the ratio | ratio ignored |
|
|
|
|
- Verification:
|
|
- Headless build and tests green.
|
|
- Unity batchmode has no `error CS`.
|
|
- Negative greps:
|
|
- `rg -n "RunSave\.(Clear|Commit|Capture)" Assets/Scripts --glob '!ServerShared/**'` is empty.
|
|
- `rg -n "CurrentGalaxy|IsTutorial\s*=" Assets/Scripts` is empty.
|
|
- `rg -n "new Wormhole" Assets/Scripts` matches only `Zone.cs`.
|
|
- `rg -n "new ItemManager\(" Assets/Scripts` matches only `Run.cs`.
|
|
- `rg -n "ZoneGenerator\.GenerateZone|new Zone\(" Assets/Scripts --glob '!ServerShared/**'` is empty.
|
|
- `rg -n "CurrentEntity\s*=[^=]" Assets/Scripts --glob '!ServerShared/**'` is empty.
|
|
- `rg -n "DateTime\.Now" Assets/Scripts/ServerShared/ItemManager.cs` is empty.
|
|
- Ledger:
|
|
- `ActionGameManager` about -190 +45; `MainMenu` -4 +2; `ZoneRenderer` -9 +3;
|
|
`InventoryPanel` -2 +1; `SectorMap` ±12 renames; `ItemManager` ±1; `Zone` +10;
|
|
`Run.cs` about +190; tests about +200.
|
|
- Statics: -2 +1.
|
|
- Net production code is about +40. That buys the invariant: the lifecycle has one
|
|
owner, and every rule it holds existed before in a MonoBehaviour.
|
|
|
|
### Cut 2: fire control rolls hits; death and loot resolve in `Zone` (forks F, A, W, T, V, E, L)
|
|
|
|
Rulings, 2026-09-17: "Just roll the dice based on weapon stats, targeting system stats
|
|
(new subsystem) and sensor state." And: "We want the player as well as the AI to be able
|
|
to aim for specific subsystems once it has gathered enough target data to reveal those
|
|
subsystems". Anchors are at HEAD `562cdcf2`. Hands' uncommitted `Behaviors.cs` edit
|
|
moves the `BehaviorData` union list down by 10 lines.
|
|
|
|
**Body facts this cut stands on** (**(probe)** of `GameData/Aetheria.cc` read-only, CultLib
|
|
`a0813c6` worktree since removed, plus a GUID grep of effect prefabs):
|
|
- 51 designs, 37 products. `LonginusX` is the only ship hull (ControlModule, Sensors,
|
|
Energy 2, Launcher 2, Radiator 2, Reactor, AetherDrive). `Turret` has Ballistic 2 and
|
|
Sensors 1. `Zenith` has no weapon hardpoints.
|
|
- 18 weapon designs, all single-target, lowered by four effects: `ProjectileManager` 8,
|
|
`LaserManager` 3 (pulse, velocity 0), `GuidedProjectileManager` 5 (3 `GuidedWeaponData`
|
|
-> `InstantWeapon`, 2 `LauncherData` -> `LockWeapon`), `LightningGunManager` 1.
|
|
- **No design uses** `ConstantWeaponData`, `ConstantLaser`, `ConstantLightning`,
|
|
`ConstantParticleWeapon`, `HitscanEffect`, `Mine`, airburst (`Flak Cannon`), MIRV
|
|
splits or `ShieldData`.
|
|
- Sensor state exists: `Sensor.Execute` accumulates `Entity.EntityInfoGathered[other]` in
|
|
[0,1] with decay (`Sensor.cs:121-154`); `TargetDetectionInfoThreshold` (0.1) gates
|
|
`VisibleEntities` (`Entity.cs:196-207`); `LockWeapon.cs:92` scales lock by info.
|
|
- **`TargetArmorInfoThreshold` (0.5) and `TargetGearInfoThreshold` (0.8) are authored and
|
|
read by no code** (`Settings.cs:207-208`, `Settings.asset:320-321`): the reveal tiers
|
|
the ruling describes, unwired.
|
|
- Sensors-hardpoint designs are `not if i see you first`, `The Bat` and `Tractor Beam` (no
|
|
behaviors). `EquipHardpoints` (`LoadoutGenerator.cs:208-228`) can fit the Tractor Beam
|
|
into LonginusX's only Sensors slot; that ship gathers no info, so sees and hits nothing.
|
|
- **What decides a firing arc today** (**(probe)** of the prefab YAML, world transforms
|
|
composed from `m_LocalPosition`/`m_LocalRotation`/`m_Father`, plus a catalog read of
|
|
`HullData.Hardpoints`):
|
|
- Gameplay uses only the barrel's current world forward. `EntityInstance.cs:514-517`
|
|
copies `FiringPoint[0]` (`WeaponHardpoint.cs:7`, matched by name to
|
|
`HardpointData.Transform` at `:268`) into `HardpointTransforms`. `Combat.cs:100-102`
|
|
fires when `dot(barrel, toTarget) > .99` (about 8 degrees). `TurretController.cs:80-83`
|
|
does the same through `Behavior.Direction` (`Behaviors.cs:31-33`). The player's fire is
|
|
not gated at all.
|
|
- Barrels swing through `ArticulationPoint` (`ArticulationPoint.cs`). It yaws and
|
|
pitches toward `LookAtPoint` (`EntityInstance.cs:400-403`, `:519`), clamped to authored
|
|
`YawMin/YawMax/PitchMin/PitchMax` relative to its parent, at `Speed` degrees per
|
|
second. No other arc data exists: no angle field on `HardpointData`, `WeaponData` or any
|
|
effect.
|
|
- Catalog hulls and their weapon hardpoints (all `Rotation None`):
|
|
- `LonginusX` -> `Ships/Longinus.prefab`. `En.L`/`En.R` barrels sit under
|
|
`ArticulationPoint`s with yaw [-1, 1], pitch [-80, 80], speed 60: fixed forward.
|
|
`La.L`/`La.R` tubes have no articulation, forward (0,0,1). Both tubes are authored at
|
|
the same local position (-2.23, 0.35, 0.36), a presentation defect.
|
|
- `Turret` -> `Turrets/Turret.prefab`. `Left Gun`/`Right Gun` barrels sit under a
|
|
`Turret Yaw Pivot` with yaw [-720, 720] (unlimited) and a pitch point [-45, 28],
|
|
speed 15.
|
|
- `Zenith` -> `Stations/Zenith.prefab`: no weapon hardpoints.
|
|
- `Djinni.prefab` carries articulation but no catalog hull references it.
|
|
- `ShipInstance.cs:70` and `EntityInstance.cs:242,257` match thruster, sound and
|
|
radiator transforms by name. They are presentation and do not touch arcs.
|
|
- **Mount direction convention** (matches thrusters). `Behavior.Direction`'s fallback
|
|
(`Behaviors.cs:37`) is `Entity.Direction.Rotate(item.Rotation)` (`Extensions.cs:152-166`):
|
|
`None` = forward, `Reversed` = aft, `Clockwise` = right, `CounterClockwise` = left
|
|
(`Agent.cs:72` takes `Clockwise` as right). A thruster pushes against that vector
|
|
(`Thruster.cs:79`): `None` thrusters are the reverse group and `Reversed` the forward group
|
|
(`Ship.cs:103-107`), `CounterClockwise` the right group (`:109`). A weapon fires along it.
|
|
`Thruster.cs:62`'s `float2(1,0).Rotate(...)` is the torque lever, not the mount direction.
|
|
A hardpoint-mounted item takes the hardpoint's rotation (`Entity.cs:559`), so the mount
|
|
direction is authored as `HardpointData.Rotation`, already catalog data.
|
|
- **Aim is planar.** `Entity.Direction` is `float2`; steering reads `LookDirection.xz`
|
|
(`Ship.cs:278`). Height differences come from terrain and `GridOffset`; pitch exists
|
|
only in `ArticulationPoint`.
|
|
|
|
**Owner: `FireControl`, a new ServerShared static class (`FireControl.cs`).**
|
|
- It owns the engage gate, hit probability, reveal, the roll, cell choice and damage
|
|
application.
|
|
- Its only state is the pending shots under fork F (b)/(c), and that state lives in
|
|
`Zone`.
|
|
- Inputs:
|
|
- weapon stats `Damage`, `Penetration`, `DamageSpread`, `MinRange`, `Range`, `Spread`
|
|
and `Velocity` (`Weapon.cs:22-66`), with `LockWeapon.CanFire` (`:56`) as a gate;
|
|
- the firer's targeting system, or the unaided defaults when it has none;
|
|
- sensor state, `firer.EntityInfoGathered[target]`;
|
|
- `Target` and `TargetItem`;
|
|
- both entities' positions and directions;
|
|
- the weapon's mount direction `Behavior.Direction` and `GameplaySettings.FiringArc`;
|
|
- `ItemManager.Random`, seeded by Cut 1.
|
|
- Outputs:
|
|
- `ShotOutcome { Source; Weapon; Target; bool Hit; bool Shielded; EquippedItem Aimed; int2 Cell; float FlightTime }`;
|
|
- shield `TakeHit`, or `Entity.DamageSchematic` plus `IncomingHit`.
|
|
|
|
**Rules** (defaults unless a fork is named):
|
|
- **Engage gate.** The probability is 0 and no draw is consumed when any of these holds:
|
|
- there is no target;
|
|
- the target is not in `VisibleEntities`;
|
|
- the range is outside [`MinRange`, `Range`];
|
|
- a `LockWeapon` is not locked;
|
|
- the target is outside the firing arc (fork A, ruled):
|
|
`dot(normalize(weapon.Direction.xz), normalize(toTarget.xz)) < cos(radians(FiringArc / 2))`.
|
|
- The test is planar; target height does not enter it.
|
|
- `FiringArc` is full width in degrees, default 120. The boundary is 60 degrees off
|
|
the mount direction.
|
|
- It applies to player and AI fire alike, and to guided weapons.
|
|
- **Hit probability.** `p = Accuracy * pSensor * pSpread`, multiplied by `pDeviation`
|
|
under fork F (c).
|
|
- `pSensor = saturate(unlerp(TargetDetectionInfoThreshold, Resolution, info))`.
|
|
- `pSpread = Spread > 0 ? saturate(angularRadius / (Spread / 2)) : 1`.
|
|
- `angularRadius = degrees(atan(0.5 * max(Shape.Width, Shape.Height) * SchematicCellSize / range))`.
|
|
- `Spread` is already a cone in degrees (`ProjectileManager.cs:18-23`).
|
|
- `SchematicCellSize` is the one new tuning constant. Hands measures it on the
|
|
LonginusX prefab.
|
|
- **Reveal.** `IsRevealed(observer, item)`:
|
|
1. Rank the target's non-hull equipment: hardpoint-mounted first, then size
|
|
descending, then equipment index.
|
|
2. Item `i` of `N` is revealed when
|
|
`info >= lerp(TargetArmorInfoThreshold, TargetGearInfoThreshold, i / max(1, N - 1))`.
|
|
|
|
Ordering is fork V.
|
|
- **Roll.** One draw against `p`. On a hit, a second draw against `Precision` decides
|
|
where it lands:
|
|
- on success, on `Aimed`'s footprint cells;
|
|
- otherwise, on one cell drawn uniformly over the hull `Shape`.
|
|
|
|
`Aimed` is `TargetItem` only while that item belongs to `Target` and is revealed. It
|
|
is derived when read; no loop clears it.
|
|
- **Damage.** The rule moves verbatim from `EntityInstance.cs:334-394`:
|
|
1. Expand the hit shape `round(DamageSpread)` times.
|
|
2. If `Penetration > .5`, march cells along firer->target, rotated into the target's
|
|
frame by `-target.Direction`. This replaces `transform.InverseTransformDirection`.
|
|
3. Apply `DamageSchematic(Damage, shape)` (`:277-314`, moved to `Entity`).
|
|
|
|
- An active shield that `CanTakeHit` takes the hit instead. Every effect repeats that
|
|
rule today (`Projectile.cs:60-71`).
|
|
- `Damage` stays raw. `DamageCurve` still feeds only DPS estimates.
|
|
|
|
**Targeting system (new subsystem).**
|
|
- `TargetingSystemData : BehaviorData` takes union 39. A `TargetingSystem` behavior sits
|
|
on a `GearData` item, mounted per fork T.
|
|
- Its stats are `PerformanceStat`, so lot quality reaches them through
|
|
`EquippedItem.Evaluate`, as it does for weapon stats:
|
|
- `Accuracy` (0..1): the ceiling on hit probability.
|
|
- `Resolution` (detection threshold..1): the info level at which sensor state stops
|
|
limiting hits.
|
|
- `Precision` (0..1): the chance a hit lands on the aimed item.
|
|
- `Tracking`: how much deviation it forgives. Only under fork F (c).
|
|
- Energy and heat come from the existing `EnergyDraw` and `Heat` behaviors on the item.
|
|
- An entity with no targeting system, or with its system offline or destroyed:
|
|
- fires with `GameplaySettings.UnaidedAccuracy`, `Resolution = 1` and `Precision = 0`;
|
|
- can still select a revealed item, but its hits scatter.
|
|
- A schematic hit on the targeting system degrades fire.
|
|
|
|
**Target item: one selection path.**
|
|
- `Entity.TargetItem : ReactiveProperty<EquippedItem>` sits beside `Target`
|
|
(`Entity.cs:40`).
|
|
- `bool Entity.TrySelectTargetItem(EquippedItem)` is the only writer. It accepts null, or
|
|
an item of `Target.Value` that `IsRevealed`.
|
|
- Changing `Target` nulls it.
|
|
- Three callers use it:
|
|
- the player, from a select/cycle command beside `ActionGameManager.cs:366-397`; the
|
|
schematic HUD shows revealed items, which is presentation;
|
|
- the AI, from `CombatState`;
|
|
- the playground, from `aim`.
|
|
|
|
**Per weapon kind.**
|
|
- `InstantWeapon` family:
|
|
- `Execute` (`InstantWeapon.cs:181-191`) calls `FireControl.Fire` for each burst shot.
|
|
- `OnFire` becomes `event Action<ShotOutcome>`.
|
|
- Charge multipliers already fold into `Damage` and `Spread`.
|
|
- Guided weapons (`GuidedWeaponData`, `LauncherData`) use the same roll.
|
|
- Lock time survives as the existing `LockWeapon.CanFire` gate.
|
|
- Flight is presentation.
|
|
- Lightning (`plight`) makes one single-target roll. `HitRadius` is presentation.
|
|
- `ConstantWeapon` rolls once per `GameplaySettings.BeamResolveInterval` for
|
|
`Damage * interval`, raised as `OnBeamShot(ShotOutcome)`. That is about 10 lines, and
|
|
no design uses it yet.
|
|
- Deferred, because no design needs them: airburst splash, MIRV splits, and `Mine`.
|
|
- The earlier draft's `ApplySplash` is not added.
|
|
- `TractorBeam` is not a hit. It stays Unity physics.
|
|
|
|
**AI and turrets.**
|
|
- `Combat.cs:100-102`:
|
|
- `shouldFire = FireControl.HitProbability(testWeapon, target) >= Settings.AgentMinHitProbability`.
|
|
- The `HardpointTransforms` read is deleted.
|
|
- `AgentMinHitProbability` replaces `AgentFiringMinDot`, which nothing reads.
|
|
- `:84-98` (looking at the predicted intercept) survives only under fork F (c).
|
|
- Subsystem policy: through `TrySelectTargetItem`, select the revealed target `Weapon`
|
|
item with the highest `RangeDamagePerSecond(range)`, or null if there is none.
|
|
- `TurretController.cs:80-83` uses the same gate and threshold; its `.99f` dot is deleted.
|
|
- `HardpointTransforms` is deleted everywhere:
|
|
- `Entity.HardpointTransforms` (`Entity.cs:52-53`);
|
|
- its writer, `EntityInstance.cs:514-517`;
|
|
- its reader branch in `Behavior.Direction` (`Behaviors.cs:31-34`).
|
|
- `Behavior.Direction` keeps only its item-rotation path (`Behaviors.cs:37-38`). It is the
|
|
one mount-direction reader, the same in both lowerings, and nothing extracts direction
|
|
from prefabs.
|
|
- `Barrels` and `ArticulationPoint` stay in Unity for `GetBarrel`, crosshairs and barrel
|
|
animation. A shot rolled 60 degrees off a fixed Longinus barrel still flies from that
|
|
barrel; that is presentation.
|
|
- Under the default arc, turrets (hull `Direction` never rotates; `TurretController`
|
|
writes only `LookDirection`) engage only within 60 degrees of their spawn facing. That is
|
|
fork W.
|
|
|
|
**Death, loot and pickup** (carried from the earlier draft). Loot into the simulation did
|
|
not land with this cut; cut `loot-1` of campaign `aetheria-release` owns it now (ruling
|
|
`jettison-shared-floating-items`).
|
|
- `Zone` subscribes to the `Death` of each entity it adds. On death it:
|
|
- removes the entity and its agent;
|
|
- drops each non-hull item with probability `LootDropProbability`, and all cargo,
|
|
using `ItemManager.Random`;
|
|
- adds the drops to
|
|
`Zone.Loot : ReactiveCollection<LootDrop { ItemInstance Item; float3 Position; float2 Velocity }>`.
|
|
- `LootDropProbability` and `LootDropVelocity` move from `GameSettings.cs:19-20` to
|
|
`GameplaySettings` (fork S).
|
|
- `Run.Died` reads no zone membership, so subscription order does not matter.
|
|
- `bool Zone.TryPickUp(Entity, LootDrop)` stores the item in the first bay that accepts
|
|
it and removes the drop. Contact detection is fork L.
|
|
|
|
**Unity becomes presentation.**
|
|
- `InstantWeaponEffectManager.Fire` (`:7`) takes the `ShotOutcome`.
|
|
- Effects fly to the rolled cell's world point, or to a miss offset, and play the
|
|
impact on arrival.
|
|
- They apply nothing.
|
|
- Hit loops, `SendHit` and `TakeHit` are deleted from the four used effects:
|
|
- `Projectile.cs:57-93,99-112`;
|
|
- `GuidedProjectile.cs:152-189`;
|
|
- `Laser.cs:40-66`;
|
|
- `Lightning.cs:34-52`; its endpoint capture stays as a visual.
|
|
- `HullCollider.cs:13-41,48-65`: `Hit`, `Splash`, `SendHit`, `SendSplash` and their
|
|
argument classes are deleted. `OnCollisionEnter` (ship collision) stays, deferred.
|
|
- `EntityInstance.cs`:
|
|
- `:277-396` is deleted;
|
|
- `:405-437` keeps only the destroy effect.
|
|
- `ZoneRenderer.DropItem` lowers `Loot.ObserveAdd`.
|
|
- `ShieldManager.cs:25-41` calls `Zone.TryPickUp`. `Loot.ObserveRemove` destroys the
|
|
pickup.
|
|
|
|
**Catalog and loadouts.**
|
|
- Add two targeting-system designs, 1-cell and 2-cell.
|
|
- Each has products from at least the manufacturers that sell the capacitor.
|
|
- The capacitor is the existing required interior item, so this lets `IsAvailable` find
|
|
one in every galaxy.
|
|
- `LoadoutGenerator.FillInterior` (after `:250-260`) picks one with `required: true` for
|
|
every entity that has `Weapons`:
|
|
- the starting LonginusX, which uses the same generator;
|
|
- NPC ships and turrets.
|
|
|
|
Zenith gets none.
|
|
- Authoring follows the Cut C precedent:
|
|
- a Hands scratch console upserts the records;
|
|
- Unity and CultCache Studio are closed;
|
|
- whether Studio renders union 39 is an operator check.
|
|
- **This lands after provenance Cut C.** Cut C re-upserts every `ItemData` and deletes
|
|
`ItemData.Manufacturer`.
|
|
- `AetherDb` captures before and after:
|
|
- `census`: 2 more designs;
|
|
- `loadout 1`: 1 more interior item per armed entity;
|
|
- `dangling`: 0;
|
|
- on reopen: 0 schema reports.
|
|
|
|
**Tests** (`FireControlTests`, `ZoneDeathTests`). Each is paired with the mutation that
|
|
must kill it:
|
|
|
|
| Test | Asserts | Mutation that must kill it |
|
|
|---|---|---|
|
|
| `RollsAreSeeded` | two identical zones, same seed, 50 shots: identical `ShotOutcome` sequences | `FireControl` draws from `new Random()` |
|
|
| `ProbabilityFollowsInputs` | `p` is 0 below detection and outside range; rises with info up to `Resolution`; falls with `Spread`; is capped at `UnaidedAccuracy` with no system | drop `pSensor`; drop the range gate |
|
|
| `AimedHitLandsOnSelectedItem` | with `Precision` 1 and `p` 1, only the aimed item's durability falls | the roll ignores `Aimed` |
|
|
| `SelectionNeedsReveal` | below the item's tier `TrySelectTargetItem` is false; above it, true; a target change nulls it; info decaying below the tier makes `Aimed` null | `IsRevealed` returns true; the `Target` change skips the null |
|
|
| `RevealOrder` | hardpoint items reveal before interior items, larger before smaller | ordering dropped |
|
|
| `HardpointHitDamagesItemThenHull` | damage above armor plus item durability zeroes the item, and the remainder hits the hull | armor not subtracted; hull skipped |
|
|
| `ShieldTakesHit` | an active shield fixture absorbs the hit; the schematic is untouched | shield branch removed |
|
|
| `ArcFollowsMountRotation` | fixture hull with one weapon on a `None` hardpoint and one on a `Clockwise` hardpoint; target 90 degrees to the right: only the `Clockwise` weapon has `p > 0`; target dead ahead: only the `None` weapon | the gate uses `Entity.Direction` instead of `weapon.Direction`; `Rotate`'s `Clockwise`/`CounterClockwise` cases swapped |
|
|
| `ArcBoundaryIsHalfWidth` | `FiringArc` 120: a target 59 degrees off the mount has `p > 0`, one at 61 degrees has `p == 0` and consumes no draw | compare against `cos(radians(FiringArc))`; drop the arc gate |
|
|
| `ArcIsPlanar` | a target inside the arc horizontally but well above the firer still has `p > 0` | the gate uses the 3D `toTarget` |
|
|
| `CombatStateKillsHeadless` | a `Minion` with a targeting system and a target in range steps headless without throwing and lowers target durability | restore `Combat.cs:101`; the threshold compares against 2 |
|
|
| `NpcDeathRemovesEntityAndAgent` | entity and agent are gone after death | removal omitted |
|
|
| `LootDropsAreSeeded` | same seed, identical drop lists | the drop roll uses a clock RNG |
|
|
| `PickUpStoresAndRemoves` | the item is stored and the drop removed; a full bay keeps the drop | drop removed on failure |
|
|
| `ShotResolvesOnArrival` (F (b)/(c)) | no damage before `range / Velocity`; a target removed before arrival takes none | damage applied at fire time |
|
|
|
|
**Playground.** Cut 3's `hit` command is withdrawn (fork D).
|
|
- New command `aim <item#>|none`, and new fact `target.revealed`.
|
|
- `provenance-c.play` kills by fire:
|
|
1. `target nearest`
|
|
2. `pilot minion`
|
|
3. `step <s>`
|
|
4. `expect zone.entities == <n-1>`
|
|
5. `pickup all`
|
|
6. `expect cargo.count > 0`
|
|
|
|
Hands fixes `<s>` after the first seeded run.
|
|
- In `lifecycle.play`, the pilot dies by parking unarmed at an armed NPC and stepping
|
|
until `expect ended == true`.
|
|
|
|
**Verification.**
|
|
- The headless build and tests are green, and each mutation above goes red.
|
|
- Two playground runs give byte-identical traces.
|
|
- These negative greps are each empty:
|
|
- `rg -n "HardpointTransforms" Assets/Scripts`
|
|
- `rg -n "\.99f" Assets/Scripts/ServerShared/Agents/States/Combat.cs Assets/Scripts/ServerShared/Behaviors/TurretController.cs`
|
|
- `rg -n "ArticulationPoint|FiringPoint" Assets/Scripts/ServerShared`
|
|
- `rg -n "SendHit|SendSplash|DamageSchematic|TakeHit\(|Durability\s*[-+]?=" Assets/Scripts --glob '!ServerShared/**'`
|
|
- `rg -n "Physics\." Assets/Scripts/Gameplay/Weapons Assets/Scripts/Gameplay/HullCollider.cs`
|
|
- `rg -n "Random\.value|onUnitSphere" Assets/Scripts/Gameplay/EntityInstance.cs`
|
|
- `rg -n "Entities\.Remove" Assets/Scripts --glob '!ServerShared/**'`
|
|
- Unity batchmode with the editor closed (`docs/item-provenance-cut.md` §6) reports no
|
|
`error CS`.
|
|
- Operator checks in Unity:
|
|
- shots show rolled impacts and misses;
|
|
- damage matches the HUD;
|
|
- reveal and selection work;
|
|
- kills drop loot, and pickup stores it;
|
|
- the tractor beam still pulls.
|
|
|
|
**Risks.**
|
|
- The Tractor Beam can take LonginusX's only Sensors slot; that NPC never sees or fights.
|
|
Check `loadout` over several seeds. The fix is data or `EquipHardpoints`, not fire
|
|
control.
|
|
- Balance numbers (`UnaidedAccuracy`, `SchematicCellSize`, stat ranges) are first guesses;
|
|
the playground is the tuning harness.
|
|
|
|
**Ledger.** `EntityInstance` about -155 +8; `HullCollider` -35; the four used effects
|
|
-75 +35; `ShieldManager` -8 +3; `Combat` -5 +8; `TurretController` ±4; `Behaviors` -8;
|
|
`Entity` -2 +70; `InstantWeapon`/`ConstantWeapon` ±15; `FireControl.cs` +165 (arc gate);
|
|
`TargetingSystem.cs` +45; `Zone` +50; `LoadoutGenerator` +12; `Settings` +6 -1; tests
|
|
+360; catalog 2 designs plus products. Fork E (b) deletes a further ~560. Net production
|
|
about +80, or -480 with E (b). It buys the invariant: combat resolves without Unity and
|
|
every hit rule has one owner.
|
|
|
|
**Cut 2 forks**, ranked by what they block:
|
|
|
|
**F. When a shot resolves.** Blocks the `FireControl` API and the tests.
|
|
|
|
**Ruled (c) (operator, 2026-09-17: "I like c"):** the shot resolves on arrival, and its hit
|
|
chance falls with how far the target deviated from the predicted intercept.
|
|
|
|
- The scope doc says projectiles "resolve on arrival against how far the target
|
|
deviated". The ruling names only weapon, targeting and sensor inputs, and does not
|
|
settle this either way.
|
|
- (a) Roll and apply at fire time. The smallest change; visuals arrive late, and
|
|
maneuvering does not evade.
|
|
- (b) Roll at fire time, and apply after `range / Velocity` from a pending queue in
|
|
`Zone`. Visuals and damage agree, but there is still no evasion.
|
|
- (c) (b), plus a deviation factor at arrival:
|
|
`pDeviation = saturate(1 - distance(target, predicted intercept) / (hullRadius + Tracking))`.
|
|
It keeps the scope doc's evasion and the look-at-intercept code, for about 25 more
|
|
lines.
|
|
- **Recommend (c).** It is the scope doc's standing design, and it fits the ruling as one
|
|
more factor. If "just roll the dice" meant dropping evasion, take (b).
|
|
|
|
**A. Firing arc.** Blocks `CombatState` and how the player's ship feels.
|
|
|
|
**Ruled (operator, 2026-09-17), in two steps.** First: "Firing arcs are determined per
|
|
hardpoint, that data is unfortunately baked into the ship prefabs at the moment". Then: "I'd
|
|
say default to 120 degrees, use existing item rotation direction to determine the mount
|
|
direction, like how the reaction thrusters work". Then: "simulation is fundamentally 2D, all the 3D-ness is
|
|
set dressing". So the arc check is planar by rule, not by approximation. `Entity.Position` is
|
|
a `float3`, so `ArcIsPlanar` stays: it pins that height never decides a shot.
|
|
|
|
- The mount direction is the item's `ItemRotation` applied to the ship's `Direction`, the
|
|
thruster convention (Body facts above). It is per hardpoint because a mounted item takes
|
|
`HardpointData.Rotation`.
|
|
- The arc is `GameplaySettings.FiringArc`, 120 degrees full width, one authored value.
|
|
- Nothing is extracted from prefabs. `FiringPoint` and `ArticulationPoint` become
|
|
presentation. The prefabs carry no arc width a catalog field would preserve, except the
|
|
turret's unlimited yaw (fork W).
|
|
- No `HardpointData` field is added unless fork W takes (a).
|
|
|
|
Options as mapped before the ruling:
|
|
- (a) No arc; facing is presentation.
|
|
- (b) Gate on `dot(weapon.Direction, toTarget) >= FireArcDot`, one authored setting. The
|
|
ship must roughly face its target, and player and AI share the gate.
|
|
- (c) The arc as a probability factor.
|
|
- **Recommend (b), with a wide arc.** It keeps piloting meaningful without precision
|
|
aiming. (a) is right if articulated turrets are meant to cover every hardpoint.
|
|
|
|
**W. Turret arcs under the 120 degree default.** Blocks `TurretController` and the turret
|
|
catalog record.
|
|
- The turret prefab yaws without limit (`Turret Yaw Pivot`, yaw [-720, 720]). Headless, the
|
|
turret hull's `Direction` stays at spawn facing, so a 120 degree arc on its `None` guns
|
|
turns today's all-round turret into a forward-only one.
|
|
- (a) Per-hardpoint override: `HardpointData.FiringArc : float?` (`Key(6)`, absent means
|
|
`GameplaySettings.FiringArc`). Author 360 on the turret's `Left Gun` and `Right Gun` in
|
|
Cut 2's catalog upsert, after provenance Cut C. One field, with prefab evidence for its
|
|
one consumer. `AetherDb` reopen still reports 0 schemas; test
|
|
`ArcOverrideReplacesDefault` (a 360 hardpoint fires at a target behind; mutation: the
|
|
override is ignored).
|
|
- (b) `TurretController` rotates the turret entity's `Direction` toward `LookDirection` at
|
|
a slew rate, so the default arc tracks. That adds headless articulation state, and turrets
|
|
that can be flanked by speed.
|
|
- (c) Accept forward-only turrets.
|
|
- **Recommend (a).** It is the smallest shape that keeps the authored turret behaviour, and
|
|
it leaves ships on the default.
|
|
|
|
**Ruled (a) (operator, 2026-09-17):** "a, turrets get 360 degrees; we want turrets on some ships
|
|
too, after all". `HardpointData.FiringArc` is an optional per-hardpoint override, where
|
|
empty means the `GameplaySettings` default. It is set to 360 on turret hardpoints. Because
|
|
ship hulls will also carry turret hardpoints, the override belongs to the hardpoint, not to
|
|
the turret entity type.
|
|
|
|
**T. Where the targeting system mounts.** Blocks the catalog and loadout work.
|
|
|
|
**Ruled (a) (operator, 2026-09-17):** "absolutely as interior gear, Tool is fine". It is
|
|
interior `GearData` with `HardpointType.Tool`, placed by `FillInterior`, and can be revealed,
|
|
targeted and refitted like any other item.
|
|
|
|
- (a) Interior `GearData` (`HardpointType.Tool`), placed by `FillInterior` like the
|
|
capacitor. No hull or prefab edits.
|
|
- (b) A behavior on the control-module designs (`Cockpit 2x2`, `Turret Control Module`).
|
|
No new item, but then it is not a separate subsystem to hit or refit.
|
|
- (c) The Sensors hardpoint. It competes for the one sensor slot.
|
|
- **Recommend (a).**
|
|
|
|
**V. Reveal ordering.** Blocks `RevealOrder` only; a default exists.
|
|
- (a) Hardpoint-mounted, then size, then index. Static, so reveal does not flicker.
|
|
- (b) Rank by each item's current share of `VisibilitySources`, so emitters reveal first.
|
|
Reveal flickers as emissions change.
|
|
- (c) Reveal every item at `TargetGearInfoThreshold`, with no ranking.
|
|
- **Recommend (a).** Emissions already drive how fast info accumulates.
|
|
|
|
**E. The six effect families no design uses.** Blocks only the Unity-side ledger.
|
|
|
|
**Ruled: keep every weapon and behavior type (operator, 2026-09-17).** None of (a)-(c). Operator's words:
|
|
"careful with the deletion beam, we had TONS of content in the game before the big breach, using every
|
|
single behavior type. I rebuilt just the bare minimum content to make the game playable, that doesn't
|
|
mean we want to delete all the weapon types we created".
|
|
- Today's catalog is the minimum rebuilt after the breach, not the consumer set.
|
|
The consumers are the pre-breach content.
|
|
The consumer audit for weapon and behavior types reads that history, not the live catalog.
|
|
- Fire control maps every weapon and behavior type: constant beams, hitscan, lightning, guided,
|
|
airburst, missile splits, mines and shields.
|
|
Cut 2 has nothing to delete or defer on the grounds that no current design uses a type.
|
|
|
|
- (a) Strip their hit loops and keep them.
|
|
- (b) Delete their scripts, managers and prefabs once a GUID grep shows no catalog or
|
|
scene reference. That covers `ConstantLaser*`, `ConstantLightning*`,
|
|
`ConstantParticleWeapon*`, `Hitscan*`, `Mine*`, and `ShieldManager`'s mine branch
|
|
(`:42-45`).
|
|
- **Recommend (b).** About 560 lines that serve nothing.
|
|
|
|
**L. Loot contact.** Low-blocking.
|
|
|
|
**Ruled (operator, 2026-09-17): pickup is a capability behavior, a timed grab.**
|
|
- **The owner is simulation.** A pickup is a timed grab of a selected item in range: extend,
|
|
envelop, pull. When it completes, the zone commits the item. Range, duration and
|
|
cancellation are rules in `ServerShared`, shared by the player, the AI and the playground.
|
|
- **No Unity collision decides a pickup.** The collision path in `ShieldManager` is retired.
|
|
- **Field shields are not universal.** Operator: "Each of the things that field shield can do
|
|
should be individual capabilities, which can be tied to a distinct visual presentation."
|
|
Impact absorption, pickup, melee blade and propulsion effect are separate capabilities.
|
|
- **The tractor beam item provides only pickup.** Operator: "it might have some volumetric
|
|
glowy vfx but it would behave the same as the field shield's pickup ability".
|
|
- **`FieldDriver` is one presentation** of these capabilities, not an owner of any.
|
|
- **The tractor beam is a generic Tool item** (operator, 2026-09-17: "Just put tractor behavior
|
|
on a generic tool item"). It is interior `HardpointType.Tool` gear, like the targeting system,
|
|
so it no longer competes with the sensor for LonginusX's single Sensors slot. The catalog
|
|
change lands with the pickup capability.
|
|
- **Presentation is chosen per item, independently of capability** (operator, 2026-09-17). One
|
|
presentation, such as the field shield, can visualize any subset of an item's capabilities:
|
|
only propulsion, only shielding, or several at once. Other presentations (an IFS shatter
|
|
plane, flow particles) can visualize the same capability events. Simulation emits capability
|
|
events; presentations subscribe and decide nothing.
|
|
- **Absorption has a damage type mask** (operator, 2026-09-17). The absorb capability's data
|
|
declares which `DamageType`s it absorbs, and a hit whose type is outside the mask is not
|
|
absorbed. `DamageType` (`Enums.cs:110`) is not a flags enum; the capability cut decides the
|
|
mask's representation. Today `Shield.CanTakeHit` ignores the type entirely. The presentation
|
|
seam's `AbsorbEvent` already carries the hit's `DamageType`.
|
|
- **Simulation owns the grabbed object and its pose** (operator, 2026-09-17: "why would the grab
|
|
object not exist in the engine-free sim? It was floating free before, and the grab transition
|
|
moves it into the ship's local space").
|
|
- A loot object is simulation state in zone space.
|
|
- The grab phases move its pose from zone space into ship-local space. On completion the item
|
|
enters cargo; on cancellation it floats free again with its current velocity.
|
|
- `GrabEvent` names the loot object by a typed simulation identity, and presenters read the
|
|
simulated pose.
|
|
- Deletion line, in the pickup capability cut: the presentation seam's int target handle,
|
|
`IGrabPresenter`'s Transform and velocity parameters, the binder's grab resolver delegates,
|
|
and `FieldDriver` moving the grabbed transform itself.
|
|
- **Bodies, and one detection interface (operator, 2026-09-17).**
|
|
- **Kinematic bodies** are moved by forces. `ServerShared` gets a `Body` (position, velocity,
|
|
response to force, detection signature) that `Zone` steps under `GetForce`.
|
|
- `Entity` has a `Body`.
|
|
- Loot is a body plus an item instance.
|
|
- A mine is a body plus a payload, and carries its launcher's transponder credential.
|
|
- Hostility, stance, equipment and heat stay on `Entity`. A loot object or mine is not an
|
|
`Entity`, because `Entity` keeps pairwise state with every other entity in the zone.
|
|
- **Rail bodies** (planets, gas giants, suns, asteroids) keep analytic positions as a function
|
|
of time. They generate the gravity field and are not integrated.
|
|
- **Rail and kinematic bodies share one detection interface:** identity, position and
|
|
signature. Sensors detect through it, so the detection-gated stance query and later
|
|
electronic warfare cover every kind of body alike. Operator: "rail bodies and kinematic
|
|
bodies should share a detection interface. With good enough signature masking I should be
|
|
able to float along in a debris field pretending to be junk."
|
|
- (a) `Zone.TryPickUp` is the one commit.
|
|
- Contact detection stays Unity collision, per the scope doc's deferral.
|
|
- The playground's `pickup all` matches drops by spawn position within
|
|
`DockingDistance`.
|
|
- (b) `Zone` owns drop motion and proximity pickup every step, and Unity renders it. That
|
|
pulls `TractorBeam` headless too.
|
|
- **Recommend (a).** Say plainly that contact has two detectors until the tractor beam
|
|
moves.
|
|
|
|
### Cut 3: `AetherDb play`
|
|
|
|
- `AetherDb.cs`: `Open` takes `dataDir`. Run and player paths come from it. It defaults
|
|
to `GameData` for the existing commands and is required for `play`.
|
|
- `Program.cs`: `case "play": return Play(args)`.
|
|
- Usage: `play <scenario-file|-> --data <dir> [--seed <u>]`.
|
|
- `play` refuses a `--data` that resolves to `<root>/GameData`.
|
|
- New `tools/AetherDb/Playground.cs`. It is a line interpreter over `Run` with no
|
|
simulation rules of its own. One command per line; `#` starts a comment.
|
|
|
|
| Command | Calls |
|
|
|---|---|
|
|
| `new [tutorial] [galaxy-seed=<u>]` | `Galaxy(...)` from authored settings, then `Run.New` |
|
|
| `continue` | `Run.Continue`; fails the scenario when null |
|
|
| `reopen` | dispose cache, `AetheriaStores.Open` again (quit/relaunch; the layer where §2.6 shows) |
|
|
| `step <seconds>` | `Run.Step(1/60)` repeated |
|
|
| `move <x> <y>` / `look <x> <z>` / `tractor <0..1>` | pilot entity fields, as `ActionGameManager.cs:1189-1216` |
|
|
| `target nearest\|none\|<entity#>` | `Pilot.Target.Value` |
|
|
| `fire <group> on\|off` | `WeaponGroups[g].weapons` Activate/Deactivate, as `ActionBarSlot.cs:190-204` |
|
|
| `gear <equipment#> on\|off` | `IActivatedBehavior` Activate/Deactivate |
|
|
| `pilot manual\|minion\|patrol\|goto station <i>\|goto wormhole <i>` | `Run.PilotAgent` |
|
|
| `dock` / `undock` / `board <entity#>` / `warp` / `tow` | `Run.TryDock` / `Undock` / `Board` / `TryEnterWormhole` / `Tow` |
|
|
| `hit <entity#> <damage> [hardpoint <k>]` | `Entity.ApplyHit` (Cut 2) |
|
|
| `pickup all` | `Zone.TryPickUp` for drops within `DockingDistance` |
|
|
| `save` | `Run.Save(empty)` |
|
|
| `expect <fact> <op> <value>` | facts: `ended`, `zone`, `docked`, `pilot.hull`, `zone.entities`, `run.savedgame`, `lots.absent`, `lots.mismatched`, `cargo.count` |
|
|
| `state` | trace one state line |
|
|
|
|
- `goto` needs `MoveToPositionState : MoveToState` in `MoveTo.cs` (about 6 lines),
|
|
wrapped in a one-state `Agent`. It is the only new agent code.
|
|
- The trace goes to stdout, one line per step command and per `Run` event:
|
|
`t=<zone time> zone=<name> <event> k=v…`. `state` adds pilot position, velocity,
|
|
hull, target, docked, entity and agent counts, lot count and `run.cc` bytes.
|
|
- An exception prints its type, message and first frame, then ends the scenario.
|
|
- Exit code is failed expectations plus escaped exceptions.
|
|
- `lots.absent` and `lots.mismatched` reuse the checks in `Program.cs:399-408`. They
|
|
are extracted to one method that `save` also calls, not copied.
|
|
- First smoke, `tools/AetherDb/scenarios/lifecycle.play`, checked in:
|
|
|
|
```
|
|
new galaxy-seed=42
|
|
state
|
|
move 0 1
|
|
step 5
|
|
pilot goto station 0
|
|
step 60
|
|
dock
|
|
expect docked == true
|
|
undock
|
|
pilot goto wormhole 0
|
|
step 60
|
|
warp
|
|
step 5
|
|
expect run.savedgame == true
|
|
reopen
|
|
continue
|
|
expect zone == <the warp target, printed by the trace>
|
|
expect lots.absent == 0
|
|
expect lots.mismatched == 0
|
|
target nearest
|
|
pilot minion
|
|
step 30
|
|
hit pilot 100000
|
|
expect ended == true
|
|
expect run.savedgame == false
|
|
```
|
|
|
|
- Hands fixes the literal zone name after the first run. The scenario checks that it
|
|
reproduces, not what it is.
|
|
- Verification:
|
|
- `dotnet run --project tools/AetherDb -- play tools/AetherDb/scenarios/lifecycle.play --data <scratch>`
|
|
exits 0.
|
|
- Running it twice gives byte-identical traces.
|
|
- Mutation: revert Cut 0, and the scenario exits non-zero at `continue` with the
|
|
`int2` message.
|
|
- Mutation: `Run.Died` omits `Clear`, and `expect run.savedgame == false` fails.
|
|
- Negative: `rg -n "RunSave\.|ZoneGenerator|\.Position\s*=|Durability\s*=" tools/AetherDb/Playground.cs`
|
|
is empty.
|
|
- Ledger: `Playground.cs` about +220; `AetherDb.cs` ±4; `Program.cs` +3 and ±10
|
|
(extraction); `MoveTo.cs` +6; scenario +26. No new target.
|
|
|
|
### Cut 4: consumer scenarios
|
|
|
|
These are scenario files only; no code.
|
|
- `cultcache-smoke.play` covers migration smoke steps 3-8 minus the rendering and UI
|
|
parts:
|
|
- new, warp, warp again with the `SavedZone` count constant (`expect run.savedzones == <n>`, a new fact);
|
|
- reopen and continue on the same zone and entity;
|
|
- reopen before any step and check every visited zone still holds contents;
|
|
- die, then the run is cleared.
|
|
- Steps 1-2 (`player.cc` from settings UI), 3's input screen and 9 (loadout UI) stay
|
|
operator-only.
|
|
- `provenance-c.play` covers the Cut C checks:
|
|
- new; kill an NPC with `hit`; `pickup all`;
|
|
- `expect cargo.count > 0`;
|
|
- warp; reopen; continue;
|
|
- `expect lots.absent == 0`, `expect lots.mismatched == 0`;
|
|
- die; `expect run.savedgame == false`.
|
|
- Tier colour, Manufacturer row, trade purchase and Studio rendering stay
|
|
operator-only.
|
|
|
|
## 5. Forks
|
|
|
|
**R. Reopen defect.** Blocking: every Continue and two HEAD tests.
|
|
- (a) `PersistedBehaviors` as a pairs array. This is local, matches the sibling pack
|
|
fields, and saves are discarded anyway.
|
|
- (b) CultLib: a hash-resistant comparer for CultMath value types in
|
|
`CultMessagePackSecurity`. That is a foundation change with a new pin and a Unity
|
|
package release, and it keeps a map for 0-12 entries.
|
|
- (c) Key persisted behaviors by equipment index. It saves bytes but couples to
|
|
equipment order, which `Pack` does not promise.
|
|
- **Recommend (a).**
|
|
|
|
**P. Where the playground lives.**
|
|
- (a) `AetherDb play`. It already has settings, stores and the save checks.
|
|
- (b) Scenarios only as xunit tests. They cannot use authored settings without dragging
|
|
YamlDotNet into tests, and agents cannot puppeteer interactively.
|
|
- (c) A new `tools/AetherPlay` executable, which duplicates store and settings loading
|
|
for no second consumer.
|
|
- **Recommend (a),** with lifecycle rules tested in xunit (Cut 1 and 2 tests) and
|
|
sessions in `play`.
|
|
|
|
**H. How `CombatState` decides to fire.**
|
|
- (a) Read `Weapon.Direction`, the same reader the shot uses.
|
|
- (b) A ServerShared articulation model owning barrel aim, with Unity rendering it. That
|
|
is correct under gimbals but a new subsystem with no playground need yet.
|
|
- (c) The playground writes `HardpointTransforms`. Rejected: a second writer
|
|
compensating for a missing owner.
|
|
- **Recommend (a).**
|
|
- **Superseded by the fire-control ruling (Cut 2).** Nothing reads barrel aim for
|
|
gameplay. `CombatState` fires when `FireControl.HitProbability` clears
|
|
`AgentMinHitProbability`, and `HardpointTransforms` is deleted. Fork A (firing arc)
|
|
replaces this question.
|
|
|
|
**D. How far combat goes headless.**
|
|
**Ruled (operator, 2026-09-17): none of (a)-(c).** Hit detection goes away entirely, as
|
|
`docs/three-gates-scope.md` "Fire control" already planned: "We wanted to get rid of hit
|
|
detection anyway, remember? Just roll the dice based on weapon stats, targeting system
|
|
stats (new subsystem) and sensor state." Cut 2 is re-mapped as fire control in
|
|
`ServerShared`: a hit is a roll over weapon stats, the stats of a new targeting-system
|
|
subsystem, and sensor state. The playground's `hit` stand-in is not needed.
|
|
A second ruling the same day adds subsystem aim: "We want the player as well as the AI to be
|
|
able to aim for specific subsystems once it has gathered enough target data to reveal those
|
|
subsystems".
|
|
|
|
Consequences:
|
|
- Cut 2 is fire control plus the parts of (a) that survive: `DamageSchematic` moves to
|
|
`Entity`, and death removal, loot roll and the pickup commit move to `Zone`.
|
|
- Effects and `HullCollider` stop deciding damage.
|
|
- (b)'s "two truths about hits" cannot arise, because Unity detects nothing.
|
|
- Cut 3's `hit <entity#>` command and its §3 forbidden-writer exception are withdrawn.
|
|
Cut 3's command table, `lifecycle.play` (`hit pilot 100000`) and Cut 4's
|
|
`provenance-c.play` ("kill an NPC with `hit`") still name it. They are to be re-mapped
|
|
to fire (`target`, `aim`, `fire`/`pilot minion`, `step`) per Cut 2's Playground note.
|
|
- §7's "`hit` is not ballistics" risk becomes "rolled balance is not tuned".
|
|
- New forks F, A, T, V, E and L live in Cut 2. Fork H is superseded.
|
|
|
|
Options as mapped before the ruling:
|
|
- (a) Move damage application, death removal, loot roll and pickup into ServerShared;
|
|
Unity keeps hit detection; the playground's `hit` supplies hits.
|
|
- (b) (a) plus headless ballistics. That is a projectile simulation Unity would have to
|
|
adopt, or else two truths about hits. It needs its own map.
|
|
- (c) Nothing moves; the playground's fights stop at `OnFire`, and the Cut C
|
|
kill-and-loot check stays operator-only.
|
|
- **Recommend (a).** The operator asked for fight and loot. Without (a) or (b) the
|
|
playground cannot kill anything by the rules the game uses.
|
|
|
|
**S. The two Unity-only lifecycle settings.**
|
|
- **Superseded 2026-09-17** by `docs/settings-globals-cut.md` (operator: "All settings should be in globals"). Settings move to a catalog global read through CultCache; `AuthoredSettings` and the YAML reader are deleted, so the options below, the `Run.New` settings arguments and the `AuthoredSettings` uses above are history. Re-map this cut against that one before Hands.
|
|
- (a) Move `StartingHullName`, `WormholeDistanceRatio` and (Cut 2) `LootDrop*` into
|
|
`GameplaySettings`, with a text edit of `Settings.asset` while Unity is closed.
|
|
- (b) Pass them as `Run` constructor arguments read from `GameSettings`; the playground
|
|
reads them by name from YAML. That leaves two readers of one authored value.
|
|
- (c) A new `RunRules` bundle, which is a type with no owner the existing settings class
|
|
lacks.
|
|
- **Recommend (a).**
|
|
|
|
**N. Is the scene handoff static acceptable?** Low-blocking.
|
|
- (a) `ActionGameManager.NewRun : (Galaxy, bool)?`, consumed once by `Start`.
|
|
- (b) `MainMenu` builds the whole `Run` on its generation thread. That moves zone
|
|
generation's run-store upserts off the main thread, and the `CultCache` threading
|
|
contract is not checked here.
|
|
- **Recommend (a).**
|
|
|
|
## 6. Subtraction ledger
|
|
|
|
| Cut | Removed | Added | Targets, schemas, stores |
|
|
|---|---|---|---|
|
|
| 0 | `Dictionary<int2,…>` field and map-building pack code, ~6 | pairs pack/unpack ~6; test ~25 | `EntityPack` key 5 wire shape map -> array; no new schema |
|
|
| 1 | `ActionGameManager` ~190 (incl. dead `IntroCutscene` 28), 2 statics, `MainMenu` 4, `ZoneRenderer` 9, `InventoryPanel` 2, clock seed | `Run.cs` ~190; `Zone` +10; AGM handlers ~45; 1 static; tests ~200 | 0 targets; `Settings.asset` two keys move |
|
|
| 2 | `EntityInstance` ~155, `HullCollider` ~35, used effects' hit loops ~75, `HardpointTransforms` and its readers ~15, `AgentFiringMinDot`; fork E (b) a further ~560 | `FireControl.cs` ~160; `TargetingSystem.cs` ~45; `Entity` ~70; `Zone` ~50; effects ~35; `LoadoutGenerator` ~12; tests ~320 | 0 targets; `BehaviorData` union 39; 2 catalog designs plus products (after provenance Cut C); `LootDrop*` keys move; `SchematicCellSize`, `UnaidedAccuracy`, `BeamResolveInterval`, `AgentMinHitProbability`, `FiringArc` added; fork W (a) adds `HardpointData` key 6 and turret overrides |
|
|
| 3 | extracted duplicate of the `save` lot check | `Playground.cs` ~220; `MoveTo` +6; scenario 26 | 0 targets; `AetherDb` gains one command |
|
|
| 4 | — | 2 scenario files | — |
|
|
|
|
Net production code across Cuts 0-3 is about +340, or about -220 if fork E (b) deletes the
|
|
unused effects. Tests are about +540. No executable, package, daemon or store is added.
|
|
Rules move from MonoBehaviours to ServerShared. The genuinely new behavior is:
|
|
- `MoveToPositionState` and the scenario interpreter;
|
|
- fire control's roll, which replaces physics hit detection per the operator's ruling;
|
|
- subsystem reveal and selection;
|
|
- the targeting-system item.
|
|
In exchange, Continue works again (Cut 0), and agents can falsify lifecycle, save and
|
|
provenance claims without the editor.
|
|
|
|
## 7. Risks
|
|
|
|
- **Unity batchmode is the only proof for Cuts 1-2's `Assembly-CSharp` edits.** It needs
|
|
the editor closed. The operator's in-editor play remains the proof for the lowering
|
|
(cameras, menus, action bar, pickup visuals).
|
|
- **`Zone` subscribing `Death` changes NPC death timing in Unity.** Removal now happens
|
|
inside the damage call rather than in `EntityInstance`'s subscriber. The operator's
|
|
fight check covers it.
|
|
- **Rolled combat is untuned.** A passing scenario proves that the roll, damage, death,
|
|
loot and save follow the rules. It does not prove the rules are fun. The balance numbers
|
|
in Cut 2 are first guesses.
|
|
- **Cut B's recorded green is contradicted by §2.5.** Soul should rerun the suite before
|
|
trusting any provenance verification that reopens a store.
|