Files
WeirdUtils/src/main.zig
T
MarcelineVQ b7f293c8c5 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.
2026-02-23 16:02:55 -08:00

468 lines
16 KiB
Zig

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;
}