Tactical Action · Architecture

Game Modes & Live Service

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

9sections12 minread1diagram

On this page

V4 is six genuinely different games — a Rainbow-Six breach, a Hitman sandbox, a Commandos squad-tactics puzzle, a Black-Myth boss duel, an Age-of-Empires ladder, a couch-co-op Contra run — that share one roster, one perception model, and one online backbone. Nothing fuses that catalogue into a shippable product unless a player can go from "pick a mode" to "a match is running" without each genre forking the engine, and unless the game keeps growing for years after launch through seasons, battle passes, ranked ladders, and DLC operators. Those two jobs — mode orchestration and live service — are the subject of this page. The seam that makes the first work is the V4Modes module: a pair of game-instance subsystems plus a real UGameFeatureAction that discovers ruleset plugins on disk, streams their assets, and registers them as switchable modes. The seam that makes the second work is UV4LiveServiceSubsystem composing hand-authored JSON manifests into season/battle-pass/ranked/calendar data, backed by a Rust online-services crate that mounts the §56 service surface. This page explains how both are wired, what is real C++/Rust on disk versus authored-but- uncooked content, and where the shipped code is honestly simpler than the architecture prose. The hub for the V4 architecture set is ../V4_ARCHITECTURE.md.

The design choice underneath everything is the same one the rest of V4 follows: a "mode" is data plus a load plan, not a C++ subclass. A mode is an FV4ModeDefinition value, and a ruleset is a GameFeature plugin discovered from a JSON stand-in. That keeps the mode registry exhaustively unit-testable headless — long before any map is cooked — and it keeps live-service content (cosmetics, DLC, events) flowing through the same plugin boundary that the per-cell rulesets already use, never touching the deterministic gameplay core.

What ships, honestly#

The mode-orchestration machinery is real, compiled, and tested, and it goes one engine-integration step further than its V2 ancestor. UV4ModeSubsystem (V4/ue/Source/V4Modes/Private/V4ModeSubsystem.cpp, 245 lines) implements registration, lookup, a four-result transition, weighted quick-play selection, and a local-multiplayer admission gate as ordinary C++. UV4ModeBootstrapSubsystem (268 lines) scans the plugins directory for mode stand-ins and registers them. UV4GameFeatureAction_ActivateModeAssets (200 lines) is a genuine UGameFeatureAction subclass that performs real async asset streaming through UAssetManager on activation — the exact boundary V2's mode module stopped short of. The catalogue on disk is substantial: 34 *.uplugin GameFeature descriptors under V4/ue/Plugins/ (e.g. V4Mode_Tactical_R6Modern, V4Mode_Stealth_Hitman, V4Mode_RTS_Asymmetric, V4DLC_Operator_Season01_Nyx), most with their own Source/ module, *.Build.cs, and a compiled Binaries/Linux/libUnrealEditor-*.so. Two automation specs pin it: V4/ue/Source/V4Tests/Private/V4ModesTests/ModeSpec.cpp (89 TestTrue/TestEqual assertions over the registry, bootstrap, quick-play, and GameFeature action) and V4LiveServiceTests/LiveServiceSpec.cpp (84 assertions), alongside per-plugin specs such as R6ModernSpec.cpp (103) and ReplayModeSpec.cpp (123) inside the ~92-file V4Tests suite.

The live-service subsystem is real domain code, not a manifest reader that returns its input: UV4LiveServiceSubsystem (V4/ue/Source/V4OnlineServices/Private/V4LiveServiceSubsystem.cpp, 1116 lines) exposes ~40 BlueprintPure builders and validators for season cadence, battle passes, cell-themed cosmetics, operator/mission DLC, the live calendar, ranked ladders, partnerships, and signature events. The Rust backend is real too: apps/v4/online-services is one Axum crate whose service_router() (src/lib.rs:24) nests 20 sub-routers onto tested domain services — OAuth+PKCE login, Glicko-2 matchmaking, session lifecycle, the replay vault, leaderboards, moderation, compliance, community, store, partnerships, and esports brackets.

