Quick Design Spec: Netcode Session
Type: New Small System Scope: The session model — who hosts, how friends join and leave, the two authority rails every system builds on, and the late-join sync contract. Does NOT own: wire-level anything (RPC shapes, replication representations, tick rates, intent validation, transport finalization — all → the netcode ADR, gated on the friend playtest), or any system's disconnect semantics (each GDD's edge cases own theirs). Date: 2026-08-27 Estimated Implementation: model prototype-validated; session plumbing ~3–4 days once the ADR locks
Overview
Host-and-play, prototype-confirmed: one friend's machine is the world, everyone else connects to it. The architecture is two authority rails, used consistently by every system designed since: things you own feel instant (your movement, your bag), things the world owns resolve on the host (damage, storage, crafts, structures). The prototype proved the feel holds at simulated 70–140 ms; one real friend-group playtest over real internet gates the ADR that locks the wire. This spec exists so the session model is written down and the twelve systems referencing "the host" all mean the same thing.
Core Rules
- The host is the world. MVP: whoever owns the save file hosts (de facto fixed host — the concept's fixed-vs-rotating question stays open as a group decision before Tier 1 persistence work; save-anyone-can-host is Tier 1 first-to-slip with fixed host as its fallback). NGO 2.x only (no 1.x APIs — Forbidden Patterns); transport **UTP + Unity Relay at MVP, Steam at ship** behind
ISessionLauncher(QQ-04; decided in ADR-0008, 2026-09-11 — replaces "Steam transport expected"). - Two authority rails (the TD boundary, binding on every system):
- Owner rail (owner-authoritative, client-predicted): your movement, your dash i-frames, your CARRY mutations, outcome-independent FX (stances, swing visuals, ghost previews).
- Host rail (host-authoritative, broadcast-confirmed): damage, HP, stagger,
SharedStorage, craft commits, structure placement/demolish, harvest resolution, all registries (KnownRecipes,KnownLandTemplates), the maker's-mark table. No third rail exists; a new feature declares its rail in its own doc. The tide is on neither — it is a pure function every machine computes identically from(seed, t, config). - Session capacity: 2 at MVP (host + 1), 2–5 at Tier 1. The 3rd–5th player is additive risk, not qualitatively new (concept MVP definition) — **no mechanic branches on player *count***, by design (party scaling levers live in Enemy AI density, never in netcode). Presence is a different axis: combat Rule 9's Downed-vs-Dead reads whether another living player is on the land, and Base Building Edge Case 22's exit is social — those branch on who is present, never on how many, and are the only such branches (carve-out recorded 2026-08-27, cross-GDD W7).
- Late-join sync contract. A joining client receives: `worldSeed + configVersion + t` (from which the entire tide state derives purely — the host replicates
tonly — every derived boolean/float is recomputed locally from the same pure function (ADR-0001/0008, aligned 2026-09-11) — never schedule state; a client's own evaluation is presentation-only, tide Edge Case 9 — wording aligned 2026-08-27, cross-GDD W8), the full structure registry snapshot (small — BB's stated default),SharedStoragecontents, bothKnown*sets, the fullInstanceMetadatatable (low hundreds of rows — crafting Edge Case 21; a partial sync would race the signature test on a late-joiner's first rack inspection), and current player states. The world is mostly math; the sync is mostly small tables. A rejoin after disconnect is simply a late join — no separate reconnect path exists. - Leaving and losing players. A client disconnect mid-anything resolves by the owning system's edge case (the shared pattern all five docs use: fired atomic commits never un-happen; un-fired intents are true no-ops). The player's CARRY persists host-side, **and their character despawns** (no offline bodies — nothing simulates for the absent). On rejoin, position follows tide Edge Case 1's existing rule: still-docked land → back where you stood; departed → deposited at the home shore, bag intact. **Session-scoped player state (ruled 2026-08-27, cross-GDD review C1 — reconciles combat Edge Case 2):** the host retains each departed player's HP, Downed status, and remaining bleed-out timer for the rest of the session; a rejoin restores them — a live disconnector returns at their last HP (no disconnect-to-heal via rejoin), a Downed disconnector whose timer is still running returns Downed and revivable, and one whose timer expired returns Dead → respawned home per combat Rule 9. Only a session end resets to full HP (save-persistence Rule 7). **The host quitting ends the session for everyone** — no host migration, ever (Forbidden Patterns; Valheim precedent). The session ending is a save-file fact, not a failure state: everything confirmed is durable (save-persistence Rule 3).
- Forbidden Patterns restated as session law (the full list is binding in technical preferences): no rollback/reconciliation netcode, no dedicated servers, no host migration, no offline simulation, no remote-player prediction. The netcode ADR may add detail; it may not relax these.
- The ADR boundary (explicit, so this spec can't scope-creep): transport final choice, connection/reconnection flow details, RPC shapes for every commit named in Rule 2's host rail, replication representations (NetworkVariable vs RPC per state), intent-validation bounds, tick/send rates, relevancy — **all deferred to
/architecture-decisionafter the friend-group playtest** (the CD-mandated gate). This spec locks model, the ADR locks wire. **Decided 2026-09-11: ADR-0008 (docs/architecture/adr-0008-netcode-wire.md), Accepted under the Gate 0 provisional pass — the friend playtest is now its confirmation.**
Tuning Knobs
No knobs live here — session capacity is a scope fact, and every timing/rate value is the ADR's to set with real-network data. This section exists to say so.
Acceptance Criteria
- [ ] [UNIT] Rail audit (schema-level): every host-rail mutation named in Rule 2 resolves through a host commit path; no owner-rail write touches shared state (the TD boundary as a code-structure fact).
- [ ] [INTEGRATION] Late join mid-session: the joiner derives the identical tide state from
(seed, config, t)alone (no schedule state was transmitted — onlytand derived values, tide AC-47/50) and receives table snapshots — including the fullInstanceMetadatatable — that match the host byte-for-byte. - [ ] [INTEGRATION] Disconnect → rejoin round-trip: the character despawns on disconnect, the rejoin flows through the late-join path, CARRY is bit-identical, HP / Downed status / remaining bleed-out timer carry over per Rule 5 (combat AC-41's mirror), and position follows tide Edge Case 1's rule.
- [ ] [INTEGRATION] Host quit mid-session: all clients end cleanly to menu; reloading the host's save restores everything confirmed before the quit (save-persistence's durability contract, session-side).
- [ ] [INTEGRATION] Client disconnect during a host-rail commit: the commit resolves per the owning system's edge case (spot-checked against crafting EC5, BB EC8–9, harvesting EC1 — one shared pattern, three witnesses).
- [ ] [EXPERIENTIAL] The friend-group playtest criterion (the ADR gate, restated): dash-and-strike over real internet at 2–3 players feels like the prototype did at simulated latency — instant own-actions, "not teleporty" remotes.
- [ ] Regression: no code path predicts a remote player or rewinds host state (Forbidden Patterns hold at the session layer).
Systems Index
Already tracked as MVP system #2 (Core, "Prototype-validated | quick-spec + ADR after friend playtest"). On approval: status → Designed (quick-spec; ADR pending playtest) — with this, **all 13 MVP systems are designed**.
Forward refs owned elsewhere: everything in Rule 7 → netcode ADR (/architecture-decision, after the friend playtest — the one remaining external gate); Steam lobby/invite UX → UI/HUD /ux-design queue; fixed-vs-rotating host → group decision before Tier 1.