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:
+28
-94
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user