Compare commits

...
8 Commits
Author SHA1 Message Date
Brues ead52e79ce fix(editbox): caret, selection and click rows follow tall inline icons
A multi-line edit box places its caret, selection pieces and a click's
cursor by rows of one height (font height + spacing, pixel-aligned), while
the render moves a line holding an icon taller than the font, and every
line after it, down by the overflow, and the box grows to match. Below the
first tall-icon line the caret and highlight sat above their text, and
OnCursorChanged reported that y, so ScrollingEditBoxMixin's scroll-into-
view stopped short (pfUI chat copy with faction emblems).

4.3.4's CSimpleEditBox has the same fault: UpdateVisibleCursor,
UpdateHighlightArea and ConvertCoordinateToIndex are icon-blind too. So
this goes past 4.3.4, as editbox/CharClass does. EditBox::IconRows co-hooks
the three placers and gives each the render's move, from the new
Text::InlineTexture::LineIconShift over the box's own line table: the
click's y is mapped onto the uniform row before the engine reads it; the
selection piece and caret are re-anchored after, with the engine's own x;
the caret's OnCursorChanged is muted for the original call and fired once
with the corrected y through the engine's forwarder, with the same packing
PlaceCaret uses (verified in the disassembly and the binary's constants).

Also fixes the "caret positioner" misnomer for 0x77DE70 (the selection-
piece placer) in Offsets.h and the docs.
2026-10-30 01:15:01 -05:00
Brues 7cb6cda101 feat(smf): TOP insert mode
SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP) now shows the
newest message at the top, older ones below it, as retail's mixin does.

1.12's line layout anchors each line once, when it makes it, through one
SetPoint call (0x00788975). That call is redirected: lines of a TOP frame
take the TOP anchors, every other line keeps the engine's own arguments.
The layout remakes the lines when the frame resizes, so the redirect
covers that too. A mode change re-anchors the existing lines with the
layout's own steps and refreshes the display.

Insert mode moves out of Scroll.cpp into InsertMode.cpp.
2026-10-30 00:20:19 -05:00
Brues 5ab497f2c7 feat(smf): scroll offset, insert mode, display-refreshed callbacks
Retail's ScrollingMessageFrame SetScrollOffset, GetScrollOffset,
GetMaxScrollRange, ScrollByAmount, SetInsertMode / GetInsertMode with the
SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals, and
AddOnDisplayRefreshedCallback, which ScrollUtil's
InitScrollingMessageFrameWithScrollBar calls.

The offset is the engine's own: SetScrollFromBottom's body (0x00788700)
is 4.3.4's SetScrollOffset step for step, and GetCurrentScroll is
GetScrollOffset under retail's name. The setters clamp to
0..GetNumMessages()-1 first, as retail does; the engine wraps. 1.12's
layout stacks lines bottom-up only, so the insert mode is BOTTOM and
SetInsertMode raises an error for anything else.

The callbacks run after the frame's own update (0x00788460, OnUpdate then
the fader), where retail's OnPostUpdate runs them, for a frame the line
layout, AddMessage or ScrollByAmount marked. The layout hook moves out of
Text::InlineTexture into ScrollingMessageFrame::Layout, which owns it and
fans out to both.
2026-10-29 23:56:24 -05:00
Brues 205d1fd43b feat(smf): ScrollingMessageFrame GetMessageInfo
Retail's smf:GetMessageInfo(index): text, r, g, b, then every argument
AddMessage took after the colour. Index 1 is the oldest message, and an
index outside 1..GetNumMessages() returns nothing.

The text and colour come from the engine's record in the frame's message
ring, so UpdateColorByID's changes read back. The record keeps only an
integer id after the colour, so AddMessage is registered again over the
engine's: Storm's hash links a new method node at the head of its bucket
and the lookup takes the first match. It runs the engine's AddMessage,
then keeps the extra arguments for the record just written, in a
registry table reused per record.
2026-10-29 23:43:45 -05:00
Brues 950c9deb76 feat(death): C_DeathInfo self-resurrect options, SELF_RES_SPELL_CHANGED
C_DeathInfo.GetSelfResurrectOptions, C_DeathInfo.UseSelfResurrectOption
and Enum.SelfResurrectOptionType over what 1.12 already serves through
HasSoulstone and UseSoulstone. Both pick one option the same way:
PLAYER_SELF_RES_SPELL when the server set it, else the first carried
item whose on-use spell is a self-resurrect. The list holds that option
under HasSoulstone's alive gate, and using it calls the engine's
UseSoulstone. Nothing in 1.12 limits a self-resurrection, so canUse is
true, isLimited false and priority 0.

SELF_RES_SPELL_CHANGED is 4.3.4's watcher on PLAYER_SELF_RES_SPELL from
SetActiveMirrorHandlers. 1.12 watches no such field, so the watcher joins
CGPlayer_C::AddActiveWatchers and leaves before its removal, the lifetime
of the engine's own player watchers.
2026-10-29 20:37:40 -05:00
Brues e7ccc11309 feat(unit): UnitHasIncomingResurrection, INCOMING_RESURRECT_CHANGED
4.3.4's CGIncomingResurrection. SMSG_SPELL_START adds a resurrect cast
under its object target, a unit or a corpse. SMSG_SPELL_GO, the failure
packets and ClearCastingSpell (through Spell::Cast, Turtle's only
interrupt signal) end it. A cast whose caster leaves view never reports
its end, so it ends on the next world tick. Each change fires the event
for every token naming the target, or the corpse's owner. 1.12's class
resurrections carry effect 113, not 4.3.4's 18, so both count.

Object::Corpse::ByOwner, lifted from spell/Range.cpp, now skips bones as
4.3.4's ClntObjMgrFindCorpseByOwner does. Turtle's bones keep their
owner, so old bones hid a ghost's live corpse, from this API and from
the range check's ghost redirect.

HasEffect moves from CombatLog to Spell::Lookup, and the SpellCastTargets
object mask 0x8A02 becomes one Offsets.h constant.
2026-10-29 19:21:47 -05:00
Brues bab31b5f7b docs(unit): bound the GetShapeshiftForm example to the bar
GetShapeshiftForm returns the bar count + 1 for a form that is not on
the stance bar, as 3.3.5 and 4.3.4 do. The example passed that index to
GetShapeshiftFormInfo, which returns nils for it.
2026-10-28 23:31:49 -05:00
Brues e0e24bab34 feat(macro): SetMacroDisplay range unit
C_Macro.SetMacroDisplay(macroSlot, value [, unit]) takes the unit the
button's range check uses, so a publisher's [@unit] reaches the range
check as SetMacroSpell's target does. The token stays a token: each
lookup resolves it again, so the range follows a new mouseover, focus or
target with no republish. A token that names no unit leaves the target
in charge.

