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,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 24 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 (24, listen-server) pillars from [[Pillars]].