From b12fc820ef6c463584f97f36e3cc1d08d0ef34ae Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Mon, 27 Jul 2026 21:47:55 -0700 Subject: [PATCH] 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/ --- .gitignore | 34 +- DLL_README.md | 355 ------ IDEAS.md | 31 +- LICENSE | 31 + README.md | 503 +++++--- RELEASE_NOTES.md | 108 -- RELEASING.md | 63 +- TODO.md | 15 + build.zig | 11 +- src/bench/main.zig | 42 +- src/clickthrough/SUPERWOW_CRASH.md | 87 ++ src/clickthrough/SUPERWOW_TIMBER_CONFLICT.md | 285 +++++ src/clickthrough/clickthrough.zig | 2 +- src/clickthrough/portal_visual.zig | 340 ++++++ src/dpslog/HANDOFF_CLEU_FILTERS.md | 154 +++ src/dpslog/RESEARCH.md | 102 ++ src/dpslog/WSBT/WSBT_CLEUAdapter.lua | 2 +- .../WeirdDPSMate/DPSMate_CLEUAdapter.lua | 105 +- .../WeirdDPSMate/DPSMate_DataBuilder.lua | 65 +- src/framecrash/RESEARCH.md | 40 + .../addon/WeirdUtils_LogSessions.toc | 1 - src/minimapicons/minimapicons.zig | 14 +- src/superweirdo/superweirdo.zig | 180 +++ src/transform44/PERF_ANALYSIS.md | 101 ++ src/transform44/bone_sse_baseline.zig | 2 +- .../decompiled/decomp_RTQ_pipeline.c | 167 +++ .../decompiled/decomp_RenderComplexGeometry.c | 166 +++ .../decomp_RenderTextToVertexBuffer.c | 1044 +++++++++++++++++ .../decomp_processLinkedListCollision.c | 255 ++++ .../decompiled/decomp_renderTextToBuffer.c | 659 +++++++++++ src/transmogfix/transmogfix.zig | 21 +- src/weirdperformance/bone_sse.zig | 2 +- src/weirdperformance/bone_sse64.zig | 2 +- src/weirdperformance/transform_capture.zig | 564 +++++++++ src/weirdperformance/weirdperformance.zig | 14 +- src/worldmarkers/MPQ_LOAD_ORDER.md | 42 + src/worldmarkers/demo/MarkerPresets.lua | 143 +++ src/worldmarkers/demo/MarkerPresets.toc | 7 + src/worldmarkers/worldmarkers.zig | 5 +- src/wow.zig | 8 + tools/png2blp.py | 188 +++ tools/stormlib.py | 330 ++++++ 42 files changed, 5563 insertions(+), 727 deletions(-) delete mode 100644 DLL_README.md create mode 100644 LICENSE create mode 100644 TODO.md create mode 100644 src/clickthrough/SUPERWOW_CRASH.md create mode 100644 src/clickthrough/SUPERWOW_TIMBER_CONFLICT.md create mode 100644 src/clickthrough/portal_visual.zig create mode 100644 src/dpslog/HANDOFF_CLEU_FILTERS.md create mode 100644 src/superweirdo/superweirdo.zig create mode 100644 src/transform44/PERF_ANALYSIS.md create mode 100644 src/transform44/decompiled/decomp_RTQ_pipeline.c create mode 100644 src/transform44/decompiled/decomp_RenderComplexGeometry.c create mode 100644 src/transform44/decompiled/decomp_RenderTextToVertexBuffer.c create mode 100644 src/transform44/decompiled/decomp_processLinkedListCollision.c create mode 100644 src/transform44/decompiled/decomp_renderTextToBuffer.c create mode 100644 src/weirdperformance/transform_capture.zig create mode 100644 src/worldmarkers/MPQ_LOAD_ORDER.md create mode 100644 src/worldmarkers/demo/MarkerPresets.lua create mode 100644 src/worldmarkers/demo/MarkerPresets.toc create mode 100644 tools/png2blp.py create mode 100644 tools/stormlib.py diff --git a/.gitignore b/.gitignore index f78a757..7d1fe9a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,39 @@ ideas/ wiki/ src/**/assets_backup/ .kangentic/ -.claude/settings.local.json +.claude/ kangentic.local.json remote/ + +# agent/editor scratch +.memsearch/ +.sisyphus/ +.vscode/ +AGENTS.md +src/**/AGENTS.md +CLAUDE.md + +# build artifacts +logsession.zip +src/transform44/cycles.out + +# vendored third-party addon, not ours to relicense +src/dpslog/WSBT/* +!src/dpslog/WSBT/WSBT_CLEUAdapter.lua + +# separate upstream repos, checked out here for reference only +# WeirdThreat -> github.com/MarcelineVQ/TWThreat +# uwu-logs -> github.com/Ridepad/uwu-logs +src/dpslog/WeirdThreat/ +src/dpslog/uwu-logs/ + +# internal planning notes +SHARING_AUDIT.md + +__pycache__/ +*.pyc + +# fetched at runtime by tools/stormlib.py +tools/libstorm.so +tools/lib/ diff --git a/DLL_README.md b/DLL_README.md deleted file mode 100644 index 0439320..0000000 --- a/DLL_README.md +++ /dev/null @@ -1,355 +0,0 @@ -# WeirdUtils - -This package provides many pre-built DLLs for enhancing the vanilla 1.12 client WoW gameplay experience, aimed in particular at ease of use and accessibility but also bug fixes. - -You may get all features by installing `weirdutils.dll`, or choose any selection of features via individual DLLs. -On Turtle WoW, place your chosen DLLs next to your `WoW.exe` and add them to your `dlls.txt`. For other versions you will need some sort of DLL loader. - ---- - -## Features - -### World Markers - -Place up to 5 animated colored markers (Cataclysm style) at any position in the world, useful for raid positioning, pull planning, or route marking. Requires party/raid leader or raid assist. - -- `/worldmarker 1` through `/worldmarker 5` (or `/wm 1`) -- place a marker where your cursor is pointing -- `/worldmarker 1 target` -- place a marker on a unit (player, target, mouseover, etc.) -- `/clearworldmarker` (or `/cwm`) -- remove all markers -- `/clearworldmarker 2` -- remove a specific marker - -Keybindings for placing each marker and clearing all markers are available in the Key Bindings menu. - -Markers automatically sync with group members who also have WeirdUtils installed. When a leader/assist places or clears a marker, all group members see it. Markers persist across zone transitions and respawn when you return to the area. - -Lua API for addon developers: - -- `WorldMarker(index)` -- place marker at cursor (returns x,y,z,areaId on success, nil if no permission, -1 on failure) -- `WorldMarker(index, "unit")` -- place marker at a unit's position -- `WorldMarker(index, x, y, z)` -- place marker at world coordinates -- `ClearWorldMarker(index)` / `ClearWorldMarker()` -- remove one or all markers (returns 1 on success, nil if no permission) -- `GetWorldMarker(index)` -- returns x,y,z,areaId for an active marker, nil if empty -- `CanSetWorldMarker()` -- returns 1 if the local player is party/raid leader or raid assist, nil otherwise - -**DLL:** `worldmarkers.dll` - ---- - -### Outlines - -Renders glowing colored outlines around units, improving visibility in crowded encounters. - -- `/outlines` or `/ol` -- toggle outlines on or off - -A keybinding is available in the Key Bindings menu. - -**DLL:** `outline.dll` - ---- - -### Interact - -Smart interaction helpers for faster farming and dungeon runs: - -- **Interact Nearest** -- right-clicks the closest interactable NPC or object within 5 yards -- **Loot All Corpses** -- bulk loots all nearby corpses in sequence - -Best used via keybindings (available in the Key Bindings menu) or macros: -``` -/run InteractNearest(1) -/run LootAllCorpses() -``` - -**DLL:** `interact.dll` - ---- - -### PNG Screenshots - -Saves screenshots as compressed PNG files instead of the default uncompressed TGA format. Runs on a background thread with no frame drops. - -Controlled via the `screenshotQuality` CVar (saved to config.wtf): - -- `/script SetCVar("screenshotQuality", "6")` -- set compression level (1 = fast, 9 = smallest, default 6) -- `/script SetCVar("screenshotQuality", "0")` -- disable PNG, use original TGA format - -**DLL:** `pngscreenshots.dll` - ---- - -### Crash Fix - -Prevents a class of crashes caused by stale UI frame anchor pointers. No configuration needed, install and forget. - -**DLL:** `framecrash.dll` - ---- - -### Transmog Fix - -Eliminates FPS drops caused by rapid equipment visual updates when transmogged items lose durability. No configuration needed, install and forget. - -**DLL:** `transmogfix.dll` - ---- - -### Custom Data/ Assets - -Enables loading loose game asset files (models, textures, etc.) from the `Data/` directory without repacking MPQ archives. Place files in `Data/` mirroring the game's internal paths (e.g. `Data/Character/Troll/Female/TrollFemale.m2`) and they will be used instead of the MPQ version. - -Also allows multi-character patch archive names (e.g. `patch-12.mpq`, `patch-jimbo.mpq`). - -Patch archives are sorted case-insensitively by filename - last in the sort gets highest priority, and all patches override the base archives. - -No configuration needed, install and forget. - -**DLL:** `customassets.dll` - ---- - -### Utility Minimap Trackings - -Adds TBC/WotLK-style minimap tracking icons for NPC types, game objects, and quest givers. -Replaces the native tracking dropdown with a combined menu showing both spell tracking and NPC category tracking. -Can be disabled from the normal AddOn menu. Preferences saved per-character. - -- Click the minimap tracking icon to open the dropdown -- Check/uncheck NPC categories to toggle their minimap icons -- Spell tracking (Hunter tracking, Find Herbs, etc.) remains available alongside NPC tracking -- "Hide in Cities" toggle suppresses NPC icons in capital cities - -Tracks various npc types and useful objects like Oranges and Brainwasher and Mailbox. - -**DLL:** `minimapicons.dll` - ---- - -### Clickthrough - -Smart cursor targeting that prioritizes useful interactions. Instead of always selecting the nearest object under the cursor, the module finds the most useful target along the ray in priority order. - -- Lootable corpses first, then interactable game objects/portals, then interactable NPCs, then normal selection -- Dead non-lootable corpses can still be selected when nothing more useful is behind them -- Disabled inside battlegrounds to prevent targeting objectives through enemy players - -Can replace SuperWoW's `Clickthrough()` toggle with always-on smart targeting that doesn't require a manual toggle and preserves the ability to select dead bodies when needed. Disable corpse-clickthrough in SuperAPI if you want this. - -No configuration needed, install and forget. - -**DLL:** `clickthrough.dll` - ---- - -### Log Sessions - -Organizes the combat, raw combat, and chat logs into per-character directories with timestamped filenames: - -``` -Logs\\\WoWChatLog_YYYYMMDD_HHMMSS.txt -Logs\\\WoWCombatLog_YYYYMMDD_HHMMSS.txt -Logs\\\WoWRawCombatLog_YYYYMMDD_HHMMSS.txt (superwow only) -``` - -Every character login begins with a marker line (`COMBATLOG_SESSION` or `CHAT_SESSION`) identifying the character and realm. -If a log file for the same character was written to within the last 60 minutes, the same logfile will be used instead of creating a new one. - -Lua API for addon developers: - -- `GetCombatLogPath()` -- returns the current combat log file path -- `GetChatLogPath()` -- returns the current chat log file path - -No other configuration needed, install and forget. - -**DLL:** `logsessions.dll` - ---- - -### DPSLog (Combat Log Events) - -Provides WotLK 3.3.5-style `COMBAT_LOG_EVENT_UNFILTERED` for the vanilla client. Fires a single unified event with structured arguments instead of vanilla's fragmented localized text events. Enables modern DPS meter addons without expensive string parsing. - -37 subevents covering all combat interactions: damage (spell, melee, periodic, environmental, damage shield, damage split), healing (direct, periodic, overheal tracking), misses (all types), auras (applied, removed, refreshed, broken, dose changes), casts (start, success, failed, interrupted), power (energize, drain, leech), dispels, extra attacks, deaths, and kills. - -Each event includes source/destination GUIDs, names, unit flags, raid flags, spell info, and all WotLK-standard suffix fields. Booleans (critical, glancing, crushing) use WotLK semantics: `nil` for false, `"1"` for true. Names are `nil` when the client can't resolve the unit (despawned, out-of-range). Null GUIDs use `0x80000000` flags. - -When `/combatlog` is active, also writes structured WotLK-style CSV to `Logs\WeirdCombatLog.txt` (replaces vanilla's combat log file with parseable data). - -Also provides: - -- `CombatLogGetCurrentEventInfo()` -- WotLK-style lazy arg retrieval (call from event handler): - -```lua -local f = CreateFrame("Frame") -f:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") -f:SetScript("OnEvent", function() - local sub, srcGUID, srcName, srcFlags, srcRaidFlags, - dstGUID, dstName, dstFlags, dstRaidFlags = CombatLogGetCurrentEventInfo() - -- suffix args follow (spellId, spellName, etc.) -- see wiki for full layout -end) -``` - -- `GetSpellInfo(spellId)` -- the TBC/WotLK spell lookup API: - -```lua -local name, rank, icon, castTime, minRange, maxRange, spellId = GetSpellInfo(133) -``` - -- `UnitCastingInfo("unit")` -- TBC/WotLK cast bar query (works on any visible unit): - -```lua -local name, rank, text, icon, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo("target") -if name then - -- startTime/endTime are in milliseconds (compare with GetTime()*1000) -end -``` - -- `UnitChannelInfo("unit")` -- TBC/WotLK channel bar query: - -```lua -local name, rank, text, icon, startTime, endTime, isTradeSkill, notInterruptible = UnitChannelInfo("target") -``` - -See the [DPSLog wiki page](https://codeberg.org/gwenael/WeirdUtils/wiki/DPSLog) for full event reference and addon developer guide. - -**DLL:** `dpslog.dll` - ---- - -### SuperWoW Heal Text Fix - -Fixes duplicate floating heal numbers caused by SuperWoW 1.5. Only relevant if you use SuperWoW. No configuration needed, install and forget. - -**DLL:** `healtextfix.dll` - ---- - -### Big Cursor - -Upscales the hardware cursor for improved visibility without losing sharpness. Supports fractional scales from 1.0 (off) to 4.0. - -- `/script SetCursorScale(1.2)` -- set cursor scale (default 1.2x) -- `/script SetCursorScale(1)` -- disable (use original 32x32 cursor) - -This value is saved to the `cursorScale` CVar in tenths: `/script SetCVar("cursorScale", "15")` for 1.5x. - -Lua API for addon developers: - -- `SetCursorScale(n)` -- set scale factor (1.0-4.0), takes effect on next cursor change -- `GetCursorScale()` -- returns current scale factor - -**DLL:** `bigcursor.dll` - ---- - -### Performance - -Engine-level optimizations that reduce CPU time on math, rendering helpers, file lookups, and data decompression. - -- **SIMD Math** -- replaces 20+ internal math functions with SSE/AVX equivalents covering skeletal animation, particle rendering, frustum culling, collision detection, text glyph caching, and float-to-integer conversion -- **Data Decompression** -- swaps the game's 2004-era zlib with a modern library (2.2x faster). Loading screen times reduced by at least 13% -- **MPQ File Cache** -- caches archive file lookups so repeat file opens skip the archive chain walk. Saving 50-160ms every 15 seconds during heavy gameplay -- **Timer Calibration** -- recalibrates the OS performance counter for accurate animation timing. Ported from [VanillaFixes](https://github.com/hannesmann/vanillafixes) -- **Lua Runtime** -- custom slab allocator (O(1) free/realloc), incremental/generational GC (turns the ~5s stop-the-world freeze into ~9ms chunks), faster string interning (~40% on `luaS_newlstr`), and a literal prefilter on `string.find`/`gfind`/`gsub` that kills the O(n²) backtracking addon combat log parsers inflict on every chat message - -Most noticeable in cities, raids, during zone transitions, and in addon-heavy setups. - -**DLL:** `weirdperformance.dll` - ---- - -## Why No Source Code? - -This project is distributed as pre-built DLLs only. The source code is not and will not be made publicly available. - -These DLLs work by hooking deeply into the game client's internals: memory layout, function addresses, rendering pipeline, input handling, and more. -While every feature here is built for legitimate quality-of-life use, the underlying techniques touch on too many core mechanisms that are trivially abusable. -Publishing the source would be handing a candy store to bad actors: the same hooks and patterns used to render a raid marker or fix a crash can be repurposed for cheats, exploits, and in particular automation with minimal effort. - ---- - -## Developer Notes -### Runtime Module Control API - -WeirdUtils exports three functions for querying and disabling modules at runtime in case other devs find their dll's in conflict. - -#### Exported Functions - -| Function | Signature | Description | -|---|---|---| -| `WeirdUtils_IsModuleActive` | `int __cdecl (const char *name)` | Returns 1 if the module is compiled in and currently hooked, 0 otherwise | -| `WeirdUtils_DisableModule` | `int __cdecl (const char *name)` | Unhooks the named module. Returns 1 if found, 0 otherwise | -| `WeirdUtils_DisableAll` | `int __cdecl (void)` | Unhooks all modules and core hooks. Returns count of modules disabled | - -Module names are case-insensitive and match the released dll names: - -`customassets`, `framecrash`, `logsessions`, `transmogfix`, `minimapicons`, `healtextfix`, `bigcursor`, `worldmarkers`, `interact`, `outline`, `pngscreenshots`, `clickthrough`, `dpslog`, `weirdperformance` - -There is no re-enable API. - -#### C/C++ Header - -A header-only `include/weirdutils_api.h` is provided that handles DLL discovery and runtime resolution automatically. No .lib file needed: - -```c -#include "weirdutils_api.h" - -// Returns 0 if WeirdUtils isn't loaded - safe to call unconditionally -if (WeirdUtils_IsModuleActive("transmogfix")) - WeirdUtils_DisableModule("transmogfix"); -``` - -The header tries all known DLL names (`weirdutils.dll`, `worldmarkers.dll`, etc.) via `GetModuleHandleA`, so it works regardless of which DLL variant is loaded. - -#### Raw GetProcAddress - -If you prefer not to use the header: - -```c -HMODULE hMod = GetModuleHandleA("weirdutils.dll"); -if (hMod) { - typedef int (__cdecl *IsActiveFn)(const char *); - IsActiveFn isActive = (IsActiveFn)GetProcAddress(hMod, "WeirdUtils_IsModuleActive"); - if (isActive && isActive("transmogfix")) { - typedef int (__cdecl *DisableFn)(const char *); - DisableFn disable = (DisableFn)GetProcAddress(hMod, "WeirdUtils_DisableModule"); - if (disable) disable("transmogfix"); - } -} -``` - -### Version Query API - -WeirdUtils registers a Lua global table and query function for addon developers to detect which modules are loaded and their versions. Available from the login screen onward. - -#### `GetWeirdUtilsVersion()` - -Returns the `WeirdUtils` table containing all enabled modules and their version strings: - -```lua -local modules = GetWeirdUtilsVersion() -for name, version in pairs(modules) do - print(name .. " v" .. version) -- e.g. "dpslog v1.0" -end -``` - -#### `GetWeirdUtilsVersion("modulename")` - -Returns the version string for a specific module, or `nil` if not loaded: - -```lua -if GetWeirdUtilsVersion("dpslog") then - -- DPSLog is available, register for COMBAT_LOG_EVENT_UNFILTERED -end - -local ver = GetWeirdUtilsVersion("minimapicons") -- "1.0" or nil -``` - -The `WeirdUtils` table is additive -- if multiple independent DLLs are loaded (e.g. `dpslog.dll` and `minimapicons.dll` separately), each adds its own modules to the shared table. - ---- - -### Module Mutexes - -Each module also holds a named mutex while active: `Local\WeirdUtils__` (e.g. `Local\WeirdUtils_framecrash_12345`). The exception is transmogfix, which uses `Local\TransmogCoalesceHook_` for legacy reasons. - -If you see the mutex, the module is loaded - and can use the Runtime Module Control API to disable it. If you don't see it, the module isn't active and you're free to hook those functions yourself. diff --git a/IDEAS.md b/IDEAS.md index 063d9f0..fb746aa 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -25,8 +25,31 @@ Goals: - Prevent clicking GOs in states 2 and 3 (clickthrough module, GO filter pass) - Render states 2/3 in greyscale or desaturated to visually distinguish from usable ones -Click blocking fits in clickthrough's existing cascade filter (checkObjTypeDetour). -Greyscale rendering needs research into the client's GO model draw path -- may need -to intercept material/texture setup or set a per-object color tint before the draw call. +Click blocking: DONE (portal_filter.zig in clickthrough module). -Module: clickthrough (own file, e.g. portal_filter.zig) +Greyscale rendering research: +- `SetModelAlpha` (0x710da0): writes float to sceneObject+0x1C4 (__thiscall). Alpha only. +- `SetAlphaValue` (0x76ac50): writes float to entity+0xBC, calls through vtable+0x8C. +- `SetModelAlpha_2` (0x76d120): reads byte at entity+0xC8, propagates to entity+0x318. +- Fade system (0x672ef0): writes alpha to entity[0x22] (entity+0x88) via SetMemoryPointer. +- D3D9 render states: outline module (d3d9_hook.zig) has full pattern for per-object + D3D state manipulation via stencil buffer. +- Greyscale options: + 1. D3DRS_TEXTUREFACTOR + D3DTOP_MODULATE per draw call (set grey color before object draws) + 2. Pixel shader override (D3D9 SetPixelShader to a desaturation shader) + 3. Find an existing color tint field on the model instance (not found yet) +- `adjustColorSaturation` (0x74d794): client has a desaturation function built in. + Takes (outputRGBA*, inputRGBA*, saturationFactor). 0.0 = greyscale, 1.0 = full color. + Uses standard luminance weights at 0x818878/187c/1880. No xrefs found (may be unused). +- `GetModelDiffuseColor` / `SetModelDiffuseColor`: reads/writes sceneObject+0x184 (3 floats RGB). + `GetModelAmbientColor` / `SetModelAmbientColor`: sceneObject+0x190 (3 floats RGB). + `SetModelAlpha`: sceneObject+0x1C4 (1 float). These are data accessors, not code functions. +- M2 model rendering does NOT go through simple GxDevice SetTexture wrappers -- uses a + different submission path. Only RenderTextureQuads (UI) calls GxDevice::SetTexture. +- Viable approaches: + 1. Write grey diffuse color (e.g. 0.3, 0.3, 0.3) to sceneObject+0x184 per frame + for targeted GOs. Need to find the GO -> scene object pointer chain. + 2. Hook D3D9 DrawIndexedPrimitive (like outline module) and set D3DRS_TEXTUREFACTOR + to grey for targeted objects. Requires identifying which DIP calls belong to which GO. + 3. Set alpha to 0.5 via sceneObject+0x1C4 for a simpler "faded" visual instead of greyscale. +- Next: find the GO -> entity -> scene object pointer chain so we can access +0x184. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9738f23 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to + +--- + +EXCEPTION: src/dpslog/WeirdDPSMate/ is a fork of DPSMate by Shino + and is licensed under the GNU General Public License v3. +See src/dpslog/WeirdDPSMate/LICENSE. The dedication above does not +apply to that directory. diff --git a/README.md b/README.md index 348bc3e..4613da5 100644 --- a/README.md +++ b/README.md @@ -1,178 +1,391 @@ # WeirdUtils -All-in-one WoW 1.12.1 (build 5875) utility DLL. Injected as a 32-bit DLL into the -game process via Wine/DXVK on Linux. Provides screen-space outlines, screenshots, -interaction helpers, and an embedded addon with Lua API + keybindings. +This package provides many pre-built DLLs for enhancing the vanilla 1.12 client WoW gameplay experience, aimed in particular at ease of use and accessibility but also bug fixes. -## Current Features +You may get all features by installing `weirdutils.dll`, or choose any selection of features via individual DLLs. +On Turtle WoW, place your chosen DLLs next to your `WoW.exe` and add them to your `dlls.txt`. For other versions you will need some sort of DLL loader. -| Module | Description | +--- + +## Features + +### World Markers + +Place up to 5 animated colored markers (Cataclysm style) at any position in the world, useful for raid positioning, pull planning, or route marking. Requires party/raid leader or raid assist. + +- `/worldmarker 1` through `/worldmarker 5` (or `/wm 1`) -- place a marker where your cursor is pointing +- `/worldmarker 1 target` -- place a marker on a unit (player, target, mouseover, etc.) +- `/clearworldmarker` (or `/cwm`) -- remove all markers +- `/clearworldmarker 2` -- remove a specific marker + +Keybindings for placing each marker and clearing all markers are available in the Key Bindings menu. + +Markers automatically sync with group members who also have WeirdUtils installed. When a leader/assist places or clears a marker, all group members see it. Markers persist across zone transitions and respawn when you return to the area. + +Lua API for addon developers: + +- `WorldMarker(index)` -- place marker at cursor (returns x,y,z,areaId on success, nil if no permission, -1 on failure) +- `WorldMarker(index, "unit")` -- place marker at a unit's position +- `WorldMarker(index, x, y, z)` -- place marker at world coordinates +- `ClearWorldMarker(index)` / `ClearWorldMarker()` -- remove one or all markers (returns 1 on success, nil if no permission) +- `GetWorldMarker(index)` -- returns x,y,z,areaId for an active marker, nil if empty +- `CanSetWorldMarker()` -- returns 1 if the local player is party/raid leader or raid assist, nil otherwise + +**DLL:** `worldmarkers.dll` + +--- + +### Outlines + +Renders glowing colored outlines around units, improving visibility in crowded encounters. + +- `/outlines` or `/ol` -- toggle outlines on or off + +A keybinding is available in the Key Bindings menu. + +**DLL:** `outline.dll` + +--- + +### Interact + +Smart interaction helpers for faster farming and dungeon runs: + +- **Interact Nearest** -- right-clicks the closest interactable NPC or object within 5 yards +- **Loot All Corpses** -- bulk loots all nearby corpses in sequence + +Best used via keybindings (available in the Key Bindings menu) or macros: +``` +/run InteractNearest(1) +/run LootAllCorpses() +``` + +**DLL:** `interact.dll` + +--- + +### PNG Screenshots + +Saves screenshots as compressed PNG files instead of the default uncompressed TGA format. Runs on a background thread with no frame drops. + +Controlled via the `screenshotQuality` CVar (saved to config.wtf): + +- `/script SetCVar("screenshotQuality", "6")` -- set compression level (1 = fast, 9 = smallest, default 6) +- `/script SetCVar("screenshotQuality", "0")` -- disable PNG, use original TGA format + +**DLL:** `pngscreenshots.dll` + +--- + +### Crash Fix + +Prevents a class of crashes caused by stale UI frame anchor pointers. No configuration needed, install and forget. + +**DLL:** `framecrash.dll` + +--- + +### Transmog Fix + +Eliminates FPS drops caused by rapid equipment visual updates when transmogged items lose durability. No configuration needed, install and forget. + +**DLL:** `transmogfix.dll` + +--- + +### Custom Data/ Assets + +Enables loading loose game asset files (models, textures, etc.) from the `Data/` directory without repacking MPQ archives. Place files in `Data/` mirroring the game's internal paths (e.g. `Data/Character/Troll/Female/TrollFemale.m2`) and they will be used instead of the MPQ version. + +Also allows multi-character patch archive names (e.g. `patch-12.mpq`, `patch-jimbo.mpq`). + +Patch archives are sorted case-insensitively by filename - last in the sort gets highest priority, and all patches override the base archives. + +No configuration needed, install and forget. + +**DLL:** `customassets.dll` + +--- + +### Utility Minimap Trackings + +Adds TBC/WotLK-style minimap tracking icons for NPC types, game objects, and quest givers. +Replaces the native tracking dropdown with a combined menu showing both spell tracking and NPC category tracking. +Can be disabled from the normal AddOn menu. Preferences saved per-character. + +- Click the minimap tracking icon to open the dropdown +- Check/uncheck NPC categories to toggle their minimap icons +- Spell tracking (Hunter tracking, Find Herbs, etc.) remains available alongside NPC tracking +- "Hide in Cities" toggle suppresses NPC icons in capital cities + +Tracks various npc types and useful objects like Oranges and Brainwasher and Mailbox. + +**DLL:** `minimapicons.dll` + +--- + +### Clickthrough + +Smart cursor targeting that prioritizes useful interactions. Instead of always selecting the nearest object under the cursor, the module finds the most useful target along the ray in priority order. + +- Lootable corpses first, then interactable game objects/portals, then interactable NPCs, then normal selection +- Dead non-lootable corpses can still be selected when nothing more useful is behind them +- Disabled inside battlegrounds to prevent targeting objectives through enemy players + +Can replace SuperWoW's `Clickthrough()` toggle with always-on smart targeting that doesn't require a manual toggle and preserves the ability to select dead bodies when needed. Disable corpse-clickthrough in SuperAPI if you want this. + +No configuration needed, install and forget. + +**DLL:** `clickthrough.dll` + +--- + +### Log Sessions + +Organizes the combat, raw combat, and chat logs into per-character, per-day files: + +``` +Logs\\\WoWChatLog_YYYY_MM_DD.txt +Logs\\\WoWCombatLog_YYYY_MM_DD.txt +Logs\\\WoWRawCombatLog_YYYY_MM_DD.txt (superwow only) +``` + +Every character login begins with a marker line (`COMBATLOG_SESSION` or `CHAT_SESSION`) identifying the character and realm. +If today's log file for that character already exists, it is appended to instead of creating a new one, so a day of play stays in one file even across multiple logins or `/reload`s. + +Lua API for addon developers: + +- `GetCombatLogPath()` -- returns the current combat log file path +- `GetChatLogPath()` -- returns the current chat log file path + +No other configuration needed, install and forget. + +**DLL:** `logsessions.dll` + +--- + +### DPSLog (Combat Log Events) + +Provides WotLK 3.3.5-style `COMBAT_LOG_EVENT_UNFILTERED` for the vanilla client. Fires a single unified event with structured arguments instead of vanilla's fragmented localized text events. Enables modern DPS meter addons without expensive string parsing. + +37 subevents covering all combat interactions: damage (spell, melee, periodic, environmental, damage shield, damage split), healing (direct, periodic, overheal tracking), misses (all types), auras (applied, removed, refreshed, broken, dose changes), casts (start, success, failed, interrupted), power (energize, drain, leech), dispels, extra attacks, deaths, and kills. + +Each event includes source/destination GUIDs, names, unit flags, raid flags, spell info, and all WotLK-standard suffix fields. Booleans (critical, glancing, crushing) use WotLK semantics: `nil` for false, `"1"` for true. Names are `nil` when the client can't resolve the unit (despawned, out-of-range). Null GUIDs use `0x80000000` flags. + +When `/combatlog` is active, also writes structured WotLK-style CSV to `Logs\WeirdCombatLog.txt` (replaces vanilla's combat log file with parseable data). + +Also provides: + +- `CombatLogGetCurrentEventInfo()` -- WotLK-style lazy arg retrieval (call from event handler): + +```lua +local f = CreateFrame("Frame") +f:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") +f:SetScript("OnEvent", function() + local sub, srcGUID, srcName, srcFlags, srcRaidFlags, + dstGUID, dstName, dstFlags, dstRaidFlags = CombatLogGetCurrentEventInfo() + -- suffix args follow (spellId, spellName, etc.) -- see wiki for full layout +end) +``` + +- `GetSpellInfo(spellId)` -- the TBC/WotLK spell lookup API: + +```lua +local name, rank, icon, castTime, minRange, maxRange, spellId = GetSpellInfo(133) +``` + +- `UnitCastingInfo("unit")` -- TBC/WotLK cast bar query (works on any visible unit): + +```lua +local name, rank, text, icon, startTime, endTime, isTradeSkill, castID, notInterruptible = UnitCastingInfo("target") +if name then + -- startTime/endTime are in milliseconds (compare with GetTime()*1000) +end +``` + +- `UnitChannelInfo("unit")` -- TBC/WotLK channel bar query: + +```lua +local name, rank, text, icon, startTime, endTime, isTradeSkill, notInterruptible = UnitChannelInfo("target") +``` + +See the [DPSLog wiki page](https://codeberg.org/gwenael/WeirdUtils/wiki/DPSLog) for full event reference and addon developer guide. + +**DLL:** `dpslog.dll` + +--- + +### SuperWoW Heal Text Fix + +Fixes duplicate floating heal numbers caused by SuperWoW 1.5. Only relevant if you use SuperWoW. No configuration needed, install and forget. + +**DLL:** `healtextfix.dll` + +--- + +### Big Cursor + +Upscales the hardware cursor for improved visibility without losing sharpness. Supports fractional scales from 1.0 (off) to 4.0. + +- `/script SetCursorScale(1.2)` -- set cursor scale (default 1.2x) +- `/script SetCursorScale(1)` -- disable (use original 32x32 cursor) + +This value is saved to the `cursorScale` CVar in tenths: `/script SetCVar("cursorScale", "15")` for 1.5x. + +Lua API for addon developers: + +- `SetCursorScale(n)` -- set scale factor (1.0-4.0), takes effect on next cursor change +- `GetCursorScale()` -- returns current scale factor + +**DLL:** `bigcursor.dll` + +--- + +### Performance + +Engine-level optimizations that reduce CPU time on math, rendering helpers, file lookups, and data decompression. + +- **SIMD Math** -- replaces 20+ internal math functions with SSE/AVX equivalents covering skeletal animation, particle rendering, frustum culling, collision detection, text glyph caching, and float-to-integer conversion +- **Data Decompression** -- swaps the game's 2004-era zlib with a modern library (2.2x faster). Loading screen times reduced by at least 13% +- **MPQ File Cache** -- caches archive file lookups so repeat file opens skip the archive chain walk. Saving 50-160ms every 15 seconds during heavy gameplay +- **Timer Calibration** -- recalibrates the client's RDTSC against the OS high-resolution counter for accurate animation timing, and raises the OS timer resolution to 0.5ms. Ported from [VanillaFixes](https://github.com/hannesmann/vanillafixes) +- **Lua Runtime** -- custom slab allocator (O(1) free/realloc), incremental/generational GC (turns the ~1s stop-the-world freeze into ~9ms chunks), faster string interning (~40% on `luaS_newlstr`), and a literal prefilter on `string.find`/`gfind`/`gsub` that kills the O(n²) backtracking addon combat log parsers inflict on every chat message + +Most noticeable in cities, raids, during zone transitions, and in addon-heavy setups. + +**DLL:** `weirdperformance.dll` + +--- + +## Source Code + +This project is no longer actively developed. The full source is now published here, +in the public domain (see `LICENSE`), so it can be forked, salvaged, or learned from +rather than bit-rotting on a private disk. + +Earlier releases shipped as pre-built DLLs only. That is no longer the case - the +binaries on the releases page and the source in this repo are the same project. + +Fair warning to anyone building on this: these DLLs hook deeply into the client's +internals - memory layout, function addresses, rendering pipeline, input handling. +Much of it is specific to 1.12.1 build 5875 and will not survive a different client +build. Some modules (`interact`, `clickthrough`) intercept input and hit-testing; +what you do with them on someone else's server is between you and that server's admins. + +### Building + +Requires Zig 0.16 and a checkout of [zhook](https://codeberg.org/marcelinevq/zhook) +as a sibling directory - `build.zig.zon` refers to it by relative path: + +```sh +git clone https://codeberg.org/marcelinevq/zhook +git clone https://codeberg.org/MarcelineVQ/WeirdUtils +cd WeirdUtils +zig build # zig-out/bin/weirdutils.dll +zig build all-variants -Doptimize=ReleaseSmall # + one DLL per module +``` + +Target is `x86-windows-msvc` (32-bit DLL); it is developed on Linux with the game +running under Wine/DXVK. Per-module build flags are listed by `zig build --help`. + +### Layout + +| Path | What | |---|---| -| **Outline** | JFA-based screen-space outlines for targets, raid marks, dead players. See [src/outline/README.md](src/outline/README.md). | -| **Screenshot** | Hooks CTgaFile::Write for screenshot capture. | -| **Interact** | Nearest NPC/object interaction, bulk looting with queue processing. | -| **Markers** | World-space raid markers (5 colors) using M2 model entities. Proximity respawn, group sync, animated spawn/despawn. Lua API + slash commands (`/wm`, `/cwm`). | -| **Framecrash** | Anchor vtable guards - prevents crashes from dangling relativeTo pointers and NULL frame refs. | -| **DPSLog** | WotLK-style `COMBAT_LOG_EVENT` for vanilla 1.12.1. Fires structured events with full WotLK 3.3.5 CLEU parity: spell prefix (spellId, spellName, spellSchool), source/dest names, overkill, overheal, all damage/heal/miss/aura/cast subevents. Includes `GetSpellInfo(spellId)` Lua API. Embedded tracker addon. See [wiki/DPSLog.md](wiki/DPSLog.md). | -| **Minimap Icons** | Minimap icon fixes. | -| **Transmogfix** | Coalesces transmog durability update packets to prevent death frame drops. | -| **Data Assets** | Loose file loading, permissive MPQ glob patterns, pre-indexed file hash set. | -| **Healtextfix** | Heal text display fix. | -| **Embedded Addon** | Virtual addons loaded from DLL memory - .toc, .lua, .xml, .m2, .blp served via file I/O hooks (LoadFile + Storm layer). No on-disk addon folder needed. | -| **Lua Protection Bypass** | Stubs the Lua callback address validator to allow C function registration. | +| `src/main.zig` | DLL entry, module init, Lua API registration | +| `src//` | One directory per feature module; embedded addons live in `addon/` subdirs | +| `src/offsets.zig`, `src/wow.zig` | Client addresses and memory access wrappers | +| `build.zig` | Module list, versions, per-module build flags, variant DLL generation | +| `docs/`, `src/*/RESEARCH.md` | Reverse-engineering notes for the subsystems being hooked | -## Consolidation Plan +`src/dpslog/WeirdDPSMate/` is a fork of DPSMate and stays under GPL-3.0 - see its +own `LICENSE`. Everything else is unlicensed/public domain. -WeirdUtils replaces the standalone utility DLLs in the parent directory. All -development happens here - shared code, shared hooking infrastructure, one build -system. The standalone DLLs are being retired. +--- -| Standalone DLL | Purpose | Integration Status | +## Developer Notes +### Runtime Module Control API + +WeirdUtils exports three functions for querying and disabling modules at runtime in case other devs find their dll's in conflict. + +#### Exported Functions + +| Function | Signature | Description | |---|---|---| -| `../assetfix` | Loose file loading, permissive MPQ glob patterns, pre-indexed file hash set | Not started | -| `../transmogfix` | Death frame drop fix - coalesces transmog durability update packets | Not started | -| `../interact` | Nearest interact + bulk loot | Partially integrated | +| `WeirdUtils_IsModuleActive` | `int __cdecl (const char *name)` | Returns 1 if the module is compiled in and currently hooked, 0 otherwise | +| `WeirdUtils_DisableModule` | `int __cdecl (const char *name)` | Unhooks the named module. Returns 1 if found, 0 otherwise | +| `WeirdUtils_DisableAll` | `int __cdecl (void)` | Unhooks all modules and core hooks. Returns count of modules disabled | -### Compile-Time Feature Gating +Module names are case-insensitive and match the released dll names: -Each module is gated behind a build flag. The same codebase produces both the -all-in-one DLL and individual feature DLLs - just different compile flags. -Users can pick the full package or grab only the features they want. +`customassets`, `framecrash`, `logsessions`, `transmogfix`, `minimapicons`, `healtextfix`, `bigcursor`, `worldmarkers`, `interact`, `outline`, `pngscreenshots`, `clickthrough`, `dpslog`, `weirdperformance` -```zig -// build.zig options (planned) -const enable_customassets = b.option(bool, "customassets", "Enable loose file loading & permissive patch glob") orelse true; -const enable_transmogfix = b.option(bool, "transmogfix", "Enable transmog coalesce fix") orelse true; -const enable_interact = b.option(bool, "interact", "Enable interact helpers") orelse true; -const enable_outline = b.option(bool, "outline", "Enable outline rendering") orelse true; +There is no re-enable API. + +#### C/C++ Header + +A header-only `include/weirdutils_api.h` is provided that handles DLL discovery and runtime resolution automatically. No .lib file needed: + +```c +#include "weirdutils_api.h" + +// Returns 0 if WeirdUtils isn't loaded - safe to call unconditionally +if (WeirdUtils_IsModuleActive("transmogfix")) + WeirdUtils_DisableModule("transmogfix"); ``` -```sh -# Full build - all features in one DLL -zig build +The header tries all known DLL names (`weirdutils.dll`, `worldmarkers.dll`, etc.) via `GetModuleHandleA`, so it works regardless of which DLL variant is loaded. -# Single-feature builds - one DLL per feature for individual distribution -zig build -Dcustomassets=true -Dtransmogfix=false -Dinteract=false -Doutline=false -zig build -Dcustomassets=false -Dtransmogfix=true -Dinteract=false -Doutline=false -# etc. -``` +#### Raw GetProcAddress -Release artifacts: -- `weirdutils.dll` - everything -- `customassets.dll` - just asset/MPQ fixes -- `transmogfix.dll` - just transmog coalesce -- `interact.dll` - just interact/loot helpers -- `outline.dll` - just outline rendering +If you prefer not to use the header: -All built from this repo, all sharing the same hook library and codebase. - -### Per-Feature Named Mutex - -A user might load the full DLL alongside one of the smaller single-feature DLLs -(e.g. they use `weirdutils.dll` for everything but also have `customassets.dll` from -before they switched). Each feature module claims a **named mutex** on load - if -it's already held, that module skips hook installation. This way any combination -of DLLs coexists safely with no duplicate hooks. - -```zig -// Each module creates a process-specific named mutex on init -const mutex = CreateMutexA(null, 1, "Local\\WeirdUtils_CustomAssetsHook_{pid}"); -if (GetLastError() == ERROR_ALREADY_EXISTS) { - // Another DLL already owns this feature's hooks - skip - CloseHandle(mutex); - return; +```c +HMODULE hMod = GetModuleHandleA("weirdutils.dll"); +if (hMod) { + typedef int (__cdecl *IsActiveFn)(const char *); + IsActiveFn isActive = (IsActiveFn)GetProcAddress(hMod, "WeirdUtils_IsModuleActive"); + if (isActive && isActive("transmogfix")) { + typedef int (__cdecl *DisableFn)(const char *); + DisableFn disable = (DisableFn)GetProcAddress(hMod, "WeirdUtils_DisableModule"); + if (disable) disable("transmogfix"); + } } -// First to load wins - install hooks ``` -This is per-feature, not per-DLL. The full DLL claims one mutex per enabled -feature. A single-feature DLL claims one mutex. Whichever loads first owns the -hooks; the duplicate gracefully becomes a no-op. +### Version Query API -## Planned: Ground-Projected Markers +WeirdUtils registers a Lua global table and query function for addon developers to detect which modules are loaded and their versions. Available from the login screen onward. -World-space markers projected onto terrain, similar to raid markers but driven -programmatically. Use cases: +#### `GetWeirdUtilsVersion()` -- Visual range indicators (spell range circles, aggro radius) -- Waypoint markers for navigation -- Area-of-effect visualization -- Custom raid positioning markers +Returns the `WeirdUtils` table containing all enabled modules and their version strings: -Implementation will require: -- Projecting screen-space or world-space coordinates onto the terrain mesh -- Rendering textured quads or circles that conform to terrain height -- Integration with the D3D9 hook pipeline (rendered during EndScene or as - additional geometry injected into the scene) - -## Distribution - -This repo is private (source not published to avoid empowering bad actors). -Distribution uses a separate **public release repo** that contains only a -user-facing README and binary releases - no source code. - -- **This repo** (private): all source, development, docs -- **Public repo** (e.g. `WeirdUtils`): README with feature descriptions + - GitHub Releases with DLL downloads - -Release workflow: -```sh -# Build all variants from this repo -zig build # weirdutils.dll (full) -zig build -Doutline=true -Deverything-else=false # outline.dll -# ... etc for each single-feature build - -# Publish to the public repo -gh release create v1.0 --repo YourName/WeirdUtils \ - --title "v1.0" --notes "Release notes" \ - ./zig-out/lib/weirdutils.dll \ - ./builds/outline.dll \ - ./builds/customassets.dll +```lua +local modules = GetWeirdUtilsVersion() +for name, version in pairs(modules) do + print(name .. " v" .. version) -- e.g. "dpslog v1.0" +end ``` -## Project Structure +#### `GetWeirdUtilsVersion("modulename")` -``` -weirdutils/ - build.zig Build configuration - src/ - main.zig DLL entry, Lua API, file I/O hook, embedded addon - screenshot.zig Screenshot capture hook - interact.zig Interact + loot helpers - png.zig PNG encoding for screenshots - outline/ Outline subsystem (see src/outline/README.md) - api.zig Public API, Lua command handler - d3d9_hook.zig D3D9 vtable hooks, JFA pipeline, shaders - model_hook.zig M2 batch reordering, rendering_outline flag - tracker.zig Per-frame object/model tracking - types.zig D3D9 constants, outline colors, categories - offsets.zig WoW memory addresses and struct offsets - wow.zig Game memory access wrappers - addon/ Embedded addon files (.toc, .lua, .xml) - libs/ - hook/ Shared x86 inline hooking library (trampoline, fastcall thunks) - docs/ Design docs, research notes, shader analysis - reference/ C reference implementations +Returns the version string for a specific module, or `nil` if not loaded: + +```lua +if GetWeirdUtilsVersion("dpslog") then + -- DPSLog is available, register for COMBAT_LOG_EVENT_UNFILTERED +end + +local ver = GetWeirdUtilsVersion("minimapicons") -- "1.0" or nil ``` -## Build +The `WeirdUtils` table is additive -- if multiple independent DLLs are loaded (e.g. `dpslog.dll` and `minimapicons.dll` separately), each adds its own modules to the shared table. -```sh -cd /media/storage/projects/zig/weirdutils -zig build -``` +--- -Target: x86-windows-msvc (32-bit DLL), Zig 0.16 (patched: fastcall inreg fix). -Host: Linux (Arch), game runs via Wine/DXVK. +### Module Mutexes -## Hook Installation Order +Each module also holds a named mutex while active: `Local\WeirdUtils__` (e.g. `Local\WeirdUtils_framecrash_12345`). The exception is transmogfix, which uses `Local\TransmogCoalesceHook_` for legacy reasons. -Hooks are installed in a specific sequence to handle dependencies: - -1. **DLL_PROCESS_ATTACH** - Lua protection bypass, file I/O hook, LoadScriptFunctions, - LoadAddonsRecursively, interact hooks, GameEngine_MainInitialize, CGGameUI_Shutdown -2. **GameEngine_MainInitialize** (one-shot) - screenshot hook, outline model hooks -3. **First model hook callback** (deferred) - D3D9 vtable hooks (EndScene, DIP, Reset) - -D3D9 hooks are deferred because creating a dummy device during engine init -corrupts the d3d9 proxy's state. +If you see the mutex, the module is loaded - and can use the Runtime Module Control API to disable it. If you don't see it, the module isn't active and you're free to hook those functions yourself. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5e588e4..629e7c3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -36,111 +36,3 @@ Track notable changes here between releases. Clear this file when cutting a new minimap. Reads the per-player quest status cached on unit objects by the client's SMSG_QUESTGIVER_STATUS handler. Enabled by default in the tracking dropdown, togglable like other NPC tracking categories. - -## Enhancements - -- **Clickthrough**: Lootable corpses now click-through over players. Previously only - interactable NPCs (vendors, quest givers) and game objects would win over a blocking - player. Now dead units with the lootable flag are also prioritized, so you can loot - corpses through the player crowd without having to reposition. - -- **World Markers**: Disabled in battlegrounds via Map.dbc mapType check. Markers cannot - be placed while in a battleground instance. - -- **Clickthrough**: Reduced log noise -- logging changed from file to console-only. - Also ignores assets_backup directories in custom asset scanning. - -## Bug Fixes - -- **World Markers**: Fixed marker placement over game objects -- re-raycast now uses - terrain-only flags to find the ground position beneath a GO, instead of placing the - marker at the GO's collision point. - -- **DPSLog**: Fixed dynamic event slot search and environmental damage parameter ordering. - -- **Addon System**: Fixed addon files not loading -- switched from @hasField to @hasDecl - for compile-time module introspection. - -## Internal / Performance - -- **Transform44 Profiling** - render pipeline profiling module with 39 hotspot hooks, - A/B testing framework for comparing baseline vs optimized code paths, SSE replacements - for ClipPolygonToSinglePlane (4x), BuildTrianglePlanes (10x), rotateMatrixByAxisAngle - (4.3x), RayTriangleIntersection (1.3x), multiplyMatrix4x4 (4.3x). - -- **VanillaFixes Math Polyfill** - 17 UnitXP x87 FPU replacement hooks verified via - Ghidra prologue/epilogue disassembly and benchmarked against original WoW.exe bytes. - Hooks install in lateInit() to clobber UnitXP's hooks with correct calling conventions - (thiscall vs fastcall verified from assembly). A/B tested via bitmask toggle. - - Micro-benchmark results (x86 Linux harness, original x87 bytes mmap'd executable): - ``` - Winners (SSE faster): Neutral (~1.0x): Losers (x87 faster): - rotMat3x3: 2.1x (158->72) matMulVec3: 1.0x dotProduct: 0.4x (5->11) - rotMat4x4: 2.1x (162->76) multiply3x3: 1.0x evaluatePolynomial: 0.6x (11->16) - planeNormal: 1.7x (58->33) crossProduct: 1.0x squaredMagnitude: 0.7x (7->9) - transformAABox: 1.2x (89->69) applyTranslation: 0.9x vec3MulScalar: 0.8x - vecMulMat4: 1.1x scaleByVec: 0.9x vec3MulAssign: 0.8x - scaleByScalar: 1.1x (some runs) quatMulMat4: 0.8x - ``` - Losers are at function call overhead floor (original x87 is 5-11 cycles, close to - bare CALL/RET cost). Future direction: patch original bytes in-place at load time - to eliminate call overhead entirely. - - Also includes CriticalSection SpinCount=4000 optimization (from UnitXP) and - blit_hub memcpy fast paths for matching pixel formats. - -- **Math SSE Benchmark Harness** (`zig build bench` / `zig build run-bench`) - standalone - x86 Linux micro-benchmark that extracts original x87 function bytes from WoW.exe via - Ghidra, mmaps them executable, and profiles against our SSE replacements. Fresh data - each iteration to avoid overflow/denormal artifacts. Correctness validation included. - -- **Glyph Shadow Cache** - direct-mapped O(1) bypass for the game's 4-bucket hash table - in GetOrCreateCharacterGlyph. Reduces glyph lookup from ~3.65% frame time. - -- **MPQ File Cache (filecache)** - 2-way set-associative cache for File_FindInArchive - (0x6549a0). Skips the MPQ chain walk and per-archive hash table probe on repeat file - opens. Cache hits cost ~1000 cycles vs ~30000 cycles for full search. 60-90% hit rate - during gameplay, saving 6-160ms per 15s reporting period depending on scene load. - Validated via game's own FindAndIncrementResourceReference to handle archive lifecycle. - -- **Timer Fix (VanillaFixes port)** - TSC calibration, OS timer resolution (0.5ms via - NtSetTimerResolution), and Windows 11 power throttling disable. Ported from - hannesmann/vanillafixes. Primarily benefits native Windows; no measurable impact on - Wine/Linux but applied unconditionally. - -- **MPQ File Cache** - hooks File_FindInArchive (0x6549a0) to cache archive lookup - results. First open does full MPQ chain walk (~60K cycles), subsequent opens hit - direct-mapped cache with filename verification (~300 cycles). 80% hit rate in testing. - Moved from standalone file_perf module to transform44 sub-module (file_cache.zig). - -- **Logging Module** - centralized logging with auto-prefix, file and/or console routing. - All modules now route output through the shared Logger. - -- **Addon System Refactor** - module list now derived from build.zig, inactive addon - prefixes pruned at runtime. Fully data-driven. - -- **Shared Offsets** - consolidated shared game offsets and accessor functions into - offsets.zig and wow.zig, eliminating per-module duplication. - -- **Build System** - default optimize changed from Debug to ReleaseFast (works around - Zig fastcall inreg LLVM bug). Logging available in all modes except ReleaseSmall. - -## To Explore - -- **Driver env vars on load** - set environment variables like `RADV_TEX_ANISO=16` from - the DLL at load time, allowing driver-level anisotropic filtering while setting the - in-game option to off/low. Avoids the double-filtering performance hit of game AF - stacked on top of driver AF. Same approach could apply to other Mesa/RADV/DXVK knobs. - -## DLL_README Gaps - -Features documented in DLL_README but never included in a release: -- World Markers -- Outlines -- Interact -- Crash Fix (framecrash) - -Features implemented but not yet documented in DLL_README: -- Clickthrough lootable corpse priority -- World Markers battleground restriction diff --git a/RELEASING.md b/RELEASING.md index 482e832..61a7d7e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,15 +4,25 @@ ## How the remote repo works -This project is developed entirely locally. The remote repo is **only** a -distribution point for releases - no source code is pushed. +`main` on the remote holds the full source, the user-facing `README.md`, +`include/weirdutils_api.h`, and issue templates under `.gitea/`. Releases +attach pre-built DLLs to a tag on `main`. -The remote `main` branch contains `README.md` (built from the local -`DLL_README.md`), `weirdutils_api.h`, and issue templates under `.gitea/`. +This was previously a distribution-only remote - source lived locally and only +a trimmed README plus binaries were pushed. That is no longer the case; develop +against `main` and push directly. -A local clone of the remote repo lives at `remote/WeirdUtils/`. The wiki -lives at `remote/wiki/`. Use these for all remote operations - no tmp clones -needed. +The wiki clone lives at `remote/wiki/`. `remote/WeirdUtils/` is the old +distribution-only clone and is obsolete - it can be deleted. + +## 0. Sync tags + +`tea` creates tags on Codeberg, so fetch them before comparing against the +previous release: + +```sh +git fetch --tags +``` ## 1. Bump module versions @@ -63,28 +73,20 @@ Verify: ls -lh zig-out/bin/weirdutils.dll zig-out/variants/*.dll ``` -## 3. Update the remote README +## 3. Update the README -The remote README should match the features in this release. Start from -`DLL_README.md` and remove the sections for modules not being released - -keep the header, install instructions, and included feature sections exactly -as they are. Do NOT remove trailing spaces in `DLL_README.md` - they are -intentional Markdown line breaks. +`README.md` documents every module, not just the released subset - the source +is public, so there is nothing to trim. Add a section for any new module. -The module name list in the Developer Notes section must also only list -released module names. - -```sh -cd remote/WeirdUtils -# edit README.md: remove sections for modules not in this release -git add README.md -git commit -m "Update README for vX.Y.Z" -git push origin main -cd ../.. -``` +Do NOT remove trailing spaces in `README.md` - they are intentional Markdown +line breaks. ## 4. Write the release notes +Write the release notes to `/tmp/release-notes-vX.Y.Z.md` - NOT inside the +project directory. They are a transient artifact consumed by `tea --note-file` +and should not pollute the repo root or end up in git status. + Use this template - fill in the sections that apply, delete the rest. Use `-` (not em dash) anywhere a dash would be used. @@ -147,11 +149,12 @@ tea release assets delete --repo MarcelineVQ/WeirdUtils -y v0.4.0 minimapicons.d tea release assets create --repo MarcelineVQ/WeirdUtils v0.4.0 zig-out/variants/minimapicons.dll ``` -## 6. Hide source archives +## 6. Source archives -Codeberg attaches empty source tar/zip by default. Hide them via API: +Codeberg attaches source tar/zip to each release automatically. Now that the +repo contains the source, leave them visible - they are a useful artifact. -Get your token from the tea config: +To hide them anyway, get your token from the tea config: ```sh grep 'token:' ~/.config/tea/config.yml | head -1 | awk '{print $2}' @@ -185,10 +188,10 @@ print(r[0]['id']) if r else print('not found') ## Checklist +- [ ] Remote clone tags synced (`cd remote/WeirdUtils && git fetch --tags`) - [ ] Module versions bumped in `build.zig` for changed modules - [ ] Check `RELEASE_NOTES.md` for unreleased changes — move into release notes - [ ] Built with `ReleaseSmall` (both default and `all-variants`) -- [ ] Remote README updated — no unreleased module sections or names -- [ ] `weirdutils_api.h` trimmed to released DLL names only and uploaded +- [ ] `README.md` has a section for any new module +- [ ] `include/weirdutils_api.h` lists all module DLL names - [ ] Release created and DLLs uploaded via `tea` -- [ ] Source archives hidden diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..82de8d5 --- /dev/null +++ b/TODO.md @@ -0,0 +1,15 @@ +- [x] Setup Atelier #atelier + + Atelier helps you manage AI coding tasks, plans, and TODOs in a visual Kanban board. + + **Getting Started:** + - Click **Setup** to configure Atelier with recommended defaults + - Or click **Customize** to choose your own settings + - Drag items between columns to update their status + - Click an item to view and edit its content + [Full documentation](https://atelier.dev) + [Setup](vscode://Atelier.atelier/customize?agent=claude\&defaults=true) [Customize](vscode://Atelier.atelier/customize?agent=claude) +- [ ] [add scaling to minimapicons](/home/august/.atelier/projects/-media-storage-projects-zig-weirdutils/plans/minimap-icon-scaling.md) +- [ ] [Investigate the functions behind wow.exe lua string.find and find out how to optimize them as they seem to be extremely expensive for the work they do, at least when the game is ran via wine](/home/august/.atelier/projects/-media-storage-projects-zig-weirdutils/plans/lua-string-pattern-optimization.md) +- [ ] allow dead player-pets to be selectable in clikthrough +- [x] adjust our basic settings for atelier to use opus for work as the larger context window is useful diff --git a/build.zig b/build.zig index ffcbf74..21f45b3 100644 --- a/build.zig +++ b/build.zig @@ -66,6 +66,12 @@ pub fn build(b: *std.Build) void { const build_options = b.addOptions(); addModuleOptionsFromArray(b, build_options, &module_enabled); + // transform_capture: records game-x87 transformMatrix4x4 state to disk for + // offline bench parity. Mutually exclusive with bone_sse64 (capture needs + // the real game output; bone_sse64 replaces it). + const transform_capture_enabled = b.option(bool, "transform_capture", + "Record transformMatrix4x4 inputs/outputs to disk (disables bone_sse64)") orelse false; + build_options.addOption(bool, "transform_capture", transform_capture_enabled); const build_options_module = build_options.createModule(); const zhook_dep = b.dependency("zhook", .{ @@ -281,7 +287,10 @@ pub fn build(b: *std.Build) void { }); bench.root_module.addObject(bench_math_sse); bench.root_module.addObject(bench_silicon_sse); - bench.root_module.addObject(bench_bone_sse); + // bench_bone_sse64 imports bone_sse, so it provides both transformImpl_SSE + // and transformImpl_SSE64 symbols. Linking bench_bone_sse too would cause + // duplicate-symbol errors. + _ = bench_bone_sse; bench.root_module.addObject(bench_bone_sse64); bench.root_module.addObject(bench_bone_baseline); bench.root_module.addObject(bench_particle_sse); diff --git a/src/bench/main.zig b/src/bench/main.zig index d1b0c07..2e4615a 100644 --- a/src/bench/main.zig +++ b/src/bench/main.zig @@ -128,14 +128,25 @@ fn mapWowSections() bool { // Map additional pages for runtime constants that live outside .rdata: // 0x80C000-0x813000 covers 0x80C5C8 (billboard epsilon) and 0x811610 (SHORT_TO_FLOAT) // 0xCF0000-0xCF1000 covers 0xCF04C4 (boneKeyframe init flag) and 0xCF043C (pivot constants) + // 0x876000-0x877000 covers 0x876504 (matrix-multiply dispatch table — statically + // holds 0x74A7AD initializer; we write 0x74A7C6 directly to skip CPUID init) _ = mapZeroed(0x80C000, 0x8000); // covers 0x80C000-0x814000 _ = mapZeroed(0xCF0000, 0x1000); // covers 0xCF0000-0xCF1000 + _ = mapZeroed(0x876000, 0x1000); // covers 0x876504 dispatch table (real-game-bytes bench) // Write runtime constant values @as(*align(1) u32, @ptrFromInt(0x811610)).* = 0x38000100; // SHORT_TO_FLOAT ~1/32767 @as(*align(1) u32, @ptrFromInt(0x8029D4)).* = 0x34800000; // billboard epsilon @as(*align(1) u32, @ptrFromInt(0x80C5C8)).* = 0x35800000; // billboard sq epsilon @as(*align(1) u32, @ptrFromInt(0x80297C)).* = 0x40400000; // 3.0 @as(*align(1) u32, @ptrFromInt(0x802990)).* = 0x40C00000; // 6.0 + // NOTE: benching against the real game x87 function at 0x714260 requires + // initialising several dispatch tables in .data (0x876504, 0x876594, ...) + // that select x87 vs SSE math implementations based on CPUID. Running the + // game's init at 0x74FB49 pulls in heap/init dependencies we don't satisfy. + // For now, the bench uses the Zig reimpl baseline (transformImpl_BASELINE). + // Real-game-bytes parity is better done in-game via a hook diff. + @as(*align(1) u32, @ptrFromInt(0x876504)).* = 0x0074A7C6; + print(" [debug] *(0x876504) = 0x{x}\n", .{@as(*align(1) u32, @ptrFromInt(0x876504)).*}); sections_mapped = true; return true; } @@ -977,6 +988,8 @@ pub fn main() void { report("__ftol", t_best, s_best, mismatches == 0); } + } // close outer disabled block so transform44 runs standalone + // ========================================================================= // transform44: SSE implementation benchmark — comprehensive fixture // Exercises: bone loop (rot/trans/scale/static/billboard), texAnim, @@ -1663,7 +1676,11 @@ pub fn main() void { const sb: u32 = @bitCast(@as(f32, 1.0)); const transformImpl_SSE = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, .{ .name = "transformImpl_SSE" }); const transformImpl_SSE64 = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, .{ .name = "transformImpl_SSE64" }); - const transformImpl_BASELINE = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.c) void, .{ .name = "transformImpl_BASELINE" }); + // Baseline: Zig reimplementation (bit-identical to bone_sse by construction). + // Attempting to call the real game function at 0x714260 requires running + // game init code (CPUID-based dispatch population, possibly heap setup) + // that isn't feasible from this isolated bench fixture. See mapWowSections. + const transformImpl_BASELINE = @extern(*const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, .{ .name = "transformImpl_BASELINE" }); // Pre-set boneKeyframe init flag so we skip the atexit call (Windows CRT, can't run on Linux) @as(*u8, @ptrFromInt(0xCF04C4)).* = 1; @@ -1687,7 +1704,7 @@ pub fn main() void { // --- Benchmark both BASELINE and SSE --- const run_bench_fn = struct { - fn run(func: *const fn (u32, u32, u32, u32, u32) callconv(.c) void, so2: u32, pm: u32, pp: u32, po: u32, sb2: u32, scene: *[0x400]u8, actx: *[0x20]u8, iters: u32) u64 { + fn run(func: *const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, so2: u32, pm: u32, pp: u32, po: u32, sb2: u32, scene: *[0x400]u8, actx: *[0x20]u8, iters: u32) u64 { var best_inner: u64 = std.math.maxInt(u64); for (0..5) |_| { const t = rdtsc(); @@ -1714,6 +1731,9 @@ pub fn main() void { const pp = @intFromPtr(&pos); const po = @intFromPtr(&ofs); + const best_base = run_bench_fn(transformImpl_BASELINE, so, pm, pp, po, sb, &scene_obj, &anim_ctx_mem, T44_ITERS); + const avg_base = best_base / T44_ITERS; + const best_sse = run_bench_fn(transformImpl_SSE, so, pm, pp, po, sb, &scene_obj, &anim_ctx_mem, T44_ITERS); const avg_sse = best_sse / T44_ITERS; @@ -1731,7 +1751,7 @@ pub fn main() void { const best_sse64 = run_bench_fn(transformImpl_SSE64, so, pm, pp, po, sb, &scene_obj, &anim_ctx_mem, T44_ITERS); const avg_sse64 = best_sse64 / T44_ITERS; - print(" BASELINE: {d} cycles/call (frozen)\n", .{BASELINE_CYCLES}); + print(" BASELINE: {d} cycles/call (frozen ref {d})\n", .{ avg_base, BASELINE_CYCLES }); print(" SSE: {d} cycles/call", .{avg_sse}); if (avg_sse < BASELINE_CYCLES) { const pct = (BASELINE_CYCLES - avg_sse) * 100 / BASELINE_CYCLES; @@ -1782,7 +1802,7 @@ pub fn main() void { }; const reset_and_run = struct { - fn go(func: *const fn (u32, u32, u32, u32, u32) callconv(.c) void, so3: u32, pm3: u32, pp3: u32, po3: u32, sb3: u32, scene3: *[0x400]u8, actx3: *[0x20]u8, brt3: [*]u8, bc: usize) void { + fn go(func: *const fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void, so3: u32, pm3: u32, pp3: u32, po3: u32, sb3: u32, scene3: *[0x400]u8, actx3: *[0x20]u8, brt3: [*]u8, bc: usize) void { wu(u32, actx3[0x0C..0x10], 500, .little); wu(u32, scene3[0x40..0x44], 0, .little); // Re-init bone_rt anim_slot/sec_slot fields @@ -1849,14 +1869,14 @@ pub fn main() void { // calcColorValues_SSE -- disabled: no standalone SSE export yet // bench_calcColorValues(); - // si_frustumCullBBox -- fastcall(bbox_ECX, flags_EDX, radius_stack) -> u32 - bench_frustumCullBBox(); + if (false) { // disabled: pulls in silicon_sse exports not linked into bench + // si_frustumCullBBox -- fastcall(bbox_ECX, flags_EDX, radius_stack) -> u32 + bench_frustumCullBBox(); - // si_processLinkedListCollision -- fastcall(listHead_ECX, queryBox_EDX, resultBuf_stack, flags_stack) -> u32 - // Builds a fake linked list with 8 nodes to benchmark AABB overlap test. - bench_processLinkedListCollision(); - - } // end disabled block + // si_processLinkedListCollision -- fastcall(listHead_ECX, queryBox_EDX, resultBuf_stack, flags_stack) -> u32 + // Builds a fake linked list with 8 nodes to benchmark AABB overlap test. + bench_processLinkedListCollision(); + } print("\n", .{}); } diff --git a/src/clickthrough/SUPERWOW_CRASH.md b/src/clickthrough/SUPERWOW_CRASH.md new file mode 100644 index 0000000..8eea7c5 --- /dev/null +++ b/src/clickthrough/SUPERWOW_CRASH.md @@ -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) diff --git a/src/clickthrough/SUPERWOW_TIMBER_CONFLICT.md b/src/clickthrough/SUPERWOW_TIMBER_CONFLICT.md new file mode 100644 index 0000000..600820c --- /dev/null +++ b/src/clickthrough/SUPERWOW_TIMBER_CONFLICT.md @@ -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. diff --git a/src/clickthrough/clickthrough.zig b/src/clickthrough/clickthrough.zig index c10a6ef..95b4fe9 100644 --- a/src/clickthrough/clickthrough.zig +++ b/src/clickthrough/clickthrough.zig @@ -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(); diff --git a/src/clickthrough/portal_visual.zig b/src/clickthrough/portal_visual.zig new file mode 100644 index 0000000..c1da27b --- /dev/null +++ b/src/clickthrough/portal_visual.zig @@ -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(); +} diff --git a/src/dpslog/HANDOFF_CLEU_FILTERS.md b/src/dpslog/HANDOFF_CLEU_FILTERS.md new file mode 100644 index 0000000..ff80fe2 --- /dev/null +++ b/src/dpslog/HANDOFF_CLEU_FILTERS.md @@ -0,0 +1,154 @@ +# Handoff: COMBAT_LOG_EVENT Filtering (CLEU vs CLEUF) + +## Goal + +Implement Blizzard's TBC/WotLK event filtering system so we provide both: +- `COMBAT_LOG_EVENT` -- filtered version (only events relevant to the player's group) +- `COMBAT_LOG_EVENT_UNFILTERED` -- all events in range (everything the client sees) + +This matches how TBC and WotLK work: addons register for whichever they need. DPS meters +use UNFILTERED for complete data, while UI elements use the filtered version to reduce noise. + +## Background + +### How Blizzard Did It + +In TBC (2.4.0), Blizzard introduced `COMBAT_LOG_EVENT_UNFILTERED` which fires for ALL +combat events the client receives. They also provided `COMBAT_LOG_EVENT` which fires only +for events passing a set of filters. + +The filtering is controlled by: +- `CombatLogClearEntries()` -- clears the combat log +- `CombatLogAddFilter(...)` -- adds a filter rule +- `CombatLogResetFilter()` -- resets to default filters +- `CombatLogGetCurrentEntry(...)` -- reads the current event (deprecated by WotLK) +- `CombatLogGetNumEntries()` -- count of buffered events + +Default filters include: +- Source or dest is the player +- Source or dest is in the player's party/raid +- Source or dest is the player's pet/guardian +- Events within a certain range + +### What We Currently Have + +We fire a single `COMBAT_LOG_EVENT` at slot 549 via SignalEventParam. Every hook fires +every event it sees -- no filtering. This is effectively UNFILTERED behavior under the +FILTERED event name. + +## Implementation Plan + +### Phase 1: Register Both Events + +Add a second event slot for `COMBAT_LOG_EVENT_UNFILTERED`: +- `COMBAT_LOG_EVENT` at slot 549 (existing) -- will become the filtered version +- `COMBAT_LOG_EVENT_UNFILTERED` at slot 550 -- fires everything (current behavior) + +In `createEventsDetour`, bump the event count by 2 instead of 1 and register both names. +Update all fire functions to fire both events (or fire UNFILTERED always and FILTERED +conditionally). + +**IMPORTANT**: Slot 550 was previously identified as a float global (0x3E800000 = 0.25). +Need to verify this is still the case or find a different slot. May need to search for +two consecutive free slots. + +### Phase 2: Implement Filtering Logic + +The filter checks whether source or dest is "interesting" to the local player: + +```zig +fn shouldFilter(src_guid: u64, dst_guid: u64) bool { + // Always pass if source or dest is the local player + if (src_guid == getPlayerGUID() or dst_guid == getPlayerGUID()) return false; + // Always pass if source or dest is in the player's group + if (isGroupMember(src_guid) or isGroupMember(dst_guid)) return false; + // Always pass if source or dest is a pet owned by a group member + if (isPetOfGroupMember(src_guid) or isPetOfGroupMember(dst_guid)) return false; + // Filter out (don't fire COMBAT_LOG_EVENT) + return true; +} +``` + +Requires: +- `getPlayerGUID()` -- already have this (0x468550) +- `isGroupMember(guid)` -- check party (0xBC6F48) and raid (0xB712A8) GUID arrays +- `isPetOfGroupMember(guid)` -- check UNIT_FIELD_SUMMONEDBY descriptor, resolve owner + +### Phase 3: Fire Functions + +Two approaches: + +**Option A: Double-fire** +Every fire function fires the event twice -- once for UNFILTERED (always), once for +FILTERED (if passes filter). Simple but doubles the SignalEventParam calls. + +**Option B: Conditional fire with shared push** +Push args to Lua stack once, then call SignalEvent for each registered event that should +receive it. Requires understanding SignalEventParam internals more deeply. + +Option A is simpler and the performance cost of an extra SignalEventParam call per event +is negligible compared to the Lua handler execution. + +### Phase 4: sourceFlags / destFlags + +With filtering in place, we can also construct the COMBATLOG_OBJECT_* bitfield that +WotLK addons use for their own filtering: + +``` +COMBATLOG_OBJECT_AFFILIATION_MINE = 0x0001 +COMBATLOG_OBJECT_AFFILIATION_PARTY = 0x0002 +COMBATLOG_OBJECT_AFFILIATION_RAID = 0x0004 +COMBATLOG_OBJECT_AFFILIATION_OUTSIDER = 0x0008 +COMBATLOG_OBJECT_REACTION_FRIENDLY = 0x0010 +COMBATLOG_OBJECT_REACTION_NEUTRAL = 0x0020 +COMBATLOG_OBJECT_REACTION_HOSTILE = 0x0040 +COMBATLOG_OBJECT_CONTROL_PLAYER = 0x0100 +COMBATLOG_OBJECT_CONTROL_NPC = 0x0200 +COMBATLOG_OBJECT_TYPE_PLAYER = 0x0400 +COMBATLOG_OBJECT_TYPE_NPC = 0x0800 +COMBATLOG_OBJECT_TYPE_PET = 0x1000 +COMBATLOG_OBJECT_TYPE_GUARDIAN = 0x2000 +COMBATLOG_OBJECT_TYPE_OBJECT = 0x4000 +``` + +These can be constructed from: +- GUID type bits (high nibble of GUID encodes player/creature/pet/gameobject) +- Group membership (party/raid roster arrays) +- Reaction (UnitReaction at 0x6061E0) +- Ownership (UNIT_FIELD_SUMMONEDBY descriptor) + +This is the same data needed for filtering, so it comes naturally after Phase 2. + +### Phase 5: Lua API (Optional) + +Provide the filter configuration API for full parity: +- `CombatLogAddFilter(srcFlags, dstFlags, eventType)` +- `CombatLogResetFilter()` +- `CombatLogGetNumEntries()` + +Most addons don't use these -- they just register for UNFILTERED and do their own +filtering. Low priority. + +## Key Addresses + +- Event slot 549: COMBAT_LOG_EVENT (existing) +- Event slot 550: needs verification (was float 0.25 in earlier research) +- Player GUID: 0x468550 (ClntObjMgrGetActivePlayer) +- Party GUIDs: 0xBC6F48 (array of 4 u64 GUIDs) +- Raid roster: 0xB712A8 (array of ptrs), count at 0xB713E0 +- UnitReaction: 0x6061E0 +- UNIT_FIELD_SUMMONEDBY: descriptor offset 0x30 (index 0x0C) + +## Dependencies + +- Phase 1 can start immediately +- Phase 2 needs isGroupMember which uses existing known addresses +- Phase 3 trivial once Phase 1+2 done +- Phase 4 is independent enhancement, can parallelize with Phase 2 + +## Risk + +- Slot 550 conflict with float global -- may need to find a safe slot +- Double-firing events may interact poorly with addons that register for both + (they'd see every event twice) -- need to ensure the events have distinct IDs +- Performance of filter check per event should be negligible (few memory reads) diff --git a/src/dpslog/RESEARCH.md b/src/dpslog/RESEARCH.md index 077fdc5..ec225c7 100644 --- a/src/dpslog/RESEARCH.md +++ b/src/dpslog/RESEARCH.md @@ -599,6 +599,20 @@ All in the 0x51xxxx addon/UI system. Zeroing/overwriting these corrupts game sta SMSG_RESURRECT_FAILED (0x252). - [ ] **UNIT_POSITION**: Periodic or event-driven unit position data in combat log events. Useful for replay/analysis tools. +- [ ] **SPELL_CAST dest coordinates**: SMSG_SPELL_START/GO include destination XYZ for + area-targeted spells via TARGET_FLAG_DEST_LOCATION (0x40) in SpellCastTargets. + - Target data format in packet: uint16 targetMask, then if 0x02: packed GUID (unit), + if 0x20: 3x float (source XYZ), if 0x40: 3x float (dest XYZ) + - Player ground AoE (Blizzard, Flamestrike, Rain of Fire, Volley): client sends clicked + position, server echoes in both SPELL_START and SPELL_GO + - Mob ground AoE (Ignite Earth, mob Flamestrike): AI script calls CastSpell(x,y,z,...), + server sets 0x40 via setDestination(), coords appear in SPELL_GO + - Self-centered AoE (Arcane Explosion, Hellfire): no 0x40, no dest coords + - Triggered spells: dest overridden to caster position regardless of original flags + - Already hooked at 0x6E7640 (spellStartDetour) -- just need to parse target mask and + extract floats when 0x40 is set, emit as extra CLEU params on SPELL_CAST_START/SUCCESS + - Server ref: SpellCastTargets::write() in tortoise-wow Spell.cpp lines 223-268 + - Enables boss mod positional warnings (e.g. Karazhan 40-man Ignite Earth ground zones) ### Completed reference - [x] RANGE_MISSED: Detected via spell ID check (75/5019) in ProcessSpellCombatResult hook. @@ -1010,3 +1024,91 @@ ProcessSpellDamageWithLocalization (0x629d30) | FrameScript_CreateEvents | FrameScript_CreateEvents | 0x703D90 | | FrameScript_RegisterFunction | FrameScript_RegisterFunction | 0x704120 | | GetSpellNameById | (SpellDb at 0xC0D780) | 0x6264b0 | + +--- + +## Missing WotLK CLEU Subevents + +Events present in WotLK 3.3.5 COMBAT_LOG_EVENT_UNFILTERED but not yet implemented. + +### Loot Events (novel -- never existed in any WoW combat log) + +LOOT was never a COMBAT_LOG_EVENT subevent in any WoW expansion. Loot tracking addons +use separate Lua events (LOOT_OPENED, CHAT_MSG_LOOT, etc.), not the combat log. +Adding loot to our combat log would be a novel extension, not WotLK parity. + +Relevant vanilla packets if we ever want to add this: + +| Packet | Opcode | Format | +|--------|--------|--------| +| SMSG_ITEM_PUSH_RESULT | 0x166 | playerGUID(8), received(4), created(4), showInChat(4), bagSlot(1), itemSlot(4), itemID(4), suffix(4), randomProp(4), count(4) | +| SMSG_LOOT_ROLL | 0x2A2 | targetGUID(8), slot(4), rollerGUID(8), itemID(4), suffix(4), randomProp(4), rollNum(1), rollType(1) | +| SMSG_LOOT_ROLL_WON | 0x29F | targetGUID(8), slot(4), itemID(4), suffix(4), randomProp(4), winnerGUID(8), rollNum(1), rollType(1) | +| SMSG_LOOT_ALL_PASSED | 0x29E | targetGUID(8), slot(4), itemID(4), randomProp(4), suffix(4) | +| SMSG_LOOT_MONEY_NOTIFY | 0x163 | amount(4) | + +### Enchant Events (verified in WotLK/Cata logs) + +Confirmed present in real WotLK combat logs. No vanilla packet equivalent -- would need +to hook the client enchant application/removal functions. + +| Subevent | Description | +|----------|-------------| +| ENCHANT_APPLIED | Item enchanted (temp or permanent) | +| ENCHANT_REMOVED | Enchant removed/expired | + +Verified WotLK format: +``` +ENCHANT_APPLIED,srcGUID,srcName,srcFlags,srcRaidFlags,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName" +ENCHANT_REMOVED,0x0000000000000000,nil,0x80000000,0x80000000,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName" +``` + +Note: ENCHANT_REMOVED source is always null GUID (no "remover"). + +### SPELL_CREATE (verified in WotLK/Cata logs) + +Fires when a player creates a game object (trap, totem, feast, cauldron). +Uses standard spell prefix. Dest is the created GO's GUID. + +``` +SPELL_CREATE,srcGUID,srcName,srcFlags,srcRaidFlags,goGUID,"goName",goFlags,goRaidFlags,spellID,"spellName",spellSchool +``` + +### Combat Events (low priority) + +| Subevent | Description | Notes | +|----------|-------------|-------| +| UNIT_DISSIPATES | Pet/totem/guardian despawn | Variant of UNIT_DIED, not in WotLK sample but in retail | +| SPELL_ABSORBED | Damage absorbed by shield | Present in Shadowlands+, not in WotLK sample | +| SPELL_HEAL_ABSORBED | Healing absorbed by anti-heal | Shadowlands+, no vanilla mechanic | +| SWING_DAMAGE_LANDED | Melee hit after absorb | Shadowlands+, not in WotLK | +| SPELL_BUILDING_* | Structure damage prefix | WotLK siege content, N/A in vanilla | +| *_DURABILITY_DAMAGE | Equipment durability loss | Rarely useful | + +### Metadata Events (Shadowlands+ only, NOT in WotLK) + +These do NOT appear in WotLK/Cata combat logs. They were added in later expansions +(combat log version 16+). Listed for reference only. + +| Subevent | Description | First seen | +|----------|-------------|------------| +| ENCOUNTER_START | Boss encounter begins | Shadowlands+ | +| ENCOUNTER_END | Boss encounter ends | Shadowlands+ | +| ZONE_CHANGE | Player changes zone | Shadowlands+ | +| COMBATANT_INFO | Gear/talent snapshot | Shadowlands+ | +| CHALLENGE_MODE_START/END | M+ key start/end | Shadowlands+ | +| EMOTE | Boss emote text | Shadowlands+ | +| MAP_CHANGE | Map transition | Shadowlands+ | +| ARENA_MATCH_START/END | Arena match | Shadowlands+ | + +### Current Implementation Status + +37 subevents implemented across 23 hooks (Phases 1-5 complete): +- Damage: SWING/RANGE/SPELL/PERIODIC/SHIELD/SPLIT/ENVIRONMENTAL (7) +- Missed: SWING/RANGE/SPELL/PERIODIC/SHIELD (5) +- Heal: SPELL/PERIODIC (2) +- Power: ENERGIZE/DRAIN/PERIODIC_ENERGIZE/PERIODIC_DRAIN/PERIODIC_LEECH (5) +- Aura: APPLIED/REMOVED/DOSE(x2)/REFRESH/BROKEN/BROKEN_SPELL (7) +- Cast: START/SUCCESS/FAILED (3) +- Misc: INTERRUPT/DISPEL/DISPEL_FAILED/STOLEN/EXTRA_ATTACKS/SUMMON/RESURRECT/INSTAKILL (8) +- Death: UNIT_DIED/UNIT_DESTROYED/PARTY_KILL (3) diff --git a/src/dpslog/WSBT/WSBT_CLEUAdapter.lua b/src/dpslog/WSBT/WSBT_CLEUAdapter.lua index 3a46d41..0cc4b21 100644 --- a/src/dpslog/WSBT/WSBT_CLEUAdapter.lua +++ b/src/dpslog/WSBT/WSBT_CLEUAdapter.lua @@ -486,7 +486,7 @@ local function profReport(label, tbl) end end -local benchActive = true +local benchActive = false local benchFrame = CreateFrame("Frame") local function benchCombatStart() diff --git a/src/dpslog/WeirdDPSMate/DPSMate_CLEUAdapter.lua b/src/dpslog/WeirdDPSMate/DPSMate_CLEUAdapter.lua index 9252149..3da23a8 100644 --- a/src/dpslog/WeirdDPSMate/DPSMate_CLEUAdapter.lua +++ b/src/dpslog/WeirdDPSMate/DPSMate_CLEUAdapter.lua @@ -145,6 +145,14 @@ end local profiling = false local profCLEU = { events = 0, totalMs = 0, gcStart = 0 } local profOrig = { events = 0, totalMs = 0, gcStart = 0 } + +-- Fine-grained timing buckets (microseconds, accumulated) +local profDetail = { getInfo = 0, dbCalls = 0, dispatch = 0, total = 0, count = 0 } +-- Per-DB-function timing (microseconds, accumulated) +local profDB = { + DamageDone = 0, DamageTaken = 0, EnemyDamage = 0, DeathHistory = 0, + Healing = 0, HealingTaken = 0, BuildBuffs = 0, Other = 0, count = 0 +} local profCurrent = nil -- Hook the parser's OnEvent to measure original mode @@ -204,6 +212,32 @@ local function profReport(label, tbl) "|cff00ff00[%s]|r %d events, %.1fms total, %.1f us/event, %+.1f KB gc", label, tbl.events, totalMs, avgUs, gcDelta)) + -- Detail breakdown for CLEU + if label == "CLEU" and profDetail.count > 0 then + local avgGetInfo = profDetail.getInfo / profDetail.count + local avgDB = profDetail.dbCalls / profDetail.count + local avgTotal = profDetail.total / profDetail.count + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cff00ff00[CLEU detail]|r getInfo=%.1f us, db+logic=%.1f us, total=%.1f us (%d events)", + avgGetInfo, avgDB, avgTotal, profDetail.count)) + -- Per-DB-function breakdown + if profDB.count > 0 then + local n = profDB.count + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cff00ff00[DB funcs]|r DD=%.0f DT=%.0f ED=%.0f DH=%.0f H=%.0f HT=%.0f Buf=%.0f Oth=%.0f us/evt", + profDB.DamageDone/n, profDB.DamageTaken/n, profDB.EnemyDamage/n, + profDB.DeathHistory/n, profDB.Healing/n, profDB.HealingTaken/n, + profDB.BuildBuffs/n, profDB.Other/n)) + end + profDetail.getInfo = 0 + profDetail.dbCalls = 0 + profDetail.total = 0 + profDetail.count = 0 + profDB.DamageDone = 0; profDB.DamageTaken = 0; profDB.EnemyDamage = 0 + profDB.DeathHistory = 0; profDB.Healing = 0; profDB.HealingTaken = 0 + profDB.BuildBuffs = 0; profDB.Other = 0; profDB.count = 0 + end + -- If we have both measurements, show comparison if lastCLEUAvg and lastOrigAvg and lastOrigAvg > 0 then local pct = ((lastCLEUAvg - lastOrigAvg) / lastOrigAvg) * 100 @@ -216,38 +250,42 @@ local function profReport(label, tbl) end end --- /dpsbench -- enables per-combat A/B profiling. Each combat: measure, report, flip. -local benchActive = true +-- Always-on CLEU profiling: report stats at every combat end. +-- /dpsbench toggles A/B mode (alternates CLEU/ORIGINAL each combat for comparison). +local benchActive = false local benchFrame = CreateFrame("Frame") -local function benchCombatStart() - if not benchActive then return end +local function combatStart() profCurrent = cleuActive and profCLEU or profOrig profReset(profCurrent) - debugprofilestart() -- start the monotonic clock for this combat + debugprofilestart() profiling = true - local label = cleuActive and "CLEU" or "ORIGINAL" - DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[DPS Bench]|r combat started, measuring %s", label)) + if benchActive then + local label = cleuActive and "CLEU" or "ORIGINAL" + DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[DPS Bench]|r measuring %s", label)) + end end -local function benchCombatEnd() - if not benchActive or not profiling then return end +local function combatEnd() + if not profiling then return end profiling = false local label = cleuActive and "CLEU" or "ORIGINAL" profReport(label, profCurrent) - toggle() - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff00ff00[DPS Bench]|r next combat will use: %s", cleuActive and "CLEU" or "ORIGINAL")) + if benchActive then + toggle() + DEFAULT_CHAT_FRAME:AddMessage(string.format( + "|cff00ff00[DPS Bench]|r next combat: %s", cleuActive and "CLEU" or "ORIGINAL")) + end end benchFrame:RegisterEvent("PLAYER_REGEN_DISABLED") benchFrame:RegisterEvent("PLAYER_REGEN_ENABLED") benchFrame:SetScript("OnEvent", function() if event == "PLAYER_REGEN_DISABLED" then - benchCombatStart() + combatStart() elseif event == "PLAYER_REGEN_ENABLED" then - benchCombatEnd() + combatEnd() end end) @@ -256,11 +294,10 @@ SlashCmdList["DPSBENCH"] = function() benchActive = not benchActive if benchActive then DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff00ff00[DPS Bench]|r enabled. Current mode: %s. Enter combat to begin.", + "|cff00ff00[DPS Bench]|r A/B enabled. Current mode: %s. Enter combat to begin.", cleuActive and "CLEU" or "ORIGINAL")) else - profiling = false - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[DPS Bench]|r disabled.") + DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[DPS Bench]|r A/B disabled (stats still reported).") end end @@ -280,16 +317,17 @@ local FailDB = DPSMate.Parser.FailDB cleuHandler = function() if not CombatLogGetCurrentEventInfo then return end - -- Single call — positions differ by prefix type: - -- Swing: p1=amount, p2=overkill, p3=school, ... - -- Spell: p1=spellId, p2=spellName, p3=spellSchool, p4=amount, ... - -- Env: p1=envType, p2=amount, ... + + local t0 = profiling and debugprofilestop() + local sub, srcGUID, srcName, srcFlags, srcRaidFlags, dstGUID, dstName, dstFlags, dstRaidFlags, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12 = CombatLogGetCurrentEventInfo() if not sub then return end + local t1 = profiling and debugprofilestop() + if not srcName or srcName == "" then srcName = "Unknown" end if not dstName or dstName == "" then dstName = "Unknown" end @@ -306,11 +344,17 @@ cleuHandler = function() local crush = crushing and 1 or 0 local hit = (crit == 0 and glanc == 0 and crush == 0) and 1 or 0 + local ta, tb + if profiling then ta = debugprofilestop() end DB:DamageDone(srcName, AAttack, hit, crit, 0, 0, 0, 0, amount, glanc, 0) + if profiling then tb = debugprofilestop(); profDB.DamageDone = profDB.DamageDone + (tb - ta); ta = tb end DB:DamageTaken(dstName, AAttack, hit, crit, 0, 0, 0, 0, amount, srcName, crush, 0) + if profiling then tb = debugprofilestop(); profDB.DamageTaken = profDB.DamageTaken + (tb - ta); ta = tb end DB:EnemyDamage(1, DPSMateEDT, dstName, AAttack, hit, crit, 0, 0, 0, 0, amount, srcName, 0, crush) DB:EnemyDamage(2, DPSMateEDD, srcName, AAttack, hit, crit, 0, 0, 0, 0, amount, dstName, 0, 0) + if profiling then tb = debugprofilestop(); profDB.EnemyDamage = profDB.EnemyDamage + (tb - ta); ta = tb end DB:DeathHistory(dstName, srcName, AAttack, amount, hit, crit, "hit", crush) + if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end if absorbed > 0 then DB:SetUnregisterVariables(absorbed, AAttack, srcName) DB:Absorb(AAttack, dstName, srcName) @@ -346,11 +390,17 @@ cleuHandler = function() local hit = (crit == 0 and glanc == 0 and crush == 0) and 1 or 0 local abilityName = (sub == "SPELL_PERIODIC_DAMAGE") and (spellName .. "(Periodic)") or spellName + local ta, tb + if profiling then ta = debugprofilestop() end DB:DamageDone(srcName, abilityName, hit, crit, 0, 0, 0, 0, amount, glanc, 0) + if profiling then tb = debugprofilestop(); profDB.DamageDone = profDB.DamageDone + (tb - ta); ta = tb end DB:DamageTaken(dstName, abilityName, hit, crit, 0, 0, 0, 0, amount, srcName, crush, 0) + if profiling then tb = debugprofilestop(); profDB.DamageTaken = profDB.DamageTaken + (tb - ta); ta = tb end DB:EnemyDamage(1, DPSMateEDT, dstName, abilityName, hit, crit, 0, 0, 0, 0, amount, srcName, 0, crush) DB:EnemyDamage(2, DPSMateEDD, srcName, abilityName, hit, crit, 0, 0, 0, 0, amount, dstName, 0, 0) + if profiling then tb = debugprofilestop(); profDB.EnemyDamage = profDB.EnemyDamage + (tb - ta); ta = tb end DB:DeathHistory(dstName, srcName, abilityName, amount, hit, crit, "hit", crush) + if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end if spellSchool then DB:AddSpellSchool(abilityName, spellSchool) end if absorbed > 0 then DB:SetUnregisterVariables(absorbed, abilityName, srcName) @@ -396,10 +446,15 @@ cleuHandler = function() local effective = amount - overheal if effective < 0 then effective = 0 end + local ta, tb + if profiling then ta = debugprofilestop() end DB:Healing(1, DPSMateHealingTaken, srcName, spellName, hit, crit, effective) DB:Healing(2, DPSMateOverhealing, srcName, spellName, hit, crit, overheal) + if profiling then tb = debugprofilestop(); profDB.Healing = profDB.Healing + (tb - ta); ta = tb end DB:HealingTaken(1, DPSMateHealingTaken, srcName, spellName, hit, crit, effective, dstName) + if profiling then tb = debugprofilestop(); profDB.HealingTaken = profDB.HealingTaken + (tb - ta); ta = tb end DB:DeathHistory(dstName, srcName, spellName, effective, hit, crit, "heal", 0) + if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end -- ======================================================================== -- AURA events (+ absorb shield lifecycle) @@ -481,6 +536,14 @@ cleuHandler = function() if not Parser.petToOwnerMap[dstName] then Parser.petToOwnerMap[dstName] = {} end Parser.petToOwnerMap[dstName][srcName] = true end + + if profiling and t0 then + local t2 = debugprofilestop() + profDetail.getInfo = profDetail.getInfo + (t1 - t0) + profDetail.dbCalls = profDetail.dbCalls + (t2 - t1) + profDetail.total = profDetail.total + (t2 - t0) + profDetail.count = profDetail.count + 1 + end end -- Set the measured wrapper as the actual handler diff --git a/src/dpslog/WeirdDPSMate/DPSMate_DataBuilder.lua b/src/dpslog/WeirdDPSMate/DPSMate_DataBuilder.lua index 7b111dc..d8461d2 100644 --- a/src/dpslog/WeirdDPSMate/DPSMate_DataBuilder.lua +++ b/src/dpslog/WeirdDPSMate/DPSMate_DataBuilder.lua @@ -875,6 +875,7 @@ function DPSMate.DB:OnGroupUpdate() local num = GetNumRaidMembers() DPSMate.Parser.TargetParty = {} DPSMate.Parser.petToOwnerMap = {} + petQualifiedCache = {} if num<=0 then type = "party" num = GetNumPartyMembers() @@ -993,18 +994,36 @@ function DPSMate.DB:OnGroupUpdate() DPSMate.Parser:AssociateShaman("None", "None", true) end +-- Cache: raw pet name -> qualified "PetName (OwnerName)" to avoid repeated pairs() iteration +local petQualifiedCache = {} + function DPSMate.DB:BuildUser(Dname, Dclass) if not Dname then Dname = "?!NIL Name?!" end - -- Auto-qualify raw pet/totem names via petToOwnerMap (single-owner only) - local pom = DPSMate.Parser.petToOwnerMap - if pom and pom[Dname] then - local count, singleOwner = 0, nil - for o, _ in pairs(pom[Dname]) do - count = count + 1 - singleOwner = o - end - if count == 1 then - Dname = Dname .. " (" .. singleOwner .. ")" + -- Fast path: already-known non-pet name — skip regex + petToOwnerMap entirely + local u = DPSUser[Dname] + if u then return u[1] end + -- Check pet name cache (avoids repeated pairs() iteration on petToOwnerMap) + local cached = petQualifiedCache[Dname] + if cached then + u = DPSUser[cached] + if u then return u[1] end + Dname = cached + else + -- Auto-qualify raw pet/totem names via petToOwnerMap (single-owner only) + local pom = DPSMate.Parser.petToOwnerMap + if pom and pom[Dname] then + local count, singleOwner = 0, nil + for o, _ in pairs(pom[Dname]) do + count = count + 1 + singleOwner = o + end + if count == 1 then + local qualified = Dname .. " (" .. singleOwner .. ")" + petQualifiedCache[Dname] = qualified + u = DPSUser[qualified] + if u then return u[1] end + Dname = qualified + end end end local _,_, pet,owner = strfind(Dname,"(.+)%s%((.+)%)") @@ -1102,7 +1121,8 @@ function DPSMate.DB:Threat(cause, spellname, target, value, amount) target = self:BuildUser(target) cause = self:BuildUser(cause) spellname = self:BuildAbility(spellname) - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do + local val = tablemodes[cat] if not DPSThreat[cat] then DPSThreat[cat] = {} end if not DPSThreat[cat][cause] then DPSThreat[cat][cause] = {} @@ -1743,7 +1763,7 @@ function DPSMate.DB:RegisterAbsorb(owner, ability, abilityTarget) owner = self:BuildUser(owner) abilityTarget = self:BuildUser(abilityTarget) ability = self:BuildAbility(ability) - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] if not DPSAbsorb[cat] then DPSAbsorb[cat] = {} end if not DPSAbsorb[cat][abilityTarget] then DPSAbsorb[cat][abilityTarget] = {} @@ -1777,7 +1797,7 @@ function DPSMate.DB:UnregisterAbsorb(ability, abilityTarget) ability = self:BuildAbility(ability) abilityTarget = self:BuildUser(abilityTarget) local AbsorbingAbility - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] AbsorbingAbility = self:GetActiveAbsorbAbilityByPlayer(ability, abilityTarget, cat) if AbsorbingAbility[1] then path = DPSAbsorb[cat][abilityTarget][AbsorbingAbility[1]][AbsorbingAbility[2]][AbsorbingAbility[3]]["i"] @@ -1889,7 +1909,7 @@ function DPSMate.DB:Absorb(ability, abilityTarget, incTarget) abilityTarget = self:BuildUser(abilityTarget) ability = self:BuildAbility(ability) local AbsorbingAbility - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] local abilityName = DPSMate:GetAbilityById(ability) local abilityEntry = abilityName and DPSMateAbility[abilityName] if not abilityEntry then break end @@ -2094,7 +2114,7 @@ function DPSMate.DB:Dispels(cause, Dname, target, ability) target = self:BuildUser(target) Dname = self:BuildAbility(Dname) ability = self:BuildAbility(ability) - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] if not DPSDispel[cat] then DPSDispel[cat] = {} end if not DPSDispel[cat][cause] then DPSDispel[cat][cause] = { @@ -2130,13 +2150,13 @@ function DPSMate.DB:UnregisterDeath(target) if strfind(target, "%s") then return end target = self:BuildUser(target) local p - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do if DPSDeath[cat] and DPSDeath[cat][target] and DPSDeath[cat][target][1] then DPSDeath[cat][target][1]["i"][1]=1 DPSDeath[cat][target][1]["i"][2]=GameTime_GT() if cat==1 and DPSMate.Parser.TargetParty[DPSMate:GetUserById(target)] and DPSDeath[cat][target][1][1] then p = DPSDeath[cat][target][1][1] - DPSMate:Broadcast(4, DPSMate:GetUserById(target), DPSMate:GetUserById(p[1]), DPSMate:GetAbilityById(p[2]), p[3]) + DPSMate:Broadcast(4, DPSMate:GetUserById(target), DPSMate:GetUserById(p[1]), DPSMate:GetAbilityById(p[2]), p[3]) end end end @@ -2148,7 +2168,8 @@ function DPSMate.DB:DeathHistory(target, cause, ability, amount, hit, crit, type cause = self:BuildUser(cause) ability = self:BuildAbility(ability) local hitCritCrush - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do + local val = tablemodes[cat] if not DPSDeath[cat] then DPSDeath[cat] = {} end if not DPSDeath[cat][target] then DPSDeath[cat][target] = {} @@ -2245,7 +2266,7 @@ function DPSMate.DB:Kick(cause, target, causeAbility, targetAbility) cause = self:BuildUser(cause) causeAbility = self:BuildAbility(causeAbility) targetAbility = self:BuildAbility(targetAbility) - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] if not DPSInterrupt[cat] then DPSInterrupt[cat] = {} end if not DPSInterrupt[cat][cause] then DPSInterrupt[cat][cause] = { @@ -2310,7 +2331,7 @@ function DPSMate.DB:BuildBuffs(cause, target, ability, bool) target = self:BuildUser(target) cause = self:BuildUser(cause) ability = self:BuildAbility(ability) - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] if not DPSAurasGained[cat] then DPSAurasGained[cat] = {} end if not DPSAurasGained[cat][target] then DPSAurasGained[cat][target] = {} @@ -2347,7 +2368,7 @@ function DPSMate.DB:DestroyBuffs(target, ability) target = self:BuildUser(target) ability = self:BuildAbility(ability) local TL - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] if not DPSAurasGained[cat] then DPSAurasGained[cat] = {} end if not DPSAurasGained[cat][target] then DPSAurasGained[cat][target] = {} @@ -2557,7 +2578,7 @@ function DPSMate.DB:BuildFail(type, user, cause, ability, amount) cause = self:BuildUser(cause) ability = self:BuildAbility(ability) local time = GameTime_GT() - for cat, val in pairs(tablemodes) do + for cat = 1, 2 do local val = tablemodes[cat] if not DPSFail[cat] then DPSFail[cat] = {} end if not DPSFail[cat][cause] then DPSFail[cat][cause] = {} diff --git a/src/framecrash/RESEARCH.md b/src/framecrash/RESEARCH.md index 67b333f..dd48912 100644 --- a/src/framecrash/RESEARCH.md +++ b/src/framecrash/RESEARCH.md @@ -836,3 +836,43 @@ Called from: ### Module currently DISABLED by default Build flag changed to `orelse false` in build.zig. Enable with `-Dframecrash=true`. + +--- + +## Uninvestigated: NULL this in PrepareModelForRender (0x710450) + +Crash observed 2026-03-25: +``` +0x0071045A: MOV EAX, [ESI + 0x10] -- ESI=ECX=0 (NULL this pointer) +ACCESS_VIOLATION reading 0x00000010 +``` + +Call chain: +``` +CM2Scene_ExecuteRenderPass (0x708900) + -> RenderBatches (0x70B371) + -> DrawCallback (0x70D960) + -> ValidateClientObjectAndProcessWithManagerLookup (0x6C7780) + -> PrepareModelForRender (0x710450) -- CRASH +``` + +Root cause: at 0x6C77A8, `MOV EAX, [EAX+0xD8]` reads a field that can be NULL. The +parent object is NULL-checked but the +0xD8 field is not. Then `MOV ECX, EAX` passes +NULL as this to PrepareModelForRender. + +```asm +0x6C77A4: TEST EAX, EAX ; parent object null check +0x6C77A6: JZ skip ; OK +0x6C77A8: MOV EAX, [EAX+0xD8] ; read child -- CAN BE NULL +0x6C77AE: PUSH 0 +0x6C77B0: PUSH 0 +0x6C77B2: MOV ECX, EAX ; this = possibly NULL +0x6C77B4: CALL PrepareModelForRender ; CRASH if ECX=0 +``` + +PrepareModelForRender: thiscall(ECX=this, stack: 2 params), RET 0x8. + +Fix options: +1. Hook PrepareModelForRender with NULL-this guard (protects all callers) +2. Patch call site at 0x6C77AE to add TEST EAX,EAX + JZ (needs code cave, only 11 bytes available for 15 bytes of patched code) +3. Binary patch PrepareModelForRender prologue (needs to preserve RET 0x8 stack cleanup) diff --git a/src/logsessions/addon/WeirdUtils_LogSessions.toc b/src/logsessions/addon/WeirdUtils_LogSessions.toc index 73dce04..812b6d4 100644 --- a/src/logsessions/addon/WeirdUtils_LogSessions.toc +++ b/src/logsessions/addon/WeirdUtils_LogSessions.toc @@ -3,5 +3,4 @@ ## Notes: Session-based combat and chat log management ## Version: 1.0 - LogSessions.lua diff --git a/src/minimapicons/minimapicons.zig b/src/minimapicons/minimapicons.zig index 7cc16db..ef746cb 100644 --- a/src/minimapicons/minimapicons.zig +++ b/src/minimapicons/minimapicons.zig @@ -550,14 +550,14 @@ fn worldPosToMinimapCoords( }); } -fn getFrameUnkScale(info: u32) f32 { +fn getFrameUnkScale(info: u32) ?f32 { const frame = hook.readMem(u32, info + ADDR.MI_FRAME); - if (!isValidPtr(frame)) return 1.0; + if (!isValidPtr(frame)) return null; const fsp = frame + ADDR.FRAME_SCRIPT_PART; const vtable = hook.readMem(u32, fsp); - if (!isValidPtr(vtable)) return 1.0; + if (!isValidPtr(vtable)) return null; const fn_addr = hook.readMem(u32, vtable + 7 * 4); - if (!isValidPtr(fn_addr)) return 1.0; + if (!isValidPtr(fn_addr)) return null; // __thiscall(fsp_ECX) → f32 on FPU ST(0) return hook.call(fn (u32) callconv(hook.cc.thiscall) f32, fn_addr, .{fsp}); } @@ -837,6 +837,10 @@ fn trackObject(info: u32, guid_lo: u32, guid_hi: u32, blip: Blip) void { // Cache minimap info per frame — same for all objects in one enumeration cycle if (!g_minimap_info.valid) { + // If the minimap frame scale can't be resolved (transient state during + // zone transitions), skip all blips this cycle rather than projecting + // with a wrong scale that pushes blips outside the circle. + const unk_scale = getFrameUnkScale(info) orelse return; g_minimap_info = .{ .cur = .{ .x = hook.readMem(f32, info + ADDR.MI_POS), @@ -845,7 +849,7 @@ fn trackObject(info: u32, guid_lo: u32, guid_hi: u32, blip: Blip) void { }, .radius = hook.readMem(f32, info + ADDR.MI_RADIUS), .layout_scale = hook.readMem(f32, info + ADDR.MI_LAYOUT_SCALE), - .unk_scale = getFrameUnkScale(info), + .unk_scale = unk_scale, .valid = true, }; } diff --git a/src/superweirdo/superweirdo.zig b/src/superweirdo/superweirdo.zig new file mode 100644 index 0000000..9b6aaf6 --- /dev/null +++ b/src/superweirdo/superweirdo.zig @@ -0,0 +1,180 @@ +//! SuperWeirdo -- GO loot sparkle for interactable objects +//! +//! Replicates the SuperWoW loot sparkle feature via two patches and three +//! cleanup hooks: +//! +//! Patches: +//! 1. UpdateUnitStatusFlags (0x60c520) status flags initializer: +//! 0x60c560: imm32 0x00000000 -> 0x00002000 +//! Sets bit 13 in status flags, enabling sparkle visual on qualifying objects. +//! +//! 2. AttachSpellVisualToUnit (0x61fc00) object type mask: +//! 0x61fc1c: MOV ECX,0x8 -> MOV ECX,0x1 +//! Changes type filter from Unit-only (0x8) to Object base (0x1), +//! allowing visual attachments on GOs. +//! +//! 3. Data constant at 0x838f7c: 0x14 -> 0x0D +//! Adjusts sparkle-related rendering parameter. +//! +//! Cleanup hooks (prevent leaked render nodes on GO lifecycle changes): +//! Vanilla GOs never had visual attachments, so their destroy/update paths +//! don't clean them up. We hook three GO lifecycle functions and walk the +//! render node list (obj+0xB4) to unlink+release any sparkle nodes +//! (identified by node+0x2C & 0x8). + +const std = @import("std"); +const hook = @import("zhook"); +const logging = @import("../logging.zig"); +const mod_mutex = @import("../mutex.zig"); + +pub const module_name: [*:0]const u8 = "superweirdo"; + +// -- Sparkle cleanup logic ---------------------------------------------------- + +/// Walk obj+0xB4 render node linked list. Find the first node with flag 0x8 +/// set at node+0x2C (visual attachment marker), unlink and release it. +fn cleanupSparkleNodes(obj: u32) void { + var node: u32 = hook.readMem(u32, obj + 0xB4); + while (node != 0) { + const flags: u8 = hook.readMem(u8, node + 0x2C); + if (flags & 0x8 != 0) { + // UnlinkObjectFromList: __thiscall(ECX=node) + hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6203a0, .{node}); + // DecrementRefCountAndCleanup: __thiscall(ECX=node) + hook.call(fn (u32) callconv(hook.cc.thiscall) void, 0x6210e0, .{node}); + return; + } + node = hook.readMem(u32, node + 0x8C); + } +} + +// -- Detour hooks ------------------------------------------------------------- + +// Hook 1: UpdateGameObjectAnimationAndDB (0x5f7c40) +// __thiscall(ECX=this), 3 stack params, RET 0xC +const GoAnimFn = fn (u32, u32, u32, u32) callconv(hook.cc.thiscall) void; +var go_anim_hook: hook.Detour(GoAnimFn) = .{}; + +fn goAnimDetour(this: u32, p1: u32, p2: u32, p3: u32) callconv(hook.cc.thiscall) void { + go_anim_hook.callOriginal(.{ this, p1, p2, p3 }); + cleanupSparkleNodes(this); +} + +// Hook 2: GO update function (0x5f7ed0) +// __thiscall(ECX=this), no stack params, RET +const GoThiscallFn = fn (u32) callconv(hook.cc.thiscall) void; +var go_update_hook: hook.Detour(GoThiscallFn) = .{}; + +fn goUpdateDetour(this: u32) callconv(hook.cc.thiscall) void { + go_update_hook.callOriginal(.{this}); + cleanupSparkleNodes(this); +} + +// Hook 3: GameObjectCacheCallback (0x5f7e40) +// __thiscall(ECX=this), no stack params, RET +var go_cache_hook: hook.Detour(GoThiscallFn) = .{}; + +fn goCacheDetour(this: u32) callconv(hook.cc.thiscall) void { + cleanupSparkleNodes(this); + go_cache_hook.callOriginal(.{this}); +} + +// -- Byte patches ------------------------------------------------------------- + +const Patch = struct { + addr: usize, + old: []const u8, + new: []const u8, + desc: []const u8, +}; + +const byte_patches = [_]Patch{ + // UpdateUnitStatusFlags: init flags to 0x2000 (bit 13) instead of 0 + .{ + .addr = 0x0060c560, + .old = &.{ 0x00, 0x00, 0x00, 0x00 }, + .new = &.{ 0x00, 0x20, 0x00, 0x00 }, + .desc = "status flags init 0x2000", + }, + // AttachSpellVisualToUnit: type mask 0x8 (Unit) -> 0x1 (Object base) + .{ + .addr = 0x0061fc1c, + .old = &.{ 0xB9, 0x08, 0x00, 0x00 }, + .new = &.{ 0xB9, 0x01, 0x00, 0x00 }, + .desc = "visual attach type mask", + }, + // Data value at 0x838f7c: 0x14 -> 0x0D + .{ + .addr = 0x00838f7c, + .old = &.{ 0x14, 0x00, 0x00, 0x00 }, + .new = &.{ 0x0D, 0x00, 0x00, 0x00 }, + .desc = "sparkle data constant", + }, +}; + +// -- Module interface --------------------------------------------------------- + +var g_mutex: ?*anyopaque = null; +var g_is_hook_owner: bool = false; +var g_patches_applied: [byte_patches.len]bool = .{false} ** byte_patches.len; +var log: logging.Logger = .{}; + +pub fn isActive() bool { + return g_is_hook_owner; +} + +pub fn installHooks() void { + const result = mod_mutex.acquire(module_name); + g_mutex = result.handle; + g_is_hook_owner = result.is_owner; + if (!g_is_hook_owner) return; + + log = logging.Logger.open(module_name, .console); + + // Byte patches + inline for (byte_patches, 0..) |p, i| { + const current = hook.readMem([p.old.len]u8, p.addr); + if (std.mem.eql(u8, ¤t, p.old)) { + hook.writeProtected(p.addr, p.new); + g_patches_applied[i] = true; + log.fmt("{s}: patched\n", .{p.desc}); + } else if (std.mem.eql(u8, ¤t, p.new)) { + g_patches_applied[i] = true; + log.fmt("{s}: already active\n", .{p.desc}); + } else { + log.fmt("{s}: unexpected bytes at 0x{x}, skipping\n", .{ p.desc, p.addr }); + } + } + + // Detour hooks for GO sparkle cleanup + if (go_anim_hook.attach(0x5f7c40, &goAnimDetour) != .ok) { + log.print("WARN: failed to attach goAnimDetour\n"); + } + if (go_update_hook.attach(0x5f7ed0, &goUpdateDetour) != .ok) { + log.print("WARN: failed to attach goUpdateDetour\n"); + } + if (go_cache_hook.attach(0x5f7e40, &goCacheDetour) != .ok) { + log.print("WARN: failed to attach goCacheDetour\n"); + } + log.print("GO cleanup hooks installed\n"); +} + +pub fn removeHooks() void { + if (!g_is_hook_owner) return; + + go_cache_hook.detach(); + go_update_hook.detach(); + go_anim_hook.detach(); + + inline for (byte_patches, 0..) |p, i| { + if (g_patches_applied[i]) { + hook.writeProtected(p.addr, p.old); + g_patches_applied[i] = false; + log.fmt("{s}: restored\n", .{p.desc}); + } + } + + log.close(); + mod_mutex.release(&g_mutex); + g_is_hook_owner = false; +} diff --git a/src/transform44/PERF_ANALYSIS.md b/src/transform44/PERF_ANALYSIS.md new file mode 100644 index 0000000..6dddbbb --- /dev/null +++ b/src/transform44/PERF_ANALYSIS.md @@ -0,0 +1,101 @@ +# WoW 1.12.1 CPU Profiling Analysis + +Source: `perf.data.perfparser` (July 2025 recording via hotspot) +Exported: `cycles.out` (stack-collapsed format) + +## Full CPU Time Breakdown + +| % | Category | Notes | +|---|---|---| +| 33.18% | **Hooked WoW functions** | 38 hooks in transform44 module | +| 19.19% | GPU/Driver | d3d9.dll (DXVK) + amdvlk32.so -- untouchable | +| 18.74% | WoW long tail | ~2000+ functions each <0.15% -- not worth hooking | +| 13.15% | WoW mid-tier | 50 functions at 0.15-0.39% -- hookable but diminishing returns | +| 8.43% | Lua VM | lua_vm_execute, luaS_newlstr, etc. -- interpreter overhead | +| 5.38% | Unresolved | WoW.exe code not in Ghidra symbol map | +| 1.93% | Wine/System | ntdll, kernel32, wine internals | + +## Hooked Functions (38 total, by self-time %) + +### Already existed (6 hooks) +| % | Address | Name | Convention | +|---|---|---|---| +| 2.80% | 0x714260 | transformMatrix4x4 | thiscall RET 0x10 | +| 2.06% | 0x713d50 | findInterpolationIndices | thiscall RET 0x10 | +| 0.73% | 0x707680 | renderFrame | thiscall RET 0x4 | +| 0.44% | 0x713ea0 | interpolateAnimationKeyframes | fastcall RET 0x8 | +| - | 0x708900 | executeSceneRenderPass | thiscall RET 0x4 | +| - | 0x76FB00 | RenderTextureQuads | fastcall RET | +| - | 0x616620 | CMovement::Process | thiscall RET 0x8 | + +### New perf-identified hotspots (28 hooks) +| % | Address | Name | Convention | +|---|---|---|---| +| 3.95% | 0x6318c0 | ClipPolygonToSinglePlane | stdcall RET 0x4 | +| 3.65% | 0x5ca2d0 | GetOrCreateCharacterGlyph | stdcall RET 0x8 | +| 1.73% | 0x7b2a50 | RenderParticleSprites | thiscall RET 0x8 | +| 1.57% | 0x6abc40 | processLinkedListCollision | thiscall RET 0x8 | +| 1.18% | 0x6afad0 | UpdateEntityAndChunksPositions | thiscall RET | +| 1.15% | 0x765650 | renderAllFrameLayers | thiscall RET 0x4 | +| 1.07% | 0x5ccbe0 | RenderTextToVertexBuffer | thiscall RET 0x18 | +| 1.02% | 0x58a3d0 | RenderComplexGeometry | stdcall RET 0x24 | +| 0.93% | 0x6c1f70 | updateEntitiesInBounds | thiscall RET 0x4 | +| 0.88% | 0x5cdf40 | updateTextFrameCounter | thiscall RET | +| 0.66% | 0x6816f0 | AddToSpatialGrid | thiscall RET | +| 0.65% | 0x7c29f0 | ray_tri_intersect_idx_ushort | stdcall RET 0x10 | +| 0.64% | 0x710b90 | ManageLinkedListNode | thiscall RET 0x4 | +| 0.63% | 0x7b9b10 | calculateColorValues | thiscall RET 0x18 | +| 0.61% | 0x686640 | SetVector3 | thiscall RET | +| 0.59% | 0x6b8c60 | PerformSpatialCulling | thiscall RET 0x8 | +| 0.59% | 0x6b88e0 | performCollisionDetection | thiscall RET 0x8 | +| 0.55% | 0x7b5a10 | ProcessActiveParticles | stdcall RET 0x8 | +| 0.53% | 0x404130 | CallbackIterator | stdcall RET 0x10 | +| 0.50% | 0x464890 | FindObjectByGUID | stdcall RET 0x8 | +| 0.49% | 0x632700 | RayTriangleIntersection | thiscall RET 0x20 | +| 0.48% | 0x70cb30 | DrawBatchProj | thiscall RET | +| 0.47% | 0x702000 | FindLuaFunction | stdcall RET 0x4 | +| 0.44% | 0x5a0f50 | RenderSpriteQuads | thiscall RET 0xc | +| 0.44% | 0x718960 | renderSceneNode | thiscall RET | +| 0.44% | 0x6cffc0 | generateTerrainChunk | thiscall RET | +| 0.43% | 0x593840 | D3D_SetTexture | thiscall RET 0x8 | +| 0.42% | 0x6b8b70 | checkBoundingBoxIntersection | stdcall RET 0x8 | + +### Unresolved-callee hooks (4 hooks) +| % | Address | Name | Convention | +|---|---|---|---| +| ~0.5% | 0x7bdd60 | rotateMatrixByAxisAngle | thiscall RET 0xc | +| ~0.1% | 0x632460 | BuildTrianglePlanes | thiscall RET 0xc | +| ~0.2% | 0x7b3d20 | SetupParticleRendering | thiscall RET 0x4 | +| ~0.2% | 0x5ce0c0 | renderTextLine | thiscall RET 0x10 | + +## Unhooked Mid-Tier (50 functions, 13.15% total) + +Not worth individual hooks -- too small or too high-frequency (hook overhead would distort): + +| % | Name | Why not hook | +|---|---|---| +| 0.39% | compareRenderItemsExtended | Sort comparator, millions of calls | +| 0.39% | GetCachedData | Cache accessor, extremely hot path | +| 0.39% | raycastPickObjects | Moderate frequency | +| 0.35% | inflateDecodeLiteralsAndLengths | Decompression, bursty | +| 0.34% | UpdateParticlePhysics | Per-particle, very hot | +| 0.34% | updateAnimationSystem | Could be interesting entry point | +| 0.32% | ClntObjMgrObjectPtr | Object lookup, called everywhere | +| 0.29% | multiplyMatrix4x4 | Tiny function, massive call count | +| 0.26% | quickSortArray | Sort impl, millions of comparisons | +| ... | (40 more at 0.15-0.31%) | | + +## Key Insights + +1. **GPU/Driver is 19%** -- nothing we can do about d3d9.dll/amdvlk overhead +2. **Lua VM is 8.4%** -- addon code execution, not optimizable from DLL side +3. **Frustum clipping (ClipPolygonToSinglePlane) is the #1 WoW hotspot at 3.95%** -- pure math, SSE candidate via binary patch (no hook overhead) +4. **Font rendering (GetOrCreateCharacterGlyph) is #2 at 3.65%** -- potential cache optimization +5. **Bone pipeline (t44 + findInterp + interpKf) totals ~5.3%** -- SSE interp hook overhead negates savings; binary patch or full t44 rewrite needed +6. **Hook overhead matters** -- for functions called >10k/frame (lerp, matrix multiply, sort comparators), detour trampoline cost (~30 cycles) exceeds any savings + +## Files + +- `cycles.out` -- raw stack-collapsed perf data +- `perf.data.perfparser` -- hotspot binary cache (5.3GB) +- Ghidra symbols: `/media/faststore/tmp/Dis/symbols.nm` diff --git a/src/transform44/bone_sse_baseline.zig b/src/transform44/bone_sse_baseline.zig index 76340f1..f4f9b61 100644 --- a/src/transform44/bone_sse_baseline.zig +++ b/src/transform44/bone_sse_baseline.zig @@ -1007,7 +1007,7 @@ fn calcScaledInverse(this_mat: u32, out: u32, scale: f32) void { // mat3(offset_vec3*), mat4(scale_float_bits) // ============================================================================= -export fn transformImpl_BASELINE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.c) void { +export fn transformImpl_BASELINE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void { @setEvalBranchQuota(50000); // ========================================================================= diff --git a/src/transform44/decompiled/decomp_RTQ_pipeline.c b/src/transform44/decompiled/decomp_RTQ_pipeline.c new file mode 100644 index 0000000..23c634f --- /dev/null +++ b/src/transform44/decompiled/decomp_RTQ_pipeline.c @@ -0,0 +1,167 @@ +// RTQ (RenderTextureQuads) full rendering pipeline +// All functions in the per-item draw call chain +// +// Call chain per item: +// RenderTextureQuads (0x76FB00) -- the outer loop +// -> InitializeRenderingPipeline (0x58A2A0) -- stores vertCount, calls RenderComplexGeometry +// -> RenderComplexGeometry (0x58A3D0) -- format detect, VB create/fill, DrawPrimitive +// -> CreateVertexBuffer (0x58A140 -> 0x594500) -- pool VB allocation +// -> LockVertexBuffer (0x58A080) -- vtable call to CGxDevice+0xA8 +// -> [per-vertex interleave loop] +// -> UnlockVertexBuffer (0x58A0A0) -- vtable call to CGxDevice+0xAC +// -> DrawPrimitive (0x58A7C0) -- state update + SetRenderingCommand +// -> RenderVertexBuffer (0x58A2E0) -- creates INDEX VB, issues DrawIndexedPrimitive +// -> CreateAndBindVertexBuffer (0x58A750) -- index VB (bufferType=1) +// -> CallGfxDeviceMethod_Wrapper (0x58A830) -- actual D3D9 DrawIndexedPrimitive +// -> EmptyRenderFunction (0x58A340) -- RET (no-op) + +// === InitializeRenderingPipeline (0x58A2A0) -- 54 bytes === +// __fastcall(ECX=vertCount, EDX=xyzPtr, 11 stack params) +// Stores vertCount to global g_currentPrimitiveType and tail-calls RenderComplexGeometry. +void __fastcall InitializeRenderingPipeline(int vertCount, void** xyzPtr, /* ...11 stack params */) +{ + g_currentPrimitiveType = (void*)vertCount; // stored at some global + RenderComplexGeometry(vertCount, xyzPtr, /* forward all params */); +} + +// === CreateVertexBuffer wrapper (0x58A140) -- 24 bytes === +// __fastcall(ECX=bufferType, EDX=vertexSize, stack: vertexCount) +// Thin wrapper: loads CGxDevice from global, calls D3D_CreateVertexBuffer. +void __fastcall CreateVertexBuffer(int bufferType, int vertexSize, int vertexCount) +{ + D3D_CreateVertexBuffer(CGxDeviceD3d__device, bufferType, vertexSize, vertexCount); +} + +// === D3D_CreateVertexBuffer (0x594500) -- 75 bytes === +// __thiscall(ECX=CGxDevice, stack: bufferType, vertexSize, vertexCount) +// Returns a pool handle. The pool is indexed by bufferType at CGxDevice+0x26CC. +// Reuses existing D3D9 VB unless the requested size exceeds current allocation. +int __thiscall D3D_CreateVertexBuffer(void* this, int bufferType, int vertexSize, int vertexCount) +{ + int poolHandle = *(int*)((int)this + bufferType * 4 + 0x26CC); + int d3dVB = *(int*)(poolHandle + 8); + if (d3dVB != 0 && *(uint*)(d3dVB + 0x10) < (uint)(vertexSize * vertexCount)) { + // Existing VB too small -- resize via vtable call + (*(code**)(*this + 0xA0))(d3dVB, vertexSize * vertexCount); + } + SetDimensionsAndSize(poolHandle, vertexSize, vertexCount); + return poolHandle; +} + +// === SetDimensionsAndSize (0x5946F0) -- 32 bytes === +void SetDimensionsAndSize(int handle, int vertexSize, int vertexCount) +{ + *(int*)(handle + 0x0C) = vertexSize; + *(int*)(handle + 0x10) = vertexCount; + *(int*)(handle + 0x14) = vertexSize * vertexCount; + *(char*)(handle + 0x1C) = 0; // clear dirty flag +} + +// === LockVertexBuffer (0x58A080) -- 18 bytes === +// __fastcall(ECX=poolHandle) +// Calls CGxDevice vtable[0xA8/4 = 42] to lock the D3D9 VB. +// Returns pointer to locked VB memory. +void* __fastcall LockVertexBuffer(void* poolHandle) +{ + return (*(code**)(*(int*)CGxDeviceD3d__device + 0xA8))(poolHandle); +} + +// === UnlockVertexBuffer (0x58A0A0) -- 27 bytes === +// __fastcall(ECX=poolHandle, EDX=byteCount) +// Calls CGxDevice vtable[0xAC/4 = 43] to unlock, then marks pool handle active. +void __fastcall UnlockVertexBuffer(int poolHandle, int byteCount) +{ + (*(code**)(*(int*)CGxDeviceD3d__device + 0xAC))(poolHandle, byteCount); + SetObjectActiveFlag(poolHandle); +} + +// === UpdateBufferData (0x58A0C0) -- 57 bytes === +// __fastcall(ECX=poolHandle, EDX=srcData, stack: byteCount, unused) +// Used by CreateAndBindVertexBuffer for INDEX buffer filling. +// If byteCount==0, auto-computes from handle's vertexSize*vertexCount. +void __fastcall UpdateBufferData(int poolHandle, void* srcData, int byteCount, void* unused) +{ + if (byteCount == 0) { + byteCount = *(int*)(poolHandle + 0x10) * *(int*)(poolHandle + 0x0C); + } + (*(code**)(*(int*)CGxDeviceD3d__device + 0xB0))(poolHandle, srcData, byteCount, unused); + SetObjectActiveFlag(poolHandle); +} + +// === DrawPrimitive / GxDevice dispatch (0x58A7C0) -- 54 bytes === +// __fastcall(ECX=poolHandle, EDX=formatCode) +// Reads per-format state from table at 0x809C00 (16 bytes per entry): +// +0x00: ptr to state array (GxDevice render state descriptors) +// +0x04: state array element count +// +0x08: primitive type mapping +// +0x0C: dirty flags bitmask +// Applies state, marks dirty, stores draw command in CGxDevice for D3D9 submission. +void __fastcall DrawPrimitive(int poolHandle, int formatCode) +{ + int* stateTable = (int*)(0x809C00 + formatCode * 0x10); + UpdateGfxStateArray(poolHandle, stateTable[0], stateTable[1]); + MarkStateDirty(stateTable[3]); + SetRenderingCommand(CGxDeviceD3d__device, poolHandle, formatCode); +} + +// === SetRenderingCommand (0x592AA0) -- 40 bytes === +// __thiscall(ECX=CGxDevice, stack: poolHandle, formatCode) +// Stores the draw command into CGxDevice for later D3D9 submission. +void __thiscall SetRenderingCommand(void* this, void* poolHandle, int formatCode) +{ + *(int*)((int)this + 0x27E0) = formatCode; + *(void**)((int)this + 0x27E4) = poolHandle; + *(int*)((int)this + 0x27E8) = *(int*)(0x809C08 + formatCode * 0x10); +} + +// === RenderVertexBuffer (0x58A2E0) -- 82 bytes === +// __fastcall(ECX=primType, EDX=vertCount, stack: indexPtr) +// Creates index VB (bufferType=1), binds it, issues DrawIndexedPrimitive. +void __fastcall RenderVertexBuffer(int primType, int vertCount, void* indexPtr) +{ + if (g_currentPrimitiveType != NULL) { + CreateAndBindVertexBuffer(vertCount, indexPtr); // index VB + short adjustedPrimType = (short)g_currentPrimitiveType - 1; + // Build draw call struct on stack + struct { void* primPtr; void* unused; short vertCount; short flags; } call; + call.primPtr = primType; + call.unused = NULL; + call.vertCount = vertCount; + call.flags = 0; + CallGfxDeviceMethod_Wrapper(&call, 1); // -> D3D9 DrawIndexedPrimitive + } +} + +// === CreateAndBindVertexBuffer (0x58A750) -- 44 bytes === +// __fastcall(ECX=vertCount, EDX=dataPtr) +// Used for INDEX buffer (bufferType=1, vertexSize=2 = sizeof(u16)). +void __fastcall CreateAndBindVertexBuffer(int vertCount, void* dataPtr) +{ + int handle = CreateVertexBuffer(1, 2, vertCount); // pool slot 1, 2 bytes/index + UpdateBufferData(handle, dataPtr, 0, NULL); + SetStreamSource(handle); // bind as index stream +} + +// === Vertex format tables (from game memory) === +// +// Stride table at 0x85A7A8 (indexed by format code): +// fmt 0: stride=12 xyz only +// fmt 1: stride=24 xyz + defaultTC +// fmt 2: stride=28 xyz + defaultTC + additional(4) +// fmt 3: stride=32 xyz + defaultTC + texcoord1(8) +// fmt 4: stride=36 xyz + defaultTC + additional(4) + texcoord1(8) +// fmt 5: stride=40 xyz + defaultTC + texcoord1(8) + texcoord2(8) +// fmt 6: stride=44 xyz + defaultTC + additional(4) + texcoord1(8) + texcoord2(8) +// fmt 7: stride=16 xyz + additional(4) +// fmt 8: stride=24 xyz + additional(4) + texcoord1(8) +// fmt 9: stride=32 xyz + additional(4) + texcoord1(8) + texcoord2(8) +// fmt 10: stride=20 xyz + texcoord1(8) +// fmt 11: stride=28 xyz + texcoord1(8) + texcoord2(8) +// +// Element offset table at 0x8097A8 (indexed by format*13 + element): +// Element 0 = xyz position (always at offset 0) +// Element 3 = defaultTexCoord +// Element 4 = additionalData (color) +// Element 5 = texcoord1 +// Element 6 = texcoord2 +// Value -1 = element not present in this format diff --git a/src/transform44/decompiled/decomp_RenderComplexGeometry.c b/src/transform44/decompiled/decomp_RenderComplexGeometry.c new file mode 100644 index 0000000..ff1b6bd --- /dev/null +++ b/src/transform44/decompiled/decomp_RenderComplexGeometry.c @@ -0,0 +1,166 @@ +// RenderComplexGeometry (0x58A3D0) -- 886 bytes +// __fastcall(ECX=vertCount, EDX=xyzPtr, 11 stack params) +// +// Called from InitializeRenderingPipeline (0x58A2A0) which just stores +// vertCount to global [0xC0ED2C] then tail-calls this function. +// +// Purpose: Determine vertex format from which input pointers are non-null, +// create/reuse a pool VB, lock it, interleave all input arrays into the VB +// at the computed stride, unlock, then issue a DrawPrimitive. +// +// Parameters (after fastcall mapping): +// param_1 = vertCount (ECX) +// param_2 = xyzPtr (EDX) -- 3 floats per vert, stride in param_3 +// param_3 = xyzStride (stack) -- typically 0x0C (12 bytes) +// param_4 = defaultTexCoordPtr (stack) -- game constant at 0xCF4CF4, 3 floats/vert +// param_5 = defaultTexCoordStride (stack) -- 0 means use param_4 as single value +// param_6 = additionalDataPtr (stack) -- per-vert color (DWORD), or NULL +// param_7 = additionalDataStride (stack) +// param_8 = texCoord1Ptr (stack) -- unused in RTQ path +// param_9 = texCoord1Stride (stack) +// param_10 = texCoord2Ptr (stack) -- UV coords, 2 floats/vert +// param_11 = texCoord2Stride (stack) -- typically 8 +// +// Vertex format table: +// Format codes 0-11, determined by which of param_4/6/8/10 are non-null. +// Stride table at 0x85A7A8: [12,24,28,32,36,40,44,16,24,32,20,28] +// Element offset table at 0x8097A8: indexed by (format*13 + element)*4 +// Elements: 0=xyz, 3=defaultTC, 4=additional, 5=texcoord1, 6=texcoord2 +// +// For RTQ with additionalData (format 4, stride 36): +// [0-11] xyz (12 bytes, 3 floats) +// [12-23] defaultTC (12 bytes, 3 floats from 0xCF4CF4) +// [24-27] additional (4 bytes, DWORD color) +// [28-35] texcoord2 (8 bytes, 2 floats UV) +// +// For RTQ without additionalData (format 1, stride 24): +// [0-11] xyz (12 bytes) +// [12-23] defaultTC (12 bytes) +// +// Per-vertex loop: +// 1. Copy xyz (3 dwords) at element offset 0 +// 2. Copy defaultTC (3 dwords) at element offset 3 (12 bytes) +// 3. Check lighting flag at CGxDevice+0x258: +// - If flag == 1: byte-swap color (BGRA -> RGBA or similar) +// - Else: copy color as-is +// 4. Copy additional data (1 dword) at element offset 4 +// 5. Copy texcoord1 (2 dwords) at element offset 5 +// 6. Copy texcoord2 (2 dwords) at element offset 6 +// Each pointer advances by its respective stride per vertex. +// VB write pointer advances by the interleaved stride per vertex. +// +// After loop: UnlockVertexBuffer, then DrawPrimitive(poolHandle, formatCode) +// +// DrawPrimitive (0x58A7C0): +// Reads from a per-format-code table at 0x809C00 (stride 0x10): +// +0x00: state array ptr +// +0x04: state array count +// +0x08: D3D primitive type table index +// +0x0C: dirty flags mask +// Calls UpdateGfxStateArray to apply GxDevice state changes, +// then MarkStateDirty, then SetRenderingCommand which stores +// the draw command into CGxDevice+0x27E0..0x27E8 for later +// submission to D3D9. +// +// Pool VB system (D3D_CreateVertexBuffer at 0x594500): +// CGxDevice has pool slots at +0x26CC indexed by bufferType. +// RenderComplexGeometry uses bufferType=0. +// Pool handle struct: +// +0x08: D3D9 IDirect3DVertexBuffer9* +// +0x0C: vertexSize (bytes per vertex for this format) +// +0x10: vertexCount +// +0x14: total bytes (vertexSize * vertexCount) +// +0x1C: dirty flag (cleared by SetDimensionsAndSize) +// The pool VB is reused across calls. Only recreated if the +// requested size (vertexSize * vertexCount) exceeds the current +// allocation at +0x14. This means no D3D9 CreateVertexBuffer +// overhead on normal frames. +// +// RenderVertexBuffer (0x58A2E0): +// Called AFTER RenderComplexGeometry returns. Uses a DIFFERENT pool +// (bufferType=1) for index data. Creates/binds an index VB with +// the quad indices {0,1,2,0,2,3}, then calls DrawIndexedPrimitive +// via CallGfxDeviceMethod_Wrapper. + +void __fastcall +RenderComplexGeometry( + int vertCount, // ECX + void** xyzPtr, // EDX + int xyzStride, // [ebp+0x08] + void** defaultTCPtr, // [ebp+0x0C] + int defaultTCStride, // [ebp+0x10] + void** additionalPtr, // [ebp+0x14] + int additionalStride, // [ebp+0x18] + void** texCoord1Ptr, // [ebp+0x1C] + int texCoord1Stride, // [ebp+0x20] + void** texCoord2Ptr, // [ebp+0x24] + int texCoord2Stride) // [ebp+0x28] +{ + // Step 1: Format detection -- determine which of the 12 interleaved + // vertex formats to use based on which input pointers are non-null. + int formatCode = 1; // default: xyz + defaultTC + // Complex nested-if tree mapping (defaultTC, additional, tc1, tc2) + // presence to format codes 0-11. See format table above. + + // Step 2: Get stride and allocate pool VB + int stride = GetDataPointerByIndex(formatCode); // stride table lookup + void* poolHandle = CreateVertexBuffer(0, stride, vertCount); + char* vbData = LockVertexBuffer(poolHandle); + + // Step 3: Compute write pointers for each element within the VB + char* xyzDst = vbData + GetMatrixElementPointer(formatCode, 0); + char* tcDst = (defaultTCPtr) ? vbData + GetMatrixElementPointer(formatCode, 3) : &dummy; + char* addDst = (additionalPtr) ? vbData + GetMatrixElementPointer(formatCode, 4) : &dummy; + char* tc1Dst = (texCoord1Ptr) ? vbData + GetMatrixElementPointer(formatCode, 5) : &dummy; + char* tc2Dst = (texCoord2Ptr) ? vbData + GetMatrixElementPointer(formatCode, 6) : &dummy; + + // Per-element advance: stride if present, 0 if writing to dummy + int tcAdv = defaultTCPtr ? stride : 0; + int addAdv = additionalPtr ? stride : 0; + int tc1Adv = texCoord1Ptr ? stride : 0; + int tc2Adv = texCoord2Ptr ? stride : 0; + + // Step 4: Per-vertex interleave loop + for (int v = 0; v < vertCount; v++) { + // XYZ: always 12 bytes (3 floats) + *(int*)(xyzDst + 0) = *(int*)(xyzPtr + 0); + *(int*)(xyzDst + 4) = *(int*)(xyzPtr + 4); + *(int*)(xyzDst + 8) = *(int*)(xyzPtr + 8); + xyzDst += stride; + xyzPtr += xyzStride; + + // DefaultTC: 12 bytes (3 floats) + *(int*)(tcDst + 0) = *(int*)(defaultTCPtr + 0); + *(int*)(tcDst + 4) = *(int*)(defaultTCPtr + 4); + *(int*)(tcDst + 8) = *(int*)(defaultTCPtr + 8); + tcDst += tcAdv; + defaultTCPtr += defaultTCStride; + + // Additional (color): conditional byte-swap based on lighting flag + int lightingInfo = UpdateLightingOffset(); // returns CGxDevice + 0x23C + if (*(int*)(lightingInfo + 0x1C) == 1) { + // Byte swap: BGRA -> RGBA (swap bytes 0 and 2) + *(int*)(addDst) = CONCAT(byte3, byte0, byte1, byte2); + } else { + *(int*)(addDst) = *(int*)(additionalPtr); + } + addDst += addAdv; + additionalPtr += additionalStride; + + // TexCoord1: 8 bytes (2 floats) + *(int*)(tc1Dst + 0) = *(int*)(texCoord1Ptr + 0); + *(int*)(tc1Dst + 4) = *(int*)(texCoord1Ptr + 4); + tc1Dst += tc1Adv; + texCoord1Ptr += texCoord1Stride; + + // TexCoord2: 8 bytes (2 floats) + *(int*)(tc2Dst + 0) = *(int*)(texCoord2Ptr + 0); + *(int*)(tc2Dst + 4) = *(int*)(texCoord2Ptr + 4); + tc2Dst += tc2Adv; + texCoord2Ptr += texCoord2Stride; + } + + // Step 5: Finalize + UnlockVertexBuffer(poolHandle, stride * vertCount); + DrawPrimitive(poolHandle, formatCode); +} diff --git a/src/transform44/decompiled/decomp_RenderTextToVertexBuffer.c b/src/transform44/decompiled/decomp_RenderTextToVertexBuffer.c new file mode 100644 index 0000000..2192927 --- /dev/null +++ b/src/transform44/decompiled/decomp_RenderTextToVertexBuffer.c @@ -0,0 +1,1044 @@ +// ======================================================== +// RenderTextToVertexBuffer -- 0x005ccbe0 +// Function: RenderTextToVertexBuffer +// Range: 0x005ccbe0 - 0x005cd29d (1726 bytes) +// Ghidra signature: undefined RenderTextToVertexBuffer(void * this, byte * param_1, int param_2, uint * param_3, float * param_4, uint * param_5, int * param_6) +// Calling convention (Ghidra): __thiscall +// ======================================================== + +// ===================== DECOMPILATION ===================== + +/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */ + +void __thiscall +RenderTextToVertexBuffer + (void *this,byte *param_1,int param_2,uint *param_3,float *param_4,uint *param_5, + int *param_6) + +{ + float fVar1; + uint uVar2; + undefined *puVar3; + char *pcVar4; + float10 *__return_storage_ptr__; + uint *puVar5; + undefined4 *puVar6; + byte bVar7; + undefined *puVar8; + uint uVar9; + int iVar10; + undefined **ppuVar11; + uint uVar12; + undefined4 unaff_ESI; + char *string1; + int *piVar13; + float10 *unaff_EDI; + int *piVar14; + float10 extraout_ST0; + float10 extraout_ST0_00; + float10 extraout_ST0_01; + float10 extraout_ST0_02; + float10 fVar15; + float10 extraout_ST0_03; + float10 extraout_ST0_04; + ulonglong uVar16; + float in_stack_ffffff3c; + float in_stack_ffffff40; + float in_stack_ffffff44; + float in_stack_ffffff48; + undefined *local_98; + undefined *local_94; + byte local_90; + undefined *local_8c; + byte local_88; + undefined *local_84; + byte local_80; + undefined *local_7c; + undefined4 local_78; + undefined4 uStack_74; + uint local_70; + undefined4 uStack_6c; + undefined *local_68; + undefined *local_64; + undefined *local_60; + undefined *local_5c; + undefined *local_58; + undefined *local_54; + undefined **ppuStack_50; + undefined *local_4c; + undefined *local_48; + undefined *local_44; + undefined *local_40; + undefined *local_3c; + undefined *local_38; + undefined *local_34; + undefined *local_30; + undefined *local_2c; + undefined *local_28; + undefined *local_24; + undefined *local_20; + undefined *local_1c; + undefined *local_18; + undefined *local_14; + undefined *local_10; + undefined *local_c; + undefined *local_8; + + local_18 = (undefined *)this; + if ((*(byte *)((int)this + 0x5c) & 8) != 0) { + local_c = (undefined *)((int)this + 0xa0); + local_10 = &DAT_00000008; + do { + iVar10 = *(int *)local_c; + if (iVar10 != 0) { + if (*(int *)(iVar10 + 0x1c) != 0) { + uVar12 = 0; + do { + conditionalFree((void *)(*(int *)(iVar10 + 0x20) + uVar12 * 4),0); + uVar12 = uVar12 + 1; + } while (uVar12 < *(uint *)(iVar10 + 0x1c)); + } + *(undefined4 *)(iVar10 + 0x1c) = 0; + } + local_c = local_c + 4; + local_10 = local_10 + -1; + } while (local_10 != (undefined *)0x0); + } + local_38 = (undefined *)*param_4; + local_34 = (undefined *)param_4[1]; + local_30 = (undefined *)param_4[2]; + local_8 = (undefined *)0x0; + ConvertPixelsToScreen + ((void *)(*(uint *)((int)this + 0x5c) >> 7 & 1),*(float10 **)((int)this + 0x1c), + (float)unaff_EDI); + local_48 = (undefined *)(float)extraout_ST0; + local_54 = GetTextureSize(*(int *)((int)this + 0x44)); + ppuStack_50 = (undefined **)0x0; + local_10 = (undefined *)((float)local_48 / (float)local_54); + ConvertPixelsToScreen + ((void *)(*(uint *)((int)this + 0x5c) >> 7 & 1),*(float10 **)((int)this + 0x1c), + (float)unaff_EDI); + local_14 = (undefined *)(float)extraout_ST0_00; + uVar12 = *(uint *)(*(int *)((int)this + 0x44) + 0x180); + fVar1 = _DAT_0080306c; + if (((uVar12 & 8) != 0) || (fVar1 = _DAT_00801628, (uVar12 & 1) != 0)) { + local_14 = (undefined *)((float)local_14 + fVar1); + } + if (*param_6 == 2) { + param_6[4] = (int)local_38; + } + if ((DAT_00c2b9f0 & 1) == 0) { + DAT_00c2b9f0 = DAT_00c2b9f0 | 1; + _DAT_00c2b9f8 = 0.001953125; + _DAT_00c2b9fc = 0.001953125; + _DAT_00c2ba00 = -0.001953125; + _DAT_00c2ba04 = -0.001953125; + validateMemoryOperation((int *)&DAT_005cd2c0); + } + bVar7 = *param_1; + local_3c = (undefined *)0x0; + param_4 = (float *)0x0; + while ((bVar7 != 0 && (param_2 != 0))) { + local_40 = (undefined *)0x0; + puVar3 = ParseTextFormatCodes + (param_1,&local_44,(uint *)&local_40,*(uint *)((int)this + 0x5c), + (uint *)&local_c); + param_1 = param_1 + (int)local_44; + param_2 = param_2 - (int)local_44; + if (local_8 != (undefined *)0x0) { + if ((*(byte *)((int)this + 0x5c) & 0x10) == 0) { + GetOrCreateCharacterGlyph + (*(void **)((int)this + 0x44),(float10 *)local_8,(float10 *)local_c,unaff_EDI); + fVar15 = extraout_ST0_02; + } + else { + GetOrCreateKerningPair + (*(void **)((int)this + 0x44),(float10 *)local_8,(float10 *)local_c,unaff_EDI); + fVar15 = extraout_ST0_01; + } + local_3c = (undefined *)(float)fVar15; + } + param_4 = (float *)((float)local_3c * (float)local_10); + switch(puVar3) { + case (undefined *)0x0: + if ((*(byte *)((int)this + 0x5c) & 8) == 0) { + local_40 = (undefined *)CONCAT13(*(undefined1 *)((int)this + 0x2f),local_40._0_3_); + *param_3 = (uint)local_40; + } + break; + case (undefined *)0x1: + *param_3 = *(uint *)((int)this + 0x2c); + break; + case (undefined *)0x2: + break; + default: + __return_storage_ptr__ = + (float10 *)GetOrCreateCharacterTexture(*(void **)((int)this + 0x44),(uint)local_c); + local_1c = (undefined *)__return_storage_ptr__; + if (__return_storage_ptr__ != (float10 *)0x0) { + uVar12 = *(uint *)((int)__return_storage_ptr__ + 0x28); + bVar7 = (byte)uVar12; + *param_5 = *param_5 | 1 << (bVar7 & 0x1f); + if (*(int *)((int)this + uVar12 * 4 + 0xa0) == 0) { + puVar5 = AllocateTextLineTexture(); + *(uint **)((int)this + uVar12 * 4 + 0xa0) = puVar5; + } + puVar3 = *(undefined **)((int)this + uVar12 * 4 + 0xa0); + local_4c = puVar3; + if ((*(byte *)((int)this + 0x5c) & 8) == 0) { + local_8 = (undefined *)(*(int *)(puVar3 + 0x1c) + 4); + if (*(undefined **)(puVar3 + 0x18) < local_8) { + uVar12 = *(uint *)(puVar3 + 0x24); + if (uVar12 == 0) { + uVar12 = CalculateOptimalSize64_2(puVar3 + 0x18,(uint)local_8); + } + uVar12 = AlignToMultiple6((uint)local_8,uVar12); + ResizeVectorArray(puVar3 + 0x18,uVar12); + } + uVar12 = 0; + do { + puVar5 = (uint *)(*(int *)(puVar3 + 0x20) + (*(int *)(puVar3 + 0x1c) + uVar12) * 4); + if (puVar5 != (uint *)0x0) { + *puVar5 = *param_3; + } + uVar12 = uVar12 + 1; + } while (uVar12 < 4); + puVar8 = *(undefined **)(puVar3 + 0x1c); + *(undefined **)(puVar3 + 0x1c) = puVar8 + 4; + __return_storage_ptr__ = (float10 *)local_1c; + if ((*(byte *)((int)this + 0x5c) & 0x20) != 0) { + local_98._0_1_ = bVar7; + local_90 = bVar7; + local_88 = bVar7; + local_80 = bVar7; + local_94 = puVar8; + puVar5 = (uint *)((int)this + 0x8c); + local_7c = puVar8 + 3; + local_84 = puVar8 + 2; + puVar3 = (undefined *)(*(int *)((int)this + 0x90) + 4); + local_8c = puVar8 + 1; + ppuStack_50 = &local_98; + if ((undefined *)*puVar5 < puVar3) { + uVar12 = *(uint *)((int)this + 0x98); + if (uVar12 == 0) { + uVar12 = findPowerOfTwo(puVar5,(uint)puVar3); + } + puVar8 = puVar3; + if ((uint)puVar3 % uVar12 != 0) { + puVar8 = puVar3 + (uVar12 - (uint)puVar3 % uVar12); + } + local_8 = puVar3; + resizeGradientBuffer(puVar5,(uint)puVar8); + } + uVar12 = 0; + ppuVar11 = ppuStack_50; + do { + puVar6 = (undefined4 *) + (*(int *)((int)this + 0x94) + (*(int *)((int)this + 0x90) + uVar12) * 8); + if (puVar6 != (undefined4 *)0x0) { + *puVar6 = *ppuVar11; + puVar6[1] = ppuVar11[1]; + } + ppuVar11 = ppuVar11 + 2; + uVar12 = uVar12 + 1; + } while (uVar12 < 4); + *(int *)((int)this + 0x90) = *(int *)((int)this + 0x90) + 4; + __return_storage_ptr__ = (float10 *)local_1c; + } + } + uVar12 = *(uint *)((int)this + 0x5c); + if (-1 < (char)uVar12) { + uVar16 = __ftol(); + local_78 = (undefined4)uVar16; + uStack_74 = 0; + param_4 = (float *)(float)(uVar16 & 0xffffffff); + } + local_68 = (undefined *)((float)param_4 + (float)local_38); + uVar16 = CONCAT44(*(undefined4 *)((int)this + 0x1c),uVar12 >> 7) & 0xffffffffffffff01; + local_60 = local_30; + local_5c = (undefined *)0x0; + local_58 = (undefined *)0x0; + local_64 = local_34; + local_38 = local_68; + CalculateTextBounds(*(void **)((int)this + 0x44),__return_storage_ptr__,(int)uVar16, + (char)(uVar16 >> 0x20),unaff_EDI); + local_70 = *(uint *)((int)__return_storage_ptr__ + 0x48); + local_68 = (undefined *)(float)(extraout_ST0_03 + (float10)(float)local_38); + uStack_6c = 0; + local_8 = (undefined *) + ((float)*(uint *)((int)__return_storage_ptr__ + 0x48) * (float)local_10); + if (-1 < *(char *)((int)this + 0x5c)) { + truncateFloatWithValidation + (SUB84((double)(float)local_8,0),(double)CONCAT44(unaff_ESI,unaff_EDI)); + local_8 = (undefined *)(float)extraout_ST0_04; + } + puVar3 = local_4c; + if ((*(uint *)(*(int *)((int)this + 0x44) + 0x180) & 8) == 0) { + if ((*(uint *)(*(int *)((int)this + 0x44) + 0x180) & 1) != 0) { + local_64 = (undefined *)((float)local_64 - StaticFloat1_0); + } + } + else { + local_64 = (undefined *)((float)local_64 - _DAT_00801628); + } + puVar8 = local_4c + 8; + uVar12 = *(int *)(local_4c + 0xc) + 4; + local_64 = (undefined *) + ((float)*(int *)(local_1c + 0x58) * (float)local_10 + (float)local_64); + if (*(uint *)(local_4c + 8) < uVar12) { + uVar9 = *(uint *)(local_4c + 0x14); + if (uVar9 == 0) { + if (uVar12 < 0xc) { + uVar9 = uVar12; + for (uVar2 = *(int *)(local_4c + 0xc) + 3U & uVar12; uVar2 != 0; + uVar2 = uVar2 - 1 & uVar2) { + uVar9 = uVar2; + } + if (uVar9 == 0) { + uVar9 = 1; + } + } + else { + *(undefined4 *)(local_4c + 0x14) = 0xc; + uVar9 = 0xc; + } + } + uVar12 = alignToNextMultiple(uVar12,uVar9); + resizeVertexBuffer(puVar8,uVar12); + } + uVar12 = 0; + do { + puVar6 = (undefined4 *) + (*(int *)(puVar3 + 0x10) + (*(int *)(puVar3 + 0xc) + uVar12) * 0x14); + if (puVar6 != (undefined4 *)0x0) { + ppuVar11 = &local_68; + for (iVar10 = 5; iVar10 != 0; iVar10 = iVar10 + -1) { + *puVar6 = *ppuVar11; + ppuVar11 = ppuVar11 + 1; + puVar6 = puVar6 + 1; + } + } + uVar12 = uVar12 + 1; + } while (uVar12 < 4); + iVar10 = *(int *)(puVar3 + 0xc); + *(int *)(puVar3 + 0xc) = iVar10 + 4; + iVar10 = ((iVar10 + 4) * 5 + -0x14) * 4; + puVar6 = (undefined4 *)(*(int *)(local_4c + 0x10) + iVar10); + if ((char)local_18[0x5c] < '\0') { + fVar1 = (float)puVar6[0xf]; + *puVar6 = puVar6[5]; + puVar6[1] = puVar6[0xb]; + puVar6[0xf] = (float)local_8 + fVar1; + puVar6[10] = (float)local_8 + fVar1; + fVar1 = (float)puVar6[0x10]; + puVar6[0x10] = (float)local_48 + fVar1; + puVar6[6] = (float)local_48 + fVar1; + } + else { + *puVar6 = puVar6[5]; + fVar1 = (float)puVar6[0xf]; + puVar6[0xf] = (float)local_8 + fVar1; + puVar6[10] = (float)local_8 + fVar1; + fVar1 = (float)puVar6[0x10]; + puVar6[0x10] = (float)local_14 + fVar1; + puVar6[6] = (float)local_14 + fVar1; + puVar6[1] = puVar6[0xb]; + } + iVar10 = *(int *)(local_4c + 0x10) + iVar10; + local_2c = *(undefined **)(local_1c + 0x60); + local_28 = *(undefined **)(local_1c + 100); + local_24 = *(undefined **)(local_1c + 0x68); + local_20 = *(undefined **)(local_1c + 0x6c); + puVar3 = local_20; + if ((char)local_18[0x5c] < '\0') { + local_2c = (undefined *)((float)local_2c + _DAT_00c2b9f8); + local_28 = (undefined *)((float)local_28 + _DAT_00c2b9fc); + local_24 = (undefined *)((float)local_24 + _DAT_00c2ba00); + puVar3 = (undefined *)((float)local_20 + _DAT_00c2ba04); + } + *(undefined **)(iVar10 + 0x48) = puVar3; + *(undefined **)(iVar10 + 0x34) = puVar3; + *(undefined **)(iVar10 + 0x38) = local_24; + *(undefined **)(iVar10 + 0x10) = local_24; + *(undefined **)(iVar10 + 0x4c) = local_2c; + *(undefined **)(iVar10 + 0x24) = local_2c; + *(undefined **)(iVar10 + 0x20) = local_28; + *(undefined **)(iVar10 + 0xc) = local_28; + local_8 = local_c; + this = local_18; + } + break; + case (undefined *)0x4: + if (*param_6 != 2) { + iVar10 = ((int)param_1 - (int)local_44) + 2; + param_6[10] = (int)(local_44 + -4); + param_6[6] = (int)(local_44 + -4); + *param_6 = 2; + param_6[9] = iVar10; + param_6[5] = iVar10; + param_6[7] = (int)param_1 - (int)local_44; + pcVar4 = FindSubstringInString((char *)param_1,&DAT_0084453c); + if (pcVar4 == (char *)0x0) { + param_6[8] = 0; + param_6[2] = (int)((float)param_4 + (float)local_38); + } + else { + string1 = pcVar4 + 2; + iVar10 = SafeStringCompareWithLength(string1,&DAT_00844538,2); + if ((iVar10 == 0) && + (iVar10 = SafeStringCompareWithLength((char *)(param_6[7] + -10),&DAT_0085f7ec,2), + iVar10 == 0)) { + string1 = pcVar4 + 4; + param_6[7] = param_6[7] + -10; + } + param_6[8] = (int)string1 - param_6[7]; + param_6[2] = (int)((float)param_4 + (float)local_38); + } + } + break; + case (undefined *)0x5: + if (*param_6 == 2) { + param_6[4] = (int)((float)param_4 + (float)local_38); + piVar14 = (int *)&stack0xffffff3c; + piVar13 = param_6; + for (iVar10 = 8; piVar13 = piVar13 + 1, iVar10 != 0; iVar10 = iVar10 + -1) { + *piVar14 = *piVar13; + piVar14 = piVar14 + 1; + } + *param_6 = 0; + AddRectangleToBuffer + (local_18,in_stack_ffffff3c,in_stack_ffffff40,in_stack_ffffff44,in_stack_ffffff48) + ; + this = local_18; + } + } + bVar7 = *param_1; + } + param_6[4] = (int)((float)param_4 + (float)local_38); + return; +} + + + +// =================== FULL DISASSEMBLY ==================== +// 0x005ccbe0 55 PUSH EBP +// 0x005ccbe1 8b ec MOV EBP, ESP +// 0x005ccbe3 81 ec 94 00 00 00 SUB ESP, 0x94 +// 0x005ccbe9 53 PUSH EBX +// 0x005ccbea 56 PUSH ESI +// 0x005ccbeb 57 PUSH EDI +// 0x005ccbec 8b f9 MOV EDI, ECX +// 0x005ccbee f6 47 5c 08 TEST byte ptr [EDI + 0x5c], 0x8 +// 0x005ccbf2 89 7d ec MOV dword ptr [EBP + -0x14], EDI +// 0x005ccbf5 74 57 JZ 0x005ccc4e +// 0x005ccbf7 8d 87 a0 00 00 00 LEA EAX, [EDI + 0xa0] +// 0x005ccbfd 89 45 f8 MOV dword ptr [EBP + -0x8], EAX +// 0x005ccc00 c7 45 f4 08 00 00 00 MOV dword ptr [EBP + -0xc], 0x8 +// 0x005ccc07 8b 4d f8 MOV ECX, dword ptr [EBP + -0x8] +// 0x005ccc0a 8b 31 MOV ESI, dword ptr [ECX] +// 0x005ccc0c 85 f6 TEST ESI, ESI +// 0x005ccc0e 74 2c JZ 0x005ccc3c +// 0x005ccc10 8b 46 1c MOV EAX, dword ptr [ESI + 0x1c] +// 0x005ccc13 85 c0 TEST EAX, EAX +// 0x005ccc15 76 1e JBE 0x005ccc35 +// 0x005ccc17 33 db XOR EBX, EBX +// 0x005ccc19 8d a4 24 00 00 00 00 LEA ESP, [ESP] +// 0x005ccc20 8b 56 20 MOV EDX, dword ptr [ESI + 0x20] +// 0x005ccc23 6a 00 PUSH 0x0 +// 0x005ccc25 8d 0c 9a LEA ECX, [EDX + EBX*0x4] +// 0x005ccc28 e8 93 15 00 00 CALL 0x005ce1c0 +// 0x005ccc2d 8b 46 1c MOV EAX, dword ptr [ESI + 0x1c] +// 0x005ccc30 43 INC EBX +// 0x005ccc31 3b d8 CMP EBX, EAX +// 0x005ccc33 72 eb JC 0x005ccc20 +// 0x005ccc35 c7 46 1c 00 00 00 00 MOV dword ptr [ESI + 0x1c], 0x0 +// 0x005ccc3c 8b 4d f8 MOV ECX, dword ptr [EBP + -0x8] +// 0x005ccc3f 8b 45 f4 MOV EAX, dword ptr [EBP + -0xc] +// 0x005ccc42 83 c1 04 ADD ECX, 0x4 +// 0x005ccc45 48 DEC EAX +// 0x005ccc46 89 4d f8 MOV dword ptr [EBP + -0x8], ECX +// 0x005ccc49 89 45 f4 MOV dword ptr [EBP + -0xc], EAX +// 0x005ccc4c 75 b9 JNZ 0x005ccc07 +// 0x005ccc4e 8b 45 14 MOV EAX, dword ptr [EBP + 0x14] +// 0x005ccc51 8b 08 MOV ECX, dword ptr [EAX] +// 0x005ccc53 8b 50 04 MOV EDX, dword ptr [EAX + 0x4] +// 0x005ccc56 8b 40 08 MOV EAX, dword ptr [EAX + 0x8] +// 0x005ccc59 89 4d cc MOV dword ptr [EBP + -0x34], ECX +// 0x005ccc5c 8b 4f 1c MOV ECX, dword ptr [EDI + 0x1c] +// 0x005ccc5f 51 PUSH ECX +// 0x005ccc60 8b 4f 5c MOV ECX, dword ptr [EDI + 0x5c] +// 0x005ccc63 c1 e9 07 SHR ECX, 0x7 +// 0x005ccc66 33 f6 XOR ESI, ESI +// 0x005ccc68 83 e1 01 AND ECX, 0x1 +// 0x005ccc6b 89 55 d0 MOV dword ptr [EBP + -0x30], EDX +// 0x005ccc6e 89 45 d4 MOV dword ptr [EBP + -0x2c], EAX +// 0x005ccc71 89 75 fc MOV dword ptr [EBP + -0x4], ESI +// 0x005ccc74 e8 27 a3 ff ff CALL 0x005c6fa0 +// 0x005ccc79 d9 5d bc FSTP float ptr [EBP + -0x44] +// 0x005ccc7c 8b 4f 44 MOV ECX, dword ptr [EDI + 0x44] +// 0x005ccc7f e8 0c e2 ff ff CALL 0x005cae90 +// 0x005ccc84 89 45 b0 MOV dword ptr [EBP + -0x50], EAX +// 0x005ccc87 89 75 b4 MOV dword ptr [EBP + -0x4c], ESI +// 0x005ccc8a df 6d b0 FILD qword ptr [EBP + -0x50] +// 0x005ccc8d 8b 4f 5c MOV ECX, dword ptr [EDI + 0x5c] +// 0x005ccc90 8b 57 1c MOV EDX, dword ptr [EDI + 0x1c] +// 0x005ccc93 c1 e9 07 SHR ECX, 0x7 +// 0x005ccc96 d8 7d bc FDIVR float ptr [EBP + -0x44] +// 0x005ccc99 52 PUSH EDX +// 0x005ccc9a 83 e1 01 AND ECX, 0x1 +// 0x005ccc9d d9 5d f4 FSTP float ptr [EBP + -0xc] +// 0x005ccca0 e8 fb a2 ff ff CALL 0x005c6fa0 +// 0x005ccca5 d9 5d f0 FSTP float ptr [EBP + -0x10] +// 0x005ccca8 8b 47 44 MOV EAX, dword ptr [EDI + 0x44] +// 0x005cccab 8b 80 80 01 00 00 MOV EAX, dword ptr [EAX + 0x180] +// 0x005cccb1 a8 08 TEST AL, 0x8 +// 0x005cccb3 74 0b JZ 0x005cccc0 +// 0x005cccb5 d9 45 f0 FLD float ptr [EBP + -0x10] +// 0x005cccb8 d8 05 6c 30 80 00 FADD float ptr [0x0080306c] +// 0x005cccbe eb 0d JMP 0x005ccccd +// 0x005cccc0 a8 01 TEST AL, 0x1 +// 0x005cccc2 74 0c JZ 0x005cccd0 +// 0x005cccc4 d9 45 f0 FLD float ptr [EBP + -0x10] +// 0x005cccc7 d8 05 28 16 80 00 FADD float ptr [0x00801628] +// 0x005ccccd d9 5d f0 FSTP float ptr [EBP + -0x10] +// 0x005cccd0 8b 5d 1c MOV EBX, dword ptr [EBP + 0x1c] +// 0x005cccd3 83 3b 02 CMP dword ptr [EBX], 0x2 +// 0x005cccd6 75 06 JNZ 0x005cccde +// 0x005cccd8 8b 4d cc MOV ECX, dword ptr [EBP + -0x34] +// 0x005cccdb 89 4b 10 MOV dword ptr [EBX + 0x10], ECX +// 0x005cccde a0 f0 b9 c2 00 MOV AL, [0x00c2b9f0] +// 0x005ccce3 a8 01 TEST AL, 0x1 +// 0x005ccce5 75 40 JNZ 0x005ccd27 +// 0x005ccce7 8a d0 MOV DL, AL +// 0x005ccce9 80 ca 01 OR DL, 0x1 +// 0x005cccec 68 c0 d2 5c 00 PUSH 0x5cd2c0 +// 0x005cccf1 88 15 f0 b9 c2 00 MOV byte ptr [0x00c2b9f0], DL +// 0x005cccf7 c7 05 f8 b9 c2 00 00 00 00 3b MOV dword ptr [0x00c2b9f8], 0x3b000000 +// 0x005ccd01 c7 05 fc b9 c2 00 00 00 00 3b MOV dword ptr [0x00c2b9fc], 0x3b000000 +// 0x005ccd0b c7 05 00 ba c2 00 00 00 00 bb MOV dword ptr [0x00c2ba00], 0xbb000000 +// 0x005ccd15 c7 05 04 ba c2 00 00 00 00 bb MOV dword ptr [0x00c2ba04], 0xbb000000 +// 0x005ccd1f e8 cb cd e3 ff CALL 0x00409aef +// 0x005ccd24 83 c4 04 ADD ESP, 0x4 +// 0x005ccd27 8b 55 08 MOV EDX, dword ptr [EBP + 0x8] +// 0x005ccd2a 80 3a 00 CMP byte ptr [EDX], 0x0 +// 0x005ccd2d c7 45 c8 00 00 00 00 MOV dword ptr [EBP + -0x38], 0x0 +// 0x005ccd34 c7 45 14 00 00 00 00 MOV dword ptr [EBP + 0x14], 0x0 +// 0x005ccd3b 0f 84 4b 05 00 00 JZ 0x005cd28c +// 0x005ccd41 8b 45 0c MOV EAX, dword ptr [EBP + 0xc] +// 0x005ccd44 85 c0 TEST EAX, EAX +// 0x005ccd46 0f 84 40 05 00 00 JZ 0x005cd28c +// 0x005ccd4c 8b 4f 5c MOV ECX, dword ptr [EDI + 0x5c] +// 0x005ccd4f 8d 45 f8 LEA EAX, [EBP + -0x8] +// 0x005ccd52 50 PUSH EAX +// 0x005ccd53 51 PUSH ECX +// 0x005ccd54 8b 4d 08 MOV ECX, dword ptr [EBP + 0x8] +// 0x005ccd57 8d 55 c4 LEA EDX, [EBP + -0x3c] +// 0x005ccd5a 52 PUSH EDX +// 0x005ccd5b 8d 55 c0 LEA EDX, [EBP + -0x40] +// 0x005ccd5e c7 45 c4 00 00 00 00 MOV dword ptr [EBP + -0x3c], 0x0 +// 0x005ccd65 e8 a6 5a ff ff CALL 0x005c2810 +// 0x005ccd6a 8b 55 c0 MOV EDX, dword ptr [EBP + -0x40] +// 0x005ccd6d 8b 4d 0c MOV ECX, dword ptr [EBP + 0xc] +// 0x005ccd70 8b f0 MOV ESI, EAX +// 0x005ccd72 01 55 08 ADD dword ptr [EBP + 0x8], EDX +// 0x005ccd75 8b 45 fc MOV EAX, dword ptr [EBP + -0x4] +// 0x005ccd78 2b ca SUB ECX, EDX +// 0x005ccd7a 85 c0 TEST EAX, EAX +// 0x005ccd7c 89 4d 0c MOV dword ptr [EBP + 0xc], ECX +// 0x005ccd7f 74 2e JZ 0x005ccdaf +// 0x005ccd81 f6 47 5c 10 TEST byte ptr [EDI + 0x5c], 0x10 +// 0x005ccd85 74 12 JZ 0x005ccd99 +// 0x005ccd87 8b 45 f8 MOV EAX, dword ptr [EBP + -0x8] +// 0x005ccd8a 8b 4d fc MOV ECX, dword ptr [EBP + -0x4] +// 0x005ccd8d 50 PUSH EAX +// 0x005ccd8e 51 PUSH ECX +// 0x005ccd8f 8b 4f 44 MOV ECX, dword ptr [EDI + 0x44] +// 0x005ccd92 e8 19 d7 ff ff CALL 0x005ca4b0 +// 0x005ccd97 eb 10 JMP 0x005ccda9 +// 0x005ccd99 8b 55 f8 MOV EDX, dword ptr [EBP + -0x8] +// 0x005ccd9c 8b 45 fc MOV EAX, dword ptr [EBP + -0x4] +// 0x005ccd9f 8b 4f 44 MOV ECX, dword ptr [EDI + 0x44] +// 0x005ccda2 52 PUSH EDX +// 0x005ccda3 50 PUSH EAX +// 0x005ccda4 e8 27 d5 ff ff CALL 0x005ca2d0 +// 0x005ccda9 8b 55 c0 MOV EDX, dword ptr [EBP + -0x40] +// 0x005ccdac d9 5d c8 FSTP float ptr [EBP + -0x38] +// 0x005ccdaf 83 fe 05 CMP ESI, 0x5 +// 0x005ccdb2 d9 45 c8 FLD float ptr [EBP + -0x38] +// 0x005ccdb5 d8 4d f4 FMUL float ptr [EBP + -0xc] +// 0x005ccdb8 d9 5d 14 FSTP float ptr [EBP + 0x14] +// 0x005ccdbb 0f 87 02 01 00 00 JA 0x005ccec3 +// 0x005ccdc1 ff 24 b5 a0 d2 5c 00 JMP dword ptr [ESI*0x4 + 0x5cd2a0] +// 0x005ccdc8 83 3b 02 CMP dword ptr [EBX], 0x2 +// 0x005ccdcb 0f 84 af 04 00 00 JZ 0x005cd280 +// 0x005ccdd1 8b 4d 08 MOV ECX, dword ptr [EBP + 0x8] +// 0x005ccdd4 8b c1 MOV EAX, ECX +// 0x005ccdd6 2b c2 SUB EAX, EDX +// 0x005ccdd8 83 c2 fc ADD EDX, -0x4 +// 0x005ccddb 8d 70 02 LEA ESI, [EAX + 0x2] +// 0x005ccdde 89 53 28 MOV dword ptr [EBX + 0x28], EDX +// 0x005ccde1 89 53 18 MOV dword ptr [EBX + 0x18], EDX +// 0x005ccde4 ba 3c 45 84 00 MOV EDX, 0x84453c +// 0x005ccde9 c7 03 02 00 00 00 MOV dword ptr [EBX], 0x2 +// 0x005ccdef 89 73 24 MOV dword ptr [EBX + 0x24], ESI +// 0x005ccdf2 89 73 14 MOV dword ptr [EBX + 0x14], ESI +// 0x005ccdf5 89 43 1c MOV dword ptr [EBX + 0x1c], EAX +// 0x005ccdf8 e8 a3 e6 07 00 CALL 0x0064b4a0 +// 0x005ccdfd 8b f0 MOV ESI, EAX +// 0x005ccdff 85 f6 TEST ESI, ESI +// 0x005cce01 74 4d JZ 0x005cce50 +// 0x005cce03 6a 02 PUSH 0x2 +// 0x005cce05 68 38 45 84 00 PUSH 0x844538 +// 0x005cce0a 83 c6 02 ADD ESI, 0x2 +// 0x005cce0d 56 PUSH ESI +// 0x005cce0e e8 6d d6 07 00 CALL 0x0064a480 +// 0x005cce13 85 c0 TEST EAX, EAX +// 0x005cce15 75 23 JNZ 0x005cce3a +// 0x005cce17 8b 4b 1c MOV ECX, dword ptr [EBX + 0x1c] +// 0x005cce1a 6a 02 PUSH 0x2 +// 0x005cce1c 68 ec f7 85 00 PUSH 0x85f7ec +// 0x005cce21 83 e9 0a SUB ECX, 0xa +// 0x005cce24 51 PUSH ECX +// 0x005cce25 e8 56 d6 07 00 CALL 0x0064a480 +// 0x005cce2a 85 c0 TEST EAX, EAX +// 0x005cce2c 75 0c JNZ 0x005cce3a +// 0x005cce2e 8b 43 1c MOV EAX, dword ptr [EBX + 0x1c] +// 0x005cce31 83 c6 02 ADD ESI, 0x2 +// 0x005cce34 83 c0 f6 ADD EAX, -0xa +// 0x005cce37 89 43 1c MOV dword ptr [EBX + 0x1c], EAX +// 0x005cce3a d9 45 14 FLD float ptr [EBP + 0x14] +// 0x005cce3d 8b 43 1c MOV EAX, dword ptr [EBX + 0x1c] +// 0x005cce40 d8 45 cc FADD float ptr [EBP + -0x34] +// 0x005cce43 2b f0 SUB ESI, EAX +// 0x005cce45 89 73 20 MOV dword ptr [EBX + 0x20], ESI +// 0x005cce48 d9 5b 08 FSTP float ptr [EBX + 0x8] +// 0x005cce4b e9 30 04 00 00 JMP 0x005cd280 +// 0x005cce50 d9 45 14 FLD float ptr [EBP + 0x14] +// 0x005cce53 c7 43 20 00 00 00 00 MOV dword ptr [EBX + 0x20], 0x0 +// 0x005cce5a d8 45 cc FADD float ptr [EBP + -0x34] +// 0x005cce5d d9 5b 08 FSTP float ptr [EBX + 0x8] +// 0x005cce60 e9 1b 04 00 00 JMP 0x005cd280 +// 0x005cce65 83 3b 02 CMP dword ptr [EBX], 0x2 +// 0x005cce68 0f 85 12 04 00 00 JNZ 0x005cd280 +// 0x005cce6e d9 45 14 FLD float ptr [EBP + 0x14] +// 0x005cce71 83 ec 20 SUB ESP, 0x20 +// 0x005cce74 d8 45 cc FADD float ptr [EBP + -0x34] +// 0x005cce77 8b fc MOV EDI, ESP +// 0x005cce79 8d 73 04 LEA ESI, [EBX + 0x4] +// 0x005cce7c b9 08 00 00 00 MOV ECX, 0x8 +// 0x005cce81 d9 5b 10 FSTP float ptr [EBX + 0x10] +// 0x005cce84 f3 a5 MOVSD.REP ES:EDI, ESI +// 0x005cce86 8b 4d ec MOV ECX, dword ptr [EBP + -0x14] +// 0x005cce89 c7 03 00 00 00 00 MOV dword ptr [EBX], 0x0 +// 0x005cce8f e8 7c 04 00 00 CALL 0x005cd310 +// 0x005cce94 e9 e4 03 00 00 JMP 0x005cd27d +// 0x005cce99 8b 57 2c MOV EDX, dword ptr [EDI + 0x2c] +// 0x005cce9c 8b 45 10 MOV EAX, dword ptr [EBP + 0x10] +// 0x005cce9f 89 10 MOV dword ptr [EAX], EDX +// 0x005ccea1 e9 da 03 00 00 JMP 0x005cd280 +// 0x005ccea6 f6 47 5c 08 TEST byte ptr [EDI + 0x5c], 0x8 +// 0x005cceaa 0f 85 d0 03 00 00 JNZ 0x005cd280 +// 0x005cceb0 8a 4f 2f MOV CL, byte ptr [EDI + 0x2f] +// 0x005cceb3 8b 45 10 MOV EAX, dword ptr [EBP + 0x10] +// 0x005cceb6 88 4d c7 MOV byte ptr [EBP + -0x39], CL +// 0x005cceb9 8b 55 c4 MOV EDX, dword ptr [EBP + -0x3c] +// 0x005ccebc 89 10 MOV dword ptr [EAX], EDX +// 0x005ccebe e9 bd 03 00 00 JMP 0x005cd280 +// 0x005ccec3 8b 4d f8 MOV ECX, dword ptr [EBP + -0x8] +// 0x005ccec6 51 PUSH ECX +// 0x005ccec7 8b 4f 44 MOV ECX, dword ptr [EDI + 0x44] +// 0x005cceca e8 01 dd ff ff CALL 0x005cabd0 +// 0x005ccecf 8b f0 MOV ESI, EAX +// 0x005cced1 85 f6 TEST ESI, ESI +// 0x005cced3 89 75 e8 MOV dword ptr [EBP + -0x18], ESI +// 0x005cced6 0f 84 a4 03 00 00 JZ 0x005cd280 +// 0x005ccedc 8b 5e 28 MOV EBX, dword ptr [ESI + 0x28] +// 0x005ccedf 8b 45 18 MOV EAX, dword ptr [EBP + 0x18] +// 0x005ccee2 8b cb MOV ECX, EBX +// 0x005ccee4 ba 01 00 00 00 MOV EDX, 0x1 +// 0x005ccee9 d3 e2 SHL EDX, CL +// 0x005cceeb 09 10 OR dword ptr [EAX], EDX +// 0x005cceed 8b 84 9f a0 00 00 00 MOV EAX, dword ptr [EDI + EBX*0x4 + 0xa0] +// 0x005ccef4 85 c0 TEST EAX, EAX +// 0x005ccef6 75 0c JNZ 0x005ccf04 +// 0x005ccef8 e8 33 b7 ff ff CALL 0x005c8630 +// 0x005ccefd 89 84 9f a0 00 00 00 MOV dword ptr [EDI + EBX*0x4 + 0xa0], EAX +// 0x005ccf04 f6 47 5c 08 TEST byte ptr [EDI + 0x5c], 0x8 +// 0x005ccf08 8b 84 9f a0 00 00 00 MOV EAX, dword ptr [EDI + EBX*0x4 + 0xa0] +// 0x005ccf0f 89 45 b8 MOV dword ptr [EBP + -0x48], EAX +// 0x005ccf12 0f 85 21 01 00 00 JNZ 0x005cd039 +// 0x005ccf18 8b 48 18 MOV ECX, dword ptr [EAX + 0x18] +// 0x005ccf1b 8d 70 18 LEA ESI, [EAX + 0x18] +// 0x005ccf1e 8b 46 04 MOV EAX, dword ptr [ESI + 0x4] +// 0x005ccf21 83 c0 04 ADD EAX, 0x4 +// 0x005ccf24 3b c1 CMP EAX, ECX +// 0x005ccf26 89 45 fc MOV dword ptr [EBP + -0x4], EAX +// 0x005ccf29 76 26 JBE 0x005ccf51 +// 0x005ccf2b 8b 46 0c MOV EAX, dword ptr [ESI + 0xc] +// 0x005ccf2e 85 c0 TEST EAX, EAX +// 0x005ccf30 75 0b JNZ 0x005ccf3d +// 0x005ccf32 8b 45 fc MOV EAX, dword ptr [EBP + -0x4] +// 0x005ccf35 50 PUSH EAX +// 0x005ccf36 8b ce MOV ECX, ESI +// 0x005ccf38 e8 53 e0 fc ff CALL 0x0059af90 +// 0x005ccf3d 8b 4d fc MOV ECX, dword ptr [EBP + -0x4] +// 0x005ccf40 50 PUSH EAX +// 0x005ccf41 51 PUSH ECX +// 0x005ccf42 8b ce MOV ECX, ESI +// 0x005ccf44 e8 87 e0 fc ff CALL 0x0059afd0 +// 0x005ccf49 50 PUSH EAX +// 0x005ccf4a 8b ce MOV ECX, ESI +// 0x005ccf4c e8 bf 6d f3 ff CALL 0x00503d10 +// 0x005ccf51 33 c9 XOR ECX, ECX +// 0x005ccf53 8b 56 04 MOV EDX, dword ptr [ESI + 0x4] +// 0x005ccf56 8b 46 08 MOV EAX, dword ptr [ESI + 0x8] +// 0x005ccf59 03 d1 ADD EDX, ECX +// 0x005ccf5b 8d 04 90 LEA EAX, [EAX + EDX*0x4] +// 0x005ccf5e 85 c0 TEST EAX, EAX +// 0x005ccf60 74 07 JZ 0x005ccf69 +// 0x005ccf62 8b 55 10 MOV EDX, dword ptr [EBP + 0x10] +// 0x005ccf65 8b 12 MOV EDX, dword ptr [EDX] +// 0x005ccf67 89 10 MOV dword ptr [EAX], EDX +// 0x005ccf69 41 INC ECX +// 0x005ccf6a 83 f9 04 CMP ECX, 0x4 +// 0x005ccf6d 72 e4 JC 0x005ccf53 +// 0x005ccf6f 8b 56 04 MOV EDX, dword ptr [ESI + 0x4] +// 0x005ccf72 83 c2 04 ADD EDX, 0x4 +// 0x005ccf75 89 56 04 MOV dword ptr [ESI + 0x4], EDX +// 0x005ccf78 f6 47 5c 20 TEST byte ptr [EDI + 0x5c], 0x20 +// 0x005ccf7c 8b f2 MOV ESI, EDX +// 0x005ccf7e 8d 46 fc LEA EAX, [ESI + -0x4] +// 0x005ccf81 0f 84 af 00 00 00 JZ 0x005cd036 +// 0x005ccf87 8b b7 90 00 00 00 MOV ESI, dword ptr [EDI + 0x90] +// 0x005ccf8d 8d 50 02 LEA EDX, [EAX + 0x2] +// 0x005ccf90 8d 48 01 LEA ECX, [EAX + 0x1] +// 0x005ccf93 88 9d 6c ff ff ff MOV byte ptr [EBP + 0xffffff6c], BL +// 0x005ccf99 88 9d 74 ff ff ff MOV byte ptr [EBP + 0xffffff74], BL +// 0x005ccf9f 88 9d 7c ff ff ff MOV byte ptr [EBP + 0xffffff7c], BL +// 0x005ccfa5 88 5d 84 MOV byte ptr [EBP + -0x7c], BL +// 0x005ccfa8 89 85 70 ff ff ff MOV dword ptr [EBP + 0xffffff70], EAX +// 0x005ccfae 83 c0 03 ADD EAX, 0x3 +// 0x005ccfb1 8d 9f 8c 00 00 00 LEA EBX, [EDI + 0x8c] +// 0x005ccfb7 89 45 88 MOV dword ptr [EBP + -0x78], EAX +// 0x005ccfba 8b 03 MOV EAX, dword ptr [EBX] +// 0x005ccfbc 89 55 80 MOV dword ptr [EBP + -0x80], EDX +// 0x005ccfbf 83 c6 04 ADD ESI, 0x4 +// 0x005ccfc2 3b f0 CMP ESI, EAX +// 0x005ccfc4 8d 95 6c ff ff ff LEA EDX, [EBP + 0xffffff6c] +// 0x005ccfca 89 8d 78 ff ff ff MOV dword ptr [EBP + 0xffffff78], ECX +// 0x005ccfd0 89 55 b4 MOV dword ptr [EBP + -0x4c], EDX +// 0x005ccfd3 76 32 JBE 0x005cd007 +// 0x005ccfd5 8b 4b 0c MOV ECX, dword ptr [EBX + 0xc] +// 0x005ccfd8 85 c9 TEST ECX, ECX +// 0x005ccfda 75 0a JNZ 0x005ccfe6 +// 0x005ccfdc 56 PUSH ESI +// 0x005ccfdd 8b cb MOV ECX, EBX +// 0x005ccfdf e8 6c 13 00 00 CALL 0x005ce350 +// 0x005ccfe4 8b c8 MOV ECX, EAX +// 0x005ccfe6 33 d2 XOR EDX, EDX +// 0x005ccfe8 8b c6 MOV EAX, ESI +// 0x005ccfea f7 f1 DIV ECX +// 0x005ccfec 89 75 fc MOV dword ptr [EBP + -0x4], ESI +// 0x005ccfef 85 d2 TEST EDX, EDX +// 0x005ccff1 74 06 JZ 0x005ccff9 +// 0x005ccff3 2b ca SUB ECX, EDX +// 0x005ccff5 03 ce ADD ECX, ESI +// 0x005ccff7 eb 03 JMP 0x005ccffc +// 0x005ccff9 8b 4d fc MOV ECX, dword ptr [EBP + -0x4] +// 0x005ccffc 51 PUSH ECX +// 0x005ccffd 8b cb MOV ECX, EBX +// 0x005ccfff e8 8c 13 00 00 CALL 0x005ce390 +// 0x005cd004 8b 55 b4 MOV EDX, dword ptr [EBP + -0x4c] +// 0x005cd007 33 c0 XOR EAX, EAX +// 0x005cd009 8d a4 24 00 00 00 00 LEA ESP, [ESP] +// 0x005cd010 8b 4b 04 MOV ECX, dword ptr [EBX + 0x4] +// 0x005cd013 8b 73 08 MOV ESI, dword ptr [EBX + 0x8] +// 0x005cd016 03 c8 ADD ECX, EAX +// 0x005cd018 8d 0c ce LEA ECX, [ESI + ECX*0x8] +// 0x005cd01b 85 c9 TEST ECX, ECX +// 0x005cd01d 74 0a JZ 0x005cd029 +// 0x005cd01f 8b 32 MOV ESI, dword ptr [EDX] +// 0x005cd021 89 31 MOV dword ptr [ECX], ESI +// 0x005cd023 8b 72 04 MOV ESI, dword ptr [EDX + 0x4] +// 0x005cd026 89 71 04 MOV dword ptr [ECX + 0x4], ESI +// 0x005cd029 83 c2 08 ADD EDX, 0x8 +// 0x005cd02c 40 INC EAX +// 0x005cd02d 83 f8 04 CMP EAX, 0x4 +// 0x005cd030 72 de JC 0x005cd010 +// 0x005cd032 83 43 04 04 ADD dword ptr [EBX + 0x4], 0x4 +// 0x005cd036 8b 75 e8 MOV ESI, dword ptr [EBP + -0x18] +// 0x005cd039 8b 5f 5c MOV EBX, dword ptr [EDI + 0x5c] +// 0x005cd03c 84 db TEST BL, BL +// 0x005cd03e 78 1e JS 0x005cd05e +// 0x005cd040 d9 45 14 FLD float ptr [EBP + 0x14] +// 0x005cd043 d8 05 24 fa 7f 00 FADD float ptr [0x007ffa24] +// 0x005cd049 e8 62 d2 e3 ff CALL 0x0040a2b0 +// 0x005cd04e 89 45 8c MOV dword ptr [EBP + -0x74], EAX +// 0x005cd051 c7 45 90 00 00 00 00 MOV dword ptr [EBP + -0x70], 0x0 +// 0x005cd058 df 6d 8c FILD qword ptr [EBP + -0x74] +// 0x005cd05b d9 5d 14 FSTP float ptr [EBP + 0x14] +// 0x005cd05e d9 45 14 FLD float ptr [EBP + 0x14] +// 0x005cd061 8b 4d d4 MOV ECX, dword ptr [EBP + -0x2c] +// 0x005cd064 d8 45 cc FADD float ptr [EBP + -0x34] +// 0x005cd067 8b 45 d0 MOV EAX, dword ptr [EBP + -0x30] +// 0x005cd06a c1 eb 07 SHR EBX, 0x7 +// 0x005cd06d 81 e3 01 ff ff ff AND EBX, 0xffffff01 +// 0x005cd073 d9 5d cc FSTP float ptr [EBP + -0x34] +// 0x005cd076 8b 55 cc MOV EDX, dword ptr [EBP + -0x34] +// 0x005cd079 89 55 9c MOV dword ptr [EBP + -0x64], EDX +// 0x005cd07c 8b 57 1c MOV EDX, dword ptr [EDI + 0x1c] +// 0x005cd07f 52 PUSH EDX +// 0x005cd080 53 PUSH EBX +// 0x005cd081 89 4d a4 MOV dword ptr [EBP + -0x5c], ECX +// 0x005cd084 8b 4f 44 MOV ECX, dword ptr [EDI + 0x44] +// 0x005cd087 56 PUSH ESI +// 0x005cd088 c7 45 a8 00 00 00 00 MOV dword ptr [EBP + -0x58], 0x0 +// 0x005cd08f c7 45 ac 00 00 00 00 MOV dword ptr [EBP + -0x54], 0x0 +// 0x005cd096 89 45 a0 MOV dword ptr [EBP + -0x60], EAX +// 0x005cd099 e8 e2 df ff ff CALL 0x005cb080 +// 0x005cd09e d8 45 cc FADD float ptr [EBP + -0x34] +// 0x005cd0a1 8b 46 48 MOV EAX, dword ptr [ESI + 0x48] +// 0x005cd0a4 89 45 94 MOV dword ptr [EBP + -0x6c], EAX +// 0x005cd0a7 8a 47 5c MOV AL, byte ptr [EDI + 0x5c] +// 0x005cd0aa d9 5d 9c FSTP float ptr [EBP + -0x64] +// 0x005cd0ad 84 c0 TEST AL, AL +// 0x005cd0af c7 45 98 00 00 00 00 MOV dword ptr [EBP + -0x68], 0x0 +// 0x005cd0b6 df 6d 94 FILD qword ptr [EBP + -0x6c] +// 0x005cd0b9 d8 4d f4 FMUL float ptr [EBP + -0xc] +// 0x005cd0bc d9 5d fc FSTP float ptr [EBP + -0x4] +// 0x005cd0bf 78 14 JS 0x005cd0d5 +// 0x005cd0c1 d9 45 fc FLD float ptr [EBP + -0x4] +// 0x005cd0c4 83 ec 08 SUB ESP, 0x8 +// 0x005cd0c7 dd 1c 24 FSTP double ptr [ESP] +// 0x005cd0ca e8 70 2e 17 00 CALL 0x0073ff3f +// 0x005cd0cf d9 5d fc FSTP float ptr [EBP + -0x4] +// 0x005cd0d2 83 c4 08 ADD ESP, 0x8 +// 0x005cd0d5 8b 4f 44 MOV ECX, dword ptr [EDI + 0x44] +// 0x005cd0d8 d9 45 a0 FLD float ptr [EBP + -0x60] +// 0x005cd0db 8b 81 80 01 00 00 MOV EAX, dword ptr [ECX + 0x180] +// 0x005cd0e1 a8 08 TEST AL, 0x8 +// 0x005cd0e3 74 08 JZ 0x005cd0ed +// 0x005cd0e5 d8 25 28 16 80 00 FSUB float ptr [0x00801628] +// 0x005cd0eb eb 0a JMP 0x005cd0f7 +// 0x005cd0ed a8 01 TEST AL, 0x1 +// 0x005cd0ef 74 06 JZ 0x005cd0f7 +// 0x005cd0f1 d8 25 d8 f9 7f 00 FSUB float ptr [0x007ff9d8] +// 0x005cd0f7 8b 55 e8 MOV EDX, dword ptr [EBP + -0x18] +// 0x005cd0fa db 42 58 FILD dword ptr [EDX + 0x58] +// 0x005cd0fd 8b 45 b8 MOV EAX, dword ptr [EBP + -0x48] +// 0x005cd100 8b 48 08 MOV ECX, dword ptr [EAX + 0x8] +// 0x005cd103 8d 58 08 LEA EBX, [EAX + 0x8] +// 0x005cd106 d8 4d f4 FMUL float ptr [EBP + -0xc] +// 0x005cd109 8b 43 04 MOV EAX, dword ptr [EBX + 0x4] +// 0x005cd10c 83 c0 04 ADD EAX, 0x4 +// 0x005cd10f 3b c1 CMP EAX, ECX +// 0x005cd111 d8 c1 FADD ST0, ST1 +// 0x005cd113 d9 5d a0 FSTP float ptr [EBP + -0x60] +// 0x005cd116 dd d8 FSTP ST0 +// 0x005cd118 76 48 JBE 0x005cd162 +// 0x005cd11a 8b 4b 0c MOV ECX, dword ptr [EBX + 0xc] +// 0x005cd11d 85 c9 TEST ECX, ECX +// 0x005cd11f 75 30 JNZ 0x005cd151 +// 0x005cd121 83 f8 0c CMP EAX, 0xc +// 0x005cd124 8b c8 MOV ECX, EAX +// 0x005cd126 73 1d JNC 0x005cd145 +// 0x005cd128 8d 50 ff LEA EDX, [EAX + -0x1] +// 0x005cd12b 23 d0 AND EDX, EAX +// 0x005cd12d 74 0a JZ 0x005cd139 +// 0x005cd12f 90 NOP +// 0x005cd130 8b ca MOV ECX, EDX +// 0x005cd132 8d 51 ff LEA EDX, [ECX + -0x1] +// 0x005cd135 23 d1 AND EDX, ECX +// 0x005cd137 75 f7 JNZ 0x005cd130 +// 0x005cd139 83 f9 01 CMP ECX, 0x1 +// 0x005cd13c 73 13 JNC 0x005cd151 +// 0x005cd13e b9 01 00 00 00 MOV ECX, 0x1 +// 0x005cd143 eb 0c JMP 0x005cd151 +// 0x005cd145 c7 43 0c 0c 00 00 00 MOV dword ptr [EBX + 0xc], 0xc +// 0x005cd14c b9 0c 00 00 00 MOV ECX, 0xc +// 0x005cd151 51 PUSH ECX +// 0x005cd152 50 PUSH EAX +// 0x005cd153 8b cb MOV ECX, EBX +// 0x005cd155 e8 96 10 00 00 CALL 0x005ce1f0 +// 0x005cd15a 50 PUSH EAX +// 0x005cd15b 8b cb MOV ECX, EBX +// 0x005cd15d e8 ae 10 00 00 CALL 0x005ce210 +// 0x005cd162 33 d2 XOR EDX, EDX +// 0x005cd164 8b 43 04 MOV EAX, dword ptr [EBX + 0x4] +// 0x005cd167 03 c2 ADD EAX, EDX +// 0x005cd169 8d 0c 80 LEA ECX, [EAX + EAX*0x4] +// 0x005cd16c 8b 43 08 MOV EAX, dword ptr [EBX + 0x8] +// 0x005cd16f 8d 3c 88 LEA EDI, [EAX + ECX*0x4] +// 0x005cd172 85 ff TEST EDI, EDI +// 0x005cd174 74 0a JZ 0x005cd180 +// 0x005cd176 b9 05 00 00 00 MOV ECX, 0x5 +// 0x005cd17b 8d 75 9c LEA ESI, [EBP + -0x64] +// 0x005cd17e f3 a5 MOVSD.REP ES:EDI, ESI +// 0x005cd180 42 INC EDX +// 0x005cd181 83 fa 04 CMP EDX, 0x4 +// 0x005cd184 72 de JC 0x005cd164 +// 0x005cd186 8b 7b 04 MOV EDI, dword ptr [EBX + 0x4] +// 0x005cd189 8b 75 b8 MOV ESI, dword ptr [EBP + -0x48] +// 0x005cd18c 8b 55 ec MOV EDX, dword ptr [EBP + -0x14] +// 0x005cd18f 83 c7 04 ADD EDI, 0x4 +// 0x005cd192 89 7b 04 MOV dword ptr [EBX + 0x4], EDI +// 0x005cd195 8b 46 10 MOV EAX, dword ptr [ESI + 0x10] +// 0x005cd198 8b df MOV EBX, EDI +// 0x005cd19a 8d 4c 9b ec LEA ECX, [EBX + EBX*0x4 + -0x14] +// 0x005cd19e 8a 5a 5c MOV BL, byte ptr [EDX + 0x5c] +// 0x005cd1a1 c1 e1 02 SHL ECX, 0x2 +// 0x005cd1a4 03 c1 ADD EAX, ECX +// 0x005cd1a6 84 db TEST BL, BL +// 0x005cd1a8 79 25 JNS 0x005cd1cf +// 0x005cd1aa d9 45 fc FLD float ptr [EBP + -0x4] +// 0x005cd1ad 8b 78 14 MOV EDI, dword ptr [EAX + 0x14] +// 0x005cd1b0 d8 40 3c FADD float ptr [EAX + 0x3c] +// 0x005cd1b3 89 38 MOV dword ptr [EAX], EDI +// 0x005cd1b5 8b 78 2c MOV EDI, dword ptr [EAX + 0x2c] +// 0x005cd1b8 89 78 04 MOV dword ptr [EAX + 0x4], EDI +// 0x005cd1bb d9 50 3c FST float ptr [EAX + 0x3c] +// 0x005cd1be d9 58 28 FSTP float ptr [EAX + 0x28] +// 0x005cd1c1 d9 45 bc FLD float ptr [EBP + -0x44] +// 0x005cd1c4 d8 40 40 FADD float ptr [EAX + 0x40] +// 0x005cd1c7 d9 50 40 FST float ptr [EAX + 0x40] +// 0x005cd1ca d9 58 18 FSTP float ptr [EAX + 0x18] +// 0x005cd1cd eb 23 JMP 0x005cd1f2 +// 0x005cd1cf d9 40 14 FLD float ptr [EAX + 0x14] +// 0x005cd1d2 d9 18 FSTP float ptr [EAX] +// 0x005cd1d4 d9 45 fc FLD float ptr [EBP + -0x4] +// 0x005cd1d7 d8 40 3c FADD float ptr [EAX + 0x3c] +// 0x005cd1da d9 50 3c FST float ptr [EAX + 0x3c] +// 0x005cd1dd d9 58 28 FSTP float ptr [EAX + 0x28] +// 0x005cd1e0 d9 45 f0 FLD float ptr [EBP + -0x10] +// 0x005cd1e3 d8 40 40 FADD float ptr [EAX + 0x40] +// 0x005cd1e6 d9 50 40 FST float ptr [EAX + 0x40] +// 0x005cd1e9 d9 58 18 FSTP float ptr [EAX + 0x18] +// 0x005cd1ec d9 40 2c FLD float ptr [EAX + 0x2c] +// 0x005cd1ef d9 58 04 FSTP float ptr [EAX + 0x4] +// 0x005cd1f2 8b 46 10 MOV EAX, dword ptr [ESI + 0x10] +// 0x005cd1f5 03 c1 ADD EAX, ECX +// 0x005cd1f7 8b 4d e8 MOV ECX, dword ptr [EBP + -0x18] +// 0x005cd1fa 83 c1 60 ADD ECX, 0x60 +// 0x005cd1fd 8b 31 MOV ESI, dword ptr [ECX] +// 0x005cd1ff 89 75 d8 MOV dword ptr [EBP + -0x28], ESI +// 0x005cd202 8b 71 04 MOV ESI, dword ptr [ECX + 0x4] +// 0x005cd205 89 75 dc MOV dword ptr [EBP + -0x24], ESI +// 0x005cd208 8b 71 08 MOV ESI, dword ptr [ECX + 0x8] +// 0x005cd20b 8b 49 0c MOV ECX, dword ptr [ECX + 0xc] +// 0x005cd20e 89 4d e4 MOV dword ptr [EBP + -0x1c], ECX +// 0x005cd211 8a 4a 5c MOV CL, byte ptr [EDX + 0x5c] +// 0x005cd214 84 c9 TEST CL, CL +// 0x005cd216 89 75 e0 MOV dword ptr [EBP + -0x20], ESI +// 0x005cd219 79 2f JNS 0x005cd24a +// 0x005cd21b d9 45 d8 FLD float ptr [EBP + -0x28] +// 0x005cd21e d8 05 f8 b9 c2 00 FADD float ptr [0x00c2b9f8] +// 0x005cd224 d9 5d d8 FSTP float ptr [EBP + -0x28] +// 0x005cd227 d9 45 dc FLD float ptr [EBP + -0x24] +// 0x005cd22a d8 05 fc b9 c2 00 FADD float ptr [0x00c2b9fc] +// 0x005cd230 d9 5d dc FSTP float ptr [EBP + -0x24] +// 0x005cd233 d9 45 e0 FLD float ptr [EBP + -0x20] +// 0x005cd236 d8 05 00 ba c2 00 FADD float ptr [0x00c2ba00] +// 0x005cd23c d9 5d e0 FSTP float ptr [EBP + -0x20] +// 0x005cd23f d9 45 e4 FLD float ptr [EBP + -0x1c] +// 0x005cd242 d8 05 04 ba c2 00 FADD float ptr [0x00c2ba04] +// 0x005cd248 eb 03 JMP 0x005cd24d +// 0x005cd24a d9 45 e4 FLD float ptr [EBP + -0x1c] +// 0x005cd24d 8b 55 e0 MOV EDX, dword ptr [EBP + -0x20] +// 0x005cd250 d9 50 48 FST float ptr [EAX + 0x48] +// 0x005cd253 8b 5d 1c MOV EBX, dword ptr [EBP + 0x1c] +// 0x005cd256 d9 58 34 FSTP float ptr [EAX + 0x34] +// 0x005cd259 8b ca MOV ECX, EDX +// 0x005cd25b 89 50 38 MOV dword ptr [EAX + 0x38], EDX +// 0x005cd25e 8b 55 d8 MOV EDX, dword ptr [EBP + -0x28] +// 0x005cd261 89 48 10 MOV dword ptr [EAX + 0x10], ECX +// 0x005cd264 8b ca MOV ECX, EDX +// 0x005cd266 89 50 4c MOV dword ptr [EAX + 0x4c], EDX +// 0x005cd269 8b 55 dc MOV EDX, dword ptr [EBP + -0x24] +// 0x005cd26c 89 48 24 MOV dword ptr [EAX + 0x24], ECX +// 0x005cd26f 8b ca MOV ECX, EDX +// 0x005cd271 89 50 20 MOV dword ptr [EAX + 0x20], EDX +// 0x005cd274 89 48 0c MOV dword ptr [EAX + 0xc], ECX +// 0x005cd277 8b 55 f8 MOV EDX, dword ptr [EBP + -0x8] +// 0x005cd27a 89 55 fc MOV dword ptr [EBP + -0x4], EDX +// 0x005cd27d 8b 7d ec MOV EDI, dword ptr [EBP + -0x14] +// 0x005cd280 8b 45 08 MOV EAX, dword ptr [EBP + 0x8] +// 0x005cd283 80 38 00 CMP byte ptr [EAX], 0x0 +// 0x005cd286 0f 85 b5 fa ff ff JNZ 0x005ccd41 +// 0x005cd28c d9 45 14 FLD float ptr [EBP + 0x14] +// 0x005cd28f 5f POP EDI +// 0x005cd290 d8 45 cc FADD float ptr [EBP + -0x34] +// 0x005cd293 5e POP ESI +// 0x005cd294 d9 5b 10 FSTP float ptr [EBX + 0x10] +// 0x005cd297 5b POP EBX +// 0x005cd298 8b e5 MOV ESP, EBP +// 0x005cd29a 5d POP EBP +// 0x005cd29b c2 18 00 RET 0x18 +// Total instructions: 552 + +// ============= PROLOGUE (first 30 instructions) ========== +// 0x005ccbe0 55 PUSH EBP +// 0x005ccbe1 8b ec MOV EBP, ESP +// 0x005ccbe3 81 ec 94 00 00 00 SUB ESP, 0x94 +// 0x005ccbe9 53 PUSH EBX +// 0x005ccbea 56 PUSH ESI +// 0x005ccbeb 57 PUSH EDI +// 0x005ccbec 8b f9 MOV EDI, ECX +// 0x005ccbee f6 47 5c 08 TEST byte ptr [EDI + 0x5c], 0x8 +// 0x005ccbf2 89 7d ec MOV dword ptr [EBP + -0x14], EDI +// 0x005ccbf5 74 57 JZ 0x005ccc4e +// 0x005ccbf7 8d 87 a0 00 00 00 LEA EAX, [EDI + 0xa0] +// 0x005ccbfd 89 45 f8 MOV dword ptr [EBP + -0x8], EAX +// 0x005ccc00 c7 45 f4 08 00 00 00 MOV dword ptr [EBP + -0xc], 0x8 +// 0x005ccc07 8b 4d f8 MOV ECX, dword ptr [EBP + -0x8] +// 0x005ccc0a 8b 31 MOV ESI, dword ptr [ECX] +// 0x005ccc0c 85 f6 TEST ESI, ESI +// 0x005ccc0e 74 2c JZ 0x005ccc3c +// 0x005ccc10 8b 46 1c MOV EAX, dword ptr [ESI + 0x1c] +// 0x005ccc13 85 c0 TEST EAX, EAX +// 0x005ccc15 76 1e JBE 0x005ccc35 +// 0x005ccc17 33 db XOR EBX, EBX +// 0x005ccc19 8d a4 24 00 00 00 00 LEA ESP, [ESP] +// 0x005ccc20 8b 56 20 MOV EDX, dword ptr [ESI + 0x20] +// 0x005ccc23 6a 00 PUSH 0x0 +// 0x005ccc25 8d 0c 9a LEA ECX, [EDX + EBX*0x4] +// 0x005ccc28 e8 93 15 00 00 CALL 0x005ce1c0 +// 0x005ccc2d 8b 46 1c MOV EAX, dword ptr [ESI + 0x1c] +// 0x005ccc30 43 INC EBX +// 0x005ccc31 3b d8 CMP EBX, EAX +// 0x005ccc33 72 eb JC 0x005ccc20 + +// ================ ALL RET INSTRUCTIONS =================== +// 0x005cd29b c2 18 00 RET 0x18 + +// =========== CALLING CONVENTION ANALYSIS ================= +// RET cleans up 0x18 (24) bytes from stack +// This suggests 6 stack parameters (after ECX/EDX if thiscall/fastcall) + +// =============== XREFS TO (callers) ====================== +// 0x005cde50 from renderTextToBuffer (0x005cdc20) type=UNCONDITIONAL_CALL +// Total callers: 1 + +// =============== XREFS FROM (callees) ==================== +// CALL 0x005ce1c0 conditionalFree (from 0x005ccc28) +// CALL 0x005c6fa0 ConvertPixelsToScreen (from 0x005ccc74) +// CALL 0x005cae90 GetTextureSize (from 0x005ccc7f) +// CALL 0x00409aef validateMemoryOperation (from 0x005ccd1f) +// CALL 0x005c2810 ParseTextFormatCodes (from 0x005ccd65) +// CALL 0x005ca4b0 GetOrCreateKerningPair (from 0x005ccd92) +// CALL 0x005ca2d0 GetOrCreateCharacterGlyph (from 0x005ccda4) +// CALL 0x0064b4a0 FindSubstringInString (from 0x005ccdf8) +// CALL 0x0064a480 SafeStringCompareWithLength (from 0x005cce0e) +// CALL 0x005cd310 AddRectangleToBuffer (from 0x005cce8f) +// CALL 0x005cabd0 GetOrCreateCharacterTexture (from 0x005cceca) +// CALL 0x005c8630 AllocateTextLineTexture (from 0x005ccef8) +// CALL 0x0059af90 CalculateOptimalSize64_2 (from 0x005ccf38) +// CALL 0x0059afd0 AlignToMultiple6 (from 0x005ccf44) +// CALL 0x00503d10 ResizeVectorArray (from 0x005ccf4c) +// CALL 0x005ce350 findPowerOfTwo (from 0x005ccfdf) +// CALL 0x005ce390 resizeGradientBuffer (from 0x005ccfff) +// CALL 0x0040a2b0 __ftol (from 0x005cd049) +// CALL 0x005cb080 CalculateTextBounds (from 0x005cd099) +// CALL 0x0073ff3f truncateFloatWithValidation (from 0x005cd0ca) +// CALL 0x005ce1f0 alignToNextMultiple (from 0x005cd155) +// CALL 0x005ce210 resizeVertexBuffer (from 0x005cd15d) +// Total unique callees: 22 diff --git a/src/transform44/decompiled/decomp_processLinkedListCollision.c b/src/transform44/decompiled/decomp_processLinkedListCollision.c new file mode 100644 index 0000000..5fcc35a --- /dev/null +++ b/src/transform44/decompiled/decomp_processLinkedListCollision.c @@ -0,0 +1,255 @@ +// ============================================================================= +// processLinkedListCollision @ 0x006abc40 +// Function range: 0x006abc40 -- 0x006abd88 (329 bytes) +// Generated by Ghidra headless analysis +// ============================================================================= + +// ============================================================================= +// SECTION 1: DECOMPILATION +// ============================================================================= + + +undefined * __fastcall +processLinkedListCollision(int *param_1,float *param_2,uint *param_3,uint param_4) + +{ + int iVar1; + uint uVar2; + int iVar3; + undefined *puVar4; + undefined4 *puVar5; + undefined **ppuVar6; + undefined *local_28; + undefined *local_24; + undefined *local_20; + undefined *local_1c; + undefined *local_18; + undefined *local_14; + undefined *local_10; + undefined *local_c; + undefined *local_8; + + if ((param_4 & 0xf0000f) != 0) { + local_8 = (undefined *)param_1[2]; + local_10 = (undefined *)param_1; + local_c = (undefined *)param_2; + if ((((uint)local_8 & 1) != 0) || (local_8 == (undefined *)0x0)) { + local_8 = (undefined *)0x0; + } + while ((puVar4 = local_8, ((uint)local_8 & 1) == 0 && (local_8 != (undefined *)0x0))) { + iVar1 = *(int *)(local_8 + 4); + if (((*(ushort *)(iVar1 + 0xc) & 0x100) == 0) && + ((*(undefined **)(iVar1 + 0x8c) != PTR_00c89f20 && (*(int *)(iVar1 + 0x88) != 0)))) { + if (*(int *)(iVar1 + 0x180) == 0 && *(int *)(iVar1 + 0x184) == 0) { + uVar2 = param_4 & 0xf; + } + else { + uVar2 = param_4 & 0xf00000; + } + if (uVar2 != 0) { + if (-1 < (char)*(ushort *)(iVar1 + 0xc)) { + return (undefined *)0x0; + } + puVar5 = (undefined4 *)(iVar1 + 0x14c); + ppuVar6 = &local_28; + for (iVar3 = 6; iVar3 != 0; iVar3 = iVar3 + -1) { + *ppuVar6 = (undefined *)*puVar5; + puVar5 = puVar5 + 1; + ppuVar6 = ppuVar6 + 1; + } + if (((((float)local_28 < *(float *)(local_c + 0xc) != + ((float)local_28 == *(float *)(local_c + 0xc))) && + ((float)local_24 < *(float *)(local_c + 0x10) != + ((float)local_24 == *(float *)(local_c + 0x10)))) && + ((float)local_20 < *(float *)(local_c + 0x14) != + ((float)local_20 == *(float *)(local_c + 0x14)))) && + (((*(float *)local_c <= (float)local_1c && (*(float *)(local_c + 4) <= (float)local_18) + ) && (*(float *)(local_c + 8) <= (float)local_14)))) { + addGeometryToBuffer((float *)local_c,iVar1,param_3); + puVar4 = local_8; + } + *(undefined **)(iVar1 + 0x8c) = PTR_00c89f20; + } + } + local_8 = *(undefined **)(puVar4 + *(int *)local_10 + 4); + } + } + return (undefined *)0x1; +} + + + +// ============================================================================= +// SECTION 2: FULL DISASSEMBLY +// ============================================================================= +// +// 0x006abc40 55 PUSH EBP +// 0x006abc41 8b ec MOV EBP,ESP +// 0x006abc43 83 ec 24 SUB ESP,0x24 +// 0x006abc46 53 PUSH EBX +// 0x006abc47 8b c1 MOV EAX,ECX +// 0x006abc49 8b 4d 0c MOV ECX,dword ptr [EBP + 0xc] +// 0x006abc4c f7 c1 0f 00 f0 00 TEST ECX,0xf0000f +// 0x006abc52 56 PUSH ESI +// 0x006abc53 57 PUSH EDI +// 0x006abc54 89 55 f8 MOV dword ptr [EBP + -0x8],EDX +// 0x006abc57 89 45 f4 MOV dword ptr [EBP + -0xc],EAX +// 0x006abc5a 0f 84 1b 01 00 00 JZ 0x006abd7b +// 0x006abc60 8b 40 08 MOV EAX,dword ptr [EAX + 0x8] +// 0x006abc63 a8 01 TEST AL,0x1 +// 0x006abc65 75 04 JNZ 0x006abc6b +// 0x006abc67 85 c0 TEST EAX,EAX +// 0x006abc69 75 07 JNZ 0x006abc72 +// 0x006abc6b 33 d2 XOR EDX,EDX +// 0x006abc6d 89 55 fc MOV dword ptr [EBP + -0x4],EDX +// 0x006abc70 eb 06 JMP 0x006abc78 +// 0x006abc72 89 45 fc MOV dword ptr [EBP + -0x4],EAX +// 0x006abc75 8b 55 fc MOV EDX,dword ptr [EBP + -0x4] +// 0x006abc78 f6 c2 01 TEST DL,0x1 +// 0x006abc7b 0f 85 fa 00 00 00 JNZ 0x006abd7b +// 0x006abc81 85 d2 TEST EDX,EDX +// 0x006abc83 0f 84 f2 00 00 00 JZ 0x006abd7b +// 0x006abc89 8b 5a 04 MOV EBX,dword ptr [EDX + 0x4] +// 0x006abc8c 66 8b 43 0c MOV AX,word ptr [EBX + 0xc] +// 0x006abc90 f6 c4 01 TEST AH,0x1 +// 0x006abc93 0f 85 c1 00 00 00 JNZ 0x006abd5a +// 0x006abc99 8b 35 20 9f c8 00 MOV ESI,dword ptr [0x00c89f20] +// 0x006abc9f 39 b3 8c 00 00 00 CMP dword ptr [EBX + 0x8c],ESI +// 0x006abca5 0f 84 af 00 00 00 JZ 0x006abd5a +// 0x006abcab 8b b3 88 00 00 00 MOV ESI,dword ptr [EBX + 0x88] +// 0x006abcb1 85 f6 TEST ESI,ESI +// 0x006abcb3 0f 84 a1 00 00 00 JZ 0x006abd5a +// 0x006abcb9 8b b3 80 01 00 00 MOV ESI,dword ptr [EBX + 0x180] +// 0x006abcbf 0b b3 84 01 00 00 OR ESI,dword ptr [EBX + 0x184] +// 0x006abcc5 74 08 JZ 0x006abccf +// 0x006abcc7 f7 c1 00 00 f0 00 TEST ECX,0xf00000 +// 0x006abccd eb 03 JMP 0x006abcd2 +// 0x006abccf f6 c1 0f TEST CL,0xf +// 0x006abcd2 0f 84 82 00 00 00 JZ 0x006abd5a +// 0x006abcd8 84 c0 TEST AL,AL +// 0x006abcda 0f 89 90 00 00 00 JNS 0x006abd70 +// 0x006abce0 8d b3 4c 01 00 00 LEA ESI,[EBX + 0x14c] +// 0x006abce6 b9 06 00 00 00 MOV ECX,0x6 +// 0x006abceb 8d 7d dc LEA EDI,[EBP + -0x24] +// 0x006abcee f3 a5 MOVSD.REP ES:EDI,ESI +// 0x006abcf0 d9 45 dc FLD float ptr [EBP + -0x24] +// 0x006abcf3 8b 4d f8 MOV ECX,dword ptr [EBP + -0x8] +// 0x006abcf6 d8 59 0c FCOMP float ptr [ECX + 0xc] +// 0x006abcf9 df e0 FNSTSW AX +// 0x006abcfb f6 c4 41 TEST AH,0x41 +// 0x006abcfe 7a 4e JP 0x006abd4e +// 0x006abd00 d9 45 e0 FLD float ptr [EBP + -0x20] +// 0x006abd03 d8 59 10 FCOMP float ptr [ECX + 0x10] +// 0x006abd06 df e0 FNSTSW AX +// 0x006abd08 f6 c4 41 TEST AH,0x41 +// 0x006abd0b 7a 41 JP 0x006abd4e +// 0x006abd0d d9 45 e4 FLD float ptr [EBP + -0x1c] +// 0x006abd10 d8 59 14 FCOMP float ptr [ECX + 0x14] +// 0x006abd13 df e0 FNSTSW AX +// 0x006abd15 f6 c4 41 TEST AH,0x41 +// 0x006abd18 7a 34 JP 0x006abd4e +// 0x006abd1a d9 45 e8 FLD float ptr [EBP + -0x18] +// 0x006abd1d d8 19 FCOMP float ptr [ECX] +// 0x006abd1f df e0 FNSTSW AX +// 0x006abd21 f6 c4 01 TEST AH,0x1 +// 0x006abd24 75 28 JNZ 0x006abd4e +// 0x006abd26 d9 45 ec FLD float ptr [EBP + -0x14] +// 0x006abd29 d8 59 04 FCOMP float ptr [ECX + 0x4] +// 0x006abd2c df e0 FNSTSW AX +// 0x006abd2e f6 c4 01 TEST AH,0x1 +// 0x006abd31 75 1b JNZ 0x006abd4e +// 0x006abd33 d9 45 f0 FLD float ptr [EBP + -0x10] +// 0x006abd36 d8 59 08 FCOMP float ptr [ECX + 0x8] +// 0x006abd39 df e0 FNSTSW AX +// 0x006abd3b f6 c4 01 TEST AH,0x1 +// 0x006abd3e 75 0e JNZ 0x006abd4e +// 0x006abd40 8b 45 08 MOV EAX,dword ptr [EBP + 0x8] +// 0x006abd43 50 PUSH EAX +// 0x006abd44 8b d3 MOV EDX,EBX +// 0x006abd46 e8 45 00 00 00 CALL 0x006abd90 +// 0x006abd4b 8b 55 fc MOV EDX,dword ptr [EBP + -0x4] +// 0x006abd4e 8b 0d 20 9f c8 00 MOV ECX,dword ptr [0x00c89f20] +// 0x006abd54 89 8b 8c 00 00 00 MOV dword ptr [EBX + 0x8c],ECX +// 0x006abd5a 8b 45 f4 MOV EAX,dword ptr [EBP + -0xc] +// 0x006abd5d 8b 00 MOV EAX,dword ptr [EAX] +// 0x006abd5f 8b 4c 10 04 MOV ECX,dword ptr [EAX + EDX*0x1 + 0x4] +// 0x006abd63 03 c2 ADD EAX,EDX +// 0x006abd65 89 4d fc MOV dword ptr [EBP + -0x4],ECX +// 0x006abd68 8b 4d 0c MOV ECX,dword ptr [EBP + 0xc] +// 0x006abd6b e9 05 ff ff ff JMP 0x006abc75 +// 0x006abd70 5f POP EDI +// 0x006abd71 5e POP ESI +// 0x006abd72 33 c0 XOR EAX,EAX +// 0x006abd74 5b POP EBX +// 0x006abd75 8b e5 MOV ESP,EBP +// 0x006abd77 5d POP EBP +// 0x006abd78 c2 08 00 RET 0x8 +// 0x006abd7b 5f POP EDI +// 0x006abd7c 5e POP ESI +// 0x006abd7d b8 01 00 00 00 MOV EAX,0x1 +// 0x006abd82 5b POP EBX +// 0x006abd83 8b e5 MOV ESP,EBP +// 0x006abd85 5d POP EBP +// 0x006abd86 c2 08 00 RET 0x8 +// +// Total instructions: 108 + +// ============================================================================= +// SECTION 3: PROLOGUE (first 30 instructions) +// ============================================================================= +// +// 0x006abc40 55 PUSH EBP +// 0x006abc41 8b ec MOV EBP,ESP +// 0x006abc43 83 ec 24 SUB ESP,0x24 +// 0x006abc46 53 PUSH EBX +// 0x006abc47 8b c1 MOV EAX,ECX +// 0x006abc49 8b 4d 0c MOV ECX,dword ptr [EBP + 0xc] +// 0x006abc4c f7 c1 0f 00 f0 00 TEST ECX,0xf0000f +// 0x006abc52 56 PUSH ESI +// 0x006abc53 57 PUSH EDI +// 0x006abc54 89 55 f8 MOV dword ptr [EBP + -0x8],EDX +// 0x006abc57 89 45 f4 MOV dword ptr [EBP + -0xc],EAX +// 0x006abc5a 0f 84 1b 01 00 00 JZ 0x006abd7b +// 0x006abc60 8b 40 08 MOV EAX,dword ptr [EAX + 0x8] +// 0x006abc63 a8 01 TEST AL,0x1 +// 0x006abc65 75 04 JNZ 0x006abc6b +// 0x006abc67 85 c0 TEST EAX,EAX +// 0x006abc69 75 07 JNZ 0x006abc72 +// 0x006abc6b 33 d2 XOR EDX,EDX +// 0x006abc6d 89 55 fc MOV dword ptr [EBP + -0x4],EDX +// 0x006abc70 eb 06 JMP 0x006abc78 +// 0x006abc72 89 45 fc MOV dword ptr [EBP + -0x4],EAX +// 0x006abc75 8b 55 fc MOV EDX,dword ptr [EBP + -0x4] +// 0x006abc78 f6 c2 01 TEST DL,0x1 +// 0x006abc7b 0f 85 fa 00 00 00 JNZ 0x006abd7b +// 0x006abc81 85 d2 TEST EDX,EDX +// 0x006abc83 0f 84 f2 00 00 00 JZ 0x006abd7b +// 0x006abc89 8b 5a 04 MOV EBX,dword ptr [EDX + 0x4] +// 0x006abc8c 66 8b 43 0c MOV AX,word ptr [EBX + 0xc] +// 0x006abc90 f6 c4 01 TEST AH,0x1 +// 0x006abc93 0f 85 c1 00 00 00 JNZ 0x006abd5a + +// ============================================================================= +// SECTION 4: ALL RET INSTRUCTIONS (stack cleanup indicator) +// ============================================================================= +// +// 0x006abd78 c2 08 00 RET 0x8 +// 0x006abd86 c2 08 00 RET 0x8 + +// ============================================================================= +// SECTION 5: XREFS TO 0x006abc40 (callers) +// ============================================================================= +// +// 0x006aad4c [UNCONDITIONAL_CALL] in processGeometryBoundsCheck (0x006aaab0) +// 0x006ab4a1 [UNCONDITIONAL_CALL] in processTerrainChunkMeshGeneration (0x006aadc0) +// +// Total callers: 2 + +// ============================================================================= +// SECTION 6: XREFS FROM 0x006abc40 (callees) +// ============================================================================= +// +// 0x006abd90 addGeometryToBuffer +// +// Total callees: 1 + diff --git a/src/transform44/decompiled/decomp_renderTextToBuffer.c b/src/transform44/decompiled/decomp_renderTextToBuffer.c new file mode 100644 index 0000000..ef9283e --- /dev/null +++ b/src/transform44/decompiled/decomp_renderTextToBuffer.c @@ -0,0 +1,659 @@ +// ============================================================================= +// renderTextToBuffer @ 0x005CDC20 +// Decompiled from WoW.exe 1.12.1 (build 5875) via Ghidra 11.4.2 +// Single caller of RenderTextToVertexBuffer (0x5CCBE0) +// ============================================================================= + +// ----------------------------------------------------------------------------- +// CALLING CONVENTION (verified from assembly) +// ----------------------------------------------------------------------------- +// __thiscall: ECX = this (text object ptr), saved to EBX immediately +// 0x005cdc20 PUSH EBP +// 0x005cdc21 MOV EBP,ESP +// 0x005cdc23 SUB ESP,0x74 +// 0x005cdc26 PUSH EBX +// 0x005cdc27 PUSH ESI +// 0x005cdc28 MOV EBX,ECX <-- this ptr saved to EBX +// ... +// 0x005cdee8 RET <-- single RET, no stack cleanup = __thiscall (0 stack params) +// +// Signature: void __thiscall renderTextToBuffer(TextObject* this) +// No stack parameters. Single RET (no RET N). + +// ----------------------------------------------------------------------------- +// XREFS TO (callers) +// ----------------------------------------------------------------------------- +// 0x005cd6aa from validateAndPrepareText (UNCONDITIONAL_CALL) +// 0x005cd426 from GetVertexBufferData (UNCONDITIONAL_CALL) +// +// Two callers: +// 1. validateAndPrepareText (0x5cd6aa) -- validation/preparation path +// 2. GetVertexBufferData (0x5cd426) -- vertex buffer retrieval path + +// ----------------------------------------------------------------------------- +// XREFS FROM (callees) +// ----------------------------------------------------------------------------- +// 0x005c6fa0 ConvertPixelsToScreen +// 0x0040a2b0 __ftol (float-to-long) +// 0x005c2810 ParseTextFormatCodes +// 0x005c7260 WrapTextToWidth +// 0x005c7010 ConvertPixelsToScreenAlt +// 0x005ccbe0 RenderTextToVertexBuffer <<<< the target +// 0x005cd310 AddRectangleToBuffer +// 0x005cdf70 finalizeTextLayout +// 0x005cd4d0 renderFadeEffect + +// ----------------------------------------------------------------------------- +// TEXT OBJECT FIELD MAP (offsets from this/EBX) +// ----------------------------------------------------------------------------- +// +0x1c float lineHeight (or spacing-related metric) +// +0x28 float xOffset (used when bit7 of flags is clear) +// +0x2c float color/style data (passed to RenderTextToVertexBuffer) +// +0x34 float indentOrShadow (used when flags bit0 is set) +// +0x3c float maxWidth (passed to WrapTextToWidth) +// +0x40 float maxHeight (vertical overflow check) +// +0x44 void* fontObject (passed to WrapTextToWidth as ECX) +// +0x48 char* textString (the actual text to render) +// +0x54 int alignment (0=left, 1=center, 2=right) +// +0x58 float some metric (initial value for local_c / line step) +// +0x5c uint flags bitfield: +// bit0: has indent/shadow +// bit1 (0x02): single-line mode (breaks loop after first line) +// bit5 (0x20): has fade effect +// bit7 (0x80): pixel mode vs screen mode +// +0x60 uint resultFlags (OR'd with per-line flags from RenderTextToVertexBuffer) +// +0x68 int fadeStart (passed to renderFadeEffect) +// +0x6c int fadeEnd (passed to renderFadeEffect) +// +0x90 uint lineCountOutput (zeroed at start, not the dirty flag) +// +0x9c int LINE COUNTER / DIRTY FLAG -- THE KEY FIELD +// - Checked FIRST: if (this+0x9c != 0) return immediately +// - Incremented after each line rendered +// - Also incremented for format-code-only lines (ParseTextFormatCodes returns 2) +// - Acts as both "already rendered" guard AND line counter + +// ----------------------------------------------------------------------------- +// DIRTY/VALID FLAG ANALYSIS +// ----------------------------------------------------------------------------- +// +// The field at +0x9c is the critical gate. The function's VERY FIRST check is: +// +// if (this->field_0x9c != 0) return; // already rendered, skip +// if (this->textString == NULL) return; // no text +// if (*this->textString == '\0') return; // empty text +// +// Assembly proof: +// 0x005cdc2a MOV EAX,dword ptr [EBX + 0x9c] +// 0x005cdc33 CMP EAX,EDI ; EDI = 0 +// 0x005cdc35 JNZ 0x005cdee2 ; bail if non-zero +// 0x005cdc3b MOV EAX,dword ptr [EBX + 0x48] +// 0x005cdc3e CMP EAX,EDI +// 0x005cdc40 JZ 0x005cdee2 ; bail if text ptr is NULL +// 0x005cdc46 CMP byte ptr [EAX],0x0 +// 0x005cdc49 JZ 0x005cdee2 ; bail if text is empty +// +// Then +0x9c is INCREMENTED after each rendered line: +// 0x005cde55 MOV ECX,dword ptr [EBX + 0x9c] +// 0x005cde5e INC ECX +// 0x005cde62 MOV dword ptr [EBX + 0x9c],ECX +// +// And also incremented for format-code lines (ParseTextFormatCodes == 2): +// 0x005cdd81 MOV EAX,dword ptr [EBX + 0x9c] +// 0x005cdd8c INC EAX +// 0x005cdd90 MOV dword ptr [EBX + 0x9c],EAX +// +// CONCLUSION: +0x9c serves as BOTH: +// 1. A "dirty/needs-render" flag (0 = needs render, non-zero = already done) +// 2. A line counter (counts lines processed during rendering) +// +// To force re-render: set this->field_0x9c = 0 +// To prevent render: set this->field_0x9c = non-zero +// +// The callers (validateAndPrepareText, GetVertexBufferData) presumably +// reset +0x9c to 0 when text changes, triggering re-render on next call. + +// +0x90 is zeroed at entry: +// 0x005cdc4f MOV dword ptr [EBX + 0x90],EDI ; = 0 +// This appears to be a separate output counter, not the dirty flag. + +// ----------------------------------------------------------------------------- +// FLOW SUMMARY +// ----------------------------------------------------------------------------- +// +// 1. Guard: if +0x9c != 0 || textString is null/empty -> return +// 2. Zero +0x90 (line output counter?) +// 3. Compute vertical layout params from +0x1c, +0x58, flags +// 4. Loop over text lines: +// a. Check vertical overflow (accumulated height vs +0x40 maxHeight) +// b. ParseTextFormatCodes -- handle color/format escapes (returns 2 = consumed) +// c. WrapTextToWidth -- break text at word boundaries for +0x3c width +// d. Handle alignment (+0x54): left(0), center(1), right(2) via ConvertPixelsToScreenAlt +// e. CALL RenderTextToVertexBuffer(this, textPtr, charCount, &color, &offset, &flags, &state) +// f. Increment +0x9c (line counter) +// g. If state==2, call AddRectangleToBuffer (highlight/selection rect) +// h. OR per-line flags into +0x60 +// i. If flags bit1 (single-line mode), break +// j. Advance to next line +// 5. Call finalizeTextLayout(this) +// 6. If flags bit5 (fade), call renderFadeEffect(this, fadeStart, fadeEnd) + +// ----------------------------------------------------------------------------- +// RenderTextToVertexBuffer CALL SITE DETAIL (at 0x5CDE50) +// ----------------------------------------------------------------------------- +// __thiscall: ECX = this (text object) +// Stack args (6, pushed right-to-left): +// push &state (EBP-0x74, local_74 area -- tracks render state, value 2 = highlight) +// push &flags (EBP-0x24, output flags OR'd into +0x60) +// push charCount (EBP-0x18, from WrapTextToWidth output) +// push &offset (EBP-0x48, vertical/horizontal position floats) +// push &color (EBP-0x30, color/style data) +// push textPtr (ESI, current position in text string) +// +// Assembly at call site: +// 0x005cde39 LEA EAX,[EBP + -0x74] +// 0x005cde3c PUSH EAX ; &state +// 0x005cde3d LEA ECX,[EBP + -0x24] +// 0x005cde40 PUSH ECX ; &flags +// 0x005cde41 MOV ECX,dword ptr [EBP + -0x18] +// 0x005cde44 LEA EDX,[EBP + -0x48] +// 0x005cde47 PUSH EDX ; &offset +// 0x005cde48 LEA EAX,[EBP + -0x30] +// 0x005cde4b PUSH EAX ; &color +// 0x005cde4c PUSH ECX ; charCount +// 0x005cde4d PUSH ESI ; textPtr +// 0x005cde4e MOV ECX,EBX ; this +// 0x005cde50 CALL 0x005ccbe0 ; RenderTextToVertexBuffer + + +// ============================================================================= +// DECOMPILED C (Ghidra raw output, lightly annotated) +// ============================================================================= + +/* WARNING: Variable defined which should be unmapped: local_88 */ +void __fastcall renderTextToBuffer(void *param_1) // ECX = this +{ + byte bVar1; + uint uVar2; + undefined *puVar3; + int iVar4; + undefined **ppuVar5; + byte *pbVar6; + float unaff_EDI; + undefined4 *puVar7; + float10 extraout_ST0; + float10 fVar8; + float10 extraout_ST0_00; + float10 extraout_ST0_01; + float10 extraout_ST0_02; + ulonglong uVar9; + float10 *pfVar10; + float fVar11; + undefined *local_88; + undefined *local_78; + undefined *local_74; + undefined *local_70; + undefined *local_6c; + undefined *local_68; + undefined *local_4c; + undefined *local_48; + undefined *local_44; + undefined *local_40; + uint uStack_3c; + undefined *local_38; + undefined *local_34; + undefined *local_30; + undefined *local_2c; + undefined *local_28; + undefined *local_24; + undefined *local_20; + undefined *local_1c; + undefined *local_18; + undefined *local_14; + undefined *local_10; + undefined *local_c; + byte local_5; + + // --- GUARD: dirty/valid check --- + if (((*(int *)((int)param_1 + 0x9c) != 0) || // already rendered? + (*(char **)((int)param_1 + 0x48) == (char *)0x0)) || // no text ptr? + (**(char **)((int)param_1 + 0x48) == '\0')) { // empty text? + return; + } + + // --- Reset line output counter --- + *(undefined4 *)((int)param_1 + 0x90) = 0; + + // --- Read fade params --- + local_2c = *(undefined **)((int)param_1 + 0x6c); // fadeEnd + local_38 = *(undefined **)((int)param_1 + 0x68); // fadeStart + + // --- Convert line height to screen coords --- + ConvertPixelsToScreen( + (void *)(*(uint *)((int)param_1 + 0x5c) >> 7 & 1), + (float10 *)-*(float *)((int)param_1 + 0x1c), + unaff_EDI); + + uVar2 = *(uint *)((int)param_1 + 0x5c) & 0x80; // pixel mode flag + local_4c = (undefined *)0x0; + + if (uVar2 == 0) { + local_44 = *(undefined **)((int)param_1 + 0x28); // xOffset + local_48 = (undefined *)(float)extraout_ST0; + } else { + local_48 = (undefined *)0x0; + local_44 = (undefined *)0x0; + } + + local_34 = *(undefined **)((int)param_1 + 0x2c); // color/style + local_10 = *(undefined **)((int)param_1 + 0x58); // line step metric + pbVar6 = *(byte **)((int)param_1 + 0x48); // text string ptr + + // Init state vars + local_78 = (undefined *)0x0; + local_70 = (undefined *)0x0; + local_68 = (undefined *)0x0; + local_20 = (undefined *)0x0; + local_1c = (undefined *)0x0; + local_14 = (undefined *)0x0; + + // Compute line height (pixel mode vs screen mode) + if (uVar2 == 0) { + // Screen mode: round to integer pixels, then back to screen coords + local_40 = PTR_00c2b9a0; + uStack_3c = 0; + uVar9 = __ftol(); + local_40 = (undefined *)uVar9; + uStack_3c = 0; + local_18 = (undefined *)(float)(uVar9 & 0xffffffff); + local_10 = (undefined *)(float)((float10)(uVar9 & 0xffffffff) / extraout_ST0_00); + ConvertPixelsToScreen((void *)0x0, *(float10 **)((int)param_1 + 0x1c), unaff_EDI); + fVar8 = extraout_ST0_01 + (float10)(float)local_18; + } else { + // Pixel mode: just add + fVar8 = (float10)(float)local_10 + (float10)*(float *)((int)param_1 + 0x1c); + } + + local_c = (undefined *)(float)fVar8; // total line step (height + spacing) + local_5 = 1; // first line flag + local_74 = (undefined *)((float)local_c + (float)local_48); // current Y position + + bVar1 = *pbVar6; + local_18 = (undefined *)0x0; // accumulated vertical height + local_6c = local_48; + + // --- MAIN RENDERING LOOP: iterate over lines --- + do { + // Check: end of text OR vertical overflow + if (((bVar1 == 0) || + (*(float *)((int)param_1 + 0x40) <= (float)local_18)) || + (local_18 = (undefined *)((float)local_18 + *(float *)((int)param_1 + 0x1c) + (float)local_10), + bVar1 == 0)) + goto LAB_005cdebc; // done + + // Parse format/color codes + puVar3 = ParseTextFormatCodes( + pbVar6, &local_30, (uint *)0x0, + *(uint *)((int)param_1 + 0x5c), &uStack_3c); + + local_14 = (undefined *)0x0; + + if (puVar3 == (undefined *)0x2) { + // Format code consumed entire segment -- adjust position, skip render + local_48 = (undefined *)((float)local_48 - (float)local_c); + *(int *)((int)param_1 + 0x9c) = *(int *)((int)param_1 + 0x9c) + 1; // increment line counter + pbVar6 = pbVar6 + (int)local_30; + } else { + // Check word wrap flag + if (((uint)*(float10 **)((int)param_1 + 0x5c) & 1) == 0) { + local_24 = (undefined *)0x0; + } else { + local_24 = *(undefined **)((int)param_1 + 0x34); // indent + } + + pfVar10 = *(float10 **)((int)param_1 + 0x1c); + + // Word-wrap the text to fit maxWidth + WrapTextToWidth( + *(float10 **)((int)param_1 + 0x44), // font + pbVar6, // text + pfVar10, // line height + *(float *)((int)param_1 + 0x3c), // maxWidth + (int *)&local_1c, // out: charCount + (float *)&local_14, // out: line width + &local_20, // out: next line ptr + (float10 *)local_24, // indent + *(float10 **)((int)param_1 + 0x5c), // flags + &local_5); // first line flag + + // Check if wrapping produced valid output + if (((local_20 == pbVar6) || (local_20 == (undefined *)0x0)) || + ((local_1c == (undefined *)0x0 && (*local_20 == '\0')))) { +LAB_005cdebc: + // --- FINALIZE --- + finalizeTextLayout((int)param_1); + if ((*(byte *)((int)param_1 + 0x5c) & 0x20) == 0) { + return; + } + // Fade effect + if ((local_38 == (undefined *)0xffffffff) && + (local_2c == (undefined *)0xffffffff)) { + return; + } + renderFadeEffect(param_1, (int)local_38, (int)local_2c); + return; + } + + // --- ALIGNMENT --- + puVar3 = local_14; + if (*(int *)((int)param_1 + 0x54) == 2) { + // Right-aligned +LAB_005cde14: + ConvertPixelsToScreenAlt( + (void *)(*(uint *)((int)param_1 + 0x5c) >> 7 & 1), + (float10 *)-(float)puVar3, unaff_EDI); + local_4c = (undefined *)(float)extraout_ST0_02; + } else if (*(int *)((int)param_1 + 0x54) == 1) { + // Center-aligned + puVar3 = (undefined *)((float)local_14 * StaticFloat0_5); + goto LAB_005cde14; + } + // else: left-aligned, local_4c stays 0 + + local_28 = (undefined *)0x0; + if (local_78 != (undefined *)0x0) { + local_70 = local_4c; + } + + fVar11 = 8.528623e-39; // junk / uninitialized + puVar3 = local_1c; + + // --- THE CALL: render this line's glyphs to vertex buffer --- + RenderTextToVertexBuffer( + param_1, // this (ECX) + pbVar6, // textPtr (current line start) + (int)local_1c, // charCount + (uint *)&local_34, // &color/style + (float *)&local_4c, // &position offset + (uint *)&local_28, // &output flags + (int *)&local_78); // &render state + + // Increment line counter (+0x9c) + *(int *)((int)param_1 + 0x9c) = *(int *)((int)param_1 + 0x9c) + 1; + + // If state == 2, add highlight/selection rectangle + if (local_78 == (undefined *)0x2) { + ppuVar5 = &local_74; + puVar7 = (undefined4 *)&stack0xffffff5c; + for (iVar4 = 8; iVar4 != 0; iVar4 = iVar4 + -1) { + *puVar7 = *ppuVar5; + ppuVar5 = ppuVar5 + 1; + puVar7 = puVar7 + 1; + } + AddRectangleToBuffer(param_1, (float)pfVar10, fVar11, (float)pbVar6, (float)puVar3); + } + + // Advance vertical position + local_48 = (undefined *)((float)local_48 - (float)local_c); + + // Accumulate result flags + *(uint *)((int)param_1 + 0x60) = *(uint *)((int)param_1 + 0x60) | (uint)local_28; + + // Move to next line + pbVar6 = local_20; + + // Single-line mode check: if bit1 set, done after first line + if ((*(byte *)((int)param_1 + 0x5c) & 2) != 0) + goto LAB_005cdebc; + } + + bVar1 = *pbVar6; + local_6c = (undefined *)((float)local_6c - (float)local_c); + local_74 = (undefined *)((float)local_74 - (float)local_c); + } while (true); +} + + +// ============================================================================= +// FULL DISASSEMBLY LISTING +// ============================================================================= +// +// 0x005cdc20 PUSH EBP +// 0x005cdc21 MOV EBP,ESP +// 0x005cdc23 SUB ESP,0x74 +// 0x005cdc26 PUSH EBX +// 0x005cdc27 PUSH ESI +// 0x005cdc28 MOV EBX,ECX +// 0x005cdc2a MOV EAX,dword ptr [EBX + 0x9c] +// 0x005cdc30 PUSH EDI +// 0x005cdc31 XOR EDI,EDI +// 0x005cdc33 CMP EAX,EDI +// 0x005cdc35 JNZ 0x005cdee2 +// 0x005cdc3b MOV EAX,dword ptr [EBX + 0x48] +// 0x005cdc3e CMP EAX,EDI +// 0x005cdc40 JZ 0x005cdee2 +// 0x005cdc46 CMP byte ptr [EAX],0x0 +// 0x005cdc49 JZ 0x005cdee2 +// 0x005cdc4f MOV dword ptr [EBX + 0x90],EDI +// 0x005cdc55 FLD float ptr [EBX + 0x1c] +// 0x005cdc58 MOV ECX,dword ptr [EBX + 0x6c] +// 0x005cdc5b FCHS +// 0x005cdc5d MOV EAX,dword ptr [EBX + 0x68] +// 0x005cdc60 PUSH ECX +// 0x005cdc61 MOV dword ptr [EBP + -0x28],ECX +// 0x005cdc64 FSTP float ptr [ESP] +// 0x005cdc67 MOV ECX,dword ptr [EBX + 0x5c] +// 0x005cdc6a SHR ECX,0x7 +// 0x005cdc6d AND ECX,0x1 +// 0x005cdc70 MOV dword ptr [EBP + -0x34],EAX +// 0x005cdc73 CALL 0x005c6fa0 +// 0x005cdc78 MOV EAX,dword ptr [EBX + 0x5c] +// 0x005cdc7b AND EAX,0x80 +// 0x005cdc80 MOV dword ptr [EBP + -0x48],EDI +// 0x005cdc83 JZ 0x005cdc8f +// 0x005cdc85 FSTP ST0 +// 0x005cdc87 MOV dword ptr [EBP + -0x44],EDI +// 0x005cdc8a MOV dword ptr [EBP + -0x40],EDI +// 0x005cdc8d JMP 0x005cdc98 +// 0x005cdc8f MOV EDX,dword ptr [EBX + 0x28] +// 0x005cdc92 FSTP float ptr [EBP + -0x44] +// 0x005cdc95 MOV dword ptr [EBP + -0x40],EDX +// 0x005cdc98 CMP EAX,EDI +// 0x005cdc9a MOV ECX,dword ptr [EBX + 0x2c] +// 0x005cdc9d MOV EDX,dword ptr [EBX + 0x58] +// 0x005cdca0 MOV ESI,dword ptr [EBX + 0x48] +// 0x005cdca3 MOV dword ptr [EBP + -0x30],ECX +// 0x005cdca6 MOV dword ptr [EBP + -0x74],EDI +// 0x005cdca9 MOV dword ptr [EBP + -0x6c],0x0 +// 0x005cdcb0 MOV dword ptr [EBP + -0x64],0x0 +// 0x005cdcb7 MOV dword ptr [EBP + -0x1c],EDI +// 0x005cdcba MOV dword ptr [EBP + -0x18],EDI +// 0x005cdcbd MOV dword ptr [EBP + -0x10],0x0 +// 0x005cdcc4 MOV dword ptr [EBP + -0xc],EDX +// 0x005cdcc7 JZ 0x005cdcd1 +// 0x005cdcc9 FLD float ptr [EBP + -0xc] +// 0x005cdccc FADD float ptr [EBX + 0x1c] +// 0x005cdccf JMP 0x005cdd10 +// 0x005cdcd1 MOV EAX,[0x00c2b9a0] +// 0x005cdcd6 MOV dword ptr [EBP + -0x3c],EAX +// 0x005cdcd9 MOV dword ptr [EBP + -0x38],EDI +// 0x005cdcdc FILD qword ptr [EBP + -0x3c] +// 0x005cdcdf FLD float ptr [EBP + -0xc] +// 0x005cdce2 FMUL ST1 +// 0x005cdce4 FADD float ptr [0x00808120] +// 0x005cdcea CALL 0x0040a2b0 +// 0x005cdcef MOV dword ptr [EBP + -0x3c],EAX +// 0x005cdcf2 MOV dword ptr [EBP + -0x38],EDI +// 0x005cdcf5 FILD qword ptr [EBP + -0x3c] +// 0x005cdcf8 MOV ECX,dword ptr [EBX + 0x1c] +// 0x005cdcfb PUSH ECX +// 0x005cdcfc FST float ptr [EBP + -0x14] +// 0x005cdcff XOR ECX,ECX +// 0x005cdd01 FDIV ST0,ST1 +// 0x005cdd03 FSTP float ptr [EBP + -0xc] +// 0x005cdd06 FSTP ST0 +// 0x005cdd08 CALL 0x005c6fa0 +// 0x005cdd0d FADD float ptr [EBP + -0x14] +// 0x005cdd10 MOV EDX,dword ptr [EBP + -0x44] +// 0x005cdd13 FSTP float ptr [EBP + -0x8] +// 0x005cdd16 FLD float ptr [EBP + -0x8] +// 0x005cdd19 MOV byte ptr [EBP + -0x1],0x1 +// 0x005cdd1d FADD float ptr [EBP + -0x44] +// 0x005cdd20 MOV CL,byte ptr [ESI] +// 0x005cdd22 TEST CL,CL +// 0x005cdd24 MOV dword ptr [EBP + -0x68],EDX +// 0x005cdd27 FSTP float ptr [EBP + -0x70] +// 0x005cdd2a MOV dword ptr [EBP + -0x14],0x0 +// 0x005cdd31 JZ 0x005cdebc +// 0x005cdd37 FLD float ptr [EBP + -0x14] +// 0x005cdd3a FCOMP float ptr [EBX + 0x40] +// 0x005cdd3d FNSTSW AX +// 0x005cdd3f TEST AH,0x5 +// 0x005cdd42 JP 0x005cdebc +// 0x005cdd48 TEST CL,CL +// 0x005cdd4a FLD float ptr [EBP + -0x14] +// 0x005cdd4d FADD float ptr [EBX + 0x1c] +// 0x005cdd50 FADD float ptr [EBP + -0xc] +// 0x005cdd53 FSTP float ptr [EBP + -0x14] +// 0x005cdd56 JZ 0x005cdebc +// 0x005cdd5c MOV ECX,dword ptr [EBX + 0x5c] +// 0x005cdd5f LEA EAX,[EBP + -0x38] +// 0x005cdd62 PUSH EAX +// 0x005cdd63 PUSH ECX +// 0x005cdd64 PUSH EDI +// 0x005cdd65 LEA EDX,[EBP + -0x2c] +// 0x005cdd68 MOV ECX,ESI +// 0x005cdd6a CALL 0x005c2810 +// 0x005cdd6f CMP EAX,0x2 +// 0x005cdd72 MOV dword ptr [EBP + -0x10],0x0 +// 0x005cdd79 JNZ 0x005cdd9b +// 0x005cdd7b FLD float ptr [EBP + -0x44] +// 0x005cdd7e MOV ECX,dword ptr [EBP + -0x2c] +// 0x005cdd81 MOV EAX,dword ptr [EBX + 0x9c] +// 0x005cdd87 FSUB float ptr [EBP + -0x8] +// 0x005cdd8a ADD ESI,ECX +// 0x005cdd8c INC EAX +// 0x005cdd8d FSTP float ptr [EBP + -0x44] +// 0x005cdd90 MOV dword ptr [EBX + 0x9c],EAX +// 0x005cdd96 JMP 0x005cdea0 +// 0x005cdd9b MOV EAX,dword ptr [EBX + 0x5c] +// 0x005cdd9e TEST AL,0x1 +// 0x005cdda0 JZ 0x005cddaa +// 0x005cdda2 MOV EDX,dword ptr [EBX + 0x34] +// 0x005cdda5 MOV dword ptr [EBP + -0x20],EDX +// 0x005cdda8 JMP 0x005cddb1 +// 0x005cddaa MOV dword ptr [EBP + -0x20],0x0 +// 0x005cddb1 MOV EDX,dword ptr [EBP + -0x20] +// 0x005cddb4 LEA ECX,[EBP + -0x1] +// 0x005cddb7 PUSH ECX +// 0x005cddb8 PUSH EAX +// 0x005cddb9 PUSH EDX +// 0x005cddba LEA EAX,[EBP + -0x1c] +// 0x005cddbd PUSH EAX +// 0x005cddbe MOV EAX,dword ptr [EBX + 0x3c] +// 0x005cddc1 LEA ECX,[EBP + -0x10] +// 0x005cddc4 PUSH ECX +// 0x005cddc5 MOV ECX,dword ptr [EBX + 0x1c] +// 0x005cddc8 LEA EDX,[EBP + -0x18] +// 0x005cddcb PUSH EDX +// 0x005cddcc PUSH EAX +// 0x005cddcd PUSH ECX +// 0x005cddce MOV ECX,dword ptr [EBX + 0x44] +// 0x005cddd1 MOV EDX,ESI +// 0x005cddd3 CALL 0x005c7260 +// 0x005cddd8 MOV EAX,dword ptr [EBP + -0x1c] +// 0x005cdddb CMP EAX,ESI +// 0x005cdddd JZ 0x005cdebc +// 0x005cdde3 CMP EAX,EDI +// 0x005cdde5 JZ 0x005cdebc +// 0x005cddeb CMP dword ptr [EBP + -0x18],EDI +// 0x005cddee JNZ 0x005cddf9 +// 0x005cddf0 CMP byte ptr [EAX],0x0 +// 0x005cddf3 JZ 0x005cdebc +// 0x005cddf9 MOV EAX,dword ptr [EBX + 0x54] +// 0x005cddfc CMP EAX,0x2 +// 0x005cddff JNZ 0x005cde06 +// 0x005cde01 FLD float ptr [EBP + -0x10] +// 0x005cde04 JMP 0x005cde14 +// 0x005cde06 CMP EAX,0x1 +// 0x005cde09 JNZ 0x005cde2b +// 0x005cde0b FLD float ptr [EBP + -0x10] +// 0x005cde0e FMUL float ptr [0x007ffa24] +// 0x005cde14 PUSH ECX +// 0x005cde15 FCHS +// 0x005cde17 MOV ECX,dword ptr [EBX + 0x5c] +// 0x005cde1a FSTP float ptr [ESP] +// 0x005cde1d SHR ECX,0x7 +// 0x005cde20 AND ECX,0x1 +// 0x005cde23 CALL 0x005c7010 +// 0x005cde28 FSTP float ptr [EBP + -0x48] +// 0x005cde2b CMP dword ptr [EBP + -0x74],EDI +// 0x005cde2e MOV dword ptr [EBP + -0x24],EDI +// 0x005cde31 JZ 0x005cde39 +// 0x005cde33 MOV EDX,dword ptr [EBP + -0x48] +// 0x005cde36 MOV dword ptr [EBP + -0x6c],EDX +// 0x005cde39 LEA EAX,[EBP + -0x74] +// 0x005cde3c PUSH EAX +// 0x005cde3d LEA ECX,[EBP + -0x24] +// 0x005cde40 PUSH ECX +// 0x005cde41 MOV ECX,dword ptr [EBP + -0x18] +// 0x005cde44 LEA EDX,[EBP + -0x48] +// 0x005cde47 PUSH EDX +// 0x005cde48 LEA EAX,[EBP + -0x30] +// 0x005cde4b PUSH EAX +// 0x005cde4c PUSH ECX +// 0x005cde4d PUSH ESI +// 0x005cde4e MOV ECX,EBX +// 0x005cde50 CALL 0x005ccbe0 +// 0x005cde55 MOV ECX,dword ptr [EBX + 0x9c] +// 0x005cde5b MOV EAX,dword ptr [EBP + -0x74] +// 0x005cde5e INC ECX +// 0x005cde5f CMP EAX,0x2 +// 0x005cde62 MOV dword ptr [EBX + 0x9c],ECX +// 0x005cde68 JNZ 0x005cde82 +// 0x005cde6a SUB ESP,0x20 +// 0x005cde6d MOV EDI,ESP +// 0x005cde6f MOV ECX,0x8 +// 0x005cde74 LEA ESI,[EBP + -0x70] +// 0x005cde77 MOVSD.REP ES:EDI,ESI +// 0x005cde79 MOV ECX,EBX +// 0x005cde7b CALL 0x005cd310 +// 0x005cde80 XOR EDI,EDI +// 0x005cde82 FLD float ptr [EBP + -0x44] +// 0x005cde85 MOV ECX,dword ptr [EBX + 0x60] +// 0x005cde88 MOV EDX,dword ptr [EBP + -0x24] +// 0x005cde8b FSUB float ptr [EBP + -0x8] +// 0x005cde8e MOV AL,byte ptr [EBX + 0x5c] +// 0x005cde91 MOV ESI,dword ptr [EBP + -0x1c] +// 0x005cde94 OR ECX,EDX +// 0x005cde96 FSTP float ptr [EBP + -0x44] +// 0x005cde99 TEST AL,0x2 +// 0x005cde9b MOV dword ptr [EBX + 0x60],ECX +// 0x005cde9e JNZ 0x005cdebc +// 0x005cdea0 FLD float ptr [EBP + -0x68] +// 0x005cdea3 MOV CL,byte ptr [ESI] +// 0x005cdea5 TEST CL,CL +// 0x005cdea7 FSUB float ptr [EBP + -0x8] +// 0x005cdeaa FSTP float ptr [EBP + -0x68] +// 0x005cdead FLD float ptr [EBP + -0x70] +// 0x005cdeb0 FSUB float ptr [EBP + -0x8] +// 0x005cdeb3 FSTP float ptr [EBP + -0x70] +// 0x005cdeb6 JNZ 0x005cdd37 +// 0x005cdebc MOV ECX,EBX +// 0x005cdebe CALL 0x005cdf70 +// 0x005cdec3 TEST byte ptr [EBX + 0x5c],0x20 +// 0x005cdec7 JZ 0x005cdee2 +// 0x005cdec9 MOV EAX,dword ptr [EBP + -0x34] +// 0x005cdecc CMP EAX,-0x1 +// 0x005cdecf JNZ 0x005cded6 +// 0x005cded1 CMP dword ptr [EBP + -0x28],EAX +// 0x005cded4 JZ 0x005cdee2 +// 0x005cded6 MOV ECX,dword ptr [EBP + -0x28] +// 0x005cded9 PUSH ECX +// 0x005cdeda PUSH EAX +// 0x005cdedb MOV ECX,EBX +// 0x005cdedd CALL 0x005cd4d0 +// 0x005cdee2 POP EDI +// 0x005cdee3 POP ESI +// 0x005cdee4 POP EBX +// 0x005cdee5 MOV ESP,EBP +// 0x005cdee7 POP EBP +// 0x005cdee8 RET diff --git a/src/transmogfix/transmogfix.zig b/src/transmogfix/transmogfix.zig index efae297..208f974 100644 --- a/src/transmogfix/transmogfix.zig +++ b/src/transmogfix/transmogfix.zig @@ -108,7 +108,7 @@ const OtherPending = struct { var g_other_pending: [OTHER_PENDING_SIZE]OtherPending = [1]OtherPending{.{}} ** OTHER_PENDING_SIZE; var g_other_pending_count: i32 = 0; -var g_cached_visible_item: [19]u32 = .{0} ** 19; + const UNIT_CACHE_SIZE: u32 = 64; @@ -193,8 +193,6 @@ fn cachePlayerState() bool { const field_index = PLAYER_VISIBLE_ITEM_1_0 + (slot * VISIBLE_ITEM_STRIDE); g_cache.visible_items[i] = hook.readMem(u32, g_cache.player_desc + field_index * 4); - - g_cached_visible_item[i] = g_cache.visible_items[i]; } g_cache.valid = true; @@ -488,20 +486,23 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32 if (!g_cache.valid or g_cache.player_obj != obj) { _ = cachePlayerState(); } - if (val == 0 and g_cached_visible_item[slot] != 0) { + // Read live descriptor — g_cached_visible_item can be stale if + // another DLL (e.g. VanillaHelpers) writes directly to the + // descriptor, bypassing SetBlock and our hook. + const current_visible = readUnitVisibleItem(obj, slot); + if (val == 0 and current_visible != 0) { // CLEAR detected - check if INV_SLOT is already empty (real unequip) if (g_cache.valid and g_cache.equipped_guids[slot] == 0) { - g_cached_visible_item[slot] = 0; return callOriginalSetBlock(obj, index, value); } // Start transmog pattern tracking if (!g_local_pending[slot].active) g_local_pending_count += 1; - g_local_pending[slot].original_visible_item = g_cached_visible_item[slot]; + g_local_pending[slot].original_visible_item = current_visible; g_local_pending[slot].timestamp = now; g_local_pending[slot].active = true; g_local_pending[slot].has_durability = false; - log.fmt("BLOCK clear slot={d:2} item=0x{X:0>8}\n", .{ slot, g_cached_visible_item[slot] }); + log.fmt("BLOCK clear slot={d:2} item=0x{X:0>8}\n", .{ slot, current_visible }); return 1; // Block the clear } else if (val != 0 and g_local_pending[slot].active) { if (val == g_local_pending[slot].original_visible_item) { @@ -537,10 +538,6 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32 } } - // Update cache for non-blocked writes - if (val != 0) { - g_cached_visible_item[slot] = val; - } } else if (g_enabled) { // =========== OTHER PLAYERS =========== const guid = getUnitGuid(obj); @@ -628,7 +625,6 @@ fn hookSetBlock(obj: u32, index: u32, value: u32) callconv(hook.cc.thiscall) u32 const field_index = PLAYER_VISIBLE_ITEM_1_0 + (@as(u32, @intCast(es)) * VISIBLE_ITEM_STRIDE); _ = callOriginalSetBlock(obj, field_index, 0); - g_cached_visible_item[es] = 0; g_local_pending[es].active = false; g_local_pending[es].has_durability = false; g_local_pending_count -= 1; @@ -780,7 +776,6 @@ pub fn installHooks() void { g_other_pending_count = 0; g_cache = .{}; g_unit_cache = [1]UnitVisualState{.{}} ** UNIT_CACHE_SIZE; - g_cached_visible_item = .{0} ** 19; // Hook 1: SetBlock if (set_block_hook.attach(ADDR_SetBlock, &hookSetBlock) != .ok) return; diff --git a/src/weirdperformance/bone_sse.zig b/src/weirdperformance/bone_sse.zig index 06a2cde..d5f1d24 100644 --- a/src/weirdperformance/bone_sse.zig +++ b/src/weirdperformance/bone_sse.zig @@ -1098,7 +1098,7 @@ pub fn calcScaledInverse(this_mat: u32, out: u32, scale: f32) void { // mat3(offset_vec3*), mat4(scale_float_bits) // ============================================================================= -pub fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void { +pub export fn transformImpl_SSE(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void { @setEvalBranchQuota(50000); // ========================================================================= diff --git a/src/weirdperformance/bone_sse64.zig b/src/weirdperformance/bone_sse64.zig index 52531bd..258d02d 100644 --- a/src/weirdperformance/bone_sse64.zig +++ b/src/weirdperformance/bone_sse64.zig @@ -284,7 +284,7 @@ fn attachmentRecursion64(this: u32, model_hdr: u32, bone_out_base: u32, frame_ct // scene objects stay on the f64 pipeline. // ============================================================================= -pub fn transformImpl_SSE64(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void { +pub export fn transformImpl_SSE64(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void { @setEvalBranchQuota(50000); // Section 1: Entry checks diff --git a/src/weirdperformance/transform_capture.zig b/src/weirdperformance/transform_capture.zig new file mode 100644 index 0000000..be1ea28 --- /dev/null +++ b/src/weirdperformance/transform_capture.zig @@ -0,0 +1,564 @@ +//! transform_capture — snapshot transformMatrix4x4 inputs + outputs for offline +//! bench/parity replay. Enabled via a feature flag in weirdperformance.zig. +//! +//! Design: +//! - Hook transformMatrix4x4 (0x714260). +//! - On entry: compute a path hash from SceneObject flags + model_hdr counts. +//! Skip if already seen. +//! - Walk reachable pointer graph; record each referenced memory page +//! (4KB granularity, keyed by page address). Save INPUT snapshot. +//! - Call the game original via callOriginal. +//! - Save OUTPUT snapshot of all pages we know get written. +//! - Write to disk: one .trxcap file per unique path hash. +//! +//! Replay (bench): +//! - Read .trxcap file. +//! - For each page in the input snapshot, MAP_FIXED at its VA, copy data. +//! - Call transformImpl_SSE64 (or SSE or game's x87). +//! - Diff memory against the saved output snapshot. +//! +//! This captures real game state and gives us true parity coverage, but has a +//! state-pollution avoidance problem in-game: we can't run *both* game x87 and +//! our SSE impls on the same live SceneObject without state bleeding between +//! calls. The solution is to replay offline with each impl starting from a +//! fresh copy of the captured input snapshot. + +const std = @import("std"); +const hook = @import("zhook"); +const logging = @import("../logging.zig"); + +var log: logging.Logger = .{}; + +// Win32 file/dir APIs — we run inside a Windows DLL, no libc filesystem. +const HANDLE = *anyopaque; +const INVALID_HANDLE: HANDLE = @ptrFromInt(std.math.maxInt(usize)); +extern "kernel32" fn CreateFileA( + name: [*:0]const u8, + desiredAccess: u32, + shareMode: u32, + security: ?*anyopaque, + creationDisposition: u32, + flags: u32, + template: ?HANDLE, +) callconv(.{ .x86_stdcall = .{} }) HANDLE; +extern "kernel32" fn WriteFile( + h: HANDLE, + buf: [*]const u8, + bytes: u32, + written: *u32, + ov: ?*anyopaque, +) callconv(.{ .x86_stdcall = .{} }) i32; +extern "kernel32" fn CloseHandle(h: HANDLE) callconv(.{ .x86_stdcall = .{} }) i32; +extern "kernel32" fn CreateDirectoryA(name: [*:0]const u8, security: ?*anyopaque) callconv(.{ .x86_stdcall = .{} }) i32; +extern "kernel32" fn SetFilePointer(h: HANDLE, lo: i32, hi: ?*i32, method: u32) callconv(.{ .x86_stdcall = .{} }) u32; + +const GENERIC_WRITE: u32 = 0x40000000; +const FILE_SHARE_READ: u32 = 0x1; +const CREATE_ALWAYS: u32 = 2; +const OPEN_ALWAYS: u32 = 4; +const FILE_ATTRIBUTE_NORMAL: u32 = 0x80; +const FILE_END: u32 = 2; + +fn writeAll(h: HANDLE, data: []const u8) bool { + var written: u32 = 0; + const ok = WriteFile(h, data.ptr, @intCast(data.len), &written, null); + return ok != 0 and written == data.len; +} + +pub const module_name: [*:0]const u8 = "transform_capture"; + +// Hook into game transformMatrix4x4 +const TransformFn = fn (u32, u32, u32, u32, u32) callconv(.{ .x86_thiscall = .{} }) void; +var transform_hook: hook.Detour(TransformFn) = .{}; + +// ============================================================================= +// SceneObject offsets (same as bone_sse, duplicated here so we don't couple) +// ============================================================================= + +const SO_MODEL_DATA_PTR: u32 = 0x010; +const SO_ANIM_CTX_PTR: u32 = 0x02C; +const SO_MODEL_CTR_PTR: u32 = 0x030; +const SO_SYNC_VALUE: u32 = 0x040; +const SO_BONE_RT_BASE: u32 = 0x090; +const SO_BONE_OUT_PTR: u32 = 0x094; +const SO_TEX_ANIM_OUT: u32 = 0x0A0; +const SO_COLOR_ANIM_OUT: u32 = 0x0A8; +const SO_SCALE1: u32 = 0x0AC; +const SO_SCALE2: u32 = 0x0B0; +const SO_SCALE3: u32 = 0x0B4; +const SO_HIERARCHY_PTR: u32 = 0x1C8; +const SO_HIERARCHY_IDX: u32 = 0x1DC; +const SO_FIELD_200: u32 = 0x200; +const SO_PARTICLE1: u32 = 0x3C4; +const SO_PARTICLE2: u32 = 0x3C8; +const SO_PARTICLE3: u32 = 0x3D0; +const SO_PARTICLE4: u32 = 0x3D4; + +// ============================================================================= +// Memory helpers +// ============================================================================= + +inline fn ru32(addr: u32) u32 { + return @as(*const u32, @ptrFromInt(addr)).*; +} +inline fn ru16(addr: u32) u16 { + return @as(*align(1) const u16, @ptrFromInt(addr)).*; +} + +// ============================================================================= +// Page capture infrastructure +// ============================================================================= + +const PAGE_SIZE: u32 = 0x1000; +const PAGE_MASK: u32 = 0xFFFFF000; +const MAX_CAPTURES: usize = 64; // cap on unique-hash scenarios we record +const MAX_PAGES_PER_CAPTURE: usize = 512; // each capture can touch up to 2MB + +/// One captured page's raw bytes plus its VA. +const PageBlock = extern struct { + va: u32, + data: [PAGE_SIZE]u8, +}; + +/// One captured scenario: input pages + output pages + entry args. +const Capture = struct { + path_hash: u32 = 0, + this: u32 = 0, + mat1: u32 = 0, + mat2: u32 = 0, + mat3: u32 = 0, + mat4: u32 = 0, + + input_page_count: u32 = 0, + output_page_count: u32 = 0, + input_pages: [*]PageBlock = undefined, + output_pages: [*]PageBlock = undefined, +}; + +// One scratch slot — we write captures to disk immediately, so only one in- +// flight capture needs memory at a time. +var scratch: Capture = .{}; +var scratch_input_pages: [MAX_PAGES_PER_CAPTURE]PageBlock align(16) = undefined; +var scratch_output_pages: [MAX_PAGES_PER_CAPTURE]PageBlock align(16) = undefined; + +// Deduplication: array of hashes we've already dumped. +var seen_hashes: [MAX_CAPTURES]u32 = [_]u32{0} ** MAX_CAPTURES; +var seen_hash_count: usize = 0; + +// Re-entry guard (transformMatrix4x4 recurses into itself for attachments). +var in_capture: bool = false; + +// Total captures written (== seen_hash_count, kept separate for clarity) +var capture_count: usize = 0; + +// Total hook calls seen (for coverage statistics) +var total_calls: u64 = 0; + +// Captures-written-per-log-slot so we can track distribution of hash values +var call_counts_per_hash: [MAX_CAPTURES]u32 = [_]u32{0} ** MAX_CAPTURES; + +// How many consecutive calls have yielded no new hash (for "coverage saturated" heuristic). +var calls_since_new_hash: u64 = 0; + +// Output directory — set at install time. Default = current working dir. +var capture_dir: []const u8 = "."; + +// Per-capture scratch: pages we've already saved this iteration. +var seen_pages: [MAX_PAGES_PER_CAPTURE]u32 = [_]u32{0} ** MAX_PAGES_PER_CAPTURE; +var seen_count: usize = 0; + +fn seenAdd(page_va: u32) bool { + // Linear scan; MAX_PAGES_PER_CAPTURE is small enough that this is cheap. + for (seen_pages[0..seen_count]) |p| if (p == page_va) return false; + if (seen_count >= MAX_PAGES_PER_CAPTURE) return false; + seen_pages[seen_count] = page_va; + seen_count += 1; + return true; +} + +fn seenReset() void { + seen_count = 0; +} + +/// Copy one 4KB page from game memory into the destination array. +/// Returns true if the page was new (added to seen list). +fn captureOnePage(addr: u32, dst: [*]PageBlock, idx: *u32) bool { + const page_va = addr & PAGE_MASK; + if (!seenAdd(page_va)) return false; + if (idx.* >= MAX_PAGES_PER_CAPTURE) return false; + dst[idx.*].va = page_va; + @memcpy(&dst[idx.*].data, @as([*]const u8, @ptrFromInt(page_va))[0..PAGE_SIZE]); + idx.* += 1; + return true; +} + +/// Capture a memory range (rounded to pages). +fn capturePagesCovering(addr: u32, size: u32, dst: [*]PageBlock, idx: *u32) void { + if (addr == 0 or size == 0) return; + const start_page = addr & PAGE_MASK; + const end_addr = addr +% size; + var cur = start_page; + while (cur < end_addr) : (cur +%= PAGE_SIZE) { + _ = captureOnePage(cur, dst, idx); + } +} + +// ============================================================================= +// Graph walk — identify all pages reachable from the SceneObject that +// transformMatrix4x4 might read. +// ============================================================================= + +fn walkAndCapture(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32, dst: [*]PageBlock, idx: *u32) void { + _ = mat4; + + // Direct pages + capturePagesCovering(this, 0x400, dst, idx); + capturePagesCovering(mat1, 0x40, dst, idx); + capturePagesCovering(mat2, 0x0C, dst, idx); + capturePagesCovering(mat3, 0x0C, dst, idx); + + // Following pointers + const anim_ctx = ru32(this + SO_ANIM_CTX_PTR); + capturePagesCovering(anim_ctx, 0x20, dst, idx); + + const model_ctr = ru32(this + SO_MODEL_CTR_PTR); + capturePagesCovering(model_ctr, 0x140, dst, idx); + if (model_ctr == 0) return; + + const model_hdr = ru32(model_ctr + 0x130); + capturePagesCovering(model_hdr, 0x200, dst, idx); + if (model_hdr == 0) return; + + // model_hdr reference arrays + const gs_durations = ru32(model_hdr + 0x18); + const gs_count = ru32(model_hdr + 0x14); + capturePagesCovering(gs_durations, gs_count * 4, dst, idx); + + const anim_lookup = ru32(model_hdr + 0x20); + capturePagesCovering(anim_lookup, 0x1000, dst, idx); // conservative; actual size unknown + + const bone_count = ru32(model_hdr + 0x34); + const bone_defs = ru32(model_hdr + 0x38); + capturePagesCovering(bone_defs, bone_count * 0x6C, dst, idx); + + // bone_rt / bone_out + const bone_rt = ru32(this + SO_BONE_RT_BASE); + capturePagesCovering(bone_rt, bone_count * 0x118, dst, idx); + const bone_out = ru32(this + SO_BONE_OUT_PTR); + capturePagesCovering(bone_out, bone_count * 0x40, dst, idx); + + // AnimData inside bone_defs references keyframe arrays + var bi: u32 = 0; + var bd = bone_defs; + while (bi < bone_count) : ({ + bi += 1; + bd += 0x6C; + }) { + // Three animation tracks per bone (trans/rot/scale) + inline for ([_]u32{ 0x0C, 0x28, 0x44 }) |track_off| { + const ad = bd + track_off; + const ranges = ru32(ad + 0x08); + const ranges_count = ru32(ad + 0x04); // nRanges + capturePagesCovering(ranges, ranges_count * 8, dst, idx); + const ts = ru32(ad + 0x10); + const kf_count = ru32(ad + 0x0C); + capturePagesCovering(ts, kf_count * 4, dst, idx); + const kf_base = ru32(ad + 0x18); + // Stride varies by track — trans/scale=12, rot=16, but spline modes + // use 36. Conservative: use 36 × kf_count per track. + capturePagesCovering(kf_base, kf_count * 36, dst, idx); + } + } + + // Output-buffer pages (will be WRITTEN but we need to capture input state + // too in case the game reads before writing) + capturePagesCovering(ru32(this + SO_TEX_ANIM_OUT), 0x200, dst, idx); + capturePagesCovering(ru32(this + SO_COLOR_ANIM_OUT), 0x200, dst, idx); + capturePagesCovering(ru32(this + SO_SCALE1), 0x200, dst, idx); + capturePagesCovering(ru32(this + SO_SCALE2), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_SCALE3), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_HIERARCHY_PTR), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_FIELD_200), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_PARTICLE1), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_PARTICLE2), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_PARTICLE3), 0x800, dst, idx); + capturePagesCovering(ru32(this + SO_PARTICLE4), 0x400, dst, idx); + + // Attachment data + const attach_count = ru32(model_hdr + 0x104); + const attach_data = ru32(model_hdr + 0x108); + capturePagesCovering(attach_data, attach_count * 0x30, dst, idx); + + // Texture/color/word anim data + inline for ([_]struct { cnt: u32, dat: u32, stride: u32 }{ + .{ .cnt = 0x54, .dat = 0x58, .stride = 0x34 }, // tex anim + .{ .cnt = 0x64, .dat = 0x68, .stride = 0x28 }, // color anim + .{ .cnt = 0x74, .dat = 0x78, .stride = 0x1C }, // word anim + .{ .cnt = 0xAC, .dat = 0xB0, .stride = 0x54 }, // bone keyframe + .{ .cnt = 0x11C, .dat = 0x120, .stride = 0xD4 }, // ribbon emitter + .{ .cnt = 0x124, .dat = 0x128, .stride = 0x7C }, // particle emitter + .{ .cnt = 0x134, .dat = 0x138, .stride = 0xDC }, // partsec + .{ .cnt = 0x13C, .dat = 0x140, .stride = 0x1F8 }, // partlarge + }) |sec| { + const c = ru32(model_hdr + sec.cnt); + const d = ru32(model_hdr + sec.dat); + capturePagesCovering(d, c * sec.stride, dst, idx); + } +} + +// ============================================================================= +// Path hash — distinguishes different code paths through transformMatrix4x4. +// Cheap to compute; aim to hit every major branch. +// ============================================================================= + +fn pathHash(this: u32) u32 { + var h: u32 = 0x811C9DC5; + + const model_ctr = ru32(this + SO_MODEL_CTR_PTR); + if (model_ctr == 0) return h; + const model_hdr = ru32(model_ctr + 0x130); + if (model_hdr == 0) return h; + + const bone_count = ru32(model_hdr + 0x34); + h = (h ^ bone_count) *% 0x01000193; + + const bone_defs = ru32(model_hdr + 0x38); + if (bone_count > 0 and bone_defs != 0) { + // Flags of first few bones (encodes billboard types, etc.) + const max_bones = @min(bone_count, 6); + var bi: u32 = 0; + while (bi < max_bones) : (bi += 1) { + const flags = ru32(bone_defs + bi * 0x6C + 0x04); + h = (h ^ flags) *% 0x01000193; + } + } + + // Which post-bone-loop sections are populated? + inline for ([_]u32{ 0x14, 0x54, 0x64, 0x74, 0xAC, 0x104, 0x11C, 0x124, 0x134, 0x13C }) |off| { + const v = ru32(model_hdr + off); + h = (h ^ (if (v == 0) @as(u32, 0) else 1)) *% 0x01000193; + } + + // emitter_ctx presence + const emitter_ctx = ru32(this + 0x1CC); + h = (h ^ (if (emitter_ctx == 0) @as(u32, 0) else 1)) *% 0x01000193; + + return h; +} + +// ============================================================================= +// Detour +// ============================================================================= + +fn transformDetour(this: u32, mat1: u32, mat2: u32, mat3: u32, mat4: u32) callconv(.{ .x86_thiscall = .{} }) void { + // Re-entry guard: transformMatrix4x4 recurses for attachment children. + if (in_capture) { + transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 }); + return; + } + + total_calls +%= 1; + + if (capture_count < MAX_CAPTURES) { + const h = pathHash(this); + var known_idx: ?usize = null; + for (seen_hashes[0..seen_hash_count], 0..) |sh, i| if (sh == h) { + known_idx = i; + break; + }; + if (known_idx) |idx| { + call_counts_per_hash[idx] +%= 1; + calls_since_new_hash +%= 1; + } else { + in_capture = true; + defer in_capture = false; + + scratch.path_hash = h; + scratch.this = this; + scratch.mat1 = mat1; + scratch.mat2 = mat2; + scratch.mat3 = mat3; + scratch.mat4 = mat4; + scratch.input_page_count = 0; + scratch.output_page_count = 0; + + seenReset(); + walkAndCapture(this, mat1, mat2, mat3, mat4, &scratch_input_pages, &scratch.input_page_count); + + transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 }); + + var oi: u32 = 0; + var pi: usize = 0; + while (pi < scratch.input_page_count) : (pi += 1) { + if (oi >= MAX_PAGES_PER_CAPTURE) break; + scratch_output_pages[oi].va = scratch_input_pages[pi].va; + @memcpy(&scratch_output_pages[oi].data, @as([*]const u8, @ptrFromInt(scratch_input_pages[pi].va))[0..PAGE_SIZE]); + oi += 1; + } + scratch.output_page_count = oi; + scratch.input_pages = &scratch_input_pages; + scratch.output_pages = &scratch_output_pages; + + const dump_err = dumpOneToDir(capture_dir, scratch); + const cov_err = appendCoverageLog(h, scratch.input_page_count); + log.fmt("capture: hash=0x{x:08} pages={d} dump={any} cov={any}\n", .{ + h, scratch.input_page_count, dump_err, cov_err, + }); + + if (seen_hash_count < MAX_CAPTURES) { + seen_hashes[seen_hash_count] = h; + call_counts_per_hash[seen_hash_count] = 1; + seen_hash_count += 1; + } + capture_count += 1; + calls_since_new_hash = 0; + return; + } + } else { + calls_since_new_hash +%= 1; + } + + transform_hook.callOriginal(.{ this, mat1, mat2, mat3, mat4 }); +} + +// ============================================================================= +// File format +// ============================================================================= + +const MAGIC: [4]u8 = .{ 'T', 'R', 'X', 'C' }; +const VERSION: u32 = 1; + +const FileHeader = extern struct { + magic: [4]u8 = MAGIC, + version: u32 = VERSION, + path_hash: u32, + this: u32, + mat1: u32, + mat2: u32, + mat3: u32, + mat4: u32, + input_page_count: u32, + output_page_count: u32, +}; + +/// Build a null-terminated path: "/". +fn buildPath(buf: []u8, dir: []const u8, filename: []const u8) ![:0]const u8 { + const total = dir.len + 1 + filename.len + 1; + if (total > buf.len) return error.NoSpaceLeft; + @memcpy(buf[0..dir.len], dir); + buf[dir.len] = '/'; + @memcpy(buf[dir.len + 1 ..][0..filename.len], filename); + buf[dir.len + 1 + filename.len] = 0; + return buf[0 .. dir.len + 1 + filename.len :0]; +} + +fn buildDirZ(buf: []u8, dir: []const u8) ![:0]const u8 { + if (dir.len + 1 > buf.len) return error.NoSpaceLeft; + @memcpy(buf[0..dir.len], dir); + buf[dir.len] = 0; + return buf[0..dir.len :0]; +} + +fn dumpOneToDir(dir_path: []const u8, c: Capture) !void { + var dir_buf: [256]u8 = undefined; + const dir_z = try buildDirZ(&dir_buf, dir_path); + _ = CreateDirectoryA(dir_z.ptr, null); // ignore result: already-exists is fine + + var name_buf: [64]u8 = undefined; + const fname = try std.fmt.bufPrint(&name_buf, "{x:08}.trxcap", .{c.path_hash}); + + var path_buf: [512]u8 = undefined; + const path = try buildPath(&path_buf, dir_path, fname); + + const h = CreateFileA(path.ptr, GENERIC_WRITE, FILE_SHARE_READ, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, null); + if (h == INVALID_HANDLE) return error.CreateFailed; + defer _ = CloseHandle(h); + + const hdr = FileHeader{ + .path_hash = c.path_hash, + .this = c.this, + .mat1 = c.mat1, + .mat2 = c.mat2, + .mat3 = c.mat3, + .mat4 = c.mat4, + .input_page_count = c.input_page_count, + .output_page_count = c.output_page_count, + }; + if (!writeAll(h, std.mem.asBytes(&hdr))) return error.WriteFailed; + if (!writeAll(h, @as([*]const u8, @ptrCast(c.input_pages))[0 .. c.input_page_count * @sizeOf(PageBlock)])) return error.WriteFailed; + if (!writeAll(h, @as([*]const u8, @ptrCast(c.output_pages))[0 .. c.output_page_count * @sizeOf(PageBlock)])) return error.WriteFailed; +} + +/// Append a line to coverage.log whenever we see a new path hash. +fn appendCoverageLog(new_hash: u32, page_count: u32) !void { + var dir_buf: [256]u8 = undefined; + const dir_z = try buildDirZ(&dir_buf, capture_dir); + _ = CreateDirectoryA(dir_z.ptr, null); + + var path_buf: [512]u8 = undefined; + const path = try buildPath(&path_buf, capture_dir, "coverage.log"); + + const h = CreateFileA(path.ptr, GENERIC_WRITE, FILE_SHARE_READ, null, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, null); + if (h == INVALID_HANDLE) return error.CreateFailed; + defer _ = CloseHandle(h); + _ = SetFilePointer(h, 0, null, FILE_END); + + var line_buf: [256]u8 = undefined; + const line = try std.fmt.bufPrint(&line_buf, "[#{d}] new hash 0x{x:08} (total_calls={d}, pages={d})\n", .{ + seen_hash_count + 1, new_hash, total_calls, page_count, + }); + if (!writeAll(h, line)) return error.WriteFailed; +} + +// ============================================================================= +// Install / Remove +// ============================================================================= + +/// Install with the given output directory (relative to WoW's current working +/// directory, usually the game install folder). Captures + coverage.log will +/// land there. Pass "." to write them next to WoW.exe. +pub fn install() bool { + return installTo("transform_captures"); +} + +pub fn installTo(dir: []const u8) bool { + log = logging.Logger.open("transform_capture", .both); + capture_dir = dir; + const ok = transform_hook.attach(0x714260, &transformDetour) == .ok; + log.fmt("install: hook_attach={any} dir={s}\n", .{ ok, dir }); + + // Smoke-test the write path so we know if file I/O is broken before any + // transform calls arrive. + var dir_buf: [256]u8 = undefined; + const dir_z = buildDirZ(&dir_buf, capture_dir) catch return ok; + const mkdir_ok = CreateDirectoryA(dir_z.ptr, null); + log.fmt("install: mkdir returned {d}\n", .{mkdir_ok}); + + var path_buf: [512]u8 = undefined; + const path = buildPath(&path_buf, capture_dir, "install_test.txt") catch return ok; + const h = CreateFileA(path.ptr, GENERIC_WRITE, FILE_SHARE_READ, null, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, null); + if (h == INVALID_HANDLE) { + log.fmt("install: CreateFileA FAILED for {s}\n", .{path}); + } else { + _ = writeAll(h, "transform_capture install test\n"); + _ = CloseHandle(h); + log.fmt("install: wrote install_test.txt ok\n", .{}); + } + return ok; +} + +pub fn remove() void { + transform_hook.detach(); +} + +pub fn getCaptureCount() usize { + return capture_count; +} + +pub fn getTotalCalls() u64 { + return total_calls; +} + +pub fn getCallsSinceNewHash() u64 { + return calls_since_new_hash; +} diff --git a/src/weirdperformance/weirdperformance.zig b/src/weirdperformance/weirdperformance.zig index 8f39d18..e76c9bc 100644 --- a/src/weirdperformance/weirdperformance.zig +++ b/src/weirdperformance/weirdperformance.zig @@ -19,6 +19,7 @@ const mod_mutex = @import("../mutex.zig"); const inflate_hook = @import("inflate_hook.zig"); const timer_fix = @import("timer_fix.zig"); const filecache = @import("filecache.zig"); +const transform_capture = @import("transform_capture.zig"); pub const module_name: [*:0]const u8 = "weirdperformance"; @@ -364,8 +365,17 @@ pub fn installHooks() void { log = logging.Logger.open(module_name, .both); var installed: u32 = 0; - // Bone transform SSE - if (transform_hook.attach(0x714260, &bone_sse64.transformImpl_SSE64) == .ok) installed += 1; + // Bone transform: either the f64 SSE impl for normal operation, OR the + // transform_capture hook for recording game-x87 state to disk. + // Compile-time flag picks one: the two are mutually exclusive because + // capture requires the game's x87 output (bone_sse64 would replace it). + const build_options = @import("build_options"); + const capture_mode = @hasDecl(build_options, "transform_capture") and build_options.transform_capture; + if (capture_mode) { + if (transform_capture.install()) installed += 1; + } else { + if (transform_hook.attach(0x714260, &bone_sse64.transformImpl_SSE64) == .ok) installed += 1; + } // Frustum clip SSE (1.9x speedup) if (clip_hook.attach(0x6318C0, &clip_sse.clipPolygonToSinglePlane) == .ok) installed += 1; diff --git a/src/worldmarkers/MPQ_LOAD_ORDER.md b/src/worldmarkers/MPQ_LOAD_ORDER.md new file mode 100644 index 0000000..0fe6499 --- /dev/null +++ b/src/worldmarkers/MPQ_LOAD_ORDER.md @@ -0,0 +1,42 @@ +# MPQ Load Order — WoW 1.12.1 + +`MPQ_InitializeArchives` (0x403740) loads in two phases. + +## Phase 1 — Base archives (loaded by index) + +Filename table at `0x82e12c`: + +| Index | Archive | +|-------|---------| +| 0 | model.MPQ | +| 1 | texture.MPQ | +| 2 | terrain.MPQ | +| 3 | wmo.MPQ | +| 4 | sound.MPQ | +| 5 | misc.MPQ | +| 6 | interface.MPQ | +| 7 | fonts.MPQ | +| 8 | speech.MPQ | +| 9 | dbc.MPQ | +| 10 | speech2.MPQ | + +Each opened via `OpenMPQArchiveWithPaths` (0x403b00) which tries `Data\name` then `..\Data\name`. + +## Phase 2 — Patch archives + +- `MPQArchiveEnumerator` (0x4039b0) discovers `patch-?.MPQ` files via glob +- Then opens `patch.MPQ` and any discovered `patch-X.MPQ` archives + +## Search order at file lookup time + +`File_FindInArchive` (0x6549a0) searches the global archive array at `0x8826b4` recursively — later-registered archives (patches) are checked first. The array is searched from `count-1` down to `0`, so patches override base content. + +## Global archive array + +``` +Struct at 0x8826b4: + +0x00 [0x8826b4]: capacity + +0x04 [0x8826b8]: count + +0x08 [0x8826bc]: array_ptr (SArchive**) + +0x0C [0x8826c0]: growth_incr +``` \ No newline at end of file diff --git a/src/worldmarkers/demo/MarkerPresets.lua b/src/worldmarkers/demo/MarkerPresets.lua new file mode 100644 index 0000000..6afb1c1 --- /dev/null +++ b/src/worldmarkers/demo/MarkerPresets.lua @@ -0,0 +1,143 @@ +-- MarkerPresets: save and restore world marker layouts. +-- Demonstrates the WorldMarker / GetWorldMarker / ClearWorldMarker API. +-- +-- /mp save — snapshot all current marker positions +-- /mp place — restore a saved layout (places all markers at once) +-- /mp clear — clear all markers +-- /mp list — list saved presets +-- /mp delete — delete a preset + +local NUM_MARKERS = 5 +local PREFIX = "|cff00ccff[Markers]|r " + +local function msg(text) + DEFAULT_CHAT_FRAME:AddMessage(PREFIX .. text) +end + +local function savePreset(name) + if not name or name == "" then + msg("Usage: /mp save ") + return + end + + local preset = {} + local count = 0 + for i = 1, NUM_MARKERS do + local x, y, z, area = GetWorldMarker(i) + if x then + preset[i] = { x = x, y = y, z = z, area = area } + count = count + 1 + end + end + + if count == 0 then + msg("No markers to save.") + return + end + + MarkerPresets_Saved[name] = preset + msg("Saved |cffffffff" .. name .. "|r (" .. count .. " marker" .. (count > 1 and "s" or "") .. ")") +end + +local function placePreset(name) + if not name or name == "" then + msg("Usage: /mp place ") + return + end + + local preset = MarkerPresets_Saved[name] + if not preset then + msg("No preset named |cffffffff" .. name .. "|r") + return + end + + if not CanSetWorldMarkers() then + msg("|cffff4444No permission|r (need leader or assist)") + return + end + + -- Clear existing markers, then place the saved ones + ClearWorldMarker() + + local count = 0 + for i = 1, NUM_MARKERS do + local m = preset[i] + if m then + WorldMarker(i, m.x, m.y, m.z) + count = count + 1 + end + end + + msg("Placed |cffffffff" .. name .. "|r (" .. count .. " marker" .. (count > 1 and "s" or "") .. ")") +end + +local function clearMarkers() + if not CanSetWorldMarkers() then + msg("|cffff4444No permission|r (need leader or assist)") + return + end + ClearWorldMarker() + msg("Cleared all markers.") +end + +local function listPresets() + local any = false + for name, preset in pairs(MarkerPresets_Saved) do + local count = 0 + for i = 1, NUM_MARKERS do + if preset[i] then count = count + 1 end + end + msg(" |cffffffff" .. name .. "|r — " .. count .. " marker" .. (count > 1 and "s" or "")) + any = true + end + if not any then + msg("No saved presets.") + end +end + +local function deletePreset(name) + if not name or name == "" then + msg("Usage: /mp delete ") + return + end + if not MarkerPresets_Saved[name] then + msg("No preset named |cffffffff" .. name .. "|r") + return + end + MarkerPresets_Saved[name] = nil + msg("Deleted |cffffffff" .. name .. "|r") +end + +local f = CreateFrame("Frame") +f:RegisterEvent("VARIABLES_LOADED") +f:SetScript("OnEvent", function() + MarkerPresets_Saved = MarkerPresets_Saved or {} +end) + +SLASH_MARKERPRESETS1 = "/mp" +SLASH_MARKERPRESETS2 = "/markerpreset" +SlashCmdList["MARKERPRESETS"] = function(input) + local cmd, rest = string.match(input, "^(%S+)%s*(.*)") + if not cmd then + msg("Commands: save | place | clear | list | delete") + return + end + + cmd = string.lower(cmd) + local name = rest ~= "" and rest or nil + + if cmd == "save" then + savePreset(name) + elseif cmd == "place" or cmd == "load" then + placePreset(name) + elseif cmd == "clear" then + clearMarkers() + elseif cmd == "list" or cmd == "ls" then + listPresets() + elseif cmd == "delete" or cmd == "del" or cmd == "rm" then + deletePreset(name) + else + msg("Unknown command: " .. cmd) + msg("Commands: save | place | clear | list | delete") + end +end diff --git a/src/worldmarkers/demo/MarkerPresets.toc b/src/worldmarkers/demo/MarkerPresets.toc new file mode 100644 index 0000000..fbf757f --- /dev/null +++ b/src/worldmarkers/demo/MarkerPresets.toc @@ -0,0 +1,7 @@ +## Interface: 11200 +## Title: [WeirdUtils] Marker Presets +## Notes: Save and restore world marker positions. Requires WeirdUtils World Markers. +## Version: 1.0 +## SavedVariablesPerCharacter: MarkerPresetsDB + +MarkerPresets.lua \ No newline at end of file diff --git a/src/worldmarkers/worldmarkers.zig b/src/worldmarkers/worldmarkers.zig index 874e2ad..f2267c5 100644 --- a/src/worldmarkers/worldmarkers.zig +++ b/src/worldmarkers/worldmarkers.zig @@ -22,7 +22,6 @@ const std = @import("std"); const hook = @import("zhook"); const lua = @import("../lua.zig"); const o = @import("offsets.zig"); -const offsets = @import("../offsets.zig"); const wow = @import("../wow.zig"); const logging = @import("../logging.zig"); @@ -356,7 +355,7 @@ fn placeMarker(index: usize, pos: Vec3) bool { // Store persistent definition marker_defs[index] = .{ .pos = pos, - .area_id = hook.readMem(u32, offsets.ZONE_AREA_ID), + .area_id = wow.getMapId(), .active = true, }; @@ -590,7 +589,7 @@ fn tickAnimations() void { if (player != 0) { const player_pos = getUnitPosition(player); if (player_pos.x != 0 or player_pos.y != 0 or player_pos.z != 0) { - const current_area = hook.readMem(u32, offsets.ZONE_AREA_ID); + const current_area = wow.getMapId(); for (0..NUM_MARKERS) |i| { if (!marker_defs[i].active) continue; if (marker_entities[i] != null) continue; // entity alive, skip diff --git a/src/wow.zig b/src/wow.zig index c7f2399..a2b38f9 100644 --- a/src/wow.zig +++ b/src/wow.zig @@ -189,6 +189,14 @@ pub fn isInBattleground() bool { return map_type == o.MAP_TYPE_BATTLEGROUND; } +/// Current map ID from the object manager (ObjMgr+0xCC). +/// Returns 0xFFFFFFFF if the object manager is not available. +pub fn getMapId() u32 { + const obj_mgr = hook.readMem(u32, o.OBJECT_MANAGER_PTR); + if (obj_mgr == 0) return 0xFFFFFFFF; + return hook.readMem(u32, obj_mgr + o.OBJMGR_MAP_ID_OFFSET); +} + // ============================================================================= // Game function wrappers // ============================================================================= diff --git a/tools/png2blp.py b/tools/png2blp.py new file mode 100644 index 0000000..abb991c --- /dev/null +++ b/tools/png2blp.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Convert PNG to BLP2 (DXT3 compressed, with mipmaps).""" + +import struct +import sys +from PIL import Image + + +def rgb_to_565(r, g, b): + return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3) + + +def color_565_to_rgb(c): + r = ((c >> 11) & 0x1F) << 3 + g = ((c >> 5) & 0x3F) << 2 + b = (c & 0x1F) << 3 + return r, g, b + + +def color_distance(r0, g0, b0, r1, g1, b1): + return (r0 - r1) ** 2 + (g0 - g1) ** 2 + (b0 - b1) ** 2 + + +def encode_dxt1_block(pixels): + """Encode a 4x4 block of (r,g,b) tuples into 8-byte DXT1 color block. + pixels: list of 16 (r,g,b) tuples in row-major order. + """ + # Find min/max colors by luminance + min_c = min(pixels, key=lambda c: c[0] * 299 + c[1] * 587 + c[2] * 114) + max_c = max(pixels, key=lambda c: c[0] * 299 + c[1] * 587 + c[2] * 114) + + color0 = rgb_to_565(*max_c) + color1 = rgb_to_565(*min_c) + + # Ensure color0 > color1 for 4-color mode + if color0 == color1: + # All same color, indices all 0 + return struct.pack('> 4) << (i * 4) + + alpha_data = struct.pack(' 1 or h > 1: + w = max(1, w // 2) + h = max(1, h // 2) + levels.append(img.resize((w, h), Image.LANCZOS)) + return [(l.size[0], l.size[1], l.tobytes()) for l in levels] + + +def png_to_blp2(png_path, blp_path): + img = Image.open(png_path).convert('RGBA') + width, height = img.size + + mipmaps = generate_mipmaps(img) + num_mipmaps = min(len(mipmaps), 16) + + # Encode each mipmap level as DXT3 + encoded = [] + for w, h, data in mipmaps[:num_mipmaps]: + encoded.append(encode_dxt3_image(data, w, h)) + + # BLP2 header: 1172 bytes + # 4 (magic) + 4 (type) + 1 (compression) + 1 (alpha_depth) + 1 (alpha_type) + # + 1 (has_mips) + 4 (width) + 4 (height) + 64 (offsets) + 64 (lengths) + # + 1024 (palette) = 1172 + header_size = 4 + 4 + 1 + 1 + 1 + 1 + 4 + 4 + 64 + 64 + 1024 + + # Calculate offsets + offsets = [0] * 16 + lengths = [0] * 16 + offset = header_size + for i, data in enumerate(encoded): + offsets[i] = offset + lengths[i] = len(data) + offset += len(data) + + # Build header + header = bytearray() + header.extend(b'BLP2') + header.extend(struct.pack(' {blp_path}') + print(f' {width}x{height}, {num_mipmaps} mipmaps, DXT3') + print(f' {offset} bytes total') + + +if __name__ == '__main__': + if len(sys.argv) != 3: + print(f'Usage: {sys.argv[0]} input.png output.blp') + sys.exit(1) + png_to_blp2(sys.argv[1], sys.argv[2]) diff --git a/tools/stormlib.py b/tools/stormlib.py new file mode 100644 index 0000000..ba6f2ea --- /dev/null +++ b/tools/stormlib.py @@ -0,0 +1,330 @@ +""" +StormLib ctypes wrapper for MPQ archive operations. +Auto-downloads prebuilt StormLib from GitHub releases if not found locally. +""" + +import ctypes +from ctypes import c_bool, c_char_p, c_uint32, c_uint64, c_void_p, byref, POINTER +from pathlib import Path +import platform + +SCRIPT_DIR = Path(__file__).parent +LIB_DIR = SCRIPT_DIR / "lib" + +STORMLIB_VERSION = "v9.31" +STORMLIB_RELEASE_URL = f"https://github.com/ladislav-zezula/StormLib/releases/download/{STORMLIB_VERSION}" + + +def _download_stormlib(): + """Download prebuilt StormLib for the current platform.""" + import urllib.request + import ssl + import tempfile + import shutil + import subprocess + + system = platform.system().lower() + machine = platform.machine().lower() + + LIB_DIR.mkdir(parents=True, exist_ok=True) + + if system == "linux" and machine in ("x86_64", "amd64"): + lib_name = "libstorm.so" + deb_name = f"libstorm-dev_{STORMLIB_VERSION}_amd64.deb" + url = f"{STORMLIB_RELEASE_URL}/{deb_name}" + lib_path = LIB_DIR / lib_name + + print(f"Downloading StormLib {STORMLIB_VERSION}...") + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + pkg_path = tmpdir / "stormlib.deb" + req = urllib.request.Request(url, headers={"User-Agent": "M2Editor/1.0"}) + # Try default SSL first, fall back to unverified if certs missing + try: + ctx = ssl.create_default_context() + resp_obj = urllib.request.urlopen(req, timeout=60, context=ctx) + except ssl.SSLCertVerificationError: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + resp_obj = urllib.request.urlopen(req, timeout=60, context=ctx) + with resp_obj as resp: + pkg_path.write_bytes(resp.read()) + subprocess.run(["ar", "x", str(pkg_path)], cwd=tmpdir, check=True, capture_output=True) + for data_tar in tmpdir.glob("data.tar.*"): + subprocess.run(["tar", "xf", str(data_tar)], cwd=tmpdir, check=True, capture_output=True) + break + for so_file in tmpdir.rglob("libstorm.so*"): + if so_file.is_file() and not so_file.is_symlink(): + shutil.copy2(so_file, lib_path) + return lib_path + + elif system == "windows" and machine in ("x86_64", "amd64"): + lib_name = "storm.dll" + url = f"{STORMLIB_RELEASE_URL}/stormlib_dll.zip" + lib_path = LIB_DIR / lib_name + + print(f"Downloading StormLib {STORMLIB_VERSION}...") + import zipfile + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + pkg_path = tmpdir / "stormlib.zip" + req = urllib.request.Request(url, headers={"User-Agent": "M2Editor/1.0"}) + with urllib.request.urlopen(req, timeout=60) as resp: + pkg_path.write_bytes(resp.read()) + with zipfile.ZipFile(pkg_path) as zf: + zf.extractall(tmpdir) + for dll in tmpdir.rglob("storm.dll"): + if "x64" in str(dll.parent).lower() or "64" in str(dll.parent): + shutil.copy2(dll, lib_path) + return lib_path + for dll in tmpdir.rglob("storm.dll"): + shutil.copy2(dll, lib_path) + return lib_path + + raise ImportError(f"No prebuilt StormLib for {platform.system()} {platform.machine()}") + + +def _load_stormlib(): + """Find and load the StormLib shared library.""" + system = platform.system().lower() + lib_name = "libstorm.so" if system == "linux" else "storm.dll" + + search_paths = [ + LIB_DIR / lib_name, + Path("/usr/local/lib") / lib_name, + Path("/usr/lib") / lib_name, + ] + + for path in search_paths: + if path.exists(): + try: + return ctypes.CDLL(str(path)) + except OSError: + continue + + lib_path = _download_stormlib() + return ctypes.CDLL(str(lib_path)) + + +_lib = _load_stormlib() + +# Type aliases +HANDLE = c_void_p +DWORD = c_uint32 +LCID = c_uint32 +ULONGLONG = c_uint64 +TCHAR = c_char_p + +# MPQ constants +MPQ_CREATE_LISTFILE = 0x00100000 +MPQ_CREATE_ATTRIBUTES = 0x00200000 +MPQ_CREATE_ARCHIVE_V1 = 0x00000000 + +MPQ_FILE_COMPRESS = 0x00000200 +MPQ_FILE_REPLACEEXISTING = 0x80000000 + +MPQ_COMPRESSION_ZLIB = 0x02 +MPQ_OPEN_READ_ONLY = 0x00000100 +SFILE_OPEN_FROM_MPQ = 0x00000000 + +# Function signatures +_lib.SFileCreateArchive.argtypes = [TCHAR, DWORD, DWORD, POINTER(HANDLE)] +_lib.SFileCreateArchive.restype = c_bool + +_lib.SFileOpenArchive.argtypes = [TCHAR, DWORD, DWORD, POINTER(HANDLE)] +_lib.SFileOpenArchive.restype = c_bool + +_lib.SFileCloseArchive.argtypes = [HANDLE] +_lib.SFileCloseArchive.restype = c_bool + +_lib.SFileFlushArchive.argtypes = [HANDLE] +_lib.SFileFlushArchive.restype = c_bool + +_lib.SFileAddFileEx.argtypes = [HANDLE, TCHAR, c_char_p, DWORD, DWORD, DWORD] +_lib.SFileAddFileEx.restype = c_bool + +_lib.SFileCreateFile.argtypes = [HANDLE, c_char_p, ULONGLONG, DWORD, LCID, DWORD, POINTER(HANDLE)] +_lib.SFileCreateFile.restype = c_bool + +_lib.SFileWriteFile.argtypes = [HANDLE, c_void_p, DWORD, DWORD] +_lib.SFileWriteFile.restype = c_bool + +_lib.SFileFinishFile.argtypes = [HANDLE] +_lib.SFileFinishFile.restype = c_bool + +_lib.SFileCloseFile.argtypes = [HANDLE] +_lib.SFileCloseFile.restype = c_bool + +_lib.SFileHasFile.argtypes = [HANDLE, c_char_p] +_lib.SFileHasFile.restype = c_bool + +_lib.SFileCompactArchive.argtypes = [HANDLE, TCHAR, c_bool] +_lib.SFileCompactArchive.restype = c_bool + +_lib.SFileOpenFileEx.argtypes = [HANDLE, c_char_p, DWORD, POINTER(HANDLE)] +_lib.SFileOpenFileEx.restype = c_bool + +_lib.SFileGetFileSize.argtypes = [HANDLE, POINTER(DWORD)] +_lib.SFileGetFileSize.restype = DWORD + +_lib.SFileReadFile.argtypes = [HANDLE, c_void_p, DWORD, POINTER(DWORD), c_void_p] +_lib.SFileReadFile.restype = c_bool + +_lib.SFileRemoveFile.argtypes = [HANDLE, c_char_p, DWORD] +_lib.SFileRemoveFile.restype = c_bool + +class SFILE_FIND_DATA(ctypes.Structure): + _fields_ = [ + ("cFileName", ctypes.c_char * 1024), + ("szPlainName", ctypes.c_char_p), + ("dwHashIndex", DWORD), + ("dwBlockIndex", DWORD), + ("dwFileSize", DWORD), + ("dwFileFlags", DWORD), + ("dwCompSize", DWORD), + ("dwFileTimeLo", DWORD), + ("dwFileTimeHi", DWORD), + ("lcLocale", LCID), + ] + +_lib.SFileFindFirstFile.argtypes = [HANDLE, c_char_p, POINTER(SFILE_FIND_DATA), c_char_p] +_lib.SFileFindFirstFile.restype = HANDLE + +_lib.SFileFindNextFile.argtypes = [HANDLE, POINTER(SFILE_FIND_DATA)] +_lib.SFileFindNextFile.restype = c_bool + +_lib.SFileFindClose.argtypes = [HANDLE] +_lib.SFileFindClose.restype = c_bool + + +class StormLibError(Exception): + pass + + +class MPQArchive: + """Context manager for MPQ archive operations.""" + + def __init__(self, path, mode='r', max_files=0): + self.path = Path(path) + self.mode = mode + self.handle = HANDLE() + self._closed = False + + path_bytes = str(self.path).encode('utf-8') + + if mode == 'r': + if not _lib.SFileOpenArchive(path_bytes, 0, MPQ_OPEN_READ_ONLY, byref(self.handle)): + raise StormLibError(f"Failed to open archive: {path}") + elif mode == 'w': + if max_files == 0: + max_files = 4096 + flags = MPQ_CREATE_LISTFILE | MPQ_CREATE_ATTRIBUTES | MPQ_CREATE_ARCHIVE_V1 + if not _lib.SFileCreateArchive(path_bytes, flags, max_files, byref(self.handle)): + raise StormLibError(f"Failed to create archive: {path}") + elif mode == 'a': + if not _lib.SFileOpenArchive(path_bytes, 0, 0, byref(self.handle)): + raise StormLibError(f"Failed to open archive for writing: {path}") + else: + raise ValueError(f"Invalid mode: {mode}") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + return False + + def close(self): + if not self._closed and self.handle: + _lib.SFileFlushArchive(self.handle) + _lib.SFileCloseArchive(self.handle) + self._closed = True + + def add_file(self, source_path, archive_name, + compression=MPQ_COMPRESSION_ZLIB, + flags=MPQ_FILE_COMPRESS | MPQ_FILE_REPLACEEXISTING): + """Add a file from disk to the archive.""" + if self.mode == 'r': + raise StormLibError("Archive opened in read-only mode") + source_bytes = str(source_path).encode('utf-8') + archive_bytes = archive_name.replace('/', '\\').encode('utf-8') + if not _lib.SFileAddFileEx(self.handle, source_bytes, archive_bytes, + flags, compression, compression): + raise StormLibError(f"Failed to add file: {source_path} -> {archive_name}") + + def add_data(self, data, archive_name, + compression=MPQ_COMPRESSION_ZLIB, + flags=MPQ_FILE_COMPRESS | MPQ_FILE_REPLACEEXISTING): + """Add raw bytes as a file in the archive.""" + if self.mode == 'r': + raise StormLibError("Archive opened in read-only mode") + archive_bytes = archive_name.replace('/', '\\').encode('utf-8') + file_handle = HANDLE() + if not _lib.SFileCreateFile(self.handle, archive_bytes, 0, len(data), 0, + flags, byref(file_handle)): + raise StormLibError(f"Failed to create file in archive: {archive_name}") + try: + if not _lib.SFileWriteFile(file_handle, data, len(data), compression): + raise StormLibError(f"Failed to write file data: {archive_name}") + if not _lib.SFileFinishFile(file_handle): + raise StormLibError(f"Failed to finish file: {archive_name}") + except: + _lib.SFileCloseFile(file_handle) + raise + + def has_file(self, archive_name): + archive_bytes = archive_name.replace('/', '\\').encode('utf-8') + return _lib.SFileHasFile(self.handle, archive_bytes) + + def read_file(self, archive_name): + """Read a file from the archive.""" + archive_bytes = archive_name.replace('/', '\\').encode('utf-8') + file_handle = HANDLE() + if not _lib.SFileOpenFileEx(self.handle, archive_bytes, SFILE_OPEN_FROM_MPQ, + byref(file_handle)): + raise StormLibError(f"Failed to open file: {archive_name}") + try: + high_size = DWORD() + size = _lib.SFileGetFileSize(file_handle, byref(high_size)) + if size == 0xFFFFFFFF: + raise StormLibError(f"Failed to get file size: {archive_name}") + buffer = ctypes.create_string_buffer(size) + read_size = DWORD() + if not _lib.SFileReadFile(file_handle, buffer, size, byref(read_size), None): + raise StormLibError(f"Failed to read file: {archive_name}") + return buffer.raw[:read_size.value] + finally: + _lib.SFileCloseFile(file_handle) + + def remove_file(self, archive_name): + if self.mode == 'r': + raise StormLibError("Archive opened in read-only mode") + archive_bytes = archive_name.replace('/', '\\').encode('utf-8') + if not _lib.SFileRemoveFile(self.handle, archive_bytes, 0): + raise StormLibError(f"Failed to remove file: {archive_name}") + + def compact(self): + if self.mode == 'r': + raise StormLibError("Archive opened in read-only mode") + if not _lib.SFileCompactArchive(self.handle, None, False): + raise StormLibError("Failed to compact archive") + + def list_files(self, pattern="*"): + """List all files in the archive matching a pattern.""" + files = [] + find_data = SFILE_FIND_DATA() + find_handle = _lib.SFileFindFirstFile(self.handle, pattern.encode('utf-8'), + byref(find_data), None) + if not find_handle: + return files + try: + while True: + filename = find_data.cFileName.decode('utf-8', errors='replace') + if filename and not filename.startswith('('): + files.append(filename) + if not _lib.SFileFindNextFile(find_handle, byref(find_data)): + break + finally: + _lib.SFileFindClose(find_handle) + return files