fix #234: port retail appraisal floaty and inscriptions

This commit is contained in:
Erik 2026-07-23 12:12:57 +02:00
parent 643cdfe66e
commit f1a7912160
23 changed files with 1310 additions and 106 deletions

View file

@ -74,6 +74,44 @@ Copy this block when adding a new issue:
---
## #234 — Monster assessment held busy and examination used the shared panel
**Status:** DONE — 2026-07-23, corrective connected gate pending
**Severity:** HIGH
**Filed:** 2026-07-23
**Component:** appraisal protocol / retained examination UI / item writing
**Description:** Assessing a monster appeared to hang the UI with the busy
cursor. Examination also replaced Inventory/Skills in the shared primary
panel, while retail opens its own floaty window. Assessed item inscriptions
were display-only.
**Root cause / status:** The local IdentifyResponse enum did not match
ACE/retail: creature profile bit `0x0100` was labeled WeaponProfile. The strict
parser consequently rejected normal creature responses before the appraisal
owner could release its balanced busy reference. The examination LayoutDesc
had also been incorrectly registered as synthetic main-panel id
`0x80000001`; retail's concrete root is `gmFloatyExaminationUI`. Exact response
bits/order are now pinned by literal packet tests, the floaty is registered as
an independent top-level window, and the imported multiline field sends
retail's CP-1252 `SetInscription (0x00BF)` under its ownership/authorship rules.
**Files:** `src/AcDream.Core.Net/Messages/AppraiseInfoParser.cs`;
`src/AcDream.App/UI/RetailUiRuntime.cs`;
`src/AcDream.App/UI/Layout/AppraisalUiController.cs`;
`src/AcDream.App/UI/UiField.cs`;
`src/AcDream.Core.Net/Messages/InventoryActions.cs`.
**Research:**
[`research/2026-07-23-retail-appraisal-ui-pseudocode.md`](research/2026-07-23-retail-appraisal-ui-pseudocode.md).
**Acceptance:** Monster/NPC/player assessment opens and clears busy; the
examination window coexists with the shared primary panel; an owned
inscribable weapon can be edited/cleared and reassessed; foreign/nonowned/
noninscribable cases show retail's exact permission messages.
---
## #235 — Capped/RDP jump presentation aliases the 30 Hz object clock
**Status:** OPEN

View file

