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:
parent
6b1ae4fb76
commit
643cdfe66e
24 changed files with 17132 additions and 40 deletions
193
docs/research/2026-07-23-retail-appraisal-ui-pseudocode.md
Normal file
193
docs/research/2026-07-23-retail-appraisal-ui-pseudocode.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue