Replace manual world object creation with native CreateEntityInstance_WithAttachment API
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).
This commit is contained in:
@@ -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.
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+60
-110
@@ -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 {
|
||||
|
||||
+35
-60
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user