Port the retail horizontal ItemList empty-slot padding and share UIItem shortcut-number graphics with the status toolbar. Preserve all authored face children on compound DAT buttons so the three-piece Cast control reflows and renders as one complete button. Co-authored-by: OpenAI Codex <codex@openai.com>
623 lines
25 KiB
Markdown
623 lines
25 KiB
Markdown
# Retail magic UI and casting pseudocode
|
||
|
||
This note is the implementation oracle for the M3 cast lifecycle and retained
|
||
spell UI. Retail addresses refer to the September 2013 EoR named client.
|
||
Wire layouts were cross-checked against ACE and the existing holtburger-derived
|
||
`PlayerDescriptionParser`.
|
||
|
||
## Cast request ownership
|
||
|
||
Retail: `ClientMagicSystem::CastSpell` `0x00568040` and
|
||
`ClientMagicSystem::FreeHandsAndCastSpell` `0x00566EF0`.
|
||
|
||
```text
|
||
CastSpell(spellId, useSelectedTarget):
|
||
spell = SpellTable[spellId]
|
||
if components are required:
|
||
formula = GetAppropriateSpellFormula(spell)
|
||
if any required component is absent:
|
||
display "You do not have all of this spell's components."
|
||
return
|
||
|
||
if spell targets self:
|
||
target = local player id (or untargeted when no SmartBox exists)
|
||
FreeHandsAndCastSpell(spellId, target)
|
||
return
|
||
|
||
if spell has no target type:
|
||
MaybeStopCompletely()
|
||
send CastUntargetedSpell(spellId)
|
||
increment UI busy count
|
||
return
|
||
|
||
target = selected object
|
||
if target == 0:
|
||
display "You must select a suitable target."
|
||
return
|
||
if the selected GUID no longer resolves to an object:
|
||
return silently
|
||
if ObjectCompatibleWithSpell(target, spellId):
|
||
FreeHandsAndCastSpell(spellId, target)
|
||
reselect target
|
||
|
||
FreeHandsAndCastSpell(spellId, target):
|
||
MaybeStopCompletely()
|
||
if target == 0: send CastUntargetedSpell(spellId)
|
||
else: send CastTargetedSpell(target, spellId)
|
||
increment UI busy count
|
||
```
|
||
|
||
The request is sent immediately. ACE remains authoritative for turning,
|
||
casting motion, component consumption, mana use, fizzle, impact, damage, and
|
||
enchantment state. The client does not invent a parallel gameplay outcome.
|
||
|
||
### Formula selection and target compatibility
|
||
|
||
Retail: `ClientMagicSystem::GetAppropriateSpellFormula` `0x00567D50`,
|
||
`CSpellBase::InqScarabOnlyFormula` `0x00597050`,
|
||
`ACCWeenieObject::MagicPackIsOwned` `0x0058CB80`, and
|
||
`ClientMagicSystem::ObjectCompatibleWithSpellTargetType` `0x00567230`.
|
||
|
||
```text
|
||
GetAppropriateSpellFormula(spell):
|
||
schoolFocusWcid = SchoolOfMagic2WCID(spell.school)
|
||
infusedProperty = [War 0x129, Life 0x128, Item 0x127,
|
||
Creature 0x126, Void 0x148][school]
|
||
if infusedProperty > 0 OR a directly carried side-pack has schoolFocusWcid:
|
||
retain every power component in formula order
|
||
strongest = highest retained component power
|
||
append prismatic taper SCID 0xBC:
|
||
power 1 => 1; power 2 => 2; power 3/4/7 => 3;
|
||
power 5/6/8/9/10 => 4
|
||
return result
|
||
return account-customized formula
|
||
|
||
ObjectCompatibleWithSpellTargetType(target, targetMask):
|
||
special = targetMask & 0x8107
|
||
reject self when special == 0
|
||
reject stacks
|
||
require (targetMask & target.type) != 0 OR special != 0
|
||
require target.IsPlayer OR target.publicBitfield has BF_ATTACKABLE
|
||
require target.petOwner == 0
|
||
```
|
||
|
||
The final Player/Attackable and pet gates apply after both the ordinary type
|
||
match and the special-mask bypass. Formula entries are SCIDs; inventory and
|
||
favorite-component events use WCIDs, resolved through DualEnumIDMap
|
||
`0x27000002`. The magic-school focus map is retail enum category `0x28`, key
|
||
`0x10000001`.
|
||
|
||
## Authoritative enchantment events
|
||
|
||
Retail dispatchers:
|
||
|
||
- `CM_Magic::DispatchUI_UpdateEnchantment` `0x006A32F0`
|
||
- `CM_Magic::DispatchUI_RemoveEnchantment` `0x006A2FB0`
|
||
- `CM_Magic::DispatchUI_DispelEnchantment` `0x006A2F20`
|
||
|
||
```text
|
||
0x02C2 UpdateEnchantment:
|
||
unpack one Enchantment record
|
||
|
||
0x02C4 UpdateMultipleEnchantments:
|
||
u32 count
|
||
repeat count times: unpack Enchantment record
|
||
|
||
Enchantment record (60 or 64 bytes):
|
||
u16 spellId
|
||
u16 layer
|
||
u16 spellCategory
|
||
u16 hasSpellSetId
|
||
u32 powerLevel
|
||
f64 startTime
|
||
f64 duration
|
||
u32 casterGuid
|
||
f32 degradeModifier
|
||
f32 degradeLimit
|
||
f64 lastDegraded
|
||
u32 statModType
|
||
u32 statModKey
|
||
f32 statModValue
|
||
if hasSpellSetId: u32 spellSetId
|
||
|
||
0x02C3 RemoveEnchantment:
|
||
u16 spellId
|
||
u16 layer
|
||
|
||
0x02C7 DispelEnchantment:
|
||
u16 spellId
|
||
u16 layer
|
||
|
||
0x02C5 / 0x02C8 Remove/DispelMultipleEnchantments:
|
||
u32 count
|
||
repeat count times: u16 spellId, u16 layer
|
||
|
||
0x02C6 PurgeEnchantments:
|
||
no payload; clear the authoritative active set
|
||
```
|
||
|
||
## Magic combat page (spell bar)
|
||
|
||
Retail: `gmSpellcastingUI::PostInit` `0x004C5D00`,
|
||
`SpellCastSubMenu::Init` `0x004C5A90`, and `gmSpellcastingUI::Cast`
|
||
`0x004C6050`.
|
||
|
||
The spell bar is the authored Magic page inside combat layout `0x21000073`.
|
||
It is not an advanced-combat placeholder.
|
||
|
||
```text
|
||
combat basic page 0x1000005C
|
||
spellcasting page 0x10000061
|
||
spellcasting content 0x100000A2
|
||
spell name text 0x1000048B
|
||
spellcasting background 0x100000A0
|
||
cast button 0x100000B2
|
||
endowment icon 0x100000B1
|
||
|
||
favorite tab buttons 0x100000A3..0x100000A9, 0x100005C2
|
||
favorite list groups 0x100000AA..0x100000B0, 0x100005C3
|
||
item list within each group 0x100000B6
|
||
```
|
||
|
||
There are eight favorite tabs. Each list is authoritative character state.
|
||
Selecting a spell updates its name and current spell. Cast invokes
|
||
`ClientMagicSystem::CastSpell(selectedSpell, true)`. PageUp/PageDown and the
|
||
retail spell input actions change tab/selection; the cast-current action casts
|
||
the current selection. Favorite edits are sent to ACE.
|
||
|
||
The visible row is still an ordinary `UIElement_ItemList`; it is not a
|
||
spell-specific strip painted by `gmSpellcastingUI`:
|
||
|
||
```text
|
||
UIElement_ItemList::UpdateEmptySlots @ 0x004E3700
|
||
if list is visible and FixedSlots (0x10000015) == -1:
|
||
read row/column constraints 0x5F and 0x60
|
||
when the horizontal constraint is unbounded:
|
||
occupiedWidth = itemCount * cellWidth
|
||
append floor((listWidth - occupiedWidth) / cellWidth) empty UIItems
|
||
when the list shrinks, remove only trailing UIItems in ItemSlot_Empty state
|
||
never remove an occupied trailing item merely because it exceeds the viewport
|
||
|
||
SpellCastSubMenu::UpdateShortcutOverlays @ 0x004C5BE0
|
||
for each UIItem in list order:
|
||
if index < 9: SetShortcutNum(item, index, ghosted=false)
|
||
else: SetShortcutNum(item, -1, ghosted=false)
|
||
|
||
UIElement_UIItem::SetShortcutNum @ 0x004E1590
|
||
occupied spell shortcut -> regular digit array 0x10000042
|
||
empty padding cell -> empty digit array 0x1000005E
|
||
```
|
||
|
||
Thus the first nine visible cells use the same blue 1–9 UIItem overlays as the
|
||
main status toolbar, including empty cells, while later cells retain only the
|
||
authored `ItemSlot_Empty` background. The count is based on the reflowed list
|
||
width (639 pixels at the 800-pixel design parent, 439 pixels inside the live
|
||
600-pixel combat page, yielding 13 whole 32-pixel cells).
|
||
|
||
The Cast button is likewise fully DAT-authored rather than a synthesized red
|
||
rectangle. Element `0x100000B2` is 75 by 32 pixels and its face is three stateful
|
||
children authored against a 79-pixel design parent: left `0..31`, middle
|
||
`32..46`, and right `47..78`. Apply each child's independent retail edge policy
|
||
after the button reflows; at 75 pixels they become `0..31`, `32..42`, and
|
||
`43..74`. Collapsing child-authored button media to the first child renders only
|
||
the left cap and is not retail behavior.
|
||
|
||
Outbound character events (`CM_Character`):
|
||
|
||
```text
|
||
0x01E3 AddSpellFavorite(spellId:u32, position:i32, tab:i32)
|
||
0x01E4 RemoveSpellFavorite(spellId:u32, tab:i32)
|
||
0x0286 SpellbookFilterEvent(filter:u32)
|
||
0x0224 SetDesiredComponentLevel(componentDid:u32, amount:i32)
|
||
```
|
||
|
||
## Spellbook and component book
|
||
|
||
Retail: `gmSpellbookUI::PostInit` `0x0048B2B0` and
|
||
`gmSpellComponentUI::PostInit` `0x0048A190`.
|
||
|
||
Both pages are authored in layout `0x21000034` (300 by 600 pixels):
|
||
|
||
```text
|
||
window root 0x100002A8
|
||
spellbook tab 0x100002A9
|
||
component tab 0x100002AA
|
||
close button 0x100002AB
|
||
spellbook page 0x100002AC (base 0x10000294 / 0x21000032)
|
||
component page 0x100002AD (base 0x100002A7 / 0x21000033)
|
||
|
||
spell list 0x10000295
|
||
school buttons 0x10000298..0x1000029B, 0x100005C0
|
||
level buttons 1..7 0x1000029C..0x100002A2
|
||
level button 8 0x1000054E
|
||
|
||
component item list 0x10000464
|
||
component scrollbar 0x10000465
|
||
```
|
||
|
||
The two top-level tabs are not `UIElement_Button` instances. They resolve from
|
||
base `0x1000043A` as `UIElement_Text` (type 12), with state 11 `Closed` and
|
||
state 12 `Open`. Both states set `PassToChildren`; the three child chrome
|
||
elements carry the left/center/right Closed/Open RenderSurface media.
|
||
Their direct string property `0x17` resolves through the installed string
|
||
tables to `Spellbook` (string id `49867858`) and `Components` (string id
|
||
`79360818`).
|
||
|
||
Retail state and text construction used by these tabs:
|
||
|
||
```text
|
||
UIElement_Text::UIElement_Text @ 0x00468570
|
||
construct UIElement_Scrollable
|
||
bitField = 0x300 // OneLine bit 0x2 is clear
|
||
horizontalJustification = 2
|
||
verticalJustification = 4
|
||
marginUp/Down/Left/Right = 0
|
||
|
||
UIElement_Text::OnSetAttribute @ 0x0046A640
|
||
property 0x20 only:
|
||
SetOneLine(value)
|
||
// absent 0x20 therefore remains multi-line; do not force it true
|
||
|
||
UIElement_Text::CalcJustification @ 0x00467260
|
||
available = max(scrollableExtent, surfaceExtent) - leadingMargin - trailingMargin
|
||
if justification is Center:
|
||
offset = available / 2 - contentExtent / 2
|
||
else if justification is Right or Bottom:
|
||
offset = available - contentExtent
|
||
else:
|
||
offset = 0
|
||
return leadingMargin + offset
|
||
|
||
UIElement::SetState @ 0x00464E70
|
||
next = desc.AccessStateDesc(stateId)
|
||
if next != current:
|
||
current = next
|
||
if next.PassToChildren:
|
||
for each child:
|
||
child.SetState(stateId)
|
||
diff old/new effective properties
|
||
call OnSetAttribute for changed properties
|
||
|
||
Retained rendering reduction:
|
||
draw the UIElement_Text background
|
||
draw its three stateful chrome children
|
||
draw the authored glyph content as the parent's foreground
|
||
```
|
||
|
||
Retail `UIElement_Text::DrawSelf @ 0x00467AA0` owns the glyphs while the child
|
||
regions own the state chrome. acdream's sprites and DAT glyphs share one
|
||
submission-ordered batch, so submitting the glyphs in the parent's foreground
|
||
pass preserves that same visible composition; submitting the entire parent
|
||
before its retained children lets the center chrome overpaint the caption.
|
||
This rule is derived structurally from `PassToChildren`, not from the two
|
||
spellbook element ids.
|
||
|
||
The same mechanism applies to the Character window `0x2100002E`. Its
|
||
Attributes `0x10000228`, Skills `0x10000229`, and Titles `0x10000538` controls
|
||
are also compound Type-12 tabs with Closed/Open state color and three authored
|
||
chrome children. The controller reduction is therefore shared:
|
||
|
||
```text
|
||
BindRetailTab(tab, activate):
|
||
tab.Click = activate
|
||
|
||
SetActiveTab(active):
|
||
for each tab:
|
||
tab.SetState(tab == active ? Open(12) : Closed(11))
|
||
// UIElement::SetState propagates the same state to the retained children
|
||
```
|
||
|
||
Page visibility remains controller-owned because the three character page
|
||
containers carry no active-page visibility state. Tab appearance does not:
|
||
injecting replacement sprites or hard-coded caption colors bypasses the exact
|
||
same DAT state machine already used by Spellbook/Components.
|
||
|
||
Layout `0x21000034` supplies the 300 by 600 content surface, including its tab
|
||
bar and close control, but not the enclosing movable-window border. Production
|
||
mounts it through the same shared retained nine-slice frame used by the
|
||
Attributes/Skills window (`0x2100002E`). Treating the root as already framed
|
||
removes the gold outer border.
|
||
|
||
The Magic-combat favorite tab captions are authored text (`I` through `VIII`)
|
||
and intentionally omit property `0x20`. Their small rectangles still render
|
||
because retail begins with zero text margins. A generic four-pixel inset on
|
||
all sides leaves less than one DAT-font line and clips the captions completely;
|
||
that inset is not retail behavior.
|
||
|
||
Spellbook rows come only from the authoritative learned-spell set. Retail
|
||
sorts by `CSpellBase::_display_order`; school and level buttons apply the
|
||
server-persisted filter bitfield. Component rows use the installed component
|
||
table and the server-persisted desired purchase amounts.
|
||
|
||
### Component-book listbox templates and synchronization
|
||
|
||
Named-retail anchors:
|
||
|
||
- `gmSpellComponentUI::PostInit @ 0x0048A190`
|
||
- `gmSpellComponentUI::ListenToElementMessage @ 0x0048A420`
|
||
- `gmSpellComponentUI::SyncComponentRegionWithData @ 0x0048A620`
|
||
- `gmSpellComponentUI::UpdateComponents @ 0x0048A910`
|
||
- `gmSpellComponentUI::RecvNotice_SelectionChanged @ 0x00489C40`
|
||
- `gmSpellComponentUI::UpdateBuyRates @ 0x00489FE0`
|
||
|
||
The component page is not a generic icon catalog. LayoutDesc `0x21000033`
|
||
authors a listbox with two entry templates, referenced in order by list
|
||
property `0x64`:
|
||
|
||
```text
|
||
component list 0x10000464 (284 x 600)
|
||
component scrollbar 0x10000465 (16 x 600)
|
||
|
||
template 0 / category 0x10000466 (284 x 32, UIElement_Text)
|
||
background 0x06001392
|
||
centered category font 0x40000004
|
||
|
||
template 1 / component 0x10000467 (284 x 32, UIElement_Field)
|
||
normal background 0x06005E21
|
||
Highlight background 0x06005E22
|
||
icon region 0x10000468 (x=0, 32 x 32)
|
||
name 0x10000469 (x=42, width=180, height=32)
|
||
owned count 0x1000046A (x=224, y=0, width=60, height=15)
|
||
desired purchase count 0x1000046B (x=224, y=15, width=60, height=15)
|
||
```
|
||
|
||
The seven localized headings are string hashes in local string table
|
||
`0x23000001`: `SCARABS`, `HERBS`, `POWDERED GEMS`, `ALCHEMICAL SUBSTANCES`,
|
||
`TALISMANS`, `TAPERS`, and `PEAS`. They correspond in order to retail
|
||
`SpellComponentCategory` values 0 through 6. Category 6 is Peas; treating it
|
||
as an invented "Prismatic Components" group is not retail behavior.
|
||
|
||
```text
|
||
PostInit():
|
||
componentList = GetChildRecursive(0x10000464) as ListBox
|
||
register component/player/selection notices
|
||
if PlayerDescription already arrived: UpdateBuyRates(all components)
|
||
|
||
UpdateComponents(change):
|
||
tracker = PlayerSystem.ComponentTracker
|
||
insertion = 0
|
||
for category in 0..6:
|
||
if tracker.componentLists[category] is empty:
|
||
remove a stale category entry for this category, if present
|
||
continue
|
||
|
||
if entry[insertion] is not this category header:
|
||
header = list.AddItemFromTemplateList(templateIndex=0, insertion)
|
||
header.attribute[0x1000004D] = category
|
||
header.text = localizedCategoryTitle[category]
|
||
insertion++
|
||
|
||
reconcile component rows against tracker.componentLists[category]
|
||
each new row = list.AddItemFromTemplateList(templateIndex=1, insertion)
|
||
row.attribute[0x1000004C] = componentWCID
|
||
row[0x10000468] = MakeIcon(componentWCID)
|
||
row[0x10000469] = componentName
|
||
row[0x1000046A] = totalOwnedCount
|
||
row[0x1000046B] = desiredPurchaseCount
|
||
row[0x1000046B].inputFilter = NumberInputFilter
|
||
insertion++
|
||
|
||
SyncComponentRegionWithData(row, componentData):
|
||
row[0x1000046A].text = componentData.numItems
|
||
|
||
UIElement_Text::DrawSelf(rowText, visibleSurface):
|
||
position each glyph line using CalcJustification
|
||
draw intersecting glyphs clipped to visibleSurface
|
||
do not discard a line merely because its line box exceeds the surface
|
||
|
||
ListenToElementMessage(message):
|
||
if selection changed:
|
||
if selected entry is a category:
|
||
clear selected world object
|
||
else if selected entry is a component:
|
||
select the first owned object represented by that component region
|
||
|
||
if edit completed on 0x1000046B:
|
||
parse integer
|
||
if 0 <= value <= 5000:
|
||
send SetDesiredComponentLevel(componentWCID, value)
|
||
update local PlayerModule projection
|
||
else:
|
||
restore the authoritative desired value in the field
|
||
|
||
RecvNotice_SelectionChanged():
|
||
if the selected world object belongs to a tracked component region:
|
||
select and expose its matching component row
|
||
else:
|
||
clear component-list selection
|
||
```
|
||
|
||
The retained port instantiates these resolved templates through
|
||
`ComponentBookTemplateFactory` and `UiTemplateListSlot`, the architectural
|
||
equivalent of `AddItemFromTemplateList`. Controllers bind only the live icon,
|
||
texts, selection, and edit callbacks. They do not duplicate the row sprites,
|
||
fonts, or geometry.
|
||
|
||
The owned-count child is 15 pixels high while installed font `0x40000000`
|
||
advances 16 pixels. Retail's `UIElement_Text::DrawSelf @ 0x00467AA0` clips the
|
||
glyph blits against the supplied visible surface, so the count remains visible.
|
||
A retained renderer that accepts only fully contained lines suppresses this
|
||
field and is not retail-conformant.
|
||
|
||
### Spellbook row, selection, filtering, dragging, and deletion
|
||
|
||
Named-retail anchors:
|
||
|
||
- `gmSpellbookUI::IsFilteredOut @ 0x0048AEB0`
|
||
- `gmSpellbookUI::GetSortedInsertionPlace @ 0x0048B440`
|
||
- `gmSpellbookUI::SetSelected @ 0x0048B540`
|
||
- `gmSpellbookUI::UpdateFilter @ 0x0048B5E0`
|
||
- `gmSpellbookUI::AddSpell @ 0x0048B7A0`
|
||
- `gmSpellbookUI::DeleteSpellDialogCallback @ 0x0048BC10`
|
||
- `gmSpellbookUI::DeleteSpell @ 0x0048BD50`
|
||
- `gmSpellbookUI::ListenToElementMessage @ 0x0048BFD0`
|
||
- `UIElement_UIItem::Init_UIItem_Spell_Shortcut @ 0x004E1140`
|
||
- `UIElement_ItemList::ItemList_AddSpellShortcut @ 0x004E40A0`
|
||
- `UIElement_ItemList::ItemList_InsertSpellShortcut @ 0x004E41B0`
|
||
- `CM_Magic::Event_RemoveSpell @ 0x006A3220`
|
||
|
||
DAT structure:
|
||
|
||
```text
|
||
spell list 0x10000295 (280 x 224)
|
||
spell scrollbar 0x10000296 (16 x 224)
|
||
spell-row prototype property 0x1000000E -> 0x10000343
|
||
shared UIItem catalog 0x21000037
|
||
row prototype 0x10000343 (280 x 32)
|
||
row background 0x06001396
|
||
selected overlay child 0x10000342 -> 0x06001397
|
||
label child 0x10000344 (x=42, width=230)
|
||
icon child 0x1000033B (x=0, 32 x 32)
|
||
school/level filters Type-1, 90 x 14; label property on parent
|
||
filter indicator child 0x10000328 (x=0, y=1, 13 x 13)
|
||
filter Normal/Highlight art 0x06004D15 / 0x06004D17
|
||
delete button 0x100002A5
|
||
delete label child 0x100002A6 (100 x 32, centered, font 0x40000001)
|
||
```
|
||
|
||
The row's authored background contains the narrow horizontal separator visible
|
||
between entries. It is not an invented procedural line. The selection surface
|
||
is a separate overlay, so selecting a spell does not replace its icon or label.
|
||
The filter controls are ordinary `UIElement_Button` objects whose states have
|
||
`PassToChildren`; the green selected face is therefore the `Highlight` media on
|
||
their 13-pixel child, while the parent owns the label and the 90-pixel hit box.
|
||
The label begins four pixels after that child. Conversely, the Delete button
|
||
owns its frame media but its caption/font live in a full-size text child. A
|
||
retained leaf must lift those authored child roles; discarding all button
|
||
children loses both the filter indicator and the Delete caption.
|
||
|
||
```text
|
||
AddSpell(spellID):
|
||
if spell metadata is missing: return
|
||
if IsFilteredOut(spell): return
|
||
insertion = first existing row whose displayOrder > spell.displayOrder
|
||
list.InsertSpellShortcut(spellID, insertion)
|
||
|
||
SetSelected(spellID):
|
||
for each spell shortcut row:
|
||
row.selectedOverlay = (row.spellID == spellID)
|
||
if selected: list.ScrollItemIntoView(row)
|
||
selectedSpellID = spellID
|
||
|
||
ListenToElementMessage(message):
|
||
if message came from a spell shortcut row:
|
||
if message kind is click or double-click: SetSelected(row.spellID)
|
||
if message kind is AddSpellShortcut/drag notice:
|
||
CM_Magic.SendNotice_AddSpellShortcut(row.spellID)
|
||
if message is click on a school/level filter:
|
||
UpdateFilter(clickedFilter)
|
||
if message is click on 0x100002A5:
|
||
DeleteSpell()
|
||
|
||
UpdateFilter(button):
|
||
mask = exact school/level bit assigned to button
|
||
if button is currently Highlight: filters |= mask
|
||
else: filters &= ~mask
|
||
if filters changed:
|
||
CM_Character.Event_SpellbookFilterEvent(filters)
|
||
rebuild list from authoritative learned spells
|
||
scroll list to first row
|
||
|
||
IsFilteredOut(spell):
|
||
school bits: Creature=0x0001, Item=0x0002, Life=0x0004,
|
||
War=0x0008, Void=0x2000
|
||
level bits: I=0x0010 through VIII=0x0800
|
||
return school bit is disabled OR level bit is disabled
|
||
|
||
DeleteSpell():
|
||
if selectedSpellID == 0: return false
|
||
name = ClientMagicSystem.GetSpellName(selectedSpellID)
|
||
message = "Are you sure you want to remove {name} from your spellbook? "
|
||
"You will no longer be able to cast this spell unless you learn it again!"
|
||
dialog properties include selectedSpellID under 0x1000003F
|
||
DialogFactory.MakeCallbackDialogInCurrentUI(message, callback)
|
||
|
||
DeleteSpellDialogCallback(dialogProperties):
|
||
if confirmation result property 0x92 is true:
|
||
spellID = property 0x1000003F
|
||
CM_Magic.Event_RemoveSpell(spellID)
|
||
|
||
CM_Magic.Event_RemoveSpell(spellID):
|
||
send GameAction opcode 0x01A8 followed by spellID
|
||
|
||
RecvNotice_SpellRemoved(spellID):
|
||
rebuild from the authoritative player description
|
||
```
|
||
|
||
The client does not optimistically erase the row when Yes is clicked. It waits
|
||
for the server's `MagicRemoveSpell` notice. Dragging a row similarly carries a
|
||
spell-shortcut identity into the open spellcasting favorite list; it never
|
||
removes the spell from the learned-spell set.
|
||
|
||
## Active enchantment panels
|
||
|
||
Retail: `gmEffectsUI::PostInit` `0x004B7560` and
|
||
`gmEffectsUI::RebuildList` `0x004B8350`.
|
||
|
||
```text
|
||
layout 0x2100001B
|
||
positive root 0x1000011F (inherits 0x10000122)
|
||
negative root 0x10000121 (inherits 0x10000122)
|
||
shared effects template 0x10000122
|
||
effect list 0x10000123
|
||
information text 0x10000126
|
||
effect UI type property 0x1000000C
|
||
```
|
||
|
||
The `0x10000184`/`0x10000185` values used by `gmPanelUI` are panel-slot
|
||
identities, not roots in effects LayoutDesc `0x2100001B`. The two authored
|
||
effects roots carry their title/close overrides and inherit the list,
|
||
scrollbar, information area, and remaining chrome from `0x10000122`.
|
||
|
||
Retail `gmPanelUI::SetupChildren` (`0x004BC9E0`) registers those children as
|
||
panel IDs 4 and 5. `RecvNotice_SetPanelVisibility` (`0x004BC6F0`) owns one
|
||
active child: showing either effect panel hides the previous main panel, while
|
||
closing the active child hides the shared host. DAT bool property `0x10000049`
|
||
selects the exceptional save-and-restore-previous behavior. The resolved
|
||
Helpful/positive root sets it, so closing Helpful restores the preceding
|
||
ordinary panel. Harmful/negative does not set it and leaves the host empty.
|
||
|
||
The reopen path is not the main toolbar. Retail
|
||
`gmUIElement_EffectsIndicator::PostInit` (`0x004E6930`) reads effect type 1 or
|
||
2 from property `0x1000000C`, and `Update` (`0x004E6A50`) ghosts the button
|
||
when no matching enchantment exists. The floating indicators layout is
|
||
`0x21000071`; Helpful button `0x100000F5` opens panel 4 and Harmful button
|
||
`0x100000F6` opens panel 5. The docked equivalent is layout `0x21000015`.
|
||
|
||
Retail `LayoutDesc::InqFullDesc` (`0x0069A520`) resolves the base first and
|
||
then calls `ElementDesc::Incorporate` (`0x0069B5A0`). Child tables are merged
|
||
by element identity: base-only children remain, same-identity children are
|
||
incorporated recursively, and derived-only children are appended with their
|
||
read order shifted by the retained base-only count.
|
||
|
||
When visible, each panel rebuilds from the authoritative active-enchantment
|
||
snapshot and refreshes each token's duration at most once per second.
|
||
`CEnchantmentRegistry::GetEnchantmentsInEffect` (`0x00594540`) merges only the
|
||
multiplicative and additive registries, so Vitae and cooldown records neither
|
||
appear in these panels nor enable an indicator. Each incoming entry is dueled
|
||
against the current winner for its spell category (`Duel @ 0x005942B0`): higher
|
||
power wins, with later start time breaking an equal-power tie. Positive and
|
||
negative panels then test the winning spell's Beneficial flag and sort
|
||
alphabetically.
|
||
Live `AddEnchantmentToList @ 0x00593DF0` inserts a new identity at the bucket
|
||
head, while an existing identity refreshes in place; bulk UnPack retains its
|
||
received head-to-tail order. Since an exact power/start tie keeps the first
|
||
duel incumbent, the newest incrementally added identity wins an exact tie.
|
||
|
||
The Helpful/Harmful indicators intentionally use a different projection.
|
||
`CountSpellsInList @ 0x00593CD0` feeds the registry's two indicator counters by
|
||
visiting every raw mult/add node before any category duel. Therefore opposite-
|
||
sign spells in the same category can enable both indicators while only the duel
|
||
winner appears in the effects list.
|
||
Remaining duration is `(startTime + duration) - current game time`. Selecting
|
||
an active token stores its spell ID, so every layer of that spell highlights
|
||
together and shows the shared name/description; clicking either layer again
|
||
clears the selection.
|
||
|
||
## Portal presentation boundary
|
||
|
||
Recall and portal presentation remains DAT-driven. `/ls` starts the retail
|
||
recall motion (`0x10000153`); its animation hooks/PES chain supplies the cloud.
|
||
Remote disappearance and materialization use authoritative Hidden/UnHide state
|
||
and typed physics scripts. No generic teleport effect, guessed color, or local
|
||
teleport outcome is added by the spell UI work.
|