Four honest caveats matter. First, the content is uncooked. A scan of the 34 plugins finds 170 *.uasset.v4asset.json JSON stand-ins and zero binary .uasset — V4 represents every asset and level as a JSON descriptor under the .uasset.v4asset.json / .umap.v4asset.json convention. The plugin scaffolding, per-mode C++ (e.g. V4R6ModernGadgetCatalog.cpp), and data tables (DT_Operator_R6Modern.json) are genuinely on disk; the cooked binaries are not. Second, the shipped mode "state machine" is flatter than the prose. The architecture's MainMenu → ModeSelect → … → MatchResult diagram (§Mode-State Machine) is design intent; V4ModeTypes.h encodes only a current-mode pointer and EV4ModeTransitionResult (four values: Succeeded, AlreadyActive, UnknownMode, Blocked). Third, the 25-row service-inventory table is one crate, not 25 deployments, and its state is in-memory Arc<Mutex<T>>, not the PostgreSQL/Redis/ClickHouse the Data Stores section names — those are the deployment target. Fourth, a stale-doc correction: the architecture's 2026-06-12 note that "only the login router mounts real routes" is now outdated — the shipped src/http.rs mounts real REST routes onto every service (matchmaking /tickets+/find, sessions /allocate+/transition, replays /star, …). Code is the authority.

The mode-definition data model#

Everything starts from FV4ModeDefinition (V4/ue/Source/V4Modes/Public/V4ModeTypes.h:17). It is deliberately lean — five fields, not V2's twenty-odd:

  • ModeId (FName) — the registry key; an empty id is rejected at registration.
  • DisplayName (FText) — localized label.
  • GameFeaturePluginURL (FString) — the plugin the mode activates.
  • EntryMap (TSoftObjectPtr<UWorld>) — the level to open, by soft path.
  • ActivationAssets (TArray<FSoftObjectPath>) — assets streamed on activation.

There is no NetworkModel/SaveModel enum carried on the definition the way V2 does it; in V4 those concerns live in the cell module and the online subsystems, and the mode definition stays a thin routing record. EV4ModeTransitionResult (:8) and FV4QuickPlayEntry (:56, an id + clamped Weight + bEnabled flag) complete the type surface. This minimalism is the point: the registry is a router, and everything heavier hangs off the plugin it names.

Mode discovery, registration & activation#

The interesting architecture is the three-stage pipeline that turns an on-disk plugin tree into switchable, registered modes. It is encoded across three files, and it is the part of V4 that crosses the GameFeature engine boundary V2 left as a recorded "step."

flowchart TD subgraph Disk["V4/ue/Plugins/*"] U[*.uplugin descriptor] S[Content/**/*.uasset.v4asset.json stand-in] end Boot[UV4ModeBootstrapSubsystem::Initialize] -->|FindFilesRecursive| S Boot -->|parse modeId/displayName/entryMap| Desc[FV4ModeStandInDescriptor] Desc --> Action[UV4GameFeatureAction_ActivateModeAssets] Action -->|OnGameFeatureActivating| Stream[UAssetManager async load] Action -->|RegisterModeWithSubsystem| Reg[UV4ModeSubsystem::RegisterMode] Reg --> Map[(TMap ModeId to FV4ModeDefinition)] Player[AV4ModeRoutingActor / QuickPlay] -->|TransitionToMode| Map Map --> Result{EV4ModeTransitionResult} Result -->|Succeeded| Cur[CurrentModeId set + OnModeTransitionCompleted] Result -->|UnknownMode / AlreadyActive| Broadcast[delegate broadcast, no state change]

