From 5927568fe8471b5d4ca20dbf21eb90d3d11c3019 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 12 Mar 2026 10:36:24 -0700 Subject: [PATCH] Refactor addon system to be data-driven from build.zig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build options now provide addon_name, addon_hidden, and file lists per module. addons.zig derives everything from these — no hardcoded module list. Added addon_hidden flag (WorldMarkers uses it to stay unlisted). Changed dpslog default to enabled. --- build.zig | 14 ++-- src/addons.zig | 189 +++++++++++++++++++++++++++---------------------- 2 files changed, 114 insertions(+), 89 deletions(-) diff --git a/build.zig b/build.zig index d8d3572..6b04678 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,8 @@ const ModuleDesc = struct { src_dir: ?[]const u8 = null, /// WoW addon folder name. Non-null means this module has an addon/ dir. addon_name: ?[]const u8 = null, + /// If true, addon is hidden from the in-game addon list (always loaded). + addon_hidden: bool = false, }; /// Single source of truth for all modules. Adding a module here is enough @@ -16,7 +18,7 @@ const module_list = [_]ModuleDesc{ .{ .name = "pngscreenshots", .desc = "Enable screenshot module", .src_dir = "screenshot" }, .{ .name = "interact", .desc = "Enable interact module", .addon_name = "Interact" }, .{ .name = "outline", .desc = "Enable outline module", .default = false, .addon_name = "Outline" }, - .{ .name = "worldmarkers", .desc = "Enable world markers module", .src_dir = "markers", .addon_name = "WorldMarkers" }, + .{ .name = "worldmarkers", .desc = "Enable world markers module", .src_dir = "markers", .addon_name = "WorldMarkers", .addon_hidden = true }, .{ .name = "framecrash", .desc = "Enable framecrash fix", .default = false }, .{ .name = "logsessions", .desc = "Enable log session rotation", .addon_name = "LogSessions" }, .{ .name = "minimapicons", .desc = "Enable custom minimap icons", .addon_name = "MinimapIcons" }, @@ -25,7 +27,8 @@ const module_list = [_]ModuleDesc{ .{ .name = "healtextfix", .desc = "Enable SuperWoW heal text fix" }, .{ .name = "bigcursor", .desc = "Enable big cursor module" }, .{ .name = "clickthrough", .desc = "Enable GO click-through (enlarge GO model bounds)" }, - .{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false }, + .{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = true }, + .{ .name = "transform44", .desc = "Enable transformMatrix4x4 hook", .default = false }, }; pub fn build(b: *std.Build) void { @@ -111,8 +114,11 @@ fn addFileListOptions(b: *std.Build, opts: *std.Build.Step.Options) void { for (module_list) |mod| { const src_dir = mod.src_dir orelse mod.name; - // Addon files: _addon_files = ["screenshot/addon/Screenshot.lua", ...] - if (mod.addon_name != null) { + // Addon: _addon_name, _addon_hidden, _addon_files + if (mod.addon_name) |aname| { + const full_name = std.fmt.allocPrint(a, "WeirdUtils_{s}", .{aname}) catch unreachable; + opts.addOption([]const u8, std.fmt.allocPrint(a, "{s}_addon_name", .{mod.name}) catch unreachable, full_name); + opts.addOption(bool, std.fmt.allocPrint(a, "{s}_addon_hidden", .{mod.name}) catch unreachable, mod.addon_hidden); const addon_rel = std.fmt.allocPrint(a, "src/{s}/addon", .{src_dir}) catch unreachable; const files = listDir(a, io, root, addon_rel); var paths: std.ArrayList([]const u8) = .empty; diff --git a/src/addons.zig b/src/addons.zig index d47260e..30fcf93 100644 --- a/src/addons.zig +++ b/src/addons.zig @@ -5,9 +5,13 @@ // 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. +// All addon configuration is driven by build.zig (single source of truth). +// Build options per module: {name}_addon_name, {name}_addon_hidden, +// {name}_addon_files, {name}_asset_files. This file derives everything from +// those options — no hardcoded module list. +// +// The is_active callback is wired by convention: each module that exports +// pub fn isActive() bool gets called at runtime to gate addon loading. // // ============================================================================= @@ -17,21 +21,36 @@ const logging = @import("logging.zig"); var log: logging.Logger = .{}; const build_options = @import("build_options"); -// 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; +// ============================================================================= +// Module list — must match build.zig module_list names. +// This is the only place module names appear; everything else is derived. +// ============================================================================= + +const module_names = [_][]const u8{ + "interact", + "outline", + "worldmarkers", + "logsessions", + "minimapicons", + "dpslog", }; -// 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 {}; +// ============================================================================= +// is_active wiring — convention-based imports +// ============================================================================= + +fn moduleIsActive(comptime name: []const u8) ?*const fn () bool { + if (!@field(build_options, "enable_" ++ name)) return null; + const mod = if (eqlSlice(name, "interact")) @import("interact/interact.zig") + else if (eqlSlice(name, "outline")) @import("outline/api.zig") + else if (eqlSlice(name, "worldmarkers")) @import("markers/markers.zig") + else if (eqlSlice(name, "logsessions")) @import("logsessions/logsessions.zig") + else if (eqlSlice(name, "minimapicons")) @import("minimapicons/minimapicons.zig") + else if (eqlSlice(name, "dpslog")) @import("dpslog/dpslog.zig") + else struct {}; + if (@hasDecl(mod, "isActive")) return &mod.isActive; + return null; +} // ============================================================================= // Embedded file types @@ -47,29 +66,8 @@ const AddonPrefix = struct { 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 +// Comptime helpers // ============================================================================= fn comptimeBasename(comptime path: []const u8) []const u8 { @@ -81,8 +79,6 @@ fn comptimeBasename(comptime path: []const u8) []const u8 { 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; @@ -128,7 +124,6 @@ fn embedFiles(comptime paths: []const []const u8) [paths.len]FileEntry { } } -/// 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]; @@ -203,45 +198,70 @@ fn appendAssetPrefixes(result: anytype, start_idx: usize, comptime paths: []cons } } +/// Check if a module has an addon (addon_name build option exists and is non-empty). +fn hasAddon(comptime name: []const u8) bool { + if (!@hasField(build_options, name ++ "_addon_name")) return false; + return @field(build_options, name ++ "_addon_name").len > 0; +} + +/// Get addon file paths for a module (empty if no addon or no files). +fn getAddonFiles(comptime name: []const u8) []const []const u8 { + if (!@hasField(build_options, name ++ "_addon_files")) return &.{}; + return @field(build_options, name ++ "_addon_files"); +} + +/// Get asset file paths for a module (empty if no assets). +fn getAssetFiles(comptime name: []const u8) []const []const u8 { + if (!@hasField(build_options, name ++ "_asset_files")) return &.{}; + return @field(build_options, name ++ "_asset_files"); +} + +/// Get hidden flag for a module. +fn isHidden(comptime name: []const u8) bool { + if (!@hasField(build_options, name ++ "_addon_hidden")) return false; + return @field(build_options, name ++ "_addon_hidden"); +} + +// ============================================================================= +// Build the addon prefix table from build options +// ============================================================================= + fn buildAllPrefixes() []const AddonPrefix { @setEvalBranchQuota(50000); comptime { + // Count 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); - if (asset_paths.len > 0) { - const pfx = assetsPrefixFromOpt(opt); - count += countUniqueAssetDirs(asset_paths, pfx); - } + for (module_names) |name| { + if (!@field(build_options, "enable_" ++ name)) continue; + if (!hasAddon(name)) continue; + const addon_files = getAddonFiles(name); + if (addon_files.len > 0) count += 1; + const asset_files = getAssetFiles(name); + if (asset_files.len > 0) { + const pfx = assetsPrefixFromOpt(name ++ "_asset_files"); + count += countUniqueAssetDirs(asset_files, 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; - } + for (module_names) |name| { + if (!@field(build_options, "enable_" ++ name)) continue; + if (!hasAddon(name)) continue; + const addon_name = @field(build_options, name ++ "_addon_name"); + const addon_files = getAddonFiles(name); + if (addon_files.len > 0) { + const files = embedFiles(addon_files); + result[idx] = .{ + .prefix = "Interface\\AddOns\\" ++ 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 asset_files = getAssetFiles(name); + if (asset_files.len > 0) { + const pfx = assetsPrefixFromOpt(name ++ "_asset_files"); + idx = appendAssetPrefixes(&result, idx, asset_files, pfx); } } const final = result; @@ -303,18 +323,20 @@ var setup_addons_hook: hook.Detour(fn (u32) callconv(hook.cc.fastcall) void) = . fn setupAddonsDetour(mgr_ptr: u32) callconv(hook.cc.fastcall) 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; + inline for (module_names) |name| { + if (!@field(build_options, "enable_" ++ name)) continue; + if (comptime !hasAddon(name)) continue; + if (comptime getAddonFiles(name).len == 0) continue; - const active = if (mod.is_active) |f| f() else true; + const active = if (comptime moduleIsActive(name)) |f| f() else true; if (active) { - const name: [*:0]const u8 = comptime (mod.addon_name.? ++ "\x00").ptr; - log.fmt("registering embedded addon: {s}\n", .{name}); - callLoadAddonTOC(name); + const addon_name = comptime @field(build_options, name ++ "_addon_name"); + const name_z: [*:0]const u8 = comptime (addon_name ++ "\x00").ptr; + log.fmt("registering embedded addon: {s}\n", .{name_z}); + callLoadAddonTOC(name_z); - if (mod.hidden) { - hideAddonFromList(name); + if (comptime isHidden(name)) { + hideAddonFromList(name_z); } } } @@ -323,9 +345,6 @@ fn setupAddonsDetour(mgr_ptr: u32) callconv(hook.cc.fastcall) void { /// Find an addon struct in the linked list by name and set +0x29 to 1, /// which excludes it from the flat display array built by DeserializeAddonData. fn hideAddonFromList(name: [*:0]const u8) void { - // Walk the addon linked list (PTR_00be1b6c). Each node has: - // +0x14: name pointer - // next: *(PTR_00be1b64 + 4 + node) const list_base = hook.readMem(u32, 0x00be1b64); var node = hook.readMem(u32, 0x00be1b6c); while (node != 0 and (node & 1) == 0) { @@ -348,10 +367,10 @@ fn callLoadAddonTOC(addon_name: [*:0]const u8) void { // Install / Remove // ============================================================================= -/// True if any embed_modules entry has addon files compiled in. +/// True if any module has embedded addon files compiled in. const has_addons = blk: { - for (embed_modules) |mod| { - if (mod.addon_name != null and @field(build_options, "enable_" ++ mod.option)) + for (module_names) |name| { + if (@field(build_options, "enable_" ++ name) and hasAddon(name) and getAddonFiles(name).len > 0) break :blk true; } break :blk false;