diff --git a/src/customassets/customassets.zig b/src/customassets/customassets.zig index e631661..2cefbe4 100644 --- a/src/customassets/customassets.zig +++ b/src/customassets/customassets.zig @@ -5,8 +5,8 @@ // 1. Patches patch-?.MPQ → patch-*.MPQ so multi-char patch names work // 2. NOPs two gates in File_FindInArchive so CheckFileExistence runs for // all files, not just Interface/AddOns -// 3. Hooks CheckFileExistence with an O(1) hash set of pre-indexed loose -// files so non-existent files skip GetFileAttributesA entirely +// 3. Indexes loose Data/ files into an O(1) hash set; main.zig's +// CheckFileExistence hook calls looseFilesLookup() to serve them // // ============================================================================= @@ -161,55 +161,32 @@ fn looseFilesCleanup() void { arena.deinit(); } -fn looseFilesLookup(game_path_ptr: u32) ?[*]const u8 { - if (game_path_ptr == 0) return null; +/// Check if a game path matches a loose disk file. If found and output_buffer_ptr +/// is non-zero, writes the disk path into it. Returns true on hit. +/// Called from main.zig's CheckFileExistence hook. +pub fn looseFilesLookup(game_path_ptr: u32, output_buffer_ptr: u32) bool { + if (game_path_ptr == 0) return false; const raw: [*]const u8 = @ptrFromInt(game_path_ptr); const path = raw[0..cStrLen(raw)]; var norm_buf: [MAX_PATH]u8 = undefined; - if (path.len > MAX_PATH) return null; + if (path.len > MAX_PATH) return false; @memcpy(norm_buf[0..path.len], path); normalizeInPlace(norm_buf[0..path.len]); - const result = loose_files.get(norm_buf[0..path.len]); - if (result) |disk_path| { - return disk_path.ptr; - } - return null; -} + const disk_path = loose_files.get(norm_buf[0..path.len]) orelse return false; -// ============================================================================= -// Hook: CheckFileExistence (0x654DD0) -// ============================================================================= -// __fastcall(ECX=filename, EDX=flags, stack=outputBuffer) → EAX + con.fmt("[customassets] loose hit: \"{s}\"\n", .{path}); -const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; -const CheckFileExistenceFn = fn (u32, u32, u32) callconv(fc) u32; - -var cfe_hook: hook.Detour(CheckFileExistenceFn) = .{}; - -fn checkFileExistenceDetour(filename_ptr: u32, flags: u32, output_buffer_ptr: u32) callconv(fc) u32 { - if (filename_ptr != 0) { - if (looseFilesLookup(filename_ptr)) |disk_path| { - const raw: [*]const u8 = @ptrFromInt(filename_ptr); - con.fmt("[customassets] loose hit: \"{s}\"\n", .{raw[0..cStrLen(raw)]}); - - if (output_buffer_ptr != 0) { - const disk_len = cStrLen(disk_path); - const out: [*]u8 = @ptrFromInt(output_buffer_ptr); - if (disk_len < MAX_PATH) { - @memcpy(out[0..disk_len], disk_path[0..disk_len]); - out[disk_len] = 0; - } - } - return 1; + if (output_buffer_ptr != 0) { + const disk_len = cStrLen(disk_path.ptr); + const out: [*]u8 = @ptrFromInt(output_buffer_ptr); + if (disk_len < MAX_PATH) { + @memcpy(out[0..disk_len], disk_path.ptr[0..disk_len]); + out[disk_len] = 0; } } - return cfe_hook.callOriginal(.{ filename_ptr, flags, output_buffer_ptr }); -} - -fn installHook() bool { - return cfe_hook.attach(0x654DD0, &checkFileExistenceDetour) == .ok; + return true; } // ============================================================================= @@ -299,13 +276,11 @@ pub fn installHooks() void { applyGlobPatch(); applyLooseFilePatches(); looseFilesInit(); - if (!installHook()) return; installed = true; } pub fn removeHooks() void { if (g_is_hook_owner and installed) { - cfe_hook.detach(); revertLooseFilePatches(); revertGlobPatch(); looseFilesCleanup(); diff --git a/src/main.zig b/src/main.zig index e479520..5bb43d9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -751,6 +751,29 @@ fn loadModelAsyncDetour(model: u32, file_handle: u32, should_use_callback: u32) return model_load_hook.callOriginal(.{ model, file_handle, should_use_callback }); } +// --- Hook 6: CheckFileExistence (0x654DD0) --- +// __fastcall(ECX=filename, EDX=flags, stack=outputBuffer) → EAX (bool) +// Tells the game whether a file exists in the VFS. Without this, embedded +// files are invisible to preloadFileWithFlags, so LoadAddonRecursive skips +// Bindings.xml (and SavedVariables) for our addons. + +const CheckFileExistenceFn = fn (u32, u32, u32) callconv(fc) u32; +var cfe_hook: hook.Detour(CheckFileExistenceFn) = .{}; + +fn checkFileExistenceDetour(filename_ptr: u32, flags: u32, output_buffer_ptr: u32) callconv(fc) u32 { + if (filename_ptr != 0) { + // Embedded DLL files + const path: [*:0]const u8 = @ptrFromInt(filename_ptr); + if (findEmbeddedFile(path) != null) return 1; + + // Loose disk files (customassets module) + if (build_opts.customassets and customassets.isActive()) { + if (customassets.looseFilesLookup(filename_ptr, output_buffer_ptr)) return 1; + } + } + return cfe_hook.callOriginal(.{ filename_ptr, flags, output_buffer_ptr }); +} + // --- Install/remove in-memory file hooks --- fn installFileHooks() void { @@ -759,10 +782,12 @@ fn installFileHooks() void { _ = read_file_hook.attach(0x648460, &readFileDetour); _ = cleanup_file_handle_hook.attach(0x648730, &cleanupFileHandleDetour); _ = model_load_hook.attach(0x71d4e0, &loadModelAsyncDetour); + _ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour); con.print("[file] in-memory file hooks installed\n"); } fn removeFileHooks() void { + cfe_hook.detach(); model_load_hook.detach(); process_async_hook.detach(); cleanup_file_handle_hook.detach(); @@ -828,10 +853,10 @@ fn callLoadAddonTOC(addon_name: [*:0]const u8) void { // ============================================================================= // Hook: LoadAddonsRecursively (0x51F600) -// With SetupAddonProcessing registering our addons, the game's own -// LoadAddonRecursive now handles loading files, Bindings.xml, and saved -// variables for embedded addons. This hook is kept for any future -// post-load work but no longer manually loads addon files. +// Non-hidden addons are fully handled by the game's LoadAddonRecursive (which +// respects enabled/disabled state) — our CheckFileExistence hook makes their +// embedded files visible to preloadFileWithFlags. Hidden addons bypass +// LoadAddonRecursive, so we load their TOC and bindings explicitly here. // ============================================================================= var load_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{}; @@ -866,16 +891,17 @@ fn loadAddonsDetour(error_handler: u32) callconv(fc) void { error_handler, ); } - } - // Bindings.xml must be loaded explicitly for all addons -- the game's - // preloadFileWithFlags doesn't go through our file hook. - if (comptime hasFile(paths, "Bindings.xml")) { - callLoadUIBindingsFromFile( - "Interface\\AddOns\\" ++ addon_name ++ "\\Bindings.xml", - &md5ctx, - error_handler, - ); + // Hidden addons bypass LoadAddonRecursive, so load bindings + // explicitly. Non-hidden addons get theirs loaded by the game + // (which respects addon enabled/disabled state). + if (comptime hasFile(paths, "Bindings.xml")) { + callLoadUIBindingsFromFile( + "Interface\\AddOns\\" ++ addon_name ++ "\\Bindings.xml", + &md5ctx, + error_handler, + ); + } } } }