CRADL Examine Implementation
Companion to EXAMINE_SYSTEM.md (the contract) and ARCHITECTURE.md. This doc tracks the build order for v1 examine: phased delivery, per-phase rationale, task checklists, and verification gates. The contract doc says what examine is; this doc says what we build first, what depends on what, and how we know each step works.
This build extends live systems — the Examine verb, UExamineAbility, context-menu dispatch, and every emit site already exist on develop; the work adds the description data spine, editor tooling, and identity plumbing behind them. No prior implementation doc is a prerequisite.
Conventions
- Phase status legend:
[ ]not started ·[~]in progress ·[x]done ·[!]blocked / deferred. - Verification gate: every phase ends with a runnable demo / observable behavior. If a phase can't be verified end-to-end, it's split.
- Cheat commands: test fixtures land under
UCradlDebugComponent(Source/CRADL/Player/CradlDebugComponent.h) exec functions guarded by#if !UE_BUILD_SHIPPING. Per CLAUDE.md, declarations are unconditional; only the body is guarded. - Per CLAUDE.md "validators in lockstep": any phase that touches
FExamineRowupdatesUCradlExamineTableValidatorunder Source/CRADLEditor/Validators/ in the same change. - Per CLAUDE.md "Building": after the C++ edits for a phase land, Claude compiles with the documented
Build.batcall (UE: Build Editor (Development)) to verify the phase links clean before reporting it done. Runtime/PIE verification remains the user's job.
Phase tracking
| Phase | Title | Status | Unblocks |
|---|---|---|---|
| 0 | Data scaffolding — row struct, settings, registry | [x] (code; DT_Examine authoring is user-side) |
All later phases |
| 1 | Editor tooling — validator + seeder | [x] (code; seed run is user-side) |
Authoring worklist; 2's verification data |
| 2 | Resolution ladder — item paths live | [x] |
3, 4, 5 |
| 3 | World actor identity — GetExamineKey() + overrides |
[x] |
4 |
| 4 | Enemy examine | [x] |
— (parallel-able with 5) |
| 5 | Store entry payload path | [x] |
— (parallel-able with 3 & 4; depends only on 2) |
Phase 0 — Data Scaffolding
Goal. The description data spine compiles and loads: FExamineRow, UCradlExamineSettings, UExamineRegistry (with key helpers and UExamineItemPayload), and an authored-but-empty DT_Examine the registry ingests at GameInstance startup. No consumer behavior changes.
Rationale. Every later phase reads this spine — the validator and seeder need the row struct and settings pointer, the ability needs FindDescription and the key helpers. Mirrors the contract's Description store and Description registry sections, which in turn mirror UItemRegistry (Source/CRADL/Inventory/ItemRegistry.h) wholesale.
Tasks.
- [x] Row struct — Source/CRADL/Interaction/ExamineRow.h (new):
- [x]
FExamineRow : FTableRowBase—FName ExamineKey(mirrors row name, same convention asFItemRow::ItemId),FText Description. No tag fields, no other data. - [x] Settings — Source/CRADL/Interaction/CradlExamineSettings.h /
.cpp(new): - [x]
UCradlExamineSettings : UDeveloperSettingswith a softUDataTablepointer forDT_Examine— mirror the shape ofUCradlInventorySettings(Source/CRADL/Inventory/CradlInventorySettings.h). - [x] Registry — Source/CRADL/Interaction/ExamineRegistry.h /
.cpp(new): - [x]
UExamineRegistry : UGameInstanceSubsystem—Initializeloads the settings table (LoadSynchronousis acceptable here only, per the contract's registry footgun), row-struct-checkedLoadFromTable(reject non-FExamineRowtables, mirroringUItemRegistry::LoadFromTable), keyed store, instanceFindDescription(FName Key)+ staticFindDescription(const UObject* WorldContext, FName Key). - [x] Static key helpers + suffix constants (
MakeItemKeyand siblings per the contract's Key grammar) — the@literal and suffix strings live here and nowhere else. - [x]
UExamineItemPayload— minimal transientUObject,UPROPERTY()-decoratedFName ItemIdcarrier for Phase 5, declared in the same header. - [x] One load-summary log line (row count) at Warning level per
feedback_log_level_warning_for_diagnostics.md, so Phase 0 is PIE-observable before any consumer exists. - [ ] Content (user editor step): create
DT_Examinewith row structFExamineRow; pointUCradlExamineSettingsat it. - [ ] Tags: none — the contract's Tag Taxonomy introduces no new tags; nothing to add to the .ini or
CradlGameplayTags.h.
Verification.
- Compile clean (Claude builds via
Build.bat; links exit-0). UCradlExamineSettingsappears in Project Settings;DT_Examineaccepts a hand-authoredCoin@itemrow (user-side).- PIE: the registry's Warning-level load summary reports the hand-authored row count.
Exits. Phase 1 has a struct and settings pointer to validate/seed against; Phase 2 has FindDescription and the key helpers; Phase 5 has UExamineItemPayload.
Phase 1 — Editor Tooling: Validator + Seeder
Goal. The table polices itself and populates itself: UCradlExamineTableValidator enforces the key grammar and surfaces the authoring worklist; UCradlSeedExamineRowsCommandlet stubs every key from all six sources, add-missing-only.
Rationale. Tooling before behavior (test-fixture-before-content): once this lands, DT_Examine carries the full seeded key set as validator-flagged stubs, so every later phase's PIE verification has real rows to resolve against — and the CLAUDE.md validator-lockstep debt from Phase 0's new struct is settled.
Tasks.
- [x] Validator — Source/CRADLEditor/Validators/CradlExamineTableValidator.h /
.cpp(new): - [x]
UEditorValidatorBasesubclass;CanValidateAsseton row struct ==FExamineRow(mirrorUCradlItemTableValidatorin Source/CRADLEditor/Validators/CradlItemTableValidator.cpp). - [x] Grammar (Error): exactly one
@, non-empty id, suffix in the closed set;ExamineKeyfield mirrors row name. - [x] Worklist (Error): empty
Descriptionfails — the seeded-stub authoring queue. - [x] Cross-resolution (Warning, orphan = stale-row signal, never deletion):
@iteminUCradlInventorySettings::ItemTablerow map;@node/@enemyresolve to aUGatheringNodeDefinition/UEnemyDefinitionasset by name;@station/@teleportresolve via string tag request;@terminalin the literal set{bank, loadout, store}. - [x] Seeder — Source/CRADLEditor/Seed/CradlSeedExamineRowsCommandlet.h /
.cpp(new): - [x] Mirror
UCradlSeedItemRowsCommandlet(Source/CRADLEditor/Seed/CradlSeedItemRowsCommandlet.cpp): add-missing-only, key-only stubs with emptyDescription,-DryRun,MarkPackageDirty+UPackage::SavePackage. - [x] Six native enumerators, no python stage:
@itemfromUCradlInventorySettings::ItemTablerow map (skipRetiredItemsids and, defensively, any row name containing@);@node/@enemyfrom an asset-registry class scan;@station/@teleportfrom tag-manager descendants ofStation/Teleport.Network(including modal-only virtual stations likeStation.Alchemy, per the contract's seeding footgun);@terminalfrom the hardcoded literal set. - [x] Compose every key through the
UExamineRegistrystatic helpers — no@literals in the commandlet. - [x] VS Code tasks — .vscode/tasks.json:
UE: Seed Examine Rows (Dry Run)andUE: Seed Examine Rows(-run=CradlSeedExamineRows), mirroring the item-seeder tasks but with no pythondependsOn.
Verification.
- Compile clean (Claude builds via
Build.bat; links exit-0). - Dry-run task logs would-add counts per suffix; real run populates
DT_Examine; re-run adds zero (idempotent, add-missing-only). - Asset validation on
DT_Examine: every stub fails on emptyDescription(the worklist); authoring one description clears that row; a hand-mangled row name (Coin,Coin@bogus,a@b@c) fails grammar; a row keyed to a deleted/renamed source warns as orphan without failing the table.
Footguns.
- The commandlet and validator live in
CRADLEditor— resolveStation/Teleport.NetworkviaUGameplayTagsManager/FGameplayTag::RequestGameplayTagstring requests, neverCradlTags::symbols (LNK2001, perreference_native_tag_no_cross_module_export.md). - Commandlets run before the asset registry finishes its async scan — force a synchronous scan before the
@node/@enemyenumeration or those suffixes silently seed nothing. - Do not touch
UCradlItemTableValidator's@-reservation check — it is scoped toFItemRowtables and stays as-is.
Exits. DT_Examine holds the full stable key set; Phases 2–5 verify against seeded rows plus a starter batch of authored descriptions.
Phase 2 — Resolution Ladder: Item Paths Live
Goal. UExamineAbility resolves instead of stubbing: item payload (step 1) and container slot (step 2) run the shared item resolution; everything else falls through to the now-honest "Nothing of interest" fallback. Examining items in bag, bank, and equipment shows authored text in PIE.
Rationale. The three container emitters already dispatch everything the ladder needs (UInventoryComponent / UEquipmentComponent / UBankContainerComponent GatherSlotActions), so item examine goes live with zero emit-side changes — the fastest end-to-end proof of the whole spine. Step 3 (world actors) is deliberately absent until Phase 3; world examine keeps today's fallback behavior in the interim.
Tasks.
- [x] Ability — Source/CRADL/Abilities/ExamineAbility.h /
.cpp(modify): - [x] Replace the stub body with the ladder from the contract's Runtime resolution: step 1
OptionalObject→UExamineItemPayload; step 2OptionalObject→IItemContainer(Source/CRADL/Inventory/ItemContainerInterface.h) + slot decode via theCradlAbilityPayloadhelpers (Source/CRADL/Abilities/ItemEventMagnitude.h), empty slot → fallback; step 4 fallback = the existingLOCTEXT("ExamineStub", "Nothing of interest"). - [x] Shared item resolution:
UItemRegistry::IsNotedIdbefore key composition → canonical noted text (OSRS-style swap-at-any-bank copy; designer-tunable viaUCradlExamineSettings::NotedItemDescription, code-side default as empty-setting guard); otherwiseMakeItemKey(ItemId)→UExamineRegistry::FindDescription. - [x] Miss handling: no row or row with empty
Description(an unauthored stub) → fallback, never an empty toast. - [x] Keep
LocalOnly/ GameplayEvent trigger /PostMessage(Message.Source.Interaction, Info, …)exactly as-is; post the description text only, noSourceLabelprefix. - [x] Test fixture — Source/CRADL/Player/CradlDebugComponent.h /
.cpp(modify): - [x] Exec function (
Debug_ExamineKey <key>— codebase-uniformDebug_prefix) that runsUExamineRegistry::FindDescriptionon a raw key and posts the result through the same message channel — declaration unconditional, body#if !UE_BUILD_SHIPPINGguarded per CLAUDE.md.
Verification.
- Compile clean (Claude builds via
Build.bat; links exit-0). - PIE: examine an authored item in bag / bank / equipment → description toast; a seeded-but-unauthored item → "Nothing of interest"; a noted stack in the bag → the canonical note text.
- PIE:
CradlExamineKey Coin@itemround-trips the registry directly;CradlExamineKey garbage@itemposts the miss path. - PIE: world-actor and store examine still post the fallback (unchanged until Phases 3 and 5).
Footguns.
- No concrete container or actor types in the ability — the ladder touches only
IItemContainer, the payload class, and the registries (contract rule). - The noted branch must check the raw slot id before composing — composing first and string-inspecting after is the exact bug the contract's Noted items rule forbids.
Exits. The ladder exists; Phase 3 inserts step 3 (world actors), Phase 5 lights up step 1's emit side (store).
Phase 3 — World Actor Identity
Goal. IInteractable::GetExamineKey() exists with a NAME_None default, the ability's step 3 consumes it, and all seven world implementers answer: ground items, gathering nodes, crafting stations, teleport stations, and the three terminals show authored text in PIE.
Rationale. One interface addition unlocks every world emitter at once — each actor answers with identity it already owns (replicated item payload, definition asset, tag, or class literal), per the contract's Actor identity table. Depends on Phase 2's ladder.
Tasks.
- [x] Interface — Source/CRADL/Interaction/InteractableInterface.h (modify):
virtual FName GetExamineKey() const { return NAME_None; }alongsideGetActionPayload. - [x] Ability — Source/CRADL/Abilities/ExamineAbility.cpp (modify): insert ladder step 3 —
Target→IInteractable::GetExamineKey(),NAME_None→ fallback. (Landed with Phase 2's ladder rewrite alongside the interface default.) - [x] Overrides (each composes through the
UExamineRegistrystatic helpers — no@literals at call sites): - [x]
AGroundItem(Source/CRADL/World/GroundItem.cpp) —MakeItemKey(UItemRegistry::ResolveBaseId(Payload.ItemId)); DevComment recording the deliberate ground-noted asymmetry (base description, not note text) per the contract footgun. - [x]
AGatheringNode(Source/CRADL/World/GatheringNode.cpp) —{Definition asset name}@node; nullDefinition→NAME_None. - [x]
ACraftingStation(Source/CRADL/World/CraftingStation.cpp) —{StationTag full name}@station; invalid tag →NAME_None. - [x]
ATeleportStation(Source/CRADL/World/TeleportStation.cpp) —{NetworkTag full name}@teleport(per-network granularity is deliberate). - [x]
ABankTerminal(Source/CRADL/World/BankTerminal.cpp),ALoadoutTerminal(Source/CRADL/Loadout/LoadoutTerminal.cpp),AStoreTerminal(Source/CRADL/Store/StoreTerminal.cpp) — thebank/loadout/store@terminalliterals via the registry helper.
Verification.
- Compile clean (Claude builds via
Build.bat; links exit-0). - PIE: examine a ground item → the item's description (identical to bag examine of the same item); a gathering node, a crafting station, a teleport station, and each terminal → their authored text; unauthored stubs → fallback.
- Non-examine interactables (
AQuestGiverActor,AGatedInteractable) are unaffected — they inherit theNAME_Nonedefault and emit no Examine action.
Footguns.
- Full tag names in keys (
Station.Smithing.Ingot, notIngot) — contract Key grammar. - Do not invent definition assets or new tags for the terminals — per-class literals are the contract's answer.
- Descriptions stay strictly per-key: no
ExamineKeyOverrideUPROPERTY, rejected in the contract's Actor identity footguns.
Exits. Phase 4's enemy override has its virtual; world examine is feature-complete.
Phase 4 — Enemy Examine
Goal. Enemies become examinable: AEnemyCharacter::GatherActions appends a non-default Examine entry, and GetExamineKey() answers {ActiveDefinition asset name}@enemy.
Rationale. The one examinable category with no examine entry today. Identity is free — ActiveDefinition (UEnemyDefinition* in Source/CRADL/Enemy/EnemyDefinition.h) already replicates, and per-definition granularity matches OSRS per-monster examine. Depends on Phase 3's virtual only; parallel-able with Phase 5.
Tasks.
- [x] Source/CRADL/Enemy/EnemyCharacter.h /
.cpp(modify): - [x] Append the Examine
FContextAction(ActionTag = Action.Trigger.Examine, neverbDefault) after the existingCombat.Engageentry inGatherActions(reuses the Engage row's appraisedSourceLabel+ color so both rows read identically). - [x]
GetExamineKey()override —{ActiveDefinition asset name}@enemyvia the registry helper; nullActiveDefinition→NAME_None.
Verification.
- Compile clean (Claude builds via
Build.bat; links exit-0). - PIE: right-click an enemy → Examine appears below Engage; left-click still engages; examine → the definition's authored text; two instances of the same definition post identical text.
Footguns.
ATargetDummy(Source/CRADL/Combat/TargetDummy.cpp) stays Engage-only — decided in the contract; do not wire it.- The menu entry is client-local construction (
GatherActionsruns on the examining client) — nothing here replicates, per the contract's Replication audit.
Exits. Enemy examine complete; with Phase 5, every v1 examinable category is live.
Phase 5 — Store Entry Payload Path
Goal. The store buy menu's Examine entry resolves as an item: FContextAction gains its optional transient payload field, the dispatcher forwards it in the no-descriptor branch, and UStoreWidget::ShowBuyMenu fills a UExamineItemPayload — lighting up ladder step 1, which has been waiting since Phase 2.
Rationale. The store is the only emit site whose action carries neither SourceActor nor SourceContainer; the contract's Store entries section resolves it with the widget's own DispatchTransact/UTransactRequest precedent. Depends only on Phase 2; parallel-able with Phases 3 and 4.
Tasks.
- [x] Source/CRADL/Interaction/ContextAction.h (modify): optional transient payload object field on
FContextAction(namedActionPayloadper the shadowing rule),UPROPERTY()-decorated per CLAUDE.md's undecorated-UObject*ban. - [x] Source/CRADL/Interaction/InteractionComponent.cpp (modify):
DispatchContextActionforwards the field asOptionalObjectin the branch that today dispatches an emptyFGameplayEventData(noSourceActor, noSourceContainer). World and container branches untouched. - [x] Source/CRADL/UI/StoreWidget.cpp (modify):
- [x] The buy menu (
HandleEntryRightClicked) fills aUExamineItemPayloadwith the id the entry rides (BagBoundId— the noted twin while the Note toggle is on, mirroringDispatchTransact) — no noted→base pre-resolving in the widget; the shared item resolution's noted branch owns the note copy. - [x] Update the now-fulfilled "when item-aware examine ships" comment in the same change, per
feedback_no_clean_up_later.md.
Verification.
- Compile clean (Claude builds via
Build.bat; links exit-0). - PIE: right-click a vendor entry → Examine → text identical to examining the same item in the bag; with buy-as-note toggled (entries riding the noted id), examine posts the canonical note text.
- PIE: store Buy/transact flows unchanged (the dispatcher's other branches were not touched).
Footguns.
- The transient
UObjectinOptionalObjectis safe only because Examine isLocalOnly— the no-stable-NetGUID caveat in Source/CRADL/Abilities/ItemEventMagnitude.h applies to event data that crosses the wire. Don't copy this carrier into a replicated verb. - No
@storesuffix, no vendor-specific rows — store examine is item examine, byte-identical text.
Exits. All v1 examine surfaces live. Remaining work is pure content: burning down the validator worklist by authoring descriptions in DT_Examine.