Reflow selected Helpful/Harmful spell descriptions through the retained retail text shaper, bind their authored 0x10000127 information scrollbar independently from the list scrollbar, and allow display-only text to consume wheel scrolling without becoming selectable. Format Vitae recovery experience through retail's grouped integer presentation. Release build succeeds and all 5,832 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
27 KiB
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.
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.
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_UpdateEnchantment0x006A32F0CM_Magic::DispatchUI_RemoveEnchantment0x006A2FB0CM_Magic::DispatchUI_DispelEnchantment0x006A2F20
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.
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:
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 two layers must not be conflated. The favorite ItemList is inherited through
LayoutDesc 0x21000010; its attribute 0x1000000E selects UIItem prototype
0x10000341 in catalog 0x21000037. That prototype's m_elem_Icon
(0x1000033B) maps ItemSlot_Empty to the brown/gold surface 0x06001A97.
The blue toolbar-style 0x060074CF surface is not the unnumbered favorite-cell
background. Resolve the list prototype after cross-layout inheritance, then add
the independent shortcut-number overlay only for indices zero through eight.
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):
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):
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:
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:
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 @ 0x0048A190gmSpellComponentUI::ListenToElementMessage @ 0x0048A420gmSpellComponentUI::SyncComponentRegionWithData @ 0x0048A620gmSpellComponentUI::UpdateComponents @ 0x0048A910gmSpellComponentUI::RecvNotice_SelectionChanged @ 0x00489C40gmSpellComponentUI::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:
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.
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 @ 0x0048AEB0gmSpellbookUI::GetSortedInsertionPlace @ 0x0048B440gmSpellbookUI::SetSelected @ 0x0048B540gmSpellbookUI::UpdateFilter @ 0x0048B5E0gmSpellbookUI::AddSpell @ 0x0048B7A0gmSpellbookUI::DeleteSpellDialogCallback @ 0x0048BC10gmSpellbookUI::DeleteSpell @ 0x0048BD50gmSpellbookUI::ListenToElementMessage @ 0x0048BFD0UIElement_UIItem::Init_UIItem_Spell_Shortcut @ 0x004E1140UIElement_ItemList::ItemList_AddSpellShortcut @ 0x004E40A0UIElement_ItemList::ItemList_InsertSpellShortcut @ 0x004E41B0CM_Magic::Event_RemoveSpell @ 0x006A3220
DAT structure:
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.
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.
layout 0x2100001B
positive root 0x1000011F (inherits 0x10000122)
negative root 0x10000121 (inherits 0x10000122)
shared effects template 0x10000122
effect list 0x10000123
effect-list scrollbar 0x10000124
information text 0x10000126
information scrollbar 0x10000127
effect row template 0x10000128 (300 x 32)
row icon / name / duration 0x10000129 / 0x1000012A / 0x1000012B
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.
gmEffectsUI::UpdateSelection @ 0x004B7F90 concatenates spell name, a blank
line, and spell description, then passes that one literal to
UIElement_Text::SetText. The text element reflows the literal to its authored
280-pixel information width. LayoutDesc 0x2100001B supplies a separate
scrollbar 0x10000127 for that 78-pixel-high information text; it does not
reuse the effect-list scrollbar 0x10000124.
InfoRegion::InfoRegion @ 0x004F1450 creates each row with
UIElement_ListBox::AddItemFromTemplateList(template 0). In installed retail
data the icon is x=0 width 32, name is x=37 width 188, and duration is x=225
width 50. EffectInfoRegion::Update @ 0x004F1C00 formats finite time as
m:ss or h:mm:ss; a negative/permanent duration leaves the field empty.
gmEffectsUI::UpdateSelection @ 0x004B7F90 displays localized
ID_Effects_Info_SelectASpell while nothing is selected, otherwise spell name
plus two newlines plus description. Normal rows use state 1 and every row with
the selected spell ID uses Highlight state 6.
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.