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.
This commit is contained in:
Erik 2026-07-23 11:34:08 +02:00
parent 6b1ae4fb76
commit 643cdfe66e
24 changed files with 17132 additions and 40 deletions

View file

@ -0,0 +1,193 @@
# 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
```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
```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.
`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.