From 7ccab8f8023270c614ef001b252eb4aab71dec68 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Sun, 8 Mar 2026 12:59:51 -0700 Subject: [PATCH] Extract addon management into addons.zig with is_active gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move embedded file table, addon registration hooks, and loading logic from main.zig into addons.zig. Each module's is_active callback gates addon loading on mutex ownership so individual DLLs only load addons for modules they control. Addon hooks are skipped entirely at comptime when no addon-bearing modules are compiled in. Also consolidates CheckFileExistence hook — customassets no longer hooks it directly, instead exporting looseFilesLookup for main.zig's hook. --- src/addons.zig | 450 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 421 +-------------------------------------------- 2 files changed, 456 insertions(+), 415 deletions(-) create mode 100644 src/addons.zig diff --git a/src/addons.zig b/src/addons.zig new file mode 100644 index 0000000..3ea09fe --- /dev/null +++ b/src/addons.zig @@ -0,0 +1,450 @@ +// ============================================================================= +// Embedded addon file management +// ============================================================================= +// +// Comptime-built table of embedded addon/asset files, plus hooks for addon +// registration (SetupAddonProcessing) and loading (LoadAddonsRecursively). +// +// Modules declare their addon files via embed_modules. The is_active callback +// gates addon loading on mutex ownership — if another DLL instance owns the +// module, we don't load its addon. +// +// ============================================================================= + +const std = @import("std"); +const hook = @import("zhook"); +const con = @import("console.zig"); +const build_options = @import("build_options"); + +const fc: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; +const sc: std.builtin.CallingConvention = .{ .x86_stdcall = .{} }; + +// Build option convenience aliases +const build_opts = struct { + const interact = build_options.enable_interact; + const outline = build_options.enable_outline; + const worldmarkers = build_options.enable_worldmarkers; + const logsessions = build_options.enable_logsessions; + const minimapicons = build_options.enable_minimapicons; +}; + +// Conditional module imports (only for is_active callbacks) +const interact = if (build_opts.interact) @import("interact/interact.zig") else struct {}; +const outline = if (build_opts.outline) @import("outline/api.zig") else struct {}; +const markers = if (build_opts.worldmarkers) @import("markers/markers.zig") else struct {}; +const logsessions = if (build_opts.logsessions) @import("logsessions/logsessions.zig") else struct {}; +const minimapicons = if (build_opts.minimapicons) @import("minimapicons/minimapicons.zig") else struct {}; + +// ============================================================================= +// Embedded file types +// ============================================================================= + +pub const FileEntry = struct { + name: []const u8, + data: []const u8, +}; + +const AddonPrefix = struct { + prefix: []const u8, + files: []const FileEntry, +}; + +/// Module descriptor for compile-time embed generation. +const EmbedModule = struct { + option: []const u8, + addon_name: ?[]const u8 = null, + addon_files_opt: ?[]const u8 = null, + asset_files_opt: ?[]const u8 = null, + /// If true, addon is hidden from addon list, always loaded. + hidden: bool = false, + /// Runtime check — addon is only loaded if this returns true. + /// Null means always load (when compiled in). + is_active: ?*const fn () bool = null, +}; + +const embed_modules = [_]EmbedModule{ + .{ .option = "interact", .addon_name = "WeirdUtils_Interact", .addon_files_opt = "interact_addon_files", .is_active = if (build_opts.interact) &interact.isActive else null }, + .{ .option = "outline", .addon_name = "WeirdUtils_Outline", .addon_files_opt = "outline_addon_files", .is_active = if (build_opts.outline) &outline.isActive else null }, + .{ .option = "worldmarkers", .addon_name = "WeirdUtils_WorldMarkers", .addon_files_opt = "worldmarkers_addon_files", .asset_files_opt = "worldmarkers_asset_files", .hidden = true, .is_active = if (build_opts.worldmarkers) &markers.isActive else null }, + .{ .option = "logsessions", .addon_name = "WeirdUtils_LogSessions", .addon_files_opt = "logsessions_addon_files", .is_active = if (build_opts.logsessions) &logsessions.isActive else null }, + .{ .option = "minimapicons", .addon_name = "WeirdUtils_MinimapIcons", .addon_files_opt = "minimapicons_addon_files", .asset_files_opt = "minimapicons_asset_files", .is_active = if (build_opts.minimapicons) &minimapicons.isActive else null }, +}; + +// ============================================================================= +// Comptime path helpers +// ============================================================================= + +fn comptimeBasename(comptime path: []const u8) []const u8 { + var i = path.len; + while (i > 0) { + i -= 1; + if (path[i] == '/') return path[i + 1 ..]; + } + return path; +} + +/// Extract WoW-style parent directory: "markers/assets/Spells/foo.m2" with +/// assets_prefix="markers/assets/" → "Spells\\" +fn comptimeWowDir(comptime path: []const u8, comptime assets_prefix: []const u8) []const u8 { + const rel = path[assets_prefix.len..]; + var last_slash: usize = 0; + var found = false; + for (rel, 0..) |c, i| { + if (c == '/') { + last_slash = i; + found = true; + } + } + if (!found) return ""; + const dir = rel[0..last_slash]; + return comptime blk: { + var buf: [dir.len + 1]u8 = undefined; + for (dir, 0..) |c, i| { + buf[i] = if (c == '/') '\\' else c; + } + buf[dir.len] = '\\'; + const final = buf; + break :blk &final; + }; +} + +fn eqlSlice(a: []const u8, b: []const u8) bool { + if (a.len != b.len) return false; + for (a, b) |x, y| { + if (x != y) return false; + } + return true; +} + +fn embedFiles(comptime paths: []const []const u8) [paths.len]FileEntry { + comptime { + var entries: [paths.len]FileEntry = undefined; + for (paths, 0..) |path, i| { + entries[i] = .{ + .name = comptimeBasename(path), + .data = @embedFile(path), + }; + } + const final = entries; + return final; + } +} + +/// Derive the assets/ path prefix from the option name pattern. +fn assetsPrefixFromOpt(comptime opt: []const u8) []const u8 { + const paths = @field(build_options, opt); + const first = paths[0]; + for (first, 0..) |_, i| { + if (i + 8 <= first.len and eqlSlice(first[i .. i + 8], "/assets/")) { + return first[0 .. i + 8]; + } + } + return first; +} + +fn countUniqueAssetDirs(comptime paths: []const []const u8, comptime assets_prefix: []const u8) usize { + var dirs: [paths.len][]const u8 = undefined; + var n: usize = 0; + for (paths) |path| { + const dir = comptimeWowDir(path, assets_prefix); + if (dir.len == 0) continue; + var found = false; + for (dirs[0..n]) |d| { + if (eqlSlice(d, dir)) { + found = true; + break; + } + } + if (!found) { + dirs[n] = dir; + n += 1; + } + } + return n; +} + +fn appendAssetPrefixes(result: anytype, start_idx: usize, comptime paths: []const []const u8, comptime assets_prefix: []const u8) usize { + comptime { + var dirs: [paths.len][]const u8 = undefined; + var n_dirs: usize = 0; + for (paths) |path| { + const dir = comptimeWowDir(path, assets_prefix); + if (dir.len == 0) continue; + var found = false; + for (dirs[0..n_dirs]) |d| { + if (eqlSlice(d, dir)) { + found = true; + break; + } + } + if (!found) { + dirs[n_dirs] = dir; + n_dirs += 1; + } + } + + var idx = start_idx; + for (dirs[0..n_dirs]) |dir| { + var file_count: usize = 0; + for (paths) |path| { + if (eqlSlice(comptimeWowDir(path, assets_prefix), dir)) file_count += 1; + } + var files: [file_count]FileEntry = undefined; + var fi: usize = 0; + for (paths) |path| { + if (eqlSlice(comptimeWowDir(path, assets_prefix), dir)) { + files[fi] = .{ .name = comptimeBasename(path), .data = @embedFile(path) }; + fi += 1; + } + } + const final_files = files; + result[idx] = .{ .prefix = dir, .files = &final_files }; + idx += 1; + } + return idx; + } +} + +fn buildAllPrefixes() []const AddonPrefix { + @setEvalBranchQuota(50000); + comptime { + var count: usize = 0; + for (embed_modules) |mod| { + if (!@field(build_options, "enable_" ++ mod.option)) continue; + if (mod.addon_files_opt) |opt| { + if (@field(build_options, opt).len > 0) count += 1; + } + if (mod.asset_files_opt) |opt| { + const asset_paths = @field(build_options, opt); + if (asset_paths.len > 0) { + const pfx = assetsPrefixFromOpt(opt); + count += countUniqueAssetDirs(asset_paths, pfx); + } + } + } + + var result: [count]AddonPrefix = undefined; + var idx: usize = 0; + for (embed_modules) |mod| { + if (!@field(build_options, "enable_" ++ mod.option)) continue; + if (mod.addon_files_opt) |opt| { + const paths = @field(build_options, opt); + if (paths.len > 0) { + const files = embedFiles(paths); + result[idx] = .{ + .prefix = "Interface\\AddOns\\" ++ mod.addon_name.? ++ "\\", + .files = &files, + }; + idx += 1; + } + } + if (mod.asset_files_opt) |opt| { + const asset_paths = @field(build_options, opt); + if (asset_paths.len > 0) { + const pfx = assetsPrefixFromOpt(opt); + idx = appendAssetPrefixes(&result, idx, asset_paths, pfx); + } + } + } + const final = result; + return &final; + } +} + +const addon_prefixes = buildAllPrefixes(); + +// ============================================================================= +// Embedded file lookup +// ============================================================================= + +pub fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry { + const path_span = std.mem.span(path); + + for (addon_prefixes) |*addon| { + if (path_span.len <= addon.prefix.len) continue; + + // Case-insensitive prefix check + var prefix_match = true; + 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) { + prefix_match = false; + break; + } + } + if (!prefix_match) continue; + + const relative = path_span[addon.prefix.len..]; + for (addon.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: SetupAddonProcessing (0x51C740) +// Called during HandleLogin. After the original runs (which scans +// Interface\AddOns\ on the filesystem), we call LoadAddonTOC for each +// DLL-embedded addon to register them in the game's addon hash table. +// ============================================================================= + +var setup_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{}; + +fn setupAddonsDetour(mgr_ptr: u32) callconv(fc) void { + setup_addons_hook.callOriginal(.{mgr_ptr}); + + inline for (embed_modules) |mod| { + if (comptime mod.addon_name == null) continue; + if (!@field(build_options, "enable_" ++ mod.option)) continue; + + if (mod.hidden) continue; + + const active = if (mod.is_active) |f| f() else true; + if (active) { + const name: [*:0]const u8 = comptime (mod.addon_name.? ++ "\x00").ptr; + con.fmt("[addons] registering embedded addon: {s}\n", .{name}); + callLoadAddonTOC(name); + } + } +} + +fn callLoadAddonTOC(addon_name: [*:0]const u8) void { + asm volatile ( + \\call *%[func] + : + : [_] "{ecx}" (@intFromPtr(addon_name)), + [func] "{eax}" (@as(u32, 0x0051c9b0)), + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); +} + +// ============================================================================= +// Hook: LoadAddonsRecursively (0x51F600) +// Non-hidden addons are fully handled by the game's LoadAddonRecursive (which +// respects enabled/disabled state) — the CheckFileExistence hook in main.zig +// 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) = .{}; + +fn loadAddonsDetour(error_handler: u32) callconv(fc) void { + load_addons_hook.callOriginal(.{error_handler}); + + var md5ctx = std.mem.zeroes([88]u8); + + inline for (embed_modules) |mod| { + if (comptime mod.addon_name != null and + @field(build_options, "enable_" ++ mod.option) and + mod.hidden) + { + const active = if (mod.is_active) |f| f() else true; + if (active) { + const addon_name = comptime mod.addon_name.?; + const paths = comptime @field(build_options, mod.addon_files_opt.?); + + // Hidden addons are not registered via LoadAddonTOC, so the game + // doesn't know about them. Load their files directly to keep them + // invisible in the addon list. + const toc_name = comptime findTocName(paths); + if (toc_name) |tn| { + callLoadFileListWithIncludes( + "Interface\\AddOns\\" ++ addon_name ++ "\\" ++ tn, + &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, + ); + } + } + } + } +} + +fn findTocName(comptime paths: []const []const u8) ?[]const u8 { + for (paths) |path| { + const name = comptimeBasename(path); + if (name.len >= 4 and eqlSlice(name[name.len - 4 ..], ".toc")) return name; + } + return null; +} + +fn hasFile(comptime paths: []const []const u8, comptime target: []const u8) bool { + for (paths) |path| { + if (eqlSlice(comptimeBasename(path), target)) return true; + } + return false; +} + +fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_handler: u32) void { + asm volatile ( + \\push %[eh] + \\call *%[func] + : + : [_] "{ecx}" (@intFromPtr(toc_path)), + [_] "{edx}" (@intFromPtr(md5ctx)), + [eh] "r" (error_handler), + [func] "r" (@as(u32, 0x6EDB90)), + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); +} + +fn callLoadUIBindingsFromFile(path: [*:0]const u8, md5ctx: *[88]u8, callback: u32) void { + const binding_mgr = hook.readMem(u32, 0xB71290); + asm volatile ( + \\push %[cb] + \\push %[md5] + \\push %[path] + \\call *%[func] + : + : [_] "{ecx}" (binding_mgr), + [func] "{edx}" (@as(u32, 0x4B6F70)), + [path] "r" (@intFromPtr(path)), + [md5] "r" (@intFromPtr(md5ctx)), + [cb] "r" (callback), + : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); +} + +// ============================================================================= +// Install / Remove +// ============================================================================= + +/// True if any embed_modules entry has addon files compiled in. +const has_addons = blk: { + for (embed_modules) |mod| { + if (mod.addon_name != null and @field(build_options, "enable_" ++ mod.option)) + break :blk true; + } + break :blk false; +}; + +pub fn install() void { + if (!has_addons) return; + _ = setup_addons_hook.attach(0x51C740, &setupAddonsDetour); + _ = load_addons_hook.attach(0x51F600, &loadAddonsDetour); +} + +pub fn uninstall() void { + if (!has_addons) return; + load_addons_hook.detach(); + setup_addons_hook.detach(); +} diff --git a/src/main.zig b/src/main.zig index 5bb43d9..4159a67 100644 --- a/src/main.zig +++ b/src/main.zig @@ -114,274 +114,12 @@ fn registerLuaFunctions() void { } // ============================================================================= -// Embedded addon files — auto-generated from addon/ and assets/ directories +// Embedded addon files // ============================================================================= const build_options = @import("build_options"); - -const AddonPrefix = struct { - prefix: []const u8, - files: []const FileEntry, -}; - -const FileEntry = struct { - name: []const u8, - data: []const u8, -}; - -/// Module descriptor for compile-time embed generation. -/// addon_name: WoW AddOns folder name (determines prefix + TOC/Bindings paths). -/// option: build_options field name for enable flag. -/// addon_files_opt / asset_files_opt: build_options field names for file lists. -const EmbedModule = struct { - option: []const u8, - addon_name: ?[]const u8 = null, - addon_files_opt: ?[]const u8 = null, - asset_files_opt: ?[]const u8 = null, - /// If true, addon is marked as SECURE (hidden from addon list, always loaded). - hidden: bool = false, -}; - -const embed_modules = [_]EmbedModule{ - .{ .option = "screenshot" }, - .{ .option = "interact", .addon_name = "WeirdUtils_Interact", .addon_files_opt = "interact_addon_files" }, - .{ .option = "outline", .addon_name = "WeirdUtils_Outline", .addon_files_opt = "outline_addon_files" }, - .{ .option = "worldmarkers", .addon_name = "WeirdUtils_WorldMarkers", .addon_files_opt = "worldmarkers_addon_files", .asset_files_opt = "worldmarkers_asset_files", .hidden = true }, - .{ .option = "logsessions", .addon_name = "WeirdUtils_LogSessions", .addon_files_opt = "logsessions_addon_files" }, - .{ .option = "minimapicons", .addon_name = "WeirdUtils_MinimapIcons", .addon_files_opt = "minimapicons_addon_files", .asset_files_opt = "minimapicons_asset_files" }, -}; - -/// Extract filename from a path like "markers/addon/Foo.lua" → "Foo.lua" -fn comptimeBasename(comptime path: []const u8) []const u8 { - var i = path.len; - while (i > 0) { - i -= 1; - if (path[i] == '/') return path[i + 1 ..]; - } - return path; -} - -/// Extract WoW-style parent directory: "markers/assets/Spells/foo.m2" with -/// assets_prefix="markers/assets/" → "Spells\\" -fn comptimeWowDir(comptime path: []const u8, comptime assets_prefix: []const u8) []const u8 { - const rel = path[assets_prefix.len..]; // e.g. "Spells/foo.m2" - // Find last '/' to separate dir from filename - var last_slash: usize = 0; - var found = false; - for (rel, 0..) |c, i| { - if (c == '/') { - last_slash = i; - found = true; - } - } - if (!found) return ""; - const dir = rel[0..last_slash]; // e.g. "Spells" - // Convert '/' to '\' and add trailing '\' - return comptime blk: { - var buf: [dir.len + 1]u8 = undefined; - for (dir, 0..) |c, i| { - buf[i] = if (c == '/') '\\' else c; - } - buf[dir.len] = '\\'; - const final = buf; - break :blk &final; - }; -} - -/// Embed a list of files, returning an array of FileEntry. -fn embedFiles(comptime paths: []const []const u8) [paths.len]FileEntry { - comptime { - var entries: [paths.len]FileEntry = undefined; - for (paths, 0..) |path, i| { - entries[i] = .{ - .name = comptimeBasename(path), - .data = @embedFile(path), - }; - } - const final = entries; - return final; - } -} - -/// Build all AddonPrefix entries from embed_modules at comptime. -fn buildAllPrefixes() []const AddonPrefix { - @setEvalBranchQuota(50000); - comptime { - // First pass: count total prefix entries - var count: usize = 0; - for (embed_modules) |mod| { - if (!@field(build_options, "enable_" ++ mod.option)) continue; - - if (mod.addon_files_opt) |opt| { - if (@field(build_options, opt).len > 0) count += 1; - } - if (mod.asset_files_opt) |opt| { - const asset_paths = @field(build_options, opt); - // Determine assets_prefix from first path (e.g. "markers/assets/") - if (asset_paths.len > 0) { - const pfx = assetsPrefixFromOpt(opt); - count += countUniqueAssetDirs(asset_paths, pfx); - } - } - } - - // Second pass: build array - var result: [count]AddonPrefix = undefined; - var idx: usize = 0; - for (embed_modules) |mod| { - if (!@field(build_options, "enable_" ++ mod.option)) continue; - - if (mod.addon_files_opt) |opt| { - const paths = @field(build_options, opt); - if (paths.len > 0) { - const files = embedFiles(paths); - result[idx] = .{ - .prefix = "Interface\\AddOns\\" ++ mod.addon_name.? ++ "\\", - .files = &files, - }; - idx += 1; - } - } - if (mod.asset_files_opt) |opt| { - const asset_paths = @field(build_options, opt); - if (asset_paths.len > 0) { - const pfx = assetsPrefixFromOpt(opt); - idx = appendAssetPrefixes(&result, idx, asset_paths, pfx); - } - } - } - const final = result; - return &final; - } -} - -/// Derive the assets/ path prefix from the option name pattern. -/// "worldmarkers_asset_files" → look at first path to get "markers/assets/" -fn assetsPrefixFromOpt(comptime opt: []const u8) []const u8 { - const paths = @field(build_options, opt); - // First path is like "markers/assets/Spells/foo.m2" — find "/assets/" to get prefix - const first = paths[0]; - for (first, 0..) |_, i| { - if (i + 8 <= first.len and eqlSlice(first[i .. i + 8], "/assets/")) { - return first[0 .. i + 8]; // e.g. "markers/assets/" - } - } - return first; // fallback -} - -fn eqlSlice(a: []const u8, b: []const u8) bool { - if (a.len != b.len) return false; - for (a, b) |x, y| { - if (x != y) return false; - } - return true; -} - -/// Count unique parent directories in asset paths. -fn countUniqueAssetDirs(comptime paths: []const []const u8, comptime assets_prefix: []const u8) usize { - var dirs: [paths.len][]const u8 = undefined; - var n: usize = 0; - for (paths) |path| { - const dir = comptimeWowDir(path, assets_prefix); - if (dir.len == 0) continue; - var found = false; - for (dirs[0..n]) |d| { - if (eqlSlice(d, dir)) { - found = true; - break; - } - } - if (!found) { - dirs[n] = dir; - n += 1; - } - } - return n; -} - -/// Append asset prefix entries grouped by WoW directory. -fn appendAssetPrefixes(result: anytype, start_idx: usize, comptime paths: []const []const u8, comptime assets_prefix: []const u8) usize { - comptime { - // Collect unique directories - var dirs: [paths.len][]const u8 = undefined; - var n_dirs: usize = 0; - for (paths) |path| { - const dir = comptimeWowDir(path, assets_prefix); - if (dir.len == 0) continue; - var found = false; - for (dirs[0..n_dirs]) |d| { - if (eqlSlice(d, dir)) { - found = true; - break; - } - } - if (!found) { - dirs[n_dirs] = dir; - n_dirs += 1; - } - } - - var idx = start_idx; - for (dirs[0..n_dirs]) |dir| { - // Count files in this directory - var file_count: usize = 0; - for (paths) |path| { - if (eqlSlice(comptimeWowDir(path, assets_prefix), dir)) file_count += 1; - } - // Build file entries for this group - var files: [file_count]FileEntry = undefined; - var fi: usize = 0; - for (paths) |path| { - if (eqlSlice(comptimeWowDir(path, assets_prefix), dir)) { - files[fi] = .{ .name = comptimeBasename(path), .data = @embedFile(path) }; - fi += 1; - } - } - const final_files = files; - result[idx] = .{ .prefix = dir, .files = &final_files }; - idx += 1; - } - return idx; - } -} - -const addon_prefixes = buildAllPrefixes(); - -fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry { - const path_span = std.mem.span(path); - - for (addon_prefixes) |*addon| { - if (path_span.len <= addon.prefix.len) continue; - - // Case-insensitive prefix check - var prefix_match = true; - 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) { - prefix_match = false; - break; - } - } - if (!prefix_match) continue; - - const relative = path_span[addon.prefix.len..]; - for (addon.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; -} +const addons = @import("addons.zig"); +const findEmbeddedFile = addons.findEmbeddedFile; // ============================================================================= // Hook: LoadFileWithTextureResourceFallback (0x648620) @@ -807,150 +545,6 @@ fn loadScriptFunctionsDetour() callconv(sc) void { registerLuaFunctions(); } -// ============================================================================= -// Hook: SetupAddonProcessing (0x51C740) -// Called during HandleLogin. After the original runs (which scans Interface\AddOns\ -// on the filesystem), we call LoadAddonTOC for each DLL-embedded addon to register -// them in the game's internal addon hash table. This makes the game aware of our -// addons for SavedVariables, Bindings.xml, and proper load ordering. -// ============================================================================= - -var setup_addons_hook: hook.Detour(fn (u32) callconv(fc) void) = .{}; - -fn setupAddonsDetour(mgr_ptr: u32) callconv(fc) void { - setup_addons_hook.callOriginal(.{mgr_ptr}); - - // Register each embedded addon via LoadAddonTOC. The game reads the .toc - // file through LoadFileWithTextureResourceFallback, which our loadFileDetour - // intercepts to serve the embedded content. This populates the addon's - // SavedVariablesPerCharacter list so the game saves/loads them from WTF/. - inline for (embed_modules) |mod| { - if (comptime mod.addon_name == null) continue; - if (!@field(build_options, "enable_" ++ mod.option)) continue; - - const load = if (comptime std.mem.eql(u8, mod.option, "worldmarkers")) - markers.isActive() - else - true; - - if (load and !mod.hidden) { - const name: [*:0]const u8 = comptime (mod.addon_name.? ++ "\x00").ptr; - con.fmt("[addons] registering embedded addon: {s}\n", .{name}); - callLoadAddonTOC(name); - } - } -} - -/// Call LoadAddonTOC (0x0051c9b0) -- __fastcall(ECX=addonName), RET -fn callLoadAddonTOC(addon_name: [*:0]const u8) void { - asm volatile ( - \\call *%[func] - : - : [_] "{ecx}" (@intFromPtr(addon_name)), - [func] "{eax}" (@as(u32, 0x0051c9b0)), - : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); -} - -// ============================================================================= -// Hook: LoadAddonsRecursively (0x51F600) -// 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) = .{}; - -fn loadAddonsDetour(error_handler: u32) callconv(fc) void { - load_addons_hook.callOriginal(.{error_handler}); - - var md5ctx = std.mem.zeroes([88]u8); - - inline for (embed_modules) |mod| { - if (comptime mod.addon_name == null) continue; - if (!@field(build_options, "enable_" ++ mod.option)) continue; - - const load = if (comptime std.mem.eql(u8, mod.option, "worldmarkers")) - markers.isActive() - else - true; - - if (load) { - const addon_name = comptime mod.addon_name.?; - const paths = comptime @field(build_options, mod.addon_files_opt.?); - - if (mod.hidden) { - // Hidden addons are not registered via LoadAddonTOC, so the game - // doesn't know about them. Load their files directly to keep them - // invisible in the addon list. - const toc_name = comptime findTocName(paths); - if (toc_name) |tn| { - callLoadFileListWithIncludes( - "Interface\\AddOns\\" ++ addon_name ++ "\\" ++ tn, - &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, - ); - } - } - } - } -} - -/// Find the .toc filename from an addon file path list. -fn findTocName(comptime paths: []const []const u8) ?[]const u8 { - for (paths) |path| { - const name = comptimeBasename(path); - if (name.len >= 4 and eqlSlice(name[name.len - 4 ..], ".toc")) return name; - } - return null; -} - -/// Check if a filename exists in the path list. -fn hasFile(comptime paths: []const []const u8, comptime target: []const u8) bool { - for (paths) |path| { - if (eqlSlice(comptimeBasename(path), target)) return true; - } - return false; -} - -fn callLoadFileListWithIncludes(toc_path: [*:0]const u8, md5ctx: *[88]u8, error_handler: u32) void { - asm volatile ( - \\push %[eh] - \\call *%[func] - : - : [_] "{ecx}" (@intFromPtr(toc_path)), - [_] "{edx}" (@intFromPtr(md5ctx)), - [eh] "r" (error_handler), - [func] "r" (@as(u32, 0x6EDB90)), - : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); -} - -fn callLoadUIBindingsFromFile(path: [*:0]const u8, md5ctx: *[88]u8, callback: u32) void { - const binding_mgr = hook.readMem(u32, 0xB71290); - asm volatile ( - \\push %[cb] - \\push %[md5] - \\push %[path] - \\call *%[func] - : - : [_] "{ecx}" (binding_mgr), - [func] "{edx}" (@as(u32, 0x4B6F70)), - [path] "r" (@intFromPtr(path)), - [md5] "r" (@intFromPtr(md5ctx)), - [cb] "r" (callback), - : .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }); -} // ============================================================================= // Hook: GameEngine_MainInitialize (0x46a400) @@ -1066,8 +660,7 @@ fn install() void { if (m.install) |inst| inst(); } - _ = setup_addons_hook.attach(0x51C740, &setupAddonsDetour); - _ = load_addons_hook.attach(0x51F600, &loadAddonsDetour); + addons.install(); _ = engine_init_hook.attach(0x46a400, &engineInitDetour); _ = logout_hook.attach(0x491180, &logoutDetour); _ = shutdown_hook.attach(0x490BD0, &shutdownDetour); @@ -1085,8 +678,7 @@ fn uninstall() void { if (modules[i].remove) |rm| rm(); } - load_addons_hook.detach(); - setup_addons_hook.detach(); + addons.uninstall(); lsf_hook.detach(); file_hook.detach(); removeFileHooks(); @@ -1162,8 +754,7 @@ fn disableAll() callconv(.c) i32 { shutdown_hook.detach(); logout_hook.detach(); engine_init_hook.detach(); - load_addons_hook.detach(); - setup_addons_hook.detach(); + addons.uninstall(); lsf_hook.detach(); file_hook.detach(); removeFileHooks();