Fix marker crash on logout and add module lifecycle table

The marker cleanup hook on CleanupWorldAndEntities was never installed —
markers.installHooks() was missing from install() in main.zig. Entities
created via WorldMarker were never cleaned up before the game's atexit
handler iterated the hash table over freed heap memory.

Key changes:
- Add markers.installHooks() call (the actual crash fix)
- Replace manual install/uninstall/shutdown lists with a single modules
  table that drives all three phases — prevents this class of bug
- Gate marker Lua functions, addon, and keybindings behind isActive()
  so they're skipped when another DLL owns the hooks
- Add world_cleanup_hook.detach() to removeHooks() (was missing)
- Migrate from vendored libs/hook to external zhook dependency
- Unify installHooks return types to void across all modules
- Add diagnostic logging to marker cleanup (temporary, for testing)
This commit is contained in:
MarcelineVQ
2026-03-02 07:34:18 -08:00
parent 43313f439d
commit 475070e910
25 changed files with 1793 additions and 1749 deletions
+28 -94
View File
@@ -13,7 +13,7 @@
//! implementation function.
const std = @import("std");
const hook = @import("hook");
const hook = @import("zhook");
const api = @import("api.zig");
const o = @import("offsets.zig");
const types = @import("types.zig");
@@ -25,15 +25,19 @@ const wow = @import("wow.zig");
// Calling convention constants
// =============================================================================
const THISCALL = std.builtin.CallingConvention{ .x86_thiscall = .{} };
const tc: std.builtin.CallingConvention = .{ .x86_thiscall = .{} };
// =============================================================================
// Hook state
// =============================================================================
var render_draw_hook: hook.Hook = .{};
var manage_render_hook: hook.Hook = .{};
var draw_batch_hook: hook.Hook = .{};
const RenderDrawFn = fn (u32, u32, u32, u32, u32) callconv(tc) void;
const ManageRenderFn = fn (u32, u32) callconv(tc) void;
const DrawBatchFn = fn (u32) callconv(tc) void;
var render_draw_hook: hook.Detour(RenderDrawFn) = .{};
var manage_render_hook: hook.Detour(ManageRenderFn) = .{};
var draw_batch_hook: hook.Detour(DrawBatchFn) = .{};
/// D3D9 hooks are deferred until the first model hook fires, because creating
/// a dummy D3D9 device during engine init corrupts the proxy's state.
@@ -71,7 +75,7 @@ var reordered_indices: [MAX_REORDER]i32 = undefined;
// through other players and gear (since those aren't in depth when stencil
// is written). The outline composites on top of everything in EndScene.
fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(THISCALL) void {
fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(tc) void {
// One-time: install D3D9 hooks now that the game is actively rendering.
if (d3d9_deferred_pending) {
d3d9_deferred_pending = false;
@@ -80,7 +84,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
// Skip reordering if nothing to outline or too many batches
if (!tracker.enabled or !tracker.hasTargets() or batch_count == 0 or batch_count > MAX_REORDER) {
callOrigRenderDraw(this, view_matrix, batch_data, batch_indices, batch_count);
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
return;
}
@@ -102,7 +106,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
}
if (outline_count == 0) {
callOrigRenderDraw(this, view_matrix, batch_data, batch_indices, batch_count);
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
return;
}
@@ -134,26 +138,7 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
indices[i] = reordered_indices[i];
}
callOrigRenderDraw(this, view_matrix, batch_data, batch_indices, batch_count);
}
fn callOrigRenderDraw(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) void {
// __thiscall: ECX = this, stack = viewMatrix, batchData, batchIndices, batchCount
// Callee cleans 16 bytes (4 stack params).
// Pack args into a struct so we only need one "r" register to address them.
const args = [4]u32{ view_matrix, batch_data, batch_indices, batch_count };
asm volatile (
\\push 12(%[a])
\\push 8(%[a])
\\push 4(%[a])
\\push (%[a])
\\call *%[func]
:
: [_] "{ecx}" (this),
[a] "r" (&args),
[func] "r" (render_draw_hook.trampoline),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
);
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
}
// =============================================================================
@@ -162,22 +147,13 @@ fn callOrigRenderDraw(this: u32, view_matrix: u32, batch_data: u32, batch_indice
// __thiscall(model_ECX, addToList_stack)
// Native thiscall detour — no thunk needed.
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(THISCALL) void {
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(tc) void {
// Classify the model when it's being ADDED to the render list
if (add_to_list == 1 and model != 0 and tracker.enabled and tracker.hasTargets()) {
tracker.classifyModel(model);
}
// Call original: __thiscall(model_ECX, addToList_stack)
asm volatile (
\\push %[add]
\\call *%[func]
:
: [_] "{ecx}" (model),
[add] "r" (add_to_list),
[func] "r" (manage_render_hook.trampoline),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
);
manage_render_hook.callOriginal(.{ model, add_to_list });
}
// =============================================================================
@@ -189,26 +165,10 @@ fn manageRenderDetour(model: u32, add_to_list: u32) callconv(THISCALL) void {
// wrong ret instructions for functions with ≤2 register params. The naked
// function bridges fastcall → cdecl and calls the implementation function.
fn drawBatchProjEntry() callconv(.naked) void {
// __fastcall(ECX): ECX = render context, 0 stack args.
// Bridge to cdecl: push edx + ecx as args, call impl, cleanup, ret.
asm volatile (
\\push %%edx
\\push %%ecx
\\call *%%eax
\\add $8, %%esp
\\ret
:
: [_] "{eax}" (@intFromPtr(&drawBatchProjImpl))
);
}
fn drawBatchProjImpl(ctx: u32, _edx: u32) callconv(.c) void {
_ = _edx;
fn drawBatchProjDetour(ctx: u32) callconv(tc) void {
// Fast path: no tracking enabled or nothing tracked → just call original
if (!tracker.enabled or !tracker.hasTargets()) {
callOrigDrawBatch(ctx);
draw_batch_hook.callOriginal(.{ctx});
return;
}
@@ -224,60 +184,34 @@ fn drawBatchProjImpl(ctx: u32, _edx: u32) callconv(.c) void {
rendering_outline = true;
current_model = model_ptr;
callOrigDrawBatch(ctx);
draw_batch_hook.callOriginal(.{ctx});
rendering_outline = false;
current_model = 0;
} else {
// Normal rendering — no special handling needed
callOrigDrawBatch(ctx);
draw_batch_hook.callOriginal(.{ctx});
}
}
fn callOrigDrawBatch(ctx: u32) void {
asm volatile ("call *%[func]"
:
: [_] "{ecx}" (ctx),
[func] "r" (draw_batch_hook.trampoline),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
);
}
// =============================================================================
// Install / Remove
// =============================================================================
pub fn installHooks() bool {
// CM2SceneRenderDraw — native thiscall detour, no thunk needed.
// Prologue is 9 bytes: PUSH EBP (1) + MOV EBP,ESP (2) + SUB ESP,0x80 (6).
if (!render_draw_hook.install(
o.FN_CM2SCENE_RENDER_DRAW,
9,
@intFromPtr(&renderDrawDetour),
&.{},
)) return false;
if (render_draw_hook.attach(o.FN_CM2SCENE_RENDER_DRAW, &renderDrawDetour) != .ok)
return false;
// ManageRenderListNode — native thiscall detour, no thunk needed.
if (!manage_render_hook.install(
o.FN_CM2MODEL_MANAGE_RENDER_LIST,
6,
@intFromPtr(&manageRenderDetour),
&.{},
)) return false;
if (manage_render_hook.attach(o.FN_CM2MODEL_MANAGE_RENDER_LIST, &manageRenderDetour) != .ok)
return false;
// DrawBatchProj — naked entry bridges fastcall → cdecl, no thunk needed.
if (!draw_batch_hook.install(
o.FN_DRAW_BATCH_PROJ,
6,
@intFromPtr(&drawBatchProjEntry),
&.{},
)) return false;
if (draw_batch_hook.attach(o.FN_DRAW_BATCH_PROJ, &drawBatchProjDetour) != .ok)
return false;
return true;
}
pub fn removeHooks() void {
draw_batch_hook.remove();
manage_render_hook.remove();
render_draw_hook.remove();
draw_batch_hook.detach();
manage_render_hook.detach();
render_draw_hook.detach();
}