mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 07:30:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead52e79ce | ||
|
|
7cb6cda101 | ||
|
|
5ab497f2c7 | ||
|
|
205d1fd43b | ||
|
|
950c9deb76 | ||
|
|
e7ccc11309 | ||
|
|
bab31b5f7b | ||
|
|
e0e24bab34 |
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user