feat: engine pieces for retail's ScrollBox

What the ScrollBox port needs from the DLL, and three engine bugs it hit.

Fixes:
- GetWidth / GetHeight answer a rect-derived size over the layout scale,
  as 4.3.4 does. 1.12's getters leave the division out, so an anchored
  region under a UI scale read scaled (a 160-high track at 0.91 answered
  145.6, and the scroll bar thumb stopped short). GetRect and
  GetScaledRect use them.
- A <Size>, <Anchor> or <Offset> with only `x` applies. The engine's
  dimension reader decides "no attributes" from `y` alone; a co-hook
  answers as 4.3.4 does. Xml::Anchors' "0" attribute workaround goes.
- KeyValue type="global" walks a dotted path, as retail's own templates
  need (ScrollControllerMixin.Directions.Decrease).

New:
- <Texture atlas="..." useAtlasSize="...">, applied as SetAtlas.
- securecall / securecallfunction / secureexecuterange, as probed on
  retail: results returned, errors to the error handler, the range walk
  goes on after one.
- AddSourceLocationExclude(fileName): GetSourceLocation steps over that
  file's levels.
- region:AdjustPointsOffset(x, y) / ClearPointsOffset(), re-setting each
  anchor through the engine's SetPoint. Retail, probed: Clear zeroes the
  offsets rather than undoing adjustments.
