Files
ClassicAPI/src/debug/Probe.cpp
T
Brues d9102bae3b Relicense source headers from LGPL to GPL v3
The repo was internally inconsistent: the LICENSE file is GPL v3 but every
src/ header carried the LGPL v3 notice and the README said "LGPL v3 or
later". LGPL was also the wrong fit — this is an injected mod plus a
bundled addon, not a library other code links against, so LGPL's linking
exception buys nothing.

It's also required: src/voicechat/Tts.cpp is ported from awesome_wotlk's
VoiceChat.cpp, which is GPL v3, and a derivative of GPL code cannot be
relicensed under more permissive terms. Dependencies stay compatible —
MinHook (BSD-2) and the nampower-mirrored packet shapes (BSD) are
GPL-compatible, and SAPI is covered by the system-library exception.

Swept "GNU Lesser General Public License" -> "GNU General Public License"
across all 257 src/ headers and changed the README license line to match
the GPL v3 LICENSE file. Comment-only; no build impact.
2026-06-29 16:38:16 -05:00

409 lines
15 KiB
C++

// 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/>.
// Temporary probe helpers used to derive byte offsets in the player /
// local-player structs. Intended to be deleted once the offsets are
// baked in.
//
// Bound globals:
//
// _classicapi_DescDump(start, len) → string
// _classicapi_PlayerDump(start, len) → string
// Return a space-separated `0xOFFSET=0xVALUE` string of non-zero
// u32 entries in the half-open range [start, start+len) of the
// player's m_objectFields descriptor (DescDump) or the local
// CGPlayer object directly (PlayerDump). Use DescDump for
// UpdateField-broadcast state (mount, stealth visibility);
// PlayerDump for client-only state (movement flags).
// Useful for short ranges (~0x100 bytes); longer ranges may
// truncate when /dump'd.
//
// _classicapi_DescLog(label, start, len)
// _classicapi_PlayerLog(label, start, len)
// Same dump shape, but appends the result to `debug.log` via
// Debug::Log::Append rather than returning it. Use for ranges
// too long for /dump's output buffer (e.g., scanning the whole
// CGPlayer object for movement flags).
//
// _classicapi_SnapAll()
// Captures a baseline snapshot of three regions on the player:
// • m_objectFields descriptor [0, 0x400)
// • CGPlayer object [0, 0x2000)
// • [unit + OFF_CGPLAYER_INFO] [0, 0x2000) (the +0xE68 sub-struct)
// Diff functions below compare the *live* memory to this snapshot.
// Workflow: stand idle, call SnapAll, perform the action whose
// state you want to localize (e.g. click warlock summoning portal),
// then call DiffAll[Log] before the state clears. Re-snap any time.
//
// _classicapi_DiffAll() → string
// _classicapi_DiffAllLog(label)
// Emit `OFF=OLD→NEW` for every u32 that has changed since the last
// SnapAll, grouped into DESC / PLAYER / EXTRA sections. DiffAll
// returns the string (use for short diffs via /dump); DiffAllLog
// appends to debug.log (use for long diffs where /dump truncates).
#include "Game.h"
#include "Log.h"
#include "Offsets.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
namespace Debug::Probe {
namespace {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
const uint8_t *ResolvePlayer() {
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
return static_cast<const uint8_t *>(fn("player"));
}
const uint8_t *ResolvePlayerDescriptor() {
auto *player = ResolvePlayer();
if (player == nullptr)
return nullptr;
return *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_UNIT_DESCRIPTOR);
}
// [unit + OFF_CGPLAYER_INFO] — the +0xE68 sub-struct documented in
// Offsets.h. Holds PLAYER_FLAGS (+0x08) and visible-items (+0x118).
// Most other +0xE68 fields are undocumented and are the reason this
// region is a high-yield target for change-detection scans.
const uint8_t *ResolvePlayerExtra() {
auto *player = ResolvePlayer();
if (player == nullptr)
return nullptr;
return *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_CGPLAYER_INFO);
}
// Camera object pointer chain — `[*0x00B4B2BC + 0x65B8]`. Verified
// via `Script_CameraZoomIn` (`0x0050B400`) which loads the same
// chain to call `FUN_0050FC60` on the camera. The camera's body
// has fields like `+0x128` (flags) and `+0x15C` (zoom).
const uint8_t *ResolveCamera() {
const uint8_t *gameState = *reinterpret_cast<const uint8_t *const *>(0x00B4B2BC);
if (gameState == nullptr)
return nullptr;
return *reinterpret_cast<const uint8_t *const *>(gameState + 0x65B8);
}
// Writes non-zero u32 entries in [start, start+len) into `out` as
// space-separated `0xOFFSET=0xVALUE` pairs. Returns the length
// written (excluding the nul terminator). Output is truncated if it
// would exceed `outLen-1` bytes; partial output is still well-formed
// (doesn't end mid-pair).
int FormatNonZeroU32s(const uint8_t *base, int start, int len,
char *out, int outLen) {
if (base == nullptr || out == nullptr || outLen <= 0)
return 0;
int pos = 0;
out[0] = '\0';
for (int off = 0; off + 4 <= len && pos < outLen - 32; off += 4) {
const uint32_t v = *reinterpret_cast<const uint32_t *>(base + start + off);
if (v != 0) {
const int n = std::snprintf(out + pos, outLen - pos,
"0x%X=0x%X ", start + off, v);
if (n < 0)
break;
pos += n;
}
}
if (pos == 0) {
const int n = std::snprintf(out, outLen, "(all zero)");
pos = n > 0 ? n : 0;
}
return pos;
}
void DumpToLua(void *L, const uint8_t *base, int start, int len) {
static char buf[3000];
FormatNonZeroU32s(base, start, len, buf, sizeof(buf));
Game::Lua::PushString(L, buf);
}
void DumpToFile(const char *label, const uint8_t *base, int start, int len) {
static char buf[8192];
FormatNonZeroU32s(base, start, len, buf, sizeof(buf));
Debug::Log::Append(label, buf);
}
void ReadStartLen(void *L, int argBase, int *outStart, int *outLen,
int defaultLen) {
*outStart = 0;
*outLen = defaultLen;
if (Game::Lua::IsNumber(L, argBase))
*outStart = static_cast<int>(Game::Lua::ToNumber(L, argBase));
if (Game::Lua::IsNumber(L, argBase + 1))
*outLen = static_cast<int>(Game::Lua::ToNumber(L, argBase + 1));
}
int __fastcall Script_DescDump(void *L) {
int start, len;
ReadStartLen(L, 1, &start, &len, 0x200);
DumpToLua(L, ResolvePlayerDescriptor(), start, len);
return 1;
}
int __fastcall Script_PlayerDump(void *L) {
int start, len;
ReadStartLen(L, 1, &start, &len, 0x200);
DumpToLua(L, ResolvePlayer(), start, len);
return 1;
}
int __fastcall Script_DescLog(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: _classicapi_DescLog(label, start, len)");
return 0;
}
const char *label = Game::Lua::ToString(L, 1);
int start, len;
ReadStartLen(L, 2, &start, &len, 0x200);
DumpToFile(label, ResolvePlayerDescriptor(), start, len);
return 0;
}
int __fastcall Script_PlayerLog(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: _classicapi_PlayerLog(label, start, len)");
return 0;
}
const char *label = Game::Lua::ToString(L, 1);
int start, len;
ReadStartLen(L, 2, &start, &len, 0x200);
DumpToFile(label, ResolvePlayer(), start, len);
return 0;
}
// Snapshot of a fixed-size byte range, used by Snap/Diff to compare
// the live memory against a captured baseline.
struct Snap {
static constexpr int kCap = 0x2000;
uint8_t data[kCap];
int start;
int len;
bool valid;
};
Snap descSnap;
Snap playerSnap;
Snap extraSnap;
Snap cameraSnap;
void TakeSnap(Snap &s, const uint8_t *base, int start, int len) {
s.valid = false;
s.start = start;
s.len = 0;
if (base == nullptr || len <= 0)
return;
if (len > Snap::kCap)
len = Snap::kCap;
std::memcpy(s.data, base + start, static_cast<size_t>(len));
s.len = len;
s.valid = true;
}
// Emit one `=== <label> ===` header line + each changed u32 as
// `0xOFFSET=0xOLD->0xNEW`. Stops cleanly before overrunning the output
// buffer (leaves room for the final nul + one safety margin). Returns
// bytes written (excluding nul).
int FormatDiff(const Snap &s, const uint8_t *base, const char *label,
char *out, int outLen) {
if (out == nullptr || outLen <= 0)
return 0;
int pos = 0;
const int header = std::snprintf(out + pos, outLen - pos, "%s: ", label);
if (header > 0)
pos += header;
if (!s.valid) {
const int n = std::snprintf(out + pos, outLen - pos, "(no snapshot) ");
if (n > 0)
pos += n;
return pos;
}
if (base == nullptr) {
const int n = std::snprintf(out + pos, outLen - pos, "(no base) ");
if (n > 0)
pos += n;
return pos;
}
int changes = 0;
for (int off = 0; off + 4 <= s.len && pos < outLen - 48; off += 4) {
const uint32_t oldV = *reinterpret_cast<const uint32_t *>(s.data + off);
const uint32_t newV = *reinterpret_cast<const uint32_t *>(
base + s.start + off);
if (oldV == newV)
continue;
const int n = std::snprintf(out + pos, outLen - pos,
"0x%X=0x%X->0x%X ",
s.start + off, oldV, newV);
if (n < 0)
break;
pos += n;
changes++;
}
if (changes == 0) {
const int n = std::snprintf(out + pos, outLen - pos, "(unchanged) ");
if (n > 0)
pos += n;
}
return pos;
}
int __fastcall Script_SnapAll(void *) {
TakeSnap(descSnap, ResolvePlayerDescriptor(), 0, 0x400);
TakeSnap(playerSnap, ResolvePlayer(), 0, 0x2000);
TakeSnap(extraSnap, ResolvePlayerExtra(), 0, 0x2000);
TakeSnap(cameraSnap, ResolveCamera(), 0, 0x800);
return 0;
}
int __fastcall Script_DiffAll(void *L) {
static char buf[4500];
int pos = 0;
pos += FormatDiff(descSnap, ResolvePlayerDescriptor(), "DESC",
buf + pos, sizeof(buf) - pos);
pos += FormatDiff(playerSnap, ResolvePlayer(), "PLAYER",
buf + pos, sizeof(buf) - pos);
pos += FormatDiff(extraSnap, ResolvePlayerExtra(), "EXTRA",
buf + pos, sizeof(buf) - pos);
pos += FormatDiff(cameraSnap, ResolveCamera(), "CAMERA",
buf + pos, sizeof(buf) - pos);
if (pos >= 0 && pos < static_cast<int>(sizeof(buf)))
buf[pos] = '\0';
else
buf[sizeof(buf) - 1] = '\0';
Game::Lua::PushString(L, buf);
return 1;
}
int __fastcall Script_DiffAllLog(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: _classicapi_DiffAllLog(label)");
return 0;
}
const char *label = Game::Lua::ToString(L, 1);
static char buf[16384];
int pos = 0;
pos += FormatDiff(descSnap, ResolvePlayerDescriptor(), "DESC",
buf + pos, sizeof(buf) - pos);
if (pos < static_cast<int>(sizeof(buf)) - 2) {
buf[pos++] = '\n';
}
pos += FormatDiff(playerSnap, ResolvePlayer(), "PLAYER",
buf + pos, sizeof(buf) - pos);
if (pos < static_cast<int>(sizeof(buf)) - 2) {
buf[pos++] = '\n';
}
pos += FormatDiff(extraSnap, ResolvePlayerExtra(), "EXTRA",
buf + pos, sizeof(buf) - pos);
if (pos < static_cast<int>(sizeof(buf)) - 2) {
buf[pos++] = '\n';
}
pos += FormatDiff(cameraSnap, ResolveCamera(), "CAMERA",
buf + pos, sizeof(buf) - pos);
if (pos >= 0 && pos < static_cast<int>(sizeof(buf)))
buf[pos] = '\0';
else
buf[sizeof(buf) - 1] = '\0';
Debug::Log::Append(label, buf);
return 0;
}
// Walks SkillLineAbility.dbc and returns a string with every row
// matching `spellID`, formatted as
// `row=N id=I skill=S race=0xR class=0xC excR=0xR excC=0xC minRank=M sup=S acq=A`
// Multiple rows are joined with newlines. Returns "(none)" if no
// match. Use to diagnose why a spell appears (or doesn't) in
// GetCurrentLevelSpells — Turtle WoW custom spells with weird
// classMask values surface here clearly.
int __fastcall Script_SLADumpForSpell(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: _classicapi_SLADumpForSpell(spellID)");
return 0;
}
const int wantSpellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
static_cast<uintptr_t>(Offsets::VAR_SKILL_LINE_ABILITY_RECORDS));
const int count = *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_SKILL_LINE_ABILITY_COUNT));
static char buf[4096];
int pos = 0;
buf[0] = '\0';
if (records == nullptr || count <= 0) {
Game::Lua::PushString(L, "(no SLA records)");
return 1;
}
for (int i = 1; i <= count && pos < static_cast<int>(sizeof(buf)) - 256; ++i) {
const uint8_t *rec = records[i];
if (rec == nullptr)
continue;
const int spellID = *reinterpret_cast<const int *>(
rec + Offsets::OFF_SLA_SPELL_ID);
if (spellID != wantSpellID)
continue;
const int id = *reinterpret_cast<const int *>(rec + 0x00);
const int skill = *reinterpret_cast<const int *>(rec + 0x04);
const uint32_t race = *reinterpret_cast<const uint32_t *>(
rec + Offsets::OFF_SLA_RACE_MASK);
const uint32_t cls = *reinterpret_cast<const uint32_t *>(
rec + Offsets::OFF_SLA_CLASS_MASK);
const uint32_t excR = *reinterpret_cast<const uint32_t *>(
rec + Offsets::OFF_SLA_EXCLUDE_RACE);
const uint32_t excC = *reinterpret_cast<const uint32_t *>(
rec + Offsets::OFF_SLA_EXCLUDE_CLASS);
const int minRank = *reinterpret_cast<const int *>(rec + 0x1C);
const int sup = *reinterpret_cast<const int *>(rec + 0x20);
const int acq = *reinterpret_cast<const int *>(rec + 0x24);
const int n = std::snprintf(buf + pos, sizeof(buf) - pos,
"row=%d id=%d skill=%d race=0x%X class=0x%X excR=0x%X excC=0x%X minRank=%d sup=%d acq=%d\n",
i, id, skill, race, cls, excR, excC, minRank, sup, acq);
if (n < 0)
break;
pos += n;
}
if (pos == 0)
Game::Lua::PushString(L, "(none)");
else
Game::Lua::PushString(L, buf);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("_classicapi_DescDump", &Script_DescDump);
Game::Lua::RegisterGlobalFunction("_classicapi_PlayerDump", &Script_PlayerDump);
Game::Lua::RegisterGlobalFunction("_classicapi_DescLog", &Script_DescLog);
Game::Lua::RegisterGlobalFunction("_classicapi_PlayerLog", &Script_PlayerLog);
Game::Lua::RegisterGlobalFunction("_classicapi_SnapAll", &Script_SnapAll);
Game::Lua::RegisterGlobalFunction("_classicapi_DiffAll", &Script_DiffAll);
Game::Lua::RegisterGlobalFunction("_classicapi_DiffAllLog", &Script_DiffAllLog);
Game::Lua::RegisterGlobalFunction("_classicapi_SLADumpForSpell",
&Script_SLADumpForSpell);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Debug::Probe