acdream/docs/research/2026-07-23-retail-appraisal-ui-pseudocode.md
Erik 643cdfe66e feat(ui): port retail appraisal panel
Preserve retail's one-pending-appraisal busy lifetime, parse the complete gated response, and mount the authored examination layout in the shared main-panel host. Keep known 3D preview and inscription-write gaps explicit in AP-110.
2026-07-23 11:34:08 +02:00

6.4 KiB

Retail appraisal request and examination UI pseudocode

Date: 2026-07-23 Scope: status-bar Assess request, IdentifyObjectResponse parsing, and the retained examination window.

Sources

  • Named retail:
    • gmToolbarUI::ListenToElementMessage @ 0x004BEE90
    • gmExaminationUI::RecvNotice_ExamineObject @ 0x004AB7B0
    • gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0
    • gmExaminationUI::SetActiveExamineUI @ 0x004AB420
    • gmExaminationUI::UseTime @ 0x004AB530
    • gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0
    • BasicCreatureExamineUI::SetAppraiseInfo @ 0x004B3F70
    • CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0
    • CharExamineUI::SetAppraiseInfo @ 0x004B45F0
    • ItemExamineUI::SetAppraiseInfo @ 0x004B72B0
    • gmFloatyExaminationUI::ResizeTo @ 0x004D4470
  • Retail LayoutDesc 0x2100006B, root 0x100005F2 (gmFloatyExaminationUI, authored size 310 x 400 pixels).
  • ACE:
    • GameEventIdentifyObjectResponse
    • AppraiseInfo.Write
    • HookProfile.Write
  • ACViewer's HookAppraisalProfile representation, used as a second interpretation check for the three-field hook profile.

The named retail client is the behavior oracle. ACE and ACViewer only clarify the server packet and structure interpretation.

Toolbar request

on Examine button:
    if selected object id != 0:
        ClientUISystem.ExamineObject(selected id)
    else:
        enter TARGET_MODE_EXAMINE

This was already present in acdream before this slice and does not need a second command path.

Examination request lifetime

on ExamineObject(guid):
    if guid == 0:
        return

    if awaitingAppraisalId == 0:
        ClientUISystem.IncrementBusyCount()

    awaitingAppraisalId = guid
    send CM_Item.Event_Appraise(guid)
    clear pending profile pointer

Replacing one pending GUID does not add a second busy reference. The response for the current pending GUID releases that one reference.

IdentifyObjectResponse packet order

read guid
read flags
read success

if IntStatsTable:             read packed int table
if Int64StatsTable:           read packed int64 table
if BoolStatsTable:            read packed bool table
if FloatStatsTable:           read packed double table
if StringStatsTable:          read packed string table
if DidStatsTable:             read packed DID table
if SpellBook:                 read spell list
if ArmorProfile:              read armor profile
if CreatureProfile:           read creature profile
if WeaponProfile:             read weapon profile
if HookProfile:               read flags, validLocations, ammoType (three u32)
if ArmorEnchantmentBitfield:  read highlight/color (two u16)
if WeaponEnchantmentBitfield: read highlight/color (two u16)
if ResistEnchantmentBitfield: read highlight/color (two u16)
if ArmorLevels:               read nine u32 armor levels

Every gated field is positional. A truncated gated field rejects the packet; returning a partial profile would let later fields masquerade as earlier ones.

Applying an appraisal

SetAppraiseInfo(guid, profile):
    if guid == 0:
        return
    if guid != awaitingAppraisalId and guid != currentAppraisalId:
        return

    firstResponse = false
    if guid == awaitingAppraisalId:
        awaitingAppraisalId = 0
        ClientUISystem.DecrementBusyCount()
        currentAppraisalId = guid
        firstResponse = true

    if profile has no CreatureProfile:
        subview = item
    else if profile has string property Template (5)
         or int property EquipmentSetId (0x105):
        subview = character/player
    else:
        subview = creature

    object = ClientObjMaintSystem.GetWeenieObject(guid)
    if object is absent:
        return

    titleName = profile string 0x34 if present
                else object.GetObjectName(NAME_APPROPRIATE)
    title = titleName with stack count

    newlySelectedInSubview = guid != subview.currentObjectId
    subview.Init(guid, object)
    subview.SetAppraiseInfo(profile, newlySelectedInSubview)
    SetActiveExamineUI(subview)

    if firstResponse:
        show examination window

Only the first response to an explicit request forces the window visible. Background refreshes update the current object without reopening a closed window.

Subview and refresh behavior

SetActiveExamineUI(selected):
    hide item, creature, character, and spell subviews
    show selected subview only

on time pulse:
    if examination window is visible
       and combat mode is not peace
       and active subview is creature or character
       and currentAppraisalId != 0
       and 0.75 seconds elapsed:
        send another Event_Appraise(currentAppraisalId)

When the examination window's selection-follow option is active, a selection change appraises the new selected GUID; clearing selection hides the window. That optional retail preference remains outside this slice.

Item presentation order

ItemExamineUI::SetAppraiseInfo clears and rebuilds the authored item text in this order:

  1. icon;
  2. value and burden (the EoR layout has no separate value/burden controls, so these enter the main text);
  3. inscription/signature;
  4. tinkering/workmanship;
  5. set and rating information;
  6. weapon or armor profile;
  7. defense and armor modifiers;
  8. magic, special properties, usage and wield requirements;
  9. caster/boost/healer/capacity/lock/mana/uses/craftsman details;
  10. sale restriction, rare status, full magic, and description.

The main text and inscription areas use their authored scrollbars. A newly selected object resets those scroll positions to the top; a refresh of the same object preserves the user's position.

Window behavior

The LayoutDesc root already contains retail's complete floaty window chrome, close control, item/creature/character/spell subviews, and scrollbars. gmFloatyExaminationUI::ResizeTo delegates to ordinary UIElement::ResizeTo and persists width/height; it adds no special resize algorithm.

Slice boundary

This slice ports the request lifetime, packet integrity, subview choice, authored outer layout, basic report content, scrollbar ownership, and combat refresh. Retail's exact 3D object/creature preview, all specialized ItemExamineUI detail branches, selection-follow preference, and SetInscriptionEditableState plus inscription submission remain explicit AP-110 work. Until that permission/write transaction exists, the imported inscription field is display-only rather than accepting edits that cannot be sent.