Publish source: Unlicense, public README, repo hygiene
The remote was previously a distribution-only point for pre-built DLLs. This opens the source. - LICENSE: Unlicense, with a GPL-3.0 carve-out for src/dpslog/WeirdDPSMate (a DPSMate fork that keeps its own license) - README.md replaces the stale internal one with the user-facing docs from DLL_README.md, swapping the 'Why No Source Code?' section for build and layout notes. DLL_README.md is dropped; one README now serves both. - RELEASING.md: drop the trim-the-README-per-release dance and the remote/WeirdUtils/ distribution clone, both obsolete now - gitignore agent/editor scratch, build caches, the vendored WSBT addon, and the WeirdThreat/uwu-logs checkouts (separate upstream repos) - Commit outstanding module work: superweirdo, clickthrough portal visuals, transform44 decompiles, worldmarkers demo presets, tools/
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
# SuperWoWhook + Timber UnitBuff Crash
|
||||
|
||||
## Bug
|
||||
|
||||
SuperWoWhook.dll crashes at DLL offset 0x1688 (`MOVZX ESI, word [ESI+EAX*2]`)
|
||||
when UnitBuff is called for another player who has no buff in the queried slot.
|
||||
Not a WeirdUtils or Timber bug -- root cause is a data error in SuperWoWhook's
|
||||
inline patching template.
|
||||
|
||||
## Root Cause
|
||||
|
||||
SuperWoWhook's `SuperWoW_BuildUnitBuffHook` (0x10002460) patches UnitBuff's
|
||||
return epilogues with CALL instructions that redirect to DLL formatter blocks.
|
||||
Each CALL site has a paired return-landing patch that rewrites the bytes after
|
||||
the CALL with proper register-restore code (POP EDI; POP ESI; POP EBX; ...).
|
||||
|
||||
Template entries 88-103 contain 8 CALL+landing pairs. All 7 first pairs follow
|
||||
the pattern `landing = CALL_addr + 5`:
|
||||
|
||||
| Pair | CALL addr | Landing | OK? |
|
||||
|------|------------|------------|----------------|
|
||||
| 0 | 0x519AF1 | 0x519AF6 | +5, correct |
|
||||
| 1 | 0x519B19 | 0x519B1E | +5, correct |
|
||||
| 2 | 0x519C48 | 0x519C4D | +5, correct |
|
||||
| 3 | 0x519C6D | 0x519C72 | +5, correct |
|
||||
| 4 | 0x519708 | 0x51970D | +5, correct |
|
||||
| 5 | 0x519729 | 0x51972E | +5, correct |
|
||||
| 6 | 0x51981D | 0x519822 | +5, correct |
|
||||
| **7**| **0x51983B** | **0x519830** | **-16, WRONG** |
|
||||
|
||||
Pair 7's landing is at 0x519830 (should be 0x519840). The template dword at
|
||||
`0x1001B240 + 103*4 = 0x1001B3DC` contains `0x00519830` instead of `0x00519840`.
|
||||
|
||||
### What happens
|
||||
|
||||
The "other player no buff" path (JE at 0x519803 taken -> 0x519829):
|
||||
|
||||
1. `lua_pushnil` called at 0x51982B, returns to 0x519830
|
||||
2. Code at 0x519830 was patched with `5F 5E 5B 8B` (meant for 0x519840)
|
||||
3. This creates: `POP EDI; POP ESI; POP EBX; MOV EBP,[EDX+0]` -- the `8B` from
|
||||
the patch combines with `6A 00` from the original code to form `MOV EBP,[EDX]`
|
||||
4. EBP is overwritten with whatever EDX points to (addon string data: "AltA")
|
||||
5. `lua_pushnumber` called at 0x519836 with shifted stack
|
||||
6. CALL at 0x51983B jumps to formatter `UnitBuff_ReturnFieldU16_3vals` (0x10001680)
|
||||
7. Formatter reads `[EBP-0x10]` with EBP = 0x616C7441 -> ACCESS_VIOLATION
|
||||
|
||||
Meanwhile, 0x519840 (the actual CALL return address) is never patched and has
|
||||
`00 00 5B 8B E5 5D C3` -- `ADD [EAX],AL` would crash even if the formatter survived.
|
||||
|
||||
### Why intermittent
|
||||
|
||||
The crash only triggers when:
|
||||
- Querying buffs on another player (not self) -- self uses Timber's TW path
|
||||
- That player has no buff in the queried slot -- triggers the "no buff" JE path
|
||||
- EDX happens to point to readable memory -- if [EDX] faults, different crash site
|
||||
|
||||
### Why Timber-specific reports
|
||||
|
||||
- On vanilla, the same bug exists but is less visible: the "other player" path is
|
||||
rarely exercised by addons compared to the "self" path
|
||||
- On Timber, the "self" path is redirected through TW code (bypassing SuperWoWhook),
|
||||
so addons calling UnitBuff("player", N) never hit SuperWoWhook's patches -- only
|
||||
UnitBuff("target", N) etc. can trigger it
|
||||
|
||||
## Fix
|
||||
|
||||
Single byte fix in SuperWoWhook.dll: change the dword at file offset corresponding
|
||||
to VA `0x1001B3DC` from `0x00519830` to `0x00519840`.
|
||||
|
||||
## Crash Signature
|
||||
|
||||
- ACCESS_VIOLATION at SuperWoWhook offset 0x1688
|
||||
- `MOVZX ESI, word [ESI + EAX*2]` with invalid address
|
||||
- EBP = ASCII text (e.g. 0x616C7441 = "AltA") -- corrupted by MOV EBP,[EDX]
|
||||
- Stack contains 0x00519840 and addon strings ("DBG:AceEvent20Frame")
|
||||
|
||||
## Ghidra Labels
|
||||
|
||||
SuperWoWhook.dll:
|
||||
- `SuperWoW_BuildUnitBuffHook` (0x10002460)
|
||||
- `UnitBuff_ReturnFieldU16_3vals` (0x10001680) -- crash site
|
||||
- Template at 0x1001B240 (197 dwords), replacement data at local_328
|
||||
|
||||
WoW.exe (Timber):
|
||||
- UnitBuff other-player path: 0x519780-0x519846
|
||||
- JE at 0x519803: "no buff found" branch to 0x519829
|
||||
- Two epilogues: 0x51981D (buff found, pair 6 OK), 0x51983B (no buff, pair 7 BROKEN)
|
||||
@@ -0,0 +1,285 @@
|
||||
# SuperWoWhook vs Timber: UnitBuff/UnitDebuff Patch Conflict
|
||||
|
||||
## Overview
|
||||
|
||||
Both Timber (modified WoW.exe) and SuperWoWhook.dll extend `Lua_UnitBuff` and
|
||||
`Lua_UnitDebuff` to return extra values (spell ID, dispel type). SuperWoWhook
|
||||
patches vanilla return sites that Timber has already moved or replaced, causing
|
||||
stack corruption and crashes.
|
||||
|
||||
## What Each System Does
|
||||
|
||||
### Vanilla WoW (unmodified)
|
||||
- `UnitBuff(unit, index)` returns 2 values: texture, count
|
||||
- `UnitDebuff(unit, index)` returns 3 values: texture, count, dispelType
|
||||
|
||||
### Timber (TW_ extensions at 0xD06xxx)
|
||||
- `UnitBuff` returns 3 values: texture, count, **spellID**
|
||||
- `UnitDebuff` returns 4 values: texture, count, dispelType, **spellID**
|
||||
- Implemented via JMP patches into TW_ code at 0xD06xxx
|
||||
|
||||
### SuperWoWhook (runtime code injection)
|
||||
- Extends both functions to add similar extra return values
|
||||
- Built for vanilla byte layout -- patches specific return epilogues
|
||||
- Uses WriteProcessMemory at runtime to overwrite code
|
||||
|
||||
## UnitBuff Conflict (0x519500)
|
||||
|
||||
### Vanilla return path (found match, has icon):
|
||||
```
|
||||
519703: CALL 0x6F3810 ; lua_pushnumber(count)
|
||||
519708: POP EDI ; epilogue
|
||||
519709: POP ESI ; <-- SuperWoWhook patches 4 bytes here
|
||||
51970A: MOV EAX, 0x2 ; (replaces return count + epilogue)
|
||||
51970F: POP EBX
|
||||
519710: MOV ESP, EBP
|
||||
519712: POP EBP
|
||||
519713: RET ; returns 2 values
|
||||
```
|
||||
|
||||
### Timber replaces this with:
|
||||
```
|
||||
519703: JMP 0x00D06585 ; -> TW_UnitBuff_PushDuration
|
||||
(0x519708-0x519713 is now dead code, never reached)
|
||||
519714: MOV ESI, [EBP-0x4] ; (nil icon path continues here)
|
||||
519717: MOV ECX, ESI
|
||||
...
|
||||
```
|
||||
|
||||
### What TW_UnitBuff_PushDuration does (0xD06585):
|
||||
```
|
||||
D06585: CALL 0x6F3810 ; lua_pushnumber(count) -- was on FPU stack
|
||||
D0658A: SUB ESP, 0x8
|
||||
D0658D: MOV ECX, ESI
|
||||
D0658F: MOV EAX, [EBP-0x24] ; load saved spell ID (stashed by TW_Lua_UnitBuff_Extended)
|
||||
D06594: MOV dword [EBP-0x18], EAX
|
||||
D06597: FILD dword [EBP-0x18] ; convert to float
|
||||
D0659A: FSTP qword [ESP]
|
||||
D0659D: CALL 0x6F3810 ; lua_pushnumber(spellID)
|
||||
D065A2: JMP 0xD066AF ; -> ReturnConstant3_b (return 3)
|
||||
```
|
||||
|
||||
**Conflict**: SuperWoWhook writes 4 bytes at 0x519709 -- dead code in Timber
|
||||
(JMP at 0x519703 bypasses it). This specific patch is **harmless** since it
|
||||
never executes.
|
||||
|
||||
## UnitDebuff Conflict (0x519860) -- THE CRASH
|
||||
|
||||
### Vanilla return path 1 (has dispel type string):
|
||||
```
|
||||
519AEC: CALL 0x6F3890 ; lua_pushstring(dispelType)
|
||||
519AF1: POP EDI ; epilogue
|
||||
519AF2: POP ESI ; <-- SuperWoWhook patches 4 bytes here
|
||||
519AF3: MOV EAX, 0x3 ; (replaces return count + epilogue)
|
||||
519AF8: POP EBX
|
||||
519AF9: MOV ESP, EBP
|
||||
519AFB: POP EBP
|
||||
519AFC: RET ; returns 3 values
|
||||
```
|
||||
|
||||
### Timber replaces this with:
|
||||
```
|
||||
519AEC: JMP 0x00D065B4 ; -> TW_UnitDebuff_PushDispelType
|
||||
(0x519AF1-0x519AFC is dead code, never reached by original path)
|
||||
519AFD: MOV ESI, [EBP-0xC] ; (nil path starts here -- DIFFERENT CODE)
|
||||
519B00: MOV ECX, ESI ; <-- This is where 0x519AF2+0x0E lands!
|
||||
519B02: CALL 0x6F37F0 ; lua_pushnil
|
||||
...
|
||||
```
|
||||
|
||||
**Conflict**: SuperWoWhook writes 4 bytes at **0x519AF2**. In vanilla this was
|
||||
`POP ESI; MOV EAX, 0x3` (the return-3 epilogue). In Timber this is dead code
|
||||
between the JMP at 0x519AEC and the nil path at 0x519AFD. SuperWoWhook writes
|
||||
a relative jump here. The bytes are technically dead on the Timber happy path,
|
||||
BUT if SuperWoWhook's other patches redirect execution INTO this dead zone,
|
||||
the corrupted bytes execute and crash.
|
||||
|
||||
### Vanilla return path 2 (nil dispel, fallback):
|
||||
```
|
||||
519B14: CALL 0x6F37F0 ; lua_pushnil
|
||||
519B19: POP EDI ; epilogue
|
||||
519B1A: POP ESI ; <-- SuperWoWhook patches 4 bytes here
|
||||
519B1B: MOV EAX, 0x3
|
||||
519B20: POP EBX
|
||||
519B21: MOV ESP, EBP
|
||||
519B23: POP EBP
|
||||
519B24: RET ; returns 3 values
|
||||
```
|
||||
|
||||
### Timber replaces this with:
|
||||
```
|
||||
519B14: JMP 0x00D065D4 ; -> TW_UnitDebuff_PushNilFallback
|
||||
(0x519B19-0x519B24 is dead code)
|
||||
519B25: LEA ECX, [EBP-0x20] ; (next function or unrelated code)
|
||||
519B28: CALL 0x496400
|
||||
...
|
||||
```
|
||||
|
||||
**Conflict**: SuperWoWhook writes 4 bytes at **0x519B1A**. In Timber this is
|
||||
dead code after the JMP at 0x519B14. Timber's disassembler doesn't even show
|
||||
instructions at 0x519B1A. SuperWoWhook writes corrupted jump bytes into this
|
||||
dead zone. Same risk as above.
|
||||
|
||||
### UnitDebuff return paths that are IDENTICAL (safe):
|
||||
```
|
||||
519C49: POP ESI ; <-- SuperWoWhook patches (SAME in both)
|
||||
519C4A: MOV EAX, 0x3
|
||||
|
||||
519C6E: POP ESI ; <-- SuperWoWhook patches (SAME in both)
|
||||
519C6F: MOV EAX, 0x3
|
||||
```
|
||||
These paths were NOT modified by Timber. SuperWoWhook's patches here are safe.
|
||||
|
||||
## Crash Mechanism
|
||||
|
||||
### Execution trace
|
||||
|
||||
The crash stack has return address `0x51983B` (UnitBuff epilogue: POP EDI, POP ESI,
|
||||
MOV EAX 2, ... RET). This is a UnitBuff return path that Timber did NOT modify --
|
||||
the "no match" or error path returning 2 values. From this path, execution entered
|
||||
SuperWoWhook's return-value formatter (offset 0x1688) via one of the 27 rel32 JMP
|
||||
patches.
|
||||
|
||||
The crash block at 0x10001688 reads `[EBP-0x10]` (aura data ptr) and `[EBP-0x8]`
|
||||
(aura index) from UnitBuff's stack frame. These are locals set up by UnitBuff's
|
||||
prologue. If EBP is valid, this works.
|
||||
|
||||
### The corruption
|
||||
|
||||
EBP = `0x616C7441` = ASCII "AltA". This is string data, not a stack address. The
|
||||
saved EBP was overwritten BEFORE SuperWoWhook's code runs -- SuperWoWhook's code
|
||||
is the victim, not the cause of the corruption.
|
||||
|
||||
The string "AltA" likely comes from `ChatFrameEditBox:SetAltArrowKeyMode(false)` --
|
||||
called by pfUI or shaguTweaks during an AceEvent OnUpdate handler. The stack also
|
||||
contains "DBG:AceEvent20Frame".
|
||||
|
||||
### Possible causes
|
||||
|
||||
1. **Lua addon buffer overflow**: a Lua addon (pfUI, shaguTweaks) called from an
|
||||
AceEvent OnUpdate handler overflows a fixed-size buffer in the C call chain,
|
||||
writing "AltArrowKeyMode" string data over the saved EBP on the stack. This
|
||||
happens on a re-entrant Lua call from within UnitBuff processing.
|
||||
|
||||
2. **SuperWoWhook code cave stack collision**: SuperWoWhook's code cave may use
|
||||
stack space that overlaps with Timber's larger UnitBuff frame (Timber adds
|
||||
`[EBP-0x24]` for spell ID). If SuperWoWhook's code cave uses the same stack
|
||||
offsets for different purposes, the frames collide.
|
||||
|
||||
3. **Re-entrant UnitBuff call**: UnitBuff -> SuperWoWhook -> fires Lua event
|
||||
(UNIT_CASTEVENT) -> addon handler calls UnitBuff again -> second invocation
|
||||
corrupts the first's stack frame.
|
||||
|
||||
### What SuperWoWhook patches are harmless on Timber
|
||||
|
||||
All 5 UnitBuff/UnitDebuff return-epilogue patches (0x519709, 0x519AF2, 0x519B1A,
|
||||
0x519C49, 0x519C6E) land on dead code or unmodified code. These patches alone
|
||||
don't cause the crash -- they redirect to SuperWoWhook's formatters which work
|
||||
fine IF EBP is valid.
|
||||
|
||||
### Confirmed: Stack Frame Size Differs
|
||||
|
||||
**Vanilla UnitBuff prologue:**
|
||||
```
|
||||
519503: SUB ESP, 0x20 ; 32 bytes of locals
|
||||
```
|
||||
|
||||
**Timber UnitBuff prologue:**
|
||||
```
|
||||
519503: SUB ESP, 0x24 ; 36 bytes of locals (+4 for [EBP-0x24] spell ID)
|
||||
```
|
||||
|
||||
Timber enlarged the stack frame by 4 bytes to store the spell ID at `[EBP-0x24]`.
|
||||
However, this alone does NOT explain the crash:
|
||||
|
||||
- The formatter blocks use EBP-relative addressing (`[EBP-0x10]`, `[EBP-0x8]`)
|
||||
which is unaffected by the frame size change
|
||||
- The formatters end with `RET`, bypassing the original epilogue entirely --
|
||||
they never do `POP EBP` so the shifted saved-register positions don't matter
|
||||
- If this were the sole cause, UnitBuff would crash on EVERY call, not intermittently
|
||||
|
||||
### Root Cause: Formatter Uses Wrong Locals on Other-Player Path
|
||||
|
||||
**Confirmed via Unicorn x86 emulation.**
|
||||
|
||||
SuperWoWhook's formatter at offset 0x1680 does:
|
||||
```
|
||||
MOV ESI, [EBP-0x10] ; assumes: unit data pointer
|
||||
MOV EAX, [EBP-0x8] ; assumes: aura slot index
|
||||
MOVZX ESI, word [ESI+EAX*2] ; reads u16 from aura array
|
||||
```
|
||||
|
||||
This is correct for the **local player** aura iteration path (0x51960F+),
|
||||
where `[EBP-0x10]` = unit object and `[EBP-0x8]` = aura iteration index.
|
||||
|
||||
But SuperWoWhook patches the **other player** return sites at 0x51981D and
|
||||
0x51983B with E9 JMPs to this same formatter. On the other-player path:
|
||||
|
||||
- `[EBP-0x10]` = unit object pointer (from ClntObjMgrObjectPtr)
|
||||
- `[EBP-0x8]` = NOT an aura index -- it's the Lua buff index from lua_tonumber
|
||||
|
||||
The formatter computes `unit_obj + lua_buff_index * 2` and reads a u16.
|
||||
This is NOT a valid aura array access. Depending on the unit pointer and
|
||||
buff index values:
|
||||
|
||||
- Usually: reads from valid heap -> returns garbage data (wrong but no crash)
|
||||
- Sometimes: reads from unmapped memory or guard page -> ACCESS_VIOLATION
|
||||
|
||||
The EBP="AltA" corruption in the crash reports is a RED HERRING. The real
|
||||
crash is the formatter reading `[ESI+EAX*2]` with ESI=unit_ptr and EAX=buff_index
|
||||
on a code path where those locals hold different data than expected. When
|
||||
the computed address (unit_ptr + buff_index*2) happens to land on unmapped
|
||||
memory, it crashes. When it lands on mapped memory, it silently returns
|
||||
wrong data.
|
||||
|
||||
### Emulation Evidence
|
||||
|
||||
Unicorn x86 emulation of Timber+SuperWoWhook patched UnitBuff:
|
||||
|
||||
**Test 1** (no-icon other-player path -> 0x51983B -> formatter):
|
||||
```
|
||||
STEP 11: SW PATCH (nil-icon other) EBP=0x0010effc
|
||||
STEP 12: *** SW FORMATTER *** EBP=0x0010effc ESI=0x00000000
|
||||
MEM ERR: [0x01000000] at EIP=0x10001688 (MOVZX ESI, [ESI+EAX*2])
|
||||
```
|
||||
|
||||
**Test 2** (has-icon other-player path -> 0x51981D -> formatter):
|
||||
```
|
||||
STEP 13: *** SW FORMATTER *** EBP=0x0010effc ESI=0x02000000
|
||||
MEM ERR: [0x01000000] at EIP=0x10001688 (MOVZX ESI, [ESI+EAX*2])
|
||||
```
|
||||
|
||||
Both crash at the same instruction. The formatter works on the local-player
|
||||
path (tested separately, returns correctly), but crashes on the other-player
|
||||
path because the locals at [EBP-0x10] and [EBP-0x8] mean different things.
|
||||
|
||||
### Why It's Intermittent
|
||||
|
||||
The crash only occurs when `unit_ptr + buff_index*2` points to unmapped
|
||||
memory. Most unit object pointers are in the heap (0x1xxxxxxx-0x3xxxxxxx
|
||||
range) and buff indices are small (0-31), so `unit_ptr + 0..62` usually
|
||||
lands in mapped heap. The crash happens when:
|
||||
- The unit was recently freed (dangling pointer)
|
||||
- The unit is at a high heap address where +index*2 crosses a page boundary
|
||||
- Memory pressure causes the page to be unmapped
|
||||
|
||||
### Fix
|
||||
|
||||
SuperWoWhook should use **different formatters** for the local-player and
|
||||
other-player return paths, since the stack frame locals differ between them.
|
||||
Or it should not patch the other-player return sites (0x51981D, 0x51983B)
|
||||
at all, since Timber doesn't extend those paths with spell ID anyway.
|
||||
|
||||
## Fix Options
|
||||
|
||||
1. **SuperWoWhook detects Timber**: check if 0x519703 is a JMP (byte 0xE9)
|
||||
before patching. If so, skip UnitBuff/UnitDebuff patches since Timber
|
||||
already provides spell ID.
|
||||
|
||||
2. **Timber provides a flag**: export a marker (global variable or named
|
||||
mutex) that SuperWoWhook checks before patching.
|
||||
|
||||
3. **Users disable SuperWoWhook**: since Timber already provides the extra
|
||||
return values, SuperWoWhook's UnitBuff extension is redundant on Timber.
|
||||
|
||||
Not a WeirdUtils issue. WeirdUtils is not loaded in either crash.
|
||||
@@ -196,7 +196,7 @@ pub fn installHooks() void {
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
log = logging.Logger.open(module_name, .both);
|
||||
log = logging.Logger.open(module_name, .console);
|
||||
_ = cotp_hook.attach(ADDR_CheckObjectTypePermissions, &checkObjTypeDetour);
|
||||
_ = wit_hook.attach(ADDR_WorldIntersectionTest, &worldIntersectDetour);
|
||||
// _ = portal_visual.install();
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
//! portal_visual -- grey out unusable player-summoned portals/rituals.
|
||||
//!
|
||||
//! Hooks the M2 render pipeline to desaturate GO types 18 (ritual) and 22
|
||||
//! (portal) whose creator is a player not in the local player's group.
|
||||
//!
|
||||
//! Three hooks:
|
||||
//! ManageRenderListNode (0x710B90) -- tags portal models when added to render list
|
||||
//! DrawBatchProjected (0x70CB30) -- sets rendering_portal flag around batch draw
|
||||
//! DrawIndexedPrimitive (D3D9 vtable[82]) -- swaps pixel shader to desaturate
|
||||
|
||||
const hook = @import("zhook");
|
||||
const wow = @import("../wow.zig");
|
||||
const offsets = @import("../offsets.zig");
|
||||
const portal_filter = @import("portal_filter.zig");
|
||||
const logging = @import("../logging.zig");
|
||||
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
const WINAPI = @import("std").builtin.CallingConvention.winapi;
|
||||
extern "kernel32" fn IsBadReadPtr(lp: u32, ucb: u32) callconv(WINAPI) i32;
|
||||
extern "kernel32" fn VirtualProtect(addr: *anyopaque, size: usize, new: u32, old: *u32) callconv(WINAPI) i32;
|
||||
|
||||
const MODEL_OWNER: usize = 0x28;
|
||||
const RENDER_CTX_MODEL: usize = 0x3310;
|
||||
|
||||
// Tagged portal model set -- direct-mapped cache
|
||||
const TAG_SIZE: u32 = 256;
|
||||
const TAG_MASK: u32 = TAG_SIZE - 1;
|
||||
var tagged_models: [TAG_SIZE]u32 = .{0} ** TAG_SIZE;
|
||||
|
||||
// Stores the GO object pointer alongside the model tag so we can access entity
|
||||
const TagEntry = struct { model: u32 = 0, owner: u32 = 0 };
|
||||
var tagged_entries: [TAG_SIZE]TagEntry = .{TagEntry{}} ** TAG_SIZE;
|
||||
|
||||
// Track entities we've already triggered fade on (one-shot test)
|
||||
var fade_triggered: [TAG_SIZE]u32 = .{0} ** TAG_SIZE;
|
||||
|
||||
// CreateFadeEffect: __thiscall(entity_ECX, fadeTime_f32_stack)
|
||||
const ENTITY_OFFSET: usize = 0x88;
|
||||
const CreateFadeEffectFn = fn (u32, f32) callconv(hook.cc.thiscall) void;
|
||||
const createFadeEffect: *const CreateFadeEffectFn = @ptrFromInt(0x672DF0);
|
||||
|
||||
// Deferred fade queue -- CreateFadeEffect is NOT safe to call during render list
|
||||
// traversal (ManageRenderListNode). Queue owner ptrs and process next frame.
|
||||
const FADE_QUEUE_SIZE: u32 = 16;
|
||||
var fade_queue: [FADE_QUEUE_SIZE]u32 = .{0} ** FADE_QUEUE_SIZE;
|
||||
var fade_queue_count: u32 = 0;
|
||||
|
||||
fn queueFade(owner: u32) void {
|
||||
if (fade_queue_count < FADE_QUEUE_SIZE) {
|
||||
fade_queue[fade_queue_count] = owner;
|
||||
fade_queue_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn processFadeQueue() void {
|
||||
var i: u32 = 0;
|
||||
while (i < fade_queue_count) : (i += 1) {
|
||||
triggerFade(fade_queue[i]);
|
||||
fade_queue[i] = 0;
|
||||
}
|
||||
fade_queue_count = 0;
|
||||
}
|
||||
|
||||
fn triggerFade(owner: u32) void {
|
||||
if (IsBadReadPtr(owner, 0x90) != 0) return;
|
||||
const entity = hook.readMem(u32, owner + ENTITY_OFFSET);
|
||||
if (entity == 0) return;
|
||||
if (IsBadReadPtr(entity, 0xC0) != 0) return;
|
||||
const scene_obj = hook.readMem(u32, entity + 0x88);
|
||||
if (scene_obj == 0) return;
|
||||
log.fmt("triggerFade: owner=0x{x} entity=0x{x} scene=0x{x}\n", .{ owner, entity, scene_obj });
|
||||
createFadeEffect(entity, 1.0);
|
||||
}
|
||||
|
||||
fn classifyModel(model: u32) void {
|
||||
const owner = hook.readMem(u32, model + MODEL_OWNER);
|
||||
if (owner == 0) return;
|
||||
if (IsBadReadPtr(owner, 0x20) != 0) return;
|
||||
const obj_type = hook.readMem(u32, owner + 0x14);
|
||||
if (obj_type != 5) {
|
||||
if (tagged_models[model & TAG_MASK] == model) {
|
||||
tagged_models[model & TAG_MASK] = 0;
|
||||
tagged_entries[model & TAG_MASK] = .{};
|
||||
}
|
||||
return;
|
||||
}
|
||||
const desc = wow.getDescriptor(owner);
|
||||
if (!wow.isValidPtr(desc)) return;
|
||||
const go_type = hook.readMem(u32, desc + offsets.DESC_GO_TYPE);
|
||||
if ((go_type == 18 or go_type == 22) and portal_filter.shouldFilter(desc)) {
|
||||
tagged_models[model & TAG_MASK] = model;
|
||||
tagged_entries[model & TAG_MASK] = .{ .model = model, .owner = owner };
|
||||
} else {
|
||||
if (tagged_models[model & TAG_MASK] == model) {
|
||||
tagged_models[model & TAG_MASK] = 0;
|
||||
tagged_entries[model & TAG_MASK] = .{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn isTagged(model: u32) bool {
|
||||
return tagged_models[model & TAG_MASK] == model;
|
||||
}
|
||||
|
||||
var rendering_portal: bool = false;
|
||||
var log_count: u32 = 0;
|
||||
|
||||
// =============================================================================
|
||||
// Hook: ManageRenderListNode (0x710B90)
|
||||
// =============================================================================
|
||||
|
||||
const ManageRenderFn = fn (u32, u32) callconv(hook.cc.thiscall) void;
|
||||
var manage_hook: hook.Detour(ManageRenderFn) = .{};
|
||||
|
||||
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(hook.cc.thiscall) void {
|
||||
if (model != 0 and add_to_list == 1) {
|
||||
classifyModel(model);
|
||||
if (isTagged(model)) {
|
||||
const entry = tagged_entries[model & TAG_MASK];
|
||||
// One-shot: trigger fade test on first detection
|
||||
if (entry.owner != 0 and fade_triggered[model & TAG_MASK] != model) {
|
||||
fade_triggered[model & TAG_MASK] = model;
|
||||
triggerFadeTest(entry.owner);
|
||||
}
|
||||
}
|
||||
} else if (model != 0) {
|
||||
if (tagged_models[model & TAG_MASK] == model) {
|
||||
tagged_models[model & TAG_MASK] = 0;
|
||||
tagged_entries[model & TAG_MASK] = .{};
|
||||
}
|
||||
}
|
||||
manage_hook.callOriginal(.{ model, add_to_list });
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Hook: DrawBatchProjected (0x70CB30)
|
||||
// =============================================================================
|
||||
|
||||
const DrawBatchFn = fn (u32) callconv(hook.cc.thiscall) void;
|
||||
var draw_batch_hook: hook.Detour(DrawBatchFn) = .{};
|
||||
|
||||
const MODEL_ALPHA: usize = 0x180; // written by fade system via SetMemoryPointer
|
||||
const DIM_ALPHA: u32 = @bitCast(@as(f32, 0.35));
|
||||
const FULL_ALPHA: u32 = @bitCast(@as(f32, 1.0));
|
||||
|
||||
fn drawBatchDetour(ctx: u32) callconv(hook.cc.thiscall) void {
|
||||
const model_ptr = if (wow.isValidPtr(ctx +% @as(u32, @intCast(RENDER_CTX_MODEL))))
|
||||
hook.readMem(u32, ctx + RENDER_CTX_MODEL)
|
||||
else
|
||||
0;
|
||||
|
||||
if (model_ptr != 0 and isTagged(model_ptr)) {
|
||||
// Write dim alpha to the model's opacity field before batch draws
|
||||
const saved = hook.readMem(u32, model_ptr + MODEL_ALPHA);
|
||||
const dest: *u32 = @ptrFromInt(model_ptr + MODEL_ALPHA);
|
||||
dest.* = DIM_ALPHA;
|
||||
|
||||
rendering_portal = true;
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
rendering_portal = false;
|
||||
|
||||
dest.* = saved;
|
||||
} else {
|
||||
draw_batch_hook.callOriginal(.{ctx});
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// D3D9 DIP hook + desaturation pixel shader
|
||||
// =============================================================================
|
||||
|
||||
inline fn vt(obj: *anyopaque) [*]usize {
|
||||
return @ptrFromInt(hook.readMem(u32, @intFromPtr(obj)));
|
||||
}
|
||||
|
||||
const VT_DIP: usize = 82;
|
||||
const VT_CreatePixelShader: usize = 106;
|
||||
const VT_SetPixelShader: usize = 107;
|
||||
const VT_GetPixelShader: usize = 108;
|
||||
const VT_SetPSConstantF: usize = 109;
|
||||
|
||||
var orig_dip: usize = 0;
|
||||
var d3d9_vtable: ?[*]usize = null;
|
||||
var desat_shader: ?*anyopaque = null;
|
||||
|
||||
// ps_2_0 desaturation shader: samples texture, converts to greyscale via luminance.
|
||||
// c0 = luminance weights (0.299, 0.587, 0.114, 0.0)
|
||||
//
|
||||
// ps_2_0
|
||||
// dcl t0.xy
|
||||
// dcl_2d s0
|
||||
// texld r0, t0, s0 ; sample texture
|
||||
// dp3 r1.x, r0, c0 ; grey = dot(rgb, luma)
|
||||
// mov r1.y, r1.x ; replicate
|
||||
// mov r1.z, r1.x
|
||||
// mov r1.w, r0.w ; preserve alpha
|
||||
// mov oC0, r1
|
||||
//
|
||||
// Assembled from the D3D shader token spec (ps_2_0 format):
|
||||
const desat_shader_bytecode = [_]u32{
|
||||
0xFFFF0200, // ps_2_0
|
||||
// dcl t0.xy
|
||||
0x0200001F, 0x80000000, 0xB0030000,
|
||||
// dcl_2d s0
|
||||
0x0200001F, 0x90000000, 0xA00F0800,
|
||||
// texld r0, t0, s0
|
||||
0x03000042, 0x800F0000, 0xB0E40000, 0xA0E40800,
|
||||
// dp3 r1.x, r0, c0
|
||||
0x03000008, 0x80010001, 0x80E40000, 0xA0E40000,
|
||||
// mov r1.y, r1.x
|
||||
0x02000001, 0x80020001, 0x80000001,
|
||||
// mov r1.z, r1.x
|
||||
0x02000001, 0x80040001, 0x80000001,
|
||||
// mov r1.w, r0.w
|
||||
0x02000001, 0x80080001, 0x80FF0000,
|
||||
// mov oC0, r1
|
||||
0x02000001, 0x800F0800, 0x80E40001,
|
||||
// end
|
||||
0x0000FFFF,
|
||||
};
|
||||
|
||||
const luma_weights = [4]f32{ 0.299, 0.587, 0.114, 0.0 };
|
||||
|
||||
fn createShader(device: *anyopaque) bool {
|
||||
const createFn: *const fn (*anyopaque, [*]const u32, **anyopaque) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_CreatePixelShader]);
|
||||
var shader: ?*anyopaque = null;
|
||||
const hr = createFn(device, &desat_shader_bytecode, @ptrCast(&shader));
|
||||
if (hr >= 0 and shader != null) {
|
||||
desat_shader = shader;
|
||||
return true;
|
||||
}
|
||||
log.fmt("CreatePixelShader failed: hr=0x{x}\n", .{@as(u32, @bitCast(hr))});
|
||||
return false;
|
||||
}
|
||||
|
||||
fn hkDIP(
|
||||
device: *anyopaque,
|
||||
prim_type: u32,
|
||||
base_vtx: i32,
|
||||
min_vtx: u32,
|
||||
num_verts: u32,
|
||||
start_idx: u32,
|
||||
prim_count: u32,
|
||||
) callconv(hook.cc.stdcall) i32 {
|
||||
const origFn: *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(orig_dip);
|
||||
|
||||
if (rendering_portal and desat_shader != null) {
|
||||
// Save current pixel shader
|
||||
var saved_ps: ?*anyopaque = null;
|
||||
const getFn: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_GetPixelShader]);
|
||||
_ = getFn(device, &saved_ps);
|
||||
|
||||
// Set desaturation shader + luminance weights
|
||||
const setFn: *const fn (*anyopaque, ?*anyopaque) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_SetPixelShader]);
|
||||
_ = setFn(device, desat_shader);
|
||||
|
||||
const setConstFn: *const fn (*anyopaque, u32, [*]const f32, u32) callconv(hook.cc.stdcall) i32 =
|
||||
@ptrFromInt(vt(device)[VT_SetPSConstantF]);
|
||||
_ = setConstFn(device, 0, &luma_weights, 1);
|
||||
|
||||
const result = origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
|
||||
|
||||
// Restore pixel shader
|
||||
_ = setFn(device, saved_ps);
|
||||
if (saved_ps) |ps| {
|
||||
const relFn: *const fn (*anyopaque) callconv(hook.cc.stdcall) u32 = @ptrFromInt(vt(ps)[2]);
|
||||
_ = relFn(ps);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
|
||||
}
|
||||
|
||||
fn patchVtableEntry(vtable_ptr: [*]usize, idx: usize, new_fn: usize, old_fn: *usize) bool {
|
||||
old_fn.* = vtable_ptr[idx];
|
||||
var old_prot: u32 = 0;
|
||||
const addr: *anyopaque = @ptrFromInt(@intFromPtr(&vtable_ptr[idx]));
|
||||
if (VirtualProtect(addr, @sizeOf(usize), 0x40, &old_prot) == 0) return false;
|
||||
vtable_ptr[idx] = new_fn;
|
||||
_ = VirtualProtect(addr, @sizeOf(usize), old_prot, &old_prot);
|
||||
return true;
|
||||
}
|
||||
|
||||
fn restoreVtableEntry(vtable_ptr: [*]usize, idx: usize, old_fn: usize) void {
|
||||
var old_prot: u32 = 0;
|
||||
const addr: *anyopaque = @ptrFromInt(@intFromPtr(&vtable_ptr[idx]));
|
||||
if (VirtualProtect(addr, @sizeOf(usize), 0x40, &old_prot) == 0) return;
|
||||
vtable_ptr[idx] = old_fn;
|
||||
_ = VirtualProtect(addr, @sizeOf(usize), old_prot, &old_prot);
|
||||
}
|
||||
|
||||
fn getD3D9VTable() ?[*]usize {
|
||||
const gx = hook.readMem(u32, offsets.GX_DEVICE_PTR);
|
||||
if (gx == 0) return null;
|
||||
const dev = hook.readMem(u32, gx + offsets.GX_DEVICE_D3D_OFFSET);
|
||||
if (dev == 0) return null;
|
||||
const vtable_addr = hook.readMem(u32, dev);
|
||||
if (vtable_addr == 0) return null;
|
||||
return @ptrFromInt(vtable_addr);
|
||||
}
|
||||
|
||||
var d3d9_initialized: bool = false;
|
||||
|
||||
fn initD3D9() void {
|
||||
// DIP shader hook disabled -- testing model+0x180 alpha approach
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / Remove
|
||||
// =============================================================================
|
||||
|
||||
pub fn install() bool {
|
||||
log = logging.Logger.open("portal_visual", .both);
|
||||
if (manage_hook.attach(0x710B90, &manageRenderDetour) != .ok) return false;
|
||||
if (draw_batch_hook.attach(0x70CB30, &drawBatchDetour) != .ok) {
|
||||
manage_hook.detach();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Deferred D3D9 init -- call from lateInit when device exists.
|
||||
pub fn lateInit() void {
|
||||
initD3D9();
|
||||
}
|
||||
|
||||
pub fn remove() void {
|
||||
if (d3d9_vtable) |vtbl| {
|
||||
if (orig_dip != 0) restoreVtableEntry(vtbl, VT_DIP, orig_dip);
|
||||
}
|
||||
draw_batch_hook.detach();
|
||||
manage_hook.detach();
|
||||
}
|
||||
Reference in New Issue
Block a user