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

609 lines
22 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::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
```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)
```
`gmExaminationUI::gmExaminationUI @ 0x004AB2B0` initializes
`m_examineNewlySelectedItem` to one. Selection-follow is therefore retail's
default examination behavior, not an optional preference:
```text
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`.
```text
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:
```text
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:
```text
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
```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, 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.