Fighting Game · Architecture

Game Modes, Mode Plugins, Training & Replay

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

12sections15 minread1diagram1table

On this page

V2 ships as one fighting game that has to host dozens of distinct experiences — a CPU arcade ladder, a branching story campaign, a 100-player shard hub, a card-collector faction season, a turn-based hex campaign, a 30-entrant Royal Rumble — without any one of them forking the match loop or the save system. The seam that makes that possible is the V2Modes module: a game-instance subsystem that holds a registry of mode definitions, plans how each one is loaded, drives a seven-state lifecycle, and injects per-mode HUD without ever touching the deterministic combat core. The registry is small and real (V2/ue/Source/V2Modes/Public/V2ModeRegistrySubsystem.h, V2/ue/Source/V2Modes/Private/V2ModeRegistrySubsystem.cpp, ~500 lines); the data it governs is enormous. V2ModeTypes.h is a single 347 KB header declaring 135 enum classes and 286 USTRUCTs, and V2ModeCatalog.cpp (~274 KB) is the C++ factory that builds the canonical content for every mode. This is the page that explains how a player goes from "pick Arcade" to "a match is running," how training and replay are layered on top of the same deterministic simulation, and — just as importantly — which parts of that story are shipped C++ versus convention-built path strings pointing at artifacts that do not yet exist on disk. The hub for the V2 architecture set is ../V2_ARCHITECTURE.md.

Why a registry of definitions rather than 52 hand-written game modes? Because V2 treats a "mode" as data plus a load plan, not a subclass. A mode is an FV2ModeDefinition value — an id, a category, a network model, a save model, a player-count band, a Game Feature plugin URL, a primary map path, a dependency list, and a set of HUD injections — and activating it is a deterministic, testable transformation of that value into an ordered list of load steps. That choice is the same discipline the rest of V2 follows (see V2 Product Promise on the target-artifact convention): the registry can be exhaustively unit-tested headless, long before any of the maps, widgets, or Game Feature plugins it names actually exist.

What ships, honestly#

The mode-registry machinery is real, complete, and heavily tested. UV2ModeRegistrySubsystem implements registration, lookup, validation, the load-plan builder, activation/deactivation, the lifecycle delegate, and the Hathor UGC-world bridge as ordinary C++ that compiles into the editor and runs under automation. FV2ModeCatalog::BuildDefaultModeDefinitions() constructs a fixed roster of 52 canonical modes (Modes.Reserve(52) and exactly 52 MakeMode(...) calls, V2ModeCatalog.cpp:6285+), and the catalogue's per-mode content builders (arcade ladders, story chapters, MyGM booking sim, Royal Rumble entry scheduling, the arcade scoring evaluators) contain real domain math, not placeholders. The replay subsystem is equally real: a versioned binary file format with determinism hashes (V2Input), a hand-rolled encoder/decoder, and a replay-takeover-to-practice creator in V2OnlineServices that validates against a live replay store and fails loud when the replay is missing. The single automation spec V2/ue/Source/V2Tests/Automation/Modes.Module.spec.cpp carries 713 TestTrue/TestEqual/TestNotNull assertions over the registry, catalogue, and championship data; Input.Module.spec.cpp references the replay types 156 times.

Three honest caveats matter. First, the per-mode Game Feature plugins do not exist as content on disk. MakeMode synthesizes each definition's GameFeaturePluginURL as Plugins/GameFeatures/V2Mode_<Id>/V2Mode_<Id>.uplugin and its PrimaryMapPath as /Game/V2/Modes/<Id>/L_<Id> (BuildPluginURL/BuildMapPath, V2ModeCatalog.cpp:30-44), but a scan of V2/ue/Plugins finds only three .uplugin files total (V2AICommentary, V2AdaptiveAI, V2AssetLinter) and none named V2Mode_* or V2Event_*. These URLs and map paths are target artifacts in the precise sense V2 Product Promise defines — names the registry can plan against today and content production fills in later. Second, activation records the plan; it does not execute a real GameFeature load. ActivateMode builds the plan, loads any required C++ modules through FModuleManager, and records an ActivateGameFeature step in ExecutedSteps — but it never calls UGameFeaturesSubsystem, never opens a level, and never spawns a widget. It is an honest orchestration model that stops at the engine-integration boundary rather than faking it. Third, a spec/code discrepancy on the replay budget: the architecture prose says "≤ 1 MB / 5-min match," but the shipped FV2ReplayFileCompressionProfile pins TargetMaxBytesPerMinute = 1048576 — 1 MiB per minute, five times looser than the prose. The code is the authority; treat the prose as aspirational. The narrative tables in the monolith (V2Event_Marathon, V2Mode_TimeAttack, …) are likewise design intent, not shipped plugin directories.

