# 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`. - 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`) **and `Unity.Collections`** (baking source-gen). A nested baker class must **not** be named `Baker` (it shadows `Baker` → 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()` 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`** (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, ~30–40s 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` → `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()`). **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 "/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`).