Add markers module, framecrash stub, debug console, build-flag wiring

Markers: client-side world object system using CreateGameObject. Places
M2 models at arbitrary world positions via Lua commands (/mark test,
/mark pos). Includes embedded addon, xyz.m2/blp assets served from DLL
memory, position helpers from unit movement struct, and object lifecycle
management (create, cleanup, reposition, alpha, animation).

Framecrash: stub module with reference to crash at 0x007A2452.

Console: debug output via AllocConsole/WriteConsoleA, compiles out
entirely in non-Debug builds. Used by markers and file serve logging.

Build: markers added to feature flag matrix and all-variants step.
Main: conditional markers import, Lua function registration, embedded
addon + asset file serving, console init/deinit lifecycle.

Outline README: added misc planned features and debug mode notes.
This commit is contained in:
MarcelineVQ
2026-02-26 15:30:21 -08:00
parent cd7e585412
commit 37d42d09c2
12 changed files with 548 additions and 5 deletions
+5
View File
@@ -0,0 +1,5 @@
<Bindings>
<Binding name="Toggle Test Marker" header="MARKERS">
TestMarkerToggle()
</Binding>
</Bindings>
+46
View File
@@ -0,0 +1,46 @@
-- Markers addon (embedded in DLL, loaded from memory)
-- Part of WeirdUtils - only loaded when markers module is compiled
MARKERS_VERSION = 1
BINDING_HEADER_MARKERS = "Markers"
local frame = CreateFrame("Frame")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")
frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r v" .. MARKERS_VERSION .. " loaded")
end
end)
SLASH_MARKERS1 = "/markers"
SLASH_MARKERS2 = "/mark"
SlashCmdList["MARKERS"] = 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 test - Toggle test marker at player position")
DEFAULT_CHAT_FRAME:AddMessage(" /mark pos - Show player position")
DEFAULT_CHAT_FRAME:AddMessage(" /mark on - Create test marker")
DEFAULT_CHAT_FRAME:AddMessage(" /mark off - Destroy test marker")
elseif msg == "test" or msg == "toggle" then
TestMarkerToggle()
elseif msg == "on" or msg == "create" then
TestMarkerCreate()
elseif msg == "off" or msg == "destroy" then
TestMarkerDestroy()
elseif msg == "pos" or msg == "position" then
local x, y, z = GetPlayerPosition()
DEFAULT_CHAT_FRAME:AddMessage(string.format("Position: %.2f, %.2f, %.2f", x, y, z))
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Unknown command:|r " .. msg)
end
end
+8
View File
@@ -0,0 +1,8 @@
## Interface: 11200
## Title: Markers
## Notes: Client-side raid markers and indicators
## Version: 1.0
## Dependencies: WeirdUtils
Markers.lua
Bindings.xml
Binary file not shown.
Binary file not shown.
+271
View File
@@ -0,0 +1,271 @@
//! Client-side marker system using CreateGameObject_WithProperties
//!
//! Creates world objects at arbitrary positions for raid markers,
//! player indicators, etc.
const hook = @import("hook");
const o = @import("offsets.zig");
const wow = @import("../outline/wow.zig");
const con = @import("../console.zig");
// =============================================================================
// Position helpers
// =============================================================================
pub const Vec3 = struct {
x: f32,
y: f32,
z: f32,
};
/// Get unit position from movement struct
pub fn getUnitPosition(unit: u32) Vec3 {
if (unit == 0) return .{ .x = 0, .y = 0, .z = 0 };
const movement = hook.readMem(u32, unit + o.UNIT_MOVEMENT_OFFSET);
if (movement == 0 or movement < 0x10000) return .{ .x = 0, .y = 0, .z = 0 };
return .{
.x = hook.readMem(f32, movement + o.MOVEMENT_POS_X),
.y = hook.readMem(f32, movement + o.MOVEMENT_POS_Y),
.z = hook.readMem(f32, movement + o.MOVEMENT_POS_Z),
};
}
// =============================================================================
// GameObject creation - function pointer wrappers
// =============================================================================
/// CreateGameObject_WithProperties — __fastcall:
/// ECX = model, EDX = callback1
/// Stack (callee-clean, RET 0x14): callback2, x, y, z, flags
pub fn createGameObject(
model: ?*anyopaque,
callback1: ?*anyopaque,
callback2: ?*anyopaque,
x: f32,
y: f32,
z: f32,
flags: u32,
) ?*anyopaque {
// 5 stack params (callee cleans via RET 0x14)
const stack_args = [5]u32{
if (callback2) |c| @intCast(@intFromPtr(c)) else 0,
@as(u32, @bitCast(x)),
@as(u32, @bitCast(y)),
@as(u32, @bitCast(z)),
flags,
};
const result: u32 = asm volatile (
\\ push 16(%[a])
\\ push 12(%[a])
\\ push 8(%[a])
\\ push 4(%[a])
\\ push (%[a])
\\ call *%[func]
: [ret] "={eax}" (-> u32),
: [_] "{ecx}" (if (model) |m| @intFromPtr(m) else @as(usize, 0)),
[_] "{edx}" (if (callback1) |c| @intFromPtr(c) else @as(usize, 0)),
[a] "r" (&stack_args),
[func] "r" (o.FN_CREATE_GAMEOBJECT),
: .{ .memory = true, .cc = true }
);
return if (result != 0) @ptrFromInt(result) else null;
}
/// CleanupWorldObject(obj) - __thiscall: ECX = obj, 0 stack params
pub fn cleanupWorldObject(obj: *anyopaque) void {
asm volatile ("call *%[func]"
:
: [_] "{ecx}" (@intFromPtr(obj)),
[func] "r" (o.FN_CLEANUP_WORLD_OBJECT),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
);
}
/// Update object position directly
pub fn setObjectPosition(obj: *anyopaque, pos: Vec3) void {
const ptr: u32 = @intCast(@intFromPtr(obj));
@as(*f32, @ptrFromInt(ptr + o.OBJ_POS_X)).* = pos.x;
@as(*f32, @ptrFromInt(ptr + o.OBJ_POS_Y)).* = pos.y;
@as(*f32, @ptrFromInt(ptr + o.OBJ_POS_Z)).* = pos.z;
}
/// Set object alpha (0-255)
pub fn setObjectAlpha(obj: *anyopaque, alpha: u8) void {
const ptr: u32 = @intCast(@intFromPtr(obj));
// ARGB format - alpha in high byte
const color: u32 = (@as(u32, alpha) << 24) | 0x00189680;
@as(*u32, @ptrFromInt(ptr + o.OBJ_COLOR)).* = color;
}
// =============================================================================
// Animation
// =============================================================================
/// PlayAnimation(obj, animId) - __thiscall: ECX = obj, 1 stack param (callee-clean, RET 4)
pub fn playAnimation(obj: *anyopaque, anim_id: u32) void {
asm volatile (
\\push %[anim]
\\call *%[func]
:
: [_] "{ecx}" (@intFromPtr(obj)),
[anim] "r" (anim_id),
[func] "r" (o.FN_PLAY_ANIMATION),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
);
}
// =============================================================================
// Marker management
// =============================================================================
var test_marker: ?*anyopaque = null;
const MODEL_PATH: [*:0]const u8 = "World\\ArtTest\\Boxtest\\xyz.m2";
/// loadModelByName — __fastcall(ECX=path), returns model cache entry or null.
fn loadModel(path: [*:0]const u8) ?*anyopaque {
con.fmt("[markers] loadModelByName(\"{s}\")\n", .{@as([*:0]const u8, path)});
const result: u32 = asm volatile ("call *%[func]"
: [ret] "={eax}" (-> u32),
: [_] "{ecx}" (@intFromPtr(path)),
[func] "r" (o.FN_LOAD_MODEL_BY_NAME),
: .{ .edx = true, .memory = true, .cc = true }
);
if (result != 0) {
con.fmt("[markers] -> model at 0x{X:0>8}\n", .{result});
} else {
con.print("[markers] -> FAILED (returned null)\n");
}
return if (result != 0) @ptrFromInt(result) else null;
}
/// Create a test marker at player position
pub fn createTestMarker() ?*anyopaque {
const player = wow.getLocalPlayer();
if (player == 0) {
con.print("[markers] createTestMarker: no local player\n");
return null;
}
const pos = getUnitPosition(player);
con.fmt("[markers] player pos = {d:.1}, {d:.1}, {d:.1}\n", .{ pos.x, pos.y, pos.z });
if (pos.x == 0 and pos.y == 0 and pos.z == 0) return null;
const model = loadModel(MODEL_PATH);
con.fmt("[markers] createGameObject(model={?}, pos=({d:.1},{d:.1},{d:.1}))\n", .{
@as(?usize, if (model) |m| @intFromPtr(m) else null),
pos.x, pos.y, pos.z + 1.0,
});
const marker = createGameObject(model, null, null, pos.x, pos.y, pos.z + 1.0, 0);
if (marker) |m| {
con.fmt("[markers] -> object at 0x{X:0>8}\n", .{@intFromPtr(m)});
test_marker = m;
} else {
con.print("[markers] -> createGameObject FAILED\n");
}
return marker;
}
/// Destroy test marker
pub fn destroyTestMarker() void {
if (test_marker) |m| {
cleanupWorldObject(m);
test_marker = null;
}
}
/// Update test marker position to follow player
pub fn updateTestMarker() void {
if (test_marker == null) return;
const player = wow.getLocalPlayer();
if (player == 0) return;
const pos = getUnitPosition(player);
if (pos.x == 0 and pos.y == 0 and pos.z == 0) return;
setObjectPosition(test_marker.?, .{ .x = pos.x, .y = pos.y, .z = pos.z + 1.0 });
}
// =============================================================================
// Lua helpers
// =============================================================================
/// lua_pushnumber at 0x6F3810: __fastcall(L_ECX, double_on_stack). Callee cleans (RET 8).
fn luaPushNumber(L: u32, n: f64) void {
const raw: [2]u32 = @bitCast(n);
asm volatile (
\\push %[hi]
\\push %[lo]
\\call *%[func]
:
: [_] "{ecx}" (L),
[lo] "r" (raw[0]),
[hi] "r" (raw[1]),
[func] "r" (@as(u32, 0x6F3810)),
: .{ .eax = true, .edx = true, .memory = true, .cc = true }
);
}
// =============================================================================
// Lua API
// =============================================================================
/// Lua: TestMarkerCreate() - create marker at player position
pub fn luaTestMarkerCreate(L: u32) callconv(.c) u32 {
if (createTestMarker()) |_| {
hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker created")));
} else {
hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Failed to create marker")));
}
return 1;
}
/// Lua: TestMarkerDestroy() - destroy test marker
pub fn luaTestMarkerDestroy(L: u32) callconv(.c) u32 {
destroyTestMarker();
hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker destroyed")));
return 1;
}
/// Lua: TestMarkerToggle() - toggle marker on/off
pub fn luaTestMarkerToggle(L: u32) callconv(.c) u32 {
if (test_marker != null) {
destroyTestMarker();
hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker off")));
} else {
_ = createTestMarker();
hook.fastcall(void, 0x6F3890, L, @intFromPtr(@as([*:0]const u8, "Marker on")));
}
return 1;
}
/// Lua: local x, y, z = GetPlayerPosition()
pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 {
const player = wow.getLocalPlayer();
if (player == 0) return 0;
const pos = getUnitPosition(player);
luaPushNumber(L, @floatCast(pos.x));
luaPushNumber(L, @floatCast(pos.y));
luaPushNumber(L, @floatCast(pos.z));
return 3;
}
// =============================================================================
// Install hooks
// =============================================================================
pub fn installHooks() void {
// Nothing to hook yet - markers are created via Lua commands
}
pub fn removeHooks() void {
destroyTestMarker();
}
+96
View File
@@ -0,0 +1,96 @@
//! Offsets for marker/game object system
// =============================================================================
// Unit position
// =============================================================================
/// Unit + this → movement struct pointer
pub const UNIT_MOVEMENT_OFFSET: usize = 0x118;
/// Movement struct + this → X coordinate (float)
pub const MOVEMENT_POS_X: usize = 0x10;
/// Movement struct + this → Y coordinate (float)
pub const MOVEMENT_POS_Y: usize = 0x14;
/// Movement struct + this → Z coordinate (float)
pub const MOVEMENT_POS_Z: usize = 0x18;
// =============================================================================
// GameObject creation functions
// =============================================================================
/// CreateGameObject_WithProperties(model_ECX, callback1_EDX, callback2, x, y, z, flags)
/// __fastcall — ECX=model, EDX=callback1, 5 stack params. Callee cleans (RET 0x14).
/// Ghidra: 55 8B EC 83 EC 14 ... 89 4D FC (saves ECX) ... 89 55 F8 (saves EDX) ... C2 14 00.
pub const FN_CREATE_GAMEOBJECT: usize = 0x00670db0;
/// AllocateAndInitializeWorldObject(initializeFlag)
/// __fastcall returns void**
pub const FN_ALLOCATE_WORLD_OBJECT: usize = 0x006a0930;
/// CleanupWorldObject(object)
/// __thiscall
pub const FN_CLEANUP_WORLD_OBJECT: usize = 0x0069d730;
/// DestroyWorldObjectAndRelease(object)
/// __fastcall
pub const FN_DESTROY_WORLD_OBJECT: usize = 0x006a0a70;
// =============================================================================
// Model loading
// =============================================================================
/// loadModelByName(path)
/// __fastcall returns model cache entry
pub const FN_LOAD_MODEL_BY_NAME: usize = 0x006d4640;
/// createModelAttachment(resourceManager, path, flags)
/// __thiscall returns render context
pub const FN_CREATE_MODEL_ATTACHMENT: usize = 0x00707350;
// =============================================================================
// Animation control
// =============================================================================
/// PlayAnimation(object, animId)
/// __thiscall
pub const FN_PLAY_ANIMATION: usize = 0x0076cf50;
/// CM2Model__PlayBoneAnimation(model, boneIndex, animId, seqIndex, animData, speed, blend, queue)
/// __thiscall
pub const FN_PLAY_BONE_ANIMATION: usize = 0x007121a0;
/// HasAnimation(model, animId)
/// __thiscall returns bool
pub const FN_HAS_ANIMATION: usize = 0x00711960;
// =============================================================================
// Animation IDs (from string table)
// =============================================================================
pub const ANIM_SPAWN: u32 = 0; // TODO: find actual ID
pub const ANIM_DESPAWN: u32 = 0; // TODO: find actual ID
pub const ANIM_BIRTH: u32 = 0; // TODO: find actual ID
// =============================================================================
// Object structure offsets (from CreateGameObject_WithProperties analysis)
// =============================================================================
/// WorldObject + this → model pointer
pub const OBJ_MODEL: usize = 0x88;
/// WorldObject + this → position X (float*)
pub const OBJ_POS_X: usize = 0xC0;
/// WorldObject + this → position Y (float*)
pub const OBJ_POS_Y: usize = 0xC4;
/// WorldObject + this → position Z (float*)
pub const OBJ_POS_Z: usize = 0xC8;
/// WorldObject + this → color ARGB (alpha in high byte)
pub const OBJ_COLOR: usize = 0x24;
/// WorldObject + this → flags
pub const OBJ_FLAGS: usize = 0x90;