The mode-definition data model#

Everything starts from FV2ModeDefinition (V2ModeTypes.h:9037). Its fields are the vocabulary the rest of the system reasons over:

  • CategoryEV2ModeCategory, 14 values: Arcade, Story, Tower, Vault, Hub, OpenWorld, Brawler, Career, Booking, Collector, BattleRoyal, StrategyRpg, Crew, Roguelike. Category drives default HUD injection selection.
  • NetworkModelEV2ModeNetworkModel, 7 values: Offline, RollbackOneVsOne, ClientServerHub, ClientServerMultiFighter, ClientServerOpenWorld, AsyncService, LocalTurnBased. This is the field that decides whether a mode needs the online backbone.
  • PreferredSessionMode — an EV2NetcodeSessionMode (e.g. Rollback, ClientServer, LanTournament) handed to V2Netcode; see Rollback Netcode & Tag Team.
  • SaveModelEV2ModeSaveModel, 8 values: None, Profile, StorySlot, CareerSlot, UniverseSeason, SharedWorld, OnlineService, Vault. Each maps to a slot shape in Live-Ops, Store & Progression.
  • Player bandsMinPlayers/MaxPlayers/MaxSpectators, plus the flags bRequiresOnlineServices, bRequiresProfile, bSupportsReplay, bSupportsRollbackDeterminism, bAvailableAtLaunch.
  • WiringGameFeaturePluginURL, OwningModuleName, PrimaryMapPath, Tags, an array of FV2ModeDependencySpec, and an array of FV2ModeHUDInjectionSpec.

IsValidDefinition() (V2ModeTypes.cpp:10090) is a real gate, not a truthiness check: it rejects a definition with no id, no display name, no DesignSection, no Game Feature URL, no owning module, an inverted player range (MaxPlayers < MinPlayers), a negative spectator cap, or any invalid dependency or HUD injection. RequiresNetworkServices() returns true when bRequiresOnlineServices is set or the network model is any of the three client-server variants or AsyncService — that single predicate is what the load planner uses to refuse an online mode that the caller asked to run offline.

The canonical catalogue — 52 modes#

BuildDefaultModeDefinitions() is the source of truth for what V2 offers. Each entry is one MakeMode(...) call carrying the id, display name, the design-section reference that ties it back to V2_TODOS.md, and the category/net/save tuple. MakeMode (V2ModeCatalog.cpp:1744) fills in the convention-built plugin URL, owning module, and map path, attaches the default dependency set, and asks BuildDefaultHUDInjectionsForMode for category-aware HUD. A representative slice:

Mode id Category Network model Save model Players
Mode.Arcade.Classic Arcade Offline Profile 1–2
Mode.Arcade.GhostBattle Arcade AsyncService OnlineService 1–2
Mode.Story Story Offline StorySlot 1
Mode.Tower.Time Tower AsyncService OnlineService 1–2
Mode.BattleHub Hub ClientServerHub SharedWorld 1–100
Mode.WorldTour OpenWorld ClientServerOpenWorld SharedWorld 1
Mode.WWE.Universe Booking AsyncService UniverseSeason 1–4
Mode.MyFaction Collector AsyncService OnlineService 1–5
Mode.RoyalRumble BattleRoyal ClientServerMultiFighter Profile 2–30
Mode.TagTeam.ThreeVsThree Brawler ClientServerMultiFighter Profile 2–6
Mode.ChessKombat StrategyRpg LocalTurnBased Profile 1–2
Mode.RoguelikeAdventure Roguelike AsyncService Profile 1–2

The full 52 cover three Arcade variants, Story, four Tower/Tournament/Survivor modes, the Krypt vault, Battle Hub and its sub-modes, World Tour and its vehicle/social variants, Tekken Force, Devil Within, the WWE Universe/Showcase/ Slammys trio, four career modes, MyGM, MyFACTION, MyRISE, four Royal Rumble / Battle Royale entries (including three 100-player variants up to Mode.BattleRoyale.Hybrid100), tag-team 2v2/3v3, UFC Fight Night booking, the SoulCalibur strategy-RPG suite, Def Jam story and tag battle, Crew/Faction, a roguelike, and three "specialty" rulesets (boxing sim, Bushido Blade one-hit, MK one-hit tournament). BuildCanonicalModeIds() simply projects the ids out of this list, and IsCanonicalModeId() is Contains over it — so the registry's notion of "a real mode" is exactly this catalogue and nothing else. This page's job is the core catalogue; the monolith also carries a separate match-flow variant matrix (Local Versus, Time/Score Attack, Wager, Handicap, First Blood, Best-of-N, LAN — each a lightweight wrapper around a standard fight) and a list of signature-event plugins (V2Event_KingOfIronFist, V2Event_WrestleMania, V2Event_EVO_Top8, …), both of which are design intent rather than shipped plugin directories today.

