19 Commits

Author SHA1 Message Date
sindoring 8284eefb2b новый коммит на батче элементов 2026-09-17 21:09:52 +04:00
sindoring d0b6ac7cff новый warcraftxl 2026-09-17 20:58:29 +04:00
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_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY= AWS_SECRET_ACCESS_KEY=
AWS_SESSION_TOKEN=
AWS_DEFAULT_REGION= AWS_DEFAULT_REGION=
AWS_BUCKET= AWS_BUCKET=
AWS_ENDPOINT= AWS_ENDPOINT=
@@ -10,3 +11,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=
PRODUCTION_REALMLIST= PRODUCTION_REALMLIST=
PTR_REALMLIST= PTR_REALMLIST=
LOCAL_REALMLIST= LOCAL_REALMLIST=
SSH_USER=root
SSH_HOST=strictlypragmatic.ru
+2
View File
@@ -5,6 +5,8 @@ manifest.json
.vscode .vscode
dist dist
build/ build/
/new customization mpqs/*.mpq
Wow*.exe Wow*.exe
*.backup.exe *.backup.exe
Logs/ 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"] [submodule "vendor/modules/wxl-unit-outline"]
path = vendor/modules/wxl-unit-outline path = vendor/modules/wxl-unit-outline
url = https://github.com/WarcraftXL/wxl-unit-outline.git url = https://github.com/WarcraftXL/wxl-unit-outline.git
branch = main branch = v1.1
[submodule "vendor/modules/wxl-modern-adt"] [submodule "vendor/modules/wxl-modern-adt"]
path = vendor/modules/wxl-modern-adt path = vendor/modules/wxl-modern-adt
url = https://github.com/WarcraftXL/wxl-modern-adt.git url = https://github.com/WarcraftXL/wxl-modern-adt.git
branch = main 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) cmake_minimum_required(VERSION 3.20)
project(MoonWellWarcraftXL LANGUAGES CXX) project(MoonWellWarcraftXL LANGUAGES CXX)
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to # WarcraftXL is an untouched upstream v1.1 submodule. Every project-specific
# its aggregate DLL target here, so updating the framework does not mix our # feature below is built as a separately loaded Extensions/<name>/<name>.dll.
# client-specific offsets and policy into the upstream checkout.
add_subdirectory(vendor/warcraftxl) 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_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
set(WXL_LOCAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules")
function(wxl_collect_sources output) function(wxl_add_external_extension extension_name extension_dir)
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN}) if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
set(${output} "${collected}" PARENT_SCOPE) 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() endfunction()
if(TARGET WarcraftXL) # Official WarcraftXL 1.1 extensions.
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME wxl_add_external_extension(wxl-db2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-db2")
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp" wxl_add_external_extension(wxl-modern-adt "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt")
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp") wxl_add_external_extension(wxl-modern-blp "${WXL_LOCAL_MODULES_DIR}/wxl-modern-blp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX) wxl_add_external_extension(wxl-modern-m2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-m2")
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX wxl_add_external_extension(wxl-modern-wmo "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-wmo")
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}") wxl_add_external_extension(wxl-grasswind "${WXL_EXTERNAL_MODULES_DIR}/wxl-grasswind")
endif() wxl_add_external_extension(wxl-unit-outline "${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline")
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")
target_sources(WarcraftXL PRIVATE # MoonWell-owned extensions. wxl-fdid-moonwell sorts after wxl-db2 and before
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp" # the modern asset extensions, so it can layer custom CSV mappings over wxl.fdid.
${WXL_MODERN_ASSETS_RUNTIME} wxl_add_external_extension(MoonWell "${WXL_LOCAL_MODULES_DIR}/moonwell")
${WXL_MODERN_RENDER_RUNTIME} if(TARGET MoonWell)
${WXL_UNIT_OUTLINE_RUNTIME} target_link_libraries(MoonWell PRIVATE shell32)
${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")
endif() 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) # Recovery loader retained for existing -NativeRenderer workflows. WarcraftXL
wxl_collect_sources(WXL_MOONWELL_HOST # 1.1 already ships a native D3D9 forwarding proxy as its standard renderer.
"${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.
if(CMAKE_SIZEOF_VOID_P EQUAL 4) if(CMAKE_SIZEOF_VOID_P EQUAL 4)
add_library(MoonWellLoader SHARED add_library(MoonWellLoader SHARED
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp" "${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). Исходники клиентских изменений 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 загружается как обычно, локальный MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения `d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
только к памяти запущенного процесса. только к памяти запущенного процесса.
@@ -14,25 +14,25 @@ MoonWell-патчей. Неподписанный оригинальный execu
- одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`; - одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`;
- загрузка WarcraftXL без import-table patch и без отдельного injector. - загрузка WarcraftXL без import-table patch и без отдельного injector.
Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому При установке сборщик включает Large Address Aware в копии `Wow.exe`; исходный
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает `Wow_Original.exe` остаётся неизменным и используется как проверенный шаблон.
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
## Структура ## Структура
```text ```text
modules/moonwell/ MoonWell runtime-модуль WarcraftXL modules/ отдельные MoonWell extensions для WarcraftXL 1.1
vendor/warcraftxl/ закреплённый upstream git submodule vendor/warcraftxl/ закреплённый upstream git submodule
vendor/modules/ закреплённые модули WarcraftXL vendor/modules/ закреплённые модули WarcraftXL
src/Data/ исходники MPQ-патчей src/Data/ исходники MPQ-патчей
tool/ сборщик MPQ tool/ сборщик MPQ
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host build-warcraftxl.ps1 сборка Win32 runtime, proxy и extensions
run.ps1 полная сборка, установка и запуск клиента run.ps1 полная сборка, установка и запуск клиента
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git) Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
``` ```
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и Подключены совместимые с 1.1 extensions: `wxl-db2`, `wxl-modern-adt`, `wxl-modern-m2`,
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule. `wxl-modern-wmo`, `wxl-grasswind` и `wxl-unit-outline`. BLP-адаптер и MoonWell-изменения
собираются как отдельные DLL; исходники upstream core не изменяются.
Для обновления всех зависимостей WarcraftXL: Для обновления всех зависимостей WarcraftXL:
```powershell ```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+; - CMake 3.20+;
- Rust/Cargo для существующего MPQ-сборщика; - Rust/Cargo для существующего MPQ-сборщика;
- Python 3 и DVC с поддержкой S3 для содержимого внешних графических патчей
(`py -m pip install -r requirements-dvc.txt`);
- локальный оригинальный `Wow_Original.exe` с SHA-256 - локальный оригинальный `Wow_Original.exe` с SHA-256
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`. `AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
@@ -64,7 +66,7 @@ git submodule update --init --recursive
.\deploy.ps1 .\deploy.ps1
``` ```
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host, Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, native D3D9 proxy и extensions,
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`. `WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
@@ -77,8 +79,6 @@ git submodule update --init --recursive
# Быстро обновить только WarcraftXL без пересборки MPQ # Быстро обновить только WarcraftXL без пересборки MPQ
.\deploy.ps1 -SkipDataBuild .\deploy.ps1 -SkipDataBuild
# Аварийный native D3D9 вместо D3D9On12
.\deploy.ps1 -NativeRenderer
``` ```
Без установки в клиент: Без установки в клиент:
@@ -99,18 +99,8 @@ git submodule update --init --recursive
1. проверяет хеш `Wow_Original.exe`; 1. проверяет хеш `Wow_Original.exe`;
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`; 2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
3. восстанавливает оригинальный `Wow.exe`; 3. восстанавливает оригинальный `Wow.exe`;
4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`; 4. устанавливает `WarcraftXL.dll`, native D3D9 proxy и DLL в `Extensions\`;
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`; 5. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
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
```
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`. Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
@@ -121,8 +111,13 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
.\run.ps1 .\run.ps1
``` ```
Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
`Utils\WarcraftXLHost.log` внутри клиента.
## Авторизация через лаунчер
Release-клиент принимает только одноразовую launcher-сессию и при прямом запуске показывает
кнопку открытия лаунчера вместо полей логина и пароля. Контракт переменных запуска, требования
к auth-серверу и Debug-режим описаны в [LAUNCHER_AUTH.md](LAUNCHER_AUTH.md).
## MPQ-пакеты ## MPQ-пакеты
@@ -136,8 +131,18 @@ WarcraftXL не требуется и в объектное хранилище
- `patch-ruRU-5` — русская локализация и интерфейс; - `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные. - `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 распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным MoonWell extensions распространяются с доступным соответствующим исходным кодом. Репозиторий не
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`. должен публиковать 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' $ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot $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' $cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
if (-not (Test-Path -LiteralPath $cmake)) { if (-not (Test-Path -LiteralPath $cmake)) {
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue $cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
@@ -84,11 +129,9 @@ if (-not (Test-Path -LiteralPath $cmake)) {
$cmake = $cmakeCommand.Source $cmake = $cmakeCommand.Source
} }
& git -C $repoRoot submodule update --init --recursive Initialize-MissingSubmodules -RepositoryPath $repoRoot
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32' $win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
$stockExe = Join-Path $repoRoot 'Wow_Original.exe' $stockExe = Join-Path $repoRoot 'Wow_Original.exe'
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) { 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=' & $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' } 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.' } 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" $win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll' $warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll' $selectedProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll" $recoveryProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe' $extensionNames = Get-MoonWellExtensionNames
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources' if ($NativeRenderer) {
Write-Warning '-NativeRenderer is no longer needed: WarcraftXL 1.1 uses native D3D9 by default.'
}
function Install-WarcraftXLArtifacts { function Install-WarcraftXLArtifacts {
param( param(
@@ -127,12 +166,12 @@ function Install-WarcraftXLArtifacts {
[string]$Destination [string]$Destination
) )
$utils = Join-Path $Destination 'Utils' Remove-MoonWellRuntimeGarbage -Destination $Destination
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
$utils = Join-Path $Destination 'Utils'
New-Item ` New-Item `
-ItemType Directory ` -ItemType Directory `
-Path $Destination, $utils, $loosePatch ` -Path $Destination, $utils `
-Force | -Force |
Out-Null Out-Null
@@ -156,20 +195,17 @@ function Install-WarcraftXLArtifacts {
-Force -Force
Copy-Item ` Copy-Item `
-LiteralPath $nativeProxy ` -LiteralPath $recoveryProxy `
-Destination (Join-Path $utils 'd3d9-native.dll') ` -Destination (Join-Path $utils 'd3d9-native.dll') `
-Force -Force
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 ` Copy-Item `
-LiteralPath $hostExe ` -LiteralPath $extensionSource `
-Destination (Join-Path $utils 'WarcraftXLHost.exe') ` -Destination (Join-Path $extensionDestination "$extensionName.dll") `
-Force
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
Copy-Item `
-LiteralPath $_.FullName `
-Destination $loosePatch `
-Recurse `
-Force -Force
} }
} }
@@ -196,14 +232,17 @@ if ($Deploy) {
} }
Install-WarcraftXLArtifacts -Destination $ClientPath 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)) { if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath) $PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
Install-WarcraftXLArtifacts -Destination $PackagePath Install-WarcraftXLArtifacts -Destination $PackagePath
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $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 1.1 build complete (native D3D9 proxy): $win32ArtifactDir"
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
+4
View File
@@ -9,6 +9,8 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from patch_layout import PATCH_LAYOUT
IGNORED_TOP_LEVEL_DIRS = { IGNORED_TOP_LEVEL_DIRS = {
"Cache", "Cache",
@@ -49,6 +51,8 @@ def is_ignored(rel_path: Path) -> bool:
return True return True
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES: if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
return True 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]) 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 "pet_template_display_id": 5448
}, },
"mounts": [], "mounts": [],
"pets": [ "pets": []
{
"folder": "src/Data/patch-Z/Creature/Catslime",
"name": "Cat Slime",
"name_ru": "Котослизень",
"display_scale": 1.0
}
]
} }
+33 -11
View File
@@ -13,6 +13,7 @@ param(
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot $repoRoot = $PSScriptRoot
. (Join-Path $repoRoot 'pipeline-layout.ps1')
function Import-ProjectEnvironment { function Import-ProjectEnvironment {
$envFile = Join-Path $repoRoot '.env' $envFile = Join-Path $repoRoot '.env'
@@ -46,6 +47,10 @@ $PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) { if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath" throw "Client directory was not found: $ClientPath"
} }
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object { $runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase) $_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
@@ -58,20 +63,27 @@ if ($runningClients.Count) {
Write-Host 'Stopping the running WoW client...' Write-Host 'Stopping the running WoW client...'
$runningClients | Stop-Process -Force $runningClients | Stop-Process -Force
Start-Sleep -Seconds 2 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 $env:WOW_HOME = $ClientPath
Write-Host "MoonWell client: $ClientPath" Write-Host "MoonWell client: $ClientPath"
Write-Host "Package staging: $PackagePath" Write-Host "Package staging: $PackagePath"
Write-Host '[1/5] Initializing WarcraftXL dependencies...' Write-Host '[1/5] Preserving initialized WarcraftXL worktrees...'
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
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) { if (-not $SkipDataBuild) {
Write-Host '[2/5] Building MPQ patches...' Write-Host '[2/5] Building MPQ patches...'
@@ -102,6 +114,18 @@ if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' } if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
Write-Host '[4/5] Synchronizing package files...' 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 & robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
$robocopyExitCode = $LASTEXITCODE $robocopyExitCode = $LASTEXITCODE
if ($robocopyExitCode -ge 8) { if ($robocopyExitCode -ge 8) {
@@ -110,8 +134,6 @@ if ($robocopyExitCode -ge 8) {
if (-not $SkipManifest) { if (-not $SkipManifest) {
Write-Host '[5/5] Building launcher manifest...' 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') ` & $python.Source (Join-Path $repoRoot 'build_manifest.py') `
--dir $PackagePath --output (Join-Path $repoRoot 'manifest.json') --dir $PackagePath --output (Join-Path $repoRoot 'manifest.json')
if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' } 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 $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 ''
Write-Host 'Deployment complete.' Write-Host 'Deployment complete.'
Write-Host " Renderer: $renderer" Write-Host " Renderer: $renderer"
Write-Host " Wow.exe SHA-256: $wowHash" 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) { if ($Launch) {
Write-Host 'Launching Wow.exe...' 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; return E_NOINTERFACE;
} }
// Compatibility exports referenced by the current WarcraftXL core. MoonWell // Legacy compatibility exports retained for the recovery proxy. WarcraftXL 1.1
// does not enable its D3D9On12 backend, so these intentionally report no D3D12 // uses native D3D9 and ignores the former D3D9On12 bridge.
// device and a neutral supersampling factor.
extern "C" void* WxlD3D12Device() { return nullptr; } extern "C" void* WxlD3D12Device() { return nullptr; }
extern "C" void* WxlD3D12Queue() { return nullptr; } extern "C" void* WxlD3D12Queue() { return nullptr; }
extern "C" void WxlD3D12DrainDebug() {} 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 // changes are verified and applied to the process image during WarcraftXL's
// boot phase, before GlueXML is loaded. // boot phase, before GlueXML is loaded.
#include "core/Logger.hpp" #include "game/Script.hpp"
#include "core/Hook.hpp" #include "wxl/PluginApi.h"
#include "core/Mem.hpp" #include "SpellOverrides.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"
#include "offsets/engine/Gx.hpp"
#include <windows.h> #include <windows.h>
#include <shellapi.h>
#include <array> #include <array>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstdio>
#include <cstring> #include <cstring>
#include <cstdarg>
#include <cwchar>
namespace moonwell namespace moonwell
{ {
namespace 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; constexpr uint32_t kTraitorFlag = 0x40000000u;
bool g_loginCharacterIsTraitor = false; 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; GxSetProjectionFn g_nextSetProjection = nullptr;
// FrameXML's stock SetCreature only accepts a creature entry and waits // FrameXML's stock SetCreature only accepts a creature entry and waits
@@ -64,10 +362,10 @@ namespace moonwell
int __cdecl SetCreatureDisplayInfoHook(void* state) int __cdecl SetCreatureDisplayInfoHook(void* state)
{ {
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0; 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; 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) if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
return result; return result;
@@ -102,7 +400,7 @@ namespace moonwell
void InstallEncounterJournalModelPreview() void InstallEncounterJournalModelPreview()
{ {
if (!wxl::core::hook::Install("MoonWellSetCreatureDisplayInfo", kSetCreature, if (!Hook("MoonWellSetCreatureDisplayInfo", kSetCreature,
&SetCreatureDisplayInfoHook, &g_nextSetCreature)) &SetCreatureDisplayInfoHook, &g_nextSetCreature))
{ {
WLOG_ERROR("moonwell: encounter journal model hook installation failed"); WLOG_ERROR("moonwell: encounter journal model hook installation failed");
@@ -151,14 +449,14 @@ namespace moonwell
int __cdecl SetCharacterCreateCamera(void* state) int __cdecl SetCharacterCreateCamera(void* state)
{ {
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1) const bool enabled = state && wxl::game::script::IsNumber(state, 1)
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0; && wxl::game::script::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2) const float faceZoom = state && wxl::game::script::IsNumber(state, 2)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f; ? static_cast<float>(wxl::game::script::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3) const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f; ? static_cast<float>(wxl::game::script::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4) double requestedDuration = state && wxl::game::script::IsNumber(state, 4)
? wxl::runtime::lua::ToNumber(state, 4) : 500.0; ? wxl::game::script::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0; if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0; if (requestedDuration > 2000.0) requestedDuration = 2000.0;
@@ -202,15 +500,16 @@ namespace moonwell
void InstallCharacterCreateCamera() void InstallCharacterCreateCamera()
{ {
namespace gx = wxl::offsets::engine::gx; constexpr uintptr_t kGxDeviceVTable = 0x00A2E718;
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable); constexpr unsigned kGxSetProjectionSlot = 0xA0 / 4;
void** slot = &vtable[gx::kGxSetProjectionSlot]; void** vtable = reinterpret_cast<void**>(kGxDeviceVTable);
void** slot = &vtable[kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook)) if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return; return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot); g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook); 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; g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed"); WLOG_ERROR("moonwell: character-create projection hook installation failed");
@@ -222,17 +521,17 @@ namespace moonwell
int __cdecl SetLoginCharacterFlags(void* state) int __cdecl SetLoginCharacterFlags(void* state)
{ {
uint32_t flags = 0; uint32_t flags = 0;
if (state && wxl::runtime::lua::IsNumber(state, 1)) if (state && wxl::game::script::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1)); flags = static_cast<uint32_t>(wxl::game::script::ToNumber(state, 1));
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0; g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0); wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1; return 1;
} }
int __cdecl IsTraitor(void* state) int __cdecl IsTraitor(void* state)
{ {
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0); wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1; return 1;
} }
@@ -259,7 +558,7 @@ namespace moonwell
name, reinterpret_cast<void*>(address)); name, reinterpret_cast<void*>(address));
return false; 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, WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address)); reinterpret_cast<void*>(address));
@@ -371,7 +670,7 @@ namespace moonwell
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) - const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size())); (patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel)); 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); VirtualFree(cave, 0, MEM_RELEASE);
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed"); WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
@@ -390,40 +689,251 @@ namespace moonwell
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above"); WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
else else
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)"); 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 return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags)
// shared buffered logger, then make startup diagnostics durable. || callback == reinterpret_cast<uintptr_t>(&IsTraitor)
Sleep(1000); || callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera)
wxl::core::log::Flush(); || callback == reinterpret_cast<uintptr_t>(&GetLaunchMode)
return 0; || 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); RegisterLuaFunctionsForCurrentState();
if (thread) CloseHandle(thread); 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;
wxl::runtime::lua::RegisterFunction(
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags); constexpr uintptr_t kRegisterFunction = 0x00817F90;
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor); const auto registrar = reinterpret_cast<RegisterFunctionFn>(kRegisterFunction);
wxl::runtime::lua::RegisterFunction( g_registeringMoonWell = true;
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera); registrar("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot); registrar("MoonWellIsTraitor", &IsTraitor);
wxl::runtime::modules::Register( registrar("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
"moonwell-character-create-camera", &InstallCharacterCreateCamera); registrar("MoonWellGetLaunchMode", &GetLaunchMode);
wxl::runtime::modules::Register( registrar("MoonWellConsumeLauncherAuth", &ConsumeLauncherAuth);
"moonwell-encounter-journal-models", &InstallEncounterJournalModelPreview); registrar("MoonWellOpenLauncher", &OpenLauncher);
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush); g_registeringMoonWell = false;
InterlockedExchangePointer(&g_registeredState, state);
WLOG_INFO("moonwell: Lua functions registered for state %p", state);
}
void RegisterLuaFunctionsForCurrentState()
{
constexpr uintptr_t kGetContext = 0x00817DB0;
RegisterLuaFunctions(reinterpret_cast<GetContextFn>(kGetContext)());
}
void RegisterLuaFunctionsBeforeHooksEnable()
{
RegisterLuaFunctionsForCurrentState();
}
void __cdecl OnClientMessage(void*, const void*)
{
RegisterLuaFunctionsForCurrentState();
}
struct WindowSearch
{
DWORD processId;
HWND window;
};
BOOL CALLBACK FindClientWindow(HWND window, LPARAM parameter)
{
auto* search = reinterpret_cast<WindowSearch*>(parameter);
DWORD processId = 0;
GetWindowThreadProcessId(window, &processId);
if (processId != search->processId || !IsWindowVisible(window))
return TRUE;
search->window = window;
return FALSE;
}
DWORD WINAPI ScheduleLuaBootstrap(LPVOID)
{
HWND clientWindow = nullptr;
WLOG_INFO("moonwell: Lua bootstrap scheduler started");
for (unsigned attempt = 0; attempt < 600; ++attempt)
{
if (!clientWindow)
{
clientWindow = FindWindowW(L"GxWindowClass", nullptr);
DWORD ownerProcessId = 0;
if (clientWindow)
GetWindowThreadProcessId(clientWindow, &ownerProcessId);
if (ownerProcessId != GetCurrentProcessId())
clientWindow = nullptr;
if (!clientWindow)
{
WindowSearch search{GetCurrentProcessId(), nullptr};
EnumWindows(&FindClientWindow, reinterpret_cast<LPARAM>(&search));
clientWindow = search.window;
}
}
if (clientWindow)
{
WLOG_INFO("moonwell: client window %p found for Lua bootstrap", clientWindow);
// BeginServerLogin only snapshots the supplied credentials and starts
// the client's asynchronous login state machine. Waiting here keeps the
// call clear of engine initialization and Glue archive mounting.
for (unsigned attempt = 0;
attempt < 80 && g_hasLauncherAuth;
++attempt)
{
Sleep(250);
SubmitLauncherAuthorization();
}
return g_hasLauncherAuth ? 1 : 0;
}
Sleep(100);
}
WLOG_ERROR("moonwell: timed out waiting for UI-thread Lua bootstrap");
return 1;
}
void __cdecl ExecuteHook(const char* source, uintptr_t argument2, uintptr_t argument3)
{
RegisterLuaFunctionsForCurrentState();
if (g_nextExecute) g_nextExecute(source, argument2, argument3);
}
int __cdecl InitializeLuaHook(void* allocatorContext)
{
const int initialized = g_nextInitializeLua
? g_nextInitializeLua(allocatorContext)
: 0;
if (initialized)
RegisterLuaFunctionsForCurrentState();
return initialized;
}
void __cdecl FramePumpHook(float deltaSeconds, uint32_t frameTimeMs)
{
RegisterLuaFunctionsForCurrentState();
if (g_nextFramePump) g_nextFramePump(deltaSeconds, frameTimeMs);
RegisterLuaFunctionsForCurrentState();
if (g_hasLauncherAuth
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
SubmitLauncherAuthorization();
}
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
{
// The engine-init callback and synchronous Glue loader run on the same client
// thread. Polling here catches Lua becoming live before AccountLogin executes,
// without ever touching the state from background asset I/O workers.
if (GetCurrentThreadId() == g_clientThreadId)
RegisterLuaFunctionsForCurrentState();
const int result = g_nextFileOpen ? g_nextFileOpen(archive, name, flags, out) : 0;
return result;
}
void __cdecl GlueModelRenderHook(void* frame)
{
RegisterLuaFunctionsForCurrentState();
if (g_nextGlueModelRender) g_nextGlueModelRender(frame);
}
void __cdecl OnFrame(void*, const void*)
{
RegisterLuaFunctionsForCurrentState();
// Glue scripting is not a reliable authentication trigger: a broken
// cosmetic widget can abort AccountLogin_OnShow before it reaches the
// login call. Present is emitted on the client thread after the engine
// and Glue subsystem are ready, so submit once after a short grace period.
if (g_hasLauncherAuth
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
SubmitLauncherAuthorization();
}
bool InstallLuaBridge()
{
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
&g_nextValidateCallback);
const bool executor = HookByName("Lua.Execute", &ExecuteHook, &g_nextExecute);
const bool initializer = Hook("Lua.Initialize", 0x00819BB0,
&InitializeLuaHook, &g_nextInitializeLua);
const bool defaultLogin = Hook("MoonWell.DefaultServerLogin", 0x004DC260,
&DefaultServerLoginHook, &g_nextDefaultServerLogin);
const bool framePump = HookByName("Frame.Pump", &FramePumpHook, &g_nextFramePump);
const bool fileOpen = HookByName("Io.FileOpen", &FileOpenHook, &g_nextFileOpen);
const bool glueRender = HookByName("Gx.GlueModelRender", &GlueModelRenderHook,
&g_nextGlueModelRender);
if (!validator || !executor || !initializer || !defaultLogin
|| !framePump || !fileOpen || !glueRender)
WLOG_ERROR("moonwell: Lua bridge hook installation failed");
return validator && executor && initializer && defaultLogin
&& framePump && fileOpen && glueRender;
} }
} g_registration;
} }
} }
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 // 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 "game/Io.hpp"
#include "core/Logger.hpp" #include "wxl/PluginApi.h"
#include "mpq/MpqStore.hpp" #include "wxl/StorageApi.h"
#include <cctype> #include <cctype>
#include <charconv> #include <charconv>
@@ -15,7 +15,7 @@
#include <system_error> #include <system_error>
#include <vector> #include <vector>
namespace moonwell::host namespace moonwell::spells
{ {
namespace namespace
{ {
@@ -38,8 +38,34 @@ namespace moonwell::host
std::once_flag g_loadOnce; std::once_flag g_loadOnce;
bool g_ready = false; bool g_ready = false;
std::vector<uint8_t> g_spellDbc; const WXL_Api* g_api = nullptr;
std::vector<uint8_t> g_skillDbc; 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) uint32_t ReadU32(const uint8_t* data)
{ {
@@ -226,39 +252,59 @@ namespace moonwell::host
void Load() void Load()
{ {
const std::string root = wxl::host::ClientRoot(); std::vector<uint8_t> overrideBytes;
wxl::host::mpq::MpqStore store; if (!ReadAll(kOverridesPath.data(), overrideBytes))
std::vector<uint8_t> overrideBytes, spellBase, skillBase;
if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes))
return; return;
std::vector<Override> overrides; if (!ParseOverrides(overrideBytes, g_overrides))
if (!ParseOverrides(overrideBytes, overrides) ||
!store.ReadAll(kSpellPath, spellBase) || !store.ReadAll(kSkillPath, skillBase) ||
!BuildSpellDbc(spellBase, overrides, g_spellDbc) ||
!BuildSkillDbc(skillBase, overrides, g_skillDbc))
{ {
WLOG_ERROR("moonwell-spells: failed to build custom companion DBCs"); Log(WXL_LOG_ERROR, "failed to parse WXLSpellOverrides.tsv");
return; return;
} }
g_ready = true; 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; if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath)) return false;
std::call_once(g_loadOnce, &Load); std::call_once(g_loadOnce, &Load);
if (!g_ready) return false; if (!g_ready) return false;
out = SamePath(name, kSpellPath) ? g_spellDbc : g_skillDbc;
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;
}
}
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)
{
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; return true;
} }
struct Registrar
{
Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); }
};
Registrar g_registrar;
}
} }
+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( param(
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[ValidateSet("production", "ptr", "local")] [ValidateSet("production", "ptr", "local")]
[string]$Env = "local" [string]$Env = "local",
[switch]$DeveloperLogin
) )
# Stop on errors # Stop on errors
@@ -9,6 +10,7 @@ $ErrorActionPreference = "Stop"
# --- Resolve project root (folder where script is located) # --- Resolve project root (folder where script is located)
$ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path $ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
. (Join-Path $ROOT 'pipeline-layout.ps1')
# --- Load .env if WOW_HOME not already set # --- Load .env if WOW_HOME not already set
if (-not $env:WOW_HOME) { if (-not $env:WOW_HOME) {
@@ -67,9 +69,16 @@ if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and
-not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and -not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and
-not $isAdministrator) { -not $isAdministrator) {
Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..." Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..."
$elevationArguments = @(
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
"`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
)
if ($DeveloperLogin) {
$elevationArguments += "-DeveloperLogin"
}
$elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait ` $elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait `
-WorkingDirectory $ROOT ` -WorkingDirectory $ROOT `
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env -ArgumentList $elevationArguments
exit $elevatedProcess.ExitCode exit $elevatedProcess.ExitCode
} }
@@ -80,11 +89,16 @@ $TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat" $RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1" $WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
Assert-SafePackagePath `
-PackagePath $DIST_DIR `
-RepositoryPath $ROOT `
-ClientPath $WOW_HOME
function Stop-WowRuntime { function Stop-WowRuntime {
param([Parameter(Mandatory=$true)][string]$ClientPath) param([Parameter(Mandatory=$true)][string]$ClientPath)
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') + '\' $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 { try {
$_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase) $_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase)
} catch { } catch {
@@ -96,7 +110,7 @@ function Stop-WowRuntime {
return return
} }
Write-Host "Stopping the running WoW client and WarcraftXL host..." Write-Host "Stopping the running WoW client..."
$runtimeProcesses | Stop-Process -Force $runtimeProcesses | Stop-Process -Force
try { try {
@@ -118,7 +132,14 @@ if (!(Test-Path $TOOL)) {
Pop-Location 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/..." Write-Host "Building MPQ archives from src/ -> dist/..."
& $TOOL $SRC_DIR $DIST_DIR & $TOOL $SRC_DIR $DIST_DIR
if ($LASTEXITCODE -ne 0) { 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. # are uploaded by upload_to_s3.py and consumed by the launcher manifest.
Stop-WowRuntime -ClientPath $WOW_HOME Stop-WowRuntime -ClientPath $WOW_HOME
Write-Host "Building and packaging WarcraftXL..." Write-Host "Building and packaging WarcraftXL..."
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME ` $runtimeConfiguration = if ($DeveloperLogin) { "Debug" } else { "Release" }
& $WXL_BUILD_SCRIPT -Configuration $runtimeConfiguration -ClientPath $WOW_HOME `
-PackagePath $DIST_DIR -Deploy -PackagePath $DIST_DIR -Deploy
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Error "WarcraftXL build/deploy failed!" Write-Error "WarcraftXL build/deploy failed!"
@@ -145,6 +167,22 @@ if ($LASTEXITCODE -ne 0) {
# --- Sync dist/ -> WOW_HOME # --- Sync dist/ -> WOW_HOME
Write-Host "Syncing 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 $DIST_DIR $WOW_HOME /E /NFL /NDL
# robocopy exit codes 0-7 are success/warnings; 8+ are errors # robocopy exit codes 0-7 are success/warnings; 8+ are errors
if ($LASTEXITCODE -ge 8) { if ($LASTEXITCODE -ge 8) {
@@ -171,4 +209,11 @@ if ($realmlist) {
# --- Run WoW reload script # --- Run WoW reload script
Write-Host "Launching WoW..." Write-Host "Launching WoW..."
cmd /c $RELOAD_SCRIPT if ($DeveloperLogin) {
$env:MOONWELL_DEV_LOGIN = "1"
}
try {
cmd /c $RELOAD_SCRIPT
} finally {
Remove-Item Env:MOONWELL_DEV_LOGIN -ErrorAction SilentlyContinue
}
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 = { local LoginState = {
autoLoginTimer = nil, autoLoginDelay = Config.AUTO_LOGIN_DELAY, autoLoginAttempted = false, musicTimer = 0, sceneTimer = 0, currentFrame = 0, lastUpdateTime = 0, currentScene = 1 autoLoginTimer = nil, autoLoginDelay = Config.AUTO_LOGIN_DELAY, autoLoginAttempted = false, musicTimer = 0, sceneTimer = 0, currentFrame = 0, lastUpdateTime = 0, currentScene = 1,
launchMode = "locked", launcherLoginAttempted = false, luaBridgePending = true
} }
local ModelManager = { local ModelManager = {
@@ -51,7 +52,7 @@ local ModelManager = {
} }
local UICache = { local UICache = {
accountEdit = nil, passwordEdit = nil, saveAccountName = nil, savePassword = nil, autoLogin = nil, autoLoginText = nil, loginButton = nil, versionText = nil, realmName = nil, upgradeButton = nil, tosFrame = nil, tosAccept = nil, tosDecline = nil, backgroundTexture = nil, animatedTexture = nil, newLogo = nil, newLogoFrame = nil accountEdit = nil, passwordEdit = nil, saveAccountName = nil, savePassword = nil, autoLogin = nil, autoLoginText = nil, saveOptionsFrame = nil, launcherHint = nil, loginButton = nil, versionText = nil, realmName = nil, upgradeButton = nil, tosFrame = nil, tosAccept = nil, tosDecline = nil, backgroundTexture = nil, animatedTexture = nil, newLogo = nil, newLogoFrame = nil
} }
LOGIN_MODEL_LIGHTS = { LOGIN_MODEL_LIGHTS = {
@@ -78,6 +79,8 @@ local function InitializeUICache()
UICache.savePassword = _G["AccountLoginSavePassword"] UICache.savePassword = _G["AccountLoginSavePassword"]
UICache.autoLogin = _G["AccountLoginAutoLogin"] UICache.autoLogin = _G["AccountLoginAutoLogin"]
UICache.autoLoginText = _G["AccountLoginAutoLoginText"] UICache.autoLoginText = _G["AccountLoginAutoLoginText"]
UICache.saveOptionsFrame = _G["AccountLoginSaveOptionsFrame"]
UICache.launcherHint = _G["AccountLoginLauncherHint"]
UICache.loginButton = _G["AccountLoginLoginButton"] UICache.loginButton = _G["AccountLoginLoginButton"]
UICache.versionText = _G["AccountLoginVersion"] UICache.versionText = _G["AccountLoginVersion"]
UICache.realmName = _G["AccountLoginRealmName"] UICache.realmName = _G["AccountLoginRealmName"]
@@ -94,6 +97,106 @@ end
function GetLoginState() function GetLoginState()
return LoginState return LoginState
end end
local function ResolveLaunchMode()
if type(MoonWellGetLaunchMode) ~= "function" then
return "locked"
end
local mode = MoonWellGetLaunchMode()
if mode == "launcher" or mode == "developer" then
return mode
end
return "locked"
end
local function ApplyLaunchMode()
local developerMode = LoginState.launchMode == "developer"
local launcherMode = LoginState.launchMode == "launcher"
if launcherMode then
AccountLoginUI:Hide()
else
AccountLoginUI:Show()
AccountLoginUI:SetAlpha(1)
end
if developerMode then
UICache.accountEdit:Show()
UICache.passwordEdit:Show()
UICache.saveOptionsFrame:Show()
UICache.launcherHint:Hide()
UICache.loginButton:SetText(LOGIN)
else
UICache.accountEdit:Hide()
UICache.passwordEdit:Hide()
UICache.saveOptionsFrame:Hide()
UICache.launcherHint:Show()
UICache.loginButton:SetText("Авторизоваться")
end
end
function AccountLogin_OpenLauncher()
PlaySound("gsLogin")
local opened = false
if type(MoonWellOpenLauncher) == "function" then
opened = MoonWellOpenLauncher() and true or false
end
if not opened then
GlueDialog_Show("MOONWELL_LAUNCHER_NOT_FOUND")
end
end
function AccountLogin_PrimaryAction()
if LoginState.launchMode == "developer" then
AccountLogin_Login()
else
AccountLogin_OpenLauncher()
end
end
local function TryLauncherLogin()
if LoginState.launchMode ~= "launcher" or LoginState.launcherLoginAttempted then
return false
end
LoginState.launcherLoginAttempted = true
if type(MoonWellConsumeLauncherAuth) ~= "function" then
LoginState.launchMode = "locked"
ApplyLaunchMode()
return false
end
local accountName, ticket = MoonWellConsumeLauncherAuth()
if not accountName or accountName == "" or not ticket or ticket == "" then
LoginState.launchMode = "locked"
ApplyLaunchMode()
return false
end
SetSavedAccountName("")
SetSavedAccountList("")
SetUsesToken(false)
AccountLoginUI:Hide()
PlaySound("gsLogin")
DefaultServerLogin(accountName, ticket)
accountName = nil
ticket = nil
return true
end
local function TryLauncherFallbackLogin()
if not LoginState.luaBridgePending or LoginState.launcherLoginAttempted then
return false
end
-- MoonWell.dll intercepts this stock callback and substitutes the one-time
-- launcher credentials. With no launcher data it intentionally does nothing.
LoginState.launcherLoginAttempted = true
DefaultServerLogin("__MOONWELL_LAUNCHER__", "__MOONWELL_LAUNCHER__")
return true
end
-- ============================================================================ -- ============================================================================
-- SISTEMA DE ANIMACIÓN BLP -- SISTEMA DE ANIMACIÓN BLP
-- ============================================================================ -- ============================================================================
@@ -144,6 +247,11 @@ GlueDialogTypes["AUTO_LOGIN"] = {
UICache.autoLogin:SetChecked(0) UICache.autoLogin:SetChecked(0)
end, end,
} }
GlueDialogTypes["MOONWELL_LAUNCHER_NOT_FOUND"] = {
text = "Не удалось открыть лаунчер MoonWell. Запустите его вручную и нажмите «Играть».",
button1 = OKAY,
}
-- ============================================================================ -- ============================================================================
-- GESTIÓN DE MODELOS 3D -- GESTIÓN DE MODELOS 3D
-- ============================================================================ -- ============================================================================
@@ -151,10 +259,8 @@ local function CreateLoginModel(parent, modelData)
local model = CreateFrame("Model", nil, parent) local model = CreateFrame("Model", nil, parent)
local width, height = parent:GetSize() local width, height = parent:GetSize()
model:SetSize( model:SetWidth(width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1), model:SetHeight(height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1)
)
model:SetPoint("CENTER") model:SetPoint("CENTER")
model:SetModel("Character/Human/Male/HumanMale.mdx") model:SetModel("Character/Human/Male/HumanMale.mdx")
model:SetCamera(1) model:SetCamera(1)
@@ -194,6 +300,13 @@ end
-- ACTUALIZACIÓN DE LA ESCENA -- ACTUALIZACIÓN DE LA ESCENA
-- ============================================================================ -- ============================================================================
function LoginScene_OnUpdate(self, elapsed) function LoginScene_OnUpdate(self, elapsed)
if LoginState.luaBridgePending and type(MoonWellGetLaunchMode) == "function" then
LoginState.luaBridgePending = false
LoginState.launchMode = ResolveLaunchMode()
ApplyLaunchMode()
TryLauncherLogin()
end
if Config.LOGIN_AMBIENCE and not self.ambiencePlayed then if Config.LOGIN_AMBIENCE and not self.ambiencePlayed then
PlayGlueAmbience(Config.LOGIN_AMBIENCE, Config.AMBIENCE_FADE_TIME) PlayGlueAmbience(Config.LOGIN_AMBIENCE, Config.AMBIENCE_FADE_TIME)
self.ambiencePlayed = true self.ambiencePlayed = true
@@ -283,8 +396,8 @@ function AccountLogin_OnLoad(self)
model:SetModel("Character/Human/Male/HumanMale.mdx") model:SetModel("Character/Human/Male/HumanMale.mdx")
model:SetPoint("CENTER", 0, 0) model:SetPoint("CENTER", 0, 0)
model:SetSize(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1), model:SetWidth(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1)) model:SetHeight(self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
model:SetCamera(1) model:SetCamera(1)
if data[LOGIN_MODEL_STRUCT.LIGHT] then if data[LOGIN_MODEL_STRUCT.LIGHT] then
@@ -312,6 +425,14 @@ function AccountLogin_OnLoad(self)
end end
function AccountLogin_OnShow(self) function AccountLogin_OnShow(self)
LoginState.luaBridgePending = type(MoonWellGetLaunchMode) ~= "function"
LoginState.launchMode = ResolveLaunchMode()
ApplyLaunchMode()
-- Submit launcher credentials before constructing the decorative login UI.
-- A cosmetic Lua error must never be able to block authentication.
TryLauncherFallbackLogin()
self:Show() self:Show()
self:SetAlpha(1) self:SetAlpha(1)
WorldOfWarcraftRating:Hide() WorldOfWarcraftRating:Hide()
@@ -341,12 +462,10 @@ function AccountLogin_OnShow(self)
elseif self.animatedTexture then elseif self.animatedTexture then
self.animatedTexture:Hide() self.animatedTexture:Hide()
end end
AccountLoginUI:Show()
AccountLoginUI:SetAlpha(1)
if not self.newLogo then if not self.newLogo then
self.newLogoFrame = CreateFrame("Frame", nil, self) self.newLogoFrame = CreateFrame("Frame", nil, self)
self.newLogoFrame:SetSize(Config.LOGO_SIZE, Config.LOGO_SIZE) self.newLogoFrame:SetWidth(Config.LOGO_SIZE)
self.newLogoFrame:SetHeight(Config.LOGO_SIZE)
self.newLogoFrame:SetPoint("TOPLEFT", Config.LOGO_POSITION_X, Config.LOGO_POSITION_Y) self.newLogoFrame:SetPoint("TOPLEFT", Config.LOGO_POSITION_X, Config.LOGO_POSITION_Y)
self.newLogo = self.newLogoFrame:CreateTexture("AccountLoginNewLogo", "OVERLAY") self.newLogo = self.newLogoFrame:CreateTexture("AccountLoginNewLogo", "OVERLAY")
@@ -376,7 +495,14 @@ function AccountLogin_OnShow(self)
self.modelUpdateCount = 0 self.modelUpdateCount = 0
self.modelUpdateTimer = 0 self.modelUpdateTimer = 0
local accountName, password = unpack(string_explode(GetSavedAccountName(), "#&|&#")) local accountName, password = "", ""
if LoginState.launchMode == "developer" then
accountName, password = unpack(string_explode(GetSavedAccountName(), "#&|&#"))
else
SetSavedAccountName("")
SetSavedAccountList("")
SetUsesToken(false)
end
UICache.accountEdit:SetText(accountName or "") UICache.accountEdit:SetText(accountName or "")
UICache.passwordEdit:SetText(password or "") UICache.passwordEdit:SetText(password or "")
@@ -390,13 +516,15 @@ function AccountLogin_OnShow(self)
UICache.realmName:SetText("No Recent Server") UICache.realmName:SetText("No Recent Server")
end end
if LoginState.launchMode == "developer" then
if accountName == "" then if accountName == "" then
AccountLogin_FocusAccountName() AccountLogin_FocusAccountName()
else else
AccountLogin_FocusPassword() AccountLogin_FocusPassword()
end end
end
if UICache.savePassword:GetChecked() then if LoginState.launchMode == "developer" and UICache.savePassword:GetChecked() then
UICache.autoLoginText:Show() UICache.autoLoginText:Show()
UICache.autoLogin:Show() UICache.autoLogin:Show()
else else
@@ -415,7 +543,11 @@ function AccountLogin_OnShow(self)
ACCOUNT_MSG_BODY_LOADED = false ACCOUNT_MSG_BODY_LOADED = false
ACCOUNT_MSG_CURRENT_INDEX = nil ACCOUNT_MSG_CURRENT_INDEX = nil
ApplyLaunchMode()
AccountLogin_CheckAutoLogin() AccountLogin_CheckAutoLogin()
if not TryLauncherLogin() then
TryLauncherFallbackLogin()
end
self:SetScript("OnUpdate", LoginScene_OnUpdate) self:SetScript("OnUpdate", LoginScene_OnUpdate)
end end
@@ -450,11 +582,15 @@ function AccountLogin_OnHide(self)
end end
function AccountLogin_FocusPassword() function AccountLogin_FocusPassword()
if LoginState.launchMode == "developer" then
UICache.passwordEdit:SetFocus() UICache.passwordEdit:SetFocus()
end
end end
function AccountLogin_FocusAccountName() function AccountLogin_FocusAccountName()
if LoginState.launchMode == "developer" then
UICache.accountEdit:SetFocus() UICache.accountEdit:SetFocus()
end
end end
function AccountLogin_OnKeyDown(key) function AccountLogin_OnKeyDown(key)
@@ -462,7 +598,7 @@ function AccountLogin_OnKeyDown(key)
if ConnectionHelpFrame:IsShown() then if ConnectionHelpFrame:IsShown() then
ConnectionHelpFrame:Hide() ConnectionHelpFrame:Hide()
AccountLoginUI:Show() AccountLoginUI:Show()
elseif SurveyNotificationFrame:IsShown() then elseif SurveyNotificationFrame and SurveyNotificationFrame:IsShown() then
else else
AccountLogin_Exit() AccountLogin_Exit()
end end
@@ -471,10 +607,10 @@ function AccountLogin_OnKeyDown(key)
return return
elseif TOSFrame:IsShown() or ConnectionHelpFrame:IsShown() then elseif TOSFrame:IsShown() or ConnectionHelpFrame:IsShown() then
return return
elseif SurveyNotificationFrame:IsShown() then elseif SurveyNotificationFrame and SurveyNotificationFrame:IsShown() then
AccountLogin_SurveyNotificationDone(1) AccountLogin_SurveyNotificationDone(1)
end end
AccountLogin_Login() AccountLogin_PrimaryAction()
elseif key == "PRINTSCREEN" then elseif key == "PRINTSCREEN" then
Screenshot() Screenshot()
end end
@@ -526,6 +662,11 @@ end
-- SISTEMA DE LOGIN CON GUARDADO DE CONTRASEÑA -- SISTEMA DE LOGIN CON GUARDADO DE CONTRASEÑA
-- ============================================================================ -- ============================================================================
function AccountLogin_Login() function AccountLogin_Login()
if LoginState.launchMode ~= "developer" then
AccountLogin_OpenLauncher()
return
end
PlaySound("gsLogin") PlaySound("gsLogin")
local accountName = UICache.accountEdit:GetText() local accountName = UICache.accountEdit:GetText()
local password = UICache.passwordEdit:GetText() local password = UICache.passwordEdit:GetText()
@@ -557,6 +698,11 @@ end
-- SISTEMA DE AUTOLOGIN -- SISTEMA DE AUTOLOGIN
-- ============================================================================ -- ============================================================================
function AccountLogin_CheckAutoLogin() function AccountLogin_CheckAutoLogin()
if LoginState.launchMode ~= "developer" then
LoginState.autoLoginTimer = nil
return
end
if not LoginState.autoLoginAttempted then if not LoginState.autoLoginAttempted then
LoginState.autoLoginAttempted = true LoginState.autoLoginAttempted = true
local savedAccountInfo = GetSavedAccountName() local savedAccountInfo = GetSavedAccountName()
@@ -811,6 +957,12 @@ function AccountLogin_Exit()
end end
function AccountLogin_ShowSurveyNotification() function AccountLogin_ShowSurveyNotification()
if not SurveyNotificationFrame or
not SurveyNotificationAccept or
not SurveyNotificationDecline then
return
end
GlueDialog:Hide() GlueDialog:Hide()
AccountLoginUI:Hide() AccountLoginUI:Hide()
SurveyNotificationAccept:Enable() SurveyNotificationAccept:Enable()
@@ -819,11 +971,19 @@ function AccountLogin_ShowSurveyNotification()
end end
function AccountLogin_SurveyNotificationDone(accepted) function AccountLogin_SurveyNotificationDone(accepted)
if SurveyNotificationFrame then
SurveyNotificationFrame:Hide() SurveyNotificationFrame:Hide()
end
if SurveyNotificationAccept then
SurveyNotificationAccept:Disable() SurveyNotificationAccept:Disable()
end
if SurveyNotificationDecline then
SurveyNotificationDecline:Disable() SurveyNotificationDecline:Disable()
end
if type(SurveyNotificationDone) == "function" then
SurveyNotificationDone(accepted) SurveyNotificationDone(accepted)
AccountLoginUI:Show() end
ApplyLaunchMode()
end end
function AccountLogin_ShowUserAgreements() function AccountLogin_ShowUserAgreements()
@@ -115,6 +115,18 @@
</Anchor> </Anchor>
</Anchors> </Anchors>
</FontString> </FontString>
<FontString name="AccountLoginLauncherHint" inherits="GlueFontNormal" justifyH="CENTER" text="Вход в игру выполняется через лаунчер MoonWell.">
<Size>
<AbsDimension x="420" y="24"/>
</Size>
<Anchors>
<Anchor point="BOTTOM">
<Offset>
<AbsDimension x="0" y="270"/>
</Offset>
</Anchor>
</Anchors>
</FontString>
<Texture name="WorldOfWarcraftRating" hidden="true"> <Texture name="WorldOfWarcraftRating" hidden="true">
<Size> <Size>
<AbsDimension x="128" y="128"/> <AbsDimension x="128" y="128"/>
@@ -195,7 +207,7 @@
<!-- ====================================== --> <!-- ====================================== -->
<!-- CAMPOS DE TEXTO: USUARIO Y CONTRASEÑA --> <!-- CAMPOS DE TEXTO: USUARIO Y CONTRASEÑA -->
<!-- ====================================== --> <!-- ====================================== -->
<EditBox name="AccountLoginAccountEdit" inherits="GlueDark_EditBoxTemplate" letters="320"> <EditBox name="AccountLoginAccountEdit" inherits="GlueDark_EditBoxTemplate" letters="320" hidden="true">
<Size x="200" y="37"/> <Size x="200" y="37"/>
<Anchors> <Anchors>
<Anchor point="BOTTOM"> <Anchor point="BOTTOM">
@@ -246,7 +258,7 @@
</Scripts> </Scripts>
</EditBox> </EditBox>
<EditBox name="AccountLoginPasswordEdit" inherits="GlueDark_EditBoxTemplate" letters="16" password="1"> <EditBox name="AccountLoginPasswordEdit" inherits="GlueDark_EditBoxTemplate" letters="16" password="1" hidden="true">
<Size x="200" y="37"/> <Size x="200" y="37"/>
<Anchors> <Anchors>
<Anchor point="BOTTOM"> <Anchor point="BOTTOM">
@@ -299,7 +311,7 @@
<!-- ====================================== --> <!-- ====================================== -->
<!-- CHECKBOXES: GUARDAR CUENTA Y CONTRASEÑA --> <!-- CHECKBOXES: GUARDAR CUENTA Y CONTRASEÑA -->
<!-- ====================================== --> <!-- ====================================== -->
<Frame name="AccountLoginSaveOptionsFrame"> <Frame name="AccountLoginSaveOptionsFrame" hidden="true">
<Size x="200" y="60"/> <Size x="200" y="60"/>
<Anchors> <Anchors>
<Anchor point="TOP" relativeTo="AccountLoginPasswordEdit" relativePoint="BOTTOM"> <Anchor point="TOP" relativeTo="AccountLoginPasswordEdit" relativePoint="BOTTOM">
@@ -883,7 +895,7 @@
<!-- ====================================== --> <!-- ====================================== -->
<!-- BOTÓN DE LOGIN --> <!-- BOTÓN DE LOGIN -->
<!-- ====================================== --> <!-- ====================================== -->
<Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="LOGIN"> <Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="Авторизоваться">
<Size x="200" y="55"/> <Size x="200" y="55"/>
<Anchors> <Anchors>
<Anchor point="BOTTOM"> <Anchor point="BOTTOM">
@@ -905,7 +917,7 @@
</Layers> </Layers>
<Scripts> <Scripts>
<OnClick> <OnClick>
AccountLogin_Login(); AccountLogin_PrimaryAction();
</OnClick> </OnClick>
</Scripts> </Scripts>
</Button> </Button>
@@ -4,6 +4,7 @@
## Author: Rochet2 <https://github.com/Rochet2> ## Author: Rochet2 <https://github.com/Rochet2>
## SavedVariables: AIO_sv, AIO_sv_Addons ## SavedVariables: AIO_sv, AIO_sv_Addons
## SavedVariablesPerCharacter: AIO_sv_char ## SavedVariablesPerCharacter: AIO_sv_char
## X-Hidden: 1
#dependencies #dependencies
Dep_LibWindow-1.1\LibStub.lua 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. -- as well as associate it with a slash command.
-- @class file -- @class file
-- @name AceConfig-3.0 -- @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 AceConfig-3.0
@@ -19,8 +19,8 @@ if not AceConfig then return end
local cfgreg = LibStub("AceConfigRegistry-3.0") local cfgreg = LibStub("AceConfigRegistry-3.0")
local cfgcmd = LibStub("AceConfigCmd-3.0") local cfgcmd = LibStub("AceConfigCmd-3.0")
--TODO: local cfgdlg = LibStub("AceConfigDialog-3.0", true) local cfgdlg = LibStub("AceConfigDialog-3.0")
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0", true) --TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0")
-- Lua APIs -- Lua APIs
local pcall, error, type, pairs = pcall, error, type, pairs 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. -- You can supply a slash command (or a table of slash commands) to register with AceConfigCmd directly.
-- @paramsig appName, options [, slashcmd] -- @paramsig appName, options [, slashcmd]
-- @param appName The application name for the config table. -- @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. -- @param slashcmd A slash command to register for the option table, or a table of slash commands.
-- @usage -- @usage
-- local AceConfig = LibStub("AceConfig-3.0") -- local AceConfig = LibStub("AceConfig-3.0")
@@ -1,10 +1,10 @@
--- AceConfigDialog-3.0 generates AceGUI-3.0 based windows based on option tables. --- AceConfigDialog-3.0 generates AceGUI-3.0 based windows based on option tables.
-- @class file -- @class file
-- @name AceConfigDialog-3.0 -- @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 LibStub = LibStub
local MAJOR, MINOR = "AceConfigDialog-3.0", 50 local MAJOR, MINOR = "AceConfigDialog-3.0", 47
local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR) local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
if not AceConfigDialog then return end 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.apps = AceConfigDialog.frame.apps or {}
AceConfigDialog.frame.closing = AceConfigDialog.frame.closing or {} AceConfigDialog.frame.closing = AceConfigDialog.frame.closing or {}
AceConfigDialog.frame.closeAllOverride = AceConfigDialog.frame.closeAllOverride or {}
local gui = LibStub("AceGUI-3.0") local gui = LibStub("AceGUI-3.0")
local reg = LibStub("AceConfigRegistry-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 tconcat, tinsert, tsort, tremove = table.concat, table.insert, table.sort, table.remove
local strmatch, format = string.match, string.format local strmatch, format = string.match, string.format
local assert, loadstring, error = assert, loadstring, error local assert, loadstring, error = assert, loadstring, error
local pairs, next, select, type, unpack, wipe = pairs, next, select, type, unpack, wipe local pairs, next, select, type, unpack = pairs, next, select, type, unpack
local rawset, tostring, tonumber = rawset, tostring, tonumber local rawset, tostring = rawset, tostring
local math_min, math_max, math_floor = math.min, math.max, math.floor 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 -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
@@ -77,7 +76,7 @@ Dispatchers[0] = function(func)
end end
local function safecall(func, ...) local function safecall(func, ...)
return Dispatchers[select("#", ...)](func, ...) return Dispatchers[select('#', ...)](func, ...)
end end
local width_multiplier = 170 local width_multiplier = 170
@@ -104,7 +103,7 @@ Group Types
local new, del, copy local new, del, copy
--newcount, delcount,createdcount,cached = 0,0,0 --newcount, delcount,createdcount,cached = 0,0,0
do do
local pool = setmetatable({},{__mode="k"}) local pool = setmetatable({},{__mode='k'})
function new() function new()
--newcount = newcount + 1 --newcount = newcount + 1
local t = next(pool) local t = next(pool)
@@ -241,7 +240,7 @@ local function GetOptionsMemberValue(membername, option, options, path, appName,
info.handler = handler info.handler = handler
info.option = option info.option = option
info.type = option.type info.type = option.type
info.uiType = "dialog" info.uiType = 'dialog'
info.uiName = MAJOR info.uiName = MAJOR
local a, b, c ,d local a, b, c ,d
@@ -322,8 +321,8 @@ local function compareOptions(a,b)
end end
local OrderA, OrderB = tempOrders[a] or 100, tempOrders[b] or 100 local OrderA, OrderB = tempOrders[a] or 100, tempOrders[b] or 100
if OrderA == OrderB then if OrderA == OrderB then
local NameA = (type(tempNames[a]) == "string") and tempNames[a] or "" local NameA = (type(tempNames[a] == "string") and tempNames[a]) or ""
local NameB = (type(tempNames[b]) == "string") and tempNames[b] or "" local NameB = (type(tempNames[b] == "string") and tempNames[b]) or ""
return NameA:upper() < NameB:upper() return NameA:upper() < NameB:upper()
end end
if OrderA < 0 then if OrderA < 0 then
@@ -479,7 +478,7 @@ function AceConfigDialog:SelectGroup(appName, ...)
local treevalue local treevalue
local treestatus local treestatus
for n = 1, select("#",...) do for n = 1, select('#',...) do
local key = select(n, ...) local key = select(n, ...)
if group.childGroups == "tab" or group.childGroups == "select" then 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) 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) GameTooltip:AddLine(user.text,0.5, 0.5, 0.8, 1)
end end
if type(desc) == "string" then if type(desc) == "string" then
@@ -562,10 +561,10 @@ end
local function GetFuncName(option) local function GetFuncName(option)
local type = option.type local type = option.type
if type == "execute" then if type == 'execute' then
return "func" return 'func'
else else
return "set" return 'set'
end end
end end
local function confirmPopup(appName, rootframe, basepath, info, message, func, ...) 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)) AceConfigDialog:Open(appName, rootframe, unpack(basepath or emptyTbl))
del(info) del(info)
end end
for i = 1, select("#", ...) do for i = 1, select('#', ...) do
t[i] = select(i, ...) or false t[i] = select(i, ...) or false
end end
t.timeout = 0 t.timeout = 0
@@ -654,7 +653,7 @@ local function ActivateControl(widget, event, ...)
info.handler = handler info.handler = handler
info.option = option info.option = option
info.type = option.type info.type = option.type
info.uiType = "dialog" info.uiType = 'dialog'
info.uiName = MAJOR info.uiName = MAJOR
local name local name
@@ -765,13 +764,13 @@ local function ActivateControl(widget, event, ...)
end end
end end
local iscustom = user.rootframe:GetUserData("iscustom") local iscustom = user.rootframe:GetUserData('iscustom')
local rootframe local rootframe
if iscustom then if iscustom then
rootframe = user.rootframe rootframe = user.rootframe
end end
local basepath = user.rootframe:GetUserData("basepath") local basepath = user.rootframe:GetUserData('basepath')
if type(func) == "string" then if type(func) == "string" then
if handler and handler[func] then if handler and handler[func] then
confirmPopup(user.appName, rootframe, basepath, info, confirmText, handler[func], handler, info, ...) 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 iscustom = user.rootframe:GetUserData('iscustom')
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
--full refresh of the frame, some controls dont cause this on all events --full refresh of the frame, some controls dont cause this on all events
if option.type == "color" then if option.type == "color" then
if event == "OnValueConfirmed" then if event == "OnValueConfirmed" then
@@ -835,16 +834,13 @@ local function ActivateControl(widget, event, ...)
end end
local function ActivateSlider(widget, event, value) 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 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 if step then
value = math_floor((value - min) / step + 0.5) * step + min value = math_floor((value - min) / step + 0.5) * step + min
end end
value = math_max(value, min) value = math_max(math_min(value,max),min)
end
if max then
value = math_min(value, max)
end end
ActivateControl(widget,event,value) ActivateControl(widget,event,value)
end end
@@ -852,10 +848,10 @@ end
--called from a checkbox that is part of an internally created multiselect group --called from a checkbox that is part of an internally created multiselect group
--this type is safe to refresh on activation of one control --this type is safe to refresh on activation of one control
local function ActivateMultiControl(widget, event, ...) local function ActivateMultiControl(widget, event, ...)
ActivateControl(widget, event, widget:GetUserData("value"), ...) ActivateControl(widget, event, widget:GetUserData('value'), ...)
local user = widget:GetUserDataTable() local user = widget:GetUserDataTable()
local iscustom = user.rootframe:GetUserData("iscustom") local iscustom = user.rootframe:GetUserData('iscustom')
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
if iscustom then if iscustom then
AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath)) AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath))
else else
@@ -866,8 +862,8 @@ end
local function MultiControlOnClosed(widget, event, ...) local function MultiControlOnClosed(widget, event, ...)
local user = widget:GetUserDataTable() local user = widget:GetUserDataTable()
if user.valuechanged then if user.valuechanged then
local iscustom = user.rootframe:GetUserData("iscustom") local iscustom = user.rootframe:GetUserData('iscustom')
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
if iscustom then if iscustom then
AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath)) AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath))
else else
@@ -877,7 +873,7 @@ local function MultiControlOnClosed(widget, event, ...)
end end
local function FrameOnClose(widget, event) local function FrameOnClose(widget, event)
local appName = widget:GetUserData("appName") local appName = widget:GetUserData('appName')
AceConfigDialog.OpenFrames[appName] = nil AceConfigDialog.OpenFrames[appName] = nil
gui:Release(widget) gui:Release(widget)
end end
@@ -1088,7 +1084,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName) local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
local image, width, height = GetOptionsMemberValue("image",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") control = gui:Create("Icon")
if not width then if not width then
width = GetOptionsMemberValue("imageWidth",v, options, path, appName) 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 if not height then
height = GetOptionsMemberValue("imageHeight",v, options, path, appName) height = GetOptionsMemberValue("imageHeight",v, options, path, appName)
end end
if type(imageCoords) == "table" then if type(imageCoords) == 'table' then
control:SetImage(image, unpack(imageCoords)) control:SetImage(image, unpack(imageCoords))
else else
control:SetImage(image) 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 image = GetOptionsMemberValue("image", v, options, path, appName)
local imageCoords = GetOptionsMemberValue("imageCoords", v, options, path, appName) local imageCoords = GetOptionsMemberValue("imageCoords", v, options, path, appName)
if type(image) == "string" then if type(image) == 'string' then
if type(imageCoords) == "table" then if type(imageCoords) == 'table' then
control:SetImage(image, unpack(imageCoords)) control:SetImage(image, unpack(imageCoords))
else else
control:SetImage(image) control:SetImage(image)
@@ -1243,8 +1239,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
local text = values[value] local text = values[value]
local check = gui:Create("CheckBox") local check = gui:Create("CheckBox")
check:SetLabel(text) check:SetLabel(text)
check:SetUserData("value", value) check:SetUserData('value', value)
check:SetUserData("text", text) check:SetUserData('text', text)
check:SetDisabled(disabled) check:SetDisabled(disabled)
check:SetTriState(v.tristate) check:SetTriState(v.tristate)
check:SetValue(GetOptionsMemberValue("get",v, options, path, appName, value)) 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 imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
local image, width, height = GetOptionsMemberValue("image",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 if not width then
width = GetOptionsMemberValue("imageWidth",v, options, path, appName) width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
end end
if not height then if not height then
height = GetOptionsMemberValue("imageHeight",v, options, path, appName) height = GetOptionsMemberValue("imageHeight",v, options, path, appName)
end end
if type(imageCoords) == "table" then if type(imageCoords) == 'table' then
control:SetImage(image, unpack(imageCoords)) control:SetImage(image, unpack(imageCoords))
else else
control:SetImage(image) control:SetImage(image)
@@ -1362,7 +1358,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
end end
local function BuildPath(path, ...) local function BuildPath(path, ...)
for i = 1, select("#",...) do for i = 1, select('#',...) do
tinsert(path, (select(i,...))) tinsert(path, (select(i,...)))
end end
end end
@@ -1665,12 +1661,9 @@ local function RefreshOnUpdate(this)
if this.closeAll then if this.closeAll then
for k, v in pairs(AceConfigDialog.OpenFrames) do for k, v in pairs(AceConfigDialog.OpenFrames) do
if not this.closeAllOverride[k] then
v:Hide() v:Hide()
end end
end
this.closeAll = nil this.closeAll = nil
wipe(this.closeAllOverride)
end end
for appName in pairs(this.apps) do for appName in pairs(this.apps) do
@@ -1682,7 +1675,7 @@ local function RefreshOnUpdate(this)
for key, widget in pairs(AceConfigDialog.BlizOptions[appName]) do for key, widget in pairs(AceConfigDialog.BlizOptions[appName]) do
local user = widget:GetUserDataTable() local user = widget:GetUserDataTable()
if widget:IsVisible() then 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 end
end end
@@ -1767,7 +1760,7 @@ function AceConfigDialog:Open(appName, container, ...)
tinsert(path, container) tinsert(path, container)
container = nil container = nil
end end
for n = 1, select("#",...) do for n = 1, select('#',...) do
tinsert(path, (select(n, ...))) tinsert(path, (select(n, ...)))
end end
@@ -1775,10 +1768,10 @@ function AceConfigDialog:Open(appName, container, ...)
if container then if container then
f = container f = container
f:ReleaseChildren() f:ReleaseChildren()
f:SetUserData("appName", appName) f:SetUserData('appName', appName)
f:SetUserData("iscustom", true) f:SetUserData('iscustom', true)
if #path > 0 then if #path > 0 then
f:SetUserData("basepath", copy(path)) f:SetUserData('basepath', copy(path))
end end
local status = AceConfigDialog:GetStatusTable(appName) local status = AceConfigDialog:GetStatusTable(appName)
if not status.width then if not status.width then
@@ -1802,9 +1795,9 @@ function AceConfigDialog:Open(appName, container, ...)
end end
f:ReleaseChildren() f:ReleaseChildren()
f:SetCallback("OnClose", FrameOnClose) f:SetCallback("OnClose", FrameOnClose)
f:SetUserData("appName", appName) f:SetUserData('appName', appName)
if #path > 0 then if #path > 0 then
f:SetUserData("basepath", copy(path)) f:SetUserData('basepath', copy(path))
end end
f:SetTitle(name or "") f:SetTitle(name or "")
local status = AceConfigDialog:GetStatusTable(appName) local status = AceConfigDialog:GetStatusTable(appName)
@@ -1816,11 +1809,6 @@ function AceConfigDialog:Open(appName, container, ...)
f:Show() f:Show()
end end
del(path) 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 end
-- convert pre-39 BlizOptions structure to the new format -- 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 old = AceConfigDialog.BlizOptions
local new = {} local new = {}
for key, widget in pairs(old) do 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 if not new[appName] then new[appName] = {} end
new[appName][key] = widget new[appName][key] = widget
end end
@@ -1838,12 +1826,12 @@ else
end end
local function FeedToBlizPanel(widget, event) local function FeedToBlizPanel(widget, event)
local path = widget:GetUserData("path") local path = widget:GetUserData('path')
AceConfigDialog:Open(widget:GetUserData("appName"), widget, unpack(path or emptyTbl)) AceConfigDialog:Open(widget:GetUserData('appName'), widget, unpack(path or emptyTbl))
end end
local function ClearBlizPanel(widget, event) local function ClearBlizPanel(widget, event)
local appName = widget:GetUserData("appName") local appName = widget:GetUserData('appName')
AceConfigDialog.frame.closing[appName] = true AceConfigDialog.frame.closing[appName] = true
AceConfigDialog.frame:SetScript("OnUpdate", RefreshOnUpdate) AceConfigDialog.frame:SetScript("OnUpdate", RefreshOnUpdate)
end end
@@ -1869,8 +1857,8 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
local BlizOptions = AceConfigDialog.BlizOptions local BlizOptions = AceConfigDialog.BlizOptions
local key = appName local key = appName
for n = 1, select("#", ...) do for n = 1, select('#', ...) do
key = key.."\001"..select(n, ...) key = key..'\001'..select(n, ...)
end end
if not BlizOptions[appName] then if not BlizOptions[appName] then
@@ -1883,13 +1871,13 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
group:SetName(name or appName, parent) group:SetName(name or appName, parent)
group:SetTitle(name or appName) group:SetTitle(name or appName)
group:SetUserData("appName", appName) group:SetUserData('appName', appName)
if select("#", ...) > 0 then if select('#', ...) > 0 then
local path = {} local path = {}
for n = 1, select("#",...) do for n = 1, select('#',...) do
tinsert(path, (select(n, ...))) tinsert(path, (select(n, ...)))
end end
group:SetUserData("path", path) group:SetUserData('path', path)
end end
group:SetCallback("OnShow", FeedToBlizPanel) group:SetCallback("OnShow", FeedToBlizPanel)
group:SetCallback("OnHide", ClearBlizPanel) group:SetCallback("OnHide", ClearBlizPanel)
@@ -39,8 +39,8 @@
-- end -- end
-- @class file -- @class file
-- @name AceDB-3.0.lua -- @name AceDB-3.0.lua
-- @release $Id: AceDB-3.0.lua 940 2010-06-19 08:01:47Z nevcairiel $ -- @release $Id: AceDB-3.0.lua 914 2010-03-08 12:09:22Z nevcairiel $
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 21 local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 20
local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR) local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)
if not AceDB then return end -- No upgrade needed 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) error("Cannot delete the active profile in an AceDBObject.", 2)
end 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) error("Cannot delete profile '" .. name .. "'. It does not exist.", 2)
end end
self.profiles[name] = nil self.sv.profiles[name] = nil
-- populate to child namespaces -- populate to child namespaces
if self.children then if self.children then
@@ -537,7 +537,7 @@ function DBObjectLib:CopyProfile(name, silent)
error("Cannot have the same source and destination profiles.", 2) error("Cannot have the same source and destination profiles.", 2)
end 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) error("Cannot copy profile '" .. name .. "'. It does not exist.", 2)
end end
@@ -545,7 +545,7 @@ function DBObjectLib:CopyProfile(name, silent)
DBObjectLib.ResetProfile(self, nil, true) DBObjectLib.ResetProfile(self, nil, true)
local profile = self.profile local profile = self.profile
local source = self.profiles[name] local source = self.sv.profiles[name]
copyTable(source, profile) copyTable(source, profile)
@@ -1,8 +1,8 @@
--- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles. --- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles.
-- @class file -- @class file
-- @name AceDBOptions-3.0 -- @name AceDBOptions-3.0
-- @release $Id: AceDBOptions-3.0.lua 938 2010-06-13 07:21:38Z nevcairiel $ -- @release $Id: AceDBOptions-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 12 local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 11
local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR) local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR)
if not AceDBOptions then return end -- No upgrade needed 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_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 = "Reset Profile",
reset_sub = "Reset the current profile to the default", 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 = "New",
new_sub = "Create a new empty profile.", new_sub = "Create a new empty profile.",
choose = "Existing Profiles", choose = "Existing Profiles",
@@ -5,7 +5,7 @@
-- --
-- **Note**: When using AceGUI-3.0 directly, please do not modify the frames of the widgets directly, -- **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 -- 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. -- implement a proper API to modify it.
-- @usage -- @usage
-- local AceGUI = LibStub("AceGUI-3.0") -- local AceGUI = LibStub("AceGUI-3.0")
@@ -24,8 +24,8 @@
-- f:AddChild(btn) -- f:AddChild(btn)
-- @class file -- @class file
-- @name AceGUI-3.0 -- @name AceGUI-3.0
-- @release $Id: AceGUI-3.0.lua 924 2010-05-13 15:12:20Z nevcairiel $ -- @release $Id: AceGUI-3.0.lua 919 2010-05-09 11:36:03Z nevcairiel $
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 33 local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 31
local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR) local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR)
if not AceGUI then return end -- No upgrade needed if not AceGUI then return end -- No upgrade needed
@@ -98,7 +98,7 @@ Dispatchers[0] = function(func)
end end
local function safecall(func, ...) local function safecall(func, ...)
return Dispatchers[select("#", ...)](func, ...) return Dispatchers[select('#', ...)](func, ...)
end end
-- Recycling functions -- Recycling functions
@@ -173,16 +173,16 @@ function AceGUI:Create(type)
if WidgetRegistry[type] then if WidgetRegistry[type] then
local widget = newWidget(type) local widget = newWidget(type)
if rawget(widget, "Acquire") then if rawget(widget,'Acquire') then
widget.OnAcquire = widget.Acquire widget.OnAcquire = widget.Acquire
widget.Acquire = nil widget.Acquire = nil
elseif rawget(widget, "Aquire") then elseif rawget(widget,'Aquire') then
widget.OnAcquire = widget.Aquire widget.OnAcquire = widget.Aquire
widget.Aquire = nil widget.Aquire = nil
end end
if rawget(widget, "Release") then if rawget(widget,'Release') then
widget.OnRelease = rawget(widget, "Release") widget.OnRelease = rawget(widget,'Release')
widget.Release = nil widget.Release = nil
end end
@@ -191,8 +191,8 @@ function AceGUI:Create(type)
else else
error(("Widget type %s doesn't supply an OnAcquire Function"):format(type)) error(("Widget type %s doesn't supply an OnAcquire Function"):format(type))
end end
-- Set the default Layout ("List") -- Set the default Layout ('List')
safecall(widget.SetLayout, widget, "List") safecall(widget.SetLayout, widget, 'List')
safecall(widget.ResumeLayout, widget) safecall(widget.ResumeLayout, widget)
return widget return widget
end end
@@ -204,14 +204,14 @@ end
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well. -- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
-- @param widget The widget to release -- @param widget The widget to release
function AceGUI:Release(widget) function AceGUI:Release(widget)
safecall(widget.PauseLayout, widget) safecall( widget.PauseLayout, widget )
widget:Fire("OnRelease") widget:Fire("OnRelease")
safecall(widget.ReleaseChildren, widget) safecall( widget.ReleaseChildren, widget )
if widget.OnRelease then if widget.OnRelease then
widget:OnRelease() widget:OnRelease()
-- else else
-- error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type)) error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
end end
for k in pairs(widget.userdata) do for k in pairs(widget.userdata) do
widget.userdata[k] = nil widget.userdata[k] = nil
@@ -267,6 +267,7 @@ end
--[[ --[[
Widgets must provide the following functions Widgets must provide the following functions
OnAcquire() - Called when the object is acquired, should set everything to a default hidden state 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 And the following members
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes 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 content - frame or derivitive that children will be anchored to
The Widget can supply the following Optional Members 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 :OnWidthSet(width) - Called when the width of the widget is changed
:OnHeightSet(height) - Called when the height 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 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 -- -- Widget Base Template --
-------------------------- --------------------------
do 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 local WidgetBase = AceGUI.WidgetBase
WidgetBase.SetParent = function(self, parent) WidgetBase.SetParent = function(self, parent)
@@ -301,6 +312,7 @@ do
frame:SetParent(nil) frame:SetParent(nil)
frame:SetParent(parent.content) frame:SetParent(parent.content)
self.parent = parent self.parent = parent
--fixlevels(parent.frame,parent.frame:GetChildren())
end end
WidgetBase.SetCallback = function(self, name, func) WidgetBase.SetCallback = function(self, name, func)
@@ -433,7 +445,7 @@ do
if self.LayoutPaused then if self.LayoutPaused then
return return
end end
safecall(self.LayoutFunc, self.content, self.children) safecall(self.LayoutFunc,self.content, self.children)
end end
--call this function to layout, makes sure layed out objects get a frame to get sizes etc --call this function to layout, makes sure layed out objects get a frame to get sizes etc
@@ -512,7 +524,7 @@ do
end end
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 --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.base = WidgetContainerBase
widget.content.obj = widget widget.content.obj = widget
widget.frame.obj = widget widget.frame.obj = widget
widget.content:SetScript("OnSizeChanged", ContentResize) widget.content:SetScript("OnSizeChanged",ContentResize)
widget.frame:SetScript("OnSizeChanged", FrameResize) widget.frame:SetScript("OnSizeChanged",FrameResize)
setmetatable(widget, {__index = WidgetContainerBase}) setmetatable(widget,{__index=WidgetContainerBase})
widget:SetLayout("List") widget:SetLayout("List")
return widget
end end
--- Register a widget-class as a widget. --- Register a widget-class as a widget.
@@ -539,9 +550,8 @@ do
widget.events = {} widget.events = {}
widget.base = WidgetBase widget.base = WidgetBase
widget.frame.obj = widget widget.frame.obj = widget
widget.frame:SetScript("OnSizeChanged", FrameResize) widget.frame:SetScript("OnSizeChanged",FrameResize)
setmetatable(widget, {__index = WidgetBase}) setmetatable(widget,{__index=WidgetBase})
return widget
end end
end end
@@ -614,6 +624,50 @@ function AceGUI:GetWidgetVersion(type)
return WidgetVersions[type] return WidgetVersions[type]
end 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 -- -- Layouts --
------------- -------------
@@ -622,11 +676,13 @@ end
A Layout is a func that takes 2 parameters A Layout is a func that takes 2 parameters
content - the frame that widgets will be placed inside content - the frame that widgets will be placed inside
children - a table containing the widgets to layout children - a table containing the widgets to layout
]] ]]
-- Very simple Layout, Children are stacked on top of each other down the left side -- Very simple Layout, Children are stacked on top of each other down the left side
AceGUI:RegisterLayout("List", AceGUI:RegisterLayout("List",
function(content, children) function(content, children)
local height = 0 local height = 0
local width = content.width or content:GetWidth() or 0 local width = content.width or content:GetWidth() or 0
for i = 1, #children do for i = 1, #children do
@@ -636,21 +692,25 @@ AceGUI:RegisterLayout("List",
frame:ClearAllPoints() frame:ClearAllPoints()
frame:Show() frame:Show()
if i == 1 then if i == 1 then
frame:SetPoint("TOPLEFT", content) frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
else else
frame:SetPoint("TOPLEFT", children[i-1].frame, "BOTTOMLEFT") frame:SetPoint("TOPLEFT",children[i-1].frame,"BOTTOMLEFT",0,0)
end end
if child.width == "fill" then if child.width == "fill" then
child:SetWidth(width) 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 if child.DoLayout then
child:DoLayout() child:DoLayout()
end end
elseif child.width == "relative" then elseif child.width == "relative" then
child:SetWidth(width * child.relWidth) child:SetWidth(width * child.relWidth)
if child.OnWidthSet then
child:OnWidthSet(content.width or content:GetWidth())
end
if child.DoLayout then if child.DoLayout then
child:DoLayout() child:DoLayout()
end end
@@ -658,8 +718,9 @@ AceGUI:RegisterLayout("List",
height = height + (frame.height or frame:GetHeight() or 0) height = height + (frame.height or frame:GetHeight() or 0)
end end
safecall(content.obj.LayoutFinished, content.obj, nil, height) safecall( content.obj.LayoutFinished, content.obj, nil, height )
end) end
)
-- A single control fills the whole content area -- A single control fills the whole content area
AceGUI:RegisterLayout("Fill", AceGUI:RegisterLayout("Fill",
@@ -669,9 +730,10 @@ AceGUI:RegisterLayout("Fill",
children[1]:SetHeight(content:GetHeight() or 0) children[1]:SetHeight(content:GetHeight() or 0)
children[1].frame:SetAllPoints(content) children[1].frame:SetAllPoints(content)
children[1].frame:Show() 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) end
)
AceGUI:RegisterLayout("Flow", AceGUI:RegisterLayout("Flow",
function(content, children) function(content, children)
@@ -717,7 +779,7 @@ AceGUI:RegisterLayout("Flow",
frame:ClearAllPoints() frame:ClearAllPoints()
if i == 1 then if i == 1 then
-- anchor the first control to the top left -- anchor the first control to the top left
frame:SetPoint("TOPLEFT", content) frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
rowheight = frameheight rowheight = frameheight
rowoffset = frameoffset rowoffset = frameoffset
rowstart = frame rowstart = frame
@@ -736,7 +798,7 @@ AceGUI:RegisterLayout("Flow",
break break
end end
--anchor the previous row, we will now know its height and offset --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 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 --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 rowstart = frame
@@ -754,21 +816,25 @@ AceGUI:RegisterLayout("Flow",
--offset is always the larger of the two offsets --offset is always the larger of the two offsets
rowoffset = math_max(rowoffset, frameoffset) 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 usedwidth = framewidth + usedwidth
end end
end end
if child.width == "fill" then if child.width == "fill" then
child:SetWidth(width) child:SetWidth(width)
frame:SetPoint("RIGHT", content) frame:SetPoint("RIGHT",content,"RIGHT",0,0)
usedwidth = 0 usedwidth = 0
rowstart = frame rowstart = frame
rowstartoffset = frameoffset rowstartoffset = frameoffset
if child.OnWidthSet then
child:OnWidthSet(width)
end
if child.DoLayout then if child.DoLayout then
child:DoLayout() child:DoLayout()
end end
@@ -778,28 +844,33 @@ AceGUI:RegisterLayout("Flow",
elseif child.width == "relative" then elseif child.width == "relative" then
child:SetWidth(width * child.relWidth) child:SetWidth(width * child.relWidth)
if child.OnWidthSet then
child:OnWidthSet(width)
end
if child.DoLayout then if child.DoLayout then
child:DoLayout() child:DoLayout()
end end
elseif oversize then elseif oversize then
if width > 1 then if width > 1 then
frame:SetPoint("RIGHT", content) frame:SetPoint("RIGHT",content,"RIGHT",0,0)
end end
end end
if child.height == "fill" then if child.height == "fill" then
frame:SetPoint("BOTTOM", content) frame:SetPoint("BOTTOM",content,"BOTTOM")
isfullheight = true isfullheight = true
end end
end end
--anchor the last row, if its full height needs a special case since its height has just been changed by the anchor --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 if isfullheight then
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -height) rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-height)
elseif rowstart then elseif rowstart then
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3)) rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
end end
height = height + rowheight + 3 height = height + rowheight + 3
safecall(content.obj.LayoutFinished, content.obj, nil, height) safecall( content.obj.LayoutFinished, content.obj, nil, height )
end) 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/ <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"> ..\FrameXML\UI.xsd">
<Script file="AceGUI-3.0.lua"/> <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-Button.lua"/>
<Script file="widgets\AceGUIWidget-CheckBox.lua"/> <Script file="widgets\AceGUIWidget-CheckBox.lua"/>
<Script file="widgets\AceGUIWidget-ColorPicker.lua"/> <Script file="widgets\AceGUIWidget-ColorPicker.lua"/>
<Script file="widgets\AceGUIWidget-DropDownGroup.lua"/>
<Script file="widgets\AceGUIWidget-DropDown.lua"/> <Script file="widgets\AceGUIWidget-DropDown.lua"/>
<Script file="widgets\AceGUIWidget-DropDown-Items.lua"/> <Script file="widgets\AceGUIWidget-DropDown-Items.lua"/>
<Script file="widgets\AceGUIWidget-EditBox.lua"/> <Script file="widgets\AceGUIWidget-EditBox.lua"/>
<Script file="widgets\AceGUIWidget-Frame.lua"/>
<Script file="widgets\AceGUIWidget-Window.lua"/>
<Script file="widgets\AceGUIWidget-Heading.lua"/> <Script file="widgets\AceGUIWidget-Heading.lua"/>
<Script file="widgets\AceGUIWidget-Icon.lua"/> <Script file="widgets\AceGUIWidget-InlineGroup.lua"/>
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
<Script file="widgets\AceGUIWidget-Keybinding.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-Label.lua"/>
<Script file="widgets\AceGUIWidget-MultiLineEditBox.lua"/> <Script file="widgets\AceGUIWidget-MultiLineEditBox.lua"/>
<Script file="widgets\AceGUIWidget-Slider.lua"/> <Script file="widgets\AceGUIWidget-BlizOptionsGroup.lua"/>
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
<Script file="widgets\AceGUIWidget-Icon.lua"/>
</Ui> </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