0/0
CRADL // DOCUMENTATION
PORTAL DEV WIKI EXAMINE_SYSTEM
UTC 00:00:00
RETURN
EXAMINE_SYSTEM.md 3981 words ~18 min read Updated 2026-07-13

CRADL Examine System

Companion to ARCHITECTURE.md. This document is the contract examine must satisfy — its description store, key grammar, runtime resolution ladder, actor identity surface, offline seeding, and validation. Implementation patterns, flavor-text authoring, and per-row copy live elsewhere; what's here does not change without a deliberate edit to this file.

North Star

Examine is OSRS's flavor verb: pure presentation, zero mechanics. Every examinable thing in the game answers from a single authored description table, and anything that can't produce a key — or produces one with no authored row — falls back to the honest "Nothing of interest" the stub posts today. Descriptions are data, never behavior: no gameplay state is read at examine time beyond identity, and nothing replicates. Examine reuses every dispatch pattern the context-menu system established: it adds a lookup, not a foundation.

Quick Reference

Topic Answer Section
Where do descriptions live? One parallel DataTable of FExamineRow (new) — no base-type extension Description store
How are rows keyed? {id}@{source}, exactly one @, one suffix per enumeration source Key grammar
Who serves lookups? UExamineRegistry (new) GameInstance subsystem, mirrors UItemRegistry Description registry
How does examine find its key? Resolution ladder inside UExamineAbility: container slot → actor GetExamineKey() → fallback Runtime resolution
How do world actors identify themselves? IInteractable::GetExamineKey() (new) virtual, default NAME_None Actor identity
Do enemies get examine? Yes — new GatherActions entry keyed off replicated ActiveDefinition Enemy examine
What about noted items? IsNotedId → one canonical code-side LOCTEXT; never @noted@item keys Noted items
What about store entries? Keep the entry; resolves as {ItemId}@item via a transient payload — no store namespace Store entries
How do rows stay in sync with sources? UCradlSeedExamineRowsCommandlet (new), add-missing-only, no python stage Offline seeding
How are rows validated? UCradlExamineTableValidator (new): grammar, worklist, cross-resolution Validation
What replicates? Nothing new — LocalOnly ability over static asset data Replication audit

Description store

Rule: Descriptions live in one new DataTable whose row struct is FExamineRow (new): FName ExamineKey (mirrors the row name, same convention as FItemRow::ItemId) and FText Description. No existing type grows a description field — not FItemRow, not the definition data assets, not actor base classes.

Why: Nothing in the project has a flavor-text home today (FItemRow in Source/CRADL/Inventory/ItemRow.h carries only DisplayName), and examinables span DataTable rows, UPrimaryDataAsset definitions, gameplay tags, and bare actor classes. Extending each base type means N parallel struct edits, N validator updates, and a new edit for every future examinable category. One parallel table keyed by a composite id covers all categories with a single struct, a single registry, and a single validator — and flavor text is exactly the kind of cross-category presentation data that doesn't belong on gameplay types.

Implementation surface: - Files: Source/CRADL/Interaction/ExamineRow.h (new) - Structs: FExamineRow : FTableRowBase (new)ExamineKey, Description - Assets: DT_Examine (new), referenced by soft pointer from UCradlExamineSettings (new) (see Description registry)

Footguns: - Don't reach for base-type extension (option 1 in the original design discussion) — rejected; see Why. - FText in a DataTable is localizable as-is; if rows are ever bulk-imported from CSV, remember tag-free FText columns import as plain strings (no special quoting needed — the (TagName="...") rule in feedback_fgameplaytag_csv_import_format.md applies only to tag columns, and FExamineRow deliberately has none). - Per feedback_presentation_struct_resolve_tags.md, presentation structs must not carry raw FGameplayTag fields — FExamineRow complies by construction (keys are FName; tags only appear inside key strings, already resolved).

Related: Key grammar, Validation


Key grammar

Rule: Row names (and the mirrored ExamineKey field) follow {id}@{source} — exactly one @, non-empty id, and a source suffix drawn from a closed set where each suffix maps to exactly one enumeration source:

Suffix {id} is Enumerated from Example
@item DT_Items row name (base ids only) UCradlInventorySettings::ItemTable row map Coin@item
@node UGatheringNodeDefinition asset name asset registry scan by class DA_IronNode@node
@enemy UEnemyDefinition asset name asset registry scan by class DA_Rat@enemy
@station full Station.* tag name tag-manager children of Station Station.Smithing.Ingot@station
@teleport full Teleport.Network.* tag name tag-manager children of Teleport.Network Teleport.Network.SpiritTree@teleport
@terminal hardcoded per-class literal the literal set {bank, loadout, store} bank@terminal

