Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e53b3c1784 | |||
| 23b3a3b864 | |||
| 597f4030a7 | |||
| ebd76275a1 | |||
| 6addd9bb51 | |||
| abde71ceb0 |
@@ -0,0 +1,3 @@
|
|||||||
|
/config.local
|
||||||
|
/tmp
|
||||||
|
/cache
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[cache]
|
||||||
|
type = "hardlink,copy"
|
||||||
|
[core]
|
||||||
|
remote = customization
|
||||||
|
['remote "customization"']
|
||||||
|
url = s3://warcraft-client/dvc/moonwell-client-sources/customization
|
||||||
|
endpointurl = https://storage.yandexcloud.net
|
||||||
|
region = ru-central1
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Add patterns of files dvc should ignore, which could improve
|
||||||
|
# the performance. Learn more at
|
||||||
|
# https://dvc.org/doc/user-guide/dvcignore
|
||||||
@@ -2,6 +2,7 @@ WOW_HOME=C:\Games\World of Warcraft 3.3.5
|
|||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_SESSION_TOKEN=
|
||||||
AWS_DEFAULT_REGION=
|
AWS_DEFAULT_REGION=
|
||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_ENDPOINT=
|
AWS_ENDPOINT=
|
||||||
@@ -10,3 +11,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=
|
|||||||
PRODUCTION_REALMLIST=
|
PRODUCTION_REALMLIST=
|
||||||
PTR_REALMLIST=
|
PTR_REALMLIST=
|
||||||
LOCAL_REALMLIST=
|
LOCAL_REALMLIST=
|
||||||
|
|
||||||
|
SSH_USER=root
|
||||||
|
SSH_HOST=strictlypragmatic.ru
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 запуске.
|
||||||
@@ -47,6 +47,8 @@ git submodule update --remote vendor/warcraftxl vendor/modules/*
|
|||||||
- Visual Studio 2022 с C++ toolchain для Win32;
|
- Visual Studio 2022 с C++ toolchain для Win32;
|
||||||
- CMake 3.20+;
|
- CMake 3.20+;
|
||||||
- Rust/Cargo для существующего MPQ-сборщика;
|
- Rust/Cargo для существующего MPQ-сборщика;
|
||||||
|
- Python 3 и DVC с поддержкой S3 для содержимого внешних графических патчей
|
||||||
|
(`py -m pip install -r requirements-dvc.txt`);
|
||||||
- локальный оригинальный `Wow_Original.exe` с SHA-256
|
- локальный оригинальный `Wow_Original.exe` с SHA-256
|
||||||
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
|
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
|
||||||
|
|
||||||
@@ -111,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 для сборки
|
||||||
@@ -128,6 +136,11 @@ WarcraftXL не требуется и в объектное хранилище
|
|||||||
не изменяют `graphicsPatches`. При пересечении списков репозиторный патч имеет
|
не изменяют `graphicsPatches`. При пересечении списков репозиторный патч имеет
|
||||||
приоритет.
|
приоритет.
|
||||||
|
|
||||||
|
Распакованное содержимое больших патчей кастомизации хранится через DVC в
|
||||||
|
`assets/graphics/`, а бинарные объекты — в настроенном S3. Инструкции по первичной
|
||||||
|
загрузке, изменению и импорту MPQ находятся в
|
||||||
|
[`assets/graphics/README.md`](assets/graphics/README.md).
|
||||||
|
|
||||||
## Лицензирование
|
## Лицензирование
|
||||||
|
|
||||||
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
|
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/patch-X
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 012ce72d8dfdf089b20a28f6e6158b58.dir
|
||||||
|
size: 1195310
|
||||||
|
nfiles: 4
|
||||||
|
hash: md5
|
||||||
|
path: patch-X
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/patch-ruRU-X
|
||||||
|
/patch-ruRU-Y
|
||||||
|
/backup-ruRU
|
||||||
|
/patch-ruRU-6
|
||||||
|
/patch-ruRU-A
|
||||||
|
/patch-ruRU-B
|
||||||
|
/patch-ruRU-E
|
||||||
|
/patch-ruRU-H
|
||||||
|
/patch-ruRU-I
|
||||||
|
/patch-ruRU-M
|
||||||
|
/patch-ruRU-S
|
||||||
|
/patch-ruRU-T
|
||||||
|
/patch-ruRU-U
|
||||||
|
/patch-ruRU-W
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: b120587dfcd255f78392ca52705a00ff.dir
|
||||||
|
size: 38876467
|
||||||
|
nfiles: 161
|
||||||
|
hash: md5
|
||||||
|
path: backup-ruRU
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 9ca2ee746d80e8a0c951c5ef476ae654.dir
|
||||||
|
size: 1064210
|
||||||
|
nfiles: 3
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-6
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 97232ae859eaa1a16b137efd1a2e9122.dir
|
||||||
|
size: 2748988973
|
||||||
|
nfiles: 8817
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-A
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: bb686aebf7c9cae2c02433abde9a9f19.dir
|
||||||
|
size: 771051083
|
||||||
|
nfiles: 2911
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-B
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: d6b2fe0e3f23c47dc944ca4a8f82d97a.dir
|
||||||
|
size: 9275755440
|
||||||
|
nfiles: 21977
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-E
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 5e95536d2c6f6ff792f05b800105bedc.dir
|
||||||
|
size: 3727395837
|
||||||
|
nfiles: 40296
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-H
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 5db07768b0814c970d84a99b6072fd9f.dir
|
||||||
|
size: 31453961
|
||||||
|
nfiles: 186
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-I
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: e927ae98c9971f378a82feb3299fdcb8.dir
|
||||||
|
size: 69521651
|
||||||
|
nfiles: 1552
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-M
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 449383bbe05ebebb6bad2c37da7793c4.dir
|
||||||
|
size: 142167464
|
||||||
|
nfiles: 1751
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-S
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: fc19500b5ce0fd312a415a3118afd58d.dir
|
||||||
|
size: 656858260
|
||||||
|
nfiles: 1810
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-T
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 783909a6c2ebed5d4f43516fe1ea68bb.dir
|
||||||
|
size: 460642379
|
||||||
|
nfiles: 1837
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-U
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: e2a1448eb0e443797acd577fc4b67249.dir
|
||||||
|
size: 44597910
|
||||||
|
nfiles: 484
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-W
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 3d322d290959d1ead612f3b76860a1ea.dir
|
||||||
|
size: 3984134962
|
||||||
|
nfiles: 29495
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-X
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
outs:
|
||||||
|
- md5: 42f3a8ac35674e33c0a4420c84cadcb8.dir
|
||||||
|
size: 11053212781
|
||||||
|
nfiles: 36191
|
||||||
|
hash: md5
|
||||||
|
path: patch-ruRU-Y
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Графические MPQ-патчи
|
||||||
|
|
||||||
|
В этой директории через DVC версионируется распакованное содержимое всех 15 патчей из
|
||||||
|
`graphicsPatches` в `patch-layout.json`. Git хранит небольшие файлы `*.dvc`, а сами ресурсы —
|
||||||
|
в S3 remote `customization`. Хэшированные пути в S3 являются внутренней адресацией DVC:
|
||||||
|
исходное дерево с обычными именами восстанавливается командой `dvc pull`.
|
||||||
|
|
||||||
|
## Первичная настройка
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -m pip install -r requirements-dvc.txt
|
||||||
|
Copy-Item .env.example .env # если локального .env ещё нет
|
||||||
|
.\configure-dvc.ps1
|
||||||
|
py -m dvc pull
|
||||||
|
```
|
||||||
|
|
||||||
|
Можно скачать один патч, передав его указатель явно:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -m dvc pull assets/graphics/Data/ruRU/patch-ruRU-X.dvc
|
||||||
|
```
|
||||||
|
|
||||||
|
Секреты записываются только в `.dvc/config.local`; этот файл исключён из Git. Вместо ключей
|
||||||
|
из `.env` можно использовать AWS profile: `.\configure-dvc.ps1 -Profile moonwell`.
|
||||||
|
|
||||||
|
## Изменение ресурсов
|
||||||
|
|
||||||
|
DVC-кэш использует hardlink на NTFS. Перед ручным изменением дерева отвяжите его от кэша:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -m dvc unprotect assets/graphics/Data/ruRU/patch-ruRU-X
|
||||||
|
# изменить, добавить или удалить файлы
|
||||||
|
py -m dvc add assets/graphics/Data/ruRU/patch-ruRU-X
|
||||||
|
py -m dvc push
|
||||||
|
git add assets/graphics/Data/ruRU/patch-ruRU-X.dvc
|
||||||
|
```
|
||||||
|
|
||||||
|
Сначала выполняйте `dvc push`, затем коммитьте Git-указатель, чтобы коммит не ссылался на ещё
|
||||||
|
не загруженные объекты.
|
||||||
|
|
||||||
|
## Импорт MPQ
|
||||||
|
|
||||||
|
`mpqextract` принимает встроенный `(listfile)` и любое количество внешних listfile. Он извлекает
|
||||||
|
все известные пути, а блоки с неизвестными именами сохраняет в `__mpqmeta__/unresolved/`.
|
||||||
|
Манифест `__mpqmeta__/unresolved.json` связывает каждый blob с MPQ-хэшами и индексом блока,
|
||||||
|
поэтому содержимое архива не теряется даже при неполном listfile.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo run --release --manifest-path tool/Cargo.toml --bin mpqextract -- `
|
||||||
|
'patch.mpq' 'assets/graphics/Data/ruRU/patch-ruRU-N' 'master-list.txt'
|
||||||
|
```
|
||||||
|
|
||||||
|
В `patch-ruRU-A`, `B`, `S`, `T`, `U` и `W` есть такие безымянные блоки. Для их безопасной
|
||||||
|
пересборки нужен авторитетный listfile с исходными путями; пока он не найден, исходные MPQ в
|
||||||
|
дистрибуционном S3 остаются каноническими архивами. `mpqpack` намеренно отказывается собирать
|
||||||
|
дерево с `__mpqmeta__`, поскольку подмена путей хэшами изменила бы семантику патча.
|
||||||
|
|
||||||
|
Полностью разрешённое дерево собирается так:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo run --release --manifest-path tool/Cargo.toml --bin mpqpack -- `
|
||||||
|
'assets/graphics/Data/ruRU/patch-ruRU-X' 'build/graphics/patch-ruRU-X.MPQ'
|
||||||
|
```
|
||||||
|
|
||||||
|
Сводка исходных объектов, DVC-хэшей и количества разрешённых блоков находится в
|
||||||
|
`assets/graphics/sources.json`.
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
{
|
||||||
|
"sourcePrefix": "s3://warcraft-client/World of Warcraft/",
|
||||||
|
"patches": [
|
||||||
|
{
|
||||||
|
"archive": "Data/patch-X.MPQ",
|
||||||
|
"archiveBytes": 496584,
|
||||||
|
"dvcPointer": "assets/graphics/Data/patch-X.dvc",
|
||||||
|
"dvcMd5": "012ce72d8dfdf089b20a28f6e6158b58.dir",
|
||||||
|
"extractedBytes": 1195310,
|
||||||
|
"trackedFiles": 4,
|
||||||
|
"resolvedFiles": 4,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/backup-ruRU.MPQ",
|
||||||
|
"archiveBytes": 23981093,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/backup-ruRU.dvc",
|
||||||
|
"dvcMd5": "b120587dfcd255f78392ca52705a00ff.dir",
|
||||||
|
"extractedBytes": 38876467,
|
||||||
|
"trackedFiles": 161,
|
||||||
|
"resolvedFiles": 161,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-6.MPQ",
|
||||||
|
"archiveBytes": 375062,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-6.dvc",
|
||||||
|
"dvcMd5": "9ca2ee746d80e8a0c951c5ef476ae654.dir",
|
||||||
|
"extractedBytes": 1064210,
|
||||||
|
"trackedFiles": 3,
|
||||||
|
"resolvedFiles": 3,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-A.mpq",
|
||||||
|
"archiveBytes": 1451338841,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-A.dvc",
|
||||||
|
"dvcMd5": "97232ae859eaa1a16b137efd1a2e9122.dir",
|
||||||
|
"extractedBytes": 2748988973,
|
||||||
|
"trackedFiles": 8817,
|
||||||
|
"resolvedFiles": 5649,
|
||||||
|
"unresolvedBlocks": 3167
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-B.mpq",
|
||||||
|
"archiveBytes": 384938550,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-B.dvc",
|
||||||
|
"dvcMd5": "bb686aebf7c9cae2c02433abde9a9f19.dir",
|
||||||
|
"extractedBytes": 771051083,
|
||||||
|
"trackedFiles": 2911,
|
||||||
|
"resolvedFiles": 1679,
|
||||||
|
"unresolvedBlocks": 1231
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-E.mpq",
|
||||||
|
"archiveBytes": 1862937450,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-E.dvc",
|
||||||
|
"dvcMd5": "d6b2fe0e3f23c47dc944ca4a8f82d97a.dir",
|
||||||
|
"extractedBytes": 9275755440,
|
||||||
|
"trackedFiles": 21977,
|
||||||
|
"resolvedFiles": 21977,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-H.MPQ",
|
||||||
|
"archiveBytes": 2154531136,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-H.dvc",
|
||||||
|
"dvcMd5": "5e95536d2c6f6ff792f05b800105bedc.dir",
|
||||||
|
"extractedBytes": 3727395837,
|
||||||
|
"trackedFiles": 40296,
|
||||||
|
"resolvedFiles": 40296,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-I.mpq",
|
||||||
|
"archiveBytes": 14891516,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-I.dvc",
|
||||||
|
"dvcMd5": "5db07768b0814c970d84a99b6072fd9f.dir",
|
||||||
|
"extractedBytes": 31453961,
|
||||||
|
"trackedFiles": 186,
|
||||||
|
"resolvedFiles": 186,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-M.MPQ",
|
||||||
|
"archiveBytes": 48772900,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-M.dvc",
|
||||||
|
"dvcMd5": "e927ae98c9971f378a82feb3299fdcb8.dir",
|
||||||
|
"extractedBytes": 69521651,
|
||||||
|
"trackedFiles": 1552,
|
||||||
|
"resolvedFiles": 1552,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-S.mpq",
|
||||||
|
"archiveBytes": 80492633,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-S.dvc",
|
||||||
|
"dvcMd5": "449383bbe05ebebb6bad2c37da7793c4.dir",
|
||||||
|
"extractedBytes": 142167464,
|
||||||
|
"trackedFiles": 1751,
|
||||||
|
"resolvedFiles": 1432,
|
||||||
|
"unresolvedBlocks": 318
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-T.mpq",
|
||||||
|
"archiveBytes": 418595462,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-T.dvc",
|
||||||
|
"dvcMd5": "fc19500b5ce0fd312a415a3118afd58d.dir",
|
||||||
|
"extractedBytes": 656858260,
|
||||||
|
"trackedFiles": 1810,
|
||||||
|
"resolvedFiles": 1731,
|
||||||
|
"unresolvedBlocks": 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-U.mpq",
|
||||||
|
"archiveBytes": 233678341,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-U.dvc",
|
||||||
|
"dvcMd5": "783909a6c2ebed5d4f43516fe1ea68bb.dir",
|
||||||
|
"extractedBytes": 460642379,
|
||||||
|
"trackedFiles": 1837,
|
||||||
|
"resolvedFiles": 1372,
|
||||||
|
"unresolvedBlocks": 464
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-W.mpq",
|
||||||
|
"archiveBytes": 29433979,
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-W.dvc",
|
||||||
|
"dvcMd5": "e2a1448eb0e443797acd577fc4b67249.dir",
|
||||||
|
"extractedBytes": 44597910,
|
||||||
|
"trackedFiles": 484,
|
||||||
|
"resolvedFiles": 381,
|
||||||
|
"unresolvedBlocks": 102
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-X.MPQ",
|
||||||
|
"archiveBytes": 2673854694,
|
||||||
|
"archiveSha256": "CD8CED6FA6C863D509DAE458E6DD44C3986B8B57731B6DC6192FE94125F8C1FD",
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-X.dvc",
|
||||||
|
"dvcMd5": "3d322d290959d1ead612f3b76860a1ea.dir",
|
||||||
|
"extractedBytes": 3984134962,
|
||||||
|
"trackedFiles": 29495,
|
||||||
|
"resolvedFiles": 29495,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"archive": "Data/ruRU/patch-ruRU-Y.MPQ",
|
||||||
|
"archiveBytes": 3001665276,
|
||||||
|
"archiveSha256": "534D496C1A1EE023B910ABC80B727BA88BB09CD2F5767100FC18EE41F4306B90",
|
||||||
|
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-Y.dvc",
|
||||||
|
"dvcMd5": "42f3a8ac35674e33c0a4420c84cadcb8.dir",
|
||||||
|
"extractedBytes": 11053212781,
|
||||||
|
"trackedFiles": 36191,
|
||||||
|
"resolvedFiles": 36191,
|
||||||
|
"unresolvedBlocks": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
param(
|
||||||
|
[string]$EnvFile = (Join-Path $PSScriptRoot '.env'),
|
||||||
|
[string]$Profile
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-EnvValue([string]$Path, [string]$Name) {
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$line = Get-Content -LiteralPath $Path |
|
||||||
|
Where-Object { $_ -match "^$([regex]::Escape($Name))=" } |
|
||||||
|
Select-Object -First 1
|
||||||
|
if ($line) {
|
||||||
|
return $line.Substring($Name.Length + 1).Trim()
|
||||||
|
}
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-Dvc([string[]]$Arguments) {
|
||||||
|
& py -m dvc @Arguments
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "DVC command failed: dvc $($Arguments -join ' ')"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot '.dvc\config'))) {
|
||||||
|
throw 'DVC is not initialized in this repository.'
|
||||||
|
}
|
||||||
|
|
||||||
|
& py -c 'import dvc, dvc_s3' *> $null
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw 'DVC with S3 support is not installed. Run: py -m pip install -r requirements-dvc.txt'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Profile) {
|
||||||
|
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'profile', $Profile)
|
||||||
|
} else {
|
||||||
|
$accessKey = Get-EnvValue $EnvFile 'AWS_ACCESS_KEY_ID'
|
||||||
|
$secretKey = Get-EnvValue $EnvFile 'AWS_SECRET_ACCESS_KEY'
|
||||||
|
$sessionToken = Get-EnvValue $EnvFile 'AWS_SESSION_TOKEN'
|
||||||
|
|
||||||
|
if (-not $accessKey -or -not $secretKey) {
|
||||||
|
throw "AWS credentials are missing in $EnvFile. Pass -Profile or fill AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY."
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'access_key_id', $accessKey)
|
||||||
|
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'secret_access_key', $secretKey)
|
||||||
|
if ($sessionToken) {
|
||||||
|
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'session_token', $sessionToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host 'Local DVC credentials configured. They are stored in ignored .dvc/config.local.'
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
dvc[s3]==3.67.1
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
## Dependencies: CallbackHandler-1.0, AIO_Client, LibStub, Ace3
|
## Dependencies: CallbackHandler-1.0, AIO_Client, LibStub, Ace3
|
||||||
## X-Hidden: 1
|
## X-Hidden: 1
|
||||||
|
|
||||||
# Libraries
|
|
||||||
embeds.xml
|
|
||||||
|
|
||||||
# Locale
|
# Locale
|
||||||
Locale\enUS.lua
|
Locale\enUS.lua
|
||||||
Locale\ruRU.lua
|
Locale\ruRU.lua
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,64 @@
|
|||||||
|
# Sync DBC files from src with files on a server
|
||||||
|
|
||||||
|
# Parse .env
|
||||||
|
if (Test-Path ".env") {
|
||||||
|
Get-Content ".env" | ForEach-Object {
|
||||||
|
if ($_ -match '^[^#\s]+=.+') {
|
||||||
|
$name, $value = $_ -split '=', 2
|
||||||
|
$name = $name.Trim()
|
||||||
|
$value = $value.Trim().Trim('"').Trim("'")
|
||||||
|
[System.Environment]::SetEnvironmentVariable($name, $value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Error ".env not found!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $env:SSH_USER
|
||||||
|
$sshHost = $env:SSH_HOST
|
||||||
|
$port = $env:SSH_PORT
|
||||||
|
|
||||||
|
if (-not $user -or -not $sshHost) {
|
||||||
|
Write-Error "SSH_USER or SSH_HOST env variables are not defined"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $port) {
|
||||||
|
$port = "22"
|
||||||
|
}
|
||||||
|
|
||||||
|
$input = @(
|
||||||
|
".\src\Data\ruRU\patch-ruRU-5\DBFilesClient\Achievement.dbc",
|
||||||
|
".\src\Data\ruRU\patch-ruRU-Z\DBFilesClient\Spell.dbc",
|
||||||
|
".\src\Data\ruRU\patch-ruRU-Z\DBFilesClient\BarberShopStyle.dbc",
|
||||||
|
".\src\Data\ruRU\patch-ruRU-Z\DBFilesClient\CreatureDisplayInfoExtra.dbc",
|
||||||
|
".\src\Data\ruRU\patch-ruRU-Z\DBFilesClient\SpellVisualKitModelAttach.dbc"
|
||||||
|
)
|
||||||
|
|
||||||
|
$output = "/home/mangos/moonwell-server/env/dist/data/dbc"
|
||||||
|
|
||||||
|
foreach ($file in $input) {
|
||||||
|
if (Test-Path $file) {
|
||||||
|
$fileName = Split-Path $file -Leaf
|
||||||
|
$outputPath = "$output/$fileName"
|
||||||
|
|
||||||
|
Write-Host "Copying $fileName to $output..."
|
||||||
|
|
||||||
|
scp -P $port $file "${user}@${sshHost}:${output}"
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "Running chmod..."
|
||||||
|
|
||||||
|
ssh -p $port "${user}@${sshHost}" "chown mangos:mangos '$outputPath' && chmod 775 '$outputPath'"
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Write-Warning "Input file not found: $file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Restarting server..."
|
||||||
|
ssh -p $port "${user}@${sshHost}" "cd /home/mangos/moonwell-server && docker compose restart ac-worldserver"
|
||||||
|
|
||||||
|
Write-Host "Done."
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use wow_mpq::{Archive, special_files::parse_listfile};
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct UnresolvedFile {
|
||||||
|
name_hash_a: String,
|
||||||
|
name_hash_b: String,
|
||||||
|
hash_index: usize,
|
||||||
|
block_index: usize,
|
||||||
|
size: u64,
|
||||||
|
compressed_size: u64,
|
||||||
|
flags: String,
|
||||||
|
blob: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct UnresolvedManifest {
|
||||||
|
version: u32,
|
||||||
|
archive_blocks: usize,
|
||||||
|
resolved_blocks: usize,
|
||||||
|
special_blocks: usize,
|
||||||
|
unresolved: Vec<UnresolvedFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guess_extension(data: &[u8]) -> &'static str {
|
||||||
|
match data {
|
||||||
|
[b'B', b'L', b'P', b'1' | b'2', ..] => "blp",
|
||||||
|
[b'M', b'D', b'2', b'0' | b'1', ..] => "m2",
|
||||||
|
[b'S', b'K', b'I', b'N', ..] => "skin",
|
||||||
|
[b'W', b'D', b'B', b'C', ..] => "dbc",
|
||||||
|
[b'O', b'g', b'g', b'S', ..] => "ogg",
|
||||||
|
[b'R', b'I', b'F', b'F', ..] => "wav",
|
||||||
|
[0x89, b'P', b'N', b'G', ..] => "png",
|
||||||
|
[b'D', b'D', b'S', b' ', ..] => "dds",
|
||||||
|
[0xff, 0xd8, 0xff, ..] => "jpg",
|
||||||
|
_ => "bin",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn safe_output_path(root: &Path, archive_path: &str) -> Result<PathBuf, Box<dyn Error>> {
|
||||||
|
let normalized = archive_path.replace('\\', "/");
|
||||||
|
let mut relative = PathBuf::new();
|
||||||
|
|
||||||
|
for part in normalized.split('/') {
|
||||||
|
if part.is_empty() || part == "." {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if part == ".." || part.contains(':') {
|
||||||
|
return Err(format!("unsafe archive path: {archive_path}").into());
|
||||||
|
}
|
||||||
|
relative.push(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
if relative.as_os_str().is_empty()
|
||||||
|
|| relative
|
||||||
|
.components()
|
||||||
|
.any(|component| !matches!(component, Component::Normal(_)))
|
||||||
|
{
|
||||||
|
return Err(format!("unsafe archive path: {archive_path}").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(root.join(relative))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_archive(
|
||||||
|
archive_path: &Path,
|
||||||
|
output_dir: &Path,
|
||||||
|
external_listfiles: &[String],
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
if output_dir.exists() && fs::read_dir(output_dir)?.next().is_some() {
|
||||||
|
return Err(format!(
|
||||||
|
"output directory is not empty: {} (extract to a new directory)",
|
||||||
|
output_dir.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut archive = Archive::open(archive_path)?;
|
||||||
|
let all_entries = archive.list_all()?;
|
||||||
|
let all_blocks: HashSet<usize> = all_entries
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| entry.table_indices.and_then(|(_, block)| block))
|
||||||
|
.collect();
|
||||||
|
let hashes_by_block: HashMap<usize, (u32, u32)> = archive
|
||||||
|
.list_all_with_hashes()?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|entry| Some((entry.table_indices?.1?, entry.hashes?)))
|
||||||
|
.collect();
|
||||||
|
let mut special_blocks = HashSet::new();
|
||||||
|
for special in [
|
||||||
|
"(listfile)",
|
||||||
|
"(attributes)",
|
||||||
|
"(signature)",
|
||||||
|
"(patch_metadata)",
|
||||||
|
] {
|
||||||
|
if let Some(info) = archive.find_file(special)? {
|
||||||
|
special_blocks.insert(info.block_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut files = Vec::new();
|
||||||
|
if let Ok(listfile) = archive.read_file("(listfile)") {
|
||||||
|
files.extend(parse_listfile(&listfile)?);
|
||||||
|
}
|
||||||
|
for listfile_path in external_listfiles {
|
||||||
|
files.extend(parse_listfile(&fs::read(listfile_path)?)?);
|
||||||
|
}
|
||||||
|
if files.is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"{} has no readable (listfile); provide an external listfile",
|
||||||
|
archive_path.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::create_dir_all(output_dir)?;
|
||||||
|
let mut seen = HashSet::new();
|
||||||
|
let mut matched_blocks = HashSet::new();
|
||||||
|
let mut extracted = 0usize;
|
||||||
|
|
||||||
|
for archive_name in files {
|
||||||
|
if archive_name.starts_with('(') && archive_name.ends_with(')') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MPQ file lookup is case-insensitive. Avoid writing the same logical path
|
||||||
|
// twice on Windows when a malformed listfile contains casing duplicates.
|
||||||
|
if !seen.insert(archive_name.to_ascii_lowercase()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(info) = archive.find_file(&archive_name)? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !matched_blocks.insert(info.block_index) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let output_path = safe_output_path(output_dir, &archive_name)?;
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = archive
|
||||||
|
.read_file(&archive_name)
|
||||||
|
.map_err(|error| format!("cannot extract {archive_name}: {error}"))?;
|
||||||
|
fs::write(&output_path, data)?;
|
||||||
|
extracted += 1;
|
||||||
|
|
||||||
|
if extracted.is_multiple_of(1000) {
|
||||||
|
println!("Extracted {extracted} files...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Extracted {extracted} files from {} to {} ({} data blocks, {} special blocks)",
|
||||||
|
archive_path.display(),
|
||||||
|
output_dir.display(),
|
||||||
|
all_blocks.len(),
|
||||||
|
special_blocks.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let unresolved_blocks: Vec<usize> = all_blocks
|
||||||
|
.difference(&special_blocks)
|
||||||
|
.filter(|block| !matched_blocks.contains(block))
|
||||||
|
.copied()
|
||||||
|
.collect();
|
||||||
|
if !unresolved_blocks.is_empty() {
|
||||||
|
let metadata_root = output_dir.join("__mpqmeta__");
|
||||||
|
let blob_root = metadata_root.join("unresolved");
|
||||||
|
fs::create_dir_all(&blob_root)?;
|
||||||
|
let mut unresolved = Vec::with_capacity(unresolved_blocks.len());
|
||||||
|
|
||||||
|
for block_index in unresolved_blocks {
|
||||||
|
let entry = all_entries
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.table_indices.and_then(|(_, block)| block) == Some(block_index))
|
||||||
|
.ok_or_else(|| format!("missing metadata for block {block_index}"))?;
|
||||||
|
let (hash_index, _) = entry
|
||||||
|
.table_indices
|
||||||
|
.ok_or_else(|| format!("missing table indices for block {block_index}"))?;
|
||||||
|
if entry.is_encrypted() || entry.is_patch_file() {
|
||||||
|
return Err(format!(
|
||||||
|
"unresolved block {block_index} is encrypted or is a binary patch"
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
let (name_hash_a, name_hash_b) = hashes_by_block
|
||||||
|
.get(&block_index)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| format!("missing name hashes for block {block_index}"))?;
|
||||||
|
let data = archive.read_file_by_indices(hash_index, Some(block_index))?;
|
||||||
|
let extension = guess_extension(&data);
|
||||||
|
let blob_name =
|
||||||
|
format!("{name_hash_a:08x}-{name_hash_b:08x}-{block_index:08}.{extension}");
|
||||||
|
fs::write(blob_root.join(&blob_name), data)?;
|
||||||
|
unresolved.push(UnresolvedFile {
|
||||||
|
name_hash_a: format!("{name_hash_a:08x}"),
|
||||||
|
name_hash_b: format!("{name_hash_b:08x}"),
|
||||||
|
hash_index,
|
||||||
|
block_index,
|
||||||
|
size: entry.size,
|
||||||
|
compressed_size: entry.compressed_size,
|
||||||
|
flags: format!("0x{:08x}", entry.flags),
|
||||||
|
blob: format!("unresolved/{blob_name}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
unresolved.sort_by_key(|entry| entry.block_index);
|
||||||
|
let manifest = UnresolvedManifest {
|
||||||
|
version: 1,
|
||||||
|
archive_blocks: all_blocks.len(),
|
||||||
|
resolved_blocks: matched_blocks.len(),
|
||||||
|
special_blocks: special_blocks.len(),
|
||||||
|
unresolved,
|
||||||
|
};
|
||||||
|
fs::write(
|
||||||
|
metadata_root.join("unresolved.json"),
|
||||||
|
serde_json::to_vec_pretty(&manifest)?,
|
||||||
|
)?;
|
||||||
|
println!(
|
||||||
|
"Stored {} path-unresolved data blocks in {}",
|
||||||
|
manifest.unresolved.len(),
|
||||||
|
metadata_root.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 3 {
|
||||||
|
eprintln!("Usage: mpqextract <archive.mpq> <output_dir> [external-listfile ...]");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_archive(Path::new(&args[1]), Path::new(&args[2]), &args[3..])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_safe_relative_paths() {
|
||||||
|
let root = Path::new("output");
|
||||||
|
assert_eq!(
|
||||||
|
safe_output_path(root, "Character\\Human\\Human.m2").unwrap(),
|
||||||
|
root.join("Character").join("Human").join("Human.m2")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_parent_and_drive_paths() {
|
||||||
|
assert!(safe_output_path(Path::new("output"), "..\\secret").is_err());
|
||||||
|
assert!(safe_output_path(Path::new("output"), "C:\\secret").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use wow_mpq::Archive;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 2 {
|
||||||
|
eprintln!("Usage: mpqinfo <archive.mpq>");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let archive_path = Path::new(&args[1]);
|
||||||
|
let mut archive = Archive::open(archive_path)?;
|
||||||
|
let entries = archive.list()?;
|
||||||
|
let file_count = entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| !entry.name.starts_with('('))
|
||||||
|
.count();
|
||||||
|
let unpacked_bytes: u64 = entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| !entry.name.starts_with('('))
|
||||||
|
.map(|entry| entry.size)
|
||||||
|
.sum();
|
||||||
|
let encrypted_files = entries.iter().filter(|entry| entry.is_encrypted()).count();
|
||||||
|
let patch_files = entries.iter().filter(|entry| entry.is_patch_file()).count();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}\t{}\t{}\t{} encrypted\t{} patch",
|
||||||
|
archive_path.display(),
|
||||||
|
file_count,
|
||||||
|
unpacked_bytes,
|
||||||
|
encrypted_files,
|
||||||
|
patch_files
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
use wow_mpq::{Archive, special_files::parse_listfile};
|
||||||
|
|
||||||
|
fn collect_inputs(path: &Path, inputs: &mut Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
|
||||||
|
if path.is_file() {
|
||||||
|
inputs.push(path.to_path_buf());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in WalkDir::new(path) {
|
||||||
|
let entry = entry?;
|
||||||
|
if entry.file_type().is_file() {
|
||||||
|
inputs.push(entry.path().to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() < 3 {
|
||||||
|
eprintln!("Usage: mpqmasterlist <output.txt> <archive-or-listfile> [...]");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = Path::new(&args[1]);
|
||||||
|
let mut inputs = Vec::new();
|
||||||
|
for input in &args[2..] {
|
||||||
|
collect_inputs(Path::new(input), &mut inputs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut names = BTreeMap::new();
|
||||||
|
let mut sources = 0usize;
|
||||||
|
for input in inputs {
|
||||||
|
let extension = input
|
||||||
|
.extension()
|
||||||
|
.and_then(|value| value.to_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let data = if extension.eq_ignore_ascii_case("mpq") {
|
||||||
|
let mut archive = match Archive::open(&input) {
|
||||||
|
Ok(archive) => archive,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
match archive.read_file("(listfile)") {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
} else if extension.eq_ignore_ascii_case("txt") || extension.eq_ignore_ascii_case("csv") {
|
||||||
|
fs::read(&input)?
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
for name in parse_listfile(&data)? {
|
||||||
|
names.entry(name.to_ascii_lowercase()).or_insert(name);
|
||||||
|
}
|
||||||
|
sources += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = output.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let mut contents = names.into_values().collect::<Vec<_>>().join("\r\n");
|
||||||
|
contents.push_str("\r\n");
|
||||||
|
fs::write(output, contents)?;
|
||||||
|
println!(
|
||||||
|
"Wrote {} unique paths from {sources} listfiles to {}",
|
||||||
|
parse_listfile(&fs::read(output)?)?.len(),
|
||||||
|
output.display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
use wow_mpq::compression::flags;
|
||||||
|
use wow_mpq::{ArchiveBuilder, FormatVersion, ListfileOption};
|
||||||
|
|
||||||
|
fn collect_files(input_dir: &Path) -> Result<Vec<(PathBuf, String)>, Box<dyn Error>> {
|
||||||
|
let root = input_dir.canonicalize()?;
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in WalkDir::new(&root) {
|
||||||
|
let entry = entry?;
|
||||||
|
if !entry.file_type().is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = entry.path().canonicalize()?;
|
||||||
|
let archive_path = source
|
||||||
|
.strip_prefix(&root)?
|
||||||
|
.to_string_lossy()
|
||||||
|
.replace('\\', "/");
|
||||||
|
files.push((source, archive_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort_by(|left, right| {
|
||||||
|
left.1
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.cmp(&right.1.to_ascii_lowercase())
|
||||||
|
.then_with(|| left.1.cmp(&right.1))
|
||||||
|
});
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pack_directory(input_dir: &Path, output_path: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
if input_dir.join("__mpqmeta__").exists() {
|
||||||
|
return Err(format!(
|
||||||
|
"{} contains path-unresolved MPQ blocks and cannot be rebuilt safely",
|
||||||
|
input_dir.display()
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = collect_files(input_dir)?;
|
||||||
|
if files.is_empty() {
|
||||||
|
return Err(format!("input directory contains no files: {}", input_dir.display()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(parent) = output_path.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = ArchiveBuilder::new()
|
||||||
|
.version(FormatVersion::V2)
|
||||||
|
.block_size(7)
|
||||||
|
.default_compression(flags::ZLIB)
|
||||||
|
.listfile_option(ListfileOption::Generate);
|
||||||
|
|
||||||
|
let total = files.len();
|
||||||
|
for (index, (source, archive_path)) in files.into_iter().enumerate() {
|
||||||
|
builder = builder.add_file(source, &archive_path);
|
||||||
|
let packed = index + 1;
|
||||||
|
if packed.is_multiple_of(1000) {
|
||||||
|
println!("Queued {packed}/{total} files...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.build(output_path)?;
|
||||||
|
println!(
|
||||||
|
"Packed {total} files from {} to {}",
|
||||||
|
input_dir.display(),
|
||||||
|
output_path.display()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
if args.len() != 3 {
|
||||||
|
eprintln!("Usage: mpqpack <input_dir> <archive.mpq>");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
pack_directory(Path::new(&args[1]), Path::new(&args[2]))
|
||||||
|
}
|
||||||
Vendored
+1
-1
Submodule vendor/warcraftxl updated: 1508beb6ec...30395282a0
Reference in New Issue
Block a user