Unify the toolbar shortcut manager with Runtime inventory state, route retail-ordered shortcut and spellbook command effects through the canonical owners, and make retained controllers borrow those exact instances. Remove the item-interaction transaction fallback and add graphical/no-window parity plus failure-safe terminal ownership-ledger coverage. Co-authored-by: Codex <codex@openai.com>
154 lines
4.1 KiB
Markdown
154 lines
4.1 KiB
Markdown
# Slice J4.5 — retail local gameplay-command ordering
|
|
|
|
## Scope
|
|
|
|
This note fixes the command/local-state ordering used while J4.5 removes the
|
|
last graphical-only shortcut and spellbook mirrors. It does not add a new
|
|
gameplay algorithm. It preserves the existing packet builders and moves the
|
|
retail `PlayerModule` mutation onto the one Runtime-owned state object.
|
|
|
|
Oracle:
|
|
|
|
- `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
|
- `docs/research/named-retail/acclient.h`
|
|
|
|
The existing wire ports remain:
|
|
|
|
- `AcDream.Core.Net.Messages.ClientCommandRequests`
|
|
- `AcDream.Core.Net.WorldSession`
|
|
- `AcDream.App.Net.LiveSessionCommandRouter`
|
|
|
|
## Retail pseudocode
|
|
|
|
### Toolbar shortcut add
|
|
|
|
Retail anchors:
|
|
|
|
- `gmToolbarUI::AddShortcut @ 0x004BD9A0`
|
|
- `PlayerModule::AddShortCut @ 0x005D29A0`
|
|
- `ShortCutManager::AddShortCut @ 0x005D5790`
|
|
|
|
```text
|
|
if slot is valid and the toolbar accepts the item:
|
|
create ShortCutData(slot, objectId, spellId)
|
|
send Event_AddShortCut(data)
|
|
playerModule.AddShortCut(data)
|
|
|
|
ShortCutManager.AddShortCut(data):
|
|
if data.index is outside [0, 18):
|
|
return false
|
|
if the slot already owns a record:
|
|
replace all three record fields in place
|
|
else:
|
|
allocate and install one record
|
|
return true
|
|
```
|
|
|
|
### Toolbar shortcut remove
|
|
|
|
Retail anchors:
|
|
|
|
- `gmToolbarUI::RemoveShortcut @ 0x004BD450`
|
|
- `PlayerModule::RemoveShortCut @ 0x005D29E0`
|
|
- `ShortCutManager::RemoveShortCut @ 0x005D5690`
|
|
|
|
```text
|
|
find the slot containing the selected shortcut
|
|
flush the visible slot
|
|
if this is a persisted removal:
|
|
send Event_RemoveShortCut(slot)
|
|
playerModule.RemoveShortCut(slot)
|
|
|
|
ShortCutManager.RemoveShortCut(slot):
|
|
if slot is outside [0, 18):
|
|
return
|
|
destroy the record if present
|
|
set the slot to empty
|
|
```
|
|
|
|
### Favorite spell add/remove
|
|
|
|
Retail anchors:
|
|
|
|
- spell-menu handler at `0x004C64C0`
|
|
- `PlayerModule::AddSpellFavorite @ 0x005D43E0`
|
|
- `PlayerModule::RemoveSpellFavorite @ 0x005D4910`
|
|
|
|
```text
|
|
add:
|
|
if tab is in [0, 8):
|
|
insert spellId at position in that tab
|
|
send Event_AddSpellFavorite(spellId, position, tab)
|
|
|
|
remove:
|
|
if tab is in [0, 8):
|
|
remove spellId from that tab
|
|
send Event_RemoveSpellFavorite(spellId, tab)
|
|
```
|
|
|
|
Unlike toolbar shortcuts, favorite-spell state changes locally before the
|
|
outbound event.
|
|
|
|
### Spellbook filter
|
|
|
|
Retail anchors:
|
|
|
|
- `gmSpellbookUI::UpdateFilter @ 0x0048B5E0`
|
|
|
|
```text
|
|
derive the changed school/level bit
|
|
derive the next filter bitfield from the button state
|
|
if next differs from current:
|
|
playerModule.SetSpellbookFilter(next)
|
|
send Event_SpellbookFilterEvent(next)
|
|
rebuild the visible spell list
|
|
scroll to row zero
|
|
```
|
|
|
|
### Desired component level
|
|
|
|
Retail anchors:
|
|
|
|
- `gmSpellComponentUI::ListenToElementMessage @ 0x0048A420`
|
|
- `PlayerModule::SetDesiredCompLevel @ 0x005D4940`
|
|
|
|
```text
|
|
parse the edited amount
|
|
if amount is in [0, 5000]:
|
|
send Event_SetDesiredComponentLevel(componentId, amount)
|
|
playerModule.SetDesiredCompLevel(componentId, amount)
|
|
else:
|
|
restore the current value in the editor
|
|
|
|
PlayerModule.SetDesiredCompLevel(componentId, amount):
|
|
if amount is outside [0, 5000]:
|
|
return false
|
|
lazily create the desired-component table
|
|
replace an existing value or add a new value
|
|
return true
|
|
```
|
|
|
|
Retail retains an explicit zero-valued entry. J4.5 preserves that distinction;
|
|
only the clear-list command destroys the complete desired-component table.
|
|
|
|
### Clear desired components
|
|
|
|
Retail anchors:
|
|
|
|
- chat-command handler at `0x0056FE4A`
|
|
- `PlayerModule::ClearDesiredCompList @ 0x005D2A00`
|
|
|
|
```text
|
|
send Event_SetDesiredComponentLevel(INVALID_DID, -1)
|
|
destroy the complete desired-component table
|
|
set the table pointer to null
|
|
publish the spell-component UI update notice
|
|
```
|
|
|
|
## J4.5 integration consequence
|
|
|
|
The graphical toolbar, retained spell UI, plugins, and future no-window host
|
|
must all mutate the same Runtime-owned `ShortcutStore` and `Spellbook`.
|
|
Graphical controllers may initiate the operation, but they may not retain or
|
|
reconstruct another mutable slot/favorite/component collection. Commands stay
|
|
synchronous; no additional frame or background queue is introduced.
|