38 Commits

Author SHA1 Message Date
sindoring 597f4030a7 dvc 2026-08-22 19:10:11 +04:00
gasaichandesu ebd76275a1 fix(transmog): remove embed.xml from Transmogrification.toc because this
file does not exist anymore
2026-08-19 20:28:53 +04:00
gasaichandesu 6addd9bb51 feat(premium): add summon squire spell to Spell.dbc 2026-08-19 16:59:32 +04:00
sindoring a984e705c3 обновление сборщика 2026-08-16 16:56:34 +04:00
sindoring 1a80ca8ad8 new customization 2026-08-13 00:55:19 +04:00
sindoring b4a2eb8bc2 переход на warcraftxl 1.1 2026-08-10 17:40:03 +04:00
gasaichandesu ae178d4a18 Merge branch 'master' of ssh://git.moon-well.online:2222/sindoring/moonwell-client 2026-08-08 07:16:25 +04:00
gasaichandesu e5ad71bc59 refactor(addons): extract shared addons to top level AddOns directory 2026-08-08 07:15:33 +04:00
sindoring b0ac94509a wow original exe 2026-08-08 00:40:16 +04:00
sindoring 2e98622454 warcraftxl without submodukle 2026-08-07 17:47:58 +04:00
sindoring e48bb6fc03 fix 2026-08-07 14:28:45 +04:00
gasaichan a3b7b834b7 Merge pull request 'feat(transmog): add transmogrification feature' (#2) from feature/transmog into master
Reviewed-on: #2
2026-08-07 10:04:04 +03:00
gasaichandesu f82bbf252c Merge branch 'master' of ssh://git.moon-well.online:2222/sindoring/moonwell-client into feature/transmog 2026-08-07 08:07:32 +04:00
gasaichandesu a3fc195f14 feat(transmog): add transmogrification feature 2026-08-07 08:03:16 +04:00
sindoring c1c2b4e8bb добавлен упущенный laa 2026-08-05 01:58:29 +04:00
sindoring 83f5e08689 фикс манифеста 2026-08-04 20:15:00 +04:00
sindoring 76ca0cc851 fix 2026-08-04 08:49:51 +04:00
sindoring 398245d27d fix dbc 2026-07-27 00:31:44 +04:00
sindoring 887ab7879b одиночные подземелья 2026-07-26 22:48:10 +04:00
sindoring e3b8d2649e Update WarcraftXL storage fallback for loose models 2026-07-23 09:51:05 +04:00
sindoring e916cdea19 Add MDX aliases for modern creature models 2026-07-23 01:41:39 +04:00
sindoring befdaa456d Stage modern creature assets through WarcraftXL host 2026-07-23 01:36:59 +04:00
sindoring 7291fff664 Add server model metadata for custom pets 2026-07-23 01:25:12 +04:00
sindoring 790506fc7a Generate companion summon spells 2026-07-23 01:14:05 +04:00
sindoring 87329f2633 Automate custom mount and pet DBC generation 2026-07-23 00:37:42 +04:00
sindoring 8375b95f7b Add retail catslime model support 2026-07-23 00:19:46 +04:00
sindoring 01f476ca1b fix case-insensitive manifest merging 2026-07-19 16:19:36 +04:00
sindoring 63c9af7846 fix production manifest publishing 2026-07-19 16:09:51 +04:00
sindoring f02f4d73f0 fix ej 2026-07-19 15:39:52 +04:00
sindoring 721833bb50 Merge branch 'encounter-journal' 2026-07-19 12:50:06 +04:00
sindoring b30a3554cf фикс журнала 2026-07-19 12:42:10 +04:00
sindoring 595ed5091d восстановил камеру создания персонажа 2026-07-18 19:08:12 +04:00
sindoring 2291ace9f7 Merge branch 'master' into encounter-journal 2026-07-18 15:49:11 +04:00
sindoring d5e2dce359 правки по encounter journal 2026-06-13 15:07:58 +04:00
sindoring 22ae941c4a Merge branch 'master' into encounter-journal 2026-06-13 11:52:07 +04:00
sindoring b0da9ec10b базовый encounter journal 2026-06-13 11:46:39 +04:00
sindoring d8d94f10b0 стабильная версия encounter journal 2026-05-16 23:43:39 +04:00
sindoring 255485daca work in progress 2026-05-06 23:15:58 +04:00
948 changed files with 71645 additions and 344 deletions
+3
View File
@@ -0,0 +1,3 @@
/config.local
/tmp
/cache
+8
View File
@@ -0,0 +1,8 @@
[cache]
type = "hardlink,copy"
[core]
remote = customization
['remote "customization"']
url = s3://warcraft-client/dvc/moonwell-client-sources/customization
endpointurl = https://storage.yandexcloud.net
region = ru-central1
+3
View File
@@ -0,0 +1,3 @@
# Add patterns of files dvc should ignore, which could improve
# the performance. Learn more at
# https://dvc.org/doc/user-guide/dvcignore
+8
View File
@@ -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=
PRODUCTION_REALMLIST=
PTR_REALMLIST=
LOCAL_REALMLIST=
SSH_USER=root
SSH_HOST=strictlypragmatic.ru
+2
View File
@@ -5,6 +5,8 @@ manifest.json
.vscode
dist
build/
/new customization mpqs/*.mpq
Wow*.exe
*.backup.exe
Logs/
!Wow_Original.exe
+19 -9
View File
@@ -1,15 +1,25 @@
[submodule "vendor/warcraftxl"]
path = vendor/warcraftxl
url = https://github.com/WarcraftXL/wxl-core.git
[submodule "vendor/modules/wxl-modern-assets"]
path = vendor/modules/wxl-modern-assets
url = https://github.com/WarcraftXL/wxl-modern-assets.git
[submodule "vendor/modules/wxl-modern-render"]
path = vendor/modules/wxl-modern-render
url = https://github.com/WarcraftXL/wxl-modern-render.git
[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
+63 -50
View File
@@ -1,64 +1,77 @@
cmake_minimum_required(VERSION 3.20)
project(MoonWellWarcraftXL LANGUAGES CXX)
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
# its aggregate DLL target here, so updating the framework does not mix our
# client-specific offsets and policy into the upstream checkout.
# WarcraftXL is an untouched upstream v1.1 submodule. Every project-specific
# feature below is built as a separately loaded Extensions/<name>/<name>.dll.
add_subdirectory(vendor/warcraftxl)
set(WXL_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/warcraftxl")
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
set(WXL_LOCAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules")
function(wxl_collect_sources output)
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
set(${output} "${collected}" PARENT_SCOPE)
function(wxl_add_external_extension extension_name extension_dir)
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
return()
endif()
file(GLOB_RECURSE extension_sources CONFIGURE_DEPENDS "${extension_dir}/src/*.cpp")
if(NOT extension_sources)
message(FATAL_ERROR "WarcraftXL extension '${extension_name}' has no sources in ${extension_dir}/src")
endif()
set(extension_shared_sources "")
if(extension_name STREQUAL "wxl-modern-m2")
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
"${WXL_CORE_DIR}/src/engine/assets/shared/models/m2/*.cpp")
elseif(extension_name STREQUAL "wxl-modern-blp")
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
"${WXL_CORE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
endif()
file(GLOB wxl_sdk_sources CONFIGURE_DEPENDS "${WXL_CORE_DIR}/src/game/*.cpp")
add_library(${extension_name} SHARED
${extension_sources}
${extension_shared_sources}
${wxl_sdk_sources})
set_target_properties(${extension_name} PROPERTIES
OUTPUT_NAME "${extension_name}"
PREFIX "")
target_include_directories(${extension_name} PRIVATE
"${extension_dir}/src"
"${WXL_CORE_DIR}/include"
"${WXL_CORE_DIR}/src")
target_compile_features(${extension_name} PRIVATE cxx_std_20)
target_compile_definitions(${extension_name} PRIVATE
WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS WXL_EXTENSION)
if(CLIENT_PATH)
add_custom_command(TARGET ${extension_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"${CLIENT_PATH}/Extensions/${extension_name}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${extension_name}>"
"${CLIENT_PATH}/Extensions/${extension_name}/${extension_name}.dll"
COMMENT "Deploy ${extension_name}.dll -> Extensions/${extension_name}")
endif()
endfunction()
if(TARGET WarcraftXL)
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
endif()
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
# Official WarcraftXL 1.1 extensions.
wxl_add_external_extension(wxl-db2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-db2")
wxl_add_external_extension(wxl-modern-adt "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt")
wxl_add_external_extension(wxl-modern-blp "${WXL_LOCAL_MODULES_DIR}/wxl-modern-blp")
wxl_add_external_extension(wxl-modern-m2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-m2")
wxl_add_external_extension(wxl-modern-wmo "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-wmo")
wxl_add_external_extension(wxl-grasswind "${WXL_EXTERNAL_MODULES_DIR}/wxl-grasswind")
wxl_add_external_extension(wxl-unit-outline "${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline")
target_sources(WarcraftXL PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
${WXL_MODERN_ASSETS_RUNTIME}
${WXL_MODERN_RENDER_RUNTIME}
${WXL_UNIT_OUTLINE_RUNTIME}
${WXL_MODERN_ADT_RUNTIME})
# 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")
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")
# wxl-modern-render needs its module-root includes and D3D12 import library.
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
endif()
if(TARGET WarcraftXLHost)
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
endif()
wxl_collect_sources(WXL_MODERN_ADT_HOST
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
target_sources(WarcraftXLHost PRIVATE
${WXL_MODERN_ASSETS_HOST}
${WXL_MODERN_ADT_HOST})
endif()
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
# Recovery loader retained for existing -NativeRenderer workflows. WarcraftXL
# 1.1 already ships a native D3D9 forwarding proxy as its standard renderer.
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
add_library(MoonWellLoader SHARED
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
+83
View File
@@ -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`.
+355
View File
@@ -0,0 +1,355 @@
# Совместимость CVPatch и расширенной кастомизации
Документ описывает фактический набор графических MPQ MoonWell для клиента
**WoW 3.3.5a, build 12340**, их порядок загрузки, конфликты DBC, сборку слоя
совместимости и серверные требования парикмахерской.
## Исходные данные и происхождение
Старые архивы `patch-ruRU-A..W` относятся к **CVPatch (Collection Visual
Patch)** от Vary/vvladoon. Найденная тема оригинального проекта:
<https://forum.uwow.biz/threads/cvpatch-collection-visual-patch-lich-king-3-3-5.296264/>
Источник новых архивов утрачен. Единственная сохранённая подпись находится в
`new customization mpqs/readme.txt`:
- `patch-a-001.mpq``Model and Textures NPC`;
- `patch-b-002.mpq``Characters HD The War Within`;
- `patch-k.mpq``Interface`.
Новые архивы нельзя считать официальным или полностью автономным комплектом
для 3.3.5a: в них отсутствовал совместимый `BarberShopStyle.dbc`, а часть DBC
содержит данные более новых версий игры.
## Установленная часть CVPatch
| Патч | Назначение | Зависимости и пересечения |
|---|---|---|
| A | Основные модели существ, гуманоидов и транспорта | Основа для старого B |
| B | Альтернативные модели существ, гуманоидов и транспорта | Требует A |
| E | Текстуры экипировки в повышенном разрешении | Независимый |
| H | HD-модели игровых персонажей Leeviathan | Перекрывается новыми X/Y на одинаковых путях |
| I | Новые модели и текстуры брони и оружия | Изначально требовал H; его item-поля сохраняются при слиянии |
| M | Карты классических подземелий и пещер | Независимый |
| S | Эффекты заклинаний и способностей | Содержит собственные spell DBC |
| T | Текстуры зданий и зон | Независимый |
| U | Окружение Низины Арати и Ущелья Песни Войны | Независимый |
| W | Вода | Независимый |
Старые буквенные имена не меняются. X, Y и Z специально выбраны после W,
чтобы новый комплект и исправления имели приоритет без переименования CVPatch.
## Новые исходные архивы
### `patch-a-001.mpq` → `patch-ruRU-X.MPQ`
Размер исходника — около 2,49 GiB, `(listfile)` содержит 29 495 файлов:
- 16 023 запечённых NPC-текстуры в `Textures\bakednpctextures`;
- 13 465 файлов моделей, skin, анимаций и текстур в `Character`;
- модели стандартных рас и ряда NPC-рас: Vrykul, Naga, Taunka, Forest Troll и
других;
- 28 120 BLP, 1 245 ANIM, 94 SKIN и 29 M2;
- 7 DBC.
DBC внутри X:
| DBC | Строк | Роль |
|---|---:|---|
| `CharacterFacialHairStyles.dbc` | 272 | Штатный объём лицевой кастомизации |
| `CharHairGeosets.dbc` | 370 | Штатный объём причёсок |
| `CharSections.dbc` | 10 060 | Секции текстур персонажей |
| `CreatureDisplayInfoExtra.dbc` | 15 477 | Дополнительные параметры отображения существ |
| `EmotesTextSound.dbc` | 782 | Звуки текстовых эмоций |
| `HelmetGeosetVisData.dbc` | 21 | Скрытие geoset под шлемами |
| `SpellVisualKitModelAttach.dbc` | 594 | Крепление моделей визуальных эффектов |
X нужен текущему комбинированному комплекту для NPC-моделей и запечённых
текстур. Для новых причёсок главным является Y, который загружается позже и
перекрывает одноимённые таблицы X.
### `patch-b-002.mpq` → `patch-ruRU-Y.MPQ`
Размер исходника — около 2,80 GiB, `(listfile)` содержит 36 191 файл:
- 36 180 файлов в `Character`;
- HD-модели и текстуры десяти стандартных игровых рас WotLK;
- 34 934 BLP, 1 021 ANIM, 125 BONE, 80 SKIN и 20 M2;
- 11 DBC с существенно расширенной кастомизацией.
DBC внутри Y:
| DBC | Строк | Роль |
|---|---:|---|
| `CharacterFacialHairStyles.dbc` | 3 616 | Бороды, серьги, рога и другие лицевые варианты |
| `CharHairGeosets.dbc` | 854 | Geoset и номера причёсок |
| `CharHairTextures.dbc` | 177 | Текстуры волос |
| `CharSections.dbc` | 614 212 | Расширенные секции кожи, лица, волос и их сочетаний |
| `CharVariations.dbc` | 62 | Наборы вариаций персонажей |
| `ChrRaces.dbc` | 41 | Описания рас, включая данные за пределами штатного WotLK |
| `CreatureDisplayInfo.dbc` | 24 282 | Display ID моделей |
| `CreatureDisplayInfoExtra.dbc` | 15 477 | Раса, пол, внешность и предметные поля display ID |
| `CreatureFamily.dbc` | 40 | Семейства существ |
| `CreatureModelData.dbc` | 1 353 | Пути и параметры моделей |
| `HelmetGeosetVisData.dbc` | 65 | Расширенные правила отображения под шлемами |
Y обязателен для расширенной кастомизации. Z не содержит копии всех этих
таблиц и не заменяет Y.
Наличие современных race ID в DBC не добавляет новые игровые расы в ядро или
интерфейс 3.3.5a. Текущая интеграция поддерживает расширение внешности десяти
штатных рас. Новые игровые расы потребуют отдельной доработки клиента, UI,
сетевого протокола и сервера.
### `patch-k.mpq` — не устанавливается
Размер — около 53 MiB, `(listfile)` содержит 673 файла только в `Interface`:
- 547 BLP;
- 68 SKIN и 24 M2 для экранов входа и выбора персонажа;
- 15 Lua, 12 XML, TOC и вспомогательные файлы.
Архив меняет `Interface\Glues`, `Interface\GlueXML`, модели экранов входа и
создания персонажа. Он намеренно исключён из сборки, потому что перекрывает
кастомный интерфейс MoonWell. Устанавливать его вручную поверх текущего клиента
нельзя без отдельного разбора и слияния UI.
## Итоговый порядок загрузки
В локализованной цепочке `Data\ruRU` используется следующий порядок:
1. штатные архивы клиента, включая `patch-ruRU-3.MPQ`;
2. старые CVPatch `patch-ruRU-A..W`;
3. `patch-ruRU-X.MPQ` — неизменённая копия `patch-a-001.mpq`;
4. `patch-ruRU-Y.MPQ` — неизменённая копия `patch-b-002.mpq`;
5. `patch-ruRU-Z.MPQ` — сгенерированный слой совместимости MoonWell.
Z является последней инстанцией для конфликтующих DBC. Нельзя переносить его
раньше X/Y или устанавливать другой locale MPQ после Z, который содержит те же
таблицы.
## Содержимое `patch-ruRU-Z.MPQ`
В текущей сборке Z содержит:
- `BarberShopStyle.dbc` — расширенная таблица парикмахерской;
- `CreatureDisplayInfoExtra.dbc` — результат слияния новых display ID и
предметных полей CVPatch I;
- `SpellVisualKitModelAttach.dbc` — более полная версия из CVPatch S;
- `CreatureModelData.dbc` — сгенерированное слияние штатных, CVPatch и новых
моделей;
- `CreatureDisplayInfo.dbc` — сгенерированное слияние display ID;
- `WXLFileData.csv` и `WXLSpellOverrides.tsv` для WarcraftXL.
Последние четыре файла добавляются упаковщиком автоматически; поэтому состав
папки `src\Data\ruRU\patch-ruRU-Z` и готового MPQ не обязан совпадать буквально.
### Правила слияния DBC
`tool/scripts/build-customization-compat.ps1` выполняет следующие операции:
- берёт штатную базу из `patch-ruRU-3.MPQ`;
- сохраняет уникальные изменения `CreatureModelData` из CVPatch A;
- сохраняет уникальные изменения `CreatureDisplayInfo` из CVPatch B;
- отдаёт приоритет Y при реальном конфликте одной и той же строки;
- переносит изменённые item-поля `CreatureDisplayInfoExtra` из CVPatch I в
новые строки Y;
- восстанавливает `SpellVisualKitModelAttach.dbc` из CVPatch S;
- генерирует расширенный `BarberShopStyle.dbc`;
- обновляет входные `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` для
генератора MoonWell creatures.
Так Z не позволяет Y случайно уничтожить старые изменения существ, экипировки
и эффектов, а генерация MoonWell creatures не уничтожает новые display ID.
## Расширенная парикмахерская
### Причина исходного падения
Y расширил:
- `CharHairGeosets.dbc` с 370 до 854 строк;
- `CharacterFacialHairStyles.dbc` с 272 до 3 616 строк.
При этом исходные новые MPQ не содержали `BarberShopStyle.dbc`. Клиент продолжал
использовать штатную таблицу на 491 строку. При выборе нового варианта функция
`GetBarberShopTotalCost` не находила запись, после чего WoW падал с
`ERROR #132 / ACCESS_VIOLATION`. В зафиксированном крэше вызов шёл от
`BarberShopFrameSelector1Next`.
### Как сформирована новая таблица
Генератор:
- сохраняет все 491 штатную строку и исходную строковую таблицу байт-в-байт;
- добавляет отсутствующие причёски из `CharHairGeosets.dbc` Y;
- добавляет отсутствующую растительность и украшения из
`CharacterFacialHairStyles.dbc` Y;
- работает только с сочетаниями расы, пола и типа, уже существующими в штатном
`BarberShopStyle.dbc`, то есть с поддерживаемыми расами WotLK;
- сохраняет штатные строки типа 3 для особой кастомизации рыцарей смерти;
- назначает новые уникальные ID после максимального штатного ID;
- проверяет отсутствие дубликатов и полноту покрытия.
Результат:
- 3 765 строк всего;
- 152 новые причёски;
- 3 122 новых варианта растительности, серёг и других особенностей;
- максимальный номер лицевого варианта — 255, что помещается в поле внешности
персонажа 3.3.5a.
В исходном комплекте нет названий новых стилей. Поэтому сейчас они отображаются
как `New hairstyle N` и `New appearance N`. Это только подписи: на выбор модели
и сохранение внешности они не влияют.
## Что требуется клиенту и серверу
| Компонент | Клиент | Сервер |
|---|---|---|
| `patch-ruRU-X.MPQ` | Да, для текущего комплекта NPC/персонажей | Нет |
| `patch-ruRU-Y.MPQ` | Да, для моделей, текстур и основных customization DBC | Нет |
| `patch-ruRU-Z.MPQ` | Да, для совместимости и исправления парикмахерской | Нет как MPQ |
| Сгенерированный `BarberShopStyle.dbc` | Уже находится внутри Z | Да, отдельным DBC |
| Остальные клиентские DBC из X/Y | Загружаются из MPQ | Для текущих десяти рас не копируются |
Серверную копию нужно положить в каталог DBC worldserver, обычно:
```text
<server>/data/dbc/BarberShopStyle.dbc
```
После замены требуется полный перезапуск worldserver: DBC загружаются при
старте. Клиентская и серверная копии `BarberShopStyle.dbc` должны быть одним и
тем же сгенерированным файлом. Без серверной копии клиент больше не должен
падать при переключении, но сервер может отклонить применение нового стиля или
неверно рассчитать его стоимость.
Готовая серверная копия после запуска генератора находится в:
```text
build/customization-compat/server-dbc/BarberShopStyle.dbc
```
Тот же отслеживаемый исходник для Z находится в:
```text
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/BarberShopStyle.dbc
```
Не следует копировать на сервер `ChrRaces.dbc`, `CharSections.dbc` или весь
набор DBC из Y без отдельной задачи и проверки ядра.
## Сборка
### Предварительные условия
В `.env` должен быть задан корректный `WOW_HOME`. В клиенте должны находиться:
- штатный `Data\ruRU\patch-ruRU-3.MPQ`;
- CVPatch A, B, I и S, используемые генератором как источники слияния;
- остальные CVPatch, если они должны остаться в итоговой установке.
В репозитории должны оставаться:
- `new customization mpqs\patch-a-001.mpq`;
- `new customization mpqs\patch-b-002.mpq`;
- `tool\target\release\mpqread.exe`.
### Пересоздание DBC совместимости
```powershell
.\tool\scripts\build-customization-compat.ps1
```
Скрипт обновляет:
```text
assets/dbc/3.3.5a/CreatureModelData.dbc
assets/dbc/3.3.5a/CreatureDisplayInfo.dbc
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/CreatureDisplayInfoExtra.dbc
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/SpellVisualKitModelAttach.dbc
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/BarberShopStyle.dbc
build/customization-compat/server-dbc/BarberShopStyle.dbc
```
### Упаковка MPQ
```powershell
.\tool\target\release\tool.exe .\src .\dist
```
Упаковщик:
- собирает Z из исходников и сгенерированных MoonWell DBC;
- не копирует внешние графические X/Y в `dist` и не включает их в managed manifest;
- не добавляет `patch-k.mpq`.
Для полной локальной сборки и синхронизации клиента можно использовать:
```powershell
.\run.ps1 -Env local
```
Этот сценарий также собирает WarcraftXL, останавливает блокирующие процессы и
синхронизирует весь `dist` в `WOW_HOME`. Для проверки только изменения Z можно
собрать архив упаковщиком и скопировать лишь
`dist\Data\ruRU\patch-ruRU-Z.MPQ`.
Сборка этих архивов не должна автоматически изменять `manifest.json`. Манифест
обновляется владельцем проекта отдельно после локальной проверки.
## Исходники MPQ и установленные копии
Файлы в `new customization mpqs` используются только как локальные источники
генератора таблиц совместимости. В готовом клиенте X и Y управляются отдельно от
репозиторных патчей.
Списки репозиторных и внешних графических патчей находятся в
`patch-layout.json`. `deploy.ps1`, `run.ps1`, генератор манифеста и S3 uploader
синхронизируют только список `repositoryPatches`. Пути из `graphicsPatches`
сохраняются без копирования и удаления. Если один путь указан в обоих списках,
`repositoryPatches` имеет приоритет.
Z не консолидирует многогигабайтные модели и текстуры X/Y. Удаление X или Y из
клиента не освобождается наличием текущих DBC в Z.
## Проверка после сборки
Минимальный сценарий локальной проверки:
1. Полностью закрыть WoW перед заменой MPQ.
2. Проверить наличие X, Y и Z в `Data\ruRU`.
3. Запустить клиент и открыть создание персонажа.
4. Проверить оба пола и основные расы, пролистать причёски и лицевые варианты.
5. Создать тестового персонажа, войти в мир и проверить модель после повторного
входа.
6. Открыть парикмахерскую и несколько раз нажать вперёд/назад у каждого
селектора.
7. Проверить отображение стоимости и отсутствие `ERROR #132`.
8. Применить новый стиль, выйти из мира и войти снова.
9. Проверить старую броню, NPC-модели и эффекты заклинаний, которые могли быть
затронуты конфликтующими DBC.
Для шага 8 сервер уже должен использовать сгенерированный
`BarberShopStyle.dbc`.
## Диагностика
| Симптом | Вероятная причина |
|---|---|
| Новых причёсок нет | Отсутствует Y или более поздний MPQ перекрыл его `CharHairGeosets`/`CharSections` |
| Модель отсутствует или отображается неверно | X/Y не скопированы полностью либо повреждены многогигабайтные архивы |
| Падение при переключении в парикмахерской | Z отсутствует, загружен раньше Y или его `BarberShopStyle.dbc` перекрыт |
| Вариант выбирается, но не применяется | На сервере остался штатный `BarberShopStyle.dbc` или worldserver не перезапущен |
| Исчезли старые NPC/display ID | Z не пересобран после изменения источников CVPatch или Y |
| Проблемы со старой экипировкой | Потеряны item-поля CVPatch I в `CreatureDisplayInfoExtra.dbc` |
| Изменился экран входа/создания персонажа | `patch-k.mpq` был установлен вручную |
Перед заменой рабочего Z рекомендуется сохранять предыдущий архив. Последняя
локальная резервная копия, созданная во время исправления парикмахерской:
```text
build/customization-compat/client-backup/patch-ruRU-Z.before-barbershop.MPQ
```
+28 -29
View File
@@ -2,7 +2,7 @@
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL): `Wow.exe` больше не содержит
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL/wxl-core) **1.1.220**: `Wow.exe` больше не содержит
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
только к памяти запущенного процесса.
@@ -14,25 +14,25 @@ MoonWell-патчей. Неподписанный оригинальный execu
- одиннадцатое значение `GetCharacterInfo``charFlags`, включая флаг предателя `0x40000000`;
- загрузка WarcraftXL без import-table patch и без отдельного injector.
Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
При установке сборщик включает Large Address Aware в копии `Wow.exe`; исходный
`Wow_Original.exe` остаётся неизменным и используется как проверенный шаблон.
## Структура
```text
modules/moonwell/ MoonWell runtime-модуль WarcraftXL
modules/ отдельные MoonWell extensions для WarcraftXL 1.1
vendor/warcraftxl/ закреплённый upstream git submodule
vendor/modules/ закреплённые модули WarcraftXL
src/Data/ исходники MPQ-патчей
tool/ сборщик MPQ
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host
build-warcraftxl.ps1 сборка Win32 runtime, proxy и extensions
run.ps1 полная сборка, установка и запуск клиента
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
```
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule.
Подключены совместимые с 1.1 extensions: `wxl-db2`, `wxl-modern-adt`, `wxl-modern-m2`,
`wxl-modern-wmo`, `wxl-grasswind` и `wxl-unit-outline`. BLP-адаптер и MoonWell-изменения
собираются как отдельные DLL; исходники upstream core не изменяются.
Для обновления всех зависимостей WarcraftXL:
```powershell
@@ -44,9 +44,11 @@ git submodule update --remote vendor/warcraftxl vendor/modules/*
## Требования
- Visual Studio 2022 с C++ toolchain для Win32 и x64;
- Visual Studio 2022 с C++ toolchain для Win32;
- CMake 3.20+;
- Rust/Cargo для существующего MPQ-сборщика;
- Python 3 и DVC с поддержкой S3 для содержимого внешних графических патчей
(`py -m pip install -r requirements-dvc.txt`);
- локальный оригинальный `Wow_Original.exe` с SHA-256
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
@@ -64,7 +66,7 @@ git submodule update --init --recursive
.\deploy.ps1
```
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host,
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, native D3D9 proxy и extensions,
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
@@ -77,8 +79,6 @@ git submodule update --init --recursive
# Быстро обновить только WarcraftXL без пересборки MPQ
.\deploy.ps1 -SkipDataBuild
# Аварийный native D3D9 вместо D3D9On12
.\deploy.ps1 -NativeRenderer
```
Без установки в клиент:
@@ -99,18 +99,8 @@ git submodule update --init --recursive
1. проверяет хеш `Wow_Original.exe`;
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
3. восстанавливает оригинальный `Wow.exe`;
4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`;
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`;
6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом
модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и
установить native-переходник без modern-render:
```powershell
.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
4. устанавливает `WarcraftXL.dll`, native D3D9 proxy и DLL в `Extensions\`;
5. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
@@ -121,8 +111,7 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
.\run.ps1
```
Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и
`Utils\WarcraftXLHost.log` внутри клиента.
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
## MPQ-пакеты
@@ -136,8 +125,18 @@ WarcraftXL не требуется и в объектное хранилище
- `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
Управляемые репозиторием MPQ и внешние графические патчи перечислены отдельно в
`patch-layout.json`. Сборка и deploy синхронизируют только `repositoryPatches` и
не изменяют `graphicsPatches`. При пересечении списков репозиторный патч имеет
приоритет.
Распакованное содержимое больших патчей кастомизации хранится через DVC в
`assets/graphics/`, а бинарные объекты — в настроенном S3. Инструкции по первичной
загрузке, изменению и импорту MPQ находятся в
[`assets/graphics/README.md`](assets/graphics/README.md).
## Лицензирование
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный upstream submodule. MoonWell-модуль,
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`.
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
MoonWell extensions распространяются с доступным соответствующим исходным кодом. Репозиторий не
должен публиковать Blizzard assets или `Wow.exe`.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
/patch-X
+6
View File
@@ -0,0 +1,6 @@
outs:
- md5: 012ce72d8dfdf089b20a28f6e6158b58.dir
size: 1195310
nfiles: 4
hash: md5
path: patch-X
+14
View File
@@ -0,0 +1,14 @@
/patch-ruRU-X
/patch-ruRU-Y
/backup-ruRU
/patch-ruRU-6
/patch-ruRU-A
/patch-ruRU-B
/patch-ruRU-E
/patch-ruRU-H
/patch-ruRU-I
/patch-ruRU-M
/patch-ruRU-S
/patch-ruRU-T
/patch-ruRU-U
/patch-ruRU-W
@@ -0,0 +1,6 @@
outs:
- md5: b120587dfcd255f78392ca52705a00ff.dir
size: 38876467
nfiles: 161
hash: md5
path: backup-ruRU
@@ -0,0 +1,6 @@
outs:
- md5: 9ca2ee746d80e8a0c951c5ef476ae654.dir
size: 1064210
nfiles: 3
hash: md5
path: patch-ruRU-6
@@ -0,0 +1,6 @@
outs:
- md5: 97232ae859eaa1a16b137efd1a2e9122.dir
size: 2748988973
nfiles: 8817
hash: md5
path: patch-ruRU-A
@@ -0,0 +1,6 @@
outs:
- md5: bb686aebf7c9cae2c02433abde9a9f19.dir
size: 771051083
nfiles: 2911
hash: md5
path: patch-ruRU-B
@@ -0,0 +1,6 @@
outs:
- md5: d6b2fe0e3f23c47dc944ca4a8f82d97a.dir
size: 9275755440
nfiles: 21977
hash: md5
path: patch-ruRU-E
@@ -0,0 +1,6 @@
outs:
- md5: 5e95536d2c6f6ff792f05b800105bedc.dir
size: 3727395837
nfiles: 40296
hash: md5
path: patch-ruRU-H
@@ -0,0 +1,6 @@
outs:
- md5: 5db07768b0814c970d84a99b6072fd9f.dir
size: 31453961
nfiles: 186
hash: md5
path: patch-ruRU-I
@@ -0,0 +1,6 @@
outs:
- md5: e927ae98c9971f378a82feb3299fdcb8.dir
size: 69521651
nfiles: 1552
hash: md5
path: patch-ruRU-M
@@ -0,0 +1,6 @@
outs:
- md5: 449383bbe05ebebb6bad2c37da7793c4.dir
size: 142167464
nfiles: 1751
hash: md5
path: patch-ruRU-S
@@ -0,0 +1,6 @@
outs:
- md5: fc19500b5ce0fd312a415a3118afd58d.dir
size: 656858260
nfiles: 1810
hash: md5
path: patch-ruRU-T
@@ -0,0 +1,6 @@
outs:
- md5: 783909a6c2ebed5d4f43516fe1ea68bb.dir
size: 460642379
nfiles: 1837
hash: md5
path: patch-ruRU-U
@@ -0,0 +1,6 @@
outs:
- md5: e2a1448eb0e443797acd577fc4b67249.dir
size: 44597910
nfiles: 484
hash: md5
path: patch-ruRU-W
@@ -0,0 +1,6 @@
outs:
- md5: 3d322d290959d1ead612f3b76860a1ea.dir
size: 3984134962
nfiles: 29495
hash: md5
path: patch-ruRU-X
@@ -0,0 +1,6 @@
outs:
- md5: 42f3a8ac35674e33c0a4420c84cadcb8.dir
size: 11053212781
nfiles: 36191
hash: md5
path: patch-ruRU-Y
+66
View File
@@ -0,0 +1,66 @@
# Графические MPQ-патчи
В этой директории через DVC версионируется распакованное содержимое всех 15 патчей из
`graphicsPatches` в `patch-layout.json`. Git хранит небольшие файлы `*.dvc`, а сами ресурсы —
в S3 remote `customization`. Хэшированные пути в S3 являются внутренней адресацией DVC:
исходное дерево с обычными именами восстанавливается командой `dvc pull`.
## Первичная настройка
```powershell
py -m pip install -r requirements-dvc.txt
Copy-Item .env.example .env # если локального .env ещё нет
.\configure-dvc.ps1
py -m dvc pull
```
Можно скачать один патч, передав его указатель явно:
```powershell
py -m dvc pull assets/graphics/Data/ruRU/patch-ruRU-X.dvc
```
Секреты записываются только в `.dvc/config.local`; этот файл исключён из Git. Вместо ключей
из `.env` можно использовать AWS profile: `.\configure-dvc.ps1 -Profile moonwell`.
## Изменение ресурсов
DVC-кэш использует hardlink на NTFS. Перед ручным изменением дерева отвяжите его от кэша:
```powershell
py -m dvc unprotect assets/graphics/Data/ruRU/patch-ruRU-X
# изменить, добавить или удалить файлы
py -m dvc add assets/graphics/Data/ruRU/patch-ruRU-X
py -m dvc push
git add assets/graphics/Data/ruRU/patch-ruRU-X.dvc
```
Сначала выполняйте `dvc push`, затем коммитьте Git-указатель, чтобы коммит не ссылался на ещё
не загруженные объекты.
## Импорт MPQ
`mpqextract` принимает встроенный `(listfile)` и любое количество внешних listfile. Он извлекает
все известные пути, а блоки с неизвестными именами сохраняет в `__mpqmeta__/unresolved/`.
Манифест `__mpqmeta__/unresolved.json` связывает каждый blob с MPQ-хэшами и индексом блока,
поэтому содержимое архива не теряется даже при неполном listfile.
```powershell
cargo run --release --manifest-path tool/Cargo.toml --bin mpqextract -- `
'patch.mpq' 'assets/graphics/Data/ruRU/patch-ruRU-N' 'master-list.txt'
```
В `patch-ruRU-A`, `B`, `S`, `T`, `U` и `W` есть такие безымянные блоки. Для их безопасной
пересборки нужен авторитетный listfile с исходными путями; пока он не найден, исходные MPQ в
дистрибуционном S3 остаются каноническими архивами. `mpqpack` намеренно отказывается собирать
дерево с `__mpqmeta__`, поскольку подмена путей хэшами изменила бы семантику патча.
Полностью разрешённое дерево собирается так:
```powershell
cargo run --release --manifest-path tool/Cargo.toml --bin mpqpack -- `
'assets/graphics/Data/ruRU/patch-ruRU-X' 'build/graphics/patch-ruRU-X.MPQ'
```
Сводка исходных объектов, DVC-хэшей и количества разрешённых блоков находится в
`assets/graphics/sources.json`.
+157
View File
@@ -0,0 +1,157 @@
{
"sourcePrefix": "s3://warcraft-client/World of Warcraft/",
"patches": [
{
"archive": "Data/patch-X.MPQ",
"archiveBytes": 496584,
"dvcPointer": "assets/graphics/Data/patch-X.dvc",
"dvcMd5": "012ce72d8dfdf089b20a28f6e6158b58.dir",
"extractedBytes": 1195310,
"trackedFiles": 4,
"resolvedFiles": 4,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/backup-ruRU.MPQ",
"archiveBytes": 23981093,
"dvcPointer": "assets/graphics/Data/ruRU/backup-ruRU.dvc",
"dvcMd5": "b120587dfcd255f78392ca52705a00ff.dir",
"extractedBytes": 38876467,
"trackedFiles": 161,
"resolvedFiles": 161,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-6.MPQ",
"archiveBytes": 375062,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-6.dvc",
"dvcMd5": "9ca2ee746d80e8a0c951c5ef476ae654.dir",
"extractedBytes": 1064210,
"trackedFiles": 3,
"resolvedFiles": 3,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-A.mpq",
"archiveBytes": 1451338841,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-A.dvc",
"dvcMd5": "97232ae859eaa1a16b137efd1a2e9122.dir",
"extractedBytes": 2748988973,
"trackedFiles": 8817,
"resolvedFiles": 5649,
"unresolvedBlocks": 3167
},
{
"archive": "Data/ruRU/patch-ruRU-B.mpq",
"archiveBytes": 384938550,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-B.dvc",
"dvcMd5": "bb686aebf7c9cae2c02433abde9a9f19.dir",
"extractedBytes": 771051083,
"trackedFiles": 2911,
"resolvedFiles": 1679,
"unresolvedBlocks": 1231
},
{
"archive": "Data/ruRU/patch-ruRU-E.mpq",
"archiveBytes": 1862937450,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-E.dvc",
"dvcMd5": "d6b2fe0e3f23c47dc944ca4a8f82d97a.dir",
"extractedBytes": 9275755440,
"trackedFiles": 21977,
"resolvedFiles": 21977,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-H.MPQ",
"archiveBytes": 2154531136,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-H.dvc",
"dvcMd5": "5e95536d2c6f6ff792f05b800105bedc.dir",
"extractedBytes": 3727395837,
"trackedFiles": 40296,
"resolvedFiles": 40296,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-I.mpq",
"archiveBytes": 14891516,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-I.dvc",
"dvcMd5": "5db07768b0814c970d84a99b6072fd9f.dir",
"extractedBytes": 31453961,
"trackedFiles": 186,
"resolvedFiles": 186,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-M.MPQ",
"archiveBytes": 48772900,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-M.dvc",
"dvcMd5": "e927ae98c9971f378a82feb3299fdcb8.dir",
"extractedBytes": 69521651,
"trackedFiles": 1552,
"resolvedFiles": 1552,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-S.mpq",
"archiveBytes": 80492633,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-S.dvc",
"dvcMd5": "449383bbe05ebebb6bad2c37da7793c4.dir",
"extractedBytes": 142167464,
"trackedFiles": 1751,
"resolvedFiles": 1432,
"unresolvedBlocks": 318
},
{
"archive": "Data/ruRU/patch-ruRU-T.mpq",
"archiveBytes": 418595462,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-T.dvc",
"dvcMd5": "fc19500b5ce0fd312a415a3118afd58d.dir",
"extractedBytes": 656858260,
"trackedFiles": 1810,
"resolvedFiles": 1731,
"unresolvedBlocks": 78
},
{
"archive": "Data/ruRU/patch-ruRU-U.mpq",
"archiveBytes": 233678341,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-U.dvc",
"dvcMd5": "783909a6c2ebed5d4f43516fe1ea68bb.dir",
"extractedBytes": 460642379,
"trackedFiles": 1837,
"resolvedFiles": 1372,
"unresolvedBlocks": 464
},
{
"archive": "Data/ruRU/patch-ruRU-W.mpq",
"archiveBytes": 29433979,
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-W.dvc",
"dvcMd5": "e2a1448eb0e443797acd577fc4b67249.dir",
"extractedBytes": 44597910,
"trackedFiles": 484,
"resolvedFiles": 381,
"unresolvedBlocks": 102
},
{
"archive": "Data/ruRU/patch-ruRU-X.MPQ",
"archiveBytes": 2673854694,
"archiveSha256": "CD8CED6FA6C863D509DAE458E6DD44C3986B8B57731B6DC6192FE94125F8C1FD",
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-X.dvc",
"dvcMd5": "3d322d290959d1ead612f3b76860a1ea.dir",
"extractedBytes": 3984134962,
"trackedFiles": 29495,
"resolvedFiles": 29495,
"unresolvedBlocks": 0
},
{
"archive": "Data/ruRU/patch-ruRU-Y.MPQ",
"archiveBytes": 3001665276,
"archiveSha256": "534D496C1A1EE023B910ABC80B727BA88BB09CD2F5767100FC18EE41F4306B90",
"dvcPointer": "assets/graphics/Data/ruRU/patch-ruRU-Y.dvc",
"dvcMd5": "42f3a8ac35674e33c0a4420c84cadcb8.dir",
"extractedBytes": 11053212781,
"trackedFiles": 36191,
"resolvedFiles": 36191,
"unresolvedBlocks": 0
}
]
}
+170 -28
View File
@@ -8,8 +8,120 @@ param(
[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
@@ -17,11 +129,9 @@ if (-not (Test-Path -LiteralPath $cmake)) {
$cmake = $cmakeCommand.Source
}
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
Initialize-MissingSubmodules -RepositoryPath $repoRoot
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
@@ -37,37 +147,66 @@ if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader
& $cmake --build $win32BuildDir --config $Configuration --parallel 4
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
$selectedProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$recoveryProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$extensionNames = Get-MoonWellExtensionNames
if ($NativeRenderer) {
Write-Warning '-NativeRenderer is no longer needed: WarcraftXL 1.1 uses native D3D9 by default.'
}
function Install-WarcraftXLArtifacts {
param([Parameter(Mandatory)][string]$Destination)
param(
[Parameter(Mandatory)]
[string]$Destination
)
Remove-MoonWellRuntimeGarbage -Destination $Destination
$utils = Join-Path $Destination 'Utils'
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
New-Item -ItemType Directory -Path $Destination, $utils, $loosePatch -Force | Out-Null
New-Item `
-ItemType Directory `
-Path $Destination, $utils `
-Force |
Out-Null
Copy-Item -LiteralPath $stockExe -Destination (Join-Path $Destination 'Wow.exe') -Force
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 $nativeProxy -Destination (Join-Path $utils 'd3d9-native.dll') -Force
Copy-Item -LiteralPath $hostExe -Destination (Join-Path $utils 'WarcraftXLHost.exe') -Force
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
Copy-Item -LiteralPath $_.FullName -Destination $loosePatch -Recurse -Force
$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
}
}
@@ -93,14 +232,17 @@ if ($Deploy) {
}
Install-WarcraftXLArtifacts -Destination $ClientPath
Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath"
Write-Host "Installed WarcraftXL 1.1 runtime and extensions in $ClientPath"
}
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
Install-WarcraftXLArtifacts -Destination $PackagePath
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
}
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
Write-Host "WarcraftXL 1.1 build complete (native D3D9 proxy): $win32ArtifactDir"
+5
View File
@@ -9,6 +9,8 @@ import sys
from pathlib import Path
from typing import Optional
from patch_layout import PATCH_LAYOUT
IGNORED_TOP_LEVEL_DIRS = {
"Cache",
@@ -20,6 +22,7 @@ IGNORED_TOP_LEVEL_DIRS = {
IGNORED_DIRS_ANYWHERE = {
".git",
".moonwell_launcher",
".vscode"
}
IGNORED_TOP_LEVEL_FILES = {
"Wow.moonwell-patched.backup.exe",
@@ -48,6 +51,8 @@ def is_ignored(rel_path: Path) -> bool:
return True
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
return True
if PATCH_LAYOUT.is_external_graphics_patch(rel_path.as_posix()):
return True
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
+56
View File
@@ -0,0 +1,56 @@
param(
[string]$EnvFile = (Join-Path $PSScriptRoot '.env'),
[string]$Profile
)
$ErrorActionPreference = 'Stop'
function Get-EnvValue([string]$Path, [string]$Name) {
if (-not (Test-Path -LiteralPath $Path)) {
return $null
}
$line = Get-Content -LiteralPath $Path |
Where-Object { $_ -match "^$([regex]::Escape($Name))=" } |
Select-Object -First 1
if ($line) {
return $line.Substring($Name.Length + 1).Trim()
}
return $null
}
function Invoke-Dvc([string[]]$Arguments) {
& py -m dvc @Arguments
if ($LASTEXITCODE -ne 0) {
throw "DVC command failed: dvc $($Arguments -join ' ')"
}
}
if (-not (Test-Path -LiteralPath (Join-Path $PSScriptRoot '.dvc\config'))) {
throw 'DVC is not initialized in this repository.'
}
& py -c 'import dvc, dvc_s3' *> $null
if ($LASTEXITCODE -ne 0) {
throw 'DVC with S3 support is not installed. Run: py -m pip install -r requirements-dvc.txt'
}
if ($Profile) {
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'profile', $Profile)
} else {
$accessKey = Get-EnvValue $EnvFile 'AWS_ACCESS_KEY_ID'
$secretKey = Get-EnvValue $EnvFile 'AWS_SECRET_ACCESS_KEY'
$sessionToken = Get-EnvValue $EnvFile 'AWS_SESSION_TOKEN'
if (-not $accessKey -or -not $secretKey) {
throw "AWS credentials are missing in $EnvFile. Pass -Profile or fill AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY."
}
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'access_key_id', $accessKey)
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'secret_access_key', $secretKey)
if ($sessionToken) {
Invoke-Dvc @('remote', 'modify', '--local', 'customization', 'session_token', $sessionToken)
}
}
Write-Host 'Local DVC credentials configured. They are stored in ignored .dvc/config.local.'
+14
View File
@@ -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": []
}
+11
View File
@@ -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
}
}
}
+37 -14
View File
@@ -13,6 +13,7 @@ param(
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
. (Join-Path $repoRoot 'pipeline-layout.ps1')
function Import-ProjectEnvironment {
$envFile = Join-Path $repoRoot '.env'
@@ -46,6 +47,10 @@ $PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath"
}
Assert-SafePackagePath `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
@@ -58,29 +63,37 @@ if ($runningClients.Count) {
Write-Host 'Stopping the running WoW client...'
$runningClients | Stop-Process -Force
Start-Sleep -Seconds 2
Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
} | Stop-Process -Force
}
$env:WOW_HOME = $ClientPath
Write-Host "MoonWell client: $ClientPath"
Write-Host "Package staging: $PackagePath"
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
Write-Host '[1/5] Preserving initialized WarcraftXL worktrees...'
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
$previousPackageFiles = @(
Get-RelativeFilePaths -Root $PackagePath
)
if (-not $SkipDataBuild) {
Write-Host 'Resetting package staging to prevent stale build artifacts...'
Reset-MoonWellPackageStaging `
-PackagePath $PackagePath `
-RepositoryPath $repoRoot `
-ClientPath $ClientPath
} else {
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
Remove-MoonWellRuntimeGarbage -Destination $PackagePath
}
if (-not $SkipDataBuild) {
Write-Host '[2/5] Building MPQ patches...'
$tool = Join-Path $repoRoot 'tool\target\release\tool.exe'
if (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
if (-not $cargo) { throw 'Cargo was not found and the MPQ builder is not compiled.' }
$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
@@ -101,6 +114,18 @@ if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
Write-Host '[4/5] Synchronizing package files...'
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) { throw 'Python was not found; package validation cannot run.' }
& $python.Source (Join-Path $repoRoot 'validate_package.py') --dir $PackagePath
if ($LASTEXITCODE -ne 0) { throw 'Package validation failed.' }
$currentPackageFiles = @(Get-RelativeFilePaths -Root $PackagePath)
Remove-StalePackageFilesFromClient `
-ClientPath $ClientPath `
-PreviousFiles $previousPackageFiles `
-CurrentFiles $currentPackageFiles
Remove-MoonWellRuntimeGarbage -Destination $ClientPath
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
$robocopyExitCode = $LASTEXITCODE
if ($robocopyExitCode -ge 8) {
@@ -109,8 +134,6 @@ if ($robocopyExitCode -ge 8) {
if (-not $SkipManifest) {
Write-Host '[5/5] Building launcher manifest...'
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) { throw 'Python was not found; launcher manifest was not generated.' }
& $python.Source (Join-Path $repoRoot 'build_manifest.py') `
--dir $PackagePath --output (Join-Path $repoRoot 'manifest.json')
if ($LASTEXITCODE -ne 0) { throw 'Launcher manifest generation failed.' }
@@ -119,12 +142,12 @@ if (-not $SkipManifest) {
}
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' }
$renderer = 'WarcraftXL 1.1 native D3D9 proxy'
Write-Host ''
Write-Host 'Deployment complete.'
Write-Host " Renderer: $renderer"
Write-Host " Wow.exe SHA-256: $wowHash"
Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')"
Write-Host " Extensions: $(Join-Path $ClientPath 'Extensions')"
if ($Launch) {
Write-Host 'Launching Wow.exe...'
+2 -3
View File
@@ -50,9 +50,8 @@ extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** outp
return E_NOINTERFACE;
}
// Compatibility exports referenced by the current WarcraftXL core. MoonWell
// does not enable its D3D9On12 backend, so these intentionally report no D3D12
// device and a neutral supersampling factor.
// Legacy compatibility exports retained for the recovery proxy. WarcraftXL 1.1
// uses native D3D9 and ignores the former D3D9On12 bridge.
extern "C" void* WxlD3D12Device() { return nullptr; }
extern "C" void* WxlD3D12Queue() { return nullptr; }
extern "C" void WxlD3D12DrainDebug() {}
+161
View File
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// FileDataID overlay for selected retail assets shipped in MoonWell patches.
#include "game/Io.hpp"
#include "wxl/FdidApi.h"
#include "wxl/PluginApi.h"
#include <windows.h>
#include <charconv>
#include <cstdint>
#include <mutex>
#include <string>
#include <string_view>
#include <system_error>
#include <unordered_map>
#include <vector>
namespace moonwell::fdid
{
namespace
{
constexpr std::string_view kFileDataMap = "WXLFileData.csv";
std::once_flag g_loadOnce;
std::unordered_map<uint32_t, std::string> g_paths;
const WXL_Api* g_api = nullptr;
const char*(__cdecl* g_resolveTexture)(uint32_t) = nullptr;
const char*(__cdecl* g_resolveModel)(uint32_t) = nullptr;
bool ReadAll(const char* path, std::vector<uint8_t>& out)
{
void* handle = nullptr;
if (!wxl::game::io::FileOpen(path, wxl::game::io::kOpenWholeFile, &handle) || !handle)
return false;
uint32_t high = 0;
const uint32_t size = wxl::game::io::FileSize(handle, &high);
if (high != 0)
{
wxl::game::io::FileClose(handle);
return false;
}
out.resize(size);
uint32_t read = 0;
const bool ok = (size == 0 || wxl::game::io::FileRead(handle, out.data(), size, &read))
&& read == size;
wxl::game::io::FileClose(handle);
if (!ok) out.clear();
return ok;
}
std::string_view Trim(std::string_view value)
{
while (!value.empty() && (value.front() == ' ' || value.front() == '\t' ||
value.front() == '\r' || value.front() == '\n'))
value.remove_prefix(1);
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' ||
value.back() == '\r' || value.back() == '\n'))
value.remove_suffix(1);
return value;
}
void Load()
{
std::vector<uint8_t> bytes;
if (!ReadAll(kFileDataMap.data(), bytes))
{
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_WARN, "MoonWellFdid", "FileDataID map '%s' was not found",
kFileDataMap.data());
return;
}
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
size_t lineStart = 0;
while (lineStart < text.size())
{
size_t lineEnd = text.find('\n', lineStart);
if (lineEnd == std::string_view::npos) lineEnd = text.size();
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
lineStart = lineEnd + 1;
if (line.empty() || line.front() == '#') continue;
const size_t comma = line.find(',');
if (comma == std::string_view::npos) continue;
const std::string_view idText = Trim(line.substr(0, comma));
std::string_view pathText = Trim(line.substr(comma + 1));
uint32_t fileDataId = 0;
const auto parsed = std::from_chars(idText.data(), idText.data() + idText.size(), fileDataId);
if (parsed.ec != std::errc{} || parsed.ptr != idText.data() + idText.size() ||
fileDataId == 0 || pathText.empty())
continue;
std::string path(pathText);
for (char& c : path) if (c == '/') c = '\\';
g_paths[fileDataId] = std::move(path);
}
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_INFO, "MoonWellFdid", "loaded %zu custom FileDataID path(s)",
g_paths.size());
}
const char* ResolveCustom(uint32_t fileDataId)
{
std::call_once(g_loadOnce, &Load);
const auto found = g_paths.find(fileDataId);
return found == g_paths.end() ? nullptr : found->second.c_str();
}
const char* __cdecl ResolveTexture(uint32_t fileDataId)
{
if (const char* custom = ResolveCustom(fileDataId)) return custom;
return g_resolveTexture ? g_resolveTexture(fileDataId) : nullptr;
}
const char* __cdecl ResolveModel(uint32_t fileDataId)
{
if (const char* custom = ResolveCustom(fileDataId)) return custom;
return g_resolveModel ? g_resolveModel(fileDataId) : nullptr;
}
bool PatchResolver(WXL_FdidApi* fdid)
{
if (!fdid || fdid->apiVersion != WXL_FDID_API_VERSION) return false;
g_resolveTexture = fdid->ResolveTexture;
g_resolveModel = fdid->ResolveModel;
DWORD oldProtection = 0;
if (!VirtualProtect(fdid, sizeof(*fdid), PAGE_READWRITE, &oldProtection)) return false;
fdid->ResolveTexture = &ResolveTexture;
fdid->ResolveModel = &ResolveModel;
DWORD ignored = 0;
VirtualProtect(fdid, sizeof(*fdid), oldProtection, &ignored);
return true;
}
}
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWellFdid", 1, WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
moonwell::fdid::g_api = api;
auto* fdid = static_cast<WXL_FdidApi*>(api->GetInterface("wxl.fdid", WXL_FDID_API_VERSION));
if (!moonwell::fdid::PatchResolver(fdid))
{
api->Log(WXL_LOG_ERROR, "MoonWellFdid", "wxl.fdid v1 is unavailable");
return 0;
}
api->Log(WXL_LOG_INFO, "MoonWellFdid", "custom FileDataID overlay installed");
return 1;
}
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// MoonWell's specific-archive fallback, kept outside the WarcraftXL core.
#include "wxl/PluginApi.h"
#include <atomic>
#include <cstdint>
namespace
{
const WXL_Api* g_api = nullptr;
using FileOpenFn = int(__stdcall*)(void* archive, const char* name, uint32_t flags, void** out);
FileOpenFn g_nextOpen = nullptr;
std::atomic<uint32_t> g_logCount{0};
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
{
if (!g_nextOpen) return 0;
const int nativeResult = g_nextOpen(archive, name, flags, out);
if (nativeResult || archive == nullptr || !name || !*name) return nativeResult;
// A dependency requested through one concrete MPQ may live in a higher-priority loose
// Patch-*.MPQ directory. Retry only after that exact archive misses, using the client's
// normal global search path. This is the MoonWell-only behavior that used to patch core.
if (out) *out = nullptr;
const int fallbackResult = g_nextOpen(nullptr, name, flags, out);
if (fallbackResult && g_api && g_api->Log)
{
const uint32_t index = g_logCount.fetch_add(1, std::memory_order_relaxed);
if (index < 32)
g_api->Log(WXL_LOG_INFO, "MoonWellStorageFallback",
"specific archive miss resolved globally: '%s'", name);
}
return fallbackResult;
}
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWellStorageFallback", 1,
WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION || !api->HookAttachByName) return 0;
g_api = api;
const int installed = api->HookAttachByName(
"Io.FileOpen", reinterpret_cast<void*>(&FileOpenHook),
reinterpret_cast<void**>(&g_nextOpen), WXL_HOOK_DEFAULT_PRIORITY);
api->Log(installed ? WXL_LOG_INFO : WXL_LOG_ERROR, "MoonWellStorageFallback", "%s",
installed ? "specific-archive fallback installed" : "Io.FileOpen hook failed");
return installed;
}
+221 -49
View File
@@ -5,29 +5,155 @@
// changes are verified and applied to the process image during WarcraftXL's
// boot phase, before GlueXML is loaded.
#include "core/Logger.hpp"
#include "core/Mem.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"
#include "offsets/engine/Gx.hpp"
#include "game/Script.hpp"
#include "wxl/PluginApi.h"
#include "SpellOverrides.hpp"
#include <windows.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <cstdarg>
namespace moonwell
{
namespace
{
const WXL_Api* g_api = nullptr;
void Log(int level, const char* format, ...)
{
if (!g_api || !g_api->Log) return;
char message[1024]{};
va_list args;
va_start(args, format);
vsnprintf_s(message, sizeof(message), _TRUNCATE, format, args);
va_end(args);
g_api->Log(level, "MoonWell", "%s", message);
}
#define WLOG_INFO(...) Log(WXL_LOG_INFO, __VA_ARGS__)
#define WLOG_ERROR(...) Log(WXL_LOG_ERROR, __VA_ARGS__)
bool PatchMemory(void* destination, const void* source, size_t size)
{
DWORD oldProtection = 0;
if (!VirtualProtect(destination, size, PAGE_EXECUTE_READWRITE, &oldProtection))
return false;
std::memcpy(destination, source, size);
FlushInstructionCache(GetCurrentProcess(), destination, size);
DWORD ignored = 0;
VirtualProtect(destination, size, oldProtection, &ignored);
return true;
}
template <class Fn>
bool Hook(const char* name, uintptr_t address, Fn* detour, Fn** original)
{
return g_api && g_api->HookAttach && g_api->HookAttach(
name, address, reinterpret_cast<void*>(detour),
reinterpret_cast<void**>(original), WXL_HOOK_DEFAULT_PRIORITY) != 0;
}
template <class Fn>
bool HookByName(const char* point, Fn* detour, Fn** original)
{
return g_api && g_api->HookAttachByName && g_api->HookAttachByName(
point, reinterpret_cast<void*>(detour), reinterpret_cast<void**>(original),
WXL_HOOK_DEFAULT_PRIORITY) != 0;
}
constexpr uint32_t kTraitorFlag = 0x40000000u;
bool g_loginCharacterIsTraitor = false;
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
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;
@@ -60,7 +186,6 @@ namespace moonwell
return;
}
// Smoothstep avoids a visible jerk at both ends of the move.
t = t * t * (3.0f - 2.0f * t);
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
camera.verticalOffset = camera.startVerticalOffset
@@ -69,14 +194,14 @@ namespace moonwell
int __cdecl SetCharacterCreateCamera(void* state)
{
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
const bool enabled = state && wxl::game::script::IsNumber(state, 1)
&& wxl::game::script::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::game::script::IsNumber(state, 2)
? static_cast<float>(wxl::game::script::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3)
? static_cast<float>(wxl::game::script::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::game::script::IsNumber(state, 4)
? wxl::game::script::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
@@ -120,15 +245,16 @@ namespace moonwell
void InstallCharacterCreateCamera()
{
namespace gx = wxl::offsets::engine::gx;
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
void** slot = &vtable[gx::kGxSetProjectionSlot];
constexpr uintptr_t kGxDeviceVTable = 0x00A2E718;
constexpr unsigned kGxSetProjectionSlot = 0xA0 / 4;
void** vtable = reinterpret_cast<void**>(kGxDeviceVTable);
void** slot = &vtable[kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
if (!g_nextSetProjection || !PatchMemory(slot, &replacement, sizeof(replacement)))
{
g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed");
@@ -140,17 +266,17 @@ namespace moonwell
int __cdecl SetLoginCharacterFlags(void* state)
{
uint32_t flags = 0;
if (state && wxl::runtime::lua::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));
if (state && wxl::game::script::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::game::script::ToNumber(state, 1));
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1;
}
int __cdecl IsTraitor(void* state)
{
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1;
}
@@ -177,7 +303,7 @@ namespace moonwell
name, reinterpret_cast<void*>(address));
return false;
}
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(address), replacement.data(), N))
if (!PatchMemory(reinterpret_cast<void*>(address), replacement.data(), N))
{
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address));
@@ -289,7 +415,7 @@ namespace moonwell
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
if (!PatchMemory(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
{
VirtualFree(cave, 0, MEM_RELEASE);
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
@@ -308,38 +434,84 @@ namespace moonwell
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
else
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
wxl::core::log::Flush();
}
DWORD WINAPI FlushRuntimeLog(LPVOID)
using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function);
using ValidateCallbackFn = void(__cdecl*)(uintptr_t);
using GetContextFn = void*(__cdecl*)();
RegisterFunctionFn g_nextRegisterFunction = nullptr;
ValidateCallbackFn g_nextValidateCallback = nullptr;
void* g_registeredState = nullptr;
bool g_registeringMoonWell = false;
bool IsMoonWellCallback(uintptr_t callback)
{
// Let the remainder of RunAll() and the core-ready line reach the
// shared buffered logger, then make startup diagnostics durable.
Sleep(1000);
wxl::core::log::Flush();
return 0;
return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags)
|| callback == reinterpret_cast<uintptr_t>(&IsTraitor)
|| callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera);
}
void InstallRuntimeLogFlush()
void __cdecl ValidateCallbackHook(uintptr_t callback)
{
HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
if (thread) CloseHandle(thread);
if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
g_nextValidateCallback(callback);
}
struct Registration
void RegisterLuaFunctionsForCurrentState()
{
Registration()
{
wxl::runtime::lua::RegisterFunction(
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
wxl::runtime::lua::RegisterFunction(
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
wxl::runtime::modules::Register(
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
}
} g_registration;
if (!g_nextRegisterFunction || g_registeringMoonWell) return;
constexpr uintptr_t kGetContext = 0x00817DB0;
void* state = reinterpret_cast<GetContextFn>(kGetContext)();
if (!state || state == g_registeredState) return;
g_registeringMoonWell = true;
g_nextRegisterFunction("MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
g_nextRegisterFunction("MoonWellIsTraitor", &IsTraitor);
g_nextRegisterFunction("MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
g_registeringMoonWell = false;
g_registeredState = state;
WLOG_INFO("moonwell: Lua functions registered for state %p", state);
}
void __cdecl RegisterFunctionHook(const char* name, wxl::game::script::Function function)
{
if (g_nextRegisterFunction) g_nextRegisterFunction(name, function);
RegisterLuaFunctionsForCurrentState();
}
bool InstallLuaBridge()
{
const bool validator = HookByName("Lua.ValidateFunctionPointer", &ValidateCallbackHook,
&g_nextValidateCallback);
const bool registrar = HookByName("Lua.RegisterFunction", &RegisterFunctionHook,
&g_nextRegisterFunction);
if (!validator || !registrar)
WLOG_ERROR("moonwell: Lua bridge hook installation failed");
return validator && registrar;
}
}
}
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::InstallBoot();
const bool lua = moonwell::InstallLuaBridge();
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;
}
+310
View File
@@ -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;
}
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
struct WXL_Api;
namespace moonwell::spells
{
bool Install(const WXL_Api* api);
}
+2
View File
@@ -0,0 +1,2 @@
file(GLOB_RECURSE WXL_EXT_SHARED_SRC CONFIGURE_DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
+77
View File
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// WarcraftXL 1.1 BLP transform extension.
#include "engine/assets/shared/textures/blp/BlpTranscode.hpp"
#include "wxl/ModernBlpApi.h"
#include "wxl/PluginApi.h"
#include "wxl/StorageApi.h"
#include <cstdint>
#include <span>
#include <utility>
#include <vector>
namespace
{
const WXL_Api* g_api = nullptr;
constexpr uint32_t kMaxTextureEdge = 1024;
int __cdecl Transcode(const char* name, const uint8_t* raw, uint32_t rawLen,
const WXL_ByteSink* sink)
{
if (!raw || !sink || !sink->Write) return 0;
namespace blp = wxl::modern::assets::textures::blp;
const std::span<const uint8_t> input(raw, rawLen);
std::vector<uint8_t> capped;
const bool didCap = blp::CapBlpMips(input, capped, kMaxTextureEdge);
const std::span<const uint8_t> source = didCap
? std::span<const uint8_t>(capped.data(), capped.size()) : input;
std::vector<uint8_t> transcoded;
if (blp::TranscodeBlp(source, transcoded))
{
sink->Write(sink->ctx, transcoded.data(), static_cast<uint32_t>(transcoded.size()));
if (g_api && g_api->Log)
g_api->Log(WXL_LOG_DEBUG, "wxl-modern-blp", "%s%s BGRA->DXT5",
name ? name : "?", didCap ? " capped" : "");
return 1;
}
if (!didCap) return 0;
sink->Write(sink->ctx, capped.data(), static_cast<uint32_t>(capped.size()));
return 1;
}
const WXL_ModernBlpApi g_blpApi = {
sizeof(WXL_ModernBlpApi), WXL_MODERN_BLP_API_VERSION, &Transcode,
};
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "wxl-modern-blp", 1, WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
g_api = api;
auto* storage = static_cast<WXL_StorageApi*>(
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
!storage->RegisterClientTransform)
{
api->Log(WXL_LOG_ERROR, "wxl-modern-blp", "wxl.storage v1 is unavailable");
return 0;
}
storage->RegisterClientTransform(".blp", &Transcode);
api->PublishInterface("wxl.modern-blp", WXL_MODERN_BLP_API_VERSION,
const_cast<WXL_ModernBlpApi*>(&g_blpApi));
api->Log(WXL_LOG_INFO, "wxl-modern-blp", "BLP transcode and mip cap active");
return 1;
}
+26
View File
@@ -0,0 +1,26 @@
{
"repositoryPatches": [
"Data/patch-4.MPQ",
"Data/patch-Z.MPQ",
"Data/ruRU/patch-ruRU-4.MPQ",
"Data/ruRU/patch-ruRU-5.MPQ",
"Data/ruRU/patch-ruRU-Z.MPQ"
],
"graphicsPatches": [
"Data/patch-X.MPQ",
"Data/ruRU/backup-ruRU.MPQ",
"Data/ruRU/patch-ruRU-6.MPQ",
"Data/ruRU/patch-ruRU-A.mpq",
"Data/ruRU/patch-ruRU-B.mpq",
"Data/ruRU/patch-ruRU-E.mpq",
"Data/ruRU/patch-ruRU-H.MPQ",
"Data/ruRU/patch-ruRU-I.mpq",
"Data/ruRU/patch-ruRU-M.MPQ",
"Data/ruRU/patch-ruRU-S.mpq",
"Data/ruRU/patch-ruRU-T.mpq",
"Data/ruRU/patch-ruRU-U.mpq",
"Data/ruRU/patch-ruRU-W.mpq",
"Data/ruRU/patch-ruRU-X.MPQ",
"Data/ruRU/patch-ruRU-Y.MPQ"
]
}
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
import json
from dataclasses import dataclass
from pathlib import Path
def normalized_path(path: str) -> str:
return path.replace("\\", "/").casefold()
@dataclass(frozen=True)
class PatchLayout:
repository_patches: frozenset[str]
graphics_patches: frozenset[str]
def is_repository_patch(self, path: str) -> bool:
return normalized_path(path) in self.repository_patches
def is_graphics_patch(self, path: str) -> bool:
return normalized_path(path) in self.graphics_patches
def is_external_graphics_patch(self, path: str) -> bool:
key = normalized_path(path)
return key in self.graphics_patches and key not in self.repository_patches
def load_patch_layout(path: Path | None = None) -> PatchLayout:
config_path = path or Path(__file__).resolve().with_name("patch-layout.json")
data = json.loads(config_path.read_text(encoding="utf-8"))
repository = frozenset(normalized_path(item) for item in data["repositoryPatches"])
graphics = frozenset(normalized_path(item) for item in data["graphicsPatches"])
return PatchLayout(repository_patches=repository, graphics_patches=graphics)
PATCH_LAYOUT = load_patch_layout()
+241
View File
@@ -0,0 +1,241 @@
$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)"
}
}
Remove-EmptyMoonWellDirectories -Destination $Destination
}
function Remove-StalePackageFilesFromClient {
param(
[Parameter(Mandatory)][string]$ClientPath,
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$PreviousFiles,
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$CurrentFiles
)
$current = @{}
foreach ($relativePath in $CurrentFiles) {
$current[$relativePath.ToLowerInvariant()] = $true
}
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
foreach ($relativePath in $PreviousFiles) {
if ($current.ContainsKey($relativePath.ToLowerInvariant()) -or
(Test-MoonWellPreservedDataPatch -RelativePath $relativePath)) {
continue
}
$target = [System.IO.Path]::GetFullPath((Join-Path $clientRoot $relativePath))
if (-not $target.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Package inventory contains a path outside the client: $relativePath"
}
if (Test-Path -LiteralPath $target -PathType Leaf) {
Remove-Item -LiteralPath $target -Force
Write-Host "Removed stale packaged file from client: $target"
}
}
Remove-EmptyMoonWellDirectories -Destination $ClientPath
}
+1
View File
@@ -0,0 +1 @@
dvc[s3]==3.67.1
+118 -1
View File
@@ -1,8 +1,15 @@
param(
[Parameter(Mandatory=$false)]
[ValidateSet("production", "ptr", "local")]
[string]$Env = "local"
)
# 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,6 +38,42 @@ 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..."
$elevatedProcess = Start-Process -FilePath "powershell.exe" -Verb RunAs -PassThru -Wait `
-WorkingDirectory $ROOT `
-ArgumentList "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "`"$($MyInvocation.MyCommand.Path)`"", "-Env", $Env
exit $elevatedProcess.ExitCode
}
# --- Paths
$SRC_DIR = Join-Path $ROOT "src"
$DIST_DIR = Join-Path $ROOT "dist"
@@ -38,6 +81,41 @@ $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)) {
Write-Host "Building Rust tool..."
@@ -46,7 +124,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) {
@@ -62,6 +147,7 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
# --- 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..."
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
-PackagePath $DIST_DIR -Deploy
@@ -72,6 +158,22 @@ if ($LASTEXITCODE -ne 0) {
# --- Sync dist/ -> WOW_HOME
Write-Host "Syncing dist/ -> WOW_HOME..."
$python = Get-Command python -ErrorAction SilentlyContinue
if (-not $python) {
Write-Error "Python is required for package validation."
exit 1
}
& $python.Source (Join-Path $ROOT 'validate_package.py') --dir $DIST_DIR
if ($LASTEXITCODE -ne 0) {
Write-Error "Package validation failed!"
exit 1
}
$currentPackageFiles = @(Get-RelativeFilePaths -Root $DIST_DIR)
Remove-StalePackageFilesFromClient `
-ClientPath $WOW_HOME `
-PreviousFiles $previousPackageFiles `
-CurrentFiles $currentPackageFiles
Remove-MoonWellRuntimeGarbage -Destination $WOW_HOME
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
# robocopy exit codes 0-7 are success/warnings; 8+ are errors
if ($LASTEXITCODE -ge 8) {
@@ -81,6 +183,21 @@ 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
Binary file not shown.
@@ -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)
@@ -714,6 +714,44 @@ local function CharacterCreate_UpdateCamera(immediate)
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 = "";
@@ -818,7 +856,7 @@ function CharacterCreate_OnLoad(self)
self:SetSequence(0)
self:SetCamera(0)
SetCharCustomizeFrame("CharacterCreate")
CharacterCreate.numRaces = 0
CharacterCreate.selectedRace = 0
CharacterCreate.numClasses = 0
@@ -860,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)
@@ -869,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)
@@ -890,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)
@@ -899,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)
@@ -913,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
@@ -1002,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();
@@ -1026,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
@@ -1113,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
@@ -1155,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;
@@ -1180,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)
@@ -1246,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);
@@ -1261,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, ...);
@@ -1311,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)
@@ -1356,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");
@@ -1382,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");
@@ -1480,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];
@@ -1493,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
@@ -1529,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();
@@ -1537,7 +1575,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
maleButton.checkedTexture:Hide();
end
end
if femaleButton and femaleButton.checkedTexture then
if femaleButton:GetChecked() then
femaleButton.checkedTexture:Show();
@@ -1641,7 +1679,7 @@ function CharacterRace_OnClick(self, id)
SetCharacterClass(classIndex);
CharacterCreate_UpdateHairCustomization();
CharacterChangeFixup();
CharacterCreate_UpdateButtonCheckedStates();
@@ -1666,7 +1704,7 @@ function SetCharacterGender(sex)
CharacterCreateEnumerateRaces(GetAvailableRaces());
CharacterCreateEnumerateClasses(GetAvailableClasses());
SetCharacterRace(GetSelectedRace());
local _,_,classIndex = GetSelectedClass();
if ( PAID_SERVICE_TYPE ) then
classIndex = PaidChange_GetCurrentClassIndex();
@@ -1680,7 +1718,7 @@ 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();
@@ -1707,7 +1745,7 @@ function CharacterCreate_ResetState(immediate)
CharCreatePersonalizeButton:Show();
CharCreateOkayButton:Hide();
CharacterCreate_UpdateGameModeVisibility();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
end
@@ -1753,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)
@@ -1775,7 +1813,7 @@ function SetButtonDesaturated(button, desaturated, r, g, b)
g = 0.5;
b = 0.5;
end
icon:SetVertexColor(r, g, b);
end
@@ -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>
@@ -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
@@ -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>
@@ -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>
@@ -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
@@ -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>
@@ -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)
@@ -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>
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,178 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local assert, pairs, type = assert, pairs, type
-- WoW APIs
local CreateFrame = CreateFrame
--[[
Selection Group controls all have an interface to select a group for thier contents
None of them will auto size to thier contents, and should usually be used with a scrollframe
unless you know that the controls will fit inside
]]
--------------------------
-- Dropdown Group --
--------------------------
--[[
Events :
OnGroupSelected
]]
do
local Type = "DropdownGroup"
local Version = 13
local function OnAcquire(self)
self.dropdown:SetText("")
self:SetDropdownWidth(200)
self:SetTitle("")
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self.dropdown.list = nil
self.status = nil
for k in pairs(self.localstatus) do
self.localstatus[k] = nil
end
end
local PaneBackdrop = {
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 3, right = 3, top = 5, bottom = 3 }
}
local function SetTitle(self,title)
self.titletext:SetText(title)
self.dropdown.frame:ClearAllPoints()
if title and title ~= "" then
self.dropdown.frame:SetPoint("TOPRIGHT", self.frame, "TOPRIGHT", -2, 0)
else
self.dropdown.frame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", -1, 0)
end
end
local function SelectedGroup(self,event,value)
local group = self.parentgroup
local status = group.status or group.localstatus
status.selected = value
self.parentgroup:Fire("OnGroupSelected", value)
end
local function SetGroupList(self,list)
self.dropdown:SetList(list)
end
-- called to set an external table to store status in
local function SetStatusTable(self, status)
assert(type(status) == "table")
self.status = status
end
local function SetGroup(self,group)
self.dropdown:SetValue(group)
local status = self.status or self.localstatus
status.selected = group
self:Fire("OnGroupSelected", group)
end
local function OnWidthSet(self, width)
local content = self.content
local contentwidth = width - 26
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 - 63
if contentheight < 0 then
contentheight = 0
end
content:SetHeight(contentheight)
content.height = contentheight
end
local function LayoutFinished(self, width, height)
self:SetHeight((height or 0) + 63)
end
local function SetDropdownWidth(self, width)
self.dropdown:SetWidth(width)
end
local function Constructor()
local frame = CreateFrame("Frame")
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetTitle = SetTitle
self.SetGroupList = SetGroupList
self.SetGroup = SetGroup
self.SetStatusTable = SetStatusTable
self.SetDropdownWidth = SetDropdownWidth
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
self.LayoutFinished = LayoutFinished
self.localstatus = {}
self.frame = frame
frame.obj = self
frame:SetHeight(100)
frame:SetWidth(100)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
local titletext = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
titletext:SetPoint("TOPLEFT", frame, "TOPLEFT", 4, -5)
titletext:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -5)
titletext:SetJustifyH("LEFT")
titletext:SetHeight(18)
self.titletext = titletext
local dropdown = AceGUI:Create("Dropdown")
self.dropdown = dropdown
dropdown.frame:SetParent(frame)
dropdown.frame:SetFrameLevel(dropdown.frame:GetFrameLevel() + 2)
dropdown.parentgroup = self
dropdown:SetCallback("OnValueChanged",SelectedGroup)
dropdown.frame:SetPoint("TOPLEFT",frame,"TOPLEFT", -1, 0)
dropdown.frame:Show()
dropdown:SetLabel("")
local border = CreateFrame("Frame",nil,frame)
self.border = border
border:SetPoint("TOPLEFT",frame,"TOPLEFT",0,-26)
border:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,3)
border:SetBackdrop(PaneBackdrop)
border:SetBackdropColor(0.1,0.1,0.1,0.5)
border:SetBackdropBorderColor(0.4,0.4,0.4)
--Container Support
local content = CreateFrame("Frame",nil,border)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",border,"TOPLEFT",10,-10)
content:SetPoint("BOTTOMRIGHT",border,"BOTTOMRIGHT",-10,10)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,235 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local tostring = tostring
-- WoW APIs
local PlaySound = PlaySound
local GetCursorInfo, ClearCursor, GetSpellName = GetCursorInfo, ClearCursor, GetSpellName
local CreateFrame, UIParent = CreateFrame, UIParent
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: AceGUIEditBoxInsertLink, ChatFontNormal, OKAY
local Type = "EditBox"
local Version = 14
if not AceGUIEditBoxInsertLink then
-- upgradeable hook
hooksecurefunc("ChatEdit_InsertLink", function(...) return _G.AceGUIEditBoxInsertLink(...) end)
end
function _G.AceGUIEditBoxInsertLink(text)
for i = 1, AceGUI:GetNextWidgetNum(Type)-1 do
local editbox = _G["AceGUI-3.0EditBox"..i]
if editbox and editbox:IsVisible() and editbox:HasFocus() then
editbox:Insert(text)
return true
end
end
end
--------------------------
-- Edit box --
--------------------------
--[[
Events :
OnTextChanged
OnEnterPressed
]]
do
local function OnAcquire(self)
self:SetHeight(26)
self:SetWidth(200)
self:SetDisabled(false)
self:SetLabel()
self.showbutton = true
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self:SetDisabled(false)
self:SetText()
end
local function Control_OnEnter(this)
this.obj:Fire("OnEnter")
end
local function Control_OnLeave(this)
this.obj:Fire("OnLeave")
end
local function EditBox_OnEscapePressed(this)
this:ClearFocus()
end
local function ShowButton(self)
if self.showbutton then
self.button:Show()
self.editbox:SetTextInsets(0,20,3,3)
end
end
local function HideButton(self)
self.button:Hide()
self.editbox:SetTextInsets(0,0,3,3)
end
local function EditBox_OnEnterPressed(this)
local self = this.obj
local value = this:GetText()
local cancel = self:Fire("OnEnterPressed",value)
if not cancel then
PlaySound("igMainMenuOptionCheckBoxOn")
HideButton(self)
end
end
local function Button_OnClick(this)
local editbox = this.obj.editbox
editbox:ClearFocus()
EditBox_OnEnterPressed(editbox)
end
local function EditBox_OnReceiveDrag(this)
local self = this.obj
local type, id, info = GetCursorInfo()
if type == "item" then
self:SetText(info)
self:Fire("OnEnterPressed",info)
ClearCursor()
elseif type == "spell" then
local name, rank = GetSpellName(id, info)
if rank and rank:match("%d") then
name = name.."("..rank..")"
end
self:SetText(name)
self:Fire("OnEnterPressed",name)
ClearCursor()
end
HideButton(self)
AceGUI:ClearFocus()
end
local function EditBox_OnTextChanged(this)
local self = this.obj
local value = this:GetText()
if tostring(value) ~= tostring(self.lasttext) then
self:Fire("OnTextChanged",value)
self.lasttext = value
ShowButton(self)
end
end
local function SetDisabled(self, disabled)
self.disabled = disabled
if disabled then
self.editbox:EnableMouse(false)
self.editbox:ClearFocus()
self.editbox:SetTextColor(0.5,0.5,0.5)
self.label:SetTextColor(0.5,0.5,0.5)
else
self.editbox:EnableMouse(true)
self.editbox:SetTextColor(1,1,1)
self.label:SetTextColor(1,.82,0)
end
end
local function SetText(self, text)
self.lasttext = text or ""
self.editbox:SetText(text or "")
self.editbox:SetCursorPosition(0)
HideButton(self)
end
local function SetLabel(self, text)
if text and text ~= "" then
self.label:SetText(text)
self.label:Show()
self.editbox:SetPoint("TOPLEFT",self.frame,"TOPLEFT",7,-18)
self:SetHeight(44)
self.alignoffset = 30
else
self.label:SetText("")
self.label:Hide()
self.editbox:SetPoint("TOPLEFT",self.frame,"TOPLEFT",7,0)
self:SetHeight(26)
self.alignoffset = 12
end
end
local function Constructor()
local num = AceGUI:GetNextWidgetNum(Type)
local frame = CreateFrame("Frame",nil,UIParent)
local editbox = CreateFrame("EditBox","AceGUI-3.0EditBox"..num,frame,"InputBoxTemplate")
local self = {}
self.type = Type
self.num = num
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetDisabled = SetDisabled
self.SetText = SetText
self.SetLabel = SetLabel
self.frame = frame
frame.obj = self
self.editbox = editbox
editbox.obj = self
self.alignoffset = 30
frame:SetHeight(44)
frame:SetWidth(200)
editbox:SetScript("OnEnter",Control_OnEnter)
editbox:SetScript("OnLeave",Control_OnLeave)
editbox:SetAutoFocus(false)
editbox:SetFontObject(ChatFontNormal)
editbox:SetScript("OnEscapePressed",EditBox_OnEscapePressed)
editbox:SetScript("OnEnterPressed",EditBox_OnEnterPressed)
editbox:SetScript("OnTextChanged",EditBox_OnTextChanged)
editbox:SetScript("OnReceiveDrag", EditBox_OnReceiveDrag)
editbox:SetScript("OnMouseDown", EditBox_OnReceiveDrag)
editbox:SetTextInsets(0,0,3,3)
editbox:SetMaxLetters(256)
editbox:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",6,0)
editbox:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
editbox:SetHeight(19)
local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,-2)
label:SetPoint("TOPRIGHT",frame,"TOPRIGHT",0,-2)
label:SetJustifyH("LEFT")
label:SetHeight(18)
self.label = label
local button = CreateFrame("Button",nil,editbox,"UIPanelButtonTemplate")
button:SetWidth(40)
button:SetHeight(20)
button:SetPoint("RIGHT",editbox,"RIGHT",-2,0)
button:SetText(OKAY)
button:SetScript("OnClick", Button_OnClick)
button:Hide()
self.button = button
button.obj = self
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,308 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local pairs, assert, type = pairs, assert, type
-- 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: CLOSE
----------------
-- Main Frame --
----------------
--[[
Events :
OnClose
]]
do
local Type = "Frame"
local Version = 11
local FrameBackdrop = {
bgFile="Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile="Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 8, right = 8, top = 8, bottom = 8 }
}
local PaneBackdrop = {
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 3, right = 3, top = 5, bottom = 3 }
}
local function frameOnClose(this)
this.obj:Fire("OnClose")
end
local function closeOnClick(this)
PlaySound("gsTitleOptionExit")
this.obj:Hide()
end
local function frameOnMouseDown(this)
AceGUI:ClearFocus()
end
local function titleOnMouseDown(this)
this:GetParent():StartMoving()
AceGUI:ClearFocus()
end
local function frameOnMouseUp(this)
local frame = this:GetParent()
frame:StopMovingOrSizing()
local self = frame.obj
local status = self.status or self.localstatus
status.width = frame:GetWidth()
status.height = frame:GetHeight()
status.top = frame:GetTop()
status.left = frame:GetLeft()
end
local function sizerseOnMouseDown(this)
this:GetParent():StartSizing("BOTTOMRIGHT")
AceGUI:ClearFocus()
end
local function sizersOnMouseDown(this)
this:GetParent():StartSizing("BOTTOM")
AceGUI:ClearFocus()
end
local function sizereOnMouseDown(this)
this:GetParent():StartSizing("RIGHT")
AceGUI:ClearFocus()
end
local function SetTitle(self,title)
self.titletext:SetText(title)
end
local function SetStatusText(self,text)
self.statustext:SetText(text)
end
local function Hide(self)
self.frame:Hide()
end
local function Show(self)
self.frame:Show()
end
local function OnAcquire(self)
self.frame:SetParent(UIParent)
self.frame:SetFrameStrata("FULLSCREEN_DIALOG")
self:ApplyStatus()
self:Show()
end
local function OnRelease(self)
self.status = nil
for k in pairs(self.localstatus) do
self.localstatus[k] = nil
end
end
-- called to set an external table to store status in
local function SetStatusTable(self, status)
assert(type(status) == "table")
self.status = status
self:ApplyStatus()
end
local function ApplyStatus(self)
local status = self.status or self.localstatus
local frame = self.frame
self:SetWidth(status.width or 700)
self:SetHeight(status.height or 500)
if status.top and status.left then
frame:SetPoint("TOP",UIParent,"BOTTOM",0,status.top)
frame:SetPoint("LEFT",UIParent,"LEFT",status.left,0)
else
frame:SetPoint("CENTER",UIParent,"CENTER")
end
end
local function OnWidthSet(self, width)
local content = self.content
local contentwidth = width - 34
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 - 57
if contentheight < 0 then
contentheight = 0
end
content:SetHeight(contentheight)
content.height = contentheight
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = "Frame"
self.Hide = Hide
self.Show = Show
self.SetTitle = SetTitle
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetStatusText = SetStatusText
self.SetStatusTable = SetStatusTable
self.ApplyStatus = ApplyStatus
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
self.localstatus = {}
self.frame = frame
frame.obj = self
frame:SetWidth(700)
frame:SetHeight(500)
frame:SetPoint("CENTER",UIParent,"CENTER",0,0)
frame:EnableMouse()
frame:SetMovable(true)
frame:SetResizable(true)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
frame:SetScript("OnMouseDown", frameOnMouseDown)
frame:SetBackdrop(FrameBackdrop)
frame:SetBackdropColor(0,0,0,1)
frame:SetScript("OnHide",frameOnClose)
frame:SetMinResize(400,200)
frame:SetToplevel(true)
local closebutton = CreateFrame("Button",nil,frame,"UIPanelButtonTemplate")
closebutton:SetScript("OnClick", closeOnClick)
closebutton:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-27,17)
closebutton:SetHeight(20)
closebutton:SetWidth(100)
closebutton:SetText(CLOSE)
self.closebutton = closebutton
closebutton.obj = self
local statusbg = CreateFrame("Frame",nil,frame)
statusbg:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",15,15)
statusbg:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-132,15)
statusbg:SetHeight(24)
statusbg:SetBackdrop(PaneBackdrop)
statusbg:SetBackdropColor(0.1,0.1,0.1)
statusbg:SetBackdropBorderColor(0.4,0.4,0.4)
self.statusbg = statusbg
local statustext = statusbg:CreateFontString(nil,"OVERLAY","GameFontNormal")
self.statustext = statustext
statustext:SetPoint("TOPLEFT",statusbg,"TOPLEFT",7,-2)
statustext:SetPoint("BOTTOMRIGHT",statusbg,"BOTTOMRIGHT",-7,2)
statustext:SetHeight(20)
statustext:SetJustifyH("LEFT")
statustext:SetText("")
local title = CreateFrame("Frame",nil,frame)
self.title = title
title:EnableMouse()
title:SetScript("OnMouseDown",titleOnMouseDown)
title:SetScript("OnMouseUp", frameOnMouseUp)
local titlebg = frame:CreateTexture(nil,"OVERLAY")
titlebg:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
titlebg:SetTexCoord(0.31,0.67,0,0.63)
titlebg:SetPoint("TOP",frame,"TOP",0,12)
titlebg:SetWidth(100)
titlebg:SetHeight(40)
local titlebg_l = frame:CreateTexture(nil,"OVERLAY")
titlebg_l:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
titlebg_l:SetTexCoord(0.21,0.31,0,0.63)
titlebg_l:SetPoint("RIGHT",titlebg,"LEFT",0,0)
titlebg_l:SetWidth(30)
titlebg_l:SetHeight(40)
local titlebg_right = frame:CreateTexture(nil,"OVERLAY")
titlebg_right:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
titlebg_right:SetTexCoord(0.67,0.77,0,0.63)
titlebg_right:SetPoint("LEFT",titlebg,"RIGHT",0,0)
titlebg_right:SetWidth(30)
titlebg_right:SetHeight(40)
title:SetAllPoints(titlebg)
local titletext = title:CreateFontString(nil,"OVERLAY","GameFontNormal")
titletext:SetPoint("TOP",titlebg,"TOP",0,-14)
self.titletext = titletext
local sizer_se = CreateFrame("Frame",nil,frame)
sizer_se:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
sizer_se:SetWidth(25)
sizer_se:SetHeight(25)
sizer_se:EnableMouse()
sizer_se:SetScript("OnMouseDown",sizerseOnMouseDown)
sizer_se:SetScript("OnMouseUp", frameOnMouseUp)
self.sizer_se = sizer_se
local line1 = sizer_se:CreateTexture(nil, "BACKGROUND")
self.line1 = line1
line1:SetWidth(14)
line1:SetHeight(14)
line1:SetPoint("BOTTOMRIGHT", -8, 8)
line1:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
local x = 0.1 * 14/17
line1:SetTexCoord(0.05 - x, 0.5, 0.05, 0.5 + x, 0.05, 0.5 - x, 0.5 + x, 0.5)
local line2 = sizer_se:CreateTexture(nil, "BACKGROUND")
self.line2 = line2
line2:SetWidth(8)
line2:SetHeight(8)
line2:SetPoint("BOTTOMRIGHT", -8, 8)
line2:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
local x = 0.1 * 8/17
line2:SetTexCoord(0.05 - x, 0.5, 0.05, 0.5 + x, 0.05, 0.5 - x, 0.5 + x, 0.5)
local sizer_s = CreateFrame("Frame",nil,frame)
sizer_s:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-25,0)
sizer_s:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",0,0)
sizer_s:SetHeight(25)
sizer_s:EnableMouse()
sizer_s:SetScript("OnMouseDown",sizersOnMouseDown)
sizer_s:SetScript("OnMouseUp", frameOnMouseUp)
self.sizer_s = sizer_s
local sizer_e = CreateFrame("Frame",nil,frame)
sizer_e:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,25)
sizer_e:SetPoint("TOPRIGHT",frame,"TOPRIGHT",0,0)
sizer_e:SetWidth(25)
sizer_e:EnableMouse()
sizer_e:SetScript("OnMouseDown",sizereOnMouseDown)
sizer_e:SetScript("OnMouseUp", frameOnMouseUp)
self.sizer_e = sizer_e
--Container Support
local content = CreateFrame("Frame",nil,frame)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",frame,"TOPLEFT",17,-27)
content:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-17,40)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,76 @@
local AceGUI = LibStub("AceGUI-3.0")
-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent
--------------------------
-- Heading --
--------------------------
do
local Type = "Heading"
local Version = 5
local function OnAcquire(self)
self:SetText("")
self:SetFullWidth()
self:SetHeight(18)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
end
local function SetText(self, text)
self.label:SetText(text or "")
if (text or "") == "" then
self.left:SetPoint("RIGHT",self.frame,"RIGHT",-3,0)
self.right:Hide()
else
self.left:SetPoint("RIGHT",self.label,"LEFT",-5,0)
self.right:Show()
end
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetText = SetText
self.frame = frame
frame.obj = self
frame:SetHeight(18)
local label = frame:CreateFontString(nil,"BACKGROUND","GameFontNormal")
label:SetPoint("TOP",frame,"TOP",0,0)
label:SetPoint("BOTTOM",frame,"BOTTOM",0,0)
label:SetJustifyH("CENTER")
label:SetHeight(18)
self.label = label
local left = frame:CreateTexture(nil, "BACKGROUND")
self.left = left
left:SetHeight(8)
left:SetPoint("LEFT",frame,"LEFT",3,0)
left:SetPoint("RIGHT",label,"LEFT",-5,0)
left:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
left:SetTexCoord(0.81, 0.94, 0.5, 1)
local right = frame:CreateTexture(nil, "BACKGROUND")
self.right = right
right:SetHeight(8)
right:SetPoint("RIGHT",frame,"RIGHT",-3,0)
right:SetPoint("LEFT",label,"RIGHT",5,0)
right:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
right:SetTexCoord(0.81, 0.94, 0.5, 1)
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,149 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local select = select
-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent
--------------------------
-- Label --
--------------------------
do
local Type = "Icon"
local Version = 12
local function OnAcquire(self)
self:SetHeight(110)
self:SetWidth(110)
self:SetLabel("")
self:SetImage(nil)
self:SetImageSize(64, 64)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self:SetDisabled(false)
end
local function SetLabel(self, text)
if text and text ~= "" then
self.label:Show()
self.label:SetText(text)
self:SetHeight(self.image:GetHeight() + 25)
else
self.label:Hide()
self:SetHeight(self.image:GetHeight() + 10)
end
end
local function SetImage(self, path, ...)
local image = self.image
image:SetTexture(path)
if image:GetTexture() then
self.imageshown = true
local n = select('#', ...)
if n == 4 or n == 8 then
image:SetTexCoord(...)
else
image:SetTexCoord(0, 1, 0, 1)
end
else
self.imageshown = nil
end
end
local function SetImageSize(self, width, height)
self.image:SetWidth(width)
self.image:SetHeight(height)
--self.frame:SetWidth(width + 30)
if self.label:IsShown() then
self:SetHeight(height + 25)
else
self:SetHeight(height + 10)
end
end
local function SetDisabled(self, disabled)
self.disabled = disabled
if disabled then
self.frame:Disable()
self.label:SetTextColor(0.5,0.5,0.5)
self.image:SetVertexColor(0.5, 0.5, 0.5, 0.5)
else
self.frame:Enable()
self.label:SetTextColor(1,1,1)
self.image:SetVertexColor(1, 1, 1)
end
end
local function OnClick(this, button)
this.obj:Fire("OnClick", button)
AceGUI:ClearFocus()
end
local function OnEnter(this)
this.obj.highlight:Show()
this.obj:Fire("OnEnter")
end
local function OnLeave(this)
this.obj.highlight:Hide()
this.obj:Fire("OnLeave")
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.frame = frame
self.SetImage = SetImage
self.SetImageSize = SetImageSize
-- SetText should be deprecated along the way
self.SetText = SetLabel
self.SetDisabled = SetDisabled
frame.obj = self
frame:SetHeight(110)
frame:SetWidth(110)
frame:EnableMouse(true)
frame:SetScript("OnClick", OnClick)
frame:SetScript("OnLeave", OnLeave)
frame:SetScript("OnEnter", OnEnter)
local label = frame:CreateFontString(nil,"BACKGROUND","GameFontHighlight")
label:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",0,0)
label:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
label:SetJustifyH("CENTER")
label:SetJustifyV("TOP")
label:SetHeight(18)
self.label = label
local image = frame:CreateTexture(nil,"BACKGROUND")
self.image = image
image:SetWidth(64)
image:SetHeight(64)
image:SetPoint("TOP",frame,"TOP",0,-5)
local highlight = frame:CreateTexture(nil,"OVERLAY")
self.highlight = highlight
highlight:SetAllPoints(image)
highlight:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-Tab-Highlight")
highlight:SetTexCoord(0,1,0.23,0.77)
highlight:SetBlendMode("ADD")
highlight:Hide()
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,138 @@
local AceGUI = LibStub("AceGUI-3.0")
-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent
-------------
-- 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
]]
--------------------------
-- Inline Group --
--------------------------
--[[
This is a simple grouping container, no selection
It will resize automatically to the height of the controls added to it
]]
do
local Type = "InlineGroup"
local Version = 6
local function OnAcquire(self)
self:SetWidth(300)
self:SetHeight(100)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
end
local PaneBackdrop = {
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 3, right = 3, top = 5, bottom = 3 }
}
local function SetTitle(self,title)
self.titletext:SetText(title)
end
local function LayoutFinished(self, width, height)
if self.noAutoHeight then return end
self:SetHeight((height or 0) + 40)
end
local function OnWidthSet(self, width)
local content = self.content
local contentwidth = width - 20
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 - 20
if contentheight < 0 then
contentheight = 0
end
content:SetHeight(contentheight)
content.height = contentheight
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetTitle = SetTitle
self.frame = frame
self.LayoutFinished = LayoutFinished
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
frame.obj = self
frame:SetHeight(100)
frame:SetWidth(100)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
local titletext = frame:CreateFontString(nil,"OVERLAY","GameFontNormal")
titletext:SetPoint("TOPLEFT",frame,"TOPLEFT",14,0)
titletext:SetPoint("TOPRIGHT",frame,"TOPRIGHT",-14,0)
titletext:SetJustifyH("LEFT")
titletext:SetHeight(18)
self.titletext = titletext
local border = CreateFrame("Frame",nil,frame)
self.border = border
border:SetPoint("TOPLEFT",frame,"TOPLEFT",0,-17)
border:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-1,3)
border:SetBackdrop(PaneBackdrop)
border:SetBackdropColor(0.1,0.1,0.1,0.5)
border:SetBackdropBorderColor(0.4,0.4,0.4)
--Container Support
local content = CreateFrame("Frame",nil,border)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",border,"TOPLEFT",10,-10)
content:SetPoint("BOTTOMRIGHT",border,"BOTTOMRIGHT",-10,10)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,219 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local select, max = select, math.max
-- 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: GameFontHighlightSmall
--------------------------
-- Label --
--------------------------
do
local Type = "InteractiveLabel"
local Version = 6
local function OnAcquire(self)
self:SetHeight(18)
self:SetWidth(200)
self:SetText("")
self:SetImage(nil)
self:SetColor()
self:SetFontObject()
self:SetHighlight()
self:SetHighlightTexCoord()
end
local function OnRelease(self)
self:SetDisabled(false)
self.frame:ClearAllPoints()
self.frame:Hide()
end
local function UpdateImageAnchor(self)
local width = self.frame.width or self.frame:GetWidth() or 0
local image = self.image
local label = self.label
local frame = self.frame
local height
label:ClearAllPoints()
image:ClearAllPoints()
if self.imageshown then
local imagewidth = image:GetWidth()
if (width - imagewidth) < 200 or (label:GetText() or "") == "" then
--image goes on top centered when less than 200 width for the text, or if there is no text
image:SetPoint("TOP",frame,"TOP",0,0)
label:SetPoint("TOP",image,"BOTTOM",0,0)
label:SetPoint("LEFT",frame,"LEFT",0,0)
label:SetWidth(width)
height = image:GetHeight() + label:GetHeight()
else
--image on the left
local imageheight = image:GetHeight()
local labelheight = label:GetHeight()
--center image with label
if imageheight > labelheight then
image:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
label:SetPoint("LEFT",image,"RIGHT",0,0)
else
label:SetPoint("TOPLEFT",frame,"TOPLEFT",imagewidth,0)
image:SetPoint("RIGHT",label,"LEFT",0,0)
end
label:SetWidth(width - imagewidth)
height = max(imageheight, labelheight)
end
else
--no image shown
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
label:SetWidth(width)
height = self.label:GetHeight()
end
self.resizing = true
self.frame:SetHeight(height)
self.frame.height = height
self.resizing = nil
end
local function SetText(self, text)
self.label:SetText(text or "")
UpdateImageAnchor(self)
end
local function SetColor(self, r, g, b)
if not (r and g and b) then
r, g, b = 1, 1, 1
end
self.label:SetVertexColor(r, g, b)
end
local function OnWidthSet(self, width)
if self.resizing then return end
UpdateImageAnchor(self)
end
local function SetImage(self, path, ...)
local image = self.image
image:SetTexture(path)
if image:GetTexture() then
self.imageshown = true
local n = select('#', ...)
if n == 4 or n == 8 then
image:SetTexCoord(...)
end
else
self.imageshown = nil
end
UpdateImageAnchor(self)
end
local function SetFont(self, font, height, flags)
self.label:SetFont(font, height, flags)
end
local function SetFontObject(self, font)
self.label:SetFontObject(font or GameFontHighlightSmall)
end
local function SetImageSize(self, width, height)
self.image:SetWidth(width)
self.image:SetHeight(height)
UpdateImageAnchor(self)
end
local function SetHighlight(self, ...)
self.highlight:SetTexture(...)
end
local function SetHighlightTexCoord(self, ...)
if select('#', ...) >= 1 then
self.highlight:SetTexCoord(...)
else
self.highlight:SetTexCoord(0, 1, 0, 1)
end
end
local function SetDisabled(self,disabled)
self.disabled = disabled
if disabled then
self.frame:EnableMouse(false)
self.label:SetTextColor(0.5, 0.5, 0.5)
else
self.frame:EnableMouse(true)
self.label:SetTextColor(1, 1, 1)
end
end
local function OnEnter(this)
this.obj.highlight:Show()
this.obj:Fire("OnEnter")
end
local function OnLeave(this)
this.obj.highlight:Hide()
this.obj:Fire("OnLeave")
end
local function OnClick(this, ...)
this.obj:Fire("OnClick", ...)
AceGUI:ClearFocus()
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
frame:EnableMouse(true)
frame:SetScript("OnEnter", OnEnter)
frame:SetScript("OnLeave", OnLeave)
frame:SetScript("OnMouseDown", OnClick)
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetText = SetText
self.SetColor = SetColor
self.frame = frame
self.OnWidthSet = OnWidthSet
self.SetImage = SetImage
self.SetImageSize = SetImageSize
self.SetFont = SetFont
self.SetFontObject = SetFontObject
self.SetHighlight = SetHighlight
self.SetHighlightTexCoord = SetHighlightTexCoord
self.SetDisabled = SetDisabled
frame.obj = self
frame:SetHeight(18)
frame:SetWidth(200)
local label = frame:CreateFontString(nil,"BACKGROUND","GameFontHighlightSmall")
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
label:SetWidth(200)
label:SetJustifyH("LEFT")
label:SetJustifyV("TOP")
self.label = label
local highlight = frame:CreateTexture(nil, "OVERLAY")
highlight:SetTexture(nil)
highlight:SetAllPoints()
highlight:SetBlendMode("ADD")
highlight:Hide()
self.highlight = highlight
local image = frame:CreateTexture(nil,"BACKGROUND")
self.image = image
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,230 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
-- WoW APIs
local IsShiftKeyDown, IsControlKeyDown, IsAltKeyDown = IsShiftKeyDown, IsControlKeyDown, IsAltKeyDown
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: NOT_BOUND
--------------------------
-- Keybinding --
--------------------------
do
local Type = "Keybinding"
local Version = 13
local ControlBackdrop = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
}
local function Control_OnEnter(this)
this.obj:Fire("OnEnter")
end
local function Control_OnLeave(this)
this.obj:Fire("OnLeave")
end
local function keybindingMsgFixWidth(this)
this:SetWidth(this.msg:GetWidth()+10)
this:SetScript("OnUpdate",nil)
end
local function Keybinding_OnClick(this, button)
if button == "LeftButton" or button == "RightButton" then
local self = this.obj
if self.waitingForKey then
this:EnableKeyboard(false)
self.msgframe:Hide()
this:UnlockHighlight()
self.waitingForKey = nil
else
this:EnableKeyboard(true)
self.msgframe:Show()
this:LockHighlight()
self.waitingForKey = true
end
end
AceGUI:ClearFocus()
end
local ignoreKeys = nil
local function Keybinding_OnKeyDown(this, key)
local self = this.obj
if self.waitingForKey then
local keyPressed = key
if keyPressed == "ESCAPE" then
keyPressed = ""
else
if not ignoreKeys then
ignoreKeys = {
["BUTTON1"] = true, ["BUTTON2"] = true,
["UNKNOWN"] = true,
["LSHIFT"] = true, ["LCTRL"] = true, ["LALT"] = true,
["RSHIFT"] = true, ["RCTRL"] = true, ["RALT"] = true,
}
end
if ignoreKeys[keyPressed] then return end
if IsShiftKeyDown() then
keyPressed = "SHIFT-"..keyPressed
end
if IsControlKeyDown() then
keyPressed = "CTRL-"..keyPressed
end
if IsAltKeyDown() then
keyPressed = "ALT-"..keyPressed
end
end
this:EnableKeyboard(false)
self.msgframe:Hide()
this:UnlockHighlight()
self.waitingForKey = nil
if not self.disabled then
self:SetKey(keyPressed)
self:Fire("OnKeyChanged",keyPressed)
end
end
end
local function Keybinding_OnMouseDown(this, button)
if button == "LeftButton" or button == "RightButton" then
return
elseif button == "MiddleButton" then
button = "BUTTON3"
elseif button == "Button4" then
button = "BUTTON4"
elseif button == "Button5" then
button = "BUTTON5"
end
Keybinding_OnKeyDown(this, button)
end
local function OnAcquire(self)
self:SetWidth(200)
self:SetHeight(44)
self:SetLabel("")
self:SetKey("")
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self.waitingForKey = nil
self.msgframe:Hide()
self:SetDisabled(false)
end
local function SetDisabled(self, disabled)
self.disabled = disabled
if disabled then
self.button:Disable()
self.label:SetTextColor(0.5,0.5,0.5)
else
self.button:Enable()
self.label:SetTextColor(1,1,1)
end
end
local function SetKey(self, key)
if (key or "") == "" then
self.button:SetText(NOT_BOUND)
self.button:SetNormalFontObject("GameFontNormal")
else
self.button:SetText(key)
self.button:SetNormalFontObject("GameFontHighlight")
end
end
local function SetLabel(self, label)
self.label:SetText(label or "")
if (label or "") == "" then
self.alignoffset = nil
self:SetHeight(24)
else
self.alignoffset = 30
self:SetHeight(44)
end
end
local function Constructor()
local num = AceGUI:GetNextWidgetNum(Type)
local frame = CreateFrame("Frame",nil,UIParent)
local button = CreateFrame("Button","AceGUI-3.0 KeybindingButton"..num,frame,"UIPanelButtonTemplate2")
local self = {}
self.type = Type
self.num = num
local text = button:GetFontString()
text:SetPoint("LEFT",button,"LEFT",7,0)
text:SetPoint("RIGHT",button,"RIGHT",-7,0)
button:SetScript("OnClick",Keybinding_OnClick)
button:SetScript("OnKeyDown",Keybinding_OnKeyDown)
button:SetScript("OnEnter",Control_OnEnter)
button:SetScript("OnLeave",Control_OnLeave)
button:SetScript("OnMouseDown",Keybinding_OnMouseDown)
button:RegisterForClicks("AnyDown")
button:EnableMouse()
button:SetHeight(24)
button:SetWidth(200)
button:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT",0,0)
button:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
frame:SetWidth(200)
frame:SetHeight(44)
self.alignoffset = 30
self.button = button
local label = frame:CreateFontString(nil,"OVERLAY","GameFontHighlight")
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
label:SetPoint("TOPRIGHT",frame,"TOPRIGHT",0,0)
label:SetJustifyH("CENTER")
label:SetHeight(18)
self.label = label
local msgframe = CreateFrame("Frame",nil,UIParent)
msgframe:SetHeight(30)
msgframe:SetBackdrop(ControlBackdrop)
msgframe:SetBackdropColor(0,0,0)
msgframe:SetFrameStrata("FULLSCREEN_DIALOG")
msgframe:SetFrameLevel(1000)
self.msgframe = msgframe
local msg = msgframe:CreateFontString(nil,"OVERLAY","GameFontNormal")
msg:SetText("Press a key to bind, ESC to clear the binding or click the button again to cancel")
msgframe.msg = msg
msg:SetPoint("TOPLEFT",msgframe,"TOPLEFT",5,-5)
msgframe:SetScript("OnUpdate", keybindingMsgFixWidth)
msgframe:SetPoint("BOTTOM",button,"TOP",0,0)
msgframe:Hide()
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetLabel = SetLabel
self.SetDisabled = SetDisabled
self.SetKey = SetKey
self.frame = frame
frame.obj = self
button.obj = self
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,158 @@
local Type, Version = "Label", 20
local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
-- Lua APIs
local max, select = math.max, select
-- 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: GameFontHighlightSmall
--[[-----------------------------------------------------------------------------
Scripts
-------------------------------------------------------------------------------]]
local function UpdateImageAnchor(self)
local frame = self.frame
local width = frame.width or frame:GetWidth() or 0
local image = self.image
local label = self.label
local height
label:ClearAllPoints()
image:ClearAllPoints()
if self.imageshown then
local imagewidth = image:GetWidth()
if (width - imagewidth) < 200 or (label:GetText() or "") == "" then
-- image goes on top centered when less than 200 width for the text, or if there is no text
image:SetPoint("TOP", frame, "TOP", 0, 0)
label:SetPoint("TOP", image, "BOTTOM", 0, 0)
label:SetPoint("LEFT", frame, "LEFT", 0, 0)
label:SetPoint("RIGHT", frame, "RIGHT", 0, 0)
height = image:GetHeight() + label:GetHeight()
else
-- image on the left
image:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
label:SetPoint("TOPLEFT", image, "TOPRIGHT", 4, 0)
label:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
height = max(image:GetHeight(), label:GetHeight())
end
else
-- no image shown
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
label:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
height = label:GetHeight()
end
self.resizing = true
frame:SetHeight(height)
frame.height = height
self.resizing = nil
end
--[[-----------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------------]]
local methods = {
["OnAcquire"] = function(self)
self:SetHeight(18)
self:SetWidth(200)
self:SetText("")
self:SetImage(nil)
self:SetImageSize(16, 16)
self:SetColor()
self.label:SetFontObject(nil)
self.label:SetFont(GameFontHighlightSmall:GetFont())
end,
["OnRelease"] = function(self)
self.frame:ClearAllPoints()
self.frame:Hide()
end,
["SetText"] = function(self, text)
self.label:SetText(text or "")
UpdateImageAnchor(self)
end,
["SetColor"] = function(self, r, g, b)
if not (r and g and b) then
r, g, b = 1, 1, 1
end
self.label:SetVertexColor(r, g, b)
end,
["OnWidthSet"] = function(self, width)
if self.resizing then return end
UpdateImageAnchor(self)
end,
["SetImage"] = function(self, path, ...)
local image = self.image
image:SetTexture(path)
if image:GetTexture() then
self.imageshown = true
local n = select('#', ...)
if n == 4 or n == 8 then
image:SetTexCoord(...)
else
image:SetTexCoord(0, 1, 0, 1)
end
else
self.imageshown = nil
end
UpdateImageAnchor(self)
end,
["SetFont"] = function(self, font, height, flags)
self.label:SetFont(font, height, flags)
end,
["SetFontObject"] = function(self, font)
self.label:SetFontObject(font or GameFontHighlightSmall)
end,
["SetImageSize"] = function(self, width, height)
self.image:SetWidth(width)
self.image:SetHeight(height)
UpdateImageAnchor(self)
end,
}
--[[-----------------------------------------------------------------------------
Constructor
-------------------------------------------------------------------------------]]
local function Constructor()
local frame = CreateFrame("Frame", nil, UIParent)
frame:Hide()
local label = frame:CreateFontString(nil, "BACKGROUND", "GameFontHighlightSmall")
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
label:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, 0)
label:SetJustifyH("LEFT")
label:SetJustifyV("TOP")
local image = frame:CreateTexture(nil, "BACKGROUND")
-- create widget
local widget = {
label = label,
image = image,
frame = frame,
type = Type
}
for method, func in pairs(methods) do
widget[method] = func
end
frame.obj, label.obj, image.obj = widget, widget, widget
AceGUI:RegisterAsWidget(widget)
return widget
end
AceGUI:RegisterWidgetType(Type, Constructor, Version)
@@ -0,0 +1,266 @@
local Type, Version = "MultiLineEditBox", 20
local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
--[[-----------------------------------------------------------------------------
Scripts
-------------------------------------------------------------------------------]]
local function OnClick(self) -- Button
self = self.obj
self.editBox:ClearFocus()
if not self:Fire("OnEnterPressed", self.editBox:GetText()) then
self.button:Disable()
end
end
local function OnCursorChanged(self, _, y, _, cursorHeight) -- EditBox
self, y = self.obj.scrollFrame, -y
local offset = self:GetVerticalScroll()
if y < offset then
self:SetVerticalScroll(y)
else
y = y + cursorHeight - self:GetHeight()
if y > offset then
self:SetVerticalScroll(y)
end
end
end
local function OnEditFocusLost(self) -- EditBox
self:HighlightText(0, 0)
end
local function OnEnter(self) -- EditBox / ScrollFrame
self = self.obj
if not self.entered then
self.entered = true
self:Fire("OnEnter")
end
end
local function OnLeave(self) -- EditBox / ScrollFrame
self = self.obj
if self.entered then
self.entered = nil
self:Fire("OnLeave")
end
end
local function OnMouseUp(self) -- ScrollFrame
self = self.obj.editBox
self:SetFocus()
self:SetCursorPosition(self:GetNumLetters())
end
local function OnReceiveDrag(self) -- EditBox / ScrollFrame
local type, id, info = GetCursorInfo()
if type == "spell" then
info, id = GetSpellName(id, info)
if id and id:match("%d") then
info = info .. "(" .. id .. ")"
end
elseif type ~= "item" then
return
end
ClearCursor()
self = self.obj
local editBox = self.editBox
if not editBox:HasFocus() then
editBox:SetFocus()
editBox:SetCursorPosition(editBox:GetNumLetters())
end
editBox:Insert(info)
self.button:Enable()
end
local function OnSizeChanged(self, width, height) -- ScrollFrame
self.obj.editBox:SetWidth(width)
end
local function OnTextChanged(self, userInput) -- EditBox
if userInput then
self = self.obj
self:Fire("OnTextChanged", self.editBox:GetText())
self.button:Enable()
end
end
local function OnTextSet(self) -- EditBox
self:HighlightText(0, 0)
self:SetCursorPosition(self:GetNumLetters())
self:SetCursorPosition(0)
self.obj.button:Disable()
end
function OnVerticalScroll(self, offset) -- ScrollFrame
local editBox = self.obj.editBox
editBox:SetHitRectInsets(0, 0, offset, editBox:GetHeight() - offset - self:GetHeight())
end
--[[-----------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------------]]
local methods = {
["GetText"] = function(self)
return self.editBox:GetText()
end,
["OnAcquire"] = function(self)
self.editBox:SetText("")
self:SetDisabled(false)
self:SetWidth(200)
self:SetNumLines()
self.entered = nil
end,
["OnRelease"] = function(self)
self.frame:ClearAllPoints()
self.frame:Hide()
end,
["SetDisabled"] = function(self, disabled)
local editBox = self.editBox
if disabled then
editBox:ClearFocus()
editBox:EnableMouse(false)
editBox:SetTextColor(0.5, 0.5, 0.5)
self.label:SetTextColor(0.5, 0.5, 0.5)
self.scrollFrame:EnableMouse(false)
self.button:Disable()
else
editBox:EnableMouse(true)
editBox:SetTextColor(1, 1, 1)
self.label:SetTextColor(1, 0.82, 0)
self.scrollFrame:EnableMouse(true)
end
end,
["SetLabel"] = function(self, text)
if text and text ~= "" then
self.label:SetText(text)
if self.labelHeight ~= 10 then
self.labelHeight = 10
self.scrollBar:SetPoint("TOP", self.label, "BOTTOM", 0, -19)
self:SetHeight(self.frame.height + 10)
self.label:Show()
end
elseif self.labelHeight ~= 0 then
self.labelHeight = 0
self.label:Hide()
self.scrollBar:SetPoint("TOP", self.frame, "TOP", 0, -23)
self:SetHeight(self.frame.height - 10)
end
end,
["SetNumLines"] = function(self, value)
if not value or value < 4 then
value = 4
end
self:SetHeight(value * 14 + 41 + self.labelHeight)
end,
["SetText"] = function(self, text)
self.editBox:SetText(text)
end
}
--[[-----------------------------------------------------------------------------
Constructor
-------------------------------------------------------------------------------]]
local backdrop = {
bgFile = [[Interface\Tooltips\UI-Tooltip-Background]],
edgeFile = [[Interface\Tooltips\UI-Tooltip-Border]], edgeSize = 16,
insets = { left = 4, right = 3, top = 4, bottom = 3 }
}
local function Constructor()
local frame = CreateFrame("Frame", nil, UIParent)
frame:Hide()
local widgetNum = AceGUI:GetNextWidgetNum(Type)
local label = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -4)
label:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, -4)
label:SetJustifyH("LEFT")
label:SetText(ACCEPT)
label:SetHeight(10)
local button = CreateFrame("Button", ("%s%dButton"):format(Type, widgetNum), frame, "UIPanelButtonTemplate2")
button:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 4)
button:SetHeight(22)
button:SetWidth(label:GetStringWidth() + 24)
button:SetText(ACCEPT)
button:SetScript("OnClick", OnClick)
button:Disable()
local text = button:GetFontString()
text:ClearAllPoints()
text:SetPoint("TOPLEFT", button, "TOPLEFT", 5, -5)
text:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -5, 1)
text:SetJustifyV("MIDDLE")
local scrollBG = CreateFrame("Frame", nil, frame)
scrollBG:SetBackdrop(backdrop)
scrollBG:SetBackdropColor(0, 0, 0)
scrollBG:SetBackdropBorderColor(0.4, 0.4, 0.4)
local scrollFrame = CreateFrame("ScrollFrame", ("%s%dScrollFrame"):format(Type, widgetNum), frame, "UIPanelScrollFrameTemplate")
local scrollBar = _G[scrollFrame:GetName() .. "ScrollBar"]
scrollBar:ClearAllPoints()
scrollBar:SetPoint("TOP", label, "BOTTOM", 0, -19)
scrollBar:SetPoint("BOTTOM", button, "TOP", 0, 18)
scrollBar:SetPoint("RIGHT", frame, "RIGHT")
scrollBG:SetPoint("TOPRIGHT", scrollBar, "TOPLEFT", 0, 19)
scrollBG:SetPoint("BOTTOMLEFT", button, "TOPLEFT")
scrollFrame:SetPoint("TOPLEFT", scrollBG, "TOPLEFT", 5, -6)
scrollFrame:SetPoint("BOTTOMRIGHT", scrollBG, "BOTTOMRIGHT", -4, 4)
scrollFrame:SetScript("OnEnter", OnEnter)
scrollFrame:SetScript("OnLeave", OnLeave)
scrollFrame:SetScript("OnMouseUp", OnMouseUp)
scrollFrame:SetScript("OnReceiveDrag", OnReceiveDrag)
scrollFrame:SetScript("OnSizeChanged", OnSizeChanged)
scrollFrame:HookScript("OnVerticalScroll", OnVerticalScroll)
local editBox = CreateFrame("EditBox", nil, scrollFrame)
editBox:SetAllPoints()
editBox:SetFontObject(ChatFontNormal)
editBox:SetMultiLine(true)
editBox:EnableMouse(true)
editBox:SetAutoFocus(false)
editBox:SetCountInvisibleLetters(false)
editBox:SetScript("OnCursorChanged", OnCursorChanged)
editBox:SetScript("OnEditFocusLost", OnEditFocusLost)
editBox:SetScript("OnEnter", OnEnter)
editBox:SetScript("OnEscapePressed", editBox.ClearFocus)
editBox:SetScript("OnLeave", OnLeave)
editBox:SetScript("OnMouseDown", OnReceiveDrag)
editBox:SetScript("OnReceiveDrag", OnReceiveDrag)
editBox:SetScript("OnTextChanged", OnTextChanged)
editBox:SetScript("OnTextSet", OnTextSet)
scrollFrame:SetScrollChild(editBox)
local widget = {
button = button,
editBox = editBox,
frame = frame,
label = label,
labelHeight = 10,
scrollBar = scrollBar,
scrollFrame = scrollFrame,
type = Type
}
for method, func in pairs(methods) do
widget[method] = func
end
button.obj, editBox.obj, scrollFrame.obj = widget, widget, widget
AceGUI:RegisterAsWidget(widget)
return widget
end
AceGUI:RegisterWidgetType(Type, Constructor, Version)

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