acdream/docs/research/2026-07-23-retail-appraisal-ui-pseudocode.md
Erik bc47bc4917 feat(ui): complete retail item appraisal reports
Restore the authored examination geometry and top-origin item list, then port retail's ordered weapon, armor, magic, requirement, capacity, cooldown, use, and description branches into a dedicated formatter with DAT spell prose. Keep the remaining specialized display-name and preview gaps explicit in AP-110.

Co-authored-by: Codex <codex@openai.com>
2026-07-23 17:03:26 +02:00

22 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::Init @ 0x004AB9C0
    • BasicCreatureExamineUI::BasicCreatureExamineUI @ 0x004ACD30
    • BasicCreatureExamineUI::SetLevelValueText @ 0x004B3E70
    • BasicCreatureExamineUI::SetAppraiseInfo @ 0x004B3F70
    • CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0
    • CharExamineUI::SetAppraiseInfo @ 0x004B45F0
    • AttributeInfoRegion::Update @ 0x004F1D90
    • Attribute2ndInfoRegion::Update @ 0x004F1E80
    • AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0
    • ItemExamineUI::SetAppraiseInfo @ 0x004B72B0
    • ItemExamineUI::HandleInscriptionGainingFocus @ 0x004AC0E0
    • ItemExamineUI::HandleInscriptionMousePresses @ 0x004AC230
    • ItemExamineUI::SetInscriptionEditableState @ 0x004AC720
    • ItemExamineUI::SetInscription @ 0x004AE2F0
    • ItemExamineUI::HandleInscriptionLosingFocus @ 0x004AE620
    • gmFloatyExaminationUI::ResizeTo @ 0x004D4470
    • AppraisalProfile::IsHookedItemInscribable @ 0x005B2F90
    • HookAppraisalProfile::IsInscribable @ 0x005B64C0
    • CM_Writing::Event_SetInscription @ 0x006A98B0
    • PublicWeenieDesc::BF_INSCRIBABLE in acclient.h:6434
  • Retail LayoutDesc 0x2100006B, root 0x100005F2 (gmFloatyExaminationUI, authored size 310 x 400 pixels).
  • ACE:
    • IdentifyResponseFlags
    • GameEventIdentifyObjectResponse
    • AppraiseInfo.Write
    • HookProfile.Write
    • GameActionSetInscription
    • Player.HandleActionSetInscription
  • 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

The flag values are part of the wire contract and are not the field-order indices:

IntStatsTable             0x0001
BoolStatsTable            0x0002
FloatStatsTable           0x0004
StringStatsTable          0x0008
SpellBook                 0x0010
WeaponProfile             0x0020
HookProfile               0x0040
ArmorProfile              0x0080
CreatureProfile           0x0100
ArmorEnchantmentBitfield  0x0200
ResistEnchantmentBitfield 0x0400
WeaponEnchantmentBitfield 0x0800
DidStatsTable             0x1000
Int64StatsTable           0x2000
ArmorLevels               0x4000

ACE writes the gated bodies in the order below, independent of the numerical position of each flag.

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)

gmExaminationUI::gmExaminationUI @ 0x004AB2B0 initializes m_examineNewlySelectedItem to one. Selection-follow is therefore retail's default examination behavior, not an optional preference:

on SelectionChanged:
    if examination window is not visible:
        return
    if selected object id != 0:
        ClientUISystem.ExamineObject(selected id)
    else:
        hide examination window

Creature presentation

BasicCreatureExamineUI binds viewport 0x10000148, base-stat list box 0x10000149, miscellaneous-rating list box 0x10000335, level value 0x1000014C, and creature display name 0x1000014E. Its rows are instances of LayoutDesc 0x2100006B template 0x10000166, whose label and value fields are 0x1000012A and 0x1000012B.

