Compare commits

...
Author SHA1 Message Date
Brues d021ae3e3d docs(faction): describe the reputation functions
Descriptors for GetFactionIDByIndex, GetFactionInfoByID,
GetFactionParentID, the 17 undocumented C_Reputation functions, the
FactionData structure, and the FACTION_STANDING_CHANGED event.
C_Reputation.IsFactionInactive and SetFactionInactive mirror stock
globals, but no Blizzard client has them under C_Reputation, so they
are marked as ClassicAPI extensions.
2026-10-30 02:34:17 -05:00
Brues f4862dc817 docs(equipmentset): describe the C_EquipmentSet functions
Descriptors for the 18 undocumented C_EquipmentSet functions,
GameTooltip:SetEquipmentSet, and the EQUIPMENT_SETS_CHANGED,
EQUIPMENT_SWAP_PENDING and EQUIPMENT_SWAP_FINISHED events. They
describe what the functions return here, which differs from retail in
places: an icon path, not a file ID; numMissing, not numLost;
GetIgnoredSlots lists slot numbers.
2026-10-30 02:34:17 -05:00
Brues 3a6f53f919 docs(aura): describe the C_UnitAuras functions
Descriptors for the 16 undocumented C_UnitAuras functions, plus the
AuraData structure that GetAuraDataByAuraInstanceID already named but
nothing defined. The four positional getters share the Classic Era
UnitAura return list.
2026-10-30 02:34:17 -05:00
Brues 19b299d3df docs(container): describe the C_Container functions
Descriptors for the 17 undocumented C_Container functions and the
ItemQuestInfo structure. SwapItems, MoveItem, AutoStoreItem,
GetContainerItemCharges, GetContainerItemRepairCost and
IsContainerItemOpenable are not in retail's API and are marked as
ClassicAPI extensions.
2026-10-30 02:34:17 -05:00
Brues 77b304ae68 feat(smf): RemoveMessagesByPredicate
ScrollingMessageFrame:RemoveMessagesByPredicate(predicate), retail's: the
predicate is called with each message's GetMessageInfo values, oldest
first, and the messages it returns true for are removed; the rest keep
their order. It covers 4.3.4's RemoveMessagesByAccessID / ByExtraData,
whose int fields the retail-shaped records here do not have.

1.12's ring has no compaction of its own; the removal is 4.3.4's
RemoveLines loop over it: every kept record moves forward in ring order to
the first free slot (freeing the text it lands on, nulling the one it
left), the position moves back by the removed count, and the
extra-argument tables move with their records. Then the engine's own
layout (0x00788750) from the record at the kept scroll offset, clamped to
the oldest, and its re-arm (0x00788B80) - a layout at the bottom keeps the
fade state, so 4.3.4 re-arms after - or its Clear (0x007882B0) when
nothing is kept.