Why: The @ grammar deliberately rhymes with the noted-item convention (UItemRegistry::MakeNotedId in Source/CRADL/Inventory/ItemRegistry.cpp) but inverts its authorship rule: in DT_Items, @ marks runtime-derived ids and is validator-forbidden in authored rows (UCradlItemTableValidator in Source/CRADLEditor/Validators/CradlItemTableValidator.cpp); in the examine table every authored row must carry one. The reservation is per-row-struct, so there is no conflict. One-suffix-per-source keeps both the seeder and the validator mechanical: each suffix has exactly one enumerator and exactly one cross-resolution check, with no union resolvers — which is why teleport networks get their own @teleport suffix instead of piggybacking on @station.

Implementation surface: - Key composition lives in static helpers on UExamineRegistry (new) (e.g. MakeItemKey, plus suffix name constants), mirroring how UItemRegistry::MakeNotedId/IsNotedId/ResolveBaseId keep the @noted literal in one place. The @ literal and suffix strings never appear at call sites. - "Exactly one @" is safe for @item keys because UCradlItemTableValidator already forbids authored @ in DT_Items row names.

Footguns: - Never construct keys by string concatenation at call sites — always the registry helpers, or the grammar drifts per-caller. - Tag-keyed rows use the full tag name (Station.Smithing.Ingot, not Ingot) — mechanical, collision-proof, derivable by both seeder and validator without a leaf-uniqueness rule. - Definition-keyed rows (@node, @enemy) key on the asset name. Renaming a definition asset orphans its row; the validator surfaces this as a warning (see Validation), it is not silently rebuilt.

Related: Description store, Offline seeding, Validation


Description registry

Rule: A new UExamineRegistry : UGameInstanceSubsystem (new) serves lookups. It mirrors UItemRegistry wholesale: a soft DataTable pointer on a new per-domain settings class UCradlExamineSettings : UDeveloperSettings (new), loaded in Initialize, a row-struct-checked LoadFromTable, a TMap<FName, FText> (or FExamineRow) store, and both an instance FindDescription(FName Key) and a static FindDescription(const UObject* WorldContext, FName Key) convenience. Direct subsystem access, no interface.

Why: Per feedback_reuse_proven_pipeline.md, mirror the proven pipeline end-to-end: UItemRegistry (Source/CRADL/Inventory/ItemRegistry.h) is the established settings-referenced-table registry and is itself accessed directly as a subsystem with static WorldContext conveniences. The CLAUDE.md interface rule targets cross-system touchpoints with plausible second implementers; a read-only flavor-text lookup with one consumer (UExamineAbility) and one data source is the "interface would be pure ceremony" carve-out — the same call UItemRegistry already made.

Implementation surface: - Files: Source/CRADL/Interaction/ExamineRegistry.h / .cpp (new), Source/CRADL/Interaction/CradlExamineSettings.h / .cpp (new) - Classes: UExamineRegistry (new), UCradlExamineSettings (new) (per-domain UDeveloperSettings subclass, same pattern as UCradlInventorySettings in Source/CRADL/Inventory/CradlInventorySettings.h and UCradlEnemySettings in Source/CRADL/Enemy/CradlEnemySettings.h) - Static key helpers + suffix constants (see Key grammar)

Footguns: - LoadSynchronous on the settings soft pointer is acceptable only in Initialize (subsystem startup, matching UItemRegistry::Initialize) — never lazily at examine time; the CLAUDE.md sync-load ban applies to gameplay code paths. - Reject tables whose row struct isn't FExamineRow, exactly as UItemRegistry::LoadFromTable rejects non-FItemRow tables.

Related: Runtime resolution, Validation


Runtime resolution

Rule: UExamineAbility (Source/CRADL/Abilities/ExamineAbility.cpp) stays LocalOnly and GameplayEvent-triggered on Action.Trigger.Examine, and gains a resolution ladder over the FGameplayEventData it already receives:

  1. Item payload — if OptionalObject casts to UExamineItemPayload (new) (see Store entries), run the shared item resolution on its ItemId.
  2. Container slot — else if OptionalObject casts to IItemContainer, decode the slot index from EventMagnitude via the CradlAbilityPayload helpers (Source/CRADL/Abilities/ItemEventMagnitude.h) and run the shared item resolution on GetSlot(Index).ItemId. Empty slot → fallback.
  3. World actor — else if Target casts to IInteractable, ask GetExamineKey() (see Actor identity). NAME_None → fallback.
  4. Fallback — no key, or key with no row / empty Description: post the existing LOCTEXT("ExamineStub", "Nothing of interest"), which becomes the honest miss path instead of the whole feature.

