Tactical Action · Architecture

Networking & Determinism

A focused page within the Tactical Action Architecture documentation. The full map and every sibling page live in the Architecture hub.

10sections13 minread1diagram1table

On this page

V4 is not one game with one netcode — it is a tactical-action universe of cells (an RTS, tactical FPS PvP, Battle Royale, twitch shooters, stealth sandboxes, ARPG and co-op campaigns) running on one engine, and the single most consequential networking decision in the project is that each cell picks the netcode model that fits its loop. A 200-pop RTS match cannot afford to replicate every unit's state, so it ships only inputs and trusts every client to reach the same world deterministically. A 5v5 tactical round cannot trust the client with a kill, so it is server-authoritative with lag-compensated hit-reg. A twitch TDM round cannot wait a round-trip to move your own pawn, so it predicts locally and reconciles. A single-player Hitman level needs none of that, but it does need a seedable replay. All four postures, plus the replication graph, dedicated-server allocation, host migration and anti-cheat that sit underneath them, live in one Unreal C++ module: V4Netcode (V4/ue/Source/V4Netcode/, nine .cpp / ten .h plus V4Netcode.Build.cs, which declares Core/CoreUObject/Engine/V4Core/Json and the four networking modules NetworkPrediction, Iris, OnlineSubsystem, ReplicationGraph). The module is real and compiles on the on-box engine — the build tree carries 21 V4Netcode/*.o objects under V4/ue/Intermediate/Build/Linux/x64/UnrealEditor/Development/V4Netcode/. This page is the architecture-side companion for V4's netcode and determinism, part of the Netcode, Modes & World group; the section hub is ../V4_ARCHITECTURE.md.

What ships, honestly#

The netcode logic is real, deterministic-by-construction where it must be, and test-driven. Five automation specs under V4/ue/Source/V4Tests/Private/V4NetcodeTests/ exercise it and assert computed values, not shapes: V4.Netcode.Lockstep.DeterminismAndRecovery, V4.Netcode.RollbackPrediction.{WindowAndFiring,ServerReconcileReplay,TwitchModeWiring}, V4.Netcode.ClientServer.ProvisionReplicationIrisHostMigration, V4.Netcode.HitRegistration.LagCompensation, and V4.Netcode.LocalDeterminism.RngAndPveReplay. They drive a live UV4RTSLockstepSubsystem through resimulation and desync recovery, replay a rollback rewind and check the exact corrected coordinate, rewind a lag-compensated target between interpolated samples, and round-trip the replay codecs.

Three honest qualifications keep the rest of the page at face value.

  • This is a deterministic library, not a wired net driver. There is no FSocket, no ISocketSubsystem, no UFUNCTION(Server/Client/NetMulticast) RPC, and no replicated UPROPERTY / DOREPLIFETIME anywhere in the module. The prediction, reconciliation, hashing, interest-band and budgeting algorithms are implemented and tested; binding them to a live UNetDriver and pushing bytes over a wire is not in this module. (V4NetcodeModule.cpp is an empty IModuleInterface shell — the module is its types and subsystems, not a running service.)
  • Iris and NetworkPrediction are declared dependencies but not actually called. A search of the module finds no Iris API symbols (UReplicationSystem, UE::Net::, FReplicationFragment) and no NetworkPrediction simulation types (TNetworkedSimulation, FNetworkPredictionData). The rollback component is hand-rolled on a plain UActorComponent, and UV4IrisMigrationValidator is a bandwidth-planning data contract (it tallies legacy-vs-Iris bytes per channel), not a live Iris migration. The one real engine-networking integration is UV4ReplicationBandNode : UReplicationGraphNode_ActorList (V4ReplicationBandBudgeter.h:39), which genuinely overrides GatherActorListsForConnection.
  • Determinism is enforced by integer-state hashing and a seeded, order-audited RNG — not by a strict-FP build. Unlike V2 (V2_STRICT_FP=1) or V7's dedicated-realm target (/fp:strict), no V4 target sets any compiler determinism flag (V4.Target.cs and V4TournamentServer.Target.cs set neither /fp:strict nor DETERMINISM). V4's determinism therefore lives in what gets hashed (integers only) and how RNG is consumed (canonical order), described honestly below. The float-math rollback path is safe precisely because it is server-reconciled, never cross-machine lockstep.

The per-cell netcode model#

The architecture monolith's promise — "choose a netcode strategy per cell" — is realized as four concrete code paths, each with its own authority and determinism posture:

Cell / mode Strategy Authority Determinism posture
RTS (Tactics) Deterministic lockstep, 25 Hz Shared (every client re-sims) Integer input-digest + seeded RNG
Tactical FPS PvP (R6/5v5) Client-server + reconcile Server (lag-comp hit-reg) Server-authoritative, float OK
Twitch PvP (TDM, S&D, Spies-vs-Mercs) Rollback-emulated prediction Server (fire authority) Predict-and-confirm, float OK
Battle Royale (Warzone 100p) Client-server + relevancy bands Server Server-authoritative, float OK
PvE / single-player / co-op Local deterministic Local Seeded, serializable replay

The wiring between a mode and its netcode profile is data, not code branches. UV4RollbackPredictionProfileCatalog::LoadDefaultProfiles (V4RollbackEmulatedComponent.cpp:309) registers four twitch profiles keyed by (ModePluginName, RuleSetId)V4Mode_Tactical_CoDMultiplayer.TDM, …SearchAndDestroy (a one-life round), V4Mode_Tactical_CoDWarzone.BattleRoyale (100 players), and V4Mode_Stealth_SpiesVsMercs.Default — each carrying an 8-frame prediction window, a 60 Hz prediction rate, and a ReplicationProfileId that points at the matching replication band set. The test …TwitchModeWiring confirms the catalog resolves S&D as bOneLifeRound and configures a live UV4RollbackEmulatedComponent from the profile.

flowchart TD M[Match starts: which cell?] --> Q{Netcode profile} Q -->|RTS Tactics| LS[Deterministic lockstep 25 Hz] Q -->|Twitch PvP TDM / S&D / SvM| RB[Rollback-emulated prediction] Q -->|Tactical / Battle Royale| CS[Client-server authoritative] Q -->|PvE / single-player| LD[Local deterministic + seeded replay] LS --> H[Chained integer state hash + SplitMix64 RNG] LS --> D{Peer hashes agree?} D -->|no| R[Rollback to frame-N, request authoritative snapshot] RB --> P[8-frame predicted buffer] --> AC{Server confirms?} AC -->|mismatch| RR[Rewind to confirmed frame, replay later inputs] CS --> LC[Lag-compensated hit-reg: rewind target history] --> RG[ReplicationGraph band budgeter]

RTS deterministic lockstep#

The RTS cell is the one place V4 must be bit-deterministic across machines, because no game state is replicated — only inputs are. UV4RTSLockstepSubsystem (a UGameInstanceSubsystem) owns that control plane. It advances on a fixed integer step: TickLockstep(DeltaSeconds, …) (V4RTSLockstepSubsystem.cpp:90) accumulates wall-clock time and emits exactly one frame per 1/TickRateHz (default 25 Hz, 40 ms), so the same input stream produces the same frame count regardless of render rate — the spec pins it: "25 Hz lockstep does not advance before 40 ms" then "advances exactly at 40 ms." Each tick, pending inputs for that frame are gathered (ConsumeInputBroadcastBundle) and run through a single canonical sort (SortInputs, :287) ordering by player id, then command string, then primary/secondary values, then target cell — so two clients that received the same inputs in different network order sort them identically before hashing.

The determinism digest is ComputeDeterministicHash(PreviousHash, Frame, Inputs) (:227). It chains the previous hash with MatchConfigHash, both halves of the 64-bit RngSeed, the frame number, and every sorted input field, folding each with HashCombineFast. The subtle, load-bearing detail is FName hashing: a raw GetTypeHash(FName) hashes the process-local name-table index, which differs between machines, so the subsystem uses StableNameHash (:316) = FCrc::StrCrc32 over the name string, making the digest content-based and stable across peers. The spec asserts the property directly: "Same inputs produce same deterministic hash regardless of arrival order."

Determinism, honestly labeled#

Two precise statements keep this from over-claiming.

First, what the digest actually hashes is the input history, config, and seed — not unit positions. AdvanceSimulationTick chains CurrentStateHash forward over every frame's sorted inputs (:74). That is a genuine desync detector for a lockstep whose only divergence sources are input ordering, config drift, or RNG misuse — but folding the RTS gameplay sim's own unit state into the digest is the RTS simulation's responsibility (the V4RTS module), and is not exercised by this module's tests. The subsystem provides the mechanism (per-frame chained hash, HashIntervalFrames-sampled checkpoints every 100 frames via StoreFrame, GetStateHashSample); the gameplay state it would hash is wired in elsewhere.

Second, the real cross-machine guard is the RNG-consumption audit. Match RNG is a named algorithm — V4SplitMix64 (V4LocalDeterminism.cpp:10), the standard SplitMix64 with the 0x9E3779B97F4A7C15 increment and the two mix constants — seeded per match. Every draw goes through DrawMatchRandomInt(UnitId, Reason, …) (:189), which logs an FV4RngConsumptionRecord with a monotonic draw index, and ValidateRngConsumptionOrder (:208) asserts the log is in canonical (frame, unitId, reason, drawIndex) order. That catches the classic lockstep desync — non-deterministic iteration order consuming the RNG stream differently on two machines — before it diverges the sim. The spec proves both halves: "Per-match Splitmix64 RNG is deterministic" and "RNG consumption audit accepts canonical unit order."