The one piece of mode content that lives as external data rather than C++ is the arcade mini-game suite: V2/balance/modes/arcade-mini-game-suite.json (schema v2.modes.arcade-mini-game-suite-data.v1, section 133) enumerates eight cabinets — Pinball, Air Hockey, Mini-Golf, Darts, Pool/Billiards, Cooking, Photo Tournament, Tekken Bowl — and those eight match EV2ArcadeMiniGameSuiteKind one-for-one. BuildDefaultArcadeMiniGameSuiteCatalog() provides the runtime mirror, and RegisterArcadeMiniGameSuiteCatalog lets a caller override it after validation.

Activating a mode — the load plan#

The interesting logic is BuildModeLoadPlan (V2ModeRegistrySubsystem.cpp:283). Given an FV2ModeLoadRequest (mode id, profile id, source context, and the bPreloadOnly / bForceReload / bAllowOnlineServices / bInjectHUD / bOpenPrimaryMap switches), it validates the request, finds the definition, refuses online modes when bAllowOnlineServices is false, and then emits an ordered list of FV2ModeLoadSteps. The step kinds are a nine-value enum (EV2ModeLoadStepKind), and the planner adds them conditionally with explicit sort orders so the final plan is deterministic:

flowchart TD Req[FV2ModeLoadRequest] --> Val{IsValidRequest?} Val -- no --> FailV[Failed + reason] Val -- yes --> Find{Mode registered?} Find -- no --> FailF[Failed + reason] Find -- yes --> Net{Online needed but disallowed?} Net -- yes --> FailN[Failed + reason] Net -- no --> Steps[Emit sorted steps] Steps --> S10[10 ValidateRequest] S10 --> S20[20 DeactivatePreviousMode if a different mode is active] S20 --> S30[30 LoadRequiredModules if dependencies exist] S30 --> S40[40 ActivateGameFeature] S40 --> S50[50 RegisterGameplayRules] S50 --> S60[60 ResolveNetworkModel] S60 --> S70[70 InjectHUD if injections exist] S70 --> S80[80 OpenPrimaryMap optional, skipped on preload] S80 --> S90[90 MarkReady]

