Move marker animation tick to DLL-side OnWorldUpdate hook

- Hook OnWorldUpdate (0x482EA0) for per-frame tick while world is active
- Remove Lua OnUpdate animation driver (ProcessMarkerAnimations)
- Add DistanceToMark(index) Lua API returning player-to-marker distance
- Track marker positions DLL-side for distance queries
- Clean up positions on marker clear and world teardown
This commit is contained in:
MarcelineVQ
2026-03-02 08:31:23 -08:00
parent 4b6e4ceaa9
commit e474b61c37
4 changed files with 99 additions and 82 deletions
+1 -1
View File
@@ -262,7 +262,7 @@ fn registerLuaFunctions() void {
registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker));
registerFunction("ClearWorldMarker", @intFromPtr(&markers.luaClearWorldMarker));
registerFunction("GetPlayerPosition", @intFromPtr(&markers.luaGetPlayerPosition));
registerFunction("ProcessMarkerAnimations", @intFromPtr(&markers.luaProcessAnimations));
registerFunction("DistanceToMark", @intFromPtr(&markers.luaDistanceToMark));
}
}
+31 -67
View File
@@ -14,78 +14,42 @@ frame:SetScript("OnEvent", function()
end
end)
-- Per-frame animation driver: queues Hold after Stand finishes on new
-- markers and re-queues it periodically so it never falls back to Stand.
local animFrame = CreateFrame("Frame")
animFrame:SetScript("OnUpdate", function()
ProcessMarkerAnimations()
end)
SLASH_MARKERS1 = "/markers"
SLASH_MARKERS2 = "/mark"
SlashCmdList["MARKERS"] = function(msg)
SLASH_WORLDMARKER1 = "/worldmarker"
SLASH_WORLDMARKER2 = "/wm"
SlashCmdList["WORLDMARKER"] = function(msg)
msg = string.lower(msg or "")
if msg == "" or msg == "help" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r commands:")
DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> - Place marker at cursor")
DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> <unit> - Place marker at unit")
DEFAULT_CHAT_FRAME:AddMessage(" /mark <1-5> <x> <y> <z> - Place marker at coords")
DEFAULT_CHAT_FRAME:AddMessage(" /mark clear [1-5] - Clear one or all markers")
DEFAULT_CHAT_FRAME:AddMessage(" /mark pos - Show player position")
-- Parse arguments
local parts = {}
for word in string.gfind(msg, "%S+") do
table.insert(parts, word)
end
elseif msg == "clear" then
ClearWorldMarker()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers:|r all cleared")
elseif msg == "pos" or msg == "position" then
local x, y, z = GetPlayerPosition()
if x then
DEFAULT_CHAT_FRAME:AddMessage(string.format("Position: %.2f, %.2f, %.2f", x, y, z))
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000No player position available|r")
end
local index = tonumber(parts[1])
if not index then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00World Markers|r commands:")
DEFAULT_CHAT_FRAME:AddMessage(" /wm 1-5 - Place marker at cursor")
DEFAULT_CHAT_FRAME:AddMessage(" /wm 1-5 <unit> - Place marker at unit")
DEFAULT_CHAT_FRAME:AddMessage(" /cwm [1-5] - Clear one or all markers")
return
end
if parts[2] then
-- /wm 1 target
WorldMarker(index, parts[2])
else
-- Parse arguments
local parts = {}
for word in string.gfind(msg, "%S+") do
table.insert(parts, word)
end
-- /wm 1 (cursor position)
WorldMarker(index)
end
end
-- /mark clear <index>
if parts[1] == "clear" then
local index = tonumber(parts[2])
if index then
ClearWorldMarker(index)
else
ClearWorldMarker()
end
return
end
local index = tonumber(parts[1])
if not index then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /mark <1-5> [unit | x y z] or /mark clear")
return
end
if tonumber(parts[2]) then
-- /mark 1 x y z
local x = tonumber(parts[2])
local y = tonumber(parts[3])
local z = tonumber(parts[4])
if x and y and z then
WorldMarker(index, x, y, z)
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Usage:|r /mark <1-5> <x> <y> <z>")
end
elseif parts[2] then
-- /mark 1 target
WorldMarker(index, parts[2])
else
-- /mark 1 (cursor position)
WorldMarker(index)
end
SLASH_CLEARWORLDMARKER1 = "/clearworldmarker"
SLASH_CLEARWORLDMARKER2 = "/cwm"
SlashCmdList["CLEARWORLDMARKER"] = function(msg)
local index = tonumber(msg or "")
if index then
ClearWorldMarker(index)
else
ClearWorldMarker()
end
end
+58 -14
View File
@@ -47,11 +47,11 @@ const ANIM_DECAY: u32 = 159; // 666ms shrink-out
const DECAY_DURATION_MS: u32 = 650; // slightly over 666ms to ensure animation completes
const MODEL_PATHS = [NUM_MARKERS][*:0]const u8{
"Spells\\Raid_UI_FX_Yellow.m2",
"Spells\\Raid_UI_FX_Cyan.m2",
"Spells\\Raid_UI_FX_Green.m2",
"Spells\\Raid_UI_FX_Purple.m2",
"Spells\\Raid_UI_FX_Red.m2",
"Spells\\Raid_UI_FX_Cyan.m2", // 1 - Blue Square
"Spells\\Raid_UI_FX_Green.m2", // 2 - Green Triangle
"Spells\\Raid_UI_FX_Purple.m2", // 3 - Purple Diamond
"Spells\\Raid_UI_FX_Red.m2", // 4 - Red Cross
"Spells\\Raid_UI_FX_Yellow.m2", // 5 - Yellow Star
};
// =============================================================================
@@ -69,13 +69,14 @@ pub const Vec3 = struct {
// =============================================================================
var marker_entities: [NUM_MARKERS]?*anyopaque = .{null} ** NUM_MARKERS;
var marker_positions: [NUM_MARKERS]?Vec3 = .{null} ** NUM_MARKERS;
var marker_created_tick: [NUM_MARKERS]u32 = .{0} ** NUM_MARKERS;
var hold_queued: [NUM_MARKERS]bool = .{false} ** NUM_MARKERS;
const STAND_DURATION_MS: u32 = 3900;
// Entities playing their Decay animation before destruction.
// Cleaned up every frame by luaProcessAnimations (via Lua OnUpdate).
// Cleaned up every frame by tickAnimations (via OnWorldUpdate hook).
const MAX_DESPAWNING = 8;
const DespawningEntity = struct {
entity: *anyopaque,
@@ -322,10 +323,11 @@ fn placeMarker(index: usize, pos: Vec3) bool {
};
// Stand (grow-in) is queued by the engine in CM2Model_CreateForModelObject.
// Hold is deferred to the per-frame callback (luaProcessAnimations) once
// Hold is deferred to the per-frame callback (tickAnimations) once
// the model is fully initialised (model+0x10 != 0), so it doesn't clobber
// Stand in the command queue.
marker_entities[index] = obj;
marker_positions[index] = pos;
hold_queued[index] = false;
marker_created_tick[index] = GetTickCount();
con.fmt("[markers] marker {d} placed at {d:.1}, {d:.1}, {d:.1}\n", .{ index + 1, pos.x, pos.y, pos.z });
@@ -339,6 +341,7 @@ fn clearMarker(index: usize) void {
if (marker_entities[index]) |existing| {
beginDespawn(existing);
marker_entities[index] = null;
marker_positions[index] = null;
hold_queued[index] = false;
}
}
@@ -351,6 +354,7 @@ fn clearAllMarkers() void {
if (marker_entities[i]) |existing| {
beginDespawn(existing);
marker_entities[i] = null;
marker_positions[i] = null;
any = true;
}
}
@@ -435,10 +439,9 @@ pub fn luaClearWorldMarker(L: u32) callconv(.c) u32 {
return 0;
}
/// Per-frame callback (driven by Lua OnUpdate).
/// Per-frame animation tick (driven by OnWorldUpdate hook).
/// Queues Hold once after Stand finishes, then cleans up despawning entities.
pub fn luaProcessAnimations(L: u32) callconv(.c) u32 {
_ = L;
fn tickAnimations() void {
const now = GetTickCount();
cleanupDespawning();
@@ -451,8 +454,6 @@ pub fn luaProcessAnimations(L: u32) callconv(.c) u32 {
playAnimation(entity, ANIM_HOLD, true);
hold_queued[i] = true;
}
return 0;
}
/// Lua: local x, y, z = GetPlayerPosition()
@@ -467,11 +468,46 @@ pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 {
return 3;
}
/// Lua: local dist = DistanceToMark(index)
/// Returns distance in yards from player to the stored marker position,
/// or nil if the marker doesn't exist or there's no player.
pub fn luaDistanceToMark(L: u32) callconv(.c) u32 {
const nargs = lapi.gettop(L);
if (nargs < 1 or !lapi.isnumber(L, 1)) return 0;
const raw_index = @as(i32, @intFromFloat(lapi.tonumber(L, 1)));
if (raw_index < 1 or raw_index > NUM_MARKERS) return 0;
const index: usize = @intCast(raw_index - 1);
const mark_pos = marker_positions[index] orelse return 0;
const player = wow.getLocalPlayer();
if (player == 0) return 0;
const player_pos = getUnitPosition(player);
if (player_pos.x == 0 and player_pos.y == 0 and player_pos.z == 0) return 0;
const dx = player_pos.x - mark_pos.x;
const dy = player_pos.y - mark_pos.y;
const dz = player_pos.z - mark_pos.z;
const dist = @sqrt(dx * dx + dy * dy + dz * dz);
lapi.pushnumber(L, @floatCast(dist));
return 1;
}
// =============================================================================
// World teardown hook
// =============================================================================
var world_cleanup_hook: hook.Detour(fn () callconv(sc) void) = .{};
var world_update_hook: hook.Detour(fn (u32) callconv(fc) void) = .{};
/// OnWorldUpdate hook — per-frame tick while world is active.
/// Drives animation state (Hold queue after Stand, despawn cleanup).
fn worldUpdateDetour(frame: u32) callconv(fc) void {
tickAnimations();
world_update_hook.callOriginal(.{frame});
}
/// Pre-hook on CleanupWorldAndEntities (0x66fc40).
/// Destroys all our entities via CleanupEntity_ProcessAttachments before the
@@ -501,6 +537,7 @@ fn destroyAllEntities() void {
}
for (&hold_queued) |*h| h.* = false;
for (&marker_created_tick) |*t| t.* = 0;
for (&marker_positions) |*p| p.* = null;
for (&despawning, 0..) |*slot, i| {
if (slot.*) |d| {
@@ -542,10 +579,16 @@ pub fn installHooks() void {
}
g_is_hook_owner = true;
// Hook OnWorldUpdate for per-frame animation tick (runs every frame while world is active).
if (world_update_hook.attach(o.FN_ON_WORLD_UPDATE, &worldUpdateDetour) != .ok) {
con.print("[markers] FAILED to hook OnWorldUpdate!\n");
} else {
con.print("[markers] hooked OnWorldUpdate OK\n");
}
// Hook CleanupWorldAndEntities to destroy our entities before world teardown.
// This fires on map change, logout, AND exit — before heaps are destroyed.
const hook_result = world_cleanup_hook.attach(o.FN_CLEANUP_WORLD_AND_ENTITIES, &worldCleanupDetour);
if (hook_result != .ok) {
if (world_cleanup_hook.attach(o.FN_CLEANUP_WORLD_AND_ENTITIES, &worldCleanupDetour) != .ok) {
con.print("[markers] FAILED to hook CleanupWorldAndEntities!\n");
} else {
con.print("[markers] hooked CleanupWorldAndEntities OK\n");
@@ -557,6 +600,7 @@ pub fn removeHooks() void {
// destroyAllEntities is idempotent — if worldCleanupDetour already ran,
// all slots are null and this is a no-op.
destroyAllEntities();
world_update_hook.detach();
world_cleanup_hook.detach();
}
+9
View File
@@ -91,6 +91,15 @@ pub const WF_HIT_TERRAIN_X: usize = 0x360;
pub const WF_HIT_TERRAIN_Y: usize = 0x364;
pub const WF_HIT_TERRAIN_Z: usize = 0x368;
// =============================================================================
// Per-frame world update
// =============================================================================
/// OnWorldUpdate — __fastcall(ECX=worldFrame), no stack params, void return.
/// Called every frame while the world is active (in-game, not login screen).
/// Part of CGWorldFrame update pipeline.
pub const FN_ON_WORLD_UPDATE: usize = 0x00482EA0;
// =============================================================================
// Model creation
// =============================================================================