Repository navigation
Expand file tree
/
Copy pathFrameStack.cpp
More file actions
386 lines (347 loc) · 16.9 KB
/
Copy pathFrameStack.cpp
File metadata and controls
386 lines (347 loc) · 16.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
// 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/>.
// `GameTooltip:SetFrameStack([showHidden [, showRegions [, highlightIndexChanged]]])`
// -> highlightFrame — fills the tooltip with every frame under the cursor,
// grouped by strata, top first, and returns the highlighted entry. The engine
// half of Blizzard_DebugTools' /fstack (FrameStackTooltip calls it on each
// OnUpdate, and again on each Alt press with the highlight delta).
//
// The list is 4.3.4's CGTooltip::SetFrameStack (0x0046D380 in build 15595),
// which 1.12 lacks but whose every input it has:
//
// - the frame list CSimpleTop::m_frames: `*ctx + OFF_UI_CONTEXT_FRAME_LIST_HEAD`,
// next at OFF_FRAME_ENUM_NEXT — the list Script_EnumerateFrames walks;
// - the cursor CSimpleTop::m_mousePosition (OFF_UI_CONTEXT_MOUSE_POSITION),
// converted with the engine's NDCToDDC into the space frame rects live in;
// - CLayoutFrame::GetRect (FUN_REGION_GET_RECT) on the frame's layout
// sub-object, and the frame's visible / enabled-input / strata / level
// fields — the same fields IsVisible / IsMouseEnabled / GetFrameStrata /
// GetFrameLevel read;
// - FrameStrataToString, the two default tooltip colors, and the
// DEBUG_FRAMESTACK global string (shipped in the addon's locale files).
//
// The plain layout rect is used, not the mouse hit rect (hit-rect insets,
// clipping): 4.3.4 lists what is drawn under the cursor, including frames
// that do not take the mouse — those just get the other color.
//
// The rest is the current client's. Its Lua passes (showHidden, showRegions,
// highlightIndexChanged) and keeps the return as the highlight frame, and
// its string table holds the method's formats in one run — "-->",
// "%s<%d> %s", " <%d> %s %s %s %.2f %.2f", " <%d> %s %.2f %f.2",
// "ANCHORS" — with "SOURCE" / " %s" beside them:
//
// - showRegions lists each listed frame's textures and font strings under
// the cursor right after the frame, in FRAMESTACK_REGION_COLOR, at the
// frame's level (a region has none of its own), the name indented three
// more spaces;
// - one entry is highlighted. Its line opens with "-->" in place of the
// indent, and after the whole list come its anchors — one line per set
// point, in slot order, with what GetPoint reports: the point, the
// target's name, the target's point, the offsets in UI units — and, when
// Frame::SourceLocation knows it, its source. highlightIndexChanged moves
// the highlight that many entries (retail's Lua passes +1 / -1 per Alt
// press, 0 otherwise); the index persists between calls so the highlight
// stays put while the cursor rests, and wraps at either end.
// - the highlighted object is the return value and the argument of
// OnTooltipSetFrameStack (Tooltip::SetEvents), fired before Show as
// 4.3.4 does.
//
// The anchor target is read through the node's own getter, as GetPoint does,
// after Frame::GetPointGuard's dangling-target cleanup — the same guard the
// GetPoint override runs — since a node whose target frame was freed still
// holds its pointer in 1.12. The non-highlight indent and the "ANCHORS"
// header are inferred from the string table, not from a retail capture.
#include "Game.h"
#include "Offsets.h"
#include "frame/GetPointGuard.h"
#include "frame/SourceLocation.h"
#include "tooltip/Lines.h"
#include "tooltip/SetEvents.h"
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <vector>
namespace Tooltip::FrameStack {
namespace {
using GetName_t = const char *(__thiscall *)(void *object);
using GetRect_t = int(__thiscall *)(void *layout, float *rect);
using NDCToDDC_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
using StrataToString_t = const char *(__fastcall *)(int strata);
using PointToString_t = const char *(__fastcall *)(int point);
using GetRelative_t = void *(__thiscall *)(void *node);
// 4.3.4's line colors, packed {b,g,r,a} like every add-line color; two of each
// so neighbouring frame levels alternate (s_frameStack*Colors). Regions take
// the client's FRAMESTACK_REGION_COLOR (UI::ColorData).
constexpr uint32_t kMouseColors[2] = {0xFFFF9966, 0xFFDDDD88};
constexpr uint32_t kColors[2] = {0xFF6699FF, 0xFF88DDDD};
constexpr uint32_t kHiddenColors[2] = {0xFF666666, 0xFF999999};
constexpr uint32_t kRegionColor = 0xFF88DDDD;
// One past the highest strata (TOOLTIP = 8), so the first entry always opens
// a strata header — 4.3.4's `lastStrata = 9`.
constexpr int kNoStrata = 9;
// 4.3.4's FrameStackInfo, plus the object itself (the highlight needs it)
// and whether it is a region.
struct Entry {
void *object;
char name[1024];
int strata;
int level;
bool mouseEnabled;
bool region;
uint32_t visible;
};
// The highlighted entry's index into the list, kept across calls.
int g_highlightIndex = 0;
// QSortFrameStack: strata, then level, both high to low, then name.
bool Before(const Entry &a, const Entry &b) {
if (a.strata != b.strata)
return a.strata > b.strata;
if (a.level != b.level)
return a.level > b.level;
return Game::StrCmpI(a.name, b.name) < 0;
}
// The object's display name, as 4.3.4 forms it: the global name when
// `_G[name]` is this object, else the object's Lua table as "table: %p",
// followed by "(name)" when the object has a name.
void FormatName(void *L, void *object, char *out, size_t size) {
auto *vtable = Game::Read<uintptr_t *>(object, 0);
const char *name = reinterpret_cast<GetName_t>(
vtable[Offsets::VTBL_SCRIPTOBJECT_GET_NAME / sizeof(uintptr_t)])(object);
if (name == nullptr || *name == '\0') {
Game::Lua::PushObject(L, object);
std::snprintf(out, size, "%s: %p", Game::Lua::TypeName(L, Game::Lua::Type(L, -1)),
Game::Lua::ToPointer(L, -1));
Game::Lua::SetTop(L, -2);
return;
}
Game::Lua::PushString(L, name);
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE) {
void *resolved = Game::Lua::ResolveObject(L, -1);
Game::Lua::SetTop(L, -2);
if (resolved == object) {
std::snprintf(out, size, "%s", name);
return;
}
Game::Lua::PushObject(L, object);
}
std::snprintf(out, size, "%s: %p (%s)", Game::Lua::TypeName(L, Game::Lua::Type(L, -1)),
Game::Lua::ToPointer(L, -1), name);
Game::Lua::SetTop(L, -2);
}
// Whether the cursor (x, y) lies in the object's layout rect; false when the
// rect cannot be resolved.
bool UnderCursor(void *object, float x, float y) {
float rect[4]; // {minY, minX, maxY, maxX}
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(
static_cast<uint8_t *>(object) + Offsets::OFF_REGION_ANCHOR, rect) == 0)
return false;
return x >= rect[1] && x <= rect[3] && y <= rect[2] && y >= rect[0];
}
// The frame's regions under the cursor, after the frame's entry.
void AddRegions(void *L, const Entry &frame, float x, float y, bool showHidden,
std::vector<Entry> &list) {
Game::ForEachFrameListObject(frame.object, Offsets::OFF_FRAME_REGION_LIST, [&](void *region) {
const uint32_t visible = Game::Read<uint32_t>(region, Offsets::OFF_REGION_ACTUALLY_SHOWN);
if ((visible == 0 && !showHidden) || !UnderCursor(region, x, y))
return;
Entry &e = list.emplace_back();
e.object = region;
FormatName(L, region, e.name, sizeof(e.name));
e.strata = frame.strata;
e.level = frame.level;
e.mouseEnabled = false;
e.region = true;
e.visible = visible;
});
}
// The highlighted entry's detail lines, after the list: its anchors, then
// its source. Headers in the tooltip's text color, lines in the entry's.
void AddDetails(void *L, void *self, void *object, const uint32_t &color,
const uint32_t &headerColor) {
Frame::GetPointGuard::CleanDanglingAnchors(object);
auto pointName = reinterpret_cast<PointToString_t>(Offsets::FUN_FRAME_POINT_TO_STRING);
char text[1024];
int n = 0;
for (int slot = 0; slot < Offsets::REGION_ANCHOR_SLOT_COUNT; ++slot) {
auto *node = Game::Read<uint8_t *>(object, Offsets::OFF_REGION_ANCHOR_ARRAY + slot * 4);
if (node == nullptr)
continue;
if (n++ == 0)
Lines::Add(self, "ANCHORS", headerColor);
auto *vtable = Game::Read<uintptr_t *>(node, 0);
auto *relative = static_cast<uint8_t *>(reinterpret_cast<GetRelative_t>(
vtable[Offsets::VTBL_ANCHOR_NODE_GET_RELATIVE / sizeof(uintptr_t)])(node));
const double x = Game::InternalToPixel(Game::Read<float>(node, Offsets::OFF_ANCHOR_NODE_X));
const double y = Game::InternalToPixel(Game::Read<float>(node, Offsets::OFF_ANCHOR_NODE_Y));
// A screen anchor's target is the top itself, a bare CLayoutFrame with
// no region in front of it (Offsets VAR_UI_CONTEXT_PTR): no name to
// show, and `relative - OFF_REGION_ANCHOR` must not be formed.
const bool toScreen =
relative == nullptr || relative == Game::Read<uint8_t *>(Offsets::VAR_UI_CONTEXT_PTR);
if (!toScreen) {
char relativeName[1024];
FormatName(L, relative - Offsets::OFF_REGION_ANCHOR, relativeName, sizeof(relativeName));
std::snprintf(text, sizeof(text), " <%d> %s %s %s %.2f %.2f", n, pointName(slot),
relativeName,
pointName(Game::Read<int>(node, Offsets::OFF_ANCHOR_NODE_RELATIVE_POINT)),
x, y);
} else {
std::snprintf(text, sizeof(text), " <%d> %s %.2f %.2f", n, pointName(slot), x, y);
}
Lines::Add(self, text, color);
}
if (const char *location = Frame::SourceLocation::Get(object)) {
Lines::Add(self, "SOURCE", headerColor);
std::snprintf(text, sizeof(text), " %s", location);
Lines::Add(self, text, color);
}
}
int __fastcall Script_GameTooltipSetFrameStack(void *L) {
void *self = Game::Lua::ResolveTooltip(L);
if (self == nullptr)
return 0;
const bool showHidden = Game::Lua::ToBoolean(L, 2) != 0;
const bool showRegions = Game::Lua::ToBoolean(L, 3) != 0;
const int highlightDelta =
Game::Lua::IsNumber(L, 4) ? static_cast<int>(Game::Lua::ToNumber(L, 4)) : 0;
Lines::Clear(self);
auto *ctx = Game::Read<uint8_t *>(Offsets::VAR_UI_CONTEXT_PTR);
if (ctx == nullptr)
return 0;
const float *mouse = Game::Ptr<float>(ctx, Offsets::OFF_UI_CONTEXT_MOUSE_POSITION);
float x = 0.0f;
float y = 0.0f;
reinterpret_cast<NDCToDDC_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&x, &y, mouse[0], mouse[1]);
void *frame = Game::Read<void *>(ctx, Offsets::OFF_UI_CONTEXT_FRAME_LIST_HEAD);
if (frame == nullptr || (reinterpret_cast<uintptr_t>(frame) & 1) != 0)
return 0;
std::vector<Entry> frames;
for (; frame != nullptr && (reinterpret_cast<uintptr_t>(frame) & 1) == 0;
frame = Game::Read<void *>(frame, Offsets::OFF_FRAME_ENUM_NEXT)) {
const uint32_t visible = Game::Read<uint32_t>(frame, Offsets::OFF_FRAME_VISIBLE);
if ((visible == 0 && !showHidden) || !UnderCursor(frame, x, y))
continue;
Entry &e = frames.emplace_back();
e.object = frame;
FormatName(L, frame, e.name, sizeof(e.name));
e.strata = Game::Read<int>(frame, Offsets::OFF_FRAME_STRATA);
e.level = Game::Read<int>(frame, Offsets::OFF_FRAME_LEVEL);
e.mouseEnabled = (Game::Read<uint32_t>(frame, Offsets::OFF_FRAME_ENABLED_EVENTS) &
(1u << Offsets::SIMPLE_EVENT_MOUSE)) != 0;
e.region = false;
e.visible = visible;
}
std::sort(frames.begin(), frames.end(), Before);
// Display order: each frame, then (showRegions) its regions.
std::vector<Entry> list;
list.reserve(frames.size());
for (Entry &e : frames) {
list.push_back(e);
if (showRegions)
AddRegions(L, list.back(), x, y, showHidden, list);
}
const int count = static_cast<int>(list.size());
int highlightAt = -1;
if (count > 0) {
g_highlightIndex = ((g_highlightIndex + highlightDelta) % count + count) % count;
highlightAt = g_highlightIndex;
}
const uint32_t textColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_TEXT_COLOR);
const uint32_t titleColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_TITLE_COLOR);
using GetText_t = const char *(__fastcall *)(const char *name, int count, int gender);
const char *header =
reinterpret_cast<GetText_t>(Offsets::FUN_FRAMESCRIPT_GET_TEXT)("DEBUG_FRAMESTACK", -1, 0);
// The cursor in UI pixels — the values GetCursorPosition returns.
char text[1024];
std::snprintf(text, sizeof(text), "(%.2f, %.2f)", Game::InternalToPixel(x),
Game::InternalToPixel(y));
Lines::AddDouble(self, header, text, textColor, titleColor);
auto strataName = reinterpret_cast<StrataToString_t>(Offsets::FUN_FRAME_STRATA_TO_STRING);
int toggle = 1;
int lastStrata = kNoStrata;
int lastLevel = 0;
const uint32_t *highlightColor = nullptr;
for (int i = 0; i < count; ++i) {
const Entry &e = list[i];
if (!e.region) {
if (e.strata != lastStrata) {
// 1.12's name table starts at BACKGROUND; 4.3.4's names 0.
const char *name = (e.strata == Offsets::FRAME_STRATA_WORLD)
? "WORLD"
: strataName(e.strata);
Lines::AddDouble(self, name, nullptr, textColor, textColor);
lastLevel = e.level;
lastStrata = e.strata;
toggle = 0;
}
if (e.level != lastLevel) {
toggle = (toggle + 1) % 2;
lastLevel = e.level;
}
}
const bool highlighted = (i == highlightAt);
std::snprintf(text, sizeof(text), "%s<%d> %s%s", highlighted ? "-->" : " ", e.level,
e.region ? " " : "", e.name);
const uint32_t &color = !e.visible ? kHiddenColors[toggle]
: e.region ? kRegionColor
: e.mouseEnabled ? kMouseColors[toggle]
: kColors[toggle];
Lines::Add(self, text, color);
if (highlighted)
highlightColor = &color;
}
void *highlight = (highlightAt >= 0) ? list[highlightAt].object : nullptr;
if (highlight != nullptr)
AddDetails(L, self, highlight, *highlightColor, textColor);
SetEvents::FireFrameStack(self, highlight);
Game::Lua::SetTop(L, 1);
Lines::Show(L);
if (highlight == nullptr)
return 0;
Game::Lua::PushObject(L, highlight);
return 1;
}
const Game::Lua::FrameMethodEntry g_methods[] = {
{"SetFrameStack", &Script_GameTooltipSetFrameStack},
};
const Game::Doc::Field kArgs[] = {
Game::Doc::Opt("showHidden", "bool", "false", "Also list hidden frames, in grey."),
Game::Doc::Opt("showRegions", "bool", "false",
"Also list the textures and font strings under the cursor, after their frame."),
Game::Doc::Opt("highlightIndexChanged", "number", "0",
"Moves the highlight this many entries down the list (negative: up). The "
"highlight stays where it is between calls."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Opt("highlightFrame", "ScriptRegion", nullptr,
"The highlighted frame or region; nil when nothing is under the cursor."),
};
const Game::Doc::Function kSetFrameStack{
"Fills the tooltip with every frame under the cursor, grouped by strata from the top, and "
"returns the highlighted entry. Frames that take the mouse are orange, other shown frames "
"blue, regions cyan. The highlighted entry opens with \"-->\" and lists its anchors and, "
"when known, its source.",
kArgs, kRets};
const Game::Doc::Method g_methodDocs[] = {
{"SetFrameStack", &kSetFrameStack},
};
void RegisterLuaFunctions() {
Game::Lua::RegisterFrameMethods(
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY), g_methods,
g_methodDocs);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Tooltip::FrameStack