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:
MarcelineVQ
2026-02-23 16:02:55 -08:00
commit b7f293c8c5
11 changed files with 1814 additions and 0 deletions
+345
View File
@@ -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();
}