mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d021ae3e3d | ||
|
|
f4862dc817 | ||
|
|
3a6f53f919 | ||
|
|
19b299d3df | ||
|
|
77b304ae68 | ||
|
|
c965e49117 | ||
|
|
ead52e79ce | ||
|
|
7cb6cda101 | ||
|
|
5ab497f2c7 | ||
|
|
205d1fd43b | ||
|
|
950c9deb76 |
@@ -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), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `: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` |
|
||||
@@ -313,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` |
|
||||
@@ -360,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,22 @@ 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),
|
||||
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
|
||||
before the oldest; a full frame drops it, as retail's CircularBuffer
|
||||
`PushBack` does — 4.3.4 overwrites the oldest there).
|
||||
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
|
||||
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
|
||||
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
|
||||
whose int fields our retail-shaped records do not have). The compaction
|
||||
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
|
||||
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
|
||||
or Clear (0x007882B0) when nothing is kept.
|
||||
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
|
||||
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
|
||||
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
|
||||
|
||||
+215
@@ -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)
|
||||
@@ -241,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)
|
||||
@@ -380,6 +385,12 @@ 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:BackFillMessage(text, r, g, b, ...)`](#smfbackfillmessagetext-r-g-b-)
|
||||
- [`smf:RemoveMessagesByPredicate(predicate)`](#smfremovemessagesbypredicatepredicate)
|
||||
- [`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)
|
||||
@@ -4359,6 +4370,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
|
||||
@@ -5783,6 +5845,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.
|
||||
@@ -8599,6 +8669,151 @@ 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:BackFillMessage(text, r, g, b, ...)`
|
||||
|
||||
On ScrollingMessageFrame. Adds a message as the oldest message of the frame.
|
||||
`AddMessage` adds the newest message. `BackFillMessage` adds history behind
|
||||
the messages that the frame already has. An addon that rebuilds a frame from
|
||||
a history walks the history from the newest entry to the oldest and adds each
|
||||
entry with this function. The messages that the frame already has do not
|
||||
move.
|
||||
|
||||
- `text`, `r`, `g`, `b` and the extra values work as in `AddMessage`.
|
||||
Without `r`, `g` and `b`, the message takes the color of the frame's font.
|
||||
`GetMessageInfo` returns the extra values after the color.
|
||||
- If the frame is at its line limit (`SetMaxLines`), the function drops the
|
||||
message, because the message is older than each message that the frame
|
||||
keeps.
|
||||
- If the frame is scrolled to the bottom, the frame shows the message when
|
||||
there is room above its other lines. If the frame is scrolled up, the frame
|
||||
keeps its view.
|
||||
|
||||
```lua
|
||||
ChatFrame1:BackFillMessage("older", 1, 1, 1)
|
||||
ChatFrame1:GetMessageInfo(1) -- "older", 1, 1, 1
|
||||
```
|
||||
|
||||
### `smf:RemoveMessagesByPredicate(predicate)`
|
||||
|
||||
On ScrollingMessageFrame. Removes the messages that `predicate` selects. The
|
||||
function calls `predicate(text, r, g, b, ...)` for each message, from the
|
||||
oldest to the newest, with the values that `GetMessageInfo` returns for the
|
||||
message. If `predicate` returns true, the function removes the message. The
|
||||
other messages keep their order. If `predicate` is not a function, the
|
||||
function gives an error.
|
||||
|
||||
- The function calls `predicate` for all messages before it removes any
|
||||
message. If `predicate` gives an error for a message, the frame keeps that
|
||||
message.
|
||||
- The frame keeps its scroll offset (`GetScrollOffset`). If fewer messages
|
||||
remain than the offset, the offset becomes the oldest message.
|
||||
- The frame shows the lines that remain at once. If no message remains, the
|
||||
result is the same as `smf:Clear()`.
|
||||
|
||||
```lua
|
||||
-- remove the messages that were added with the extra value 7
|
||||
ChatFrame1:RemoveMessagesByPredicate(function(text, r, g, b, id)
|
||||
return id == 7
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
+162
-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;
|
||||
@@ -4582,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
|
||||
@@ -4993,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
|
||||
@@ -12811,6 +12838,93 @@ 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 rest of the record as the ring writer 0x00788150 fills it: shown
|
||||
// (1), the frame's visible and fade times, and the integer id.
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN = 0x8,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME = 0xC,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME = 0x10,
|
||||
OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID = 0x14,
|
||||
// The bottom flag (4.3.4's m_atBottom): the ring writer lays the lines
|
||||
// out from the newest record when it is set (0x00788150 tail), and
|
||||
// ScrollToBottom re-arms the shown lines instead of laying out
|
||||
// (0x007886D0; benilla-re ui/messageframe "Scrolling brings lines back").
|
||||
OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM = 0x354,
|
||||
// The visible and fade times the writer copies into each record
|
||||
// (0x00788150: record+0xC = +0x364, record+0x10 = +0x368; the setters
|
||||
// 0x00788090 / 0x007880F0 store them).
|
||||
OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME = 0x364,
|
||||
OFF_SCROLLINGMESSAGEFRAME_FADE_TIME = 0x368,
|
||||
// The colour (CImVector) of the object at OFF_SCROLLINGMESSAGEFRAME_FONT,
|
||||
// the message colour AddMessage takes without r, g, b
|
||||
// (0x00792B3C..0x00792B45: `[[smf+0x35C]+0x58]`).
|
||||
OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR = 0x58,
|
||||
// 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,
|
||||
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
|
||||
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
|
||||
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
|
||||
// RearmLines (`void __thiscall(smf)`, 4.3.4's RefreshMessages): every
|
||||
// shown line back to alpha 0xFF, shown, the frame's two times, and its
|
||||
// node shown (benilla-re ui/messageframe "Scrolling brings lines back").
|
||||
// FUN_SMF_DISPLAY_REFRESH (LayoutLines) owns +0x348 (the scroll record)
|
||||
// and +0x354 (at bottom = record == position) itself, and re-arms only
|
||||
// when it moves off the bottom; a layout at the bottom keeps the fade
|
||||
// state, so 4.3.4 re-arms explicitly after a removal's layout.
|
||||
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
|
||||
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
|
||||
@@ -12979,6 +13093,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
|
||||
@@ -12996,7 +13155,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
|
||||
|
||||
+190
-14
@@ -702,40 +702,216 @@ const Game::Doc::Function kCancelUnitBuff{
|
||||
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
|
||||
"SpellGlobals"};
|
||||
|
||||
const Game::Doc::Field kAuraDataFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("icon", "string", "The icon's texture path."),
|
||||
Game::Doc::Req("applications", "number", "The stack count."),
|
||||
Game::Doc::Req("spellId", "number"),
|
||||
Game::Doc::Req("dispelName", "string",
|
||||
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
|
||||
Game::Doc::Req("isHelpful", "bool"),
|
||||
Game::Doc::Req("isHarmful", "bool"),
|
||||
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
|
||||
Game::Doc::Req("expirationTime", "number",
|
||||
"When the aura ends, in GetTime() seconds; 0 when not known."),
|
||||
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
|
||||
"Nil when the cast was not seen or the caster has no unit token."),
|
||||
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
|
||||
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
|
||||
Game::Doc::Opt("auraInstanceID", "number", nullptr,
|
||||
"Nil for an aura that is not in the unit's aura list."),
|
||||
Game::Doc::Req("charges", "number", "Always 0."),
|
||||
Game::Doc::Req("maxCharges", "number", "Always 0."),
|
||||
Game::Doc::Req("timeMod", "number", "Always 1."),
|
||||
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
|
||||
"True when any player or a player's pet applied the aura."),
|
||||
Game::Doc::Req("canApplyAura", "bool",
|
||||
"True when the player or the pet knows the spell that applies the aura."),
|
||||
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
|
||||
Game::Doc::Req("isStealable", "bool",
|
||||
"True when the player can remove this buff from an enemy."),
|
||||
Game::Doc::Req("isBossAura", "bool", "Always false."),
|
||||
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
|
||||
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
|
||||
};
|
||||
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
|
||||
"One aura on a unit."};
|
||||
|
||||
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
|
||||
// `Data::PushPositional` is the source of the order.
|
||||
const Game::Doc::Field kPositionalRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr,
|
||||
"Nil, as the only return, when there is no such aura."),
|
||||
Game::Doc::Opt("icon", "string"),
|
||||
Game::Doc::Opt("count", "number"),
|
||||
Game::Doc::Opt("dispelType", "string", nullptr,
|
||||
"Empty when the aura has no dispel type."),
|
||||
Game::Doc::Opt("duration", "number"),
|
||||
Game::Doc::Opt("expirationTime", "number"),
|
||||
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
|
||||
Game::Doc::Opt("isStealable", "bool"),
|
||||
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("spellId", "number"),
|
||||
Game::Doc::Opt("canApplyAura", "bool"),
|
||||
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("castByPlayer", "bool", nullptr,
|
||||
"True when a player applied the aura; false for a pet."),
|
||||
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kAuraRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kByIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByIndex{
|
||||
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
|
||||
kAuraRets};
|
||||
const Game::Doc::Function kUnitAura{
|
||||
"Returns the aura at an index as positional values, without making a table.",
|
||||
kByIndexArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kLockedIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Function kGetBuffDataByIndex{
|
||||
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
const Game::Doc::Function kGetDebuffDataByIndex{
|
||||
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kLockedFilterArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Further tokens such as PLAYER or RAID; the range is fixed."),
|
||||
};
|
||||
const Game::Doc::Function kUnitBuff{
|
||||
"Returns the buff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
const Game::Doc::Function kUnitDebuff{
|
||||
"Returns the debuff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kSlotsArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters"),
|
||||
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
|
||||
Game::Doc::Opt("continuationToken", "number", nullptr,
|
||||
"The first return of the previous call, to get the next batch."),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to fill with the slot IDs instead; the call then returns the "
|
||||
"token and the count. A ClassicAPI extension."),
|
||||
};
|
||||
const Game::Doc::Field kSlotsRets[] = {
|
||||
Game::Doc::Opt("outContinuationToken", "number", nullptr,
|
||||
"Nil when this batch reaches the last aura."),
|
||||
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraSlots{
|
||||
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
|
||||
kSlotsRets};
|
||||
|
||||
const Game::Doc::Field kBySlotArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySlot{
|
||||
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
|
||||
const Game::Doc::Function kUnitAuraBySlot{
|
||||
"Returns the aura that a slot ID names as positional values, without making a table.",
|
||||
kBySlotArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kBySpellIDArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuraBySpellID{
|
||||
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
|
||||
kBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetPlayerAuraBySpellID{
|
||||
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kBySpellNameArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySpellName{
|
||||
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
|
||||
kBySpellNameArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kUnitAurasArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"With no HELPFUL or HARMFUL token, both are returned."),
|
||||
};
|
||||
const Game::Doc::Field kUnitAurasRets[] = {
|
||||
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuras{
|
||||
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
|
||||
kUnitAurasRets};
|
||||
|
||||
const Game::Doc::Field kDispelColorArgs[] = {
|
||||
Game::Doc::Opt("dispelName", "string", nullptr,
|
||||
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
|
||||
"no-type color."),
|
||||
};
|
||||
const Game::Doc::Field kDispelColorRets[] = {
|
||||
Game::Doc::Req("color", "colorRGBA"),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDispelTypeColor{
|
||||
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
|
||||
kDispelColorRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
|
||||
&Script_GetAuraDataByIndex);
|
||||
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
|
||||
&Script_GetBuffDataByIndex);
|
||||
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
|
||||
&Script_GetDebuffDataByIndex);
|
||||
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
|
||||
&Script_UnitAura);
|
||||
&Script_UnitAura, &kUnitAura);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
|
||||
&Script_UnitBuff);
|
||||
&Script_UnitBuff, &kUnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
&Script_UnitDebuff, &kUnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots);
|
||||
&Script_GetAuraSlots, &kGetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot);
|
||||
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
&Script_GetPlayerAuraBySpellID);
|
||||
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
|
||||
&Script_GetAuraDataBySpellName);
|
||||
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
|
||||
&Script_GetUnitAuras);
|
||||
&Script_GetUnitAuras, &kGetUnitAuras);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
|
||||
&Script_GetAuraDispelTypeColor);
|
||||
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
|
||||
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
|
||||
&kCancelUnitBuff);
|
||||
}
|
||||
|
||||
@@ -193,9 +193,23 @@ int __fastcall Script_RegisterComboDuration(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Req("baseSeconds", "number",
|
||||
"The duration at zero combo points; each point adds a fifth of the "
|
||||
"difference to maxSeconds."),
|
||||
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterComboDuration{
|
||||
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
|
||||
&Script_RegisterComboDuration);
|
||||
&Script_RegisterComboDuration, &kRegisterComboDuration);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+22
-1
@@ -946,10 +946,31 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDurationModArgs[] = {
|
||||
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
|
||||
Game::Doc::Req("triggerSchool", "number",
|
||||
"The school index the trigger must have, or below 0 for any school."),
|
||||
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
|
||||
Game::Doc::Req("affectedFamilyFlags", "number",
|
||||
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
|
||||
Game::Doc::Req("affectedIcon", "number",
|
||||
"The SpellIconID the aura must have, or 0 for any icon."),
|
||||
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
|
||||
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
|
||||
};
|
||||
const Game::Doc::Field kDurationModRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
|
||||
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
|
||||
"that caster lands.",
|
||||
kDurationModArgs, kDurationModRets, nullptr, true};
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
&Script_RegisterAuraDurationModifierByTrigger,
|
||||
&kRegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
|
||||
@@ -62,9 +62,23 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Opt("dstBag", "BagIndex", "0",
|
||||
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
|
||||
};
|
||||
const Game::Doc::Function kAutoStoreItem{
|
||||
"Moves an item and lets the server choose where it goes, merging it into "
|
||||
"existing stacks first.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
|
||||
&Script_C_Container_AutoStoreItem);
|
||||
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -43,9 +43,21 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemCharges{
|
||||
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
|
||||
&Script_C_Container_GetContainerItemCharges);
|
||||
&Script_C_Container_GetContainerItemCharges,
|
||||
&kGetContainerItemCharges);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -33,9 +33,22 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("itemID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("startTime", "number",
|
||||
"When the cooldown started, in GetTime() seconds; 0 when none."),
|
||||
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
|
||||
Game::Doc::Req("enable", "number",
|
||||
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemCooldown{
|
||||
"Returns the cooldown of an item's use effect.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
|
||||
&Script_C_Container_GetItemCooldown);
|
||||
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,9 +40,22 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("durability", "number", nullptr,
|
||||
"Nil when the slot is empty or the item has no durability."),
|
||||
Game::Doc::Opt("maxDurability", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemDurability{
|
||||
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
|
||||
&Script_C_Container_GetContainerItemDurability);
|
||||
&Script_C_Container_GetContainerItemDurability,
|
||||
&kGetContainerItemDurability);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -80,10 +80,25 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("inSet", "bool"),
|
||||
Game::Doc::Opt("setList", "string", nullptr,
|
||||
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
|
||||
"Returns whether the item in a bag slot is saved in an equipment set, and the "
|
||||
"names of those sets.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Container", "GetContainerItemEquipmentSetInfo",
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo);
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo,
|
||||
&kGetContainerItemEquipmentSetInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -182,15 +182,45 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kBagArgs[] = {
|
||||
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
|
||||
};
|
||||
const Game::Doc::Field kNumFreeSlotsRets[] = {
|
||||
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
|
||||
Game::Doc::Req("bagFamily", "number",
|
||||
"The bag's family bitmask; 0 for a bag that takes any item."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerNumFreeSlots{
|
||||
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
|
||||
kBagArgs, kNumFreeSlotsRets};
|
||||
|
||||
const Game::Doc::Field kFreeSlotsRets[] = {
|
||||
Game::Doc::Opt("freeSlots", "table", nullptr,
|
||||
"The empty slot numbers in ascending order; nil when the bag does not exist."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerFreeSlots{
|
||||
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
|
||||
|
||||
const Game::Doc::Field kTotalRets[] = {
|
||||
Game::Doc::Req("totalFreeSlots", "number"),
|
||||
};
|
||||
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
|
||||
"Returns the number of empty slots in the backpack and the equipped bags that take "
|
||||
"any item.",
|
||||
{}, kTotalRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
&Script_C_Container_GetContainerNumFreeSlots,
|
||||
&kGetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
|
||||
&Script_C_Container_GetContainerFreeSlots);
|
||||
&Script_C_Container_GetContainerFreeSlots,
|
||||
&kGetContainerFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
|
||||
&kCalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,9 +40,20 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("hasItem", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasContainerItem{
|
||||
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
|
||||
&Script_C_Container_HasContainerItem);
|
||||
&Script_C_Container_HasContainerItem, &kHasContainerItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -117,13 +117,27 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kHasRets[] = {
|
||||
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
|
||||
};
|
||||
const Game::Doc::Function kPlayerHasHearthstone{
|
||||
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
|
||||
|
||||
const Game::Doc::Field kUseRets[] = {
|
||||
Game::Doc::Req("used", "bool",
|
||||
"True when a hearthstone was found; the cast itself can still fail."),
|
||||
};
|
||||
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
|
||||
kUseRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
|
||||
&Script_C_Container_PlayerHasHearthstone);
|
||||
&Script_C_Container_PlayerHasHearthstone,
|
||||
&kPlayerHasHearthstone);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
|
||||
&Script_C_Container_UseHearthstone);
|
||||
&Script_C_Container_UseHearthstone, &kUseHearthstone);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+11
-1
@@ -41,9 +41,19 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
|
||||
&Script_C_Container_GetContainerItemID);
|
||||
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -57,9 +57,23 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Req("dstBag", "BagIndex"),
|
||||
Game::Doc::Req("dstSlot", "luaIndex"),
|
||||
Game::Doc::Req("count", "number", "From 1 to 255."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
|
||||
};
|
||||
const Game::Doc::Function kMoveItem{
|
||||
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
|
||||
true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
|
||||
&Script_C_Container_MoveItem);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
|
||||
&kMoveItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -37,9 +37,24 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("isOpenable", "bool", nullptr,
|
||||
"Nil when the slot is empty or the item's data is not cached yet."),
|
||||
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
|
||||
};
|
||||
const Game::Doc::Function kIsContainerItemOpenable{
|
||||
"Returns whether the item in a bag slot can be opened, and whether the player can "
|
||||
"open it now.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
|
||||
&Script_C_Container_IsContainerItemOpenable);
|
||||
&Script_C_Container_IsContainerItemOpenable,
|
||||
&kIsContainerItemOpenable);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -97,9 +97,29 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kItemQuestInfoFields[] = {
|
||||
Game::Doc::Req("isQuestItem", "bool"),
|
||||
Game::Doc::Opt("questID", "number", nullptr,
|
||||
"The quest the item begins; nil when it begins none."),
|
||||
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
|
||||
};
|
||||
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("questInfo", "ItemQuestInfo",
|
||||
"Always a table; an empty slot reports no quest."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemQuestInfo{
|
||||
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
|
||||
&Script_C_Container_GetContainerItemQuestInfo);
|
||||
&Script_C_Container_GetContainerItemQuestInfo,
|
||||
&kGetContainerItemQuestInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -37,9 +37,21 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemRepairCost{
|
||||
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
|
||||
nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
|
||||
&Script_C_Container_GetContainerItemRepairCost);
|
||||
&Script_C_Container_GetContainerItemRepairCost,
|
||||
&kGetContainerItemRepairCost);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -49,9 +49,21 @@ int __fastcall Script_C_Container_SwapItems(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Req("dstBag", "BagIndex"),
|
||||
Game::Doc::Req("dstSlot", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
|
||||
};
|
||||
const Game::Doc::Function kSwapItems{
|
||||
"Swaps the contents of two bag slots without the cursor.", kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SwapItems",
|
||||
&Script_C_Container_SwapItems);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SwapItems", &Script_C_Container_SwapItems,
|
||||
&kSwapItems);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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
|
||||
+152
-18
@@ -543,46 +543,180 @@ int __fastcall Script_UseEquipmentSet(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kSetIDArgs[] = {
|
||||
Game::Doc::Req("setID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "luaIndex", "An equipment slot, from 1 to 19."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kCanUseRets[] = {
|
||||
Game::Doc::Req("canUseEquipmentSets", "bool", "False while a set swap is in progress."),
|
||||
};
|
||||
const Game::Doc::Function kCanUseEquipmentSets{
|
||||
"Returns whether an equipment set can be used now.", {}, kCanUseRets};
|
||||
|
||||
const Game::Doc::Field kNumRets[] = {
|
||||
Game::Doc::Req("numEquipmentSets", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetNumEquipmentSets{
|
||||
"Returns the number of equipment sets the character has.", {}, kNumRets};
|
||||
|
||||
const Game::Doc::Field kIDsRets[] = {
|
||||
Game::Doc::Req("setIDs", "table", "The set IDs in the order the sets were created."),
|
||||
};
|
||||
const Game::Doc::Function kGetEquipmentSetIDs{
|
||||
"Returns the IDs of all the character's equipment sets.", {}, kIDsRets};
|
||||
|
||||
const Game::Doc::Field kIDArgs[] = {
|
||||
Game::Doc::Req("name", "cstring", "The exact name; case matters."),
|
||||
};
|
||||
const Game::Doc::Field kIDRets[] = {
|
||||
Game::Doc::Opt("setID", "number", nullptr, "Nil when no set has that name."),
|
||||
};
|
||||
const Game::Doc::Function kGetEquipmentSetID{
|
||||
"Returns the ID of the equipment set with a name.", kIDArgs, kIDRets};
|
||||
|
||||
const Game::Doc::Field kInfoRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr,
|
||||
"Nil, with the rest, when the set does not exist."),
|
||||
Game::Doc::Opt("icon", "string", nullptr, "The icon's texture name."),
|
||||
Game::Doc::Opt("setID", "number"),
|
||||
Game::Doc::Opt("isEquipped", "bool", nullptr,
|
||||
"True when every slot that is not ignored holds what the set saved."),
|
||||
Game::Doc::Opt("numItems", "number", nullptr,
|
||||
"numEquipped plus numInInventory plus numMissing."),
|
||||
Game::Doc::Opt("numEquipped", "number"),
|
||||
Game::Doc::Opt("numInInventory", "number", nullptr,
|
||||
"In the bags, or in the bank while the bank is open."),
|
||||
Game::Doc::Opt("numMissing", "number"),
|
||||
Game::Doc::Opt("numIgnored", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetEquipmentSetInfo{
|
||||
"Returns an equipment set's name, icon, and where its items are.", kSetIDArgs,
|
||||
kInfoRets};
|
||||
|
||||
const Game::Doc::Field kIgnoredRets[] = {
|
||||
Game::Doc::Opt("ignoredSlots", "table", nullptr,
|
||||
"The numbers of the ignored slots; nil when the set does not exist."),
|
||||
};
|
||||
const Game::Doc::Function kGetIgnoredSlots{
|
||||
"Returns the slots an equipment set leaves alone.", kSetIDArgs, kIgnoredRets};
|
||||
|
||||
const Game::Doc::Field kItemIDsRets[] = {
|
||||
Game::Doc::Opt("itemIDs", "table", nullptr,
|
||||
"Keyed by slot: the item ID, 0 for an empty slot, 1 for an ignored slot, "
|
||||
"or -1 when not known."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemIDs{
|
||||
"Returns the item ID an equipment set saved for each slot.", kSetIDArgs, kItemIDsRets};
|
||||
|
||||
const Game::Doc::Field kLocationsRets[] = {
|
||||
Game::Doc::Opt("locations", "table", nullptr,
|
||||
"Keyed by slot: a packed location, 0 for an empty slot, 1 for an ignored "
|
||||
"slot, or -1 for a missing item."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemLocations{
|
||||
"Returns where each item of an equipment set is now.", kSetIDArgs, kLocationsRets};
|
||||
|
||||
const Game::Doc::Field kCreateArgs[] = {
|
||||
Game::Doc::Req("name", "cstring", "Shorter than 32 characters, with no line break."),
|
||||
Game::Doc::Opt("icon", "string", "\"INV_Misc_QuestionMark\"", "A texture name."),
|
||||
};
|
||||
const Game::Doc::Field kCreateRets[] = {
|
||||
Game::Doc::Opt("setID", "number", nullptr, "Nil when the name is already in use."),
|
||||
};
|
||||
const Game::Doc::Function kCreateEquipmentSet{
|
||||
"Saves the equipped items as a new equipment set.", kCreateArgs, kCreateRets};
|
||||
|
||||
const Game::Doc::Field kSaveArgs[] = {
|
||||
Game::Doc::Req("setID", "number"),
|
||||
Game::Doc::Opt("icon", "string", nullptr, "A new texture name; nil keeps the icon."),
|
||||
};
|
||||
const Game::Doc::Function kSaveEquipmentSet{
|
||||
"Saves the equipped items into an existing equipment set.", kSaveArgs, {}};
|
||||
|
||||
const Game::Doc::Field kModifyArgs[] = {
|
||||
Game::Doc::Req("setID", "number"),
|
||||
Game::Doc::Req("newName", "cstring"),
|
||||
};
|
||||
const Game::Doc::Function kModifyEquipmentSet{
|
||||
"Renames an equipment set; does nothing when another set has the name.", kModifyArgs,
|
||||
{}};
|
||||
|
||||
const Game::Doc::Function kDeleteEquipmentSet{
|
||||
"Deletes an equipment set and removes it from the action bars.", kSetIDArgs, {}};
|
||||
|
||||
const Game::Doc::Function kIgnoreSlotForSave{
|
||||
"Makes the next save of an equipment set leave a slot alone.", kSlotArgs, {}};
|
||||
const Game::Doc::Function kUnignoreSlotForSave{
|
||||
"Makes the next save of an equipment set include a slot again.", kSlotArgs, {}};
|
||||
|
||||
const Game::Doc::Field kIsIgnoredRets[] = {
|
||||
Game::Doc::Req("isSlotIgnored", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsSlotIgnoredForSave{
|
||||
"Returns whether the next save of an equipment set leaves a slot alone.", kSlotArgs,
|
||||
kIsIgnoredRets};
|
||||
const Game::Doc::Function kClearIgnoredSlotsForSave{
|
||||
"Makes the next save of an equipment set include every slot.", {}, {}};
|
||||
|
||||
const Game::Doc::Field kLockedRets[] = {
|
||||
Game::Doc::Opt("hasLockedItems", "bool", nullptr, "Nil when the set does not exist."),
|
||||
};
|
||||
const Game::Doc::Function kEquipmentSetContainsLockedItems{
|
||||
"Returns whether an item of an equipment set is locked by a move in progress.",
|
||||
kSetIDArgs, kLockedRets};
|
||||
|
||||
const Game::Doc::Field kUseRets[] = {
|
||||
Game::Doc::Req("setWasEquipped", "bool",
|
||||
"True when the swap starts; EQUIPMENT_SWAP_FINISHED gives the result."),
|
||||
};
|
||||
const Game::Doc::Function kUseEquipmentSet{
|
||||
"Equips the items of an equipment set.", kSetIDArgs, kUseRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CanUseEquipmentSets",
|
||||
&Script_CanUseEquipmentSets);
|
||||
&Script_CanUseEquipmentSets, &kCanUseEquipmentSets);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetNumEquipmentSets",
|
||||
&Script_GetNumEquipmentSets);
|
||||
&Script_GetNumEquipmentSets, &kGetNumEquipmentSets);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetIDs",
|
||||
&Script_GetEquipmentSetIDs);
|
||||
&Script_GetEquipmentSetIDs, &kGetEquipmentSetIDs);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetID",
|
||||
&Script_GetEquipmentSetID);
|
||||
&Script_GetEquipmentSetID, &kGetEquipmentSetID);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetInfo",
|
||||
&Script_GetEquipmentSetInfo);
|
||||
&Script_GetEquipmentSetInfo, &kGetEquipmentSetInfo);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetIgnoredSlots",
|
||||
&Script_GetIgnoredSlots);
|
||||
&Script_GetIgnoredSlots, &kGetIgnoredSlots);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemIDs",
|
||||
&Script_GetItemIDs);
|
||||
&Script_GetItemIDs, &kGetItemIDs);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemLocations",
|
||||
&Script_GetItemLocations);
|
||||
&Script_GetItemLocations, &kGetItemLocations);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CreateEquipmentSet",
|
||||
&Script_CreateEquipmentSet);
|
||||
&Script_CreateEquipmentSet, &kCreateEquipmentSet);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "SaveEquipmentSet",
|
||||
&Script_SaveEquipmentSet);
|
||||
&Script_SaveEquipmentSet, &kSaveEquipmentSet);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ModifyEquipmentSet",
|
||||
&Script_ModifyEquipmentSet);
|
||||
&Script_ModifyEquipmentSet, &kModifyEquipmentSet);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "DeleteEquipmentSet",
|
||||
&Script_DeleteEquipmentSet);
|
||||
&Script_DeleteEquipmentSet, &kDeleteEquipmentSet);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IgnoreSlotForSave",
|
||||
&Script_IgnoreSlotForSave);
|
||||
&Script_IgnoreSlotForSave, &kIgnoreSlotForSave);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UnignoreSlotForSave",
|
||||
&Script_UnignoreSlotForSave);
|
||||
&Script_UnignoreSlotForSave, &kUnignoreSlotForSave);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IsSlotIgnoredForSave",
|
||||
&Script_IsSlotIgnoredForSave);
|
||||
&Script_IsSlotIgnoredForSave, &kIsSlotIgnoredForSave);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ClearIgnoredSlotsForSave",
|
||||
&Script_ClearIgnoredSlotsForSave);
|
||||
&Script_ClearIgnoredSlotsForSave,
|
||||
&kClearIgnoredSlotsForSave);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet",
|
||||
"EquipmentSetContainsLockedItems",
|
||||
&Script_EquipmentSetContainsLockedItems);
|
||||
&Script_EquipmentSetContainsLockedItems,
|
||||
&kEquipmentSetContainsLockedItems);
|
||||
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UseEquipmentSet",
|
||||
&Script_UseEquipmentSet);
|
||||
&Script_UseEquipmentSet, &kUseEquipmentSet);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+25
-3
@@ -151,8 +151,30 @@ constexpr int LOC_IGNORED = 1;
|
||||
// when the swap settles — see
|
||||
// `EquipmentSet::Pending`. `success` is
|
||||
// 1 or nil, never 0 (a Lua 0 is true).
|
||||
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
|
||||
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
|
||||
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
|
||||
inline const Game::Doc::Event kSetsChangedDoc{
|
||||
"EquipmentSet",
|
||||
"Fires when an equipment set is created, saved, renamed, or deleted, or when the slots "
|
||||
"ignored for the next save change.",
|
||||
{}};
|
||||
inline const Game::Doc::Field kSwapPendingPayload[] = {
|
||||
Game::Doc::Req("setID", "number"),
|
||||
};
|
||||
inline const Game::Doc::Event kSwapPendingDoc{
|
||||
"EquipmentSet", "Fires when C_EquipmentSet.UseEquipmentSet has sent all its moves.",
|
||||
kSwapPendingPayload};
|
||||
inline const Game::Doc::Field kSwapFinishedPayload[] = {
|
||||
Game::Doc::Opt("result", "number", nullptr, "1 when the swap succeeded, else nil."),
|
||||
Game::Doc::Req("setID", "number"),
|
||||
};
|
||||
inline const Game::Doc::Event kSwapFinishedDoc{
|
||||
"EquipmentSet", "Fires when the moves of an equipment set swap are done.",
|
||||
kSwapFinishedPayload};
|
||||
|
||||
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED",
|
||||
&kSetsChangedDoc};
|
||||
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING",
|
||||
&kSwapPendingDoc};
|
||||
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED",
|
||||
&kSwapFinishedDoc};
|
||||
|
||||
} // namespace EquipmentSet
|
||||
|
||||
@@ -218,6 +218,16 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetEquipmentSet", &Script_GameTooltipSetEquipmentSet},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kSetEquipmentSetArgs[] = {
|
||||
Game::Doc::Req("name", "cstring", "The name of the equipment set."),
|
||||
};
|
||||
const Game::Doc::Function kSetEquipmentSet{
|
||||
"Shows an equipment set in the tooltip: its name and where its items are.",
|
||||
kSetEquipmentSetArgs, {}};
|
||||
const Game::Doc::Method g_methodDocs[] = {
|
||||
{"SetEquipmentSet", &kSetEquipmentSet},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
@@ -226,7 +236,8 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
|
||||
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+207
-19
@@ -669,54 +669,242 @@ static int __fastcall Script_C_Reputation_SetWatchedFactionByID(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kFactionDataFields[] = {
|
||||
Game::Doc::Req("factionID", "number"),
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("description", "string", "Empty when the faction has none."),
|
||||
Game::Doc::Req("reaction", "luaIndex", "The rank, from 1 for Hated to 8 for Exalted."),
|
||||
Game::Doc::Req("currentReactionThreshold", "number",
|
||||
"The standing at the bottom of the current rank."),
|
||||
Game::Doc::Req("nextReactionThreshold", "number",
|
||||
"The standing at the top of the current rank."),
|
||||
Game::Doc::Req("currentStanding", "number"),
|
||||
Game::Doc::Req("atWarWith", "bool"),
|
||||
Game::Doc::Req("canToggleAtWar", "bool",
|
||||
"False below -3000 standing, when the war state is fixed, or with no "
|
||||
"reputation slot."),
|
||||
Game::Doc::Req("isChild", "bool", "Always false."),
|
||||
Game::Doc::Req("isHeader", "bool", "True when the faction is a header in the reputation list."),
|
||||
Game::Doc::Req("isHeaderWithRep", "bool", "Always false."),
|
||||
Game::Doc::Req("isCollapsed", "bool", "True for a collapsed header."),
|
||||
Game::Doc::Req("isWatched", "bool", "True when the faction is shown above the experience bar."),
|
||||
Game::Doc::Req("hasBonusRepGain", "bool", "Always false."),
|
||||
Game::Doc::Req("canSetInactive", "bool",
|
||||
"True when the faction has a reputation slot and is not a header."),
|
||||
Game::Doc::Req("isAccountWide", "bool", "Always false."),
|
||||
};
|
||||
const Game::Doc::Structure kFactionData{"FactionData", "Reputation", kFactionDataFields,
|
||||
"A faction's reputation data."};
|
||||
|
||||
const Game::Doc::Field kFactionIDArgs[] = {
|
||||
Game::Doc::Req("factionID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kSortIndexArgs[] = {
|
||||
Game::Doc::Req("factionSortIndex", "luaIndex", "A position in the reputation list."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kFactionIDByIndexArgs[] = {
|
||||
Game::Doc::Req("factionIndex", "luaIndex", "A position in the reputation list."),
|
||||
};
|
||||
const Game::Doc::Field kFactionIDByIndexRets[] = {
|
||||
Game::Doc::Opt("factionID", "number", nullptr,
|
||||
"0 for a header with no faction of its own, such as Other or Inactive; nil "
|
||||
"when the index is out of range."),
|
||||
};
|
||||
const Game::Doc::Function kGetFactionIDByIndex{
|
||||
"Returns the faction ID of the entry at a position in the reputation list.",
|
||||
kFactionIDByIndexArgs, kFactionIDByIndexRets, "ReputationGlobals", true};
|
||||
|
||||
const Game::Doc::Field kFactionInfoByIDRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr, "Nil, with the rest, when no faction has the ID."),
|
||||
Game::Doc::Opt("description", "string", nullptr, "Empty when the faction has none."),
|
||||
Game::Doc::Opt("standingID", "number", nullptr, "The rank, from 1 for Hated to 8 for Exalted."),
|
||||
Game::Doc::Opt("barMin", "number", nullptr, "The standing at the bottom of the current rank."),
|
||||
Game::Doc::Opt("barMax", "number", nullptr, "The standing at the top of the current rank."),
|
||||
Game::Doc::Opt("barValue", "number", nullptr, "The current standing."),
|
||||
Game::Doc::Opt("atWarWith", "number", nullptr,
|
||||
"1 when the player is at war with the faction, else nil."),
|
||||
Game::Doc::Opt("canToggleAtWar", "number", nullptr,
|
||||
"1 when the at-war state can be changed, else nil."),
|
||||
Game::Doc::Opt("isHeader", "number", nullptr,
|
||||
"1 when the faction is a header in the reputation list, else nil."),
|
||||
Game::Doc::Opt("isCollapsed", "number", nullptr, "1 for a collapsed header, else nil."),
|
||||
Game::Doc::Opt("isWatched", "number", nullptr,
|
||||
"1 when the faction is shown above the experience bar, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kGetFactionInfoByID{
|
||||
"Returns the same values as GetFactionInfo for a faction ID, whether or not the faction "
|
||||
"is in the reputation list.",
|
||||
kFactionIDArgs, kFactionInfoByIDRets, "ReputationGlobals"};
|
||||
|
||||
const Game::Doc::Field kFactionParentIDRets[] = {
|
||||
Game::Doc::Opt("parentFactionID", "number", nullptr,
|
||||
"0 for a faction with no parent; nil when no faction has the ID."),
|
||||
};
|
||||
const Game::Doc::Function kGetFactionParentID{
|
||||
"Returns the ID of a faction's parent faction, such as Alliance for Stormwind.",
|
||||
kFactionIDArgs, kFactionParentIDRets, "ReputationGlobals", true};
|
||||
|
||||
const Game::Doc::Field kSetWatchedArgs[] = {
|
||||
Game::Doc::Req("factionID", "number",
|
||||
"0, or an ID with no reputation slot, clears the watched faction; a negative "
|
||||
"ID does nothing."),
|
||||
};
|
||||
const Game::Doc::Function kSetWatchedFactionByID{
|
||||
"Sets the faction shown above the experience bar.", kSetWatchedArgs, {}};
|
||||
|
||||
const Game::Doc::Field kSetSelectedByIDArgs[] = {
|
||||
Game::Doc::Req("factionID", "number", "0 clears the selection; a negative ID does nothing."),
|
||||
};
|
||||
const Game::Doc::Function kSetSelectedFactionByID{
|
||||
"Selects a faction in the reputation pane by ID, even when the faction is not listed.",
|
||||
kSetSelectedByIDArgs, {}, nullptr, true};
|
||||
|
||||
const Game::Doc::Function kToggleFactionAtWarByID{
|
||||
"Toggles whether the player is at war with a faction, by ID; the same limits apply as "
|
||||
"for C_Reputation.ToggleFactionAtWar.",
|
||||
kFactionIDArgs, {}, nullptr, true};
|
||||
|
||||
const Game::Doc::Field kIsActiveRets[] = {
|
||||
Game::Doc::Req("isActive", "bool",
|
||||
"False under the Inactive header, and for anything with no reputation slot."),
|
||||
};
|
||||
const Game::Doc::Function kIsFactionActive{
|
||||
"Returns whether the faction at a position in the reputation list is active, that is, "
|
||||
"not under the Inactive header.",
|
||||
kSortIndexArgs, kIsActiveRets};
|
||||
const Game::Doc::Function kIsFactionActiveByID{
|
||||
"Returns whether a faction is active, that is, not under the Inactive header.",
|
||||
kFactionIDArgs, kIsActiveRets, nullptr, true};
|
||||
|
||||
const Game::Doc::Field kIsInactiveRets[] = {
|
||||
Game::Doc::Opt("isInactive", "number", nullptr,
|
||||
"1 when the faction is under the Inactive header, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsFactionInactive{
|
||||
"Returns whether the faction at a position in the reputation list is under the Inactive "
|
||||
"header, as 1 or nil.",
|
||||
kSortIndexArgs, kIsInactiveRets, nullptr, true};
|
||||
|
||||
const Game::Doc::Function kSetFactionInactiveByID{
|
||||
"Moves a faction under the Inactive header of the reputation list, by ID.",
|
||||
kFactionIDArgs, {}, nullptr, true};
|
||||
const Game::Doc::Function kSetFactionActiveByID{
|
||||
"Moves a faction out from under the Inactive header of the reputation list, by ID.",
|
||||
kFactionIDArgs, {}, nullptr, true};
|
||||
const Game::Doc::Function kSetFactionInactive{
|
||||
"Moves the faction at a position in the reputation list under the Inactive header.",
|
||||
kSortIndexArgs, {}, nullptr, true};
|
||||
const Game::Doc::Function kSetFactionActive{
|
||||
"Moves the faction at a position in the reputation list out from under the Inactive "
|
||||
"header.",
|
||||
kSortIndexArgs, {}};
|
||||
|
||||
const Game::Doc::Function kToggleFactionAtWar{
|
||||
"Toggles whether the player is at war with the faction at a position in the reputation "
|
||||
"list.",
|
||||
kSortIndexArgs, {}};
|
||||
const Game::Doc::Function kSetSelectedFaction{
|
||||
"Selects the faction at a position in the reputation list.", kSortIndexArgs, {}};
|
||||
|
||||
const Game::Doc::Field kWatchedFactionDataRets[] = {
|
||||
Game::Doc::Opt("watchedFactionData", "FactionData", nullptr,
|
||||
"Nil when no faction is watched."),
|
||||
};
|
||||
const Game::Doc::Function kGetWatchedFactionData{
|
||||
"Returns the faction shown above the experience bar.", {}, kWatchedFactionDataRets};
|
||||
|
||||
const Game::Doc::Field kStandingsRets[] = {
|
||||
Game::Doc::Req("standings", "table",
|
||||
"Keyed by faction ID; each value is the faction's standing."),
|
||||
};
|
||||
const Game::Doc::Function kGetFactionStandings{
|
||||
"Returns the standing of every faction the character has reputation with, keyed by "
|
||||
"faction ID.",
|
||||
{}, kStandingsRets, nullptr, true};
|
||||
|
||||
const Game::Doc::Field kFactionDataByIDRets[] = {
|
||||
Game::Doc::Opt("factionData", "FactionData", nullptr, "Nil when no faction has the ID."),
|
||||
};
|
||||
const Game::Doc::Function kGetFactionDataByID{
|
||||
"Returns a faction's reputation data by ID, whether or not the faction is in the "
|
||||
"reputation list.",
|
||||
kFactionIDArgs, kFactionDataByIDRets};
|
||||
|
||||
const Game::Doc::Field kFactionDataByIndexRets[] = {
|
||||
Game::Doc::Opt("factionData", "FactionData", nullptr,
|
||||
"Nil when the index is out of range or names the Other or Inactive header."),
|
||||
};
|
||||
const Game::Doc::Function kGetFactionDataByIndex{
|
||||
"Returns the reputation data of the faction at a position in the reputation list.",
|
||||
kSortIndexArgs, kFactionDataByIndexRets};
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex);
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID);
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID);
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex,
|
||||
&kGetFactionIDByIndex);
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID,
|
||||
&kGetFactionInfoByID);
|
||||
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID,
|
||||
&kGetFactionParentID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetWatchedFactionByID",
|
||||
&Script_C_Reputation_SetWatchedFactionByID);
|
||||
&Script_C_Reputation_SetWatchedFactionByID,
|
||||
&kSetWatchedFactionByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetSelectedFactionByID",
|
||||
&Script_C_Reputation_SetSelectedFactionByID);
|
||||
&Script_C_Reputation_SetSelectedFactionByID,
|
||||
&kSetSelectedFactionByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "ToggleFactionAtWarByID",
|
||||
&Script_C_Reputation_ToggleFactionAtWarByID);
|
||||
&Script_C_Reputation_ToggleFactionAtWarByID,
|
||||
&kToggleFactionAtWarByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActive",
|
||||
&Script_C_Reputation_IsFactionActive);
|
||||
&Script_C_Reputation_IsFactionActive, &kIsFactionActive);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActiveByID",
|
||||
&Script_C_Reputation_IsFactionActiveByID);
|
||||
&Script_C_Reputation_IsFactionActiveByID,
|
||||
&kIsFactionActiveByID);
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "IsFactionInactive",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)));
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)),
|
||||
&kIsFactionInactive);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionInactiveByID",
|
||||
&Script_C_Reputation_SetFactionInactiveByID);
|
||||
&Script_C_Reputation_SetFactionInactiveByID,
|
||||
&kSetFactionInactiveByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionActiveByID",
|
||||
&Script_C_Reputation_SetFactionActiveByID);
|
||||
&Script_C_Reputation_SetFactionActiveByID,
|
||||
&kSetFactionActiveByID);
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "SetFactionInactive",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)));
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)),
|
||||
&kSetFactionInactive);
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "SetFactionActive",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)));
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)),
|
||||
&kSetFactionActive);
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "ToggleFactionAtWar",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)));
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)),
|
||||
&kToggleFactionAtWar);
|
||||
// The stock global, mirrored into the namespace — the engine's own
|
||||
// handler already has the Lua C ABI, so it registers as-is.
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Reputation", "SetSelectedFaction",
|
||||
reinterpret_cast<Game::Lua::CFunction>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)));
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)),
|
||||
&kSetSelectedFaction);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetWatchedFactionData",
|
||||
&Script_C_Reputation_GetWatchedFactionData);
|
||||
&Script_C_Reputation_GetWatchedFactionData,
|
||||
&kGetWatchedFactionData);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionStandings",
|
||||
&Script_C_Reputation_GetFactionStandings);
|
||||
&Script_C_Reputation_GetFactionStandings,
|
||||
&kGetFactionStandings);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByID",
|
||||
&Script_C_Reputation_GetFactionDataByID);
|
||||
&Script_C_Reputation_GetFactionDataByID,
|
||||
&kGetFactionDataByID);
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByIndex",
|
||||
&Script_C_Reputation_GetFactionDataByIndex);
|
||||
&Script_C_Reputation_GetFactionDataByIndex,
|
||||
&kGetFactionDataByIndex);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -50,7 +50,15 @@ using GetStanding_t = int(__fastcall *)(int factionID);
|
||||
|
||||
constexpr const char *kEventName = "FACTION_STANDING_CHANGED";
|
||||
|
||||
const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("factionID", "number"),
|
||||
Game::Doc::Req("newStanding", "number", "The faction's standing after the change."),
|
||||
Game::Doc::Req("repGained", "number", "Negative for a loss."),
|
||||
};
|
||||
const Game::Doc::Event kEventDoc{"Reputation", "Fires when the player's reputation changes.",
|
||||
kPayload};
|
||||
|
||||
const Event::Custom::AutoReserve _reserve{kEventName, &kEventDoc};
|
||||
|
||||
// In-flight snapshot of the current rep change. Set on hook entry,
|
||||
// cleared on hook exit. WoW's main thread is the only one that runs
|
||||
@@ -67,9 +75,22 @@ int __fastcall Script_C_Reputation_GetLastStandingChange(void *L) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("factionID", "number", nullptr,
|
||||
"Nil, with the rest, outside a FACTION_STANDING_CHANGED or "
|
||||
"CHAT_MSG_COMBAT_FACTION_CHANGE handler."),
|
||||
Game::Doc::Opt("newStanding", "number", nullptr, "The faction's standing after the change."),
|
||||
Game::Doc::Opt("repGained", "number", nullptr, "Negative for a loss."),
|
||||
};
|
||||
const Game::Doc::Function kGetLastStandingChange{
|
||||
"Returns the reputation change that FACTION_STANDING_CHANGED and "
|
||||
"CHAT_MSG_COMBAT_FACTION_CHANGE report, while their handlers run.",
|
||||
{}, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Reputation", "GetLastStandingChange",
|
||||
&Script_C_Reputation_GetLastStandingChange);
|
||||
&Script_C_Reputation_GetLastStandingChange,
|
||||
&kGetLastStandingChange);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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,494 @@
|
||||
// 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.
|
||||
//
|
||||
// `BackFillMessage(text, r, g, b, ...)`, retail's, adds a message as the
|
||||
// OLDEST — the combat log's refilter walks its history newest to oldest and
|
||||
// fills the frame behind the lines it already shows (Blizzard_CombatLog's
|
||||
// RefilterUpdate). It writes the ring itself; see the function.
|
||||
//
|
||||
// `RemoveMessagesByPredicate(predicate)`, retail's, removes the messages the
|
||||
// predicate selects by their GetMessageInfo values. The ring has no
|
||||
// compaction of its own; 4.3.4's RemoveLines loop is run over it, the
|
||||
// extra-argument tables moving with their records. See the function.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "scrollingmessageframe/DisplayRefreshed.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
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 record → extra-arguments table of the frame whose Lua table is
|
||||
// at stack slot 1, or nil when it has none and `create` is false.
|
||||
void PushRecords(void *L, 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]
|
||||
}
|
||||
|
||||
// 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;
|
||||
PushRecords(L, create); // [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;
|
||||
}
|
||||
|
||||
// AddMessage's colour byte (0x00792A1B..0x00792ACC): the value clamped to
|
||||
// 0..1 (NaN to 1), then trunc(v × 255 + 0.5).
|
||||
uint32_t ColorByte(double value) {
|
||||
double clamped = 1.0;
|
||||
if (value < 0.0)
|
||||
clamped = 0.0;
|
||||
else if (value < 1.0)
|
||||
clamped = static_cast<float>(value);
|
||||
return static_cast<uint32_t>(static_cast<int>(clamped * 255.0 + 0.5)) & 0xFFu;
|
||||
}
|
||||
|
||||
// The colour AddMessage builds from stack slots 3..5 — alpha 0xFF, R, G, B —
|
||||
// or the frame font's when they are not all numbers (0x007929CD..0x00792B45).
|
||||
uint32_t MessageColor(void *L, void *frame) {
|
||||
using namespace Game::Lua;
|
||||
if (IsNumber(L, 3) && IsNumber(L, 4) && IsNumber(L, 5))
|
||||
return 0xFF000000u | (ColorByte(ToNumber(L, 3)) << 16) | (ColorByte(ToNumber(L, 4)) << 8) |
|
||||
ColorByte(ToNumber(L, 5));
|
||||
const void *font = Game::Read<const void *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT);
|
||||
return font != nullptr ? Game::Read<uint32_t>(font, Offsets::OFF_SCROLLINGMESSAGEFRAME_FONT_COLOR) : 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
// `ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...)`, retail's: the
|
||||
// message becomes the OLDEST. Retail's mixin pushes the entry at the back of
|
||||
// its history buffer and drops it when the buffer is full ("Won't overwrite
|
||||
// front"); 4.3.4's AddMessage(..., addToTop) writes the same slot but, full,
|
||||
// overwrites the oldest. The slot is 4.3.4's: once the count has grown, the
|
||||
// free one before the oldest, `(pos - count + 1) mod max` (pos 0 for an empty
|
||||
// ring), and the record is written as the ring writer 0x00788150 writes it —
|
||||
// the text a Storm copy (the ring frees its texts with SMemFree), AddMessage's
|
||||
// colour, shown, the frame's two times, id 0. Nothing already in the ring
|
||||
// moves, so a scrolled-up frame keeps its view; at the bottom the lines are
|
||||
// laid out again from the newest, as the writer does, which shows the new
|
||||
// line when there is room above the others.
|
||||
int __fastcall Script_BackFillMessage(void *L) {
|
||||
using namespace Game::Lua;
|
||||
using StormAlloc_t = void *(__stdcall *)(size_t size, const char *file, int line, int flags);
|
||||
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
|
||||
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
|
||||
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:BackFillMessage";
|
||||
|
||||
const int top = GetTop(L);
|
||||
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
const char *text = IsString(L, 2) ? ToString(L, 2) : nullptr;
|
||||
if (text == nullptr || *text == '\0')
|
||||
return 0;
|
||||
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
int &count = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
if (max <= 0 || count >= max)
|
||||
return 0; // full: the message is older than every kept one
|
||||
const uint32_t color = MessageColor(L, frame);
|
||||
const size_t bytes = std::strlen(text) + 1;
|
||||
char *copy = static_cast<char *>(
|
||||
reinterpret_cast<StormAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC)(bytes, kStormFile, __LINE__, 0));
|
||||
if (copy == nullptr)
|
||||
return 0;
|
||||
std::memcpy(copy, text, bytes);
|
||||
|
||||
++count;
|
||||
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
|
||||
if (pos == -1)
|
||||
pos = 0;
|
||||
int slot = pos - count + 1;
|
||||
if (slot < 0)
|
||||
slot += max;
|
||||
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
|
||||
slot * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
char *&stored = *reinterpret_cast<char **>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
|
||||
if (stored != nullptr)
|
||||
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(stored, kStormFile, __LINE__, 0);
|
||||
stored = copy;
|
||||
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR) = color;
|
||||
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN) = 1;
|
||||
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME) =
|
||||
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME);
|
||||
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME) =
|
||||
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FADE_TIME);
|
||||
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID) = 0;
|
||||
StoreExtras(L, slot, top);
|
||||
|
||||
if (Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0)
|
||||
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, pos);
|
||||
// Scrolled up, the message lands without a layout.
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The record of message `index`, 1 the oldest: (position - count + index),
|
||||
// plus the limit when negative.
|
||||
int RecordOf(void *frame, int index) {
|
||||
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count + index;
|
||||
if (record < 0)
|
||||
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
return record;
|
||||
}
|
||||
|
||||
// Pushes a record's GetMessageInfo values — its text, r, g, b, then its extra
|
||||
// arguments — for the frame whose Lua table is at stack slot 1, and returns
|
||||
// how many.
|
||||
int PushMessage(void *L, void *frame, int record) {
|
||||
using namespace Game::Lua;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
return PushMessage(L, frame, RecordOf(frame, static_cast<int>(index)));
|
||||
}
|
||||
|
||||
// `ScrollingMessageFrame:RemoveMessagesByPredicate(predicate)`, retail's: the
|
||||
// predicate is called with each message's GetMessageInfo values, oldest
|
||||
// first, and the messages it returns true for are removed; the rest keep
|
||||
// their order. Retail's mixin rebuilds its history buffer and re-renders at
|
||||
// the kept scroll offset. The removal is 4.3.4's RemoveLines over 1.12's
|
||||
// ring: every kept record moves forward in ring order to the first free slot
|
||||
// (freeing the text it lands on, nulling the one it left), the position moves
|
||||
// back by the removed count, and the extra-argument tables move with their
|
||||
// records. Then the engine's own layout from the record at the kept offset
|
||||
// (clamped to the oldest) and its re-arm — a layout at the bottom keeps the
|
||||
// fade state, so 4.3.4 re-arms after — or its Clear when nothing is kept.
|
||||
//
|
||||
// The predicate runs for every message before anything moves (retail builds
|
||||
// its new list first). A predicate that errors keeps that message, as
|
||||
// retail's safesecurecall does; the engine's error handler reports it.
|
||||
int __fastcall Script_RemoveMessagesByPredicate(void *L) {
|
||||
using namespace Game::Lua;
|
||||
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
|
||||
using Smf_t = void(__fastcall *)(void *smf);
|
||||
using ScrollOffset_t = int(__fastcall *)(void *smf);
|
||||
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
|
||||
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:RemoveMessagesByPredicate";
|
||||
|
||||
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:RemoveMessagesByPredicate(predicate)", ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
if (count <= 0 || max <= 0)
|
||||
return 0;
|
||||
const int top = GetTop(L);
|
||||
|
||||
// The predicate over every message, through the engine's error handler.
|
||||
int errIdx = 0;
|
||||
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
if (errRef > 0) {
|
||||
RawGetI(L, REGISTRY_INDEX, errRef);
|
||||
errIdx = GetTop(L);
|
||||
}
|
||||
std::vector<uint8_t> remove(static_cast<size_t>(count), 0);
|
||||
int kept = count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
CheckStack(L, 2);
|
||||
PushValue(L, 2);
|
||||
const int args = PushMessage(L, frame, RecordOf(frame, i + 1));
|
||||
if (PCall(L, args, 1, errIdx) == 0 && ToBoolean(L, -1) != 0) {
|
||||
remove[static_cast<size_t>(i)] = 1;
|
||||
--kept;
|
||||
}
|
||||
SetTop(L, -2);
|
||||
}
|
||||
SetTop(L, top);
|
||||
if (kept == count)
|
||||
return 0;
|
||||
|
||||
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
|
||||
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
|
||||
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
|
||||
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
|
||||
PushRecords(L, false);
|
||||
const int table = GetTop(L);
|
||||
const bool extras = Type(L, table) == TYPE_TABLE;
|
||||
int src = pos - count + 1;
|
||||
if (src < 0)
|
||||
src += max;
|
||||
int dst = src;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (!remove[static_cast<size_t>(i)]) {
|
||||
if (src != dst) {
|
||||
uint8_t *to = records + dst * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
uint8_t *from = records + src * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
char *&landedOn = *reinterpret_cast<char **>(to + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
|
||||
if (landedOn != nullptr)
|
||||
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(landedOn, kStormFile, __LINE__, 0);
|
||||
std::memcpy(to, from, Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE);
|
||||
*reinterpret_cast<char **>(from + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT) = nullptr;
|
||||
if (extras) {
|
||||
PushNumber(L, dst);
|
||||
RawGetI(L, table, src);
|
||||
RawSet(L, table); // records[dst] = records[src]
|
||||
PushNumber(L, src);
|
||||
PushNil(L);
|
||||
RawSet(L, table); // records[src] = nil
|
||||
}
|
||||
}
|
||||
dst = (dst + 1) % max;
|
||||
} else if (extras) {
|
||||
PushNumber(L, src);
|
||||
PushNil(L);
|
||||
RawSet(L, table); // records[src] = nil
|
||||
}
|
||||
src = (src + 1) % max;
|
||||
}
|
||||
SetTop(L, top);
|
||||
|
||||
if (kept == 0) {
|
||||
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_CLEAR)(frame);
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
pos += kept - count;
|
||||
if (pos < 0)
|
||||
pos += max;
|
||||
Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT) = kept;
|
||||
// The lines again from the record at the kept offset, then re-armed.
|
||||
const int shown = (offset < kept) ? (offset < 0 ? 0 : offset) : kept - 1;
|
||||
int record = pos - shown;
|
||||
if (record < 0)
|
||||
record += max;
|
||||
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, record);
|
||||
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_REARM_LINES)(frame);
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"AddMessage", &Script_AddMessage},
|
||||
{"BackFillMessage", &Script_BackFillMessage},
|
||||
{"GetMessageInfo", &Script_GetMessageInfo},
|
||||
{"RemoveMessagesByPredicate", &Script_RemoveMessagesByPredicate},
|
||||
};
|
||||
|
||||
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 kBackFillMessageArgs[] = {
|
||||
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 kBackFillMessage{
|
||||
"Adds a message as the oldest, to fill in history behind the messages the frame has. A frame at "
|
||||
"its line limit drops it.",
|
||||
kBackFillMessageArgs, {}};
|
||||
|
||||
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::Field kRemoveMessagesByPredicateArgs[] = {
|
||||
Game::Doc::Req("predicate", "function",
|
||||
"Called with each message's GetMessageInfo values, oldest first; true removes the message."),
|
||||
};
|
||||
const Game::Doc::Function kRemoveMessagesByPredicate{
|
||||
"Removes the messages the predicate returns true for. The rest keep their order, and the frame "
|
||||
"keeps its scroll offset.",
|
||||
kRemoveMessagesByPredicateArgs, {}};
|
||||
|
||||
const Game::Doc::Method kDocs[] = {
|
||||
{"AddMessage", &kAddMessage},
|
||||
{"BackFillMessage", &kBackFillMessage},
|
||||
{"GetMessageInfo", &kGetMessageInfo},
|
||||
{"RemoveMessagesByPredicate", &kRemoveMessagesByPredicate},
|
||||
};
|
||||
|
||||
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, 4, kDocs, 4);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user