Co-Op Layer
This commit is contained in:
@@ -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.25–1.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 2–4. 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.
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
id: DR-005
|
||||
title: M4 connection model — explicit ConnectionConfig + netcode request components; Direct IP/LAN now, Unity Relay deferred
|
||||
status: accepted
|
||||
date: 2026-06-01
|
||||
tags:
|
||||
- decision
|
||||
- netcode
|
||||
- connection
|
||||
- co-op
|
||||
- lan
|
||||
permalink: gamevault/07-sessions/decisions/dr-005-m4-connection-model-direct-ip
|
||||
---
|
||||
|
||||
# DR-005 — M4 Connection Model (Direct IP/LAN; Relay deferred)
|
||||
|
||||
## Context
|
||||
|
||||
M4 ([[Milestones]]) = co-op for 2–4 players. The roadmap framed it as "client-hosted listen-server over Unity Relay," and [[Backlog]] flagged **"decide relay provider"** as a blocker. The operator scoped this pass to **playable LAN co-op** with transport **Direct IP/LAN now, Unity Relay deferred**. Until now `GameBootstrap` hard-coded `AutoConnectPort = 7979` → a single in-proc client auto-connected over IPC; there was no host-vs-join choice, no multi-client path, and all players spawned on one point. The existing per-connection spawn (`GoInGameServerSystem` stamps `GhostOwner`, links the player to the connection's `LinkedEntityGroup` for auto-despawn) already generalised to N players — only connection establishment + spawn spread were missing. Settled at intake, validated against context7 (Netcode 1.13.2) + runtime. Extends [[DR-003_M2_Combat_Netcode_Architecture]].
|
||||
|
||||
## Decision
|
||||
|
||||
1. **No auto-connect.** `GameBootstrap.AutoConnectPort = 0` (was 7979); worlds are created idle and wait for an explicit listen/connect.
|
||||
2. **Connection driven by a `ConnectionConfig` singleton + per-world control systems.** `ConnectionConfig { ConnectionMode Mode; FixedString64Bytes Address; ushort Port; bool Requested }` (Simulation, created per world). `ServerConnectionControlSystem` (ServerSimulation) turns a Host request into a `NetworkStreamRequestListen { AnyIpv4:port }`; `ClientConnectionControlSystem` (ClientSimulation | ThinClientSimulation) turns a Join request into a `NetworkStreamRequestConnect { Parse(addr,port) }`. Both clear `Requested`. **Request components, NOT manual `NetworkStreamDriver.Listen/Connect`** — race-free (netcode's receive system acts once the driver store is ready) and identical across server / client / thin worlds.
|
||||
3. **Two entry points.** Player builds: `ConnectionUI` (IMGUI Host / Join+IP, in `ProjectM.Client`) writes `ConnectionConfig` into `ClientServerBootstrap.ServerWorld` / `ClientWorld`; hides once connected; Host shown only where a server world exists. Editor: `EditorAutoHostSystem` (`#if UNITY_EDITOR`, runs once per world, then self-disables) seeds Host(loopback) in the server world and Join(loopback) in the client + **every** thin-client world — reproducing the old zero-config playflow, now multi-client.
|
||||
4. **Deterministic spawn spread.** `PlayerSpawnMath.SpawnOffset(networkId, radius, slots)` (pure, in Simulation) places each connection on a ring slot keyed by `NetworkId` (slots evenly spaced; spills to concentric outer rings past `slots`); `GoInGameServerSystem` applies `SpawnPoint + offset`. Tunable via baked `PlayerSpawner.SpawnRingRadius` / `RingSlots` (default 2.5 / 4). Server-only, no RNG — unit-tested in `PlayerSpawnRingTests`.
|
||||
5. **Transport: Direct IP/LAN now; Unity Relay deferred.** Closes the [[Backlog]] "decide relay provider" blocker for this slice: the eventual remote transport is **Unity Relay**, layered later on top of the same `ConnectionConfig` flow (swap the endpoint source for a relay allocation + join code). The request-component path is transport-agnostic, so the gameplay/connection code does not change.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Thin-client connect is the load-bearing surface, and it works.** With `AutoConnectPort=0` thin worlds connect only because `ClientConnectionControlSystem` carries `ThinClientSimulation` and `EditorAutoHostSystem` seeds each thin world. Runtime-validated: 3 clients (1 real + 2 thin) → server `conns=3 players=3` at distinct ring slots `(2.5,0)/(0,2.5)/(-2.5,0)`; the real client sees all three (own owner-predicted + two interpolated); a clean disconnect (`NetworkStreamRequestDisconnect`) drops server **and** client to 2 via the `LinkedEntityGroup` path. No ghost-classification errors under multi-client load + firing.
|
||||
- **`Unity.Networking.Transport` is now a direct asmdef reference** on `ProjectM.Client` + `ProjectM.Server` (`NetworkEndpoint` lives there; transitive-via-`Unity.NetCode` visibility does NOT satisfy the compiler — same class of gotcha as `Unity.Transforms`).
|
||||
- **Editor vs build divergence is intentional and small:** auto-host in editor, manual UI in builds. In-editor the request path is still exercised (auto-host uses it), so it is not an untested code path.
|
||||
- **Relay deferral keeps this pass service-free** (no Unity Gaming Services dependency; fully validated in-session). Revisit when remote (non-LAN) play is needed: add a relay-allocation step feeding `ConnectionConfig` endpoints, then re-test connect + the connect-retry window.
|
||||
- **Connect ordering relies on transport's connect-retry:** both server-listen and client-connect are seeded ~frame 1, so a client may issue connect before the server is listening; the transport retries until the listener is up (fine over loopback/LAN). Watch this if Relay introduces longer setup latency.
|
||||
|
||||
Mirrors the server-authoritative + small-co-op (2–4, listen-server) pillars from [[Pillars]].
|
||||
Reference in New Issue
Block a user