Itemization Tooling Suite¶
Summary: An editor-only (
ProjectEternalEditormodule) toolset that closes the feedback loop between authoring itemization numbers and seeing their player-facing consequences. It lets designers simulate rolls / loot drops / crafts before committing content, audit dead modifiers and archetype coverage, find where any modifier or item is referenced, validate data on save + in CI, and push seeded items straight into PIE. Every simulator reuses the runtime generation code through a shared subsystem, so sim results equal in-game results.
Table of Contents¶
- Why This Exists
- Shared Foundation
- Tool Inventory
- Roll Simulator
- Loot Manager & Drop Simulator
- Craft Simulator & Greed Monte Carlo
- Tooltip Preview
- Archetype Coverage Matrix
- Where-Used Index
- Data Validation
- Send-to-PIE
- Authoring Console Commands (headless)
- Source References
- Tips & Gotchas
- Related Systems
- Recent Changes
Why This Exists¶
Project Eternal takes a no-rarity stance: every modifier, base type, loot entry, and recipe must deserve its slot. But distribution is invisible until PIE — designers author weights and tiers blind. The feedback loop is the tool. The suite makes weight tuning, dead-content detection, and player-facing tooltip verification a pre-commit editor workflow instead of a runtime guess.
The core credibility principle: simulators never reimplement game logic. They call the same runtime generation/tooltip code the game calls, seeded with an FRandomStream, so a sim roll and a PIE roll from the same seed are identical.
Shared Foundation¶
┌────────────────────────────────────────────────────────────────┐
│ UEditorItemSimSubsystem (UEditorSubsystem) │
│ • owns an editor UModifierPoolManager (live + aux + echo + │
│ greed pools) │
│ • GenerateItems / GenerateFromManifest(Asset) → │
│ RUNTIME UItemGenerationLibrary::GenerateItemProperties │
│ with a seeded FRandomStream │
│ • refreshes on asset-registry changes │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ UItemizationIndexSubsystem (UEditorSubsystem) │
│ • scans base types, manifests, ALL pools, recipes, loot │
│ tables → reverse maps │
│ • "which pools/base-types/materials roll this modifier?" │
│ • "which loot tables drop / recipes consume this item?" │
│ • EnsureCurrent() auto-refresh on query │
└────────────────────────────────────────────────────────────────┘
│ both feed the shared Slate widgets ▼
SDistributionHistogram · SItemTooltipPreview · SRollResultList · SWhereUsedPanel
Runtime prep (Phase 0, behavior-preserving): an FRandomStream was threaded through ItemGenerationLibrary → ModifierPoolManager::SelectRandomModifiers (default path unchanged when no stream is supplied); ArchetypeTags (TArray<FGameplayTag>) added to FItemBaseTypeDefinition + UItemManifestDataAsset (manifest wins when set); dev-only UEternalCheatManager added the GiveItem <ItemID> [ItemLevel] [Seed] exec command routed through the existing UItemSpawner server path.
Tool Inventory¶
| Tool | What it does | Entry point |
|---|---|---|
| Roll Simulator | Seeded batch-generate N items; histogram appearance %, tier/affix-count distributions, family saturation; dead-mod audit | Modifier Manager (3rd pane) |
| Loot Drop Simulator | Simulate N kills vs a loot table; drops-per-entry, items-per-kill, ilvl spread, currency totals | Loot Manager (sim panel) |
| Tooltip Preview | Live in-game tooltip render of any roll (no drift — real ViewModel) | Item Manager, Roll/Loot/Craft results |
| Craft Simulator | Run N real crafts through actual operation CDOs; outcome + per-family change histograms, essence totals | Crafting Manager (3rd pane) |
| Greed Monte Carlo | Repeated greed accrual/consumption trials; crafts-to-consumption, blessing distribution | Crafting Manager → Craft Sim section |
| Archetype Coverage | Archetype.* × 12 GDD slots dashboard; flags empty / content-starved cells | Archetype Coverage tab |
| Where-Used Index | Reverse references for any modifier / item, with jump links | Modifier & Item Manager details panels |
| Data Validation | On-save + commandlet validators for pools, base types, recipes, loot tables + JSON mirror-drift | Manager "Validate" buttons; -run=EternalValidation (CI + pre-push, see Content Validation) |
| Send-to-PIE | Spawn/stage a seeded item in a running PIE session | Item preview, Roll Sim rows, Crafting toolbar |
| Authoring Console Commands | Headless JSON modifier-pool import/export (deltas + canonical full-state sync) + GE modifier-row / CDO retune (reviewable, scriptable) | Editor console (Eternal.*) |
Roll Simulator¶
Modifier Manager's third pane. Pick a base type + item level, generate N seeded items, and read aggregated distributions:
- per-modifier appearance % (histogram)
- per-modifier tier distribution
- affix-count distribution
- modifier-family saturation
- dead-mod audit (static analysis surfacing mods that can never roll or never appear)
Per-mod focus mode isolates a single modifier's behavior. Results feed Tooltip Preview and the Send-to-PIE bridge.
Loot Manager & Drop Simulator¶
A dedicated manager for loot-table assets: table editing, parent-chain effective view (what an entry actually drops after inheritance), and a drop simulator that runs N kills and charts drops-per-entry, items-per-kill, item-level spread, and currency totals — validating the no-rarity lens at drop time. Sample drops render through Tooltip Preview. A FAssetTypeActions_LootTable adds an "Open in Loot Manager" context action on loot-table assets.
Craft Simulator & Greed Monte Carlo¶
Crafting Manager's third pane:
- Craft Simulator — runs N real crafts against a seeded target item through the actual operation CDOs and real ingredient manifests, aggregating outcome distribution, per-family modifier-change histograms (added/removed/rerolled), essence costs, and validation-failure tallies.
- Greed Monte Carlo (collapsible section) — simulates repeated greed accrual/consumption cycles to tune the Greed blessing distribution and consumption thresholds: crafts-to-consumption histogram, cumulative P(consumed ≤ N), mean/median, per-state dwell, and blessing distribution.
Tooltip Preview¶
SItemTooltipPreview snapshots a manifest into a transient UItemObject, runs the runtime UItemTooltipViewModel::UpdateFromItem, and renders the result in Slate — no reimplementation, no drift. Detailed/simple toggle. It appears in the Item Manager right pane and inline in every simulator's result set, so designers always see the exact text, tier, and state a player would.
Archetype Coverage Matrix¶
A dashboard plotting Archetype.* tags against the 12 GDD equipment slots. Each cell shows item count and anchor count and red-flags empty cells — enforcing the "no archetype content-starved" constraint. Exports to clipboard as markdown.
The matrix reads
ArchetypeTagson base types / manifests. It stays all-red until content is tagged — tagging is authoring work, not a tooling bug.
Where-Used Index¶
SWhereUsedPanel, embedded in the Modifier Manager and Item Manager details panels, lazily queries UItemizationIndexSubsystem for reverse references: which pools/base-types/materials reference a modifier; which loot tables drop / recipes consume or produce an item. Expandable sections carry jump links that navigate to the target manager tab.
Data Validation¶
UEditorValidatorBase subclasses run on save and headlessly via the EternalValidation commandlet,
which CI runs on every PR and the pre-push hook runs when Content/** changes — delivery, exit codes, and
the full validator registry live in Content Validation.
Shared checks live in ItemizationValidationCore.
| Validator | Catches |
|---|---|
UModifierPoolValidator |
Cross-pool duplicate IDs, level ranges, TargetProperty whitelist (must be in TagsToAttributes), unreachable tiers (the "rolls 0.0" trap), family tags, spawn-weight carriers, Local-scope string rules, asset↔JSON mirror drift (per-property compare against Tools/*.json marked {"sync": true} — drift means "importing would change the pool") |
UItemBaseTypeValidator |
Duplicate BaseTypeIDs, ImplicitModifierID resolution, bSupportsModifiers coverage |
UCraftingRecipeValidator |
Ingredient ItemID/tag resolution, outcome manifests, weights, operations |
ULootTableValidator |
Parent cycles, null manifests, non-positive weights, ItemLevelOffsetRange sanity |
Send-to-PIE¶
EditorPieUtils bridges editor → a running in-process PIE session. "Spawn in PIE" (Item preview, Roll Sim rows) and "Stage in PIE" (Crafting toolbar) route an ItemID + optional ItemLevel/Seed through UEternalCheatManager::GiveItem (or the default server-RPC path). Because both sides share the seed and the runtime generation code, a previewed roll reproduces exactly in-game.
| Helper | Purpose |
|---|---|
IsPieRunning() |
In-process PIE presence check |
GetPieCheatManager() |
Cheat manager of the first PIE world that has a local player controller — the listen host on a listen session, the first client on a dedicated one |
TryGiveItemInPie() |
Give a seeded item; toast on completion |
GetPieCheatManager() walks every PIE world context rather than calling GetPIEWorldContext(): under
bLaunchSeparateServer that accessor resolves the dedicated-server context, which has no local player
controller at all — the client world beside it does. Walking makes the result deterministic in both
session shapes.
Related cheat execs¶
Beyond GiveItem, UEternalCheatManager carries two navigation execs that pair with seeded-item testing:
| Exec | Purpose |
|---|---|
TeleportToRoom <query> |
Teleport the pawn to a placed dungeon room/tile. Query matches a room's slot tag or room-data asset name by substring, or is a numeric room ID from ListDungeonRooms. Lands on the navmesh nearest the tile centre |
ListDungeonRooms |
Read-only log of every placed content room/tile — room ID, slot tag, room-data asset name, world centre. These are the IDs TeleportToRoom accepts |
Both require authority. A client can still invoke them: the player controller relays a cheat string to the server, which dispatches it through the server-side cheat manager (see Unreal MCP & Python Guide).
Authoring Console Commands (headless)¶
Six editor-console commands (runtime module, #if WITH_EDITOR) let modifier-pool and Blueprint/GE
retunes happen as reviewable, scriptable text instead of Designer clicks — the same reason the
simulators exist for numbers. All of them load the target asset, edit it (or export it), and
SavePackage/write in place; none run in a cooked build.
| Command | Purpose |
|---|---|
Eternal.Modifiers.ImportJson <JsonPath> [PoolObjectPath] |
Apply a JSON delta (remove / upsert modifier definitions) — or, with the sync flag, a wholesale replace — to a UModifierPoolDataAsset, rebuild its ID cache, and save |
Eternal.Modifiers.ExportJson [OutPath] [PoolObjectPath] |
Export all of a pool's definitions to sync-importable JSON (default Tools/ModifierPool.json) — the canonical-pool workflow below |
Eternal.GE.DumpModifiers <BlueprintPath> |
List a GameplayEffect Blueprint's modifier rows — index, attribute, op, static value |
Eternal.GE.SetModifierValue <BlueprintPath> <AttributeName> <NewValue> [RowIndex] |
Replace a GE modifier row's static magnitude (refuses non-static ScalableFloat); RowIndex disambiguates when multiple rows target the same attribute |
Eternal.BP.DumpCDO <BlueprintPath> [PropertyNameFilter] |
Dump CDO property values via ExportText, optionally filtered by property-name substring |
Eternal.BP.SetCDOProperty <BlueprintPath> <PropertyName> <ValueText...> |
Import a property value from ExportText and save. Now works on plain UObjects / data assets too — LoadBlueprintCDO falls back to the object itself and saves its own package |
Eternal.GE.SetModifierValueandEternal.BP.SetCDOPropertyuse UE ExportText syntax for the value; quote it when it contains spaces (e.g. a tag container"(GameplayTags=((TagName=\"Event.Hit.Blocked\")))"). A sibling PIE-only diagnostic,Eternal.Debug.DumpAttributes [ActorNameSubstring], dumps a running actor's ASC attributes/tags but is not an authoring command.
Eternal.Modifiers.ImportJson — Delta Format¶
JsonPath is absolute or relative to the project root; convention is Tools/ModifierImports/*.json for
deltas and Tools/ModifierPool.json for the canonical file. PoolObjectPath defaults to
/Game/DataAssets/Items/Modifiers/ModifierPool.ModifierPool. The JSON root supports:
| Key | Type | Effect |
|---|---|---|
removeIDs |
["modifier_id", ...] |
Each ID is RemoveAll-ed from ModifierDefinitions (warns if it matched nothing) |
upsert |
[{ FModifierDefinition }, ...] |
Each entry is deserialized to an FModifierDefinition, then replaced in place when a definition with the same ModifierID exists, else appended |
sync |
true |
The upsert array is the pool: ModifierDefinitions is replaced wholesale. Aborts without touching the pool if any entry fails the skip guards (a skip under sync would mean silent deletion), and refuses to combine with removeIDs |
pool |
"/Game/.../Pool.Pool" |
The pool this file belongs to (written by ExportJson). Used as the import target when no [PoolObjectPath] arg is given; a mismatching explicit arg aborts — so a sync file can never wipe the wrong pool |
Canonical Pool Workflow (Tools/ModifierPool.json)¶
Tools/ModifierPool.json — written by Eternal.Modifiers.ExportJson, carrying "sync": true — is the
reviewable source of truth for the pool. The loop:
- Edit
Tools/ModifierPool.json(or re-export after a Designer-side edit — see caution below). Eternal.Modifiers.ImportJson Tools/ModifierPool.json— sync-replaces the pool; on-save validator gates.- Commit the JSON and the pool uasset together. The JSON's git diff is the balance changelog.
Dated files in Tools/ModifierImports/ remain as history but are no longer the way to make changes.
Caution: a sync import destroys any Modifier Manager hand-edit made since the last export. After any Designer-side pool edit, run
Eternal.Modifiers.ExportJsonbefore editing the JSON. Exported field keys are camelCase (FJsonObjectConverterstandardized case); import matching is case-insensitive, so both spellings work.
Field values follow FJsonObjectConverter conventions: enums by name ("Prefix", "Global", "Flat"),
gameplay tags as bare already-registered strings, and the tier map as nested Int32Range objects
("TiersValueRanges": {"Tier1": {"LowerBound": {"Type": "Inclusive", "Value": -30}, "UpperBound": ...}}).
An unregistered tag imports as None and would author a dead mod, so register tags first.
An upsert entry is skipped (logged) when it converts with an empty ModifierID, has an invalid
ModifierFamily, or has no valid TargetProperty while also being non-Local scope and having
no GrantedAbility. That last exception is what lets granted-ability-only proc affixes import: a
proc has no stat line, so it legitimately carries no TargetProperty — the ability is granted on equip
instead (see Item Generation for that modifier shape).
Worked example — Tools/ModifierImports/bleed_on_block_2026-07-02.json upserts a single
granted-ability-only suffix (no TargetProperty, no TiersValueRanges):
{
"upsert": [
{
"ModifierID": "bleed_on_block_suffix",
"ModifierName": "of Reprisal",
"ModifierFamily": "Modifiers.Family.BleedOnBlock",
"ModifierType": "Suffix",
"ModifierScope": "Global",
"ModifierOperation": "Flat",
"GrantedAbility": "/Game/Gameplay/GameplayAbilities/OnHit/GA_OnBlock_Bleed.GA_OnBlock_Bleed_C",
"MinimumItemLevel": 10,
"DescriptionTemplate": "Blocked Attackers Bleed",
"SpawnWeights": [
{ "ItemTag": "GameItems.Equipment.Weapons.OneHanded.Shield", "Weight": 10, "bRequired": false }
]
}
]
}
Validator Interaction¶
ImportJson does not validate the content itself beyond the per-entry skip guards above — after
applying the delta it calls RebuildLookupCaches() and saves. The real check is the on-save
UModifierPoolValidator, which runs during SavePackage and emits AssetCheck log
lines — read those to confirm the import is clean. That validator skips tier-coverage for the
granted-ability-only proc shape (!TargetProperty.IsValid() && GrantedAbility), so an affix with no
TiersValueRanges is not flagged as "rolls 0.0".
Source References¶
| Component | Location |
|---|---|
UEditorItemSimSubsystem |
Source/ProjectEternalEditor/Public/Shared/Simulation/EditorItemSimSubsystem.h |
UItemizationIndexSubsystem |
Source/ProjectEternalEditor/Public/Shared/Indexing/ItemizationIndexSubsystem.h |
| Shared widgets | Source/ProjectEternalEditor/Public/Shared/Widgets/ (SDistributionHistogram, SItemTooltipPreview, SRollResultList, SWhereUsedPanel) |
| Roll Simulator | Source/ProjectEternalEditor/Public/ModifierManager/SModifierRollSimPanel.h |
| Loot Manager | Source/ProjectEternalEditor/Public/LootManager/ (SLootManagerTab, SLootSimPanel, SLootDetailsPanel, SLootListPanel) |
| Craft Sim / Greed MC | Source/ProjectEternalEditor/Public/CraftingManager/ (SCraftSimPanel, SGreedMonteCarloPanel) |
| Item preview pane | Source/ProjectEternalEditor/Public/ItemManager/SItemManifestPreviewPane.h |
| Archetype Coverage | Source/ProjectEternalEditor/Public/ArchetypeCoverage/SArchetypeCoverageTab.h |
| Validators | Source/ProjectEternalEditor/Public/Validation/ (ItemizationValidationCore + 4 validators) |
| Send-to-PIE | Source/ProjectEternalEditor/Public/Shared/EditorPieUtils.h |
| Cheat exec (runtime) | Source/ProjectEternal/Public/Debug/EternalCheatManager.h |
| Tab/menu registration | Source/ProjectEternalEditor/Private/ProjectEternalEditor.cpp |
Eternal.Modifiers.ImportJson / ExportJson console commands |
Source/ProjectEternalEditor/Private/Authoring/ModifierImportCommand.cpp |
Eternal.GE.* / Eternal.BP.* console commands |
Source/ProjectEternalEditor/Private/Authoring/GameplayEffectEditCommand.cpp |
| Canonical modifier-pool JSON (sync full-state) | Tools/ModifierPool.json |
| Modifier-pool import JSON deltas (history only) | Tools/ModifierImports/*.json |
Tips & Gotchas¶
| Gotcha | Detail |
|---|---|
| Archetype matrix all-red | Expected until ArchetypeTags are authored on base types/manifests |
| Tag-only ingredients | Crafting recipes that match ingredients by tag (not ItemID) are not simulated |
| Manifest vs base-type tags | When a manifest sets ArchetypeTags, it overrides the base type's |
| Seeds | Same seed + same content = identical roll in sim and PIE; change content and old seeds drift |
| Blueprint crafting operations | Disabled in sim (CDO execution path is C++-only) |
Related Systems¶
- Crafting System — operations the Craft Sim executes
- Greed System — tuned by the Greed Monte Carlo
- Loot System — tables the Loot Sim drives
- Item System —
UItemObject/ manifest the sim builds - Unreal MCP + Python Automation Guide — companion editor-automation reference
Recent Changes¶
| Date | Change | Impact |
|---|---|---|
| 2026-06-11 | Itemization Tooling Suite (phases 0–7) | Shared UEditorItemSimSubsystem (runtime-accurate seeded gen) + UItemizationIndexSubsystem (reverse index); Roll/Loot/Craft simulators + Greed Monte Carlo; live Tooltip Preview; Archetype Coverage matrix; Where-Used panels; 4 on-save/commandlet validators; Send-to-PIE bridge |
| 2026-07-02 | Headless authoring console commands | Added Authoring Console Commands: Eternal.Modifiers.ImportJson (JSON remove/upsert deltas → UModifierPoolDataAsset, rebuilds ID cache, saves; validated on save by UModifierPoolValidator, which now skips tier-coverage for granted-ability-only proc affixes) plus Eternal.GE.DumpModifiers / Eternal.GE.SetModifierValue (GE modifier-row retune) and Eternal.BP.DumpCDO / Eternal.BP.SetCDOProperty (the latter now edits plain UObjects / data assets, not just Blueprint CDOs). Delta files live in Tools/ModifierImports/*.json |
| 2026-08-06 | Send-to-PIE resolution documented as "first PIE world with a local PC"; added the TeleportToRoom / ListDungeonRooms execs and the client→server cheat relay |
GetPIEWorldContext() resolves the pawnless dedicated-server world under bLaunchSeparateServer, so the bridge walks every PIE context instead; the room execs give seeded-item tests a way to reach the content that drops them |