From 9d6977cd64c193658598f50c1e48ae88baafa3a7 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Tue, 21 Jul 2026 13:19:16 -0700 Subject: [PATCH] =?UTF-8?q?Docs:=2007-21=20auto-recast=20root-cause=20corr?= =?UTF-8?q?ection=20(wire-count=20InputEvent=20=E2=98=85),=20harness=20les?= =?UTF-8?q?sons,=20CLAUDE.md=20net-zero=20trims?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- .../dots-dev/references/validation-harness.md | 15 ++++++++++ CLAUDE.md | 12 ++++---- ...026-07-21_AbilityBar_Recast_Diagnosis_I.md | 29 ++++++++++++++++++- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.claude/skills/dots-dev/references/validation-harness.md b/.claude/skills/dots-dev/references/validation-harness.md index 61e537240..235c938fa 100644 --- a/.claude/skills/dots-dev/references/validation-harness.md +++ b/.claude/skills/dots-dev/references/validation-harness.md @@ -87,6 +87,21 @@ DOES propagate into parented children's `LocalToWorld` (verified live on 6.5.0). - Forcing an animation pose: disable the drive system, write params via `AnimatorParametersAspect`; measure ground-contact via feet-Y only on non-deformed geometry. +## Input-path validation (07-21, the auto-recast false negative) + +- **A guardian window must OUTLIVE the period it judges.** Frames ≠ ticks: an unfocused editor can run FASTER + than 60 fps, so a 500-frame watch can end BEFORE a 7 s cooldown does. The morning "sim is clean" verdict was + a false negative for exactly this; the 2000-frame re-run caught a recast at every cooldown reopen. Size the + window in TICKS of the judged period × 2, converted generously. +- **`DebugInputInjectionSystem` writes the COMPONENT — it can never reproduce wire-semantics bugs.** To exercise + the real gather → copy-layer → command-buffer path headless, queue synthetic device events: + `StateEvent.From(Keyboard.current, out var ev); InputControlExtensions.WriteValueIntoEvent(kb.digit1Key, 1f, ev); InputSystem.QueueEvent(ev);` + (+ a 0f release event). Works unfocused with runInBackground. +- **Wire truth: `InputEvent` counts ACCUMULATE across commands** (the per-frame gather reset never reaches the + buffer). Test fixtures that push `InputBufferData` must model that: press = count STEP at one + tick, and the stepped count PERSISTS in every later entry — a "release = count 0" entry is a wire-impossible + shape that hides the ever-pressed class of bug (and double-fires a delta gate). + ## EditMode fixtures for prediction-gated systems (07-21, AbilityFireSystemConeTests) - **`NetworkTime.Flags` is INTERNAL** — a system gated on `IsFirstTimeFullyPredictingTick` silently no-ops in any diff --git a/CLAUDE.md b/CLAUDE.md index 475c692f7..bace74fab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server- - **Size check** — bash: `wc -c CLAUDE.md` · PowerShell: `(Get-Item CLAUDE.md).Length`. Must be `< 40960`. - **Archive, don't delete.** When trimming, append the verbose / least-hot detail to the obsidian reference note `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md` under a **new dated heading** (never overwrite an older snapshot), and leave a one-line pointer + the relevant `[[DR-###]]` link here. - **Net-zero rule:** every addition is paid for by a condensation elsewhere. Keep only the hottest, highest-recurrence operational rules inline (flag them **★**); depth lives in the archive + DRs. -- Condensation history → archive dated headings: 06-17 · 07-04(a–c) · 07-06 · 07-07(b,c) · 07-13 · 07-15 · 07-16 (what each paid is noted in the archive). +- Condensation history → the archive's dated headings (each edit's payment noted there). ## Stack — Unity 6.5.1 (`6000.5.1f1`, stable) as of 2026-06-27 @@ -26,7 +26,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server- | `com.unity.mathematics` | 1.4.0 | (transitive) | | `com.rukhanka.animation` | **2.9.0** | Local pkg (`Packages/com.rukhanka.animation`). ECS skeletal animation (Burst CPU/GPU skinning). Resolves on 6.5.0 via SemVer floor. Netcode replication **OFF** → client-derived. See [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. | -Values match `packages-lock.json` (reconciled 06-17; URP 17.5.0; `ai.assistant` removed 07-04). **History:** 6.6.0a6 transport bug — [[DR-002_Unity66_Alpha_Netcode_Transport]] + archive. +Values match `packages-lock.json` (URP 17.5.0). **History:** 6.6.0a6 transport bug — [[DR-002_Unity66_Alpha_Netcode_Transport]] + archive. ## Namespaces & assembly split @@ -67,7 +67,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **Move ownerless INTERPOLATED ghosts (enemies, pickups) SERVER-ONLY in the plain `SimulationSystemGroup`** — they aren't predicted; the server has no rollback. Stock `LocalTransform` replication carries position (no hand-written `[GhostField]`). A contact `DamageEvent` appended there drains the *following* tick (~16ms, fine for melee). - **`PhysicsVelocity` auto-replicates** (Netcode ships the default variant + serializer) — drive a predicted-physics body by writing `PhysicsVelocity.Linear`, not by teleporting `LocalTransform`. - **Ownerless interpolated ghost ≠ owner-predicted for buffer replication.** A server-spawned ownerless ghost replicates a `[GhostField] IBufferElementData` to all clients with **no `OwnerSendType` / no `GhostOwner`** — server mutations just propagate. `OwnerSendType.All` + `GhostOwner` are only for a predicting owner to recompute its own state. -- **One-off shared-state actions belong on an `IRpcCommand`, not a predicted `InputEvent`** (RPCs are reliable; one-shot `InputEvent`s — like `Fire` — drop under server tick-batching). RPC payloads are plain blittable scalars (`int CellX/CellZ`, not `int2`; no `[GhostField]`). For a SINGLE shared target resolve a **server singleton** — never put an `Entity` in the command; use ghost-id+spawn-tick (`SpawnedGhostEntityMap`) only for many targets. +- **One-off shared-state actions belong on an `IRpcCommand`, not a predicted `InputEvent`** (RPCs are reliable; one-shot `InputEvent`s drop under tick-batching). **★ Raw `InputBufferData` reads: `InputEvent.IsSet` = EVER-pressed — wire counts ACCUMULATE (only the decoded component is delta-corrected); gate on a count-STEP vs the prior tick's command, else one press recasts at every cooldown reopen (07-21).** RPC payloads are plain blittable scalars (`int CellX/CellZ`, not `int2`; no `[GhostField]`). For a SINGLE shared target resolve a **server singleton** — never put an `Entity` in the command; use ghost-id+spawn-tick (`SpawnedGhostEntityMap`) only for many targets. - **Apply server-only RPC effects in the server `SimulationSystemGroup`, NOT the predicted loop** (rollback would double-apply). Mutating a `DynamicBuffer` is not a structural change, so it's safe while iterating a different query. - **A system-ordering CYCLE is INVISIBLE to plain-Entities EditMode tests** (they register systems individually, unsorted) — it only throws `ComponentSystemSorter` "circular dependency cycle" at **world creation (Play)**. When you add cross-system `[UpdateBefore/After]`, re-audit the EXISTING `[Update*]` attributes of the systems you order around and **always Play-validate**. [[DR-017_Persistent_Base_Player_Driven_Pacing]] - **A dev/debug `IRpcCommand` wire TYPE must be UNCONDITIONAL** — the RpcCollection hash must match across release/dev peers; `#if`-gate only the send/receive SYSTEMS, never the struct. **Re-mean bytes, don't rename**: unchanged byte VALUES keep the `[GhostField]` serializer identical → re-bake-free (only authoring *default-value* edits re-bake the subscene). @@ -88,7 +88,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor`. Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`) — changing them invalidates placed structures. - **Structures:** only `Type` replicates (client derives `Cell`); **occupancy is DERIVED** from live ghosts, never baked. See [[DR-014_M6_Build_Structures_Automation_Foundation]]. - **Co-op placement atomicity:** commit `StorageMath.Withdraw` + cell-reservation **in-place in the RPC foreach** (only `Instantiate` via ECB) so two same-tick requests for one cell can't both pass. Ledger spends generally: afford→act else SOFT-FAIL (no cooldown-burn), read LIVE in-loop (no hoist); a Health-less entity silently drops OUT of an aggro snapshot (snapshot ABOVE the early-return). -- **DR-051 purge (07-15) ★:** siege/cycle/core/turret/automation + legacy `AbilityRef` path + onboarding **DELETED** (git = the archive; retired bullets → gotchas archive 07-15). **Retired byte VALUES stay reserved, never renumbered** (`StructureType` 1-4, `ResourceId.Charge`, `DebugOp` 3/10/11, `TuningKnob` 20-23); `DebugOp.SpawnWave`/`EndSiege` RE-MEANT (force-wave / quiet-arena). **Waves UNGATED** — a baked `WaveDirectorAuthoring` decides by placement. Sockets are THE ability model (frame loadout seeded unconditionally at spawn); `CharacterId`→`FrameKind`. [[DR-051_Lantern_Realignment_Purge]]. +- **DR-051 purge (07-15) ★:** siege/cycle/core/turret/automation + legacy `AbilityRef` path + onboarding **DELETED** (git = the archive). **Retired byte VALUES stay reserved, never renumbered** (`StructureType` 1-4, `ResourceId.Charge`, `DebugOp` 3/10/11, `TuningKnob` 20-23); `DebugOp.SpawnWave`/`EndSiege` RE-MEANT (force-wave / quiet-arena). **Waves UNGATED** — a baked `WaveDirectorAuthoring` decides by placement. Sockets are THE ability model (frame loadout seeded unconditionally at spawn); `CharacterId`→`FrameKind`. [[DR-051_Lantern_Realignment_Purge]]. - **Harvest:** in-run nodes→PERSONAL `InventorySlot` (`[GhostField] OwnerSendType.All`, spill→ledger); `G`=deposit at base. Inventory/equipment PAUSED → archive 2026-06-12 + [[DR-026_Inventory_Equipment_Progression_Foundation]]. - **Disk persistence (`SaveData`, single-slot atomic JSON, versioned) ★:** **born-correct load** — `CycleDirectorSpawnSystem` (the ledger/RunInfo/meta host) applies the menu-staged `PendingSave` AT SPAWN; `BaseRestoreSystem` replays structures charge-free + HP. **v7 = a FRESH EPOCH: `MinLoadableVersion = CurrentVersion = 7`** (older saves rejected → New Game); additive again going forward. `RunDirectorSystem`'s terminal bank is the sole autosave trigger. See [[DR-019_Frontend_Menu_Settings_Saves_Build]] + [[DR-051_Lantern_Realignment_Purge]]. @@ -153,7 +153,7 @@ Full rules: `.claude/skills/dots-dev/references/dots-conventions.md` (in-repo; t ## Testing -- **Default = plain-Entities EditMode test:** create a `World`, register the system in `SimulationSystemGroup`, tick, assert. Public API, version-independent. Example: `Assets/_Project/Tests/EditMode/HealthApplyDamageSystemTests.cs`. Run via `run_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"])`. +- **Default = plain-Entities EditMode test:** create a `World`, register the system in `SimulationSystemGroup`, tick, assert. Public API, version-independent. e.g. `HealthApplyDamageSystemTests`. Run via `run_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"])`. - **`NetCodeTestWorld` is `internal`** (6.5.0 re-check: not even loaded outside test asmdefs), exposed only to a fixed `[InternalsVisibleTo]` allow-list — to use it, name a test asmdef to match an entry (e.g. `Unity.NetcodeSamples.EditModeTests`) or vendor the test utils. Netcode world boot is covered by the Play Mode check, not a NetCodeTestWorld test. See [[DR-001_Netcode_Test_Harness]]. - Burst/source-gen errors surface at editor compile, not a plain build — always `read_console` after script changes, and run a play/tick test, not just a compile. **Cover swept hit-detection with a tunnelling regression test** (the point-check tunnel bug doesn't surface in a point-based unit test). @@ -173,4 +173,4 @@ Full protocol + per-layer detail: [[Documentation_Protocol]] (`Docs/Vault/_Meta/ ## Per-machine setup (NOT in git — redo on each machine) -`.mcp.json` is committed + portable (`${CLAUDE_PROJECT_DIR}`); the **`dots-dev` skill travels with the repo** (`.claude/skills/dots-dev/`). Each machine still needs: (1) `uv`/`uvx` + Obsidian app + `obsidian-cli` (the `unity-mcp-skill` + native `memory/` are machine-local, don't sync); (2) basic-memory registration — `uvx basic-memory project add gamevault "/Docs/Vault" --default` then `uvx basic-memory reindex --full --search --embeddings --project gamevault`; (3) Unity 6.5 open + the Unity-MCP bridge connected (`mcpforunity://editor/state` → `ready_for_tools`). +`.mcp.json` is committed + portable (`${CLAUDE_PROJECT_DIR}`); the **`dots-dev` skill travels with the repo** (`.claude/skills/dots-dev/`). Each machine still needs: (1) `uv`/`uvx` + Obsidian app + `obsidian-cli` (machine-local, don't sync); (2) basic-memory registration — `uvx basic-memory project add gamevault "/Docs/Vault" --default` then `uvx basic-memory reindex --full --search --embeddings --project gamevault`; (3) Unity 6.5 open + the Unity-MCP bridge connected (`mcpforunity://editor/state` → `ready_for_tools`). diff --git a/Docs/Vault/07_Sessions/2026/2026-07-21_AbilityBar_Recast_Diagnosis_I.md b/Docs/Vault/07_Sessions/2026/2026-07-21_AbilityBar_Recast_Diagnosis_I.md index edc49ae60..8e1fe8820 100644 --- a/Docs/Vault/07_Sessions/2026/2026-07-21_AbilityBar_Recast_Diagnosis_I.md +++ b/Docs/Vault/07_Sessions/2026/2026-07-21_AbilityBar_Recast_Diagnosis_I.md @@ -3,7 +3,34 @@ **Operator: "after casting an ability - it seems to auto recast - also I want to be able to see the cds of socketed abilities + dash. do a rework of the ui."** -## Auto-recast: diagnosed NOT a sim bug — a telegraph misread (fixed) +> ## ⚠ CORRECTION (same day, operator re-report): the auto-recast WAS a real sim bug +> +> The section below stands as the record of a **false negative**: the morning guardian window (500 frames on a +> fast unfocused editor) ended BEFORE the 7 s Vortex cooldown did, so "1 press = 1 stamp over 8 s" proved +> nothing about the reopen. The operator's sharper re-report ("as soon as the cd is over it recasts") forced a +> longer watch through the REAL input path (synthetic `QueueStateEvent` keyboard taps), which reproduced a +> **self-sustaining recast loop: a new cast at EVERY cooldown reopen, forever, from one press** +> (`t3933 NF0+zone; t4353 NF0+zone; …`). +> +> **Root cause (pre-existing, shipped with the 07-15 socket kit):** Netcode's copy layer ACCUMULATES +> `InputEvent` counts on the wire — the gather's per-frame reset never reaches the command buffer (live proof: +> every buffered command carried `Socket0.Count=2, IsSet=true` while both decoded components read clean). Only +> the decoded COMPONENT is delta-corrected (why Blink/melee/dash never looped). `AbilityFireSystem`'s windup +> resolve reads the RAW buffer (`GetDataAtTick`) and gated on `IsSet` = `Count > 0` = "ever pressed" — so after +> the first press of a session, every socket refired the instant its cooldown reopened. +> +> **Fix:** the gate is now a count-STEP vs the previous tick's command (Netcode's own decode semantics; missing +> previous command = no-fire — dropping a buffer-edge windup press beats an infinite loop). Test helper +> rewritten to model the wire truth (monotonic counts persisting across commands) + regression +> `Cast_Never_Refires_When_The_Cooldown_Reopens` (fails against the old gate). **430/430**; live re-verify: +> one tap → one cast → silence across the reopen + two more full periods. The zone-telegraph drain-inward +> change below remains correct (it WAS also visually mimicking recasts) but was not the root cause. +> +> **Lessons banked:** a guardian window must OUTLIVE the period it's judging (frames ≠ ticks on a fast editor); +> `DebugInputInjectionSystem` writes the component and can NEVER reproduce wire-semantics bugs — synthetic +> `QueueStateEvent` through the real gather can; the wire-count gotcha is now a CLAUDE.md ★. + +## Auto-recast: the original (WRONG) presentation-only diagnosis — kept for the record - Static analysis: the gather is edge-clean (`Socket0 = default` + `.Set()` only on `wasPressedThisFrame`, PlayerInputGatherSystem:180-186); the injection system self-expires. No sim path to a refire.