Skip to content

Data Authoring Pipeline

Summary: How game data (modifiers, recipes, items, balance, loot) should be stored, edited, and loaded in Project Eternal. The split between JSON-as-source-of-truth and UASSET DataAssets is a project-wide rule, not a per-system preference.

Table of Contents


The Rule

Pick storage by data shape and authoring frequency, not by personal preference.

Data shape Format Examples
Flat, reference-light, frequently-edited, AI-authorable JSON (source of truth) Modifier definitions, crafting recipes, balance config, loot/spawn weights
Asset-heavy, reference-rich, infrequently-edited UASSET (DataAsset) Item manifests + fragments, abilities, montages, levels, anything owning hard references to textures/meshes/sounds/classes

When in doubt: does this entity hold hard references to other UE assets? If yes → UASSET. If no → JSON.


Why a Split

Neither format wins outright.

UASSETs are good at: asset references (icons, meshes, abilities), polymorphism via TInstancedStruct, the property editor's tooltips/enum dropdowns/validation, asset registry rename fix-up, "find all references."

UASSETs are bad at: PR review (binary diffs are unreadable), merge conflicts, bulk edits, AI-authored changes, round-tripping (read → modify → write back without booting the editor), CI validation.

JSON is good at: PR review, merge resolution, bulk edits, LLM authoring, CI validation, hot-reload, format parity with the backend.

JSON is bad at: asset references (stringly-typed, no rename fix-up), polymorphism (manual discriminators), schema drift, no free editor UI.

The split exists because game content has both shapes. Forcing everything into one format creates pain on the side that doesn't fit.


Current State

System Format Notes
UBalanceSubsystem JSON in /Config/Balance/ Hot-reloadable via ReloadBalanceData(). Reference implementation.
Modifier definitions UASSET (UModifierPoolDataAsset) Migration candidate — see Migration Order
Crafting recipes UASSET (UCraftingRecipeDataAsset) Migration candidate (after modifiers)
Item manifests + fragments UASSET (UItemManifestDataAsset) Stay UASSET. Fragment polymorphism + asset references make JSON costly.
API persistence JSON (FJsonObjectConverter) Wire format only. Items reference ItemID strings; modifiers serialize inline.

Note: USTRUCTs already round-trip to JSON via FJsonObjectConverter::UStructToJsonObjectString. We are not locked in to either format.


JSON-First Systems

File layout

Content/Data/
├── Modifiers/
│   ├── Offensive/
│   │   ├── bleed_chance.json
│   │   └── physical_damage.json
│   └── Defensive/
└── Recipes/
    └── crafting/

Conventions

  • One entity per file when entities are heavyweight (modifiers, recipes). One file per pool/table when entities are tiny rows (loot weights).
  • Use _comment fields for inline notes — JSON has no comment syntax. Sidecar .md files for longer rationale.
  • IDs are snake_case strings, stable forever (they're foreign keys).
  • Soft references to UE assets (abilities, textures) are stored as path strings or ClassName_C identifiers. The importer is responsible for verifying they resolve.

Format choice: stay on JSON

Not YAML, not TOML, not JSON5. Reasons: - FJsonObjectConverter round-trips USTRUCTs for free. Other formats require a hand-written parser. - The backend already speaks JSON. Same format end-to-end. - LLMs produce JSON with the lowest error rate of any structured format. YAML's whitespace sensitivity is a real source of bad output.


UASSET-First Systems

Keep UASSET when any of these apply: - Entity owns hard references to other UE assets (textures, meshes, montages, GameplayCues, ability classes) - Polymorphism via TInstancedStruct (e.g. FItemFragment) - Editing is rare and per-instance variation matters - Live thumbnail previews / scene previews are valuable during authoring

Editor tools for UASSET-first systems (Modifier Manager, Crafting Manager, future Item Manager) follow the split-architecture convention in CLAUDE.md (top-level tab + list panel + details panel).


The Import Commandlet Pattern

When migrating a system to JSON, JSON is the source of truth and UASSETs are build artifacts, not the other way around.

Content/Data/Modifiers/*.json          ← humans + AI edit this
ImportModifiersFromJson commandlet     ← runs locally + in CI
UModifierPoolDataAsset (generated)     ← runtime keeps reading this

Why a commandlet, not runtime JSON loading: - No engine code changes for runtime systems - No cooking changes - Validation runs at import time, not per-game-launch - Runtime hot-path stays on UASSET (asset registry handles references, soft pointers, etc.) - Existing editor tools become viewers/validators, not authoring surfaces — they keep working

The exception is UBalanceSubsystem, which loads JSON directly because the data is shallow scalars and hot-reload during dev is the whole point. Don't copy that pattern for content that references other assets.


Non-Negotiables

Status (2026-07-08): these are requirements for IF/WHEN a system migrates to JSON-first — not a description of what exists. Today only the ModifierPool JSON mirror + import/export console commands exist; there is no import commandlet. CI now validates content on every PR (Content/** triggers + the EternalValidation commandlet — see Content Validation), including an asset↔JSON mirror-drift check for synced pools, which covers non-negotiable #2's intent for the mirrors that exist. Read the list below as the contract any future migration must satisfy.

These are the things that turn "JSON is great" into "JSON ruined everything" 18 months in. Skipping any of them is worse than not migrating at all.

  1. The import commandlet hard-fails on unknown or missing fields. Schema drift is the #1 silent-failure mode.
  2. CI runs the import commandlet on every PR. Broken JSON never reaches main.
  3. Every soft reference (GA_OnHit_Bleed_C, texture paths, gameplay tags) is resolved at import time. A reference that doesn't exist fails the import.
  4. Schema is documented in one place (this folder, or alongside the importer source). New fields require updating the doc and the importer in the same PR.
  5. IDs are immutable. Renaming a modifier ID is a migration, not an edit. Add a deprecation alias if needed.
  6. Editor tools become validators. When a system migrates to JSON, its editor tool stops being an authoring surface and starts being a read-only viewer that surfaces validation errors.

Migration Order

Don't migrate everything at once. Don't migrate two systems in parallel.

  1. Finish the Crafting Recipe Editor on its current branch. Ship it. Use it for real content (e.g. Test Experience #2 bleed shard). Get one more data point on whether the editor-tool approach scales.
  2. Build a JSON export commandlet for UModifierPoolDataAsset. One direction only. Run it. Inspect the output. Decide whether editing JSON is genuinely better than editing the asset.
  3. If step 2 is a clear win, build the importer + CI hook. Modifiers become JSON-first. Modifier Manager becomes a viewer.
  4. Crafting recipes follow if and only if step 3 was a clear win. Don't migrate two systems based on one data point.
  5. Item manifests stay UASSET until there's a concrete, named reason to move them. Fragment polymorphism cost is real.

When Adding a New Data System

Apply the rule from the top of this doc. If the new system is JSON-first, follow the import commandlet pattern and the non-negotiables. If it's UASSET-first, follow the editor tool conventions in CLAUDE.md.

Document the choice in the system's own implementation doc under ImplementationDocs/, with a one-line justification referencing the rule.