Fighting Game · Architecture

Build, Cook, Assets, Data & Production

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

7sections13 minread1diagram1table

On this page

Everything on the other architecture pages — the deterministic combat sim, the rollback netcode, the roster, the racing rulesets — eventually has to be built into a shippable game for a fleet of platforms and kept inside a budget tight enough to hold a locked frame rate on a Switch 2. This page is V2's production substrate: the BuildGraph/cook/patch pipeline that turns source into platform packages, the asset budgets that bound what artists can ship, and the data architecture that makes V2's frame-data numbers a CSV a designer edits rather than C++ a programmer recompiles. It is the least glamorous and most load-bearing layer in the project — a frame-data typo that escapes the validators is a balance bug in everyone's hands, and an asset that escapes the budget catalog is a streaming stall on the platform with the least memory to spare. This page is also deliberate about which parts are running config and CI, which are typed-and-validated C++ policy, and which are still specification no cooked build yet satisfies. The section hub is ../V2_ARCHITECTURE.md.

What ships, honestly#

Four layers, stated plainly, because the split between real and aspirational is the whole point of this page.

  • The build pipeline is real config. V2/ue/Build/Build.xml is a genuine, 95-line BuildGraph script with six per-platform agents, real Compile / Cook / BuildCookRun nodes, a shared deterministic DDC, and an Aggregate that ties the whole fan-out together (Build.xml:93). Three real *.Target.cs rules under V2/ue/Source define the game client, editor, and dedicated server.
  • The asset budgets are typed C++ policy with validators — not prose. The numbers below (80k tris, 64 MB textures, the Switch 2 1.5 GB streaming pool) live as fields on FV2AssetBudgetStreamingCatalog and its sibling structs in V2/ue/Source/V2Editor/Public/V2EditorTypes.h, each with an IsValidPolicy() validator that fails on a wrong value, exercised by AssetBudgetStreaming.spec.cpp and cross-checked against a committed contract by check-v2-asset-budget-streaming.py. This is stronger than "a budget the audit checks against": the budget is a validated object.
  • The data pipeline is real, but narrower than the old prose implied. The spreadsheet-of-truth (V2/balance/data/frame-data.csv) and its branch-stamped export (export-v2-frame-data-csv.pyV2/balance/exports/<branch>/frame-data.csv, validated by check-v2-frame-data-csv-export.py) are real, and the CSV is ingested into the cook by Sophia. But the often-claimed "CSV↔DataTable round-trip importer commandlet" does not exist in V2Editor (a grep for CSV/DataTable import returns nothing); the only commandlet there is the region-variant one. That gap is named here, not papered over.
  • The content is a logic-and-data skeleton. Measured live in this tree: 8 real .uasset binaries (the DA_RegionVariant_* data assets under V2/ue/Content/V2/Regions/) and 0 .umap — no character meshes, stages, animations, VFX, or audio binaries. The budgets and validators are real and would catch regressions; the cooked content they bound is not in the repository. Likewise V2/legal/rights-manifest.json (cited by the architecture) is not presentV2/legal/ holds rating-boards.json, platform-cert-bans.json, and sub-processors.md instead.

The honest summary: the process and the validators are real, the cooked binaries the process exists to produce are not.

Build, cook & patch#

The BuildGraph script#

V2/ue/Build/Build.xml declares the platform matrix as three properties: client platforms Win64;PS5;XSX;Switch2 (Build.xml:13), server platforms LinuxServer-x86_64;LinuxServer-ARM64;Win64Server (:14), and Mac as dev-only editor (:15). It then defines six Agents — Win64 Editor And Client (:18), PS5 Client (:42), XSX Client (:51), Switch2 Client (:60), Linux Dedicated Server (:69), and Mac Developer Editor (:84). Each client agent runs the same disciplined node sequence: compile the shipping V2 target, run a Sophia release-candidate ingest, Cook with -ddc=$(ReferenceDDC) -unversioned -compressed, then BuildCookRun with -build -cook -stage -pak -archive into a per-platform archive directory (e.g. Build.xml:28-32 for Win64). The Linux server agent builds both x86_64 and ARM64 dedicated-server architectures (:70, :76) — the dedicated / validator server the online backbone re-simulation pool runs on. An Aggregate Name="V2 BuildGraph All" (:93) requires every platform build plus the shader-distribution plan, so "build everything" is one named target rather than a runbook.

Two pieces make the cook reproducible. ReferenceDDC (Build.xml:5) is a shared Derived Data Cache path described in the file itself as being there "for deterministic cooks," and the Plan V2 Shader Distribution node (:22) shards shader permutations across the farm via plan-v2-shader-distribution.py with a --max-permutations-per-shard cap of 512 (:7).

