ActivatedBehavior replaces IActivatedBehavior so Activate/Deactivate can be internal under Unity's C# 9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 KiB
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.
- the headless half of the CultCache-migration play smoke
(
Invariants:
- One owner of the run lifecycle. The playground and
ActionGameManagerboth 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.ccorGameData/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 atActionGameManager.cs:411-412. AI:Combat.cs:56,106,108.
- Player movement and AI movement write the same
- Player-only inputs, all plain entity writes that an agent could make:
TractorPower(ActionGameManager.cs:1214-1216);OverrideShutdown,HeatsinksEnabled, shieldEnabled,Sensor.Ping(:339-362);- gear
ActivatedBehavior(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).
- interact, which means warp, dock or undock (
- False: combat resolves in Unity.
InstantWeapon.OnFireonly 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 withUnityEngine.Random. - Pickup is a collision trigger (
ShieldManager.cs:29). - (probe) Weapon groups fired 6
OnFireevents at an enemy 50 units ahead. Enemy hull stayed 500.0 -> 500.0.
- False:
CombatStateneeds Unity-written data. It indexesShip.HardpointTransforms(Combat.cs:101). OnlyEntityInstance.cs:516writes that dictionary, from barrel transforms.- (probe) A
Minionwith a target throwsKeyNotFoundException: '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).CombatStatedoes not use it.
- (probe) A
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.csprojcompilesAssets/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,AetheriaStoresandItemManagerall 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
LonginusXspawned throughLoadoutGenerator; - thrust by hand-set
MovementDirectionreached 142 m/s in 5 s; - a
Minionpatrol moved the player ship 642 units in 10 s; TryDockandTryUndocksucceeded (Entity.cs:721-755);Ship.EnterWormholefollowed by stepping firedOnEnteredWormhole(Ship.cs:82-90,372);Deathfired on hull damage (Entity.cs:277-280);RunSave.CaptureandRunSave.Commitwrote a 489 KBrun.cc.
- Settings:
GameSettingsis a ScriptableObject (GameSettings.cs:11) holding ServerShared classes.tools/AetherDb/AuthoredSettings.csreads them from the asset YAML. The probe placed every field (0 unplaced).- Two lifecycle inputs are Unity-only
GameSettingsfields:StartingHullName(GameSettings.cs:17, authoredLonginusX) andWormholeDistanceRatio(:23, authored 0.75).
- Randomness:
ItemManager.Randomis clock-seeded (ItemManager.cs:20).Galaxyis clock-seeded only whenseed == 0(Galaxy.cs:108,158).Zoneseeds from the zone name (Zone.cs:54).Zone.Updateruns belt updates onTask.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 aBaseStategraph. Minion(Minion.cs:7-21) patrols (PatrolOrbits.cs), auto-targets the first visible enemy (:14), and entersCombatState.- 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 (
PopulateLevelmoves the entity, not agents). - There is no "fly to a point" state.
MoveToStateis abstract and its only concrete state isMoveToOrbitState(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 callsAetheriaStores.OpenwithGameData/run.cchard-wired (AetherDb.cs).save(Program.cs:349-410) is the only played-save reader. - It already owns
AuthoredSettings(YamlDotNet) and referencesAetheria.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.MaterializedLotsSurviveSaveAndReloadRunSaveTests.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, aDictionary<int2, PersistentBehaviorData[]>(EntitySerializer.cs:200), written at:35-40and 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.
CultMessagePackSecurityrunsUntrustedDataand adds a comparer only forCultRecordRef<T>keys.- No other run-store type has a struct dictionary key.
SavedGameusesintkeys, the ledger usesint, andPlayerSettingsusesstring. - 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)
TargetNearestpicks the farthest visible enemy:MaxBy(length(...))(ActionGameManager.cs:378-379).- The interact loop enters every wormhole in range, then calls
Dock()anyway (:312-317). SaveRunpassesDockedEntity ?? CurrentEntity(:245), butBindToEntitynullsDockedEntity(:946). The order is fragile, not wrong today.
2.8 The probe (discarded)
A scratch console referencing Aetheria.Shared and AuthoredSettings.cs. It:
- Loaded authored settings.
- Opened the stores (catalog read-only; run and player in temp).
- Generated a galaxy with seed 42 and set
ItemManager.Randomto seed 7. - Generated zones outward from the entrance until one held ships and stations.
- Spawned the player ship, drove it by hand, then by a
Minion. - Forced a target, fired groups by hand, then docked, undocked and entered a wormhole.
- 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
CultCachefromAetheriaStores.Open, composed by the caller; GameplaySettings,ZoneGenerationSettings,PlanetSettings;- a
uint seedforItemManager; Action<string> log;- for a new run, a caller-built
GalaxyandisTutorial; dtper step;- commands from either lowering;
SavedActionBarBinding[]at save, owned by the Unity action bar (the playground passes empty).
- the
-
Outputs:
- state:
Galaxy,ItemManager,Zone,Pilot(the pilotedEntity),StationandBaywhen docked,TowingStation,Ended; - events:
ZoneEntered(Zone),PilotChanged(Entity),Docked(Entity, EquippedDockingBay),Undocked,WarpArrived(GalaxyZone),Saved,Died(CauseOfDeath); - store writes through
RunSave.CommitandRunSave.Clearonly.
- state:
-
Derived state:
ActionGameManager.CurrentEntity,.Zone,.ItemManager,.DockedEntity,.DockingBayand.TowingStationbecome read-only projections ofRun, for display only.ZoneRenderer.WormholeInstancesbecomes render-only: its keys areZone.Wormholes, and it is no longer the wormhole list gameplay reads.SectorMapreadsRun.Galaxyfor display.
-
Forbidden writers after the cut:
ActionGameManagermay not callRunSave.Clear,RunSave.Capture,RunSave.Commit,ZoneGenerator.GenerateZoneornew Zone, and may not add or remove the pilot inZone.Entities.- It may not construct
ItemManager, spawn the player ship, subscribeDeathto end the run, or setCurrentEntity,DockedEntity,DockingBayorTowingStation. MainMenumay not callRunSave.Clearor constructGalaxy(cache, saved).InventoryPanelmay not writeCurrentEntityorDockingBay.DockedShip.ZoneRenderermay not createWormholeobjects.- The playground may not write
Position,Velocity,Hull.Durabilityor zone membership directly. It has nohitcommand; damage comes only from fire control (fork D ruling).
-
Fire control (Cut 2).
FireControl(ServerShared) owns hit, cell and damage;Zoneowns death removal, loot drops and the pickup commit.Entity.TargetItemis a command, written only throughTrySelectTargetItem. The aimed item is derived from it plus reveal.- Unity weapon effects and
HullColliderare presentation ofShotOutcome. - Forbidden deciders of hits or damage: Unity weapon effects,
HullCollider, anyPhysics.*query,EntityInstance,ShieldManager(it may only callZone.TryPickUp), the playground, andEntity.HardpointTransforms(deleted). - Firing arc: the mount direction is
Behavior.Direction, derived from the ship'sDirectionand the mounted item'sItemRotation(itselfHardpointData.Rotation,Entity.cs:559); the arc width isGameplaySettings.FiringArc(fork W for overrides). PrefabWeaponHardpoint.FiringPointtransforms andArticulationPointlimits 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 inUpdate, then callsrun.Step(Time.deltaTime). The playground updates its pilot agent insideRun.Step, beforeZone.Update, so the order matches. - Direct interaction (Unity's Interact key), programmatic interaction (playground
dock/warp) and Continue's docked landing all commit throughRun.TryDock,Run.TryEnterWormholeandRun.Land. - A new run and Continue share
Run.EnterZone. - Warp arrival and quit share
Run.Save.
- Manual input and agent input write the same entity fields (§1), and both go before
-
Pilot agent:
Run.PilotAgent(nullable) is owned byRun, not byZone.Agents, so it follows the pilot across warps.- Unity leaves it null. It may never be non-null while Unity input is enabled.
Zone.Agentsstays the owner of NPC agents.
-
Deletion line:
ActionGameManager: bodies ofSaveRun,EnterWormhole,PopulateLevel's simulation half,StartGame,Dock,DoDock's state half,Undock's checks,TowShip,Die's clear, andIntroCutscene(entirely);- statics
CurrentGalaxyandIsTutorial; 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/AetherDbandtests/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-CSharpis proven only by Unity batchmode with the editor closed, perdocs/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:PersistedBehaviorsbecomes(int2 position, PersistentBehaviorData[] data)[], matchingEquipmentat:197-199.- Key 5 is kept. The wire shape changes from a map to an array; saves are discarded
and no
GameData/run.ccexists. :35-40packs pairs.:126-130builds a local lookup at unpack.
- Key 5 is kept. The wire shape changes from a map to an array; saves are discarded
and no
RunSaveTests.csBarePackinitializer:Array.Empty<...>().- Verification:
- The two HEAD-red tests go green: 34 passed.
- Mutation: restore the
Dictionary<int2,…>type. Both tests go red with theint2comparer message. - Add
ReopenedPackKeepsPersistedBehaviorstoRunSaveTests. 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.
- Mutation: pack drops
- Negative:
rg -n "Dictionary<int2" Assets/Scripts/ServerSharedis 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
CurrentGalaxyandIsTutorial(:96-97). SaveRunbody:243-248becomesRun?.Save(actionBar). Quit (:236) calls it.EnterWormhole:615-637and the detect loop:312-316becomeRun.TryEnterWormhole().WarpArriveddrivesSectorMap.QueueZoneReveal.PopulateLevel:639-675: the simulation half moves intoRun.EnterZone. What stays is aZoneEnteredhandler:ZoneRenderer.LoadZone,PlayMusic, rebind.StartGame:725-770becomesRun.NeworRun.ContinueinStart. It keeps the sector reveal and action-bar restore in Unity. Restore readsmin(slots, saved.Length), so a playground-written save continues in Unity.Dock:801-823becomesRun.TryDock().DoDock:825-843keeps cameras and menus onDocked; its state writes:828,830,840-841go toRun.Undock:845-887becomesRun.Undock(), returningUndockResult. The dialog text maps from the result.TowShip:889-913becomesRun.Tow().Die:1080-1088becomes aDiedhandler: unbind, menus, post-fx,SavePlayerSettings. TheRunSave.Clearat:1088goes.:1053subscribesRun.Died.:264constructs noItemManager.Update:1218becomesRun.Step(Time.deltaTime).
MainMenu.cs:- Continue (
:102-107) setsActionGameManager.NewRun = nulland loads the scene. - New Game drops
:112and setsNewRun = (sector, tutorial)at:138,166.
- Continue (
SectorMap.cs:114-310readsActionGameManager.Instance.Run.Galaxy.InventoryPanel.cs:117-118becomesGameManager.Run.Board(ship).ZoneRenderer.cs:222-231iterateszone.Wormholes.ItemManager.cs:20: the seed becomes a constructor argument. Call sites: 11 in tests, 1 inAetherDb, andRun.
- Settings (fork S (a)):
StartingHullNameandWormholeDistanceRatiomove fromGameSettings.cs:17,23toGameplaySettings(Settings.cs:155).- The asset keys move under
GameplaySettings:inSettings.assetby text edit, with Unity closed. - Readers
ActionGameManager.cs:738andZoneRenderergo throughItemManager.GameplaySettings.
Zone.cs:public Wormhole[] Wormholes, built in the constructor fromGalaxyZone.AdjacentZoneswith the rule fromZoneRenderer.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)RunSave.Clear.ItemManageroverRunSave.Lots, seeded.EnterZone(Entrance).- Spawn the player ship per
:734-743. - 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), thenEnterZone(saved zone), thenLand(saved entity). Landhandles an orbital entity per:753-756: pilot is theIsPlayerShipchild, and the run is docked there.
- Returns null when there is no
void Step(float dt)- Throws if
Ended. - Updates
PilotAgent, thenZone.Update(dt).
- Throws if
void EnterZone(GalaxyZone): the rule from:643-666. It raisesZoneEntered.bool TryEnterWormhole()- Picks the nearest
Zone.Wormholesentry withinGameplaySettings.WormholeExitRadius. Requires the pilot to be aShip, not mid-animation, and undocked. - On
OnEnteredWormhole:EnterZone(target).- Discover adjacent zones.
ExitWormholeat the back-link wormhole withWormholeExitVelocity * ItemManager.Random.NextFloat2Direction().Save(lastActionBar).- Raise
WarpArrived.
- Picks the nearest
EquippedDockingBay TryDock(): the rule from:803-821, withinDockingDistance.UndockResult Undock():Ok,NotDocked,MissingCockpit,MissingThruster,MissingReactor,BayNotEmpty. Check order as:850-885.bool Board(Ship): docked only; the ship must be the station'sIsPlayerShipchild; updatesBay.DockedShip.void Tow(): the rule from:891-911.void Save(SavedActionBarBinding[] actionBar)RunSave.Capture(cache, Galaxy, Zone, Station ?? Pilot, isTutorial, actionBar), thenCommit(…, ItemManager.Lots).- No-op when
Ended. - Remembers
actionBarfor warp saves.
- Death:
RunsubscribesPilot.Deathwhenever the pilot changes. On death:Ended = true.RunSave.Clear.- Dispose subscriptions.
- Raise
Died(cause).
Agent PilotAgent { get; set; }: setting it whileEndedthrows.
- Tests (in
RunTests, same project, fixture catalog asLoadoutTests). 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/Scriptsis empty.rg -n "new Wormhole" Assets/Scriptsmatches onlyZone.cs.rg -n "new ItemManager\(" Assets/Scriptsmatches onlyRun.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.csis empty.
- Ledger:
ActionGameManagerabout -190 +45;MainMenu-4 +2;ZoneRenderer-9 +3;InventoryPanel-2 +1;SectorMap±12 renames;ItemManager±1;Zone+10;Run.csabout +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.
LonginusXis the only ship hull (ControlModule, Sensors, Energy 2, Launcher 2, Radiator 2, Reactor, AetherDrive).Turrethas Ballistic 2 and Sensors 1.Zenithhas no weapon hardpoints. - 18 weapon designs, all single-target, lowered by four effects:
ProjectileManager8,LaserManager3 (pulse, velocity 0),GuidedProjectileManager5 (3GuidedWeaponData->InstantWeapon, 2LauncherData->LockWeapon),LightningGunManager1. - No design uses
ConstantWeaponData,ConstantLaser,ConstantLightning,ConstantParticleWeapon,HitscanEffect,Mine, airburst (Flak Cannon), MIRV splits orShieldData. - Sensor state exists:
Sensor.ExecuteaccumulatesEntity.EntityInfoGathered[other]in [0,1] with decay (Sensor.cs:121-154);TargetDetectionInfoThreshold(0.1) gatesVisibleEntities(Entity.cs:196-207);LockWeapon.cs:92scales lock by info. TargetArmorInfoThreshold(0.5) andTargetGearInfoThreshold(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 BatandTractor 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 ofHullData.Hardpoints):- Gameplay uses only the barrel's current world forward.
EntityInstance.cs:514-517copiesFiringPoint[0](WeaponHardpoint.cs:7, matched by name toHardpointData.Transformat:268) intoHardpointTransforms.Combat.cs:100-102fires whendot(barrel, toTarget) > .99(about 8 degrees).TurretController.cs:80-83does the same throughBehavior.Direction(Behaviors.cs:31-33). The player's fire is not gated at all. - Barrels swing through
ArticulationPoint(ArticulationPoint.cs). It yaws and pitches towardLookAtPoint(EntityInstance.cs:400-403,:519), clamped to authoredYawMin/YawMax/PitchMin/PitchMaxrelative to its parent, atSpeeddegrees per second. No other arc data exists: no angle field onHardpointData,WeaponDataor any effect. - Catalog hulls and their weapon hardpoints (all
Rotation None):LonginusX->Ships/Longinus.prefab.En.L/En.Rbarrels sit underArticulationPoints with yaw [-1, 1], pitch [-80, 80], speed 60: fixed forward.La.L/La.Rtubes 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 Gunbarrels sit under aTurret Yaw Pivotwith yaw [-720, 720] (unlimited) and a pitch point [-45, 28], speed 15.Zenith->Stations/Zenith.prefab: no weapon hardpoints.Djinni.prefabcarries articulation but no catalog hull references it.
ShipInstance.cs:70andEntityInstance.cs:242,257match thruster, sound and radiator transforms by name. They are presentation and do not touch arcs.
- Gameplay uses only the barrel's current world forward.
- Mount direction convention (matches thrusters).
Behavior.Direction's fallback (Behaviors.cs:37) isEntity.Direction.Rotate(item.Rotation)(Extensions.cs:152-166):None= forward,Reversed= aft,Clockwise= right,CounterClockwise= left (Agent.cs:72takesClockwiseas right). A thruster pushes against that vector (Thruster.cs:79):Nonethrusters are the reverse group andReversedthe forward group (Ship.cs:103-107),CounterClockwisethe right group (:109). A weapon fires along it.Thruster.cs:62'sfloat2(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 asHardpointData.Rotation, already catalog data. - Aim is planar.
Entity.Directionisfloat2; steering readsLookDirection.xz(Ship.cs:278). Height differences come from terrain andGridOffset; pitch exists only inArticulationPoint.
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,SpreadandVelocity(Weapon.cs:22-66), withLockWeapon.CanFire(:56) as a gate; - the firer's targeting system, or the unaided defaults when it has none;
- sensor state,
firer.EntityInfoGathered[target]; TargetandTargetItem;- both entities' positions and directions;
- the weapon's mount direction
Behavior.DirectionandGameplaySettings.FiringArc; ItemManager.Random, seeded by Cut 1.
- weapon stats
- Outputs:
ShotOutcome { Source; Weapon; Target; bool Hit; bool Shielded; EquippedItem Aimed; int2 Cell; float FlightTime };- shield
TakeHit, orEntity.DamageSchematicplusIncomingHit.
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
LockWeaponis 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.
FiringArcis 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 bypDeviationunder 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)).Spreadis already a cone in degrees (ProjectileManager.cs:18-23).SchematicCellSizeis the one new tuning constant. Hands measures it on the LonginusX prefab.
-
Reveal.
IsRevealed(observer, item):- Rank the target's non-hull equipment: hardpoint-mounted first, then size descending, then equipment index.
- Item
iofNis revealed wheninfo >= lerp(TargetArmorInfoThreshold, TargetGearInfoThreshold, i / max(1, N - 1)).
Ordering is fork V.
-
Roll. One draw against
p. On a hit, a second draw againstPrecisiondecides where it lands:- on success, on
Aimed's footprint cells; - otherwise, on one cell drawn uniformly over the hull
Shape.
AimedisTargetItemonly while that item belongs toTargetand is revealed. It is derived when read; no loop clears it. - on success, on
-
Damage. The rule moves verbatim from
EntityInstance.cs:334-394:- Expand the hit shape
round(DamageSpread)times. - If
Penetration > .5, march cells along firer->target, rotated into the target's frame by-target.Direction. This replacestransform.InverseTransformDirection. - Apply
DamageSchematic(Damage, shape)(:277-314, moved toEntity).
- An active shield that
CanTakeHittakes the hit instead. Every effect repeats that rule today (Projectile.cs:60-71). Damagestays raw.DamageCurvestill feeds only DPS estimates.
- Expand the hit shape
Targeting system (new subsystem).
TargetingSystemData : BehaviorDatatakes union 39. ATargetingSystembehavior sits on aGearDataitem, mounted per fork T.- Its stats are
PerformanceStat, so lot quality reaches them throughEquippedItem.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
EnergyDrawandHeatbehaviors on the item. - An entity with no targeting system, or with its system offline or destroyed:
- fires with
GameplaySettings.UnaidedAccuracy,Resolution = 1andPrecision = 0; - can still select a revealed item, but its hits scatter.
- fires with
- A schematic hit on the targeting system degrades fire.
Target item: one selection path.
Entity.TargetItem : ReactiveProperty<EquippedItem>sits besideTarget(Entity.cs:40).bool Entity.TrySelectTargetItem(EquippedItem)is the only writer. It accepts null, or an item ofTarget.ValuethatIsRevealed.- Changing
Targetnulls 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.
- the player, from a select/cycle command beside
Per weapon kind.
InstantWeaponfamily:Execute(InstantWeapon.cs:181-191) callsFireControl.Firefor each burst shot.OnFirebecomesevent Action<ShotOutcome>.- Charge multipliers already fold into
DamageandSpread.
- Guided weapons (
GuidedWeaponData,LauncherData) use the same roll.- Lock time survives as the existing
LockWeapon.CanFiregate. - Flight is presentation.
- Lock time survives as the existing
- Lightning (
plight) makes one single-target roll.HitRadiusis presentation. ConstantWeaponrolls once perGameplaySettings.BeamResolveIntervalforDamage * interval, raised asOnBeamShot(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
ApplySplashis not added.
- The earlier draft's
TractorBeamis not a hit. It stays Unity physics.
AI and turrets.
Combat.cs:100-102:shouldFire = FireControl.HitProbability(testWeapon, target) >= Settings.AgentMinHitProbability.- The
HardpointTransformsread is deleted. AgentMinHitProbabilityreplacesAgentFiringMinDot, which nothing reads.:84-98(looking at the predicted intercept) survives only under fork F (c).
- Subsystem policy: through
TrySelectTargetItem, select the revealed targetWeaponitem with the highestRangeDamagePerSecond(range), or null if there is none. TurretController.cs:80-83uses the same gate and threshold; its.99fdot is deleted.HardpointTransformsis 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.Directionkeeps 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.BarrelsandArticulationPointstay in Unity forGetBarrel, 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
Directionnever rotates;TurretControllerwrites onlyLookDirection) 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).
Zonesubscribes to theDeathof each entity it adds. On death it:- removes the entity and its agent;
- drops each non-hull item with probability
LootDropProbability, and all cargo, usingItemManager.Random; - adds the drops to
Zone.Loot : ReactiveCollection<LootDrop { ItemInstance Item; float3 Position; float2 Velocity }>.
LootDropProbabilityandLootDropVelocitymove fromGameSettings.cs:19-20toGameplaySettings(fork S).Run.Diedreads 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 theShotOutcome.- 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,
SendHitandTakeHitare 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,SendSplashand their argument classes are deleted.OnCollisionEnter(ship collision) stays, deferred.EntityInstance.cs::277-396is deleted;:405-437keeps only the destroy effect.
ZoneRenderer.DropItemlowersLoot.ObserveAdd.ShieldManager.cs:25-41callsZone.TryPickUp.Loot.ObserveRemovedestroys 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
IsAvailablefind one in every galaxy.
-
LoadoutGenerator.FillInterior(after:250-260) picks one withrequired: truefor every entity that hasWeapons:- 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
ItemDataand deletesItemData.Manufacturer. -
AetherDbcaptures 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 facttarget.revealed. -
provenance-c.playkills by fire:target nearestpilot minionstep <s>expect zone.entities == <n-1>pickup allexpect 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 untilexpect 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/Scriptsrg -n "\.99f" Assets/Scripts/ServerShared/Agents/States/Combat.cs Assets/Scripts/ServerShared/Behaviors/TurretController.csrg -n "ArticulationPoint|FiringPoint" Assets/Scripts/ServerSharedrg -n "SendHit|SendSplash|DamageSchematic|TakeHit\(|Durability\s*[-+]?=" Assets/Scripts --glob '!ServerShared/**'rg -n "Physics\." Assets/Scripts/Gameplay/Weapons Assets/Scripts/Gameplay/HullCollider.csrg -n "Random\.value|onUnitSphere" Assets/Scripts/Gameplay/EntityInstance.csrg -n "Entities\.Remove" Assets/Scripts --glob '!ServerShared/**'
- Unity batchmode with the editor closed (
docs/item-provenance-cut.md§6) reports noerror 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
loadoutover several seeds. The fix is data orEquipHardpoints, 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 / Velocityfrom a pending queue inZone. 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
ItemRotationapplied to the ship'sDirection, the thruster convention (Body facts above). It is per hardpoint because a mounted item takesHardpointData.Rotation. - The arc is
GameplaySettings.FiringArc, 120 degrees full width, one authored value. - Nothing is extracted from prefabs.
FiringPointandArticulationPointbecome presentation. The prefabs carry no arc width a catalog field would preserve, except the turret's unlimited yaw (fork W). - No
HardpointDatafield 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'sDirectionstays at spawn facing, so a 120 degree arc on itsNoneguns turns today's all-round turret into a forward-only one. - (a) Per-hardpoint override:
HardpointData.FiringArc : float?(Key(6), absent meansGameplaySettings.FiringArc). Author 360 on the turret'sLeft GunandRight Gunin Cut 2's catalog upsert, after provenance Cut C. One field, with prefab evidence for its one consumer.AetherDbreopen still reports 0 schemas; testArcOverrideReplacesDefault(a 360 hardpoint fires at a target behind; mutation: the override is ignored). - (b)
TurretControllerrotates the turret entity'sDirectiontowardLookDirectionat 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 byFillInteriorlike 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*, andShieldManager'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
ShieldManageris 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".
FieldDriveris 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.Toolgear, 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
DamageTypes 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. TodayShield.CanTakeHitignores the type entirely. The presentation seam'sAbsorbEventalready carries the hit'sDamageType. - 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.
GrabEventnames 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, andFieldDrivermoving the grabbed transform itself.
- Bodies, and one detection interface (operator, 2026-09-17).
- Kinematic bodies are moved by forces.
ServerSharedgets aBody(position, velocity, response to force, detection signature) thatZonesteps underGetForce.Entityhas aBody.- 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 anEntity, becauseEntitykeeps 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."
- Kinematic bodies are moved by forces.
- (a)
Zone.TryPickUpis the one commit.- Contact detection stays Unity collision, per the scope doc's deferral.
- The playground's
pickup allmatches drops by spawn position withinDockingDistance.
- (b)
Zoneowns drop motion and proximity pickup every step, and Unity renders it. That pullsTractorBeamheadless too. - Recommend (a). Say plainly that contact has two detectors until the tractor beam moves.
Cut 3: AetherDb play
AetherDb.cs:OpentakesdataDir. Run and player paths come from it. It defaults toGameDatafor the existing commands and is required forplay.Program.cs:case "play": return Play(args).- Usage:
play <scenario-file|-> --data <dir> [--seed <u>]. playrefuses a--datathat resolves to<root>/GameData.
- Usage:
- New
tools/AetherDb/Playground.cs. It is a line interpreter overRunwith 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 |
ActivatedBehavior 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 |
gotoneedsMoveToPositionState : MoveToStateinMoveTo.cs(about 6 lines), wrapped in a one-stateAgent. It is the only new agent code.- The trace goes to stdout, one line per step command and per
Runevent:t=<zone time> zone=<name> <event> k=v….stateadds pilot position, velocity, hull, target, docked, entity and agent counts, lot count andrun.ccbytes. - An exception prints its type, message and first frame, then ends the scenario.
- Exit code is failed expectations plus escaped exceptions.
lots.absentandlots.mismatchedreuse the checks inProgram.cs:399-408. They are extracted to one method thatsavealso 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
continuewith theint2message. - Mutation:
Run.DiedomitsClear, andexpect run.savedgame == falsefails. - Negative:
rg -n "RunSave\.|ZoneGenerator|\.Position\s*=|Durability\s*=" tools/AetherDb/Playground.csis empty.
- Ledger:
Playground.csabout +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.playcovers migration smoke steps 3-8 minus the rendering and UI parts:- new, warp, warp again with the
SavedZonecount 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.ccfrom settings UI), 3's input screen and 9 (loadout UI) stay operator-only.
- new, warp, warp again with the
provenance-c.playcovers 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.
- new; kill an NPC with
5. Forks
R. Reopen defect. Blocking: every Continue and two HEAD tests.
- (a)
PersistedBehaviorsas 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
Packdoes 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/AetherPlayexecutable, 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.
CombatStatefires whenFireControl.HitProbabilityclearsAgentMinHitProbability, andHardpointTransformsis 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:
DamageSchematicmoves toEntity, and death removal, loot roll and the pickup commit move toZone. - Effects and
HullColliderstop 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'sprovenance-c.play("kill an NPC withhit") 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 "
hitis 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
hitsupplies 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;AuthoredSettingsand the YAML reader are deleted, so the options below, theRun.Newsettings arguments and theAuthoredSettingsuses above are history. Re-map this cut against that one before Hands. - (a) Move
StartingHullName,WormholeDistanceRatioand (Cut 2)LootDrop*intoGameplaySettings, with a text edit ofSettings.assetwhile Unity is closed. - (b) Pass them as
Runconstructor arguments read fromGameSettings; the playground reads them by name from YAML. That leaves two readers of one authored value. - (c) A new
RunRulesbundle, 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 byStart. - (b)
MainMenubuilds the wholeRunon its generation thread. That moves zone generation's run-store upserts off the main thread, and theCultCachethreading 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:
MoveToPositionStateand 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-CSharpedits. It needs the editor closed. The operator's in-editor play remains the proof for the lowering (cameras, menus, action bar, pickup visuals). ZonesubscribingDeathchanges NPC death timing in Unity. Removal now happens inside the damage call rather than inEntityInstance'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.