Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e53b3c1784 | |||
| 23b3a3b864 | |||
| 597f4030a7 | |||
| ebd76275a1 | |||
| 6addd9bb51 | |||
| abde71ceb0 | |||
| a984e705c3 | |||
| 1a80ca8ad8 | |||
| b4a2eb8bc2 | |||
| ae178d4a18 | |||
| e5ad71bc59 | |||
| b0ac94509a | |||
| 2e98622454 | |||
| e48bb6fc03 | |||
| a3b7b834b7 | |||
| f82bbf252c | |||
| a3fc195f14 | |||
| c1c2b4e8bb | |||
| 83f5e08689 | |||
| 76ca0cc851 | |||
| 398245d27d | |||
| 887ab7879b | |||
| e3b8d2649e | |||
| e916cdea19 | |||
| befdaa456d | |||
| 7291fff664 | |||
| 790506fc7a | |||
| 87329f2633 | |||
| 8375b95f7b | |||
| 01f476ca1b | |||
| 63c9af7846 | |||
| f02f4d73f0 | |||
| 721833bb50 | |||
| b30a3554cf | |||
| 595ed5091d | |||
| 85f80445d1 | |||
| 9fdb7ed718 | |||
| 698de84762 | |||
| cddaf2c515 | |||
| 2291ace9f7 | |||
| 26ff2b3b94 | |||
| d5e2dce359 | |||
| 22ae941c4a | |||
| 08c1b9da3f | |||
| b0da9ec10b | |||
| d8d94f10b0 | |||
| 255485daca |
@@ -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
|
||||
+9
-1
@@ -2,7 +2,15 @@ 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=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
|
||||
PRODUCTION_REALMLIST=
|
||||
PTR_REALMLIST=
|
||||
LOCAL_REALMLIST=
|
||||
|
||||
SSH_USER=root
|
||||
SSH_HOST=strictlypragmatic.ru
|
||||
|
||||
+7
-1
@@ -3,4 +3,10 @@ __pycache__/
|
||||
manifest.json
|
||||
.claude
|
||||
.vscode
|
||||
dist
|
||||
dist
|
||||
build/
|
||||
/new customization mpqs/*.mpq
|
||||
Wow*.exe
|
||||
*.backup.exe
|
||||
Logs/
|
||||
!Wow_Original.exe
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
[submodule "vendor/modules/wxl-unit-outline"]
|
||||
path = vendor/modules/wxl-unit-outline
|
||||
url = https://github.com/WarcraftXL/wxl-unit-outline.git
|
||||
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
|
||||
@@ -0,0 +1,46 @@
|
||||
# MoonWell Client Sources — Agent Guide
|
||||
|
||||
## Проект
|
||||
|
||||
WoW-клиент на базе патча **3.3.5a (build 12340, WotLK)**. Репозиторий содержит UI-аддоны и
|
||||
кастомные интерфейсы для приватного сервера MoonWell.
|
||||
|
||||
Стек: Lua 5.1, FrameXML (XML + Lua), WoW Addon API 3.3.5.
|
||||
|
||||
---
|
||||
|
||||
## Навыки (Skills)
|
||||
|
||||
Перед тем как работать с UI — **прочитай соответствующий файл из `ai-skills/`**.
|
||||
|
||||
| Задача | Файл |
|
||||
|--------|------|
|
||||
| Верстка фреймов, XML, Lua CreateFrame, pixel-perfect из Figma, текстуры, шрифты, якоря | [`ai-skills/ui/SKILL.md`](ai-skills/ui/SKILL.md) |
|
||||
| Виджеты, типы фреймов, Button/EditBox/ScrollFrame, события, SavedVariables, Blizzard API | [`ai-skills/ui/widgets-and-api.md`](ai-skills/ui/widgets-and-api.md) |
|
||||
|
||||
Читай **весь файл** скилла, а не отдельные секции — там есть ограничения API, типичные ошибки
|
||||
и готовые шаблоны.
|
||||
|
||||
---
|
||||
|
||||
## Ключевые ограничения (всегда держи в голове)
|
||||
|
||||
- `## Interface: 30300` — никаких retail-API
|
||||
- Нет `SetSize()` → только `SetWidth()` / `SetHeight()`
|
||||
- Нет `C_Timer`, `CreateFramePool`, `PixelUtil`
|
||||
- Текстуры: только степени двойки, максимум 512×512 px, формат `.tga` / `.blp`
|
||||
- XML-атрибуты: `relativeTo` (не `relativeKey`), `$parentX` (не `$parent.X`)
|
||||
- Кнопки и ScrollFrame в XML должны иметь `name`
|
||||
|
||||
---
|
||||
|
||||
## Структура аддона
|
||||
|
||||
```
|
||||
AddonName/
|
||||
├── AddonName.toc ← обязательно, имя файла = имя папки
|
||||
├── AddonName.xml ← верстка (загружается первой)
|
||||
└── AddonName.lua ← логика
|
||||
```
|
||||
|
||||
Порядок файлов в TOC важен: XML раньше Lua.
|
||||
@@ -0,0 +1,87 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(MoonWellWarcraftXL LANGUAGES CXX)
|
||||
|
||||
# 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_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()
|
||||
|
||||
# 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")
|
||||
|
||||
# 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")
|
||||
|
||||
# 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"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.def")
|
||||
set_target_properties(MoonWellLoader PROPERTIES
|
||||
OUTPUT_NAME "d3d9-native"
|
||||
PREFIX ""
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/artifacts")
|
||||
target_compile_definitions(MoonWellLoader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||
endif()
|
||||
@@ -0,0 +1,788 @@
|
||||
# План расширенной кастомизации персонажей MoonWell
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Создать для клиента WoW 3.3.5a (build 12340) расширенную систему внешности, визуально и по удобству близкую к retail, сохранив оригинальный `Wow.exe` и используя WarcraftXL для всех клиентских расширений.
|
||||
|
||||
Система должна:
|
||||
|
||||
- поддерживать стандартные параметры 3.3.5a и дополнительные параметры MoonWell;
|
||||
- показывать одинаковую внешность при создании, выборе персонажа, входе в мир и другим игрокам;
|
||||
- хранить дополнительные параметры на сервере и считать сервер источником истины;
|
||||
- поддерживать текстурные слои, geoset базовой модели и отдельные прикрепляемые M2-модели;
|
||||
- работать без Eluna/AIO в качестве постоянного транспорта состояния;
|
||||
- позволять добавлять новые варианты преимущественно данными и ассетами;
|
||||
- иметь версионирование формата и безопасный откат.
|
||||
|
||||
## 2. Не входит в первую версию
|
||||
|
||||
- полное копирование внутренних `ChrCustomization*` DB2 и протокола актуального retail-клиента;
|
||||
- поддержка всех retail-рас, которых нет в MoonWell;
|
||||
- динамическое изменение скелета или пропорций тела;
|
||||
- автоматический импорт всех retail-ассетов без ручной проверки;
|
||||
- публикация полного дампа современных файлов Blizzard;
|
||||
- изменение бинарника `Wow.exe` на диске.
|
||||
|
||||
## 3. Базовые ограничения
|
||||
|
||||
### Клиент
|
||||
|
||||
- WoW 3.3.5a, build 12340, Lua 5.1, `## Interface: 30300`.
|
||||
- `Wow.exe` должен оставаться оригинальным с контролируемым SHA-256.
|
||||
- Новое нативное поведение реализуется модулем MoonWell внутри WarcraftXL.
|
||||
- GlueXML и FrameXML не используют retail API (`C_Timer`, `CreateFramePool`, `PixelUtil` и т. п.).
|
||||
- Для анимаций интерфейса используется `OnUpdate`.
|
||||
- UI-текстуры: `.blp`/`.tga`, размеры по степеням двойки, не более 512×512 на отдельный UI-тайл.
|
||||
- В Lua/XML использовать `SetWidth`/`SetHeight`, а не `SetSize`.
|
||||
|
||||
### Ассеты и рендер
|
||||
|
||||
- Исходные современные M2/MD21 обрабатываются `wxl-modern-assets`.
|
||||
- Каждая модель проверяется на поддерживаемую версию формата, лимит костей, материалы, анимации и attachment points.
|
||||
- Не использовать OBJ/FBX/glTF как вход для игрового клиента: нужны исходные `.m2`, `.skin`, `.skel`, `.anim` и `.blp`.
|
||||
- UI-превью и модель в мире должны использовать один каталог вариантов и одинаковые идентификаторы.
|
||||
|
||||
### Сервер
|
||||
|
||||
- Сервер валидирует расу, пол, доступность и совместимость каждого выбора.
|
||||
- Клиент не может назначить себе недоступный вариант внешности.
|
||||
- Дополнительные параметры хранятся отдельно от стандартных байтов внешности 3.3.5a.
|
||||
- Изменения схемы БД поставляются миграциями и имеют обратимый путь.
|
||||
|
||||
## 4. Целевая архитектура
|
||||
|
||||
```text
|
||||
Retail CDN (выбранные FileDataID)
|
||||
│
|
||||
▼
|
||||
Asset manifest + downloader + validation
|
||||
│
|
||||
▼
|
||||
Patch*.MPQ / loose Patch-WXL assets
|
||||
│
|
||||
├── wxl-modern-assets
|
||||
├── адаптированный wxl-equip-extension
|
||||
└── MoonWell customization runtime
|
||||
│
|
||||
Lua API / GlueXML UI
|
||||
│
|
||||
▼
|
||||
MoonWell customization protocol
|
||||
│
|
||||
▼
|
||||
AzerothCore storage + validation + broadcast
|
||||
```
|
||||
|
||||
### Компоненты
|
||||
|
||||
1. **Asset pipeline** — получает только перечисленные зависимости по `FileDataID`, сохраняет манифест и проверяет комплектность.
|
||||
2. **`wxl-modern-assets`** — загружает и приводит современные M2/MD21 и текстуры к контракту клиента 3.3.5a.
|
||||
3. **Адаптированный `wxl-equip-extension`** — прикрепляет M2, поддерживает collection M2, два модельных канала, attachment points и фильтрацию geoset.
|
||||
4. **MoonWell customization runtime** — управляет независимыми косметическими каналами, текстурными слоями, geoset базовой модели и Lua API.
|
||||
5. **GlueXML UI** — категории, список вариантов, камера, случайный выбор, сброс и предварительный просмотр.
|
||||
6. **Серверный модуль** — хранение, валидация, загрузка, изменение и распространение внешности.
|
||||
|
||||
## 5. Типы параметров внешности
|
||||
|
||||
### Стандартные параметры 3.3.5a
|
||||
|
||||
- пол;
|
||||
- цвет кожи;
|
||||
- лицо;
|
||||
- причёска;
|
||||
- цвет волос;
|
||||
- стандартная особенность лица/борода.
|
||||
|
||||
Они продолжают использовать штатные поля персонажа и стандартный пакет создания.
|
||||
|
||||
### Дополнительные текстурные параметры
|
||||
|
||||
- шрамы;
|
||||
- татуировки тела;
|
||||
- татуировки лица;
|
||||
- макияж;
|
||||
- рисунок/отметины;
|
||||
- дополнительные оттенки кожи;
|
||||
- дополнительные слои лица.
|
||||
|
||||
Реализация: расширение `CharSections`, каталог слоёв и модификация/перехват клиентского композитора текстуры персонажа.
|
||||
|
||||
### Geoset базовой модели
|
||||
|
||||
- дополнительные причёски;
|
||||
- части бороды и усов;
|
||||
- форма ушей/бровей;
|
||||
- варианты клыков;
|
||||
- встроенные рога;
|
||||
- скрытие частей базовой модели.
|
||||
|
||||
Реализация: таблица выбора geoset по расе, полу, категории и варианту.
|
||||
|
||||
### Прикрепляемые M2-элементы
|
||||
|
||||
- серьги и пирсинг;
|
||||
- украшения для волос;
|
||||
- объёмные бороды;
|
||||
- рога и дополнительные клыки;
|
||||
- протезы;
|
||||
- венцы, диадемы и прочие аксессуары;
|
||||
- независимые объёмные элементы тела.
|
||||
|
||||
Реализация: косметические каналы поверх механизма `wxl-equip-extension`.
|
||||
|
||||
## 6. Косметические каналы
|
||||
|
||||
Кастомизация не должна занимать реальные слоты экипировки. Нужен отдельный реестр каналов:
|
||||
|
||||
```text
|
||||
hair_extra
|
||||
beard_extra
|
||||
horns
|
||||
ears
|
||||
tusks
|
||||
piercing_1
|
||||
piercing_2
|
||||
accessory_head
|
||||
accessory_face
|
||||
accessory_body_1
|
||||
accessory_body_2
|
||||
```
|
||||
|
||||
Для каждого канала задаются:
|
||||
|
||||
- стабильный числовой `option_id`;
|
||||
- допустимые расы и пол;
|
||||
- `display_id` или прямое описание M2;
|
||||
- Model1/Model2;
|
||||
- Texture1/Texture2;
|
||||
- attachment point для каждого модельного канала;
|
||||
- список разрешённых geoset collection M2;
|
||||
- флаги суффиксов расы/пола и структуры каталогов;
|
||||
- правила конфликта со шлемом и настоящей экипировкой;
|
||||
- приоритет и взаимоисключающие каналы;
|
||||
- значение сброса (`choice_id = 0`).
|
||||
|
||||
## 7. Адаптация `wxl-equip-extension`
|
||||
|
||||
### Обязательные работы
|
||||
|
||||
- закрепить точную ревизию репозитория как submodule;
|
||||
- адаптировать сборочную структуру к текущему WarcraftXL MoonWell;
|
||||
- проверить пересечение хуков с `wxl-modern-assets` и существующими `OnItemSlotChange`, `OnItemSlotClear`, `OnM2PerFrameUpdate`;
|
||||
- вынести разбор Model1/Model2, `Icon2`, attachment points и collection geoset в переиспользуемый слой;
|
||||
- добавить API независимых косметических каналов, не привязанных к надетому предмету;
|
||||
- устранить утечку/остаток collection M2 после снятия или смены варианта;
|
||||
- обеспечить корректное уничтожение дочерних моделей при смене персонажа, карты, формы и пола;
|
||||
- исключить двойное прикрепление модели при повторной синхронизации;
|
||||
- очистить мусорные Model1/Model2 в старых строках `ItemDisplayInfo.dbc`;
|
||||
- добавить диагностический лог разрешения пути, модели, текстуры, attachment point и geoset;
|
||||
- реализовать безопасный fallback при отсутствии ассета.
|
||||
|
||||
### Проверки совместимости
|
||||
|
||||
- персонаж игрока;
|
||||
- другие игроки;
|
||||
- NPC с тем же `display_id`;
|
||||
- экран создания;
|
||||
- экран выбора;
|
||||
- парикмахерская;
|
||||
- смена экипировки;
|
||||
- превращения и shapeshift;
|
||||
- транспорт;
|
||||
- телепорт и смена карты;
|
||||
- повторный вход и `/reload`.
|
||||
|
||||
## 8. Каталог данных кастомизации
|
||||
|
||||
Нужен единый, версионируемый источник данных, из которого генерируются клиентское и серверное представления.
|
||||
|
||||
Предлагаемая запись:
|
||||
|
||||
```text
|
||||
option_id
|
||||
choice_id
|
||||
race_mask
|
||||
gender_mask
|
||||
category
|
||||
label_key
|
||||
render_type
|
||||
char_section_id
|
||||
geoset_ids
|
||||
model_1
|
||||
model_2
|
||||
texture_1
|
||||
texture_2
|
||||
attachment_1
|
||||
attachment_2
|
||||
flags
|
||||
conflict_group
|
||||
unlock_condition
|
||||
asset_manifest_id
|
||||
enabled
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- `(option_id, choice_id)` никогда не переиспользуется с другим смыслом;
|
||||
- удалённый вариант остаётся распознаваемым для миграции старых персонажей;
|
||||
- порядок отображения UI хранится отдельно от стабильного идентификатора;
|
||||
- клиент и сервер собираются из одной версии каталога;
|
||||
- версия каталога входит в сетевое рукопожатие.
|
||||
|
||||
## 9. Серверное хранение
|
||||
|
||||
### Предлагаемая таблица
|
||||
|
||||
```sql
|
||||
character_customization (
|
||||
guid INT UNSIGNED NOT NULL,
|
||||
option_id SMALLINT UNSIGNED NOT NULL,
|
||||
choice_id INT UNSIGNED NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (guid, option_id)
|
||||
)
|
||||
```
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- индекс по `guid`;
|
||||
- внешний ключ или очистка записей при удалении персонажа;
|
||||
- миграция с начальными значениями для существующих персонажей;
|
||||
- сервис сброса неизвестных/отключённых вариантов;
|
||||
- аудит изменения внешности при необходимости.
|
||||
|
||||
### Серверная логика
|
||||
|
||||
- загрузить набор при `Character::LoadFromDB` или эквивалентном этапе;
|
||||
- валидировать выбор по расе, полу, режиму игры и доступности;
|
||||
- сохранять транзакционно;
|
||||
- включать состояние в данные экрана выбора персонажа;
|
||||
- отправлять полный snapshot при входе в мир;
|
||||
- отправлять delta при изменении;
|
||||
- рассылать актуальное состояние игрокам в зоне видимости;
|
||||
- отправлять snapshot при появлении объекта в зоне видимости нового клиента;
|
||||
- ограничить частоту изменений;
|
||||
- запретить недоступные комбинации независимо от клиентского UI.
|
||||
|
||||
## 10. Сетевой протокол MoonWell
|
||||
|
||||
### Общие требования
|
||||
|
||||
- собственный namespace/opcode или надёжный WarcraftXL transport;
|
||||
- версия протокола и версия каталога;
|
||||
- явная длина сообщения и лимит числа записей;
|
||||
- little-endian поля с фиксированной шириной;
|
||||
- сервер является источником истины;
|
||||
- неизвестные поля пропускаются или отклоняются согласно версии;
|
||||
- отсутствие расширения не ломает стандартный вход в игру.
|
||||
|
||||
### Сообщения
|
||||
|
||||
```text
|
||||
CMSG_MOONWELL_CUSTOMIZATION_CREATE
|
||||
protocol_version
|
||||
catalog_version
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
|
||||
SMSG_MOONWELL_CUSTOMIZATION_RESULT
|
||||
result_code
|
||||
normalized choices
|
||||
|
||||
SMSG_MOONWELL_CUSTOMIZATION_SNAPSHOT
|
||||
player_guid
|
||||
catalog_version
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
|
||||
CMSG_MOONWELL_CUSTOMIZATION_UPDATE
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
|
||||
SMSG_MOONWELL_CUSTOMIZATION_DELTA
|
||||
player_guid
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
```
|
||||
|
||||
### Экран выбора персонажа
|
||||
|
||||
Предпочтительный вариант — отдельный snapshot после получения списка персонажей. Не расширять штатный пакет без строгой проверки длины и совместимости.
|
||||
|
||||
Клиент должен:
|
||||
|
||||
- сопоставить snapshot с GUID персонажа;
|
||||
- применить его к модели выбранного персонажа;
|
||||
- хранить данные только до обновления от сервера;
|
||||
- показывать стандартную внешность, если snapshot отсутствует или версия несовместима.
|
||||
|
||||
## 11. Клиентский runtime API
|
||||
|
||||
Планируемый API WarcraftXL:
|
||||
|
||||
```text
|
||||
MoonWellCustomizationGetCatalogVersion()
|
||||
MoonWellCustomizationGetOptions(race, gender)
|
||||
MoonWellCustomizationGetChoices(optionId)
|
||||
MoonWellCustomizationGetCurrent(optionId)
|
||||
MoonWellCustomizationPreview(optionId, choiceId)
|
||||
MoonWellCustomizationResetPreview()
|
||||
MoonWellCustomizationCommitPreview()
|
||||
MoonWellCustomizationRandomize()
|
||||
MoonWellCustomizationApplySnapshot(guid, data)
|
||||
MoonWellCustomizationClear(guid)
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- Lua получает только безопасные операции высокого уровня;
|
||||
- нативный слой владеет указателями моделей и их жизненным циклом;
|
||||
- повторное применение snapshot идемпотентно;
|
||||
- preview не меняет серверное состояние;
|
||||
- при ошибке модель возвращается к последнему подтверждённому snapshot;
|
||||
- смена расы или пола пересчитывает допустимость всех параметров;
|
||||
- отсутствие M2/текстуры отключает только конкретный вариант, а не весь экран.
|
||||
|
||||
## 12. Применение внешности
|
||||
|
||||
Порядок применения должен быть фиксированным:
|
||||
|
||||
1. базовая раса и пол;
|
||||
2. стандартные параметры 3.3.5a;
|
||||
3. `CharSections` и текстурные слои;
|
||||
4. geoset базовой модели;
|
||||
5. скрытие/конфликты с экипировкой;
|
||||
6. независимые косметические M2-каналы;
|
||||
7. обновление материалов и композитной текстуры;
|
||||
8. окончательное обновление позы и камеры превью.
|
||||
|
||||
При смене одного параметра обновляется только затронутый слой, если это безопасно. Полная пересборка модели используется как fallback.
|
||||
|
||||
## 13. Интерфейс создания персонажа
|
||||
|
||||
### Структура
|
||||
|
||||
- категории слева или справа от модели;
|
||||
- прокручиваемая сетка вариантов;
|
||||
- текстовое название активной категории;
|
||||
- состояние выбранного варианта;
|
||||
- кнопки случайного выбора и сброса;
|
||||
- возврат к выбору расы/класса без потери валидных параметров;
|
||||
- подтверждение создания только после серверной валидации;
|
||||
- плавное приближение камеры к зоне редактирования.
|
||||
|
||||
### Категории первой версии
|
||||
|
||||
- кожа;
|
||||
- лицо;
|
||||
- причёска;
|
||||
- цвет волос;
|
||||
- борода/особенность лица;
|
||||
- шрамы;
|
||||
- татуировки;
|
||||
- серьги/пирсинг;
|
||||
- расовый аксессуар.
|
||||
|
||||
Категории скрываются, если для текущей расы/пола нет вариантов.
|
||||
|
||||
### Камера
|
||||
|
||||
- профиль камеры задаётся по расе, полу и категории;
|
||||
- отдельно настраиваются distance, вертикальная цель, горизонтальный сдвиг и FOV;
|
||||
- переход использует easing через `OnUpdate`;
|
||||
- короткие и средние расы имеют отдельные профили;
|
||||
- при выходе из кастомизации камера плавно возвращается к полному росту;
|
||||
- вращение персонажа доступно во всех режимах;
|
||||
- UI не перекрывает лицо и активный аксессуар на 4:3, 16:9 и 16:10.
|
||||
|
||||
### Ограничения UI 3.3.5a
|
||||
|
||||
- XML загружается раньше Lua;
|
||||
- именованные Button и ScrollFrame;
|
||||
- `relativeTo`, а не `relativeKey`;
|
||||
- `$parentX`, а не `$parent.X`;
|
||||
- `SetWidth`/`SetHeight` вместо `SetSize`;
|
||||
- таймеры и анимации через `OnUpdate`;
|
||||
- UI-атласы режутся на тайлы не более 512×512.
|
||||
|
||||
## 14. Парикмахерская и изменение существующего персонажа
|
||||
|
||||
- использовать тот же каталог и preview API;
|
||||
- получать текущий серверный snapshot при открытии;
|
||||
- считать стоимость на сервере;
|
||||
- подтверждать изменение одним атомарным запросом;
|
||||
- откатывать preview при закрытии или отказе сервера;
|
||||
- блокировать недоступные варианты;
|
||||
- поддерживать бесплатные/платные категории отдельными правилами;
|
||||
- обновлять внешность окружающим только после подтверждения.
|
||||
|
||||
## 15. Получение retail-ассетов без установки клиента
|
||||
|
||||
### Инструменты
|
||||
|
||||
- `wow.export` в online-режиме — поиск и предварительный просмотр;
|
||||
- Wago Tools/wow-listfile — поиск пути и `FileDataID`;
|
||||
- TACTSharp/TACTTool — точечная загрузка исходных файлов с CDN Blizzard.
|
||||
|
||||
### Манифест ассета
|
||||
|
||||
```text
|
||||
asset_id
|
||||
source_product
|
||||
source_build
|
||||
file_data_id
|
||||
original_path
|
||||
target_path
|
||||
sha256
|
||||
kind
|
||||
parent_asset_id
|
||||
license_note
|
||||
```
|
||||
|
||||
### Пайплайн
|
||||
|
||||
1. Выбрать модель и исходный retail build.
|
||||
2. Зафиксировать корневой `FileDataID`.
|
||||
3. Выгрузить M2 и определить зависимости.
|
||||
4. Загрузить `.skin`, `.skel`, `.anim` и текстуры.
|
||||
5. Проверить хэши и комплектность.
|
||||
6. Проверить модель в изолированном preview.
|
||||
7. Проверить downport через `wxl-modern-assets`.
|
||||
8. Добавить запись в каталог кастомизации.
|
||||
9. Упаковать только выбранные файлы.
|
||||
10. Не помещать массовый дамп retail-клиента в Git.
|
||||
|
||||
### Автоматические проверки ассета
|
||||
|
||||
- поддерживаемая версия M2/MD21;
|
||||
- наличие всех skin-профилей;
|
||||
- разрешимые FileDataID и пути текстур;
|
||||
- наличие внешнего скелета/анимаций;
|
||||
- attachment points для целевой расы;
|
||||
- лимит костей на draw после разбиения;
|
||||
- валидные материалы и blend modes;
|
||||
- отсутствие коллизий целевых путей;
|
||||
- размер итогового MPQ и память после загрузки;
|
||||
- корректное освобождение ресурсов.
|
||||
|
||||
## 16. Этапы реализации
|
||||
|
||||
### Этап 0. Исследование и фиксация контрактов
|
||||
|
||||
- [ ] Зафиксировать ревизии WarcraftXL и всех модулей.
|
||||
- [ ] Зафиксировать исходный retail build для ассетов.
|
||||
- [ ] Составить карту существующих хуков персонажа и DBC.
|
||||
- [ ] Проверить структуру штатных пакетов создания/выбора персонажа.
|
||||
- [ ] Зафиксировать каталог option/choice ID.
|
||||
- [ ] Утвердить правила доступности и монетизации.
|
||||
- [ ] Выбрать первую расу для вертикального среза: человек.
|
||||
|
||||
**Результат:** техническая спецификация без конфликтующих точек расширения.
|
||||
|
||||
### Этап 1. Интеграция equip-extension
|
||||
|
||||
- [ ] Добавить репозиторий как закреплённый submodule.
|
||||
- [ ] Адаптировать CMake/module layout.
|
||||
- [ ] Собрать Win32 runtime и x64 host.
|
||||
- [ ] Устранить конфликт хуков с текущим WarcraftXL.
|
||||
- [ ] Исправить снятие collection M2.
|
||||
- [ ] Очистить мусорные поля `ItemDisplayInfo.dbc`.
|
||||
- [ ] Добавить smoke-тест Model1/Model2 на каждом поддержанном слоте.
|
||||
- [ ] Добавить диагностическое логирование.
|
||||
|
||||
**Результат:** современная объёмная экипировка работает и корректно снимается.
|
||||
|
||||
### Этап 2. Независимые косметические каналы
|
||||
|
||||
- [ ] Отделить attachment runtime от реальных слотов экипировки.
|
||||
- [ ] Реализовать реестр косметических каналов.
|
||||
- [ ] Реализовать add/replace/remove/clear.
|
||||
- [ ] Реализовать conflict groups и приоритеты.
|
||||
- [ ] Реализовать скрытие под шлемом.
|
||||
- [ ] Добавить API preview.
|
||||
- [ ] Проверить смену карты, формы и пола.
|
||||
|
||||
**Результат:** аксессуар можно показать без создания фиктивного предмета.
|
||||
|
||||
### Этап 3. Asset pipeline
|
||||
|
||||
- [ ] Добавить входной manifest с FileDataID.
|
||||
- [ ] Реализовать пакетную загрузку через CDN.
|
||||
- [ ] Добавить локальный кэш по хэшу.
|
||||
- [ ] Автоматически проверять зависимости.
|
||||
- [ ] Формировать отчёт о пропущенных файлах.
|
||||
- [ ] Интегрировать выбранные ассеты в MPQ build.
|
||||
- [ ] Добавить контроль размера и дубликатов.
|
||||
|
||||
**Результат:** воспроизводимая сборка выбранных retail-ассетов без полного клиента.
|
||||
|
||||
### Этап 4. Серверное хранение и протокол
|
||||
|
||||
- [ ] Добавить миграцию `character_customization`.
|
||||
- [ ] Реализовать серверный каталог допустимых вариантов.
|
||||
- [ ] Реализовать create/update validation.
|
||||
- [ ] Реализовать snapshot и delta.
|
||||
- [ ] Подключить данные к character enum/select flow.
|
||||
- [ ] Рассылать состояние в visibility range.
|
||||
- [ ] Добавить rate limit и журнал ошибок.
|
||||
- [ ] Добавить fallback для клиента без совместимой версии.
|
||||
|
||||
**Результат:** параметры сохраняются и одинаково видны всем клиентам.
|
||||
|
||||
### Этап 5. Текстурные слои и geoset
|
||||
|
||||
- [ ] Расширить/переопределить `CharSections` данными MoonWell.
|
||||
- [ ] Реализовать слои шрамов, татуировок и макияжа.
|
||||
- [ ] Реализовать таблицу geoset базовой модели.
|
||||
- [ ] Синхронизировать цветовые зависимости.
|
||||
- [ ] Обеспечить совместимость с надетой бронёй.
|
||||
- [ ] Добавить полную пересборку как fallback.
|
||||
|
||||
**Результат:** работают не только прикрепляемые M2, но и полноценные параметры тела/лица.
|
||||
|
||||
### Этап 6. Новый UI создания персонажа
|
||||
|
||||
- [ ] Создать адаптивную панель категорий.
|
||||
- [ ] Создать прокручиваемую сетку choices.
|
||||
- [ ] Подключить preview API.
|
||||
- [ ] Добавить race/gender/category camera profiles.
|
||||
- [ ] Добавить randomize и reset.
|
||||
- [ ] Добавить обработку недоступного ассета.
|
||||
- [ ] Добавить серверное подтверждение создания.
|
||||
- [ ] Проверить 4:3, 16:9, 16:10 и UI scale.
|
||||
|
||||
**Результат:** законченный retail-подобный пользовательский сценарий.
|
||||
|
||||
### Этап 7. Вертикальный срез — человек
|
||||
|
||||
- [ ] Мужская и женская модели.
|
||||
- [ ] Стандартные категории.
|
||||
- [ ] Минимум одна новая текстурная категория.
|
||||
- [ ] Минимум два M2-косметических канала.
|
||||
- [ ] Сохранение, выбор персонажа и отображение в мире.
|
||||
- [ ] Видимость другим игрокам.
|
||||
- [ ] Парикмахерская/изменение существующего персонажа.
|
||||
- [ ] Полный regression-тест экипировки.
|
||||
|
||||
**Результат:** одна раса полностью проходит production-сценарий.
|
||||
|
||||
### Этап 8. Масштабирование на все расы WotLK
|
||||
|
||||
- [ ] Дворфы.
|
||||
- [ ] Ночные эльфы.
|
||||
- [ ] Гномы.
|
||||
- [ ] Дренеи.
|
||||
- [ ] Орки.
|
||||
- [ ] Нежить.
|
||||
- [ ] Таурены.
|
||||
- [ ] Тролли.
|
||||
- [ ] Эльфы крови.
|
||||
- [ ] Отдельная настройка камер для каждого пола.
|
||||
- [ ] Матрица конфликтов расовых элементов с экипировкой.
|
||||
|
||||
**Результат:** единый уровень качества для всех доступных рас.
|
||||
|
||||
### Этап 9. Стабилизация и выпуск
|
||||
|
||||
- [ ] Нагрузочный тест большого скопления игроков.
|
||||
- [ ] Проверка утечек модели/текстур при многократной смене вариантов.
|
||||
- [ ] Проверка холодного кэша WarcraftXLHost.
|
||||
- [ ] Проверка отсутствующих/повреждённых ассетов.
|
||||
- [ ] Версионирование manifest, каталога и протокола.
|
||||
- [ ] Миграция существующих персонажей.
|
||||
- [ ] Rollback-пакет.
|
||||
- [ ] Документация для добавления новой категории/расы.
|
||||
- [ ] Обновление deploy-скрипта и launcher manifest.
|
||||
|
||||
**Результат:** воспроизводимый production-релиз.
|
||||
|
||||
## 17. Матрица тестирования
|
||||
|
||||
### Персонажи
|
||||
|
||||
- каждая раса × каждый пол;
|
||||
- стандартный режим × предатель;
|
||||
- новый персонаж × существующий персонаж;
|
||||
- минимальное × максимальное число выбранных параметров;
|
||||
- случайная комбинация × преднамеренно конфликтная комбинация.
|
||||
|
||||
### Состояния клиента
|
||||
|
||||
- создание;
|
||||
- выбор;
|
||||
- загрузочный экран;
|
||||
- вход в мир;
|
||||
- появление другого игрока;
|
||||
- телепорт;
|
||||
- смерть/дух;
|
||||
- транспорт;
|
||||
- shapeshift/превращение;
|
||||
- парикмахерская;
|
||||
- смена экипировки;
|
||||
- скрытие шлема/плаща;
|
||||
- `/reload`;
|
||||
- переподключение;
|
||||
- выход и повторный вход.
|
||||
|
||||
### Графика
|
||||
|
||||
- native renderer и основной D3D9On12 путь;
|
||||
- разные уровни component texture quality;
|
||||
- включённые/выключенные SSAA, SSAO и AA;
|
||||
- дальние LOD;
|
||||
- тени;
|
||||
- прозрачные и emissive материалы;
|
||||
- несколько одинаковых моделей одновременно;
|
||||
- 4:3, 16:9, 16:10 и разные UI scale.
|
||||
|
||||
### Сеть и сервер
|
||||
|
||||
- потеря/повтор snapshot;
|
||||
- snapshot до появления модели и после неё;
|
||||
- неизвестная версия каталога;
|
||||
- неизвестный option/choice;
|
||||
- запрещённый выбор;
|
||||
- слишком большое сообщение;
|
||||
- быстрое повторное изменение;
|
||||
- одновременное изменение и телепорт;
|
||||
- сохранение при logout/crash;
|
||||
- удаление и восстановление персонажа.
|
||||
|
||||
## 18. Производительность
|
||||
|
||||
Целевые ограничения:
|
||||
|
||||
- не выполнять разбор каталога каждый кадр;
|
||||
- не пересобирать всю модель при изменении независимого аксессуара;
|
||||
- кэшировать разрешённые пути и обработанные modern assets;
|
||||
- не создавать дубликаты одной и той же дочерней M2-модели;
|
||||
- освобождать attachment при удалении/скрытии объекта;
|
||||
- ограничить число одновременных косметических M2 на персонажа;
|
||||
- иметь настройку отключения дальних косметических attachment;
|
||||
- измерять время первой загрузки, потребление RAM/VRAM и frame time;
|
||||
- логирование hot path отключено в release-сборке.
|
||||
|
||||
Предварительный бюджет первой версии:
|
||||
|
||||
- до 6 независимых M2-каналов на персонажа;
|
||||
- не более двух моделей на канал;
|
||||
- отсутствие постоянной работы Lua `OnUpdate`, кроме активной UI-анимации;
|
||||
- пересборка полной внешности только при snapshot, смене расы/пола или восстановлении после ошибки.
|
||||
|
||||
## 19. Безопасность и целостность
|
||||
|
||||
- сервер проверяет все ID и комбинации;
|
||||
- клиент не передаёт пути файлов серверу;
|
||||
- протокол содержит только числовые option/choice ID;
|
||||
- сообщения имеют максимальный размер и максимальное число записей;
|
||||
- сервер нормализует дубликаты option ID;
|
||||
- неизвестные значения не записываются в БД;
|
||||
- стоимость изменения рассчитывается сервером;
|
||||
- unlock condition проверяется сервером;
|
||||
- клиентский каталог не считается доказательством доступности;
|
||||
- журналировать несовпадение версии и попытки недопустимых изменений.
|
||||
|
||||
## 20. Сборка, доставка и откат
|
||||
|
||||
### Сборка
|
||||
|
||||
- закреплённые submodule revisions;
|
||||
- asset manifest с SHA-256;
|
||||
- генерация клиентского и серверного каталога из одного источника;
|
||||
- сборка WarcraftXL Win32 и Host x64;
|
||||
- сборка MPQ;
|
||||
- автоматическая проверка оригинального `Wow.exe`;
|
||||
- формирование launcher manifest.
|
||||
|
||||
### Версионирование
|
||||
|
||||
- `customization_protocol_version`;
|
||||
- `customization_catalog_version`;
|
||||
- `customization_asset_version`;
|
||||
- совместимая версия WarcraftXL runtime.
|
||||
|
||||
### Откат
|
||||
|
||||
- сервер умеет не отправлять расширенные сообщения;
|
||||
- клиент при отсутствии данных показывает стандартную внешность;
|
||||
- отключение модуля не блокирует вход персонажа;
|
||||
- миграция БД не удаляет стандартные параметры;
|
||||
- предыдущие MPQ/runtime хранятся как отдельный release artifact;
|
||||
- неизвестные сохранённые choices не удаляются автоматически без миграции.
|
||||
|
||||
## 21. Оценка трудоёмкости
|
||||
|
||||
Для одного разработчика при наличии подготовленных ассетов:
|
||||
|
||||
| Работа | Оценка |
|
||||
|---|---:|
|
||||
| Адаптация и исправление equip-extension | 4–7 дней |
|
||||
| Независимые косметические каналы | 4–7 дней |
|
||||
| Asset pipeline | 3–5 дней |
|
||||
| Серверное хранение и протокол | 4–7 дней |
|
||||
| Текстурные слои и geoset | 5–10 дней |
|
||||
| Новый UI создания/изменения | 5–8 дней |
|
||||
| Полный вертикальный срез одной расы | 5–10 дней |
|
||||
| Каждая следующая раса | 2–5 дней |
|
||||
| Стабилизация и regression | 7–12 дней |
|
||||
|
||||
Ориентиры:
|
||||
|
||||
- технический прототип одной расы: 1–2 недели;
|
||||
- production-вертикаль людей: 3–5 недель;
|
||||
- все десять рас WotLK: 2–3 месяца;
|
||||
- насыщенность категориями, близкая к retail: 3–6 месяцев.
|
||||
|
||||
Главный переменный фактор — подготовка и проверка ассетов, а не серверная схема.
|
||||
|
||||
## 22. Рекомендуемый первый milestone
|
||||
|
||||
Вертикальный срез для людей обоих полов:
|
||||
|
||||
- штатные пять категорий;
|
||||
- одна текстурная категория: шрамы;
|
||||
- два независимых M2-канала: серьги и аксессуар для волос;
|
||||
- серверное сохранение;
|
||||
- snapshot на экране выбора;
|
||||
- видимость другим игрокам;
|
||||
- изменение через парикмахерскую;
|
||||
- камера по категориям;
|
||||
- корректная работа со шлемом;
|
||||
- один полный набор automated/manual acceptance checks.
|
||||
|
||||
Этот milestone должен подтвердить всю архитектуру до масштабирования на остальные расы.
|
||||
|
||||
## 23. Критерии готовности production-версии
|
||||
|
||||
- [ ] `Wow.exe` идентичен оригиналу по SHA-256.
|
||||
- [ ] Все расширения поставляются через WarcraftXL и MPQ.
|
||||
- [ ] Клиент и сервер используют одинаковую версию каталога.
|
||||
- [ ] Внешность сохраняется после выхода и перезапуска клиента.
|
||||
- [ ] Экран создания, экран выбора и модель в мире совпадают.
|
||||
- [ ] Другие игроки видят подтверждённую сервером внешность.
|
||||
- [ ] Недоступные варианты невозможно применить изменённым клиентом.
|
||||
- [ ] Смена/снятие варианта не оставляет collection M2.
|
||||
- [ ] Нет placeholder-моделей из мусорных строк `ItemDisplayInfo.dbc`.
|
||||
- [ ] Нет заметных утечек памяти при многократном preview.
|
||||
- [ ] Отсутствующий ассет не приводит к падению клиента.
|
||||
- [ ] Все расы и оба пола проходят тестовую матрицу.
|
||||
- [ ] Камера корректно кадрирует низких, средних и высоких персонажей.
|
||||
- [ ] UI работает на 4:3, 16:9, 16:10 и разных UI scale.
|
||||
- [ ] Реализован и проверен rollback.
|
||||
- [ ] Описан процесс добавления новой категории, choice и ассета.
|
||||
|
||||
## 24. Открытые решения перед началом реализации
|
||||
|
||||
- точная ревизия `wxl-equip-extension` для закрепления;
|
||||
- список категорий первого релиза;
|
||||
- исходный retail build для моделей;
|
||||
- набор моделей для вертикального среза людей;
|
||||
- формат единого каталога: JSON, CSV или генератор из SQL/YAML;
|
||||
- транспорт custom packets между WarcraftXL и AzerothCore;
|
||||
- способ передачи snapshots на экране выбора;
|
||||
- политика скрытия аксессуаров под шлемом;
|
||||
- правила доступности и разблокировки вариантов;
|
||||
- использование парикмахерской или отдельного UI изменения внешности;
|
||||
- лимит косметических M2 на одного персонажа;
|
||||
- политика хранения выбранных Blizzard-ассетов и доступа к ним;
|
||||
- минимальная поддерживаемая версия launcher/runtime.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Автоматический импорт mount и pet
|
||||
|
||||
Список современных моделей находится в `custom-creatures.json`. В нём есть две независимые
|
||||
секции:
|
||||
|
||||
```json
|
||||
{
|
||||
"mounts": [
|
||||
"assets/creatures/my_mount"
|
||||
],
|
||||
"pets": [
|
||||
"assets/creatures/my_pet"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Папка должна быть результатом **Raw M2 export** из wow.export и содержать:
|
||||
|
||||
- один основной `.m2`;
|
||||
- `*.manifest.json`;
|
||||
- все упомянутые в manifest текстуры, skin, skeleton, bone и animation-файлы.
|
||||
|
||||
Разрешены абсолютные пути, например `C:/Users/sindo/wow.export/creature/catslime`, но для
|
||||
воспроизводимой сборки папку лучше скопировать внутрь репозитория и указать относительный путь.
|
||||
|
||||
## Что делает сборщик
|
||||
|
||||
При обычном запуске `tool.exe`, `run.ps1` или `deploy.ps1` сборщик:
|
||||
|
||||
1. читает manifest каждой модели и размещает retail M2 со всеми зависимостями в loose-каталоге
|
||||
`Data/Patch-WXL.MPQ`, создавая рядом `.mdx`-алиас для загрузчика WoW 3.3.5, чтобы WarcraftXL всегда
|
||||
преобразовывал MD21 до загрузки клиентом;
|
||||
2. генерирует `WXLFileData.csv` для WarcraftXL;
|
||||
3. дописывает модели в `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc`;
|
||||
4. сохраняет стабильные ID в `custom-creatures.lock.json`;
|
||||
5. для `pets` создаёт companion summon spell и строку skill line `778`;
|
||||
6. кладёт серверные DBC, SQL и таблицу ID в `build/server-dbc`.
|
||||
|
||||
Полный `Spell.dbc` в патч не копируется. WarcraftXL читает компактный
|
||||
`WXLSpellOverrides.tsv` и при открытии DBC добавляет новые spell-записи к штатному файлу клиента.
|
||||
|
||||
Перестановка моделей в списке не меняет уже выданные ID. Lock-файл необходимо хранить в Git.
|
||||
|
||||
## Дополнительные параметры
|
||||
|
||||
Если значений по умолчанию недостаточно, вместо строки используется объект:
|
||||
|
||||
```json
|
||||
{
|
||||
"folder": "assets/creatures/my_mount",
|
||||
"key": "my_mount",
|
||||
"name": "My Mount",
|
||||
"name_ru": "Мой транспорт",
|
||||
"archive": "Creature/MyMount",
|
||||
"model_file": "my_mount.m2",
|
||||
"template_display_id": 2404,
|
||||
"display_scale": 1.0,
|
||||
"model_scale": 1.0,
|
||||
"collision_width": 0.6111,
|
||||
"collision_height": 2.031,
|
||||
"mount_height": 1.8657,
|
||||
"flags": 0
|
||||
}
|
||||
```
|
||||
|
||||
Для `mounts` по умолчанию клонируется верховая лошадь с display ID `2404`, для `pets` —
|
||||
компаньон-кот с display ID `5448`. Шаблон задаёт физические размеры, звук и прочие поля DBC;
|
||||
параметры объекта позволяют их переопределить.
|
||||
|
||||
## Установка на сервер
|
||||
|
||||
Для каждого элемента `pets` сборщик автоматически выдаёт `creatureEntry`, `summonSpellID` и
|
||||
`skillLineAbilityID`. Значения находятся в `build/server-dbc/custom-creatures.csv`.
|
||||
|
||||
1. Скопировать `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` из `build/server-dbc` в
|
||||
серверный каталог `data/dbc`.
|
||||
2. Применить `build/server-dbc/custom-creatures.sql` к `acore_world`.
|
||||
3. Перезапустить worldserver: DBC и таблицы `spell_dbc` загружаются только при старте.
|
||||
4. Для проверки изучить выданный spell командой `.learn <summonSpellID>`.
|
||||
|
||||
SQL создаёт `creature_template`, `creature_model_info`, `creature_template_model`, companion spell и строку skill line.
|
||||
Генерация полноценного mount spell для секции `mounts` остаётся отдельным этапом: mount требует
|
||||
другой spell-шаблон с аурами скорости и `SPELL_AURA_MOUNTED`.
|
||||
@@ -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 запуске.
|
||||
@@ -1,90 +1,148 @@
|
||||
# MoonWell Client Sources
|
||||
|
||||
Репозиторий с ресурсами и исполняемым файлом модифицированного клиента World of Warcraft 3.3.5a.
|
||||
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
|
||||
|
||||
Основная цель проекта: хранить и сопровождать клиентские изменения поверх стандартных MPQ-пакетов WoW, включая интерфейс, графику, звук и дополнительные данные для кастомных систем сервера.
|
||||
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL/wxl-core) **1.1.220**: `Wow.exe` больше не содержит
|
||||
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
|
||||
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
|
||||
только к памяти запущенного процесса.
|
||||
|
||||
## Что находится в репозитории
|
||||
## Что перенесено из модифицированного Wow.exe
|
||||
|
||||
- `Wow.exe` - модифицированный клиентский исполняемый файл.
|
||||
- Поддерживает использование до 4 ГБ ОЗУ.
|
||||
- Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели.
|
||||
- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`.
|
||||
- Содержит графические элементы внутриигрового магазина.
|
||||
- Основная зона изменений: `Interface/Store_UI`.
|
||||
- `patch-ruRU-4/` - наши изменения поверх стандартных `patch-ruRU-1.MPQ`, `patch-ruRU-2.MPQ` и `patch-ruRU-3.MPQ`.
|
||||
- Содержит ресурсы локали `ruRU`, связанные с экраном входа, `GlueXML`, заставками и звуком.
|
||||
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/LoginScreen`, `Sound/Ambience`, `Sound/Music`.
|
||||
- `patch-ruRU-5/` - пакет русской локализации и дополнительных интерфейсных ресурсов.
|
||||
- Содержит перевод на русский язык для glue-интерфейса и связанных экранов клиента.
|
||||
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/GluesVideo`, `Interface/Loginscreen`, `Interface/tooltips`, `Interface/cinematics`.
|
||||
- `patch-Z/` - кастомный пакет для модуля Mythic+.
|
||||
- Содержит графические ресурсы Mythic+.
|
||||
- Также включает связанные клиентские данные в `DBFielsClient/` и иконки в `Interface/Icons/`.
|
||||
- разблокировка пользовательского `GlueXML`;
|
||||
- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`;
|
||||
- одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`;
|
||||
- загрузка WarcraftXL без import-table patch и без отдельного injector.
|
||||
|
||||
## Структура по назначению
|
||||
При установке сборщик включает Large Address Aware в копии `Wow.exe`; исходный
|
||||
`Wow_Original.exe` остаётся неизменным и используется как проверенный шаблон.
|
||||
|
||||
### Внутриигровой магазин
|
||||
## Структура
|
||||
|
||||
Файлы магазина находятся в [`patch-4/Interface/Store_UI`](./patch-4/Interface/Store_UI).
|
||||
```text
|
||||
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 и extensions
|
||||
run.ps1 полная сборка, установка и запуск клиента
|
||||
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
|
||||
```
|
||||
|
||||
Примеры ресурсов:
|
||||
Подключены совместимые с 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:
|
||||
|
||||
- `Currencies/Gold.blp`
|
||||
- `Currencies/Token.blp`
|
||||
- `Frames/StoreFrame_Main.blp`
|
||||
```powershell
|
||||
git submodule update --remote vendor/warcraftxl vendor/modules/*
|
||||
```
|
||||
|
||||
### Экран входа и локализованные ресурсы
|
||||
После обновления обязательно пересоберите и проверьте вход/создание персонажа: API WarcraftXL
|
||||
пока развивается.
|
||||
|
||||
Файлы экрана входа и связанные ресурсы находятся в [`patch-ruRU-4`](./patch-ruRU-4).
|
||||
## Требования
|
||||
|
||||
Основные каталоги:
|
||||
- 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`.
|
||||
|
||||
- `Interface/GlueXML` - Lua/XML-исходники интерфейса загрузки и логина
|
||||
- `Interface/Glues` - графика и модели для glue-экрана
|
||||
- `Interface/LoginScreen` - фон и связанные изображения
|
||||
- `Sound/Ambience/GlueScreen` - фоновые звуки
|
||||
- `Sound/Music/GlueScreenMusic` - музыка экрана входа
|
||||
После клонирования инициализируйте submodule:
|
||||
|
||||
### Русский перевод для `patch-ruRU-5`
|
||||
```powershell
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5).
|
||||
## Сборка WarcraftXL
|
||||
|
||||
Основные каталоги:
|
||||
Быстрое полное развёртывание клиента одной командой:
|
||||
|
||||
- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа
|
||||
- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect`
|
||||
- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов
|
||||
- `Interface/Loginscreen` - фон и изображения экрана входа
|
||||
- `Interface/tooltips` - локализованные рамки и оформление тултипов
|
||||
- `Interface/cinematics` - элементы интерфейса для роликов и заставок
|
||||
```powershell
|
||||
.\deploy.ps1
|
||||
```
|
||||
|
||||
Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов.
|
||||
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, native D3D9 proxy и extensions,
|
||||
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
|
||||
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
|
||||
|
||||
### Mythic+
|
||||
Полезные варианты:
|
||||
|
||||
Файлы Mythic+ находятся в [`patch-Z`](./patch-Z).
|
||||
```powershell
|
||||
# Явный путь и запуск клиента после установки
|
||||
.\deploy.ps1 -ClientPath 'D:\Games\World of Warcraft' -StopClient -Launch
|
||||
|
||||
Основные каталоги:
|
||||
# Быстро обновить только WarcraftXL без пересборки MPQ
|
||||
.\deploy.ps1 -SkipDataBuild
|
||||
|
||||
- `Interface/MythicPlus/textures` - текстуры интерфейса Mythic+
|
||||
- `Interface/MythicPlus/sounds` - звуки таймеров и событий
|
||||
- `Interface/Icons` - дополнительные иконки
|
||||
- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом
|
||||
```
|
||||
|
||||
## Как воспринимать содержимое репозитория
|
||||
Без установки в клиент:
|
||||
|
||||
- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде.
|
||||
- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения.
|
||||
- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла.
|
||||
```powershell
|
||||
.\build-warcraftxl.ps1 -Configuration Release
|
||||
```
|
||||
|
||||
## Для чего используется
|
||||
С установкой:
|
||||
|
||||
Проект нужен для сопровождения клиентской части MoonWell:
|
||||
```powershell
|
||||
.\build-warcraftxl.ps1 -Configuration Release `
|
||||
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
|
||||
```
|
||||
|
||||
- кастомизации GlueXML;
|
||||
- поддержки русского перевода для `patch-ruRU-5`;
|
||||
- поддержки внутриигрового магазина;
|
||||
- добавления графики и данных для Mythic+;
|
||||
- хранения пакетов графических улучшений;
|
||||
- работы с модифицированным `Wow.exe` для WoW 3.3.5a.
|
||||
При `-Deploy` скрипт:
|
||||
|
||||
1. проверяет хеш `Wow_Original.exe`;
|
||||
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
|
||||
3. восстанавливает оригинальный `Wow.exe`;
|
||||
4. устанавливает `WarcraftXL.dll`, native D3D9 proxy и DLL в `Extensions\`;
|
||||
5. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
|
||||
|
||||
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
|
||||
|
||||
Полная сборка MPQ, установка WarcraftXL и запуск:
|
||||
|
||||
```powershell
|
||||
$env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
|
||||
.\run.ps1
|
||||
```
|
||||
|
||||
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
|
||||
|
||||
## Авторизация через лаунчер
|
||||
|
||||
Release-клиент принимает только одноразовую launcher-сессию и при прямом запуске показывает
|
||||
кнопку открытия лаунчера вместо полей логина и пароля. Контракт переменных запуска, требования
|
||||
к auth-серверу и Debug-режим описаны в [LAUNCHER_AUTH.md](LAUNCHER_AUTH.md).
|
||||
|
||||
## MPQ-пакеты
|
||||
|
||||
Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки
|
||||
WarcraftXL не требуется и в объектное хранилище не загружается.
|
||||
|
||||
Основные пакеты:
|
||||
|
||||
- `patch-4` — ресурсы внутриигрового магазина;
|
||||
- `patch-ruRU-4` — экран входа и GlueXML;
|
||||
- `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 extensions распространяются с доступным соответствующим исходным кодом. Репозиторий не
|
||||
должен публиковать Blizzard assets или `Wow.exe`.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,535 @@
|
||||
---
|
||||
name: wow-335a-ui
|
||||
description: >
|
||||
Верстка и разработка UI-аддонов для World of Warcraft 3.3.5a (WotLK, build 12340).
|
||||
Используй этот скилл при любой работе с интерфейсом WoW 3.3.5: создание фреймов
|
||||
через XML или Lua CreateFrame(), позиционирование через SetPoint/якоря, слои
|
||||
(BACKGROUND/ARTWORK/OVERLAY/HIGHLIGHT), страты (frameStrata), текстуры, шрифты,
|
||||
кнопки, ScrollFrame, скрипты-события, структура аддона (TOC + XML + Lua),
|
||||
SavedVariables, работа с Blizzard API патча 3.3.5. Также применяй когда речь идёт
|
||||
об AzerothCore, кастомных интерфейсах для WoW-прайват серверов 3.3.5a, Eluna
|
||||
или модификации FrameXML. ОБЯЗАТЕЛЬНО применяй при задачах pixel-perfect переноса
|
||||
дизайна (Figma/PSD/скриншот) в WoW UI: пересчёт координат с учётом UIParent scale,
|
||||
конвертация цветов HEX→RGBA, подбор шрифтов, подготовка текстур (степени двойки,
|
||||
TexCoord-кроп), устранение расхождений между макетом и игровым интерфейсом.
|
||||
---
|
||||
|
||||
# WoW 3.3.5a — Верстка UI (FrameXML / AddOn Development)
|
||||
|
||||
## Ключевые ограничения патча 3.3.5a
|
||||
|
||||
- **API версии**: `## Interface: 30300` (build 12340)
|
||||
- **Lua версия**: Lua 5.1
|
||||
- **Текстуры**: только степени двойки по ширине/высоте (8, 16, 32, 64, 128, 256, 512 px); максимум 512×512
|
||||
- **Нет** `:SetSize()` — только `:SetWidth()` / `:SetHeight()` (или через XML `<Size>`)
|
||||
- **Нет** `hooksecurefunc` для защищённых фреймов без taint
|
||||
- Отсутствуют многие retail-API: нет `C_Timer`, нет `CreateFramePool`, нет `PixelUtil`
|
||||
- Таймеры — через `OnUpdate` с накоплением `elapsed`
|
||||
|
||||
---
|
||||
|
||||
## Pixel-Perfect: перенос дизайна из Figma/PSD в WoW UI
|
||||
|
||||
### Главная проблема: система координат WoW ≠ пиксели экрана
|
||||
|
||||
WoW рисует UI в **виртуальных единицах**, а не в экранных пикселях. Размер виртуального экрана зафиксирован движком на **768 единиц по высоте**, ширина зависит от соотношения сторон:
|
||||
|
||||
| Разрешение | Виртуальная ширина | Виртуальная высота |
|
||||
|-----------|-------------------|-------------------|
|
||||
| 1024×768 | 1024 | 768 |
|
||||
| 1280×960 | 1024 | 768 |
|
||||
| 1920×1080 | 1365 | 768 |
|
||||
| 2560×1440 | 1365 | 768 |
|
||||
|
||||
> **Вывод**: высота UIParent всегда = 768. Всё, что сделано в Figma на 768px по высоте, переносится 1:1 без пересчёта. При другой высоте холста в Figma — нужен коэффициент.
|
||||
|
||||
### Пересчёт координат из Figma в WoW
|
||||
|
||||
```lua
|
||||
-- Формула: wow_value = figma_value * (768 / figma_canvas_height)
|
||||
-- Если макет сделан на 1080px:
|
||||
local SCALE = 768 / 1080 -- ≈ 0.711
|
||||
|
||||
local function px(figma_px)
|
||||
return math.floor(figma_px * SCALE + 0.5) -- округление к ближайшему
|
||||
end
|
||||
|
||||
-- Использование:
|
||||
frame:SetWidth(px(400)) -- figma: 400px → wow: 284
|
||||
frame:SetHeight(px(200)) -- figma: 200px → wow: 142
|
||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
||||
```
|
||||
|
||||
> Если макет 768px в высоту — `SCALE = 1.0`, пересчёт не нужен.
|
||||
|
||||
### UIParent scale и как он ломает пиксели
|
||||
|
||||
Игрок может изменить масштаб UI в настройках. Это сдвигает все координаты.
|
||||
|
||||
```lua
|
||||
-- Получить текущий масштаб UIParent
|
||||
local uiScale = UIParent:GetScale() -- обычно 0.64–1.0
|
||||
|
||||
-- Перевести экранные пиксели в виртуальные единицы WoW
|
||||
local function screenPxToWow(px)
|
||||
local screenH = select(2, GetPhysicalScreenSize()) -- реальный размер экрана
|
||||
return px * (768 / screenH) / uiScale
|
||||
end
|
||||
|
||||
-- Принудительно зафиксировать scale (использовать с умом!)
|
||||
-- UIParent:SetScale(1.0) -- сломает все стандартные фреймы
|
||||
```
|
||||
|
||||
**Рекомендация**: не трогай `UIParent:SetScale()`. Вместо этого рисуй в виртуальных единицах и принимай, что у разных игроков масштаб разный. Для абсолютного pixel-perfect на конкретном разрешении — дай опцию ручного масштабирования фрейма в настройках.
|
||||
|
||||
```lua
|
||||
-- Масштабируй только свой фрейм
|
||||
frame:SetScale(0.85)
|
||||
-- Тогда все дочерние элементы тоже масштабируются
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Цвета: Figma HEX/RGBA → WoW RGBA
|
||||
|
||||
WoW принимает цвета в диапазоне **0.0–1.0** (не 0–255).
|
||||
|
||||
```lua
|
||||
-- Конвертер HEX → WoW RGBA
|
||||
local function hex(str, a)
|
||||
str = str:gsub("#", "")
|
||||
local r = tonumber(str:sub(1,2), 16) / 255
|
||||
local g = tonumber(str:sub(3,4), 16) / 255
|
||||
local b = tonumber(str:sub(5,6), 16) / 255
|
||||
return r, g, b, a or 1.0
|
||||
end
|
||||
|
||||
-- Использование:
|
||||
tex:SetVertexColor(hex("#FF6A00")) -- непрозрачный
|
||||
tex:SetVertexColor(hex("#FF6A00", 0.8)) -- 80% непрозрачность
|
||||
fs:SetTextColor(hex("#FFD100"))
|
||||
frame:SetBackdropColor(hex("#1A1A2E", 0.95))
|
||||
frame:SetBackdropBorderColor(hex("#4A90D9"))
|
||||
```
|
||||
|
||||
**Figma opacity → WoW alpha**: значение из Figma (например, 75%) делится на 100 → `0.75`.
|
||||
|
||||
```lua
|
||||
-- Figma: opacity 60%
|
||||
frame:SetAlpha(0.60)
|
||||
tex:SetAlpha(0.60)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Шрифты: как приблизить к дизайну
|
||||
|
||||
WoW 3.3.5a поддерживает только `.ttf` шрифты, помещённые в папку аддона.
|
||||
|
||||
```lua
|
||||
-- Кастомный шрифт из аддона
|
||||
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", 14, "OUTLINE")
|
||||
-- Флаги: "", "OUTLINE", "THICKOUTLINE", "MONOCHROME", "OUTLINE,MONOCHROME"
|
||||
```
|
||||
|
||||
**Соответствие Figma → WoW размер шрифта**:
|
||||
|
||||
| Figma (макет 1080px) | WoW pt (при SCALE=0.711) |
|
||||
|---------------------|--------------------------|
|
||||
| 12 px | 9 |
|
||||
| 14 px | 10 |
|
||||
| 16 px | 11 |
|
||||
| 18 px | 13 |
|
||||
| 24 px | 17 |
|
||||
| 32 px | 23 |
|
||||
|
||||
```lua
|
||||
-- Автоматический пересчёт размера шрифта
|
||||
local function fontSize(figma_pt)
|
||||
return math.max(8, math.floor(figma_pt * SCALE + 0.5))
|
||||
end
|
||||
fs:SetFont("Fonts\\FRIZQT__.TTF", fontSize(18), "OUTLINE")
|
||||
```
|
||||
|
||||
> **Минимальный читаемый размер в WoW**: 8 pt. Ниже — нечитаемо.
|
||||
|
||||
**Встроенные шрифты WoW 3.3.5a**:
|
||||
```
|
||||
Fonts\FRIZQT__.TTF — основной интерфейсный (без кириллицы!)
|
||||
Fonts\MORPHEUS.ttf — декоративный (заголовки)
|
||||
Fonts\ARIALN.TTF — Arial Narrow
|
||||
Fonts\skurri.ttf — шрифт повреждений
|
||||
```
|
||||
|
||||
> **Кириллица**: встроенные шрифты WoW не поддерживают кириллицу. Положи в аддон `.ttf` с кириллицей (например, PT Sans, Roboto) и подключи явно.
|
||||
|
||||
---
|
||||
|
||||
### Текстуры: подготовка под ограничения движка
|
||||
|
||||
#### Требования к текстурам WoW 3.3.5a
|
||||
- Ширина и высота — **степени двойки**: 2, 4, 8, 16, 32, 64, 128, 256, 512
|
||||
- Максимум: **512×512 px**
|
||||
- Формат: `.tga` (RGBA, 32-bit) или `.blp`
|
||||
- Текстура не обязана быть квадратной: 512×64 — ок, 256×128 — ок
|
||||
|
||||
#### Как уместить большой элемент дизайна
|
||||
|
||||
Если элемент из Figma больше 512px — разрезай на части:
|
||||
|
||||
```
|
||||
[512×256] [512×256] ← верхняя половина (левая + правая)
|
||||
[512×256] [512×256] ← нижняя половина
|
||||
```
|
||||
|
||||
```lua
|
||||
-- Сборка из 4 текстур
|
||||
local tl = frame:CreateTexture(nil, "BACKGROUND")
|
||||
tl:SetTexture("Interface\\AddOns\\MyAddon\\bg_tl")
|
||||
tl:SetPoint("TOPLEFT", frame, "TOPLEFT")
|
||||
tl:SetWidth(256); tl:SetHeight(128)
|
||||
|
||||
local tr = frame:CreateTexture(nil, "BACKGROUND")
|
||||
tr:SetTexture("Interface\\AddOns\\MyAddon\\bg_tr")
|
||||
tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT")
|
||||
tr:SetWidth(256); tr:SetHeight(128)
|
||||
-- ... и так далее
|
||||
```
|
||||
|
||||
#### TexCoord — показать только часть текстуры
|
||||
|
||||
Если в одном файле несколько элементов (спрайт-лист), используй `SetTexCoord`:
|
||||
|
||||
```lua
|
||||
-- SetTexCoord(left, right, top, bottom) — от 0.0 до 1.0
|
||||
-- Пример: иконка занимает правый нижний квадрант 256×256 атласа
|
||||
tex:SetTexCoord(0.5, 1.0, 0.5, 1.0)
|
||||
|
||||
-- Формула для ячейки в сетке (col/row с нуля):
|
||||
local cols, rows = 4, 4
|
||||
local col, row = 2, 1 -- третья колонка, вторая строка
|
||||
tex:SetTexCoord(
|
||||
col/cols, -- left
|
||||
(col+1)/cols, -- right
|
||||
row/rows, -- top
|
||||
(row+1)/rows -- bottom
|
||||
)
|
||||
```
|
||||
|
||||
> **Важно**: при SetTexCoord текстура тайлится внутри заданного Size фрейма. SetAllPoints растягивает — SetWidth/SetHeight + SetPoint = точный размер.
|
||||
|
||||
---
|
||||
|
||||
### Позиционирование: от Figma к SetPoint
|
||||
|
||||
В Figma позиция элемента — это X,Y от левого верхнего угла холста (или родителя). В WoW — якорная система.
|
||||
|
||||
**Алгоритм переноса**:
|
||||
|
||||
1. Определи ближайший угол/сторону в Figma (от чего считать)
|
||||
2. Вычисли отступ от этой стороны
|
||||
3. Пересчитай через `px()`
|
||||
|
||||
```lua
|
||||
-- Figma (холст 1365×768):
|
||||
-- Элемент: X=50, Y=30, W=200, H=100
|
||||
-- → привязка от TOPLEFT UIParent
|
||||
|
||||
frame:SetWidth(px(200))
|
||||
frame:SetHeight(px(100))
|
||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
||||
-- ^^^ ^^^
|
||||
-- X offset Y offset (отрицательный! вниз)
|
||||
```
|
||||
|
||||
> **Знаки осей WoW**: X растёт вправо (+), Y растёт вверх (+). Отступ вниз от TOPLEFT — **отрицательный**.
|
||||
|
||||
| Figma anchor | WoW SetPoint |
|
||||
|-------------|-------------|
|
||||
| Top-Left | `"TOPLEFT", UIParent, "TOPLEFT", x, -y` |
|
||||
| Top-Right | `"TOPRIGHT", UIParent, "TOPRIGHT", -x, -y` |
|
||||
| Bottom-Left | `"BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y` |
|
||||
| Bottom-Right | `"BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -x, y` |
|
||||
| Center | `"CENTER", UIParent, "CENTER", x - W/2_canvas, y` |
|
||||
|
||||
---
|
||||
|
||||
### Вспомогательный модуль: px-утилиты (вставь в начало Lua)
|
||||
|
||||
```lua
|
||||
-- PixelHelper — pixel-perfect вёрстка из Figma
|
||||
local PH = {}
|
||||
|
||||
PH.CANVAS_HEIGHT = 768 -- высота холста в Figma (измени если другая!)
|
||||
|
||||
function PH.scale()
|
||||
return 768 / PH.CANVAS_HEIGHT
|
||||
end
|
||||
|
||||
-- Пересчёт px из Figma → WoW единицы
|
||||
function PH.px(v)
|
||||
return math.floor(v * PH.scale() + 0.5)
|
||||
end
|
||||
|
||||
-- Цвет из HEX (#RRGGBB или #RRGGBBAA)
|
||||
function PH.color(hex, alpha)
|
||||
hex = hex:gsub("#","")
|
||||
local r = tonumber(hex:sub(1,2),16)/255
|
||||
local g = tonumber(hex:sub(3,4),16)/255
|
||||
local b = tonumber(hex:sub(5,6),16)/255
|
||||
local a = alpha or (hex:len() == 8 and tonumber(hex:sub(7,8),16)/255) or 1
|
||||
return r, g, b, a
|
||||
end
|
||||
|
||||
-- Размер шрифта из Figma → WoW
|
||||
function PH.font(pt)
|
||||
return math.max(8, math.floor(pt * PH.scale() + 0.5))
|
||||
end
|
||||
|
||||
-- Установка якоря с автопересчётом
|
||||
function PH.point(frame, point, relativeTo, relPoint, x, y)
|
||||
frame:SetPoint(point, relativeTo, relPoint, PH.px(x or 0), PH.px(y or 0))
|
||||
end
|
||||
|
||||
-- Размер фрейма из Figma
|
||||
function PH.size(frame, w, h)
|
||||
frame:SetWidth(PH.px(w))
|
||||
frame:SetHeight(PH.px(h))
|
||||
end
|
||||
|
||||
-- Экспорт глобально (или оставь локальным)
|
||||
MyAddon_PH = PH
|
||||
```
|
||||
|
||||
**Использование**:
|
||||
```lua
|
||||
local px = MyAddon_PH.px
|
||||
local color = MyAddon_PH.color
|
||||
local pxfont = MyAddon_PH.font
|
||||
|
||||
MyAddon_PH.size(frame, 400, 250)
|
||||
MyAddon_PH.point(frame, "TOPLEFT", UIParent, "TOPLEFT", 50, -30)
|
||||
frame:SetBackdropColor(color("#1A1A2E", 0.95))
|
||||
fs:SetTextColor(color("#FFD100"))
|
||||
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", pxfont(16), "OUTLINE")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Чеклист pixel-perfect переноса
|
||||
|
||||
- [ ] Холст Figma = 768px в высоту (или задан `PH.CANVAS_HEIGHT`)
|
||||
- [ ] Все размеры и отступы пересчитаны через `px()`
|
||||
- [ ] Цвета конвертированы через `color()`
|
||||
- [ ] Текстуры нарезаны до 512×512, размеры кратны степени двойки
|
||||
- [ ] Шрифты `.ttf` подключены из папки аддона (если кириллица или кастомный шрифт)
|
||||
- [ ] Размеры шрифтов пересчитаны через `font()`
|
||||
- [ ] Y-отступы от верхних якорей — **отрицательные**
|
||||
- [ ] Проверено на scale UIParent ≠ 1.0 (изменить в настройках игры для теста)
|
||||
- [ ] Текстуры без SetAllPoints — точный SetWidth/SetHeight + SetPoint
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
MyAddon/
|
||||
├── MyAddon.toc ← обязателен, имя файла = имя папки
|
||||
├── MyAddon.xml ← верстка фреймов (опционально)
|
||||
└── MyAddon.lua ← логика
|
||||
```
|
||||
|
||||
### TOC-файл (3.3.5a)
|
||||
|
||||
```toc
|
||||
## Interface: 30300
|
||||
## Title: My Addon
|
||||
## Notes: Описание аддона
|
||||
## Author: Ilyas
|
||||
## Version: 1.0
|
||||
## SavedVariables: MyAddonDB
|
||||
|
||||
MyAddon.xml
|
||||
MyAddon.lua
|
||||
```
|
||||
|
||||
> Файлы загружаются **в порядке перечисления**. XML раньше Lua — фреймы существуют к моменту выполнения скриптов.
|
||||
|
||||
---
|
||||
|
||||
## XML-верстка
|
||||
|
||||
### Минимальный шаблон XML-файла
|
||||
|
||||
```xml
|
||||
<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="MyAddon.lua"/>
|
||||
|
||||
<Frame name="MyAddon_Frame" parent="UIParent" toplevel="true" enableMouse="true" movable="true">
|
||||
<Size>
|
||||
<AbsDimension x="300" y="200"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
|
||||
edgeFile="Interface\DialogFrame\UI-DialogBox-Border"
|
||||
tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize><AbsValue val="32"/></TileSize>
|
||||
<EdgeSize><AbsValue val="32"/></EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormalLarge" text="Заголовок">
|
||||
<Anchors>
|
||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentClose" inherits="UIPanelCloseButton">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>MyAddon_Frame_OnLoad(self)</OnLoad>
|
||||
<OnMouseDown>self:StartMoving()</OnMouseDown>
|
||||
<OnMouseUp>self:StopMovingOrSizing()</OnMouseUp>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
</Ui>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Точки якоря (FRAMEPOINT)
|
||||
|
||||
| Точка | Расположение |
|
||||
|-------|-------------|
|
||||
| `TOPLEFT` | Верхний левый угол |
|
||||
| `TOP` | Верхний центр |
|
||||
| `TOPRIGHT` | Верхний правый угол |
|
||||
| `LEFT` | Левый центр |
|
||||
| `CENTER` | Центр |
|
||||
| `RIGHT` | Правый центр |
|
||||
| `BOTTOMLEFT` | Нижний левый угол |
|
||||
| `BOTTOM` | Нижний центр |
|
||||
| `BOTTOMRIGHT` | Нижний правый угол |
|
||||
|
||||
### SetPoint через Lua
|
||||
|
||||
```lua
|
||||
-- frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
|
||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, -10)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetPoint("BOTTOMRIGHT", anotherFrame, "TOPRIGHT", 0, 5)
|
||||
|
||||
-- Сброс якорей перед переустановкой
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER")
|
||||
```
|
||||
|
||||
> **Правило**: фрейм должен иметь достаточно якорей для однозначного определения позиции **и** размера. Либо Size + один якорь, либо два якоря задающих оба измерения.
|
||||
|
||||
---
|
||||
|
||||
## Слои и страты
|
||||
|
||||
### Draw Layers (внутри фрейма, сверху вниз по видимости)
|
||||
|
||||
```
|
||||
BACKGROUND (0) → BORDER (1) → ARTWORK (2) → OVERLAY (3) → HIGHLIGHT (4)
|
||||
```
|
||||
|
||||
- `HIGHLIGHT` — автоматически показывается при наведении мыши (требует `enableMouse="true"`)
|
||||
- FontString всегда рисуется **поверх** Texture в том же слое
|
||||
- Порядок текстур в одном слое **не гарантирован** → разноси по слоям
|
||||
|
||||
### Frame Strata (между фреймами)
|
||||
|
||||
```
|
||||
WORLD → BACKGROUND → LOW → MEDIUM → HIGH → DIALOG → FULLSCREEN → FULLSCREEN_DIALOG → TOOLTIP
|
||||
```
|
||||
|
||||
```lua
|
||||
frame:SetFrameStrata("DIALOG")
|
||||
frame:SetFrameLevel(5) -- внутри страты (0–255)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Виджеты, события, Blizzard API
|
||||
|
||||
> Полный справочник — в `references/widgets-and-api.md`.
|
||||
> Читай его когда нужны: типы фреймов, Button/EditBox/ScrollFrame/StatusBar, события OnEvent/OnUpdate, SavedVariables, пути к текстурам, типичные ошибки.
|
||||
|
||||
---
|
||||
|
||||
## Шаблонный аддон «с нуля» (минимальный рабочий пример)
|
||||
|
||||
**MyAddon.toc**
|
||||
```toc
|
||||
## Interface: 30300
|
||||
## Title: MyAddon
|
||||
## SavedVariables: MyAddonDB
|
||||
MyAddon.lua
|
||||
```
|
||||
|
||||
**MyAddon.lua**
|
||||
```lua
|
||||
local ADDON_NAME = "MyAddon"
|
||||
|
||||
local f = CreateFrame("Frame", ADDON_NAME.."_Frame", UIParent)
|
||||
f:SetWidth(250)
|
||||
f:SetHeight(150)
|
||||
f:SetPoint("CENTER")
|
||||
f:SetFrameStrata("DIALOG")
|
||||
f:SetMovable(true)
|
||||
f:EnableMouse(true)
|
||||
f:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = {left=11, right=12, top=12, bottom=11}
|
||||
})
|
||||
f:Hide()
|
||||
|
||||
-- Заголовок
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||
title:SetPoint("TOP", f, "TOP", 0, -12)
|
||||
title:SetText("My Addon")
|
||||
|
||||
-- Закрытие
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
|
||||
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 4, 4)
|
||||
close:SetScript("OnClick", function() f:Hide() end)
|
||||
|
||||
-- Перетаскивание
|
||||
f:SetScript("OnMouseDown", function(self, btn)
|
||||
if btn == "LeftButton" then self:StartMoving() end
|
||||
end)
|
||||
f:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)
|
||||
|
||||
-- Инициализация
|
||||
f:RegisterEvent("ADDON_LOADED")
|
||||
f:SetScript("OnEvent", function(self, event, name)
|
||||
if event == "ADDON_LOADED" and name == ADDON_NAME then
|
||||
MyAddonDB = MyAddonDB or {}
|
||||
print("|cff00ff00"..ADDON_NAME.."|r загружен. /myaddon")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Слэш-команда
|
||||
SLASH_MYADDON1 = "/myaddon"
|
||||
SlashCmdList["MYADDON"] = function()
|
||||
if f:IsShown() then f:Hide() else f:Show() end
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,256 @@
|
||||
# WoW 3.3.5a — Виджеты, события и Blizzard API
|
||||
|
||||
## Создание фреймов через Lua
|
||||
|
||||
### CreateFrame
|
||||
|
||||
```lua
|
||||
-- CreateFrame(frameType, name, parent, template)
|
||||
local f = CreateFrame("Frame", "MyAddon_MainFrame", UIParent)
|
||||
f:SetWidth(300)
|
||||
f:SetHeight(200)
|
||||
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
f:SetFrameStrata("DIALOG")
|
||||
f:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
||||
})
|
||||
f:SetBackdropColor(0, 0, 0, 0.8)
|
||||
```
|
||||
|
||||
### Типы фреймов (frameType)
|
||||
|
||||
| Тип | Назначение |
|
||||
|-----|-----------|
|
||||
| `Frame` | Базовый контейнер |
|
||||
| `Button` | Кнопка |
|
||||
| `EditBox` | Поле ввода текста |
|
||||
| `ScrollFrame` | Прокручиваемая область |
|
||||
| `StatusBar` | Полоска (HP, мана, XP) |
|
||||
| `Cooldown` | Кулдаун-свип на иконке |
|
||||
| `GameTooltip` | Тултип |
|
||||
| `MessageFrame` | Прокручиваемые сообщения |
|
||||
| `Model` | 3D-модель |
|
||||
| `MovieFrame` | Видео |
|
||||
|
||||
---
|
||||
|
||||
## Текстуры
|
||||
|
||||
```lua
|
||||
local tex = frame:CreateTexture(nil, "ARTWORK")
|
||||
tex:SetTexture("Interface\\AddOns\\MyAddon\\images\\icon")
|
||||
tex:SetAllPoints(frame)
|
||||
tex:SetWidth(32)
|
||||
tex:SetHeight(32)
|
||||
tex:SetPoint("CENTER", frame, "CENTER")
|
||||
tex:SetVertexColor(1, 0.5, 0, 1)
|
||||
tex:SetAlpha(0.8)
|
||||
|
||||
-- Вырезка части текстуры
|
||||
tex:SetTexCoord(0, 0.5, 0, 0.5)
|
||||
```
|
||||
|
||||
## FontString (текст)
|
||||
|
||||
```lua
|
||||
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
fs:SetPoint("CENTER", frame, "CENTER")
|
||||
fs:SetText("Привет, Азерот!")
|
||||
fs:SetTextColor(1, 1, 0, 1)
|
||||
fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
|
||||
```
|
||||
|
||||
### Стандартные шаблоны шрифтов
|
||||
|
||||
| Шаблон | Применение |
|
||||
|--------|-----------|
|
||||
| `GameFontNormal` | Обычный текст интерфейса |
|
||||
| `GameFontNormalLarge` | Заголовки |
|
||||
| `GameFontNormalSmall` | Мелкий текст |
|
||||
| `GameFontHighlight` | Подсвеченный |
|
||||
| `GameFontDisable` | Неактивный (серый) |
|
||||
| `NumberFontNormal` | Числа |
|
||||
| `ChatFontNormal` | Чат |
|
||||
|
||||
---
|
||||
|
||||
## Button
|
||||
|
||||
```lua
|
||||
local btn = CreateFrame("Button", "MyAddon_Btn", frame, "UIPanelButtonTemplate")
|
||||
btn:SetWidth(80)
|
||||
btn:SetHeight(22)
|
||||
btn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 10)
|
||||
btn:SetText("Нажми")
|
||||
btn:SetScript("OnClick", function(self)
|
||||
print("Нажато!")
|
||||
end)
|
||||
|
||||
btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-Button-Up")
|
||||
btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-Button-Down")
|
||||
btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
|
||||
```
|
||||
|
||||
### Шаблоны кнопок
|
||||
|
||||
| Шаблон | Вид |
|
||||
|--------|-----|
|
||||
| `UIPanelButtonTemplate` | Стандартная кнопка интерфейса |
|
||||
| `UIPanelCloseButton` | Крестик закрытия окна |
|
||||
| `UIMenuButtonStretchTemplate` | Растягиваемая кнопка меню |
|
||||
| `SecureActionButtonTemplate` | Для действий в бою (protected) |
|
||||
|
||||
---
|
||||
|
||||
## ScrollFrame
|
||||
|
||||
```lua
|
||||
local scroll = CreateFrame("ScrollFrame", "MyAddon_Scroll", frame, "UIPanelScrollFrameTemplate")
|
||||
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
|
||||
scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 8)
|
||||
|
||||
local child = CreateFrame("Frame", "MyAddon_ScrollChild", scroll)
|
||||
child:SetWidth(scroll:GetWidth())
|
||||
child:SetHeight(400)
|
||||
scroll:SetScrollChild(child)
|
||||
```
|
||||
|
||||
## EditBox
|
||||
|
||||
```lua
|
||||
local eb = CreateFrame("EditBox", "MyAddon_Input", frame, "InputBoxTemplate")
|
||||
eb:SetWidth(200)
|
||||
eb:SetHeight(20)
|
||||
eb:SetPoint("CENTER", frame, "CENTER")
|
||||
eb:SetAutoFocus(false)
|
||||
eb:SetMaxLetters(255)
|
||||
eb:SetScript("OnEnterPressed", function(self)
|
||||
local text = self:GetText()
|
||||
self:ClearFocus()
|
||||
end)
|
||||
```
|
||||
|
||||
## StatusBar
|
||||
|
||||
```lua
|
||||
local bar = CreateFrame("StatusBar", nil, frame)
|
||||
bar:SetWidth(200)
|
||||
bar:SetHeight(16)
|
||||
bar:SetPoint("CENTER", frame, "CENTER")
|
||||
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
||||
bar:SetStatusBarColor(0, 1, 0, 1)
|
||||
bar:SetMinMaxValues(0, 100)
|
||||
bar:SetValue(75)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Скрипты-события
|
||||
|
||||
```lua
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
frame:RegisterEvent("CHAT_MSG_SAY")
|
||||
|
||||
frame:SetScript("OnEvent", function(self, event, ...)
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
-- вход в мир
|
||||
elseif event == "CHAT_MSG_SAY" then
|
||||
local msg, author = ...
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
### Ключевые события
|
||||
|
||||
| Событие | Когда |
|
||||
|---------|-------|
|
||||
| `PLAYER_ENTERING_WORLD` | Вход в мир / reload |
|
||||
| `ADDON_LOADED` | Аддон загружен (arg1 == имя) |
|
||||
| `PLAYER_LOGIN` | После входа |
|
||||
| `PLAYER_LOGOUT` | При выходе |
|
||||
| `VARIABLES_LOADED` | SavedVariables загружены |
|
||||
| `UI_ERROR_MESSAGE` | Ошибка интерфейса |
|
||||
|
||||
### OnUpdate — таймер
|
||||
|
||||
```lua
|
||||
local elapsed_total = 0
|
||||
frame:SetScript("OnUpdate", function(self, elapsed)
|
||||
elapsed_total = elapsed_total + elapsed
|
||||
if elapsed_total >= 1.0 then
|
||||
elapsed_total = 0
|
||||
-- действие каждую секунду
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SavedVariables
|
||||
|
||||
```lua
|
||||
-- В TOC: ## SavedVariables: MyAddonDB
|
||||
frame:RegisterEvent("ADDON_LOADED")
|
||||
frame:SetScript("OnEvent", function(self, event, addonName)
|
||||
if event == "ADDON_LOADED" and addonName == "MyAddon" then
|
||||
MyAddonDB = MyAddonDB or { position = { "CENTER", 0, 0 } }
|
||||
local p = MyAddonDB.position
|
||||
self:ClearAllPoints()
|
||||
self:SetPoint(p[1], UIParent, p[1], p[2], p[3])
|
||||
end
|
||||
end)
|
||||
|
||||
frame:SetScript("OnMouseUp", function(self)
|
||||
self:StopMovingOrSizing()
|
||||
local point, _, _, x, y = self:GetPoint()
|
||||
MyAddonDB.position = { point, x, y }
|
||||
end)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полезные Blizzard-утилиты (3.3.5a)
|
||||
|
||||
```lua
|
||||
_G["MyAddon_Frame"]:Show()
|
||||
IsAddOnLoaded("MyAddon")
|
||||
UnitName("player")
|
||||
UnitClass("target")
|
||||
UnitHealth("player"), UnitHealthMax("player")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Красный текст|r")
|
||||
print("Белый текст")
|
||||
UnitAffectingCombat("player")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Часто используемые пути к текстурам Blizzard
|
||||
|
||||
```
|
||||
Interface\DialogFrame\UI-DialogBox-Background
|
||||
Interface\DialogFrame\UI-DialogBox-Border
|
||||
Interface\DialogFrame\UI-DialogBox-Gold-Background
|
||||
Interface\Tooltips\UI-Tooltip-Background
|
||||
Interface\Tooltips\UI-Tooltip-Border
|
||||
Interface\Buttons\UI-Panel-Button-Up
|
||||
Interface\Buttons\UI-Panel-Button-Down
|
||||
Interface\Icons\INV_Misc_QuestionMark
|
||||
Interface\TargetingFrame\UI-StatusBar
|
||||
Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Типичные ошибки и решения
|
||||
|
||||
| Ошибка | Причина | Решение |
|
||||
|--------|---------|---------|
|
||||
| `attempt to index global 'X' (nil)` | Фрейм не создан | Проверь порядок файлов в TOC |
|
||||
| `SetPoint anchor family connection` | Циклические якоря | Переписать цепочку якорей |
|
||||
| `Script ran too long` | Бесконечный цикл OnUpdate | Early return или счётчик |
|
||||
| Текстура не отображается | Размер не кратен 2 / >512px | Пересохрани в TGA/BLP |
|
||||
| `bad argument #1 (string expected)` | nil вместо строки | `if val then` |
|
||||
| Frame не кликается | Нет `enableMouse="true"` | Добавить атрибут |
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release',
|
||||
[string]$ClientPath = '',
|
||||
[string]$PackagePath = '',
|
||||
[switch]$Deploy,
|
||||
[switch]$NativeRenderer
|
||||
)
|
||||
|
||||
function Enable-LargeAddressAware {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
$bytes = [System.IO.File]::ReadAllBytes($Path)
|
||||
|
||||
if ($bytes.Length -lt 64) {
|
||||
throw "File is too small to be a valid PE executable: $Path"
|
||||
}
|
||||
|
||||
# DOS header: MZ
|
||||
if ($bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) {
|
||||
throw "DOS MZ signature was not found: $Path"
|
||||
}
|
||||
|
||||
# IMAGE_DOS_HEADER.e_lfanew
|
||||
$peOffset = [BitConverter]::ToInt32($bytes, 0x3C)
|
||||
|
||||
if ($peOffset -lt 0 -or ($peOffset + 24) -gt $bytes.Length) {
|
||||
throw "Invalid PE header offset in: $Path"
|
||||
}
|
||||
|
||||
# PE\0\0 signature
|
||||
$peSignature = [BitConverter]::ToUInt32($bytes, $peOffset)
|
||||
|
||||
if ($peSignature -ne 0x00004550) {
|
||||
throw "PE signature was not found: $Path"
|
||||
}
|
||||
|
||||
# IMAGE_FILE_HEADER starts after the 4-byte PE signature.
|
||||
# Characteristics is at offset 18 inside IMAGE_FILE_HEADER.
|
||||
$characteristicsOffset = $peOffset + 4 + 18
|
||||
$characteristics = [BitConverter]::ToUInt16(
|
||||
$bytes,
|
||||
$characteristicsOffset
|
||||
)
|
||||
|
||||
$newCharacteristics = [uint16](
|
||||
$characteristics -bor 0x0020
|
||||
)
|
||||
|
||||
if ($newCharacteristics -ne $characteristics) {
|
||||
$encoded = [BitConverter]::GetBytes($newCharacteristics)
|
||||
|
||||
$bytes[$characteristicsOffset] = $encoded[0]
|
||||
$bytes[$characteristicsOffset + 1] = $encoded[1]
|
||||
|
||||
[System.IO.File]::WriteAllBytes($Path, $bytes)
|
||||
|
||||
Write-Host (
|
||||
"LAA enabled for {0}: 0x{1:X4} -> 0x{2:X4}" -f
|
||||
$Path,
|
||||
$characteristics,
|
||||
$newCharacteristics
|
||||
) -ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
Write-Host (
|
||||
"LAA is already enabled for {0}: 0x{1:X4}" -f
|
||||
$Path,
|
||||
$characteristics
|
||||
) -ForegroundColor DarkGreen
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
if (-not $cmakeCommand) { throw 'CMake 3.20+ was not found.' }
|
||||
$cmake = $cmakeCommand.Source
|
||||
}
|
||||
|
||||
Initialize-MissingSubmodules -RepositoryPath $repoRoot
|
||||
|
||||
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
|
||||
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
|
||||
|
||||
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||
if (-not (Test-Path -LiteralPath $stockExe -PathType Leaf)) {
|
||||
throw "Stock executable was not found: $stockExe"
|
||||
}
|
||||
$expectedStockHash = 'AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8'
|
||||
$stockHash = (Get-FileHash -LiteralPath $stockExe -Algorithm SHA256).Hash
|
||||
if ($stockHash -ne $expectedStockHash) {
|
||||
throw "Wow_Original.exe is not the verified MoonWell 3.3.5a stock executable (SHA-256 $stockHash)."
|
||||
}
|
||||
}
|
||||
|
||||
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
|
||||
& $cmake --build $win32BuildDir --config $Configuration --parallel 4
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
|
||||
|
||||
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
|
||||
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
|
||||
$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(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Destination
|
||||
)
|
||||
|
||||
Remove-MoonWellRuntimeGarbage -Destination $Destination
|
||||
|
||||
$utils = Join-Path $Destination 'Utils'
|
||||
New-Item `
|
||||
-ItemType Directory `
|
||||
-Path $Destination, $utils `
|
||||
-Force |
|
||||
Out-Null
|
||||
|
||||
$destinationExe = Join-Path $Destination 'Wow.exe'
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $stockExe `
|
||||
-Destination $destinationExe `
|
||||
-Force
|
||||
|
||||
Enable-LargeAddressAware -Path $destinationExe
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $warcraftXL `
|
||||
-Destination (Join-Path $Destination 'WarcraftXL.dll') `
|
||||
-Force
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $selectedProxy `
|
||||
-Destination (Join-Path $Destination 'd3d9.dll') `
|
||||
-Force
|
||||
|
||||
Copy-Item `
|
||||
-LiteralPath $recoveryProxy `
|
||||
-Destination (Join-Path $utils 'd3d9-native.dll') `
|
||||
-Force
|
||||
|
||||
foreach ($extensionName in $extensionNames) {
|
||||
$extensionSource = Join-Path $win32BuildDir "$Configuration\$extensionName.dll"
|
||||
$extensionDestination = Join-Path $Destination "Extensions\$extensionName"
|
||||
New-Item -ItemType Directory -Path $extensionDestination -Force | Out-Null
|
||||
Copy-Item `
|
||||
-LiteralPath $extensionSource `
|
||||
-Destination (Join-Path $extensionDestination "$extensionName.dll") `
|
||||
-Force
|
||||
}
|
||||
}
|
||||
|
||||
if ($Deploy) {
|
||||
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
$ClientPath = 'C:\Program Files (x86)\World of Warcraft'
|
||||
}
|
||||
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
|
||||
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
|
||||
throw "Client directory was not found: $ClientPath"
|
||||
}
|
||||
|
||||
$clientExe = Join-Path $ClientPath 'Wow.exe'
|
||||
if (Test-Path -LiteralPath $clientExe -PathType Leaf) {
|
||||
$clientHash = (Get-FileHash -LiteralPath $clientExe -Algorithm SHA256).Hash
|
||||
if ($clientHash -ne $stockHash) {
|
||||
$legacyBackup = Join-Path $ClientPath 'Wow.moonwell-patched.backup.exe'
|
||||
if (-not (Test-Path -LiteralPath $legacyBackup)) {
|
||||
Copy-Item -LiteralPath $clientExe -Destination $legacyBackup
|
||||
}
|
||||
Write-Host "Legacy executable saved as $legacyBackup"
|
||||
}
|
||||
}
|
||||
|
||||
Install-WarcraftXLArtifacts -Destination $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"
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -20,6 +22,11 @@ IGNORED_TOP_LEVEL_DIRS = {
|
||||
IGNORED_DIRS_ANYWHERE = {
|
||||
".git",
|
||||
".moonwell_launcher",
|
||||
".vscode"
|
||||
}
|
||||
IGNORED_TOP_LEVEL_FILES = {
|
||||
"Wow.moonwell-patched.backup.exe",
|
||||
"WarcraftXL_on12.disable",
|
||||
}
|
||||
|
||||
ENV_VAR_NAME = "WOW_HOME"
|
||||
@@ -42,6 +49,10 @@ def is_ignored(rel_path: Path) -> bool:
|
||||
return False
|
||||
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
|
||||
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.'
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"base_dbc_dir": "assets/dbc/3.3.5a",
|
||||
"model_data_id_start": 50000,
|
||||
"display_info_id_start": 50000,
|
||||
"creature_entry_id_start": 5000000,
|
||||
"summon_spell_id_start": 110000,
|
||||
"skill_line_ability_id_start": 30000,
|
||||
"defaults": {
|
||||
"mount_template_display_id": 2404,
|
||||
"pet_template_display_id": 5448
|
||||
},
|
||||
"mounts": [],
|
||||
"pets": []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"models": {
|
||||
"catslime": {
|
||||
"model_data_id": 50000,
|
||||
"display_info_id": 50000,
|
||||
"creature_entry": 5000000,
|
||||
"summon_spell_id": 110000,
|
||||
"skill_line_ability_id": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ClientPath = '',
|
||||
[string]$PackagePath = '',
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release',
|
||||
[switch]$SkipDataBuild,
|
||||
[switch]$SkipManifest,
|
||||
[switch]$NativeRenderer,
|
||||
[switch]$StopClient,
|
||||
[switch]$Launch
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = $PSScriptRoot
|
||||
. (Join-Path $repoRoot 'pipeline-layout.ps1')
|
||||
|
||||
function Import-ProjectEnvironment {
|
||||
$envFile = Join-Path $repoRoot '.env'
|
||||
if (-not (Test-Path -LiteralPath $envFile -PathType Leaf)) { return }
|
||||
|
||||
foreach ($line in Get-Content -LiteralPath $envFile -Encoding UTF8) {
|
||||
if ($line -notmatch '^\s*([^#][^=]+)=(.*)$') { continue }
|
||||
$name = $matches[1].Trim()
|
||||
$value = $matches[2].Trim().Trim('"').Trim("'")
|
||||
if ($name -and -not (Test-Path "Env:$name")) {
|
||||
Set-Item -Path "Env:$name" -Value $value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Import-ProjectEnvironment
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
$ClientPath = if ($env:WOW_HOME) {
|
||||
$env:WOW_HOME
|
||||
} else {
|
||||
'C:\Program Files (x86)\World of Warcraft'
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||
$PackagePath = Join-Path $repoRoot 'dist'
|
||||
}
|
||||
|
||||
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
|
||||
$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)
|
||||
})
|
||||
if ($runningClients.Count) {
|
||||
if (-not $StopClient) {
|
||||
throw 'Wow.exe is running. Close the client or repeat deployment with -StopClient.'
|
||||
}
|
||||
|
||||
Write-Host 'Stopping the running WoW client...'
|
||||
$runningClients | Stop-Process -Force
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
$env:WOW_HOME = $ClientPath
|
||||
Write-Host "MoonWell client: $ClientPath"
|
||||
Write-Host "Package staging: $PackagePath"
|
||||
|
||||
Write-Host '[1/5] Preserving initialized WarcraftXL worktrees...'
|
||||
|
||||
$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...'
|
||||
$tool = Join-Path $repoRoot 'tool\target\release\tool.exe'
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if ($cargo) {
|
||||
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
|
||||
} elseif (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
|
||||
throw 'Cargo was not found and the MPQ builder is not compiled.'
|
||||
}
|
||||
|
||||
& $tool (Join-Path $repoRoot 'src') $PackagePath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'MPQ patch build failed.' }
|
||||
} else {
|
||||
Write-Host '[2/5] MPQ build skipped.'
|
||||
}
|
||||
|
||||
Write-Host '[3/5] Building and installing WarcraftXL...'
|
||||
$buildArguments = @{
|
||||
Configuration = $Configuration
|
||||
ClientPath = $ClientPath
|
||||
PackagePath = $PackagePath
|
||||
Deploy = $true
|
||||
}
|
||||
if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
|
||||
& (Join-Path $repoRoot 'build-warcraftxl.ps1') @buildArguments
|
||||
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) {
|
||||
throw "Package synchronization failed with robocopy exit code $robocopyExitCode."
|
||||
}
|
||||
|
||||
if (-not $SkipManifest) {
|
||||
Write-Host '[5/5] Building launcher manifest...'
|
||||
& $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.' }
|
||||
} else {
|
||||
Write-Host '[5/5] Manifest generation skipped.'
|
||||
}
|
||||
|
||||
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
|
||||
$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 " Extensions: $(Join-Path $ClientPath 'Extensions')"
|
||||
|
||||
if ($Launch) {
|
||||
Write-Host 'Launching Wow.exe...'
|
||||
Start-Process -FilePath (Join-Path $ClientPath 'Wow.exe') -WorkingDirectory $ClientPath
|
||||
}
|
||||
|
||||
# robocopy uses successful non-zero exit codes; do not leak one to callers.
|
||||
$global:LASTEXITCODE = 0
|
||||
@@ -0,0 +1,64 @@
|
||||
// Minimal D3D9 forwarding proxy for loading WarcraftXL without editing Wow.exe.
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d9.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
using Create9Fn = IDirect3D9* (WINAPI*)(UINT);
|
||||
using Create9ExFn = HRESULT (WINAPI*)(UINT, IDirect3D9Ex**);
|
||||
|
||||
HMODULE g_systemD3D9 = nullptr;
|
||||
Create9Fn g_create9 = nullptr;
|
||||
Create9ExFn g_create9Ex = nullptr;
|
||||
|
||||
void ResolveSystemD3D9()
|
||||
{
|
||||
if (g_systemD3D9) return;
|
||||
char path[MAX_PATH] = {};
|
||||
const UINT length = GetSystemDirectoryA(path, MAX_PATH);
|
||||
if (!length || length >= MAX_PATH - 10) return;
|
||||
lstrcatA(path, "\\d3d9.dll");
|
||||
g_systemD3D9 = LoadLibraryA(path);
|
||||
if (!g_systemD3D9) return;
|
||||
g_create9 = reinterpret_cast<Create9Fn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9"));
|
||||
g_create9Ex = reinterpret_cast<Create9ExFn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9Ex"));
|
||||
}
|
||||
|
||||
void LoadWarcraftXL()
|
||||
{
|
||||
static bool attempted = false;
|
||||
if (attempted) return;
|
||||
attempted = true;
|
||||
LoadLibraryA("WarcraftXL.dll");
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" IDirect3D9* WINAPI Direct3DCreate9(UINT sdkVersion)
|
||||
{
|
||||
ResolveSystemD3D9();
|
||||
LoadWarcraftXL();
|
||||
return g_create9 ? g_create9(sdkVersion) : nullptr;
|
||||
}
|
||||
|
||||
extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** output)
|
||||
{
|
||||
ResolveSystemD3D9();
|
||||
LoadWarcraftXL();
|
||||
if (g_create9Ex) return g_create9Ex(sdkVersion, output);
|
||||
if (output) *output = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
// 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() {}
|
||||
extern "C" void WxlSetSsaaFactor(float) {}
|
||||
extern "C" float WxlGetSsaaFactor() { return 1.0f; }
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
LIBRARY d3d9
|
||||
EXPORTS
|
||||
Direct3DCreate9
|
||||
Direct3DCreate9Ex
|
||||
WxlD3D12Device
|
||||
WxlD3D12Queue
|
||||
WxlD3D12DrainDebug
|
||||
WxlSetSsaaFactor
|
||||
WxlGetSsaaFactor
|
||||
@@ -1,339 +0,0 @@
|
||||
"""
|
||||
Wow.exe binary patcher for MoonWell custom features.
|
||||
|
||||
Patches:
|
||||
1. Script_CreateCharacter: reads lua_tonumber(L, 2) as outfitId,
|
||||
stores to global, passes to CMSG_CHAR_CREATE packet instead of 0.
|
||||
2. GetCharacterInfo: adds 11th return value — charFlags as a Lua number,
|
||||
read from struct_offset +0x170 (same field as ghost flag 0x2000).
|
||||
Lua checks bit 0x40000000 (CHARACTER_FLAG_UNK31) for traitor status.
|
||||
|
||||
Usage:
|
||||
python make_modes_in_exe.py <path_to_Wow.exe>
|
||||
|
||||
A backup (Wow.exe.bak) is created before patching.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PE helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _u16(data, off):
|
||||
return struct.unpack_from("<H", data, off)[0]
|
||||
|
||||
def _u32(data, off):
|
||||
return struct.unpack_from("<I", data, off)[0]
|
||||
|
||||
|
||||
def parse_pe_sections(data):
|
||||
if data[0:2] != b"MZ":
|
||||
raise ValueError("Not a valid PE file (missing MZ)")
|
||||
pe_off = _u32(data, 0x3C)
|
||||
if data[pe_off:pe_off + 4] != b"PE\x00\x00":
|
||||
raise ValueError("Not a valid PE file (missing PE signature)")
|
||||
coff = pe_off + 4
|
||||
num_secs = _u16(data, coff + 2)
|
||||
opt_size = _u16(data, coff + 16)
|
||||
opt = coff + 20
|
||||
magic = _u16(data, opt)
|
||||
if magic == 0x10b:
|
||||
image_base = _u32(data, opt + 28)
|
||||
elif magic == 0x20b:
|
||||
image_base = struct.unpack_from("<Q", data, opt + 24)[0]
|
||||
else:
|
||||
raise ValueError(f"Unknown PE magic: 0x{magic:04x}")
|
||||
sec_table = opt + opt_size
|
||||
sections = []
|
||||
for i in range(num_secs):
|
||||
o = sec_table + i * 40
|
||||
name = data[o:o + 8].rstrip(b"\x00").decode("ascii", errors="replace")
|
||||
virt_size = _u32(data, o + 8)
|
||||
virt_addr = _u32(data, o + 12)
|
||||
raw_size = _u32(data, o + 16)
|
||||
raw_off = _u32(data, o + 20)
|
||||
sections.append((name, virt_addr, virt_size, raw_off, raw_size))
|
||||
return image_base, sections
|
||||
|
||||
|
||||
def va_to_offset(va, sections, image_base):
|
||||
rva = va - image_base
|
||||
for name, va0, vs, ro, _ in sections:
|
||||
if va0 <= rva < va0 + vs:
|
||||
return ro + (rva - va0)
|
||||
raise ValueError(f"VA 0x{va:08x} not in any section")
|
||||
|
||||
|
||||
def offset_to_va(off, sections, image_base):
|
||||
for name, va0, _, ro, rs in sections:
|
||||
if ro <= off < ro + rs:
|
||||
return image_base + va0 + (off - ro)
|
||||
raise ValueError(f"File offset 0x{off:08x} not in any section")
|
||||
|
||||
|
||||
def find_cave(data, sections, image_base, size, search_from_va):
|
||||
"""Return VA of the first run of >= size consecutive CC bytes after search_from_va."""
|
||||
text = next((s for s in sections if s[0] == ".text"), None)
|
||||
if not text is None:
|
||||
_, va0, vs, ro, rs = text
|
||||
else:
|
||||
raise ValueError("No .text section")
|
||||
|
||||
rva_start = max(search_from_va - image_base, va0)
|
||||
off_start = ro + (rva_start - va0)
|
||||
off_end = ro + min(vs, rs)
|
||||
|
||||
i = off_start
|
||||
while i <= off_end - size:
|
||||
if data[i] != 0xcc:
|
||||
i += 1
|
||||
continue
|
||||
j = i
|
||||
while j < off_end and data[j] == 0xcc:
|
||||
j += 1
|
||||
if j - i >= size:
|
||||
return offset_to_va(i, sections, image_base)
|
||||
i = j
|
||||
raise ValueError(f"No {size}-byte code cave found from VA 0x{search_from_va:08x}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch 1 & 2: outfitId in CMSG_CHAR_CREATE (fixed addresses)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FIXED_PATCHES = [
|
||||
(
|
||||
0x004e0c60,
|
||||
bytes.fromhex(
|
||||
"558bec568b75086a0156e8f1d23600"
|
||||
"83c40885c0740d6a006a0156e860d4"
|
||||
"360083c40c6a006a0156e853d43600"
|
||||
"50e8edf6ffff83c41033c05e5dc3cc"
|
||||
"cccccccc"
|
||||
),
|
||||
bytes.fromhex(
|
||||
"558bec568b75086a0256e8c1d33600"
|
||||
"db1c245883c404a221 42ac006a006a"
|
||||
"0156e85bd4360050e8f5f6ffff83c4"
|
||||
"1033c05e5dc38855f08a1521 42ac00"
|
||||
"8855f4c3"
|
||||
).replace(b" ", b""), # spaces stripped for readability
|
||||
"Script_CreateCharacter: read outfitId from Lua arg 2 -> global",
|
||||
),
|
||||
(
|
||||
0x004e042f,
|
||||
bytes.fromhex("8855f0c645f400"),
|
||||
bytes.fromhex("e85f08000090 90").replace(b" ", b""),
|
||||
"Packet builder: CALL helper -> load face + outfitId from global",
|
||||
),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch 3: GetCharacterInfo — add 11th return value (charFlags as number)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Assembly context (FUN_004e3170 epilogue, valid branch):
|
||||
#
|
||||
# 004e31cd 56 PUSH ESI ← saves caller's ESI
|
||||
# 004e31ce 8b f0 MOV ESI, EAX ← ESI = char struct ptr
|
||||
# ... [function body — ESI used throughout for char struct] ...
|
||||
# 004e32e5 8b 8e 70010000 MOV ECX, [ESI+0x170] ← charFlags (ghost flag check)
|
||||
# ...
|
||||
# 004e332d 83 c4 2c ADD ESP, 0x2c ← clean accumulated args ← PATCH HERE
|
||||
# 004e3330 5e POP ESI ← restores caller's ESI (ESI valid until here)
|
||||
# 004e3331 b8 0a 00 00 00 MOV EAX, 0xa (dead after patch)
|
||||
# 004e3336 5f POP EDI ← JMP back target
|
||||
# 004e3337 8b e5 MOV ESP, EBP
|
||||
# 004e3339 5d POP EBP
|
||||
# 004e333a c3 RET
|
||||
#
|
||||
# KEY: ESI = char struct ptr up to and including ADD ESP,0x2c (004e332d).
|
||||
# After POP ESI (004e3330) it is caller's ESI — NOT char struct.
|
||||
# Previous approach: CALL cave at 004e3331 (after POP ESI) -> ESI already wrong.
|
||||
# Fix: JMP cave at 004e332d (before POP ESI) -> ESI still valid in cave.
|
||||
#
|
||||
# New approach: patch 5 bytes at 004e332d with JMP cave.
|
||||
# Old bytes: 83 c4 2c (ADD ESP,0x2c) + 5e (POP ESI) + b8 (MOV EAX first byte)
|
||||
# New bytes: e9 XX XX XX XX JMP cave
|
||||
# The 4 dead bytes at 004e3332-004e3335 are unreachable.
|
||||
#
|
||||
# Cave (30 bytes, found automatically via CC scan over entire .text):
|
||||
# 83 ec 08 SUB ESP, 8 ; make room for double
|
||||
# db 86 70 01 00 00 FILD [ESI+0x170] ; load charFlags into FPU (ESI valid here!)
|
||||
# dd 1c 24 FSTP [ESP] ; store as double
|
||||
# 57 PUSH EDI ; lua_State* L
|
||||
# e8 XX XX XX XX CALL FUN_0084e2a0 ; lua_pushnumber(L, charFlags)
|
||||
# 83 c4 38 ADD ESP, 0x38 ; merged: own 0xc + stolen 0x2c
|
||||
# 5e POP ESI ; stolen: restore caller's ESI
|
||||
# 6a 0b PUSH 0xb ; \ EAX = 11 in 3 bytes
|
||||
# 58 POP EAX ; / (vs MOV EAX,0xb = 5 bytes)
|
||||
# e9 XX XX XX XX JMP 004e3336 ; continue epilogue (POP EDI ...)
|
||||
#
|
||||
# Lua code reads info[11] and checks bit 0x40000000 (CHARACTER_FLAG_UNK31 = traitor).
|
||||
# charFlags confirmed at [ESI+0x170]: same field used for ghost flag (AND ECX,0x2000).
|
||||
|
||||
_CHARINFO_PATCH_VA = 0x004e332d # ADD ESP,0x2c (5 bytes including POP ESI + MOV EAX[0])
|
||||
_CHARINFO_JMPBACK_VA = 0x004e3336 # POP EDI — resume epilogue after cave
|
||||
_CHARINFO_CAVE_SIZE = 30
|
||||
_CHARINFO_CAVE_SEARCH_FROM = 0x00401000 # search entire .text for first 35-byte CC block
|
||||
_LUA_PUSHNUMBER_VA = 0x0084e2a0 # lua_pushnumber
|
||||
|
||||
|
||||
def _build_charinfo_patches(data, sections, image_base):
|
||||
"""
|
||||
Build the dynamic GetCharacterInfo patches.
|
||||
Returns list of (va, old_bytes, new_bytes, description), [] if already applied,
|
||||
or None on unrecoverable error.
|
||||
"""
|
||||
patch_off = va_to_offset(_CHARINFO_PATCH_VA, sections, image_base)
|
||||
current = bytes(data[patch_off:patch_off + 5])
|
||||
|
||||
# Already patched with new JMP approach?
|
||||
if current[0:1] == b"\xe9":
|
||||
return []
|
||||
|
||||
# Verify first 4 bytes look like the epilogue (ADD ESP,0x2c + POP ESI).
|
||||
# Accept both unpatched (b8 at [4]) and old-CALL-patched (e8 at [4]) states.
|
||||
if current[0:4] != bytes.fromhex("83c42c5e"):
|
||||
print(f"[WARN] 0x{_CHARINFO_PATCH_VA:08x}: unexpected bytes {current.hex(' ')}")
|
||||
print(" Expected ADD ESP,0x2c + POP ESI at this address.")
|
||||
print(" Restore Wow.exe.bak and re-run.")
|
||||
return None
|
||||
|
||||
old_b = current # exact 5 bytes currently in file (b8 or e8 at index 4)
|
||||
|
||||
# Find a 35-byte code cave in .text
|
||||
cave_va = find_cave(data, sections, image_base,
|
||||
_CHARINFO_CAVE_SIZE, _CHARINFO_CAVE_SEARCH_FROM)
|
||||
|
||||
# Cave layout offsets (30 bytes total):
|
||||
# 0 SUB ESP,8 (3)
|
||||
# 3 FILD [ESI+0x170] (6)
|
||||
# 9 FSTP [ESP] (3)
|
||||
# 12 PUSH EDI (1)
|
||||
# 13 CALL lua_pushnumber (5) — next instr at 18
|
||||
# 18 ADD ESP, 0x38 (3) merged: own 0xc + stolen 0x2c
|
||||
# 21 POP ESI (1) stolen
|
||||
# 22 PUSH 0xb (2) \ shorter than MOV EAX,0xb (5 bytes)
|
||||
# 24 POP EAX (1) /
|
||||
# 25 JMP 004e3336 (5) — next instr at cave_va+30
|
||||
call_rel = _LUA_PUSHNUMBER_VA - (cave_va + 18)
|
||||
jmp_back_rel = _CHARINFO_JMPBACK_VA - (cave_va + 30)
|
||||
|
||||
cave_bytes = (
|
||||
bytes.fromhex("83ec08") # SUB ESP, 8 (3)
|
||||
+ bytes.fromhex("db8670010000") # FILD [ESI+0x170] (6)
|
||||
+ bytes.fromhex("dd1c24") # FSTP [ESP] (3)
|
||||
+ bytes.fromhex("57") # PUSH EDI (1)
|
||||
+ b"\xe8" + struct.pack("<i", call_rel) # CALL lua_pushnumber (5)
|
||||
+ bytes.fromhex("83c438") # ADD ESP, 0x38 (0xc+0x2c) (3)
|
||||
+ bytes.fromhex("5e") # POP ESI [stolen] (1)
|
||||
+ bytes.fromhex("6a0b") # PUSH 0xb (2)
|
||||
+ bytes.fromhex("58") # POP EAX -> EAX=11 (1)
|
||||
+ b"\xe9" + struct.pack("<i", jmp_back_rel) # JMP 004e3336 (5)
|
||||
)
|
||||
assert len(cave_bytes) == _CHARINFO_CAVE_SIZE, \
|
||||
f"Cave size {len(cave_bytes)} != {_CHARINFO_CAVE_SIZE}"
|
||||
|
||||
# Patch: JMP cave (5 bytes at 004e332d)
|
||||
jmp_rel = cave_va - (_CHARINFO_PATCH_VA + 5)
|
||||
patch_new = b"\xe9" + struct.pack("<i", jmp_rel)
|
||||
|
||||
return [
|
||||
(
|
||||
cave_va,
|
||||
bytes([0xcc] * _CHARINFO_CAVE_SIZE),
|
||||
cave_bytes,
|
||||
f"GetCharacterInfo cave @ 0x{cave_va:08x}: push charFlags, stolen epilogue",
|
||||
),
|
||||
(
|
||||
_CHARINFO_PATCH_VA,
|
||||
old_b,
|
||||
patch_new,
|
||||
"GetCharacterInfo: JMP to cave before POP ESI (ESI=char struct), return 11",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: python {Path(sys.argv[0]).name} <path_to_Wow.exe>")
|
||||
sys.exit(1)
|
||||
|
||||
exe_path = Path(sys.argv[1])
|
||||
if not exe_path.exists():
|
||||
print(f"Error: file not found: {exe_path}")
|
||||
sys.exit(1)
|
||||
|
||||
data = bytearray(exe_path.read_bytes())
|
||||
image_base, sections = parse_pe_sections(data)
|
||||
|
||||
print(f"Image base: 0x{image_base:08x}")
|
||||
print(f"Sections: {len(sections)}")
|
||||
for n, va, vs, ro, rs in sections:
|
||||
print(f" {n:8s} VA=0x{va:08x} VSize=0x{vs:08x} Raw=0x{ro:08x} RSize=0x{rs:08x}")
|
||||
print()
|
||||
|
||||
charinfo_patches = _build_charinfo_patches(data, sections, image_base)
|
||||
if charinfo_patches is None:
|
||||
sys.exit(1)
|
||||
all_patches = list(_FIXED_PATCHES) + charinfo_patches
|
||||
|
||||
# Verify all first
|
||||
ok = True
|
||||
for va, old_b, new_b, desc in all_patches:
|
||||
off = va_to_offset(va, sections, image_base)
|
||||
cur = bytes(data[off:off + len(old_b)])
|
||||
if cur == new_b:
|
||||
print(f"[SKIP] 0x{va:08x}: already patched — {desc}")
|
||||
elif cur == old_b:
|
||||
print(f"[ OK ] 0x{va:08x}: verified — {desc}")
|
||||
else:
|
||||
print(f"[FAIL] 0x{va:08x}: {desc}")
|
||||
print(f" Expected: {old_b.hex(' ')}")
|
||||
print(f" Found: {cur.hex(' ')}")
|
||||
ok = False
|
||||
|
||||
if not ok:
|
||||
print("\nByte mismatch — aborting. Is this the correct Wow.exe (3.3.5a build 12340)?")
|
||||
sys.exit(1)
|
||||
|
||||
# Backup
|
||||
bak = exe_path.with_suffix(".exe.bak")
|
||||
if not bak.exists():
|
||||
shutil.copy2(exe_path, bak)
|
||||
print(f"\nBackup: {bak}")
|
||||
else:
|
||||
print(f"\nBackup already exists: {bak}")
|
||||
|
||||
# Apply
|
||||
applied = 0
|
||||
for va, old_b, new_b, desc in all_patches:
|
||||
off = va_to_offset(va, sections, image_base)
|
||||
cur = bytes(data[off:off + len(old_b)])
|
||||
if cur == new_b:
|
||||
continue
|
||||
data[off:off + len(old_b)] = new_b
|
||||
applied += 1
|
||||
print(f"[PATCH] 0x{va:08x}: {desc}")
|
||||
|
||||
if applied == 0:
|
||||
print("\nNothing to patch.")
|
||||
else:
|
||||
exe_path.write_bytes(data)
|
||||
print(f"\n{applied} patch(es) written to {exe_path}")
|
||||
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,939 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// MoonWell compatibility module for WarcraftXL.
|
||||
//
|
||||
// The stock 3.3.5a executable stays untouched on disk. These narrowly scoped
|
||||
// changes are verified and applied to the process image during WarcraftXL's
|
||||
// boot phase, before GlueXML is loaded.
|
||||
|
||||
#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;
|
||||
|
||||
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
|
||||
// for its client cache record. Encounter Journal already has the exact
|
||||
// CreatureDisplayInfo ID, so accept it as an optional second argument.
|
||||
constexpr uintptr_t kSetCreature = 0x00597960;
|
||||
constexpr uintptr_t kGetCurrentModelFrame = 0x004A81B0;
|
||||
constexpr uintptr_t kModelFrameTypeToken = 0x00C0E4D4;
|
||||
constexpr uintptr_t kApplyCreatureCacheRecord = 0x00597700;
|
||||
|
||||
using SetCreatureFn = int(__cdecl*)(void* state);
|
||||
using ApplyCreatureCacheRecordFn = void(__fastcall*)(void* frame, void* edx,
|
||||
const void* cacheRecord);
|
||||
SetCreatureFn g_nextSetCreature = nullptr;
|
||||
alignas(4) std::array<uint32_t, 10> g_encounterJournalCreatureRecord{};
|
||||
|
||||
void* GetCurrentModelFrame(void* state, uint32_t typeToken)
|
||||
{
|
||||
// FrameScript_GetObject takes the type token on the stack, but the
|
||||
// 3.3.5 client also expects lua_State in ESI (an internal calling
|
||||
// convention not expressible with a regular C function pointer).
|
||||
void* frame = nullptr;
|
||||
__asm
|
||||
{
|
||||
mov esi, state
|
||||
push typeToken
|
||||
mov eax, kGetCurrentModelFrame
|
||||
call eax
|
||||
add esp, 4
|
||||
mov frame, eax
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
int __cdecl SetCreatureDisplayInfoHook(void* state)
|
||||
{
|
||||
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0;
|
||||
if (!state || !wxl::game::script::IsNumber(state, 3))
|
||||
return result;
|
||||
|
||||
const double requestedDisplayInfo = wxl::game::script::ToNumber(state, 3);
|
||||
if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
|
||||
return result;
|
||||
|
||||
const auto displayInfo = static_cast<uint32_t>(requestedDisplayInfo);
|
||||
__try
|
||||
{
|
||||
const uint32_t typeToken = *reinterpret_cast<const uint32_t*>(kModelFrameTypeToken);
|
||||
if (!typeToken)
|
||||
return result;
|
||||
|
||||
void* frame = GetCurrentModelFrame(state, typeToken);
|
||||
if (!frame)
|
||||
return result;
|
||||
|
||||
// Both the initial loader and the later character-appearance
|
||||
// pass read displayInfo at +0x24. Keep this record alive and
|
||||
// attach it to the frame: character models use it asynchronously
|
||||
// to resolve CreatureDisplayInfoExtra, baked skin and equipment.
|
||||
g_encounterJournalCreatureRecord.fill(0);
|
||||
g_encounterJournalCreatureRecord[9] = displayInfo;
|
||||
*reinterpret_cast<const void**>(static_cast<uint8_t*>(frame) + 0x378) =
|
||||
g_encounterJournalCreatureRecord.data();
|
||||
reinterpret_cast<ApplyCreatureCacheRecordFn>(kApplyCreatureCacheRecord)(
|
||||
frame, nullptr, g_encounterJournalCreatureRecord.data());
|
||||
}
|
||||
__except (EXCEPTION_EXECUTE_HANDLER)
|
||||
{
|
||||
WLOG_ERROR("moonwell: SetCreature displayInfo bridge failed for %u", displayInfo);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void InstallEncounterJournalModelPreview()
|
||||
{
|
||||
if (!Hook("MoonWellSetCreatureDisplayInfo", kSetCreature,
|
||||
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
|
||||
{
|
||||
WLOG_ERROR("moonwell: encounter journal model hook installation failed");
|
||||
return;
|
||||
}
|
||||
WLOG_INFO("moonwell: encounter journal displayInfo model bridge installed");
|
||||
}
|
||||
|
||||
struct CameraTransition
|
||||
{
|
||||
float zoom = 1.0f;
|
||||
float verticalOffset = 0.0f;
|
||||
float startZoom = 1.0f;
|
||||
float startVerticalOffset = 0.0f;
|
||||
float targetZoom = 1.0f;
|
||||
float targetVerticalOffset = 0.0f;
|
||||
DWORD startedAt = 0;
|
||||
DWORD duration = 0;
|
||||
} g_characterCreateCamera;
|
||||
|
||||
void UpdateCharacterCreateCamera(DWORD now)
|
||||
{
|
||||
auto& camera = g_characterCreateCamera;
|
||||
if (!camera.duration)
|
||||
{
|
||||
camera.zoom = camera.targetZoom;
|
||||
camera.verticalOffset = camera.targetVerticalOffset;
|
||||
return;
|
||||
}
|
||||
|
||||
const DWORD elapsed = now - camera.startedAt;
|
||||
float t = static_cast<float>(elapsed) / static_cast<float>(camera.duration);
|
||||
if (t >= 1.0f)
|
||||
{
|
||||
camera.zoom = camera.targetZoom;
|
||||
camera.verticalOffset = camera.targetVerticalOffset;
|
||||
camera.duration = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
t = t * t * (3.0f - 2.0f * t);
|
||||
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
|
||||
camera.verticalOffset = camera.startVerticalOffset
|
||||
+ (camera.targetVerticalOffset - camera.startVerticalOffset) * t;
|
||||
}
|
||||
|
||||
int __cdecl SetCharacterCreateCamera(void* state)
|
||||
{
|
||||
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;
|
||||
|
||||
const DWORD now = GetTickCount();
|
||||
UpdateCharacterCreateCamera(now);
|
||||
auto& camera = g_characterCreateCamera;
|
||||
camera.startZoom = camera.zoom;
|
||||
camera.startVerticalOffset = camera.verticalOffset;
|
||||
camera.targetZoom = enabled && faceZoom > 1.0f ? faceZoom : 1.0f;
|
||||
camera.targetVerticalOffset = enabled ? faceVerticalOffset : 0.0f;
|
||||
camera.startedAt = now;
|
||||
camera.duration = static_cast<DWORD>(requestedDuration);
|
||||
UpdateCharacterCreateCamera(now);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void __fastcall CharacterCreateProjectionHook(void* self, void* edx, const void* projection)
|
||||
{
|
||||
if (!g_nextSetProjection)
|
||||
return;
|
||||
|
||||
UpdateCharacterCreateCamera(GetTickCount());
|
||||
const auto& camera = g_characterCreateCamera;
|
||||
if (projection && (camera.zoom != 1.0f || camera.verticalOffset != 0.0f))
|
||||
{
|
||||
const float* source = static_cast<const float*>(projection);
|
||||
if (source[11] > 0.5f)
|
||||
{
|
||||
float adjusted[16];
|
||||
std::memcpy(adjusted, source, sizeof(adjusted));
|
||||
adjusted[0] *= camera.zoom;
|
||||
adjusted[5] *= camera.zoom;
|
||||
adjusted[9] += camera.verticalOffset;
|
||||
g_nextSetProjection(self, edx, adjusted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
g_nextSetProjection(self, edx, projection);
|
||||
}
|
||||
|
||||
void InstallCharacterCreateCamera()
|
||||
{
|
||||
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 || !PatchMemory(slot, &replacement, sizeof(replacement)))
|
||||
{
|
||||
g_nextSetProjection = nullptr;
|
||||
WLOG_ERROR("moonwell: character-create projection hook installation failed");
|
||||
return;
|
||||
}
|
||||
WLOG_INFO("moonwell: smooth character-create camera installed");
|
||||
}
|
||||
|
||||
int __cdecl SetLoginCharacterFlags(void* state)
|
||||
{
|
||||
uint32_t flags = 0;
|
||||
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::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __cdecl IsTraitor(void* state)
|
||||
{
|
||||
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Patch
|
||||
{
|
||||
const char* name;
|
||||
uintptr_t address;
|
||||
const uint8_t* expected;
|
||||
const uint8_t* replacement;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
template <size_t N>
|
||||
bool Apply(const char* name, uintptr_t address,
|
||||
const std::array<uint8_t, N>& expected,
|
||||
const std::array<uint8_t, N>& replacement)
|
||||
{
|
||||
const auto* current = reinterpret_cast<const uint8_t*>(address);
|
||||
if (std::memcmp(current, replacement.data(), N) == 0)
|
||||
return true;
|
||||
if (std::memcmp(current, expected.data(), N) != 0)
|
||||
{
|
||||
WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required",
|
||||
name, reinterpret_cast<void*>(address));
|
||||
return false;
|
||||
}
|
||||
if (!PatchMemory(reinterpret_cast<void*>(address), replacement.data(), N))
|
||||
{
|
||||
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
|
||||
reinterpret_cast<void*>(address));
|
||||
return false;
|
||||
}
|
||||
WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast<void*>(address));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InstallGlueUnlock()
|
||||
{
|
||||
// Same stock-client checks formerly changed in the distributed Wow.exe.
|
||||
// They allow custom GlueXML while keeping WarcraftXL's callback validator intact.
|
||||
bool ok = true;
|
||||
ok &= Apply("glue archive override", 0x005F4DBF,
|
||||
std::array<uint8_t, 1>{0x74}, std::array<uint8_t, 1>{0xEB});
|
||||
ok &= Apply("glue callback gate", 0x00816625,
|
||||
std::array<uint8_t, 1>{0x75}, std::array<uint8_t, 1>{0xEB});
|
||||
ok &= Apply("glue callback result A", 0x0081663F,
|
||||
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
|
||||
ok &= Apply("glue callback result B", 0x00816695,
|
||||
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
|
||||
ok &= Apply("glue callback compare", 0x00816746,
|
||||
std::array<uint8_t, 1>{0x7F}, std::array<uint8_t, 1>{0xEB});
|
||||
ok &= Apply("glue callback return", 0x0081675F,
|
||||
std::array<uint8_t, 7>{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D},
|
||||
std::array<uint8_t, 7>{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED});
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool InstallCharacterModePacket()
|
||||
{
|
||||
// CreateCharacter(name, modeId): capture Lua argument 2 and place it in
|
||||
// the final CMSG_CHAR_CREATE byte that is zero in the stock client.
|
||||
constexpr std::array<uint8_t, 64> expectedCreate = {
|
||||
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00,
|
||||
0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4,
|
||||
0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00,
|
||||
0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3,
|
||||
0xCC,0xCC,0xCC,0xCC,0xCC
|
||||
};
|
||||
constexpr std::array<uint8_t, 64> replacementCreate = {
|
||||
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00,
|
||||
0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A,
|
||||
0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4,
|
||||
0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC,
|
||||
0x00,0x88,0x55,0xF4,0xC3
|
||||
};
|
||||
constexpr std::array<uint8_t, 7> expectedPacket =
|
||||
{0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00};
|
||||
constexpr std::array<uint8_t, 7> replacementPacket =
|
||||
{0xE8,0x5F,0x08,0x00,0x00,0x90,0x90};
|
||||
|
||||
return Apply("CreateCharacter mode argument", 0x004E0C60,
|
||||
expectedCreate, replacementCreate)
|
||||
&& Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F,
|
||||
expectedPacket, replacementPacket);
|
||||
}
|
||||
|
||||
bool InstallCharacterFlags()
|
||||
{
|
||||
// GetCharacterInfo(index) gains return value 11: the character flags
|
||||
// from CharacterInfo+0x170. The glue code uses bit 0x40000000.
|
||||
constexpr uintptr_t patchAddress = 0x004E332D;
|
||||
constexpr uintptr_t returnAddress = 0x004E3336;
|
||||
constexpr uintptr_t luaPushNumber = 0x0084E2A0;
|
||||
constexpr std::array<uint8_t, 5> expected = {0x83,0xC4,0x2C,0x5E,0xB8};
|
||||
|
||||
if (*reinterpret_cast<const uint8_t*>(patchAddress) == 0xE9)
|
||||
return true;
|
||||
if (std::memcmp(reinterpret_cast<const void*>(patchAddress), expected.data(), expected.size()) != 0)
|
||||
{
|
||||
WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p",
|
||||
reinterpret_cast<void*>(patchAddress));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* cave = static_cast<uint8_t*>(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE,
|
||||
PAGE_EXECUTE_READWRITE));
|
||||
if (!cave)
|
||||
{
|
||||
WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t prefix[] = {
|
||||
0x83,0xEC,0x08, // sub esp, 8
|
||||
0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h]
|
||||
0xDD,0x1C,0x24, // fstp qword ptr [esp]
|
||||
0x57, // push edi (lua_State*)
|
||||
0xE8,0,0,0,0, // call lua_pushnumber
|
||||
0x83,0xC4,0x38, // cleanup own args + stolen add esp,2Ch
|
||||
0x5E, // stolen pop esi
|
||||
0x6A,0x0B,0x58, // eax = 11
|
||||
0xE9,0,0,0,0 // jump to stock epilogue
|
||||
};
|
||||
static_assert(sizeof(prefix) == 30);
|
||||
std::memcpy(cave, prefix, sizeof(prefix));
|
||||
|
||||
const auto callRel = static_cast<int32_t>(luaPushNumber -
|
||||
(reinterpret_cast<uintptr_t>(cave) + 18));
|
||||
const auto backRel = static_cast<int32_t>(returnAddress -
|
||||
(reinterpret_cast<uintptr_t>(cave) + 30));
|
||||
std::memcpy(cave + 14, &callRel, sizeof(callRel));
|
||||
std::memcpy(cave + 26, &backRel, sizeof(backRel));
|
||||
FlushInstructionCache(GetCurrentProcess(), cave, 30);
|
||||
|
||||
std::array<uint8_t, 5> jump{0xE9,0,0,0,0};
|
||||
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
|
||||
(patchAddress + jump.size()));
|
||||
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
|
||||
if (!PatchMemory(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
|
||||
{
|
||||
VirtualFree(cave, 0, MEM_RELEASE);
|
||||
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
|
||||
return false;
|
||||
}
|
||||
WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11");
|
||||
return true;
|
||||
}
|
||||
|
||||
void InstallBoot()
|
||||
{
|
||||
const bool glue = InstallGlueUnlock();
|
||||
const bool create = InstallCharacterModePacket();
|
||||
const bool flags = InstallCharacterFlags();
|
||||
if (!glue || !create || !flags)
|
||||
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
|
||||
else
|
||||
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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 RegisterLuaFunctionsForCurrentState();
|
||||
|
||||
void __cdecl ValidateCallbackHook(uintptr_t callback)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
|
||||
g_nextValidateCallback(callback);
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions(void* state)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (!clientWindow)
|
||||
{
|
||||
clientWindow = FindWindowW(L"GxWindowClass", nullptr);
|
||||
DWORD ownerProcessId = 0;
|
||||
if (clientWindow)
|
||||
GetWindowThreadProcessId(clientWindow, &ownerProcessId);
|
||||
if (ownerProcessId != GetCurrentProcessId())
|
||||
clientWindow = nullptr;
|
||||
if (!clientWindow)
|
||||
{
|
||||
WindowSearch search{GetCurrentProcessId(), nullptr};
|
||||
EnumWindows(&FindClientWindow, reinterpret_cast<LPARAM>(&search));
|
||||
clientWindow = search.window;
|
||||
}
|
||||
}
|
||||
if (clientWindow)
|
||||
{
|
||||
WLOG_INFO("moonwell: client window %p found for Lua bootstrap", clientWindow);
|
||||
// BeginServerLogin only snapshots the supplied credentials and starts
|
||||
// the client's asynchronous login state machine. Waiting here keeps the
|
||||
// call clear of engine initialization and Glue archive mounting.
|
||||
for (unsigned attempt = 0;
|
||||
attempt < 80 && g_hasLauncherAuth;
|
||||
++attempt)
|
||||
{
|
||||
Sleep(250);
|
||||
SubmitLauncherAuthorization();
|
||||
}
|
||||
return g_hasLauncherAuth ? 1 : 0;
|
||||
}
|
||||
Sleep(100);
|
||||
}
|
||||
WLOG_ERROR("moonwell: timed out waiting for UI-thread Lua bootstrap");
|
||||
return 1;
|
||||
}
|
||||
|
||||
void __cdecl ExecuteHook(const char* source, uintptr_t argument2, uintptr_t argument3)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_nextExecute) g_nextExecute(source, argument2, argument3);
|
||||
}
|
||||
|
||||
int __cdecl InitializeLuaHook(void* allocatorContext)
|
||||
{
|
||||
const int initialized = g_nextInitializeLua
|
||||
? g_nextInitializeLua(allocatorContext)
|
||||
: 0;
|
||||
if (initialized)
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
return initialized;
|
||||
}
|
||||
|
||||
void __cdecl FramePumpHook(float deltaSeconds, uint32_t frameTimeMs)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_nextFramePump) g_nextFramePump(deltaSeconds, frameTimeMs);
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_hasLauncherAuth
|
||||
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
|
||||
SubmitLauncherAuthorization();
|
||||
}
|
||||
|
||||
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
|
||||
{
|
||||
// The engine-init callback and synchronous Glue loader run on the same client
|
||||
// thread. Polling here catches Lua becoming live before AccountLogin executes,
|
||||
// without ever touching the state from background asset I/O workers.
|
||||
if (GetCurrentThreadId() == g_clientThreadId)
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
const int result = g_nextFileOpen ? g_nextFileOpen(archive, name, flags, out) : 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
void __cdecl GlueModelRenderHook(void* frame)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
if (g_nextGlueModelRender) g_nextGlueModelRender(frame);
|
||||
}
|
||||
|
||||
void __cdecl OnFrame(void*, const void*)
|
||||
{
|
||||
RegisterLuaFunctionsForCurrentState();
|
||||
// Glue scripting is not a reliable authentication trigger: a broken
|
||||
// cosmetic widget can abort AccountLogin_OnShow before it reaches the
|
||||
// login call. Present is emitted on the client thread after the engine
|
||||
// and Glue subsystem are ready, so submit once after a short grace period.
|
||||
if (g_hasLauncherAuth
|
||||
&& GetTickCount() - g_launcherAuthCapturedAt >= 3000)
|
||||
SubmitLauncherAuthorization();
|
||||
}
|
||||
|
||||
bool InstallLuaBridge()
|
||||
{
|
||||
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
|
||||
&g_nextValidateCallback);
|
||||
const bool executor = HookByName("Lua.Execute", &ExecuteHook, &g_nextExecute);
|
||||
const bool initializer = Hook("Lua.Initialize", 0x00819BB0,
|
||||
&InitializeLuaHook, &g_nextInitializeLua);
|
||||
const bool defaultLogin = Hook("MoonWell.DefaultServerLogin", 0x004DC260,
|
||||
&DefaultServerLoginHook, &g_nextDefaultServerLogin);
|
||||
const bool framePump = HookByName("Frame.Pump", &FramePumpHook, &g_nextFramePump);
|
||||
const bool fileOpen = HookByName("Io.FileOpen", &FileOpenHook, &g_nextFileOpen);
|
||||
const bool glueRender = HookByName("Gx.GlueModelRender", &GlueModelRenderHook,
|
||||
&g_nextGlueModelRender);
|
||||
if (!validator || !executor || !initializer || !defaultLogin
|
||||
|| !framePump || !fileOpen || !glueRender)
|
||||
WLOG_ERROR("moonwell: Lua bridge hook installation failed");
|
||||
return validator && executor && initializer && defaultLogin
|
||||
&& framePump && fileOpen && glueRender;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Appends compact MoonWell spell overrides to stock 3.3.5 DBC bytes.
|
||||
|
||||
#include "game/Io.hpp"
|
||||
#include "wxl/PluginApi.h"
|
||||
#include "wxl/StorageApi.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::spells
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view kOverridesPath = "WXLSpellOverrides.tsv";
|
||||
constexpr std::string_view kSpellPath = "DBFilesClient\\Spell.dbc";
|
||||
constexpr std::string_view kSkillPath = "DBFilesClient\\SkillLineAbility.dbc";
|
||||
constexpr uint32_t kSpellFields = 234;
|
||||
constexpr uint32_t kSpellRecordSize = kSpellFields * 4;
|
||||
constexpr uint32_t kSkillFields = 14;
|
||||
constexpr uint32_t kSkillRecordSize = kSkillFields * 4;
|
||||
|
||||
struct Override
|
||||
{
|
||||
uint32_t spellId = 0;
|
||||
uint32_t templateSpellId = 0;
|
||||
uint32_t creatureEntry = 0;
|
||||
uint32_t skillLineAbilityId = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
bool g_ready = false;
|
||||
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)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
std::memcpy(&value, data, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
void WriteU32(uint8_t* data, uint32_t value)
|
||||
{
|
||||
std::memcpy(data, &value, sizeof(value));
|
||||
}
|
||||
|
||||
std::string_view Trim(std::string_view value)
|
||||
{
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front())))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back())))
|
||||
value.remove_suffix(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool ParseU32(std::string_view text, uint32_t& value)
|
||||
{
|
||||
text = Trim(text);
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size();
|
||||
}
|
||||
|
||||
bool SamePath(std::string_view left, std::string_view right)
|
||||
{
|
||||
if (left.size() != right.size()) return false;
|
||||
for (size_t i = 0; i < left.size(); ++i)
|
||||
{
|
||||
const unsigned char a = static_cast<unsigned char>(left[i] == '/' ? '\\' : left[i]);
|
||||
const unsigned char b = static_cast<unsigned char>(right[i] == '/' ? '\\' : right[i]);
|
||||
if (std::tolower(a) != std::tolower(b)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseOverrides(const std::vector<uint8_t>& bytes, std::vector<Override>& out)
|
||||
{
|
||||
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;
|
||||
|
||||
std::string_view fields[5];
|
||||
size_t fieldStart = 0;
|
||||
bool valid = true;
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
{
|
||||
const size_t tab = line.find('\t', fieldStart);
|
||||
if (tab == std::string_view::npos) { valid = false; break; }
|
||||
fields[i] = line.substr(fieldStart, tab - fieldStart);
|
||||
fieldStart = tab + 1;
|
||||
}
|
||||
if (!valid) return false;
|
||||
fields[4] = Trim(line.substr(fieldStart));
|
||||
|
||||
Override item;
|
||||
if (!ParseU32(fields[0], item.spellId) ||
|
||||
!ParseU32(fields[1], item.templateSpellId) ||
|
||||
!ParseU32(fields[2], item.creatureEntry) ||
|
||||
!ParseU32(fields[3], item.skillLineAbilityId) || fields[4].empty())
|
||||
return false;
|
||||
item.name.assign(fields[4]);
|
||||
out.push_back(std::move(item));
|
||||
}
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
bool ValidateWdbc(const std::vector<uint8_t>& bytes, uint32_t fields, uint32_t recordSize,
|
||||
uint32_t& count, uint32_t& stringSize, size_t& recordsEnd)
|
||||
{
|
||||
if (bytes.size() < 20 || std::memcmp(bytes.data(), "WDBC", 4) != 0) return false;
|
||||
count = ReadU32(bytes.data() + 4);
|
||||
if (ReadU32(bytes.data() + 8) != fields || ReadU32(bytes.data() + 12) != recordSize)
|
||||
return false;
|
||||
stringSize = ReadU32(bytes.data() + 16);
|
||||
recordsEnd = 20ull + static_cast<size_t>(count) * recordSize;
|
||||
return recordsEnd <= bytes.size() && recordsEnd + stringSize == bytes.size();
|
||||
}
|
||||
|
||||
const uint8_t* FindSpell(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t id)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSpellRecordSize;
|
||||
if (ReadU32(row) == id) return row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* FindSkillRow(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t spellId)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSkillRecordSize;
|
||||
if (ReadU32(row + 4) == 778 && ReadU32(row + 8) == spellId) return row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool BuildSpellDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
uint32_t count = 0, stringSize = 0;
|
||||
size_t recordsEnd = 0;
|
||||
if (!ValidateWdbc(base, kSpellFields, kSpellRecordSize, count, stringSize, recordsEnd))
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> records;
|
||||
records.reserve(overrides.size() * kSpellRecordSize);
|
||||
std::vector<uint8_t> strings;
|
||||
for (const Override& item : overrides)
|
||||
{
|
||||
const uint8_t* source = FindSpell(base, count, item.templateSpellId);
|
||||
if (!source) return false;
|
||||
const size_t recordStart = records.size();
|
||||
records.insert(records.end(), source, source + kSpellRecordSize);
|
||||
uint8_t* row = records.data() + recordStart;
|
||||
WriteU32(row, item.spellId);
|
||||
WriteU32(row + 110 * 4, item.creatureEntry);
|
||||
|
||||
const uint32_t nameOffset = stringSize + static_cast<uint32_t>(strings.size());
|
||||
strings.insert(strings.end(), item.name.begin(), item.name.end());
|
||||
strings.push_back(0);
|
||||
const std::string description = "Призывает или отпускает спутника: " + item.name + ".";
|
||||
const uint32_t descriptionOffset = stringSize + static_cast<uint32_t>(strings.size());
|
||||
strings.insert(strings.end(), description.begin(), description.end());
|
||||
strings.push_back(0);
|
||||
for (uint32_t field = 136; field <= 151; ++field) WriteU32(row + field * 4, nameOffset);
|
||||
for (uint32_t field = 170; field <= 185; ++field) WriteU32(row + field * 4, descriptionOffset);
|
||||
for (uint32_t field = 187; field <= 202; ++field) WriteU32(row + field * 4, 0);
|
||||
}
|
||||
|
||||
out.reserve(base.size() + records.size() + strings.size());
|
||||
out.insert(out.end(), base.begin(), base.begin() + 20);
|
||||
WriteU32(out.data() + 4, count + static_cast<uint32_t>(overrides.size()));
|
||||
WriteU32(out.data() + 16, stringSize + static_cast<uint32_t>(strings.size()));
|
||||
out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd);
|
||||
out.insert(out.end(), records.begin(), records.end());
|
||||
out.insert(out.end(), base.begin() + recordsEnd, base.end());
|
||||
out.insert(out.end(), strings.begin(), strings.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildSkillDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
uint32_t count = 0, stringSize = 0;
|
||||
size_t recordsEnd = 0;
|
||||
if (!ValidateWdbc(base, kSkillFields, kSkillRecordSize, count, stringSize, recordsEnd))
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> records;
|
||||
records.reserve(overrides.size() * kSkillRecordSize);
|
||||
for (const Override& item : overrides)
|
||||
{
|
||||
const uint8_t* source = FindSkillRow(base, count, item.templateSpellId);
|
||||
if (!source) return false;
|
||||
const size_t recordStart = records.size();
|
||||
records.insert(records.end(), source, source + kSkillRecordSize);
|
||||
uint8_t* row = records.data() + recordStart;
|
||||
WriteU32(row, item.skillLineAbilityId);
|
||||
WriteU32(row + 8, item.spellId);
|
||||
}
|
||||
|
||||
out.reserve(base.size() + records.size());
|
||||
out.insert(out.end(), base.begin(), base.begin() + 20);
|
||||
WriteU32(out.data() + 4, count + static_cast<uint32_t>(overrides.size()));
|
||||
out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd);
|
||||
out.insert(out.end(), records.begin(), records.end());
|
||||
out.insert(out.end(), base.begin() + recordsEnd, base.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
std::vector<uint8_t> overrideBytes;
|
||||
if (!ReadAll(kOverridesPath.data(), overrideBytes))
|
||||
return;
|
||||
|
||||
if (!ParseOverrides(overrideBytes, g_overrides))
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "failed to parse WXLSpellOverrides.tsv");
|
||||
return;
|
||||
}
|
||||
g_ready = true;
|
||||
if (g_api && g_api->Log)
|
||||
g_api->Log(WXL_LOG_INFO, "MoonWellSpells", "loaded %zu companion spell override(s)",
|
||||
g_overrides.size());
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const std::vector<uint8_t> base(raw, raw + rawLen);
|
||||
std::vector<uint8_t> out;
|
||||
const bool built = SamePath(name, kSpellPath)
|
||||
? BuildSpellDbc(base, g_overrides, out)
|
||||
: BuildSkillDbc(base, g_overrides, out);
|
||||
if (!built)
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "failed to build custom companion DBC");
|
||||
return 0;
|
||||
}
|
||||
sink->Write(sink->ctx, out.data(), static_cast<uint32_t>(out.size()));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool Install(const WXL_Api* api)
|
||||
{
|
||||
g_api = api;
|
||||
if (!api || !api->GetInterface) return false;
|
||||
auto* storage = static_cast<WXL_StorageApi*>(
|
||||
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
|
||||
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
|
||||
!storage->RegisterClientTransform)
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "wxl.storage v1 is unavailable");
|
||||
return false;
|
||||
}
|
||||
storage->RegisterClientTransform(".dbc", &Transform);
|
||||
Log(WXL_LOG_INFO, "DBC override transform registered");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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,8 +1,16 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateSet("production", "ptr", "local")]
|
||||
[string]$Env = "local",
|
||||
[switch]$DeveloperLogin
|
||||
)
|
||||
|
||||
# Stop on errors
|
||||
$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) {
|
||||
@@ -31,11 +39,90 @@ if (-not $env:WOW_HOME) {
|
||||
$WOW_HOME = $env:WOW_HOME
|
||||
Write-Host "WOW_HOME = $WOW_HOME"
|
||||
|
||||
function Test-ExistingFileWriteAccess {
|
||||
param([Parameter(Mandatory=$true)][string]$Path)
|
||||
|
||||
try {
|
||||
$stream = [System.IO.File]::Open(
|
||||
$Path,
|
||||
[System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::ReadWrite
|
||||
)
|
||||
$stream.Close()
|
||||
return $true
|
||||
} catch [System.UnauthorizedAccessException] {
|
||||
return $false
|
||||
} catch {
|
||||
# Sharing violations are handled later by Stop-WowRuntime. They do not
|
||||
# indicate that the whole script needs an elevated token.
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
$installedWarcraftXL = Join-Path $WOW_HOME "WarcraftXL.dll"
|
||||
$isAdministrator = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||
[Security.Principal.WindowsBuiltInRole]::Administrator
|
||||
)
|
||||
|
||||
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 $elevationArguments
|
||||
exit $elevatedProcess.ExitCode
|
||||
}
|
||||
|
||||
# --- Paths
|
||||
$SRC_DIR = Join-Path $ROOT "src"
|
||||
$DIST_DIR = Join-Path $ROOT "dist"
|
||||
$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" -ErrorAction SilentlyContinue | Where-Object {
|
||||
try {
|
||||
$_.Path -and $_.Path.StartsWith($clientRoot, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
$false
|
||||
}
|
||||
})
|
||||
|
||||
if (-not $runtimeProcesses.Count) {
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Stopping the running WoW client..."
|
||||
$runtimeProcesses | Stop-Process -Force
|
||||
|
||||
try {
|
||||
$runtimeProcesses | Wait-Process -Timeout 10 -ErrorAction Stop
|
||||
} catch {
|
||||
$stillRunning = @($runtimeProcesses | Where-Object { -not $_.HasExited })
|
||||
if ($stillRunning.Count) {
|
||||
$processNames = ($stillRunning | ForEach-Object { "$($_.ProcessName) (PID $($_.Id))" }) -join ", "
|
||||
throw "Could not stop processes that lock WarcraftXL files: $processNames"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Ensure tool exists
|
||||
if (!(Test-Path $TOOL)) {
|
||||
@@ -45,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) {
|
||||
@@ -59,8 +153,36 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files
|
||||
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
|
||||
Stop-WowRuntime -ClientPath $WOW_HOME
|
||||
Write-Host "Building and packaging WarcraftXL..."
|
||||
$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!"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- 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) {
|
||||
@@ -70,6 +192,28 @@ if ($LASTEXITCODE -ge 8) {
|
||||
|
||||
Write-Host "MPQ archives deployed to $WOW_HOME"
|
||||
|
||||
# --- Write realmlist.wtf based on selected environment
|
||||
$realmlist = switch ($Env) {
|
||||
"production" { $env:PRODUCTION_REALMLIST }
|
||||
"ptr" { $env:PTR_REALMLIST }
|
||||
"local" { $env:LOCAL_REALMLIST }
|
||||
}
|
||||
|
||||
if ($realmlist) {
|
||||
$realmlistPath = Join-Path $WOW_HOME "Data\ruRU\realmlist.wtf"
|
||||
Set-Content -Path $realmlistPath -Value "set realmlist $realmlist" -Encoding ASCII
|
||||
Write-Host "Realmlist ($Env): $realmlist"
|
||||
} else {
|
||||
Write-Warning "REALMLIST for '$Env' is not set in .env -- skipping realmlist.wtf"
|
||||
}
|
||||
|
||||
# --- 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.
@@ -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)
|
||||
@@ -474,7 +610,7 @@ function AccountLogin_OnKeyDown(key)
|
||||
elseif 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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -23,7 +23,7 @@ FACTION_BACKDROP_COLOR_TABLE = {
|
||||
["Alliance"] = {0.5, 0.5, 0.5, 0.09, 0.09, 0.19},
|
||||
["Horde"] = {0.5, 0.2, 0.2, 0.19, 0.05, 0.05},
|
||||
};
|
||||
FRAMES_TO_BACKDROP_COLOR = {
|
||||
FRAMES_TO_BACKDROP_COLOR = {
|
||||
"CharacterCreateCharacterRace",
|
||||
"CharacterCreateCharacterClass",
|
||||
-- "CharacterCreateCharacterFaction",
|
||||
@@ -36,11 +36,11 @@ RACE_ICON_TCOORDS = {
|
||||
["NIGHTELF_MALE"] = {0.126953125, 0.189453125, 0.000000000, 0.125953125},
|
||||
["DRAENEI_MALE"] = {0.255859375, 0.318359375, 0.000000000, 0.125953125},
|
||||
|
||||
["HUMAN_FEMALE"] = {0.000000000, 0.062500000, 0.128906250, 0.253906250},
|
||||
["DWARF_FEMALE"] = {0.063476563, 0.125976563, 0.128906250, 0.253906250},
|
||||
["GNOME_FEMALE"] = {0.192382813, 0.254882813, 0.128906250, 0.253906250},
|
||||
["NIGHTELF_FEMALE"] = {0.127441406, 0.189941406, 0.128906250, 0.253906250},
|
||||
["DRAENEI_FEMALE"] = {0.256347656, 0.318359375, 0.128906250, 0.253906250},
|
||||
["HUMAN_FEMALE"] = {0.000000000, 0.062500000, 0.128906250, 0.253906250},
|
||||
["DWARF_FEMALE"] = {0.063476563, 0.125976563, 0.128906250, 0.253906250},
|
||||
["GNOME_FEMALE"] = {0.192382813, 0.254882813, 0.128906250, 0.253906250},
|
||||
["NIGHTELF_FEMALE"] = {0.127441406, 0.189941406, 0.128906250, 0.253906250},
|
||||
["DRAENEI_FEMALE"] = {0.256347656, 0.318359375, 0.128906250, 0.253906250},
|
||||
|
||||
["ORC_MALE"] = {0.062011719, 0.000000000, 0.262695313, 0.384765625},
|
||||
["SCOURGE_MALE"] = {0.125976563, 0.063964844, 0.262695313, 0.384765625},
|
||||
@@ -48,10 +48,10 @@ RACE_ICON_TCOORDS = {
|
||||
["TROLL_MALE"] = {0.253906250, 0.192382813, 0.262695313, 0.384765625},
|
||||
["BLOODELF_MALE"] = {0.317871094, 0.256835938, 0.262695313, 0.384765625},
|
||||
|
||||
["ORC_FEMALE"] = {0.060546875, 0.000000000, 0.391601563, 0.512695313},
|
||||
["SCOURGE_FEMALE"] = {0.124023438, 0.063476563, 0.391601563, 0.512695313},
|
||||
["TAUREN_FEMALE"] = {0.187988281, 0.127441406, 0.391601563, 0.512695313},
|
||||
["TROLL_FEMALE"] = {0.252441406, 0.191894531, 0.391601563, 0.512695313},
|
||||
["ORC_FEMALE"] = {0.060546875, 0.000000000, 0.391601563, 0.512695313},
|
||||
["SCOURGE_FEMALE"] = {0.124023438, 0.063476563, 0.391601563, 0.512695313},
|
||||
["TAUREN_FEMALE"] = {0.187988281, 0.127441406, 0.391601563, 0.512695313},
|
||||
["TROLL_FEMALE"] = {0.252441406, 0.191894531, 0.391601563, 0.512695313},
|
||||
["BLOODELF_FEMALE"] = {0.316894531, 0.256835938, 0.391601563, 0.512695313},
|
||||
};
|
||||
CLASS_ICON_TCOORDS = {
|
||||
@@ -137,23 +137,23 @@ local function HideAllTooltips()
|
||||
if AllianceTooltip then
|
||||
AllianceTooltip:Hide()
|
||||
end
|
||||
|
||||
|
||||
if HordeTooltip then
|
||||
HordeTooltip:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local backdrop = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
insets = { left = 4, right = 4, top = 4, bottom = 4 }
|
||||
};
|
||||
|
||||
local Backdrop2 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\ui-tooltip-border-maw",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\ui-tooltip-border-maw",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
insets = { left = 4, right = 4, top = 4, bottom = 4 }
|
||||
};
|
||||
|
||||
@@ -164,10 +164,10 @@ local function GetOrCreateRaceTooltip(button)
|
||||
tooltip:SetSize(280, 150)
|
||||
tooltip:SetFrameStrata("TOOLTIP")
|
||||
tooltip:SetBackdropColor(0, 0, 0, 1)
|
||||
|
||||
|
||||
local raceID = button:GetID()
|
||||
local faction = _G.GetFactionForRaceID(raceID)
|
||||
|
||||
|
||||
if faction == "Alliance" then
|
||||
tooltip:SetPoint("LEFT", CharacterCreateFrame, "LEFT", 160, 50)
|
||||
elseif faction == "Horde" then
|
||||
@@ -194,7 +194,7 @@ local function GetOrCreateRaceTooltip(button)
|
||||
tooltip.rightClickText:SetPoint("TOP", tooltip.detailsText, "BOTTOM", 0, -10)
|
||||
tooltip.rightClickText:SetTextColor(0.8, 0.8, 0.8)
|
||||
tooltip.rightClickText:SetJustifyH("CENTER")
|
||||
|
||||
|
||||
raceTooltips[button] = tooltip
|
||||
end
|
||||
return raceTooltips[button]
|
||||
@@ -217,7 +217,7 @@ local function UpdateRaceTooltip(button, toggleDetails)
|
||||
|
||||
tooltip.text:ClearAllPoints()
|
||||
tooltip.text:SetPoint("TOPLEFT", tooltip, "TOPLEFT", 10, -5)
|
||||
|
||||
|
||||
tooltip.detailsText:ClearAllPoints()
|
||||
tooltip.detailsText:SetPoint("TOPLEFT", tooltip.text, "BOTTOMLEFT", 0, -5)
|
||||
|
||||
@@ -271,7 +271,7 @@ local function UpdateRaceTooltip(button, toggleDetails)
|
||||
|
||||
local containerHeight = tooltip["spellIcon"..i]:GetHeight() + tooltip["spellDesc"..i]:GetHeight() + 3
|
||||
tooltip["spellContainer"..i]:SetHeight(containerHeight)
|
||||
|
||||
|
||||
lastElement = tooltip["spellContainer"..i]
|
||||
end
|
||||
end
|
||||
@@ -308,10 +308,10 @@ local function UpdateRaceTooltip(button, toggleDetails)
|
||||
end
|
||||
end
|
||||
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
tooltip.rightClickText:GetHeight() + 25
|
||||
|
||||
|
||||
tooltip:SetHeight(baseHeight + heightToAdd)
|
||||
detailedRaceTooltips[button] = toggleDetails
|
||||
tooltip:ClearAllPoints()
|
||||
@@ -346,7 +346,7 @@ local function GetOrCreateClassTooltip(button)
|
||||
tooltip.Roles:SetWidth(284)
|
||||
tooltip.Roles:SetWordWrap(true)
|
||||
tooltip.Roles:SetJustifyH("LEFT")
|
||||
|
||||
|
||||
classTooltips[button] = tooltip
|
||||
end
|
||||
|
||||
@@ -370,7 +370,7 @@ local function GetValidRacesForClass(classID)
|
||||
table.insert(hordeRaces, raceName)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
return allianceRaces, hordeRaces
|
||||
end
|
||||
local function UpdateClassTooltip(button)
|
||||
@@ -382,7 +382,7 @@ local function UpdateClassTooltip(button)
|
||||
|
||||
local buttonX, buttonY = button:GetCenter()
|
||||
local parentX, parentY = CharacterCreateFrame:GetCenter()
|
||||
|
||||
|
||||
if buttonX > parentX then
|
||||
tooltip:SetPoint("BOTTOMRIGHT", button, "TOPRIGHT", 0, 10)
|
||||
else
|
||||
@@ -394,7 +394,7 @@ local function UpdateClassTooltip(button)
|
||||
|
||||
tooltip.text:ClearAllPoints()
|
||||
tooltip.text:SetPoint("TOPLEFT", tooltip, "TOPLEFT", 8, -8)
|
||||
|
||||
|
||||
tooltip.detailsText:ClearAllPoints()
|
||||
tooltip.detailsText:SetPoint("TOPLEFT", tooltip.text, "BOTTOMLEFT", 0, -5)
|
||||
|
||||
@@ -405,7 +405,7 @@ local function UpdateClassTooltip(button)
|
||||
tooltip.RestrictionText = tooltip.RestrictionText or tooltip:CreateFontString(nil, "OVERLAY")
|
||||
tooltip.RestrictionText:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
|
||||
tooltip.RestrictionText:SetJustifyH("LEFT")
|
||||
|
||||
|
||||
tooltip.FactionText = tooltip.FactionText or tooltip:CreateFontString(nil, "OVERLAY")
|
||||
tooltip.FactionText:SetFont("Fonts\\FRIZQT__.TTF", 11)
|
||||
tooltip.FactionText:SetWordWrap(true)
|
||||
@@ -416,12 +416,12 @@ local function UpdateClassTooltip(button)
|
||||
coloredRoles = string.gsub(coloredRoles, "Дальний бой", "|cffff2020Дальний бой|r")
|
||||
coloredRoles = string.gsub(coloredRoles, "Танк", "|cff0070ddТанк|r")
|
||||
coloredRoles = string.gsub(coloredRoles, "Лекарь", "|cff20c000Лекарь|r")
|
||||
|
||||
|
||||
if not tooltip.Roles then
|
||||
tooltip.Roles = tooltip:CreateFontString(nil, "OVERLAY")
|
||||
tooltip.Roles:SetFont("Fonts\\FRIZQT__.TTF", 11)
|
||||
end
|
||||
|
||||
|
||||
tooltip.Roles:SetText("|cFFFFFFFF"..FUNTION_INF.."|r\n\n "..coloredRoles)
|
||||
|
||||
if showRestriction then
|
||||
@@ -435,20 +435,20 @@ local function UpdateClassTooltip(button)
|
||||
tooltip.RestrictionText:Show()
|
||||
|
||||
local allianceRaces, hordeRaces = GetValidRacesForClass(classID)
|
||||
|
||||
|
||||
local factionText = ""
|
||||
|
||||
|
||||
if #allianceRaces > 0 then
|
||||
factionText = ALLIANCE_RACE .. " |cFFFFFFFF" .. table.concat(allianceRaces, ", ") .. "|r"
|
||||
end
|
||||
|
||||
|
||||
if #hordeRaces > 0 then
|
||||
if factionText ~= "" then
|
||||
factionText = factionText .. "\n\n"
|
||||
end
|
||||
factionText = factionText .. "\n" .. HORDE_RACE .. " |cFFFFFFFF" .. table.concat(hordeRaces, ", ") .. "|r"
|
||||
end
|
||||
|
||||
|
||||
tooltip.FactionText:SetPoint("TOPLEFT", tooltip.RestrictionText, "BOTTOMLEFT", 0, -15)
|
||||
tooltip.FactionText:SetTextColor(1, 0.82, 0)
|
||||
tooltip.FactionText:SetText(factionText)
|
||||
@@ -459,18 +459,18 @@ local function UpdateClassTooltip(button)
|
||||
maxWidth = math.max(maxWidth, tooltip.RestrictionText:GetWidth())
|
||||
maxWidth = math.max(maxWidth, tooltip.FactionText:GetWidth())
|
||||
maxWidth = math.max(280, math.min(maxWidth + 16, 400))
|
||||
|
||||
|
||||
tooltip:SetWidth(maxWidth)
|
||||
tooltip.FactionText:SetWidth(maxWidth - 16)
|
||||
|
||||
local restrictionHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
|
||||
local restrictionHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
tooltip.Roles:GetHeight() +
|
||||
tooltip.RestrictionText:GetHeight() +
|
||||
tooltip.RestrictionText:GetHeight() +
|
||||
tooltip.FactionText:GetHeight() + 80
|
||||
|
||||
|
||||
tooltip:SetHeight(restrictionHeight)
|
||||
|
||||
|
||||
else
|
||||
tooltip.RestrictionText:Hide()
|
||||
tooltip.FactionText:Hide()
|
||||
@@ -484,10 +484,10 @@ local function UpdateClassTooltip(button)
|
||||
maxWidth = math.max(280, math.min(maxWidth + 16, 400))
|
||||
tooltip:SetWidth(maxWidth)
|
||||
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
local baseHeight = tooltip.text:GetHeight() +
|
||||
tooltip.detailsText:GetHeight() +
|
||||
tooltip.Roles:GetHeight() + 30
|
||||
|
||||
|
||||
tooltip:SetHeight(baseHeight)
|
||||
end
|
||||
end
|
||||
@@ -498,7 +498,7 @@ function CharacterCreate_MoveTexturesToBackground()
|
||||
if button then
|
||||
local normalTex = _G[button:GetName().."NormalTexture"]
|
||||
local pushedTex = _G[button:GetName().."PushedTexture"]
|
||||
|
||||
|
||||
if normalTex then
|
||||
normalTex:SetDrawLayer("BACKGROUND")
|
||||
end
|
||||
@@ -513,7 +513,7 @@ function CharacterCreate_MoveTexturesToBackground()
|
||||
if button then
|
||||
local normalTex = _G[button:GetName().."NormalTexture"]
|
||||
local pushedTex = _G[button:GetName().."PushedTexture"]
|
||||
|
||||
|
||||
if normalTex then
|
||||
normalTex:SetDrawLayer("BACKGROUND")
|
||||
end
|
||||
@@ -525,12 +525,12 @@ function CharacterCreate_MoveTexturesToBackground()
|
||||
|
||||
local maleButton = CharacterCreateGenderButtonMale
|
||||
local femaleButton = CharacterCreateGenderButtonFemale
|
||||
|
||||
|
||||
if maleButton then
|
||||
_G[maleButton:GetName().."NormalTexture"]:SetDrawLayer("BACKGROUND")
|
||||
_G[maleButton:GetName().."PushedTexture"]:SetDrawLayer("BACKGROUND")
|
||||
end
|
||||
|
||||
|
||||
if femaleButton then
|
||||
_G[femaleButton:GetName().."NormalTexture"]:SetDrawLayer("BACKGROUND")
|
||||
_G[femaleButton:GetName().."PushedTexture"]:SetDrawLayer("BACKGROUND")
|
||||
@@ -558,7 +558,7 @@ local function CreateFactionTooltip(parent, factionName)
|
||||
tooltip.description:SetWordWrap(true)
|
||||
tooltip.description:SetJustifyH("LEFT")
|
||||
tooltip.description:SetTextColor(0.9, 0.9, 0.9)
|
||||
|
||||
|
||||
return tooltip
|
||||
end
|
||||
|
||||
@@ -566,7 +566,7 @@ local function UpdateTooltipSize(tooltip)
|
||||
local titleHeight = tooltip.title:GetHeight()
|
||||
local descHeight = tooltip.description:GetHeight()
|
||||
local totalHeight = titleHeight + descHeight + 30
|
||||
|
||||
|
||||
tooltip:SetHeight(totalHeight)
|
||||
tooltip:SetWidth(300)
|
||||
end
|
||||
@@ -580,7 +580,7 @@ function CharacterCreate_PositionRaceButtons()
|
||||
local button = _G["CharacterCreateRaceButton"..i]
|
||||
if button then
|
||||
button:ClearAllPoints()
|
||||
|
||||
|
||||
if i == 1 then
|
||||
button:SetPoint("TOP", AllianceLogoFrame, "BOTTOM", -horizontalOffset, verticalStart)
|
||||
else
|
||||
@@ -594,7 +594,7 @@ function CharacterCreate_PositionRaceButtons()
|
||||
local button = _G["CharacterCreateRaceButton"..i]
|
||||
if button then
|
||||
button:ClearAllPoints()
|
||||
|
||||
|
||||
if i == 6 then
|
||||
button:SetPoint("TOP", HordeLogoFrame, "BOTTOM", horizontalOffset, verticalStart)
|
||||
else
|
||||
@@ -607,12 +607,12 @@ end
|
||||
|
||||
function CharacterCreate_PositionClassButtons()
|
||||
local buttonSpacing = 80
|
||||
|
||||
|
||||
for i = 1, MAX_CLASSES_PER_RACE do
|
||||
local button = _G["CharacterCreateClassButton"..i]
|
||||
if button then
|
||||
button:ClearAllPoints()
|
||||
|
||||
|
||||
if i == 1 then
|
||||
button:SetPoint("CENTER", CharacterCreateFrame, "BOTTOM", -360, 80)
|
||||
else
|
||||
@@ -624,16 +624,16 @@ function CharacterCreate_PositionClassButtons()
|
||||
end
|
||||
|
||||
function CharacterCreate_PositionGenderButtons()
|
||||
local genderSpacing = 310
|
||||
|
||||
local genderSpacing = 310
|
||||
|
||||
local maleButton = CharacterCreateGenderButtonMale
|
||||
local femaleButton = CharacterCreateGenderButtonFemale
|
||||
|
||||
|
||||
if maleButton then
|
||||
maleButton:ClearAllPoints()
|
||||
maleButton:SetPoint("CENTER", CharacterCreateFrame, "CENTER", -(genderSpacing/2), -250)
|
||||
end
|
||||
|
||||
|
||||
if femaleButton then
|
||||
femaleButton:ClearAllPoints()
|
||||
femaleButton:SetPoint("CENTER", CharacterCreateFrame, "CENTER", (genderSpacing/2), -250)
|
||||
@@ -676,6 +676,82 @@ function CharacterCreate_UpdateGameModeVisibility()
|
||||
end
|
||||
end
|
||||
|
||||
-- The stock creation scene frames each race at a different height. Projection
|
||||
-- zoom therefore needs a per-race vertical target to keep the face in the same
|
||||
-- part of the screen. Values are indexed by GetSelectedSex(): 2 male, 3 female.
|
||||
local CHARACTER_CREATE_CAMERA_PROFILES = {
|
||||
HUMAN = { [2] = -0.28, [3] = -0.20 },
|
||||
ORC = { [2] = -0.50, [3] = -0.30 },
|
||||
DWARF = { [2] = 0.30, [3] = 0.38 },
|
||||
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
|
||||
SCOURGE = { [2] = 0.18, [3] = 0.27 },
|
||||
TAUREN = { [2] = -0.78, [3] = -0.62 },
|
||||
GNOME = { [2] = 0.64, [3] = 0.72 },
|
||||
TROLL = { [2] = -0.70, [3] = -0.55 },
|
||||
BLOODELF = { [2] = -0.30, [3] = -0.22 },
|
||||
DRAENEI = { [2] = -0.62, [3] = -0.55 },
|
||||
};
|
||||
|
||||
local function CharacterCreate_GetCameraProfile()
|
||||
local _, raceFile = GetNameForRace();
|
||||
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
|
||||
local sex = GetSelectedSex();
|
||||
return 2.0, raceProfile and raceProfile[sex] or -0.20;
|
||||
end
|
||||
|
||||
local function CharacterCreate_UpdateCamera(immediate)
|
||||
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
|
||||
local mode = showFace and 1 or 0;
|
||||
if CharacterCreate.cameraMode == mode and not immediate then
|
||||
return;
|
||||
end
|
||||
|
||||
CharacterCreate.cameraMode = mode;
|
||||
CharacterCreate:SetCamera(0);
|
||||
if MoonWellSetCharacterCreateCamera then
|
||||
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
|
||||
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
|
||||
end
|
||||
end
|
||||
|
||||
-- The stock creation scene frames each race at a different height. Projection
|
||||
-- zoom therefore needs a per-race vertical target to keep the face in frame.
|
||||
-- Values are indexed by GetSelectedSex(): 2 male, 3 female.
|
||||
local CHARACTER_CREATE_CAMERA_PROFILES = {
|
||||
HUMAN = { [2] = -0.28, [3] = -0.20 },
|
||||
ORC = { [2] = -0.50, [3] = -0.30 },
|
||||
DWARF = { [2] = 0.30, [3] = 0.38 },
|
||||
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
|
||||
SCOURGE = { [2] = 0.18, [3] = 0.27 },
|
||||
TAUREN = { [2] = -0.78, [3] = -0.62 },
|
||||
GNOME = { [2] = 0.64, [3] = 0.72 },
|
||||
TROLL = { [2] = -0.70, [3] = -0.55 },
|
||||
BLOODELF = { [2] = -0.30, [3] = -0.22 },
|
||||
DRAENEI = { [2] = -0.62, [3] = -0.55 },
|
||||
};
|
||||
|
||||
local function CharacterCreate_GetCameraProfile()
|
||||
local _, raceFile = GetNameForRace();
|
||||
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
|
||||
local sex = GetSelectedSex();
|
||||
return 2.0, raceProfile and raceProfile[sex] or -0.20;
|
||||
end
|
||||
|
||||
local function CharacterCreate_UpdateCamera(immediate)
|
||||
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
|
||||
local mode = showFace and 1 or 0;
|
||||
if CharacterCreate.cameraMode == mode and not immediate then
|
||||
return;
|
||||
end
|
||||
|
||||
CharacterCreate.cameraMode = mode;
|
||||
CharacterCreate:SetCamera(0);
|
||||
if MoonWellSetCharacterCreateCamera then
|
||||
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
|
||||
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
|
||||
end
|
||||
end
|
||||
|
||||
local function CharacterCreate_GetEnteredName()
|
||||
local text = "";
|
||||
|
||||
@@ -701,6 +777,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
|
||||
CharacterCreate.gameModeSelectionMode = false;
|
||||
end
|
||||
|
||||
CharacterCreate_UpdateCamera();
|
||||
|
||||
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
||||
|
||||
if showGameModes then
|
||||
@@ -778,7 +856,7 @@ function CharacterCreate_OnLoad(self)
|
||||
self:SetSequence(0)
|
||||
self:SetCamera(0)
|
||||
SetCharCustomizeFrame("CharacterCreate")
|
||||
|
||||
|
||||
CharacterCreate.numRaces = 0
|
||||
CharacterCreate.selectedRace = 0
|
||||
CharacterCreate.numClasses = 0
|
||||
@@ -820,7 +898,7 @@ function CharacterCreate_OnLoad(self)
|
||||
AllianceLogoFrame:SetSize(100, 100)
|
||||
AllianceLogoFrame:SetPoint("TOPLEFT", -16, 16)
|
||||
AllianceLogoFrame:EnableMouse(true)
|
||||
|
||||
|
||||
AllianceLogoFrame:SetScript("OnEnter", function(self)
|
||||
AllianceTooltip.title:SetText(ALLIANCE)
|
||||
AllianceTooltip.description:SetText(FACTION_ALLIANCE_DESCRIPTION)
|
||||
@@ -829,7 +907,7 @@ function CharacterCreate_OnLoad(self)
|
||||
AllianceTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 90, 60)
|
||||
AllianceTooltip:Show()
|
||||
end)
|
||||
|
||||
|
||||
AllianceLogoFrame:SetScript("OnLeave", function(self)
|
||||
AllianceTooltip:Hide()
|
||||
end)
|
||||
@@ -850,7 +928,7 @@ function CharacterCreate_OnLoad(self)
|
||||
HordeLogoFrame:SetSize(100, 100)
|
||||
HordeLogoFrame:SetPoint("TOPRIGHT", 16, 16)
|
||||
HordeLogoFrame:EnableMouse(true)
|
||||
|
||||
|
||||
HordeLogoFrame:SetScript("OnEnter", function(self)
|
||||
HordeTooltip.title:SetText(HORDE)
|
||||
HordeTooltip.description:SetText(FACTION_HORDE_DESCRIPTION)
|
||||
@@ -859,7 +937,7 @@ function CharacterCreate_OnLoad(self)
|
||||
HordeTooltip:SetPoint("TOPRIGHT", self, "BOTTOMRIGHT", -90, 60)
|
||||
HordeTooltip:Show()
|
||||
end)
|
||||
|
||||
|
||||
HordeLogoFrame:SetScript("OnLeave", function(self)
|
||||
HordeTooltip:Hide()
|
||||
end)
|
||||
@@ -873,7 +951,7 @@ function CharacterCreate_SetupCustomButtons()
|
||||
{button = CharCreateRandomizeButton, width = 36, height = 36},
|
||||
{button = CharacterCreateRandomName, width = 30, height = 30}
|
||||
}
|
||||
|
||||
|
||||
local texturePath = "Interface\\Glues\\CharacterCreate\\charactercreate"
|
||||
|
||||
for _, btnInfo in ipairs(buttons) do
|
||||
@@ -943,6 +1021,7 @@ function CharacterCreate_TogglePersonalization()
|
||||
CharCreatePersonalizeButton:Show();
|
||||
CharCreateOkayButton:Hide();
|
||||
CharacterCreate_UpdateGameModeVisibility();
|
||||
CharacterCreate_UpdateCamera();
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
@@ -961,7 +1040,7 @@ end)
|
||||
|
||||
function CharacterCreate_OnShow()
|
||||
CharacterCreate.personalizationMode = false;
|
||||
|
||||
|
||||
for i=1, MAX_CLASSES_PER_RACE, 1 do
|
||||
local button = _G["CharacterCreateClassButton"..i];
|
||||
button:Enable();
|
||||
@@ -985,7 +1064,7 @@ function CharacterCreate_OnShow()
|
||||
CharacterCreate.personalizationMode = false;
|
||||
CharacterCreate.gameModeSelectionMode = false;
|
||||
CharCreateOkayButton:Hide();
|
||||
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
end
|
||||
@@ -1001,7 +1080,7 @@ function CharacterCreate_OnShow()
|
||||
CharacterChangeFixup();
|
||||
CharacterCreate_CreateGenderButtonTextures();
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
CharacterCreate_ResetState();
|
||||
CharacterCreate_ResetState(true);
|
||||
|
||||
hideScheduled = true
|
||||
HideNameEditFrame:Show()
|
||||
@@ -1013,7 +1092,7 @@ end
|
||||
function CharacterCreate_OnHide()
|
||||
PAID_SERVICE_CHARACTER_ID = nil;
|
||||
PAID_SERVICE_TYPE = nil;
|
||||
CharacterCreate_ResetState();
|
||||
CharacterCreate_ResetState(true);
|
||||
|
||||
for button, tooltip in pairs(raceTooltips) do
|
||||
if tooltip then tooltip:Hide() end
|
||||
@@ -1072,11 +1151,11 @@ function CharacterCreateEnumerateRaces(...)
|
||||
local raceID = index
|
||||
local faction = _G.GetFactionForRaceID(raceID)
|
||||
local borderColor
|
||||
|
||||
|
||||
if faction == "Alliance" then
|
||||
borderColor = {0.0, 0.4, 1.0}
|
||||
else
|
||||
borderColor = {1.0, 0.0, 0.0}
|
||||
borderColor = {1.0, 0.0, 0.0}
|
||||
end
|
||||
|
||||
if not button.staticTexture then
|
||||
@@ -1114,10 +1193,10 @@ function CharacterCreateEnumerateRaces(...)
|
||||
if not button.texturesMoved then
|
||||
button:SetScript("OnMouseDown", nil);
|
||||
button:SetScript("OnMouseUp", nil);
|
||||
|
||||
|
||||
button.texturesMoved = true;
|
||||
end
|
||||
|
||||
|
||||
button:Show();
|
||||
if ( select(i+2, ...) == 1 ) then
|
||||
button.enable = true;
|
||||
@@ -1139,7 +1218,7 @@ function CharacterCreateEnumerateRaces(...)
|
||||
tooltip:Hide()
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
button:SetScript("OnMouseDown", function(self, clickedButton)
|
||||
if self:IsEnabled() and clickedButton == "RightButton" then
|
||||
local tooltip = GetOrCreateRaceTooltip(self)
|
||||
@@ -1205,7 +1284,7 @@ function CharacterCreateEnumerateClasses(...)
|
||||
button.nameFrame = CreateFrame("Frame", nil, button);
|
||||
button.nameFrame:SetSize(112, 40);
|
||||
button.nameFrame:SetPoint("TOP", button, "BOTTOM", 0, -5);
|
||||
|
||||
|
||||
button.nameFrame.text = button.nameFrame:CreateFontString(nil, "OVERLAY");
|
||||
button.nameFrame.text:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE");
|
||||
button.nameFrame.text:SetPoint("CENTER", 0, 0);
|
||||
@@ -1220,10 +1299,10 @@ function CharacterCreateEnumerateClasses(...)
|
||||
if not button.texturesMoved then
|
||||
button:SetScript("OnMouseDown", nil);
|
||||
button:SetScript("OnMouseUp", nil);
|
||||
|
||||
|
||||
button.texturesMoved = true;
|
||||
end
|
||||
|
||||
|
||||
button:Show();
|
||||
|
||||
local className = select(i, ...);
|
||||
@@ -1270,7 +1349,7 @@ function CharacterCreateEnumerateClasses(...)
|
||||
tooltip:Hide()
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
button:SetScript("OnMouseDown", function(self, clickedButton)
|
||||
if self:IsEnabled() and clickedButton == "RightButton" then
|
||||
local tooltip = GetOrCreateClassTooltip(self)
|
||||
@@ -1315,14 +1394,14 @@ function CharacterCreate_CreateGenderButtonTextures()
|
||||
maleButton.staticTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
maleButton.staticTexture:SetSize(86, 86);
|
||||
maleButton.staticTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
maleButton.highlightTexture = maleButton:CreateTexture(maleButton:GetName().."HighlightTexture", "HIGHLIGHT");
|
||||
maleButton.highlightTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
maleButton.highlightTexture:SetAlpha(0.5);
|
||||
maleButton.highlightTexture:SetBlendMode("ADD");
|
||||
maleButton.highlightTexture:SetSize(86, 86);
|
||||
maleButton.highlightTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
maleButton.checkedTexture = maleButton:CreateTexture(maleButton:GetName().."CheckedTexture", "OVERLAY");
|
||||
maleButton.checkedTexture:SetDrawLayer("OVERLAY", 7);
|
||||
maleButton.checkedTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorderRace_H");
|
||||
@@ -1341,14 +1420,14 @@ function CharacterCreate_CreateGenderButtonTextures()
|
||||
femaleButton.staticTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
femaleButton.staticTexture:SetSize(86, 86);
|
||||
femaleButton.staticTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
femaleButton.highlightTexture = femaleButton:CreateTexture(femaleButton:GetName().."HighlightTexture", "HIGHLIGHT");
|
||||
femaleButton.highlightTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
|
||||
femaleButton.highlightTexture:SetAlpha(0.5);
|
||||
femaleButton.highlightTexture:SetBlendMode("ADD");
|
||||
femaleButton.highlightTexture:SetSize(86, 86);
|
||||
femaleButton.highlightTexture:SetPoint("CENTER", 0, 0);
|
||||
|
||||
|
||||
femaleButton.checkedTexture = femaleButton:CreateTexture(femaleButton:GetName().."CheckedTexture", "OVERLAY");
|
||||
femaleButton.checkedTexture:SetDrawLayer("OVERLAY", 7);
|
||||
femaleButton.checkedTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorderRace_H");
|
||||
@@ -1439,7 +1518,7 @@ function SetCharacterClass(id)
|
||||
end
|
||||
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
|
||||
|
||||
local className, classFileName, _, tank, healer, damage = GetSelectedClass();
|
||||
local abilityIndex = 0;
|
||||
local tempText = _G["CLASS_INFO_"..classFileName..abilityIndex];
|
||||
@@ -1452,7 +1531,7 @@ function SetCharacterClass(id)
|
||||
local coords = CLASS_ICON_TCOORDS[classFileName];
|
||||
CharacterCreateClassIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
|
||||
CharacterCreateClassLabel:SetText(className);
|
||||
CharacterCreateClassRolesText:SetText(abilityText);
|
||||
CharacterCreateClassRolesText:SetText(abilityText);
|
||||
CharacterCreateClassText:SetText(GetFlavorText("CLASS_"..strupper(classFileName), GetSelectedSex()).."|n|n");
|
||||
CharacterCreateClassScrollFrameScrollBar:SetValue(0);
|
||||
end
|
||||
@@ -1488,7 +1567,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
|
||||
|
||||
local maleButton = CharacterCreateGenderButtonMale;
|
||||
local femaleButton = CharacterCreateGenderButtonFemale;
|
||||
|
||||
|
||||
if maleButton and maleButton.checkedTexture then
|
||||
if maleButton:GetChecked() then
|
||||
maleButton.checkedTexture:Show();
|
||||
@@ -1496,7 +1575,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
|
||||
maleButton.checkedTexture:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if femaleButton and femaleButton.checkedTexture then
|
||||
if femaleButton:GetChecked() then
|
||||
femaleButton.checkedTexture:Show();
|
||||
@@ -1518,6 +1597,7 @@ end
|
||||
|
||||
function CharacterCreate_UpdateModel(self)
|
||||
UpdateCustomizationScene();
|
||||
CharacterCreate_UpdateCamera();
|
||||
self:AdvanceTime();
|
||||
end
|
||||
|
||||
@@ -1599,7 +1679,7 @@ function CharacterRace_OnClick(self, id)
|
||||
SetCharacterClass(classIndex);
|
||||
|
||||
CharacterCreate_UpdateHairCustomization();
|
||||
|
||||
|
||||
CharacterChangeFixup();
|
||||
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
@@ -1624,7 +1704,7 @@ function SetCharacterGender(sex)
|
||||
CharacterCreateEnumerateRaces(GetAvailableRaces());
|
||||
CharacterCreateEnumerateClasses(GetAvailableClasses());
|
||||
SetCharacterRace(GetSelectedRace());
|
||||
|
||||
|
||||
local _,_,classIndex = GetSelectedClass();
|
||||
if ( PAID_SERVICE_TYPE ) then
|
||||
classIndex = PaidChange_GetCurrentClassIndex();
|
||||
@@ -1638,15 +1718,16 @@ function SetCharacterGender(sex)
|
||||
fileString = strupper(fileString);
|
||||
local coords = RACE_ICON_TCOORDS[fileString.."_"..gender];
|
||||
CharacterCreateRaceIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
|
||||
|
||||
|
||||
CharacterChangeFixup();
|
||||
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
end
|
||||
|
||||
function CharacterCreate_ResetState()
|
||||
function CharacterCreate_ResetState(immediate)
|
||||
CharacterCreate.personalizationMode = false;
|
||||
CharacterCreate.gameModeSelectionMode = false;
|
||||
CharacterCreate_UpdateCamera(immediate);
|
||||
CharacterCreateRaceButtonsContainer:Show();
|
||||
CharacterCreateClassButtonsContainer:Show();
|
||||
CharacterCreateGenderButtonsContainer:Show();
|
||||
@@ -1664,7 +1745,7 @@ function CharacterCreate_ResetState()
|
||||
CharCreatePersonalizeButton:Show();
|
||||
CharCreateOkayButton:Hide();
|
||||
CharacterCreate_UpdateGameModeVisibility();
|
||||
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
end
|
||||
@@ -1710,7 +1791,7 @@ end
|
||||
function CharacterCreate_UpdateHairCustomization()
|
||||
CharacterCustomizationButtonFrame3Text:SetText(_G["HAIR_"..GetHairCustomization().."_STYLE"]);
|
||||
CharacterCustomizationButtonFrame4Text:SetText(_G["HAIR_"..GetHairCustomization().."_COLOR"]);
|
||||
CharacterCustomizationButtonFrame5Text:SetText(_G["FACIAL_HAIR_"..GetFacialHairCustomization()]);
|
||||
CharacterCustomizationButtonFrame5Text:SetText(_G["FACIAL_HAIR_"..GetFacialHairCustomization()]);
|
||||
end
|
||||
|
||||
function SetButtonDesaturated(button, desaturated, r, g, b)
|
||||
@@ -1732,7 +1813,7 @@ function SetButtonDesaturated(button, desaturated, r, g, b)
|
||||
g = 0.5;
|
||||
b = 0.5;
|
||||
end
|
||||
|
||||
|
||||
icon:SetVertexColor(r, g, b);
|
||||
end
|
||||
|
||||
|
||||
@@ -1139,6 +1139,12 @@ end
|
||||
function CharacterSelect_EnterWorld()
|
||||
PlaySound("gsCharacterSelectionEnterWorld");
|
||||
StopGlueAmbience();
|
||||
-- Persist the server-provided game-mode bit across the GlueXML -> FrameXML
|
||||
-- Lua-state transition. WarcraftXL owns the value for this Wow.exe process.
|
||||
if ( type(MoonWellSetLoginCharacterFlags) == "function" ) then
|
||||
local info = { GetCharacterInfo(CharacterSelect.selectedIndex) };
|
||||
MoonWellSetLoginCharacterFlags(info[11] or 0);
|
||||
end
|
||||
EnterWorld();
|
||||
end
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
@@ -0,0 +1,57 @@
|
||||
--- AceConfig-3.0 wrapper library.
|
||||
-- Provides an API to register an options table with the config registry,
|
||||
-- as well as associate it with a slash command.
|
||||
-- @class file
|
||||
-- @name AceConfig-3.0
|
||||
-- @release $Id: AceConfig-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $
|
||||
|
||||
--[[
|
||||
AceConfig-3.0
|
||||
|
||||
Very light wrapper library that combines all the AceConfig subcomponents into one more easily used whole.
|
||||
|
||||
]]
|
||||
|
||||
local MAJOR, MINOR = "AceConfig-3.0", 2
|
||||
local AceConfig = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfig then return end
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
local cfgcmd = LibStub("AceConfigCmd-3.0")
|
||||
local cfgdlg = LibStub("AceConfigDialog-3.0")
|
||||
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local pcall, error, type, pairs = pcall, error, type, pairs
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- :RegisterOptionsTable(appName, options, slashcmd, persist)
|
||||
--
|
||||
-- - appName - (string) application name
|
||||
-- - options - table or function ref, see AceConfigRegistry
|
||||
-- - slashcmd - slash command (string) or table with commands, or nil to NOT create a slash command
|
||||
|
||||
--- Register a option table with the AceConfig registry.
|
||||
-- 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)
|
||||
-- @param slashcmd A slash command to register for the option table, or a table of slash commands.
|
||||
-- @usage
|
||||
-- local AceConfig = LibStub("AceConfig-3.0")
|
||||
-- AceConfig:RegisterOptionsTable("MyAddon", myOptions, {"/myslash", "/my"})
|
||||
function AceConfig:RegisterOptionsTable(appName, options, slashcmd)
|
||||
local ok,msg = pcall(cfgreg.RegisterOptionsTable, self, appName, options)
|
||||
if not ok then error(msg, 2) end
|
||||
|
||||
if slashcmd then
|
||||
if type(slashcmd) == "table" then
|
||||
for _,cmd in pairs(slashcmd) do
|
||||
cfgcmd:CreateChatCommand(cmd, appName)
|
||||
end
|
||||
else
|
||||
cfgcmd:CreateChatCommand(slashcmd, appName)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
<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">
|
||||
<Include file="AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
|
||||
<Include file="AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
|
||||
<Include file="AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
|
||||
<!--<Include file="AceConfigDropdown-3.0\AceConfigDropdown-3.0.xml"/>-->
|
||||
<Script file="AceConfig-3.0.lua"/>
|
||||
</Ui>
|
||||
+787
@@ -0,0 +1,787 @@
|
||||
--- AceConfigCmd-3.0 handles access to an options table through the "command line" interface via the ChatFrames.
|
||||
-- @class file
|
||||
-- @name AceConfigCmd-3.0
|
||||
-- @release $Id: AceConfigCmd-3.0.lua 904 2009-12-13 11:56:37Z nevcairiel $
|
||||
|
||||
--[[
|
||||
AceConfigCmd-3.0
|
||||
|
||||
Handles commandline optionstable access
|
||||
|
||||
REQUIRES: AceConsole-3.0 for command registration (loaded on demand)
|
||||
|
||||
]]
|
||||
|
||||
-- TODO: plugin args
|
||||
|
||||
|
||||
local MAJOR, MINOR = "AceConfigCmd-3.0", 12
|
||||
local AceConfigCmd = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigCmd then return end
|
||||
|
||||
AceConfigCmd.commands = AceConfigCmd.commands or {}
|
||||
local commands = AceConfigCmd.commands
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
local AceConsole -- LoD
|
||||
local AceConsoleName = "AceConsole-3.0"
|
||||
|
||||
-- Lua APIs
|
||||
local strsub, strsplit, strlower, strmatch, strtrim = string.sub, string.split, string.lower, string.match, string.trim
|
||||
local format, tonumber, tostring = string.format, tonumber, tostring
|
||||
local tsort, tinsert = table.sort, table.insert
|
||||
local select, pairs, next, type = select, pairs, next, type
|
||||
local error, assert = error, assert
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, SELECTED_CHAT_FRAME, DEFAULT_CHAT_FRAME
|
||||
|
||||
|
||||
local L = setmetatable({}, { -- TODO: replace with proper locale
|
||||
__index = function(self,k) return k end
|
||||
})
|
||||
|
||||
|
||||
|
||||
local function print(msg)
|
||||
(SELECTED_CHAT_FRAME or DEFAULT_CHAT_FRAME):AddMessage(msg)
|
||||
end
|
||||
|
||||
-- constants used by getparam() calls below
|
||||
|
||||
local handlertypes = {["table"]=true}
|
||||
local handlermsg = "expected a table"
|
||||
|
||||
local functypes = {["function"]=true, ["string"]=true}
|
||||
local funcmsg = "expected function or member name"
|
||||
|
||||
|
||||
-- pickfirstset() - picks the first non-nil value and returns it
|
||||
|
||||
local function pickfirstset(...)
|
||||
for i=1,select("#",...) do
|
||||
if select(i,...)~=nil then
|
||||
return select(i,...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- err() - produce real error() regarding malformed options tables etc
|
||||
|
||||
local function err(info,inputpos,msg )
|
||||
local cmdstr=" "..strsub(info.input, 1, inputpos-1)
|
||||
error(MAJOR..": /" ..info[0] ..cmdstr ..": "..(msg or "malformed options table"), 2)
|
||||
end
|
||||
|
||||
|
||||
-- usererr() - produce chatframe message regarding bad slash syntax etc
|
||||
|
||||
local function usererr(info,inputpos,msg )
|
||||
local cmdstr=strsub(info.input, 1, inputpos-1);
|
||||
print("/" ..info[0] .. " "..cmdstr ..": "..(msg or "malformed options table"))
|
||||
end
|
||||
|
||||
|
||||
-- callmethod() - call a given named method (e.g. "get", "set") with given arguments
|
||||
|
||||
local function callmethod(info, inputpos, tab, methodtype, ...)
|
||||
local method = info[methodtype]
|
||||
if not method then
|
||||
err(info, inputpos, "'"..methodtype.."': not set")
|
||||
end
|
||||
|
||||
info.arg = tab.arg
|
||||
info.option = tab
|
||||
info.type = tab.type
|
||||
|
||||
if type(method)=="function" then
|
||||
return method(info, ...)
|
||||
elseif type(method)=="string" then
|
||||
if type(info.handler[method])~="function" then
|
||||
err(info, inputpos, "'"..methodtype.."': '"..method.."' is not a member function of "..tostring(info.handler))
|
||||
end
|
||||
return info.handler[method](info.handler, info, ...)
|
||||
else
|
||||
assert(false) -- type should have already been checked on read
|
||||
end
|
||||
end
|
||||
|
||||
-- callfunction() - call a given named function (e.g. "name", "desc") with given arguments
|
||||
|
||||
local function callfunction(info, tab, methodtype, ...)
|
||||
local method = tab[methodtype]
|
||||
|
||||
info.arg = tab.arg
|
||||
info.option = tab
|
||||
info.type = tab.type
|
||||
|
||||
if type(method)=="function" then
|
||||
return method(info, ...)
|
||||
else
|
||||
assert(false) -- type should have already been checked on read
|
||||
end
|
||||
end
|
||||
|
||||
-- do_final() - do the final step (set/execute) along with validation and confirmation
|
||||
|
||||
local function do_final(info, inputpos, tab, methodtype, ...)
|
||||
if info.validate then
|
||||
local res = callmethod(info,inputpos,tab,"validate",...)
|
||||
if type(res)=="string" then
|
||||
usererr(info, inputpos, "'"..strsub(info.input, inputpos).."' - "..res)
|
||||
return
|
||||
end
|
||||
end
|
||||
-- console ignores .confirm
|
||||
|
||||
callmethod(info,inputpos,tab,methodtype, ...)
|
||||
end
|
||||
|
||||
|
||||
-- getparam() - used by handle() to retreive and store "handler", "get", "set", etc
|
||||
|
||||
local function getparam(info, inputpos, tab, depth, paramname, types, errormsg)
|
||||
local old,oldat = info[paramname], info[paramname.."_at"]
|
||||
local val=tab[paramname]
|
||||
if val~=nil then
|
||||
if val==false then
|
||||
val=nil
|
||||
elseif not types[type(val)] then
|
||||
err(info, inputpos, "'" .. paramname.. "' - "..errormsg)
|
||||
end
|
||||
info[paramname] = val
|
||||
info[paramname.."_at"] = depth
|
||||
end
|
||||
return old,oldat
|
||||
end
|
||||
|
||||
|
||||
-- iterateargs(tab) - custom iterator that iterates both t.args and t.plugins.*
|
||||
local dummytable={}
|
||||
|
||||
local function iterateargs(tab)
|
||||
if not tab.plugins then
|
||||
return pairs(tab.args)
|
||||
end
|
||||
|
||||
local argtabkey,argtab=next(tab.plugins)
|
||||
local v
|
||||
|
||||
return function(_, k)
|
||||
while argtab do
|
||||
k,v = next(argtab, k)
|
||||
if k then return k,v end
|
||||
if argtab==tab.args then
|
||||
argtab=nil
|
||||
else
|
||||
argtabkey,argtab = next(tab.plugins, argtabkey)
|
||||
if not argtabkey then
|
||||
argtab=tab.args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function checkhidden(info, inputpos, tab)
|
||||
if tab.cmdHidden~=nil then
|
||||
return tab.cmdHidden
|
||||
end
|
||||
local hidden = tab.hidden
|
||||
if type(hidden) == "function" or type(hidden) == "string" then
|
||||
info.hidden = hidden
|
||||
hidden = callmethod(info, inputpos, tab, 'hidden')
|
||||
info.hidden = nil
|
||||
end
|
||||
return hidden
|
||||
end
|
||||
|
||||
local function showhelp(info, inputpos, tab, depth, noHead)
|
||||
if not noHead then
|
||||
print("|cff33ff99"..info.appName.."|r: Arguments to |cffffff78/"..info[0].."|r "..strsub(info.input,1,inputpos-1)..":")
|
||||
end
|
||||
|
||||
local sortTbl = {} -- [1..n]=name
|
||||
local refTbl = {} -- [name]=tableref
|
||||
|
||||
for k,v in iterateargs(tab) do
|
||||
if not refTbl[k] then -- a plugin overriding something in .args
|
||||
tinsert(sortTbl, k)
|
||||
refTbl[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
tsort(sortTbl, function(one, two)
|
||||
local o1 = refTbl[one].order or 100
|
||||
local o2 = refTbl[two].order or 100
|
||||
if type(o1) == "function" or type(o1) == "string" then
|
||||
info.order = o1
|
||||
info[#info+1] = one
|
||||
o1 = callmethod(info, inputpos, refTbl[one], "order")
|
||||
info[#info] = nil
|
||||
info.order = nil
|
||||
end
|
||||
if type(o2) == "function" or type(o1) == "string" then
|
||||
info.order = o2
|
||||
info[#info+1] = two
|
||||
o2 = callmethod(info, inputpos, refTbl[two], "order")
|
||||
info[#info] = nil
|
||||
info.order = nil
|
||||
end
|
||||
if o1<0 and o2<0 then return o1<o2 end
|
||||
if o2<0 then return true end
|
||||
if o1<0 then return false end
|
||||
if o1==o2 then return tostring(one)<tostring(two) end -- compare names
|
||||
return o1<o2
|
||||
end)
|
||||
|
||||
for i = 1, #sortTbl do
|
||||
local k = sortTbl[i]
|
||||
local v = refTbl[k]
|
||||
if not checkhidden(info, inputpos, v) then
|
||||
if v.type ~= "description" and v.type ~= "header" then
|
||||
-- recursively show all inline groups
|
||||
local name, desc = v.name, v.desc
|
||||
if type(name) == "function" then
|
||||
name = callfunction(info, v, 'name')
|
||||
end
|
||||
if type(desc) == "function" then
|
||||
desc = callfunction(info, v, 'desc')
|
||||
end
|
||||
if v.type == "group" and pickfirstset(v.cmdInline, v.inline, false) then
|
||||
print(" "..(desc or name)..":")
|
||||
local oldhandler,oldhandler_at = getparam(info, inputpos, v, depth, "handler", handlertypes, handlermsg)
|
||||
showhelp(info, inputpos, v, depth, true)
|
||||
info.handler,info.handler_at = oldhandler,oldhandler_at
|
||||
else
|
||||
local key = k:gsub(" ", "_")
|
||||
print(" |cffffff78"..key.."|r - "..(desc or name or ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function keybindingValidateFunc(text)
|
||||
if text == nil or text == "NONE" then
|
||||
return nil
|
||||
end
|
||||
text = text:upper()
|
||||
local shift, ctrl, alt
|
||||
local modifier
|
||||
while true do
|
||||
if text == "-" then
|
||||
break
|
||||
end
|
||||
modifier, text = strsplit('-', text, 2)
|
||||
if text then
|
||||
if modifier ~= "SHIFT" and modifier ~= "CTRL" and modifier ~= "ALT" then
|
||||
return false
|
||||
end
|
||||
if modifier == "SHIFT" then
|
||||
if shift then
|
||||
return false
|
||||
end
|
||||
shift = true
|
||||
end
|
||||
if modifier == "CTRL" then
|
||||
if ctrl then
|
||||
return false
|
||||
end
|
||||
ctrl = true
|
||||
end
|
||||
if modifier == "ALT" then
|
||||
if alt then
|
||||
return false
|
||||
end
|
||||
alt = true
|
||||
end
|
||||
else
|
||||
text = modifier
|
||||
break
|
||||
end
|
||||
end
|
||||
if text == "" then
|
||||
return false
|
||||
end
|
||||
if not text:find("^F%d+$") and text ~= "CAPSLOCK" and text:len() ~= 1 and (text:byte() < 128 or text:len() > 4) and not _G["KEY_" .. text] then
|
||||
return false
|
||||
end
|
||||
local s = text
|
||||
if shift then
|
||||
s = "SHIFT-" .. s
|
||||
end
|
||||
if ctrl then
|
||||
s = "CTRL-" .. s
|
||||
end
|
||||
if alt then
|
||||
s = "ALT-" .. s
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- handle() - selfrecursing function that processes input->optiontable
|
||||
-- - depth - starts at 0
|
||||
-- - retfalse - return false rather than produce error if a match is not found (used by inlined groups)
|
||||
|
||||
local function handle(info, inputpos, tab, depth, retfalse)
|
||||
|
||||
if not(type(tab)=="table" and type(tab.type)=="string") then err(info,inputpos) end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Grab hold of handler,set,get,func,etc if set (and remember old ones)
|
||||
-- Note that we do NOT validate if method names are correct at this stage,
|
||||
-- the handler may change before they're actually used!
|
||||
|
||||
local oldhandler,oldhandler_at = getparam(info,inputpos,tab,depth,"handler",handlertypes,handlermsg)
|
||||
local oldset,oldset_at = getparam(info,inputpos,tab,depth,"set",functypes,funcmsg)
|
||||
local oldget,oldget_at = getparam(info,inputpos,tab,depth,"get",functypes,funcmsg)
|
||||
local oldfunc,oldfunc_at = getparam(info,inputpos,tab,depth,"func",functypes,funcmsg)
|
||||
local oldvalidate,oldvalidate_at = getparam(info,inputpos,tab,depth,"validate",functypes,funcmsg)
|
||||
--local oldconfirm,oldconfirm_at = getparam(info,inputpos,tab,depth,"confirm",functypes,funcmsg)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Act according to .type of this table
|
||||
|
||||
if tab.type=="group" then
|
||||
------------ group --------------------------------------------
|
||||
|
||||
if type(tab.args)~="table" then err(info, inputpos) end
|
||||
if tab.plugins and type(tab.plugins)~="table" then err(info,inputpos) end
|
||||
|
||||
-- grab next arg from input
|
||||
local _,nextpos,arg = (info.input):find(" *([^ ]+) *", inputpos)
|
||||
if not arg then
|
||||
showhelp(info, inputpos, tab, depth)
|
||||
return
|
||||
end
|
||||
nextpos=nextpos+1
|
||||
|
||||
-- loop .args and try to find a key with a matching name
|
||||
for k,v in iterateargs(tab) do
|
||||
if not(type(k)=="string" and type(v)=="table" and type(v.type)=="string") then err(info,inputpos, "options table child '"..tostring(k).."' is malformed") end
|
||||
|
||||
-- is this child an inline group? if so, traverse into it
|
||||
if v.type=="group" and pickfirstset(v.cmdInline, v.inline, false) then
|
||||
info[depth+1] = k
|
||||
if handle(info, inputpos, v, depth+1, true)==false then
|
||||
info[depth+1] = nil
|
||||
-- wasn't found in there, but that's ok, we just keep looking down here
|
||||
else
|
||||
return -- done, name was found in inline group
|
||||
end
|
||||
-- matching name and not a inline group
|
||||
elseif strlower(arg)==strlower(k:gsub(" ", "_")) then
|
||||
info[depth+1] = k
|
||||
return handle(info,nextpos,v,depth+1)
|
||||
end
|
||||
end
|
||||
|
||||
-- no match
|
||||
if retfalse then
|
||||
-- restore old infotable members and return false to indicate failure
|
||||
info.handler,info.handler_at = oldhandler,oldhandler_at
|
||||
info.set,info.set_at = oldset,oldset_at
|
||||
info.get,info.get_at = oldget,oldget_at
|
||||
info.func,info.func_at = oldfunc,oldfunc_at
|
||||
info.validate,info.validate_at = oldvalidate,oldvalidate_at
|
||||
--info.confirm,info.confirm_at = oldconfirm,oldconfirm_at
|
||||
return false
|
||||
end
|
||||
|
||||
-- couldn't find the command, display error
|
||||
usererr(info, inputpos, "'"..arg.."' - " .. L["unknown argument"])
|
||||
return
|
||||
end
|
||||
|
||||
local str = strsub(info.input,inputpos);
|
||||
|
||||
if tab.type=="execute" then
|
||||
------------ execute --------------------------------------------
|
||||
do_final(info, inputpos, tab, "func")
|
||||
|
||||
|
||||
|
||||
elseif tab.type=="input" then
|
||||
------------ input --------------------------------------------
|
||||
|
||||
local res = true
|
||||
if tab.pattern then
|
||||
if not(type(tab.pattern)=="string") then err(info, inputpos, "'pattern' - expected a string") end
|
||||
if not strmatch(str, tab.pattern) then
|
||||
usererr(info, inputpos, "'"..str.."' - " .. L["invalid input"])
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", str)
|
||||
|
||||
|
||||
|
||||
elseif tab.type=="toggle" then
|
||||
------------ toggle --------------------------------------------
|
||||
local b
|
||||
local str = strtrim(strlower(str))
|
||||
if str=="" then
|
||||
b = callmethod(info, inputpos, tab, "get")
|
||||
|
||||
if tab.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if b then
|
||||
b = nil
|
||||
elseif b == nil then
|
||||
b = false
|
||||
else
|
||||
b = true
|
||||
end
|
||||
else
|
||||
b = not b
|
||||
end
|
||||
|
||||
elseif str==L["on"] then
|
||||
b = true
|
||||
elseif str==L["off"] then
|
||||
b = false
|
||||
elseif tab.tristate and str==L["default"] then
|
||||
b = nil
|
||||
else
|
||||
if tab.tristate then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'on', 'off' or 'default', or no argument to toggle."], str))
|
||||
else
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'on' or 'off', or no argument to toggle."], str))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", b)
|
||||
|
||||
|
||||
elseif tab.type=="range" then
|
||||
------------ range --------------------------------------------
|
||||
local val = tonumber(str)
|
||||
if not val then
|
||||
usererr(info, inputpos, "'"..str.."' - "..L["expected number"])
|
||||
return
|
||||
end
|
||||
if type(info.step)=="number" then
|
||||
val = val- (val % info.step)
|
||||
end
|
||||
if type(info.min)=="number" and val<info.min then
|
||||
usererr(info, inputpos, val.." - "..format(L["must be equal to or higher than %s"], tostring(info.min)) )
|
||||
return
|
||||
end
|
||||
if type(info.max)=="number" and val>info.max then
|
||||
usererr(info, inputpos, val.." - "..format(L["must be equal to or lower than %s"], tostring(info.max)) )
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", val)
|
||||
|
||||
|
||||
elseif tab.type=="select" then
|
||||
------------ select ------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
|
||||
local values = tab.values
|
||||
if type(values) == "function" or type(values) == "string" then
|
||||
info.values = values
|
||||
values = callmethod(info, inputpos, tab, "values")
|
||||
info.values = nil
|
||||
end
|
||||
|
||||
if str == "" then
|
||||
local b = callmethod(info, inputpos, tab, "get")
|
||||
local fmt = "|cffffff78- [%s]|r %s"
|
||||
local fmt_sel = "|cffffff78- [%s]|r %s |cffff0000*|r"
|
||||
print(L["Options for |cffffff78"..info[#info].."|r:"])
|
||||
for k, v in pairs(values) do
|
||||
if b == k then
|
||||
print(fmt_sel:format(k, v))
|
||||
else
|
||||
print(fmt:format(k, v))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local ok
|
||||
for k,v in pairs(values) do
|
||||
if strlower(k)==str then
|
||||
str = k -- overwrite with key (in case of case mismatches)
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ok then
|
||||
usererr(info, inputpos, "'"..str.."' - "..L["unknown selection"])
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", str)
|
||||
|
||||
elseif tab.type=="multiselect" then
|
||||
------------ multiselect -------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
|
||||
local values = tab.values
|
||||
if type(values) == "function" or type(values) == "string" then
|
||||
info.values = values
|
||||
values = callmethod(info, inputpos, tab, "values")
|
||||
info.values = nil
|
||||
end
|
||||
|
||||
if str == "" then
|
||||
local fmt = "|cffffff78- [%s]|r %s"
|
||||
local fmt_sel = "|cffffff78- [%s]|r %s |cffff0000*|r"
|
||||
print(L["Options for |cffffff78"..info[#info].."|r (multiple possible):"])
|
||||
for k, v in pairs(values) do
|
||||
if callmethod(info, inputpos, tab, "get", k) then
|
||||
print(fmt_sel:format(k, v))
|
||||
else
|
||||
print(fmt:format(k, v))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
--build a table of the selections, checking that they exist
|
||||
--parse for =on =off =default in the process
|
||||
--table will be key = true for options that should toggle, key = [on|off|default] for options to be set
|
||||
local sels = {}
|
||||
for v in str:gmatch("[^ ]+") do
|
||||
--parse option=on etc
|
||||
local opt, val = v:match('(.+)=(.+)')
|
||||
--get option if toggling
|
||||
if not opt then
|
||||
opt = v
|
||||
end
|
||||
|
||||
--check that the opt is valid
|
||||
local ok
|
||||
for k,v in pairs(values) do
|
||||
if strlower(k)==opt then
|
||||
opt = k -- overwrite with key (in case of case mismatches)
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not ok then
|
||||
usererr(info, inputpos, "'"..opt.."' - "..L["unknown selection"])
|
||||
return
|
||||
end
|
||||
|
||||
--check that if val was supplied it is valid
|
||||
if val then
|
||||
if val == L["on"] or val == L["off"] or (tab.tristate and val == L["default"]) then
|
||||
--val is valid insert it
|
||||
sels[opt] = val
|
||||
else
|
||||
if tab.tristate then
|
||||
usererr(info, inputpos, format(L["'%s' '%s' - expected 'on', 'off' or 'default', or no argument to toggle."], v, val))
|
||||
else
|
||||
usererr(info, inputpos, format(L["'%s' '%s' - expected 'on' or 'off', or no argument to toggle."], v, val))
|
||||
end
|
||||
return
|
||||
end
|
||||
else
|
||||
-- no val supplied, toggle
|
||||
sels[opt] = true
|
||||
end
|
||||
end
|
||||
|
||||
for opt, val in pairs(sels) do
|
||||
local newval
|
||||
|
||||
if (val == true) then
|
||||
--toggle the option
|
||||
local b = callmethod(info, inputpos, tab, "get", opt)
|
||||
|
||||
if tab.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if b then
|
||||
b = nil
|
||||
elseif b == nil then
|
||||
b = false
|
||||
else
|
||||
b = true
|
||||
end
|
||||
else
|
||||
b = not b
|
||||
end
|
||||
newval = b
|
||||
else
|
||||
--set the option as specified
|
||||
if val==L["on"] then
|
||||
newval = true
|
||||
elseif val==L["off"] then
|
||||
newval = false
|
||||
elseif val==L["default"] then
|
||||
newval = nil
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", opt, newval)
|
||||
end
|
||||
|
||||
|
||||
elseif tab.type=="color" then
|
||||
------------ color --------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
if str == "" then
|
||||
--TODO: Show current value
|
||||
return
|
||||
end
|
||||
|
||||
local r, g, b, a
|
||||
|
||||
if tab.hasAlpha then
|
||||
if str:len() == 8 and str:find("^%x*$") then
|
||||
--parse a hex string
|
||||
r,g,b,a = tonumber(str:sub(1, 2), 16) / 255, tonumber(str:sub(3, 4), 16) / 255, tonumber(str:sub(5, 6), 16) / 255, tonumber(str:sub(7, 8), 16) / 255
|
||||
else
|
||||
--parse seperate values
|
||||
r,g,b,a = str:match("^([%d%.]+) ([%d%.]+) ([%d%.]+) ([%d%.]+)$")
|
||||
r,g,b,a = tonumber(r), tonumber(g), tonumber(b), tonumber(a)
|
||||
end
|
||||
if not (r and g and b and a) then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'RRGGBBAA' or 'r g b a'."], str))
|
||||
return
|
||||
end
|
||||
|
||||
if r >= 0.0 and r <= 1.0 and g >= 0.0 and g <= 1.0 and b >= 0.0 and b <= 1.0 and a >= 0.0 and a <= 1.0 then
|
||||
--values are valid
|
||||
elseif r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255 and a >= 0 and a <= 255 then
|
||||
--values are valid 0..255, convert to 0..1
|
||||
r = r / 255
|
||||
g = g / 255
|
||||
b = b / 255
|
||||
a = a / 255
|
||||
else
|
||||
--values are invalid
|
||||
usererr(info, inputpos, format(L["'%s' - values must all be either in the range 0..1 or 0..255."], str))
|
||||
end
|
||||
else
|
||||
a = 1.0
|
||||
if str:len() == 6 and str:find("^%x*$") then
|
||||
--parse a hex string
|
||||
r,g,b = tonumber(str:sub(1, 2), 16) / 255, tonumber(str:sub(3, 4), 16) / 255, tonumber(str:sub(5, 6), 16) / 255
|
||||
else
|
||||
--parse seperate values
|
||||
r,g,b = str:match("^([%d%.]+) ([%d%.]+) ([%d%.]+)$")
|
||||
r,g,b = tonumber(r), tonumber(g), tonumber(b)
|
||||
end
|
||||
if not (r and g and b) then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'RRGGBB' or 'r g b'."], str))
|
||||
return
|
||||
end
|
||||
if r >= 0.0 and r <= 1.0 and g >= 0.0 and g <= 1.0 and b >= 0.0 and b <= 1.0 then
|
||||
--values are valid
|
||||
elseif r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255 then
|
||||
--values are valid 0..255, convert to 0..1
|
||||
r = r / 255
|
||||
g = g / 255
|
||||
b = b / 255
|
||||
else
|
||||
--values are invalid
|
||||
usererr(info, inputpos, format(L["'%s' - values must all be either in the range 0-1 or 0-255."], str))
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", r,g,b,a)
|
||||
|
||||
elseif tab.type=="keybinding" then
|
||||
------------ keybinding --------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
if str == "" then
|
||||
--TODO: Show current value
|
||||
return
|
||||
end
|
||||
local value = keybindingValidateFunc(str:upper())
|
||||
if value == false then
|
||||
usererr(info, inputpos, format(L["'%s' - Invalid Keybinding."], str))
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", value)
|
||||
|
||||
elseif tab.type=="description" then
|
||||
------------ description --------------------
|
||||
-- ignore description, GUI config only
|
||||
else
|
||||
err(info, inputpos, "unknown options table item type '"..tostring(tab.type).."'")
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle the chat command.
|
||||
-- This is usually called from a chat command handler to parse the command input as operations on an aceoptions table.\\
|
||||
-- AceConfigCmd uses this function internally when a slash command is registered with `:CreateChatCommand`
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param input The commandline input (as given by the WoW handler, i.e. without the command itself)
|
||||
-- @usage
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceConsole-3.0")
|
||||
-- -- Use AceConsole-3.0 to register a Chat Command
|
||||
-- MyAddon:RegisterChatCommand("mychat", "ChatCommand")
|
||||
--
|
||||
-- -- Show the GUI if no input is supplied, otherwise handle the chat input.
|
||||
-- function MyAddon:ChatCommand(input)
|
||||
-- -- Assuming "MyOptions" is the appName of a valid options table
|
||||
-- if not input or input:trim() == "" then
|
||||
-- LibStub("AceConfigDialog-3.0"):Open("MyOptions")
|
||||
-- else
|
||||
-- LibStub("AceConfigCmd-3.0").HandleCommand(MyAddon, "mychat", "MyOptions", input)
|
||||
-- end
|
||||
-- end
|
||||
function AceConfigCmd:HandleCommand(slashcmd, appName, input)
|
||||
|
||||
local optgetter = cfgreg:GetOptionsTable(appName)
|
||||
if not optgetter then
|
||||
error([[Usage: HandleCommand("slashcmd", "appName", "input"): 'appName' - no options table "]]..tostring(appName)..[[" has been registered]], 2)
|
||||
end
|
||||
local options = assert( optgetter("cmd", MAJOR) )
|
||||
|
||||
local info = { -- Don't try to recycle this, it gets handed off to callbacks and whatnot
|
||||
[0] = slashcmd,
|
||||
appName = appName,
|
||||
options = options,
|
||||
input = input,
|
||||
self = self,
|
||||
handler = self,
|
||||
uiType = "cmd",
|
||||
uiName = MAJOR,
|
||||
}
|
||||
|
||||
handle(info, 1, options, 0) -- (info, inputpos, table, depth)
|
||||
end
|
||||
|
||||
--- Utility function to create a slash command handler.
|
||||
-- Also registers tab completion with AceTab
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
function AceConfigCmd:CreateChatCommand(slashcmd, appName)
|
||||
if not AceConsole then
|
||||
AceConsole = LibStub(AceConsoleName)
|
||||
end
|
||||
if AceConsole.RegisterChatCommand(self, slashcmd, function(input)
|
||||
AceConfigCmd.HandleCommand(self, slashcmd, appName, input) -- upgradable
|
||||
end,
|
||||
true) then -- succesfully registered so lets get the command -> app table in
|
||||
commands[slashcmd] = appName
|
||||
end
|
||||
end
|
||||
|
||||
--- Utility function that returns the options table that belongs to a slashcommand.
|
||||
-- Designed to be used for the AceTab interface.
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @return The options table associated with the slash command (or nil if the slash command was not registered)
|
||||
function AceConfigCmd:GetChatCommandOptions(slashcmd)
|
||||
return commands[slashcmd]
|
||||
end
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<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="AceConfigCmd-3.0.lua"/>
|
||||
</Ui>
|
||||
+1889
File diff suppressed because it is too large
Load Diff
+4
@@ -0,0 +1,4 @@
|
||||
<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="AceConfigDialog-3.0.lua"/>
|
||||
</Ui>
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
--- AceConfigRegistry-3.0 handles central registration of options tables in use by addons and modules.\\
|
||||
-- Options tables can be registered as raw tables, OR as function refs that return a table.\\
|
||||
-- Such functions receive three arguments: "uiType", "uiName", "appName". \\
|
||||
-- * Valid **uiTypes**: "cmd", "dropdown", "dialog". This is verified by the library at call time. \\
|
||||
-- * The **uiName** field is expected to contain the full name of the calling addon, including version, e.g. "FooBar-1.0". This is verified by the library at call time.\\
|
||||
-- * The **appName** field is the options table name as given at registration time \\
|
||||
--
|
||||
-- :IterateOptionsTables() (and :GetOptionsTable() if only given one argument) return a function reference that the requesting config handling addon must call with valid "uiType", "uiName".
|
||||
-- @class file
|
||||
-- @name AceConfigRegistry-3.0
|
||||
-- @release $Id: AceConfigRegistry-3.0.lua 921 2010-05-09 15:49:14Z nevcairiel $
|
||||
local MAJOR, MINOR = "AceConfigRegistry-3.0", 12
|
||||
local AceConfigRegistry = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigRegistry then return end
|
||||
|
||||
AceConfigRegistry.tables = AceConfigRegistry.tables or {}
|
||||
|
||||
local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
|
||||
|
||||
if not AceConfigRegistry.callbacks then
|
||||
AceConfigRegistry.callbacks = CallbackHandler:New(AceConfigRegistry)
|
||||
end
|
||||
|
||||
-- Lua APIs
|
||||
local tinsert, tconcat = table.insert, table.concat
|
||||
local strfind, strmatch = string.find, string.match
|
||||
local type, tostring, select, pairs = type, tostring, select, pairs
|
||||
local error, assert = error, assert
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Validating options table consistency:
|
||||
|
||||
|
||||
AceConfigRegistry.validated = {
|
||||
-- list of options table names ran through :ValidateOptionsTable automatically.
|
||||
-- CLEARED ON PURPOSE, since newer versions may have newer validators
|
||||
cmd = {},
|
||||
dropdown = {},
|
||||
dialog = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
local function err(msg, errlvl, ...)
|
||||
local t = {}
|
||||
for i=select("#",...),1,-1 do
|
||||
tinsert(t, (select(i, ...)))
|
||||
end
|
||||
error(MAJOR..":ValidateOptionsTable(): "..tconcat(t,".")..msg, errlvl+2)
|
||||
end
|
||||
|
||||
|
||||
local isstring={["string"]=true, _="string"}
|
||||
local isstringfunc={["string"]=true,["function"]=true, _="string or funcref"}
|
||||
local istable={["table"]=true, _="table"}
|
||||
local ismethodtable={["table"]=true,["string"]=true,["function"]=true, _="methodname, funcref or table"}
|
||||
local optstring={["nil"]=true,["string"]=true, _="string"}
|
||||
local optstringfunc={["nil"]=true,["string"]=true,["function"]=true, _="string or funcref"}
|
||||
local optnumber={["nil"]=true,["number"]=true, _="number"}
|
||||
local optmethod={["nil"]=true,["string"]=true,["function"]=true, _="methodname or funcref"}
|
||||
local optmethodfalse={["nil"]=true,["string"]=true,["function"]=true,["boolean"]={[false]=true}, _="methodname, funcref or false"}
|
||||
local optmethodnumber={["nil"]=true,["string"]=true,["function"]=true,["number"]=true, _="methodname, funcref or number"}
|
||||
local optmethodtable={["nil"]=true,["string"]=true,["function"]=true,["table"]=true, _="methodname, funcref or table"}
|
||||
local optmethodbool={["nil"]=true,["string"]=true,["function"]=true,["boolean"]=true, _="methodname, funcref or boolean"}
|
||||
local opttable={["nil"]=true,["table"]=true, _="table"}
|
||||
local optbool={["nil"]=true,["boolean"]=true, _="boolean"}
|
||||
local optboolnumber={["nil"]=true,["boolean"]=true,["number"]=true, _="boolean or number"}
|
||||
|
||||
local basekeys={
|
||||
type=isstring,
|
||||
name=isstringfunc,
|
||||
desc=optstringfunc,
|
||||
descStyle=optstring,
|
||||
order=optmethodnumber,
|
||||
validate=optmethodfalse,
|
||||
confirm=optmethodbool,
|
||||
confirmText=optstring,
|
||||
disabled=optmethodbool,
|
||||
hidden=optmethodbool,
|
||||
guiHidden=optmethodbool,
|
||||
dialogHidden=optmethodbool,
|
||||
dropdownHidden=optmethodbool,
|
||||
cmdHidden=optmethodbool,
|
||||
icon=optstringfunc,
|
||||
iconCoords=optmethodtable,
|
||||
handler=opttable,
|
||||
get=optmethodfalse,
|
||||
set=optmethodfalse,
|
||||
func=optmethodfalse,
|
||||
arg={["*"]=true},
|
||||
width=optstring,
|
||||
}
|
||||
|
||||
local typedkeys={
|
||||
header={},
|
||||
description={
|
||||
image=optstringfunc,
|
||||
imageCoords=optmethodtable,
|
||||
imageHeight=optnumber,
|
||||
imageWidth=optnumber,
|
||||
fontSize=optstringfunc,
|
||||
},
|
||||
group={
|
||||
args=istable,
|
||||
plugins=opttable,
|
||||
inline=optbool,
|
||||
cmdInline=optbool,
|
||||
guiInline=optbool,
|
||||
dropdownInline=optbool,
|
||||
dialogInline=optbool,
|
||||
childGroups=optstring,
|
||||
},
|
||||
execute={
|
||||
image=optstringfunc,
|
||||
imageCoords=optmethodtable,
|
||||
imageHeight=optnumber,
|
||||
imageWidth=optnumber,
|
||||
},
|
||||
input={
|
||||
pattern=optstring,
|
||||
usage=optstring,
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
multiline=optboolnumber,
|
||||
},
|
||||
toggle={
|
||||
tristate=optbool,
|
||||
image=optstringfunc,
|
||||
imageCoords=optmethodtable,
|
||||
},
|
||||
tristate={
|
||||
},
|
||||
range={
|
||||
min=optnumber,
|
||||
softMin=optnumber,
|
||||
max=optnumber,
|
||||
softMax=optnumber,
|
||||
step=optnumber,
|
||||
bigStep=optnumber,
|
||||
isPercent=optbool,
|
||||
},
|
||||
select={
|
||||
values=ismethodtable,
|
||||
style={
|
||||
["nil"]=true,
|
||||
["string"]={dropdown=true,radio=true},
|
||||
_="string: 'dropdown' or 'radio'"
|
||||
},
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
},
|
||||
multiselect={
|
||||
values=ismethodtable,
|
||||
style=optstring,
|
||||
tristate=optbool,
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
},
|
||||
color={
|
||||
hasAlpha=optbool,
|
||||
},
|
||||
keybinding={
|
||||
-- TODO
|
||||
},
|
||||
}
|
||||
|
||||
local function validateKey(k,errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
if type(k)~="string" then
|
||||
err("["..tostring(k).."] - key is not a string", errlvl,...)
|
||||
end
|
||||
if strfind(k, "[%c\127]") then
|
||||
err("["..tostring(k).."] - key name contained control characters", errlvl,...)
|
||||
end
|
||||
end
|
||||
|
||||
local function validateVal(v, oktypes, errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
local isok=oktypes[type(v)] or oktypes["*"]
|
||||
|
||||
if not isok then
|
||||
err(": expected a "..oktypes._..", got '"..tostring(v).."'", errlvl,...)
|
||||
end
|
||||
if type(isok)=="table" then -- isok was a table containing specific values to be tested for!
|
||||
if not isok[v] then
|
||||
err(": did not expect "..type(v).." value '"..tostring(v).."'", errlvl,...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function validate(options,errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
-- basic consistency
|
||||
if type(options)~="table" then
|
||||
err(": expected a table, got a "..type(options), errlvl,...)
|
||||
end
|
||||
if type(options.type)~="string" then
|
||||
err(".type: expected a string, got a "..type(options.type), errlvl,...)
|
||||
end
|
||||
|
||||
-- get type and 'typedkeys' member
|
||||
local tk = typedkeys[options.type]
|
||||
if not tk then
|
||||
err(".type: unknown type '"..options.type.."'", errlvl,...)
|
||||
end
|
||||
|
||||
-- make sure that all options[] are known parameters
|
||||
for k,v in pairs(options) do
|
||||
if not (tk[k] or basekeys[k]) then
|
||||
err(": unknown parameter", errlvl,tostring(k),...)
|
||||
end
|
||||
end
|
||||
|
||||
-- verify that required params are there, and that everything is the right type
|
||||
for k,oktypes in pairs(basekeys) do
|
||||
validateVal(options[k], oktypes, errlvl,k,...)
|
||||
end
|
||||
for k,oktypes in pairs(tk) do
|
||||
validateVal(options[k], oktypes, errlvl,k,...)
|
||||
end
|
||||
|
||||
-- extra logic for groups
|
||||
if options.type=="group" then
|
||||
for k,v in pairs(options.args) do
|
||||
validateKey(k,errlvl,"args",...)
|
||||
validate(v, errlvl,k,"args",...)
|
||||
end
|
||||
if options.plugins then
|
||||
for plugname,plugin in pairs(options.plugins) do
|
||||
if type(plugin)~="table" then
|
||||
err(": expected a table, got '"..tostring(plugin).."'", errlvl,tostring(plugname),"plugins",...)
|
||||
end
|
||||
for k,v in pairs(plugin) do
|
||||
validateKey(k,errlvl,tostring(plugname),"plugins",...)
|
||||
validate(v, errlvl,k,tostring(plugname),"plugins",...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Validates basic structure and integrity of an options table \\
|
||||
-- Does NOT verify that get/set etc actually exist, since they can be defined at any depth
|
||||
-- @param options The table to be validated
|
||||
-- @param name The name of the table to be validated (shown in any error message)
|
||||
-- @param errlvl (optional number) error level offset, default 0 (=errors point to the function calling :ValidateOptionsTable)
|
||||
function AceConfigRegistry:ValidateOptionsTable(options,name,errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
name = name or "Optionstable"
|
||||
if not options.name then
|
||||
options.name=name -- bit of a hack, the root level doesn't really need a .name :-/
|
||||
end
|
||||
validate(options,errlvl,name)
|
||||
end
|
||||
|
||||
--- Fires a "ConfigTableChange" callback for those listening in on it, allowing config GUIs to refresh.
|
||||
-- You should call this function if your options table changed from any outside event, like a game event
|
||||
-- or a timer.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
function AceConfigRegistry:NotifyChange(appName)
|
||||
if not AceConfigRegistry.tables[appName] then return end
|
||||
AceConfigRegistry.callbacks:Fire("ConfigTableChange", appName)
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Registering and retreiving options tables:
|
||||
|
||||
|
||||
-- validateGetterArgs: helper function for :GetOptionsTable (or, rather, the getter functions returned by it)
|
||||
|
||||
local function validateGetterArgs(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+2
|
||||
if uiType~="cmd" and uiType~="dropdown" and uiType~="dialog" then
|
||||
error(MAJOR..": Requesting options table: 'uiType' - invalid configuration UI type, expected 'cmd', 'dropdown' or 'dialog'", errlvl)
|
||||
end
|
||||
if not strmatch(uiName, "[A-Za-z]%-[0-9]") then -- Expecting e.g. "MyLib-1.2"
|
||||
error(MAJOR..": Requesting options table: 'uiName' - badly formatted or missing version number. Expected e.g. 'MyLib-1.2'", errlvl)
|
||||
end
|
||||
end
|
||||
|
||||
--- Register an options table with the config registry.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param options The options table, OR a function reference that generates it on demand. \\
|
||||
-- See the top of the page for info on arguments passed to such functions.
|
||||
function AceConfigRegistry:RegisterOptionsTable(appName, options)
|
||||
if type(options)=="table" then
|
||||
if options.type~="group" then -- quick sanity checker
|
||||
error(MAJOR..": RegisterOptionsTable(appName, options): 'options' - missing type='group' member in root group", 2)
|
||||
end
|
||||
AceConfigRegistry.tables[appName] = function(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
validateGetterArgs(uiType, uiName, errlvl)
|
||||
if not AceConfigRegistry.validated[uiType][appName] then
|
||||
AceConfigRegistry:ValidateOptionsTable(options, appName, errlvl) -- upgradable
|
||||
AceConfigRegistry.validated[uiType][appName] = true
|
||||
end
|
||||
return options
|
||||
end
|
||||
elseif type(options)=="function" then
|
||||
AceConfigRegistry.tables[appName] = function(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
validateGetterArgs(uiType, uiName, errlvl)
|
||||
local tab = assert(options(uiType, uiName, appName))
|
||||
if not AceConfigRegistry.validated[uiType][appName] then
|
||||
AceConfigRegistry:ValidateOptionsTable(tab, appName, errlvl) -- upgradable
|
||||
AceConfigRegistry.validated[uiType][appName] = true
|
||||
end
|
||||
return tab
|
||||
end
|
||||
else
|
||||
error(MAJOR..": RegisterOptionsTable(appName, options): 'options' - expected table or function reference", 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns an iterator of ["appName"]=funcref pairs
|
||||
function AceConfigRegistry:IterateOptionsTables()
|
||||
return pairs(AceConfigRegistry.tables)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
--- Query the registry for a specific options table.
|
||||
-- If only appName is given, a function is returned which you
|
||||
-- can call with (uiType,uiName) to get the table.\\
|
||||
-- If uiType&uiName are given, the table is returned.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param uiType The type of UI to get the table for, one of "cmd", "dropdown", "dialog"
|
||||
-- @param uiName The name of the library/addon querying for the table, e.g. "MyLib-1.0"
|
||||
function AceConfigRegistry:GetOptionsTable(appName, uiType, uiName)
|
||||
local f = AceConfigRegistry.tables[appName]
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
|
||||
if uiType then
|
||||
return f(uiType,uiName,1) -- get the table for us
|
||||
else
|
||||
return f -- return the function
|
||||
end
|
||||
end
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<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="AceConfigRegistry-3.0.lua"/>
|
||||
</Ui>
|
||||
+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)
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
--- 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 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
|
||||
|
||||
-- Lua APIs
|
||||
local pairs, next = pairs, next
|
||||
|
||||
-- WoW APIs
|
||||
local UnitClass = UnitClass
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: NORMAL_FONT_COLOR_CODE, FONT_COLOR_CODE_CLOSE
|
||||
|
||||
AceDBOptions.optionTables = AceDBOptions.optionTables or {}
|
||||
AceDBOptions.handlers = AceDBOptions.handlers or {}
|
||||
|
||||
--[[
|
||||
Localization of AceDBOptions-3.0
|
||||
]]
|
||||
|
||||
local L = {
|
||||
default = "Default",
|
||||
intro = "You can change the active database profile, so you can have different settings for every character.",
|
||||
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 exisiting profiles.",
|
||||
new = "New",
|
||||
new_sub = "Create a new empty profile.",
|
||||
choose = "Existing Profiles",
|
||||
choose_sub = "Select one of your currently available profiles.",
|
||||
copy_desc = "Copy the settings from one existing profile into the currently active profile.",
|
||||
copy = "Copy From",
|
||||
delete_desc = "Delete existing and unused profiles from the database to save space, and cleanup the SavedVariables file.",
|
||||
delete = "Delete a Profile",
|
||||
delete_sub = "Deletes a profile from the database.",
|
||||
delete_confirm = "Are you sure you want to delete the selected profile?",
|
||||
profiles = "Profiles",
|
||||
profiles_sub = "Manage Profiles",
|
||||
current = "Current Profile:",
|
||||
}
|
||||
|
||||
local LOCALE = GetLocale()
|
||||
if LOCALE == "deDE" then
|
||||
L["default"] = "Standard"
|
||||
L["intro"] = "Hier kannst du das aktive Datenbankprofile \195\164ndern, damit du verschiedene Einstellungen f\195\188r jeden Charakter erstellen kannst, wodurch eine sehr flexible Konfiguration m\195\182glich wird."
|
||||
L["reset_desc"] = "Setzt das momentane Profil auf Standardwerte zur\195\188ck, f\195\188r den Fall das mit der Konfiguration etwas schief lief oder weil du einfach neu starten willst."
|
||||
L["reset"] = "Profil zur\195\188cksetzen"
|
||||
L["reset_sub"] = "Das aktuelle Profil auf Standard zur\195\188cksetzen."
|
||||
L["choose_desc"] = "Du kannst ein neues Profil erstellen, indem du einen neuen Namen in der Eingabebox 'Neu' eingibst, oder w\195\164hle eines der vorhandenen Profile aus."
|
||||
L["new"] = "Neu"
|
||||
L["new_sub"] = "Ein neues Profil erstellen."
|
||||
L["choose"] = "Vorhandene Profile"
|
||||
L["choose_sub"] = "W\195\164hlt ein bereits vorhandenes Profil aus."
|
||||
L["copy_desc"] = "Kopiere die Einstellungen von einem vorhandenen Profil in das aktive Profil."
|
||||
L["copy"] = "Kopieren von..."
|
||||
L["delete_desc"] = "L\195\182sche vorhandene oder unbenutzte Profile aus der Datenbank um Platz zu sparen und um die SavedVariables Datei 'sauber' zu halten."
|
||||
L["delete"] = "Profil l\195\182schen"
|
||||
L["delete_sub"] = "L\195\182scht ein Profil aus der Datenbank."
|
||||
L["delete_confirm"] = "Willst du das ausgew\195\164hlte Profil wirklich l\195\182schen?"
|
||||
L["profiles"] = "Profile"
|
||||
L["profiles_sub"] = "Profile verwalten"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "frFR" then
|
||||
L["default"] = "D\195\169faut"
|
||||
L["intro"] = "Vous pouvez changer le profil actuel afin d'avoir des param\195\168tres diff\195\169rents pour chaque personnage, permettant ainsi d'avoir une configuration tr\195\168s flexible."
|
||||
L["reset_desc"] = "R\195\169initialise le profil actuel au cas o\195\185 votre configuration est corrompue ou si vous voulez tout simplement faire table rase."
|
||||
L["reset"] = "R\195\169initialiser le profil"
|
||||
L["reset_sub"] = "R\195\169initialise le profil actuel avec les param\195\168tres par d\195\169faut."
|
||||
L["choose_desc"] = "Vous pouvez cr\195\169er un nouveau profil en entrant un nouveau nom dans la bo\195\174te de saisie, ou en choississant un des profils d\195\169j\195\160 existants."
|
||||
L["new"] = "Nouveau"
|
||||
L["new_sub"] = "Cr\195\169\195\169e un nouveau profil vierge."
|
||||
L["choose"] = "Profils existants"
|
||||
L["choose_sub"] = "Permet de choisir un des profils d\195\169j\195\160 disponibles."
|
||||
L["copy_desc"] = "Copie les param\195\168tres d'un profil d\195\169j\195\160 existant dans le profil actuellement actif."
|
||||
L["copy"] = "Copier \195\160 partir de"
|
||||
L["delete_desc"] = "Supprime les profils existants inutilis\195\169s de la base de donn\195\169es afin de gagner de la place et de nettoyer le fichier SavedVariables."
|
||||
L["delete"] = "Supprimer un profil"
|
||||
L["delete_sub"] = "Supprime un profil de la base de donn\195\169es."
|
||||
L["delete_confirm"] = "Etes-vous s\195\187r de vouloir supprimer le profil s\195\169lectionn\195\169 ?"
|
||||
L["profiles"] = "Profils"
|
||||
L["profiles_sub"] = "Gestion des profils"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "koKR" then
|
||||
L["default"] = "기본값"
|
||||
L["intro"] = "모든 캐릭터의 다양한 설정과 사용중인 데이터베이스 프로필, 어느것이던지 매우 다루기 쉽게 바꿀수 있습니다."
|
||||
L["reset_desc"] = "단순히 다시 새롭게 구성을 원하는 경우, 현재 프로필을 기본값으로 초기화 합니다."
|
||||
L["reset"] = "프로필 초기화"
|
||||
L["reset_sub"] = "현재의 프로필을 기본값으로 초기화 합니다"
|
||||
L["choose_desc"] = "새로운 이름을 입력하거나, 이미 있는 프로필중 하나를 선택하여 새로운 프로필을 만들 수 있습니다."
|
||||
L["new"] = "새로운 프로필"
|
||||
L["new_sub"] = "새로운 프로필을 만듭니다."
|
||||
L["choose"] = "프로필 선택"
|
||||
L["choose_sub"] = "당신이 현재 이용할수 있는 프로필을 선택합니다."
|
||||
L["copy_desc"] = "현재 사용중인 프로필에, 선택한 프로필의 설정을 복사합니다."
|
||||
L["copy"] = "복사"
|
||||
L["delete_desc"] = "데이터베이스에 사용중이거나 저장된 프로파일 삭제로 SavedVariables 파일의 정리와 공간 절약이 됩니다."
|
||||
L["delete"] = "프로필 삭제"
|
||||
L["delete_sub"] = "데이터베이스의 프로필을 삭제합니다."
|
||||
L["delete_confirm"] = "정말로 선택한 프로필의 삭제를 원하십니까?"
|
||||
L["profiles"] = "프로필"
|
||||
L["profiles_sub"] = "프로필 설정"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "esES" or LOCALE == "esMX" then
|
||||
L["default"] = "Por defecto"
|
||||
L["intro"] = "Puedes cambiar el perfil activo de tal manera que cada personaje tenga diferentes configuraciones."
|
||||
L["reset_desc"] = "Reinicia el perfil actual a los valores por defectos, en caso de que se haya estropeado la configuración o quieras volver a empezar de nuevo."
|
||||
L["reset"] = "Reiniciar Perfil"
|
||||
L["reset_sub"] = "Reinicar el perfil actual al de por defecto"
|
||||
L["choose_desc"] = "Puedes crear un nuevo perfil introduciendo un nombre en el recuadro o puedes seleccionar un perfil de los ya existentes."
|
||||
L["new"] = "Nuevo"
|
||||
L["new_sub"] = "Crear un nuevo perfil vacio."
|
||||
L["choose"] = "Perfiles existentes"
|
||||
L["choose_sub"] = "Selecciona uno de los perfiles disponibles."
|
||||
L["copy_desc"] = "Copia los ajustes de un perfil existente al perfil actual."
|
||||
L["copy"] = "Copiar de"
|
||||
L["delete_desc"] = "Borra los perfiles existentes y sin uso de la base de datos para ganar espacio y limpiar el archivo SavedVariables."
|
||||
L["delete"] = "Borrar un Perfil"
|
||||
L["delete_sub"] = "Borra un perfil de la base de datos."
|
||||
L["delete_confirm"] = "¿Estas seguro que quieres borrar el perfil seleccionado?"
|
||||
L["profiles"] = "Perfiles"
|
||||
L["profiles_sub"] = "Manejar Perfiles"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "zhTW" then
|
||||
L["default"] = "預設"
|
||||
L["intro"] = "你可以選擇一個活動的資料設定檔,這樣你的每個角色就可以擁有不同的設定值,可以給你的插件設定帶來極大的靈活性。"
|
||||
L["reset_desc"] = "將當前的設定檔恢復到它的預設值,用於你的設定檔損壞,或者你只是想重來的情況。"
|
||||
L["reset"] = "重置設定檔"
|
||||
L["reset_sub"] = "將當前的設定檔恢復為預設值"
|
||||
L["choose_desc"] = "你可以通過在文本框內輸入一個名字創立一個新的設定檔,也可以選擇一個已經存在的設定檔。"
|
||||
L["new"] = "新建"
|
||||
L["new_sub"] = "新建一個空的設定檔。"
|
||||
L["choose"] = "現有的設定檔"
|
||||
L["choose_sub"] = "從當前可用的設定檔裏面選擇一個。"
|
||||
L["copy_desc"] = "從當前某個已保存的設定檔複製到當前正使用的設定檔。"
|
||||
L["copy"] = "複製自"
|
||||
L["delete_desc"] = "從資料庫裏刪除不再使用的設定檔,以節省空間,並且清理SavedVariables檔。"
|
||||
L["delete"] = "刪除一個設定檔"
|
||||
L["delete_sub"] = "從資料庫裏刪除一個設定檔。"
|
||||
L["delete_confirm"] = "你確定要刪除所選擇的設定檔嗎?"
|
||||
L["profiles"] = "設定檔"
|
||||
L["profiles_sub"] = "管理設定檔"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "zhCN" then
|
||||
L["default"] = "默认"
|
||||
L["intro"] = "你可以选择一个活动的数据配置文件,这样你的每个角色就可以拥有不同的设置值,可以给你的插件配置带来极大的灵活性。"
|
||||
L["reset_desc"] = "将当前的配置文件恢复到它的默认值,用于你的配置文件损坏,或者你只是想重来的情况。"
|
||||
L["reset"] = "重置配置文件"
|
||||
L["reset_sub"] = "将当前的配置文件恢复为默认值"
|
||||
L["choose_desc"] = "你可以通过在文本框内输入一个名字创立一个新的配置文件,也可以选择一个已经存在的配置文件。"
|
||||
L["new"] = "新建"
|
||||
L["new_sub"] = "新建一个空的配置文件。"
|
||||
L["choose"] = "现有的配置文件"
|
||||
L["choose_sub"] = "从当前可用的配置文件里面选择一个。"
|
||||
L["copy_desc"] = "从当前某个已保存的配置文件复制到当前正使用的配置文件。"
|
||||
L["copy"] = "复制自"
|
||||
L["delete_desc"] = "从数据库里删除不再使用的配置文件,以节省空间,并且清理SavedVariables文件。"
|
||||
L["delete"] = "删除一个配置文件"
|
||||
L["delete_sub"] = "从数据库里删除一个配置文件。"
|
||||
L["delete_confirm"] = "你确定要删除所选择的配置文件么?"
|
||||
L["profiles"] = "配置文件"
|
||||
L["profiles_sub"] = "管理配置文件"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "ruRU" then
|
||||
L["default"] = "По умолчанию"
|
||||
L["intro"] = "Изменяя активный профиль, вы можете задать различные настройки модификаций для каждого персонажа."
|
||||
L["reset_desc"] = "Если ваша конфигурации испорчена или если вы хотите настроить всё заново - сбросьте текущий профиль на стандартные значения."
|
||||
L["reset"] = "Сброс профиля"
|
||||
L["reset_sub"] = "Сброс текущего профиля на стандартный"
|
||||
L["choose_desc"] = "Вы можете создать новый профиль, введя название в поле ввода, или выбрать один из уже существующих профилей."
|
||||
L["new"] = "Новый"
|
||||
L["new_sub"] = "Создать новый чистый профиль"
|
||||
L["choose"] = "Существующие профили"
|
||||
L["choose_sub"] = "Выбор одиного из уже доступных профилей"
|
||||
L["copy_desc"] = "Скопировать настройки из выбранного профиля в активный."
|
||||
L["copy"] = "Скопировать из"
|
||||
L["delete_desc"] = "Удалить существующий и неиспользуемый профиль из БД для сохранения места, и очистить SavedVariables файл."
|
||||
L["delete"] = "Удалить профиль"
|
||||
L["delete_sub"] = "Удаление профиля из БД"
|
||||
L["delete_confirm"] = "Вы уверены, что вы хотите удалить выбранный профиль?"
|
||||
L["profiles"] = "Профили"
|
||||
L["profiles_sub"] = "Управление профилями"
|
||||
--L["current"] = "Current Profile:"
|
||||
end
|
||||
|
||||
local defaultProfiles
|
||||
local tmpprofiles = {}
|
||||
|
||||
-- Get a list of available profiles for the specified database.
|
||||
-- You can specify which profiles to include/exclude in the list using the two boolean parameters listed below.
|
||||
-- @param db The db object to retrieve the profiles from
|
||||
-- @param common If true, getProfileList will add the default profiles to the return list, even if they have not been created yet
|
||||
-- @param nocurrent If true, then getProfileList will not display the current profile in the list
|
||||
-- @return Hashtable of all profiles with the internal name as keys and the display name as value.
|
||||
local function getProfileList(db, common, nocurrent)
|
||||
local profiles = {}
|
||||
|
||||
-- copy existing profiles into the table
|
||||
local currentProfile = db:GetCurrentProfile()
|
||||
for i,v in pairs(db:GetProfiles(tmpprofiles)) do
|
||||
if not (nocurrent and v == currentProfile) then
|
||||
profiles[v] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- add our default profiles to choose from ( or rename existing profiles)
|
||||
for k,v in pairs(defaultProfiles) do
|
||||
if (common or profiles[k]) and not (nocurrent and k == currentProfile) then
|
||||
profiles[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return profiles
|
||||
end
|
||||
|
||||
--[[
|
||||
OptionsHandlerPrototype
|
||||
prototype class for handling the options in a sane way
|
||||
]]
|
||||
local OptionsHandlerPrototype = {}
|
||||
|
||||
--[[ Reset the profile ]]
|
||||
function OptionsHandlerPrototype:Reset()
|
||||
self.db:ResetProfile()
|
||||
end
|
||||
|
||||
--[[ Set the profile to value ]]
|
||||
function OptionsHandlerPrototype:SetProfile(info, value)
|
||||
self.db:SetProfile(value)
|
||||
end
|
||||
|
||||
--[[ returns the currently active profile ]]
|
||||
function OptionsHandlerPrototype:GetCurrentProfile()
|
||||
return self.db:GetCurrentProfile()
|
||||
end
|
||||
|
||||
--[[
|
||||
List all active profiles
|
||||
you can control the output with the .arg variable
|
||||
currently four modes are supported
|
||||
|
||||
(empty) - return all available profiles
|
||||
"nocurrent" - returns all available profiles except the currently active profile
|
||||
"common" - returns all avaialble profiles + some commonly used profiles ("char - realm", "realm", "class", "Default")
|
||||
"both" - common except the active profile
|
||||
]]
|
||||
function OptionsHandlerPrototype:ListProfiles(info)
|
||||
local arg = info.arg
|
||||
local profiles
|
||||
if arg == "common" and not self.noDefaultProfiles then
|
||||
profiles = getProfileList(self.db, true, nil)
|
||||
elseif arg == "nocurrent" then
|
||||
profiles = getProfileList(self.db, nil, true)
|
||||
elseif arg == "both" then -- currently not used
|
||||
profiles = getProfileList(self.db, (not self.noDefaultProfiles) and true, true)
|
||||
else
|
||||
profiles = getProfileList(self.db)
|
||||
end
|
||||
|
||||
return profiles
|
||||
end
|
||||
|
||||
function OptionsHandlerPrototype:HasNoProfiles(info)
|
||||
local profiles = self:ListProfiles(info)
|
||||
return ((not next(profiles)) and true or false)
|
||||
end
|
||||
|
||||
--[[ Copy a profile ]]
|
||||
function OptionsHandlerPrototype:CopyProfile(info, value)
|
||||
self.db:CopyProfile(value)
|
||||
end
|
||||
|
||||
--[[ Delete a profile from the db ]]
|
||||
function OptionsHandlerPrototype:DeleteProfile(info, value)
|
||||
self.db:DeleteProfile(value)
|
||||
end
|
||||
|
||||
--[[ fill defaultProfiles with some generic values ]]
|
||||
local function generateDefaultProfiles(db)
|
||||
defaultProfiles = {
|
||||
["Default"] = L["default"],
|
||||
[db.keys.char] = db.keys.char,
|
||||
[db.keys.realm] = db.keys.realm,
|
||||
[db.keys.class] = UnitClass("player")
|
||||
}
|
||||
end
|
||||
|
||||
--[[ create and return a handler object for the db, or upgrade it if it already existed ]]
|
||||
local function getOptionsHandler(db, noDefaultProfiles)
|
||||
if not defaultProfiles then
|
||||
generateDefaultProfiles(db)
|
||||
end
|
||||
|
||||
local handler = AceDBOptions.handlers[db] or { db = db, noDefaultProfiles = noDefaultProfiles }
|
||||
|
||||
for k,v in pairs(OptionsHandlerPrototype) do
|
||||
handler[k] = v
|
||||
end
|
||||
|
||||
AceDBOptions.handlers[db] = handler
|
||||
return handler
|
||||
end
|
||||
|
||||
--[[
|
||||
the real options table
|
||||
]]
|
||||
local optionsTable = {
|
||||
desc = {
|
||||
order = 1,
|
||||
type = "description",
|
||||
name = L["intro"] .. "\n",
|
||||
},
|
||||
descreset = {
|
||||
order = 9,
|
||||
type = "description",
|
||||
name = L["reset_desc"],
|
||||
},
|
||||
reset = {
|
||||
order = 10,
|
||||
type = "execute",
|
||||
name = L["reset"],
|
||||
desc = L["reset_sub"],
|
||||
func = "Reset",
|
||||
},
|
||||
current = {
|
||||
order = 11,
|
||||
type = "description",
|
||||
name = function(info) return L["current"] .. " " .. NORMAL_FONT_COLOR_CODE .. info.handler:GetCurrentProfile() .. FONT_COLOR_CODE_CLOSE end,
|
||||
width = "default",
|
||||
},
|
||||
choosedesc = {
|
||||
order = 20,
|
||||
type = "description",
|
||||
name = "\n" .. L["choose_desc"],
|
||||
},
|
||||
new = {
|
||||
name = L["new"],
|
||||
desc = L["new_sub"],
|
||||
type = "input",
|
||||
order = 30,
|
||||
get = false,
|
||||
set = "SetProfile",
|
||||
},
|
||||
choose = {
|
||||
name = L["choose"],
|
||||
desc = L["choose_sub"],
|
||||
type = "select",
|
||||
order = 40,
|
||||
get = "GetCurrentProfile",
|
||||
set = "SetProfile",
|
||||
values = "ListProfiles",
|
||||
arg = "common",
|
||||
},
|
||||
copydesc = {
|
||||
order = 50,
|
||||
type = "description",
|
||||
name = "\n" .. L["copy_desc"],
|
||||
},
|
||||
copyfrom = {
|
||||
order = 60,
|
||||
type = "select",
|
||||
name = L["copy"],
|
||||
desc = L["copy_desc"],
|
||||
get = false,
|
||||
set = "CopyProfile",
|
||||
values = "ListProfiles",
|
||||
disabled = "HasNoProfiles",
|
||||
arg = "nocurrent",
|
||||
},
|
||||
deldesc = {
|
||||
order = 70,
|
||||
type = "description",
|
||||
name = "\n" .. L["delete_desc"],
|
||||
},
|
||||
delete = {
|
||||
order = 80,
|
||||
type = "select",
|
||||
name = L["delete"],
|
||||
desc = L["delete_sub"],
|
||||
get = false,
|
||||
set = "DeleteProfile",
|
||||
values = "ListProfiles",
|
||||
disabled = "HasNoProfiles",
|
||||
arg = "nocurrent",
|
||||
confirm = true,
|
||||
confirmText = L["delete_confirm"],
|
||||
},
|
||||
}
|
||||
|
||||
--- Get/Create a option table that you can use in your addon to control the profiles of AceDB-3.0.
|
||||
-- @param db The database object to create the options table for.
|
||||
-- @return The options table to be used in AceConfig-3.0
|
||||
-- @usage
|
||||
-- -- Assuming `options` is your top-level options table and `self.db` is your database:
|
||||
-- options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
|
||||
function AceDBOptions:GetOptionsTable(db, noDefaultProfiles)
|
||||
local tbl = AceDBOptions.optionTables[db] or {
|
||||
type = "group",
|
||||
name = L["profiles"],
|
||||
desc = L["profiles_sub"],
|
||||
}
|
||||
|
||||
tbl.handler = getOptionsHandler(db, noDefaultProfiles)
|
||||
tbl.args = optionsTable
|
||||
|
||||
AceDBOptions.optionTables[db] = tbl
|
||||
return tbl
|
||||
end
|
||||
|
||||
-- upgrade existing tables
|
||||
for db,tbl in pairs(AceDBOptions.optionTables) do
|
||||
tbl.handler = getOptionsHandler(db)
|
||||
tbl.args = optionsTable
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
<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="AceDBOptions-3.0.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,876 @@
|
||||
--- **AceGUI-3.0** provides access to numerous widgets which can be used to create GUIs.
|
||||
-- AceGUI is used by AceConfigDialog to create the option GUIs, but you can use it by itself
|
||||
-- to create any custom GUI. There are more extensive examples in the test suite in the Ace3
|
||||
-- stand-alone distribution.
|
||||
--
|
||||
-- **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
|
||||
-- implement a proper API to modify it.
|
||||
-- @usage
|
||||
-- local AceGUI = LibStub("AceGUI-3.0")
|
||||
-- -- Create a container frame
|
||||
-- local f = AceGUI:Create("Frame")
|
||||
-- f:SetCallback("OnClose",function(widget) AceGUI:Release(widget) end)
|
||||
-- f:SetTitle("AceGUI-3.0 Example")
|
||||
-- f:SetStatusText("Status Bar")
|
||||
-- f:SetLayout("Flow")
|
||||
-- -- Create a button
|
||||
-- local btn = AceGUI:Create("Button")
|
||||
-- btn:SetWidth(170)
|
||||
-- btn:SetText("Button !")
|
||||
-- btn:SetCallback("OnClick", function() print("Click!") end)
|
||||
-- -- Add the button to the container
|
||||
-- f:AddChild(btn)
|
||||
-- @class file
|
||||
-- @name AceGUI-3.0
|
||||
-- @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
|
||||
|
||||
-- Lua APIs
|
||||
local tconcat, tremove, tinsert = table.concat, table.remove, table.insert
|
||||
local select, pairs, next, type = select, pairs, next, type
|
||||
local error, assert, loadstring = error, assert, loadstring
|
||||
local setmetatable, rawget, rawset = setmetatable, rawget, rawset
|
||||
local math_max = math.max
|
||||
|
||||
-- WoW APIs
|
||||
local UIParent = UIParent
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: geterrorhandler, LibStub
|
||||
|
||||
--local con = LibStub("AceConsole-3.0",true)
|
||||
|
||||
AceGUI.WidgetRegistry = AceGUI.WidgetRegistry or {}
|
||||
AceGUI.LayoutRegistry = AceGUI.LayoutRegistry or {}
|
||||
AceGUI.WidgetBase = AceGUI.WidgetBase or {}
|
||||
AceGUI.WidgetContainerBase = AceGUI.WidgetContainerBase or {}
|
||||
AceGUI.WidgetVersions = AceGUI.WidgetVersions or {}
|
||||
|
||||
-- local upvalues
|
||||
local WidgetRegistry = AceGUI.WidgetRegistry
|
||||
local LayoutRegistry = AceGUI.LayoutRegistry
|
||||
local WidgetVersions = AceGUI.WidgetVersions
|
||||
|
||||
--[[
|
||||
xpcall safecall implementation
|
||||
]]
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local xpcall, eh = ...
|
||||
local method, ARGS
|
||||
local function call() return method(ARGS) end
|
||||
|
||||
local function dispatch(func, ...)
|
||||
method = func
|
||||
if not method then return end
|
||||
ARGS = ...
|
||||
return xpcall(call, eh)
|
||||
end
|
||||
|
||||
return dispatch
|
||||
]]
|
||||
|
||||
local ARGS = {}
|
||||
for i = 1, argCount do ARGS[i] = "arg"..i end
|
||||
code = code:gsub("ARGS", tconcat(ARGS, ", "))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
Dispatchers[0] = function(func)
|
||||
return xpcall(func, errorhandler)
|
||||
end
|
||||
|
||||
local function safecall(func, ...)
|
||||
return Dispatchers[select('#', ...)](func, ...)
|
||||
end
|
||||
|
||||
-- Recycling functions
|
||||
local newWidget, delWidget
|
||||
do
|
||||
-- Version Upgrade in Minor 29
|
||||
-- Internal Storage of the objects changed, from an array table
|
||||
-- to a hash table, and additionally we introduced versioning on
|
||||
-- the widgets which would discard all widgets from a pre-29 version
|
||||
-- anyway, so we just clear the storage now, and don't try to
|
||||
-- convert the storage tables to the new format.
|
||||
-- This should generally not cause *many* widgets to end up in trash,
|
||||
-- since once dialogs are opened, all addons should be loaded already
|
||||
-- and AceGUI should be on the latest version available on the users
|
||||
-- setup.
|
||||
-- -- nevcairiel - Nov 2nd, 2009
|
||||
if oldminor and oldminor < 29 and AceGUI.objPools then
|
||||
AceGUI.objPools = nil
|
||||
end
|
||||
|
||||
AceGUI.objPools = AceGUI.objPools or {}
|
||||
local objPools = AceGUI.objPools
|
||||
--Returns a new instance, if none are available either returns a new table or calls the given contructor
|
||||
function newWidget(type)
|
||||
if not WidgetRegistry[type] then
|
||||
error("Attempt to instantiate unknown widget type", 2)
|
||||
end
|
||||
|
||||
if not objPools[type] then
|
||||
objPools[type] = {}
|
||||
end
|
||||
|
||||
local newObj = next(objPools[type])
|
||||
if not newObj then
|
||||
newObj = WidgetRegistry[type]()
|
||||
newObj.AceGUIWidgetVersion = WidgetVersions[type]
|
||||
else
|
||||
objPools[type][newObj] = nil
|
||||
-- if the widget is older then the latest, don't even try to reuse it
|
||||
-- just forget about it, and grab a new one.
|
||||
if not newObj.AceGUIWidgetVersion or newObj.AceGUIWidgetVersion < WidgetVersions[type] then
|
||||
return newWidget(type)
|
||||
end
|
||||
end
|
||||
return newObj
|
||||
end
|
||||
-- Releases an instance to the Pool
|
||||
function delWidget(obj,type)
|
||||
if not objPools[type] then
|
||||
objPools[type] = {}
|
||||
end
|
||||
if objPools[type][obj] then
|
||||
error("Attempt to Release Widget that is already released", 2)
|
||||
end
|
||||
objPools[type][obj] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-------------------
|
||||
-- API Functions --
|
||||
-------------------
|
||||
|
||||
-- Gets a widget Object
|
||||
|
||||
--- Create a new Widget of the given type.
|
||||
-- This function will instantiate a new widget (or use one from the widget pool), and call the
|
||||
-- OnAcquire function on it, before returning.
|
||||
-- @param type The type of the widget.
|
||||
-- @return The newly created widget.
|
||||
function AceGUI:Create(type)
|
||||
if WidgetRegistry[type] then
|
||||
local widget = newWidget(type)
|
||||
|
||||
if rawget(widget,'Acquire') then
|
||||
widget.OnAcquire = widget.Acquire
|
||||
widget.Acquire = nil
|
||||
elseif rawget(widget,'Aquire') then
|
||||
widget.OnAcquire = widget.Aquire
|
||||
widget.Aquire = nil
|
||||
end
|
||||
|
||||
if rawget(widget,'Release') then
|
||||
widget.OnRelease = rawget(widget,'Release')
|
||||
widget.Release = nil
|
||||
end
|
||||
|
||||
if widget.OnAcquire then
|
||||
widget:OnAcquire()
|
||||
else
|
||||
error(("Widget type %s doesn't supply an OnAcquire Function"):format(type))
|
||||
end
|
||||
-- Set the default Layout ('List')
|
||||
safecall(widget.SetLayout, widget, 'List')
|
||||
safecall(widget.ResumeLayout, widget)
|
||||
return widget
|
||||
end
|
||||
end
|
||||
|
||||
--- Releases a widget Object.
|
||||
-- This function calls OnRelease on the widget and places it back in the widget pool.
|
||||
-- Any data on the widget is being erased, and the widget will be hidden.\\
|
||||
-- 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 )
|
||||
widget:Fire("OnRelease")
|
||||
safecall( widget.ReleaseChildren, widget )
|
||||
|
||||
if widget.OnRelease then
|
||||
widget:OnRelease()
|
||||
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
|
||||
end
|
||||
for k in pairs(widget.events) do
|
||||
widget.events[k] = nil
|
||||
end
|
||||
widget.width = nil
|
||||
widget.relWidth = nil
|
||||
widget.height = nil
|
||||
widget.relHeight = nil
|
||||
widget.noAutoHeight = nil
|
||||
widget.frame:ClearAllPoints()
|
||||
widget.frame:Hide()
|
||||
widget.frame:SetParent(UIParent)
|
||||
widget.frame.width = nil
|
||||
widget.frame.height = nil
|
||||
if widget.content then
|
||||
widget.content.width = nil
|
||||
widget.content.height = nil
|
||||
end
|
||||
delWidget(widget, widget.type)
|
||||
end
|
||||
|
||||
-----------
|
||||
-- Focus --
|
||||
-----------
|
||||
|
||||
|
||||
--- Called when a widget has taken focus.
|
||||
-- e.g. Dropdowns opening, Editboxes gaining kb focus
|
||||
-- @param widget The widget that should be focused
|
||||
function AceGUI:SetFocus(widget)
|
||||
if self.FocusedWidget and self.FocusedWidget ~= widget then
|
||||
safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
|
||||
end
|
||||
self.FocusedWidget = widget
|
||||
end
|
||||
|
||||
|
||||
--- Called when something has happened that could cause widgets with focus to drop it
|
||||
-- e.g. titlebar of a frame being clicked
|
||||
function AceGUI:ClearFocus()
|
||||
if self.FocusedWidget then
|
||||
safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
|
||||
self.FocusedWidget = nil
|
||||
end
|
||||
end
|
||||
|
||||
-------------
|
||||
-- Widgets --
|
||||
-------------
|
||||
--[[
|
||||
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
|
||||
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
|
||||
: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
|
||||
AceGUI already sets a handler to the event
|
||||
:LayoutFinished(width, height) - called after a layout has finished, the width and height will be the width and height of the
|
||||
area used for controls. These can be nil if the layout used the existing size to layout the controls.
|
||||
|
||||
]]
|
||||
|
||||
--------------------------
|
||||
-- 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)
|
||||
local frame = self.frame
|
||||
frame:SetParent(nil)
|
||||
frame:SetParent(parent.content)
|
||||
self.parent = parent
|
||||
--fixlevels(parent.frame,parent.frame:GetChildren())
|
||||
end
|
||||
|
||||
WidgetBase.SetCallback = function(self, name, func)
|
||||
if type(func) == "function" then
|
||||
self.events[name] = func
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.Fire = function(self, name, ...)
|
||||
if self.events[name] then
|
||||
local success, ret = safecall(self.events[name], self, name, ...)
|
||||
if success then
|
||||
return ret
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.SetWidth = function(self, width)
|
||||
self.frame:SetWidth(width)
|
||||
self.frame.width = width
|
||||
if self.OnWidthSet then
|
||||
self:OnWidthSet(width)
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.SetRelativeWidth = function(self, width)
|
||||
if width <= 0 or width > 1 then
|
||||
error(":SetRelativeWidth(width): Invalid relative width.", 2)
|
||||
end
|
||||
self.relWidth = width
|
||||
self.width = "relative"
|
||||
end
|
||||
|
||||
WidgetBase.SetHeight = function(self, height)
|
||||
self.frame:SetHeight(height)
|
||||
self.frame.height = height
|
||||
if self.OnHeightSet then
|
||||
self:OnHeightSet(height)
|
||||
end
|
||||
end
|
||||
|
||||
--[[ WidgetBase.SetRelativeHeight = function(self, height)
|
||||
if height <= 0 or height > 1 then
|
||||
error(":SetRelativeHeight(height): Invalid relative height.", 2)
|
||||
end
|
||||
self.relHeight = height
|
||||
self.height = "relative"
|
||||
end ]]
|
||||
|
||||
WidgetBase.IsVisible = function(self)
|
||||
return self.frame:IsVisible()
|
||||
end
|
||||
|
||||
WidgetBase.IsShown= function(self)
|
||||
return self.frame:IsShown()
|
||||
end
|
||||
|
||||
WidgetBase.Release = function(self)
|
||||
AceGUI:Release(self)
|
||||
end
|
||||
|
||||
WidgetBase.SetPoint = function(self, ...)
|
||||
return self.frame:SetPoint(...)
|
||||
end
|
||||
|
||||
WidgetBase.ClearAllPoints = function(self)
|
||||
return self.frame:ClearAllPoints()
|
||||
end
|
||||
|
||||
WidgetBase.GetNumPoints = function(self)
|
||||
return self.frame:GetNumPoints()
|
||||
end
|
||||
|
||||
WidgetBase.GetPoint = function(self, ...)
|
||||
return self.frame:GetPoint(...)
|
||||
end
|
||||
|
||||
WidgetBase.GetUserDataTable = function(self)
|
||||
return self.userdata
|
||||
end
|
||||
|
||||
WidgetBase.SetUserData = function(self, key, value)
|
||||
self.userdata[key] = value
|
||||
end
|
||||
|
||||
WidgetBase.GetUserData = function(self, key)
|
||||
return self.userdata[key]
|
||||
end
|
||||
|
||||
WidgetBase.IsFullHeight = function(self)
|
||||
return self.height == "fill"
|
||||
end
|
||||
|
||||
WidgetBase.SetFullHeight = function(self, isFull)
|
||||
if isFull then
|
||||
self.height = "fill"
|
||||
else
|
||||
self.height = nil
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.IsFullWidth = function(self)
|
||||
return self.width == "fill"
|
||||
end
|
||||
|
||||
WidgetBase.SetFullWidth = function(self, isFull)
|
||||
if isFull then
|
||||
self.width = "fill"
|
||||
else
|
||||
self.width = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- local function LayoutOnUpdate(this)
|
||||
-- this:SetScript("OnUpdate",nil)
|
||||
-- this.obj:PerformLayout()
|
||||
-- end
|
||||
|
||||
local WidgetContainerBase = AceGUI.WidgetContainerBase
|
||||
|
||||
WidgetContainerBase.PauseLayout = function(self)
|
||||
self.LayoutPaused = true
|
||||
end
|
||||
|
||||
WidgetContainerBase.ResumeLayout = function(self)
|
||||
self.LayoutPaused = nil
|
||||
end
|
||||
|
||||
WidgetContainerBase.PerformLayout = function(self)
|
||||
if self.LayoutPaused then
|
||||
return
|
||||
end
|
||||
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
|
||||
WidgetContainerBase.DoLayout = function(self)
|
||||
self:PerformLayout()
|
||||
-- if not self.parent then
|
||||
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
|
||||
-- end
|
||||
end
|
||||
|
||||
WidgetContainerBase.AddChild = function(self, child, beforeWidget)
|
||||
if beforeWidget then
|
||||
local siblingIndex = 1
|
||||
for _, widget in pairs(self.children) do
|
||||
if widget == beforeWidget then
|
||||
break
|
||||
end
|
||||
siblingIndex = siblingIndex + 1
|
||||
end
|
||||
tinsert(self.children, siblingIndex, child)
|
||||
else
|
||||
tinsert(self.children, child)
|
||||
end
|
||||
child:SetParent(self)
|
||||
child.frame:Show()
|
||||
self:DoLayout()
|
||||
end
|
||||
|
||||
WidgetContainerBase.AddChildren = function(self, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
local child = select(i, ...)
|
||||
tinsert(self.children, child)
|
||||
child:SetParent(self)
|
||||
child.frame:Show()
|
||||
end
|
||||
self:DoLayout()
|
||||
end
|
||||
|
||||
WidgetContainerBase.ReleaseChildren = function(self)
|
||||
local children = self.children
|
||||
for i = 1,#children do
|
||||
AceGUI:Release(children[i])
|
||||
children[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetLayout = function(self, Layout)
|
||||
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
|
||||
if adjust then
|
||||
self.noAutoHeight = nil
|
||||
else
|
||||
self.noAutoHeight = true
|
||||
end
|
||||
end
|
||||
|
||||
local function FrameResize(this)
|
||||
local self = this.obj
|
||||
if this:GetWidth() and this:GetHeight() then
|
||||
if self.OnWidthSet then
|
||||
self:OnWidthSet(this:GetWidth())
|
||||
end
|
||||
if self.OnHeightSet then
|
||||
self:OnHeightSet(this:GetHeight())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ContentResize(this)
|
||||
if this:GetWidth() and this:GetHeight() then
|
||||
this.width = this:GetWidth()
|
||||
this.height = this:GetHeight()
|
||||
this.obj:DoLayout()
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(WidgetContainerBase,{__index=WidgetBase})
|
||||
|
||||
--One of these function should be called on each Widget Instance as part of its creation process
|
||||
|
||||
--- Register a widget-class as a container for newly created widgets.
|
||||
-- @param widget The widget class
|
||||
function AceGUI:RegisterAsContainer(widget)
|
||||
widget.children = {}
|
||||
widget.userdata = {}
|
||||
widget.events = {}
|
||||
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:SetLayout("List")
|
||||
end
|
||||
|
||||
--- Register a widget-class as a widget.
|
||||
-- @param widget The widget class
|
||||
function AceGUI:RegisterAsWidget(widget)
|
||||
widget.userdata = {}
|
||||
widget.events = {}
|
||||
widget.base = WidgetBase
|
||||
widget.frame.obj = widget
|
||||
widget.frame:SetScript("OnSizeChanged",FrameResize)
|
||||
setmetatable(widget,{__index=WidgetBase})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
------------------
|
||||
-- Widget API --
|
||||
------------------
|
||||
|
||||
--- Registers a widget Constructor, this function returns a new instance of the Widget
|
||||
-- @param Name The name of the widget
|
||||
-- @param Constructor The widget constructor function
|
||||
-- @param Version The version of the widget
|
||||
function AceGUI:RegisterWidgetType(Name, Constructor, Version)
|
||||
assert(type(Constructor) == "function")
|
||||
assert(type(Version) == "number")
|
||||
|
||||
local oldVersion = WidgetVersions[Name]
|
||||
if oldVersion and oldVersion >= Version then return end
|
||||
|
||||
WidgetVersions[Name] = Version
|
||||
WidgetRegistry[Name] = Constructor
|
||||
end
|
||||
|
||||
--- Registers a Layout Function
|
||||
-- @param Name The name of the layout
|
||||
-- @param LayoutFunc Reference to the layout function
|
||||
function AceGUI:RegisterLayout(Name, LayoutFunc)
|
||||
assert(type(LayoutFunc) == "function")
|
||||
if type(Name) == "string" then
|
||||
Name = Name:upper()
|
||||
end
|
||||
LayoutRegistry[Name] = LayoutFunc
|
||||
end
|
||||
|
||||
--- Get a Layout Function from the registry
|
||||
-- @param Name The name of the layout
|
||||
function AceGUI:GetLayout(Name)
|
||||
if type(Name) == "string" then
|
||||
Name = Name:upper()
|
||||
end
|
||||
return LayoutRegistry[Name]
|
||||
end
|
||||
|
||||
AceGUI.counts = AceGUI.counts or {}
|
||||
|
||||
--- A type-based counter to count the number of widgets created.
|
||||
-- This is used by widgets that require a named frame, e.g. when a Blizzard
|
||||
-- Template requires it.
|
||||
-- @param type The widget type
|
||||
function AceGUI:GetNextWidgetNum(type)
|
||||
if not self.counts[type] then
|
||||
self.counts[type] = 0
|
||||
end
|
||||
self.counts[type] = self.counts[type] + 1
|
||||
return self.counts[type]
|
||||
end
|
||||
|
||||
--- Return the number of created widgets for this type.
|
||||
-- In contrast to GetNextWidgetNum, the number is not incremented.
|
||||
-- @param type The widget type
|
||||
function AceGUI:GetWidgetCount(type)
|
||||
return self.counts[type] or 0
|
||||
end
|
||||
|
||||
--- Return the version of the currently registered widget type.
|
||||
-- @param type The widget type
|
||||
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 --
|
||||
-------------
|
||||
|
||||
--[[
|
||||
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
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
|
||||
local frame = child.frame
|
||||
frame:ClearAllPoints()
|
||||
frame:Show()
|
||||
if i == 1 then
|
||||
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT",children[i-1].frame,"BOTTOMLEFT",0,0)
|
||||
end
|
||||
|
||||
if child.width == "fill" then
|
||||
child:SetWidth(width)
|
||||
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
|
||||
end
|
||||
|
||||
height = height + (frame.height or frame:GetHeight() or 0)
|
||||
end
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, height )
|
||||
end
|
||||
)
|
||||
|
||||
-- A single control fills the whole content area
|
||||
AceGUI:RegisterLayout("Fill",
|
||||
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() )
|
||||
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
|
||||
local rowstartoffset
|
||||
local lastrowstart
|
||||
local isfullheight
|
||||
|
||||
local frameoffset
|
||||
local lastframeoffset
|
||||
local oversize
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
oversize = nil
|
||||
local frame = child.frame
|
||||
local frameheight = frame.height or frame:GetHeight() or 0
|
||||
local framewidth = frame.width or frame:GetWidth() or 0
|
||||
lastframeoffset = frameoffset
|
||||
-- HACK: Why did we set a frameoffset of (frameheight / 2) ?
|
||||
-- That was moving all widgets half the widgets size down, is that intended?
|
||||
-- Actually, it seems to be neccessary for many cases, we'll leave it in for now.
|
||||
-- If widgets seem to anchor weirdly with this, provide a valid alignoffset for them.
|
||||
-- TODO: Investigate moar!
|
||||
frameoffset = child.alignoffset or (frameheight / 2)
|
||||
|
||||
if child.width == "relative" then
|
||||
framewidth = width * child.relWidth
|
||||
end
|
||||
|
||||
frame:Show()
|
||||
frame:ClearAllPoints()
|
||||
if i == 1 then
|
||||
-- anchor the first control to the top left
|
||||
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
|
||||
rowheight = frameheight
|
||||
rowoffset = frameoffset
|
||||
rowstart = frame
|
||||
rowstartoffset = frameoffset
|
||||
usedwidth = framewidth
|
||||
if usedwidth > width then
|
||||
oversize = true
|
||||
end
|
||||
else
|
||||
-- if there isn't available width for the control start a new row
|
||||
-- if a control is "fill" it will be on a row of its own full width
|
||||
if usedwidth == 0 or ((framewidth) + usedwidth > width) or child.width == "fill" then
|
||||
if isfullheight then
|
||||
-- a previous row has already filled the entire height, there's nothing we can usefully do anymore
|
||||
-- (maybe error/warn about this?)
|
||||
break
|
||||
end
|
||||
--anchor the previous row, we will now know its height and offset
|
||||
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
|
||||
rowstartoffset = frameoffset
|
||||
rowheight = frameheight
|
||||
rowoffset = frameoffset
|
||||
usedwidth = framewidth
|
||||
if usedwidth > width then
|
||||
oversize = true
|
||||
end
|
||||
-- put the control on the current row, adding it to the width and checking if the height needs to be increased
|
||||
else
|
||||
--handles cases where the new height is higher than either control because of the offsets
|
||||
--math.max(rowheight-rowoffset+frameoffset, frameheight-frameoffset+rowoffset)
|
||||
|
||||
--offset is always the larger of the two offsets
|
||||
rowoffset = math_max(rowoffset, frameoffset)
|
||||
|
||||
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,"RIGHT",0,0)
|
||||
|
||||
usedwidth = 0
|
||||
rowstart = frame
|
||||
rowstartoffset = frameoffset
|
||||
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(width)
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
rowheight = frame.height or frame:GetHeight() or 0
|
||||
rowoffset = child.alignoffset or (rowheight / 2)
|
||||
rowstartoffset = rowoffset
|
||||
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,"RIGHT",0,0)
|
||||
end
|
||||
end
|
||||
|
||||
if child.height == "fill" then
|
||||
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)
|
||||
elseif rowstart then
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
|
||||
end
|
||||
|
||||
height = height + rowheight + 3
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, height )
|
||||
end
|
||||
)
|
||||
@@ -0,0 +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"/>
|
||||
<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-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-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
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
local PlaySound = PlaySound
|
||||
local CreateFrame, UIParent = CreateFrame, UIParent
|
||||
|
||||
--------------------------
|
||||
-- Button --
|
||||
--------------------------
|
||||
do
|
||||
local Type = "Button"
|
||||
local Version = 13
|
||||
|
||||
local function OnAcquire(self)
|
||||
-- restore default values
|
||||
self:SetHeight(24)
|
||||
self:SetWidth(200)
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
self:SetDisabled(false)
|
||||
end
|
||||
|
||||
local function Button_OnClick(this, ...)
|
||||
PlaySound("igMainMenuOption")
|
||||
this.obj:Fire("OnClick", ...)
|
||||
AceGUI:ClearFocus()
|
||||
end
|
||||
|
||||
local function Button_OnEnter(this)
|
||||
this.obj:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function Button_OnLeave(this)
|
||||
this.obj:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function SetText(self, text)
|
||||
self.text:SetText(text or "")
|
||||
end
|
||||
|
||||
local function SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.frame:Disable()
|
||||
else
|
||||
self.frame:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local num = AceGUI:GetNextWidgetNum(Type)
|
||||
local name = "AceGUI30Button"..num
|
||||
local frame = CreateFrame("Button",name,UIParent,"UIPanelButtonTemplate2")
|
||||
local self = {}
|
||||
self.num = num
|
||||
self.type = Type
|
||||
self.frame = frame
|
||||
|
||||
local left = _G[name .. "Left"]
|
||||
local right = _G[name .. "Right"]
|
||||
local middle = _G[name .. "Middle"]
|
||||
|
||||
left:SetPoint("TOP", frame, "TOP", 0, 0)
|
||||
left:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0)
|
||||
|
||||
right:SetPoint("TOP", frame, "TOP", 0, 0)
|
||||
right:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0)
|
||||
|
||||
middle:SetPoint("TOP", frame, "TOP", 0, 0)
|
||||
middle:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0)
|
||||
|
||||
local text = frame:GetFontString()
|
||||
self.text = text
|
||||
text:ClearAllPoints()
|
||||
text:SetPoint("TOPLEFT",frame,"TOPLEFT", 15, -1)
|
||||
text:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT", -15, 1)
|
||||
text:SetJustifyV("MIDDLE")
|
||||
|
||||
frame:SetScript("OnClick",Button_OnClick)
|
||||
frame:SetScript("OnEnter",Button_OnEnter)
|
||||
frame:SetScript("OnLeave",Button_OnLeave)
|
||||
|
||||
self.SetText = SetText
|
||||
self.SetDisabled = SetDisabled
|
||||
|
||||
frame:EnableMouse(true)
|
||||
|
||||
frame:SetHeight(24)
|
||||
frame:SetWidth(200)
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local select = select
|
||||
|
||||
-- WoW APIs
|
||||
local PlaySound = PlaySound
|
||||
local CreateFrame, UIParent = CreateFrame, UIParent
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: SetDesaturation, GameFontHighlight
|
||||
|
||||
--------------------------
|
||||
-- Check Box --
|
||||
--------------------------
|
||||
--[[
|
||||
Events :
|
||||
OnValueChanged
|
||||
|
||||
]]
|
||||
do
|
||||
local Type = "CheckBox"
|
||||
local Version = 16
|
||||
|
||||
local function OnAcquire(self)
|
||||
self:SetValue(false)
|
||||
self.tristate = nil
|
||||
self:SetHeight(24)
|
||||
self:SetWidth(200)
|
||||
self:SetImage()
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
self.check:Hide()
|
||||
self.highlight:Hide()
|
||||
self.down = nil
|
||||
self.checked = nil
|
||||
self:SetType()
|
||||
self:SetDisabled(false)
|
||||
self:SetDescription(nil)
|
||||
end
|
||||
|
||||
local function CheckBox_OnEnter(this)
|
||||
local self = this.obj
|
||||
self.highlight:Show()
|
||||
self:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function CheckBox_OnLeave(this)
|
||||
local self = this.obj
|
||||
self.highlight:Hide()
|
||||
self:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function CheckBox_OnMouseUp(this)
|
||||
local self = this.obj
|
||||
if not self.disabled then
|
||||
self:ToggleChecked()
|
||||
|
||||
if self.checked then
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
else -- for both nil and false (tristate)
|
||||
PlaySound("igMainMenuOptionCheckBoxOff")
|
||||
end
|
||||
|
||||
self:Fire("OnValueChanged",self.checked)
|
||||
self.text:SetPoint("LEFT",self.check,"RIGHT",0,0)
|
||||
end
|
||||
self.down = nil
|
||||
end
|
||||
|
||||
local function CheckBox_OnMouseDown(this)
|
||||
local self = this.obj
|
||||
if not self.disabled then
|
||||
self.text:SetPoint("LEFT",self.check,"RIGHT",1,-1)
|
||||
self.down = true
|
||||
end
|
||||
AceGUI:ClearFocus()
|
||||
end
|
||||
|
||||
local function SetDisabled(self,disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.frame:Disable()
|
||||
self.text:SetTextColor(0.5,0.5,0.5)
|
||||
SetDesaturation(self.check, true)
|
||||
else
|
||||
self.frame:Enable()
|
||||
self.text:SetTextColor(1,1,1)
|
||||
if self.tristate and self.checked == nil then
|
||||
SetDesaturation(self.check, true)
|
||||
else
|
||||
SetDesaturation(self.check, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SetValue(self,value)
|
||||
local check = self.check
|
||||
self.checked = value
|
||||
if value then
|
||||
SetDesaturation(self.check, false)
|
||||
self.check:Show()
|
||||
else
|
||||
--Nil is the unknown tristate value
|
||||
if self.tristate and value == nil then
|
||||
SetDesaturation(self.check, true)
|
||||
self.check:Show()
|
||||
else
|
||||
SetDesaturation(self.check, false)
|
||||
self.check:Hide()
|
||||
end
|
||||
end
|
||||
SetDisabled(self, self.disabled)
|
||||
end
|
||||
|
||||
local function SetTriState(self, enabled)
|
||||
self.tristate = enabled
|
||||
self:SetValue(self:GetValue())
|
||||
end
|
||||
|
||||
local function GetValue(self)
|
||||
return self.checked
|
||||
end
|
||||
|
||||
local function SetType(self, type)
|
||||
local checkbg = self.checkbg
|
||||
local check = self.check
|
||||
local highlight = self.highlight
|
||||
|
||||
if type == "radio" then
|
||||
checkbg:SetHeight(16)
|
||||
checkbg:SetWidth(16)
|
||||
checkbg:SetTexture("Interface\\Buttons\\UI-RadioButton")
|
||||
checkbg:SetTexCoord(0,0.25,0,1)
|
||||
check:SetHeight(16)
|
||||
check:SetWidth(16)
|
||||
check:SetTexture("Interface\\Buttons\\UI-RadioButton")
|
||||
check:SetTexCoord(0.25,0.5,0,1)
|
||||
check:SetBlendMode("ADD")
|
||||
highlight:SetTexture("Interface\\Buttons\\UI-RadioButton")
|
||||
highlight:SetTexCoord(0.5,0.75,0,1)
|
||||
else
|
||||
checkbg:SetHeight(24)
|
||||
checkbg:SetWidth(24)
|
||||
checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
|
||||
checkbg:SetTexCoord(0,1,0,1)
|
||||
check:SetHeight(24)
|
||||
check:SetWidth(24)
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
check:SetTexCoord(0,1,0,1)
|
||||
check:SetBlendMode("BLEND")
|
||||
highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
|
||||
highlight:SetTexCoord(0,1,0,1)
|
||||
end
|
||||
end
|
||||
|
||||
local function ToggleChecked(self)
|
||||
local value = self:GetValue()
|
||||
if self.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if value then
|
||||
self:SetValue(nil)
|
||||
elseif value == nil then
|
||||
self:SetValue(false)
|
||||
else
|
||||
self:SetValue(true)
|
||||
end
|
||||
else
|
||||
self:SetValue(not self:GetValue())
|
||||
end
|
||||
end
|
||||
|
||||
local function SetLabel(self, label)
|
||||
self.text:SetText(label)
|
||||
end
|
||||
|
||||
local function SetDescription(self, desc)
|
||||
if desc then
|
||||
if not self.desc then
|
||||
local desc = self.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
desc:ClearAllPoints()
|
||||
desc:SetPoint("TOPLEFT", self.check, "TOPRIGHT", 5, -21)
|
||||
desc:SetWidth(self.frame.width - 30)
|
||||
desc:SetJustifyH("LEFT")
|
||||
desc:SetJustifyV("TOP")
|
||||
self.desc = desc
|
||||
end
|
||||
self.desc:Show()
|
||||
--self.text:SetFontObject(GameFontNormal)
|
||||
self.desc:SetText(desc)
|
||||
self:SetHeight(28 + self.desc:GetHeight())
|
||||
else
|
||||
if self.desc then
|
||||
self.desc:SetText("")
|
||||
self.desc:Hide()
|
||||
end
|
||||
self.text:SetFontObject(GameFontHighlight)
|
||||
self:SetHeight(24)
|
||||
end
|
||||
end
|
||||
|
||||
local function SetImage(self, path, ...)
|
||||
local image = self.image
|
||||
image:SetTexture(path)
|
||||
|
||||
if image:GetTexture() then
|
||||
local n = select('#', ...)
|
||||
if n == 4 or n == 8 then
|
||||
image:SetTexCoord(...)
|
||||
else
|
||||
image:SetTexCoord(0, 1, 0, 1)
|
||||
end
|
||||
end
|
||||
self:AlignImage()
|
||||
end
|
||||
|
||||
local function AlignImage(self)
|
||||
local img = self.image:GetTexture()
|
||||
self.text:ClearAllPoints()
|
||||
if not img then
|
||||
self.text:SetPoint("LEFT", self.check, "RIGHT", 0, 0)
|
||||
self.text:SetPoint("RIGHT", self.frame, "RIGHT", 0, 0)
|
||||
else
|
||||
self.text:SetPoint("LEFT", self.image,"RIGHT", 1, 0)
|
||||
self.text:SetPoint("RIGHT", self.frame,"RIGHT", 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnWidthSet(self, width)
|
||||
if self.desc and self.desc:GetText() ~= "" and self.desc:GetText() ~= nil then
|
||||
self.desc:SetWidth(width - 30)
|
||||
self:SetHeight(28 + self.desc:GetHeight())
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Button",nil,UIParent)
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.SetValue = SetValue
|
||||
self.GetValue = GetValue
|
||||
self.SetDisabled = SetDisabled
|
||||
self.SetType = SetType
|
||||
self.ToggleChecked = ToggleChecked
|
||||
self.SetLabel = SetLabel
|
||||
self.SetTriState = SetTriState
|
||||
self.SetDescription = SetDescription
|
||||
self.OnWidthSet = OnWidthSet
|
||||
self.SetImage = SetImage
|
||||
self.AlignImage = AlignImage
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
local text = frame:CreateFontString(nil,"OVERLAY","GameFontHighlight")
|
||||
self.text = text
|
||||
|
||||
frame:SetScript("OnEnter",CheckBox_OnEnter)
|
||||
frame:SetScript("OnLeave",CheckBox_OnLeave)
|
||||
frame:SetScript("OnMouseUp",CheckBox_OnMouseUp)
|
||||
frame:SetScript("OnMouseDown",CheckBox_OnMouseDown)
|
||||
frame:EnableMouse()
|
||||
local checkbg = frame:CreateTexture(nil,"ARTWORK")
|
||||
self.checkbg = checkbg
|
||||
checkbg:SetWidth(24)
|
||||
checkbg:SetHeight(24)
|
||||
checkbg:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
|
||||
checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
|
||||
local check = frame:CreateTexture(nil,"OVERLAY")
|
||||
self.check = check
|
||||
check:SetWidth(24)
|
||||
check:SetHeight(24)
|
||||
check:SetPoint("CENTER",checkbg,"CENTER",0,0)
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
|
||||
local highlight = frame:CreateTexture(nil, "OVERLAY")
|
||||
self.highlight = highlight
|
||||
highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetAllPoints(checkbg)
|
||||
highlight:Hide()
|
||||
|
||||
local image = frame:CreateTexture(nil, "OVERLAY")
|
||||
self.image = image
|
||||
image:SetHeight(16)
|
||||
image:SetWidth(16)
|
||||
image:SetPoint("LEFT", check, "RIGHT", 1, 0)
|
||||
|
||||
text:SetJustifyH("LEFT")
|
||||
frame:SetHeight(24)
|
||||
frame:SetWidth(200)
|
||||
text:SetHeight(18)
|
||||
text:SetPoint("LEFT",check,"RIGHT",0,0)
|
||||
text:SetPoint("RIGHT",frame,"RIGHT",0,0)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- WoW APIs
|
||||
local CreateFrame, UIParent = CreateFrame, UIParent
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: ShowUIPanel, HideUIPanel, ColorPickerFrame, OpacitySliderFrame
|
||||
|
||||
--------------------------
|
||||
-- ColorPicker --
|
||||
--------------------------
|
||||
do
|
||||
local Type = "ColorPicker"
|
||||
local Version = 11
|
||||
|
||||
local function OnAcquire(self)
|
||||
self.HasAlpha = false
|
||||
self:SetColor(0,0,0,1)
|
||||
self:SetHeight(24)
|
||||
self:SetWidth(200)
|
||||
end
|
||||
|
||||
local function SetLabel(self, text)
|
||||
self.text:SetText(text)
|
||||
end
|
||||
|
||||
local function SetColor(self,r,g,b,a)
|
||||
self.r = r
|
||||
self.g = g
|
||||
self.b = b
|
||||
self.a = a or 1
|
||||
self.colorSwatch:SetVertexColor(r,g,b,a)
|
||||
end
|
||||
|
||||
local function Control_OnEnter(this)
|
||||
this.obj:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function Control_OnLeave(this)
|
||||
this.obj:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function SetHasAlpha(self, HasAlpha)
|
||||
self.HasAlpha = HasAlpha
|
||||
end
|
||||
|
||||
local function ColorCallback(self,r,g,b,a,isAlpha)
|
||||
if not self.HasAlpha then
|
||||
a = 1
|
||||
end
|
||||
self:SetColor(r,g,b,a)
|
||||
if ColorPickerFrame:IsVisible() then
|
||||
--colorpicker is still open
|
||||
|
||||
self:Fire("OnValueChanged",r,g,b,a)
|
||||
else
|
||||
--colorpicker is closed, color callback is first, ignore it,
|
||||
--alpha callback is the final call after it closes so confirm now
|
||||
if isAlpha then
|
||||
self:Fire("OnValueConfirmed",r,g,b,a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ColorSwatch_OnClick(this)
|
||||
HideUIPanel(ColorPickerFrame)
|
||||
local self = this.obj
|
||||
if not self.disabled then
|
||||
ColorPickerFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
ColorPickerFrame.func = function()
|
||||
local r,g,b = ColorPickerFrame:GetColorRGB()
|
||||
local a = 1 - OpacitySliderFrame:GetValue()
|
||||
ColorCallback(self,r,g,b,a)
|
||||
end
|
||||
|
||||
ColorPickerFrame.hasOpacity = self.HasAlpha
|
||||
ColorPickerFrame.opacityFunc = function()
|
||||
local r,g,b = ColorPickerFrame:GetColorRGB()
|
||||
local a = 1 - OpacitySliderFrame:GetValue()
|
||||
ColorCallback(self,r,g,b,a,true)
|
||||
end
|
||||
local r, g, b, a = self.r, self.g, self.b, self.a
|
||||
if self.HasAlpha then
|
||||
ColorPickerFrame.opacity = 1 - (a or 0)
|
||||
end
|
||||
ColorPickerFrame:SetColorRGB(r, g, b)
|
||||
|
||||
ColorPickerFrame.cancelFunc = function()
|
||||
ColorCallback(self,r,g,b,a,true)
|
||||
end
|
||||
ShowUIPanel(ColorPickerFrame)
|
||||
end
|
||||
AceGUI:ClearFocus()
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
local function SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if self.disabled then
|
||||
self.frame:Disable()
|
||||
self.text:SetTextColor(0.5,0.5,0.5)
|
||||
else
|
||||
self.frame:Enable()
|
||||
self.text:SetTextColor(1,1,1)
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Button",nil,UIParent)
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.SetLabel = SetLabel
|
||||
self.SetColor = SetColor
|
||||
self.SetDisabled = SetDisabled
|
||||
self.SetHasAlpha = SetHasAlpha
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
local text = frame:CreateFontString(nil,"OVERLAY","GameFontHighlight")
|
||||
self.text = text
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetTextColor(1,1,1)
|
||||
frame:SetHeight(24)
|
||||
frame:SetWidth(200)
|
||||
text:SetHeight(24)
|
||||
frame:SetScript("OnClick", ColorSwatch_OnClick)
|
||||
frame:SetScript("OnEnter",Control_OnEnter)
|
||||
frame:SetScript("OnLeave",Control_OnLeave)
|
||||
|
||||
local colorSwatch = frame:CreateTexture(nil, "OVERLAY")
|
||||
self.colorSwatch = colorSwatch
|
||||
colorSwatch:SetWidth(19)
|
||||
colorSwatch:SetHeight(19)
|
||||
colorSwatch:SetTexture("Interface\\ChatFrame\\ChatFrameColorSwatch")
|
||||
local texture = frame:CreateTexture(nil, "BACKGROUND")
|
||||
colorSwatch.texture = texture
|
||||
texture:SetWidth(16)
|
||||
texture:SetHeight(16)
|
||||
texture:SetTexture(1,1,1)
|
||||
texture:Show()
|
||||
|
||||
local checkers = frame:CreateTexture(nil, "BACKGROUND")
|
||||
colorSwatch.checkers = checkers
|
||||
checkers:SetTexture("Tileset\\Generic\\Checkers")
|
||||
checkers:SetDesaturated(true)
|
||||
checkers:SetVertexColor(1,1,1,0.75)
|
||||
checkers:SetTexCoord(.25,0,0.5,.25)
|
||||
checkers:SetWidth(14)
|
||||
checkers:SetHeight(14)
|
||||
checkers:Show()
|
||||
|
||||
local highlight = frame:CreateTexture(nil, "BACKGROUND")
|
||||
self.highlight = highlight
|
||||
highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetAllPoints(frame)
|
||||
highlight:Hide()
|
||||
|
||||
texture:SetPoint("CENTER", colorSwatch, "CENTER")
|
||||
checkers:SetPoint("CENTER", colorSwatch, "CENTER")
|
||||
colorSwatch:SetPoint("LEFT", frame, "LEFT", 0, 0)
|
||||
text:SetPoint("LEFT",colorSwatch,"RIGHT",2,0)
|
||||
text:SetPoint("RIGHT",frame,"RIGHT")
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
--[[ $Id: AceGUIWidget-DropDown-Items.lua 916 2010-03-15 12:24:36Z nevcairiel $ ]]--
|
||||
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local select, assert = select, assert
|
||||
|
||||
-- WoW APIs
|
||||
local PlaySound = PlaySound
|
||||
local CreateFrame = CreateFrame
|
||||
|
||||
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 function fixstrata(strata, parent, ...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
parent:SetFrameStrata(strata)
|
||||
while child do
|
||||
fixstrata(strata, child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- ItemBase is the base "class" for all dropdown items.
|
||||
-- Each item has to use ItemBase.Create(widgetType) to
|
||||
-- create an initial 'self' value.
|
||||
-- ItemBase will add common functions and ui event handlers.
|
||||
-- Be sure to keep basic usage when you override functions.
|
||||
|
||||
local ItemBase = {
|
||||
-- NOTE: The ItemBase version is added to each item's version number
|
||||
-- to ensure proper updates on ItemBase changes.
|
||||
-- Use at least 1000er steps.
|
||||
version = 1000,
|
||||
counter = 0,
|
||||
}
|
||||
|
||||
function ItemBase.Frame_OnEnter(this)
|
||||
local self = this.obj
|
||||
|
||||
if self.useHighlight then
|
||||
self.highlight:Show()
|
||||
end
|
||||
self:Fire("OnEnter")
|
||||
|
||||
if self.specialOnEnter then
|
||||
self.specialOnEnter(self)
|
||||
end
|
||||
end
|
||||
|
||||
function ItemBase.Frame_OnLeave(this)
|
||||
local self = this.obj
|
||||
|
||||
self.highlight:Hide()
|
||||
self:Fire("OnLeave")
|
||||
|
||||
if self.specialOnLeave then
|
||||
self.specialOnLeave(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
function ItemBase.OnAcquire(self)
|
||||
self.frame:SetToplevel(true)
|
||||
self.frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
function ItemBase.OnRelease(self)
|
||||
self:SetDisabled(false)
|
||||
self.pullout = nil
|
||||
self.frame:SetParent(nil)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
-- NOTE: this is called by a Dropdown-Pullout.
|
||||
-- Do not call this method directly
|
||||
function ItemBase.SetPullout(self, pullout)
|
||||
self.pullout = pullout
|
||||
|
||||
self.frame:SetParent(nil)
|
||||
self.frame:SetParent(pullout.itemFrame)
|
||||
self.parent = pullout.itemFrame
|
||||
fixlevels(pullout.itemFrame, pullout.itemFrame:GetChildren())
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.SetText(self, text)
|
||||
self.text:SetText(text or "")
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.GetText(self)
|
||||
return self.text:GetText()
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.SetPoint(self, ...)
|
||||
self.frame:SetPoint(...)
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.Show(self)
|
||||
self.frame:Show()
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.Hide(self)
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.useHighlight = false
|
||||
self.text:SetTextColor(.5, .5, .5)
|
||||
else
|
||||
self.useHighlight = true
|
||||
self.text:SetTextColor(1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
-- NOTE: this is called by a Dropdown-Pullout.
|
||||
-- Do not call this method directly
|
||||
function ItemBase.SetOnLeave(self, func)
|
||||
self.specialOnLeave = func
|
||||
end
|
||||
|
||||
-- exported
|
||||
-- NOTE: this is called by a Dropdown-Pullout.
|
||||
-- Do not call this method directly
|
||||
function ItemBase.SetOnEnter(self, func)
|
||||
self.specialOnEnter = func
|
||||
end
|
||||
|
||||
function ItemBase.Create(type)
|
||||
-- NOTE: Most of the following code is copied from AceGUI-3.0/Dropdown widget
|
||||
local count = AceGUI:GetNextWidgetNum(type)
|
||||
local frame = CreateFrame("Button", "AceGUI30DropDownItem"..count)
|
||||
local self = {}
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
self.type = type
|
||||
|
||||
self.useHighlight = true
|
||||
|
||||
frame:SetHeight(17)
|
||||
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
local text = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
|
||||
text:SetTextColor(1,1,1)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetPoint("TOPLEFT",frame,"TOPLEFT",18,0)
|
||||
text:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-8,0)
|
||||
self.text = text
|
||||
|
||||
local highlight = frame:CreateTexture(nil, "OVERLAY")
|
||||
highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetHeight(14)
|
||||
highlight:ClearAllPoints()
|
||||
highlight:SetPoint("RIGHT",frame,"RIGHT",-3,0)
|
||||
highlight:SetPoint("LEFT",frame,"LEFT",5,0)
|
||||
highlight:Hide()
|
||||
self.highlight = highlight
|
||||
|
||||
local check = frame:CreateTexture("OVERLAY")
|
||||
check:SetWidth(16)
|
||||
check:SetHeight(16)
|
||||
check:SetPoint("LEFT",frame,"LEFT",3,-1)
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
check:Hide()
|
||||
self.check = check
|
||||
|
||||
local sub = frame:CreateTexture("OVERLAY")
|
||||
sub:SetWidth(16)
|
||||
sub:SetHeight(16)
|
||||
sub:SetPoint("RIGHT",frame,"RIGHT",-3,-1)
|
||||
sub:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
|
||||
sub:Hide()
|
||||
self.sub = sub
|
||||
|
||||
frame:SetScript("OnEnter", ItemBase.Frame_OnEnter)
|
||||
frame:SetScript("OnLeave", ItemBase.Frame_OnLeave)
|
||||
|
||||
self.OnAcquire = ItemBase.OnAcquire
|
||||
self.OnRelease = ItemBase.OnRelease
|
||||
|
||||
self.SetPullout = ItemBase.SetPullout
|
||||
self.GetText = ItemBase.GetText
|
||||
self.SetText = ItemBase.SetText
|
||||
self.SetDisabled = ItemBase.SetDisabled
|
||||
|
||||
self.SetPoint = ItemBase.SetPoint
|
||||
self.Show = ItemBase.Show
|
||||
self.Hide = ItemBase.Hide
|
||||
|
||||
self.SetOnLeave = ItemBase.SetOnLeave
|
||||
self.SetOnEnter = ItemBase.SetOnEnter
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Template for items:
|
||||
|
||||
-- Item:
|
||||
--
|
||||
do
|
||||
local widgetType = "Dropdown-Item-"
|
||||
local widgetVersion = 1
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
--]]
|
||||
|
||||
-- Item: Header
|
||||
-- A single text entry.
|
||||
-- Special: Different text color and no highlight
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Header"
|
||||
local widgetVersion = 1
|
||||
|
||||
local function OnEnter(this)
|
||||
local self = this.obj
|
||||
self:Fire("OnEnter")
|
||||
|
||||
if self.specialOnEnter then
|
||||
self.specialOnEnter(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnLeave(this)
|
||||
local self = this.obj
|
||||
self:Fire("OnLeave")
|
||||
|
||||
if self.specialOnLeave then
|
||||
self.specialOnLeave(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported, override
|
||||
local function SetDisabled(self, disabled)
|
||||
ItemBase.SetDisabled(self, disabled)
|
||||
if not disabled then
|
||||
self.text:SetTextColor(1, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.SetDisabled = SetDisabled
|
||||
|
||||
self.frame:SetScript("OnEnter", OnEnter)
|
||||
self.frame:SetScript("OnLeave", OnLeave)
|
||||
|
||||
self.text:SetTextColor(1, 1, 0)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Execute
|
||||
-- A simple button
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Execute"
|
||||
local widgetVersion = 1
|
||||
|
||||
local function Frame_OnClick(this, button)
|
||||
local self = this.obj
|
||||
if self.disabled then return end
|
||||
self:Fire("OnClick")
|
||||
if self.pullout then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.frame:SetScript("OnClick", Frame_OnClick)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Toggle
|
||||
-- Some sort of checkbox for dropdown menus.
|
||||
-- Does not close the pullout on click.
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Toggle"
|
||||
local widgetVersion = 3
|
||||
|
||||
local function UpdateToggle(self)
|
||||
if self.value then
|
||||
self.check:Show()
|
||||
else
|
||||
self.check:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
ItemBase.OnRelease(self)
|
||||
self:SetValue(nil)
|
||||
end
|
||||
|
||||
local function Frame_OnClick(this, button)
|
||||
local self = this.obj
|
||||
if self.disabled then return end
|
||||
self.value = not self.value
|
||||
if self.value then
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
else
|
||||
PlaySound("igMainMenuOptionCheckBoxOff")
|
||||
end
|
||||
UpdateToggle(self)
|
||||
self:Fire("OnValueChanged", self.value)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetValue(self, value)
|
||||
self.value = value
|
||||
UpdateToggle(self)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetValue(self)
|
||||
return self.value
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.frame:SetScript("OnClick", Frame_OnClick)
|
||||
|
||||
self.SetValue = SetValue
|
||||
self.GetValue = GetValue
|
||||
self.OnRelease = OnRelease
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Menu
|
||||
-- Shows a submenu on mouse over
|
||||
-- Does not close the pullout on click
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Menu"
|
||||
local widgetVersion = 2
|
||||
|
||||
local function OnEnter(this)
|
||||
local self = this.obj
|
||||
self:Fire("OnEnter")
|
||||
|
||||
if self.specialOnEnter then
|
||||
self.specialOnEnter(self)
|
||||
end
|
||||
|
||||
self.highlight:Show()
|
||||
|
||||
if not self.disabled and self.submenu then
|
||||
self.submenu:Open("TOPLEFT", self.frame, "TOPRIGHT", self.pullout:GetRightBorderWidth(), 0, self.frame:GetFrameLevel() + 100)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnHide(this)
|
||||
local self = this.obj
|
||||
if self.submenu then
|
||||
self.submenu:Close()
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetMenu(self, menu)
|
||||
assert(menu.type == "Dropdown-Pullout")
|
||||
self.submenu = menu
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function CloseMenu(self)
|
||||
self.submenu:Close()
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.sub:Show()
|
||||
|
||||
self.frame:SetScript("OnEnter", OnEnter)
|
||||
self.frame:SetScript("OnHide", OnHide)
|
||||
|
||||
self.SetMenu = SetMenu
|
||||
self.CloseMenu = CloseMenu
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Separator
|
||||
-- A single line to separate items
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Separator"
|
||||
local widgetVersion = 1
|
||||
|
||||
-- exported, override
|
||||
local function SetDisabled(self, disabled)
|
||||
ItemBase.SetDisabled(self, disabled)
|
||||
self.useHighlight = false
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.SetDisabled = SetDisabled
|
||||
|
||||
local line = self.frame:CreateTexture(nil, "OVERLAY")
|
||||
line:SetHeight(1)
|
||||
line:SetTexture(.5, .5, .5)
|
||||
line:SetPoint("LEFT", self.frame, "LEFT", 10, 0)
|
||||
line:SetPoint("RIGHT", self.frame, "RIGHT", -10, 0)
|
||||
|
||||
self.text:Hide()
|
||||
|
||||
self.useHighlight = false
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
--[[ $Id: AceGUIWidget-DropDown.lua 916 2010-03-15 12:24:36Z nevcairiel $ ]]--
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local min, max, floor = math.min, math.max, math.floor
|
||||
local select, pairs, ipairs = select, pairs, ipairs
|
||||
local tsort = table.sort
|
||||
|
||||
-- WoW APIs
|
||||
local PlaySound = PlaySound
|
||||
local UIParent, CreateFrame = UIParent, CreateFrame
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: CLOSE
|
||||
|
||||
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 function fixstrata(strata, parent, ...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
parent:SetFrameStrata(strata)
|
||||
while child do
|
||||
fixstrata(strata, child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local widgetType = "Dropdown-Pullout"
|
||||
local widgetVersion = 3
|
||||
|
||||
--[[ Static data ]]--
|
||||
|
||||
local backdrop = {
|
||||
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
edgeSize = 32,
|
||||
tileSize = 32,
|
||||
tile = true,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
}
|
||||
local sliderBackdrop = {
|
||||
bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
|
||||
edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
|
||||
tile = true, tileSize = 8, edgeSize = 8,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
||||
}
|
||||
|
||||
local defaultWidth = 200
|
||||
local defaultMaxHeight = 600
|
||||
|
||||
--[[ UI Event Handlers ]]--
|
||||
|
||||
-- HACK: This should be no part of the pullout, but there
|
||||
-- is no other 'clean' way to response to any item-OnEnter
|
||||
-- Used to close Submenus when an other item is entered
|
||||
local function OnEnter(item)
|
||||
local self = item.pullout
|
||||
for k, v in ipairs(self.items) do
|
||||
if v.CloseMenu and v ~= item then
|
||||
v:CloseMenu()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- See the note in Constructor() for each scroll related function
|
||||
local function OnMouseWheel(this, value)
|
||||
this.obj:MoveScroll(value)
|
||||
end
|
||||
|
||||
local function OnScrollValueChanged(this, value)
|
||||
this.obj:SetScroll(value)
|
||||
end
|
||||
|
||||
local function OnSizeChanged(this)
|
||||
this.obj:FixScroll()
|
||||
end
|
||||
|
||||
--[[ Exported methods ]]--
|
||||
|
||||
-- exported
|
||||
local function SetScroll(self, value)
|
||||
local status = self.scrollStatus
|
||||
local frame, child = self.scrollFrame, self.itemFrame
|
||||
local height, viewheight = frame:GetHeight(), child:GetHeight()
|
||||
|
||||
local offset
|
||||
if height > viewheight then
|
||||
offset = 0
|
||||
else
|
||||
offset = floor((viewheight - height) / 1000 * value)
|
||||
end
|
||||
child:ClearAllPoints()
|
||||
child:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, offset)
|
||||
child:SetPoint("TOPRIGHT", frame, "TOPRIGHT", self.slider:IsShown() and -12 or 0, offset)
|
||||
status.offset = offset
|
||||
status.scrollvalue = value
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function MoveScroll(self, value)
|
||||
local status = self.scrollStatus
|
||||
local frame, child = self.scrollFrame, self.itemFrame
|
||||
local height, viewheight = frame:GetHeight(), child:GetHeight()
|
||||
|
||||
if height > viewheight then
|
||||
self.slider:Hide()
|
||||
else
|
||||
self.slider:Show()
|
||||
local diff = height - viewheight
|
||||
local delta = 1
|
||||
if value < 0 then
|
||||
delta = -1
|
||||
end
|
||||
self.slider:SetValue(min(max(status.scrollvalue + delta*(1000/(diff/45)),0), 1000))
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function FixScroll(self)
|
||||
local status = self.scrollStatus
|
||||
local frame, child = self.scrollFrame, self.itemFrame
|
||||
local height, viewheight = frame:GetHeight(), child:GetHeight()
|
||||
local offset = status.offset or 0
|
||||
|
||||
if viewheight < height then
|
||||
self.slider:Hide()
|
||||
child:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, offset)
|
||||
self.slider:SetValue(0)
|
||||
else
|
||||
self.slider:Show()
|
||||
local value = (offset / (viewheight - height) * 1000)
|
||||
if value > 1000 then value = 1000 end
|
||||
self.slider:SetValue(value)
|
||||
self:SetScroll(value)
|
||||
if value < 1000 then
|
||||
child:ClearAllPoints()
|
||||
child:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, offset)
|
||||
child:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, offset)
|
||||
status.offset = offset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnAcquire(self)
|
||||
self.frame:SetParent(UIParent)
|
||||
--self.itemFrame:SetToplevel(true)
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnRelease(self)
|
||||
self:Clear()
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function AddItem(self, item)
|
||||
self.items[#self.items + 1] = item
|
||||
|
||||
local h = #self.items * 16
|
||||
self.itemFrame:SetHeight(h)
|
||||
self.frame:SetHeight(min(h + 34, self.maxHeight)) -- +34: 20 for scrollFrame placement (10 offset) and +14 for item placement
|
||||
|
||||
item.frame:SetPoint("LEFT", self.itemFrame, "LEFT")
|
||||
item.frame:SetPoint("RIGHT", self.itemFrame, "RIGHT")
|
||||
|
||||
item:SetPullout(self)
|
||||
item:SetOnEnter(OnEnter)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function Open(self, point, relFrame, relPoint, x, y)
|
||||
local items = self.items
|
||||
local frame = self.frame
|
||||
local itemFrame = self.itemFrame
|
||||
|
||||
frame:SetPoint(point, relFrame, relPoint, x, y)
|
||||
|
||||
|
||||
local height = 8
|
||||
for i, item in pairs(items) do
|
||||
if i == 1 then
|
||||
item:SetPoint("TOP", itemFrame, "TOP", 0, -2)
|
||||
else
|
||||
item:SetPoint("TOP", items[i-1].frame, "BOTTOM", 0, 1)
|
||||
end
|
||||
|
||||
item:Show()
|
||||
|
||||
height = height + 16
|
||||
end
|
||||
itemFrame:SetHeight(height)
|
||||
fixstrata("TOOLTIP", frame, frame:GetChildren())
|
||||
frame:Show()
|
||||
self:Fire("OnOpen")
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function Close(self)
|
||||
self.frame:Hide()
|
||||
self:Fire("OnClose")
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function Clear(self)
|
||||
local items = self.items
|
||||
for i, item in pairs(items) do
|
||||
AceGUI:Release(item)
|
||||
items[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function IterateItems(self)
|
||||
return ipairs(self.items)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetHideOnLeave(self, val)
|
||||
self.hideOnLeave = val
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetMaxHeight(self, height)
|
||||
self.maxHeight = height or defaultMaxHeight
|
||||
if self.frame:GetHeight() > height then
|
||||
self.frame:SetHeight(height)
|
||||
elseif (self.itemFrame:GetHeight() + 34) < height then
|
||||
self.frame:SetHeight(self.itemFrame:GetHeight() + 34) -- see :AddItem
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetRightBorderWidth(self)
|
||||
return 6 + (self.slider:IsShown() and 12 or 0)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetLeftBorderWidth(self)
|
||||
return 6
|
||||
end
|
||||
|
||||
--[[ Constructor ]]--
|
||||
|
||||
local function Constructor()
|
||||
local count = AceGUI:GetNextWidgetNum(widgetType)
|
||||
local frame = CreateFrame("Frame", "AceGUI30Pullout"..count, UIParent)
|
||||
local self = {}
|
||||
self.count = count
|
||||
self.type = widgetType
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
self.OnAcquire = OnAcquire
|
||||
self.OnRelease = OnRelease
|
||||
|
||||
self.AddItem = AddItem
|
||||
self.Open = Open
|
||||
self.Close = Close
|
||||
self.Clear = Clear
|
||||
self.IterateItems = IterateItems
|
||||
self.SetHideOnLeave = SetHideOnLeave
|
||||
|
||||
self.SetScroll = SetScroll
|
||||
self.MoveScroll = MoveScroll
|
||||
self.FixScroll = FixScroll
|
||||
|
||||
self.SetMaxHeight = SetMaxHeight
|
||||
self.GetRightBorderWidth = GetRightBorderWidth
|
||||
self.GetLeftBorderWidth = GetLeftBorderWidth
|
||||
|
||||
self.items = {}
|
||||
|
||||
self.scrollStatus = {
|
||||
scrollvalue = 0,
|
||||
}
|
||||
|
||||
self.maxHeight = defaultMaxHeight
|
||||
|
||||
frame:SetBackdrop(backdrop)
|
||||
frame:SetBackdropColor(0, 0, 0)
|
||||
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
frame:SetClampedToScreen(true)
|
||||
frame:SetWidth(defaultWidth)
|
||||
frame:SetHeight(self.maxHeight)
|
||||
--frame:SetToplevel(true)
|
||||
|
||||
-- NOTE: The whole scroll frame code is copied from the AceGUI-3.0 widget ScrollFrame
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, frame)
|
||||
local itemFrame = CreateFrame("Frame", nil, scrollFrame)
|
||||
|
||||
self.scrollFrame = scrollFrame
|
||||
self.itemFrame = itemFrame
|
||||
|
||||
scrollFrame.obj = self
|
||||
itemFrame.obj = self
|
||||
|
||||
local slider = CreateFrame("Slider", "AceGUI30PulloutScrollbar"..count, scrollFrame)
|
||||
slider:SetOrientation("VERTICAL")
|
||||
slider:SetHitRectInsets(0, 0, -10, 0)
|
||||
slider:SetBackdrop(sliderBackdrop)
|
||||
slider:SetWidth(8)
|
||||
slider:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Vertical")
|
||||
slider:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
self.slider = slider
|
||||
slider.obj = self
|
||||
|
||||
scrollFrame:SetScrollChild(itemFrame)
|
||||
scrollFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -12)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -6, 12)
|
||||
scrollFrame:EnableMouseWheel(true)
|
||||
scrollFrame:SetScript("OnMouseWheel", OnMouseWheel)
|
||||
scrollFrame:SetScript("OnSizeChanged", OnSizeChanged)
|
||||
scrollFrame:SetToplevel(true)
|
||||
scrollFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
itemFrame:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
|
||||
itemFrame:SetPoint("TOPRIGHT", scrollFrame, "TOPRIGHT", -12, 0)
|
||||
itemFrame:SetHeight(400)
|
||||
itemFrame:SetToplevel(true)
|
||||
itemFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
slider:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", -16, 0)
|
||||
slider:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", -16, 0)
|
||||
slider:SetScript("OnValueChanged", OnScrollValueChanged)
|
||||
slider:SetMinMaxValues(0, 1000)
|
||||
slider:SetValueStep(1)
|
||||
slider:SetValue(0)
|
||||
|
||||
scrollFrame:Show()
|
||||
itemFrame:Show()
|
||||
slider:Hide()
|
||||
|
||||
self:FixScroll()
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion)
|
||||
end
|
||||
|
||||
do
|
||||
local widgetType = "Dropdown"
|
||||
local widgetVersion = 22
|
||||
|
||||
--[[ Static data ]]--
|
||||
|
||||
--[[ UI event handler ]]--
|
||||
|
||||
local function Control_OnEnter(this)
|
||||
this.obj:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function Control_OnLeave(this)
|
||||
this.obj:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function Dropdown_OnHide(this)
|
||||
local self = this.obj
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
|
||||
local function Dropdown_TogglePullout(this)
|
||||
local self = this.obj
|
||||
PlaySound("igMainMenuOptionCheckBoxOn") -- missleading name, but the Blizzard code uses this sound
|
||||
if self.open then
|
||||
self.open = nil
|
||||
self.pullout:Close()
|
||||
AceGUI:ClearFocus()
|
||||
else
|
||||
self.open = true
|
||||
self.pullout:SetWidth(self.frame:GetWidth())
|
||||
self.pullout:Open("TOPLEFT", self.frame, "BOTTOMLEFT", 0, self.label:IsShown() and -2 or 0)
|
||||
AceGUI:SetFocus(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnPulloutOpen(this)
|
||||
local self = this.userdata.obj
|
||||
local value = self.value
|
||||
|
||||
if not self.multiselect then
|
||||
for i, item in this:IterateItems() do
|
||||
item:SetValue(item.userdata.value == value)
|
||||
end
|
||||
end
|
||||
|
||||
self.open = true
|
||||
end
|
||||
|
||||
local function OnPulloutClose(this)
|
||||
local self = this.userdata.obj
|
||||
self.open = nil
|
||||
self:Fire("OnClosed")
|
||||
end
|
||||
|
||||
local function ShowMultiText(self)
|
||||
local text
|
||||
for i, widget in self.pullout:IterateItems() do
|
||||
if widget.type == "Dropdown-Item-Toggle" then
|
||||
if widget:GetValue() then
|
||||
if text then
|
||||
text = text..", "..widget:GetText()
|
||||
else
|
||||
text = widget:GetText()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self:SetText(text)
|
||||
end
|
||||
|
||||
local function OnItemValueChanged(this, event, checked)
|
||||
local self = this.userdata.obj
|
||||
|
||||
if self.multiselect then
|
||||
self:Fire("OnValueChanged", this.userdata.value, checked)
|
||||
ShowMultiText(self)
|
||||
else
|
||||
if checked then
|
||||
self:SetValue(this.userdata.value)
|
||||
self:Fire("OnValueChanged", this.userdata.value)
|
||||
else
|
||||
this:SetValue(true)
|
||||
end
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Exported methods ]]--
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnAcquire(self)
|
||||
local pullout = AceGUI:Create("Dropdown-Pullout")
|
||||
self.pullout = pullout
|
||||
pullout.userdata.obj = self
|
||||
pullout:SetCallback("OnClose", OnPulloutClose)
|
||||
pullout:SetCallback("OnOpen", OnPulloutOpen)
|
||||
self.pullout.frame:SetFrameLevel(self.frame:GetFrameLevel() + 1)
|
||||
fixlevels(self.pullout.frame, self.pullout.frame:GetChildren())
|
||||
|
||||
self:SetHeight(44)
|
||||
self:SetWidth(200)
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnRelease(self)
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
AceGUI:Release(self.pullout)
|
||||
self.pullout = nil
|
||||
|
||||
self:SetText("")
|
||||
self:SetLabel("")
|
||||
self:SetDisabled(false)
|
||||
self:SetMultiselect(false)
|
||||
|
||||
self.value = nil
|
||||
self.list = nil
|
||||
self.open = nil
|
||||
self.hasClose = nil
|
||||
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.text:SetTextColor(0.5,0.5,0.5)
|
||||
self.button:Disable()
|
||||
self.label:SetTextColor(0.5,0.5,0.5)
|
||||
else
|
||||
self.button:Enable()
|
||||
self.label:SetTextColor(1,.82,0)
|
||||
self.text:SetTextColor(1,1,1)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function ClearFocus(self)
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetText(self, text)
|
||||
self.text:SetText(text or "")
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetLabel(self, text)
|
||||
if text and text ~= "" then
|
||||
self.label:SetText(text)
|
||||
self.label:Show()
|
||||
self.dropdown:SetPoint("TOPLEFT",self.frame,"TOPLEFT",-15,-18)
|
||||
self.frame:SetHeight(44)
|
||||
else
|
||||
self.label:SetText("")
|
||||
self.label:Hide()
|
||||
self.dropdown:SetPoint("TOPLEFT",self.frame,"TOPLEFT",-15,0)
|
||||
self.frame:SetHeight(26)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetValue(self, value)
|
||||
if self.list then
|
||||
self:SetText(self.list[value] or "")
|
||||
end
|
||||
self.value = value
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetValue(self)
|
||||
return self.value
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetItemValue(self, item, value)
|
||||
if not self.multiselect then return end
|
||||
for i, widget in self.pullout:IterateItems() do
|
||||
if widget.userdata.value == item then
|
||||
if widget.SetValue then
|
||||
widget:SetValue(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
ShowMultiText(self)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetItemDisabled(self, item, disabled)
|
||||
for i, widget in self.pullout:IterateItems() do
|
||||
if widget.userdata.value == item then
|
||||
widget:SetDisabled(disabled)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function AddListItem(self, value, text)
|
||||
local item = AceGUI:Create("Dropdown-Item-Toggle")
|
||||
item:SetText(text)
|
||||
item.userdata.obj = self
|
||||
item.userdata.value = value
|
||||
item:SetCallback("OnValueChanged", OnItemValueChanged)
|
||||
self.pullout:AddItem(item)
|
||||
end
|
||||
|
||||
local function AddCloseButton(self)
|
||||
if not self.hasClose then
|
||||
local close = AceGUI:Create("Dropdown-Item-Execute")
|
||||
close:SetText(CLOSE)
|
||||
self.pullout:AddItem(close)
|
||||
self.hasClose = true
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local sortlist = {}
|
||||
local function SetList(self, list)
|
||||
self.list = list
|
||||
self.pullout:Clear()
|
||||
self.hasClose = nil
|
||||
if not list then return end
|
||||
|
||||
for v in pairs(list) do
|
||||
sortlist[#sortlist + 1] = v
|
||||
end
|
||||
tsort(sortlist)
|
||||
|
||||
for i, value in pairs(sortlist) do
|
||||
AddListItem(self, value, list[value])
|
||||
sortlist[i] = nil
|
||||
end
|
||||
if self.multiselect then
|
||||
ShowMultiText(self)
|
||||
AddCloseButton(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function AddItem(self, value, text)
|
||||
if self.list then
|
||||
self.list[value] = text
|
||||
AddListItem(self, value, text)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetMultiselect(self, multi)
|
||||
self.multiselect = multi
|
||||
if multi then
|
||||
ShowMultiText(self)
|
||||
AddCloseButton(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetMultiselect(self)
|
||||
return self.multiselect
|
||||
end
|
||||
|
||||
--[[ Constructor ]]--
|
||||
|
||||
local function Constructor()
|
||||
local count = AceGUI:GetNextWidgetNum(widgetType)
|
||||
local frame = CreateFrame("Frame", nil, UIParent)
|
||||
local dropdown = CreateFrame("Frame", "AceGUI30DropDown"..count, frame, "UIDropDownMenuTemplate")
|
||||
|
||||
local self = {}
|
||||
self.type = widgetType
|
||||
self.frame = frame
|
||||
self.dropdown = dropdown
|
||||
self.count = count
|
||||
frame.obj = self
|
||||
dropdown.obj = self
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.ClearFocus = ClearFocus
|
||||
|
||||
self.SetText = SetText
|
||||
self.SetValue = SetValue
|
||||
self.GetValue = GetValue
|
||||
self.SetList = SetList
|
||||
self.SetLabel = SetLabel
|
||||
self.SetDisabled = SetDisabled
|
||||
self.AddItem = AddItem
|
||||
self.SetMultiselect = SetMultiselect
|
||||
self.GetMultiselect = GetMultiselect
|
||||
self.SetItemValue = SetItemValue
|
||||
self.SetItemDisabled = SetItemDisabled
|
||||
|
||||
self.alignoffset = 31
|
||||
|
||||
frame:SetHeight(44)
|
||||
frame:SetWidth(200)
|
||||
frame:SetScript("OnHide",Dropdown_OnHide)
|
||||
|
||||
dropdown:ClearAllPoints()
|
||||
dropdown:SetPoint("TOPLEFT",frame,"TOPLEFT",-15,0)
|
||||
dropdown:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",17,0)
|
||||
dropdown:SetScript("OnHide", nil)
|
||||
|
||||
local left = _G[dropdown:GetName() .. "Left"]
|
||||
local middle = _G[dropdown:GetName() .. "Middle"]
|
||||
local right = _G[dropdown:GetName() .. "Right"]
|
||||
|
||||
middle:ClearAllPoints()
|
||||
right:ClearAllPoints()
|
||||
|
||||
middle:SetPoint("LEFT", left, "RIGHT", 0, 0)
|
||||
middle:SetPoint("RIGHT", right, "LEFT", 0, 0)
|
||||
right:SetPoint("TOPRIGHT", dropdown, "TOPRIGHT", 0, 17)
|
||||
|
||||
local button = _G[dropdown:GetName() .. "Button"]
|
||||
self.button = button
|
||||
button.obj = self
|
||||
button:SetScript("OnEnter",Control_OnEnter)
|
||||
button:SetScript("OnLeave",Control_OnLeave)
|
||||
button:SetScript("OnClick",Dropdown_TogglePullout)
|
||||
|
||||
local text = _G[dropdown:GetName() .. "Text"]
|
||||
self.text = text
|
||||
text.obj = self
|
||||
text:ClearAllPoints()
|
||||
text:SetPoint("RIGHT", right, "RIGHT" ,-43, 2)
|
||||
text:SetPoint("LEFT", left, "LEFT", 25, 2)
|
||||
|
||||
local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
|
||||
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
|
||||
label:SetPoint("TOPRIGHT",frame,"TOPRIGHT",0,0)
|
||||
label:SetJustifyH("LEFT")
|
||||
label:SetHeight(18)
|
||||
label:Hide()
|
||||
self.label = label
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion)
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user