Targets & determinism#

The three real target rules under V2/ue/Source are not interchangeable boilerplate. V2.Target.cs is the Game client and bakes determinism into the binary: it adds DETERMINISM=1, V2_DETERMINISM=1, and V2_STRICT_FP=1 and compiles Win64 with /fp:strict /fp:except-, so the floating-point behavior the rollback netcode depends on is a compiler contract, not a hope. V2Editor.Target.cs is the Editor target and pulls in the V2Tests module so automation runs in-editor. V2Server.Target.cs is the Server target and strips the client surface — V2_NO_RENDERER, V2_NO_AUDIO, V2_HEADLESS_SERVER, and disabled Niagara / CommonUI / Metasound plugins — for the minimal-memory dedicated server, building for Linux x86_64, Linux ARM64, and Win64.

Cook, distributed compile & Sophia ingest#

The cook itself is the Cook task inside each platform node, always -unversioned -compressed against the shared ReferenceDDC. Distributed builds run on UnrealBuildAccelerator / Horde with a ≥ 75% cache-hit-rate gate and shared DDC for editor performance (architecture §Build). The distinctive wiring is Sophia release-candidate ingestion: before each platform's cook, run-sophia-release-candidate-ingest.py ingests V2/balance/data/frame-data.csv and the patch-notes corpus into a per-platform ingestion manifest (Build.xml:9, :30, :45, :63, :72 …), so the same frame-data spreadsheet that documents a move's startup is the artifact stamped into the release candidate at cook time.

CI exercises a slice of this on every PR. .github/workflows/v2-build.yml runs a Win64 editor, automation, cook job (:28) that compiles the editor (:2334), runs the V2.* automation suite (:2344), enforces the combat frame budget (:2357), and cooks a Win64 shipping build (:2367) before uploading reports (:2377). Upstream of the cook the same workflow runs check-v2-buildgraph.py (:46), check-frame-budget.py --self-test (:2312), the asset-name linter (:2313), and the frame-data export check (:1942).

Patch & chunking#

Patching is chunked and incremental per platform with signed manifests, and the size budgets are not prose — they are an enforced gate. V2/ue/Tools/check-v2-day-one-patch-hotfix-recovery.py asserts dayOnePatchBudgetGB == 5 (:124) and subsequentPatchTypicalGB == 2 (:125), failing the build with "Day-one patch policy must cap day-one patch at 5GB and subsequent patches at 2GB typical." Siblings cover the rest of the post-launch surface: check-v2-server-hot-patching.py (data-only balance hotfix), check-v2-anti-cheat-self-update.py, and check-v2-drm-removal-pathway.py (the documented Denuvo-removal window). The chunk boundaries patching rides are the same per-fighter chunks the streaming catalog defines (next section), which is exactly why a DLC fighter can ship without re-downloading the base game — the live-ops & store DLC delivery relies on those boundaries.

Asset budgets & streaming#

A fighting game is a worst case for memory: up to eight high-detail characters, a stage, VFX, and per-locale audio resident at once at a locked frame rate. V2's budget is therefore explicit, per-fighter, per-stage, and — crucially — a typed C++ catalog with validators, not a wiki page.

The typed budget catalog#

check-v2-asset-budget-streaming.py enumerates the real surface it validates: FV2AssetBudgetFighterProfile, FV2AssetBudgetStageProfile, FV2AssetBudgetStreamingPolicy, FV2AssetBudgetAuditPolicy, FV2AssetBudgetShaderComplexityPolicy, FV2AssetBudgetStageParticlePolicy, FV2AssetBudgetMemoryPolicy, FV2AssetBudgetPSOCachePolicy, FV2AssetBudgetAsyncLoadingPolicy, all aggregated by FV2AssetBudgetStreamingCatalog with an IsValidCatalog() validator — every one a real struct in V2/ue/Source/V2Editor/Public/V2EditorTypes.h. The checker cross-references the C++ against a committed AssetBudgetStreaming_V2_Contract.json, and AssetBudgetStreaming.spec.cpp (V2/ue/Source/V2Tests/Private/Editor/) exercises the validators in-engine.

The numbers are real field defaults, and they are the values the validators reject deviations from:

