From ab6f5ea4b9d3b887a372f4d1fbfbc914ca5aa241 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Fri, 27 Feb 2026 11:18:48 -0800 Subject: [PATCH] Replace manual world object creation with native CreateEntityInstance_WithAttachment API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markers are now created via the game's high-level entity factory (0x6707c0) which handles spatial registration, render setup, and lifecycle internally. Removed ~300 lines of dead code: manual game object list insertion, A/B mode switching, parity scanning, and model attachment wrappers. Creation works (markers visible). Destruction still crashes — needs correct destructor for entities created via CreateWorldUnit path (not DestroyWorldObjectAndRelease). --- CLAUDE_PURE_ZIG_OUTLINE_PLAN.md | 91 ----------------- src/main.zig | 10 ++ src/markers/addon/Markers.lua | 15 ++- src/markers/markers.zig | 170 +++++++++++--------------------- src/markers/offsets.zig | 95 +++++++----------- 5 files changed, 112 insertions(+), 269 deletions(-) delete mode 100644 CLAUDE_PURE_ZIG_OUTLINE_PLAN.md diff --git a/CLAUDE_PURE_ZIG_OUTLINE_PLAN.md b/CLAUDE_PURE_ZIG_OUTLINE_PLAN.md deleted file mode 100644 index 347d7e5..0000000 --- a/CLAUDE_PURE_ZIG_OUTLINE_PLAN.md +++ /dev/null @@ -1,91 +0,0 @@ -# Task: Pure Zig Unit Outline Reimplementation for weirdutils - -## Mission -Reimplement the WoW unit outlining system in **pure Zig** inside `zig/weirdutils`, with cleanly separated source files and no runtime dependency on the Idris C/C++ outline implementation. - -## Explicit Tooling Permission -You are explicitly authorized to use **Code Executor MCP freely**, including controlling **Ghidra** for reverse engineering support (function signatures, prologues, calling conventions, offsets, call sites). - -Use this freedom proactively to verify: -- Function prologues and patch-safe lengths -- rel32 fixup needs for call/jmp in overwritten prologues -- Calling convention correctness (__thiscall/__fastcall/__stdcall) -- Data structure offsets relevant to model tracking/render hooks - -## Repos / Paths -- Target implementation: `/media/storage/projects/zig/weirdutils` -- Reference implementation: `/media/storage/projects/idris/dlls` -- Existing Zig hook library reference: `/media/storage/projects/zig/hook` - -## Hard Constraints -1. **Pure Zig runtime path for outlines** (C/C++ outline files from Idris are reference-only). -2. Keep existing weirdutils features working (screenshot/interact/addon load). -3. Split code into focused modules; avoid giant monolith file growth. -4. Maintain x86 WoW 1.12.1 calling-convention correctness. -5. Preserve hook-chain safety (do not break existing detours). - -## Desired Source Layout (implement this or a clearly better equivalent) -Create these Zig modules under `src/outline/`: -- `offsets.zig` – constants for addresses/offsets -- `types.zig` – enums/structs (object types, vectors, draw categories) -- `wow.zig` – game memory access wrappers -- `tracker.zig` – per-frame model/category tracking state -- `model_hook.zig` – WoW render pipeline hooks for model classification -- `d3d9_hook.zig` – D3D9 DIP/EndScene hook logic for outline rendering -- `shader.zig` – optional shader helpers/constants (if used) -- `api.zig` – public init/reset/config functions exposed to main - -Also update: -- `src/main.zig` (initialize/cleanup outline subsystem) -- `build.zig` (if needed for new modules) -- `src/addon/WeirdUtils.lua` (outline command UX/status) - -## Behavior Requirements -Implement category-based outlines: -- Dead friendly players/corpses: through-wall style visibility -- Raid-marked units: clear colored outlines -- Current target (enemy NPC): emphasized outline - -Implement robust per-frame reset + repopulation logic to avoid stale tracking. - -## Implementation Phases -### Phase 1 – Mapping + verification -- Use Ghidra + references to verify prologues and hook patch sizes. -- Produce a concise mapping note in `docs/outline-port-notes.md`. - -### Phase 2 – Core Zig architecture -- Create modular outline subsystem under `src/outline/`. -- Port tracking/state logic and category assignment. - -### Phase 3 – Hook integration -- Implement render hooks with safe trampoline usage using existing Zig hook patterns. -- Integrate into weirdutils startup/shutdown flow. - -### Phase 4 – User controls + defaults -- Add `/wu outline` command family: - - `/wu outline on` - - `/wu outline off` - - `/wu outline status` - -### Phase 5 – Validation -- Ensure weirdutils still builds for x86 windows msvc. -- Smoke-check no regressions in interact/screenshot command registration paths. -- Add a brief test checklist in `docs/outline-validation.md`. - -## Deliverables -1. Pure Zig outline subsystem in separated files. -2. Updated integration in main/addon. -3. `docs/outline-port-notes.md` with verified hook/prologue decisions. -4. `docs/outline-validation.md` with run/test checklist. -5. Final summary with changed files and known limitations. - -## Guardrails -- Do not run destructive git operations. -- Do not remove existing features. -- If a prologue/callconv is uncertain, verify in Ghidra before patching. - -## Completion Signal -When fully done, run: -`openclaw system event --text "Done: pure Zig outline port implemented in weirdutils" --mode now` - -Then print a short completion summary. \ No newline at end of file diff --git a/src/main.zig b/src/main.zig index 840443c..4c0d898 100644 --- a/src/main.zig +++ b/src/main.zig @@ -537,6 +537,12 @@ fn engineInitDetour() callconv(sc) void { var shutdown_hook: hook.Hook = .{}; fn shutdownDetour() callconv(sc) void { + // Clean up world objects BEFORE game shutdown — atexit handlers run before DllMain + // so we must destroy markers here, not in uninstall(). + if (build_opts.markers) { + markers.removeHooks(); + } + const orig = shutdown_hook.getTrampoline(*const fn () callconv(sc) void); orig(); } @@ -570,6 +576,10 @@ fn uninstall() void { shutdown_hook.remove(); engine_init_hook.remove(); + // Markers must be cleaned up first — destroys world objects while game systems are still alive + if (build_opts.markers) { + markers.removeHooks(); + } if (build_opts.outline) { outline.cleanup(); } diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua index c3ba0e7..77edb09 100644 --- a/src/markers/addon/Markers.lua +++ b/src/markers/addon/Markers.lua @@ -6,7 +6,6 @@ MARKERS_VERSION = 1 BINDING_HEADER_MARKERS = "Markers" local frame = CreateFrame("Frame") -frame:RegisterEvent("ADDON_LOADED") frame:RegisterEvent("PLAYER_LOGIN") frame:SetScript("OnEvent", function() @@ -19,27 +18,27 @@ 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") - + DEFAULT_CHAT_FRAME:AddMessage(" /mark pos - Show player position") + 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 diff --git a/src/markers/markers.zig b/src/markers/markers.zig index 83b2b8f..3c9f691 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -1,7 +1,12 @@ -//! Client-side marker system using CreateGameObject_WithProperties +//! Client-side marker system //! //! Creates world objects at arbitrary positions for raid markers, //! player indicators, etc. +//! +//! Uses CreateEntityInstance_WithAttachment (0x6707c0) — the game's +//! native high-level entity creation API. For M2 models, this routes +//! through CreateWorldUnit which handles all spatial registration, +//! render setup, and lifecycle management. const hook = @import("hook"); const o = @import("offsets.zig"); @@ -33,28 +38,21 @@ pub fn getUnitPosition(unit: u32) Vec3 { } // ============================================================================= -// GameObject creation - function pointer wrappers +// Game function 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) +/// CreateEntityInstance_WithAttachment — __fastcall, RET 0x14. +/// ECX=modelPath, EDX=positionVec3ptr, stack: facing, flags, updateNow, param6, param7. +/// Routes M2 models through CreateWorldUnit, WMO models through CreateGameObject. +/// Returns a fully-registered game entity pointer. +fn createEntityInstance(path: [*:0]const u8, pos: *[3]f32, facing: f32, flags: u32, update_now: u32) ?*anyopaque { + const facing_bits: u32 = @bitCast(facing); 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, + facing_bits, // param_3: facing angle + flags, // param_4: flags/type + update_now, // param_5: update position immediately (1=yes) + 0, // param_6 + 0, // param_7 }; const result: u32 = asm volatile ( @@ -65,55 +63,22 @@ pub fn createGameObject( \\ 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)), + : [_] "{ecx}" (@intFromPtr(path)), + [_] "{edx}" (@intFromPtr(pos)), [a] "r" (&stack_args), - [func] "r" (o.FN_CREATE_GAMEOBJECT), + [func] "r" (o.FN_CREATE_ENTITY_INSTANCE), : .{ .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 { +/// DestroyWorldObjectAndRelease — __fastcall(ECX=obj), tail JMP. +fn destroyWorldObject(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), + [func] "r" (o.FN_DESTROY_WORLD_OBJECT), : .{ .eax = true, .edx = true, .memory = true, .cc = true } ); } @@ -124,30 +89,18 @@ pub fn playAnimation(obj: *anyopaque, anim_id: u32) void { var test_marker: ?*anyopaque = null; -const MODEL_PATH: [*:0]const u8 = "World\\ArtTest\\Boxtest\\xyz.m2"; +const MODEL_PATH: [*:0]const u8 = "Spells\\ErrorCube.mdx"; -/// 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 +/// Create a test marker at player position using the native entity creation API. pub fn createTestMarker() ?*anyopaque { + if (test_marker != null) { + con.print("[markers] marker already exists, destroy first\n"); + return test_marker; + } + const player = wow.getLocalPlayer(); if (player == 0) { - con.print("[markers] createTestMarker: no local player\n"); + con.print("[markers] no local player\n"); return null; } @@ -155,42 +108,39 @@ pub fn createTestMarker() ?*anyopaque { 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); + var position = [3]f32{ pos.x, pos.y, pos.z + 2.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"); - } + con.print("[markers] calling CreateEntityInstance_WithAttachment...\n"); + const obj = createEntityInstance(MODEL_PATH, &position, 0.0, 0, 1) orelse { + con.print("[markers] CreateEntityInstance_WithAttachment FAILED\n"); + return null; + }; - return marker; + const obj_ptr: u32 = @intCast(@intFromPtr(obj)); + con.fmt("[markers] entity created at 0x{X:0>8}\n", .{obj_ptr}); + + // Debug dump key fields + const refcount = hook.readMem(u16, obj_ptr + 0xE); + const flags_90 = hook.readMem(u32, obj_ptr + 0x90); + const model_88 = hook.readMem(u32, obj_ptr + 0x88); + const pos_x = hook.readMem(f32, obj_ptr + 0x5C); + const pos_y = hook.readMem(f32, obj_ptr + 0x60); + const pos_z = hook.readMem(f32, obj_ptr + 0x64); + con.fmt("[markers] refcount={d} flags90=0x{X:0>8} model88=0x{X:0>8}\n", .{ refcount, flags_90, model_88 }); + con.fmt("[markers] bsph(+5C)={d:.1},{d:.1},{d:.1}\n", .{ pos_x, pos_y, pos_z }); + + test_marker = obj; + return obj; } -/// Destroy test marker +/// Destroy the test marker. Safe to call multiple times. pub fn destroyTestMarker() void { - if (test_marker) |m| { - cleanupWorldObject(m); - test_marker = null; - } -} + const marker = test_marker orelse return; + 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 }); + con.print("[markers] destroying marker...\n"); + destroyWorldObject(marker); + con.print("[markers] marker destroyed\n"); } // ============================================================================= @@ -263,7 +213,7 @@ pub fn luaGetPlayerPosition(L: u32) callconv(.c) u32 { // ============================================================================= pub fn installHooks() void { - // Nothing to hook yet - markers are created via Lua commands + // Nothing to hook - markers are created via Lua commands } pub fn removeHooks() void { diff --git a/src/markers/offsets.zig b/src/markers/offsets.zig index 604f2e2..fcf0abc 100644 --- a/src/markers/offsets.zig +++ b/src/markers/offsets.zig @@ -17,80 +17,55 @@ pub const MOVEMENT_POS_Y: usize = 0x14; pub const MOVEMENT_POS_Z: usize = 0x18; // ============================================================================= -// GameObject creation functions +// Entity creation (high-level API) // ============================================================================= -/// 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; +/// CreateEntityInstance_WithAttachment — __fastcall, RET 0x14 (5 stack params). +/// ECX = modelPath (char*), EDX = position (float[3]*) +/// Stack: facing (float), flags (int), updateNow (int), param6 (int), param7 (int) +/// Returns: entity pointer (int*). +/// +/// Routes M2 models (no ".wmo" in path) through CreateWorldUnit. +/// Routes WMO models (".wmo" in path) through CreateGameObject + ModelAttachment_CreateNode +/// + global list insertion + SetObjectTransformation. +/// +/// Both paths call UpdateWorldPosition when updateNow != 0. +/// Increments refcount at entity+0x0E. +pub const FN_CREATE_ENTITY_INSTANCE: usize = 0x006707c0; + +// ============================================================================= +// World object lifecycle +// ============================================================================= /// 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 +/// DestroyWorldObjectAndRelease(object) — __fastcall, ECX=obj, no stack params. +/// Unlinks from world object list (+0x10/+0x14), calls virtual destructor, frees heap. +/// Ends with tail JMP to ReleaseToHeap — from caller's perspective, a normal return. 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; +/// DecrementReferenceCount(obj) — __fastcall, ECX=obj, no stack params. +/// Decrements ref count; when it reaches 0, calls virtual destructor to free. +pub const FN_DECREMENT_REFCOUNT: usize = 0x007103a0; // ============================================================================= -// Animation control +// Model creation // ============================================================================= -/// 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; +/// CM2Model_CreateForModelObject(modelPath_ECX, worldObj_EDX, forceInit) +/// __fastcall, RET 0x04. ECX=modelPath(char*), EDX=worldObject, 1 stack param. +/// Complete model creation pipeline: createModelAttachment, SetModelScale, +/// SetCallbackFunctions, SetRenderCallbacks, PlayBoneAnimation, CM2Model_Initialize. +/// Returns 1 on success, 0 on failure. Stores render context at worldObj+0x88. +pub const FN_CM2_CREATE_FOR_MODEL_OBJECT: usize = 0x00695100; // ============================================================================= -// Animation IDs (from string table) +// Transform and position // ============================================================================= -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; +/// UpdateObjectTransform_CalculateBounds — __fastcall, RET 0x0C +/// ECX = world object, EDX = 4x4 transform matrix (float[16]) +/// Stack: bounds (float[6] min/max), halfExtents (float[3]), forceUpdate (int) +pub const FN_UPDATE_OBJECT_TRANSFORM: usize = 0x006717d0;