Extract addon management into addons.zig with is_active gating
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.
This commit is contained in:
+450
@@ -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();
|
||||
}
|
||||
+6
-415
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user