on creature appraisal:
    level = int property 25
    level text = decimal level when positive, otherwise "???"

    creatureType = int property 2
    display name = EnumMapper 0x2200000E[creatureType]
    replace underscores in display name with spaces

    clear stat list
    append rows in this exact order:
        Strength      primary attribute 1
        Endurance     primary attribute 2
        Coordination  primary attribute 4
        Quickness     primary attribute 3
        Focus         primary attribute 5
        Self          primary attribute 6
        Health        current secondary 2 / maximum secondary 1, show percent
        Stamina       current secondary 4 / maximum secondary 3
        Mana          current secondary 6 / maximum secondary 5

    clear miscellaneous list
    damageRating          = int property 0x133
    damageResistRating    = int property 0x134
    critRating            = int property 0x139
    critDamageRating      = int property 0x13A
    critResistRating      = int property 0x13B
    critDamageResist      = int property 0x13C
    healingBoostRating    = int property 0x143
    dotResistRating       = int property 0x15E
    lifeResistRating      = int property 0x15F

    if any displayed rating group is nonzero:
        append blank row
        if damageRating or critRating or critDamageRating is positive:
            append "Dmg/CritDmg" = "Rating: damageRating/critDamageRating"
        if damageResistRating or critResistRating or critDamageResist is positive:
            append "Dmg/CritDmg" = "Resist: damageResistRating/critDamageResist"
        if dotResistRating or lifeResistRating is positive:
            append "DoT/Life:" = "Resist: dotResistRating/lifeResistRating"
        append blank row

Successful primary values are decimal integers. Successful Health is current/max (percent %); Stamina and Mana are current/max. On a failed assessment, Health still shows the percentage when both values are known, while every other row is ???. The profile highlight/color bitfields select one of retail's four authored FontInfo entries (normal, beneficial, detrimental, or incomplete); exact FontInfo-list binding remains AP-110. The 2013 client reads HealingBoost but does not append it to this creature rating list. CritRating and CritResistRating determine whether their paired row exists, while the displayed right-hand value is CritDamageRating or CritDamageResistRating exactly as retail formats it.

The authored viewport and list overlap. Retail draws the creature between the row chrome and the row text. The modern retained RTT seam reproduces that ordering with two synchronized copies of the same DAT row template: media-only chrome below the viewport and media-free label/value descendants above it. Both copies share one pixel scroll model. The 292-pixel authored row is inset four pixels within the 300-pixel list surface, giving the text a balanced left/right margin without changing template geometry.

The animated preview is a private CreatureMode, not the live world object:

clear the previous private creature
clone selected CPhysicsObj with makeObject
set clone heading to 191.367905 degrees
query the clone bounding box

width  = bbox.max.x - bbox.min.x
depth  = bbox.max.y - bbox.min.y
height = bbox.max.z - bbox.min.z
viewportHeightOverWidth = viewport.height / viewport.width
fittedVerticalSpan = max(height, width * viewportHeightOverWidth)
center = (bbox.min + bbox.max) / 2
camera.position = (
    center.x,
    center.y - (fittedVerticalSpan * 1.20710683 + depth / 2),
    center.z)
camera.direction = identity (+Y)

viewport light:
    type = DISTANT_LIGHT
    intensity = 2
    direction = (0.3, 1.9, 0.65)

insert clone into the private CreatureMode

ACDream's modern rendering equivalent uses the same isolated render-to-texture owner as the inventory paperdoll. The examination clone has its own stable render identity, fixed heading and retail camera/light, while its mesh-part transforms are refreshed from the live target each frame so its current animation is visible without registering a second gameplay entity. Retail's clone instead advances the motion state copied by CPhysicsObj::MorphToExistingObject; that narrow sequencer-ownership adaptation is recorded under AP-92.

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 individual branches are not a generic property dump. They project the wire profile into retail prose:

