Skip to content

Itemization Tooling Suite

Summary: An editor-only (ProjectEternalEditor module) 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

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 ItemGenerationLibraryModifierPoolManager::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 ArchetypeTags on 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.

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.SetModifierValue and Eternal.BP.SetCDOProperty use 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:

  1. Edit Tools/ModifierPool.json (or re-export after a Designer-side edit — see caution below).
  2. Eternal.Modifiers.ImportJson Tools/ModifierPool.json — sync-replaces the pool; on-save validator gates.
  3. 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.ExportJson before editing the JSON. Exported field keys are camelCase (FJsonObjectConverter standardized 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 exampleTools/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)


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