The predicate runs for every message before anything moves, through the
engine's error handler; a predicate that errors keeps that message, as
retail's safesecurecall does. GetMessageInfo shares its record lookup and
value push with it.
2026-10-30 02:14:08 -05:00
Brues c965e49117 feat(smf): BackFillMessage
ScrollingMessageFrame:BackFillMessage(text, r, g, b, ...), retail's: the
message becomes the oldest, for rebuilding a frame from history newest to
oldest (Blizzard_CombatLog's refilter adds each entry with it).

The ring's writer 0x00788150 has one slot rule: the one after the newest.
BackFillMessage writes the slot 4.3.4's AddMessage(..., addToTop) picks —
the free one before the oldest, (pos - count + 1) mod max once the count
has grown — and fills the record as the writer does: a Storm copy of the
text, AddMessage's colour (clamped, x255 + 0.5 truncated, alpha 0xFF; the
font's without r, g, b), shown, the frame's two times, id 0, and the extra
arguments kept for GetMessageInfo. 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.

A frame at its line limit drops the message, as retail's CircularBuffer
PushBack does ("Won't overwrite front"); 4.3.4 overwrites the oldest slot
there, which Blizzard_CombatLog never reaches.
2026-10-30 01:45:47 -05:00
Brues ead52e79ce fix(editbox): caret, selection and click rows follow tall inline icons
A multi-line edit box places its caret, selection pieces and a click's
cursor by rows of one height (font height + spacing, pixel-aligned), while
the render moves a line holding an icon taller than the font, and every
line after it, down by the overflow, and the box grows to match. Below the
first tall-icon line the caret and highlight sat above their text, and
OnCursorChanged reported that y, so ScrollingEditBoxMixin's scroll-into-
view stopped short (pfUI chat copy with faction emblems).

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

Also fixes the "caret positioner" misnomer for 0x77DE70 (the selection-
piece placer) in Offsets.h and the docs.
2026-10-30 01:15:01 -05:00
31 changed files with 1656 additions and 101 deletions
+1 -1
View File
@@ -86,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`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
+11 -3
View File
@@ -4086,9 +4086,17 @@ Each entry still needs the usual check of how the engine does it before building
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
through a redirect of the layout's one line-anchor call, 0x00788975).
- **ScrollingMessageFrame** `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData` (4.3.4 C; retail
has `RemoveMessagesByPredicate`): 1.12 has no ring compaction.
through a redirect of the layout's one line-anchor call, 0x00788975),
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
before the oldest; a full frame drops it, as retail's CircularBuffer
`PushBack` does — 4.3.4 overwrites the oldest there).
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
whose int fields our retail-shaped records do not have). The compaction
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
or Clear (0x007882B0) when nothing is kept.
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
+50
View File
@@ -386,6 +386,8 @@ build instructions.
- [Script binding types](#script-binding-types)
- [`smf:SetHyperlinksEnabled(enabled)` / `smf:GetHyperlinksEnabled()`](#smfsethyperlinksenabledenabled--smfgethyperlinksenabled)
- [`smf:GetMessageInfo(index)`](#smfgetmessageinfoindex)
- [`smf:BackFillMessage(text, r, g, b, ...)`](#smfbackfillmessagetext-r-g-b-)
- [`smf:RemoveMessagesByPredicate(predicate)`](#smfremovemessagesbypredicatepredicate)
- [`smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`](#smfsetscrolloffsetoffset--smfgetscrolloffset--smfgetmaxscrollrange--smfscrollbyamountamount)
- [`smf:SetInsertMode(mode)` / `smf:GetInsertMode()`](#smfsetinsertmodemode--smfgetinsertmode)
- [`smf:AddOnDisplayRefreshedCallback(callback)`](#smfaddondisplayrefreshedcallbackcallback)
@@ -8696,6 +8698,54 @@ for i = 1, ChatFrame1:GetNumMessages() do
end
```
### `smf:BackFillMessage(text, r, g, b, ...)`
On ScrollingMessageFrame. Adds a message as the oldest message of the frame.
`AddMessage` adds the newest message. `BackFillMessage` adds history behind
the messages that the frame already has. An addon that rebuilds a frame from
a history walks the history from the newest entry to the oldest and adds each
entry with this function. The messages that the frame already has do not
move.
- `text`, `r`, `g`, `b` and the extra values work as in `AddMessage`.
Without `r`, `g` and `b`, the message takes the color of the frame's font.
`GetMessageInfo` returns the extra values after the color.
- If the frame is at its line limit (`SetMaxLines`), the function drops the
message, because the message is older than each message that the frame
keeps.
- If the frame is scrolled to the bottom, the frame shows the message when
there is room above its other lines. If the frame is scrolled up, the frame
keeps its view.
```lua
ChatFrame1:BackFillMessage("older", 1, 1, 1)
ChatFrame1:GetMessageInfo(1) -- "older", 1, 1, 1
```
### `smf:RemoveMessagesByPredicate(predicate)`
On ScrollingMessageFrame. Removes the messages that `predicate` selects. The
function calls `predicate(text, r, g, b, ...)` for each message, from the
oldest to the newest, with the values that `GetMessageInfo` returns for the
message. If `predicate` returns true, the function removes the message. The
other messages keep their order. If `predicate` is not a function, the
function gives an error.
- The function calls `predicate` for all messages before it removes any
message. If `predicate` gives an error for a message, the frame keeps that
message.
- The frame keeps its scroll offset (`GetScrollOffset`). If fewer messages
remain than the offset, the offset becomes the oldest message.
- The frame shows the lines that remain at once. If no message remains, the
result is the same as `smf:Clear()`.
```lua
-- remove the messages that were added with the extra value 7
ChatFrame1:RemoveMessagesByPredicate(function(text, r, g, b, id)
return id == 7
end)
```
### `smf:SetScrollOffset(offset)` / `smf:GetScrollOffset()` / `smf:GetMaxScrollRange()` / `smf:ScrollByAmount(amount)`
On ScrollingMessageFrame. The scroll offset is the number of lines that the
+22 -4
View File
@@ -86,9 +86,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
(`49934b0`, `b9d0705`).
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
engine's own input escape (below).
the mouse and the arrow keys treat an icon as one character, Ctrl+C
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
selection and click rows follow the lines a tall icon moves (below;
4.3.4's own rows do not). Typed markup stays literal by the engine's own
input escape (below).
- **Payload** — the full
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
@@ -377,7 +379,7 @@ funnel for exactly 4 callers:
- `Script_GetStringWidth` (0x0079E510)
- the GameTooltip auto-size (0x00530640)
- the editbox caret positioner (0x0077DE70)
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
which feeds auto-width fontstring layout)
@@ -523,6 +525,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
icon with the following character into one step; ours is its own step.
- **Caret x** — `FUN_0077DA80` measures the line through
`FUN_00772AE0`, whose co-hook adds the icons.
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
moves a tall-icon line and every line after it down by the overflow
(`InitializeText`; the emitter co-hook) and the box grows to match (the
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
`Text::InlineTexture::LineIconShift` over the box's own line table: the
click's y is moved onto the uniform row of the icon-aware row under it
before the engine maps it; a selection piece is re-anchored after the
engine placed it, with the engine's own x; the caret likewise, then
`OnCursorChanged` fired once with the moved y through
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
fire muted for the call by emptying the slot), so
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
`RebuildLabel` and `MeasureWindow` count characters with
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
+78 -1
View File
@@ -12857,6 +12857,26 @@ enum Offsets {
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
@@ -12868,6 +12888,17 @@ enum Offsets {
// 0 when empty; the GetCurrentScroll method pushes it.
FUN_SCROLLINGMESSAGEFRAME_SCROLL_TO_OFFSET = 0x00788700,
FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET = 0x00788AC0,
// Clear (`void __thiscall(smf)`, 4.3.4's Clear): count 0, position 0, a
// layout from 0, +0x350 = 0, position -1. The Clear method's body.
FUN_SCROLLINGMESSAGEFRAME_CLEAR = 0x007882B0,
// RearmLines (`void __thiscall(smf)`, 4.3.4's RefreshMessages): every
// shown line back to alpha 0xFF, shown, the frame's two times, and its
// node shown (benilla-re ui/messageframe "Scrolling brings lines back").
// FUN_SMF_DISPLAY_REFRESH (LayoutLines) owns +0x348 (the scroll record)
// and +0x354 (at bottom = record == position) itself, and re-arms only
// when it moves off the bottom; a layout at the bottom keeps the fade
// state, so 4.3.4 re-arms explicitly after a removal's layout.
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
// The frame's OnFrameUpdate override (vtable+0x38, see
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
@@ -13062,6 +13093,51 @@ enum Offsets {
// ending at 0x0077D49D.
PATCH_EDITBOX_TEXT_CHANGED_FIRE = 0x0077D485,
OFF_EDITBOX_ONTEXTCHANGED_SLOT = 0x418,
// --- Edit box rows (EditBox::IconRows) ---
// The mode flags byte (benilla-re ui/editbox): bit 0 autoFocus, 1
// multiLine, 2 numeric, 3 password, 4 ignoreArrows. Every row placer
// below tests it (`TEST byte ptr [ESI+0x318], 0x2 / 0x8`). Not the dirty
// word OFF_EDITBOX_FLAGS (+0x31c).
OFF_EDITBOX_MODE_FLAGS = 0x318,
EDITBOX_MODE_MULTILINE = 0x02,
EDITBOX_MODE_PASSWORD = 0x08,
// The password copy of the text (every byte '*'), the buffer the label
// and the rows read with EDITBOX_MODE_PASSWORD set (4.3.4's
// m_textHidden); OFF_EDITBOX_TEXT otherwise. RebuildLabel 0x0077D4D0
// refreshes it.
OFF_EDITBOX_TEXT_MASKED = 0x334,
// The visible window in bytes: its first byte and its length (4.3.4's
// m_visiblePosition / m_visibleLength). PlaceCaret hides the caret and
// fires nothing for a cursor outside it (0x0077DA89..0x0077DAA7).
OFF_EDITBOX_VISIBLE_START = 0x348,
OFF_EDITBOX_VISIBLE_LENGTH = 0x34C,
// The caret texture (4.3.4's m_cursorTexture), anchored by PlaceCaret.
OFF_EDITBOX_CARET = 0x368,
// OnCursorChanged's script slot; PlaceCaret fires it when non-zero
// (0x0077DD8C).
OFF_EDITBOX_ONCURSORCHANGED_SLOT = 0x428,
// The three row placers, all `__thiscall`: PlaceCaret (no args; 4.3.4's
// UpdateVisibleCursor), PlaceSelectionPiece `(editbox, texture, from, to)`
// `ret 0xC` (UpdateHighlightArea) and GetIndexAtPoint `(editbox, float x,
// float y, int *index)` `ret 0xC` (ConvertCoordinateToIndex). Each steps
// rows of FUN_PIXEL_ALIGN_Y(fontH × scale) + FUN_PIXEL_ALIGN_Y(spacing ×
// scale), the first two divided by the box's OFF_LAYOUT_SCALE (benilla-re
// ui/editbox "The caret" / "Selection"; Ghidra). Co-hooked by
// EditBox::IconRows: tall inline icons move the rows.
FUN_EDITBOX_PLACE_CARET = 0x0077DA80,
FUN_EDITBOX_PLACE_SELECTION_PIECE = 0x0077DE70,
FUN_EDITBOX_INDEX_AT_POINT = 0x0077D0D0,
// "%f%f%f%f" (.data; verified in the file at 0x479CB8) — the format
// PlaceCaret fires OnCursorChanged with through
// FUN_FRAME_RUN_SCRIPT_VARIADIC: x, y, width, height, each a double of
// FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X(FUN_COORD_GET_SCALED_SCREEN_ASPECT()
// × [FLOAT_SCRIPT_COORD_SCALE] × value) — 4.3.4's
// RunOnCursorChangedScript (0x0077DDA0..0x0077DE20).
VAR_SCRIPT_FMT_FFFF = 0x00879CB8,
// 1024.0f (.rdata; verified in the file at 0x3FFD68): the anchor→script
// push factor every script coordinate push multiplies by (the `1024` of
// the GetFont note at FUN_COORD_GET_SCALED_SCREEN_ASPECT).
FLOAT_SCRIPT_COORD_SCALE = 0x007FFD68,
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
// frees the whole buffer and zeroes the container. ClearHistory calls it
@@ -13079,7 +13155,8 @@ enum Offsets {
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
// (0x00530640), the editbox selection-piece placer (0x0077DE70, a whole
// last line's width), and the layout
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
+190 -14
View File
@@ -702,40 +702,216 @@ const Game::Doc::Function kCancelUnitBuff{
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
"SpellGlobals"};
const Game::Doc::Field kAuraDataFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("icon", "string", "The icon's texture path."),
Game::Doc::Req("applications", "number", "The stack count."),
Game::Doc::Req("spellId", "number"),
Game::Doc::Req("dispelName", "string",
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
Game::Doc::Req("isHelpful", "bool"),
Game::Doc::Req("isHarmful", "bool"),
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
Game::Doc::Req("expirationTime", "number",
"When the aura ends, in GetTime() seconds; 0 when not known."),
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
"Nil when the cast was not seen or the caster has no unit token."),
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
Game::Doc::Opt("auraInstanceID", "number", nullptr,
"Nil for an aura that is not in the unit's aura list."),
Game::Doc::Req("charges", "number", "Always 0."),
Game::Doc::Req("maxCharges", "number", "Always 0."),
Game::Doc::Req("timeMod", "number", "Always 1."),
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
"True when any player or a player's pet applied the aura."),
Game::Doc::Req("canApplyAura", "bool",
"True when the player or the pet knows the spell that applies the aura."),
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
Game::Doc::Req("isStealable", "bool",
"True when the player can remove this buff from an enemy."),
Game::Doc::Req("isBossAura", "bool", "Always false."),
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
};
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
"One aura on a unit."};
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
// `Data::PushPositional` is the source of the order.
const Game::Doc::Field kPositionalRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, as the only return, when there is no such aura."),
Game::Doc::Opt("icon", "string"),
Game::Doc::Opt("count", "number"),
Game::Doc::Opt("dispelType", "string", nullptr,
"Empty when the aura has no dispel type."),
Game::Doc::Opt("duration", "number"),
Game::Doc::Opt("expirationTime", "number"),
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
Game::Doc::Opt("isStealable", "bool"),
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
Game::Doc::Opt("spellId", "number"),
Game::Doc::Opt("canApplyAura", "bool"),
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
Game::Doc::Opt("castByPlayer", "bool", nullptr,
"True when a player applied the aura; false for a pet."),
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
};
const Game::Doc::Field kAuraRets[] = {
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
};
const Game::Doc::Field kByIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
};
const Game::Doc::Function kGetAuraDataByIndex{
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
kAuraRets};
const Game::Doc::Function kUnitAura{
"Returns the aura at an index as positional values, without making a table.",
kByIndexArgs, kPositionalRets};
const Game::Doc::Field kLockedIndexArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
};
const Game::Doc::Function kGetBuffDataByIndex{
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Function kGetDebuffDataByIndex{
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
const Game::Doc::Field kLockedFilterArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("index", "luaIndex"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"Further tokens such as PLAYER or RAID; the range is fixed."),
};
const Game::Doc::Function kUnitBuff{
"Returns the buff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Function kUnitDebuff{
"Returns the debuff at an index as positional values, without making a table.",
kLockedFilterArgs, kPositionalRets};
const Game::Doc::Field kSlotsArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters"),
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
Game::Doc::Opt("continuationToken", "number", nullptr,
"The first return of the previous call, to get the next batch."),
Game::Doc::Opt("buf", "table", nullptr,
"A table to fill with the slot IDs instead; the call then returns the "
"token and the count. A ClassicAPI extension."),
};
const Game::Doc::Field kSlotsRets[] = {
Game::Doc::Opt("outContinuationToken", "number", nullptr,
"Nil when this batch reaches the last aura."),
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
};
const Game::Doc::Function kGetAuraSlots{
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
kSlotsRets};
const Game::Doc::Field kBySlotArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
};
const Game::Doc::Function kGetAuraDataBySlot{
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
const Game::Doc::Function kUnitAuraBySlot{
"Returns the aura that a slot ID names as positional values, without making a table.",
kBySlotArgs, kPositionalRets};
const Game::Doc::Field kBySpellIDArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellID", "number"),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetUnitAuraBySpellID{
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
kBySpellIDArgs, kAuraRets};
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
Game::Doc::Req("spellID", "number"),
};
const Game::Doc::Function kGetPlayerAuraBySpellID{
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
const Game::Doc::Field kBySpellNameArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
};
const Game::Doc::Function kGetAuraDataBySpellName{
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
kBySpellNameArgs, kAuraRets};
const Game::Doc::Field kUnitAurasArgs[] = {
Game::Doc::Req("unit", "UnitToken"),
Game::Doc::Opt("filter", "AuraFilters", nullptr,
"With no HELPFUL or HARMFUL token, both are returned."),
};
const Game::Doc::Field kUnitAurasRets[] = {
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
};
const Game::Doc::Function kGetUnitAuras{
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
kUnitAurasRets};
const Game::Doc::Field kDispelColorArgs[] = {
Game::Doc::Opt("dispelName", "string", nullptr,
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
"no-type color."),
};
const Game::Doc::Field kDispelColorRets[] = {
Game::Doc::Req("color", "colorRGBA"),
};
const Game::Doc::Function kGetAuraDispelTypeColor{
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
kDispelColorRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
&Script_GetAuraDataByIndex);
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
&Script_GetBuffDataByIndex);
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
&Script_GetDebuffDataByIndex);
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
&Script_UnitAura);
&Script_UnitAura, &kUnitAura);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
&Script_UnitBuff);
&Script_UnitBuff, &kUnitBuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
&Script_UnitDebuff);
&Script_UnitDebuff, &kUnitDebuff);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
&Script_GetAuraSlots);
&Script_GetAuraSlots, &kGetAuraSlots);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
&Script_GetAuraDataBySlot);
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
&Script_GetAuraDataByAuraInstanceID,
&kGetAuraDataByAuraInstanceID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
&Script_UnitAuraBySlot);
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
&Script_GetUnitAuraBySpellID);
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
&Script_GetPlayerAuraBySpellID);
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
&Script_GetAuraDataBySpellName);
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
&Script_GetUnitAuras);
&Script_GetUnitAuras, &kGetUnitAuras);
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
&Script_GetAuraDispelTypeColor);
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
&kCancelUnitBuff);
}
+15 -1
View File
@@ -193,9 +193,23 @@ int __fastcall Script_RegisterComboDuration(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("spellID", "number"),
Game::Doc::Req("baseSeconds", "number",
"The duration at zero combo points; each point adds a fifth of the "
"difference to maxSeconds."),
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterComboDuration{
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
&Script_RegisterComboDuration);
&Script_RegisterComboDuration, &kRegisterComboDuration);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+22 -1
View File
@@ -946,10 +946,31 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
return 1;
}
const Game::Doc::Field kDurationModArgs[] = {
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
Game::Doc::Req("triggerSchool", "number",
"The school index the trigger must have, or below 0 for any school."),
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
Game::Doc::Req("affectedFamilyFlags", "number",
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
Game::Doc::Req("affectedIcon", "number",
"The SpellIconID the aura must have, or 0 for any icon."),
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
};
const Game::Doc::Field kDurationModRets[] = {
Game::Doc::Req("success", "bool"),
};
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
"that caster lands.",
kDurationModArgs, kDurationModRets, nullptr, true};
void RegisterDurationModLua() {
Game::Lua::RegisterTableFunction("C_UnitAuras",
"RegisterAuraDurationModifierByTrigger",
&Script_RegisterAuraDurationModifierByTrigger);
&Script_RegisterAuraDurationModifierByTrigger,
&kRegisterAuraDurationModifierByTrigger);
}
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
+15 -1
View File
@@ -62,9 +62,23 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Opt("dstBag", "BagIndex", "0",
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
};
const Game::Doc::Function kAutoStoreItem{
"Moves an item and lets the server choose where it goes, merging it into "
"existing stacks first.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
&Script_C_Container_AutoStoreItem);
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+13 -1
View File
@@ -43,9 +43,21 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemCharges{
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
&Script_C_Container_GetContainerItemCharges);
&Script_C_Container_GetContainerItemCharges,
&kGetContainerItemCharges);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -1
View File
@@ -33,9 +33,22 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
return 3;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("itemID", "number"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("startTime", "number",
"When the cooldown started, in GetTime() seconds; 0 when none."),
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
Game::Doc::Req("enable", "number",
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
};
const Game::Doc::Function kGetItemCooldown{
"Returns the cooldown of an item's use effect.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
&Script_C_Container_GetItemCooldown);
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -1
View File
@@ -40,9 +40,22 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("durability", "number", nullptr,
"Nil when the slot is empty or the item has no durability."),
Game::Doc::Opt("maxDurability", "number"),
};
const Game::Doc::Function kGetContainerItemDurability{
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
&Script_C_Container_GetContainerItemDurability);
&Script_C_Container_GetContainerItemDurability,
&kGetContainerItemDurability);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -1
View File
@@ -80,10 +80,25 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
return 2;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("inSet", "bool"),
Game::Doc::Opt("setList", "string", nullptr,
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
};
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
"Returns whether the item in a bag slot is saved in an equipment set, and the "
"names of those sets.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction(
"C_Container", "GetContainerItemEquipmentSetInfo",
&Script_C_Container_GetContainerItemEquipmentSetInfo);
&Script_C_Container_GetContainerItemEquipmentSetInfo,
&kGetContainerItemEquipmentSetInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+33 -3
View File
@@ -182,15 +182,45 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
return 1;
}
const Game::Doc::Field kBagArgs[] = {
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
};
const Game::Doc::Field kNumFreeSlotsRets[] = {
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
Game::Doc::Req("bagFamily", "number",
"The bag's family bitmask; 0 for a bag that takes any item."),
};
const Game::Doc::Function kGetContainerNumFreeSlots{
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
kBagArgs, kNumFreeSlotsRets};
const Game::Doc::Field kFreeSlotsRets[] = {
Game::Doc::Opt("freeSlots", "table", nullptr,
"The empty slot numbers in ascending order; nil when the bag does not exist."),
};
const Game::Doc::Function kGetContainerFreeSlots{
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
const Game::Doc::Field kTotalRets[] = {
Game::Doc::Req("totalFreeSlots", "number"),
};
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
"Returns the number of empty slots in the backpack and the equipped bags that take "
"any item.",
{}, kTotalRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
&Script_C_Container_GetContainerNumFreeSlots);
&Script_C_Container_GetContainerNumFreeSlots,
&kGetContainerNumFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
&Script_C_Container_GetContainerFreeSlots);
&Script_C_Container_GetContainerFreeSlots,
&kGetContainerFreeSlots);
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
&kCalculateTotalNumberOfFreeBagSlots);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+12 -1
View File
@@ -40,9 +40,20 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("hasItem", "bool"),
};
const Game::Doc::Function kHasContainerItem{
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
&Script_C_Container_HasContainerItem);
&Script_C_Container_HasContainerItem, &kHasContainerItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -2
View File
@@ -117,13 +117,27 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
return 1;
}
const Game::Doc::Field kHasRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
};
const Game::Doc::Function kPlayerHasHearthstone{
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("used", "bool",
"True when a hearthstone was found; the cast itself can still fail."),
};
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
&Script_C_Container_PlayerHasHearthstone);
&Script_C_Container_PlayerHasHearthstone,
&kPlayerHasHearthstone);
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
&Script_C_Container_UseHearthstone);
&Script_C_Container_UseHearthstone, &kUseHearthstone);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+11 -1
View File
@@ -41,9 +41,19 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
};
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
&Script_C_Container_GetContainerItemID);
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -2
View File
@@ -57,9 +57,23 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
Game::Doc::Req("count", "number", "From 1 to 255."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kMoveItem{
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
&Script_C_Container_MoveItem);
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
&kMoveItem);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+16 -1
View File
@@ -37,9 +37,24 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("isOpenable", "bool", nullptr,
"Nil when the slot is empty or the item's data is not cached yet."),
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
};
const Game::Doc::Function kIsContainerItemOpenable{
"Returns whether the item in a bag slot can be opened, and whether the player can "
"open it now.",
kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
&Script_C_Container_IsContainerItemOpenable);
&Script_C_Container_IsContainerItemOpenable,
&kIsContainerItemOpenable);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+21 -1
View File
@@ -97,9 +97,29 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
return 1;
}
const Game::Doc::Field kItemQuestInfoFields[] = {
Game::Doc::Req("isQuestItem", "bool"),
Game::Doc::Opt("questID", "number", nullptr,
"The quest the item begins; nil when it begins none."),
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
};
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("questInfo", "ItemQuestInfo",
"Always a table; an empty slot reports no quest."),
};
const Game::Doc::Function kGetContainerItemQuestInfo{
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
&Script_C_Container_GetContainerItemQuestInfo);
&Script_C_Container_GetContainerItemQuestInfo,
&kGetContainerItemQuestInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+13 -1
View File
@@ -37,9 +37,21 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(void *L) {
L, Item::Location::ResolveBag(L, bagID, slotIndex));
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("containerIndex", "BagIndex"),
Game::Doc::Req("slotIndex", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
};
const Game::Doc::Function kGetContainerItemRepairCost{
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
&Script_C_Container_GetContainerItemRepairCost);
&Script_C_Container_GetContainerItemRepairCost,
&kGetContainerItemRepairCost);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+14 -2
View File
@@ -49,9 +49,21 @@ int __fastcall Script_C_Container_SwapItems(void *L) {
return 1;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("srcBag", "BagIndex"),
Game::Doc::Req("srcSlot", "luaIndex"),
Game::Doc::Req("dstBag", "BagIndex"),
Game::Doc::Req("dstSlot", "luaIndex"),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
};
const Game::Doc::Function kSwapItems{
"Swaps the contents of two bag slots without the cursor.", kArgs, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Container", "SwapItems",
&Script_C_Container_SwapItems);
Game::Lua::RegisterTableFunction("C_Container", "SwapItems", &Script_C_Container_SwapItems,
&kSwapItems);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+308
View File
@@ -0,0 +1,308 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Tall inline icons move the edit box's rows.
//
// A multi-line edit box places its caret, its selection pieces and a click's
// cursor by rows of one height — the label's font height plus its spacing,
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
// those rows once a line holds an icon taller than the font: the render moves
// that line and every line after it down by the overflow (4.3.4's
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
// grows by the same amount through the string-height co-hook. So below the
// first tall-icon line the caret and the highlight sat above their text by
// the summed overflow, and OnCursorChanged reported that y, which
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
// puts a tall icon in an edit box.
//
// Each placer keeps its engine code and gets the move the render makes, from
// Text::InlineTexture::LineIconShift over the box's own line table (the
// breaks the render lays out), split by the label's vertical justify as the
// emitter splits it:
// - the click: its y is moved onto the uniform row that the icon-aware row
// under it has, before the engine maps it;
// - a selection piece: re-anchored after the engine placed it, with the
// engine's own x, lower by the move;
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
// moved y through the engine's own forwarder. The engine's fire is muted
// for the call by emptying the script slot: it reads the row y before
// anything can correct it.
// A single-line box has one row and needs nothing: its label is justified
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
#include "Game.h"
#include "Offsets.h"
#include "text/InlineTexture.h"
#include <cstdint>
namespace EditBox::IconRows {
namespace {
using PlaceCaret_t = void(__fastcall *)(void *editBox);
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
using PixelAlignY_t = float(__stdcall *)(float length);
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
int flag);
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
using ScreenAspect_t = float(__cdecl *)();
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
PlaceCaret_t g_origPlaceCaret = nullptr;
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
IndexAtPoint_t g_origIndexAtPoint = nullptr;
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
// A multi-line box's rows: its label, the buffer the line table indexes (the
// '*' copy of a password box) and the table — each line's first byte, the
// last entry the end.
struct Rows {
void *fs = nullptr;
const uint8_t *text = nullptr;
const uint32_t *starts = nullptr;
int count = 0; // entries; lines = count - 1
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
};
bool MultiLineRows(void *editBox, Rows &rows) {
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
return false;
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
? Offsets::OFF_EDITBOX_TEXT_MASKED
: Offsets::OFF_EDITBOX_TEXT);
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
rows.scale > 0.0f;
}
// The line holding byte `pos` — the placers' walk down the table.
int LineOf(const Rows &rows, uint32_t pos) {
int line = 0;
while (pos >= rows.starts[line + 1]) {
if (line >= rows.count - 2)
break;
++line;
}
return line;
}
bool LineShift(const Rows &rows, int line, float &before, float &after) {
before = 0.0f;
after = 0.0f;
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
return end > start &&
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
}
// How far the render moves line `line`'s text below its uniform row: the
// moves of every line above it, and its own text's move.
float ShiftOf(const Rows &rows, int line) {
float shift = 0.0f;
for (int i = 0; i <= line; ++i) {
float before = 0.0f;
float after = 0.0f;
if (!LineShift(rows, i, before, after))
continue;
shift += before;
if (i < line)
shift += after;
}
return shift;
}
// The placers' row in the box's parent units: the label's font height and
// spacing, each pixel-aligned at the box's scale (PlaceCaret
// 0x0077DAEB..0x0077DB0E).
float RowHeight(const Rows &rows) {
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
return align(fontH * rows.scale) + align(spacing * rows.scale);
}
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
static_cast<int>(to - from));
}
// The x the placers anchor byte `pos` of `line` at, and the label point they
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
// False for none, which anchors nothing.
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
const uint32_t start = rows.starts[line];
const uint32_t end = rows.starts[line + 1];
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
case 1: // LEFT
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
return true;
case 2: // CENTER
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
x -= MeasureRun(rows, start, end) * 0.5f;
relPoint = Offsets::FRAMEPOINT_TOP;
return true;
case 4: // RIGHT
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
return true;
default:
return false;
}
}
// Anchors `region` as the placer did, at `y`.
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
void *anchor = AnchorOf(region);
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
relPoint, x, y, flag);
}
// --- the click ---------------------------------------------------------------
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origIndexAtPoint(editBox, edx, x, y, index);
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
return g_origIndexAtPoint(editBox, edx, x, y, index);
// The engine walks rows down from the label's top while the click is
// below the row (strictly), stopping at the last line. Walk the rows as
// the render lays them out: each taller by its line's moves.
const float row = RowHeight(rows);
const float below = rect[2] - y;
int line = 0;
float top = 0.0f; // the icon-aware top of `line`, below the label's top
bool moved = false;
for (;;) {
float before = 0.0f;
float after = 0.0f;
if (LineShift(rows, line, before, after))
moved = true;
const float height = row + (before + after) * rows.scale;
if (!(below > top + height) || line >= rows.count - 2)
break;
top += height;
++line;
}
// Without a move the rows are the engine's own; with one, the click is
// moved onto the middle of the uniform row the engine maps to `line`.
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
}
// --- a selection piece --------------------------------------------------------
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
Rows rows;
if (!MultiLineRows(editBox, rows) || from < 0)
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
const int line = LineOf(rows, static_cast<uint32_t>(from));
const float shift = ShiftOf(rows, line);
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
if (!(shift > 0.0f) || piece == nullptr)
return;
// The piece starts at `from`, clamped to its line.
uint32_t pos = static_cast<uint32_t>(from);
if (pos < rows.starts[line])
pos = rows.starts[line];
float x = 0.0f;
int relPoint = 0;
if (!AnchorX(rows, line, pos, x, relPoint))
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
Anchor(rows, piece, relPoint, x, y, 0);
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
}
// --- the caret ----------------------------------------------------------------
void __fastcall PlaceCaret_h(void *editBox) {
Rows rows;
if (!MultiLineRows(editBox, rows))
return g_origPlaceCaret(editBox);
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
// Outside the window the engine hides the caret and fires nothing.
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
return g_origPlaceCaret(editBox);
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
const float shift = ShiftOf(rows, line);
if (!(shift > 0.0f))
return g_origPlaceCaret(editBox);
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
const uint32_t handler = slot[0];
slot[0] = 0;
g_origPlaceCaret(editBox);
slot[0] = handler;
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
if (caret == nullptr)
return;
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
float x = 0.0f;
int relPoint = 0;
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
Anchor(rows, caret, relPoint, x, y, 1);
if (handler == 0)
return;
// OnCursorChanged(x, y, width, height) as the engine fires it: each
// value by the anchor→script factor, pushed as a double.
void *anchor = AnchorOf(caret);
void **vtable = *static_cast<void ***>(anchor);
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
const double args[4] = {push(x), push(y), push(width), push(height)};
void *L = Game::Lua::State();
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
args[3]);
if (L != nullptr)
Game::Lua::SetTop(L, savedTop);
}
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
reinterpret_cast<void **>(&g_origPlaceCaret)};
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
reinterpret_cast<void *>(&IndexAtPoint_h),
reinterpret_cast<void **>(&g_origIndexAtPoint)};
} // namespace
} // namespace EditBox::IconRows
+152 -18
View File
@@ -543,46 +543,180 @@ int __fastcall Script_UseEquipmentSet(void *L) {
return 1;
}
const Game::Doc::Field kSetIDArgs[] = {
Game::Doc::Req("setID", "number"),
};
const Game::Doc::Field kSlotArgs[] = {
Game::Doc::Req("slot", "luaIndex", "An equipment slot, from 1 to 19."),
};
const Game::Doc::Field kCanUseRets[] = {
Game::Doc::Req("canUseEquipmentSets", "bool", "False while a set swap is in progress."),
};
const Game::Doc::Function kCanUseEquipmentSets{
"Returns whether an equipment set can be used now.", {}, kCanUseRets};
const Game::Doc::Field kNumRets[] = {
Game::Doc::Req("numEquipmentSets", "number"),
};
const Game::Doc::Function kGetNumEquipmentSets{
"Returns the number of equipment sets the character has.", {}, kNumRets};
const Game::Doc::Field kIDsRets[] = {
Game::Doc::Req("setIDs", "table", "The set IDs in the order the sets were created."),
};
const Game::Doc::Function kGetEquipmentSetIDs{
"Returns the IDs of all the character's equipment sets.", {}, kIDsRets};
const Game::Doc::Field kIDArgs[] = {
Game::Doc::Req("name", "cstring", "The exact name; case matters."),
};
const Game::Doc::Field kIDRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when no set has that name."),
};
const Game::Doc::Function kGetEquipmentSetID{
"Returns the ID of the equipment set with a name.", kIDArgs, kIDRets};
const Game::Doc::Field kInfoRets[] = {
Game::Doc::Opt("name", "string", nullptr,
"Nil, with the rest, when the set does not exist."),
Game::Doc::Opt("icon", "string", nullptr, "The icon's texture name."),
Game::Doc::Opt("setID", "number"),
Game::Doc::Opt("isEquipped", "bool", nullptr,
"True when every slot that is not ignored holds what the set saved."),
Game::Doc::Opt("numItems", "number", nullptr,
"numEquipped plus numInInventory plus numMissing."),
Game::Doc::Opt("numEquipped", "number"),
Game::Doc::Opt("numInInventory", "number", nullptr,
"In the bags, or in the bank while the bank is open."),
Game::Doc::Opt("numMissing", "number"),
Game::Doc::Opt("numIgnored", "number"),
};
const Game::Doc::Function kGetEquipmentSetInfo{
"Returns an equipment set's name, icon, and where its items are.", kSetIDArgs,
kInfoRets};
const Game::Doc::Field kIgnoredRets[] = {
Game::Doc::Opt("ignoredSlots", "table", nullptr,
"The numbers of the ignored slots; nil when the set does not exist."),
};
const Game::Doc::Function kGetIgnoredSlots{
"Returns the slots an equipment set leaves alone.", kSetIDArgs, kIgnoredRets};
const Game::Doc::Field kItemIDsRets[] = {
Game::Doc::Opt("itemIDs", "table", nullptr,
"Keyed by slot: the item ID, 0 for an empty slot, 1 for an ignored slot, "
"or -1 when not known."),
};
const Game::Doc::Function kGetItemIDs{
"Returns the item ID an equipment set saved for each slot.", kSetIDArgs, kItemIDsRets};
const Game::Doc::Field kLocationsRets[] = {
Game::Doc::Opt("locations", "table", nullptr,
"Keyed by slot: a packed location, 0 for an empty slot, 1 for an ignored "
"slot, or -1 for a missing item."),
};
const Game::Doc::Function kGetItemLocations{
"Returns where each item of an equipment set is now.", kSetIDArgs, kLocationsRets};
const Game::Doc::Field kCreateArgs[] = {
Game::Doc::Req("name", "cstring", "Shorter than 32 characters, with no line break."),
Game::Doc::Opt("icon", "string", "\"INV_Misc_QuestionMark\"", "A texture name."),
};
const Game::Doc::Field kCreateRets[] = {
Game::Doc::Opt("setID", "number", nullptr, "Nil when the name is already in use."),
};
const Game::Doc::Function kCreateEquipmentSet{
"Saves the equipped items as a new equipment set.", kCreateArgs, kCreateRets};
const Game::Doc::Field kSaveArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Opt("icon", "string", nullptr, "A new texture name; nil keeps the icon."),
};
const Game::Doc::Function kSaveEquipmentSet{
"Saves the equipped items into an existing equipment set.", kSaveArgs, {}};
const Game::Doc::Field kModifyArgs[] = {
Game::Doc::Req("setID", "number"),
Game::Doc::Req("newName", "cstring"),
};
const Game::Doc::Function kModifyEquipmentSet{
"Renames an equipment set; does nothing when another set has the name.", kModifyArgs,
{}};
const Game::Doc::Function kDeleteEquipmentSet{
"Deletes an equipment set and removes it from the action bars.", kSetIDArgs, {}};
const Game::Doc::Function kIgnoreSlotForSave{
"Makes the next save of an equipment set leave a slot alone.", kSlotArgs, {}};
const Game::Doc::Function kUnignoreSlotForSave{
"Makes the next save of an equipment set include a slot again.", kSlotArgs, {}};
const Game::Doc::Field kIsIgnoredRets[] = {
Game::Doc::Req("isSlotIgnored", "bool"),
};
const Game::Doc::Function kIsSlotIgnoredForSave{
"Returns whether the next save of an equipment set leaves a slot alone.", kSlotArgs,
kIsIgnoredRets};
const Game::Doc::Function kClearIgnoredSlotsForSave{
"Makes the next save of an equipment set include every slot.", {}, {}};
const Game::Doc::Field kLockedRets[] = {
Game::Doc::Opt("hasLockedItems", "bool", nullptr, "Nil when the set does not exist."),
};
const Game::Doc::Function kEquipmentSetContainsLockedItems{
"Returns whether an item of an equipment set is locked by a move in progress.",
kSetIDArgs, kLockedRets};
const Game::Doc::Field kUseRets[] = {
Game::Doc::Req("setWasEquipped", "bool",
"True when the swap starts; EQUIPMENT_SWAP_FINISHED gives the result."),
};
const Game::Doc::Function kUseEquipmentSet{
"Equips the items of an equipment set.", kSetIDArgs, kUseRets};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CanUseEquipmentSets",
&Script_CanUseEquipmentSets);
&Script_CanUseEquipmentSets, &kCanUseEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetNumEquipmentSets",
&Script_GetNumEquipmentSets);
&Script_GetNumEquipmentSets, &kGetNumEquipmentSets);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetIDs",
&Script_GetEquipmentSetIDs);
&Script_GetEquipmentSetIDs, &kGetEquipmentSetIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetID",
&Script_GetEquipmentSetID);
&Script_GetEquipmentSetID, &kGetEquipmentSetID);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetEquipmentSetInfo",
&Script_GetEquipmentSetInfo);
&Script_GetEquipmentSetInfo, &kGetEquipmentSetInfo);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetIgnoredSlots",
&Script_GetIgnoredSlots);
&Script_GetIgnoredSlots, &kGetIgnoredSlots);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemIDs",
&Script_GetItemIDs);
&Script_GetItemIDs, &kGetItemIDs);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "GetItemLocations",
&Script_GetItemLocations);
&Script_GetItemLocations, &kGetItemLocations);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "CreateEquipmentSet",
&Script_CreateEquipmentSet);
&Script_CreateEquipmentSet, &kCreateEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "SaveEquipmentSet",
&Script_SaveEquipmentSet);
&Script_SaveEquipmentSet, &kSaveEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ModifyEquipmentSet",
&Script_ModifyEquipmentSet);
&Script_ModifyEquipmentSet, &kModifyEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "DeleteEquipmentSet",
&Script_DeleteEquipmentSet);
&Script_DeleteEquipmentSet, &kDeleteEquipmentSet);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IgnoreSlotForSave",
&Script_IgnoreSlotForSave);
&Script_IgnoreSlotForSave, &kIgnoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UnignoreSlotForSave",
&Script_UnignoreSlotForSave);
&Script_UnignoreSlotForSave, &kUnignoreSlotForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "IsSlotIgnoredForSave",
&Script_IsSlotIgnoredForSave);
&Script_IsSlotIgnoredForSave, &kIsSlotIgnoredForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "ClearIgnoredSlotsForSave",
&Script_ClearIgnoredSlotsForSave);
&Script_ClearIgnoredSlotsForSave,
&kClearIgnoredSlotsForSave);
Game::Lua::RegisterTableFunction("C_EquipmentSet",
"EquipmentSetContainsLockedItems",
&Script_EquipmentSetContainsLockedItems);
&Script_EquipmentSetContainsLockedItems,
&kEquipmentSetContainsLockedItems);
Game::Lua::RegisterTableFunction("C_EquipmentSet", "UseEquipmentSet",
&Script_UseEquipmentSet);
&Script_UseEquipmentSet, &kUseEquipmentSet);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+25 -3
View File
@@ -151,8 +151,30 @@ constexpr int LOC_IGNORED = 1;
// when the swap settles — see
// `EquipmentSet::Pending`. `success` is
// 1 or nil, never 0 (a Lua 0 is true).
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED"};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING"};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED"};
inline const Game::Doc::Event kSetsChangedDoc{
"EquipmentSet",
"Fires when an equipment set is created, saved, renamed, or deleted, or when the slots "
"ignored for the next save change.",
{}};
inline const Game::Doc::Field kSwapPendingPayload[] = {
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapPendingDoc{
"EquipmentSet", "Fires when C_EquipmentSet.UseEquipmentSet has sent all its moves.",
kSwapPendingPayload};
inline const Game::Doc::Field kSwapFinishedPayload[] = {
Game::Doc::Opt("result", "number", nullptr, "1 when the swap succeeded, else nil."),
Game::Doc::Req("setID", "number"),
};
inline const Game::Doc::Event kSwapFinishedDoc{
"EquipmentSet", "Fires when the moves of an equipment set swap are done.",
kSwapFinishedPayload};
inline const Event::Custom::AutoReserve kEvtSetsChanged{"EQUIPMENT_SETS_CHANGED",
&kSetsChangedDoc};
inline const Event::Custom::AutoReserve kEvtSwapPending{"EQUIPMENT_SWAP_PENDING",
&kSwapPendingDoc};
inline const Event::Custom::AutoReserve kEvtSwapFinished{"EQUIPMENT_SWAP_FINISHED",
&kSwapFinishedDoc};
} // namespace EquipmentSet
+12 -1
View File
@@ -218,6 +218,16 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetEquipmentSet", &Script_GameTooltipSetEquipmentSet},
};
const Game::Doc::Field kSetEquipmentSetArgs[] = {
Game::Doc::Req("name", "cstring", "The name of the equipment set."),
};
const Game::Doc::Function kSetEquipmentSet{
"Shows an equipment set in the tooltip: its name and where its items are.",
kSetEquipmentSetArgs, {}};
const Game::Doc::Method g_methodDocs[] = {
{"SetEquipmentSet", &kSetEquipmentSet},
};
} // namespace
static void RegisterLuaFunctions() {
@@ -226,7 +236,8 @@ static void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
static_cast<int>(sizeof(g_methodDocs) / sizeof(g_methodDocs[0])));
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+207 -19
View File
@@ -669,54 +669,242 @@ static int __fastcall Script_C_Reputation_SetWatchedFactionByID(void *L) {
return 0;
}
const Game::Doc::Field kFactionDataFields[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("name", "string"),
Game::Doc::Req("description", "string", "Empty when the faction has none."),
Game::Doc::Req("reaction", "luaIndex", "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Req("currentReactionThreshold", "number",
"The standing at the bottom of the current rank."),
Game::Doc::Req("nextReactionThreshold", "number",
"The standing at the top of the current rank."),
Game::Doc::Req("currentStanding", "number"),
Game::Doc::Req("atWarWith", "bool"),
Game::Doc::Req("canToggleAtWar", "bool",
"False below -3000 standing, when the war state is fixed, or with no "
"reputation slot."),
Game::Doc::Req("isChild", "bool", "Always false."),
Game::Doc::Req("isHeader", "bool", "True when the faction is a header in the reputation list."),
Game::Doc::Req("isHeaderWithRep", "bool", "Always false."),
Game::Doc::Req("isCollapsed", "bool", "True for a collapsed header."),
Game::Doc::Req("isWatched", "bool", "True when the faction is shown above the experience bar."),
Game::Doc::Req("hasBonusRepGain", "bool", "Always false."),
Game::Doc::Req("canSetInactive", "bool",
"True when the faction has a reputation slot and is not a header."),
Game::Doc::Req("isAccountWide", "bool", "Always false."),
};
const Game::Doc::Structure kFactionData{"FactionData", "Reputation", kFactionDataFields,
"A faction's reputation data."};
const Game::Doc::Field kFactionIDArgs[] = {
Game::Doc::Req("factionID", "number"),
};
const Game::Doc::Field kSortIndexArgs[] = {
Game::Doc::Req("factionSortIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexArgs[] = {
Game::Doc::Req("factionIndex", "luaIndex", "A position in the reputation list."),
};
const Game::Doc::Field kFactionIDByIndexRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"0 for a header with no faction of its own, such as Other or Inactive; nil "
"when the index is out of range."),
};
const Game::Doc::Function kGetFactionIDByIndex{
"Returns the faction ID of the entry at a position in the reputation list.",
kFactionIDByIndexArgs, kFactionIDByIndexRets, "ReputationGlobals", true};
const Game::Doc::Field kFactionInfoByIDRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil, with the rest, when no faction has the ID."),
Game::Doc::Opt("description", "string", nullptr, "Empty when the faction has none."),
Game::Doc::Opt("standingID", "number", nullptr, "The rank, from 1 for Hated to 8 for Exalted."),
Game::Doc::Opt("barMin", "number", nullptr, "The standing at the bottom of the current rank."),
Game::Doc::Opt("barMax", "number", nullptr, "The standing at the top of the current rank."),
Game::Doc::Opt("barValue", "number", nullptr, "The current standing."),
Game::Doc::Opt("atWarWith", "number", nullptr,
"1 when the player is at war with the faction, else nil."),
Game::Doc::Opt("canToggleAtWar", "number", nullptr,
"1 when the at-war state can be changed, else nil."),
Game::Doc::Opt("isHeader", "number", nullptr,
"1 when the faction is a header in the reputation list, else nil."),
Game::Doc::Opt("isCollapsed", "number", nullptr, "1 for a collapsed header, else nil."),
Game::Doc::Opt("isWatched", "number", nullptr,
"1 when the faction is shown above the experience bar, else nil."),
};
const Game::Doc::Function kGetFactionInfoByID{
"Returns the same values as GetFactionInfo for a faction ID, whether or not the faction "
"is in the reputation list.",
kFactionIDArgs, kFactionInfoByIDRets, "ReputationGlobals"};
const Game::Doc::Field kFactionParentIDRets[] = {
Game::Doc::Opt("parentFactionID", "number", nullptr,
"0 for a faction with no parent; nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionParentID{
"Returns the ID of a faction's parent faction, such as Alliance for Stormwind.",
kFactionIDArgs, kFactionParentIDRets, "ReputationGlobals", true};
const Game::Doc::Field kSetWatchedArgs[] = {
Game::Doc::Req("factionID", "number",
"0, or an ID with no reputation slot, clears the watched faction; a negative "
"ID does nothing."),
};
const Game::Doc::Function kSetWatchedFactionByID{
"Sets the faction shown above the experience bar.", kSetWatchedArgs, {}};
const Game::Doc::Field kSetSelectedByIDArgs[] = {
Game::Doc::Req("factionID", "number", "0 clears the selection; a negative ID does nothing."),
};
const Game::Doc::Function kSetSelectedFactionByID{
"Selects a faction in the reputation pane by ID, even when the faction is not listed.",
kSetSelectedByIDArgs, {}, nullptr, true};
const Game::Doc::Function kToggleFactionAtWarByID{
"Toggles whether the player is at war with a faction, by ID; the same limits apply as "
"for C_Reputation.ToggleFactionAtWar.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Field kIsActiveRets[] = {
Game::Doc::Req("isActive", "bool",
"False under the Inactive header, and for anything with no reputation slot."),
};
const Game::Doc::Function kIsFactionActive{
"Returns whether the faction at a position in the reputation list is active, that is, "
"not under the Inactive header.",
kSortIndexArgs, kIsActiveRets};
const Game::Doc::Function kIsFactionActiveByID{
"Returns whether a faction is active, that is, not under the Inactive header.",
kFactionIDArgs, kIsActiveRets, nullptr, true};
const Game::Doc::Field kIsInactiveRets[] = {
Game::Doc::Opt("isInactive", "number", nullptr,
"1 when the faction is under the Inactive header, else nil."),
};
const Game::Doc::Function kIsFactionInactive{
"Returns whether the faction at a position in the reputation list is under the Inactive "
"header, as 1 or nil.",
kSortIndexArgs, kIsInactiveRets, nullptr, true};
const Game::Doc::Function kSetFactionInactiveByID{
"Moves a faction under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActiveByID{
"Moves a faction out from under the Inactive header of the reputation list, by ID.",
kFactionIDArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionInactive{
"Moves the faction at a position in the reputation list under the Inactive header.",
kSortIndexArgs, {}, nullptr, true};
const Game::Doc::Function kSetFactionActive{
"Moves the faction at a position in the reputation list out from under the Inactive "
"header.",
kSortIndexArgs, {}};
const Game::Doc::Function kToggleFactionAtWar{
"Toggles whether the player is at war with the faction at a position in the reputation "
"list.",
kSortIndexArgs, {}};
const Game::Doc::Function kSetSelectedFaction{
"Selects the faction at a position in the reputation list.", kSortIndexArgs, {}};
const Game::Doc::Field kWatchedFactionDataRets[] = {
Game::Doc::Opt("watchedFactionData", "FactionData", nullptr,
"Nil when no faction is watched."),
};
const Game::Doc::Function kGetWatchedFactionData{
"Returns the faction shown above the experience bar.", {}, kWatchedFactionDataRets};
const Game::Doc::Field kStandingsRets[] = {
Game::Doc::Req("standings", "table",
"Keyed by faction ID; each value is the faction's standing."),
};
const Game::Doc::Function kGetFactionStandings{
"Returns the standing of every faction the character has reputation with, keyed by "
"faction ID.",
{}, kStandingsRets, nullptr, true};
const Game::Doc::Field kFactionDataByIDRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr, "Nil when no faction has the ID."),
};
const Game::Doc::Function kGetFactionDataByID{
"Returns a faction's reputation data by ID, whether or not the faction is in the "
"reputation list.",
kFactionIDArgs, kFactionDataByIDRets};
const Game::Doc::Field kFactionDataByIndexRets[] = {
Game::Doc::Opt("factionData", "FactionData", nullptr,
"Nil when the index is out of range or names the Other or Inactive header."),
};
const Game::Doc::Function kGetFactionDataByIndex{
"Returns the reputation data of the faction at a position in the reputation list.",
kSortIndexArgs, kFactionDataByIndexRets};
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID);
Game::Lua::RegisterGlobalFunction("GetFactionIDByIndex", &Script_GetFactionIDByIndex,
&kGetFactionIDByIndex);
Game::Lua::RegisterGlobalFunction("GetFactionInfoByID", &Script_GetFactionInfoByID,
&kGetFactionInfoByID);
Game::Lua::RegisterGlobalFunction("GetFactionParentID", &Script_GetFactionParentID,
&kGetFactionParentID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetWatchedFactionByID",
&Script_C_Reputation_SetWatchedFactionByID);
&Script_C_Reputation_SetWatchedFactionByID,
&kSetWatchedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetSelectedFactionByID",
&Script_C_Reputation_SetSelectedFactionByID);
&Script_C_Reputation_SetSelectedFactionByID,
&kSetSelectedFactionByID);
Game::Lua::RegisterTableFunction("C_Reputation", "ToggleFactionAtWarByID",
&Script_C_Reputation_ToggleFactionAtWarByID);
&Script_C_Reputation_ToggleFactionAtWarByID,
&kToggleFactionAtWarByID);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActive",
&Script_C_Reputation_IsFactionActive);
&Script_C_Reputation_IsFactionActive, &kIsFactionActive);
Game::Lua::RegisterTableFunction("C_Reputation", "IsFactionActiveByID",
&Script_C_Reputation_IsFactionActiveByID);
&Script_C_Reputation_IsFactionActiveByID,
&kIsFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "IsFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_IS_FACTION_INACTIVE)),
&kIsFactionInactive);
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionInactiveByID",
&Script_C_Reputation_SetFactionInactiveByID);
&Script_C_Reputation_SetFactionInactiveByID,
&kSetFactionInactiveByID);
Game::Lua::RegisterTableFunction("C_Reputation", "SetFactionActiveByID",
&Script_C_Reputation_SetFactionActiveByID);
&Script_C_Reputation_SetFactionActiveByID,
&kSetFactionActiveByID);
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionInactive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_INACTIVE)),
&kSetFactionInactive);
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetFactionActive",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_FACTION_ACTIVE)),
&kSetFactionActive);
Game::Lua::RegisterTableFunction(
"C_Reputation", "ToggleFactionAtWar",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_FACTION_TOGGLE_AT_WAR)),
&kToggleFactionAtWar);
// The stock global, mirrored into the namespace — the engine's own
// handler already has the Lua C ABI, so it registers as-is.
Game::Lua::RegisterTableFunction(
"C_Reputation", "SetSelectedFaction",
reinterpret_cast<Game::Lua::CFunction>(
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)));
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_SET_SELECTED_FACTION)),
&kSetSelectedFaction);
Game::Lua::RegisterTableFunction("C_Reputation", "GetWatchedFactionData",
&Script_C_Reputation_GetWatchedFactionData);
&Script_C_Reputation_GetWatchedFactionData,
&kGetWatchedFactionData);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionStandings",
&Script_C_Reputation_GetFactionStandings);
&Script_C_Reputation_GetFactionStandings,
&kGetFactionStandings);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByID",
&Script_C_Reputation_GetFactionDataByID);
&Script_C_Reputation_GetFactionDataByID,
&kGetFactionDataByID);
Game::Lua::RegisterTableFunction("C_Reputation", "GetFactionDataByIndex",
&Script_C_Reputation_GetFactionDataByIndex);
&Script_C_Reputation_GetFactionDataByIndex,
&kGetFactionDataByIndex);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+23 -2
View File
@@ -50,7 +50,15 @@ using GetStanding_t = int(__fastcall *)(int factionID);
constexpr const char *kEventName = "FACTION_STANDING_CHANGED";
const Event::Custom::AutoReserve _reserve{kEventName};
const Game::Doc::Field kPayload[] = {
Game::Doc::Req("factionID", "number"),
Game::Doc::Req("newStanding", "number", "The faction's standing after the change."),
Game::Doc::Req("repGained", "number", "Negative for a loss."),
};
const Game::Doc::Event kEventDoc{"Reputation", "Fires when the player's reputation changes.",
kPayload};
const Event::Custom::AutoReserve _reserve{kEventName, &kEventDoc};
// In-flight snapshot of the current rep change. Set on hook entry,
// cleared on hook exit. WoW's main thread is the only one that runs
@@ -67,9 +75,22 @@ int __fastcall Script_C_Reputation_GetLastStandingChange(void *L) {
return 3;
}
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("factionID", "number", nullptr,
"Nil, with the rest, outside a FACTION_STANDING_CHANGED or "
"CHAT_MSG_COMBAT_FACTION_CHANGE handler."),
Game::Doc::Opt("newStanding", "number", nullptr, "The faction's standing after the change."),
Game::Doc::Opt("repGained", "number", nullptr, "Negative for a loss."),
};
const Game::Doc::Function kGetLastStandingChange{
"Returns the reputation change that FACTION_STANDING_CHANGED and "
"CHAT_MSG_COMBAT_FACTION_CHANGE report, while their handlers run.",
{}, kRets, nullptr, true};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Reputation", "GetLastStandingChange",
&Script_C_Reputation_GetLastStandingChange);
&Script_C_Reputation_GetLastStandingChange,
&kGetLastStandingChange);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+279 -14
View File
@@ -42,12 +42,24 @@
// The extra arguments live in a registry table keyed by the frame's Lua table,
// then by record. A record's table is refilled when the record is overwritten,
// so a full ring makes no garbage.
//
// `BackFillMessage(text, r, g, b, ...)`, retail's, adds a message as the
// OLDEST — the combat log's refilter walks its history newest to oldest and
// fills the frame behind the lines it already shows (Blizzard_CombatLog's
// RefilterUpdate). It writes the ring itself; see the function.
//
// `RemoveMessagesByPredicate(predicate)`, retail's, removes the messages the
// predicate selects by their GetMessageInfo values. The ring has no
// compaction of its own; 4.3.4's RemoveLines loop is run over it, the
// extra-argument tables moving with their records. See the function.
#include "Game.h"
#include "Offsets.h"
#include "scrollingmessageframe/DisplayRefreshed.h"
#include <cstdint>
#include <cstring>
#include <vector>
namespace ScrollingMessageFrame::MessageInfo {
@@ -60,9 +72,9 @@ constexpr int kFirstExtra = 6; // self, text, r, g, b, then the extra arguments
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) {
// Pushes the record → extra-arguments table of the frame whose Lua table is
// at stack slot 1, or nil when it has none and `create` is false.
void PushRecords(void *L, bool create) {
using namespace Game::Lua;
if (g_extrasRef == kNoRef) {
if (!create) {
@@ -83,6 +95,13 @@ void PushExtras(void *L, int record, bool create) {
RawSet(L, -4); // frames[self] = records
}
Remove(L, -2); // [records]
}
// Pushes the extra-argument table of record `record` of the frame whose Lua
// table is at stack slot 1, or nil when it has none and `create` is false.
void PushExtras(void *L, int record, bool create) {
using namespace Game::Lua;
PushRecords(L, create); // [records]
if (Type(L, -1) != TYPE_TABLE)
return;
RawGetI(L, -1, record); // [records, extras]
@@ -146,27 +165,113 @@ int __fastcall Script_AddMessage(void *L) {
return 0;
}
int __fastcall Script_GetMessageInfo(void *L) {
// 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;
if (!IsNumber(L, 2)) {
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
const char *text = IsString(L, 2) ? ToString(L, 2) : nullptr;
if (text == nullptr || *text == '\0')
return 0;
}
const double index = ToNumber(L, 2);
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
int &count = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (max <= 0 || count >= max)
return 0; // full: the message is older than every kept one
const uint32_t color = MessageColor(L, frame);
const size_t bytes = std::strlen(text) + 1;
char *copy = static_cast<char *>(
reinterpret_cast<StormAlloc_t>(Offsets::FUN_STORM_SMEM_ALLOC)(bytes, kStormFile, __LINE__, 0));
if (copy == nullptr)
return 0;
std::memcpy(copy, text, bytes);
++count;
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
if (pos == -1)
pos = 0;
int slot = pos - count + 1;
if (slot < 0)
slot += max;
uint8_t *record = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
slot * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
char *&stored = *reinterpret_cast<char **>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (stored != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(stored, kStormFile, __LINE__, 0);
stored = copy;
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR) = color;
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_SHOWN) = 1;
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_VISIBLE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_VISIBLE_TIME);
*reinterpret_cast<uint32_t *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_FADE_TIME) =
Game::Read<uint32_t>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_FADE_TIME);
*reinterpret_cast<int *>(record + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_ID) = 0;
StoreExtras(L, slot, top);
if (Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_AT_BOTTOM) != 0)
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, pos);
// Scrolled up, the message lands without a layout.
DisplayRefreshed::MarkDirty(frame);
return 0;
}
// The record of message `index`, 1 the oldest: (position - count + index),
// plus the limit when negative.
int RecordOf(void *frame, int index) {
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
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);
int record = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS) - count + index;
if (record < 0)
record += Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
return record;
}
// Pushes a record's GetMessageInfo values — its text, r, g, b, then its extra
// arguments — for the frame whose Lua table is at stack slot 1, and returns
// how many.
int PushMessage(void *L, void *frame, int record) {
using namespace Game::Lua;
const auto *message = Game::Read<const uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES) +
record * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
const uint32_t color = Game::Read<uint32_t>(message, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COLOR);
PushExtras(L, record, false);
const int extras = GetTop(L);
const int n = Type(L, extras) == TYPE_TABLE ? ExtraCount(L, extras) : 0;
@@ -181,9 +286,146 @@ int __fastcall Script_GetMessageInfo(void *L) {
return 4 + n;
}
int __fastcall Script_GetMessageInfo(void *L) {
using namespace Game::Lua;
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (!IsNumber(L, 2)) {
Error(L, "Usage: %s:GetMessageInfo(index)", ObjectDisplayName(frame));
return 0;
}
const double index = ToNumber(L, 2);
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
if (!(index >= 1 && index <= count) || index != static_cast<int>(index))
return 0;
return PushMessage(L, frame, RecordOf(frame, static_cast<int>(index)));
}
// `ScrollingMessageFrame:RemoveMessagesByPredicate(predicate)`, retail's: the
// predicate is called with each message's GetMessageInfo values, oldest
// first, and the messages it returns true for are removed; the rest keep
// their order. Retail's mixin rebuilds its history buffer and re-renders at
// the kept scroll offset. The removal is 4.3.4's RemoveLines over 1.12's
// ring: every kept record moves forward in ring order to the first free slot
// (freeing the text it lands on, nulling the one it left), the position moves
// back by the removed count, and the extra-argument tables move with their
// records. Then the engine's own layout from the record at the kept offset
// (clamped to the oldest) and its re-arm — a layout at the bottom keeps the
// fade state, so 4.3.4 re-arms after — or its Clear when nothing is kept.
//
// The predicate runs for every message before anything moves (retail builds
// its new list first). A predicate that errors keeps that message, as
// retail's safesecurecall does; the engine's error handler reports it.
int __fastcall Script_RemoveMessagesByPredicate(void *L) {
using namespace Game::Lua;
using Layout_t = void(__fastcall *)(void *smf, void *edx, int newestRecord);
using Smf_t = void(__fastcall *)(void *smf);
using ScrollOffset_t = int(__fastcall *)(void *smf);
using StormFree_t = int(__stdcall *)(void *ptr, const char *file, int line, int flags);
static const char kStormFile[] = "ClassicAPI ScrollingMessageFrame:RemoveMessagesByPredicate";
void *frame = ResolveTypedObject(L, 1, Offsets::VAR_SCROLLINGMESSAGEFRAME_LUA_TYPE_ID);
if (frame == nullptr)
return 0;
if (Type(L, 2) != TYPE_FUNCTION) {
Error(L, "Usage: %s:RemoveMessagesByPredicate(predicate)", ObjectDisplayName(frame));
return 0;
}
const int count = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT);
const int max = Game::Read<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MAX_MESSAGES);
if (count <= 0 || max <= 0)
return 0;
const int top = GetTop(L);
// The predicate over every message, through the engine's error handler.
int errIdx = 0;
const int errRef = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
if (errRef > 0) {
RawGetI(L, REGISTRY_INDEX, errRef);
errIdx = GetTop(L);
}
std::vector<uint8_t> remove(static_cast<size_t>(count), 0);
int kept = count;
for (int i = 0; i < count; ++i) {
CheckStack(L, 2);
PushValue(L, 2);
const int args = PushMessage(L, frame, RecordOf(frame, i + 1));
if (PCall(L, args, 1, errIdx) == 0 && ToBoolean(L, -1) != 0) {
remove[static_cast<size_t>(i)] = 1;
--kept;
}
SetTop(L, -2);
}
SetTop(L, top);
if (kept == count)
return 0;
// 4.3.4's RemoveLines: compact the kept records forward in ring order.
const int offset = reinterpret_cast<ScrollOffset_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_SCROLL_OFFSET)(frame);
int &pos = Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_POS);
uint8_t *records = Game::Read<uint8_t *>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGES);
PushRecords(L, false);
const int table = GetTop(L);
const bool extras = Type(L, table) == TYPE_TABLE;
int src = pos - count + 1;
if (src < 0)
src += max;
int dst = src;
for (int i = 0; i < count; ++i) {
if (!remove[static_cast<size_t>(i)]) {
if (src != dst) {
uint8_t *to = records + dst * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
uint8_t *from = records + src * Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE;
char *&landedOn = *reinterpret_cast<char **>(to + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT);
if (landedOn != nullptr)
reinterpret_cast<StormFree_t>(Offsets::FUN_STORM_SMEM_FREE)(landedOn, kStormFile, __LINE__, 0);
std::memcpy(to, from, Offsets::SCROLLINGMESSAGEFRAME_MESSAGE_STRIDE);
*reinterpret_cast<char **>(from + Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_TEXT) = nullptr;
if (extras) {
PushNumber(L, dst);
RawGetI(L, table, src);
RawSet(L, table); // records[dst] = records[src]
PushNumber(L, src);
PushNil(L);
RawSet(L, table); // records[src] = nil
}
}
dst = (dst + 1) % max;
} else if (extras) {
PushNumber(L, src);
PushNil(L);
RawSet(L, table); // records[src] = nil
}
src = (src + 1) % max;
}
SetTop(L, top);
if (kept == 0) {
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_CLEAR)(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
pos += kept - count;
if (pos < 0)
pos += max;
Game::Ref<int>(frame, Offsets::OFF_SCROLLINGMESSAGEFRAME_MESSAGE_COUNT) = kept;
// The lines again from the record at the kept offset, then re-armed.
const int shown = (offset < kept) ? (offset < 0 ? 0 : offset) : kept - 1;
int record = pos - shown;
if (record < 0)
record += max;
reinterpret_cast<Layout_t>(Offsets::FUN_SMF_DISPLAY_REFRESH)(frame, nullptr, record);
reinterpret_cast<Smf_t>(Offsets::FUN_SCROLLINGMESSAGEFRAME_REARM_LINES)(frame);
DisplayRefreshed::MarkDirty(frame);
return 0;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"AddMessage", &Script_AddMessage},
{"BackFillMessage", &Script_BackFillMessage},
{"GetMessageInfo", &Script_GetMessageInfo},
{"RemoveMessagesByPredicate", &Script_RemoveMessagesByPredicate},
};
const Game::Doc::Field kAddMessageArgs[] = {
@@ -195,6 +437,18 @@ const Game::Doc::Field kAddMessageArgs[] = {
};
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."),
};
@@ -210,16 +464,27 @@ const Game::Doc::Function kGetMessageInfo{
"when there is no message at that index.",
kGetMessageInfoArgs, kGetMessageInfoRets};
const Game::Doc::Field kRemoveMessagesByPredicateArgs[] = {
Game::Doc::Req("predicate", "function",
"Called with each message's GetMessageInfo values, oldest first; true removes the message."),
};
const Game::Doc::Function kRemoveMessagesByPredicate{
"Removes the messages the predicate returns true for. The rest keep their order, and the frame "
"keeps its scroll offset.",
kRemoveMessagesByPredicateArgs, {}};
const Game::Doc::Method kDocs[] = {
{"AddMessage", &kAddMessage},
{"BackFillMessage", &kBackFillMessage},
{"GetMessageInfo", &kGetMessageInfo},
{"RemoveMessagesByPredicate", &kRemoveMessagesByPredicate},
};
void RegisterLuaFunctions() {
// Each registration pass is a new Lua state; the old state's ref is gone.
g_extrasRef = kNoRef;
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_SCROLLINGMESSAGEFRAME_METHOD_REGISTRY),
kMethods, 2, kDocs, 2);
kMethods, 4, kDocs, 4);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
+26
View File
@@ -2234,6 +2234,32 @@ void PrepareForReload() {
g_nodeBakedColor.clear();
}
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after) {
before = 0.0f;
after = 0.0f;
if (!g_inlineEnabled || !LooksReadable(fs) || !LooksReadable(text) || len <= 0 ||
!HasInlineTexture(text, len))
return false;
FsPenFont pf;
if (!ResolveFsPenFont(fs, pf))
return false;
const float overflow = IconOverflowPx(text, len, pf.fontHPen, pf.layoutScale);
if (!(overflow > 0.0f))
return false;
// The node's justify-V as the block creator (FUN_0044d420) reads the
// style word: TOP (8) wins, then BOTTOM (0x20), else middle — the value
// the emitter reads from the node to split the move.
const uint32_t style = Game::Read<uint32_t>(fs, Offsets::OFF_FONTSTRING_FLAGS);
const int justifyV = (style & 0x8u) ? 0 : (style & 0x20u) ? 2 : 1;
const float move = (justifyV == 1) ? std::floor(overflow * 0.5f) : overflow;
const float toFs = pf.fontHInt / pf.fontHPen; // pen px → the getter's units
if (justifyV != 0)
before = move * toFs;
if (justifyV != 2)
after = move * toFs;
return true;
}
static const Game::ReloadAutoRegister _reloadReg{&PrepareForReload};
} // namespace Text::InlineTexture
+11
View File
@@ -13,6 +13,8 @@
#pragma once
#include <cstdint>
namespace Text::InlineTexture {
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
@@ -20,4 +22,13 @@ namespace Text::InlineTexture {
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
void PrepareForReload();
// The room the tall inline icons on the line [text, text + len) take when
// `fs` renders it, in the font string's own units (its height getter's):
// `before` is how far the render moves that line's text down, `after` how
// far it moves the lines after it — 4.3.4's InitializeText offsets, split by
// the font string's vertical justify (top: none / all; middle: half / half,
// floored; bottom: all / none). Both 0, and false, without an icon taller
// than the font or with the feature off.
bool LineIconShift(void *fs, const uint8_t *text, int len, float &before, float &after);
} // namespace Text::InlineTexture