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
This commit is contained in:
MarcelineVQ
2026-03-10 13:53:29 -07:00
parent 1a9f0c237e
commit 3e37b96788
14 changed files with 370 additions and 495 deletions
+4 -4
View File
@@ -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;
+13 -64
View File
@@ -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;
+8 -19
View File
@@ -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 {
+8 -34
View File
@@ -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;
+6 -157
View File
@@ -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;
+31 -75
View File
@@ -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;
+167
View File
@@ -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;
+3 -4
View File
@@ -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);
+1 -1
View File
@@ -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
+2 -85
View File
@@ -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)
// =============================================================================
+1 -1
View File
@@ -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");
+2 -2
View File
@@ -55,7 +55,8 @@ const ERROR_ALREADY_EXISTS: u32 = 183;
// CVar for compression level persistence (09, 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;
+5 -8
View File
@@ -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;
+119 -41
View File
@@ -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| {