V4 is one universe wearing six bodies — a tactical FPS cell, a stealth/Hitman
cell, an asymmetric RTS cell, a Wukong-style ARPG cell, a top-down tactics cell,
and the shared Battle Hub that stitches them together. The promise that makes
that a universe rather than six unrelated games is identity-shaped: one
account, one operator roster, one currency ledger, one progression record, every
cell, every platform. A confirmed kill in the Hitman cell, a ranked win in the
tactical cell, and a unique boss clear in the ARPG cell all have to land in the
same profile, mint the same kind of server-validated ledger entry, and survive a
platform switch from PS5 to PC without a player losing a single cosmetic. That
is what this page is about: the online backbone — the client-side
V4OnlineServices subsystems and the Rust service tier under apps/v4 that
back them — and persistence — the V4Persistence module that owns
save-game, profile, the currency ledger, contract progress, and the Hitman
persistent world. The deterministic match runtime these services wrap lives one
layer down in Networking & Determinism; the
seasonal ranked operations and DLC cadence layered on top are in
Game Modes & Live Service. The section hub is
../V4_ARCHITECTURE.md.
What ships, honestly#
The split is clean and worth stating before the detail. Two tiers are real code, and a small set of seams are honestly labelled as deployment substrate rather than wired integrations.
The client tier is real Unreal C++. V4/ue/Source/V4OnlineServices ships
fifteen UGameInstanceSubsystems — login, matchmaking, session, presence,
friends, party, leaderboard, replay-upload, crossplay, live-service, moderation,
compliance, online-contract, and contract-schedule — over a typed surface
(V4OnlineTypes.h), and its V4OnlineServices.Build.cs really declares the
OnlineSubsystem, OnlineServicesInterface, OnlineServicesCommon, and
EOSShared dependencies that make the EOS abstraction load-bearing rather than
decorative. V4/ue/Source/V4Persistence ships nine more subsystems over a 20 KB
typed model (V4PersistenceTypes.h): save-game, profile, currency ledger,
save-sync, save-migration, replay-store, contract-progress, and the Hitman
persistent-world subsystem.
The service tier is real Rust + Axum. apps/v4/online-services is a
v4-online-services crate whose service-manifest.json enumerates 25
services and whose service_router() (in src/lib.rs) nests every one of
them over genuine, tested service structs — Glicko-2 matchmaking, OAuth2 + PKCE
login, replay retention, leaderboards, moderation, compliance, store, esports,
and the rest. The domain logic is not CRUD: it is a backtracking exact-fill
matchmaker, a dependency-free SHA-256/PKCE implementation, a replay-vault
retention calculus, and a right-to-be-forgotten scrubber, each carrying inline
#[cfg(test)] cases that assert computed answers (a Glicko-2 fairness window, a
region that minimises worst-case QoS, a 14-day expiry boundary).
Three honest labels travel with the rest of the page:
- The shipped router is wider than the architecture note.
V4_ARCHITECTURE.mdcarries a 2026-06-12 note that "only the login router mounts real routes today." That note is now stale:service_router()inlib.rs:24-47nests all 20 service routers, andsrc/http.rsbacks each with realArc<Mutex<Service>>state and real handlers. The login router is no longer the exception. - The services hold in-memory state. Each Rust service keeps a
BTreeMapbehind anArc<Mutex<…>>; the PostgreSQL / Redis / ClickHouse / S3 stores the architecture names are the specified deployment substrate, not yet wired into these crates. The reference implementations are real and tested; the database backends are the next integration step, labelled as such. - V4 does not ride the
@oshunplane. Unlike V2's event-bus-native backbone, V4's services are a self-contained Rust workspace (v4_shared::ServiceName). There is no@oshun/event-busor@oshun/identitycomposition inapps/v4,V4/store,V4/community, orV4/ue— the only@oshunstring anywhere in V4 is inside a vendored Next.js build artifact. Where V2 publishes cross-domain events, V4 keeps its own service graph.
The online backbone#
Client subsystems and the EOS abstraction#
Gameplay code never branches on platform. Every menu, lobby, and matchmaking
screen calls a V4OnlineServices subsystem whose types speak one enum —
EV4OnlinePlatform { EOS, PSN, XBL, NN, Steam, Apple, Google } — and the
platform-specific online stacks sit behind it. UV4LoginSubsystem is a clean
client-side state machine: BeginLogin moves an account to
EV4LoginState::Pending, CompleteLogin resolves it to SignedIn or Failed,
and IsPlatformSupported gates the seven launch providers
(V4LoginSubsystem.cpp). The heavy cryptographic exchange deliberately does not
live in the client — it lives in the Rust login-service, where it can be audited
and rotated server-side.
The crossplay subsystem is the clearest example of V4's honesty discipline.
UV4CrossplaySubsystem::BuildDefaultPlatformIntegrations() returns the launch
contract — the capabilities V4 commits to per platform — and every row is
stamped bMeasured = false with a comment that reads, verbatim, "a target
statement, not a probe … it must never be read as 'the SDK is wired'." The
live truth comes from a separate path: ProbePlatformIntegration calls
IOnlineSubsystem::Get(...) and reads GetIdentityInterface().IsValid(),
GetPurchaseInterface().IsValid(), GetFriendsInterface(),
GetPartyInterface(), and GetSessionInterface() on the real subsystem,
returning a struct with bMeasured = true; when no SDK is registered on the
host, every capability honestly reads false. BuildIntegrationGapReport diffs
contract against probe and emits a line for each "contracted capability not
wired on this host." That is exactly the fail-loud seam the codebase asks for:
the contract never masquerades as the measurement.
Matchmaking and the ranked ladder#
Matchmaking is the backbone's most fairness-sensitive job, and V4 implements it
on both sides of the wire. On the client, UV4MatchmakingSubsystem carries
FV4MatchmakingTicket rows (mode, region, ladder, integer SkillRating) and a
BuildMatchmakingDiagnostic call that powers the
WBP_RankedMatchmakingDiagnostic explanation panel — it computes PlayerMMR,
OpponentAverageMMR, MMRDelta, bWithinTolerance, and a human-readable
explanation so a player can see why a match formed.
The authoritative skill model is server-side and genuine. MatchmakingService
(src/matchmaking.rs) runs a Glicko-2 expected-score fairness window:
glicko2_expected_score applies the q = ln(10)/400 scale factor — with a
checked-in comment recording the 2026-06-12 fix for a missing q that had
flattened every pairing toward even — and a candidate joins the match only when
its expected score against the seed stays within
FAIR_MATCH_EXPECTED_SCORE_WINDOW = 0.15 of 0.5. find_match is a real
depth-first backtracking exact-fill: it seeds on the longest-waiting solo
(prefer_solo_queue, FIFO within party size), recurses through fill_exact to
assemble exactly team_size players from mixed party sizes, and requires one
region every member can play in — then picks the region with the best worst-case
latency. The tests prove the behaviour rather than the shape: a 2400-rated smurf
is excluded from a 1500-lobby and stays queued; a 3+2 backtrack fills a
5-stack while a lone solo waits; and SA wins over EU because its shared
worst-case QoS is 45 ms versus 80 ms.
The ranked ladder on top is seasonal and data-driven. launch_season_cadence()
encodes a 90-day season, a 10-day pre-season reset, 5 placement
matches, and a day-45 mid-season balance patch;
DA_RankedSeasonArchitecture defines the five year-one ladders
(Ranked.Tactical.R6Modern, Ranked.Stealth.SpiesVsMercs,
Ranked.RTS.Asymmetric, Ranked.RTS.Historical, Ranked.ARPG.BossRush) with
Diamond+ 14-day decay and public /ranked MMR histograms. The launch SLO the
architecture commits to is concrete enough to test: matchmaking p99 ≤ 35 s
under 5× expected launch concurrency.
Identity, login, and cross-progression#
LoginService (src/lib.rs) is the account root. Its OAuth surface
(login_router) exposes /oauth/providers, /oauth/authorize, /oauth/token,
and /oauth/refresh, and the flow underneath is real RFC 6749 + RFC 7636 PKCE:
src/oauth.rs carries a dependency-free, FIPS 180-4 SHA-256 used for the S256
code-challenge binding, so the authorization-code exchange is genuinely
cryptographic and deterministically testable rather than a synthesised token
string. Refresh is single-use with replay defense — redeem_refresh_token
validates the presented token against its expected derivation, inserts it into a
consumed_refresh_tokens set, and rejects a replay with
RefreshGrantError::ReplayedRefreshToken before rotating to the next
generation.
Cross-progression roots every player in one first-party account with link/unlink
semantics on both sides of the wire. LoginService::link_platform_account
refuses to bind a provider identity already attached to a different account and
keeps at least one primary link alive on unlink; the in-engine mirror,
UV4CrossplaySubsystem::LinkPlatformAccount / UnlinkPlatformAccount, enforces
the same invariants on FV4FirstPartyAccount and promotes a new primary if the
old one is removed. Currency portability is encoded as policy, not prose:
BuildDefaultCurrencyRules() makes OperatorTokens and EliteTokens
cross-platform soft currencies while CombatPoints is per-platform hard
currency, and CanCurrencyMoveCrossPlatform returns true only for a soft,
cross-platform, non-per-platform currency. Per-surface session policy is real
too — cross_surface_session_policies() gives the companion app, wiki, roadmap,
and spectator portal each its own idle and absolute timeout with rotation on.
Replay vault, right-to-be-forgotten, and speedrun ghosts#
ReplayService is one of the most domain-specific structs in the tree. Uploads
default to a 14-day retention (DEFAULT_REPLAY_RETENTION_DAYS); starring
moves a replay into a lifetime vault capped at 500 per account
(MAX_STARRED_REPLAYS_PER_ACCOUNT), with star_archive_for_account_at refusing
a wrong-owner star and expired_replays computing expiry as
created + retention_days × 86 400 ≤ now while skipping anything starred. The
right-to-be-forgotten path, scrub_deleted_account_from_replay, walks a
replay's participant pawns, clears the deleted account's identifiers, swaps in
the Skin.Anonymous.DeletedAccount skin, and flips privacy_scrubbed — a
deleted player still appears in preserved replays, but only as an anonymous
silhouette. The shared speedrun ghost archive accepts only verified_by_server
submissions carrying replay, input-stream, and checkpoint hashes; a faster ghost
replaces the active route ghost while SpeedrunGhostStatus::Superseded and
superseded_by_ghost_id preserve the full lineage for audits. On the client,
UV4ReplayStoreSubsystem and UV4ReplayUploadSubsystem hold the local
FV4ReplayRecord model and the upload Progress field that the UI watches.
Moderation, anti-cheat, and compliance#
Anti-cheat and moderation deliberately share one tested CaseService: the
anti_cheat_router and moderation_router both delegate to case_router().
Report intake requires real evidence — submit_player_report rejects any report
whose attached recent_gameplay_clip is missing a cloud URI or has zero
duration — and scan_content is a graded classifier, not a coin flip: nudity ≥
85 or violence ≥ 90 blocks distribution, lower scores route to human review, and
a Lilith rights match triggers a takedown cascade plus an appeal. Appeals
resolve at https://support.v4.game/appeal?sanctionId=…. The architecture is
explicit that ML behavioural detection flags for review and that permanent
bans require replay evidence + telemetry evidence + manual review — model output
prioritises a queue, it never silently disciplines. Kernel-mode EAC/BattlEye is
named as the client anti-cheat tier; like V2, that driver is a planned surface,
while the replay-validation integrity check rides the determinism the engine
already guarantees. Compliance is its own tested service: ComplianceService
exposes DSAR export, right-to-delete, statement-of-reasons logging, an age-gate,
and per-region residency, and the in-engine UV4ComplianceSubsystem resolves
regional privacy defaults across EU/UK/US/BR/JP/KR/IN/CN (China voice-off;
CN/KR/IN cosmetic-only economy).
Persistence and save#
Save game and cloud sync#
UV4SaveGameBase is the model behind every cell's save. A slot
(FV4CellSaveSlot) is keyed CellId.SlotId, versioned, and carries its payload
as PayloadJson, a ChapterId, and a set of honest booleans — bAutoSave,
bQuickSave, bCloudSyncQueued, bCloudSynced. The save policy is real:
BuildDefaultAutoSavePolicy is a 60-second interval, PvE-only,
save-on-chapter-transition; ShouldAutoSave enforces all three; and
PutQuickSave flatly refuses a non-PvE quick-save
(if (!bPvE) return false) so competitive modes cannot save-scum.
MigrateSlotsToVersion walks every slot and lifts it to a target version, and
UV4SaveMigrationSubsystem registers FV4SaveMigrationStep from/to pairs so a
schema bump is a registered migration, not an ad-hoc rewrite.
UV4SaveSyncSubsystem owns cross-platform sync metadata:
QueueCloudSyncForSave mints a PlatformSlotId of the form
platform.cell.slot, MarkSynced records a LastSyncUtc and a ConflictToken
for last-writer reconciliation, and IsCloudSyncEnabled gates per slot. Honest
scope note: this subsystem is the sync ledger and conflict-token tracker —
the actual durable transport is the platform-layer OnlineServices SaveGame
surface the architecture names, not bytes this class writes itself.
Profile, operator XP, and the codex#
UV4ProfileSubsystem implements the cross-cell progression math. The model is a
linear 1000 XP per level (DA_ProgressionModel): operator levels run 1-100
and clamp at 99,000 XP, account levels run 1-1000 and clamp at 999,000 XP, and
the anti-grind rule is enforced in code — ClampOperatorDailyXPGrant returns 0
once a day's operator XP reaches the 5000 XP cap, and otherwise clamps the
grant to the remaining headroom. UpsertProfile sanitises on the way in (drops
None keys, floors XP at zero). The codex is genuinely populated:
BuildCodexCategories returns the eight unlockable categories (Operator,
Weapon, Gadget, Map, Faction, Civilization, Ruleset, Vehicle) and
BuildLaunchCodexEntrySamples ships real, authored entries — Cobra's Shadow War
dossier, a modular service rifle's recoil identity, the MH-6 Little Bird's
insertion routes — each with lore text, gallery ids, design commentary, and
account/operator-level gates that CanUnlockCodexEntry checks. XP is
server-authoritative by design: the client reports a result, the server
validates against telemetry, and the grant lands as a ledger entry.
The currency ledger#
UV4CurrencyLedger is a three-tier, audit-trailed ledger.
BuildDefaultCurrencyDefinitions defines Operator Tokens (soft, earned,
cross-platform), Combat Points (hard, purchased, per-platform), and Elite
Tokens (premium, earned through ranked, cross-platform). ApplyTransaction
rejects an unsupported currency or a transaction that would drive the balance
negative, mints a GUID TransactionId, records BalanceAfter, and appends to
an AuditTrail — every movement is reconstructable.
ApplyServerValidatedTransaction adds the integrity gate the architecture
promises: it requires a non-empty ServerValidationToken, demands a
PlatformId for any per-platform currency, and sets bServerValidated. The
cross-platform flag is computed from the currency, not asserted —
bCrossPlatform = CurrencyId != "CombatPoints" — so the per-platform hard
currency can never silently leak across a store boundary.
Cosmetic inventory and the persistent world#
Cosmetics are owned, not just listed. UnlockCosmeticItem validates the
category against the nine launch categories (operator/weapon skins, charms,
banner and calling cards, emotes, stickers, badges, sprays), records a
FV4CosmeticInventoryItem with its SourceId and GrantedAtUtc, and
HasCrossPlatformCosmetic confirms an item travels with the account. The
FV4ProfileState that carries all of this — XP, per-operator XP, unlocked
origin comics and codex entries, the cosmetic inventory, and settings — is the
single object cross-progression syncs.
The most distinctive persistence surface is the Hitman cell's living world.
UV4HitmanPersistentWorldSubsystem records per-profile NPC state
(RecordNpcObserved, RecordNpcKilled with story-mode and primary-target
flags) and decides, via ShouldSpawnNpcForMode, whether a story-dead NPC stays
gone or a Quick Mission resets it (ResetQuickMissionRuntimeState). On top of
that per-player state it builds the community layer the wiki publishes:
RecordSeasonalNpcKill rolls up all-map kills, BuildReadOnlyWorldStateFeed
emits the read-only season snapshot (bReadOnly = true),
BuildWorldStateRippleEvents maps Hitman assassinations to campaign branch
flags, and RecordCommunityRaidContribution / BuildSeasonalWorldBossRaids
drive the async Eclipse Leviathan world-boss. Critically, ripple events carry
bDeterministicBeforeLoad = true: cross-cell hooks resolve before a mission
loads, so live community telemetry can change the fiction around a match but
never the deterministic combat simulation inside it.
Where this connects#
- Down to the match: Networking & Determinism — the deterministic sim the server-side replay-validation integrity check depends on, and the dedicated-server allocation the session-service drives.
- Sideways to live service: Game Modes & Live Service — the seasonal ranked operations, the store and battle-pass entitlements, and the DLC GameFeature cadence that ride this backbone.
- Platform foundations: Persistence & Data — the Postgres / Redis / ClickHouse / object-storage substrate these services are specified to deploy onto, and the account-root and data-residency posture they inherit.