Refactor addon system: derive module list from build.zig, prune inactive prefixes at runtime
- Eliminate hardcoded module_names in addons.zig — now derived from
build.zig via all_module_names build option
- Add module_active.zig runtime registry: main.zig registers isActive
pointers during install(), addons.zig queries them without importing
each module directly
- Prune embedded file prefixes at startup: after all modules claim
mutexes, build a runtime active_prefixes table excluding modules we
don't own. findEmbeddedFile searches only active entries — no
per-lookup isActive check on the hot path
- Tag both addon and asset prefixes with module_name so pruning applies
to all embedded files for a module
- Rename src/markers/ → src/worldmarkers/, src/outline/api.zig →
src/outline/outline.zig to follow {name}/{name}.zig convention
This commit is contained in:
@@ -18,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", .addon_hidden = true },
|
||||
.{ .name = "worldmarkers", .desc = "Enable world markers module", .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" },
|
||||
@@ -30,6 +30,7 @@ const module_list = [_]ModuleDesc{
|
||||
.{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false },
|
||||
.{ .name = "transform44", .desc = "Enable transformMatrix4x4 hook", .default = false },
|
||||
.{ .name = "addonperf", .desc = "Enable addon memory/CPU profiling API" },
|
||||
.{ .name = "file_perf", .desc = "Enable file access profiling", .default = false, .addon_name = "FilePerf", .addon_hidden = true },
|
||||
};
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
@@ -86,6 +87,14 @@ pub fn build(b: *std.Build) void {
|
||||
opts.addOption(bool, "enable_" ++ m.name, std.mem.eql(u8, m.name, variant_mod.name));
|
||||
}
|
||||
addFileListOptions(b, opts);
|
||||
// Variant builds also need the module name list for addons.zig
|
||||
const names: []const []const u8 = comptime blk: {
|
||||
var n: [module_list.len][]const u8 = undefined;
|
||||
for (module_list, 0..) |m2, mi| n[mi] = m2.name;
|
||||
const final = n;
|
||||
break :blk &final;
|
||||
};
|
||||
opts.addOption([]const []const u8, "all_module_names", names);
|
||||
|
||||
const variant_lib = b.addLibrary(.{
|
||||
.name = variant_mod.name,
|
||||
@@ -116,6 +125,14 @@ fn addModuleOptions(b: *std.Build, opts: *std.Build.Step.Options) void {
|
||||
inline for (module_list) |mod| {
|
||||
opts.addOption(bool, "enable_" ++ mod.name, b.option(bool, mod.name, mod.desc) orelse mod.default);
|
||||
}
|
||||
// Pass full module name list so addons.zig doesn't need a hardcoded copy
|
||||
const names: []const []const u8 = comptime blk: {
|
||||
var n: [module_list.len][]const u8 = undefined;
|
||||
for (module_list, 0..) |mod, i| n[i] = mod.name;
|
||||
const final = n;
|
||||
break :blk &final;
|
||||
};
|
||||
opts.addOption([]const []const u8, "all_module_names", names);
|
||||
addFileListOptions(b, opts);
|
||||
}
|
||||
|
||||
|
||||
+51
-31
@@ -22,35 +22,18 @@ var log: logging.Logger = .{};
|
||||
const build_options = @import("build_options");
|
||||
|
||||
// =============================================================================
|
||||
// Module list — must match build.zig module_list names.
|
||||
// This is the only place module names appear; everything else is derived.
|
||||
// Module list — derived from build.zig via all_module_names build option.
|
||||
// Convention: each module's main source is at {name}/{name}.zig.
|
||||
// =============================================================================
|
||||
|
||||
const module_names = [_][]const u8{
|
||||
"interact",
|
||||
"outline",
|
||||
"worldmarkers",
|
||||
"logsessions",
|
||||
"minimapicons",
|
||||
"dpslog",
|
||||
};
|
||||
const module_names = build_options.all_module_names;
|
||||
|
||||
// =============================================================================
|
||||
// is_active wiring — convention-based imports
|
||||
// is_active wiring — runtime registry populated by main.zig during install().
|
||||
// No per-module imports needed here; adding addon_name in build.zig is enough.
|
||||
// =============================================================================
|
||||
|
||||
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;
|
||||
}
|
||||
const module_active = @import("module_active.zig");
|
||||
|
||||
// =============================================================================
|
||||
// Embedded file types
|
||||
@@ -64,6 +47,8 @@ pub const FileEntry = struct {
|
||||
const AddonPrefix = struct {
|
||||
prefix: []const u8,
|
||||
files: []const FileEntry,
|
||||
/// Module name for runtime isActive gating. Null = always serve.
|
||||
module_name: ?[*:0]const u8 = null,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
@@ -156,7 +141,7 @@ fn countUniqueAssetDirs(comptime paths: []const []const u8, comptime assets_pref
|
||||
return n;
|
||||
}
|
||||
|
||||
fn appendAssetPrefixes(result: anytype, start_idx: usize, comptime paths: []const []const u8, comptime assets_prefix: []const u8) usize {
|
||||
fn appendAssetPrefixes(result: anytype, start_idx: usize, comptime paths: []const []const u8, comptime assets_prefix: []const u8, comptime mod_name: ?[*:0]const u8) usize {
|
||||
comptime {
|
||||
var dirs: [paths.len][]const u8 = undefined;
|
||||
var n_dirs: usize = 0;
|
||||
@@ -191,7 +176,7 @@ fn appendAssetPrefixes(result: anytype, start_idx: usize, comptime paths: []cons
|
||||
}
|
||||
}
|
||||
const final_files = files;
|
||||
result[idx] = .{ .prefix = dir, .files = &final_files };
|
||||
result[idx] = .{ .prefix = dir, .files = &final_files, .module_name = mod_name };
|
||||
idx += 1;
|
||||
}
|
||||
return idx;
|
||||
@@ -250,18 +235,20 @@ fn buildAllPrefixes() []const AddonPrefix {
|
||||
if (!hasAddon(name)) continue;
|
||||
const addon_name = @field(build_options, name ++ "_addon_name");
|
||||
const addon_files = getAddonFiles(name);
|
||||
const name_z: [*:0]const u8 = (name ++ "\x00").ptr;
|
||||
if (addon_files.len > 0) {
|
||||
const files = embedFiles(addon_files);
|
||||
result[idx] = .{
|
||||
.prefix = "Interface\\AddOns\\" ++ addon_name ++ "\\",
|
||||
.files = &files,
|
||||
.module_name = name_z,
|
||||
};
|
||||
idx += 1;
|
||||
}
|
||||
const asset_files = getAssetFiles(name);
|
||||
if (asset_files.len > 0) {
|
||||
const pfx = assetsPrefixFromOpt(name ++ "_asset_files");
|
||||
idx = appendAssetPrefixes(&result, idx, asset_files, pfx);
|
||||
idx = appendAssetPrefixes(&result, idx, asset_files, pfx, name_z);
|
||||
}
|
||||
}
|
||||
const final = result;
|
||||
@@ -269,16 +256,37 @@ fn buildAllPrefixes() []const AddonPrefix {
|
||||
}
|
||||
}
|
||||
|
||||
const addon_prefixes = buildAllPrefixes();
|
||||
const all_prefixes = buildAllPrefixes();
|
||||
|
||||
// =============================================================================
|
||||
// Embedded file lookup
|
||||
// Runtime-pruned prefix table — only includes prefixes for modules that own
|
||||
// their mutex. Built once in install() after all modules have claimed mutexes.
|
||||
// findEmbeddedFile searches only this table, no per-lookup isActive checks.
|
||||
// =============================================================================
|
||||
|
||||
var active_prefixes: [all_prefixes.len]*const AddonPrefix = undefined;
|
||||
var active_count: usize = 0;
|
||||
|
||||
fn pruneInactivePrefixes() void {
|
||||
active_count = 0;
|
||||
for (all_prefixes) |*prefix| {
|
||||
const active = if (prefix.module_name) |mod_name| module_active.isActive(mod_name) else true;
|
||||
if (active) {
|
||||
active_prefixes[active_count] = prefix;
|
||||
active_count += 1;
|
||||
}
|
||||
}
|
||||
log.fmt("addon prefixes: {d}/{d} active\n", .{ active_count, all_prefixes.len });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Embedded file lookup — searches only active (mutex-owning) prefixes.
|
||||
// =============================================================================
|
||||
|
||||
pub fn findEmbeddedFile(path: [*:0]const u8) ?*const FileEntry {
|
||||
const path_span = std.mem.span(path);
|
||||
|
||||
for (addon_prefixes) |*addon| {
|
||||
for (active_prefixes[0..active_count]) |addon| {
|
||||
if (path_span.len <= addon.prefix.len) continue;
|
||||
|
||||
// Case-insensitive prefix check
|
||||
@@ -323,13 +331,24 @@ 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});
|
||||
|
||||
// Register addons only for active (mutex-owning) modules.
|
||||
// Use the pruned prefix table — each addon prefix with files corresponds
|
||||
// to an Interface\AddOns\ entry. We match by module_name to find which
|
||||
// addon_name to register.
|
||||
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 (comptime moduleIsActive(name)) |f| f() else true;
|
||||
if (active) {
|
||||
const module_name_z: [*:0]const u8 = comptime (name ++ "\x00").ptr;
|
||||
// Check if this module survived pruning (i.e., owns its mutex)
|
||||
const is_active = for (active_prefixes[0..active_count]) |p| {
|
||||
if (p.module_name) |mn| {
|
||||
if (std.mem.orderZ(u8, mn, module_name_z) == .eq) break true;
|
||||
}
|
||||
} else false;
|
||||
|
||||
if (is_active) {
|
||||
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});
|
||||
@@ -379,6 +398,7 @@ const has_addons = blk: {
|
||||
pub fn install() void {
|
||||
if (!has_addons) return;
|
||||
log = logging.Logger.open("addons", .console);
|
||||
pruneInactivePrefixes();
|
||||
_ = setup_addons_hook.attach(0x51C740, &setupAddonsDetour);
|
||||
}
|
||||
|
||||
|
||||
+42
-22
@@ -20,13 +20,14 @@ const build_opts = struct {
|
||||
const dpslog = @import("build_options").enable_dpslog;
|
||||
const transform44 = @import("build_options").enable_transform44;
|
||||
const addonperf = @import("build_options").enable_addonperf;
|
||||
const file_perf = @import("build_options").enable_file_perf;
|
||||
};
|
||||
|
||||
// Conditional module imports
|
||||
const screenshot = if (build_opts.screenshot) @import("screenshot/screenshot.zig") else struct {};
|
||||
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 outline = if (build_opts.outline) @import("outline/outline.zig") else struct {};
|
||||
const markers = if (build_opts.worldmarkers) @import("worldmarkers/worldmarkers.zig") else struct {};
|
||||
const framecrash = if (build_opts.framecrash) @import("framecrash/framecrash.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 {};
|
||||
@@ -38,20 +39,12 @@ const clickthrough = if (build_opts.clickthrough) @import("clickthrough/clickthr
|
||||
const dpslog = if (build_opts.dpslog) @import("dpslog/dpslog.zig") else struct {};
|
||||
const transform44 = if (build_opts.transform44) @import("transform44/transform44.zig") else struct {};
|
||||
const addonperf = if (build_opts.addonperf) @import("addonperf/addonperf.zig") else struct {};
|
||||
const file_perf = if (build_opts.file_perf) @import("file_perf/file_perf.zig") else struct {};
|
||||
|
||||
const module_active = @import("module_active.zig");
|
||||
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
|
||||
const SYSTEMTIME = extern struct {
|
||||
wYear: u16,
|
||||
wMonth: u16,
|
||||
wDayOfWeek: u16,
|
||||
wDay: u16,
|
||||
wHour: u16,
|
||||
wMinute: u16,
|
||||
wSecond: u16,
|
||||
wMilliseconds: u16,
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Lua Protection Bypass
|
||||
// =============================================================================
|
||||
@@ -120,6 +113,9 @@ fn registerLuaFunctions() void {
|
||||
registerFunction("ResetAddOnCPUUsage", @intFromPtr(&addonperf.luaResetAddOnCPUUsage));
|
||||
registerFunction("GetScriptCPUUsage", @intFromPtr(&addonperf.luaGetScriptCPUUsage));
|
||||
}
|
||||
if (build_opts.file_perf and file_perf.isActive()) {
|
||||
registerFunction("ResetFilePerfCounters", @intFromPtr(&file_perf.luaResetCounters));
|
||||
}
|
||||
if (build_opts.worldmarkers and markers.isActive()) {
|
||||
// User-facing functions stay global
|
||||
registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker));
|
||||
@@ -211,7 +207,6 @@ const LoadModelFn = fn (u32, u32, u32) callconv(hook.cc.thiscall) u32;
|
||||
var model_load_hook: hook.Detour(LoadModelFn) = .{};
|
||||
|
||||
// Windows API imports for async handling
|
||||
extern "kernel32" fn GetLocalTime(lpSystemTime: *SYSTEMTIME) callconv(WINAPI) void;
|
||||
extern "kernel32" fn EnterCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void;
|
||||
extern "kernel32" fn LeaveCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void;
|
||||
extern "kernel32" fn SetEvent(hEvent: *anyopaque) callconv(WINAPI) i32;
|
||||
@@ -249,11 +244,6 @@ fn openFileDetour(
|
||||
flags: u32,
|
||||
handle_out: *u32,
|
||||
) callconv(hook.cc.stdcall) u32 {
|
||||
var st: SYSTEMTIME = undefined;
|
||||
GetLocalTime(&st);
|
||||
log.to(.file).fmt("{d}/{d} {d:0>2}:{d:0>2}:{d:0>2}.{d:0>3} {s}\n", .{
|
||||
st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, std.mem.span(path),
|
||||
});
|
||||
if (findEmbeddedFile(path)) |entry| {
|
||||
// Allocate and zero-fill 0x60-byte file context
|
||||
const ctx = allocateGameBuffer(0x60) orelse {
|
||||
@@ -283,7 +273,10 @@ fn openFileDetour(
|
||||
return 2; // success (non-zero type code)
|
||||
}
|
||||
|
||||
return open_file_hook.callOriginal(.{ archive_ptr, path, flags, handle_out });
|
||||
const tsc_start = if (build_opts.file_perf) file_perf.beginOpen() else 0;
|
||||
const ret = open_file_hook.callOriginal(.{ archive_ptr, path, flags, handle_out });
|
||||
if (build_opts.file_perf) file_perf.endOpen(path, tsc_start);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// --- Hook 2: GetFileSizeFromHandle (0x6487f0) ---
|
||||
@@ -299,7 +292,17 @@ fn getFileSizeDetour(
|
||||
return size;
|
||||
}
|
||||
|
||||
return get_file_size_hook.callOriginal(.{ file_ctx, high_size_out });
|
||||
const size = get_file_size_hook.callOriginal(.{ file_ctx, high_size_out });
|
||||
|
||||
// Track file sizes for cache budget estimation
|
||||
if (build_opts.file_perf and file_perf.isActive()) {
|
||||
const path_ptr = hook.readMem(u32, file_ctx + 0x0C);
|
||||
if (path_ptr != 0) {
|
||||
file_perf.recordFileSize(@as([*:0]const u8, @ptrFromInt(path_ptr)), size);
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
// --- Hook 3: ReadFileFromMultipleSources (0x648460) ---
|
||||
@@ -335,7 +338,19 @@ fn readFileDetour(
|
||||
return 1; // success
|
||||
}
|
||||
|
||||
return read_file_hook.callOriginal(.{ ctx, buffer, size, bytes_read_out, async_ptr, param6 });
|
||||
const ret = read_file_hook.callOriginal(.{ ctx, buffer, size, bytes_read_out, async_ptr, param6 });
|
||||
|
||||
// Track reads for profiling — caller return address tells us who calls ReadFile
|
||||
if (build_opts.file_perf and file_perf.isActive() and ret != 0) {
|
||||
const path_ptr = hook.readMem(u32, ctx + 0x0C);
|
||||
if (path_ptr != 0) {
|
||||
const actual_read = if (bytes_read_out) |out| out.* else size;
|
||||
const caller_addr: u32 = @truncate(@returnAddress());
|
||||
file_perf.recordRead(@as([*:0]const u8, @ptrFromInt(path_ptr)), actual_read, caller_addr);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// --- Hook 4: processAsyncFileOperation (0x647350) ---
|
||||
@@ -647,6 +662,7 @@ const modules = [_]ModuleHooks{
|
||||
if (build_opts.dpslog) .{ .name = dpslog.module_name, .install = dpslog.installHooks, .remove = dpslog.removeHooks, .is_active = dpslog.isActive } else .{},
|
||||
if (build_opts.transform44) .{ .name = transform44.module_name, .install = transform44.installHooks, .remove = transform44.removeHooks, .is_active = transform44.isActive } else .{},
|
||||
if (build_opts.addonperf) .{ .name = addonperf.module_name, .install = addonperf.installHooks, .remove = addonperf.removeHooks, .is_active = addonperf.isActive } else .{},
|
||||
if (build_opts.file_perf) .{ .name = file_perf.module_name, .install = file_perf.installHooks, .remove = file_perf.removeHooks, .is_active = file_perf.isActive } else .{},
|
||||
if (build_opts.worldmarkers) .{ .name = markers.module_name, .install = markers.installHooks, .remove = markers.removeHooks, .is_active = markers.isActive } else .{},
|
||||
if (build_opts.interact) .{ .name = interact.module_name, .install = interact.installHooks, .remove = interact.removeHooks, .is_active = interact.isActive } else .{},
|
||||
if (build_opts.outline) .{ .name = outline.module_name, .remove = outline.cleanup, .is_active = outline.isActive } else .{},
|
||||
@@ -675,6 +691,10 @@ fn install() void {
|
||||
|
||||
inline for (modules) |m| {
|
||||
if (m.install) |inst| inst();
|
||||
// Register isActive for addons.zig runtime lookup
|
||||
if (m.name) |name| {
|
||||
if (m.is_active) |f| module_active.register(name, f);
|
||||
}
|
||||
}
|
||||
|
||||
addons.install();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Runtime registry for module isActive() function pointers.
|
||||
// Populated by main.zig during install(), queried by addons.zig during login.
|
||||
// Exists solely to avoid duplicating module imports in addons.zig.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const MAX = 32;
|
||||
var names: [MAX][*:0]const u8 = undefined;
|
||||
var fns: [MAX]*const fn () bool = undefined;
|
||||
var count: usize = 0;
|
||||
|
||||
/// Register a module's isActive function. Called from main.zig install().
|
||||
pub fn register(name: [*:0]const u8, f: *const fn () bool) void {
|
||||
if (count >= MAX) return;
|
||||
names[count] = name;
|
||||
fns[count] = f;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
/// Check if a module is active by name. Returns true if unknown (no isActive).
|
||||
pub fn isActive(name: [*:0]const u8) bool {
|
||||
for (0..count) |i| {
|
||||
if (std.mem.orderZ(u8, names[i], name) == .eq) return fns[i]();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const api = @import("api.zig");
|
||||
const api = @import("outline.zig");
|
||||
const o = @import("offsets.zig");
|
||||
const types = @import("types.zig");
|
||||
const tracker = @import("tracker.zig");
|
||||
|
||||
Reference in New Issue
Block a user