mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 22:50:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19397782f7 | ||
|
|
284b951038 | ||
|
|
1df48772f4 |
@@ -27,6 +27,7 @@ Util\EventUtil.lua
|
||||
Util\ItemUtil.lua
|
||||
Util\AddOnCompat.lua
|
||||
Util\FunctionUtil.lua
|
||||
Util\CacheUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
-- On-demand, throttled cache warmer (/warmcache). A brute-force scan of the
|
||||
-- whole ID space floods the client's network buffers and panics in
|
||||
-- WDataStoreSmallBuffer ("not enough memory") — the engine can't hold that
|
||||
-- many pending queries. So this warms a *bounded* range, started/stopped
|
||||
-- manually, at a low fixed rate. Tune WARM_RATE / WARM_INTERVAL if needed,
|
||||
-- but keep it modest. Note: IDs the server doesn't know (gaps in the range)
|
||||
-- get no response, so their pending entries linger — warm ranges you expect
|
||||
-- to be mostly real, not arbitrary millions.
|
||||
--
|
||||
-- /warmcache item 1 40000 warm item IDs 1..40000
|
||||
-- /warmcache <type> <a> <b> type = item | quest | creature | object | all
|
||||
-- /warmcache stop abort an in-progress warm
|
||||
local WARM_RATE = 20 -- IDs queued per tick
|
||||
local WARM_INTERVAL = 0.2 -- seconds between ticks (-> ~20 IDs/sec)
|
||||
|
||||
local WARM_FN = {
|
||||
item = function(id) C_Item.RequestLoadItemDataByID(id) end,
|
||||
quest = function(id) C_QuestLog.RequestLoadQuestByID(id) end,
|
||||
creature = function(id) C_CreatureInfo.RequestLoadCreatureByID(id) end,
|
||||
object = function(id) C_GameObjectInfo.RequestLoadGameObjectByID(id) end,
|
||||
all = function(id)
|
||||
C_QuestLog.RequestLoadQuestByID(id)
|
||||
C_Item.RequestLoadItemDataByID(id)
|
||||
C_CreatureInfo.RequestLoadCreatureByID(id)
|
||||
C_GameObjectInfo.RequestLoadGameObjectByID(id)
|
||||
end,
|
||||
}
|
||||
|
||||
local warm = { running = false, cancel = false }
|
||||
|
||||
local function warm_msg(s)
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff66ccff[WarmCache]|r " .. s)
|
||||
end
|
||||
|
||||
local function warm_step(fn, id, stop, lastMsg)
|
||||
if warm.cancel then
|
||||
warm.running = false
|
||||
warm_msg("stopped at " .. (id - 1))
|
||||
return
|
||||
end
|
||||
local n = 0
|
||||
while n < WARM_RATE and id <= stop do
|
||||
fn(id)
|
||||
n = n + 1
|
||||
id = id + 1
|
||||
end
|
||||
if id > stop then
|
||||
warm.running = false
|
||||
warm_msg("done (through " .. stop .. ")")
|
||||
else
|
||||
if (id - 1) - lastMsg >= 1000 then
|
||||
warm_msg((id - 1) .. " / " .. stop .. " ...")
|
||||
lastMsg = id - 1
|
||||
end
|
||||
C_Timer.After(WARM_INTERVAL, function() warm_step(fn, id, stop, lastMsg) end)
|
||||
end
|
||||
end
|
||||
|
||||
SLASH_WARMCACHE1 = "/warmcache"
|
||||
SlashCmdList["WARMCACHE"] = function(msg)
|
||||
local args = {}
|
||||
for tok in string.gmatch(msg or "", "%S+") do
|
||||
table.insert(args, string.lower(tok))
|
||||
end
|
||||
local cmd = args[1]
|
||||
|
||||
if cmd == "stop" then
|
||||
if warm.running then warm.cancel = true else warm_msg("not running") end
|
||||
return
|
||||
end
|
||||
if warm.running then
|
||||
warm_msg("already running - /warmcache stop first")
|
||||
return
|
||||
end
|
||||
|
||||
local fn = WARM_FN[cmd]
|
||||
local a, b = tonumber(args[2]), tonumber(args[3])
|
||||
if not fn or not a or not b or a < 1 or b < a then
|
||||
warm_msg("usage: /warmcache item|quest|creature|object|all <startID> <stopID>")
|
||||
warm_msg(" /warmcache stop")
|
||||
return
|
||||
end
|
||||
|
||||
warm.running, warm.cancel = true, false
|
||||
warm_msg(string.format("warming %s %d..%d (~%d/sec, /warmcache stop to abort)",
|
||||
cmd, a, b, math.floor(WARM_RATE / WARM_INTERVAL)))
|
||||
warm_step(fn, a, b, a - 1)
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
local className = UnitClass('player')
|
||||
local classFile, classID = UnitClassBase('player')
|
||||
local className, classFile, classID = UnitClass('player'), UnitClassBase('player')
|
||||
|
||||
PlayerUtil = PlayerUtil or {}
|
||||
|
||||
@@ -32,12 +31,8 @@ function PlayerUtil.GetCurrentSpeed()
|
||||
return GetUnitSpeed("player") / 7 * 100
|
||||
end
|
||||
|
||||
local playerGuid
|
||||
function GetPlayerGuid()
|
||||
if not playerGuid then
|
||||
playerGuid = UnitGUID("player")
|
||||
end
|
||||
return playerGuid
|
||||
return UnitGUID("player")
|
||||
end
|
||||
|
||||
function IsPlayerGuid(guid)
|
||||
|
||||
@@ -1651,6 +1651,19 @@ enum Offsets {
|
||||
OFF_UNIT_DESCRIPTOR_CLASS_BYTE = 0x79,
|
||||
OFF_UNIT_DESCRIPTOR_SEX_BYTE = 0x7A,
|
||||
|
||||
// The local player's UNIT_FIELD_BYTES_0 race/class/sex bytes, mirrored
|
||||
// into a login-time global that is populated at character-enter —
|
||||
// BEFORE the in-world CGUnit/descriptor exists. Script_UnitClass
|
||||
// (0x00518350) and Script_UnitRace special-case the literal "player"
|
||||
// token to read these (Script_UnitClass calls FUN_005abde0 →
|
||||
// `return DAT_00c27e81`), which is why UnitClass("player") resolves at
|
||||
// addon-load while a descriptor read returns nil until the player
|
||||
// object spawns. Getters: race FUN_005abdd0, class FUN_005abde0,
|
||||
// sex FUN_005abdf0.
|
||||
VAR_PLAYER_RACE_BYTE = 0x00C27E80,
|
||||
VAR_PLAYER_CLASS_BYTE = 0x00C27E81,
|
||||
VAR_PLAYER_SEX_BYTE = 0x00C27E82,
|
||||
|
||||
// SkillLineAbility.dbc — maps each (class, race) pair to its
|
||||
// learnable spell list with skill-rank gating. Indexed by record
|
||||
// ID; reads via the standard `records[id]` pattern. Used by
|
||||
@@ -4205,6 +4218,29 @@ enum Offsets {
|
||||
VAR_GLUE_LOGIN_READY1 = 0x00B41DFC,
|
||||
VAR_GLUE_LOGIN_READY2 = 0x00B41E04,
|
||||
VAR_GLUE_LOGIN_INPROGRESS = 0x00B41DA0,
|
||||
// The same `VAR_GLUE_LOGIN_INPROGRESS` global doubles as a glue
|
||||
// state-code: `FUN_GLUE_ENTER_WORLD` sets it to `8` ("entering world")
|
||||
// on a successful commit, which `Unit::Identity`'s enter-world co-hook
|
||||
// gates on (the rename / char-locked bail paths return before it).
|
||||
GLUE_STATE_ENTERING_WORLD = 8,
|
||||
|
||||
// `FUN_0046B500` — the enter-world worker behind `Script_EnterWorld` (the
|
||||
// glue "Enter World" button: `Script_EnterWorld` is just `call this; ret`).
|
||||
// Computes the selected character's struct into VAR_GLUE_SELECTED_CHAR and,
|
||||
// on success, sets VAR_GLUE_LOGIN_INPROGRESS = 8. `Unit::Identity` co-hooks
|
||||
// it to capture the selected character's GUID *before* the engine creates
|
||||
// the in-world player object — so `UnitGUID("player")` / `PlayerGuid()`
|
||||
// answer from addon-load time instead of returning nil/0 until the
|
||||
// SMSG_UPDATE_OBJECT that populates the player object (≈PLAYER_LOGIN).
|
||||
FUN_GLUE_ENTER_WORLD = 0x0046B500,
|
||||
|
||||
// Selected-character struct pointer (glue): char-array base +
|
||||
// selectedIndex*0x120, written by FUN_GLUE_ENTER_WORLD. The character's
|
||||
// 64-bit login GUID is at offset 0x00 (name at +0x08, race +0x100, class
|
||||
// +0x101, level +0x108 — verified against `Script_GetCharacterInfo` at
|
||||
// `0x004732A0`). Entries are filled from SMSG_CHAR_ENUM.
|
||||
VAR_GLUE_SELECTED_CHAR = 0x00B41DF4,
|
||||
OFF_GLUE_CHAR_GUID = 0x00,
|
||||
|
||||
// Cursor state — single global type-tag at VAR_CURSOR_TYPE
|
||||
// dispatches reads through one of several payload slots depending
|
||||
|
||||
+25
-14
@@ -11,6 +11,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -71,21 +72,31 @@ int __fastcall Script_UnitClassBase(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
// Mirror Script_UnitClass: the literal "player" token reads the
|
||||
// login-time class byte global (populated at character-enter, before
|
||||
// the in-world descriptor exists), so this resolves at addon-load
|
||||
// instead of returning nil until the player object spawns. All other
|
||||
// tokens go through the unit descriptor.
|
||||
uint8_t classByte;
|
||||
if (Unit::Identity::IsPlayerToken(token)) {
|
||||
classByte = *reinterpret_cast<const uint8_t *>(Offsets::VAR_PLAYER_CLASS_BYTE);
|
||||
} else {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
classByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
}
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
const uint8_t classByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE);
|
||||
if (classByte == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
|
||||
+66
-13
@@ -26,6 +26,41 @@ namespace Unit::Identity {
|
||||
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
|
||||
// The selected character's GUID, captured at the glue "Enter World" click
|
||||
// (the FUN_GLUE_ENTER_WORLD co-hook below) — i.e. before the engine has
|
||||
// created the in-world player object whose +0xC0 holds the live GUID.
|
||||
// `PlayerGuid()` serves this as a fallback during the early-login window so
|
||||
// `UnitGUID("player")` answers from addon-load time instead of returning nil
|
||||
// until SMSG_UPDATE_OBJECT lands (≈PLAYER_LOGIN). 0 = not captured yet.
|
||||
// Overwritten on every enter, so a character switch can't serve a stale GUID;
|
||||
// only ever read when the live engine value is still 0, and identical to it
|
||||
// once that's populated.
|
||||
static uint64_t g_charSelectGuid = 0;
|
||||
|
||||
// `FUN_GLUE_ENTER_WORLD` is `void(void)` (no args). Co-hook it to snapshot the
|
||||
// selected character's GUID once the entry is committed.
|
||||
using EnterWorld_t = void(__cdecl *)();
|
||||
static EnterWorld_t g_origEnterWorld = nullptr;
|
||||
|
||||
static void __cdecl EnterWorld_h() {
|
||||
g_origEnterWorld();
|
||||
// Only on a genuine commit — the rename / char-locked bail paths return
|
||||
// before the worker sets the state code to "entering world".
|
||||
if (*reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GLUE_LOGIN_INPROGRESS)) !=
|
||||
Offsets::GLUE_STATE_ENTERING_WORLD)
|
||||
return;
|
||||
const uintptr_t charStruct = *reinterpret_cast<const uintptr_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_GLUE_SELECTED_CHAR));
|
||||
if (charStruct != 0)
|
||||
g_charSelectGuid = *reinterpret_cast<const uint64_t *>(
|
||||
charStruct + Offsets::OFF_GLUE_CHAR_GUID);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _enterWorldHook{
|
||||
Offsets::FUN_GLUE_ENTER_WORLD, reinterpret_cast<void *>(&EnterWorld_h),
|
||||
reinterpret_cast<void **>(&g_origEnterWorld)};
|
||||
|
||||
// FUN_TOKEN_TO_GUID's `"player"` path resolves the GUID by looking the
|
||||
// local player up in the engine's object manager: it calls FUN_00468550
|
||||
// to read `[VAR_LOCAL_PLAYER_PTR + 0xC0]`, then `FUN_00468460(type=0x10,
|
||||
@@ -38,21 +73,27 @@ using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
// either. Suffixed tokens (`"playertarget"` and the like) still go
|
||||
// through the engine so the chained `.target` walk works.
|
||||
//
|
||||
// This does NOT help at addon file-load time on first login. The
|
||||
// engine populates `+0xC0` from SMSG_UPDATE_OBJECT processing in the
|
||||
// main loop, which happens AFTER `FrameScript_Initialize` (and the
|
||||
// addon load pass it triggers) finishes. The engine itself gates its
|
||||
// own post-init player setup at the tail of FrameScript_Initialize on
|
||||
// `FUN_00468550() != 0` for the same reason — the GUID isn't ready
|
||||
// yet. Addons that need the player GUID at boot must wait for
|
||||
// `PLAYER_LOGIN`. See the trace notes in commit history.
|
||||
// The engine populates `+0xC0` from SMSG_UPDATE_OBJECT processing in the
|
||||
// main loop, which happens AFTER `FrameScript_Initialize` (and the addon
|
||||
// load pass it triggers) finishes — so the engine value is 0 at addon
|
||||
// file-load on first login. We bridge that window with `g_charSelectGuid`,
|
||||
// captured at the glue "Enter World" click (see the co-hook above): the
|
||||
// selected character's GUID is known well before the player object exists,
|
||||
// and it's the same value `+0xC0` will hold once it's live. So
|
||||
// `UnitGUID("player")` / `PlayerGuid()` answer correctly from addon-load
|
||||
// time. (The player *object* still doesn't exist until `+0xC0` populates —
|
||||
// this only makes the GUID *value* available early, for keying SavedVariables,
|
||||
// comparisons, our aura/cast caster checks, etc.)
|
||||
uint64_t PlayerGuid() {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
if (player != nullptr) {
|
||||
const uint64_t guid = *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
if (guid != 0)
|
||||
return guid; // live engine value — authoritative
|
||||
}
|
||||
return g_charSelectGuid; // early-login fallback (0 until char-select)
|
||||
}
|
||||
|
||||
uint64_t GuidForObject(const void *unitObject) {
|
||||
@@ -66,8 +107,20 @@ uint64_t GuidForObject(const void *unitObject) {
|
||||
return *reinterpret_cast<const uint64_t *>(block);
|
||||
}
|
||||
|
||||
bool IsPlayerToken(const char *token) {
|
||||
if (token == nullptr)
|
||||
return false;
|
||||
// Mirror Script_UnitClass/Script_UnitRace byte-for-byte: they compare the
|
||||
// token via Storm's SStrCmpI(token, "player", 0x7FFFFFFF) (the call at
|
||||
// 0x005183B0), not the CRT _stricmp. Same case-insensitive result for the
|
||||
// ASCII literal, but routed through the engine's own comparator.
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
return sstrcmpi(token, "player", 0x7FFFFFFF) == 0;
|
||||
}
|
||||
|
||||
uint64_t GuidForToken(const char *token) {
|
||||
if (token != nullptr && _stricmp(token, "player") == 0)
|
||||
if (IsPlayerToken(token))
|
||||
return PlayerGuid();
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(Offsets::FUN_TOKEN_TO_GUID);
|
||||
return fn(token);
|
||||
|
||||
@@ -36,6 +36,13 @@ uint64_t PlayerGuid();
|
||||
// caster cache keys on it from both the lookup and the OnAuraAdded hook).
|
||||
uint64_t GuidForObject(const void *unitObject);
|
||||
|
||||
// Case-insensitive match against the literal `"player"` token, mirroring
|
||||
// the engine's `SStrCmpI(token, "player")` fast-path tests (e.g.
|
||||
// `Script_UnitClass`/`Script_UnitRace`, which special-case `"player"` to
|
||||
// read login-time globals available before the in-world descriptor spawns).
|
||||
// Returns false for a null pointer.
|
||||
bool IsPlayerToken(const char *token);
|
||||
|
||||
// Resolves a unit token (`"player"`, `"target"`, `"party3"`, …) to its
|
||||
// 64-bit GUID, or 0 if the token currently maps to nothing. `"player"` is
|
||||
// read from the canonical local-player pointer so it works regardless of
|
||||
|
||||
+25
-14
@@ -11,6 +11,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -51,21 +52,31 @@ int __fastcall Script_UnitRaceBase(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
// Mirror Script_UnitRace: the literal "player" token reads the
|
||||
// login-time race byte global (populated at character-enter, before
|
||||
// the in-world descriptor exists), so this resolves at addon-load
|
||||
// instead of returning nil until the player object spawns. All other
|
||||
// tokens go through the unit descriptor.
|
||||
uint8_t raceByte;
|
||||
if (Unit::Identity::IsPlayerToken(token)) {
|
||||
raceByte = *reinterpret_cast<const uint8_t *>(Offsets::VAR_PLAYER_RACE_BYTE);
|
||||
} else {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
raceByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
}
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
}
|
||||
const uint8_t raceByte = *(desc + Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE);
|
||||
if (raceByte == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
|
||||
Reference in New Issue
Block a user