Shared item resolution (steps 1–2 funnel here): noted id (UItemRegistry::IsNotedId) → the canonical noted text (see Noted items); otherwise key = MakeItemKey(ItemId)FindDescription.

Resolved text posts through the existing PostMessage(Message.Source.Interaction, Info, …) channel the stub already uses.

Why: The dispatcher (UInteractionComponent::DispatchContextAction in Source/CRADL/Interaction/InteractionComponent.cpp) already puts almost everything the ladder needs on the wire — the container + slot for all three menu emitters (UInventoryComponent, UEquipmentComponent, UBankContainerComponent GatherSlotActions), and the actor in Target for every world emitter; the one source that carries neither descriptor (the store buy menu) rides step 1's payload (see Store entries). Resolving in the ability keeps FContextAction free of duplicated identity for sources that already carry it (per feedback_push_back_on_duplicate_identity.md) and keeps per-actor knowledge out of the ability via the interface.

Implementation surface: - Files: Source/CRADL/Abilities/ExamineAbility.h / .cpp (modify) - Reads: IItemContainer::GetSlot (Source/CRADL/Inventory/ItemContainerInterface.h), UItemRegistry::IsNotedId, UExamineRegistry::FindDescription (new)

Footguns: - Do not branch on concrete container or actor types inside the ability — the ladder touches only IItemContainer, IInteractable, and the registries. - A key that resolves to a row whose Description is still empty (a seeded stub not yet authored) must hit the fallback, not post an empty toast. - Examine posts the description text only, OSRS-style — no SourceLabel prefix. The message-log entry needs no relational data, consistent with feedback_client_resolves_color_token.md's note that pawn-less sources can't provide it anyway.

Related: Actor identity, Noted items, Replication audit


Actor identity

Rule: IInteractable (Source/CRADL/Interaction/InteractableInterface.h) gains one virtual: virtual FName GetExamineKey() const { return NAME_None; } (new) — "no flavor text" by default, sitting alongside GetActionPayload as the per-actor examine hook. Overrides:

Implementer Returns Notes
AGroundItem (Source/CRADL/World/GroundItem.cpp) MakeItemKey(ResolveBaseId(Payload.ItemId)) ground examine collapses into item examine
AGatheringNode (Source/CRADL/World/GatheringNode.cpp) {Definition asset name}@node null DefinitionNAME_None
ACraftingStation (Source/CRADL/World/CraftingStation.cpp) {StationTag full name}@station invalid tag → NAME_None
ATeleportStation (Source/CRADL/World/TeleportStation.cpp) {NetworkTag full name}@teleport description granularity is per-network: every station on a network shares text — deliberate, it's the only data identity the actor carries
ABankTerminal (Source/CRADL/World/BankTerminal.cpp) bank@terminal literal per-class literal via the registry helper — do not invent definition assets or new tags for terminals
ALoadoutTerminal (Source/CRADL/Loadout/LoadoutTerminal.cpp) loadout@terminal literal
AStoreTerminal (Source/CRADL/Store/StoreTerminal.cpp) store@terminal literal describes the terminal actor, not its catalog
AEnemyCharacter (Source/CRADL/Enemy/EnemyCharacter.cpp) {ActiveDefinition asset name}@enemy see Enemy examine

Why: Every world emitter already puts the actor in Target; a defaulted interface virtual lets each actor answer with the identity it already owns (definition asset, tag, or class), with zero dispatcher changes and no growth of FContextAction. This is the same per-actor-hook shape as GetActionPayload — and consistent with the CLAUDE.md interface-first rule at a genuine cross-system touchpoint (ability → world actors, many implementers).

Footguns: - Interactables that don't emit an Examine action (AQuestGiverActor, AGatedInteractable, the IInteractable::GatherActions default single-action implementation in Source/CRADL/Interaction/InteractableInterface.cpp) inherit the NAME_None default and need no override. Adding examine to them later is one GatherActions entry + one override. - Descriptions are strictly per-key (per-definition / per-tag / per-class) — actors carry per-instance SourceLabel overrides today, but a per-instance description override (ExamineKeyOverride UPROPERTY or similar) was considered and rejected; don't add one speculatively. - A noted stack on the ground shows the base item's description (the override base-resolves), while the same stack examined in the bag shows the canonical note text — a deliberate asymmetry (per feedback_dont_symmetrize_speculatively.md, record it in a DevComment on the AGroundItem override rather than engineering a symmetric path): the ability's noted branch needs the raw slot id, and the ground override only has one FName to answer with.

