339 lines
11 KiB
Markdown
339 lines
11 KiB
Markdown
# 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`
|
|
- `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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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.
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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.
|
|
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
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
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.
|