Add module control API, bigcursor with fractional scaling, shared mutex
Module control API: - Three cdecl exports: WeirdUtils_IsModuleActive, WeirdUtils_DisableModule, WeirdUtils_DisableAll - C header include/weirdutils_api.h for runtime DLL discovery - All modules gain pub module_name, isActive(), shared mutex via src/mutex.zig - build.zig refactored to module_list array of ModuleDesc Bigcursor: - D3D9 vtable hooks on SetCursorProperties/ShowCursor - Scale2x/Scale3x pixel-art upscalers in pure Zig (replaces 12K-line hqx C) - Bilinear resampler for fractional scales (1.0-4.0, default 1.2) - Win32 cursor via CreateIconIndirect bypasses D3D9 32x32 limit - FNV-1a hash cache for ~16 cursor bitmaps - Lua API: SetCursorScale(n) / GetCursorScale() - CVar cursorScale for Config.wtf persistence (tenths) Other: - Add dpslog module stub - Docs and README updates
This commit is contained in:
@@ -59,16 +59,16 @@ Archives are stored in a **dynamic array** managed as a struct:
|
||||
Global archive array struct at 0x8826b4:
|
||||
+0x00 [0x8826b4]: capacity (max slots)
|
||||
+0x04 [0x8826b8]: count (current number of archives)
|
||||
+0x08 [0x8826bc]: array_ptr (SArchive** — pointer to array of SArchive pointers)
|
||||
+0x08 [0x8826bc]: array_ptr (SArchive** - pointer to array of SArchive pointers)
|
||||
+0x0C [0x8826c0]: growth_incr (allocation growth increment)
|
||||
|
||||
RTTI tag: ".PAVSArchive@@" at 0x82e248
|
||||
```
|
||||
|
||||
Managed by:
|
||||
- `GrowArchiveArray` (0x4045a0) — __thiscall, resizes the array
|
||||
- `ResizeArchiveArray` (0x4046f0) — sets initial capacity
|
||||
- `MPQ_CleanupAllArchives` (0x403c70) — iterates count→0 calling Archive_Close
|
||||
- `GrowArchiveArray` (0x4045a0) - __thiscall, resizes the array
|
||||
- `ResizeArchiveArray` (0x4046f0) - sets initial capacity
|
||||
- `MPQ_CleanupAllArchives` (0x403c70) - iterates count→0 calling Archive_Close
|
||||
|
||||
### SArchive Object (Minimal Wrapper)
|
||||
|
||||
@@ -114,18 +114,18 @@ From `InitializeArchiveStructure` (0x655bf0), the full MPQ handle (pointed to by
|
||||
| +0x290 | `[0xa4]` | Attributes offset |
|
||||
| +0x294 | `[0xa5]` | Hash table allocated buffer |
|
||||
|
||||
The I/O vtable at `+0x140 [0x50]` is critical — it provides the read callbacks that Storm uses to access the archive data. Read calls go through `(*(code **)(*param_1[0x50] + 4))(...)`.
|
||||
The I/O vtable at `+0x140 [0x50]` is critical - it provides the read callbacks that Storm uses to access the archive data. Read calls go through `(*(code **)(*param_1[0x50] + 4))(...)`.
|
||||
|
||||
### Archive Registration (How MPQs Are Opened)
|
||||
|
||||
```
|
||||
MPQ_InitializeArchives (0x403740)
|
||||
│
|
||||
├─ ResizeArchiveArray(...) — allocate the global array
|
||||
├─ ResizeArchiveArray(...) - allocate the global array
|
||||
│
|
||||
├─ For each base MPQ (model, texture, terrain, wmo, sound, misc, interface, fonts, dbc):
|
||||
│ └─ OpenMPQArchiveWithPaths(name, param2, index) [0x403b00]
|
||||
│ ├─ FormatPath(buf, 0x104, pathIndex, name) — tries "Data\name" then "..\Data\name"
|
||||
│ ├─ FormatPath(buf, 0x104, pathIndex, name) - tries "Data\name" then "..\Data\name"
|
||||
│ └─ Archive_OpenUnified(path, ..., &PTR_008826bc[index]) [0x648dd0]
|
||||
│ └─ OpenFileWithValidation(path, ..., &archive) [0x655690]
|
||||
│ └─ InitializeArchiveStructure(archive, ...) [0x655bf0]
|
||||
@@ -189,7 +189,7 @@ struct IOObject { // 0x118 bytes total
|
||||
[4] +0x10: 0x66e1c0 = IOManagerDestructor
|
||||
[5] +0x14: 0x66e1e0 = IOManagerCleanup
|
||||
[6] +0x18: 0x66e200 = (unknown)
|
||||
[7] +0x1C: 0x7f800000 = (float NaN — padding/sentinel)
|
||||
[7] +0x1C: 0x7f800000 = (float NaN - padding/sentinel)
|
||||
[8] +0x20: 0x66e2d0 = ValidateIOOperation
|
||||
[9] +0x24: 0x66e310 = GetIOResult
|
||||
[10]+0x28: 0x66e340 = CancelIOOperation
|
||||
@@ -235,7 +235,7 @@ The `SArchive` wrapper at the top level (8 bytes: `{type, handle}`) bridges thes
|
||||
|
||||
Storm has no "register a provider" API. Every type code (0-4) is hardcoded to specific OS handle types. The I/O vtable exists but is deeply intertwined with async thread managers and buffer management (0x118 bytes of state). Creating a custom IO object is theoretically possible but requires replicating the async infrastructure.
|
||||
|
||||
The most practical "virtual filesystem" approaches use **real OS handles** that Storm can consume natively — either through temp files or through MPQ archives that Storm opens and manages itself.
|
||||
The most practical "virtual filesystem" approaches use **real OS handles** that Storm can consume natively - either through temp files or through MPQ archives that Storm opens and manages itself.
|
||||
|
||||
---
|
||||
|
||||
@@ -245,11 +245,11 @@ The most practical "virtual filesystem" approaches use **real OS handles** that
|
||||
|
||||
Use Windows `CreateFile` with `FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE` to create **real OS file handles** backed primarily by the filesystem cache (RAM). Then hook only `openFileWithOptions` to redirect Storm to our temp files.
|
||||
|
||||
Windows `FILE_ATTRIBUTE_TEMPORARY` tells the cache manager to avoid flushing to disk if possible — the data lives in RAM. `FILE_FLAG_DELETE_ON_CLOSE` auto-deletes the file when the last handle closes, even on crash.
|
||||
Windows `FILE_ATTRIBUTE_TEMPORARY` tells the cache manager to avoid flushing to disk if possible - the data lives in RAM. `FILE_FLAG_DELETE_ON_CLOSE` auto-deletes the file when the last handle closes, even on crash.
|
||||
|
||||
### Implementation
|
||||
|
||||
1. **DLL init** — for each embedded asset:
|
||||
1. **DLL init** - for each embedded asset:
|
||||
```c
|
||||
// Get temp directory
|
||||
GetTempPath(MAX_PATH, tempDir);
|
||||
@@ -271,7 +271,7 @@ Windows `FILE_ATTRIBUTE_TEMPORARY` tells the cache manager to avoid flushing to
|
||||
// Store mapping: "Spells\\WU_XYZ.m2" → tempPath
|
||||
```
|
||||
|
||||
2. **Hook `openFileWithOptions` (0x6477c0)** — single hook:
|
||||
2. **Hook `openFileWithOptions` (0x6477c0)** - single hook:
|
||||
```
|
||||
openFileDetour(archive, path, flags, handle_out):
|
||||
if path matches our asset map:
|
||||
@@ -280,18 +280,18 @@ Windows `FILE_ATTRIBUTE_TEMPORARY` tells the cache manager to avoid flushing to
|
||||
return original(archive, path, flags, handle_out)
|
||||
```
|
||||
|
||||
3. **No other hooks needed** — Storm opens the temp file with `CreateFileA`, gets a real handle. All native I/O works:
|
||||
3. **No other hooks needed** - Storm opens the temp file with `CreateFileA`, gets a real handle. All native I/O works:
|
||||
- `GetFileSizeFromHandle` → type-0 dispatch → `fstat` on real handle ✓
|
||||
- `ReadFileFromMultipleSources` → type-0 dispatch → `fileReadWithLock` on real handle ✓
|
||||
- `processAsyncFileOperation` → reads from real handle via worker thread ✓
|
||||
- `CleanupFileHandleResources` → closes real handle ✓
|
||||
- `loadModelFromFileAsync` → async task reads real handle ✓
|
||||
|
||||
4. **Cleanup** — on DLL unload, close our original handles. `FILE_FLAG_DELETE_ON_CLOSE` handles the rest. If the process crashes, Windows still cleans up temp files.
|
||||
4. **Cleanup** - on DLL unload, close our original handles. `FILE_FLAG_DELETE_ON_CLOSE` handles the rest. If the process crashes, Windows still cleans up temp files.
|
||||
|
||||
### Why This Works For Async
|
||||
|
||||
The entire reason our current approach needs 6 hooks is that fake type-0 contexts with NULL handles fail when the async executor calls `fileReadWithLock` directly. With real temp file handles, the async executor reads from a real OS handle — no hooks needed on the read path at all.
|
||||
The entire reason our current approach needs 6 hooks is that fake type-0 contexts with NULL handles fail when the async executor calls `fileReadWithLock` directly. With real temp file handles, the async executor reads from a real OS handle - no hooks needed on the read path at all.
|
||||
|
||||
### Path Redirection Strategy
|
||||
|
||||
@@ -325,7 +325,7 @@ Files should go in the system temp directory (`GetTempPath`), NOT the game direc
|
||||
`FILE_ATTRIBUTE_TEMPORARY` is a hint to the Windows cache manager:
|
||||
- Data is kept in the filesystem cache (RAM) and written to disk lazily
|
||||
- For small files (~20 assets, totaling a few MB), Windows will almost certainly keep everything in cache
|
||||
- This is NOT a guarantee — under memory pressure, Windows may flush to disk
|
||||
- This is NOT a guarantee - under memory pressure, Windows may flush to disk
|
||||
- But for our use case (~2-3 MB total), the data effectively stays in RAM
|
||||
|
||||
### Key Addresses
|
||||
@@ -343,10 +343,10 @@ OR:
|
||||
| `locateFileInDirectories` | 0x647e60 | __fastcall(6) | Return temp path as disk file |
|
||||
|
||||
### Pros
|
||||
- **Reduces from 6 hooks to 1** — the biggest win
|
||||
- **All async I/O works natively** — real OS handles, no fake contexts
|
||||
- **Reduces from 6 hooks to 1** - the biggest win
|
||||
- **All async I/O works natively** - real OS handles, no fake contexts
|
||||
- No fake file contexts, no critical section management, no refcount tracking
|
||||
- No MPQ building required — files are individual temp files
|
||||
- No MPQ building required - files are individual temp files
|
||||
- Auto-cleanup via `FILE_FLAG_DELETE_ON_CLOSE`
|
||||
- Temp files are invisible to the user (in system temp dir)
|
||||
- The `loadFileDetour` (0x648620) for addon files could remain unchanged
|
||||
@@ -356,7 +356,7 @@ OR:
|
||||
- Requires `CreateFile`/`WriteFile` Win32 calls from Zig (straightforward via `@import("std").os.windows`)
|
||||
- ~20 temp files created at init (small overhead, ~2-3 MB total)
|
||||
- Temp file creation adds a few ms to DLL init
|
||||
- If DLL loads after `scanDirectoriesForFiles` (likely), the disk hash table won't have our files — must use path redirection hook rather than relying on native discovery
|
||||
- If DLL loads after `scanDirectoriesForFiles` (likely), the disk hash table won't have our files - must use path redirection hook rather than relying on native discovery
|
||||
|
||||
---
|
||||
|
||||
@@ -386,9 +386,9 @@ Build a real MPQ archive at compile time containing all embedded assets. Write i
|
||||
| Global array struct | 0x8826b4 | `{capacity, count, array_ptr, growth}` |
|
||||
|
||||
### Pros
|
||||
- **Eliminates ALL 6 Storm I/O hooks** — game reads MPQ natively
|
||||
- **Eliminates ALL 6 Storm I/O hooks** - game reads MPQ natively
|
||||
- Zero fake file contexts, zero async handling
|
||||
- Proven approach — this is how patch.MPQ works
|
||||
- Proven approach - this is how patch.MPQ works
|
||||
- The `loadFileDetour` (0x648620) for addon files could remain unchanged
|
||||
|
||||
### Cons
|
||||
@@ -434,10 +434,10 @@ For disk-based MPQs, this vtable points to functions that call `ReadFile`/`SetFi
|
||||
#### B1: Fake the File Backing (Simpler)
|
||||
|
||||
1. Embed MPQ in DLL via `@embedFile`
|
||||
2. Hook `openFileWithOptions` to intercept when Storm opens `"WeirdUtils.mpq"` — create a fake file context pointing to the embedded MPQ blob
|
||||
2. Hook `openFileWithOptions` to intercept when Storm opens `"WeirdUtils.mpq"` - create a fake file context pointing to the embedded MPQ blob
|
||||
3. Add **seek support** to the fake context (current implementation only does flat memcpy from offset 0)
|
||||
4. Call `Archive_OpenUnified("WeirdUtils.mpq", ...)` — Storm reads the MPQ header, hash table, block table through our hooked I/O
|
||||
5. For subsequent reads FROM the archive (when loading files within the MPQ), Storm seeks within the same archive file handle — our hook serves the right bytes
|
||||
4. Call `Archive_OpenUnified("WeirdUtils.mpq", ...)` - Storm reads the MPQ header, hash table, block table through our hooked I/O
|
||||
5. For subsequent reads FROM the archive (when loading files within the MPQ), Storm seeks within the same archive file handle - our hook serves the right bytes
|
||||
|
||||
**Key difference from current approach**: Instead of matching ~20 individual paths and creating ~20 fake contexts, we serve **one** fake file (the MPQ itself). Storm handles all per-file hash lookup, decompression, and I/O natively.
|
||||
|
||||
@@ -448,8 +448,8 @@ For disk-based MPQs, this vtable points to functions that call `ReadFile`/`SetFi
|
||||
- File path matching reduces from ~20 paths to 1
|
||||
|
||||
**What we might drop**:
|
||||
- `loadModelAsyncDetour` (0x71d4e0) — this hook exists because M2 async loading bypasses our read hook for type-0 fake contexts. But archive files go through Storm's type-4 read path, which reads from the archive handle. If the archive handle is our fake context with seek support, Storm's own async code should work.
|
||||
- `processAsyncDetour` (0x647350) — same reasoning; the async executor reads from the archive file context, which goes through our hooked `readFileDetour`.
|
||||
- `loadModelAsyncDetour` (0x71d4e0) - this hook exists because M2 async loading bypasses our read hook for type-0 fake contexts. But archive files go through Storm's type-4 read path, which reads from the archive handle. If the archive handle is our fake context with seek support, Storm's own async code should work.
|
||||
- `processAsyncDetour` (0x647350) - same reasoning; the async executor reads from the archive file context, which goes through our hooked `readFileDetour`.
|
||||
|
||||
**Risk**: The async executor might call `fileReadWithLock` directly on the archive handle, bypassing `ReadFileFromMultipleSources`. This is the exact problem that forced hooks 4 and 6 in the current approach. Needs verification.
|
||||
|
||||
@@ -483,11 +483,11 @@ For disk-based MPQs, this vtable points to functions that call `ReadFile`/`SetFi
|
||||
|
||||
### Concept
|
||||
|
||||
Instead of hooking individual I/O functions, hook `locateFileInDirectories` (0x647e60) — the single function that decides WHERE a file comes from (disk, hash cache, or archive). Make it return type=0 (disk) with a path that resolves to our embedded data.
|
||||
Instead of hooking individual I/O functions, hook `locateFileInDirectories` (0x647e60) - the single function that decides WHERE a file comes from (disk, hash cache, or archive). Make it return type=0 (disk) with a path that resolves to our embedded data.
|
||||
|
||||
### Why This Doesn't Quite Work
|
||||
|
||||
`locateFileInDirectories` only decides the file *location* — it doesn't serve data. After it returns type=0, `openFileWithOptions` calls `openFileHandle` to actually open the disk file. If there's no real file on disk, this fails.
|
||||
`locateFileInDirectories` only decides the file *location* - it doesn't serve data. After it returns type=0, `openFileWithOptions` calls `openFileHandle` to actually open the disk file. If there's no real file on disk, this fails.
|
||||
|
||||
However, this could work in combination with disk writes (see Approach A) or with a minimal I/O hook.
|
||||
|
||||
@@ -548,7 +548,7 @@ This works for **disk-based** loose files but not for in-memory embedded data. H
|
||||
|
||||
| Target | Address | Patch |
|
||||
|--------|---------|-------|
|
||||
| `CheckFileExistence` | 0x654DD0 | Hook target — `__fastcall(ECX=filename, EDX=flags, [esp+4]=output)` |
|
||||
| `CheckFileExistence` | 0x654DD0 | Hook target - `__fastcall(ECX=filename, EDX=flags, [esp+4]=output)` |
|
||||
| Gate 1 (JZ) | 0x654b5c | `74 25 → 90 90` (NOP) |
|
||||
| Gate 2 (JNZ) | 0x654b6a | `75 17 → 90 90` (NOP) |
|
||||
| Glob pattern byte | 0x82edc2 | `3F → 2A` ('?' → '*') for multi-char patch names |
|
||||
@@ -576,20 +576,20 @@ This works for **disk-based** loose files but not for in-memory embedded data. H
|
||||
### Best Overall: Approach E (Windows Temp File Handles)
|
||||
|
||||
**Reasoning**:
|
||||
1. **Reduces from 6 hooks to 1** — the single biggest complexity reduction possible
|
||||
2. **All async I/O works natively** — real OS handles eliminate the entire class of async-bypass bugs that forced hooks 4, 5, and 6
|
||||
3. **No MPQ building required** — avoids the entire hash table encryption / block table / header format complexity
|
||||
4. **No new RE work** — we already know `openFileWithOptions` (0x6477c0) intimately
|
||||
5. **Auto-cleanup** — `FILE_FLAG_DELETE_ON_CLOSE` handles cleanup even on crash
|
||||
6. **Low risk** — we're giving Storm exactly what it expects (real disk files), just in a temp location
|
||||
7. **Files live in RAM** — `FILE_ATTRIBUTE_TEMPORARY` keeps data in the filesystem cache for our small (~2-3 MB) asset set
|
||||
1. **Reduces from 6 hooks to 1** - the single biggest complexity reduction possible
|
||||
2. **All async I/O works natively** - real OS handles eliminate the entire class of async-bypass bugs that forced hooks 4, 5, and 6
|
||||
3. **No MPQ building required** - avoids the entire hash table encryption / block table / header format complexity
|
||||
4. **No new RE work** - we already know `openFileWithOptions` (0x6477c0) intimately
|
||||
5. **Auto-cleanup** - `FILE_FLAG_DELETE_ON_CLOSE` handles cleanup even on crash
|
||||
6. **Low risk** - we're giving Storm exactly what it expects (real disk files), just in a temp location
|
||||
7. **Files live in RAM** - `FILE_ATTRIBUTE_TEMPORARY` keeps data in the filesystem cache for our small (~2-3 MB) asset set
|
||||
|
||||
**Next steps for Approach E**:
|
||||
1. Write a prototype: create temp files from embedded data at DLL init
|
||||
2. Hook `openFileWithOptions` to redirect matching paths to temp file paths
|
||||
3. Verify M2 model loading works end-to-end (including textures via async path)
|
||||
4. If successful, remove hooks 2-6 and the fake file context infrastructure
|
||||
5. Keep `loadFileDetour` (0x648620) for addon files — these use a different pipeline
|
||||
5. Keep `loadFileDetour` (0x648620) for addon files - these use a different pipeline
|
||||
|
||||
### Runner-Up: Approach A (Temp MPQ on Disk)
|
||||
|
||||
@@ -680,7 +680,7 @@ The customassets project's `CheckFileExistence` hook and glob pattern patch coul
|
||||
|--------|---------|-------------|
|
||||
| Archive array capacity | 0x8826b4 | Max archive slots |
|
||||
| Archive array count | 0x8826b8 | Current archive count |
|
||||
| Archive array pointer | 0x8826bc | `SArchive**` — array of pointers |
|
||||
| Archive array pointer | 0x8826bc | `SArchive**` - array of pointers |
|
||||
| Archive array growth | 0x8826c0 | Growth increment |
|
||||
| Disk hash table base | 0xc521e8 | File path → disk path hash table |
|
||||
| Disk hash table mask | 0xc521f0 | Hash mask (0xffffffff = disabled) |
|
||||
|
||||
+116
-116
@@ -1,7 +1,7 @@
|
||||
# M2 Model Loading Research
|
||||
|
||||
## Root Cause (Confirmed)
|
||||
M2 model loading uses `openFileWithOptions` (0x6477c0) directly — it **never** goes through our hooked `LoadFileWithTextureResourceFallback` (0x648620). That's why embedded M2 files aren't served.
|
||||
M2 model loading uses `openFileWithOptions` (0x6477c0) directly - it **never** goes through our hooked `LoadFileWithTextureResourceFallback` (0x648620). That's why embedded M2 files aren't served.
|
||||
|
||||
## M2 Loading Call Chain
|
||||
```
|
||||
@@ -30,17 +30,17 @@ createModelAttachment (0x707350)
|
||||
| `CleanupFileHandleResources` (0x648730) | `__stdcall` | 1 | `RET 0x04` |
|
||||
| `loadModelFromFileAsync` (0x71d4e0) | `__thiscall` | ECX=this, 2 stack | `RET 0x08` |
|
||||
|
||||
## GetFileSizeFromHandle (0x6487f0) — dispatches on context type
|
||||
- Type 0: `fstatFileHandle(ctx[1]+0x10)` — needs real file handle
|
||||
- Type 1: returns `ctx[5]` (value at +0x14) — **simplest, just returns a stored value**
|
||||
## GetFileSizeFromHandle (0x6487f0) - dispatches on context type
|
||||
- Type 0: `fstatFileHandle(ctx[1]+0x10)` - needs real file handle
|
||||
- Type 1: returns `ctx[5]` (value at +0x14) - **simplest, just returns a stored value**
|
||||
- Types 2/3: `GetAudioStreamPosition(ctx[0xf])`
|
||||
- Type 4: `getFileSize(ctx[0x10])`
|
||||
|
||||
## ReadFileFromMultipleSources (0x648460) — dispatches on context type
|
||||
- Type 0: `fileReadWithLock(buffer, 1, size, ctx[1])` — needs real file handle
|
||||
## ReadFileFromMultipleSources (0x648460) - dispatches on context type
|
||||
- Type 0: `fileReadWithLock(buffer, 1, size, ctx[1])` - needs real file handle
|
||||
- Other types: use respective handle fields
|
||||
|
||||
## initializeFileContext (0x647290) — __thiscall, ECX=ctx, 1 stack param (type)
|
||||
## initializeFileContext (0x647290) - __thiscall, ECX=ctx, 1 stack param (type)
|
||||
```c
|
||||
TraverseListNodes((LPCRITICAL_SECTION)(this + 0x24)); // init critical section
|
||||
*(uint *)this = param_1; // +0x00: type
|
||||
@@ -57,7 +57,7 @@ TraverseListNodes((LPCRITICAL_SECTION)(this + 0x24)); // init critical section
|
||||
*(uint *)(this + 0x1C) = 0;
|
||||
```
|
||||
|
||||
## CleanupFileHandleResources (0x648730) — __stdcall, 1 param, RET 0x04
|
||||
## CleanupFileHandleResources (0x648730) - __stdcall, 1 param, RET 0x04
|
||||
```c
|
||||
if (ctx + 0x04 != NULL) closeFileStreamSafely(ctx + 0x04); // disk file handle
|
||||
if (ctx + 0x3C != 0) Stream_CompareBuffers(ctx + 0x3C); // stream
|
||||
@@ -82,7 +82,7 @@ if (ctx + 0x40 != NULL) closeArchiveFile(ctx + 0x40); // archive
|
||||
```
|
||||
Queued via `AsyncTask_QueueForExecution` (0x443ae0). Executor reads from file handle into buffer, then calls completion callback.
|
||||
|
||||
## loadResourceByPath (0x706a50) — __thiscall, ECX=resourceMgr
|
||||
## loadResourceByPath (0x706a50) - __thiscall, ECX=resourceMgr
|
||||
After `openFileWithOptions` succeeds:
|
||||
1. Allocates 0x164-byte model object via `M2_AllocateModelBuffer`
|
||||
2. `initializeModelObject(modelObj, resourceMgr)`
|
||||
@@ -90,7 +90,7 @@ After `openFileWithOptions` succeeds:
|
||||
4. On success: copies normalized path into modelObj+0x20, sets up hash links
|
||||
5. On failure: `CleanupFileHandleResources`, free model object, return NULL
|
||||
|
||||
## LoadFileWithTextureResourceFallback (0x648620) — our hooked function
|
||||
## LoadFileWithTextureResourceFallback (0x648620) - our hooked function
|
||||
Calls `openFileWithOptions(param_1, path, async_flag, &handle_out)`, then:
|
||||
1. `GetFileSizeFromHandle(handle, NULL)` → size
|
||||
2. `M2_AllocateModelBuffer(size + extra_alloc)` → buffer
|
||||
@@ -119,7 +119,7 @@ Every file context type needs a **real file handle or archive handle** for the r
|
||||
|
||||
### Option 3: Hook openFileWithOptions only
|
||||
- Make it produce a context that works through existing read pipeline
|
||||
- **Hardest** — requires understanding all read paths for the chosen type
|
||||
- **Hardest** - requires understanding all read paths for the chosen type
|
||||
|
||||
## Implementation: In-Memory File Serving (chosen: Option 1 extended)
|
||||
|
||||
@@ -136,21 +136,21 @@ Every file context type needs a **real file handle or archive handle** for the r
|
||||
|
||||
### Fake File Context Layout
|
||||
Allocated via `allocateGameBuffer(0x60)`, zero-filled, then:
|
||||
- `initializeFileContext(ctx, 0)` — sets type=0, inits critsec at +0x24
|
||||
- `initializeFileContext(ctx, 0)` - sets type=0, inits critsec at +0x24
|
||||
- `+0x0C`: duplicated path string (game-allocated)
|
||||
- `+0x30`: embedded data pointer (custom field, points into DLL .rdata)
|
||||
- `+0x34`: embedded data size (custom field)
|
||||
- **Detection**: `type==0 && handle(+0x04)==NULL && *(ctx+0x30)!=0`
|
||||
- Return value from openFileWithOptions: 2 (non-zero = success)
|
||||
|
||||
### processAsyncFileOperation (0x647350) — verified via Ghidra
|
||||
### processAsyncFileOperation (0x647350) - verified via Ghidra
|
||||
- `__fastcall(ECX=request)`, plain `RET` (c3)
|
||||
- Request: `+0x08`=file_ctx, `+0x0C`=dest_buf, `+0x10`=read_size, `+0x14`=seek/event struct
|
||||
- Event handle at `*(*(request+0x14)+4)` — signaled via `SetEvent`
|
||||
- Event handle at `*(*(request+0x14)+4)` - signaled via `SetEvent`
|
||||
- Cleanup epilogue: decrement `*(ctx+0x5c)`, `LeaveCriticalSection(ctx+0x24)`, signal event, conditional `CleanupFileHandleResources`
|
||||
- Close-after-read flag at `*(ctx+0x58)`
|
||||
|
||||
### CleanupFileHandleResources (0x648730) — verified decompile
|
||||
### CleanupFileHandleResources (0x648730) - verified decompile
|
||||
```c
|
||||
void CleanupFileHandleResources(int ctx) {
|
||||
if (ctx == 0) return;
|
||||
@@ -160,21 +160,21 @@ void CleanupFileHandleResources(int ctx) {
|
||||
if (*(ctx+0x08)) { /* check+free sub-buffer */ FreeMemory(*(ctx+0x08)); }
|
||||
cleanupFileContext(ctx); // destroy critsec
|
||||
FreeMemory(ctx); // free 0x60 struct
|
||||
// NOTE: does NOT free path at +0x0C — we must free it ourselves
|
||||
// NOTE: does NOT free path at +0x0C - we must free it ourselves
|
||||
}
|
||||
```
|
||||
FreeMemory (SMemFree) at **0x646430** — `__stdcall(ptr, src_str, flags)`.
|
||||
FreeMemory (SMemFree) at **0x646430** - `__stdcall(ptr, src_str, flags)`.
|
||||
|
||||
### Crash: Hook Install Order Matters
|
||||
**Symptom**: Crash on game load in `loadFileDetour` calling `file_hook.getTrampoline()`.
|
||||
The trampoline memory (VirtualAlloc'd) contained zeros instead of the saved prologue.
|
||||
|
||||
**Analysis**:
|
||||
- Crash at `0x075D5E88` (trampoline memory) — bytes: `00 00 00 00`
|
||||
- Crash at `0x075D5E88` (trampoline memory) - bytes: `00 00 00 00`
|
||||
- Return address `0x04AA1542` in weirdutils.dll = `CALL *%eax` in `loadFileDetour`
|
||||
- `getTrampoline` (compiled at DLL+0x3250) reads `file_hook.trampoline` field from `.data` section at `0x1015c0d8`, checks non-NULL, calls through it
|
||||
|
||||
**Root cause**: `LoadFileWithTextureResourceFallback` (0x648620) internally calls `openFileWithOptions` (0x6477c0). If `file_hook` is installed FIRST (copying the 0x648620 prologue to its trampoline), and THEN we patch 0x6477c0, the trampoline's execution context is disrupted. The `file_hook` trampoline runs the original 0x648620 prologue which eventually calls 0x6477c0 — but if 0x6477c0 was patched after the trampoline was built, there may be page-level or VirtualProtect interactions that corrupt the trampoline's allocated memory.
|
||||
**Root cause**: `LoadFileWithTextureResourceFallback` (0x648620) internally calls `openFileWithOptions` (0x6477c0). If `file_hook` is installed FIRST (copying the 0x648620 prologue to its trampoline), and THEN we patch 0x6477c0, the trampoline's execution context is disrupted. The `file_hook` trampoline runs the original 0x648620 prologue which eventually calls 0x6477c0 - but if 0x6477c0 was patched after the trampoline was built, there may be page-level or VirtualProtect interactions that corrupt the trampoline's allocated memory.
|
||||
|
||||
**Fix**: Install Storm I/O hooks (`installFileHooks`) BEFORE `file_hook` at 0x648620. Remove in reverse order.
|
||||
|
||||
@@ -184,20 +184,20 @@ There are TWO independent async systems for file I/O:
|
||||
|
||||
**1. High-level async task system** (used by M2 model + texture loading):
|
||||
- Queue: `AsyncTask_QueueForExecution` (0x443ae0)
|
||||
- Worker: `AsyncTaskWorkerThread` (0x443360) — calls **ReadFileFromMultipleSources** (our hook 3!)
|
||||
- Main thread: `ProcessAsyncTasksWithTimeLimit` (0x443E70) — calls completion callbacks
|
||||
- Worker: `AsyncTaskWorkerThread` (0x443360) - calls **ReadFileFromMultipleSources** (our hook 3!)
|
||||
- Main thread: `ProcessAsyncTasksWithTimeLimit` (0x443E70) - calls completion callbacks
|
||||
- Task structure: `[0]=file_ctx, [1]=buffer, [2]=size, [3]=callback_ctx, [4]=callback_fn`
|
||||
- For M2 models: callback = `onModelLoadComplete` (0x71d5e0), executor = `asyncFileReader` (0x71d610)
|
||||
- For textures: callback = `TextureLoadCallback` (0x44a500), queued from `LoadTextureFromPath` (0x44a310)
|
||||
|
||||
**2. Low-level file async system** (NOT used for texture/M2 loading):
|
||||
- `processAsyncFileOperation` (0x647350) — calls **fileReadWithLock** (0x740c97) directly
|
||||
- `processAsyncFileOperation` (0x647350) - calls **fileReadWithLock** (0x740c97) directly
|
||||
- Request: `+0x08=file_ctx, +0x0C=dest_buf, +0x10=read_size, +0x14=seek/event`
|
||||
- Type-dispatched: type 0→fileReadWithLock, type 1→decompressFileData, type 2/3→stream, type 4→archive
|
||||
- Has close-after-read flag at ctx+0x58, refcount at ctx+0x5c
|
||||
- NOT installed as a hook — not needed since texture loading uses the high-level system
|
||||
- NOT installed as a hook - not needed since texture loading uses the high-level system
|
||||
|
||||
### onModelLoadComplete (0x71d5e0) — Verified Decompile
|
||||
### onModelLoadComplete (0x71d5e0) - Verified Decompile
|
||||
```c
|
||||
void __fastcall onModelLoadComplete(void *modelObject) {
|
||||
CleanupFileHandleResources(**(int **)(modelObject + 0xc)); // free file handle via task
|
||||
@@ -208,7 +208,7 @@ void __fastcall onModelLoadComplete(void *modelObject) {
|
||||
```
|
||||
**Critical**: cleanup file handle BEFORE processLoadedModelData. Our hook matches this order.
|
||||
|
||||
### cleanupFileContext (0x6472d0) — Verified Decompile
|
||||
### cleanupFileContext (0x6472d0) - Verified Decompile
|
||||
```c
|
||||
void __fastcall cleanupFileContext(int param_1) {
|
||||
if (*(param_1 + 0x1c)) { cleanupInflateContext(*(param_1+0x1c)); FreeMemory(*(param_1+0x1c)); }
|
||||
@@ -220,7 +220,7 @@ void __fastcall cleanupFileContext(int param_1) {
|
||||
```
|
||||
NOTE: cleanupFileContext DOES free +0x0C (path string). Do NOT free it manually.
|
||||
|
||||
### TextureLoadCallback (0x44a500) — Verified Decompile
|
||||
### TextureLoadCallback (0x44a500) - Verified Decompile
|
||||
```c
|
||||
void __fastcall TextureLoadCallback(int texture_obj) {
|
||||
puVar1 = ProcessTextureData(texture_obj);
|
||||
@@ -233,7 +233,7 @@ void __fastcall TextureLoadCallback(int texture_obj) {
|
||||
```
|
||||
Called by ProcessAsyncTasksWithTimeLimit on the main thread after worker completes.
|
||||
|
||||
### LoadTextureFromPath (0x44a310) — Texture Async Task Setup
|
||||
### LoadTextureFromPath (0x44a310) - Texture Async Task Setup
|
||||
```c
|
||||
puVar5 = openFileWithOptions(NULL, path, flags, &file_handle); // hook 1 creates fake
|
||||
puVar7 = AllocateAsyncTaskObject();
|
||||
@@ -251,7 +251,7 @@ AsyncTask_QueueForExecution(task);
|
||||
- Crash at EIP=0x00000000 after processLoadedModelData returns 1
|
||||
- Stack shows TextureLoadCallback (0x44A526 = after CALL CleanupFileHandleResources)
|
||||
- File context 0x36596088 on stack (was M2 ctx, freed, reused as BLP ctx)
|
||||
- EDX=0x36596080 (ctx-8), EBP=04AA3212 (in weirdutils.dll — corrupted frame ptr)
|
||||
- EDX=0x36596080 (ctx-8), EBP=04AA3212 (in weirdutils.dll - corrupted frame ptr)
|
||||
- Crash appears to be inside our cleanupFileHandleDetour during BLP context cleanup
|
||||
- BLP data IS served correctly (AsyncTaskWorkerThread → ReadFileFromMultipleSources → hook 3)
|
||||
- Investigation ongoing: possible stack corruption in callCleanupFileContext or freeGameBuffer
|
||||
@@ -274,7 +274,7 @@ WorldFrameUpdate(this, deltaTime):
|
||||
if hitType == 2: HandleTargetSelection(this, &localResult) // object hover
|
||||
```
|
||||
|
||||
### UpdateHitTest (0x481F00) — __fastcall(ECX=worldFrame)
|
||||
### UpdateHitTest (0x481F00) - __fastcall(ECX=worldFrame)
|
||||
Called on **click events** (not every frame). Performs the same raycast but stores
|
||||
the result persistently at `worldFrame + 0x350`:
|
||||
```c
|
||||
@@ -301,14 +301,14 @@ void __fastcall UpdateHitTest(void *worldFrame) {
|
||||
| +0x388 | 4 | f32 | Ray distance |
|
||||
|
||||
### AoE Targeting Reticle Globals (only during spell targeting)
|
||||
- `0x00B4B3A0` Vec3 — terrain position under cursor (written by HandleGroundTargeting)
|
||||
- `0x00B4B3B0` f32 — spell targeting radius
|
||||
- `0x0083DC2C` u32 — validity: 0=valid, 1=out-of-range, 3=updating
|
||||
- `0x00CECAC0` u16 — spell targeting state flags (0x20=terrain, 0x40=secondary)
|
||||
- `0x00B4B3A0` Vec3 - terrain position under cursor (written by HandleGroundTargeting)
|
||||
- `0x00B4B3B0` f32 - spell targeting radius
|
||||
- `0x0083DC2C` u32 - validity: 0=valid, 1=out-of-range, 3=updating
|
||||
- `0x00CECAC0` u16 - spell targeting state flags (0x20=terrain, 0x40=secondary)
|
||||
|
||||
### Click-to-Move Destination
|
||||
- `0x00C4D890` Vec3 — destination (only when CTM initiated)
|
||||
- `0x00C4D888` u32 — movement mode
|
||||
- `0x00C4D890` Vec3 - destination (only when CTM initiated)
|
||||
- `0x00C4D888` u32 - movement mode
|
||||
|
||||
### Key Functions
|
||||
| Address | Name | Convention | Params |
|
||||
@@ -326,10 +326,10 @@ void __fastcall UpdateHitTest(void *worldFrame) {
|
||||
### Approach for Markers
|
||||
### HitTestPoint / WorldIntersectionTest Return Values
|
||||
`WorldIntersectionTest(rayStart, rayEnd, gameStateFlags, result)` returns:
|
||||
- `0` — no intersection (sky) — coords NOT written to result
|
||||
- `gameStateFlags & 1` — terrain hit — coords written. Returns 1 only during
|
||||
- `0` - no intersection (sky) - coords NOT written to result
|
||||
- `gameStateFlags & 1` - terrain hit - coords written. Returns 1 only during
|
||||
AoE targeting (bit 0 set), otherwise returns 0 even on valid terrain hit
|
||||
- `2` — object hit (closer than terrain)
|
||||
- `2` - object hit (closer than terrain)
|
||||
|
||||
So hitType=0 is ambiguous: either "terrain hit in normal mode" or "no hit at all".
|
||||
To distinguish: zero the result coords before calling, then check if they were written.
|
||||
@@ -337,7 +337,7 @@ To distinguish: zero the result coords before calling, then check if they were w
|
||||
### Approach for Markers
|
||||
Call `UpdateHitTest(worldFrame)` to perform the raycast and store result at
|
||||
`worldFrame+0x350`. Zero intersection coords before the call, then check if
|
||||
they were populated. This is safe from Lua callbacks — `HitTestPoint`
|
||||
they were populated. This is safe from Lua callbacks - `HitTestPoint`
|
||||
saves/restores view matrices. The persistent result at `worldFrame+0x358` is
|
||||
normally only click-updated, but overwriting it is harmless.
|
||||
|
||||
@@ -553,19 +553,19 @@ cause the visual glitch. Need to identify which bone tracks use global sequences
|
||||
|
||||
---
|
||||
|
||||
## World Teardown — Entity Cleanup Crash Investigation
|
||||
## World Teardown - Entity Cleanup Crash Investigation
|
||||
|
||||
### Crash Details
|
||||
- **Crash function**: 0x687220 — generic linked-list unlink operation
|
||||
- First crash at 0x687243: `mov [edx], esi` — write to freed memory
|
||||
- Second crash at 0x687221: `mov esi, [ecx]` — read from freed memory
|
||||
- **When**: Logout to character select, map transitions — NOT during normal gameplay
|
||||
- **Crash function**: 0x687220 - generic linked-list unlink operation
|
||||
- First crash at 0x687243: `mov [edx], esi` - write to freed memory
|
||||
- Second crash at 0x687221: `mov esi, [ecx]` - read from freed memory
|
||||
- **When**: Logout to character select, map transitions - NOT during normal gameplay
|
||||
- **Thread**: Background/worker thread (very short stack: WoW.exe → kernel32 → ntdll)
|
||||
- **Root cause**: WDOODADDEF heap teardown iterates linked list, hits freed or corrupt node
|
||||
|
||||
### Decompiled Crash Function (0x687220)
|
||||
```c
|
||||
// Linked-list unlink — removes node from intrusive doubly-linked list
|
||||
// Linked-list unlink - removes node from intrusive doubly-linked list
|
||||
void __fastcall UnlinkFromList(int *param_1) {
|
||||
int prev = *param_1; // param_1[0] = prev pointer
|
||||
if (prev != 0) {
|
||||
@@ -597,7 +597,7 @@ void __fastcall UnlinkFromList(int *param_1) {
|
||||
|
||||
### World Teardown Chain (Ghidra-verified)
|
||||
```
|
||||
CleanupWorldAndEntities (0x66fc40) — void(), no params, __stdcall
|
||||
CleanupWorldAndEntities (0x66fc40) - void(), no params, __stdcall
|
||||
├── CleanupEntityList_ProcessAll() ← iterates UNKNOWN list
|
||||
└── CleanupWorldAndReleaseResources (0x697ac0)
|
||||
├── ClearWorldObjectsAndResetState (0x6a6710) ← iterates linked list at PTR_00c96088
|
||||
@@ -608,13 +608,13 @@ CleanupWorldAndEntities (0x66fc40) — void(), no params, __stdcall
|
||||
```
|
||||
|
||||
### Callers of CleanupWorldAndEntities (0x66fc40)
|
||||
- `InitializeWorldScene` (0x401bc0) — **map change** (cleans old world before loading new)
|
||||
- `ShutdownClientSystems` (0x401ee0) — **full game exit**
|
||||
- `InitializeWorldScene` (0x401bc0) - **map change** (cleans old world before loading new)
|
||||
- `ShutdownClientSystems` (0x401ee0) - **full game exit**
|
||||
|
||||
### Callers of ClearWorldObjectsAndResetState (0x6a6710)
|
||||
- `LoadWorldMap` (0x6941f0) — map loading
|
||||
- `UpdateWorldAndGameObjects` (0x698390) — periodic world update (chunk unloading?)
|
||||
- `CleanupWorldAndReleaseResources` (0x697ac0) — full teardown
|
||||
- `LoadWorldMap` (0x6941f0) - map loading
|
||||
- `UpdateWorldAndGameObjects` (0x698390) - periodic world update (chunk unloading?)
|
||||
- `CleanupWorldAndReleaseResources` (0x697ac0) - full teardown
|
||||
- `SimpleWorldUpdate` (0x694920)
|
||||
|
||||
### Entity Type Dispatch in CleanupEntity_ProcessAttachments (0x670d50)
|
||||
@@ -636,7 +636,7 @@ void __fastcall CleanupEntity_ProcessAttachments(entity) {
|
||||
```
|
||||
- M2 entities (CreateWorldUnit → WDOODADDEF heap) have flag 0x40
|
||||
- WMO entities (CreateGameObject → WMAPOBJDEF heap) have flag 0x8
|
||||
- Ghidra names are misleading — `cleanupGameObject` handles M2/WDOODADDEF, `destroyWorldEnvironment` handles WMO/WMAPOBJDEF
|
||||
- Ghidra names are misleading - `cleanupGameObject` handles M2/WDOODADDEF, `destroyWorldEnvironment` handles WMO/WMAPOBJDEF
|
||||
|
||||
### CleanupEntity_ProcessAttachments Callers (ONLY 3 in entire binary)
|
||||
- `processCinematicExit` (0x6e4940)
|
||||
@@ -645,10 +645,10 @@ void __fastcall CleanupEntity_ProcessAttachments(entity) {
|
||||
- **NOT called by CleanupEntityList_ProcessAll** or any teardown function
|
||||
|
||||
### WDOODADDEF Heap (0xCA7E20) References
|
||||
- `AllocateRenderableObject` (0x6a07f7) — allocates from heap
|
||||
- `CleanupVisualEffectAndRelease` (0x6a0916) — frees to heap
|
||||
- `InitializeWorldSystem` (0x691f4f) — initializes heap
|
||||
- `CleanupWorldSystem` (0x692241) — tears down heap
|
||||
- `AllocateRenderableObject` (0x6a07f7) - allocates from heap
|
||||
- `CleanupVisualEffectAndRelease` (0x6a0916) - frees to heap
|
||||
- `InitializeWorldSystem` (0x691f4f) - initializes heap
|
||||
- `CleanupWorldSystem` (0x692241) - tears down heap
|
||||
- Called by `ShutdownAllGameSystems` (0x66fb00)
|
||||
|
||||
### Other Key Addresses
|
||||
@@ -656,9 +656,9 @@ void __fastcall CleanupEntity_ProcessAttachments(entity) {
|
||||
- `CleanupWorldSystem` (0x6920c0) → called by ShutdownAllGameSystems
|
||||
- `cleanupSecondaryResources` (0x6a6c70) → called from CleanupWorldAndEntities + CleanupWorldSystem
|
||||
- This calls `cleanupGameObject` (0x6a67a0) and `destroyWorldEnvironment` (0x6a6870) on entities
|
||||
- `gameQuit` (0x41f9b0) — fires on disconnect/quit (ref: UnitXP_SP3)
|
||||
- `gameQuit` (0x41f9b0) - fires on disconnect/quit (ref: UnitXP_SP3)
|
||||
|
||||
### World Unit Hash Table (0xCA7DC0) — The Crash Structure
|
||||
### World Unit Hash Table (0xCA7DC0) - The Crash Structure
|
||||
|
||||
The crash occurs during teardown of a **hash table at 0xCA7DC0** that tracks all world units
|
||||
(doodads created via `CreateWorldUnit`). Our entities ARE in this table.
|
||||
@@ -682,7 +682,7 @@ The crash occurs during teardown of a **hash table at 0xCA7DC0** that tracks all
|
||||
- `FindOrCreateWorldUnit` (0x694e90): reads 0xCA7DC4, 0xCA7DDC
|
||||
- `complexListInitializerWithCleanup` (0x69f670): reads/writes ALL (init + teardown)
|
||||
|
||||
### CreateWorldUnit (0x694980) — Entity Registration (Decompiled)
|
||||
### CreateWorldUnit (0x694980) - Entity Registration (Decompiled)
|
||||
|
||||
`CreateWorldUnit` inserts entities into **three** linked lists:
|
||||
```c
|
||||
@@ -722,7 +722,7 @@ int *CreateWorldUnit(char *modelPath, float *pos, float facing, int param4) {
|
||||
|
||||
**Critical**: Entity is in 3 lists. All 3 must be unlinked during cleanup.
|
||||
|
||||
### CleanupVisualEffectAndRelease (0x6a0840) — What It Unlinks
|
||||
### CleanupVisualEffectAndRelease (0x6a0840) - What It Unlinks
|
||||
|
||||
`CleanupVisualEffectAndRelease` unlinks from up to **three** linked lists:
|
||||
```c
|
||||
@@ -807,21 +807,21 @@ Only 3 CALLs visible in the function body: `FindSubstringInString`, `CreateGameO
|
||||
`ModelAttachment_CreateNode`. The M2 path (`CreateWorldUnit`) must be via tail-call or
|
||||
the decompiler inlined it.
|
||||
|
||||
### CleanupWorldAndEntities (0x66fc40) — Full Chain (Decompiled)
|
||||
### CleanupWorldAndEntities (0x66fc40) - Full Chain (Decompiled)
|
||||
|
||||
```c
|
||||
void CleanupWorldAndEntities(void) {
|
||||
CleanupEntityList_ProcessAll(); // 0x672c40 — iterates PTR_00c7b2dc (NOT hash table)
|
||||
CleanupEntityList_ProcessAll(); // 0x672c40 - iterates PTR_00c7b2dc (NOT hash table)
|
||||
CleanupWorldAndReleaseResources(); // 0x697ac0
|
||||
PTR_00c7b748 = 0;
|
||||
}
|
||||
```
|
||||
|
||||
### CleanupEntityList_ProcessAll (0x672c40) — Decompiled
|
||||
### CleanupEntityList_ProcessAll (0x672c40) - Decompiled
|
||||
|
||||
Iterates the linked list at `PTR_00c7b2dc` (NOT the hash table at 0xCA7DC0).
|
||||
For each entry, calls `CleanupObjectAttachments_FreeMemory` which ends with
|
||||
`DestroyWorldObjectAndRelease`. Our entities are NOT in `PTR_00c7b2dc` —
|
||||
`DestroyWorldObjectAndRelease`. Our entities are NOT in `PTR_00c7b2dc` -
|
||||
they're only registered in the hash table. So this function doesn't touch them.
|
||||
|
||||
```c
|
||||
@@ -838,24 +838,24 @@ void CleanupEntityList_ProcessAll(void) {
|
||||
```
|
||||
|
||||
**PTR_00c7b2dc xrefs** (who manages this list):
|
||||
- `CleanupEntityList_ProcessAll` (0x672c40) — reads/iterates
|
||||
- `UpdateFadeEffects_ProcessTimers` (0x672efe) — reads
|
||||
- `DestroyFileMapping` (0x66f460) — reads + writes (teardown)
|
||||
- `SetFileAttributes` (0x66f440) — writes (initialization)
|
||||
- `CreateFadeEffect_EntityManagement` (0x672e76) — reads (entity insertion?)
|
||||
- `CleanupEntityList_ProcessAll` (0x672c40) - reads/iterates
|
||||
- `UpdateFadeEffects_ProcessTimers` (0x672efe) - reads
|
||||
- `DestroyFileMapping` (0x66f460) - reads + writes (teardown)
|
||||
- `SetFileAttributes` (0x66f440) - writes (initialization)
|
||||
- `CreateFadeEffect_EntityManagement` (0x672e76) - reads (entity insertion?)
|
||||
|
||||
Our entities created via `CreateEntityInstance_WithAttachment` are NOT added to this
|
||||
list. The callers (`CastSpellByID_Extended`, `CreateGameObjectPathEffect`) probably add
|
||||
the returned entity to PTR_00c7b2dc themselves. We don't.
|
||||
|
||||
### Vtable at 0x0081089c — Hash Table Entry Destructors
|
||||
### Vtable at 0x0081089c - Hash Table Entry Destructors
|
||||
|
||||
```
|
||||
[0] 0x006a1170 DestroyMapDoodadDefinition — WDOODADDEF destructor
|
||||
[0] 0x006a1170 DestroyMapDoodadDefinition - WDOODADDEF destructor
|
||||
[1] 0x006a11a0 LoadAndAddMapDoodadToList
|
||||
[2] 0x006a14d0 MapDoodadDestructor
|
||||
[3] 0x006a1260 CleanupMapDoodadContainer
|
||||
[4] 0x006a1320 DestroyMapObjectDefinition — WMAPOBJDEF destructor
|
||||
[4] 0x006a1320 DestroyMapObjectDefinition - WMAPOBJDEF destructor
|
||||
[5] 0x006a1350 LoadAndAddMapObjectToList
|
||||
[6] 0x006a1590 MapObjectDestructor
|
||||
[7] 0x006a1410 CleanupMapObjectContainer
|
||||
@@ -869,9 +869,9 @@ void DestroyMapDoodadDefinition(undefined **param_1) {
|
||||
}
|
||||
```
|
||||
This is a simple destructor: calls entity vtable[0](0) then frees via SMemFree.
|
||||
NOT the same as CleanupVisualEffectAndRelease — doesn't unlink from lists.
|
||||
NOT the same as CleanupVisualEffectAndRelease - doesn't unlink from lists.
|
||||
|
||||
### hashTableTeardownLoop (0x69f740) — atexit Handler (Decompiled)
|
||||
### hashTableTeardownLoop (0x69f740) - atexit Handler (Decompiled)
|
||||
|
||||
Registered via `validateMemoryOperation` (atexit) at 0x69f730. Runs during process exit.
|
||||
```c
|
||||
@@ -912,7 +912,7 @@ void hashTableTeardownLoop(void) {
|
||||
If an entity was freed (by our cleanup) but not unlinked from these lists, the teardown
|
||||
follows dangling pointers into freed heap memory.
|
||||
|
||||
### cleanupGameObject (0x6a67a0) — Decompiled
|
||||
### cleanupGameObject (0x6a67a0) - Decompiled
|
||||
|
||||
```c
|
||||
void __fastcall cleanupGameObject(undefined **param_1) {
|
||||
@@ -939,7 +939,7 @@ void __fastcall cleanupGameObject(undefined **param_1) {
|
||||
}
|
||||
```
|
||||
|
||||
### CleanupVisualEffectAndRelease (0x6a0840) — What It Actually Unlinks
|
||||
### CleanupVisualEffectAndRelease (0x6a0840) - What It Actually Unlinks
|
||||
|
||||
```c
|
||||
void __fastcall CleanupVisualEffectAndRelease(entity) {
|
||||
@@ -963,7 +963,7 @@ void __fastcall CleanupVisualEffectAndRelease(entity) {
|
||||
**CONFIRMED**: `CleanupVisualEffectAndRelease` unlinks entity[4]/[5] (global list)
|
||||
and conditionally entity[0x2e-0x31]. It does NOT unlink from the **hash bucket list**.
|
||||
|
||||
### ManageLinkedList (0x695ef0) — Intrusive List Insertion
|
||||
### ManageLinkedList (0x695ef0) - Intrusive List Insertion
|
||||
|
||||
```c
|
||||
void __thiscall ManageLinkedList(void *this, int *entity, int mode, int insert_point) {
|
||||
@@ -1063,29 +1063,29 @@ So the 3 intrusive list node offsets within a WDOODADDEF entity are:
|
||||
After `ManageLinkedList` insertion, entity[0x2F] should always be non-zero
|
||||
(sentinel has bit 0 set = odd address).
|
||||
|
||||
### InitializeRenderableObject (0x6a7d00) — Entity Initialization
|
||||
### InitializeRenderableObject (0x6a7d00) - Entity Initialization
|
||||
|
||||
Called from `AllocateRenderableObject`. Zeroes most fields including:
|
||||
- entity[0x2E] = 0, entity[0x2F] = 0 (bucket list node — zeroed before insertion)
|
||||
- entity[0x30] = 0, entity[0x31] = 0 (global list node — zeroed before insertion)
|
||||
- entity[0x2E] = 0, entity[0x2F] = 0 (bucket list node - zeroed before insertion)
|
||||
- entity[0x30] = 0, entity[0x31] = 0 (global list node - zeroed before insertion)
|
||||
- entity[2] |= 0x40 (sets the M2/WDOODADDEF flag)
|
||||
- entity[0] = vtable PTR_DestroyRenderableObject_00810a74
|
||||
|
||||
### Callers of CreateEntityInstance_WithAttachment — What They Do After
|
||||
### Callers of CreateEntityInstance_WithAttachment - What They Do After
|
||||
|
||||
Only 3 callers in entire binary:
|
||||
|
||||
1. **`CreateGameObjectPathEffect` (0x5f8030)** — `__thiscall` on a game object
|
||||
1. **`CreateGameObjectPathEffect` (0x5f8030)** - `__thiscall` on a game object
|
||||
- Does NOT save the return value! Fire-and-forget.
|
||||
- Passes `(path, pos, facing, 0, 0, param_1, param_2)` — param_6/7 are parent refs
|
||||
- Passes `(path, pos, facing, 0, 0, param_1, param_2)` - param_6/7 are parent refs
|
||||
|
||||
2. **`CastSpellByID_Extended` (0x6e4b60)** — spell casting
|
||||
2. **`CastSpellByID_Extended` (0x6e4b60)** - spell casting
|
||||
- Stores in global `PTR_00ceca8c`
|
||||
- Calls `SetEntityFlag_ToggleBit(entity, 0)` = sets `entity[0xD] |= 1`
|
||||
- Cleaned up by `processCinematicExit` → `CleanupEntity_ProcessAttachments(PTR_00ceca8c)`
|
||||
- Passes `(path, pos, 0.0, 0, 0, 0, 0)` — update_now=0!
|
||||
- Passes `(path, pos, 0.0, 0, 0, 0, 0)` - update_now=0!
|
||||
|
||||
3. **Unknown (0x6e5a6e)** — likely another spell effect
|
||||
3. **Unknown (0x6e5a6e)** - likely another spell effect
|
||||
|
||||
**Key differences from our call**:
|
||||
- Both native callers pass `update_now=0` (param_5). We pass `update_now=1`.
|
||||
@@ -1098,7 +1098,7 @@ Only 3 callers in entire binary:
|
||||
Map doodads use `FindOrCreateWorldUnit` (0x694e90) called from `AttachDoodadObjects` (0x695b1e).
|
||||
These are separate creation paths that both register in the hash table but through different code.
|
||||
|
||||
### CleanupWorldAndReleaseResources (0x697ac0) — Full Chain
|
||||
### CleanupWorldAndReleaseResources (0x697ac0) - Full Chain
|
||||
|
||||
```c
|
||||
void CleanupWorldAndReleaseResources(void) {
|
||||
@@ -1119,7 +1119,7 @@ atexit teardown at 0x691830 (separate from hash table teardown). Base offset is
|
||||
|
||||
Disabled ALL our cleanup (no CleanupEntity_ProcessAttachments, no world_cleanup_hook,
|
||||
no removeHooks cleanup). Created 5 markers, replaced with 5 more, closed game.
|
||||
**Still crashed.** This means the crash is NOT caused by our cleanup — the game's
|
||||
**Still crashed.** This means the crash is NOT caused by our cleanup - the game's
|
||||
own atexit handler can't handle our entities even when they're fully intact.
|
||||
|
||||
The WDOODADDEF heap is destroyed by `CleanupWorldSystem` (called from
|
||||
@@ -1138,22 +1138,22 @@ and are NOT registered in any cleanup tracking list. They're cleaned up explicit
|
||||
map change/exit, it would have the same crash problem as our entities.
|
||||
|
||||
The game avoids this because spells always end before map transitions. But we don't
|
||||
have that guarantee — our markers persist across frames until explicitly cleared.
|
||||
have that guarantee - our markers persist across frames until explicitly cleared.
|
||||
|
||||
### TODO
|
||||
- [x] Decompile 0x672c40 (CleanupEntityList_ProcessAll) — iterates PTR_00c7b2dc, not hash table
|
||||
- [x] Decompile hashTableTeardownLoop (0x69f740) — atexit handler, iterates all hash entries
|
||||
- [x] Check vtable at 0x0081089c — DestroyMapDoodadDefinition, simple free
|
||||
- [x] Decompile ContainerLookup (0x687960) — converts between list spaces
|
||||
- [x] Decompile cleanupGameObject + CleanupVisualEffectAndRelease — handles all 3 lists IF entity[0x2F]!=0
|
||||
- [x] Decompile ManageLinkedList (0x695ef0) — intrusive list with base offset
|
||||
- [x] Decompile 0x672c40 (CleanupEntityList_ProcessAll) - iterates PTR_00c7b2dc, not hash table
|
||||
- [x] Decompile hashTableTeardownLoop (0x69f740) - atexit handler, iterates all hash entries
|
||||
- [x] Check vtable at 0x0081089c - DestroyMapDoodadDefinition, simple free
|
||||
- [x] Decompile ContainerLookup (0x687960) - converts between list spaces
|
||||
- [x] Decompile cleanupGameObject + CleanupVisualEffectAndRelease - handles all 3 lists IF entity[0x2F]!=0
|
||||
- [x] Decompile ManageLinkedList (0x695ef0) - intrusive list with base offset
|
||||
- [x] Confirm bucket base offset = 0xB8 from InitializeHashTable
|
||||
- [x] TEST: no-cleanup build still crashes — crash is NOT from our cleanup
|
||||
- [x] Decompile native callers — neither registers in extra lists
|
||||
- [x] TEST: no-cleanup build still crashes - crash is NOT from our cleanup
|
||||
- [x] Decompile native callers - neither registers in extra lists
|
||||
- [x] **Investigate spell-spawned game objects (e.g. mailbox summon) as reference**
|
||||
- Spell entities are NOT "persistent game objects" — they're client-side visual effects only
|
||||
- Spell entities are NOT "persistent game objects" - they're client-side visual effects only
|
||||
- `processCinematicExit` (0x6e4940) explicitly cleans them: `CleanupEntity_ProcessAttachments(entity); entity = NULL;`
|
||||
- Called before every new spell cast — spell entities NEVER survive to teardown
|
||||
- Called before every new spell cast - spell entities NEVER survive to teardown
|
||||
- If a spell entity survived to atexit, it would crash too (same bug as ours)
|
||||
- [x] Check what `ClearWorldObjectsAndResetState` (PTR_00c96088) contains
|
||||
- PTR_00c96088 is a **terrain chunk list**, NOT a WDOODADDEF entity list
|
||||
@@ -1161,12 +1161,12 @@ have that guarantee — our markers persist across frames until explicitly clear
|
||||
- `LoadWorldTerrainChunk` writes to PTR_00c96084 (the list head)
|
||||
- Map doodads are attached to parent chunks via `ModelAttachment_CreateNode` in `AttachDoodadObjects`
|
||||
- `destroyPrimaryGameObject` (0x6a69f0) destroys the parent chunk, which walks attachment children
|
||||
- We CANNOT participate in this list — it's for terrain chunks, not standalone entities
|
||||
- We CANNOT participate in this list - it's for terrain chunks, not standalone entities
|
||||
- [x] Determine proper entity lifecycle for persistent world objects
|
||||
- **There is no native path for standalone persistent WDOODADDEF entities**
|
||||
- All native callers either: (a) attach to parent chunks, or (b) explicitly clean up before teardown
|
||||
- Correct approach: explicit cleanup via `CleanupEntity_ProcessAttachments` before teardown
|
||||
- Hook point: `CleanupWorldAndEntities` (0x66fc40) PRE-hook — fires for exit, logout, AND map change
|
||||
- Hook point: `CleanupWorldAndEntities` (0x66fc40) PRE-hook - fires for exit, logout, AND map change
|
||||
|
||||
## Entity Lifecycle Solution (Confirmed)
|
||||
|
||||
@@ -1201,7 +1201,7 @@ Hook `CleanupWorldAndEntities` (0x66fc40). Before calling the original:
|
||||
1. Call `CleanupEntity_ProcessAttachments` on all active marker entities
|
||||
2. Call `CleanupEntity_ProcessAttachments` on all despawning entities
|
||||
3. Null all entity pointers / reset state
|
||||
4. Call original — hash table no longer contains our entries → no crash
|
||||
4. Call original - hash table no longer contains our entries → no crash
|
||||
|
||||
This handles ALL scenarios: game exit, logout, map change.
|
||||
|
||||
@@ -1215,7 +1215,7 @@ void __fastcall destroyPrimaryGameObject(undefined **param_1) {
|
||||
}
|
||||
```
|
||||
|
||||
#### processCinematicExit (0x6e4940) — Spell Entity Cleanup
|
||||
#### processCinematicExit (0x6e4940) - Spell Entity Cleanup
|
||||
```c
|
||||
// After handling cinematic/targeting state...
|
||||
if (PTR_00ceca8c != NULL) {
|
||||
@@ -1224,7 +1224,7 @@ if (PTR_00ceca8c != NULL) {
|
||||
}
|
||||
```
|
||||
|
||||
#### CreateEntityInstance_WithAttachment (0x6707c0) — M2 Path
|
||||
#### CreateEntityInstance_WithAttachment (0x6707c0) - M2 Path
|
||||
```c
|
||||
int * __fastcall CreateEntityInstance_WithAttachment(
|
||||
char *modelPath, float *pos, float facing, int flags, int updateNow, int p6, int p7) {
|
||||
@@ -1242,18 +1242,18 @@ int * __fastcall CreateEntityInstance_WithAttachment(
|
||||
}
|
||||
```
|
||||
|
||||
#### hashTableTeardownLoop (0x69f740) — The Crash Site
|
||||
#### hashTableTeardownLoop (0x69f740) - The Crash Site
|
||||
```c
|
||||
void hashTableTeardownLoop(void) {
|
||||
if ((DAT_00ca7cf0 & 1) == 0) {
|
||||
DAT_00ca7cf0 |= 1;
|
||||
_DAT_00ca7dc0 = &PTR_DestroyMapDoodadDefinition_0081089c;
|
||||
// Walk global list — crashes here if entries point to freed heap
|
||||
// Walk global list - crashes here if entries point to freed heap
|
||||
while (PTR_00ca7dcc is valid) {
|
||||
piVar2 = ValidateLinkedList(&PTR_00ca7dc4, PTR_00ca7dcc);
|
||||
CalculateDistance3D(piVar2); // reads from freed entity memory
|
||||
}
|
||||
// Walk each hash bucket — also crashes
|
||||
// Walk each hash bucket - also crashes
|
||||
for each bucket in PTR_00ca7ddc {
|
||||
while (bucket entry is valid) {
|
||||
piVar2 = ValidateLinkedList(bucket, entry);
|
||||
@@ -1288,32 +1288,32 @@ void ClearWorldObjectsAndResetState(void) {
|
||||
|
||||
**Every native M2 entity created via `CreateEntityInstance_WithAttachment` is explicitly cleaned up
|
||||
by a parent.** The atexit handler (hashTableTeardownLoop) is a safety net for the hash table data
|
||||
structure — it should NEVER encounter live entities in normal operation.
|
||||
structure - it should NEVER encounter live entities in normal operation.
|
||||
|
||||
#### All 3 callers of CreateEntityInstance_WithAttachment:
|
||||
|
||||
1. **CastSpellByID_Extended (0x6e4b60)** — spell targeting reticle
|
||||
1. **CastSpellByID_Extended (0x6e4b60)** - spell targeting reticle
|
||||
- Stores entity in global `PTR_00ceca8c`
|
||||
- Cleaned up by `processCinematicExit` before every new spell cast
|
||||
- Also cleaned up by `executeSpellOrItem` (0x6e54f0)
|
||||
|
||||
2. **CreateGameObjectPathEffect (0x5f8030)** — game object visual effect
|
||||
2. **CreateGameObjectPathEffect (0x5f8030)** - game object visual effect
|
||||
- Called by `CreatePathObjectByUnitType` (0x5f4970)
|
||||
- Return value saved at parent+0x10 (despite Ghidra typing it as void)
|
||||
- Cleaned up by `DestroyPathObjectIfPresent` (0x5f4950) → `CleanupEntity_ProcessAttachments`
|
||||
- Parent is a spell effect object (SpellEffectWithPath class at ~0x5f4800)
|
||||
- `SpellEffectWithPathDestructor` (0x5f48d0) destroys parent during spell teardown
|
||||
|
||||
3. **Unknown caller (0x6e5a6e)** — likely in `executeSpellOrItem` (0x6e54f0)
|
||||
3. **Unknown caller (0x6e5a6e)** - likely in `executeSpellOrItem` (0x6e54f0)
|
||||
- Same pattern as #1
|
||||
|
||||
#### All 3 callers of CleanupEntity_ProcessAttachments (0x670d50):
|
||||
- `processCinematicExit` (0x6e4940) — spell cleanup
|
||||
- `executeSpellOrItem` (0x6e54f0) — spell/item cleanup
|
||||
- `DestroyPathObjectIfPresent` (0x5f4950) — path effect cleanup
|
||||
- `processCinematicExit` (0x6e4940) - spell cleanup
|
||||
- `executeSpellOrItem` (0x6e54f0) - spell/item cleanup
|
||||
- `DestroyPathObjectIfPresent` (0x5f4950) - path effect cleanup
|
||||
|
||||
#### CleanupVisualEffectAndRelease has only ONE caller:
|
||||
- `cleanupGameObject` (0x6a67a0) — the M2/WDOODADDEF cleanup function
|
||||
- `cleanupGameObject` (0x6a67a0) - the M2/WDOODADDEF cleanup function
|
||||
|
||||
So the full cleanup chain is always:
|
||||
```
|
||||
@@ -1332,7 +1332,7 @@ Parent destroyed
|
||||
|------|--------|----------|-----------------|------|
|
||||
| PTR_00c96088 | 0xC | Terrain chunk wrappers → doodad parents | destroyPrimaryGameObject | various |
|
||||
| PTR_00c9e358 | via PTR_00c9e350 | WMO entity wrappers (from CreateEntityInstance_WithAttachment WMO path) | destroyWorldEnvironment | WMAPOBJDEF |
|
||||
| PTR_00c962bc | 0xC | **Empty in 1.12.1** — no insertion code found, only init+atexit | cleanupGameObject (conditional) | WDOODADDEF |
|
||||
| PTR_00c962bc | 0xC | **Empty in 1.12.1** - no insertion code found, only init+atexit | cleanupGameObject (conditional) | WDOODADDEF |
|
||||
| PTR_00ca8044 | via PTR_00ca803c | Map manager objects | CleanupAndReleaseMemoryBlock | PTR_00ca7e10 (4th heap) |
|
||||
| PTR_00c7b2dc | 0xC | WENTITY fade effect wrappers | CleanupObjectAttachments_FreeMemory → DestroyWorldObjectAndRelease | WENTITY |
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ end
|
||||
-- Delimiter is ":" (pipe "|" is WoW's escape char for color codes)
|
||||
--
|
||||
-- Messages:
|
||||
-- P:idx:x:y:z:area — live placement (everyone processes)
|
||||
-- C:idx — clear one marker
|
||||
-- CA — clear all markers
|
||||
-- SR — sync request (non-leader asking leader to send defs)
|
||||
-- LSR — leader sync request (leader asking anyone to send defs)
|
||||
-- SF:idx:x:y:z:area — sync fill (only processed by requester in sync mode)
|
||||
-- P:idx:x:y:z:area - live placement (everyone processes)
|
||||
-- C:idx - clear one marker
|
||||
-- CA - clear all markers
|
||||
-- SR - sync request (non-leader asking leader to send defs)
|
||||
-- LSR - leader sync request (leader asking anyone to send defs)
|
||||
-- SF:idx:x:y:z:area - sync fill (only processed by requester in sync mode)
|
||||
--
|
||||
-- Permission model: ALL permission checks are enforced DLL-side.
|
||||
-- WorldMarker/ClearWorldMarker: DLL checks local player is leader/assist.
|
||||
@@ -265,7 +265,7 @@ local function onAddonMessage(prefix, message, channel, sender)
|
||||
return
|
||||
end
|
||||
|
||||
-- P, C, CA — DLL checks sender permission via SetMarkerDef/ClearMarkerDef
|
||||
-- P, C, CA - DLL checks sender permission via SetMarkerDef/ClearMarkerDef
|
||||
if cmd == "P" then
|
||||
local idx, x, y, z, areaId = parseMarkerFields(parts)
|
||||
if idx then
|
||||
@@ -315,7 +315,7 @@ local function scheduleSyncRequest()
|
||||
end
|
||||
|
||||
-- =============================================================================
|
||||
-- Roster change tracking — retriggerable debounce timer
|
||||
-- Roster change tracking - retriggerable debounce timer
|
||||
-- Each roster event that increases group size adds 1s to the timer (starts at
|
||||
-- 5s, caps at 10s). When the timer expires, one broadcast fires. This
|
||||
-- guarantees delivery after the storm of events settles.
|
||||
|
||||
+48
-64
@@ -6,17 +6,17 @@
|
||||
//! Entities are respawned automatically when the player approaches within 200y.
|
||||
//!
|
||||
//! Lua API (globals):
|
||||
//! WorldMarker(index, x, y, z) — place marker at coordinates
|
||||
//! WorldMarker(index, "unit") — place marker at unit's position
|
||||
//! WorldMarker(index) — place marker at cursor terrain position
|
||||
//! ClearWorldMarker(index) — remove specific marker (1-5)
|
||||
//! ClearWorldMarker() — remove all markers
|
||||
//! CanSetWorldMarkers() — returns 1 if leader/assist, nil otherwise
|
||||
//! WorldMarker(index, x, y, z) - place marker at coordinates
|
||||
//! WorldMarker(index, "unit") - place marker at unit's position
|
||||
//! WorldMarker(index) - place marker at cursor terrain position
|
||||
//! ClearWorldMarker(index) - remove specific marker (1-5)
|
||||
//! ClearWorldMarker() - remove all markers
|
||||
//! CanSetWorldMarkers() - returns 1 if leader/assist, nil otherwise
|
||||
//!
|
||||
//! Lua API (WorldMarkers table — internal, used by addon):
|
||||
//! Lua API (WorldMarkers table - internal, used by addon):
|
||||
//! WorldMarkers.SetMarkerDef(i, x, y, z, area, sender)
|
||||
//! WorldMarkers.ClearMarkerDef([index,] sender)
|
||||
//! WorldMarkers.GetMarkerDef(index) — returns x, y, z, areaId or nil
|
||||
//! WorldMarkers.GetMarkerDef(index) - returns x, y, z, areaId or nil
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
@@ -33,6 +33,9 @@ extern "kernel32" fn GetLastError() callconv(WINAPI) u32;
|
||||
extern "kernel32" fn GetCurrentProcessId() callconv(WINAPI) u32;
|
||||
extern "kernel32" fn GetTickCount() callconv(WINAPI) u32;
|
||||
const ERROR_ALREADY_EXISTS: u32 = 183;
|
||||
const mod_mutex = @import("../mutex.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "worldmarkers";
|
||||
|
||||
var g_mutex: ?*anyopaque = null;
|
||||
var g_is_hook_owner: bool = false;
|
||||
@@ -77,7 +80,7 @@ pub const Vec3 = struct {
|
||||
// State
|
||||
// =============================================================================
|
||||
|
||||
/// Persistent marker definition — survives zone transitions.
|
||||
/// Persistent marker definition - survives zone transitions.
|
||||
const MarkerDef = struct {
|
||||
pos: Vec3,
|
||||
area_id: u32,
|
||||
@@ -88,7 +91,7 @@ const EMPTY_DEF: MarkerDef = .{ .pos = .{ .x = 0, .y = 0, .z = 0 }, .area_id = 0
|
||||
/// Marker definitions persist across zone transitions (NOT cleared in worldCleanupDetour).
|
||||
var marker_defs: [NUM_MARKERS]MarkerDef = .{EMPTY_DEF} ** NUM_MARKERS;
|
||||
|
||||
/// Transient entity state — cleared on zone transition / teardown.
|
||||
/// Transient entity state - cleared on zone transition / teardown.
|
||||
var marker_entities: [NUM_MARKERS]?*anyopaque = .{null} ** NUM_MARKERS;
|
||||
var marker_created_tick: [NUM_MARKERS]u32 = .{0} ** NUM_MARKERS;
|
||||
var hold_queued: [NUM_MARKERS]bool = .{false} ** NUM_MARKERS;
|
||||
@@ -115,7 +118,7 @@ const fc = std.builtin.CallingConvention{ .x86_fastcall = .{} };
|
||||
const sc = std.builtin.CallingConvention{ .x86_stdcall = .{} };
|
||||
|
||||
// =============================================================================
|
||||
// Permission check — leader or raid officer required
|
||||
// Permission check - leader or raid officer required
|
||||
// =============================================================================
|
||||
|
||||
/// Get local player GUID via GetPlayerGUID (0x468550).
|
||||
@@ -132,7 +135,7 @@ fn getPlayerGUID() u64 {
|
||||
}
|
||||
|
||||
/// Look up a player name from the name cache by GUID.
|
||||
/// Calls RetrieveNPCDataFromCache — __thiscall(ECX=cache), 6 stack params, RET 0x18.
|
||||
/// Calls RetrieveNPCDataFromCache - __thiscall(ECX=cache), 6 stack params, RET 0x18.
|
||||
fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 {
|
||||
if (guid_lo == 0 and guid_hi == 0) return null;
|
||||
var name_buf: [2]u32 = .{ 0, 0 };
|
||||
@@ -140,7 +143,9 @@ fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 {
|
||||
guid_lo,
|
||||
guid_hi,
|
||||
@intFromPtr(&name_buf),
|
||||
0, 0, 0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
};
|
||||
const result: u32 = asm volatile (
|
||||
\\ push 20(%[a])
|
||||
@@ -159,7 +164,7 @@ fn getNameFromGUID(guid_lo: u32, guid_hi: u32) ?[*:0]const u8 {
|
||||
}
|
||||
|
||||
/// Check if the local player has permission to place/clear markers.
|
||||
/// Uses direct memory reads — no Lua state required.
|
||||
/// Uses direct memory reads - no Lua state required.
|
||||
/// Requires: party leader, raid leader, or raid officer (assist).
|
||||
fn canSetMarkers() bool {
|
||||
const player_guid = getPlayerGUID();
|
||||
@@ -191,7 +196,7 @@ fn canSetMarkers() bool {
|
||||
}
|
||||
|
||||
/// Check if a named sender has permission (leader or raid officer).
|
||||
/// Used to authenticate incoming addon messages — sender name comes from
|
||||
/// Used to authenticate incoming addon messages - sender name comes from
|
||||
/// CHAT_MSG_ADDON arg4 (server-verified, can't be spoofed).
|
||||
fn senderHasPermission(sender: [*:0]const u8) bool {
|
||||
const sender_span = std.mem.span(sender);
|
||||
@@ -224,7 +229,6 @@ fn senderHasPermission(sender: [*:0]const u8) bool {
|
||||
return std.mem.eql(u8, std.mem.span(leader_name), sender_span);
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Position helpers
|
||||
// =============================================================================
|
||||
@@ -261,14 +265,14 @@ fn getCursorTerrainPosition() ?Vec3 {
|
||||
if (world_frame == 0 or world_frame < 0x10000) return null;
|
||||
|
||||
// Zero the intersection point before raycasting so we can detect "no hit"
|
||||
// (HitTestPoint returns 0 for both "terrain hit" and "no hit" in normal mode —
|
||||
// (HitTestPoint returns 0 for both "terrain hit" and "no hit" in normal mode -
|
||||
// WorldIntersectionTest returns gameStateFlags & 1, which is 0 outside AoE targeting.
|
||||
// On a real hit the coords are overwritten; on sky/no-hit they stay zeroed.)
|
||||
@as(*align(1) u32, @ptrFromInt(world_frame + o.WF_HIT_TERRAIN_X)).* = 0;
|
||||
@as(*align(1) u32, @ptrFromInt(world_frame + o.WF_HIT_TERRAIN_Y)).* = 0;
|
||||
@as(*align(1) u32, @ptrFromInt(world_frame + o.WF_HIT_TERRAIN_Z)).* = 0;
|
||||
|
||||
// UpdateHitTest — __fastcall(ECX=worldFrame)
|
||||
// UpdateHitTest - __fastcall(ECX=worldFrame)
|
||||
hook.fastcall(void, o.FN_UPDATE_HIT_TEST, world_frame, 0);
|
||||
|
||||
const x = hook.readMem(f32, world_frame + o.WF_HIT_TERRAIN_X);
|
||||
@@ -284,7 +288,7 @@ fn getCursorTerrainPosition() ?Vec3 {
|
||||
// Game function wrappers
|
||||
// =============================================================================
|
||||
|
||||
/// CreateEntityInstance_WithAttachment — __fastcall, RET 0x14.
|
||||
/// CreateEntityInstance_WithAttachment - __fastcall, RET 0x14.
|
||||
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{
|
||||
@@ -312,7 +316,7 @@ fn createEntityInstance(path: [*:0]const u8, pos: *[3]f32, facing: f32, flags: u
|
||||
return if (result != 0) @ptrFromInt(result) else null;
|
||||
}
|
||||
|
||||
/// CleanupEntity_ProcessAttachments — __fastcall(ECX=entity), no stack params.
|
||||
/// CleanupEntity_ProcessAttachments - __fastcall(ECX=entity), no stack params.
|
||||
fn cleanupEntity(obj: *anyopaque) void {
|
||||
asm volatile ("call *%[func]"
|
||||
:
|
||||
@@ -326,7 +330,7 @@ fn cleanupEntity(obj: *anyopaque) void {
|
||||
// =============================================================================
|
||||
|
||||
/// Play an animation on an entity's M2 model render context (entity+0x88).
|
||||
/// CM2Model__PlayBoneAnimation — __thiscall(ECX=model), RET 0x1c.
|
||||
/// CM2Model__PlayBoneAnimation - __thiscall(ECX=model), RET 0x1c.
|
||||
fn playAnimation(entity: *anyopaque, anim_id: u32, queue: bool) void {
|
||||
const entity_addr = @intFromPtr(entity);
|
||||
const model = hook.readMem(u32, entity_addr + 0x88);
|
||||
@@ -393,7 +397,7 @@ fn beginDespawn(entity: *anyopaque) void {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// All slots full — force-cleanup the oldest and reuse slot 0
|
||||
// All slots full - force-cleanup the oldest and reuse slot 0
|
||||
if (despawning[0]) |old| {
|
||||
cleanupEntity(old.entity);
|
||||
}
|
||||
@@ -410,7 +414,7 @@ fn beginDespawn(entity: *anyopaque) void {
|
||||
fn placeMarker(index: usize, pos: Vec3) bool {
|
||||
if (index >= NUM_MARKERS) return false;
|
||||
|
||||
// Clear existing entity in this slot (but not the def — we're about to overwrite it)
|
||||
// Clear existing entity in this slot (but not the def - we're about to overwrite it)
|
||||
clearEntity(index);
|
||||
|
||||
// Store persistent definition
|
||||
@@ -486,7 +490,7 @@ fn clearAllMarkers() void {
|
||||
pub fn luaWorldMarker(L: lua.State) callconv(.c) u32 {
|
||||
if (!canSetMarkers()) {
|
||||
con.print("[worldmarkers] WorldMarker: no permission\n");
|
||||
return 0; // nil — addon shows user message
|
||||
return 0; // nil - addon shows user message
|
||||
}
|
||||
|
||||
const nargs = lua.gettop(L);
|
||||
@@ -542,7 +546,7 @@ pub fn luaClearWorldMarker(L: lua.State) callconv(.c) u32 {
|
||||
const lua_type: *const fn (lua.State, i32) callconv(fc) i32 = @ptrFromInt(0x6F3400);
|
||||
|
||||
if (nargs == 0 or lua_type(L, 1) == 0) {
|
||||
// No args or nil — clear all
|
||||
// No args or nil - clear all
|
||||
clearAllMarkers();
|
||||
lua.pushnumber(L, 1.0);
|
||||
return 1;
|
||||
@@ -590,7 +594,7 @@ fn tickAnimations() void {
|
||||
hold_queued[i] = true;
|
||||
}
|
||||
|
||||
// Zombie detection — entity exists but refcount dropped to 1 (culled by game).
|
||||
// Zombie detection - entity exists but refcount dropped to 1 (culled by game).
|
||||
// Check every 3 seconds to avoid per-frame overhead.
|
||||
if (now -% last_zombie_tick >= ZOMBIE_CHECK_INTERVAL_MS) {
|
||||
last_zombie_tick = now;
|
||||
@@ -607,7 +611,7 @@ fn tickAnimations() void {
|
||||
}
|
||||
}
|
||||
|
||||
// Proximity respawn — check every ~1 second
|
||||
// Proximity respawn - check every ~1 second
|
||||
if (now -% last_respawn_tick >= RESPAWN_CHECK_INTERVAL_MS) {
|
||||
last_respawn_tick = now;
|
||||
|
||||
@@ -638,7 +642,7 @@ fn tickAnimations() void {
|
||||
|
||||
/// Lua: SetMarkerDef(index, x, y, z, areaId, senderName)
|
||||
/// Store a marker definition without immediately spawning. Used by the addon
|
||||
/// when receiving remote marker data — proximity respawn handles entity creation.
|
||||
/// when receiving remote marker data - proximity respawn handles entity creation.
|
||||
/// senderName is verified against the group roster for leader/officer permission.
|
||||
pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 {
|
||||
const nargs = lua.gettop(L);
|
||||
@@ -673,15 +677,15 @@ pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Lua: ClearMarkerDef(senderName) — clear all
|
||||
/// Lua: ClearMarkerDef(index, senderName) — clear one
|
||||
/// Lua: ClearMarkerDef(senderName) - clear all
|
||||
/// Lua: ClearMarkerDef(index, senderName) - clear one
|
||||
/// senderName is verified against the group roster for leader/officer permission.
|
||||
pub fn luaClearMarkerDef(L: lua.State) callconv(.c) u32 {
|
||||
const nargs = lua.gettop(L);
|
||||
if (nargs < 1) return 0;
|
||||
|
||||
if (lua.isstring(L, 1) and (nargs == 1 or !lua.isnumber(L, 1))) {
|
||||
// ClearMarkerDef(senderName) — clear all
|
||||
// ClearMarkerDef(senderName) - clear all
|
||||
const sender = lua.tostring(L, 1) orelse return 0;
|
||||
if (!senderHasPermission(sender)) {
|
||||
con.fmt("[worldmarkers] ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
|
||||
@@ -692,7 +696,7 @@ pub fn luaClearMarkerDef(L: lua.State) callconv(.c) u32 {
|
||||
}
|
||||
|
||||
if (nargs >= 2 and lua.isnumber(L, 1) and lua.isstring(L, 2)) {
|
||||
// ClearMarkerDef(index, senderName) — clear one
|
||||
// ClearMarkerDef(index, senderName) - clear one
|
||||
const sender = lua.tostring(L, 2) orelse return 0;
|
||||
if (!senderHasPermission(sender)) {
|
||||
con.fmt("[worldmarkers] ClearMarkerDef: sender '{s}' denied\n", .{std.mem.span(sender)});
|
||||
@@ -737,8 +741,6 @@ pub fn luaCanSetMarkers(L: lua.State) callconv(.c) u32 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// World teardown hook
|
||||
// =============================================================================
|
||||
@@ -746,7 +748,7 @@ pub fn luaCanSetMarkers(L: lua.State) callconv(.c) u32 {
|
||||
var world_cleanup_hook: hook.Detour(fn () callconv(sc) void) = .{};
|
||||
var world_update_hook: hook.Detour(fn (u32) callconv(fc) void) = .{};
|
||||
|
||||
/// OnWorldUpdate hook — per-frame tick while world is active.
|
||||
/// OnWorldUpdate hook - per-frame tick while world is active.
|
||||
/// Drives animation state (Hold queue after Stand, despawn cleanup).
|
||||
fn worldUpdateDetour(frame: u32) callconv(fc) void {
|
||||
tickAnimations();
|
||||
@@ -755,7 +757,7 @@ fn worldUpdateDetour(frame: u32) callconv(fc) void {
|
||||
|
||||
/// Pre-hook on CleanupWorldAndEntities (0x66fc40).
|
||||
/// Destroys all our entities via CleanupEntity_ProcessAttachments before the
|
||||
/// game's teardown runs — the same pattern every native caller uses (e.g.
|
||||
/// game's teardown runs - the same pattern every native caller uses (e.g.
|
||||
/// processCinematicExit, DestroyPathObjectIfPresent). This unlinks them from
|
||||
/// the WDOODADDEF hash table so the atexit handler never touches freed memory.
|
||||
fn worldCleanupDetour() callconv(sc) void {
|
||||
@@ -765,8 +767,8 @@ fn worldCleanupDetour() callconv(sc) void {
|
||||
}
|
||||
|
||||
/// Destroy all tracked entities (active markers + despawning).
|
||||
/// Does NOT clear marker_defs — definitions persist for respawn.
|
||||
/// Idempotent — safe to call multiple times.
|
||||
/// Does NOT clear marker_defs - definitions persist for respawn.
|
||||
/// Idempotent - safe to call multiple times.
|
||||
fn destroyAllEntities() void {
|
||||
var count: u32 = 0;
|
||||
for (&marker_entities, 0..) |*slot, i| {
|
||||
@@ -803,21 +805,10 @@ fn destroyAllEntities() void {
|
||||
pub fn installHooks() void {
|
||||
con.print("[worldmarkers] Module loaded\n");
|
||||
|
||||
var mutex_name_buf: [64]u8 = undefined;
|
||||
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\WeirdUtils_WorldMarkersHook_{d}", .{GetCurrentProcessId()}) catch return;
|
||||
mutex_name_buf[mutex_name.len] = 0;
|
||||
|
||||
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
|
||||
if (g_mutex == null) return;
|
||||
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
_ = CloseHandle(g_mutex.?);
|
||||
g_mutex = null;
|
||||
g_is_hook_owner = false;
|
||||
con.print("[worldmarkers] Another DLL owns world markers (mutex taken), skipping\n");
|
||||
return;
|
||||
}
|
||||
g_is_hook_owner = true;
|
||||
const result = mod_mutex.acquire(module_name);
|
||||
g_mutex = result.handle;
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
// Hook OnWorldUpdate for per-frame animation tick (runs every frame while world is active).
|
||||
if (world_update_hook.attach(o.FN_ON_WORLD_UPDATE, &worldUpdateDetour) != .ok) {
|
||||
@@ -827,7 +818,7 @@ pub fn installHooks() void {
|
||||
}
|
||||
|
||||
// Hook CleanupWorldAndEntities to destroy our entities before world teardown.
|
||||
// This fires on map change, logout, AND exit — before heaps are destroyed.
|
||||
// This fires on map change, logout, AND exit - before heaps are destroyed.
|
||||
if (world_cleanup_hook.attach(o.FN_CLEANUP_WORLD_AND_ENTITIES, &worldCleanupDetour) != .ok) {
|
||||
con.print("[worldmarkers] FAILED to hook CleanupWorldAndEntities!\n");
|
||||
} else {
|
||||
@@ -835,7 +826,7 @@ pub fn installHooks() void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from CGGameUI_Shutdown (logout/exit) — wipes marker definitions.
|
||||
/// Called from CGGameUI_Shutdown (logout/exit) - wipes marker definitions.
|
||||
/// Does NOT touch hooks or mutex. Entities are destroyed separately by
|
||||
/// worldCleanupDetour which fires after shutdown.
|
||||
pub fn onShutdown() void {
|
||||
@@ -845,19 +836,12 @@ pub fn onShutdown() void {
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
// destroyAllEntities is idempotent — if worldCleanupDetour already ran,
|
||||
// destroyAllEntities is idempotent - if worldCleanupDetour already ran,
|
||||
// all slots are null and this is a no-op.
|
||||
destroyAllEntities();
|
||||
world_update_hook.detach();
|
||||
world_cleanup_hook.detach();
|
||||
}
|
||||
|
||||
if (g_is_hook_owner) {
|
||||
if (g_mutex) |m| {
|
||||
_ = ReleaseMutex(m);
|
||||
_ = CloseHandle(m);
|
||||
g_mutex = null;
|
||||
}
|
||||
mod_mutex.release(&g_mutex);
|
||||
}
|
||||
g_is_hook_owner = false;
|
||||
}
|
||||
|
||||
+32
-32
@@ -20,7 +20,7 @@ pub const MOVEMENT_POS_Z: usize = 0x18;
|
||||
// Entity creation (high-level API)
|
||||
// =============================================================================
|
||||
|
||||
/// CreateEntityInstance_WithAttachment — __fastcall, RET 0x14 (5 stack params).
|
||||
/// 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*).
|
||||
@@ -37,7 +37,7 @@ pub const FN_CREATE_ENTITY_INSTANCE: usize = 0x006707c0;
|
||||
// World teardown (map unload / logout / exit)
|
||||
// =============================================================================
|
||||
|
||||
/// CleanupWorldAndEntities — void(), no params, __stdcall.
|
||||
/// CleanupWorldAndEntities - void(), no params, __stdcall.
|
||||
/// Top-level world teardown: calls CleanupEntityList_ProcessAll, then
|
||||
/// CleanupWorldAndReleaseResources (which iterates heaps and force-frees).
|
||||
/// Called from InitializeWorldScene (map change) and ShutdownClientSystems (exit).
|
||||
@@ -52,21 +52,21 @@ pub const FN_CLEANUP_WORLD_AND_ENTITIES: usize = 0x0066fc40;
|
||||
/// __fastcall returns void**
|
||||
pub const FN_ALLOCATE_WORLD_OBJECT: usize = 0x006a0930;
|
||||
|
||||
/// DestroyWorldObjectAndRelease(object) — __fastcall, ECX=obj, no stack params.
|
||||
/// DestroyWorldObjectAndRelease(object) - __fastcall, ECX=obj, no stack params.
|
||||
/// Unlinks from world object list (+0x10/+0x14), calls virtual destructor, frees heap.
|
||||
/// ONLY for objects on WENTITY heap (from AllocateAndInitializeWorldObject).
|
||||
pub const FN_DESTROY_WORLD_OBJECT: usize = 0x006a0a70;
|
||||
|
||||
/// CleanupEntity_ProcessAttachments(entity) — __fastcall, ECX=entity, no stack params.
|
||||
/// CleanupEntity_ProcessAttachments(entity) - __fastcall, ECX=entity, no stack params.
|
||||
/// High-level destructor counterpart to CreateEntityInstance_WithAttachment.
|
||||
/// Walks and frees attachment children, decrements refcount at +0x0E, then
|
||||
/// dispatches to type-specific destructor based on flags at +0x8:
|
||||
/// flag 0x8 (M2): destroyWorldEnvironment (0x6a6870) — scene graph removal + free
|
||||
/// flag 0x40 (WMO): cleanupGameObject (0x6a67a0) — render detach + spatial unlink + free
|
||||
/// flag 0x8 (M2): destroyWorldEnvironment (0x6a6870) - scene graph removal + free
|
||||
/// flag 0x40 (WMO): cleanupGameObject (0x6a67a0) - render detach + spatial unlink + free
|
||||
/// Only actually frees when refcount reaches 0.
|
||||
pub const FN_CLEANUP_ENTITY: usize = 0x00670d50;
|
||||
|
||||
/// DecrementReferenceCount(obj) — __fastcall, ECX=obj, no stack params.
|
||||
/// 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;
|
||||
|
||||
@@ -74,10 +74,10 @@ pub const FN_DECREMENT_REFCOUNT: usize = 0x007103a0;
|
||||
// Cursor terrain position
|
||||
// =============================================================================
|
||||
|
||||
/// WorldFrame global pointer — *(u32*)PTR = worldFrame object.
|
||||
/// WorldFrame global pointer - *(u32*)PTR = worldFrame object.
|
||||
pub const PTR_WORLD_FRAME: usize = 0x00B4B2BC;
|
||||
|
||||
/// UpdateHitTest — __fastcall(ECX=worldFrame), no stack params.
|
||||
/// UpdateHitTest - __fastcall(ECX=worldFrame), no stack params.
|
||||
/// Raycasts from camera through mouse cursor, stores result at worldFrame+0x350:
|
||||
/// +0x350: hit type (0=none, 1=terrain, 2=object)
|
||||
/// +0x360: terrain intersection X (f32)
|
||||
@@ -95,7 +95,7 @@ pub const WF_HIT_TERRAIN_Z: usize = 0x368;
|
||||
// Zone / area identification
|
||||
// =============================================================================
|
||||
|
||||
/// Current zone area ID — numeric, locale-safe zone identifier.
|
||||
/// Current zone area ID - numeric, locale-safe zone identifier.
|
||||
/// Updated by the game as the player moves between areas.
|
||||
pub const ZONE_AREA_ID: usize = 0x00B4E314;
|
||||
|
||||
@@ -103,7 +103,7 @@ pub const ZONE_AREA_ID: usize = 0x00B4E314;
|
||||
// Per-frame world update
|
||||
// =============================================================================
|
||||
|
||||
/// OnWorldUpdate — __fastcall(ECX=worldFrame), no stack params, void return.
|
||||
/// OnWorldUpdate - __fastcall(ECX=worldFrame), no stack params, void return.
|
||||
/// Called every frame while the world is active (in-game, not login screen).
|
||||
/// Part of CGWorldFrame update pipeline.
|
||||
pub const FN_ON_WORLD_UPDATE: usize = 0x00482EA0;
|
||||
@@ -123,7 +123,7 @@ pub const FN_CM2_CREATE_FOR_MODEL_OBJECT: usize = 0x00695100;
|
||||
// Animation
|
||||
// =============================================================================
|
||||
|
||||
/// CM2Model__PlayBoneAnimation — __thiscall(ECX=modelRenderCtx), RET 0x1c (7 stack params).
|
||||
/// CM2Model__PlayBoneAnimation - __thiscall(ECX=modelRenderCtx), RET 0x1c (7 stack params).
|
||||
/// (boneIndex, animId, sequenceIndex, animData*, speed, blendMode, queueAnimation)
|
||||
/// boneIndex: 0xFFFFFFFF = all bones
|
||||
/// animId: M2 animation ID (0=Stand, 158=Hold, 159=Decay for Raid_UI_FX)
|
||||
@@ -138,75 +138,75 @@ pub const FN_PLAY_BONE_ANIMATION: usize = 0x007121a0;
|
||||
// Transform and position
|
||||
// =============================================================================
|
||||
|
||||
/// UpdateObjectTransform_CalculateBounds — __fastcall, RET 0x0C
|
||||
/// 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;
|
||||
|
||||
// =============================================================================
|
||||
// File I/O (Storm) — for in-memory file serving
|
||||
// File I/O (Storm) - for in-memory file serving
|
||||
// =============================================================================
|
||||
|
||||
/// openFileWithOptions — __stdcall(4), RET 0x10, prologue=9
|
||||
/// openFileWithOptions - __stdcall(4), RET 0x10, prologue=9
|
||||
/// (archive_ptr, path, flags, handle_out) → type_code (0=fail, 1-4=success)
|
||||
pub const FN_OPEN_FILE_WITH_OPTIONS: usize = 0x006477c0;
|
||||
|
||||
/// GetFileSizeFromHandle — __stdcall(2), RET 0x08, prologue=6
|
||||
/// GetFileSizeFromHandle - __stdcall(2), RET 0x08, prologue=6
|
||||
/// (file_context, high_size_out) → size
|
||||
pub const FN_GET_FILE_SIZE: usize = 0x006487f0;
|
||||
|
||||
/// ReadFileFromMultipleSources — __stdcall(6), RET 0x18, prologue=6
|
||||
/// ReadFileFromMultipleSources - __stdcall(6), RET 0x18, prologue=6
|
||||
/// (context, buffer, size, bytes_read_out, async_ptr, param6) → bool
|
||||
/// async_ptr==NULL: synchronous read. Non-NULL: queues async operation.
|
||||
pub const FN_READ_FILE: usize = 0x00648460;
|
||||
|
||||
/// CleanupFileHandleResources — __stdcall(1), RET 0x04, prologue=7
|
||||
/// CleanupFileHandleResources - __stdcall(1), RET 0x04, prologue=7
|
||||
/// (file_context) → 1
|
||||
pub const FN_CLEANUP_FILE_HANDLE: usize = 0x00648730;
|
||||
|
||||
/// processAsyncFileOperation — __fastcall(ECX=request), plain RET, prologue=7
|
||||
/// processAsyncFileOperation - __fastcall(ECX=request), plain RET, prologue=7
|
||||
/// Request structure: +0x08=file_ctx, +0x0C=dest_buf, +0x10=read_size,
|
||||
/// +0x14=seek/event_struct (*(+0x14)+4 = event handle)
|
||||
pub const FN_PROCESS_ASYNC_FILE_OP: usize = 0x00647350;
|
||||
|
||||
/// initializeFileContext — __thiscall(ECX=ctx, type)
|
||||
/// initializeFileContext - __thiscall(ECX=ctx, type)
|
||||
/// Sets context type, initializes critical section at +0x24, zeroes fields.
|
||||
pub const FN_INIT_FILE_CONTEXT: usize = 0x00647290;
|
||||
|
||||
/// cleanupFileContext — __thiscall(ECX=ctx)
|
||||
/// cleanupFileContext - __thiscall(ECX=ctx)
|
||||
/// Destroys critical section, cleanup companion to initializeFileContext.
|
||||
pub const FN_CLEANUP_FILE_CONTEXT: usize = 0x006472d0;
|
||||
|
||||
/// FreeMemory (SMemFree) — __stdcall(3): (ptr, src_str, flags)
|
||||
/// FreeMemory (SMemFree) - __stdcall(3): (ptr, src_str, flags)
|
||||
pub const FN_FREE_MEMORY: usize = 0x00646430;
|
||||
|
||||
// =============================================================================
|
||||
// M2 model loading (async pipeline)
|
||||
// =============================================================================
|
||||
|
||||
/// loadModelFromFileAsync — __thiscall(ECX=model_obj), 2 stack params, RET 0x08
|
||||
/// loadModelFromFileAsync - __thiscall(ECX=model_obj), 2 stack params, RET 0x08
|
||||
/// (fileHandle: **ctx, shouldUseCallback: int) → 1
|
||||
/// Prologue: 55 8b ec 8b 55 0c 56 8b f1 — safe sizes: [6, 7, 9]
|
||||
/// Prologue: 55 8b ec 8b 55 0c 56 8b f1 - safe sizes: [6, 7, 9]
|
||||
/// Allocates async task to read file and call onModelLoadComplete when done.
|
||||
/// The async executor at 0x71d610 calls fileReadWithLock directly, bypassing
|
||||
/// our ReadFileFromMultipleSources hook — hence this hook fills the buffer
|
||||
/// our ReadFileFromMultipleSources hook - hence this hook fills the buffer
|
||||
/// synchronously for fake file contexts.
|
||||
pub const FN_LOAD_MODEL_ASYNC: usize = 0x0071d4e0;
|
||||
|
||||
/// processLoadedModelData — __fastcall(ECX=model), no stack params, plain RET
|
||||
/// processLoadedModelData - __fastcall(ECX=model), no stack params, plain RET
|
||||
/// Parses model header from buffer at model+0x130 (ptr) / model+0x134 (size),
|
||||
/// initializes model resources, sets bit 0 of model+8 when done.
|
||||
pub const FN_PROCESS_LOADED_MODEL_DATA: usize = 0x0071d640;
|
||||
|
||||
// =============================================================================
|
||||
// Permission check — leader / raid officer
|
||||
// Permission check - leader / raid officer
|
||||
// =============================================================================
|
||||
|
||||
/// Group leader GUID (64-bit): low u32 at +0, high u32 at +4.
|
||||
/// Valid for both party leader and raid leader.
|
||||
pub const LEADER_GUID: usize = 0x00bc75f8;
|
||||
|
||||
/// Raid roster — array of 40 pointers to roster entry structs.
|
||||
/// Raid roster - array of 40 pointers to roster entry structs.
|
||||
/// Entry layout: +0x00/+0x04 = GUID (u64), +0x08 = subgroup, +0x0C = rank.
|
||||
/// Rank: 0 = member, 1 = assistant, 2 = leader.
|
||||
pub const RAID_ROSTER_ARRAY: usize = 0x00b712a8;
|
||||
@@ -217,18 +217,18 @@ pub const RAID_MEMBER_COUNT: usize = 0x00b713e0;
|
||||
/// Offset within a roster entry to the rank field (i32).
|
||||
pub const ROSTER_ENTRY_RANK: usize = 0x0C;
|
||||
|
||||
/// Party member GUIDs — 4 slots, 8 bytes each (lo/hi u32 pairs).
|
||||
/// Party member GUIDs - 4 slots, 8 bytes each (lo/hi u32 pairs).
|
||||
/// Contains other party members (not local player). Stride = 8.
|
||||
pub const PARTY_MEMBER_GUIDS: usize = 0x00bc6f48;
|
||||
|
||||
/// GetPlayerGUID — __fastcall(), no params, returns EAX(low):EDX(high).
|
||||
/// GetPlayerGUID - __fastcall(), no params, returns EAX(low):EDX(high).
|
||||
pub const FN_GET_PLAYER_GUID: usize = 0x00468550;
|
||||
|
||||
/// RetrieveNPCDataFromCache — __thiscall(ECX=cache_obj), 6 stack params, RET 0x18.
|
||||
/// RetrieveNPCDataFromCache - __thiscall(ECX=cache_obj), 6 stack params, RET 0x18.
|
||||
/// (guid_low, guid_high, name_buf_ptr, 0, 0, 0) → char* name or NULL.
|
||||
/// Used by GetRaidRosterInfo to resolve player names from GUIDs.
|
||||
pub const FN_NAME_CACHE_LOOKUP: usize = 0x0055f080;
|
||||
|
||||
/// Name cache object — static instance at this address. Passed as ECX (this)
|
||||
/// Name cache object - static instance at this address. Passed as ECX (this)
|
||||
/// to RetrieveNPCDataFromCache.
|
||||
pub const NAME_CACHE_OBJ: usize = 0x00c0e228;
|
||||
|
||||
Reference in New Issue
Block a user