// SPDX-License-Identifier: GPL-3.0-or-later // MoonWell compatibility module for WarcraftXL. // // The stock 3.3.5a executable stays untouched on disk. These narrowly scoped // changes are verified and applied to the process image during WarcraftXL's // boot phase, before GlueXML is loaded. #include "game/Script.hpp" #include "wxl/PluginApi.h" #include "SpellOverrides.hpp" #include #include #include #include #include #include #include #include #include namespace moonwell { namespace { const WXL_Api* g_api = nullptr; void Log(int level, const char* format, ...) { if (!g_api || !g_api->Log) return; char message[1024]{}; va_list args; va_start(args, format); vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args); va_end(args); g_api->Log(level, "MoonWell", "%s", message); } #define WLOG_INFO(...) Log(WXL_LOG_INFO, __VA_ARGS__) #define WLOG_ERROR(...) Log(WXL_LOG_ERROR, __VA_ARGS__) bool PatchMemory(void* destination, const void* source, size_t size) { DWORD oldProtection = 0; if (!VirtualProtect(destination, size, PAGE_EXECUTE_READWRITE, &oldProtection)) return false; std::memcpy(destination, source, size); FlushInstructionCache(GetCurrentProcess(), destination, size); DWORD ignored = 0; VirtualProtect(destination, size, oldProtection, &ignored); return true; } template bool Hook(const char* name, uintptr_t address, Fn* detour, Fn** original) { return g_api && g_api->HookAttach && g_api->HookAttach( name, address, reinterpret_cast(detour), reinterpret_cast(original), WXL_HOOK_DEFAULT_PRIORITY) != 0; } template bool HookByName(const char* point, Fn* detour, Fn** original) { return g_api && g_api->HookAttachByName && g_api->HookAttachByName( point, reinterpret_cast(detour), reinterpret_cast(original), WXL_HOOK_DEFAULT_PRIORITY) != 0; } constexpr uint32_t kTraitorFlag = 0x40000000u; bool g_loginCharacterIsTraitor = false; constexpr char kLaunchAccountVariable[] = "MOONWELL_LAUNCH_ACCOUNT"; constexpr char kLaunchTicketVariable[] = "MOONWELL_LAUNCH_TICKET"; constexpr char kDeveloperLoginVariable[] = "MOONWELL_DEV_LOGIN"; constexpr char kLauncherLoginMarker[] = "__MOONWELL_LAUNCHER__"; constexpr wchar_t kLauncherProtocol[] = L"moonwell://authorize?source=client"; constexpr std::array kLauncherExecutables = { L"MoonWell.exe", L"MoonWellLauncher.exe", }; constexpr size_t kMaxLaunchAccountLength = 320; constexpr size_t kLaunchTicketLength = 16; std::array g_launchAccount{}; std::array g_launchTicket{}; volatile LONG g_hasLauncherAuth = 0; bool g_launchedWithTicket = false; bool g_developerLogin = false; volatile LONG g_launcherLoginState = 0; // 0=pending, 1=claimed, 2=submitted DWORD g_launcherAuthCapturedAt = 0; DWORD g_clientThreadId = 0; template bool ConsumeEnvironmentVariable(const char* name, std::array& output) { SetLastError(ERROR_SUCCESS); const DWORD required = GetEnvironmentVariableA(name, nullptr, 0); if (!required) { SetEnvironmentVariableA(name, nullptr); return false; } if (required > output.size()) { SetEnvironmentVariableA(name, nullptr); WLOG_ERROR("moonwell: rejected oversized launcher environment value %s", name); return false; } const DWORD written = GetEnvironmentVariableA( name, output.data(), static_cast(output.size())); SetEnvironmentVariableA(name, nullptr); return written > 0 && written < output.size(); } template bool IsPrintableAscii(const std::array& value) { for (const unsigned char character : value) { if (character == 0) return true; if (character < 0x21 || character > 0x7e) return false; } return false; } bool IsValidLauncherTicket() { if (std::strlen(g_launchTicket.data()) != kLaunchTicketLength) return false; for (size_t index = 0; index < kLaunchTicketLength; ++index) { const char character = g_launchTicket[index]; if (!((character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9'))) return false; } return true; } void ClearLauncherAuth() { SecureZeroMemory(g_launchAccount.data(), g_launchAccount.size()); SecureZeroMemory(g_launchTicket.data(), g_launchTicket.size()); InterlockedExchange(&g_hasLauncherAuth, 0); } void CaptureLaunchEnvironment() { const bool hasAccount = ConsumeEnvironmentVariable( kLaunchAccountVariable, g_launchAccount); const bool hasTicket = ConsumeEnvironmentVariable( kLaunchTicketVariable, g_launchTicket); g_hasLauncherAuth = hasAccount && hasTicket && IsPrintableAscii(g_launchAccount) && IsValidLauncherTicket() ? 1 : 0; InterlockedExchange(&g_launcherLoginState, 0); g_launchedWithTicket = g_hasLauncherAuth != 0; g_launcherAuthCapturedAt = g_hasLauncherAuth ? GetTickCount() : 0; if (!g_hasLauncherAuth) { if (hasAccount || hasTicket) WLOG_ERROR("moonwell: incomplete or invalid launcher authorization data"); ClearLauncherAuth(); } std::array developerValue{}; const bool developerRequested = ConsumeEnvironmentVariable( kDeveloperLoginVariable, developerValue); #if defined(_DEBUG) g_developerLogin = developerRequested && std::strcmp(developerValue.data(), "1") == 0; #else g_developerLogin = false; #endif SecureZeroMemory(developerValue.data(), developerValue.size()); if (g_hasLauncherAuth) WLOG_INFO("moonwell: launcher authorization data accepted"); else if (g_developerLogin) WLOG_INFO("moonwell: developer login enabled for this Debug build"); } int __cdecl GetLaunchMode(void* state) { const char* mode = g_launchedWithTicket ? "launcher" : (g_developerLogin ? "developer" : "locked"); wxl::game::script::PushString(state, mode); return 1; } int __cdecl ConsumeLauncherAuth(void* state) { constexpr uintptr_t kLoginGlueReady = 0x00B6B474; constexpr uintptr_t kLoginServerHost = 0x00B6AF54; constexpr uintptr_t kLoginServerPort = 0x00B6AF5C; constexpr uintptr_t kLoginBusy = 0x00B6AA38; if (!g_hasLauncherAuth || !*reinterpret_cast(kLoginGlueReady) || !*reinterpret_cast(kLoginServerHost) || !*reinterpret_cast(kLoginServerPort) || *reinterpret_cast(kLoginBusy)) return 0; if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0) return 0; if (!g_hasLauncherAuth) { InterlockedExchange(&g_launcherLoginState, 0); return 0; } wxl::game::script::PushString(state, g_launchAccount.data()); wxl::game::script::PushString(state, g_launchTicket.data()); ClearLauncherAuth(); InterlockedExchange(&g_launcherLoginState, 2); WLOG_INFO("moonwell: launcher authorization consumed by Lua bridge"); return 2; } bool TryOpenAdjacentLauncher() { std::array path{}; const DWORD length = GetModuleFileNameW(nullptr, path.data(), static_cast(path.size())); if (!length || length >= path.size()) return false; wchar_t* slash = std::wcsrchr(path.data(), L'\\'); if (!slash) return false; ++slash; const size_t prefixLength = static_cast(slash - path.data()); for (const wchar_t* executable : kLauncherExecutables) { const size_t launcherLength = std::wcslen(executable); if (prefixLength + launcherLength >= path.size()) continue; std::wmemcpy(slash, executable, launcherLength + 1); if (GetFileAttributesW(path.data()) == INVALID_FILE_ATTRIBUTES) continue; if (reinterpret_cast(ShellExecuteW( nullptr, L"open", path.data(), nullptr, nullptr, SW_SHOWNORMAL)) > 32) return true; } return false; } int __cdecl OpenLauncher(void* state) { bool opened = TryOpenAdjacentLauncher(); if (!opened) { opened = reinterpret_cast(ShellExecuteW( nullptr, L"open", kLauncherProtocol, nullptr, nullptr, SW_SHOWNORMAL)) > 32; } wxl::game::script::PushBoolean(state, opened); return 1; } using DefaultServerLoginCallbackFn = int(__cdecl*)(void*); using BeginServerLoginFn = void(__cdecl*)(const char*, const char*); DefaultServerLoginCallbackFn g_nextDefaultServerLogin = nullptr; bool SubmitLauncherAuthorization() { if (!g_hasLauncherAuth || g_launcherLoginState != 0) return false; constexpr uintptr_t kBeginServerLogin = 0x004D8A30; constexpr uintptr_t kLoginGlueReady = 0x00B6B474; constexpr uintptr_t kLoginServerHost = 0x00B6AF54; constexpr uintptr_t kLoginServerPort = 0x00B6AF5C; constexpr uintptr_t kLoginBusy = 0x00B6AA38; if (!*reinterpret_cast(kLoginGlueReady) || !*reinterpret_cast(kLoginServerHost) || !*reinterpret_cast(kLoginServerPort) || *reinterpret_cast(kLoginBusy)) return false; if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0) return false; if (!g_hasLauncherAuth) { InterlockedExchange(&g_launcherLoginState, 0); return false; } reinterpret_cast(kBeginServerLogin)( g_launchAccount.data(), g_launchTicket.data()); if (!*reinterpret_cast(kLoginBusy)) { InterlockedExchange(&g_launcherLoginState, 0); return false; } ClearLauncherAuth(); InterlockedExchange(&g_launcherLoginState, 2); WLOG_INFO("moonwell: launcher authorization submitted to login engine"); return true; } int __cdecl DefaultServerLoginHook(void* state) { const char* account = wxl::game::script::IsString(state, 1) ? wxl::game::script::ToString(state, 1) : nullptr; if (!account || std::strcmp(account, kLauncherLoginMarker) != 0) return g_nextDefaultServerLogin ? g_nextDefaultServerLogin(state) : 0; SubmitLauncherAuthorization(); return 0; } using GxSetProjectionFn = void(__fastcall*)(void* self, void* edx, const void* projection); GxSetProjectionFn g_nextSetProjection = nullptr; // FrameXML's stock SetCreature only accepts a creature entry and waits // for its client cache record. Encounter Journal already has the exact // CreatureDisplayInfo ID, so accept it as an optional second argument. constexpr uintptr_t kSetCreature = 0x00597960; constexpr uintptr_t kGetCurrentModelFrame = 0x004A81B0; constexpr uintptr_t kModelFrameTypeToken = 0x00C0E4D4; constexpr uintptr_t kApplyCreatureCacheRecord = 0x00597700; using SetCreatureFn = int(__cdecl*)(void* state); using ApplyCreatureCacheRecordFn = void(__fastcall*)(void* frame, void* edx, const void* cacheRecord); SetCreatureFn g_nextSetCreature = nullptr; alignas(4) std::array g_encounterJournalCreatureRecord{}; void* GetCurrentModelFrame(void* state, uint32_t typeToken) { // FrameScript_GetObject takes the type token on the stack, but the // 3.3.5 client also expects lua_State in ESI (an internal calling // convention not expressible with a regular C function pointer). void* frame = nullptr; __asm { mov esi, state push typeToken mov eax, kGetCurrentModelFrame call eax add esp, 4 mov frame, eax } return frame; } int __cdecl SetCreatureDisplayInfoHook(void* state) { const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0; if (!state || !wxl::game::script::IsNumber(state, 3)) return result; const double requestedDisplayInfo = wxl::game::script::ToNumber(state, 3); if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0) return result; const auto displayInfo = static_cast(requestedDisplayInfo); __try { const uint32_t typeToken = *reinterpret_cast(kModelFrameTypeToken); if (!typeToken) return result; void* frame = GetCurrentModelFrame(state, typeToken); if (!frame) return result; // Both the initial loader and the later character-appearance // pass read displayInfo at +0x24. Keep this record alive and // attach it to the frame: character models use it asynchronously // to resolve CreatureDisplayInfoExtra, baked skin and equipment. g_encounterJournalCreatureRecord.fill(0); g_encounterJournalCreatureRecord[9] = displayInfo; *reinterpret_cast(static_cast(frame) + 0x378) = g_encounterJournalCreatureRecord.data(); reinterpret_cast(kApplyCreatureCacheRecord)( frame, nullptr, g_encounterJournalCreatureRecord.data()); } __except (EXCEPTION_EXECUTE_HANDLER) { WLOG_ERROR("moonwell: SetCreature displayInfo bridge failed for %u", displayInfo); } return result; } void InstallEncounterJournalModelPreview() { if (!Hook("MoonWellSetCreatureDisplayInfo", kSetCreature, &SetCreatureDisplayInfoHook, &g_nextSetCreature)) { WLOG_ERROR("moonwell: encounter journal model hook installation failed"); return; } WLOG_INFO("moonwell: encounter journal displayInfo model bridge installed"); } struct CameraTransition { float zoom = 1.0f; float verticalOffset = 0.0f; float startZoom = 1.0f; float startVerticalOffset = 0.0f; float targetZoom = 1.0f; float targetVerticalOffset = 0.0f; DWORD startedAt = 0; DWORD duration = 0; } g_characterCreateCamera; void UpdateCharacterCreateCamera(DWORD now) { auto& camera = g_characterCreateCamera; if (!camera.duration) { camera.zoom = camera.targetZoom; camera.verticalOffset = camera.targetVerticalOffset; return; } const DWORD elapsed = now - camera.startedAt; float t = static_cast(elapsed) / static_cast(camera.duration); if (t >= 1.0f) { camera.zoom = camera.targetZoom; camera.verticalOffset = camera.targetVerticalOffset; camera.duration = 0; return; } t = t * t * (3.0f - 2.0f * t); camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t; camera.verticalOffset = camera.startVerticalOffset + (camera.targetVerticalOffset - camera.startVerticalOffset) * t; } int __cdecl SetCharacterCreateCamera(void* state) { const bool enabled = state && wxl::game::script::IsNumber(state, 1) && wxl::game::script::ToNumber(state, 1) != 0.0; const float faceZoom = state && wxl::game::script::IsNumber(state, 2) ? static_cast(wxl::game::script::ToNumber(state, 2)) : 2.0f; const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3) ? static_cast(wxl::game::script::ToNumber(state, 3)) : -0.65f; double requestedDuration = state && wxl::game::script::IsNumber(state, 4) ? wxl::game::script::ToNumber(state, 4) : 500.0; if (requestedDuration < 0.0) requestedDuration = 0.0; if (requestedDuration > 2000.0) requestedDuration = 2000.0; const DWORD now = GetTickCount(); UpdateCharacterCreateCamera(now); auto& camera = g_characterCreateCamera; camera.startZoom = camera.zoom; camera.startVerticalOffset = camera.verticalOffset; camera.targetZoom = enabled && faceZoom > 1.0f ? faceZoom : 1.0f; camera.targetVerticalOffset = enabled ? faceVerticalOffset : 0.0f; camera.startedAt = now; camera.duration = static_cast(requestedDuration); UpdateCharacterCreateCamera(now); return 0; } void __fastcall CharacterCreateProjectionHook(void* self, void* edx, const void* projection) { if (!g_nextSetProjection) return; UpdateCharacterCreateCamera(GetTickCount()); const auto& camera = g_characterCreateCamera; if (projection && (camera.zoom != 1.0f || camera.verticalOffset != 0.0f)) { const float* source = static_cast(projection); if (source[11] > 0.5f) { float adjusted[16]; std::memcpy(adjusted, source, sizeof(adjusted)); adjusted[0] *= camera.zoom; adjusted[5] *= camera.zoom; adjusted[9] += camera.verticalOffset; g_nextSetProjection(self, edx, adjusted); return; } } g_nextSetProjection(self, edx, projection); } void InstallCharacterCreateCamera() { constexpr uintptr_t kGxDeviceVTable = 0x00A2E718; constexpr unsigned kGxSetProjectionSlot = 0xA0 / 4; void** vtable = reinterpret_cast(kGxDeviceVTable); void** slot = &vtable[kGxSetProjectionSlot]; if (*slot == reinterpret_cast(&CharacterCreateProjectionHook)) return; g_nextSetProjection = reinterpret_cast(*slot); void* replacement = reinterpret_cast(&CharacterCreateProjectionHook); if (!g_nextSetProjection || !PatchMemory(slot, &replacement, sizeof(replacement))) { g_nextSetProjection = nullptr; WLOG_ERROR("moonwell: character-create projection hook installation failed"); return; } WLOG_INFO("moonwell: smooth character-create camera installed"); } int __cdecl SetLoginCharacterFlags(void* state) { uint32_t flags = 0; if (state && wxl::game::script::IsNumber(state, 1)) flags = static_cast(wxl::game::script::ToNumber(state, 1)); g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0; wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor); return 1; } int __cdecl IsTraitor(void* state) { wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor); return 1; } struct Patch { const char* name; uintptr_t address; const uint8_t* expected; const uint8_t* replacement; size_t size; }; template bool Apply(const char* name, uintptr_t address, const std::array& expected, const std::array& replacement) { const auto* current = reinterpret_cast(address); if (std::memcmp(current, replacement.data(), N) == 0) return true; if (std::memcmp(current, expected.data(), N) != 0) { WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required", name, reinterpret_cast(address)); return false; } if (!PatchMemory(reinterpret_cast(address), replacement.data(), N)) { WLOG_ERROR("moonwell: '%s' patch failed at %p", name, reinterpret_cast(address)); return false; } WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast(address)); return true; } bool InstallGlueUnlock() { // Same stock-client checks formerly changed in the distributed Wow.exe. // They allow custom GlueXML while keeping WarcraftXL's callback validator intact. bool ok = true; ok &= Apply("glue archive override", 0x005F4DBF, std::array{0x74}, std::array{0xEB}); ok &= Apply("glue callback gate", 0x00816625, std::array{0x75}, std::array{0xEB}); ok &= Apply("glue callback result A", 0x0081663F, std::array{0x01}, std::array{0x03}); ok &= Apply("glue callback result B", 0x00816695, std::array{0x01}, std::array{0x03}); ok &= Apply("glue callback compare", 0x00816746, std::array{0x7F}, std::array{0xEB}); ok &= Apply("glue callback return", 0x0081675F, std::array{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D}, std::array{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED}); return ok; } bool InstallCharacterModePacket() { // CreateCharacter(name, modeId): capture Lua argument 2 and place it in // the final CMSG_CHAR_CREATE byte that is zero in the stock client. constexpr std::array expectedCreate = { 0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00, 0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4, 0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00, 0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3, 0xCC,0xCC,0xCC,0xCC,0xCC }; constexpr std::array replacementCreate = { 0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00, 0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A, 0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4, 0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC, 0x00,0x88,0x55,0xF4,0xC3 }; constexpr std::array expectedPacket = {0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00}; constexpr std::array replacementPacket = {0xE8,0x5F,0x08,0x00,0x00,0x90,0x90}; return Apply("CreateCharacter mode argument", 0x004E0C60, expectedCreate, replacementCreate) && Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F, expectedPacket, replacementPacket); } bool InstallCharacterFlags() { // GetCharacterInfo(index) gains return value 11: the character flags // from CharacterInfo+0x170. The glue code uses bit 0x40000000. constexpr uintptr_t patchAddress = 0x004E332D; constexpr uintptr_t returnAddress = 0x004E3336; constexpr uintptr_t luaPushNumber = 0x0084E2A0; constexpr std::array expected = {0x83,0xC4,0x2C,0x5E,0xB8}; if (*reinterpret_cast(patchAddress) == 0xE9) return true; if (std::memcmp(reinterpret_cast(patchAddress), expected.data(), expected.size()) != 0) { WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p", reinterpret_cast(patchAddress)); return false; } auto* cave = static_cast(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); if (!cave) { WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError()); return false; } const uint8_t prefix[] = { 0x83,0xEC,0x08, // sub esp, 8 0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h] 0xDD,0x1C,0x24, // fstp qword ptr [esp] 0x57, // push edi (lua_State*) 0xE8,0,0,0,0, // call lua_pushnumber 0x83,0xC4,0x38, // cleanup own args + stolen add esp,2Ch 0x5E, // stolen pop esi 0x6A,0x0B,0x58, // eax = 11 0xE9,0,0,0,0 // jump to stock epilogue }; static_assert(sizeof(prefix) == 30); std::memcpy(cave, prefix, sizeof(prefix)); const auto callRel = static_cast(luaPushNumber - (reinterpret_cast(cave) + 18)); const auto backRel = static_cast(returnAddress - (reinterpret_cast(cave) + 30)); std::memcpy(cave + 14, &callRel, sizeof(callRel)); std::memcpy(cave + 26, &backRel, sizeof(backRel)); FlushInstructionCache(GetCurrentProcess(), cave, 30); std::array jump{0xE9,0,0,0,0}; const auto caveRel = static_cast(reinterpret_cast(cave) - (patchAddress + jump.size())); std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel)); if (!PatchMemory(reinterpret_cast(patchAddress), jump.data(), jump.size())) { VirtualFree(cave, 0, MEM_RELEASE); WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed"); return false; } WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11"); return true; } void InstallBoot() { const bool glue = InstallGlueUnlock(); const bool create = InstallCharacterModePacket(); const bool flags = InstallCharacterFlags(); if (!glue || !create || !flags) WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above"); else WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)"); } using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function); using ValidateCallbackFn = void(__cdecl*)(uintptr_t); using GetContextFn = void*(__cdecl*)(); using ExecuteFn = void(__cdecl*)(const char*, uintptr_t, uintptr_t); using InitializeLuaFn = int(__cdecl*)(void*); using FramePumpFn = void(__cdecl*)(float, uint32_t); using FileOpenFn = int(__stdcall*)(void*, const char*, uint32_t, void**); using GlueModelRenderFn = void(__cdecl*)(void*); ValidateCallbackFn g_nextValidateCallback = nullptr; ExecuteFn g_nextExecute = nullptr; InitializeLuaFn g_nextInitializeLua = nullptr; FramePumpFn g_nextFramePump = nullptr; FileOpenFn g_nextFileOpen = nullptr; GlueModelRenderFn g_nextGlueModelRender = nullptr; PVOID volatile g_registeredState = nullptr; bool g_registeringMoonWell = false; bool IsMoonWellCallback(uintptr_t callback) { return callback == reinterpret_cast(&SetLoginCharacterFlags) || callback == reinterpret_cast(&IsTraitor) || callback == reinterpret_cast(&SetCharacterCreateCamera) || callback == reinterpret_cast(&GetLaunchMode) || callback == reinterpret_cast(&ConsumeLauncherAuth) || callback == reinterpret_cast(&OpenLauncher); } void RegisterLuaFunctionsForCurrentState(); void __cdecl ValidateCallbackHook(uintptr_t callback) { RegisterLuaFunctionsForCurrentState(); if (!IsMoonWellCallback(callback) && g_nextValidateCallback) g_nextValidateCallback(callback); } void RegisterLuaFunctions(void* state) { if (g_registeringMoonWell) return; if (!state || state == g_registeredState) return; constexpr uintptr_t kRegisterFunction = 0x00817F90; const auto registrar = reinterpret_cast(kRegisterFunction); g_registeringMoonWell = true; registrar("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags); registrar("MoonWellIsTraitor", &IsTraitor); registrar("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera); registrar("MoonWellGetLaunchMode", &GetLaunchMode); registrar("MoonWellConsumeLauncherAuth", &ConsumeLauncherAuth); registrar("MoonWellOpenLauncher", &OpenLauncher); g_registeringMoonWell = false; InterlockedExchangePointer(&g_registeredState, state); WLOG_INFO("moonwell: Lua functions registered for state %p", state); } void RegisterLuaFunctionsForCurrentState() { constexpr uintptr_t kGetContext = 0x00817DB0; RegisterLuaFunctions(reinterpret_cast(kGetContext)()); } void RegisterLuaFunctionsBeforeHooksEnable() { RegisterLuaFunctionsForCurrentState(); } void __cdecl OnClientMessage(void*, const void*) { RegisterLuaFunctionsForCurrentState(); } struct WindowSearch { DWORD processId; HWND window; }; BOOL CALLBACK FindClientWindow(HWND window, LPARAM parameter) { auto* search = reinterpret_cast(parameter); DWORD processId = 0; GetWindowThreadProcessId(window, &processId); if (processId != search->processId || !IsWindowVisible(window)) return TRUE; search->window = window; return FALSE; } DWORD WINAPI ScheduleLuaBootstrap(LPVOID) { HWND clientWindow = nullptr; WLOG_INFO("moonwell: Lua bootstrap scheduler started"); for (unsigned attempt = 0; attempt < 600; ++attempt) { if (!clientWindow) { clientWindow = FindWindowW(L"GxWindowClass", nullptr); DWORD ownerProcessId = 0; if (clientWindow) GetWindowThreadProcessId(clientWindow, &ownerProcessId); if (ownerProcessId != GetCurrentProcessId()) clientWindow = nullptr; if (!clientWindow) { WindowSearch search{GetCurrentProcessId(), nullptr}; EnumWindows(&FindClientWindow, reinterpret_cast(&search)); clientWindow = search.window; } } if (clientWindow) { WLOG_INFO("moonwell: client window %p found for Lua bootstrap", clientWindow); // BeginServerLogin only snapshots the supplied credentials and starts // the client's asynchronous login state machine. Waiting here keeps the // call clear of engine initialization and Glue archive mounting. for (unsigned attempt = 0; attempt < 80 && g_hasLauncherAuth; ++attempt) { Sleep(250); SubmitLauncherAuthorization(); } return g_hasLauncherAuth ? 1 : 0; } Sleep(100); } WLOG_ERROR("moonwell: timed out waiting for UI-thread Lua bootstrap"); return 1; } void __cdecl ExecuteHook(const char* source, uintptr_t argument2, uintptr_t argument3) { RegisterLuaFunctionsForCurrentState(); if (g_nextExecute) g_nextExecute(source, argument2, argument3); } int __cdecl InitializeLuaHook(void* allocatorContext) { const int initialized = g_nextInitializeLua ? g_nextInitializeLua(allocatorContext) : 0; if (initialized) RegisterLuaFunctionsForCurrentState(); return initialized; } void __cdecl FramePumpHook(float deltaSeconds, uint32_t frameTimeMs) { RegisterLuaFunctionsForCurrentState(); if (g_nextFramePump) g_nextFramePump(deltaSeconds, frameTimeMs); RegisterLuaFunctionsForCurrentState(); if (g_hasLauncherAuth && GetTickCount() - g_launcherAuthCapturedAt >= 3000) SubmitLauncherAuthorization(); } int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out) { // The engine-init callback and synchronous Glue loader run on the same client // thread. Polling here catches Lua becoming live before AccountLogin executes, // without ever touching the state from background asset I/O workers. if (GetCurrentThreadId() == g_clientThreadId) RegisterLuaFunctionsForCurrentState(); const int result = g_nextFileOpen ? g_nextFileOpen(archive, name, flags, out) : 0; return result; } void __cdecl GlueModelRenderHook(void* frame) { RegisterLuaFunctionsForCurrentState(); if (g_nextGlueModelRender) g_nextGlueModelRender(frame); } void __cdecl OnFrame(void*, const void*) { RegisterLuaFunctionsForCurrentState(); // Glue scripting is not a reliable authentication trigger: a broken // cosmetic widget can abort AccountLogin_OnShow before it reaches the // login call. Present is emitted on the client thread after the engine // and Glue subsystem are ready, so submit once after a short grace period. if (g_hasLauncherAuth && GetTickCount() - g_launcherAuthCapturedAt >= 3000) SubmitLauncherAuthorization(); } bool InstallLuaBridge() { const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook, &g_nextValidateCallback); const bool executor = HookByName("Lua.Execute", &ExecuteHook, &g_nextExecute); const bool initializer = Hook("Lua.Initialize", 0x00819BB0, &InitializeLuaHook, &g_nextInitializeLua); const bool defaultLogin = Hook("MoonWell.DefaultServerLogin", 0x004DC260, &DefaultServerLoginHook, &g_nextDefaultServerLogin); const bool framePump = HookByName("Frame.Pump", &FramePumpHook, &g_nextFramePump); const bool fileOpen = HookByName("Io.FileOpen", &FileOpenHook, &g_nextFileOpen); const bool glueRender = HookByName("Gx.GlueModelRender", &GlueModelRenderHook, &g_nextGlueModelRender); if (!validator || !executor || !initializer || !defaultLogin || !framePump || !fileOpen || !glueRender) WLOG_ERROR("moonwell: Lua bridge hook installation failed"); return validator && executor && initializer && defaultLogin && framePump && fileOpen && glueRender; } } } const WXL_PluginInfo* __cdecl WXL_Query(void) { static const WXL_PluginInfo info = { sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWell", 1, WXL_CLIENT_BUILD, }; return &info; } int __cdecl WXL_Load(const WXL_Api* api) { if (!api || api->apiVersion != WXL_API_VERSION) return 0; moonwell::g_api = api; moonwell::g_clientThreadId = GetCurrentThreadId(); moonwell::CaptureLaunchEnvironment(); moonwell::InstallBoot(); const bool lua = moonwell::InstallLuaBridge(); // Event ordinal 5 is wxl::events::Event::OnFrame in API v1. It is emitted // from Present on the client thread even while only the Glue UI is active. if (api->Subscribe) { api->Subscribe(5, &moonwell::OnFrame, nullptr); api->Subscribe(17, &moonwell::OnClientMessage, nullptr); } moonwell::RegisterLuaFunctionsBeforeHooksEnable(); if (HANDLE bootstrapThread = CreateThread( nullptr, 0, &moonwell::ScheduleLuaBootstrap, nullptr, 0, nullptr)) CloseHandle(bootstrapThread); else api->Log(WXL_LOG_ERROR, "MoonWell", "%s", "failed to start Lua bootstrap scheduler"); moonwell::InstallCharacterCreateCamera(); moonwell::InstallEncounterJournalModelPreview(); const bool spells = moonwell::spells::Install(api); const bool ready = lua && spells; api->Log(ready ? WXL_LOG_INFO : WXL_LOG_ERROR, "MoonWell", "%s", ready ? "MoonWell extension loaded" : "MoonWell extension loaded with errors"); return ready ? 1 : 0; }