Add research notes for SuperWoW events and transformMatrix4x4 analysis

This commit is contained in:
MarcelineVQ
2026-03-12 10:38:11 -07:00
parent 48ebbdfe52
commit c7a293bec4
2 changed files with 209 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# SuperWoW Event Registration — Ghidra Analysis
Analysis of SuperWoWhook.dll
## CreateEvents Hook (0x100056c0)
1. Calls the original `FrameScript_CreateEvents` via its trampoline — this lets the
engine (and any earlier hooks like nampower) build the internal event table normally.
2. After the original returns, checks if the count that was passed in was > 200 (0xC8).
This distinguishes the main event table call (549 events) from the GlueXML call
(~26 events at `0xB41E70`). If ≤ 200, it does nothing.
3. Reads the internal array pointer from `PTR_00ceef68` (the struct at `0xceef60` has
`{count, capacity, array_ptr}` at offsets +0, +4, +8).
4. Calls `DuplicateStringWithAllocation` (0x64a620, stdcall) twice — once for
`"UNIT_CASTEVENT"`, once for `"RAW_COMBATLOG"`. This allocates via `SMemAlloc` and
copies the string, giving engine-owned memory that won't be freed unexpectedly.
5. Writes each allocated name pointer into the internal table at hardcoded offsets:
`array + 0x2580` (slot 600 × 16 bytes) and `array + 0x2590` (slot 601 × 16 bytes).
Each internal table entry is 16 bytes: `{name_ptr, 0, self_ptr, self_ptr|1}` — it
only writes the name at +0.
## resize_lua_event_array Hook (0x10005710)
1. This is the function that reallocates the internal event table array. It's
`__thiscall(ECX=0xceef60, stack=new_capacity)`.
2. SuperWoW intercepts every call. If the requested count is > 200 (again, skipping
GlueXML), it overwrites the count argument with 700 (0x2BC) before calling the
original.
3. This ensures the internal array is always large enough for slots 600-601, regardless
of how many base events the engine requests.
4. `RET 0x4` — thiscall cleans the one stack parameter.
## Ordering
`resize_lua_event_array` is called internally by `FrameScript_CreateEvents` as it
processes entries and needs to grow the array. So the resize hook fires during the
original CreateEvents call, expanding capacity to 700 before the CreateEvents hook's
post-processing writes to slots 600/601. By the time SuperWoW writes its events, the
array is already large enough.
## What It Doesn't Do
It doesn't touch the input name array at all, doesn't modify `maxEventId`, and doesn't
fill unused entry fields beyond the name pointer. The engine only needs the name at +0
for `RegisterEvent`/`SignalEvent` lookups.
## Contrast with Nampower
Nampower overwrites entries in the **input name array** (the `ECX` parameter to
`FrameScript_CreateEvents`). The input array at `0xBE1198` has 549 string pointers.
Nampower hooks CreateEvents, bumps `maxEventId` from 549 to 551, and writes its event
names (`SPELL_DAMAGE_EVENT_SELF`, `SPELL_DAMAGE_EVENT_OTHER`) directly into the input
array at slots 549 and 550 (addresses `0xBE1A2C` and `0xBE1A30`). The function then
processes these as normal entries when building the internal table.
Problem: slot 550+ overlap with float globals in the `.data` section — `0xBE1A30` is
actually `float 0.25` (0x3E800000).
## Our Approach
Follows SuperWoW's pattern: post-CreateEvents write into the internal table at slot 650,
with a resize hook to ensure capacity ≥ 700. Compatible with both SuperWoW and nampower
in the hook chain.
+146
View File
@@ -0,0 +1,146 @@
# transformMatrix4x4 (0x714260) -- Research Notes
## Function Overview
- **Address**: 0x714260
- **Size**: 17703 bytes (0x4527)
- **Convention**: `__thiscall(ECX=SceneObject*, stack: Matrix4x4*, Matrix4x4*, Matrix4x4*, Matrix4x4*)`
- **Returns**: void
- **Epilogue**: `RET 0x10` (4 stack params, normal exit) and `RET 0x4` (early exit path)
- **Recursive**: calls itself at 0x0071875c for child scene objects
## Calling Convention Evidence
```
PROLOGUE:
0x00714260 PUSH EBP
0x00714261 MOV EBP,ESP
0x00714263 SUB ESP,0x19c ; 412 bytes of locals
0x00714269 PUSH EBX
0x0071426a MOV EBX,ECX ; this = ECX (thiscall)
```
Stack frame: 0x19c (412) bytes of locals. Massive function.
## Callers
| Address | Function | Notes |
|---------|----------|-------|
| 0x707662 | processLinkedObjectList (0x707600) | |
| 0x7077b6 | renderFrame (0x707680) | |
| 0x707824 | renderFrame (0x707680) | Second call in same function |
| 0x714069 | updateAnimationTransform (0x714000) | |
| 0x714158 | updateAnimationTransform (0x714000) | |
| 0x71417e | updateAnimationTransform (0x714000) | |
| 0x7191b2 | renderSceneNode (0x718960) | |
| 0x71875c | transformMatrix4x4 (0x714260) | Recursive self-call |
## Internal Calls
| Address | Function | Count | Purpose |
|---------|----------|-------|---------|
| 0x713d50 | findInterpolationIndices | 58 | Core animation interpolation index lookup |
| 0x713ea0 | interpolateAnimationKeyframes | 2 | Full keyframe interpolation |
| 0x71af20 | getInterpolatedFloat | 4 | Single float interpolation |
| 0x71aff0 | getIndexOffset | 12 | Animation index calculation |
| 0x71b010 | setShortValue | 12 | Write short values |
| 0x74a7c0 | initParticlePixelShaderGeneration | 3 | Particle system setup |
| 0x74b6b5 | initPixelShaderDispatcher5 | 1 | Pixel shader setup |
| 0x7b5e60 | TransformParticleVelocities | 1 | Particle velocity transforms |
| 0x7b5f60 | IsParticleBufferEmpty | 1 | Check particle buffer state |
| 0x7b7bc0 | TransformParticleVectors | 1 | Particle vector transforms |
| 0x7bd820 | calculateScaledInverseMatrix | 1 | Inverse matrix for billboarding? |
| 0x7bdca0 | scaleMatrix3x3ByVector | 2 | Scale 3x3 portion of matrix |
| 0x7bdc40 | ApplyTranslationMatrix | 5 | Apply translation to matrix |
| 0x7bddb0 | rotateMatrixByQuaternion | 1 | Quaternion rotation |
| 0x4549f0 | emptyFunction | 10 | No-op (likely stripped debug/assert) |
| 0x409aef | validateMemoryOperation | 1 | Memory validation |
| 0x40a2b0 | __ftol | 4 | Float-to-long conversion |
## High-Level Structure
### Entry Checks (lines 110-111)
```c
if (this->model_data_ptr != NULL &&
this->transform_sync_value != *(this->animation_context_ptr + 0x10))
```
Bails immediately if no model data or transform is already up to date (sync value matches).
### Global Sequence Processing (lines 137-149)
Iterates global sequence array at `model+0x130`, computes per-sequence time offsets using
`animation_context_ptr+0xC` (current timestamp) modulo sequence duration.
### Identity Matrix Init (lines 163-194)
Sets up two identity matrices: `local_74` (4x4) and a second 3x4 matrix in `local_e8..local_ac`.
### Main Bone Loop (lines 203-2204)
```c
do {
pMVar23 = param_3 * 0x6c + *(local_18 + 0x38); // bone def from model
puVar20 = param_3 * 0x118 + this->unknown_0x80; // bone runtime state
...
param_3++;
} while (param_3 < *(local_18 + 0x34)); // bone count
```
Each bone is 0x6c (108) bytes in the model definition and 0x118 (280) bytes in runtime state.
Per-bone processing:
1. **Parent bone inheritance** (lines 210-268): Copy transform from parent bone if parent index != -1
2. **Animation time computation** (lines 230-267): Handle looping vs clamped animations, compute current keyframe position
3. **Blend weight (crossfade)** (lines 334-367): Hermite interpolation for animation blending
4. **Bone flags processing** (lines 368-478): Billboard types (flags & 7):
- 0x2: Cylindrical billboard (normalize rotation columns)
- 0x4: Spherical billboard (inherit parent rotation)
- 0x6: Full billboard (copy parent rotation directly)
- Flag 0x1: Fixed translation vs pivot-relative
5. **Scale interpolation** (lines 522-572): `scaleMatrix3x3ByVector` with interpolated scale
6. **Translation interpolation** (lines 583-628): Add interpolated translation to pivot
7. **Rotation interpolation** (quaternion, lines 630+): `rotateMatrixByQuaternion`
8. **Matrix composition** (lines 1050+): `ApplyTranslationMatrix` to build final bone matrix
9. **Write to output** (lines 480-492): Copy final matrix to bone transform array at `this->transform_vec2_x`
### Attachment Processing (lines 2206-2257)
After all bones, iterates attached child objects:
- Extracts parent bone matrix
- Applies attachment offset translation
- **Recursive call** to transformMatrix4x4 for each child SceneObject
### Sync Value Update (line 2259)
```c
this->transform_sync_value = *(this->animation_context_ptr + 0x10);
```
Marks transform as up to date.
## Key Data Structures
### SceneObject (this pointer)
| Offset | Field | Type | Notes |
|--------|-------|------|-------|
| +0x10 | model_data_ptr | void* | NULL check for early bail |
| +0x2C | ptr_at_2c | void* | -> model header? |
| +0x30 | animation_context_ptr | void* | +0x0C=timestamp, +0x10=sync_value |
| +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 data? |
### Bone Definition (0x6c = 108 bytes per bone in model)
From `model+0x38` array. Contains:
- Flags, parent bone index, billboard type
- Keyframe data pointers for translation, rotation, scale
- Pivot point (Vec3)
### Bone Runtime State (0x118 = 280 bytes per bone)
From `this->unknown_0x80` array. Contains:
- Current interpolation indices and weights
- Interpolated translation, rotation, scale values
- Blend state for animation crossfading
- Final composed 4x4 transform matrix
## Key Observations
1. **Performance critical**: Called per-frame for every visible M2 model with animated bones
2. **58 calls to findInterpolationIndices**: This is the hot inner function
3. **Recursive for attachments**: Child objects (weapons, shoulders, etc.) recurse through this same function
4. **Two animation blend sources**: Primary animation + blend target with crossfade weight at puVar20[0x43]
5. **Billboard support**: Flags-based billboard types for UI/particle-facing bones