# Build, Data & Content Pipeline

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](../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`), a `Jenkinsfile.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_ by
  `apps/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_*.json` data tables under
  `V4/ue/Content` are validated by `v4-data-linter.mjs` (required-field and
  schedule-graph checks), the 891 asset stand-ins by `ue-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`) by `v4-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
  `.uasset` binaries, exactly 1 real `.umap`** (`V4Core/Maps/BootMap.umap`, 15
  KB, authored by `create_boot_map.py`), against **891 `.v4asset.json`
  stand-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](./online-services-persistence.md) 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](./gas-layout.md)
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`:

1. **Build Editor** —
   `Compile Target="V4Editor" Platform="Linux" Configuration="Development"` (the
   Linux editor is what cooks headlessly).
2. **Cook Platform** — `Cook` with arguments
   `-unversioned -compressed -iterate -ddc=$(ReferenceDDC)`, followed by a
   `ValidateCook` command.
3. **Author Differential Patch** — a `V4DiffPatch` command feeding
   `V4/release/patches/differential-patches.json`.
4. An **`Aggregate`** named `BuildCookPatch` tying 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 (`6442450944` bytes); the estimate must not exceed it; the
  patch must carry **no base-install content** (`contentInPatch === false`); and
  it must include both an `anti_cheat_update` and a `closed_beta_balance`
  payload. 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 be
  `patch-service`, global rollout must be ≤ 30 minutes, the **only** allowed
  payload type is `data_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`,
  and `rollback_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](./world-streaming-procgen.md) 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 `telemetryEvidence` must specify a sample
  window ≥ 7 days, a minimum match count ≥ 10,000, a dashboard path, and the
  four metrics `pickRatePercent`, `winRateDeltaPercent`,
  `counterplaySuccessPercent`, `matchCount`. Every individual change must cite a
  `metricSource` and a match count at or above that floor — a balance note
  without telemetry behind it fails.
- **Balanced change set.** Entries must include **both** `Buff` and `Nerf`
  changes, each with `target`, `before`, `after`, and `rationale` — 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_:
  `winRates` and `matchCounts` must 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 === 6` and dodge i-frames at
  `dodgeIFrameFrames.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](./telemetry-observability-performance.md).

```mermaid
flowchart TD
  subgraph Authoring [Authored source, text-only]
    DT[19 DT_*.json DataTables]
    TAGS[DefaultGameplayTags.ini]
    LEDGER[public-balance-ledger.json<br/>balance-operations.json]
    STANDIN[891 .v4asset.json stand-ins]
  end
  subgraph Gates [CI validators · v4/scripts]
    DLINT[v4-data-linter.mjs<br/>required fields + schedule graph]
    ALINT[ue-asset-linter.mjs<br/>naming · poly · refs · cycles]
    BAL[v4-public-balance-ledger-check.mjs<br/>telemetry evidence + matrices]
    BCP[v4-build-cook-patch-check.mjs<br/>manifest + budgets]
  end
  subgraph Pipeline [Build pipeline · config only]
    BG[V4BuildCookPatch.xml]
    MAN[build-cook-patch.json<br/>10 platforms · strip profiles]
    PATCH[day-one ≤6GiB · hotfix · differential]
  end
  DT --> DLINT
  TAGS --> DLINT
  STANDIN --> ALINT
  LEDGER --> BAL
  MAN --> BCP
  BG --> BCP
  PATCH --> BCP
  DLINT --> CI{PR gate}
  ALINT --> CI
  BAL --> CI
  BCP --> CI
  CI -.->|no binary depot attached| COOK[(cooked .uasset / .umap<br/>NOT in repo)]
```

## 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_PREFIXES` table 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 `references` entry 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_Move` must include the `UV4CoverStickInputModifier`; cover-anchor
  graphs must declare a `mapId`, ≥ 2 anchors with 3D location and normal, and
  edges that only reference existing anchors.
- **Plugin cycles.** `.uplugin` descriptors 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](./game-modes-live-service.md) 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](./presentation-pipelines.md); the cooked binaries
themselves remain the project's open frontier.

## Related

- [./presentation-pipelines.md](./presentation-pipelines.md) — the
  audio/VFX/HLOD and roster art this pipeline budgets and lints but does not
  bake.
- [./telemetry-observability-performance.md](./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](./world-streaming-procgen.md) — the
  `DT_ProcgenChunk` / `DT_WorldPartitionProfile` data tables this pipeline
  validates and feeds to the streaming and procgen solvers.
- [./gas-layout.md](./gas-layout.md) — the GAS module stack
  `V4Gameplay.Build.cs` depends on and the gameplay-tag tree this pipeline
  validates.
- [../V4_ARCHITECTURE.md](../V4_ARCHITECTURE.md) — the orientation hub.