The engine's resolver vets the unit at publish and raises its "Unknown
unit name" error for anything else, as UnitExists does. It tells a
token's family from its spelling alone, so a token that passes once
never raises at a later lookup. The unit is ignored for false and for an
empty value, and a publish that changes only the unit updates it.
2026-10-28 23:19:36 -05:00
31 changed files with 2252 additions and 85 deletions
+6 -2
View File
@@ -71,6 +71,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
@@ -85,7 +86,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
@@ -133,7 +134,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
@@ -279,6 +280,7 @@ when launching with `-console`), not as Lua functions. See the
| `GLOBAL_MOUSE_DOWN` | `button` |
| `GLOBAL_MOUSE_UP` | `button` |
| `HEARTHSTONE_BOUND` | *(none)* |
| `INCOMING_RESURRECT_CHANGED` | `unitTarget` |
| `INVENTORY_SEARCH_UPDATE` | *(none)* |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
@@ -312,6 +314,7 @@ when launching with `-console`), not as Lua functions. See the
| `QUEST_QUERY_COMPLETE` | *(none)* (Turtle WoW only) |
| `QUEST_REMOVED` | `questID` |
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
| `SELF_RES_SPELL_CHANGED` | *(none)* |
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
@@ -359,6 +362,7 @@ when launching with `-console`), not as Lua functions. See the
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
</details>
+8 -3
View File
@@ -4081,9 +4081,14 @@ Each entry still needs the usual check of how the engine does it before building
`GetRotatesTexture` (`CSimpleStatusBar.cpp:135`, the vertical texcoord path
at `:285`). Retail adds `SetFillStyle` (STANDARD / CENTER / REVERSE),
`SetColorFill`, `SetStatusBarDesaturated` / `Desaturation`.
- **ScrollingMessageFrame** `SetInsertMode` / `GetInsertMode`,
`GetMessageInfo`, `RemoveMessagesByAccessID`, `RemoveMessagesByExtraData`,
`SetScrollOffset` — in C in 4.3.4 (retail moved the type to Lua).
- **DONE** (`src/scrollingmessageframe/`, retail's shapes).
**ScrollingMessageFrame** `GetMessageInfo`, `SetScrollOffset` (1.12's
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
through a redirect of the layout's one line-anchor call, 0x00788975).
- **ScrollingMessageFrame** `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData` (4.3.4 C; retail
has `RemoveMessagesByPredicate`): 1.12 has no ring compaction.
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
+247 -9
View File
@@ -182,6 +182,10 @@ build instructions.
- [`GetCursorInfo()`](#getcursorinfo)
- [`CursorHasMacro()`](#cursorhasmacro)
- [DeathInfo](#deathinfo)
- [`C_DeathInfo.GetSelfResurrectOptions()`](#c_deathinfogetselfresurrectoptions)
- [`C_DeathInfo.UseSelfResurrectOption(optionType, id)`](#c_deathinfouseselfresurrectoptionoptiontype-id)
- [EquipmentSet](#equipmentset)
- [Overview & file format](#overview--file-format)
- [`C_EquipmentSet.CanUseEquipmentSets()`](#c_equipmentsetcanuseequipmentsets)
@@ -223,6 +227,7 @@ build instructions.
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
- [`WEAR_EQUIPMENT_SET` event](#wear_equipment_set-event)
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
- [`INCOMING_RESURRECT_CHANGED` event](#incoming_resurrect_changed-event)
- [`INVENTORY_SEARCH_UPDATE` event](#inventory_search_update-event)
- [`LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
- [`LEARNED_SPELL_IN_SKILL_LINE` event](#learned_spell_in_skill_line-event)
@@ -240,6 +245,7 @@ build instructions.
- [`QUEST_QUERY_COMPLETE` event](#quest_query_complete-event)
- [`QUEST_REMOVED` event](#quest_removed-event)
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
- [`SELF_RES_SPELL_CHANGED` event](#self_res_spell_changed-event)
- [`SOUNDKIT_FINISHED` event](#soundkit_finished-event)
- [`UNIT_AURA_DELTA` event](#unit_aura_delta-event)
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
@@ -379,6 +385,10 @@ build instructions.
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
- [Script binding types](#script-binding-types)
- [`smf:SetHyperlinksEnabled(enabled)` / `smf:GetHyperlinksEnabled()`](#smfsethyperlinksenabledenabled--smfgethyperlinksenabled)
- [`smf:GetMessageInfo(index)`](#smfgetmessageinfoindex)
- [`smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`](#smfsetscrolloffsetoffset--smfgetscrolloffset--smfgetmaxscrollrange--smfscrollbyamountamount)
- [`smf:SetInsertMode(mode)` / `smf:GetInsertMode()`](#smfsetinsertmodemode--smfgetinsertmode)
- [`smf:AddOnDisplayRefreshedCallback(callback)`](#smfaddondisplayrefreshedcallbackcallback)
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
- [`frame:RegisterUnitEvent(event, ...units)`](#frameregisteruniteventevent-units)
- [`frame:GetEffectiveAlpha()`](#framegeteffectivealpha)
@@ -644,7 +654,7 @@ build instructions.
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
- [`C_Macro.CreateMacro` / `C_Macro.EditMacro`](#c_macrocreatemacro--c_macroeditmacro)
- [`C_Macro.GetMacroIcon(macroSlot)`](#c_macrogetmacroiconmacroslot)
- [`C_Macro.SetMacroDisplay(macroSlot, value)`](#c_macrosetmacrodisplaymacroslot-value)
- [`C_Macro.SetMacroDisplay(macroSlot, value [, unit])`](#c_macrosetmacrodisplaymacroslot-value--unit)
- [Mail](#mail)
- [`GetSendMailItemLink([attachmentIndex])`](#getsendmailitemlinkattachmentindex)
@@ -1011,6 +1021,7 @@ build instructions.
- [`UnitPowerType(unit)`](#unitpowertypeunit)
- [`UnitSpellHaste(unit)`](#unitspellhasteunit)
- [`UnitSpellTargetName(unit)`](#unitspelltargetnameunit)
- [`UnitHasIncomingResurrection(unit)`](#unithasincomingresurrectionunit)
- [`CanBeRaidTarget(unit)`](#canberaidtargetunit)
- [UnitAuras](#unitauras)
@@ -4357,6 +4368,57 @@ Changes to what the cursor holds are reported by the
Returns `true` if the cursor holds a macro, and `false` if it does not.
## DeathInfo
### `C_DeathInfo.GetSelfResurrectOptions()`
```
options = C_DeathInfo.GetSelfResurrectOptions()
```
Returns an array of the ways that the dead player can resurrect themselves.
Each entry is a `SelfResurrectOption` table:
| Field | Type | Description |
|---|---|---|
| `name` | string | The name of the spell or the item. |
| `optionType` | number | `Enum.SelfResurrectOptionType.Spell` (0) or `Enum.SelfResurrectOptionType.Item` (1). |
| `id` | number | The spell ID or the item ID. |
| `canUse` | boolean | Always `true`. |
| `isLimited` | boolean | Always `false`. |
| `priority` | number | Always `0`. |
The array has one entry at most. A self-resurrection spell, such as
Soulstone Resurrection or Reincarnation, comes before an item. The array is
empty when the player is alive or a ghost, or has no option. The function
returns nothing before the player enters the world.
```lua
local options = C_DeathInfo.GetSelfResurrectOptions()
if options and options[1] then
print("You can resurrect with", options[1].name)
end
```
[`SELF_RES_SPELL_CHANGED`](#self_res_spell_changed-event) fires when the
self-resurrection spell changes.
### `C_DeathInfo.UseSelfResurrectOption(optionType, id)`
Resurrects the dead player with an option from
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions).
`optionType` and `id` must match an entry that it returns now. If they do
not match, the call does nothing. An item option uses the item, and this
stops a spell cast in progress.
```lua
local options = C_DeathInfo.GetSelfResurrectOptions()
local option = options and options[1]
if option then
C_DeathInfo.UseSelfResurrectOption(option.optionType, option.id)
end
```
## EquipmentSet
`C_EquipmentSet.*` — named sets of gear you can save and equip again
@@ -5297,6 +5359,25 @@ forwarding, which [`C_Reputation.GetLastStandingChange`](#c_reputationgetlaststa
exposes — so addons can read the structured payload from inside the
chat event without re-parsing the localized string.
### `INCOMING_RESURRECT_CHANGED` event
Fires when a resurrection cast at a unit starts or stops. `arg1` is the unit
token. The event gives no value.
[`UnitHasIncomingResurrection`](#unithasincomingresurrectionunit) gives the
new value.
The event fires one time for each token that names the unit, for example
`"party1"` and `"target"`. If the cast is at the corpse of a released player,
`arg1` names that player.
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("INCOMING_RESURRECT_CHANGED")
f:SetScript("OnEvent", function()
print(arg1, UnitHasIncomingResurrection(arg1))
end)
```
### `INVENTORY_SEARCH_UPDATE` event
Fires with no payload when
@@ -5762,6 +5843,14 @@ quest invalidated, etc.). Only fires on a real successful turn-in.
> done, quest removed from log). Hooking the packet handler gives
> us a clean turn-in signal.
### `SELF_RES_SPELL_CHANGED` event
Fires with no payload when the self-resurrection spell of the player
changes. For example, it fires when the player dies with a Soulstone, and
again when the player comes back to life. When it fires, read
[`C_DeathInfo.GetSelfResurrectOptions`](#c_deathinfogetselfresurrectoptions)
again. A change to an item option does not fire this event.
### `SOUNDKIT_FINISHED` event
Fires when a sound finishes playing.
@@ -8578,6 +8667,103 @@ ChatFrame1:SetHyperlinksEnabled(false) -- links in this chat window stop react
ChatFrame1:GetHyperlinksEnabled() -- false
```
### `smf:GetMessageInfo(index)`
On ScrollingMessageFrame. Returns one message of the frame: its text, its
color, and the extra values that `AddMessage` got for the message. Index 1 is
the oldest message, and `smf:GetNumMessages()` is the index of the newest
message. If no message has the index, the function returns nothing. If
`index` is not a number, the function gives an error.
- `text` is the text that the frame keeps, with its color codes.
- `r`, `g` and `b` are the color that the frame shows for the message. After
`UpdateColorByID` changes the color, these values also change. The frame
keeps each color value in steps of 1/255, so `0.5` returns as `0.50196…`.
- `smf:AddMessage(text, r, g, b, ...)` keeps all the values after `b`.
`GetMessageInfo` returns these values in the same order, nil values
included.
An addon that replaces `AddMessage` and gives only `text, r, g, b` to the
original function removes the extra values.
```lua
ChatFrame1:AddMessage("hello", 1, 0.5, 0, 7, "extra")
ChatFrame1:GetMessageInfo(ChatFrame1:GetNumMessages())
-- "hello", 1, 0.50196078431373, 0, 7, "extra"
for i = 1, ChatFrame1:GetNumMessages() do
local text, r, g, b = ChatFrame1:GetMessageInfo(i) -- oldest message first
end
```
### `smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`
On ScrollingMessageFrame. The scroll offset is the number of lines that the
frame is scrolled away from the newest message. When the frame is not
scrolled, the offset is 0. The largest offset is `GetMaxScrollRange()`, that
is `GetNumMessages() - 1`, or 0 for an empty frame. At that offset, the oldest
message is the bottom line, or the top line in TOP insert mode.
- `SetScrollOffset(offset)` scrolls to `offset`. An offset less than 0 or
more than `GetMaxScrollRange()` changes to the nearest limit.
- `ScrollByAmount(amount)` adds `amount` to the offset. A positive amount
scrolls to older messages. The function also shows the faded lines
again, also when the offset does not change.
- If `offset` or `amount` is not a number, the function gives an error.
```lua
ChatFrame1:ScrollByAmount(3) -- 3 lines up
ChatFrame1:GetScrollOffset() -- 3
ChatFrame1:SetScrollOffset(ChatFrame1:GetMaxScrollRange()) -- the oldest message
ChatFrame1:SetScrollOffset(0) -- the newest message again
```
### `smf:SetInsertMode(mode)` / `smf:GetInsertMode()`
On ScrollingMessageFrame. The insert mode tells where the newest message
shows.
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM` (2) shows the newest message
at the bottom, with older messages above it. This is the default.
- `SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP` (1) shows the newest message at
the top, with older messages below it.
In both modes, the scroll offset counts lines from the newest message. A
change of mode moves the lines that show and runs the display-refreshed
callbacks. If `mode` is the current mode, `SetInsertMode` does nothing. If
`mode` is not one of the two values, the function gives an error.
```lua
local f = CreateFrame("ScrollingMessageFrame", nil, UIParent)
f:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP)
f:AddMessage("first")
f:AddMessage("second") -- shows above "first"
f:GetInsertMode() -- 1
```
### `smf:AddOnDisplayRefreshedCallback(callback)`
On ScrollingMessageFrame. Adds a function that the frame calls as
`callback(frame)` after it refreshes the messages that it shows. The frame
refreshes after a new message, a scroll, a resize, a font change, or
`Clear()`.
- The callbacks run one time, in the next update of the frame, in the order
that you added them.
- A hidden frame runs its callbacks when it shows again.
- An error in one callback goes to the error handler, and the other callbacks
still run.
- A change that a callback makes to the frame does not run the callbacks
again.
- If `callback` is not a function, the method gives an error. You cannot
remove a callback.
```lua
ChatFrame1:AddOnDisplayRefreshedCallback(function(frame)
MyScrollLabel:SetText(frame:GetScrollOffset() .. " / " .. frame:GetMaxScrollRange())
end)
```
### `frame:IsEventRegistered(event)`
Returns `true` if the frame is registered for `event`, else `false` (also
@@ -14968,7 +15154,7 @@ to.
If SuperCleveRoidMacros is loaded, it controls macro display, and ClassicAPI
does not evaluate `#showtooltip`. A build that hands its own results over
through
[`C_Macro.SetMacroDisplay`](#c_macrosetmacrodisplaymacroslot-value) keeps
[`C_Macro.SetMacroDisplay`](#c_macrosetmacrodisplaymacroslot-value--unit) keeps
`#showtooltip` working for the macros it does not claim.
A macro that names a condition
@@ -15342,7 +15528,7 @@ mark, and the macro's own icon otherwise.
selector edits. Use this function when you want the icon a macro shows, and
`GetMacroInfo` when you want the one the player picked.
### `C_Macro.SetMacroDisplay(macroSlot, value)`
### `C_Macro.SetMacroDisplay(macroSlot, value [, unit])`
Tells the client what a macro is about, so the action button shows it. For
an addon that parses macros itself and wants the button to follow, instead
@@ -15350,9 +15536,10 @@ of replacing `GetActionTexture`, `GetActionCooldown`, `IsUsableAction` and
the rest in Lua.
```lua
C_Macro.SetMacroDisplay(1, "Frostbolt") -- show this
C_Macro.SetMacroDisplay(1, false) -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil) -- released, `#showtooltip` resumes
C_Macro.SetMacroDisplay(1, "Frostbolt") -- show this
C_Macro.SetMacroDisplay(1, "Flash Heal", "mouseover") -- show this, range to the mouseover
C_Macro.SetMacroDisplay(1, false) -- mine, nothing matched, show ?
C_Macro.SetMacroDisplay(1, nil) -- released, `#showtooltip` resumes
```
`macroSlot` is the macro index, the same one
@@ -15366,6 +15553,31 @@ it, the same as a directive that names its own value.
Returns `true` when the value named a spell or an item.
**The range unit.** `unit` is optional. It names the unit that the button's
range check uses in place of your target. Usually it is the `[@unit]` of the
clause that your parser matched. It takes any unit token or GUID string that
`UnitExists` takes, for example `"mouseover"`, `"focus"` or
`"party2target"`. If you give no `unit`, or `nil`, the range check uses your
target.
The client finds the unit again each time it checks the button's range. So
if `"mouseover"` moves to a different unit, the range follows it with no new
call. The same is true for `"focus"`, `"target"` and every other token. This
is different from the `target` of
[`SetMacroSpell`](#setmacrospellmacro-spell--target--setmacroitemmacro-item--target),
which is found once, when you call it.
If the token points at no unit at that time, for example `"mouseover"` with
nothing under the cursor, the range check uses your target. If the unit is
too far away for the client to see it, for example a party member in another
zone, the button has no range state.
The function ignores `unit` when `value` is `false`, `nil` or an empty
string, because the macro then aims at nothing. A call with the same `value`
and a different `unit` changes the unit. If `unit` is not a unit token, the
function raises the error `Unknown unit name: <unit>`, the same error that
`UnitExists` gives. Then it publishes nothing.
**What the button picks up.** The spell is written where the client keeps
each macro's spell, which is the field its own buttons read. So cooldown,
range, usable, out-of-mana greying, the current-cast highlight and
@@ -15379,8 +15591,8 @@ button, on the cursor while you drag the macro, and in the macro window.
The icon replaces the macro's own only when that is the question mark, which
is the same rule `#showtooltip` follows. A macro with a chosen icon keeps it.
**Call it whenever your answer changes.** Nothing is re-evaluated for you.
The value stands until you publish another, and it survives the client
**Call it whenever your answer changes.** Nothing is re-evaluated for you,
except the unit that a `unit` token points at. The value stands until you publish another, and it survives the client
re-reading the macro, so an edit elsewhere cannot quietly replace it.
Publishing takes the macro over: `#showtooltip` is not parsed for it while
@@ -20560,7 +20772,7 @@ is the number that `[stance:N]` and `[form:N]` test.
```lua
local index = GetShapeshiftForm()
if index > 0 then
if index > 0 and index <= GetNumShapeshiftForms() then
local icon, name = GetShapeshiftFormInfo(index)
print("active:", name)
end
@@ -23142,6 +23354,32 @@ The player's target lands with the confirming packet, about one round-trip
after the cast starts. So `UnitSpellTargetName("player")` can read `nil` for
the first part of your own cast, then return the name.
### `UnitHasIncomingResurrection(unit)`
```
hasIncomingResurrection = UnitHasIncomingResurrection(unit)
```
Returns true while somebody casts a resurrection spell at `unit`. Otherwise
it returns false. If `unit` is a released player, the function also includes
casts at the corpse of that player.
```lua
if UnitHasIncomingResurrection("party1") then
print(UnitName("party1"), "has an incoming resurrection")
end
```
The value changes to true when the cast starts. It changes to false when the
cast stops, for any reason. This includes a completed cast that the target did
not accept yet.
You see a cast only when its caster is near you. If the caster goes out of
range during the cast, the value changes to false.
[`INCOMING_RESURRECT_CHANGED`](#incoming_resurrect_changed-event) fires at
each change.
### `CanBeRaidTarget(unit)`
Returns true if you can put a raid target icon on the unit. Otherwise it
+22 -4
View File
@@ -86,9 +86,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
(`49934b0`, `b9d0705`).
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
engine's own input escape (below).
the mouse and the arrow keys treat an icon as one character, Ctrl+C
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
selection and click rows follow the lines a tall icon moves (below;
4.3.4's own rows do not). Typed markup stays literal by the engine's own
input escape (below).
- **Payload** — the full
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
@@ -377,7 +379,7 @@ funnel for exactly 4 callers:
- `Script_GetStringWidth` (0x0079E510)
- the GameTooltip auto-size (0x00530640)
- the editbox caret positioner (0x0077DE70)
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
which feeds auto-width fontstring layout)
@@ -523,6 +525,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
icon with the following character into one step; ours is its own step.
- **Caret x** — `FUN_0077DA80` measures the line through
`FUN_00772AE0`, whose co-hook adds the icons.
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
moves a tall-icon line and every line after it down by the overflow
(`InitializeText`; the emitter co-hook) and the box grows to match (the
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
`Text::InlineTexture::LineIconShift` over the box's own line table: the
click's y is moved onto the uniform row of the icon-aware row under it
before the engine maps it; a selection piece is re-anchored after the
engine placed it, with the engine's own x; the caret likewise, then
`OnCursorChanged` fired once with the moved y through
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
fire muted for the call by emptying the slot), so
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
`RebuildLabel` and `MeasureWindow` count characters with
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
+138 -2
View File
@@ -988,7 +988,8 @@ enum Offsets {
// message. Every new message shifts the newest index, so EVERY visible slot
// is re-SetText'd -> its inline icons must be recomputed; the engine builds
// the line lazily at paint, so icons land one frame after the glyphs (the
// one-frame lag). Text::InlineTexture co-hooks this and, post-refresh,
// one-frame lag). ScrollingMessageFrame::Layout owns the hook and fans
// out after it; Text::InlineTexture listens and, post-refresh,
// forces each icon-bearing line to resolve+build and applies its icon
// placement synchronously — pre-render (the refresh runs from the SMF's
// Lua/event-driven methods, never mid-render), so icons draw with their
@@ -2425,6 +2426,16 @@ enum Offsets {
// right after that same unlock — Player::Equipment co-hooks it to do so.
FUN_INV_SLOT_CHANGED_OBSERVER = 0x005DDCF0,
// CGPlayer_C::AddActiveWatchers / its removal — `void __thiscall
// (CGPlayer_C *)`, plain `ret`. Register and remove the active player's
// bank-PLAYER observers (inventory, quests, coinage, skills, the
// 0x005DE4B0 event fields); 4.3.4's SetActiveMirrorHandlers (0x0056E430)
// / UnsetActiveMirrorHandlers. Add runs on arrival (0x005DEA58) and
// reactivation (0x005E9C00); remove only at 0x005E9B50, for the active
// player.
FUN_PLAYER_ADD_ACTIVE_WATCHERS = 0x005DD8A0,
FUN_PLAYER_REMOVE_ACTIVE_WATCHERS = 0x005DE5B0,
// PLAYER_FIELD_INV_SLOT_HEAD — first of the 19 equipment-slot u64 GUID
// fields in the player descriptor (0x4A8..0x538, stride 8; 0x540 is
// the first equipped-bag slot). 0-based slot = (offset - 0x4A8) >> 3;
@@ -3396,6 +3407,13 @@ enum Offsets {
SMSG_SPELL_CHANNEL_UPDATE = 0x13A,
SMSG_SPELL_DELAYED = 0x1E2,
SMSG_SPELL_FAILED_OTHER = 0x2A6,
// SpellCastTargets mask bits that carry a packed object GUID right after
// the u16 mask in SMSG_SPELL_START / SMSG_SPELL_GO: unit 0x2, PvP corpse
// 0x200, game object 0x800, corpse 0x8000 — one GUID for whichever is
// set (the client's target reader 0x007E4ED0; tortoise-wow
// SpellCastTargets::write). A resurrect aimed at a released player
// carries the corpse.
SPELL_TARGET_FLAGS_OBJECT = 0x8A02,
// Melee swing result broadcast. Body (verified against the server's own
// writer, tortoise-wow Unit::SendAttackStateUpdate): u32 hitInfo,
// packedGuid attacker, packedGuid victim, u32 totalDamage, u8 subCount,
@@ -4575,6 +4593,12 @@ enum Offsets {
// (see OFF_UNIT_FIELD_CHANNEL_SPELL below); PLAYER_FLAGS only
// exists on the +0xE68 sub-struct here.
OFF_CGPLAYER_INFO = 0xE68,
// PLAYER_SELF_RES_SPELL in that block (UpdateField table: player index
// 0x40C, owner-only): the spell the server offers a dead player to raise
// themselves (Soulstone Resurrection, Reincarnation), 0 for none. Also
// its offset in observer bank DESC_OBSERVER_BANK_PLAYER. Read by
// HasSoulstone (0x0048ACC6) and UseSoulstone (0x0048ADB6).
OFF_CGPLAYER_INFO_SELF_RES_SPELL = 0x1030,
// The engine's explored-area bit test (benilla `IsAreaExplored`):
// __thiscall(this = player + OFF_CGPLAYER_INFO, int areaBit) → nonzero
// when explored, ret 4. Reads `[this] + 0xE6C` (the 0x100-byte
@@ -4986,6 +5010,16 @@ enum Offsets {
// target is passed, so passing zero (no target) is fine for most
// items. `flag` is 0 in every Script_UseContainerItem call site.
FUN_ITEM_USE = 0x005D8D00,
// The player's self-resurrect item — `CGItem_C *__thiscall(CGPlayer_C *)`,
// plain `ret`: the first carried item (equipment, bags, backpack,
// keyring) with an on-use spell of effect 94 (SELF_RESURRECT), or null.
// HasSoulstone and UseSoulstone ask it when PLAYER_SELF_RES_SPELL is 0.
FUN_PLAYER_SELF_RES_ITEM = 0x005ED630,
// Script_UseSoulstone — the Lua C function itself, `int __fastcall(L)`;
// reads no argument and pushes nothing. Sends CMSG_SELF_RES (0x2B3, no
// body) when PLAYER_SELF_RES_SPELL is set, else cancels the cast
// (0x006E4940(1, 1, 0x1C)) and uses FUN_PLAYER_SELF_RES_ITEM's item.
FUN_SCRIPT_USE_SOULSTONE = 0x0048AD70,
// `CGPlayer::AutoEquipCursorItem` — `__thiscall(CGPlayer *this, int flag)`.
// The engine-internal helper that `Script_AutoEquipCursorItem`
// (`0x0048A040`) is a thin wrapper around: that wrapper just
@@ -12804,6 +12838,62 @@ enum Offsets {
OFF_SIMPLEHTML_FONTS = 0x350,
VAR_SIMPLEHTML_LUA_TYPE_ID = 0x00CF4F64,
FUN_SIMPLEHTML_READ_TEXT_TYPE = 0x00795D80,
// The ScrollingMessageFrame's message ring. GetNumMessages (0x007934D0)
// pushes the count. The ring's one writer, 0x00788150 (`__thiscall(smf,
// text, const CImVector *color, int id)`, `ret 0xC`), called only by the
// AddMessage method (FUN_SCRIPT_..._ADD_MESSAGE, `int __fastcall(L)`),
// raises the count up to the limit, advances the position (wrapping at
// the limit) and fills the record there. So message i, 1 the oldest, is
// record (position - count + i), plus the limit when negative. Records
// are 0x18 bytes: the text (an SStrDupA copy), the colour (a CImVector,
// the dword 0xAARRGGBB: AddMessage builds it R, G, B with alpha 0xFF at
// 0x00792ADD..0x00792B11), shown +0x8, the visible and fade times +0xC and
// +0x10, and the integer id +0x14 (benilla-re ui/messageframe).
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE = 0x00792900,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT = 0x33C,
OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES = 0x340,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS = 0x344, // the newest record; -1 when empty
OFF_SCROLLINGMESSAGEFRAME_MESSAGES = 0x394, // the record array
SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE = 0x18,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT = 0x0,
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR = 0x4,
// The frame's scroll in retail's units, the lines it is scrolled up from
// the newest message. ScrollToOffset (`void __thiscall(smf, int offset)`,
// `ret 4`) is the body of SetScrollFromBottom (0x00793070), 4.3.4's
// SetScrollOffset (0x0051E6F0) step for step: the record (position -
// offset + count) % count, then it re-arms the shown lines (0x00788B80)
// when that is the current scroll and lays the lines out from it
// (FUN_SMF_DISPLAY_REFRESH) otherwise. ScrollOffset (`int
// __thiscall(smf)`) is the inverse, (position - scroll + count) % count,
// 0 when empty; the GetCurrentScroll method pushes it.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
// The frame's OnFrameUpdate override (vtable+0x38, see
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
// FUN_FRAME_ON_UPDATE (OnUpdate), then the fader.
FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE = 0x00788460,
// The line layout (FUN_SMF_DISPLAY_REFRESH) anchors a line once, when it
// creates the line's font string, through the one SetPoint call at
// CALL_SMF_LAYOUT_ANCHOR_LINE (`E8 rel32` to FUN_REGION_SET_POINT; ESI the
// frame, EDI the font string, its parent at OFF_REGION_PARENT already set
// by SetRegionParent 0x0077FD10): the first line (BOTTOMLEFT, the frame,
// BOTTOMLEFT, left inset, bottom inset), each next one (BOTTOMLEFT, the
// line before, TOPLEFT, 0, FUN_PIXEL_ALIGN_Y(spacing)), then
// FUN_LAYOUT_RESIZE(1). The layout is the only maker of lines (the array
// grow 0x00789060 and the node constructor 0x00788D90 have no other
// caller). OnRectChanged (0x00788310) destroys every line when the message
// area changes size and lays them out again; Clear (0x007882B0) only hides
// them. The insets are the four floats SetInsets (0x00787FB0) stores and
// OnRectChanged subtracts from the rect: bottom, left, top, right.
CALL_SMF_LAYOUT_ANCHOR_LINE = 0x00788975,
OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM = 0x37C,
OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT = 0x380,
OFF_SCROLLINGMESSAGEFRAME_INSET_TOP = 0x384,
OFF_FONTSTRING_SPACING = 0xF4, // float, the line spacing
// PixelAlignY - `float __stdcall(float)`, `ret 4`: rounds a vertical
// length up to whole device pixels (benilla-re ui/regions).
FUN_PIXEL_ALIGN_Y = 0x00766750,
// Focused-editbox global — holds the CSimpleEditBox that currently has
// keyboard focus (the one showing a cursor), or 0 when no input field is
@@ -12972,6 +13062,51 @@ enum Offsets {
// ending at 0x0077D49D.
PATCH_EDITBOX_TEXT_CHANGED_FIRE = 0x0077D485,
OFF_EDITBOX_ONTEXTCHANGED_SLOT = 0x418,
// --- Edit box rows (EditBox::IconRows) ---
// The mode flags byte (benilla-re ui/editbox): bit 0 autoFocus, 1
// multiLine, 2 numeric, 3 password, 4 ignoreArrows. Every row placer
// below tests it (`TEST byte ptr [ESI+0x318], 0x2 / 0x8`). Not the dirty
// word OFF_EDITBOX_FLAGS (+0x31c).
OFF_EDITBOX_MODE_FLAGS = 0x318,
EDITBOX_MODE_MULTILINE = 0x02,
EDITBOX_MODE_PASSWORD = 0x08,
// The password copy of the text (every byte '*'), the buffer the label
// and the rows read with EDITBOX_MODE_PASSWORD set (4.3.4's
// m_textHidden); OFF_EDITBOX_TEXT otherwise. RebuildLabel 0x0077D4D0
// refreshes it.
OFF_EDITBOX_TEXT_MASKED = 0x334,
// The visible window in bytes: its first byte and its length (4.3.4's
// m_visiblePosition / m_visibleLength). PlaceCaret hides the caret and
// fires nothing for a cursor outside it (0x0077DA89..0x0077DAA7).
OFF_EDITBOX_VISIBLE_START = 0x348,
OFF_EDITBOX_VISIBLE_LENGTH = 0x34C,
// The caret texture (4.3.4's m_cursorTexture), anchored by PlaceCaret.
OFF_EDITBOX_CARET = 0x368,
// OnCursorChanged's script slot; PlaceCaret fires it when non-zero
// (0x0077DD8C).
OFF_EDITBOX_ONCURSORCHANGED_SLOT = 0x428,
// The three row placers, all `__thiscall`: PlaceCaret (no args; 4.3.4's
// UpdateVisibleCursor), PlaceSelectionPiece `(editbox, texture, from, to)`
// `ret 0xC` (UpdateHighlightArea) and GetIndexAtPoint `(editbox, float x,
// float y, int *index)` `ret 0xC` (ConvertCoordinateToIndex). Each steps
// rows of FUN_PIXEL_ALIGN_Y(fontH × scale) + FUN_PIXEL_ALIGN_Y(spacing ×
// scale), the first two divided by the box's OFF_LAYOUT_SCALE (benilla-re
// ui/editbox "The caret" / "Selection"; Ghidra). Co-hooked by
// EditBox::IconRows: tall inline icons move the rows.
FUN_EDITBOX_PLACE_CARET = 0x0077DA80,
FUN_EDITBOX_PLACE_SELECTION_PIECE = 0x0077DE70,
FUN_EDITBOX_INDEX_AT_POINT = 0x0077D0D0,
// "%f%f%f%f" (.data; verified in the file at 0x479CB8) — the format
// PlaceCaret fires OnCursorChanged with through
// FUN_FRAME_RUN_SCRIPT_VARIADIC: x, y, width, height, each a double of
// FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X(FUN_COORD_GET_SCALED_SCREEN_ASPECT()
// × [FLOAT_SCRIPT_COORD_SCALE] × value) — 4.3.4's
// RunOnCursorChangedScript (0x0077DDA0..0x0077DE20).
VAR_SCRIPT_FMT_FFFF = 0x00879CB8,
// 1024.0f (.rdata; verified in the file at 0x3FFD68): the anchor→script
// push factor every script coordinate push multiplies by (the `1024` of
// the GetFont note at FUN_COORD_GET_SCALED_SCREEN_ASPECT).
FLOAT_SCRIPT_COORD_SCALE = 0x007FFD68,
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
// frees the whole buffer and zeroes the container. ClearHistory calls it
@@ -12989,7 +13124,8 @@ enum Offsets {
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
// (0x00530640), the editbox selection-piece placer (0x0077DE70, a whole
// last line's width), and the layout
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
+8 -10
View File
@@ -42,6 +42,7 @@
#include "combatlog/RecentCasts.h"
#include "net/PacketDispatch.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -54,13 +55,9 @@ using Net::CDataStore;
using Net::Read;
using Net::ReadPackedGuid;
constexpr uint32_t SMSG_SPELL_START = 0x131;
constexpr uint32_t SMSG_SPELL_GO = 0x132;
// The cast flag that keeps a cast out of the combat log (both servers'
// CAST_FLAG_HIDDEN_COMBATLOG); 4.3.4 tests the same bit.
constexpr uint16_t CAST_FLAG_HIDDEN_COMBATLOG = 0x1;
constexpr uint16_t TARGET_FLAG_OBJECT = 0x8A02;
constexpr uint8_t SPELL_MISS_REFLECT = 11;
// 4.3.4's SpellHasOpenLockEffect tests 33 alone; 1.12's success line also
@@ -85,7 +82,7 @@ int CastingTime(void *unit, int spellId) {
// The rules both cast builders share.
bool Logged(const uint8_t *spell) {
return !Hidden(spell) && !HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
return !Hidden(spell) && !Spell::Lookup::HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX4) &
ATTR_EX4_NO_CAST_LOG) == 0;
}
@@ -127,7 +124,8 @@ void SpellGo(CDataStore *p) {
Read<uint8_t>(p);
}
const uint16_t targetMask = Read<uint16_t>(p);
const uint64_t target = (targetMask & TARGET_FLAG_OBJECT) ? ReadPackedGuid(p) : 0;
const uint64_t target =
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? ReadPackedGuid(p) : 0;
if (Overrun(p))
return;
RecentCasts::Record(caster, static_cast<uint32_t>(spellId), hitList, hits);
@@ -135,17 +133,17 @@ void SpellGo(CDataStore *p) {
return;
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, caster);
const uint8_t *spell = SpellRecord(spellId);
if (unit == nullptr || !Logged(spell) || HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) ||
CastingTime(unit, spellId) != 0)
if (unit == nullptr || !Logged(spell) ||
Spell::Lookup::HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) || CastingTime(unit, spellId) != 0)
return;
Entry e = Begin(caster, target, SPELL_CAST_SUCCESS, spell);
Queue(e);
}
void OnPacket(uint32_t opcode, CDataStore *p) {
if (opcode == SMSG_SPELL_START)
if (opcode == Offsets::SMSG_SPELL_START)
SpellStart(p);
else if (opcode == SMSG_SPELL_GO)
else if (opcode == Offsets::SMSG_SPELL_GO)
SpellGo(p);
}
-10
View File
@@ -609,16 +609,6 @@ bool Ranged(const uint8_t *spell) {
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
bool HasEffect(const uint8_t *spell, int effect) {
if (spell == nullptr)
return false;
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT + i * 4) == effect)
return true;
}
return false;
}
bool HasAuraEffect(const uint8_t *spell, int auraName) {
if (spell == nullptr)
return false;
-3
View File
@@ -158,9 +158,6 @@ bool Ranged(const uint8_t *spell);
// A school index (Spell.dbc School, the packets' school) as a school mask.
uint32_t SchoolMask(uint32_t school);
// Whether one of the spell's three effects is `effect`.
bool HasEffect(const uint8_t *spell, int effect);
// Whether one of the spell's three effects applies the aura `auraName`
// (EffectApplyAuraName).
bool HasAuraEffect(const uint8_t *spell, int auraName);
+2 -1
View File
@@ -14,6 +14,7 @@
#include "combatlog/RecentCasts.h"
#include "combatlog/Entry.h"
#include "spell/Lookup.h"
#include "time/Clock.h"
#include <cstdint>
@@ -80,7 +81,7 @@ const uint8_t *SpellByEffect(uint64_t caster, uint64_t target, int effectA, int
if (c.caster != caster || !Hit(c, target))
return false;
const uint8_t *spell = SpellRecord(static_cast<int>(c.spellId));
if (!HasEffect(spell, effectA) && !HasEffect(spell, effectB))
if (!Spell::Lookup::HasEffect(spell, effectA) && !Spell::Lookup::HasEffect(spell, effectB))
return false;
found = spell;
return true;
+218
View File
@@ -0,0 +1,218 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_DeathInfo.GetSelfResurrectOptions()`, `C_DeathInfo.UseSelfResurrectOption
// (optionType, id)` and `SELF_RES_SPELL_CHANGED` — the self-resurrection that
// 1.12 serves through HasSoulstone (0x0048AC80) and UseSoulstone (0x0048AD70).
//
// 1.12 offers at most one way for a dead player to raise themselves, and both
// functions pick it the same way: PLAYER_SELF_RES_SPELL when the server set it
// (Soulstone Resurrection, Reincarnation), else the first carried item whose
// on-use spell is a self-resurrect. So the list holds that one option, under
// HasSoulstone's gate (a living player has none), and using it calls
// UseSoulstone itself. Nothing in 1.12 limits a self-resurrection, so every
// option can be used and none is limited.
//
// SELF_RES_SPELL_CHANGED is 4.3.4's: CGPlayer_C::SetActiveMirrorHandlers
// watches PLAYER_SELF_RES_SPELL with OnUpdatePlayerScriptEvent. 1.12 watches
// no such field, so the same watcher joins 1.12's AddActiveWatchers and
// leaves with its removal, the lifetime of the engine's own player watchers.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "item/Link.h"
#include "spell/Lookup.h"
#include "unit/Identity.h"
#include <cstdint>
namespace Death::SelfResurrect {
namespace {
constexpr int OPTION_TYPE_SPELL = 0;
constexpr int OPTION_TYPE_ITEM = 1;
const Game::Doc::Event kChangedDoc{
"DeathInfo",
"The player's self-resurrection spell changed. Read C_DeathInfo.GetSelfResurrectOptions.",
{}};
const Event::Custom::AutoReserve kChanged{"SELF_RES_SPELL_CHANGED", &kChangedDoc};
struct Option {
const char *name;
int optionType;
int id;
};
// HasSoulstone's answer: the option, or false while the player lives or has
// none. `itemName` holds an item option's name.
bool CurrentOption(Option &out, char *itemName, size_t itemNameSize) {
const uint8_t *player = Unit::Identity::PlayerObject();
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
if (player == nullptr || desc == nullptr ||
Game::Read<int32_t>(desc, Offsets::OFF_UNIT_FIELD_HEALTH) > 0)
return false;
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
const int spellID =
info != nullptr ? Game::Read<int32_t>(info, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL) : 0;
if (spellID != 0) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
out = {rec != nullptr
? Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES +
Game::Read<int>(Offsets::VAR_LOCALE_INDEX) *
4)
: "UNKNOWN",
OPTION_TYPE_SPELL, spellID};
return true;
}
using SelfResItem_t = const uint8_t *(__fastcall *)(const void *player);
const uint8_t *item =
reinterpret_cast<SelfResItem_t>(Offsets::FUN_PLAYER_SELF_RES_ITEM)(player);
if (item == nullptr)
return false;
Item::Link::NameFromCGItem(item, itemName, itemNameSize);
const auto *fields = Game::Read<const uint8_t *>(item, Offsets::OFF_UNIT_GUID_PTR);
out = {itemName, OPTION_TYPE_ITEM,
fields != nullptr ? Game::Read<int32_t>(fields, Offsets::OFF_OBJECT_FIELD_ENTRY) : 0};
return true;
}
int __fastcall Script_GetSelfResurrectOptions(void *L) {
if (Unit::Identity::PlayerObject() == nullptr)
return 0;
Game::Lua::NewTable(L);
char itemName[0x400];
Option option;
if (CurrentOption(option, itemName, sizeof(itemName))) {
Game::Lua::PushNumber(L, 1);
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "name", option.name);
Game::Lua::SetFieldNumber(L, "optionType", option.optionType);
Game::Lua::SetFieldNumber(L, "id", option.id);
Game::Lua::SetFieldBool(L, "canUse", true);
Game::Lua::SetFieldBool(L, "isLimited", false);
Game::Lua::SetFieldNumber(L, "priority", 0);
Game::Lua::SetTable(L, -3);
}
return 1;
}
int __fastcall Script_UseSelfResurrectOption(void *L) {
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: C_DeathInfo.UseSelfResurrectOption(optionType, id)");
return 0;
}
char itemName[0x400];
Option option;
if (CurrentOption(option, itemName, sizeof(itemName)) &&
option.optionType == static_cast<int>(Game::Lua::ToNumber(L, 1)) &&
option.id == static_cast<int>(Game::Lua::ToNumber(L, 2)))
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_USE_SOULSTONE)(L);
return 0;
}
// Observer callback ABI per the FUN_DESC_OBSERVER_REGISTER block in
// Offsets.h: __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
// oldValues*, userArg), returns 1, callee cleans 0x10. It runs only when the
// field's bytes changed.
int __fastcall SelfResSpellChanged(uint32_t /*fieldOffset*/, uint32_t /*size*/,
uint32_t /*guidLo*/, uint32_t /*guidHi*/,
const void * /*oldValues*/, void * /*userArg*/) {
Event::Custom::Fire(kChanged.Slot(), "");
return 1;
}
using Register_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
uint32_t guidHi, int size, const void *callback,
void *userArg1, void *userArg2);
using Unregister_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
uint32_t guidHi, const void *callback, void *userArg1);
using Watchers_t = void(__fastcall *)(void *player, void *edx);
Watchers_t g_origAddWatchers = nullptr;
Watchers_t g_origRemoveWatchers = nullptr;
void __fastcall AddActiveWatchers_h(void *player, void *edx) {
g_origAddWatchers(player, edx);
const uint64_t guid = Unit::Identity::GuidForObject(player);
reinterpret_cast<Register_t>(Offsets::FUN_DESC_OBSERVER_REGISTER)(
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 4,
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr, nullptr);
}
// Removed before the original, while the registry is as the engine's own
// removals find it.
void __fastcall RemoveActiveWatchers_h(void *player, void *edx) {
const uint64_t guid = Unit::Identity::GuidForObject(player);
reinterpret_cast<Unregister_t>(Offsets::FUN_DESC_OBSERVER_UNREGISTER)(
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr);
g_origRemoveWatchers(player, edx);
}
const Game::HookAutoRegister _addHook{Offsets::FUN_PLAYER_ADD_ACTIVE_WATCHERS,
reinterpret_cast<void *>(&AddActiveWatchers_h),
reinterpret_cast<void **>(&g_origAddWatchers)};
const Game::HookAutoRegister _removeHook{Offsets::FUN_PLAYER_REMOVE_ACTIVE_WATCHERS,
reinterpret_cast<void *>(&RemoveActiveWatchers_h),
reinterpret_cast<void **>(&g_origRemoveWatchers)};
const Game::Lua::EnumIntegerEntry kOptionTypeEntries[] = {
{"Spell", OPTION_TYPE_SPELL},
{"Item", OPTION_TYPE_ITEM},
};
const Game::Doc::Field kOptionFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
Game::Doc::Req("id", "number", "The spell ID or item ID."),
Game::Doc::Req("canUse", "bool"),
Game::Doc::Req("isLimited", "bool"),
Game::Doc::Req("priority", "number", "Lower comes first."),
};
const Game::Doc::Structure kOption{"SelfResurrectOption", "DeathInfo", kOptionFields,
"A way for the dead player to resurrect themselves."};
const Game::Doc::Field kGetRets[] = {
Game::Doc::Req("options", "table", "Array of SelfResurrectOption; empty while alive."),
};
const Game::Doc::Function kGetSelfResurrectOptions{
"The ways the dead player can resurrect themselves.", {}, kGetRets};
const Game::Doc::Field kUseArgs[] = {
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
Game::Doc::Req("id", "number"),
};
const Game::Doc::Function kUseSelfResurrectOption{
"Resurrects the dead player with one of the options from GetSelfResurrectOptions.",
kUseArgs, {}};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_DeathInfo", "GetSelfResurrectOptions",
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
"DeathInfo");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Death::SelfResurrect
+308
View File
@@ -0,0 +1,308 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Tall inline icons move the edit box's rows.
//
// A multi-line edit box places its caret, its selection pieces and a click's
// cursor by rows of one height — the label's font height plus its spacing,
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
// those rows once a line holds an icon taller than the font: the render moves
// that line and every line after it down by the overflow (4.3.4's
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
// grows by the same amount through the string-height co-hook. So below the
// first tall-icon line the caret and the highlight sat above their text by
// the summed overflow, and OnCursorChanged reported that y, which
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
// puts a tall icon in an edit box.
//
// Each placer keeps its engine code and gets the move the render makes, from
// Text::InlineTexture::LineIconShift over the box's own line table (the
// breaks the render lays out), split by the label's vertical justify as the
// emitter splits it:
// - the click: its y is moved onto the uniform row that the icon-aware row
// under it has, before the engine maps it;
// - a selection piece: re-anchored after the engine placed it, with the
// engine's own x, lower by the move;
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
// moved y through the engine's own forwarder. The engine's fire is muted
// for the call by emptying the script slot: it reads the row y before
// anything can correct it.
// A single-line box has one row and needs nothing: its label is justified
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
#include "Game.h"
#include "Offsets.h"
#include "text/InlineTexture.h"
#include <cstdint>
namespace EditBox::IconRows {
namespace {
using PlaceCaret_t = void(__fastcall *)(void *editBox);
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
using PixelAlignY_t = float(__stdcall *)(float length);
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
using ScreenAspect_t = float(__cdecl *)();
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
PlaceCaret_t g_origPlaceCaret = nullptr;
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
IndexAtPoint_t g_origIndexAtPoint = nullptr;
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// A multi-line box's rows: its label, the buffer the line table indexes (the
// '*' copy of a password box) and the table — each line's first byte, the
// last entry the end.
struct Rows {
void *fs = nullptr;
const uint8_t *text = nullptr;
const uint32_t *starts = nullptr;
int count = 0; // entries; lines = count - 1
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
};
bool MultiLineRows(void *editBox, Rows &rows) {
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
return false;
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
? Offsets::OFF_EDITBOX_TEXT_MASKED
: Offsets::OFF_EDITBOX_TEXT);
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
rows.scale > 0.0f;
}
// The line holding byte `pos` — the placers' walk down the table.
int LineOf(const Rows &rows, uint32_t pos) {
int line = 0;
while (pos >= rows.starts[line + 1]) {
if (line >= rows.count - 2)
break;
++line;
}
return line;
}
bool LineShift(const Rows &rows, int line, float &before, float &after) {
before = 0.0f;
after = 0.0f;
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
return end > start &&
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
}
// How far the render moves line `line`'s text below its uniform row: the
// moves of every line above it, and its own text's move.
float ShiftOf(const Rows &rows, int line) {
float shift = 0.0f;
for (int i = 0; i <= line; ++i) {
float before = 0.0f;
float after = 0.0f;
if (!LineShift(rows, i, before, after))
continue;
shift += before;
if (i < line)
shift += after;
}
return shift;
}
// The placers' row in the box's parent units: the label's font height and
// spacing, each pixel-aligned at the box's scale (PlaceCaret
// 0x0077DAEB..0x0077DB0E).
float RowHeight(const Rows &rows) {
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
return align(fontH * rows.scale) + align(spacing * rows.scale);
}
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
static_cast<int>(to - from));
}
// The x the placers anchor byte `pos` of `line` at, and the label point they
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
// False for none, which anchors nothing.
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
case 1: // LEFT
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
return true;
case 2: // CENTER
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
x -= MeasureRun(rows, start, end) * 0.5f;
relPoint = Offsets::FRAMEPOINT_TOP;
return true;
case 4: // RIGHT
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
return true;
default:
return false;
}
}
// Anchors `region` as the placer did, at `y`.
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
void *anchor = AnchorOf(region);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
relPoint, x, y, flag);
}
// --- the click ---------------------------------------------------------------
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origIndexAtPoint(editBox, edx, x, y, index);
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
return g_origIndexAtPoint(editBox, edx, x, y, index);
// The engine walks rows down from the label's top while the click is
// below the row (strictly), stopping at the last line. Walk the rows as
// the render lays them out: each taller by its line's moves.
const float row = RowHeight(rows);
const float below = rect[2] - y;
int line = 0;
float top = 0.0f; // the icon-aware top of `line`, below the label's top
bool moved = false;
for (;;) {
float before = 0.0f;
float after = 0.0f;
if (LineShift(rows, line, before, after))
moved = true;
const float height = row + (before + after) * rows.scale;
if (!(below > top + height) || line >= rows.count - 2)
break;
top += height;
++line;
}
// Without a move the rows are the engine's own; with one, the click is
// moved onto the middle of the uniform row the engine maps to `line`.
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
}
// --- a selection piece --------------------------------------------------------
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
Rows rows;
if (!MultiLineRows(editBox, rows) || from < 0)
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
const int line = LineOf(rows, static_cast<uint32_t>(from));
const float shift = ShiftOf(rows, line);
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
if (!(shift > 0.0f) || piece == nullptr)
return;
// The piece starts at `from`, clamped to its line.
uint32_t pos = static_cast<uint32_t>(from);
if (pos < rows.starts[line])
pos = rows.starts[line];
float x = 0.0f;
int relPoint = 0;
if (!AnchorX(rows, line, pos, x, relPoint))
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
Anchor(rows, piece, relPoint, x, y, 0);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
}
// --- the caret ----------------------------------------------------------------
void __fastcall PlaceCaret_h(void *editBox) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origPlaceCaret(editBox);
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
// Outside the window the engine hides the caret and fires nothing.
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
return g_origPlaceCaret(editBox);
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
const float shift = ShiftOf(rows, line);
if (!(shift > 0.0f))
return g_origPlaceCaret(editBox);
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
const uint32_t handler = slot[0];
slot[0] = 0;
g_origPlaceCaret(editBox);
slot[0] = handler;
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
if (caret == nullptr)
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
float x = 0.0f;
int relPoint = 0;
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
Anchor(rows, caret, relPoint, x, y, 1);
if (handler == 0)
return;
// OnCursorChanged(x, y, width, height) as the engine fires it: each
// value by the anchor→script factor, pushed as a double.
void *anchor = AnchorOf(caret);
void **vtable = *static_cast<void ***>(anchor);
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
const double args[4] = {push(x), push(y), push(width), push(height)};
void *L = Game::Lua::State();
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
args[3]);
if (L != nullptr)
Game::Lua::SetTop(L, savedTop);
}
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
reinterpret_cast<void **>(&g_origPlaceCaret)};
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
reinterpret_cast<void *>(&IndexAtPoint_h),
reinterpret_cast<void **>(&g_origIndexAtPoint)};
} // namespace
} // namespace EditBox::IconRows
+20 -3
View File
@@ -11,7 +11,7 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Macro.SetMacroDisplay(macroSlot, value)` — the Lua door onto
// `C_Macro.SetMacroDisplay(macroSlot, value [, unit])` — the Lua door onto
// `Macro::ShowTooltip::Publish`, for an addon that has its own macro parser
// and wants the display done properly rather than by replacing the action
// globals. See the ShowTooltip header for what publishing buys.
@@ -27,6 +27,12 @@
// item-before-spell precedence, `item:N`, links, inventory slots and
// `bag slot` for free.
//
// `unit` is the `[@unit]` the publisher's clause matched: the unit the
// button's range check uses. Unlike `SetMacroSpell`'s target it stays a
// token, resolved at every lookup (`PublishOptions::liveUnit`), so a
// publisher whose clause still matches need not republish when the
// mouseover, focus or target moves.
//
// `SetMacroSpell(macro, spell [, target])` and `SetMacroItem(macro, item
// [, target])` are 4.3.4's narrower doors onto the same publish: a spellbook
// spell only, or an item only, aimed at `target` (the unit the button's range
@@ -37,6 +43,7 @@
#include "Offsets.h"
#include "macro/Arg.h"
#include "macro/ShowTooltip.h"
#include "unit/Identity.h"
#include <cstdint>
@@ -46,7 +53,7 @@ namespace {
int __fastcall Script_C_Macro_SetMacroDisplay(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: C_Macro.SetMacroDisplay(macroSlot, value)");
Game::Lua::Error(L, "Usage: C_Macro.SetMacroDisplay(macroSlot, value [, unit])");
return 0;
}
const int macroSlot = static_cast<int>(Game::Lua::ToNumber(L, 1));
@@ -63,7 +70,17 @@ int __fastcall Script_C_Macro_SetMacroDisplay(void *L) {
}
const char *value = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
Game::Lua::PushBoolean(L, Macro::ShowTooltip::Publish(macroSlot, value) ? 1 : 0);
Macro::ShowTooltip::PublishOptions options;
// A value that names nothing aims at nothing, so its unit is not read.
if (value != nullptr && value[0] != '\0' && Game::Lua::IsString(L, 3)) {
options.unit = Game::Lua::ToString(L, 3);
options.liveUnit = true;
// The engine's resolver vets the token: anything that is not one
// raises its "Unknown unit name" error, exactly as UnitExists does,
// before anything is published.
Unit::Identity::GuidForToken(options.unit);
}
Game::Lua::PushBoolean(L, Macro::ShowTooltip::Publish(macroSlot, value, options) ? 1 : 0);
return 1;
}
+12 -2
View File
@@ -160,6 +160,9 @@ struct Entry {
uint32_t isPet = 0;
int itemID = 0;
uint64_t unit = 0; // Info::unit
// A published `PublishOptions::liveUnit` token, resolved afresh by every
// lookup in place of `unit`. Empty for none.
char unitToken[kOptionsMax] = {};
uint32_t lastEvalMs = 0;
};
@@ -1233,7 +1236,8 @@ bool LookupEntries(uint32_t macroID, Info *out) {
out->spellID = e.spellID;
out->isPet = e.isPet;
out->itemID = e.itemID;
out->unit = e.unit;
out->unit = e.unitToken[0] != '\0' ? ::Unit::Identity::GuidForToken(e.unitToken)
: e.unit;
// A publisher re-publishes the moment its answer changes, so a claimed
// macro never needs the polling refresh a condition of ours does.
out->conditional = false;
@@ -1327,7 +1331,13 @@ bool Publish(int macroSlot, const char *value, const PublishOptions &options) {
ResolveValue(value, &r, /*explicitValue=*/true,
/*itemOnly=*/options.accept == Accept::Item);
}
r.unit = UnitGuid(options.unit);
// Set on every publish, so one that changes only the unit still lands:
// `ApplyResolution` stores the unit before its unchanged-value early out.
if (options.liveUnit && options.unit != nullptr)
std::snprintf(e.unitToken, sizeof(e.unitToken), "%s", options.unit);
else
e.unitToken[0] = '\0';
r.unit = options.liveUnit ? 0 : UnitGuid(options.unit);
BusyScope busy;
ApplyResolution(L, e, r, matched);
+10 -2
View File
@@ -72,8 +72,10 @@ struct Info {
// from it has to keep re-reading while the cursor rests on the button.
bool conditional;
// The unit the macro's spell is aimed at, 4.3.4's MACRONODE::m_target: the
// `[@unit]` of the clause that matched, or the target a `SetMacroSpell` /
// `SetMacroItem` named. 0 means the current target, as for any action.
// `[@unit]` of the clause that matched, the target a `SetMacroSpell` /
// `SetMacroItem` named, or whoever the token a `SetMacroDisplay` named
// points at as of this lookup. 0 means the current target, as for any
// action.
uint64_t unit;
};
@@ -86,6 +88,12 @@ struct PublishOptions {
Accept accept = Accept::Any;
// A unit token or a name the spell is aimed at; null for the current target.
const char *unit = nullptr;
// Resolve `unit` at every lookup rather than once, here, so the macro
// keeps following whoever the token names (a new mouseover, focus or
// target) with no republish. `unit` must then be a token the engine's
// resolver accepts: one that resolves once never raises later, since the
// resolver tells a token's family from its spelling alone.
bool liveUnit = false;
// The claim ends when the macro's body is edited, as 4.3.4's SetMacroSpell
// and SetMacroItem last only until the macro is parsed again. Otherwise it
// lasts until `Release` or a reload.
+73
View File
@@ -0,0 +1,73 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "object/Corpse.h"
#include "Game.h"
#include "Offsets.h"
#include "object/Resolve.h"
namespace Object::Corpse {
namespace {
// `ClntObjMgrEnumVisibleObjects(cb, ctx)`; the callback's dummy EDX slot
// puts the trailing GUID where the engine passes it. Return non-zero to keep
// iterating.
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
const uint8_t *Fields(const void *corpse) {
return Game::Read<const uint8_t *>(corpse, Offsets::OFF_UNIT_DESCRIPTOR);
}
uint64_t Owner(const void *corpse) {
const uint8_t *fields = Fields(corpse);
return fields != nullptr ? Game::Read<uint64_t>(fields, Offsets::OFF_CORPSE_FIELD_OWNER) : 0;
}
struct Scan {
uint64_t owner;
void *corpse;
};
// Bones keep their owner (tortoise-wow Map.cpp, the corpse-to-bones
// conversion), so every earlier death leaves one beside the live corpse;
// 4.3.4 skips them.
int __fastcall ScanCallback(Scan *scan, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid, nullptr, 0);
if (obj == nullptr || Owner(obj) != scan->owner ||
(Game::Read<uint32_t>(Fields(obj), Offsets::OFF_CORPSE_FIELD_FLAGS) &
Offsets::CORPSE_FLAG_BONES) != 0)
return 1;
scan->corpse = obj;
return 0;
}
} // namespace
void *ByOwner(uint64_t owner) {
if (owner == 0 || !Object::RegistryReady())
return nullptr;
Scan scan{owner, nullptr};
reinterpret_cast<EnumVisibleObjects_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS)(
reinterpret_cast<EnumCallback_t>(&ScanCallback), &scan);
return scan.corpse;
}
uint64_t OwnerOf(uint64_t guid) {
const void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid);
return obj != nullptr ? Owner(obj) : 0;
}
} // namespace Object::Corpse
+30
View File
@@ -0,0 +1,30 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// Player corpses (`TYPEMASK_CORPSE`) and the player each belongs to, read
// from the corpse's CORPSE_FIELD_OWNER.
namespace Object::Corpse {
// The visible corpse `owner` left, not counting bones, or nullptr — 4.3.4's
// ClntObjMgrFindCorpseByOwner, over the visible objects.
void *ByOwner(uint64_t owner);
// The player who left the corpse `guid`, or 0 when `guid` is not a visible
// corpse.
uint64_t OwnerOf(uint64_t guid);
} // namespace Object::Corpse
@@ -0,0 +1,182 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `smf:AddOnDisplayRefreshedCallback(callback)`, retail's: callback(frame)
// runs each time the frame refreshes what it shows.
//
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua) marks its
// display dirty on each change - a message, a scroll, a resize, a fade reset -
// and refreshes in its OnPostUpdate. So the callbacks run once per update of
// a shown frame that changed, in the order they were added, each under the
// error handler (secureexecuterange). The mark is cleared after them, so a
// change a callback makes (a scroll bar setting the offset) does not run them
// again.
//
// 1.12 refreshes what the frame shows in its line layout
// (ScrollingMessageFrame::Layout), which marks the frame. A message added
// while the frame is scrolled up changes no line and runs no layout, but it
// moves the scroll offset and range, so AddMessage marks the frame too, as
// ScrollByAmount does for retail's fade reset. The callbacks run after the
// frame's own update (FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE: OnUpdate, then the
// fader), which the engine's update pass calls once per frame for each shown
// frame - where retail's OnPostUpdate runs.
#include "scrollingmessageframe/DisplayRefreshed.h"
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/Layout.h"
#include <unordered_map>
namespace ScrollingMessageFrame::DisplayRefreshed {
namespace {
// Frames with callbacks, and whether each has a refresh pending. Every frame
// is rebuilt on /reload, so the map is cleared then.
std::unordered_map<void *, bool> g_frames;
// The frame-table → callback-list table, in the registry of the Lua state
// that made it. LUA_NOREF until first use in each state.
constexpr int kNoRef = -2;
int g_callbacksRef = kNoRef;
// Runs the callbacks of the frame, each as callback(frame) under the
// engine's script error handler.
void RunCallbacks(void *smf) {
using namespace Game::Lua;
void *L = State();
if (L == nullptr || g_callbacksRef == kNoRef)
return;
const int top = GetTop(L);
PushObject(L, smf);
const int frame = GetTop(L);
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
PushValue(L, frame);
RawGet(L, -2);
const int callbacks = GetTop(L);
if (Type(L, callbacks) == TYPE_TABLE) {
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
for (int i = 1;; ++i) {
int errIdx = 0;
if (errRef > 0) {
RawGetI(L, REGISTRY_INDEX, errRef);
errIdx = GetTop(L);
}
RawGetI(L, callbacks, i);
if (Type(L, -1) == TYPE_NIL)
break;
PushValue(L, frame);
PCall(L, 1, 0, errIdx);
SetTop(L, callbacks);
}
}
SetTop(L, top);
}
using OnUpdate_t = void(__fastcall *)(void *smf, void *edx, float elapsed);
OnUpdate_t g_origOnUpdate = nullptr;
void __fastcall OnUpdate_h(void *smf, void *edx, float elapsed) {
g_origOnUpdate(smf, edx, elapsed);
const auto it = g_frames.find(smf);
if (it == g_frames.end() || !it->second)
return;
RunCallbacks(smf);
const auto again = g_frames.find(smf); // a callback can add frames
if (again != g_frames.end())
again->second = false;
}
const Game::HookAutoRegister _updateHook{Offsets::FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE,
reinterpret_cast<void *>(&OnUpdate_h),
reinterpret_cast<void **>(&g_origOnUpdate)};
const ScrollingMessageFrame::Layout::AutoSubscribe _layoutSub{&MarkDirty};
int __fastcall Script_AddOnDisplayRefreshedCallback(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (Type(L, 2) != TYPE_FUNCTION) {
Error(L, "Usage: %s:AddOnDisplayRefreshedCallback(callback)", ObjectDisplayName(frame));
return 0;
}
if (g_callbacksRef == kNoRef) {
NewTable(L);
g_callbacksRef = Ref(L, REGISTRY_INDEX);
}
RawGetI(L, REGISTRY_INDEX, g_callbacksRef);
const int frames = GetTop(L);
PushValue(L, 1);
RawGet(L, frames);
if (Type(L, -1) != TYPE_TABLE) {
SetTop(L, frames);
NewTable(L);
PushValue(L, 1);
PushValue(L, -2);
RawSet(L, frames); // frames[self] = callbacks
}
const int callbacks = GetTop(L);
int n = 1;
for (;; ++n) {
RawGetI(L, callbacks, n);
const bool end = Type(L, -1) == TYPE_NIL;
SetTop(L, -2);
if (end)
break;
}
PushNumber(L, n);
PushValue(L, 2);
RawSet(L, callbacks);
g_frames.emplace(frame, false);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddOnDisplayRefreshedCallback", &Script_AddOnDisplayRefreshedCallback},
};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("callback", "function", "Called as callback(frame)."),
};
const Game::Doc::Function kAddOnDisplayRefreshedCallback{
"Adds a function that runs after the frame refreshes the messages it shows.", kArgs, {}};
const Game::Doc::Method kDocs[] = {
{"AddOnDisplayRefreshedCallback", &kAddOnDisplayRefreshedCallback},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_callbacksRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 1, kDocs, 1);
}
void PrepareForReload() { g_frames.clear(); }
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::ReloadAutoRegister _reload{&PrepareForReload};
} // namespace
void MarkDirty(void *smf) {
const auto it = g_frames.find(smf);
if (it != g_frames.end())
it->second = true;
}
} // namespace ScrollingMessageFrame::DisplayRefreshed
@@ -0,0 +1,22 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
namespace ScrollingMessageFrame::DisplayRefreshed {
// Marks a change to what the frame shows that its line layout does not
// report; the frame's display-refreshed callbacks run at its next update.
void MarkDirty(void *smf);
} // namespace ScrollingMessageFrame::DisplayRefreshed
+194
View File
@@ -0,0 +1,194 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Retail's insert mode on ScrollingMessageFrame: `SetInsertMode(mode)`,
// `GetInsertMode()` and the SCROLLING_MESSAGE_FRAME_INSERT_MODE_* globals.
//
// Retail's mixin (Blizzard_SharedXML/ScrollingMessageFrame.lua, RefreshLayout)
// stacks the lines up from the bottom in BOTTOM mode, the newest message at
// the bottom, and down from the top in TOP mode: the first line's TOPLEFT on
// the frame's TOPLEFT, each next line's TOPLEFT on the BOTTOMLEFT of the line
// before, the spacing below it. The scroll offset counts from the newest
// message in both modes. Setting the mode the frame has does nothing; a change
// lays every line out again and refreshes the display.
//
// 1.12's line layout is retail's BOTTOM mode. It anchors a line once, when it
// makes the line, through one SetPoint call (Offsets.h,
// CALL_SMF_LAYOUT_ANCHOR_LINE), so that call is redirected: a line of a frame
// in TOP mode takes the TOP anchors, every other line the engine's own. The
// layout makes the lines again when the frame changes size, which the
// redirect covers too. When a frame's mode changes, its lines are anchored
// again in order with the layout's own steps: clear the points, set the
// point, lay the line out now. The hyperlink buttons and inline textures are
// anchored to the lines, so they move with them. Retail's frame has no text
// insets; the TOP mode's first line sits the top inset below the frame's top,
// as the BOTTOM mode's sits the bottom inset above its bottom.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "frame/Destruction.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
#include <unordered_set>
namespace ScrollingMessageFrame::InsertMode {
namespace {
// SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP / _BOTTOM.
constexpr int kInsertModeTop = 1;
constexpr int kInsertModeBottom = 2;
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using PixelAlignY_t = float(__stdcall *)(float length);
// The frames in TOP mode. Every frame is rebuilt on /reload, so the set is
// cleared then.
std::unordered_set<void *> g_topFrames;
bool IsTop(void *smf) { return g_topFrames.count(smf) != 0; }
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// Sets a line's anchor as the layout gives it - (BOTTOMLEFT, the frame,
// BOTTOMLEFT, x, y) for the first line, (BOTTOMLEFT, the line before, TOPLEFT,
// x, y) for the others - or, for a frame in TOP mode, its mirror.
void SetLinePoint(void *smf, void *anchor, int point, void *relative, int relativePoint, float x, float y,
int flag) {
if (IsTop(smf)) {
point = Offsets::FRAMEPOINT_TOPLEFT;
if (relativePoint == Offsets::FRAMEPOINT_BOTTOMLEFT) { // the first line, on the frame
relativePoint = Offsets::FRAMEPOINT_TOPLEFT;
y = -Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_TOP);
} else {
relativePoint = Offsets::FRAMEPOINT_BOTTOMLEFT;
y = -y;
}
}
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, point, relative, relativePoint, x, y, flag);
}
// The layout's SetPoint for a line it has just made; the line's parent is
// the frame.
void __fastcall AnchorNewLine(void *anchor, void *, int point, void *relative, int relativePoint, float x, float y,
int flag) {
void *line = static_cast<uint8_t *>(anchor) - Offsets::OFF_REGION_ANCHOR;
SetLinePoint(Game::Read<void *>(line, Offsets::OFF_REGION_PARENT), anchor, point, relative, relativePoint, x, y,
flag);
}
// Anchors the frame's lines again, in order, with the layout's arguments and
// steps.
void AnchorLines(void *smf) {
const int count = Game::Read<int>(smf, Offsets::OFF_SMF_LINE_COUNT);
const auto *lines = Game::Read<const uint8_t *>(smf, Offsets::OFF_SMF_LINE_ARRAY);
void *previous = nullptr;
for (int i = 0; i < count; ++i) {
void *line = Game::Read<void *>(lines + i * Offsets::SMF_LINE_STRIDE, Offsets::OFF_SMF_LINE_FONTSTRING);
void *anchor = AnchorOf(line);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
if (previous == nullptr)
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(smf), Offsets::FRAMEPOINT_BOTTOMLEFT,
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_LEFT),
Game::Read<float>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_INSET_BOTTOM), 1);
else
SetLinePoint(smf, anchor, Offsets::FRAMEPOINT_BOTTOMLEFT, AnchorOf(previous), Offsets::FRAMEPOINT_TOPLEFT,
0.0f,
reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y)(
Game::Read<float>(line, Offsets::OFF_FONTSTRING_SPACING)),
1);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(anchor, nullptr, 1);
previous = line;
}
}
int __fastcall Script_SetInsertMode(void *L) {
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
const double mode = Game::Lua::IsNumber(L, 2) ? Game::Lua::ToNumber(L, 2) : 0;
if (mode != kInsertModeTop && mode != kInsertModeBottom) {
Game::Lua::Error(L,
"Usage: %s:SetInsertMode(SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or "
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM)",
Game::Lua::ObjectDisplayName(frame));
return 0;
}
const bool top = mode == kInsertModeTop;
if (top == IsTop(frame))
return 0;
if (top)
g_topFrames.insert(frame);
else
g_topFrames.erase(frame);
AnchorLines(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
int __fastcall Script_GetInsertMode(void *L) {
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
Game::Lua::PushNumber(L, IsTop(frame) ? kInsertModeTop : kInsertModeBottom);
return 1;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"SetInsertMode", &Script_SetInsertMode},
{"GetInsertMode", &Script_GetInsertMode},
};
const Game::Doc::Field kModeArgs[] = {
Game::Doc::Req("mode", "number",
"SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP or SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM."),
};
const Game::Doc::Field kModeRets[] = {
Game::Doc::Req("mode", "number", "BOTTOM until SetInsertMode changes it."),
};
const Game::Doc::Function kSetInsertMode{
"Sets where the newest message shows: at the top, with older messages below it, or at the bottom.", kModeArgs,
{}};
const Game::Doc::Function kGetInsertMode{"Returns where the newest message shows.", {}, kModeRets};
const Game::Doc::Method kDocs[] = {
{"SetInsertMode", &kSetInsertMode},
{"GetInsertMode", &kGetInsertMode},
};
void RegisterLuaFunctions() {
void *L = Game::Lua::State();
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_TOP", kInsertModeTop);
Game::Lua::SetGlobalNumber(L, "SCROLLING_MESSAGE_FRAME_INSERT_MODE_BOTTOM", kInsertModeBottom);
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 2, kDocs, 2);
Common::RedirectCall(Offsets::CALL_SMF_LAYOUT_ANCHOR_LINE, Offsets::FUN_REGION_SET_POINT,
reinterpret_cast<const void *>(&AnchorNewLine));
}
void PrepareForReload() { g_topFrames.clear(); }
void OnFrameDestroyed(void *frame) { g_topFrames.erase(frame); }
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::ReloadAutoRegister _reload{&PrepareForReload};
const Frame::Destruction::AutoSubscribe _destructionSub{&OnFrameDestroyed};
} // namespace
} // namespace ScrollingMessageFrame::InsertMode
+41
View File
@@ -0,0 +1,41 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "scrollingmessageframe/Layout.h"
#include "Game.h"
#include "Offsets.h"
namespace ScrollingMessageFrame::Layout {
namespace {
AutoSubscribe *g_head = nullptr;
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
Layout_t g_origLayout = nullptr;
void __fastcall Layout_h(void *smf, void *edx, int newestRecord) {
g_origLayout(smf, edx, newestRecord);
for (const AutoSubscribe *s = g_head; s != nullptr; s = s->next)
s->fn(smf);
}
const Game::HookAutoRegister _hook{Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&Layout_h),
reinterpret_cast<void **>(&g_origLayout)};
} // namespace
AutoSubscribe::AutoSubscribe(Listener fn) : fn(fn), next(g_head) { g_head = this; }
} // namespace ScrollingMessageFrame::Layout
+35
View File
@@ -0,0 +1,35 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// Shared observer for the ScrollingMessageFrame line layout
// FUN_SMF_DISPLAY_REFRESH (Offsets.h), which lays out the lines the frame
// shows after every change to them: a message added at the bottom, a scroll,
// a resize, a font change, Clear. A listener runs after it, with the frame.
//
// MinHook allows ONE hook per address and more than one feature follows the
// layout (Text::InlineTexture's early icon apply, the display-refreshed
// callbacks), so this module owns the hook and fans out. Declare a file-scope
// static const ScrollingMessageFrame::Layout::AutoSubscribe _sub{&OnLayout};
namespace ScrollingMessageFrame::Layout {
using Listener = void (*)(void *smf);
struct AutoSubscribe {
explicit AutoSubscribe(Listener fn);
Listener fn;
AutoSubscribe *next;
};
} // namespace ScrollingMessageFrame::Layout
+229
View File
@@ -0,0 +1,229 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `ScrollingMessageFrame:GetMessageInfo(index)`, retail's: the message's
// text, its colour, then every argument AddMessage took after the colour.
//
// Retail's ScrollingMessageFrame is a Lua mixin whose AddMessage(message, r,
// g, b, ...) packs the call into an entry and GetMessageInfo unpacks it
// (Blizzard_SharedXML/ScrollingMessageFrame.lua). Index 1 is the oldest
// message; the extra arguments come back as given, nils and count included;
// an index outside 1..GetNumMessages() returns nothing. (4.3.4's C method
// returns text, accessID, typeID, extraData instead.)
//
// The text and colour come from the engine's own record of the message
// (Offsets.h, OFF_SCROLLINGMESSAGEFRAME_MESSAGES), so a colour
// UpdateColorByID changed reads back changed, as retail's AdjustMessageColors
// does. The record keeps the colour as bytes, so a component reads back as
// byte/255. It keeps the first extra argument only as an integer (0 for
// anything else), so the extra arguments are kept beside the ring instead.
//
// AddMessage is registered again on the ScrollingMessageFrame registry, over
// the engine's. The registrar (0x00701D80) adds a node for the name whatever
// is there; Storm's TSHashTable::InternalNew links a new node at the head of
// its bucket (NewNode(1)), and the method lookup (0x00702000) returns the
// first match, so this one answers. It runs the engine's AddMessage, then
// records the extra arguments for the record the engine wrote. The engine
// adds a message exactly when the text is a nonempty string
// (0x0079299C..0x007929C7: lua_isstring, lua_tostring, first byte); a bad
// `self` raises a Lua error instead. The ring's one writer has no other
// caller, so every record was written through here.
//
// The extra arguments live in a registry table keyed by the frame's Lua table,
// then by record. A record's table is refilled when the record is overwritten,
// so a full ring makes no garbage.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
namespace ScrollingMessageFrame::MessageInfo {
namespace {
constexpr int kFirstExtra = 6; // self, text, r, g, b, then the extra arguments
// The frame-table → record → extra-arguments table, in the registry of the Lua
// state that made it. LUA_NOREF until first use in each state.
constexpr int kNoRef = -2;
int g_extrasRef = kNoRef;
// Pushes the extra-argument table of record `record` of the frame whose Lua
// table is at stack slot 1, or nil when it has none and `create` is false.
void PushExtras(void *L, int record, bool create) {
using namespace Game::Lua;
if (g_extrasRef == kNoRef) {
if (!create) {
PushNil(L);
return;
}
NewTable(L);
g_extrasRef = Ref(L, REGISTRY_INDEX);
}
RawGetI(L, REGISTRY_INDEX, g_extrasRef); // [frames]
PushValue(L, 1);
RawGet(L, -2); // [frames, records]
if (Type(L, -1) != TYPE_TABLE && create) {
SetTop(L, -2);
NewTable(L);
PushValue(L, 1);
PushValue(L, -2);
RawSet(L, -4); // frames[self] = records
}
Remove(L, -2); // [records]
if (Type(L, -1) != TYPE_TABLE)
return;
RawGetI(L, -1, record); // [records, extras]
if (Type(L, -1) != TYPE_TABLE && create) {
SetTop(L, -2);
NewTable(L);
PushNumber(L, record);
PushValue(L, -2);
RawSet(L, -4); // records[record] = extras
}
Remove(L, -2); // [extras]
}
// The `n` of the extra-argument table at absolute stack slot `extras`.
int ExtraCount(void *L, int extras) {
using namespace Game::Lua;
PushString(L, "n");
RawGet(L, extras);
const int n = static_cast<int>(ToNumber(L, -1));
SetTop(L, -2);
return n;
}
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
void StoreExtras(void *L, int record, int top) {
using namespace Game::Lua;
const int count = top >= kFirstExtra ? top - kFirstExtra + 1 : 0;
PushExtras(L, record, count > 0);
const int extras = GetTop(L);
if (Type(L, extras) == TYPE_TABLE) {
const int was = ExtraCount(L, extras);
for (int i = 1; i <= count; ++i) {
PushNumber(L, i);
PushValue(L, kFirstExtra - 1 + i);
RawSet(L, extras);
}
for (int i = count + 1; i <= was; ++i) {
PushNumber(L, i);
PushNil(L);
RawSet(L, extras);
}
PushString(L, "n");
PushNumber(L, count);
RawSet(L, extras);
}
SetTop(L, -2);
}
int __fastcall Script_AddMessage(void *L) {
const int top = Game::Lua::GetTop(L);
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE)(L);
const char *text = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
if (text == nullptr || *text == '\0')
return 0;
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
StoreExtras(L, Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS), top);
// Scrolled up, the engine adds the message without a layout.
DisplayRefreshed::MarkDirty(frame);
return 0;
}
int __fastcall Script_GetMessageInfo(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (!IsNumber(L, 2)) {
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
return 0;
}
const double index = ToNumber(L, 2);
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (!(index >= 1 && index <= count) || index != static_cast<int>(index))
return 0;
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count +
static_cast<int>(index);
if (record < 0)
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
const auto *message = Game::Read<const uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
record * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
const uint32_t color = Game::Read<uint32_t>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR);
PushExtras(L, record, false);
const int extras = GetTop(L);
const int n = Type(L, extras) == TYPE_TABLE ? ExtraCount(L, extras) : 0;
CheckStack(L, 4 + n);
PushString(L, Game::Read<const char *>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT));
PushNumber(L, ((color >> 16) & 0xFF) / 255.0);
PushNumber(L, ((color >> 8) & 0xFF) / 255.0);
PushNumber(L, (color & 0xFF) / 255.0);
for (int i = 1; i <= n; ++i)
RawGetI(L, extras, i);
Remove(L, extras);
return 4 + n;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddMessage", &Script_AddMessage},
{"GetMessageInfo", &Script_GetMessageInfo},
};
const Game::Doc::Field kAddMessageArgs[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
Game::Doc::Opt("g", "number"),
Game::Doc::Opt("b", "number"),
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
};
const Game::Doc::Function kAddMessage{"Adds a message to the frame.", kAddMessageArgs, {}};
const Game::Doc::Field kGetMessageInfoArgs[] = {
Game::Doc::Req("index", "luaIndex", "1 is the oldest message."),
};
const Game::Doc::Field kGetMessageInfoRets[] = {
Game::Doc::Req("text", "string"),
Game::Doc::Req("r", "number"),
Game::Doc::Req("g", "number"),
Game::Doc::Req("b", "number"),
Game::Doc::Vararg("any", "The arguments AddMessage took after the colour."),
};
const Game::Doc::Function kGetMessageInfo{
"Returns a message's text, its colour, and the extra arguments it was added with, or nothing "
"when there is no message at that index.",
kGetMessageInfoArgs, kGetMessageInfoRets};
const Game::Doc::Method kDocs[] = {
{"AddMessage", &kAddMessage},
{"GetMessageInfo", &kGetMessageInfo},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_extrasRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 2, kDocs, 2);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace ScrollingMessageFrame::MessageInfo
+142
View File
@@ -0,0 +1,142 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Retail's scroll offset on ScrollingMessageFrame: `SetScrollOffset(offset)`,
// `GetScrollOffset()`, `GetMaxScrollRange()` and `ScrollByAmount(amount)`.
//
// Retail's offset (Blizzard_SharedXML/ScrollingMessageFrame.lua) counts the
// lines the frame is scrolled up from the newest message, 0 at the bottom, up
// to GetMaxScrollRange() = max(GetNumMessages() - 1, 0), where the oldest
// message is the bottom line - the place 1.12's own ScrollToTop goes. 1.12
// keeps the same offset: ScrollToOffset and ScrollOffset (Offsets.h) are its
// two directions, and GetScrollOffset is the engine's GetCurrentScroll under
// retail's name. Retail clamps the offset to its range; the engine wraps one
// outside it, so the setters clamp first. SetScrollOffset to the offset the
// frame has does nothing, as on retail. ScrollByAmount always calls the
// engine, which re-arms the shown lines' fade when the offset stays: retail's
// ResetAllFadeTimes.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
namespace ScrollingMessageFrame::Scroll {
namespace {
using ScrollToOffset_t = void(__fastcall *)(void *smf, void *edx, int offset);
using ScrollOffset_t = int(__fastcall *)(void *smf);
int ScrollOffset(void *smf) {
return reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(smf);
}
int MaxScrollRange(void *smf) {
const int count = Game::Read<int>(smf, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
return count > 1 ? count - 1 : 0;
}
// `offset` clamped to 0..MaxScrollRange (NaN to 0).
int Clamped(void *smf, double offset) {
const int max = MaxScrollRange(smf);
return offset >= 0 ? (offset < max ? static_cast<int>(offset) : max) : 0;
}
void ScrollTo(void *smf, int offset) {
reinterpret_cast<ScrollToOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET)(smf, nullptr, offset);
}
void *ResolveSmf(void *L) {
return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
}
int __fastcall Script_SetScrollOffset(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: %s:SetScrollOffset(offset)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
const int offset = Clamped(frame, Game::Lua::ToNumber(L, 2));
if (offset != ScrollOffset(frame))
ScrollTo(frame, offset);
return 0;
}
int __fastcall Script_GetMaxScrollRange(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
Game::Lua::PushNumber(L, MaxScrollRange(frame));
return 1;
}
int __fastcall Script_ScrollByAmount(void *L) {
void *frame = ResolveSmf(L);
if (frame == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: %s:ScrollByAmount(amount)", Game::Lua::ObjectDisplayName(frame));
return 0;
}
ScrollTo(frame, Clamped(frame, ScrollOffset(frame) + Game::Lua::ToNumber(L, 2)));
DisplayRefreshed::MarkDirty(frame);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"SetScrollOffset", &Script_SetScrollOffset},
{"GetScrollOffset",
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL)},
{"GetMaxScrollRange", &Script_GetMaxScrollRange},
{"ScrollByAmount", &Script_ScrollByAmount},
};
const Game::Doc::Field kOffsetArgs[] = {
Game::Doc::Req("offset", "number", "Lines from the newest message; clamped to 0..GetMaxScrollRange()."),
};
const Game::Doc::Field kOffsetRets[] = {
Game::Doc::Req("offset", "number", "Lines from the newest message; 0 when not scrolled."),
};
const Game::Doc::Field kRangeRets[] = {
Game::Doc::Req("maxScrollRange", "number"),
};
const Game::Doc::Field kAmountArgs[] = {
Game::Doc::Req("amount", "number", "Positive scrolls toward older messages."),
};
const Game::Doc::Function kSetScrollOffset{"Scrolls the frame to a number of lines from the newest message.",
kOffsetArgs, {}};
const Game::Doc::Function kGetScrollOffset{"Returns how many lines the frame is scrolled from the newest message.",
{}, kOffsetRets};
const Game::Doc::Function kGetMaxScrollRange{"Returns the largest scroll offset: one less than the message count.",
{}, kRangeRets};
const Game::Doc::Function kScrollByAmount{"Scrolls the frame by a number of lines and shows the faded lines again.",
kAmountArgs, {}};
const Game::Doc::Method kDocs[] = {
{"SetScrollOffset", &kSetScrollOffset},
{"GetScrollOffset", &kGetScrollOffset},
{"GetMaxScrollRange", &kGetMaxScrollRange},
{"ScrollByAmount", &kScrollByAmount},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 4, kDocs, 4);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace ScrollingMessageFrame::Scroll
+2
View File
@@ -88,6 +88,7 @@
#include "time/Clock.h"
#include "unit/Focus.h"
#include "unit/Identity.h"
#include "unit/IncomingResurrection.h"
#include <cstdint>
@@ -691,6 +692,7 @@ void ParseChannelUpdate(Net::CDataStore *packet) {
void HandleCastAborted(uint64_t guid, int spellID, bool castSlotOnly = false) {
if (guid == 0 || spellID == 0)
return;
Unit::IncomingResurrection::OnCastEnded(guid, spellID);
// Remote units: invalidate the cached cast/channel so
// UnitCastingInfo/UnitChannelInfo stop reporting it this tick.
for (auto &e : g_remoteCasts) {
+10
View File
@@ -52,6 +52,16 @@ bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
return (flags & flagMask) != 0;
}
bool HasEffect(const uint8_t *spellRecord, int effect) {
if (spellRecord == nullptr)
return false;
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
if (Game::Read<int>(spellRecord, Offsets::OFF_SPELL_RECORD_EFFECT + i * 4) == effect)
return true;
}
return false;
}
int SpellbookSlotToID(int slot1Based, int bookType) {
const int slot = slot1Based - 1;
if (slot < 0 || slot >= Offsets::SPELLBOOK_MAX_SLOTS)
+4
View File
@@ -31,6 +31,10 @@ const uint8_t *RecordForID(int spellID);
bool IsFitToFamily(const uint8_t *spellRecord, uint32_t family,
uint64_t flagMask);
// Whether one of the spell's three effects is `effect` — 4.3.4's
// SpellHasEffect. False for a null record.
bool HasEffect(const uint8_t *spellRecord, int effect);
// Resolves a 1-based Lua-facing spellbook slot to the spellID stored
// there. `bookType` is `0` for the player spellbook, `1` for the pet
// spellbook (matches the engine's encoding from `Script_GetSpellName`).
+2 -25
View File
@@ -15,6 +15,7 @@
#include "Game.h"
#include "Offsets.h"
#include "object/Corpse.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "unit/Position.h"
@@ -114,8 +115,6 @@ constexpr int32_t TARGET_PET = 5;
using Relation_t = bool(__thiscall *)(void *caster, void *other);
using CreatureType_t = int(__thiscall *)(void *unit);
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
bool Relation(uintptr_t fn, void *caster, void *other) {
return reinterpret_cast<Relation_t>(fn)(caster, other);
@@ -181,28 +180,6 @@ uint32_t TargetWord(const uint8_t *rec, bool &aimsAtPet) {
return word;
}
struct CorpseScan {
uint64_t owner;
void *corpse;
};
int __fastcall CorpseScanCallback(CorpseScan *scan, void * /*unusedEdx*/, uint64_t guid) {
void *obj = Object::ByGuid(Offsets::TYPEMASK_CORPSE, guid, nullptr, 0);
if (obj == nullptr ||
Game::Read<uint64_t>(Fields(obj), Offsets::OFF_CORPSE_FIELD_OWNER) != scan->owner)
return 1;
scan->corpse = obj;
return 0;
}
// 4.3.4's ClntObjMgrFindCorpseByOwner, over the visible objects.
void *CorpseOf(uint64_t owner) {
CorpseScan scan{owner, nullptr};
reinterpret_cast<EnumVisibleObjects_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS)(
reinterpret_cast<EnumCallback_t>(&CorpseScanCallback), &scan);
return scan.corpse;
}
// A unit target (0x006E4511..0x006E469A).
bool AdmitsUnit(void *caster, const uint8_t *rec, uint32_t word, void *unit) {
const uint8_t *fields = Fields(unit);
@@ -281,7 +258,7 @@ void *AdmittedTarget(void *caster, const uint8_t *rec, void *target) {
if (info != nullptr &&
(Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_FLAGS) &
Offsets::PLAYER_FLAG_GHOST) != 0) {
if (void *corpse = CorpseOf(Game::Read<uint64_t>(Head(target), 0)))
if (void *corpse = Object::Corpse::ByOwner(Game::Read<uint64_t>(Head(target), 0)))
return corpse;
}
}
+31 -9
View File
@@ -79,6 +79,7 @@
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/Layout.h"
#include "text/InlineTexturePool.h"
#include "text/PtrProbe.h"
#include "texture/Atlas.h"
@@ -2085,7 +2086,8 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
// So a line's icons are recorded — and its regions placed — one frame AFTER its
// glyphs draw, because a frame's textures (our icon regions) draw before its
// fontstring glyphs and the placement isn't computed until the glyph build runs
// mid-paint. This co-hook closes it: after the refresh sets up the lines (still
// mid-paint. This layout listener (ScrollingMessageFrame::Layout, which owns
// the hook) closes it: after the refresh sets up the lines (still
// PRE-RENDER — the refresh runs from the SMF's Lua/event-driven methods, never
// mid-render), force each icon-bearing line to resolve+build and apply its icon
// placement synchronously, so the icons draw with their glyphs the same frame.
@@ -2096,9 +2098,6 @@ static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
// disabled independently of the whole feature; the SEH latch trips it on a fault.
bool g_earlyApply = true;
using SmfRefresh_t = void(__fastcall *)(void *smf, void *edx, int newestIdx);
SmfRefresh_t g_smfRefreshOriginal = nullptr;
using EnsureBuilt_t = int(__fastcall *)(void *node);
using RealizeAnchor_t = void(__thiscall *)(void *anchor, int flag);
using RebuildStringFs_t = void(__fastcall *)(void *fs);
@@ -2183,15 +2182,12 @@ void SafeSmfEarlyApply(void *smf) {
}
}
void __fastcall SmfRefresh_h(void *smf, void *edx, int newestIdx) {
g_smfRefreshOriginal(smf, edx, newestIdx);
void OnSmfLayout(void *smf) {
if (g_inlineEnabled && g_earlyApply)
SafeSmfEarlyApply(smf);
}
static const Game::HookAutoRegister _smfRefreshHook{
Offsets::FUN_SMF_DISPLAY_REFRESH, reinterpret_cast<void *>(&SmfRefresh_h),
reinterpret_cast<void **>(&g_smfRefreshOriginal)};
static const ScrollingMessageFrame::Layout::AutoSubscribe _smfLayoutSub{&OnSmfLayout};
// --- Lua control surface ---------------------------------------------------
@@ -2238,6 +2234,32 @@ void PrepareForReload() {
g_nodeBakedColor.clear();
}
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after) {
before = 0.0f;
after = 0.0f;
if (!g_inlineEnabled || !LooksReadable(fs) || !LooksReadable(text) || len <= 0 ||
!HasInlineTexture(text, len))
return false;
FsPenFont pf;
if (!ResolveFsPenFont(fs, pf))
return false;
const float overflow = IconOverflowPx(text, len, pf.fontHPen, pf.layoutScale);
if (!(overflow > 0.0f))
return false;
// The node's justify-V as the block creator (FUN_0044d420) reads the
// style word: TOP (8) wins, then BOTTOM (0x20), else middle — the value
// the emitter reads from the node to split the move.
const uint32_t style = Game::Read<uint32_t>(fs, Offsets::OFF_FONTSTRING_FLAGS);
const int justifyV = (style & 0x8u) ? 0 : (style & 0x20u) ? 2 : 1;
const float move = (justifyV == 1) ? std::floor(overflow * 0.5f) : overflow;
const float toFs = pf.fontHInt / pf.fontHPen; // pen px → the getter's units
if (justifyV != 0)
before = move * toFs;
if (justifyV != 2)
after = move * toFs;
return true;
}
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace Text::InlineTexture
+11
View File
@@ -13,6 +13,8 @@
#pragma once
#include <cstdint>
namespace Text::InlineTexture {
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
@@ -20,4 +22,13 @@ namespace Text::InlineTexture {
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
void PrepareForReload();
// The room the tall inline icons on the line [text, text + len) take when
// `fs` renders it, in the font string's own units (its height getter's):
// `before` is how far the render moves that line's text down, `after` how
// far it moves the lines after it — 4.3.4's InitializeText offsets, split by
// the font string's vertical justify (top: none / all; middle: half / half,
// floored; bottom: all / none). Both 0, and false, without an icon taller
// than the font or with the feature off.
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after);
} // namespace Text::InlineTexture
+217
View File
@@ -0,0 +1,217 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `UnitHasIncomingResurrection(unit)` and `INCOMING_RESURRECT_CHANGED` —
// 4.3.4's CGIncomingResurrection (Ui/IncomingResurrection.cpp) and the two
// calls its spell code makes into it:
// - SMSG_SPELL_START: a spell with a resurrect effect and an object target
// adds {caster, spell} under that target
// (PendingSpellCast::OnSpellStartPacket, 0x008ACD80);
// - the cast's end removes it (FinishPendingSpellCast, 0x008AF840, which
// SPELL_GO and every failure run).
// Each change signals INCOMING_RESURRECT_CHANGED to every token naming the
// target, or the corpse's owner when the target is a corpse
// (Script_SendUnitSignal). The query answers for the unit itself, then for
// its corpse (HasIncomingResurrection, 0x009FCF80).
//
// Where 1.12 differs:
// - Resurrect effects. 4.3.4 tests effect 18 alone. 1.12's class
// resurrections (Resurrection, Redemption, Ancestral Spirit, Rebirth,
// every rank) carry 113, RESURRECT_NEW, and 18 only item and NPC spells
// (Spell.dbc), so both count.
// - The cast's end. 1.12 keeps no pending cast. Its end is the caster's
// SMSG_SPELL_GO, or CGUnit_C::ClearCastingSpell and the failure packets,
// which `Spell::Cast` owns and reports through OnCastEnded. Turtle sends
// observers no failure packet on an interrupt; SuperWoW calls
// ClearCastingSpell instead (docs/notes/cast-channel.md).
// - Lifetime. 4.3.4 empties the table on entering and leaving the world. A
// cast whose caster leaves view never reports its end, so an entry whose
// caster is no longer visible ends on the next world tick, which covers
// a world change as well.
// - One cast per caster, as the unit's single precast slot (+0xC8C) holds:
// a caster's new resurrection replaces its last.
#include "unit/IncomingResurrection.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "net/PacketDispatch.h"
#include "object/Corpse.h"
#include "object/Resolve.h"
#include "spell/Lookup.h"
#include "tick/WorldTick.h"
#include "unit/Identity.h"
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Unit::IncomingResurrection {
namespace {
constexpr int SPELL_EFFECT_RESURRECT = 18;
constexpr int SPELL_EFFECT_RESURRECT_NEW = 113;
const Game::Doc::Field kChangedPayload[] = {
Game::Doc::Req("unitTarget", "UnitToken", "The unit, or the player whose corpse it is."),
};
const Game::Doc::Event kChangedDoc{
"Unit", "A resurrection cast at the unit began or ended. Read UnitHasIncomingResurrection.",
kChangedPayload};
const Event::Custom::AutoReserve kChanged{"INCOMING_RESURRECT_CHANGED", &kChangedDoc};
struct Entry {
uint64_t target; // the cast's object target: a unit or a corpse
uint64_t unit; // who hears the change: the corpse's owner, else target
uint64_t caster;
int spellID;
};
std::vector<Entry> g_entries;
void Signal(uint64_t unit) {
const int slot = kChanged.Slot();
if (!Event::Custom::HasListeners(slot))
return;
char tokens[16][32];
const int n = Unit::Identity::TokensForGUID(unit, tokens, 16);
for (int i = 0; i < n; ++i)
Event::Custom::Fire(slot, "%s", tokens[i]);
}
// Ends `caster`'s resurrection: of `spellID`, or any for 0.
void End(uint64_t caster, int spellID) {
for (auto it = g_entries.begin(); it != g_entries.end(); ++it) {
if (it->caster == caster && (spellID == 0 || it->spellID == spellID)) {
const uint64_t unit = it->unit;
g_entries.erase(it);
Signal(unit);
return;
}
}
}
void Begin(uint64_t target, uint64_t caster, int spellID) {
for (const Entry &e : g_entries) {
if (e.caster == caster && e.target == target && e.spellID == spellID)
return;
}
End(caster, 0);
const uint64_t owner = Object::Corpse::OwnerOf(target);
const uint64_t unit = owner != 0 ? owner : target;
g_entries.push_back({target, unit, caster, spellID});
Signal(unit);
}
bool Targeted(uint64_t target) {
for (const Entry &e : g_entries) {
if (e.target == target)
return true;
}
return false;
}
bool HasIncomingResurrection(uint64_t guid) {
if (guid == 0 || g_entries.empty())
return false;
if (Targeted(guid))
return true;
const void *corpse = Object::Corpse::ByOwner(guid);
return corpse != nullptr && Targeted(Unit::Identity::GuidForObject(corpse));
}
bool IsResurrect(int spellID) {
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
return Spell::Lookup::HasEffect(rec, SPELL_EFFECT_RESURRECT) ||
Spell::Lookup::HasEffect(rec, SPELL_EFFECT_RESURRECT_NEW);
}
// SMSG_SPELL_START / SMSG_SPELL_GO: packed item or caster, packed caster,
// spell, then for SPELL_START u16 flags, u32 cast time and the targets.
void OnPacket(uint32_t opcode, Net::CDataStore *p) {
if (opcode != Offsets::SMSG_SPELL_START && opcode != Offsets::SMSG_SPELL_GO)
return;
if (opcode == Offsets::SMSG_SPELL_GO && g_entries.empty())
return;
Net::ReadPackedGuid(p);
const uint64_t caster = Net::ReadPackedGuid(p);
const int spellID = static_cast<int>(Net::Read<uint32_t>(p));
if (opcode == Offsets::SMSG_SPELL_GO) {
if (p->m_read <= p->m_size)
End(caster, spellID);
return;
}
Net::Read<uint16_t>(p);
Net::Read<uint32_t>(p);
const uint16_t targetMask = Net::Read<uint16_t>(p);
const uint64_t target =
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? Net::ReadPackedGuid(p) : 0;
if (p->m_read > p->m_size || caster == 0 || target == 0 || !IsResurrect(spellID))
return;
Begin(target, caster, spellID);
}
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
void OnWorldTick() {
for (size_t i = 0; i < g_entries.size();) {
if (Object::ByGuid(Offsets::TYPEMASK_UNIT, g_entries[i].caster) != nullptr) {
++i;
continue;
}
const uint64_t unit = g_entries[i].unit;
g_entries.erase(g_entries.begin() + static_cast<std::ptrdiff_t>(i));
Signal(unit);
}
}
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
int __fastcall Script_UnitHasIncomingResurrection(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitHasIncomingResurrection(\"unit\")");
return 0;
}
Game::Lua::PushBool(L, HasIncomingResurrection(
Unit::Identity::GuidForToken(Game::Lua::ToString(L, 1))));
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasIncomingResurrection", "bool"),
};
const Game::Doc::Function kUnitHasIncomingResurrection{
"Whether a resurrection is being cast at the unit, or at the corpse of a released player.",
kArgs, kRets, "UnitGlobals"};
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("UnitHasIncomingResurrection",
&Script_UnitHasIncomingResurrection,
&kUnitHasIncomingResurrection);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
void OnCastEnded(uint64_t caster, int spellID) {
if (caster != 0 && spellID != 0 && !g_entries.empty())
End(caster, spellID);
}
} // namespace Unit::IncomingResurrection
+28
View File
@@ -0,0 +1,28 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// 4.3.4's CGIncomingResurrection: the resurrections being cast at each unit
// or corpse. Backs `UnitHasIncomingResurrection` and
// `INCOMING_RESURRECT_CHANGED`.
namespace Unit::IncomingResurrection {
// `caster` stopped casting `spellID`: CGUnit_C::ClearCastingSpell or a
// failure packet, both owned by `Spell::Cast`. Ends the resurrection it was
// casting, if that is one.
void OnCastEnded(uint64_t caster, int spellID);
} // namespace Unit::IncomingResurrection