mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 14:47:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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), `: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,17 @@ 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).
|
||||
- **ScrollingMessageFrame** `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData` (4.3.4 C; retail
|
||||
has `RemoveMessagesByPredicate`): 1.12 has no ring compaction.
|
||||
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
|
||||
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
|
||||
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
|
||||
|
||||
+190
@@ -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,11 @@ 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: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 +4369,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 +5844,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 +8668,127 @@ 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: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
|
||||
|
||||
+151
-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,82 @@ 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,
|
||||
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 +13082,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 +13144,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
|
||||
|
||||
@@ -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
|
||||
@@ -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,338 @@
|
||||
// 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.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "scrollingmessageframe/DisplayRefreshed.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace ScrollingMessageFrame::MessageInfo {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kFirstExtra = 6; // self, text, r, g, b, then the extra arguments
|
||||
|
||||
// The frame-table → record → extra-arguments table, in the registry of the Lua
|
||||
// state that made it. LUA_NOREF until first use in each state.
|
||||
constexpr int kNoRef = -2;
|
||||
int g_extrasRef = kNoRef;
|
||||
|
||||
// Pushes the extra-argument table of record `record` of the frame whose Lua
|
||||
// table is at stack slot 1, or nil when it has none and `create` is false.
|
||||
void PushExtras(void *L, int record, bool create) {
|
||||
using namespace Game::Lua;
|
||||
if (g_extrasRef == kNoRef) {
|
||||
if (!create) {
|
||||
PushNil(L);
|
||||
return;
|
||||
}
|
||||
NewTable(L);
|
||||
g_extrasRef = Ref(L, REGISTRY_INDEX);
|
||||
}
|
||||
RawGetI(L, REGISTRY_INDEX, g_extrasRef); // [frames]
|
||||
PushValue(L, 1);
|
||||
RawGet(L, -2); // [frames, records]
|
||||
if (Type(L, -1) != TYPE_TABLE && create) {
|
||||
SetTop(L, -2);
|
||||
NewTable(L);
|
||||
PushValue(L, 1);
|
||||
PushValue(L, -2);
|
||||
RawSet(L, -4); // frames[self] = records
|
||||
}
|
||||
Remove(L, -2); // [records]
|
||||
if (Type(L, -1) != TYPE_TABLE)
|
||||
return;
|
||||
RawGetI(L, -1, record); // [records, extras]
|
||||
if (Type(L, -1) != TYPE_TABLE && create) {
|
||||
SetTop(L, -2);
|
||||
NewTable(L);
|
||||
PushNumber(L, record);
|
||||
PushValue(L, -2);
|
||||
RawSet(L, -4); // records[record] = extras
|
||||
}
|
||||
Remove(L, -2); // [extras]
|
||||
}
|
||||
|
||||
// The `n` of the extra-argument table at absolute stack slot `extras`.
|
||||
int ExtraCount(void *L, int extras) {
|
||||
using namespace Game::Lua;
|
||||
PushString(L, "n");
|
||||
RawGet(L, extras);
|
||||
const int n = static_cast<int>(ToNumber(L, -1));
|
||||
SetTop(L, -2);
|
||||
return n;
|
||||
}
|
||||
|
||||
// Stores stack slots kFirstExtra..top as the extra arguments of `record`.
|
||||
void StoreExtras(void *L, int record, int top) {
|
||||
using namespace Game::Lua;
|
||||
const int count = top >= kFirstExtra ? top - kFirstExtra + 1 : 0;
|
||||
PushExtras(L, record, count > 0);
|
||||
const int extras = GetTop(L);
|
||||
if (Type(L, extras) == TYPE_TABLE) {
|
||||
const int was = ExtraCount(L, extras);
|
||||
for (int i = 1; i <= count; ++i) {
|
||||
PushNumber(L, i);
|
||||
PushValue(L, kFirstExtra - 1 + i);
|
||||
RawSet(L, extras);
|
||||
}
|
||||
for (int i = count + 1; i <= was; ++i) {
|
||||
PushNumber(L, i);
|
||||
PushNil(L);
|
||||
RawSet(L, extras);
|
||||
}
|
||||
PushString(L, "n");
|
||||
PushNumber(L, count);
|
||||
RawSet(L, extras);
|
||||
}
|
||||
SetTop(L, -2);
|
||||
}
|
||||
|
||||
int __fastcall Script_AddMessage(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SCROLLINGMESSAGEFRAME_ADD_MESSAGE)(L);
|
||||
const char *text = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
if (text == nullptr || *text == '\0')
|
||||
return 0;
|
||||
void *frame = Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
StoreExtras(L, Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS), top);
|
||||
// Scrolled up, the engine adds the message without a layout.
|
||||
DisplayRefreshed::MarkDirty(frame);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMessageInfo(void *L) {
|
||||
using namespace Game::Lua;
|
||||
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (!IsNumber(L, 2)) {
|
||||
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
|
||||
return 0;
|
||||
}
|
||||
const double index = ToNumber(L, 2);
|
||||
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
|
||||
if (!(index >= 1 && index <= count) || index != static_cast<int>(index))
|
||||
return 0;
|
||||
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count +
|
||||
static_cast<int>(index);
|
||||
if (record < 0)
|
||||
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
|
||||
const auto *message = Game::Read<const uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
|
||||
record * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
|
||||
const uint32_t color = Game::Read<uint32_t>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR);
|
||||
|
||||
PushExtras(L, record, false);
|
||||
const int extras = GetTop(L);
|
||||
const int n = Type(L, extras) == TYPE_TABLE ? ExtraCount(L, extras) : 0;
|
||||
CheckStack(L, 4 + n);
|
||||
PushString(L, Game::Read<const char *>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT));
|
||||
PushNumber(L, ((color >> 16) & 0xFF) / 255.0);
|
||||
PushNumber(L, ((color >> 8) & 0xFF) / 255.0);
|
||||
PushNumber(L, (color & 0xFF) / 255.0);
|
||||
for (int i = 1; i <= n; ++i)
|
||||
RawGetI(L, extras, i);
|
||||
Remove(L, extras);
|
||||
return 4 + n;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"AddMessage", &Script_AddMessage},
|
||||
{"BackFillMessage", &Script_BackFillMessage},
|
||||
{"GetMessageInfo", &Script_GetMessageInfo},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kAddMessageArgs[] = {
|
||||
Game::Doc::Req("text", "string"),
|
||||
Game::Doc::Opt("r", "number", nullptr, "Without r, g and b the message takes the frame's font colour."),
|
||||
Game::Doc::Opt("g", "number"),
|
||||
Game::Doc::Opt("b", "number"),
|
||||
Game::Doc::Vararg("any", "Kept with the message; GetMessageInfo returns them after its colour."),
|
||||
};
|
||||
const Game::Doc::Function kAddMessage{"Adds a message to the frame.", kAddMessageArgs, {}};
|
||||
|
||||
const Game::Doc::Field 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::Method kDocs[] = {
|
||||
{"AddMessage", &kAddMessage},
|
||||
{"BackFillMessage", &kBackFillMessage},
|
||||
{"GetMessageInfo", &kGetMessageInfo},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
// Each registration pass is a new Lua state; the old state's ref is gone.
|
||||
g_extrasRef = kNoRef;
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
|
||||
kMethods, 3, kDocs, 3);
|
||||
}
|
||||
|
||||
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