Surface Budget Source
Principal fighter, LOD0 ≤ 80k tris V2EditorTypes.h:4601
Mid-roster / crowd, LOD0 ≤ 60k / ≤ 25k tris :4604, :4607
Fighter textures ≤ 64 MB (4K) / 32 MB (2K) / 8 MB (Switch 2 1K) :4610, :4613, :4616
Fighter animation ≤ 800 unique anims, ≤ 80 MB compressed package :4619, :4622
Fighter audio ≤ 50 MB per locale :4625
Stage ≤ 4M tris, ≤ 256 MB textures, ≤ 30 MB locale audio :4657, :4660, :4666
Stage particles 100k outdoor / 60k indoor / 80k arena :4793, :4796, :4799
Streaming pools PS5 / XSX 4 GB, PC 8 GB, Switch 2 1.5 GB :4692:4701

FV2AssetBudgetStreamingPolicy::IsValidPolicy (V2EditorTypes.cpp:3725) hard- checks the pool sizes — PS5StreamingPoolMB != 4096 || … || Switch2StreamingPoolMB != 1536 fails with "Streaming pool policy must be PS5 XSX 4GB, PC 8GB, and Switch2 1.5GB." (:3727-3732). The 1.5 GB Switch 2 pool is the binding constraint that drives the entire LOD/streaming design.

The streaming model & per-fighter chunks#

Streaming is priority-driven, and the priorities are boolean policy fields, not comments: bVisibleFightersAndStageHighPriority (:4707), bOffscreenPartnerFightersDemoted (:4710), bVoiceBanksStreamedOnDemand (:4713), bMusicTracksStreamed (:4716), and bCrowdInstancedMeshBoneImpostorLOD (:4681) — in-match fighters and the stage are prioritized, off-screen tag partners are demoted, voice and music stream on demand, and crowds use instanced static meshes promoting to bone-animated impostors at distance. HasRequiredFighterChunks() (V2EditorTypes.cpp:3716) requires each fighter to split into exactly the Character, Voice, BaseCostume, AltCostume, and CosmeticPack chunks (:3718-3722) — the boundaries incremental install and DLC ride.

The weekly audit#

FV2AssetBudgetAuditPolicy (V2EditorTypes.h:4723) makes the audit a policy object too: a WeeklyAuditCheckIds set with bWeeklyAssetAuditReport, bNamingConventionLinterGate, bBudgetLinterGate, and bEveryNewAssetMustPassCI flags, validated by HasRequiredAuditChecks() / IsValidPolicy(). The weekly report flags orphan / oversize / missing-LOD / missing-skin / redundant-material assets, and a coarser CI gate (check-v2-asset-size-budget.py against V2/ue/Build/AssetPipeline/v2-asset-size-budget.json) warns at 100 MiB and fails at 500 MiB per asset — so budget drift is caught as a report or a red PR, not a shipped regression.

Data architecture: the spreadsheet is the source of truth#

This is the part most worth understanding, because it is what makes V2 balanceable without recompiling C++.

frame-data.csv → branch-stamped export#

The canonical source is V2/balance/data/frame-data.csv, an 11-column sheet (fighter_id, move_id, move_name, ruleset, startup_frames, active_frames, recovery_frames, on_hit_advantage, on_block_advantage, damage, build_branch). export-v2-frame-data-csv.py (144 lines) reads it, validates that the header tuple matches exactly and that no (fighter_id, move_id) pair is duplicated (:69, :82), stamps the current branch into build_branch, and writes V2/balance/exports/<branch>/frame-data.csv. Its --check mode (:104-109) re-derives the export and fails if the committed file drifts from the source, so the documentation, the balance dashboards, and the game read one canonical dataset. check-v2-frame-data-csv-export.py (219 lines) layers a JSON-schema (frame-data.schema.json) and a contract (FrameDataCsvExport_V2_Contract.json) check on top, comparing every exported row against the canonical source.

Typed DataAsset contracts & validators#

Runtime move/fighter data lives in typed C++ data assets — UV2MoveFrameData, UV2FighterData, and their moveset/style/arena siblings — and the integrity gates are real spec suites, not shape checks. MoveDataAsset.spec.cpp (1,428 lines, V2/ue/Source/V2Tests/Private/Combat/) asserts startup/active/ recovery frames, hitbox/hurtbox geometry, dash-cancel windows, armor and invincibility frames, adaptive-trigger profiles, and haptic curves; FighterDataAsset.spec.cpp builds a complete fighter (skeleton, mesh, voice bank, theme music, signature moveset) and validates loadout, stat block, finishers, and weapons. Per-asset Python checkers (check-v2-move-data-asset.py, …fighter…, …moveset…, …style…) gate the same contracts in CI. These are the "move / fighter / moveset / finisher / style / ruleset / arena / entrance" validators the architecture lists.

What is honestly still spec#

