|
|
|
@@ -19,7 +19,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
|
|
|
|
|
| `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:** 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`.
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
@@ -67,14 +67,14 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
|
|
|
|
|
- **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.
|
|
|
|
|
- **Frontend world lifecycle (menu → on-demand worlds) ★:** `CreateLocalWorld` is `internal` in 1.13.2 — use the public `CreateClientWorld` / `CreateServerWorld` (they register the `ServerWorld` / `ClientWorld` statics the connection 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** — run 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 first → set the default to the server world → `LoadScene(Game)`). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
|
|
|
|
|
- **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<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). **When TWO target types share one projectile pass (resource nodes + Blight clutter), UNIFY the sweep into one best-target loop + one shared destroyed-bitset** — separate sweep systems each `DestroyEntity` a projectile that overlaps both → double-destroy at Playback (DR-018). **A float per-hit yield cast `(int)` that ALSO gates despawn is an immortal-sink footgun:** a sub-1.0 value → `(int)`=0 → no deposit AND no `Remaining` decrement, yet the shot is still consumed → an unkillable target that silently eats projectiles. Guard with `math.max(1,(int)yield)` in the consumer **and** `[Min(1f)]` on the authoring (the node path had it; the clutter sibling didn't).
|
|
|
|
|
- **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.
|
|
|
|
@@ -82,14 +82,14 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
|
|
|
|
|
- **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 shipped** — see the Automation + persistence bullets below.
|
|
|
|
|
- **M7 Automation (server-only, never predicted) ★:** `Harvester` / `Conveyor` / `Fabricator` are buildable machines on the same `PlacedStructure` ghost; each stores `PeriodTicks` + **server-only** `MachineInput` / `MachineOutput` buffers (NOT `[GhostField]`). Production runs in the plain server `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]` (Harvester→Conveyor→Fabricator) and replicates 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, no loss). `RuntimePlacedTag` marks player-built machines for the save-scan; `BuildPlaceSystem` stamps `LastProcessedTick=0` so runtime-placed machines hit `NeedsInit`. See [[DR-020_M7_Automation_Production_Chains]].
|
|
|
|
|
- **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<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 **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 (in-game UI, on UI Toolkit) ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings` / `EventSystem` plumbing and 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); it 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` (Input System project) 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 legality check), left-click places via the `BuildPlaceRequest` RPC, right-click/Esc cancels, `[`/`]`/R rotates a conveyor; `Fire` is suppressed while build mode is active. See [[DR-021_HUD_UITK_BuildPalette]].
|
|
|
|
|
- **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]].
|
|
|
|
|
|
|
|
|
|
### 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**.
|
|
|
|
@@ -106,7 +106,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
|
|
|
|
|
- **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 the 6.4 stack — Latios/Kinemation isn't 6.4-compatible, Unity's official ECS-animation is vaporware). **Netcode replication OFF** (`RUKHANKA_WITH_NETCODE` undefined) → animation is **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 (with the define off, no Rukhanka component is a ghost component → ghost hash/snapshot unchanged).
|
|
|
|
|
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.
|
|
|
|
@@ -115,6 +115,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation
|
|
|
|
|
- **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`).
|
|
|
|
@@ -128,8 +129,8 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation
|
|
|
|
|
## 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)` — the subscene streams into the on-demand world only if a netcode world is the `DefaultGameObjectInjectionWorld` when the scene loads.
|
|
|
|
|
- **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 the Game scene at the +1000 offset; gameplay nodes/gates are baked subscene entities. See [[DR-013_M6_Aether_Cycle_Region_Split]].
|
|
|
|
|
- **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)
|
|
|
|
|
|
|
|
|
@@ -166,12 +167,8 @@ Full rules: `~/.claude/skills/dots-dev/references/dots-conventions.md` (Windows:
|
|
|
|
|
| **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 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. If `find_symbol` errors/stalls, **fall back to `Glob`/`Grep`**.
|
|
|
|
|
- **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 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`).
|
|
|
|
|
`.mcp.json` is committed + portable (`${CLAUDE_PROJECT_DIR}`); the **`dots-dev` skill travels with the repo** (`.claude/skills/dots-dev/`). Each machine still needs: (1) `uv`/`uvx` + Obsidian app + `obsidian-cli` (the `unity-mcp-skill` + native `memory/` are machine-local, don't sync); (2) basic-memory registration — `uvx basic-memory project add gamevault "<repo>/Docs/Vault" --default` then `uvx basic-memory reindex --full --search --embeddings --project gamevault`; (3) Unity 6.4 open + the Unity-MCP bridge connected (`mcpforunity://editor/state` → `ready_for_tools`).
|
|
|
|
|