Docs: 07-21 auto-recast root-cause correction (wire-count InputEvent ★), harness lessons, CLAUDE.md net-zero trims
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<float>(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<PlayerInput>` 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
|
||||
|
||||
@@ -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 "<repo>/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 "<repo>/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`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user