940 lines
40 KiB
C++
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;
|
|
}
|