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
+3 -3
View File
@@ -2,7 +2,7 @@
tags:
- moc
- home
updated: 2026-06-04
updated: 2026-06-06
permalink: gamevault/00-home/home
---
@@ -15,8 +15,8 @@ Multiplayer game on **Unity DOTS** (Entities) + **Netcode for Entities** (server
- **Vision** → [[Pillars]] — design pillars & locked decisions · [[Identity]] — the fiction (sci-fi frontier colony)
- **Game Design** → [[Systems_Index]] — per-system design docs
- **Roadmap** → [[Milestones]] · [[Backlog]]
- **Sessions** → `07_Sessions/2026/` — dated work logs (latest: [[2026-06-04_M8_Persistent_Base_Player_Driven_Pacing]])
- **Decisions** → `07_Sessions/_Decisions/` — decision records DR-001 … DR-017 · [[DR-001_Netcode_Test_Harness]] · latest [[DR-017_Persistent_Base_Player_Driven_Pacing]]
- **Sessions** → `07_Sessions/2026/` — dated work logs (latest: [[2026-06-06_Cleanup_Alignment]])
- **Decisions** → `07_Sessions/_Decisions/` — decision records DR-001 … DR-021 · [[DR-001_Netcode_Test_Harness]] · latest [[DR-021_HUD_UITK_BuildPalette]]
- **Meta** → [[Documentation_Protocol]] · [[Tags]]
- **Templates** → [[Session_Log_Template]] · [[Decision_Record_Template]]
+12 -1
View File
@@ -2,7 +2,7 @@
tags:
- design
- index
updated: 2026-06-04
updated: 2026-06-06
permalink: gamevault/02-game-design/systems-index
---
@@ -48,6 +48,17 @@ One design doc per gameplay system, linked here. Each should state: purpose, com
- **Systems:** `ResourceHarvestSystem` **unified** to sweep nodes AND clutter in one best-target loop (required — two separate sweeps would double-destroy an overlapping projectile at ECB playback; `math.max(1,(int)yield)` guards the immortal-sink); `ExpeditionFieldSystem` scatters/clears clutter beside the node field (distinct seed); `BuildPlaceSystem` **unchanged** (already type-generic) — new generic `StructureAuthoring{byte Kind}` + two additive `StructureCatalog` rows + `BuildSendSystem` V/N keys. Client: `WorldFeedbackSystem` (observe-only `PresentationSystemGroup` — chip/shatter juice, proximity-gated so region-transit stays silent) + live-tunable `WorldFeelConfig`.
- **Netcode/world shape:** clutter = ownerless interpolated, region-scoped via GhostRelevancy (like nodes); Wall/Pylon = ownerless interpolated structure ghosts (`PlacedStructure.Type` byte-additive → **no re-bake**); Wall carries a `PhysicsCollider` (CC-blocking), Pylon cosmetic. Visual: `M_Aether_Wild`/`M_Aether_Ordered` palette materials + a classic-URP Blightfield rock basin in `Game.unity` (relief via props, **no terrain**). Status: **built + validated** (EditMode **142/142**; in-editor Play introspected via execute_code; 4-lens adversarial review → 4 findings fixed). Decisions: [[DR-018_World_Space_Cohesion_Pass]] (builds on the M6 region split / build pipeline + M8 persistent base — those systems are documented in their DRs).
### M7 — Automation: self-running production chains (Harvester → Conveyor → Fabricator) · [[2026-06-05_M7_Automation]]
- **Components** (`ProjectM.Simulation/Automation`): `Harvester{byte ResourceId;int Yield;int PeriodTicks}` (fixed-yield generator), `Fabricator{byte In/OutResourceId;int In/OutAmount;int PeriodTicks}` (recipe → global ledger), `Conveyor{byte Direction;int PeriodTicks}`; server-only buffers `MachineInput`/`MachineOutput` (DISTINCT types, NO `[GhostField]`); `ConveyorItem` (enableable, baked DISABLED); `RuntimePlacedTag` (player-built = persistable, single source of truth). Pure math: `ProductionMath` (NeedsInit/CyclesDue/RemainingTicks/RestoreNextTick), `ConveyorMath` (DirOffset/CellKey/ResolveMoves — deterministic), `MachineSlotMath` (byte-id deposit/withdraw, the non-replicated twin of `StorageMath`).
- **Systems** (`ProjectM.Server/Automation`, server-only, plain `SimulationSystemGroup` ordered chain `[UpdateAfter(PredictedSimulationSystemGroup)]` → Harvester → Conveyor → Fabricator): `HarvesterProductionSystem` (→ own `MachineOutput`), `ConveyorTransportSystem` (pull adjacent upstream output → `ResolveMoves` → settle `ConveyorItem`/deposit sink `MachineInput`), `FabricatorProductionSystem` (input-limited recipe → global ledger). Persistence: `SaveStructureScan` (shared autosave/quit scan of `RuntimePlacedTag` structures), one-shot `BaseRestoreSystem` (charge-free replay), `SaveData` v2 (`StructureSave[]` + flat `StructureIoRow[]`, cooldown as epoch-independent REMAINING ticks). Build: `BuildPlaceSystem` stamps conveyor `Direction`+`RuntimePlacedTag`; `BuildSendSystem` H/F/C keys + `[`/`]` rotate; `BuildPlaceRequest` +`byte Direction`. Authoring: 3 MonoBehaviours + bakers; `Harvester/Fabricator/Conveyor.prefab` (dup `Turret.prefab`) in the catalog.
- **Netcode shape:** production is **SERVER-ONLY** (no prediction/rollback); results replicate ONLY via the global `StorageEntry` ledger ([GhostField], untagged CycleDirector ghost) + `PlacedStructure.Type`; per-machine I/O buffers never hit the wire. "Offline catch-up" = within-session tick math + a preserved stockpile across quit (NO wall-clock minting). Status: **built + runtime-validated** (real netcode worlds, server==client; quit→Continue restore; EditMode **190/190**). Decisions: [[DR-020_M7_Automation_Production_Chains]] (builds on [[DR-014_M6_Build_Structures_Automation_Foundation]]).
### Presentation — UI Toolkit HUD + build-palette HUD · [[2026-06-05_HUD_Rework_UITK_BuildPalette]]
- **Client (presentation/UI):** `HudSystem` rewritten on **UI Toolkit** (client `PresentationSystemGroup`, observe-only) — a runtime UIDocument (shared `RuntimePanelSettings`, sortingOrder 50 behind the pause overlay; root `pickingMode=Ignore`) in the Aether-cyan `MenuUi`/`HudUi` palette: health+cooldown bars, HUSKS, phase/cycle/countdown + resources + location + goal, DOWNED overlay, and a **build-palette bar** lazy-built from the client `StructureCatalog` (cost + affordability + selection highlight). `BuildPaletteState` (client-local), `BuildPreviewMath` (pure validity, unit-tested), `BuildSendSystem` (cursor→cell ground **ghost** green/red, left-click place, right-click/Esc cancel, [/]/R rotate; self-contained guards), `PlayerInputGatherSystem` (fire suppressed in build mode), `PauseMenuController.Open` (pause guard). `CombatFeedbackSystem` damage numbers re-skinned to the Aether/Blight palette.
- **Netcode shape:** none new — UI observes replicated state (ledger/cycle/goal/player ghost) + the client-baked catalog; placement reuses the M7 `BuildPlaceRequest` RPC (server-authoritative). Status: **built + runtime-validated** (UITK render + palette + selection + ground ghost + placement; EditMode **194/194**). Decisions: [[DR-021_HUD_UITK_BuildPalette]] (builds on [[DR-019_Frontend_Menu_Settings_Saves_Build]] UITK frontend + [[DR-020_M7_Automation_Production_Chains]] structures).
## Conventions
DOTS/ECS conventions live in repo `CLAUDE.md` and the `dots-dev` skill's `dots-conventions.md`. Don't duplicate volatile API details here — link to context7-derived notes instead.
+28 -3
View File
@@ -2,7 +2,7 @@
tags:
- roadmap
- backlog
updated: 2026-06-04
updated: 2026-06-06
permalink: gamevault/06-roadmap/backlog
---
@@ -14,6 +14,31 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
> **2026-06-04 World-space cohesion pass** ([[2026-06-04_World_Space_Cohesion_Pass]], [[DR-018_World_Space_Cohesion_Pass]]) — delivered + validated (EditMode 127→**142**): **destructible "clearing"** (`BlightClutter` ghost smashed by the **unified** harvest sweep → Biomass scrap + smash juice, scattered/cleared with the expedition field); **build out the base** (additive `Wall`=5 / `Pylon`=6 buildables — Wall blocks the player, V/N keys); and a first **visual cohesion layer** (Aether cyan/orange material pair, the 3 new ghosts recoloured, the barren +1000 Blightfield turned into a 21-piece Synty rock/debris basin + orange wild-lights — relief via props, **no terrain**). In-editor Play introspected via execute_code; a 4-lens adversarial review (23→4) caught + fixed an immortal-shot-sink (fractional `ScrapPerHit` truncation). Base-side cohesion accents + clearing-as-gating deferred (see the 2026-06-04 follow-ups below).
> **2026-06-05 Game infrastructure** ([[2026-06-05_Game_Infrastructure_Menu_Settings_Saves]], [[DR-019_Frontend_Menu_Settings_Saves_Build]]) — delivered + runtime-validated (EditMode 142→**152**): a **UI Toolkit main menu** (Single/Host/Join+IP/Continue/Settings/Quit) on the **Netcode frontend pattern** (on-demand `Create*World`, subscene streams into them, frame-boundary `DisposeAllWorlds` teardown, MenuWorld recreated on return); **Graphics + Audio settings** that drive the engine (`SettingsService` JSON at `persistentDataPath`; `GameVolume` Master/Music/Sfx buses); a **saves foundation** (versioned single-slot JSON, **born-correct** goal/ledger load at director spawn, autosave on Siege→Calm + quit-to-menu); editor `ProjectM/Build/Windows Player` + Boot-Into-Menu toggle; build order MainMenu(0)/Game(1). Editor instant-play + MPPM loop unchanged by default.
> **2026-06-05 M7 — Automation** ([[2026-06-05_M7_Automation]], [[DR-020_M7_Automation_Production_Chains]]) — delivered + runtime-validated (EditMode 152→**190**): the full **Harvester → Conveyor → Fabricator** self-running chain (server-only, deterministic), auto-gathering EXISTING resources (default 2 Ore → 1 Aether) into the global ledger; **SaveData v2** structure persistence (quit autosave → Continue restore, charge-free, born-correct, chain resumes); deterministic conveyor (`ConveyorMath.ResolveMoves`, shuffle-invariant); single gated catch-up (`ProductionMath`); one-shot `BaseRestoreSystem`; `RuntimePlacedTag` source-of-truth; 3 machine prefabs duplicated from `Turret.prefab` + catalog rows.
> **2026-06-06 Cleanup & alignment** ([[2026-06-06_Cleanup_Alignment]]) — repo hygiene before more dev (no gameplay change): deleted crash-recovery scenes (`_Recovery/`) + a stray empty `Assets/Resources.meta`; removed the legacy IMGUI `ConnectionUI` (3 scenes + script + 4 stale comments); DRY'd the duplicated UITK `Round`/`Border` helpers into `MenuUi` (canonical) + small HUD/build-input tidy (husk-count cache, table-driven build hotkeys); re-aligned `CLAUDE.md` (scene split, on-demand frontend, M7 Automation, disk persistence, UITK HUD, new build-gotchas) + the vault roadmap to the three 2026-06-05 sessions. EditMode green, console clean; landed as grouped commits.
### 2026-06-06 Cleanup follow-ups
- [ ] **Optional further code-tidy** (low value; deliberately skipped this pass to limit churn on freshly-validated systems — none affect behavior): `SettingsService` migrate-then-clamp ordering; `ConveyorTransportSystem`'s 6 parallel snapshot arrays → one struct array; `WorldFeedbackSystem.TintForResource` if-chain → byte→Color lookup; `PlayerInputGatherSystem` device-active dedup. [[2026-06-06_Cleanup_Alignment]].
### 2026-06-05 M7 Automation follow-ups
- [ ] **Throughput visuals** (item-on-belt): server-only machine buffers don't reach the client (clients see only `PlacedStructure.Type`); add ONE small replicated byte if live belt visuals are wanted.
- [x] **Build-palette HUD + ghost preview** — done 2026-06-05: UITK build-palette bar (6 buildables + cost + affordability + selection) + click-to-place with a green/red ground ghost, conveyor rotate, fire-suppress. Conveyor-facing arrow/icons still open. [[DR-021_HUD_UITK_BuildPalette]], [[2026-06-05_HUD_Rework_UITK_BuildPalette]].
- [ ] **Relevancy ceiling**`RegionRelevancySystem`'s O(structures×connections)/tick scan becomes load-bearing at higher conveyor counts (DR-014 ceiling note); batch it when counts grow.
- [ ] **Recipe depth** — multi-input fabricator recipes; fabricator→conveyor output chaining (additive: give the fabricator a `MachineOutput`); per-machine distinct meshes (machines reuse the Turret mesh as a placeholder).
- [ ] **Operator live play-through** of the chain + a real 2-build LAN run under latency (single-client validated this pass).
### 2026-06-05 Game-infrastructure follow-ups
- [x] **Build the standalone** — the first player build hit a pre-existing URP blocker (`UniversalRenderPipelineGlobalSettings` `m_AssetVersion=11` > package `k_LastVersion=10`, from the reverted 6.6 alpha; URP migrates forward-only so its build preprocessor rejected it — blocked builds, not editor Play). Fixed (reset the stamp to 10); `ProjectM/Build/Windows Player` now produces a complete Mono Win64 standalone (`Builds/Windows/`, both scenes + baked subscene). See [[2026-06-05_Game_Infrastructure_Menu_Settings_Saves]].
- [ ] **Operator-run validation**: launch `Builds/Windows/ProjectM.exe` → confirm it boots the menu → Single/Host/Join play; a real **2-build LAN Host↔Join**; MPPM co-op regression in default-editor mode.
- [ ] **Controls settings + key rebinding** (Input System rebinding UI) — deliberately out of the Graphics/Audio scope this pass.
- [ ] **Full base-state saves** (placed structures / threat / storage) — the `SaveData` schema is versioned so this is additive (no migration of the ledger/goal slice).
- [ ] **Unity Relay** for remote (non-LAN) Host/Join — swap the endpoint source feeding `ConnectionConfig` (the frontend lifecycle is transport-agnostic).
- [ ] **Dedicated-server build** — the `RequestedPlayType==Server` auto-host branch is in but UNTESTED this pass.
- [x] **Remove the legacy IMGUI `ConnectionUI`** — done 2026-06-06 (cleanup pass): the `NetConnectionUI` GameObject removed from `Game.unity` / `DevSandbox.unity` / `SampleScene.unity`, `ConnectionUI.cs` deleted, and the 4 stale doc-comments repointed to the UITK `MainMenuController` / `WorldLauncher`. The UITK frontend menu now owns Host/Join+IP everywhere. [[2026-06-06_Cleanup_Alignment]].
- [ ] **Volume-slider theming**: sliders render via the default runtime theme; if a real build shows them unstyled, swap to +/- cycle rows (the enum settings already use cycle rows).
- [x] Upgrade Unity 6.4 → 6.6 — done (now `6000.6.0a6`). Entities/Collections/Graphics → 6.5.0; **Netcode → 6.6.0 and Physics → 6.5.0 also renumbered into the editor line** (not independent 1.x as [[DR-001_Netcode_Test_Harness]] assumed). See [[2026-05-30_M1_Player_Slice]].
- [x] Define the core gameplay loop and the first predicted player ghost — delivered as M1 ([[2026-05-30_M1_Player_Slice]]).
- [x] **Re-validate the M1 play-tick on a stable Unity 6.x** — moot/subsumed 2026-06-04: M1M6 are all runtime-validated on the stable **6.4.7** line (the 6.6 alpha netcode bug never affected 6.4.7). The original "blocked on 6.6 alpha" framing ([[DR-002_Unity66_Alpha_Netcode_Transport]]) no longer applies.
@@ -21,7 +46,7 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
- [x] Optional template cleanup: remove `com.unity.visualscripting`, `Assets/TutorialInfo/`, `Assets/Readme.asset`**done 2026-06-03** (pre-M6 cleanup; see the "2026-06-03 Visual & Controls Polish" section below + [[2026-06-03_Pre_M6_Cleanup]]). Duplicate of the now-checked item there.
- [x] Decide **relay provider** before M4 — resolved: **Direct IP/LAN now, Unity Relay later** ([[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]).
- [x] Decide home-base **grid 2D vs 3D** before M6 — resolved 2026-06-02: **planar single-level `int2` grid**, CellSize 1.0, 32×32 plot (full 3D/stacked deferred). Locked in `BaseGridMath` — [[DR-008_M5_HomeBase_BaseLayer_Storage]].
- [ ] Decide **production replication** (predicted vs server-only) before M7 (automation).
- [x] Decide **production replication** (predicted vs server-only) before M7 (automation) — resolved 2026-06-05: **server-only** (production runs in the plain server `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]`; results replicate via the existing global ledger + `PlacedStructure` ghosts, never predicted; per-machine I/O buffers stay server-only). Delivered as **M7** ([[DR-020_M7_Automation_Production_Chains]], [[2026-06-05_M7_Automation]]).
- [x] **M2 follow-up — Burst cache health** — confirmed clean 2026-06-03: warm focused play shows **no** "not a known Burst entry point" managed-fallback messages + fast (~3s) play-enter. The M2-era corruption is gone. [[2026-06-03_Pre_M6_Cleanup]] (orig [[2026-05-31_M2_Combat]] / [[DR-003_M2_Combat_Netcode_Architecture]]).
- [ ] **M2 follow-up — live interactive fire test** (focused Play Mode: press Space / LMB / RT → predicted projectile + dummy HP drop). The server combat loop + replication are validated; the input→`AbilityFireSystem`→predicted-spawn→classification path is only validated structurally.
- [x] **M2 follow-up — mouse-cursor aim for KBM** — done 2026-06-03: client-side camera ground-ray cursor aim rides the existing `PlayerInput.Aim` `[GhostField]` (no new netcode surface); device auto-switch (KBM/gamepad) replicated as a `byte`; strafe-while-aiming free. [[DR-012_Aim_Controls_Cursor_Gamepad]], [[2026-06-03_Aim_Controls_Cursor_Gamepad]].
@@ -81,6 +106,6 @@ Surfaced by [[DR-018_World_Space_Cohesion_Pass]] (the balanced-slice scope delib
- [ ] **Base-side cohesion accents**: cyan ordered-Aether lights + perimeter relief + re-enabled tiled base ground (deferred this pass to avoid clashing with the tuned SSAO/ACES look — the Blightfield got the dressing budget).
- [ ] **Clearing as a spatial mechanic**: shipped as juice + scrap only; make cleared cells buildable / reveal hidden nodes (the "functional gating" fork from the intake gate).
- [ ] **Build-palette HUD + ghost-preview**: dedicated V/N keys for now; a selectable palette + placement preview is the UX follow-up.
- [x] **Build-palette HUD + ghost-preview** — done 2026-06-05 (UITK palette + click-to-place ground ghost). [[DR-021_HUD_UITK_BuildPalette]].
- [ ] **Per-`Variant` clutter meshes**: `BlightClutter.Variant` is replicated + round-robined but the prefab is single-mesh; swap per-variant meshes (additive, no schema change).
- [x] **Focused-editor validation** (2026-06-04): confirmed in Play — Wall **stops the player CC at the collider face** (X 2.50→3.257, server==client); Blightfield **reads orange-wild rock basin** vs cyan base (real player-view shots); **clutter-clear loop** deposits Biomass + shatters + replicates (BIO 0→10, clutter 14→13); node harvest works (AETHER 0→30). Console clean. **Still open:** deep basin density/lighting **feel-tuning**.
+7 -4
View File
@@ -2,7 +2,7 @@
tags:
- roadmap
- milestones
updated: 2026-06-04
updated: 2026-06-06
permalink: gamevault/06-roadmap/milestones
---
@@ -19,9 +19,12 @@ permalink: gamevault/06-roadmap/milestones
| **M5.5 — Game feel & identity** | Bridge "tech-demo → game": the **Husk** enemy (server AI, interpolated ghost), player death/respawn, combat juice (damage numbers/VFX/SFX/camera shake), a core HUD, and a sci-fi look pass — under the new fiction ([[Identity]], sci-fi frontier colony) | ✅ Done 2026-06-02 — runtime-validated on 6.4.7: Husks spawn(6)+replicate+chase+strike; death→respawn loop; HUD (health/cooldown/threat/downed); emissive dark-sci-fi look. EditMode **74/74**. ctx7-verified APIs. **Deepened same day:** auto-target on Husks, replicated respawn-invulnerability, and a `WaveSystem` threat director (escalating waves of 3 Husk variants — Grunt/Swarmer/Brute) replacing the flat sustain — runtime-validated (wave 1→2 escalation 4→6, distinct maxHP 30/15/80). [[DR-009_GameFeel_Identity_FirstBlood]], [[2026-06-02_GameFeel_Identity]], [[2026-06-02_GameFeel_Deepening]] |
| **— 2026-06-03 Visual & controls polish —** | Non-milestone polish layered on M5.5 (no mechanical rework): HDRP→URP art import + reusable converter; a cohesive **Synty** sci-fi colony world (cosmetic SampleScene GameObjects) + **GabrielAguiar** combat VFX; **KBM mouse-cursor aim + gamepad aim** with last-actuation device auto-switch (rides the existing `PlayerInput.Aim` ghost field). | ✅ Done 2026-06-03 — [[DR-010_Art_Import_URP_Conversion_Visual_Upgrade]], [[DR-011_Synty_World_VFX_Integration]], [[DR-012_Aim_Controls_Cursor_Gamepad]] |
| **— 2026-06-03 Pre-M6 cleanup —** | Loose-ends pass before M6: vault roadmap reconcile, Unity-template + orphaned-material removal, rate-limited turning, console/runtime health gate. | ✅ Done 2026-06-03 — [[2026-06-03_Pre_M6_Cleanup]] |
| **M6 — The Aether Cycle (core loop)** | Reframed from "grid build placement" into the first vertical slice of the **core game loop**: Expedition (gather) → Defend (wave) → Build/Charge (spend), persistent base + procedural sorties, escalating toward a goal. Build placement is now Stage 3 of this milestone. | 🚧 In progress 2026-06-03**Stages 04 done + runtime-validated** on 6.4.7 (M6 core loop systems complete): **base/expedition split via coordinate-region + `GhostRelevancy`** (player transit despawns/re-grants the other region's ghosts; server==client); a **server phase-director** (Expedition→Defend→Build→Expedition auto-cycle, cycle 1→2, Husk `WaveSystem` only in Defend, escalation 4→6); and **resources + harvest** — a **global CycleDirector ghost** carrying the replicated `CycleState` + a shared resource **ledger** (relevant in every region, unlike the base storage), a procedural **expedition field** (8 resource-node ghosts seeded per cycle, region-scoped), and a tunnel-safe **harvest** sweep depositing into the ledger; client **HUD** shows phase + resource counts. Supersedes DR-008's "split requires streaming" framing. **Stage 3** (generic automation-ready **structure model** + data-driven catalog + grid **build-placement** RPC with co-op-atomic commit + a hitscan **turret** that auto-defends + **ability tiers** via a bounded StatModifier) and **Stage 4 goal meter** are **done + validated** (turret placed/Ore-deducted/replicated; two same-tick requests → one build; turrets killed the wave; ability damage 20→30 bounded; goal increments per cycle). Disk-persistence **writer deferred to post-M7** (M7-additive surface — tick fields + frozen schema — baked now); the structure model is the M7 production-chain foundation. Playable walk-in-gate loop with build/spend, visible in the HUD. — [[DR-014_M6_Build_Structures_Automation_Foundation]] — [[DR-013_M6_Aether_Cycle_Region_Split]], [[2026-06-03_M6_Aether_Cycle_CoreLoop]] |
| **— 2026-06-04 Polish & backlog-clear pass —** | Comprehensive *sequential* polish across hygiene/reconcile, system-level tests, HUD (TMP + replicated wave number), procedural audio + combat juice, ghost-prop reskin + post-processing, new gameplay content, controls/UX, and a validation-harness + operator handoff. Clears the open [[Backlog]]. | 🚧 In progress 2026-06-04 |
| **M6 — The Aether Cycle (core loop)** | Reframed from "grid build placement" into the first vertical slice of the **core game loop**: Expedition (gather) → Defend (wave) → Build/Charge (spend), persistent base + procedural sorties, escalating toward a goal. Build placement is now Stage 3 of this milestone. | ✅ Done 2026-06-03 (forced-timer rhythm later superseded by M8's player-driven pacing)**Stages 04 done + runtime-validated** on 6.4.7 (M6 core loop systems complete): **base/expedition split via coordinate-region + `GhostRelevancy`** (player transit despawns/re-grants the other region's ghosts; server==client); a **server phase-director** (Expedition→Defend→Build→Expedition auto-cycle, cycle 1→2, Husk `WaveSystem` only in Defend, escalation 4→6); and **resources + harvest** — a **global CycleDirector ghost** carrying the replicated `CycleState` + a shared resource **ledger** (relevant in every region, unlike the base storage), a procedural **expedition field** (8 resource-node ghosts seeded per cycle, region-scoped), and a tunnel-safe **harvest** sweep depositing into the ledger; client **HUD** shows phase + resource counts. Supersedes DR-008's "split requires streaming" framing. **Stage 3** (generic automation-ready **structure model** + data-driven catalog + grid **build-placement** RPC with co-op-atomic commit + a hitscan **turret** that auto-defends + **ability tiers** via a bounded StatModifier) and **Stage 4 goal meter** are **done + validated** (turret placed/Ore-deducted/replicated; two same-tick requests → one build; turrets killed the wave; ability damage 20→30 bounded; goal increments per cycle). Disk-persistence **writer deferred to post-M7** (M7-additive surface — tick fields + frozen schema — baked now); the structure model is the M7 production-chain foundation. Playable walk-in-gate loop with build/spend, visible in the HUD. — [[DR-014_M6_Build_Structures_Automation_Foundation]] — [[DR-013_M6_Aether_Cycle_Region_Split]], [[2026-06-03_M6_Aether_Cycle_CoreLoop]] |
| **— 2026-06-04 Polish & backlog-clear pass —** | Comprehensive *sequential* polish across hygiene/reconcile, system-level tests, HUD (TMP + replicated wave number), procedural audio + combat juice, ghost-prop reskin + post-processing, new gameplay content, controls/UX, and a validation-harness + operator handoff. Clears the open [[Backlog]]. | ✅ Done 2026-06-04 — [[2026-06-04_Polish_Backlog_Pass]] |
| **M8 — Persistent base & player-driven pacing** | Replace the forced-timer cycle with a persistent **Calm** base (no countdown) + **player-initiated** expeditions + **event-triggered** sieges (composite **ThreatDirector**, post-expedition retaliation); **dev-tools-over-RPC** (spawn/force-siege/grant/teleport/god-mode) + a dedicated **Game** scene + **DevSandbox**. | ✅ Done 2026-06-04 — runtime-validated on 6.4.7 (server+client, focused editor): boots into **Calm** (no timer); a returning player arms a retaliation siege (Gate→ThreatDirector→CyclePhase→Wave), the atomic WaveState seed spawns the **exact** configured size, **server==client** (phase + husk ghosts replicated); dev overlay drives the real server RPCs (unconditional wire type → handshake intact); god-mode baked present+disabled; `Game.unity` boots the calm base with the new "AT BASE — deploy when ready" HUD. EditMode **137/137**. Supersedes the M6 forced-timer rhythm. — [[DR-017_Persistent_Base_Player_Driven_Pacing]], [[2026-06-04_M8_Persistent_Base_Player_Driven_Pacing]] |
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
| **— 2026-06-05 Game infrastructure —** | The surrounding shell to build + test the game: a **UI Toolkit main menu** (Single / Host / Join+IP / Continue / Settings / Quit) on the **Netcode frontend pattern** (on-demand world creation, subscene-streams-into-them, clean teardown); **Graphics + Audio settings** that drive the engine (persisted JSON); a **saves foundation** + minimal slice (born-correct goal/ledger load, autosave on Siege→Calm + quit); a one-click **build** menu + scene order. | ✅ Done 2026-06-05 — runtime-validated (focused editor): menu renders → Single → worlds created → subscene streams → connect→spawn→playable → teardown→menu → Continue **born-correct** (charge/ledger restored, no flicker); settings apply live; EditMode **152/152**. Editor instant-play + MPPM loop unchanged (toggle to test the menu); builds boot the menu. — [[DR-019_Frontend_Menu_Settings_Saves_Build]], [[2026-06-05_Game_Infrastructure_Menu_Settings_Saves]] |
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ✅ Done 2026-06-05 — **full Harvester → Conveyor → Fabricator chain, server-only + deterministic, auto-gathering EXISTING resources (default 2 Ore → 1 Aether) into the global ledger so setup compounds; persistence folded in (SaveData v2).** Runtime-validated on 6.4.7 (real netcode worlds): catalog bakes the 3 machine ghost prefabs; a placed live chain runs end-to-end (Ore cost withdrawn; Harvester→Conveyor→Fabricator→ledger), **server Aether == client Aether** (replicated); quit autosaved v2 (3 structures + epoch-independent REMAINING-tick cooldowns), Continue restored all 3 charge-free at the exact cells with born-correct ledger and the chain **resumed producing**. Deterministic conveyor (snapshot→stable-sort→at-most-one-claim→stall, shuffle-invariant); single gated catch-up path (`CyclesDue` lower-bound 0, input-limited, period-0-guarded); one-shot `BaseRestoreSystem`; `RuntimePlacedTag` single-source-of-truth. EditMode **190/190**. Resolves "production replication = **server-only**". — [[DR-020_M7_Automation_Production_Chains]], [[2026-06-05_M7_Automation]] |
| **— 2026-06-05 HUD rework (UITK + build palette) —** | Convert the in-game HUD to UI Toolkit (consistent with the menu / pause / settings) + a full build-palette HUD: click-to-place with a green/red ground-ghost, conveyor rotate, fire-suppress; combat juice re-skinned to the Aether palette. | ✅ Done 2026-06-05 — runtime-validated (real netcode session): UITK HUD renders all panels; palette shows 6 buildables + costs; selection highlights + a live ground ghost; placement via the build RPC works. EditMode **190→194**. — [[DR-021_HUD_UITK_BuildPalette]], [[2026-06-05_HUD_Rework_UITK_BuildPalette]] |
| **— 2026-06-06 Cleanup & alignment —** | Repo hygiene before more dev: delete dead assets (`_Recovery/`, stray `Resources.meta`), remove the legacy IMGUI `ConnectionUI` (scenes + script + stale comments), DRY the UITK `Round`/`Border` helpers + small HUD/build refactors, and align `CLAUDE.md` + the vault to the scene split / Automation / Saves / UITK surfaces. | ✅ Done 2026-06-06 — EditMode green, console clean; grouped commits. — [[2026-06-06_Cleanup_Alignment]] |
Promote items from [[Backlog]] here when committed.
@@ -0,0 +1,82 @@
---
date: 2026-06-05
type: session
tags: [session, frontend, menu, ui-toolkit, settings, saves, netcode, world-lifecycle, build, infra]
---
# Session 2026-06-05 — Game infrastructure: Main Menu, Settings, Saves foundation, Build wiring
## Goal
Operator: *"Create all the surrounding infra around the game. A main menu with the various options to play (host/join/single etc.), settings that actually adjust settings within the game, persistent saves. Basically everything needed to be able to build the game and test it."*
Intake decisions: **Saves** = foundation + a minimal real slice (resource ledger + goal charge + settings); **UI** = **UI Toolkit**; **Settings** = **Graphics + Audio** only; **Build** = wire-up only (operator runs the actual standalone build). Full rationale + the de-risked architecture: [[DR-019_Frontend_Menu_Settings_Saves_Build]].
The core change: adopt the Netcode **frontend pattern** — boot a menu with NO netcode worlds, create the right worlds **on demand** per menu choice, load the gameplay scene *after* the worlds exist (the subscene-streaming invariant). Replaces the always-create-both-worlds bootstrap.
## Process
- **Phase 13 (research, context7-first):** 3 Explore agents (netcode world-lifecycle · UITK runtime · settings/persistence/save-slice grounding) + direct ctx7 on the Netcode 1.13.2 `ClientServerBootstrap` frontend API. Confirmed `CreateClientWorld/CreateServerWorld(name)` are the public on-demand helpers ("frontends that allow Create Game vs Join Game"), `RequestedPlayType` is the MPPM switch, `CreateLocalWorld` is **internal** in 1.13.2.
- **Phase 4 (design) + adversarial review (Workflow, ultracode):** one Plan agent drafted the file-by-file map; a 3-critic + synthesis Workflow (netcode-lifecycle · determinism/teardown · UITK/build) red-teamed it. The review changed the design materially (see Decisions).
- Plan approved → 5 staged steps, each `create/edit → read_console → EditMode → (Play where relevant)`.
## Done
### Stage 1 — Settings (Graphics + Audio that actually change the game)
- `Client/Settings/GameVolume.cs` (static **Music/Sfx** bus trims — NOT named `AudioSettings`, which collides with `UnityEngine.AudioSettings`; Subsystem-reset), `GameSettings.cs` (`[Serializable]` model + `Clamped()`/`Defaults()`), `SettingsService.cs` (boot-load+apply via `[RuntimeInitializeOnLoadMethod]`; atomic JSON at `persistentDataPath`; `Apply` drives `Screen`/`QualitySettings`/`Application.targetFrameRate` + `AudioListener.volume`=Master + the buses).
- Wired `GameVolume` into the 3 client audio systems (`AmbientAudioSystem`=Music; `CombatFeedbackSystem`+`WorldFeedbackSystem`=Sfx, one edit each at their `PlayClip` helper). **Master rides `AudioListener.volume`; buses multiply per-call — no double-attenuation.**
- Tests: `GameSettingsTests` ×5 (JSON round-trip, clamps, defaults, bus statics).
### Stage 3 — Saves foundation + minimal real slice
- `Simulation/Persistence/`: `SaveData`(versioned `{GoalCharge,GoalTarget,LedgerRow[],SavedAtMs}` — array as a FIELD), `SaveComponents` (`PendingSave`+`PendingSaveLedgerRow` unmanaged bridge, `SaveRequest` byte flag), `SaveApply.WriteLedger` (Burst-safe buffer copy), `SaveService` (single-slot JSON, atomic, null on bad version).
- **Born-correct load** (review fix): `CycleDirectorSpawnSystem` applies a staged `PendingSave` AT SPAWN (GoalProgress + StorageEntry ledger), so the director ghost never replicates a default first. **Autosave:** `CyclePhaseSystem` raises the `SaveRequest` byte on the Siege→Calm checkpoint; server `SaveWriteSystem` (managed, host-only) writes the JSON. Quit-to-menu also saves (`WorldLauncher.TrySaveFromServer`).
- `ServerConnectionControlSystem` now binds `LoopbackIpv4` when Address=="127.0.0.1" (single-player, no firewall prompt) else AnyIpv4.
- Tests: `SavePersistenceTests` ×5 (schema round-trip, version handling, `WriteLedger` overwrite/empty).
### Stage 2 — UITK front end + world lifecycle
- `Client/UI/`: `MenuUi` (palette + styled element factories + **EventSystem(+InputSystemUIInputModule)** ensure + Resources PanelSettings load), `SessionRunner` (persistent `DontDestroyOnLoad` coroutine host), `WorldLauncher` (the **single** lifecycle funnel: `StartSession`/`TeardownToMenu`), `SettingsScreen` (shared Graphics/Audio panel — cycle rows + volume sliders), `MainMenuController` (Single/Host/Join+IP/Continue/Settings/Quit; ensures the menu world on return), `PauseMenuController` (Esc overlay) + `PauseMenuSystem` (client spawns it).
- Asset: `Assets/_Project/Resources/RuntimePanelSettings.asset` + `RuntimeTheme.tss` (default runtime theme). Scene: `Assets/Scenes/MainMenu.unity` (Camera, Light, EventSystem, MenuRoot=UIDocument+MainMenuController).
### Stage 4 — Bootstrap branch + build
- `GameBootstrap.Initialize` branch: **editor default = today's instant-play + MPPM unchanged**; 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; **player build** → frontend menu (`return false` → single default menu world), `Server` playtype → auto-host.
- `Editor/BuildTool.cs`: `ProjectM/Build/Windows Player` (`BuildPipeline.BuildPlayer`, MainMenu+Game) + the Boot-Into-Menu toggle. `EditorBuildSettings` reordered: **0:MainMenu 1:Game 2:DevSandbox 3:SampleScene**.
## Validation
- **EditMode 152/152** (142 prior + 10 new), console clean.
- **Runtime (focused editor, Play, via `execute_code`/screenshots):**
- Menu **renders** (UITK + hand-made theme + EventSystem) — `MainMenu_Boot.png`.
- **Single Player**: menu world disposed → ServerWorld+ClientWorld created → Game.unity loaded → **subscene streamed into the on-demand worlds** → loopback connect (`serverConns=1`) → player spawned (`serverPlayers=1`) → replicated (`clientPlayerGhosts=1`) → playable base + HUD — `MainMenu_Single_InGame.png`. *(The review's biggest risk — subscene into on-demand worlds — works.)*
- **Teardown**: `ServerWorld`/`ClientWorld` disposed, `MenuWorld` recreated, scene→MainMenu, **quit-save written** → menu shows **Continue**`MainMenu_WithContinue.png`.
- **Born-correct load** (Continue with a staged save Charge=42, ledger {1:15}): director ghost born `charge=42 target=10 ledger=[(1:15)] pendingRemaining=0` — no default-then-overwrite flicker.
- **Settings apply**: `vSync`, `AudioListener.volume`(master), `GameVolume.Music/Sfx`, `targetFrameRate` all driven by `SettingsService.Apply`.
- Only console output = the **pre-existing** in-editor "Server Tick Batching" perf warning (single-process multi-world cost; not from this work — see [[Backlog]]).
## Decisions
- **Created [[DR-019_Frontend_Menu_Settings_Saves_Build]]** (frontend world-lifecycle, UITK menu, settings, saves foundation, build wiring). Extends [[DR-005_M4_Connection_Model_Direct_IP]] (reuses `ConnectionConfig` + control systems + GoInGame handshake unchanged).
## Build gotchas (→ [[CLAUDE]] addendum candidates)
- **`CreateLocalWorld` is `internal` in Netcode 1.13.2** — use the public `CreateClientWorld`/`CreateServerWorld` (they register the `ServerWorld`/`ClientWorld` statics that `ConnectionUI`/`DebugOverlay` read); for the menu world, `DefaultWorldInitialization.Initialize(name,false)` (or `return false` from `Initialize`).
- **Subscene streams into on-demand netcode worlds IF a netcode world is the `DefaultGameObjectInjectionWorld` when the scene loads** — dispose the menu world first, set the default to the server world, then `LoadScene(Game)`. Validated (player spawns, replicates).
- **Runtime UITK needs a PanelSettings asset WITH a `themeStyleSheet`** (a `.tss` importing `unity-theme://default`) or it renders nothing; and an **EventSystem + `InputSystemUIInputModule`** (Input System project) or buttons are silently dead.
- **Don't dispose worlds inside an ECS system** — `WorldLauncher` runs all create/dispose/scene-load on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`).
- **`Cursor` is ambiguous** under `using UnityEngine.UIElements;` — fully-qualify `UnityEngine.Cursor`.
- **Validate UITK/world-lifecycle by rendering the menu in Play then driving `WorldLauncher` via `execute_code`** — no button-click simulation needed; `runInBackground=True` lets an in-Play editor tick.
- **A reverted URP/engine upgrade can stamp `UniversalRenderPipelineGlobalSettings.asset` with `m_AssetVersion` AHEAD of the current package's `k_LastVersion`** (here 11 vs 10, from the reverted 6.6 alpha — [[DR-002_Unity66_Alpha_Netcode_Transport]]). URP migrates **forward only**, so its build preprocessor (`URPPreprocessBuild`) rejects the "from-the-future" asset with *"is not at last version"***blocks builds but NOT editor Play** (so it lurks until the first build). Fix: reflection-set `m_AssetVersion` back to `k_LastVersion`, `SetDirty`+`SaveAssets` (the data already renders under the current package, so only the stamp is wrong). The Mono standalone build then succeeds (~7.5 min; `level0`=MainMenu, `level1`=Game, subscene in `StreamingAssets/EntityScenes`).
## Build (now succeeds)
- The first-ever player build surfaced a **pre-existing URP config blocker** (global-settings asset version 11 > package `k_LastVersion` 10, from the reverted 6.6 alpha) — fixed (see Build gotchas). **`ProjectM/Build/Windows Player` now produces a complete Mono Win64 standalone** (`Builds/Windows/ProjectM.exe` + 270 MB `_Data`, both scenes + the baked subscene). The build compiling proves all new runtime code (menu/settings/saves/world-lifecycle/bootstrap) is player-target clean (no editor-only-API leaks).
## Open / deferred
- **Operator-run**: launch `Builds/Windows/ProjectM.exe` and confirm it boots the menu → Single/Host/Join play; a real 2-build LAN Host↔Join; MPPM co-op regression in default-editor mode.
- Controls settings + key rebinding (out of scope); full base-state saves (structures/threat/storage — schema is versioned, additive later); Unity Relay; dedicated-server build (branch added defensively, untested).
- Legacy IMGUI `ConnectionUI` left in `Game.unity` (shows a harmless "Net: Connected" label; auto-hides). Optional cleanup — left to avoid touching the load-bearing subscene scene.
- Volume **sliders** render via the default theme; if a build shows them unstyled, swap to +/- cycle rows (the enum settings already use cycle rows).
## Next
- Operator: run the build + a real LAN join; then resume **M7 Automation** (the next milestone) or base/automation expansion on the now-buildable, persistent-save foundation.
@@ -0,0 +1,41 @@
---
date: 2026-06-05
type: session
tags: [session, hud, ui-toolkit, build-palette, presentation, uitk, juice]
---
# Session 2026-06-05 — HUD rework to UI Toolkit + build-palette HUD (click-to-place + ghost preview)
## Goal
Operator: *"Lets do a build pallete hud - rework all the hud in the game use the new uitoolkit so it is consistent."*
Intake decisions (AskUserQuestion): build palette = **full click-to-place + ground ghost preview** (green/red validity, conveyor rotation, fire suppressed in build mode); scope = the persistent screen HUD → UITK **plus** re-skin the floating combat juice to the Aether palette. Defaults taken: world-space juice (reticle/VFX) + dev overlays (DebugOverlay/legacy IMGUI ConnectionUI) untouched; HUD root `pickingMode=Ignore` so it never eats game clicks; build mode suppresses fire + is suspended while paused.
## Done
Converted the in-game HUD from code-built **uGUI** (`HudSystem`'s RawImage bars + legacy `Text`) to **UI Toolkit**, reading in the same Aether-cyan language as the menu/pause/settings (`MenuUi`). New + changed:
- **`HudUi`** (new) — UITK HUD factories (percent-width bar track+fill, bold labels, group/round/border), all `pickingMode=Ignore` by default.
- **`HudSystem`** (rewrite) — a `PresentationSystemGroup` observe-only system that now owns a runtime **UIDocument** (shared `RuntimePanelSettings`, `sortingOrder=50` — behind the pause overlay's 100), builds the tree on the first frame its root exists (panel-init timing), and pushes ECS values each frame: health+cooldown bars (bottom-left), HUSKS (top-right), phase/cycle/countdown + resources + location + goal bar (top-center), DOWNED overlay, and the **build-palette bar** (bottom-center). The palette is lazy-built from the **client-side `StructureCatalog`** (the subscene streams into both worlds), shows each buildable's Ore cost, greys the unaffordable, highlights the selected (cyan border), and hides off-base.
- **Build palette → click-to-place + ghost** (`BuildSendSystem` extended, `BuildPaletteState` new, `BuildPreviewMath` new): palette buttons set `BuildPaletteState.Selected`; `BuildSendSystem` then raycasts cursor→ground→cell (`AimMath.TryGroundHit`+`BaseGridMath.WorldToCell`), drives a procedural **translucent ground-ghost cube** (green=valid / red=blocked via `BuildPreviewMath.Evaluate` — in-plot, unoccupied, affordable; the client mirror of the server check), **left-click places** (the existing `BuildPlaceRequest` RPC), **right-click/Esc cancels**, **`[`/`]`/R rotates** a conveyor. Self-contained interaction guards: skip placing on the frame the selection changes (`_lastSelected`), suspend while `PauseMenuController.Open`, and `AimPresentation.ForceCursorVisible` shows the cursor in build mode.
- **Fire suppression** (`PlayerInputGatherSystem`, 1 line) — `Fire` is gated by `!BuildPaletteState.Active`, so the place-click never also fires.
- **Combat juice restyle** (`CombatFeedbackSystem`) — floating damage numbers re-skinned to the Aether palette + bold: **Blight orange** when you're hurt, **Aether cyan** when you hit a Husk.
- **`PauseMenuController`** — exposes `static bool Open` (for the build-mode pause guard).
- Keyboard hotkeys (B/V/N/H/F/C, `[`/`]`) kept as a power-user fallback (suppressed in palette mode); the editor `Place*` statics kept for headless validation.
## Validation
- **EditMode 190→194** (4 new `BuildPreviewMath` tests: valid / out-of-plot-first / occupied / unaffordable-with-exact-funds-ok). No regressions.
- **Runtime (real netcode session, focused editor, `execute_code` + screenshots):** the UITK HUD renders all panels in the Aether palette (`~HUD` UIDocument, 5 root groups); the build palette shows all 6 buildables + costs from the catalog; selecting Harvester **highlights** its button (cyan border) and activates build mode → `ForceCursorVisible` + a live **ground ghost at the cursor cell**; clearing hides the ghost; placement through the build RPC path works (harvester landed at the target cell). Console clean. Screenshots: `Assets/Screenshots/HudRework_Base.png`, `HudRework_BuildMode.png`.
## Decisions
[[DR-021_HUD_UITK_BuildPalette]]. Resolves the M7 follow-up "Build-palette HUD + ghost preview".
## Next-session intent
- **Per-buildable icons** + a conveyor-facing arrow on the ghost (text-only palette + cube ghost today).
- **Throughput visuals** (item-on-belt) still need a small replicated field (server-only machine buffers don't reach the client).
- Remove the legacy IMGUI `ConnectionUI` "Net: Connected" label (backlog) — the only non-UITK on-screen UI left in-game.
- Optional: a one-line "BUILD: [name] • click to place, [/] rotate, Esc cancel" hint while in build mode; a build toggle key to open/close the palette focus.
@@ -0,0 +1,49 @@
---
date: 2026-06-05
type: session
tags: [session, m7, automation, production-chains, conveyor, persistence, netcode, determinism]
---
# Session 2026-06-05 — M7 Automation: self-running production chains (Harvester → Conveyor → Fabricator)
## Goal
Operator: *"Let us start the automation milestone."***M7 — Automation**: self-running, tick-based production chains with deterministic offline catch-up, building on the DR-014 locked structure model.
Intake decisions (AskUserQuestion):
- **Scope:** the FULL chain (Harvester → Conveyor → Fabricator), not a reduced harvester-only slice.
- **What it makes:** AUTO-GATHER EXISTING resources (no new resource ids) — the chain refines existing resources (default 2 Ore → 1 Aether) into the global ledger so setup compounds.
- **Persistence:** FOLDED IN now (SaveData → v2; player-built structures survive quit→Continue).
- (Informed re-confirm after the adversarial review:) **keep the full chain** with a deterministic conveyor; **"offline catch-up" = within-session tick math + preserved stockpile** across quit (NO wall-clock minting — honors the determinism pillar).
## Process (ultracode: workflows + adversarial verification)
- **Phases 14 (research + design):** code-graph scan + context7 API verification (NetworkTick/NetworkTime accessors, ECB singleton, server-only `IBufferElementData` stays off the wire); a 3-lens design swarm — code architect · persistence/authoring architect · **adversarial netcode/determinism/reuse critic**. The critic earned its keep: flagged the conveyor as the dominant complexity/risk, a catch-up `period=0` div-by-zero + per-tick/catch-up **double-count** footgun, the restore **tick-rebase** blocker, and the `StorageEntry` singleton-collision; all folded into the plan.
- **Phase 5:** plan approved; the two informed forks resolved (keep full chain; within-session catch-up + preserved stockpile).
- **Phase 6 (source workflow → controlled MCP apply):** a draft→critique Workflow produced the source bundle. **Incident:** the Workflow's general-purpose draft agents ignored the "return source only" instruction and raw-`Write`-wrote the 3 systems + 5 tests into `Assets/` — which **compile but get no `.meta`/test-discovery until `refresh_unity scope=all mode=force`** (logged as native memory [[raw-written-cs-needs-full-refresh]]). Recovered: kept the (high-quality, internally-consistent) agent systems + tests, authored the foundation to satisfy them, and applied everything via MCP with reconciliations — the key one: **`CyclesDue` lower-bound 0, not 1** (prevents a premature mint on a restored `remaining==0` machine), and `BuildPlaceSystem` stamps `LastProcessedTick=0` so runtime-placed machines hit `NeedsInit`.
## Done — code (compiles clean; EditMode 187→**190**)
- **Components** (`Simulation/Automation/AutomationComponents.cs`): `Harvester{byte ResourceId;int Yield;int PeriodTicks}`, `Fabricator{byte In/OutResourceId;int In/OutAmount;int PeriodTicks}`, `Conveyor{byte Direction;int PeriodTicks}`, server-only buffers `MachineInput`/`MachineOutput`, `ConveyorItem` (enableable, baked DISABLED), `RuntimePlacedTag`. The DR-014 "recipe column on `StructureCatalogEntry`" was **deliberately dropped** — dead weight; the recipe is baked on each machine component (server-only, not needed client-side).
- **Pure math** (unit-tested, byte-only): `ProductionMath` (`NeedsInit`/`CyclesDue`/`RemainingTicks`/`RestoreNextTick` — the single gated catch-up path), `ConveyorMath` (`DirOffset`/`CellKey`/**`ResolveMoves`** — deterministic + order-independent: snapshot → stable-sort by CellKey → at-most-one destination claim → stall losers no-loss), `MachineSlotMath` (byte-id deposit/withdraw/total, the non-replicated twin of `StorageMath`).
- **Systems** (`Server/Automation/`, server-only, ordered `[UpdateAfter(PredictedSimulationSystemGroup)]` chain Harvester→Conveyor→Fabricator): `HarvesterProductionSystem` (fixed-yield → its `MachineOutput`), `ConveyorTransportSystem` (pull from adjacent upstream `MachineOutput``ResolveMoves` → settle `ConveyorItem` enable-bit / deposit to sink `MachineInput`), `FabricatorProductionSystem` (input-limited recipe → GLOBAL ledger). `Tuning.MaxProductionCatchup = 600`.
- **Persistence (SaveData v2):** `StructureSave[]` + flat `StructureIoRow[]` (cooldown as epoch-independent REMAINING ticks; in-flight conveyor item inline); `SaveComponents` `PendingStructure`/`PendingStructureIo` staging buffers; **shared `SaveStructureScan.Collect`** (both autosave + quit-save scan ONLY `RuntimePlacedTag` structures → kills the two-path drift the critic flagged); `SaveWriteSystem` + `WorldLauncher` (StagePendingSave + TrySaveFromServer) extended; new **one-shot `BaseRestoreSystem`** (gates on `StructureCatalog`+`BaseAnchor`+`NetworkTime`, replays each saved structure CHARGE-FREE, refills buffers + conveyor item, re-tags, self-destructs).
- **Build wiring:** `BuildPlaceRequest` +`byte Direction` (additive scalar; RPC wire-hash shifts → single-build); `BuildPlaceSystem` stamps conveyor `Direction` (via a `ComponentLookup<Conveyor>` on the prefab) + `RuntimePlacedTag` + `LastProcessedTick=0`; `BuildSendSystem` H/F/C keys + `[`/`]` conveyor-rotate + editor statics `PlaceHarvester/Fabricator/Conveyor`.
- **Authoring + prefabs:** 3 MonoBehaviours + bakers; `StructureCatalogAuthoring` +3 rows (Harvester 20 / Fabricator 30 / Conveyor 2 Ore); `Harvester/Fabricator/Conveyor.prefab` (duplicated from `Turret.prefab` → swapped authoring; ownerless-interpolated `GhostAuthoringComponent` + mesh free) wired into the catalog in the Gameplay subscene.
## Validation
- **EditMode 190/190** (35 new automation tests): catch-up gating/clamp/no-mint/period-0-guard; conveyor Y-junction tie-break + 4-cell line + shuffle-invariance + blocked-stall; fabricator input-limit (no mint-from-nothing); SaveData v2 round-trip + epoch-independent cooldown.
- **Runtime (real netcode ServerWorld+ClientWorld, focused editor, via `execute_code`):** catalog bakes all 6 entries (T1/T5/T6 + **T2/T3/T4 machine prefabs valid**); placed a live H(18,18)→C(19,18 +X)→F(20,18) chain (RPC → cost Ore 200→148); the chain **ran end-to-end** (Harvester output flowed → Conveyor transported → Fabricator `2 Ore → 1 Aether` → ledger), **SERVER Aether == CLIENT Aether** (22, then 52 — replicated). **Quit autosaved** v2 (3 structures + rebased `RemainingTicks`); **Continue restored** all 3 at the exact cells, **charge-free** (Ore unchanged at 148), born-correct ledger, and the **restored chain resumed producing**. Console clean of errors throughout.
## Decisions
[[DR-020_M7_Automation_Production_Chains]]. Resolves the open [[Backlog]] "production replication (predicted vs server-only)" question: **server-only** — production runs in the plain server `SimulationSystemGroup`; results replicate via the existing global ledger + `PlacedStructure` ghosts, never predicted.
## Next-session intent
- **Throughput visuals** (item-on-belt): server-only machine buffers don't reach the client (clients see only `PlacedStructure.Type`); add ONE small replicated byte if live belt visuals are wanted.
- **Build-palette HUD + ghost preview** + a conveyor-facing indicator (dev `[`/`]` rotate + H/F/C keys only today).
- **Relevancy ceiling:** `RegionRelevancySystem`'s O(structures×connections)/tick scan becomes load-bearing at higher conveyor counts (DR-014 ceiling note) — batch it when counts grow.
- **Recipe depth:** multi-input fabricator recipes; fabricator→conveyor output chaining (additive — give the fabricator a `MachineOutput`); per-machine distinct meshes (machines reuse the Turret mesh as a placeholder).
- **Operator live play-through** + a real 2-build LAN run to exercise the chain under latency (single-client validated).
@@ -0,0 +1,53 @@
---
date: 2026-06-06
type: session
tags: [session, cleanup, alignment, docs, refactor, hygiene, connectionui, uitk]
---
# Session 2026-06-06 — Cleanup & alignment (docs, dead code, refactor) before more development
## Goal
Operator: *"Start a cleanup session — clean up documentation code and align everything in the project so it's ready for more development."* Run via `/dots-dev` (ultracode).
Three large 2026-06-05 sessions (M7 Automation, Game-infrastructure menu/settings/saves, HUD rework to UITK) were done but **uncommitted**, with session logs + DRs already written. `CLAUDE.md` had drifted from the code (still described `SampleScene` as the gameplay scene; nothing on Automation / Saves / UITK / the frontend lifecycle), and the logs had flagged cleanup candidates (legacy IMGUI `ConnectionUI`, the `_Recovery/` artifact).
Intake (AskUserQuestion): **deep refactor + docs** · **delete aggressively** · **group & commit for me** (authorizes git writes; commits land on `main`, matching the solo history; no push).
## Process
- **Phase 01 (read-only scan):** editor-state + git + vault scan; a 3-agent Explore swarm (code/asset audit · doc-delta audit · dead-asset + git-grouping audit). Verified the risky findings myself before acting: `ConnectionUI` has **zero code deps** (all hits were `<c>ConnectionUI</c>` doc-comments); `_Recovery` = 2 tracked + 1 untracked recovery scenes; **no new asmdefs** (the new feature folders live inside the existing four — corrected an agent's wrong "add `ProjectM.Automation`/`Persistence` rows" suggestion).
- **Plan-gate:** scoped "deep refactor" to **behavior-preserving** (DRY / dead-code / readability) — explicitly NOT rewriting working netcode/sim; the determinism-critical paths (`ProductionMath.CyclesDue`, `ConveyorMath.ResolveMoves`, `BaseRestoreSystem`) were left untouched. Approved → executed A→F.
## Done
### A — Dead-asset deletion (via `manage_asset`, AssetDatabase-consistent)
- Deleted `Assets/_Recovery/` (3 crash-recovery scenes + metas; zero references, not in build settings) and the stray empty `Assets/Resources.meta` (top-level `Assets/Resources/` was empty). Kept `Assets/_Project/Resources/` (`RuntimePanelSettings` + theme) and `Assets/UI Toolkit/UnityThemes` (UITK default scaffolding).
### B — Removed the legacy IMGUI `ConnectionUI`
- Removed the standalone `NetConnectionUI` GameObject from `Game.unity`, `DevSandbox.unity`, `SampleScene.unity` (each only held Transform + `ConnectionUI`); deleted `ConnectionUI.cs` (+ meta); confirmed zero remaining GUID references in any scene. Repointed the 4 stale doc-comments (`GameBootstrap`, `DebugOverlay`, `EditorAutoHostSystem`, `ConnectionConfig`) from `<c>ConnectionUI</c>` to the UITK `MainMenuController` / `WorldLauncher` (via `script_apply_edits anchor_replace`). The UITK frontend menu owns Host/Join+IP everywhere now.
### C — Targeted behavior-preserving refactor (all via MCP, verified vs live file)
- **DRY'd the duplicated UITK `Round`/`Border` helpers:** `MenuUi` is now the canonical home (made `public`); `HudUi`'s byte-identical copies deleted and its 2 internal call sites + `HudSystem`'s 3 call sites repointed to `MenuUi.*`. `HudUi` is now a genuine thin extension.
- **`HudSystem`:** cache `_huskQuery.CalculateEntityCount()` once/frame (was called twice).
- **`BuildSendSystem`:** replaced the 6 near-identical hotkey `if` branches (drifting `cell`/`wcell`/… vars) with a `static readonly (Key, byte)[] s_BuildHotkeys` table + one loop — adding a buildable is now one row.
- Deliberately **skipped** low-value/higher-risk tidies (SettingsService clamp order, ConveyorTransport struct-array, WorldFeedback lookup, PlayerInput dedup) → logged as a [[Backlog]] follow-up.
### D — Documentation alignment
- **`CLAUDE.md`** (30.7 KB → 36.4 KB, still < the 40 KB limit): rewrote **Bootstrap & worlds** (`AutoConnectPort=0`, editor-instant-play vs frontend toggle, MainMenu(0)/Game(1) scene split, on-demand `WorldLauncher` lifecycle + subscene-streams-into-on-demand-world rule); added an **M7 Automation** + **disk-persistence** bullet to Build/structures (replacing the now-false "deferred to post-M7"); replaced the "uGUI HUD" claim with a **UITK HUD + build-palette** bullet; added build-gotchas (frontend world lifecycle, `CreateLocalWorld` internal, don't-dispose-worlds-in-a-system, URP `m_AssetVersion`-from-the-future, raw-`Write` new-`.cs`-needs-force-refresh, `script_apply_edits` anchor/delete_method on ISystems); noted the new feature folders fit the **existing four asmdefs** (no new assemblies).
- **Vault:** `Milestones` (M6 + the 2026-06-04 polish pass → ✅ Done; added rows for the HUD rework + this cleanup), `Backlog` (ConnectionUI item → done; added a delivered-note + a cleanup-follow-ups section), `Home` + `Systems_Index` (date + latest-session pointer). New DR not warranted (hygiene, no new architecture).
## Validation
- **EditMode 194/194** (`ProjectM.Tests.EditMode`, 0.29 s) — same count as pre-cleanup → no regressions, no tests lost; proves the whole codebase (refactor + ConnectionUI removal + comment edits) compiles clean.
- **Play smoke (focused editor, real netcode):** `ServerWorld` + `ClientWorld` each `conns=1 players=1` (worlds boot → loopback connect → player spawn + replicate → subscene streamed fine after the scene edits); the **UITK HUD renders correctly** (`Cleanup_PlaySmoke.png`) — rounded bars (`MenuUi.Round`), the 6-buildable palette with borders (`MenuUi.Border` + the hotkey/palette path), "HUSKS 0" (cached count), and **no "Net: Connected" IMGUI label** (ConnectionUI gone). Console clean of project errors.
- **Note:** the only console "errors" are engine-internal `Assert`s — *"Access version should be odd when acquiring lock"* from `Modules/Audio/Public/DualThreadManager.h:197` — that fire on domain reload / in Play. They are NOT from project code (audio-thread engine noise), are Assert-level, and don't affect tests/runtime. Left as-is (not a cleanup-pass concern).
## Decisions
No DR (hygiene pass). Reaffirmed conventions: edit `Assets/*.cs` only via MCP; `script_apply_edits` `anchor_replace`/`delete_method` are safe for comment/method edits even on a `struct : ISystem`; `manage_asset delete` keeps the AssetDatabase consistent for asset removals.
## Next-session intent
- Resume feature work on the now-aligned, committed base. Candidate threads from the M7 follow-ups: throughput/belt visuals (one replicated byte), recipe depth (multi-input fabricator, fabricator→conveyor chaining), per-machine meshes; operator live play-through + a real 2-build LAN run.
- Optional code-tidy follow-ups (logged in [[Backlog]]) if churn budget allows.
@@ -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.