// 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 . // 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 #include #include namespace Debug::Probe { namespace { using ResolveUnitToken_t = void *(__fastcall *)(const char *token); const uint8_t *ResolvePlayer() { auto fn = reinterpret_cast(Offsets::FUN_RESOLVE_UNIT_TOKEN); return static_cast(fn("player")); } const uint8_t *ResolvePlayerDescriptor() { auto *player = ResolvePlayer(); if (player == nullptr) return nullptr; return *reinterpret_cast( 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( 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(0x00B4B2BC); if (gameState == nullptr) return nullptr; return *reinterpret_cast(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(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(Game::Lua::ToNumber(L, argBase)); if (Game::Lua::IsNumber(L, argBase + 1)) *outLen = static_cast(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(len)); s.len = len; s.valid = true; } // Emit one `===