From b69c3a55a12381c88e25a28ef5d18ba71a71f06b Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Mon, 2 Mar 2026 21:46:42 -0800 Subject: [PATCH] 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. --- src/main.zig | 4 +- src/markers/addon/Markers.lua | 87 ++-------- src/markers/markers.zig | 292 ++++++++++++++++++++++++++-------- src/markers/offsets.zig | 35 ++++ 4 files changed, 277 insertions(+), 141 deletions(-) diff --git a/src/main.zig b/src/main.zig index 04878c6..51b98e9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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)); } } diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua index 25d9be2..42e3aa0 100644 --- a/src/markers/addon/Markers.lua +++ b/src/markers/addon/Markers.lua @@ -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 diff --git a/src/markers/markers.zig b/src/markers/markers.zig index 25f456c..4d8e852 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -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 { diff --git a/src/markers/offsets.zig b/src/markers/offsets.zig index c43f8a3..51fe0b6 100644 --- a/src/markers/offsets.zig +++ b/src/markers/offsets.zig @@ -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;