ShowWeaponAndArmor(validLocations, profile):
    ammoType = live PublicWeenieDesc ammo type
    if object is a hook:
        ammoType = HookProfile ammo type

    if validLocations contains Shield:
        append "Base Shield Level: <ArmorLevel>"
        append the player-dependent effective Shield level when available

    if validLocations contains a weapon location:
        weapon = profile.WeaponProfile
        append "Skill: <WeaponSkill>"
        if WeaponType exists:
            append the parenthesized weapon subtype

        launcher = validLocations contains MissileWeapon and ammoType != None
        append launcher ? "Damage Bonus: " : "Damage: "
        minimum = (1 - DamageVariance) * Damage
        if Damage - minimum > 0.0002:
            append "<minimum> - <maximum>"
        else:
            append "<maximum>"
        append damage type for non-launchers

        append Elemental Damage Bonus when present
        append launcher Damage Modifier when present

        if validLocations contains MeleeWeapon, MissileWeapon, or TwoHanded:
            append "Speed: <category> (<raw weapon time>)"
            categories:
                raw < 11  -> Very Fast
                raw < 31  -> Fast
                raw < 50  -> Average
                raw < 80  -> Slow
                otherwise -> Very Slow

        if launcher:
            rangeYards = min(
                85,
                2 * MaxVelocity^2 * (1 / 9.8) * 1.094)
            if rangeYards < 10:
                displayedRange = ceil(rangeYards)
            else:
                displayedRange = truncate down to a multiple of 5
            append "Range: <displayedRange> yds."
            if MaxVelocityEstimated:
                append " (based on STRENGTH 100)"

        if WeaponOffense != 1 and this is not launcher damage:
            append "Bonus to Attack Skill: <signed percent>."

    append the retail ammunition sentence:
        ammunition -> "Used as ammunition by bows/crossbows/atlatls."
        launcher   -> "Uses arrows/quarrels/atlatl darts as ammunition."

ShowArmor(profile):
    append "Armor Level: <base AL>"
    for Slashing, Piercing, Bludgeoning, Fire, Cold, Acid, Electric, Nether:
        adjective from DamageResistanceToString:
            0.0             -> None
            (0.0, 0.4)      -> Poor
            [0.4, 0.8)      -> Below Average
            [0.8, 1.2)      -> Average
            [1.2, 1.6)      -> Above Average
            [1.6, 2.0)      -> Excellent
            >= 2.0          -> Unparalleled
        append "<damage>: <adjective>  (<base AL * modifier, rounded>)"

ShowMagic(profile):
    if the spellbook exists and appraisal failed:
        append "Spells: unknown."
        return

    split spell ids by the high enchantment bit
    resolve every id with that bit masked through ClientMagicSystem

    for ordinary spells:
        append Spellcraft, Mana, and Mana Cost when present
        append "Spell Descriptions:"
        append each spell name and its DAT description

    for high-bit enchantments:
        append "Enchantments:"
        append each spell name and its DAT description

ShowSpecialProperties(profile):
    append carry limit and authored cooldown duration when present
    append cleave count
    build one "Properties: " line from:
        creature slayer, Multi-Strike, imbued-effect flags,
        Magic Absorbing, Unenchantable, Attuned/Bonded/death behavior,
        Retained, Crushing Blow, Biting Strike, Armor/Resistance Cleaving,
        Cast on Strike, Ivoryable, and Dyeable
    if an imbued-effect flag exists:
        append "This item cannot be further imbued."
    if AutowieldLeft:
        append the left-hand tether sentence

ShowRemainingItemFields(profile):
    append, in SetAppraiseInfo order:
        usage text
        level, direct heritage restriction, and wield requirements
            (up to four requirement triplets)
        use restrictions
        item level and item XP
        activation requirements
        caster, boost, healing, capacity, lock, and mana-stone data
        remaining key/charge counts
        creator, last tinkerer, and imbuer
        cannot-be-sold and rare notices
        short and long descriptions

Sources:

  • ItemExamineUI::SetAppraiseInfo @ 0x004B72B0
  • ItemExamineUI::Appraisal_ShowWieldRequirements @ 0x004AF9A0
  • ItemExamineUI::Appraisal_ShowSpecialProperties @ 0x004B0140
  • ItemExamineUI::Appraisal_ShowWeaponAndArmorData @ 0x004B10E0
  • ItemExamineUI::Appraisal_ShowArmorModsData @ 0x004B1F60
  • ItemExamineUI::Appraisal_ShowMagicInfo @ 0x004B2E10
  • ItemExamineUI::Appraisal_ShowActivationRequirements @ 0x004B3270
  • AppraisalSystem::WeaponTimeToString @ 0x005B4970
  • AppraisalSystem::DamageResistanceToString @ 0x005B5490
  • AppraisalSystem::InqWorkmanshipAdjective @ 0x005B48E0
  • ClientUISystem::DeltaTimeToString @ 0x00565E10

