Filter ritual (type 18) and mage portal (type 22) GOs whose creator is a player not in the local player's party or raid. Completely unclickable, not just deprioritized. Also: - Add wow.isInGroup() shared group membership check (party + raid) - Add party/raid addresses to shared offsets.zig - Fix dpslog party member GUID address (was 0xBC7600, correct: 0xBC6F48) Verified from client's is_player_in_allowed_list @ 0x4e7f70
367 lines
13 KiB
Zig
367 lines
13 KiB
Zig
//! Shared game memory access wrappers for WoW 1.12.1.
|
|
//!
|
|
//! Provides safe read helpers for the object manager, unit descriptors,
|
|
//! corpse fields, raid targets, map queries, and calling-convention
|
|
//! wrappers for game functions.
|
|
|
|
const std = @import("std");
|
|
const hook = @import("zhook");
|
|
const o = @import("offsets.zig");
|
|
|
|
// Re-export ObjectType so consumers can use wow.ObjectType
|
|
pub const ObjectType = enum(u32) {
|
|
null_obj = 0,
|
|
item = 1,
|
|
container = 2,
|
|
unit = 3,
|
|
player = 4,
|
|
game_object = 5,
|
|
dynamic_object = 6,
|
|
corpse = 7,
|
|
_,
|
|
};
|
|
|
|
pub const Vec3 = struct {
|
|
x: f32,
|
|
y: f32,
|
|
z: f32,
|
|
};
|
|
|
|
// =============================================================================
|
|
// Pointer validation
|
|
// =============================================================================
|
|
|
|
const WINAPI = std.builtin.CallingConvention.winapi;
|
|
|
|
extern "kernel32" fn IsBadReadPtr(
|
|
lp: ?*const anyopaque,
|
|
ucb: usize,
|
|
) callconv(WINAPI) i32;
|
|
|
|
/// Quick sanity check — reject null, low-address, and kernel-space pointers.
|
|
pub fn isValidPtr(addr: u32) bool {
|
|
return addr >= 0x10000 and addr < 0x7F000000;
|
|
}
|
|
|
|
/// Page-level read check via Windows API.
|
|
pub fn isReadablePtr(addr: u32, size: usize) bool {
|
|
if (addr < 0x10000 or addr >= 0x7F000000) return false;
|
|
return IsBadReadPtr(@ptrFromInt(addr), size) == 0;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Object manager traversal
|
|
// =============================================================================
|
|
|
|
pub fn isInGame() bool {
|
|
return hook.readMem(u32, o.IS_IN_WORLD) != 0;
|
|
}
|
|
|
|
pub fn objectFirst() u32 {
|
|
const obj_mgr = hook.readMem(u32, o.OBJECT_MANAGER_PTR);
|
|
if (obj_mgr == 0) return 0;
|
|
return hook.readMem(u32, obj_mgr + o.OBJECT_LIST_OFFSET);
|
|
}
|
|
|
|
pub fn objectNext(current: u32) u32 {
|
|
if (current == 0 or (current & 1) != 0) return 0;
|
|
const obj_mgr = hook.readMem(u32, o.OBJECT_MANAGER_PTR);
|
|
if (obj_mgr == 0) return 0;
|
|
const base = hook.readMem(u32, obj_mgr + o.OBJECT_NEXT_OFFSET);
|
|
const next = hook.readMem(u32, base + current + 4);
|
|
if (next == 0 or (next & 1) != 0) return 0;
|
|
return next;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Object field reads
|
|
// =============================================================================
|
|
|
|
pub fn getObjectType(obj: u32) ObjectType {
|
|
if (!isValidPtr(obj)) return .null_obj;
|
|
return @enumFromInt(hook.readMem(u32, obj + o.OBJECT_TYPE_OFFSET));
|
|
}
|
|
|
|
pub fn getObjectTypeRaw(obj: u32) u32 {
|
|
if (!isValidPtr(obj)) return 0;
|
|
return hook.readMem(u32, obj + o.OBJECT_TYPE_OFFSET);
|
|
}
|
|
|
|
pub fn readGUID(addr: u32) u64 {
|
|
const lo = hook.readMem(u32, addr);
|
|
const hi = hook.readMem(u32, addr + 4);
|
|
return (@as(u64, hi) << 32) | lo;
|
|
}
|
|
|
|
pub fn getObjectGUID(obj: u32) u64 {
|
|
if (!isValidPtr(obj)) return 0;
|
|
return readGUID(obj + o.OBJECT_GUID_OFFSET);
|
|
}
|
|
|
|
/// Read the descriptor/m_data pointer (at obj + 0x08).
|
|
pub fn getDescriptor(obj: u32) u32 {
|
|
if (!isValidPtr(obj)) return 0;
|
|
return hook.readMem(u32, obj + o.OBJECT_DATA_OFFSET);
|
|
}
|
|
|
|
/// Read the unit descriptor pointer (at obj + 0x110).
|
|
pub fn getUnitDescriptor(obj: u32) u32 {
|
|
if (!isValidPtr(obj)) return 0;
|
|
return hook.readMem(u32, obj + o.UNIT_DESCRIPTOR_OFFSET);
|
|
}
|
|
|
|
pub fn getObjectEntry(obj: u32) u32 {
|
|
const desc = getDescriptor(obj);
|
|
if (!isValidPtr(desc)) return 0;
|
|
return hook.readMem(u32, desc + o.DESC_ENTRY);
|
|
}
|
|
|
|
pub fn getNpcFlags(obj: u32) u32 {
|
|
const desc = getDescriptor(obj);
|
|
if (!isValidPtr(desc)) return 0;
|
|
return hook.readMem(u32, desc + o.DESC_NPC_FLAGS);
|
|
}
|
|
|
|
pub fn isLootable(obj: u32) bool {
|
|
const desc = getDescriptor(obj);
|
|
if (!isValidPtr(desc)) return false;
|
|
return (hook.readMem(u32, desc + o.DESC_UNIT_DYNAMIC_FLAGS) & o.UNIT_DYNFLAG_LOOTABLE) != 0;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Unit helpers
|
|
// =============================================================================
|
|
|
|
pub fn isUnitDead(unit: u32) bool {
|
|
if (!isValidPtr(unit)) return false;
|
|
const desc = hook.readMem(u32, unit + o.UNIT_DESCRIPTOR_OFFSET);
|
|
if (!isValidPtr(desc)) return false;
|
|
const flags = hook.readMem(u32, desc + o.UNIT_FLAGS_OFFSET);
|
|
return (flags & o.UNIT_FLAG_DEAD) != 0;
|
|
}
|
|
|
|
pub fn getUnitPosition(unit: u32) Vec3 {
|
|
if (unit == 0) return .{ .x = 0, .y = 0, .z = 0 };
|
|
const movement = hook.readMem(u32, unit + o.UNIT_MOVEMENT_OFFSET);
|
|
if (movement == 0 or movement < 0x10000) return .{ .x = 0, .y = 0, .z = 0 };
|
|
return .{
|
|
.x = hook.readMem(f32, movement + o.MOVEMENT_POS_X),
|
|
.y = hook.readMem(f32, movement + o.MOVEMENT_POS_Y),
|
|
.z = hook.readMem(f32, movement + o.MOVEMENT_POS_Z),
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Corpse helpers
|
|
// =============================================================================
|
|
|
|
pub fn isSkeletonCorpse(obj: u32) bool {
|
|
if (!isValidPtr(obj)) return false;
|
|
const desc = hook.readMem(u32, obj + o.OBJECT_DATA_OFFSET);
|
|
if (!isValidPtr(desc)) return false;
|
|
const flags = hook.readMem(u32, desc + o.CORPSE_FIELD_FLAGS);
|
|
return (flags & o.CORPSE_FLAG_BONE) != 0;
|
|
}
|
|
|
|
pub fn getCorpseOwnerGUID(obj: u32) u64 {
|
|
if (!isValidPtr(obj)) return 0;
|
|
const desc = hook.readMem(u32, obj + o.OBJECT_DATA_OFFSET);
|
|
if (!isValidPtr(desc)) return 0;
|
|
return readGUID(desc + o.CORPSE_FIELD_OWNER);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Map / Battleground detection
|
|
// =============================================================================
|
|
|
|
/// Check if the current map is a battleground by reading Map.dbc mapType.
|
|
pub fn isInBattleground() bool {
|
|
const obj_mgr = hook.readMem(u32, o.OBJECT_MANAGER_PTR);
|
|
if (obj_mgr == 0) return false;
|
|
const map_id = hook.readMem(u32, obj_mgr + o.OBJMGR_MAP_ID_OFFSET);
|
|
const dbc_max = hook.readMem(u32, o.MAP_DBC_MAX);
|
|
if (map_id > dbc_max) return false;
|
|
const table_base = hook.readMem(u32, o.MAP_DBC_DATA);
|
|
if (table_base == 0) return false;
|
|
const row = hook.readMem(u32, table_base + map_id * 4);
|
|
if (row == 0) return false;
|
|
const map_type = hook.readMem(u32, row + o.MAP_DBC_MAP_TYPE_OFFSET);
|
|
return map_type == o.MAP_TYPE_BATTLEGROUND;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Game function wrappers
|
|
// =============================================================================
|
|
|
|
/// UnitGUID("player") / UnitGUID("target") → 64-bit GUID.
|
|
pub fn unitGUID(unit_id: [*:0]const u8) u64 {
|
|
return hook.call(fn ([*:0]const u8) callconv(hook.cc.fastcall) u64, o.FN_UNIT_GUID, .{unit_id});
|
|
}
|
|
|
|
/// Resolve a GUID → object pointer via the object manager hash table.
|
|
pub fn getObjectByGUID(guid: u64) u32 {
|
|
if (guid == 0) return 0;
|
|
if (hook.readMem(u32, o.OBJECT_MANAGER_PTR) == 0) return 0;
|
|
const lo: u32 = @truncate(guid);
|
|
const hi: u32 = @truncate(guid >> 32);
|
|
const result = hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, o.FN_GET_OBJECT_BY_GUID, .{ lo, hi });
|
|
// Guard: hash table can return stale/invalid pointers for destroyed objects
|
|
if (result != 0 and !isValidPtr(result)) return 0;
|
|
return result;
|
|
}
|
|
|
|
/// Split-GUID variant for callers that already have lo/hi parts.
|
|
pub fn getObjectByGUIDSplit(guid_lo: u32, guid_hi: u32) u32 {
|
|
if (guid_lo == 0 and guid_hi == 0) return 0;
|
|
if (hook.readMem(u32, o.OBJECT_MANAGER_PTR) == 0) return 0;
|
|
return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, o.FN_GET_OBJECT_BY_GUID, .{ guid_lo, guid_hi });
|
|
}
|
|
|
|
/// ClntObjMgrGetActivePlayer → local player GUID.
|
|
pub fn getPlayerGUID() u64 {
|
|
return hook.call(fn () callconv(hook.cc.fastcall) u64, o.FN_GET_PLAYER_GUID, .{});
|
|
}
|
|
|
|
/// Get the local player's object pointer.
|
|
pub fn getLocalPlayer() u32 {
|
|
const guid = unitGUID("player");
|
|
if (guid == 0) return 0;
|
|
return getObjectByGUID(guid);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Group membership (party + raid)
|
|
// =============================================================================
|
|
|
|
/// Check if a GUID is the local player or in the player's party/raid.
|
|
pub fn isInGroup(guid: u64) bool {
|
|
if (guid == 0) return false;
|
|
if (getPlayerGUID() == guid) return true;
|
|
|
|
// Party: 4 GUID slots at PARTY_MEMBER_GUIDS
|
|
for (0..4) |i| {
|
|
const member = readGUID(@intCast(o.PARTY_MEMBER_GUIDS + i * 8));
|
|
if (member != 0 and member == guid) return true;
|
|
}
|
|
|
|
// Raid roster
|
|
const count = hook.readMem(u32, o.RAID_ROSTER_COUNT);
|
|
var j: u32 = 0;
|
|
while (j < count and j < 40) : (j += 1) {
|
|
const entry_ptr = hook.readMem(u32, @intCast(o.RAID_ROSTER_ARRAY + j * 4));
|
|
if (entry_ptr == 0) continue;
|
|
if (readGUID(entry_ptr) == guid) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Creature cache at address 0xC0E138 (not a pointer — the object IS at this address).
|
|
const CREATURE_CACHE: u32 = 0xC0E138;
|
|
|
|
/// Cache_RequestData — generic cache lookup.
|
|
/// __thiscall(ECX=cacheObj, stack: entryId, guidPtr, callback, callbackArg, flags)
|
|
/// Returns pointer to cache row or NULL.
|
|
const FN_CACHE_REQUEST: usize = 0x566240;
|
|
|
|
/// Resolve a GUID to a unit/NPC name.
|
|
pub fn getNameByGUID(guid: u64) [*:0]const u8 {
|
|
if (guid == 0) return "";
|
|
const lo: u32 = @truncate(guid);
|
|
const hi: u32 = @truncate(guid >> 32);
|
|
|
|
// Check GUID type — high bytes distinguish player vs creature
|
|
const guid_type: u32 = hi & 0xF0F00000;
|
|
if (guid_type == 0) {
|
|
// Player GUID — use player name cache (RetrieveNPCDataFromCache)
|
|
var name_buf: [2]u32 = .{ 0, 0 };
|
|
const result = hook.call(fn (u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.thiscall) u32, o.FN_NAME_CACHE_LOOKUP, .{
|
|
o.NAME_CACHE_OBJ, lo, hi, @intFromPtr(&name_buf), 0, 0, 0,
|
|
});
|
|
if (result != 0) {
|
|
return @ptrFromInt(result);
|
|
}
|
|
} else {
|
|
// Creature/pet GUID — resolve via object (safe during lazy resolution)
|
|
const obj = getObjectByGUID(guid);
|
|
if (obj != 0) {
|
|
const name = getNameByObject(obj);
|
|
if (@intFromPtr(name) != 0 and name[0] != 0) return name;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/// CGUnit_C::GetNameFromCacheOrUnknown — __thiscall(ECX=unitObj, stack=outPtr), returns char*.
|
|
const FN_UNIT_GET_NAME: usize = 0x609210;
|
|
|
|
/// Resolve a GUID to a unit name via the unit object. Only safe to call when the
|
|
/// object manager is in a stable state (NOT during SMSG_UPDATE_OBJECT processing).
|
|
pub fn getNameByObject(obj: u32) [*:0]const u8 {
|
|
if (obj == 0 or !isValidPtr(obj)) return "";
|
|
const desc = hook.readMem(u32, obj + 8);
|
|
if (desc == 0 or !isValidPtr(desc)) return "";
|
|
const name_ptr = hook.call(fn (u32, u32) callconv(hook.cc.thiscall) u32, FN_UNIT_GET_NAME, .{ obj, 0 });
|
|
if (name_ptr != 0 and isValidPtr(name_ptr)) {
|
|
return @ptrFromInt(name_ptr);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/// Get the current target's GUID.
|
|
pub fn getTargetGUID() u64 {
|
|
return unitGUID("target");
|
|
}
|
|
|
|
/// Check if a unit is friendly to the local player.
|
|
pub fn isUnitFriendly(unit: u32, local_player: u32) bool {
|
|
if (unit == 0 or local_player == 0) return false;
|
|
const reaction = hook.call(fn (u32, u32) callconv(hook.cc.thiscall) i32, o.FN_UNIT_REACTION, .{ local_player, unit });
|
|
return reaction >= 4;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Model owner resolution
|
|
// =============================================================================
|
|
|
|
const MODEL_OWNER_DIRECT: usize = 0x28;
|
|
const MODEL_OWNER_CALLBACK: usize = 0x3C0;
|
|
|
|
pub fn resolveModelOwner(model: u32) u32 {
|
|
if (!isReadablePtr(model, MODEL_OWNER_CALLBACK + 4)) return 0;
|
|
|
|
const candidate_cb = hook.readMem(u32, model + MODEL_OWNER_CALLBACK);
|
|
if (candidate_cb != 0 and isReadablePtr(candidate_cb, 0x40)) {
|
|
const guid_lo = hook.readMem(u32, candidate_cb + o.OBJECT_GUID_OFFSET);
|
|
if (guid_lo != 0 and guid_lo < 0x10000000) return candidate_cb;
|
|
}
|
|
|
|
const candidate_dir = hook.readMem(u32, model + MODEL_OWNER_DIRECT);
|
|
if (candidate_dir != 0 and isReadablePtr(candidate_dir, 0x40)) {
|
|
const guid_lo = hook.readMem(u32, candidate_dir + o.OBJECT_GUID_OFFSET);
|
|
if (guid_lo != 0 and guid_lo < 0x10000000) return candidate_dir;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// =============================================================================
|
|
// Raid target cache
|
|
// =============================================================================
|
|
|
|
var cached_raid_targets: [8]u64 = .{0} ** 8;
|
|
|
|
pub fn cacheRaidTargets() void {
|
|
for (0..8) |i| {
|
|
cached_raid_targets[i] = readGUID(@intCast(o.RAID_TARGET_ARRAY + i * 8));
|
|
}
|
|
}
|
|
|
|
pub fn getRaidMarkForGUID(guid: u64) u8 {
|
|
if (guid == 0) return 0;
|
|
for (cached_raid_targets, 0..) |rt, i| {
|
|
if (rt == guid) return @intCast(i + 1);
|
|
}
|
|
return 0;
|
|
}
|