- Enum.InputContext.
This commit is contained in:
Brues
2026-10-27 21:22:22 -05:00
parent 9edce4a9f9
commit dceec8b74c
13 changed files with 562 additions and 54 deletions
+111 -3
View File
@@ -272,9 +272,11 @@ build instructions.
- [`region:GetPoint([index])`](#regiongetpointindex)
- [`region:GetPointByName(point)`](#regiongetpointbynamepoint)
- [`region:ClearPoint(point)`](#regionclearpointpoint)
- [`region:AdjustPointsOffset(x, y)` / `region:ClearPointsOffset()`](#regionadjustpointsoffsetx-y--regionclearpointsoffset)
- [`region:IsDragging()`](#regionisdragging)
- [`region:GetDebugName([preferParentKey])`](#regiongetdebugnamepreferparentkey)
- [`region:GetSourceLocation()`](#regiongetsourcelocation)
- [`AddSourceLocationExclude(fileName)`](#addsourcelocationexcludefilename)
- [`GetMouseFoci()`](#getmousefoci)
- [`CreateFrame(frameType [, name, parent, template, id])`](#createframeframetype--name-parent-template-id)
- [`CreateFrame("Cooldown" [, name, parent, template])`](#createframecooldown--name-parent-template)
@@ -450,6 +452,7 @@ build instructions.
- [Hooks](#hooks)
- [`hooksecurefunc(name, callback)` / `hooksecurefunc(table, name, callback)`](#hooksecurefuncname-callback--hooksecurefunctable-name-callback)
- [`securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`](#securecallfunc---securecallfunctionfunc---secureexecuterangetbl-func-)
- [Input](#input)
- [`IsLeftShiftKeyDown()` / `IsRightShiftKeyDown()`](#isleftshiftkeydown--isrightshiftkeydown)
@@ -964,6 +967,8 @@ build instructions.
- [`KeyValues` in XML](#keyvalues-in-xml)
- [`mixin` in XML](#mixin-in-xml)
- [`intrinsic` frame types in XML](#intrinsic-frame-types-in-xml)
- [ScrollBox and scroll bars](#scrollbox-and-scroll-bars)
- [Atlases in XML](#atlases-in-xml)
- [Masks in XML](#masks-in-xml)
- [`function` and `method` on XML script handlers](#function-and-method-on-xml-script-handlers)
- [`style` on button font elements](#style-on-button-font-elements)
@@ -6608,8 +6613,9 @@ effect as on `GetWidth` and `GetHeight`.
### `region:GetWidth([ignoreRect])` / `region:GetHeight([ignoreRect])`
Returns the width or the height of the region. A region with no set size
returns the size that its anchors give it. If `ignoreRect` is true, the
method returns only the set size, which is 0 for a region with no set size.
returns the size that its anchors give it, in the same units as `GetTop`
and `GetBottom`, at every UI scale. If `ignoreRect` is true, the method
returns only the set size, which is 0 for a region with no set size.
```lua
local f = CreateFrame("Frame", nil, UIParent)
@@ -6703,6 +6709,21 @@ anchor at that point.
local point, relativeTo, relativePoint, x, y = MyFrame:GetPointByName("TOPLEFT")
```
### `region:AdjustPointsOffset(x, y)` / `region:ClearPointsOffset()`
`AdjustPointsOffset` adds `x` and `y` to the offset of each anchor of the
region. `ClearPointsOffset` sets the offset of each anchor to 0. Each
anchor keeps its point, its relative region and its relative point.
```lua
t:SetPoint("CENTER", 5, 7)
t:AdjustPointsOffset(1, -1) -- the anchor is now ("CENTER", 6, 6)
t:ClearPointsOffset() -- the anchor is now ("CENTER", 0, 0)
```
`ClearPointsOffset` does not undo `AdjustPointsOffset`. It removes all of
the offset, also the offset that `SetPoint` gave.
### `region:ClearPoint(point)`
Removes the anchor of the region at `point`. The other anchors stay. The
@@ -6772,6 +6793,19 @@ If `CreateFrame` runs inside `pcall`, the location is the script that called
`pcall`. Child frames that a template adds to a `CreateFrame` frame return
the location of that `CreateFrame` call.
### `AddSourceLocationExclude(fileName)`
Makes [`GetSourceLocation`](#regiongetsourcelocation) skip a script file.
A frame that code in that file creates returns the location of the code
that called into the file. Use it in a file that creates frames for other
code, such as a pool or a factory:
```lua
AddSourceLocationExclude("MyFramePool.lua")
```
`fileName` is the name of the file without its folder. Case is ignored.
### `GetMouseFoci()`
Returns a table of the mouse-enabled frames under the cursor, from top
@@ -10396,6 +10430,27 @@ but they only report "any shift/ctrl/alt" — there's no built-in way to
tell left from right. These seven functions add the missing distinction
plus an `IsModifierKeyDown()` rollup.
### `securecall(func, ...)` / `securecallfunction(func, ...)` / `secureexecuterange(tbl, func, ...)`
Call a function so that an error in it does not stop the code that called
it. The error goes to the error handler (`geterrorhandler()`).
- `securecall(func, ...)` calls `func(...)` and returns its results.
`func` can also be the name of a global function. After an error,
`securecall` returns nothing.
- `securecallfunction(func, ...)` is the same, but `func` must be a
function.
- `secureexecuterange(tbl, func, ...)` calls `func(key, value, ...)` for
each entry of `tbl`. After an error in one call, the other entries still
run. It returns nothing.
```lua
print(securecall(math.max, 1, 9)) -- 9
secureexecuterange({10, 20}, function(key, value, extra)
print(key, value, extra)
end, "x")
```
### `IsLeftShiftKeyDown()` / `IsRightShiftKeyDown()`
### `IsLeftControlKeyDown()` / `IsRightControlKeyDown()`
### `IsLeftAltKeyDown()` / `IsRightAltKeyDown()`
@@ -22584,7 +22639,7 @@ are `string` if you leave them out:
| `number` | The text as a number. |
| `boolean` | `true` if the text starts with `t`, `y` or a digit from `1` to `9` (any case), or is `on` or `enabled`. Otherwise `false`. |
| `nil` | `nil`. This type clears a field that a template set. |
| `global` | The global variable with that name, `_G[value]`. |
| `global` | The global variable with that name, `_G[value]`. A name with dots, such as `ScrollControllerMixin.Directions.Decrease`, reads one field at each dot. |
Numbers keep about seven significant digits, so integers up to 16777216
are exact.
@@ -22676,6 +22731,59 @@ local f = CreateFrame("EventFrame", nil, UIParent)
f:RegisterCallback("OnShow", function() print("shown") end, f)
```
### ScrollBox and scroll bars
ClassicAPI has the ScrollBox system of the later clients: scroll boxes,
their views, data providers, `ScrollUtil` and scroll bars. A scroll box
shows its frames only inside its own area.
| Template | Use |
|---|---|
| `WowScrollBoxList` | A list or a grid of frames from a data provider. |
| `WowScrollBox` | A scroll box with one frame that you make, such as a long text. |
| `WowClassicScrollBar` | A vertical scroll bar with the client's own scroll bar art. |
| `WowTrimScrollBar`, `MinimalScrollBar` | The same as `WowClassicScrollBar`. |
| `ScrollingEditBoxTemplate`, `ScrollingFontTemplate` | An edit box or a font string in a scroll box. |
```lua
local box = CreateFrame("Frame", nil, UIParent, "WowScrollBoxList")
box:SetSize(200, 200)
box:SetPoint("CENTER")
local bar = CreateFrame("EventFrame", nil, UIParent, "WowClassicScrollBar")
bar:SetPoint("TOPLEFT", box, "TOPRIGHT")
bar:SetPoint("BOTTOMLEFT", box, "BOTTOMRIGHT")
local view = CreateScrollBoxListLinearView()
view:SetElementExtent(20)
view:SetElementInitializer("Button", function(button, data)
if not button.text then
button.text = button:CreateFontString(nil, "OVERLAY", "GameFontNormal")
button.text:SetAllPoints()
end
button.text:SetText(data)
end)
ScrollUtil.InitScrollBoxListWithScrollBar(box, bar, view)
box:SetDataProvider(CreateDataProvider({"one", "two", "three"}))
```
The scroll bar arrows do not play a click sound.
### Atlases in XML
A `<Texture>` element takes an `atlas` attribute and a `useAtlasSize`
attribute. They are the same as
`texture:SetAtlas(atlas, useAtlasSize)`:
```xml
<Texture parentKey="Middle" atlas="UI-ClassicScrollBar-Center" useAtlasSize="true"/>
```
The atlas applies after the other content of the element. An atlas that
ClassicAPI does not know keeps the texture that the element set.
A `<Size>` element, an `<Anchor>` or an `<Offset>` can give only `x` or
only `y`. The value that it does not give is 0.
### Masks in XML
A `<Texture>` element takes a `mask` attribute. It is the same as
+3 -1
View File
@@ -11195,7 +11195,9 @@ enum Offsets {
// <AbsDimension> (or <RelDimension>) child, converted to layout units the
// way every XML size and offset is. With neither attribute and no child it
// reports the node to `status` and returns 0. The anchor loader reads each
// <Offset> with it (0x00767800 calls it, benilla-re ui/anchors).
// <Offset> with it (0x00767800 calls it, benilla-re ui/anchors). It
// decides "no attributes" from `y` alone, so a node with only `x` reports
// and answers 0; Xml::Dimension co-hooks it to answer 1 there, as 4.3.4.
FUN_XML_READ_DIMENSION = 0x006F1EB0,
// XMLTree_Load — `XMLTree *__fastcall(const char *data, uint32_t size)`,
// null when the text does not parse; the root node is `*(XMLNode **)tree`.
+132
View File
@@ -0,0 +1,132 @@
// 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/>.
// `securecall(func or name, ...)`, `securecallfunction(func, ...)` and
// `secureexecuterange(tbl, func, ...)` — the later clients' protected calls,
// less taint (1.12 has none). Retail, probed:
// - securecall returns the function's results; a string names a global
// function. An error goes to the error handler and the call returns
// nothing.
// - securecallfunction is securecall without the name lookup: a string is
// a value that cannot be called, so it errors and returns nothing.
// - secureexecuterange calls func(key, value, ...) for each entry of tbl in
// `next` order; an error in one goes to the error handler and the walk
// goes on. It returns nothing.
// The error handler is the one every engine script run passes
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF), as HookScript's wrapper uses.
//
// Registered on both Lua states, like the base-library helpers.
#include "Game.h"
#include "Offsets.h"
namespace BaseLib::SecureCall {
namespace {
// Pushes the engine's script error handler; false (nothing pushed) before
// the engine has set one.
bool PushErrorHandler(void *L) {
const int ref = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
if (ref <= 0)
return false;
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, ref);
return true;
}
// Calls the function at stack index 1 with the values above it, under the
// error handler. Returns the result count, 0 after an error.
int CallProtected(void *L) {
const int nargs = Game::Lua::GetTop(L) - 1;
int errIdx = 0;
if (PushErrorHandler(L)) {
Game::Lua::Insert(L, 1); // [handler, func, args...]
errIdx = 1;
}
if (Game::Lua::PCall(L, nargs, Game::Lua::MULTRET, errIdx) != 0)
return 0;
return Game::Lua::GetTop(L) - errIdx;
}
int __fastcall Script_securecall(void *L) {
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING) {
Game::Lua::PushValue(L, 1);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
Game::Lua::Remove(L, 1);
Game::Lua::Insert(L, 1);
}
return CallProtected(L);
}
int __fastcall Script_securecallfunction(void *L) { return CallProtected(L); }
int __fastcall Script_secureexecuterange(void *L) {
const int n = Game::Lua::GetTop(L); // tbl, func, extra args 3..n
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
return 0;
Game::Lua::PushNil(L);
while (Game::Lua::Next(L, 1) != 0) { // [.., key, value]
int errIdx = 0;
if (PushErrorHandler(L))
errIdx = n + 3;
Game::Lua::PushValue(L, 2);
Game::Lua::PushValue(L, n + 1);
Game::Lua::PushValue(L, n + 2);
for (int i = 3; i <= n; ++i)
Game::Lua::PushValue(L, i);
Game::Lua::PCall(L, n, 0, errIdx); // key, value and the n - 2 extras
Game::Lua::SetTop(L, n + 1); // the key, for the next step
}
return 0;
}
const Game::Doc::Field kCallArgs[] = {
Game::Doc::Req("func", "function", "The function to call, or for securecall the name of a global function."),
Game::Doc::Opt("...", "any", nullptr, "The arguments for the function."),
};
const Game::Doc::Field kCallRets[] = {
Game::Doc::Opt("...", "any", nullptr, "The function's results, or nothing after an error."),
};
const Game::Doc::Field kRangeArgs[] = {
Game::Doc::Req("tbl", "table", "The table whose entries are visited."),
Game::Doc::Req("func", "function", "Called as func(key, value, ...) for each entry."),
Game::Doc::Opt("...", "any", nullptr, "Extra arguments for each call."),
};
const Game::Doc::Function kSecureCall{
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
kCallArgs, kCallRets, "LuaGlobals"};
const Game::Doc::Function kSecureCallFunction{
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
kCallArgs, kCallRets, "LuaGlobals"};
const Game::Doc::Function kSecureExecuteRange{
"Calls a function for each entry of a table. An error goes to the error handler and the other entries still run.",
kRangeArgs, {}, "LuaGlobals"};
void RegisterInGame() {
Game::Lua::RegisterGlobalFunction("securecall", &Script_securecall, &kSecureCall);
Game::Lua::RegisterGlobalFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
Game::Lua::RegisterGlobalFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
}
void RegisterGlue() {
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
}
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
} // namespace
} // namespace BaseLib::SecureCall
+41
View File
@@ -0,0 +1,41 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `Enum.InputContext` — retail's InputConstants enumeration: the input an
// edit box's OnCursorChanged names as its `context`. ScrollingEditBox
// (Blizzard_SharedXML) compares it with Enum.InputContext.Keyboard.
#include "Game.h"
namespace EditBox::InputContext {
namespace {
const Game::Lua::EnumIntegerEntry kEntries[] = {
{"None", 0},
{"Keyboard", 1},
{"Mouse", 2},
{"GamePad", 3},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries,
sizeof(kEntries) / sizeof(kEntries[0]), "InputConstants");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace EditBox::InputContext
+38 -32
View File
@@ -27,9 +27,9 @@
// on the REGION base method registry, so they resolve on frames,
// buttons, textures, fontstrings — everything.
// - `region:GetWidth([ignoreRect])` / `region:GetHeight([ignoreRect])` —
// the engine's getters with the later argument. The engine answers the
// explicit size, and the rect's extent only when that size is 0; with
// ignoreRect a 0 stays 0.
// 4.3.4's getters: the explicit size, and the rect's extent over the
// layout scale only when that size is 0; with ignoreRect a 0 stays 0.
// 1.12's own leave out the division (see PushExtent).
// - `region:IsMouseOver([top, bottom, left, right])` — cursor-in-rect
// test with optional per-edge offsets, also on the REGION registry.
// - `region:GetRect()` — left, bottom, width, height, composed from the
@@ -98,41 +98,47 @@ int __fastcall Script_SetSize(void *L) {
}
using LayoutExtent_t = float(__thiscall *)(void *layout);
using LayoutDirty_t = int(__fastcall *)(void *layout);
using LayoutResize_t = void(__fastcall *)(void *layout, void *edx, int force);
using LayoutGetRect_t = int(__fastcall *)(void *layout, void *edx, float *out);
// Pushes the engine getter's answer for `self` (index 1), or 0 when
// ignoreRect is set and the explicit size in `slot` is 0 — the one case the
// engine answers from the rect instead.
void PushExtent(void *L, bool ignoreRect, int slot, uintptr_t getter) {
if (ignoreRect) {
void *self = Game::Lua::ResolveRegion(L);
if (self == nullptr)
return;
void *layout = static_cast<uint8_t *>(self) + Offsets::OFF_REGION_ANCHOR;
auto *vtable = Game::Read<uintptr_t *>(layout, 0);
if (reinterpret_cast<LayoutExtent_t>(vtable[slot / sizeof(uintptr_t)])(layout) == 0.0f) {
Game::Lua::PushNumber(L, 0.0);
return;
}
// 4.3.4's Script_GetWidth / Script_GetHeight for `self` (index 1): the
// explicit size from layout `slot`, or when it is 0 and ignoreRect is not set,
// the rect's span over the layout scale; pushed in script units. 1.12's own
// getters (FUN_SCRIPT_REGION_GETWIDTH / _GETHEIGHT) leave out the division,
// so an anchored region under a UI scale reads scaled: a 160-high track at
// scale 0.91 answered 145.6 while its GetTop - GetBottom was 160. Their other
// steps, and the edge getters' division, are the engine's.
void PushExtent(void *L, bool ignoreRect, int slot, bool vertical) {
void *self = Game::Lua::ResolveRegion(L);
if (self == nullptr)
return;
void *layout = static_cast<uint8_t *>(self) + Offsets::OFF_REGION_ANCHOR;
auto *vtable = Game::Read<uintptr_t *>(layout, 0);
float extent = reinterpret_cast<LayoutExtent_t>(vtable[slot / sizeof(uintptr_t)])(layout);
if (extent == 0.0f && !ignoreRect) {
if (reinterpret_cast<LayoutDirty_t>(Offsets::FUN_REGION_LAYOUT_DIRTY)(layout) != 0)
reinterpret_cast<LayoutResize_t>(Offsets::FUN_LAYOUT_RESIZE)(layout, nullptr, 1);
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // {bottom, left, top, right}
if (reinterpret_cast<LayoutGetRect_t>(Offsets::FUN_REGION_GET_RECT)(layout, nullptr, rect) != 0)
extent = (vertical ? rect[2] - rect[0] : rect[3] - rect[1]) /
Game::Read<float>(self, Offsets::OFF_REGION_EFFECTIVE_SCALE);
}
const int top = Game::Lua::GetTop(L);
Game::Lua::PushValue(L, 1);
CallScript(getter, L); // (.., self, value)
Game::Lua::Remove(L, top + 1);
Game::Lua::PushNumber(L, Game::InternalToPixel(extent));
}
// `region:GetWidth([ignoreRect])` / `region:GetHeight([ignoreRect])`.
int __fastcall Script_GetWidth(void *L) {
const bool ignoreRect = Game::Lua::ToBoolean(L, 2) != 0;
Game::Lua::SetTop(L, 1);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_WIDTH, Offsets::FUN_SCRIPT_REGION_GETWIDTH);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_WIDTH, false);
return 1;
}
int __fastcall Script_GetHeight(void *L) {
const bool ignoreRect = Game::Lua::ToBoolean(L, 2) != 0;
Game::Lua::SetTop(L, 1);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_HEIGHT,
Offsets::FUN_SCRIPT_REGION_GETHEIGHT);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_HEIGHT, true);
return 1;
}
@@ -140,9 +146,8 @@ int __fastcall Script_GetHeight(void *L) {
int __fastcall Script_GetSize(void *L) {
const bool ignoreRect = Game::Lua::ToBoolean(L, 2) != 0;
Game::Lua::SetTop(L, 1); // (self)
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_WIDTH, Offsets::FUN_SCRIPT_REGION_GETWIDTH);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_HEIGHT,
Offsets::FUN_SCRIPT_REGION_GETHEIGHT);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_WIDTH, false);
PushExtent(L, ignoreRect, Offsets::VTBL_LAYOUT_GET_HEIGHT, true);
return 2;
}
@@ -234,7 +239,8 @@ int __fastcall Script_IsMouseOver(void *L) {
// ---- GetRect (Region base — all region types) ------------------------------
// `region:GetRect()` → left, bottom, width, height. Composed from the
// engine's own Region getters (so all UI-scale conversion is its code).
// engine's edge getters and this file's GetWidth / GetHeight (the engine's
// leave the layout scale in a rect-derived size).
// Matches retail: returns nothing for a region with no resolved rect —
// gated on GetLeft, which pushes nil when the position isn't resolved (a
// size-only region with no anchor has width/height but no rect).
@@ -243,8 +249,8 @@ int __fastcall Script_GetRect(void *L) {
if (!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETLEFT, &left))
return 0;
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETBOTTOM, &bottom);
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETWIDTH, &width);
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETHEIGHT, &height);
CallRegionNumber(L, reinterpret_cast<uintptr_t>(&Script_GetWidth), &width);
CallRegionNumber(L, reinterpret_cast<uintptr_t>(&Script_GetHeight), &height);
Game::Lua::SetTop(L, 1);
Game::Lua::PushNumber(L, left);
Game::Lua::PushNumber(L, bottom);
@@ -263,8 +269,8 @@ int __fastcall Script_GetScaledRect(void *L) {
if (!CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETLEFT, &left))
return 0;
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETBOTTOM, &bottom);
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETWIDTH, &width);
CallRegionNumber(L, Offsets::FUN_SCRIPT_REGION_GETHEIGHT, &height);
CallRegionNumber(L, reinterpret_cast<uintptr_t>(&Script_GetWidth), &width);
CallRegionNumber(L, reinterpret_cast<uintptr_t>(&Script_GetHeight), &height);
Game::Lua::SetTop(L, 1);
Game::Lua::PushNumber(L, left * scale);
Game::Lua::PushNumber(L, bottom * scale);
+63
View File
@@ -17,6 +17,8 @@
// `point`, nothing when that slot is empty
// region:ClearPoint(point) removes the anchor at `point`
// region:IsRectValid() true when the region has a rect
// region:AdjustPointsOffset(x, y) adds (x, y) to every anchor's offset
// region:ClearPointsOffset() sets every anchor's offset to 0
//
// A layout keeps one anchor per point, in the slot of that point's index
// (benilla-re ui/anchors "Point slots"), so a point name selects a slot
@@ -28,6 +30,13 @@
// with 1, and NULL the slot. Like ClearAllPoints it does not lay the region
// out; the next anchor or size change does.
//
// AdjustPointsOffset and ClearPointsOffset re-set each anchor through the
// engine's SetPoint (FUN_REGION_SET_POINT) with the anchor's own relative
// frame and point and the new offset, so the region lays out as a script
// SetPoint would. Retail, probed: SetPoint("CENTER", 5, 7), then
// AdjustPointsOffset(1, -1) reads back (6, 6), and ClearPointsOffset then
// reads back (0, 0) - it zeroes the offsets, it does not undo adjustments.
//
// IsRectValid takes the steps of the stock edge getters (Script GetLeft):
// lay out a pending region now, then answer the rect reader's valid bit, so
// it is true exactly when GetLeft answers a number.
@@ -49,6 +58,8 @@ using DeleteNode_t = void(__thiscall *)(void *node, int flags);
using LayoutDirty_t = int(__fastcall *)(void *layout);
using Resize_t = void(__fastcall *)(void *layout, void *edx, int force);
using GetRect_t = int(__fastcall *)(void *layout, void *edx, float *out);
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint,
float x, float y, int flag);
// The point named by argument 2, or -1 after raising the usage error.
int PointArg(void *L, void *region, const char *usage) {
@@ -98,6 +109,47 @@ int __fastcall Script_ClearPoint(void *L) {
return 0;
}
// Re-sets every anchor with its offset moved by (dx, dy) in layout units, or
// with no offset when `zero`.
void ShiftPoints(void *region, float dx, float dy, bool zero) {
Frame::GetPointGuard::CleanDanglingAnchors(region);
for (int point = 0; point < Offsets::REGION_ANCHOR_SLOT_COUNT; ++point) {
uint8_t *node = Game::Read<uint8_t *>(region, Offsets::OFF_REGION_ANCHOR_ARRAY + point * 4);
if (node == nullptr)
continue;
auto *vtable = Game::Read<uintptr_t *>(node, 0);
void *relative = reinterpret_cast<GetRelative_t>(
vtable[Offsets::VTBL_ANCHOR_NODE_GET_RELATIVE / sizeof(uintptr_t)])(node);
const int relativePoint = Game::Read<int>(node, Offsets::OFF_ANCHOR_NODE_RELATIVE_POINT);
const float x = zero ? 0.0f : Game::Read<float>(node, Offsets::OFF_ANCHOR_NODE_X) + dx;
const float y = zero ? 0.0f : Game::Read<float>(node, Offsets::OFF_ANCHOR_NODE_Y) + dy;
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(
static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR, point, relative,
relativePoint, x, y, 1);
}
}
int __fastcall Script_AdjustPointsOffset(void *L) {
void *region = Game::Lua::ResolveRegion(L);
if (region == nullptr)
return 0;
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
Game::Lua::Error(L, "Usage: %s:AdjustPointsOffset(x, y)", Game::Lua::ObjectDisplayName(region));
return 0;
}
ShiftPoints(region, Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2))),
Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3))), false);
return 0;
}
int __fastcall Script_ClearPointsOffset(void *L) {
void *region = Game::Lua::ResolveRegion(L);
if (region == nullptr)
return 0;
ShiftPoints(region, 0.0f, 0.0f, true);
return 0;
}
int __fastcall Script_IsRectValid(void *L) {
void *region = Game::Lua::ResolveRegion(L);
if (region == nullptr)
@@ -115,6 +167,8 @@ const Game::Lua::FrameMethodEntry g_methods[] = {
{"GetPointByName", &Script_GetPointByName},
{"ClearPoint", &Script_ClearPoint},
{"IsRectValid", &Script_IsRectValid},
{"AdjustPointsOffset", &Script_AdjustPointsOffset},
{"ClearPointsOffset", &Script_ClearPointsOffset},
};
const Game::Doc::Field kPointArg[] = {
@@ -138,7 +192,16 @@ const Game::Doc::Function kClearPoint{"Removes the region's anchor at a point.",
const Game::Doc::Function kIsRectValid{
"Returns true when the region's position and size are resolved from its anchors.",
{}, kValidRets};
const Game::Doc::Field kOffsetArgs[] = {
Game::Doc::Req("x", "uiUnit"),
Game::Doc::Req("y", "uiUnit"),
};
const Game::Doc::Function kAdjustPointsOffset{"Adds an offset to every anchor of the region.", kOffsetArgs,
{}};
const Game::Doc::Function kClearPointsOffset{"Sets the offset of every anchor of the region to 0.", {}, {}};
const Game::Doc::Method g_methodDocs[] = {
{"AdjustPointsOffset", &kAdjustPointsOffset},
{"ClearPointsOffset", &kClearPointsOffset},
{"GetPointByName", &kGetPointByName},
{"ClearPoint", &kClearPoint},
{"IsRectValid", &kIsRectValid},
+51 -1
View File
@@ -40,6 +40,12 @@
// Textures and font strings have no record of their own; Get walks to their
// parent frame — retail's behavior for script-created regions.
//
// AddSourceLocationExclude(fileName) names a script file whose levels the
// walk steps over, so a frame a factory file makes (retail's FrameFactory.lua
// and Pools.lua exclude themselves) is the caller's. A level matches when its
// file name, the part of its path after the last separator, is fileName,
// ignoring case. The names are dropped with the Lua state that added them.
//
// Always on: retail gates the file-name lookup behind the
// enableSourceLocationLookup CVar ("UnknownFile" otherwise), a cost its Lua
// has and 5.0's two field reads do not.
@@ -56,6 +62,7 @@
#include <cstdio>
#include <string>
#include <unordered_map>
#include <vector>
namespace Frame::SourceLocation {
@@ -78,8 +85,23 @@ constexpr int kMaxScopes = 32; // CreateFrame -> OnLoad -> CreateFrame nests sha
Scope g_scopes[kMaxScopes];
int g_scopeDepth = 0;
std::vector<std::string> g_excludes;
bool Excluded(const char *source) {
if (g_excludes.empty())
return false;
const char *name = source;
for (const char *p = source; *p != '\0'; ++p)
if (*p == '\\' || *p == '/')
name = p + 1;
for (const std::string &e : g_excludes)
if (Game::StrCmpI(name, e.c_str()) == 0)
return true;
return false;
}
// "file:line" of the nearest Lua function above the running C function on
// L's call stack. False when there is none.
// L's call stack that is not in an excluded file. False when there is none.
bool CaptureCaller(void *L, char *out, size_t size) {
auto getStack = reinterpret_cast<GetStack_t>(Offsets::LUA_GET_STACK);
auto getInfo = reinterpret_cast<GetInfo_t>(Offsets::LUA_GET_INFO);
@@ -93,6 +115,8 @@ bool CaptureCaller(void *L, char *out, size_t size) {
const char *source = Game::Read<const char *>(ar, Offsets::OFF_LUA_DEBUG_SOURCE);
if (source == nullptr)
return false;
if (Excluded(source))
continue;
char name[256];
reinterpret_cast<ChunkId_t>(Offsets::LUAO_CHUNKID)(name, source, sizeof(name));
std::snprintf(out, size, "%s:%d", name, line);
@@ -138,10 +162,34 @@ int __fastcall Script_GetSourceLocation(void *L) {
return 1;
}
int __fastcall Script_AddSourceLocationExclude(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: AddSourceLocationExclude(fileName)");
return 0;
}
const char *name = Game::Lua::ToString(L, 1);
for (const std::string &e : g_excludes)
if (Game::StrCmpI(name, e.c_str()) == 0)
return 0;
g_excludes.emplace_back(name);
return 0;
}
void PrepareForReload() { g_excludes.clear(); }
const Game::ReloadAutoRegister _reload{&PrepareForReload};
const Game::Lua::FrameMethodEntry g_methods[] = {
{"GetSourceLocation", &Script_GetSourceLocation},
};
const Game::Doc::Field kExcludeArgs[] = {
Game::Doc::Req("fileName", "string", "A script file name, such as \"FrameFactory.lua\"."),
};
const Game::Doc::Function kAddSourceLocationExclude{
"Makes GetSourceLocation skip the named script file and report the code that called into it.",
kExcludeArgs, {}, "FrameScriptGlobals"};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("location", "string", nullptr,
"\"file:line\" of the CreateFrame call that made the frame, or the XML file "
@@ -156,6 +204,8 @@ const Game::Doc::Method g_methodDocs[] = {
};
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("AddSourceLocationExclude", &Script_AddSourceLocationExclude,
&kAddSourceLocationExclude);
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), g_methods,
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])), g_methodDocs,
+13
View File
@@ -567,4 +567,17 @@ void PrepareForReload() {
g_misses.clear();
}
void SetFromXml(void *region, const char *name, bool useAtlasSize) {
void *L = Game::Lua::State();
if (L == nullptr || region == nullptr || name == nullptr || *name == '\0')
return;
const int top = Game::Lua::GetTop(L);
Game::Lua::PushCClosure(L, &Script_SetAtlas, 0);
Game::Lua::PushObject(L, region);
Game::Lua::PushString(L, name);
Game::Lua::PushBool(L, useAtlasSize);
Game::Lua::PCall(L, 3, 0, 0);
Game::Lua::SetTop(L, top);
}
} // namespace Texture::Atlas
+5
View File
@@ -62,4 +62,9 @@ void RecordMiss(const char *name);
// across the gap.
void PrepareForReload();
// The XML `<Texture atlas="name" useAtlasSize="true">`: texture:SetAtlas(name,
// useAtlasSize) on `region`, run as its own Lua call so the caller's stack is
// left alone.
void SetFromXml(void *region, const char *name, bool useAtlasSize);
} // namespace Texture::Atlas
+2 -12
View File
@@ -53,7 +53,6 @@ namespace {
using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
using ReadDimension_t = int(__fastcall *)(const void *node, float *x, float *y, void *status);
using SetAttribute_t = void(__thiscall *)(void *node, const char *name, const char *value);
using GetRelative_t = void *(__thiscall *)(void *anchorNode);
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint,
float x, float y, int flag);
@@ -151,20 +150,11 @@ bool ApplyAnchor(void *object, const void *anchor, void *status) {
return true;
}
// An attribute the anchor leaves out is 0; giving it "0" keeps the reader
// from reporting a missing dimension when the other one is present.
// An attribute the anchor leaves out is 0 (Xml::Dimension reads one alone).
float x = 0.0f, y = 0.0f;
const void *dimension = inlineOffset ? anchor : FindChild(anchor, "Offset");
if (dimension != nullptr) {
if (inlineOffset) {
auto setAttr = reinterpret_cast<SetAttribute_t>(Offsets::FUN_XML_NODE_SET_ATTRIBUTE);
if (!HasValue(xs))
setAttr(const_cast<void *>(anchor), "x", "0");
if (!HasValue(ys))
setAttr(const_cast<void *>(anchor), "y", "0");
}
if (dimension != nullptr)
reinterpret_cast<ReadDimension_t>(Offsets::FUN_XML_READ_DIMENSION)(dimension, &x, &y, status);
}
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(
Game::Ptr<void>(object, Offsets::OFF_REGION_ANCHOR), point, relAnchor, relativePoint, x,
+67
View File
@@ -0,0 +1,67 @@
// 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/>.
// A dimension element with only `x`: `<Size x="18"/>`, `<Anchor point="LEFT"
// x="2"/>`.
//
// The engine's reader (FUN_XML_READ_DIMENSION) stores `x` and `y` from the
// node's attributes, then decides whether it found any by looking at the `y`
// attribute alone: with no `y` and no child element it reports "No x or y
// attributes in element" and answers 0, so the frame loader never applies the
// size it just read. 4.3.4's Dimension::Load answers true when either
// attribute is set. The hook makes that call for a node with `x`, no `y` and
// no child: the engine's reader runs as before, writing the converted `x` and
// a 0 `y`, with its warning sent to a sink that drops it, and the answer is 1.
#include "Game.h"
#include "Offsets.h"
#include "xml/FrameLoad.h"
namespace Xml::Dimension {
namespace {
using ReadDimension_t = int(__fastcall *)(const void *node, float *x, float *y, void *status);
using StatusAdd_t = void(__cdecl *)(void *status, int level, const char *fmt, ...);
ReadDimension_t g_orig = nullptr;
void __cdecl IgnoreStatus(void * /*status*/, int /*level*/, const char * /*fmt*/, ...) {}
// A status object the reader can report to: VTBL_XML_STATUS_ADD is the only
// slot it calls.
const StatusAdd_t kSilentVtable[Offsets::VTBL_XML_STATUS_ADD / sizeof(void *) + 1] = {
nullptr, nullptr, nullptr, &IgnoreStatus};
static_assert(Offsets::VTBL_XML_STATUS_ADD / sizeof(void *) == 3, "the status Add slot");
struct SilentStatus {
const StatusAdd_t *vtable = kSilentVtable;
};
bool HasValue(const char *s) { return s != nullptr && *s != '\0'; }
int __fastcall ReadDimension_h(const void *node, float *x, float *y, void *status) {
if (node != nullptr && HasValue(Attr(node, "x")) && Attr(node, "y") == nullptr &&
FirstChild(node) == nullptr) {
SilentStatus silent;
g_orig(node, x, y, &silent);
return 1;
}
return g_orig(node, x, y, status);
}
const Game::HookAutoRegister _hook{Offsets::FUN_XML_READ_DIMENSION, reinterpret_cast<void *>(&ReadDimension_h),
reinterpret_cast<void **>(&g_orig)};
} // namespace
} // namespace Xml::Dimension
+27 -4
View File
@@ -23,7 +23,10 @@
//
// Each entry is `object[key] = value`. `type` and `keyType` (default
// "string") are one of nil / boolean / number / string / global, the later
// clients' KEYVALUETYPE; "global" is `_G[text]`. Booleans and numbers go
// clients' KEYVALUETYPE; "global" is `_G[text]`, walking a dotted path a
// field at a time: retail's own templates use
// "ScrollControllerMixin.Directions.Decrease". A step that is not a table
// gives nil. Booleans and numbers go
// through the same loader helpers as `<Attributes>` (FUN_XML_ATTR_TO_BOOL,
// FUN_SSTR_TO_FLOAT).
//
@@ -42,6 +45,8 @@
#include "baselib/Ascii.h"
#include "xml/FrameLoad.h"
#include <cstring>
namespace Xml::KeyValues {
namespace {
@@ -75,10 +80,28 @@ void Push(void *L, Kind kind, const char *text) {
case Kind::String:
Game::Lua::PushString(L, text);
break;
case Kind::Global:
Game::Lua::PushString(L, text);
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
case Kind::Global: {
Game::Lua::PushValue(L, Game::Lua::GLOBALS_INDEX);
const char *start = text;
for (;;) {
const char *dot = std::strchr(start, '.');
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_TABLE) {
Game::Lua::SetTop(L, -2);
Game::Lua::PushNil(L);
break;
}
if (dot == nullptr)
Game::Lua::PushString(L, start);
else
Game::Lua::PushLString(L, start, static_cast<unsigned>(dot - start));
Game::Lua::GetTable(L, -2);
Game::Lua::Remove(L, -2);
if (dot == nullptr)
break;
start = dot + 1;
}
break;
}
default:
Game::Lua::PushNil(L);
break;
+9 -1
View File
@@ -87,13 +87,16 @@
// builder finalizes, so OnLoad sees the groups.
//
// The texture loader also reads `mask="path"` (`<Texture file=".." mask=".."/>`,
// the later-client attribute) and applies it as texture:SetMask(path).
// the later-client attribute) and applies it as texture:SetMask(path), and
// `atlas="name"` with `useAtlasSize`, applied as texture:SetAtlas(name,
// useAtlasSize).
#include "xml/FrameLoad.h"
#include "Game.h"
#include "Offsets.h"
#include "animation/Xml.h"
#include "texture/Atlas.h"
#include "texture/Mask.h"
#include "xml/Anchors.h"
#include "xml/KeyValues.h"
@@ -329,6 +332,11 @@ uintptr_t __fastcall TextureLoad_h(void *self, void *edx, const void *node, void
Animation::Xml::LoadAnimations(RegionObject(self), node, status);
if (const char *mask = Xml::Attr(node, "mask"); mask != nullptr && *mask != '\0')
Texture::Mask::SetPath(RegionObject(self), mask);
if (const char *atlas = Xml::Attr(node, "atlas"); atlas != nullptr && *atlas != '\0') {
const char *useSize = Xml::Attr(node, "useAtlasSize");
Texture::Atlas::SetFromXml(RegionObject(self), atlas,
useSize != nullptr && Game::StringToFlag(useSize));
}
}
return r;
}