From f70bffef45b2d49941cccacc120d49bea807f869 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 19 Mar 2026 17:05:24 -0700 Subject: [PATCH] dpslog: WotLK combat log file writer, null GUID handling, unique chat handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Write WotLK-format CSV to Logs\WeirdCombatLog.txt when /combatlog is active (checks COMBAT_LOG_HANDLE at 0xB50544). Resolves names, flags, spell names eagerly per event via same CLEU buffer resolution as CombatLogGetCurrentEventInfo. Uses Win32 WriteFile with OS caching. - Null GUIDs output as 0x0000000000000000,nil,0x80000000,0x0 matching WotLK format. Empty resolved names output as nil not "". - DPSLog addon: every subevent has unique chat handler with correct name via factory functions. - DAMAGE_SHIELD scans victim's auras (not attacker's) — shield owner is the victim in the packet. --- src/dpslog/dpslog.zig | 126 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/dpslog/dpslog.zig b/src/dpslog/dpslog.zig index 01ae371..31a6fef 100644 --- a/src/dpslog/dpslog.zig +++ b/src/dpslog/dpslog.zig @@ -791,7 +791,133 @@ fn cleuSpellPrefix(id: u32) void { cleuSpellId(id); } +// ============================================================================= +// Combat log file writer — WotLK CSV format +// ============================================================================= +// Writes to Logs\WeirdCombatLog.txt when the game's /combatlog is active. +// Checks COMBAT_LOG_HANDLE (0xB50544) each event — zero cost when logging is off. + +const COMBAT_LOG_HANDLE_ADDR: u32 = 0xB50544; + +const WINAPI = std.builtin.CallingConvention.winapi; +extern "kernel32" fn CreateFileA(lpFileName: [*:0]const u8, dwDesiredAccess: u32, dwShareMode: u32, lpSecurityAttributes: ?*anyopaque, dwCreationDisposition: u32, dwFlagsAndAttributes: u32, hTemplateFile: ?*anyopaque) callconv(WINAPI) usize; +extern "kernel32" fn WriteFile(hFile: usize, lpBuffer: [*]const u8, nNumberOfBytesToWrite: u32, lpNumberOfBytesWritten: ?*u32, lpOverlapped: ?*anyopaque) callconv(WINAPI) i32; +extern "kernel32" fn GetLocalTime(lpSystemTime: *SystemTime) callconv(WINAPI) void; + +const SystemTime = extern struct { + wYear: u16, wMonth: u16, wDayOfWeek: u16, wDay: u16, + wHour: u16, wMinute: u16, wSecond: u16, wMilliseconds: u16, +}; + +const INVALID_HANDLE: usize = 0xFFFFFFFF; +var g_logfile_handle: usize = INVALID_HANDLE; + +fn ensureLogFile() usize { + if (g_logfile_handle != INVALID_HANDLE) return g_logfile_handle; + g_logfile_handle = CreateFileA( + "Logs\\WeirdCombatLog.txt", + 0x40000000, // GENERIC_WRITE + 0x00000001, // FILE_SHARE_READ + null, + 4, // OPEN_ALWAYS + 0x80, // FILE_ATTRIBUTE_NORMAL + null, + ); + if (g_logfile_handle == INVALID_HANDLE) return INVALID_HANDLE; + // Seek to end + const SetFilePointer = @extern(*const fn (usize, i32, ?*i32, u32) callconv(WINAPI) u32, .{ .name = "SetFilePointer", .library_name = "kernel32" }); + _ = SetFilePointer(g_logfile_handle, 0, null, 2); + return g_logfile_handle; +} + +fn writeLogEvent() void { + // Check if game's combat logging is active + if (hook.readMem(u32, COMBAT_LOG_HANDLE_ADDR) == 0) return; + + const handle = ensureLogFile(); + if (handle == INVALID_HANDLE) return; + + var buf: [2048]u8 = undefined; + var pos: usize = 0; + + // Timestamp: M/D HH:MM:SS.mmm (two trailing spaces before event data) + var st: SystemTime = undefined; + GetLocalTime(&st); + pos += (std.fmt.bufPrint(buf[pos..], "{d}/{d} {d:0>2}:{d:0>2}:{d:0>2}.{d:0>3} ", .{ + st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, + }) catch return).len; + + // Resolve and format each CLEU entry + var first = true; + var i: u32 = 0; + while (i < g_cleu_count) : (i += 1) { + if (!first and pos < buf.len) { + buf[pos] = ','; + pos += 1; + } + + const entry = g_cleu_args[i]; + const written: []const u8 = switch (entry.typ) { + .str => std.fmt.bufPrint(buf[pos..], "{s}", .{std.mem.span(entry.arg.str)}) catch break, + .num => std.fmt.bufPrint(buf[pos..], "{d}", .{@as(i32, @bitCast(entry.arg.num))}) catch break, + .bool_val => std.fmt.bufPrint(buf[pos..], "1", .{}) catch break, + .nil => std.fmt.bufPrint(buf[pos..], "nil", .{}) catch break, + .guid => blk: { + const guid = entry.arg.guid; + if (guid == 0) { + const w = std.fmt.bufPrint(buf[pos..], "0x0000000000000000,nil,0x80000000,0x0", .{}) catch break; + first = false; + break :blk w; + } + const name = wow.getNameByGUID(guid); + const name_span = std.mem.span(name); + const flags = computeUnitFlags(guid); + const rflags = computeRaidFlags(guid); + const w = if (name_span.len == 0) + std.fmt.bufPrint(buf[pos..], "{s},nil,0x{x},0x{x}", .{ + std.mem.span(guidToString(guid)), flags, rflags, + }) catch break + else + std.fmt.bufPrint(buf[pos..], "{s},\"{s}\",0x{x},0x{x}", .{ + std.mem.span(guidToString(guid)), name_span, flags, rflags, + }) catch break; + first = false; + break :blk w; + }, + .spell_id => blk: { + const id = entry.arg.num; + const w = std.fmt.bufPrint(buf[pos..], "{d},\"{s}\",0x{x}", .{ + id, std.mem.span(getSpellName(id)), getSpellSchool(id), + }) catch break; + first = false; + break :blk w; + }, + .damage_shield_spell => blk: { + const key = entry.arg.ds_key; + const id = findDamageShieldSpell(key.attacker_guid, key.school); + const school = if (id != 0) getSpellSchool(id) else key.school; + const w = std.fmt.bufPrint(buf[pos..], "{d},\"{s}\",0x{x}", .{ + id, std.mem.span(getSpellName(id)), school, + }) catch break; + first = false; + break :blk w; + }, + }; + pos += written.len; + first = false; + } + + if (pos < buf.len) { + buf[pos] = '\n'; + pos += 1; + } + + var bytes_written: u32 = 0; + _ = WriteFile(handle, &buf, @intCast(pos), &bytes_written, null); +} + fn signalEvent() void { + writeLogEvent(); const F = fn (u32, [*:0]const u8) callconv(hook.cc.cdecl) void; @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "" }); }