Files
WeirdUtils/src/wow.zig
T
MarcelineVQ 96687bfaaa clickthrough: block unusable player-summoned portals and rituals
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
2026-03-28 11:42:38 -07:00

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;
}