Everything the other V4 pages describe — six genre cores in one engine, the netcode, the crowds, the operator roster — eventually has to become a shippable package on ten platforms and stay inside a console frame budget, and it has to do so from numbers a designer can edit without a programmer recompiling. This page is V4's production substrate: the BuildGraph/cook/patch pipeline that turns source into platform packages, the data architecture (typed DataTables plus a telemetry-backed balance ledger) that holds the thousands of tuning numbers, and the content pipeline that lints and gates every asset before it can ship. It is the least glamorous and most load-bearing layer in the project — a data-table typo that escapes the validators is a balance bug in everyone's hands, and a broken asset reference that escapes the linter is a cook failure on the build farm.
There is one fact about V4 that this page exists to state honestly, because it
shapes everything below it: this repository is text-only — no Perforce binary
depot is attached. Every Unreal content asset is therefore a JSON stand-in
(<Name>.uasset.v4asset.json / <Map>.umap.v4asset.json), not a cooked binary,
and the build/cook/patch pipeline is real configuration plus real validators
rather than a build farm that has actually produced shipping packages from this
tree. That distinction is drawn precisely throughout, in the spirit of the
orientation hub it expands on — ../V4_ARCHITECTURE.md.
What ships, honestly#
Three layers, stated plainly.
- Real config, real validators — no cooked output. The build pipeline is
described by genuine, parseable files: a BuildGraph script
(
V4/release/BuildGraph/V4BuildCookPatch.xml), a manifest (V4/release/build-cook-patch.json), aJenkinsfile.pc, a GitHub Actions workflow (.github/workflows/v4-build-cook-patch.yml), and distributed-compile configs (FASTBuild.bff,SNDBS.ps5.xml). These are wired together and enforced byapps/v4/scripts/src/v4-build-cook-patch-check.mjs, which actually reads each file and asserts its contents. What the pipeline has not done in this repo is cook a shipping binary — there is no binary content to cook. - Real, enforced data architecture. The 19
DT_*.jsondata tables underV4/ue/Contentare validated byv4-data-linter.mjs(required-field and schedule-graph checks), the 891 asset stand-ins byue-asset-linter.mjs(naming, polygon, texture, reference, orphan, and plugin-cycle rules), and the balance datasets (public-balance-ledger.json,balance-operations.json,gadget-balance-ledger.csv) byv4-public-balance-ledger-check.mjs. These are domain-specific gates that fail on bad data, not shape-only smoke tests. - Honestly labeled content gap. Measured live in this tree: 0 real
.uassetbinaries, exactly 1 real.umap(V4Core/Maps/BootMap.umap, 15 KB, authored bycreate_boot_map.py), against 891.v4asset.jsonstand-ins (547.uasset+ 344.umap). The architecture doc records 770 as of 2026-06-12; the live count is now 891. Stand-ins carry authored intent (typed fields, references) and are what the gates attest to, but they cannot be cooked, loaded, profiled, or played. Any claim that requires cooked content — PSO caches, profiled frame rates, playable maps — stays open.
The honest summary: the process is real and the validators would catch regressions, but the binaries the process exists to produce are not in this repository.
Build, cook & patch#
Build system#
V4 is an UnrealBuildTool project with three real target rules under
V4/ue/Source: V4.Target.cs (the game client), V4Editor.Target.cs, and
V4TournamentServer.Target.cs (the dedicated/validator server the
online-services backbone re-simulates on).
Each is a genuine TargetRules subclass — V4.Target.cs sets
DefaultBuildSettings = BuildSettingsVersion.V5 and
IncludeOrderVersion = EngineIncludeOrderVersion.Latest, the UE5.5-era
defaults. The codebase splits into 28 V4* C++ modules (V4Core,
V4Gameplay, V4Tactical, V4Stealth, V4Tactics, V4RTS, V4ActionRPG,
V4Procgen, V4Crowd, V4LevelOps, V4Netcode, V4OnlineServices,
V4Persistence, V4Telemetry, V4Tests, and more), each with its own
*.Build.cs ModuleRules. The dependency edges are real and meaningful:
V4Gameplay.Build.cs declares PublicDependencyModuleNames of Core,
CoreUObject, Engine, V4Core, GameplayAbilities, GameplayTags, and
GameplayTasks — i.e. the GAS stack the GAS-layout page
builds on, pulled in at the module boundary rather than ad hoc.
Distributed compile is configured for two backends, both gated:
FASTBuild for Win64 (V4/release/distributed/FASTBuild.bff, worker pool
v4-windows-buildfarm) and SN-DBS for PS5 and PS5 Pro (SNDBS.ps5.xml,
restricted runner label ps5-sdk-sndbs). The check rejects the manifest unless
FASTBuild covers Win64 and SN-DBS covers both PS5 SKUs, so the console build
path cannot silently drop its distributed compiler.
Two CI surfaces drive builds. The Jenkins PC pipeline (Jenkinsfile.pc,
agent ue5-pc-buildfarm) runs the eight-stage editor/cook/package/publish
sequence for Win64/Linux/Mac. The GitHub Actions workflow covers the
non-engine surfaces — tools, web, services — and runs
pnpm -F @v4/scripts test, an Nx affected query, and
cargo test --manifest-path apps/v4/Cargo.toml --workspace, so the TypeScript
tooling and the Rust online-services workspace are tested on every PR touching
V4/release/** or apps/v4/**.
Cook#
The cook itself is a BuildGraph graph, V4BuildCookPatch.xml, and it is small
and explicit — four nodes wired by Requires:
- Build Editor —
Compile Target="V4Editor" Platform="Linux" Configuration="Development"(the Linux editor is what cooks headlessly). - Cook Platform —
Cookwith arguments-unversioned -compressed -iterate -ddc=$(ReferenceDDC), followed by aValidateCookcommand. - Author Differential Patch — a
V4DiffPatchcommand feedingV4/release/patches/differential-patches.json. - An
AggregatenamedBuildCookPatchtying all three together.
build-cook-patch.json enumerates ten cook platforms — Windows, Mac, Linux,
PS5, PS5Pro, XSX, XSS, Switch2, iOS, Android — and the check
(REQUIRED_COOK_PLATFORMS) fails if any are missing. Each platform carries a
strip profile and a mode-plugin policy, which is where the multi-cell
breadth meets the memory budget: desktops use DesktopFull with
modePlugins: "all", PS5 uses ConsolePerformance, PS5 Pro ConsoleQuality,
but XSS strips the R6-memory-heavy plugins (ConsoleMemoryConstrained /
strip_r6_memory_heavy) and Switch2, iOS, and Android strip Warzone and
R6 (HandheldConstrained / MobileHighEnd / strip_warzone_and_r6). The
check requires every platform entry to declare all three of uatPlatform,
stripProfile, and modePlugins, and rejects any cookTimeBudgetHours > 8 —
the ≤ 8-hour-per-platform farm budget, enforced as data rather than a comment.
Patch#
Patching is governed by three datasets, each with a hard, domain-specific gate
in v4-build-cook-patch-check.mjs:
- Day-one patch (
patches/day-one-patch.json): the budget is asserted to be exactly 6 GiB (6442450944bytes); the estimate must not exceed it; the patch must carry no base-install content (contentInPatch === false); and it must include both ananti_cheat_updateand aclosed_beta_balancepayload. A day-one patch that smuggles in base content, or omits the anti-cheat delta, fails the build. - Hotfix (
hotfix/data-table-hotfix.json): the channel must bepatch-service, global rollout must be ≤ 30 minutes, the only allowed payload type isdata_table, and the dispatch must be signed and rollbackable. This is what lets a balance change ship without a binary update — and the gate guarantees a hotfix can never quietly become a code push. - Differential patches (
patches/differential-patches.json): authoring must be enabled for all ten launch platforms and must include the five required steps —chunk_manifest,binary_delta,data_table_delta,version_graph, androllback_manifest.
Data architecture: DataTables and the balance ledger#
This is the part most worth understanding, because it is what makes a six-genre game tunable at all without recompiling C++.
DataTables as the data spine#
Tuning lives in typed JSON DataTables under V4/ue/Content (19 DT_*.json
today), and v4-data-linter.mjs enforces a per-type required-field schema via
its REQUIRED_FIELDS map. The fields are domain-specific, not generic: a
DT_Operator row must carry displayName, faction, mesh, and
voiceLibrary; a DT_Weapon needs recoilCurve, damageTable, and
fireRate; a DT_Takedown needs compatibleCells, animationPair, and
soundProfile; a DT_ProcgenChunk (the chunks the
world-streaming & procgen solver assembles)
needs cellId, graphId, asset, and constraintTags; a
DT_WorldPartitionProfile needs cellSizeMeters, streamingRingRadiusCells,
and hlodBudgetMegabytes. An empty table or a row missing any required field is
an error, not a warning.
DT_Schedule gets a graph validator of its own, because Hitman-style NPC
routines are state machines, not flat rows. validateSchedule requires every
schedule to have nodes, rejects any next edge pointing at a missing node id
(schedule-orphan-node), and requires the graph to close back to its start
node (schedule-closed-cycle) — an NPC routine that never loops is a bug. The
shipped DT_Schedule_Launch.json rows model exactly this:
Arrive → Routine → Social → Reset → Arrive, a closed daily cycle the linter
proves is well-formed.
Gameplay tags#
Ability activation, stance, detection, and faction state are keyed off a single
hierarchical gameplay-tag tree authored in
V4/ue/Config/DefaultGameplayTags.ini and validated against the
REQUIRED_GAMEPLAY_TAGS list in gameplay-tags.mjs. The hierarchy is per-cell
and deep — Tactical.State.Stance.{Stand,Crouch,Prone},
Tactical.Cover.{Left,Right},
Stealth.Detection.{Hidden,Suspicious,Compromised}, ARPG.Combat.Parry.Window,
RTS.Faction.{Vanguard,Synapse,…} — so the same tag a designer types in a data
table is the tag the C++ ability system reacts to, with no string drift between
them.
The balance ledger: telemetry-backed source of truth#
Where V2 made a frame-data spreadsheet the source of truth, V4's analog is a
publicly published, telemetry-evidenced balance ledger, and it is treated
with the same rigor. v4-public-balance-ledger-check.mjs enforces a chain of
domain-specific rules:
- Evidence gates. The ledger's
telemetryEvidencemust specify a sample window ≥ 7 days, a minimum match count ≥ 10,000, a dashboard path, and the four metricspickRatePercent,winRateDeltaPercent,counterplaySuccessPercent,matchCount. Every individual change must cite ametricSourceand a match count at or above that floor — a balance note without telemetry behind it fails. - Balanced change set. Entries must include both
BuffandNerfchanges, each withtarget,before,after, andrationale— the ledger cannot be all-buffs marketing copy. - Designer's Tier List. At least six ranked entries spanning Operator,
Faction, and Civilization subject types, each tier in
{S,A,B,C}with telemetry (matchCount ≥ 7500, pick/win rates, a trend). - Matchup matrices. Three are required by id —
operator-vs-operator,faction-vs-faction,civ-vs-civ— and the check validates matrix algebra:winRatesandmatchCountsmust be rectangular against the declared rows and columns, every win rate in[0,100], every cell's sample at or above the matrix minimum. - Wukong frame data. The ARPG cell publishes parry/dodge/startup frames, and
the gate hard-codes the correctness values: a heavy parry must stay at
parryWindowFrames.width === 6and dodge i-frames atdodgeIFrameFrames.width === 9. This is a real domain assertion — it would fail if the numbers drifted.
The same gate cross-checks the service manifest, the marketing /balance-ledger
page, its Playwright e2e coverage, and the architecture/features/TODOS docs,
so the published numbers, the site, and the documentation cannot disagree. The
weekly cadence and tier definitions live in balance-operations.json, and a
machine-readable gadget-balance-ledger.csv carries per-gadget launch baselines
(charges, cooldown, radius, noise, counterplay window) one row per gadget. The
dashboards behind those metrics belong to
telemetry, observability & performance.
Content pipeline & the stand-in model#
The .v4asset.json stand-in convention#
Because no Perforce binary depot is attached, every content asset the docs and
gates reference exists as a JSON sidecar that records the asset's authored
intent: typed fields, row data, and references to C++ symbols and to other
stand-ins. A representative example is
V4Rendering/DA_ShaderWarmupPlan.uasset.v4asset.json, which declares an
assetType of DataAsset, a backing class V4ShaderWarmupPlan, PSO precache
lists, per-cell prewarm triggers, and a regression policy
(failBuildOnMissingEntry: true, missingPsoEntryBudget: 0). The sidecar
describes a real plan the engine would execute; it is not itself a cooked PSO
cache. This is the disciplined, honest representation of real-but-not-yet-binary
content, and it is the unit the gates attest to.
Asset linting#
ue-asset-linter.mjs treats stand-ins and real binaries identically (it
normalizes both to a content key) and applies the full asset-pipeline rule set:
- Naming. A
TYPE_PREFIXEStable maps each Unreal asset type to its required prefix (T_textures,M_/MI_materials,BP_/WBP_blueprints,SM_/SK_meshes,A_/ABP_animation,DT_/DA_data,PCG_graphs,NS_Niagara,IA_/IMC_input,L_/LVL_levels). An asset with no matching prefix is an error; the one real binary,BootMap, is explicitly whitelisted. - Budgets. Textures over 4096 px are an error and over 2048 px a review
warning. Meshes are checked against per-archetype polygon budgets
(
MESH_POLY_BUDGETS): Operator 100k, Hero 150k, Civilian 40k, RTSUnit 12k, ArcadeSprite 2k, Vehicle 120k, Prop 25k. Animations must have compression enabled and no uncompressed tracks. - Graph integrity. Broken references (a
referencesentry that resolves to no known asset) are errors; non-primary blueprints with zero incoming references are flagged as orphans; widget text without a localization key fails;IA_Movemust include theUV4CoverStickInputModifier; cover-anchor graphs must declare amapId, ≥ 2 anchors with 3D location and normal, and edges that only reference existing anchors. - Plugin cycles.
.uplugindescriptors are parsed into a dependency graph and walked with a depth-first visit that reports any dependency cycle — the guardrail for the GameFeature-plugin DLC strategy below.
The companion v4-data-linter.mjs and ue-asset-linter.mjs are exactly the
"PR-gate asset linter" the architecture hub lists, and they run before any cook
is even attempted, so a missing-field row or a dangling reference is caught as a
CI failure rather than a cook-farm crash.
Source control, plugins & the one real map#
Source control is split by content type: Git for source code, config, and
scripts (V4/ue/Source/, V4/ue/Config/, plus tools/, web/, services/),
Git LFS for small text-y binaries, and Perforce for the heavy binary
depot — the depot that, in this repository, is not attached, which is
precisely why the stand-in model exists. Post-launch content rides Unreal's
GameFeature plugin system: each new operator, map, or cell ships as its own
plugin (the same hot-swap mechanism the
game-modes & live-service page covers), and the
asset linter's cycle check keeps those plugin dependencies acyclic.
The single exception to the text-only rule is worth calling out as the honest
counterexample: V4/ue/Content/V4Core/Maps/BootMap.umap is a real, 15 KB,
engine-loadable .umap, generated by apps/v4/scripts/src/create_boot_map.py
through the live unreal Python API — it spawns a PlayerStart, a
DirectionalLight, and a SkyAtmosphere, then saves the package. It is the one
piece of V4 content the engine can actually load, and it stands as the
proof-of-concept for what the stand-in pipeline would produce against an
attached editor. The art, audio, and VFX budgets these gates enforce — and the
soft assets they reference but do not bake — belong to
presentation pipelines; the cooked binaries
themselves remain the project's open frontier.
Related#
- ./presentation-pipelines.md — the audio/VFX/HLOD and roster art this pipeline budgets and lints but does not bake.
- ./telemetry-observability-performance.md — the dashboards and performance-budget gates behind the balance ledger's telemetry evidence and the cook frame budget.
- ./world-streaming-procgen.md — the
DT_ProcgenChunk/DT_WorldPartitionProfiledata tables this pipeline validates and feeds to the streaming and procgen solvers. - ./gas-layout.md — the GAS module stack
V4Gameplay.Build.csdepends on and the gameplay-tag tree this pipeline validates. - ../V4_ARCHITECTURE.md — the orientation hub.