518 lines
22 KiB
C++
518 lines
22 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 <array>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdarg>
|
|
|
|
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;
|
|
|
|
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*)();
|
|
RegisterFunctionFn g_nextRegisterFunction = nullptr;
|
|
ValidateCallbackFn g_nextValidateCallback = nullptr;
|
|
void* 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);
|
|
}
|
|
|
|
void __cdecl ValidateCallbackHook(uintptr_t callback)
|
|
{
|
|
if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
|
|
g_nextValidateCallback(callback);
|
|
}
|
|
|
|
void RegisterLuaFunctionsForCurrentState()
|
|
{
|
|
if (!g_nextRegisterFunction || g_registeringMoonWell) return;
|
|
constexpr uintptr_t kGetContext = 0x00817DB0;
|
|
void* state = reinterpret_cast<GetContextFn>(kGetContext)();
|
|
if (!state || state == g_registeredState) return;
|
|
|
|
g_registeringMoonWell = true;
|
|
g_nextRegisterFunction("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
|
|
g_nextRegisterFunction("MoonWellIsTraitor", &IsTraitor);
|
|
g_nextRegisterFunction("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
|
|
g_registeringMoonWell = false;
|
|
g_registeredState = state;
|
|
WLOG_INFO("moonwell: Lua functions registered for state %p", state);
|
|
}
|
|
|
|
void __cdecl RegisterFunctionHook(const char* name, wxl::game::script::Function function)
|
|
{
|
|
if (g_nextRegisterFunction) g_nextRegisterFunction(name, function);
|
|
RegisterLuaFunctionsForCurrentState();
|
|
}
|
|
|
|
bool InstallLuaBridge()
|
|
{
|
|
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
|
|
&g_nextValidateCallback);
|
|
const bool registrar = HookByName("Lua.RegisterFunction", &RegisterFunctionHook,
|
|
&g_nextRegisterFunction);
|
|
if (!validator || !registrar)
|
|
WLOG_ERROR("moonwell: Lua bridge hook installation failed");
|
|
return validator && registrar;
|
|
}
|
|
}
|
|
}
|
|
|
|
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::InstallBoot();
|
|
const bool lua = moonwell::InstallLuaBridge();
|
|
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;
|
|
}
|