Update CLAUDE.md
This commit is contained in:
@@ -1,24 +1,24 @@
|
||||
# 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`.
|
||||
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`.
|
||||
|
||||
## Stack — reverting to Unity 6.4.7 (stable) as of 2026-05-30
|
||||
> **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 on Unity 6.4. |
|
||||
| `com.unity.physics` | **1.4.6** | Unity Physics (DOTS). Independent 1.x line on Unity 6.4. |
|
||||
| `com.unity.charactercontroller` | **1.4.2** | Unity Character Controller (DOTS, kinematic collide-and-slide). Player movement foundation (M5b). Declares `entities`/`physics` 1.3.15 but resolves on our 6.4.0/1.4.6 via SemVer floor (no downgrade). |
|
||||
| `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) |
|
||||
|
||||
> ✅ 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 above now 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.
|
||||
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:** briefly tried **6.6.0a6** (renumbers Netcode→6.6.0/Physics→6.5.0/Entities→6.5.0) but its Netcode/Transport runtime is a **confirmed engine bug** ("invalid wrapped network interface") → reverted. If returning to 6.6, expect the renumber + re-test runtime. See [[DR-002_Unity66_Alpha_Netcode_Transport]] and `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`.
|
||||
|
||||
## Namespaces & assembly split
|
||||
|
||||
@@ -27,127 +27,88 @@ Root namespace: **`ProjectM`**. Code lives under `Assets/_Project/Scripts/` in f
|
||||
| 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.Client` | `ProjectM.Client` | client world only | + Simulation, Unity.Entities.Graphics, **Unity.InputSystem**, Unity.Transforms |
|
||||
| `ProjectM.Server` | `ProjectM.Server` | server world only | + Simulation, **Unity.Transforms**, Unity.NetCode |
|
||||
| `ProjectM.Authoring` | `ProjectM.Authoring` | bake time (+ scene runtime) | Simulation, Entities, **Unity.Entities.Hybrid**, Collections, Mathematics, Unity.NetCode |
|
||||
|
||||
- **Simulation** = components + systems shared by both worlds (most gameplay). **Client/Server** = world-specific. **Authoring** = `…Authoring` MonoBehaviours + `Baker<T>`.
|
||||
- Other folders: `Assets/_Project/Subscenes/` (baked entity subscenes), `Assets/_Project/Prefabs/`, `Assets/_Project/Tests/EditMode/`.
|
||||
|
||||
### Build gotchas (learned — M1, 2026-05-30)
|
||||
## Build gotchas (distilled)
|
||||
|
||||
- **`Unity.Transforms` must be a DIRECT asmdef reference** for any assembly whose source-gen'd systems use `LocalTransform`/`LocalToWorld`. It is its own assembly; transitive visibility compiles your hand-written code but the Entities generator emits **CS0246** inside the `*.g.cs`.
|
||||
- **Authoring asmdefs need `Unity.Entities.Hybrid`** (defines `Baker<T>`) **and `Unity.Collections`** (baking source-gen). A nested baker class must **not** be named `Baker` (it shadows `Baker<T>` → CS0308/CS0246) — name it `FooBaker`.
|
||||
- **Never name an `IComponentData` `PlayerInput`**, and don't `using UnityEngine.InputSystem;` in a file that references such a component: it collides with `UnityEngine.InputSystem.PlayerInput`, and the Entities generator binds `RefRW<…>` to the *managed* class → a misleading **CS8377 "must be a non-nullable value type"**. Fully-qualify Input System types (`UnityEngine.InputSystem.Keyboard.current`) instead.
|
||||
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<T>`) **+ `Unity.Collections`** (baking source-gen). Never name a nested baker `Baker` (shadows `Baker<T>`) — 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()`**.
|
||||
- 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)
|
||||
### Burst hazards ★
|
||||
- **Cross-assembly generics + enums trip Burst internal compiler errors.** Predicted-spawn classification (`SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory<T>`, takes `ref DynamicBuffer<SnapshotDataBuffer>` 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<T>` (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.
|
||||
|
||||
- **The generated Input Actions C# wrapper must live inside an asmdef** any system needs to reference. By default it generates next to the `.inputactions` (e.g. `Assets/Settings/`), which has no asmdef → it compiles into `Assembly-CSharp`, and asmdef assemblies (`ProjectM.Client`) **cannot** reference that. Fix: set the importer's `wrapperCodePath` (in the `.inputactions.meta`) to a path inside the consuming asmdef, e.g. `Assets/_Project/Scripts/Client/Input/ProjectMInput.cs`, and delete the old generated file. Read the action map via a managed `SystemBase` holding the wrapper; gather `Fire` as a netcode **`InputEvent`** (reset the field each frame, `.Set()` on the press edge — netcode latches the absolute `Count` into the command buffer; the live component value is only the per-tick delta).
|
||||
- **Predicted-spawn classification cannot be `[BurstCompile]`d (Netcode 1.13.2).** The cross-assembly generic `Unity.NetCode.LowLevel.SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory<T>()` trips a Burst **internal compiler error** (type-hash resolution). Make the classifier a plain non-Burst `ISystem` (it only runs when spawns are received — cold path). In 1.13.2 that method takes **`ref DynamicBuffer<SnapshotDataBuffer>`** (the public HelloNetcode sample's by-value `data` is from an older version).
|
||||
- **A Burst *internal compiler error* corrupts the editor's Burst incremental cache.** After the error is fixed in code, newly-added `[BurstCompile]` entry points (systems **and** generated ghost-component serializers) keep logging `"... is not a known Burst entry point"` and run managed-fallback (slow → server tick-batching, ~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`).
|
||||
### Netcode / prediction ★
|
||||
- **`PredictedSimulationSystemGroup` runs multiple times per frame on rollback** → predicted systems must be deterministic/idempotent, filter with `.WithAll<Simulate>()`, 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.
|
||||
- **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<Dead>()`); **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<StorageEntry>`** when a second `StorageEntry` buffer exists elsewhere → "multiple instances" throw.
|
||||
|
||||
### Build gotchas (learned — M5 physics-in-prediction, 2026-06-01)
|
||||
### 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<T>` + `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 distance check tunnels through a target 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`** (it's the 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; a double destroy throws at Playback).
|
||||
|
||||
- **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.
|
||||
### 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<int2>`, 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=<sentinel>}` (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 deferred to post-M7** — freeze the save schema + bake structure tick fields now so it's additive.
|
||||
|
||||
### Build gotchas (learned — M5b Unity Character Controller, 2026-06-01)
|
||||
### 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<T>()` — 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 uGUI HUD (`RawImage` over `Texture2D.whiteTexture`, legacy `Text` + `LegacyRuntime.ttf`). 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).
|
||||
|
||||
- **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<T>` + `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.
|
||||
### 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<T>()` does NOT persist** (serializes `{fileID:0}` on save) — use `AssetDatabase.AddObjectToAsset(component, profile)` + `SaveAssets`, verify on disk.
|
||||
- **`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.
|
||||
|
||||
### Build gotchas (learned — M5 home base + shared storage, 2026-06-02)
|
||||
### 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`.
|
||||
|
||||
- **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<Dead>()` (write) vs `.WithDisabled<Dead>()` (alive-only run); `.WithAll<Simulate>()` ANDs independently. **Bake the enableable DISABLED** (`AddComponent<T>(e); SetComponentEnabled<T>(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<T>()` — 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<Font>("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<T>()` 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<RefRO<LocalTransform>>().WithAll<Projectile>()` 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<RelevantGhostForConnection,int> 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<CycleState>(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<T>` 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<StorageEntry>`** (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<int2>` (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=<sentinel>}` 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]].
|
||||
### 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**. (`Write`/`Edit` are fine for non-asset files: this vault, asmdef JSON, etc.)
|
||||
- **`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`, 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.
|
||||
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize`, sets `AutoConnectPort = 7979` (in-editor auto-connect over IPC), calls `CreateDefaultClientServerWorlds()`. Entering Play Mode creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`).
|
||||
- `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.
|
||||
- **Region split:** one server world; the expedition lives at `base + (1000,0,0)`, hidden per-connection via `GhostRelevancy` (see the Netcode gotchas). Expedition *place* = cosmetic ground/pillars in SampleScene at the +1000 offset; gameplay nodes/gates are baked subscene entities. See [[DR-013_M6_Aether_Cycle_Region_Split]].
|
||||
|
||||
## DOTS / ECS conventions (authoritative summary)
|
||||
|
||||
@@ -158,19 +119,19 @@ Full rules: `~/.claude/skills/dots-dev/references/dots-conventions.md` (Windows:
|
||||
- **Jobs:** `IJobEntity` / `IJobChunk`; thread `JobHandle` through `state.Dependency`; mark inputs `[ReadOnly]`. Allocators: `Temp` (frame), `TempJob` (one job), `Persistent` (must dispose). Burst breaks on managed types/exceptions/reflection/strings.
|
||||
- **Structural changes** (add/remove component, create/destroy entity) invalidate handles + cause sync points → batch via **`EntityCommandBuffer`** (Begin/End`Simulation`EntityCommandBufferSystem; `.AsParallelWriter()` in parallel jobs).
|
||||
- **Baking:** `…Authoring` MonoBehaviour + `class FooBaker : Baker<FooAuthoring>` → `GetEntity(authoring, TransformUsageFlags.…)` then `AddComponent`. Subscenes stream async — entities aren't present the instant a reference exists.
|
||||
- **Netcode:** ghosts = replicated entities (`GhostAuthoringComponent` + `[GhostField]`); predicted (player-controlled, rolled back) vs interpolated. Core sim runs in `PredictedSimulationSystemGroup` (fixed step, **runs multiple times per frame** on rollback → must be deterministic/idempotent; filter with `.WithAll<Simulate>()`). **Server-authoritative: clients send input (`IInputComponentData`), not state.** RPCs (`IRpcCommand`) for one-off events. **No wall-clock/`Time.deltaTime`/`System.Random` in predicted sim.**
|
||||
- **Always verify volatile DOTS/Netcode API shape via context7 at code-time** — do not trust memory. See `context7-libraries.md`. Pinned IDs for our versions: Entities → `/websites/unity3d_packages_com_unity_entities_6_5_manual`; Netcode → `/websites/unity3d_packages_com_unity_netcode_1_10_api` (closest published; we run 1.13.2 — re-resolve if a 1.13 set appears); ECS samples → `/unity-technologies/entitycomponentsystemsamples`.
|
||||
- **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<Simulate>()`). **Server-authoritative: clients send input (`IInputComponentData`), not state.** RPCs (`IRpcCommand`) for one-off events. **No wall-clock/`Time.deltaTime`/`System.Random` in predicted sim.**
|
||||
- **Always verify volatile DOTS/Netcode API shape via context7 at code-time** — do not trust memory. 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 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.
|
||||
- **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 instead.
|
||||
- **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`.
|
||||
|
||||
@@ -179,17 +140,17 @@ Full rules: `~/.claude/skills/dots-dev/references/dots-conventions.md` (Windows:
|
||||
| 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) |
|
||||
| **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 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.
|
||||
- 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 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.
|
||||
- **serena C# caveat:** its language server is flaky on Unity. If `find_symbol` errors/stalls, **fall back to `Glob`/`Grep`**.
|
||||
|
||||
## Per-machine setup (NOT in git — redo on each machine)
|
||||
|
||||
`.mcp.json` is committed and portable (`${CLAUDE_PROJECT_DIR}` only). The **`dots-dev` skill now travels with the repo** at `.claude/skills/dots-dev/` (project-level, auto-discovered by Claude Code on clone — no manual `~/.claude/skills/` copy needed). But each machine still needs:
|
||||
1. `uv`/`uvx`, the Obsidian app + `obsidian-cli`. (The `unity-mcp-skill` and native `memory/` notes remain machine-local and do **not** sync — re-install / re-create them per machine if wanted.)
|
||||
2. **basic-memory project registration** (machine-local config): `uvx basic-memory project add gamevault "<repo>/Docs/Vault" --default`, then `uvx basic-memory reindex --full --search --embeddings --project gamevault`.
|
||||
`.mcp.json` is committed and portable (`${CLAUDE_PROJECT_DIR}` only). The **`dots-dev` skill travels with the repo** at `.claude/skills/dots-dev/` (auto-discovered on clone). Each machine still needs:
|
||||
1. `uv`/`uvx`, the Obsidian app + `obsidian-cli`. (The `unity-mcp-skill` and native `memory/` notes are machine-local and do **not** sync.)
|
||||
2. **basic-memory project registration:** `uvx basic-memory project add gamevault "<repo>/Docs/Vault" --default`, then `uvx basic-memory reindex --full --search --embeddings --project gamevault`.
|
||||
3. Unity 6.4 opens the project and the CoplayDev Unity-MCP bridge connects (`mcpforunity://editor/state` → `ready_for_tools`).
|
||||
|
||||
Reference in New Issue
Block a user