11 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 @ 0x004BEE90gmExaminationUI::RecvNotice_ExamineObject @ 0x004AB7B0gmExaminationUI::RecvNotice_SelectionChanged @ 0x004AB3D0gmExaminationUI::SetActiveExamineUI @ 0x004AB420gmExaminationUI::UseTime @ 0x004AB530gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0BasicCreatureExamineUI::SetAppraiseInfo @ 0x004B3F70CreatureExamineUI::SetAppraiseInfo @ 0x004B3FF0CharExamineUI::SetAppraiseInfo @ 0x004B45F0ItemExamineUI::SetAppraiseInfo @ 0x004B72B0ItemExamineUI::HandleInscriptionGainingFocus @ 0x004AC0E0ItemExamineUI::HandleInscriptionMousePresses @ 0x004AC230ItemExamineUI::SetInscriptionEditableState @ 0x004AC720ItemExamineUI::SetInscription @ 0x004AE2F0ItemExamineUI::HandleInscriptionLosingFocus @ 0x004AE620gmFloatyExaminationUI::ResizeTo @ 0x004D4470AppraisalProfile::IsHookedItemInscribable @ 0x005B2F90HookAppraisalProfile::IsInscribable @ 0x005B64C0CM_Writing::Event_SetInscription @ 0x006A98B0PublicWeenieDesc::BF_INSCRIBABLEinacclient.h:6434
- Retail LayoutDesc
0x2100006B, root0x100005F2(gmFloatyExaminationUI, authored size 310 x 400 pixels). - ACE:
IdentifyResponseFlagsGameEventIdentifyObjectResponseAppraiseInfo.WriteHookProfile.WriteGameActionSetInscriptionPlayer.HandleActionSetInscription
- ACViewer's
HookAppraisalProfilerepresentation, 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)
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:
- icon;
- value and burden (the EoR layout has no separate value/burden controls, so these enter the main text);
- inscription/signature;
- tinkering/workmanship;
- set and rating information;
- weapon or armor profile;
- defense and armor modifiers;
- magic, special properties, usage and wield requirements;
- caster/boost/healer/capacity/lock/mana/uses/craftsman details;
- 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.
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.
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, basic report content, scrollbar ownership, and combat refresh are in
scope. Retail's exact 3D object/creature preview, exhaustive specialized
ItemExamineUI detail branches, selection-follow preference, and PSR-only
scribe-account/override behavior remain explicit AP-110 work.