From 3e37b96788164213e14552f55063cc9a4161a37b Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Tue, 10 Mar 2026 13:53:29 -0700 Subject: [PATCH] Consolidate shared game offsets and accessor functions Extract duplicated WoW 1.12.1 addresses and game accessor functions into shared modules (src/offsets.zig, src/wow.zig), replacing 5+ copies of getObjectByGUID, isInBattleground, isValidPtr, etc. - src/offsets.zig: shared address constants (object manager, descriptor fields, map/zone, core function addresses, D3D9/GX) - src/wow.zig: shared accessor functions (pointer validation, object manager traversal, field reads, unit helpers, battleground detection, game function wrappers, raid target cache) - Update 10 modules to import from shared instead of inline constants - Remove outline/wow.zig (promoted to src/wow.zig) - Trim outline/offsets.zig and markers/offsets.zig to module-specific only --- src/bigcursor/bigcursor.zig | 8 +- src/clickthrough/clickthrough.zig | 77 +++----------- src/interact/interact.zig | 27 ++--- src/markers/markers.zig | 42 ++------ src/markers/offsets.zig | 163 ++--------------------------- src/minimapicons/minimapicons.zig | 106 ++++++------------- src/offsets.zig | 167 ++++++++++++++++++++++++++++++ src/outline/d3d9_hook.zig | 7 +- src/outline/model_hook.zig | 2 +- src/outline/offsets.zig | 87 +--------------- src/outline/tracker.zig | 2 +- src/screenshot/screenshot.zig | 4 +- src/transmogfix/transmogfix.zig | 13 +-- src/{outline => }/wow.zig | 160 ++++++++++++++++++++-------- 14 files changed, 370 insertions(+), 495 deletions(-) create mode 100644 src/offsets.zig rename src/{outline => }/wow.zig (60%) diff --git a/src/bigcursor/bigcursor.zig b/src/bigcursor/bigcursor.zig index 721827e..b806a82 100644 --- a/src/bigcursor/bigcursor.zig +++ b/src/bigcursor/bigcursor.zig @@ -127,7 +127,8 @@ const IconInfo = extern struct { // WoW CVar API (1.12.1 build 5875) // ============================================================================= -const CVAR_LOOKUP: usize = 0x0063DEC0; +const offsets = @import("../offsets.zig"); +const CVAR_LOOKUP: usize = offsets.FN_CVAR_LOOKUP; // RegisterCVar: __fastcall(ECX=name, EDX=help, stack: unk1, default, callback, category, unk2, unk3) const RegisterCVarFn = *const fn ([*:0]const u8, u32, u32, [*:0]const u8, u32, u32, u32, u32) callconv(hook.cc.fastcall) u32; @@ -161,8 +162,8 @@ const SVT_UnlockRect: usize = 14; const D3DFMT_A8R8G8B8: u32 = 21; // Game's GxDevice → IDirect3DDevice9 pointer chain -const GX_DEVICE_PTR: usize = 0xC0ED38; -const GX_DEVICE_D3D_OFFSET: usize = 0x38A8; +const GX_DEVICE_PTR: usize = offsets.GX_DEVICE_PTR; +const GX_DEVICE_D3D_OFFSET: usize = offsets.GX_DEVICE_D3D_OFFSET; // ============================================================================= // COM helpers @@ -611,7 +612,6 @@ pub fn isActive() bool { } pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; diff --git a/src/clickthrough/clickthrough.zig b/src/clickthrough/clickthrough.zig index 6c7ca01..d87fffd 100644 --- a/src/clickthrough/clickthrough.zig +++ b/src/clickthrough/clickthrough.zig @@ -12,23 +12,16 @@ const std = @import("std"); const hook = @import("zhook"); const logging = @import("../logging.zig"); const mod_mutex = @import("../mutex.zig"); +const offsets = @import("../offsets.zig"); +const wow = @import("../wow.zig"); pub const module_name: [*:0]const u8 = "clickthrough"; // ============================================================================= -// WoW addresses +// WoW addresses (module-specific) // ============================================================================= const ADDR_WorldIntersectionTest: usize = 0x480DF0; -const ADDR_GetObjectByGUID: usize = 0x464870; - -// Map identification (Map.dbc lookup) -const OBJECT_MANAGER_PTR: usize = 0x00B41414; -const OBJMGR_MAP_ID_OFFSET: usize = 0xCC; -const MAP_DBC_DATA: usize = 0x00C0DAA8; -const MAP_DBC_MAX: usize = 0x00C0DAAC; -const MAP_DBC_MAP_TYPE_OFFSET: usize = 0x08; -const MAP_TYPE_BATTLEGROUND: u32 = 3; // ============================================================================= // HitTestResult layout @@ -39,11 +32,8 @@ const HIT_GUID_HI: usize = 0x04; const HIT_RESULT_SIZE: usize = 0x34; // Object struct offsets -const OBJ_DESCRIPTOR: usize = 0x08; const OBJ_TYPE_MASK_OFFSET: usize = 0x08; // at *(*(obj+8)+8) -// Object type + descriptor field offsets - // Type masks const TYPE_UNIT: u32 = 0x09; const TYPE_PLAYER: u32 = 0x19; @@ -51,60 +41,20 @@ const TYPE_PLAYER: u32 = 0x19; // Raycast flags const FLAG_GO: u32 = 0x04; -// ============================================================================= -// Object filtering -// ============================================================================= - -const OBJ_TYPE: usize = 0x14; -const OBJ_TYPE_UNIT: u32 = 3; -const OBJ_TYPE_GO: u32 = 5; -const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END(0x06) + 0x8D = 0x93 -const DESC_ENTRY: usize = 0x03 * 4; // OBJECT_FIELD_ENTRY -const ADDR_CallSpellCastHandler: usize = 0x5F8800; - -// ============================================================================= -// Battleground detection -// ============================================================================= - -/// Check if the current map is a battleground by reading Map.dbc mapType. -fn isInBattleground() bool { - const obj_mgr = hook.readMem(u32, OBJECT_MANAGER_PTR); - if (obj_mgr == 0) return false; - const map_id = hook.readMem(u32, obj_mgr + OBJMGR_MAP_ID_OFFSET); - const dbc_max = hook.readMem(u32, MAP_DBC_MAX); - if (map_id > dbc_max) return false; - const table_base = hook.readMem(u32, 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 + MAP_DBC_MAP_TYPE_OFFSET); - return map_type == MAP_TYPE_BATTLEGROUND; -} - -fn getObjectByGUID(guid_lo: u32, guid_hi: u32) u32 { - if (guid_lo == 0 and guid_hi == 0) return 0; - return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, ADDR_GetObjectByGUID, .{ guid_lo, guid_hi }); -} - /// Check if the GUID refers to an interactable GO. fn isInteractableGO(guid_lo: u32, guid_hi: u32) bool { - const obj = getObjectByGUID(guid_lo, guid_hi); + const obj = wow.getObjectByGUIDSplit(guid_lo, guid_hi); if (obj == 0) return false; - const obj_type = hook.readMem(u32, obj + OBJ_TYPE); - if (obj_type != OBJ_TYPE_GO) return false; - // CallSpellCastHandler: __fastcall(obj_ECX) -> bool - return hook.call(fn (u32) callconv(hook.cc.fastcall) u8, ADDR_CallSpellCastHandler, .{obj}) != 0; + if (wow.getObjectTypeRaw(obj) != @intFromEnum(wow.ObjectType.game_object)) return false; + return hook.call(fn (u32) callconv(hook.cc.fastcall) u8, offsets.FN_CALL_SPELL_CAST_HANDLER, .{obj}) != 0; } /// Check if the GUID refers to an NPC with interaction flags (vendor, quest giver, etc.) fn isInteractableNPC(guid_lo: u32, guid_hi: u32) bool { - const obj = getObjectByGUID(guid_lo, guid_hi); + const obj = wow.getObjectByGUIDSplit(guid_lo, guid_hi); if (obj == 0) return false; - const obj_type = hook.readMem(u32, obj + OBJ_TYPE); - if (obj_type != OBJ_TYPE_UNIT) return false; - const desc = hook.readMem(u32, obj + OBJ_DESCRIPTOR); - if (desc < 0x10000 or desc >= 0x7F000000) return false; - return hook.readMem(u32, desc + DESC_NPC_FLAGS) != 0; + if (wow.getObjectTypeRaw(obj) != @intFromEnum(wow.ObjectType.unit)) return false; + return wow.getNpcFlags(obj) != 0; } /// Check if the second raycast result is something we should click through to. @@ -140,7 +90,7 @@ fn worldIntersectDetour(world_frame: u32, ray_start: u32, ray_end: u32, flags: u const hit_type = wit_hook.callOriginal(.{ world_frame, ray_start, ray_end, flags, hit_result }); // No click-through in battlegrounds - if (!g_is_hook_owner or hit_result == 0 or hit_type != 2 or isInBattleground()) return hit_type; + if (!g_is_hook_owner or hit_result == 0 or hit_type != 2 or wow.isInBattleground()) return hit_type; // hitType 2 = object hit. Check if it's a unit/player. const buf_lo = hook.readMem(u32, hit_result + HIT_GUID_LO); @@ -148,11 +98,11 @@ fn worldIntersectDetour(world_frame: u32, ray_start: u32, ray_end: u32, flags: u if (buf_lo == 0 and buf_hi == 0) return hit_type; - const obj = getObjectByGUID(buf_lo, buf_hi); + const obj = wow.getObjectByGUIDSplit(buf_lo, buf_hi); if (obj == 0) return hit_type; - const desc_ptr = hook.readMem(u32, obj + OBJ_DESCRIPTOR); - if (desc_ptr < 0x10000 or desc_ptr >= 0x7F000000) return hit_type; + const desc_ptr = wow.getDescriptor(obj); + if (!wow.isValidPtr(desc_ptr)) return hit_type; const type_mask = hook.readMem(u32, desc_ptr + OBJ_TYPE_MASK_OFFSET); @@ -192,7 +142,6 @@ pub fn isActive() bool { } pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; diff --git a/src/interact/interact.zig b/src/interact/interact.zig index b57edc1..d738565 100644 --- a/src/interact/interact.zig +++ b/src/interact/interact.zig @@ -1,24 +1,17 @@ const std = @import("std"); const hook = @import("zhook"); const logging = @import("../logging.zig"); +const offsets = @import("../offsets.zig"); +const wow = @import("../wow.zig"); const WINAPI = std.builtin.CallingConvention.winapi; extern "kernel32" fn GetTickCount() callconv(WINAPI) u32; -extern "kernel32" fn CreateMutexA(lpMutexAttributes: ?*anyopaque, bInitialOwner: i32, lpName: [*:0]const u8) callconv(WINAPI) ?*anyopaque; -extern "kernel32" fn ReleaseMutex(hMutex: *anyopaque) callconv(WINAPI) i32; -extern "kernel32" fn CloseHandle(hObject: *anyopaque) callconv(WINAPI) i32; -extern "kernel32" fn GetLastError() callconv(WINAPI) u32; -extern "kernel32" fn GetCurrentProcessId() callconv(WINAPI) u32; -const ERROR_ALREADY_EXISTS: u32 = 183; // ============================================================================= -// Offsets +// Module-specific addresses // ============================================================================= const Offsets = struct { - const ADDR_SceneEnd: usize = 0x5A17A0; - const FUN_GET_OBJECT_POINTER: usize = 0x464870; - const FUN_IS_IN_WORLD: usize = 0xB4B424; const FUN_RIGHT_CLICK_UNIT: usize = 0x60BEA0; const FUN_RIGHT_CLICK_OBJECT: usize = 0x5F8660; const FUN_SET_TARGET: usize = 0x493540; @@ -27,7 +20,6 @@ const Offsets = struct { const LUA_ERROR: usize = 0x6F4940; const LUA_ISNUMBER: usize = 0x6F34D0; const LUA_TONUMBER: usize = 0x6F3620; - const VISIBLE_OBJECTS: usize = 0xB41414; }; // ============================================================================= @@ -60,13 +52,11 @@ const C3Vector = struct { // ============================================================================= fn getObjectPointer(guid: u64) u32 { - const lo: u32 = @truncate(guid); - const hi: u32 = @truncate(guid >> 32); - return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, Offsets.FUN_GET_OBJECT_POINTER, .{ lo, hi }); + return wow.getObjectByGUID(guid); } fn isInWorld() bool { - return hook.readMem(u8, Offsets.FUN_IS_IN_WORLD) != 0; + return wow.isInGame(); } fn getUnitPosition(unit: u32) C3Vector { @@ -158,7 +148,7 @@ pub fn interactNearest(L: *anyopaque) callconv(.c) u32 { return 0; } - const objects = hook.readMem(u32, Offsets.VISIBLE_OBJECTS); + const objects = hook.readMem(u32, offsets.OBJECT_MANAGER_PTR); var current_object = hook.readMem(u32, objects + 0xAC); const player_guid = hook.readMem(u64, objects + 0xC0); @@ -318,7 +308,7 @@ pub fn lootAllCorpses(_: *anyopaque) callconv(.c) u32 { loot_queue_index = 0; loot_active = false; - const objects = hook.readMem(u32, Offsets.VISIBLE_OBJECTS); + const objects = hook.readMem(u32, offsets.OBJECT_MANAGER_PTR); var current_object = hook.readMem(u32, objects + 0xAC); const player_guid = hook.readMem(u64, objects + 0xC0); @@ -378,14 +368,13 @@ fn hookSceneEnd(device: u32) callconv(hook.cc.thiscall) void { // ============================================================================= pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; if (!g_is_hook_owner) return; log = logging.Logger.open(module_name, .console); - _ = scene_end_hook.attach(Offsets.ADDR_SceneEnd, &hookSceneEnd); + _ = scene_end_hook.attach(offsets.FN_SCENE_END, &hookSceneEnd); } pub fn removeHooks() void { diff --git a/src/markers/markers.zig b/src/markers/markers.zig index 2135d0b..cc4c991 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -22,7 +22,8 @@ const std = @import("std"); const hook = @import("zhook"); const lua = @import("../lua.zig"); const o = @import("offsets.zig"); -const wow = @import("../outline/wow.zig"); +const offsets = @import("../offsets.zig"); +const wow = @import("../wow.zig"); const logging = @import("../logging.zig"); const WINAPI = std.builtin.CallingConvention.winapi; @@ -71,11 +72,7 @@ const MODEL_PATHS = [NUM_MARKERS][*:0]const u8{ // Types // ============================================================================= -pub const Vec3 = struct { - x: f32, - y: f32, - z: f32, -}; +pub const Vec3 = wow.Vec3; // ============================================================================= // State @@ -126,28 +123,15 @@ var despawning: [MAX_DESPAWNING]?DespawningEntity = .{null} ** MAX_DESPAWNING; /// Check if the current map is a battleground by reading Map.dbc mapType. /// Uses: ObjMgr+0xCC → mapId, then Map.dbc[mapId] → row, row+0x04 → mapType. 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); - // Map.dbc indexed pointer table: dereference base ptr, then index by mapId. - 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; + return wow.isInBattleground(); } // ============================================================================= // Permission check - leader or raid officer required // ============================================================================= -/// Get local player GUID via GetPlayerGUID (0x468550). -/// __fastcall(), no params, returns u64 via EDX:EAX. fn getPlayerGUID() u64 { - return hook.call(fn () callconv(hook.cc.fastcall) u64, o.FN_GET_PLAYER_GUID, .{}); + return wow.getPlayerGUID(); } /// Look up a player name from the name cache by GUID. @@ -232,16 +216,7 @@ fn senderHasPermission(sender: [*:0]const u8) bool { // ============================================================================= 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), - }; + return wow.getUnitPosition(unit); } /// Resolve a unit ID string ("player", "target", etc.) to a world position. @@ -392,7 +367,7 @@ fn placeMarker(index: usize, pos: Vec3) bool { // Store persistent definition marker_defs[index] = .{ .pos = pos, - .area_id = hook.readMem(u32, o.ZONE_AREA_ID), + .area_id = hook.readMem(u32, offsets.ZONE_AREA_ID), .active = true, }; @@ -619,7 +594,7 @@ fn tickAnimations() void { if (player != 0) { const player_pos = getUnitPosition(player); if (player_pos.x != 0 or player_pos.y != 0 or player_pos.z != 0) { - const current_area = hook.readMem(u32, o.ZONE_AREA_ID); + const current_area = hook.readMem(u32, offsets.ZONE_AREA_ID); for (0..NUM_MARKERS) |i| { if (!marker_defs[i].active) continue; if (marker_entities[i] != null) continue; // entity alive, skip @@ -815,7 +790,6 @@ fn destroyAllEntities() void { // ============================================================================= pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; diff --git a/src/markers/offsets.zig b/src/markers/offsets.zig index 21aba59..358de56 100644 --- a/src/markers/offsets.zig +++ b/src/markers/offsets.zig @@ -1,20 +1,6 @@ -//! Offsets for marker/game object system - -// ============================================================================= -// Unit position -// ============================================================================= - -/// Unit + this → movement struct pointer -pub const UNIT_MOVEMENT_OFFSET: usize = 0x118; - -/// Movement struct + this → X coordinate (float) -pub const MOVEMENT_POS_X: usize = 0x10; - -/// Movement struct + this → Y coordinate (float) -pub const MOVEMENT_POS_Y: usize = 0x14; - -/// Movement struct + this → Z coordinate (float) -pub const MOVEMENT_POS_Z: usize = 0x18; +//! Markers-specific WoW 1.12.1 memory addresses and struct offsets. +//! +//! Shared addresses (object manager, game functions, map/zone, etc.) live in src/offsets.zig. // ============================================================================= // Entity creation (high-level API) @@ -24,13 +10,6 @@ pub const MOVEMENT_POS_Z: usize = 0x18; /// ECX = modelPath (char*), EDX = position (float[3]*) /// Stack: facing (float), flags (int), updateNow (int), param6 (int), param7 (int) /// Returns: entity pointer (int*). -/// -/// Routes M2 models (no ".wmo" in path) through CreateWorldUnit. -/// Routes WMO models (".wmo" in path) through CreateGameObject + ModelAttachment_CreateNode -/// + global list insertion + SetObjectTransformation. -/// -/// Both paths call UpdateWorldPosition when updateNow != 0. -/// Increments refcount at entity+0x0E. pub const FN_CREATE_ENTITY_INSTANCE: usize = 0x006707c0; // ============================================================================= @@ -38,106 +17,43 @@ pub const FN_CREATE_ENTITY_INSTANCE: usize = 0x006707c0; // ============================================================================= /// CleanupWorldAndEntities - void(), no params, __stdcall. -/// Top-level world teardown: calls CleanupEntityList_ProcessAll, then -/// CleanupWorldAndReleaseResources (which iterates heaps and force-frees). -/// Called from InitializeWorldScene (map change) and ShutdownClientSystems (exit). -/// Hook this to destroy custom entities BEFORE the game's teardown begins. pub const FN_CLEANUP_WORLD_AND_ENTITIES: usize = 0x0066fc40; // ============================================================================= // World object lifecycle // ============================================================================= -/// AllocateAndInitializeWorldObject(initializeFlag) -/// __fastcall returns void** pub const FN_ALLOCATE_WORLD_OBJECT: usize = 0x006a0930; - -/// DestroyWorldObjectAndRelease(object) - __fastcall, ECX=obj, no stack params. -/// Unlinks from world object list (+0x10/+0x14), calls virtual destructor, frees heap. -/// ONLY for objects on WENTITY heap (from AllocateAndInitializeWorldObject). pub const FN_DESTROY_WORLD_OBJECT: usize = 0x006a0a70; -/// CleanupEntity_ProcessAttachments(entity) - __fastcall, ECX=entity, no stack params. -/// High-level destructor counterpart to CreateEntityInstance_WithAttachment. -/// Walks and frees attachment children, decrements refcount at +0x0E, then -/// dispatches to type-specific destructor based on flags at +0x8: -/// flag 0x8 (M2): destroyWorldEnvironment (0x6a6870) - scene graph removal + free -/// flag 0x40 (WMO): cleanupGameObject (0x6a67a0) - render detach + spatial unlink + free -/// Only actually frees when refcount reaches 0. +/// CleanupEntity_ProcessAttachments(entity) - __fastcall, ECX=entity. pub const FN_CLEANUP_ENTITY: usize = 0x00670d50; -/// DecrementReferenceCount(obj) - __fastcall, ECX=obj, no stack params. -/// Decrements ref count; when it reaches 0, calls virtual destructor to free. pub const FN_DECREMENT_REFCOUNT: usize = 0x007103a0; // ============================================================================= // Cursor terrain position // ============================================================================= -/// WorldFrame global pointer - *(u32*)PTR = worldFrame object. pub const PTR_WORLD_FRAME: usize = 0x00B4B2BC; - -/// UpdateHitTest - __fastcall(ECX=worldFrame), no stack params. -/// Raycasts from camera through mouse cursor, stores result at worldFrame+0x350. pub const FN_UPDATE_HIT_TEST: usize = 0x00481F00; -/// Offsets from worldFrame base to HitTestResult fields. -pub const WF_HIT_TYPE: usize = 0x350; // 0=terrain, 1=ground-target, 2=object -pub const WF_HIT_GUID: usize = 0x358; // u64 object GUID (0 for terrain) +pub const WF_HIT_TYPE: usize = 0x350; +pub const WF_HIT_GUID: usize = 0x358; pub const WF_HIT_TERRAIN_X: usize = 0x360; pub const WF_HIT_TERRAIN_Y: usize = 0x364; pub const WF_HIT_TERRAIN_Z: usize = 0x368; -// ============================================================================= -// Zone / area identification -// ============================================================================= - -/// Current zone area ID - numeric, locale-safe zone identifier. -/// Updated by the game as the player moves between areas. -pub const ZONE_AREA_ID: usize = 0x00B4E314; - -// ============================================================================= -// Map identification -// ============================================================================= - -/// Object Manager pointer - dereference once to get ObjMgr struct. -pub const OBJECT_MANAGER_PTR: usize = 0x00B41414; - -/// ObjMgr + this → current map ID (u32). Same as ClntObjMgrGetMapId (0x468580). -pub const OBJMGR_MAP_ID_OFFSET: usize = 0xCC; - -/// Pointer to Map.dbc indexed lookup table. Dereference to get table base, -/// then *(base + mapId * 4) → Map.dbc row pointer. -pub const MAP_DBC_DATA: usize = 0x00C0DAA8; - -/// Pointer to Map.dbc max valid index. Dereference to get the max value. -pub const MAP_DBC_MAX: usize = 0x00C0DAAC; - -/// Map.dbc row + this → mapType (u32). 0=world, 1=instance, 2=raid, 3=battleground. -/// Row layout: +0x00=mapId, +0x04=internalName(string), +0x08=mapType. -pub const MAP_DBC_MAP_TYPE_OFFSET: usize = 0x08; - -/// MapType value for battleground instances. -pub const MAP_TYPE_BATTLEGROUND: u32 = 3; - // ============================================================================= // Per-frame world update // ============================================================================= -/// OnWorldUpdate - __fastcall(ECX=worldFrame), no stack params, void return. -/// Called every frame while the world is active (in-game, not login screen). -/// Part of CGWorldFrame update pipeline. pub const FN_ON_WORLD_UPDATE: usize = 0x00482EA0; // ============================================================================= // Model creation // ============================================================================= -/// CM2Model_CreateForModelObject(modelPath_ECX, worldObj_EDX, forceInit) -/// __fastcall, RET 0x04. ECX=modelPath(char*), EDX=worldObject, 1 stack param. -/// Complete model creation pipeline: createModelAttachment, SetModelScale, -/// SetCallbackFunctions, SetRenderCallbacks, PlayBoneAnimation, CM2Model_Initialize. -/// Returns 1 on success, 0 on failure. Stores render context at worldObj+0x88. pub const FN_CM2_CREATE_FOR_MODEL_OBJECT: usize = 0x00695100; // ============================================================================= @@ -145,111 +61,44 @@ pub const FN_CM2_CREATE_FOR_MODEL_OBJECT: usize = 0x00695100; // ============================================================================= /// CM2Model__PlayBoneAnimation - __thiscall(ECX=modelRenderCtx), RET 0x1c (7 stack params). -/// (boneIndex, animId, sequenceIndex, animData*, speed, blendMode, queueAnimation) -/// boneIndex: 0xFFFFFFFF = all bones -/// animId: M2 animation ID (0=Stand, 158=Hold, 159=Decay for Raid_UI_FX) -/// sequenceIndex: -1 = random sub-sequence -/// animData: NULL for default timing -/// speed: 1.0 = normal -/// blendMode: 0 = hard cut, 1 = smooth blend -/// queueAnimation: 0 = set immediately, 1 = queue after current pub const FN_PLAY_BONE_ANIMATION: usize = 0x007121a0; // ============================================================================= // Transform and position // ============================================================================= -/// UpdateObjectTransform_CalculateBounds - __fastcall, RET 0x0C -/// ECX = world object, EDX = 4x4 transform matrix (float[16]) -/// Stack: bounds (float[6] min/max), halfExtents (float[3]), forceUpdate (int) pub const FN_UPDATE_OBJECT_TRANSFORM: usize = 0x006717d0; // ============================================================================= // File I/O (Storm) - for in-memory file serving // ============================================================================= -/// openFileWithOptions - __stdcall(4), RET 0x10, prologue=9 -/// (archive_ptr, path, flags, handle_out) → type_code (0=fail, 1-4=success) pub const FN_OPEN_FILE_WITH_OPTIONS: usize = 0x006477c0; - -/// GetFileSizeFromHandle - __stdcall(2), RET 0x08, prologue=6 -/// (file_context, high_size_out) → size pub const FN_GET_FILE_SIZE: usize = 0x006487f0; - -/// ReadFileFromMultipleSources - __stdcall(6), RET 0x18, prologue=6 -/// (context, buffer, size, bytes_read_out, async_ptr, param6) → bool -/// async_ptr==NULL: synchronous read. Non-NULL: queues async operation. pub const FN_READ_FILE: usize = 0x00648460; - -/// CleanupFileHandleResources - __stdcall(1), RET 0x04, prologue=7 -/// (file_context) → 1 pub const FN_CLEANUP_FILE_HANDLE: usize = 0x00648730; - -/// processAsyncFileOperation - __fastcall(ECX=request), plain RET, prologue=7 -/// Request structure: +0x08=file_ctx, +0x0C=dest_buf, +0x10=read_size, -/// +0x14=seek/event_struct (*(+0x14)+4 = event handle) pub const FN_PROCESS_ASYNC_FILE_OP: usize = 0x00647350; - -/// initializeFileContext - __thiscall(ECX=ctx, type) -/// Sets context type, initializes critical section at +0x24, zeroes fields. pub const FN_INIT_FILE_CONTEXT: usize = 0x00647290; - -/// cleanupFileContext - __thiscall(ECX=ctx) -/// Destroys critical section, cleanup companion to initializeFileContext. pub const FN_CLEANUP_FILE_CONTEXT: usize = 0x006472d0; - -/// FreeMemory (SMemFree) - __stdcall(3): (ptr, src_str, flags) pub const FN_FREE_MEMORY: usize = 0x00646430; // ============================================================================= // M2 model loading (async pipeline) // ============================================================================= -/// loadModelFromFileAsync - __thiscall(ECX=model_obj), 2 stack params, RET 0x08 -/// (fileHandle: **ctx, shouldUseCallback: int) → 1 -/// Prologue: 55 8b ec 8b 55 0c 56 8b f1 - safe sizes: [6, 7, 9] -/// Allocates async task to read file and call onModelLoadComplete when done. -/// The async executor at 0x71d610 calls fileReadWithLock directly, bypassing -/// our ReadFileFromMultipleSources hook - hence this hook fills the buffer -/// synchronously for fake file contexts. pub const FN_LOAD_MODEL_ASYNC: usize = 0x0071d4e0; - -/// processLoadedModelData - __fastcall(ECX=model), no stack params, plain RET -/// 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; diff --git a/src/minimapicons/minimapicons.zig b/src/minimapicons/minimapicons.zig index 3eaf106..38faa06 100644 --- a/src/minimapicons/minimapicons.zig +++ b/src/minimapicons/minimapicons.zig @@ -23,11 +23,13 @@ const hook = @import("zhook"); const lua = @import("../lua.zig"); const logging = @import("../logging.zig"); const mod_mutex = @import("../mutex.zig"); +const offsets = @import("../offsets.zig"); +const wow = @import("../wow.zig"); pub const module_name: [*:0]const u8 = "minimapicons"; // ============================================================================= -// WoW 1.12.1 addresses +// WoW 1.12.1 addresses (module-specific) // ============================================================================= const ADDR = struct { @@ -44,47 +46,30 @@ const ADDR = struct { const GxPrimLockVertexPtrs: usize = 0x58A2A0; const GxPrimDrawElements: usize = 0x58A2E0; const GxPrimUnlockVertexPtrs: usize = 0x58A340; - const GetObjectByGUID: usize = 0x464870; const CGxTexFlagsInit: usize = 0x58A980; const CStatusDestructor: usize = 0x419E30; // Static data - const BlipVertices: usize = 0xBC8230; // 4x C3Vector - const BlipNormal: usize = 0xBC829C; // C3Vector - const BlipTexCoords: usize = 0xBC77F0; // TexCoord (4x C2Vector) - const BlipVertIndices: usize = 0x807A2C; // 4x u16 + const BlipVertices: usize = 0xBC8230; + const BlipNormal: usize = 0xBC829C; + const BlipTexCoords: usize = 0xBC77F0; + const BlipVertIndices: usize = 0x807A2C; const CStatusVftable: usize = 0x7FFA10; - // Object struct offsets + // Object struct offsets (module-specific) const OBJ_VTABLE: usize = 0x00; - const OBJ_DATA: usize = 0x08; // m_data — update fields descriptor (starts at field 0) - const OBJ_TYPE: usize = 0x14; // m_objectType - const OBJ_CREATURE_CACHE: usize = 0xB30; // ptr to creature cache entry + const OBJ_CREATURE_CACHE: usize = 0xB30; - // Creature cache entry offsets (name[0..3] at +0x00..+0x0C, subname at +0x10) - const CACHE_SUBNAME: usize = 0x10; // char* subname/title (e.g. "Druid Trainer") - - // Descriptor field byte offsets (absolute_field_index * 4 from m_data) - const DESC_ENTRY: usize = 0x03 * 4; // OBJECT_FIELD_ENTRY - const DESC_NPC_FLAGS: usize = 0x93 * 4; // UNIT_NPC_FLAGS = OBJECT_END + 0x8D - const DESC_GO_TYPE: usize = 0x15 * 4; // GAMEOBJECT_TYPE_ID - const DESC_SUMMONEDBY: usize = 0x0C * 4; // UNIT_FIELD_SUMMONEDBY (GUID, 8 bytes) - const DESC_BYTES_0: usize = 0x24 * 4; // UNIT_FIELD_BYTES_0: race|class|gender|power - const DESC_GO_FLAGS: usize = 0x09 * 4; // GAMEOBJECT_FLAGS - const DESC_GO_DYN_FLAGS: usize = 0x13 * 4; // GAMEOBJECT_DYN_FLAGS - - // Function addresses - const ClntObjMgrGetActivePlayer: usize = 0x468550; - const UnitReaction: usize = 0x6061E0; - const CallSpellCastHandler: usize = 0x5f8800; + // Creature cache entry offsets + const CACHE_SUBNAME: usize = 0x10; // MINIMAPINFO struct offsets - const MI_POS: usize = 0x0C; // C3Vector + const MI_POS: usize = 0x0C; const MI_RADIUS: usize = 0x18; const MI_LAYOUT_SCALE: usize = 0x1C; const MI_FRAME: usize = 0x20; - // CGMinimapFrame: FrameScriptPart at +0x24, its vtable[7] = GetUnkScale + // CGMinimapFrame const FRAME_SCRIPT_PART: usize = 0x24; // Vtable indices @@ -282,7 +267,7 @@ const CITY_ZONES = [_]u32{ 1497, // Undercity 2040, // Alah'Thalas }; -const ZONE_AREA_ID: usize = 0x00B4E314; +const ZONE_AREA_ID: usize = offsets.ZONE_AREA_ID; var g_has_active_unit_tracking: bool = false; var g_has_active_go_tracking: bool = false; var g_has_any_filters: bool = false; @@ -354,42 +339,19 @@ pub fn isActive() bool { } // ============================================================================= -// Pointer validation +// Object helpers (delegates to shared wow module) // ============================================================================= -fn isValidPtr(addr: u32) bool { - return addr >= 0x10000 and addr < 0x7F000000; -} - -// ============================================================================= -// Object helpers -// ============================================================================= - -fn getObjectByGUID(guid_lo: u32, guid_hi: u32) u32 { - if (guid_lo == 0 and guid_hi == 0) return 0; - return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, ADDR.GetObjectByGUID, .{ guid_lo, guid_hi }); -} - -fn getObjectType(obj: u32) u32 { - if (!isValidPtr(obj)) return 0; - return hook.readMem(u32, obj + ADDR.OBJ_TYPE); -} - -fn getDescriptor(obj: u32) u32 { - if (!isValidPtr(obj)) return 0; - return hook.readMem(u32, obj + ADDR.OBJ_DATA); -} - -fn getNpcFlags(obj: u32) u32 { - const desc = getDescriptor(obj); - if (!isValidPtr(desc)) return 0; - return hook.readMem(u32, desc + ADDR.DESC_NPC_FLAGS); -} +const isValidPtr = wow.isValidPtr; +const getObjectByGUID = wow.getObjectByGUIDSplit; +const getObjectType = wow.getObjectTypeRaw; +const getDescriptor = wow.getDescriptor; +const getNpcFlags = wow.getNpcFlags; fn getGoType(obj: u32) u32 { const desc = getDescriptor(obj); if (!isValidPtr(desc)) return 0; - return hook.readMem(u32, desc + ADDR.DESC_GO_TYPE); + return hook.readMem(u32, desc + offsets.DESC_GO_TYPE); } fn getCreatureSubName(obj: u32) ?[*:0]const u8 { @@ -402,11 +364,7 @@ fn getCreatureSubName(obj: u32) ?[*:0]const u8 { return subname; } -fn getObjectEntry(obj: u32) u32 { - const desc = getDescriptor(obj); - if (!isValidPtr(desc)) return 0; - return hook.readMem(u32, desc + ADDR.DESC_ENTRY); -} +const getObjectEntry = wow.getObjectEntry; // Creature entry IDs that should be treated as reagent vendors despite having no subname. const REAGENT_VENDOR_ENTRIES = [_]u32{ @@ -423,19 +381,19 @@ fn isReagentVendorEntry(entry_id: u32) bool { fn getGoFlags(obj: u32) u32 { const desc = getDescriptor(obj); if (!isValidPtr(desc)) return 0; - return hook.readMem(u32, desc + ADDR.DESC_GO_FLAGS); + return hook.readMem(u32, desc + offsets.DESC_GO_FLAGS); } fn getGoDynFlags(obj: u32) u32 { const desc = getDescriptor(obj); if (!isValidPtr(desc)) return 0; - return hook.readMem(u32, desc + ADDR.DESC_GO_DYN_FLAGS); + return hook.readMem(u32, desc + offsets.DESC_GO_DYN_FLAGS); } fn getRace(obj: u32) u8 { const desc = getDescriptor(obj); if (!isValidPtr(desc)) return 0; - return @truncate(hook.readMem(u32, desc + ADDR.DESC_BYTES_0)); + return @truncate(hook.readMem(u32, desc + offsets.DESC_BYTES_0)); } // Alliance: Human(1), Dwarf(3), Night Elf(4), Gnome(7), High Elf(10) @@ -457,25 +415,24 @@ fn isSameFaction(race_a: u8, race_b: u8) bool { fn getSummonedByGUID(obj: u32) u64 { const desc = getDescriptor(obj); if (!isValidPtr(desc)) return 0; - const lo = hook.readMem(u32, desc + ADDR.DESC_SUMMONEDBY); - const hi = hook.readMem(u32, desc + ADDR.DESC_SUMMONEDBY + 4); + const lo = hook.readMem(u32, desc + offsets.DESC_SUMMONEDBY); + const hi = hook.readMem(u32, desc + offsets.DESC_SUMMONEDBY + 4); return (@as(u64, hi) << 32) | lo; } fn getActivePlayerGUID() u64 { - return hook.call(fn () callconv(hook.cc.fastcall) u64, ADDR.ClntObjMgrGetActivePlayer, .{}); + return wow.getPlayerGUID(); } fn getActivePlayerObject() u32 { const guid = getActivePlayerGUID(); if (guid == 0) return 0; - return getObjectByGUID(@truncate(guid), @truncate(guid >> 32)); + return wow.getObjectByGUID(guid); } -/// UnitReaction: __thiscall(localPlayer_ECX, unit_stack) -> int (>=4 = friendly). fn unitReaction(local_player: u32, unit: u32) i32 { if (local_player == 0 or unit == 0) return 0; - return hook.call(fn (u32, u32) callconv(hook.cc.thiscall) i32, ADDR.UnitReaction, .{ local_player, unit }); + return hook.call(fn (u32, u32) callconv(hook.cc.thiscall) i32, offsets.FN_UNIT_REACTION, .{ local_player, unit }); } /// Check if a unit passes faction/friendliness filters. @@ -516,7 +473,7 @@ fn isUnitAllowed(obj: u32, local_player: u32) bool { /// CallSpellCastHandler: __fastcall(obj_ECX) -> char (bool via virtual dispatch). /// This is what IsValidInteractionTarget uses for type 0x21 (GO). fn isGoInteractable(obj: u32) bool { - const result: u8 = @truncate(hook.call(fn (u32) callconv(hook.cc.fastcall) u32, ADDR.CallSpellCastHandler, .{obj})); + const result: u8 = @truncate(hook.call(fn (u32) callconv(hook.cc.fastcall) u32, offsets.FN_CALL_SPELL_CAST_HANDLER, .{obj})); if (result == 0) { log.fmt("GO 0x{x} rejected: not interactable (entry={d})\n", .{ obj, getObjectEntry(obj) }); } @@ -1189,7 +1146,6 @@ fn strEqlInsensitive(a: [*:0]const u8, b: []const u8) bool { // ============================================================================= pub fn installHooks() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; diff --git a/src/offsets.zig b/src/offsets.zig new file mode 100644 index 0000000..359a7c8 --- /dev/null +++ b/src/offsets.zig @@ -0,0 +1,167 @@ +//! Shared WoW 1.12.1 (build 5875) memory addresses and struct offsets. +//! +//! Contains only addresses/offsets duplicated across 2+ modules. +//! Module-specific offsets remain in their own files. + +// ============================================================================= +// Object Manager +// ============================================================================= + +/// Pointer to the Object Manager base. Dereference once to get the ObjMgr struct. +pub const OBJECT_MANAGER_PTR: usize = 0x00B41414; + +/// ObjMgr + this → first object in the linked list. +pub const OBJECT_LIST_OFFSET: usize = 0xAC; + +/// ObjMgr + this → base pointer for the next-object traversal table. +pub const OBJECT_NEXT_OFFSET: usize = 0xA4; + +/// ObjMgr + this → local player GUID (8 bytes). +pub const LOCAL_PLAYER_GUID_OFFSET: usize = 0xC0; + +// ============================================================================= +// Object fields (from object base pointer) +// ============================================================================= + +pub const OBJECT_TYPE_OFFSET: usize = 0x14; +pub const OBJECT_GUID_OFFSET: usize = 0x30; + +/// object + this → pointer to descriptor block (m_data). +pub const UNIT_DESCRIPTOR_OFFSET: usize = 0x110; + +/// Alternate descriptor offset used by items/corpses (at +0x08). +pub const OBJECT_DATA_OFFSET: usize = 0x08; + +// ============================================================================= +// Descriptor field indices (byte offset = index * 4 from descriptor base) +// ============================================================================= + +/// OBJECT_FIELD_ENTRY (index 0x03) +pub const DESC_ENTRY: usize = 0x03 * 4; + +/// UNIT_NPC_FLAGS = OBJECT_END(0x06) + 0x8D = 0x93 +pub const DESC_NPC_FLAGS: usize = 0x93 * 4; + +/// GAMEOBJECT_TYPE_ID (index 0x15) +pub const DESC_GO_TYPE: usize = 0x15 * 4; + +/// UNIT_FIELD_SUMMONEDBY (index 0x0C, GUID = 8 bytes) +pub const DESC_SUMMONEDBY: usize = 0x0C * 4; + +/// UNIT_FIELD_BYTES_0: race|class|gender|power (index 0x24) +pub const DESC_BYTES_0: usize = 0x24 * 4; + +/// GAMEOBJECT_FLAGS (index 0x09) +pub const DESC_GO_FLAGS: usize = 0x09 * 4; + +/// GAMEOBJECT_DYN_FLAGS (index 0x13) +pub const DESC_GO_DYN_FLAGS: usize = 0x13 * 4; + +// ============================================================================= +// Unit descriptor fields (byte offsets from descriptor base) +// ============================================================================= + +/// Descriptor + this → unit flags (uint32). Test with UNIT_FLAG_DEAD. +pub const UNIT_FLAGS_OFFSET: usize = 0x224; + +/// Bit mask: unit is dead. +pub const UNIT_FLAG_DEAD: u32 = 0x20; + +/// Descriptor + this → current HP (int32). +pub const UNIT_HP_OFFSET: usize = 0x40; + +// ============================================================================= +// Corpse descriptor fields +// ============================================================================= + +/// Corpse descriptor + this → owner GUID (8 bytes). +pub const CORPSE_FIELD_OWNER: usize = 0x18; + +/// Corpse descriptor + this → corpse flags (uint32). +pub const CORPSE_FIELD_FLAGS: usize = 0x8C; + +/// Bit mask: corpse is a skeleton (not resurrectable). +pub const CORPSE_FLAG_BONE: u32 = 0x01; + +// ============================================================================= +// Unit position (movement struct) +// ============================================================================= + +/// Unit + this → movement struct pointer +pub const UNIT_MOVEMENT_OFFSET: usize = 0x118; + +/// Movement struct position offsets +pub const MOVEMENT_POS_X: usize = 0x10; +pub const MOVEMENT_POS_Y: usize = 0x14; +pub const MOVEMENT_POS_Z: usize = 0x18; + +// ============================================================================= +// Map / Zone identification +// ============================================================================= + +/// Current zone area ID — numeric, locale-safe zone identifier. +pub const ZONE_AREA_ID: usize = 0x00B4E314; + +/// ObjMgr + this → current map ID (u32). +pub const OBJMGR_MAP_ID_OFFSET: usize = 0xCC; + +/// Pointer to Map.dbc indexed lookup table. +pub const MAP_DBC_DATA: usize = 0x00C0DAA8; + +/// Pointer to Map.dbc max valid index. +pub const MAP_DBC_MAX: usize = 0x00C0DAAC; + +/// Map.dbc row + this → mapType (u32). 0=world, 1=instance, 2=raid, 3=battleground. +pub const MAP_DBC_MAP_TYPE_OFFSET: usize = 0x08; + +/// MapType value for battleground instances. +pub const MAP_TYPE_BATTLEGROUND: u32 = 3; + +// ============================================================================= +// Game state +// ============================================================================= + +/// Non-zero when the player is logged in and in the world. +pub const IS_IN_WORLD: usize = 0xB4B424; + +// ============================================================================= +// Raid targets +// ============================================================================= + +/// Static array of 8 GUIDs (64 bytes total). Index 0 = Star, 7 = Skull. +pub const RAID_TARGET_ARRAY: usize = 0x00B71368; + +// ============================================================================= +// Core function addresses +// ============================================================================= + +/// __stdcall(guidLo, guidHi) → object pointer. RET 8. +pub const FN_GET_OBJECT_BY_GUID: usize = 0x464870; + +/// __fastcall(unitIdStr_ECX) → GUID in EAX:EDX. +pub const FN_UNIT_GUID: usize = 0x515970; + +/// __thiscall(localPlayer_ECX, targetUnit_stack) → reaction int. >=4 = friendly. +pub const FN_UNIT_REACTION: usize = 0x6061E0; + +/// __fastcall(), no params, returns EAX(low):EDX(high). +pub const FN_GET_PLAYER_GUID: usize = 0x00468550; + +/// __fastcall(obj_ECX) → bool. GO interactability check. +pub const FN_CALL_SPELL_CAST_HANDLER: usize = 0x5F8800; + +/// CVar lookup: __fastcall(name_ECX) → CVar* or 0. +pub const FN_CVAR_LOOKUP: usize = 0x0063DEC0; + +/// SceneEnd: __thiscall(device). Per-frame hook point. +pub const FN_SCENE_END: usize = 0x5A17A0; + +// ============================================================================= +// D3D9 / GxDevice +// ============================================================================= + +/// Game's GxDevice global pointer. +pub const GX_DEVICE_PTR: usize = 0xC0ED38; + +/// GxDevice + this → IDirect3DDevice9*. +pub const GX_DEVICE_D3D_OFFSET: usize = 0x38A8; diff --git a/src/outline/d3d9_hook.zig b/src/outline/d3d9_hook.zig index b3f8b78..e7572d9 100644 --- a/src/outline/d3d9_hook.zig +++ b/src/outline/d3d9_hook.zig @@ -1247,14 +1247,13 @@ fn restoreVtableEntry(vtable_ptr: [*]usize, idx: usize, old_fn: usize) void { // D3D9 device / vtable discovery from game's existing device // ============================================================================= -pub const GX_DEVICE_PTR: usize = 0xC0ED38; -pub const GX_DEVICE_D3D_OFFSET: usize = 0x38A8; +const offsets = @import("../offsets.zig"); fn getD3D9VTable() ?[*]usize { - const gx_device = hook.readMem(u32, GX_DEVICE_PTR); + const gx_device = hook.readMem(u32, offsets.GX_DEVICE_PTR); if (gx_device == 0) return null; - const d3d_device = hook.readMem(u32, gx_device + GX_DEVICE_D3D_OFFSET); + const d3d_device = hook.readMem(u32, gx_device + offsets.GX_DEVICE_D3D_OFFSET); if (d3d_device == 0) return null; const vtable_addr = hook.readMem(u32, d3d_device); diff --git a/src/outline/model_hook.zig b/src/outline/model_hook.zig index 4632ca8..e4dcccc 100644 --- a/src/outline/model_hook.zig +++ b/src/outline/model_hook.zig @@ -19,7 +19,7 @@ const o = @import("offsets.zig"); const types = @import("types.zig"); const tracker = @import("tracker.zig"); const d3d9_hook = @import("d3d9_hook.zig"); -const wow = @import("wow.zig"); +const wow = @import("../wow.zig"); // ============================================================================= // Calling convention constants diff --git a/src/outline/offsets.zig b/src/outline/offsets.zig index fa05d7f..b4d6923 100644 --- a/src/outline/offsets.zig +++ b/src/outline/offsets.zig @@ -1,68 +1,6 @@ -//! WoW 1.12.1 (build 5875) memory addresses and struct offsets for the outline system. +//! Outline-specific WoW 1.12.1 memory addresses and struct offsets. //! -//! Sources: Ghidra analysis, UnitXP_SP3, Idris DLL reference implementation. - -// ============================================================================= -// Object Manager -// ============================================================================= - -/// Pointer to the Object Manager base. Dereference once to get the ObjMgr struct. -pub const OBJECT_MANAGER_PTR: usize = 0x00B41414; - -/// ObjMgr + this → first object in the linked list. -pub const OBJECT_LIST_OFFSET: usize = 0xAC; - -/// ObjMgr + this → base pointer for the next-object traversal table. -pub const OBJECT_NEXT_OFFSET: usize = 0xA4; - -/// ObjMgr + this → local player GUID (8 bytes). -pub const LOCAL_PLAYER_GUID_OFFSET: usize = 0xC0; - -// ============================================================================= -// Object fields (from object base pointer) -// ============================================================================= - -pub const OBJECT_TYPE_OFFSET: usize = 0x14; -pub const OBJECT_GUID_OFFSET: usize = 0x30; - -// ============================================================================= -// Unit / Player descriptor fields -// ============================================================================= - -/// object + this → pointer to descriptor block. -pub const UNIT_DESCRIPTOR_OFFSET: usize = 0x110; - -/// Descriptor + this → current HP (int32). -pub const UNIT_HP_OFFSET: usize = 0x40; - -/// Descriptor + this → unit flags (uint32). Test with UNIT_FLAG_DEAD. -pub const UNIT_FLAGS_OFFSET: usize = 0x224; - -/// Bit mask: unit is dead. -pub const UNIT_FLAG_DEAD: u32 = 0x20; - -// ============================================================================= -// Corpse descriptor fields -// ============================================================================= - -/// Corpse object + this → pointer to corpse descriptor. -pub const CORPSE_DESCRIPTOR_OFFSET: usize = 0x8; - -/// Corpse descriptor + this → owner GUID (8 bytes). -pub const CORPSE_FIELD_OWNER: usize = 0x18; - -/// Corpse descriptor + this → corpse flags (uint32). -pub const CORPSE_FIELD_FLAGS: usize = 0x8C; - -/// Bit mask: corpse is a skeleton (not resurrectable). -pub const CORPSE_FLAG_BONE: u32 = 0x01; - -// ============================================================================= -// Raid targets -// ============================================================================= - -/// Static array of 8 GUIDs (64 bytes total). Index 0 = Star, 7 = Skull. -pub const RAID_TARGET_ARRAY: usize = 0x00B71368; +//! Shared addresses (object manager, game functions, etc.) live in src/offsets.zig. // ============================================================================= // Model ownership (offsets from model pointer) @@ -81,27 +19,6 @@ pub const MODEL_OWNER_CALLBACK: usize = 0x3C0; /// renderContext + this → current model pointer being rendered. pub const RENDER_CONTEXT_MODEL_OFFSET: usize = 0x3310; -// ============================================================================= -// Game state -// ============================================================================= - -/// Non-zero when the player is logged in and in the world. -pub const IS_IN_WORLD: usize = 0xB4B424; - -// ============================================================================= -// Function addresses -// ============================================================================= - -/// __stdcall(guidLo_stack, guidHi_stack) → object pointer (EAX). Returns 0 on miss. -/// Callee cleans 8 bytes (RET 8). NOT __fastcall - params on stack, not registers. -pub const FN_GET_OBJECT_BY_GUID: usize = 0x464870; - -/// __fastcall(unitIdStr_ECX) → GUID in EAX:EDX. Accepts "player", "target", etc. -pub const FN_UNIT_GUID: usize = 0x515970; - -/// __thiscall(localPlayer_ECX, targetUnit_stack) → reaction int (0-7). >=4 = friendly. -pub const FN_UNIT_REACTION: usize = 0x6061E0; - // ============================================================================= // Hooked function addresses (model rendering pipeline) // ============================================================================= diff --git a/src/outline/tracker.zig b/src/outline/tracker.zig index de22238..750af8e 100644 --- a/src/outline/tracker.zig +++ b/src/outline/tracker.zig @@ -13,7 +13,7 @@ const std = @import("std"); const hook = @import("zhook"); const logging = @import("../logging.zig"); -const wow = @import("wow.zig"); +const wow = @import("../wow.zig"); const o = @import("offsets.zig"); const types = @import("types.zig"); diff --git a/src/screenshot/screenshot.zig b/src/screenshot/screenshot.zig index 87eba28..d3362b9 100644 --- a/src/screenshot/screenshot.zig +++ b/src/screenshot/screenshot.zig @@ -55,7 +55,8 @@ const ERROR_ALREADY_EXISTS: u32 = 183; // CVar for compression level persistence (0–9, default 6) const CVAR_NAME = "screenshotQuality"; -const CVAR_LOOKUP: usize = 0x0063DEC0; +const offsets = @import("../offsets.zig"); +const CVAR_LOOKUP: usize = offsets.FN_CVAR_LOOKUP; const RegisterCVarFn = *const fn ([*:0]const u8, u32, u32, [*:0]const u8, u32, u32, u32, u32) callconv(hook.cc.fastcall) u32; const registerCVar: RegisterCVarFn = @ptrFromInt(0x0063DB90); @@ -296,7 +297,6 @@ fn writePng(path: [*:0]const u8, pixels: [*]const u8, width: u16, height: u16, l // ============================================================================= pub fn installHook() void { - const result = mod_mutex.acquire(module_name); g_mutex = result.handle; g_is_hook_owner = result.is_owner; diff --git a/src/transmogfix/transmogfix.zig b/src/transmogfix/transmogfix.zig index cc4381f..efae297 100644 --- a/src/transmogfix/transmogfix.zig +++ b/src/transmogfix/transmogfix.zig @@ -17,6 +17,8 @@ const std = @import("std"); const hook = @import("zhook"); const logging = @import("../logging.zig"); +const offsets = @import("../offsets.zig"); +const wow = @import("../wow.zig"); const WINAPI = std.builtin.CallingConvention.winapi; @@ -32,14 +34,11 @@ const ERROR_ALREADY_EXISTS: u32 = 183; // Game addresses // ============================================================================= -const ADDR_UnitGUID: usize = 0x515970; -const ADDR_GetObjectByGUID: usize = 0x464870; const ADDR_UpdateInvAlerts: usize = 0x4c7ee0; const ADDR_RefreshAppearance: usize = 0x60afb0; const ADDR_RefreshEquipmentDisplay: usize = 0x60ABE0; const ADDR_SetBlock: usize = 0x6142E0; const ADDR_RefreshVisualAppearance: usize = 0x5fb880; -const ADDR_SceneEnd: usize = 0x5a17a0; // ============================================================================= // Constants (1.12.1 client) @@ -61,13 +60,11 @@ const DISPLAY_INFO_TABLE_PTR: usize = 0x00c0de90; // ============================================================================= fn unitGUID(unit_id: [*:0]const u8) u64 { - return hook.call(fn ([*:0]const u8) callconv(hook.cc.fastcall) u64, ADDR_UnitGUID, .{unit_id}); + return wow.unitGUID(unit_id); } fn getObjectByGUID(guid: u64) u32 { - const lo: u32 = @truncate(guid); - const hi: u32 = @truncate(guid >> 32); - return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, ADDR_GetObjectByGUID, .{ lo, hi }); + return wow.getObjectByGUID(guid); } fn updateInventoryAlertStates() void { @@ -795,7 +792,7 @@ pub fn installHooks() void { } // Hook 3: SceneEnd - if (scene_end_hook.attach(ADDR_SceneEnd, &hookSceneEnd) != .ok) { + if (scene_end_hook.attach(offsets.FN_SCENE_END, &hookSceneEnd) != .ok) { refresh_hook.detach(); set_block_hook.detach(); return; diff --git a/src/outline/wow.zig b/src/wow.zig similarity index 60% rename from src/outline/wow.zig rename to src/wow.zig index f72e52a..f4cd3d3 100644 --- a/src/outline/wow.zig +++ b/src/wow.zig @@ -1,13 +1,31 @@ -//! Game memory access wrappers for WoW 1.12.1. +//! Shared game memory access wrappers for WoW 1.12.1. //! //! Provides safe read helpers for the object manager, unit descriptors, -//! corpse fields, raid targets, and calling-convention wrappers for -//! game functions (UnitGUID, GetObjectByGUID, UnitReaction). +//! 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"); -const types = @import("types.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 @@ -20,14 +38,13 @@ extern "kernel32" fn IsBadReadPtr( ucb: usize, ) callconv(WINAPI) i32; -/// Quick sanity check - reject null, low-address, and kernel-space pointers. +/// Quick sanity check — reject null, low-address, and kernel-space pointers. pub fn isValidPtr(addr: u32) bool { return addr >= 0x10000 and addr < 0x7F000000; } -/// Check if a pointer is readable using the Windows API (matches C++ IsValidReadPtr). -/// This does an actual page-level check, not just a range check. -fn isReadablePtr(addr: u32, size: usize) bool { +/// 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; } @@ -60,11 +77,16 @@ pub fn objectNext(current: u32) u32 { // Object field reads // ============================================================================= -pub fn getObjectType(obj: u32) types.ObjectType { +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); @@ -76,6 +98,30 @@ pub fn getObjectGUID(obj: u32) u64 { 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); +} + // ============================================================================= // Unit helpers // ============================================================================= @@ -88,13 +134,24 @@ pub fn isUnitDead(unit: u32) bool { 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.CORPSE_DESCRIPTOR_OFFSET); + 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; @@ -102,51 +159,40 @@ pub fn isSkeletonCorpse(obj: u32) bool { pub fn getCorpseOwnerGUID(obj: u32) u64 { if (!isValidPtr(obj)) return 0; - const desc = hook.readMem(u32, obj + o.CORPSE_DESCRIPTOR_OFFSET); + const desc = hook.readMem(u32, obj + o.OBJECT_DATA_OFFSET); if (!isValidPtr(desc)) return 0; return readGUID(desc + o.CORPSE_FIELD_OWNER); } // ============================================================================= -// Model owner resolution +// Map / Battleground detection // ============================================================================= -/// Try to resolve the game object that owns a render model. -/// Tries callback owner (model+0x3C0) first, then direct owner (model+0x28). -/// Uses IsBadReadPtr for page-level validation, matching the C++ IsValidReadPtr pattern. -pub fn resolveModelOwner(model: u32) u32 { - // Need to read up to model+0x3C0+4 - if (!isReadablePtr(model, o.MODEL_OWNER_CALLBACK + 4)) return 0; - - // Try callback owner first - more reliable for units - const candidate_cb = hook.readMem(u32, model + o.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; - } - - // Fallback to direct owner (already validated model is readable past 0x28) - const candidate_dir = hook.readMem(u32, model + o.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; +/// 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 (calling-convention bridges) +// Game function wrappers // ============================================================================= /// UnitGUID("player") / UnitGUID("target") → 64-bit GUID. -/// __fastcall(unitIdStr_ECX) → EDX:EAX (u64). 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. -/// Ghidra-verified: __stdcall(guidLow, guidHigh) with RET 8. pub fn getObjectByGUID(guid: u64) u32 { if (guid == 0) return 0; const lo: u32 = @truncate(guid); @@ -154,6 +200,17 @@ pub fn getObjectByGUID(guid: u64) u32 { return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, o.FN_GET_OBJECT_BY_GUID, .{ lo, hi }); } +/// 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; + 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"); @@ -167,28 +224,49 @@ pub fn getTargetGUID() u64 { } /// Check if a unit is friendly to the local player. -/// Uses UnitReaction(__thiscall): ECX = localPlayer, stack = unit → int reaction. -/// Reaction >= 4 means friendly. 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; -/// Read the 8 raid target GUIDs from WoW's static array into a local cache. pub fn cacheRaidTargets() void { for (0..8) |i| { cached_raid_targets[i] = readGUID(@intCast(o.RAID_TARGET_ARRAY + i * 8)); } } -/// Return the raid mark index (1-8) for a given GUID, or 0 if not marked. pub fn getRaidMarkForGUID(guid: u64) u8 { if (guid == 0) return 0; for (cached_raid_targets, 0..) |rt, i| {