diff --git a/docs/adr/0003-fixed-timestep-simulation.md b/docs/adr/0003-fixed-timestep-simulation.md index 727e5ff..ec002ad 100644 --- a/docs/adr/0003-fixed-timestep-simulation.md +++ b/docs/adr/0003-fixed-timestep-simulation.md @@ -1,4 +1,4 @@ -# ADR 0003: Fixed Timestep Simulation Model +# ADR 0003: Fixed Timestep Simulation Model (Tick-Driven) ## Status Accepted @@ -7,36 +7,43 @@ Accepted Technical ## Context -Variable delta time (frame-rate-dependent simulation) causes non-determinism: the same inputs produce different outcomes depending on how fast the frame loop runs. This makes replay verification impossible, testing unreliable, and gameplay inconsistent across hardware. +Variable delta time (frame-rate-dependent simulation) causes non-determinism and makes replay verification and testing unreliable. The Simulation Core must advance independently of frame rate and wall-clock time. -To achieve determinism (ADR-0002), the simulation must advance in predictable, fixed-size increments independent of frame rate or wall-clock time. +A per-step `dt_seconds` argument is a determinism footgun: even if intended constant, it invites accidental variation and weakens the Simulation Core boundary. ## Decision -The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discrete ticks**. Each tick represents a fixed duration of simulated time. +The Simulation Core (DM-0014) MUST advance in **fixed discrete ticks**. Tick duration is a match-level constant derived from match configuration and MUST NOT vary within a match. -**Requirements:** -- Simulation stepping function: `advance(state, inputs, dt_seconds)` where `dt_seconds` is constant for a match -- Tick rate is a **match-level constant** (declared in protocol handshake, e.g., 60 Hz → dt = 16.67ms) -- All time-based rules MUST be expressed in seconds (cooldowns, durations, velocities), not tick counts -- Game logic and physics MUST NOT depend on: - - Frame rate or frames-per-second - - Wall-clock time or variable delta time - - Rendering vsync or monitor refresh rate -- Simulation state transitions occur ONLY on tick boundaries +The Simulation Core stepping API MUST be tick-driven and MUST NOT accept per-call delta time. -**Initial supported tick rate:** 60 Hz (may expand to 30/120 Hz in future) +### Normative Requirements +- Match start configuration MUST declare `tick_rate_hz`; `dt_seconds = 1.0 / tick_rate_hz` is implied and constant for the match. +- Tick rate MUST be stored on the Simulation Core instance (e.g., World) for the match; it MUST NOT be sourced from global/process state. +- Simulation stepping MUST be `advance(tick: Tick, step_inputs: &[StepInput]) -> Snapshot` (or equivalent). The tick parameter is the explicit boundary tick per INV-0005; tick duration is implicit to the Simulation Core instance (configured at construction). +- The Simulation Core MUST assert that the provided tick matches its internal state (e.g., `tick == world.tick()`) to prevent misalignment. +- StepInput (DM-0027) is the simulation-plane input type; the Server Edge MUST convert AppliedInput (DM-0024) to StepInput before invoking `advance()`. +- State transitions MUST occur only on tick boundaries. +- Simulation logic MUST NOT depend on: + - frame rate / render cadence + - wall-clock time + - variable delta time +- Time-based gameplay rules SHOULD be authored in seconds (cooldowns, durations, speeds). Implementations MAY convert these to tick-domain constants at match initialization, provided the conversion is deterministic and derived solely from match configuration. + +### Initial Supported Tick Rate +- v0: 60 Hz (see [docs/networking/v0-parameters.md](../networking/v0-parameters.md)) +- Additional discrete tick rates (e.g., 30/120 Hz) require validation before adoption. ## Rationale **Why fixed timestep:** -- **Prerequisite for determinism:** Same inputs + same dt → same outputs (ADR-0002) +- **Prerequisite for determinism:** Same inputs + same tick rate → same outputs (ADR-0002) - **Consistent gameplay:** Movement/physics behave identically on slow and fast hardware - **Replayability:** Recorded inputs can be replayed at any speed without changing outcomes - **Networking:** Clients and server can synchronize on tick numbers -**Why dt-parameterized (not hardcoded tick count):** -- Time-based rules (e.g., "3 second cooldown") are intuitive and tunable -- Allows testing at different tick rates without rewriting logic -- Scales to variable tick rates (30/60/120 Hz) if needed in future +**Why tick-driven (no per-call dt):** +- Eliminates a major determinism footgun (accidental dt variation) +- Clarifies Simulation Core boundary (tick rate is configuration, not runtime parameter) +- "Author in seconds" still works; deterministic init-time conversion enables multi-rate support **Why match-level constant (not global):** - Different game modes may benefit from different tick rates @@ -52,6 +59,8 @@ The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discr - Constitution IDs: - INV-0002 (Fixed Timestep) — canonical definition - DM-0001 (Tick) — atomic unit of game time + - DM-0024 (AppliedInput) — Server Edge input selection + - DM-0027 (StepInput) — Simulation Core input type - Canonical Constitution docs: - [docs/constitution/invariants.md](../constitution/invariants.md) - [docs/constitution/domain-model.md](../constitution/domain-model.md) @@ -61,18 +70,19 @@ The Simulation Core (DM-0014, defined in ADR-0001) MUST advance in **fixed discr ## Alternatives Considered - **Variable delta time** — `advance(state, inputs, dt_variable)` where dt changes per frame. Rejected: Non-deterministic; same inputs produce different outcomes at different frame rates. +- **Per-call `dt_seconds` constant-in-practice** — `advance(inputs, dt_seconds)` where dt is "supposed to be" constant. Rejected: Footgun; boundary leak; invites accidental variation. - **Frame-locked simulation** — Couple simulation to rendering frame rate. Rejected: Determinism requires independence from frame rate. -- **Hardcoded tick rate (no dt parameter)** — Simulation logic uses tick counts instead of seconds. Rejected: Less intuitive; harder to tune; cannot test at different tick rates. +- **Hardcoded tick rate (no configurability)** — Single tick rate, no match-level configuration. Rejected: Less flexible for future game modes; testing at different rates is useful. - **Fully variable tick rate** — Allow tick rate to change mid-match. Rejected: Numerical integration differences cause subtle gameplay changes; validation surface area explodes. ## Implications - **Enables:** Determinism (ADR-0002), replay verification, tick-synchronized networking, consistent gameplay across hardware -- **Constrains:** Simulation cannot adapt to low frame rates; must use fixed-timestep integration techniques +- **Constrains:** Simulation cannot adapt to low frame rates; must use fixed-timestep integration techniques; tick rate is chosen at match start - **Migration costs:** None (greenfield project) -- **Contributor impact:** All simulation logic must be written in terms of `dt_seconds`, not frame count or wall-clock time +- **Contributor impact:** Contributors MUST keep wall-clock time out of the Simulation Core; any time progression is tick-indexed and configuration-derived. Replay artifacts MUST record tick configuration sufficient to reproduce the match's tick duration. ## Follow-ups -- Define simulation stepping API: `advance(state, inputs, dt_seconds)` +- Define concrete `World::new(seed, tick_rate_hz)` and `World::advance(tick, step_inputs)` API in simulation crate - Document integration patterns for movement/physics at fixed timestep - Validate behavior at 30/60/120 Hz before expanding supported tick rates - Add CI gate: verify same outcomes at same tick rate across multiple runs diff --git a/docs/adr/0005-v0-networking-architecture.md b/docs/adr/0005-v0-networking-architecture.md index ef40dfb..30d7091 100644 --- a/docs/adr/0005-v0-networking-architecture.md +++ b/docs/adr/0005-v0-networking-architecture.md @@ -1,7 +1,7 @@ # ADR 0005: v0 Networking Architecture ## Status -Proposed +Accepted ## Type Technical @@ -56,9 +56,10 @@ For v0, we use: - **60 Hz tick rate**, **unreliable snapshots** @ 60 Hz, **unreliable inputs** @ 60 Hz - See [docs/networking/v0-parameters.md](../networking/v0-parameters.md) for tunables - Justification: Simple 1:1 tick-to-snapshot-to-input mapping for v0 -- **Tier-0 input validation:** magnitude limit, tick window (±120 ticks), rate limit (120/sec) +- **Tier-0 input validation:** magnitude limit, tick window, rate limit + - Tick acceptance window is defined in [docs/networking/v0-parameters.md](../networking/v0-parameters.md) + - Tick targeting semantics (TargetTickFloor, InputSeq) are defined in ADR-0006 - Justification: Permissive for LAN/dev; prevents crashes from malformed inputs - - Values in [docs/networking/v0-parameters.md](../networking/v0-parameters.md) ### Determinism Scope diff --git a/docs/adr/0006-input-tick-targeting.md b/docs/adr/0006-input-tick-targeting.md new file mode 100644 index 0000000..abc1cf2 --- /dev/null +++ b/docs/adr/0006-input-tick-targeting.md @@ -0,0 +1,105 @@ +# ADR 0006: Input Tick Targeting & Server Tick Guidance + +## Status +Accepted + +## Type +Technical + +## Context +Clients must tag InputCmd messages with a target Tick. If clients choose target ticks solely based on the most recently observed server tick (from Welcome/Snapshots), that observation is inherently delayed by network latency. This produces late-tagged inputs that arrive after the server has already processed the targeted tick(s), causing dropped inputs and confusing startup behavior. + +Additionally, when multiple InputCmd messages target the same (PlayerId, Tick), the Server Edge must select one deterministically. Relying on packet arrival order is non-deterministic and transport-dependent. + +We need a deterministic, protocol-level mechanism that: +- makes the input-targeting rule explicit and testable, +- avoids relying on wall-clock timing inside the Simulation Core, +- tolerates reordering/duplication without "arrival order" semantics, +- and preserves replay verification by ensuring the Simulation Core consumes only StepInput (derived from AppliedInput). + +## Decision +The Server Edge MUST provide tick guidance to clients, and clients MUST use that guidance when selecting InputCmd target ticks. + +### Normative Requirements + +**Server-Guided Targeting (TargetTickFloor):** +- The Server Edge MUST emit TargetTickFloor (DM-0025) in ServerWelcome and in each Snapshot (DM-0007). +- TargetTickFloor MUST be computed as: `server.current_tick + INPUT_LEAD_TICKS` (see [docs/networking/v0-parameters.md](../networking/v0-parameters.md) for v0 value). +- TargetTickFloor MUST be monotonic non-decreasing per Session (DM-0008); resets on session re-establishment or new MatchId (DM-0021). +- Game Clients MUST target InputCmd.tick values >= TargetTickFloor (clients MUST clamp upward). +- Game Clients MUST NOT target InputCmd.tick values earlier than TargetTickFloor. + +**Deterministic Input Selection (InputSeq):** +- InputCmd MUST carry an InputSeq (DM-0026) that is monotonically increasing per Session. +- When multiple InputCmd messages are received for the same (PlayerId, Tick), the Server Edge MUST deterministically select the winning InputCmd using InputSeq (greatest wins) or an equivalent deterministic mechanism. +- InputSeq selection MUST NOT depend on packet arrival order or other runtime artifacts. + +**Input Pipeline:** +- The Server Edge MUST buffer inputs by (PlayerId, Tick) within the InputTickWindow (DM-0022). +- For each Tick T processed, the Server Edge MUST produce exactly one AppliedInput (DM-0024) per participating player, using LastKnownIntent (DM-0023) fallback when no valid input exists for T. +- The Server Edge MUST convert AppliedInput to StepInput (DM-0027) before invoking the Simulation Core. +- The Server Edge MUST invoke `advance(tick, step_inputs)` with the explicit tick T per INV-0005. +- The Simulation Core MUST assert that the provided tick matches its internal state (e.g., `tick == world.tick()`). +- The Simulation Core MUST consume only StepInput and MUST NOT consume raw InputCmd or AppliedInput directly. + +### Startup Behavior + +In v0, the first simulation step (tick 0 → 1) uses neutral LastKnownIntent for all players because clients cannot provide inputs before receiving ServerWelcome. + +**Rationale:** ServerWelcome is sent when the match starts (after all required clients connect). At that moment, `server.current_tick = 0`. ServerWelcome.TargetTickFloor = `0 + INPUT_LEAD_TICKS = 1`. Clients target tick 1 or later. The server's first `advance()` call (processing tick 0) has no client inputs, so LastKnownIntent (zero-intent) is used for all players. + +Tier-0 validation focuses on responsiveness starting with the first eligible tick (tick 1 with INPUT_LEAD_TICKS=1). + +## Rationale +**Why server-provided tick guidance:** +- Avoids stale client tick observations being treated as authoritative targeting information +- Eliminates first-input ambiguity (clients know exactly which tick to target) +- Reduces late-input drops compared to pure snapshot-driven targeting + +**Why sequence-based tie-breaker (InputSeq):** +- Makes "latest wins" deterministic and independent of packet arrival order +- On transports with guaranteed ordering (e.g., ENet sequenced channels), InputSeq is defense-in-depth +- On unordered transports (e.g., WebTransport datagrams), InputSeq becomes required + +**Why separate StepInput from AppliedInput:** +- Preserves Simulation Core boundary (INV-0004); simulation code never sees protocol-plane types +- AppliedInput is what the Server Edge selected (protocol truth); StepInput is what the sim consumes +- Prevents implementers from accidentally passing raw client messages into the sim + +## Constraints & References (no prose duplication) +- Constitution IDs: + - INV-0004 (Simulation Core Isolation) + - INV-0005 (Tick-Indexed I/O Contract) + - INV-0006 (Replay Verifiability) + - INV-0007 (Deterministic Ordering & Canonicalization) + - DM-0001 (Tick) + - DM-0006 (InputCmd) + - DM-0022 (InputTickWindow) + - DM-0023 (LastKnownIntent) + - DM-0024 (AppliedInput) + - DM-0025 (TargetTickFloor) + - DM-0026 (InputSeq) + - DM-0027 (StepInput) +- Related ADRs: + - ADR-0003 (Fixed Timestep) — defines tick-driven stepping API + - ADR-0005 (v0 Networking Architecture) — transport/channel architecture +- Parameters: + - [docs/networking/v0-parameters.md](../networking/v0-parameters.md) — INPUT_LEAD_TICKS value + +## Alternatives Considered +- **Client-only snapshot-driven targeting (no server guidance)** — Rejected: Stale observation leads to dropped inputs at startup and under RTT variation. Requires clients to guess server state. +- **Start delay / first-input barrier** — Acceptable as auxiliary mitigation, but insufficient as the long-term targeting model. Adds latency to match start. +- **RTT / clock-based tick estimation** — Viable later for prediction/reconciliation, but adds complexity. Server guidance achieves robust behavior with less coupling for v0. +- **Arrival-order tie-breaking** — Rejected: Non-deterministic; depends on transport and network conditions. Breaks replay verification. + +## Implications +- **Enables:** Deterministic input selection independent of transport; robust startup behavior; replay verification via AppliedInput +- **Constrains:** Protocol messages (Welcome/Snapshot/InputCmd) carry additional semantics (TargetTickFloor, InputSeq); clients must implement clamping +- **Migration costs:** None (greenfield protocol) +- **Contributor impact:** Contributors must understand the input pipeline (InputCmd → AppliedInput → StepInput) and respect plane boundaries + +## Follow-ups +- Update v0 multiplayer slice spec to incorporate TargetTickFloor and InputSeq semantics +- Add Tier-0 tests validating deterministic selection under duplication/reordering +- Add Tier-0 tests validating "first movement" occurs without prolonged input drops +- Document input pipeline in contributor handbook diff --git a/docs/adr/0007-state-digest-algorithm-canonical-serialization.md b/docs/adr/0007-state-digest-algorithm-canonical-serialization.md new file mode 100644 index 0000000..63f191e --- /dev/null +++ b/docs/adr/0007-state-digest-algorithm-canonical-serialization.md @@ -0,0 +1,101 @@ +# ADR 0007: StateDigest Algorithm (v0) + +## Status +Accepted + +## Type +Technical + +## Context +Replay verification (INV-0006) requires at least one deterministic verification anchor that can be recomputed during replay and compared against the recorded value. The Domain Model defines StateDigest (DM-0018) as a digest over a canonical serialization of authoritative World (DM-0002) state at a specific Tick (DM-0001), and requires a digest algorithm identifier so the procedure is unambiguous and versionable. + +Today, the v0 spec contains a concrete digest algorithm definition. Keeping the concrete algorithm in a spec creates drift risk: future specs may restate (or subtly change) the algorithm, breaking replay verification and eroding determinism guarantees. This ADR centralizes the decision, provides an explicit algorithm identifier, and defines the versioning/change policy. + +StateDigest is a regression/verification mechanism, not a security primitive; cryptographic collision resistance is not required for v0. + +## Decision +Define **StateDigest v0** as a deterministic 64-bit digest computed by the Simulation Core over a canonical byte representation of World state at a specific Tick. + +### Algorithm Identifier +The ReplayArtifact (DM-0017) MUST record a `state_digest_algo_id` identifying the exact algorithm/canonicalization used. + +For v0, the required value is: + +- `state_digest_algo_id = "statedigest-v0-fnv1a64-le-f64canon-eidasc-posvel"` + +Any change to the procedure that could alter outputs MUST mint a new identifier (see “Change Policy”). + +### Digest Algorithm (v0) +- **Hash:** FNV-1a 64-bit + - offset basis: `0xcbf29ce484222325` + - prime: `0x100000001b3` +- **Purpose:** determinism regression check only; collisions are an accepted risk for v0. + +### Canonicalization Rules (v0) +Applied prior to hashing (ref: INV-0007): +- For all `f64` values: + - `-0.0` MUST be canonicalized to `+0.0` + - Any NaN MUST be canonicalized to the quiet NaN bit pattern `0x7ff8000000000000` + +### Byte Encoding (v0) +- All integers and floats MUST be encoded as **little-endian** bytes. +- `f64` values MUST be encoded by their canonicalized IEEE-754 bit pattern. + +### Included Data & Ordering (v0) +StateDigest(v0) hashes the following sequence of bytes: + +1) `tick` as `u64` (little-endian) + +2) For each entity, iterated in **EntityId (DM-0020) ascending order** (ref: INV-0007): + - `entity_id` as `u64` (little-endian) + - `position[0]` as `f64` (canonicalized, little-endian) + - `position[1]` as `f64` (canonicalized, little-endian) + - `velocity[0]` as `f64` (canonicalized, little-endian) + - `velocity[1]` as `f64` (canonicalized, little-endian) + +### Ownership +- The Simulation Core (DM-0014) MUST provide the canonical StateDigest computation. +- The Server Edge (DM-0011) MUST treat StateDigest as an opaque value and record it as part of ReplayArtifact verification anchors (DM-0017, INV-0006). + +## Rationale +- **Single source of truth:** A concrete algorithm belongs in an ADR so specs can reference it without duplication. +- **Determinism and traceability:** Explicit canonicalization and deterministic ordering are required to make digest comparison meaningful (INV-0007). +- **Versionability:** An explicit `state_digest_algo_id` prevents ambiguity and enables controlled evolution without breaking old replays. +- **Simplicity for v0:** FNV-1a 64-bit is easy to implement, fast, and adequate as a regression anchor under v0 scope. + +## Constraints & References (no prose duplication) +- Constitution IDs: + - INV-0006 (Replay Verifiability) + - INV-0007 (Deterministic Ordering & Canonicalization) + - DM-0017 (ReplayArtifact) + - DM-0018 (StateDigest) + - DM-0020 (EntityId) + - DM-0001 (Tick) + - DM-0002 (World) +- Related ADRs: + - ADR-0002 (Deterministic Simulation) + - ADR-0003 (Fixed Timestep Simulation Model) + +## Alternatives Considered +- **Cryptographic hash (e.g., SHA-256, BLAKE3):** stronger collision resistance than needed; higher implementation/CPU cost; not required for v0 regression anchoring. +- **Non-cryptographic hashes (xxHash, Murmur, etc.):** viable; FNV-1a chosen for simplicity and ease of auditing. If upgraded later, it MUST be done via a new `state_digest_algo_id`. +- **Digest computed outside Simulation Core:** rejected; increases drift risk and violates the principle that the Simulation Core owns canonicalization/verification semantics for authoritative state. + +## Implications +- Replay artifacts MUST include `state_digest_algo_id` and the digest value(s) at declared checkpoint ticks. +- Any change to: + - included fields, + - ordering, + - float canonicalization, + - byte layout/endian, + - or hash function parameters + MUST mint a new `state_digest_algo_id`. +- v0 digest scope remains “same build/same platform” per INV-0006. Cross-platform determinism is deferred. + +## Change Policy +- Changing the StateDigest procedure is a **compatibility event**. +- A change MUST be accompanied by: + - a new `state_digest_algo_id`, + - an ADR update (this ADR) documenting the new algorithm (or a superseding ADR), + - and a replay/versioning strategy (e.g., replay format version bump if required). +- Replay verification MUST select the digest procedure based on `state_digest_algo_id` recorded in the ReplayArtifact. diff --git a/docs/constitution/acceptance-kill.md b/docs/constitution/acceptance-kill.md index c43578a..45dc74a 100644 --- a/docs/constitution/acceptance-kill.md +++ b/docs/constitution/acceptance-kill.md @@ -31,8 +31,8 @@ ENTRY FORMAT (use H3 with anchor): 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). +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)); input handling per ADR-0006; 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). "Input stream" means the AppliedInput (DM-0024) stream—the per-tick inputs that were actually applied by the authoritative simulation, not raw InputCmd (DM-0006) traffic. *Non-normative note: "Acceptable consistency" means visual correctness for WASD movement in a LAN/dev environment without requiring tick-perfect lockstep rendering (client-side prediction is future work). Specific validation thresholds documented in [docs/networking/v0-parameters.md](../networking/v0-parameters.md). This is a composite criterion because all five sub-criteria are interdependent and must ship together to constitute a viable v0 milestone.* diff --git a/docs/constitution/domain-model.md b/docs/constitution/domain-model.md index 2e1a39d..703bcad 100644 --- a/docs/constitution/domain-model.md +++ b/docs/constitution/domain-model.md @@ -26,7 +26,9 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** simulation -**Definition:** The playable map space—terrain, obstacles, surfaces, and traversal features—that shapes movement and combat. +**Definition:** The authoritative simulation state container, encompassing both static environment (terrain, obstacles, surfaces, traversal features, hazard volumes) and dynamic simulation state (Entities and their evolving state). The Simulation Core (DM-0014) maintains World state and advances it each Tick (DM-0001). + +*Non-normative note: World is the complete simulation context. Static environment defines affordances and constraints; dynamic state includes player Characters, projectiles, timers, triggers, and any other state that evolves during gameplay.* ### DM-0003 — Character **Status:** Active @@ -34,7 +36,7 @@ Section groupings (H2) are optional, for organization only. **Definition:** The entity a player controls; has position, state, and can perform actions. The primary interactive agent in the simulation. -### DM-0004 — Locomotion Mode +### DM-0004 — LocomotionMode **Status:** Active **Tags:** movement @@ -44,7 +46,7 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** simulation, architecture, identity -**Definition:** An object in the simulation with a unique identity and simulation state (often including position). The base unit of dynamic game objects. A Character (DM-0003) is a kind of Entity. A World (DM-0002) contains Entities. +**Definition:** An object in the simulation with a unique identity and simulation state (often including position). The base unit of dynamic game objects. Each Entity has a unique EntityId (DM-0020) for its lifetime within the Match (DM-0010). A Character (DM-0003) is a kind of Entity. A World (DM-0002) contains Entities. *Non-normative note: Entities include players, projectiles, obstacles, pickups, timers, triggers, etc. The Entity abstraction provides the common interface for identity, state management, and lifecycle. Not all entities are spatial (e.g., match timers).* @@ -52,9 +54,9 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** networking, input, protocol -**Definition:** A tick-indexed message containing one player's inputs (movement direction, aim, actions) for a specific Tick (DM-0001). One InputCmd per player per tick. +**Definition:** A tick-indexed message containing one player's input intent (movement direction, aim, actions) for a specific Tick (DM-0001). An InputCmd represents a per-player intent sample tagged for application at a specific tick. The Server Edge (DM-0011) may receive multiple InputCmd messages for the same (PlayerId, Tick) and MUST deterministically resolve them (e.g., via InputSeq (DM-0026)) to produce exactly one AppliedInput (DM-0024). The Server Edge then converts AppliedInput into StepInput (DM-0027), and the Simulation Core (DM-0014) consumes StepInput during the Tick step. -*Non-normative note: We chose "InputCmd" over "InputFrame" because it's per-player, not per-tick-aggregate. An InputFrame would mean "all players' inputs for tick T," which is a server-side collection concern, not a protocol primitive.* +*Non-normative note: We chose "InputCmd" over "InputFrame" because it's per-player, not per-tick-aggregate. An InputFrame would mean "all players' inputs for tick T," which is a server-side collection concern, not a protocol primitive. Clients may send multiple InputCmds for the same tick due to network conditions; the Server Edge uses a deterministic selection mechanism (e.g., InputSeq greatest-wins) to produce exactly one AppliedInput per player per tick.* ### DM-0007 — Snapshot **Status:** Active @@ -76,13 +78,13 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** networking, transport, protocol -**Definition:** A logical communication lane with defined delivery semantics (reliability, ordering, sequencing), independent of transport implementation. Examples: Realtime (unreliable + sequenced), Control (reliable + ordered), Bulk (reliable + non-blocking). +**Definition:** A logical communication lane with defined delivery semantics (reliability, ordering, sequencing), independent of transport implementation. Examples: Realtime (unreliable + sequenced), Control (reliable + ordered), Bulk (reliable + independent, does not block realtime channels). -*Non-normative note: ENet channels, WebTransport streams/datagrams, and future transports all map to this semantic model. The channel abstraction is transport-agnostic.* +*Non-normative note: ENet channels, WebTransport streams/datagrams, and future transports all map to this semantic model. The channel abstraction is transport-agnostic. "Independent" means separate lanes; Bulk traffic does not cause head-of-line blocking for Realtime/Control.* ### DM-0010 — Match -**Status:** Proposed -**Tags:** orchestration, replay +**Status:** Active +**Tags:** orchestration, replay, simulation **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. @@ -118,7 +120,11 @@ Section groupings (H2) are optional, for organization only. **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.* +**Normative constraints:** +- The Simulation Core MUST consume only simulation-plane input types (StepInput, DM-0027), not protocol-plane types (InputCmd, DM-0006). +- The Server Edge (DM-0011) MUST convert AppliedInput (DM-0024) to StepInput before invoking the Simulation Core. + +*Non-normative note: The Simulation Core advances in discrete Ticks (DM-0001). It consumes validated, tick-indexed StepInput (DM-0027) values supplied by 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 @@ -136,6 +142,169 @@ Section groupings (H2) are optional, for organization only. *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.”* +### DM-0017 — ReplayArtifact +**Status:** Active +**Tags:** replay, schema, traceability + +**Definition:** A versioned, self-describing record of an authoritative Match that is sufficient to **reproduce and verify** the authoritative outcome under the replay scope defined by INV-0006. A ReplayArtifact is produced by the Server Edge and treated as the canonical input to the replay verifier. + +A ReplayArtifact MUST include enough information to deterministically re-simulate the same authoritative timeline, including at minimum: +- **format_version** (and, if applicable, a compatibility/profile identifier for the replay scope), +- **initialization data** sufficient to reconstruct the authoritative starting state used for replay (e.g., an initial Baseline (DM-0016) or equivalent canonical state seed + deterministic initialization parameters). This MUST include enough information to reproduce any determinism-critical identity allocation performed during initialization (e.g., deterministic spawn/setup order and any required participant→entity association needed to reproduce EntityId (DM-0020) assignment). +- **determinism-relevant configuration** required to interpret and reproduce the run (e.g., tick rate; any tuning parameters that affect simulation outcomes; and identifiers for any algorithms whose choice affects outcomes), +- a chronologically ordered stream of **AppliedInput (DM-0024)** values: the per-tick inputs that were actually applied by the authoritative simulation (server truth), not raw client messages, +- one or more verification anchors: **checkpoint_tick** and associated **StateDigest**(s) (DM-0018) as required by the replay procedure (e.g., checkpoint digest and/or final digest), +- **end_reason** (the authoritative termination cause for the recorded segment). + +A ReplayArtifact is distinct from Baseline (DM-0016) and Snapshot (DM-0007): Baseline/Snapshot are world-state serializations used for join sync or post-step observation, while ReplayArtifact is an authoritative "reproduction + verification" package for determinism auditing. + +*Non-normative note: ReplayArtifact proves determinism in practice by capturing "what the server actually applied." This includes any server-side fill rules (e.g., LastKnownIntent (DM-0023)) and validation effects; replay should reproduce the same StateDigest at the declared anchors under the stated replay scope (v0: same build/platform per INV-0006).* + +### DM-0018 — StateDigest +**Status:** Active +**Tags:** verification, determinism, replay + +**Definition:** A deterministic digest value computed from a **canonical serialization** of authoritative World (DM-0002) state at a specific Tick (DM-0001). StateDigest is used as a verification anchor for replay (INV-0006): when re-simulating from the same starting state and AppliedInput (DM-0024) stream, the computed StateDigest at the declared tick(s) MUST match the digest recorded in the ReplayArtifact (DM-0017) under the intended replay scope. + +A StateDigest is defined by: +- the **tick** at which it is computed, +- a **canonicalization rule** for state-to-bytes (so equivalent states serialize identically), +- and a **digest algorithm identifier** (so the digest computation is unambiguous and versionable). + +The Simulation Core (DM-0014) provides the canonical StateDigest computation for the project. + +*Non-normative note: StateDigest is a regression/verification mechanism, not a security primitive; it need not be cryptographically collision-resistant. Specific algorithm choices (e.g., bit-width, hash function, float canonicalization, iteration ordering) are owned by specs/ADRs so they can evolve intentionally while preserving the concept of "StateDigest."* + +### DM-0019 — PlayerId +**Status:** Active +**Tags:** identity, determinism, authority + +**Definition:** A stable, per-Match participant identifier used to **deterministically order and attribute inputs**, and to associate a participant with their controlled entity(ies). PlayerId is assigned and owned by the Server Edge (DM-0011) as part of Session (DM-0008) management and is unique within a Match (DM-0010). + +PlayerId is simulation-facing identity, not authentication: +- The Simulation Core (DM-0014) MUST treat PlayerId as an indexing/ordering key only (e.g., input attribution, deterministic input ordering, deterministic player↔entity association). +- The Simulation Core MUST NOT perform identity validation, authentication, or security checks based on PlayerId. +- Binding PlayerId ↔ Session/connection identity is exclusively the Server Edge’s responsibility. + +Client-provided identity is not trusted: +- If a client supplies a PlayerId (explicitly or implicitly), the Server Edge MUST ignore/overwrite it with the server-assigned PlayerId before the input reaches the Simulation Core. + +*Non-normative note: PlayerId is intentionally separate from Session identity, which is transport/security-facing and may change due to reconnects or connection churn.* + +### DM-0020 — EntityId +**Status:** Active +**Tags:** identity, entity, determinism + +**Definition:** A unique identifier for an Entity (DM-0005) within a Match (DM-0010), assigned by the Simulation Core (DM-0014). EntityId uniquely identifies an entity for its lifetime within the match and is used for stable reference in state serialization (e.g., Snapshots (DM-0007), Baselines (DM-0016), ReplayArtifact (DM-0017)). + +EntityId is determinism-relevant: +- Any allocation/assignment strategy for EntityId MUST be deterministic under identical initial state, inputs, and parameters. +- When canonical ordering of entities is required (e.g., StateDigest (DM-0018) computation), EntityId provides a stable ordering key. + +*Non-normative note: EntityId is scoped to a single match. It may be reused across different matches, but within a ReplayArtifact (DM-0017) the match scope is established, so EntityId references are unambiguous for reproduction and verification.* +*Non-normative note: Common deterministic allocation strategies include sequential allocation (counter-based) or derivation from deterministic spawn events. The strategy is a spec/implementation detail; the requirement is deterministic reproducibility under replay.* + +### DM-0021 — MatchId +**Status:** Active +**Tags:** identity, traceability, orchestration + +**Definition:** A stable identifier assigned by the Server Edge (DM-0011) at Match (DM-0010) creation that uniquely identifies the Match within the Server Edge's operational scope for the retention period of match-scoped artifacts. MatchId is used as the primary correlation key to associate and retrieve match-scoped records (e.g., replay artifact storage paths, logs, telemetry) and to prevent cross-match identifier collisions in storage and diagnostics. + +**Normative constraints:** +- MatchId MUST be assigned and owned by the Server Edge. +- MatchId MUST remain stable for the full Match lifecycle. +- MatchId MUST be collision-resistant for concurrently active matches within the operational scope. +- MatchId MUST NOT be treated as a Simulation Core (DM-0014) input and MUST NOT influence authoritative outcomes. + +*Non-normative note: In v0, replay artifacts are persisted under a MatchId-keyed path (e.g., `replays/{match_id}.replay`). MatchId exists to provide a durable match-scope handle; it is distinct from Session (DM-0008) and PlayerId (DM-0019). "Operational scope" refers to the deployment/infrastructure context in which the Server Edge operates (e.g., a single server process, a server pool, or a datacenter region).* + +### DM-0022 — InputTickWindow +**Status:** Active +**Tags:** security, input + +**Definition:** A server-defined, tick-indexed acceptance window that determines which InputCmd (DM-0006) ticks are eligible to be accepted (buffered/coalesced) by the Server Edge (DM-0011) at a given authoritative Tick (DM-0001). Inputs outside this window are rejected or clamped per the governing spec. + +**Normative constraints:** +- The Server Edge MUST define InputTickWindow relative to its authoritative current tick (e.g., `[current_tick, current_tick + MAX_FUTURE_TICKS]`), and MUST apply it consistently for validation. +- InputTickWindow MUST be expressed purely in terms of tick indices (not wall-clock time) to preserve determinism and replay verifiability. +- The Simulation Core MUST NOT depend on InputTickWindow; it only consumes the applied, per-tick inputs selected by the Server Edge. + +*Non-normative note: InputTickWindow provides a stable term for policies like “max future ticks,” client tick clamping guidance, and input acceptance tests without freezing specific constants in the Constitution.* + +### DM-0023 — LastKnownIntent (LKI) +**Status:** Active +**Tags:** input, determinism, networking + +**Definition:** A deterministic Server Edge (DM-0011) fallback concept representing the most recently accepted per-player intent (or intent components) available as of a given Tick (DM-0001). When an InputCmd (DM-0006) for player P at tick T is not available to be applied at the tick boundary, the Server Edge MAY derive the applied per-tick input for P at T using LastKnownIntent, according to the governing spec-level policy. + +**Normative constraints:** +- Any use of LastKnownIntent MUST be deterministic and tick-indexed (no wall-clock dependence). +- LastKnownIntent derivation and application MUST be owned by the Server Edge; the Simulation Core (DM-0014) consumes only the applied per-tick inputs it is given. +- Replay verifiability MUST be preserved: the AppliedInput (DM-0024) stream (including any inputs derived via LastKnownIntent) MUST be reproducible from the ReplayArtifact (DM-0017), either by recording the AppliedInput values directly or by recording sufficient rule/version information and state to reconstruct them exactly. + +*Non-normative note: LastKnownIntent names the stable concept “server-side deterministic fallback for missing per-tick input.” The specific behavior (e.g., hold-last vs. neutral intent, decay rules, per-component handling, interaction with prediction) is spec-level policy and may evolve post-v0.* + +### DM-0024 — AppliedInput +**Status:** Active +**Tags:** protocol, determinism, replay + +**Definition:** A tick-indexed, per-player input value selected and/or derived by the Server Edge (DM-0011) for application to the Simulation Core (DM-0014) during the Tick (DM-0001) transition T → T+1. AppliedInput is the canonical "input truth" recorded for replay/verification purposes and converted to StepInput (DM-0027) for Simulation Core consumption. + +**Normative constraints:** +- For each participating PlayerId (DM-0019) and each Tick T processed by the Server Edge, exactly one AppliedInput value MUST be produced for application at Tick T (including the "no input received" case, which uses LastKnownIntent (DM-0023) fallback). +- AppliedInput MUST be derived deterministically from: + - the set of received InputCmd (DM-0006) values targeting Tick T (if any), + - the server's validation/normalization rules, and + - LastKnownIntent (DM-0023) fallback when no valid input is available for Tick T. +- If multiple InputCmd values exist for the same (PlayerId, Tick T), the selection of the winning InputCmd MUST be deterministic and MUST NOT depend on runtime artifacts such as packet arrival order. Acceptable deterministic tie-breakers include InputSeq (DM-0026) or equivalent mechanisms. +- AppliedInput MUST NOT be raw client messages; it is the post-normalization result produced by the Server Edge. +- AppliedInput MUST be the only input stream recorded into ReplayArtifact (DM-0017) for verification (INV-0006). +- The Server Edge MUST convert AppliedInput to StepInput (DM-0027) before invoking the Simulation Core. + +### DM-0025 — TargetTickFloor +**Status:** Active +**Tags:** protocol, networking, state-sync + +**Definition:** A tick index emitted by the Server Edge (DM-0011) that indicates the earliest Tick (DM-0001) a Game Client (DM-0015) MUST target when tagging new InputCmd (DM-0006) messages. + +TargetTickFloor is a protocol-level floor that reduces late-input drops and eliminates startup ambiguity. Clients MUST clamp InputCmd.tick upward to at least TargetTickFloor. + +**Normative constraints:** +- TargetTickFloor MUST be derived solely from server-observable state and configuration (e.g., current server tick, InputTickWindow (DM-0022), configured lead/buffer policy). +- TargetTickFloor MUST be included in ServerWelcome and in each Snapshot (DM-0007). +- TargetTickFloor MUST be monotonic non-decreasing per Session (DM-0008); resets on session re-establishment or new MatchId (DM-0021). +- Game Clients MUST NOT target InputCmd.tick values earlier than TargetTickFloor. + +*Non-normative note: TargetTickFloor is server-guided input targeting. It avoids stale client tick observations being treated as authoritative targeting information. The floor advances with the server tick; clients always target at or above it.* + +### DM-0026 — InputSeq +**Status:** Active +**Tags:** protocol, determinism, networking + +**Definition:** A per-session, strictly monotonically increasing `u64` sequence number attached to InputCmd (DM-0006) that allows the Server Edge (DM-0011) to deterministically select among multiple InputCmd values that target the same (PlayerId (DM-0019), Tick (DM-0001)). + +**Normative constraints:** +- InputSeq MUST be a `u64` value. +- InputSeq MUST be scoped to a Session (DM-0008). It resets when a session is re-established. +- InputSeq MUST be strictly increasing (no duplicates) for each player session across transmitted InputCmd messages. +- When multiple InputCmd messages are received for the same (PlayerId, Tick), the Server Edge MUST select the InputCmd with the greatest InputSeq value. InputSeq ties are impossible if clients behave correctly; if a malformed client sends duplicate InputSeq values, the Server Edge MAY drop duplicates or use any deterministic tie-breaker (e.g., first-processed wins). +- InputSeq MUST NOT be interpreted by the Simulation Core (DM-0014); it is used only for Server Edge selection/derivation of AppliedInput (DM-0024). + +*Non-normative note: InputSeq makes "latest wins" deterministic and independent of packet arrival order. On transports with guaranteed ordering (e.g., ENet sequenced channels), InputSeq is defense-in-depth. On unordered transports (e.g., WebTransport datagrams), InputSeq becomes required for deterministic selection. The u64 range (2^64 values) is effectively unlimited for practical session durations.* + +### DM-0027 — StepInput +**Status:** Active +**Tags:** simulation, determinism + +**Definition:** A simulation-plane, per-player input value consumed by the Simulation Core (DM-0014) during a single tick step. StepInput is the Simulation Core's interface type for inputs; it is structurally equivalent to the input-relevant fields of AppliedInput (DM-0024) but exists in the simulation plane, not the protocol plane. + +**Normative constraints:** +- StepInput MUST be the only input type consumed by Simulation Core stepping functions. +- StepInput MUST NOT carry protocol-plane metadata (session info, sequence numbers, network timestamps). +- StepInput MUST carry: PlayerId (DM-0019) for deterministic ordering/attribution, and the input payload (e.g., movement direction). +- The Server Edge (DM-0011) MUST convert AppliedInput to StepInput before invoking the Simulation Core. + +*Non-normative note: The separation between AppliedInput (protocol plane) and StepInput (simulation plane) preserves the Simulation Core boundary (INV-0004). AppliedInput is what the Server Edge selected; StepInput is what the Simulation Core consumes. They are structurally similar but semantically distinct.* ## Domain Model Change Policy diff --git a/docs/constitution/id-catalog.json b/docs/constitution/id-catalog.json index c5b8c97..6b7fdd1 100644 --- a/docs/constitution/id-catalog.json +++ b/docs/constitution/id-catalog.json @@ -63,7 +63,7 @@ "id": "DM-0004", "prefix": "DM", "number": 4, - "title": "Locomotion Mode", + "title": "LocomotionMode", "status": "Active", "tags": [ "movement" @@ -157,10 +157,11 @@ "prefix": "DM", "number": 10, "title": "Match", - "status": "Proposed", + "status": "Active", "tags": [ "orchestration", - "replay" + "replay", + "simulation" ], "file": "docs/constitution/domain-model.md", "anchor": "DM-0010", @@ -261,6 +262,180 @@ "href": "docs/constitution/domain-model.md#DM-0016", "section": "Core Simulation" }, + { + "id": "DM-0017", + "prefix": "DM", + "number": 17, + "title": "ReplayArtifact", + "status": "Active", + "tags": [ + "replay", + "schema", + "traceability" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0017", + "href": "docs/constitution/domain-model.md#DM-0017", + "section": "Core Simulation" + }, + { + "id": "DM-0018", + "prefix": "DM", + "number": 18, + "title": "StateDigest", + "status": "Active", + "tags": [ + "verification", + "determinism", + "replay" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0018", + "href": "docs/constitution/domain-model.md#DM-0018", + "section": "Core Simulation" + }, + { + "id": "DM-0019", + "prefix": "DM", + "number": 19, + "title": "PlayerId", + "status": "Active", + "tags": [ + "identity", + "determinism", + "authority" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0019", + "href": "docs/constitution/domain-model.md#DM-0019", + "section": "Core Simulation" + }, + { + "id": "DM-0020", + "prefix": "DM", + "number": 20, + "title": "EntityId", + "status": "Active", + "tags": [ + "identity", + "entity", + "determinism" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0020", + "href": "docs/constitution/domain-model.md#DM-0020", + "section": "Core Simulation" + }, + { + "id": "DM-0021", + "prefix": "DM", + "number": 21, + "title": "MatchId", + "status": "Active", + "tags": [ + "identity", + "traceability", + "orchestration" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0021", + "href": "docs/constitution/domain-model.md#DM-0021", + "section": "Core Simulation" + }, + { + "id": "DM-0022", + "prefix": "DM", + "number": 22, + "title": "InputTickWindow", + "status": "Active", + "tags": [ + "security", + "input" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0022", + "href": "docs/constitution/domain-model.md#DM-0022", + "section": "Core Simulation" + }, + { + "id": "DM-0023", + "prefix": "DM", + "number": 23, + "title": "LastKnownIntent (LKI)", + "status": "Active", + "tags": [ + "input", + "determinism", + "networking" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0023", + "href": "docs/constitution/domain-model.md#DM-0023", + "section": "Core Simulation" + }, + { + "id": "DM-0024", + "prefix": "DM", + "number": 24, + "title": "AppliedInput", + "status": "Active", + "tags": [ + "protocol", + "determinism", + "replay" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0024", + "href": "docs/constitution/domain-model.md#DM-0024", + "section": "Core Simulation" + }, + { + "id": "DM-0025", + "prefix": "DM", + "number": 25, + "title": "TargetTickFloor", + "status": "Active", + "tags": [ + "protocol", + "networking", + "state-sync" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0025", + "href": "docs/constitution/domain-model.md#DM-0025", + "section": "Core Simulation" + }, + { + "id": "DM-0026", + "prefix": "DM", + "number": 26, + "title": "InputSeq", + "status": "Active", + "tags": [ + "protocol", + "determinism", + "networking" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0026", + "href": "docs/constitution/domain-model.md#DM-0026", + "section": "Core Simulation" + }, + { + "id": "DM-0027", + "prefix": "DM", + "number": 27, + "title": "StepInput", + "status": "Active", + "tags": [ + "simulation", + "determinism" + ], + "file": "docs/constitution/domain-model.md", + "anchor": "DM-0027", + "href": "docs/constitution/domain-model.md#DM-0027", + "section": "Core Simulation" + }, { "id": "INV-0001", "prefix": "INV", @@ -357,6 +532,22 @@ "href": "docs/constitution/invariants.md#INV-0006", "section": "Simulation Correctness" }, + { + "id": "INV-0007", + "prefix": "INV", + "number": 7, + "title": "Deterministic Ordering & Canonicalization", + "status": "Active", + "tags": [ + "determinism", + "verification", + "traceability" + ], + "file": "docs/constitution/invariants.md", + "anchor": "INV-0007", + "href": "docs/constitution/invariants.md#INV-0007", + "section": "Simulation Correctness" + }, { "id": "KC-0001", "prefix": "KC", @@ -429,7 +620,7 @@ "id": "ADR-0003", "prefix": "ADR", "number": 3, - "title": "ADR 0003: Fixed Timestep Simulation Model", + "title": "ADR 0003: Fixed Timestep Simulation Model (Tick-Driven)", "status": "Unknown", "tags": [], "file": "docs/adr/0003-fixed-timestep-simulation.md", @@ -460,5 +651,29 @@ "anchor": "", "href": "docs/adr/0005-v0-networking-architecture.md", "section": "" + }, + { + "id": "ADR-0006", + "prefix": "ADR", + "number": 6, + "title": "ADR 0006: Input Tick Targeting & Server Tick Guidance", + "status": "Unknown", + "tags": [], + "file": "docs/adr/0006-input-tick-targeting.md", + "anchor": "", + "href": "docs/adr/0006-input-tick-targeting.md", + "section": "" + }, + { + "id": "ADR-0007", + "prefix": "ADR", + "number": 7, + "title": "ADR 0007: StateDigest Algorithm (v0)", + "status": "Unknown", + "tags": [], + "file": "docs/adr/0007-state-digest-algorithm-canonical-serialization.md", + "anchor": "", + "href": "docs/adr/0007-state-digest-algorithm-canonical-serialization.md", + "section": "" } ] diff --git a/docs/constitution/id-index-by-tag.md b/docs/constitution/id-index-by-tag.md index 40c8065..bd637d2 100644 --- a/docs/constitution/id-index-by-tag.md +++ b/docs/constitution/id-index-by-tag.md @@ -16,6 +16,7 @@ This index is link-only. All substance lives in the canonical Constitution docum ## authority +- [DM-0019](./domain-model.md#DM-0019) — PlayerId - [INV-0003](./invariants.md#INV-0003) — Authoritative Simulation ## connection @@ -26,19 +27,31 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0001](./domain-model.md#DM-0001) — Tick - [DM-0014](./domain-model.md#DM-0014) — Simulation Core +- [DM-0018](./domain-model.md#DM-0018) — StateDigest +- [DM-0019](./domain-model.md#DM-0019) — PlayerId +- [DM-0020](./domain-model.md#DM-0020) — EntityId +- [DM-0023](./domain-model.md#DM-0023) — LastKnownIntent (LKI) +- [DM-0024](./domain-model.md#DM-0024) — AppliedInput +- [DM-0026](./domain-model.md#DM-0026) — InputSeq +- [DM-0027](./domain-model.md#DM-0027) — StepInput - [INV-0001](./invariants.md#INV-0001) — Deterministic Simulation - [INV-0002](./invariants.md#INV-0002) — Fixed Timestep - [INV-0004](./invariants.md#INV-0004) — Simulation Core Isolation - [INV-0006](./invariants.md#INV-0006) — Replay Verifiability +- [INV-0007](./invariants.md#INV-0007) — Deterministic Ordering & Canonicalization - [KC-0001](./acceptance-kill.md#KC-0001) — Simulation Core Boundary Violation ## entity - [DM-0003](./domain-model.md#DM-0003) — Character +- [DM-0020](./domain-model.md#DM-0020) — EntityId ## identity - [DM-0005](./domain-model.md#DM-0005) — Entity +- [DM-0019](./domain-model.md#DM-0019) — PlayerId +- [DM-0020](./domain-model.md#DM-0020) — EntityId +- [DM-0021](./domain-model.md#DM-0021) — MatchId ## infrastructure @@ -47,10 +60,12 @@ This index is link-only. All substance lives in the canonical Constitution docum ## input - [DM-0006](./domain-model.md#DM-0006) — InputCmd +- [DM-0022](./domain-model.md#DM-0022) — InputTickWindow +- [DM-0023](./domain-model.md#DM-0023) — LastKnownIntent (LKI) ## movement -- [DM-0004](./domain-model.md#DM-0004) — Locomotion Mode +- [DM-0004](./domain-model.md#DM-0004) — LocomotionMode ## networking @@ -63,6 +78,9 @@ This index is link-only. All substance lives in the canonical Constitution docum - [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 +- [DM-0023](./domain-model.md#DM-0023) — LastKnownIntent (LKI) +- [DM-0025](./domain-model.md#DM-0025) — TargetTickFloor +- [DM-0026](./domain-model.md#DM-0026) — InputSeq - [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) — Simulation Core Boundary Violation @@ -75,6 +93,7 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0010](./domain-model.md#DM-0010) — Match - [DM-0012](./domain-model.md#DM-0012) — Matchmaker +- [DM-0021](./domain-model.md#DM-0021) — MatchId ## phase0 @@ -98,19 +117,30 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0007](./domain-model.md#DM-0007) — Snapshot - [DM-0009](./domain-model.md#DM-0009) — Channel - [DM-0016](./domain-model.md#DM-0016) — Baseline +- [DM-0024](./domain-model.md#DM-0024) — AppliedInput +- [DM-0025](./domain-model.md#DM-0025) — TargetTickFloor +- [DM-0026](./domain-model.md#DM-0026) — InputSeq ## 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 +- [DM-0017](./domain-model.md#DM-0017) — ReplayArtifact +- [DM-0018](./domain-model.md#DM-0018) — StateDigest +- [DM-0024](./domain-model.md#DM-0024) — AppliedInput - [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 - [KC-0002](./acceptance-kill.md#KC-0002) — Replay Verification Blocker +## schema + +- [DM-0017](./domain-model.md#DM-0017) — ReplayArtifact + ## security +- [DM-0022](./domain-model.md#DM-0022) — InputTickWindow - [INV-0003](./invariants.md#INV-0003) — Authoritative Simulation ## simulation @@ -119,7 +149,9 @@ 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-0010](./domain-model.md#DM-0010) — Match - [DM-0014](./domain-model.md#DM-0014) — Simulation Core +- [DM-0027](./domain-model.md#DM-0027) — StepInput - [INV-0001](./invariants.md#INV-0001) — Deterministic Simulation - [INV-0002](./invariants.md#INV-0002) — Fixed Timestep @@ -127,6 +159,7 @@ This index is link-only. All substance lives in the canonical Constitution docum - [DM-0007](./domain-model.md#DM-0007) — Snapshot - [DM-0016](./domain-model.md#DM-0016) — Baseline +- [DM-0025](./domain-model.md#DM-0025) — TargetTickFloor ## testability @@ -134,7 +167,10 @@ This index is link-only. All substance lives in the canonical Constitution docum ## traceability +- [DM-0017](./domain-model.md#DM-0017) — ReplayArtifact +- [DM-0021](./domain-model.md#DM-0021) — MatchId - [INV-0005](./invariants.md#INV-0005) — Tick-Indexed I/O Contract +- [INV-0007](./invariants.md#INV-0007) — Deterministic Ordering & Canonicalization ## transport @@ -142,5 +178,7 @@ This index is link-only. All substance lives in the canonical Constitution docum ## verification +- [DM-0018](./domain-model.md#DM-0018) — StateDigest - [INV-0006](./invariants.md#INV-0006) — Replay Verifiability +- [INV-0007](./invariants.md#INV-0007) — Deterministic Ordering & Canonicalization - [KC-0002](./acceptance-kill.md#KC-0002) — Replay Verification Blocker diff --git a/docs/constitution/id-index.md b/docs/constitution/id-index.md index 2cb2d9b..b75806d 100644 --- a/docs/constitution/id-index.md +++ b/docs/constitution/id-index.md @@ -13,13 +13,14 @@ This index is link-only. All substance lives in the canonical Constitution docum - [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 +- [INV-0007](./invariants.md#INV-0007) — Deterministic Ordering & Canonicalization ## Domain Model - [DM-0001](./domain-model.md#DM-0001) — Tick - [DM-0002](./domain-model.md#DM-0002) — World - [DM-0003](./domain-model.md#DM-0003) — Character -- [DM-0004](./domain-model.md#DM-0004) — Locomotion Mode +- [DM-0004](./domain-model.md#DM-0004) — LocomotionMode - [DM-0005](./domain-model.md#DM-0005) — Entity - [DM-0006](./domain-model.md#DM-0006) — InputCmd - [DM-0007](./domain-model.md#DM-0007) — Snapshot @@ -32,6 +33,17 @@ This index is link-only. All substance lives in the canonical Constitution docum - [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 +- [DM-0017](./domain-model.md#DM-0017) — ReplayArtifact +- [DM-0018](./domain-model.md#DM-0018) — StateDigest +- [DM-0019](./domain-model.md#DM-0019) — PlayerId +- [DM-0020](./domain-model.md#DM-0020) — EntityId +- [DM-0021](./domain-model.md#DM-0021) — MatchId +- [DM-0022](./domain-model.md#DM-0022) — InputTickWindow +- [DM-0023](./domain-model.md#DM-0023) — LastKnownIntent (LKI) +- [DM-0024](./domain-model.md#DM-0024) — AppliedInput +- [DM-0025](./domain-model.md#DM-0025) — TargetTickFloor +- [DM-0026](./domain-model.md#DM-0026) — InputSeq +- [DM-0027](./domain-model.md#DM-0027) — StepInput ## Acceptance Criteria @@ -47,6 +59,8 @@ This index is link-only. All substance lives in the canonical Constitution docum - [ADR-0000](../adr/0000-adr-template.md) — ADR XXXX: - [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-0003](../adr/0003-fixed-timestep-simulation.md) — ADR 0003: Fixed Timestep Simulation Model (Tick-Driven) - [ADR-0004](../adr/0004-server-authoritative-architecture.md) — ADR 0004: Server-Authoritative Architecture - [ADR-0005](../adr/0005-v0-networking-architecture.md) — ADR 0005: v0 Networking Architecture +- [ADR-0006](../adr/0006-input-tick-targeting.md) — ADR 0006: Input Tick Targeting & Server Tick Guidance +- [ADR-0007](../adr/0007-state-digest-algorithm-canonical-serialization.md) — ADR 0007: StateDigest Algorithm (v0) diff --git a/docs/constitution/invariants.md b/docs/constitution/invariants.md index b0ee29e..a1d43eb 100644 --- a/docs/constitution/invariants.md +++ b/docs/constitution/invariants.md @@ -34,7 +34,11 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** authority, networking, security -**Statement:** The authoritative simulation instance MUST be the single source of truth for all game-outcome-affecting state. Clients are untrusted. Client messages MUST be treated as intent and MUST NOT directly author authoritative state transitions. Inputs MUST be validated before affecting authoritative state. +**Statement:** The authoritative simulation instance MUST be the single source of truth for all game-outcome-affecting state. Clients are untrusted. Client messages MUST be treated as intent and MUST NOT directly author authoritative state transitions. + +Inputs MUST be validated and sanitized by the Server Edge (DM-0011) before they affect authoritative state or are delivered to the Simulation Core (DM-0014). The Simulation Core MUST NOT perform client-facing validation, authentication, or identity binding; it operates only on StepInput (DM-0027) values derived from AppliedInput (DM-0024) by the Server Edge. + +Player identity binding is a trust boundary: PlayerId (DM-0019) assignment and binding to Session (DM-0008) is exclusively owned by the Server Edge; the Simulation Core treats PlayerId only as an indexing/ordering key. Client-provided PlayerId values (explicit or implicit) MUST be ignored and overwritten by the Server Edge with the session-assigned PlayerId before inputs reach the Simulation Core. *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.* @@ -44,6 +48,13 @@ Section groupings (H2) are optional, for organization only. **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. +Debug/telemetry mechanisms (e.g., time synchronization probes, instrumentation) MUST NOT influence authoritative outcomes; they may observe and report, but MUST NOT change simulation decisions or state transitions. + +**Enforcement (normative):** This isolation MUST be mechanically enforced in CI for the Simulation Core crate/module. At minimum: +- Dependencies for the Simulation Core MUST be allowlisted (or equivalently, non-allowlisted dependencies MUST be denied) to prevent accidental introduction of I/O/network/time capabilities. +- CI MUST run a forbidden-API/source scan over the Simulation Core to reject disallowed surfaces, including (at minimum): file and socket I/O, system calls, wall-clock time reads, thread sleeps, environment access, and unseeded/ambient randomness. +- Conditional-compilation or build-flag “escape hatches” (e.g., `cfg(...)` paths) MUST NOT be used to introduce prohibited capabilities into the Simulation Core in any build mode. + *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 @@ -58,9 +69,34 @@ Section groupings (H2) are optional, for organization only. **Status:** Active **Tags:** determinism, replay, verification -**Statement:** The system MUST support match reproduction from a replay artifact. A replay artifact MUST include: initial state snapshot, random seed(s), ruleset/tuning parameters, and chronologically ordered input stream with tick associations. Replay validation MUST verify equivalence of authoritative outcome at a defined checkpoint (e.g., match end tick or final state hash). +**Statement:** The system MUST support match reproduction from a ReplayArtifact (DM-0017). + +**v0 Replay Scope:** For v0, the declared replay scope MUST be "same produced binary artifact on the same target triple/profile." Replay verification MUST execute the exact produced artifact that generated the ReplayArtifact. Rebuilding, relinking, or substituting dependencies or resources between the authoritative run and verification is forbidden under v0 scope. + +A ReplayArtifact MUST be versioned and self-describing enough to reproduce the authoritative outcome under the declared replay scope. At minimum, it MUST include: +- a format/version identifier, +- sufficient initialization data to reconstruct the authoritative starting state for replay (e.g., an initial Baseline (DM-0016) or equivalent canonical state seed + deterministic initialization parameters), +- all determinism-relevant seed(s)/parameters and any required algorithm identifiers (e.g., RNG algorithm ID; StateDigest algorithm ID), +- a chronologically ordered stream of AppliedInput (DM-0024) values with tick associations (i.e., the per-tick inputs that were actually applied by the authoritative simulation, not raw client messages), +- a defined verification anchor: checkpoint_tick and associated StateDigest(s) (DM-0018) required by the replay procedure (e.g., checkpoint digest and/or final digest). + +**Tick-Boundary Checkpoint Semantics:** Any recorded checkpoint_tick MUST denote the world tick value immediately after completing the last applied tick step (post-step). Replay anchors (including checkpoint ticks and digests) MUST NOT be recorded mid-tick. Authoritative termination with respect to replay anchoring MUST occur only at tick boundaries. + +**Initialization Verification Anchor:** Replay validation MUST verify at least one initialization anchor before applying any replayed inputs or steps (e.g., a StateDigest of the initial Baseline or pre-step starting state) in addition to at least one outcome anchor (e.g., final or checkpoint digest). If the initialization anchor fails, replay verification MUST fail immediately. + +Replay validation MUST verify outcome equivalence by comparing the computed StateDigest at the declared anchor tick(s) to the digest recorded in the ReplayArtifact. + +*Non-normative note: Equivalence verification may use state hashing, periodic snapshots, or final outcome comparison; the key requirement is that the system can prove determinism in practice, not just assert it in theory. "Authoritative outcome" is robust to early termination or disconnect scenarios. For v0, replay verification is scoped to same build + same platform (cross-platform determinism deferred to post-v0; see ADR-0005 Determinism Scope).* + +### <a id="INV-0007"></a> INV-0007 — Deterministic Ordering & Canonicalization +**Status:** Active +**Tags:** determinism, verification, traceability + +**Statement:** Whenever the Simulation Core processes a set or collection that can affect outcomes (e.g., multiple inputs, entities, events, collisions) within a tick, it MUST use a stable, deterministic ordering that is independent of runtime artifacts (hash iteration order, pointer/address order, thread scheduling, or platform-specific container behavior). + +When a canonical byte representation is required for verification (e.g., StateDigest (DM-0018) computation), the state-to-bytes process MUST be explicitly canonicalized such that equivalent authoritative states serialize identically. The digest/canonicalization procedure MUST be versionable and unambiguous (e.g., via algorithm identifiers recorded in the ReplayArtifact (DM-0017)). -*Non-normative note: Equivalence verification may use state hashing, periodic snapshots, or final outcome comparison. The key requirement is that the system can prove determinism in practice, not just assert it in theory. "Authoritative outcome" is robust to early termination or disconnect scenarios. For v0, replay verification is scoped to same build + same platform (cross-platform determinism deferred to post-v0; see ADR-0005 Determinism Scope).* +*Non-normative note: Deterministic ordering is typically achieved by sorting on stable keys such as PlayerId (DM-0019) for StepInput (DM-0027) values and EntityId (DM-0020) for entity iteration. Canonicalization must eliminate representational ambiguity (e.g., stable iteration order; stable float handling; stable serialization layout) so replay verification is meaningful.* ## Invariant Change Policy diff --git a/docs/networking/v0-parameters.md b/docs/networking/v0-parameters.md index 00034d4..3cd971b 100644 --- a/docs/networking/v0-parameters.md +++ b/docs/networking/v0-parameters.md @@ -11,7 +11,15 @@ These values may change as we iterate, without requiring changes to invariants o | snapshot_rate_hz | 60 | One snapshot per tick (v0 simplicity) | | input_send_rate_hz | 60 | Target send rate; may be clamped to tick rate | | input_rate_limit_per_sec | 120 | Tier-0 spam control | -| input_tick_window_ticks | ±120 | Tier-0 sanity window (dev/LAN posture) | +| max_future_ticks | 120 | Maximum ticks ahead a client can target (InputTickWindow upper bound) | +| input_tick_window | `[current_tick, current_tick + max_future_ticks]` | Future-only acceptance; late inputs dropped | +| input_lead_ticks | 1 | TargetTickFloor = server.current_tick + input_lead_ticks | + +## Parameter definitions + +- **max_future_ticks:** Defines the InputTickWindow (DM-0022) upper bound. Inputs targeting `cmd.tick > current_tick + max_future_ticks` are rejected. +- **input_tick_window:** Future-only acceptance window. Inputs with `cmd.tick < current_tick` (late) are always dropped. This is not a symmetric ± window. +- **input_lead_ticks:** Used to compute TargetTickFloor (DM-0025) in ServerWelcome and Snapshots. Clients target at least `TargetTickFloor = server.current_tick + input_lead_ticks`. ## Change policy diff --git a/docs/specs/FS-0007-v0-multiplayer-slice.md b/docs/specs/FS-0007-v0-multiplayer-slice.md new file mode 100644 index 0000000..3907b13 --- /dev/null +++ b/docs/specs/FS-0007-v0-multiplayer-slice.md @@ -0,0 +1,350 @@ +--- +status: Draft +issue: 7 +title: v0 Two-Client Multiplayer Slice +--- + +# FS-0007: v0 Two-Client Multiplayer Slice + +> **Status:** Draft +> **Issue:** [#7](https://github.com/project-flowstate/flowstate/issues/7) +> **Owner:** @danieldilly +> **Date:** 2025-12-21 + +--- + +## 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. + +## Trace Map + +| ID | Relationship | Notes | +|----|--------------|-------| +| AC-0001 | Implements | Primary acceptance criterion | +| INV-0001 | Constrains | Deterministic simulation | +| INV-0002 | Constrains | Fixed timestep | +| INV-0003 | Constrains | Server authoritative | +| INV-0004 | Constrains | Simulation Core isolation | +| INV-0005 | Constrains | Tick-indexed I/O | +| INV-0006 | Constrains | Replay verifiability | +| INV-0007 | Constrains | Deterministic ordering/canonicalization | +| KC-0001 | Constrains | Kill: Simulation Core boundary violation | +| KC-0002 | Constrains | Kill: Replay cannot reproduce outcome | +| DM-0001..DM-0027 | Implements | See Domain Concepts table | +| ADR-0001 | Implements | Authoritative Multiplayer Architecture | +| ADR-0002 | Implements | Deterministic simulation requirements | +| ADR-0003 | Implements | Fixed timestep (tick-driven stepping API) | +| ADR-0004 | Implements | Server-authoritative architecture | +| ADR-0005 | Implements | v0 networking (ENet, Protobuf, same-build scope) | +| ADR-0006 | Implements | Input tick targeting & TargetTickFloor | +| ADR-0007 | Implements | StateDigest algorithm (FNV-1a 64-bit, canonicalization) | + +## Domain Concepts + +| Concept | ID | v0 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 EntityId | +| InputCmd | DM-0006 | Tick-indexed movement intent (logical concept; wire: InputCmdProto) | +| Snapshot | DM-0007 | Post-step world state at tick T+1 | +| Session | DM-0008 | Per-client connection lifecycle | +| Channel | DM-0009 | Realtime (unreliable+sequenced) or Control (reliable+ordered) | +| Match | DM-0010 | Game instance lifecycle; scopes replay | +| Server Edge | DM-0011 | Transport, validation, tick scheduling | +| Simulation Core | DM-0014 | Deterministic game logic; no I/O | +| Game Client | DM-0015 | Player runtime; rendering, input | +| Baseline | DM-0016 | Pre-step state at tick T | +| ReplayArtifact | DM-0017 | Versioned record for replay verification | +| StateDigest | DM-0018 | Deterministic digest (see ADR-0007) | +| PlayerId | DM-0019 | Per-Match participant identifier | +| EntityId | DM-0020 | Unique identifier for Entity | +| MatchId | DM-0021 | Stable Match correlation key | +| InputTickWindow | DM-0022 | Server tick-indexed acceptance window | +| LastKnownIntent | DM-0023 | Input continuity fallback | +| AppliedInput | DM-0024 | Post-normalization input (Server Edge truth) | +| TargetTickFloor | DM-0025 | Server-emitted tick floor for input targeting | +| InputSeq | DM-0026 | Per-session sequence for deterministic selection | +| StepInput | DM-0027 | Simulation-plane input consumed by advance() | + +**Pipeline:** InputCmd (protocol) → AppliedInput (Server Edge selection) → StepInput (Simulation Core). + +## Interfaces + +### Simulation Core Types + +```rust +/// Ref: DM-0001 +pub type Tick = u64; +/// Ref: DM-0019 (v0 representation) +pub type PlayerId = u8; +/// Ref: DM-0020 +pub type EntityId = u64; + +/// Ref: DM-0027. Simulation-plane input consumed by advance(). +/// player_id is an association key used to match intent to player's entity; +/// Server Edge owns identity binding (INV-0003). +/// StepInput values passed to advance() MUST be sorted by player_id ascending +/// for deterministic iteration (INV-0007), not for semantic discrimination. +pub struct StepInput { + pub player_id: PlayerId, + pub move_dir: [f64; 2], // Magnitude <= 1.0 +} + +/// Ref: DM-0016. Pre-step world state at tick T. +/// Digest computed via World::state_digest() per ADR-0007. +/// entities MUST be sorted by entity_id ascending (INV-0007). +pub struct Baseline { + pub tick: Tick, + pub entities: Vec<EntitySnapshot>, + pub digest: u64, // state_digest() at this tick +} + +/// Ref: DM-0007. Post-step world state at tick T+1. +/// Digest computed via World::state_digest() per ADR-0007. +/// entities MUST be sorted by entity_id ascending (INV-0007). +pub struct Snapshot { + pub tick: Tick, + pub entities: Vec<EntitySnapshot>, + pub digest: u64, // state_digest() at this tick +} + +pub struct EntitySnapshot { + pub entity_id: EntityId, + pub position: [f64; 2], + pub velocity: [f64; 2], +} + +/// Ref: DM-0002 +pub struct World { /* opaque to Server Edge */ } + +impl World { + /// Create world. dt_seconds = 1.0 / tick_rate_hz computed internally. + pub fn new(seed: u64, tick_rate_hz: u32) -> Self; + pub fn spawn_character(&mut self, player_id: PlayerId) -> EntityId; + /// Ref: DM-0016. Postcondition: baseline().tick == world.tick() + pub fn baseline(&self) -> Baseline; + /// Ref: ADR-0003, ADR-0006. + /// Precondition: tick MUST == self.tick(). + /// Postconditions: world.tick() == tick + 1; snapshot.tick == tick + 1. + /// step_inputs MUST be sorted by player_id ascending (INV-0007). + pub fn advance(&mut self, tick: Tick, step_inputs: &[StepInput]) -> Snapshot; + pub fn tick(&self) -> Tick; + /// Ref: ADR-0007 + pub fn state_digest(&self) -> u64; + pub fn tick_rate_hz(&self) -> u32; +} +``` + +### Protocol Messages + +Per ADR-0005. Channel mappings: + +| Message | Channel | Direction | Key Fields | +|---------|---------|-----------|------------| +| `ClientHello` | Control | C→S | Handshake initiation | +| `ServerWelcome` | Control | S→C | `target_tick_floor`, `tick_rate_hz`, `player_id` | +| `JoinBaseline` | Control | S→C | Baseline (DM-0016) | +| `InputCmdProto` | Realtime | C→S | `tick`, `input_seq`, `move_dir` (no `player_id` - bound by Server Edge) | +| `SnapshotProto` | Realtime | S→C | Snapshot + `target_tick_floor` | +| `TimeSyncPing` | Control | C→S | `client_timestamp` | +| `TimeSyncPong` | Control | S→C | `server_tick`, `server_timestamp`, `ping_timestamp_echo` | + +**Normative requirements:** +- ServerWelcome and every SnapshotProto MUST include `target_tick_floor` (DM-0025, ADR-0006). +- `target_tick_floor` MUST be computed as `server.current_tick + input_lead_ticks`. +- Server MUST emit target_tick_floor as monotonic non-decreasing for the match; clients MUST take max to ensure their local floor is monotonic. + +**Notes:** +- `player_id` assignment: first session = 0, second session = 1 (bound by Server Edge from session, not from protocol). +- Clients MUST take `max(previous, received)` when updating local TargetTickFloor. +- Clients MUST target `InputCmd.tick >= TargetTickFloor`. +- TimeSync is debug/telemetry only; MUST NOT affect authoritative outcomes. + +## Determinism Notes + +This feature is the foundation of the determinism guarantee. Key constraints: + +- **Simulation Core isolation (INV-0004, KC-0001):** No I/O, networking, wall-clock, ambient RNG. Enforced via crate separation, CI dependency allowlist, and forbidden-API source scan. +- **Fixed timestep (INV-0002):** `tick_rate_hz` configured at `World::new()` only; `dt_seconds` computed internally. +- **Deterministic ordering (INV-0007):** Inputs sorted by `player_id`; entities iterated by `EntityId` ascending. +- **StateDigest (ADR-0007):** FNV-1a 64-bit with canonicalization (`-0.0` → `+0.0`, NaN → quiet NaN). +- **Same-build scope (ADR-0005):** v0 guarantees determinism for same binary artifact + same target triple only. + +Replay verification validates initialization anchor (baseline digest) and final outcome digest per INV-0006. + +## Validation Rules + +Server Edge validates inputs BEFORE converting to StepInput. Parameters from [v0-parameters.md](../networking/v0-parameters.md). + +| Check | Behavior | +|-------|----------| +| NaN/Inf in move_dir | DROP + LOG | +| Magnitude > 1.0 | CLAMP to unit length + LOG | +| Tick window: `cmd.tick < current_tick` | DROP (late) | +| Tick window: `cmd.tick > current_tick + max_future_ticks` | DROP (too far future) | +| Rate limit exceeded | DROP + LOG | +| InputSeq non-increasing | DROP non-increasing cmd + LOG protocol violation (clients MUST send strictly increasing input_seq per session) | +| Multiple InputCmdProto for same (session, tick) | Keep greatest `input_seq`; if equal, keep first-seen and LOG protocol violation | + +**LastKnownIntent (DM-0023):** Missing input at current_tick → reuse last move_dir. Initial = `[0, 0]`. + +## Server Tick Loop (Non-Normative Pseudocode) + +Constants from [v0-parameters.md](../networking/v0-parameters.md): `tick_rate_hz=60`, `max_future_ticks=120`, `input_lead_ticks=1`. + +``` +// Wait for two sessions; only then send ServerWelcome + JoinBaseline +wait for two client connections +world = World::new(seed, tick_rate_hz) // tick_rate_hz from v0-parameters.md +spawn characters (player_id 0 first, then 1) +target_tick_floor = world.tick() + input_lead_ticks // input_lead_ticks from v0-parameters.md + +send ServerWelcome (target_tick_floor, tick_rate_hz, player_id) to each client +send JoinBaseline (world.baseline()) to both clients + +loop (paced at tick_rate_hz): + current_tick = world.tick() + target_tick_floor = current_tick + input_lead_ticks + + // Buffer incoming inputs with validation + InputSeq selection + server_edge.receive_and_buffer_inputs() + + // Produce AppliedInput per player (from buffer or LastKnownIntent) + applied_inputs = [] + for each player: + if input_buffer[player][current_tick] exists: + applied_inputs.append(buffer entry) + update current_intent[player] + else: + applied_inputs.append(LastKnownIntent fallback) + + // Record for replay, convert to StepInput, advance + replay_artifact.inputs.extend(sorted(applied_inputs)) + step_inputs = convert applied_inputs to StepInput + snapshot = world.advance(current_tick, step_inputs) + + broadcast(snapshot, target_tick_floor) + +on match end: + // Complete current tick before ending (no mid-tick termination) + replay_artifact.final_digest = world.state_digest() + replay_artifact.checkpoint_tick = world.tick() + write replay artifact to replays/{match_id}.replay +``` + +## Client Input Targeting (Non-Normative Pseudocode) + +Per ADR-0006: + +``` +// On ServerWelcome: +target_tick_floor = ServerWelcome.target_tick_floor +input_seq = 0 + +// On each SnapshotProto: +target_tick_floor = max(target_tick_floor, SnapshotProto.target_tick_floor) + +// When sending input: +InputCmdProto.tick = target_tick_floor // Or higher +InputCmdProto.input_seq = ++input_seq +``` + +## Replay Artifact (DM-0017) + +Required fields per INV-0006: + +| Field | Purpose | +|-------|---------| +| `replay_format_version` | Schema version (start at 1) | +| `initial_baseline` | Baseline at match start tick (DM-0016); v0 starts at tick 0 | +| `seed` | RNG seed | +| `rng_algorithm` | e.g., "ChaCha8Rng" | +| `tick_rate_hz` | Simulation tick rate | +| `state_digest_algo_id` | Per ADR-0007 | +| `entity_spawn_order` | Deterministic EntityId assignment | +| `player_entity_mapping` | player_id → EntityId | +| `tuning_parameters` | Any sim-affecting parameters (e.g., move_speed); v0 may be empty | +| `inputs` | AppliedInput stream (DM-0024) sorted by: (1) tick ascending, (2) player_id ascending. Gaps filled by LastKnownIntent (DM-0023) and recorded. | +| `build_fingerprint` | Binary identity (e.g., git commit + target triple); enables same-build verification | +| `final_digest` | StateDigest at checkpoint_tick (ADR-0007) | +| `checkpoint_tick` | Post-step tick for verification | +| `end_reason` | "complete" or "disconnect" | + +**Verification (ref INV-0006):** +1. Verify `artifact.build_fingerprint` matches current binary: CI/Tier-0 MUST fail on mismatch; dev MAY warn and proceed +2. Validate AppliedInput stream integrity: MUST contain exactly one entry per (player_id, tick) for each tick in range [initial_baseline.tick, checkpoint_tick); fail immediately if missing or extra entries +3. Initialize World with `World::new(artifact.seed, artifact.tick_rate_hz)` +4. Verify `world.baseline().digest == artifact.initial_baseline.digest` (fail immediately if mismatch) +5. Replay ticks [initial_baseline.tick, checkpoint_tick): convert AppliedInput → StepInput, call `world.advance(t, step_inputs)` +6. Assert `world.tick() == checkpoint_tick` +7. Assert `world.state_digest() == artifact.final_digest` + +**Location:** `replays/{match_id}.replay` (untracked, gitignored) + +## Gate Plan + +### Tier 0 (Must pass before merge) + +- [ ] **T0.1:** Two clients connect, complete handshake (ServerWelcome with TargetTickFloor + tick_rate_hz + player_id) +- [ ] **T0.2:** JoinBaseline delivers initial Baseline; clients display Characters +- [ ] **T0.3:** Clients tag inputs per ADR-0006: InputCmd.tick >= TargetTickFloor, InputSeq monotonic +- [ ] **T0.4:** WASD produces movement; both clients see own + opponent via Snapshots +- [ ] **T0.5:** Simulation Core isolation enforced: crate separation, dependency allowlist (CI), forbidden-API scan (CI); advance() takes explicit tick per ADR-0003 +- [ ] **T0.6:** Validation per v0-parameters.md: magnitude clamp, NaN/Inf drop, tick window, rate limit, InputSeq selection (DM-0026), LastKnownIntent (DM-0023), player_id bound to session (INV-0003) +- [ ] **T0.7:** Malformed inputs do not crash server +- [ ] **T0.8:** TimeSync ping/pong implemented (debug/telemetry only) +- [ ] **T0.9:** Replay artifact generated with all required fields +- [ ] **T0.10:** Replay verification: baseline digest check, half-open [0, checkpoint_tick), final digest match +- [ ] **T0.10a:** Initialization anchor failure: mutated baseline digest fails immediately +- [ ] **T0.11:** Future input non-interference: input for T+k (k > window) rejected; T+1 input buffered without affecting T +- [ ] **T0.12:** LastKnownIntent determinism: input gaps filled, recorded in artifact, replay produces same digest +- [ ] **T0.13:** Validation matrix: NaN, magnitude, tick window, rate limit, InputSeq selection with tie-breaking +- [ ] **T0.14:** Disconnect handling: complete current tick, persist artifact with end_reason="disconnect", clean shutdown +- [ ] **T0.15:** `just ci` passes + +### Tier 1 (Tracked follow-up) + +- [ ] Extended replay test: 10,000+ ticks +- [ ] Client-side interpolation +- [ ] Graceful disconnect handling +- [ ] Stricter validation (Tier-1 security posture) + +### Tier 2 (Aspirational) + +- [ ] Cross-platform determinism +- [ ] Client-side prediction + reconciliation +- [ ] Snapshot delta compression +- [ ] WebTransport adapter + +## Acceptance Criteria + +Maps to AC-0001 sub-criteria: + +- [ ] **AC-0001.1:** Two clients connect, handshake, receive Baseline, remain synchronized +- [ ] **AC-0001.2:** WASD movement works; LastKnownIntent for missing inputs; TargetTickFloor-based targeting +- [ ] **AC-0001.3:** Replay verification passes (baseline + final digest); Simulation Core has no I/O; tick_rate_hz fixed at construction; advance() takes explicit tick + StepInput +- [ ] **AC-0001.4:** Validation per v0-parameters.md; InputSeq selection with deterministic tie-breaking; future inputs buffered correctly; player_id bound to session (INV-0003); disconnect → complete tick → persist artifact → shutdown +- [ ] **AC-0001.5:** ReplayArtifact produced with all fields; reproduces outcome on same build/platform + +## Non-Goals + +Explicitly out of scope: + +- Client-side prediction / reconciliation +- Cross-platform determinism (v0 = same-build/same-platform per ADR-0005) +- Web clients +- Matchmaking / lobbies / orchestration +- Collision / terrain +- Combat / abilities +- Snapshot delta compression +- `.proto` files (v0 uses inline prost derive) diff --git a/docs/specs/FS-TBD-v0-multiplayer-slice.md b/docs/specs/FS-TBD-v0-multiplayer-slice.md deleted file mode 100644 index 3e7c199..0000000 --- a/docs/specs/FS-TBD-v0-multiplayer-slice.md +++ /dev/null @@ -1,703 +0,0 @@ ---- -status: Draft -issue: 7 -title: v0 Two-Client Multiplayer Slice ---- - -# FS-0007: v0 Two-Client Multiplayer Slice - -> **Status:** Draft -> **Issue:** [#7](https://github.com/project-flowstate/flowstate/issues/7) -> **Author:** @copilot -> **Date:** 2025-12-21 - ---- - -## 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: [#7](https://github.com/project-flowstate/flowstate/issues/7) - -## 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 | - diff --git a/docs/specs/_TEMPLATE.md b/docs/specs/_TEMPLATE.md index f003675..79245cc 100644 --- a/docs/specs/_TEMPLATE.md +++ b/docs/specs/_TEMPLATE.md @@ -8,23 +8,15 @@ title: Short descriptive title > **Status:** Draft | Approved | Implemented > **Issue:** [#NNNN](https://github.com/ORG/REPO/issues/NNNN) -> **Author:** @username -> **Date:** YYYY-MM-DD - ---- +> **Owner:** @username +> **Contributors:** @user1, @user2 (optional) +> **Date:** YYYY-MM-DD ## Problem <!-- REQUIRED (linter-enforced) --> <!-- What problem does this feature solve? Be specific. --> -## Issue - -<!-- REQUIRED (linter-enforced) --> -<!-- Link to the GitHub issue. Must match frontmatter `issue` field. --> - -- Issue: [#NNNN](https://github.com/ORG/REPO/issues/NNNN) - ## Trace Map <!-- REQUIRED (linter-enforced) --> @@ -62,85 +54,90 @@ pub struct ExampleType { ### Changed Types -- `ExistingType`: Added `new_field: Option<u32>` +* `ExistingType`: Added `new_field: Option<u32>` ### New Messages / Events -- None +* None ## Determinism Notes <!-- REQUIRED (linter-enforced) --> + <!-- How does this feature impact simulation correctness? --> + <!-- If no sim impact, state "No simulation impact." --> -- This feature affects simulation state by: ... -- Determinism preserved because: ... -- Replay verification: ... +* This feature affects simulation state by: ... +* Determinism preserved because: ... +* Replay verification: ... ## Gate Plan <!-- REQUIRED (linter-enforced) --> + <!-- Tier 0 must have at least one bullet item. --> ### Tier 0 (Must pass before merge) -- [ ] Unit tests for `ExampleType` -- [ ] `just ci` passes -- [ ] Determinism check: replay test with fixed seed +* [ ] Unit tests for `ExampleType` +* [ ] `just ci` passes +* [ ] Determinism check: replay test with fixed seed ### Tier 1 (Tracked follow-up) -- [ ] Performance benchmark (Issue: #TBD) -- [ ] Extended replay test (100k ticks) +* [ ] Performance benchmark (Issue: #TBD) +* [ ] Extended replay test (100k ticks) ### Tier 2 (Aspirational) -- [ ] "Feel" metrics (not yet formalized) +* [ ] "Feel" metrics (not yet formalized) ## Acceptance Criteria <!-- REQUIRED (linter-enforced) --> + <!-- Observable outcomes that define "done". Must be objectively verifiable. --> -- [ ] Criterion 1: When X happens, Y is observable -- [ ] Criterion 2: System state satisfies Z after operation -- [ ] Criterion 3: `just ci` passes with new tests +* [ ] Criterion 1: When X happens, Y is observable +* [ ] Criterion 2: System state satisfies Z after operation +* [ ] Criterion 3: `just ci` passes with new tests ## Non-Goals <!-- OPTIONAL --> + <!-- What is explicitly out of scope? --> -- This spec does NOT address: ... -- Deferred to future work: ... +* This spec does NOT address: ... +* Deferred to future work: ... ## Risks <!-- OPTIONAL --> + <!-- Known risks and mitigations. --> -| Risk | Likelihood | Impact | Mitigation | -|------|------------|--------|------------| -| Risk 1 | Low | Medium | Mitigation strategy | +| Risk | Likelihood | Impact | Mitigation | +| ------ | ---------- | ------ | ------------------- | +| Risk 1 | Low | Medium | Mitigation strategy | ## Alternatives <!-- OPTIONAL --> + <!-- Other approaches considered and why they weren't chosen. --> ### Alternative A: ... -- Pros: ... -- Cons: ... -- Rejected because: ... - ---- +* Pros: ... +* Cons: ... +* Rejected because: ... ## Changelog <!-- Update as the spec evolves. --> -| Date | Author | Change | -|------|--------|--------| -| YYYY-MM-DD | @username | Initial draft | +| Date | Owner | Change | +| ---------- | --------- | ------------- | +| YYYY-MM-DD | @username | Initial draft | \ No newline at end of file diff --git a/scripts/spec_lint.py b/scripts/spec_lint.py index 4225b5b..8b9eccc 100644 --- a/scripts/spec_lint.py +++ b/scripts/spec_lint.py @@ -31,7 +31,6 @@ # Required sections (must exist and have content) REQUIRED_SECTIONS = [ "Problem", - "Issue", "Trace Map", "Domain Concepts", "Interfaces",