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
- Why a Split
- Current State
- JSON-First Systems
- UASSET-First Systems
- The Import Commandlet Pattern
- Non-Negotiables
- Migration Order
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
_commentfields for inline notes — JSON has no comment syntax. Sidecar.mdfiles for longer rationale. - IDs are
snake_casestrings, stable forever (they're foreign keys). - Soft references to UE assets (abilities, textures) are stored as path strings or
ClassName_Cidentifiers. 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 + theEternalValidationcommandlet — 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.
- The import commandlet hard-fails on unknown or missing fields. Schema drift is the #1 silent-failure mode.
- CI runs the import commandlet on every PR. Broken JSON never reaches
main. - 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. - 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.
- IDs are immutable. Renaming a modifier ID is a migration, not an edit. Add a deprecation alias if needed.
- 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.
- 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.
- 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. - If step 2 is a clear win, build the importer + CI hook. Modifiers become JSON-first. Modifier Manager becomes a viewer.
- Crafting recipes follow if and only if step 3 was a clear win. Don't migrate two systems based on one data point.
- 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.