0/0
CRADL // DOCUMENTATION
PORTAL DEV WIKI EXAMINE_IMPLEMENTATION
UTC 00:00:00
RETURN
EXAMINE_IMPLEMENTATION.md 2587 words ~12 min read Updated 2026-07-13

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 FExamineRow updates UCradlExamineTableValidator under 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.bat call (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 : FTableRowBaseFName ExamineKey (mirrors row name, same convention as FItemRow::ItemId), FText Description. No tag fields, no other data.
  • [x] Settings — Source/CRADL/Interaction/CradlExamineSettings.h / .cpp (new):
  • [x] UCradlExamineSettings : UDeveloperSettings with a soft UDataTable pointer for DT_Examine — mirror the shape of UCradlInventorySettings (Source/CRADL/Inventory/CradlInventorySettings.h).
  • [x] Registry — Source/CRADL/Interaction/ExamineRegistry.h / .cpp (new):
  • [x] UExamineRegistry : UGameInstanceSubsystemInitialize loads the settings table (LoadSynchronous is acceptable here only, per the contract's registry footgun), row-struct-checked LoadFromTable (reject non-FExamineRow tables, mirroring UItemRegistry::LoadFromTable), keyed store, instance FindDescription(FName Key) + static FindDescription(const UObject* WorldContext, FName Key).
  • [x] Static key helpers + suffix constants (MakeItemKey and siblings per the contract's Key grammar) — the @ literal and suffix strings live here and nowhere else.
  • [x] UExamineItemPayload — minimal transient UObject, UPROPERTY()-decorated FName ItemId carrier 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_Examine with row struct FExamineRow; point UCradlExamineSettings at 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).
  • UCradlExamineSettings appears in Project Settings; DT_Examine accepts a hand-authored Coin@item row (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] UEditorValidatorBase subclass; CanValidateAsset on row struct == FExamineRow (mirror UCradlItemTableValidator in Source/CRADLEditor/Validators/CradlItemTableValidator.cpp).
  • [x] Grammar (Error): exactly one @, non-empty id, suffix in the closed set; ExamineKey field mirrors row name.
  • [x] Worklist (Error): empty Description fails — the seeded-stub authoring queue.
  • [x] Cross-resolution (Warning, orphan = stale-row signal, never deletion): @item in UCradlInventorySettings::ItemTable row map; @node/@enemy resolve to a UGatheringNodeDefinition / UEnemyDefinition asset by name; @station/@teleport resolve via string tag request; @terminal in 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 empty Description, -DryRun, MarkPackageDirty + UPackage::SavePackage.
  • [x] Six native enumerators, no python stage: @item from UCradlInventorySettings::ItemTable row map (skip RetiredItems ids and, defensively, any row name containing @); @node/@enemy from an asset-registry class scan; @station/@teleport from tag-manager descendants of Station / Teleport.Network (including modal-only virtual stations like Station.Alchemy, per the contract's seeding footgun); @terminal from the hardcoded literal set.
  • [x] Compose every key through the UExamineRegistry static helpers — no @ literals in the commandlet.
  • [x] VS Code tasks — .vscode/tasks.json: UE: Seed Examine Rows (Dry Run) and UE: Seed Examine Rows (-run=CradlSeedExamineRows), mirroring the item-seeder tasks but with no python dependsOn.

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 empty Description (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 — resolve Station / Teleport.Network via UGameplayTagsManager / FGameplayTag::RequestGameplayTag string requests, never CradlTags:: symbols (LNK2001, per reference_native_tag_no_cross_module_export.md).
  • Commandlets run before the asset registry finishes its async scan — force a synchronous scan before the @node/@enemy enumeration or those suffixes silently seed nothing.
  • Do not touch UCradlItemTableValidator's @-reservation check — it is scoped to FItemRow tables 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 OptionalObjectUExamineItemPayload; step 2 OptionalObjectIItemContainer (Source/CRADL/Inventory/ItemContainerInterface.h) + slot decode via the CradlAbilityPayload helpers (Source/CRADL/Abilities/ItemEventMagnitude.h), empty slot → fallback; step 4 fallback = the existing LOCTEXT("ExamineStub", "Nothing of interest").
  • [x] Shared item resolution: UItemRegistry::IsNotedId before key composition → canonical noted text (OSRS-style swap-at-any-bank copy; designer-tunable via UCradlExamineSettings::NotedItemDescription, code-side default as empty-setting guard); otherwise MakeItemKey(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, no SourceLabel prefix.
  • [x] Test fixture — Source/CRADL/Player/CradlDebugComponent.h / .cpp (modify):
  • [x] Exec function (Debug_ExamineKey <key> — codebase-uniform Debug_ prefix) that runs UExamineRegistry::FindDescription on a raw key and posts the result through the same message channel — declaration unconditional, body #if !UE_BUILD_SHIPPING guarded 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@item round-trips the registry directly; CradlExamineKey garbage@item posts 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.

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 the NAME_None default and emit no Examine action.

Footguns.

  • Full tag names in keys (Station.Smithing.Ingot, not Ingot) — 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 ExamineKeyOverride UPROPERTY, 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, never bDefault) after the existing Combat.Engage entry in GatherActions (reuses the Engage row's appraised SourceLabel + color so both rows read identically).
  • [x] GetExamineKey() override — {ActiveDefinition asset name}@enemy via the registry helper; null ActiveDefinitionNAME_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 (GatherActions runs 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 (named ActionPayload per the shadowing rule), UPROPERTY()-decorated per CLAUDE.md's undecorated-UObject* ban.
  • [x] Source/CRADL/Interaction/InteractionComponent.cpp (modify): DispatchContextAction forwards the field as OptionalObject in the branch that today dispatches an empty FGameplayEventData (no SourceActor, no SourceContainer). World and container branches untouched.
  • [x] Source/CRADL/UI/StoreWidget.cpp (modify):
  • [x] The buy menu (HandleEntryRightClicked) fills a UExamineItemPayload with the id the entry rides (BagBoundId — the noted twin while the Note toggle is on, mirroring DispatchTransact) — 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 UObject in OptionalObject is safe only because Examine is LocalOnly — 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 @store suffix, 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.