@ -205,7 +205,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| ~~AP-107~~ | **RETIRED 2026-07-11 (Wave 3.3 / #197)** — typed `OfferPrimaryClick` returns `NotActive`, `ConsumedSuccess`, or `ConsumedRejected`; every retained item surface plus radar/world offers active target mode before local selection/open/use fallback. Rejections are consumed and cannot drift selection. | `src/AcDream.App/UI/ItemInteractionController.cs`; inventory/paperdoll/toolbar/radar/world call sites | — | — | `UIElement_ItemList::HandleTargetedUseLeftClick @ 0x004E24D0` |
| AP-108 | Paperdoll/AutoWield still omit the remaining missile/held restrictions and corrupt-mask branch of full `AutoWieldIsLegal`, dual-wield/off-hand rules, double-click examine/drag from the doll, body-part selection lighting, and retail's synchronous `" - cannot unwield the %s"` failure suffix (the current send seam reports rejection asynchronously). **AutoWear legality retired from this row 2026-07-23:** inventory activation and paperdoll drops now apply the retail clothing-priority/location blocker lookup and exact `"You must remove your %s to wear that"` system notice. **Primary replacement retired 2026-07-14; Aetheria retired 2026-07-13.** | `src/AcDream.App/UI/Layout/PaperdollController.cs`; `src/AcDream.App/UI/AutoWieldController.cs` | Basic equip slots, Aetheria, live doll, AutoWear conflict reporting, and primary weapon/incompatible shield/mismatched ammo blocker sequencing work in peace and war | Remaining illegal/off-hand cases, asynchronous dequip rejection wording, doll examine/drag, and selection lighting still differ functionally | `CPlayerSystem::AutoWieldIsLegal @ 0x0055ED60`; `CPlayerSystem::AutoWearIsLegal @ 0x0055EF40`; `CPlayerSystem::AutoWield @ 0x00560A60`; `gmPaperDollUI @ 0x004A3590..0x004A5F90` |
| AP-109 | Character Titles page is inert and live displayed-title/luminance state is absent | `src/AcDream.App/UI/Layout/CharacterStatController.cs`; `CharacterSheetProvider.cs` | Attributes/skills core output is user-accepted | Titles cannot be selected/displayed and level-200 luminance fields are missing | `gmCharacterTitleUI @ 0x0049A610`; `gmStatManagementUI::UpdateExperience @ 0x004F0A70` |
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination now has its authored outer layout and basic item/creature/character reports, but still lacks the exact 3D preview and exhaustive specialized detail regions, conditional inscription-write transaction, and selection-follow preference. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, and the core examination request/presentation loop cover the active loops; the remaining examination mechanisms require additional render and item-write owners rather than fabricated content | Specialized assessments omit some retail fields/3D preview and inscriptions are display-only; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog |
| AP-110 | Remaining retained gameplay panels and world HUD are absent: advanced-combat powerbar, residual social/floating chat, quests/map/options/smartbox, vendor/trade/salvage/tinkering, mini-game gameplay, Link Status NAK/retransmission packet-loss averaging, and D.6 nameplates/floaters. Examination now has its independent authored floaty layout, item/creature/character reports, and conditional inscription-write transaction, but still lacks the exact 3D preview, exhaustive specialized detail regions, and selection-follow preference. | `src/AcDream.App/UI/RetailUiRuntime.cs`; `src/AcDream.App/UI/Layout/AppraisalUiController.cs`; `src/AcDream.Core.Net/LinkStatusSnapshot.cs`; D.5/D.6 roadmap | Basic combat, M3 magic/Link/Vitae surfaces, and the core examination request/presentation/inscription loop cover the active loops; the remaining examination mechanisms require the retail preview render owner and additional field projections rather than fabricated content | Specialized assessments omit some retail fields/3D preview; other absent panels remain unavailable; real packet loss is displayed as 0.00% instead of retail's moving average | `gmExaminationUI::SetAppraiseInfo @ 0x004ADAE0`; `ItemExamineUI::SetAppraiseInfo @ 0x004B72B0`; `ItemExamineUI::SetInscription @ 0x004AE2F0`; `CM_Writing::Event_SetInscription @ 0x006A98B0`; `CLinkStatusAverages::GetAveragePacketLoss @ 0x00546610`; LayoutDesc catalog |
| ~~AP-111~~ | **RETIRED 2026-07-11 (M2 held-object parenting)** — equipped hand items are no longer omitted from the render world. CreateObject now preserves Placement/Parent/position timestamp bootstrap; live `0xF749` ParentEvent is parsed with retail sequence freshness; a focused render controller resolves `Setup.HoldingLocations`, applies the child's placement frame, and recomposes the separate child entity after every parent animation tick. Pickup retains the weenie's visual metadata for a later wield. | `src/AcDream.Core.Net/Messages/{CreateObject,ParentEvent}.cs`; `src/AcDream.Core/Meshing/EquippedChildAttachment.cs`; `src/AcDream.App/Rendering/EquippedChildRenderController.cs` | — | — | `ClientCombatSystem::GetDefaultCombatMode @ 0x0056B310`; `SmartBox::HandleParentEvent @ 0x004535D0`; `CPhysicsObj::set_parent @ 0x00515A90`; `CPhysicsObj::UpdateChild @ 0x00512D50` |
| AP-112 | The basic combat bar ports visibility, height selection, desired-power slider, exact 1.0/0.8-second charge, ready-stance gating, request/release, `MaybeStopCompletely`, server-response queueing, and auto-repeat, but still omits `StartAttackRequest`'s `FinishJump` call and exact trained-Recklessness visibility semantics (IA-20 keeps the dark range as the accepted baseline) | `src/AcDream.App/Combat/CombatAttackController.cs`; `src/AcDream.App/UI/Layout/CombatUiController.cs` | The shared player movement owner now performs retail's server-control-gated full stop and movement report before an attack build; the remaining seams require the jump owner and a distinct Recklessness treatment | Starting an attack while charging a jump may not finish that jump exactly when retail does; trained/untrained Recklessness presentation is identical | `ClientCombatSystem::StartAttackRequest @ 0x0056C040`; `CommandInterpreter::MaybeStopCompletely @ 0x006B3B90`; `gmCombatUI::ListenToElementMessage @ 0x004CC430` |
| AP-113 | Invalid lifestone-command arguments display the local text `Usage: /lifestone`; retail definitely emits a local usage/error line but Binary Ninja misidentifies the referenced wide-string address, so its exact wording is not yet recovered | `src/AcDream.UI.Abstractions/Panels/Chat/ChatCommandRouter.cs`; `RetailClientCommandCatalog.cs` | The behavior boundary is exact (handled locally, no chat and no game action); only a low-impact diagnostic sentence differs | `/ls now` can show different wording/color from retail while still refusing the invalid request correctly | `ClientCommunicationSystem::DoLifestone @ 0x0056FC70` |

View file

@ -30,11 +30,15 @@ ports the optional public shared-cooldown metadata, the authoritative
cooldown-enchantment lookup, the exact ten-step bucket formula, and DAT
sprites `0x060067CF..0x060067D8` through the shared retained `UiItemSlot`
presentation; its connected gate passed. The existing magnifying-glass Assess
command was already retail-faithful. Slice 3 is now code-complete: the complete
appraisal response reaches a balanced pending/current request owner and the
imported 310 x 400 examination layout through the canonical shared main-panel
host. Item, creature, and character views, authored scrollbars, first-response
visibility, and visible-combat refresh are pinned; the connected examination
command was already retail-faithful. Slice 3's first connected gate exposed
three deeper defects, now corrected: the response enum mislabeled ACE's
creature bit `0x0100` and dropped monster packets while retaining the busy
cursor; the imported 310 x 400 examination layout was incorrectly hosted as
shared main-panel content; and inscription was display-only. Exact flag
decoding now reaches the balanced request owner, retail's
`gmFloatyExaminationUI` has an independent top-level lifetime, and the
imported multiline field sends the exact `SetInscription (0x00BF)`
transaction under retail ownership/authorship rules. The corrective connected
gate is next.
---

View file

@ -97,11 +97,15 @@ AutoWear blockers, and carried-use delivery is user-confirmed. The requested
adjacent cooldown follow-up now preserves the item's shared group/duration,
projects the authoritative cooldown-enchantment bucket, and draws retail's ten
DAT-authored radial steps across all item-slot surfaces; its connected gate
passed. The pre-existing Assess command has been reconciled against retail and
Slice 3 is code-complete: strict full appraisal parsing, balanced
pending/current request ownership, and LayoutDesc `0x2100006B` item,
creature, and character views now live in the canonical shared main-panel
host. Its connected examination gate is next.
passed. The pre-existing Assess command has been reconciled against retail.
Slice 3's first connected gate found and corrected the real remaining defects:
ACE creature flag `0x0100` was mislabeled as a weapon profile and left the
appraisal busy reference held, retail's examination UI had been mounted as a
shared main-panel page rather than its own floaty window, and inscription was
display-only. Exact response flags, the independent
`gmFloatyExaminationUI` lifetime, and the retail `SetInscription (0x00BF)`
edit transaction are now ported. The corrective connected
examination/inscription gate is next.
Carried:
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus

View file

@ -2,9 +2,11 @@
**Status:** Slice 1 user-accepted 2026-07-23. Slice 2 Use-hand selection,
zero-useability, carried direct-use, AutoWear correction, and the requested
shared item-cooldown follow-up passed their connected gates. The existing
retail Assess command has been reconciled and Slice 3's examination panel is
code-complete; its connected visual gate is next.
shared item-cooldown follow-up passed their connected gates. Slice 3's first
connected gate exposed an incorrect IdentifyResponse flag table, a non-retail
shared-panel mount, and its missing inscription transaction. All three are
corrected; the independent floaty examination window awaits its corrective
connected gate.
**Milestone:** M4 prerequisite/preamble.
**Architecture:** retained gameplay UI over shared selection, object, and
interaction state. `GameWindow` remains a composition/callback shell.
@ -19,7 +21,7 @@ quest/emote/character-creation bodies begin:
2. The status bar's hand and magnifying-glass controls invoke the same Use and
Assess commands as their keyboard paths.
3. Assessing a creature, player, NPC, or object opens its retail information in
the canonical shared main-panel host.
the independent movable/resizable retail floaty examination window.
4. World picking can resolve visible equipped children, such as a character's
wielded weapon, while selection markers remain anchored to the picked child.
5. Vendor use opens the authored vendor surface, publishes its inventory, and
@ -38,7 +40,7 @@ selection state, or optimistic inventory outcomes.
|---|---|---|
| 1 | Spell-bar overflow arrows | `SpellcastingUiController` + generic retained scrollbar/list |
| 2 | Status Use/Assess commands | focused status-bar controller binding to the existing action router |
| 3 | Assessment information panel | new retained panel controller mounted by the canonical main-panel host |
| 3 | Assessment information panel | retained controller in an independent floaty examination window |
| 4 | Equipped-child world picking | pure world-query/picking policy plus presentation anchor |
| 5 | Vendor browse lifecycle | vendor session/controller plus authored panel |
| 6 | Vendor transactions | server-authoritative buy/sell command and reconciliation owner |
@ -114,19 +116,29 @@ retail response owner around it. One shared UI-busy reference covers the
latest pending GUID, stale replies are rejected, the accepted reply becomes
the current appraisal, and combat-time creature/player refreshes occur every
0.75 seconds only while the examination window is visible. Complete
`IdentifyObjectResponse` parsing now includes the three-word HookProfile and
rejects truncated gated payloads rather than continuing from a corrupt cursor.
`IdentifyObjectResponse` parsing now uses ACE/retail's exact flag values and
positional order, includes the three-word HookProfile, and rejects truncated
gated payloads rather than continuing from a corrupt cursor. The first live
monster gate found that `0x0100` had been mislabeled WeaponProfile; the parser
dropped every creature response before response acceptance, leaving the one
busy reference held. A literal ACE `0x0100` creature fixture now protects that
packet-to-controller path.
LayoutDesc `0x2100006B`, root `0x100005F2`, supplies the complete 310 x 400
floaty chrome, title, item/creature/character subviews, close control, and
authored scrollbars. The panel participates in the canonical one-active
main-panel geometry/lifecycle rather than owning a parallel location. Basic
item, creature, and player reports are live; exact 3D preview/specialized
detail regions, conditional inscription editing, and selection-follow remain
the narrowed AP-110 residual. Research:
authored scrollbars. `gmFloatyExaminationUI` is an independent top-level
window, so Inventory/Skills/Spellbook no longer replace it or inherit its
geometry. The imported multiline inscription field now ports
`ItemExamineUI`'s public/hook inscribability, inventory ownership and
same-scribe permission rules, placeholder/signature presentation, exact
failure notices, focus commit behavior, and CP-1252 `SetInscription (0x00BF)`
transaction. Basic item, creature, and player reports are live; exact 3D
preview/specialized detail regions and selection-follow remain the narrowed
AP-110 residual. Research:
[`../research/2026-07-23-retail-appraisal-ui-pseudocode.md`](../research/2026-07-23-retail-appraisal-ui-pseudocode.md).
The focused parser/router/request/controller/fixture tests pass, as do the
Release solution build with 17 pre-existing tracked test warnings, 3,488 App
tests / 3 skips, and 7,885 complete-solution tests / 5 skips.
Release solution build with 17 pre-existing tracked test warnings, 3,496 App
tests / 3 skips, and 7,913 complete-solution tests / 5 skips.
## Slice 1 — spell-bar overflow arrows
@ -224,20 +236,27 @@ Named retail references and executable pseudocode are recorded in
3. `AppraisalUiController` owns response acceptance, item/creature/character
subview selection, report projection, scroll preservation, first-response
visibility, and visible-combat refresh.
4. `RetailUiRuntime` imports the retail layout and registers it with
`RetailPanelUiController`, so Inventory, Skills, Spellbook, effects, and
Examination remain views of the same movable/resizable main panel.
4. `RetailUiRuntime` imports and registers retail's
`gmFloatyExaminationUI` as its own top-level window. It deliberately does
not enter `RetailPanelUiController`; Inventory, Skills, Spellbook, and
effects keep their shared main-panel geometry while Examination coexists.
5. Network workers deliver immutable parsed data through the existing
session router; retained state changes remain on the update thread.
6. `AppraisalUiController` owns inscription presentation and optimistic field
state; `WorldSession` owns the exact `0x00BF` GameAction send. Authoritative
inventory/appraisal data remains the source on the next response.
### Connected gate
- Select an item and click the magnifying glass (or press Assess): the shared
panel should show its name, available properties, descriptions,
inscription/signature, retail chrome, and working scrollbar.
- Select an item and click the magnifying glass (or press Assess): a separate
floaty window should show its name, available properties, descriptions,
inscription/signature, retail chrome, and working scrollbars.
- Assess a monster, NPC, and player: the correct creature/character page
should appear and the busy cursor should clear.
- In combat, leave a creature assessment open long enough to observe a health
refresh; closing it or opening another shared panel must stop refreshes.
- Move/resize Examination, switch to Inventory or Skills, and confirm the
shared rectangle is retained.
refresh; closing the examination window must stop refreshes.
- Keep Examination open while opening/moving Inventory or Skills and confirm
both windows coexist with independent rectangles.
- Assess an owned inscribable weapon. Edit and clear its inscription by
clicking elsewhere, then reassess it. An item authored by another player is
read-only and reports the exact retail permission line when clicked.

View file

@ -17,13 +17,25 @@ retained examination window.
- `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.
@ -63,6 +75,30 @@ 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
@ -178,16 +214,126 @@ same object preserves the user's position.
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.
## Slice boundary
## Inscription presentation and eligibility
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.
```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.