summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Matthew Wozniak <me@woz.blue> 2025-08-11 18:17:40 -0400
committerGravatar Matthew Wozniak <me@woz.blue> 2025-08-11 20:46:00 -0400
commit89055b019e2e2d49f8813d3578f6bc338326ca47 (patch)
treecdd097e134454937ccd35324b55d4055c2c2f4ed
downloadrt2-master.tar.gz
rt2-master.zip
intial commitHEADmaster
-rw-r--r--build.zig39
-rw-r--r--build.zig.zon16
-rw-r--r--hook/LICENSE12
-rw-r--r--hook/build.zig21
-rw-r--r--hook/build.zig.zon12
-rw-r--r--hook/src/Hook.zig237
-rw-r--r--hook/src/HookManager.zig80
-rw-r--r--hook/src/mem.zig199
-rw-r--r--hook/src/utils.zig198
-rw-r--r--hook/src/x86.zig670
-rw-r--r--hook/src/zhook.zig9
-rw-r--r--src/main.zig608
-rw-r--r--src/root.zig24
13 files changed, 2125 insertions, 0 deletions
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..f2ab70d
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,39 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ // for now, this only supports 32 bit windows builds, so just build for that
+ // by default
+ const target = b.standardTargetOptions(.{ .default_target = .{
+ .os_tag = .windows,
+ .cpu_arch = .x86,
+ .abi = .msvc,
+ } });
+ const optimize = b.standardOptimizeOption(.{});
+
+ const hook = b.dependency("hook", .{});
+
+ const exe = b.addExecutable(.{
+ .name = "rt2",
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/main.zig"),
+ .target = target,
+ .optimize = optimize,
+ }),
+ });
+
+ b.installArtifact(exe);
+ exe.root_module.addImport("hook", hook.module("hook"));
+ exe.linkSystemLibrary("kernel32");
+ exe.linkLibC();
+
+ exe.addIncludePath(b.path("../../../scoop/persist/vcpkg/installed/x86-windows-static/include"));
+
+ const system_libraries = [_][]const u8 {
+ "avcodec", "avutil", "avformat", "swscale", "vorbisfile", "libx264", "x265-static",
+ "swresample", "ws2_32", "mfuuid", "strmiids", "ole32", "user32", "bcrypt", "secur32",
+ "aom", "opus", "libmp3lame-static", "opus", "libmpghip-static",
+ };
+ for (system_libraries) |library| {
+ exe.linkSystemLibrary2(library, .{.preferred_link_mode = .static});
+ }
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..8ad8cbd
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,16 @@
+.{
+ .name = .rt2,
+ .version = "0.0.0",
+ .fingerprint = 0x2bb0885edd13d89c, // Changing this has security and trust implications.
+ .minimum_zig_version = "0.15.0-dev.1092+d772c0627",
+ .dependencies = .{
+ .hook = .{
+ .path = "hook/",
+ },
+ },
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ },
+}
diff --git a/hook/LICENSE b/hook/LICENSE
new file mode 100644
index 0000000..ec7482e
--- /dev/null
+++ b/hook/LICENSE
@@ -0,0 +1,12 @@
+Copyright (c) 2025 evanlin96069
+
+Permission to use, copy, modify, and/or distribute this software for any purpose
+with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
+TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+THIS SOFTWARE.
diff --git a/hook/build.zig b/hook/build.zig
new file mode 100644
index 0000000..6e13e7c
--- /dev/null
+++ b/hook/build.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const mod = b.addModule("hook", .{
+ .root_source_file = b.path("src/zhook.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+ if (target.result.os.tag == .windows)
+ mod.linkSystemLibrary("kernel32", .{});
+
+ const mod_tests = b.addTest(.{
+ .root_module = mod,
+ });
+ const run_mod_tests = b.addRunArtifact(mod_tests);
+ const test_step = b.step("test", "Run tests");
+ test_step.dependOn(&run_mod_tests.step);
+}
diff --git a/hook/build.zig.zon b/hook/build.zig.zon
new file mode 100644
index 0000000..f9c531b
--- /dev/null
+++ b/hook/build.zig.zon
@@ -0,0 +1,12 @@
+.{
+ .name = .hook,
+ .version = "0.0.0",
+ .fingerprint = 0xa45843556d3689d1, // Changing this has security and trust implications.
+ .minimum_zig_version = "0.15.0-dev.1149+4e6a04929",
+ .dependencies = .{},
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ },
+}
diff --git a/hook/src/Hook.zig b/hook/src/Hook.zig
new file mode 100644
index 0000000..7a8ca06
--- /dev/null
+++ b/hook/src/Hook.zig
@@ -0,0 +1,237 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+const x86 = @import("x86.zig");
+const utils = @import("utils.zig");
+
+const loadValue = @import("mem.zig").loadValue;
+
+const Hook = @This();
+
+const HookType = enum {
+ vmt,
+ detour,
+};
+
+const Rel32Patch = struct {
+ offset: u32, // offset into the trampoline
+ dest: u32, // absolute address rel32 points to
+ orig: u32, // original data in the instruction
+};
+
+// Windows doesn't use PIC
+const PICPatch = switch (builtin.os.tag) {
+ .linux => struct {
+ offset: u32, // offset into the original function
+ orig: u32, // original data in the instruction
+ },
+ .windows => void,
+ else => @compileError("Unsupported OS"),
+};
+
+const HookData = union(HookType) {
+ const HookVMTResult = struct {
+ vt: [*]*const anyopaque,
+ index: u32,
+ };
+
+ const HookDetourResult = struct {
+ func: [*]u8,
+ trampoline: []u8,
+ rel32_patch: ?Rel32Patch = null,
+ pic_patch: ?PICPatch = null,
+ };
+
+ vmt: HookVMTResult,
+ detour: HookDetourResult,
+};
+
+orig: ?*const anyopaque,
+data: HookData,
+
+pub fn hookVMT(vt: [*]*const anyopaque, index: usize, target: *const anyopaque) !Hook {
+ const orig: *const anyopaque = vt[index];
+ const entry_ptr: [*]u8 = @ptrCast(vt + index);
+
+ const bytes = std.mem.toBytes(target);
+ try utils.patchCode(entry_ptr, &bytes, 0b001); // restore to read-only
+
+ return Hook{
+ .orig = orig,
+ .data = .{
+ .vmt = .{
+ .vt = vt,
+ .index = index,
+ },
+ },
+ };
+}
+
+// Trampoline memory must have rwx permissions
+pub fn hookDetour(func: *anyopaque, target: *const anyopaque, trampoline: []u8) !Hook {
+ var mem: [*]u8 = @ptrCast(func);
+
+ // Hook the underlying thing if the function jmp immediately.
+ while (mem[0] == x86.Opcode.Op1IW.jmpiw) {
+ const offset = loadValue(u32, mem + 1);
+ mem = @ptrFromInt(@intFromPtr(mem + 5) +% offset);
+ }
+
+ var rel32_patch: ?Rel32Patch = null;
+ var pic_patch: ?PICPatch = null;
+
+ var len: usize = 0;
+ while (true) : (len += try x86.x86_len(mem + len)) {
+ if (len >= 5) break;
+
+ // No checks for rel16 at all. I don't think we will encounter them.
+ const op0 = mem[len];
+ switch (op0) {
+ x86.Opcode.Op1I8.jmpi8,
+ x86.Opcode.Op1I8.jcxz,
+ x86.Opcode.Op1I8.jo,
+ x86.Opcode.Op1I8.jno,
+ x86.Opcode.Op1I8.jb,
+ x86.Opcode.Op1I8.jnb,
+ x86.Opcode.Op1I8.jz,
+ x86.Opcode.Op1I8.jnz,
+ x86.Opcode.Op1I8.jna,
+ x86.Opcode.Op1I8.ja,
+ x86.Opcode.Op1I8.js,
+ x86.Opcode.Op1I8.jns,
+ x86.Opcode.Op1I8.jp,
+ x86.Opcode.Op1I8.jnp,
+ x86.Opcode.Op1I8.jl,
+ x86.Opcode.Op1I8.jnl,
+ x86.Opcode.Op1I8.jng,
+ x86.Opcode.Op1I8.jg,
+ => {
+ // TODO: Make it rel32 jump in the trampoline
+ return error.BadInstruction;
+ },
+
+ x86.Opcode.Op1IW.jmpiw,
+ x86.Opcode.Op1IW.call,
+ => {
+ const offset = loadValue(u32, mem + len + 1);
+ rel32_patch = .{
+ .offset = len + 1,
+ .dest = @intFromPtr(mem + len + 5) +% offset,
+ .orig = offset,
+ };
+
+ if (op0 == x86.Opcode.Op1IW.call and builtin.os.tag == .linux) {
+ // Look for PIC pattern:
+ // call __i686.get_pc_thunk.reg
+ // add reg, imm32
+ if (utils.matchPIC(mem + len)) |off| {
+ const imm32 = loadValue(u32, mem + len + off);
+ pic_patch = .{
+ .offset = len + off,
+ .orig = imm32,
+ };
+ }
+ }
+ },
+
+ x86.Opcode.op2_byte => {
+ const op1 = mem[len + 1];
+ switch (op1) {
+ x86.Opcode.Op2IW.joii,
+ x86.Opcode.Op2IW.jnoii,
+ x86.Opcode.Op2IW.jbii,
+ x86.Opcode.Op2IW.jnbii,
+ x86.Opcode.Op2IW.jzii,
+ x86.Opcode.Op2IW.jnzii,
+ x86.Opcode.Op2IW.jnaii,
+ x86.Opcode.Op2IW.jaii,
+ x86.Opcode.Op2IW.jsii,
+ x86.Opcode.Op2IW.jnsii,
+ x86.Opcode.Op2IW.jpii,
+ x86.Opcode.Op2IW.jnpii,
+ x86.Opcode.Op2IW.jlii,
+ x86.Opcode.Op2IW.jnlii,
+ x86.Opcode.Op2IW.jngii,
+ x86.Opcode.Op2IW.jgii,
+ => {
+ const offset = loadValue(u32, mem + len + 2);
+ rel32_patch = .{
+ .offset = len + 2,
+ .dest = @intFromPtr(mem + len + 6) +% offset,
+ .orig = offset,
+ };
+ },
+ else => {},
+ }
+ },
+ else => {},
+ }
+ }
+
+ const trampoline_size = len + 5;
+ if (trampoline.len < trampoline_size) {
+ return error.OutOfTrampoline;
+ }
+
+ @memcpy(trampoline[0..len], mem);
+ trampoline[len] = x86.Opcode.Op1IW.jmpiw;
+ const jmp1_offset: *align(1) u32 = @ptrCast(trampoline.ptr + len + 1);
+ jmp1_offset.* = @intFromPtr(mem + len) -% @intFromPtr(trampoline.ptr + len + 5);
+
+ if (rel32_patch) |r| {
+ const rel_patch: *align(1) u32 = @ptrCast(trampoline.ptr + r.offset);
+ rel_patch.* = r.dest -% (@intFromPtr(trampoline.ptr + r.offset + 4));
+ }
+
+ var detour: [5]u8 = undefined;
+ detour[0] = x86.Opcode.Op1IW.jmpiw;
+ const jmp2_offset: *align(1) u32 = @ptrCast(&detour[1]);
+ jmp2_offset.* = @intFromPtr(target) -% @intFromPtr(mem + 5);
+
+ try utils.patchCode(mem, detour[0..], 0b101);
+
+ if (builtin.os.tag == .linux) {
+ if (pic_patch) |p| {
+ const delta: u32 = @intFromPtr(trampoline.ptr) -% @intFromPtr(mem);
+ const new_value: u32 = p.orig -% delta;
+
+ const bytes = std.mem.toBytes(new_value);
+ try utils.patchCode(mem + p.offset, &bytes, 0b101);
+ }
+ }
+
+ return Hook{
+ .orig = trampoline.ptr,
+ .data = .{ .detour = .{
+ .func = mem,
+ .trampoline = trampoline[0..trampoline_size],
+ .rel32_patch = rel32_patch,
+ .pic_patch = pic_patch,
+ } },
+ };
+}
+
+pub fn unhook(self: *Hook) !void {
+ const orig = self.orig orelse return;
+ switch (self.data) {
+ .vmt => |v| {
+ const entry_ptr: [*]u8 = @ptrCast(v.vt + v.index);
+ const bytes = std.mem.toBytes(orig);
+ try utils.patchCode(entry_ptr, &bytes, 0b001); // restore to read-only
+ },
+ .detour => |v| {
+ if (v.rel32_patch) |r| {
+ const orig_patch: *align(1) u32 = @ptrCast(v.trampoline.ptr + r.offset);
+ orig_patch.* = r.orig;
+ }
+ try utils.patchCode(v.func, v.trampoline[0 .. v.trampoline.len - 5], 0b101);
+ if (builtin.os.tag == .linux) {
+ if (v.pic_patch) |p| {
+ const bytes = std.mem.toBytes(p.orig);
+ try utils.patchCode(v.func + p.offset, &bytes, 0b101);
+ }
+ }
+ },
+ }
+ self.orig = null;
+}
diff --git a/hook/src/HookManager.zig b/hook/src/HookManager.zig
new file mode 100644
index 0000000..259fc3e
--- /dev/null
+++ b/hook/src/HookManager.zig
@@ -0,0 +1,80 @@
+const std = @import("std");
+const builtin = @import("builtin");
+
+const Hook = @import("Hook.zig");
+const mem = @import("mem.zig");
+const utils = @import("utils.zig");
+
+const HookManager = @This();
+
+hooks: std.ArrayList(Hook),
+exec_page: []u8,
+
+pub fn init(alloc: std.mem.Allocator) !HookManager {
+ // create our exectuable page to store trampolines
+ const page_size = std.heap.page_size_min;
+ const exec_page: []u8 = try alloc.alignedAlloc(u8, .fromByteUnits(page_size), page_size);
+ if (builtin.os.tag == .windows) {
+ var oldprotect: u32 = undefined;
+ try std.os.windows.VirtualProtect(exec_page.ptr, page_size, std.os.windows.PAGE_EXECUTE_READWRITE, &oldprotect);
+ } else if (builtin.os.tag == .linux) {
+ const PROT = std.os.linux.PROT;
+ try std.os.linux.mprotect(exec_page.ptr, page_size, PROT.EXEC | PROT.WRITE | PROT.READ);
+ } else {
+ @compileError("unsupported os");
+ }
+ return HookManager{
+ .hooks = std.ArrayList(Hook).init(alloc),
+ .exec_page = exec_page[0..],
+ };
+}
+
+pub fn deinit(self: *HookManager) usize {
+ var count: usize = 0;
+ for (self.hooks.items) |*hook| {
+ hook.unhook() catch continue;
+ count += 1;
+ }
+
+ self.hooks.deinit();
+ return count;
+}
+
+pub fn findAndHook(self: *HookManager, T: type, module: []const u8, patterns: []const []const ?u8, target: *const anyopaque) !T {
+ const match = mem.scanUniquePatterns(module, patterns) orelse {
+ return error.PatternNotFound;
+ };
+
+ return self.hookDetour(T, match.ptr, target);
+}
+
+pub fn hookVMT(self: *HookManager, vt: [*]*const anyopaque, index: usize, target: anytype) !@TypeOf(target) {
+ var hook = try Hook.hookVMT(vt, index, target);
+ errdefer hook.unhook() catch {};
+
+ try self.hooks.append(hook);
+
+ return @ptrCast(hook.orig.?);
+}
+
+pub fn hookDetour(self: *HookManager, func: anytype, target: @TypeOf(func)) !@TypeOf(func) {
+ var hook = try Hook.hookDetour(@constCast(func), target, self.exec_page);
+ errdefer hook.unhook() catch {};
+
+ try self.hooks.append(hook);
+
+ self.exec_page = self.exec_page[hook.data.detour.trampoline.len..];
+
+ return @ptrCast(hook.orig.?);
+}
+
+pub fn hookSymbol(self: *HookManager, module_name: []const u8, func: [:0]const u8, target: anytype) !@TypeOf(target) {
+ comptime std.debug.assert(@typeInfo(@TypeOf(target)) == .pointer);
+
+ var module = try std.DynLib.open(module_name);
+ // this just decreases refcount by 1, we don't ever hook things that aren't
+ // already loaded so it doesn't matter
+ defer module.close();
+ const func_ptr = module.lookup(@TypeOf(target), func) orelse return error.NoSuchSymbol;
+ return self.hookDetour(func_ptr, target);
+}
diff --git a/hook/src/mem.zig b/hook/src/mem.zig
new file mode 100644
index 0000000..62aec00
--- /dev/null
+++ b/hook/src/mem.zig
@@ -0,0 +1,199 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const testing = std.testing;
+
+const utils = @import("utils.zig");
+
+const isHex = utils.isHex;
+const makeHex = utils.makeHex;
+
+pub fn makePattern(comptime str: []const u8) []const ?u8 {
+ return comptime blk: {
+ @setEvalBranchQuota(10000);
+ var it = std.mem.splitSequence(u8, str, " ");
+ var pat: []const ?u8 = &.{};
+
+ while (it.next()) |byte| {
+ if (byte.len != 2) {
+ @compileError("Each byte should be 2 characters");
+ }
+ if (byte[0] == '?') {
+ if (byte[1] != '?') {
+ @compileError("The second question mark is missing");
+ }
+ pat = pat ++ .{null};
+ } else if (isHex(byte[0])) {
+ if (!isHex(byte[1])) {
+ @compileError("The second hex digit is missing");
+ }
+ const n = try std.fmt.parseInt(u8, byte, 16);
+ pat = pat ++ .{n};
+ } else {
+ @compileError("Only hex digits, spaces and question marks are allowed");
+ }
+ }
+ break :blk pat;
+ };
+}
+
+pub fn makePatterns(comptime arr: anytype) []const []const ?u8 {
+ return comptime blk: {
+ var patterns: []const []const ?u8 = &.{};
+ for (arr) |str| {
+ const pat: []const []const ?u8 = &.{makePattern(str)};
+ patterns = patterns ++ pat;
+ }
+ break :blk patterns;
+ };
+}
+
+pub fn scanFirst(mem: []const u8, pattern: []const ?u8) ?usize {
+ if (mem.len < pattern.len) {
+ return null;
+ }
+
+ var offset: usize = 0;
+ outer: while (offset < mem.len - pattern.len + 1) : (offset += 1) {
+ for (pattern, 0..) |byte, j| {
+ if (byte) |b| {
+ if (b != mem[offset + j]) {
+ continue :outer;
+ }
+ }
+ }
+ return offset;
+ }
+
+ return null;
+}
+
+pub fn scanUnique(mem: []const u8, pattern: []const ?u8) ?usize {
+ if (scanFirst(mem, pattern)) |offset| {
+ if (scanFirst(mem[offset + pattern.len ..], pattern) != null) {
+ return null;
+ }
+ return offset;
+ }
+
+ return null;
+}
+
+pub const MatchedPattern = struct {
+ index: usize,
+ ptr: [*]const u8,
+};
+
+pub fn scanAllPatterns(mem: []const u8, patterns: []const []const ?u8, data: *std.ArrayList(MatchedPattern)) !void {
+ for (patterns, 0..) |pattern, i| {
+ var base: usize = 0;
+ while (scanFirst(mem[base..], pattern)) |offset| {
+ try data.append(MatchedPattern{
+ .index = i,
+ .ptr = mem.ptr + base + offset,
+ });
+ base += offset + pattern.len;
+ }
+ }
+}
+
+pub fn scanUniquePatterns(mem: []const u8, patterns: []const []const ?u8) ?MatchedPattern {
+ var match: ?MatchedPattern = null;
+ for (patterns, 0..) |pattern, i| {
+ if (scanFirst(mem, pattern)) |offset| {
+ if (scanFirst(mem[offset + pattern.len ..], pattern) != null) {
+ return null;
+ }
+
+ if (match != null) {
+ return null;
+ }
+
+ match = .{
+ .index = i,
+ .ptr = mem.ptr + offset,
+ };
+ }
+ }
+
+ return match;
+}
+
+test "Scan first pattern" {
+ const mem = makeHex("F6 05 12 34 56 78 12");
+
+ // Match at the start
+ const test_pattern1 = makePattern("F6 05 12");
+ const result1 = scanFirst(mem, test_pattern1);
+ try testing.expect(result1 != null);
+ if (result1) |offset| {
+ try testing.expectEqual(0, offset);
+ }
+
+ // Match at the middle
+ const test_pattern2 = makePattern("12 34 56");
+ const result2 = scanFirst(mem, test_pattern2);
+ try testing.expect(result2 != null);
+ if (result2) |offset| {
+ try testing.expectEqual(2, offset);
+ }
+
+ // Match at the end
+ const test_pattern3 = makePattern("56 78 12");
+ const result3 = scanFirst(mem, test_pattern3);
+ try testing.expect(result3 != null);
+ if (result3) |offset| {
+ try testing.expectEqual(4, offset);
+ }
+}
+
+test "Scan unique patterns" {
+ const mem = makeHex("F6 05 12 34 56 78 12");
+ const test_patterns = makePatterns(.{
+ "00 00 ?? ?? 12",
+ "12 ?? 56",
+ "F6 05 00 34",
+ });
+
+ const result = scanUniquePatterns(mem, test_patterns);
+ try testing.expect(result != null);
+ if (result) |r| {
+ try testing.expectEqual(1, r.index);
+ try testing.expectEqual(mem.ptr + 2, r.ptr);
+ }
+}
+
+test "Scan unique patterns with multiple matches" {
+ const mem = makeHex("12 34 56 12 34 56 78 9A BC DE");
+
+ const test_patterns1 = makePatterns(.{
+ "12 34 56", // Non-unique match
+ });
+ try testing.expect(scanUniquePatterns(mem, test_patterns1) == null);
+
+ const test_patterns2 = makePatterns(.{
+ "12 ?? ?? 12", // Unique match
+ "9A BC DE", // Unique match
+ });
+ try testing.expect(scanUniquePatterns(mem, test_patterns2) == null);
+
+ const test_patterns3 = makePatterns(.{
+ "12 34 56", // Non-unique match
+ "9A BC DE", // Unique match
+ });
+ try testing.expect(scanUniquePatterns(mem, test_patterns3) == null);
+}
+
+pub fn loadValue(T: type, ptr: [*]const u8) T {
+ const val: *align(1) const T = @ptrCast(ptr);
+ return val.*;
+}
+
+pub fn setValue(T: type, ptr: [*]u8, value: T) void {
+ const val: *align(1) T = @ptrCast(ptr);
+ val.* = value;
+}
+
+test "Load value from memory" {
+ const mem = makeHex("E9 B1 9A 78 56"); // jmp
+ try testing.expectEqual(0x56789AB1, loadValue(u32, mem.ptr + 1));
+}
diff --git a/hook/src/utils.zig b/hook/src/utils.zig
new file mode 100644
index 0000000..57fdfec
--- /dev/null
+++ b/hook/src/utils.zig
@@ -0,0 +1,198 @@
+const std = @import("std");
+const w = std.os.windows;
+const builtin = @import("builtin");
+
+const x86 = @import("x86.zig");
+const mem = @import("mem.zig");
+
+const MODULEINFO = extern struct {
+ lpBaseOfDll: w.LPVOID,
+ SizeOfImage: w.DWORD,
+ EntryPoint: w.LPVOID,
+};
+
+extern "kernel32" fn FlushInstructionCache(hProcess: w.HANDLE, lpBaseAddress: w.LPCVOID, dwSize: w.SIZE_T) callconv(.winapi) w.BOOL;
+extern "kernel32" fn GetModuleHandleW(lpModuleName: ?w.LPCWSTR) callconv(.winapi) w.HMODULE;
+extern "kernel32" fn GetModuleInformation(hProcess: w.HANDLE, hModule: w.HMODULE, lpmodinfo: *MODULEINFO, cb: w.DWORD) callconv(.winapi) w.BOOL;
+extern "kernel32" fn GetCurrentProcess() callconv(.winapi) w.HANDLE;
+
+pub inline fn isHex(c: u8) bool {
+ return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F');
+}
+
+pub fn makeHex(comptime str: []const u8) []const u8 {
+ return comptime blk: {
+ @setEvalBranchQuota(10000);
+ var it = std.mem.splitSequence(u8, str, " ");
+ var pat: []const u8 = &.{};
+
+ while (it.next()) |byte| {
+ if (byte.len != 2) {
+ @compileError("Each byte should be 2 characters");
+ }
+ if (isHex(byte[0])) {
+ if (!isHex(byte[1])) {
+ @compileError("The second hex digit is missing");
+ }
+ const n = try std.fmt.parseInt(u8, byte, 16);
+ pat = pat ++ .{n};
+ } else {
+ @compileError("Only hex digits are allowed");
+ }
+ }
+ break :blk pat;
+ };
+}
+
+pub fn patchCode(addr: [*]u8, data: []const u8, restore_protect: u32) !void {
+ if (builtin.os.tag == .windows) {
+ var old_protect: w.DWORD = undefined;
+
+ try w.VirtualProtect(addr, data.len, w.PAGE_EXECUTE_READWRITE, &old_protect);
+ @memcpy(addr, data);
+
+ _ = FlushInstructionCache(GetCurrentProcess(), addr, data.len);
+ try w.VirtualProtect(addr, data.len, old_protect, &old_protect);
+ } else {
+ const page_size = std.heap.page_size_min;
+ const addr_int = @intFromPtr(addr);
+ const page_start = addr_int & ~(page_size - 1);
+ const page_end = addr_int + data.len;
+ const page_len = (page_end - page_start + page_size - 1) & ~(page_size - 1);
+
+ const prot_all = 0b111; // rwx
+
+ if (std.c.mprotect(@ptrFromInt(page_start), page_len, prot_all) != 0)
+ return error.MProtectWritable;
+
+ @memcpy(addr, data);
+
+ if (std.c.mprotect(@ptrFromInt(page_start), page_len, restore_protect) != 0)
+ return error.MProtectRestore;
+ }
+}
+
+// Windows: Return entire module memory
+// Linux: Return the code segment memory
+pub fn getModule(comptime module_name: []const u8) ?[]const u8 {
+ return switch (builtin.os.tag) {
+ .windows => getModuleWindows(module_name),
+ .linux => getModuleLinux(module_name, 0b101) catch return null,
+ else => @compileError("getModule is not available for this target"),
+ };
+}
+
+// Return the entire module memory
+pub fn getEntireModule(comptime module_name: []const u8) ?[]const u8 {
+ return switch (builtin.os.tag) {
+ .windows => getModuleWindows(module_name),
+ .linux => getModuleLinux(module_name, 0) catch return null,
+ else => @compileError("getModule is not available for this target"),
+ };
+}
+
+fn getModuleWindows(comptime module_name: []const u8) ?[]const u8 {
+ const dll_name = module_name ++ ".dll";
+ const path_w = std.unicode.utf8ToUtf16LeStringLiteral(dll_name);
+ const dll = w.GetModuleHandleW(path_w) orelse return null;
+ var info: w.MODULEINFO = undefined;
+ if (GetModuleInformation(GetCurrentProcess(), dll, &info, @sizeOf(MODULEINFO)) == 0) {
+ return null;
+ }
+ const module: [*]const u8 = @ptrCast(dll);
+ return module[0..info.SizeOfImage];
+}
+
+fn getModuleLinux(comptime module_name: []const u8, permission: u32) !?[]const u8 {
+ const file_name = module_name ++ ".so";
+
+ const allocator = std.heap.page_allocator;
+ var file = try std.fs.openFileAbsolute("/proc/self/maps", .{ .mode = .read_only });
+ defer file.close();
+ var reader = file.reader();
+
+ var base: usize = 0;
+ var end: usize = 0;
+ var found = false;
+
+ while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', 4096)) |line| {
+ defer allocator.free(line);
+
+ // Example format:
+ // de228000-de229000 r--p 00000000 00:29 1026008 /usr/lib/libstdc++.so.6.0.33
+
+ if (!std.mem.endsWith(u8, line, file_name)) {
+ if (found) break;
+ continue;
+ }
+
+ const pos = line.len - file_name.len;
+ if (line[pos - 1] != '/' and line[pos - 1] != ' ') continue;
+
+ const dash = std.mem.indexOfScalar(u8, line, '-') orelse continue;
+ const space = std.mem.indexOfScalarPos(u8, line, dash + 1, ' ') orelse continue;
+
+ const perms_start = space + 1;
+ if (line.len < perms_start + 4) continue;
+ const read = line[perms_start];
+ const write = line[perms_start + 1];
+ const exec = line[perms_start + 2];
+ if (permission & 0b001 != 0 and read == '-') continue;
+ if (permission & 0b010 != 0 and write == '-') continue;
+ if (permission & 0b100 != 0 and exec == '-') continue;
+
+ const start_hex = line[0..dash];
+ const end_hex = line[dash + 1 .. space];
+
+ const start_addr = try std.fmt.parseInt(usize, start_hex, 16);
+ const end_addr = try std.fmt.parseInt(usize, end_hex, 16);
+
+ if (!found) {
+ base = start_addr;
+ end = end_addr;
+ found = true;
+ } else if (start_addr == end) {
+ end = end_addr;
+ } else {
+ break;
+ }
+ }
+
+ if (!found) return null;
+
+ const size = end - base;
+ const ptr: [*]const u8 = @ptrFromInt(base);
+ return ptr[0..size];
+}
+
+// Match call + add pattern
+// If matched, inst + len will be the start of the imm32
+pub fn matchPIC(inst: [*]const u8) ?u32 {
+ if (inst[0] != x86.Opcode.Op1.call) return null;
+ if (inst[5] == x86.Opcode.Op1.alumiw) {
+ const modrm = inst[6];
+ // mod must be 0b11 (register operand)
+ if ((modrm & 0b1100_0000) != 0b1100_0000) return null;
+ // reg/opcode must be 0b000 (ADD)
+ if ((modrm & 0b0011_1000) != 0b0000_0000) return null;
+
+ // rm should not be 0b100 (ESP)
+ // Although it's rare, compiler occasionally uses EBP for PIC
+ const rm = modrm & 0b0000_0111;
+ if (rm == 0b100) return null;
+ return 7;
+ } else if (inst[5] == x86.Opcode.Op1.addeaxi) {
+ return 6;
+ }
+ return null;
+}
+
+const GOT_pattern = mem.makePattern("E8 ?? ?? ?? ?? 05 ?? ?? ?? ?? 8D 80");
+
+pub fn findGOTAddr(module: []const u8) ?u32 {
+ if (mem.scanFirst(module, GOT_pattern)) |offset| {
+ const imm32 = mem.loadValue(u32, module.ptr + offset + 6);
+ return @intFromPtr(module.ptr + offset + 5) +% imm32;
+ }
+ return null;
+}
diff --git a/hook/src/x86.zig b/hook/src/x86.zig
new file mode 100644
index 0000000..60b9666
--- /dev/null
+++ b/hook/src/x86.zig
@@ -0,0 +1,670 @@
+const std = @import("std");
+const testing = std.testing;
+
+const makeHex = @import("utils.zig").makeHex;
+
+pub const Opcode = struct {
+ pub const Prefixes = struct {
+ pub const es = 0x26;
+ pub const cs = 0x2E;
+ pub const ss = 0x36;
+ pub const ds = 0x3E;
+ pub const fs = 0x64;
+ pub const gs = 0x65;
+ pub const opsz = 0x66;
+ pub const adsz = 0x67;
+ pub const lock = 0xF0;
+ pub const repn = 0xF2;
+ pub const rep = 0xF3;
+ };
+
+ pub const Op1No = struct {
+ pub const pushes = 0x06;
+ pub const popes = 0x07;
+ pub const pushcs = 0x0E;
+ pub const pushss = 0x16;
+ pub const popss = 0x17;
+ pub const pushds = 0x1E;
+ pub const popds = 0x1F;
+ pub const daa = 0x27;
+ pub const das = 0x2F;
+ pub const aaa = 0x37;
+ pub const aas = 0x3F;
+ pub const inceax = 0x40;
+ pub const incecx = 0x41;
+ pub const incedx = 0x42;
+ pub const incebx = 0x43;
+ pub const incesp = 0x44;
+ pub const incebp = 0x45;
+ pub const incesi = 0x46;
+ pub const incedi = 0x47;
+ pub const deceax = 0x48;
+ pub const dececx = 0x49;
+ pub const decedx = 0x4A;
+ pub const decebx = 0x4B;
+ pub const decesp = 0x4C;
+ pub const decebp = 0x4D;
+ pub const decesi = 0x4E;
+ pub const decedi = 0x4F;
+ pub const pusheax = 0x50;
+ pub const pushecx = 0x51;
+ pub const pushedx = 0x52;
+ pub const pushebx = 0x53;
+ pub const pushesp = 0x54;
+ pub const pushebp = 0x55;
+ pub const pushesi = 0x56;
+ pub const pushedi = 0x57;
+ pub const popeax = 0x58;
+ pub const popecx = 0x59;
+ pub const popedx = 0x5A;
+ pub const popebx = 0x5B;
+ pub const popesp = 0x5C;
+ pub const popebp = 0x5D;
+ pub const popesi = 0x5E;
+ pub const popedi = 0x5F;
+ pub const pusha = 0x60;
+ pub const popa = 0x61;
+ pub const nop = 0x90;
+ pub const xchgecxeax = 0x91;
+ pub const xchgedxeax = 0x92;
+ pub const xchgebxeax = 0x93;
+ pub const xchgespeax = 0x94;
+ pub const xchgebpeax = 0x95;
+ pub const xchgesieax = 0x96;
+ pub const xchgedieax = 0x97;
+ pub const cwde = 0x98;
+ pub const cdq = 0x99;
+ pub const wait = 0x9B;
+ pub const pushf = 0x9C;
+ pub const popf = 0x9D;
+ pub const sahf = 0x9E;
+ pub const lahf = 0x9F;
+ pub const movs8 = 0xA4;
+ pub const movsw = 0xA5;
+ pub const cmps8 = 0xA6;
+ pub const cmpsw = 0xA7;
+ pub const stos8 = 0xAA;
+ pub const stosd = 0xAB;
+ pub const lods8 = 0xAC;
+ pub const lodsd = 0xAD;
+ pub const scas8 = 0xAE;
+ pub const scasd = 0xAF;
+ pub const ret = 0xC3;
+ pub const leave = 0xC9;
+ pub const retf = 0xCB;
+ pub const int3 = 0xCC;
+ pub const into = 0xCE;
+ pub const xlat = 0xD7;
+ pub const cmc = 0xF5;
+ pub const clc = 0xF8;
+ pub const stc = 0xF9;
+ pub const cli = 0xFA;
+ pub const sti = 0xFB;
+ pub const cld = 0xFC;
+ pub const std = 0xFD;
+ };
+ pub const Op1I8 = struct {
+ pub const addali = 0x04;
+ pub const orali = 0x0C;
+ pub const adcali = 0x14;
+ pub const sbbali = 0x1C;
+ pub const andali = 0x24;
+ pub const subali = 0x2C;
+ pub const xorali = 0x34;
+ pub const cmpali = 0x3C;
+ pub const pushi8 = 0x6A;
+ pub const testali = 0xA8;
+ pub const jo = 0x70;
+ pub const jno = 0x71;
+ pub const jb = 0x72;
+ pub const jnb = 0x73;
+ pub const jz = 0x74;
+ pub const jnz = 0x75;
+ pub const jna = 0x76;
+ pub const ja = 0x77;
+ pub const js = 0x78;
+ pub const jns = 0x79;
+ pub const jp = 0x7A;
+ pub const jnp = 0x7B;
+ pub const jl = 0x7C;
+ pub const jnl = 0x7D;
+ pub const jng = 0x7E;
+ pub const jg = 0x7F;
+ pub const movali = 0xB0;
+ pub const movcli = 0xB1;
+ pub const movdli = 0xB2;
+ pub const movbli = 0xB3;
+ pub const movahi = 0xB4;
+ pub const movchi = 0xB5;
+ pub const movdhi = 0xB6;
+ pub const movbhi = 0xB7;
+ pub const int = 0xCD;
+ pub const amx = 0xD4;
+ pub const adx = 0xD5;
+ pub const loopnz = 0xE0;
+ pub const loopz = 0xE1;
+ pub const loop = 0xE2;
+ pub const jcxz = 0xE3;
+ pub const jmpi8 = 0xEB;
+ };
+ pub const Op1IW = struct {
+ pub const addeaxi = 0x05;
+ pub const oreaxi = 0x0D;
+ pub const adceaxi = 0x15;
+ pub const sbbeaxi = 0x1D;
+ pub const andeaxi = 0x25;
+ pub const subeaxi = 0x2D;
+ pub const xoreaxi = 0x35;
+ pub const cmpeaxi = 0x3D;
+ pub const pushiw = 0x68;
+ pub const testeaxi = 0xA9;
+ pub const moveaxi = 0xB8;
+ pub const movecxi = 0xB9;
+ pub const movedxi = 0xBA;
+ pub const movebxi = 0xBB;
+ pub const movespi = 0xBC;
+ pub const movebpi = 0xBD;
+ pub const movesii = 0xBE;
+ pub const movedii = 0xBF;
+ pub const call = 0xE8;
+ pub const jmpiw = 0xE9;
+ };
+ pub const Op1IWI = struct {
+ pub const movalii = 0xA0;
+ pub const moveaxii = 0xA1;
+ pub const moviial = 0xA2;
+ pub const moviieax = 0xA3;
+ };
+ pub const Op1I16 = struct {
+ pub const reti16 = 0xC2;
+ pub const retfi16 = 0xCA;
+ };
+ pub const Op1Mrm = struct {
+ pub const addmr8 = 0x00;
+ pub const addmrw = 0x01;
+ pub const addrm8 = 0x02;
+ pub const addrmw = 0x03;
+ pub const ormr8 = 0x08;
+ pub const ormrw = 0x09;
+ pub const orrm8 = 0x0A;
+ pub const orrmw = 0x0B;
+ pub const adcmr8 = 0x10;
+ pub const adcmrw = 0x11;
+ pub const adcrm8 = 0x12;
+ pub const adcrmw = 0x13;
+ pub const sbbmr8 = 0x18;
+ pub const sbbmrw = 0x19;
+ pub const sbbrm8 = 0x1A;
+ pub const sbbrmw = 0x1B;
+ pub const andmr8 = 0x20;
+ pub const andmrw = 0x21;
+ pub const andrm8 = 0x22;
+ pub const andrmw = 0x23;
+ pub const submr8 = 0x28;
+ pub const submrw = 0x29;
+ pub const subrm8 = 0x2A;
+ pub const subrmw = 0x2B;
+ pub const xormr8 = 0x30;
+ pub const xormrw = 0x31;
+ pub const xorrm8 = 0x32;
+ pub const xorrmw = 0x33;
+ pub const cmpmr8 = 0x38;
+ pub const cmpmrw = 0x39;
+ pub const cmprm8 = 0x3A;
+ pub const cmprmw = 0x3B;
+ pub const arpl = 0x63;
+ pub const testmr8 = 0x84;
+ pub const testmrw = 0x85;
+ pub const xchgmr8 = 0x86;
+ pub const xchgmrw = 0x87;
+ pub const movmr8 = 0x88;
+ pub const movmrw = 0x89;
+ pub const movrm8 = 0x8A;
+ pub const movrmw = 0x8B;
+ pub const movms = 0x8C;
+ pub const lea = 0x8D;
+ pub const movsm = 0x8E;
+ pub const popm = 0x8F;
+ pub const shiftm18 = 0xD0;
+ pub const shiftm1w = 0xD1;
+ pub const shiftmcl8 = 0xD2;
+ pub const shiftmclw = 0xD3;
+ pub const fltblk1 = 0xD8;
+ pub const fltblk2 = 0xD9;
+ pub const fltblk3 = 0xDA;
+ pub const fltblk4 = 0xDB;
+ pub const fltblk5 = 0xDC;
+ pub const fltblk6 = 0xDD;
+ pub const fltblk7 = 0xDE;
+ pub const fltblk8 = 0xDF;
+ pub const miscm8 = 0xFE;
+ pub const miscmw = 0xFF;
+ };
+ pub const Op1MrmI8 = struct {
+ pub const imulmi8 = 0x6B;
+ pub const alumi8 = 0x80;
+ pub const alumi8x = 0x82;
+ pub const alumi8s = 0x83;
+ pub const shiftmi8 = 0xC0;
+ pub const shiftmiw = 0xC1;
+ pub const movmi8 = 0xC6;
+ };
+ pub const Op1MrmIW = struct {
+ pub const imulmiw = 0x69;
+ pub const alumiw = 0x81;
+ pub const movmiw = 0xC7;
+ };
+
+ pub const Op1Extra = struct {
+ const enter = 0xC8;
+ const crazy8 = 0xF6;
+ const crazyw = 0xF7;
+ };
+
+ pub const Op2No = struct {
+ pub const rdtsc = 0x31;
+ pub const rdpmd = 0x33;
+ pub const sysenter = 0x34;
+ pub const pushfs = 0xA0;
+ pub const popfs = 0xA1;
+ pub const cpuid = 0xA2;
+ pub const pushgs = 0xA8;
+ pub const popgs = 0xA9;
+ pub const rsm = 0xAA;
+ pub const bswapeax = 0xC8;
+ pub const bswapecx = 0xC9;
+ pub const bswapedx = 0xCA;
+ pub const bswapebx = 0xCB;
+ pub const bswapesp = 0xCC;
+ pub const bswapebp = 0xCD;
+ pub const bswapesi = 0xCE;
+ pub const bswapedi = 0xCF;
+ pub const emms = 0x77;
+ };
+ pub const Op2IW = struct {
+ pub const joii = 0x80;
+ pub const jnoii = 0x81;
+ pub const jbii = 0x82;
+ pub const jnbii = 0x83;
+ pub const jzii = 0x84;
+ pub const jnzii = 0x85;
+ pub const jnaii = 0x86;
+ pub const jaii = 0x87;
+ pub const jsii = 0x88;
+ pub const jnsii = 0x89;
+ pub const jpii = 0x8A;
+ pub const jnpii = 0x8B;
+ pub const jlii = 0x8C;
+ pub const jnlii = 0x8D;
+ pub const jngii = 0x8E;
+ pub const jgii = 0x8F;
+ };
+ pub const Op2Mrm = struct {
+ pub const nop = 0x0D;
+ pub const hints1 = 0x18;
+ pub const hints2 = 0x19;
+ pub const hints3 = 0x1A;
+ pub const hints4 = 0x1B;
+ pub const hints5 = 0x1C;
+ pub const hints6 = 0x1D;
+ pub const hints7 = 0x1E;
+ pub const hints8 = 0x1F;
+ pub const cmovo = 0x40;
+ pub const cmovno = 0x41;
+ pub const cmovb = 0x42;
+ pub const cmovnb = 0x43;
+ pub const cmovz = 0x44;
+ pub const cmovnz = 0x45;
+ pub const cmovna = 0x46;
+ pub const cmova = 0x47;
+ pub const cmovs = 0x48;
+ pub const cmovns = 0x49;
+ pub const cmovp = 0x4A;
+ pub const cmovnp = 0x4B;
+ pub const cmovl = 0x4C;
+ pub const cmovnl = 0x4D;
+ pub const cmovng = 0x4E;
+ pub const cmovg = 0x4F;
+ pub const seto = 0x90;
+ pub const setno = 0x91;
+ pub const setb = 0x92;
+ pub const setnb = 0x93;
+ pub const setz = 0x94;
+ pub const setnz = 0x95;
+ pub const setna = 0x96;
+ pub const seta = 0x97;
+ pub const sets = 0x98;
+ pub const setns = 0x99;
+ pub const setp = 0x9A;
+ pub const setnp = 0x9B;
+ pub const setl = 0x9C;
+ pub const setnl = 0x9D;
+ pub const setng = 0x9E;
+ pub const setg = 0x9F;
+ pub const btmr = 0xA3;
+ pub const shldmrcl = 0xA5;
+ pub const bts = 0xAB;
+ pub const shrdmrcl = 0xAD;
+ pub const misc = 0xAE;
+ pub const imul = 0xAF;
+ pub const cmpxchg8 = 0xB0;
+ pub const cmpxchgw = 0xB1;
+ pub const movzx8 = 0xB6;
+ pub const movzxw = 0xB7;
+ pub const popcnt = 0xB8;
+ pub const btcrm = 0xBB;
+ pub const bsf = 0xBC;
+ pub const bsr = 0xBD;
+ pub const movsx8 = 0xBE;
+ pub const movsxw = 0xBF;
+ pub const xaddrm8 = 0xC0;
+ pub const xaddrmw = 0xC1;
+ pub const cmpxchg64 = 0xC7;
+ pub const movrm128 = 0x10;
+ pub const movmr128 = 0x11;
+ pub const movlrm = 0x12;
+ pub const movlmr = 0x13;
+ pub const unpckl = 0x14;
+ pub const unpckh = 0x15;
+ pub const movhrm = 0x16;
+ pub const movhmr = 0x17;
+ pub const movarm = 0x28;
+ pub const movamr = 0x29;
+ pub const cvtif64 = 0x2A;
+ pub const movnt = 0x2B;
+ pub const cvtft64 = 0x2C;
+ pub const cvtfi64 = 0x2D;
+ pub const ucomi = 0x2E;
+ pub const comi = 0x2F;
+ pub const movmsk = 0x50;
+ pub const sqrt = 0x51;
+ pub const rsqrt = 0x52;
+ pub const rcp = 0x53;
+ pub const and_ = 0x54;
+ pub const andn = 0x55;
+ pub const or_ = 0x56;
+ pub const xor = 0x57;
+ pub const add = 0x58;
+ pub const mul = 0x59;
+ pub const cvtff128 = 0x5A;
+ pub const cvtfi128 = 0x5B;
+ pub const sub = 0x5C;
+ pub const div = 0x5D;
+ pub const min = 0x5E;
+ pub const max = 0x5F;
+ pub const punpcklbw = 0x60;
+ pub const punpcklbd = 0x61;
+ pub const punpckldq = 0x62;
+ pub const packsswb = 0x63;
+ pub const pcmpgtb = 0x64;
+ pub const pcmpgtw = 0x65;
+ pub const pcmpgtd = 0x66;
+ pub const packuswb = 0x67;
+ pub const punpckhbw = 0x68;
+ pub const punpckhwd = 0x69;
+ pub const punpckhdq = 0x6A;
+ pub const packssdw = 0x6B;
+ pub const punpcklqdq = 0x6C;
+ pub const punpckhqdq = 0x6D;
+ pub const movdrm = 0x6E;
+ pub const movqrm = 0x6F;
+ pub const pcmpeqb = 0x74;
+ pub const pcmpeqw = 0x75;
+ pub const pcmpeqd = 0x76;
+ pub const movdmr = 0x7E;
+ pub const movqmr = 0x7F;
+ pub const movnti = 0xC3;
+ pub const addsub = 0xD0;
+ pub const psrlw = 0xD1;
+ pub const psrld = 0xD2;
+ pub const psrlq = 0xD3;
+ pub const paddq = 0xD4;
+ pub const pmullw = 0xD5;
+ pub const movqrr = 0xD6;
+ pub const pmovmskb = 0xD7;
+ pub const psubusb = 0xD8;
+ pub const psubusw = 0xD9;
+ pub const pminub = 0xDA;
+ pub const pand = 0xDB;
+ pub const paddusb = 0xDC;
+ pub const paddusw = 0xDD;
+ pub const pmaxub = 0xDE;
+ pub const pandn = 0xDF;
+ pub const pavgb = 0xE0;
+ pub const psraw = 0xE1;
+ pub const psrad = 0xE2;
+ pub const pavgw = 0xE3;
+ pub const pmulhuw = 0xE4;
+ pub const pmulhw = 0xE5;
+ pub const cvtq = 0xE6;
+ pub const movntq = 0xE7;
+ pub const psubsb = 0xE8;
+ pub const psubsw = 0xE9;
+ pub const pminsb = 0xEA;
+ pub const pminsw = 0xEB;
+ pub const paddsb = 0xEC;
+ pub const paddsw = 0xED;
+ pub const pmaxsw = 0xEE;
+ pub const pxor = 0xEF;
+ pub const lddqu = 0xF0;
+ pub const psllw = 0xF1;
+ pub const pslld = 0xF2;
+ pub const psllq = 0xF3;
+ pub const pmuludq = 0xF4;
+ pub const pmaddwd = 0xF5;
+ pub const psabdw = 0xF6;
+ pub const maskmovq = 0xF7;
+ pub const psubb = 0xF8;
+ pub const psubw = 0xF9;
+ pub const psubd = 0xFA;
+ pub const psubq = 0xFB;
+ pub const paddb = 0xFC;
+ pub const paddw = 0xFD;
+ pub const paddd = 0xFE;
+ };
+ pub const Op2MrmI8 = struct {
+ pub const shldmri = 0xA4;
+ pub const shrdmri = 0xAC;
+ pub const btxmi = 0xBA;
+ pub const pshuf = 0x70;
+ pub const pswi = 0x71;
+ pub const psdi = 0x72;
+ pub const psqi = 0x73;
+ pub const cmpsi = 0xC2;
+ pub const pinsrw = 0xC4;
+ pub const pextrw = 0xC5;
+ pub const shuf = 0xC6;
+ };
+
+ pub const op2_byte = 0x0F;
+ pub const op3_1 = 0x38;
+ pub const op3_2 = 0x3A;
+ pub const op3dnow = 0x0F;
+};
+
+// Constructs a ModRM byte
+pub fn modrm(mod: u8, reg: u8, rm: u8) u8 {
+ return mod << 6 | reg << 3 | rm;
+}
+
+fn mrmsib(b: [*]const u8, address_len: usize) usize {
+ if (address_len == 4 or b[0] & 0xC0 != 0) {
+ const sib: usize = if (address_len == 4 and b[0] < 0xC0 and (b[0] & 7) == 4) 1 else 0;
+ if ((b[0] & 0xC0) == 0x40) {
+ return 2 + sib;
+ }
+ if ((b[0] & 0xC0) == 0x00) {
+ if ((b[0] & 7) != 5) {
+ if (sib == 1 and (b[1] & 7) == 5) {
+ return if (b[0] & 0x40 != 0) 3 else 6;
+ }
+ return 1 + sib;
+ }
+ return 1 + address_len + sib;
+ }
+ if ((b[0] & 0xC0) == 0x80) {
+ return 1 + address_len + sib;
+ }
+ }
+ if (address_len == 2 and (b[0] & 0xC7) == 0x06) {
+ return 3;
+ }
+ return 1;
+}
+
+fn is_field(comptime T: type, byte: u8) bool {
+ @setEvalBranchQuota(100000);
+ for (@typeInfo(T).@"struct".decls) |decl| {
+ const decl_ptr = &@field(T, decl.name);
+ if (decl_ptr.* == byte) {
+ return true;
+ }
+ }
+ return false;
+}
+
+pub fn x86_len(address: [*]const u8) !usize {
+ var b = address;
+
+ var prefix_len: usize = 0;
+ var operand_len: usize = 4;
+ var address_len: usize = 4;
+
+ // prefixes
+ while (prefix_len < 14 and switch (b[0]) {
+ inline 0x00...0xFF => |byte| blk: {
+ break :blk comptime is_field(Opcode.Prefixes, byte);
+ },
+ }) : ({
+ prefix_len += 1;
+ b += 1;
+ }) {
+ if (b[0] == Opcode.Prefixes.opsz) {
+ operand_len = 2;
+ } else if (b[0] == Opcode.Prefixes.adsz) {
+ address_len = 2;
+ }
+ }
+
+ // opcode
+ return switch (b[0]) {
+ Opcode.op2_byte => switch (b[1]) {
+ Opcode.op3_1,
+ Opcode.op3_2,
+ Opcode.op3dnow,
+ => error.UnsupportedInstruction,
+ inline else => |byte| blk: {
+ if (comptime is_field(Opcode.Op2No, byte)) {
+ break :blk prefix_len + 2;
+ }
+ if (comptime is_field(Opcode.Op2IW, byte)) {
+ break :blk prefix_len + 2 + operand_len;
+ }
+ if (comptime is_field(Opcode.Op2Mrm, byte)) {
+ break :blk prefix_len + 2 + mrmsib(b + 2, address_len);
+ }
+ if (comptime is_field(Opcode.Op2MrmI8, byte)) {
+ operand_len = 1;
+ break :blk prefix_len + 2 + operand_len + mrmsib(b + 2, address_len);
+ }
+ break :blk error.UnsupportedInstruction;
+ },
+ },
+ inline else => |byte| blk: {
+ if (comptime is_field(Opcode.Op1No, byte)) {
+ break :blk prefix_len + 1;
+ }
+ if (comptime is_field(Opcode.Op1I8, byte)) {
+ operand_len = 1;
+ break :blk prefix_len + 1 + operand_len;
+ }
+ if (comptime is_field(Opcode.Op1IW, byte)) {
+ break :blk prefix_len + 1 + operand_len;
+ }
+ if (comptime is_field(Opcode.Op1IWI, byte)) {
+ break :blk prefix_len + 1 + address_len;
+ }
+ if (comptime is_field(Opcode.Op1I16, byte)) {
+ break :blk prefix_len + 3;
+ }
+ if (comptime is_field(Opcode.Op1Mrm, byte)) {
+ break :blk prefix_len + 1 + mrmsib(b + 1, address_len);
+ }
+ if (comptime is_field(Opcode.Op1MrmI8, byte)) {
+ operand_len = 1;
+ break :blk prefix_len + 1 + operand_len + mrmsib(b + 1, address_len);
+ }
+ if (comptime is_field(Opcode.Op1MrmIW, byte)) {
+ break :blk prefix_len + 1 + operand_len + mrmsib(b + 1, address_len);
+ }
+ if (byte == Opcode.Op1Extra.enter) {
+ break :blk prefix_len + 4;
+ }
+ if (byte == Opcode.Op1Extra.crazy8 or byte == Opcode.Op1Extra.crazyw) {
+ if (byte == Opcode.Op1Extra.crazy8) {
+ operand_len = 1;
+ }
+
+ if ((b[1] & 0x38) >= 0x10) {
+ operand_len = 0;
+ }
+
+ break :blk prefix_len + 1 + operand_len + mrmsib(b + 1, address_len);
+ }
+ break :blk error.UnsupportedInstruction;
+ },
+ };
+}
+
+test "Simple x86 instruction lengths" {
+ const nop = makeHex("90");
+ try testing.expectEqual(1, try x86_len(nop.ptr));
+ const push_eax = makeHex("50");
+ try testing.expectEqual(1, try x86_len(push_eax.ptr));
+ const mov_eax = makeHex("B8 78 56 34 12");
+ try testing.expectEqual(5, try x86_len(mov_eax.ptr));
+ const add_mem_eax = makeHex("00 00");
+ try testing.expectEqual(2, try x86_len(add_mem_eax.ptr));
+ const mov_ax = makeHex("66 B8 34 12");
+ try testing.expectEqual(4, try x86_len(mov_ax.ptr));
+ const add_mem_disp32 = makeHex("00 80 78 56 34 12");
+ try testing.expectEqual(6, try x86_len(add_mem_disp32.ptr));
+ const add_eax_imm = makeHex("05 78 56 34 12");
+ try testing.expectEqual(5, try x86_len(add_eax_imm.ptr));
+}
+
+test "The \"crazy\" instructions should be given correct lengths" {
+ const test8 = makeHex("F6 05 12 34 56 78 12");
+ try testing.expectEqual(7, try x86_len(test8.ptr));
+ const test16 = makeHex("66 F7 05 12 34 56 78 12");
+ try testing.expectEqual(9, try x86_len(test16.ptr));
+ const test32 = makeHex("F7 05 12 34 56 78 12 34 56 78");
+ try testing.expectEqual(10, try x86_len(test32.ptr));
+ const not8 = makeHex("F6 15 12 34 56 78");
+ try testing.expectEqual(6, try x86_len(not8.ptr));
+ const not16 = makeHex("66 F7 15 12 34 56 78");
+ try testing.expectEqual(7, try x86_len(not16.ptr));
+ const not32 = makeHex("F7 15 12 34 56 78");
+ try testing.expectEqual(6, try x86_len(not32.ptr));
+}
+
+test "SIB bytes should be decoded correctly" {
+ const fstp = makeHex("D9 1C 24");
+ try testing.expectEqual(3, try x86_len(fstp.ptr));
+}
+
+test "mov AL, moff8 instructions should be decoded correctly" {
+ const mov_moff8_al = makeHex("A2 DA 78 B4 0D");
+ try testing.expectEqual(5, try x86_len(mov_moff8_al.ptr));
+ const mov_al_moff8 = makeHex("A0 28 DF 5C 66");
+ try testing.expectEqual(5, try x86_len(mov_al_moff8.ptr));
+}
+
+test "16-bit MRM instructions should be decoded correctly" {
+ const fiadd_off16 = makeHex("67 DA 06 DF 11");
+ try testing.expectEqual(5, try x86_len(fiadd_off16.ptr));
+ const fld_tword = makeHex("67 DB 2E 99 C4");
+ try testing.expectEqual(5, try x86_len(fld_tword.ptr));
+ const add_off16_bl = makeHex("67 00 1E F5 BB");
+ try testing.expectEqual(5, try x86_len(add_off16_bl.ptr));
+}
diff --git a/hook/src/zhook.zig b/hook/src/zhook.zig
new file mode 100644
index 0000000..82e184b
--- /dev/null
+++ b/hook/src/zhook.zig
@@ -0,0 +1,9 @@
+pub const x86 = @import("x86.zig");
+pub const mem = @import("mem.zig");
+pub const Hook = @import("Hook.zig");
+pub const HookManager = @import("HookManager.zig");
+pub const utils = @import("utils.zig");
+
+test {
+ @import("std").testing.refAllDecls(@This());
+}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..3d40078
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,608 @@
+const std = @import("std");
+const w = std.os.windows;
+const hook = @import("hook");
+
+const c = @cImport({
+ @cInclude("libavcodec/avcodec.h");
+ @cInclude("libavformat/avformat.h");
+ @cInclude("libswscale/swscale.h");
+ @cInclude("libswresample/swresample.h");
+ @cInclude("libavutil/opt.h");
+});
+
+// for whatever reason, winsock just doesn't export this but libsrt expects it
+// so we can just throw it in here
+export const in6addr_any: [32]u8 = .{ 0 } ** 32;
+
+fn virtualFunc(This: type, comptime func_name: []const u8, Params: []const type, RT: type) fn (*This, anytype) RT {
+ var params: [Params.len + 1]std.builtin.Type.Fn.Param = undefined;
+ params[0] = .{ .is_generic = false, .is_noalias = false, .type = *This };
+ for (params[1..], Params) |*param, Param| param.* = .{
+ .is_generic = false,
+ .is_noalias = false,
+ .type = Param,
+ };
+ const Fn = *@Type(.{
+ .@"fn" = .{
+ .is_generic = false,
+ .is_var_args = false,
+ .params = &params,
+ .return_type = RT,
+ .calling_convention = .{ .x86_thiscall = .{} },
+ },
+ });
+
+ return struct {
+ pub fn func(this: *This, args: anytype) RT {
+ const vtable: *[*]Fn = @ptrCast(@constCast(this));
+ const type_name = comptime name: {
+ const fqn = @typeName(This);
+ // find the last .
+ var iter = std.mem.splitScalar(u8, fqn, '.');
+ var shortName: []const u8 = undefined;
+ while (iter.next()) |seg| shortName = seg;
+ break :name shortName;
+ };
+ const idx = @field(@field(gamedata, type_name), "vtidx_" ++ func_name);
+ const target_location = vtable.*[idx];
+ std.log.debug("calling {s}::{s} @ 0x{x}",
+ .{ type_name, func_name, @intFromPtr(target_location) });
+ return @call(.auto, target_location, .{this} ++ args);
+ }
+ }.func;
+}
+
+const CreateInterfaceFn = *const fn ([*:0]const u8, ?*c_int) callconv(.c) *align(4) anyopaque;
+
+const GameData = struct {
+ const HL2PRE20TH = GameData{
+ .IVEngineClient = .{
+ .vtidx_IsPlayingDemo = 76,
+ .vtidx_ClientCmd = 7,
+ .vtidx_Time = 14,
+ .vtidx_GetAppID = 97,
+ .iface_name = "VEngineClient014",
+ },
+ .off_S_TransferStereo16 = 0x7fac0,
+ };
+
+ IVEngineClient: struct {
+ vtidx_IsPlayingDemo: usize,
+ vtidx_Time: usize,
+ vtidx_GetAppID: usize,
+ vtidx_ClientCmd: usize,
+ iface_name: [:0]const u8,
+ },
+ CDemoPlayer: struct {
+ vtidx_GetDemoFile: usize = 2,
+ vtidx_GetPlaybackTicks: usize = 3,
+ vtidx_GetTotalTicks: usize = 4,
+ vtidx_StartPlayback: usize = 5,
+ vtidx_IsPlayingBack: usize = 6,
+ } = .{},
+ CEngineAPI: struct {
+ iface_name: [:0]const u8 = "VENGINE_LAUNCHER_API_VERSION004",
+ vtidx_SetEngineWindow: usize = 7,
+ } = .{},
+ CEngineVGui: struct {
+ iface_name: [:0]const u8 = "VEngineVGui001",
+ vtidx_Init: usize = 3,
+ } = .{},
+ CEngineTool: struct {
+ iface_name: [:0]const u8 = "VENGINETOOL003",
+ vtidx_StartMovieRecording: usize = 65,
+ } = .{},
+ // TODO: implement a good way to find this
+ off_S_TransferStereo16: usize,
+ CVideoMode_MaterialSystem: struct {
+ vtidx_WriteMovieFrame: usize = 27,
+ vtidx_ReadScreenPixels: usize = 30,
+ } = .{},
+};
+
+const IVEngineClient = extern struct {
+ vt: [*]usize,
+ const isRecordingDemo =
+ virtualFunc(IVEngineClient, "IsPlayingDemo", &.{}, bool);
+ const clientCmd =
+ virtualFunc(IVEngineClient, "ClientCmd", &.{[*:0]const u8}, void);
+ const time = virtualFunc(IVEngineClient, "Time", &.{}, f32);
+ const getAppId = virtualFunc(IVEngineClient, "GetAppID", &.{}, c_int);
+};
+
+const CDemoPlayer = extern struct {
+ vt: [*]usize,
+ const getPlaybackTicks =
+ virtualFunc(CDemoPlayer, "GetPlaybackTicks", &.{}, c_int);
+ const getTotalTicks =
+ virtualFunc(CDemoPlayer, "GetTotalTicks", &.{}, c_int);
+ const startPlayback =
+ virtualFunc(CDemoPlayer, "StartPlayback", &.{[*]const u8, bool}, bool);
+ const isPlayingBack =
+ virtualFunc(CDemoPlayer, "IsPlayingBack", &.{}, bool);
+};
+
+const CEngineAPI = extern struct {
+ vt: [*]usize,
+};
+
+const CEngineVGui = extern struct {
+ vt: [*]*const anyopaque,
+};
+
+const CVideoMode_MaterialSystem = extern struct {
+ vt: [*]*const anyopaque,
+ const readScreenPixels = virtualFunc(CVideoMode_MaterialSystem, "ReadScreenPixels",
+ &.{c_int, c_int, c_int, c_int, *anyopaque, i32}, void);
+};
+
+const CEngineTool = extern struct {
+ vt: [*]*const anyopaque,
+};
+
+const Sample = extern struct { left: i32, right: i32 };
+
+var api: struct {
+ engine_factory: CreateInterfaceFn,
+ engclient: *IVEngineClient,
+ demoplayer: *CDemoPlayer,
+ engineapi: *CEngineAPI,
+ videomode: *CVideoMode_MaterialSystem,
+ engvgui: *CEngineVGui,
+ enginetool: *CEngineTool,
+ S_TransferStereo16: *const fn(*anyopaque, [*]Sample, u32, u32) callconv(.c) void,
+ movieinfo: *MovieInfo,
+
+ fn init(this: *@This()) !void {
+ var engine_dll = try std.DynLib.open("engine");
+ defer engine_dll.close();
+
+ const engine_factory = engine_dll.lookup(CreateInterfaceFn, "CreateInterface").?;
+ this.engclient =
+ @ptrCast(engine_factory(gamedata.IVEngineClient.iface_name, null));
+ this.engineapi =
+ @ptrCast(engine_factory(gamedata.CEngineAPI.iface_name, null));
+ this.engvgui =
+ @ptrCast(engine_factory(gamedata.CEngineVGui.iface_name, null));
+ this.enginetool =
+ @ptrCast(engine_factory(gamedata.CEngineTool.iface_name, null));
+ // CEngineClient::IsPlayingDemo immediately loads &demoplayer into ECX
+ const isplayingdemo: [*]u8 =
+ @ptrFromInt(this.engclient.vt[gamedata.IVEngineClient.vtidx_IsPlayingDemo]);
+ std.debug.assert(isplayingdemo[0] == hook.x86.Opcode.Op1Mrm.movrmw);
+ std.debug.assert(isplayingdemo[1] == hook.x86.modrm(0, 1, 5));
+ // +2 is the first byte past the mov opcode and modr/m byte
+ this.demoplayer = @as(*align(1) **CDemoPlayer, @ptrCast(isplayingdemo + 2)).*.*;
+ std.log.debug("demoplayer: {x}", .{@intFromPtr(this.demoplayer)});
+ // CEngineAPI::SetWindow does 2 virtual calls, one to unset the window
+ // and one to apply the new one. videomode is loaded into ECX for the
+ // second one
+ var p: [*]u8 =
+ @ptrFromInt(this.engineapi.vt[gamedata.CEngineAPI.vtidx_SetEngineWindow]);
+ var mov_ecx_count: u8 = 0;
+ while (hook.x86.x86_len(p)) |len| {
+ if (p[0] == hook.x86.Opcode.Op1Mrm.movrmw and
+ p[1] == hook.x86.modrm(0, 1, 5))
+ mov_ecx_count += 1;
+ if (mov_ecx_count == 2) {
+ this.videomode = @as(*align(1) **CVideoMode_MaterialSystem, @ptrCast(p + 2)).*.*;
+ break;
+ }
+ p += len;
+ } else |err| return err;
+ std.log.debug("videomode: {x}", .{ @intFromPtr(this.videomode) });
+ // CEngineTool::StartMovieRecording first checks if there's currently a
+ // movie playing. Check first call.
+ p = @constCast(@ptrCast(this.enginetool.vt[gamedata.CEngineTool.vtidx_StartMovieRecording]));
+ while (hook.x86.x86_len(p)) |len| {
+ if (p[0] == hook.x86.Opcode.Op1IW.call) {
+ const offset = @as(*align(1) usize, @ptrCast(p + 1)).*;
+ p = @ptrFromInt(@addWithOverflow(@intFromPtr(p + 5), offset)[0]);
+ break;
+ }
+ p += len;
+ } else |err| return err;
+ // CL_IsRecordingMovie checks the first byte of the first field of
+ // movieinfo, which, well, is just the pointer to the whole thing.
+ std.log.debug("CL_IsRecordingMovie: {*}", .{p});
+ std.debug.assert(p[0] == hook.x86.Opcode.Op1MrmI8.alumi8);
+ std.debug.assert(p[1] == hook.x86.modrm(0, 7, 5));
+ this.movieinfo = @as(*align(1) *MovieInfo, @ptrCast(p + 2)).*;
+ std.log.debug("movieinfo: {x}", .{ @intFromPtr(this.movieinfo) });
+ }
+} = undefined;
+
+// example
+const gamedata = GameData.HL2PRE20TH;
+
+const Config = struct {
+ pub const GameBuild = enum { Hl2Pre20th };
+
+ build: GameBuild,
+ width: u16 = 1920,
+ height: u16 = 1080,
+ framerate: u9 = 60,
+ bitrate: u32 = 20000,
+ mod: []const u8 = "hl2",
+ video_codec: [:0]const u8 = "libx264",
+ extraargs: []const u8 = "",
+
+ pub fn readFromFile(path: []const u8, allocator: std.mem.Allocator) !Config {
+ const cfg = try std.fs.cwd()
+ .readFileAllocOptions(allocator, path, 4096, null, .@"1", 0);
+ var diag: std.zon.parse.Diagnostics = .{};
+ return std.zon.parse.fromSlice(Config, allocator, cfg, &diag, .{}) catch |err|
+ switch (err) {
+ error.ParseZon => {
+ var stderr = std.fs.File.stderr().writer(&.{});
+ try diag.format(&stderr.interface);
+ return err;
+ },
+ else => return err,
+ };
+ }
+};
+
+const MovieInfo = extern struct {
+ name: [256:0]u8,
+ curframe: c_int,
+ kind: c_int,
+ jpeg_quality: c_int,
+};
+
+var render: struct {
+ orig_WriteMovieFrame: *const @TypeOf(hook_WriteMovieFrame),
+ orig_S_TransferStereo16: *const @TypeOf(hook_S_TransferStereo16),
+
+ v: struct {
+ codec: *const c.AVCodec,
+ ctx: *c.AVCodecContext,
+ stream: *c.AVStream,
+ yuv_frame: *c.AVFrame,
+ sws: *c.SwsContext,
+ nextpts: u32,
+ },
+
+ a: struct {
+ codec: *const c.AVCodec,
+ ctx: *c.AVCodecContext,
+ swr: *c.SwrContext,
+ frame: *c.AVFrame,
+ pcm_frame: *c.AVFrame,
+ stream: *c.AVStream,
+ nextpts: u32,
+ frame_idx: u32,
+ },
+ format: *c.AVFormatContext,
+ pixels: []Bgr,
+
+ const Bgr = extern struct { b: u8, g: u8, r: u8 };
+
+ // HEY!! 'this' in this function is NOT the render struct!
+ fn hook_WriteMovieFrame(this: *CVideoMode_MaterialSystem,
+ _: *MovieInfo) callconv(.{ .x86_thiscall = .{} }) void {
+ std.log.debug("frame!", .{});
+ // 3: source engine IMAGE_FORMAT_BGR888
+ this.readScreenPixels(.{0, 0, config.width, config.height, render.pixels.ptr, 3});
+ const stride: c_int = config.width * @sizeOf(Bgr);
+ // convert to yuv
+ _ = c.sws_scale(render.v.sws, @ptrCast(&render.pixels.ptr), &stride, 0,
+ config.height, &render.v.yuv_frame.data, &render.v.yuv_frame.linesize);
+
+ render.v.yuv_frame.pts = render.v.nextpts; render.v.nextpts += 1;
+ render.v.yuv_frame.time_base = render.v.ctx.time_base;
+ // 1 'time_base'
+ render.v.yuv_frame.duration = 1;
+
+ fftest("send a frame for encoding",
+ c.avcodec_send_frame(render.v.ctx, render.v.yuv_frame)) catch return;
+
+ var pkt: ?*c.AVPacket = c.av_packet_alloc() orelse {
+ std.log.err("failed to alloc packet!", .{});
+ return;
+ };
+
+ var r: c_int = 0;
+ while (true) {
+ r = c.avcodec_receive_packet(render.v.ctx, pkt);
+ if (r == c.AVERROR(c.EAGAIN) or r == c.AVERROR(c.AVERROR_EOF))
+ break;
+ fftest("encode a frame", r) catch return;
+
+ pkt.?.stream_index = render.v.stream.index;
+ c.av_packet_rescale_ts(pkt, render.v.ctx.time_base, render.v.stream.time_base);
+ fftest("write frame to file", c.av_interleaved_write_frame(render.format, pkt)) catch return;
+ }
+ c.av_packet_unref(pkt);
+ c.av_packet_free(&pkt);
+ }
+
+
+ fn hook_S_TransferStereo16(p: *anyopaque, samples_ptr: [*]Sample, start: u32, end: u32) callconv(.c) void {
+ const samples = samples_ptr[0..(end - start)/2];
+ for (samples) |_| {
+ //render.a.pcm_frame.data[0][]
+ }
+ render.orig_S_TransferStereo16(p, samples_ptr, start, end);
+ }
+
+ fn fftest(msg: []const u8, v: c_int) !void {
+ if (v != 0) {
+ var buf: [256:0]u8 = .{0} ** 256;
+ _ = c.av_strerror(v, &buf, 256);
+ std.log.err("failed to {s} ({s})", .{msg, buf[0..:0].ptr});
+ return error.FFmpegError;
+ }
+ }
+
+ pub fn init(this: *@This(), allocator: std.mem.Allocator) !void {
+ std.log.info("available video codecs:", .{});
+ var i: ?*anyopaque = null;
+ while (c.av_codec_iterate(&i)) |codec| {
+ if (codec.*.type == c.AVMEDIA_TYPE_VIDEO) {
+ std.log.info("\t{s} ", .{ codec.*.name.? });
+ }
+ }
+ // make sure the game knows we are rendering a video
+ api.movieinfo.name[0] = 'a';
+ // but we don't want it to do anything else
+ api.movieinfo.kind = 0;
+
+ var r: i32 = 0;
+ var format: ?*c.AVFormatContext = null;
+ r = c.avformat_alloc_output_context2(&format, null, null, "test.mp4");
+ this.format = format orelse {
+ std.log.err("couldn't create output context! ({})", .{r});
+ return error.FFmpegError;
+ };
+
+ // create video codec
+ this.v.codec = c.avcodec_find_encoder_by_name(config.video_codec) orelse {
+ std.log.err("could't find video encoder", .{});
+ return error.FFmpegError;
+ };
+ this.v.ctx = c.avcodec_alloc_context3(this.v.codec) orelse {
+ std.log.err("couldn't alloc video codec context", .{});
+ return error.FFmpegError;
+ };
+ this.v.ctx.width = config.width;
+ this.v.ctx.height = config.height;
+ this.v.ctx.pix_fmt = c.AV_PIX_FMT_YUV420P;
+ this.v.ctx.time_base = c.av_make_q(1, config.framerate);
+ this.v.ctx.framerate = c.av_make_q(config.framerate, 1);
+ this.v.ctx.gop_size = 2 * config.framerate;
+ this.v.ctx.bit_rate = 1000 * config.bitrate;
+ //this.v.ctx.rc_max_rate = this.v.ctx.bit_rate;
+ try fftest("set crf", c.av_opt_set_int(this.v.ctx.priv_data, "crf", 24, 0));
+ // TODO: extra encoder arguments
+ r = c.avcodec_open2(this.v.ctx, this.v.codec, null);
+ if (r < 0) {
+ std.log.err("failed to create video encoder ({})", .{ r });
+ return error.FFmpegError;
+ }
+
+ // stream
+ const stream = c.avformat_new_stream(this.format, this.v.codec);
+ if (stream == null) {
+ std.log.err("failed to create video stream", .{});
+ return error.FFmpegError;
+ }
+ this.v.stream = stream.?;
+
+ r = c.avcodec_parameters_from_context(this.v.stream.codecpar, this.v.ctx);
+ if (r < 0) {
+ std.log.err("failed to copy params from video context ({})", .{r});
+ return error.FFmpegError;
+ }
+ this.v.stream.time_base = this.v.ctx.time_base;
+ this.v.stream.avg_frame_rate = this.v.ctx.framerate;
+
+ // frame
+ this.v.yuv_frame = c.av_frame_alloc();
+ this.v.yuv_frame.width = config.width;
+ this.v.yuv_frame.height = config.height;
+ this.v.yuv_frame.color_range = c.AVCOL_RANGE_JPEG;
+ this.v.yuv_frame.time_base = this.v.ctx.time_base;
+ this.v.yuv_frame.format = this.v.ctx.pix_fmt;
+
+ r = c.av_frame_get_buffer(this.v.yuv_frame, 0);
+ if (r < 0) {
+ std.log.err("failed to alloc frame buffer ({})", .{r});
+ return error.FFmpegError;
+ }
+
+ // sws
+ this.v.sws = c.sws_getContext(config.width, config.height,
+ c.AV_PIX_FMT_BGR24, config.width, config.height,
+ this.v.yuv_frame.format, 0, null, null, null) orelse {
+ std.log.err("failed to create swscontext", .{});
+ return error.FFmpegError;
+ };
+
+ // audio codec
+ const acodec = c.avcodec_find_encoder(c.AV_CODEC_ID_AAC);
+ if (acodec == null) {
+ std.log.err("couldn't find audio encoder", .{});
+ return error.FFmpegError;
+ }
+ this.a.codec = acodec;
+ this.a.ctx = c.avcodec_alloc_context3(this.a.codec) orelse {
+ std.log.err("couldn't create audio encoder context", .{});
+ return error.FFmpegError;
+ };
+ this.a.ctx.bit_rate = 256000;
+ this.a.ctx.sample_fmt = c.AV_SAMPLE_FMT_FLTP;
+ this.a.ctx.sample_rate = 44100;
+ this.a.ctx.profile = c.FF_PROFILE_AAC_MAIN;
+ this.a.ctx.time_base = c.av_make_q(1, 44100);
+
+ this.a.ctx.ch_layout.order = c.AV_CHANNEL_ORDER_NATIVE;
+ this.a.ctx.ch_layout.nb_channels = 2;
+ this.a.ctx.ch_layout.u.map = null; // not custom channel order
+ this.a.ctx.ch_layout.u.mask = c.AV_CH_LAYOUT_STEREO;
+ this.a.ctx.ch_layout.@"opaque" = null;
+
+ try fftest("open audio codec", c.avcodec_open2(this.a.ctx, this.a.codec, null));
+
+ // audio resampler
+ this.a.swr = c.swr_alloc() orelse {
+ std.log.err("failed to alloc swr context", .{});
+ return error.FFmpegError;
+ };
+
+ try fftest("set audio sample rate", c.av_opt_set_int(this.a.swr, "in_sample_rate", 44100, 0));
+ try fftest("set audio sample format", c.av_opt_set_sample_fmt(this.a.swr, "in_sample_fmt", c.AV_SAMPLE_FMT_S16P, 0));
+ try fftest("set audio channel layout", c.av_opt_set_chlayout(this.a.swr, "in_chlayout", &this.a.ctx.ch_layout, 0));
+ try fftest("set out sample rate", c.av_opt_set_int(this.a.swr, "out_sample_rate", this.a.ctx.sample_rate, 0));
+ try fftest("set out sample format", c.av_opt_set_sample_fmt(this.a.swr, "out_sample_fmt", this.a.ctx.sample_fmt, 0));
+ try fftest("set out channel layout", c.av_opt_set_chlayout(this.a.swr, "out_chlayout", &this.a.ctx.ch_layout, 0));
+
+ r = c.swr_init(this.a.swr);
+ if (r < 0) {
+ std.log.err("couldn't start audio resampler ({})", .{r});
+ return error.FFmpegError;
+ }
+
+ const frame = c.av_frame_alloc();
+ this.a.frame = frame orelse {
+ std.log.err("failed to alloc audio frame", .{});
+ return error.FFmpegError;
+ };
+ this.a.frame.nb_samples = this.a.ctx.frame_size;
+ this.a.frame.format = this.a.ctx.sample_fmt;
+ this.a.frame.ch_layout = this.a.ctx.ch_layout;
+ r = c.av_frame_get_buffer(this.a.frame, 0);
+ if (r < 0) {
+ std.log.err("couldn't alloc frame buffer", .{});
+ return error.FFmpegError;
+ }
+
+ std.log.info("Audio frame nb_samples: {}", .{this.a.frame.nb_samples});
+
+ const astream = c.avformat_new_stream(this.format, null);
+ if (astream == null) {
+ std.log.err("failed to create audio stream", .{});
+ return error.FFmpegError;
+ }
+ this.a.stream = astream.?;
+ try fftest("set stream parameters from ctx",
+ c.avcodec_parameters_from_context(this.a.stream.codecpar, this.a.ctx));
+ this.a.stream.time_base = this.a.ctx.time_base;
+
+ r = c.avcodec_open2(this.a.ctx, this.a.codec, null);
+ if (r < 0) {
+ std.log.err("couldn't open audio codec", .{});
+ return error.FFmpegError;
+ }
+
+ try fftest("open container file", c.avio_open(&this.format.*.pb, "test.mp4", c.AVIO_FLAG_WRITE));
+ try fftest("write header", c.avformat_write_header(this.format, null));
+
+ this.v.nextpts = 0;
+ this.a.nextpts = 0;
+ this.a.frame_idx = 0;
+ const width_big: usize = config.width;
+ const height_big: usize = config.height;
+ this.pixels = try allocator.alloc(Bgr, width_big * height_big);
+
+ //this.orig_WriteMovieFrame = try hookman.hookVMT(
+ // api.videomode.vt,
+ // gamedata.CVideoMode_MaterialSystem.vtidx_WriteMovieFrame,
+ // &hook_WriteMovieFrame
+ //);
+ //this.orig_S_TransferStereo16 =
+ // try hookman.hookDetour(api.S_TransferStereo16, &hook_S_TransferStereo16);
+ }
+} = undefined;
+
+extern "kernel32" fn SetDllDirectoryA(lpPathName: [*]const u8) callconv(.winapi) c_int;
+
+var orig_Init: *const @TypeOf(hook_Init) = undefined;
+fn hook_Init(this: *CEngineVGui) callconv(.{ .x86_thiscall = .{} }) void {
+ orig_Init(this);
+ api.init() catch |err| {
+ std.log.err("failed to init api! {}", .{err});
+ std.process.exit(1);
+ };
+ render.init(gpa.allocator()) catch |err| {
+ std.log.err("failed to init render! {}", .{err});
+ std.process.exit(1);
+ };
+ api.engclient.clientCmd(.{"sv_cheats 1"});
+ api.engclient.clientCmd(.{"host_framerate 60"});
+ // _ = api.demoplayer.startPlayback(.{"test.dem", false});
+}
+
+var hooked_engine = false;
+var orig_LoadLibraryExA: *const @TypeOf(hook_LoadLibraryExA) = undefined;
+fn hook_LoadLibraryExA(filename: [*:0]const u8,
+ handle: w.HANDLE, flags: u32) callconv(.winapi) ?w.HMODULE {
+ const ret = orig_LoadLibraryExA(filename, handle, flags)
+ orelse return null; // we only want to log things that actually load
+ // get the base name of the module
+ var iter = std.mem.splitScalar(u8, std.mem.span(filename), '\\');
+ var shortname: []const u8 = undefined;
+ while (iter.next()) |seg| shortname = seg;
+ shortname = shortname[0 .. shortname.len - 4]; // cut off ".dll"
+ std.log.info("LoadLibrary: {s}", .{shortname});
+
+ // have a guard, the game likes to try and load engine multiple times and we
+ // don't want to try and double hook
+ if (std.mem.eql(u8, shortname, "engine") and !hooked_engine) {
+ var eng = std.DynLib { .inner = .{ .dll = ret } };
+ const factory = eng.lookup(CreateInterfaceFn, "CreateInterface").?;
+ const engvgui: *CEngineVGui =
+ @ptrCast(factory(gamedata.CEngineVGui.iface_name, null));
+ orig_Init = hookman.hookVMT(engvgui.vt,
+ gamedata.CEngineVGui.vtidx_Init, &hook_Init) catch return ret;
+ // since we already have the engine dll base now, just grab the sound
+ // offsets we need
+ const base = @intFromPtr(ret);
+ api.S_TransferStereo16 =
+ @ptrFromInt(base + gamedata.off_S_TransferStereo16);
+ hooked_engine = true;
+ }
+
+ return ret;
+}
+
+var cmdline: [128:0]u8 = .{0} ** 128;
+fn hook_GetCommandLineA() callconv(.winapi) [*:0]const u8 {
+ return &cmdline;
+}
+
+var hookman: hook.HookManager = undefined;
+var config: Config = undefined;
+var gpa: std.heap.GeneralPurposeAllocator(.{}) = undefined;
+pub fn main() !void {
+ gpa = std.heap.GeneralPurposeAllocator(.{}).init;
+
+ // cd to the exe
+ var buf: [4096]u8 = undefined;
+ const goalcwd = try std.fs.selfExeDirPath(&buf);
+ std.log.info("cd to {s}", .{goalcwd});
+ try (try std.fs.openDirAbsolute(goalcwd, .{})).setAsCwd();
+
+ config = try Config.readFromFile("config.zon", gpa.allocator());
+ std.log.info("{}", .{config});
+
+ _ = try std.fmt.bufPrint(&cmdline,
+ "hl2.exe -game {s} -w {} -h {} -window -novid -console {s}",
+ .{ config.mod, config.width, config.height, config.extraargs });
+
+ if (SetDllDirectoryA("bin/") == 0) return error.SetDllDirectoryFailed;
+
+ hookman = try hook.HookManager.init(gpa.allocator());
+ defer _ = hookman.deinit();
+
+ // hook loadlibrary so we know when the game is fully loaded
+ orig_LoadLibraryExA =
+ try hookman.hookSymbol("kernel32", "LoadLibraryExA", &hook_LoadLibraryExA);
+ // easy way to pass the game stuff like resolution, etc
+ _ = try hookman.hookSymbol("kernel32", "GetCommandLineA", &hook_GetCommandLineA);
+
+ var launcher = try std.DynLib.open("bin/launcher.dll");
+ const launcherMain =
+ launcher.lookup(*const fn (?*anyopaque, ?*anyopaque) callconv(.c) c_int, "LauncherMain");
+ _ = launcherMain.?(null, null);
+}
diff --git a/src/root.zig b/src/root.zig
new file mode 100644
index 0000000..9afb8de
--- /dev/null
+++ b/src/root.zig
@@ -0,0 +1,24 @@
+//! By convention, root.zig is the root source file when making a library.
+const std = @import("std");
+
+pub fn bufferedPrint() !void {
+ // Stdout is for the actual output of your application, for example if you
+ // are implementing gzip, then only the compressed bytes should be sent to
+ // stdout, not any debugging messages.
+ const stdout_file = std.fs.File.stdout().deprecatedWriter();
+ // Buffering can improve performance significantly in print-heavy programs.
+ var bw = std.io.bufferedWriter(stdout_file);
+ const stdout = bw.writer();
+
+ try stdout.print("Run `zig build test` to run the tests.\n", .{});
+
+ try bw.flush(); // Don't forget to flush!
+}
+
+pub fn add(a: i32, b: i32) i32 {
+ return a + b;
+}
+
+test "basic add functionality" {
+ try std.testing.expect(add(3, 7) == 10);
+}