Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 068d4782a9 | |||
| 66eb505af9 | |||
| e53b3c1784 | |||
| 23b3a3b864 | |||
| 597f4030a7 | |||
| ebd76275a1 | |||
| 6addd9bb51 | |||
| abde71ceb0 | |||
| a984e705c3 | |||
| 1a80ca8ad8 | |||
| b4a2eb8bc2 | |||
| ae178d4a18 | |||
| e5ad71bc59 | |||
| b0ac94509a | |||
| 2e98622454 | |||
| e48bb6fc03 | |||
| a3b7b834b7 |
@@ -0,0 +1,3 @@
|
||||
/config.local
|
||||
/tmp
|
||||
/cache
|
||||
@@ -0,0 +1,8 @@
|
||||
[cache]
|
||||
type = "hardlink,copy"
|
||||
[core]
|
||||
remote = customization
|
||||
['remote "customization"']
|
||||
url = s3://warcraft-client/dvc/moonwell-client-sources/customization
|
||||
endpointurl = https://storage.yandexcloud.net
|
||||
region = ru-central1
|
||||
@@ -0,0 +1,3 @@
|
||||
# Add patterns of files dvc should ignore, which could improve
|
||||
# the performance. Learn more at
|
||||
# https://dvc.org/doc/user-guide/dvcignore
|
||||
@@ -2,6 +2,7 @@ WOW_HOME=C:\Games\World of Warcraft 3.3.5
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_SESSION_TOKEN=
|
||||
AWS_DEFAULT_REGION=
|
||||
AWS_BUCKET=
|
||||
AWS_ENDPOINT=
|
||||
@@ -10,3 +11,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
PRODUCTION_REALMLIST=
|
||||
PTR_REALMLIST=
|
||||
LOCAL_REALMLIST=
|
||||
|
||||
SSH_USER=root
|
||||
SSH_HOST=strictlypragmatic.ru
|
||||
|
||||
@@ -5,6 +5,8 @@ manifest.json
|
||||
.vscode
|
||||
dist
|
||||
build/
|
||||
/new customization mpqs/*.mpq
|
||||
Wow*.exe
|
||||
*.backup.exe
|
||||
Logs/
|
||||
!Wow_Original.exe
|
||||
|
||||
+18
-13
@@ -1,20 +1,25 @@
|
||||
[submodule "vendor/warcraftxl"]
|
||||
path = vendor/warcraftxl
|
||||
url = https://github.com/WarcraftXL/wxl-core.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-assets"]
|
||||
path = vendor/modules/wxl-modern-assets
|
||||
url = https://github.com/WarcraftXL/wxl-modern-assets.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-modern-render"]
|
||||
path = vendor/modules/wxl-modern-render
|
||||
url = https://github.com/WarcraftXL/wxl-modern-render.git
|
||||
branch = main
|
||||
[submodule "vendor/modules/wxl-unit-outline"]
|
||||
path = vendor/modules/wxl-unit-outline
|
||||
url = https://github.com/WarcraftXL/wxl-unit-outline.git
|
||||
branch = main
|
||||
branch = v1.1
|
||||
[submodule "vendor/modules/wxl-modern-adt"]
|
||||
path = vendor/modules/wxl-modern-adt
|
||||
url = https://github.com/WarcraftXL/wxl-modern-adt.git
|
||||
branch = main
|
||||
[submodule "vendor/warcraftxl"]
|
||||
path = vendor/warcraftxl
|
||||
url = https://github.com/WarcraftXL/wxl-core.git
|
||||
branch = v1.1
|
||||
[submodule "vendor/modules/wxl-db2"]
|
||||
path = vendor/modules/wxl-db2
|
||||
url = https://github.com/WarcraftXL/wxl-db2.git
|
||||
[submodule "vendor/modules/wxl-modern-m2"]
|
||||
path = vendor/modules/wxl-modern-m2
|
||||
url = https://github.com/sindoring/wxl-modern-m2.git
|
||||
branch = agent/guard-oversized-m2-batches
|
||||
[submodule "vendor/modules/wxl-modern-wmo"]
|
||||
path = vendor/modules/wxl-modern-wmo
|
||||
url = https://github.com/WarcraftXL/wxl-modern-wmo.git
|
||||
[submodule "vendor/modules/wxl-grasswind"]
|
||||
path = vendor/modules/wxl-grasswind
|
||||
url = https://github.com/WarcraftXL/wxl-grasswind.git
|
||||
|
||||
+65
-52
@@ -1,67 +1,80 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(MoonWellWarcraftXL LANGUAGES CXX)
|
||||
|
||||
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
|
||||
# its aggregate DLL target here, so updating the framework does not mix our
|
||||
# client-specific offsets and policy into the upstream checkout.
|
||||
# WarcraftXL is an untouched upstream v1.1 submodule. Every project-specific
|
||||
# feature below is built as a separately loaded Extensions/<name>/<name>.dll.
|
||||
add_subdirectory(vendor/warcraftxl)
|
||||
|
||||
set(WXL_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/warcraftxl")
|
||||
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
|
||||
set(WXL_LOCAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules")
|
||||
|
||||
function(wxl_collect_sources output)
|
||||
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
|
||||
set(${output} "${collected}" PARENT_SCOPE)
|
||||
function(wxl_add_external_extension extension_name extension_dir)
|
||||
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
return()
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE extension_sources CONFIGURE_DEPENDS "${extension_dir}/src/*.cpp")
|
||||
if(NOT extension_sources)
|
||||
message(FATAL_ERROR "WarcraftXL extension '${extension_name}' has no sources in ${extension_dir}/src")
|
||||
endif()
|
||||
|
||||
set(extension_shared_sources "")
|
||||
if(extension_name STREQUAL "wxl-modern-m2")
|
||||
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
|
||||
"${WXL_CORE_DIR}/src/engine/assets/shared/models/m2/*.cpp")
|
||||
elseif(extension_name STREQUAL "wxl-modern-blp")
|
||||
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
|
||||
"${WXL_CORE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
|
||||
endif()
|
||||
|
||||
file(GLOB wxl_sdk_sources CONFIGURE_DEPENDS "${WXL_CORE_DIR}/src/game/*.cpp")
|
||||
add_library(${extension_name} SHARED
|
||||
${extension_sources}
|
||||
${extension_shared_sources}
|
||||
${wxl_sdk_sources})
|
||||
set_target_properties(${extension_name} PROPERTIES
|
||||
OUTPUT_NAME "${extension_name}"
|
||||
PREFIX "")
|
||||
target_include_directories(${extension_name} PRIVATE
|
||||
"${extension_dir}/src"
|
||||
"${WXL_CORE_DIR}/include"
|
||||
"${WXL_CORE_DIR}/src")
|
||||
target_compile_features(${extension_name} PRIVATE cxx_std_20)
|
||||
target_compile_definitions(${extension_name} PRIVATE
|
||||
WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS WXL_EXTENSION)
|
||||
|
||||
if(CLIENT_PATH)
|
||||
add_custom_command(TARGET ${extension_name} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
"${CLIENT_PATH}/Extensions/${extension_name}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${extension_name}>"
|
||||
"${CLIENT_PATH}/Extensions/${extension_name}/${extension_name}.dll"
|
||||
COMMENT "Deploy ${extension_name}.dll -> Extensions/${extension_name}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if(TARGET WarcraftXL)
|
||||
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
|
||||
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||
endif()
|
||||
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
|
||||
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
|
||||
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||
# Official WarcraftXL 1.1 extensions.
|
||||
wxl_add_external_extension(wxl-db2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-db2")
|
||||
wxl_add_external_extension(wxl-modern-adt "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt")
|
||||
wxl_add_external_extension(wxl-modern-blp "${WXL_LOCAL_MODULES_DIR}/wxl-modern-blp")
|
||||
wxl_add_external_extension(wxl-modern-m2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-m2")
|
||||
wxl_add_external_extension(wxl-modern-wmo "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-wmo")
|
||||
wxl_add_external_extension(wxl-grasswind "${WXL_EXTERNAL_MODULES_DIR}/wxl-grasswind")
|
||||
wxl_add_external_extension(wxl-unit-outline "${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline")
|
||||
|
||||
target_sources(WarcraftXL PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
|
||||
${WXL_MODERN_ASSETS_RUNTIME}
|
||||
${WXL_MODERN_RENDER_RUNTIME}
|
||||
${WXL_UNIT_OUTLINE_RUNTIME}
|
||||
${WXL_MODERN_ADT_RUNTIME})
|
||||
|
||||
# wxl-modern-render needs its module-root includes and D3D12 import library.
|
||||
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
|
||||
# MoonWell-owned extensions. wxl-fdid-moonwell sorts after wxl-db2 and before
|
||||
# the modern asset extensions, so it can layer custom CSV mappings over wxl.fdid.
|
||||
wxl_add_external_extension(MoonWell "${WXL_LOCAL_MODULES_DIR}/moonwell")
|
||||
if(TARGET MoonWell)
|
||||
target_link_libraries(MoonWell PRIVATE shell32)
|
||||
endif()
|
||||
wxl_add_external_extension(wxl-fdid-moonwell "${WXL_LOCAL_MODULES_DIR}/moonwell-fdid")
|
||||
wxl_add_external_extension(wxl-moonwell-storage-fallback
|
||||
"${WXL_LOCAL_MODULES_DIR}/moonwell-storage-fallback")
|
||||
|
||||
if(TARGET WarcraftXLHost)
|
||||
wxl_collect_sources(WXL_MOONWELL_HOST
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/host/*.cpp")
|
||||
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
|
||||
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||
endif()
|
||||
wxl_collect_sources(WXL_MODERN_ADT_HOST
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||
|
||||
target_sources(WarcraftXLHost PRIVATE
|
||||
${WXL_MOONWELL_HOST}
|
||||
${WXL_MODERN_ASSETS_HOST}
|
||||
${WXL_MODERN_ADT_HOST})
|
||||
endif()
|
||||
|
||||
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
|
||||
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
|
||||
# Recovery loader retained for existing -NativeRenderer workflows. WarcraftXL
|
||||
# 1.1 already ships a native D3D9 forwarding proxy as its standard renderer.
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
add_library(MoonWellLoader SHARED
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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 запуске.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
|
||||
|
||||
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL): `Wow.exe` больше не содержит
|
||||
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL/wxl-core) **1.1.220**: `Wow.exe` больше не содержит
|
||||
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
|
||||
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
|
||||
только к памяти запущенного процесса.
|
||||
@@ -14,25 +14,25 @@ MoonWell-патчей. Неподписанный оригинальный execu
|
||||
- одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`;
|
||||
- загрузка WarcraftXL без import-table patch и без отдельного injector.
|
||||
|
||||
Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому
|
||||
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает
|
||||
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
|
||||
При установке сборщик включает Large Address Aware в копии `Wow.exe`; исходный
|
||||
`Wow_Original.exe` остаётся неизменным и используется как проверенный шаблон.
|
||||
|
||||
## Структура
|
||||
|
||||
```text
|
||||
modules/moonwell/ MoonWell runtime-модуль WarcraftXL
|
||||
modules/ отдельные MoonWell extensions для WarcraftXL 1.1
|
||||
vendor/warcraftxl/ закреплённый upstream git submodule
|
||||
vendor/modules/ закреплённые модули WarcraftXL
|
||||
src/Data/ исходники MPQ-патчей
|
||||
tool/ сборщик MPQ
|
||||
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host
|
||||
build-warcraftxl.ps1 сборка Win32 runtime, proxy и extensions
|
||||
run.ps1 полная сборка, установка и запуск клиента
|
||||
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
|
||||
```
|
||||
|
||||
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и
|
||||
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule.
|
||||
Подключены совместимые с 1.1 extensions: `wxl-db2`, `wxl-modern-adt`, `wxl-modern-m2`,
|
||||
`wxl-modern-wmo`, `wxl-grasswind` и `wxl-unit-outline`. BLP-адаптер и MoonWell-изменения
|
||||
собираются как отдельные DLL; исходники upstream core не изменяются.
|
||||
Для обновления всех зависимостей WarcraftXL:
|
||||
|
||||
```powershell
|
||||
@@ -44,9 +44,11 @@ git submodule update --remote vendor/warcraftxl vendor/modules/*
|
||||
|
||||
## Требования
|
||||
|
||||
- Visual Studio 2022 с C++ toolchain для Win32 и x64;
|
||||
- Visual Studio 2022 с C++ toolchain для Win32;
|
||||
- CMake 3.20+;
|
||||
- Rust/Cargo для существующего MPQ-сборщика;
|
||||
- Python 3 и DVC с поддержкой S3 для содержимого внешних графических патчей
|
||||
(`py -m pip install -r requirements-dvc.txt`);
|
||||
- локальный оригинальный `Wow_Original.exe` с SHA-256
|
||||
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
|
||||
|
||||
@@ -64,7 +66,7 @@ git submodule update --init --recursive
|
||||
.\deploy.ps1
|
||||
```
|
||||
|
||||
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host,
|
||||
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, native D3D9 proxy и extensions,
|
||||
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
|
||||
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
|
||||
|
||||
@@ -77,8 +79,6 @@ git submodule update --init --recursive
|
||||
# Быстро обновить только WarcraftXL без пересборки MPQ
|
||||
.\deploy.ps1 -SkipDataBuild
|
||||
|
||||
# Аварийный native D3D9 вместо D3D9On12
|
||||
.\deploy.ps1 -NativeRenderer
|
||||
```
|
||||
|
||||
Без установки в клиент:
|
||||
@@ -99,18 +99,8 @@ git submodule update --init --recursive
|
||||
1. проверяет хеш `Wow_Original.exe`;
|
||||
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
|
||||
3. восстанавливает оригинальный `Wow.exe`;
|
||||
4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`;
|
||||
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`;
|
||||
6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
|
||||
|
||||
Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом
|
||||
модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и
|
||||
установить native-переходник без modern-render:
|
||||
|
||||
```powershell
|
||||
.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer `
|
||||
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
|
||||
```
|
||||
4. устанавливает `WarcraftXL.dll`, native D3D9 proxy и DLL в `Extensions\`;
|
||||
5. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
|
||||
|
||||
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
|
||||
|
||||
@@ -121,8 +111,13 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
|
||||
.\run.ps1
|
||||
```
|
||||
|
||||
Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и
|
||||
`Utils\WarcraftXLHost.log` внутри клиента.
|
||||
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
|
||||
|
||||
## Авторизация через лаунчер
|
||||
|
||||
Release-клиент принимает только одноразовую launcher-сессию и при прямом запуске показывает
|
||||
кнопку открытия лаунчера вместо полей логина и пароля. Контракт переменных запуска, требования
|
||||
к auth-серверу и Debug-режим описаны в [LAUNCHER_AUTH.md](LAUNCHER_AUTH.md).
|
||||
|
||||
## MPQ-пакеты
|
||||
|
||||
@@ -136,8 +131,18 @@ WarcraftXL не требуется и в объектное хранилище
|
||||
- `patch-ruRU-5` — русская локализация и интерфейс;
|
||||
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
|
||||
|
||||
Управляемые репозиторием MPQ и внешние графические патчи перечислены отдельно в
|
||||
`patch-layout.json`. Сборка и deploy синхронизируют только `repositoryPatches` и
|
||||
не изменяют `graphicsPatches`. При пересечении списков репозиторный патч имеет
|
||||
приоритет.
|
||||
|
||||
Распакованное содержимое больших патчей кастомизации хранится через DVC в
|
||||
`assets/graphics/`, а бинарные объекты — в настроенном S3. Инструкции по первичной
|
||||
загрузке, изменению и импорту MPQ находятся в
|
||||
[`assets/graphics/README.md`](assets/graphics/README.md).
|
||||
|
||||
## Лицензирование
|
||||
|
||||
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный upstream submodule. MoonWell-модуль,
|
||||
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным
|
||||
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`.
|
||||
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
|
||||
MoonWell extensions распространяются с доступным соответствующим исходным кодом. Репозиторий не
|
||||
должен публиковать Blizzard assets или `Wow.exe`.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
/patch-X
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 012ce72d8dfdf089b20a28f6e6158b58.dir
|
||||
size: 1195310
|
||||
nfiles: 4
|
||||
hash: md5
|
||||
path: patch-X
|
||||
@@ -0,0 +1,14 @@
|
||||
/patch-ruRU-X
|
||||
/patch-ruRU-Y
|
||||
/backup-ruRU
|
||||
/patch-ruRU-6
|
||||
/patch-ruRU-A
|
||||
/patch-ruRU-B
|
||||
/patch-ruRU-E
|
||||
/patch-ruRU-H
|
||||
/patch-ruRU-I
|
||||
/patch-ruRU-M
|
||||
/patch-ruRU-S
|
||||
/patch-ruRU-T
|
||||
/patch-ruRU-U
|
||||
/patch-ruRU-W
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: b120587dfcd255f78392ca52705a00ff.dir
|
||||
size: 38876467
|
||||
nfiles: 161
|
||||
hash: md5
|
||||
path: backup-ruRU
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 9ca2ee746d80e8a0c951c5ef476ae654.dir
|
||||
size: 1064210
|
||||
nfiles: 3
|
||||
hash: md5
|
||||
path: patch-ruRU-6
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 97232ae859eaa1a16b137efd1a2e9122.dir
|
||||
size: 2748988973
|
||||
nfiles: 8817
|
||||
hash: md5
|
||||
path: patch-ruRU-A
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: bb686aebf7c9cae2c02433abde9a9f19.dir
|
||||
size: 771051083
|
||||
nfiles: 2911
|
||||
hash: md5
|
||||
path: patch-ruRU-B
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: d6b2fe0e3f23c47dc944ca4a8f82d97a.dir
|
||||
size: 9275755440
|
||||
nfiles: 21977
|
||||
hash: md5
|
||||
path: patch-ruRU-E
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 5e95536d2c6f6ff792f05b800105bedc.dir
|
||||
size: 3727395837
|
||||
nfiles: 40296
|
||||
hash: md5
|
||||
path: patch-ruRU-H
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 5db07768b0814c970d84a99b6072fd9f.dir
|
||||
size: 31453961
|
||||
nfiles: 186
|
||||
hash: md5
|
||||
path: patch-ruRU-I
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: e927ae98c9971f378a82feb3299fdcb8.dir
|
||||
size: 69521651
|
||||
nfiles: 1552
|
||||
hash: md5
|
||||
path: patch-ruRU-M
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 449383bbe05ebebb6bad2c37da7793c4.dir
|
||||
size: 142167464
|
||||
nfiles: 1751
|
||||
hash: md5
|
||||
path: patch-ruRU-S
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: fc19500b5ce0fd312a415a3118afd58d.dir
|
||||
size: 656858260
|
||||
nfiles: 1810
|
||||
hash: md5
|
||||
path: patch-ruRU-T
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 783909a6c2ebed5d4f43516fe1ea68bb.dir
|
||||
size: 460642379
|
||||
nfiles: 1837
|
||||
hash: md5
|
||||
path: patch-ruRU-U
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: e2a1448eb0e443797acd577fc4b67249.dir
|
||||
size: 44597910
|
||||
nfiles: 484
|
||||
hash: md5
|
||||
path: patch-ruRU-W
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 3d322d290959d1ead612f3b76860a1ea.dir
|
||||
size: 3984134962
|
||||
nfiles: 29495
|
||||
hash: md5
|
||||
path: patch-ruRU-X
|
||||
@@ -0,0 +1,6 @@
|
||||
outs:
|
||||
- md5: 42f3a8ac35674e33c0a4420c84cadcb8.dir
|
||||
size: 11053212781
|
||||
nfiles: 36191
|
||||
hash: md5
|
||||
path: patch-ruRU-Y
|
||||
@@ -0,0 +1,66 @@
|
||||
# Графические MPQ-патчи
|
||||
|
||||
В этой директории через DVC версионируется распакованное содержимое всех 15 патчей из
|
||||
`graphicsPatches` в `patch-layout.json`. Git хранит небольшие файлы `*.dvc`, а сами ресурсы —
|
||||
в S3 remote `customization`. Хэшированные пути в S3 являются внутренней адресацией DVC:
|
||||
исходное дерево с обычными именами восстанавливается командой `dvc pull`.
|
||||
|
||||
## Первичная настройка
|
||||
|
||||
```powershell
|
||||
py -m pip install -r requirements-dvc.txt
|
||||
Copy-Item .env.example .env # если локального .env ещё нет
|
||||
.\configure-dvc.ps1
|
||||
py -m dvc pull
|
||||
```
|
||||
|
||||
Можно скачать один патч, передав его указатель явно:
|
||||
|
||||
```powershell
|
||||
py -m dvc pull assets/graphics/Data/ruRU/patch-ruRU-X.dvc
|
||||
```
|
||||
|
||||
Секреты записываются только в `.dvc/config.local`; этот файл исключён из Git. Вместо ключей
|
||||
из `.env` можно использовать AWS profile: `.\configure-dvc.ps1 -Profile moonwell`.
|
||||
|
||||
## Изменение ресурсов
|
||||
|
||||
DVC-кэш использует hardlink на NTFS. Перед ручным изменением дерева отвяжите его от кэша:
|
||||
|
||||
```powershell
|
||||
py -m dvc unprotect assets/graphics/Data/ruRU/patch-ruRU-X
|
||||
# изменить, добавить или удалить файлы
|
||||
py -m dvc add assets/graphics/Data/ruRU/patch-ruRU-X
|
||||
py -m dvc push
|
||||
git add assets/graphics/Data/ruRU/patch-ruRU-X.dvc
|
||||
```
|
||||
|
||||
Сначала выполняйте `dvc push`, затем коммитьте Git-указатель, чтобы коммит не ссылался на ещё
|
||||
не загруженные объекты.
|
||||
|
||||
## Импорт MPQ
|
||||
|
||||
`mpqextract` принимает встроенный `(listfile)` и любое количество внешних listfile. Он извлекает
|
||||
все известные пути, а блоки с неизвестными именами сохраняет в `__mpqmeta__/unresolved/`.
|
||||
Манифест `__mpqmeta__/unresolved.json` связывает каждый blob с MPQ-хэшами и индексом блока,
|
||||
поэтому содержимое архива не теряется даже при неполном listfile.
|
||||
|
||||
```powershell
|
||||
cargo run --release --manifest-path tool/Cargo.toml --bin mpqextract -- `
|
||||
'patch.mpq' 'assets/graphics/Data/ruRU/patch-ruRU-N' 'master-list.txt'
|
||||
```
|
||||
|
||||
В `patch-ruRU-A`, `B`, `S`, `T`, `U` и `W` есть такие безымянные блоки. Для их безопасной
|
||||
пересборки нужен авторитетный listfile с исходными путями; пока он не найден, исходные MPQ в
|
||||
дистрибуционном S3 остаются каноническими архивами. `mpqpack` намеренно отказывается собирать
|
||||
дерево с `__mpqmeta__`, поскольку подмена путей хэшами изменила бы семантику патча.
|
||||
|
||||
Полностью разрешённое дерево собирается так:
|
||||
|
||||
```powershell
|
||||
cargo run --release --manifest-path tool/Cargo.toml --bin mpqpack -- `
|
||||
'assets/graphics/Data/ruRU/patch-ruRU-X' 'build/graphics/patch-ruRU-X.MPQ'
|
||||
```
|
||||
|
||||
Сводка исходных объектов, DVC-хэшей и количества разрешённых блоков находится в
|
||||
`assets/graphics/sources.json`.
|
||||
@@ -0,0 +1,157 @@
|
||||
{
|
||||
"sourcePrefix": "s3://warcraft-client/World of Warcraft/",
|
||||
"patches": [
|
||||
{
|
||||
"archive": "Data/patch-X.MPQ",
|
||||
"archiveBytes": 496584,
|
||||
"dvcPointer": "assets/graphics/Data/patch-X.dvc",
|
||||
"dvcMd5": "012ce72d8dfdf089b20a28f6e6158b58.dir",
|
||||
"extractedBytes": 1195310,
|
||||
"trackedFiles": 4,
|
||||
"resolvedFiles": 4,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/backup-ruRU.MPQ",
|
||||
"archiveBytes": 23981093,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/backup-ruRU.dvc",
|
||||
"dvcMd5": "b120587dfcd255f78392ca52705a00ff.dir",
|
||||
"extractedBytes": 38876467,
|
||||
"trackedFiles": 161,
|
||||
"resolvedFiles": 161,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-6.MPQ",
|
||||
"archiveBytes": 375062,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-6.dvc",
|
||||
"dvcMd5": "9ca2ee746d80e8a0c951c5ef476ae654.dir",
|
||||
"extractedBytes": 1064210,
|
||||
"trackedFiles": 3,
|
||||
"resolvedFiles": 3,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-A.mpq",
|
||||
"archiveBytes": 1451338841,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-A.dvc",
|
||||
"dvcMd5": "97232ae859eaa1a16b137efd1a2e9122.dir",
|
||||
"extractedBytes": 2748988973,
|
||||
"trackedFiles": 8817,
|
||||
"resolvedFiles": 5649,
|
||||
"unresolvedBlocks": 3167
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-B.mpq",
|
||||
"archiveBytes": 384938550,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-B.dvc",
|
||||
"dvcMd5": "bb686aebf7c9cae2c02433abde9a9f19.dir",
|
||||
"extractedBytes": 771051083,
|
||||
"trackedFiles": 2911,
|
||||
"resolvedFiles": 1679,
|
||||
"unresolvedBlocks": 1231
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-E.mpq",
|
||||
"archiveBytes": 1862937450,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-E.dvc",
|
||||
"dvcMd5": "d6b2fe0e3f23c47dc944ca4a8f82d97a.dir",
|
||||
"extractedBytes": 9275755440,
|
||||
"trackedFiles": 21977,
|
||||
"resolvedFiles": 21977,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-H.MPQ",
|
||||
"archiveBytes": 2154531136,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-H.dvc",
|
||||
"dvcMd5": "5e95536d2c6f6ff792f05b800105bedc.dir",
|
||||
"extractedBytes": 3727395837,
|
||||
"trackedFiles": 40296,
|
||||
"resolvedFiles": 40296,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-I.mpq",
|
||||
"archiveBytes": 14891516,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-I.dvc",
|
||||
"dvcMd5": "5db07768b0814c970d84a99b6072fd9f.dir",
|
||||
"extractedBytes": 31453961,
|
||||
"trackedFiles": 186,
|
||||
"resolvedFiles": 186,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-M.MPQ",
|
||||
"archiveBytes": 48772900,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-M.dvc",
|
||||
"dvcMd5": "e927ae98c9971f378a82feb3299fdcb8.dir",
|
||||
"extractedBytes": 69521651,
|
||||
"trackedFiles": 1552,
|
||||
"resolvedFiles": 1552,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-S.mpq",
|
||||
"archiveBytes": 80492633,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-S.dvc",
|
||||
"dvcMd5": "449383bbe05ebebb6bad2c37da7793c4.dir",
|
||||
"extractedBytes": 142167464,
|
||||
"trackedFiles": 1751,
|
||||
"resolvedFiles": 1432,
|
||||
"unresolvedBlocks": 318
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-T.mpq",
|
||||
"archiveBytes": 418595462,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-T.dvc",
|
||||
"dvcMd5": "fc19500b5ce0fd312a415a3118afd58d.dir",
|
||||
"extractedBytes": 656858260,
|
||||
"trackedFiles": 1810,
|
||||
"resolvedFiles": 1731,
|
||||
"unresolvedBlocks": 78
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-U.mpq",
|
||||
"archiveBytes": 233678341,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-U.dvc",
|
||||
"dvcMd5": "783909a6c2ebed5d4f43516fe1ea68bb.dir",
|
||||
"extractedBytes": 460642379,
|
||||
"trackedFiles": 1837,
|
||||
"resolvedFiles": 1372,
|
||||
"unresolvedBlocks": 464
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-W.mpq",
|
||||
"archiveBytes": 29433979,
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-W.dvc",
|
||||
"dvcMd5": "e2a1448eb0e443797acd577fc4b67249.dir",
|
||||
"extractedBytes": 44597910,
|
||||
"trackedFiles": 484,
|
||||
"resolvedFiles": 381,
|
||||
"unresolvedBlocks": 102
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-X.MPQ",
|
||||
"archiveBytes": 2673854694,
|
||||
"archiveSha256": "CD8CED6FA6C863D509DAE458E6DD44C3986B8B57731B6DC6192FE94125F8C1FD",
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-X.dvc",
|
||||
"dvcMd5": "3d322d290959d1ead612f3b76860a1ea.dir",
|
||||
"extractedBytes": 3984134962,
|
||||
"trackedFiles": 29495,
|
||||
"resolvedFiles": 29495,
|
||||
"unresolvedBlocks": 0
|
||||
},
|
||||
{
|
||||
"archive": "Data/ruRU/patch-ruRU-Y.MPQ",
|
||||
"archiveBytes": 3001665276,
|
||||
"archiveSha256": "534D496C1A1EE023B910ABC80B727BA88BB09CD2F5767100FC18EE41F4306B90",
|
||||
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-Y.dvc",
|
||||
"dvcMd5": "42f3a8ac35674e33c0a4420c84cadcb8.dir",
|
||||
"extractedBytes": 11053212781,
|
||||
"trackedFiles": 36191,
|
||||
"resolvedFiles": 36191,
|
||||
"unresolvedBlocks": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
+70
-31
@@ -75,8 +75,53 @@ function Enable-LargeAddressAware {
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-MissingSubmodules {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepositoryPath
|
||||
)
|
||||
|
||||
$modulesFile = Join-Path $RepositoryPath '.gitmodules'
|
||||
if (-not (Test-Path -LiteralPath $modulesFile -PathType Leaf)) {
|
||||
return
|
||||
}
|
||||
|
||||
$configuredModules = @(
|
||||
& git -C $RepositoryPath config --file .gitmodules --get-regexp '^submodule\..*\.path$'
|
||||
)
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
throw "Failed to read submodules from $modulesFile"
|
||||
}
|
||||
|
||||
foreach ($configuredModule in $configuredModules) {
|
||||
$fields = $configuredModule -split '\s+', 2
|
||||
if ($fields.Count -ne 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
$relativePath = $fields[1].Trim()
|
||||
$modulePath = Join-Path $RepositoryPath $relativePath
|
||||
$gitMarker = Join-Path $modulePath '.git'
|
||||
|
||||
# Updating an initialized submodule checks out the SHA recorded by its parent and detaches
|
||||
# any active development branch. Only initialize genuinely missing worktrees.
|
||||
if (-not (Test-Path -LiteralPath $gitMarker)) {
|
||||
Write-Host "Initializing missing submodule: $modulePath"
|
||||
& git -C $RepositoryPath submodule update --init -- $relativePath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to initialize submodule: $modulePath"
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $modulePath -PathType Container) {
|
||||
Initialize-MissingSubmodules -RepositoryPath $modulePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = $PSScriptRoot
|
||||
. (Join-Path $repoRoot 'pipeline-layout.ps1')
|
||||
$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
|
||||
if (-not (Test-Path -LiteralPath $cmake)) {
|
||||
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
|
||||
@@ -84,11 +129,9 @@ if (-not (Test-Path -LiteralPath $cmake)) {
|
||||
$cmake = $cmakeCommand.Source
|
||||
}
|
||||
|
||||
& git -C $repoRoot submodule update --init --recursive
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
|
||||
Initialize-MissingSubmodules -RepositoryPath $repoRoot
|
||||
|
||||
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
|
||||
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
|
||||
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
|
||||
|
||||
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||
@@ -104,22 +147,18 @@ if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||
|
||||
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
|
||||
& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader
|
||||
& $cmake --build $win32BuildDir --config $Configuration --parallel 4
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
|
||||
|
||||
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
|
||||
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
|
||||
|
||||
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
|
||||
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
|
||||
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
|
||||
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
|
||||
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
|
||||
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
|
||||
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
|
||||
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
|
||||
$selectedProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
|
||||
$recoveryProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
|
||||
$extensionNames = Get-MoonWellExtensionNames
|
||||
|
||||
if ($NativeRenderer) {
|
||||
Write-Warning '-NativeRenderer is no longer needed: WarcraftXL 1.1 uses native D3D9 by default.'
|
||||
}
|
||||
|
||||
function Install-WarcraftXLArtifacts {
|
||||
param(
|
||||
@@ -127,12 +166,12 @@ function Install-WarcraftXLArtifacts {
|
||||
[string]$Destination
|
||||
)
|
||||
|
||||
$utils = Join-Path $Destination 'Utils'
|
||||
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
|
||||
Remove-MoonWellRuntimeGarbage -Destination $Destination
|
||||
|
||||
$utils = Join-Path $Destination 'Utils'
|
||||
New-Item `
|
||||
-ItemType Directory `
|
||||
-Path $Destination, $utils, $loosePatch `
|
||||
-Path $Destination, $utils `
|
||||
-Force |
|
||||
Out-Null
|
||||
|
||||
@@ -156,20 +195,17 @@ function Install-WarcraftXLArtifacts {
|
||||
-Force
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $nativeProxy `
|
||||
-LiteralPath $recoveryProxy `
|
||||
-Destination (Join-Path $utils 'd3d9-native.dll') `
|
||||
-Force
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $hostExe `
|
||||
-Destination (Join-Path $utils 'WarcraftXLHost.exe') `
|
||||
-Force
|
||||
|
||||
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
|
||||
foreach ($extensionName in $extensionNames) {
|
||||
$extensionSource = Join-Path $win32BuildDir "$Configuration\$extensionName.dll"
|
||||
$extensionDestination = Join-Path $Destination "Extensions\$extensionName"
|
||||
New-Item -ItemType Directory -Path $extensionDestination -Force | Out-Null
|
||||
Copy-Item `
|
||||
-LiteralPath $_.FullName `
|
||||
-Destination $loosePatch `
|
||||
-Recurse `
|
||||
-LiteralPath $extensionSource `
|
||||
-Destination (Join-Path $extensionDestination "$extensionName.dll") `
|
||||
-Force
|
||||
}
|
||||
}
|
||||
@@ -196,14 +232,17 @@ if ($Deploy) {
|
||||
}
|
||||
|
||||
Install-WarcraftXLArtifacts -Destination $ClientPath
|
||||
Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath"
|
||||
Write-Host "Installed WarcraftXL 1.1 runtime and extensions in $ClientPath"
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $repoRoot `
|
||||
-ClientPath $ClientPath
|
||||
Install-WarcraftXLArtifacts -Destination $PackagePath
|
||||
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
|
||||
}
|
||||
|
||||
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
|
||||
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
|
||||
Write-Host "WarcraftXL 1.1 build complete (native D3D9 proxy): $win32ArtifactDir"
|
||||
|
||||
@@ -9,6 +9,8 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from patch_layout import PATCH_LAYOUT
|
||||
|
||||
|
||||
IGNORED_TOP_LEVEL_DIRS = {
|
||||
"Cache",
|
||||
@@ -49,6 +51,8 @@ def is_ignored(rel_path: Path) -> bool:
|
||||
return True
|
||||
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
|
||||
return True
|
||||
if PATCH_LAYOUT.is_external_graphics_patch(rel_path.as_posix()):
|
||||
return True
|
||||
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
param(
|
||||
[string]$EnvFile = (Join-Path $PSScriptRoot '.env'),
|
||||
[string]$Profile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-EnvValue([string]$Path, [string]$Name) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$line = Get-Content -LiteralPath $Path |
|
||||
Where-Object { $_ -match "^$([regex]::Escape($Name))=" } |
|
||||
Select-Object -First 1
|
||||
if ($line) {
|
||||
return $line.Substring($Name.Length + 1).Trim()
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Invoke-Dvc([string[]]$Arguments) {
|
||||
& py -m dvc @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "DVC command failed: dvc $($Arguments -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot '.dvc\config'))) {
|
||||
throw 'DVC is not initialized in this repository.'
|
||||
}
|
||||
|
||||
& py -c 'import dvc, dvc_s3' *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'DVC with S3 support is not installed. Run: py -m pip install -r requirements-dvc.txt'
|
||||
}
|
||||
|
||||
if ($Profile) {
|
||||
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'profile', $Profile)
|
||||
} else {
|
||||
$accessKey = Get-EnvValue $EnvFile 'AWS_ACCESS_KEY_ID'
|
||||
$secretKey = Get-EnvValue $EnvFile 'AWS_SECRET_ACCESS_KEY'
|
||||
$sessionToken = Get-EnvValue $EnvFile 'AWS_SESSION_TOKEN'
|
||||
|
||||
if (-not $accessKey -or -not $secretKey) {
|
||||
throw "AWS credentials are missing in $EnvFile. Pass -Profile or fill AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY."
|
||||
}
|
||||
|
||||
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'access_key_id', $accessKey)
|
||||
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'secret_access_key', $secretKey)
|
||||
if ($sessionToken) {
|
||||
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'session_token', $sessionToken)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host 'Local DVC credentials configured. They are stored in ignored .dvc/config.local.'
|
||||
@@ -10,12 +10,5 @@
|
||||
"pet_template_display_id": 5448
|
||||
},
|
||||
"mounts": [],
|
||||
"pets": [
|
||||
{
|
||||
"folder": "src/Data/patch-Z/Creature/Catslime",
|
||||
"name": "Cat Slime",
|
||||
"name_ru": "Котослизень",
|
||||
"display_scale": 1.0
|
||||
}
|
||||
]
|
||||
"pets": []
|
||||
}
|
||||
|
||||
+33
-11
@@ -13,6 +13,7 @@ param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = $PSScriptRoot
|
||||
. (Join-Path $repoRoot 'pipeline-layout.ps1')
|
||||
|
||||
function Import-ProjectEnvironment {
|
||||
$envFile = Join-Path $repoRoot '.env'
|
||||
@@ -46,6 +47,10 @@ $PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
|
||||
throw "Client directory was not found: $ClientPath"
|
||||
}
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $repoRoot `
|
||||
-ClientPath $ClientPath
|
||||
|
||||
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
@@ -58,20 +63,27 @@ if ($runningClients.Count) {
|
||||
Write-Host 'Stopping the running WoW client...'
|
||||
$runningClients | Stop-Process -Force
|
||||
Start-Sleep -Seconds 2
|
||||
Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} | Stop-Process -Force
|
||||
}
|
||||
|
||||
$env:WOW_HOME = $ClientPath
|
||||
Write-Host "MoonWell client: $ClientPath"
|
||||
Write-Host "Package staging: $PackagePath"
|
||||
|
||||
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
|
||||
& git -C $repoRoot submodule update --init --recursive
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
|
||||
Write-Host '[1/5] Preserving initialized WarcraftXL worktrees...'
|
||||
|
||||
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||
$previousPackageFiles = @(
|
||||
Get-RelativeFilePaths -Root $PackagePath
|
||||
)
|
||||
if (-not $SkipDataBuild) {
|
||||
Write-Host 'Resetting package staging to prevent stale build artifacts...'
|
||||
Reset-MoonWellPackageStaging `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $repoRoot `
|
||||
-ClientPath $ClientPath
|
||||
} else {
|
||||
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||
Remove-MoonWellRuntimeGarbage -Destination $PackagePath
|
||||
}
|
||||
|
||||
if (-not $SkipDataBuild) {
|
||||
Write-Host '[2/5] Building MPQ patches...'
|
||||
@@ -102,6 +114,18 @@ if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
|
||||
|
||||
Write-Host '[4/5] Synchronizing package files...'
|
||||
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $python) { throw 'Python was not found; package validation cannot run.' }
|
||||
& $python.Source (Join-Path $repoRoot 'validate_package.py') --dir $PackagePath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Package validation failed.' }
|
||||
|
||||
$currentPackageFiles = @(Get-RelativeFilePaths -Root $PackagePath)
|
||||
Remove-StalePackageFilesFromClient `
|
||||
-ClientPath $ClientPath `
|
||||
-PreviousFiles $previousPackageFiles `
|
||||
-CurrentFiles $currentPackageFiles
|
||||
Remove-MoonWellRuntimeGarbage -Destination $ClientPath
|
||||
|
||||
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
|
||||
$robocopyExitCode = $LASTEXITCODE
|
||||
if ($robocopyExitCode -ge 8) {
|
||||
@@ -110,8 +134,6 @@ if ($robocopyExitCode -ge 8) {
|
||||
|
||||
if (-not $SkipManifest) {
|
||||
Write-Host '[5/5] Building launcher manifest...'
|
||||
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $python) { throw 'Python was not found; launcher manifest was not generated.' }
|
||||
& $python.Source (Join-Path $repoRoot 'build_manifest.py') `
|
||||
--dir $PackagePath --output (Join-Path $repoRoot 'manifest.json')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' }
|
||||
@@ -120,12 +142,12 @@ if (-not $SkipManifest) {
|
||||
}
|
||||
|
||||
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
|
||||
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' }
|
||||
$renderer = 'WarcraftXL 1.1 native D3D9 proxy'
|
||||
Write-Host ''
|
||||
Write-Host 'Deployment complete.'
|
||||
Write-Host " Renderer: $renderer"
|
||||
Write-Host " Wow.exe SHA-256: $wowHash"
|
||||
Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')"
|
||||
Write-Host " Extensions: $(Join-Path $ClientPath 'Extensions')"
|
||||
|
||||
if ($Launch) {
|
||||
Write-Host 'Launching Wow.exe...'
|
||||
|
||||
+2
-3
@@ -50,9 +50,8 @@ extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** outp
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
// Compatibility exports referenced by the current WarcraftXL core. MoonWell
|
||||
// does not enable its D3D9On12 backend, so these intentionally report no D3D12
|
||||
// device and a neutral supersampling factor.
|
||||
// Legacy compatibility exports retained for the recovery proxy. WarcraftXL 1.1
|
||||
// uses native D3D9 and ignores the former D3D9On12 bridge.
|
||||
extern "C" void* WxlD3D12Device() { return nullptr; }
|
||||
extern "C" void* WxlD3D12Queue() { return nullptr; }
|
||||
extern "C" void WxlD3D12DrainDebug() {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,326 @@
|
||||
// changes are verified and applied to the process image during WarcraftXL's
|
||||
// boot phase, before GlueXML is loaded.
|
||||
|
||||
#include "core/Logger.hpp"
|
||||
#include "core/Hook.hpp"
|
||||
#include "core/Mem.hpp"
|
||||
#include "runtime/LuaBindings.hpp"
|
||||
#include "runtime/ModuleInstall.hpp"
|
||||
#include "offsets/engine/Gx.hpp"
|
||||
#include "game/Script.hpp"
|
||||
#include "wxl/PluginApi.h"
|
||||
#include "SpellOverrides.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdarg>
|
||||
#include <cwchar>
|
||||
|
||||
namespace moonwell
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const WXL_Api* g_api = nullptr;
|
||||
|
||||
void Log(int level, const char* format, ...)
|
||||
{
|
||||
if (!g_api || !g_api->Log) return;
|
||||
char message[1024]{};
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args);
|
||||
va_end(args);
|
||||
g_api->Log(level, "MoonWell", "%s", message);
|
||||
}
|
||||
|
||||
#define WLOG_INFO(...) Log(WXL_LOG_INFO, __VA_ARGS__)
|
||||
#define WLOG_ERROR(...) Log(WXL_LOG_ERROR, __VA_ARGS__)
|
||||
|
||||
bool PatchMemory(void* destination, const void* source, size_t size)
|
||||
{
|
||||
DWORD oldProtection = 0;
|
||||
if (!VirtualProtect(destination, size, PAGE_EXECUTE_READWRITE, &oldProtection))
|
||||
return false;
|
||||
std::memcpy(destination, source, size);
|
||||
FlushInstructionCache(GetCurrentProcess(), destination, size);
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(destination, size, oldProtection, &ignored);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class Fn>
|
||||
bool Hook(const char* name, uintptr_t address, Fn* detour, Fn** original)
|
||||
{
|
||||
return g_api && g_api->HookAttach && g_api->HookAttach(
|
||||
name, address, reinterpret_cast<void*>(detour),
|
||||
reinterpret_cast<void**>(original), WXL_HOOK_DEFAULT_PRIORITY) != 0;
|
||||
}
|
||||
|
||||
template <class Fn>
|
||||
bool HookByName(const char* point, Fn* detour, Fn** original)
|
||||
{
|
||||
return g_api && g_api->HookAttachByName && g_api->HookAttachByName(
|
||||
point, reinterpret_cast<void*>(detour), reinterpret_cast<void**>(original),
|
||||
WXL_HOOK_DEFAULT_PRIORITY) != 0;
|
||||
}
|
||||
|
||||
constexpr uint32_t kTraitorFlag = 0x40000000u;
|
||||
bool g_loginCharacterIsTraitor = false;
|
||||
|
||||
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
|
||||
constexpr char kLaunchAccountVariable[] = "MOONWELL_LAUNCH_ACCOUNT";
|
||||
constexpr char kLaunchTicketVariable[] = "MOONWELL_LAUNCH_TICKET";
|
||||
constexpr char kDeveloperLoginVariable[] = "MOONWELL_DEV_LOGIN";
|
||||
constexpr char kLauncherLoginMarker[] = "__MOONWELL_LAUNCHER__";
|
||||
constexpr wchar_t kLauncherProtocol[] = L"moonwell://authorize?source=client";
|
||||
constexpr std::array<const wchar_t*, 2> kLauncherExecutables = {
|
||||
L"MoonWell.exe", L"MoonWellLauncher.exe",
|
||||
};
|
||||
constexpr size_t kMaxLaunchAccountLength = 320;
|
||||
constexpr size_t kLaunchTicketLength = 16;
|
||||
|
||||
std::array<char, kMaxLaunchAccountLength + 1> g_launchAccount{};
|
||||
std::array<char, kLaunchTicketLength + 1> g_launchTicket{};
|
||||
volatile LONG g_hasLauncherAuth = 0;
|
||||
bool g_launchedWithTicket = false;
|
||||
bool g_developerLogin = false;
|
||||
volatile LONG g_launcherLoginState = 0; // 0=pending, 1=claimed, 2=submitted
|
||||
DWORD g_launcherAuthCapturedAt = 0;
|
||||
DWORD g_clientThreadId = 0;
|
||||
|
||||
template <size_t Size>
|
||||
bool ConsumeEnvironmentVariable(const char* name, std::array<char, Size>& output)
|
||||
{
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
const DWORD required = GetEnvironmentVariableA(name, nullptr, 0);
|
||||
if (!required)
|
||||
{
|
||||
SetEnvironmentVariableA(name, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (required > output.size())
|
||||
{
|
||||
SetEnvironmentVariableA(name, nullptr);
|
||||
WLOG_ERROR("moonwell: rejected oversized launcher environment value %s", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
const DWORD written = GetEnvironmentVariableA(
|
||||
name, output.data(), static_cast<DWORD>(output.size()));
|
||||
SetEnvironmentVariableA(name, nullptr);
|
||||
return written > 0 && written < output.size();
|
||||
}
|
||||
|
||||
template <size_t Size>
|
||||
bool IsPrintableAscii(const std::array<char, Size>& value)
|
||||
{
|
||||
for (const unsigned char character : value)
|
||||
{
|
||||
if (character == 0)
|
||||
return true;
|
||||
if (character < 0x21 || character > 0x7e)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsValidLauncherTicket()
|
||||
{
|
||||
if (std::strlen(g_launchTicket.data()) != kLaunchTicketLength)
|
||||
return false;
|
||||
|
||||
for (size_t index = 0; index < kLaunchTicketLength; ++index)
|
||||
{
|
||||
const char character = g_launchTicket[index];
|
||||
if (!((character >= 'A' && character <= 'Z')
|
||||
|| (character >= '0' && character <= '9')))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearLauncherAuth()
|
||||
{
|
||||
SecureZeroMemory(g_launchAccount.data(), g_launchAccount.size());
|
||||
SecureZeroMemory(g_launchTicket.data(), g_launchTicket.size());
|
||||
InterlockedExchange(&g_hasLauncherAuth, 0);
|
||||
}
|
||||
|
||||
void CaptureLaunchEnvironment()
|
||||
{
|
||||
const bool hasAccount = ConsumeEnvironmentVariable(
|
||||
kLaunchAccountVariable, g_launchAccount);
|
||||
const bool hasTicket = ConsumeEnvironmentVariable(
|
||||
kLaunchTicketVariable, g_launchTicket);
|
||||
|
||||
g_hasLauncherAuth = hasAccount && hasTicket
|
||||
&& IsPrintableAscii(g_launchAccount)
|
||||
&& IsValidLauncherTicket() ? 1 : 0;
|
||||
InterlockedExchange(&g_launcherLoginState, 0);
|
||||
g_launchedWithTicket = g_hasLauncherAuth != 0;
|
||||
g_launcherAuthCapturedAt = g_hasLauncherAuth ? GetTickCount() : 0;
|
||||
|
||||
if (!g_hasLauncherAuth)
|
||||
{
|
||||
if (hasAccount || hasTicket)
|
||||
WLOG_ERROR("moonwell: incomplete or invalid launcher authorization data");
|
||||
ClearLauncherAuth();
|
||||
}
|
||||
|
||||
std::array<char, 8> developerValue{};
|
||||
const bool developerRequested = ConsumeEnvironmentVariable(
|
||||
kDeveloperLoginVariable, developerValue);
|
||||
#if defined(_DEBUG)
|
||||
g_developerLogin = developerRequested
|
||||
&& std::strcmp(developerValue.data(), "1") == 0;
|
||||
#else
|
||||
g_developerLogin = false;
|
||||
#endif
|
||||
SecureZeroMemory(developerValue.data(), developerValue.size());
|
||||
|
||||
if (g_hasLauncherAuth)
|
||||
WLOG_INFO("moonwell: launcher authorization data accepted");
|
||||
else if (g_developerLogin)
|
||||
WLOG_INFO("moonwell: developer login enabled for this Debug build");
|
||||
}
|
||||
|
||||
int __cdecl GetLaunchMode(void* state)
|
||||
{
|
||||
const char* mode = g_launchedWithTicket
|
||||
? "launcher"
|
||||
: (g_developerLogin ? "developer" : "locked");
|
||||
wxl::game::script::PushString(state, mode);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __cdecl ConsumeLauncherAuth(void* state)
|
||||
{
|
||||
constexpr uintptr_t kLoginGlueReady = 0x00B6B474;
|
||||
constexpr uintptr_t kLoginServerHost = 0x00B6AF54;
|
||||
constexpr uintptr_t kLoginServerPort = 0x00B6AF5C;
|
||||
constexpr uintptr_t kLoginBusy = 0x00B6AA38;
|
||||
if (!g_hasLauncherAuth
|
||||
|| !*reinterpret_cast<volatile uint8_t*>(kLoginGlueReady)
|
||||
|| !*reinterpret_cast<void* volatile*>(kLoginServerHost)
|
||||
|| !*reinterpret_cast<void* volatile*>(kLoginServerPort)
|
||||
|| *reinterpret_cast<volatile uint32_t*>(kLoginBusy))
|
||||
return 0;
|
||||
|
||||
if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0)
|
||||
return 0;
|
||||
if (!g_hasLauncherAuth)
|
||||
{
|
||||
InterlockedExchange(&g_launcherLoginState, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
wxl::game::script::PushString(state, g_launchAccount.data());
|
||||
wxl::game::script::PushString(state, g_launchTicket.data());
|
||||
ClearLauncherAuth();
|
||||
InterlockedExchange(&g_launcherLoginState, 2);
|
||||
WLOG_INFO("moonwell: launcher authorization consumed by Lua bridge");
|
||||
return 2;
|
||||
}
|
||||
|
||||
bool TryOpenAdjacentLauncher()
|
||||
{
|
||||
std::array<wchar_t, MAX_PATH> path{};
|
||||
const DWORD length = GetModuleFileNameW(nullptr, path.data(),
|
||||
static_cast<DWORD>(path.size()));
|
||||
if (!length || length >= path.size())
|
||||
return false;
|
||||
|
||||
wchar_t* slash = std::wcsrchr(path.data(), L'\\');
|
||||
if (!slash)
|
||||
return false;
|
||||
|
||||
++slash;
|
||||
const size_t prefixLength = static_cast<size_t>(slash - path.data());
|
||||
for (const wchar_t* executable : kLauncherExecutables)
|
||||
{
|
||||
const size_t launcherLength = std::wcslen(executable);
|
||||
if (prefixLength + launcherLength >= path.size())
|
||||
continue;
|
||||
|
||||
std::wmemcpy(slash, executable, launcherLength + 1);
|
||||
if (GetFileAttributesW(path.data()) == INVALID_FILE_ATTRIBUTES)
|
||||
continue;
|
||||
|
||||
if (reinterpret_cast<INT_PTR>(ShellExecuteW(
|
||||
nullptr, L"open", path.data(), nullptr, nullptr, SW_SHOWNORMAL)) > 32)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __cdecl OpenLauncher(void* state)
|
||||
{
|
||||
bool opened = TryOpenAdjacentLauncher();
|
||||
if (!opened)
|
||||
{
|
||||
opened = reinterpret_cast<INT_PTR>(ShellExecuteW(
|
||||
nullptr, L"open", kLauncherProtocol, nullptr, nullptr, SW_SHOWNORMAL)) > 32;
|
||||
}
|
||||
|
||||
wxl::game::script::PushBoolean(state, opened);
|
||||
return 1;
|
||||
}
|
||||
|
||||
using DefaultServerLoginCallbackFn = int(__cdecl*)(void*);
|
||||
using BeginServerLoginFn = void(__cdecl*)(const char*, const char*);
|
||||
DefaultServerLoginCallbackFn g_nextDefaultServerLogin = nullptr;
|
||||
|
||||
bool SubmitLauncherAuthorization()
|
||||
{
|
||||
if (!g_hasLauncherAuth || g_launcherLoginState != 0)
|
||||
return false;
|
||||
|
||||
constexpr uintptr_t kBeginServerLogin = 0x004D8A30;
|
||||
constexpr uintptr_t kLoginGlueReady = 0x00B6B474;
|
||||
constexpr uintptr_t kLoginServerHost = 0x00B6AF54;
|
||||
constexpr uintptr_t kLoginServerPort = 0x00B6AF5C;
|
||||
constexpr uintptr_t kLoginBusy = 0x00B6AA38;
|
||||
if (!*reinterpret_cast<volatile uint8_t*>(kLoginGlueReady)
|
||||
|| !*reinterpret_cast<void* volatile*>(kLoginServerHost)
|
||||
|| !*reinterpret_cast<void* volatile*>(kLoginServerPort)
|
||||
|| *reinterpret_cast<volatile uint32_t*>(kLoginBusy))
|
||||
return false;
|
||||
|
||||
if (InterlockedCompareExchange(&g_launcherLoginState, 1, 0) != 0)
|
||||
return false;
|
||||
if (!g_hasLauncherAuth)
|
||||
{
|
||||
InterlockedExchange(&g_launcherLoginState, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
reinterpret_cast<BeginServerLoginFn>(kBeginServerLogin)(
|
||||
g_launchAccount.data(), g_launchTicket.data());
|
||||
if (!*reinterpret_cast<volatile uint32_t*>(kLoginBusy))
|
||||
{
|
||||
InterlockedExchange(&g_launcherLoginState, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
ClearLauncherAuth();
|
||||
InterlockedExchange(&g_launcherLoginState, 2);
|
||||
WLOG_INFO("moonwell: launcher authorization submitted to login engine");
|
||||
return true;
|
||||
}
|
||||
|
||||
int __cdecl DefaultServerLoginHook(void* state)
|
||||
{
|
||||
const char* account = wxl::game::script::IsString(state, 1)
|
||||
? wxl::game::script::ToString(state, 1)
|
||||
: nullptr;
|
||||
if (!account || std::strcmp(account, kLauncherLoginMarker) != 0)
|
||||
return g_nextDefaultServerLogin ? g_nextDefaultServerLogin(state) : 0;
|
||||
|
||||
SubmitLauncherAuthorization();
|
||||
return 0;
|
||||
}
|
||||
|
||||
using GxSetProjectionFn = void(__fastcall*)(void* self, void* edx, const void* projection);
|
||||
GxSetProjectionFn g_nextSetProjection = nullptr;
|
||||
|
||||
// FrameXML's stock SetCreature only accepts a creature entry and waits
|
||||
@@ -64,10 +362,10 @@ namespace moonwell
|
||||
int __cdecl SetCreatureDisplayInfoHook(void* state)
|
||||
{
|
||||
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0;
|
||||
if (!state || !wxl::runtime::lua::IsNumber(state, 3))
|
||||
if (!state || !wxl::game::script::IsNumber(state, 3))
|
||||
return result;
|
||||
|
||||
const double requestedDisplayInfo = wxl::runtime::lua::ToNumber(state, 3);
|
||||
const double requestedDisplayInfo = wxl::game::script::ToNumber(state, 3);
|
||||
if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
|
||||
return result;
|
||||
|
||||
@@ -102,8 +400,8 @@ namespace moonwell
|
||||
|
||||
void InstallEncounterJournalModelPreview()
|
||||
{
|
||||
if (!wxl::core::hook::Install("MoonWellSetCreatureDisplayInfo", kSetCreature,
|
||||
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
|
||||
if (!Hook("MoonWellSetCreatureDisplayInfo", kSetCreature,
|
||||
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
|
||||
{
|
||||
WLOG_ERROR("moonwell: encounter journal model hook installation failed");
|
||||
return;
|
||||
@@ -151,14 +449,14 @@ namespace moonwell
|
||||
|
||||
int __cdecl SetCharacterCreateCamera(void* state)
|
||||
{
|
||||
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
|
||||
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
|
||||
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
|
||||
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
|
||||
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
|
||||
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
|
||||
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
|
||||
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
|
||||
const bool enabled = state && wxl::game::script::IsNumber(state, 1)
|
||||
&& wxl::game::script::ToNumber(state, 1) != 0.0;
|
||||
const float faceZoom = state && wxl::game::script::IsNumber(state, 2)
|
||||
? static_cast<float>(wxl::game::script::ToNumber(state, 2)) : 2.0f;
|
||||
const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3)
|
||||
? static_cast<float>(wxl::game::script::ToNumber(state, 3)) : -0.65f;
|
||||
double requestedDuration = state && wxl::game::script::IsNumber(state, 4)
|
||||
? wxl::game::script::ToNumber(state, 4) : 500.0;
|
||||
if (requestedDuration < 0.0) requestedDuration = 0.0;
|
||||
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
|
||||
|
||||
@@ -202,15 +500,16 @@ namespace moonwell
|
||||
|
||||
void InstallCharacterCreateCamera()
|
||||
{
|
||||
namespace gx = wxl::offsets::engine::gx;
|
||||
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
|
||||
void** slot = &vtable[gx::kGxSetProjectionSlot];
|
||||
constexpr uintptr_t kGxDeviceVTable = 0x00A2E718;
|
||||
constexpr unsigned kGxSetProjectionSlot = 0xA0 / 4;
|
||||
void** vtable = reinterpret_cast<void**>(kGxDeviceVTable);
|
||||
void** slot = &vtable[kGxSetProjectionSlot];
|
||||
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
|
||||
return;
|
||||
|
||||
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
|
||||
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
|
||||
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
|
||||
if (!g_nextSetProjection || !PatchMemory(slot, &replacement, sizeof(replacement)))
|
||||
{
|
||||
g_nextSetProjection = nullptr;
|
||||
WLOG_ERROR("moonwell: character-create projection hook installation failed");
|
||||
@@ -222,17 +521,17 @@ namespace moonwell
|
||||
int __cdecl SetLoginCharacterFlags(void* state)
|
||||
{
|
||||
uint32_t flags = 0;
|
||||
if (state && wxl::runtime::lua::IsNumber(state, 1))
|
||||
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));
|
||||
if (state && wxl::game::script::IsNumber(state, 1))
|
||||
flags = static_cast<uint32_t>(wxl::game::script::ToNumber(state, 1));
|
||||
|
||||
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
|
||||
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
|
||||
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __cdecl IsTraitor(void* state)
|
||||
{
|
||||
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
|
||||
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -259,7 +558,7 @@ namespace moonwell
|
||||
name, reinterpret_cast<void*>(address));
|
||||
return false;
|
||||
}
|
||||
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(address), replacement.data(), N))
|
||||
if (!PatchMemory(reinterpret_cast<void*>(address), replacement.data(), N))
|
||||
{
|
||||
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
|
||||
reinterpret_cast<void*>(address));
|
||||
@@ -371,7 +670,7 @@ namespace moonwell
|
||||
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
|
||||
(patchAddress + jump.size()));
|
||||
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
|
||||
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
|
||||
if (!PatchMemory(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
|
||||
{
|
||||
VirtualFree(cave, 0, MEM_RELEASE);
|
||||
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
|
||||
@@ -390,40 +689,251 @@ namespace moonwell
|
||||
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
|
||||
else
|
||||
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
|
||||
wxl::core::log::Flush();
|
||||
}
|
||||
|
||||
DWORD WINAPI FlushRuntimeLog(LPVOID)
|
||||
using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function);
|
||||
using ValidateCallbackFn = void(__cdecl*)(uintptr_t);
|
||||
using GetContextFn = void*(__cdecl*)();
|
||||
using ExecuteFn = void(__cdecl*)(const char*, uintptr_t, uintptr_t);
|
||||
using InitializeLuaFn = int(__cdecl*)(void*);
|
||||
using FramePumpFn = void(__cdecl*)(float, uint32_t);
|
||||
using FileOpenFn = int(__stdcall*)(void*, const char*, uint32_t, void**);
|
||||
using GlueModelRenderFn = void(__cdecl*)(void*);
|
||||
ValidateCallbackFn g_nextValidateCallback = nullptr;
|
||||
ExecuteFn g_nextExecute = nullptr;
|
||||
InitializeLuaFn g_nextInitializeLua = nullptr;
|
||||
FramePumpFn g_nextFramePump = nullptr;
|
||||
FileOpenFn g_nextFileOpen = nullptr;
|
||||
GlueModelRenderFn g_nextGlueModelRender = nullptr;
|
||||
PVOID volatile g_registeredState = nullptr;
|
||||
bool g_registeringMoonWell = false;
|
||||
|
||||
bool IsMoonWellCallback(uintptr_t callback)
|
||||
{
|
||||
// Let the remainder of RunAll() and the core-ready line reach the
|
||||
// shared buffered logger, then make startup diagnostics durable.
|
||||
Sleep(1000);
|
||||
wxl::core::log::Flush();
|
||||
return 0;
|
||||
return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags)
|
||||
|| callback == reinterpret_cast<uintptr_t>(&IsTraitor)
|
||||
|| callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera)
|
||||
|| callback == reinterpret_cast<uintptr_t>(&GetLaunchMode)
|
||||
|| callback == reinterpret_cast<uintptr_t>(&ConsumeLauncherAuth)
|
||||
|| callback == reinterpret_cast<uintptr_t>(&OpenLauncher);
|
||||
}
|
||||
|
||||
void InstallRuntimeLogFlush()
|
||||
void RegisterLuaFunctionsForCurrentState();
|
||||
|
||||
void __cdecl ValidateCallbackHook(uintptr_t callback)
|
||||
{
|
||||
HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
|
||||
if (thread) CloseHandle(thread);
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
|
||||
g_nextValidateCallback(callback);
|
||||
}
|
||||
|
||||
struct Registration
|
||||
void RegisterLuaFunctions(void* state)
|
||||
{
|
||||
Registration()
|
||||
if (g_registeringMoonWell) return;
|
||||
if (!state || state == g_registeredState) return;
|
||||
|
||||
constexpr uintptr_t kRegisterFunction = 0x00817F90;
|
||||
const auto registrar = reinterpret_cast<RegisterFunctionFn>(kRegisterFunction);
|
||||
g_registeringMoonWell = true;
|
||||
registrar("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
|
||||
registrar("MoonWellIsTraitor", &IsTraitor);
|
||||
registrar("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
|
||||
registrar("MoonWellGetLaunchMode", &GetLaunchMode);
|
||||
registrar("MoonWellConsumeLauncherAuth", &ConsumeLauncherAuth);
|
||||
registrar("MoonWellOpenLauncher", &OpenLauncher);
|
||||
g_registeringMoonWell = false;
|
||||
InterlockedExchangePointer(&g_registeredState, state);
|
||||
WLOG_INFO("moonwell: Lua functions registered for state %p", state);
|
||||
}
|
||||
|
||||
void RegisterLuaFunctionsForCurrentState()
|
||||
{
|
||||
constexpr uintptr_t kGetContext = 0x00817DB0;
|
||||
RegisterLuaFunctions(reinterpret_cast<GetContextFn>(kGetContext)());
|
||||
}
|
||||
|
||||
void RegisterLuaFunctionsBeforeHooksEnable()
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
}
|
||||
|
||||
void __cdecl OnClientMessage(void*, const void*)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
}
|
||||
|
||||
struct WindowSearch
|
||||
{
|
||||
DWORD processId;
|
||||
HWND window;
|
||||
};
|
||||
|
||||
BOOL CALLBACK FindClientWindow(HWND window, LPARAM parameter)
|
||||
{
|
||||
auto* search = reinterpret_cast<WindowSearch*>(parameter);
|
||||
DWORD processId = 0;
|
||||
GetWindowThreadProcessId(window, &processId);
|
||||
if (processId != search->processId || !IsWindowVisible(window))
|
||||
return TRUE;
|
||||
|
||||
search->window = window;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
DWORD WINAPI ScheduleLuaBootstrap(LPVOID)
|
||||
{
|
||||
HWND clientWindow = nullptr;
|
||||
WLOG_INFO("moonwell: Lua bootstrap scheduler started");
|
||||
for (unsigned attempt = 0; attempt < 600; ++attempt)
|
||||
{
|
||||
wxl::runtime::lua::RegisterFunction(
|
||||
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
|
||||
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
|
||||
wxl::runtime::lua::RegisterFunction(
|
||||
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
|
||||
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
|
||||
wxl::runtime::modules::Register(
|
||||
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
|
||||
wxl::runtime::modules::Register(
|
||||
"moonwell-encounter-journal-models", &InstallEncounterJournalModelPreview);
|
||||
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
|
||||
if (!clientWindow)
|
||||
{
|
||||
clientWindow = FindWindowW(L"GxWindowClass", nullptr);
|
||||
DWORD ownerProcessId = 0;
|
||||
if (clientWindow)
|
||||
GetWindowThreadProcessId(clientWindow, &ownerProcessId);
|
||||
if (ownerProcessId != GetCurrentProcessId())
|
||||
clientWindow = nullptr;
|
||||
if (!clientWindow)
|
||||
{
|
||||
WindowSearch search{GetCurrentProcessId(), nullptr};
|
||||
EnumWindows(&FindClientWindow, reinterpret_cast<LPARAM>(&search));
|
||||
clientWindow = search.window;
|
||||
}
|
||||
}
|
||||
if (clientWindow)
|
||||
{
|
||||
WLOG_INFO("moonwell: client window %p found for Lua bootstrap", clientWindow);
|
||||
// BeginServerLogin only snapshots the supplied credentials and starts
|
||||
// the client's asynchronous login state machine. Waiting here keeps the
|
||||
// call clear of engine initialization and Glue archive mounting.
|
||||
for (unsigned attempt = 0;
|
||||
attempt < 80 && g_hasLauncherAuth;
|
||||
++attempt)
|
||||
{
|
||||
Sleep(250);
|
||||
SubmitLauncherAuthorization();
|
||||
}
|
||||
return g_hasLauncherAuth ? 1 : 0;
|
||||
}
|
||||
Sleep(100);
|
||||
}
|
||||
} g_registration;
|
||||
WLOG_ERROR("moonwell: timed out waiting for UI-thread Lua bootstrap");
|
||||
return 1;
|
||||
}
|
||||
|
||||
void __cdecl ExecuteHook(const char* source, uintptr_t argument2, uintptr_t argument3)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_nextExecute) g_nextExecute(source, argument2, argument3);
|
||||
}
|
||||
|
||||
int __cdecl InitializeLuaHook(void* allocatorContext)
|
||||
{
|
||||
const int initialized = g_nextInitializeLua
|
||||
? g_nextInitializeLua(allocatorContext)
|
||||
: 0;
|
||||
if (initialized)
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
return initialized;
|
||||
}
|
||||
|
||||
void __cdecl FramePumpHook(float deltaSeconds, uint32_t frameTimeMs)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_nextFramePump) g_nextFramePump(deltaSeconds, frameTimeMs);
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_hasLauncherAuth
|
||||
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
|
||||
SubmitLauncherAuthorization();
|
||||
}
|
||||
|
||||
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
|
||||
{
|
||||
// The engine-init callback and synchronous Glue loader run on the same client
|
||||
// thread. Polling here catches Lua becoming live before AccountLogin executes,
|
||||
// without ever touching the state from background asset I/O workers.
|
||||
if (GetCurrentThreadId() == g_clientThreadId)
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
const int result = g_nextFileOpen ? g_nextFileOpen(archive, name, flags, out) : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
void __cdecl GlueModelRenderHook(void* frame)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_nextGlueModelRender) g_nextGlueModelRender(frame);
|
||||
}
|
||||
|
||||
void __cdecl OnFrame(void*, const void*)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
// Glue scripting is not a reliable authentication trigger: a broken
|
||||
// cosmetic widget can abort AccountLogin_OnShow before it reaches the
|
||||
// login call. Present is emitted on the client thread after the engine
|
||||
// and Glue subsystem are ready, so submit once after a short grace period.
|
||||
if (g_hasLauncherAuth
|
||||
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
|
||||
SubmitLauncherAuthorization();
|
||||
}
|
||||
|
||||
bool InstallLuaBridge()
|
||||
{
|
||||
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
|
||||
&g_nextValidateCallback);
|
||||
const bool executor = HookByName("Lua.Execute", &ExecuteHook, &g_nextExecute);
|
||||
const bool initializer = Hook("Lua.Initialize", 0x00819BB0,
|
||||
&InitializeLuaHook, &g_nextInitializeLua);
|
||||
const bool defaultLogin = Hook("MoonWell.DefaultServerLogin", 0x004DC260,
|
||||
&DefaultServerLoginHook, &g_nextDefaultServerLogin);
|
||||
const bool framePump = HookByName("Frame.Pump", &FramePumpHook, &g_nextFramePump);
|
||||
const bool fileOpen = HookByName("Io.FileOpen", &FileOpenHook, &g_nextFileOpen);
|
||||
const bool glueRender = HookByName("Gx.GlueModelRender", &GlueModelRenderHook,
|
||||
&g_nextGlueModelRender);
|
||||
if (!validator || !executor || !initializer || !defaultLogin
|
||||
|| !framePump || !fileOpen || !glueRender)
|
||||
WLOG_ERROR("moonwell: Lua bridge hook installation failed");
|
||||
return validator && executor && initializer && defaultLogin
|
||||
&& framePump && fileOpen && glueRender;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WXL_PluginInfo* __cdecl WXL_Query(void)
|
||||
{
|
||||
static const WXL_PluginInfo info = {
|
||||
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWell", 1, WXL_CLIENT_BUILD,
|
||||
};
|
||||
return &info;
|
||||
}
|
||||
|
||||
int __cdecl WXL_Load(const WXL_Api* api)
|
||||
{
|
||||
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
|
||||
moonwell::g_api = api;
|
||||
moonwell::g_clientThreadId = GetCurrentThreadId();
|
||||
|
||||
moonwell::CaptureLaunchEnvironment();
|
||||
moonwell::InstallBoot();
|
||||
const bool lua = moonwell::InstallLuaBridge();
|
||||
// Event ordinal 5 is wxl::events::Event::OnFrame in API v1. It is emitted
|
||||
// from Present on the client thread even while only the Glue UI is active.
|
||||
if (api->Subscribe)
|
||||
{
|
||||
api->Subscribe(5, &moonwell::OnFrame, nullptr);
|
||||
api->Subscribe(17, &moonwell::OnClientMessage, nullptr);
|
||||
}
|
||||
moonwell::RegisterLuaFunctionsBeforeHooksEnable();
|
||||
if (HANDLE bootstrapThread = CreateThread(
|
||||
nullptr, 0, &moonwell::ScheduleLuaBootstrap, nullptr, 0, nullptr))
|
||||
CloseHandle(bootstrapThread);
|
||||
else
|
||||
api->Log(WXL_LOG_ERROR, "MoonWell", "%s", "failed to start Lua bootstrap scheduler");
|
||||
moonwell::InstallCharacterCreateCamera();
|
||||
moonwell::InstallEncounterJournalModelPreview();
|
||||
const bool spells = moonwell::spells::Install(api);
|
||||
const bool ready = lua && spells;
|
||||
api->Log(ready ? WXL_LOG_INFO : WXL_LOG_ERROR, "MoonWell", "%s",
|
||||
ready ? "MoonWell extension loaded" : "MoonWell extension loaded with errors");
|
||||
return ready ? 1 : 0;
|
||||
}
|
||||
|
||||
+72
-26
@@ -1,9 +1,9 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Appends compact MoonWell spell overrides to the stock 3.3.5 DBCs at open time.
|
||||
// Appends compact MoonWell spell overrides to stock 3.3.5 DBC bytes.
|
||||
|
||||
#include "Host.hpp"
|
||||
#include "core/Logger.hpp"
|
||||
#include "mpq/MpqStore.hpp"
|
||||
#include "game/Io.hpp"
|
||||
#include "wxl/PluginApi.h"
|
||||
#include "wxl/StorageApi.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
@@ -15,7 +15,7 @@
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::host
|
||||
namespace moonwell::spells
|
||||
{
|
||||
namespace
|
||||
{
|
||||
@@ -38,8 +38,34 @@ namespace moonwell::host
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
bool g_ready = false;
|
||||
std::vector<uint8_t> g_spellDbc;
|
||||
std::vector<uint8_t> g_skillDbc;
|
||||
const WXL_Api* g_api = nullptr;
|
||||
std::vector<Override> g_overrides;
|
||||
|
||||
void Log(int level, const char* message)
|
||||
{
|
||||
if (g_api && g_api->Log) g_api->Log(level, "MoonWellSpells", "%s", message);
|
||||
}
|
||||
|
||||
bool ReadAll(const char* path, std::vector<uint8_t>& out)
|
||||
{
|
||||
void* handle = nullptr;
|
||||
if (!wxl::game::io::FileOpen(path, wxl::game::io::kOpenWholeFile, &handle) || !handle)
|
||||
return false;
|
||||
uint32_t high = 0;
|
||||
const uint32_t size = wxl::game::io::FileSize(handle, &high);
|
||||
if (high != 0)
|
||||
{
|
||||
wxl::game::io::FileClose(handle);
|
||||
return false;
|
||||
}
|
||||
out.resize(size);
|
||||
uint32_t read = 0;
|
||||
const bool ok = (size == 0 || wxl::game::io::FileRead(handle, out.data(), size, &read))
|
||||
&& read == size;
|
||||
wxl::game::io::FileClose(handle);
|
||||
if (!ok) out.clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint32_t ReadU32(const uint8_t* data)
|
||||
{
|
||||
@@ -226,39 +252,59 @@ namespace moonwell::host
|
||||
|
||||
void Load()
|
||||
{
|
||||
const std::string root = wxl::host::ClientRoot();
|
||||
wxl::host::mpq::MpqStore store;
|
||||
std::vector<uint8_t> overrideBytes, spellBase, skillBase;
|
||||
if (root.empty() || !store.Mount(root) || !store.ReadAll(kOverridesPath, overrideBytes))
|
||||
std::vector<uint8_t> overrideBytes;
|
||||
if (!ReadAll(kOverridesPath.data(), overrideBytes))
|
||||
return;
|
||||
|
||||
std::vector<Override> overrides;
|
||||
if (!ParseOverrides(overrideBytes, overrides) ||
|
||||
!store.ReadAll(kSpellPath, spellBase) || !store.ReadAll(kSkillPath, skillBase) ||
|
||||
!BuildSpellDbc(spellBase, overrides, g_spellDbc) ||
|
||||
!BuildSkillDbc(skillBase, overrides, g_skillDbc))
|
||||
if (!ParseOverrides(overrideBytes, g_overrides))
|
||||
{
|
||||
WLOG_ERROR("moonwell-spells: failed to build custom companion DBCs");
|
||||
Log(WXL_LOG_ERROR, "failed to parse WXLSpellOverrides.tsv");
|
||||
return;
|
||||
}
|
||||
g_ready = true;
|
||||
WLOG_INFO("moonwell-spells: appended %zu companion spell(s)", overrides.size());
|
||||
if (g_api && g_api->Log)
|
||||
g_api->Log(WXL_LOG_INFO, "MoonWellSpells", "loaded %zu companion spell override(s)",
|
||||
g_overrides.size());
|
||||
}
|
||||
|
||||
bool Provide(std::string_view name, std::vector<uint8_t>& out)
|
||||
int __cdecl Transform(const char* rawName, const uint8_t* raw, uint32_t rawLen,
|
||||
const WXL_ByteSink* sink)
|
||||
{
|
||||
if (!rawName || !raw || !sink || !sink->Write) return 0;
|
||||
const std::string_view name(rawName);
|
||||
if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath)) return false;
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
if (!g_ready) return false;
|
||||
out = SamePath(name, kSpellPath) ? g_spellDbc : g_skillDbc;
|
||||
return true;
|
||||
|
||||
const std::vector<uint8_t> base(raw, raw + rawLen);
|
||||
std::vector<uint8_t> out;
|
||||
const bool built = SamePath(name, kSpellPath)
|
||||
? BuildSpellDbc(base, g_overrides, out)
|
||||
: BuildSkillDbc(base, g_overrides, out);
|
||||
if (!built)
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "failed to build custom companion DBC");
|
||||
return 0;
|
||||
}
|
||||
sink->Write(sink->ctx, out.data(), static_cast<uint32_t>(out.size()));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
struct Registrar
|
||||
bool Install(const WXL_Api* api)
|
||||
{
|
||||
g_api = api;
|
||||
if (!api || !api->GetInterface) return false;
|
||||
auto* storage = static_cast<WXL_StorageApi*>(
|
||||
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
|
||||
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
|
||||
!storage->RegisterClientTransform)
|
||||
{
|
||||
Registrar() { wxl::host::RegisterProvider("moonwell-companion-spells", &Provide); }
|
||||
};
|
||||
|
||||
Registrar g_registrar;
|
||||
Log(WXL_LOG_ERROR, "wxl.storage v1 is unavailable");
|
||||
return false;
|
||||
}
|
||||
storage->RegisterClientTransform(".dbc", &Transform);
|
||||
Log(WXL_LOG_INFO, "DBC override transform registered");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
struct WXL_Api;
|
||||
|
||||
namespace moonwell::spells
|
||||
{
|
||||
bool Install(const WXL_Api* api);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GLOB_RECURSE WXL_EXT_SHARED_SRC CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
dvc[s3]==3.67.1
|
||||
@@ -1,7 +1,8 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("production", "ptr", "local")]
|
||||
[string]$Env = "local"
|
||||
[string]$Env = "local",
|
||||
[switch]$DeveloperLogin
|
||||
)
|
||||
|
||||
# Stop on errors
|
||||
@@ -9,6 +10,7 @@ $ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Resolve project root (folder where script is located)
|
||||
$ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. (Join-Path $ROOT 'pipeline-layout.ps1')
|
||||
|
||||
# --- Load .env if WOW_HOME not already set
|
||||
if (-not $env:WOW_HOME) {
|
||||
@@ -67,9 +69,16 @@ if ((Test-Path -LiteralPath $installedWarcraftXL -PathType Leaf) -and
|
||||
-not (Test-ExistingFileWriteAccess -Path $installedWarcraftXL) -and
|
||||
-not $isAdministrator) {
|
||||
Write-Host "WarcraftXL.dll requires administrator access. Requesting elevation..."
|
||||
$elevationArguments = @(
|
||||
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
|
||||
"`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
|
||||
)
|
||||
if ($DeveloperLogin) {
|
||||
$elevationArguments += "-DeveloperLogin"
|
||||
}
|
||||
$elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait `
|
||||
-WorkingDirectory $ROOT `
|
||||
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
|
||||
-ArgumentList $elevationArguments
|
||||
exit $elevatedProcess.ExitCode
|
||||
}
|
||||
|
||||
@@ -80,11 +89,16 @@ $TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
|
||||
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
|
||||
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
|
||||
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $DIST_DIR `
|
||||
-RepositoryPath $ROOT `
|
||||
-ClientPath $WOW_HOME
|
||||
|
||||
function Stop-WowRuntime {
|
||||
param([Parameter(Mandatory=$true)][string]$ClientPath)
|
||||
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\') + '\'
|
||||
$runtimeProcesses = @(Get-Process -Name "Wow", "WarcraftXLHost" -ErrorAction SilentlyContinue | Where-Object {
|
||||
$runtimeProcesses = @(Get-Process -Name "Wow" -ErrorAction SilentlyContinue | Where-Object {
|
||||
try {
|
||||
$_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
@@ -96,7 +110,7 @@ function Stop-WowRuntime {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Stopping the running WoW client and WarcraftXL host..."
|
||||
Write-Host "Stopping the running WoW client..."
|
||||
$runtimeProcesses | Stop-Process -Force
|
||||
|
||||
try {
|
||||
@@ -118,7 +132,14 @@ if (!(Test-Path $TOOL)) {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# --- Build MPQ archives into dist/
|
||||
# --- Build MPQ archives into a clean dist/
|
||||
$previousPackageFiles = @(
|
||||
Get-RelativeFilePaths -Root $DIST_DIR
|
||||
)
|
||||
Reset-MoonWellPackageStaging `
|
||||
-PackagePath $DIST_DIR `
|
||||
-RepositoryPath $ROOT `
|
||||
-ClientPath $WOW_HOME
|
||||
Write-Host "Building MPQ archives from src/ -> dist/..."
|
||||
& $TOOL $SRC_DIR $DIST_DIR
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
@@ -136,7 +157,8 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
|
||||
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
|
||||
Stop-WowRuntime -ClientPath $WOW_HOME
|
||||
Write-Host "Building and packaging WarcraftXL..."
|
||||
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
|
||||
$runtimeConfiguration = if ($DeveloperLogin) { "Debug" } else { "Release" }
|
||||
& $WXL_BUILD_SCRIPT -Configuration $runtimeConfiguration -ClientPath $WOW_HOME `
|
||||
-PackagePath $DIST_DIR -Deploy
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "WarcraftXL build/deploy failed!"
|
||||
@@ -145,6 +167,22 @@ if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
# --- Sync dist/ -> WOW_HOME
|
||||
Write-Host "Syncing dist/ -> WOW_HOME..."
|
||||
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $python) {
|
||||
Write-Error "Python is required for package validation."
|
||||
exit 1
|
||||
}
|
||||
& $python.Source (Join-Path $ROOT 'validate_package.py') --dir $DIST_DIR
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Package validation failed!"
|
||||
exit 1
|
||||
}
|
||||
$currentPackageFiles = @(Get-RelativeFilePaths -Root $DIST_DIR)
|
||||
Remove-StalePackageFilesFromClient `
|
||||
-ClientPath $WOW_HOME `
|
||||
-PreviousFiles $previousPackageFiles `
|
||||
-CurrentFiles $currentPackageFiles
|
||||
Remove-MoonWellRuntimeGarbage -Destination $WOW_HOME
|
||||
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
|
||||
# robocopy exit codes 0-7 are success/warnings; 8+ are errors
|
||||
if ($LASTEXITCODE -ge 8) {
|
||||
@@ -171,4 +209,11 @@ if ($realmlist) {
|
||||
|
||||
# --- Run WoW reload script
|
||||
Write-Host "Launching WoW..."
|
||||
cmd /c $RELOAD_SCRIPT
|
||||
if ($DeveloperLogin) {
|
||||
$env:MOONWELL_DEV_LOGIN = "1"
|
||||
}
|
||||
try {
|
||||
cmd /c $RELOAD_SCRIPT
|
||||
} finally {
|
||||
Remove-Item Env:MOONWELL_DEV_LOGIN -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"fileDataID": 3730958,
|
||||
"textures": [
|
||||
{
|
||||
"fileDataID": 3732460,
|
||||
"file": "catslime_3732460.blp"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732461,
|
||||
"file": "catslime_3732461.blp"
|
||||
}
|
||||
],
|
||||
"skins": [
|
||||
{
|
||||
"fileDataID": 3732472,
|
||||
"file": "catslime00.skin"
|
||||
}
|
||||
],
|
||||
"lodSkins": [
|
||||
{
|
||||
"fileDataID": 3732473,
|
||||
"file": "catslime_lod01.skin"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732474,
|
||||
"file": "catslime_lod02.skin"
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732475,
|
||||
"file": "catslime_lod03.skin"
|
||||
}
|
||||
],
|
||||
"anims": [
|
||||
{
|
||||
"fileDataID": 3732462,
|
||||
"file": "catslime0097-00.anim",
|
||||
"animID": 97,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732463,
|
||||
"file": "catslime0096-00.anim",
|
||||
"animID": 96,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732464,
|
||||
"file": "catslime0098-00.anim",
|
||||
"animID": 98,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732465,
|
||||
"file": "catslime0100-00.anim",
|
||||
"animID": 100,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732466,
|
||||
"file": "catslime0099-00.anim",
|
||||
"animID": 99,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732467,
|
||||
"file": "catslime0101-00.anim",
|
||||
"animID": 101,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732468,
|
||||
"file": "catslime0069-00.anim",
|
||||
"animID": 69,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732469,
|
||||
"file": "catslime0123-00.anim",
|
||||
"animID": 123,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732470,
|
||||
"file": "catslime0061-00.anim",
|
||||
"animID": 61,
|
||||
"subAnimID": 0
|
||||
},
|
||||
{
|
||||
"fileDataID": 3732471,
|
||||
"file": "catslime0075-00.anim",
|
||||
"animID": 75,
|
||||
"subAnimID": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -43,7 +43,8 @@ local Config = {
|
||||
}
|
||||
|
||||
local LoginState = {
|
||||
autoLoginTimer = nil, autoLoginDelay = Config.AUTO_LOGIN_DELAY, autoLoginAttempted = false, musicTimer = 0, sceneTimer = 0, currentFrame = 0, lastUpdateTime = 0, currentScene = 1
|
||||
autoLoginTimer = nil, autoLoginDelay = Config.AUTO_LOGIN_DELAY, autoLoginAttempted = false, musicTimer = 0, sceneTimer = 0, currentFrame = 0, lastUpdateTime = 0, currentScene = 1,
|
||||
launchMode = "locked", launcherLoginAttempted = false, luaBridgePending = true
|
||||
}
|
||||
|
||||
local ModelManager = {
|
||||
@@ -51,7 +52,7 @@ local ModelManager = {
|
||||
}
|
||||
|
||||
local UICache = {
|
||||
accountEdit = nil, passwordEdit = nil, saveAccountName = nil, savePassword = nil, autoLogin = nil, autoLoginText = nil, loginButton = nil, versionText = nil, realmName = nil, upgradeButton = nil, tosFrame = nil, tosAccept = nil, tosDecline = nil, backgroundTexture = nil, animatedTexture = nil, newLogo = nil, newLogoFrame = nil
|
||||
accountEdit = nil, passwordEdit = nil, saveAccountName = nil, savePassword = nil, autoLogin = nil, autoLoginText = nil, saveOptionsFrame = nil, launcherHint = nil, loginButton = nil, versionText = nil, realmName = nil, upgradeButton = nil, tosFrame = nil, tosAccept = nil, tosDecline = nil, backgroundTexture = nil, animatedTexture = nil, newLogo = nil, newLogoFrame = nil
|
||||
}
|
||||
|
||||
LOGIN_MODEL_LIGHTS = {
|
||||
@@ -78,6 +79,8 @@ local function InitializeUICache()
|
||||
UICache.savePassword = _G["AccountLoginSavePassword"]
|
||||
UICache.autoLogin = _G["AccountLoginAutoLogin"]
|
||||
UICache.autoLoginText = _G["AccountLoginAutoLoginText"]
|
||||
UICache.saveOptionsFrame = _G["AccountLoginSaveOptionsFrame"]
|
||||
UICache.launcherHint = _G["AccountLoginLauncherHint"]
|
||||
UICache.loginButton = _G["AccountLoginLoginButton"]
|
||||
UICache.versionText = _G["AccountLoginVersion"]
|
||||
UICache.realmName = _G["AccountLoginRealmName"]
|
||||
@@ -94,6 +97,106 @@ end
|
||||
function GetLoginState()
|
||||
return LoginState
|
||||
end
|
||||
|
||||
local function ResolveLaunchMode()
|
||||
if type(MoonWellGetLaunchMode) ~= "function" then
|
||||
return "locked"
|
||||
end
|
||||
|
||||
local mode = MoonWellGetLaunchMode()
|
||||
if mode == "launcher" or mode == "developer" then
|
||||
return mode
|
||||
end
|
||||
return "locked"
|
||||
end
|
||||
|
||||
local function ApplyLaunchMode()
|
||||
local developerMode = LoginState.launchMode == "developer"
|
||||
local launcherMode = LoginState.launchMode == "launcher"
|
||||
|
||||
if launcherMode then
|
||||
AccountLoginUI:Hide()
|
||||
else
|
||||
AccountLoginUI:Show()
|
||||
AccountLoginUI:SetAlpha(1)
|
||||
end
|
||||
|
||||
if developerMode then
|
||||
UICache.accountEdit:Show()
|
||||
UICache.passwordEdit:Show()
|
||||
UICache.saveOptionsFrame:Show()
|
||||
UICache.launcherHint:Hide()
|
||||
UICache.loginButton:SetText(LOGIN)
|
||||
else
|
||||
UICache.accountEdit:Hide()
|
||||
UICache.passwordEdit:Hide()
|
||||
UICache.saveOptionsFrame:Hide()
|
||||
UICache.launcherHint:Show()
|
||||
UICache.loginButton:SetText("Авторизоваться")
|
||||
end
|
||||
end
|
||||
|
||||
function AccountLogin_OpenLauncher()
|
||||
PlaySound("gsLogin")
|
||||
local opened = false
|
||||
if type(MoonWellOpenLauncher) == "function" then
|
||||
opened = MoonWellOpenLauncher() and true or false
|
||||
end
|
||||
|
||||
if not opened then
|
||||
GlueDialog_Show("MOONWELL_LAUNCHER_NOT_FOUND")
|
||||
end
|
||||
end
|
||||
|
||||
function AccountLogin_PrimaryAction()
|
||||
if LoginState.launchMode == "developer" then
|
||||
AccountLogin_Login()
|
||||
else
|
||||
AccountLogin_OpenLauncher()
|
||||
end
|
||||
end
|
||||
|
||||
local function TryLauncherLogin()
|
||||
if LoginState.launchMode ~= "launcher" or LoginState.launcherLoginAttempted then
|
||||
return false
|
||||
end
|
||||
|
||||
LoginState.launcherLoginAttempted = true
|
||||
if type(MoonWellConsumeLauncherAuth) ~= "function" then
|
||||
LoginState.launchMode = "locked"
|
||||
ApplyLaunchMode()
|
||||
return false
|
||||
end
|
||||
|
||||
local accountName, ticket = MoonWellConsumeLauncherAuth()
|
||||
if not accountName or accountName == "" or not ticket or ticket == "" then
|
||||
LoginState.launchMode = "locked"
|
||||
ApplyLaunchMode()
|
||||
return false
|
||||
end
|
||||
|
||||
SetSavedAccountName("")
|
||||
SetSavedAccountList("")
|
||||
SetUsesToken(false)
|
||||
AccountLoginUI:Hide()
|
||||
PlaySound("gsLogin")
|
||||
DefaultServerLogin(accountName, ticket)
|
||||
accountName = nil
|
||||
ticket = nil
|
||||
return true
|
||||
end
|
||||
|
||||
local function TryLauncherFallbackLogin()
|
||||
if not LoginState.luaBridgePending or LoginState.launcherLoginAttempted then
|
||||
return false
|
||||
end
|
||||
|
||||
-- MoonWell.dll intercepts this stock callback and substitutes the one-time
|
||||
-- launcher credentials. With no launcher data it intentionally does nothing.
|
||||
LoginState.launcherLoginAttempted = true
|
||||
DefaultServerLogin("__MOONWELL_LAUNCHER__", "__MOONWELL_LAUNCHER__")
|
||||
return true
|
||||
end
|
||||
-- ============================================================================
|
||||
-- SISTEMA DE ANIMACIÓN BLP
|
||||
-- ============================================================================
|
||||
@@ -144,6 +247,11 @@ GlueDialogTypes["AUTO_LOGIN"] = {
|
||||
UICache.autoLogin:SetChecked(0)
|
||||
end,
|
||||
}
|
||||
|
||||
GlueDialogTypes["MOONWELL_LAUNCHER_NOT_FOUND"] = {
|
||||
text = "Не удалось открыть лаунчер MoonWell. Запустите его вручную и нажмите «Играть».",
|
||||
button1 = OKAY,
|
||||
}
|
||||
-- ============================================================================
|
||||
-- GESTIÓN DE MODELOS 3D
|
||||
-- ============================================================================
|
||||
@@ -151,10 +259,8 @@ local function CreateLoginModel(parent, modelData)
|
||||
local model = CreateFrame("Model", nil, parent)
|
||||
local width, height = parent:GetSize()
|
||||
|
||||
model:SetSize(
|
||||
width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1),
|
||||
height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1)
|
||||
)
|
||||
model:SetWidth(width * (modelData[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
|
||||
model:SetHeight(height * (modelData[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
|
||||
model:SetPoint("CENTER")
|
||||
model:SetModel("Character/Human/Male/HumanMale.mdx")
|
||||
model:SetCamera(1)
|
||||
@@ -194,6 +300,13 @@ end
|
||||
-- ACTUALIZACIÓN DE LA ESCENA
|
||||
-- ============================================================================
|
||||
function LoginScene_OnUpdate(self, elapsed)
|
||||
if LoginState.luaBridgePending and type(MoonWellGetLaunchMode) == "function" then
|
||||
LoginState.luaBridgePending = false
|
||||
LoginState.launchMode = ResolveLaunchMode()
|
||||
ApplyLaunchMode()
|
||||
TryLauncherLogin()
|
||||
end
|
||||
|
||||
if Config.LOGIN_AMBIENCE and not self.ambiencePlayed then
|
||||
PlayGlueAmbience(Config.LOGIN_AMBIENCE, Config.AMBIENCE_FADE_TIME)
|
||||
self.ambiencePlayed = true
|
||||
@@ -283,8 +396,8 @@ function AccountLogin_OnLoad(self)
|
||||
|
||||
model:SetModel("Character/Human/Male/HumanMale.mdx")
|
||||
model:SetPoint("CENTER", 0, 0)
|
||||
model:SetSize(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1),
|
||||
self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
|
||||
model:SetWidth(self:GetWidth() / (data[LOGIN_MODEL_STRUCT.WIDTH_SQUISH] or 1))
|
||||
model:SetHeight(self:GetHeight() / (data[LOGIN_MODEL_STRUCT.HEIGHT_SQUISH] or 1))
|
||||
model:SetCamera(1)
|
||||
|
||||
if data[LOGIN_MODEL_STRUCT.LIGHT] then
|
||||
@@ -312,6 +425,14 @@ function AccountLogin_OnLoad(self)
|
||||
end
|
||||
|
||||
function AccountLogin_OnShow(self)
|
||||
LoginState.luaBridgePending = type(MoonWellGetLaunchMode) ~= "function"
|
||||
LoginState.launchMode = ResolveLaunchMode()
|
||||
ApplyLaunchMode()
|
||||
|
||||
-- Submit launcher credentials before constructing the decorative login UI.
|
||||
-- A cosmetic Lua error must never be able to block authentication.
|
||||
TryLauncherFallbackLogin()
|
||||
|
||||
self:Show()
|
||||
self:SetAlpha(1)
|
||||
WorldOfWarcraftRating:Hide()
|
||||
@@ -341,12 +462,10 @@ function AccountLogin_OnShow(self)
|
||||
elseif self.animatedTexture then
|
||||
self.animatedTexture:Hide()
|
||||
end
|
||||
AccountLoginUI:Show()
|
||||
AccountLoginUI:SetAlpha(1)
|
||||
|
||||
if not self.newLogo then
|
||||
self.newLogoFrame = CreateFrame("Frame", nil, self)
|
||||
self.newLogoFrame:SetSize(Config.LOGO_SIZE, Config.LOGO_SIZE)
|
||||
self.newLogoFrame:SetWidth(Config.LOGO_SIZE)
|
||||
self.newLogoFrame:SetHeight(Config.LOGO_SIZE)
|
||||
self.newLogoFrame:SetPoint("TOPLEFT", Config.LOGO_POSITION_X, Config.LOGO_POSITION_Y)
|
||||
|
||||
self.newLogo = self.newLogoFrame:CreateTexture("AccountLoginNewLogo", "OVERLAY")
|
||||
@@ -376,7 +495,14 @@ function AccountLogin_OnShow(self)
|
||||
self.modelUpdateCount = 0
|
||||
self.modelUpdateTimer = 0
|
||||
|
||||
local accountName, password = unpack(string_explode(GetSavedAccountName(), "#&|&#"))
|
||||
local accountName, password = "", ""
|
||||
if LoginState.launchMode == "developer" then
|
||||
accountName, password = unpack(string_explode(GetSavedAccountName(), "#&|&#"))
|
||||
else
|
||||
SetSavedAccountName("")
|
||||
SetSavedAccountList("")
|
||||
SetUsesToken(false)
|
||||
end
|
||||
UICache.accountEdit:SetText(accountName or "")
|
||||
UICache.passwordEdit:SetText(password or "")
|
||||
|
||||
@@ -390,13 +516,15 @@ function AccountLogin_OnShow(self)
|
||||
UICache.realmName:SetText("No Recent Server")
|
||||
end
|
||||
|
||||
if accountName == "" then
|
||||
AccountLogin_FocusAccountName()
|
||||
else
|
||||
AccountLogin_FocusPassword()
|
||||
if LoginState.launchMode == "developer" then
|
||||
if accountName == "" then
|
||||
AccountLogin_FocusAccountName()
|
||||
else
|
||||
AccountLogin_FocusPassword()
|
||||
end
|
||||
end
|
||||
|
||||
if UICache.savePassword:GetChecked() then
|
||||
if LoginState.launchMode == "developer" and UICache.savePassword:GetChecked() then
|
||||
UICache.autoLoginText:Show()
|
||||
UICache.autoLogin:Show()
|
||||
else
|
||||
@@ -415,7 +543,11 @@ function AccountLogin_OnShow(self)
|
||||
ACCOUNT_MSG_BODY_LOADED = false
|
||||
ACCOUNT_MSG_CURRENT_INDEX = nil
|
||||
|
||||
ApplyLaunchMode()
|
||||
AccountLogin_CheckAutoLogin()
|
||||
if not TryLauncherLogin() then
|
||||
TryLauncherFallbackLogin()
|
||||
end
|
||||
self:SetScript("OnUpdate", LoginScene_OnUpdate)
|
||||
end
|
||||
|
||||
@@ -450,11 +582,15 @@ function AccountLogin_OnHide(self)
|
||||
end
|
||||
|
||||
function AccountLogin_FocusPassword()
|
||||
UICache.passwordEdit:SetFocus()
|
||||
if LoginState.launchMode == "developer" then
|
||||
UICache.passwordEdit:SetFocus()
|
||||
end
|
||||
end
|
||||
|
||||
function AccountLogin_FocusAccountName()
|
||||
UICache.accountEdit:SetFocus()
|
||||
if LoginState.launchMode == "developer" then
|
||||
UICache.accountEdit:SetFocus()
|
||||
end
|
||||
end
|
||||
|
||||
function AccountLogin_OnKeyDown(key)
|
||||
@@ -462,7 +598,7 @@ function AccountLogin_OnKeyDown(key)
|
||||
if ConnectionHelpFrame:IsShown() then
|
||||
ConnectionHelpFrame:Hide()
|
||||
AccountLoginUI:Show()
|
||||
elseif SurveyNotificationFrame:IsShown() then
|
||||
elseif SurveyNotificationFrame and SurveyNotificationFrame:IsShown() then
|
||||
else
|
||||
AccountLogin_Exit()
|
||||
end
|
||||
@@ -471,10 +607,10 @@ function AccountLogin_OnKeyDown(key)
|
||||
return
|
||||
elseif TOSFrame:IsShown() or ConnectionHelpFrame:IsShown() then
|
||||
return
|
||||
elseif SurveyNotificationFrame:IsShown() then
|
||||
elseif SurveyNotificationFrame and SurveyNotificationFrame:IsShown() then
|
||||
AccountLogin_SurveyNotificationDone(1)
|
||||
end
|
||||
AccountLogin_Login()
|
||||
AccountLogin_PrimaryAction()
|
||||
elseif key == "PRINTSCREEN" then
|
||||
Screenshot()
|
||||
end
|
||||
@@ -526,6 +662,11 @@ end
|
||||
-- SISTEMA DE LOGIN CON GUARDADO DE CONTRASEÑA
|
||||
-- ============================================================================
|
||||
function AccountLogin_Login()
|
||||
if LoginState.launchMode ~= "developer" then
|
||||
AccountLogin_OpenLauncher()
|
||||
return
|
||||
end
|
||||
|
||||
PlaySound("gsLogin")
|
||||
local accountName = UICache.accountEdit:GetText()
|
||||
local password = UICache.passwordEdit:GetText()
|
||||
@@ -557,6 +698,11 @@ end
|
||||
-- SISTEMA DE AUTOLOGIN
|
||||
-- ============================================================================
|
||||
function AccountLogin_CheckAutoLogin()
|
||||
if LoginState.launchMode ~= "developer" then
|
||||
LoginState.autoLoginTimer = nil
|
||||
return
|
||||
end
|
||||
|
||||
if not LoginState.autoLoginAttempted then
|
||||
LoginState.autoLoginAttempted = true
|
||||
local savedAccountInfo = GetSavedAccountName()
|
||||
@@ -811,6 +957,12 @@ function AccountLogin_Exit()
|
||||
end
|
||||
|
||||
function AccountLogin_ShowSurveyNotification()
|
||||
if not SurveyNotificationFrame or
|
||||
not SurveyNotificationAccept or
|
||||
not SurveyNotificationDecline then
|
||||
return
|
||||
end
|
||||
|
||||
GlueDialog:Hide()
|
||||
AccountLoginUI:Hide()
|
||||
SurveyNotificationAccept:Enable()
|
||||
@@ -819,11 +971,19 @@ function AccountLogin_ShowSurveyNotification()
|
||||
end
|
||||
|
||||
function AccountLogin_SurveyNotificationDone(accepted)
|
||||
SurveyNotificationFrame:Hide()
|
||||
SurveyNotificationAccept:Disable()
|
||||
SurveyNotificationDecline:Disable()
|
||||
SurveyNotificationDone(accepted)
|
||||
AccountLoginUI:Show()
|
||||
if SurveyNotificationFrame then
|
||||
SurveyNotificationFrame:Hide()
|
||||
end
|
||||
if SurveyNotificationAccept then
|
||||
SurveyNotificationAccept:Disable()
|
||||
end
|
||||
if SurveyNotificationDecline then
|
||||
SurveyNotificationDecline:Disable()
|
||||
end
|
||||
if type(SurveyNotificationDone) == "function" then
|
||||
SurveyNotificationDone(accepted)
|
||||
end
|
||||
ApplyLaunchMode()
|
||||
end
|
||||
|
||||
function AccountLogin_ShowUserAgreements()
|
||||
|
||||
@@ -115,6 +115,18 @@
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="AccountLoginLauncherHint" inherits="GlueFontNormal" justifyH="CENTER" text="Вход в игру выполняется через лаунчер MoonWell.">
|
||||
<Size>
|
||||
<AbsDimension x="420" y="24"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="270"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<Texture name="WorldOfWarcraftRating" hidden="true">
|
||||
<Size>
|
||||
<AbsDimension x="128" y="128"/>
|
||||
@@ -195,7 +207,7 @@
|
||||
<!-- ====================================== -->
|
||||
<!-- CAMPOS DE TEXTO: USUARIO Y CONTRASEÑA -->
|
||||
<!-- ====================================== -->
|
||||
<EditBox name="AccountLoginAccountEdit" inherits="GlueDark_EditBoxTemplate" letters="320">
|
||||
<EditBox name="AccountLoginAccountEdit" inherits="GlueDark_EditBoxTemplate" letters="320" hidden="true">
|
||||
<Size x="200" y="37"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM">
|
||||
@@ -246,7 +258,7 @@
|
||||
</Scripts>
|
||||
</EditBox>
|
||||
|
||||
<EditBox name="AccountLoginPasswordEdit" inherits="GlueDark_EditBoxTemplate" letters="16" password="1">
|
||||
<EditBox name="AccountLoginPasswordEdit" inherits="GlueDark_EditBoxTemplate" letters="16" password="1" hidden="true">
|
||||
<Size x="200" y="37"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM">
|
||||
@@ -299,7 +311,7 @@
|
||||
<!-- ====================================== -->
|
||||
<!-- CHECKBOXES: GUARDAR CUENTA Y CONTRASEÑA -->
|
||||
<!-- ====================================== -->
|
||||
<Frame name="AccountLoginSaveOptionsFrame">
|
||||
<Frame name="AccountLoginSaveOptionsFrame" hidden="true">
|
||||
<Size x="200" y="60"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP" relativeTo="AccountLoginPasswordEdit" relativePoint="BOTTOM">
|
||||
@@ -883,7 +895,7 @@
|
||||
<!-- ====================================== -->
|
||||
<!-- BOTÓN DE LOGIN -->
|
||||
<!-- ====================================== -->
|
||||
<Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="LOGIN">
|
||||
<Button name="AccountLoginLoginButton" inherits="GlueButtonTemplateBlue" text="Авторизоваться">
|
||||
<Size x="200" y="55"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM">
|
||||
@@ -905,7 +917,7 @@
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
AccountLogin_Login();
|
||||
AccountLogin_PrimaryAction();
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
## Author: Rochet2 <https://github.com/Rochet2>
|
||||
## SavedVariables: AIO_sv, AIO_sv_Addons
|
||||
## SavedVariablesPerCharacter: AIO_sv_char
|
||||
## X-Hidden: 1
|
||||
|
||||
#dependencies
|
||||
Dep_LibWindow-1.1\LibStub.lua
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
-- This file is only there in standalone Ace3 and provides handy dev tool stuff I guess
|
||||
-- for now only /rl to reload your UI :)
|
||||
-- note the complete overkill use of AceAddon and console, ain't it cool?
|
||||
|
||||
-- GLOBALS: next, loadstring, ReloadUI, geterrorhandler
|
||||
-- GLOBALS: BINDING_HEADER_ACE3, BINDING_NAME_RELOADUI, Ace3, LibStub
|
||||
|
||||
-- BINDINGs labels
|
||||
BINDING_HEADER_ACE3 = "Ace3"
|
||||
BINDING_NAME_RELOADUI = "ReloadUI"
|
||||
--
|
||||
|
||||
local gui = LibStub("AceGUI-3.0")
|
||||
local reg = LibStub("AceConfigRegistry-3.0")
|
||||
local dialog = LibStub("AceConfigDialog-3.0")
|
||||
|
||||
Ace3 = LibStub("AceAddon-3.0"):NewAddon("Ace3", "AceConsole-3.0")
|
||||
local Ace3 = Ace3
|
||||
|
||||
local selectedgroup
|
||||
local frame
|
||||
local select
|
||||
local status = {}
|
||||
local configs = {}
|
||||
|
||||
local function frameOnClose()
|
||||
gui:Release(frame)
|
||||
frame = nil
|
||||
end
|
||||
|
||||
local function RefreshConfigs()
|
||||
for name in reg:IterateOptionsTables() do
|
||||
configs[name] = name
|
||||
end
|
||||
end
|
||||
|
||||
local function ConfigSelected(widget, event, value)
|
||||
selectedgroup = value
|
||||
dialog:Open(value, widget)
|
||||
end
|
||||
|
||||
local old_CloseSpecialWindows
|
||||
|
||||
-- GLOBALS: CloseSpecialWindows, next
|
||||
function Ace3:Open()
|
||||
if not old_CloseSpecialWindows then
|
||||
old_CloseSpecialWindows = CloseSpecialWindows
|
||||
CloseSpecialWindows = function()
|
||||
local found = old_CloseSpecialWindows()
|
||||
if frame then
|
||||
frame:Hide()
|
||||
return true
|
||||
end
|
||||
return found
|
||||
end
|
||||
end
|
||||
RefreshConfigs()
|
||||
if next(configs) == nil then
|
||||
self:Print("No Configs are Registered")
|
||||
return
|
||||
end
|
||||
|
||||
if not frame then
|
||||
frame = gui:Create("Frame")
|
||||
frame:ReleaseChildren()
|
||||
frame:SetTitle("Ace3 Options")
|
||||
frame:SetLayout("FILL")
|
||||
frame:SetCallback("OnClose", frameOnClose)
|
||||
|
||||
select = gui:Create("DropdownGroup")
|
||||
select:SetGroupList(configs)
|
||||
select:SetCallback("OnGroupSelected", ConfigSelected)
|
||||
frame:AddChild(select)
|
||||
end
|
||||
if not selectedgroup then
|
||||
selectedgroup = next(configs)
|
||||
end
|
||||
select:SetGroup(selectedgroup)
|
||||
frame:Show()
|
||||
end
|
||||
|
||||
local function RefreshOnUpdate(this)
|
||||
select:SetGroup(selectedgroup)
|
||||
this:SetScript("OnUpdate", nil)
|
||||
end
|
||||
|
||||
function Ace3:ConfigTableChanged(event, appName)
|
||||
if selectedgroup == appName and frame then
|
||||
frame.frame:SetScript("OnUpdate", RefreshOnUpdate)
|
||||
end
|
||||
end
|
||||
|
||||
reg.RegisterCallback(Ace3, "ConfigTableChange", "ConfigTableChanged")
|
||||
|
||||
function Ace3:PrintCmd(input)
|
||||
input = input:trim():match("^(.-);*$")
|
||||
local func, err = loadstring("LibStub(\"AceConsole-3.0\"):Print(" .. input .. ")")
|
||||
if not func then
|
||||
LibStub("AceConsole-3.0"):Print("Error: " .. err)
|
||||
else
|
||||
func()
|
||||
end
|
||||
end
|
||||
|
||||
function Ace3:OnInitialize()
|
||||
self:RegisterChatCommand("ace3", function() self:Open() end)
|
||||
self:RegisterChatCommand("rl", function() ReloadUI() end)
|
||||
self:RegisterChatCommand("print", "PrintCmd")
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
## Interface: 30300
|
||||
## X-Curse-Packaged-Version: r922
|
||||
## X-Curse-Project-Name: Ace3
|
||||
## X-Curse-Project-ID: ace3
|
||||
## X-Curse-Repository-ID: wow/ace3/mainline
|
||||
|
||||
## Title: Lib: Ace3
|
||||
## Notes: AddOn development framework
|
||||
## Author: Ace3 Development Team
|
||||
## Dependencies: LibStub, CallbackHandler-1.0
|
||||
## X-Website: http://www.wowace.com
|
||||
## X-Category: Library
|
||||
## X-License: Limited BSD
|
||||
|
||||
AceAddon-3.0\AceAddon-3.0.xml
|
||||
AceEvent-3.0\AceEvent-3.0.xml
|
||||
AceTimer-3.0\AceTimer-3.0.xml
|
||||
AceBucket-3.0\AceBucket-3.0.xml
|
||||
AceHook-3.0\AceHook-3.0.xml
|
||||
AceDB-3.0\AceDB-3.0.xml
|
||||
AceDBOptions-3.0\AceDBOptions-3.0.xml
|
||||
AceLocale-3.0\AceLocale-3.0.xml
|
||||
AceConsole-3.0\AceConsole-3.0.xml
|
||||
AceGUI-3.0\AceGUI-3.0.xml
|
||||
AceConfig-3.0\AceConfig-3.0.xml
|
||||
AceComm-3.0\AceComm-3.0.xml
|
||||
AceTab-3.0\AceTab-3.0.xml
|
||||
AceSerializer-3.0\AceSerializer-3.0.xml
|
||||
|
||||
Ace3.lua
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
-- as well as associate it with a slash command.
|
||||
-- @class file
|
||||
-- @name AceConfig-3.0
|
||||
-- @release $Id: AceConfig-3.0.lua 969 2010-10-07 02:11:48Z shefki $
|
||||
-- @release $Id: AceConfig-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $
|
||||
|
||||
--[[
|
||||
AceConfig-3.0
|
||||
@@ -19,8 +19,8 @@ if not AceConfig then return end
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
local cfgcmd = LibStub("AceConfigCmd-3.0")
|
||||
--TODO: local cfgdlg = LibStub("AceConfigDialog-3.0", true)
|
||||
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0", true)
|
||||
local cfgdlg = LibStub("AceConfigDialog-3.0")
|
||||
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local pcall, error, type, pairs = pcall, error, type, pairs
|
||||
@@ -36,7 +36,7 @@ local pcall, error, type, pairs = pcall, error, type, pairs
|
||||
-- You can supply a slash command (or a table of slash commands) to register with AceConfigCmd directly.
|
||||
-- @paramsig appName, options [, slashcmd]
|
||||
-- @param appName The application name for the config table.
|
||||
-- @param options The option table (or a function to generate one on demand). http://www.wowace.com/addons/ace3/pages/ace-config-3-0-options-tables/
|
||||
-- @param options The option table (or a function to generate one on demand)
|
||||
-- @param slashcmd A slash command to register for the option table, or a table of slash commands.
|
||||
-- @usage
|
||||
-- local AceConfig = LibStub("AceConfig-3.0")
|
||||
+56
-68
@@ -1,10 +1,10 @@
|
||||
--- AceConfigDialog-3.0 generates AceGUI-3.0 based windows based on option tables.
|
||||
-- @class file
|
||||
-- @name AceConfigDialog-3.0
|
||||
-- @release $Id: AceConfigDialog-3.0.lua 967 2010-09-25 08:20:55Z nevcairiel $
|
||||
-- @release $Id: AceConfigDialog-3.0.lua 921 2010-05-09 15:49:14Z nevcairiel $
|
||||
|
||||
local LibStub = LibStub
|
||||
local MAJOR, MINOR = "AceConfigDialog-3.0", 50
|
||||
local MAJOR, MINOR = "AceConfigDialog-3.0", 47
|
||||
local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigDialog then return end
|
||||
@@ -15,7 +15,6 @@ AceConfigDialog.frame = AceConfigDialog.frame or CreateFrame("Frame")
|
||||
|
||||
AceConfigDialog.frame.apps = AceConfigDialog.frame.apps or {}
|
||||
AceConfigDialog.frame.closing = AceConfigDialog.frame.closing or {}
|
||||
AceConfigDialog.frame.closeAllOverride = AceConfigDialog.frame.closeAllOverride or {}
|
||||
|
||||
local gui = LibStub("AceGUI-3.0")
|
||||
local reg = LibStub("AceConfigRegistry-3.0")
|
||||
@@ -24,8 +23,8 @@ local reg = LibStub("AceConfigRegistry-3.0")
|
||||
local tconcat, tinsert, tsort, tremove = table.concat, table.insert, table.sort, table.remove
|
||||
local strmatch, format = string.match, string.format
|
||||
local assert, loadstring, error = assert, loadstring, error
|
||||
local pairs, next, select, type, unpack, wipe = pairs, next, select, type, unpack, wipe
|
||||
local rawset, tostring, tonumber = rawset, tostring, tonumber
|
||||
local pairs, next, select, type, unpack = pairs, next, select, type, unpack
|
||||
local rawset, tostring = rawset, tostring
|
||||
local math_min, math_max, math_floor = math.min, math.max, math.floor
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
@@ -77,7 +76,7 @@ Dispatchers[0] = function(func)
|
||||
end
|
||||
|
||||
local function safecall(func, ...)
|
||||
return Dispatchers[select("#", ...)](func, ...)
|
||||
return Dispatchers[select('#', ...)](func, ...)
|
||||
end
|
||||
|
||||
local width_multiplier = 170
|
||||
@@ -104,7 +103,7 @@ Group Types
|
||||
local new, del, copy
|
||||
--newcount, delcount,createdcount,cached = 0,0,0
|
||||
do
|
||||
local pool = setmetatable({},{__mode="k"})
|
||||
local pool = setmetatable({},{__mode='k'})
|
||||
function new()
|
||||
--newcount = newcount + 1
|
||||
local t = next(pool)
|
||||
@@ -241,7 +240,7 @@ local function GetOptionsMemberValue(membername, option, options, path, appName,
|
||||
info.handler = handler
|
||||
info.option = option
|
||||
info.type = option.type
|
||||
info.uiType = "dialog"
|
||||
info.uiType = 'dialog'
|
||||
info.uiName = MAJOR
|
||||
|
||||
local a, b, c ,d
|
||||
@@ -322,8 +321,8 @@ local function compareOptions(a,b)
|
||||
end
|
||||
local OrderA, OrderB = tempOrders[a] or 100, tempOrders[b] or 100
|
||||
if OrderA == OrderB then
|
||||
local NameA = (type(tempNames[a]) == "string") and tempNames[a] or ""
|
||||
local NameB = (type(tempNames[b]) == "string") and tempNames[b] or ""
|
||||
local NameA = (type(tempNames[a] == "string") and tempNames[a]) or ""
|
||||
local NameB = (type(tempNames[b] == "string") and tempNames[b]) or ""
|
||||
return NameA:upper() < NameB:upper()
|
||||
end
|
||||
if OrderA < 0 then
|
||||
@@ -479,7 +478,7 @@ function AceConfigDialog:SelectGroup(appName, ...)
|
||||
local treevalue
|
||||
local treestatus
|
||||
|
||||
for n = 1, select("#",...) do
|
||||
for n = 1, select('#',...) do
|
||||
local key = select(n, ...)
|
||||
|
||||
if group.childGroups == "tab" or group.childGroups == "select" then
|
||||
@@ -543,7 +542,7 @@ local function OptionOnMouseOver(widget, event)
|
||||
|
||||
GameTooltip:SetText(name, 1, .82, 0, 1)
|
||||
|
||||
if opt.type == "multiselect" then
|
||||
if opt.type == 'multiselect' then
|
||||
GameTooltip:AddLine(user.text,0.5, 0.5, 0.8, 1)
|
||||
end
|
||||
if type(desc) == "string" then
|
||||
@@ -562,10 +561,10 @@ end
|
||||
|
||||
local function GetFuncName(option)
|
||||
local type = option.type
|
||||
if type == "execute" then
|
||||
return "func"
|
||||
if type == 'execute' then
|
||||
return 'func'
|
||||
else
|
||||
return "set"
|
||||
return 'set'
|
||||
end
|
||||
end
|
||||
local function confirmPopup(appName, rootframe, basepath, info, message, func, ...)
|
||||
@@ -595,7 +594,7 @@ local function confirmPopup(appName, rootframe, basepath, info, message, func, .
|
||||
AceConfigDialog:Open(appName, rootframe, unpack(basepath or emptyTbl))
|
||||
del(info)
|
||||
end
|
||||
for i = 1, select("#", ...) do
|
||||
for i = 1, select('#', ...) do
|
||||
t[i] = select(i, ...) or false
|
||||
end
|
||||
t.timeout = 0
|
||||
@@ -654,7 +653,7 @@ local function ActivateControl(widget, event, ...)
|
||||
info.handler = handler
|
||||
info.option = option
|
||||
info.type = option.type
|
||||
info.uiType = "dialog"
|
||||
info.uiType = 'dialog'
|
||||
info.uiName = MAJOR
|
||||
|
||||
local name
|
||||
@@ -765,13 +764,13 @@ local function ActivateControl(widget, event, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local iscustom = user.rootframe:GetUserData("iscustom")
|
||||
local iscustom = user.rootframe:GetUserData('iscustom')
|
||||
local rootframe
|
||||
|
||||
if iscustom then
|
||||
rootframe = user.rootframe
|
||||
end
|
||||
local basepath = user.rootframe:GetUserData("basepath")
|
||||
local basepath = user.rootframe:GetUserData('basepath')
|
||||
if type(func) == "string" then
|
||||
if handler and handler[func] then
|
||||
confirmPopup(user.appName, rootframe, basepath, info, confirmText, handler[func], handler, info, ...)
|
||||
@@ -799,8 +798,8 @@ local function ActivateControl(widget, event, ...)
|
||||
|
||||
|
||||
|
||||
local iscustom = user.rootframe:GetUserData("iscustom")
|
||||
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl
|
||||
local iscustom = user.rootframe:GetUserData('iscustom')
|
||||
local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
|
||||
--full refresh of the frame, some controls dont cause this on all events
|
||||
if option.type == "color" then
|
||||
if event == "OnValueConfirmed" then
|
||||
@@ -835,16 +834,13 @@ local function ActivateControl(widget, event, ...)
|
||||
end
|
||||
|
||||
local function ActivateSlider(widget, event, value)
|
||||
local option = widget:GetUserData("option")
|
||||
local option = widget:GetUserData('option')
|
||||
local min, max, step = option.min or (not option.softMin and 0 or nil), option.max or (not option.softMax and 100 or nil), option.step
|
||||
if min then
|
||||
if min and max then
|
||||
if step then
|
||||
value = math_floor((value - min) / step + 0.5) * step + min
|
||||
end
|
||||
value = math_max(value, min)
|
||||
end
|
||||
if max then
|
||||
value = math_min(value, max)
|
||||
value = math_max(math_min(value,max),min)
|
||||
end
|
||||
ActivateControl(widget,event,value)
|
||||
end
|
||||
@@ -852,10 +848,10 @@ end
|
||||
--called from a checkbox that is part of an internally created multiselect group
|
||||
--this type is safe to refresh on activation of one control
|
||||
local function ActivateMultiControl(widget, event, ...)
|
||||
ActivateControl(widget, event, widget:GetUserData("value"), ...)
|
||||
ActivateControl(widget, event, widget:GetUserData('value'), ...)
|
||||
local user = widget:GetUserDataTable()
|
||||
local iscustom = user.rootframe:GetUserData("iscustom")
|
||||
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl
|
||||
local iscustom = user.rootframe:GetUserData('iscustom')
|
||||
local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
|
||||
if iscustom then
|
||||
AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath))
|
||||
else
|
||||
@@ -866,8 +862,8 @@ end
|
||||
local function MultiControlOnClosed(widget, event, ...)
|
||||
local user = widget:GetUserDataTable()
|
||||
if user.valuechanged then
|
||||
local iscustom = user.rootframe:GetUserData("iscustom")
|
||||
local basepath = user.rootframe:GetUserData("basepath") or emptyTbl
|
||||
local iscustom = user.rootframe:GetUserData('iscustom')
|
||||
local basepath = user.rootframe:GetUserData('basepath') or emptyTbl
|
||||
if iscustom then
|
||||
AceConfigDialog:Open(user.appName, user.rootframe, unpack(basepath))
|
||||
else
|
||||
@@ -877,7 +873,7 @@ local function MultiControlOnClosed(widget, event, ...)
|
||||
end
|
||||
|
||||
local function FrameOnClose(widget, event)
|
||||
local appName = widget:GetUserData("appName")
|
||||
local appName = widget:GetUserData('appName')
|
||||
AceConfigDialog.OpenFrames[appName] = nil
|
||||
gui:Release(widget)
|
||||
end
|
||||
@@ -1088,7 +1084,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
|
||||
local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
|
||||
local image, width, height = GetOptionsMemberValue("image",v, options, path, appName)
|
||||
|
||||
if type(image) == "string" then
|
||||
if type(image) == 'string' then
|
||||
control = gui:Create("Icon")
|
||||
if not width then
|
||||
width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
|
||||
@@ -1096,7 +1092,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
|
||||
if not height then
|
||||
height = GetOptionsMemberValue("imageHeight",v, options, path, appName)
|
||||
end
|
||||
if type(imageCoords) == "table" then
|
||||
if type(imageCoords) == 'table' then
|
||||
control:SetImage(image, unpack(imageCoords))
|
||||
else
|
||||
control:SetImage(image)
|
||||
@@ -1150,8 +1146,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
|
||||
local image = GetOptionsMemberValue("image", v, options, path, appName)
|
||||
local imageCoords = GetOptionsMemberValue("imageCoords", v, options, path, appName)
|
||||
|
||||
if type(image) == "string" then
|
||||
if type(imageCoords) == "table" then
|
||||
if type(image) == 'string' then
|
||||
if type(imageCoords) == 'table' then
|
||||
control:SetImage(image, unpack(imageCoords))
|
||||
else
|
||||
control:SetImage(image)
|
||||
@@ -1243,8 +1239,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
|
||||
local text = values[value]
|
||||
local check = gui:Create("CheckBox")
|
||||
check:SetLabel(text)
|
||||
check:SetUserData("value", value)
|
||||
check:SetUserData("text", text)
|
||||
check:SetUserData('value', value)
|
||||
check:SetUserData('text', text)
|
||||
check:SetDisabled(disabled)
|
||||
check:SetTriState(v.tristate)
|
||||
check:SetValue(GetOptionsMemberValue("get",v, options, path, appName, value))
|
||||
@@ -1304,14 +1300,14 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
|
||||
local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
|
||||
local image, width, height = GetOptionsMemberValue("image",v, options, path, appName)
|
||||
|
||||
if type(image) == "string" then
|
||||
if type(image) == 'string' then
|
||||
if not width then
|
||||
width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
|
||||
end
|
||||
if not height then
|
||||
height = GetOptionsMemberValue("imageHeight",v, options, path, appName)
|
||||
end
|
||||
if type(imageCoords) == "table" then
|
||||
if type(imageCoords) == 'table' then
|
||||
control:SetImage(image, unpack(imageCoords))
|
||||
else
|
||||
control:SetImage(image)
|
||||
@@ -1362,7 +1358,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
|
||||
end
|
||||
|
||||
local function BuildPath(path, ...)
|
||||
for i = 1, select("#",...) do
|
||||
for i = 1, select('#',...) do
|
||||
tinsert(path, (select(i,...)))
|
||||
end
|
||||
end
|
||||
@@ -1665,12 +1661,9 @@ local function RefreshOnUpdate(this)
|
||||
|
||||
if this.closeAll then
|
||||
for k, v in pairs(AceConfigDialog.OpenFrames) do
|
||||
if not this.closeAllOverride[k] then
|
||||
v:Hide()
|
||||
end
|
||||
v:Hide()
|
||||
end
|
||||
this.closeAll = nil
|
||||
wipe(this.closeAllOverride)
|
||||
end
|
||||
|
||||
for appName in pairs(this.apps) do
|
||||
@@ -1682,7 +1675,7 @@ local function RefreshOnUpdate(this)
|
||||
for key, widget in pairs(AceConfigDialog.BlizOptions[appName]) do
|
||||
local user = widget:GetUserDataTable()
|
||||
if widget:IsVisible() then
|
||||
AceConfigDialog:Open(widget:GetUserData("appName"), widget, unpack(user.basepath or emptyTbl))
|
||||
AceConfigDialog:Open(widget:GetUserData('appName'), widget, unpack(user.basepath or emptyTbl))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1767,7 +1760,7 @@ function AceConfigDialog:Open(appName, container, ...)
|
||||
tinsert(path, container)
|
||||
container = nil
|
||||
end
|
||||
for n = 1, select("#",...) do
|
||||
for n = 1, select('#',...) do
|
||||
tinsert(path, (select(n, ...)))
|
||||
end
|
||||
|
||||
@@ -1775,10 +1768,10 @@ function AceConfigDialog:Open(appName, container, ...)
|
||||
if container then
|
||||
f = container
|
||||
f:ReleaseChildren()
|
||||
f:SetUserData("appName", appName)
|
||||
f:SetUserData("iscustom", true)
|
||||
f:SetUserData('appName', appName)
|
||||
f:SetUserData('iscustom', true)
|
||||
if #path > 0 then
|
||||
f:SetUserData("basepath", copy(path))
|
||||
f:SetUserData('basepath', copy(path))
|
||||
end
|
||||
local status = AceConfigDialog:GetStatusTable(appName)
|
||||
if not status.width then
|
||||
@@ -1802,9 +1795,9 @@ function AceConfigDialog:Open(appName, container, ...)
|
||||
end
|
||||
f:ReleaseChildren()
|
||||
f:SetCallback("OnClose", FrameOnClose)
|
||||
f:SetUserData("appName", appName)
|
||||
f:SetUserData('appName', appName)
|
||||
if #path > 0 then
|
||||
f:SetUserData("basepath", copy(path))
|
||||
f:SetUserData('basepath', copy(path))
|
||||
end
|
||||
f:SetTitle(name or "")
|
||||
local status = AceConfigDialog:GetStatusTable(appName)
|
||||
@@ -1816,11 +1809,6 @@ function AceConfigDialog:Open(appName, container, ...)
|
||||
f:Show()
|
||||
end
|
||||
del(path)
|
||||
|
||||
if AceConfigDialog.frame.closeAll then
|
||||
-- close all is set, but thats not good, since we're just opening here, so force it
|
||||
AceConfigDialog.frame.closeAllOverride[appName] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- convert pre-39 BlizOptions structure to the new format
|
||||
@@ -1828,7 +1816,7 @@ if oldminor and oldminor < 39 and AceConfigDialog.BlizOptions then
|
||||
local old = AceConfigDialog.BlizOptions
|
||||
local new = {}
|
||||
for key, widget in pairs(old) do
|
||||
local appName = widget:GetUserData("appName")
|
||||
local appName = widget:GetUserData('appName')
|
||||
if not new[appName] then new[appName] = {} end
|
||||
new[appName][key] = widget
|
||||
end
|
||||
@@ -1838,12 +1826,12 @@ else
|
||||
end
|
||||
|
||||
local function FeedToBlizPanel(widget, event)
|
||||
local path = widget:GetUserData("path")
|
||||
AceConfigDialog:Open(widget:GetUserData("appName"), widget, unpack(path or emptyTbl))
|
||||
local path = widget:GetUserData('path')
|
||||
AceConfigDialog:Open(widget:GetUserData('appName'), widget, unpack(path or emptyTbl))
|
||||
end
|
||||
|
||||
local function ClearBlizPanel(widget, event)
|
||||
local appName = widget:GetUserData("appName")
|
||||
local appName = widget:GetUserData('appName')
|
||||
AceConfigDialog.frame.closing[appName] = true
|
||||
AceConfigDialog.frame:SetScript("OnUpdate", RefreshOnUpdate)
|
||||
end
|
||||
@@ -1869,8 +1857,8 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
|
||||
local BlizOptions = AceConfigDialog.BlizOptions
|
||||
|
||||
local key = appName
|
||||
for n = 1, select("#", ...) do
|
||||
key = key.."\001"..select(n, ...)
|
||||
for n = 1, select('#', ...) do
|
||||
key = key..'\001'..select(n, ...)
|
||||
end
|
||||
|
||||
if not BlizOptions[appName] then
|
||||
@@ -1883,13 +1871,13 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
|
||||
group:SetName(name or appName, parent)
|
||||
|
||||
group:SetTitle(name or appName)
|
||||
group:SetUserData("appName", appName)
|
||||
if select("#", ...) > 0 then
|
||||
group:SetUserData('appName', appName)
|
||||
if select('#', ...) > 0 then
|
||||
local path = {}
|
||||
for n = 1, select("#",...) do
|
||||
for n = 1, select('#',...) do
|
||||
tinsert(path, (select(n, ...)))
|
||||
end
|
||||
group:SetUserData("path", path)
|
||||
group:SetUserData('path', path)
|
||||
end
|
||||
group:SetCallback("OnShow", FeedToBlizPanel)
|
||||
group:SetCallback("OnHide", ClearBlizPanel)
|
||||
+6
-6
@@ -39,8 +39,8 @@
|
||||
-- end
|
||||
-- @class file
|
||||
-- @name AceDB-3.0.lua
|
||||
-- @release $Id: AceDB-3.0.lua 940 2010-06-19 08:01:47Z nevcairiel $
|
||||
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 21
|
||||
-- @release $Id: AceDB-3.0.lua 914 2010-03-08 12:09:22Z nevcairiel $
|
||||
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 20
|
||||
local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)
|
||||
|
||||
if not AceDB then return end -- No upgrade needed
|
||||
@@ -507,11 +507,11 @@ function DBObjectLib:DeleteProfile(name, silent)
|
||||
error("Cannot delete the active profile in an AceDBObject.", 2)
|
||||
end
|
||||
|
||||
if not rawget(self.profiles, name) and not silent then
|
||||
if not rawget(self.sv.profiles, name) and not silent then
|
||||
error("Cannot delete profile '" .. name .. "'. It does not exist.", 2)
|
||||
end
|
||||
|
||||
self.profiles[name] = nil
|
||||
self.sv.profiles[name] = nil
|
||||
|
||||
-- populate to child namespaces
|
||||
if self.children then
|
||||
@@ -537,7 +537,7 @@ function DBObjectLib:CopyProfile(name, silent)
|
||||
error("Cannot have the same source and destination profiles.", 2)
|
||||
end
|
||||
|
||||
if not rawget(self.profiles, name) and not silent then
|
||||
if not rawget(self.sv.profiles, name) and not silent then
|
||||
error("Cannot copy profile '" .. name .. "'. It does not exist.", 2)
|
||||
end
|
||||
|
||||
@@ -545,7 +545,7 @@ function DBObjectLib:CopyProfile(name, silent)
|
||||
DBObjectLib.ResetProfile(self, nil, true)
|
||||
|
||||
local profile = self.profile
|
||||
local source = self.profiles[name]
|
||||
local source = self.sv.profiles[name]
|
||||
|
||||
copyTable(source, profile)
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
--- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles.
|
||||
-- @class file
|
||||
-- @name AceDBOptions-3.0
|
||||
-- @release $Id: AceDBOptions-3.0.lua 938 2010-06-13 07:21:38Z nevcairiel $
|
||||
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 12
|
||||
-- @release $Id: AceDBOptions-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
|
||||
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 11
|
||||
local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR)
|
||||
|
||||
if not AceDBOptions then return end -- No upgrade needed
|
||||
@@ -30,7 +30,7 @@ local L = {
|
||||
reset_desc = "Reset the current profile back to its default values, in case your configuration is broken, or you simply want to start over.",
|
||||
reset = "Reset Profile",
|
||||
reset_sub = "Reset the current profile to the default",
|
||||
choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already existing profiles.",
|
||||
choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already exisiting profiles.",
|
||||
new = "New",
|
||||
new_sub = "Create a new empty profile.",
|
||||
choose = "Existing Profiles",
|
||||
+141
-70
@@ -5,7 +5,7 @@
|
||||
--
|
||||
-- **Note**: When using AceGUI-3.0 directly, please do not modify the frames of the widgets directly,
|
||||
-- as any "unknown" change to the widgets will cause addons that get your widget out of the widget pool
|
||||
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we"ll
|
||||
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we'll
|
||||
-- implement a proper API to modify it.
|
||||
-- @usage
|
||||
-- local AceGUI = LibStub("AceGUI-3.0")
|
||||
@@ -24,8 +24,8 @@
|
||||
-- f:AddChild(btn)
|
||||
-- @class file
|
||||
-- @name AceGUI-3.0
|
||||
-- @release $Id: AceGUI-3.0.lua 924 2010-05-13 15:12:20Z nevcairiel $
|
||||
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 33
|
||||
-- @release $Id: AceGUI-3.0.lua 919 2010-05-09 11:36:03Z nevcairiel $
|
||||
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 31
|
||||
local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR)
|
||||
|
||||
if not AceGUI then return end -- No upgrade needed
|
||||
@@ -98,7 +98,7 @@ Dispatchers[0] = function(func)
|
||||
end
|
||||
|
||||
local function safecall(func, ...)
|
||||
return Dispatchers[select("#", ...)](func, ...)
|
||||
return Dispatchers[select('#', ...)](func, ...)
|
||||
end
|
||||
|
||||
-- Recycling functions
|
||||
@@ -173,16 +173,16 @@ function AceGUI:Create(type)
|
||||
if WidgetRegistry[type] then
|
||||
local widget = newWidget(type)
|
||||
|
||||
if rawget(widget, "Acquire") then
|
||||
if rawget(widget,'Acquire') then
|
||||
widget.OnAcquire = widget.Acquire
|
||||
widget.Acquire = nil
|
||||
elseif rawget(widget, "Aquire") then
|
||||
elseif rawget(widget,'Aquire') then
|
||||
widget.OnAcquire = widget.Aquire
|
||||
widget.Aquire = nil
|
||||
end
|
||||
|
||||
if rawget(widget, "Release") then
|
||||
widget.OnRelease = rawget(widget, "Release")
|
||||
if rawget(widget,'Release') then
|
||||
widget.OnRelease = rawget(widget,'Release')
|
||||
widget.Release = nil
|
||||
end
|
||||
|
||||
@@ -191,8 +191,8 @@ function AceGUI:Create(type)
|
||||
else
|
||||
error(("Widget type %s doesn't supply an OnAcquire Function"):format(type))
|
||||
end
|
||||
-- Set the default Layout ("List")
|
||||
safecall(widget.SetLayout, widget, "List")
|
||||
-- Set the default Layout ('List')
|
||||
safecall(widget.SetLayout, widget, 'List')
|
||||
safecall(widget.ResumeLayout, widget)
|
||||
return widget
|
||||
end
|
||||
@@ -204,14 +204,14 @@ end
|
||||
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
|
||||
-- @param widget The widget to release
|
||||
function AceGUI:Release(widget)
|
||||
safecall(widget.PauseLayout, widget)
|
||||
safecall( widget.PauseLayout, widget )
|
||||
widget:Fire("OnRelease")
|
||||
safecall(widget.ReleaseChildren, widget)
|
||||
safecall( widget.ReleaseChildren, widget )
|
||||
|
||||
if widget.OnRelease then
|
||||
widget:OnRelease()
|
||||
-- else
|
||||
-- error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
|
||||
else
|
||||
error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
|
||||
end
|
||||
for k in pairs(widget.userdata) do
|
||||
widget.userdata[k] = nil
|
||||
@@ -267,6 +267,7 @@ end
|
||||
--[[
|
||||
Widgets must provide the following functions
|
||||
OnAcquire() - Called when the object is acquired, should set everything to a default hidden state
|
||||
OnRelease() - Called when the object is Released, should remove any anchors and hide the Widget
|
||||
|
||||
And the following members
|
||||
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
|
||||
@@ -280,7 +281,6 @@ end
|
||||
content - frame or derivitive that children will be anchored to
|
||||
|
||||
The Widget can supply the following Optional Members
|
||||
:OnRelease() - Called when the object is Released, should remove any additional anchors and clear any data
|
||||
:OnWidthSet(width) - Called when the width of the widget is changed
|
||||
:OnHeightSet(height) - Called when the height of the widget is changed
|
||||
Widgets should not use the OnSizeChanged events of thier frame or content members, use these methods instead
|
||||
@@ -294,6 +294,17 @@ end
|
||||
-- Widget Base Template --
|
||||
--------------------------
|
||||
do
|
||||
local function fixlevels(parent,...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
while child do
|
||||
child:SetFrameLevel(parent:GetFrameLevel()+1)
|
||||
fixlevels(child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local WidgetBase = AceGUI.WidgetBase
|
||||
|
||||
WidgetBase.SetParent = function(self, parent)
|
||||
@@ -301,6 +312,7 @@ do
|
||||
frame:SetParent(nil)
|
||||
frame:SetParent(parent.content)
|
||||
self.parent = parent
|
||||
--fixlevels(parent.frame,parent.frame:GetChildren())
|
||||
end
|
||||
|
||||
WidgetBase.SetCallback = function(self, name, func)
|
||||
@@ -433,7 +445,7 @@ do
|
||||
if self.LayoutPaused then
|
||||
return
|
||||
end
|
||||
safecall(self.LayoutFunc, self.content, self.children)
|
||||
safecall(self.LayoutFunc,self.content, self.children)
|
||||
end
|
||||
|
||||
--call this function to layout, makes sure layed out objects get a frame to get sizes etc
|
||||
@@ -512,7 +524,7 @@ do
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(WidgetContainerBase, {__index=WidgetBase})
|
||||
setmetatable(WidgetContainerBase,{__index=WidgetBase})
|
||||
|
||||
--One of these function should be called on each Widget Instance as part of its creation process
|
||||
|
||||
@@ -525,11 +537,10 @@ do
|
||||
widget.base = WidgetContainerBase
|
||||
widget.content.obj = widget
|
||||
widget.frame.obj = widget
|
||||
widget.content:SetScript("OnSizeChanged", ContentResize)
|
||||
widget.frame:SetScript("OnSizeChanged", FrameResize)
|
||||
setmetatable(widget, {__index = WidgetContainerBase})
|
||||
widget.content:SetScript("OnSizeChanged",ContentResize)
|
||||
widget.frame:SetScript("OnSizeChanged",FrameResize)
|
||||
setmetatable(widget,{__index=WidgetContainerBase})
|
||||
widget:SetLayout("List")
|
||||
return widget
|
||||
end
|
||||
|
||||
--- Register a widget-class as a widget.
|
||||
@@ -539,9 +550,8 @@ do
|
||||
widget.events = {}
|
||||
widget.base = WidgetBase
|
||||
widget.frame.obj = widget
|
||||
widget.frame:SetScript("OnSizeChanged", FrameResize)
|
||||
setmetatable(widget, {__index = WidgetBase})
|
||||
return widget
|
||||
widget.frame:SetScript("OnSizeChanged",FrameResize)
|
||||
setmetatable(widget,{__index=WidgetBase})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -614,6 +624,50 @@ function AceGUI:GetWidgetVersion(type)
|
||||
return WidgetVersions[type]
|
||||
end
|
||||
|
||||
--[[ Widget Template
|
||||
|
||||
--------------------------
|
||||
-- Widget Name --
|
||||
--------------------------
|
||||
do
|
||||
local Type = "Type"
|
||||
|
||||
local function OnAcquire(self)
|
||||
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Frame",nil,UIParent)
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
--Container Support
|
||||
--local content = CreateFrame("Frame",nil,frame)
|
||||
--self.content = content
|
||||
|
||||
--AceGUI:RegisterAsContainer(self)
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor)
|
||||
end
|
||||
|
||||
|
||||
]]
|
||||
|
||||
-------------
|
||||
-- Layouts --
|
||||
-------------
|
||||
@@ -622,13 +676,15 @@ end
|
||||
A Layout is a func that takes 2 parameters
|
||||
content - the frame that widgets will be placed inside
|
||||
children - a table containing the widgets to layout
|
||||
|
||||
]]
|
||||
|
||||
-- Very simple Layout, Children are stacked on top of each other down the left side
|
||||
AceGUI:RegisterLayout("List",
|
||||
function(content, children)
|
||||
local height = 0
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
function(content, children)
|
||||
|
||||
local height = 0
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
|
||||
@@ -636,21 +692,25 @@ AceGUI:RegisterLayout("List",
|
||||
frame:ClearAllPoints()
|
||||
frame:Show()
|
||||
if i == 1 then
|
||||
frame:SetPoint("TOPLEFT", content)
|
||||
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", children[i-1].frame, "BOTTOMLEFT")
|
||||
frame:SetPoint("TOPLEFT",children[i-1].frame,"BOTTOMLEFT",0,0)
|
||||
end
|
||||
|
||||
if child.width == "fill" then
|
||||
child:SetWidth(width)
|
||||
frame:SetPoint("RIGHT", content)
|
||||
|
||||
frame:SetPoint("RIGHT",content,"RIGHT")
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(content.width or content:GetWidth())
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
elseif child.width == "relative" then
|
||||
child:SetWidth(width * child.relWidth)
|
||||
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(content.width or content:GetWidth())
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
@@ -658,43 +718,45 @@ AceGUI:RegisterLayout("List",
|
||||
|
||||
height = height + (frame.height or frame:GetHeight() or 0)
|
||||
end
|
||||
safecall(content.obj.LayoutFinished, content.obj, nil, height)
|
||||
end)
|
||||
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, height )
|
||||
end
|
||||
)
|
||||
|
||||
-- A single control fills the whole content area
|
||||
AceGUI:RegisterLayout("Fill",
|
||||
function(content, children)
|
||||
function(content, children)
|
||||
if children[1] then
|
||||
children[1]:SetWidth(content:GetWidth() or 0)
|
||||
children[1]:SetHeight(content:GetHeight() or 0)
|
||||
children[1].frame:SetAllPoints(content)
|
||||
children[1].frame:Show()
|
||||
safecall(content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight())
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight() )
|
||||
end
|
||||
end)
|
||||
|
||||
end
|
||||
)
|
||||
|
||||
AceGUI:RegisterLayout("Flow",
|
||||
function(content, children)
|
||||
--used height so far
|
||||
local height = 0
|
||||
--width used in the current row
|
||||
local usedwidth = 0
|
||||
--height of the current row
|
||||
local rowheight = 0
|
||||
local rowoffset = 0
|
||||
local lastrowoffset
|
||||
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
|
||||
--control at the start of the row
|
||||
local rowstart
|
||||
function(content, children)
|
||||
--used height so far
|
||||
local height = 0
|
||||
--width used in the current row
|
||||
local usedwidth = 0
|
||||
--height of the current row
|
||||
local rowheight = 0
|
||||
local rowoffset = 0
|
||||
local lastrowoffset
|
||||
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
|
||||
--control at the start of the row
|
||||
local rowstart
|
||||
local rowstartoffset
|
||||
local lastrowstart
|
||||
local isfullheight
|
||||
|
||||
local frameoffset
|
||||
local lastframeoffset
|
||||
local oversize
|
||||
local lastrowstart
|
||||
local isfullheight
|
||||
|
||||
local frameoffset
|
||||
local lastframeoffset
|
||||
local oversize
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
oversize = nil
|
||||
@@ -717,7 +779,7 @@ AceGUI:RegisterLayout("Flow",
|
||||
frame:ClearAllPoints()
|
||||
if i == 1 then
|
||||
-- anchor the first control to the top left
|
||||
frame:SetPoint("TOPLEFT", content)
|
||||
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
|
||||
rowheight = frameheight
|
||||
rowoffset = frameoffset
|
||||
rowstart = frame
|
||||
@@ -736,7 +798,7 @@ AceGUI:RegisterLayout("Flow",
|
||||
break
|
||||
end
|
||||
--anchor the previous row, we will now know its height and offset
|
||||
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3))
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
|
||||
height = height + rowheight + 3
|
||||
--save this as the rowstart so we can anchor it after the row is complete and we have the max height and offset of controls in it
|
||||
rowstart = frame
|
||||
@@ -754,21 +816,25 @@ AceGUI:RegisterLayout("Flow",
|
||||
|
||||
--offset is always the larger of the two offsets
|
||||
rowoffset = math_max(rowoffset, frameoffset)
|
||||
rowheight = math_max(rowheight, rowoffset + (frameheight / 2))
|
||||
|
||||
frame:SetPoint("TOPLEFT", children[i-1].frame, "TOPRIGHT", 0, frameoffset - lastframeoffset)
|
||||
rowheight = math_max(rowheight,rowoffset+(frameheight/2))
|
||||
--print("type:", child.type, "offset:",frameoffset-lastframeoffset)
|
||||
frame:SetPoint("TOPLEFT",children[i-1].frame,"TOPRIGHT",0,frameoffset-lastframeoffset)
|
||||
usedwidth = framewidth + usedwidth
|
||||
end
|
||||
end
|
||||
|
||||
if child.width == "fill" then
|
||||
child:SetWidth(width)
|
||||
frame:SetPoint("RIGHT", content)
|
||||
frame:SetPoint("RIGHT",content,"RIGHT",0,0)
|
||||
|
||||
usedwidth = 0
|
||||
rowstart = frame
|
||||
rowstartoffset = frameoffset
|
||||
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(width)
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
@@ -778,28 +844,33 @@ AceGUI:RegisterLayout("Flow",
|
||||
elseif child.width == "relative" then
|
||||
child:SetWidth(width * child.relWidth)
|
||||
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(width)
|
||||
end
|
||||
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
elseif oversize then
|
||||
if width > 1 then
|
||||
frame:SetPoint("RIGHT", content)
|
||||
frame:SetPoint("RIGHT",content,"RIGHT",0,0)
|
||||
end
|
||||
end
|
||||
|
||||
if child.height == "fill" then
|
||||
frame:SetPoint("BOTTOM", content)
|
||||
frame:SetPoint("BOTTOM",content,"BOTTOM")
|
||||
isfullheight = true
|
||||
end
|
||||
end
|
||||
|
||||
--anchor the last row, if its full height needs a special case since its height has just been changed by the anchor
|
||||
if isfullheight then
|
||||
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -height)
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-height)
|
||||
elseif rowstart then
|
||||
rowstart:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -(height + (rowoffset - rowstartoffset) + 3))
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
|
||||
end
|
||||
|
||||
height = height + rowheight + 3
|
||||
safecall(content.obj.LayoutFinished, content.obj, nil, height)
|
||||
end)
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, height )
|
||||
end
|
||||
)
|
||||
+15
-17
@@ -1,28 +1,26 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceGUI-3.0.lua"/>
|
||||
<!-- Container -->
|
||||
<Script file="widgets\AceGUIContainer-BlizOptionsGroup.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-DropDownGroup.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-Frame.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-InlineGroup.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-ScrollFrame.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-SimpleGroup.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-TabGroup.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-TreeGroup.lua"/>
|
||||
<Script file="widgets\AceGUIContainer-Window.lua"/>
|
||||
<!-- Widgets -->
|
||||
<Script file="widgets\AceGUIWidget-Button.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-CheckBox.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-ColorPicker.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-DropDownGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-DropDown.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-DropDown-Items.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-EditBox.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Heading.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Icon.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Keybinding.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Frame.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Window.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Heading.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-InlineGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Keybinding.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-ScrollFrame.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-SimpleGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Slider.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-TabGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-TreeGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Label.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-MultiLineEditBox.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Slider.lua"/>
|
||||
</Ui>
|
||||
<Script file="widgets\AceGUIWidget-BlizOptionsGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Icon.lua"/>
|
||||
</Ui>
|
||||
+153
@@ -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
Reference in New Issue
Block a user