Stage one — discovery. On Initialize, UV4ModeBootstrapSubsystem (V4ModeBootstrapSubsystem.cpp:51) walks FPaths::ProjectPluginsDir(), finds each *.uplugin, and recursively collects *.uasset.v4asset.json stand-ins under its Content/. ParseModeStandIn (:137) deserializes each with FJsonSerializer, keeps only files carrying a modeId (catalogs and charts share the suffix and are skipped), derives the mount root and object path, and reads displayName, contentRoot, entryMap, and maxPlayers. Duplicate ids are de-duplicated with a logged warning, and the descriptor list is sorted for determinism. This is real file I/O and JSON parsing, not a hardcoded roster.

Stage two — the GameFeature action. Each descriptor becomes a UV4GameFeatureAction_ActivateModeAssets. Its lifecycle methods are real overrides of the engine's UGameFeatureAction contract: OnGameFeatureActivating (V4GameFeatureAction_ActivateModeAssets.cpp:25) issues a genuine UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(...) at AsyncLoadHighPriority for every valid ActivationAssets path, registers the mode with every live game instance's UV4ModeSubsystem, and subscribes to FWorldDelegates::OnStartGameInstance so late-starting instances still get the mode. OnGameFeatureDeactivating unregisters and releases the streamable handle; FlushAssetLoad/GetLoadedAssetCount (:114) let a test block on and count the load. This is the seam V2's ActivateMode explicitly stopped at ("records an ActivateGameFeature step … never calls UGameFeaturesSubsystem"). V4 actually drives the streamable manager and the GameFeature callbacks. The honest limit: with no cooked .uasset on disk, the soft paths resolve to nothing at runtime — but the loading machinery, handle lifecycle, and counting are real and pinned by ModeSpec.cpp.

Stage three — the registry. UV4ModeSubsystem::RegisterMode (:15) stores the definition by id (rejecting an empty id); TransitionToMode (:44) is the switch. It returns UnknownMode for an unregistered id, AlreadyActive if you re-select the current mode (still echoing the plugin URL), and otherwise CompleteTransition sets CurrentModeId, records LastTransition, and broadcasts OnModeTransitionCompleted so UI and telemetry follow the switch without polling. UnregisterMode refuses to remove the currently-active mode — the same self-protection V2's registry has. AV4ModeRoutingActor (V4ModeRoutingActor.cpp:11) is the in-level entry point: a placeable actor with RouteToMode, RouteToConfiguredMode, and RouteToQuickPlay, each resolving the subsystem off the game instance and returning success only on a Succeeded transition.

Quick play & deterministic mode selection#

Quick play is a deterministic weighted lottery, which matters because V4 shares daily-mission and playlist seeds across the community (see ./world-streaming-procgen.md on seed-identical generation). The algorithm appears twice — UV4ModeSubsystem::SelectQuickPlayMode (V4ModeSubsystem.cpp:193) and the standalone UV4QuickPlayPlaylistRunner::SelectMode (V4QuickPlayPlaylistRunner.cpp:10) — and both compute the same thing: sum the Weight of every enabled entry whose mode is registered, take Roll = FMath::Abs(Seed) % TotalWeight, then walk the playlist subtracting weights until Roll lands in an entry's band. Same seed, same registered set → same mode, every time. The runner exists so a UI flow can preview a selection without committing a transition, while RunQuickPlay (:101) does both: select, then TransitionToMode, returning true only if the transition succeeds. It is a small, real algorithm whose test would fail on any non-deterministic or hardcoded return.

Local multiplayer & couch/LAN admission#

