Initial commit: WeirdUtils DLL for WoW 1.12.1
Lua protection bypass, embedded addon loading, async PNG screenshots with self-contained deflate compressor (store + fixed Huffman). 15KB ReleaseSmall.
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
reference/
|
||||
research/
|
||||
@@ -0,0 +1,30 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.resolveTargetQuery(.{
|
||||
.cpu_arch = .x86,
|
||||
.os_tag = .windows,
|
||||
.abi = .msvc,
|
||||
});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const hook_mod = b.dependency("hook", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}).module("hook");
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "weirdutils",
|
||||
.linkage = .dynamic,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "hook", .module = hook_mod },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
b.installArtifact(lib);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
.{
|
||||
.name = .weirdutils,
|
||||
.version = "0.1.0",
|
||||
.fingerprint = 0x54f0a9542562d318,
|
||||
.dependencies = .{
|
||||
.hook = .{
|
||||
.path = "libs/hook",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const hwbp = b.option(bool, "hwbp", "Use hardware breakpoint hooks instead of inline patching") orelse false;
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(bool, "use_hwbp", hwbp);
|
||||
|
||||
const hook_mod = b.addModule("hook", .{
|
||||
.root_source_file = b.path("src/hook.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
hook_mod.addOptions("config", options);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.{
|
||||
.name = .hook,
|
||||
.version = "0.1.0",
|
||||
.fingerprint = 0xa4584355bc807307,
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
//! x86 inline hooking library for Windows DLL injection.
|
||||
//!
|
||||
//! Provides a `Hook` struct for patching function prologues with JMP detours,
|
||||
//! building trampolines to call the original, and chaining with other hooks.
|
||||
//! Also includes memory read/write helpers, rel32 arithmetic, a generic
|
||||
//! `fastcall` caller, and a fastcall-to-cdecl thunk builder.
|
||||
//!
|
||||
//! ## Quick start
|
||||
//!
|
||||
//! ```zig
|
||||
//! const hook = @import("hook.zig");
|
||||
//!
|
||||
//! var my_hook = hook.Hook{};
|
||||
//!
|
||||
//! // One-shot: prepare trampoline + patch in one call.
|
||||
//! // The last arg is a slice of opcode offsets within the prologue that
|
||||
//! // contain E8/E9 (CALL/JMP rel32) instructions needing fixup.
|
||||
//! _ = my_hook.install(target_addr, prologue_size, @intFromPtr(&detour), &.{1});
|
||||
//!
|
||||
//! // Two-phase (when you need the alloc block before patching, e.g. for a thunk):
|
||||
//! _ = my_hook.prepare(target_addr, prologue_size, &.{});
|
||||
//! const thunk_buf = my_hook.mem.? + 32;
|
||||
//! _ = hook.buildFastcallToCdeclThunk(thunk_buf, @intFromPtr(&hookImpl), 1);
|
||||
//! my_hook.activate(@intFromPtr(thunk_buf));
|
||||
//!
|
||||
//! // Call the original from inside the detour:
|
||||
//! const orig = my_hook.getTrampoline(*const fn () callconv(.{ .x86_stdcall = .{} }) void);
|
||||
//! orig();
|
||||
//!
|
||||
//! // Unhook (restore original bytes, free memory):
|
||||
//! my_hook.remove();
|
||||
//! ```
|
||||
|
||||
const std = @import("std");
|
||||
const use_hwbp = @import("config").use_hwbp;
|
||||
|
||||
// =============================================================================
|
||||
// Windows API
|
||||
// =============================================================================
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
pub const PAGE_EXECUTE_READWRITE: u32 = 0x40;
|
||||
pub const MEM_COMMIT: u32 = 0x1000;
|
||||
pub const MEM_RELEASE: u32 = 0x8000;
|
||||
|
||||
extern "kernel32" fn VirtualProtect(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
flNewProtect: u32,
|
||||
lpflOldProtect: *u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn VirtualAlloc(
|
||||
lpAddress: ?*anyopaque,
|
||||
dwSize: usize,
|
||||
flAllocationType: u32,
|
||||
flProtect: u32,
|
||||
) callconv(WINAPI) ?[*]u8;
|
||||
|
||||
extern "kernel32" fn VirtualFree(
|
||||
lpAddress: *anyopaque,
|
||||
dwSize: usize,
|
||||
dwFreeType: u32,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
// =============================================================================
|
||||
// Hardware breakpoint support (DR0-DR3 + Vectored Exception Handler)
|
||||
// =============================================================================
|
||||
|
||||
const CONTEXT_DEBUG_REGISTERS: u32 = 0x00010010;
|
||||
const EXCEPTION_SINGLE_STEP: u32 = 0x80000004;
|
||||
const EXCEPTION_CONTINUE_EXECUTION: i32 = -1;
|
||||
const EXCEPTION_CONTINUE_SEARCH: i32 = 0;
|
||||
|
||||
extern "kernel32" fn GetCurrentThread() callconv(WINAPI) *anyopaque;
|
||||
|
||||
extern "kernel32" fn GetThreadContext(
|
||||
hThread: *anyopaque,
|
||||
lpContext: *CONTEXT,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn SetThreadContext(
|
||||
hThread: *anyopaque,
|
||||
lpContext: *const CONTEXT,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn AddVectoredExceptionHandler(
|
||||
First: u32,
|
||||
Handler: *const fn (*EXCEPTION_POINTERS) callconv(WINAPI) i32,
|
||||
) callconv(WINAPI) ?*anyopaque;
|
||||
|
||||
extern "kernel32" fn RemoveVectoredExceptionHandler(
|
||||
Handle: *anyopaque,
|
||||
) callconv(WINAPI) u32;
|
||||
|
||||
const CONTEXT = extern struct {
|
||||
ContextFlags: u32,
|
||||
Dr0: u32,
|
||||
Dr1: u32,
|
||||
Dr2: u32,
|
||||
Dr3: u32,
|
||||
Dr6: u32,
|
||||
Dr7: u32,
|
||||
FloatSave: [112]u8,
|
||||
SegGs: u32,
|
||||
SegFs: u32,
|
||||
SegEs: u32,
|
||||
SegDs: u32,
|
||||
Edi: u32,
|
||||
Esi: u32,
|
||||
Ebx: u32,
|
||||
Edx: u32,
|
||||
Ecx: u32,
|
||||
Eax: u32,
|
||||
Ebp: u32,
|
||||
Eip: u32,
|
||||
SegCs: u32,
|
||||
EFlags: u32,
|
||||
Esp: u32,
|
||||
SegSs: u32,
|
||||
ExtendedRegisters: [512]u8,
|
||||
};
|
||||
|
||||
const EXCEPTION_RECORD = extern struct {
|
||||
ExceptionCode: u32,
|
||||
ExceptionFlags: u32,
|
||||
ExceptionRecord: ?*EXCEPTION_RECORD,
|
||||
ExceptionAddress: ?*anyopaque,
|
||||
NumberParameters: u32,
|
||||
ExceptionInformation: [15]usize,
|
||||
};
|
||||
|
||||
const EXCEPTION_POINTERS = extern struct {
|
||||
ExceptionRecord: *EXCEPTION_RECORD,
|
||||
ContextRecord: *CONTEXT,
|
||||
};
|
||||
|
||||
var hwbp_slots: [4]?*Hook = .{ null, null, null, null };
|
||||
var hwbp_detours: [4]usize = .{ 0, 0, 0, 0 };
|
||||
var veh_handle: ?*anyopaque = null;
|
||||
|
||||
fn vehHandler(info: *EXCEPTION_POINTERS) callconv(WINAPI) i32 {
|
||||
if (info.ExceptionRecord.ExceptionCode != EXCEPTION_SINGLE_STEP)
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
|
||||
const eip = info.ContextRecord.Eip;
|
||||
for (0..4) |i| {
|
||||
if (hwbp_slots[i]) |h| {
|
||||
if (h.target == eip) {
|
||||
info.ContextRecord.Eip = @intCast(hwbp_detours[i]);
|
||||
return EXCEPTION_CONTINUE_EXECUTION;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Memory helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Read a value of type `T` from an arbitrary memory address (unaligned).
|
||||
pub fn readMem(comptime T: type, addr: usize) T {
|
||||
return @as(*align(1) const T, @ptrFromInt(addr)).*;
|
||||
}
|
||||
|
||||
/// Write raw bytes to an arbitrary memory address. No protection change —
|
||||
/// caller must ensure the page is writable (or use `writeProtected`).
|
||||
pub fn writeMem(addr: usize, bytes: []const u8) void {
|
||||
const dest: [*]u8 = @ptrFromInt(addr);
|
||||
for (bytes, 0..) |b, i| {
|
||||
dest[i] = b;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write bytes to a potentially read-only/executable page. Temporarily sets
|
||||
/// PAGE_EXECUTE_READWRITE, writes, then restores the original protection.
|
||||
pub fn writeProtected(addr: usize, bytes: []const u8) void {
|
||||
var old: u32 = 0;
|
||||
_ = VirtualProtect(@ptrFromInt(addr), bytes.len, PAGE_EXECUTE_READWRITE, &old);
|
||||
writeMem(addr, bytes);
|
||||
_ = VirtualProtect(@ptrFromInt(addr), bytes.len, old, &old);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Rel32 helpers
|
||||
// =============================================================================
|
||||
|
||||
/// Resolve the absolute target of an E8 (CALL) or E9 (JMP) at `addr`.
|
||||
/// Reads the signed rel32 operand at addr+1 and computes addr+5+offset.
|
||||
pub fn rel32Target(addr: usize) usize {
|
||||
const offset: u32 = @bitCast(@as(*align(1) const i32, @ptrFromInt(addr + 1)).*);
|
||||
return (addr + 5) +% offset;
|
||||
}
|
||||
|
||||
/// Write a rel32 displacement into dest[0..4] such that a JMP/CALL
|
||||
/// from address `from` reaches `to`. Displacement = to - (from + 4).
|
||||
pub fn writeRel32(dest: [*]u8, from: usize, to: usize) void {
|
||||
std.mem.writeInt(u32, dest[0..4], to -% (from + 4), .little);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Calling convention helper
|
||||
// =============================================================================
|
||||
|
||||
/// Call a __fastcall function at `addr` with ECX and EDX arguments.
|
||||
/// Dispatches on return type: void, f64 (x87 ST0), or integer/pointer (EAX).
|
||||
pub fn fastcall(comptime R: type, addr: usize, ecx: anytype, edx: anytype) R {
|
||||
if (R == f64) {
|
||||
return asm volatile ("call *%[func]"
|
||||
: [ret] "={st}" (-> f64),
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
} else if (R == void) {
|
||||
asm volatile ("call *%[func]"
|
||||
:
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
} else {
|
||||
return asm volatile ("call *%[func]"
|
||||
: [ret] "={eax}" (-> R),
|
||||
: [_] "{ecx}" (ecx), [_] "{edx}" (edx), [func] "r" (addr),
|
||||
: .{ .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook struct
|
||||
// =============================================================================
|
||||
|
||||
const ALLOC_SIZE: usize = 64;
|
||||
const TRAMPOLINE_RESERVE: usize = 32;
|
||||
const MAX_PROLOGUE: usize = 16;
|
||||
|
||||
pub const Hook = struct {
|
||||
mem: ?[*]u8 = null,
|
||||
trampoline: usize = 0,
|
||||
target: usize = 0,
|
||||
prologue_size: usize = 0,
|
||||
saved_bytes: [MAX_PROLOGUE]u8 = undefined,
|
||||
|
||||
/// Allocate executable memory, save current bytes at target, and build the
|
||||
/// trampoline. Does NOT patch the target yet — call activate() after.
|
||||
/// `rel32_fixups` is a slice of opcode offsets within the prologue that
|
||||
/// contain E8/E9 instructions needing rel32 adjustment.
|
||||
pub fn prepare(
|
||||
self: *Hook,
|
||||
target: usize,
|
||||
prologue_size: usize,
|
||||
rel32_fixups: []const usize,
|
||||
) bool {
|
||||
if (self.mem != null) return true;
|
||||
|
||||
const mem = VirtualAlloc(null, ALLOC_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE) orelse return false;
|
||||
self.mem = mem;
|
||||
self.target = target;
|
||||
self.prologue_size = prologue_size;
|
||||
|
||||
// Save current bytes for remove()
|
||||
const src: [*]const u8 = @ptrFromInt(target);
|
||||
@memcpy(self.saved_bytes[0..prologue_size], src[0..prologue_size]);
|
||||
|
||||
// Build trampoline
|
||||
self.trampoline = @intFromPtr(mem);
|
||||
|
||||
if (src[0] == 0xE9) {
|
||||
// Another DLL already hooked — resolve their JMP and chain through it
|
||||
const other_detour = rel32Target(target);
|
||||
mem[0] = 0xE9;
|
||||
writeRel32(mem + 1, self.trampoline + 1, other_detour);
|
||||
} else {
|
||||
// Original prologue — copy bytes, fix up any relative instructions, JMP back
|
||||
@memcpy(mem[0..prologue_size], src[0..prologue_size]);
|
||||
|
||||
for (rel32_fixups) |opcode_offset| {
|
||||
const abs_target = rel32Target(target + opcode_offset);
|
||||
const tramp_operand = self.trampoline + opcode_offset + 1;
|
||||
writeRel32(mem + opcode_offset + 1, tramp_operand, abs_target);
|
||||
}
|
||||
|
||||
mem[prologue_size] = 0xE9;
|
||||
writeRel32(
|
||||
mem + prologue_size + 1,
|
||||
self.trampoline + prologue_size + 1,
|
||||
target + prologue_size,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Write the E9 JMP patch (inline mode) or set a hardware breakpoint
|
||||
/// (HWBP mode) to redirect target → detour_addr.
|
||||
pub fn activate(self: *Hook, detour_addr: usize) void {
|
||||
if (use_hwbp) {
|
||||
// Register VEH on first use
|
||||
if (veh_handle == null) {
|
||||
veh_handle = AddVectoredExceptionHandler(1, &vehHandler);
|
||||
}
|
||||
// Find free DR slot
|
||||
const slot: u2 = for (0..4) |i| {
|
||||
if (hwbp_slots[i] == null) break @as(u2, @intCast(i));
|
||||
} else return; // all 4 slots occupied
|
||||
|
||||
hwbp_slots[slot] = self;
|
||||
hwbp_detours[slot] = detour_addr;
|
||||
|
||||
const thread = GetCurrentThread();
|
||||
var ctx: CONTEXT = std.mem.zeroes(CONTEXT);
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
_ = GetThreadContext(thread, &ctx);
|
||||
|
||||
// Set DRn to target address
|
||||
switch (slot) {
|
||||
0 => { ctx.Dr0 = @intCast(self.target); },
|
||||
1 => { ctx.Dr1 = @intCast(self.target); },
|
||||
2 => { ctx.Dr2 = @intCast(self.target); },
|
||||
3 => { ctx.Dr3 = @intCast(self.target); },
|
||||
}
|
||||
|
||||
// Enable local execute breakpoint in DR7
|
||||
const s: u5 = slot;
|
||||
ctx.Dr7 |= @as(u32, 1) << (s * 2);
|
||||
ctx.Dr7 &= ~(@as(u32, 0xF) << (16 + s * 4));
|
||||
|
||||
_ = SetThreadContext(thread, &ctx);
|
||||
} else {
|
||||
var patch: [MAX_PROLOGUE]u8 = .{0x90} ** MAX_PROLOGUE;
|
||||
patch[0] = 0xE9;
|
||||
writeRel32(patch[1..5], self.target + 1, detour_addr);
|
||||
writeProtected(self.target, patch[0..self.prologue_size]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience: prepare + activate in one call.
|
||||
pub fn install(
|
||||
self: *Hook,
|
||||
target: usize,
|
||||
prologue_size: usize,
|
||||
detour_addr: usize,
|
||||
rel32_fixups: []const usize,
|
||||
) bool {
|
||||
if (!self.prepare(target, prologue_size, rel32_fixups)) return false;
|
||||
self.activate(detour_addr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Restore original bytes (inline) or clear the DR slot (HWBP), then free
|
||||
/// the trampoline memory.
|
||||
pub fn remove(self: *Hook) void {
|
||||
if (self.mem == null) return;
|
||||
|
||||
if (use_hwbp) {
|
||||
for (0..4) |i| {
|
||||
if (hwbp_slots[i]) |h| {
|
||||
if (h == self) {
|
||||
const thread = GetCurrentThread();
|
||||
var ctx: CONTEXT = std.mem.zeroes(CONTEXT);
|
||||
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
|
||||
_ = GetThreadContext(thread, &ctx);
|
||||
|
||||
switch (@as(u2, @intCast(i))) {
|
||||
0 => { ctx.Dr0 = 0; },
|
||||
1 => { ctx.Dr1 = 0; },
|
||||
2 => { ctx.Dr2 = 0; },
|
||||
3 => { ctx.Dr3 = 0; },
|
||||
}
|
||||
const s: u5 = @intCast(i);
|
||||
ctx.Dr7 &= ~(@as(u32, 1) << (s * 2));
|
||||
ctx.Dr7 &= ~(@as(u32, 0xF) << (16 + s * 4));
|
||||
|
||||
_ = SetThreadContext(thread, &ctx);
|
||||
|
||||
hwbp_slots[i] = null;
|
||||
hwbp_detours[i] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
writeProtected(self.target, self.saved_bytes[0..self.prologue_size]);
|
||||
}
|
||||
|
||||
_ = VirtualFree(@ptrFromInt(@intFromPtr(self.mem.?)), 0, MEM_RELEASE);
|
||||
self.mem = null;
|
||||
}
|
||||
|
||||
/// Cast trampoline address to a typed function pointer for calling the original.
|
||||
pub fn getTrampoline(self: *const Hook, comptime T: type) T {
|
||||
return @ptrFromInt(self.trampoline);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Thunk builder: fastcall(ECX, EDX, stack...) → cdecl(stack, stack, stack...)
|
||||
// =============================================================================
|
||||
|
||||
/// Build a fastcall-to-cdecl bridge thunk in `buf`.
|
||||
/// `cdecl_fn` is the address of the cdecl target function.
|
||||
/// `stack_arg_count` is the number of extra stack arguments beyond ECX/EDX.
|
||||
/// Returns the total thunk size in bytes.
|
||||
///
|
||||
/// Generated code:
|
||||
/// push dword [esp + 4*N] ; for each stack arg, right-to-left
|
||||
/// ...
|
||||
/// push edx ; arg 2
|
||||
/// push ecx ; arg 1
|
||||
/// mov eax, <cdecl_fn>
|
||||
/// call eax
|
||||
/// add esp, (2 + stack_arg_count) * 4
|
||||
/// ret (stack_arg_count * 4)
|
||||
pub fn buildFastcallToCdeclThunk(buf: [*]u8, cdecl_fn: usize, stack_arg_count: u8) usize {
|
||||
var pos: usize = 0;
|
||||
|
||||
// Push stack args right-to-left. At entry, [esp] = return addr,
|
||||
// [esp+4] = first stack arg, [esp+8] = second, etc.
|
||||
// But each push shifts esp, so we always read from [esp + 4 * stack_arg_count]
|
||||
// (the offset stays constant because we push the same number of times as the depth grows).
|
||||
var i: u8 = stack_arg_count;
|
||||
while (i > 0) : (i -= 1) {
|
||||
// push dword ptr [esp + 4 * stack_arg_count]
|
||||
buf[pos] = 0xFF;
|
||||
buf[pos + 1] = 0x74;
|
||||
buf[pos + 2] = 0x24;
|
||||
buf[pos + 3] = stack_arg_count * 4;
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
// push edx (arg 2)
|
||||
buf[pos] = 0x52;
|
||||
pos += 1;
|
||||
|
||||
// push ecx (arg 1)
|
||||
buf[pos] = 0x51;
|
||||
pos += 1;
|
||||
|
||||
// mov eax, <cdecl_fn>
|
||||
buf[pos] = 0xB8;
|
||||
std.mem.writeInt(u32, buf[pos + 1 ..][0..4], @intCast(cdecl_fn), .little);
|
||||
pos += 5;
|
||||
|
||||
// call eax
|
||||
buf[pos] = 0xFF;
|
||||
buf[pos + 1] = 0xD0;
|
||||
pos += 2;
|
||||
|
||||
// add esp, (2 + stack_arg_count) * 4 (cdecl caller cleanup)
|
||||
const cleanup: u8 = (2 + stack_arg_count) * 4;
|
||||
buf[pos] = 0x83;
|
||||
buf[pos + 1] = 0xC4;
|
||||
buf[pos + 2] = cleanup;
|
||||
pos += 3;
|
||||
|
||||
// ret (stack_arg_count * 4) (fastcall callee cleans stack args)
|
||||
if (stack_arg_count == 0) {
|
||||
buf[pos] = 0xC3; // ret
|
||||
pos += 1;
|
||||
} else {
|
||||
buf[pos] = 0xC2; // ret imm16
|
||||
std.mem.writeInt(u16, buf[pos + 1 ..][0..2], @as(u16, stack_arg_count) * 4, .little);
|
||||
pos += 3;
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
-- WeirdUtils addon (embedded in DLL, loaded from memory)
|
||||
|
||||
WEIRDUTILS_VERSION = 1
|
||||
|
||||
local frame = CreateFrame("Frame")
|
||||
frame:RegisterEvent("ADDON_LOADED")
|
||||
frame:RegisterEvent("PLAYER_LOGIN")
|
||||
|
||||
frame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_LOGIN" then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WeirdUtils|r v" .. WEIRDUTILS_VERSION .. " loaded")
|
||||
end
|
||||
end)
|
||||
|
||||
SLASH_WEIRDUTILS1 = "/weirdutils"
|
||||
SLASH_WEIRDUTILS2 = "/wu"
|
||||
SlashCmdList["WEIRDUTILS"] = function(msg)
|
||||
if msg == "version" then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("WeirdUtils v" .. WEIRDUTILS_VERSION)
|
||||
elseif msg == "test" then
|
||||
local result = WeirdUtilsTest()
|
||||
DEFAULT_CHAT_FRAME:AddMessage("C function returned: " .. tostring(result))
|
||||
elseif msg == "ss" or msg == "screenshot" then
|
||||
local on, level = WeirdUtilsScreenshot()
|
||||
DEFAULT_CHAT_FRAME:AddMessage("Screenshots: " .. (on and "ON" or "OFF") .. " (quality " .. level .. ")")
|
||||
elseif string.find(msg, "^ss ") or string.find(msg, "^screenshot ") then
|
||||
local sub = string.match(msg, "^%S+ (.+)")
|
||||
if sub == "on" or sub == "enable" then
|
||||
WeirdUtilsScreenshot("enable")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("Screenshots enabled")
|
||||
elseif sub == "off" or sub == "disable" then
|
||||
WeirdUtilsScreenshot("disable")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("Screenshots disabled")
|
||||
elseif tonumber(sub) then
|
||||
WeirdUtilsScreenshot("quality", tonumber(sub))
|
||||
DEFAULT_CHAT_FRAME:AddMessage("Screenshot quality: " .. sub)
|
||||
end
|
||||
else
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00WeirdUtils|r commands:")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" /wu version - Show version")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" /wu test - Test C function call")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" /wu ss - Screenshot status")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" /wu ss on|off - Enable/disable PNG screenshots")
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" /wu ss 0-9 - Set compression level")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,5 @@
|
||||
## Interface: 11200
|
||||
## Title: WeirdUtils
|
||||
## Notes: Utility functions provided by weirdutils DLL
|
||||
## Version: 1.0
|
||||
WeirdUtils.lua
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const screenshot = @import("screenshot.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
const sc: std.builtin.CallingConvention = .{ .x86_stdcall = .{} };
|
||||
|
||||
// =============================================================================
|
||||
// Lua Protection Bypass
|
||||
// =============================================================================
|
||||
|
||||
var protection_hook: hook.Hook = .{};
|
||||
|
||||
/// Empty detour — replaces the Lua callback address validator at 0x42a320.
|
||||
/// Prologue: 55 8B EC 83 EC 40 = 6 bytes, no fixups
|
||||
fn luaProtectionDetour() callconv(.c) void {}
|
||||
|
||||
// =============================================================================
|
||||
// Lua C API wrappers (WoW 1.12.1 — all __fastcall, L in ECX)
|
||||
// =============================================================================
|
||||
|
||||
pub const lua = struct {
|
||||
pub const State = *anyopaque;
|
||||
|
||||
pub fn getContext() State {
|
||||
const f: *const fn () callconv(fc) State = @ptrFromInt(0x7040D0);
|
||||
return f();
|
||||
}
|
||||
|
||||
pub fn gettop(L: State) i32 {
|
||||
const f: *const fn (State) callconv(fc) i32 = @ptrFromInt(0x6F3070);
|
||||
return f(L);
|
||||
}
|
||||
|
||||
pub fn settop(L: State, index: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F3080);
|
||||
f(L, index);
|
||||
}
|
||||
|
||||
pub fn pop(L: State, n: i32) void {
|
||||
settop(L, -n - 1);
|
||||
}
|
||||
|
||||
pub fn pushvalue(L: State, index: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F3350);
|
||||
f(L, index);
|
||||
}
|
||||
|
||||
pub fn remove(L: State, index: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F30D0);
|
||||
f(L, index);
|
||||
}
|
||||
|
||||
pub fn insert(L: State, index: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F31A0);
|
||||
f(L, index);
|
||||
}
|
||||
|
||||
pub fn typeOf(L: State, index: i32) i32 {
|
||||
const f: *const fn (State, i32) callconv(fc) i32 = @ptrFromInt(0x6F3400);
|
||||
return f(L, index);
|
||||
}
|
||||
|
||||
pub fn typeName(L: State, tp: i32) [*:0]const u8 {
|
||||
const f: *const fn (State, i32) callconv(fc) [*:0]const u8 = @ptrFromInt(0x6F3480);
|
||||
return f(L, tp);
|
||||
}
|
||||
|
||||
pub fn isnumber(L: State, index: i32) bool {
|
||||
const f: *const fn (State, i32) callconv(fc) u32 = @ptrFromInt(0x6F34D0);
|
||||
return f(L, index) != 0;
|
||||
}
|
||||
|
||||
pub fn isstring(L: State, index: i32) bool {
|
||||
const f: *const fn (State, i32) callconv(fc) u32 = @ptrFromInt(0x6F3510);
|
||||
return f(L, index) != 0;
|
||||
}
|
||||
|
||||
pub fn pushnil(L: State) void {
|
||||
const f: *const fn (State) callconv(fc) void = @ptrFromInt(0x6F37F0);
|
||||
f(L);
|
||||
}
|
||||
|
||||
pub fn pushnumber(L: State, n: f64) void {
|
||||
const f: *const fn (State, f64) callconv(fc) void = @ptrFromInt(0x6F3810);
|
||||
f(L, n);
|
||||
}
|
||||
|
||||
pub fn pushstring(L: State, s: [*:0]const u8) void {
|
||||
const f: *const fn (State, [*:0]const u8) callconv(fc) void = @ptrFromInt(0x6F3890);
|
||||
f(L, s);
|
||||
}
|
||||
|
||||
pub fn pushboolean(L: State, b: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F39F0);
|
||||
f(L, b);
|
||||
}
|
||||
|
||||
pub fn pushcclosure(L: State, func: usize, n: i32) void {
|
||||
const f: *const fn (State, usize, i32) callconv(fc) void = @ptrFromInt(0x6F3B80);
|
||||
f(L, func, n);
|
||||
}
|
||||
|
||||
pub fn tonumber(L: State, index: i32) f64 {
|
||||
const f: *const fn (State, i32) callconv(fc) f64 = @ptrFromInt(0x6F3620);
|
||||
return f(L, index);
|
||||
}
|
||||
|
||||
pub fn tostring(L: State, index: i32) ?[*:0]const u8 {
|
||||
const f: *const fn (State, i32) callconv(fc) ?[*:0]const u8 = @ptrFromInt(0x6F3690);
|
||||
return f(L, index);
|
||||
}
|
||||
|
||||
pub fn toboolean(L: State, index: i32) i32 {
|
||||
const f: *const fn (State, i32) callconv(fc) i32 = @ptrFromInt(0x6F3660);
|
||||
return f(L, index);
|
||||
}
|
||||
|
||||
pub fn newtable(L: State) void {
|
||||
const f: *const fn (State) callconv(fc) void = @ptrFromInt(0x6F3C90);
|
||||
f(L);
|
||||
}
|
||||
|
||||
pub fn settable(L: State, index: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F3E20);
|
||||
f(L, index);
|
||||
}
|
||||
|
||||
pub fn gettable(L: State, index: i32) void {
|
||||
const f: *const fn (State, i32) callconv(fc) void = @ptrFromInt(0x6F3A40);
|
||||
f(L, index);
|
||||
}
|
||||
|
||||
pub fn next(L: State, index: i32) i32 {
|
||||
const f: *const fn (State, i32) callconv(fc) i32 = @ptrFromInt(0x6F4450);
|
||||
return f(L, index);
|
||||
}
|
||||
|
||||
pub fn pcall(L: State, nargs: i32, nresults: i32, errfunc: i32) i32 {
|
||||
const f: *const fn (State, i32, i32, i32) callconv(fc) i32 = @ptrFromInt(0x6F41A0);
|
||||
return f(L, nargs, nresults, errfunc);
|
||||
}
|
||||
|
||||
pub fn luaError(L: State, msg: [*:0]const u8) void {
|
||||
// lua_error is __cdecl(L, msg) at 0x6F4940
|
||||
asm volatile (
|
||||
\\push %[msg]
|
||||
\\push %[L]
|
||||
\\call *%[func]
|
||||
\\add $8, %%esp
|
||||
:
|
||||
: [L] "r" (@intFromPtr(L)),
|
||||
[msg] "r" (@intFromPtr(msg)),
|
||||
[func] "r" (@as(u32, 0x6F4940)),
|
||||
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
pub const LuaReg = extern struct {
|
||||
name: ?[*:0]const u8,
|
||||
func: usize,
|
||||
};
|
||||
|
||||
pub fn openlib(L: State, libname: ?[*:0]const u8, funcs: [*]const LuaReg, nup: i32) void {
|
||||
const f: *const fn (State, ?[*:0]const u8, [*]const LuaReg, i32) callconv(fc) void = @ptrFromInt(0x6F4DC0);
|
||||
f(L, libname, funcs, nup);
|
||||
}
|
||||
|
||||
pub fn checknumber(L: State, index: i32) f64 {
|
||||
const f: *const fn (State, i32) callconv(fc) f64 = @ptrFromInt(0x6F4C80);
|
||||
return f(L, index);
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Game function wrappers
|
||||
// =============================================================================
|
||||
|
||||
/// FrameScript::Register(name_ECX, func_EDX) at 0x704120
|
||||
fn registerFunction(name: [*:0]const u8, func_addr: usize) void {
|
||||
hook.fastcall(void, 0x704120, @intFromPtr(name), func_addr);
|
||||
}
|
||||
|
||||
/// M2_AllocateModelBuffer(size, source_file, line, flags) at 0x6462E0
|
||||
/// __stdcall, 4 params, ret 0x10 — allocates from WoW's internal memory pool.
|
||||
/// Returned buffer can be freed by game code via FreeFileResourceMemory.
|
||||
fn allocateGameBuffer(size: u32) ?[*]u8 {
|
||||
return asm volatile (
|
||||
\\push $0
|
||||
\\push $0
|
||||
\\push %[src]
|
||||
\\push %[size]
|
||||
\\call *%[func]
|
||||
: [ret] "={eax}" (-> ?[*]u8),
|
||||
: [size] "r" (size),
|
||||
[src] "r" (@intFromPtr(@as([*:0]const u8, "weirdutils"))),
|
||||
[func] "r" (@as(u32, 0x6462E0)),
|
||||
: .{ .ecx = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Custom C functions (callable from Lua)
|
||||
// =============================================================================
|
||||
//
|
||||
// FIX: Cannot use the lua.* wrappers (e.g. lua.pushstring) from inside Lua
|
||||
// C callbacks. The wrappers call through typed callconv(fc) function pointers,
|
||||
// but Zig 0.15's x86 codegen is broken for callconv(fc) — it generates
|
||||
// `ret 0x4` instead of plain `ret` for fastcall calls with ≤2 register params,
|
||||
// corrupting the stack. Work around by using hook.fastcall / raw inline asm
|
||||
// which bypasses Zig's calling-convention codegen entirely.
|
||||
|
||||
fn weirdUtilsTest(L: lua.State) callconv(.c) u32 {
|
||||
hook.fastcall(void, 0x6F3890, @intFromPtr(L), @intFromPtr(@as([*:0]const u8, "WeirdUtils is working!")));
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn weirdUtilsVersion(L: lua.State) callconv(.c) u32 {
|
||||
// lua_pushnumber is __fastcall(L_ECX, f64_stack) — f64 skips EDX and goes
|
||||
// on the stack. Callee cleans with ret 8.
|
||||
asm volatile (
|
||||
\\sub $8, %%esp
|
||||
\\fld1
|
||||
\\fstpl (%%esp)
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (@intFromPtr(L)),
|
||||
[func] "r" (@as(u32, 0x6F3810)),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn registerLuaFunctions() void {
|
||||
registerFunction("WeirdUtilsTest", @intFromPtr(&weirdUtilsTest));
|
||||
registerFunction("WeirdUtilsVersion", @intFromPtr(&weirdUtilsVersion));
|
||||
registerFunction("WeirdUtilsScreenshot", @intFromPtr(&screenshot.screenshotCommand));
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Embedded addon files
|
||||
// =============================================================================
|
||||
|
||||
const addon_prefix = "Interface\\AddOns\\WeirdUtils\\";
|
||||
|
||||
const FileEntry = struct {
|
||||
name: []const u8,
|
||||
data: []const u8,
|
||||
};
|
||||
|
||||
const embedded_files = [_]FileEntry{
|
||||
.{ .name = "WeirdUtils.toc", .data = @embedFile("addon/WeirdUtils.toc") },
|
||||
.{ .name = "WeirdUtils.lua", .data = @embedFile("addon/WeirdUtils.lua") },
|
||||
};
|
||||
|
||||
fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry {
|
||||
const path_span = std.mem.span(path);
|
||||
if (path_span.len <= addon_prefix.len) return null;
|
||||
// Case-insensitive prefix check — WoW paths use mixed case
|
||||
for (path_span[0..addon_prefix.len], addon_prefix) |a, b| {
|
||||
const la = if (a >= 'A' and a <= 'Z') a + 32 else a;
|
||||
const lb = if (b >= 'A' and b <= 'Z') b + 32 else b;
|
||||
if (la != lb) return null;
|
||||
}
|
||||
const relative = path_span[addon_prefix.len..];
|
||||
for (&embedded_files) |*entry| {
|
||||
if (relative.len != entry.name.len) continue;
|
||||
var match = true;
|
||||
for (relative, entry.name) |a, b| {
|
||||
const la = if (a >= 'A' and a <= 'Z') a + 32 else a;
|
||||
const lb = if (b >= 'A' and b <= 'Z') b + 32 else b;
|
||||
if (la != lb) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: LoadFileWithTextureResourceFallback (0x648620)
|
||||
// __stdcall(unk, path, buf_out, size_out, extra_alloc, flags, async) → ret 0x1C
|
||||
// Prologue: 55 8B EC 8B 4D 1C = 6 bytes, no fixups
|
||||
//
|
||||
// The central file I/O function — all .toc, .lua, .xml, and texture file reads
|
||||
// go through here. We intercept reads for our addon prefix and serve from
|
||||
// DLL-embedded memory, allocated with the game's own allocator so the game
|
||||
// can free the buffer normally.
|
||||
// =============================================================================
|
||||
|
||||
var file_hook: hook.Hook = .{};
|
||||
|
||||
fn loadFileDetour(
|
||||
unk: u32,
|
||||
path: [*:0]const u8,
|
||||
buf_out: *?[*]u8,
|
||||
size_out: ?*u32,
|
||||
extra_alloc: u32,
|
||||
flags: u32,
|
||||
async_ptr: u32,
|
||||
) callconv(sc) u32 {
|
||||
if (findEmbeddedFile(path)) |entry| {
|
||||
const data_len: u32 = @intCast(entry.data.len);
|
||||
const total = data_len + extra_alloc;
|
||||
const buf = allocateGameBuffer(total) orelse return 0;
|
||||
|
||||
@memcpy(buf[0..entry.data.len], entry.data);
|
||||
|
||||
// Zero extra bytes (null terminator for text files when extra_alloc=1)
|
||||
if (extra_alloc > 0) {
|
||||
@memset(buf[entry.data.len..][0..extra_alloc], 0);
|
||||
}
|
||||
|
||||
buf_out.* = buf;
|
||||
if (size_out) |s| s.* = data_len;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Not our file — forward to original
|
||||
const orig = file_hook.getTrampoline(
|
||||
*const fn (u32, [*:0]const u8, *?[*]u8, ?*u32, u32, u32, u32) callconv(sc) u32,
|
||||
);
|
||||
return orig(unk, path, buf_out, size_out, extra_alloc, flags, async_ptr);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: LoadScriptFunctions (0x490250)
|
||||
// Prologue: 56 E8 FA 60 27 00 = 6 bytes, fixup at offset 1
|
||||
// =============================================================================
|
||||
|
||||
var lsf_hook: hook.Hook = .{};
|
||||
|
||||
fn loadScriptFunctionsDetour() callconv(sc) void {
|
||||
const orig = lsf_hook.getTrampoline(*const fn () callconv(sc) void);
|
||||
orig();
|
||||
registerLuaFunctions();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: LoadAddonsRecursively (0x51F600)
|
||||
// __fastcall(error_handler_ECX) — prologue: 53 8B 1D ... = 7 bytes, no fixups
|
||||
// After all real addons load, we call loadFileListWithIncludes with our .toc
|
||||
// path, triggering the full addon loading pipeline (toc parse → lua exec →
|
||||
// xml parse) with file reads served from embedded memory via the file hook.
|
||||
// =============================================================================
|
||||
|
||||
var load_addons_hook: hook.Hook = .{};
|
||||
|
||||
fn loadAddonsDetour(error_handler: u32, _edx: u32) callconv(.c) void {
|
||||
_ = _edx;
|
||||
|
||||
// Call original — loads all player addons
|
||||
callOrigLoadAddons(error_handler);
|
||||
|
||||
// Trigger our addon through the real loading pipeline.
|
||||
// loadFileListWithIncludes will read our .toc via LoadFileWithTextureResourceFallback
|
||||
// (intercepted by file_hook), parse it, and call processIncludeFile for each entry,
|
||||
// which loads .lua files through the same hooked path.
|
||||
//
|
||||
// FIX: loadFileListWithIncludes unconditionally calls MD5_Update on the md5ctx
|
||||
// param (EDX). Passing NULL here caused the original crash-on-load — an access
|
||||
// violation inside MD5_Update. Allocate a zeroed 88-byte context on the stack.
|
||||
var md5ctx = std.mem.zeroes([88]u8);
|
||||
callLoadFileListWithIncludes(
|
||||
"Interface\\AddOns\\WeirdUtils\\WeirdUtils.toc",
|
||||
&md5ctx,
|
||||
error_handler,
|
||||
);
|
||||
}
|
||||
|
||||
fn callOrigLoadAddons(error_handler: u32) void {
|
||||
asm volatile (
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (error_handler),
|
||||
[func] "r" (load_addons_hook.trampoline),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
/// loadFileListWithIncludes(path_ECX, md5ctx_EDX, error_handler_stack)
|
||||
/// __fastcall at 0x6EDB90, ret 4 (1 stack param)
|
||||
fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_handler: u32) void {
|
||||
// __fastcall: ECX=path, EDX=md5ctx, stack: error_handler
|
||||
// Callee cleans the 1 stack arg (ret 4)
|
||||
asm volatile (
|
||||
\\push %[eh]
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (@intFromPtr(toc_path)),
|
||||
[_] "{edx}" (@intFromPtr(md5ctx)),
|
||||
[eh] "r" (error_handler),
|
||||
[func] "r" (@as(u32, 0x6EDB90)),
|
||||
: .{ .eax = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: CGGameUI_Shutdown (0x490BD0)
|
||||
// Prologue: 56 E8 7A 83 FC FF = 6 bytes, fixup at offset 1
|
||||
// =============================================================================
|
||||
|
||||
var shutdown_hook: hook.Hook = .{};
|
||||
|
||||
fn shutdownDetour() callconv(sc) void {
|
||||
const orig = shutdown_hook.getTrampoline(*const fn () callconv(sc) void);
|
||||
orig();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Init / Cleanup
|
||||
// =============================================================================
|
||||
|
||||
fn install() void {
|
||||
// 1. Lua protection bypass — empty stub replaces address validator
|
||||
_ = protection_hook.install(0x42a320, 6, @intFromPtr(&luaProtectionDetour), &.{});
|
||||
|
||||
// 2. File I/O hook — serve embedded addon files from memory
|
||||
// Must be installed before LoadAddonsRecursively hook fires
|
||||
_ = file_hook.install(0x648620, 6, @intFromPtr(&loadFileDetour), &.{});
|
||||
|
||||
// 3. LoadScriptFunctions — register C functions after built-in commands
|
||||
_ = lsf_hook.install(0x490250, 6, @intFromPtr(&loadScriptFunctionsDetour), &.{1});
|
||||
|
||||
// 4. LoadAddonsRecursively — trigger our addon load after real addons
|
||||
// Uses thunk: __fastcall(ECX) → cdecl(ecx_val, edx_val)
|
||||
if (load_addons_hook.prepare(0x51F600, 7, &.{})) {
|
||||
const thunk = load_addons_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&loadAddonsDetour), 0);
|
||||
load_addons_hook.activate(@intFromPtr(thunk));
|
||||
}
|
||||
|
||||
// 5. Screenshot hook — async PNG capture replacing TGA write
|
||||
screenshot.installHook();
|
||||
|
||||
// 6. CGGameUI_Shutdown — cleanup
|
||||
_ = shutdown_hook.install(0x490BD0, 6, @intFromPtr(&shutdownDetour), &.{1});
|
||||
}
|
||||
|
||||
fn uninstall() void {
|
||||
shutdown_hook.remove();
|
||||
screenshot.removeHook();
|
||||
load_addons_hook.remove();
|
||||
lsf_hook.remove();
|
||||
file_hook.remove();
|
||||
protection_hook.remove();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DLL entry point
|
||||
// =============================================================================
|
||||
|
||||
pub export fn DllMain(
|
||||
_: ?*anyopaque,
|
||||
reason: u32,
|
||||
_: ?*anyopaque,
|
||||
) callconv(WINAPI) i32 {
|
||||
switch (reason) {
|
||||
1 => install(), // DLL_PROCESS_ATTACH
|
||||
0 => uninstall(), // DLL_PROCESS_DETACH
|
||||
else => {},
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
//! Minimal PNG encoder with deflate compression.
|
||||
//! No large struct literals or lookup tables — entire module adds ~2KB to .rdata.
|
||||
//! Supports store blocks (level 0) and fixed-Huffman encoding (levels 1-9).
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
// =============================================================================
|
||||
// Public interface
|
||||
// =============================================================================
|
||||
|
||||
pub const Level = enum(u4) {
|
||||
store = 0,
|
||||
level_1 = 1,
|
||||
level_2 = 2,
|
||||
level_3 = 3,
|
||||
level_4 = 4,
|
||||
level_5 = 5,
|
||||
level_6 = 6,
|
||||
level_7 = 7,
|
||||
level_8 = 8,
|
||||
level_9 = 9,
|
||||
};
|
||||
|
||||
pub fn mapLevel(user_level: i32) Level {
|
||||
return switch (user_level) {
|
||||
0 => .store,
|
||||
1...9 => @enumFromInt(@as(u4, @intCast(std.math.clamp(user_level, 0, 9)))),
|
||||
else => .level_6,
|
||||
};
|
||||
}
|
||||
|
||||
/// Write an RGB24 PNG to a raw file handle (HANDLE from CreateFileA).
|
||||
/// `write_fn` is called with (ctx, data) to emit bytes.
|
||||
pub fn encode(
|
||||
ctx: anytype,
|
||||
write_fn: fn (@TypeOf(ctx), []const u8) void,
|
||||
pixels: [*]const u8,
|
||||
width: u16,
|
||||
height: u16,
|
||||
level: Level,
|
||||
) void {
|
||||
var s = Stream(@TypeOf(ctx)){ .ctx = ctx, .write_fn = write_fn };
|
||||
|
||||
const w: u32 = width;
|
||||
const h: u32 = height;
|
||||
|
||||
// PNG signature
|
||||
s.writeRaw("\x89PNG\r\n\x1a\n");
|
||||
|
||||
// IHDR
|
||||
{
|
||||
var ihdr: [13]u8 = undefined;
|
||||
writeU32BE(ihdr[0..4], w);
|
||||
writeU32BE(ihdr[4..8], h);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 2; // color type RGB
|
||||
ihdr[10] = 0; // compression
|
||||
ihdr[11] = 0; // filter
|
||||
ihdr[12] = 0; // interlace
|
||||
s.writeChunk("IHDR", &ihdr);
|
||||
}
|
||||
|
||||
// IDAT
|
||||
if (@intFromEnum(level) == 0) {
|
||||
writeIdatStore(&s, pixels, w, h);
|
||||
} else {
|
||||
writeIdatFixedHuffman(&s, pixels, w, h);
|
||||
}
|
||||
|
||||
// IEND
|
||||
s.writeChunk("IEND", &[0]u8{});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Stream wrapper — tracks CRC and Adler inline
|
||||
// =============================================================================
|
||||
|
||||
fn Stream(comptime Ctx: type) type {
|
||||
return struct {
|
||||
ctx: Ctx,
|
||||
write_fn: *const fn (Ctx, []const u8) void,
|
||||
crc: u32 = 0xFFFFFFFF,
|
||||
adler_a: u32 = 1,
|
||||
adler_b: u32 = 0,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn writeRaw(self: *Self, data: []const u8) void {
|
||||
self.write_fn(self.ctx, data);
|
||||
}
|
||||
|
||||
fn writeCrc(self: *Self, data: []const u8) void {
|
||||
self.writeRaw(data);
|
||||
self.crc = crc32Update(self.crc, data);
|
||||
}
|
||||
|
||||
fn writeCrcAdler(self: *Self, data: []const u8) void {
|
||||
self.writeRaw(data);
|
||||
self.crc = crc32Update(self.crc, data);
|
||||
adlerUpdate(&self.adler_a, &self.adler_b, data);
|
||||
}
|
||||
|
||||
fn writeChunk(self: *Self, chunk_type: *const [4]u8, data: []const u8) void {
|
||||
var buf: [4]u8 = undefined;
|
||||
writeU32BE(&buf, @intCast(data.len));
|
||||
self.writeRaw(&buf);
|
||||
self.writeRaw(chunk_type);
|
||||
self.writeRaw(data);
|
||||
var crc: u32 = 0xFFFFFFFF;
|
||||
crc = crc32Update(crc, chunk_type);
|
||||
crc = crc32Update(crc, data);
|
||||
writeU32BE(&buf, crc ^ 0xFFFFFFFF);
|
||||
self.writeRaw(&buf);
|
||||
}
|
||||
|
||||
fn beginChunk(self: *Self, chunk_type: *const [4]u8, payload_len: u32) void {
|
||||
var buf: [4]u8 = undefined;
|
||||
writeU32BE(&buf, payload_len);
|
||||
self.writeRaw(&buf);
|
||||
self.writeRaw(chunk_type);
|
||||
self.crc = 0xFFFFFFFF;
|
||||
self.crc = crc32Update(self.crc, chunk_type);
|
||||
}
|
||||
|
||||
fn endChunk(self: *Self) void {
|
||||
var buf: [4]u8 = undefined;
|
||||
writeU32BE(&buf, self.crc ^ 0xFFFFFFFF);
|
||||
self.writeRaw(&buf);
|
||||
}
|
||||
|
||||
fn resetAdler(self: *Self) void {
|
||||
self.adler_a = 1;
|
||||
self.adler_b = 0;
|
||||
}
|
||||
|
||||
fn adlerFinish(self: *Self) u32 {
|
||||
return (self.adler_b << 16) | self.adler_a;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Store blocks (level 0) — no compression, byte-aligned
|
||||
// =============================================================================
|
||||
|
||||
fn writeIdatStore(s: anytype, pixels: [*]const u8, w: u32, h: u32) void {
|
||||
const row_bytes: u32 = 1 + w * 3;
|
||||
const raw_size: u32 = h * row_bytes;
|
||||
const max_block: u32 = 65535;
|
||||
const num_blocks: u32 = (raw_size + max_block - 1) / max_block;
|
||||
const idat_payload: u32 = 2 + num_blocks * 5 + raw_size + 4;
|
||||
|
||||
s.beginChunk("IDAT", idat_payload);
|
||||
s.resetAdler();
|
||||
|
||||
// Zlib header
|
||||
const zlib_hdr = [2]u8{ 0x78, 0x01 };
|
||||
s.writeCrc(&zlib_hdr);
|
||||
|
||||
var raw_remaining: u32 = raw_size;
|
||||
var y: u32 = 0;
|
||||
var row_off: u32 = 0;
|
||||
|
||||
while (raw_remaining > 0) {
|
||||
const block_size: u32 = @min(raw_remaining, max_block);
|
||||
const bs16: u16 = @intCast(block_size);
|
||||
var hdr: [5]u8 = undefined;
|
||||
hdr[0] = if (raw_remaining <= max_block) @as(u8, 0x01) else 0x00;
|
||||
hdr[1] = @truncate(bs16);
|
||||
hdr[2] = @truncate(bs16 >> 8);
|
||||
hdr[3] = hdr[1] ^ 0xFF;
|
||||
hdr[4] = hdr[2] ^ 0xFF;
|
||||
s.writeCrc(&hdr);
|
||||
|
||||
var written: u32 = 0;
|
||||
while (written < block_size) {
|
||||
if (row_off == 0) {
|
||||
const fb = [1]u8{0x00};
|
||||
s.writeCrcAdler(&fb);
|
||||
row_off = 1;
|
||||
written += 1;
|
||||
} else {
|
||||
const pix_off = y * w * 3 + (row_off - 1);
|
||||
const left_row = w * 3 - (row_off - 1);
|
||||
const left_blk = block_size - written;
|
||||
const n = @min(left_row, left_blk);
|
||||
const data = pixels[pix_off..][0..n];
|
||||
s.writeCrcAdler(data);
|
||||
row_off += n;
|
||||
written += n;
|
||||
if (row_off > w * 3) {
|
||||
row_off = 0;
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
raw_remaining -= block_size;
|
||||
}
|
||||
|
||||
var buf: [4]u8 = undefined;
|
||||
writeU32BE(&buf, s.adlerFinish());
|
||||
s.writeCrc(&buf);
|
||||
s.endChunk();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Fixed Huffman (levels 1-9) — RFC 1951 §3.2.6 fixed codes, literals only
|
||||
//
|
||||
// Each byte is Huffman-coded using the fixed table. No LZ77 matching.
|
||||
// This gives ~10-20% compression on typical screenshot data with zero
|
||||
// runtime state beyond a small bit buffer.
|
||||
// =============================================================================
|
||||
|
||||
const BitBuf = struct {
|
||||
bits: u32 = 0,
|
||||
nbits: u5 = 0,
|
||||
|
||||
fn write(self: *BitBuf, s: anytype, code: u32, len: u5) void {
|
||||
self.bits |= code << self.nbits;
|
||||
self.nbits += len;
|
||||
while (self.nbits >= 8) {
|
||||
const byte = [1]u8{@truncate(self.bits)};
|
||||
s.writeCrcAdler(&byte);
|
||||
self.bits >>= 8;
|
||||
self.nbits -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(self: *BitBuf, s: anytype) void {
|
||||
if (self.nbits > 0) {
|
||||
const byte = [1]u8{@truncate(self.bits)};
|
||||
s.writeCrcAdler(&byte);
|
||||
self.bits = 0;
|
||||
self.nbits = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// RFC 1951 fixed Huffman: encode a literal byte (0-255) or end-of-block (256).
|
||||
fn fixedLiteral(bb: *BitBuf, s: anytype, val: u16) void {
|
||||
// RFC 1951 §3.2.6 fixed Huffman code table:
|
||||
// 0-143: 8 bits, codes 00110000-10111111
|
||||
// 144-255: 9 bits, codes 110010000-111111111
|
||||
// 256-279: 7 bits, codes 0000000-0010111
|
||||
// 280-287: 8 bits, codes 11000000-11000111
|
||||
if (val <= 143) {
|
||||
const code: u9 = @as(u9, @intCast(val)) + 0x30;
|
||||
bb.write(s, bitReverse(u9, code, 8), 8);
|
||||
} else if (val <= 255) {
|
||||
const code: u9 = @as(u9, @intCast(val - 144)) + 0x190;
|
||||
bb.write(s, bitReverse(u9, code, 9), 9);
|
||||
} else if (val <= 279) {
|
||||
const code: u9 = @intCast(val - 256);
|
||||
bb.write(s, bitReverse(u9, code, 7), 7);
|
||||
} else {
|
||||
const code: u9 = @as(u9, @intCast(val - 280)) + 0xC0;
|
||||
bb.write(s, bitReverse(u9, code, 8), 8);
|
||||
}
|
||||
}
|
||||
|
||||
fn bitReverse(comptime T: type, val: T, n: u5) u32 {
|
||||
const full = @bitReverse(val);
|
||||
const shift: u5 = @intCast(@typeInfo(T).int.bits - @as(u8, n));
|
||||
return @as(u32, full) >> shift;
|
||||
}
|
||||
|
||||
fn writeIdatFixedHuffman(s: anytype, pixels: [*]const u8, w: u32, h: u32) void {
|
||||
// We can't precompute IDAT payload size for Huffman, so we buffer the
|
||||
// entire deflate stream, then write it as one IDAT chunk.
|
||||
// For a 1024x768 screenshot, fixed Huffman with only literals produces
|
||||
// roughly 8-9 bits per byte ≈ same size or slightly larger than raw.
|
||||
// But the Sub filter makes most bytes small, yielding good compression.
|
||||
|
||||
// Allocate output buffer: worst case ~9 bits/byte * raw_size / 8 + overhead
|
||||
const row_bytes: u32 = 1 + w * 3;
|
||||
const raw_size: u32 = h * row_bytes;
|
||||
// Worst case: 9 bits per byte + block headers + zlib overhead
|
||||
const max_out: u32 = (raw_size / 8) * 9 + raw_size / 8 + 1024;
|
||||
const out_buf = std.heap.page_allocator.alloc(u8, max_out) catch {
|
||||
// Fall back to store blocks
|
||||
writeIdatStore(s, pixels, w, h);
|
||||
return;
|
||||
};
|
||||
defer std.heap.page_allocator.free(out_buf);
|
||||
|
||||
// Compress into buffer
|
||||
var out_pos: u32 = 0;
|
||||
|
||||
// Zlib header
|
||||
out_buf[0] = 0x78;
|
||||
out_buf[1] = 0x9C; // default compression
|
||||
out_pos = 2;
|
||||
|
||||
var adler_a: u32 = 1;
|
||||
var adler_b: u32 = 0;
|
||||
|
||||
// Single fixed-Huffman block (BFINAL=1, BTYPE=01)
|
||||
var bb: BitBuf = .{};
|
||||
|
||||
// Pack bits into out_buf via a mini stream
|
||||
const OutStream = struct {
|
||||
buf: []u8,
|
||||
pos: *u32,
|
||||
// Dummy fields matching the CrcAdler interface
|
||||
fn writeCrcAdler(self: *@This(), data: []const u8) void {
|
||||
for (data) |byte| {
|
||||
if (self.pos.* < self.buf.len) {
|
||||
self.buf[self.pos.*] = byte;
|
||||
self.pos.* += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
var out_stream = OutStream{ .buf = out_buf, .pos = &out_pos };
|
||||
|
||||
// BFINAL=1, BTYPE=01 (fixed Huffman)
|
||||
bb.write(&out_stream, 0b011, 3);
|
||||
|
||||
// Encode each scanline with Sub filter
|
||||
var y: u32 = 0;
|
||||
while (y < h) : (y += 1) {
|
||||
const row_start = y * w * 3;
|
||||
|
||||
// Filter byte: 1 = Sub
|
||||
const filter_byte: u8 = 1;
|
||||
adlerUpdate(&adler_a, &adler_b, &[1]u8{filter_byte});
|
||||
fixedLiteral(&bb, &out_stream, filter_byte);
|
||||
|
||||
// First pixel: Sub filter with no left neighbor = raw bytes
|
||||
var x: u32 = 0;
|
||||
while (x < w * 3) : (x += 1) {
|
||||
const raw = pixels[row_start + x];
|
||||
const filtered: u8 = if (x >= 3)
|
||||
raw -% pixels[row_start + x - 3]
|
||||
else
|
||||
raw;
|
||||
adlerUpdate(&adler_a, &adler_b, &[1]u8{filtered});
|
||||
fixedLiteral(&bb, &out_stream, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
// End of block marker (256)
|
||||
fixedLiteral(&bb, &out_stream, 256);
|
||||
bb.flush(&out_stream);
|
||||
|
||||
// Adler-32 (big-endian, NOT bit-packed — appended as raw bytes after deflate)
|
||||
const adler = (adler_b << 16) | adler_a;
|
||||
if (out_pos + 4 <= out_buf.len) {
|
||||
out_buf[out_pos] = @truncate(adler >> 24);
|
||||
out_buf[out_pos + 1] = @truncate(adler >> 16);
|
||||
out_buf[out_pos + 2] = @truncate(adler >> 8);
|
||||
out_buf[out_pos + 3] = @truncate(adler);
|
||||
out_pos += 4;
|
||||
}
|
||||
|
||||
// Write as single IDAT chunk
|
||||
s.writeChunk("IDAT", out_buf[0..out_pos]);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CRC-32 (1KB comptime table) and Adler-32
|
||||
// =============================================================================
|
||||
|
||||
const crc32_table: [256]u32 = blk: {
|
||||
@setEvalBranchQuota(10000);
|
||||
var table: [256]u32 = undefined;
|
||||
for (0..256) |n| {
|
||||
var c: u32 = @intCast(n);
|
||||
for (0..8) |_| {
|
||||
c = if (c & 1 != 0) 0xEDB88320 ^ (c >> 1) else c >> 1;
|
||||
}
|
||||
table[n] = c;
|
||||
}
|
||||
break :blk table;
|
||||
};
|
||||
|
||||
fn crc32Update(crc: u32, data: []const u8) u32 {
|
||||
var c = crc;
|
||||
for (data) |b| {
|
||||
c = crc32_table[(c ^ b) & 0xFF] ^ (c >> 8);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
fn adlerUpdate(a: *u32, b: *u32, data: []const u8) void {
|
||||
var remaining = data;
|
||||
while (remaining.len > 0) {
|
||||
const n = @min(remaining.len, 5552);
|
||||
for (remaining[0..n]) |byte| {
|
||||
a.* += byte;
|
||||
b.* += a.*;
|
||||
}
|
||||
a.* %= 65521;
|
||||
b.* %= 65521;
|
||||
remaining = remaining[n..];
|
||||
}
|
||||
}
|
||||
|
||||
fn writeU32BE(buf: *[4]u8, val: u32) void {
|
||||
buf[0] = @truncate(val >> 24);
|
||||
buf[1] = @truncate(val >> 16);
|
||||
buf[2] = @truncate(val >> 8);
|
||||
buf[3] = @truncate(val);
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
const std = @import("std");
|
||||
const hook = @import("hook");
|
||||
const png = @import("png.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
// =============================================================================
|
||||
// Windows API
|
||||
// =============================================================================
|
||||
|
||||
const HANDLE = *anyopaque;
|
||||
|
||||
const SYSTEMTIME = extern struct {
|
||||
wYear: u16,
|
||||
wMonth: u16,
|
||||
wDayOfWeek: u16,
|
||||
wDay: u16,
|
||||
wHour: u16,
|
||||
wMinute: u16,
|
||||
wSecond: u16,
|
||||
wMilliseconds: u16,
|
||||
};
|
||||
|
||||
extern "kernel32" fn GetLocalTime(lpSystemTime: *SYSTEMTIME) callconv(WINAPI) void;
|
||||
|
||||
extern "kernel32" fn CreateFileA(
|
||||
lpFileName: [*:0]const u8,
|
||||
dwDesiredAccess: u32,
|
||||
dwShareMode: u32,
|
||||
lpSecurityAttributes: ?*anyopaque,
|
||||
dwCreationDisposition: u32,
|
||||
dwFlagsAndAttributes: u32,
|
||||
hTemplateFile: ?HANDLE,
|
||||
) callconv(WINAPI) ?HANDLE;
|
||||
|
||||
extern "kernel32" fn WriteFile(
|
||||
hFile: HANDLE,
|
||||
lpBuffer: [*]const u8,
|
||||
nNumberOfBytesToWrite: u32,
|
||||
lpNumberOfBytesWritten: ?*u32,
|
||||
lpOverlapped: ?*anyopaque,
|
||||
) callconv(WINAPI) i32;
|
||||
|
||||
extern "kernel32" fn CloseHandle(hObject: HANDLE) callconv(WINAPI) i32;
|
||||
|
||||
// =============================================================================
|
||||
// State
|
||||
// =============================================================================
|
||||
|
||||
var enabled: bool = true;
|
||||
var compression_level: i32 = 6; // user-facing 0–9, kept for Lua interface
|
||||
var tga_hook: hook.Hook = .{};
|
||||
var screenshot_dir: [260]u8 = undefined;
|
||||
var screenshot_dir_len: usize = 0;
|
||||
var screenshot_counter: u32 = 1;
|
||||
|
||||
// =============================================================================
|
||||
// Ring buffer queue (max 8 pending screenshots)
|
||||
// =============================================================================
|
||||
|
||||
const MAX_PENDING = 8;
|
||||
|
||||
const PendingScreenshot = struct {
|
||||
buffer: [*]u8,
|
||||
width: u16,
|
||||
height: u16,
|
||||
size: u32,
|
||||
};
|
||||
|
||||
var queue: [MAX_PENDING]PendingScreenshot = undefined;
|
||||
var queue_head: usize = 0;
|
||||
var queue_tail: usize = 0;
|
||||
var queue_count: usize = 0;
|
||||
var mutex: std.Thread.Mutex = .{};
|
||||
var worker_running: bool = false;
|
||||
|
||||
fn enqueue(shot: PendingScreenshot) bool {
|
||||
if (queue_count >= MAX_PENDING) return false;
|
||||
queue[queue_tail] = shot;
|
||||
queue_tail = (queue_tail + 1) % MAX_PENDING;
|
||||
queue_count += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn dequeue() ?PendingScreenshot {
|
||||
if (queue_count == 0) return null;
|
||||
const shot = queue[queue_head];
|
||||
queue_head = (queue_head + 1) % MAX_PENDING;
|
||||
queue_count -= 1;
|
||||
return shot;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Directory extraction — capture path prefix from game's first TGA filename
|
||||
// =============================================================================
|
||||
|
||||
fn extractDir(filename_ptr: u32) void {
|
||||
const path: [*:0]const u8 = @ptrFromInt(filename_ptr);
|
||||
const span = std.mem.span(path);
|
||||
var last_sep: usize = 0;
|
||||
for (span, 0..) |c, i| {
|
||||
if (c == '\\' or c == '/') last_sep = i + 1;
|
||||
}
|
||||
if (last_sep > 0 and last_sep <= screenshot_dir.len) {
|
||||
@memcpy(screenshot_dir[0..last_sep], span[0..last_sep]);
|
||||
screenshot_dir_len = last_sep;
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Call original CTgaFile::Write — __thiscall(self_ECX, filename_stack) ret 4
|
||||
// =============================================================================
|
||||
|
||||
fn callOriginal(self: u32, filename: u32) i32 {
|
||||
return asm volatile (
|
||||
\\push %[filename]
|
||||
\\call *%[func]
|
||||
: [ret] "={eax}" (-> i32),
|
||||
: [_] "{ecx}" (self),
|
||||
[filename] "r" (filename),
|
||||
[func] "r" (tga_hook.trampoline),
|
||||
: .{ .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Detour: CTgaFile::Write at 0x5a4810
|
||||
// Thunked from __fastcall(self_ECX, _EDX, filename_stack) → cdecl
|
||||
// =============================================================================
|
||||
|
||||
fn tgaWriteDetour(self: u32, _edx: u32, filename: u32) callconv(.c) i32 {
|
||||
_ = _edx;
|
||||
|
||||
if (!enabled) return callOriginal(self, filename);
|
||||
|
||||
// Validate TGA header fields
|
||||
const pixel_data = hook.readMem(u32, self + 0x04);
|
||||
const additional_header = hook.readMem(u8, self + 0x08);
|
||||
const color_map_type = hook.readMem(u8, self + 0x09);
|
||||
const image_type = hook.readMem(u8, self + 0x0a);
|
||||
|
||||
if (pixel_data == 0 or filename == 0 or image_type != 2 or additional_header != 0 or color_map_type != 0) {
|
||||
return callOriginal(self, filename);
|
||||
}
|
||||
|
||||
const width = hook.readMem(u16, self + 0x14);
|
||||
const height = hook.readMem(u16, self + 0x16);
|
||||
const size: u32 = @as(u32, width) * @as(u32, height) * 3;
|
||||
|
||||
// Capture screenshot directory from the first TGA path we see
|
||||
if (screenshot_dir_len == 0) extractDir(filename);
|
||||
|
||||
// Allocate buffer and copy BGR pixel data
|
||||
const buffer = std.heap.page_allocator.alloc(u8, size) catch
|
||||
return callOriginal(self, filename);
|
||||
const src: [*]const u8 = @ptrFromInt(pixel_data);
|
||||
@memcpy(buffer, src[0..size]);
|
||||
|
||||
// Enqueue for async processing
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
|
||||
if (!enqueue(.{ .buffer = buffer.ptr, .width = width, .height = height, .size = size })) {
|
||||
std.heap.page_allocator.free(buffer);
|
||||
return callOriginal(self, filename);
|
||||
}
|
||||
|
||||
// Spawn worker if not already running
|
||||
if (!worker_running) {
|
||||
worker_running = true;
|
||||
const thread = std.Thread.spawn(.{}, workerThread, .{}) catch {
|
||||
worker_running = false;
|
||||
return 1;
|
||||
};
|
||||
thread.detach();
|
||||
}
|
||||
|
||||
return 1; // suppress original TGA write
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Worker thread — dequeues shots, converts BGR→RGB, writes PNG
|
||||
// =============================================================================
|
||||
|
||||
fn workerThread() void {
|
||||
while (true) {
|
||||
var shot: PendingScreenshot = undefined;
|
||||
{
|
||||
mutex.lock();
|
||||
defer mutex.unlock();
|
||||
if (dequeue()) |s| {
|
||||
shot = s;
|
||||
} else {
|
||||
worker_running = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
processScreenshot(shot);
|
||||
}
|
||||
}
|
||||
|
||||
fn processScreenshot(shot: PendingScreenshot) void {
|
||||
defer std.heap.page_allocator.free(shot.buffer[0..shot.size]);
|
||||
|
||||
// BGR → RGB swap
|
||||
const total: u32 = @as(u32, shot.width) * @as(u32, shot.height);
|
||||
var i: u32 = 0;
|
||||
while (i < total) : (i += 1) {
|
||||
const off = i * 3;
|
||||
const tmp = shot.buffer[off];
|
||||
shot.buffer[off] = shot.buffer[off + 2];
|
||||
shot.buffer[off + 2] = tmp;
|
||||
}
|
||||
|
||||
// Generate filename: {dir}WoWScrnShot_MMDDYY_HHMMSS_N.png
|
||||
var st: SYSTEMTIME = undefined;
|
||||
GetLocalTime(&st);
|
||||
|
||||
var name_buf: [260]u8 = undefined;
|
||||
const name_slice = std.fmt.bufPrint(&name_buf, "{s}WoWScrnShot_{:0>2}{:0>2}{:0>2}_{:0>2}{:0>2}{:0>2}_{}.png", .{
|
||||
screenshot_dir[0..screenshot_dir_len],
|
||||
st.wMonth,
|
||||
st.wDay,
|
||||
st.wYear % @as(u16, 100),
|
||||
st.wHour,
|
||||
st.wMinute,
|
||||
st.wSecond,
|
||||
screenshot_counter,
|
||||
}) catch return;
|
||||
|
||||
screenshot_counter += 1;
|
||||
if (screenshot_counter > 999) screenshot_counter = 1;
|
||||
|
||||
// Null-terminate for CreateFileA
|
||||
if (name_slice.len >= name_buf.len) return;
|
||||
name_buf[name_slice.len] = 0;
|
||||
|
||||
const level = png.mapLevel(compression_level);
|
||||
writePng(@ptrCast(name_slice.ptr), shot.buffer, shot.width, shot.height, level);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// File I/O bridge for png.zig
|
||||
// =============================================================================
|
||||
|
||||
fn writeToFile(handle: HANDLE, data: []const u8) void {
|
||||
var off: usize = 0;
|
||||
while (off < data.len) {
|
||||
var written: u32 = 0;
|
||||
_ = WriteFile(handle, data[off..].ptr, @intCast(data.len - off), &written, null);
|
||||
if (written == 0) return;
|
||||
off += written;
|
||||
}
|
||||
}
|
||||
|
||||
fn writePng(path: [*:0]const u8, pixels: [*]const u8, width: u16, height: u16, level: png.Level) void {
|
||||
const handle = CreateFileA(path, 0x40000000, 0, null, 2, 0x80, null) orelse return;
|
||||
defer _ = CloseHandle(handle);
|
||||
png.encode(handle, writeToFile, pixels, width, height, level);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Lua helper: push f64 onto Lua stack via __fastcall(L_ECX, f64_on_stack)
|
||||
// =============================================================================
|
||||
|
||||
fn luaPushNumber(L_ptr: usize, n: f64) void {
|
||||
// lua_pushnumber at 0x6F3810 is __fastcall(L, double)
|
||||
// double skips EDX, goes on stack (8 bytes). Callee cleans with ret 8.
|
||||
const raw: [2]u32 = @bitCast(n);
|
||||
asm volatile (
|
||||
\\push %[hi]
|
||||
\\push %[lo]
|
||||
\\call *%[func]
|
||||
:
|
||||
: [_] "{ecx}" (L_ptr),
|
||||
[lo] "r" (raw[0]),
|
||||
[hi] "r" (raw[1]),
|
||||
[func] "r" (@as(u32, 0x6F3810)),
|
||||
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Lua C function: WeirdUtilsScreenshot(...)
|
||||
// No args → returns enabled (bool), compression_level (number)
|
||||
// ("enable") → enable PNG screenshots
|
||||
// ("disable") → disable (fall through to original TGA)
|
||||
// ("quality", N) → set compression level 0–9 (kept for addon compat)
|
||||
// =============================================================================
|
||||
|
||||
pub fn screenshotCommand(L: *anyopaque) callconv(.c) u32 {
|
||||
const L_ptr = @intFromPtr(L);
|
||||
|
||||
// lua_gettop(L) — __fastcall(L_ECX), EDX unused
|
||||
const nargs = hook.fastcall(i32, 0x6F3070, L_ptr, @as(u32, 0));
|
||||
|
||||
if (nargs == 0) {
|
||||
// lua_pushboolean(L, enabled)
|
||||
hook.fastcall(void, 0x6F39F0, L_ptr, @as(i32, if (enabled) 1 else 0));
|
||||
// lua_pushnumber(L, compression_level)
|
||||
luaPushNumber(L_ptr, @floatFromInt(compression_level));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// lua_tostring(L, 1) — __fastcall(L_ECX, index_EDX)
|
||||
const raw_str = hook.fastcall(usize, 0x6F3690, L_ptr, @as(i32, 1));
|
||||
if (raw_str != 0) {
|
||||
const str: [*:0]const u8 = @ptrFromInt(raw_str);
|
||||
const arg = std.mem.span(str);
|
||||
|
||||
if (std.mem.eql(u8, arg, "enable")) {
|
||||
enabled = true;
|
||||
} else if (std.mem.eql(u8, arg, "disable")) {
|
||||
enabled = false;
|
||||
} else if (std.mem.eql(u8, arg, "quality")) {
|
||||
if (nargs >= 2) {
|
||||
// lua_tonumber(L, 2) — __fastcall(L_ECX, index_EDX), returns f64 in ST(0)
|
||||
const level = hook.fastcall(f64, 0x6F3620, L_ptr, @as(i32, 2));
|
||||
compression_level = std.math.clamp(@as(i32, @intFromFloat(level)), 0, 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / Remove
|
||||
// =============================================================================
|
||||
|
||||
pub fn installHook() void {
|
||||
// CTgaFile::Write at 0x5a4810
|
||||
// __thiscall(self, filename) — prologue: 55 8B EC 83 EC 08 = 6 bytes, no fixups
|
||||
// Thunk: fastcall(ECX=self, EDX, stack: filename) → cdecl(self, edx, filename)
|
||||
if (tga_hook.prepare(0x5a4810, 6, &.{})) {
|
||||
const thunk = tga_hook.mem.? + 32;
|
||||
_ = hook.buildFastcallToCdeclThunk(thunk, @intFromPtr(&tgaWriteDetour), 1);
|
||||
tga_hook.activate(@intFromPtr(thunk));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn removeHook() void {
|
||||
tga_hook.remove();
|
||||
}
|
||||
Reference in New Issue
Block a user