3 Commits

Author SHA1 Message Date
sindoring e53b3c1784 авторитарный логин через лаунчер 2026-08-30 02:18:27 +04:00
sindoring 23b3a3b864 Merge branch 'master' into launcher-authoritive-auth 2026-08-29 14:42:21 +04:00
sindoring abde71ceb0 wip 2026-08-17 20:24:00 +04:00
9 changed files with 753 additions and 48 deletions
+3
View File
@@ -66,6 +66,9 @@ wxl_add_external_extension(wxl-unit-outline "${WXL_EXTERNAL_MODULES_DIR}/wxl-uni
# MoonWell-owned extensions. wxl-fdid-moonwell sorts after wxl-db2 and before # MoonWell-owned extensions. wxl-fdid-moonwell sorts after wxl-db2 and before
# the modern asset extensions, so it can layer custom CSV mappings over wxl.fdid. # the modern asset extensions, so it can layer custom CSV mappings over wxl.fdid.
wxl_add_external_extension(MoonWell "${WXL_LOCAL_MODULES_DIR}/moonwell") wxl_add_external_extension(MoonWell "${WXL_LOCAL_MODULES_DIR}/moonwell")
if(TARGET MoonWell)
target_link_libraries(MoonWell PRIVATE shell32)
endif()
wxl_add_external_extension(wxl-fdid-moonwell "${WXL_LOCAL_MODULES_DIR}/moonwell-fdid") wxl_add_external_extension(wxl-fdid-moonwell "${WXL_LOCAL_MODULES_DIR}/moonwell-fdid")
wxl_add_external_extension(wxl-moonwell-storage-fallback wxl_add_external_extension(wxl-moonwell-storage-fallback
"${WXL_LOCAL_MODULES_DIR}/moonwell-storage-fallback") "${WXL_LOCAL_MODULES_DIR}/moonwell-storage-fallback")
+87
View File
@@ -0,0 +1,87 @@
# Авторизация MoonWell через лаунчер
Клиентская часть не принимает логин и пароль в Release-сборке. Лаунчер передаёт игре
короткоживущую учётную сессию через environment block дочернего процесса, а GlueXML сразу
запускает стандартный SRP-вход и не показывает форму логина.
## Контракт запуска клиента
Перед `CreateProcess` лаунчер должен добавить в environment block только запускаемого `Wow.exe`:
```text
MOONWELL_LAUNCH_ACCOUNT=<account name>
MOONWELL_LAUNCH_TICKET=<single-use SRP password>
```
Обе переменные обязательны. Нативный модуль читает и удаляет их из окружения процесса до
загрузки GlueXML. Значения не должны передаваться в аргументах командной строки, писаться в
файл, лог или реестр.
Ограничения значений:
- account: печатный ASCII без пробелов, не более 320 байт;
- ticket: ровно 16 символов из `A-Z0-9` (около 82 бит при равномерной генерации);
- ticket должен быть криптографически случайным, одноразовым и жить не более 60 секунд;
- выпуск нового ticket должен отзывать предыдущий активный ticket этого аккаунта.
Пример Win32-последовательности лаунчера:
1. Пользователь авторизуется в лаунчере.
2. Лаунчер запрашивает у backend одноразовый game ticket.
3. Лаунчер создаёт отдельный environment block с двумя переменными выше.
4. Лаунчер запускает `Wow.exe` через `CreateProcessW`.
5. Лаунчер сразу затирает ticket в своей памяти.
Кнопка «Авторизоваться» сначала ищет `MoonWell.exe` или `MoonWellLauncher.exe` рядом с `Wow.exe`. Если файла нет,
она открывает URI `moonwell://authorize?source=client`. Установщик лаунчера должен зарегистрировать
схему `moonwell` для текущего пользователя.
## Обязательное изменение auth-сервера
Скрытие полей в GlueXML не является серверной защитой: пользователь контролирует свой клиент.
Чтобы вход действительно был возможен только через лаунчер, auth-сервер обязан для обычных
аккаунтов отказаться от постоянного password verifier и принимать только активный launcher ticket.
Для совместимости с неизменённым SRP-клиентом ticket используется как временный пароль:
1. Backend хранит для аккаунта один активный ticket либо подготовленные из него временные
SRP `salt` и `verifier`.
2. На `CMD_AUTH_LOGON_CHALLENGE` auth-сервер ищет активный непросроченный ticket аккаунта.
Если ticket отсутствует, сервер отклоняет вход до выполнения обычной password-проверки.
3. SRP challenge строится по временному verifier.
4. После успешного `CMD_AUTH_LOGON_PROOF` ticket атомарно помечается использованным.
5. Повторный proof, повторный запуск и истёкший ticket отклоняются.
Поскольку challenge содержит аккаунт, но не сам ticket, одновременно должен существовать не более
одного активного ticket на аккаунт.
Рекомендуется дополнительно привязать ticket к версии клиента и идентификатору launcher-сессии,
ограничить частоту выпуска и не возвращать его ни в какие аналитические события.
## Режим разработчика
Ручной логин компилируется только в `Debug`-варианте `MoonWell.dll`. Для локального запуска:
```powershell
.\run.ps1 -Env local -DeveloperLogin
```
Authserver должен независимо разрешить тот же аккаунт записью в `launcher_dev_account`
с точным `allowed_ip` и `enabled = 1`. Один только Debug-режим клиента никогда не обходит
серверную проверку.
Скрипт собирает Debug runtime и передаёт `MOONWELL_DEV_LOGIN=1` только запускаемому процессу.
Переменная одноразово удаляется нативным модулем. В Release-сборке она игнорируется.
Auth-сервер также должен отдельно разрешать постоянный SRP verifier только dev-аккаунтам — лучше
по allowlist аккаунтов вместе с VPN/IP-ограничением. Нельзя считать Debug-флаг клиента границей
безопасности и нельзя распространять Debug DLL игрокам.
## Поведение интерфейса
- валидная launcher-сессия: `AccountLoginUI` скрывается и сразу вызывается
`DefaultServerLogin(account, ticket)`;
- прямой Release-запуск: поля логина, пароля и параметры сохранения скрыты, основная кнопка
открывает лаунчер;
- Debug + `MOONWELL_DEV_LOGIN=1`: показывается прежняя форма ручного входа;
- сохранённые ранее логин и пароль удаляются при любом не-dev запуске.
+6
View File
@@ -113,6 +113,12 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента. Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
## Авторизация через лаунчер
Release-клиент принимает только одноразовую launcher-сессию и при прямом запуске показывает
кнопку открытия лаунчера вместо полей логина и пароля. Контракт переменных запуска, требования
к auth-серверу и Debug-режим описаны в [LAUNCHER_AUTH.md](LAUNCHER_AUTH.md).
## MPQ-пакеты ## MPQ-пакеты
Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки
+439 -17
View File
@@ -10,6 +10,7 @@
#include "SpellOverrides.hpp" #include "SpellOverrides.hpp"
#include <windows.h> #include <windows.h>
#include <shellapi.h>
#include <array> #include <array>
#include <cstddef> #include <cstddef>
@@ -17,6 +18,7 @@
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
#include <cstdarg> #include <cstdarg>
#include <cwchar>
namespace moonwell namespace moonwell
{ {
@@ -69,6 +71,259 @@ namespace moonwell
constexpr uint32_t kTraitorFlag = 0x40000000u; constexpr uint32_t kTraitorFlag = 0x40000000u;
bool g_loginCharacterIsTraitor = false; 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); using GxSetProjectionFn = void(__fastcall*)(void* self, void* edx, const void* projection);
GxSetProjectionFn g_nextSetProjection = nullptr; GxSetProjectionFn g_nextSetProjection = nullptr;
@@ -439,55 +694,207 @@ namespace moonwell
using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function); using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function);
using ValidateCallbackFn = void(__cdecl*)(uintptr_t); using ValidateCallbackFn = void(__cdecl*)(uintptr_t);
using GetContextFn = void*(__cdecl*)(); using GetContextFn = void*(__cdecl*)();
RegisterFunctionFn g_nextRegisterFunction = nullptr; 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; ValidateCallbackFn g_nextValidateCallback = nullptr;
void* g_registeredState = 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 g_registeringMoonWell = false;
bool IsMoonWellCallback(uintptr_t callback) bool IsMoonWellCallback(uintptr_t callback)
{ {
return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags) return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags)
|| callback == reinterpret_cast<uintptr_t>(&IsTraitor) || callback == reinterpret_cast<uintptr_t>(&IsTraitor)
|| callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera); || 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) void __cdecl ValidateCallbackHook(uintptr_t callback)
{ {
RegisterLuaFunctionsForCurrentState();
if (!IsMoonWellCallback(callback) && g_nextValidateCallback) if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
g_nextValidateCallback(callback); g_nextValidateCallback(callback);
} }
void RegisterLuaFunctionsForCurrentState() void RegisterLuaFunctions(void* state)
{ {
if (!g_nextRegisterFunction || g_registeringMoonWell) return; if (g_registeringMoonWell) return;
constexpr uintptr_t kGetContext = 0x00817DB0;
void* state = reinterpret_cast<GetContextFn>(kGetContext)();
if (!state || state == g_registeredState) return; if (!state || state == g_registeredState) return;
constexpr uintptr_t kRegisterFunction = 0x00817F90;
const auto registrar = reinterpret_cast<RegisterFunctionFn>(kRegisterFunction);
g_registeringMoonWell = true; g_registeringMoonWell = true;
g_nextRegisterFunction("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags); registrar("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
g_nextRegisterFunction("MoonWellIsTraitor", &IsTraitor); registrar("MoonWellIsTraitor", &IsTraitor);
g_nextRegisterFunction("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera); registrar("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
registrar("MoonWellGetLaunchMode", &GetLaunchMode);
registrar("MoonWellConsumeLauncherAuth", &ConsumeLauncherAuth);
registrar("MoonWellOpenLauncher", &OpenLauncher);
g_registeringMoonWell = false; g_registeringMoonWell = false;
g_registeredState = state; InterlockedExchangePointer(&g_registeredState, state);
WLOG_INFO("moonwell: Lua functions registered for state %p", state); WLOG_INFO("moonwell: Lua functions registered for state %p", state);
} }
void __cdecl RegisterFunctionHook(const char* name, wxl::game::script::Function function) void RegisterLuaFunctionsForCurrentState()
{
constexpr uintptr_t kGetContext = 0x00817DB0;
RegisterLuaFunctions(reinterpret_cast<GetContextFn>(kGetContext)());
}
void RegisterLuaFunctionsBeforeHooksEnable()
{ {
if (g_nextRegisterFunction) g_nextRegisterFunction(name, function);
RegisterLuaFunctionsForCurrentState(); 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() bool InstallLuaBridge()
{ {
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook, const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
&g_nextValidateCallback); &g_nextValidateCallback);
const bool registrar = HookByName("Lua.RegisterFunction", &RegisterFunctionHook, const bool executor = HookByName("Lua.Execute", &ExecuteHook, &g_nextExecute);
&g_nextRegisterFunction); const bool initializer = Hook("Lua.Initialize", 0x00819BB0,
if (!validator || !registrar) &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"); WLOG_ERROR("moonwell: Lua bridge hook installation failed");
return validator && registrar; return validator && executor && initializer && defaultLogin
&& framePump && fileOpen && glueRender;
} }
} }
} }
@@ -504,9 +911,24 @@ int __cdecl WXL_Load(const WXL_Api* api)
{ {
if (!api || api->apiVersion != WXL_API_VERSION) return 0; if (!api || api->apiVersion != WXL_API_VERSION) return 0;
moonwell::g_api = api; moonwell::g_api = api;
moonwell::g_clientThreadId = GetCurrentThreadId();
moonwell::CaptureLaunchEnvironment();
moonwell::InstallBoot(); moonwell::InstallBoot();
const bool lua = moonwell::InstallLuaBridge(); 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::InstallCharacterCreateCamera();
moonwell::InstallEncounterJournalModelPreview(); moonwell::InstallEncounterJournalModelPreview();
const bool spells = moonwell::spells::Install(api); const bool spells = moonwell::spells::Install(api);
+13
View File
@@ -205,6 +205,19 @@ function Remove-MoonWellRuntimeGarbage {
} }
} }
# GlueXML is shipped by patch-ruRU-4.MPQ. Historical development copies in
# the client root override that archive and can restore the password form.
foreach ($relativePath in @(
'Interface\GlueXML\AccountLogin.lua',
'Interface\GlueXML\AccountLogin.xml'
)) {
$legacyOverride = Join-Path $Destination $relativePath
if (Test-Path -LiteralPath $legacyOverride -PathType Leaf) {
Remove-Item -LiteralPath $legacyOverride -Force
Write-Host "Removed obsolete loose GlueXML override: $legacyOverride"
}
}
Remove-EmptyMoonWellDirectories -Destination $Destination Remove-EmptyMoonWellDirectories -Destination $Destination
} }
+20 -4
View File
@@ -1,7 +1,8 @@
param( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("production", "ptr", "local")] [ValidateSet("production", "ptr", "local")]
[string]$Env = "local" [string]$Env = "local",
[switch]$DeveloperLogin
) )
# Stop on errors # Stop on errors
@@ -68,9 +69,16 @@ if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and
-not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and -not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and
-not $isAdministrator) { -not $isAdministrator) {
Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..." Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..."
$elevationArguments = @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
"`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
)
if ($DeveloperLogin) {
$elevationArguments += "-DeveloperLogin"
}
$elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait ` $elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait `
-WorkingDirectory $ROOT ` -WorkingDirectory $ROOT `
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env -ArgumentList $elevationArguments
exit $elevatedProcess.ExitCode exit $elevatedProcess.ExitCode
} }
@@ -149,7 +157,8 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
# are uploaded by upload_to_s3.py and consumed by the launcher manifest. # are uploaded by upload_to_s3.py and consumed by the launcher manifest.
Stop-WowRuntime -ClientPath $WOW_HOME Stop-WowRuntime -ClientPath $WOW_HOME
Write-Host "Building and packaging WarcraftXL..." Write-Host "Building and packaging WarcraftXL..."
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME ` $runtimeConfiguration = if ($DeveloperLogin) { "Debug" } else { "Release" }
& $WXL_BUILD_SCRIPT -Configuration $runtimeConfiguration -ClientPath $WOW_HOME `
-PackagePath $DIST_DIR -Deploy -PackagePath $DIST_DIR -Deploy
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "WarcraftXL build/deploy failed!" Write-Error "WarcraftXL build/deploy failed!"
@@ -200,4 +209,11 @@ if ($realmlist) {
# --- Run WoW reload script # --- Run WoW reload script
Write-Host "Launching WoW..." Write-Host "Launching WoW..."
cmd /c $RELOAD_SCRIPT if ($DeveloperLogin) {
$env:MOONWELL_DEV_LOGIN = "1"
}
try {
cmd /c $RELOAD_SCRIPT
} finally {
Remove-Item Env:MOONWELL_DEV_LOGIN -ErrorAction SilentlyContinue
}
@@ -43,7 +43,8 @@ local Config = {
} }
local LoginState = { local LoginState = {
autoLoginTimer = nil, autoLoginDelay = Config.AUTO_LOGIN_DELAY, autoLoginAttempted = false, musicTimer = 0, sceneTimer = 0, currentFrame = 0, lastUpdateTime = 0, currentScene = 1 autoLoginTimer = nil, autoLoginDelay = Config.AUTO_LOGIN_DELAY, autoLoginAttempted = false, musicTimer = 0, sceneTimer = 0, currentFrame = 0, lastUpdateTime = 0, currentScene = 1,
launchMode = "locked", launcherLoginAttempted = false, luaBridgePending = true
} }
local ModelManager = { local ModelManager = {
@@ -51,7 +52,7 @@ local ModelManager = {
} }
local UICache = { local UICache = {
accountEdit = nil, passwordEdit = nil, saveAccountName = nil, savePassword = nil, autoLogin = nil, autoLoginText = nil, loginButton = nil, versionText = nil, realmName = nil, upgradeButton = nil, tosFrame = nil, tosAccept = nil, tosDecline = nil, backgroundTexture = nil, animatedTexture = nil, newLogo = nil, newLogoFrame = nil accountEdit = nil, passwordEdit = nil, saveAccountName = nil, savePassword = nil, autoLogin = nil, autoLoginText = nil, saveOptionsFrame = nil, launcherHint = nil, loginButton = nil, versionText = nil, realmName = nil, upgradeButton = nil, tosFrame = nil, tosAccept = nil, tosDecline = nil, backgroundTexture = nil, animatedTexture = nil, newLogo = nil, newLogoFrame = nil
} }
LOGIN_MODEL_LIGHTS = { LOGIN_MODEL_LIGHTS = {
@@ -78,6 +79,8 @@ local function InitializeUICache()
UICache.savePassword = _G["AccountLoginSavePassword"] UICache.savePassword = _G["AccountLoginSavePassword"]
UICache.autoLogin = _G["AccountLoginAutoLogin"] UICache.autoLogin = _G["AccountLoginAutoLogin"]
UICache.autoLoginText = _G["AccountLoginAutoLoginText"] UICache.autoLoginText = _G["AccountLoginAutoLoginText"]
UICache.saveOptionsFrame = _G["AccountLoginSaveOptionsFrame"]
UICache.launcherHint = _G["AccountLoginLauncherHint"]
UICache.loginButton = _G["AccountLoginLoginButton"] UICache.loginButton = _G["AccountLoginLoginButton"]
UICache.versionText = _G["AccountLoginVersion"] UICache.versionText = _G["AccountLoginVersion"]
UICache.realmName = _G["AccountLoginRealmName"] UICache.realmName = _G["AccountLoginRealmName"]
@@ -94,6 +97,106 @@ end
function GetLoginState() function GetLoginState()
return LoginState return LoginState
end end
local function ResolveLaunchMode()
if type(MoonWellGetLaunchMode) ~= "function" then
return "locked"
end
local mode = MoonWellGetLaunchMode()
if mode == "launcher" or mode == "developer" then
return mode
end
return "locked"
end
local function ApplyLaunchMode()
local developerMode = LoginState.launchMode == "developer"
local launcherMode = LoginState.launchMode == "launcher"
if launcherMode then
AccountLoginUI:Hide()
else
AccountLoginUI:Show()
AccountLoginUI:SetAlpha(1)
end
if developerMode then
UICache.accountEdit:Show()
UICache.passwordEdit:Show()
UICache.saveOptionsFrame:Show()
UICache.launcherHint:Hide()
UICache.loginButton:SetText(LOGIN)
else
UICache.accountEdit:Hide()
UICache.passwordEdit:Hide()
UICache.saveOptionsFrame:Hide()
UICache.launcherHint:Show()
UICache.loginButton:SetText("Авторизоваться")
end
end
function AccountLogin_OpenLauncher()
PlaySound("gsLogin")
local opened = false
if type(MoonWellOpenLauncher) == "function" then
opened = MoonWellOpenLauncher() and true or false
end
if not opened then
GlueDialog_Show("MOONWELL_LAUNCHER_NOT_FOUND")
end
end
function AccountLogin_PrimaryAction()
if LoginState.launchMode == "developer" then
AccountLogin_Login()
else
AccountLogin_OpenLauncher()
end
end
local function TryLauncherLogin()
if LoginState.launchMode ~= "launcher" or LoginState.launcherLoginAttempted then
return false
end
LoginState.launcherLoginAttempted = true
if type(MoonWellConsumeLauncherAuth) ~= "function" then
LoginState.launchMode = "locked"
ApplyLaunchMode()
return false
end
local accountName, ticket = MoonWellConsumeLauncherAuth()
if not accountName or accountName == "" or not ticket or ticket == "" then
LoginState.launchMode = "locked"
ApplyLaunchMode()
return false
end
SetSavedAccountName("")
SetSavedAccountList("")
SetUsesToken(false)
AccountLoginUI:Hide()
PlaySound("gsLogin")
DefaultServerLogin(accountName, ticket)
accountName = nil
ticket = nil
return true
end
local function TryLauncherFallbackLogin()
if not LoginState.luaBridgePending or LoginState.launcherLoginAttempted then
return false
end
-- MoonWell.dll intercepts this stock callback and substitutes the one-time
-- launcher credentials. With no launcher data it intentionally does nothing.
LoginState.launcherLoginAttempted = true
DefaultServerLogin("__MOONWELL_LAUNCHER__", "__MOONWELL_LAUNCHER__")
return true
end
-- ============================================================================ -- ============================================================================
-- SISTEMA DE ANIMACIÓN BLP -- SISTEMA DE ANIMACIÓN BLP
-- ============================================================================ -- ============================================================================
@@ -144,6 +247,11 @@ GlueDialogTypes["AUTO_LOGIN"] = {
UICache.autoLogin:SetChecked(0) UICache.autoLogin:SetChecked(0)
end, end,
} }
GlueDialogTypes["MOONWELL_LAUNCHER_NOT_FOUND"] = {
text = "Не удалось открыть лаунчер MoonWell. Запустите его вручную и нажмите «Играть».",
button1 = OKAY,
}
-- ============================================================================ -- ============================================================================
-- GESTIÓN DE MODELOS 3D -- GESTIÓN DE MODELOS 3D
-- ============================================================================ -- ============================================================================
@@ -151,10 +259,8 @@ local function CreateLoginModel(parent, modelData)
local model = CreateFrame("Model", nil, parent) local model = CreateFrame("Model", nil, parent)
local width, height = parent:GetSize() local width, height = parent:GetSize()
model:SetSize( model:SetWidth(width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1), model:SetHeight(height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1)
)
model:SetPoint("CENTER") model:SetPoint("CENTER")
model:SetModel("Character/Human/Male/HumanMale.mdx") model:SetModel("Character/Human/Male/HumanMale.mdx")
model:SetCamera(1) model:SetCamera(1)
@@ -194,6 +300,13 @@ end
-- ACTUALIZACIÓN DE LA ESCENA -- ACTUALIZACIÓN DE LA ESCENA
-- ============================================================================ -- ============================================================================
function LoginScene_OnUpdate(self, elapsed) function LoginScene_OnUpdate(self, elapsed)
if LoginState.luaBridgePending and type(MoonWellGetLaunchMode) == "function" then
LoginState.luaBridgePending = false
LoginState.launchMode = ResolveLaunchMode()
ApplyLaunchMode()
TryLauncherLogin()
end
if Config.LOGIN_AMBIENCE and not self.ambiencePlayed then if Config.LOGIN_AMBIENCE and not self.ambiencePlayed then
PlayGlueAmbience(Config.LOGIN_AMBIENCE, Config.AMBIENCE_FADE_TIME) PlayGlueAmbience(Config.LOGIN_AMBIENCE, Config.AMBIENCE_FADE_TIME)
self.ambiencePlayed = true self.ambiencePlayed = true
@@ -283,8 +396,8 @@ function AccountLogin_OnLoad(self)
model:SetModel("Character/Human/Male/HumanMale.mdx") model:SetModel("Character/Human/Male/HumanMale.mdx")
model:SetPoint("CENTER", 0, 0) model:SetPoint("CENTER", 0, 0)
model:SetSize(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1), model:SetWidth(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1)) model:SetHeight(self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
model:SetCamera(1) model:SetCamera(1)
if data[LOGIN_MODEL_STRUCT.LIGHT] then if data[LOGIN_MODEL_STRUCT.LIGHT] then
@@ -312,6 +425,14 @@ function AccountLogin_OnLoad(self)
end end
function AccountLogin_OnShow(self) function AccountLogin_OnShow(self)
LoginState.luaBridgePending = type(MoonWellGetLaunchMode) ~= "function"
LoginState.launchMode = ResolveLaunchMode()
ApplyLaunchMode()
-- Submit launcher credentials before constructing the decorative login UI.
-- A cosmetic Lua error must never be able to block authentication.
TryLauncherFallbackLogin()
self:Show() self:Show()
self:SetAlpha(1) self:SetAlpha(1)
WorldOfWarcraftRating:Hide() WorldOfWarcraftRating:Hide()
@@ -341,12 +462,10 @@ function AccountLogin_OnShow(self)
elseif self.animatedTexture then elseif self.animatedTexture then
self.animatedTexture:Hide() self.animatedTexture:Hide()
end end
AccountLoginUI:Show()
AccountLoginUI:SetAlpha(1)
if not self.newLogo then if not self.newLogo then
self.newLogoFrame = CreateFrame("Frame", nil, self) self.newLogoFrame = CreateFrame("Frame", nil, self)
self.newLogoFrame:SetSize(Config.LOGO_SIZE, Config.LOGO_SIZE) self.newLogoFrame:SetWidth(Config.LOGO_SIZE)
self.newLogoFrame:SetHeight(Config.LOGO_SIZE)
self.newLogoFrame:SetPoint("TOPLEFT", Config.LOGO_POSITION_X, Config.LOGO_POSITION_Y) self.newLogoFrame:SetPoint("TOPLEFT", Config.LOGO_POSITION_X, Config.LOGO_POSITION_Y)
self.newLogo = self.newLogoFrame:CreateTexture("AccountLoginNewLogo", "OVERLAY") self.newLogo = self.newLogoFrame:CreateTexture("AccountLoginNewLogo", "OVERLAY")
@@ -376,7 +495,14 @@ function AccountLogin_OnShow(self)
self.modelUpdateCount = 0 self.modelUpdateCount = 0
self.modelUpdateTimer = 0 self.modelUpdateTimer = 0
local accountName, password = unpack(string_explode(GetSavedAccountName(), "#&|&#")) local accountName, password = "", ""
if LoginState.launchMode == "developer" then
accountName, password = unpack(string_explode(GetSavedAccountName(), "#&|&#"))
else
SetSavedAccountName("")
SetSavedAccountList("")
SetUsesToken(false)
end
UICache.accountEdit:SetText(accountName or "") UICache.accountEdit:SetText(accountName or "")
UICache.passwordEdit:SetText(password or "") UICache.passwordEdit:SetText(password or "")
@@ -390,13 +516,15 @@ function AccountLogin_OnShow(self)
UICache.realmName:SetText("No Recent Server") UICache.realmName:SetText("No Recent Server")
end end
if accountName == "" then if LoginState.launchMode == "developer" then
AccountLogin_FocusAccountName() if accountName == "" then
else AccountLogin_FocusAccountName()
AccountLogin_FocusPassword() else
AccountLogin_FocusPassword()
end
end end
if UICache.savePassword:GetChecked() then if LoginState.launchMode == "developer" and UICache.savePassword:GetChecked() then
UICache.autoLoginText:Show() UICache.autoLoginText:Show()
UICache.autoLogin:Show() UICache.autoLogin:Show()
else else
@@ -415,7 +543,11 @@ function AccountLogin_OnShow(self)
ACCOUNT_MSG_BODY_LOADED = false ACCOUNT_MSG_BODY_LOADED = false
ACCOUNT_MSG_CURRENT_INDEX = nil ACCOUNT_MSG_CURRENT_INDEX = nil
ApplyLaunchMode()
AccountLogin_CheckAutoLogin() AccountLogin_CheckAutoLogin()
if not TryLauncherLogin() then
TryLauncherFallbackLogin()
end
self:SetScript("OnUpdate", LoginScene_OnUpdate) self:SetScript("OnUpdate", LoginScene_OnUpdate)
end end
@@ -450,11 +582,15 @@ function AccountLogin_OnHide(self)
end end
function AccountLogin_FocusPassword() function AccountLogin_FocusPassword()
UICache.passwordEdit:SetFocus() if LoginState.launchMode == "developer" then
UICache.passwordEdit:SetFocus()
end
end end
function AccountLogin_FocusAccountName() function AccountLogin_FocusAccountName()
UICache.accountEdit:SetFocus() if LoginState.launchMode == "developer" then
UICache.accountEdit:SetFocus()
end
end end
function AccountLogin_OnKeyDown(key) function AccountLogin_OnKeyDown(key)
@@ -474,7 +610,7 @@ function AccountLogin_OnKeyDown(key)
elseif SurveyNotificationFrame:IsShown() then elseif SurveyNotificationFrame:IsShown() then
AccountLogin_SurveyNotificationDone(1) AccountLogin_SurveyNotificationDone(1)
end end
AccountLogin_Login() AccountLogin_PrimaryAction()
elseif key == "PRINTSCREEN" then elseif key == "PRINTSCREEN" then
Screenshot() Screenshot()
end end
@@ -526,6 +662,11 @@ end
-- SISTEMA DE LOGIN CON GUARDADO DE CONTRASEÑA -- SISTEMA DE LOGIN CON GUARDADO DE CONTRASEÑA
-- ============================================================================ -- ============================================================================
function AccountLogin_Login() function AccountLogin_Login()
if LoginState.launchMode ~= "developer" then
AccountLogin_OpenLauncher()
return
end
PlaySound("gsLogin") PlaySound("gsLogin")
local accountName = UICache.accountEdit:GetText() local accountName = UICache.accountEdit:GetText()
local password = UICache.passwordEdit:GetText() local password = UICache.passwordEdit:GetText()
@@ -557,6 +698,11 @@ end
-- SISTEMA DE AUTOLOGIN -- SISTEMA DE AUTOLOGIN
-- ============================================================================ -- ============================================================================
function AccountLogin_CheckAutoLogin() function AccountLogin_CheckAutoLogin()
if LoginState.launchMode ~= "developer" then
LoginState.autoLoginTimer = nil
return
end
if not LoginState.autoLoginAttempted then if not LoginState.autoLoginAttempted then
LoginState.autoLoginAttempted = true LoginState.autoLoginAttempted = true
local savedAccountInfo = GetSavedAccountName() local savedAccountInfo = GetSavedAccountName()
@@ -115,6 +115,18 @@
</Anchor> </Anchor>
</Anchors> </Anchors>
</FontString> </FontString>
<FontString name="AccountLoginLauncherHint" inherits="GlueFontNormal" justifyH="CENTER" text="Вход в игру выполняется через лаунчер MoonWell.">
<Size>
<AbsDimension x="420" y="24"/>
</Size>
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="0" y="270"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="WorldOfWarcraftRating" hidden="true"> <Texture name="WorldOfWarcraftRating" hidden="true">
<Size> <Size>
<AbsDimension x="128" y="128"/> <AbsDimension x="128" y="128"/>
@@ -195,7 +207,7 @@
<!-- ====================================== --> <!-- ====================================== -->
<!-- CAMPOS DE TEXTO: USUARIO Y CONTRASEÑA --> <!-- CAMPOS DE TEXTO: USUARIO Y CONTRASEÑA -->
<!-- ====================================== --> <!-- ====================================== -->
<EditBox name="AccountLoginAccountEdit" inherits="GlueDark_EditBoxTemplate" letters="320"> <EditBox name="AccountLoginAccountEdit" inherits="GlueDark_EditBoxTemplate" letters="320" hidden="true">
<Size x="200" y="37"/> <Size x="200" y="37"/>
<Anchors> <Anchors>
<Anchor point="BOTTOM"> <Anchor point="BOTTOM">
@@ -246,7 +258,7 @@
</Scripts> </Scripts>
</EditBox> </EditBox>
<EditBox name="AccountLoginPasswordEdit" inherits="GlueDark_EditBoxTemplate" letters="16" password="1"> <EditBox name="AccountLoginPasswordEdit" inherits="GlueDark_EditBoxTemplate" letters="16" password="1" hidden="true">
<Size x="200" y="37"/> <Size x="200" y="37"/>
<Anchors> <Anchors>
<Anchor point="BOTTOM"> <Anchor point="BOTTOM">
@@ -299,7 +311,7 @@
<!-- ====================================== --> <!-- ====================================== -->
<!-- CHECKBOXES: GUARDAR CUENTA Y CONTRASEÑA --> <!-- CHECKBOXES: GUARDAR CUENTA Y CONTRASEÑA -->
<!-- ====================================== --> <!-- ====================================== -->
<Frame name="AccountLoginSaveOptionsFrame"> <Frame name="AccountLoginSaveOptionsFrame" hidden="true">
<Size x="200" y="60"/> <Size x="200" y="60"/>
<Anchors> <Anchors>
<Anchor point="TOP" relativeTo="AccountLoginPasswordEdit" relativePoint="BOTTOM"> <Anchor point="TOP" relativeTo="AccountLoginPasswordEdit" relativePoint="BOTTOM">
@@ -883,7 +895,7 @@
<!-- ====================================== --> <!-- ====================================== -->
<!-- BOTÓN DE LOGIN --> <!-- BOTÓN DE LOGIN -->
<!-- ====================================== --> <!-- ====================================== -->
<Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="LOGIN"> <Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="Авторизоваться">
<Size x="200" y="55"/> <Size x="200" y="55"/>
<Anchors> <Anchors>
<Anchor point="BOTTOM"> <Anchor point="BOTTOM">
@@ -905,7 +917,7 @@
</Layers> </Layers>
<Scripts> <Scripts>
<OnClick> <OnClick>
AccountLogin_Login(); AccountLogin_PrimaryAction();
</OnClick> </OnClick>
</Scripts> </Scripts>
</Button> </Button>