Related: Runtime resolution, Enemy examine


Enemy examine

Rule: AEnemyCharacter::GatherActions (today emits only the Combat.Engage entry) additionally appends an Examine FContextAction (ActionTag = Action.Trigger.Examine, non-default), and the class overrides GetExamineKey() to return {ActiveDefinition asset name}@enemyNAME_None while ActiveDefinition is null.

Why: Enemies are the one examinable category with no examine entry at all today; adding it is in scope for v1. Identity is free: ActiveDefinition (UEnemyDefinition*, replicated) is exactly the per-archetype identity a shared description wants — all instances of a definition share flavor text, matching OSRS's per-monster (not per-spawn) examine.

Implementation surface: - Files: Source/CRADL/Enemy/EnemyCharacter.h / .cpp (modify) - Reads: ActiveDefinition (UEnemyDefinition in Source/CRADL/Enemy/EnemyDefinition.h)

Footguns: - The examine entry must never be bDefault — left-click on an enemy stays Engage. - ATargetDummy (Source/CRADL/Combat/TargetDummy.cpp) also emits only Engage and stays that way — decided: no examine on the dummy; leave it as-is.

Related: Actor identity, Replication audit


Noted items

Rule: When an item-flavored path (item payload or container slot) resolves a noted id (UItemRegistry::IsNotedId), the ability posts one canonical code-side LOCTEXT (OSRS-style: a note swappable at any bank for the item itself) and performs no table lookup. Noted keys never exist: no @noted@item rows, no per-item noted rows, and the seeder never emits them (DT_Items holds only base rows; noted twins are runtime-synthesized in UItemRegistry::RegisterRow).

Why: Every note reads the same in OSRS; per-item noted rows would double the table for identical text. Skipping ResolveBaseId composition on the noted branch also guarantees the malformed Coin@noted@item key is unrepresentable rather than merely invalid.

Footguns: - The check is IsNotedId before key composition — composing first and string-inspecting the key after is the bug this rule exists to prevent. - Ground-item noted stacks intentionally bypass this branch (see Actor identity footguns).

Related: Runtime resolution, Key grammar


Store entries

Rule: The store buy menu (UStoreWidget / UVendorEntryWidget) keeps its Examine entry, and it resolves as an item — the entry's ItemId runs through the same shared item resolution as bag, bank, equipment, and ground examine. There is no @store suffix, no vendor-specific rows, and no store namespace: a vendor entry is always a proxy for a DT_Items row. Because the store's examine action carries neither SourceActor nor SourceContainer (and UStoreWidget::ShowBuyMenu drops the ItemId at gather time today), identity rides a transient carrier: FContextAction gains an optional payload object field (new) that UInteractionComponent::DispatchContextAction forwards as OptionalObject when no source descriptor is set, and the store fills it with a UExamineItemPayload (new) (a minimal transient UObject carrying FName ItemId).

Why: Store items are proxies to DT_Items rows, so store examine must produce byte-identical text to examining the same item anywhere else — a parallel namespace would fork the copy. The carrier shape is this widget's own established pattern: DispatchTransact already sends a transient UTransactRequest through OptionalObject. The new FContextAction field is the widget-sourced counterpart of IInteractable::GetActionPayload (world actors answer for their payload; descriptor-less menu actions carry theirs on the action), not a duplicate identity field — the store has no other channel.

Implementation surface: - Files: Source/CRADL/Interaction/ContextAction.h (modify — optional TObjectPtr<UObject> payload field, UPROPERTY()-decorated per CLAUDE.md), Source/CRADL/Interaction/InteractionComponent.cpp (modify — forward the field in the no-descriptor dispatch branch), Source/CRADL/UI/StoreWidget.cpp (modify — fill the payload in ShowBuyMenu) - Classes: UExamineItemPayload (new), declared alongside UExamineRegistry in Source/CRADL/Interaction/ExamineRegistry.h

