From 53fb1003687ff144dd995e66d6006af20934e5b9 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 12 Mar 2026 11:45:35 -0700 Subject: [PATCH] Fix bone count: model container is at this+0x30 not this+0x2C Ghidra decompiler swapped the two fields. Assembly verification shows: +0x2C = animation_context_ptr (sync check at +0x10) +0x30 = model_container_ptr (+0x130 = M2 model header) Bone count chain: *(*(*(this+0x30) + 0x130) + 0x34) --- src/transform44/RESEARCH.md | 23 ++++++++++++++++++----- src/transform44/transform44.zig | 13 ++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/transform44/RESEARCH.md b/src/transform44/RESEARCH.md index 47f263f..47151e7 100644 --- a/src/transform44/RESEARCH.md +++ b/src/transform44/RESEARCH.md @@ -115,23 +115,36 @@ Marks transform as up to date. ## Key Data Structures ### SceneObject (this pointer) + +**WARNING**: Ghidra decompiler swaps +0x2C and +0x30 labels. Assembly is authoritative. + | Offset | Field | Type | Notes | |--------|-------|------|-------| | +0x10 | model_data_ptr | void* | NULL check for early bail | -| +0x2C | ptr_at_2c | void* | -> context struct (NOT M2 header directly!) | -| +0x30 | animation_context_ptr | void* | +0x0C=timestamp, +0x10=sync_value | -| +0x40 | transform_sync_value | int | Compared with anim_ctx+0x10 | +| +0x2C | animation_context_ptr | void* | +0x0C=timestamp, +0x10=sync_value | +| +0x30 | model_container_ptr | void* | +0x130 = M2 model header | +| +0x40 | transform_sync_value | int | Compared with *(anim_ctx+0x10) | | +0x80 | unknown_0x80 | uint | Bone runtime state array base | | +0x1CC | field_0x1cc | int* | Emitter/particle context | -### Context Struct (at *(this+0x2C)) +Assembly proof (0x714277-0x714293): +```asm +MOV EAX, [EBX + 0x2c] ; EAX = animation_context_ptr +MOV ECX, [EBX + 0x40] ; ECX = sync_value +CMP ECX, [EAX + 0x10] ; sync check: this+0x40 vs *(this+0x2C)+0x10 +... +MOV EDX, [EBX + 0x30] ; EDX = model_container_ptr +MOV EDI, [EDX + 0x130] ; EDI = M2 model header +``` + +### Model Container (at *(this+0x30)) | Offset | Field | Notes | |--------|-------|-------| | +0x14 | global sequence count | Loop bound for GS processing | | +0x18 | global sequence durations array | | | +0x130 | M2 model header pointer | **This is the actual model** | -**Pointer chain to bone count**: `*(*(*(this+0x2C) + 0x130) + 0x34)` +**Pointer chain to bone count**: `*(*(*(this+0x30) + 0x130) + 0x34)` ### Bone Definition (0x6c = 108 bytes per bone in model) From `model+0x38` array (where model = `*(*(this+0x2C) + 0x130)`). Contains: diff --git a/src/transform44/transform44.zig b/src/transform44/transform44.zig index 2232de2..69ffc63 100644 --- a/src/transform44/transform44.zig +++ b/src/transform44/transform44.zig @@ -93,22 +93,25 @@ fn transformDetour(this: u32, edx: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u const start = rdtsc(); // Check sync gate — predict early exit + // Assembly truth (NOT Ghidra decompiler labels): + // +0x2C = animation_context_ptr (sync check at +0x10, timestamp at +0x0C) + // +0x30 = model_container_ptr (+0x130 = M2 model header) const model_data = hook.readMem(u32, this + 0x10); var is_early = false; var bone_count: u32 = 0; if (model_data == 0) { is_early = true; } else { - const anim_ctx = hook.readMem(u32, this + 0x30); + const anim_ctx = hook.readMem(u32, this + 0x2C); if (anim_ctx != 0) { const sync_val = hook.readMem(u32, this + 0x40); const anim_sync = hook.readMem(u32, anim_ctx + 0x10); if (sync_val == anim_sync) is_early = true; } - // Model header is at *(*(this+0x2C) + 0x130), bone count at +0x34 - const ptr_2c = hook.readMem(u32, this + 0x2C); - if (ptr_2c != 0) { - const model_hdr = hook.readMem(u32, ptr_2c + 0x130); + // Model header: *(*(this+0x30) + 0x130), bone count at +0x34 + const model_ctr = hook.readMem(u32, this + 0x30); + if (model_ctr != 0) { + const model_hdr = hook.readMem(u32, model_ctr + 0x130); if (model_hdr != 0) { bone_count = hook.readMem(u32, model_hdr + 0x34); }