When a peer hash does disagree, CheckRemoteFrameHash flags bDesynced, and RecoverFromDesync (:138) rolls the current frame back to Frame − RecoveryRollbackFrames (default 4), discards confirmed frames and recovery snapshots at or after that point, and applies the authoritative hash — the "desynced client pauses, requests snapshot, replays" recovery the monolith describes. A late joiner is rebuilt with BuildLateJoinerRebuildFrame (:163), which returns the exact recovery snapshot or the nearest earlier one. IsPauseThresholdExceeded enforces the ≤ MaxInputWaitSeconds (3 s) stall before a match is flagged for replay-loss recovery. The whole input stream serializes through UV4RTSLockstepReplaySerializer (header + seed + bundle stream) and re-plays through UV4RTSLockstepReplayPlayer — the monolith's "~1 MB per 30-minute match, input-only" replay format.

Rollback-emulated prediction (twitch PvP)#

For TDM, Search & Destroy and Spies-vs-Mercs, V4 layers prediction-with-confirm on top of the client-server stack — explicitly not full peer-to-peer rollback like a fighting game; the server stays authoritative. UV4RollbackEmulatedComponent predicts the local pawn each frame in SimulatePredictedInputFrame(Frame, MoveInput, FireInput, DeltaSeconds) (V4RollbackEmulatedComponent.cpp:68): it integrates movement (speed-clamped so a diagonal can't out-run a cardinal), records an FV4RollbackInputFrame (move, delta, predicted location and velocity, fire intent) into a ring, and trims the ring to MaxRollbackFrames (default 8 — 133 ms at 60 Hz; the spec asserts "defaults to the documented 8-frame window" and "8 frames at 60 Hz is about 133 ms").

Reconciliation is ReconcileAuthoritativeInputFrame(AuthoritativeFrame, OutCorrection) (:92). It finds the confirmed frame in the buffer; if the predicted location is within ErrorTolerance of the authoritative one, nothing rewinds (cheap path). On a mismatch it snaps that frame to the authoritative position and replays every later buffered input forward from there, recomputing each predicted location and velocity and marking them bReplayedFromCorrection — the rewind-and-replay step, resolved in one call. The spec nails the arithmetic: after predicting to X=30 over three frames, an authoritative frame-1 at X=15 reconciles to a final predicted X=25 with one replayed frame and a CorrectionDelta.X of −5. Firing has its own authority: predicted fires hold a sequence in PendingFireSequences; a server frame that confirms or rejects the fire moves it to confirmed or RejectedFireSequences ("Rejected fire sequence is retained for cosmetic rollback"), so a denied shot can un-play its tracer. This is the layer the prediction-profile catalog configures per twitch mode.

Client-server authority & lag-compensated hit-reg#

Every networked shooter, rollback or not, validates the kill on the server. UV4ServerAuthorityComponent keeps a per-target history ring of FV4LagCompensatedSamples (server time, location, box extent), sorted and trimmed to MaxRewindSeconds (the monolith's ~300 ms clamp) in RecordTargetSample. When a client reports a shot, ValidateServerAuthoritativeHit (V4ServerAuthorityComponent.cpp:51) does four things in order:

  1. Anti-cheat plausibilityIsTraceDirectionPlausible (:95) rejects a trace whose direction diverges from the reported view direction by more than MaxTraceViewAngleDegrees, setting bRejectedByAntiCheat. This is the server's "impossible angle" guard.
  2. RewindFindRewoundSample (:158) clamps the client's fire time into the retained window (flagging bRewindClamped when the shot is older than the window) and interpolates the target's location, rotation and extent between the two bracketing samples. The spec checks the interpolated center lands at X=150 between X=100 and X=200 samples, and that an out-of-window shot clamps to the oldest retained position rather than teleporting the target forward.
  3. Geometry — a line shot uses a slab-method segment-vs-AABB test (SegmentIntersectsExpandedBox, :204); a shotgun-style cone uses an angular test with per-distance slack (ConeIntersectsExpandedBox), both against the box expanded by ExtraTolerance.
  4. PenetrationEvaluatePenetration (:111) walks ordered cover layers, subtracting thickness × resistance energy per layer and hard-blocking on a bHardBlocks surface. The spec drives a 25-energy round through two 5 cm layers (both penetrate, energy 25 → 5) and a steel plate (blocked, surface reported).

Replication graph & bandwidth budgeting#

The Battle Royale and tactical cells partition relevancy through Unreal's ReplicationGraph, and this is where V4 actually touches the engine net path. UV4ReplicationBandNode (V4ReplicationBandBudgeter.cpp:80) subclasses UReplicationGraphNode_ActorList and overrides GatherActorListsForConnection to estimate the band's per-frame cost and gate the real Super:: gather on a budget: if UV4ReplicationBandBudgeter::TrySpend (:25) would exceed the band's MaxBytesPerFrame, the node simply doesn't gather, throttling the lowest-priority actors first. UV4ReplicationGraphProfileCatalog::LoadDefaultProfiles (V4ClientServerNetcode.cpp:216) defines the band sets per match type — the BR profile is the monolith's four-ring A→D model (200 m full → >1500 m dormant), with budgets shrinking 8192 → 1024 bytes/frame and cull distances growing 0 → 120000 cm. The spec confirms "BR band A admits one critical actor" while "BR band D throttles excessive far actors."

The Iris story is the honest one from the top: UV4IrisMigrationValidator (:322) records a per-channel legacy-vs-Iris byte rate, keeps rollback-sensitive channels (movement) on the legacy path, and computes a BandwidthReductionFraction over the rest — the monolith's "~30 % saving." The spec asserts the rollback channel stays legacy and the migration saves ≥ 35 % of non-rollback bandwidth. It is a planning and conformance artifact that proves the policy arithmetic; it is not a call into the real Iris runtime.

Dedicated servers, host migration & anti-cheat#

UV4DedicatedServerRouter::ConfigureDefaultRegions (:70) provisions the monolith's ten regions (NA-East/West, EU-West/East, LATAM, APAC, AU-NZ, ME, Africa, India) with capacities and estimated pings, and AllocateServerForMatchmaking (:100) honors a preferred region when it has headroom, else greedily picks the lowest-ping enabled region with capacity, mints a ServerId and :7777 endpoint, and increments the region's active count (released cleanly by ReleaseAllocation). Host migration is UV4HostMigrationCoordinator::BuildMigrationPlan (:365): it is disabled for dedicated/public rooms and only runs for private rooms, picking the highest HostScore eligible peer (ties broken by freshest ack) and estimating a swap time the ValidateSubTwoSecondSwap gate holds under 2 s — the spec confirms a public room cannot migrate while a private room picks Player.Two (score 65 over 40).

Anti-cheat spans both the router and UV4AntiCheatSubsystem. The router's RouteAntiCheatSignal escalates by severity (≥ 8 → kick + anti-cheat.eac-review, ≥ 4 → anti-cheat.live-review, else telemetry). The subsystem validates an EAC config (kernel-mode required, ranked + BR protected across five platforms), classifies behavior in EvaluateBehavior (V4AntiCheatSubsystem.cpp:88 — impossible-recoil over 20+ shots at ≥ 0.98 control, wallhack pre-aim patterns), computes a clamped TrustScore, defines a ten-tier ban ladder, and — the competitive-integrity piece — ValidateLadderReplay (:59) requires three independently produced integrity hashes (replay, server-state, input); identical hashes are rejected as forged. The live anti-cheat service, matchmaker and replay store these route into are the subject of ./online-services-persistence.md.

World state & the determinism boundary#

Above the per-cell netcode sits a cross-cell world state contract, expressed as data in V4/world-state/. persistent-world-economy-deep.json and world-boss-community-raid.json describe ripple events where one cell's outcome seeds another's — and crucially, each carries an explicit determinismPolicy that respects the netcode boundary: a Hitman kill that unlocks a CoD objective is "Resolved before mission load from signed season state," and an RTS season win that lifts Wukong lore is "read-only area context and does not mutate combat frames." In other words, cross-cell world state is resolved at load boundaries and never injected mid-simulation, so it can never perturb a deterministic lockstep frame or a server-authoritative tick. The manifest is also honest about its own data: a dataStatusNote records that community counters start at zero and that previously published non-zero values "have been removed as fabricated-progress data" — the same fail-loud discipline the netcode module follows.

How it connects#

The netcode module is consumed, not standalone. The mode plugins that pick a prediction profile and a replication band set — and the live-service playlists that route players into them — are detailed in ./game-modes-live-service.md; the (ModePluginName, RuleSetId) keys in the rollback catalog are exactly those modes. The server-authoritative replication of non-combat state — guard suspicion, schedule clocks, crowd panic — is the AI side of the same client-server backbone, distinct from the rollback path, and is covered in ./ai-perception-stealth.md. The dedicated-server allocation, matchmaking, anti-cheat service, ladder-replay validation and persistent-world / save-sync stores that the router and anti-cheat subsystem feed live in ./online-services-persistence.md. The section hub is ../V4_ARCHITECTURE.md.