feat: port retail magic lifecycle and retained spell UI
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>
This commit is contained in:
parent
7b7ffcd278
commit
07be994d97
84 changed files with 17822 additions and 1051 deletions
|
|
@ -0,0 +1,277 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue