// This file is part of ClassicAPI. // // ClassicAPI is free software: you can redistribute it and/or modify it under the terms // of the GNU General Public License as published by the Free Software Foundation, either // version 3 of the License, or (at your option) any later version. // // ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. See the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along with // ClassicAPI. If not, see . #include "Common.h" #include "Game.h" #include "MinHook.h" #include "Offsets.h" #include "config/FileSwitch.h" #include "event/Custom.h" #include "player/NameCache.h" #include "text/InlineTexture.h" #include "text/InlineTexturePool.h" #include "texture/Mask.h" #include "texture/Transform.h" static Game::FrameScript_Initialize_t FrameScript_Initialize_o = nullptr; static Game::LoadScriptFunctions_t LoadScriptFunctions_o = nullptr; static Game::LoadGlueScriptFunctions_t LoadGlueScriptFunctions_o = nullptr; // `Frame::RegisterEvent` is `__thiscall(this, eventName)`. MSVC can't emit // __thiscall on free functions, but a __fastcall function with a dummy EDX // arg matches the same register layout (ECX=this, EDX unused, stack=name). using FrameRegisterEvent_t = void(__fastcall *)(void *frame, void *edx, const char *eventName); static FrameRegisterEvent_t FrameRegisterEvent_o = nullptr; static void __fastcall InvalidFunctionPtrCheck_h() {} static bool __fastcall FrameScript_Initialize_h() { // Fire every module's PrepareForReload BEFORE the engine tears down the old // event table / Lua state. Each module that keeps reload-fragile state (Lua // refs, frame-pointer-keyed maps, cached event-slot indices) self-registers // via `static const Game::ReloadAutoRegister _reload{&PrepareForReload};`, // so this one call clears them all — a new module can't forget to wire in // (see the ReloadAutoRegister doc in Game.h). This hook fires on both // `/reload` and `/logout` (the engine re-inits the Lua state in both). Game::RunReloadCleanups(); // Persist the name cache before the engine tears down — a SAVE, not a clear, // so it stays an explicit call rather than a ReloadAutoRegister. Player::NameCache::Flush(); FrameScript_Initialize_o(); return true; } static void __fastcall LoadScriptFunctions_h() { LoadScriptFunctions_o(); Game::RunModuleRegistrations(); // Permit `Event::Custom::TryClaim` to actually write to the event // table from here on. Earlier writes (during the engine's own // boot-time `RegisterEvent` flurry, plus SuperWoWhook/etc.) can // race with the engine's table init and trigger `SMemFree` on slots // it still considers in-flight. Event::Custom::EnableWrites(); } static void __stdcall LoadGlueScriptFunctions_h() { LoadGlueScriptFunctions_o(); Game::RunGlueModuleRegistrations(); // World→glue teardown destroyed every world fontstring and text node the // inline-icon maps reference; forget them now that the glue UI is booting. // (Glue→world is covered by FrameScript_Initialize_h; the node-free hook // handles individual deaths, but bulk teardown paths may bypass it.) Text::InlineTexture::PrepareForReload(); Text::InlineTexturePool::PrepareForReload(); Texture::Transform::PrepareForReload(); Texture::Mask::PrepareForReload(); } // Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot // for any custom event still waiting. By this point the engine's table // is fully populated and SuperWoWhook / other DLLs have done their // post-rebuild writes, so the table state is settled and our backwards // walk finds genuine NULL slots near the tail. static void __fastcall FrameRegisterEvent_h(void *frame, void *edx, const char *eventName) { Event::Custom::RetryClaims(); // Throttled-internally; no-op when the cache or scan toggle is off. Player::NameCache::Tick(); FrameRegisterEvent_o(frame, edx, eventName); } // --------------------------------------------------------------------------- // Hook install — kept OFF the Windows loader lock. // // Every MH_EnableHook freezes all process threads (CreateToolhelp32Snapshot + // SuspendThread/GetThreadContext each). Doing ~90 of those from DllMain, under // the loader lock, is the pattern MinHook documents as unsafe: on machines // whose security stack intercepts thread suspension it stalled the remote // LoadLibrary thread past VanillaFixes' 10-second injection deadline, so VF // read the still-running thread as STILL_ACTIVE (259) and showed its generic // "compatible client" error — a false diagnosis of a slow load. // // So DllMain installs nothing. The install runs later, on a thread that does // NOT hold the loader lock, via one of two triggers that both funnel through // the latched EnsureInitialized(): // * VanillaFixes' `Load` export, called on the game's main thread after // injection (the sanctioned VF extension point), or // * a fallback worker thread we spawn from DllMain, ONLY when VanillaFixes // is not the loader (no VfPatcher.dll in the process), for injectors that // never call `Load`. // // The worker MUST stand down under VanillaFixes. VF creates the process // suspended and injects every dlls.txt DLL sequentially via remote threads; // a worker spawned from our DllMain starts the moment our own LoadLibrary // releases the loader lock — i.e. while VF is still injecting the NEXT // dlls.txt DLL. Its ~90-prologue install (plus the MH_ApplyQueued // thread-freeze) then races that DLL's DllMain patching the same engine // functions, which made SuperWoWhook.dll fail to load whenever ClassicAPI // preceded it in dlls.txt. `Load` has no such race: VfPatcher calls it on the // game's main thread after the process resumes, when every DllMain in the // injection chain has already completed, serialized in dlls.txt order. // --------------------------------------------------------------------------- static volatile LONG g_initClaimed = 0; // 0 until a thread takes the installer role static volatile LONG g_initDone = 0; // 0 until the install has finished static volatile LONG g_initResult = 1; // 0 = success, 1 = failure (until proven) static volatile LONG g_mhInitialized = 0; // MH_Initialize succeeded (gates detach teardown) static bool CreateAndQueue(uintptr_t offset, void *hook, void **original) { auto *target = reinterpret_cast(offset); if (MH_CreateHook(target, hook, original) != MH_OK) return false; // Queue only — a single MH_ApplyQueued below applies the whole batch in // one thread-freeze. if (MH_QueueEnableHook(target) != MH_OK) return false; return true; } static bool InstallHooks() { if (MH_Initialize() != MH_OK) return false; InterlockedExchange(&g_mhInitialized, 1); if (!CreateAndQueue(Offsets::FUN_INVALID_FUNCTION_PTR_CHECK, reinterpret_cast(InvalidFunctionPtrCheck_h), nullptr)) return false; // Four core init hooks — each runs glue logic (PrepareForReload, // RunModuleRegistrations, RunGlueModuleRegistrations, EnableWrites, // RetryClaims) tightly coupled to DllMain state, so a declarative // HookAutoRegister wouldn't simplify them. if (!CreateAndQueue(Offsets::FUN_FRAME_SCRIPT_INITIALIZE, reinterpret_cast(FrameScript_Initialize_h), reinterpret_cast(&FrameScript_Initialize_o))) return false; if (!CreateAndQueue(Offsets::FUN_LOAD_SCRIPT_FUNCTIONS, reinterpret_cast(LoadScriptFunctions_h), reinterpret_cast(&LoadScriptFunctions_o))) return false; if (!CreateAndQueue(Offsets::FUN_LOAD_GLUE_SCRIPT_FUNCTIONS, reinterpret_cast(LoadGlueScriptFunctions_h), reinterpret_cast(&LoadGlueScriptFunctions_o))) return false; if (!CreateAndQueue(Offsets::FUN_FRAME_REGISTER_EVENT, reinterpret_cast(FrameRegisterEvent_h), reinterpret_cast(&FrameRegisterEvent_o))) return false; // All feature hooks declared via `Game::HookAutoRegister` at file scope // in their respective modules (create + queue-enable, no apply yet). if (!Game::RunHookRegistrations()) return false; // One thread-freeze that activates every queued hook at once. return MH_ApplyQueued() == MH_OK; } // Runs InstallHooks exactly once. Returns 0 on success, 1 on failure. If a // second caller arrives while the install is in flight, it blocks briefly so // both callers observe the real result. static DWORD EnsureInitialized() { if (InterlockedCompareExchange(&g_initClaimed, 1, 0) == 0) { g_initResult = InstallHooks() ? 0 : 1; InterlockedExchange(&g_initDone, 1); } else { while (InterlockedCompareExchange(&g_initDone, 0, 0) == 0) Sleep(1); } return static_cast(g_initResult); } static DWORD WINAPI InitWorker(LPVOID) { EnsureInitialized(); return 0; } // VanillaFixes calls this on the game's MAIN thread after injection // (InitAdditionalDLLs -> GetProcAddress(module, "Load")), outside the loader // lock and with no timeout — the sanctioned VF extension point. Returns 0 on // success; VF reports any non-zero result to the user. Exported undecorated // as "Load" via src/ClassicAPI.def. extern "C" DWORD Load() { return EnsureInitialized(); } BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) { if (reason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); // The ONE thing that has to happen this early, and the only module this // file names directly. The engine reads its config file during boot, // several calls before VanillaFixes invokes `Load`, so a `-config` // switch cannot be applied from the normal install path — by then the // file has been read. This is a single 4-byte store, not a hook, so it // carries none of the loader-lock cost described above. Config::FileSwitch::Apply(); // Install nothing here (loader lock — see the block comment above). // // Under VanillaFixes, `Load` is the ONLY install trigger. VfPatcher.dll // is injected before any dlls.txt DLL, so its presence identifies VF as // the loader; and the `Load` mechanism shipped in the same VF release // (v1.4) as dlls.txt support itself, so any VF that loaded us will call // it. Spawning the worker here would race the DllMains of the dlls.txt // DLLs VF injects after us (see the block comment above). if (GetModuleHandleW(L"VfPatcher.dll") == nullptr) { // Any other injector: no `Load` caller exists, so install from a // worker off the lock. The new thread cannot run its body until // DllMain returns and the loader lock releases, and we never wait // on it. DisableThreadLibraryCalls suppressed its THREAD_ATTACH. HANDLE worker = CreateThread(nullptr, 0, InitWorker, nullptr, 0, nullptr); if (worker != nullptr) CloseHandle(worker); } } else if (reason == DLL_PROCESS_DETACH) { // Clean /quit path — only tear down if we actually initialized. // Flush before MH_Uninitialize — the cache's file I/O uses Win32 // directly (no MinHook involvement), so either order works in // practice, but flushing first lets us bail early if the hook // teardown ever grows side effects. Hard process termination (task // manager kill) bypasses this path; that's an inherent OS // limitation, and the 5-minute backstop flush in Remember() covers // the worst case there. if (g_mhInitialized) { Player::NameCache::Flush(); MH_Uninitialize(); } } return TRUE; }