* Write the Unreal documentation set: design, steps, ideas, decisions and specs Rewrites the design and engineering docs of the two earlier Unity projects (Adventurer Guild, Project Malleable) for an Unreal Engine 5 prototype that builds and proves three things in order: a movement controller, fighting and crafting. Neither earlier core loop is carried; their ideas are catalogued as features with what each needs. - Docs/Design.md: the human summary (pillars, fixed decisions, glossary) - Docs/Steps.md: a ladder of fifteen proofs, the first six in full - Docs/Ideas.md: salvaged ideas from both projects, none scheduled - Docs/Decisions.md: D-01..D-36, open decisions OD-01..OD-08, deferrals - Docs/Spec/: notation, Architecture, Movement, Interaction, Combat, Crafting, Networking, Telemetry, UI, written as Unreal C++ skeletons - CLAUDE.md and README.md for the repository * Add the stat block: one attribute set on every body, every influence an effect Every body (player, enemy, NPC) carries the same UStatBlockAttributeSet, and every outside influence on it is a gameplay effect: base values, buffs, debuffs, ground surfaces, carried weight, gear, being downed. Counters are tags and attributes on the receiver: immunity blocks by tag, resistance scales by attribute, cleanse removes by tag. Same status from several sources: strongest wins; different statuses multiply. - Docs/Spec/Stats.md: the block, effects and their five sources (abilities, areas, surfaces, items, the world), stacking, counters, readers, tests - Movement: the movement component's tagged speed-multiplier map is removed; speed is tuning times the MoveSpeed attribute; the ground surface trace turns mud into an effect; the gym gets mud and ice patches - Combat: the attribute set moves to Stats; enemies and kits carry FStatBlockDefaults; the funnel reads MagicResist and Immune.Damage tags; taunt and mark are statuses - Interaction: carried weight is GE_Encumbered against CarryCapacity - Crafting: class bonuses are the WorkSpeed and WorkQuality attributes; enchantments may grant a holder effect - Steps: step 5 builds the block with mud, a volume and an immunity - Decisions D-37 and D-38; design summary, CLAUDE.md, indexes, worklog
13 KiB
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, what an insert does in 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 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 |
| 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 1on. 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
ServerRPC validates its inputs and its instigator.WithValidationis 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 plainTArray, 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 (
NetCullDistanceSquaredon 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; 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 (<Project>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.
// Source/<Project>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.