Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
277 lines
11 KiB
Markdown
277 lines
11 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.
|
|
|
|
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
|
|
```
|
|
|
|
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.
|
|
|
|
## 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.
|