The frequently-claimed "CSV↔DataTable round-trip importer commandlet" in V2Editor is not implemented — a grep across V2/ue/Source/V2Editor for CSV parsing or DataTable import returns nothing, and the module's only commandlet is V2CreateRegionVariantsCommandlet. The real round-trip is the CSV-to-CSV branch-stamping export above, plus Sophia's cook-time ingest of the sheet; the import of authored CSV into engine DataTables on cook is the specified-but-unbuilt half. Naming this is the difference between a documented pipeline and a fabricated one.

flowchart LR S[V2/balance/data/frame-data.csv<br/>spreadsheet of truth] -->|export + branch-stamp| EX[V2/balance/exports/branch/frame-data.csv] S -->|cook-time ingest| SOPHIA[Sophia release-candidate manifest] EX --> CHK[check-v2-frame-data-csv-export.py<br/>schema + contract] C[(FV2AssetBudgetStreamingCatalog<br/>typed budgets)] --> VAL[IsValidCatalog · AssetBudgetStreaming.spec.cpp] VAL --> CI{PR gate · v2-build.yml} CHK --> CI FB[check-frame-budget.py ≤16.6ms] --> CI CI --> COOK[BuildGraph cook · 6 platform agents] COOK -.->|no character/stage binaries in repo| BIN[(cooked .uasset / .umap<br/>NOT present)] RC[V2CreateRegionVariantsCommandlet] --> REG[(8 DA_RegionVariant_*.uasset<br/>the only real binaries)]

Production pipeline#

The production pipeline binds capture, consent, and cert into the build. Most of it lives as real apps/v2 packages rather than engine code, and the region-variant path is the one place it produces a real binary.

Studio and markerless capture run through real service packages: aja-studio-mocap-pipeline, aja-markerless-capture-pipeline, and aja-bellona-livelink-export handle Vicon / OptiTrack ingest, cleanup, V2-skeleton retarget, and Bellona/UE import. Every capture first passes aja-consent-nil-ledger, which requires active consent and a Themis likeness (NIL) link before ingest — revocation blocks future Aja processing and Bellona cook for dependent clips. The likeness-safety gate is aphrodite-licensed-likeness-safety, backed by aphrodite-consent-surfaces and aphrodite-age-gate, run before likeness surfaces, creator-suite publishing, or replay export. These directories exist under apps/v2/; the VO (loudness-normalized Opus banks) and music (MetaSounds graph) pipelines they feed are documented in the architecture.

Region & cert variants — the one real binary surface#

V2CreateRegionVariantsCommandlet (V2/ue/Source/V2Editor/Private/) reads the rating-board manifest (V2/legal/rating-boards.json), applies per-region content-policy flags (gore, dismemberment, profanity, localization compliance), and writes the 8 DA_RegionVariant_*.uasset data assets under V2/ue/Content/V2/Regions/ (JP, BR, KR, AU, NZ, DE, US, EU). These are the only real cooked binaries in V2's content tree, and they stand as the proof-of-concept for what the rest of the pipeline would produce against an attached editor and art depot.

Milestones & rights — what's specified#

The milestone gates (Greenlight → Alpha → Beta → Gold master) and the per-asset rights verification are documented obligations rather than committed artifacts in this tree: V2/legal/ carries rating-boards.json, platform-cert-bans.json, and sub-processors.md, but the architecture-cited V2/legal/rights-manifest.json is not present, and no V2/docs/production/milestones/ directory exists here. The rights-verification step that gates "every shipping asset has cleared rights or is original-IP" therefore remains spec until that manifest lands.

Edge cases & failure modes#

  • The Switch 2 pool is the hard floor. IsValidPolicy rejects any streaming catalog whose Switch 2 pool isn't exactly 1.5 GB; the LOD chains and 1K texture packs exist to fit it, and every other platform inherits the discipline.
  • A frame-data typo fails the build, not the player. The export --check mode plus check-v2-frame-data-csv-export.py fail CI if the committed export drifts from V2/balance/data/frame-data.csv, and the per-asset spec suites assert computed frame values — so the documented startup and the executed startup cannot disagree.
  • Hotfixes can never become code pushes. The day-one/hotfix checker caps the day-one patch at 5 GB and constrains hotfixes to data-only payloads, so a balance change ships without a binary update and a binary change can't smuggle through the hotfix channel.
  • Revoked consent blocks the cook. A likeness whose NIL consent is revoked fails aja-consent-nil-ledger / aphrodite-licensed-likeness-safety before Bellona cook for any dependent clip — capture rights are a build gate, not a legal afterthought.
  • No binaries means open claims stay open. With 8 region-variant assets and zero .umap, anything requiring cooked content — profiled frame rates, PSO caches, a playable stage — is not demonstrable from this tree, and this page declines to claim it is.

Where this connects#