Files
moonwell-client/modules/moonwell/src/MoonWell.cpp
T
2026-08-17 20:24:00 +04:00

940 lines
40 KiB
C++

// 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 <windows.h>
#include <shellapi.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <cwchar>
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 <class Fn>
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<void*>(detour),
reinterpret_cast<void**>(original), WXL_HOOK_DEFAULT_PRIORITY) != 0;
}
template <class Fn>
bool HookByName(const char* point, Fn* detour, Fn** original)
{
return g_api && g_api->HookAttachByName && g_api->HookAttachByName(
point, reinterpret_cast<void*>(detour), reinterpret_cast<void**>(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<const wchar_t*, 2> kLauncherExecutables = {
L"MoonWell.exe", L"MoonWellLauncher.exe",
};
constexpr size_t kMaxLaunchAccountLength = 320;
constexpr size_t kLaunchTicketLength = 16;
std::array<char, kMaxLaunchAccountLength + 1> g_launchAccount{};
std::array<char, kLaunchTicketLength + 1> 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 <size_t Size>
bool ConsumeEnvironmentVariable(const char* name, std::array<char, Size>& 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<DWORD>(output.size()));
SetEnvironmentVariableA(name, nullptr);
return written > 0 && written < output.size();
}
template <size_t Size>
bool IsPrintableAscii(const std::array<char, Size>& 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<char, 8> 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<volatile uint8_t*>(kLoginGlueReady)
|| !*reinterpret_cast<void* volatile*>(kLoginServerHost)
|| !*reinterpret_cast<void* volatile*>(kLoginServerPort)
|| *reinterpret_cast<volatile uint32_t*>(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<wchar_t, MAX_PATH> path{};
const DWORD length = GetModuleFileNameW(nullptr, path.data(),
static_cast<DWORD>(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<size_t>(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<INT_PTR>(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<INT_PTR>(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<volatile uint8_t*>(kLoginGlueReady)
|| !*reinterpret_cast<void* volatile*>(kLoginServerHost)
|| !*reinterpret_cast<void* volatile*>(kLoginServerPort)
|| *reinterpret_cast<volatile uint32_t*>(kLoginBusy))
return false;
if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0)
return false;
if (!g_hasLauncherAuth)
{
InterlockedExchange(&g_launcherLoginState, 0);
return false;
}
reinterpret_cast<BeginServerLoginFn>(kBeginServerLogin)(
g_launchAccount.data(), g_launchTicket.data());
if (!*reinterpret_cast<volatile uint32_t*>(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<uint32_t, 10> 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<uint32_t>(requestedDisplayInfo);
__try
{
const uint32_t typeToken = *reinterpret_cast<const uint32_t*>(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<const void**>(static_cast<uint8_t*>(frame) + 0x378) =
g_encounterJournalCreatureRecord.data();
reinterpret_cast<ApplyCreatureCacheRecordFn>(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<float>(elapsed) / static_cast<float>(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<float>(wxl::game::script::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3)
? static_cast<float>(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<DWORD>(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<const float*>(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<void**>(kGxDeviceVTable);
void** slot = &vtable[kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&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<uint32_t>(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 <size_t N>
bool Apply(const char* name, uintptr_t address,
const std::array<uint8_t, N>& expected,
const std::array<uint8_t, N>& replacement)
{
const auto* current = reinterpret_cast<const uint8_t*>(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<void*>(address));
return false;
}
if (!PatchMemory(reinterpret_cast<void*>(address), replacement.data(), N))
{
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address));
return false;
}
WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast<void*>(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<uint8_t, 1>{0x74}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback gate", 0x00816625,
std::array<uint8_t, 1>{0x75}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback result A", 0x0081663F,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback result B", 0x00816695,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback compare", 0x00816746,
std::array<uint8_t, 1>{0x7F}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback return", 0x0081675F,
std::array<uint8_t, 7>{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D},
std::array<uint8_t, 7>{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<uint8_t, 64> 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<uint8_t, 64> 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<uint8_t, 7> expectedPacket =
{0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00};
constexpr std::array<uint8_t, 7> 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<uint8_t, 5> expected = {0x83,0xC4,0x2C,0x5E,0xB8};
if (*reinterpret_cast<const uint8_t*>(patchAddress) == 0xE9)
return true;
if (std::memcmp(reinterpret_cast<const void*>(patchAddress), expected.data(), expected.size()) != 0)
{
WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p",
reinterpret_cast<void*>(patchAddress));
return false;
}
auto* cave = static_cast<uint8_t*>(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<int32_t>(luaPushNumber -
(reinterpret_cast<uintptr_t>(cave) + 18));
const auto backRel = static_cast<int32_t>(returnAddress -
(reinterpret_cast<uintptr_t>(cave) + 30));
std::memcpy(cave + 14, &callRel, sizeof(callRel));
std::memcpy(cave + 26, &backRel, sizeof(backRel));
FlushInstructionCache(GetCurrentProcess(), cave, 30);
std::array<uint8_t, 5> jump{0xE9,0,0,0,0};
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
if (!PatchMemory(reinterpret_cast<void*>(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<uintptr_t>(&SetLoginCharacterFlags)
|| callback == reinterpret_cast<uintptr_t>(&IsTraitor)
|| callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera)
|| callback == reinterpret_cast<uintptr_t>(&GetLaunchMode)
|| callback == reinterpret_cast<uintptr_t>(&ConsumeLauncherAuth)
|| callback == reinterpret_cast<uintptr_t>(&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<RegisterFunctionFn>(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<GetContextFn>(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<WindowSearch*>(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<LPARAM>(&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;
}