ACE's AppraiseInfo serializer and property enums were used to pin the typed-table IDs carried by the network profile. ACViewer's portal.dat SpellTable path was used as the second reference check that item spell names and descriptions come from DAT spell metadata rather than a hand-maintained client list. Retail's named functions remain the behavioral oracle.

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. The root's concrete retail type is gmFloatyExaminationUI, not a child of gmPanelUI. It is mounted and persisted as its own top-level retained window. Showing it must not hide, replace, resize, or reposition the shared Inventory/Skills/Spellbook panel. gmFloatyExaminationUI::ResizeTo delegates to ordinary UIElement::ResizeTo and persists width/height; it adds no special resize algorithm.

The authored root is exactly 310 by 400 pixels and has 310-by-400 minimum constraints. Its maximum constraints are 2000 by 2000, so 310 by 400 is the retail opening/default size, not a fixed-size panel. The main item report control is authored with bottom vertical justification, but ItemExamineUI::AddItemInfo appends rows from the start of the cleared text surface. The retained projection must therefore lay out that generated report from the top while leaving the separate authored inscription behavior intact.

Inscription presentation and eligibility

SetInscription(profile):
    reset inscription field to its unfocused state
    clear displayed text, signature, old inscription, scribe name/account

    inscribable = object.PublicWeenieDesc has BF_INSCRIBABLE
    if object is a hook:
        inscribable = profile.HookProfile exists
                      and (HookProfile.Flags & 1) != 0

    if not inscribable:
        hide inscription field and signature
        return

    scribe = profile string property 8, or empty
    oldInscription = profile string property 7, or empty

    if scribe is empty:
        field = "<Inscribe here>"
        signature = ""
    else:
        field = oldInscription
        signature = "--" + scribe
        if local player is a PSR and scribe-account property 0x17 exists:
            signature += " <" + scribeAccount + ">"

    show field and signature
    SetInscriptionEditableState()

SetInscriptionEditableState:
    editable = object exists
               and object has BF_INSCRIBABLE
               and object is owned by the local player
               and (scribe is empty
                    or case-insensitive scribe == local player name)
    PSR may override the ownership/scribe restriction
    field.Editable = editable
    field.Selectable = editable
    inscription background receives mouse presses only when not editable

The ordinary-player port does not fabricate PSR status. It applies the exact ownership/name rule; an eventual PSR player-description capability can add the retail override without changing this transaction.

For hooks, the appraisal HookProfile is the source of truth for presentation: AppraisalProfile::IsHookedItemInscribable returns false when it is absent and otherwise returns bit 0. The live public description remains the source used by SetInscriptionEditableState, matching the two retail functions rather than merging them into one guessed rule.

Noneditable mouse presses report retail's exact reason:

if scribe is nonempty and differs from local player:
    "Only <scribe> can change the inscription"
else if object is absent or not owned:
    "Item must be in your inventory to inscribe."
else if BF_INSCRIBABLE is absent:
    "This item is not inscribable."

Inscription edit transaction

on inscription focus gained:
    if scribe is empty:
        switch to the normal editing state
        clear "<Inscribe here>"
    signature = "--" + local player name

on inscription focus lost:
    if current object id == 0:
        return

    text = current field text
    skipSend = text is empty and original scribe is empty
    if not skipSend and text != oldInscription:
        send CM_Writing.Event_SetInscription(object id, text)

    if text is empty:
        restore unfocused state
        field = "<Inscribe here>"
        signature = ""
        scribe name/account = ""
    else:
        scribe name = local player name
        signature = "--" + local player name

    oldInscription = text

CM_Writing::Event_SetInscription sends a normal UI-counter GameAction:

u32 0xF7B1       GameAction envelope
u32 uiCounter
u32 0x00BF       SetInscription
u32 objectGuid
String16L text   CP-1252, padded to a four-byte boundary

There is no successful server reply in retail captures. The client therefore updates its field/signature optimistically after sending; ACE deliberately implements the same behavior.

Remaining slice boundary

The request lifetime, exact packet flags, independent floaty window, inscription permission/edit/write transaction, subview choice, authored outer layout, ordered creature stats and ratings, animated creature preview, viewport/list/text compositing, selection-follow, scrollbar ownership, and combat refresh are in scope. Retail's item-object preview, exhaustive specialized ItemExamineUI detail branches, exact appraisal FontInfo-list selection, and PSR-only scribe-account/override behavior remain explicit AP-110 work.