Footguns: - A transient UObject in OptionalObject is safe here only because Examine is LocalOnly — the no-stable-NetGUID caveat documented in Source/CRADL/Abilities/ItemEventMagnitude.h applies to abilities whose event data crosses the wire. Don't copy this carrier into a replicated verb. - Buy-as-note entries ride the noted id itself (per project_noted_items_system.md), so the payload's ItemId may be noted — the shared item resolution's IsNotedId branch handles it; do not pre-resolve in the widget. - The comment in Source/CRADL/UI/StoreWidget.cpp anticipating "when item-aware examine ships" is fulfilled by this wiring — per feedback_no_clean_up_later.md, updating it lands in the same change. - The dispatcher's existing world and container branches are untouched — the payload field participates only in the branch that today dispatches an empty FGameplayEventData.

Related: Runtime resolution, Noted items


Offline seeding

Rule: A new editor commandlet UCradlSeedExamineRowsCommandlet (new) keeps the table's key set stable against all six sources. It mirrors UCradlSeedItemRowsCommandlet (Source/CRADLEditor/Seed/CradlSeedItemRowsCommandlet.cpp) wholesale: add-missing-only (existing rows never modified or removed), key-only stubs with empty Description (deliberately validator-invalid as the authoring worklist), -DryRun support, and MarkPackageDirty + UPackage::SavePackage persistence. Unlike the item seeder it needs no python resolve stage — every source enumerates natively in-editor:

  • @item — row map of UCradlInventorySettings::ItemTable, skipping ids in UCradlInventorySettings::RetiredItems (UCradlRetiredItemSet) and (defensively) any row name containing @
  • @node / @enemy — asset-registry scan by class (UGatheringNodeDefinition, UEnemyDefinition)
  • @station / @teleport — tag-manager descendants of Station and Teleport.Network
  • @terminal — the hardcoded literal set {bank, loadout, store}

Why: The seeded-stub worklist is the proven mechanism for "every id must eventually have authored data" (per project_seed_item_rows_pump.md); the examine table has the same shape at larger fan-in. The item seeder's python stage exists because its sources are CSVs outside the editor's reach — all six examine sources are engine-visible, so the extra stage would be ceremony.

Implementation surface: - Files: Source/CRADLEditor/Seed/CradlSeedExamineRowsCommandlet.h / .cpp (new) - Invocation: -run=CradlSeedExamineRows [-DryRun]; new VS Code tasks mirroring "UE: Seed Item Rows (Dry Run)" / "UE: Seed Item Rows" in .vscode/tasks.json, with no python dependsOn

Footguns: - Commandlets run before the asset registry finishes its async scan — the @node/@enemy enumeration must force a synchronous scan (ScanPathsSynchronous / wait-for-completion) or it will silently seed nothing for those suffixes. - The commandlet lives in CRADLEditor and therefore resolves Station / Teleport.Network via UGameplayTagsManager string requests, never CradlTags:: symbols (per reference_native_tag_no_cross_module_export.md — LNK2001). - Orphan removal is not the seeder's job — deleted sources surface as validator warnings and are removed by hand (see Validation). - Seed all descendants of Station, including modal-only virtual stations (e.g. Station.Alchemy, which no world ACraftingStation places) — a harmless authored row beats a special-case exclusion list.

Related: Key grammar, Validation


Validation

