Co-Op Layer

This commit is contained in:
Luis Gonzalez
2026-06-01 10:48:18 -07:00
parent 1f647dd5e1
commit e851d5f8e9
29 changed files with 667 additions and 20 deletions
@@ -0,0 +1,53 @@
---
date: 2026-06-01
type: session
tags: [session, dots, netcode, m4, co-op, lan, connection, bugfix, prediction]
permalink: gamevault/07-sessions/2026/2026-06-01-m4-lan-co-op-and-classification-fix
---
# Session 2026-06-01 — M4 LAN Co-op kickoff + projectile-classification cascade fix
## Goal
Two parts: (1) **fix the runtime console error cascade** that fired "when moving around and shooting" left by the M2/M3 work, and (2) start **M4 — Co-op**. Operator-scoped this pass to **playable LAN co-op** (multi-client correctness + a Host/Join connection flow with host-IP entry so a standalone build can join over LAN); transport **Direct IP/LAN now, Unity Relay deferred**. Architecture locked in [[DR-005_M4_Connection_Model_Direct_IP]].
## Part 1 — Projectile-classification cascade (root-caused + fixed)
**The "ton of errors" was one bug.** `ProjectileClassificationSystem` declared its `PredictedGhostSpawn` buffer lookup `[ReadOnly]` (`GetBufferLookup<PredictedGhostSpawn>(true)` + `[ReadOnly]` on the job field) but the job **writes** it via `predictedSpawnList.RemoveAtSwapBack(j)` on a match. So **every projectile spawn** threw `InvalidOperationException: …[ReadOnly]… writing to it` at `ProjectileClassificationSystem.cs:177`, which aborted classification → the predicted spawn was never paired → duplicate ghost → `Found a ghost … does not have an entity connected``Received baseline for a ghost we do not have``reset their entire ack history``Ghost ID n already added to the spawned ghost map`**server tick batching**. One root cause, full cascade.
**Fix (3 lines):** `GetBufferLookup<PredictedGhostSpawn>(false)` + drop `[ReadOnly]` on the field (matches the default `DefaultGhostSpawnClassificationSystem`, which removes matched entries from the list; `ProjectileLookup` stays `[ReadOnly]`). The deliberate non-`[BurstCompile]` decision is unchanged.
**This corrects the M2 misdiagnosis.** [[2026-05-31_M2_Combat]] / [[Milestones]] attributed these ghost-map errors to "server tick-batching from running two editors at once." That was wrong — the tick-batching was a *downstream symptom* of the `[ReadOnly]` write, not the cause. Validated: a single in-editor client, moving + firing, now produces **zero** of the five cascade signatures while projectiles spawn and classify (cooldown advances across fires). Residual `Server Tick Batching` warnings are the unfocused-background-editor perf artifact (sim ticks faster than it renders, 1.251.75 ticks/frame) — **not** Burst-cache corruption (no "not a known Burst entry point") and not the cascade; they clear when the Game view is focused / in a build.
## Part 2 — M4 Playable LAN Host/Join (Direct IP) — see [[DR-005_M4_Connection_Model_Direct_IP]]
Reused the existing per-connection spawn + `LinkedEntityGroup` auto-despawn (already N-player-ready). Added:
- **Simulation:** `ConnectionConfig` singleton (`ConnectionMode {None,Host,Join}` + address/port/Requested); `EditorAutoHostSystem` (`#if UNITY_EDITOR`, once per world, seeds Host(loopback) on server + Join(loopback) on client/thin worlds, self-disables); `PlayerSpawnMath.SpawnOffset` (pure ring-slot math); `PlayerSpawner` gained `SpawnRingRadius`/`RingSlots`.
- **Server:** `ServerConnectionControlSystem` (Host → `NetworkStreamRequestListen{AnyIpv4:port}`); `GoInGameServerSystem` now applies the deterministic per-`NetworkId` ring offset to the spawn.
- **Client:** `ClientConnectionControlSystem` (Join → `NetworkStreamRequestConnect{Parse}`, runs in client **and thin** worlds); `ConnectionUI` IMGUI Host/Join+IP panel (build entry point; hides once connected).
- **Authoring/scene:** `PlayerSpawnerAuthoring` bakes radius/slots (default 2.5/4); `Gameplay.unity` re-baked; `NetConnectionUI` GameObject (with `ConnectionUI`) added to `SampleScene` + saved.
- **Bootstrap:** `GameBootstrap.AutoConnectPort 7979 → 0` (connection now explicit).
- **Asmdefs:** added `Unity.Networking.Transport` to `ProjectM.Client` + `ProjectM.Server` (needed to name `NetworkEndpoint`; transitive-via-NetCode doesn't satisfy the compiler — Unity.Transforms-class gotcha).
### Validation
- **EditMode 45/45 green** (38 prior + 7 new `PlayerSpawnRingTests`); existing M1/M2/M3 suites unaffected.
- **Single-client (make-or-break):** no-auto-connect + `EditorAutoHostSystem` + request components → in-proc client connects to loopback, gets `NetworkId`, server spawns its player at `(2.5,0)` (NetworkId-1 ring slot, not origin). Subscene re-bake confirmed (`spawner.radius=2.5 slots=4`).
- **3-client co-op (1 real + 2 thin):** server `conns=3 inGame=3 players=3` at distinct ring slots `(2.5,0)/(0,2.5)/(-2.5,0)` — no stacking; the real `ClientWorld` sees all 3 (own owner-predicted + 2 interpolated); thin worlds connect (`conns=1` each, `players=0` locally as expected). Continuous movement replicates (server≈client with prediction lead). **Console clean of all five cascade signatures** under multi-client load + firing.
- **Disconnect:** `NetworkStreamRequestDisconnect` on a thin connection → server + client drop to `players=2` via `LinkedEntityGroup`.
### Method
context7-led API confirm (`NetworkStreamRequestListen/Connect`, `NetworkEndpoint`, `ClientServerBootstrap.Create*World`, thin-client prefs) → plan-gated → compile-checkpointed clusters with `read_console` after each. Part 1 isolated + validated first so the fix couldn't be masked by the connection refactor. MCP `script_apply_edits` (anchor) for edits, `create_script` for new files, `Write` for the full `GameBootstrap` rewrite, `execute_code` for runtime world inspection + input injection.
## Decisions
- [[DR-005_M4_Connection_Model_Direct_IP]] — no auto-connect; `ConnectionConfig` + request-component control systems; editor auto-host vs build UI; deterministic ring spawn; **Direct IP/LAN now, Unity Relay deferred** (closes the [[Backlog]] relay-provider blocker for this slice).
## Open / deferred
- **Unity Relay transport** — layer onto the same `ConnectionConfig` flow (relay allocation + join code feeding the endpoint); needs Unity Gaming Services. Deferred.
- **Real two-build LAN join** — operator-side: build a ClientServer host + a Client player, run on one LAN, Join-by-IP. The in-editor path (incl. thin clients) is validated; the standalone build join is not yet exercised this session.
- **One-shot `Fire` under tick-batching** — continuous input replicates fine, but single-shot `Fire` events can drop in the unfocused editor; focus the Game view (or a build) for reliable fire validation. Pre-existing artifact, not introduced here.
- **`[ReadOnly]` regression lock** — a reflection-based EditMode guard would need `ProjectM.Client` in the test asmdef (pulls InputSystem/Graphics); skipped in favour of the runtime console-clean proof. Reconsider if the field regresses.
- **Spawn-point variety** — single ring around one `SpawnPoint`; fine for 24. Per-team/spread layouts later.
## Next
Either (a) **Unity Relay transport** to make M4 remote-playable (layer on `ConnectionConfig`), or (b) advance to **M5 — Home base + physics** per [[Milestones]]. Recommend a quick real-LAN two-build smoke test first to confirm the `ConnectionUI` join path end-to-end.