Move permission checks DLL-side: canSetMarkers, senderHasPermission, senderInGroup

Replace Lua-side permission checks with direct memory reads in the DLL.
All marker mutation functions now verify sender identity against the
raid roster / party leader GUID without touching Lua state.

- Add getPlayerGUID (0x468550), getNameFromGUID (name cache at 0xc0e228)
- Add canSetMarkers: checks local player is leader/officer via roster
- Add senderHasPermission: verifies sender name against roster ranks
- Add senderInGroup: weaker check for sync relay (any rank)
- WorldMarker/ClearWorldMarker return 1/nil for addon feedback
- SetMarkerDef/ClearMarkerDef take sender name param, verify DLL-side
- SetMarkerDefSync: dual check (local=leader + sender in group)
- CanSetMarkers() Lua function for addon broadcast decisions
- Remove Lua-side canSetMarkers/senderHasPermission from Markers.lua
- Add offsets: LEADER_GUID, RAID_ROSTER_ARRAY, RAID_MEMBER_COUNT, etc.
This commit is contained in:
MarcelineVQ
2026-03-02 21:46:42 -08:00
parent 0c2b038e45
commit b69c3a55a1
4 changed files with 277 additions and 141 deletions
+2 -2
View File
@@ -111,12 +111,12 @@ fn registerLuaFunctions() void {
if (build_opts.markers and markers.isActive()) {
registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker));
registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker));
registerFunction("GetPlayerPosition", @intFromPtr(&markers.luaGetPlayerPosition));
registerFunction("DistanceToMark", @intFromPtr(&markers.luaDistanceToMark));
registerFunction("SetMarkerDef", @intFromPtr(&markers.luaSetMarkerDef));
registerFunction("SetMarkerDefSync", @intFromPtr(&markers.luaSetMarkerDefSync));
registerFunction("ClearMarkerDef", @intFromPtr(&markers.luaClearMarkerDef));
registerFunction("GetMarkerDef", @intFromPtr(&markers.luaGetMarkerDef));
registerFunction("GetCurrentAreaId", @intFromPtr(&markers.luaGetCurrentAreaId));
registerFunction("CanSetMarkers", @intFromPtr(&markers.luaCanSetMarkers));
}
}
+16 -71
View File
@@ -1,7 +1,7 @@
-- Markers addon (embedded in DLL, loaded from memory)
-- Part of WeirdUtils - only loaded when markers module is compiled
MARKERS_VERSION = 3
MARKERS_VERSION = 4
BINDING_HEADER_MARKERS = "Markers"
@@ -16,6 +16,11 @@ end
-- =============================================================================
-- Addon message protocol
-- Delimiter is ":" (pipe "|" is WoW's escape char for color codes)
--
-- Permission model: ALL permission checks are enforced DLL-side.
-- WorldMarker/ClearWorldMarker: DLL checks local player is leader/assist.
-- SetMarkerDef/ClearMarkerDef: DLL checks sender name against roster.
-- CanSetMarkers(): DLL returns 1 if local player has permission.
-- =============================================================================
local MSG_PREFIX = "WMark"
@@ -30,59 +35,9 @@ local function getChannel()
return nil
end
local function canSetMarkers()
if GetNumRaidMembers() > 0 then
for i = 1, 40 do
local name, rank = GetRaidRosterInfo(i)
if name == UnitName("player") then
log("canSet: raid rank=" .. rank .. (rank >= 1 and " YES" or " NO"))
return rank >= 1 -- 1=assist, 2=leader
end
end
log("canSet: not in raid roster")
return false
elseif GetNumPartyMembers() > 0 then
local result = IsPartyLeader()
log("canSet: party leader=" .. tostring(result))
return result
end
log("canSet: solo YES")
return true -- solo: always allowed
end
local function senderHasPermission(sender)
if GetNumRaidMembers() > 0 then
for i = 1, 40 do
local name, rank = GetRaidRosterInfo(i)
if name == sender then
log("perm: " .. sender .. " rank=" .. rank .. (rank >= 1 and " OK" or " NO"))
return rank >= 1
end
end
log("perm: " .. sender .. " not in roster")
return false
elseif GetNumPartyMembers() > 0 then
-- Find the sender's unit ID to check if they're party leader
for i = 1, GetNumPartyMembers() do
if UnitName("party" .. i) == sender then
local isLeader = UnitIsPartyLeader("party" .. i)
log("perm: " .. sender .. "=party" .. i .. " ldr=" .. tostring(isLeader))
return isLeader == 1
end
end
log("perm: " .. sender .. " not in party")
return false
end
log("perm: no group")
return false
end
local function broadcastPlace(index, x, y, z, areaId)
local ch = getChannel()
if not ch then
log("send: no channel")
return
end
if not ch then return end
local msg = "P:" .. index .. ":" .. x .. ":" .. y .. ":" .. z .. ":" .. areaId
log("SEND [" .. ch .. "] " .. msg)
SendAddonMessage(MSG_PREFIX, msg, ch)
@@ -193,6 +148,7 @@ end
-- =============================================================================
-- Addon message handler
-- All mutation commands pass sender name to the DLL for permission check.
-- =============================================================================
local function parseMarkerFields(parts)
@@ -226,12 +182,10 @@ local function onAddonMessage(prefix, message, channel, sender)
local cmd = parts[1]
log(" cmd=" .. tostring(cmd) .. " n=" .. table.getn(parts))
-- SR/LSR/SF don't mutate state — handled before permission check.
-- P, C, CA mutate state and require sender to have leader/assist permission.
if cmd == "SR" then
-- Normal sync request: only leader/assist responds
log(" sync request from " .. tostring(sender))
if canSetMarkers() then
if CanSetMarkers() then
broadcastAllDefs()
end
return
@@ -251,37 +205,28 @@ local function onAddonMessage(prefix, message, channel, sender)
end
local idx, x, y, z, areaId = parseMarkerFields(parts)
if idx then
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
SetMarkerDef(idx, x, y, z, areaId)
SetMarkerDefSync(idx, x, y, z, areaId, sender)
else
log(" PARSE FAIL")
end
return
end
-- Mutation commands require permission
if not senderHasPermission(sender) then
log(" no perm, skip")
return
end
-- P, C, CA — DLL checks sender permission via SetMarkerDef/ClearMarkerDef
if cmd == "P" then
local idx, x, y, z, areaId = parseMarkerFields(parts)
if idx then
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
SetMarkerDef(idx, x, y, z, areaId)
SetMarkerDef(idx, x, y, z, areaId, sender)
else
log(" PARSE FAIL")
end
elseif cmd == "C" then
local idx = tonumber(parts[2])
if idx then
log(" ClearMarkerDef(" .. idx .. ")")
ClearMarkerDef(idx)
ClearMarkerDef(idx, sender)
end
elseif cmd == "CA" then
log(" ClearAll")
ClearMarkerDef()
ClearMarkerDef(sender)
else
log(" unknown: " .. tostring(cmd))
end
@@ -299,7 +244,7 @@ local function broadcastSyncRequest()
end
-- Reset first-responder lock before requesting
syncSender = nil
local cmd = canSetMarkers() and "LSR" or "SR"
local cmd = CanSetMarkers() and "LSR" or "SR"
log("SEND [" .. ch .. "] " .. cmd)
SendAddonMessage(MSG_PREFIX, cmd, ch)
end
@@ -357,7 +302,7 @@ rosterTimer:SetScript("OnUpdate", function()
rosterTimer.pending = false
rosterTimer.extensions = 0
log("roster timer fired, broadcasting")
if canSetMarkers() then
if CanSetMarkers() then
broadcastAllDefs()
end
end
+224 -68
View File
@@ -115,27 +115,147 @@ const sc = std.builtin.CallingConvention{ .x86_stdcall = .{} };
// Permission check — leader or raid officer required
// =============================================================================
/// WoW Lua C functions: ECX=L, plain ret, return count of pushed values.
const LuaCFn = *const fn (lua.State) callconv(fc) u32;
const FN_IS_PARTY_LEADER: LuaCFn = @ptrFromInt(0x004e9130);
const FN_IS_RAID_OFFICER: LuaCFn = @ptrFromInt(0x004bb910);
/// Get local player GUID via GetPlayerGUID (0x468550).
/// __fastcall(), no params, returns EAX(low):EDX(high).
fn getPlayerGUID() u64 {
var lo: u32 = undefined;
var hi: u32 = undefined;
asm volatile ("call *%[func]"
: [_] "={eax}" (lo),
[_] "={edx}" (hi),
: [func] "r" (o.FN_GET_PLAYER_GUID),
: .{ .ecx = true, .memory = true, .cc = true });
return (@as(u64, hi) << 32) | lo;
}
/// Call a WoW Lua C function that pushes 1 value, check if it pushed
/// a truthy value (non-nil), then pop the result.
fn luaBoolCheck(L: lua.State, func: LuaCFn) bool {
const before = lua.gettop(L);
_ = func(L);
const after = lua.gettop(L);
if (after <= before) return false;
const is_nil = lua.typeOf(L, -1) == 0;
lua.settop(L, before);
return !is_nil;
/// Look up a player name from the name cache by GUID.
/// Calls RetrieveNPCDataFromCache — __thiscall(ECX=cache), 6 stack params, RET 0x18.
fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 {
if (guid_lo == 0 and guid_hi == 0) return null;
var name_buf: [2]u32 = .{ 0, 0 };
const stack_args = [6]u32{
guid_lo,
guid_hi,
@intFromPtr(&name_buf),
0, 0, 0,
};
const result: u32 = asm volatile (
\\ push 20(%[a])
\\ push 16(%[a])
\\ push 12(%[a])
\\ push 8(%[a])
\\ push 4(%[a])
\\ push (%[a])
\\ call *%[func]
: [ret] "={eax}" (-> u32),
: [_] "{ecx}" (@as(u32, o.NAME_CACHE_OBJ)),
[a] "r" (&stack_args),
[func] "r" (@as(u32, o.FN_NAME_CACHE_LOOKUP)),
: .{ .edx = true, .memory = true, .cc = true });
return if (result != 0) @ptrFromInt(result) else null;
}
/// Check if the local player has permission to place/clear markers.
/// Uses direct memory reads — no Lua state required.
/// Requires: party leader, raid leader, or raid officer (assist).
fn canSetMarkers(L: lua.State) bool {
return luaBoolCheck(L, FN_IS_PARTY_LEADER) or luaBoolCheck(L, FN_IS_RAID_OFFICER);
fn canSetMarkers() bool {
const player_guid = getPlayerGUID();
if (player_guid == 0) return false;
const player_lo: u32 = @truncate(player_guid);
const player_hi: u32 = @truncate(player_guid >> 32);
const raid_count = hook.readMem(u32, o.RAID_MEMBER_COUNT);
if (raid_count > 0) {
// Raid: check if player is leader or has rank > 0 (officer/assist)
const count = @min(raid_count, 40);
for (0..count) |i| {
const entry = hook.readMem(u32, o.RAID_ROSTER_ARRAY + i * 4);
if (entry == 0 or entry < 0x10000) continue;
const guid_lo = hook.readMem(u32, entry);
const guid_hi = hook.readMem(u32, entry + 4);
if (guid_lo == player_lo and guid_hi == player_hi) {
return hook.readMem(i32, entry + o.ROSTER_ENTRY_RANK) > 0;
}
}
return false;
}
// Party: check if player is the leader
const leader_lo = hook.readMem(u32, o.LEADER_GUID);
const leader_hi = hook.readMem(u32, o.LEADER_GUID + 4);
if (leader_lo == 0 and leader_hi == 0) return false;
return player_lo == leader_lo and player_hi == leader_hi;
}
/// Check if a named sender has permission (leader or raid officer).
/// Used to authenticate incoming addon messages — sender name comes from
/// CHAT_MSG_ADDON arg4 (server-verified, can't be spoofed).
fn senderHasPermission(sender: [*:0]const u8) bool {
const sender_span = std.mem.span(sender);
if (sender_span.len == 0) return false;
const raid_count = hook.readMem(u32, o.RAID_MEMBER_COUNT);
if (raid_count > 0) {
// Raid: find sender in roster, check rank > 0
const count = @min(raid_count, 40);
for (0..count) |i| {
const entry = hook.readMem(u32, o.RAID_ROSTER_ARRAY + i * 4);
if (entry == 0 or entry < 0x10000) continue;
const rank = hook.readMem(i32, entry + o.ROSTER_ENTRY_RANK);
if (rank <= 0) continue; // skip non-officers
const guid_lo = hook.readMem(u32, entry);
const guid_hi = hook.readMem(u32, entry + 4);
const name = getNameFromGUID(guid_lo, guid_hi) orelse continue;
if (std.mem.eql(u8, std.mem.span(name), sender_span)) {
return true;
}
}
return false;
}
// Party: check if sender is the leader
const leader_lo = hook.readMem(u32, o.LEADER_GUID);
const leader_hi = hook.readMem(u32, o.LEADER_GUID + 4);
if (leader_lo == 0 and leader_hi == 0) return false;
const leader_name = getNameFromGUID(leader_lo, leader_hi) orelse return false;
return std.mem.eql(u8, std.mem.span(leader_name), sender_span);
}
/// Check if a named sender is in the group (any rank).
/// Weaker than senderHasPermission — used for sync relay (SF) where the
/// sender is just echoing stored data, not issuing a command.
fn senderInGroup(sender: [*:0]const u8) bool {
const sender_span = std.mem.span(sender);
if (sender_span.len == 0) return false;
const raid_count = hook.readMem(u32, o.RAID_MEMBER_COUNT);
if (raid_count > 0) {
// Raid: find sender anywhere in roster (any rank)
const count = @min(raid_count, 40);
for (0..count) |i| {
const entry = hook.readMem(u32, o.RAID_ROSTER_ARRAY + i * 4);
if (entry == 0 or entry < 0x10000) continue;
const guid_lo = hook.readMem(u32, entry);
const guid_hi = hook.readMem(u32, entry + 4);
const name = getNameFromGUID(guid_lo, guid_hi) orelse continue;
if (std.mem.eql(u8, std.mem.span(name), sender_span)) {
return true;
}
}
return false;
}
// Party: check if sender is any party member
for (0..4) |i| {
const guid_lo = hook.readMem(u32, o.PARTY_MEMBER_GUIDS + i * 8);
const guid_hi = hook.readMem(u32, o.PARTY_MEMBER_GUIDS + i * 8 + 4);
if (guid_lo == 0 and guid_hi == 0) continue;
const name = getNameFromGUID(guid_lo, guid_hi) orelse continue;
if (std.mem.eql(u8, std.mem.span(name), sender_span)) {
return true;
}
}
return false;
}
// =============================================================================
@@ -397,7 +517,7 @@ fn clearAllMarkers() void {
/// Lua: local ok = WorldMarker(index [, x, y, z | "unitId"])
/// Returns 1 on success, nil on permission denied.
pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
if (!canSetMarkers(L)) {
if (!canSetMarkers()) {
con.print("[markers] WorldMarker: no permission\n");
return 0; // nil — addon shows user message
}
@@ -446,7 +566,7 @@ pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
/// Lua: local ok = ClearWorldMarker([index])
/// Returns 1 on success, nil on permission denied.
pub fn luaClearWorldMarker(L: lua.State) callconv(.c) u32 {
if (!canSetMarkers(L)) {
if (!canSetMarkers()) {
con.print("[markers] ClearWorldMarker: no permission\n");
return 0;
}
@@ -549,53 +669,20 @@ fn tickAnimations() void {
}
}
/// Lua: local x, y, z = GetPlayerPosition()
pub fn luaGetPlayerPosition(L: lua.State) callconv(.c) u32 {
const player = wow.getLocalPlayer();
if (player == 0) return 0;
const pos = getUnitPosition(player);
lua.pushnumber(L, @floatCast(pos.x));
lua.pushnumber(L, @floatCast(pos.y));
lua.pushnumber(L, @floatCast(pos.z));
return 3;
}
/// Lua: local dist = DistanceToMark(index)
/// Returns distance in yards from player to the stored marker position,
/// or nil if the marker doesn't exist or there's no player.
pub fn luaDistanceToMark(L: lua.State) callconv(.c) u32 {
const nargs = lua.gettop(L);
if (nargs < 1 or !lua.isnumber(L, 1)) return 0;
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
const index: usize = @intCast(raw_index - 1);
if (!marker_defs[index].active) return 0;
const mark_pos = marker_defs[index].pos;
const player = wow.getLocalPlayer();
if (player == 0) return 0;
const player_pos = getUnitPosition(player);
if (player_pos.x == 0 and player_pos.y == 0 and player_pos.z == 0) return 0;
const dx = player_pos.x - mark_pos.x;
const dy = player_pos.y - mark_pos.y;
const dz = player_pos.z - mark_pos.z;
const dist = @sqrt(dx * dx + dy * dy + dz * dz);
lua.pushnumber(L, @floatCast(dist));
return 1;
}
/// Lua: SetMarkerDef(index, x, y, z, areaId)
/// Lua: SetMarkerDef(index, x, y, z, areaId, senderName)
/// Store a marker definition without immediately spawning. Used by the addon
/// when receiving remote marker data — proximity respawn handles entity creation.
/// senderName is verified against the group roster for leader/officer permission.
pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 {
const nargs = lua.gettop(L);
if (nargs < 5) return 0;
if (!lua.isnumber(L, 1) or !lua.isnumber(L, 2) or !lua.isnumber(L, 3) or !lua.isnumber(L, 4) or !lua.isnumber(L, 5)) return 0;
if (nargs < 6) return 0;
if (!lua.isnumber(L, 1) or !lua.isnumber(L, 2) or !lua.isnumber(L, 3) or !lua.isnumber(L, 4) or !lua.isnumber(L, 5) or !lua.isstring(L, 6)) return 0;
const sender = lua.tostring(L, 6) orelse return 0;
if (!senderHasPermission(sender)) {
con.fmt("[markers] SetMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
return 0;
}
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
@@ -619,20 +706,78 @@ pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 {
return 0;
}
/// Lua: ClearMarkerDef([index])
/// Clear a definition (and its entity if alive). No args = clear all.
/// Lua: SetMarkerDefSync(index, x, y, z, areaId, senderName)
/// Like SetMarkerDef but for sync relay (SF messages). Two checks:
/// 1. Local player must be leader/assist (only they request syncs)
/// 2. Sender must be in the group (any rank — they're just relaying data)
pub fn luaSetMarkerDefSync(L: lua.State) callconv(.c) u32 {
const nargs = lua.gettop(L);
if (nargs < 6) return 0;
if (!lua.isnumber(L, 1) or !lua.isnumber(L, 2) or !lua.isnumber(L, 3) or !lua.isnumber(L, 4) or !lua.isnumber(L, 5) or !lua.isstring(L, 6)) return 0;
if (!canSetMarkers()) {
con.print("[markers] SetMarkerDefSync: local player not leader/assist\n");
return 0;
}
const sender = lua.tostring(L, 6) orelse return 0;
if (!senderInGroup(sender)) {
con.fmt("[markers] SetMarkerDefSync: sender '{s}' not in group\n", .{std.mem.span(sender)});
return 0;
}
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
const index: usize = @intCast(raw_index - 1);
const x: f32 = @floatCast(lua.tonumber(L, 2));
const y: f32 = @floatCast(lua.tonumber(L, 3));
const z: f32 = @floatCast(lua.tonumber(L, 4));
const area_id: u32 = @intFromFloat(lua.tonumber(L, 5));
clearEntity(index);
marker_defs[index] = .{
.pos = .{ .x = x, .y = y, .z = z },
.area_id = area_id,
.active = true,
};
con.fmt("[markers] SetMarkerDefSync [{d}] at {d:.1},{d:.1},{d:.1} area={d}\n", .{ index + 1, x, y, z, area_id });
return 0;
}
/// Lua: ClearMarkerDef(senderName) — clear all
/// Lua: ClearMarkerDef(index, senderName) — clear one
/// senderName is verified against the group roster for leader/officer permission.
pub fn luaClearMarkerDef(L: lua.State) callconv(.c) u32 {
const nargs = lua.gettop(L);
if (nargs == 0) {
if (nargs < 1) return 0;
if (lua.isstring(L, 1) and (nargs == 1 or !lua.isnumber(L, 1))) {
// ClearMarkerDef(senderName) — clear all
const sender = lua.tostring(L, 1) orelse return 0;
if (!senderHasPermission(sender)) {
con.fmt("[markers] ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
return 0;
}
clearAllMarkers();
return 0;
}
if (!lua.isnumber(L, 1)) return 0;
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
if (nargs >= 2 and lua.isnumber(L, 1) and lua.isstring(L, 2)) {
// ClearMarkerDef(index, senderName) — clear one
const sender = lua.tostring(L, 2) orelse return 0;
if (!senderHasPermission(sender)) {
con.fmt("[markers] ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
return 0;
}
const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
clearMarker(@intCast(raw_index - 1));
return 0;
}
clearMarker(@intCast(raw_index - 1));
return 0;
}
@@ -655,6 +800,17 @@ pub fn luaGetMarkerDef(L: lua.State) callconv(.c) u32 {
return 4;
}
/// Lua: local ok = CanSetMarkers()
/// Returns 1 if the local player has permission (leader/assist), nil otherwise.
/// Used by the addon for broadcast/sync decisions.
pub fn luaCanSetMarkers(L: lua.State) callconv(.c) u32 {
if (canSetMarkers()) {
lua.pushnumber(L, 1.0);
return 1;
}
return 0;
}
/// Lua: local areaId = GetCurrentAreaId()
/// Returns the current zone area ID from the game global.
pub fn luaGetCurrentAreaId(L: lua.State) callconv(.c) u32 {
+35
View File
@@ -197,3 +197,38 @@ pub const FN_LOAD_MODEL_ASYNC: usize = 0x0071d4e0;
/// Parses model header from buffer at model+0x130 (ptr) / model+0x134 (size),
/// initializes model resources, sets bit 0 of model+8 when done.
pub const FN_PROCESS_LOADED_MODEL_DATA: usize = 0x0071d640;
// =============================================================================
// Permission check — leader / raid officer
// =============================================================================
/// Group leader GUID (64-bit): low u32 at +0, high u32 at +4.
/// Valid for both party leader and raid leader.
pub const LEADER_GUID: usize = 0x00bc75f8;
/// Raid roster — array of 40 pointers to roster entry structs.
/// Entry layout: +0x00/+0x04 = GUID (u64), +0x08 = subgroup, +0x0C = rank.
/// Rank: 0 = member, 1 = assistant, 2 = leader.
pub const RAID_ROSTER_ARRAY: usize = 0x00b712a8;
/// Number of raid members (u32). 0 when not in a raid.
pub const RAID_MEMBER_COUNT: usize = 0x00b713e0;
/// Offset within a roster entry to the rank field (i32).
pub const ROSTER_ENTRY_RANK: usize = 0x0C;
/// Party member GUIDs — 4 slots, 8 bytes each (lo/hi u32 pairs).
/// Contains other party members (not local player). Stride = 8.
pub const PARTY_MEMBER_GUIDS: usize = 0x00bc6f48;
/// GetPlayerGUID — __fastcall(), no params, returns EAX(low):EDX(high).
pub const FN_GET_PLAYER_GUID: usize = 0x00468550;
/// RetrieveNPCDataFromCache — __thiscall(ECX=cache_obj), 6 stack params, RET 0x18.
/// (guid_low, guid_high, name_buf_ptr, 0, 0, 0) → char* name or NULL.
/// Used by GetRaidRosterInfo to resolve player names from GUIDs.
pub const FN_NAME_CACHE_LOOKUP: usize = 0x0055f080;
/// Name cache object — static instance at this address. Passed as ECX (this)
/// to RetrieveNPCDataFromCache.
pub const NAME_CACHE_OBJ: usize = 0x00c0e228;