17 Commits

Author SHA1 Message Date
sindoring 068d4782a9 обновление warcraftxl 2026-09-04 21:58:47 +04:00
sindoring 66eb505af9 fix 2026-08-30 02:55:15 +04:00
sindoring e53b3c1784 авторитарный логин через лаунчер 2026-08-30 02:18:27 +04:00
sindoring 23b3a3b864 Merge branch 'master' into launcher-authoritive-auth 2026-08-29 14:42:21 +04:00
sindoring 597f4030a7 dvc 2026-08-22 19:10:11 +04:00
gasaichandesu ebd76275a1 fix(transmog): remove embed.xml from Transmogrification.toc because this
file does not exist anymore
2026-08-19 20:28:53 +04:00
gasaichandesu 6addd9bb51 feat(premium): add summon squire spell to Spell.dbc 2026-08-19 16:59:32 +04:00
sindoring abde71ceb0 wip 2026-08-17 20:24:00 +04:00
sindoring a984e705c3 обновление сборщика 2026-08-16 16:56:34 +04:00
sindoring 1a80ca8ad8 new customization 2026-08-13 00:55:19 +04:00
sindoring b4a2eb8bc2 переход на warcraftxl 1.1 2026-08-10 17:40:03 +04:00
gasaichandesu ae178d4a18 Merge branch 'master' of ssh://git.moon-well.online:2222/sindoring/moonwell-client 2026-08-08 07:16:25 +04:00
gasaichandesu e5ad71bc59 refactor(addons): extract shared addons to top level AddOns directory 2026-08-08 07:15:33 +04:00
sindoring b0ac94509a wow original exe 2026-08-08 00:40:16 +04:00
sindoring 2e98622454 warcraftxl without submodukle 2026-08-07 17:47:58 +04:00
sindoring e48bb6fc03 fix 2026-08-07 14:28:45 +04:00
gasaichan a3b7b834b7 Merge pull request 'feat(transmog): add transmogrification feature' (#2) from feature/transmog into master
Reviewed-on: #2
2026-08-07 10:04:04 +03:00
210 changed files with 8667 additions and 9395 deletions
+3
View File
@@ -0,0 +1,3 @@
/config.local
/tmp
/cache
+8
View File
@@ -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
+3
View File
@@ -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
+4
View File
@@ -2,6 +2,7 @@ WOW_HOME=C:\Games\World of Warcraft 3.3.5
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=
AWS_DEFAULT_REGION=
AWS_BUCKET=
AWS_ENDPOINT=
@@ -10,3 +11,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=
PRODUCTION_REALMLIST=
PTR_REALMLIST=
LOCAL_REALMLIST=
SSH_USER=root
SSH_HOST=strictlypragmatic.ru
+2
View File
@@ -5,6 +5,8 @@ manifest.json
.vscode
dist
build/
/new customization mpqs/*.mpq
Wow*.exe
*.backup.exe
Logs/
!Wow_Original.exe
+18 -13
View File
@@ -1,20 +1,25 @@
[submodule "vendor/warcraftxl"]
path = vendor/warcraftxl
url = https://github.com/WarcraftXL/wxl-core.git
branch = main
[submodule "vendor/modules/wxl-modern-assets"]
path = vendor/modules/wxl-modern-assets
url = https://github.com/WarcraftXL/wxl-modern-assets.git
branch = main
[submodule "vendor/modules/wxl-modern-render"]
path = vendor/modules/wxl-modern-render
url = https://github.com/WarcraftXL/wxl-modern-render.git
branch = main
[submodule "vendor/modules/wxl-unit-outline"]
path = vendor/modules/wxl-unit-outline
url = https://github.com/WarcraftXL/wxl-unit-outline.git
branch = main
branch = v1.1
[submodule "vendor/modules/wxl-modern-adt"]
path = vendor/modules/wxl-modern-adt
url = https://github.com/WarcraftXL/wxl-modern-adt.git
branch = main
[submodule "vendor/warcraftxl"]
path = vendor/warcraftxl
url = https://github.com/WarcraftXL/wxl-core.git
branch = v1.1
[submodule "vendor/modules/wxl-db2"]
path = vendor/modules/wxl-db2
url = https://github.com/WarcraftXL/wxl-db2.git
[submodule "vendor/modules/wxl-modern-m2"]
path = vendor/modules/wxl-modern-m2
url = https://github.com/sindoring/wxl-modern-m2.git
branch = agent/guard-oversized-m2-batches
[submodule "vendor/modules/wxl-modern-wmo"]
path = vendor/modules/wxl-modern-wmo
url = https://github.com/WarcraftXL/wxl-modern-wmo.git
[submodule "vendor/modules/wxl-grasswind"]
path = vendor/modules/wxl-grasswind
url = https://github.com/WarcraftXL/wxl-grasswind.git
+65 -52
View File
@@ -1,67 +1,80 @@
cmake_minimum_required(VERSION 3.20)
project(MoonWellWarcraftXL LANGUAGES CXX)
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
# its aggregate DLL target here, so updating the framework does not mix our
# client-specific offsets and policy into the upstream checkout.
# WarcraftXL is an untouched upstream v1.1 submodule. Every project-specific
# feature below is built as a separately loaded Extensions/<name>/<name>.dll.
add_subdirectory(vendor/warcraftxl)
set(WXL_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/warcraftxl")
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
set(WXL_LOCAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules")
function(wxl_collect_sources output)
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
set(${output} "${collected}" PARENT_SCOPE)
function(wxl_add_external_extension extension_name extension_dir)
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
return()
endif()
file(GLOB_RECURSE extension_sources CONFIGURE_DEPENDS "${extension_dir}/src/*.cpp")
if(NOT extension_sources)
message(FATAL_ERROR "WarcraftXL extension '${extension_name}' has no sources in ${extension_dir}/src")
endif()
set(extension_shared_sources "")
if(extension_name STREQUAL "wxl-modern-m2")
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
"${WXL_CORE_DIR}/src/engine/assets/shared/models/m2/*.cpp")
elseif(extension_name STREQUAL "wxl-modern-blp")
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
"${WXL_CORE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
endif()
file(GLOB wxl_sdk_sources CONFIGURE_DEPENDS "${WXL_CORE_DIR}/src/game/*.cpp")
add_library(${extension_name} SHARED
${extension_sources}
${extension_shared_sources}
${wxl_sdk_sources})
set_target_properties(${extension_name} PROPERTIES
OUTPUT_NAME "${extension_name}"
PREFIX "")
target_include_directories(${extension_name} PRIVATE
"${extension_dir}/src"
"${WXL_CORE_DIR}/include"
"${WXL_CORE_DIR}/src")
target_compile_features(${extension_name} PRIVATE cxx_std_20)
target_compile_definitions(${extension_name} PRIVATE
WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS WXL_EXTENSION)
if(CLIENT_PATH)
add_custom_command(TARGET ${extension_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"${CLIENT_PATH}/Extensions/${extension_name}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${extension_name}>"
"${CLIENT_PATH}/Extensions/${extension_name}/${extension_name}.dll"
COMMENT "Deploy ${extension_name}.dll -> Extensions/${extension_name}")
endif()
endfunction()
if(TARGET WarcraftXL)
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
endif()
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
# Official WarcraftXL 1.1 extensions.
wxl_add_external_extension(wxl-db2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-db2")
wxl_add_external_extension(wxl-modern-adt "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt")
wxl_add_external_extension(wxl-modern-blp "${WXL_LOCAL_MODULES_DIR}/wxl-modern-blp")
wxl_add_external_extension(wxl-modern-m2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-m2")
wxl_add_external_extension(wxl-modern-wmo "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-wmo")
wxl_add_external_extension(wxl-grasswind "${WXL_EXTERNAL_MODULES_DIR}/wxl-grasswind")
wxl_add_external_extension(wxl-unit-outline "${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline")
target_sources(WarcraftXL PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
${WXL_MODERN_ASSETS_RUNTIME}
${WXL_MODERN_RENDER_RUNTIME}
${WXL_UNIT_OUTLINE_RUNTIME}
${WXL_MODERN_ADT_RUNTIME})
# wxl-modern-render needs its module-root includes and D3D12 import library.
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
# 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.
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-moonwell-storage-fallback
"${WXL_LOCAL_MODULES_DIR}/moonwell-storage-fallback")
if(TARGET WarcraftXLHost)
wxl_collect_sources(WXL_MOONWELL_HOST
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/host/*.cpp")
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
endif()
wxl_collect_sources(WXL_MODERN_ADT_HOST
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
target_sources(WarcraftXLHost PRIVATE
${WXL_MOONWELL_HOST}
${WXL_MODERN_ASSETS_HOST}
${WXL_MODERN_ADT_HOST})
endif()
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
# Recovery loader retained for existing -NativeRenderer workflows. WarcraftXL
# 1.1 already ships a native D3D9 forwarding proxy as its standard renderer.
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
add_library(MoonWellLoader SHARED
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
+355
View File
@@ -0,0 +1,355 @@
# Совместимость CVPatch и расширенной кастомизации
Документ описывает фактический набор графических MPQ MoonWell для клиента
**WoW 3.3.5a, build 12340**, их порядок загрузки, конфликты DBC, сборку слоя
совместимости и серверные требования парикмахерской.
## Исходные данные и происхождение
Старые архивы `patch-ruRU-A..W` относятся к **CVPatch (Collection Visual
Patch)** от Vary/vvladoon. Найденная тема оригинального проекта:
<https://forum.uwow.biz/threads/cvpatch-collection-visual-patch-lich-king-3-3-5.296264/>
Источник новых архивов утрачен. Единственная сохранённая подпись находится в
`new customization mpqs/readme.txt`:
- `patch-a-001.mpq``Model and Textures NPC`;
- `patch-b-002.mpq``Characters HD The War Within`;
- `patch-k.mpq``Interface`.
Новые архивы нельзя считать официальным или полностью автономным комплектом
для 3.3.5a: в них отсутствовал совместимый `BarberShopStyle.dbc`, а часть DBC
содержит данные более новых версий игры.
## Установленная часть CVPatch
| Патч | Назначение | Зависимости и пересечения |
|---|---|---|
| A | Основные модели существ, гуманоидов и транспорта | Основа для старого B |
| B | Альтернативные модели существ, гуманоидов и транспорта | Требует A |
| E | Текстуры экипировки в повышенном разрешении | Независимый |
| H | HD-модели игровых персонажей Leeviathan | Перекрывается новыми X/Y на одинаковых путях |
| I | Новые модели и текстуры брони и оружия | Изначально требовал H; его item-поля сохраняются при слиянии |
| M | Карты классических подземелий и пещер | Независимый |
| S | Эффекты заклинаний и способностей | Содержит собственные spell DBC |
| T | Текстуры зданий и зон | Независимый |
| U | Окружение Низины Арати и Ущелья Песни Войны | Независимый |
| W | Вода | Независимый |
Старые буквенные имена не меняются. X, Y и Z специально выбраны после W,
чтобы новый комплект и исправления имели приоритет без переименования CVPatch.
## Новые исходные архивы
### `patch-a-001.mpq` → `patch-ruRU-X.MPQ`
Размер исходника — около 2,49 GiB, `(listfile)` содержит 29 495 файлов:
- 16 023 запечённых NPC-текстуры в `Textures\bakednpctextures`;
- 13 465 файлов моделей, skin, анимаций и текстур в `Character`;
- модели стандартных рас и ряда NPC-рас: Vrykul, Naga, Taunka, Forest Troll и
других;
- 28 120 BLP, 1 245 ANIM, 94 SKIN и 29 M2;
- 7 DBC.
DBC внутри X:
| DBC | Строк | Роль |
|---|---:|---|
| `CharacterFacialHairStyles.dbc` | 272 | Штатный объём лицевой кастомизации |
| `CharHairGeosets.dbc` | 370 | Штатный объём причёсок |
| `CharSections.dbc` | 10 060 | Секции текстур персонажей |
| `CreatureDisplayInfoExtra.dbc` | 15 477 | Дополнительные параметры отображения существ |
| `EmotesTextSound.dbc` | 782 | Звуки текстовых эмоций |
| `HelmetGeosetVisData.dbc` | 21 | Скрытие geoset под шлемами |
| `SpellVisualKitModelAttach.dbc` | 594 | Крепление моделей визуальных эффектов |
X нужен текущему комбинированному комплекту для NPC-моделей и запечённых
текстур. Для новых причёсок главным является Y, который загружается позже и
перекрывает одноимённые таблицы X.
### `patch-b-002.mpq` → `patch-ruRU-Y.MPQ`
Размер исходника — около 2,80 GiB, `(listfile)` содержит 36 191 файл:
- 36 180 файлов в `Character`;
- HD-модели и текстуры десяти стандартных игровых рас WotLK;
- 34 934 BLP, 1 021 ANIM, 125 BONE, 80 SKIN и 20 M2;
- 11 DBC с существенно расширенной кастомизацией.
DBC внутри Y:
| DBC | Строк | Роль |
|---|---:|---|
| `CharacterFacialHairStyles.dbc` | 3 616 | Бороды, серьги, рога и другие лицевые варианты |
| `CharHairGeosets.dbc` | 854 | Geoset и номера причёсок |
| `CharHairTextures.dbc` | 177 | Текстуры волос |
| `CharSections.dbc` | 614 212 | Расширенные секции кожи, лица, волос и их сочетаний |
| `CharVariations.dbc` | 62 | Наборы вариаций персонажей |
| `ChrRaces.dbc` | 41 | Описания рас, включая данные за пределами штатного WotLK |
| `CreatureDisplayInfo.dbc` | 24 282 | Display ID моделей |
| `CreatureDisplayInfoExtra.dbc` | 15 477 | Раса, пол, внешность и предметные поля display ID |
| `CreatureFamily.dbc` | 40 | Семейства существ |
| `CreatureModelData.dbc` | 1 353 | Пути и параметры моделей |
| `HelmetGeosetVisData.dbc` | 65 | Расширенные правила отображения под шлемами |
Y обязателен для расширенной кастомизации. Z не содержит копии всех этих
таблиц и не заменяет Y.
Наличие современных race ID в DBC не добавляет новые игровые расы в ядро или
интерфейс 3.3.5a. Текущая интеграция поддерживает расширение внешности десяти
штатных рас. Новые игровые расы потребуют отдельной доработки клиента, UI,
сетевого протокола и сервера.
### `patch-k.mpq` — не устанавливается
Размер — около 53 MiB, `(listfile)` содержит 673 файла только в `Interface`:
- 547 BLP;
- 68 SKIN и 24 M2 для экранов входа и выбора персонажа;
- 15 Lua, 12 XML, TOC и вспомогательные файлы.
Архив меняет `Interface\Glues`, `Interface\GlueXML`, модели экранов входа и
создания персонажа. Он намеренно исключён из сборки, потому что перекрывает
кастомный интерфейс MoonWell. Устанавливать его вручную поверх текущего клиента
нельзя без отдельного разбора и слияния UI.
## Итоговый порядок загрузки
В локализованной цепочке `Data\ruRU` используется следующий порядок:
1. штатные архивы клиента, включая `patch-ruRU-3.MPQ`;
2. старые CVPatch `patch-ruRU-A..W`;
3. `patch-ruRU-X.MPQ` — неизменённая копия `patch-a-001.mpq`;
4. `patch-ruRU-Y.MPQ` — неизменённая копия `patch-b-002.mpq`;
5. `patch-ruRU-Z.MPQ` — сгенерированный слой совместимости MoonWell.
Z является последней инстанцией для конфликтующих DBC. Нельзя переносить его
раньше X/Y или устанавливать другой locale MPQ после Z, который содержит те же
таблицы.
## Содержимое `patch-ruRU-Z.MPQ`
В текущей сборке Z содержит:
- `BarberShopStyle.dbc` — расширенная таблица парикмахерской;
- `CreatureDisplayInfoExtra.dbc` — результат слияния новых display ID и
предметных полей CVPatch I;
- `SpellVisualKitModelAttach.dbc` — более полная версия из CVPatch S;
- `CreatureModelData.dbc` — сгенерированное слияние штатных, CVPatch и новых
моделей;
- `CreatureDisplayInfo.dbc` — сгенерированное слияние display ID;
- `WXLFileData.csv` и `WXLSpellOverrides.tsv` для WarcraftXL.
Последние четыре файла добавляются упаковщиком автоматически; поэтому состав
папки `src\Data\ruRU\patch-ruRU-Z` и готового MPQ не обязан совпадать буквально.
### Правила слияния DBC
`tool/scripts/build-customization-compat.ps1` выполняет следующие операции:
- берёт штатную базу из `patch-ruRU-3.MPQ`;
- сохраняет уникальные изменения `CreatureModelData` из CVPatch A;
- сохраняет уникальные изменения `CreatureDisplayInfo` из CVPatch B;
- отдаёт приоритет Y при реальном конфликте одной и той же строки;
- переносит изменённые item-поля `CreatureDisplayInfoExtra` из CVPatch I в
новые строки Y;
- восстанавливает `SpellVisualKitModelAttach.dbc` из CVPatch S;
- генерирует расширенный `BarberShopStyle.dbc`;
- обновляет входные `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` для
генератора MoonWell creatures.
Так Z не позволяет Y случайно уничтожить старые изменения существ, экипировки
и эффектов, а генерация MoonWell creatures не уничтожает новые display ID.
## Расширенная парикмахерская
### Причина исходного падения
Y расширил:
- `CharHairGeosets.dbc` с 370 до 854 строк;
- `CharacterFacialHairStyles.dbc` с 272 до 3 616 строк.
При этом исходные новые MPQ не содержали `BarberShopStyle.dbc`. Клиент продолжал
использовать штатную таблицу на 491 строку. При выборе нового варианта функция
`GetBarberShopTotalCost` не находила запись, после чего WoW падал с
`ERROR #132 / ACCESS_VIOLATION`. В зафиксированном крэше вызов шёл от
`BarberShopFrameSelector1Next`.
### Как сформирована новая таблица
Генератор:
- сохраняет все 491 штатную строку и исходную строковую таблицу байт-в-байт;
- добавляет отсутствующие причёски из `CharHairGeosets.dbc` Y;
- добавляет отсутствующую растительность и украшения из
`CharacterFacialHairStyles.dbc` Y;
- работает только с сочетаниями расы, пола и типа, уже существующими в штатном
`BarberShopStyle.dbc`, то есть с поддерживаемыми расами WotLK;
- сохраняет штатные строки типа 3 для особой кастомизации рыцарей смерти;
- назначает новые уникальные ID после максимального штатного ID;
- проверяет отсутствие дубликатов и полноту покрытия.
Результат:
- 3 765 строк всего;
- 152 новые причёски;
- 3 122 новых варианта растительности, серёг и других особенностей;
- максимальный номер лицевого варианта — 255, что помещается в поле внешности
персонажа 3.3.5a.
В исходном комплекте нет названий новых стилей. Поэтому сейчас они отображаются
как `New hairstyle N` и `New appearance N`. Это только подписи: на выбор модели
и сохранение внешности они не влияют.
## Что требуется клиенту и серверу
| Компонент | Клиент | Сервер |
|---|---|---|
| `patch-ruRU-X.MPQ` | Да, для текущего комплекта NPC/персонажей | Нет |
| `patch-ruRU-Y.MPQ` | Да, для моделей, текстур и основных customization DBC | Нет |
| `patch-ruRU-Z.MPQ` | Да, для совместимости и исправления парикмахерской | Нет как MPQ |
| Сгенерированный `BarberShopStyle.dbc` | Уже находится внутри Z | Да, отдельным DBC |
| Остальные клиентские DBC из X/Y | Загружаются из MPQ | Для текущих десяти рас не копируются |
Серверную копию нужно положить в каталог DBC worldserver, обычно:
```text
<server>/data/dbc/BarberShopStyle.dbc
```
После замены требуется полный перезапуск worldserver: DBC загружаются при
старте. Клиентская и серверная копии `BarberShopStyle.dbc` должны быть одним и
тем же сгенерированным файлом. Без серверной копии клиент больше не должен
падать при переключении, но сервер может отклонить применение нового стиля или
неверно рассчитать его стоимость.
Готовая серверная копия после запуска генератора находится в:
```text
build/customization-compat/server-dbc/BarberShopStyle.dbc
```
Тот же отслеживаемый исходник для Z находится в:
```text
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/BarberShopStyle.dbc
```
Не следует копировать на сервер `ChrRaces.dbc`, `CharSections.dbc` или весь
набор DBC из Y без отдельной задачи и проверки ядра.
## Сборка
### Предварительные условия
В `.env` должен быть задан корректный `WOW_HOME`. В клиенте должны находиться:
- штатный `Data\ruRU\patch-ruRU-3.MPQ`;
- CVPatch A, B, I и S, используемые генератором как источники слияния;
- остальные CVPatch, если они должны остаться в итоговой установке.
В репозитории должны оставаться:
- `new customization mpqs\patch-a-001.mpq`;
- `new customization mpqs\patch-b-002.mpq`;
- `tool\target\release\mpqread.exe`.
### Пересоздание DBC совместимости
```powershell
.\tool\scripts\build-customization-compat.ps1
```
Скрипт обновляет:
```text
assets/dbc/3.3.5a/CreatureModelData.dbc
assets/dbc/3.3.5a/CreatureDisplayInfo.dbc
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/CreatureDisplayInfoExtra.dbc
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/SpellVisualKitModelAttach.dbc
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/BarberShopStyle.dbc
build/customization-compat/server-dbc/BarberShopStyle.dbc
```
### Упаковка MPQ
```powershell
.\tool\target\release\tool.exe .\src .\dist
```
Упаковщик:
- собирает Z из исходников и сгенерированных MoonWell DBC;
- не копирует внешние графические X/Y в `dist` и не включает их в managed manifest;
- не добавляет `patch-k.mpq`.
Для полной локальной сборки и синхронизации клиента можно использовать:
```powershell
.\run.ps1 -Env local
```
Этот сценарий также собирает WarcraftXL, останавливает блокирующие процессы и
синхронизирует весь `dist` в `WOW_HOME`. Для проверки только изменения Z можно
собрать архив упаковщиком и скопировать лишь
`dist\Data\ruRU\patch-ruRU-Z.MPQ`.
Сборка этих архивов не должна автоматически изменять `manifest.json`. Манифест
обновляется владельцем проекта отдельно после локальной проверки.
## Исходники MPQ и установленные копии
Файлы в `new customization mpqs` используются только как локальные источники
генератора таблиц совместимости. В готовом клиенте X и Y управляются отдельно от
репозиторных патчей.
Списки репозиторных и внешних графических патчей находятся в
`patch-layout.json`. `deploy.ps1`, `run.ps1`, генератор манифеста и S3 uploader
синхронизируют только список `repositoryPatches`. Пути из `graphicsPatches`
сохраняются без копирования и удаления. Если один путь указан в обоих списках,
`repositoryPatches` имеет приоритет.
Z не консолидирует многогигабайтные модели и текстуры X/Y. Удаление X или Y из
клиента не освобождается наличием текущих DBC в Z.
## Проверка после сборки
Минимальный сценарий локальной проверки:
1. Полностью закрыть WoW перед заменой MPQ.
2. Проверить наличие X, Y и Z в `Data\ruRU`.
3. Запустить клиент и открыть создание персонажа.
4. Проверить оба пола и основные расы, пролистать причёски и лицевые варианты.
5. Создать тестового персонажа, войти в мир и проверить модель после повторного
входа.
6. Открыть парикмахерскую и несколько раз нажать вперёд/назад у каждого
селектора.
7. Проверить отображение стоимости и отсутствие `ERROR #132`.
8. Применить новый стиль, выйти из мира и войти снова.
9. Проверить старую броню, NPC-модели и эффекты заклинаний, которые могли быть
затронуты конфликтующими DBC.
Для шага 8 сервер уже должен использовать сгенерированный
`BarberShopStyle.dbc`.
## Диагностика
| Симптом | Вероятная причина |
|---|---|
| Новых причёсок нет | Отсутствует Y или более поздний MPQ перекрыл его `CharHairGeosets`/`CharSections` |
| Модель отсутствует или отображается неверно | X/Y не скопированы полностью либо повреждены многогигабайтные архивы |
| Падение при переключении в парикмахерской | Z отсутствует, загружен раньше Y или его `BarberShopStyle.dbc` перекрыт |
| Вариант выбирается, но не применяется | На сервере остался штатный `BarberShopStyle.dbc` или worldserver не перезапущен |
| Исчезли старые NPC/display ID | Z не пересобран после изменения источников CVPatch или Y |
| Проблемы со старой экипировкой | Потеряны item-поля CVPatch I в `CreatureDisplayInfoExtra.dbc` |
| Изменился экран входа/создания персонажа | `patch-k.mpq` был установлен вручную |
Перед заменой рабочего Z рекомендуется сохранять предыдущий архив. Последняя
локальная резервная копия, созданная во время исправления парикмахерской:
```text
build/customization-compat/client-backup/patch-ruRU-Z.before-barbershop.MPQ
```
+87
View File
@@ -0,0 +1,87 @@
# Авторизация MoonWell через лаунчер
Клиентская часть не принимает логин и пароль в Release-сборке. Лаунчер передаёт игре
короткоживущую учётную сессию через environment block дочернего процесса, а GlueXML сразу
запускает стандартный SRP-вход и не показывает форму логина.
## Контракт запуска клиента
Перед `CreateProcess` лаунчер должен добавить в environment block только запускаемого `Wow.exe`:
```text
MOONWELL_LAUNCH_ACCOUNT=<account name>
MOONWELL_LAUNCH_TICKET=<single-use SRP password>
```
Обе переменные обязательны. Нативный модуль читает и удаляет их из окружения процесса до
загрузки GlueXML. Значения не должны передаваться в аргументах командной строки, писаться в
файл, лог или реестр.
Ограничения значений:
- account: печатный ASCII без пробелов, не более 320 байт;
- ticket: ровно 16 символов из `A-Z0-9` (около 82 бит при равномерной генерации);
- ticket должен быть криптографически случайным, одноразовым и жить не более 60 секунд;
- выпуск нового ticket должен отзывать предыдущий активный ticket этого аккаунта.
Пример Win32-последовательности лаунчера:
1. Пользователь авторизуется в лаунчере.
2. Лаунчер запрашивает у backend одноразовый game ticket.
3. Лаунчер создаёт отдельный environment block с двумя переменными выше.
4. Лаунчер запускает `Wow.exe` через `CreateProcessW`.
5. Лаунчер сразу затирает ticket в своей памяти.
Кнопка «Авторизоваться» сначала ищет `MoonWell.exe` или `MoonWellLauncher.exe` рядом с `Wow.exe`. Если файла нет,
она открывает URI `moonwell://authorize?source=client`. Установщик лаунчера должен зарегистрировать
схему `moonwell` для текущего пользователя.
## Обязательное изменение auth-сервера
Скрытие полей в GlueXML не является серверной защитой: пользователь контролирует свой клиент.
Чтобы вход действительно был возможен только через лаунчер, auth-сервер обязан для обычных
аккаунтов отказаться от постоянного password verifier и принимать только активный launcher ticket.
Для совместимости с неизменённым SRP-клиентом ticket используется как временный пароль:
1. Backend хранит для аккаунта один активный ticket либо подготовленные из него временные
SRP `salt` и `verifier`.
2. На `CMD_AUTH_LOGON_CHALLENGE` auth-сервер ищет активный непросроченный ticket аккаунта.
Если ticket отсутствует, сервер отклоняет вход до выполнения обычной password-проверки.
3. SRP challenge строится по временному verifier.
4. После успешного `CMD_AUTH_LOGON_PROOF` ticket атомарно помечается использованным.
5. Повторный proof, повторный запуск и истёкший ticket отклоняются.
Поскольку challenge содержит аккаунт, но не сам ticket, одновременно должен существовать не более
одного активного ticket на аккаунт.
Рекомендуется дополнительно привязать ticket к версии клиента и идентификатору launcher-сессии,
ограничить частоту выпуска и не возвращать его ни в какие аналитические события.
## Режим разработчика
Ручной логин компилируется только в `Debug`-варианте `MoonWell.dll`. Для локального запуска:
```powershell
.\run.ps1 -Env local -DeveloperLogin
```
Authserver должен независимо разрешить тот же аккаунт записью в `launcher_dev_account`
с точным `allowed_ip` и `enabled = 1`. Один только Debug-режим клиента никогда не обходит
серверную проверку.
Скрипт собирает Debug runtime и передаёт `MOONWELL_DEV_LOGIN=1` только запускаемому процессу.
Переменная одноразово удаляется нативным модулем. В Release-сборке она игнорируется.
Auth-сервер также должен отдельно разрешать постоянный SRP verifier только dev-аккаунтам — лучше
по allowlist аккаунтов вместе с VPN/IP-ограничением. Нельзя считать Debug-флаг клиента границей
безопасности и нельзя распространять Debug DLL игрокам.
## Поведение интерфейса
- валидная launcher-сессия: `AccountLoginUI` скрывается и сразу вызывается
`DefaultServerLogin(account, ticket)`;
- прямой Release-запуск: поля логина, пароля и параметры сохранения скрыты, основная кнопка
открывает лаунчер;
- Debug + `MOONWELL_DEV_LOGIN=1`: показывается прежняя форма ручного входа;
- сохранённые ранее логин и пароль удаляются при любом не-dev запуске.
+34 -29
View File
@@ -2,7 +2,7 @@
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL): `Wow.exe` больше не содержит
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL/wxl-core) **1.1.220**: `Wow.exe` больше не содержит
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
только к памяти запущенного процесса.
@@ -14,25 +14,25 @@ MoonWell-патчей. Неподписанный оригинальный execu
- одиннадцатое значение `GetCharacterInfo``charFlags`, включая флаг предателя `0x40000000`;
- загрузка WarcraftXL без import-table patch и без отдельного injector.
Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
При установке сборщик включает Large Address Aware в копии `Wow.exe`; исходный
`Wow_Original.exe` остаётся неизменным и используется как проверенный шаблон.
## Структура
```text
modules/moonwell/ MoonWell runtime-модуль WarcraftXL
modules/ отдельные MoonWell extensions для WarcraftXL 1.1
vendor/warcraftxl/ закреплённый upstream git submodule
vendor/modules/ закреплённые модули WarcraftXL
src/Data/ исходники MPQ-патчей
tool/ сборщик MPQ
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host
build-warcraftxl.ps1 сборка Win32 runtime, proxy и extensions
run.ps1 полная сборка, установка и запуск клиента
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
```
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule.
Подключены совместимые с 1.1 extensions: `wxl-db2`, `wxl-modern-adt`, `wxl-modern-m2`,
`wxl-modern-wmo`, `wxl-grasswind` и `wxl-unit-outline`. BLP-адаптер и MoonWell-изменения
собираются как отдельные DLL; исходники upstream core не изменяются.
Для обновления всех зависимостей WarcraftXL:
```powershell
@@ -44,9 +44,11 @@ git submodule update --remote vendor/warcraftxl vendor/modules/*
## Требования
- Visual Studio 2022 с C++ toolchain для Win32 и x64;
- Visual Studio 2022 с C++ toolchain для Win32;
- CMake 3.20+;
- Rust/Cargo для существующего MPQ-сборщика;
- Python 3 и DVC с поддержкой S3 для содержимого внешних графических патчей
(`py -m pip install -r requirements-dvc.txt`);
- локальный оригинальный `Wow_Original.exe` с SHA-256
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
@@ -64,7 +66,7 @@ git submodule update --init --recursive
.\deploy.ps1
```
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host,
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, native D3D9 proxy и extensions,
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
@@ -77,8 +79,6 @@ git submodule update --init --recursive
# Быстро обновить только WarcraftXL без пересборки MPQ
.\deploy.ps1 -SkipDataBuild
# Аварийный native D3D9 вместо D3D9On12
.\deploy.ps1 -NativeRenderer
```
Без установки в клиент:
@@ -99,18 +99,8 @@ git submodule update --init --recursive
1. проверяет хеш `Wow_Original.exe`;
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
3. восстанавливает оригинальный `Wow.exe`;
4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`;
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`;
6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом
модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и
установить native-переходник без modern-render:
```powershell
.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
4. устанавливает `WarcraftXL.dll`, native D3D9 proxy и DLL в `Extensions\`;
5. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
@@ -121,8 +111,13 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
.\run.ps1
```
Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и
`Utils\WarcraftXLHost.log` внутри клиента.
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
## Авторизация через лаунчер
Release-клиент принимает только одноразовую launcher-сессию и при прямом запуске показывает
кнопку открытия лаунчера вместо полей логина и пароля. Контракт переменных запуска, требования
к auth-серверу и Debug-режим описаны в [LAUNCHER_AUTH.md](LAUNCHER_AUTH.md).
## MPQ-пакеты
@@ -136,8 +131,18 @@ WarcraftXL не требуется и в объектное хранилище
- `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
Управляемые репозиторием MPQ и внешние графические патчи перечислены отдельно в
`patch-layout.json`. Сборка и deploy синхронизируют только `repositoryPatches` и
не изменяют `graphicsPatches`. При пересечении списков репозиторный патч имеет
приоритет.
Распакованное содержимое больших патчей кастомизации хранится через DVC в
`assets/graphics/`, а бинарные объекты — в настроенном S3. Инструкции по первичной
загрузке, изменению и импорту MPQ находятся в
[`assets/graphics/README.md`](assets/graphics/README.md).
## Лицензирование
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный upstream submodule. MoonWell-модуль,
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`.
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
MoonWell extensions распространяются с доступным соответствующим исходным кодом. Репозиторий не
должен публиковать Blizzard assets или `Wow.exe`.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
/patch-X
+6
View File
@@ -0,0 +1,6 @@
outs:
- md5: 012ce72d8dfdf089b20a28f6e6158b58.dir
size: 1195310
nfiles: 4
hash: md5
path: patch-X
+14
View File
@@ -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
+66
View File
@@ -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`.
+157
View File
@@ -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
}
]
}
+70 -31
View File
@@ -75,8 +75,53 @@ function Enable-LargeAddressAware {
}
}
function Initialize-MissingSubmodules {
param(
[Parameter(Mandatory)]
[string]$RepositoryPath
)
$modulesFile = Join-Path $RepositoryPath '.gitmodules'
if (-not (Test-Path -LiteralPath $modulesFile -PathType Leaf)) {
return
}
$configuredModules = @(
& git -C $RepositoryPath config --file .gitmodules --get-regexp '^submodule\..*\.path$'
)
if ($LASTEXITCODE -gt 1) {
throw "Failed to read submodules from $modulesFile"
}
foreach ($configuredModule in $configuredModules) {
$fields = $configuredModule -split '\s+', 2
if ($fields.Count -ne 2) {
continue
}
$relativePath = $fields[1].Trim()
$modulePath = Join-Path $RepositoryPath $relativePath
$gitMarker = Join-Path $modulePath '.git'
# Updating an initialized submodule checks out the SHA recorded by its parent and detaches
# any active development branch. Only initialize genuinely missing worktrees.
if (-not (Test-Path -LiteralPath $gitMarker)) {
Write-Host "Initializing missing submodule: $modulePath"
& git -C $RepositoryPath submodule update --init -- $relativePath
if ($LASTEXITCODE -ne 0) {
throw "Failed to initialize submodule: $modulePath"
}
}
if (Test-Path -LiteralPath $modulePath -PathType Container) {
Initialize-MissingSubmodules -RepositoryPath $modulePath
}
}
}
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
. (Join-Path $repoRoot 'pipeline-layout.ps1')
$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
if (-not (Test-Path -LiteralPath $cmake)) {
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
@@ -84,11 +129,9 @@ if (-not (Test-Path -LiteralPath $cmake)) {
$cmake = $cmakeCommand.Source
}
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
Initialize-MissingSubmodules -RepositoryPath $repoRoot
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
@@ -104,22 +147,18 @@ if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader
& $cmake --build $win32BuildDir --config $Configuration --parallel 4
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
$selectedProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$recoveryProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$extensionNames = Get-MoonWellExtensionNames
if ($NativeRenderer) {
Write-Warning '-NativeRenderer is no longer needed: WarcraftXL 1.1 uses native D3D9 by default.'
}
function Install-WarcraftXLArtifacts {
param(
@@ -127,12 +166,12 @@ function Install-WarcraftXLArtifacts {
[string]$Destination
)
$utils = Join-Path $Destination 'Utils'
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
Remove-MoonWellRuntimeGarbage -Destination $Destination
$utils = Join-Path $Destination 'Utils'
New-Item `
-ItemType Directory `
-Path $Destination, $utils, $loosePatch `
-Path $Destination, $utils `
-Force |
Out-Null
@@ -156,20 +195,17 @@ function Install-WarcraftXLArtifacts {
-Force
Copy-Item `
-LiteralPath $nativeProxy `
-LiteralPath $recoveryProxy `
-Destination (Join-Path $utils 'd3d9-native.dll') `
-Force
Copy-Item `
-LiteralPath $hostExe `
-Destination (Join-Path $utils 'WarcraftXLHost.exe') `
-Force
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
foreach ($extensionName in $extensionNames) {
$extensionSource = Join-Path $win32BuildDir "$Configuration\$extensionName.dll"
$extensionDestination = Join-Path $Destination "Extensions\$extensionName"
New-Item -ItemType Directory -Path $extensionDestination -Force | Out-Null
Copy-Item `
-LiteralPath $_.FullName `
-Destination $loosePatch `
-Recurse `
-LiteralPath $extensionSource `
-Destination (Join-Path $extensionDestination "$extensionName.dll") `
-Force
}
}
@@ -196,14 +232,17 @@ if ($Deploy) {
}
Install-WarcraftXLArtifacts -Destination $ClientPath
Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath"
Write-Host "Installed WarcraftXL 1.1 runtime and extensions in $ClientPath"
}
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
Install-WarcraftXLArtifacts -Destination $PackagePath
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
}
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
Write-Host "WarcraftXL 1.1 build complete (native D3D9 proxy): $win32ArtifactDir"
+4
View File
@@ -9,6 +9,8 @@ import sys
from pathlib import Path
from typing import Optional
from patch_layout import PATCH_LAYOUT
IGNORED_TOP_LEVEL_DIRS = {
"Cache",
@@ -49,6 +51,8 @@ def is_ignored(rel_path: Path) -> bool:
return True
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
return True
if PATCH_LAYOUT.is_external_graphics_patch(rel_path.as_posix()):
return True
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
+56
View File
@@ -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.'
+1 -8
View File
@@ -10,12 +10,5 @@
"pet_template_display_id": 5448
},
"mounts": [],
"pets": [
{
"folder": "src/Data/patch-Z/Creature/Catslime",
"name": "Cat Slime",
"name_ru": "Котослизень",
"display_scale": 1.0
}
]
"pets": []
}
+33 -11
View File
@@ -13,6 +13,7 @@ param(
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
. (Join-Path $repoRoot 'pipeline-layout.ps1')
function Import-ProjectEnvironment {
$envFile = Join-Path $repoRoot '.env'
@@ -46,6 +47,10 @@ $PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath"
}
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
@@ -58,20 +63,27 @@ if ($runningClients.Count) {
Write-Host 'Stopping the running WoW client...'
$runningClients | Stop-Process -Force
Start-Sleep -Seconds 2
Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
} | Stop-Process -Force
}
$env:WOW_HOME = $ClientPath
Write-Host "MoonWell client: $ClientPath"
Write-Host "Package staging: $PackagePath"
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
Write-Host '[1/5] Preserving initialized WarcraftXL worktrees...'
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
$previousPackageFiles = @(
Get-RelativeFilePaths -Root $PackagePath
)
if (-not $SkipDataBuild) {
Write-Host 'Resetting package staging to prevent stale build artifacts...'
Reset-MoonWellPackageStaging `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
} else {
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
Remove-MoonWellRuntimeGarbage -Destination $PackagePath
}
if (-not $SkipDataBuild) {
Write-Host '[2/5] Building MPQ patches...'
@@ -102,6 +114,18 @@ if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
Write-Host '[4/5] Synchronizing package files...'
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) { throw 'Python was not found; package validation cannot run.' }
& $python.Source (Join-Path $repoRoot 'validate_package.py') --dir $PackagePath
if ($LASTEXITCODE -ne 0) { throw 'Package validation failed.' }
$currentPackageFiles = @(Get-RelativeFilePaths -Root $PackagePath)
Remove-StalePackageFilesFromClient `
-ClientPath $ClientPath `
-PreviousFiles $previousPackageFiles `
-CurrentFiles $currentPackageFiles
Remove-MoonWellRuntimeGarbage -Destination $ClientPath
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
$robocopyExitCode = $LASTEXITCODE
if ($robocopyExitCode -ge 8) {
@@ -110,8 +134,6 @@ if ($robocopyExitCode -ge 8) {
if (-not $SkipManifest) {
Write-Host '[5/5] Building launcher manifest...'
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) { throw 'Python was not found; launcher manifest was not generated.' }
& $python.Source (Join-Path $repoRoot 'build_manifest.py') `
--dir $PackagePath --output (Join-Path $repoRoot 'manifest.json')
if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' }
@@ -120,12 +142,12 @@ if (-not $SkipManifest) {
}
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' }
$renderer = 'WarcraftXL 1.1 native D3D9 proxy'
Write-Host ''
Write-Host 'Deployment complete.'
Write-Host " Renderer: $renderer"
Write-Host " Wow.exe SHA-256: $wowHash"
Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')"
Write-Host " Extensions: $(Join-Path $ClientPath 'Extensions')"
if ($Launch) {
Write-Host 'Launching Wow.exe...'
+2 -3
View File
@@ -50,9 +50,8 @@ extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** outp
return E_NOINTERFACE;
}
// Compatibility exports referenced by the current WarcraftXL core. MoonWell
// does not enable its D3D9On12 backend, so these intentionally report no D3D12
// device and a neutral supersampling factor.
// Legacy compatibility exports retained for the recovery proxy. WarcraftXL 1.1
// uses native D3D9 and ignores the former D3D9On12 bridge.
extern "C" void* WxlD3D12Device() { return nullptr; }
extern "C" void* WxlD3D12Queue() { return nullptr; }
extern "C" void WxlD3D12DrainDebug() {}
+161
View File
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// FileDataID overlay for selected retail assets shipped in MoonWell patches.
#include "game/Io.hpp"
#include "wxl/FdidApi.h"
#include "wxl/PluginApi.h"
#include <windows.h>
#include <charconv>
#include <cstdint>
#include <mutex>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>
namespace moonwell::fdid
{
namespace
{
constexpr std::string_view kFileDataMap = "WXLFileData.csv";
std::once_flag g_loadOnce;
std::unordered_map<uint32_t, std::string> g_paths;
const WXL_Api* g_api = nullptr;
const char*(__cdecl* g_resolveTexture)(uint32_t) = nullptr;
const char*(__cdecl* g_resolveModel)(uint32_t) = nullptr;
bool ReadAll(const char* path, std::vector<uint8_t>& out)
{
void* handle = nullptr;
if (!wxl::game::io::FileOpen(path, wxl::game::io::kOpenWholeFile, &handle) || !handle)
return false;
uint32_t high = 0;
const uint32_t size = wxl::game::io::FileSize(handle, &high);
if (high != 0)
{
wxl::game::io::FileClose(handle);
return false;
}
out.resize(size);
uint32_t read = 0;
const bool ok = (size == 0 || wxl::game::io::FileRead(handle, out.data(), size, &read))
&& read == size;
wxl::game::io::FileClose(handle);
if (!ok) out.clear();
return ok;
}
std::string_view Trim(std::string_view value)
{
while (!value.empty() && (value.front() == ' ' || value.front() == '\t' ||
value.front() == '\r' || value.front() == '\n'))
value.remove_prefix(1);
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' ||
value.back() == '\r' || value.back() == '\n'))
value.remove_suffix(1);
return value;
}
void Load()
{
std::vector<uint8_t> bytes;
if (!ReadAll(kFileDataMap.data(), bytes))
{
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_WARN, "MoonWellFdid", "FileDataID map '%s' was not found",
kFileDataMap.data());
return;
}
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
size_t lineStart = 0;
while (lineStart < text.size())
{
size_t lineEnd = text.find('\n', lineStart);
if (lineEnd == std::string_view::npos) lineEnd = text.size();
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
lineStart = lineEnd + 1;
if (line.empty() || line.front() == '#') continue;
const size_t comma = line.find(',');
if (comma == std::string_view::npos) continue;
const std::string_view idText = Trim(line.substr(0, comma));
std::string_view pathText = Trim(line.substr(comma + 1));
uint32_t fileDataId = 0;
const auto parsed = std::from_chars(idText.data(), idText.data() + idText.size(), fileDataId);
if (parsed.ec != std::errc{} || parsed.ptr != idText.data() + idText.size() ||
fileDataId == 0 || pathText.empty())
continue;
std::string path(pathText);
for (char& c : path) if (c == '/') c = '\\';
g_paths[fileDataId] = std::move(path);
}
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_INFO, "MoonWellFdid", "loaded %zu custom FileDataID path(s)",
g_paths.size());
}
const char* ResolveCustom(uint32_t fileDataId)
{
std::call_once(g_loadOnce, &Load);
const auto found = g_paths.find(fileDataId);
return found == g_paths.end() ? nullptr : found->second.c_str();
}
const char* __cdecl ResolveTexture(uint32_t fileDataId)
{
if (const char* custom = ResolveCustom(fileDataId)) return custom;
return g_resolveTexture ? g_resolveTexture(fileDataId) : nullptr;
}
const char* __cdecl ResolveModel(uint32_t fileDataId)
{
if (const char* custom = ResolveCustom(fileDataId)) return custom;
return g_resolveModel ? g_resolveModel(fileDataId) : nullptr;
}
bool PatchResolver(WXL_FdidApi* fdid)
{
if (!fdid || fdid->apiVersion != WXL_FDID_API_VERSION) return false;
g_resolveTexture = fdid->ResolveTexture;
g_resolveModel = fdid->ResolveModel;
DWORD oldProtection = 0;
if (!VirtualProtect(fdid, sizeof(*fdid), PAGE_READWRITE, &oldProtection)) return false;
fdid->ResolveTexture = &ResolveTexture;
fdid->ResolveModel = &ResolveModel;
DWORD ignored = 0;
VirtualProtect(fdid, sizeof(*fdid), oldProtection, &ignored);
return true;
}
}
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWellFdid", 1, WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
moonwell::fdid::g_api = api;
auto* fdid = static_cast<WXL_FdidApi*>(api->GetInterface("wxl.fdid", WXL_FDID_API_VERSION));
if (!moonwell::fdid::PatchResolver(fdid))
{
api->Log(WXL_LOG_ERROR, "MoonWellFdid", "wxl.fdid v1 is unavailable");
return 0;
}
api->Log(WXL_LOG_INFO, "MoonWellFdid", "custom FileDataID overlay installed");
return 1;
}
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// MoonWell's specific-archive fallback, kept outside the WarcraftXL core.
#include "wxl/PluginApi.h"
#include <atomic>
#include <cstdint>
namespace
{
const WXL_Api* g_api = nullptr;
using FileOpenFn = int(__stdcall*)(void* archive, const char* name, uint32_t flags, void** out);
FileOpenFn g_nextOpen = nullptr;
std::atomic<uint32_t> g_logCount{0};
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
{
if (!g_nextOpen) return 0;
const int nativeResult = g_nextOpen(archive, name, flags, out);
if (nativeResult || archive == nullptr || !name || !*name) return nativeResult;
// A dependency requested through one concrete MPQ may live in a higher-priority loose
// Patch-*.MPQ directory. Retry only after that exact archive misses, using the client's
// normal global search path. This is the MoonWell-only behavior that used to patch core.
if (out) *out = nullptr;
const int fallbackResult = g_nextOpen(nullptr, name, flags, out);
if (fallbackResult && g_api && g_api->Log)
{
const uint32_t index = g_logCount.fetch_add(1, std::memory_order_relaxed);
if (index < 32)
g_api->Log(WXL_LOG_INFO, "MoonWellStorageFallback",
"specific archive miss resolved globally: '%s'", name);
}
return fallbackResult;
}
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWellStorageFallback", 1,
WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION || !api->HookAttachByName) return 0;
g_api = api;
const int installed = api->HookAttachByName(
"Io.FileOpen", reinterpret_cast<void*>(&FileOpenHook),
reinterpret_cast<void**>(&g_nextOpen), WXL_HOOK_DEFAULT_PRIORITY);
api->Log(installed ? WXL_LOG_INFO : WXL_LOG_ERROR, "MoonWellStorageFallback", "%s",
installed ? "specific-archive fallback installed" : "Io.FileOpen hook failed");
return installed;
}
@@ -1,95 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// FileDataID resolver for selected retail assets shipped in MoonWell patches.
#include "Host.hpp"
#include "core/Logger.hpp"
#include "mpq/MpqStore.hpp"
#include <charconv>
#include <cstdint>
#include <mutex>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>
namespace moonwell::host
{
namespace
{
constexpr std::string_view kFileDataMap = "WXLFileData.csv";
std::once_flag g_loadOnce;
std::unordered_map<uint32_t, std::string> g_paths;
std::string_view Trim(std::string_view value)
{
while (!value.empty() && (value.front() == ' ' || value.front() == '\t' ||
value.front() == '\r' || value.front() == '\n'))
value.remove_prefix(1);
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' ||
value.back() == '\r' || value.back() == '\n'))
value.remove_suffix(1);
return value;
}
void Load()
{
const std::string root = wxl::host::ClientRoot();
wxl::host::mpq::MpqStore store;
std::vector<uint8_t> bytes;
if (root.empty() || !store.Mount(root) || !store.ReadAll(kFileDataMap, bytes))
{
WLOG_WARN("moonwell: FileDataID map '%.*s' was not found",
int(kFileDataMap.size()), kFileDataMap.data());
return;
}
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
size_t lineStart = 0;
while (lineStart < text.size())
{
size_t lineEnd = text.find('\n', lineStart);
if (lineEnd == std::string_view::npos) lineEnd = text.size();
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
lineStart = lineEnd + 1;
if (line.empty() || line.front() == '#') continue;
const size_t comma = line.find(',');
if (comma == std::string_view::npos) continue;
const std::string_view idText = Trim(line.substr(0, comma));
std::string_view pathText = Trim(line.substr(comma + 1));
uint32_t fileDataId = 0;
const auto parsed = std::from_chars(idText.data(), idText.data() + idText.size(), fileDataId);
if (parsed.ec != std::errc{} || parsed.ptr != idText.data() + idText.size() ||
fileDataId == 0 || pathText.empty())
continue;
std::string path(pathText);
for (char& c : path) if (c == '/') c = '\\';
g_paths[fileDataId] = std::move(path);
}
WLOG_INFO("moonwell: loaded %zu FileDataID path(s) from %.*s",
g_paths.size(), int(kFileDataMap.size()), kFileDataMap.data());
}
bool Resolve(uint32_t fileDataId, std::string& outPath)
{
std::call_once(g_loadOnce, &Load);
const auto found = g_paths.find(fileDataId);
if (found == g_paths.end()) return false;
outPath = found->second;
return true;
}
struct Registrar
{
Registrar() { wxl::host::RegisterResolver("moonwell-filedata", &Resolve); }
};
Registrar g_registrar;
}
}
+563 -53
View File
@@ -5,28 +5,326 @@
// changes are verified and applied to the process image during WarcraftXL's
// boot phase, before GlueXML is loaded.
#include "core/Logger.hpp"
#include "core/Hook.hpp"
#include "core/Mem.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"
#include "offsets/engine/Gx.hpp"
#include "game/Script.hpp"
#include "wxl/PluginApi.h"
#include "SpellOverrides.hpp"
#include <windows.h>
#include <shellapi.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <cwchar>
namespace moonwell
{
namespace
{
const WXL_Api* g_api = nullptr;
void Log(int level, const char* format, ...)
{
if (!g_api || !g_api->Log) return;
char message[1024]{};
va_list args;
va_start(args, format);
vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args);
va_end(args);
g_api->Log(level, "MoonWell", "%s", message);
}
#define WLOG_INFO(...) Log(WXL_LOG_INFO, __VA_ARGS__)
#define WLOG_ERROR(...) Log(WXL_LOG_ERROR, __VA_ARGS__)
bool PatchMemory(void* destination, const void* source, size_t size)
{
DWORD oldProtection = 0;
if (!VirtualProtect(destination, size, PAGE_EXECUTE_READWRITE, &oldProtection))
return false;
std::memcpy(destination, source, size);
FlushInstructionCache(GetCurrentProcess(), destination, size);
DWORD ignored = 0;
VirtualProtect(destination, size, oldProtection, &ignored);
return true;
}
template <class Fn>
bool Hook(const char* name, uintptr_t address, Fn* detour, Fn** original)
{
return g_api && g_api->HookAttach && g_api->HookAttach(
name, address, reinterpret_cast<void*>(detour),
reinterpret_cast<void**>(original), WXL_HOOK_DEFAULT_PRIORITY) != 0;
}
template <class Fn>
bool HookByName(const char* point, Fn* detour, Fn** original)
{
return g_api && g_api->HookAttachByName && g_api->HookAttachByName(
point, reinterpret_cast<void*>(detour), reinterpret_cast<void**>(original),
WXL_HOOK_DEFAULT_PRIORITY) != 0;
}
constexpr uint32_t kTraitorFlag = 0x40000000u;
bool g_loginCharacterIsTraitor = false;
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
constexpr char kLaunchAccountVariable[] = "MOONWELL_LAUNCH_ACCOUNT";
constexpr char kLaunchTicketVariable[] = "MOONWELL_LAUNCH_TICKET";
constexpr char kDeveloperLoginVariable[] = "MOONWELL_DEV_LOGIN";
constexpr char kLauncherLoginMarker[] = "__MOONWELL_LAUNCHER__";
constexpr wchar_t kLauncherProtocol[] = L"moonwell://authorize?source=client";
constexpr std::array<const wchar_t*, 2> kLauncherExecutables = {
L"MoonWell.exe", L"MoonWellLauncher.exe",
};
constexpr size_t kMaxLaunchAccountLength = 320;
constexpr size_t kLaunchTicketLength = 16;
std::array<char, kMaxLaunchAccountLength + 1> g_launchAccount{};
std::array<char, kLaunchTicketLength + 1> g_launchTicket{};
volatile LONG g_hasLauncherAuth = 0;
bool g_launchedWithTicket = false;
bool g_developerLogin = false;
volatile LONG g_launcherLoginState = 0; // 0=pending, 1=claimed, 2=submitted
DWORD g_launcherAuthCapturedAt = 0;
DWORD g_clientThreadId = 0;
template <size_t Size>
bool ConsumeEnvironmentVariable(const char* name, std::array<char, Size>& output)
{
SetLastError(ERROR_SUCCESS);
const DWORD required = GetEnvironmentVariableA(name, nullptr, 0);
if (!required)
{
SetEnvironmentVariableA(name, nullptr);
return false;
}
if (required > output.size())
{
SetEnvironmentVariableA(name, nullptr);
WLOG_ERROR("moonwell: rejected oversized launcher environment value %s", name);
return false;
}
const DWORD written = GetEnvironmentVariableA(
name, output.data(), static_cast<DWORD>(output.size()));
SetEnvironmentVariableA(name, nullptr);
return written > 0 && written < output.size();
}
template <size_t Size>
bool IsPrintableAscii(const std::array<char, Size>& value)
{
for (const unsigned char character : value)
{
if (character == 0)
return true;
if (character < 0x21 || character > 0x7e)
return false;
}
return false;
}
bool IsValidLauncherTicket()
{
if (std::strlen(g_launchTicket.data()) != kLaunchTicketLength)
return false;
for (size_t index = 0; index < kLaunchTicketLength; ++index)
{
const char character = g_launchTicket[index];
if (!((character >= 'A' && character <= 'Z')
|| (character >= '0' && character <= '9')))
return false;
}
return true;
}
void ClearLauncherAuth()
{
SecureZeroMemory(g_launchAccount.data(), g_launchAccount.size());
SecureZeroMemory(g_launchTicket.data(), g_launchTicket.size());
InterlockedExchange(&g_hasLauncherAuth, 0);
}
void CaptureLaunchEnvironment()
{
const bool hasAccount = ConsumeEnvironmentVariable(
kLaunchAccountVariable, g_launchAccount);
const bool hasTicket = ConsumeEnvironmentVariable(
kLaunchTicketVariable, g_launchTicket);
g_hasLauncherAuth = hasAccount && hasTicket
&& IsPrintableAscii(g_launchAccount)
&& IsValidLauncherTicket() ? 1 : 0;
InterlockedExchange(&g_launcherLoginState, 0);
g_launchedWithTicket = g_hasLauncherAuth != 0;
g_launcherAuthCapturedAt = g_hasLauncherAuth ? GetTickCount() : 0;
if (!g_hasLauncherAuth)
{
if (hasAccount || hasTicket)
WLOG_ERROR("moonwell: incomplete or invalid launcher authorization data");
ClearLauncherAuth();
}
std::array<char, 8> developerValue{};
const bool developerRequested = ConsumeEnvironmentVariable(
kDeveloperLoginVariable, developerValue);
#if defined(_DEBUG)
g_developerLogin = developerRequested
&& std::strcmp(developerValue.data(), "1") == 0;
#else
g_developerLogin = false;
#endif
SecureZeroMemory(developerValue.data(), developerValue.size());
if (g_hasLauncherAuth)
WLOG_INFO("moonwell: launcher authorization data accepted");
else if (g_developerLogin)
WLOG_INFO("moonwell: developer login enabled for this Debug build");
}
int __cdecl GetLaunchMode(void* state)
{
const char* mode = g_launchedWithTicket
? "launcher"
: (g_developerLogin ? "developer" : "locked");
wxl::game::script::PushString(state, mode);
return 1;
}
int __cdecl ConsumeLauncherAuth(void* state)
{
constexpr uintptr_t kLoginGlueReady = 0x00B6B474;
constexpr uintptr_t kLoginServerHost = 0x00B6AF54;
constexpr uintptr_t kLoginServerPort = 0x00B6AF5C;
constexpr uintptr_t kLoginBusy = 0x00B6AA38;
if (!g_hasLauncherAuth
|| !*reinterpret_cast<volatile uint8_t*>(kLoginGlueReady)
|| !*reinterpret_cast<void* volatile*>(kLoginServerHost)
|| !*reinterpret_cast<void* volatile*>(kLoginServerPort)
|| *reinterpret_cast<volatile uint32_t*>(kLoginBusy))
return 0;
if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0)
return 0;
if (!g_hasLauncherAuth)
{
InterlockedExchange(&g_launcherLoginState, 0);
return 0;
}
wxl::game::script::PushString(state, g_launchAccount.data());
wxl::game::script::PushString(state, g_launchTicket.data());
ClearLauncherAuth();
InterlockedExchange(&g_launcherLoginState, 2);
WLOG_INFO("moonwell: launcher authorization consumed by Lua bridge");
return 2;
}
bool TryOpenAdjacentLauncher()
{
std::array<wchar_t, MAX_PATH> path{};
const DWORD length = GetModuleFileNameW(nullptr, path.data(),
static_cast<DWORD>(path.size()));
if (!length || length >= path.size())
return false;
wchar_t* slash = std::wcsrchr(path.data(), L'\\');
if (!slash)
return false;
++slash;
const size_t prefixLength = static_cast<size_t>(slash - path.data());
for (const wchar_t* executable : kLauncherExecutables)
{
const size_t launcherLength = std::wcslen(executable);
if (prefixLength + launcherLength >= path.size())
continue;
std::wmemcpy(slash, executable, launcherLength + 1);
if (GetFileAttributesW(path.data()) == INVALID_FILE_ATTRIBUTES)
continue;
if (reinterpret_cast<INT_PTR>(ShellExecuteW(
nullptr, L"open", path.data(), nullptr, nullptr, SW_SHOWNORMAL)) > 32)
return true;
}
return false;
}
int __cdecl OpenLauncher(void* state)
{
bool opened = TryOpenAdjacentLauncher();
if (!opened)
{
opened = reinterpret_cast<INT_PTR>(ShellExecuteW(
nullptr, L"open", kLauncherProtocol, nullptr, nullptr, SW_SHOWNORMAL)) > 32;
}
wxl::game::script::PushBoolean(state, opened);
return 1;
}
using DefaultServerLoginCallbackFn = int(__cdecl*)(void*);
using BeginServerLoginFn = void(__cdecl*)(const char*, const char*);
DefaultServerLoginCallbackFn g_nextDefaultServerLogin = nullptr;
bool SubmitLauncherAuthorization()
{
if (!g_hasLauncherAuth || g_launcherLoginState != 0)
return false;
constexpr uintptr_t kBeginServerLogin = 0x004D8A30;
constexpr uintptr_t kLoginGlueReady = 0x00B6B474;
constexpr uintptr_t kLoginServerHost = 0x00B6AF54;
constexpr uintptr_t kLoginServerPort = 0x00B6AF5C;
constexpr uintptr_t kLoginBusy = 0x00B6AA38;
if (!*reinterpret_cast<volatile uint8_t*>(kLoginGlueReady)
|| !*reinterpret_cast<void* volatile*>(kLoginServerHost)
|| !*reinterpret_cast<void* volatile*>(kLoginServerPort)
|| *reinterpret_cast<volatile uint32_t*>(kLoginBusy))
return false;
if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0)
return false;
if (!g_hasLauncherAuth)
{
InterlockedExchange(&g_launcherLoginState, 0);
return false;
}
reinterpret_cast<BeginServerLoginFn>(kBeginServerLogin)(
g_launchAccount.data(), g_launchTicket.data());
if (!*reinterpret_cast<volatile uint32_t*>(kLoginBusy))
{
InterlockedExchange(&g_launcherLoginState, 0);
return false;
}
ClearLauncherAuth();
InterlockedExchange(&g_launcherLoginState, 2);
WLOG_INFO("moonwell: launcher authorization submitted to login engine");
return true;
}
int __cdecl DefaultServerLoginHook(void* state)
{
const char* account = wxl::game::script::IsString(state, 1)
? wxl::game::script::ToString(state, 1)
: nullptr;
if (!account || std::strcmp(account, kLauncherLoginMarker) != 0)
return g_nextDefaultServerLogin ? g_nextDefaultServerLogin(state) : 0;
SubmitLauncherAuthorization();
return 0;
}
using GxSetProjectionFn = void(__fastcall*)(void* self, void* edx, const void* projection);
GxSetProjectionFn g_nextSetProjection = nullptr;
// FrameXML's stock SetCreature only accepts a creature entry and waits
@@ -64,10 +362,10 @@ namespace moonwell
int __cdecl SetCreatureDisplayInfoHook(void* state)
{
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0;
if (!state || !wxl::runtime::lua::IsNumber(state, 3))
if (!state || !wxl::game::script::IsNumber(state, 3))
return result;
const double requestedDisplayInfo = wxl::runtime::lua::ToNumber(state, 3);
const double requestedDisplayInfo = wxl::game::script::ToNumber(state, 3);
if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
return result;
@@ -102,8 +400,8 @@ namespace moonwell
void InstallEncounterJournalModelPreview()
{
if (!wxl::core::hook::Install("MoonWellSetCreatureDisplayInfo", kSetCreature,
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
if (!Hook("MoonWellSetCreatureDisplayInfo", kSetCreature,
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
{
WLOG_ERROR("moonwell: encounter journal model hook installation failed");
return;
@@ -151,14 +449,14 @@ namespace moonwell
int __cdecl SetCharacterCreateCamera(void* state)
{
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
const bool enabled = state && wxl::game::script::IsNumber(state, 1)
&& wxl::game::script::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::game::script::IsNumber(state, 2)
? static_cast<float>(wxl::game::script::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3)
? static_cast<float>(wxl::game::script::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::game::script::IsNumber(state, 4)
? wxl::game::script::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
@@ -202,15 +500,16 @@ namespace moonwell
void InstallCharacterCreateCamera()
{
namespace gx = wxl::offsets::engine::gx;
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
void** slot = &vtable[gx::kGxSetProjectionSlot];
constexpr uintptr_t kGxDeviceVTable = 0x00A2E718;
constexpr unsigned kGxSetProjectionSlot = 0xA0 / 4;
void** vtable = reinterpret_cast<void**>(kGxDeviceVTable);
void** slot = &vtable[kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
if (!g_nextSetProjection || !PatchMemory(slot, &replacement, sizeof(replacement)))
{
g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed");
@@ -222,17 +521,17 @@ namespace moonwell
int __cdecl SetLoginCharacterFlags(void* state)
{
uint32_t flags = 0;
if (state && wxl::runtime::lua::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));
if (state && wxl::game::script::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::game::script::ToNumber(state, 1));
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1;
}
int __cdecl IsTraitor(void* state)
{
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1;
}
@@ -259,7 +558,7 @@ namespace moonwell
name, reinterpret_cast<void*>(address));
return false;
}
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(address), replacement.data(), N))
if (!PatchMemory(reinterpret_cast<void*>(address), replacement.data(), N))
{
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address));
@@ -371,7 +670,7 @@ namespace moonwell
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
if (!PatchMemory(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
{
VirtualFree(cave, 0, MEM_RELEASE);
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
@@ -390,40 +689,251 @@ namespace moonwell
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
else
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
wxl::core::log::Flush();
}
DWORD WINAPI FlushRuntimeLog(LPVOID)
using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function);
using ValidateCallbackFn = void(__cdecl*)(uintptr_t);
using GetContextFn = void*(__cdecl*)();
using ExecuteFn = void(__cdecl*)(const char*, uintptr_t, uintptr_t);
using InitializeLuaFn = int(__cdecl*)(void*);
using FramePumpFn = void(__cdecl*)(float, uint32_t);
using FileOpenFn = int(__stdcall*)(void*, const char*, uint32_t, void**);
using GlueModelRenderFn = void(__cdecl*)(void*);
ValidateCallbackFn g_nextValidateCallback = nullptr;
ExecuteFn g_nextExecute = nullptr;
InitializeLuaFn g_nextInitializeLua = nullptr;
FramePumpFn g_nextFramePump = nullptr;
FileOpenFn g_nextFileOpen = nullptr;
GlueModelRenderFn g_nextGlueModelRender = nullptr;
PVOID volatile g_registeredState = nullptr;
bool g_registeringMoonWell = false;
bool IsMoonWellCallback(uintptr_t callback)
{
// Let the remainder of RunAll() and the core-ready line reach the
// shared buffered logger, then make startup diagnostics durable.
Sleep(1000);
wxl::core::log::Flush();
return 0;
return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags)
|| callback == reinterpret_cast<uintptr_t>(&IsTraitor)
|| callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera)
|| callback == reinterpret_cast<uintptr_t>(&GetLaunchMode)
|| callback == reinterpret_cast<uintptr_t>(&ConsumeLauncherAuth)
|| callback == reinterpret_cast<uintptr_t>(&OpenLauncher);
}
void InstallRuntimeLogFlush()
void RegisterLuaFunctionsForCurrentState();
void __cdecl ValidateCallbackHook(uintptr_t callback)
{
HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
if (thread) CloseHandle(thread);
RegisterLuaFunctionsForCurrentState();
if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
g_nextValidateCallback(callback);
}
struct Registration
void RegisterLuaFunctions(void* state)
{
Registration()
if (g_registeringMoonWell) return;
if (!state || state == g_registeredState) return;
constexpr uintptr_t kRegisterFunction = 0x00817F90;
const auto registrar = reinterpret_cast<RegisterFunctionFn>(kRegisterFunction);
g_registeringMoonWell = true;
registrar("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
registrar("MoonWellIsTraitor", &IsTraitor);
registrar("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
registrar("MoonWellGetLaunchMode", &GetLaunchMode);
registrar("MoonWellConsumeLauncherAuth", &ConsumeLauncherAuth);
registrar("MoonWellOpenLauncher", &OpenLauncher);
g_registeringMoonWell = false;
InterlockedExchangePointer(&g_registeredState, state);
WLOG_INFO("moonwell: Lua functions registered for state %p", state);
}
void RegisterLuaFunctionsForCurrentState()
{
constexpr uintptr_t kGetContext = 0x00817DB0;
RegisterLuaFunctions(reinterpret_cast<GetContextFn>(kGetContext)());
}
void RegisterLuaFunctionsBeforeHooksEnable()
{
RegisterLuaFunctionsForCurrentState();
}
void __cdecl OnClientMessage(void*, const void*)
{
RegisterLuaFunctionsForCurrentState();
}
struct WindowSearch
{
DWORD processId;
HWND window;
};
BOOL CALLBACK FindClientWindow(HWND window, LPARAM parameter)
{
auto* search = reinterpret_cast<WindowSearch*>(parameter);
DWORD processId = 0;
GetWindowThreadProcessId(window, &processId);
if (processId != search->processId || !IsWindowVisible(window))
return TRUE;
search->window = window;
return FALSE;
}
DWORD WINAPI ScheduleLuaBootstrap(LPVOID)
{
HWND clientWindow = nullptr;
WLOG_INFO("moonwell: Lua bootstrap scheduler started");
for (unsigned attempt = 0; attempt < 600; ++attempt)
{
wxl::runtime::lua::RegisterFunction(
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
wxl::runtime::lua::RegisterFunction(
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
wxl::runtime::modules::Register(
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
wxl::runtime::modules::Register(
"moonwell-encounter-journal-models", &InstallEncounterJournalModelPreview);
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
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);
}
} g_registration;
WLOG_ERROR("moonwell: timed out waiting for UI-thread Lua bootstrap");
return 1;
}
void __cdecl ExecuteHook(const char* source, uintptr_t argument2, uintptr_t argument3)
{
RegisterLuaFunctionsForCurrentState();
if (g_nextExecute) g_nextExecute(source, argument2, argument3);
}
int __cdecl InitializeLuaHook(void* allocatorContext)
{
const int initialized = g_nextInitializeLua
? g_nextInitializeLua(allocatorContext)
: 0;
if (initialized)
RegisterLuaFunctionsForCurrentState();
return initialized;
}
void __cdecl FramePumpHook(float deltaSeconds, uint32_t frameTimeMs)
{
RegisterLuaFunctionsForCurrentState();
if (g_nextFramePump) g_nextFramePump(deltaSeconds, frameTimeMs);
RegisterLuaFunctionsForCurrentState();
if (g_hasLauncherAuth
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
SubmitLauncherAuthorization();
}
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
{
// The engine-init callback and synchronous Glue loader run on the same client
// thread. Polling here catches Lua becoming live before AccountLogin executes,
// without ever touching the state from background asset I/O workers.
if (GetCurrentThreadId() == g_clientThreadId)
RegisterLuaFunctionsForCurrentState();
const int result = g_nextFileOpen ? g_nextFileOpen(archive, name, flags, out) : 0;
return result;
}
void __cdecl GlueModelRenderHook(void* frame)
{
RegisterLuaFunctionsForCurrentState();
if (g_nextGlueModelRender) g_nextGlueModelRender(frame);
}
void __cdecl OnFrame(void*, const void*)
{
RegisterLuaFunctionsForCurrentState();
// Glue scripting is not a reliable authentication trigger: a broken
// cosmetic widget can abort AccountLogin_OnShow before it reaches the
// login call. Present is emitted on the client thread after the engine
// and Glue subsystem are ready, so submit once after a short grace period.
if (g_hasLauncherAuth
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
SubmitLauncherAuthorization();
}
bool InstallLuaBridge()
{
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
&g_nextValidateCallback);
const bool executor = HookByName("Lua.Execute", &ExecuteHook, &g_nextExecute);
const bool initializer = Hook("Lua.Initialize", 0x00819BB0,
&InitializeLuaHook, &g_nextInitializeLua);
const bool defaultLogin = Hook("MoonWell.DefaultServerLogin", 0x004DC260,
&DefaultServerLoginHook, &g_nextDefaultServerLogin);
const bool framePump = HookByName("Frame.Pump", &FramePumpHook, &g_nextFramePump);
const bool fileOpen = HookByName("Io.FileOpen", &FileOpenHook, &g_nextFileOpen);
const bool glueRender = HookByName("Gx.GlueModelRender", &GlueModelRenderHook,
&g_nextGlueModelRender);
if (!validator || !executor || !initializer || !defaultLogin
|| !framePump || !fileOpen || !glueRender)
WLOG_ERROR("moonwell: Lua bridge hook installation failed");
return validator && executor && initializer && defaultLogin
&& framePump && fileOpen && glueRender;
}
}
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWell", 1, WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
moonwell::g_api = api;
moonwell::g_clientThreadId = GetCurrentThreadId();
moonwell::CaptureLaunchEnvironment();
moonwell::InstallBoot();
const bool lua = moonwell::InstallLuaBridge();
// Event ordinal 5 is wxl::events::Event::OnFrame in API v1. It is emitted
// from Present on the client thread even while only the Glue UI is active.
if (api->Subscribe)
{
api->Subscribe(5, &moonwell::OnFrame, nullptr);
api->Subscribe(17, &moonwell::OnClientMessage, nullptr);
}
moonwell::RegisterLuaFunctionsBeforeHooksEnable();
if (HANDLE bootstrapThread = CreateThread(
nullptr, 0, &moonwell::ScheduleLuaBootstrap, nullptr, 0, nullptr))
CloseHandle(bootstrapThread);
else
api->Log(WXL_LOG_ERROR, "MoonWell", "%s", "failed to start Lua bootstrap scheduler");
moonwell::InstallCharacterCreateCamera();
moonwell::InstallEncounterJournalModelPreview();
const bool spells = moonwell::spells::Install(api);
const bool ready = lua && spells;
api->Log(ready ? WXL_LOG_INFO : WXL_LOG_ERROR, "MoonWell", "%s",
ready ? "MoonWell extension loaded" : "MoonWell extension loaded with errors");
return ready ? 1 : 0;
}
@@ -1,9 +1,9 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Appends compact MoonWell spell overrides to the stock 3.3.5 DBCs at open time.
// Appends compact MoonWell spell overrides to stock 3.3.5 DBC bytes.
#include "Host.hpp"
#include "core/Logger.hpp"
#include "mpq/MpqStore.hpp"
#include "game/Io.hpp"
#include "wxl/PluginApi.h"
#include "wxl/StorageApi.h"
#include <cctype>
#include <charconv>
@@ -15,7 +15,7 @@
#include <system_error>
#include <vector>
namespace moonwell::host
namespace moonwell::spells
{
namespace
{
@@ -38,8 +38,34 @@ namespace moonwell::host
std::once_flag g_loadOnce;
bool g_ready = false;
std::vector<uint8_t> g_spellDbc;
std::vector<uint8_t> g_skillDbc;
const WXL_Api* g_api = nullptr;
std::vector<Override> g_overrides;
void Log(int level, const char* message)
{
if (g_api && g_api->Log) g_api->Log(level, "MoonWellSpells", "%s", message);
}
bool ReadAll(const char* path, std::vector<uint8_t>& out)
{
void* handle = nullptr;
if (!wxl::game::io::FileOpen(path, wxl::game::io::kOpenWholeFile, &handle) || !handle)
return false;
uint32_t high = 0;
const uint32_t size = wxl::game::io::FileSize(handle, &high);
if (high != 0)
{
wxl::game::io::FileClose(handle);
return false;
}
out.resize(size);
uint32_t read = 0;
const bool ok = (size == 0 || wxl::game::io::FileRead(handle, out.data(), size, &read))
&& read == size;
wxl::game::io::FileClose(handle);
if (!ok) out.clear();
return ok;
}
uint32_t ReadU32(const uint8_t* data)
{
@@ -226,39 +252,59 @@ namespace moonwell::host
void Load()
{
const std::string root = wxl::host::ClientRoot();
wxl::host::mpq::MpqStore store;
std::vector<uint8_t> overrideBytes, spellBase, skillBase;
if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes))
std::vector<uint8_t> overrideBytes;
if (!ReadAll(kOverridesPath.data(), overrideBytes))
return;
std::vector<Override> overrides;
if (!ParseOverrides(overrideBytes, overrides) ||
!store.ReadAll(kSpellPath, spellBase) || !store.ReadAll(kSkillPath, skillBase) ||
!BuildSpellDbc(spellBase, overrides, g_spellDbc) ||
!BuildSkillDbc(skillBase, overrides, g_skillDbc))
if (!ParseOverrides(overrideBytes, g_overrides))
{
WLOG_ERROR("moonwell-spells: failed to build custom companion DBCs");
Log(WXL_LOG_ERROR, "failed to parse WXLSpellOverrides.tsv");
return;
}
g_ready = true;
WLOG_INFO("moonwell-spells: appended %zu companion spell(s)", overrides.size());
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_INFO, "MoonWellSpells", "loaded %zu companion spell override(s)",
g_overrides.size());
}
bool Provide(std::string_view name, std::vector<uint8_t>& out)
int __cdecl Transform(const char* rawName, const uint8_t* raw, uint32_t rawLen,
const WXL_ByteSink* sink)
{
if (!rawName || !raw || !sink || !sink->Write) return 0;
const std::string_view name(rawName);
if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath)) return false;
std::call_once(g_loadOnce, &Load);
if (!g_ready) return false;
out = SamePath(name, kSpellPath) ? g_spellDbc : g_skillDbc;
return true;
const std::vector<uint8_t> base(raw, raw + rawLen);
std::vector<uint8_t> out;
const bool built = SamePath(name, kSpellPath)
? BuildSpellDbc(base, g_overrides, out)
: BuildSkillDbc(base, g_overrides, out);
if (!built)
{
Log(WXL_LOG_ERROR, "failed to build custom companion DBC");
return 0;
}
sink->Write(sink->ctx, out.data(), static_cast<uint32_t>(out.size()));
return 1;
}
}
struct Registrar
bool Install(const WXL_Api* api)
{
g_api = api;
if (!api || !api->GetInterface) return false;
auto* storage = static_cast<WXL_StorageApi*>(
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
!storage->RegisterClientTransform)
{
Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); }
};
Registrar g_registrar;
Log(WXL_LOG_ERROR, "wxl.storage v1 is unavailable");
return false;
}
storage->RegisterClientTransform(".dbc", &Transform);
Log(WXL_LOG_INFO, "DBC override transform registered");
return true;
}
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
struct WXL_Api;
namespace moonwell::spells
{
bool Install(const WXL_Api* api);
}
+2
View File
@@ -0,0 +1,2 @@
file(GLOB_RECURSE WXL_EXT_SHARED_SRC CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// WarcraftXL 1.1 BLP transform extension.
#include "engine/assets/shared/textures/blp/BlpTranscode.hpp"
#include "wxl/ModernBlpApi.h"
#include "wxl/PluginApi.h"
#include "wxl/StorageApi.h"
#include <cstdint>
#include <span>
#include <utility>
#include <vector>
namespace
{
const WXL_Api* g_api = nullptr;
constexpr uint32_t kMaxTextureEdge = 1024;
int __cdecl Transcode(const char* name, const uint8_t* raw, uint32_t rawLen,
const WXL_ByteSink* sink)
{
if (!raw || !sink || !sink->Write) return 0;
namespace blp = wxl::modern::assets::textures::blp;
const std::span<const uint8_t> input(raw, rawLen);
std::vector<uint8_t> capped;
const bool didCap = blp::CapBlpMips(input, capped, kMaxTextureEdge);
const std::span<const uint8_t> source = didCap
? std::span<const uint8_t>(capped.data(), capped.size()) : input;
std::vector<uint8_t> transcoded;
if (blp::TranscodeBlp(source, transcoded))
{
sink->Write(sink->ctx, transcoded.data(), static_cast<uint32_t>(transcoded.size()));
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_DEBUG, "wxl-modern-blp", "%s%s BGRA->DXT5",
name ? name : "?", didCap ? " capped" : "");
return 1;
}
if (!didCap) return 0;
sink->Write(sink->ctx, capped.data(), static_cast<uint32_t>(capped.size()));
return 1;
}
const WXL_ModernBlpApi g_blpApi = {
sizeof(WXL_ModernBlpApi), WXL_MODERN_BLP_API_VERSION, &Transcode,
};
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "wxl-modern-blp", 1, WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
g_api = api;
auto* storage = static_cast<WXL_StorageApi*>(
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
!storage->RegisterClientTransform)
{
api->Log(WXL_LOG_ERROR, "wxl-modern-blp", "wxl.storage v1 is unavailable");
return 0;
}
storage->RegisterClientTransform(".blp", &Transcode);
api->PublishInterface("wxl.modern-blp", WXL_MODERN_BLP_API_VERSION,
const_cast<WXL_ModernBlpApi*>(&g_blpApi));
api->Log(WXL_LOG_INFO, "wxl-modern-blp", "BLP transcode and mip cap active");
return 1;
}
+26
View File
@@ -0,0 +1,26 @@
{
"repositoryPatches": [
"Data/patch-4.MPQ",
"Data/patch-Z.MPQ",
"Data/ruRU/patch-ruRU-4.MPQ",
"Data/ruRU/patch-ruRU-5.MPQ",
"Data/ruRU/patch-ruRU-Z.MPQ"
],
"graphicsPatches": [
"Data/patch-X.MPQ",
"Data/ruRU/backup-ruRU.MPQ",
"Data/ruRU/patch-ruRU-6.MPQ",
"Data/ruRU/patch-ruRU-A.mpq",
"Data/ruRU/patch-ruRU-B.mpq",
"Data/ruRU/patch-ruRU-E.mpq",
"Data/ruRU/patch-ruRU-H.MPQ",
"Data/ruRU/patch-ruRU-I.mpq",
"Data/ruRU/patch-ruRU-M.MPQ",
"Data/ruRU/patch-ruRU-S.mpq",
"Data/ruRU/patch-ruRU-T.mpq",
"Data/ruRU/patch-ruRU-U.mpq",
"Data/ruRU/patch-ruRU-W.mpq",
"Data/ruRU/patch-ruRU-X.MPQ",
"Data/ruRU/patch-ruRU-Y.MPQ"
]
}
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import json
from dataclasses import dataclass
from pathlib import Path
def normalized_path(path: str) -> str:
return path.replace("\\", "/").casefold()
@dataclass(frozen=True)
class PatchLayout:
repository_patches: frozenset[str]
graphics_patches: frozenset[str]
def is_repository_patch(self, path: str) -> bool:
return normalized_path(path) in self.repository_patches
def is_graphics_patch(self, path: str) -> bool:
return normalized_path(path) in self.graphics_patches
def is_external_graphics_patch(self, path: str) -> bool:
key = normalized_path(path)
return key in self.graphics_patches and key not in self.repository_patches
def load_patch_layout(path: Path | None = None) -> PatchLayout:
config_path = path or Path(__file__).resolve().with_name("patch-layout.json")
data = json.loads(config_path.read_text(encoding="utf-8"))
repository = frozenset(normalized_path(item) for item in data["repositoryPatches"])
graphics = frozenset(normalized_path(item) for item in data["graphicsPatches"])
return PatchLayout(repository_patches=repository, graphics_patches=graphics)
PATCH_LAYOUT = load_patch_layout()
+254
View File
@@ -0,0 +1,254 @@
$script:MoonWellExtensionNames = @(
'MoonWell',
'wxl-db2',
'wxl-fdid-moonwell',
'wxl-grasswind',
'wxl-modern-adt',
'wxl-modern-blp',
'wxl-modern-m2',
'wxl-modern-wmo',
'wxl-moonwell-storage-fallback',
'wxl-unit-outline'
)
$patchLayoutPath = Join-Path $PSScriptRoot 'patch-layout.json'
if (-not (Test-Path -LiteralPath $patchLayoutPath -PathType Leaf)) {
throw "Patch layout was not found: $patchLayoutPath"
}
$patchLayout = Get-Content -LiteralPath $patchLayoutPath -Raw -Encoding UTF8 | ConvertFrom-Json
$script:MoonWellRepositoryPatches = @($patchLayout.repositoryPatches)
$script:MoonWellGraphicsPatches = @($patchLayout.graphicsPatches)
function Get-MoonWellExtensionNames {
return @($script:MoonWellExtensionNames)
}
function Get-MoonWellRepositoryPatchPaths {
return @($script:MoonWellRepositoryPatches)
}
function Get-MoonWellGraphicsPatchPaths {
return @($script:MoonWellGraphicsPatches)
}
function Test-MoonWellRepositoryPatch {
param([Parameter(Mandatory)][string]$RelativePath)
foreach ($repositoryPatch in $script:MoonWellRepositoryPatches) {
if ($RelativePath.Replace('/', '\').Equals(
$repositoryPatch.Replace('/', '\'),
[System.StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
function Test-MoonWellGraphicsPatch {
param([Parameter(Mandatory)][string]$RelativePath)
foreach ($graphicsPatch in $script:MoonWellGraphicsPatches) {
if ($RelativePath.Replace('/', '\').Equals(
$graphicsPatch.Replace('/', '\'),
[System.StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
function Test-MoonWellPreservedDataPatch {
param([Parameter(Mandatory)][string]$RelativePath)
if (Test-MoonWellRepositoryPatch -RelativePath $RelativePath) {
return $false
}
return Test-MoonWellGraphicsPatch -RelativePath $RelativePath
}
function Assert-SafePackagePath {
param(
[Parameter(Mandatory)]
[string]$PackagePath,
[Parameter(Mandatory)]
[string]$RepositoryPath,
[Parameter()]
[string]$ClientPath
)
$packageRoot = [System.IO.Path]::GetFullPath($PackagePath).TrimEnd('\')
$repositoryRoot = [System.IO.Path]::GetFullPath($RepositoryPath).TrimEnd('\')
$pathRoot = [System.IO.Path]::GetPathRoot($packageRoot).TrimEnd('\')
if (-not $packageRoot -or $packageRoot -eq $pathRoot) {
throw "Refusing to use a filesystem root as package staging: $packageRoot"
}
if ($packageRoot.Equals($repositoryRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
$repositoryRoot.StartsWith($packageRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
throw 'Package staging must not be the repository root or one of its ancestors.'
}
if (-not [string]::IsNullOrWhiteSpace($ClientPath)) {
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
$packageIsClientOrChild =
$packageRoot.Equals($clientRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
$packageRoot.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)
$packageContainsClient =
$clientRoot.StartsWith($packageRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)
if ($packageIsClientOrChild -or $packageContainsClient) {
throw 'Package staging must be separate from the installed client tree.'
}
}
}
function Get-RelativeFilePaths {
param([Parameter(Mandatory)][string]$Root)
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
return @()
}
$absoluteRoot = [System.IO.Path]::GetFullPath($Root).TrimEnd('\')
return @(
Get-ChildItem -LiteralPath $absoluteRoot -Recurse -Force -File |
ForEach-Object { $_.FullName.Substring($absoluteRoot.Length + 1) }
)
}
function Remove-EmptyMoonWellDirectories {
param([Parameter(Mandatory)][string]$Destination)
foreach ($relativeRoot in @('Data\Patch-WXL.MPQ', 'Extensions', 'Utils')) {
$managedRoot = Join-Path $Destination $relativeRoot
if (-not (Test-Path -LiteralPath $managedRoot -PathType Container)) {
continue
}
$directories = @(
Get-ChildItem -LiteralPath $managedRoot -Recurse -Force -Directory |
Sort-Object { $_.FullName.Length } -Descending
)
$directories += Get-Item -LiteralPath $managedRoot
foreach ($directory in $directories) {
if (-not @(Get-ChildItem -LiteralPath $directory.FullName -Force).Count) {
Remove-Item -LiteralPath $directory.FullName -Force
Write-Host "Removed empty managed directory: $($directory.FullName)"
}
}
}
}
function Reset-MoonWellPackageStaging {
param(
[Parameter(Mandatory)][string]$PackagePath,
[Parameter(Mandatory)][string]$RepositoryPath,
[Parameter(Mandatory)][string]$ClientPath
)
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $RepositoryPath `
-ClientPath $ClientPath
if (Test-Path -LiteralPath $PackagePath) {
Remove-Item -LiteralPath $PackagePath -Recurse -Force
}
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
}
function Remove-MoonWellRuntimeGarbage {
param([Parameter(Mandatory)][string]$Destination)
if (-not (Test-Path -LiteralPath $Destination -PathType Container)) {
return
}
$allowedExtensions = @{}
foreach ($name in $script:MoonWellExtensionNames) {
$allowedExtensions[$name.ToLowerInvariant()] = $true
}
$extensionsRoot = Join-Path $Destination 'Extensions'
if (Test-Path -LiteralPath $extensionsRoot -PathType Container) {
foreach ($entry in Get-ChildItem -LiteralPath $extensionsRoot -Force) {
$isAllowedDirectory = $entry.PSIsContainer -and
$allowedExtensions.ContainsKey($entry.Name.ToLowerInvariant())
if (-not $isAllowedDirectory) {
Remove-Item -LiteralPath $entry.FullName -Recurse -Force
Write-Host "Removed unmanaged WarcraftXL extension artifact: $($entry.FullName)"
continue
}
foreach ($dll in Get-ChildItem -LiteralPath $entry.FullName -Force -File -Filter '*.dll') {
$expectedName = "$($entry.Name).dll"
if (-not $dll.Name.Equals($expectedName, [System.StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $dll.FullName -Force
Write-Host "Removed stale extension DLL: $($dll.FullName)"
}
}
}
}
foreach ($dll in Get-ChildItem -LiteralPath $Destination -Force -File -Filter 'WarcraftXL*.dll') {
if (-not $dll.Name.Equals('WarcraftXL.dll', [System.StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $dll.FullName -Force
Write-Host "Removed stale WarcraftXL DLL: $($dll.FullName)"
}
}
$utilsRoot = Join-Path $Destination 'Utils'
if (Test-Path -LiteralPath $utilsRoot -PathType Container) {
foreach ($legacyFile in Get-ChildItem -LiteralPath $utilsRoot -Force -File -Filter 'WarcraftXLHost*') {
Remove-Item -LiteralPath $legacyFile.FullName -Force
Write-Host "Removed obsolete WarcraftXL 1.0 host artifact: $($legacyFile.FullName)"
}
}
# 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
}
function Remove-StalePackageFilesFromClient {
param(
[Parameter(Mandatory)][string]$ClientPath,
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$PreviousFiles,
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$CurrentFiles
)
$current = @{}
foreach ($relativePath in $CurrentFiles) {
$current[$relativePath.ToLowerInvariant()] = $true
}
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
foreach ($relativePath in $PreviousFiles) {
if ($current.ContainsKey($relativePath.ToLowerInvariant()) -or
(Test-MoonWellPreservedDataPatch -RelativePath $relativePath)) {
continue
}
$target = [System.IO.Path]::GetFullPath((Join-Path $clientRoot $relativePath))
if (-not $target.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Package inventory contains a path outside the client: $relativePath"
}
if (Test-Path -LiteralPath $target -PathType Leaf) {
Remove-Item -LiteralPath $target -Force
Write-Host "Removed stale packaged file from client: $target"
}
}
Remove-EmptyMoonWellDirectories -Destination $ClientPath
}
+1
View File
@@ -0,0 +1 @@
dvc[s3]==3.67.1
+52 -7
View File
@@ -1,7 +1,8 @@
param(
[Parameter(Mandatory=$false)]
[ValidateSet("production", "ptr", "local")]
[string]$Env = "local"
[string]$Env = "local",
[switch]$DeveloperLogin
)
# Stop on errors
@@ -9,6 +10,7 @@ $ErrorActionPreference = "Stop"
# --- Resolve project root (folder where script is located)
$ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
. (Join-Path $ROOT 'pipeline-layout.ps1')
# --- Load .env if WOW_HOME not already set
if (-not $env:WOW_HOME) {
@@ -67,9 +69,16 @@ if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and
-not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and
-not $isAdministrator) {
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 `
-WorkingDirectory $ROOT `
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
-ArgumentList $elevationArguments
exit $elevatedProcess.ExitCode
}
@@ -80,11 +89,16 @@ $TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
Assert-SafePackagePath `
-PackagePath $DIST_DIR `
-RepositoryPath $ROOT `
-ClientPath $WOW_HOME
function Stop-WowRuntime {
param([Parameter(Mandatory=$true)][string]$ClientPath)
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') + '\'
$runtimeProcesses = @(Get-Process -Name "Wow", "WarcraftXLHost" -ErrorAction SilentlyContinue | Where-Object {
$runtimeProcesses = @(Get-Process -Name "Wow" -ErrorAction SilentlyContinue | Where-Object {
try {
$_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase)
} catch {
@@ -96,7 +110,7 @@ function Stop-WowRuntime {
return
}
Write-Host "Stopping the running WoW client and WarcraftXL host..."
Write-Host "Stopping the running WoW client..."
$runtimeProcesses | Stop-Process -Force
try {
@@ -118,7 +132,14 @@ if (!(Test-Path $TOOL)) {
Pop-Location
}
# --- Build MPQ archives into dist/
# --- Build MPQ archives into a clean dist/
$previousPackageFiles = @(
Get-RelativeFilePaths -Root $DIST_DIR
)
Reset-MoonWellPackageStaging `
-PackagePath $DIST_DIR `
-RepositoryPath $ROOT `
-ClientPath $WOW_HOME
Write-Host "Building MPQ archives from src/ -> dist/..."
& $TOOL $SRC_DIR $DIST_DIR
if ($LASTEXITCODE -ne 0) {
@@ -136,7 +157,8 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
Stop-WowRuntime -ClientPath $WOW_HOME
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
if ($LASTEXITCODE -ne 0) {
Write-Error "WarcraftXL build/deploy failed!"
@@ -145,6 +167,22 @@ if ($LASTEXITCODE -ne 0) {
# --- Sync dist/ -> WOW_HOME
Write-Host "Syncing dist/ -> WOW_HOME..."
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) {
Write-Error "Python is required for package validation."
exit 1
}
& $python.Source (Join-Path $ROOT 'validate_package.py') --dir $DIST_DIR
if ($LASTEXITCODE -ne 0) {
Write-Error "Package validation failed!"
exit 1
}
$currentPackageFiles = @(Get-RelativeFilePaths -Root $DIST_DIR)
Remove-StalePackageFilesFromClient `
-ClientPath $WOW_HOME `
-PreviousFiles $previousPackageFiles `
-CurrentFiles $currentPackageFiles
Remove-MoonWellRuntimeGarbage -Destination $WOW_HOME
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
# robocopy exit codes 0-7 are success/warnings; 8+ are errors
if ($LASTEXITCODE -ge 8) {
@@ -171,4 +209,11 @@ if ($realmlist) {
# --- Run WoW reload script
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
}
Binary file not shown.
@@ -1,95 +0,0 @@
{
"fileDataID": 3730958,
"textures": [
{
"fileDataID": 3732460,
"file": "catslime_3732460.blp"
},
{
"fileDataID": 3732461,
"file": "catslime_3732461.blp"
}
],
"skins": [
{
"fileDataID": 3732472,
"file": "catslime00.skin"
}
],
"lodSkins": [
{
"fileDataID": 3732473,
"file": "catslime_lod01.skin"
},
{
"fileDataID": 3732474,
"file": "catslime_lod02.skin"
},
{
"fileDataID": 3732475,
"file": "catslime_lod03.skin"
}
],
"anims": [
{
"fileDataID": 3732462,
"file": "catslime0097-00.anim",
"animID": 97,
"subAnimID": 0
},
{
"fileDataID": 3732463,
"file": "catslime0096-00.anim",
"animID": 96,
"subAnimID": 0
},
{
"fileDataID": 3732464,
"file": "catslime0098-00.anim",
"animID": 98,
"subAnimID": 0
},
{
"fileDataID": 3732465,
"file": "catslime0100-00.anim",
"animID": 100,
"subAnimID": 0
},
{
"fileDataID": 3732466,
"file": "catslime0099-00.anim",
"animID": 99,
"subAnimID": 0
},
{
"fileDataID": 3732467,
"file": "catslime0101-00.anim",
"animID": 101,
"subAnimID": 0
},
{
"fileDataID": 3732468,
"file": "catslime0069-00.anim",
"animID": 69,
"subAnimID": 0
},
{
"fileDataID": 3732469,
"file": "catslime0123-00.anim",
"animID": 123,
"subAnimID": 0
},
{
"fileDataID": 3732470,
"file": "catslime0061-00.anim",
"animID": 61,
"subAnimID": 0
},
{
"fileDataID": 3732471,
"file": "catslime0075-00.anim",
"animID": 75,
"subAnimID": 0
}
]
}
Binary file not shown.
@@ -43,7 +43,8 @@ local Config = {
}
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 = {
@@ -51,7 +52,7 @@ local ModelManager = {
}
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 = {
@@ -78,6 +79,8 @@ local function InitializeUICache()
UICache.savePassword = _G["AccountLoginSavePassword"]
UICache.autoLogin = _G["AccountLoginAutoLogin"]
UICache.autoLoginText = _G["AccountLoginAutoLoginText"]
UICache.saveOptionsFrame = _G["AccountLoginSaveOptionsFrame"]
UICache.launcherHint = _G["AccountLoginLauncherHint"]
UICache.loginButton = _G["AccountLoginLoginButton"]
UICache.versionText = _G["AccountLoginVersion"]
UICache.realmName = _G["AccountLoginRealmName"]
@@ -94,6 +97,106 @@ end
function GetLoginState()
return LoginState
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
-- ============================================================================
@@ -144,6 +247,11 @@ GlueDialogTypes["AUTO_LOGIN"] = {
UICache.autoLogin:SetChecked(0)
end,
}
GlueDialogTypes["MOONWELL_LAUNCHER_NOT_FOUND"] = {
text = "Не удалось открыть лаунчер MoonWell. Запустите его вручную и нажмите «Играть».",
button1 = OKAY,
}
-- ============================================================================
-- GESTIÓN DE MODELOS 3D
-- ============================================================================
@@ -151,10 +259,8 @@ local function CreateLoginModel(parent, modelData)
local model = CreateFrame("Model", nil, parent)
local width, height = parent:GetSize()
model:SetSize(
width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1),
height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1)
)
model:SetWidth(width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
model:SetHeight(height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
model:SetPoint("CENTER")
model:SetModel("Character/Human/Male/HumanMale.mdx")
model:SetCamera(1)
@@ -194,6 +300,13 @@ end
-- ACTUALIZACIÓN DE LA ESCENA
-- ============================================================================
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
PlayGlueAmbience(Config.LOGIN_AMBIENCE, Config.AMBIENCE_FADE_TIME)
self.ambiencePlayed = true
@@ -283,8 +396,8 @@ function AccountLogin_OnLoad(self)
model:SetModel("Character/Human/Male/HumanMale.mdx")
model:SetPoint("CENTER", 0, 0)
model:SetSize(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1),
self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
model:SetWidth(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
model:SetHeight(self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
model:SetCamera(1)
if data[LOGIN_MODEL_STRUCT.LIGHT] then
@@ -312,6 +425,14 @@ function AccountLogin_OnLoad(self)
end
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:SetAlpha(1)
WorldOfWarcraftRating:Hide()
@@ -341,12 +462,10 @@ function AccountLogin_OnShow(self)
elseif self.animatedTexture then
self.animatedTexture:Hide()
end
AccountLoginUI:Show()
AccountLoginUI:SetAlpha(1)
if not self.newLogo then
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.newLogo = self.newLogoFrame:CreateTexture("AccountLoginNewLogo", "OVERLAY")
@@ -376,7 +495,14 @@ function AccountLogin_OnShow(self)
self.modelUpdateCount = 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.passwordEdit:SetText(password or "")
@@ -390,13 +516,15 @@ function AccountLogin_OnShow(self)
UICache.realmName:SetText("No Recent Server")
end
if accountName == "" then
AccountLogin_FocusAccountName()
else
AccountLogin_FocusPassword()
if LoginState.launchMode == "developer" then
if accountName == "" then
AccountLogin_FocusAccountName()
else
AccountLogin_FocusPassword()
end
end
if UICache.savePassword:GetChecked() then
if LoginState.launchMode == "developer" and UICache.savePassword:GetChecked() then
UICache.autoLoginText:Show()
UICache.autoLogin:Show()
else
@@ -415,7 +543,11 @@ function AccountLogin_OnShow(self)
ACCOUNT_MSG_BODY_LOADED = false
ACCOUNT_MSG_CURRENT_INDEX = nil
ApplyLaunchMode()
AccountLogin_CheckAutoLogin()
if not TryLauncherLogin() then
TryLauncherFallbackLogin()
end
self:SetScript("OnUpdate", LoginScene_OnUpdate)
end
@@ -450,11 +582,15 @@ function AccountLogin_OnHide(self)
end
function AccountLogin_FocusPassword()
UICache.passwordEdit:SetFocus()
if LoginState.launchMode == "developer" then
UICache.passwordEdit:SetFocus()
end
end
function AccountLogin_FocusAccountName()
UICache.accountEdit:SetFocus()
if LoginState.launchMode == "developer" then
UICache.accountEdit:SetFocus()
end
end
function AccountLogin_OnKeyDown(key)
@@ -462,7 +598,7 @@ function AccountLogin_OnKeyDown(key)
if ConnectionHelpFrame:IsShown() then
ConnectionHelpFrame:Hide()
AccountLoginUI:Show()
elseif SurveyNotificationFrame:IsShown() then
elseif SurveyNotificationFrame and SurveyNotificationFrame:IsShown() then
else
AccountLogin_Exit()
end
@@ -471,10 +607,10 @@ function AccountLogin_OnKeyDown(key)
return
elseif TOSFrame:IsShown() or ConnectionHelpFrame:IsShown() then
return
elseif SurveyNotificationFrame:IsShown() then
elseif SurveyNotificationFrame and SurveyNotificationFrame:IsShown() then
AccountLogin_SurveyNotificationDone(1)
end
AccountLogin_Login()
AccountLogin_PrimaryAction()
elseif key == "PRINTSCREEN" then
Screenshot()
end
@@ -526,6 +662,11 @@ end
-- SISTEMA DE LOGIN CON GUARDADO DE CONTRASEÑA
-- ============================================================================
function AccountLogin_Login()
if LoginState.launchMode ~= "developer" then
AccountLogin_OpenLauncher()
return
end
PlaySound("gsLogin")
local accountName = UICache.accountEdit:GetText()
local password = UICache.passwordEdit:GetText()
@@ -557,6 +698,11 @@ end
-- SISTEMA DE AUTOLOGIN
-- ============================================================================
function AccountLogin_CheckAutoLogin()
if LoginState.launchMode ~= "developer" then
LoginState.autoLoginTimer = nil
return
end
if not LoginState.autoLoginAttempted then
LoginState.autoLoginAttempted = true
local savedAccountInfo = GetSavedAccountName()
@@ -811,6 +957,12 @@ function AccountLogin_Exit()
end
function AccountLogin_ShowSurveyNotification()
if not SurveyNotificationFrame or
not SurveyNotificationAccept or
not SurveyNotificationDecline then
return
end
GlueDialog:Hide()
AccountLoginUI:Hide()
SurveyNotificationAccept:Enable()
@@ -819,11 +971,19 @@ function AccountLogin_ShowSurveyNotification()
end
function AccountLogin_SurveyNotificationDone(accepted)
SurveyNotificationFrame:Hide()
SurveyNotificationAccept:Disable()
SurveyNotificationDecline:Disable()
SurveyNotificationDone(accepted)
AccountLoginUI:Show()
if SurveyNotificationFrame then
SurveyNotificationFrame:Hide()
end
if SurveyNotificationAccept then
SurveyNotificationAccept:Disable()
end
if SurveyNotificationDecline then
SurveyNotificationDecline:Disable()
end
if type(SurveyNotificationDone) == "function" then
SurveyNotificationDone(accepted)
end
ApplyLaunchMode()
end
function AccountLogin_ShowUserAgreements()
@@ -115,6 +115,18 @@
</Anchor>
</Anchors>
</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">
<Size>
<AbsDimension x="128" y="128"/>
@@ -195,7 +207,7 @@
<!-- ====================================== -->
<!-- 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"/>
<Anchors>
<Anchor point="BOTTOM">
@@ -246,7 +258,7 @@
</Scripts>
</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"/>
<Anchors>
<Anchor point="BOTTOM">
@@ -299,7 +311,7 @@
<!-- ====================================== -->
<!-- CHECKBOXES: GUARDAR CUENTA Y CONTRASEÑA -->
<!-- ====================================== -->
<Frame name="AccountLoginSaveOptionsFrame">
<Frame name="AccountLoginSaveOptionsFrame" hidden="true">
<Size x="200" y="60"/>
<Anchors>
<Anchor point="TOP" relativeTo="AccountLoginPasswordEdit" relativePoint="BOTTOM">
@@ -883,7 +895,7 @@
<!-- ====================================== -->
<!-- BOTÓN DE LOGIN -->
<!-- ====================================== -->
<Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="LOGIN">
<Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="Авторизоваться">
<Size x="200" y="55"/>
<Anchors>
<Anchor point="BOTTOM">
@@ -905,7 +917,7 @@
</Layers>
<Scripts>
<OnClick>
AccountLogin_Login();
AccountLogin_PrimaryAction();
</OnClick>
</Scripts>
</Button>
@@ -4,6 +4,7 @@
## Author: Rochet2 <https://github.com/Rochet2>
## SavedVariables: AIO_sv, AIO_sv_Addons
## SavedVariablesPerCharacter: AIO_sv_char
## X-Hidden: 1
#dependencies
Dep_LibWindow-1.1\LibStub.lua
@@ -0,0 +1,110 @@
-- This file is only there in standalone Ace3 and provides handy dev tool stuff I guess
-- for now only /rl to reload your UI :)
-- note the complete overkill use of AceAddon and console, ain't it cool?
-- GLOBALS: next, loadstring, ReloadUI, geterrorhandler
-- GLOBALS: BINDING_HEADER_ACE3, BINDING_NAME_RELOADUI, Ace3, LibStub
-- BINDINGs labels
BINDING_HEADER_ACE3 = "Ace3"
BINDING_NAME_RELOADUI = "ReloadUI"
--
local gui = LibStub("AceGUI-3.0")
local reg = LibStub("AceConfigRegistry-3.0")
local dialog = LibStub("AceConfigDialog-3.0")
Ace3 = LibStub("AceAddon-3.0"):NewAddon("Ace3", "AceConsole-3.0")
local Ace3 = Ace3
local selectedgroup
local frame
local select
local status = {}
local configs = {}
local function frameOnClose()
gui:Release(frame)
frame = nil
end
local function RefreshConfigs()
for name in reg:IterateOptionsTables() do
configs[name] = name
end
end
local function ConfigSelected(widget, event, value)
selectedgroup = value
dialog:Open(value, widget)
end
local old_CloseSpecialWindows
-- GLOBALS: CloseSpecialWindows, next
function Ace3:Open()
if not old_CloseSpecialWindows then
old_CloseSpecialWindows = CloseSpecialWindows
CloseSpecialWindows = function()
local found = old_CloseSpecialWindows()
if frame then
frame:Hide()
return true
end
return found
end
end
RefreshConfigs()
if next(configs) == nil then
self:Print("No Configs are Registered")
return
end
if not frame then
frame = gui:Create("Frame")
frame:ReleaseChildren()
frame:SetTitle("Ace3 Options")
frame:SetLayout("FILL")
frame:SetCallback("OnClose", frameOnClose)
select = gui:Create("DropdownGroup")
select:SetGroupList(configs)
select:SetCallback("OnGroupSelected", ConfigSelected)
frame:AddChild(select)
end
if not selectedgroup then
selectedgroup = next(configs)
end
select:SetGroup(selectedgroup)
frame:Show()
end
local function RefreshOnUpdate(this)
select:SetGroup(selectedgroup)
this:SetScript("OnUpdate", nil)
end
function Ace3:ConfigTableChanged(event, appName)
if selectedgroup == appName and frame then
frame.frame:SetScript("OnUpdate", RefreshOnUpdate)
end
end
reg.RegisterCallback(Ace3, "ConfigTableChange", "ConfigTableChanged")
function Ace3:PrintCmd(input)
input = input:trim():match("^(.-);*$")
local func, err = loadstring("LibStub(\"AceConsole-3.0\"):Print(" .. input .. ")")
if not func then
LibStub("AceConsole-3.0"):Print("Error: " .. err)
else
func()
end
end
function Ace3:OnInitialize()
self:RegisterChatCommand("ace3", function() self:Open() end)
self:RegisterChatCommand("rl", function() ReloadUI() end)
self:RegisterChatCommand("print", "PrintCmd")
end
@@ -0,0 +1,30 @@
## Interface: 30300
## X-Curse-Packaged-Version: r922
## X-Curse-Project-Name: Ace3
## X-Curse-Project-ID: ace3
## X-Curse-Repository-ID: wow/ace3/mainline
## Title: Lib: Ace3
## Notes: AddOn development framework
## Author: Ace3 Development Team
## Dependencies: LibStub, CallbackHandler-1.0
## X-Website: http://www.wowace.com
## X-Category: Library
## X-License: Limited BSD
AceAddon-3.0\AceAddon-3.0.xml
AceEvent-3.0\AceEvent-3.0.xml
AceTimer-3.0\AceTimer-3.0.xml
AceBucket-3.0\AceBucket-3.0.xml
AceHook-3.0\AceHook-3.0.xml
AceDB-3.0\AceDB-3.0.xml
AceDBOptions-3.0\AceDBOptions-3.0.xml
AceLocale-3.0\AceLocale-3.0.xml
AceConsole-3.0\AceConsole-3.0.xml
AceGUI-3.0\AceGUI-3.0.xml
AceConfig-3.0\AceConfig-3.0.xml
AceComm-3.0\AceComm-3.0.xml
AceTab-3.0\AceTab-3.0.xml
AceSerializer-3.0\AceSerializer-3.0.xml
Ace3.lua
@@ -3,7 +3,7 @@
-- as well as associate it with a slash command.
-- @class file
-- @name AceConfig-3.0
-- @release $Id: AceConfig-3.0.lua 969 2010-10-07 02:11:48Z shefki $
-- @release $Id: AceConfig-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $
--[[
AceConfig-3.0
@@ -19,8 +19,8 @@ if not AceConfig then return end
local cfgreg = LibStub("AceConfigRegistry-3.0")
local cfgcmd = LibStub("AceConfigCmd-3.0")
--TODO: local cfgdlg = LibStub("AceConfigDialog-3.0", true)
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0", true)
local cfgdlg = LibStub("AceConfigDialog-3.0")
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0")
-- Lua APIs
local pcall, error, type, pairs = pcall, error, type, pairs
@@ -36,7 +36,7 @@ local pcall, error, type, pairs = pcall, error, type, pairs
-- You can supply a slash command (or a table of slash commands) to register with AceConfigCmd directly.
-- @paramsig appName, options [, slashcmd]
-- @param appName The application name for the config table.
-- @param options The option table (or a function to generate one on demand). http://www.wowace.com/addons/ace3/pages/ace-config-3-0-options-tables/
-- @param options The option table (or a function to generate one on demand)
-- @param slashcmd A slash command to register for the option table, or a table of slash commands.
-- @usage
-- local AceConfig = LibStub("AceConfig-3.0")
@@ -1,10 +1,10 @@
--- AceConfigDialog-3.0 generates AceGUI-3.0 based windows based on option tables.
-- @class file
-- @name AceConfigDialog-3.0
-- @release $Id: AceConfigDialog-3.0.lua 967 2010-09-25 08:20:55Z nevcairiel $
-- @release $Id: AceConfigDialog-3.0.lua 921 2010-05-09 15:49:14Z nevcairiel $
local LibStub = LibStub
local MAJOR, MINOR = "AceConfigDialog-3.0", 50
local MAJOR, MINOR = "AceConfigDialog-3.0", 47
local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceConfigDialog then return end
@@ -15,7 +15,6 @@ AceConfigDialog.frame = AceConfigDialog.frame or CreateFrame("Frame")
AceConfigDialog.frame.apps = AceConfigDialog.frame.apps or {}
AceConfigDialog.frame.closing = AceConfigDialog.frame.closing or {}
AceConfigDialog.frame.closeAllOverride = AceConfigDialog.frame.closeAllOverride or {}
local gui = LibStub("AceGUI-3.0")
local reg = LibStub("AceConfigRegistry-3.0")
@@ -24,8 +23,8 @@ local reg = LibStub("AceConfigRegistry-3.0")
local tconcat, tinsert, tsort, tremove = table.concat, table.insert, table.sort, table.remove
local strmatch, format = string.match, string.format
local assert, loadstring, error = assert, loadstring, error
local pairs, next, select, type, unpack, wipe = pairs, next, select, type, unpack, wipe
local rawset, tostring, tonumber = rawset, tostring, tonumber
local pairs, next, select, type, unpack = pairs, next, select, type, unpack
local rawset, tostring = rawset, tostring
local math_min, math_max, math_floor = math.min, math.max, math.floor
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
@@ -77,7 +76,7 @@ Dispatchers[0] = function(func)
end
local function safecall(func, ...)
return Dispatchers[select("#", ...)](func, ...)
return Dispatchers[select('#', ...)](func, ...)
end
local width_multiplier = 170
@@ -104,7 +103,7 @@ Group Types
local new, del, copy
--newcount, delcount,createdcount,cached = 0,0,0
do
local pool = setmetatable({},{__mode="k"})
local pool = setmetatable({},{__mode='k'})
function new()
--newcount = newcount + 1
local t = next(pool)
@@ -241,7 +240,7 @@ local function GetOptionsMemberValue(membername, option, options, path, appName,
info.handler = handler
info.option = option
info.type = option.type
info.uiType = "dialog"
info.uiType = 'dialog'
info.uiName = MAJOR
local a, b, c ,d
@@ -322,8 +321,8 @@ local function compareOptions(a,b)
end
local OrderA, OrderB = tempOrders[a] or 100, tempOrders[b] or 100
if OrderA == OrderB then
local NameA = (type(tempNames[a]) == "string") and tempNames[a] or ""
local NameB = (type(tempNames[b]) == "string") and tempNames[b] or ""
local NameA = (type(tempNames[a] == "string") and tempNames[a]) or ""
local NameB = (type(tempNames[b] == "string") and tempNames[b]) or ""
return NameA:upper() < NameB:upper()
end
if OrderA < 0 then
@@ -479,7 +478,7 @@ function AceConfigDialog:SelectGroup(appName, ...)
local treevalue
local treestatus
for n = 1, select("#",...) do
for n = 1, select('#',...) do
local key = select(n, ...)
if group.childGroups == "tab" or group.childGroups == "select" then
@@ -543,7 +542,7 @@ local function OptionOnMouseOver(widget, event)
GameTooltip:SetText(name, 1, .82, 0, 1)
if opt.type == "multiselect" then
if opt.type == 'multiselect' then
GameTooltip:AddLine(user.text,0.5, 0.5, 0.8, 1)
end
if type(desc) == "string" then
@@ -562,10 +561,10 @@ end
local function GetFuncName(option)
local type = option.type
if type == "execute" then
return "func"
if type == 'execute' then
return 'func'
else
return "set"
return 'set'
end
end
local function confirmPopup(appName, rootframe, basepath, info, message, func, ...)
@@ -595,7 +594,7 @@ local function confirmPopup(appName, rootframe, basepath, info, message, func, .
AceConfigDialog:Open(appName, rootframe, unpack(basepath or emptyTbl))
del(info)
end
for i = 1, select("#", ...) do
for i = 1, select('#', ...) do
t[i] = select(i, ...) or false
end
t.timeout = 0
@@ -654,7 +653,7 @@ local function ActivateControl(widget, event, ...)
info.handler = handler
info.option = option
info.type = option.type
info.uiType = "dialog"
info.uiType = 'dialog'
info.uiName = MAJOR
local name
@@ -765,13 +764,13 @@ local function ActivateControl(widget, event, ...)
end
end
local iscustom = user.rootframe:GetUserData("iscustom")
local iscustom = user.rootframe:GetUserData('iscustom')
local rootframe
if iscustom then
rootframe = user.rootframe
end
local basepath = user.rootframe:GetUserData("basepath")
local basepath = user.rootframe:GetUserData('basepath')
if type(func) == "string" then
if handler and handler[func] then
confirmPopup(user.appName, rootframe, basepath, info, confirmText, handler[func], handler, info, ...)
@@ -799,8 +798,8 @@ local function ActivateControl(widget, event, ...)
local iscustom = user.rootframe:GetUserData("iscustom")
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl
local iscustom = user.rootframe:GetUserData('iscustom')
local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
--full refresh of the frame, some controls dont cause this on all events
if option.type == "color" then
if event == "OnValueConfirmed" then
@@ -835,16 +834,13 @@ local function ActivateControl(widget, event, ...)
end
local function ActivateSlider(widget, event, value)
local option = widget:GetUserData("option")
local option = widget:GetUserData('option')
local min, max, step = option.min or (not option.softMin and 0 or nil), option.max or (not option.softMax and 100 or nil), option.step
if min then
if min and max then
if step then
value = math_floor((value - min) / step + 0.5) * step + min
end
value = math_max(value, min)
end
if max then
value = math_min(value, max)
value = math_max(math_min(value,max),min)
end
ActivateControl(widget,event,value)
end
@@ -852,10 +848,10 @@ end
--called from a checkbox that is part of an internally created multiselect group
--this type is safe to refresh on activation of one control
local function ActivateMultiControl(widget, event, ...)
ActivateControl(widget, event, widget:GetUserData("value"), ...)
ActivateControl(widget, event, widget:GetUserData('value'), ...)
local user = widget:GetUserDataTable()
local iscustom = user.rootframe:GetUserData("iscustom")
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl
local iscustom = user.rootframe:GetUserData('iscustom')
local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
if iscustom then
AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath))
else
@@ -866,8 +862,8 @@ end
local function MultiControlOnClosed(widget, event, ...)
local user = widget:GetUserDataTable()
if user.valuechanged then
local iscustom = user.rootframe:GetUserData("iscustom")
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl
local iscustom = user.rootframe:GetUserData('iscustom')
local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
if iscustom then
AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath))
else
@@ -877,7 +873,7 @@ local function MultiControlOnClosed(widget, event, ...)
end
local function FrameOnClose(widget, event)
local appName = widget:GetUserData("appName")
local appName = widget:GetUserData('appName')
AceConfigDialog.OpenFrames[appName] = nil
gui:Release(widget)
end
@@ -1088,7 +1084,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
local image, width, height = GetOptionsMemberValue("image",v, options, path, appName)
if type(image) == "string" then
if type(image) == 'string' then
control = gui:Create("Icon")
if not width then
width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
@@ -1096,7 +1092,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
if not height then
height = GetOptionsMemberValue("imageHeight",v, options, path, appName)
end
if type(imageCoords) == "table" then
if type(imageCoords) == 'table' then
control:SetImage(image, unpack(imageCoords))
else
control:SetImage(image)
@@ -1150,8 +1146,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
local image = GetOptionsMemberValue("image", v, options, path, appName)
local imageCoords = GetOptionsMemberValue("imageCoords", v, options, path, appName)
if type(image) == "string" then
if type(imageCoords) == "table" then
if type(image) == 'string' then
if type(imageCoords) == 'table' then
control:SetImage(image, unpack(imageCoords))
else
control:SetImage(image)
@@ -1243,8 +1239,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
local text = values[value]
local check = gui:Create("CheckBox")
check:SetLabel(text)
check:SetUserData("value", value)
check:SetUserData("text", text)
check:SetUserData('value', value)
check:SetUserData('text', text)
check:SetDisabled(disabled)
check:SetTriState(v.tristate)
check:SetValue(GetOptionsMemberValue("get",v, options, path, appName, value))
@@ -1304,14 +1300,14 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
local image, width, height = GetOptionsMemberValue("image",v, options, path, appName)
if type(image) == "string" then
if type(image) == 'string' then
if not width then
width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
end
if not height then
height = GetOptionsMemberValue("imageHeight",v, options, path, appName)
end
if type(imageCoords) == "table" then
if type(imageCoords) == 'table' then
control:SetImage(image, unpack(imageCoords))
else
control:SetImage(image)
@@ -1362,7 +1358,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
end
local function BuildPath(path, ...)
for i = 1, select("#",...) do
for i = 1, select('#',...) do
tinsert(path, (select(i,...)))
end
end
@@ -1665,12 +1661,9 @@ local function RefreshOnUpdate(this)
if this.closeAll then
for k, v in pairs(AceConfigDialog.OpenFrames) do
if not this.closeAllOverride[k] then
v:Hide()
end
v:Hide()
end
this.closeAll = nil
wipe(this.closeAllOverride)
end
for appName in pairs(this.apps) do
@@ -1682,7 +1675,7 @@ local function RefreshOnUpdate(this)
for key, widget in pairs(AceConfigDialog.BlizOptions[appName]) do
local user = widget:GetUserDataTable()
if widget:IsVisible() then
AceConfigDialog:Open(widget:GetUserData("appName"), widget, unpack(user.basepath or emptyTbl))
AceConfigDialog:Open(widget:GetUserData('appName'), widget, unpack(user.basepath or emptyTbl))
end
end
end
@@ -1767,7 +1760,7 @@ function AceConfigDialog:Open(appName, container, ...)
tinsert(path, container)
container = nil
end
for n = 1, select("#",...) do
for n = 1, select('#',...) do
tinsert(path, (select(n, ...)))
end
@@ -1775,10 +1768,10 @@ function AceConfigDialog:Open(appName, container, ...)
if container then
f = container
f:ReleaseChildren()
f:SetUserData("appName", appName)
f:SetUserData("iscustom", true)
f:SetUserData('appName', appName)
f:SetUserData('iscustom', true)
if #path > 0 then
f:SetUserData("basepath", copy(path))
f:SetUserData('basepath', copy(path))
end
local status = AceConfigDialog:GetStatusTable(appName)
if not status.width then
@@ -1802,9 +1795,9 @@ function AceConfigDialog:Open(appName, container, ...)
end
f:ReleaseChildren()
f:SetCallback("OnClose", FrameOnClose)
f:SetUserData("appName", appName)
f:SetUserData('appName', appName)
if #path > 0 then
f:SetUserData("basepath", copy(path))
f:SetUserData('basepath', copy(path))
end
f:SetTitle(name or "")
local status = AceConfigDialog:GetStatusTable(appName)
@@ -1816,11 +1809,6 @@ function AceConfigDialog:Open(appName, container, ...)
f:Show()
end
del(path)
if AceConfigDialog.frame.closeAll then
-- close all is set, but thats not good, since we're just opening here, so force it
AceConfigDialog.frame.closeAllOverride[appName] = true
end
end
-- convert pre-39 BlizOptions structure to the new format
@@ -1828,7 +1816,7 @@ if oldminor and oldminor < 39 and AceConfigDialog.BlizOptions then
local old = AceConfigDialog.BlizOptions
local new = {}
for key, widget in pairs(old) do
local appName = widget:GetUserData("appName")
local appName = widget:GetUserData('appName')
if not new[appName] then new[appName] = {} end
new[appName][key] = widget
end
@@ -1838,12 +1826,12 @@ else
end
local function FeedToBlizPanel(widget, event)
local path = widget:GetUserData("path")
AceConfigDialog:Open(widget:GetUserData("appName"), widget, unpack(path or emptyTbl))
local path = widget:GetUserData('path')
AceConfigDialog:Open(widget:GetUserData('appName'), widget, unpack(path or emptyTbl))
end
local function ClearBlizPanel(widget, event)
local appName = widget:GetUserData("appName")
local appName = widget:GetUserData('appName')
AceConfigDialog.frame.closing[appName] = true
AceConfigDialog.frame:SetScript("OnUpdate", RefreshOnUpdate)
end
@@ -1869,8 +1857,8 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
local BlizOptions = AceConfigDialog.BlizOptions
local key = appName
for n = 1, select("#", ...) do
key = key.."\001"..select(n, ...)
for n = 1, select('#', ...) do
key = key..'\001'..select(n, ...)
end
if not BlizOptions[appName] then
@@ -1883,13 +1871,13 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
group:SetName(name or appName, parent)
group:SetTitle(name or appName)
group:SetUserData("appName", appName)
if select("#", ...) > 0 then
group:SetUserData('appName', appName)
if select('#', ...) > 0 then
local path = {}
for n = 1, select("#",...) do
for n = 1, select('#',...) do
tinsert(path, (select(n, ...)))
end
group:SetUserData("path", path)
group:SetUserData('path', path)
end
group:SetCallback("OnShow", FeedToBlizPanel)
group:SetCallback("OnHide", ClearBlizPanel)
@@ -39,8 +39,8 @@
-- end
-- @class file
-- @name AceDB-3.0.lua
-- @release $Id: AceDB-3.0.lua 940 2010-06-19 08:01:47Z nevcairiel $
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 21
-- @release $Id: AceDB-3.0.lua 914 2010-03-08 12:09:22Z nevcairiel $
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 20
local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)
if not AceDB then return end -- No upgrade needed
@@ -507,11 +507,11 @@ function DBObjectLib:DeleteProfile(name, silent)
error("Cannot delete the active profile in an AceDBObject.", 2)
end
if not rawget(self.profiles, name) and not silent then
if not rawget(self.sv.profiles, name) and not silent then
error("Cannot delete profile '" .. name .. "'. It does not exist.", 2)
end
self.profiles[name] = nil
self.sv.profiles[name] = nil
-- populate to child namespaces
if self.children then
@@ -537,7 +537,7 @@ function DBObjectLib:CopyProfile(name, silent)
error("Cannot have the same source and destination profiles.", 2)
end
if not rawget(self.profiles, name) and not silent then
if not rawget(self.sv.profiles, name) and not silent then
error("Cannot copy profile '" .. name .. "'. It does not exist.", 2)
end
@@ -545,7 +545,7 @@ function DBObjectLib:CopyProfile(name, silent)
DBObjectLib.ResetProfile(self, nil, true)
local profile = self.profile
local source = self.profiles[name]
local source = self.sv.profiles[name]
copyTable(source, profile)
@@ -1,8 +1,8 @@
--- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles.
-- @class file
-- @name AceDBOptions-3.0
-- @release $Id: AceDBOptions-3.0.lua 938 2010-06-13 07:21:38Z nevcairiel $
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 12
-- @release $Id: AceDBOptions-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 11
local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR)
if not AceDBOptions then return end -- No upgrade needed
@@ -30,7 +30,7 @@ local L = {
reset_desc = "Reset the current profile back to its default values, in case your configuration is broken, or you simply want to start over.",
reset = "Reset Profile",
reset_sub = "Reset the current profile to the default",
choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already existing profiles.",
choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already exisiting profiles.",
new = "New",
new_sub = "Create a new empty profile.",
choose = "Existing Profiles",
@@ -5,7 +5,7 @@
--
-- **Note**: When using AceGUI-3.0 directly, please do not modify the frames of the widgets directly,
-- as any "unknown" change to the widgets will cause addons that get your widget out of the widget pool
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we"ll
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we'll
-- implement a proper API to modify it.
-- @usage
-- local AceGUI = LibStub("AceGUI-3.0")
@@ -24,8 +24,8 @@
-- f:AddChild(btn)
-- @class file
-- @name AceGUI-3.0
-- @release $Id: AceGUI-3.0.lua 924 2010-05-13 15:12:20Z nevcairiel $
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 33
-- @release $Id: AceGUI-3.0.lua 919 2010-05-09 11:36:03Z nevcairiel $
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 31
local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR)
if not AceGUI then return end -- No upgrade needed
@@ -98,7 +98,7 @@ Dispatchers[0] = function(func)
end
local function safecall(func, ...)
return Dispatchers[select("#", ...)](func, ...)
return Dispatchers[select('#', ...)](func, ...)
end
-- Recycling functions
@@ -173,16 +173,16 @@ function AceGUI:Create(type)
if WidgetRegistry[type] then
local widget = newWidget(type)
if rawget(widget, "Acquire") then
if rawget(widget,'Acquire') then
widget.OnAcquire = widget.Acquire
widget.Acquire = nil
elseif rawget(widget, "Aquire") then
elseif rawget(widget,'Aquire') then
widget.OnAcquire = widget.Aquire
widget.Aquire = nil
end
if rawget(widget, "Release") then
widget.OnRelease = rawget(widget, "Release")
if rawget(widget,'Release') then
widget.OnRelease = rawget(widget,'Release')
widget.Release = nil
end
@@ -191,8 +191,8 @@ function AceGUI:Create(type)
else
error(("Widget type %s doesn't supply an OnAcquire Function"):format(type))
end
-- Set the default Layout ("List")
safecall(widget.SetLayout, widget, "List")
-- Set the default Layout ('List')
safecall(widget.SetLayout, widget, 'List')
safecall(widget.ResumeLayout, widget)
return widget
end
@@ -204,14 +204,14 @@ end
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
-- @param widget The widget to release
function AceGUI:Release(widget)
safecall(widget.PauseLayout, widget)
safecall( widget.PauseLayout, widget )
widget:Fire("OnRelease")
safecall(widget.ReleaseChildren, widget)
safecall( widget.ReleaseChildren, widget )
if widget.OnRelease then
widget:OnRelease()
-- else
-- error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
else
error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
end
for k in pairs(widget.userdata) do
widget.userdata[k] = nil
@@ -267,6 +267,7 @@ end
--[[
Widgets must provide the following functions
OnAcquire() - Called when the object is acquired, should set everything to a default hidden state
OnRelease() - Called when the object is Released, should remove any anchors and hide the Widget
And the following members
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
@@ -280,7 +281,6 @@ end
content - frame or derivitive that children will be anchored to
The Widget can supply the following Optional Members
:OnRelease() - Called when the object is Released, should remove any additional anchors and clear any data
:OnWidthSet(width) - Called when the width of the widget is changed
:OnHeightSet(height) - Called when the height of the widget is changed
Widgets should not use the OnSizeChanged events of thier frame or content members, use these methods instead
@@ -294,6 +294,17 @@ end
-- Widget Base Template --
--------------------------
do
local function fixlevels(parent,...)
local i = 1
local child = select(i, ...)
while child do
child:SetFrameLevel(parent:GetFrameLevel()+1)
fixlevels(child, child:GetChildren())
i = i + 1
child = select(i, ...)
end
end
local WidgetBase = AceGUI.WidgetBase
WidgetBase.SetParent = function(self, parent)
@@ -301,6 +312,7 @@ do
frame:SetParent(nil)
frame:SetParent(parent.content)
self.parent = parent
--fixlevels(parent.frame,parent.frame:GetChildren())
end
WidgetBase.SetCallback = function(self, name, func)
@@ -433,7 +445,7 @@ do
if self.LayoutPaused then
return
end
safecall(self.LayoutFunc, self.content, self.children)
safecall(self.LayoutFunc,self.content, self.children)
end
--call this function to layout, makes sure layed out objects get a frame to get sizes etc
@@ -512,7 +524,7 @@ do
end
end
setmetatable(WidgetContainerBase, {__index=WidgetBase})
setmetatable(WidgetContainerBase,{__index=WidgetBase})
--One of these function should be called on each Widget Instance as part of its creation process
@@ -525,11 +537,10 @@ do
widget.base = WidgetContainerBase
widget.content.obj = widget
widget.frame.obj = widget
widget.content:SetScript("OnSizeChanged", ContentResize)
widget.frame:SetScript("OnSizeChanged", FrameResize)
setmetatable(widget, {__index = WidgetContainerBase})
widget.content:SetScript("OnSizeChanged",ContentResize)
widget.frame:SetScript("OnSizeChanged",FrameResize)
setmetatable(widget,{__index=WidgetContainerBase})
widget:SetLayout("List")
return widget
end
--- Register a widget-class as a widget.
@@ -539,9 +550,8 @@ do
widget.events = {}
widget.base = WidgetBase
widget.frame.obj = widget
widget.frame:SetScript("OnSizeChanged", FrameResize)
setmetatable(widget, {__index = WidgetBase})
return widget
widget.frame:SetScript("OnSizeChanged",FrameResize)
setmetatable(widget,{__index=WidgetBase})
end
end
@@ -614,6 +624,50 @@ function AceGUI:GetWidgetVersion(type)
return WidgetVersions[type]
end
--[[ Widget Template
--------------------------
-- Widget Name --
--------------------------
do
local Type = "Type"
local function OnAcquire(self)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.frame = frame
frame.obj = self
--Container Support
--local content = CreateFrame("Frame",nil,frame)
--self.content = content
--AceGUI:RegisterAsContainer(self)
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor)
end
]]
-------------
-- Layouts --
-------------
@@ -622,13 +676,15 @@ end
A Layout is a func that takes 2 parameters
content - the frame that widgets will be placed inside
children - a table containing the widgets to layout
]]
-- Very simple Layout, Children are stacked on top of each other down the left side
AceGUI:RegisterLayout("List",
function(content, children)
local height = 0
local width = content.width or content:GetWidth() or 0
function(content, children)
local height = 0
local width = content.width or content:GetWidth() or 0
for i = 1, #children do
local child = children[i]
@@ -636,21 +692,25 @@ AceGUI:RegisterLayout("List",
frame:ClearAllPoints()
frame:Show()
if i == 1 then
frame:SetPoint("TOPLEFT", content)
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
else
frame:SetPoint("TOPLEFT", children[i-1].frame, "BOTTOMLEFT")
frame:SetPoint("TOPLEFT",children[i-1].frame,"BOTTOMLEFT",0,0)
end
if child.width == "fill" then
child:SetWidth(width)
frame:SetPoint("RIGHT", content)
frame:SetPoint("RIGHT",content,"RIGHT")
if child.OnWidthSet then
child:OnWidthSet(content.width or content:GetWidth())
end
if child.DoLayout then
child:DoLayout()
end
elseif child.width == "relative" then
child:SetWidth(width * child.relWidth)
if child.OnWidthSet then
child:OnWidthSet(content.width or content:GetWidth())
end
if child.DoLayout then
child:DoLayout()
end
@@ -658,43 +718,45 @@ AceGUI:RegisterLayout("List",
height = height + (frame.height or frame:GetHeight() or 0)
end
safecall(content.obj.LayoutFinished, content.obj, nil, height)
end)
safecall( content.obj.LayoutFinished, content.obj, nil, height )
end
)
-- A single control fills the whole content area
AceGUI:RegisterLayout("Fill",
function(content, children)
function(content, children)
if children[1] then
children[1]:SetWidth(content:GetWidth() or 0)
children[1]:SetHeight(content:GetHeight() or 0)
children[1].frame:SetAllPoints(content)
children[1].frame:Show()
safecall(content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight())
safecall( content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight() )
end
end)
end
)
AceGUI:RegisterLayout("Flow",
function(content, children)
--used height so far
local height = 0
--width used in the current row
local usedwidth = 0
--height of the current row
local rowheight = 0
local rowoffset = 0
local lastrowoffset
local width = content.width or content:GetWidth() or 0
--control at the start of the row
local rowstart
function(content, children)
--used height so far
local height = 0
--width used in the current row
local usedwidth = 0
--height of the current row
local rowheight = 0
local rowoffset = 0
local lastrowoffset
local width = content.width or content:GetWidth() or 0
--control at the start of the row
local rowstart
local rowstartoffset
local lastrowstart
local isfullheight
local frameoffset
local lastframeoffset
local oversize
local lastrowstart
local isfullheight
local frameoffset
local lastframeoffset
local oversize
for i = 1, #children do
local child = children[i]
oversize = nil
@@ -717,7 +779,7 @@ AceGUI:RegisterLayout("Flow",
frame:ClearAllPoints()
if i == 1 then
-- anchor the first control to the top left
frame:SetPoint("TOPLEFT", content)
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
rowheight = frameheight
rowoffset = frameoffset
rowstart = frame
@@ -736,7 +798,7 @@ AceGUI:RegisterLayout("Flow",
break
end
--anchor the previous row, we will now know its height and offset
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3))
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
height = height + rowheight + 3
--save this as the rowstart so we can anchor it after the row is complete and we have the max height and offset of controls in it
rowstart = frame
@@ -754,21 +816,25 @@ AceGUI:RegisterLayout("Flow",
--offset is always the larger of the two offsets
rowoffset = math_max(rowoffset, frameoffset)
rowheight = math_max(rowheight, rowoffset + (frameheight / 2))
frame:SetPoint("TOPLEFT", children[i-1].frame, "TOPRIGHT", 0, frameoffset - lastframeoffset)
rowheight = math_max(rowheight,rowoffset+(frameheight/2))
--print("type:", child.type, "offset:",frameoffset-lastframeoffset)
frame:SetPoint("TOPLEFT",children[i-1].frame,"TOPRIGHT",0,frameoffset-lastframeoffset)
usedwidth = framewidth + usedwidth
end
end
if child.width == "fill" then
child:SetWidth(width)
frame:SetPoint("RIGHT", content)
frame:SetPoint("RIGHT",content,"RIGHT",0,0)
usedwidth = 0
rowstart = frame
rowstartoffset = frameoffset
if child.OnWidthSet then
child:OnWidthSet(width)
end
if child.DoLayout then
child:DoLayout()
end
@@ -778,28 +844,33 @@ AceGUI:RegisterLayout("Flow",
elseif child.width == "relative" then
child:SetWidth(width * child.relWidth)
if child.OnWidthSet then
child:OnWidthSet(width)
end
if child.DoLayout then
child:DoLayout()
end
elseif oversize then
if width > 1 then
frame:SetPoint("RIGHT", content)
frame:SetPoint("RIGHT",content,"RIGHT",0,0)
end
end
if child.height == "fill" then
frame:SetPoint("BOTTOM", content)
frame:SetPoint("BOTTOM",content,"BOTTOM")
isfullheight = true
end
end
--anchor the last row, if its full height needs a special case since its height has just been changed by the anchor
if isfullheight then
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -height)
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-height)
elseif rowstart then
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3))
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
end
height = height + rowheight + 3
safecall(content.obj.LayoutFinished, content.obj, nil, height)
end)
safecall( content.obj.LayoutFinished, content.obj, nil, height )
end
)
@@ -1,28 +1,26 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\FrameXML\UI.xsd">
<Script file="AceGUI-3.0.lua"/>
<!-- Container -->
<Script file="widgets\AceGUIContainer-BlizOptionsGroup.lua"/>
<Script file="widgets\AceGUIContainer-DropDownGroup.lua"/>
<Script file="widgets\AceGUIContainer-Frame.lua"/>
<Script file="widgets\AceGUIContainer-InlineGroup.lua"/>
<Script file="widgets\AceGUIContainer-ScrollFrame.lua"/>
<Script file="widgets\AceGUIContainer-SimpleGroup.lua"/>
<Script file="widgets\AceGUIContainer-TabGroup.lua"/>
<Script file="widgets\AceGUIContainer-TreeGroup.lua"/>
<Script file="widgets\AceGUIContainer-Window.lua"/>
<!-- Widgets -->
<Script file="widgets\AceGUIWidget-Button.lua"/>
<Script file="widgets\AceGUIWidget-CheckBox.lua"/>
<Script file="widgets\AceGUIWidget-ColorPicker.lua"/>
<Script file="widgets\AceGUIWidget-DropDownGroup.lua"/>
<Script file="widgets\AceGUIWidget-DropDown.lua"/>
<Script file="widgets\AceGUIWidget-DropDown-Items.lua"/>
<Script file="widgets\AceGUIWidget-EditBox.lua"/>
<Script file="widgets\AceGUIWidget-Heading.lua"/>
<Script file="widgets\AceGUIWidget-Icon.lua"/>
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
<Script file="widgets\AceGUIWidget-Keybinding.lua"/>
<Script file="widgets\AceGUIWidget-Frame.lua"/>
<Script file="widgets\AceGUIWidget-Window.lua"/>
<Script file="widgets\AceGUIWidget-Heading.lua"/>
<Script file="widgets\AceGUIWidget-InlineGroup.lua"/>
<Script file="widgets\AceGUIWidget-Keybinding.lua"/>
<Script file="widgets\AceGUIWidget-ScrollFrame.lua"/>
<Script file="widgets\AceGUIWidget-SimpleGroup.lua"/>
<Script file="widgets\AceGUIWidget-Slider.lua"/>
<Script file="widgets\AceGUIWidget-TabGroup.lua"/>
<Script file="widgets\AceGUIWidget-TreeGroup.lua"/>
<Script file="widgets\AceGUIWidget-Label.lua"/>
<Script file="widgets\AceGUIWidget-MultiLineEditBox.lua"/>
<Script file="widgets\AceGUIWidget-Slider.lua"/>
</Ui>
<Script file="widgets\AceGUIWidget-BlizOptionsGroup.lua"/>
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
<Script file="widgets\AceGUIWidget-Icon.lua"/>
</Ui>
@@ -0,0 +1,153 @@
local AceGUI = LibStub("AceGUI-3.0")
-------------
-- Widgets --
-------------
--[[
Widgets must provide the following functions
Acquire() - Called when the object is aquired, should set everything to a default hidden state
Release() - Called when the object is Released, should remove any anchors and hide the Widget
And the following members
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
type - the type of the object, same as the name given to :RegisterWidget()
Widgets contain a table called userdata, this is a safe place to store data associated with the wigdet
It will be cleared automatically when a widget is released
Placing values directly into a widget object should be avoided
If the Widget can act as a container for other Widgets the following
content - frame or derivitive that children will be anchored to
The Widget can supply the following Optional Members
]]
----------------------------------
-- Blizzard Options Group --
----------------------------------
--[[
Group Designed to be added to the bliz interface options panel
]]
-- WoW APIs
local CreateFrame = CreateFrame
do
local Type = "BlizOptionsGroup"
local Version = 10
local function OnAcquire(self)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self:SetName()
end
local function okay(this)
this.obj:Fire("okay")
end
local function cancel(this)
this.obj:Fire("cancel")
end
local function defaults(this)
this.obj:Fire("defaults")
end
local function SetName(self, name, parent)
self.frame.name = name
self.frame.parent = parent
end
local function OnShow(this)
this.obj:Fire("OnShow")
end
local function OnHide(this)
this.obj:Fire("OnHide")
end
local function OnWidthSet(self, width)
local content = self.content
local contentwidth = width - 63
if contentwidth < 0 then
contentwidth = 0
end
content:SetWidth(contentwidth)
content.width = contentwidth
end
local function OnHeightSet(self, height)
local content = self.content
local contentheight = height - 26
if contentheight < 0 then
contentheight = 0
end
content:SetHeight(contentheight)
content.height = contentheight
end
local function SetTitle(self, title)
local content = self.content
content:ClearAllPoints()
if not title or title == "" then
content:SetPoint("TOPLEFT",self.frame,"TOPLEFT",10,-10)
self.label:SetText("")
else
content:SetPoint("TOPLEFT",self.frame,"TOPLEFT",10,-40)
self.label:SetText(title)
end
content:SetPoint("BOTTOMRIGHT",self.frame,"BOTTOMRIGHT",-10,10)
end
local function Constructor()
local frame = CreateFrame("Frame")
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.frame = frame
self.SetName = SetName
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
self.SetTitle = SetTitle
frame.obj = self
frame.okay = okay
frame.cancel = cancel
frame.defaults = defaults
frame:Hide()
frame:SetScript("OnHide",OnHide)
frame:SetScript("OnShow",OnShow)
local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
self.label = label
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -15)
label:SetPoint("BOTTOMRIGHT", frame, "TOPRIGHT", 10, -45)
label:SetJustifyH("LEFT")
label:SetJustifyV("TOP")
--Container Support
local content = CreateFrame("Frame",nil,frame)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",frame,"TOPLEFT",15,-10)
content:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-10,10)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end

Some files were not shown because too many files have changed in this diff Show More