Split-screen, hot-seat, and LAN skirmish are modeled as a policy gate, not a separate runtime. FV4LocalMultiplayerPolicy (V4/ue/Source/V4Modes/Public/V4LocalMultiplayer.h:50) binds an ExperienceId to a BackingModeId, an EV4LocalSessionKind (SplitScreen/HotSeat/LANSkirmish), an EV4LocalInputMode (SeparateDevices/SharedKeyboardAlternation/LANPeerInputs), a min/max local-player band or explicit SupportedPlayerCounts, console/PC support flags, and a bBackendConnectionRequired switch. CanStartLocalMultiplayerSession (V4ModeSubsystem.cpp:112) is a real fail-closed predicate: it rejects an unknown experience, an unsupported player count, the wrong platform class, or — crucially — a backend-required session when the backend is unavailable. That last clause is the honest seam between a couch session and the online services: a LAN skirmish that needs matchmaking or anti-cheat will refuse to start offline rather than fake a connection. EV4SplitScreenLayout and FV4LocalViewportRect carry the viewport geometry for one-to-four-player grids.

Live service: seasons, battle pass & the calendar#

UV4LiveServiceSubsystem (in V4OnlineServices) is where post-launch content becomes runtime data. It is a pure composition layer over hand-authored JSON manifests, and the composition is real arithmetic, not pass-through. Season cadence: BuildSeasonOneCadence and the predicates IsSeasonActiveDay, IsPreSeasonResetDay, RequiresPlacementMatchesDuringPreSeason, and IsMidSeasonBalancePatchDay encode the 90-day season with a 10-day pre-season reset, 5 placement matches, and a mid-season balance patch on day 45 — the same constants the Rust launch_season_cadence() returns (online-services/src/lib.rs:645), so client and server agree. Battle pass: BuildBattlePassTracks mints free and premium tracks at 1000 XP/tier (V4LiveServiceSubsystem.cpp:5). Calendar: BuildLiveServiceThemeWindows and ResolveLiveServiceThemeForMonth mirror V4/liveops/live-service-calendar.json's five recurring windows (Winter Operation, Lunar New Year, Spring, Summer Heat, Harvest) through a real month-in-window test, V4MonthInThemeWindow (:75), that handles the December→February wraparound correctly. BuildAnniversaryEvents, IsAnniversaryLoginBonusActive (a MM*100+DD range test, :90), BuildAnnualCharityDrives (one 100%-proceeds drive per year, validated by HasOneCharityDriveForYear), and BuildPlatformCosmeticTieIns (the six platform-family cosmetic tie-ins in the manifest, each with an approval ticket and gameplayStatsAffected: false) round out the calendar. The hard competitive line is enforced in the data itself: every cosmetic tie-in asserts no gameplay effect, and cause-led collaborations route 100% of proceeds through the store ledger.

DLC rides the same GameFeature boundary as the rulesets: BuildOperatorDLCDefinitions/BuildMissionDLCDefinitions produce FV4DLCGameFeatureDefinitions whose GameFeaturePluginURL is plugin://<PluginName> (V4MakeDLC, :34), ValidateMissionPriceTier bounds the price, and IsOperatorFreeForPremiumPass keeps the free/paid split clean. The four V4DLC_* plugins already on disk (V4DLC_Operator_Season01_Nyx, V4DLC_Campaign_Season01_Blacksite, two ADR community/parody packs) are the shipped scaffolding for that pipeline.

Ranked operations & signature events#

Ranked is its own manifest, V4/liveops/ranked-season-architecture.json, surfaced by BuildRankedLadderDefinitions over the five year-1 ladders (Ranked.Tactical.R6Modern, Ranked.Stealth.SpiesVsMercs, Ranked.RTS.Asymmetric, Ranked.RTS.Historical, Ranked.ARPG.BossRush). The rank-decay rule is real, small logic, not a flag: RequiresRankDecay (V4LiveServiceSubsystem.cpp:989) returns IsDiamondOrHigherRank(RankId) && DaysSinceLastRankedMatch >= 14, so Diamond and above decay after 14 idle days while Platinum and below never do — and ValidateRankedSeasonArchitecture pins exactly that (Diamond@14 true, Grandmaster@21 true, Platinum@30 false, :1113). BuildRankedRewardBundles, BuildMMRDistributionHistograms (the public /ranked MMR histograms), and BuildSeasonalBalanceRetrospectives complete the ranked surface.