Rule: A new UCradlExamineTableValidator (new) under Source/CRADLEditor/Validators/ validates any DataTable whose row struct is FExamineRow:

  • Grammar (Error): row name contains exactly one @, non-empty id, suffix in the closed set; ExamineKey field mirrors the row name (same mirror check as UCradlItemTableValidator's ItemId rule).
  • Worklist (Error): empty Description fails — this is the seeded-stub authoring queue, same mechanic as item stubs failing on empty DisplayName.
  • Cross-resolution (Warning): the id must resolve in its suffix's single source — @item in the item table's row map, @node/@enemy to a definition asset of the right class by name, @station/@teleport via string tag request, @terminal in the literal set. Unresolved = orphan warning, not error and not deletion: a stale row is a manual-cleanup signal, and deleting a source shouldn't hard-fail an otherwise-valid table.

Why: Per CLAUDE.md, editor-time validators shadow runtime structs and land in lockstep with the struct they validate. The Error/Warning split follows intent: Errors are authoring work the table itself owes (grammar, copy); Warnings are drift caused elsewhere (a renamed asset, a retired tag).

Implementation surface: - Files: Source/CRADLEditor/Validators/CradlExamineTableValidator.h / .cpp (new) - Pattern: UEditorValidatorBase subclass, CanValidateAsset on row struct — mirror UCradlItemTableValidator (Source/CRADLEditor/Validators/CradlItemTableValidator.cpp)

Footguns: - Tag resolution via FGameplayTag::RequestGameplayTag(FName, /*ErrorIfNotFound=*/false) string form only — CradlTags:: symbols don't link from CRADLEditor (per reference_native_tag_no_cross_module_export.md). - The existing items validator's @-reservation check needs no change — it is scoped to FItemRow tables and this table uses a different row struct. Do not weaken it. - The noted suffix is a reserved word here: @noted is not in the suffix set, so X@noted rows fail grammar — by design (see Noted items).

Related: Description store, Offline seeding


Replication audit

Rule: This system introduces zero new replicated state, RPCs, or server-mutated fields. Per-item reasoning (per feedback_p2p_replication_audit.md, stated explicitly rather than assumed):

Surface Replication answer
UExamineAbility stays LocalOnly — client-side toast, nothing crosses the wire
UExamineRegistry + DT_Examine static packaged asset data, identical on every peer by construction; loaded per-GameInstance, never mutated at runtime
FExamineRow, UCradlExamineSettings config/asset data, not gameplay state
UExamineItemPayload transient client-local UObject, created and consumed on the examining client within one dispatch; never crosses the wire (LocalOnly ability — no NetGUID requirement)
IInteractable::GetExamineKey() const local read on the examining client; every input it consumes is either local asset data or already-replicated state — AGroundItem's FItemSlot payload and AEnemyCharacter::ActiveDefinition replicate today for existing reasons
Enemy Examine FContextAction client-local menu construction (GatherActions runs on the examining client), cosmetic-only per CLAUDE.md
Container-slot path reads locally-replicated container state through IItemContainer::GetSlot; owner-only visibility semantics (COND_OwnerOnly inventories) are unchanged — you can only open the context menu on slots you can already see

Why: Examine is cosmetic by North Star; replicating any of it would violate the "never replicate cosmetic-only state" rule.


Tag Taxonomy

This system introduces no new tags (per feedback_gameplay_tag_decl_minimal.md, nothing to add to either the .ini or the header):

  • Consumes Action.Trigger.Examine — already C++-declared (CradlTags::Action_Trigger_Examine in Source/CRADL/CradlGameplayTags.h).
  • Consumes Message.Source.Interaction — already C++-declared.
  • Reads the Station.* and Teleport.Network.* namespaces from Config/DefaultGameplayTags.ini — .ini-only; the editor module resolves them by string (see Validation footguns).

Forward Code References

Path Change
Source/CRADL/Interaction/ExamineRow.h newFExamineRow
Source/CRADL/Interaction/ExamineRegistry.h / .cpp newUExamineRegistry, key helpers, suffix constants, UExamineItemPayload
Source/CRADL/Interaction/CradlExamineSettings.h / .cpp new — soft DT_Examine reference
Source/CRADL/Interaction/InteractableInterface.h modify — add GetExamineKey() default
Source/CRADL/Interaction/ContextAction.h modify — optional transient payload object field on FContextAction
Source/CRADL/Interaction/InteractionComponent.cpp modify — forward the action payload in the no-descriptor dispatch branch
Source/CRADL/Abilities/ExamineAbility.h / .cpp modify — resolution ladder replaces stub body
Source/CRADL/World/GroundItem.cpp, Source/CRADL/World/GatheringNode.cpp, Source/CRADL/World/CraftingStation.cpp, Source/CRADL/World/TeleportStation.cpp, Source/CRADL/World/BankTerminal.cpp, Source/CRADL/Loadout/LoadoutTerminal.cpp, Source/CRADL/Store/StoreTerminal.cpp modify — GetExamineKey() overrides
Source/CRADL/Enemy/EnemyCharacter.h / .cpp modify — Examine action + override
Source/CRADL/UI/StoreWidget.cpp modify — fill UExamineItemPayload in ShowBuyMenu; update the now-fulfilled "when item-aware examine ships" comment
Source/CRADLEditor/Seed/CradlSeedExamineRowsCommandlet.h / .cpp new — seeder
Source/CRADLEditor/Validators/CradlExamineTableValidator.h / .cpp new — validator
.vscode/tasks.json modify — seed tasks (no python dependsOn)
Content (DT_Examine) new — authored DataTable asset

Open Questions

None. Everything raised during design review was resolved and folded into the sections above:

  • Store entries: keep the Examine entry, resolve as items via the transient payload (see Store entries).
  • ATargetDummy: no examine, left as-is (see Enemy examine footguns).
  • Per-instance description overrides: rejected — descriptions are strictly per-key (see Actor identity footguns).