From 3b427abcac1b60df6509b4be91db9b27f8123828 Mon Sep 17 00:00:00 2001 From: Daniel Dilly Date: Sun, 21 Dec 2025 21:20:27 -0500 Subject: [PATCH 1/2] Add AC-0001 foundation: Constitution, ADRs, and v0 multiplayer spec - Add/update Constitution: invariants, domain model, acceptance/kill criteria - Refactor ADR-0001 to authoritative multiplayer architecture - Update ADRs 0002-0005 with Constitution ID references - Add FS-TBD spec for v0 two-client multiplayer slice - Update documentation (handbook, repo-map, README, copilot-instructions) Relates to AC-0001 (v0 multiplayer slice acceptance criterion) --- .github/copilot-instructions.md | 2 +- README.md | 2 +- ...-authoritative-multiplayer-architecture.md | 90 +++ docs/adr/0001-three-plane-architecture.md | 68 -- docs/adr/0002-deterministic-simulation.md | 4 +- docs/adr/0003-fixed-timestep-simulation.md | 4 +- .../0004-server-authoritative-architecture.md | 24 +- docs/adr/0005-v0-networking-architecture.md | 8 +- docs/constitution/acceptance-kill.md | 14 +- docs/constitution/domain-model.md | 63 +- docs/constitution/id-catalog.json | 82 +- docs/constitution/id-index-by-tag.md | 45 +- docs/constitution/id-index.md | 14 +- docs/constitution/invariants.md | 8 +- docs/constitution/tag-taxonomy.md | 7 +- docs/handbook.md | 20 +- docs/repo-map.md | 2 +- docs/specs/FS-TBD-v0-multiplayer-slice.md | 705 ++++++++++++++++++ 18 files changed, 1001 insertions(+), 161 deletions(-) create mode 100644 docs/adr/0001-authoritative-multiplayer-architecture.md delete mode 100644 docs/adr/0001-three-plane-architecture.md create mode 100644 docs/specs/FS-TBD-v0-multiplayer-slice.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e1b8ce5..8c596c5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -86,7 +86,7 @@ This repository uses stable Constitution IDs for traceability: - Prefer `cargo clippy` with `-D warnings` - Tests should reference Constitution IDs in comments when verifying invariants -### Simulation Plane +### Simulation Core - **Determinism is non-negotiable** — see `INV-0001`, `INV-0002` - Fixed timestep; no frame-rate-dependent logic - Authoritative state transitions only in `crates/sim` diff --git a/README.md b/README.md index d20ad35..51bb2dc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Foundation phase. Initial workspace + simulation scaffolding is in place. No gam ## What we’re optimizing for - Deterministic simulation and replay verification -- Clean authority boundaries (simulation vs client vs control plane) +- Clean authority boundaries (Simulation Core isolated from Server Edge and Game Client) - Transport-independent protocol semantics - Testability as a first-class feature - Human + agent collaboration within explicit constraints diff --git a/docs/adr/0001-authoritative-multiplayer-architecture.md b/docs/adr/0001-authoritative-multiplayer-architecture.md new file mode 100644 index 0000000..a8a0d32 --- /dev/null +++ b/docs/adr/0001-authoritative-multiplayer-architecture.md @@ -0,0 +1,90 @@ +# ADR 0001: Authoritative Multiplayer Architecture + +## Status +Accepted + +## Type +Technical + +## Context +Flowstate is a competitive multiplayer game that prioritizes correctness, testability, and long-term preservability. To achieve these goals, the system must separate concerns cleanly: game logic must be isolated from networking/session management and from presentation (rendering, input, UI). Without this separation, the simulation becomes entangled with I/O, making determinism impossible to guarantee and testing prohibitively expensive. + +The architecture must support multiple client types (native, web browser) and enable replay verification, which requires the simulation to be pure and side-effect-free. + +## Decision +Flowstate adopts an **Authoritative Multiplayer Architecture** with strict separation of concerns and clear authority boundaries: + +### Runtime Components + +**1. Game Client** — The player runtime. +- Owns: rendering, input capture, UI, interpolation, and (future) prediction/reconciliation. +- MUST NOT authoritatively decide game-outcome-affecting state. + +**2. Matchmaker** — The sole system a Game Client contacts to create/find/join a match. +- Owns: matchmaking, lobbies/queues (if any), match creation, and assignment of players to a specific Game Server Instance. +- Returns connection information/credentials for a Game Server Instance. +- Optional for LAN/dev scenarios; may not exist in local play. + +**3. Game Server Instance** — One running authoritative match runtime (process/container). +- Owns the authoritative game simulation for exactly one match lifecycle. +- Contains exactly two named subcomponents: + + **3a. Server Edge** — The networking and session boundary. + - Owns: all networking, transports, session lifecycle, input validation. + - Performs all I/O. + - Exchanges explicit, typed, tick-indexed messages with the Simulation Core. + + **3b. Simulation Core** — The deterministic game logic. + - Deterministic, fixed-timestep, replayable game rules: physics, movement, abilities, combat resolution. + - Performs NO I/O, networking, rendering, engine calls, or OS/system calls. + - Replayable purely from recorded inputs. + +### Hard Boundaries + +- The Simulation Core MUST NOT perform I/O, networking, rendering, or system calls. +- The Simulation Core MUST be deterministic and replayable from recorded inputs. +- The Game Client MUST NOT authoritatively decide game-outcome-affecting state. +- Communication across boundaries is via explicit, typed, tick-indexed message passing only. + +## Rationale +This architecture enables: +- **Determinism:** Simulation Core has no hidden state from I/O or timing. +- **Testability:** Simulation Core can be tested without graphics, network, or OS dependencies. +- **Replayability:** Record inputs, replay simulation, verify identical outcomes. +- **Multiple clients:** Native, web, headless bots all consume the same simulation. +- **Preservability:** Core game logic remains runnable decades from now (no platform lock-in). +- **Clear authority:** Each component has explicit ownership; no ambiguity about who decides what. + +**Tradeoffs:** +- Higher upfront design cost (explicit boundaries require discipline). +- Cannot use "engine-native" patterns that blur simulation/presentation (e.g., Godot signals from physics to rendering). +- Requires careful message protocol design between Server Edge and Simulation Core. + +## Constraints & References (no prose duplication) +- Constitution IDs: + - INV-0001 (Deterministic Simulation) + - INV-0002 (Fixed Timestep) + - INV-0004 (Simulation Core Isolation) +- Canonical Constitution docs: + - [docs/constitution.md](../constitution.md) — Product Thesis + - [docs/constitution/invariants.md](../constitution/invariants.md) +- Related ADRs: + - ADR-0002 (Deterministic Simulation) — defines properties of Simulation Core + - ADR-0003 (Fixed Timestep) — defines stepping model of Simulation Core + - ADR-0004 (Server-Authoritative) — defines authority model + +## Alternatives Considered +- **Monolithic engine-integrated architecture** — Use Godot's scene tree and signals for all logic. Rejected: Makes determinism and replay impossible; locks simulation to Godot's lifecycle. +- **Merge orchestration into server** — Combine matchmaking and simulation into one component. Rejected: Loses separation between orchestration and simulation; makes headless testing harder. +- **Four-layer (separate Data layer)** — Add explicit persistence layer. Deferred: Can be added later; v0 doesn't require it. + +## Implications +- **Enables:** Deterministic testing, replay verification, multiple client types, long-term preservability. +- **Constrains:** Simulation Core code cannot directly call Godot APIs, network libraries, or OS functions. +- **Migration costs:** None (greenfield project). +- **Contributor impact:** Developers must understand component boundaries and message-passing contracts. + +## Follow-ups +- Define Simulation Core I/O interface (message types for inputs/outputs) +- Establish testing patterns for Simulation Core-only tests +- Document component boundaries in repository map and handbook diff --git a/docs/adr/0001-three-plane-architecture.md b/docs/adr/0001-three-plane-architecture.md deleted file mode 100644 index 4101517..0000000 --- a/docs/adr/0001-three-plane-architecture.md +++ /dev/null @@ -1,68 +0,0 @@ -# ADR 0001: Three-Plane Architecture - -## Status -Accepted - -## Type -Technical - -## Context -Flowstate is a competitive multiplayer game that prioritizes correctness, testability, and long-term preservability. To achieve these goals, the system must separate concerns cleanly: game logic must be isolated from orchestration (matchmaking, lobbies) and presentation (rendering, input, UI). Without this separation, the simulation becomes entangled with I/O, making determinism impossible to guarantee and testing prohibitively expensive. - -The architecture must support multiple client types (native, web browser) and enable replay verification, which requires the simulation to be pure and side-effect-free. - -## Decision -Flowstate adopts a **Three-Plane Architecture** with strict separation of concerns: - -1. **Control Plane** — Orchestration: matchmaking, lobbies, session management, authentication -2. **Simulation Plane** — Authoritative deterministic game logic: physics, movement, abilities, combat resolution -3. **Client Plane** — Presentation: rendering, input capture, UI, interpolation, prediction (non-authoritative) - -Planes are logical boundaries and may be implemented within a single binary or as separate processes. The Control Plane includes the in-process I/O Boundary (network/session owner) and may also include optional external Orchestration Services (see DM-0011, DM-0012). - -**Hard boundaries:** -- The Simulation Plane MUST NOT perform I/O, networking, rendering, or system calls -- The Simulation Plane MUST be deterministic and replayable from recorded inputs -- The Client Plane MUST NOT authoritatively decide game-outcome-affecting state -- Communication between planes is via explicit, typed message passing only - -## Rationale -This architecture enables: -- **Determinism:** Simulation has no hidden state from I/O or timing -- **Testability:** Simulation can be tested without graphics, network, or OS dependencies -- **Replayability:** Record inputs, replay simulation, verify identical outcomes -- **Multiple clients:** Native, web, headless bots all consume the same simulation -- **Preservability:** Core game logic remains runnable decades from now (no platform lock-in) - -**Tradeoffs:** -- Higher upfront design cost (explicit boundaries require discipline) -- Cannot use "engine-native" patterns that blur simulation/presentation (e.g., Godot signals from physics to rendering) -- Requires careful message protocol design between planes - -## Constraints & References (no prose duplication) -- Constitution IDs: - - INV-0001 (Deterministic Simulation) - - INV-0002 (Fixed Timestep) -- Canonical Constitution docs: - - [docs/constitution.md](../constitution.md) — Product Thesis - - [docs/constitution/invariants.md](../constitution/invariants.md) -- Related ADRs: - - ADR-0002 (Deterministic Simulation) — defines properties of Simulation Plane - - ADR-0003 (Fixed Timestep) — defines stepping model of Simulation Plane - - ADR-0004 (Server-Authoritative) — defines distribution of planes - -## Alternatives Considered -- **Monolithic engine-integrated architecture** — Use Godot's scene tree and signals for all logic. Rejected: Makes determinism and replay impossible; locks simulation to Godot's lifecycle. -- **Two-plane (Client/Server only)** — Merge Control Plane into Server. Rejected: Loses separation between orchestration and simulation; makes headless testing harder. -- **Four-plane (separate Data Plane)** — Add explicit persistence layer. Deferred: Can be added later; v0 doesn't require it. - -## Implications -- **Enables:** Deterministic testing, replay verification, multiple client types, long-term preservability -- **Constrains:** Simulation code cannot directly call Godot APIs, network libraries, or OS functions -- **Migration costs:** None (greenfield project) -- **Contributor impact:** Developers must understand plane boundaries and message-passing contracts - -## Follow-ups -- Define Simulation I/O interface (message types for inputs/outputs) -- Establish testing patterns for simulation-plane-only tests -- Document plane boundaries in repository map and handbook diff --git a/docs/adr/0002-deterministic-simulation.md b/docs/adr/0002-deterministic-simulation.md index f175a9a..1094ef9 100644 --- a/docs/adr/0002-deterministic-simulation.md +++ b/docs/adr/0002-deterministic-simulation.md @@ -9,7 +9,7 @@ Technical ## Context Competitive multiplayer games require verifiable correctness. Players expect consistent outcomes: the same inputs should produce the same results every time. Without determinism, replay verification is impossible, testing becomes unreliable, and subtle bugs can manifest as desync or inconsistent match outcomes across clients. -The Simulation Plane (ADR-0001) is the authoritative source of truth for game state. If the simulation is non-deterministic, there is no "truth"—only probabilistic approximations. +The Simulation Core (DM-0014, defined in ADR-0001) is the authoritative source of truth for game state. If the simulation is non-deterministic, there is no "truth"—only probabilistic approximations. ## Decision The authoritative simulation MUST be **deterministic**: identical initial state, input sequence, seed, and tuning parameters MUST produce identical outcomes across all runs, platforms, and compiler configurations. @@ -55,7 +55,7 @@ The authoritative simulation MUST be **deterministic**: identical initial state, - Canonical Constitution docs: - [docs/constitution/invariants.md](../constitution/invariants.md) - Related ADRs: - - ADR-0001 (Three-Plane Architecture) — defines Simulation Plane where determinism applies + - ADR-0001 (Authoritative Multiplayer Architecture) — defines Simulation Core where determinism applies - ADR-0003 (Fixed Timestep) — fixed timestep is a prerequisite for determinism ## Alternatives Considered diff --git a/docs/adr/0003-fixed-timestep-simulation.md b/docs/adr/0003-fixed-timestep-simulation.md index b8daf31..727e5ff 100644 --- a/docs/adr/0003-fixed-timestep-simulation.md +++ b/docs/adr/0003-fixed-timestep-simulation.md @@ -12,7 +12,7 @@ Variable delta time (frame-rate-dependent simulation) causes non-determinism: th To achieve determinism (ADR-0002), the simulation must advance in predictable, fixed-size increments independent of frame rate or wall-clock time. ## Decision -The Simulation Plane (ADR-0001) MUST advance in **fixed discrete ticks**. Each tick represents a fixed duration of simulated time. +The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discrete ticks**. Each tick represents a fixed duration of simulated time. **Requirements:** - Simulation stepping function: `advance(state, inputs, dt_seconds)` where `dt_seconds` is constant for a match @@ -56,7 +56,7 @@ The Simulation Plane (ADR-0001) MUST advance in **fixed discrete ticks**. Each t - [docs/constitution/invariants.md](../constitution/invariants.md) - [docs/constitution/domain-model.md](../constitution/domain-model.md) - Related ADRs: - - ADR-0001 (Three-Plane Architecture) — defines Simulation Plane where fixed timestep applies + - ADR-0001 (Authoritative Multiplayer Architecture) — defines Simulation Core where fixed timestep applies - ADR-0002 (Deterministic Simulation) — fixed timestep enables determinism ## Alternatives Considered diff --git a/docs/adr/0004-server-authoritative-architecture.md b/docs/adr/0004-server-authoritative-architecture.md index d879a94..22ff278 100644 --- a/docs/adr/0004-server-authoritative-architecture.md +++ b/docs/adr/0004-server-authoritative-architecture.md @@ -9,20 +9,20 @@ Technical ## Context Competitive multiplayer games require a trusted arbiter to prevent cheating and ensure consistent outcomes. If clients authoritatively decide game-affecting state (e.g., "I hit the target", "I have 1000 resources"), players can modify their clients to cheat, and different clients may disagree on match outcomes. -The Simulation Plane (ADR-0001) is deterministic (ADR-0002), but determinism alone does not prevent cheating—clients could run modified simulations. A single authoritative instance must enforce the rules. +The Simulation Core (DM-0014, defined in ADR-0001) is deterministic (ADR-0002), but determinism alone does not prevent cheating—clients could run modified simulations. A single authoritative instance must enforce the rules. ## Decision The **server is the single source of truth** for all game-outcome-affecting state. Clients are presentation-only and never authoritatively decide hits, damage, resource changes, or state transitions. **Server responsibilities:** -- Run the authoritative Simulation Plane (ADR-0001) +- Run the authoritative Simulation Core (DM-0014, defined in ADR-0001) - Receive client inputs (movement intent, aim direction, action requests) - Advance simulation via deterministic stepping (ADR-0002, ADR-0003) -- Broadcast authoritative state snapshots to clients +- Broadcast authoritative state snapshots to Game Clients - Validate inputs (rate limiting, bounds checking, action legality) - Enforce match rules and win conditions -**Client responsibilities:** +**Game Client responsibilities:** - Capture player inputs and send to server - Receive authoritative snapshots from server - Render game state (interpolate/predict for responsiveness) @@ -30,10 +30,10 @@ The **server is the single source of truth** for all game-outcome-affecting stat - MUST NOT authoritatively decide: hits, damage, status effects, spawns, despawns, resource changes **Future ADR: Client Prediction & Reconciliation:** -- Clients MAY predict local player movement for responsiveness -- Clients MUST reconcile predicted state when authoritative snapshot arrives +- Game Clients MAY predict local player movement for responsiveness +- Game Clients MUST reconcile predicted state when authoritative snapshot arrives - Cosmetic effects (muzzle flash, VFX) are allowed but never commit game state -- (Deferred to post-v0; v0 clients render authoritative snapshots only) +- (Deferred to post-v0; v0 Game Clients render authoritative snapshots only) ## Rationale **Why server-authoritative:** @@ -65,7 +65,7 @@ The **server is the single source of truth** for all game-outcome-affecting stat - [docs/constitution.md](../constitution.md) — "server-authoritative" is explicit in-scope goal - [docs/constitution/scope-non-goals.md](../constitution/scope-non-goals.md) - Related ADRs: - - ADR-0001 (Three-Plane Architecture) — server runs Simulation Plane, clients run Client Plane + - ADR-0001 (Authoritative Multiplayer Architecture) — server runs Simulation Core, Game Clients present state - ADR-0002 (Deterministic Simulation) — server simulation is deterministic and replayable - ADR-0003 (Fixed Timestep) — server simulation advances in fixed ticks @@ -76,12 +76,12 @@ The **server is the single source of truth** for all game-outcome-affecting stat ## Implications - **Enables:** Cheat resistance, consistent match outcomes, replay verification from server perspective, competitive integrity -- **Constrains:** Requires server hosting infrastructure, clients cannot play offline (for competitive modes), latency must be masked via prediction +- **Constrains:** Requires server hosting infrastructure, Game Clients cannot play offline (for competitive modes), latency must be masked via prediction - **Migration costs:** None (greenfield project) -- **Contributor impact:** All simulation changes must consider server authority; clients cannot "decide" outcomes locally +- **Contributor impact:** All Simulation Core changes must consider server authority; Game Clients cannot "decide" outcomes locally ## Follow-ups -- Define client-server message protocol (inputs client→server, snapshots server→client) +- Define client-server message protocol (inputs Game Client→server, snapshots server→Game Client) - Implement input validation at simulation boundary (rate limiting, bounds checking) -- Document prediction/reconciliation strategy for clients (Future ADR: Client Prediction & Reconciliation) +- Document prediction/reconciliation strategy for Game Clients (Future ADR: Client Prediction & Reconciliation) - Establish server hosting requirements (headless mode, tick rate targets, scalability) diff --git a/docs/adr/0005-v0-networking-architecture.md b/docs/adr/0005-v0-networking-architecture.md index 462c287..ef40dfb 100644 --- a/docs/adr/0005-v0-networking-architecture.md +++ b/docs/adr/0005-v0-networking-architecture.md @@ -13,7 +13,7 @@ Flowstate requires client-server networked multiplayer. The goal for v0 is to de Key constraints: - The simulation MUST be deterministic (INV-0001) and advance in fixed Ticks (INV-0002) - The server is authoritative (INV-0003); client inputs are intent, not commands -- The Simulation Plane MUST NOT perform I/O or networking (INV-0004) +- The Simulation Core MUST NOT perform I/O or networking (INV-0004) - All inputs and outputs MUST carry explicit Tick identifiers (INV-0005) - Replay artifacts MUST reproduce authoritative outcomes on the same build/platform (INV-0006) @@ -30,7 +30,7 @@ We adopt a **two-layer networking architecture**: ### A. Semantic Contract (Transport-Agnostic) -The I/O Boundary (DM-0011) mediates communication between the Control Plane and Simulation Plane via **tick-indexed messages** on **logical Channels** (DM-0009) with defined semantics: +The Server Edge (DM-0011) mediates communication between Game Clients (DM-0015) and the Simulation Core (DM-0014) via **tick-indexed messages** on **logical Channels** (DM-0009) with defined semantics: - **Realtime Channel:** Unreliable + sequenced (discard older packets) - Uses: Snapshots (DM-0007), InputCmds (DM-0006) @@ -83,12 +83,12 @@ v0 guarantees determinism **within the same build and platform**. Cross-platform ## Constraints & References - Constitution IDs: - - INV-0001 (Deterministic Simulation), INV-0002 (Fixed Timestep), INV-0003 (Authoritative Simulation), INV-0004 (Simulation Plane Isolation), INV-0005 (Tick-Indexed I/O Contract), INV-0006 (Replay Verifiability) + - INV-0001 (Deterministic Simulation), INV-0002 (Fixed Timestep), INV-0003 (Authoritative Simulation), INV-0004 (Simulation Core Isolation), INV-0005 (Tick-Indexed I/O Contract), INV-0006 (Replay Verifiability) - DM-0001 (Tick), DM-0006 (InputCmd), DM-0007 (Snapshot), DM-0008 (Session), DM-0009 (Channel), DM-0010 (Match) - AC-0001 (v0 Two-Client Multiplayer Slice) - KC-0001 (Plane Boundary Violation), KC-0002 (Replay Verification Blocker) - Related ADRs: - - ADR-0001 (Three-Plane Architecture) + - ADR-0001 (Authoritative Multiplayer Architecture) - ADR-0002 (Deterministic Simulation) - ADR-0003 (Fixed Timestep Simulation) - ADR-0004 (Server Authoritative Architecture) diff --git a/docs/constitution/acceptance-kill.md b/docs/constitution/acceptance-kill.md index 1639283..c43578a 100644 --- a/docs/constitution/acceptance-kill.md +++ b/docs/constitution/acceptance-kill.md @@ -26,11 +26,11 @@ ENTRY FORMAT (use H3 with anchor): **Status:** Proposed **Tags:** phase0, networking, replay -**Pass Condition:** The system MUST demonstrate functional end-to-end multiplayer with two connected clients where: +**Pass Condition:** The system MUST demonstrate functional end-to-end multiplayer with two connected Game Clients where: -1. **Connectivity & initial authoritative state transfer (JoinBaseline):** Two native clients can connect to a server instance, complete handshake, receive initial authoritative state, and remain synchronized. -2. **Gameplay slice (WASD control):** Each client can issue WASD movement inputs; the authoritative simulation processes them; both clients see their own and the opponent's movement via snapshots with acceptable consistency. -3. **Simulation plane boundary integrity:** The authoritative simulation produces identical outcomes for identical input+seed+state across multiple runs (same build/platform), verified by Tier-0 replay test. The simulation MUST NOT perform I/O, networking, rendering, or wall-clock reads (INV-0001, INV-0002, INV-0004). +1. **Connectivity & initial authoritative state transfer (JoinBaseline):** Two native Game Clients can connect to a Game Server Instance, complete handshake, receive initial authoritative state, and remain synchronized. +2. **Gameplay slice (WASD control):** Each Game Client can issue WASD movement inputs; the authoritative simulation processes them; both Game Clients see their own and the opponent's movement via snapshots with acceptable consistency. +3. **Simulation Core boundary integrity:** The authoritative simulation produces identical outcomes for identical input+seed+state across multiple runs (same build/platform), verified by Tier-0 replay test. The Simulation Core MUST NOT perform I/O, networking, rendering, or wall-clock reads (INV-0001, INV-0002, INV-0004). 4. **Tier-0 input validation:** Server enforces magnitude limit, tick window sanity check, and rate limit (values in [docs/networking/v0-parameters.md](../networking/v0-parameters.md)); malformed or out-of-policy inputs are rejected without crashing. 5. **Replay artifact generation:** A completed match produces a replay artifact (initial state, seed, input stream, final state hash) that can reproduce the authoritative outcome on the same build/platform (INV-0006). @@ -38,13 +38,13 @@ ENTRY FORMAT (use H3 with anchor): ## Kill Criteria -### KC-0001 — Plane Boundary Violation +### KC-0001 — Simulation Core Boundary Violation **Status:** Proposed **Tags:** architecture, determinism, networking -**Kill Condition:** Reject any change that introduces networking, file I/O, rendering dependencies, wall-clock time reads, OS/system calls, game engine API calls, or other external side effects into the Simulation Plane (as defined in ADR-0001, enforced by INV-0004). This is a **hard stop**—no exceptions for expediency. +**Kill Condition:** Reject any change that introduces networking, file I/O, rendering dependencies, wall-clock time reads, OS/system calls, game engine API calls, or other external side effects into the Simulation Core (as defined in DM-0014, enforced by INV-0004). This is a **hard stop**—no exceptions for expediency. -*Non-normative note: This guards the architectural foundation. Once the boundary is compromised, determinism (INV-0001), replay (INV-0006), and testability collapse. If a feature "requires" Simulation Plane I/O, the correct solution is to refactor the feature's design, not weaken the plane boundary.* +*Non-normative note: This guards the architectural foundation. Once the boundary is compromised, determinism (INV-0001), replay (INV-0006), and testability collapse. If a feature "requires" Simulation Core I/O, the correct solution is to refactor the feature's design, not weaken the boundary.* ### KC-0002 — Replay Verification Blocker **Status:** Proposed diff --git a/docs/constitution/domain-model.md b/docs/constitution/domain-model.md index 3f53553..2e1a39d 100644 --- a/docs/constitution/domain-model.md +++ b/docs/constitution/domain-model.md @@ -60,17 +60,17 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** networking, state-sync, protocol -**Definition:** A tick-indexed serialization of authoritative world state produced at a specific Tick (DM-0001) by the authoritative simulation, transmitted to clients by the I/O Boundary (DM-0011) for state synchronization. +**Definition:** A tick-indexed serialization of authoritative world state produced at a specific Tick (DM-0001) by the Simulation Core (DM-0014), transmitted to Game Clients (DM-0015) by the Server Edge (DM-0011) for state synchronization. -*Non-normative note: Snapshots are authoritative. Clients use them for reconciliation and remote entity interpolation. Snapshot packing strategies (priority-based, budget-aware) are implementation details. The simulation produces snapshot data; the I/O Boundary handles transmission.* +*Non-normative note: Snapshots are authoritative. Game Clients use them for reconciliation and remote entity interpolation. Snapshot packing strategies (priority-based, budget-aware) are implementation details. The Simulation Core produces snapshot data; the Server Edge handles transmission.* ### DM-0008 — Session **Status:** Active -**Tags:** networking, control-plane, connection +**Tags:** networking, connection -**Definition:** A client's connection lifecycle from handshake through disconnect, including assigned player identity and synchronization state. Sessions are control-plane owned and not part of simulation state. +**Definition:** A client's connection lifecycle from handshake through disconnect, including assigned player identity and synchronization state. Sessions are owned by the Server Edge (DM-0011) and are not part of simulation state. -*Non-normative note: The simulation knows about player inputs and entity state. The I/O Boundary (DM-0011) manages session lifecycle (connections, authentication, tokens). Don't conflate simulation state with session state.* +*Non-normative note: The simulation knows about player inputs and entity state. The Server Edge (DM-0011) manages session lifecycle (connections, authentication, tokens). Don't conflate simulation state with session state.* ### DM-0009 — Channel **Status:** Active @@ -82,27 +82,60 @@ Section groupings (H2) are optional, for organization only. ### DM-0010 — Match **Status:** Proposed -**Tags:** orchestration, control-plane, replay +**Tags:** orchestration, replay **Definition:** A discrete game instance with a defined lifecycle (create → active → end), a fixed simulation tick rate, an initial authoritative state, and a set of participating Sessions (DM-0008). Match is the scope boundary for gameplay, replay artifacts, and outcome determination. -*Non-normative note: A Match corresponds to "one game" from a player's perspective. It has a stable tick rate (see [docs/networking/v0-parameters.md](../networking/v0-parameters.md) for v0 values), a known start state, and produces a complete input+output history for replay (INV-0006). Session management is control-plane logic; the simulation sees player inputs and produces world state. Match is the conceptual glue between them.* +*Non-normative note: A Match corresponds to "one game" from a player's perspective. It has a stable tick rate (see [docs/networking/v0-parameters.md](../networking/v0-parameters.md) for v0 values), a known start state, and produces a complete input+output history for replay (INV-0006). Session management is Server Edge logic; the Simulation Core sees player inputs and produces world state. Match is the conceptual glue between them.* -### DM-0011 — I/O Boundary +### DM-0011 — Server Edge **Status:** Active -**Tags:** networking, control-plane, architecture +**Tags:** networking, architecture -**Definition:** The in-process Control Plane component that owns sockets/transports, validates inputs, and exchanges tick-indexed messages with the Simulation Plane. Responsible for all external I/O and converting it into serializable, tick-indexed messages. +**Definition:** The networking and session boundary within a Game Server Instance (DM-0013). Owns sockets/transports, validates inputs, manages Sessions (DM-0008), and exchanges tick-indexed messages with the Simulation Core (DM-0014). Performs all I/O operations on behalf of the Game Server Instance. -*Non-normative note: The I/O Boundary is the runtime interface between the outside world and the deterministic simulation core. It handles networking, session lifecycle, snapshot transmission, and (in future) replay recording. It lives in the same process as the simulation but maintains a strict message-passing boundary per INV-0004.* +*Non-normative note: The Server Edge is the runtime interface between the outside world and the deterministic Simulation Core. It handles networking, session lifecycle, snapshot transmission, and (in future) replay recording. It lives in the same process as the Simulation Core but maintains a strict message-passing boundary per INV-0004. The term "I/O boundary" may be used descriptively, but Server Edge is the canonical component name.* -### DM-0012 — Orchestration Service +### DM-0012 — Matchmaker **Status:** Active -**Tags:** orchestration, control-plane, preservability +**Tags:** orchestration, infrastructure, preservability -**Definition:** An optional Control Plane component, typically external, providing matchmaking, lobbies, authentication, or server provisioning. Must not contain game rules. May not exist in LAN/dev scenarios. Orchestration Services are outside the game server's I/O Boundary and must not inject authoritative simulation inputs except through the same tick-indexed message interface used for Sessions. +**Definition:** The sole system a Game Client (DM-0015) contacts to create, find, or join a match. Owns matchmaking, lobbies/queues (if any), match creation, and assignment of players to a specific Game Server Instance (DM-0013). Returns connection information/credentials for the assigned Game Server Instance. Must not contain game rules. Optional for LAN/dev scenarios. + +*Non-normative note: The Matchmaker is the single entry point for clients seeking a match. Any other backend concepts (authentication, analytics, provisioning) are dependencies of the Matchmaker, not peer authorities. For local/LAN play, the Matchmaker may not exist—clients connect directly to a known Game Server Instance. The Matchmaker never directly manipulates simulation state.* + +### DM-0013 — Game Server Instance +**Status:** Active +**Tags:** architecture, networking + +**Definition:** One running authoritative match runtime (process/container). Owns the authoritative game simulation for exactly one match lifecycle. Contains exactly two named subcomponents: the Server Edge (DM-0011) and the Simulation Core (DM-0014). The Server Edge performs all I/O; the Simulation Core contains deterministic game logic. + +*Non-normative note: In v0, the Game Server Instance may include minimal match lifecycle logic for dev/LAN (e.g., auto-start when two clients connect), but this logic must not bypass the Server Edge or inject authoritative state directly into the Simulation Core.* + +### DM-0014 — Simulation Core +**Status:** Active +**Tags:** simulation, determinism, architecture + +**Definition:** The deterministic, fixed-timestep, replayable game rules engine that defines authoritative state transitions for World (DM-0002). It is engine-agnostic and safe to embed in multiple hosts (e.g., Game Server Instance (DM-0013) and, in future tiers, clients for prediction/rollback), but only the authoritative server instance is permitted to commit game-outcome-affecting state (see INV-0003). The Simulation Core performs NO I/O, networking, rendering, engine calls, or OS/system calls. Replayable purely from recorded inputs. + +*Non-normative note: The Simulation Core advances in discrete Ticks (DM-0001). It consumes validated Tick-indexed InputCmds (DM-0006) via the Server Edge (DM-0011), produces Baselines (DM-0016) and Snapshots (DM-0007), and maintains World state. If clients implement prediction/rollback, they MUST invoke the same Simulation Core logic (same rules/version) rather than duplicating gameplay math; client results remain non-authoritative and are reconciled to server snapshots. Isolation is enforced by INV-0004.* + +### DM-0015 — Game Client +**Status:** Active +**Tags:** presentation, networking + +**Definition:** The player runtime. Owns rendering, input capture, UI, interpolation, and (future) prediction/reconciliation. Connects to a Game Server Instance (DM-0013) via its Server Edge (DM-0011). Receives Snapshots (DM-0007) and sends InputCmds (DM-0006). MUST NOT authoritatively decide game-outcome-affecting state. + +*Non-normative note: Game Clients are untrusted. They capture player intent (inputs) and send it to the server. The server decides outcomes. Clients render authoritative state received via Snapshots. Client-side prediction is a presentation optimization that does not affect authoritative outcomes.* + +### DM-0016 — Baseline +**Status:** Active +**Tags:** networking, state-sync, protocol, replay + +**Definition:** A tick-indexed serialization of authoritative world state at a specific Tick (DM-0001) before inputs are applied at that tick. Used for join synchronization (JoinBaseline) and replay initialization. Distinguished from Snapshot (DM-0007) by timing: Baseline at tick T is pre-step state; Snapshot at tick T+1 is post-step state after inputs at tick T are applied and the step executes. Baseline.tick uses the canonical simulation tick numbering. + +*Non-normative note: Baseline eliminates ambiguity in initial state handling. When a client joins mid-match or a replay starts, Baseline provides the deterministic starting point. The Simulation Core emits Baseline as a serializable artifact; the Server Edge owns all I/O and transmission. Baseline.tick = T means “world state before any inputs are applied at tick T.”* -*Non-normative note: Examples include a matchmaking service that pairs players, a lobby server that manages pre-game chat, or a provisioning system that spins up game server instances. These services may be separate processes, separate binaries, or entirely absent for local/LAN play. They communicate with game servers via standard session/network pathways, never by directly manipulating simulation state.* ## Domain Model Change Policy diff --git a/docs/constitution/id-catalog.json b/docs/constitution/id-catalog.json index 0b7df12..c5b8c97 100644 --- a/docs/constitution/id-catalog.json +++ b/docs/constitution/id-catalog.json @@ -129,7 +129,6 @@ "status": "Active", "tags": [ "networking", - "control-plane", "connection" ], "file": "docs/constitution/domain-model.md", @@ -161,7 +160,6 @@ "status": "Proposed", "tags": [ "orchestration", - "control-plane", "replay" ], "file": "docs/constitution/domain-model.md", @@ -173,11 +171,10 @@ "id": "DM-0011", "prefix": "DM", "number": 11, - "title": "I/O Boundary", + "title": "Server Edge", "status": "Active", "tags": [ "networking", - "control-plane", "architecture" ], "file": "docs/constitution/domain-model.md", @@ -189,11 +186,11 @@ "id": "DM-0012", "prefix": "DM", "number": 12, - "title": "Orchestration Service", + "title": "Matchmaker", "status": "Active", "tags": [ "orchestration", - "control-plane", + "infrastructure", "preservability" ], "file": "docs/constitution/domain-model.md", @@ -201,6 +198,69 @@ "href": "docs/constitution/domain-model.md#DM-0012", "section": "Core Simulation" }, + { + "id": "DM-0013", + "prefix": "DM", + "number": 13, + "title": "Game Server Instance", + "status": "Active", + "tags": [ + "architecture", + "networking" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0013", + "href": "docs/constitution/domain-model.md#DM-0013", + "section": "Core Simulation" + }, + { + "id": "DM-0014", + "prefix": "DM", + "number": 14, + "title": "Simulation Core", + "status": "Active", + "tags": [ + "simulation", + "determinism", + "architecture" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0014", + "href": "docs/constitution/domain-model.md#DM-0014", + "section": "Core Simulation" + }, + { + "id": "DM-0015", + "prefix": "DM", + "number": 15, + "title": "Game Client", + "status": "Active", + "tags": [ + "presentation", + "networking" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0015", + "href": "docs/constitution/domain-model.md#DM-0015", + "section": "Core Simulation" + }, + { + "id": "DM-0016", + "prefix": "DM", + "number": 16, + "title": "Baseline", + "status": "Active", + "tags": [ + "networking", + "state-sync", + "protocol", + "replay" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0016", + "href": "docs/constitution/domain-model.md#DM-0016", + "section": "Core Simulation" + }, { "id": "INV-0001", "prefix": "INV", @@ -253,7 +313,7 @@ "id": "INV-0004", "prefix": "INV", "number": 4, - "title": "Simulation Plane Isolation", + "title": "Simulation Core Isolation", "status": "Active", "tags": [ "architecture", @@ -301,7 +361,7 @@ "id": "KC-0001", "prefix": "KC", "number": 1, - "title": "Plane Boundary Violation", + "title": "Simulation Core Boundary Violation", "status": "Proposed", "tags": [ "architecture", @@ -345,12 +405,12 @@ "id": "ADR-0001", "prefix": "ADR", "number": 1, - "title": "ADR 0001: Three-Plane Architecture", + "title": "ADR 0001: Authoritative Multiplayer Architecture", "status": "Unknown", "tags": [], - "file": "docs/adr/0001-three-plane-architecture.md", + "file": "docs/adr/0001-authoritative-multiplayer-architecture.md", "anchor": "", - "href": "docs/adr/0001-three-plane-architecture.md", + "href": "docs/adr/0001-authoritative-multiplayer-architecture.md", "section": "" }, { diff --git a/docs/constitution/id-index-by-tag.md b/docs/constitution/id-index-by-tag.md index 6d5eea0..40c8065 100644 --- a/docs/constitution/id-index-by-tag.md +++ b/docs/constitution/id-index-by-tag.md @@ -8,9 +8,11 @@ This index is link-only. All substance lives in the canonical Constitution docum ## architecture - [DM-0005](./domain-model.md#DM-0005) — Entity -- [DM-0011](./domain-model.md#DM-0011) — I/O Boundary -- [INV-0004](./invariants.md#INV-0004) — Simulation Plane Isolation -- [KC-0001](./acceptance-kill.md#KC-0001) — Plane Boundary Violation +- [DM-0011](./domain-model.md#DM-0011) — Server Edge +- [DM-0013](./domain-model.md#DM-0013) — Game Server Instance +- [DM-0014](./domain-model.md#DM-0014) — Simulation Core +- [INV-0004](./invariants.md#INV-0004) — Simulation Core Isolation +- [KC-0001](./acceptance-kill.md#KC-0001) — Simulation Core Boundary Violation ## authority @@ -20,21 +22,15 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0008](./domain-model.md#DM-0008) — Session -## control-plane - -- [DM-0008](./domain-model.md#DM-0008) — Session -- [DM-0010](./domain-model.md#DM-0010) — Match -- [DM-0011](./domain-model.md#DM-0011) — I/O Boundary -- [DM-0012](./domain-model.md#DM-0012) — Orchestration Service - ## determinism - [DM-0001](./domain-model.md#DM-0001) — Tick +- [DM-0014](./domain-model.md#DM-0014) — Simulation Core - [INV-0001](./invariants.md#INV-0001) — Deterministic Simulation - [INV-0002](./invariants.md#INV-0002) — Fixed Timestep -- [INV-0004](./invariants.md#INV-0004) — Simulation Plane Isolation +- [INV-0004](./invariants.md#INV-0004) — Simulation Core Isolation - [INV-0006](./invariants.md#INV-0006) — Replay Verifiability -- [KC-0001](./acceptance-kill.md#KC-0001) — Plane Boundary Violation +- [KC-0001](./acceptance-kill.md#KC-0001) — Simulation Core Boundary Violation ## entity @@ -44,6 +40,10 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0005](./domain-model.md#DM-0005) — Entity +## infrastructure + +- [DM-0012](./domain-model.md#DM-0012) — Matchmaker + ## input - [DM-0006](./domain-model.md#DM-0006) — InputCmd @@ -59,10 +59,13 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0007](./domain-model.md#DM-0007) — Snapshot - [DM-0008](./domain-model.md#DM-0008) — Session - [DM-0009](./domain-model.md#DM-0009) — Channel -- [DM-0011](./domain-model.md#DM-0011) — I/O Boundary +- [DM-0011](./domain-model.md#DM-0011) — Server Edge +- [DM-0013](./domain-model.md#DM-0013) — Game Server Instance +- [DM-0015](./domain-model.md#DM-0015) — Game Client +- [DM-0016](./domain-model.md#DM-0016) — Baseline - [INV-0003](./invariants.md#INV-0003) — Authoritative Simulation - [INV-0005](./invariants.md#INV-0005) — Tick-Indexed I/O Contract -- [KC-0001](./acceptance-kill.md#KC-0001) — Plane Boundary Violation +- [KC-0001](./acceptance-kill.md#KC-0001) — Simulation Core Boundary Violation ## operations @@ -71,7 +74,7 @@ This index is link-only. All substance lives in the canonical Constitution docum ## orchestration - [DM-0010](./domain-model.md#DM-0010) — Match -- [DM-0012](./domain-model.md#DM-0012) — Orchestration Service +- [DM-0012](./domain-model.md#DM-0012) — Matchmaker ## phase0 @@ -81,20 +84,26 @@ This index is link-only. All substance lives in the canonical Constitution docum - [INV-0002](./invariants.md#INV-0002) — Fixed Timestep +## presentation + +- [DM-0015](./domain-model.md#DM-0015) — Game Client + ## preservability -- [DM-0012](./domain-model.md#DM-0012) — Orchestration Service +- [DM-0012](./domain-model.md#DM-0012) — Matchmaker ## protocol - [DM-0006](./domain-model.md#DM-0006) — InputCmd - [DM-0007](./domain-model.md#DM-0007) — Snapshot - [DM-0009](./domain-model.md#DM-0009) — Channel +- [DM-0016](./domain-model.md#DM-0016) — Baseline ## replay - [AC-0001](./acceptance-kill.md#AC-0001) — v0 Two-Client Multiplayer Slice - [DM-0010](./domain-model.md#DM-0010) — Match +- [DM-0016](./domain-model.md#DM-0016) — Baseline - [INV-0001](./invariants.md#INV-0001) — Deterministic Simulation - [INV-0005](./invariants.md#INV-0005) — Tick-Indexed I/O Contract - [INV-0006](./invariants.md#INV-0006) — Replay Verifiability @@ -110,16 +119,18 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0002](./domain-model.md#DM-0002) — World - [DM-0003](./domain-model.md#DM-0003) — Character - [DM-0005](./domain-model.md#DM-0005) — Entity +- [DM-0014](./domain-model.md#DM-0014) — Simulation Core - [INV-0001](./invariants.md#INV-0001) — Deterministic Simulation - [INV-0002](./invariants.md#INV-0002) — Fixed Timestep ## state-sync - [DM-0007](./domain-model.md#DM-0007) — Snapshot +- [DM-0016](./domain-model.md#DM-0016) — Baseline ## testability -- [INV-0004](./invariants.md#INV-0004) — Simulation Plane Isolation +- [INV-0004](./invariants.md#INV-0004) — Simulation Core Isolation ## traceability diff --git a/docs/constitution/id-index.md b/docs/constitution/id-index.md index 0937049..2cb2d9b 100644 --- a/docs/constitution/id-index.md +++ b/docs/constitution/id-index.md @@ -10,7 +10,7 @@ This index is link-only. All substance lives in the canonical Constitution docum - [INV-0001](./invariants.md#INV-0001) — Deterministic Simulation - [INV-0002](./invariants.md#INV-0002) — Fixed Timestep - [INV-0003](./invariants.md#INV-0003) — Authoritative Simulation -- [INV-0004](./invariants.md#INV-0004) — Simulation Plane Isolation +- [INV-0004](./invariants.md#INV-0004) — Simulation Core Isolation - [INV-0005](./invariants.md#INV-0005) — Tick-Indexed I/O Contract - [INV-0006](./invariants.md#INV-0006) — Replay Verifiability @@ -26,8 +26,12 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0008](./domain-model.md#DM-0008) — Session - [DM-0009](./domain-model.md#DM-0009) — Channel - [DM-0010](./domain-model.md#DM-0010) — Match -- [DM-0011](./domain-model.md#DM-0011) — I/O Boundary -- [DM-0012](./domain-model.md#DM-0012) — Orchestration Service +- [DM-0011](./domain-model.md#DM-0011) — Server Edge +- [DM-0012](./domain-model.md#DM-0012) — Matchmaker +- [DM-0013](./domain-model.md#DM-0013) — Game Server Instance +- [DM-0014](./domain-model.md#DM-0014) — Simulation Core +- [DM-0015](./domain-model.md#DM-0015) — Game Client +- [DM-0016](./domain-model.md#DM-0016) — Baseline ## Acceptance Criteria @@ -35,13 +39,13 @@ This index is link-only. All substance lives in the canonical Constitution docum ## Kill Criteria -- [KC-0001](./acceptance-kill.md#KC-0001) — Plane Boundary Violation +- [KC-0001](./acceptance-kill.md#KC-0001) — Simulation Core Boundary Violation - [KC-0002](./acceptance-kill.md#KC-0002) — Replay Verification Blocker ## Architecture Decision Records - [ADR-0000](../adr/0000-adr-template.md) — ADR XXXX: -- [ADR-0001](../adr/0001-three-plane-architecture.md) — ADR 0001: Three-Plane Architecture +- [ADR-0001](../adr/0001-authoritative-multiplayer-architecture.md) — ADR 0001: Authoritative Multiplayer Architecture - [ADR-0002](../adr/0002-deterministic-simulation.md) — ADR 0002: Deterministic Simulation - [ADR-0003](../adr/0003-fixed-timestep-simulation.md) — ADR 0003: Fixed Timestep Simulation Model - [ADR-0004](../adr/0004-server-authoritative-architecture.md) — ADR 0004: Server-Authoritative Architecture diff --git a/docs/constitution/invariants.md b/docs/constitution/invariants.md index beef903..b0ee29e 100644 --- a/docs/constitution/invariants.md +++ b/docs/constitution/invariants.md @@ -38,19 +38,19 @@ Section groupings (H2) are optional, for organization only. *Non-normative note: Game-outcome-affecting state includes but is not limited to: hits, damage, status effects, resource changes, entity spawns/despawns, ability cooldowns. This rule applies regardless of whether the authoritative instance runs on a dedicated server, listen-server, or relay architecture.* -### <a id="INV-0004"></a> INV-0004 — Simulation Plane Isolation +### <a id="INV-0004"></a> INV-0004 — Simulation Core Isolation **Status:** Active **Tags:** architecture, determinism, testability -**Statement:** The Simulation Plane MUST NOT perform I/O operations, networking, rendering, wall-clock time reads, or system calls. All external communication MUST occur through explicit, serializable message boundaries owned by the Control Plane. Explicit seeded randomness consistent with INV-0001 is permitted. +**Statement:** The Simulation Core (DM-0014) MUST NOT perform I/O operations, networking, rendering, wall-clock time reads, or system calls. All external communication MUST occur through explicit, serializable message boundaries owned by the Server Edge (DM-0011). Explicit seeded randomness consistent with INV-0001 is permitted. -*Non-normative note: This enables determinism (INV-0001), testability, and replay (INV-0006). The simulation may use seeded RNG as long as the seed is recorded. "Serializable message boundaries" means no function pointers, closures, or ambient state in the interface. "Control Plane" includes the in-process I/O Boundary (DM-0011) responsible for networking and session I/O.* +*Non-normative note: This enables determinism (INV-0001), testability, and replay (INV-0006). The simulation may use seeded RNG as long as the seed is recorded. "Serializable message boundaries" means no function pointers, closures, or ambient state in the interface.* ### <a id="INV-0005"></a> INV-0005 — Tick-Indexed I/O Contract **Status:** Active **Tags:** replay, traceability, networking -**Statement:** All inputs delivered to the Simulation Plane and all outputs emitted from it MUST carry an explicit Tick (DM-0001) identifier. This identifier MUST represent the simulation tick at which the input is applied or the output is generated, not network timestamps or client-local time. For any given session/channel stream, Tick identifiers MUST be monotonic non-decreasing. +**Statement:** All inputs delivered to the Simulation Core (DM-0014) and all outputs emitted from it MUST carry an explicit Tick (DM-0001) identifier. This identifier MUST represent the simulation tick at which the input is applied or the output is generated, not network timestamps or client-local time. For any given session/channel stream, Tick identifiers MUST be monotonic non-decreasing. *Non-normative note: This boundary contract enables replay (INV-0006) and allows future delay compensation schemes without changing the simulation interface. "Applied at tick T" means the input affects the state transition from T to T+1. Monotonicity prevents pathological replay streams.* diff --git a/docs/constitution/tag-taxonomy.md b/docs/constitution/tag-taxonomy.md index 6d2ce72..c37de1f 100644 --- a/docs/constitution/tag-taxonomy.md +++ b/docs/constitution/tag-taxonomy.md @@ -30,14 +30,16 @@ Tags are NOT identity: - authority: Server-authoritative outcomes; trust boundaries; convergence; engine-agnostic simulation law. - entity: Simulation objects (characters, projectiles, spawned gameplay objects) and their identity/lifecycle. - security: Input validation, anti-cheat foundations, trusted/untrusted boundaries. -- testability: Isolation of simulation plane to enable pure unit testing without I/O mocking. +- testability: Isolation of Simulation Core to enable pure unit testing without I/O mocking. - verification: Mechanisms for proving correctness (state hashing, replay equivalence, checkpoint comparison). ### Architecture -- architecture: Plane separation, boundary contracts, module layering. +- architecture: Component separation, boundary contracts, module layering. - traceability: Tick-indexing, provenance of state transitions, replay artifact structure. - identity: Unique identifiers for entities, sessions, and other tracked objects. +- presentation: Game Client concerns: rendering, UI, input capture, interpolation. Non-authoritative. +- infrastructure: Deployment, orchestration, matchmaking. Outside the Game Server Instance boundary. ### Netcode / protocol semantics @@ -50,7 +52,6 @@ Tags are NOT identity: - input: Client input capture, encoding, and transmission. - state-sync: Snapshot generation, packing, and client state reconciliation. - connection: Session establishment, handshake, disconnection, reconnection. -- control-plane: Control-plane services and session management (distinct from simulation state). ### Gameplay pillars diff --git a/docs/handbook.md b/docs/handbook.md index 95c2af0..ee9d76f 100644 --- a/docs/handbook.md +++ b/docs/handbook.md @@ -25,16 +25,20 @@ Content and polish come later, after the simulation contract is proven. ## Architecture overview (conceptual) -Flowstate uses a three-plane conceptual model with strict boundaries: +Flowstate uses an **Authoritative Multiplayer Architecture** with strict boundaries: -- Simulation Plane (authoritative) - Canonical game rules and state transitions. Deterministic and replay-verifiable. Engine-agnostic. +**Runtime Components:** -- Client Plane (presentation) - Rendering, input capture, UI, interpolation, and eventually prediction/reconciliation. Downstream of authoritative state. +- **Game Client** (presentation) + Rendering, input capture, UI, interpolation, and eventually prediction/reconciliation. Untrusted; downstream of authoritative state. -- Control Plane (orchestration; optional) - Match lifecycle, hosting, telemetry, and dev tooling. Must not contain game rules. +- **Game Server Instance** (authoritative match runtime) + Contains two subcomponents: + - **Server Edge** — Networking, session management, input validation, all I/O operations. + - **Simulation Core** — Deterministic game logic, physics, movement, abilities. No I/O. Replayable from recorded inputs. + +- **Matchmaker** (orchestration; optional) + Match creation, player assignment. Must not contain game rules. May not exist for LAN/dev. The binding details belong in ADRs. @@ -65,7 +69,7 @@ When changing canonical Constitution docs, run `just ids-gen` to update committe ## Determinism and testing philosophy -Determinism is foundational for the simulation plane: +Determinism is foundational for the Simulation Core: - Identical inputs and seed must produce identical outcomes. - Replay and golden tests are preferred mechanisms for correctness. - If determinism is uncertain, strengthen tests before adding features. diff --git a/docs/repo-map.md b/docs/repo-map.md index 3998c29..6a11046 100644 --- a/docs/repo-map.md +++ b/docs/repo-map.md @@ -49,7 +49,7 @@ The canonical authority / precedence definition lives in one place: - `.github/`: GitHub automation, templates, workflows, contribution scaffolding - `docs/`: governance, policies, and orientation docs -- `crates/`: Rust crates (simulation plane and shared libraries) +- `crates/`: Rust crates (Simulation Core and shared libraries) - `client/`: client implementation(s) (presentation plane; engine-facing) - `protocol/`: engine-agnostic schemas/messages diff --git a/docs/specs/FS-TBD-v0-multiplayer-slice.md b/docs/specs/FS-TBD-v0-multiplayer-slice.md new file mode 100644 index 0000000..9660071 --- /dev/null +++ b/docs/specs/FS-TBD-v0-multiplayer-slice.md @@ -0,0 +1,705 @@ +--- +status: Draft +issue: TBD +title: v0 Two-Client Multiplayer Slice +--- + +# FS-TBD: v0 Two-Client Multiplayer Slice + +> **Status:** Draft +> **Issue:** [#TBD](https://github.com/project-flowstate/flowstate/issues/TBD) +> **Author:** @copilot +> **Date:** 2025-12-21 + +> **Note:** Replace `TBD` with the GitHub issue number for AC-0001 once created. + +--- + +## Problem + +The Flowstate project requires a minimal end-to-end multiplayer implementation to validate the Authoritative Multiplayer Architecture before adding gameplay complexity. Without this slice, the team cannot verify that: + +1. The Simulation Core remains deterministic and isolated from I/O +2. The Server Edge correctly mediates between Game Clients and the Simulation Core +3. Replay verification works on the same build/platform +4. The chosen networking stack (ENet + Protobuf) integrates correctly + +This spec defines the minimal implementation: two native Game Clients connect to a Game Server Instance, move Characters with WASD, and the system produces replay artifacts that prove determinism. + +## Issue + +- Issue: [#TBD](https://github.com/project-flowstate/flowstate/issues/TBD) + +## Trace Map + +| ID | Relationship | Notes | +|----|--------------|-------| +| AC-0001 | Implements | Primary acceptance criterion for this spec | +| INV-0001 | Constrains | Deterministic simulation: identical inputs + seed + state → identical outputs | +| INV-0002 | Constrains | Fixed timestep: simulation advances in fixed-size Ticks only | +| INV-0003 | Constrains | Server authoritative: clients send intent, server decides outcomes | +| INV-0004 | Constrains | Simulation Core isolation: no I/O, networking, rendering, wall-clock | +| INV-0005 | Constrains | Tick-indexed I/O: all boundary messages carry explicit Tick | +| INV-0006 | Constrains | Replay verifiability: reproduce authoritative outcome from artifact | +| KC-0001 | Constrains | Kill criterion: any Simulation Core boundary violation | +| KC-0002 | Constrains | Kill criterion: replay cannot reproduce authoritative outcome | +| DM-0001 | Implements | Tick: atomic unit of game time | +| DM-0002 | Implements | World: playable map space containing entities | +| DM-0003 | Implements | Character: player-controlled entity | +| DM-0005 | Implements | Entity: object with unique identity and simulation state | +| DM-0006 | Implements | InputCmd: tick-indexed player input | +| DM-0007 | Implements | Snapshot: tick-indexed authoritative world state | +| DM-0008 | Implements | Session: client connection lifecycle (Server Edge owned) | +| DM-0016 | Implements | Baseline: pre-step state serialization for join/replay | +| DM-0009 | Implements | Channel: logical communication lane (Realtime, Control) | +| DM-0010 | Implements | Match: discrete game instance lifecycle | +| DM-0011 | Implements | Server Edge: networking component within Game Server Instance | +| ADR-0001 | Implements | Authoritative Multiplayer Architecture | +| ADR-0002 | Implements | Deterministic simulation requirements | +| ADR-0003 | Implements | Fixed timestep simulation | +| ADR-0004 | Implements | Server-authoritative architecture | +| ADR-0005 | Implements | v0 networking architecture (ENet, Protobuf, channels) | + +## Domain Concepts + +| Concept | ID | Notes | +|---------|-----|-------| +| Tick | DM-0001 | Atomic simulation time unit | +| World | DM-0002 | Contains entities, RNG state, tick counter | +| Character | DM-0003 | Player-controlled entity with position/velocity | +| Entity | DM-0005 | Base object with unique EntityId | +| InputCmd | DM-0006 | Tick-indexed movement intent from Game Client | +| Baseline | DM-0016 | Pre-step world state serialization at tick T (used for join/replay initialization) | +| Snapshot | DM-0007 | Post-step world state serialization at tick T+1 (after inputs applied) | +| Session | DM-0008 | Per-client connection lifecycle (Server Edge) | +| Channel | DM-0009 | Realtime (unreliable+sequenced) or Control (reliable+ordered) | +| Match | DM-0010 | Game instance lifecycle; scopes replay | +| Server Edge | DM-0011 | Owns transport; validates inputs; exchanges tick-indexed messages | +| Simulation Core | DM-0014 | Deterministic game logic; no I/O | +| Game Client | DM-0015 | Player runtime; rendering, input, UI | + +> **Note:** "Baseline" (DM-0016) and "Snapshot" (DM-0007) are both serialized world state but differ semantically and temporally: Baseline is pre-step state at tick T used for join and replay initialization; Snapshot is post-step state at tick T+1 used for ongoing synchronization. This distinction eliminates ambiguity in initial state handling. +> +> **Note:** "Replay artifact" is an implementation artifact under INV-0006, not a separate domain concept. It captures the data required for replay verifiability. + +## Interfaces + +### Simulation Core Types + +The Simulation Core (`crates/sim`) exposes the following public interface: + +```rust +/// Ref: DM-0001. Atomic simulation time unit. +pub type Tick = u64; + +/// Ref: DM-0005. Unique identifier for entities. +pub type EntityId = u64; + +/// Ref: DM-0006. Tick-indexed input from a single player. +/// InputCmd.tick indicates the tick at which this input is applied. +/// Applied at tick T means: affects the state transition T → T+1. +/// +/// **player_id boundary contract (NORMATIVE):** Simulation Core MUST treat `player_id` +/// as an indexing/ordering key only (for deterministic association of inputs to entities). +/// Simulation Core MUST NOT perform identity validation or security checks on `player_id`. +/// Server Edge is the authority for binding `player_id` to a session/connection. +pub struct InputCmd { + pub tick: Tick, + pub player_id: u8, + /// Movement direction. Magnitude MUST be <= 1.0 (clamped by Server Edge). + pub move_dir: [f64; 2], +} + +/// Ref: DM-0016. Pre-step world state at tick T (before inputs applied). +/// Used for JoinBaseline and replay initial state. +pub struct Baseline { + pub tick: Tick, + pub entities: Vec<EntitySnapshot>, + pub digest: u64, +} + +/// Ref: DM-0007. Post-step world state at tick T+1 (after inputs applied and step executed). +/// After applying inputs at tick T and stepping, Snapshot.tick = T+1. +pub struct Snapshot { + pub tick: Tick, + pub entities: Vec<EntitySnapshot>, + pub digest: u64, +} + +/// Per-entity state within a Snapshot. +pub struct EntitySnapshot { + pub entity_id: EntityId, + pub position: [f64; 2], + pub velocity: [f64; 2], +} + +/// Ref: DM-0002. Authoritative world state. +pub struct World { /* opaque to Server Edge */ } + +impl World { + /// Create a new world with the given RNG seed and tick rate. + /// - `seed`: RNG seed for deterministic randomness + /// - `tick_rate_hz`: Simulation tick rate in Hz. MUST be supplied from tunable v0 parameters by Server Edge/configuration. + /// Non-normative example: 60 Hz. + /// + /// The World computes `dt_seconds = 1.0 / tick_rate_hz` internally and uses it for all ticks. + /// This eliminates the dt_seconds footgun by making it impossible to pass varying values. + pub fn new(seed: u64, tick_rate_hz: u32) -> Self; + + /// Spawn a character for a player. Returns the assigned EntityId. + pub fn spawn_character(&mut self, player_id: u8) -> EntityId; + + /// Capture pre-step state (Baseline) at current tick. + /// Used for JoinBaseline and replay initial state. + pub fn baseline(&self) -> Baseline; + + /// Advance simulation by one tick. + /// - `inputs`: Validated InputCmds for current tick (input.tick == self.tick). + /// + /// Uses the tick_rate_hz configured at construction to compute dt_seconds internally. + /// Returns Snapshot of state after this step (Snapshot.tick = self.tick + 1). + pub fn advance(&mut self, inputs: &[InputCmd]) -> Snapshot; + + /// Current tick of the world. + pub fn tick(&self) -> Tick; + + /// Compute state digest for replay verification. + pub fn state_digest(&self) -> u64; + + /// Get the configured tick rate (Hz). + pub fn tick_rate_hz(&self) -> u32; +} +``` + +### Protocol Messages + +Per ADR-0005, messages use inline Protobuf (`prost` derive). Channel mappings: + +| Message | Channel | Direction | Purpose | +|---------|---------|-----------|---------| +| `ClientHello` | Control | C→S | Handshake initiation | +| `ServerWelcome` | Control | S→C | Handshake response with session info + current server tick + tick_rate_hz | +| `JoinBaseline` | Control | S→C | Pre-step authoritative state (Baseline, DM-0016) | +| `InputCmdProto` | Realtime | C→S | Tick-indexed movement intent | +| `SnapshotProto` | Realtime | S→C | Post-step world state (Snapshot) | +| `TimeSyncPing` | Control | C→S | Client timestamp for latency measurement (v0: basic, every 2s) | +| `TimeSyncPong` | Control | S→C | Server tick + server timestamp + echo of ping timestamp (v0: basic offset tracking only) | + +Message field definitions are implementation details. The semantic contract is: +- `ServerWelcome.server_tick`: Current server tick; client uses to initialize last_server_tick_seen for snapshot-driven input tagging +- `ServerWelcome.tick_rate_hz`: Server simulation tick rate (Hz); stored for reference (not used for wall-clock estimation in v0 snapshot-driven approach) +- `ServerWelcome.player_id`: Assigned player identifier (v0: exactly two player_ids: 0 and 1); **assignment rule:** the first connected session is assigned player_id = 0; the second connected session is assigned player_id = 1. These IDs remain stable for the Session/Match. Server binds player identity to session (see player_id handling in validation rules). +- `JoinBaseline.tick`: Pre-step tick of Baseline (state before inputs applied at that tick) +- `JoinBaseline.digest`: Baseline state digest. Clients MAY ignore digest in v0, but server/CI replay verification MUST use digest checks (baseline and final outcome) as defined in the Replay Artifact Contents section. +- `InputCmdProto.tick`: The tick at which this input is applied (affects T → T+1) +- `SnapshotProto.tick`: Post-step tick of Snapshot (state after T → T+1, tick = T+1) +- `TimeSyncPing.client_timestamp`: Client wall-clock timestamp when ping sent +- `TimeSyncPong.server_tick`: Current server tick when pong sent +- `TimeSyncPong.server_timestamp`: Server wall-clock timestamp when pong sent +- `TimeSyncPong.ping_timestamp_echo`: Echo of received ping's client_timestamp for RTT calculation +- **TimeSync purpose (debug/telemetry only):** v0 MAY implement basic client-initiated ping/pong every ~2 seconds for debug visibility and telemetry. TimeSync MUST NOT affect authoritative outcomes. Correctness relies on snapshot-driven input tagging (last_server_tick_seen + INPUT_LEAD_TICKS), not wall-clock estimation. + +## Determinism Notes + +### Simulation Core Isolation (INV-0004, KC-0001) + +The Simulation Core MUST NOT: +- Perform file I/O or network operations +- Read wall-clock time (`Instant::now()`, `SystemTime`) +- Use thread-local or ambient RNG (`thread_rng()`) +- Call OS/platform APIs +- Import crates that perform the above + +**Enforcement (v0 Guardrails):** +1. **Crate separation:** `crates/sim` is a library crate with no access to server-edge modules (enforced by Cargo dependency graph) +2. **Dependency policy check (required):** CI MUST run `cargo-deny` or equivalent scripted allowlist/denylist check to prevent disallowed crates/features in sim crate. Only allowed dependencies: `rand_chacha`, `serde`, math/container crates. Dependency changes require code review. +3. **Forbidden-API source scan (required):** CI runs a fast source scan (e.g., `grep -r` or equivalent) over `crates/sim/src/` for forbidden symbols: + - `std::time::{Instant, SystemTime}`, `Instant::now`, `SystemTime::now` + - `std::fs`, `std::net`, `std::thread::sleep` + - `rand::thread_rng`, non-seeded RNG entrypoints + - Any forbidden imports fail the build +4. **Compile-time feature boundary:** No `#[cfg(feature = "server")]` or similar escape hatches in sim crate + +*Note: These are early guardrails, not a perfect proof. Code review remains the primary enforcement mechanism.* + +### Determinism Guarantees (INV-0001) + +Given identical: +- Initial `World` state (constructed with identical `seed` and `tick_rate_hz` via `World::new(seed, tick_rate_hz)`) +- `InputCmd` sequence (ordered by tick, then player_id) +- Same build (see "Same Build Constraints" below) + +The simulation produces identical `Snapshot` sequences and identical `final_digest`. + +### Same Build Constraints (v0 Determinism Scope) + +**NORMATIVE:** For v0, determinism verification (replay, CI tests) MUST use the same build constraints: + +1. **Same binary artifact:** Replay verification in CI MUST run using the exact same produced binary artifact as the server run that generated the replay artifact. Do NOT rebuild between server run and replay verification. +2. **Fixed target triple/profile:** CI MUST use a fixed target triple (e.g., `x86_64-pc-windows-msvc`) and build profile (e.g., `release` or `dev`) for all simulation/replay verification runs. +3. **No CPU-specific flags:** Avoid CPU-specific optimization flags (e.g., `-C target-cpu=native`) that can alter floating-point behavior. Use conservative target settings for reproducibility. + +**Rationale:** v0 guarantees determinism for same-build/same-platform only (per ADR-0005). Cross-platform determinism is deferred to post-v0. + +### Numeric Representation + +- All simulation math uses `f64` to minimize drift. +- Floating-point operations occur in deterministic order (single-threaded, no parallelism in v0). +- `tick_rate_hz` is configured once at `World::new(seed, tick_rate_hz)` and cannot be changed (value MUST come from [../networking/v0-parameters.md](../networking/v0-parameters.md)). World computes `dt_seconds = 1.0 / tick_rate_hz` internally once at construction and reuses it for all ticks. This eliminates the dt_seconds footgun by making it impossible to pass varying dt values. + +### RNG Usage + +- World uses an explicit, versioned RNG algorithm (e.g., `rand_chacha::ChaCha8Rng`). +- Seed is recorded in replay artifact. +- RNG calls occur in stable order (entity iteration via `BTreeMap`). +- v0 movement does not consume RNG; plumbing exists for future features. + +### StateDigest Algorithm + +The authoritative outcome checkpoint is verified via a stable 64-bit digest: + +1. **Algorithm:** FNV-1a 64-bit (offset basis `0xcbf29ce484222325`, prime `0x100000001b3`) +2. **Purpose:** Determinism regression check, not a cryptographic guarantee; collisions are possible but acceptable risk for v0 +3. **Ordering:** Entities iterated in `EntityId` ascending order (`BTreeMap` guarantees this) +4. **Canonicalization (applied before hashing):** + - Convert `-0.0` to `+0.0` for all f64 values + - Convert any NaN to the canonical bit pattern `0x7ff8000000000000` (quiet NaN) +5. **Byte encoding:** All integers and floats as little-endian bytes +6. **Included data:** `tick` (u64), then for each entity in order: `entity_id` (u64), `position[0]` (f64), `position[1]` (f64), `velocity[0]` (f64), `velocity[1]` (f64) + +### Authoritative Outcome Checkpoint + +For v0 replay verification: +- Checkpoint tick = match end tick (fixed tick count for tests) +- `final_digest` = `state_digest()` at checkpoint tick +- Replay is valid if replayed `final_digest` matches recorded `final_digest` + +## Boundary Contracts + +### Inputs into Simulation Core + +| Aspect | Specification | +|--------|---------------| +| **Type** | `Vec<InputCmd>` | +| **Tick Semantics** | `InputCmd.tick = T` means the input is applied during the T → T+1 transition. The input affects the state that becomes `Snapshot.tick = T+1`. | +| **Validation Ownership** | Server Edge validates BEFORE delivering to Simulation Core. | +| **Delivery Contract** | Simulation Core receives zero or more `InputCmd` per tick, pre-validated, for the current tick only. | +| **Ordering** | Inputs sorted by `player_id` before application for determinism. | +| **Absence Semantics (Last-Known Intent)** | Server Edge maintains per-player "current intent". If an InputCmd for player P at tick T arrives, update P's current intent and deliver it. If no InputCmd arrives for P at tick T, deliver P's last-known intent as InputCmd for tick T. Initial intent is zero (`move_dir = [0, 0]`). This provides continuity under packet loss and remains deterministic. **NORMATIVE (Testable):** When InputCmd packets are missing for some ticks, the server MUST apply the last-known intent for those missing ticks, and the filled inputs MUST be recorded in the replay artifact (as if they were received) so replay produces the same digest. | + +### Outputs from Simulation Core + +| Aspect | Specification | +|--------|---------------| +| **Baseline** | Ref: DM-0016. Pre-step state at tick T (before inputs applied). Used for JoinBaseline and replay initial state. | +| **Snapshot** | Post-step state at tick T+1 (after inputs applied and step executed). | +| **Tick Semantics** | `Baseline.tick = T` is state before inputs at T. `Snapshot.tick = T+1` is state after applying inputs at T and advancing. | +| **Digest** | Both Baseline and Snapshot include `digest` computed via StateDigest algorithm (with canonicalization). | +| **Replay Artifact** | Produced at match end, containing: initial Baseline, seed, RNG algorithm ID, tick_rate_hz, tuning parameters, input stream, final digest, checkpoint tick. | + +### Tick Semantics Diagram + +``` +World.tick = T World.tick = T+1 + │ │ + │ ── Baseline.tick=T (pre-step state) ─────►│ + │ ── InputCmd.tick=T applied ──────────────►│ + │ (affects T → T+1 step) │ + └──────────────────────────────────────────►│ ── Snapshot.tick=T+1 produced + │ (post-step state) +``` + +## Component Responsibilities + +### Simulation Core + +**Location:** `crates/sim` (library crate, no binary) + +**Owns:** +- `World`, `Entity`, `Character` state +- `advance()` stepping logic +- `state_digest()` computation +- `Snapshot` production + +**Forbidden (KC-0001):** +- File I/O, network I/O +- Wall-clock reads +- Ambient RNG +- OS/platform calls +- Rendering, audio + +### Server Edge + +**Location:** Server binary (single binary containing both Simulation Core and Server Edge) + +**Owns:** +- ENet transport adapter (DM-0011) +- Session management (DM-0008) +- Channel abstraction (DM-0009) +- Match lifecycle (DM-0010) +- Tier-0 input validation and buffering: + - Validate inputs (tick window, magnitude, NaN/Inf, rate limit) + - Buffer at most one input per player per tick (latest-wins: later arrivals for same tick overwrite) + - Reject inputs for ticks < last_applied_tick (already processed) + - Accept inputs for current_tick and limited future window + - On each simulation step at current_tick, consume buffered input for current_tick only; future-tick inputs remain buffered +- Last-known intent tracking per player (for missing input handling) +- Baseline and Snapshot broadcasting +- Replay artifact generation (written to `replays/{match_id}.replay` in v0) +- Wall-clock tick scheduling (paces simulation at tick_rate_hz) +- TimeSync pong response (basic v0: clients ping every ~2s, server responds with pong) + +**Interface to Simulation Core:** +- Calls `World::advance()` with validated inputs +- Receives `Snapshot` and broadcasts to Game Clients +- Records inputs for replay artifact + +### Game Client + +**Location:** Godot project (`client/`) + +**Owns:** +- Input capture (WASD → InputCmdProto) +- Presentation (render entities from Snapshot) +- Network connection via ENetMultiplayerPeer +- Direct connection to Game Server Instance (no orchestration service in v0) + +**Forbidden:** +- Authoritative state mutation +- Game-rule decisions + +**v0 Connection Model:** Game Clients connect directly to a known Game Server Instance address (IP:port). No matchmaking, lobbies, or orchestration service. Match starts when two clients connect. + +**v0 Handshake Gating (NORMATIVE):** Client MUST tolerate connecting and waiting without receiving ServerWelcome until the server has both clients and starts the match. The first client to connect will wait (no simulation steps, no ServerWelcome) until the second client connects. + +## Server Tick Loop + +The server binary hosts both components. The Server Edge paces the simulation: + +``` +initialize: + // NORMATIVE: Server does NOT begin stepping until two sessions are connected. + // ServerWelcome is sent only once the match is ready to start (after second client connects). + // The first client may connect and wait; no simulation steps or ServerWelcome occur until both sessions are present. + wait for two client connections + + tick_rate_hz = load from [../networking/v0-parameters.md](../networking/v0-parameters.md) // Non-normative example: 60 Hz + world = World::new(seed, tick_rate_hz) // Initializes world.tick() = 0; computes dt_seconds = 1.0 / tick_rate_hz internally + spawn characters for both connected sessions // Deterministic spawn order: player_id 0 (first connection), then player_id 1 (second connection) + record: seed, rng_algorithm, tick_rate_hz, tuning params + + baseline = world.baseline() // Pre-step state at tick 0 (both characters spawned, no inputs applied) + replay_artifact.initial_baseline = baseline + + // Initialize per-player state + for each player: + current_intent[player_id] = InputCmd{tick: 0, player_id, move_dir: [0, 0]} + last_applied_tick[player_id] = None // Option<Tick>; None means no tick applied yet + + // Once match is ready: send ServerWelcome and JoinBaseline + send ServerWelcome (with world.tick(), tick_rate_hz, player_id) on Control channel to each client + send JoinBaseline (with baseline, DM-0016) to both Game Clients on Control channel + +loop (paced by wall-clock at tick_rate_hz from [../networking/v0-parameters.md](../networking/v0-parameters.md)): + current_tick = world.tick() + + // 1. Process incoming inputs: validate and buffer + // - Validate: tick window [current_tick, current_tick + MAX_FUTURE_TICKS], magnitude, NaN/Inf, rate limit + // - Reject if last_applied_tick[player_id] is Some(tick) and cmd.tick <= tick (already processed) + // - Buffer: input_buffer[player_id][tick] = cmd (latest-wins: overwrites duplicates) + // - NORMATIVE: Receiving an input for tick T+k MUST NOT change applied intent + // for ticks < T+k. Future-tick inputs only become active when the server + // reaches that tick and consumes the buffer for current_tick. + server_edge.receive_and_buffer_inputs() + + // 2. Build input list for current_tick using last-known intent + inputs = [] + for each player: + if input_buffer[player_id][current_tick] exists: + cmd = input_buffer[player_id][current_tick] + // NORMATIVE: When consuming buffered input, ensure cmd.tick == current_tick. + // Server MUST stamp/overwrite cmd.tick to current_tick if it differs (buffer key is authoritative). + cmd.tick = current_tick + current_intent[player_id] = cmd // Update intent ONLY when consumed at current_tick + delete input_buffer[player_id][current_tick] + else: + // Missing input: reuse last-known intent + cmd = InputCmd{tick: current_tick, player_id, move_dir: current_intent[player_id].move_dir} + inputs.append(cmd) + last_applied_tick[player_id] = Some(current_tick) + + // 3. Record inputs in replay artifact (sorted by player_id) + // NORMATIVE: Replay inputs MUST be the per-tick inputs actually APPLIED by the server + // after validation, buffering rules, and last-known-intent fill (server truth), not raw client messages. + replay_artifact.inputs.extend(sorted(inputs, by: player_id)) + + // 4. Advance simulation (pure, deterministic) + // World uses tick_rate_hz configured at construction; dt_seconds computed internally + snapshot = world.advance(sorted(inputs, by: player_id)) + + // 5. Broadcast snapshot on Realtime channel + // NORMATIVE: Server broadcasts exactly one Snapshot per simulation tick + // v0 uses full snapshots at 1/tick cadence for simplicity; bandwidth/serialization optimizations + // (delta compression, throttling) are explicitly out of scope for Tier-0/AC-0001 and belong to later tiers. + server_edge.broadcast(snapshot) + + // 6. Respond to TimeSync pings (clients initiate; server responds) + server_edge.process_time_sync_pings() // Send pongs with server_tick + timestamps + +on match end (fixed tick count for v0 tests or player disconnect): + // NORMATIVE: Server MUST only end the match (and capture checkpoint_tick) immediately AFTER + // completing a tick's world.advance(); it MUST NOT end mid-tick. + // + // Disconnect timing (NORMATIVE): If a player disconnect is detected during tick T (at any point + // before or during processing), the server MUST still complete tick T's world.advance() and then + // end the match immediately after that step. Therefore checkpoint_tick MUST equal the post-step + // tick (the world tick after applying tick T, i.e., T+1). + replay_artifact.final_digest = world.state_digest() + replay_artifact.checkpoint_tick = world.tick() + if match ended due to player disconnect: + replay_artifact.end_reason = "disconnect" // Or equivalent marker + else: + replay_artifact.end_reason = "completed" // Or equivalent marker + serialize and write replay artifact to: replays/{match_id}.replay + + // NORMATIVE (Tier-0 disconnect handling summary): + // If a player disconnects after match start, the server MUST: + // 1. Complete the current tick's world.advance() (do not abort mid-tick) + // 2. Persist a replay artifact marked as aborted/disconnect with checkpoint at post-step tick + // 3. Shut down the instance cleanly (close remaining connections, flush logs) + // v0 does not support reconnection or match continuation after disconnect. +``` + +## Tier-0 Input Validation and Buffering + +Validation and buffering occur in the Server Edge BEFORE inputs reach Simulation Core. + +**Parameter values:** MUST use values from [v0-parameters.md](../networking/v0-parameters.md). + +**Constants:** +- `MAX_FUTURE_TICKS`: Maximum number of ticks ahead a client can send inputs. **Definition:** `MAX_FUTURE_TICKS := input_tick_window_ticks` where `input_tick_window_ticks` is the tunable parameter from [../networking/v0-parameters.md](../networking/v0-parameters.md). This is the single authoritative source of truth for the future window size. +- `INPUT_LEAD_TICKS`: Client-side input lead for RTT compensation. **Definition:** Fixed v0 code constant = 1 (not sourced from v0 parameters). This value is hardcoded in v0 for simplicity; post-v0 versions may make it tunable. +- `INPUT_CMDS_PER_SECOND_MAX`: Maximum input commands per second per player. **Definition:** `INPUT_CMDS_PER_SECOND_MAX := input_rate_limit_per_sec` where `input_rate_limit_per_sec` is the tunable parameter from [../networking/v0-parameters.md](../networking/v0-parameters.md). Prevents spam/DoS. +- **Constraint:** `INPUT_LEAD_TICKS MUST be <= MAX_FUTURE_TICKS` (v0 default: 1 <= input_tick_window_ticks) + +**Window interpretation (NORMATIVE):** For v0, the tick window is interpreted as **FUTURE-ONLY acceptance**. Late inputs (`cmd.tick < current_tick`) are always dropped. The parameter `input_tick_window_ticks` defines the maximum future horizon, not a symmetric (±) window. + +**Rationale for window sizing:** Large future windows increase buffering memory and abuse surface; small windows reduce tolerance to jitter. The tunable parameter allows adjustment based on deployment environment (LAN/WAN) and observed RTT/jitter profiles. + +### Validation Rules + +| Check | Behavior on Failure | +|-------|---------------------| +| **NaN/Inf in move_dir** | **DROP + LOG**: Drop input silently, log warning for debugging. | +| **Magnitude > 1.0** | **CLAMP + LOG**: Normalize to unit length, log warning, buffer clamped input. | +| **Tick outside window** | **DROP + LOG**: Drop input, log warning. **Acceptance window:** `current_tick <= cmd.tick <= current_tick + MAX_FUTURE_TICKS` where `current_tick = world.tick()` and `MAX_FUTURE_TICKS = input_tick_window_ticks` from [../networking/v0-parameters.md](../networking/v0-parameters.md). Inputs for `cmd.tick < current_tick` (late) are dropped. Inputs for `cmd.tick > current_tick + MAX_FUTURE_TICKS` (too far future) are dropped. | +| **Player identity mismatch** | **OVERRIDE + LOG**: Server MUST bind player identity to the session/connection. Server MUST ignore/overwrite any client-provided `player_id` field and stamp the session's assigned `player_id` before validation/application. If client-provided `player_id` mismatches session, server SHOULD log a warning for debugging but MUST NOT drop input solely due to mismatch. This avoids "client bug => no movement" failure mode. | +| **Already processed tick** | **DROP + LOG**: Drop input, log warning. Reject if `last_applied_tick[player_id]` is `Some(tick)` and `cmd.tick <= tick`. (`last_applied_tick` is `Option<Tick>`; `None` means no tick applied yet.) | +| **Rate limit exceeded** | **DROP + LOG**: Drop excess inputs beyond INPUT_CMDS_PER_SECOND_MAX (where `INPUT_CMDS_PER_SECOND_MAX = input_rate_limit_per_sec` from [../networking/v0-parameters.md](../networking/v0-parameters.md)), log warning. | + +**v0 disconnect policy:** Validation failures (DROP + LOG) do not disconnect clients. Server MAY disconnect for egregious abuse (e.g., sustained rate-limit violations) but this is optional for v0. Default posture: log and drop. + +### Buffering Semantics ("Latest-Wins") + +- **Per-player per-tick buffer:** `input_buffer[player_id][tick] = InputCmd` +- **Latest-wins for duplicates (before consumption):** If multiple InputCmds arrive for the same player and tick BEFORE that tick is consumed/applied, the most recent overwrites previous (consistent with unreliable+sequenced channel semantics). Once a tick is consumed (applied), any InputCmd for that tick is rejected via the "already processed" validation rule. +- **No early application:** Inputs for future ticks (cmd.tick > current_tick) are buffered and NOT applied until current_tick advances to that tick +- **Consumption:** On each simulation step at current_tick, the server consumes ONLY `input_buffer[player_id][current_tick]` (if present) and applies it; future-tick entries remain in buffer +- **Last-known intent fallback:** If no input exists for player P at current_tick, the server reuses P's last-known intent (most recent move_dir) to construct an InputCmd for current_tick + +### Last-Known Intent Tracking + +- **Initialization:** Each player's `current_intent[player_id]` starts as `move_dir = [0, 0]` +- **Update:** When an InputCmd for player P at current_tick is consumed, update `current_intent[player_id] = cmd.move_dir` +- **Reuse:** If no InputCmd for P at current_tick, create `InputCmd{tick: current_tick, player_id: P, move_dir: current_intent[P]}` +- **Determinism:** Last-known intent reuse is deterministic and replay-stable; the replay artifact records all applied inputs (including those generated from last-known intent) + +## Game Client Input Tick Tagging + +### Snapshot-Driven Tick Tagging (v0 Primary Approach) + +**Goal:** Correctness and simplicity. Clients tag inputs based on the latest authoritative server tick observed from snapshots, not wall-clock estimation. + +**State:** +``` +last_server_tick_seen = 0 // Updated from SnapshotProto.tick or ServerWelcome.server_tick +``` + +**Initialization (on ServerWelcome):** +``` +last_server_tick_seen = ServerWelcome.server_tick +tick_rate_hz = ServerWelcome.tick_rate_hz // Stored for reference, not used for estimation +``` + +**Update (on each SnapshotProto):** +``` +last_server_tick_seen = max(last_server_tick_seen, SnapshotProto.tick) +``` + +**Input Tick Tagging:** + +**Constant:** `INPUT_LEAD_TICKS = 1` (fixed v0 code constant; not tunable) + +**Rule:** When sending input, set: +``` +InputCmdProto.tick = last_server_tick_seen + INPUT_LEAD_TICKS +``` + +**Clamping (optional but recommended):** To reduce rejections, clients SHOULD clamp the tagged tick to the server's acceptance window: +``` +InputCmdProto.tick = clamp( + last_server_tick_seen + INPUT_LEAD_TICKS, + last_server_tick_seen, + last_server_tick_seen + MAX_FUTURE_TICKS +) +``` +where `MAX_FUTURE_TICKS = input_tick_window_ticks` from [../networking/v0-parameters.md](../networking/v0-parameters.md). + +**Rejection Handling:** +- If the client observes repeated rejections for "already processed" or "too far future" (detected via explicit logs/debug counters if available), it SHOULD re-base to the latest `last_server_tick_seen` from snapshots and reapply `INPUT_LEAD_TICKS`. +- No wall-clock estimation required; snapshots provide authoritative tick information. + +**Rationale:** Lead ticks compensate for RTT and jitter, ensuring the input targets a tick the server is likely to process in the future. Snapshot-driven tagging avoids wall-clock drift and simplifies correctness. + +**Tick semantics consistency:** +- `Baseline.tick = T` is pre-step state at tick T +- `InputCmd.tick = T` means "apply this intent during step T → T+1" +- `Snapshot.tick = T+1` is post-step state after applying inputs at tick T +- Client uses latest observed server tick S from snapshots, so next input targets tick S + INPUT_LEAD_TICKS + +**First inputs:** After receiving ServerWelcome at tick T, client can immediately send inputs tagged for tick T + INPUT_LEAD_TICKS (or later), even before the first snapshot arrives. + +**Continuous input and coalescing (NORMATIVE):** Clients send inputs every frame/poll cycle; the server's buffering and latest-wins semantics handle duplicates and out-of-order arrival. **Client MUST cap InputCmd send rate to INPUT_CMDS_PER_SECOND_MAX (where `INPUT_CMDS_PER_SECOND_MAX = input_rate_limit_per_sec` from [../networking/v0-parameters.md](../networking/v0-parameters.md)).** Client SHOULD send at most one InputCmd per tick per player. If multiple input updates occur within the same tick (same tagged tick value), client SHOULD coalesce by latest-wins: the most recent input state replaces earlier queued input for that tick before sending. + +v0 does not implement client-side prediction. Game Clients render authoritative Snapshot positions directly. + +### Wall-Clock Tick Estimator (Optional, Not Required for v0) + +**Note:** The wall-clock-based tick estimator (using `elapsed_seconds * tick_rate_hz`) is optional and not required for v0 correctness. It MAY be used for smoother input UX or telemetry but MUST NOT affect authoritative outcomes. If implemented, clients SHOULD still prefer snapshot-driven tagging for input tick assignment. + +**TimeSync (Debug/Telemetry Only):** +- Clients MAY send TimeSyncPing every ~2 seconds (client_timestamp) +- Server responds with TimeSyncPong (server_tick, server_timestamp, ping_timestamp_echo) +- Clients MAY compute RTT and track offset for debug visibility +- **NORMATIVE:** TimeSync MUST NOT affect authoritative outcomes. INPUT_LEAD_TICKS and server-side buffering/validation are the correctness mechanisms. + +## Replay Artifact Contents + +Per INV-0006, the replay artifact captures all data needed for reproduction: + +- **replay_format_version:** Replay artifact schema version (u32); start at 1. Required for forward compatibility. +- **initial_baseline:** Baseline (DM-0016) at tick 0 (pre-step state before any inputs applied) +- **seed:** RNG seed used to initialize World +- **rng_algorithm:** Explicit algorithm identifier. Non-normative example: "ChaCha8Rng". +- **tick_rate_hz:** Simulation tick rate (Hz). MUST match value from v0-parameters.md used at match start. Used to construct World with `World::new(seed, tick_rate_hz)` during replay. +- **tuning_parameters:** Any parameters affecting authoritative outcomes. Non-normative examples: move_speed, acceleration. +- **entity_spawn_order:** Explicit ordered list of entity spawns (type, player_id if applicable) to ensure deterministic EntityId assignment +- **player_entity_mapping:** Map of player_id → EntityId for deterministic character assignment +- **inputs:** Chronologically ordered InputCmd stream (authoritative per-tick applied inputs after validation and last-known fallback; represents server truth, not raw client messages) +- **final_digest:** StateDigest (with canonicalization) at checkpoint tick +- **checkpoint_tick:** The tick at which final_digest was computed + +**v0 artifact location:** Replay artifacts are written to an untracked local directory for development/testing. Default path: `replays/{match_id}.replay` (relative to server working directory). Tests running in CI should use a deterministic temp location or ensure the output directory exists and is writable. The `replays/` directory is added to `.gitignore`. + +**Replay verification procedure:** +1. **Deterministic initial world construction:** Load artifact; initialize `World` via `World::new(artifact.seed, artifact.tick_rate_hz)` using identical RNG algorithm, tuning parameters, and spawn/setup order. Entity identity assignment (EntityId) MUST be deterministic and match the original match's initial conditions. This produces a pre-step world at tick 0. +2. **Baseline digest verification:** Compute `world.baseline().digest` and verify equality with `artifact.initial_baseline.digest`. If mismatch, fail immediately (initialization not deterministic). +3. **Replay execution:** Iterate ticks `t` in half-open range [0, checkpoint_tick) (exclusive upper bound): collect inputs for tick `t` from artifact, call `world.advance(inputs)`. After the loop, `world.tick()` MUST equal `checkpoint_tick`. +4. **Final digest verification:** Assert `world.tick() == checkpoint_tick`, then compute digest via `world.state_digest()`; compare to `artifact.final_digest`. +5. **Result:** Pass if digests match; fail if mismatch (KC-0002 release blocker). + +**checkpoint_tick semantics:** The `checkpoint_tick` field in the replay artifact is the world tick AFTER the last applied step (i.e., the tick value the world holds at match end). Replay execution MUST end with `world.tick() == checkpoint_tick` before final digest verification. + +**Canonical digest computation:** StateDigest algorithm includes canonicalization (-0.0 → +0.0, NaN → 0x7ff8000000000000) before hashing. + +## Gate Plan + +### Tier 0 (Must pass before merge) + +- [ ] **T0.1:** Two native clients connect via ENet and complete handshake (ClientHello → ServerWelcome with server_tick + tick_rate_hz + player_id) +- [ ] **T0.2:** JoinBaseline delivers initial Baseline (DM-0016, pre-step state); both clients display Characters +- [ ] **T0.3:** Clients tag inputs using snapshot-driven approach: InputCmdProto.tick = last_server_tick_seen + INPUT_LEAD_TICKS (fixed constant = 1); first inputs use ServerWelcome.server_tick + INPUT_LEAD_TICKS +- [ ] **T0.4:** WASD input produces movement; both clients see own + opponent movement via Snapshots +- [ ] **T0.5:** Simulation Core has no I/O dependencies: crate separation enforced, dependency policy check enforced via CI (cargo-deny or equivalent), forbidden-API source scan enforced via CI (std::time, std::fs, std::net, thread_rng); tick_rate_hz configured at `World::new(seed, tick_rate_hz)` and immutable (dt_seconds computed internally) +- [ ] **T0.6:** Tier-0 input validation and buffering enforced per [v0-parameters.md](../networking/v0-parameters.md): magnitude clamped, NaN/Inf dropped+logged, tick window `current_tick <= cmd.tick <= current_tick + MAX_FUTURE_TICKS` enforced (late/too-far-future dropped+logged), already-processed ticks dropped+logged (cmd.tick <= last_applied_tick), rate limit enforced (drop+log), latest-wins per-player per-tick buffering, last-known intent reuse for missing inputs, future-tick inputs MUST NOT affect current/past ticks, player_id bound to session (client-provided value overridden, mismatch logged) +- [ ] **T0.7:** Malformed inputs do not crash server (negative test) +- [ ] **T0.8:** TimeSync ping/pong implemented (basic v0: clients ping every ~2s, server responds with pong) — debug/telemetry only, not correctness-critical +- [ ] **T0.9:** Replay artifact generated at match end with all required fields (including end_reason), written to `replays/{match_id}.replay` (untracked local directory) +- [ ] **T0.10:** **Replay range correctness test (MUST-have):** Run N ticks (non-normative example: 100), write replay artifact with checkpoint_tick = N. Replay using half-open range [0, checkpoint_tick). Assert world.tick() == checkpoint_tick at end. Verify final_digest matches artifact.final_digest. +- [ ] **T0.11:** **Future input non-interference test (MUST-have):** Enqueue input for tick T+k where k > MAX_FUTURE_TICKS; server rejects. Enqueue input for tick T+1 (within window); verify current-tick applied inputs (tick T) remain unchanged (buffer future input without affecting current step). +- [ ] **T0.12:** **Last-known intent determinism test (MUST-have):** Introduce input gaps (drop packets for ticks T+2, T+5); server fills with last-known intent. Verify filled inputs are recorded in replay artifact. Replay artifact produces same final_digest. +- [ ] **T0.13:** **Validation matrix test (MUST-have, table-driven):** Test all Server Edge validation rules: NaN/Inf rejection, magnitude clamp (>1.0 → normalized), tick window rejection (too early, too far future), already-processed rejection (cmd.tick <= last_applied_tick), rate limit rejection (exceed INPUT_CMDS_PER_SECOND_MAX where `INPUT_CMDS_PER_SECOND_MAX = input_rate_limit_per_sec`), player_id override (client mismatch → session identity used). Assert expected behavior for each case. +- [ ] **T0.14:** **Disconnect handling test (MUST-have):** Disconnect one player during tick 50 processing. Verify server completes tick 50's advance(), then ends match, persists replay artifact with end_reason="disconnect" and checkpoint_tick=51 (post-step tick), shuts down cleanly. +- [ ] **T0.15:** `just ci` passes + +### Tier 1 (Tracked follow-up) + +- [ ] Extended replay test: 10,000+ tick match +- [ ] Client-side interpolation for smoother visuals +- [ ] Graceful disconnect handling +- [ ] Stricter input validation (Tier-1 security posture) + +### Tier 2 (Aspirational) + +- [ ] Cross-platform determinism verification +- [ ] Client-side prediction + reconciliation +- [ ] Snapshot delta compression +- [ ] WebTransport adapter + +## Acceptance Criteria + +These map directly to AC-0001 sub-criteria: + +- [ ] **AC-0001.1 (Connectivity & JoinBaseline):** Two native Game Clients connect directly to known server address (no orchestration service), complete handshake (ServerWelcome with server_tick + tick_rate_hz + assigned player_id), receive initial authoritative Baseline (DM-0016, pre-step state), remain synchronized. +- [ ] **AC-0001.2 (Gameplay Slice):** Each Game Client issues WASD movement; server processes using last-known intent for missing inputs; both see own + opponent movement via Snapshots with acceptable consistency. Clients tag inputs using snapshot-driven approach (InputCmdProto.tick = last_server_tick_seen + INPUT_LEAD_TICKS where INPUT_LEAD_TICKS = 1, fixed v0 constant). Movement intent remains continuous under packet loss (no stutter from zero-intent fallbacks); visual smoothness depends on snapshot delivery. Last-known intent filling is testable and produces deterministic replay artifacts. +- [ ] **AC-0001.3 (Boundary Integrity + Replay):** Identical outcomes for identical input+seed+state+tick_rate_hz (same build/platform per "Same Build Constraints"); verified by Tier-0 replay test with deterministic initial world construction via `World::new(seed, tick_rate_hz)`, half-open tick range [0, checkpoint_tick), `world.tick() == checkpoint_tick` assertion, and canonical digest (StateDigest with -0.0/NaN canonicalization); Simulation Core MUST NOT perform I/O, networking, rendering, or wall-clock reads. Tick configuration (`tick_rate_hz`) fixed at `World::new()` to eliminate dt_seconds footgun (dt computed internally). +- [ ] **AC-0001.4 (Tier-0 Validation):** Server enforces validation per [v0-parameters.md](../networking/v0-parameters.md); tick window `[current_tick, current_tick + MAX_FUTURE_TICKS]` enforced; already-processed ticks rejected; latest-wins buffering per-player per-tick; future-tick inputs buffered and MUST NOT affect current/past ticks (only active when server reaches that tick); player_id bound to session (client-provided value overridden); malformed inputs rejected without crashing. Disconnect triggers match end after completing current tick's advance(), replay artifact persistence with end_reason marker and post-step checkpoint_tick, and clean shutdown. +- [ ] **AC-0001.5 (Replay Artifact):** Match produces replay artifact at `replays/{match_id}.replay` (untracked local directory) with all required fields (including end_reason); reproduces authoritative outcome on same build/platform via `World::new(artifact.seed, artifact.tick_rate_hz)` and recorded last-known intent sequence. Replay verification uses same binary artifact (no rebuild) and fixed target triple/profile per "Same Build Constraints". + +## Non-Goals + +Explicitly out of scope for this spec: + +- **Client-side prediction / reconciliation:** v0 Game Clients render authoritative snapshots only. +- **Cross-platform determinism:** v0 guarantees same-build/same-platform only (ADR-0005). +- **Web clients:** v0 is native Game Clients only. +- **Matchmaking / lobbies / orchestration service:** v0 auto-starts match when two Game Clients connect directly to a known server address. No service discovery, matchmaker, or relay. +- **Collision / terrain:** v0 Characters move freely without obstacles. +- **Combat / abilities:** Beyond AC-0001 scope. +- **Snapshot delta compression:** v0 sends full snapshots. +- **`.proto` files:** v0 uses inline prost derive; formal schemas in v0.2. + +## Risks + +| Risk | Likelihood | Impact | Mitigation | +|------|------------|--------|------------| +| ENet Rust crate instability | Low | Medium | Fallback to `enet-sys` if issues arise | +| f64 cross-platform variance | Low | High | v0 scoped to same-build/same-platform | +| Protobuf schema evolution | Medium | Low | v0 uses inline prost; migrate to `.proto` in v0.2 | + +## Milestones + +| # | Milestone | AC-0001 Sub-Criterion | Deliverable | +|---|-----------|----------------------|-------------| +| 1 | **Simulation Core** | Foundation | `crates/sim`: World with `new(seed, tick_rate_hz)`, Entity, `baseline()`, `advance(inputs)` (no dt parameter), `state_digest()` with canonicalization | +| 2 | **Server Skeleton** | Foundation | Server binary with ENet, sessions, tick loop using `World::new(seed, tick_rate_hz)` | +| 3 | **Connectivity & Baseline** | AC-0001.1 | Two Game Clients connect, receive ServerWelcome (with player_id) + JoinBaseline (Baseline, DM-0016) | +| 4 | **WASD Slice** | AC-0001.2 | Movement works end-to-end; clients implement snapshot-driven tick tagging (last_server_tick_seen + INPUT_LEAD_TICKS where INPUT_LEAD_TICKS = 1, fixed constant) | +| 5 | **Validation & TimeSync** | AC-0001.4 | Tier-0 input validation and buffering (tick window, latest-wins per-player per-tick, last-known intent tracking, player_id binding, disconnect handling) + basic TimeSync ping/pong (debug/telemetry only) | +| 6 | **Replay Verification** | AC-0001.3, AC-0001.5 | Artifact generated at `replays/{match_id}.replay` (with end_reason) + replay test passes with half-open range [0, checkpoint_tick), world.tick() == checkpoint_tick assertion, canonical digest, same-build constraints | + +## Assumptions + +1. **Godot 4.x** for native Game Clients (ENetMultiplayerPeer compatible). +2. **Single server binary** hosts both Simulation Core (as library) and Server Edge. +3. **Single match auto-start:** When two Game Clients connect, match begins immediately (no lobby). +4. **Character spawn positions:** Fixed positions (implementation detail). +5. **Match end condition (v0):** Fixed tick count for tests (non-normative example: 600 ticks = 10 seconds at tick_rate_hz from v0-parameters.md). Manual stop may exist for local dev. + +## Open Questions + +| # | Question | Impact | Status | +|---|----------|--------|--------| +| 1 | What is the GitHub issue number for AC-0001? | Needed for spec filename and trace block | **Awaiting maintainer** | +| 2 | Crate organization: single `crates/game` with sim module, or separate `crates/sim` + server binary? | Project structure | Recommend: `crates/sim` library + server binary that depends on it | + From f22482208bc6dac73a6cf619ad5b3da67e8f2149 Mon Sep 17 00:00:00 2001 From: Daniel Dilly <daniel.dilly@gmail.com> Date: Sun, 21 Dec 2025 21:27:20 -0500 Subject: [PATCH 2/2] Update spec to reference issue #7 (AC-0001) --- docs/specs/FS-TBD-v0-multiplayer-slice.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/specs/FS-TBD-v0-multiplayer-slice.md b/docs/specs/FS-TBD-v0-multiplayer-slice.md index 9660071..3e7c199 100644 --- a/docs/specs/FS-TBD-v0-multiplayer-slice.md +++ b/docs/specs/FS-TBD-v0-multiplayer-slice.md @@ -1,18 +1,16 @@ --- status: Draft -issue: TBD +issue: 7 title: v0 Two-Client Multiplayer Slice --- -# FS-TBD: v0 Two-Client Multiplayer Slice +# FS-0007: v0 Two-Client Multiplayer Slice > **Status:** Draft -> **Issue:** [#TBD](https://github.com/project-flowstate/flowstate/issues/TBD) +> **Issue:** [#7](https://github.com/project-flowstate/flowstate/issues/7) > **Author:** @copilot > **Date:** 2025-12-21 -> **Note:** Replace `TBD` with the GitHub issue number for AC-0001 once created. - --- ## Problem @@ -28,7 +26,7 @@ This spec defines the minimal implementation: two native Game Clients connect to ## Issue -- Issue: [#TBD](https://github.com/project-flowstate/flowstate/issues/TBD) +- Issue: [#7](https://github.com/project-flowstate/flowstate/issues/7) ## Trace Map