690 lines
41 KiB
Markdown
690 lines
41 KiB
Markdown
# World interaction completion — pre-M4 program
|
||
|
||
**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. 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 and live-confirmed. The creature-page follow-up now has retail's
|
||
ordered stat rows and animated private preview. The follow-up item report and
|
||
authored 310 x 400 layout correction are implemented and user-accepted.
|
||
Favorite-spell press-time selection and right-click local SpellPanel
|
||
examination are implemented and user-accepted. The follow-up maps
|
||
component-disabled ACE characters to the modern scarab/prismatic formula,
|
||
resolves formula icons by their DAT icon DIDs, installs those icons as each
|
||
authored template root's own foreground image, and migrates stale examination
|
||
dimensions once to the authored 310 x 400 extent. The final connected
|
||
assessment gate passed on 2026-07-24. Slice 4 equipped-child world picking
|
||
(with the Opus F1 wielded-pickup-legality correction) passed its connected
|
||
visual gate on Coldeve and was user-accepted 2026-07-29. Slices 1–4 are
|
||
complete; resume at Slice 5, vendor browsing.
|
||
**Milestone:** M4 prerequisite/preamble.
|
||
**Architecture:** retained gameplay UI over shared selection, object, and
|
||
interaction state. `GameWindow` remains a composition/callback shell.
|
||
|
||
## Outcome
|
||
|
||
Close the remaining retail interaction surfaces before the larger M4
|
||
quest/emote/character-creation bodies begin:
|
||
|
||
1. Favorite spell bars expose their DAT-authored overflow arrows and scroll
|
||
through every server-persisted favorite.
|
||
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 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
|
||
supports retail selection/browsing.
|
||
6. Vendor buy/sell transactions, quantities, pending-state ownership, and
|
||
authoritative inventory reconciliation complete the loop.
|
||
|
||
The program reuses the existing retained-window host, `SelectionState`,
|
||
`ClientObjectTable`, interaction transaction owner, and server-authoritative
|
||
inventory updates. It does not create parallel panel positions, item tables,
|
||
selection state, or optimistic inventory outcomes.
|
||
|
||
## Ordered slices
|
||
|
||
| Slice | Deliverable | Principal owner |
|
||
|---|---|---|
|
||
| 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 | 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 |
|
||
|
||
Each slice begins with named-retail research, produces pseudocode and
|
||
conformance tests, updates the divergence register if required, and lands as a
|
||
separate bisectable commit. A visual gate follows each UI-bearing slice.
|
||
|
||
Slice 1 now imports both 23-pixel arrow buttons, their rollover/pressed media,
|
||
and HideDisabled from the real combat fixture; places their artwork by the
|
||
authored leading/trailing positions; shares the list's single pixel-scroll
|
||
model; advances one 32-pixel cell per press; and exposes only actively selected
|
||
spells. Passive object/endowment refresh preserves a manual offset. The mixed
|
||
610/800-pixel DAT anchor chain is solved to a fixed 747-pixel combat root,
|
||
producing exactly 18 visible 32-pixel favorite cells (nine numbered plus nine
|
||
unnumbered) without consuming the overflow. The initial 57-test/full-suite gate passed;
|
||
the corrective 104-test focus set, 3,472 App tests / 3 skips, Release solution
|
||
build, and 7,845 complete-solution tests / 5 skips pass. The corrected
|
||
18-cell bar and both overflow directions passed the connected gate.
|
||
|
||
Slice 2 now drives the toolbar hand from canonical `SelectionState` changes and
|
||
live `ClientObjectTable` updates. The pure Core predicate ports
|
||
`gmToolbarUI::HandleSelectionChanged`: combat-use items, armor/clothing/jewelry,
|
||
and `ItemUses` values without `USEABLE_NO` remain active. Clicking still enters
|
||
the existing `ItemInteractionController` command path, so weapons use the
|
||
server-confirmed AutoWield transaction and targeted tools enter the existing
|
||
use-on-target cursor. Empty selection and explicitly unusable spell components
|
||
are ghosted and ignore clicks. Empty-selection ghosting is the user's explicit
|
||
choice over retail's generic `TARGET_MODE_USE` entry and is registered as
|
||
AP-122. The 14-test Core interaction focus, 49-test toolbar focus, Release
|
||
solution build, 3,474 App tests / 3 skips, and 7,848 complete-solution tests /
|
||
5 skips pass.
|
||
|
||
The connected selection-state gate passed. Its follow-up exposed three deeper
|
||
shared-policy/delivery defects, now corrected from named retail plus the
|
||
matching binary. `ItemUses::IsUseable @ 0x004FCCC0` tests only `USEABLE_NO`;
|
||
reset/absent value zero is usable. `ItemHolder::UseObject @ 0x00588A80` sends a
|
||
Use event immediately for both owned and world objects; acdream had
|
||
incorrectly routed the packet through a local approach lookup, where
|
||
Blackmoor's Favor has no spatial entity and was silently cancelled. Every
|
||
ordinary Use now sends once and ACE owns any authoritative MoveTo chain.
|
||
AD-27 remains only for client-side pickup completion. AutoWear applies
|
||
`CPlayerSystem::AutoWearIsLegal @ 0x0055EF40` through the same double-click and
|
||
toolbar-hand path, resolves the overlapping worn object from the
|
||
retail-ordered equipment projection, and emits the exact system line
|
||
`You must remove your <item> to wear that`. Research:
|
||
[`../research/2026-07-23-retail-item-use-and-autowear-pseudocode.md`](../research/2026-07-23-retail-item-use-and-autowear-pseudocode.md).
|
||
The focused 25-test Core and 85-test App sets pass, as do the warning-free
|
||
Release solution build, 3,476 App tests / 3 skips, and 7,857 complete-solution
|
||
tests / 5 skips. The carried-use delivery correction adds an end-to-end App
|
||
pin from Favor activation through wire dispatch and authoritative UseDone busy
|
||
release. The Release build retains the 17 tracked test-project warnings;
|
||
3,477 App tests / 3 skips and 7,858 complete-solution tests / 5 skips pass.
|
||
The user confirmed Blackmoor's Favor now activates correctly.
|
||
|
||
Before Assess, the user requested the adjacent retail item-cooldown
|
||
presentation. `PublicWeenieDesc` now preserves the optional shared cooldown id
|
||
and duration, assessed property updates reach the same object fields, and
|
||
`CEnchantmentRegistry::OnCooldown @ 0x005943C0` plus
|
||
`UIElement_UIItem::UpdateCooldownDisplay @ 0x004E1E20` are ported through the
|
||
canonical Core registry and one pure display projection. Every retained item
|
||
list shares one controller and the exact
|
||
ten DAT-authored radial sprites `0x060067CF..0x060067D8`; items with the same
|
||
group display the same server-authored cooldown. Research:
|
||
[`../research/2026-07-23-retail-item-cooldown-pseudocode.md`](../research/2026-07-23-retail-item-cooldown-pseudocode.md).
|
||
The focused parser/Core/UI/production-DAT tests, warning-free Release solution
|
||
build, 3,482 App tests / 3 skips, and 7,875 complete-solution tests / 5 skips
|
||
pass. AP-123 records only the retained toolkit's procedural-child adaptation;
|
||
the visible assets, ordering, timing, and shared-group behavior are exact.
|
||
The user accepted the live cooldown presentation 2026-07-23.
|
||
|
||
Slice 3 preserves the existing toolbar/keyboard Assess command and ports the
|
||
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 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. `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 and player reports are live. The creature page now
|
||
resolves its type through retail EnumMapper `0x2200000E`, preserves the
|
||
authored Character/Level header, creates the exact nine stat rows from
|
||
template `0x10000166`, and renders a fixed-heading animated clone through a
|
||
private viewport using retail's bounding-box camera and distant light.
|
||
The visual-gate correction also ports the separate `0x10000335`
|
||
damage/critical/resistance rating list, places authored row chrome behind the
|
||
animated preview and text in front, adds the balanced row inset, and follows
|
||
the current selection automatically while the examination window is visible.
|
||
Item-object preview, specialized item/character detail regions, and exact
|
||
creature appraisal font-state selection remain the narrowed AP-110 residual.
|
||
The item report now retains `PublicWeenieDesc` hook identity, applies
|
||
appraisal-only Value/Burden unknowns, suppresses mounted-hook sentinel
|
||
capacities, preserves retail line/paragraph boundaries, and selects the
|
||
authored white/green/red item font-color entries. 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,496 App
|
||
tests / 3 skips, and 7,913 complete-solution tests / 5 skips.
|
||
|
||
The creature presentation follow-up adds real-DAT row-template and EnumMapper
|
||
fixtures plus pure conformance coverage for row order/formatting, enchantment
|
||
semantics, failed assessments, stable clone identity, live animated mesh
|
||
updates, hydrated mesh bounds, and retail camera fitting. The warning-free App code,
|
||
Release solution build, 3,506 App tests / 3 skips, and 7,923 complete-solution
|
||
tests / 5 skips pass. The connected visual result remains the closeout gate.
|
||
|
||
The rating/layering/selection-follow correction adds 18 focused green tests,
|
||
passes the Release solution build with the 17 warnings already tracked by
|
||
#228, 3,510 App tests / 3 skips, and 7,927 complete-solution tests / 5 skips.
|
||
Its corrected connected visual result remains the closeout gate.
|
||
|
||
The item-report/layout correction restores the authored 310 x 400 examination
|
||
size for the connected profile, leaves retail's ordinary resize range intact,
|
||
keeps row chrome at its LayoutDesc origin while insetting only foreground
|
||
creature text, and starts generated item prose at the top of its authored
|
||
scroll surface. `ItemAppraisalTextFormatter` now owns the decomp-ordered item
|
||
projection outside `GameWindow` and outside the examination controller. It
|
||
ports common weapon damage ranges/speed/range/ammunition, armor protection
|
||
bands, defense/caster modifiers, workmanship, ratings, wield/use/activation
|
||
requirements, item XP/capacity/lock/mana/uses/creator/rare data, cooldown and
|
||
imbued special properties, and both short spell lists and full DAT spell
|
||
descriptions. The conformance follow-up replaces public-value fallbacks with
|
||
retail's appraisal presence semantics, restores hook/capacity/lock behavior,
|
||
and carries each `AddItemInfo` fragment's paragraph and font-color index into
|
||
the retained text shaper. AP-110 now names only the remaining specialized,
|
||
player-dependent, DAT-display-name, creature-font-state, and object-preview gaps.
|
||
Focused conformance fixtures cover the geometry layering plus melee, launcher,
|
||
armor, spell, and special-property reports. The Release solution build passes
|
||
with the 17 warnings already tracked by #228, 3,514 App tests / 3 skips, and
|
||
7,931 complete-solution tests / 5 skips. The connected visual result remains
|
||
the historical pre-acceptance checkpoint; the final gate passed 2026-07-24.
|
||
|
||
The item-format conformance correction adds literal hook-tail cursor fixtures,
|
||
wire/session/object-table propagation tests, the exact Black Phyntos Hive
|
||
report, capacity/page/lock presence cases, structured paragraph checks, and
|
||
LayoutDesc color-palette/enchantment-style coverage. The isolated Release
|
||
solution build passes with the same 17 tracked warnings; 3,522 App tests / 3
|
||
skips and 7,942 complete-solution tests / 5 skips pass while the previously
|
||
launched client retains the normal Release output lock. The corrected
|
||
connected item visual was accepted 2026-07-24.
|
||
|
||
The exhaustive item-report correction replaces the remaining generic/numeric
|
||
approximations with the complete named-retail dispatch. Equipment sets use the
|
||
literal EoR table; ratings, tinkering/salvage averages, coverage, failed weapon
|
||
unknowns, level restrictions, all three item-XP curves, activation heritage,
|
||
healer/ordinary boost behavior, rare timers, and magic `~ Name: Description`
|
||
rows now preserve retail wording and ordering. Description construction ports
|
||
lifespan prose, workmanship/material/gem decoration and portal/PK restriction
|
||
bits. `RetailAppraisalNameResolver` follows retail's master
|
||
`EnumIDMap -> sub-enum 1 DualEnumIDMap` material chain and shares the installed
|
||
creature mapper for slayers and wield requirements; a production-DAT test pins
|
||
Ruby and Ghost. AP-110 is narrowed to item preview and the projections that
|
||
need live player/localization state (effective shield, cooldown remaining, and
|
||
augmentation-cost `StringInfo`), plus character and creature-font residuals.
|
||
The focused item formatter suite passes 21/21, App Release passes 3,531 tests /
|
||
3 skips, and the complete Release solution passes 7,952 tests / 5 skips. The
|
||
connected item-report comparison passed on 2026-07-24.
|
||
|
||
The material-title and section-boundary correction carries
|
||
`PublicWeenieDesc.MaterialType` from CreateObject through the canonical object
|
||
table and resolves `ACCWeenieObject::GetObjectName(NAME_APPROPRIATE)` through
|
||
the installed DAT material map. Examination titles now produce names such as
|
||
`Reed Shark Hide Steel Toed Boots` without duplicating a material already
|
||
present in the authored base name. Empty `AddItemInfo` calls are retained as
|
||
real report fragments, restoring retail's intentional blank rows after
|
||
workmanship, before armor level, around rating/special-property blocks, and at
|
||
the later use/item-level boundaries. Focused wire, projection, object-table,
|
||
title, and boots-layout fixtures protect the full path. With the subsequent
|
||
right-click and press-time retained-item input ports, the Release solution
|
||
build and 3,548 App tests / 3 skips plus 7,979 complete-solution tests /
|
||
5 skips pass.
|
||
|
||
The world right-click follow-up ports
|
||
`UIElement_SmartBoxWrapper::MouseUp @ 0x004E5820` and the
|
||
`sr_Examine` branch of `RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`.
|
||
The configurable `SelectRight` binding now completes on release, cancels when
|
||
pointer travel crosses the retail-observed three-pixel drag threshold, and
|
||
routes through the existing world picker, lighting pulse, canonical
|
||
`SelectionState`, and appraisal request owner. Empty world space remains a
|
||
no-op, right-drag camera orbit does not appraise its release point, and the
|
||
independent configurable `SelectionExamine` action now reaches the same
|
||
request/target-mode path.
|
||
The retained follow-up ports the separate
|
||
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` branch: an occupied
|
||
backpack, side-bag, loot, paperdoll-slot, or physical toolbar cell now selects
|
||
its item and enters that same appraisal owner on a completed right-click.
|
||
Right-button movement cancels the click and can never begin an item drag.
|
||
The input-latency follow-up ports `UIElement_ListBox::MouseDown @ 0x0046E3A0`
|
||
and the physical left-click branch of
|
||
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50`. Canonical selection
|
||
and the retained green frame update during left-button down, before the
|
||
three-pixel drag threshold. Target mode is offered first. Opening, using,
|
||
equipping, looting, and shortcut activation remain completed-click or
|
||
double-click actions and are suppressed when target mode consumed the press.
|
||
Favorite spells now use the parallel non-weenie path:
|
||
`gmSpellcastingUI::ListenToElementMessage @ 0x004C7AB0` selects the favorite
|
||
on left press, while the spell branch of
|
||
`UIElement_ItemList::ListenToElementMessage @ 0x004E4D50` opens the authored
|
||
SpellPanel locally on right-click. Spell IDs never enter `SelectionState` or
|
||
the status-bar magnifier path, and no Appraise/busy transaction is invented.
|
||
The view projects exact spell fields and the current appropriate formula
|
||
through authored component template `0x1000032E`. Its corrective pass uses
|
||
each component descriptor's icon DID rather than its inventory WCID, applies
|
||
the modern scarab/prismatic formula when ACE disables component enforcement
|
||
(IA-21), and introduces per-window authored-geometry revisions so an obsolete
|
||
saved examination height resets once without discarding position or future
|
||
user resizing. The corrected formula/icons/foreground stack and authored
|
||
extent were accepted on 2026-07-24. The 61-test focused App
|
||
gate passed before this correction. The corrected focused tests, Release build
|
||
with #228's 17 tracked test warnings, 3,555 App tests / 3 skips, and 7,986
|
||
complete-solution tests / 5 skips pass.
|
||
|
||
## Slice 1 — spell-bar overflow arrows
|
||
|
||
### Retail oracle
|
||
|
||
The authored Magic combat layout contains, inside every favorite-tab group:
|
||
|
||
- horizontal scrollbar `0x100000B5`, 685×36 at the 800-pixel design width;
|
||
- decrement button `0x10000071`, 23×36;
|
||
- increment button `0x10000072`, 23×36;
|
||
- item list `0x100000B6`, inset by 23 pixels on both sides and 32 pixels high.
|
||
|
||
The list references the scrollbar through `UIElement_Scrollable` property
|
||
`0x71`. The scrollbar references increment/decrement buttons through
|
||
properties `0x77`/`0x78`, and property `0x79` enables HideDisabled. There is no
|
||
track or thumb in this specific control: the two authored arrows are the whole
|
||
visible scrollbar.
|
||
|
||
Named retail references and executable pseudocode are recorded in
|
||
[`../research/2026-07-23-retail-spellbar-overflow-pseudocode.md`](../research/2026-07-23-retail-spellbar-overflow-pseudocode.md).
|
||
|
||
### Implementation plan
|
||
|
||
1. Generalize `DatWidgetFactory.BuildScrollbar` so horizontal and vertical
|
||
scrollbars both import the referenced decrement/increment children,
|
||
including their authored positions, dimensions, Normal/rollover/pressed
|
||
media, and HideDisabled property.
|
||
2. Generalize `UiScrollbar` to use distinct authored decrement/increment
|
||
extents for rendering, hit-testing, track geometry, and dragging. Preserve
|
||
the existing 16-pixel default for layouts whose button children are absent.
|
||
3. Reproduce retail disabled presentation: a model without overflow rejects
|
||
pointer input; when HideDisabled is authored, the scrollbar also draws
|
||
nothing and does not claim hit tests.
|
||
4. In `SpellcastingUiController`, bind each group's scrollbar to its
|
||
`UiItemList.Scroll`, enable horizontal scrolling, and retain one independent
|
||
pixel offset per favorite tab.
|
||
5. Match `SpellCastSubMenu::SetSelected`: selecting a spell through keyboard,
|
||
shortcut, or code scrolls that item into view; passive state refresh does
|
||
not re-expose it.
|
||
6. Carry the combat root's mixed-parent raw-edge policies through the complete
|
||
imported tree and solve for the retail 18-cell favorite viewport; keep that
|
||
HUD capacity fixed across desktop resizes.
|
||
7. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
|
||
controller binding, and selection exposure with focused App tests.
|
||
8. Run the App Release suite, solution Release build, and complete Release
|
||
suite. Then update the roadmap/memory and request the connected visual gate:
|
||
place more favorites than fit, scroll both directions, change tabs, and
|
||
verify arrows disappear on a non-overflowing tab.
|
||
|
||
### Invariants
|
||
|
||
- DAT supplies the controls and their artwork; no new spell-bar texture or
|
||
overlay is invented.
|
||
- The scrollbar and list share one `UiScrollable`; there is no second offset.
|
||
- One arrow press moves one 32-pixel favorite cell, matching
|
||
`UIElement_ListBox::InqScrollDelta`.
|
||
- Hidden disabled arrows cannot intercept combat-page dragging or clicks.
|
||
- Existing stack, combat-power, chat, inventory, spellbook, and external
|
||
container scrollbars retain their current behavior.
|
||
- No substantial feature body enters `GameWindow`.
|
||
|
||
## Slice 2 — status Use/Assess commands
|
||
|
||
### Use-hand implementation
|
||
|
||
1. `ItemInteractionPolicy.IsToolbarUseEnabled` is the pure named-retail
|
||
selection predicate.
|
||
2. `ItemInteractionController.IsToolbarUseEnabled` adapts the selected live
|
||
object without triggering a request or consuming the use throttle.
|
||
3. `ToolbarController` subscribes to canonical `SelectionState.Changed` and
|
||
selected-object add/update/remove notices, then sets the imported button's
|
||
normal or ghosted state through `UiButton.Enabled`.
|
||
4. Enabled clicks retain the one existing activation path:
|
||
equipment enters `AutoWieldController`, ordinary use enters the normal Use
|
||
request owner, and targeted items enter `UseItemOnTarget`, which already
|
||
owns the retail target cursor.
|
||
|
||
### Slice 2 closeout
|
||
|
||
- The shared item-cooldown visual gate passed.
|
||
- `gmToolbarUI::ListenToElementMessage @ 0x004BEE90` confirms the existing
|
||
magnifying-glass path was already correct: assess the selected GUID
|
||
immediately, otherwise enter one-shot Examine target mode.
|
||
- Slice 3 owns the response lifetime and retained examination presentation;
|
||
Slice 2 adds no duplicate command path.
|
||
|
||
## Slice 3 — assessment information panel
|
||
|
||
### Implemented ownership
|
||
|
||
1. `AppraiseInfoParser` owns the complete positional response payload,
|
||
including HookProfile and strict truncation failure.
|
||
2. `ItemInteractionController` owns the pending/current GUID pair and the
|
||
balanced shared busy reference.
|
||
3. `AppraisalUiController` owns response acceptance, item/creature/character
|
||
subview selection, report projection, scroll preservation, first-response
|
||
visibility, and visible-combat refresh.
|
||
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): 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.
|
||
- On a monster, verify the animated creature is centered behind the exact
|
||
Strength/Endurance/Coordination/Quickness/Focus/Self/Health/Stamina/Mana
|
||
order, with its creature type and level in the authored header. Assess
|
||
differently sized monsters to verify the retail bounding-box fit.
|
||
- In combat, leave a creature assessment open long enough to observe a health
|
||
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.
|
||
- On the combat spell bar, press and hold a different favorite: its selection
|
||
and name should update before release. Right-click a favorite: the same
|
||
examination floaty should show the SpellPanel with name, school, mana,
|
||
duration, range, description, and component formula. The toolbar magnifier
|
||
must not treat the selected spell as an object.
|
||
- 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.
|
||
|
||
## Slice 4 — equipped-child world picking
|
||
|
||
**Status:** USER-ACCEPTED 2026-07-29 — the two-client visual gate passed on
|
||
Coldeve ("child world picking works"). Owner
|
||
shape per the program table held: pure world-query/picking policy plus
|
||
presentation anchor. No wire, physics, renderer, or
|
||
`EquippedChildRenderController` changes. `LiveEntityRuntime` gained scoped
|
||
`TryGetAttachedProjectedRecord` / `TryGetPickEligibleRecord` predicates;
|
||
`TryGetInteractionEligibleRecord` and the `_visible` set are untouched, so
|
||
radar, auto-target, and `CombatAttackTargetSource` remain wielded-item free
|
||
(regression-asserted). `WorldSelectionQuery` takes the composed child root as
|
||
an injected `Func<uint, Matrix4x4?>` beside the selection-sphere hook, wired in
|
||
`LivePresentationComposition` from `EntityEffectPoseRegistry.TryGetRootPose`.
|
||
The own-wielded `sr_Use` gate (`0x004E5BE9`) ships through the new
|
||
`IWorldSelectionQuery.IsWieldedByPlayer`. Gates: App tests 3,951/3 skips,
|
||
complete Release solution 9,783/5 skips, connected world-lifecycle gate
|
||
`RESULT=PASS`.
|
||
|
||
**Correction 2026-07-29 (Opus review finding F1, HIGH).** Making a remote's
|
||
wielded weapon selectable made the pickup chain reachable end to end for the
|
||
first time, and acdream had never ported
|
||
`ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0`'s arm at
|
||
`0x005872B7` — `!ACCWeenieObject::IsOwnedByPlayer(item) && item->pwd._location
|
||
!= 0` — so `SelectionPickUp` on another character's weapon installed a real
|
||
approach and a wire request the server rejects. That arm now ships, with
|
||
retail's own notice (`0x007e2228`) and retail's placement ahead of every other
|
||
`AttemptToPlaceInContainer @ 0x00588140` stage: one local message, no movement,
|
||
no request. The player's own wielded item is `IsOwnedByPlayer`, so it passes
|
||
the arm and takes retail's `PositionState.WIELDED` route —
|
||
`ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680` records
|
||
`IR_PUT_IN_CONTAINER`, not `IR_PICK_UP` — dispatching its container transfer
|
||
immediately with no approach. `TryGetApproach` now refuses attached children
|
||
outright for the same reason, so no approach can anchor on a wielder's root.
|
||
Picking, selection, examination, and the marker anchor are unchanged. The
|
||
slice therefore introduces no divergence, contrary to what `f6db964f`'s message
|
||
claimed; no register row is owed. Gates: App tests 3,960/3 skips, complete
|
||
Release solution 9,792/5 skips, connected world-lifecycle gate `RESULT=PASS`.
|
||
|
||
### The retail mechanism
|
||
|
||
Retail picking is render-coupled, not a scene-graph ray walk. A click arms a
|
||
per-frame selection cursor (`UIElement_SmartBoxWrapper::MouseDown @
|
||
0x004E5700` sets a `SearchReason` — sr_Select/sr_Examine/sr_Use/sr_TargetedUse,
|
||
acclient.h:6789 — then `SmartBox::find_object @ 0x00451C60` sets
|
||
`Render::set_selection_cursor @ 0x0054B750`). During the frame,
|
||
`Render::update_viewpoint @ 0x0054CDD0` builds `selection_ray` via
|
||
`Render::pick_ray @ 0x0054B610`, and EVERY drawn part accumulates hits in
|
||
`Render::GfxObjUnderSelectionRay @ 0x0054C740`: drawing-sphere test, then
|
||
per-polygon tests when enabled, keeping the closest — with the polygon winner
|
||
outranking a sphere-only winner (`GetMouseSelectionObjectID @ 0x0054C950`,
|
||
read at `SmartBox::DrawNoBlit @ 0x00454C20`).
|
||
|
||
**The child-vs-parent answer:** each hit records `part->physobj->id`
|
||
(`CPhysicsPart::get_physobj_id @ 0x0050D490`), and a part is only a candidate
|
||
when `part->physobj->id != 0` (`CPhysicsPart::Draw @ 0x0050D7A0`). Equipped
|
||
children are first-class `CPhysicsObj`s with their own ids and part arrays
|
||
(`CPhysicsObj::add_child @ 0x0050F870` via `CSetup::GetHoldingLocation @
|
||
0x005213F0`; `CPhysicsObj::UpdateChild @ 0x00512D50` composes
|
||
`Frame::combine(parent_part_frame, hold_frame)` into the child's own
|
||
`m_position` every frame). So a click on a wielded weapon returns THE WEAPON'S
|
||
GUID — there is no parent redirection in the path, and no ethereal or
|
||
wielded-specific gate: the only candidacy rule is "drawn part with a nonzero
|
||
physobj id".
|
||
|
||
Post-pick (`RecvNotice_SmartBoxObjectFound @ 0x004E5AD0`): the id must exist in
|
||
the weenie table; selection is set to the picked id itself
|
||
(`ACCWeenieObject::SetSelectedObject @ 0x0058C2E0`); the click flash
|
||
(`CPhysicsObj::SetLighting @ 0x00511A80`) is non-recursive — it lights that
|
||
object's own part array ONLY, so clicking a weapon flashes the weapon and
|
||
clicking a creature does not flash its weapon; the vivid brackets
|
||
(`VividTargetIndicator @ 0x004F5CE0`) derive from the selected object's own
|
||
selection sphere at its own position, which for a child IS the hand frame.
|
||
`sr_Use` on an object whose `_wielderID == player_id` is suppressed
|
||
(0x004E5BE9) while selection still happens; `sr_Examine` examines the child id
|
||
directly. `PositionState.WIELDED` is distinct from `IN_CONTAINER`
|
||
(acclient.h:6802), so container suppression never hides a wielded selection.
|
||
|
||
### The gap in acdream (the picker is already right)
|
||
|
||
Equipped children are already live entities with their own `ServerGuid`
|
||
(`EquippedChildRenderController.TryRealize`, :448-617) and every draw path
|
||
already publishes their selection parts under that guid
|
||
(`RetailSelectionScene.AddVisiblePart`, which only skips `serverGuid == 0`).
|
||
`RetailWorldPicker.Pick` therefore already returns the weapon as the polygon
|
||
winner. The failure is entirely downstream: `WorldSelectionQuery.PickAt`
|
||
(:137-154) requires `TryGetInteractionEligibleRecord`, whose `_visible` set
|
||
admits `LiveEntityProjectionKind.World` only (`LiveEntityRuntime.cs:1183`
|
||
excludes Pending/Attached/Hidden by design), so the winning hit is discarded
|
||
and the click reports nothing. Retail would have succeeded. Marker anchoring
|
||
has the twin problem: `ResolveVividTargetInfo` (:262-286) gates on the
|
||
World-only `TryGetSpatiallyProjectedRecord`, and `TryGetSelectionSphere`
|
||
(:293-318) anchors at `entity.Position/Rotation`, which for an attached child
|
||
is deliberately the PARENT's root pose (`ApplyParentWorldPose`, :651-658) —
|
||
brackets at the wielder's feet. The child's true composed root
|
||
(`pose.RootLocal * parentWorld`, the exact `Frame::combine` equivalent) is
|
||
already published per frame to `EntityEffectPoseRegistry` (`PublishChildPose`,
|
||
:632-644; `TryGetRootPose` :195) and is what the vfx anchors already use.
|
||
|
||
### Slice plan
|
||
|
||
1. **Pick eligibility for attached projections.** Add a scoped
|
||
`LiveEntityRuntime.TryGetPickEligibleRecord(serverGuid, localEntityId)`
|
||
accepting `World` (today's semantics) OR `Attached` (with the same
|
||
`IsSpatiallyProjected` + `WorldEntity.Id == localEntityId` staleness
|
||
recheck). Consume it in `WorldSelectionQuery.PickAt`, the lighting-pulse
|
||
identity paths, and `TryGetInteractionTarget`. **Do NOT widen
|
||
`TryGetInteractionEligibleRecord`/`_visible`** — it feeds radar,
|
||
auto-target, sticky/MoveTo establishment and `CombatAttackTargetSource`,
|
||
none of which retail lets wielded items enter (retail's radar has no
|
||
wielded blips).
|
||
2. **Marker + sphere anchor.** Branch `ResolveVividTargetInfo` onto the new
|
||
predicate, and for Attached records transform the Setup selection sphere by
|
||
`EntityEffectPoseRegistry.TryGetRootPose(localId)` (injected as a
|
||
`Func<uint, Matrix4x4?>` beside the existing selection-sphere hook) instead
|
||
of the parent-derived `entity.Position/Rotation`.
|
||
3. **Own-wielded Use gate.** In the use-immediately path, skip the Activate
|
||
enqueue when the picked object's `WielderId == playerGuid` (selection and
|
||
flash still occur) — the 0x004E5BE9 parity. If deferred, it owes an AP row.
|
||
4. **Files:** `LiveEntityRuntime.cs` (predicate), `WorldSelectionQuery.cs`,
|
||
`SelectionInteractionController.cs`, plus composition wiring for the
|
||
root-pose hook. Untouched: `RetailWorldPicker`, `RetailSelectionScene`,
|
||
`WbDrawDispatcher`, `EquippedChildRenderController`.
|
||
5. **Conformance tests** (harnesses exist in
|
||
`tests/AcDream.App.Tests/Interaction/WorldSelectionQueryTests.cs` and
|
||
`Rendering/RetailSelectionSceneTests.cs`): child part closest → resolves to
|
||
the CHILD guid; stale/withdrawn child record → null, never the parent;
|
||
marker uses the pose-registry root, not the parent root; marker suppressed
|
||
for the player's own wielded child, shown for a remote's; lighting pulse
|
||
lights the child identity only; double-click Use suppressed on own wielded.
|
||
6. **Visual gate (user, two-client):** click a remote character's wielded
|
||
weapon — selection names the weapon, the flash lights only the weapon, the
|
||
vivid brackets track the weapon through the wielder's animation (hand, not
|
||
feet), right-click opens Slice 3's examination window on the weapon, radar
|
||
shows no weapon blip, and double-clicking your OWN weapon does not fire a
|
||
Use.
|
||
|
||
### Notes
|
||
|
||
- Slice 3 dependency verified: `SelectionState.Select` stores any nonzero guid
|
||
and `RequestAppraisal` has no eligibility gate, so the examination window
|
||
works on a picked child unmodified once the pick resolves.
|
||
- Divergence register: this slice ADDS no row — it removes an undocumented
|
||
deviation (Attached exclusion from pick eligibility versus retail's
|
||
part-id pick).
|
||
- Existing architectural divergence, unchanged by this slice: retail re-arms
|
||
the pick every frame for hover/tooltips (`sr_MouseOver`); acdream picks on
|
||
demand per click against the last published frame, with an identity recheck.
|
||
|
||
## Slice 5 — vendor browse lifecycle (contract authored 2026-08-08)
|
||
|
||
**Research foundation:**
|
||
`docs/research/2026-08-08-slice5-vendor-browse-research.md` (all wire,
|
||
retail-symbol, and seam citations live there — this contract only records
|
||
DECISIONS and ordered work). Browse only; every buy/sell/accept concern is
|
||
Slice 6 (see the research doc's §D fence).
|
||
|
||
### Decisions on the research doc's eight open questions
|
||
|
||
1. `VendorState` lives in `AcDream.Core.Items`, a sibling of
|
||
`ExternalContainerState`.
|
||
2. The shared `PublicWeenieDesc`-body parser IS extracted from
|
||
`CreateObject.TryParse` FIRST, as its own behavior-preserving commit
|
||
(5.0). Existing CreateObject wire tests must pass unchanged; the
|
||
extraction adds no parsing behavior.
|
||
3. `ShopSystem::BuyPrice`/`SellPrice` (0x006B6120/0x006B6180,
|
||
byte-identical to ACE's `GetBuyCost`/`GetSellCost`) are ported NOW as
|
||
pure Core functions with golden-value conformance tests — the browse
|
||
list shows retail-correct prices from day one.
|
||
4. No request-correlation token in Slice 5: the panel always opens on the
|
||
browse/Buy tab. Slice 6 adds the sell-initiated correlation.
|
||
5. `VendorProfile::InqAcceptability` (which player items the vendor would
|
||
accept) is deferred to Slice 6 with the sell UI it gates.
|
||
6. Category/type filter tabs are IN SCOPE for retail parity. The
|
||
implementer's D0 reads `VendorItemsUI::AddTypeFilter` /
|
||
`ListContainsType` (around 0x004C05C0/0x004C0D90) into a pseudocode
|
||
note before any UI work; if that read reveals a mechanism too large
|
||
for this slice, STOP and report (fallback — flat list + register row —
|
||
requires explicit approval, not implementer discretion).
|
||
7. The vendor panel's top-level LayoutDesc id is NOT yet known: the UI
|
||
piece budgets a LayoutImporter discovery pass (the exact process that
|
||
found the examination window's 0x2100006B), cross-checked by the two
|
||
known tab-control ids (0x100000B9 Buy / 0x100000BB Sell) resolving
|
||
under the candidate root.
|
||
8. AP-110 is narrowed in the SAME COMMIT that lands the panel: "vendor"
|
||
leaves the absent-panels list; whatever sub-scope remains absent after
|
||
this slice gets its own precise row.
|
||
|
||
### Ordered work (each lands separately, bisectable)
|
||
|
||
- **5.0** — extract the shared `PublicWeenieDesc`-body parser
|
||
(behavior-preserving; wire tests unchanged; no vendor code).
|
||
- **5.1** — `ApproachVendor` (GameEvent 0x0062) inbound parser:
|
||
`VendorProfile` + the full-desc item list, against the research doc's
|
||
byte-verified field table; Core.Net tests with golden byte fixtures.
|
||
- **5.2** — `VendorState` in Core.Items + the BuyPrice/SellPrice pure
|
||
port + conformance tests.
|
||
- **5.3** — Runtime ownership: `RuntimeInventoryState` owns the vendor
|
||
session per the J4.2 pattern (generation-gated, torn down on
|
||
reset/portal/logout); the 0x0062 route opens it; close is CLIENT-LOCAL
|
||
(nothing sent on the wire) via the retail distance-watcher semantics;
|
||
`ItemInteractionController._activeVendorId` /
|
||
`ItemInteractionPolicy.ActiveVendorId` finally receive the real id.
|
||
- **5.4** — the authored vendor panel: layout-id discovery, LayoutDesc
|
||
import via the Slice-3 examination-window pattern (foreground stacking,
|
||
authored extent), browse list reusing Slice-1's retained list/scrollbar
|
||
+ DAT icon resolution, category tabs per the D0 read, prices via 5.2.
|
||
- **5.5** — register narrowing (decision 8) rides the 5.4 landing commit.
|
||
|
||
### Trap list (binding)
|
||
|
||
Do not touch: the J5.2 strict use gate's semantics (the vendor open rides
|
||
the EXISTING use transaction — no second gate, per the J4.5 invariant);
|
||
`CreateObject.TryParse` behavior (5.0 is extraction only); anything in the
|
||
Slice 6 fence (no buy/sell wire, no currency mutation, no
|
||
InqAcceptability). New event handling follows the newest existing
|
||
GameEvent handler's registration pattern, not a bespoke route.
|
||
|
||
### Gates
|
||
|
||
Per landing: build + full suite green (clean-room before each landing
|
||
commit). Slice gate (user, connected, ~3 min): approach a Holtburg
|
||
vendor, use them, the authored panel opens on the browse tab with
|
||
retail-correct items/icons/prices; category tabs filter; walking out of
|
||
range closes the panel by itself; nothing is purchasable anywhere.
|
||
|
||
## Slice 6 — vendor transactions, buy arc (contract authored 2026-08-08; user-pulled forward)
|
||
|
||
**Research:** `docs/research/2026-08-08-slice6-vendor-transactions-research.md`.
|
||
User direction: "I cant buy anything... Fix that first." Root cause of all
|
||
four reported symptoms: `VendorUiController` never touches the shared
|
||
`SelectionState`/`StackSplitQuantityState` owners every other panel uses.
|
||
|
||
### Decisions
|
||
|
||
1. **Shop items materialize into `ClientObjectTable`** while the session is
|
||
open (retail creates real CWeenieObjects from the vendor list — Slice 5
|
||
research §A.2) and are REMOVED on session close/replace/reset. The
|
||
implementer verifies retail's removal site (gmVendorUI::CloseVendor
|
||
family) and mirrors its lifecycle. This dissolves F7c's blocker:
|
||
`ExamineItemRequested` gets wired in this slice.
|
||
2. **Vendor selection is the GLOBAL selection**: a new vendor change source
|
||
on the canonical `SelectionState`; row-click selects through it; the
|
||
status bar and the existing byte-faithful `StackSplitQuantityState`
|
||
slider follow automatically (the split-size mask helper from 5.4's F2
|
||
feeds the vendor-owned seeding exactly as gmToolbarUI does at
|
||
pc:198635-198790).
|
||
3. **Buy = retail's Buy button**: immediate single-item purchase
|
||
(gmVendorUI::BuySingleItem, pc:201661) of the selected item with the
|
||
slider-chosen quantity for stacks. Outbound `0x005F`: vendorGuid,
|
||
count, per-item (i32 amount, u32 guid), TRAILING u32
|
||
alternateCurrencyId — the real client sends it (CM_Vendor::Event_Buy,
|
||
pc:689288) even though ACE's reader ignores it; we port the real
|
||
client. The request rides the EXISTING J5.2 one-request-at-a-time
|
||
reservation and completes on `UseDone` (0x01C7) — already the wired
|
||
completion signal; no second gate.
|
||
4. **Reconciliation is the existing inbound machinery**: money property
|
||
updates, inventory CreateObject, and the ApproachVendor refresh
|
||
(VendorState.Refreshed) all flow through landed handlers — the slice
|
||
VERIFIES the loop end-to-end rather than adding an owner.
|
||
5. **No double-click-to-buy**: retail has no such mechanism (confirmed
|
||
against the full named table). We match retail. If the user wants it
|
||
as a deliberate modernization it needs their explicit call + an AP row.
|
||
6. **Deferred, still AP-161**: the Add button / Buying-tab staging list
|
||
and everything Sell (0x0060 — researched, next arc).
|
||
|
||
### Ordered work (one implementer, bisectable commits)
|
||
|
||
- **6.1** shop-item materialization + removal lifecycle + examine wiring.
|
||
- **6.2** the selection coupling (source, row-click, split seeding) —
|
||
status bar + slider light up.
|
||
- **6.3** the 0x005F builder (golden-byte tests incl. the trailing dword),
|
||
Buy-button wiring, gate/UseDone completion, and the verified
|
||
reconciliation round-trip. Register: AP-161 narrowed in the landing.
|
||
|
||
### Gate (user, connected)
|
||
|
||
Select a stacked item → it shows in the status bar with the slider; pick
|
||
a quantity; Buy → coins drop by the displayed price, the stack lands in
|
||
the pack, the shop refreshes; a single-item buy works; insufficient funds
|
||
fails cleanly; the session still closes on walk-away/portal with the
|
||
materialized items removed.
|