Docs: align CLAUDE.md + vault to scene-split / Automation / Saves / UITK

CLAUDE.md: rewrite Bootstrap&worlds (scene split + on-demand frontend), add M7 Automation + disk-persistence + UITK HUD bullets, new build-gotchas, note new folders fit existing asmdefs. Vault: Milestones (M6 + polish pass -> Done; HUD + cleanup rows), Backlog (ConnectionUI done + cleanup notes), Home/Systems_Index dates. Add the 3 prior session logs + DR-019/020/021 + the 2026-06-06 cleanup log + screenshots.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:06:26 -07:00
parent adf78570f8
commit 8aed336340
24 changed files with 1144 additions and 17 deletions
@@ -0,0 +1,41 @@
---
id: DR-019
title: Front-end infrastructure — Netcode frontend menu (on-demand worlds), UI Toolkit, Graphics/Audio settings, saves foundation, build wiring
status: accepted
date: 2026-06-05
tags:
- decision
- netcode
- world-lifecycle
- frontend
- ui-toolkit
- settings
- saves
- build
permalink: gamevault/07-sessions/decisions/dr-019-frontend-menu-settings-saves-build
---
# DR-019 — Front-end infrastructure (menu, settings, saves, build)
## Context
The game had no shell: `GameBootstrap.Initialize` always created BOTH ServerWorld + ClientWorld and an editor-only auto-host dropped you straight into gameplay ([[DR-005_M4_Connection_Model_Direct_IP]]). To "build and test the game" as a product the operator asked for a **main menu** (Single/Host/Join), **settings that actually change the game**, **persistent saves**, and a **build pipeline**. Intake scoped: saves = foundation + a minimal real slice; UI = **UI Toolkit**; settings = **Graphics + Audio**; build = wire-up only. A 3-critic adversarial Workflow reviewed the design before coding (per the [[CLAUDE]] netcode-slice rule) and changed several load-bearing choices. Verified against context7 (Netcode 1.13.2) + runtime.
## Decision
1. **Netcode frontend pattern, builds vs editor.** Boot a menu with NO netcode worlds; create them **on demand** per menu choice via the public `ClientServerBootstrap.CreateServerWorld(name)` / `CreateClientWorld(name)` (which register the `ServerWorld`/`ClientWorld` statics — `ConnectionUI`/`DebugOverlay` depend on them). `CreateLocalWorld` is **internal** in 1.13.2, so the menu world is a plain default world (`Initialize` returns `false`, or `DefaultWorldInitialization.Initialize` on return-to-menu). The **editor default path is unchanged** (today's `CreateDefaultClientServerWorlds()` + MPPM + `EditorAutoHostSystem`); a `ProjectM/Boot Into Menu (Editor)` EditorPrefs toggle — gated to `RequestedPlayType==ClientAndServer` so MPPM virtual players never boot to the menu — flips the main editor to the frontend for in-editor testing. Player builds boot the menu (`Server` playtype auto-hosts).
2. **One world-lifecycle funnel, frame-boundary.** `WorldLauncher` (static) owns `StartSession(mode,ip,loadSave)` and `TeardownToMenu()`, run on a persistent `SessionRunner` (`DontDestroyOnLoad`) coroutine — never inside an ECS system. Start: dispose the menu world → create worlds (Single/Host=server+client, Join=client-only) → seed the existing `ConnectionConfig` (Single binds **loopback**, Host binds AnyIpv4) → set a netcode world as `DefaultGameObjectInjectionWorld``LoadScene(Game, Single)` so the **SubScene streams into the on-demand worlds**. Teardown: autosave (host) → `World.DisposeAllWorlds()``LoadScene(MainMenu, Single)`. **Reuses the whole connection layer unchanged** (`ConnectionConfig` + control systems + GoInGame + ring spread).
3. **UI Toolkit, code-built.** `MainMenuController` / `SettingsScreen` / `PauseMenuController` build their trees in C# with inline styles. A shared `RuntimePanelSettings.asset` (+ a `RuntimeTheme.tss` importing `unity-theme://default`) loaded from Resources, and an ensured `EventSystem` + `InputSystemUIInputModule` (the project uses the Input System), make runtime UITK render + receive input.
4. **Settings drive the engine.** `SettingsService.Apply``Screen.SetResolution`/`fullScreenMode`, `QualitySettings.SetQualityLevel`/`vSyncCount`, `Application.targetFrameRate`, and audio. **Master rides `AudioListener.volume`; `GameVolume.Music/Sfx` are per-bus trims the 3 client audio systems multiply per-call** (no double-attenuation). Persisted as versioned JSON (`JsonUtility`, atomic write) at `persistentDataPath`. `GameVolume` is deliberately NOT named `AudioSettings` (collides with `UnityEngine.AudioSettings`).
5. **Saves = born-correct, host-authoritative, additive.** Versioned single-slot JSON (`SaveData{GoalCharge,GoalTarget,LedgerRow[],SavedAtMs}`). Load is **applied at director spawn** (`CycleDirectorSpawnSystem` reads an unmanaged `PendingSave` singleton the menu staged in the ServerWorld) so the CycleDirector ghost is BORN with the saved GoalProgress + ledger and never replicates a default first. Autosave on the Siege→Calm checkpoint (Bursted `CyclePhaseSystem` raises a `SaveRequest` byte; managed host-only `SaveWriteSystem` writes) + on quit-to-menu. Schema is additive (structures/threat/storage append later behind `Version`).
6. **Build wiring only.** `Editor/BuildTool.cs` (`ProjectM/Build/Windows Player`, scenes = MainMenu+Game) + the editor toggle; `EditorBuildSettings` = `0:MainMenu 1:Game 2:DevSandbox 3:SampleScene`. The operator runs the actual standalone.
## Consequences
- **Validated end-to-end at runtime** (focused editor): menu renders → Single creates worlds, the **subscene streams into the on-demand worlds**, loopback connects, the player spawns + replicates, the base is playable → teardown disposes the netcode worlds + recreates the menu world + writes a save → Continue loads **born-correct** (`charge=42 ledger=[(1:15)] pending=0`). Settings apply live. EditMode 152/152.
- **The frontend pattern is transport-agnostic** like DR-005 — Unity Relay later swaps the endpoint source feeding `ConnectionConfig`, no change to the menu/lifecycle.
- **The save schema is frozen as the additive foundation** the [[CLAUDE]] "freeze the schema now" note asked for; the minimal slice (ledger + goal + settings) overrides the strict post-M7 deferral, but only additively.
- **Editor/build divergence is intentional and small:** the editor keeps the proven instant-play + MPPM loop by default (a toggle tests the menu); builds get the real menu. The bootstrap edit is last + the default path is byte-identical to before — no MPPM regression risk.
- **Deferred:** controls/rebinding, full base-state saves, Relay, dedicated-server build (defensive branch, untested), legacy IMGUI `ConnectionUI` removal (harmless, left to avoid touching the load-bearing `Game.unity` subscene), volume-slider theming fallback.
Extends [[DR-005_M4_Connection_Model_Direct_IP]]; mirrors the server-authoritative / small-co-op pillars from [[Pillars]].
@@ -0,0 +1,52 @@
---
id: DR-020
title: M7 Automation — server-only deterministic production chains (Harvester → Conveyor → Fabricator) + SaveData v2 structure persistence
status: accepted
date: 2026-06-05
tags:
- decision
- netcode
- automation
- production
- conveyor
- determinism
- persistence
- m7
permalink: gamevault/07-sessions/decisions/dr-020-m7-automation-production-chains
---
# DR-020 — M7 Automation: server-only deterministic production chains + SaveData v2 structure persistence
## Context
M6/DR-014 locked the structure model as "the M7 contract": `PlacedStructure{[GhostField] byte Type; int2 Cell; uint NextTick; uint LastProcessedTick}` (tick fields baked as the offline-catch-up linchpin), a data-driven `StructureCatalog`, occupancy-derived placement, and reserved type codes `Harvester=2/Fabricator=3/Conveyor=4`. M7 ([[Pillars]] automation pillar) builds the self-running chain on top. The operator chose the FULL chain (Harvester → Conveyor → Fabricator), auto-gathering EXISTING resources (no new ids), with persistence folded in. A 3-lens adversarial Workflow (code · persistence/authoring · netcode/determinism critic) reviewed the design before coding (per the [[CLAUDE]] netcode-slice rule) and changed several load-bearing choices. Verified against context7 (Entities 6.4 / Netcode 1.13.2) + EditMode (190) + runtime (real netcode worlds). See [[2026-06-05_M7_Automation]].
## Decision
1. **Production is SERVER-ONLY** (resolves the open [[Backlog]] "predicted vs server-only" question). Three `ISystem`s in the plain server `SimulationSystemGroup`, ordered `[UpdateAfter(PredictedSimulationSystemGroup)]``HarvesterProductionSystem``ConveyorTransportSystem``FabricatorProductionSystem` (a one-tick chain). No prediction, no rollback, no `Simulate` filter. Results reach clients ONLY through already-replicated state: the global `StorageEntry` ledger ([GhostField] on the untagged CycleDirector ghost) and `PlacedStructure.Type`. Per-machine I/O is **server-only buffers** (`MachineInput`/`MachineOutput` — DISTINCT element types, NO `[GhostField]` → never hit the wire; resolve the ledger via `GetSingletonEntity<ResourceLedger>()`, NEVER `GetSingleton<StorageEntry>`).
2. **The chain.** `Harvester{byte ResourceId;int Yield;int PeriodTicks}` = a fixed-yield base generator (NOT node-tapping — decoupled from the per-cycle despawning expedition field) depositing into its own `MachineOutput`. `Conveyor{byte Direction;int PeriodTicks}` pulls one item off the adjacent upstream `MachineOutput` (cell `DirOffset`) when its single `ConveyorItem` (enableable, baked disabled) is empty, then advances exactly one cell toward `Direction`. `Fabricator{In/OutResourceId,In/OutAmount,PeriodTicks}` consumes its `MachineInput` and deposits the recipe output into the GLOBAL ledger (the "auto-gather existing resources" terminal; default 2 Ore → 1 Aether). The DR-014 "recipe column on `StructureCatalogEntry`" was **dropped as dead weight** — the recipe is baked on each machine component (the catalog stays cost+prefab only).
3. **Single gated catch-up path** (`ProductionMath`, pure + unit-tested). One path per machine: if `NextTick==0` (uninitialized/just-placed) → schedule (`Last=now`, `Next=now+period`), produce nothing; else if cooling → 0; else `cycles = clamp(TicksSince(Last)/max(1,period), 0, MaxCatchup)`. **Lower bound 0, not 1** (the critic's `1` would mint a premature cycle on a restored `remaining==0` machine where `since<period`; the NextTick gate guarantees `since>=period` in the normal path). `period=math.max(1,…)` + `[Min(1)]` kills div-by-zero; the fabricator additionally clamps `runs=min(cycles, floor(input/InAmount))` (no mint-from-nothing). `BuildPlaceSystem` now stamps `LastProcessedTick=0` (was `NonZero(now)`) so runtime-placed machines initialize cleanly.
4. **Deterministic, order-independent conveyor** (`ConveyorMath.ResolveMoves`, pure + exhaustively unit-tested). Sources iterate sorted by a stable `CellKey` (NEVER hashmap order); destination occupancy is read from a PRE-move snapshot (double-buffer → exactly one cell/tick); a belt cell accepts AT MOST ONE item; ties break to the lowest-CellKey source; losers STALL with no loss; machine-input SINK cells always accept (merge). Tested with a Y-junction (deterministic winner) + a 4-cell line + **shuffle-invariance** (identical end-state under two input orders) + a blocked-cell stall.
5. **SaveData v2 = structures fold in; cooldowns as epoch-independent REMAINING ticks; "offline catch-up" is within-session only.** v2 adds `StructureSave[]` (type, cell, direction, `RemainingTicks`, in-flight conveyor item) + a flat `StructureIoRow[]` table (JsonUtility can't nest arrays-of-arrays; joined by index). Cooldown is persisted as **remaining ticks** (`NextTicknow`), not an absolute tick, so it survives the server-tick origin reset on a fresh session (sidesteps the negative-rebase blocker). On restore `LastProcessed=now` → the chain RESUMES; no production is minted for wall-clock time spent quit (the determinism pillar forbids it — the preserved stockpile, not a clock, is what compounds). `SaveService.Load` already nulls on a version mismatch, so a pre-M7 (v1) save cleanly degrades to New Game.
6. **One-shot, charge-free restore** (`BaseRestoreSystem`, server). The menu stages `PendingStructure`/`PendingStructureIo` on a SEPARATE carrier (the `PendingSave` ledger carrier is consumed+destroyed by `CycleDirectorSpawnSystem`); `BaseRestoreSystem` gates on `StructureCatalog`+`BaseAnchor`+`NetworkTime` (dodges the subscene-stream race), instantiates each saved structure from the catalog at `CellToWorld(cell)` (preserving baked Scale), re-stamps the rebased ticks, re-adds `RegionTag{Base}`+`RuntimePlacedTag`, refills buffers + conveyor item, and **destroys its carrier (one-shot)**. NEVER `Withdraw`s — the ledger restores absolutely via the existing born-correct path. A `RuntimePlacedTag` (added by `BuildPlaceSystem` + restore) is the **single source of truth**: only player-built structures are saved/restored; anything baked into the subscene stays the subscene's.
## Consequences
- **Validated at runtime on 6.4.7 (real ServerWorld+ClientWorld, single in-editor client):** catalog bakes all 6 entries (machine prefabs valid ghosts); a placed H→C→F chain ran end-to-end (Ore 200→148 cost; Harvester→Conveyor→Fabricator→ledger), **server Aether == client Aether** (replicated); quit autosaved v2 (3 structures + cooldowns); Continue restored all 3 at the exact cells, **charge-free** (Ore unchanged), born-correct ledger, and the **chain resumed producing**. EditMode **190/190**. Console clean.
- **No new asmdef.** New code under `…/Automation/` (Simulation/Server/Authoring) + persistence edits in `…/Persistence/`. Reuses `BuildPlaceSystem` placement, `StorageMath` + the global ledger, `BaseGridMath`, `TickUtil`, the `TurretFireSystem` NetworkTick-cooldown idiom, the duplicate-prefab ghost recipe, the byte-RPC pattern, and the born-correct save/load path.
- **Process incident:** the source-drafting Workflow's general-purpose agents raw-`Write`-wrote files into `Assets/`, which compile but get no `.meta`/test-discovery until a `scope=all mode=force` refresh — EditMode silently ran the old count (152) until forced. Captured as [[raw-written-cs-needs-full-refresh]]; future no-Unity-write workflows should use a read-only agentType.
## Open / deferred
- **Throughput visuals** (item-on-belt) need a small replicated field — server-only buffers don't reach the client (clients see only `Type`); accept static visuals or add one byte.
- **Build UX:** a build-palette HUD + ghost-preview + conveyor-facing indicator (dev H/F/C keys + `[`/`]` rotate today).
- **Relevancy ceiling:** `RegionRelevancySystem`'s O(structures×connections)/tick scan (DR-014 note) becomes load-bearing at higher conveyor counts — batch it when counts grow.
- **Recipe depth:** multi-input fabricator recipes; fabricator→conveyor output chaining (additive — add a `MachineOutput` to the fabricator); per-machine distinct meshes (placeholder Turret mesh today).
- **Multi-client + LAN:** the chain + replication are single-client validated; pairs with the deferred 2-build LAN tests.
Builds on [[DR-014_M6_Build_Structures_Automation_Foundation]] (the structure model + tick fields this extends), [[DR-019_Frontend_Menu_Settings_Saves_Build]] (the born-correct save/load path), [[DR-013_M6_Aether_Cycle_Region_Split]] (ledger/region), and [[DR-008_M5_HomeBase_BaseLayer_Storage]] (grid/RPC/runtime-ghost). Serves the automation pillar in [[Pillars]].
@@ -0,0 +1,44 @@
---
id: DR-021
title: In-game HUD rebuilt on UI Toolkit (consistent with the menu) + a click-to-place build-palette HUD with ground ghost preview
status: accepted
date: 2026-06-05
tags:
- decision
- hud
- ui-toolkit
- build
- presentation
- juice
permalink: gamevault/07-sessions/decisions/dr-021-hud-uitk-build-palette
---
# DR-021 — UI Toolkit HUD rework + build-palette HUD (click-to-place + ghost preview)
## Context
The frontend (menu / settings / pause) moved to UI Toolkit in DR-019, but the in-game HUD was still code-built **uGUI** (`HudSystem`: RawImage bars over `Texture2D.whiteTexture` + legacy `Text`) — two visual languages. M6/M7 added buildable structures but the only build UX was dev hotkeys (H/F/C/B/V/N at the player's cell). The operator: *"build palette hud — rework all the hud … use the new uitoolkit so it is consistent."* Intake: build palette = full click-to-place + a ground ghost preview; also re-skin the floating combat juice. Verified against the existing UITK infra + runtime.
## Decision
1. **HUD → UI Toolkit, observe-only, System-owned UIDocument.** Rewrite `HudSystem` (client `PresentationSystemGroup`) to own a runtime `UIDocument` directly (the same shape the old uGUI HudSystem used for its Canvas) rather than a separate controller MonoBehaviour — it reads ECS every frame, so a System pushing values into held `VisualElement` refs is simpler than a setter bridge. Shared `MenuUi.LoadPanelSettings()` (the menu's `RuntimePanelSettings`) at **`sortingOrder=50`** (the pause overlay is 100, so it layers on top). The tree is built on the **first OnUpdate where `rootVisualElement` exists** (the panel needs a frame to init its PanelSettings after `AddComponent`). New `HudUi` factories (percent-width bar fill, bold labels) extend `MenuUi`'s Aether palette so HUD + menu read identically.
2. **The HUD never eats game clicks.** The root + every non-interactive element is `pickingMode = PickingMode.Ignore`; only the build-palette buttons opt back in (`Position`). This is the load-bearing UITK-in-game rule — a default-picking full-screen panel silently swallows every world click. The pause overlay (sortingOrder 100, full-screen, picking) already modally covers the HUD when open.
3. **Build palette = client-catalog-driven, observe-only.** The palette is lazy-built from the **client-side `StructureCatalog`** (the gameplay subscene streams into both worlds, so the client has Type + cost without replication) and shows affordability from the replicated ledger. A palette-button click sets client-local `BuildPaletteState.Selected` (UI state, never the sim) — the HUD stays a pure observer.
4. **Click-to-place + ground ghost in `BuildSendSystem` (client sim), not presentation.** Placement sends the RPC, so it lives in the `ClientSimulation` build system, not the observe-only HUD. When a buildable is selected it raycasts cursor→ground→cell (`AimMath.TryGroundHit` + `BaseGridMath.WorldToCell`) and drives a procedural translucent **ghost cube** coloured by `BuildPreviewMath.Evaluate` (pure, unit-tested: in-plot → unoccupied → affordable, the client mirror of the server's `BuildPlaceSystem` check; occupancy derived client-side from the replicated `PlacedStructure` ghosts' positions, since `Cell` is server-only). Left-click places (the existing `BuildPlaceRequest` path), right-click/Esc cancels, `[`/`]`/R rotates a conveyor. The one-frame cursor lag of reading the camera in ClientSimulation (before its LateUpdate move) is imperceptible for cell-snapped base building.
5. **Interaction guards are self-contained.** Fire is suppressed while build mode is active (`PlayerInputGatherSystem` gates `Fire` on `!BuildPaletteState.Active`) so the place-click never fires. The frame the selection changes never also places (`_lastSelected` compare in BuildSendSystem — robust to either UITK-event/ECS ordering). Build mode is suspended while `PauseMenuController.Open` so pause-menu clicks can't place. `AimPresentation.ForceCursorVisible` shows the cursor in build mode (the aim system hides it while aiming).
6. **Juice restyle, not rebuild.** The floating damage numbers (world-space pooled `TextMesh`) stay world-space (UITK is screen-space) but are re-skinned to the Aether palette + bold: Blight-orange when the local player is hurt, Aether-cyan when you damage a Husk. The aim reticle / VFX / dev overlays are out of scope.
## Consequences
- **Validated at runtime on 6.4.7** (real ServerWorld+ClientWorld, focused editor): the UITK HUD renders every panel in the Aether palette (`~HUD` UIDocument, 5 root groups); the palette shows all 6 buildables + costs; selecting one highlights it + activates the ground ghost at the cursor cell (green/red); clearing hides it; placement through the build RPC works; console clean. **EditMode 190→194.** Screenshots: `HudRework_Base.png`, `HudRework_BuildMode.png`.
- **No new asmdef, no netcode surface change** — reuses the M7 `BuildPlaceRequest` path, `MenuUi`/PanelSettings, `AimMath`/`BaseGridMath`/`AimPresentation`. New code: `HudUi`, `BuildPaletteState`, `BuildPreviewMath` (+test); rewrite `HudSystem`; extend `BuildSendSystem`; 1-line `PlayerInputGatherSystem`; `PauseMenuController.Open`; `CombatFeedbackSystem` restyle.
- Resolves the [[Backlog]] M7 follow-up "Build-palette HUD + ghost preview".
## Open / deferred
- Per-buildable **icons** + a conveyor-facing **arrow** on the ghost (text palette + plain cube today).
- **Throughput visuals** (item-on-belt) need a small replicated field — server-only machine buffers don't reach the client (the DR-020 open item).
- Remove the legacy IMGUI `ConnectionUI` "Net: Connected" label — the only non-UITK on-screen UI left in-game.
- A build-mode HUD hint line + an open/close palette focus key.
Builds on [[DR-019_Frontend_Menu_Settings_Saves_Build]] (the UITK frontend + PanelSettings + pause pattern) and [[DR-020_M7_Automation_Production_Chains]] / [[DR-014_M6_Build_Structures_Automation_Foundation]] (the structures + catalog + `BuildPlaceRequest` this builds the palette UX on). Serves the [[Pillars]] co-op base-building pillar.