ActivateMode then runs that plan: it sets lifecycle to Loading, deactivates any previously-active mode, loads the required C++ modules for real through FModuleManager::LoadModulePtr (failing the whole activation with a specific reason if a required module won't load), and records a successful FV2ModeLoadResult carrying the applied HUD injections, the executed steps, and the resolved network/session models. The lifecycle itself is a seven-state enum (EV2ModeLifecycleState: Uninitialized → Idle → Loading → Preloaded/Active → Unloading → Failed), and every transition broadcasts OnModeLifecycleChanged(ModeId, NewState) so UI and telemetry can follow a load without polling. UnregisterMode refuses to remove the currently-active mode; DeactivateActiveMode is idempotent (deactivating with nothing active returns success at Idle). A worked example: requesting Mode.BattleHub with bAllowOnlineServices = false never produces a plan at all — BuildModeLoadPlan returns false with "Mode.BattleHub requires online services, but the request disallows them," because BattleHub's ClientServerHub network model trips RequiresNetworkServices(). Requesting it with services allowed yields a nine-step (or fewer) plan whose OpenPrimaryMap step is marked non-required and is dropped entirely under bPreloadOnly, which is how the front end pre-warms a hub before the player commits.

CaptureRegistrySnapshot() rolls the whole registry into one FV2ModeRegistrySnapshot value — registered modes, active id, lifecycle state, published Hathor world versions, and a battery of tag-team / arcade-suite readiness counts and booleans — which is the single struct the automation spec asserts against and the natural payload for a debug overlay.

HUD injection — presentation without GAS access#

Each mode definition carries FV2ModeHUDInjectionSpec entries (V2ModeTypes.h:4228): an InjectionId, a SlotName (default Slot.Center), a FSoftClassPath WidgetClassPath, a Priority, and the flags bRequired, bClearOnModeExit, and — critically — bGameplayInert. That last flag encodes the architectural rule that mode HUD is presentation only: a Royal Rumble entrants strip or a UFC scorecard interlude can be slotted in and out without ever reading the Ability System Component directly. The load planner only adds an InjectHUD step when bInjectHUD is set and the definition actually has injections, and BuildDefaultHUDInjectionsForMode derives sensible defaults from the mode's category. This dovetails with the MVVM HUD architecture described in UI, HUD, VR/AR & Accessibility, where widgets bind to view-models rather than to live gameplay state.

Training, trials & the sim/present split#

Training mode is not a separate engine; it is the same deterministic simulation as a live match, with non-deterministic tooling layered on the presentation side. V2Netcode exposes two real types (V2/ue/Source/V2Netcode/Public/V2RollbackSimWorld.h): FV2SimWorld — the integer-frame, side-effect-free simulator the rollback path drives — and FV2PresentWorld (aliased FV2_PresentWorld), which turns the latest confirmed sim frame into an FV2PresentWorldFrame for rendering. Lab overlays, hit-confirm trainers, and frame-data readouts attach to the present world; they never feed the sim world, so determinism is preserved by construction. See Combat System for the sim world's internals and Animation & Input Pipeline for how inputs reach it.

Two training-adjacent systems are modeled as real catalogue data. World Tour master training (FV2WorldTourMasterTrainingSpec, V2ModeTypes.h:5336) binds a fighter to a master, a 3–5 mission band, a SignatureMoveIds list, an 8-move equip cap, and per-relationship-tier unlocks for advanced moves and finishers — the data behind "learn a move from an NPC master and equip it on your avatar." Ghost Battle (FV2ArcadeGhostBattleRules, default mode id Mode.Arcade.GhostBattle) names the GhostBattle.GetUploadedGhosts service and flags bRequiresPlayerUploadedGhostData, the async-service hook that lets an arcade ladder node fight a recording of another player. The arcade scoring that grades those runs is a genuine algorithm, not a stub: EvaluateArcadeFightScore (V2ModeCatalog.cpp:4778) computes TimeBonus = max(0, 6000 − seconds×40), DamageBonus = round((100 − damage%) × 25) with a +500 flawless bonus, ComboBonus = min(combo×120, 2400), a flat 1500 finisher bonus, a 1000 base, and resolves a letter grade through the tuning; EvaluateArcadeRunScore aggregates per-fight results into a cumulative score and average-based rank, rejecting the whole run if any fight is invalid. These would fail immediately on a random or hardcoded return — the tests pin the exact arithmetic.

Replay pipeline & file format#

A replay in V2 is inputs plus determinism hashes, re-simulated rather than recorded as video. The file model lives in V2Input (V2/ue/Source/V2Input/Public/V2InputTypes.h):

  • FV2ReplayFileHeader (:4721) — file magic V2RF, format version, replay and match ids, ruleset and stage ids, build and content versions, platform and region, a CustomRulesHash and CosmeticManifestHash, the match start time, duration, RngSeed, tick rate (default 60 Hz), total frames, and three determinism anchors: InputStreamHash, InitialStateHash, FinalStateHash. The codec defaults to Oodle; bServerAuthoritative records provenance.
  • FV2ReplayInputEventRecord — a per-frame, per-slot FV2InputSample, the payload the re-sim actually consumes.
  • FV2ReplayStateSnapshotRecord — periodic delta-encoded snapshots with a StateHash, a bKeyframe flag, and raw PayloadBytes, used to seek without replaying from frame zero.
  • FV2ReplayFileChunkIndexEntry — a compressed-chunk index over input events and snapshots, with per-chunk CRC and compressed/uncompressed sizes.

FV2InputReplayEncoder (V2InputReplayEncoder.h) is a real binary serializer — EncodeReplayFile/DecodeReplayFile, EncodeMatchStream/DecodeMatchStream, EncodeSamples/DecodeSamples, and a HashEncodedStream — and its implementation writes the literal V,2,R,F magic bytes and length-checked names/strings, bailing out to an empty buffer on any malformed field rather than emitting a corrupt file (V2InputReplayEncoder.cpp). The byte budget is computed, not asserted: FV2ReplayFile::EstimateCompressedBytesPerMinute() does a real ceiling division, (compressedBytes × 60000 + durationMs − 1) / durationMs, against the FV2ReplayFileCompressionProfile's TargetMaxBytesPerMinute (1 MiB/min, codec non-None, positive packing sizes, bounded keyframe interval). The replay determinism gate the architecture promises — every replay re-played in CI must reproduce bit-identical outcomes — is anchored on the header's three hashes plus the per-snapshot StateHash; a drifted re-sim mismatches FinalStateHash and fails. The anti-cheat replay-drift pipeline in Online Backbone & Competitive Integrity reuses the same hashes.

Replay takeover → offline practice#

The "pause a replay at any frame and take over a side" feature is implemented in V2OnlineServices, not V2Modes. FV2ReplayTakeoverSpec (V2OnlineServicesTypes.h:13051) declares the capability — supported sides (EV2ReplayTakeoverSide: PlayerOne/PlayerTwo), use cases (EV2ReplayTakeoverUseCase: Tech, SetupResponse, CrewLab), and the flags bPauseAtAnyFrame, bConvertsToOfflinePracticeScenario, bPreservesOriginalReplay, and bFrameExactStateRestore. The runtime entry point is UV2OnlineServicesSubsystem::CreateReplayTakeoverPracticeScenario, and it is honest about its preconditions: it validates the request against the takeover spec, then requires the replay to actually exist — registered in CloudReplays or present as a queued local download — and returns false with "Replay takeover requires a registered cloud replay or queued local replay download" otherwise. Only then does it build, validate, de-duplicate, and store a FV2ReplayTakeoverPracticeScenario. The original replay is preserved read-only; the takeover produces a new practice scenario. This is the architecture's "Replay Takeover → Training Mode Scenario" arrow realized as fail-loud C++.

Tag team, multi-fighter & the crew lab#

Multi-fighter topology is its own data axis. FV2MultiFighterModeSpec (V2ModeTypes.h:4274) carries an EV2MultiFighterTopology (OneVsOne/TagTeam/FreeForAll/RoyalRumble), team and per-team fighter counts, active-fighter and total-entrant caps, and capability flags. The tag-team mechanics catalogue (FV2TagTeamMechanicsCatalog, registered through the subsystem) enumerates formats (EV2TagTeamFormatKind: 2v2 / 3v3 / Trinity), mechanics (EV2TagTeamMechanicKind: TagCancel, Snapback, DelayedHyperCombo, AssistCall, CrossAssault, XFactorPandora, BaroqueDuoCancel), and online queues (RankedTagTeam, TagCoop, CrewBattle). The registry snapshot exposes counts and readiness booleans for each, which is how a launch gate can assert "tag-team is catalogued and valid" headless. The deterministic execution of tagging, assists, and DHC lives in V2Gameplay/V2Netcode; this module owns the catalogue and topology, while Rollback Netcode & Tag Team owns the frame-accurate handshake and the per-side snapshot budget.

The Hathor UGC-world bridge#

One non-obvious seam: the mode registry is a subscriber to the Hathor sister monorepo's world-publication events. On Initialize the subsystem calls SubscribeToHathorWorldPublishedEvents(), and HandleHathorWorldPublishedEvent validates an incoming hathor.world.published event, converts it to a FV2HathorWorldVersionRef available to gameplay, and queues a FV2HathorLoreCompilerExportRequest targeting @hathor/lore-compiler with bOutOfBandMetadata and bRollbackSafe set. The "rollback-safe / out-of-band" flags are the whole point: published UGC worlds can feed lore and content into V2 modes without ever entering the deterministic frame path. This is the mode-side hook for the cross-monorepo integration detailed in Security, Compliance & Sister-Monorepo Integration; the open-world and special modes that consume those worlds are covered in Open World, Co-op & Special Modes.

Where this connects#

The mode registry is the orchestration layer that almost every other V2 system hangs off. The catalogue's network and save models are honored by the Online Backbone and persisted through Live-Ops, Store & Progression; the racing modes have their own parallel module set described in Racing & Vehicle Architecture; replays and takeovers surface in the companion and AI services (Esports, Companion & AI Services); the presentation beats that bracket each mode (intros, win quotes, entrances) are in Presentation, A/V & Signature Content; and the catalogue counts, validation gates, and replay determinism checks are enforced by the test and release machinery in Telemetry, Performance, Testing & Release Gates. For the module names and the dependency graph that keeps V2Modes from reaching into frame-critical code, start at Glossary & Module Topology. For how the cooked content behind every PrimaryMapPath is produced, see Build, Cook, Assets, Data & Production.