Files
Project-M/CLAUDE.md
T
2026-05-31 21:35:12 -07:00

102 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Project M — CLAUDE.md
Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-authoritative, input-only clients, client prediction. This file is committed and is the authoritative, cross-machine source of conventions. The `/dots-dev` skill drives feature work; the one-time stack setup lives in `Docs/dots-setup-task.md`.
## Stack — reverting to Unity 6.4.7 (stable) as of 2026-05-30
| Package | Version | Notes |
|---|---|---|
| `com.unity.entities` | **6.4.0** | Entities/Collections/Graphics track the **Editor** version (6.x). |
| `com.unity.entities.graphics` | **6.4.0** | Renders entities under URP 17.4. |
| `com.unity.collections` | 6.4.0 | (transitive) |
| `com.unity.netcode` | **1.13.2** | Netcode **for Entities** (ECS). NOT `com.unity.netcode.gameobjects`. Independent 1.x line on Unity 6.4. |
| `com.unity.physics` | **1.4.6** | Unity Physics (DOTS). Independent 1.x line on Unity 6.4. |
| `com.unity.transport` | 2.7.2 | (transitive) |
| `com.unity.burst` | 1.8.29 | (transitive) |
| `com.unity.mathematics` | 1.3.3 | (transitive) |
> ⚠️ The values above are the **Unity 6.4.7** set being reverted to — **verify against `packages-lock.json` after the editor downgrade re-resolves**, and reconcile `manifest.json` if the brief 6.6 upgrade left explicit version pins.
> **Version history & status (2026-05-30):** built on **6.4.7** (`6000.4.7f1`; Netcode 1.13.2 / Physics 1.4.6 / Entities 6.4.0). Briefly upgraded to **6.6.0a6**, where Netcode→6.6.0, Physics→6.5.0, Entities→6.5.0 all **renumbered** into the editor line — BUT the alpha's **Netcode/Transport runtime is broken** (all in-editor connections fail with "invalid wrapped network interface"; **confirmed engine bug** via a zero-gameplay repro — see `Docs/Vault` DR-002 and `Docs/UnityBugReport-Netcode-Transport-6.6.0a6.md`). **→ Reverting to Unity 6.4.7 for stable netcode runtime.** If returning to 6.6 later, expect the renumber and re-test the runtime. The M1 player slice should port to 6.4 / Netcode 1.13.2 with no or minimal changes — recompile and `read_console` after the downgrade.
## Namespaces & assembly split
Root namespace: **`ProjectM`**. Code lives under `Assets/_Project/Scripts/` in four asmdefs (never create/edit `.csproj`/`.sln`; only `.asmdef`):
| Assembly | Namespace | Runs in | References |
|---|---|---|---|
| `ProjectM.Simulation` | `ProjectM.Simulation` | **client + server** worlds | Entities, **Unity.Transforms**, Collections, Mathematics, Burst, Unity.Physics, Unity.NetCode |
| `ProjectM.Client` | `ProjectM.Client` | client world only | + Simulation, Unity.Entities.Graphics, **Unity.InputSystem** |
| `ProjectM.Server` | `ProjectM.Server` | server world only | + Simulation, **Unity.Transforms**, Unity.NetCode |
| `ProjectM.Authoring` | `ProjectM.Authoring` | bake time (+ scene runtime) | Simulation, Entities, **Unity.Entities.Hybrid**, Collections, Mathematics, Unity.NetCode |
- **Simulation** = components + systems shared by both worlds (most gameplay). **Client/Server** = world-specific. **Authoring** = `…Authoring` MonoBehaviours + `Baker<T>`.
- Other folders: `Assets/_Project/Subscenes/` (baked entity subscenes), `Assets/_Project/Prefabs/`, `Assets/_Project/Tests/EditMode/`.
### Build gotchas (learned — M1, 2026-05-30)
- **`Unity.Transforms` must be a DIRECT asmdef reference** for any assembly whose source-gen'd systems use `LocalTransform`/`LocalToWorld`. It is its own assembly; transitive visibility compiles your hand-written code but the Entities generator emits **CS0246** inside the `*.g.cs`.
- **Authoring asmdefs need `Unity.Entities.Hybrid`** (defines `Baker<T>`) **and `Unity.Collections`** (baking source-gen). A nested baker class must **not** be named `Baker` (it shadows `Baker<T>` → CS0308/CS0246) — name it `FooBaker`.
- **Never name an `IComponentData` `PlayerInput`**, and don't `using UnityEngine.InputSystem;` in a file that references such a component: it collides with `UnityEngine.InputSystem.PlayerInput`, and the Entities generator binds `RefRW<…>` to the *managed* class → a misleading **CS8377 "must be a non-nullable value type"**. Fully-qualify Input System types (`UnityEngine.InputSystem.Keyboard.current`) instead.
- `IInputComponentData` requires implementing **`FixedString512Bytes ToFixedString()`**.
- An input-gather system that reads the managed Input System belongs in `GhostInputSystemGroup` as a **non-Burst `ISystem`** (or `SystemBase`), never inside the prediction loop.
### Build gotchas (learned — M2 combat, 2026-05-31)
- **The generated Input Actions C# wrapper must live inside an asmdef** any system needs to reference. By default it generates next to the `.inputactions` (e.g. `Assets/Settings/`), which has no asmdef → it compiles into `Assembly-CSharp`, and asmdef assemblies (`ProjectM.Client`) **cannot** reference that. Fix: set the importer's `wrapperCodePath` (in the `.inputactions.meta`) to a path inside the consuming asmdef, e.g. `Assets/_Project/Scripts/Client/Input/ProjectMInput.cs`, and delete the old generated file. Read the action map via a managed `SystemBase` holding the wrapper; gather `Fire` as a netcode **`InputEvent`** (reset the field each frame, `.Set()` on the press edge — netcode latches the absolute `Count` into the command buffer; the live component value is only the per-tick delta).
- **Predicted-spawn classification cannot be `[BurstCompile]`d (Netcode 1.13.2).** The cross-assembly generic `Unity.NetCode.LowLevel.SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory<T>()` trips a Burst **internal compiler error** (type-hash resolution). Make the classifier a plain non-Burst `ISystem` (it only runs when spawns are received — cold path). In 1.13.2 that method takes **`ref DynamicBuffer<SnapshotDataBuffer>`** (the public HelloNetcode sample's by-value `data` is from an older version).
- **A Burst *internal compiler error* corrupts the editor's Burst incremental cache.** After the error is fixed in code, newly-added `[BurstCompile]` entry points (systems **and** generated ghost-component serializers) keep logging `"... is not a known Burst entry point"` and run managed-fallback (slow → server tick-batching, ~3040s play-enter). A clean compile + green tests + working runtime confirm the code is fine. Clear it with an **editor restart** (or delete `Library/BurstCache` while closed) — a domain reload alone does not.
- **Projectile/area hit tests must be swept, not point checks.** A point distance check tunnels straight through a target when the per-tick step exceeds the target radius — at high projectile speed *or* whenever the server tick-batches under load. Test the segment the projectile traversed this tick (`[curPos - dir*speed*dt, curPos]`) against each target; order the damage system `[UpdateAfter(MoveSystem)]`. (Caught at runtime, not by a point-based unit test — cover hit detection with a tunnelling regression test.)
- **In-editor input injection needs a focused Game view — unless you change two settings.** By default the Input System ignores injected/real device input while the Game view is unfocused, so headless (MCP `execute_code`) keypress simulation won't drive `IInputComponentData`. Fix (both now set in this project): `InputSettings.editorInputBehaviorInPlayMode = AllDeviceInputAlwaysGoesToGameView` + `Application.runInBackground = true`. For deterministic, device-independent validation prefer the editor-only **`DebugInputInjectionSystem`** (`ProjectM.Client`, `#if UNITY_EDITOR`): poke its statics from `execute_code``DebugInputInjectionSystem.Fire()` / `.SetMove(x,z)` / `.SetAim(x,z)` / `.Stop()` — to drive the local player's `PlayerInput` through the authentic command→prediction pipeline. (Validated: `SetMove` drives + replicates movement. One-shot `Fire` propagation needs a healthy editor — tick-batching under a degraded/corrupt-Burst editor drops one-shot `InputEvent`s while continuous values survive.)
- **Prototype presentation glue lives in `ProjectM.Client` as MonoBehaviours.** `PrototypeCameraRig` (on the Main Camera) is a tunable player-following ARPG cam (default mid 3/4 ~45° perspective) that reads the local player ghost's `LocalTransform` each LateUpdate. Bright prototype URP-Lit materials are in `Assets/_Project/Materials/` (player cyan, dummy red, projectile yellow, ground grey). `ProjectM.Client` now references `Unity.Transforms` directly (the rig reads `LocalTransform`).
## Bootstrap & worlds
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` → overrides `Initialize`, sets `AutoConnectPort = 7979` (in-editor auto-connect over IPC; set in M1 — was 0), calls `CreateDefaultClientServerWorlds()`. Entering Play Mode creates separate `ServerWorld` (`WorldFlags.GameServer`) and `ClientWorld` (`WorldFlags.GameClient`) — verified.
- `Assets/_Project/Subscenes/Gameplay.unity` is wired into `SampleScene` (GameObject `GameplaySubScene`) as a baking target. Replace `SampleScene` with a dedicated bootstrap scene when building for real.
## DOTS / ECS conventions (authoritative summary)
Full rules: `~/.claude/skills/dots-dev/references/dots-conventions.md` (Windows: `%USERPROFILE%\.claude\skills\dots-dev\references\`). These **replace** classic MonoBehaviour/GameObject patterns.
- **`struct : IComponentData`** is the default (unmanaged, Burst/job-friendly). `class : IComponentData` only for genuine managed refs (main-thread, no Burst). `IBufferElementData` for per-entity arrays. `IEnableableComponent` to toggle state without a structural change.
- **Systems:** `ISystem` (struct) + `[BurstCompile]` is the **default**; `SystemBase` only when touching managed objects. `SystemAPI.Query<…>()` to iterate. **Aspects (`IAspect`) are DEPRECATED (Entities 1.4+) — do not author new ones.** `Entities.ForEach` is legacy.
- **Jobs:** `IJobEntity` / `IJobChunk`; thread `JobHandle` through `state.Dependency`; mark inputs `[ReadOnly]`. Allocators: `Temp` (frame), `TempJob` (one job), `Persistent` (must dispose). Burst breaks on managed types/exceptions/reflection/strings.
- **Structural changes** (add/remove component, create/destroy entity) invalidate handles + cause sync points → batch via **`EntityCommandBuffer`** (Begin/End`Simulation`EntityCommandBufferSystem; `.AsParallelWriter()` in parallel jobs).
- **Baking:** `…Authoring` MonoBehaviour + `class FooBaker : Baker<FooAuthoring>``GetEntity(authoring, TransformUsageFlags.…)` then `AddComponent`. Subscenes stream async — entities aren't present the instant a reference exists.
- **Netcode:** ghosts = replicated entities (`GhostAuthoringComponent` + `[GhostField]`); predicted (player-controlled, rolled back) vs interpolated. Core sim runs in `PredictedSimulationSystemGroup` (fixed step, **runs multiple times per frame** on rollback → must be deterministic/idempotent; filter with `.WithAll<Simulate>()`). **Server-authoritative: clients send input (`IInputComponentData`), not state.** RPCs (`IRpcCommand`) for one-off events. **No wall-clock/`Time.deltaTime`/`System.Random` in predicted sim.**
- **Always verify volatile DOTS/Netcode API shape via context7 at code-time** — do not trust memory. See `context7-libraries.md`. Pinned IDs for our versions: Entities → `/websites/unity3d_packages_com_unity_entities_6_5_manual`; Netcode → `/websites/unity3d_packages_com_unity_netcode_1_10_api` (closest published; we run 1.13.2 — re-resolve if a 1.13 set appears); ECS samples → `/unity-technologies/entitycomponentsystemsamples`.
## Testing
- **Default pattern = plain-Entities EditMode test:** create a `World`, register the system in `SimulationSystemGroup`, tick, assert. Public API, always green, version-independent. Example: `Assets/_Project/Tests/EditMode/HeartbeatSystemTests.cs`. Run via Unity Test Runner or MCP `run_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"])`.
- **`NetCodeTestWorld` is `internal`** in netcode 1.13.2 (`Unity.NetCode.Tests`, assembly `Unity.NetCode.TestsUtils.Runtime.Tests`), exposed only via a fixed `[InternalsVisibleTo]` allow-list of Unity assemblies. To use it you must name a test asmdef to match an entry (e.g. `Unity.NetcodeSamples.EditModeTests`) — or vendor the test utils. See `Docs/Vault/07_Sessions/_Decisions/DR-001_Netcode_Test_Harness.md`. **This does not change on Unity 6.6.** Netcode world boot is covered by the Play Mode check, not a NetCodeTestWorld test.
- Burst/source-gen errors surface at editor compile, not a plain build — always check `read_console` after script changes, and run a play/tick test, not just a compile.
## Guardrails
- **Never** edit a `.meta` independently of its asset; delete an asset **and** its `.meta` together.
- **Never** read/write `Library/`, `Temp/`, `obj/`, `Logs/`, `UserSettings/` (generated/cache). Use MCP resources for editor state instead.
- **Never** create/edit/commit `.csproj`/`.sln` — only `.asmdef`.
- **No asset/scene edits during Play Mode.** Check `editor_state.advice.ready_for_tools` before mutating; package adds/refreshes trigger domain reloads — wait for `is_compiling=false`.
## Memory — four layers (which tool when)
| Layer | Use for | Crosses machines? |
|---|---|---|
| **In-repo vault** `Docs/Vault/` | Design docs, decision records (DR-###), session logs, roadmap — human-facing truth | **Yes** (git) |
| **basic-memory** MCP | Semantic/wikilink recall over those same vault files | Yes (indexes the vault) |
| **serena** MCP | C# symbol nav (`find_symbol`, references) of `Assets/_Project/` | N/A (from code) |
| **Native Claude memory** (`memory/`, `MEMORY.md`) | Machine-local facts, working-style, preferences | **No** |
- Where is X defined / who calls it → **serena** (fallback `Grep`/`Glob`). What did we decide / how does Z work → **basic-memory** → read the vault note. Literal string / asset GUID → **Grep/Glob**. Current DOTS API → **context7**. Conventions → this file.
- **Cross-machine rule:** durable truth goes in the **vault** or **this file** (both committed). Native `memory/` is local-only and does NOT sync — never the sole home of a decision.
- **serena C# caveat:** its language server is flaky on Unity (can auto-install the wrong .NET, `.sln` load timeouts). If `find_symbol` errors/stalls, **fall back to `Glob`/`Grep`** (or add `claude-context` with local embeddings as a code-search index). serena live-verification was deferred at setup; confirm on first use.
## Per-machine setup (NOT in git — redo on each machine)
`.mcp.json` is committed and portable (`${CLAUDE_PROJECT_DIR}` only). But each machine still needs:
1. `uv`/`uvx`, the Obsidian app + `obsidian-cli`, and the `dots-dev` skill in `~/.claude/skills/`.
2. **basic-memory project registration** (machine-local config): `uvx basic-memory project add gamevault "<repo>/Docs/Vault" --default`, then `uvx basic-memory reindex --full --search --embeddings --project gamevault`.
3. Unity 6.4 opens the project and the CoplayDev Unity-MCP bridge connects (`mcpforunity://editor/state``ready_for_tools`).