Signature event modes are the per-cell evergreen rulesets in V4/modes/signature-modes.json — ten of them, each binding a signatureModeId to a baseModeId, a cell, a stand-in asset, and a requiredFeatures list (Hitman Elusive Targets' WeeklyOneShotContracts/NoRetry/CommunityLeaderboard, Raven-Shield Anniversary's Original2003MissionFlow, CoD-Zombies horde defense, StarCraft co-op commanders). BuildSignatureEventModes and ValidateSignatureEventModes load and check them; the limited-run event modes (Halloween Night Shift, Holiday Hometown Skirmish, Pride Signal Colors, Esports All-Stars) come from DA_SignatureEventModes. Two further mode manifests round out the catalogue: arcade-mini-game-suite.json and karaoke-rhythm-mode.json.

The Rust online-services backbone#

The §56 backend lives in apps/v4/, which on disk is three crates — online-services, telemetry-ingest, and a shared library — not 25 separate deployments. online-services/src/lib.rs is the spine: service_router() nests 20 sub-routers, and service_names() enumerates the 25 logical services the table describes. The domain logic behind each is real and unit-tested:

  • Login (oauth.rs, lib.rs:311) — OAuth 2.0 Authorization-Code + PKCE (pkce_code_challenge_s256), platform identity providers (EOS/PSN/XBL/NN/Steam/ Apple/Google), and single-use refresh-token rotation with replay defense (redeem_refresh_token, :341, rejecting a malformed or already-consumed token).
  • Matchmaking (matchmaking.rs) — a Glicko-2 fairness queue with a hard window, FAIR_MATCH_EXPECTED_SCORE_WINDOW = 0.15, solo-preferred priority, and shared-region QoS selection. The Glicko-2 E-function (glicko2_expected_score, lib.rs:545) carries the correct q = ln(10)/400 scale factor — a real bug fixed 2026-06-12 when the missing factor was flattening every pairing toward even.
  • Sessions (lib.rs:672) — an allocated → starting → in_progress → completed lifecycle state machine with aborted reachable from every non-terminal state, enforced by valid_transitions.
  • Replay vault (lib.rs:774) — 14-day default retention, lifetime retention for starred replays, a 500-starred-per-account cap, expiry computation, and an account-deletion scrub that anonymizes pawn tracks. SpeedrunGhostArchive accepts only server-verified submissions and preserves supersession lineage.
  • Moderation/community/compliance/store/esports — content-scan thresholds (nudity ≥85 block / ≥60 review, Lilith rights-cascade), a six-state Player-of-the-Week curation flow, Challonge/Start.gg bracket clients (brackets.rs), and a WebRTC voice SFU (sfu.rs).

telemetry-ingest is the separate event-ingest crate (256 KiB batch cap, 30-second flush). The honest framing: this is real, tested domain logic with real Axum routes over in-memory state — the PostgreSQL/Redis/ClickHouse/S3 stores named in the architecture are the production target, and persistence is the subject of ./online-services-persistence.md.

Where this connects#

The mode registry is the orchestration layer almost every other V4 system hangs off. The cells it activates and the rulesets layered on them are detailed in ./per-cell-deep-dives.md; the GameFeature hosting model is in ./high-level-architecture.md. The network authority and lockstep/replication determinism that a mode's net model implies live in ./networking-determinism.md; the World-Partition streaming and PCG seeding behind quick-play and daily missions are in ./world-streaming-procgen.md; and the accounts, ledger, save sync, ranked MMR, and replay storage the live-service subsystem and the Rust crate read from are in ./online-services-persistence.md. For the module map V4Modes and V4OnlineServices sit inside, see ./glossary.md; for the target-artifact and JSON-stand-in conventions that make "the plugin exists, the binary doesn't" a documented choice rather than a gap, see ./product-promise.md. The catalogue index for the whole set is ../V4_ARCHITECTURE.md.