# Retail toolbar, item interaction, targeting, and cursor pseudocode **Date:** 2026-07-10 **Build oracle:** Sept. 2013 EoR `acclient.exe` v11.4186 with matching PDB **Purpose:** Wave 0 oracle for the retained retail UI fidelity plan. This note records behavior only; it does not prescribe App/Core class layout and changes no runtime code. ## Sources and confidence Primary retail sources: - `docs/research/named-retail/acclient_2013_pseudo_c.txt` - `docs/research/named-retail/acclient.h` - `docs/research/named-retail/symbols.json` Client/server cross-checks: - ACE: - `Source/ACE.Server/Network/Structure/Shortcut.cs` - `Source/ACE.Server/Network/GameAction/Actions/GameActionAddShortcut.cs` - `Source/ACE.Server/Network/GameAction/Actions/GameActionUseItem.cs` - `Source/ACE.Server/Network/GameAction/Actions/GameActionUseWithTarget.cs` - `Source/ACE.Server/Network/GameEvent/Events/GameEventPlayerDescription.cs` - `Source/ACE.Server/WorldObjects/Player_Character.cs` - holtburger: - `crates/holtburger-protocol/src/messages/player/shortcuts.rs` - `crates/holtburger-protocol/src/messages/player/events.rs` - `crates/holtburger-protocol/src/messages/object/types.rs` - `crates/holtburger-protocol/src/messages/game_action.rs` The reference repositories are present in the developer checkout rather than this worktree's intentionally sparse `references/` directory. Retail named symbols are the behavioral oracle; ACE and holtburger are wire-shape checks. Confidence labels used below: - **Exact:** control flow and fields are legible in the named pseudo-C/header. - **Exact wire:** retail, ACE, and holtburger agree on byte count and order. - **Semantic name uncertain:** numeric behavior is exact, but the decompiler lost a source enum/property name. - **Unresolved:** the named body is ambiguous enough that implementation must pin it with Ghidra/cdb or a DAT/capture fixture before porting. --- ## 1. Symbol index ### Toolbar and selected-object strip | Symbol | Address | Role | |---|---:|---| | `gmToolbarUI::RecvNotice_UpdateItemMana` | `0x004BD0C0` | Selected-item mana visibility/value | | `gmToolbarUI::RecvNotice_UpdateObjectHealth` | `0x004BD110` | Selected-object health visibility/value | | `gmToolbarUI::OnItemListDragOver` | `0x004BD150` | Shortcut drop accept state | | `gmToolbarUI::IsShortcutEligible` | `0x004BD1D0` | Item eligibility for a shortcut | | `gmToolbarUI::RecvNotice_SplitStack` | `0x004BD2A0` | Focus/select stack entry text | | `gmToolbarUI::RecvNotice_SetPanelVisibility` | `0x004BD300` | Open/closed panel-button state | | `gmToolbarUI::UseShortcut` | `0x004BD350` | Target/use/select shortcut dispatch | | `gmToolbarUI::FlushShortcuts` | `0x004BD420` | Clear all 18 visual lists | | `gmToolbarUI::RemoveShortcut` | `0x004BD450` | Remove by object id | | `gmToolbarUI::RemoveShortcutInSlotNum` | `0x004BD4F0` | Remove by slot and return object id | | `gmToolbarUI::GetFirstEmptyShortcutToTheRightOf` | `0x004BD560` | Cyclic empty-slot search | | `gmToolbarUI::IsShortcutSlotAvailable` | `0x004BD5E0` | Bounds + empty check | | `gmToolbarUI::RecvNotice_SetCombatMode` | `0x004BD610` | Shortcut-number/combat indicator refresh | | `gmToolbarUI::RecvNotice_ItemListBeginDrag` | `0x004BD930` | Remove-on-lift and remember source slot | | `gmToolbarUI::AddShortcut` | `0x004BD9A0` | Visual add plus optional wire/local model update | | `gmToolbarUI::CreateShortcutToItem` | `0x004BDAC0` | Eligibility/ownership/slot orchestration | | `gmToolbarUI::ListenToGlobalMessage` | `0x004BE4E0` | Use/select/create shortcut input actions | | `gmToolbarUI::HandleDropRelease` | `0x004BE7C0` | Inventory/toolbar/inventory-button drop branches | | `gmToolbarUI::RecvNotice_FullMergingItem` | `0x004BE9B0` | Rekey a shortcut after a full stack merge | | `gmToolbarUI::PostInit` | `0x004BEA80` | Bind buttons, meters, stack controls, and slots | | `gmToolbarUI::ListenToElementMessage` | `0x004BEE90` | Stack editing, drop, combat/use/examine buttons | | `gmToolbarUI::HandleSelectionChanged` | `0x004BF380` | Name/health/mana/stack/use-button state | | `gmToolbarUI::UpdateFromPlayerDesc` | `0x004BF810` | Restore object shortcuts from player module | ### Item interaction and target mode | Symbol | Address | Role | |---|---:|---| | `UIElement_ItemList::HandleTargetedUseLeftClick` | `0x004E24D0` | Item/spell target-mode dispatch | | `UIElement_ItemList::ListenToElementMessage` | `0x004E4D50` | Target-first item-list click ordering | | `CPlayerSystem::OnAction` | `0x00561890` | World selection/target acquisition ordering | | `CPlayerSystem::UsingItem` | `0x00562F70` | Execute `DetermineUseResult` and container paths | | `ClientUISystem::SetTargetMode` | `0x00564940` | Input-map and cursor lifecycle | | `ClientUISystem::ExecuteTargetModeForItem` | `0x00564B30` | Use/examine/use-with-target dispatch | | `ItemHolder::TargetCompatibleWithObject` | `0x00587520` | Full compatibility and user-message path | | `ItemHolder::IsTargetCompatibleWithTargetingObject` | `0x00588070` | Cursor-compatible, no-message predicate | | `ItemHolder::DetermineUseResult` | `0x00588460` | Primary-use classification | | `ItemHolder::AttemptPlaceIn3D` | `0x00588600` | Drag/drop placement decision tree | | `ItemHolder::UseObject` | `0x00588A80` | Primary use orchestration | | `ItemHolder::TargetAcquired` | `0x00588EF0` | Complete targeted use and confirmations | ### Cursor and shortcut persistence | Symbol | Address | Role | |---|---:|---| | `UIElementManager::SetCursor` | `0x0045A800` | Set current and optionally default cursor | | `UIElementManager::CheckCursor` | `0x0045ABF0` | Captured/hovered/default cursor precedence | | `UIElement::SetCursor` / `UnSetCursor` | `0x0045FF50` / `0x0045FF80` | Widget cursor changes | | `MediaMachine::Update_Cursor` | `0x00465A80` | Apply state `MediaDescCursor` to a widget | | `ClientUISystem::UpdateCursorState` | `0x00564630` | Global cursor enum selection | | `ShortCutData::ShortCutData(int,uint,uint)` | `0x005D55E0` | Three-field shortcut record | | `ShortCutManager::Pack` / `UnPack` | `0x005D5710` / `0x005D5820` | Count + packed shortcut entries | | `ShortCutManager::AddShortCut` | `0x005D5790` | Validate index and copy all three fields | --- ## 2. Exact shortcut representation and spell-bar separation ### 2.1 Retail in-memory and packed shape `acclient.h` defines: ```text ShortCutData : PackObj { int index_; uint objectID_; uint spellID_; } ShortCutManager : PackObj { ShortCutData* shortCuts_[18]; } PlayerModule : PackObj { ShortCutManager* shortcuts_; PackableList favorite_spells_[8]; ... } ``` The C++ object occupies 16 bytes because it includes the `PackObj` vtable pointer. The serialized entry is **12 bytes**, not 16: signed/unsigned 32-bit index, 32-bit object id, and one 32-bit spell word. `ShortCutManager::pack_size` starts at four bytes for the count and adds each entry's 12-byte pack size. ACE and holtburger interpret the final 32-bit word as a `LayeredSpell` encoded as `u16 spellId, u16 layer`. That interpretation is wire-compatible with retail's single `uint spellID_` field: ```text rawSpellWord = spellId | (layer << 16) ``` Therefore the faithful model must retain the raw 32-bit word losslessly. It may offer `SpellId`/`Layer` accessors, but those are a view over the word, not two additional wire fields. **Current documentation correction:** `PlayerDescriptionParser.ShortcutEntry`'s comment says "16 bytes wire size". Its actual fields and parser consume 12 bytes; 16 is only the retail C++ object size including the vptr. ### 2.2 Manager pseudocode ```text ShortCutManager.AddShortCut(entry): // 0x005D5790 if entry.index < 0 or entry.index >= 18: return false if shortCuts[entry.index] already exists: copy index, objectID, and raw spellID into it return true allocate a ShortCutData if allocation succeeds: copy index, objectID, and raw spellID store pointer (null on allocation failure) return true // retail behavior ShortCutManager.Pack(dst, available): // 0x005D5710 required = pack_size() if available < required: return required reserve/write a u32 count for slots 0..17: if entry exists: pack its three 32-bit fields count++ write count into reserved word return required ShortCutManager.UnPack(src): // 0x005D5820 destroy existing entries count = read u32 repeat count times: read 12-byte ShortCutData AddShortCut(entry) // rejects bad indices return true ``` The pseudo-C labels the 12-byte entry unpack call as `HookAppraisalProfile::UnPack`; this is an analysis alias collision. The field count, constructor, manager copy, ACE, and holtburger all establish the shortcut shape. ### 2.3 The retail toolbar is object-only in this build ```text gmToolbarUI.UpdateFromPlayerDesc(): // 0x004BF810 FlushShortcuts() module = query CPlayerModule if module and module.shortcuts: for slot in 0..17: entry = module.shortcuts.shortCuts[slot] if entry != null and entry.objectID != 0: AddShortcut(entry.objectID, slot, send=false) UpdateAmmoID() ``` `gmToolbarUI::AddShortcut` itself constructs `CShortCutData(slot, objectId, rawSpellWord=0)` when it sends a toolbar mutation. No `gmToolbarUI` path restores or renders `spellID_` when `objectID_ == 0`. The eight `PlayerModule::favorite_spells_` lists are serialized separately under the player-description `SpellLists8` flag. ACE also exposes separate `AddSpellFavorite (0x01E3)` and `RemoveSpellFavorite (0x01E4)` actions. They feed spell-bar UI, not `gmToolbarUI`'s 18 object slots. **Implementation constraint:** preserve the raw shortcut spell word through parse/store/write even though this toolbar ignores it. Do not add spell icons or spell casting to `gmToolbarUI`; implement the eight favorite-spell lists as a separate subsystem. --- ## 3. `gmToolbarUI` input, use, and selection pseudocode ### 3.1 Global input messages `ListenToGlobalMessage(1, action)` ignores shortcut actions while the stack-size entry owns focus. Otherwise: | Action ids | Result | |---|---| | `0x10000042..0x1000004D` | `UseShortcut(action - 0x10000042, use=true)` for slots 0..11 | | `0x1000004E..0x10000059` | `UseShortcut(action - 0x1000004E, use=false)` for slots 0..11 | | `0x10000132..0x10000137` | Use slots 12..17 | | `0x10000138..0x1000013D` | Select slots 12..17 | | `0x1000010D` | Create a shortcut to the selected object, auto-slot | When the stack entry has focus, action `0x27` restores its text from the global split size and relinquishes focus. The input map identifies `0x27` as the commit/ escape-style action in this context; its human key name is not recovered here. ### 3.2 Use/select/target ordering ```text gmToolbarUI.UseShortcut(slot, use): // 0x004BD350 if slot outside the visual slot array: return cell = slot's first UIItem if no UIItem: return if ClientUISystem.targetMode != NONE: if cell.itemID != 0: ExecuteTargetModeForItem(cell.itemID, targetMode) SetTargetMode(NONE) // always clear here return if use: ItemHolder.UseObject(cell.itemID, 0, 0) else: ACCWeenieObject.SetSelectedObject(cell.itemID, 0) ``` Target mode is thus checked before both normal use and selection. Toolbar target dispatch is one-shot because this method explicitly clears the mode after dispatch. ### 3.3 Use and Examine buttons On button click (`ListenToElementMessage`, message `1`): ```text Use button 0x1000019D: if selectedID != 0: ItemHolder.UseObject(selectedID, 0, 0) else: SetTargetMode(USE) Examine button 0x100001A5: if selectedID != 0: ExamineObject(selectedID) else: SetTargetMode(EXAMINE) Combat indicators 0x10000192..0x10000195: ToggleCombatMode() ``` The seven panel buttons are discovered in `PostInit` from element attribute `0x10000029`; `RecvNotice_SetPanelVisibility(panelId, visible)` sets button state `6` while visible and state `1` while hidden. --- ## 4. Toolbar drag/drop pseudocode ### 4.1 Eligibility `IsShortcutEligible(object)` accepts only when all of the following retail tests pass: 1. Object exists. 2. A particular PWD bitfield condition either is absent or the object is a player. 3. Objects whose `InqType()` includes `0x10` are rejected unless `IsPlayer()`. 4. If the object has a container, that container is not the active vendor. The decompiler has replaced the first bit's source enum name with a raw `4`; keep the numeric branch in the pseudocode fixture and recover the PWD property name before introducing a public enum. `CreateShortcutToItem` additionally requires ownership. With its pickup flag set, it first attempts `PlaceInBackpack`; with the flag clear it reports the appropriate not-owned/ineligible/full/duplicate message. For a specified slot it removes any existing occurrence of the same object and adds it at that slot. For auto-slot it passes `-1` to `AddShortcut`; `AddShortcut @ 0x004BD9A0` scans from slot zero and stops at the first empty slot. A duplicate does not get added again. ### 4.2 Shortcut aliases are not physical items Retail preserves the dragged element's source identity in `DropItemFlags`. The shared inventory-list path and paperdoll path both require `(flags & 0xE) == 0` before they show an accept/reject state or perform a physical item move. A shortcut alias therefore remains visually neutral over inventory or paperdoll, and releasing it there performs no item operation. The shortcut was already removed by `RecvNotice_ItemListBeginDrag`, so the net result is only removing the alias from the toolbar. ```text UIElement_ItemList.ItemList_DragOver(source, target): // 0x004E3400 (objectId, _, flags) = InqDropIconInfo(source) if no custom handler and (flags & 0xE) == 0: set the target's physical-item accept/reject state // alias source: leave the target neutral UIElement_ItemList.HandleDropRelease(message): // 0x004E4790 (objectId, _, flags) = InqDropIconInfo(source) if objectId != 0 and (flags & 0xE) == 0: AcceptDragObject(objectId, flags & 1) // alias source: no physical item operation gmPaperDollUI.OnItemListDragOver / HandleDropRelease: // 0x004A4270 / 0x004A4D80 apply the same (flags & 0xE) == 0 gate before wield policy gmToolbarUI.OnItemListDragOver: // 0x004BD150 accept when (flags & 0xE) == 0 or (flags & 4) != 0 ``` In acdream's typed payload, `ItemDragSource.ShortcutBar` is the `flags & 4` alias case. Inventory, equipment, and world placement controllers must never reinterpret that payload as the underlying physical object. ### 4.3 Remove-on-lift ```text RecvNotice_ItemListBeginDrag(sourceList): // 0x004BD930 find sourceList's index in the 18 slots item = sourceList.first UIItem if item.itemID != 0: RemoveShortcut(item.itemID, send=true) m_lastShortcutNumDragged = source index ``` `RemoveShortcut` flushes the visual slot, clears the object's shortcut number, then, when `send=true`, sends `Event_RemoveShortCut(index)` and removes that slot from `PlayerModule`. Releasing a lifted shortcut off the toolbar leaves it removed. ### 4.4 Empty-slot search is cyclic to the right ```text GetFirstEmptyShortcutToTheRightOf(slot): // 0x004BD560 search slot+1 .. 17 if none, search 0 .. slot return first empty, or -1 ``` It is not an inventory grid index, the original source index, or a global first-empty search unless wraparound reaches slot zero. ### 4.5 Drop release branches `InqDropIconInfo` supplies the dragged object id and `DropItemFlags`. The exact source-flag enum names are not present in the named body, but the masks and branch effects are exact: ```text HandleDropRelease(message): // 0x004BE7C0 sourceElement = message.drag source targetElement = message.drop target if either is null: return if target element id == inventory button 0x100001B1: (objectId, flags) = InqDropIconInfo(sourceElement) if (flags & 0xE) == 0: object = lookup objectId if owned: PlaceInBackpack(objectId, 0) else: AttemptToPlaceInContainer(objectId, playerId, ..., quiet=true) if operation reports false: clear object's waiting state return targetSlot = first shortcut slot that is an ancestor of targetElement if no target slot: return (objectId, flags) = InqDropIconInfo(sourceElement) if objectId == 0: return if (flags & 0xE) == 0: // fresh/non-alias source displaced = RemoveShortcutInSlotNum(targetSlot, send=true) CreateShortcutToItem(objectId, targetSlot, pickup=true, quiet=false) if displaced != 0 and displaced != objectId: empty = GetFirstEmptyShortcutToTheRightOf(targetSlot) if empty != -1: AddShortcut(displaced, empty, send=true) else if (flags & 4) != 0: // shortcut-alias source displaced = RemoveShortcutInSlotNum(targetSlot, send=true) AddShortcut(objectId, targetSlot, send=true) if displaced != 0 and displaced != objectId and IsShortcutSlotAvailable(m_lastShortcutNumDragged): AddShortcut(displaced, m_lastShortcutNumDragged, send=true) ``` This distinction is behaviorally important. A fresh inventory item dropped onto an occupied slot sends the displaced item to the first empty slot cyclically to the right of the target. Only a dragged shortcut alias attempts to restore the displaced item to `m_lastShortcutNumDragged`. ### 4.6 Stack-merge attempt rekey ```text RecvNotice_FullMergingItem(oldId, newId): // 0x004BE9B0 slot = RemoveShortcut(oldId, send=true) if slot != -1: CreateShortcutToItem(newId, slot, pickup=false, quiet=true) ``` This body updates the first matching shortcut returned by `RemoveShortcut`. Whether duplicate object shortcuts can exist through external player-description data is unresolved; `CreateShortcutToItem` normally prevents duplicates. The notice name is misleading: `ItemHolder::AttemptMerge @ 0x005878F0` broadcasts `SendNotice_FullMergingItem(oldId, newId)` immediately after `UIAttemptMerge`, for every legal merge request. It is not an ACE confirmation and is not conditional on the source stack being exhausted. `CreateShortcutToItem` also removes any existing `newId` shortcut before adding it at the old source shortcut's slot. ```text IsMergeAttemptLegal(sourceId, targetId, quiet): // 0x00586F30 if player not ready for inventory request: return false if sourceId == targetId: return false source = lookup sourceId; target = lookup targetId if either missing: return false if source.maxStackSize <= 1 or target.maxStackSize <= 1: return false if source.tradeState == 1 or target.tradeState == 1: return false if source.wcid != target.wcid: return false return target.stackSize < target.maxStackSize AttemptMerge(sourceId, targetId, quiet): // 0x005878F0 if not IsMergeAttemptLegal(...): return false requested = sourceId == selectedId ? splitSize : max(1, source.stackSize) available = target.maxStackSize - max(1, target.stackSize) amount = min(requested, available) source.UIAttemptMerge(targetId, amount) // sends 0x0054 SendNotice_FullMergingItem(sourceId, targetId) // immediate local notice Select(targetId) return true ``` acdream now has one Core-owned split-quantity state shared by the selected-object entry/slider and inventory merge. A selected source supplies that quantity to `AttemptMerge`; an unselected source retains retail's full-source default. Legality, capacity clamping, and wire ordering are unchanged. --- ## 5. Selected-object health, mana, and stack behavior ### 5.1 Initial binding `PostInit` binds the selected field and these children: | Element | Retail member | Initial state | |---:|---|---| | `0x1000019F` | selected name | empty | | `0x100001A1` | health meter | hidden | | `0x100001A2` | mana meter | hidden | | `0x100001A3` | stack-size text | hidden, `NumberInputFilter` | | `0x100001A4` | stack slider | hidden | ### 5.2 Selection refresh ```text HandleSelectionChanged(): // 0x004BF380 if cached selected id changed: cache selectedID clear name selected field state = 0 if health meter visible: QueryHealth(0) hide health meter if mana meter visible: QueryItemMana(0) hide mana meter hide stack entry and slider if selected id == 0: Use button state = 1 return object = lookup selected id if object missing: return set selected name: normal object: NAME_APPROPRIATE, or `"%d %hs"` (count then appropriate name) when stacked owned coinstack: formatted coin total/name path splitSize = 1 maxSplitSize = 1 hide stack controls if stackSize > 1: initialSplit = stackSize if object is in active vendor and passes vendor type mask: initialSplit = 1 splitSize = initialSplit maxSplitSize = max(stackSize, 1) entry text = splitSize slider value attribute 0x86 = splitSize / maxSplitSize selected field state = 0x1000000C // stacked show entry and slider else: selected field state = 0x1000000B // object selected if object is player or pet or attackable: QueryHealth(selected id) else if object is owned: QueryItemMana(selected id) construct ItemUses(useability) if no combat use, not relevant item types, and not useable: Use button state = 13 // ghosted else: Use button state = 1 ``` The exact item-type mask in the use-button ghost test is visible as separate `InqType() & 4`, `& 2`, and `& 8` checks. Preserve those numeric tests until the retail `ITEM_TYPE` names are pinned. ### 5.3 Meter notices ```text RecvNotice_UpdateObjectHealth(id, fraction): // 0x004BD110 if id != cached selected id: return if health meter hidden: show it set meter float attribute 0x69 = fraction RecvNotice_UpdateItemMana(id, fraction, valid): // 0x004BD0C0 if id != cached selected id: return if not valid: QueryItemMana(0) return if mana meter hidden: show it set meter float attribute 0x69 = fraction ``` ### 5.4 Stack editing `ACCWeenieObject::GetObjectName(NAME_APPROPRIATE) @ 0x0058E6E0` selects `PublicWeenieDesc._plural_name` whenever `_stackSize > 1`. If the optional plural string is empty, retail copies the singular and appends `s`, or `es` when the singular already ends in lowercase `s`. The CreateObject WeenieHeader carries the explicit plural as flag `0x00000001`, before ItemsCapacity. The stack entry's authored DAT horizontal justification is Right. Its number is therefore right-aligned against the slider's left edge rather than drawn from the entry's left edge. - `RecvNotice_SplitStack(selectedId)` activates and selects all text only when the selected object still has a stack size greater than one. - On entry activation, select all text. - On entry deactivation, parse unsigned text, clamp to `1..maxSplitSize`, rewrite the text if clamped, update `GenItemHolder::splitSize`, update slider attribute `0x86 = splitSize/maxSplitSize`, and broadcast the stack-slider notice. - `UIElement_Scrollbar::SetScrollbarPosition @ 0x00470EC0` clamps the normalized position to `[0,1]`, multiplies by `1000.0f`, truncates through `_ftol2`, and broadcasts that integer as message `0xA` parameter 1. - `gmToolbarUI::ListenToElementMessage @ 0x004BEFE0..0x004BF041` converts the message exactly as follows, stores it, rewrites the entry, and broadcasts the stack-slider notice: ```text positionMillis = truncate(clamp01(sliderRatio) * 1000) splitSize = clamp(1 + floor(positionMillis * maxSplitSize / 1000), 1, maxSplitSize) ``` The apparently odd extra one is verbatim retail behavior. Static cdb disassembly of the matching v11.4186 binary also pins the stack-name format string at `0x007B4738` to `"%d %hs"` and the numeric-entry format at `0x007A0184` to `"%d"`. While the pointer is moving, the scrollbar retains its own normalized position and the toolbar only rewrites the entry text. Selection refresh or entry deactivation explicitly writes attribute `0x86 = splitSize/maxSplitSize`. Consequently a pointer drag can remain at exact position `0` (quantity 1) or `1` (the maximum), rather than being redrawn from the quantized quantity during the drag. --- ## 6. `DetermineUseResult` and `CPlayerSystem::UsingItem` `DetermineUseResult` returns a numeric primary-use classification. The meanings are established by the switch in `CPlayerSystem::UsingItem @ 0x00562F70`: | Result | `UsingItem` behavior | |---:|---| | `0` | No classified primary action | | `1` | Item has a direct/targeted `ItemUses` use path | | `2` | `PlaceInBackpack` | | `3` | `AutoWield(..., SLOT_SIDE_RIGHT, ...)` | | `4` | `AutoSort` | | `5` | Attempt to open secure trade negotiations | | `6` | Open salvage panel | | `7` | Begin game with object | | `8` | `AutoWield(..., SLOT_SIDE_LEFT, ...)` | ### 6.1 Classification pseudocode ```text DetermineUseResult(object): // 0x00588460 if object is in a container or has the relevant PWD flag, or object is in the currently viewed ground container: if not wielded by somebody else: if object has item/container capacity and is not component pack: return 2 // pick up/open carrier path type = object.InqType() if owned by player: if combat-use or type bit 0x8000 or PWD bit 0x20000000: if not currently wielded by player: return 8 when PWD bit 0x40000000 is set, else 3 if valid-location masks contain an unworn usable location: return 4 if type has 0x20000000: return 6 else if type has its high bit set: return 7 if ItemUses(useability).IsUseable(): return 1 if object is another player: return 5 return 0 ``` The first capacity bit expression was corrupted into a string address by Binary Ninja. Matching-executable disassembly at `0x005884B6` resolves it exactly as `test PWD._bitfield, 0x00800000`, i.e. `BF_REQUIRES_PACKSLOT`. The complete result-2 predicate is therefore `(!BF_REQUIRES_PACKSLOT && itemsCapacity == 0 && containersCapacity == 0) || IsComponentPack()` after the loose/viewed-container and wielder gates. This was pinned on 2026-07-11 before the Core policy port. `CPlayerSystem::UsingItem` first executes result `2..8`, then independently: - sends `OpenContainedContainer` for a player-owned container; and - for a non-owned, directly usable, non-targeted container, calls `AttemptSetGroundObject`. These are not mutually exclusive with the classification switch. A pure policy port must model the complete ordered action list, not reduce the result to a single modern enum and accidentally omit the post-switch container steps. --- ## 7. `ItemHolder::UseObject` pseudocode ```text UseObject(objectId, useCurrentSelection, bypassClassification): // 0x00588A80 ui = GetUISystem() if ui is null: return object = lookup objectId if object is null: return if now is not later than m_timeLastUsed + 0.2 seconds: return m_timeLastUsed = now if player is not ready for an inventory request: return if active vendor exists and object.containerID == vendorID: return classified = false if bypassClassification == 0: result = DetermineUseResult(object) if result in 2..7: // exact bound; excludes 8 classified = true uses = ItemUses(object.useability) if classified: PlayerSystem.UsingItem(objectId, true, bypassClassification) return if object.tradeState == 1: display "cannot use while trading" message return if object.location == 0 and least-limited source use requires wielded: display "must wield" message return if uses.IsUseable_Targeted(): if useCurrentSelection == 0: ItemHolder.targetingObject = objectId SetTargetMode(USE_TARGET) display "Choose a target..." return target = ACCWeenieObject.selectedID if target == 0: display "Select your target..." return if TargetCompatibleWithObject(target, objectId, showErrors=false, showUsing=true): Event_UseWithTargetEvent(objectId, target) IncrementBusyCount() PlayerSystem.UsingItem(objectId, true, bypassClassification) return if uses.IsUseable(): if PK altar flag: show PK altar confirmation; return if NPK altar flag: show NPK altar confirmation; return if volatile-rare flag and preference requires confirmation: show volatile-rare confirmation; return Event_UseEvent(objectId) IncrementBusyCount() PlayerSystem.UsingItem(objectId, true, bypassClassification) display "Approaching" for creature/player-like type, else "Using" return PlayerSystem.UsingItem(objectId, false, bypassClassification) if object is not self: if object has the no-open/close restriction bit: display restricted message; return if attackable and combat mode is noncombat: display switch-to-combat message; return if not attackable or combat mode is noncombat: display "cannot be used" message; return ``` Important boundaries: - The 200 ms throttle timestamp advances before the ready/vendor/legality gates. - `UseObject` owns confirmation routing and busy-count increments. - `arg2` is established by call sites as "use the current selected target now" versus "enter target mode". `arg3` bypasses classification in several internal paths; its original source-level name remains unresolved. - The `2..7` bound excludes `DetermineUseResult == 8` even though `CPlayerSystem::UsingItem` has a case for 8. Preserve this asymmetry; do not normalize it without tracing the left-side-wield call path. ### 7.1 Target acquisition completion ```text TargetAcquired(targetId): // 0x00588EF0 sourceId = ItemHolder.targetingObject ItemHolder.targetingObject = 0 if not TargetCompatibleWithObject(targetId, sourceId, showErrors=false, showUsing=true): return source = lookup sourceId target = lookup targetId if source and target: if source is mana stone requiring confirmation: reject no-mana target or show mana-stone confirmation return if source is salvage-like and craft-success-dialog preference requires it: show salvage confirmation return Event_UseWithTargetEvent(sourceId, targetId) IncrementBusyCount() PlayerSystem.UsingItem(sourceId, true, 0) ``` ACE and holtburger confirm the wire actions after this policy: ```text Use (0x0036): u32 objectId UseWithTarget (0x0035): u32 sourceObjectId, u32 targetObjectId ``` --- ## 8. `AttemptPlaceIn3D` pseudocode ```text AttemptPlaceIn3D(itemId, targetId, allowGroundFallback): // 0x00588600 if player not ready for inventory request: return false item = lookup itemId if item missing: return false if targetId == playerId: return PlaceInBackpack(itemId, 0) if item is not owned by player: display "You must first pick up..." return false if item.tradeState != 0: display "You are trading..." return false if targetId == 0: if not allowGroundFallback: return false goto PLACE_ON_GROUND if AttemptMerge(itemId, targetId, quiet=false): return true target = lookup targetId if target missing: if allowGroundFallback: goto PLACE_ON_GROUND return false if target passes the retail vendor-drop PWD bit test: if splitSize == maxSplitSize: AttemptSellToVendor(targetId, itemId) return false // exact retail return display "must split the stack before..." return false if preference DragItemOnPlayerOpensSecureTrade and target.IsPlayer(): AttemptToTradeItem(targetId, itemId) return false // exact retail return if target.InqType() == 0x10: item.UIAttemptGive(targetId, GetObjectSplitSize(item)) return true if target.IsContainer(): if target is locked: display locked message; return false if targetId != ui.groundObject: display "must open first"; return false placed = AttemptToPlaceInContainer(itemId, targetId, ..., quiet=true) if placed: return true if allowGroundFallback: goto PLACE_ON_GROUND display formatted "Cannot give item to target" return false PLACE_ON_GROUND: playerPhysics = lookup player physics if playerPhysics missing or not on_ground: display "You cannot do that in mid air" return false if splitSize < maxSplitSize: item.UIAttemptSplitTo3D(splitSize) return true if item.current_state != IN_3D_VIEW: item.UIAttemptPutIn3D() return true display "Move cancelled" return false ``` The return value means neither strictly "a server message was sent" nor strictly "the item moved": vendor and secure-trade attempts deliberately return false. Callers that use it only as an action-success boolean will diverge. The vendor-drop test is now pinned. Matching-executable disassembly at `0x0058874C` loads `PWD._bitfield` and `test ah, 0x02`, i.e. overall mask `0x00000200 = BF_VENDOR`. It is an object-trait test, distinct from `ClientUISystem.vendorID`. --- ## 9. Target-mode click ordering ### 9.1 Item lists For left-click message `0x1C`, parameter `7`: ```text UIElement_ItemList.ListenToElementMessage: // 0x004E4D50 item = item under mouse if targetMode != NONE: HandleTargetedUseLeftClick(item) return base-list handler result // no selection/open fallback if item.itemID != 0: apply single-selection visual if configured SetSelectedObject(item.itemID) if this is a container list and child list exists: bind/open child container and scroll it home ``` `HandleTargetedUseLeftClick` dispatches `spellID` first, otherwise `itemID`, using the current target mode. Thus a valid or invalid target-mode click is consumed before selection/container opening. The body itself does not clear target mode. Other item-list mouse actions: - parameter `8`: select the item, then Examine; a spell-only cell examines spell. - parameter `0xA`: double-click UseObject only when not vendor/salvage and either not a container list or the item is the current ground object. ### 9.2 Toolbar slots Toolbar `UseShortcut` follows the same target-first rule, but then explicitly calls `SetTargetMode(NONE)`. This is a real caller difference and should remain in the orchestration policy unless a later input-map trace proves a shared deferred clear. ### 9.3 World clicks `CPlayerSystem::OnAction @ 0x00561890`, on the world-selection action: ```text if targetMode == USE_TARGET: SetTargetMode(NONE) // clear before completion ItemHolder.TargetAcquired(foundObjectId) consume if targetMode == EXAMINE: SetSelectedObject(foundObjectId) SetTargetMode(NONE) ExamineObject(foundObjectId) consume SetSelectedObject(foundObjectId) consume ``` **Unresolved:** the visible `CPlayerSystem::OnAction` branch does not separately execute `TARGET_MODE_USE`; it falls through to selection, while toolbar/item-list paths route mode `USE` through `ExecuteTargetModeForItem`. SmartBox/input-map coordination likely supplies the remaining world-use path. Pin that call chain in Ghidra/cdb before centralizing all world clicks behind one policy. ### 9.4 Target-mode lifecycle `SetTargetMode` registers input map `0x1000000B` at priority `0x7D0` for modes 1..3 and unregisters it for `NONE`, then refreshes the cursor. `ClientUISystem` also owns `m_bLeaveTargetMode`, processed from `UseTime @ 0x005651F0`; the named pseudo-C does not make the originating input transition sufficiently clear. Do not replace this with an arbitrary timeout or focus-loss rule. --- ## 10. Full `ClientUISystem` global cursor catalog `ClientUISystem::UpdateCursorState @ 0x00564630` resolves an enum through master `EnumIDMap` entry `6`, then calls `UIElementManager::SetCursor`. "Found" below means `SmartBox::get_found_object_id() != 0`. | Precedence branch | Not found enum | Found enum | Hotspot | |---|---:|---:|---:| | Busy count `> 0` | `0x0E` | `0x0F` | `(0,0)` | | Target `USE_TARGET` | `0x27` pending | `0x28` compatible / `0x29` incompatible | `(14,14)` | | Target `USE` | `0x0C` | `0x0D` | `(14,14)` | | Target `EXAMINE` | `0x0A` | `0x0B` | `(0,0)` | | No target mode, Magic combat | `0x05` | `0x06` | `(0,0)` | | No target mode, Melee or Missile combat | `0x03` | `0x04` | `(0,0)` | | No target mode, other/NonCombat | `0x01` | `0x02` | `(0,0)` | Enum values `0x07..0x09` have no reachable branch in this method. They may be used by another system or retained in the DAT map; do not assign semantics merely to make the range contiguous. Exact decision order: ```text UpdateCursorState(): // 0x00564630 found = SmartBox.foundObjectId != 0 enum = found ? 2 : 1 hotspot = (0,0) if busyCount > 0: enum = found ? 0x0F : 0x0E else: switch targetMode: NONE: if combat is MELEE or MISSILE: enum = found ? 4 : 3 else if combat is MAGIC: enum = found ? 6 : 5 USE: enum = found ? 0x0D : 0x0C hotspot = (14,14) EXAMINE: enum = found ? 0x0B : 0x0A USE_TARGET: hotspot = (14,14) if not found: enum = 0x27 else if IsTargetCompatibleWithTargetingObject(foundId): enum = 0x28 else: enum = 0x29 did = DBObj.GetDIDByEnum(enum, table=6) if did changed: currentCursorDid = did UIElementManager.SetCursor(did, hotspot, setAsDefault=true) ``` Production DAT golden values pinned by `RetailCursorCatalogTests`: | Enum | RenderSurface DID | |---:|---:| | `0x01` | `0x06004D68` | | `0x02` | `0x06004D69` | | `0x03` | `0x06004D6A` | | `0x04` | `0x06004D6B` | | `0x05` | `0x06004D6C` | | `0x06` | `0x06004D6D` | | `0x0A` | `0x06004D71` | | `0x0B` | `0x06004D71` | | `0x0C` | `0x06004D72` | | `0x0D` | `0x06004D72` | | `0x0E` | `0x06004D74` | | `0x0F` | `0x06004D75` | | `0x27` | `0x06004D73` | | `0x28` | `0x06005E6B` | | `0x29` | `0x06005E6A` | DAT enums `0x07..0x09` resolve to `0x06004D6E..0x06004D70`, but no reachable `UpdateCursorState` branch selects them. They remain intentionally unnamed and outside the global cursor catalog until a retail call site supplies semantics. ### 10.1 Widget cursor precedence is separate and temporal The global cursor selected above is stored as `UIElementManager`'s **default**. Retail widget states can supply their own `MediaDescCursor`: ```text MediaMachine.Update_Cursor(cursorMedia): // 0x00465A80 if media.file is valid: owner.SetCursor(file, hotspot) else: owner.UnSetCursor() UIElement.SetCursor(...): // 0x0045FF50 store widget cursor UIElementManager.CheckCursor() UIElementManager.CheckCursor(): // 0x0045ABF0 if captured element exists and HasCursor(): apply captured element cursor, setAsDefault=false else if last-entered element exists and HasCursor(): apply hovered element cursor, setAsDefault=false else: apply global default cursor, setAsDefault=true ``` Therefore retail precedence is: 1. captured widget cursor; 2. last-entered/hovered widget cursor; 3. `ClientUISystem` global default cursor. However, `ClientUISystem::UpdateCursorState` immediately applies its newly chosen cursor as well as storing it as default. A later widget cursor transition invokes `CheckCursor` and may override it; a later busy/target/combat/found-object change may reassert the global cursor. The exact behavior is event-ordered, not a static rule that target cursors always beat slot cursors or vice versa. **Plan correction:** do not hard-code "global target bullseye always wins widget cursor" as retail behavior. Port the default-versus-local ownership and the `CheckCursor` order, then pin target-mode-over-slot scenarios with event-sequence tests. --- ## 11. Current acdream gaps this oracle retires or sharpens This section describes the inspected code as of 2026-07-10; it is not an implementation patch. 1. `PlayerDescriptionParser.ShortcutEntry` consumes the correct 12 bytes but calls it 16 bytes and immediately separates the raw word into two fields. Preserve a raw-word round trip and expose layered accessors. 2. `ShortcutStore` is GUID-only, so any mutation discards the raw spell word and signed index representation. The toolbar may project object IDs, but the session store cannot if it is also the persistence/wire owner. 3. `ToolbarController.HandleDropRelease` uses the shortcut-source restoration branch for every source. Fresh inventory drops need the cyclic-right empty-slot branch. 4. Toolbar input actions, seven panel buttons, Use, Examine, mana, and stack controls are not fully wired. 5. `ItemInteractionController.ActivateItem` is a useful orchestration seam, but it is not a faithful `UseObject` port: it omits ready/vendor/trade/confirmations, the complete `DetermineUseResult` classification, container post-actions, `AttemptPlaceIn3D`, and retail busy semantics. 6. **Resolved 2026-07-11:** selection is owned by Core `SelectionState` and shared by world/radar/inventory/paperdoll/toolbar/plugins; the former `GameWindow._selectedGuid` and inventory-local selected item are gone. 7. Current cursor catalog exposes only `0x27..0x29`. The reachable global catalog also includes `0x01..0x06` and `0x0A..0x0F`. 8. Current cursor resolution uses a modern static priority chain. Retail instead has a global-default selector plus captured/hovered widget cursor arbitration. 9. Current target controller clears its pending source before compatibility and sends. That matches the one-shot `TargetAcquired` source clear, but click consumption and target-mode clearing differ by retail caller and must be tested at the router boundary. --- ## 12. Required conformance fixtures before production port 1. **Shortcut bytes:** count plus entries containing negative index, object-only, nonzero layered spell word, duplicate index, and out-of-range index. 2. **Toolbar input:** every action range above, stack-entry focus suppression, and CreateShortcut on selected/no-selected object. 3. **Drop planner:** fresh inventory to empty/occupied; shortcut alias to empty/occupied; cyclic-right wrap; no empty slot; duplicate object; release off bar; inventory-button drop. 4. **Selected strip:** unselected, health target, owned mana item, ordinary stack, vendor stack, coinstack, and unusable item. 5. **Use policy:** all `DetermineUseResult` values; direct, targeted, confirmation, vendor, trade, busy/not-ready, source-location, and cannot-use paths. 6. **Placement:** merge, vendor full/partial stack, secure trade, give, locked/ unopened/open container, grounded drop, mid-air rejection, split-to-ground, and already-in-3D cancellation. 7. **Target click sequences:** toolbar, inventory cell, paperdoll cell, world, invalid target, spell-only cell, and target-mode cancel/leave. 8. **Cursor enums:** DAT-resolved DIDs for every reachable enum and event-order sequences covering captured widget, hovered widget, global mode transition, busy transition, and found-object transition. Open retail-research questions are deliberately limited to the corrupted PWD bit names, stack-slider rounding expression, `TARGET_MODE_USE` world-click handoff, and deferred target-mode leave event. None justifies an implementation guess or a workaround.