# Networking Owns the authority model, the responsiveness tiers, the posture on prediction and tick rate, the session shape, and the two seams (identity and persistence) that let the game outgrow one machine without a rewrite. It does **not** own gameplay: what a shove does is in [Combat.md](Combat.md), what an insert does in [Crafting.md](Crafting.md). Each feature spec states what is authoritative where; this document is the rule they all follow and the reasoning behind it. Read [Architecture.md](Architecture.md) first. ## The decision ``` [DECIDED] Server-authoritative from the first line, on the engine's own replication, with the dedicated server as the real target. There is no listen-server mode in the design and no "are we multiplayer" branch anywhere in gameplay code. Standalone and listen-server play exist because the editor offers them and they are convenient; every actor, subsystem and RPC is written as if the server were a separate process on another machine, and it is tested that way from step 1 with Play In Editor's dedicated server option. The engine's actor model already is this posture: AGameModeBase exists only on the server, properties replicate one way, Server RPCs need an owning connection, movement is server-corrected. The work is to never undermine it, not to build it. ``` Why this and not the earlier project's listen server with a relay: a host-authoritative peer is the one topology that cannot grow. It ties the simulation to a player's machine, makes the host able to cheat freely, and turns every later step toward a persistent shared world into a rewrite. Choosing the server posture now costs nothing (the engine defaults to it) and closes no door. ## The authority table The rollup. Each feature spec carries its own rows and the two must agree. | State | Authority | Mechanism | | --- | --- | --- | | Player movement, jump, sprint, crouch | Server, owner predicts | Character movement's prediction and correction | | Look pitch and head yaw | Owner writes | Replicated bytes, for the rig only; the server never reads them to decide anything | | Camera | Local only | Never replicated, saved or read by the server | | Ability activation | Client requests, server decides | GAS prediction keys; the owner's animation starts immediately | | Damage, health, downed, dead | Server | The one execution; replicated attributes and tags | | Hit detection | Server, using the owner's replicated aim | A little client trust in the aim; fine in cooperative play | | Cooldowns | Server, client predicts | Cooldown effects; the HUD shows the prediction and takes the correction | | Enemy brains and spawning | Server | StateTree ticks on the server; bodies replicate | | Interaction | Client requests, server re-validates reach and permission | One RPC on the player's own component | | Held objects | Server | Attached to the carrier, `CarriedBy` replicated; no physics while held | | Dropped and thrown objects | Server | Server physics, replicated movement, predictive interpolation | | Station state, buffers, operators | Server | Replicated properties | | Crafting previews | Client computes | Hypotheticals; a wrong preview is a UI bug, never an exploit | | Crafting verdicts and commits | Server | The same pure function, on the server, is the only one that counts | | Activity inputs | Server | RPCs; the anvil strike is timestamped | | Attributes and statuses on a body | Server; effects from a body's own abilities predicted | The stat block replicates; a server-applied speed change costs one small correction, see [Stats.md](Stats.md) | | Player identity, class, known discoveries | Server | On the player state; replicated to the owner | | Session membership | Server | Game mode and game state | ## Three responsiveness tiers Several players in one room, all touching the same physical objects. The question is what each may do instantly and what has to wait for the server, and one test assigns the tier: **does a rollback have a visible victim?** ``` TIER 1: PURELY LOCAL, never networked authority camera and look, interaction targeting and prompts, every crafting preview, HUD, and your own movement through the engine's prediction. Remote players interpolate; head and spine aim replicate so gaze survives. TIER 2: PREDICT THE ANIMATION, NOT THE OWNERSHIP pick up, insert, take output, equip, hand over, fire a projectile. The reach or throw ANIMATION starts instantly and locally. The object changes hands when the server confirms. Predicting your own arm costs nothing when wrong. Predicting who got the last ingot costs a snatch-back the whole room can see, and with substances as individual contested objects that case is frequent, not theoretical. TIER 3: TIMESTAMPED AND LAG-COMPENSATED scored activity inputs: the anvil strike, and nothing else today. The client stamps the input; the server evaluates it against what was true then. Because the zone's radius is a deterministic function of time, that is arithmetic, not a rewind buffer: radius(clientTime), clamped to a 250 ms window, never scoring better than the best achievable at the earliest legitimate arrival. ``` **Standing rule:** no rollback of world state a player can already see. If a design would need one, move the decision to the server and cover the round trip with animation (tier 2), or make it a pure function of time (tier 3). ## Ask, then show Every commit-shaped moment in the game (an item assembled, a substance processed, a weapon equipped, later a purchase or a payout) already has a beat of feedback: the assembled sword materialising on the bench, the ingot coming out of the bed. That beat is the latency budget. A server round trip fits inside it with room to spare, so the client asks, waits for the commit, and then shows the result. No prediction, no reconciliation path, no possibility of a corrected number. **Never show a number as final before it is committed.** A UI that does not lie needs no reconciliation. ## Posture This is a game played with friends, not a tick-perfect competition. Nothing here is adversarial, ranked or decided by milliseconds, so the bar is "feels good to a few people in a room", and anything argued for on competitive integrity grounds is arguing from a genre this game is not in. | Not building | Why not | | --- | --- | | Rollback netcode or deterministic lockstep | Solves fighting-game and RTS problems; this is a cooperative sim | | High tick rates | 20 to 30 Hz is ample when one input in the game is timing-scored | | Client prediction beyond own movement and animation | Every further step buys responsiveness we do not need and costs rollbacks players can see | | Lag compensation anywhere but the anvil | Nothing else scores timing | | An anti-cheat arms race | Server authority removes the motive: no PvP, nothing to win by cheating but spoiling your own game | The tie-breaker when a choice is close: prefer the option that fails gracefully and legibly over the one that is technically optimal. Rejections explain themselves, nothing is destroyed silently, a stalled station says why. ## Prediction hygiene - Test every replicated feature with the editor's network emulation at 100 ms and 5 % loss before calling it done, and `p.NetShowCorrections 1` on. A correction you can see is a bug in a saved move or an ability's prediction, not a fact of life. - Server RPCs go through components on actors the client owns: the player's pawn, player state and controller. Never through a prop. An RPC on an unowned actor silently drops. - Every `Server` RPC validates its inputs and its instigator. `WithValidation` is not enough; the body re-checks reach, permission and state, because the client's claim is a claim. - Replicated arrays with per-element churn (buffers, hands, a future inventory) use `FFastArraySerializer`, not a plain `TArray`, so a change sends the element and not the array. - Actors at rest are dormant (`DORM_DormantAll`, flushed on change). A room of two hundred ingots must cost nothing while nobody touches them. - Relevancy is by distance from day one (`NetCullDistanceSquared` on every replicated actor that is not a player). A world larger than one room cannot afford everything being relevant to everyone, and a rule that exists from the first actor is free. - Never do per-client work on the server that scales with the square of the player count. If a system needs every player to know about every player, that is a design flag. ## Sessions There is one session shape: a server running a map, players joining it. The game mode handles login, spawns a `APlayerCharacter` per controller at a spawn point and re-spawns on death. Party size is the game state's player count and is read by anything that scales (the encounter budget) and stamped onto every telemetry envelope. Steam, lobbies, invitations and matchmaking are not built in these steps and are not designed here. The earlier project's Steam lobby and friends-list join are in [`../Ideas.md`](../Ideas.md); when they arrive they sit in front of the session and never inside gameplay. **Two instances on one machine:** Play In Editor with two clients and the dedicated server option is the everyday test. A packaged server (`Server` target) needs the engine built from source and arrives when the first packaged build does; nothing in the code changes for it. ## Identity ``` [DECIDED] A player is an identity that outlives a body, a session and a server. Everything about a player that would matter tomorrow lives on the player state or behind the persistence provider, never on the character actor. ``` Concretely: `APlayerState` carries a `FGuid PlayerId` minted by the server on first login (hashed for telemetry, never a platform id or a name), the class kit, the ability system component with its attributes and cooldowns, known characteristics, and later progression. The character is a body the world lends the player. A body dying, a map changing or a player reconnecting never loses any of it. ## Persistence ``` [DECIDED] Persistence is behind one provider interface with a local-file implementation. Gameplay writes records to the provider and never to disk, so a service-backed provider is a second implementation and not a refactor. ``` ```cpp // Source/Core/Persistence/PersistenceProvider.h class IPersistenceProvider { public: virtual ~IPersistenceProvider() = default; virtual bool Load(const FString& Key, FString& OutJson) = 0; // records are JSON snapshots, idempotent, never deltas virtual bool Save(const FString& Key, const FString& Json) = 0; // atomic: temp file then move virtual void Remove(const FString& Key) = 0; }; // FLocalFilePersistenceProvider writes under FPaths::ProjectSavedDir()/Records/. Registered on the // persistence game-instance subsystem at startup. Nothing is persisted in steps 1 to 15; the seam exists so the // first thing that is (a player's class, a crafted sword) does not choose the file system by accident. ``` Record rules, from the first record: identity is `FGuid`; content is referenced by primary asset id; every record carries a `schema_version` with an adjacent migration function and a monotonic `revision`; derived fields are recomputed on load; save-facing records are separate types from runtime instances. ## Telemetry over the wire Every peer emits its own events. There is no forwarding to the server, because the interesting questions (did this client's frame rate drop, which player did the shoving, did this preview disagree with the verdict) are per-peer by nature. `is_server` and `player_id` on the envelope let a run be reassembled later. A `session_id` is minted by the server and adopted by every client on join, so one session's events are one session and not four. ## Tests - Functional, run with emulation on: two clients see the same held object on a third's body; a request from beyond reach is refused; an ability activated at 100 ms shows no visible correction; a thrown object lands within tolerance on every peer; the anvil's strike scores the same for a client at 0 ms and one at 150 ms who pressed at the same moment. ## Open questions - **Q1. Reconnect.** A dropped player should reconnect into the same session with the world exactly as it continued without them. The identity decision makes it possible; the session shape does not yet do it. Write the rule down before it is discovered in a playtest. - **Q2. Voice.** Proximity voice is a large part of what makes a room of friends a room. Not in these steps; parked in Ideas. - **Q3. Seamless travel.** One map today. Travelling between maps with the party and the player state intact is the engine's seamless travel and arrives with the second map. - **Q4. Replication at scale.** The Replication Graph and the newer Iris system exist for worlds with many actors and many players. Not now; the dormancy and relevancy rules above are what keep the option cheap.