Refactor addon system to be data-driven from build.zig

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.
This commit is contained in:
MarcelineVQ
2026-03-12 10:36:24 -07:00
parent e0da2791d9
commit 5927568fe8
2 changed files with 114 additions and 89 deletions
+10 -4
View File
@@ -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: <module>_addon_files = ["screenshot/addon/Screenshot.lua", ...]
if (mod.addon_name != null) {
// Addon: <module>_addon_name, <module>_addon_hidden, <module>_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;
+104 -85
View File
@@ -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;