Add logging module with auto-prefix, route all output through Logger
Replace console.zig with logging.zig: per-module Logger with auto [name] prefix, optional file output, and destination routing. Convert all modules from manual [name] prefixes and global con.print to Logger instances. Remove redundant "Module loaded" lines. Replace OutputDebugStringA in outline/tracker with Logger. Add dpslog module with structured combat log events (SPELL_DMG, PERIODIC, HEAL, MELEE). Add clickthrough module and bigcursor D3D9 cursor scaling.
This commit is contained in:
+542
-6
@@ -1,33 +1,569 @@
|
||||
//! DPS log module.
|
||||
//! DPS log module — structured combat log events.
|
||||
//!
|
||||
//! Provides structured Lua objects for combat log events so addons can read
|
||||
//! parsed fields directly instead of re-parsing the combat log string.
|
||||
//! Hooks packet handlers for SMSG_SPELLNONMELEEDAMAGELOG,
|
||||
//! SMSG_PERIODICAURALOG, SMSG_SPELLHEALLOG, and SMSG_ATTACKERSTATEUPDATE
|
||||
//! to fire custom Lua events with structured numeric/string args, so addons
|
||||
//! can skip string parsing.
|
||||
//!
|
||||
//! TODO: Hook combat log event dispatch, build Lua tables per event type.
|
||||
//! Custom events (registered in unused event table slots):
|
||||
//! COMBAT_LOG_SPELL_DMG (551) — spell damage (direct)
|
||||
//! COMBAT_LOG_PERIODIC (552) — periodic damage/heal ticks
|
||||
//! COMBAT_LOG_HEAL (553) — direct heals
|
||||
//! COMBAT_LOG_MELEE (554) — melee damage (auto-attack + special)
|
||||
//!
|
||||
//! Lua args for COMBAT_LOG_SPELL_DMG:
|
||||
//! arg1=targetGUID, arg2=casterGUID, arg3=spellId, arg4=damage,
|
||||
//! arg5=school, arg6=absorb, arg7=resist, arg8=blocked, arg9=hitInfo
|
||||
//!
|
||||
//! Lua args for COMBAT_LOG_PERIODIC:
|
||||
//! arg1=targetGUID, arg2=casterGUID, arg3=spellId, arg4=amount,
|
||||
//! arg5=school, arg6=absorb, arg7=resist, arg8=auraType, arg9=powerType
|
||||
//!
|
||||
//! Lua args for COMBAT_LOG_HEAL:
|
||||
//! arg1=targetGUID, arg2=casterGUID, arg3=spellId, arg4=healAmount,
|
||||
//! arg5=isCrit
|
||||
//!
|
||||
//! Lua args for COMBAT_LOG_MELEE:
|
||||
//! arg1=targetGUID, arg2=attackerGUID, arg3=totalDamage, arg4=school,
|
||||
//! arg5=absorb, arg6=resist, arg7=blocked, arg8=hitInfo, arg9=victimState
|
||||
|
||||
const con = @import("../console.zig");
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const logging = @import("../logging.zig");
|
||||
const mod_mutex = @import("../mutex.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "dpslog";
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
pub fn isActive() bool {
|
||||
return g_is_hook_owner;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Custom event IDs & names
|
||||
// =============================================================================
|
||||
|
||||
/// We register events at slots 551..554 (beyond nampower's 549/550).
|
||||
/// The event name string pointer array has 4-byte stride per event ID.
|
||||
/// Nampower: 549→0xBE1A2C, 550→0xBE1A30 → base = 0xBE1198.
|
||||
/// 551: 0xBE1A34 552: 0xBE1A38 553: 0xBE1A3C 554: 0xBE1A40
|
||||
const EVENT_SPELL_DMG: u32 = 551;
|
||||
const EVENT_PERIODIC: u32 = 552;
|
||||
const EVENT_HEAL: u32 = 553;
|
||||
const EVENT_MELEE: u32 = 554;
|
||||
|
||||
/// Derived from nampower: event 549 string ptr is at 0xBE1A2C → base = 0xBE1198.
|
||||
const EVENT_STR_PTR_BASE: u32 = 0xBE1198;
|
||||
|
||||
/// Static event name strings — must outlive the process.
|
||||
const event_name_spell_dmg: [*:0]const u8 = "COMBAT_LOG_SPELL_DMG";
|
||||
const event_name_periodic: [*:0]const u8 = "COMBAT_LOG_PERIODIC";
|
||||
const event_name_heal: [*:0]const u8 = "COMBAT_LOG_HEAL";
|
||||
const event_name_melee: [*:0]const u8 = "COMBAT_LOG_MELEE";
|
||||
|
||||
// TODO: Detect nampower via GetModuleHandleA("nampower.dll") and skip
|
||||
// overlapping SpellNonMeleeDmgLog/PeriodicAuraLog hooks if loaded (nampower
|
||||
// fires SPELL_DAMAGE_EVENT_SELF/OTHER for those). Low priority — most users
|
||||
// won't run both DLLs simultaneously.
|
||||
|
||||
// =============================================================================
|
||||
// CDataStore helpers — direct memory reads from packet buffer
|
||||
// =============================================================================
|
||||
//
|
||||
// CDataStore layout (from nampower cdatastore.hpp):
|
||||
// +0x00: vtable
|
||||
// +0x04: m_buffer (u8 pointer)
|
||||
// +0x08: m_base
|
||||
// +0x0C: m_alloc
|
||||
// +0x10: m_size
|
||||
// +0x14: m_read (current read position)
|
||||
//
|
||||
// NOTE: the nampower header shows m_buffer at +0x00 after vtable, but
|
||||
// CDataStore is a virtual class — vtable is +0x00, members follow.
|
||||
// Let's match nampower's actual memory layout which treats it as:
|
||||
// m_buffer at offset after vtable. In the C++ class, the first member
|
||||
// after the vtable IS m_buffer. So:
|
||||
// +0x00: vtable ptr
|
||||
// +0x04: m_buffer
|
||||
// +0x08: m_base
|
||||
// +0x0C: m_alloc
|
||||
// +0x10: m_size
|
||||
// +0x14: m_read
|
||||
|
||||
const CDS_BUFFER = 0x04;
|
||||
const CDS_BASE = 0x08;
|
||||
const CDS_SIZE = 0x10;
|
||||
const CDS_READ = 0x14;
|
||||
|
||||
fn cdsGetRead(cds: u32) u32 {
|
||||
return hook.readMem(u32, cds + CDS_READ);
|
||||
}
|
||||
|
||||
fn cdsSetRead(cds: u32, pos: u32) void {
|
||||
@as(*align(1) u32, @ptrFromInt(cds + CDS_READ)).* = pos;
|
||||
}
|
||||
|
||||
fn cdsBuffer(cds: u32) u32 {
|
||||
return hook.readMem(u32, cds + CDS_BUFFER);
|
||||
}
|
||||
|
||||
fn cdsBase(cds: u32) u32 {
|
||||
return hook.readMem(u32, cds + CDS_BASE);
|
||||
}
|
||||
|
||||
fn cdsSize(cds: u32) u32 {
|
||||
return hook.readMem(u32, cds + CDS_SIZE);
|
||||
}
|
||||
|
||||
/// Read a T from the CDataStore at the current read position, advancing m_read.
|
||||
fn cdsGet(comptime T: type, cds: u32) ?T {
|
||||
const rpos = cdsGetRead(cds);
|
||||
const end = rpos + @sizeOf(T);
|
||||
if (end > cdsSize(cds)) return null;
|
||||
const buf = cdsBuffer(cds);
|
||||
const base = cdsBase(cds);
|
||||
const val = @as(*align(1) const T, @ptrFromInt(buf -% base + rpos)).*;
|
||||
cdsSetRead(cds, end);
|
||||
return val;
|
||||
}
|
||||
|
||||
/// Read a packed GUID from the CDataStore. Returns null on read error.
|
||||
/// Packed GUID format: 1-byte mask, then one byte per set bit in the mask.
|
||||
fn cdsGetPackedGuid(cds: u32) ?u64 {
|
||||
const mask = cdsGet(u8, cds) orelse return null;
|
||||
var guid: u64 = 0;
|
||||
inline for (0..8) |i| {
|
||||
if (mask & (@as(u8, 1) << @intCast(i)) != 0) {
|
||||
const b = cdsGet(u8, cds) orelse return null;
|
||||
guid |= @as(u64, b) << @intCast(i * 8);
|
||||
}
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GUID → hex string conversion
|
||||
// =============================================================================
|
||||
|
||||
/// Convert a u64 GUID to a 0x-prefixed hex string. Returns pointer to static buffer.
|
||||
/// Uses two alternating buffers so we can format target + caster in one call.
|
||||
var guid_bufs: [4][20]u8 = undefined;
|
||||
var guid_buf_idx: u2 = 0;
|
||||
|
||||
fn guidToString(guid: u64) [*:0]const u8 {
|
||||
const idx = guid_buf_idx;
|
||||
guid_buf_idx +%= 1;
|
||||
const buf = &guid_bufs[idx];
|
||||
|
||||
buf[0] = '0';
|
||||
buf[1] = 'x';
|
||||
const hex = "0123456789ABCDEF";
|
||||
inline for (0..16) |i| {
|
||||
const shift: u6 = @intCast((15 - i) * 4);
|
||||
buf[2 + i] = hex[@intCast((guid >> shift) & 0xF)];
|
||||
}
|
||||
buf[18] = 0;
|
||||
return @ptrCast(buf[0..18 :0]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SignalEventParam — fire a Lua event with structured args
|
||||
// =============================================================================
|
||||
//
|
||||
// SignalEventParam (0x703F50): __cdecl(eventId, fmtStr, ...)
|
||||
// The format string uses %s/%d to push args as Lua event args (arg1, arg2, ...).
|
||||
// We use typed wrapper functions since Zig can't express variadic cdecl generically.
|
||||
|
||||
/// Fire spell damage event: "%s%s%d%d%d%d%d%d%d"
|
||||
/// args: targetGuid, casterGuid, spellId, damage, school, absorb, resist, blocked, hitInfo
|
||||
fn fireSpellDamageEvent(event_id: u32, target: [*:0]const u8, caster: [*:0]const u8, spell_id: u32, damage: u32, school: u32, absorb: u32, resist: i32, blocked: u32, hit_info: u32) void {
|
||||
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, i32, u32, u32) callconv(hook.cc.cdecl) void;
|
||||
const f: *const SignalFn = @ptrFromInt(0x703F50);
|
||||
// Format: targetGuid(s), casterGuid(s), spellId(d), damage(d), school(d), absorb(d), resist(d), blocked(d), hitInfo(d)
|
||||
@call(.auto, f, .{ event_id, "%s%s%d%d%d%d%d%d%d", target, caster, spell_id, damage, school, absorb, resist, blocked, hit_info });
|
||||
}
|
||||
|
||||
/// Fire periodic event: "%s%s%d%d%d%d%d%d%d"
|
||||
/// Same format as spell damage but with auraType instead of hitInfo.
|
||||
fn firePeriodicEvent(event_id: u32, target: [*:0]const u8, caster: [*:0]const u8, spell_id: u32, amount: u32, school: u32, absorb: u32, resist: i32, aura_type: u32, power_type: u32) void {
|
||||
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, i32, u32, u32) callconv(hook.cc.cdecl) void;
|
||||
const f: *const SignalFn = @ptrFromInt(0x703F50);
|
||||
@call(.auto, f, .{ event_id, "%s%s%d%d%d%d%d%d%d", target, caster, spell_id, amount, school, absorb, resist, aura_type, power_type });
|
||||
}
|
||||
|
||||
/// Fire heal event: "%s%s%d%d%d"
|
||||
/// args: targetGuid, casterGuid, spellId, healAmount, isCrit
|
||||
fn fireHealEvent(event_id: u32, target: [*:0]const u8, caster: [*:0]const u8, spell_id: u32, amount: u32, is_crit: u32) void {
|
||||
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32) callconv(hook.cc.cdecl) void;
|
||||
const f: *const SignalFn = @ptrFromInt(0x703F50);
|
||||
@call(.auto, f, .{ event_id, "%s%s%d%d%d", target, caster, spell_id, amount, is_crit });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: FrameScript_CreateEvents (0x703D90)
|
||||
// Expands max event count to accommodate our custom event slots.
|
||||
// =============================================================================
|
||||
|
||||
/// FrameScript_CreateEvents signature: __fastcall(ECX=param1, EDX=maxEventId)
|
||||
/// Nampower hooks this as (int param_1, uint32_t maxEventId) — the first two
|
||||
/// args in fastcall go to ECX and EDX.
|
||||
var create_events_hook: hook.Detour(fn (u32, u32) callconv(hook.cc.fastcall) void) = .{};
|
||||
|
||||
fn createEventsDetour(param1: u32, max_event_id: u32) callconv(hook.cc.fastcall) void {
|
||||
// Expand to at least 556 (we use slots 551..554, need maxId > 554)
|
||||
var new_max = max_event_id;
|
||||
if (new_max < 556) {
|
||||
new_max = 556;
|
||||
log.fmt("FrameScript_CreateEvents: expanded maxEventId {d} -> {d}\n", .{ max_event_id, new_max });
|
||||
}
|
||||
create_events_hook.callOriginal(.{ param1, new_max });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: SpellNonMeleeDmgLogHandler (0x5E85E0)
|
||||
// Packet: SMSG_SPELLNONMELEEDAMAGELOG
|
||||
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
|
||||
// =============================================================================
|
||||
|
||||
const FastCallPacketHandlerFn = fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) u32;
|
||||
|
||||
var spell_dmg_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
|
||||
|
||||
fn spellNonMeleeDmgLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
// Save read position
|
||||
const saved_read = cdsGetRead(cds);
|
||||
|
||||
// Parse SMSG_SPELLNONMELEEDAMAGELOG packet
|
||||
const target_guid = cdsGetPackedGuid(cds);
|
||||
const caster_guid = cdsGetPackedGuid(cds);
|
||||
const spell_id = cdsGet(u32, cds);
|
||||
const damage = cdsGet(u32, cds);
|
||||
const school = cdsGet(u8, cds);
|
||||
const absorb = cdsGet(u32, cds);
|
||||
const resist = cdsGet(i32, cds);
|
||||
const _periodic_log = cdsGet(u8, cds);
|
||||
const _unused = cdsGet(u8, cds);
|
||||
const blocked = cdsGet(u32, cds);
|
||||
const hit_info = cdsGet(u32, cds);
|
||||
|
||||
// Restore read position before calling original
|
||||
cdsSetRead(cds, saved_read);
|
||||
|
||||
// Fire event if all fields parsed successfully
|
||||
if (target_guid != null and caster_guid != null and spell_id != null and
|
||||
damage != null and school != null and absorb != null and resist != null and
|
||||
blocked != null and hit_info != null)
|
||||
{
|
||||
const target_str = guidToString(target_guid.?);
|
||||
const caster_str = guidToString(caster_guid.?);
|
||||
|
||||
fireSpellDamageEvent(
|
||||
EVENT_SPELL_DMG,
|
||||
target_str,
|
||||
caster_str,
|
||||
spell_id.?,
|
||||
damage.?,
|
||||
@as(u32, school.?),
|
||||
absorb.?,
|
||||
resist.?,
|
||||
blocked.?,
|
||||
hit_info.?,
|
||||
);
|
||||
}
|
||||
|
||||
_ = _periodic_log;
|
||||
_ = _unused;
|
||||
|
||||
return spell_dmg_hook.callOriginal(.{ unk, opcode, unk2, cds });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: PeriodicAuraLogHandler (0x626DD0)
|
||||
// Packet: SMSG_PERIODICAURALOG
|
||||
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
|
||||
// =============================================================================
|
||||
|
||||
var periodic_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
|
||||
|
||||
fn periodicAuraLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
const saved_read = cdsGetRead(cds);
|
||||
|
||||
// Parse SMSG_PERIODICAURALOG packet
|
||||
const target_guid = cdsGetPackedGuid(cds);
|
||||
const caster_guid = cdsGetPackedGuid(cds);
|
||||
const spell_id = cdsGet(u32, cds);
|
||||
const count = cdsGet(u32, cds);
|
||||
|
||||
if (target_guid != null and caster_guid != null and spell_id != null and count != null) {
|
||||
const target_str = guidToString(target_guid.?);
|
||||
const caster_str = guidToString(caster_guid.?);
|
||||
|
||||
// Process first aura entry (count is almost always 1)
|
||||
const aura_type = cdsGet(u32, cds);
|
||||
if (aura_type) |at| {
|
||||
switch (at) {
|
||||
3, 89 => {
|
||||
// PERIODIC_DAMAGE / PERIODIC_DAMAGE_PERCENT
|
||||
const amount = cdsGet(u32, cds) orelse 0;
|
||||
const spell_school = cdsGet(u32, cds) orelse 0;
|
||||
const absorb = cdsGet(u32, cds) orelse 0;
|
||||
const resist = cdsGet(i32, cds) orelse 0;
|
||||
|
||||
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, spell_school, absorb, resist, at, 0);
|
||||
},
|
||||
8, 20 => {
|
||||
// PERIODIC_HEAL / OBS_MOD_HEALTH
|
||||
const amount = cdsGet(u32, cds) orelse 0;
|
||||
|
||||
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, 0, 0, 0, at, 0);
|
||||
},
|
||||
21, 24 => {
|
||||
// OBS_MOD_MANA / PERIODIC_ENERGIZE
|
||||
const power_type = cdsGet(u32, cds) orelse 0;
|
||||
const amount = cdsGet(u32, cds) orelse 0;
|
||||
|
||||
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, 0, 0, 0, at, power_type);
|
||||
},
|
||||
64 => {
|
||||
// PERIODIC_MANA_LEECH
|
||||
const power_type = cdsGet(u32, cds) orelse 0;
|
||||
const amount = cdsGet(u32, cds) orelse 0;
|
||||
_ = cdsGet(u32, cds); // multiplier (skip)
|
||||
|
||||
firePeriodicEvent(EVENT_PERIODIC, target_str, caster_str, spell_id.?, amount, 0, 0, 0, at, power_type);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore read position
|
||||
cdsSetRead(cds, saved_read);
|
||||
|
||||
return periodic_hook.callOriginal(.{ unk, opcode, unk2, cds });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: SpellHealLogHandler (0x5E89C0)
|
||||
// Packet: SMSG_SPELLHEALLOG (opcode 0x150)
|
||||
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
|
||||
// Packet format: targetGuid(PackedGuid), casterGuid(PackedGuid),
|
||||
// spellId(u32), healAmount(u32), isCrit(u8)
|
||||
// =============================================================================
|
||||
|
||||
var heal_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
|
||||
|
||||
fn spellHealLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
const saved_read = cdsGetRead(cds);
|
||||
|
||||
// Parse SMSG_SPELLHEALLOG packet
|
||||
const target_guid = cdsGetPackedGuid(cds);
|
||||
const caster_guid = cdsGetPackedGuid(cds);
|
||||
const spell_id = cdsGet(u32, cds);
|
||||
const heal_amount = cdsGet(u32, cds);
|
||||
const is_crit_raw = cdsGet(u8, cds);
|
||||
|
||||
cdsSetRead(cds, saved_read);
|
||||
|
||||
if (target_guid != null and caster_guid != null and spell_id != null and
|
||||
heal_amount != null and is_crit_raw != null)
|
||||
{
|
||||
const target_str = guidToString(target_guid.?);
|
||||
const caster_str = guidToString(caster_guid.?);
|
||||
const is_crit: u32 = if (is_crit_raw.? != 0) 1 else 0;
|
||||
|
||||
fireHealEvent(EVENT_HEAL, target_str, caster_str, spell_id.?, heal_amount.?, is_crit);
|
||||
}
|
||||
|
||||
return heal_hook.callOriginal(.{ unk, opcode, unk2, cds });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: MeleeDispatcher (0x6255B0)
|
||||
// Shared packet handler for opcodes 0x143-0x14A. We filter for 0x14A only.
|
||||
// Packet: SMSG_ATTACKERSTATEUPDATE (opcode 0x14A)
|
||||
// Convention: FastCall(unk, opCode, unk2, CDataStore*)
|
||||
//
|
||||
// Packet format:
|
||||
// hitInfo(u32), attackerGuid(PackedGuid), targetGuid(PackedGuid),
|
||||
// totalDamage(u32), subDamageCount(u8),
|
||||
// per sub: school(u32), damageFP(f32), damage(u32), absorb(u32), resist(u32),
|
||||
// victimState(u32), unknown1(u32), unknown2(u32), spellId(u32),
|
||||
// if hitInfo & 0x1: blocked(u32)
|
||||
// =============================================================================
|
||||
|
||||
const OPCODE_ATTACKERSTATEUPDATE: u32 = 0x14A;
|
||||
|
||||
var melee_hook: hook.Detour(FastCallPacketHandlerFn) = .{};
|
||||
|
||||
fn meleeDispatcherDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc.fastcall) u32 {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
// Only intercept SMSG_ATTACKERSTATEUPDATE; pass all other opcodes through
|
||||
if (opcode == OPCODE_ATTACKERSTATEUPDATE) {
|
||||
parseMeleePacket(cds);
|
||||
}
|
||||
|
||||
return melee_hook.callOriginal(.{ unk, opcode, unk2, cds });
|
||||
}
|
||||
|
||||
fn parseMeleePacket(cds: u32) void {
|
||||
const saved_read = cdsGetRead(cds);
|
||||
defer cdsSetRead(cds, saved_read);
|
||||
|
||||
const hit_info = cdsGet(u32, cds) orelse return;
|
||||
const attacker_guid = cdsGetPackedGuid(cds) orelse return;
|
||||
const target_guid = cdsGetPackedGuid(cds) orelse return;
|
||||
const total_damage = cdsGet(u32, cds) orelse return;
|
||||
const sub_count = cdsGet(u8, cds) orelse return;
|
||||
|
||||
// Read first sub-damage entry for school/absorb/resist
|
||||
var school: u32 = 0;
|
||||
var absorb: u32 = 0;
|
||||
var resist: u32 = 0;
|
||||
|
||||
if (sub_count > 0) {
|
||||
school = cdsGet(u32, cds) orelse 0;
|
||||
_ = cdsGet(f32, cds); // damageFP (skip)
|
||||
_ = cdsGet(u32, cds); // damage (skip, use totalDamage)
|
||||
absorb = cdsGet(u32, cds) orelse 0;
|
||||
resist = cdsGet(u32, cds) orelse 0;
|
||||
|
||||
// Skip remaining sub-damage entries
|
||||
var i: u8 = 1;
|
||||
while (i < sub_count) : (i += 1) {
|
||||
_ = cdsGet(u32, cds); // school
|
||||
_ = cdsGet(f32, cds); // damageFP
|
||||
_ = cdsGet(u32, cds); // damage
|
||||
_ = cdsGet(u32, cds); // absorb
|
||||
_ = cdsGet(u32, cds); // resist
|
||||
}
|
||||
}
|
||||
|
||||
const victim_state = cdsGet(u32, cds) orelse return;
|
||||
_ = cdsGet(u32, cds); // unknown1
|
||||
_ = cdsGet(u32, cds); // unknown2
|
||||
_ = cdsGet(u32, cds); // spellId (0 for melee, nonzero for special attacks)
|
||||
|
||||
var blocked: u32 = 0;
|
||||
if (hit_info & 0x1 != 0) {
|
||||
blocked = cdsGet(u32, cds) orelse 0;
|
||||
}
|
||||
|
||||
const target_str = guidToString(target_guid);
|
||||
const attacker_str = guidToString(attacker_guid);
|
||||
|
||||
// Fire melee event: same 9-arg format as spell damage
|
||||
// arg1=target, arg2=attacker, arg3=totalDamage, arg4=school,
|
||||
// arg5=absorb, arg6=resist, arg7=blocked, arg8=hitInfo, arg9=victimState
|
||||
fireMeleeEvent(EVENT_MELEE, target_str, attacker_str, total_damage, school, absorb, resist, blocked, hit_info, victim_state);
|
||||
}
|
||||
|
||||
/// Fire melee event: "%s%s%d%d%d%d%d%d%d"
|
||||
/// args: targetGuid, attackerGuid, totalDamage, school, absorb, resist, blocked, hitInfo, victimState
|
||||
fn fireMeleeEvent(event_id: u32, target: [*:0]const u8, attacker: [*:0]const u8, damage: u32, school: u32, absorb: u32, resist: u32, blocked: u32, hit_info: u32, victim_state: u32) void {
|
||||
const SignalFn = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void;
|
||||
const f: *const SignalFn = @ptrFromInt(0x703F50);
|
||||
@call(.auto, f, .{ event_id, "%s%s%d%d%d%d%d%d%d", target, attacker, damage, school, absorb, resist, blocked, hit_info, victim_state });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Custom event registration — write event name ptrs to unused table slots
|
||||
// =============================================================================
|
||||
|
||||
fn registerCustomEvents() void {
|
||||
// Each event slot's string name pointer is at EVENT_STR_PTR_BASE + eventId * 4
|
||||
// These are in .data (RW), so writeProtected is needed since the event name
|
||||
// array is in .rdata/.data boundary area.
|
||||
|
||||
const spell_dmg_addr = EVENT_STR_PTR_BASE + EVENT_SPELL_DMG * 4;
|
||||
const periodic_addr = EVENT_STR_PTR_BASE + EVENT_PERIODIC * 4;
|
||||
const heal_addr = EVENT_STR_PTR_BASE + EVENT_HEAL * 4;
|
||||
const melee_addr = EVENT_STR_PTR_BASE + EVENT_MELEE * 4;
|
||||
|
||||
const spell_dmg_ptr: u32 = @intFromPtr(event_name_spell_dmg);
|
||||
const periodic_ptr: u32 = @intFromPtr(event_name_periodic);
|
||||
const heal_ptr: u32 = @intFromPtr(event_name_heal);
|
||||
const melee_ptr: u32 = @intFromPtr(event_name_melee);
|
||||
|
||||
hook.writeProtected(spell_dmg_addr, std.mem.asBytes(&spell_dmg_ptr));
|
||||
hook.writeProtected(periodic_addr, std.mem.asBytes(&periodic_ptr));
|
||||
hook.writeProtected(heal_addr, std.mem.asBytes(&heal_ptr));
|
||||
hook.writeProtected(melee_addr, std.mem.asBytes(&melee_ptr));
|
||||
|
||||
log.fmt("Registered events: {d}={s}, {d}={s}, {d}={s}, {d}={s}\n", .{
|
||||
EVENT_SPELL_DMG, std.mem.span(event_name_spell_dmg),
|
||||
EVENT_PERIODIC, std.mem.span(event_name_periodic),
|
||||
EVENT_HEAL, std.mem.span(event_name_heal),
|
||||
EVENT_MELEE, std.mem.span(event_name_melee),
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / remove
|
||||
// =============================================================================
|
||||
|
||||
pub fn installHooks() void {
|
||||
con.print("[dpslog] Module loaded (stub)\n");
|
||||
|
||||
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);
|
||||
|
||||
// Register custom events (overwrite event name string pointers)
|
||||
registerCustomEvents();
|
||||
|
||||
// Hook FrameScript_CreateEvents to expand event count
|
||||
if (create_events_hook.attach(0x703D90, &createEventsDetour) != .ok) {
|
||||
log.print("FAILED to hook FrameScript_CreateEvents\n");
|
||||
} else {
|
||||
log.print("Hooked FrameScript_CreateEvents\n");
|
||||
}
|
||||
|
||||
if (spell_dmg_hook.attach(0x5E85E0, &spellNonMeleeDmgLogDetour) != .ok) {
|
||||
log.print("FAILED to hook SpellNonMeleeDmgLogHandler\n");
|
||||
} else {
|
||||
log.print("Hooked SpellNonMeleeDmgLogHandler\n");
|
||||
}
|
||||
|
||||
if (periodic_hook.attach(0x626DD0, &periodicAuraLogDetour) != .ok) {
|
||||
log.print("FAILED to hook PeriodicAuraLogHandler\n");
|
||||
} else {
|
||||
log.print("Hooked PeriodicAuraLogHandler\n");
|
||||
}
|
||||
|
||||
if (heal_hook.attach(0x5E89C0, &spellHealLogDetour) != .ok) {
|
||||
log.print("FAILED to hook SpellHealLogHandler\n");
|
||||
} else {
|
||||
log.print("Hooked SpellHealLogHandler\n");
|
||||
}
|
||||
|
||||
if (melee_hook.attach(0x6255B0, &meleeDispatcherDetour) != .ok) {
|
||||
log.print("FAILED to hook MeleeDispatcher\n");
|
||||
} else {
|
||||
log.print("Hooked MeleeDispatcher\n");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
melee_hook.detach();
|
||||
heal_hook.detach();
|
||||
periodic_hook.detach();
|
||||
spell_dmg_hook.detach();
|
||||
create_events_hook.detach();
|
||||
log.close();
|
||||
mod_mutex.release(&g_mutex);
|
||||
}
|
||||
g_is_hook_owner = false;
|
||||
|
||||
Reference in New Issue
Block a user