--- title: CLAUDE.md Build-Gotchas Archive type: reference created: 2026-06-04 --- # CLAUDE.md Build-Gotchas Archive This note holds the **full, verbose build-gotcha entries** that were condensed out of the committed `CLAUDE.md` to keep that file under its 40 KB context-load limit. The condensed one-liners in `CLAUDE.md` link here and to the per-milestone Decision Records (`Docs/Vault/07_Sessions/_Decisions/DR-###`). Nothing was deleted — this is the long-form source of truth for the operational lessons; the DRs carry the design rationale. **Condensation passes:** - **2026-06-04** (first pass) — M1–M6 long-form build lessons → the milestone sections below. - **2026-06-07** (second pass) — the newer M7+/HUD/animation distilled bullets were further tightened in `CLAUDE.md`; a **full verbatim snapshot of the pre-2026-06-07 `CLAUDE.md`** is appended at the very bottom of this note (`## 2026-06-07 — Pre-trim CLAUDE.md snapshot`) so every removed detail stays recoverable. > When a gotcha here is later proven wrong or superseded, strike it through and note the superseding DR rather than deleting it. --- ## Version history & stack status > ✅ Reconciled 2026-06-02: `manifest.json` pins aligned to the resolved **Unity 6.4.7** lock (entities/entities.graphics 6.4.0, URP 17.4.0, test-framework 1.6.0, ugui 2.0.0, multiplayer.center 1.0.1) — a no-op re-resolve (lock unchanged, console clean). The values in the CLAUDE.md stack table match `packages-lock.json`. See [[DR-008_M5_HomeBase_BaseLayer_Storage]]. > **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. --- ## 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`). ## Build gotchas (learned — M5 physics-in-prediction, 2026-06-01) - **Editing Assets `.cs` with the raw `Write` tool does NOT reliably trigger a Unity recompile** on an unfocused editor — `refresh_unity` did a domain reload *without* recompiling, so tests + `execute_code` ran a **stale assembly** (symptom: behaviour that exists in *neither* the old nor new source). **Always edit Assets `.cs` via MCP `apply_text_edits` / `create_script`** (Unity's own scripting pipeline) — never `Write`. (`Write`/`Edit` are fine for non-asset files: vault, asmdef JSON, etc.) See [[2026-06-01_M5_Physics_In_Prediction]]. - **Predicted physics is implicit — there is no `PredictedPhysics` toggle.** With the netcode-physics package present (`Unity.NetCode.Physics`, `…Physics.Hybrid`) and predicted ghosts carrying physics components, Netcode relocates `PhysicsSystemGroup` into the **`PredictedFixedStepSimulationSystemGroup`** (a child of `PredictedSimulationSystemGroup`, marked **OrderFirst**). `NetCodePhysicsConfig` only tunes lag-comp / run-mode / history. Put one in the gameplay subscene with `PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities` so the group runs whenever physics entities exist. - **Unity Physics 1.x bakes built-in `UnityEngine` colliders + `Rigidbody`** — the old `PhysicsShapeAuthoring`/`PhysicsBodyAuthoring` (Physics 0.x) are **gone** (`unity_reflect` finds neither). Author a dynamic body with a `CapsuleCollider`/`BoxCollider` + `Rigidbody` (`useGravity=false` → planar/`PhysicsGravityFactor`=0; `isKinematic=false`; `interpolation=Interpolate` → `PhysicsGraphicalSmoothing`). Static colliders = collider, no Rigidbody, baked into the subscene (present identically in server + client worlds, deterministic, no replication). - **`PhysicsVelocity` auto-replicates** — Netcode ships `PhysicsVelocityDefaultVariant` + a generated serializer, so a predicted-physics ghost needs **no hand-written `[GhostField]`** for velocity (`LocalTransform` is already replicated). Drive the character by writing `PhysicsVelocity.Linear`, not by teleporting `LocalTransform`. - **`Rigidbody.FreezeRotation` is NOT honored by the DOTS baker** (baked `PhysicsMass.InverseInertia` stays non-zero). Hold a top-down character's facing by zeroing angular velocity each tick + writing rotation directly (`PlayerAimSystem`); set `PhysicsMass.InverseInertia = float3.zero` in a baker/system if a hard lock is needed. - **Gravity-off bodies accumulate vertical contact impulses permanently** (a capsule rides up a box edge and floats away — looks like tunnelling, isn't). Pin players to the movement plane *after* the physics step: a system in `PredictedSimulationSystemGroup` `[UpdateAfter(PredictedFixedStepSimulationSystemGroup)]` clamping Y to `PlayerSpawner.SpawnPoint.y` + zeroing `Linear.y` (`PlayerPlanarConstraintSystem`). - **The predicted physics group is OrderFirst**, so a system in `PredictedSimulationSystemGroup` with `[UpdateBefore(PredictedFixedStepSimulationSystemGroup)]` is **ignored** (OrderFirst/OrderLast wins) → 1-tick velocity offset (consistent across server/client/rollback — prediction stays in sync). For same-tick application, put the system *inside* `PredictedFixedStepSimulationSystemGroup` `[UpdateBefore(Unity.Physics.Systems.PhysicsSystemGroup)]` (verified to sort before the step) — but expect cosmetic "invalid UpdateBefore" warnings from the relocation. > **Note (M5b):** `PlayerMoveSystem` + `PlayerPlanarConstraintSystem` from this milestone were **deleted** when the player became a Unity Character Controller. The predicted-physics infra (`NetCodePhysicsConfig`, baked static walls) is **kept**. See the M5b section. ## Build gotchas (learned — M5b Unity Character Controller, 2026-06-01) - **The player is now a Unity Character Controller kinematic character, NOT a dynamic Rigidbody.** `PlayerMoveSystem` + `PlayerPlanarConstraintSystem` (M5) are **deleted**. Movement: `PlayerControlSystem` maps `PlayerInput.Move` × `EffectiveCharacterStats.MoveSpeed` → `CharacterControl` (via the unit-tested `CharacterControlMath.DesiredMovement`); `CharacterProcessor` (collide-and-slide) consumes it in `CharacterPhysicsUpdateSystem` (`[UpdateInGroup(KinematicCharacterPhysicsUpdateGroup)]`, relocated into the predicted loop). The DR-006 predicted-physics infra (`NetCodePhysicsConfig`, baked static walls) is **kept** — the CC character sweeps against that same PhysicsWorld. - **A package declaring an older `com.unity.entities`/`com.unity.physics` dependency can still resolve on our renumbered stack** — Unity treats the dep as a SemVer **floor**, so Entities 6.4.0 satisfies a `1.3.15` requirement and is NOT downgraded. Don't trust a version-string mismatch as "incompatible": **probe** (add the package, confirm `packages-lock.json` kept Entities 6.4.0 / Physics 1.4.6 / Netcode 1.13.2 + a clean compile; rollback if not). CC 1.4.2 verified this way. - **CC 1.4.2 API shape = `IKinematicCharacterProcessor` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*`.** The legacy `KinematicCharacterAspect` (IAspect, instance `Update_*`) also exists but is NOT what the 1.4.x samples use — verify the installed shape with `unity_reflect`, don't assume. (A sub-agent's package-cache read disagreed with reflect; reflect + first-try clean compile won.) - **`KinematicCharacterUtilities.BakeCharacter` aborts** (logs an error, adds nothing) **if the GameObject has a `Rigidbody`** and requires uniform (1,1,1) scale. The player prefab keeps its `CapsuleCollider` (baked into `PhysicsCollider`) but the M5 `Rigidbody` was removed. Two bakers on one prefab GameObject (`PlayerAuthoring` + `PlayerCharacterAuthoring`) is fine — both resolve the same entity. - **`CharacterInterpolation` must be PredictedClient-only.** `BakeCharacter` adds it to all prefab versions; a `DefaultVariantSystemBase` registers `CharacterInterpolation → [GhostComponent(PrefabType = GhostPrefabType.PredictedClient)]` so it's stripped from server + interpolated-client prefabs (else double-interp on remotes). Verified: server ghost has no `CharacterInterpolation`, client ghost does. - **Do NOT copy the CC sample's global `LocalTransform → DontSerializeVariant`.** It is project-wide and would break the non-character ghosts here (projectiles/dummies/pickups) that rely on stock `LocalTransform` replication. Our CC character replicates position via the normal owner-predicted `LocalTransform` path; only the `CharacterInterpolation` variant is registered. - **Top-down CC config (planar, no gravity):** `AuthoringKinematicCharacterProperties` with `SnapToGround=false`, `InterpolateRotation=false` (rotation owned by `PlayerAimSystem`), `SimulateDynamicBody=false` (players don't physically push each other); gravity is handled in the processor by feeding `float3.zero` to `Update_GroundPushing` and never adding a gravity term. Result: stays on the spawn plane (y≈1) with no planar-pin system. ## Build gotchas (learned — M5 home base + shared storage, 2026-06-02) - **Ownerless interpolated ghost ≠ owner-predicted for buffer replication.** A server-spawned **ownerless** ghost (e.g. the shared storage chest) replicates a `[GhostField]` `IBufferElementData` to all clients with **no `OwnerSendType` and no `GhostOwner`** — server mutations just propagate. `[GhostComponent(OwnerSendType = SendToOwnerType.All)]` (per `StatModifier`) is **only** for the predicting *owner* to recompute its own state; adding it (or a `GhostOwner`) to an ownerless ghost is wrong. - **One-off shared-state actions belong on an `IRpcCommand`, not a predicted `InputEvent`.** RPCs are reliable, so deposit/withdraw landed even while the server tick-batched (the M2 one-shot `Fire` InputEvent drops under batching). RPC payloads are **plain blittable fields — no `[GhostField]`**; store an op as a **byte**, not an enum (cross-assembly enum-Burst hazard, same one that de-Bursted `ProjectileClassificationSystem`). For a **single** shared target, resolve it as a **server singleton** — never put an `Entity` (not stable cross-world) in the command; only reach for a ghost-id+spawn-tick (`SpawnedGhostEntityMap`) when there are many targets (and that lookup may force the handler off Burst). - **Apply server-only RPC effects in the server `SimulationSystemGroup`, NOT the predicted loop** — the predicted loop re-runs on rollback and would double-apply. (Mutating a `DynamicBuffer` is not a structural change, so it's safe to do while iterating a *different* entity query, e.g. the RPC requests.) - **Build-grid math must be deterministic + integer-stable.** Corner-origin + center-returning + **half-open** cell bounds + `math.floor` (not truncation — negatives). Lock `CellSize`/`PlotSize` as a coordinate space once: M6 placement builds on it; changing them later invalidates placed structures. (`BaseGridMath`, unit-tested in EditMode like `PlayerSpawnMath`.) - **Runtime-spawn shared ghosts; don't bake them into the subscene.** A one-shot server spawner (mirrors `UpgradePickup`/`TrainingDummy`) keeps the subscene ghost-free and dodges the prespawn section-ack/CRC handshake. Do **not** add such a ghost to a connection's `LinkedEntityGroup` if it must survive that player's disconnect (the shared base is world-owned). - **Build a correctly-configured ghost prefab by duplicating an existing one** (`UpgradePickup.prefab` → `Storage.prefab`, then swap the authoring MonoBehaviour via `manage_prefabs modify_contents`) rather than hand-adding `GhostAuthoringComponent` — its ownerless/interpolated settings (`HasOwner=0`, `DefaultGhostMode=Interpolated`) + `LinkedEntityGroupAuthoring` come along for free. - **`execute_code` runs as a method body** — **no `using` directives** (they parse as statements → "Identifier expected"); fully-qualify every type (`Unity.Entities.World`, `ProjectM.Simulation.BaseAnchor`, …). Also: world flags overlap a shared `Game` bit, so identify worlds by `world.Name == "ServerWorld"/"ClientWorld"` rather than `(Flags & GameServer)`. - **An unfocused editor throttles Edit mode to near-idle** → MCP pings time out and the bridge looks hung (it still *queues* commands — `telemetry_ping` succeeds). `Application.runInBackground` only helps in **Play** mode. If it wedges, focus or restart the editor; don't pile `refresh_unity` calls onto a blocked main thread. Prefer `refresh_unity scope=scripts` for code-only changes (`scope=all force` is heavy and contributed to a mid-session hang). ## Build gotchas (learned — M5.5 game feel & identity, 2026-06-02) - **Move an ownerless INTERPOLATED enemy ghost SERVER-ONLY in the plain `SimulationSystemGroup`, never in `PredictedSimulationSystemGroup`** (interpolated ghosts aren't predicted; the server has no rollback). Use `[UpdateAfter(PredictedSimulationSystemGroup)]`, **NOT `[UpdateBefore]`** — the predicted group is **OrderFirst** in `SimulationSystemGroup`, so `UpdateBefore`/`After` *it* is silently *ignored* (Unity logs "Ignoring invalid UpdateBefore… OrderFirst/OrderLast has higher precedence"). A plain-`SimulationSystemGroup` server system therefore always runs **after** the predicted group, so a contact `DamageEvent` it appends drains the **following** tick (~16ms, fine for melee). Stock `LocalTransform` replication carries position — **no hand-written `[GhostField]`**. Build the enemy ghost by **duplicating an existing interpolated ghost** (`UpgradePickup.prefab` → `Enemy.prefab`) so the ownerless/interpolated `GhostAuthoringComponent` comes free — the training dummy is **not** a ghost (server-only → invisible to clients). See [[DR-009_GameFeel_Identity_FirstBlood]]. - **Derive an enableable gate from already-replicated state instead of replicating it.** Player `Dead` = a LOCAL enableable derived every predicted tick from the replicated `Health<=0` (`PlayerDeathStateSystem`, runs in **both** worlds, before movement/aim/fire) — the same derive-don't-replicate idiom as `StatRecomputeSystem`/`EffectiveCharacterStats`; rollback-correct on server + owner-client with **no `[GhostEnabledBit]`**. To write the bit on a currently-disabled entity the query must visit it: `.WithPresent()` (write) vs `.WithDisabled()` (alive-only run); `.WithAll()` ANDs independently. **Bake the enableable DISABLED** (`AddComponent(e); SetComponentEnabled(e, false);` in the baker) so instances spawn in the off state (instantiated entities inherit the prefab's enabled state). Respawn TIMING is server-only (`SimulationSystemGroup`, after the predicted group). - **All juice/HUD = client-only managed `SystemBase` in `PresentationSystemGroup`** (once per frame, no rollback double-fire) that OBSERVES replicated state — never mutates the sim. Read ECS via `SystemAPI.Query` inside `OnUpdate` + `EntityManager.CompleteDependencyBeforeRO()` — NOT from a MonoBehaviour `LateUpdate` (that throws the job-safety exception the camera rig hit). `Entity` is a stable client dict key for a ghost's lifetime — **prune the cache each frame** (a pruned enemy = a kill → death VFX at its last pos; **never `DestroyEntity` a ghost from the client** — `GhostDespawnSystem` owns despawn). Netcode-safe "hit-stop" = a camera punch, **never `Time.timeScale`** (it would corrupt the deterministic sim). - **Asset-free presentation:** procedural `AudioClip.Create` SFX; a runtime `ParticleSystem` pool (Sprites/Default material + HDR start color so bursts bloom); a code-built uGUI HUD (`RawImage` over `Texture2D.whiteTexture` for anchor-driven bars + legacy `Text` with `Resources.GetBuiltinResource("LegacyRuntime.ttf")`). To edit a prefab asset's component in code: `PrefabUtility.LoadPrefabContents` → modify → **`SaveAsPrefabAsset(root, path)`** → `UnloadPrefabContents` (`SavePrefabAsset` rejects the contents root — "Can't save a Prefab instance"). Watch **shared-material bleed** when re-tinting (`M_Dummy` doubled as the wall material → orange walls; Husks got their own `M_Husk`). **ACES tonemapping needs the URP asset color grading mode = HDR** (`m_ColorGradingMode = 1`). - **The "0 = ready" raw-`uint` cooldown sentinel can collide at tick wraparound** — a computed `ServerTick + delay` can equal 0. Route every cooldown/spawn "next tick" write through **`TickUtil.NonZero(...)`** (coerce 0→1), and compare stored ticks with `new NetworkTick(raw).IsNewerThan(serverTick)` / `.TicksSince(...)` — **never** raw `<` / subtraction — the HUD cooldown bar included. (Caught by the adversarial review; `RespawnMath` already guarded it.) - **An unfocused editor stalls EditMode test INIT** ("tests did not start within timeout") and slows play-enter domain reloads — pass `run_tests(init_timeout=120000)` and retry; ask the operator to focus Unity for heavy build/test sessions (`Application.runInBackground` only helps in Play mode). ## Build gotchas (learned — art import / Synty packs → URP, 2026-06-03) The imported store art (BefourStudios; future Synty) is **HDRP-authored** but we run **URP 17.4 + Entities Graphics** → source `M_*` materials render **magenta**. The reusable converter is `Assets/_Project/Scripts/Editor/EnvArtTools.cs` (menu `ProjectM/Art/1. Convert Curated Env Materials`); it outputs stock URP/Lit to `Assets/_Project/Materials/Env/`. See [[DR-010_Art_Import_URP_Conversion_Visual_Upgrade]]. - **Convert, don't switch pipelines.** Re-author to **stock URP/Lit** (`933532a4…`, the same shader our prototypes use → DOTS-instancing/Entities-Graphics compatible). FBX meshes + `T_*_B/_N/_ORM` textures are reusable as-is; the auto-generated `MI_*` URP stubs are blank. Switching the project to HDRP is NOT an option (breaks Entities Graphics). - **A dark-lit screenshot MASKS material bugs — verify material *values*, not just the render.** `S_General` exposes a **float** `_BaseColorMultiply` and the real Color in `_AlbedoTint`. `HasProperty("_BaseColorMultiply")` is true but `GetColor()` on a float returns **(0,0,0)** (and logs a "doesn't have a color property" warning) → black albedo everywhere. **Always `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture`.** - **Gate source emission on the `_Emissive` (0/1) flag AND a fixture name** — `S_General` carries a non-zero default `_EmissiveColor` even when off; reading it unconditionally makes crates/walls/domes glow (flat color can't reproduce the source emission mask). - **`VolumeProfile.Add()` does NOT persist the override** — on save the `components` list serializes `{fileID:0}` nulls (works in-session, gone after reload). Use `AssetDatabase.AddObjectToAsset(component, profile)` per effect, then `SaveAssets`; verify non-null refs on disk. - **`LocalTransform.FromPosition()` resets Scale=1**, silently discarding a ghost prefab's authored scale (Scale is a replicated `[GhostField]` → consistent-but-wrong, not a desync). Server spawners must read the prefab's baked `LocalTransform` and override only Position (fixed in `UpgradePickupSpawnSystem`/`SharedStorageSpawnSystem`). - **High metallic + no reflection probe + dark skybox = near-black.** Keep converted env metallic low (0.1–0.2); rely on albedo + direct light. - **Static decor goes in the gameplay subscene** (Entities Graphics renders only baked/EG-spawned entities; a SampleScene MeshRenderer renders via classic URP). **Strip colliders** from cosmetic props (else they bake into the static PhysicsWorld the CC sweeps) and put **no GhostAuthoring** on scenery. Edit the subscene via `manage_scene load … additive` → place → `SaveScene` → `close_scene` (re-bakes on Play); the baked-entity view disappears while it's open additively — verify placement via `execute_code` over the scene roots, not the game view. - **HUD-free beauty shot** = a **positioned `game_view` capture** (`view_position`/`view_rotation`) — direct camera rendering excludes Screen-Space-Overlay UI. `scene_view` rejects positioned capture. - **VFX (GabrielAguiar) is now imported** (499 prefabs, ~94% Shuriken; 27 VFX-Graph). Wired into combat via [[DR-011_Synty_World_VFX_Integration]] — see the VFX gotchas below. VFX-Graph (hits/beams) packs still need separate URP setup if wanted. ## Build gotchas (learned — Synty world + GabrielAguiar VFX, 2026-06-03) The world is now a cohesive **Synty** sci-fi colony + GabrielAguiar combat VFX. See [[DR-011_Synty_World_VFX_Integration]] / [[2026-06-03_Synty_World_And_VFX]]. - **Synty is URP-native — NO conversion** (unlike the HDRP BefourStudios art). The grounded world is built as **cosmetic classic-URP GameObjects in SampleScene** (`SyntyWorld` root), NOT the DOTS subscene — the custom `Synty/Generic_Basic` shader just renders, and you never have to verify its Entities-Graphics DOTS-instancing. Only the gameplay subscene needs Entities Graphics + the baked PhysicsWorld. - **Cosmetic SampleScene colliders are inert to gameplay** — classic PhysX is separate from the DOTS PhysicsWorld (baked only from the subscene); the planar-pinned CC never sees them. So a cosmetic world needs no collider stripping for *gameplay*. - **"Grounded" = surround + horizon, not a bigger plane** — a skyline ring of tall buildings + a planet/asteroid backdrop + a `Skybox/6 Sided` space skybox + light fog killed the floating-plane far better than extending the ground. - **Swap a subscene object's VISUAL while keeping collision:** disable its MeshRenderer but keep the BoxCollider — the collider still bakes to the static `PhysicsCollider`, and a disabled renderer bakes no RenderMesh (invisible wall, collision intact). Used to retire the BefourStudios walls under the Synty world. - **A GA "projectile" prefab is NOT a passive trail** — it ships a non-kinematic `Rigidbody` + collider + `ProjectileMoveScript` (self-propels, collides, spawns secondary muzzle/hit VFX). Any authored VFX dropped into a cosmetic slot must be **stripped to particles**: destroy `Rigidbody`/`Collider` and disable `Projectile`/`Move`-named MonoBehaviours BEFORE their `Start` runs (`CombatFeedbackSystem.StripCosmetic`). Verify a prefab's *components*, not its name. - **VFX prefab → client SystemBase bridge:** a MonoBehaviour with a static `Instance` + prefab fields in the bootstrap scene (`VFXConfig`, mirrors `PrototypeCameraRig`) hands authored assets to the managed `CombatFeedbackSystem`; keep a procedural fallback so a null slot still runs. Derive VFX TTL from the prefab's longest ParticleSystem (not a blanket constant) and cap concurrent VFX to bound GC churn under swarms. - **Projectile-follow VFX:** query `SystemAPI.Query>().WithAll()` each presentation frame; dict-by-`Entity` spawn/reposition/prune (same idiom as the Health FX cache). The one-shot Fire `InputEvent` still drops under the **unfocused** editor, so fire-driven VFX (muzzle/trail/enemy-death) need a focused editor / real client to fire. ## Build gotchas (learned — aim controls: mouse cursor + gamepad, 2026-06-03) The KBM/gamepad aim rework is [[DR-012_Aim_Controls_Cursor_Gamepad]] / [[2026-06-03_Aim_Controls_Cursor_Gamepad]]. - **Client-derived aim rides the EXISTING `PlayerInput.Aim` `[GhostField]` — no new netcode surface.** Mouse-cursor aim is computed client-side in `PlayerInputGatherSystem` (managed `SystemBase`, `GhostInputSystemGroup`, once/frame): `Mouse.current.position` → `Camera.main.ScreenPointToRay` → `AimMath.PlanarAimFromRay` (pure, Burst-safe, unit-tested) against the player's movement plane → write the player→cursor direction as `Aim`. Only the resulting direction crosses the wire; predicted/server systems are unchanged. Movement (`Move`) is already decoupled from facing (`Aim`/`PlayerFacing`), so **strafe-while-aiming is free** the moment `Aim` is the cursor. Don't add a mouse binding to the `Aim` action — the gather reads the device directly (no `.inputactions` edit → no wrapper regen). - **Active input scheme = last-meaningful-actuation-wins, replicated as a `byte` (NOT an enum).** Detect KBM vs gamepad each frame (stick/trigger/button past a 0.04 lengthsq deadzone vs mouse-delta/click/movement-key; `InputDevice.lastUpdateTime` breaks ties; hold last when idle) and stream `PlayerInput.Scheme` (`InputSchemeId.KeyboardMouse=0/Gamepad=1`). It is a **byte** because it is compared inside the Burst-compiled `AbilityFireSystem` (the cross-assembly enum-in-Burst ICE hazard). The server gates the `AutoTarget` cone to `applied.InternalInput.Scheme == Gamepad` (read at the fire tick from the same `GetDataAtTick` lookup) → **precise mouse, gamepad-only assist**; mouse then predicts == server (fewer reconcile snaps). - **A static presentation bridge must reset on play-enter.** `AimPresentation.Scheme` (mirrors `PrototypeCameraRig`/`VFXConfig` statics) needs `[UnityEngine.RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]` to reset — statics survive **fast-enter-playmode** domain reloads, and a stale value flashes the wrong cursor/reticle for the first frames (caught by the adversarial review). - **Cursor/reticle = client `PresentationSystemGroup` `SystemBase` (`AimReticleSystem`) that OBSERVES, never mutates.** A flat world-space ground ring (primitive quad, `Sprites/Default` with a null-guard fallback, procedural ring texture) is the aim indicator for BOTH schemes — KBM at the cursor's ground-projection point, gamepad a fixed distance ahead along replicated `PlayerFacing`. The hardware cursor is **hidden while aiming + focused** (`Application.isFocused`-gated) and restored on focus-loss / `OnDestroy`. A radial **dead-zone** (`AimMath.PlanarAimFromRay` `deadZoneRadius`) holds facing when the cursor is over the character. **The KBM ground point is re-raycast INSIDE `AimReticleSystem`** (PresentationSystemGroup runs after the follow-cam's LateUpdate), not latched from the gather (`GhostInputSystemGroup`, before the move) — latching there drifts the ring a frame behind the cursor under the moving camera. Optional camera **aim look-ahead** (`PrototypeCameraRig.AimLeadDistance`, tunable) leads the framed point toward `PlayerFacing` (not the live cursor projection, to avoid a feedback loop). Headless validation: drive `DebugInputInjectionSystem` (now stamps `Scheme`) + force `AimPresentation.Scheme`; the **real cursor / live device-switch needs a focused Game view** (the unfocused editor can't inject mouse position). ## Build gotchas (learned — M6 Aether Cycle core loop, 2026-06-03) The M6 core-loop slice (Expedition→Defend→Build) + the base/expedition world split. See [[DR-013_M6_Aether_Cycle_Region_Split]] / [[2026-06-03_M6_Aether_Cycle_CoreLoop]]. **Stages 0–1 done; 2–4 are the continuation.** - **Base/expedition split = coordinate-region + per-connection `GhostRelevancy`, NOT `SceneSystem` streaming** (supersedes DR-008's framing). One server world; the expedition lives at `base + (1000,0,0)`; a server `RegionRelevancySystem` in `GhostSimulationSystemGroup` (before `GhostSendSystem`) sets `GhostRelevancyMode.SetIsIrrelevant` and each tick marks region-tagged ghosts irrelevant to connections whose player is in a different region. **Use `SetIsIrrelevant` (not `SetIsRelevant`)** so untagged/global ghosts (the future cycle director) stay relevant to everyone for free — you only enumerate cross-region ghosts to hide. Verify the API on the installed Netcode (1.13.2) with `unity_reflect`: `GhostRelevancy` singleton has `GhostRelevancyMode` + `NativeParallelHashMap GhostRelevancySet`; `RelevantGhostForConnection{ int Connection; int Ghost }` (`Connection`=`NetworkId.Value`, `Ghost`=`GhostInstance.ghostId`). `RegionTag{byte Region}` is **server-only (NOT a `[GhostField]`)** — the server makes all relevancy decisions; the client just gains/loses ghosts. Reuses the runtime-ghost spawn path verbatim (no baked ghosts → no prespawn handshake), no async-load race; co-op drop-in is free. - **Region transit + cycle phase use the established byte-RPC + tick-safe + server-`SimulationSystemGroup` patterns.** `RegionTransitRequest{byte TargetRegion}` (resolve sender via `ReceiveRpcCommandRequest.SourceConnection`→`NetworkId`→`GhostOwner`, flip `RegionTag`, teleport `LocalTransform`). The macro loop is a server-only `CycleState` singleton ([GhostField]-pre-annotated for the later CycleDirector ghost) driven by `CyclePhaseSystem` (`[UpdateBefore(WaveSystem)]`); it **gates `WaveSystem`** with a one-line `if (TryGetSingleton(out var c) && c.Phase != CyclePhase.Defend) return;`. All phase timers are wrap-safe `NetworkTick` (`TickUtil.NonZero` + `IsNewerThan`), never raw `uint <`. - **Editing an existing `[BurstCompile]` ISystem's SystemAPI query set on an UNFOCUSED editor can leave a STALE Burst binary** (managed assembly recompiles with shifted source-gen query indices, Burst's async recompile doesn't finish) → runtime `InvalidOperationException: "… required component type was not declared in the EntityQuery"` thrown from an *unrelated* `GetSingleton` in that system. **Tell:** the Burst stack reports the *old* line number for the failing call. Same family as the M2 Burst-cache gotcha. **Workaround:** `Jobs ▸ Burst ▸ Enable Compilation` OFF for the session (verify `BurstCompiler.Options.EnableBurstCompilation==false`) — everything runs the fresh managed source-gen. **Permanent fix = restart Unity** to clear the cache, then re-enable Burst. Prefer a **focused** editor for Burst-affecting edits. - **Shared GLOBAL game-state (cycle phase, resource ledger, goal meter) rides an UNTAGGED ghost, never a region-tagged one** — `SetIsIrrelevant` hides a region-tagged ghost (e.g. the base storage) from players in the *other* region. The M6 resource **ledger** is a `StorageEntry` buffer on the global `CycleDirector` ghost, resolved via a distinct `ResourceLedger` tag — **never `GetSingleton`** (the base storage container owns a second `StorageEntry` buffer → "multiple instances" throw). Runtime-proven: the director stays relevant to an expedition player while the base storage despawns. - **A hit/area sweep that runs in the PLAIN `SimulationSystemGroup` must NOT use `SystemAPI.Time.DeltaTime`** — that group sees the variable *wall-frame* delta, not the fixed tick step, so a `cur - dir*Speed*dt` segment is wrong. Store the per-tick step on the projectile (`Projectile.LastStep`, written by `ProjectileMoveSystem` in the fixed-step predicted group) and reconstruct the swept segment as `cur - dir*LastStep` — tunnel-safe with zero dependence on the consuming system's clock. `ResourceHarvestSystem` runs `[UpdateAfter(PredictedSimulationSystemGroup)]` so it only sees projectiles that survived `ProjectileDamageSystem` (relies on the ~1000u base/expedition coordinate gap so a base shot can't reach a node). A node hit by N projectiles in one tick: deposit per hit but `ecb.DestroyEntity` **at-most-once** (destroyed-bitset + local Remaining copy — a double destroy throws at Playback); persist the decremented `[GhostField] Remaining` via `SetComponent` so depletion carries across ticks. - **New ghost prefab recipe (proven M6):** `manage_asset duplicate` UpgradePickup.prefab → `manage_prefabs modify_contents` (swap the authoring MonoBehaviour; **strip MeshFilter+MeshRenderer for an invisible state-holder**, keep them for a visible node). Wire the baked spawner into the gameplay subscene: `manage_scene load additive` → `set_active_scene Gameplay` → `manage_gameobject create` (+ `manage_components set_property` for the prefab ref, verify via `mcpforunity://scene/gameobject/{id}/component/...`) → `save` → `set_active_scene SampleScene` → `close_scene` (re-bakes on Play). - **Run an adversarial design-review Workflow (3 critics: netcode/relevancy, determinism/prediction, reuse/scope → synthesize) BEFORE coding a netcode-heavy slice** — for M6 Stage 2 it caught every one of the above pre-implementation (relevancy trap, singleton collision, dt-trap, double-destroy, lazy-create hazard). - **`manage_gameobject create` `component_properties` SILENTLY DROPS enum + Vector3 fields** (it set object-refs and simple scalars, but baked authoring enums/`Vector3` stayed at their C# defaults — two gates baked identical, one worked only by coincidence). **Always set those via a follow-up `manage_components set_property` (with a `properties` dict) and VERIFY through the `mcpforunity://scene/gameobject/{id}/component/{Type}` resource** (or, for a ghost, by reading the baked component in `execute_code` after Play). Same caveat applies to `manage_prefabs modify_contents` `component_properties`. Per-renderer color via `manage_material set_renderer_color` defaults to a runtime **PropertyBlock that does NOT persist into Play** — create a material asset (`manage_material create`) and `assign_material_to_renderer`, or use a prefab-stage assign, for colors that survive a domain reload. - **Walk-in region gates (M6 visibility pass):** a baked `ExpeditionGate{FromRegion,ToRegion,Radius,ArrivalPos}` entity (visible primitive, collider stripped so you pass through) + a server `ExpeditionGateSystem` (plain group, `[UpdateAfter(CyclePhaseSystem)]`) proximity-transits a player whose `RegionTag` matches `FromRegion` (flip RegionTag + teleport to `ArrivalPos`, offset from the destination gate so no re-trigger). Returning to base mid-Expedition expires the cycle timer → Defend ("timer cap + early return"). The expedition is a *place* = cosmetic ground/pillars in **SampleScene** at the +1000 offset (classic URP, like SyntyWorld), not the DOTS subscene; gameplay nodes/gates are the baked subscene entities. - **Build/automation foundation (M6 Stage 3, the M7 contract):** generic `PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}` on an ownerless interpolated ghost (`RegionTag{Base}`, world-owned, runtime-spawned). **Bake the two tick fields NOW** — the turret reuses `NextTick` as its fire cooldown, and they are the deterministic-offline-catch-up linchpin M7 needs and that can't be reconstructed retroactively. Only `Type` replicates (client derives `Cell` from `LocalTransform` via `BaseGridMath.WorldToCell`). Data-driven `StructureCatalog` buffer (`{byte Type; Entity Prefab; byte CostResourceId; int CostAmount}`, modeled on `AbilityPrefabElement`); M7 adds a recipe column additively. **Occupancy is DERIVED** by scanning live structure ghosts into a Temp `NativeHashSet` (structures are the source of truth — restart/replay-safe), NEVER a mutable buffer on the immutable baked `BaseAnchor`. - **Co-op placement atomicity:** `BuildPlaceSystem` commits the `StorageMath.Withdraw` + cell-reservation **IN-PLACE inside the RPC foreach** (only the `Instantiate` goes through the ECB) — the `StorageOpReceiveSystem` idiom — so two same-tick `BuildPlaceRequest`s for one cell can't both pass (validated: → exactly one structure + one withdraw). RPC carries `int CellX/CellZ` scalars, not `int2` (scalar-only RPC precedent). - **Buildable turret = hitscan = reversed `EnemyAISystem`:** snapshot living Husks, nearest-in-same-region-within-Range, on the `NextTick` cooldown append a direct `DamageEvent{Damage, SourceNetworkId=-1}` → reuses `HealthApplyDamageSystem` (despawns at HP≤0). NO projectile → no tunnelling, no friendly-fire/team model. Plain server group `[UpdateAfter(PredictedSimulationSystemGroup)]`. - **Resource-gated ability tiers reuse `StatModifier` — no new replicated component.** `AbilityUpgradeSystem` spends Aether and grows ONE `StatModifier{Target=Damage, Op=PercentAdd, SourceId=}` on the player (**replace-by-SourceId** so the `[InternalBufferCapacity(8)]` buffer stays bounded — repeated upgrades grow one row, not append); `StatRecomputeSystem` folds it into `EffectiveAbilityStats.Damage` on both worlds (the `UpgradePickup` path). `GoalProgress{[GhostField] int Charge, Target}` lives on the global CycleDirector ghost, single-writer in `CyclePhaseSystem`. **Disk-persistence writer is deferred to post-M7** (in-session-only state, per DR-008); freeze the save schema + bake the structure tick fields now so it's additive. See [[DR-014_M6_Build_Structures_Automation_Foundation]]. --- ## 2026-06-07 — Pre-trim CLAUDE.md snapshot A **full verbatim snapshot of `CLAUDE.md` as it stood immediately before the 2026-06-07 second condensation pass** (git `HEAD` = commit `25b53cb06`, 42 126 bytes). It is reproduced here in its entirety so that every line trimmed from the live file remains recoverable from the vault, not just from git history. The original headings are demoted two levels so they nest under this section instead of polluting the archive's outline. When a future condensation pass runs, append a new dated snapshot below this one rather than overwriting it. ### 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; one-time stack setup lives in `Docs/dots-setup-task.md`. > **Build-gotcha archive:** the full, verbose per-milestone build lessons were condensed out of this file on 2026-06-04 (to stay under the 40 KB context-load limit). The distilled rules live below in **Build gotchas (distilled)**; the long-form originals are in `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`, and the design rationale in the per-milestone DRs (`Docs/Vault/07_Sessions/_Decisions/DR-###`). #### Stack — Unity 6.4.7 (`6000.4.7f1`, 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. | | `com.unity.physics` | **1.4.6** | Unity Physics (DOTS). Independent 1.x line. | | `com.unity.charactercontroller` | **1.4.2** | DOTS kinematic collide-and-slide. Declares entities/physics 1.3.15 but resolves on 6.4.0/1.4.6 via SemVer floor (no downgrade). | | `com.unity.transport` | 2.7.2 | (transitive) | | `com.unity.burst` | 1.8.29 | (transitive) | | `com.unity.mathematics` | 1.3.3 | (transitive) | | `com.rukhanka.animation` | **2.9.0** | Local pkg (`Packages/com.rukhanka.animation`). ECS skeletal animation (Burst CPU/GPU skinning). Declares entities.graphics 1.4.16 → resolves on 6.4.0 via SemVer floor. Netcode replication **OFF** → client-derived. See [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. | Values match `packages-lock.json` (reconciled 2026-06-02; URP 17.4.0, test-framework 1.6.0, ugui 2.0.0, multiplayer.center 1.0.1). **History:** 6.6.0a6 was tried + reverted (Netcode/Transport runtime engine bug "invalid wrapped network interface"); returning to 6.6 means a package renumber + runtime re-test. See [[DR-002_Unity66_Alpha_Netcode_Transport]] + the gotchas archive. #### 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**, Unity.Transforms, Unity.NetCode, **Unity.Physics + Unity.CharacterController** (KinematicCharacterBody source-gen), **Rukhanka.Runtime** (animation) | | `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/`. - Feature folders added since (`Client/UI`, `Client/Settings`, `Server/Automation`, `Server/Persistence`, `Simulation/Automation`, `Simulation/Persistence`) live **inside the existing four asmdefs — no new assemblies**. #### Build gotchas (distilled) Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`. The highest-recurrence hazards are flagged **★**. ##### Assemblies, asmdefs & source-gen - **`Unity.Transforms` must be a DIRECT asmdef reference** for any assembly whose source-gen'd systems touch `LocalTransform`/`LocalToWorld` — transitive visibility compiles hand-written code but the generator emits **CS0246** in `*.g.cs`. - **Authoring asmdefs need `Unity.Entities.Hybrid`** (`Baker`) **+ `Unity.Collections`** (baking source-gen). Never name a nested baker `Baker` (shadows `Baker`) — use `FooBaker`. - **Never name an `IComponentData` `PlayerInput`** and don't `using UnityEngine.InputSystem;` in a file referencing such a component — collides with the managed `UnityEngine.InputSystem.PlayerInput`, generator binds `RefRW<…>` to the class → misleading **CS8377**. Fully-qualify Input System types instead. - **The generated Input Actions C# wrapper must live inside the consuming asmdef** — set the importer's `wrapperCodePath` (in `.inputactions.meta`) to e.g. `Assets/_Project/Scripts/Client/Input/ProjectMInput.cs`; the default location compiles into `Assembly-CSharp` which asmdefs can't reference. No `.inputactions` edit unless you intend a wrapper regen. - `IInputComponentData` requires implementing **`FixedString512Bytes ToFixedString()`**. ##### Burst hazards ★ - **Cross-assembly generics + enums trip Burst internal compiler errors.** Predicted-spawn classification (`SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory`, takes `ref DynamicBuffer` in 1.13.2) and any enum compared inside a Bursted system are the known offenders. Make such systems **plain non-Burst `ISystem`**, and **store ops/schemes/region ids as `byte`, never `enum`** in anything Bursted or in RPC payloads. - **A Burst ICE corrupts the editor's incremental cache** → afterward, valid `[BurstCompile]` entry points log `"… is not a known Burst entry point"` + run slow managed-fallback. A clean compile + green tests + working runtime confirm the *code* is fine. **Fix = editor restart** (or delete `Library/BurstCache` while closed); a domain reload alone does NOT clear it. - **Editing a Bursted ISystem's SystemAPI query set on an UNFOCUSED editor can leave a STALE binary** → runtime `InvalidOperationException: "required component type was not declared in the EntityQuery"` from an *unrelated* `GetSingleton` (Burst stack reports the OLD line number). Workaround: Burst compilation OFF for the session; permanent fix = restart. Prefer a **focused** editor for Burst-affecting edits. ##### Netcode / prediction ★ - **`PredictedSimulationSystemGroup` runs multiple times per frame on rollback** → predicted systems must be deterministic/idempotent, filter with `.WithAll()`, and use **no wall-clock / `Time.deltaTime` / `System.Random`**. - **Predicted physics is implicit** — with the netcode-physics package present, Netcode relocates `PhysicsSystemGroup` into `PredictedFixedStepSimulationSystemGroup` (child of the predicted group, **OrderFirst**). `NetCodePhysicsConfig` only tunes lag-comp/run-mode/history; put one in the gameplay subscene with `PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities`. - **The predicted physics group is OrderFirst**, so `[UpdateBefore/After(PredictedFixedStepSimulationSystemGroup)]` from the parent predicted group sorts oddly: `UpdateBefore` is ignored (1-tick offset, still in-sync); for same-tick, put the system *inside* the fixed-step group `[UpdateBefore(PhysicsSystemGroup)]`. **`OrderFirst`/`OrderLast` ALSO wins against `UpdateBefore/After` the predicted group from the plain `SimulationSystemGroup`** — a server-only system there always runs *after* the predicted group (use `[UpdateAfter(PredictedSimulationSystemGroup)]`, never `UpdateBefore`; Unity logs "Ignoring invalid UpdateBefore…"). - **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 fields (no `[GhostField]`), scalars only (`int CellX/CellZ`, not `int2`). 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 a system with cross-system `[UpdateBefore/After]`, re-audit the EXISTING `[Update*]` attributes of the systems you order around (a new `After(A)+Before(B)` collided with B's pre-existing `After(A)`) and **always Play-validate**, not just EditMode. [[DR-017_Persistent_Base_Player_Driven_Pacing]] - **A dev/debug `IRpcCommand` wire TYPE must be UNCONDITIONAL (no `#if`)** — the reflection-built RpcCollection hash must match across release/dev peers or the connection handshake refuses. `#if UNITY_EDITOR`-gate only the send/receive SYSTEMS + overlay, never the request struct. **Re-mean bytes, don't rename**: an enum/const whose byte VALUES are unchanged keeps the `[GhostField]` serializer identical → a global-loop reframe stays re-bake-free (only authoring *default-value* edits re-bake the subscene). - **Derive enableable gates instead of replicating them.** e.g. player `Dead` = a LOCAL enableable derived every predicted tick from replicated `Health<=0` (rollback-correct on server + owner, no `[GhostEnabledBit]`). To write the bit on a disabled entity the query must visit it (`.WithPresent()`); **bake the enableable DISABLED** so instances spawn off. Respawn/death *timing* is server-only. - **Cooldown/spawn "next tick" sentinels:** route every stored tick through **`TickUtil.NonZero(...)`** (a computed `ServerTick+delay` can wrap to 0, the "ready" sentinel) and compare with `NetworkTick.IsNewerThan` / `.TicksSince`, **never** raw `uint <` / subtraction. - **`GhostRelevancy` for region splits:** use `GhostRelevancyMode.SetIsIrrelevant` (not `SetIsRelevant`) so untagged/global ghosts stay relevant for free — only enumerate cross-region ghosts to hide. `RegionTag{byte Region}` is **server-only, NOT a `[GhostField]`** (server decides relevancy; client just gains/loses ghosts). `RelevantGhostForConnection{int Connection (=NetworkId.Value); int Ghost (=GhostInstance.ghostId)}`. - **Shared GLOBAL state (cycle phase, resource ledger, goal meter) rides an UNTAGGED ghost**, never a region-tagged one (`SetIsIrrelevant` would hide it cross-region). Resolve a ledger buffer via a DISTINCT tag (`ResourceLedger`), **never `GetSingleton`** when a second `StorageEntry` buffer exists elsewhere → "multiple instances" throw. - **Frontend world lifecycle (menu → on-demand worlds) ★:** `CreateLocalWorld` is `internal` in 1.13.2 — use public `CreateClientWorld`/`CreateServerWorld` (they register the `ServerWorld`/`ClientWorld` statics the UI/overlay read); for the menu world use `DefaultWorldInitialization.Initialize(name, false)` (or `return false` from `GameBootstrap.Initialize`). **Never dispose/create worlds inside an ECS system** — do all create/dispose/scene-load on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`). The gameplay subscene streams into an on-demand world ONLY if a netcode world is the `DefaultGameObjectInjectionWorld` at `LoadScene` time (dispose the menu world → set the default to the server world → `LoadScene(Game)`). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. ##### Physics & character controller - **Unity Physics 1.x bakes built-in `UnityEngine` colliders + `Rigidbody`** (the Physics-0.x `PhysicsShapeAuthoring`/`PhysicsBodyAuthoring` are gone). Static collider (no Rigidbody) → baked into the subscene PhysicsWorld, deterministic, no replication. `Rigidbody.FreezeRotation` is **NOT** honored by the baker — zero angular velocity + write rotation each tick, or set `PhysicsMass.InverseInertia = float3.zero`. - **The player is a Unity Character Controller kinematic character** (NOT a dynamic Rigidbody — `PlayerMoveSystem`/`PlayerPlanarConstraintSystem` were deleted; the DR-006 predicted-physics infra is kept). `PlayerControlSystem` maps input → `CharacterControl`; `CharacterProcessor` collide-and-slides in the relocated `KinematicCharacterPhysicsUpdateGroup`. CC 1.4.2 API = `IKinematicCharacterProcessor` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*` (verify shape with `unity_reflect`, don't assume the legacy aspect). - **`KinematicCharacterUtilities.BakeCharacter` aborts if the GameObject has a `Rigidbody`** and needs uniform (1,1,1) scale. **`CharacterInterpolation` must be PredictedClient-only** (register a `DefaultVariantSystemBase` stripping it from server + interpolated prefabs) — else double-interp on remotes. **Do NOT copy the CC sample's global `LocalTransform → DontSerializeVariant`** (project-wide; breaks the non-character ghosts that rely on stock `LocalTransform` replication). - **Top-down CC config:** `SnapToGround=false`, `InterpolateRotation=false` (rotation owned by `PlayerAimSystem`), `SimulateDynamicBody=false`; gravity handled by feeding `float3.zero` to `Update_GroundPushing`. - **Hit/area tests must be SWEPT, not point checks** — a point check tunnels when the per-tick step exceeds the target radius (high speed *or* tick-batching); test the segment traversed this tick. **In a PLAIN `SimulationSystemGroup` system do NOT use `SystemAPI.Time.DeltaTime`** (wall-frame delta, not the fixed step) — store the per-tick step on the projectile (`Projectile.LastStep`, written in the fixed-step group) and rebuild the segment as `cur - dir*LastStep`. A node hit by N projectiles in one tick: `ecb.DestroyEntity` **at-most-once** (destroyed-bitset; double destroy throws at Playback). **TWO target types in one projectile pass (nodes + Blight clutter): UNIFY into one best-target loop + one shared destroyed-bitset** (separate sweeps each destroy a projectile overlapping both → double-destroy, DR-018). **A per-hit yield `(int)` cast that also gates despawn is an immortal-sink** (sub-1.0 → 0 → no deposit, no `Remaining` decrement, shot still consumed): guard `math.max(1,(int)yield)` + `[Min(1f)]` authoring. ##### Build / structures / grid - **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor` (not truncation — negatives). Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`, EditMode-tested) — changing them invalidates placed structures. - **`PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}`** on an ownerless interpolated ghost. **Bake the two tick fields now** (turret reuses `NextTick` as fire cooldown; they're the offline-catch-up linchpin). Only `Type` replicates (client derives `Cell` via `BaseGridMath.WorldToCell`). Data-driven `StructureCatalog` buffer. **Occupancy is DERIVED** by scanning live structure ghosts into a Temp `NativeHashSet`, never a mutable buffer on the baked `BaseAnchor`. - **Co-op placement atomicity:** commit the `StorageMath.Withdraw` + cell-reservation **in-place inside the RPC foreach** (only `Instantiate` goes through the ECB) so two same-tick requests for one cell can't both pass. - **Buildable turret = hitscan = reversed `EnemyAISystem`:** nearest living Husk in-region within Range, on `NextTick` cooldown append a direct `DamageEvent{Damage, SourceNetworkId=-1}` → reuses `HealthApplyDamageSystem`. No projectile → no tunnelling, no team model. - **Resource-gated ability tiers reuse `StatModifier`** — grow ONE `StatModifier{Target=Damage, Op=PercentAdd, SourceId=}` (replace-by-SourceId so the buffer stays bounded); `StatRecomputeSystem` folds it into `EffectiveAbilityStats` on both worlds. `GoalProgress{[GhostField] int Charge, Target}` lives on the global CycleDirector ghost. **Disk persistence shipped** — see the Automation + persistence bullets below. - **M7 Automation (server-only, never predicted) ★:** `Harvester` / `Conveyor` / `Fabricator` are buildable machines on the `PlacedStructure` ghost; each stores `PeriodTicks` + **server-only** `MachineInput` / `MachineOutput` buffers (NOT `[GhostField]`). Production runs in the plain server `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]` (Harvester→Conveyor→Fabricator), replicating only via the global ledger + `PlacedStructure`. Deterministic catch-up via `ProductionMath.CyclesDue` (**lower-bound 0, never 1** — a `1` premature-mints a restored `remaining==0` machine; period-0 guarded). Byte-only pure math (`ProductionMath` / `ConveyorMath.ResolveMoves` / `MachineSlotMath`) is EditMode-tested; `ConveyorMath` is order-independent (snapshot → stable-sort by `CellKey` → at-most-one destination claim → losers stall). `RuntimePlacedTag` marks player-built machines for the save-scan; `BuildPlaceSystem` stamps `LastProcessedTick=0` → runtime machines hit `NeedsInit`. See [[DR-020_M7_Automation_Production_Chains]]. - **Disk persistence (`SaveData`, single-slot atomic JSON at `persistentDataPath`) ★:** versioned, null on bad version, schema **additive** (bump the version, don't break it). **Born-correct load** — `CycleDirectorSpawnSystem` applies a staged `PendingSave` AT SPAWN so the director ghost never replicates a default first. Autosave on the Siege→Calm checkpoint + on quit-to-menu (`WorldLauncher.TrySaveFromServer`, host-only); `BaseRestoreSystem` replays saved structures **charge-free** with epoch-independent REMAINING-tick cooldowns + re-tags them. Shared `SaveStructureScan.Collect` (autosave + quit use ONE scan path). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. ##### Presentation / juice / VFX - **All juice/HUD = client-only managed `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire) that OBSERVES replicated state, never mutates the sim. Read ECS via `SystemAPI.Query` in `OnUpdate` + `EntityManager.CompleteDependencyBeforeRO()` — NOT a MonoBehaviour `LateUpdate` (job-safety throw). `Entity` is a stable client dict key for a ghost's lifetime — **prune the cache each frame** (a pruned enemy = a kill → death VFX); **never `DestroyEntity` a ghost from the client** (`GhostDespawnSystem` owns despawn). Hit-stop = a camera punch, **never `Time.timeScale`** (corrupts the deterministic sim). - **Asset-free presentation:** procedural `AudioClip.Create` SFX; runtime `ParticleSystem` pool (Sprites/Default + HDR start color); code-built **UI Toolkit** HUD / menus (runtime `UIDocument` + shared `RuntimePanelSettings`; see the UITK bullet below). Edit a prefab asset's component in code via `PrefabUtility.LoadPrefabContents` → modify → **`SaveAsPrefabAsset(root, path)`** → `UnloadPrefabContents`. Watch **shared-material bleed** when re-tinting. ACES tonemapping needs URP color grading mode = HDR (`m_ColorGradingMode=1`). - **Prototype glue lives in `ProjectM.Client` as MonoBehaviours:** `PrototypeCameraRig` (player-following ARPG cam), `VFXConfig` (static `Instance` + prefab fields bridging authored VFX to the managed `CombatFeedbackSystem`; keep a procedural fallback). A **static presentation bridge must reset on play-enter** via `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` (statics survive fast-enter-playmode reloads → stale flash). - **UITK HUD + menus ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings`/`EventSystem` plumbing + the canonical `Round`/`Border` helpers; `HudUi` is a thin extension (bars/labels). `HudSystem` is a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, behind the pause overlay's 100); builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` so the HUD never eats world clicks (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet`** (a `.tss` importing `unity-theme://default`) **AND** an `EventSystem` + `InputSystemUIInputModule` or buttons are silently dead. The **build palette** (lazy-built from the client `StructureCatalog`) drives click-to-place: ground-ghost preview (green/red via `BuildPreviewMath`, the client mirror of the server check), left-click → `BuildPlaceRequest` RPC, right-click/Esc cancels, `[`/`]`/R rotates; `Fire` suppressed in build mode. See [[DR-021_HUD_UITK_BuildPalette]]. **Synty skin via a build-safe `HudTheme`** (DR-024): the in-game HUD is reskinned with the InterfaceSciFiSoldierHUD/Core kit — but those sprites/fonts live under `Assets/Synty/…` (NOT Resources), so a runtime name-string `Resources.Load("Synty/…")` is **stripped from the build**; instead a curated `HudTheme : ScriptableObject` at `Assets/_Project/Resources/HudTheme.asset` holds **serialized** Sprite/Font refs (dependency-walked into the build), loaded null-safe via `HudTheme.Get()`, and **every consumer falls back to the flat look if a ref/the asset is null**. `unityBackgroundImageTintColor` MULTIPLIES (tint white skins into the Aether palette, zero asset bleed). Fonts = cached SDF `FontAsset.CreateFontAsset(Font)`+`FontDefinition.FromSDFFont`, built **once per font per session**, reset (with the theme cache) on `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]`. **Synty HUD frame/bar sprites ship authored 9-slice borders** (Box_Glass=25, Bar_Angled=80/0) — setting `unitySlice*` in code OVERRIDES them and logs a `"borders overridden by style slices"` ERROR per element; let the art border drive slicing (no `unitySlice*`), only set slices for border-0 sprites. Some Synty sprites import as Sprite **Multiple** mode (e.g. `Gradient_Outwards`) → `LoadAssetAtPath` silently returns null; verify import mode + each theme ref non-null after authoring. See [[DR-024_HUD_Synty_Skin_Theme]]. ##### Art import (HDRP store packs → URP) - BefourStudios art is **HDRP-authored** → magenta under URP 17.4 + Entities Graphics. **Convert, don't switch pipelines** (HDRP breaks Entities Graphics). Re-author to stock URP/Lit via `Assets/_Project/Scripts/Editor/EnvArtTools.cs` (menu `ProjectM/Art/1. Convert Curated Env Materials`). Synty art is **URP-native — no conversion**. - **A dark-lit screenshot MASKS material bugs — verify material *values*.** Always `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture` (`S_General`'s `_BaseColorMultiply` is a float; `GetColor` on it returns black). Gate source emission on the `_Emissive` flag AND a fixture name. Keep converted env metallic low (0.1–0.2). - **`VolumeProfile.Add()` does NOT persist** (serializes `{fileID:0}` on save) — use `AssetDatabase.AddObjectToAsset(component, profile)` + `SaveAssets`, verify on disk. - **A reverted engine/URP upgrade can stamp `UniversalRenderPipelineGlobalSettings.asset` with `m_AssetVersion` AHEAD of the package's `k_LastVersion`** (here 11 > 10, from the reverted 6.6 alpha). URP migrates **forward only**, so `URPPreprocessBuild` rejects the "from-the-future" asset (*"not at last version"*) — **blocks player builds but NOT editor Play** (lurks until the first build). Fix: reflection-set `m_AssetVersion` back to `k_LastVersion`, `SetDirty` + `SaveAssets` (data already renders under the current package, only the stamp is wrong). - **`LocalTransform.FromPosition()` resets Scale=1** — server spawners must read the prefab's baked `LocalTransform` and override only Position (Scale is a replicated `[GhostField]` → consistent-but-wrong). - **Static decor → gameplay subscene** (Entities Graphics renders only baked/EG-spawned entities); **strip colliders from cosmetic props** (else they bake into the PhysicsWorld the CC sweeps), no `GhostAuthoring` on scenery. Cosmetic SampleScene GameObjects (classic URP, `SyntyWorld` root) render via classic URP and their colliders are **inert to the DOTS PhysicsWorld** — no stripping needed there. To swap a subscene object's visual while keeping collision: disable the MeshRenderer, keep the collider. - **A GA "projectile" prefab self-propels** (non-kinematic `Rigidbody` + collider + `ProjectileMoveScript`) — strip to particles before `Start` (`CombatFeedbackSystem.StripCosmetic`). Verify a prefab's *components*, not its name. ##### Aim controls - **Client-derived aim rides the EXISTING `PlayerInput.Aim` `[GhostField]`** — mouse-cursor aim computed in `PlayerInputGatherSystem` (managed `SystemBase`, `GhostInputSystemGroup`): `Mouse.current.position` → `Camera.main.ScreenPointToRay` → `AimMath.PlanarAimFromRay` (pure, unit-tested) → player→cursor direction. Only the direction crosses the wire; strafe-while-aiming is free (`Move` already decoupled from `Aim`). - **Active scheme = last-meaningful-actuation-wins, replicated as `byte`** (`PlayerInput.Scheme`, KBM=0/Gamepad=1 — byte because compared in Bursted `AbilityFireSystem`). Server gates the `AutoTarget` cone to gamepad only → precise mouse, gamepad-only assist. - **Cursor/reticle = client `PresentationSystemGroup` `SystemBase` (`AimReticleSystem`) that OBSERVES.** Re-raycast the KBM ground point INSIDE that system (PresentationSystemGroup runs after the follow-cam's LateUpdate) — latching from the gather drifts a frame behind. Hardware cursor hidden while aiming + focused, restored on focus-loss/`OnDestroy`. ##### Animation (Rukhanka) ★ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation = **Rukhanka 2.9** (Entities-native; the only maintained option on 6.4 — Latios/Kinemation not 6.4-compatible, Unity's official ECS-animation is vaporware). **Netcode replication OFF** (`RUKHANKA_WITH_NETCODE` undefined) → **client-derived**: a client-only `SystemBase` (`PlayerAnimationDriveSystem`, `[WorldSystemFilter(LocalSimulation|ClientSimulation)]` + `[UpdateBefore(RukhankaAnimationSystemGroup)]`) reads replicated state and writes params via `AnimatorParametersAspect`/`FastAnimatorParameter`. No new `[GhostField]`s; no `DefaultVariant` strip (define off → no Rukhanka component is a ghost component → ghost hash unchanged). - **The rig must bake on the SAME entity that holds the gameplay components the drive job reads.** Rukhanka puts the param buffer + index-table on the GO with `RigDefinitionAuthoring`, so put `Animator` + `RigDefinitionAuthoring` on the **player root** (not a child) and flatten the skeleton + SMRs under it — else the single-entity drive query matches nothing. - **CPU engine still skins via Entities-Graphics GPU deformation → needs a deformation-aware material** (`AnimatedLitShader`, a multi-target ShaderGraph that includes a `UniversalTarget`; Synty atlas → its `_BaseColorMap`). Stock URP/Lit renders **unskinned static** + a `"does not support skinning"` warning — NOT magenta (magenta = reusing an HDRP sample `.mat`). - **Importing the Rukhanka "Animation Samples"** (the only source of `AnimatedLitShader`) drags in 26 sample **subscenes** (one NRE's Rukhanka's **unguarded clip baker** — `AnimationClipBaker.ReadCurvesFromTransform` reads a null bone Transform on a missing-bone clip), sample **systems that run in your worlds**, and a conflicting **TextMesh Pro** folder. Fix: `MoveAsset` the 3 deformation ShaderGraphs to `_Project/Shaders/` (GUID-preserving → material ref intact), then delete the samples tree. - **First Rukhanka bake is ~60 s, synchronous on the main thread** (editor telemetry freezes → looks like a hang, isn't); the animation blob is cached after → fast re-plays. - **The server runs Rukhanka unless you strip it** — its **deformation** systems use `[WorldSystemFilter(Default)]` (Default ⊇ ServerSimulation) so they run on the server's baked bones/meshes (the animation group is created server-side too but left empty by the bootstrap). **`ServerStripAnimationSystem`** (server-only one-shot, `[WorldSystemFilter(ServerSimulation)]`) disables every `Rukhanka.Runtime` system on the server (disabling a group cascades to its children; matched by assembly name → no type ref). *Only Play-validation caught this — the static `WorldFlags` read said the server was clean; it wasn't.* - **Build the controller via the `AnimatorController` API** (`manage_animation` silently drops enum/Vector blend-tree fields). **Skeleton-root detection = walk up from a bone to the soldier's direct child**, NOT `SkinnedMeshRenderer.rootBone` (that's the *bounds* root — the head SMR's is `Spine_03`; using it destroys the lower skeleton). - **Synty Polygon characters share one Generic skeleton**; the FBX needs **Optimize Game Objects OFF** (Rukhanka requirement; SciFiSpace was already so). Entity origin = capsule **center** (~1 m up) → offset the **un-keyed `Root` bone** local Y (clips key no Root/Hips position → Rukhanka bakes the offset as a constant → it persists through animation). Root motion **OFF** (the CC owns the transform; the blend tree is velocity-driven). - **`Unity.Physics` must be a DIRECT asmdef ref** for any system whose source-gen touches `KinematicCharacterBody` (it nests `Unity.Physics.ColliderKey`) → else CS8377/CS0012 in `*.g.cs` (same class as the `Unity.Transforms` direct-ref rule). - **ENEMIES reuse the player pipeline** — a Husk is an ownerless interpolated ghost = a remote player, so `EnemyAnimationDriveSystem` mirrors `PlayerAnimationDriveSystem`'s REMOTE path (`LocalTransform` delta velocity + prevPos cache; facing via `AnimParamMath.PlanarForward`; maxSpeed from baked `EnemyStats`; `IsAttacking = AttackWindup != 0`). **Drop `[RequireMatchingQueriesForUpdate]`** so the prune runs every frame (Husks die often → else a cache entry leaks per kill). No server/asmdef/ghost-hash change. Build enemy prefabs via the **`EnemyRigTools`** editor tool (real `PrefabUtility`; `RigDefinitionAuthoring` by reflection), **GUID-preserving** (`DeleteAsset+CopyAsset` orphans subscene refs). `WaveSystem` uses `baked.WithPosition` (not `FromPosition` → resets Scale, a `[GhostField]`). See [[DR-023_Enemy_Animation_MonsterMash]] + [[Synty_Asset_Inventory]]. ##### MCP / editor workflow ★ - **Edit Assets `.cs` ONLY via MCP `apply_text_edits` / `create_script`** (Unity's scripting pipeline) — the raw `Write` tool does NOT reliably trigger a recompile on an unfocused editor → tests/`execute_code` run a **stale assembly**. A raw-`Write`-created NEW `.cs` is worse — it gets **no `.meta` / no test-discovery** until `refresh_unity scope=all mode=force` (it compiles, but EditMode silently won't run it). (`Write`/`Edit` are fine for non-asset files: this vault, asmdef JSON, etc.) For comment/string-precise edits to existing scripts, `script_apply_edits` **`anchor_replace`** (regex anchor) + **`delete_method`** work cleanly even on a `struct : ISystem` (unlike `replace_method`). - **`apply_text_edits` with MULTIPLE non-adjacent edits in one call can MISALIGN** (a paired replace+delete hit the line *above* the target). One edit per call (or strict bottom-first), always with `precondition_sha256` (it returns the current SHA on mismatch). **`create_script` won't overwrite** an existing path; full-file rewrites = whole-span `apply_text_edits` (its brace-balance validator guards botched spans) or `manage_script delete`+`create_script` (NON-GUID-referenced files only — systems/tests, never authoring MonoBehaviours). `script_apply_edits replace_method` is safe for class methods but still **can't target a `struct : ISystem`** (use whole-span). [[DR-017_Persistent_Base_Player_Driven_Pacing]] - **`execute_code` runs as a method body** — no `using` directives (parse as statements); fully-qualify every type. Identify worlds by `world.Name == "ServerWorld"/"ClientWorld"` (flags overlap a shared `Game` bit). - **`manage_gameobject create` / `manage_prefabs modify_contents` `component_properties` SILENTLY DROP enum + Vector3 fields** — set those via a follow-up `manage_components set_property` and VERIFY through `mcpforunity://scene/gameobject/{id}/component/{Type}` (or read the baked component in `execute_code` after Play). `manage_material set_renderer_color` uses a runtime PropertyBlock that does NOT persist into Play — create + assign a material asset instead. - **New ghost prefab recipe:** `manage_asset duplicate` an existing correctly-configured ghost (e.g. `UpgradePickup.prefab`) → `manage_prefabs modify_contents` to swap the authoring MonoBehaviour (strip MeshFilter+MeshRenderer for an invisible state-holder) — its ownerless/interpolated `GhostAuthoringComponent` + `LinkedEntityGroupAuthoring` come free. **Runtime-spawn shared ghosts** via a one-shot server spawner; don't bake them into the subscene (dodges the prespawn handshake). Wire a baked spawner into the subscene: `manage_scene load additive` → `set_active_scene Gameplay` → create + set props + verify → `save` → `set_active_scene SampleScene` → `close_scene` (re-bakes on Play). - **An UNFOCUSED editor throttles Edit mode to near-idle** (MCP pings time out, bridge looks hung — it still queues; `telemetry_ping` succeeds) and stalls EditMode test INIT (pass `run_tests(init_timeout=120000)`, retry). `Application.runInBackground` only helps in **Play** mode. Don't pile `refresh_unity` onto a blocked main thread; prefer `refresh_unity scope=scripts` for code-only changes. Ask the operator to **focus Unity** for heavy build/test/Burst sessions. - **Run an adversarial design-review Workflow (netcode/relevancy · determinism/prediction · reuse/scope → synthesize) BEFORE coding a netcode-heavy slice** — it has pre-caught relevancy traps, singleton collisions, dt-traps, double-destroys. #### Bootstrap & worlds - `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + the per-world ConnectionControlSystems, no auto-connect). **Editor default = instant-into-game + MPPM** (creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`)); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. - **Scenes:** `Assets/Scenes/MainMenu.unity` (build index 0) boots the UITK frontend (menu world only, no netcode); `Assets/Scenes/Game.unity` (index 1) holds gameplay with `Assets/_Project/Subscenes/Gameplay.unity` wired in as the baked subscene (GameObject `GameplaySubScene`). `SampleScene` / `DevSandbox` are kept as reference / dev scenes. The on-demand lifecycle (`WorldLauncher` / `SessionRunner` / `MainMenuController`) creates the right worlds per menu choice (Single / Host / Join), THEN `LoadScene(Game)` (subscene-streaming rule above). - **Region split:** one server world; the expedition lives at `base + (1000,0,0)`, hidden per-connection via `GhostRelevancy` (Netcode gotchas). Place = cosmetic ground/pillars at the +1000 offset; nodes/gates are baked subscene entities. See [[DR-013_M6_Aether_Cycle_Region_Split]]. #### 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 → 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. Pinned IDs: 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 = plain-Entities EditMode test:** create a `World`, register the system in `SimulationSystemGroup`, tick, assert. Public API, version-independent. Example: `Assets/_Project/Tests/EditMode/HeartbeatSystemTests.cs`. Run via `run_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"])`. - **`NetCodeTestWorld` is `internal`** in netcode 1.13.2, 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). #### 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. - **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 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 / 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. Long-form build lessons → `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`. - **Cross-machine rule:** durable truth → the **vault** or **this file** (both committed); native `memory/` is local-only, never the sole home of a decision. **serena C# caveat:** flaky on Unity — if `find_symbol` stalls, fall back to `Glob`/`Grep`. #### 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.4 open + the Unity-MCP bridge connected (`mcpforunity://editor/state` → `ready_for_tools`).