# Validation harness — driving Unity Play mode safely from MCP Consolidates the Play-mode validation lore that used to live only in machine-local memories (screenshot-pause bug, sleep-poll ban, execute_code traps, headless-run flakiness) plus the proven server-inject harness. Load when running Phase 6 (verify ladder) or any live diagnosis. Committed + in-repo: this is the cross-machine home of these rules. ## Ground rules (violating these produces MISDIAGNOSES, not just failures) 1. **Never `Thread.Sleep` / sleep-poll inside `execute_code`.** It runs on the main thread — it freezes the entire sim, and state read "after waiting" is state from a FROZEN world. This manufactured the retracted WasAllReady "save deadlock" (2026-07-06). Spread observation across separate MCP calls instead — each round-trip gives the sim hundreds of ms to advance. 2. **The bridge's game-view capture PAUSES the editor** (`EditorApplication.Step`). Check `isPaused` first, unpause after any bridge capture. Prefer `ScreenCapture.CaptureScreenshot(path)` inside `execute_code` — async, lands at frame end; Read the PNG on the NEXT MCP call (the round-trip covers it). 3. **An unfocused editor** throttles Edit mode to near-idle (MCP looks hung; test INIT stalls — pass `init_timeout=120000`) and can leave a STALE Burst binary after Bursted query-set edits. Ask the operator to focus Unity for compile/test/Burst-heavy phases. `Application.runInBackground` only helps in Play mode. 4. **Play-enter resets static presentation configs** via `[RuntimeInitializeOnLoadMethod]` — a value poked via `execute_code` does not survive the next play-enter. Conversely: if a whole presentation slice is silent, FIRST probe its static config (`return ProjectM.Client.XFeelConfig.Enabled;`) before theorizing structurally — the 2026-07-07 harvest-feedback bug was found by that one read after a plausible transform-hierarchy hypothesis proved wrong. ## execute_code traps - Runs as a **method body**: no `using` directives (parsed as statements) — fully-qualify every type. - Identify worlds by **`world.Name == "ServerWorld"/"ClientWorld"`** (flags overlap a shared `Game` bit). - **`Type.GetMethod(name, flags)` throws AmbiguousMatchException** on overloaded methods (EntityManager generics, netcode statics) — iterate `GetMethods()` and filter by name/param-count, or call concrete generics directly in compiled code. - Rukhanka `ParameterValue` is a union — read `floatValue`/`boolValue` per the parameter type. - Return a built string (`StringBuilder`) — it's the only channel out. ## The server-inject harness (validate replicated presentation WITHOUT gameplay driving) Proven 2026-07-07 (NodeFeedbackSystem). Validates a replicated-state → client-presentation path end-to-end in ~4 MCP calls, no expedition run, no player positioning: 1. **Spawn** (server world): query the ghost prefab (`ComponentType` + `EntityQueryOptions.IncludePrefab`, pick the one with `Prefab`), `Instantiate`, then `em.GetComponentData(prefab).WithPosition(playerPos + offset)` — **never `LocalTransform.FromPosition`** (resets Scale=1; Scale replicates → consistent-but-wrong). Find the player via `KinematicCharacterBody` + `LocalTransform`. 2. **Defeat relevancy**: a region-tagged ghost spawned at base gets culled from base players — `em.SetComponentData(inst, new RegionTag { Region = 0 /* Base */ })` in the same call. 3. **Inject state changes server-side** (`SetComponentData` on the replicated component) in one call; **read the client** in the NEXT call (round-trip ≥ several ticks — replication has happened). 4. **Assert numerically, then screenshot.** e.g. client `PostTransformMatrix.c0.x` vs the expected `lerp(min, 1, frac)` and child `LocalToWorld` scale (`math.length(ltw.c0.xyz)`) vs `bakedScale × ptm`. Numbers prove the math; the screenshot proves it renders (both matter — see visual limits below). Ghost/LEG inspection idioms: `em.GetBuffer(root)`; render children = LEG entries with `MaterialMeshInfo` (the root usually has none); parenting = `Parent` component; a baked child's uniform authoring scale lands in `LocalTransform.Scale` (non-uniform → its own `PostTransformMatrix`). A root `PostTransformMatrix` DOES propagate into parented children's `LocalToWorld` (verified live on 6.5.0). ## Headless expedition-run driving (flaky — know the failure modes) > The specific nouns below (`PlayerReady`, expedition launch, respawn-reset) are **co-op-Hades-era** and become > salvage under the LANTERN pivot ([[DR-048_Lantern_Adoption_Full_Pivot]]) — they will be re-meaned to the > **descent lifecycle** (muster → Bell → pockets) when that lands. The *harness patterns* (server-inject, the > `EditorApplication.update` guardian, capture-in-the-same-call) are direction-agnostic and stay valid. - Launch by setting server `PlayerReady=1` from clean Staging. **Post-abort re-launch is intermittent** — a clean Play restart launches reliably on the first ready; re-readying after an abort may not. - **An AFK player dies to the swarm in ~15 s** and `Health` resets to the class value on respawn — a `SetComponentData` HP buff does NOT stick across respawn; the run then aborts (0 expedition players → Returning) and tears down the room. - Therefore: **capture evidence in the SAME `execute_code` call that first detects the condition** (e.g. `enemies>0`) — a separate "then screenshot" round-trip loses the window. - **Multi-sample observations (movement deltas, before/after) that can't fit one call: install an `EditorApplication.update` guardian delegate** (proven 2026-07-09, enemy-backstop smoke). The closure runs every editor frame: it can out-race damage (`Health.Current = Max` per frame — beats the respawn-reset trap above), park the player, take timed snapshots, and stash results in `EditorPrefs` (survives across `execute_code` calls — statics do NOT). Always: wrap the body in try/catch that UNSUBSCRIBES on error, add a hard timeout unsubscribe (~90 s), self-unsubscribe when done, and delete the `EditorPrefs` keys after reading them. - Prefer the server-inject harness above whenever the question doesn't actually require a live run. ## Visual/asset verification limits - **A dark/stylized frame masks material bugs** — verify VALUES: `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture` (e.g. `S_General._BaseColorMultiply` is a float; `GetColor` returns black). - **Skinned meshes lie to readers**: `WorldRenderBounds`/bone transforms return the bind-pose AABB, not the deformed pose — deformed-pose visual bugs need the operator's eyes (or a screenshot), not entity reads. - Body tint/flash validation: drive the stock EG `URPMaterialPropertyBaseColor` override on the render (LEG) children; verify on the LOCAL player (persistent, centered) rather than a transient enemy. - Forcing an animation pose: disable the drive system, write params via `AnimatorParametersAspect`; measure ground-contact via feet-Y only on non-deformed geometry. ## Input-path validation (07-21, the auto-recast false negative) - **A guardian window must OUTLIVE the period it judges.** Frames ≠ ticks: an unfocused editor can run FASTER than 60 fps, so a 500-frame watch can end BEFORE a 7 s cooldown does. The morning "sim is clean" verdict was a false negative for exactly this; the 2000-frame re-run caught a recast at every cooldown reopen. Size the window in TICKS of the judged period × 2, converted generously. - **`DebugInputInjectionSystem` writes the COMPONENT — it can never reproduce wire-semantics bugs.** To exercise the real gather → copy-layer → command-buffer path headless, queue synthetic device events: `StateEvent.From(Keyboard.current, out var ev); InputControlExtensions.WriteValueIntoEvent(kb.digit1Key, 1f, ev); InputSystem.QueueEvent(ev);` (+ a 0f release event). Works unfocused with runInBackground. - **Wire truth: `InputEvent` counts ACCUMULATE across commands** (the per-frame gather reset never reaches the buffer). Test fixtures that push `InputBufferData` must model that: press = count STEP at one tick, and the stepped count PERSISTS in every later entry — a "release = count 0" entry is a wire-impossible shape that hides the ever-pressed class of bug (and double-fires a delta gate). ## EditMode fixtures for prediction-gated systems (07-21, AbilityFireSystemConeTests) - **`NetworkTime.Flags` is INTERNAL** — a system gated on `IsFirstTimeFullyPredictingTick` silently no-ops in any plain test world (default Flags = 0). Fixture fix: box the struct + `typeof(NetworkTime).GetField("Flags", NonPublic|Instance).SetValue(boxed, NetworkTimeFlags.IsInPredictionLoop | IsFirstTimeFullyPredictingTick)`. Test-only; never reflect internals in shipped code. - **Input-buffer presses**: an `InputBufferData` entry with a set `InputEvent` keeps answering `GetDataAtTick` for every later tick — it re-fires the moment a cooldown re-opens (the MeleeComboTests C6 lesson, buffer-shaped). Always push a press at T **and a release (default command) at T+1**. - Damage paths gated `WorldUnmanaged.IsServer()` need `new World(name, WorldFlags.Game | WorldFlags.GameServer)`. ## Test-run mechanics - `run_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"])` → poll `get_test_job` (`wait_timeout=60`). Unfocused editor: `init_timeout=120000` and retry once before diagnosing. - A test count LOWER than expected with zero failures = tests not discovered (raw-written `.cs` without `refresh_unity scope=all mode=force`), not a pass.