acdream/docs/research/2026-07-10-retail-toolbar-interaction-pseudocode.md

1020 lines
39 KiB
Markdown

# 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<uint> 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
finds an empty slot; a duplicate does not get added again.
### 4.2 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.3 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.4 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.5 Full-stack merge 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.
---
## 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, or formatted name + count 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
- `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.
- Slider message `0xA` maps its normalized value to an integer in
`1..maxSplitSize`, stores it, rewrites the entry, and broadcasts the notice.
**Unresolved:** Binary Ninja lost the exact floating expression immediately before
the slider's `1 - _ftol2()` conversion at `0x004BF02D`. The clamp, endpoints, and
text update are exact, but the intermediate rounding direction must be recovered
from Ghidra/disassembly before writing conformance vectors.
---
## 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 is corrupted into a string address by Binary
Ninja. Capacity fields and `IsComponentPack` make the branch intent clear, but the
missing bit name/value is **unresolved** and must be recovered before porting.
`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.
**Semantic name uncertain:** the vendor-drop test appears in pseudo-C as a raw
bitfield byte test. The branch behavior is exact, but its PWD flag name must be
recovered before making a reusable Core policy field; do not substitute a check
against `ClientUISystem.vendorID` without that evidence.
---
## 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 already pinned in tests:
| Enum | RenderSurface DID |
|---:|---:|
| `0x27` | `0x06004D73` |
| `0x28` | `0x06005E6B` |
| `0x29` | `0x06005E6A` |
The DIDs for `0x01..0x0F` should be committed as DAT conformance fixtures before
implementation. The enum behavior above is exact even without those media IDs.
### 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. Selection remains owned by `GameWindow._selectedGuid`; the previously documented
`Core.SelectionState` file is absent in this worktree.
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.