40 Commits

Author SHA1 Message Date
sindoring 87ec8c2ce9 прототип, без успехов. может как-нибудь потом 2026-08-10 21:17:04 +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 85f80445d1 план 2026-07-18 17:05:18 +04:00
sindoring 9fdb7ed718 фикс ачивок 2026-07-18 16:54:34 +04:00
sindoring 698de84762 зум при создании персонажа 2026-07-18 16:24:02 +04:00
sindoring cddaf2c515 получение режима внтури игры 2026-07-18 16:08:08 +04:00
sindoring 2291ace9f7 Merge branch 'master' into encounter-journal 2026-07-18 15:49:11 +04:00
sindoring 26ff2b3b94 полный переход на warcraft xl 2026-07-18 15:48: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 08c1b9da3f ai ready 2026-06-13 11:51:05 +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
926 changed files with 72822 additions and 598 deletions
+5 -1
View File
@@ -5,4 +5,8 @@ AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
AWS_BUCKET=
AWS_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=
PRODUCTION_REALMLIST=
PTR_REALMLIST=
LOCAL_REALMLIST=
+7 -1
View File
@@ -3,4 +3,10 @@ __pycache__/
manifest.json
.claude
.vscode
dist
dist
build/
Wow*.exe
*.backup.exe
Logs/
local-assets/
!Wow_Original.exe
+24
View File
@@ -0,0 +1,24 @@
[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/WarcraftXL/wxl-modern-m2.git
[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
+46
View File
@@ -0,0 +1,46 @@
# MoonWell Client Sources — Agent Guide
## Проект
WoW-клиент на базе патча **3.3.5a (build 12340, WotLK)**. Репозиторий содержит UI-аддоны и
кастомные интерфейсы для приватного сервера MoonWell.
Стек: Lua 5.1, FrameXML (XML + Lua), WoW Addon API 3.3.5.
---
## Навыки (Skills)
Перед тем как работать с UI — **прочитай соответствующий файл из `ai-skills/`**.
| Задача | Файл |
|--------|------|
| Верстка фреймов, XML, Lua CreateFrame, pixel-perfect из Figma, текстуры, шрифты, якоря | [`ai-skills/ui/SKILL.md`](ai-skills/ui/SKILL.md) |
| Виджеты, типы фреймов, Button/EditBox/ScrollFrame, события, SavedVariables, Blizzard API | [`ai-skills/ui/widgets-and-api.md`](ai-skills/ui/widgets-and-api.md) |
Читай **весь файл** скилла, а не отдельные секции — там есть ограничения API, типичные ошибки
и готовые шаблоны.
---
## Ключевые ограничения (всегда держи в голове)
- `## Interface: 30300` — никаких retail-API
- Нет `SetSize()` → только `SetWidth()` / `SetHeight()`
- Нет `C_Timer`, `CreateFramePool`, `PixelUtil`
- Текстуры: только степени двойки, максимум 512×512 px, формат `.tga` / `.blp`
- XML-атрибуты: `relativeTo` (не `relativeKey`), `$parentX` (не `$parent.X`)
- Кнопки и ScrollFrame в XML должны иметь `name`
---
## Структура аддона
```
AddonName/
├── AddonName.toc ← обязательно, имя файла = имя папки
├── AddonName.xml ← верстка (загружается первой)
└── AddonName.lua ← логика
```
Порядок файлов в TOC важен: XML раньше Lua.
+86
View File
@@ -0,0 +1,86 @@
cmake_minimum_required(VERSION 3.20)
project(MoonWellWarcraftXL LANGUAGES CXX)
# WarcraftXL is an untouched upstream v1.1 submodule. Every project-specific
# feature below is built as a separately loaded Extensions/<name>/<name>.dll.
add_subdirectory(vendor/warcraftxl)
set(WXL_CORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/warcraftxl")
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
set(WXL_LOCAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/modules")
function(wxl_add_external_extension extension_name extension_dir)
if(NOT CMAKE_SIZEOF_VOID_P EQUAL 4)
return()
endif()
file(GLOB_RECURSE extension_sources CONFIGURE_DEPENDS "${extension_dir}/src/*.cpp")
if(NOT extension_sources)
message(FATAL_ERROR "WarcraftXL extension '${extension_name}' has no sources in ${extension_dir}/src")
endif()
set(extension_shared_sources "")
if(extension_name STREQUAL "wxl-modern-m2")
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
"${WXL_CORE_DIR}/src/engine/assets/shared/models/m2/*.cpp")
elseif(extension_name STREQUAL "wxl-modern-blp")
file(GLOB_RECURSE extension_shared_sources CONFIGURE_DEPENDS
"${WXL_CORE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
endif()
file(GLOB wxl_sdk_sources CONFIGURE_DEPENDS "${WXL_CORE_DIR}/src/game/*.cpp")
add_library(${extension_name} SHARED
${extension_sources}
${extension_shared_sources}
${wxl_sdk_sources})
set_target_properties(${extension_name} PROPERTIES
OUTPUT_NAME "${extension_name}"
PREFIX "")
target_include_directories(${extension_name} PRIVATE
"${extension_dir}/src"
"${WXL_CORE_DIR}/include"
"${WXL_CORE_DIR}/src")
target_compile_features(${extension_name} PRIVATE cxx_std_20)
target_compile_definitions(${extension_name} PRIVATE
WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS WXL_EXTENSION)
if(CLIENT_PATH)
add_custom_command(TARGET ${extension_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"${CLIENT_PATH}/Extensions/${extension_name}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "$<TARGET_FILE:${extension_name}>"
"${CLIENT_PATH}/Extensions/${extension_name}/${extension_name}.dll"
COMMENT "Deploy ${extension_name}.dll -> Extensions/${extension_name}")
endif()
endfunction()
# Official WarcraftXL 1.1 extensions.
wxl_add_external_extension(wxl-db2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-db2")
wxl_add_external_extension(wxl-modern-adt "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt")
wxl_add_external_extension(wxl-modern-blp "${WXL_LOCAL_MODULES_DIR}/wxl-modern-blp")
wxl_add_external_extension(wxl-modern-m2 "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-m2")
wxl_add_external_extension(wxl-modern-wmo "${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-wmo")
wxl_add_external_extension(wxl-grasswind "${WXL_EXTERNAL_MODULES_DIR}/wxl-grasswind")
wxl_add_external_extension(wxl-unit-outline "${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline")
# MoonWell-owned extensions. wxl-fdid-moonwell sorts after wxl-db2 and before
# the modern asset extensions, so it can layer custom CSV mappings over wxl.fdid.
wxl_add_external_extension(MoonWell "${WXL_LOCAL_MODULES_DIR}/moonwell")
wxl_add_external_extension(MoonwellCustomization
"${WXL_LOCAL_MODULES_DIR}/moonwell-customization")
wxl_add_external_extension(wxl-fdid-moonwell "${WXL_LOCAL_MODULES_DIR}/moonwell-fdid")
wxl_add_external_extension(wxl-moonwell-storage-fallback
"${WXL_LOCAL_MODULES_DIR}/moonwell-storage-fallback")
# Recovery loader retained for existing -NativeRenderer workflows. WarcraftXL
# 1.1 already ships a native D3D9 forwarding proxy as its standard renderer.
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
add_library(MoonWellLoader SHARED
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.def")
set_target_properties(MoonWellLoader PROPERTIES
OUTPUT_NAME "d3d9-native"
PREFIX ""
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/artifacts")
target_compile_definitions(MoonWellLoader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
endif()
+788
View File
@@ -0,0 +1,788 @@
# План расширенной кастомизации персонажей MoonWell
## 1. Цель
Создать для клиента WoW 3.3.5a (build 12340) расширенную систему внешности, визуально и по удобству близкую к retail, сохранив оригинальный `Wow.exe` и используя WarcraftXL для всех клиентских расширений.
Система должна:
- поддерживать стандартные параметры 3.3.5a и дополнительные параметры MoonWell;
- показывать одинаковую внешность при создании, выборе персонажа, входе в мир и другим игрокам;
- хранить дополнительные параметры на сервере и считать сервер источником истины;
- поддерживать текстурные слои, geoset базовой модели и отдельные прикрепляемые M2-модели;
- работать без Eluna/AIO в качестве постоянного транспорта состояния;
- позволять добавлять новые варианты преимущественно данными и ассетами;
- иметь версионирование формата и безопасный откат.
## 2. Не входит в первую версию
- полное копирование внутренних `ChrCustomization*` DB2 и протокола актуального retail-клиента;
- поддержка всех retail-рас, которых нет в MoonWell;
- динамическое изменение скелета или пропорций тела;
- автоматический импорт всех retail-ассетов без ручной проверки;
- публикация полного дампа современных файлов Blizzard;
- изменение бинарника `Wow.exe` на диске.
## 3. Базовые ограничения
### Клиент
- WoW 3.3.5a, build 12340, Lua 5.1, `## Interface: 30300`.
- `Wow.exe` должен оставаться оригинальным с контролируемым SHA-256.
- Новое нативное поведение реализуется модулем MoonWell внутри WarcraftXL.
- GlueXML и FrameXML не используют retail API (`C_Timer`, `CreateFramePool`, `PixelUtil` и т. п.).
- Для анимаций интерфейса используется `OnUpdate`.
- UI-текстуры: `.blp`/`.tga`, размеры по степеням двойки, не более 512×512 на отдельный UI-тайл.
- В Lua/XML использовать `SetWidth`/`SetHeight`, а не `SetSize`.
### Ассеты и рендер
- Исходные современные M2/MD21 обрабатываются `wxl-modern-assets`.
- Каждая модель проверяется на поддерживаемую версию формата, лимит костей, материалы, анимации и attachment points.
- Не использовать OBJ/FBX/glTF как вход для игрового клиента: нужны исходные `.m2`, `.skin`, `.skel`, `.anim` и `.blp`.
- UI-превью и модель в мире должны использовать один каталог вариантов и одинаковые идентификаторы.
### Сервер
- Сервер валидирует расу, пол, доступность и совместимость каждого выбора.
- Клиент не может назначить себе недоступный вариант внешности.
- Дополнительные параметры хранятся отдельно от стандартных байтов внешности 3.3.5a.
- Изменения схемы БД поставляются миграциями и имеют обратимый путь.
## 4. Целевая архитектура
```text
Retail CDN (выбранные FileDataID)
Asset manifest + downloader + validation
Patch*.MPQ / loose Patch-WXL assets
├── wxl-modern-assets
├── адаптированный wxl-equip-extension
└── MoonWell customization runtime
Lua API / GlueXML UI
MoonWell customization protocol
AzerothCore storage + validation + broadcast
```
### Компоненты
1. **Asset pipeline** — получает только перечисленные зависимости по `FileDataID`, сохраняет манифест и проверяет комплектность.
2. **`wxl-modern-assets`** — загружает и приводит современные M2/MD21 и текстуры к контракту клиента 3.3.5a.
3. **Адаптированный `wxl-equip-extension`** — прикрепляет M2, поддерживает collection M2, два модельных канала, attachment points и фильтрацию geoset.
4. **MoonWell customization runtime** — управляет независимыми косметическими каналами, текстурными слоями, geoset базовой модели и Lua API.
5. **GlueXML UI** — категории, список вариантов, камера, случайный выбор, сброс и предварительный просмотр.
6. **Серверный модуль** — хранение, валидация, загрузка, изменение и распространение внешности.
## 5. Типы параметров внешности
### Стандартные параметры 3.3.5a
- пол;
- цвет кожи;
- лицо;
- причёска;
- цвет волос;
- стандартная особенность лица/борода.
Они продолжают использовать штатные поля персонажа и стандартный пакет создания.
### Дополнительные текстурные параметры
- шрамы;
- татуировки тела;
- татуировки лица;
- макияж;
- рисунок/отметины;
- дополнительные оттенки кожи;
- дополнительные слои лица.
Реализация: расширение `CharSections`, каталог слоёв и модификация/перехват клиентского композитора текстуры персонажа.
### Geoset базовой модели
- дополнительные причёски;
- части бороды и усов;
- форма ушей/бровей;
- варианты клыков;
- встроенные рога;
- скрытие частей базовой модели.
Реализация: таблица выбора geoset по расе, полу, категории и варианту.
### Прикрепляемые M2-элементы
- серьги и пирсинг;
- украшения для волос;
- объёмные бороды;
- рога и дополнительные клыки;
- протезы;
- венцы, диадемы и прочие аксессуары;
- независимые объёмные элементы тела.
Реализация: косметические каналы поверх механизма `wxl-equip-extension`.
## 6. Косметические каналы
Кастомизация не должна занимать реальные слоты экипировки. Нужен отдельный реестр каналов:
```text
hair_extra
beard_extra
horns
ears
tusks
piercing_1
piercing_2
accessory_head
accessory_face
accessory_body_1
accessory_body_2
```
Для каждого канала задаются:
- стабильный числовой `option_id`;
- допустимые расы и пол;
- `display_id` или прямое описание M2;
- Model1/Model2;
- Texture1/Texture2;
- attachment point для каждого модельного канала;
- список разрешённых geoset collection M2;
- флаги суффиксов расы/пола и структуры каталогов;
- правила конфликта со шлемом и настоящей экипировкой;
- приоритет и взаимоисключающие каналы;
- значение сброса (`choice_id = 0`).
## 7. Адаптация `wxl-equip-extension`
### Обязательные работы
- закрепить точную ревизию репозитория как submodule;
- адаптировать сборочную структуру к текущему WarcraftXL MoonWell;
- проверить пересечение хуков с `wxl-modern-assets` и существующими `OnItemSlotChange`, `OnItemSlotClear`, `OnM2PerFrameUpdate`;
- вынести разбор Model1/Model2, `Icon2`, attachment points и collection geoset в переиспользуемый слой;
- добавить API независимых косметических каналов, не привязанных к надетому предмету;
- устранить утечку/остаток collection M2 после снятия или смены варианта;
- обеспечить корректное уничтожение дочерних моделей при смене персонажа, карты, формы и пола;
- исключить двойное прикрепление модели при повторной синхронизации;
- очистить мусорные Model1/Model2 в старых строках `ItemDisplayInfo.dbc`;
- добавить диагностический лог разрешения пути, модели, текстуры, attachment point и geoset;
- реализовать безопасный fallback при отсутствии ассета.
### Проверки совместимости
- персонаж игрока;
- другие игроки;
- NPC с тем же `display_id`;
- экран создания;
- экран выбора;
- парикмахерская;
- смена экипировки;
- превращения и shapeshift;
- транспорт;
- телепорт и смена карты;
- повторный вход и `/reload`.
## 8. Каталог данных кастомизации
Нужен единый, версионируемый источник данных, из которого генерируются клиентское и серверное представления.
Предлагаемая запись:
```text
option_id
choice_id
race_mask
gender_mask
category
label_key
render_type
char_section_id
geoset_ids
model_1
model_2
texture_1
texture_2
attachment_1
attachment_2
flags
conflict_group
unlock_condition
asset_manifest_id
enabled
```
Требования:
- `(option_id, choice_id)` никогда не переиспользуется с другим смыслом;
- удалённый вариант остаётся распознаваемым для миграции старых персонажей;
- порядок отображения UI хранится отдельно от стабильного идентификатора;
- клиент и сервер собираются из одной версии каталога;
- версия каталога входит в сетевое рукопожатие.
## 9. Серверное хранение
### Предлагаемая таблица
```sql
character_customization (
guid INT UNSIGNED NOT NULL,
option_id SMALLINT UNSIGNED NOT NULL,
choice_id INT UNSIGNED NOT NULL,
updated_at TIMESTAMP NOT NULL,
PRIMARY KEY (guid, option_id)
)
```
Дополнительно:
- индекс по `guid`;
- внешний ключ или очистка записей при удалении персонажа;
- миграция с начальными значениями для существующих персонажей;
- сервис сброса неизвестных/отключённых вариантов;
- аудит изменения внешности при необходимости.
### Серверная логика
- загрузить набор при `Character::LoadFromDB` или эквивалентном этапе;
- валидировать выбор по расе, полу, режиму игры и доступности;
- сохранять транзакционно;
- включать состояние в данные экрана выбора персонажа;
- отправлять полный snapshot при входе в мир;
- отправлять delta при изменении;
- рассылать актуальное состояние игрокам в зоне видимости;
- отправлять snapshot при появлении объекта в зоне видимости нового клиента;
- ограничить частоту изменений;
- запретить недоступные комбинации независимо от клиентского UI.
## 10. Сетевой протокол MoonWell
### Общие требования
- собственный namespace/opcode или надёжный WarcraftXL transport;
- версия протокола и версия каталога;
- явная длина сообщения и лимит числа записей;
- little-endian поля с фиксированной шириной;
- сервер является источником истины;
- неизвестные поля пропускаются или отклоняются согласно версии;
- отсутствие расширения не ломает стандартный вход в игру.
### Сообщения
```text
CMSG_MOONWELL_CUSTOMIZATION_CREATE
protocol_version
catalog_version
option_count
repeated(option_id, choice_id)
SMSG_MOONWELL_CUSTOMIZATION_RESULT
result_code
normalized choices
SMSG_MOONWELL_CUSTOMIZATION_SNAPSHOT
player_guid
catalog_version
option_count
repeated(option_id, choice_id)
CMSG_MOONWELL_CUSTOMIZATION_UPDATE
option_count
repeated(option_id, choice_id)
SMSG_MOONWELL_CUSTOMIZATION_DELTA
player_guid
option_count
repeated(option_id, choice_id)
```
### Экран выбора персонажа
Предпочтительный вариант — отдельный snapshot после получения списка персонажей. Не расширять штатный пакет без строгой проверки длины и совместимости.
Клиент должен:
- сопоставить snapshot с GUID персонажа;
- применить его к модели выбранного персонажа;
- хранить данные только до обновления от сервера;
- показывать стандартную внешность, если snapshot отсутствует или версия несовместима.
## 11. Клиентский runtime API
Планируемый API WarcraftXL:
```text
MoonWellCustomizationGetCatalogVersion()
MoonWellCustomizationGetOptions(race, gender)
MoonWellCustomizationGetChoices(optionId)
MoonWellCustomizationGetCurrent(optionId)
MoonWellCustomizationPreview(optionId, choiceId)
MoonWellCustomizationResetPreview()
MoonWellCustomizationCommitPreview()
MoonWellCustomizationRandomize()
MoonWellCustomizationApplySnapshot(guid, data)
MoonWellCustomizationClear(guid)
```
Требования:
- Lua получает только безопасные операции высокого уровня;
- нативный слой владеет указателями моделей и их жизненным циклом;
- повторное применение snapshot идемпотентно;
- preview не меняет серверное состояние;
- при ошибке модель возвращается к последнему подтверждённому snapshot;
- смена расы или пола пересчитывает допустимость всех параметров;
- отсутствие M2/текстуры отключает только конкретный вариант, а не весь экран.
## 12. Применение внешности
Порядок применения должен быть фиксированным:
1. базовая раса и пол;
2. стандартные параметры 3.3.5a;
3. `CharSections` и текстурные слои;
4. geoset базовой модели;
5. скрытие/конфликты с экипировкой;
6. независимые косметические M2-каналы;
7. обновление материалов и композитной текстуры;
8. окончательное обновление позы и камеры превью.
При смене одного параметра обновляется только затронутый слой, если это безопасно. Полная пересборка модели используется как fallback.
## 13. Интерфейс создания персонажа
### Структура
- категории слева или справа от модели;
- прокручиваемая сетка вариантов;
- текстовое название активной категории;
- состояние выбранного варианта;
- кнопки случайного выбора и сброса;
- возврат к выбору расы/класса без потери валидных параметров;
- подтверждение создания только после серверной валидации;
- плавное приближение камеры к зоне редактирования.
### Категории первой версии
- кожа;
- лицо;
- причёска;
- цвет волос;
- борода/особенность лица;
- шрамы;
- татуировки;
- серьги/пирсинг;
- расовый аксессуар.
Категории скрываются, если для текущей расы/пола нет вариантов.
### Камера
- профиль камеры задаётся по расе, полу и категории;
- отдельно настраиваются distance, вертикальная цель, горизонтальный сдвиг и FOV;
- переход использует easing через `OnUpdate`;
- короткие и средние расы имеют отдельные профили;
- при выходе из кастомизации камера плавно возвращается к полному росту;
- вращение персонажа доступно во всех режимах;
- UI не перекрывает лицо и активный аксессуар на 4:3, 16:9 и 16:10.
### Ограничения UI 3.3.5a
- XML загружается раньше Lua;
- именованные Button и ScrollFrame;
- `relativeTo`, а не `relativeKey`;
- `$parentX`, а не `$parent.X`;
- `SetWidth`/`SetHeight` вместо `SetSize`;
- таймеры и анимации через `OnUpdate`;
- UI-атласы режутся на тайлы не более 512×512.
## 14. Парикмахерская и изменение существующего персонажа
- использовать тот же каталог и preview API;
- получать текущий серверный snapshot при открытии;
- считать стоимость на сервере;
- подтверждать изменение одним атомарным запросом;
- откатывать preview при закрытии или отказе сервера;
- блокировать недоступные варианты;
- поддерживать бесплатные/платные категории отдельными правилами;
- обновлять внешность окружающим только после подтверждения.
## 15. Получение retail-ассетов без установки клиента
### Инструменты
- `wow.export` в online-режиме — поиск и предварительный просмотр;
- Wago Tools/wow-listfile — поиск пути и `FileDataID`;
- TACTSharp/TACTTool — точечная загрузка исходных файлов с CDN Blizzard.
### Манифест ассета
```text
asset_id
source_product
source_build
file_data_id
original_path
target_path
sha256
kind
parent_asset_id
license_note
```
### Пайплайн
1. Выбрать модель и исходный retail build.
2. Зафиксировать корневой `FileDataID`.
3. Выгрузить M2 и определить зависимости.
4. Загрузить `.skin`, `.skel`, `.anim` и текстуры.
5. Проверить хэши и комплектность.
6. Проверить модель в изолированном preview.
7. Проверить downport через `wxl-modern-assets`.
8. Добавить запись в каталог кастомизации.
9. Упаковать только выбранные файлы.
10. Не помещать массовый дамп retail-клиента в Git.
### Автоматические проверки ассета
- поддерживаемая версия M2/MD21;
- наличие всех skin-профилей;
- разрешимые FileDataID и пути текстур;
- наличие внешнего скелета/анимаций;
- attachment points для целевой расы;
- лимит костей на draw после разбиения;
- валидные материалы и blend modes;
- отсутствие коллизий целевых путей;
- размер итогового MPQ и память после загрузки;
- корректное освобождение ресурсов.
## 16. Этапы реализации
### Этап 0. Исследование и фиксация контрактов
- [ ] Зафиксировать ревизии WarcraftXL и всех модулей.
- [ ] Зафиксировать исходный retail build для ассетов.
- [ ] Составить карту существующих хуков персонажа и DBC.
- [ ] Проверить структуру штатных пакетов создания/выбора персонажа.
- [ ] Зафиксировать каталог option/choice ID.
- [ ] Утвердить правила доступности и монетизации.
- [ ] Выбрать первую расу для вертикального среза: человек.
**Результат:** техническая спецификация без конфликтующих точек расширения.
### Этап 1. Интеграция equip-extension
- [ ] Добавить репозиторий как закреплённый submodule.
- [ ] Адаптировать CMake/module layout.
- [ ] Собрать Win32 runtime и x64 host.
- [ ] Устранить конфликт хуков с текущим WarcraftXL.
- [ ] Исправить снятие collection M2.
- [ ] Очистить мусорные поля `ItemDisplayInfo.dbc`.
- [ ] Добавить smoke-тест Model1/Model2 на каждом поддержанном слоте.
- [ ] Добавить диагностическое логирование.
**Результат:** современная объёмная экипировка работает и корректно снимается.
### Этап 2. Независимые косметические каналы
- [ ] Отделить attachment runtime от реальных слотов экипировки.
- [ ] Реализовать реестр косметических каналов.
- [ ] Реализовать add/replace/remove/clear.
- [ ] Реализовать conflict groups и приоритеты.
- [ ] Реализовать скрытие под шлемом.
- [ ] Добавить API preview.
- [ ] Проверить смену карты, формы и пола.
**Результат:** аксессуар можно показать без создания фиктивного предмета.
### Этап 3. Asset pipeline
- [ ] Добавить входной manifest с FileDataID.
- [ ] Реализовать пакетную загрузку через CDN.
- [ ] Добавить локальный кэш по хэшу.
- [ ] Автоматически проверять зависимости.
- [ ] Формировать отчёт о пропущенных файлах.
- [ ] Интегрировать выбранные ассеты в MPQ build.
- [ ] Добавить контроль размера и дубликатов.
**Результат:** воспроизводимая сборка выбранных retail-ассетов без полного клиента.
### Этап 4. Серверное хранение и протокол
- [ ] Добавить миграцию `character_customization`.
- [ ] Реализовать серверный каталог допустимых вариантов.
- [ ] Реализовать create/update validation.
- [ ] Реализовать snapshot и delta.
- [ ] Подключить данные к character enum/select flow.
- [ ] Рассылать состояние в visibility range.
- [ ] Добавить rate limit и журнал ошибок.
- [ ] Добавить fallback для клиента без совместимой версии.
**Результат:** параметры сохраняются и одинаково видны всем клиентам.
### Этап 5. Текстурные слои и geoset
- [ ] Расширить/переопределить `CharSections` данными MoonWell.
- [ ] Реализовать слои шрамов, татуировок и макияжа.
- [ ] Реализовать таблицу geoset базовой модели.
- [ ] Синхронизировать цветовые зависимости.
- [ ] Обеспечить совместимость с надетой бронёй.
- [ ] Добавить полную пересборку как fallback.
**Результат:** работают не только прикрепляемые M2, но и полноценные параметры тела/лица.
### Этап 6. Новый UI создания персонажа
- [ ] Создать адаптивную панель категорий.
- [ ] Создать прокручиваемую сетку choices.
- [ ] Подключить preview API.
- [ ] Добавить race/gender/category camera profiles.
- [ ] Добавить randomize и reset.
- [ ] Добавить обработку недоступного ассета.
- [ ] Добавить серверное подтверждение создания.
- [ ] Проверить 4:3, 16:9, 16:10 и UI scale.
**Результат:** законченный retail-подобный пользовательский сценарий.
### Этап 7. Вертикальный срез — человек
- [ ] Мужская и женская модели.
- [ ] Стандартные категории.
- [ ] Минимум одна новая текстурная категория.
- [ ] Минимум два M2-косметических канала.
- [ ] Сохранение, выбор персонажа и отображение в мире.
- [ ] Видимость другим игрокам.
- [ ] Парикмахерская/изменение существующего персонажа.
- [ ] Полный regression-тест экипировки.
**Результат:** одна раса полностью проходит production-сценарий.
### Этап 8. Масштабирование на все расы WotLK
- [ ] Дворфы.
- [ ] Ночные эльфы.
- [ ] Гномы.
- [ ] Дренеи.
- [ ] Орки.
- [ ] Нежить.
- [ ] Таурены.
- [ ] Тролли.
- [ ] Эльфы крови.
- [ ] Отдельная настройка камер для каждого пола.
- [ ] Матрица конфликтов расовых элементов с экипировкой.
**Результат:** единый уровень качества для всех доступных рас.
### Этап 9. Стабилизация и выпуск
- [ ] Нагрузочный тест большого скопления игроков.
- [ ] Проверка утечек модели/текстур при многократной смене вариантов.
- [ ] Проверка холодного кэша WarcraftXLHost.
- [ ] Проверка отсутствующих/повреждённых ассетов.
- [ ] Версионирование manifest, каталога и протокола.
- [ ] Миграция существующих персонажей.
- [ ] Rollback-пакет.
- [ ] Документация для добавления новой категории/расы.
- [ ] Обновление deploy-скрипта и launcher manifest.
**Результат:** воспроизводимый production-релиз.
## 17. Матрица тестирования
### Персонажи
- каждая раса × каждый пол;
- стандартный режим × предатель;
- новый персонаж × существующий персонаж;
- минимальное × максимальное число выбранных параметров;
- случайная комбинация × преднамеренно конфликтная комбинация.
### Состояния клиента
- создание;
- выбор;
- загрузочный экран;
- вход в мир;
- появление другого игрока;
- телепорт;
- смерть/дух;
- транспорт;
- shapeshift/превращение;
- парикмахерская;
- смена экипировки;
- скрытие шлема/плаща;
- `/reload`;
- переподключение;
- выход и повторный вход.
### Графика
- native renderer и основной D3D9On12 путь;
- разные уровни component texture quality;
- включённые/выключенные SSAA, SSAO и AA;
- дальние LOD;
- тени;
- прозрачные и emissive материалы;
- несколько одинаковых моделей одновременно;
- 4:3, 16:9, 16:10 и разные UI scale.
### Сеть и сервер
- потеря/повтор snapshot;
- snapshot до появления модели и после неё;
- неизвестная версия каталога;
- неизвестный option/choice;
- запрещённый выбор;
- слишком большое сообщение;
- быстрое повторное изменение;
- одновременное изменение и телепорт;
- сохранение при logout/crash;
- удаление и восстановление персонажа.
## 18. Производительность
Целевые ограничения:
- не выполнять разбор каталога каждый кадр;
- не пересобирать всю модель при изменении независимого аксессуара;
- кэшировать разрешённые пути и обработанные modern assets;
- не создавать дубликаты одной и той же дочерней M2-модели;
- освобождать attachment при удалении/скрытии объекта;
- ограничить число одновременных косметических M2 на персонажа;
- иметь настройку отключения дальних косметических attachment;
- измерять время первой загрузки, потребление RAM/VRAM и frame time;
- логирование hot path отключено в release-сборке.
Предварительный бюджет первой версии:
- до 6 независимых M2-каналов на персонажа;
- не более двух моделей на канал;
- отсутствие постоянной работы Lua `OnUpdate`, кроме активной UI-анимации;
- пересборка полной внешности только при snapshot, смене расы/пола или восстановлении после ошибки.
## 19. Безопасность и целостность
- сервер проверяет все ID и комбинации;
- клиент не передаёт пути файлов серверу;
- протокол содержит только числовые option/choice ID;
- сообщения имеют максимальный размер и максимальное число записей;
- сервер нормализует дубликаты option ID;
- неизвестные значения не записываются в БД;
- стоимость изменения рассчитывается сервером;
- unlock condition проверяется сервером;
- клиентский каталог не считается доказательством доступности;
- журналировать несовпадение версии и попытки недопустимых изменений.
## 20. Сборка, доставка и откат
### Сборка
- закреплённые submodule revisions;
- asset manifest с SHA-256;
- генерация клиентского и серверного каталога из одного источника;
- сборка WarcraftXL Win32 и Host x64;
- сборка MPQ;
- автоматическая проверка оригинального `Wow.exe`;
- формирование launcher manifest.
### Версионирование
- `customization_protocol_version`;
- `customization_catalog_version`;
- `customization_asset_version`;
- совместимая версия WarcraftXL runtime.
### Откат
- сервер умеет не отправлять расширенные сообщения;
- клиент при отсутствии данных показывает стандартную внешность;
- отключение модуля не блокирует вход персонажа;
- миграция БД не удаляет стандартные параметры;
- предыдущие MPQ/runtime хранятся как отдельный release artifact;
- неизвестные сохранённые choices не удаляются автоматически без миграции.
## 21. Оценка трудоёмкости
Для одного разработчика при наличии подготовленных ассетов:
| Работа | Оценка |
|---|---:|
| Адаптация и исправление equip-extension | 47 дней |
| Независимые косметические каналы | 4–7 дней |
| Asset pipeline | 35 дней |
| Серверное хранение и протокол | 4–7 дней |
| Текстурные слои и geoset | 5–10 дней |
| Новый UI создания/изменения | 5–8 дней |
| Полный вертикальный срез одной расы | 5–10 дней |
| Каждая следующая раса | 2–5 дней |
| Стабилизация и regression | 712 дней |
Ориентиры:
- технический прототип одной расы: 1–2 недели;
- production-вертикаль людей: 3–5 недель;
- все десять рас WotLK: 2–3 месяца;
- насыщенность категориями, близкая к retail: 3–6 месяцев.
Главный переменный фактор — подготовка и проверка ассетов, а не серверная схема.
## 22. Рекомендуемый первый milestone
Вертикальный срез для людей обоих полов:
- штатные пять категорий;
- одна текстурная категория: шрамы;
- два независимых M2-канала: серьги и аксессуар для волос;
- серверное сохранение;
- snapshot на экране выбора;
- видимость другим игрокам;
- изменение через парикмахерскую;
- камера по категориям;
- корректная работа со шлемом;
- один полный набор automated/manual acceptance checks.
Этот milestone должен подтвердить всю архитектуру до масштабирования на остальные расы.
## 23. Критерии готовности production-версии
- [ ] `Wow.exe` идентичен оригиналу по SHA-256.
- [ ] Все расширения поставляются через WarcraftXL и MPQ.
- [ ] Клиент и сервер используют одинаковую версию каталога.
- [ ] Внешность сохраняется после выхода и перезапуска клиента.
- [ ] Экран создания, экран выбора и модель в мире совпадают.
- [ ] Другие игроки видят подтверждённую сервером внешность.
- [ ] Недоступные варианты невозможно применить изменённым клиентом.
- [ ] Смена/снятие варианта не оставляет collection M2.
- [ ] Нет placeholder-моделей из мусорных строк `ItemDisplayInfo.dbc`.
- [ ] Нет заметных утечек памяти при многократном preview.
- [ ] Отсутствующий ассет не приводит к падению клиента.
- [ ] Все расы и оба пола проходят тестовую матрицу.
- [ ] Камера корректно кадрирует низких, средних и высоких персонажей.
- [ ] UI работает на 4:3, 16:9, 16:10 и разных UI scale.
- [ ] Реализован и проверен rollback.
- [ ] Описан процесс добавления новой категории, choice и ассета.
## 24. Открытые решения перед началом реализации
- точная ревизия `wxl-equip-extension` для закрепления;
- список категорий первого релиза;
- исходный retail build для моделей;
- набор моделей для вертикального среза людей;
- формат единого каталога: JSON, CSV или генератор из SQL/YAML;
- транспорт custom packets между WarcraftXL и AzerothCore;
- способ передачи snapshots на экране выбора;
- политика скрытия аксессуаров под шлемом;
- правила доступности и разблокировки вариантов;
- использование парикмахерской или отдельного UI изменения внешности;
- лимит косметических M2 на одного персонажа;
- политика хранения выбранных Blizzard-ассетов и доступа к ним;
- минимальная поддерживаемая версия launcher/runtime.
+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`.
+102 -62
View File
@@ -1,90 +1,130 @@
# MoonWell Client Sources
Репозиторий с ресурсами и исполняемым файлом модифицированного клиента World of Warcraft 3.3.5a.
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
Основная цель проекта: хранить и сопровождать клиентские изменения поверх стандартных MPQ-пакетов WoW, включая интерфейс, графику, звук и дополнительные данные для кастомных систем сервера.
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL/wxl-core) **1.1.220**: `Wow.exe` больше не содержит
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
только к памяти запущенного процесса.
## Что находится в репозитории
## Что перенесено из модифицированного Wow.exe
- `Wow.exe` - модифицированный клиентский исполняемый файл.
- Поддерживает использование до 4 ГБ ОЗУ.
- Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели.
- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`.
- Содержит графические элементы внутриигрового магазина.
- Основная зона изменений: `Interface/Store_UI`.
- `patch-ruRU-4/` - наши изменения поверх стандартных `patch-ruRU-1.MPQ`, `patch-ruRU-2.MPQ` и `patch-ruRU-3.MPQ`.
- Содержит ресурсы локали `ruRU`, связанные с экраном входа, `GlueXML`, заставками и звуком.
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/LoginScreen`, `Sound/Ambience`, `Sound/Music`.
- `patch-ruRU-5/` - пакет русской локализации и дополнительных интерфейсных ресурсов.
- Содержит перевод на русский язык для glue-интерфейса и связанных экранов клиента.
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/GluesVideo`, `Interface/Loginscreen`, `Interface/tooltips`, `Interface/cinematics`.
- `patch-Z/` - кастомный пакет для модуля Mythic+.
- Содержит графические ресурсы Mythic+.
- Также включает связанные клиентские данные в `DBFielsClient/` и иконки в `Interface/Icons/`.
- разблокировка пользовательского `GlueXML`;
- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`;
- одиннадцатое значение `GetCharacterInfo``charFlags`, включая флаг предателя `0x40000000`;
- загрузка WarcraftXL без import-table patch и без отдельного injector.
## Структура по назначению
При установке сборщик включает Large Address Aware в копии `Wow.exe`; исходный
`Wow_Original.exe` остаётся неизменным и используется как проверенный шаблон.
### Внутриигровой магазин
## Структура
Файлы магазина находятся в [`patch-4/Interface/Store_UI`](./patch-4/Interface/Store_UI).
```text
modules/ отдельные MoonWell extensions для WarcraftXL 1.1
vendor/warcraftxl/ закреплённый upstream git submodule
vendor/modules/ закреплённые модули WarcraftXL
src/Data/ исходники MPQ-патчей
tool/ сборщик MPQ
build-warcraftxl.ps1 сборка Win32 runtime, proxy и extensions
run.ps1 полная сборка, установка и запуск клиента
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
```
Примеры ресурсов:
Подключены совместимые с 1.1 extensions: `wxl-db2`, `wxl-modern-adt`, `wxl-modern-m2`,
`wxl-modern-wmo`, `wxl-grasswind` и `wxl-unit-outline`. BLP-адаптер и MoonWell-изменения
собираются как отдельные DLL; исходники upstream core не изменяются.
Для обновления всех зависимостей WarcraftXL:
- `Currencies/Gold.blp`
- `Currencies/Token.blp`
- `Frames/StoreFrame_Main.blp`
```powershell
git submodule update --remote vendor/warcraftxl vendor/modules/*
```
### Экран входа и локализованные ресурсы
После обновления обязательно пересоберите и проверьте вход/создание персонажа: API WarcraftXL
пока развивается.
Файлы экрана входа и связанные ресурсы находятся в [`patch-ruRU-4`](./patch-ruRU-4).
## Требования
Основные каталоги:
- Visual Studio 2022 с C++ toolchain для Win32;
- CMake 3.20+;
- Rust/Cargo для существующего MPQ-сборщика;
- локальный оригинальный `Wow_Original.exe` с SHA-256
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
- `Interface/GlueXML` - Lua/XML-исходники интерфейса загрузки и логина
- `Interface/Glues` - графика и модели для glue-экрана
- `Interface/LoginScreen` - фон и связанные изображения
- `Sound/Ambience/GlueScreen` - фоновые звуки
- `Sound/Music/GlueScreenMusic` - музыка экрана входа
После клонирования инициализируйте submodule:
### Русский перевод для `patch-ruRU-5`
```powershell
git submodule update --init --recursive
```
Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5).
## Сборка WarcraftXL
Основные каталоги:
Быстрое полное развёртывание клиента одной командой:
- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа
- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect`
- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов
- `Interface/Loginscreen` - фон и изображения экрана входа
- `Interface/tooltips` - локализованные рамки и оформление тултипов
- `Interface/cinematics` - элементы интерфейса для роликов и заставок
```powershell
.\deploy.ps1
```
Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов.
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, native D3D9 proxy и extensions,
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
### Mythic+
Полезные варианты:
Файлы Mythic+ находятся в [`patch-Z`](./patch-Z).
```powershell
# Явный путь и запуск клиента после установки
.\deploy.ps1 -ClientPath 'D:\Games\World of Warcraft' -StopClient -Launch
Основные каталоги:
# Быстро обновить только WarcraftXL без пересборки MPQ
.\deploy.ps1 -SkipDataBuild
- `Interface/MythicPlus/textures` - текстуры интерфейса Mythic+
- `Interface/MythicPlus/sounds` - звуки таймеров и событий
- `Interface/Icons` - дополнительные иконки
- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом
```
## Как воспринимать содержимое репозитория
Без установки в клиент:
- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде.
- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения.
- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла.
```powershell
.\build-warcraftxl.ps1 -Configuration Release
```
## Для чего используется
С установкой:
Проект нужен для сопровождения клиентской части MoonWell:
```powershell
.\build-warcraftxl.ps1 -Configuration Release `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
- кастомизации GlueXML;
- поддержки русского перевода для `patch-ruRU-5`;
- поддержки внутриигрового магазина;
- добавления графики и данных для Mythic+;
- хранения пакетов графических улучшений;
- работы с модифицированным `Wow.exe` для WoW 3.3.5a.
При `-Deploy` скрипт:
1. проверяет хеш `Wow_Original.exe`;
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
3. восстанавливает оригинальный `Wow.exe`;
4. устанавливает `WarcraftXL.dll`, native D3D9 proxy и DLL в `Extensions\`;
5. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
Полная сборка MPQ, установка WarcraftXL и запуск:
```powershell
$env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
.\run.ps1
```
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
## MPQ-пакеты
Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки
WarcraftXL не требуется и в объектное хранилище не загружается.
Основные пакеты:
- `patch-4` — ресурсы внутриигрового магазина;
- `patch-ruRU-4` — экран входа и GlueXML;
- `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
## Лицензирование
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
MoonWell extensions распространяются с доступным соответствующим исходным кодом. Репозиторий не
должен публиковать Blizzard assets или `Wow.exe`.
Binary file not shown.
+535
View File
@@ -0,0 +1,535 @@
---
name: wow-335a-ui
description: >
Верстка и разработка UI-аддонов для World of Warcraft 3.3.5a (WotLK, build 12340).
Используй этот скилл при любой работе с интерфейсом WoW 3.3.5: создание фреймов
через XML или Lua CreateFrame(), позиционирование через SetPoint/якоря, слои
(BACKGROUND/ARTWORK/OVERLAY/HIGHLIGHT), страты (frameStrata), текстуры, шрифты,
кнопки, ScrollFrame, скрипты-события, структура аддона (TOC + XML + Lua),
SavedVariables, работа с Blizzard API патча 3.3.5. Также применяй когда речь идёт
об AzerothCore, кастомных интерфейсах для WoW-прайват серверов 3.3.5a, Eluna
или модификации FrameXML. ОБЯЗАТЕЛЬНО применяй при задачах pixel-perfect переноса
дизайна (Figma/PSD/скриншот) в WoW UI: пересчёт координат с учётом UIParent scale,
конвертация цветов HEX→RGBA, подбор шрифтов, подготовка текстур (степени двойки,
TexCoord-кроп), устранение расхождений между макетом и игровым интерфейсом.
---
# WoW 3.3.5a — Верстка UI (FrameXML / AddOn Development)
## Ключевые ограничения патча 3.3.5a
- **API версии**: `## Interface: 30300` (build 12340)
- **Lua версия**: Lua 5.1
- **Текстуры**: только степени двойки по ширине/высоте (8, 16, 32, 64, 128, 256, 512 px); максимум 512×512
- **Нет** `:SetSize()` — только `:SetWidth()` / `:SetHeight()` (или через XML `<Size>`)
- **Нет** `hooksecurefunc` для защищённых фреймов без taint
- Отсутствуют многие retail-API: нет `C_Timer`, нет `CreateFramePool`, нет `PixelUtil`
- Таймеры — через `OnUpdate` с накоплением `elapsed`
---
## Pixel-Perfect: перенос дизайна из Figma/PSD в WoW UI
### Главная проблема: система координат WoW ≠ пиксели экрана
WoW рисует UI в **виртуальных единицах**, а не в экранных пикселях. Размер виртуального экрана зафиксирован движком на **768 единиц по высоте**, ширина зависит от соотношения сторон:
| Разрешение | Виртуальная ширина | Виртуальная высота |
|-----------|-------------------|-------------------|
| 1024×768 | 1024 | 768 |
| 1280×960 | 1024 | 768 |
| 1920×1080 | 1365 | 768 |
| 2560×1440 | 1365 | 768 |
> **Вывод**: высота UIParent всегда = 768. Всё, что сделано в Figma на 768px по высоте, переносится 1:1 без пересчёта. При другой высоте холста в Figma — нужен коэффициент.
### Пересчёт координат из Figma в WoW
```lua
-- Формула: wow_value = figma_value * (768 / figma_canvas_height)
-- Если макет сделан на 1080px:
local SCALE = 768 / 1080 -- ≈ 0.711
local function px(figma_px)
return math.floor(figma_px * SCALE + 0.5) -- округление к ближайшему
end
-- Использование:
frame:SetWidth(px(400)) -- figma: 400px → wow: 284
frame:SetHeight(px(200)) -- figma: 200px → wow: 142
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
```
> Если макет 768px в высоту — `SCALE = 1.0`, пересчёт не нужен.
### UIParent scale и как он ломает пиксели
Игрок может изменить масштаб UI в настройках. Это сдвигает все координаты.
```lua
-- Получить текущий масштаб UIParent
local uiScale = UIParent:GetScale() -- обычно 0.641.0
-- Перевести экранные пиксели в виртуальные единицы WoW
local function screenPxToWow(px)
local screenH = select(2, GetPhysicalScreenSize()) -- реальный размер экрана
return px * (768 / screenH) / uiScale
end
-- Принудительно зафиксировать scale (использовать с умом!)
-- UIParent:SetScale(1.0) -- сломает все стандартные фреймы
```
**Рекомендация**: не трогай `UIParent:SetScale()`. Вместо этого рисуй в виртуальных единицах и принимай, что у разных игроков масштаб разный. Для абсолютного pixel-perfect на конкретном разрешении — дай опцию ручного масштабирования фрейма в настройках.
```lua
-- Масштабируй только свой фрейм
frame:SetScale(0.85)
-- Тогда все дочерние элементы тоже масштабируются
```
---
### Цвета: Figma HEX/RGBA → WoW RGBA
WoW принимает цвета в диапазоне **0.01.0** (не 0255).
```lua
-- Конвертер HEX → WoW RGBA
local function hex(str, a)
str = str:gsub("#", "")
local r = tonumber(str:sub(1,2), 16) / 255
local g = tonumber(str:sub(3,4), 16) / 255
local b = tonumber(str:sub(5,6), 16) / 255
return r, g, b, a or 1.0
end
-- Использование:
tex:SetVertexColor(hex("#FF6A00")) -- непрозрачный
tex:SetVertexColor(hex("#FF6A00", 0.8)) -- 80% непрозрачность
fs:SetTextColor(hex("#FFD100"))
frame:SetBackdropColor(hex("#1A1A2E", 0.95))
frame:SetBackdropBorderColor(hex("#4A90D9"))
```
**Figma opacity → WoW alpha**: значение из Figma (например, 75%) делится на 100 → `0.75`.
```lua
-- Figma: opacity 60%
frame:SetAlpha(0.60)
tex:SetAlpha(0.60)
```
---
### Шрифты: как приблизить к дизайну
WoW 3.3.5a поддерживает только `.ttf` шрифты, помещённые в папку аддона.
```lua
-- Кастомный шрифт из аддона
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", 14, "OUTLINE")
-- Флаги: "", "OUTLINE", "THICKOUTLINE", "MONOCHROME", "OUTLINE,MONOCHROME"
```
**Соответствие Figma → WoW размер шрифта**:
| Figma (макет 1080px) | WoW pt (при SCALE=0.711) |
|---------------------|--------------------------|
| 12 px | 9 |
| 14 px | 10 |
| 16 px | 11 |
| 18 px | 13 |
| 24 px | 17 |
| 32 px | 23 |
```lua
-- Автоматический пересчёт размера шрифта
local function fontSize(figma_pt)
return math.max(8, math.floor(figma_pt * SCALE + 0.5))
end
fs:SetFont("Fonts\\FRIZQT__.TTF", fontSize(18), "OUTLINE")
```
> **Минимальный читаемый размер в WoW**: 8 pt. Ниже — нечитаемо.
**Встроенные шрифты WoW 3.3.5a**:
```
Fonts\FRIZQT__.TTF — основной интерфейсный (без кириллицы!)
Fonts\MORPHEUS.ttf — декоративный (заголовки)
Fonts\ARIALN.TTF — Arial Narrow
Fonts\skurri.ttf — шрифт повреждений
```
> **Кириллица**: встроенные шрифты WoW не поддерживают кириллицу. Положи в аддон `.ttf` с кириллицей (например, PT Sans, Roboto) и подключи явно.
---
### Текстуры: подготовка под ограничения движка
#### Требования к текстурам WoW 3.3.5a
- Ширина и высота — **степени двойки**: 2, 4, 8, 16, 32, 64, 128, 256, 512
- Максимум: **512×512 px**
- Формат: `.tga` (RGBA, 32-bit) или `.blp`
- Текстура не обязана быть квадратной: 512×64 — ок, 256×128 — ок
#### Как уместить большой элемент дизайна
Если элемент из Figma больше 512px — разрезай на части:
```
[512×256] [512×256] ← верхняя половина (левая + правая)
[512×256] [512×256] ← нижняя половина
```
```lua
-- Сборка из 4 текстур
local tl = frame:CreateTexture(nil, "BACKGROUND")
tl:SetTexture("Interface\\AddOns\\MyAddon\\bg_tl")
tl:SetPoint("TOPLEFT", frame, "TOPLEFT")
tl:SetWidth(256); tl:SetHeight(128)
local tr = frame:CreateTexture(nil, "BACKGROUND")
tr:SetTexture("Interface\\AddOns\\MyAddon\\bg_tr")
tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT")
tr:SetWidth(256); tr:SetHeight(128)
-- ... и так далее
```
#### TexCoord — показать только часть текстуры
Если в одном файле несколько элементов (спрайт-лист), используй `SetTexCoord`:
```lua
-- SetTexCoord(left, right, top, bottom) — от 0.0 до 1.0
-- Пример: иконка занимает правый нижний квадрант 256×256 атласа
tex:SetTexCoord(0.5, 1.0, 0.5, 1.0)
-- Формула для ячейки в сетке (col/row с нуля):
local cols, rows = 4, 4
local col, row = 2, 1 -- третья колонка, вторая строка
tex:SetTexCoord(
col/cols, -- left
(col+1)/cols, -- right
row/rows, -- top
(row+1)/rows -- bottom
)
```
> **Важно**: при SetTexCoord текстура тайлится внутри заданного Size фрейма. SetAllPoints растягивает — SetWidth/SetHeight + SetPoint = точный размер.
---
### Позиционирование: от Figma к SetPoint
В Figma позиция элемента — это X,Y от левого верхнего угла холста (или родителя). В WoW — якорная система.
**Алгоритм переноса**:
1. Определи ближайший угол/сторону в Figma (от чего считать)
2. Вычисли отступ от этой стороны
3. Пересчитай через `px()`
```lua
-- Figma (холст 1365×768):
-- Элемент: X=50, Y=30, W=200, H=100
-- → привязка от TOPLEFT UIParent
frame:SetWidth(px(200))
frame:SetHeight(px(100))
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
-- ^^^ ^^^
-- X offset Y offset (отрицательный! вниз)
```
> **Знаки осей WoW**: X растёт вправо (+), Y растёт вверх (+). Отступ вниз от TOPLEFT — **отрицательный**.
| Figma anchor | WoW SetPoint |
|-------------|-------------|
| Top-Left | `"TOPLEFT", UIParent, "TOPLEFT", x, -y` |
| Top-Right | `"TOPRIGHT", UIParent, "TOPRIGHT", -x, -y` |
| Bottom-Left | `"BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y` |
| Bottom-Right | `"BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -x, y` |
| Center | `"CENTER", UIParent, "CENTER", x - W/2_canvas, y` |
---
### Вспомогательный модуль: px-утилиты (вставь в начало Lua)
```lua
-- PixelHelper — pixel-perfect вёрстка из Figma
local PH = {}
PH.CANVAS_HEIGHT = 768 -- высота холста в Figma (измени если другая!)
function PH.scale()
return 768 / PH.CANVAS_HEIGHT
end
-- Пересчёт px из Figma → WoW единицы
function PH.px(v)
return math.floor(v * PH.scale() + 0.5)
end
-- Цвет из HEX (#RRGGBB или #RRGGBBAA)
function PH.color(hex, alpha)
hex = hex:gsub("#","")
local r = tonumber(hex:sub(1,2),16)/255
local g = tonumber(hex:sub(3,4),16)/255
local b = tonumber(hex:sub(5,6),16)/255
local a = alpha or (hex:len() == 8 and tonumber(hex:sub(7,8),16)/255) or 1
return r, g, b, a
end
-- Размер шрифта из Figma → WoW
function PH.font(pt)
return math.max(8, math.floor(pt * PH.scale() + 0.5))
end
-- Установка якоря с автопересчётом
function PH.point(frame, point, relativeTo, relPoint, x, y)
frame:SetPoint(point, relativeTo, relPoint, PH.px(x or 0), PH.px(y or 0))
end
-- Размер фрейма из Figma
function PH.size(frame, w, h)
frame:SetWidth(PH.px(w))
frame:SetHeight(PH.px(h))
end
-- Экспорт глобально (или оставь локальным)
MyAddon_PH = PH
```
**Использование**:
```lua
local px = MyAddon_PH.px
local color = MyAddon_PH.color
local pxfont = MyAddon_PH.font
MyAddon_PH.size(frame, 400, 250)
MyAddon_PH.point(frame, "TOPLEFT", UIParent, "TOPLEFT", 50, -30)
frame:SetBackdropColor(color("#1A1A2E", 0.95))
fs:SetTextColor(color("#FFD100"))
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", pxfont(16), "OUTLINE")
```
---
### Чеклист pixel-perfect переноса
- [ ] Холст Figma = 768px в высоту (или задан `PH.CANVAS_HEIGHT`)
- [ ] Все размеры и отступы пересчитаны через `px()`
- [ ] Цвета конвертированы через `color()`
- [ ] Текстуры нарезаны до 512×512, размеры кратны степени двойки
- [ ] Шрифты `.ttf` подключены из папки аддона (если кириллица или кастомный шрифт)
- [ ] Размеры шрифтов пересчитаны через `font()`
- [ ] Y-отступы от верхних якорей — **отрицательные**
- [ ] Проверено на scale UIParent ≠ 1.0 (изменить в настройках игры для теста)
- [ ] Текстуры без SetAllPoints — точный SetWidth/SetHeight + SetPoint
---
```
MyAddon/
├── MyAddon.toc ← обязателен, имя файла = имя папки
├── MyAddon.xml ← верстка фреймов (опционально)
└── MyAddon.lua ← логика
```
### TOC-файл (3.3.5a)
```toc
## Interface: 30300
## Title: My Addon
## Notes: Описание аддона
## Author: Ilyas
## Version: 1.0
## SavedVariables: MyAddonDB
MyAddon.xml
MyAddon.lua
```
> Файлы загружаются **в порядке перечисления**. XML раньше Lua — фреймы существуют к моменту выполнения скриптов.
---
## XML-верстка
### Минимальный шаблон XML-файла
```xml
<Ui xmlns="http://www.blizzard.com/wow/ui/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<Script file="MyAddon.lua"/>
<Frame name="MyAddon_Frame" parent="UIParent" toplevel="true" enableMouse="true" movable="true">
<Size>
<AbsDimension x="300" y="200"/>
</Size>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
edgeFile="Interface\DialogFrame\UI-DialogBox-Border"
tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize><AbsValue val="32"/></TileSize>
<EdgeSize><AbsValue val="32"/></EdgeSize>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentTitle" inherits="GameFontNormalLarge" text="Заголовок">
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentClose" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor>
</Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>MyAddon_Frame_OnLoad(self)</OnLoad>
<OnMouseDown>self:StartMoving()</OnMouseDown>
<OnMouseUp>self:StopMovingOrSizing()</OnMouseUp>
</Scripts>
</Frame>
</Ui>
```
---
## Точки якоря (FRAMEPOINT)
| Точка | Расположение |
|-------|-------------|
| `TOPLEFT` | Верхний левый угол |
| `TOP` | Верхний центр |
| `TOPRIGHT` | Верхний правый угол |
| `LEFT` | Левый центр |
| `CENTER` | Центр |
| `RIGHT` | Правый центр |
| `BOTTOMLEFT` | Нижний левый угол |
| `BOTTOM` | Нижний центр |
| `BOTTOMRIGHT` | Нижний правый угол |
### SetPoint через Lua
```lua
-- frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, -10)
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
frame:SetPoint("BOTTOMRIGHT", anotherFrame, "TOPRIGHT", 0, 5)
-- Сброс якорей перед переустановкой
frame:ClearAllPoints()
frame:SetPoint("CENTER", UIParent, "CENTER")
```
> **Правило**: фрейм должен иметь достаточно якорей для однозначного определения позиции **и** размера. Либо Size + один якорь, либо два якоря задающих оба измерения.
---
## Слои и страты
### Draw Layers (внутри фрейма, сверху вниз по видимости)
```
BACKGROUND (0) → BORDER (1) → ARTWORK (2) → OVERLAY (3) → HIGHLIGHT (4)
```
- `HIGHLIGHT` — автоматически показывается при наведении мыши (требует `enableMouse="true"`)
- FontString всегда рисуется **поверх** Texture в том же слое
- Порядок текстур в одном слое **не гарантирован** → разноси по слоям
### Frame Strata (между фреймами)
```
WORLD → BACKGROUND → LOW → MEDIUM → HIGH → DIALOG → FULLSCREEN → FULLSCREEN_DIALOG → TOOLTIP
```
```lua
frame:SetFrameStrata("DIALOG")
frame:SetFrameLevel(5) -- внутри страты (0–255)
```
---
## Виджеты, события, Blizzard API
> Полный справочник — в `references/widgets-and-api.md`.
> Читай его когда нужны: типы фреймов, Button/EditBox/ScrollFrame/StatusBar, события OnEvent/OnUpdate, SavedVariables, пути к текстурам, типичные ошибки.
---
## Шаблонный аддон «с нуля» (минимальный рабочий пример)
**MyAddon.toc**
```toc
## Interface: 30300
## Title: MyAddon
## SavedVariables: MyAddonDB
MyAddon.lua
```
**MyAddon.lua**
```lua
local ADDON_NAME = "MyAddon"
local f = CreateFrame("Frame", ADDON_NAME.."_Frame", UIParent)
f:SetWidth(250)
f:SetHeight(150)
f:SetPoint("CENTER")
f:SetFrameStrata("DIALOG")
f:SetMovable(true)
f:EnableMouse(true)
f:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = {left=11, right=12, top=12, bottom=11}
})
f:Hide()
-- Заголовок
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOP", f, "TOP", 0, -12)
title:SetText("My Addon")
-- Закрытие
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 4, 4)
close:SetScript("OnClick", function() f:Hide() end)
-- Перетаскивание
f:SetScript("OnMouseDown", function(self, btn)
if btn == "LeftButton" then self:StartMoving() end
end)
f:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)
-- Инициализация
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", function(self, event, name)
if event == "ADDON_LOADED" and name == ADDON_NAME then
MyAddonDB = MyAddonDB or {}
print("|cff00ff00"..ADDON_NAME.."|r загружен. /myaddon")
end
end)
-- Слэш-команда
SLASH_MYADDON1 = "/myaddon"
SlashCmdList["MYADDON"] = function()
if f:IsShown() then f:Hide() else f:Show() end
end
```
+256
View File
@@ -0,0 +1,256 @@
# WoW 3.3.5a — Виджеты, события и Blizzard API
## Создание фреймов через Lua
### CreateFrame
```lua
-- CreateFrame(frameType, name, parent, template)
local f = CreateFrame("Frame", "MyAddon_MainFrame", UIParent)
f:SetWidth(300)
f:SetHeight(200)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
f:SetFrameStrata("DIALOG")
f:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
f:SetBackdropColor(0, 0, 0, 0.8)
```
### Типы фреймов (frameType)
| Тип | Назначение |
|-----|-----------|
| `Frame` | Базовый контейнер |
| `Button` | Кнопка |
| `EditBox` | Поле ввода текста |
| `ScrollFrame` | Прокручиваемая область |
| `StatusBar` | Полоска (HP, мана, XP) |
| `Cooldown` | Кулдаун-свип на иконке |
| `GameTooltip` | Тултип |
| `MessageFrame` | Прокручиваемые сообщения |
| `Model` | 3D-модель |
| `MovieFrame` | Видео |
---
## Текстуры
```lua
local tex = frame:CreateTexture(nil, "ARTWORK")
tex:SetTexture("Interface\\AddOns\\MyAddon\\images\\icon")
tex:SetAllPoints(frame)
tex:SetWidth(32)
tex:SetHeight(32)
tex:SetPoint("CENTER", frame, "CENTER")
tex:SetVertexColor(1, 0.5, 0, 1)
tex:SetAlpha(0.8)
-- Вырезка части текстуры
tex:SetTexCoord(0, 0.5, 0, 0.5)
```
## FontString (текст)
```lua
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
fs:SetPoint("CENTER", frame, "CENTER")
fs:SetText("Привет, Азерот!")
fs:SetTextColor(1, 1, 0, 1)
fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
```
### Стандартные шаблоны шрифтов
| Шаблон | Применение |
|--------|-----------|
| `GameFontNormal` | Обычный текст интерфейса |
| `GameFontNormalLarge` | Заголовки |
| `GameFontNormalSmall` | Мелкий текст |
| `GameFontHighlight` | Подсвеченный |
| `GameFontDisable` | Неактивный (серый) |
| `NumberFontNormal` | Числа |
| `ChatFontNormal` | Чат |
---
## Button
```lua
local btn = CreateFrame("Button", "MyAddon_Btn", frame, "UIPanelButtonTemplate")
btn:SetWidth(80)
btn:SetHeight(22)
btn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 10)
btn:SetText("Нажми")
btn:SetScript("OnClick", function(self)
print("Нажато!")
end)
btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-Button-Up")
btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-Button-Down")
btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
```
### Шаблоны кнопок
| Шаблон | Вид |
|--------|-----|
| `UIPanelButtonTemplate` | Стандартная кнопка интерфейса |
| `UIPanelCloseButton` | Крестик закрытия окна |
| `UIMenuButtonStretchTemplate` | Растягиваемая кнопка меню |
| `SecureActionButtonTemplate` | Для действий в бою (protected) |
---
## ScrollFrame
```lua
local scroll = CreateFrame("ScrollFrame", "MyAddon_Scroll", frame, "UIPanelScrollFrameTemplate")
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 8)
local child = CreateFrame("Frame", "MyAddon_ScrollChild", scroll)
child:SetWidth(scroll:GetWidth())
child:SetHeight(400)
scroll:SetScrollChild(child)
```
## EditBox
```lua
local eb = CreateFrame("EditBox", "MyAddon_Input", frame, "InputBoxTemplate")
eb:SetWidth(200)
eb:SetHeight(20)
eb:SetPoint("CENTER", frame, "CENTER")
eb:SetAutoFocus(false)
eb:SetMaxLetters(255)
eb:SetScript("OnEnterPressed", function(self)
local text = self:GetText()
self:ClearFocus()
end)
```
## StatusBar
```lua
local bar = CreateFrame("StatusBar", nil, frame)
bar:SetWidth(200)
bar:SetHeight(16)
bar:SetPoint("CENTER", frame, "CENTER")
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
bar:SetStatusBarColor(0, 1, 0, 1)
bar:SetMinMaxValues(0, 100)
bar:SetValue(75)
```
---
## Скрипты-события
```lua
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("CHAT_MSG_SAY")
frame:SetScript("OnEvent", function(self, event, ...)
if event == "PLAYER_ENTERING_WORLD" then
-- вход в мир
elseif event == "CHAT_MSG_SAY" then
local msg, author = ...
end
end)
```
### Ключевые события
| Событие | Когда |
|---------|-------|
| `PLAYER_ENTERING_WORLD` | Вход в мир / reload |
| `ADDON_LOADED` | Аддон загружен (arg1 == имя) |
| `PLAYER_LOGIN` | После входа |
| `PLAYER_LOGOUT` | При выходе |
| `VARIABLES_LOADED` | SavedVariables загружены |
| `UI_ERROR_MESSAGE` | Ошибка интерфейса |
### OnUpdate — таймер
```lua
local elapsed_total = 0
frame:SetScript("OnUpdate", function(self, elapsed)
elapsed_total = elapsed_total + elapsed
if elapsed_total >= 1.0 then
elapsed_total = 0
-- действие каждую секунду
end
end)
```
---
## SavedVariables
```lua
-- В TOC: ## SavedVariables: MyAddonDB
frame:RegisterEvent("ADDON_LOADED")
frame:SetScript("OnEvent", function(self, event, addonName)
if event == "ADDON_LOADED" and addonName == "MyAddon" then
MyAddonDB = MyAddonDB or { position = { "CENTER", 0, 0 } }
local p = MyAddonDB.position
self:ClearAllPoints()
self:SetPoint(p[1], UIParent, p[1], p[2], p[3])
end
end)
frame:SetScript("OnMouseUp", function(self)
self:StopMovingOrSizing()
local point, _, _, x, y = self:GetPoint()
MyAddonDB.position = { point, x, y }
end)
```
---
## Полезные Blizzard-утилиты (3.3.5a)
```lua
_G["MyAddon_Frame"]:Show()
IsAddOnLoaded("MyAddon")
UnitName("player")
UnitClass("target")
UnitHealth("player"), UnitHealthMax("player")
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Красный текст|r")
print("Белый текст")
UnitAffectingCombat("player")
```
---
## Часто используемые пути к текстурам Blizzard
```
Interface\DialogFrame\UI-DialogBox-Background
Interface\DialogFrame\UI-DialogBox-Border
Interface\DialogFrame\UI-DialogBox-Gold-Background
Interface\Tooltips\UI-Tooltip-Background
Interface\Tooltips\UI-Tooltip-Border
Interface\Buttons\UI-Panel-Button-Up
Interface\Buttons\UI-Panel-Button-Down
Interface\Icons\INV_Misc_QuestionMark
Interface\TargetingFrame\UI-StatusBar
Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up
```
---
## Типичные ошибки и решения
| Ошибка | Причина | Решение |
|--------|---------|---------|
| `attempt to index global 'X' (nil)` | Фрейм не создан | Проверь порядок файлов в TOC |
| `SetPoint anchor family connection` | Циклические якоря | Переписать цепочку якорей |
| `Script ran too long` | Бесконечный цикл OnUpdate | Early return или счётчик |
| Текстура не отображается | Размер не кратен 2 / >512px | Пересохрани в TGA/BLP |
| `bad argument #1 (string expected)` | nil вместо строки | `if val then` |
| Frame не кликается | Нет `enableMouse="true"` | Добавить атрибут |
Binary file not shown.
Binary file not shown.
+216
View File
@@ -0,0 +1,216 @@
[CmdletBinding()]
param(
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
[string]$ClientPath = '',
[string]$PackagePath = '',
[switch]$Deploy,
[switch]$NativeRenderer
)
function Enable-LargeAddressAware {
param(
[Parameter(Mandatory)]
[string]$Path
)
$bytes = [System.IO.File]::ReadAllBytes($Path)
if ($bytes.Length -lt 64) {
throw "File is too small to be a valid PE executable: $Path"
}
# DOS header: MZ
if ($bytes[0] -ne 0x4D -or $bytes[1] -ne 0x5A) {
throw "DOS MZ signature was not found: $Path"
}
# IMAGE_DOS_HEADER.e_lfanew
$peOffset = [BitConverter]::ToInt32($bytes, 0x3C)
if ($peOffset -lt 0 -or ($peOffset + 24) -gt $bytes.Length) {
throw "Invalid PE header offset in: $Path"
}
# PE\0\0 signature
$peSignature = [BitConverter]::ToUInt32($bytes, $peOffset)
if ($peSignature -ne 0x00004550) {
throw "PE signature was not found: $Path"
}
# IMAGE_FILE_HEADER starts after the 4-byte PE signature.
# Characteristics is at offset 18 inside IMAGE_FILE_HEADER.
$characteristicsOffset = $peOffset + 4 + 18
$characteristics = [BitConverter]::ToUInt16(
$bytes,
$characteristicsOffset
)
$newCharacteristics = [uint16](
$characteristics -bor 0x0020
)
if ($newCharacteristics -ne $characteristics) {
$encoded = [BitConverter]::GetBytes($newCharacteristics)
$bytes[$characteristicsOffset] = $encoded[0]
$bytes[$characteristicsOffset + 1] = $encoded[1]
[System.IO.File]::WriteAllBytes($Path, $bytes)
Write-Host (
"LAA enabled for {0}: 0x{1:X4} -> 0x{2:X4}" -f
$Path,
$characteristics,
$newCharacteristics
) -ForegroundColor Green
}
else {
Write-Host (
"LAA is already enabled for {0}: 0x{1:X4}" -f
$Path,
$characteristics
) -ForegroundColor DarkGreen
}
}
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
if (-not (Test-Path -LiteralPath $cmake)) {
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
if (-not $cmakeCommand) { throw 'CMake 3.20+ was not found.' }
$cmake = $cmakeCommand.Source
}
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
if (-not (Test-Path -LiteralPath $stockExe -PathType Leaf)) {
throw "Stock executable was not found: $stockExe"
}
$expectedStockHash = 'AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8'
$stockHash = (Get-FileHash -LiteralPath $stockExe -Algorithm SHA256).Hash
if ($stockHash -ne $expectedStockHash) {
throw "Wow_Original.exe is not the verified MoonWell 3.3.5a stock executable (SHA-256 $stockHash)."
}
}
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
& $cmake --build $win32BuildDir --config $Configuration --parallel 4
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
$selectedProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
$recoveryProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
$extensionNames = @(
'MoonWell',
'MoonwellCustomization',
'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'
)
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
)
$utils = Join-Path $Destination 'Utils'
New-Item `
-ItemType Directory `
-Path $Destination, $utils `
-Force |
Out-Null
$legacyHost = Join-Path $utils 'WarcraftXLHost.exe'
if (Test-Path -LiteralPath $legacyHost -PathType Leaf) {
Remove-Item -LiteralPath $legacyHost -Force
Write-Host "Removed obsolete WarcraftXL 1.0 host: $legacyHost"
}
$destinationExe = Join-Path $Destination 'Wow.exe'
Copy-Item `
-LiteralPath $stockExe `
-Destination $destinationExe `
-Force
Enable-LargeAddressAware -Path $destinationExe
Copy-Item `
-LiteralPath $warcraftXL `
-Destination (Join-Path $Destination 'WarcraftXL.dll') `
-Force
Copy-Item `
-LiteralPath $selectedProxy `
-Destination (Join-Path $Destination 'd3d9.dll') `
-Force
Copy-Item `
-LiteralPath $recoveryProxy `
-Destination (Join-Path $utils 'd3d9-native.dll') `
-Force
foreach ($extensionName in $extensionNames) {
$extensionSource = Join-Path $win32BuildDir "$Configuration\$extensionName.dll"
$extensionDestination = Join-Path $Destination "Extensions\$extensionName"
New-Item -ItemType Directory -Path $extensionDestination -Force | Out-Null
Copy-Item `
-LiteralPath $extensionSource `
-Destination (Join-Path $extensionDestination "$extensionName.dll") `
-Force
}
}
if ($Deploy) {
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
$ClientPath = 'C:\Program Files (x86)\World of Warcraft'
}
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath"
}
$clientExe = Join-Path $ClientPath 'Wow.exe'
if (Test-Path -LiteralPath $clientExe -PathType Leaf) {
$clientHash = (Get-FileHash -LiteralPath $clientExe -Algorithm SHA256).Hash
if ($clientHash -ne $stockHash) {
$legacyBackup = Join-Path $ClientPath 'Wow.moonwell-patched.backup.exe'
if (-not (Test-Path -LiteralPath $legacyBackup)) {
Copy-Item -LiteralPath $clientExe -Destination $legacyBackup
}
Write-Host "Legacy executable saved as $legacyBackup"
}
}
Install-WarcraftXLArtifacts -Destination $ClientPath
Write-Host "Installed WarcraftXL 1.1 runtime and extensions in $ClientPath"
}
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
Install-WarcraftXLArtifacts -Destination $PackagePath
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
}
Write-Host "WarcraftXL 1.1 build complete (native D3D9 proxy): $win32ArtifactDir"
+7
View File
@@ -20,6 +20,11 @@ IGNORED_TOP_LEVEL_DIRS = {
IGNORED_DIRS_ANYWHERE = {
".git",
".moonwell_launcher",
".vscode"
}
IGNORED_TOP_LEVEL_FILES = {
"Wow.moonwell-patched.backup.exe",
"WarcraftXL_on12.disable",
}
ENV_VAR_NAME = "WOW_HOME"
@@ -42,6 +47,8 @@ def is_ignored(rel_path: Path) -> bool:
return False
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
return True
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
return True
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
+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
}
}
}
+133
View File
@@ -0,0 +1,133 @@
[CmdletBinding()]
param(
[string]$ClientPath = '',
[string]$PackagePath = '',
[ValidateSet('Debug', 'Release')]
[string]$Configuration = 'Release',
[switch]$SkipDataBuild,
[switch]$SkipManifest,
[switch]$NativeRenderer,
[switch]$StopClient,
[switch]$Launch
)
$ErrorActionPreference = 'Stop'
$repoRoot = $PSScriptRoot
function Import-ProjectEnvironment {
$envFile = Join-Path $repoRoot '.env'
if (-not (Test-Path -LiteralPath $envFile -PathType Leaf)) { return }
foreach ($line in Get-Content -LiteralPath $envFile -Encoding UTF8) {
if ($line -notmatch '^\s*([^#][^=]+)=(.*)$') { continue }
$name = $matches[1].Trim()
$value = $matches[2].Trim().Trim('"').Trim("'")
if ($name -and -not (Test-Path "Env:$name")) {
Set-Item -Path "Env:$name" -Value $value
}
}
}
Import-ProjectEnvironment
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
$ClientPath = if ($env:WOW_HOME) {
$env:WOW_HOME
} else {
'C:\Program Files (x86)\World of Warcraft'
}
}
if ([string]::IsNullOrWhiteSpace($PackagePath)) {
$PackagePath = Join-Path $repoRoot 'dist'
}
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
throw "Client directory was not found: $ClientPath"
}
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
})
if ($runningClients.Count) {
if (-not $StopClient) {
throw 'Wow.exe is running. Close the client or repeat deployment with -StopClient.'
}
Write-Host 'Stopping the running WoW client...'
$runningClients | Stop-Process -Force
Start-Sleep -Seconds 2
}
$env:WOW_HOME = $ClientPath
Write-Host "MoonWell client: $ClientPath"
Write-Host "Package staging: $PackagePath"
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
& git -C $repoRoot submodule update --init --recursive
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
if (-not $SkipDataBuild) {
Write-Host '[2/5] Building MPQ patches...'
$tool = Join-Path $repoRoot 'tool\target\release\tool.exe'
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
if ($cargo) {
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
} elseif (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
throw 'Cargo was not found and the MPQ builder is not compiled.'
}
& $tool (Join-Path $repoRoot 'src') $PackagePath
if ($LASTEXITCODE -ne 0) { throw 'MPQ patch build failed.' }
} else {
Write-Host '[2/5] MPQ build skipped.'
}
Write-Host '[3/5] Building and installing WarcraftXL...'
$buildArguments = @{
Configuration = $Configuration
ClientPath = $ClientPath
PackagePath = $PackagePath
Deploy = $true
}
if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
& (Join-Path $repoRoot 'build-warcraftxl.ps1') @buildArguments
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
Write-Host '[4/5] Synchronizing package files...'
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
$robocopyExitCode = $LASTEXITCODE
if ($robocopyExitCode -ge 8) {
throw "Package synchronization failed with robocopy exit code $robocopyExitCode."
}
if (-not $SkipManifest) {
Write-Host '[5/5] Building launcher manifest...'
$python = 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.' }
} else {
Write-Host '[5/5] Manifest generation skipped.'
}
$wowHash = (Get-FileHash -LiteralPath (Join-Path $ClientPath 'Wow.exe') -Algorithm SHA256).Hash
$renderer = 'WarcraftXL 1.1 native D3D9 proxy'
Write-Host ''
Write-Host 'Deployment complete.'
Write-Host " Renderer: $renderer"
Write-Host " Wow.exe SHA-256: $wowHash"
Write-Host " Extensions: $(Join-Path $ClientPath 'Extensions')"
if ($Launch) {
Write-Host 'Launching Wow.exe...'
Start-Process -FilePath (Join-Path $ClientPath 'Wow.exe') -WorkingDirectory $ClientPath
}
# robocopy uses successful non-zero exit codes; do not leak one to callers.
$global:LASTEXITCODE = 0
+39
View File
@@ -0,0 +1,39 @@
# Asset manifest
## Blood Elf Female
- Retail build: `WOW-68887patch12.0.7_Retail`
- Race ID: `10`
- ChrModelID: `20`
- Character model FileDataID: `1100258`
- Option: Demon Hunter Horns
- Retail OptionID: `126`
- Runtime element type: Geoset in an attached collection M2
- Collection model FileDataID: `7760202`
- Collection compatibility: `RETAIL_WITH_WXL`
- Runtime virtual path: `MoonWell\Customization\BloodElfFemale\7760202_be_f.m2`
- Prototype attachment point: `11` (stock head point). Retail collection point `19` is absent from
the 3.3.5 Blood Elf model and cannot safely be force-attached.
| Prototype choice | Retail ChoiceID | Retail ElementID | GeosetID | Compatibility |
|---|---:|---:|---:|---|
| None | 1850 | — | — | RETAIL_WITH_WXL |
| Betrayer | 1851 | 2051 | 2401 | RETAIL_WITH_WXL |
| Beast | 1852 | 2052 | 2402 | RETAIL_WITH_WXL |
| Dreadlord | 1853 | 2053 | 2403 | RETAIL_WITH_WXL |
The source catalog reports two collection textures:
| FileDataID | Internal path | Type | Purpose |
|---:|---|---|---|
| 1277002 | `item\objectcomponents\collections\bloodelf_female_dh_belt.blp` | BLP | Collection material dependency |
| 2764488 | `item\objectcomponents\collections\bloodelffemale_dh_horns.blp` | BLP | Collection material dependency |
Skin files exported with the model are FileDataID `7766030` (base), `7766031`, `7766032`, and
`7766033` (LODs). The MVP requests the base sibling `7760202_be_f00.skin`.
No fallback asset is committed or silently substituted. Classification is `RETAIL_WITH_WXL`, not
`RETAIL_NATIVE`: stock 3.3.5 cannot parse this modern collection without the WarcraftXL modern-M2
extension. If it fails in a real client session, the documented next fallback is a small WotLK M2
mounted at the same virtual path; such a placeholder would be classified `WOTLK_FALLBACK` and must be
recorded here before use.
+12
View File
@@ -0,0 +1,12 @@
# Glue integration follow-up
The runtime prototype intentionally does not alter character creation. WarcraftXL v1.1 exposes typed
Glue/Lua bindings in `game/Glue.hpp` and `game/Script.hpp`, while the existing MoonWell extension shows
the validated `Lua.RegisterFunction` / `Lua.ValidateFunctionPointer` bridge used by build 12340.
A safe follow-up is to register
`ModelFrame:SetMoonwellCustomization(optionID, choiceID)` through that existing bridge, resolve the
Glue model's `CharModelObject`, and pass it to the same `CustomizationState` and renderer used here.
Cleanup must follow Glue model destruction rather than `OnWorldLeave`. This should be implemented only
after the runtime collection attachment has passed interactive visual/lifetime testing; no extra client
offset or duplicate renderer is justified for the current vertical slice.
+92
View File
@@ -0,0 +1,92 @@
# MoonwellCustomization vertical slice
## Scope
`MoonwellCustomization.dll` is a runtime-only WarcraftXL 1.1 extension for WoW 3.3.5a build
12340. It changes only an active Blood Elf Female player. State is kept in memory and is deliberately
separate from rendering; there are no server packets, database changes, AzerothCore changes, or new
Wow.exe offsets.
The data flow is:
`CustomizationOption (126) -> CustomizationChoice -> CustomizationElement (Geoset) -> collection M2`
The explicit prototype state is None plus three retail choices. The renderer owns one independent
collection render context at a time. Reapplying the same choice is a no-op; replacing a choice releases
the old context first. The overlay copies the active character's placement and bone palette without
joining the stock attachment readiness chain. `OnWorldLeave` always releases it and drops all borrowed
player pointers.
## Verified WarcraftXL API
The repository pins `vendor/warcraftxl` to `v1.1.220` (`4895cef`) on the `v1.1` branch. Source code,
not README assumptions, establishes the following behavior:
- `WXL_Query`/`WXL_Load` are the extension entry points; the runtime loads DLLs found below
`Extensions/<name>/`.
- `WXL_Api::UiAddPanel` registers an immediate-mode overlay panel. WarcraftXL v1.1 hard-codes `F9`
as its overlay toggle; press it and select **MoonWell Customization**.
- `world::ActivePlayerGuid` and `world::ResolveObject(..., kTypeMaskPlayer)` find the active player;
`unit::Model` returns its body-model/scene-node binding.
- The upstream typed `SceneNode` and `CharModelObject` views provide owner, race, gender, and root
scene node. The extension adds no offsets.
- `m2::GetRenderCtx` always creates a new scene model; `AttachToScene(..., slot 19, true)` attaches a
collection M2. Core explicitly documents 19 as the collection-M2 attachment point.
- `OnM2SkinFinalize` is the safe pre-finalize window to filter collection geosets. Runtime changes
call `FinalizeSkin` once; a recursion guard prevents its event from re-entering the filter.
- `OnBuildBonePalette` is post-engine and therefore the safe point to copy the parent character bone
palette into the collection model immediately before upload.
- `OnItemSlotChange` and `OnItemSlotClear` schedule a replacement on the next logic tick, after native
equipment work. Native equipment is never overridden.
- `OnWorldLeave` is the cleanup event. Events and UI run on WarcraftXL's main/render paths; no worker
thread calls client model bindings.
## Build
From a Visual Studio 2022 developer environment or PowerShell:
```powershell
.\build-warcraftxl.ps1 -Configuration Release
```
The required artifact is:
`build\warcraftxl-win32\Release\MoonwellCustomization.dll`
The target is Win32. The top-level CMake build compiles the extension together with the existing
WarcraftXL SDK sources; there is no second CMake project.
## Install and run
1. Install the asset files described in `docs/wow-export-assets.md` into a client MPQ.
2. Copy the DLL to
`Extensions\MoonwellCustomization\MoonwellCustomization.dll` under the client directory. The build
script does this automatically when invoked with `-Deploy -ClientPath <client>`.
3. Start the stock build-12340 client through the installed WarcraftXL `d3d9.dll` proxy.
4. Press `F9` to open the WarcraftXL overlay and select **MoonWell Customization**.
5. On Blood Elf Female, press None / Betrayer / Beast / Dreadlord. None must remove the collection;
repeated selections must not stack it. Equip and remove a helmet, then log out and back in.
6. On every other race or gender, the panel reports that the prototype is unsupported and exposes no
choice controls.
Expected log lines are tagged `[MoonwellCustomization]`: `loaded`, `player detected`, race/gender,
apply, model load, attach, detach, unsupported player, and actionable failure text.
## Known limits
- The repository intentionally excludes Blizzard assets. Without the collection M2 and sibling skin,
the client loader can fall back to its placeholder model; that is an asset-install failure, not a
successful visual test.
- Retail collection model 7760202 is a modern MD21 asset. It requires the existing `wxl-modern-m2`,
`wxl-modern-blp`, `wxl-db2`, and FDID extensions in this repository.
- Bone copying assumes the retail Blood Elf Female collection uses the same ordered skeleton as its
parent model. The code clamps to the smaller palette, but a retail build changing bone order would
need an explicit bone map.
- `FinalizeSkin` in WXL 1.1 has small internal re-finalize leaks. The implementation therefore performs
it only on an actual choice change/equipment refresh, never per frame.
- This source-level build proves compilation and packaging. A successful visual result and crash-free
logout still require an interactive client session with the external assets installed.
The next minimal step toward a broader retail-like system is to move the static option table to an
external, versioned catalog and add renderers for Texture and independent M2Attachment elements while
keeping the same `optionID -> choiceID` state boundary.
+42
View File
@@ -0,0 +1,42 @@
# Exporting the Blood Elf Female prototype assets
The local wow.export checkout has no documented headless CLI, so this document does not invent one.
Its MoonWell exporter generated the inspected catalog at:
`moonwell-customization/wotlk-races/catalog.json`
The exact source is retail `WOW-68887patch12.0.7_Retail`. Use wow.export's UI to select that product,
then export the WotLK races customization catalog/asset set. From the generated output, take:
| FileDataID | Exported file / internal path | Type | Why |
|---:|---|---|---|
| 7760202 | `assets/collections/7760202/7760202_be_f.m2` | M2 | Blood Elf Female collection containing Demon Hunter horn geosets |
| 7766030 | `assets/collections/7760202/7760202_be_f00.skin` | SKIN | Base geometry/index profile required by the M2 |
| 1277002 | `item/objectcomponents/collections/bloodelf_female_dh_belt.blp` | BLP | Declared collection texture dependency |
| 2764488 | `item/objectcomponents/collections/bloodelffemale_dh_horns.blp` | BLP | Declared collection texture dependency |
Do not add these files to git. Stage them under the ignored `local-assets/` directory, then put them in
a local MPQ with these virtual names:
```text
MoonWell\Customization\BloodElfFemale\7760202_be_f.m2
MoonWell\Customization\BloodElfFemale\7760202_be_f00.skin
item\objectcomponents\collections\bloodelf_female_dh_belt.blp
item\objectcomponents\collections\bloodelffemale_dh_horns.blp
```
Install that MPQ as a high-priority `Data\patch-*.MPQ` beside the existing MoonWell patches. Keep the
three LOD skins (`7766031``7766033`) locally for later testing, but the current MVP does not request
them explicitly.
To reproduce the DB result without changing wow.export, run the repository's read-only catalog helper:
```powershell
node tools/find_customization_assets.js `
C:\Users\sindo\wow.export\moonwell-customization\wotlk-races\catalog.json `
"Blood Elf" female Horns
```
It must report OptionID `126`, then ChoiceIDs `1851`, `1852`, `1853`. Their skinned-model elements
map geoset type `24` and IDs `1`, `2`, `3` to skin sections `2401`, `2402`, `2403`. The helper reads
already-exported JSON only; it does not access CASC and is not a runtime dependency.
+64
View File
@@ -0,0 +1,64 @@
// Minimal D3D9 forwarding proxy for loading WarcraftXL without editing Wow.exe.
#include <windows.h>
#include <d3d9.h>
namespace
{
using Create9Fn = IDirect3D9* (WINAPI*)(UINT);
using Create9ExFn = HRESULT (WINAPI*)(UINT, IDirect3D9Ex**);
HMODULE g_systemD3D9 = nullptr;
Create9Fn g_create9 = nullptr;
Create9ExFn g_create9Ex = nullptr;
void ResolveSystemD3D9()
{
if (g_systemD3D9) return;
char path[MAX_PATH] = {};
const UINT length = GetSystemDirectoryA(path, MAX_PATH);
if (!length || length >= MAX_PATH - 10) return;
lstrcatA(path, "\\d3d9.dll");
g_systemD3D9 = LoadLibraryA(path);
if (!g_systemD3D9) return;
g_create9 = reinterpret_cast<Create9Fn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9"));
g_create9Ex = reinterpret_cast<Create9ExFn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9Ex"));
}
void LoadWarcraftXL()
{
static bool attempted = false;
if (attempted) return;
attempted = true;
LoadLibraryA("WarcraftXL.dll");
}
}
extern "C" IDirect3D9* WINAPI Direct3DCreate9(UINT sdkVersion)
{
ResolveSystemD3D9();
LoadWarcraftXL();
return g_create9 ? g_create9(sdkVersion) : nullptr;
}
extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** output)
{
ResolveSystemD3D9();
LoadWarcraftXL();
if (g_create9Ex) return g_create9Ex(sdkVersion, output);
if (output) *output = nullptr;
return E_NOINTERFACE;
}
// Legacy compatibility exports retained for the recovery proxy. WarcraftXL 1.1
// uses native D3D9 and ignores the former D3D9On12 bridge.
extern "C" void* WxlD3D12Device() { return nullptr; }
extern "C" void* WxlD3D12Queue() { return nullptr; }
extern "C" void WxlD3D12DrainDebug() {}
extern "C" void WxlSetSsaaFactor(float) {}
extern "C" float WxlGetSsaaFactor() { return 1.0f; }
BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID)
{
return TRUE;
}
+9
View File
@@ -0,0 +1,9 @@
LIBRARY d3d9
EXPORTS
Direct3DCreate9
Direct3DCreate9Ex
WxlD3D12Device
WxlD3D12Queue
WxlD3D12DrainDebug
WxlSetSsaaFactor
WxlGetSsaaFactor
-339
View File
@@ -1,339 +0,0 @@
"""
Wow.exe binary patcher for MoonWell custom features.
Patches:
1. Script_CreateCharacter: reads lua_tonumber(L, 2) as outfitId,
stores to global, passes to CMSG_CHAR_CREATE packet instead of 0.
2. GetCharacterInfo: adds 11th return value — charFlags as a Lua number,
read from struct_offset +0x170 (same field as ghost flag 0x2000).
Lua checks bit 0x40000000 (CHARACTER_FLAG_UNK31) for traitor status.
Usage:
python make_modes_in_exe.py <path_to_Wow.exe>
A backup (Wow.exe.bak) is created before patching.
"""
import struct
import sys
import shutil
from pathlib import Path
# ---------------------------------------------------------------------------
# PE helpers
# ---------------------------------------------------------------------------
def _u16(data, off):
return struct.unpack_from("<H", data, off)[0]
def _u32(data, off):
return struct.unpack_from("<I", data, off)[0]
def parse_pe_sections(data):
if data[0:2] != b"MZ":
raise ValueError("Not a valid PE file (missing MZ)")
pe_off = _u32(data, 0x3C)
if data[pe_off:pe_off + 4] != b"PE\x00\x00":
raise ValueError("Not a valid PE file (missing PE signature)")
coff = pe_off + 4
num_secs = _u16(data, coff + 2)
opt_size = _u16(data, coff + 16)
opt = coff + 20
magic = _u16(data, opt)
if magic == 0x10b:
image_base = _u32(data, opt + 28)
elif magic == 0x20b:
image_base = struct.unpack_from("<Q", data, opt + 24)[0]
else:
raise ValueError(f"Unknown PE magic: 0x{magic:04x}")
sec_table = opt + opt_size
sections = []
for i in range(num_secs):
o = sec_table + i * 40
name = data[o:o + 8].rstrip(b"\x00").decode("ascii", errors="replace")
virt_size = _u32(data, o + 8)
virt_addr = _u32(data, o + 12)
raw_size = _u32(data, o + 16)
raw_off = _u32(data, o + 20)
sections.append((name, virt_addr, virt_size, raw_off, raw_size))
return image_base, sections
def va_to_offset(va, sections, image_base):
rva = va - image_base
for name, va0, vs, ro, _ in sections:
if va0 <= rva < va0 + vs:
return ro + (rva - va0)
raise ValueError(f"VA 0x{va:08x} not in any section")
def offset_to_va(off, sections, image_base):
for name, va0, _, ro, rs in sections:
if ro <= off < ro + rs:
return image_base + va0 + (off - ro)
raise ValueError(f"File offset 0x{off:08x} not in any section")
def find_cave(data, sections, image_base, size, search_from_va):
"""Return VA of the first run of >= size consecutive CC bytes after search_from_va."""
text = next((s for s in sections if s[0] == ".text"), None)
if not text is None:
_, va0, vs, ro, rs = text
else:
raise ValueError("No .text section")
rva_start = max(search_from_va - image_base, va0)
off_start = ro + (rva_start - va0)
off_end = ro + min(vs, rs)
i = off_start
while i <= off_end - size:
if data[i] != 0xcc:
i += 1
continue
j = i
while j < off_end and data[j] == 0xcc:
j += 1
if j - i >= size:
return offset_to_va(i, sections, image_base)
i = j
raise ValueError(f"No {size}-byte code cave found from VA 0x{search_from_va:08x}")
# ---------------------------------------------------------------------------
# Patch 1 & 2: outfitId in CMSG_CHAR_CREATE (fixed addresses)
# ---------------------------------------------------------------------------
_FIXED_PATCHES = [
(
0x004e0c60,
bytes.fromhex(
"558bec568b75086a0156e8f1d23600"
"83c40885c0740d6a006a0156e860d4"
"360083c40c6a006a0156e853d43600"
"50e8edf6ffff83c41033c05e5dc3cc"
"cccccccc"
),
bytes.fromhex(
"558bec568b75086a0256e8c1d33600"
"db1c245883c404a221 42ac006a006a"
"0156e85bd4360050e8f5f6ffff83c4"
"1033c05e5dc38855f08a1521 42ac00"
"8855f4c3"
).replace(b" ", b""), # spaces stripped for readability
"Script_CreateCharacter: read outfitId from Lua arg 2 -> global",
),
(
0x004e042f,
bytes.fromhex("8855f0c645f400"),
bytes.fromhex("e85f08000090 90").replace(b" ", b""),
"Packet builder: CALL helper -> load face + outfitId from global",
),
]
# ---------------------------------------------------------------------------
# Patch 3: GetCharacterInfo — add 11th return value (charFlags as number)
# ---------------------------------------------------------------------------
#
# Assembly context (FUN_004e3170 epilogue, valid branch):
#
# 004e31cd 56 PUSH ESI ← saves caller's ESI
# 004e31ce 8b f0 MOV ESI, EAX ← ESI = char struct ptr
# ... [function body — ESI used throughout for char struct] ...
# 004e32e5 8b 8e 70010000 MOV ECX, [ESI+0x170] ← charFlags (ghost flag check)
# ...
# 004e332d 83 c4 2c ADD ESP, 0x2c ← clean accumulated args ← PATCH HERE
# 004e3330 5e POP ESI ← restores caller's ESI (ESI valid until here)
# 004e3331 b8 0a 00 00 00 MOV EAX, 0xa (dead after patch)
# 004e3336 5f POP EDI ← JMP back target
# 004e3337 8b e5 MOV ESP, EBP
# 004e3339 5d POP EBP
# 004e333a c3 RET
#
# KEY: ESI = char struct ptr up to and including ADD ESP,0x2c (004e332d).
# After POP ESI (004e3330) it is caller's ESI — NOT char struct.
# Previous approach: CALL cave at 004e3331 (after POP ESI) -> ESI already wrong.
# Fix: JMP cave at 004e332d (before POP ESI) -> ESI still valid in cave.
#
# New approach: patch 5 bytes at 004e332d with JMP cave.
# Old bytes: 83 c4 2c (ADD ESP,0x2c) + 5e (POP ESI) + b8 (MOV EAX first byte)
# New bytes: e9 XX XX XX XX JMP cave
# The 4 dead bytes at 004e3332-004e3335 are unreachable.
#
# Cave (30 bytes, found automatically via CC scan over entire .text):
# 83 ec 08 SUB ESP, 8 ; make room for double
# db 86 70 01 00 00 FILD [ESI+0x170] ; load charFlags into FPU (ESI valid here!)
# dd 1c 24 FSTP [ESP] ; store as double
# 57 PUSH EDI ; lua_State* L
# e8 XX XX XX XX CALL FUN_0084e2a0 ; lua_pushnumber(L, charFlags)
# 83 c4 38 ADD ESP, 0x38 ; merged: own 0xc + stolen 0x2c
# 5e POP ESI ; stolen: restore caller's ESI
# 6a 0b PUSH 0xb ; \ EAX = 11 in 3 bytes
# 58 POP EAX ; / (vs MOV EAX,0xb = 5 bytes)
# e9 XX XX XX XX JMP 004e3336 ; continue epilogue (POP EDI ...)
#
# Lua code reads info[11] and checks bit 0x40000000 (CHARACTER_FLAG_UNK31 = traitor).
# charFlags confirmed at [ESI+0x170]: same field used for ghost flag (AND ECX,0x2000).
_CHARINFO_PATCH_VA = 0x004e332d # ADD ESP,0x2c (5 bytes including POP ESI + MOV EAX[0])
_CHARINFO_JMPBACK_VA = 0x004e3336 # POP EDI — resume epilogue after cave
_CHARINFO_CAVE_SIZE = 30
_CHARINFO_CAVE_SEARCH_FROM = 0x00401000 # search entire .text for first 35-byte CC block
_LUA_PUSHNUMBER_VA = 0x0084e2a0 # lua_pushnumber
def _build_charinfo_patches(data, sections, image_base):
"""
Build the dynamic GetCharacterInfo patches.
Returns list of (va, old_bytes, new_bytes, description), [] if already applied,
or None on unrecoverable error.
"""
patch_off = va_to_offset(_CHARINFO_PATCH_VA, sections, image_base)
current = bytes(data[patch_off:patch_off + 5])
# Already patched with new JMP approach?
if current[0:1] == b"\xe9":
return []
# Verify first 4 bytes look like the epilogue (ADD ESP,0x2c + POP ESI).
# Accept both unpatched (b8 at [4]) and old-CALL-patched (e8 at [4]) states.
if current[0:4] != bytes.fromhex("83c42c5e"):
print(f"[WARN] 0x{_CHARINFO_PATCH_VA:08x}: unexpected bytes {current.hex(' ')}")
print(" Expected ADD ESP,0x2c + POP ESI at this address.")
print(" Restore Wow.exe.bak and re-run.")
return None
old_b = current # exact 5 bytes currently in file (b8 or e8 at index 4)
# Find a 35-byte code cave in .text
cave_va = find_cave(data, sections, image_base,
_CHARINFO_CAVE_SIZE, _CHARINFO_CAVE_SEARCH_FROM)
# Cave layout offsets (30 bytes total):
# 0 SUB ESP,8 (3)
# 3 FILD [ESI+0x170] (6)
# 9 FSTP [ESP] (3)
# 12 PUSH EDI (1)
# 13 CALL lua_pushnumber (5) — next instr at 18
# 18 ADD ESP, 0x38 (3) merged: own 0xc + stolen 0x2c
# 21 POP ESI (1) stolen
# 22 PUSH 0xb (2) \ shorter than MOV EAX,0xb (5 bytes)
# 24 POP EAX (1) /
# 25 JMP 004e3336 (5) — next instr at cave_va+30
call_rel = _LUA_PUSHNUMBER_VA - (cave_va + 18)
jmp_back_rel = _CHARINFO_JMPBACK_VA - (cave_va + 30)
cave_bytes = (
bytes.fromhex("83ec08") # SUB ESP, 8 (3)
+ bytes.fromhex("db8670010000") # FILD [ESI+0x170] (6)
+ bytes.fromhex("dd1c24") # FSTP [ESP] (3)
+ bytes.fromhex("57") # PUSH EDI (1)
+ b"\xe8" + struct.pack("<i", call_rel) # CALL lua_pushnumber (5)
+ bytes.fromhex("83c438") # ADD ESP, 0x38 (0xc+0x2c) (3)
+ bytes.fromhex("5e") # POP ESI [stolen] (1)
+ bytes.fromhex("6a0b") # PUSH 0xb (2)
+ bytes.fromhex("58") # POP EAX -> EAX=11 (1)
+ b"\xe9" + struct.pack("<i", jmp_back_rel) # JMP 004e3336 (5)
)
assert len(cave_bytes) == _CHARINFO_CAVE_SIZE, \
f"Cave size {len(cave_bytes)} != {_CHARINFO_CAVE_SIZE}"
# Patch: JMP cave (5 bytes at 004e332d)
jmp_rel = cave_va - (_CHARINFO_PATCH_VA + 5)
patch_new = b"\xe9" + struct.pack("<i", jmp_rel)
return [
(
cave_va,
bytes([0xcc] * _CHARINFO_CAVE_SIZE),
cave_bytes,
f"GetCharacterInfo cave @ 0x{cave_va:08x}: push charFlags, stolen epilogue",
),
(
_CHARINFO_PATCH_VA,
old_b,
patch_new,
"GetCharacterInfo: JMP to cave before POP ESI (ESI=char struct), return 11",
),
]
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
if len(sys.argv) != 2:
print(f"Usage: python {Path(sys.argv[0]).name} <path_to_Wow.exe>")
sys.exit(1)
exe_path = Path(sys.argv[1])
if not exe_path.exists():
print(f"Error: file not found: {exe_path}")
sys.exit(1)
data = bytearray(exe_path.read_bytes())
image_base, sections = parse_pe_sections(data)
print(f"Image base: 0x{image_base:08x}")
print(f"Sections: {len(sections)}")
for n, va, vs, ro, rs in sections:
print(f" {n:8s} VA=0x{va:08x} VSize=0x{vs:08x} Raw=0x{ro:08x} RSize=0x{rs:08x}")
print()
charinfo_patches = _build_charinfo_patches(data, sections, image_base)
if charinfo_patches is None:
sys.exit(1)
all_patches = list(_FIXED_PATCHES) + charinfo_patches
# Verify all first
ok = True
for va, old_b, new_b, desc in all_patches:
off = va_to_offset(va, sections, image_base)
cur = bytes(data[off:off + len(old_b)])
if cur == new_b:
print(f"[SKIP] 0x{va:08x}: already patched — {desc}")
elif cur == old_b:
print(f"[ OK ] 0x{va:08x}: verified — {desc}")
else:
print(f"[FAIL] 0x{va:08x}: {desc}")
print(f" Expected: {old_b.hex(' ')}")
print(f" Found: {cur.hex(' ')}")
ok = False
if not ok:
print("\nByte mismatch — aborting. Is this the correct Wow.exe (3.3.5a build 12340)?")
sys.exit(1)
# Backup
bak = exe_path.with_suffix(".exe.bak")
if not bak.exists():
shutil.copy2(exe_path, bak)
print(f"\nBackup: {bak}")
else:
print(f"\nBackup already exists: {bak}")
# Apply
applied = 0
for va, old_b, new_b, desc in all_patches:
off = va_to_offset(va, sections, image_base)
cur = bytes(data[off:off + len(old_b)])
if cur == new_b:
continue
data[off:off + len(old_b)] = new_b
applied += 1
print(f"[PATCH] 0x{va:08x}: {desc}")
if applied == 0:
print("\nNothing to patch.")
else:
exe_path.write_bytes(data)
print(f"\n{applied} patch(es) written to {exe_path}")
print("Done!")
if __name__ == "__main__":
main()
@@ -0,0 +1,70 @@
#pragma once
#include <array>
#include <cstdint>
namespace moonwell::customization
{
enum class ElementType : uint8_t
{
None,
M2Attachment,
Geoset,
Texture,
};
struct CustomizationElement
{
ElementType type;
uint32_t geosetId;
const char* modelPath;
};
struct CustomizationChoice
{
uint32_t id;
const char* name;
std::array<CustomizationElement, 1> elements;
};
struct CustomizationOption
{
uint32_t id;
const char* name;
std::array<CustomizationChoice, 4> choices;
};
inline constexpr uint32_t kBloodElfRace = 10;
inline constexpr uint32_t kFemaleGender = 1;
inline constexpr uint32_t kCollectionFileDataId = 7760202;
// Stock 3.3.5 Blood Elf models have no retail collection point 19.
// Use the native head attachment point; forcing absent point 19 makes
// the parent's drawable gate fail and hides the whole character.
inline constexpr uint32_t kCollectionAttachmentSlot = 11;
inline constexpr char kCollectionPath[] =
"MoonWell\\Customization\\BloodElfFemale\\7760202_be_f.m2";
// WOTLK_FALLBACK: native 3.3.5 head components for Blood Elf Female.
// The retail collection needs a real bone map to the differently ordered
// 3.3.5 skeleton, so the working vertical slice uses native components.
inline constexpr CustomizationOption kHornsOption = {
126,
"Head Decorations (WotLK fallback)",
{{
{1850, "None", {{{ElementType::None, 0, nullptr}}}},
{1851, "Blindfold", {{{ElementType::M2Attachment, 0,
"ITEM\\OBJECTCOMPONENTS\\HEAD\\Helm_Blindfold_A_01_BeF.m2"}}}},
{1852, "Circlet I", {{{ElementType::M2Attachment, 0,
"ITEM\\OBJECTCOMPONENTS\\HEAD\\Helm_Circlet_A_01_BeF.m2"}}}},
{1853, "Circlet II", {{{ElementType::M2Attachment, 0,
"ITEM\\OBJECTCOMPONENTS\\HEAD\\Helm_Circlet_B_01_BeF.m2"}}}},
}},
};
inline const CustomizationChoice* FindChoice(uint32_t id)
{
for (const auto& choice : kHornsOption.choices)
if (choice.id == id) return &choice;
return nullptr;
}
}
@@ -0,0 +1,436 @@
#include "CustomizationRenderer.hpp"
#include <wxl/PluginApi.h>
#include "engine/events/Event.hpp"
#include "game/M2.hpp"
#include "game/World.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <limits>
#include <windows.h>
namespace moonwell::customization
{
namespace m2 = wxl::game::m2;
namespace world = wxl::game::world;
namespace sm2 = wxl::structure::m2;
namespace
{
bool IsReadableRange(const void* address, size_t size)
{
if (!address || !size) return false;
const uintptr_t begin = reinterpret_cast<uintptr_t>(address);
if (size > std::numeric_limits<uintptr_t>::max() - begin) return false;
const uintptr_t end = begin + size;
uintptr_t cursor = begin;
while (cursor < end)
{
MEMORY_BASIC_INFORMATION memory{};
if (!VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof memory))
return false;
if (memory.State != MEM_COMMIT ||
(memory.Protect & (PAGE_NOACCESS | PAGE_GUARD)) != 0)
return false;
const uintptr_t regionEnd = reinterpret_cast<uintptr_t>(memory.BaseAddress) +
memory.RegionSize;
if (regionEnd <= cursor) return false;
cursor = std::min(regionEnd, end);
}
return true;
}
}
void CustomizationRenderer::SetError(const char* message)
{
lastError_ = message;
api_->Log(WXL_LOG_ERROR, "MoonwellCustomization", "failed: %s", message);
}
bool CustomizationRenderer::DetectPlayer()
{
playerDetected_ = false;
supported_ = false;
charModelObject_ = nullptr;
parentRenderContext_ = nullptr;
race_ = gender_ = 0;
const auto guid = world::ActivePlayerGuid();
void* player = guid ? world::ResolveObject(guid, world::kTypeMaskPlayer) : nullptr;
if (!player)
{
lastError_ = "Waiting for active player object";
return false;
}
// The native equipment events are the public WXL seam that supplies
// the actual CharModelObject. Unit::Model is not useful for identity:
// it exposes a different model layer. Match the root instance's world
// transform to the active player's authoritative world position so an
// event for another visible character cannot be adopted accidentally.
auto* cmo = static_cast<m2::off::CharModelObject*>(observedCharModelObject_);
if (!cmo || !cmo->sceneNode)
{
lastError_ = "Waiting for active player CharModelObject event";
return false;
}
auto* root = static_cast<m2::off::M2Instance*>(cmo->sceneNode);
float playerPosition[3]{};
world::UnitPosition(player, playerPosition);
const float dxRow = root->placement[12] - playerPosition[0];
const float dyRow = root->placement[13] - playerPosition[1];
const float dzRow = root->placement[14] - playerPosition[2];
const float rowDistanceSq = dxRow * dxRow + dyRow * dyRow + dzRow * dzRow;
const float dxColumn = root->placement[3] - playerPosition[0];
const float dyColumn = root->placement[7] - playerPosition[1];
const float dzColumn = root->placement[11] - playerPosition[2];
const float columnDistanceSq = dxColumn * dxColumn + dyColumn * dyColumn + dzColumn * dzColumn;
if (std::min(rowDistanceSq, columnDistanceSq) > 9.0f)
{
lastError_ = "Waiting for matching active player CharModelObject";
return false;
}
if (cmo->raceId == 0 ||
cmo->raceId > 11 || cmo->genderId > 1)
{
lastError_ = "Active player CharModelObject is not ready";
return false;
}
charModelObject_ = cmo;
parentRenderContext_ = cmo->sceneNode;
race_ = cmo->raceId;
gender_ = cmo->genderId;
playerDetected_ = true;
supported_ = race_ == kBloodElfRace && gender_ == kFemaleGender;
lastError_ = supported_ ? "Ready" : "Prototype supports Blood Elf Female only.";
api_->Log(WXL_LOG_INFO, "MoonwellCustomization", "player detected");
api_->Log(WXL_LOG_INFO, "MoonwellCustomization", "race=%u gender=%u", race_, gender_);
if (!supported_)
api_->Log(WXL_LOG_WARN, "MoonwellCustomization", "unsupported player race");
return true;
}
void CustomizationRenderer::OnWorldEnter()
{
inWorld_ = true;
detectDelay_ = 0.0f;
DetectPlayer();
}
void CustomizationRenderer::OnWorldLeave()
{
inWorld_ = false;
Detach();
observedCharModelObject_ = nullptr;
charModelObject_ = nullptr;
parentRenderContext_ = nullptr;
playerDetected_ = supported_ = false;
race_ = gender_ = 0;
detectDelay_ = 0.0f;
refreshRequested_ = false;
geosetApplied_ = false;
attached_ = false;
finalizeInProgress_ = false;
lastError_ = "Outside world";
}
void CustomizationRenderer::OnUpdate(float dt)
{
// OnWorldEnter is an edge notification and can be missed when the
// extension is loaded after a world transition has already begun.
// Reconcile against the authoritative active-player GUID so the
// renderer also recovers after late loads and unusual glue flows.
const auto activePlayerGuid = world::ActivePlayerGuid();
if (!activePlayerGuid)
{
if (inWorld_) OnWorldLeave();
return;
}
if (!inWorld_)
{
inWorld_ = true;
detectDelay_ = 0.0f;
lastError_ = "Waiting for active player model";
}
if (!playerDetected_)
{
detectDelay_ -= dt;
if (detectDelay_ <= 0.0f)
{
detectDelay_ = 0.25f;
DetectPlayer();
}
return;
}
if (refreshRequested_)
{
refreshRequested_ = false;
const uint32_t choiceId = CurrentChoice();
if (choiceId != 0 && choiceId != kHornsOption.choices[0].id)
{
Detach();
if (const auto* choice = FindChoice(choiceId)) Attach(*choice);
}
}
if (attachmentRenderContext_ && attachmentModel_ && !geosetApplied_)
{
const auto* choice = FindChoice(CurrentChoice());
if (choice && choice->elements[0].type == ElementType::Geoset)
FilterGeoset(attachmentModel_, choice->elements[0].geosetId, true);
}
// CreateSceneModel loads asynchronously. Attaching it before the stock
// readiness gate succeeds makes the parent character non-drawable too.
// Keep it unattached while loading, then join the stock head point only after the
// child itself reports drawable.
if (attachmentRenderContext_ && parentRenderContext_)
{
auto* child = static_cast<m2::off::M2Instance*>(attachmentRenderContext_);
auto* parent = static_cast<m2::off::M2Instance*>(parentRenderContext_);
const auto* choice = FindChoice(CurrentChoice());
const bool collectionGeoset = choice &&
choice->elements[0].type == ElementType::Geoset;
if (collectionGeoset && !attached_)
{
std::memcpy(child->placement, parent->placement, sizeof child->placement);
child->viewDistSq = parent->viewDistSq;
child->alphaBase = parent->alphaBase;
}
if (geosetApplied_ && !attached_)
{
const int drawable = wxl::game::Native<m2::off::M2_IsDrawableFn>(
m2::off::kIsDrawable)(child, nullptr, 0, 0);
if (drawable)
{
m2::AttachToScene(parentRenderContext_, child,
kCollectionAttachmentSlot, false);
if (reinterpret_cast<void*>(static_cast<uintptr_t>(child->parent)) ==
parentRenderContext_)
{
attached_ = true;
lastError_ = "Ready";
api_->Log(WXL_LOG_INFO, "MoonwellCustomization",
"attached ready overlay slot=%u", kCollectionAttachmentSlot);
}
else
{
lastError_ = "Native head attachment was rejected";
}
}
else
{
lastError_ = "Collection loaded; waiting for drawable readiness";
}
}
}
}
void CustomizationRenderer::OnEquipmentChanged(void* charModelObject)
{
if (!playerDetected_ && charModelObject)
{
observedCharModelObject_ = charModelObject;
DetectPlayer();
}
// Item events are emitted around native equipment maintenance. Rebuild on
// the next logic tick so native slot work is complete before slot 19 is touched.
if (charModelObject == charModelObject_)
{
const auto* choice = FindChoice(CurrentChoice());
if (choice && choice->elements[0].type != ElementType::None)
refreshRequested_ = true;
}
}
void CustomizationRenderer::OnSkinFinalize(void* model)
{
if (!model || model != attachmentModel_ || finalizeInProgress_) return;
const auto* choice = FindChoice(CurrentChoice());
if (choice && choice->elements[0].type == ElementType::Geoset)
FilterGeoset(model, choice->elements[0].geosetId, false);
}
void CustomizationRenderer::OnBuildBonePalette(void* renderContext)
{
if (!renderContext || renderContext != attachmentRenderContext_) return;
const auto* choice = FindChoice(CurrentChoice());
if (!choice || choice->elements[0].type != ElementType::Geoset) return;
auto* child = static_cast<m2::off::M2Instance*>(renderContext);
auto* parent = static_cast<m2::off::M2Instance*>(parentRenderContext_);
if (!parent || !child->bonePalettePtr || !parent->bonePalettePtr)
return;
m2::M2Model childModel(reinterpret_cast<void*>(child->model));
m2::M2Model parentModel(reinterpret_cast<void*>(parent->model));
if (!childModel || !parentModel || !childModel.GetHeader() || !parentModel.GetHeader()) return;
const uint32_t count = std::min(childModel.GetHeader()->bones.count,
parentModel.GetHeader()->bones.count);
if (count)
std::memcpy(reinterpret_cast<void*>(child->bonePalettePtr),
reinterpret_cast<const void*>(parent->bonePalettePtr),
static_cast<size_t>(count) * m2::off::kBonePaletteStride);
}
bool CustomizationRenderer::FilterGeoset(void* rawModel, uint32_t geosetId, bool finalize)
{
m2::M2Model model(rawModel);
auto* skin = model ? model.GetSkin() : nullptr;
if (!IsReadableRange(skin, sizeof *skin) || !skin->indices ||
!skin->submeshes || !skin->indexCount || !skin->submeshCount)
return false;
// A newly created async scene model can expose the skin object before
// its offset-valued arrays have been relocated to pointers. Do not
// touch those arrays until both complete ranges are readable.
constexpr uint32_t kMaxIndexCount = 10u * 1024u * 1024u;
constexpr uint32_t kMaxSubmeshCount = 100u * 1024u;
if (skin->indexCount > kMaxIndexCount || skin->submeshCount > kMaxSubmeshCount ||
!IsReadableRange(skin->indices,
static_cast<size_t>(skin->indexCount) * sizeof(uint16_t)) ||
!IsReadableRange(skin->submeshes,
static_cast<size_t>(skin->submeshCount) * sizeof(sm2::M2SkinSection)))
return false;
bool found = false;
for (uint32_t i = 0; i < skin->submeshCount; ++i)
{
const sm2::M2SkinSection& section = skin->submeshes[i];
const uint32_t start = (uint32_t(section.level) << 16) | section.indexStart;
const uint32_t count = section.indexCount;
if (start > skin->indexCount || count > skin->indexCount - start) continue;
if (section.skinSectionId == geosetId)
{
found = true;
continue;
}
std::fill_n(skin->indices + start, count, uint16_t(0));
}
if (!found)
{
SetError("requested customization geoset is absent from collection skin");
return false;
}
if (finalize)
{
finalizeInProgress_ = true;
model.FinalizeSkin();
finalizeInProgress_ = false;
}
attachmentModel_ = rawModel;
geosetApplied_ = true;
return true;
}
bool CustomizationRenderer::Attach(const CustomizationChoice& choice)
{
if (!supported_ || !charModelObject_ || !parentRenderContext_)
{
SetError("supported player model is unavailable");
return false;
}
const auto& element = choice.elements[0];
if (!element.modelPath)
{
SetError("customization model path is absent");
return false;
}
api_->Log(WXL_LOG_INFO, "MoonwellCustomization", "loading model %s", element.modelPath);
auto* parent = static_cast<m2::off::M2Instance*>(parentRenderContext_);
void* scene = reinterpret_cast<void*>(static_cast<uintptr_t>(parent->scene));
if (!scene)
{
SetError("active player scene is unavailable");
return false;
}
// CreateSceneModel's native receiver is the owning scene, not the
// CharModelObject. Passing the CMO makes the loader interpret cmo+4
// as the scene's model table and faults inside 0x0081C4F0.
void* child = m2::GetRenderCtx(scene, const_cast<char*>(element.modelPath));
if (!child)
{
SetError("CreateSceneModel returned null");
return false;
}
attachmentRenderContext_ = child;
auto* instance = static_cast<m2::off::M2Instance*>(child);
attachmentModel_ = reinterpret_cast<void*>(instance->model);
geosetApplied_ = element.type != ElementType::Geoset;
attached_ = false;
// Native WotLK head components must retain their own local transform;
// AttachToScene composes it with the parent's head point. Collection
// geosets use a full-character coordinate system and are seeded below.
if (element.type == ElementType::Geoset)
{
auto* parentInstance = static_cast<m2::off::M2Instance*>(parentRenderContext_);
std::memcpy(instance->placement, parentInstance->placement, sizeof instance->placement);
instance->viewDistSq = parentInstance->viewDistSq;
instance->alphaBase = parentInstance->alphaBase;
}
api_->Log(WXL_LOG_INFO, "MoonwellCustomization", "created overlay geoset=%u",
choice.elements[0].geosetId);
lastError_ = "Loading collection model";
return true;
}
void CustomizationRenderer::Detach()
{
if (!attachmentRenderContext_) return;
if (attached_ && parentRenderContext_)
m2::DetachSlot(parentRenderContext_, kCollectionAttachmentSlot);
else
m2::ReleaseRenderCtx(attachmentRenderContext_);
attachmentRenderContext_ = nullptr;
attachmentModel_ = nullptr;
geosetApplied_ = false;
attached_ = false;
finalizeInProgress_ = false;
api_->Log(WXL_LOG_INFO, "MoonwellCustomization", "detached");
}
bool CustomizationRenderer::Apply(uint32_t optionId, uint32_t choiceId)
{
const auto* choice = FindChoice(choiceId);
if (!choice || optionId != kHornsOption.id) return false;
if (!supported_)
{
lastError_ = "Prototype supports Blood Elf Female only.";
return false;
}
const bool isNone = choice->elements[0].type == ElementType::None;
if (CurrentChoice() == choiceId && (isNone || attachmentRenderContext_)) return true;
api_->Log(WXL_LOG_INFO, "MoonwellCustomization", "apply option=%u choice=%u",
optionId, choiceId);
Detach();
state_.SetChoice(optionId, choiceId);
if (isNone) return true;
return Attach(*choice);
}
}
@@ -0,0 +1,57 @@
#pragma once
#include "CustomizationState.hpp"
#include <cstdint>
struct WXL_Api;
namespace moonwell::customization
{
class CustomizationRenderer
{
public:
explicit CustomizationRenderer(const WXL_Api* api) : api_(api) {}
void OnWorldEnter();
void OnWorldLeave();
void OnUpdate(float dt);
void OnEquipmentChanged(void* charModelObject);
void OnSkinFinalize(void* model);
void OnBuildBonePalette(void* renderContext);
bool Apply(uint32_t optionId, uint32_t choiceId);
bool PlayerDetected() const { return playerDetected_; }
bool Supported() const { return supported_; }
uint32_t Race() const { return race_; }
uint32_t Gender() const { return gender_; }
uint32_t CurrentChoice() const { return state_.Choice(kHornsOption.id); }
const char* LastError() const { return lastError_; }
private:
bool DetectPlayer();
bool Attach(const CustomizationChoice& choice);
void Detach();
bool FilterGeoset(void* model, uint32_t geosetId, bool finalize);
void SetError(const char* message);
const WXL_Api* api_ = nullptr;
CustomizationState state_;
void* observedCharModelObject_ = nullptr;
void* charModelObject_ = nullptr;
void* parentRenderContext_ = nullptr;
void* attachmentRenderContext_ = nullptr;
void* attachmentModel_ = nullptr;
uint32_t race_ = 0;
uint32_t gender_ = 0;
float detectDelay_ = 0.0f;
bool inWorld_ = false;
bool playerDetected_ = false;
bool supported_ = false;
bool refreshRequested_ = false;
bool geosetApplied_ = false;
bool attached_ = false;
bool finalizeInProgress_ = false;
const char* lastError_ = "Waiting for player";
};
}
@@ -0,0 +1,29 @@
#pragma once
#include "CustomizationData.hpp"
#include <cstdint>
namespace moonwell::customization
{
class CustomizationState
{
public:
uint32_t Choice(uint32_t optionId) const
{
return optionId == kHornsOption.id ? hornsChoice_ : 0;
}
bool SetChoice(uint32_t optionId, uint32_t choiceId)
{
if (optionId != kHornsOption.id || !FindChoice(choiceId)) return false;
hornsChoice_ = choiceId;
return true;
}
void Clear() { hornsChoice_ = 0; }
private:
uint32_t hornsChoice_ = 0;
};
}
@@ -0,0 +1,115 @@
#include "CustomizationRenderer.hpp"
#include <wxl/PluginApi.h>
#include "engine/events/Event.hpp"
#include <cstdint>
#include <cstdio>
#include <memory>
namespace moonwell::customization
{
namespace ev = wxl::events;
const WXL_Api* g_api = nullptr;
std::unique_ptr<CustomizationRenderer> g_renderer;
void __cdecl OnWorldEnter(void*, const void*) { g_renderer->OnWorldEnter(); }
void __cdecl OnWorldLeave(void*, const void*) { g_renderer->OnWorldLeave(); }
void __cdecl OnUpdate(void*, const void* raw)
{
const auto* args = static_cast<const ev::UpdateArgs*>(raw);
g_renderer->OnUpdate(args ? args->dt : 0.0f);
}
void __cdecl OnItemSlotChange(void*, const void* raw)
{
const auto* args = static_cast<const ev::ItemSlotChangeArgs*>(raw);
if (args) g_renderer->OnEquipmentChanged(args->charModelObj);
}
void __cdecl OnItemSlotClear(void*, const void* raw)
{
const auto* args = static_cast<const ev::ItemSlotClearArgs*>(raw);
if (args) g_renderer->OnEquipmentChanged(args->charModelObj);
}
void __cdecl OnSkinFinalize(void*, const void* raw)
{
const auto* args = static_cast<const ev::M2SkinFinalizeArgs*>(raw);
if (args) g_renderer->OnSkinFinalize(args->model);
}
void __cdecl OnBuildBonePalette(void*, const void* raw)
{
const auto* args = static_cast<const ev::BuildBonePaletteArgs*>(raw);
if (args) g_renderer->OnBuildBonePalette(args->renderCtx);
}
void DrawPanel(void*)
{
char line[128]{};
if (!g_renderer->PlayerDetected())
{
g_api->UiText("Player: detecting...");
g_api->UiText(g_renderer->LastError());
return;
}
std::snprintf(line, sizeof(line), "Race ID: %u%s", g_renderer->Race(),
g_renderer->Race() == kBloodElfRace ? " (Blood Elf)" : "");
g_api->UiText(line);
std::snprintf(line, sizeof(line), "Gender ID: %u%s", g_renderer->Gender(),
g_renderer->Gender() == kFemaleGender ? " (Female)" : "");
g_api->UiText(line);
g_api->UiSeparator();
if (!g_renderer->Supported())
{
g_api->UiText("Prototype supports Blood Elf Female only.");
return;
}
g_api->UiText("Option: Head Decorations (WotLK fallback)");
for (const auto& choice : kHornsOption.choices)
{
std::snprintf(line, sizeof(line), "%s%s", choice.name,
g_renderer->CurrentChoice() == choice.id ? " [selected]" : "");
if (g_api->UiButton(line))
g_renderer->Apply(kHornsOption.id, choice.id);
}
g_api->UiSeparator();
g_api->UiText(g_renderer->LastError());
}
}
const WXL_PluginInfo* __cdecl WXL_Query(void)
{
static const WXL_PluginInfo info = {
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonwellCustomization", 1, WXL_CLIENT_BUILD,
};
return &info;
}
int __cdecl WXL_Load(const WXL_Api* api)
{
using namespace moonwell::customization;
if (!api || api->apiVersion != WXL_API_VERSION ||
api->structSize < sizeof(WXL_Api) || !api->Subscribe || !api->UiAddPanel)
return 0;
g_api = api;
g_renderer = std::make_unique<CustomizationRenderer>(api);
api->Subscribe(uint32_t(ev::Event::OnWorldEnter), &OnWorldEnter, nullptr);
api->Subscribe(uint32_t(ev::Event::OnWorldLeave), &OnWorldLeave, nullptr);
api->Subscribe(uint32_t(ev::Event::OnUpdate), &OnUpdate, nullptr);
api->Subscribe(uint32_t(ev::Event::OnItemSlotChange), &OnItemSlotChange, nullptr);
api->Subscribe(uint32_t(ev::Event::OnItemSlotClear), &OnItemSlotClear, nullptr);
api->Subscribe(uint32_t(ev::Event::OnM2SkinFinalize), &OnSkinFinalize, nullptr);
api->Subscribe(uint32_t(ev::Event::OnBuildBonePalette), &OnBuildBonePalette, nullptr);
api->UiAddPanel("MoonWell Customization", &DrawPanel, nullptr);
api->Log(WXL_LOG_INFO, "MoonwellCustomization", "loaded");
return 1;
}
+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;
}
+517
View File
@@ -0,0 +1,517 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// MoonWell compatibility module for WarcraftXL.
//
// The stock 3.3.5a executable stays untouched on disk. These narrowly scoped
// changes are verified and applied to the process image during WarcraftXL's
// boot phase, before GlueXML is loaded.
#include "game/Script.hpp"
#include "wxl/PluginApi.h"
#include "SpellOverrides.hpp"
#include <windows.h>
#include <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 = void(__fastcall*)(void* self, void* edx, const void* projection);
GxSetProjectionFn g_nextSetProjection = nullptr;
// FrameXML's stock SetCreature only accepts a creature entry and waits
// for its client cache record. Encounter Journal already has the exact
// CreatureDisplayInfo ID, so accept it as an optional second argument.
constexpr uintptr_t kSetCreature = 0x00597960;
constexpr uintptr_t kGetCurrentModelFrame = 0x004A81B0;
constexpr uintptr_t kModelFrameTypeToken = 0x00C0E4D4;
constexpr uintptr_t kApplyCreatureCacheRecord = 0x00597700;
using SetCreatureFn = int(__cdecl*)(void* state);
using ApplyCreatureCacheRecordFn = void(__fastcall*)(void* frame, void* edx,
const void* cacheRecord);
SetCreatureFn g_nextSetCreature = nullptr;
alignas(4) std::array<uint32_t, 10> g_encounterJournalCreatureRecord{};
void* GetCurrentModelFrame(void* state, uint32_t typeToken)
{
// FrameScript_GetObject takes the type token on the stack, but the
// 3.3.5 client also expects lua_State in ESI (an internal calling
// convention not expressible with a regular C function pointer).
void* frame = nullptr;
__asm
{
mov esi, state
push typeToken
mov eax, kGetCurrentModelFrame
call eax
add esp, 4
mov frame, eax
}
return frame;
}
int __cdecl SetCreatureDisplayInfoHook(void* state)
{
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0;
if (!state || !wxl::game::script::IsNumber(state, 3))
return result;
const double requestedDisplayInfo = wxl::game::script::ToNumber(state, 3);
if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
return result;
const auto displayInfo = static_cast<uint32_t>(requestedDisplayInfo);
__try
{
const uint32_t typeToken = *reinterpret_cast<const uint32_t*>(kModelFrameTypeToken);
if (!typeToken)
return result;
void* frame = GetCurrentModelFrame(state, typeToken);
if (!frame)
return result;
// Both the initial loader and the later character-appearance
// pass read displayInfo at +0x24. Keep this record alive and
// attach it to the frame: character models use it asynchronously
// to resolve CreatureDisplayInfoExtra, baked skin and equipment.
g_encounterJournalCreatureRecord.fill(0);
g_encounterJournalCreatureRecord[9] = displayInfo;
*reinterpret_cast<const void**>(static_cast<uint8_t*>(frame) + 0x378) =
g_encounterJournalCreatureRecord.data();
reinterpret_cast<ApplyCreatureCacheRecordFn>(kApplyCreatureCacheRecord)(
frame, nullptr, g_encounterJournalCreatureRecord.data());
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
WLOG_ERROR("moonwell: SetCreature displayInfo bridge failed for %u", displayInfo);
}
return result;
}
void InstallEncounterJournalModelPreview()
{
if (!Hook("MoonWellSetCreatureDisplayInfo", kSetCreature,
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
{
WLOG_ERROR("moonwell: encounter journal model hook installation failed");
return;
}
WLOG_INFO("moonwell: encounter journal displayInfo model bridge installed");
}
struct CameraTransition
{
float zoom = 1.0f;
float verticalOffset = 0.0f;
float startZoom = 1.0f;
float startVerticalOffset = 0.0f;
float targetZoom = 1.0f;
float targetVerticalOffset = 0.0f;
DWORD startedAt = 0;
DWORD duration = 0;
} g_characterCreateCamera;
void UpdateCharacterCreateCamera(DWORD now)
{
auto& camera = g_characterCreateCamera;
if (!camera.duration)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
return;
}
const DWORD elapsed = now - camera.startedAt;
float t = static_cast<float>(elapsed) / static_cast<float>(camera.duration);
if (t >= 1.0f)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
camera.duration = 0;
return;
}
t = t * t * (3.0f - 2.0f * t);
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
camera.verticalOffset = camera.startVerticalOffset
+ (camera.targetVerticalOffset - camera.startVerticalOffset) * t;
}
int __cdecl SetCharacterCreateCamera(void* state)
{
const bool enabled = state && wxl::game::script::IsNumber(state, 1)
&& wxl::game::script::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::game::script::IsNumber(state, 2)
? static_cast<float>(wxl::game::script::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::game::script::IsNumber(state, 3)
? static_cast<float>(wxl::game::script::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::game::script::IsNumber(state, 4)
? wxl::game::script::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
const DWORD now = GetTickCount();
UpdateCharacterCreateCamera(now);
auto& camera = g_characterCreateCamera;
camera.startZoom = camera.zoom;
camera.startVerticalOffset = camera.verticalOffset;
camera.targetZoom = enabled && faceZoom > 1.0f ? faceZoom : 1.0f;
camera.targetVerticalOffset = enabled ? faceVerticalOffset : 0.0f;
camera.startedAt = now;
camera.duration = static_cast<DWORD>(requestedDuration);
UpdateCharacterCreateCamera(now);
return 0;
}
void __fastcall CharacterCreateProjectionHook(void* self, void* edx, const void* projection)
{
if (!g_nextSetProjection)
return;
UpdateCharacterCreateCamera(GetTickCount());
const auto& camera = g_characterCreateCamera;
if (projection && (camera.zoom != 1.0f || camera.verticalOffset != 0.0f))
{
const float* source = static_cast<const float*>(projection);
if (source[11] > 0.5f)
{
float adjusted[16];
std::memcpy(adjusted, source, sizeof(adjusted));
adjusted[0] *= camera.zoom;
adjusted[5] *= camera.zoom;
adjusted[9] += camera.verticalOffset;
g_nextSetProjection(self, edx, adjusted);
return;
}
}
g_nextSetProjection(self, edx, projection);
}
void InstallCharacterCreateCamera()
{
constexpr uintptr_t kGxDeviceVTable = 0x00A2E718;
constexpr unsigned kGxSetProjectionSlot = 0xA0 / 4;
void** vtable = reinterpret_cast<void**>(kGxDeviceVTable);
void** slot = &vtable[kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
if (!g_nextSetProjection || !PatchMemory(slot, &replacement, sizeof(replacement)))
{
g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed");
return;
}
WLOG_INFO("moonwell: smooth character-create camera installed");
}
int __cdecl SetLoginCharacterFlags(void* state)
{
uint32_t flags = 0;
if (state && wxl::game::script::IsNumber(state, 1))
flags = static_cast<uint32_t>(wxl::game::script::ToNumber(state, 1));
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1;
}
int __cdecl IsTraitor(void* state)
{
wxl::game::script::PushBoolean(state, g_loginCharacterIsTraitor);
return 1;
}
struct Patch
{
const char* name;
uintptr_t address;
const uint8_t* expected;
const uint8_t* replacement;
size_t size;
};
template <size_t N>
bool Apply(const char* name, uintptr_t address,
const std::array<uint8_t, N>& expected,
const std::array<uint8_t, N>& replacement)
{
const auto* current = reinterpret_cast<const uint8_t*>(address);
if (std::memcmp(current, replacement.data(), N) == 0)
return true;
if (std::memcmp(current, expected.data(), N) != 0)
{
WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required",
name, reinterpret_cast<void*>(address));
return false;
}
if (!PatchMemory(reinterpret_cast<void*>(address), replacement.data(), N))
{
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address));
return false;
}
WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast<void*>(address));
return true;
}
bool InstallGlueUnlock()
{
// Same stock-client checks formerly changed in the distributed Wow.exe.
// They allow custom GlueXML while keeping WarcraftXL's callback validator intact.
bool ok = true;
ok &= Apply("glue archive override", 0x005F4DBF,
std::array<uint8_t, 1>{0x74}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback gate", 0x00816625,
std::array<uint8_t, 1>{0x75}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback result A", 0x0081663F,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback result B", 0x00816695,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback compare", 0x00816746,
std::array<uint8_t, 1>{0x7F}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback return", 0x0081675F,
std::array<uint8_t, 7>{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D},
std::array<uint8_t, 7>{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED});
return ok;
}
bool InstallCharacterModePacket()
{
// CreateCharacter(name, modeId): capture Lua argument 2 and place it in
// the final CMSG_CHAR_CREATE byte that is zero in the stock client.
constexpr std::array<uint8_t, 64> expectedCreate = {
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00,
0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4,
0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00,
0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3,
0xCC,0xCC,0xCC,0xCC,0xCC
};
constexpr std::array<uint8_t, 64> replacementCreate = {
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00,
0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A,
0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4,
0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC,
0x00,0x88,0x55,0xF4,0xC3
};
constexpr std::array<uint8_t, 7> expectedPacket =
{0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00};
constexpr std::array<uint8_t, 7> replacementPacket =
{0xE8,0x5F,0x08,0x00,0x00,0x90,0x90};
return Apply("CreateCharacter mode argument", 0x004E0C60,
expectedCreate, replacementCreate)
&& Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F,
expectedPacket, replacementPacket);
}
bool InstallCharacterFlags()
{
// GetCharacterInfo(index) gains return value 11: the character flags
// from CharacterInfo+0x170. The glue code uses bit 0x40000000.
constexpr uintptr_t patchAddress = 0x004E332D;
constexpr uintptr_t returnAddress = 0x004E3336;
constexpr uintptr_t luaPushNumber = 0x0084E2A0;
constexpr std::array<uint8_t, 5> expected = {0x83,0xC4,0x2C,0x5E,0xB8};
if (*reinterpret_cast<const uint8_t*>(patchAddress) == 0xE9)
return true;
if (std::memcmp(reinterpret_cast<const void*>(patchAddress), expected.data(), expected.size()) != 0)
{
WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p",
reinterpret_cast<void*>(patchAddress));
return false;
}
auto* cave = static_cast<uint8_t*>(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!cave)
{
WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError());
return false;
}
const uint8_t prefix[] = {
0x83,0xEC,0x08, // sub esp, 8
0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h]
0xDD,0x1C,0x24, // fstp qword ptr [esp]
0x57, // push edi (lua_State*)
0xE8,0,0,0,0, // call lua_pushnumber
0x83,0xC4,0x38, // cleanup own args + stolen add esp,2Ch
0x5E, // stolen pop esi
0x6A,0x0B,0x58, // eax = 11
0xE9,0,0,0,0 // jump to stock epilogue
};
static_assert(sizeof(prefix) == 30);
std::memcpy(cave, prefix, sizeof(prefix));
const auto callRel = static_cast<int32_t>(luaPushNumber -
(reinterpret_cast<uintptr_t>(cave) + 18));
const auto backRel = static_cast<int32_t>(returnAddress -
(reinterpret_cast<uintptr_t>(cave) + 30));
std::memcpy(cave + 14, &callRel, sizeof(callRel));
std::memcpy(cave + 26, &backRel, sizeof(backRel));
FlushInstructionCache(GetCurrentProcess(), cave, 30);
std::array<uint8_t, 5> jump{0xE9,0,0,0,0};
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
if (!PatchMemory(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
{
VirtualFree(cave, 0, MEM_RELEASE);
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
return false;
}
WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11");
return true;
}
void InstallBoot()
{
const bool glue = InstallGlueUnlock();
const bool create = InstallCharacterModePacket();
const bool flags = InstallCharacterFlags();
if (!glue || !create || !flags)
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
else
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
}
using RegisterFunctionFn = void(__cdecl*)(const char*, wxl::game::script::Function);
using ValidateCallbackFn = void(__cdecl*)(uintptr_t);
using GetContextFn = void*(__cdecl*)();
RegisterFunctionFn g_nextRegisterFunction = nullptr;
ValidateCallbackFn g_nextValidateCallback = nullptr;
void* g_registeredState = nullptr;
bool g_registeringMoonWell = false;
bool IsMoonWellCallback(uintptr_t callback)
{
return callback == reinterpret_cast<uintptr_t>(&SetLoginCharacterFlags)
|| callback == reinterpret_cast<uintptr_t>(&IsTraitor)
|| callback == reinterpret_cast<uintptr_t>(&SetCharacterCreateCamera);
}
void __cdecl ValidateCallbackHook(uintptr_t callback)
{
if (!IsMoonWellCallback(callback) && g_nextValidateCallback)
g_nextValidateCallback(callback);
}
void RegisterLuaFunctionsForCurrentState()
{
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;
}
+99
View File
@@ -1,3 +1,9 @@
param(
[Parameter(Mandatory=$false)]
[ValidateSet("production", "ptr", "local")]
[string]$Env = "local"
)
# Stop on errors
$ErrorActionPreference = "Stop"
@@ -31,11 +37,78 @@ 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"
$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"
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)) {
@@ -59,6 +132,17 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
exit 1
}
# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
Stop-WowRuntime -ClientPath $WOW_HOME
Write-Host "Building and packaging WarcraftXL..."
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
-PackagePath $DIST_DIR -Deploy
if ($LASTEXITCODE -ne 0) {
Write-Error "WarcraftXL build/deploy failed!"
exit 1
}
# --- Sync dist/ -> WOW_HOME
Write-Host "Syncing dist/ -> WOW_HOME..."
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
@@ -70,6 +154,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)
@@ -676,6 +676,82 @@ function CharacterCreate_UpdateGameModeVisibility()
end
end
-- The stock creation scene frames each race at a different height. Projection
-- zoom therefore needs a per-race vertical target to keep the face in the same
-- part of the screen. Values are indexed by GetSelectedSex(): 2 male, 3 female.
local CHARACTER_CREATE_CAMERA_PROFILES = {
HUMAN = { [2] = -0.28, [3] = -0.20 },
ORC = { [2] = -0.50, [3] = -0.30 },
DWARF = { [2] = 0.30, [3] = 0.38 },
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
SCOURGE = { [2] = 0.18, [3] = 0.27 },
TAUREN = { [2] = -0.78, [3] = -0.62 },
GNOME = { [2] = 0.64, [3] = 0.72 },
TROLL = { [2] = -0.70, [3] = -0.55 },
BLOODELF = { [2] = -0.30, [3] = -0.22 },
DRAENEI = { [2] = -0.62, [3] = -0.55 },
};
local function CharacterCreate_GetCameraProfile()
local _, raceFile = GetNameForRace();
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
local sex = GetSelectedSex();
return 2.0, raceProfile and raceProfile[sex] or -0.20;
end
local function CharacterCreate_UpdateCamera(immediate)
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
local mode = showFace and 1 or 0;
if CharacterCreate.cameraMode == mode and not immediate then
return;
end
CharacterCreate.cameraMode = mode;
CharacterCreate:SetCamera(0);
if MoonWellSetCharacterCreateCamera then
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
end
end
-- The stock creation scene frames each race at a different height. Projection
-- zoom therefore needs a per-race vertical target to keep the face in frame.
-- Values are indexed by GetSelectedSex(): 2 male, 3 female.
local CHARACTER_CREATE_CAMERA_PROFILES = {
HUMAN = { [2] = -0.28, [3] = -0.20 },
ORC = { [2] = -0.50, [3] = -0.30 },
DWARF = { [2] = 0.30, [3] = 0.38 },
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
SCOURGE = { [2] = 0.18, [3] = 0.27 },
TAUREN = { [2] = -0.78, [3] = -0.62 },
GNOME = { [2] = 0.64, [3] = 0.72 },
TROLL = { [2] = -0.70, [3] = -0.55 },
BLOODELF = { [2] = -0.30, [3] = -0.22 },
DRAENEI = { [2] = -0.62, [3] = -0.55 },
};
local function CharacterCreate_GetCameraProfile()
local _, raceFile = GetNameForRace();
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
local sex = GetSelectedSex();
return 2.0, raceProfile and raceProfile[sex] or -0.20;
end
local function CharacterCreate_UpdateCamera(immediate)
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
local mode = showFace and 1 or 0;
if CharacterCreate.cameraMode == mode and not immediate then
return;
end
CharacterCreate.cameraMode = mode;
CharacterCreate:SetCamera(0);
if MoonWellSetCharacterCreateCamera then
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
end
end
local function CharacterCreate_GetEnteredName()
local text = "";
@@ -701,6 +777,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
CharacterCreate.gameModeSelectionMode = false;
end
CharacterCreate_UpdateCamera();
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
if showGameModes then
@@ -778,7 +856,7 @@ function CharacterCreate_OnLoad(self)
self:SetSequence(0)
self:SetCamera(0)
SetCharCustomizeFrame("CharacterCreate")
CharacterCreate.numRaces = 0
CharacterCreate.selectedRace = 0
CharacterCreate.numClasses = 0
@@ -820,7 +898,7 @@ function CharacterCreate_OnLoad(self)
AllianceLogoFrame:SetSize(100, 100)
AllianceLogoFrame:SetPoint("TOPLEFT", -16, 16)
AllianceLogoFrame:EnableMouse(true)
AllianceLogoFrame:SetScript("OnEnter", function(self)
AllianceTooltip.title:SetText(ALLIANCE)
AllianceTooltip.description:SetText(FACTION_ALLIANCE_DESCRIPTION)
@@ -829,7 +907,7 @@ function CharacterCreate_OnLoad(self)
AllianceTooltip:SetPoint("TOPLEFT", self, "BOTTOMLEFT", 90, 60)
AllianceTooltip:Show()
end)
AllianceLogoFrame:SetScript("OnLeave", function(self)
AllianceTooltip:Hide()
end)
@@ -850,7 +928,7 @@ function CharacterCreate_OnLoad(self)
HordeLogoFrame:SetSize(100, 100)
HordeLogoFrame:SetPoint("TOPRIGHT", 16, 16)
HordeLogoFrame:EnableMouse(true)
HordeLogoFrame:SetScript("OnEnter", function(self)
HordeTooltip.title:SetText(HORDE)
HordeTooltip.description:SetText(FACTION_HORDE_DESCRIPTION)
@@ -859,7 +937,7 @@ function CharacterCreate_OnLoad(self)
HordeTooltip:SetPoint("TOPRIGHT", self, "BOTTOMRIGHT", -90, 60)
HordeTooltip:Show()
end)
HordeLogoFrame:SetScript("OnLeave", function(self)
HordeTooltip:Hide()
end)
@@ -873,7 +951,7 @@ function CharacterCreate_SetupCustomButtons()
{button = CharCreateRandomizeButton, width = 36, height = 36},
{button = CharacterCreateRandomName, width = 30, height = 30}
}
local texturePath = "Interface\\Glues\\CharacterCreate\\charactercreate"
for _, btnInfo in ipairs(buttons) do
@@ -943,6 +1021,7 @@ function CharacterCreate_TogglePersonalization()
CharCreatePersonalizeButton:Show();
CharCreateOkayButton:Hide();
CharacterCreate_UpdateGameModeVisibility();
CharacterCreate_UpdateCamera();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
@@ -961,7 +1040,7 @@ end)
function CharacterCreate_OnShow()
CharacterCreate.personalizationMode = false;
for i=1, MAX_CLASSES_PER_RACE, 1 do
local button = _G["CharacterCreateClassButton"..i];
button:Enable();
@@ -985,7 +1064,7 @@ function CharacterCreate_OnShow()
CharacterCreate.personalizationMode = false;
CharacterCreate.gameModeSelectionMode = false;
CharCreateOkayButton:Hide();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
end
@@ -1001,7 +1080,7 @@ function CharacterCreate_OnShow()
CharacterChangeFixup();
CharacterCreate_CreateGenderButtonTextures();
CharacterCreate_UpdateButtonCheckedStates();
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
hideScheduled = true
HideNameEditFrame:Show()
@@ -1013,7 +1092,7 @@ end
function CharacterCreate_OnHide()
PAID_SERVICE_CHARACTER_ID = nil;
PAID_SERVICE_TYPE = nil;
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
for button, tooltip in pairs(raceTooltips) do
if tooltip then tooltip:Hide() end
@@ -1072,11 +1151,11 @@ function CharacterCreateEnumerateRaces(...)
local raceID = index
local faction = _G.GetFactionForRaceID(raceID)
local borderColor
if faction == "Alliance" then
borderColor = {0.0, 0.4, 1.0}
else
borderColor = {1.0, 0.0, 0.0}
borderColor = {1.0, 0.0, 0.0}
end
if not button.staticTexture then
@@ -1114,10 +1193,10 @@ function CharacterCreateEnumerateRaces(...)
if not button.texturesMoved then
button:SetScript("OnMouseDown", nil);
button:SetScript("OnMouseUp", nil);
button.texturesMoved = true;
end
button:Show();
if ( select(i+2, ...) == 1 ) then
button.enable = true;
@@ -1139,7 +1218,7 @@ function CharacterCreateEnumerateRaces(...)
tooltip:Hide()
end
end)
button:SetScript("OnMouseDown", function(self, clickedButton)
if self:IsEnabled() and clickedButton == "RightButton" then
local tooltip = GetOrCreateRaceTooltip(self)
@@ -1205,7 +1284,7 @@ function CharacterCreateEnumerateClasses(...)
button.nameFrame = CreateFrame("Frame", nil, button);
button.nameFrame:SetSize(112, 40);
button.nameFrame:SetPoint("TOP", button, "BOTTOM", 0, -5);
button.nameFrame.text = button.nameFrame:CreateFontString(nil, "OVERLAY");
button.nameFrame.text:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE");
button.nameFrame.text:SetPoint("CENTER", 0, 0);
@@ -1220,10 +1299,10 @@ function CharacterCreateEnumerateClasses(...)
if not button.texturesMoved then
button:SetScript("OnMouseDown", nil);
button:SetScript("OnMouseUp", nil);
button.texturesMoved = true;
end
button:Show();
local className = select(i, ...);
@@ -1270,7 +1349,7 @@ function CharacterCreateEnumerateClasses(...)
tooltip:Hide()
end
end)
button:SetScript("OnMouseDown", function(self, clickedButton)
if self:IsEnabled() and clickedButton == "RightButton" then
local tooltip = GetOrCreateClassTooltip(self)
@@ -1315,14 +1394,14 @@ function CharacterCreate_CreateGenderButtonTextures()
maleButton.staticTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
maleButton.staticTexture:SetSize(86, 86);
maleButton.staticTexture:SetPoint("CENTER", 0, 0);
maleButton.highlightTexture = maleButton:CreateTexture(maleButton:GetName().."HighlightTexture", "HIGHLIGHT");
maleButton.highlightTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
maleButton.highlightTexture:SetAlpha(0.5);
maleButton.highlightTexture:SetBlendMode("ADD");
maleButton.highlightTexture:SetSize(86, 86);
maleButton.highlightTexture:SetPoint("CENTER", 0, 0);
maleButton.checkedTexture = maleButton:CreateTexture(maleButton:GetName().."CheckedTexture", "OVERLAY");
maleButton.checkedTexture:SetDrawLayer("OVERLAY", 7);
maleButton.checkedTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorderRace_H");
@@ -1341,14 +1420,14 @@ function CharacterCreate_CreateGenderButtonTextures()
femaleButton.staticTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
femaleButton.staticTexture:SetSize(86, 86);
femaleButton.staticTexture:SetPoint("CENTER", 0, 0);
femaleButton.highlightTexture = femaleButton:CreateTexture(femaleButton:GetName().."HighlightTexture", "HIGHLIGHT");
femaleButton.highlightTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorder_F1");
femaleButton.highlightTexture:SetAlpha(0.5);
femaleButton.highlightTexture:SetBlendMode("ADD");
femaleButton.highlightTexture:SetSize(86, 86);
femaleButton.highlightTexture:SetPoint("CENTER", 0, 0);
femaleButton.checkedTexture = femaleButton:CreateTexture(femaleButton:GetName().."CheckedTexture", "OVERLAY");
femaleButton.checkedTexture:SetDrawLayer("OVERLAY", 7);
femaleButton.checkedTexture:SetTexture("Interface\\Glues\\CharacterCreate\\IconBorderRace_H");
@@ -1439,7 +1518,7 @@ function SetCharacterClass(id)
end
CharacterCreate_UpdateButtonCheckedStates();
local className, classFileName, _, tank, healer, damage = GetSelectedClass();
local abilityIndex = 0;
local tempText = _G["CLASS_INFO_"..classFileName..abilityIndex];
@@ -1452,7 +1531,7 @@ function SetCharacterClass(id)
local coords = CLASS_ICON_TCOORDS[classFileName];
CharacterCreateClassIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
CharacterCreateClassLabel:SetText(className);
CharacterCreateClassRolesText:SetText(abilityText);
CharacterCreateClassRolesText:SetText(abilityText);
CharacterCreateClassText:SetText(GetFlavorText("CLASS_"..strupper(classFileName), GetSelectedSex()).."|n|n");
CharacterCreateClassScrollFrameScrollBar:SetValue(0);
end
@@ -1488,7 +1567,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
local maleButton = CharacterCreateGenderButtonMale;
local femaleButton = CharacterCreateGenderButtonFemale;
if maleButton and maleButton.checkedTexture then
if maleButton:GetChecked() then
maleButton.checkedTexture:Show();
@@ -1496,7 +1575,7 @@ function CharacterCreate_UpdateButtonCheckedStates()
maleButton.checkedTexture:Hide();
end
end
if femaleButton and femaleButton.checkedTexture then
if femaleButton:GetChecked() then
femaleButton.checkedTexture:Show();
@@ -1518,6 +1597,7 @@ end
function CharacterCreate_UpdateModel(self)
UpdateCustomizationScene();
CharacterCreate_UpdateCamera();
self:AdvanceTime();
end
@@ -1599,7 +1679,7 @@ function CharacterRace_OnClick(self, id)
SetCharacterClass(classIndex);
CharacterCreate_UpdateHairCustomization();
CharacterChangeFixup();
CharacterCreate_UpdateButtonCheckedStates();
@@ -1624,7 +1704,7 @@ function SetCharacterGender(sex)
CharacterCreateEnumerateRaces(GetAvailableRaces());
CharacterCreateEnumerateClasses(GetAvailableClasses());
SetCharacterRace(GetSelectedRace());
local _,_,classIndex = GetSelectedClass();
if ( PAID_SERVICE_TYPE ) then
classIndex = PaidChange_GetCurrentClassIndex();
@@ -1638,15 +1718,16 @@ function SetCharacterGender(sex)
fileString = strupper(fileString);
local coords = RACE_ICON_TCOORDS[fileString.."_"..gender];
CharacterCreateRaceIcon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
CharacterChangeFixup();
CharacterCreate_UpdateButtonCheckedStates();
end
function CharacterCreate_ResetState()
function CharacterCreate_ResetState(immediate)
CharacterCreate.personalizationMode = false;
CharacterCreate.gameModeSelectionMode = false;
CharacterCreate_UpdateCamera(immediate);
CharacterCreateRaceButtonsContainer:Show();
CharacterCreateClassButtonsContainer:Show();
CharacterCreateGenderButtonsContainer:Show();
@@ -1664,7 +1745,7 @@ function CharacterCreate_ResetState()
CharCreatePersonalizeButton:Show();
CharCreateOkayButton:Hide();
CharacterCreate_UpdateGameModeVisibility();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
end
@@ -1710,7 +1791,7 @@ end
function CharacterCreate_UpdateHairCustomization()
CharacterCustomizationButtonFrame3Text:SetText(_G["HAIR_"..GetHairCustomization().."_STYLE"]);
CharacterCustomizationButtonFrame4Text:SetText(_G["HAIR_"..GetHairCustomization().."_COLOR"]);
CharacterCustomizationButtonFrame5Text:SetText(_G["FACIAL_HAIR_"..GetFacialHairCustomization()]);
CharacterCustomizationButtonFrame5Text:SetText(_G["FACIAL_HAIR_"..GetFacialHairCustomization()]);
end
function SetButtonDesaturated(button, desaturated, r, g, b)
@@ -1732,7 +1813,7 @@ function SetButtonDesaturated(button, desaturated, r, g, b)
g = 0.5;
b = 0.5;
end
icon:SetVertexColor(r, g, b);
end
@@ -1139,6 +1139,12 @@ end
function CharacterSelect_EnterWorld()
PlaySound("gsCharacterSelectionEnterWorld");
StopGlueAmbience();
-- Persist the server-provided game-mode bit across the GlueXML -> FrameXML
-- Lua-state transition. WarcraftXL owns the value for this Wow.exe process.
if ( type(MoonWellSetLoginCharacterFlags) == "function" ) then
local info = { GetCharacterInfo(CharacterSelect.selectedIndex) };
MoonWellSetLoginCharacterFlags(info[11] or 0);
end
EnterWorld();
end
@@ -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)
@@ -0,0 +1,241 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local pairs, assert, type = pairs, assert, type
local min, max, floor = math.min, math.max, math.floor
-- 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
]]
--------------------------
-- Scroll Frame --
--------------------------
do
local Type = "ScrollFrame"
local Version = 9
local function OnAcquire(self)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self.status = nil
-- do SetScroll after niling status, but before clearing localstatus
-- so the scroll value isnt populated back into status, but not kept in localstatus either
self:SetScroll(0)
for k in pairs(self.localstatus) do
self.localstatus[k] = nil
end
self.scrollframe:SetPoint("BOTTOMRIGHT",self.frame,"BOTTOMRIGHT",0,0)
self.scrollbar:Hide()
self.scrollBarShown = nil
self.content.height, self.content.width = nil, nil
end
local function SetScroll(self, value)
local status = self.status or self.localstatus
local viewheight = self.scrollframe:GetHeight()
local height = self.content:GetHeight()
local offset
if viewheight > height then
offset = 0
else
offset = floor((height - viewheight) / 1000.0 * value)
end
self.content:ClearAllPoints()
self.content:SetPoint("TOPLEFT", self.scrollframe, "TOPLEFT", 0, offset)
self.content:SetPoint("TOPRIGHT", self.scrollframe, "TOPRIGHT", 0, offset)
status.offset = offset
status.scrollvalue = value
end
local function MoveScroll(self, value)
local status = self.status or self.localstatus
local height, viewheight = self.scrollframe:GetHeight(), self.content:GetHeight()
if height > viewheight then
self.scrollbar:Hide()
else
self.scrollbar:Show()
local diff = height - viewheight
local delta = 1
if value < 0 then
delta = -1
end
self.scrollbar:SetValue(min(max(status.scrollvalue + delta*(1000/(diff/45)),0), 1000))
end
end
local function FixScroll(self)
if self.updateLock then return end
self.updateLock = true
local status = self.status or self.localstatus
local height, viewheight = self.scrollframe:GetHeight(), self.content:GetHeight()
local offset = status.offset or 0
local curvalue = self.scrollbar:GetValue()
if viewheight < height then
if self.scrollBarShown then
self.scrollBarShown = nil
self.scrollbar:Hide()
self.scrollbar:SetValue(0)
self.scrollframe:SetPoint("BOTTOMRIGHT",self.frame,"BOTTOMRIGHT",0,0)
self:DoLayout()
end
else
if not self.scrollBarShown then
self.scrollBarShown = true
self.scrollbar:Show()
self.scrollframe:SetPoint("BOTTOMRIGHT", self.frame,"BOTTOMRIGHT",-20,0)
self:DoLayout()
end
local value = (offset / (viewheight - height) * 1000)
if value > 1000 then value = 1000 end
self.scrollbar:SetValue(value)
self:SetScroll(value)
if value < 1000 then
self.content:ClearAllPoints()
self.content:SetPoint("TOPLEFT", self.scrollframe, "TOPLEFT", 0, offset)
self.content:SetPoint("TOPRIGHT", self.scrollframe, "TOPRIGHT", 0, offset)
status.offset = offset
end
end
self.updateLock = nil
end
local function OnMouseWheel(this, value)
this.obj:MoveScroll(value)
end
local function OnScrollValueChanged(this, value)
this.obj:SetScroll(value)
end
local function FixScrollOnUpdate(this)
this:SetScript("OnUpdate", nil)
this.obj:FixScroll()
end
local function OnSizeChanged(this)
this:SetScript("OnUpdate", FixScrollOnUpdate)
end
local function LayoutFinished(self, width, height)
self.content:SetHeight(height or 0 + 20)
self.scrollframe:SetScript("OnUpdate", FixScrollOnUpdate)
end
-- called to set an external table to store status in
local function SetStatusTable(self, status)
assert(type(status) == "table")
self.status = status
if not status.scrollvalue then
status.scrollvalue = 0
end
end
local function OnWidthSet(self, width)
local content = self.content
content.width = width
end
local function OnHeightSet(self, height)
local content = self.content
content.height = height
end
local function Constructor()
local frame = CreateFrame("Frame", nil, UIParent)
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.MoveScroll = MoveScroll
self.FixScroll = FixScroll
self.SetScroll = SetScroll
self.LayoutFinished = LayoutFinished
self.SetStatusTable = SetStatusTable
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
self.localstatus = {}
self.frame = frame
frame.obj = self
--Container Support
local scrollframe = CreateFrame("ScrollFrame", nil, frame)
scrollframe.obj = self
scrollframe:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
scrollframe:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
scrollframe:EnableMouseWheel(true)
scrollframe:SetScript("OnMouseWheel", OnMouseWheel)
scrollframe:SetScript("OnSizeChanged", OnSizeChanged)
self.scrollframe = scrollframe
local content = CreateFrame("Frame", nil, scrollframe)
content.obj = self
content:SetPoint("TOPLEFT", scrollframe, "TOPLEFT", 0, 0)
content:SetPoint("TOPRIGHT", scrollframe, "TOPRIGHT", 0, 0)
content:SetHeight(400)
self.content = content
scrollframe:SetScrollChild(content)
local num = AceGUI:GetNextWidgetNum(Type)
local name = ("AceConfigDialogScrollFrame%dScrollBar"):format(num)
local scrollbar = CreateFrame("Slider", name, scrollframe, "UIPanelScrollBarTemplate")
scrollbar.obj = self
scrollbar:SetPoint("TOPLEFT", scrollframe, "TOPRIGHT", 4, -16)
scrollbar:SetPoint("BOTTOMLEFT", scrollframe, "BOTTOMRIGHT", 4, 16)
scrollbar:SetScript("OnValueChanged", OnScrollValueChanged)
scrollbar:SetMinMaxValues(0, 1000)
scrollbar:SetValueStep(1)
scrollbar:SetValue(0)
scrollbar:SetWidth(16)
scrollbar:Hide()
self.scrollbar = scrollbar
local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
scrollbg:SetAllPoints(scrollbar)
scrollbg:SetTexture(0, 0, 0, 0.4)
self.localstatus.scrollvalue = 0
--self:FixScroll()
AceGUI:RegisterAsContainer(self)
--AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,99 @@
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
]]
--------------------------
-- Simple Group --
--------------------------
--[[
This is a simple grouping container, no selection, no borders
It will resize automatically to the height of the controls added to it
]]
do
local Type = "SimpleGroup"
local Version = 5
local function OnAcquire(self)
self:SetWidth(300)
self:SetHeight(100)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
end
local function LayoutFinished(self, width, height)
if self.noAutoHeight then return end
self:SetHeight(height or 0)
end
local function OnWidthSet(self, width)
local content = self.content
content:SetWidth(width)
content.width = width
end
local function OnHeightSet(self, height)
local content = self.content
content:SetHeight(height)
content.height = height
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
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")
--Container Support
local content = CreateFrame("Frame",nil,frame)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
content:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,284 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local min, max, floor = math.min, math.max, math.floor
local tonumber = tonumber
-- 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: GameFontHighlightSmall
--------------------------
-- Slider --
--------------------------
do
local Type = "Slider"
local Version = 10
local function OnAcquire(self)
self:SetWidth(200)
self:SetHeight(44)
self:SetDisabled(false)
self:SetIsPercent(nil)
self:SetSliderValues(0,100,1)
self:SetValue(0)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self.slider:EnableMouseWheel(false)
self:SetDisabled(false)
end
local function Control_OnEnter(this)
this.obj:Fire("OnEnter")
end
local function Control_OnLeave(this)
this.obj:Fire("OnLeave")
end
local function UpdateText(self)
local value = self.value or 0
if self.ispercent then
self.editbox:SetText(("%s%%"):format(floor(value*1000+0.5)/10))
else
self.editbox:SetText(floor(value*100+0.5)/100)
end
end
local function UpdateLabels(self)
local min, max = (self.min or 0), (self.max or 100)
if self.ispercent then
self.lowtext:SetFormattedText("%s%%",(min * 100))
self.hightext:SetFormattedText("%s%%",(max * 100))
else
self.lowtext:SetText(min)
self.hightext:SetText(max)
end
end
local function Slider_OnValueChanged(this)
local self = this.obj
if not this.setup then
local newvalue
newvalue = this:GetValue()
if newvalue ~= self.value and not self.disabled then
self.value = newvalue
self:Fire("OnValueChanged", newvalue)
end
if self.value then
local value = self.value
UpdateText(self)
end
end
end
local function Slider_OnMouseUp(this)
local self = this.obj
self:Fire("OnMouseUp",this:GetValue())
end
local function Slider_OnMouseWheel(this, v)
local self = this.obj
if not self.disabled then
local value = self.value
if v > 0 then
value = min(value + (self.step or 1),self.max)
else
value = max(value - (self.step or 1), self.min)
end
self.slider:SetValue(value)
end
end
local function SetDisabled(self, disabled)
self.disabled = disabled
if disabled then
self.slider:EnableMouse(false)
self.label:SetTextColor(.5,.5,.5)
self.hightext:SetTextColor(.5,.5,.5)
self.lowtext:SetTextColor(.5,.5,.5)
--self.valuetext:SetTextColor(.5,.5,.5)
self.editbox:SetTextColor(.5,.5,.5)
self.editbox:EnableMouse(false)
self.editbox:ClearFocus()
else
self.slider:EnableMouse(true)
self.label:SetTextColor(1,.82,0)
self.hightext:SetTextColor(1,1,1)
self.lowtext:SetTextColor(1,1,1)
--self.valuetext:SetTextColor(1,1,1)
self.editbox:SetTextColor(1,1,1)
self.editbox:EnableMouse(true)
end
end
local function SetValue(self, value)
self.slider.setup = true
self.slider:SetValue(value)
self.value = value
UpdateText(self)
self.slider.setup = nil
end
local function SetLabel(self, text)
self.label:SetText(text)
end
local function SetSliderValues(self, min, max, step)
local frame = self.slider
frame.setup = true
self.min = min
self.max = max
self.step = step
frame:SetMinMaxValues(min or 0,max or 100)
UpdateLabels(self)
frame:SetValueStep(step or 1)
if self.value then
frame:SetValue(self.value)
end
frame.setup = nil
end
local function EditBox_OnEscapePressed(this)
this:ClearFocus()
end
local function EditBox_OnEnterPressed(this)
local self = this.obj
local value = this:GetText()
if self.ispercent then
value = value:gsub('%%','')
value = tonumber(value) / 100
else
value = tonumber(value)
end
if value then
PlaySound("igMainMenuOptionCheckBoxOn")
self:Fire("OnMouseUp",value)
end
end
local function EditBox_OnEnter(this)
this:SetBackdropBorderColor(0.5,0.5,0.5,1)
end
local function EditBox_OnLeave(this)
this:SetBackdropBorderColor(0.3,0.3,0.3,0.8)
end
local function SetIsPercent(self, value)
self.ispercent = value
UpdateLabels(self)
UpdateText(self)
end
local function FrameOnMouseDown(this)
this.obj.slider:EnableMouseWheel(true)
AceGUI:ClearFocus()
end
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 = 6, bottom = 6 }
}
local ManualBackdrop = {
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
edgeFile = "Interface\\ChatFrame\\ChatFrameBackground",
tile = true, edgeSize = 1, tileSize = 5,
}
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
self.SetDisabled = SetDisabled
self.SetValue = SetValue
self.SetSliderValues = SetSliderValues
self.SetLabel = SetLabel
self.SetIsPercent = SetIsPercent
self.alignoffset = 25
frame:EnableMouse(true)
frame:SetScript("OnMouseDown",FrameOnMouseDown)
self.slider = CreateFrame("Slider",nil,frame)
local slider = self.slider
slider:SetScript("OnEnter",Control_OnEnter)
slider:SetScript("OnLeave",Control_OnLeave)
slider:SetScript("OnMouseUp", Slider_OnMouseUp)
slider.obj = self
slider:SetOrientation("HORIZONTAL")
slider:SetHeight(15)
slider:SetHitRectInsets(0,0,-10,0)
slider:SetBackdrop(SliderBackdrop)
--slider:EnableMouseWheel(true)
slider:SetScript("OnMouseWheel", Slider_OnMouseWheel)
local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormal")
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
label:SetPoint("TOPRIGHT",frame,"TOPRIGHT",0,0)
label:SetJustifyH("CENTER")
label:SetHeight(15)
self.label = label
self.lowtext = slider:CreateFontString(nil,"ARTWORK","GameFontHighlightSmall")
self.lowtext:SetPoint("TOPLEFT",slider,"BOTTOMLEFT",2,3)
self.hightext = slider:CreateFontString(nil,"ARTWORK","GameFontHighlightSmall")
self.hightext:SetPoint("TOPRIGHT",slider,"BOTTOMRIGHT",-2,3)
local editbox = CreateFrame("EditBox",nil,frame)
editbox:SetAutoFocus(false)
editbox:SetFontObject(GameFontHighlightSmall)
editbox:SetPoint("TOP",slider,"BOTTOM",0,0)
editbox:SetHeight(14)
editbox:SetWidth(70)
editbox:SetJustifyH("CENTER")
editbox:EnableMouse(true)
editbox:SetScript("OnEscapePressed",EditBox_OnEscapePressed)
editbox:SetScript("OnEnterPressed",EditBox_OnEnterPressed)
editbox:SetScript("OnEnter",EditBox_OnEnter)
editbox:SetScript("OnLeave",EditBox_OnLeave)
editbox:SetBackdrop(ManualBackdrop)
editbox:SetBackdropColor(0,0,0,0.5)
editbox:SetBackdropBorderColor(0.3,0.3,0.30,0.80)
self.editbox = editbox
editbox.obj = self
slider:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Horizontal")
frame:SetWidth(200)
frame:SetHeight(44)
slider:SetPoint("TOP",label,"BOTTOM",0,0)
slider:SetPoint("LEFT",frame,"LEFT",3,0)
slider:SetPoint("RIGHT",frame,"RIGHT",-3,0)
slider:SetValue(self.value or 0)
slider:SetScript("OnValueChanged",Slider_OnValueChanged)
AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,389 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local pairs, ipairs, assert, type = pairs, ipairs, assert, type
-- WoW APIs
local PlaySound = PlaySound
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: PanelTemplates_TabResize, PanelTemplates_SetDisabledTabState, PanelTemplates_SelectTab, PanelTemplates_DeselectTab
-------------
-- 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
]]
--------------------------
-- Tab Group --
--------------------------
do
local Type = "TabGroup"
local Version = 25
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 OnAcquire(self)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self.status = nil
for k in pairs(self.localstatus) do
self.localstatus[k] = nil
end
self.tablist = nil
for _, tab in pairs(self.tabs) do
tab:Hide()
end
self:SetTitle()
end
local function Tab_SetText(self, text)
self:_SetText(text)
local width = self.obj.frame.width or self.obj.frame:GetWidth() or 0
PanelTemplates_TabResize(self, 0, nil, width)
end
local function UpdateTabLook(self)
if self.disabled then
PanelTemplates_SetDisabledTabState(self)
elseif self.selected then
PanelTemplates_SelectTab(self)
else
PanelTemplates_DeselectTab(self)
end
end
local function Tab_SetSelected(self, selected)
self.selected = selected
UpdateTabLook(self)
end
local function Tab_OnClick(self)
if not (self.selected or self.disabled) then
PlaySound("igCharacterInfoTab")
self.obj:SelectTab(self.value)
end
end
local function Tab_SetDisabled(self, disabled)
self.disabled = disabled
UpdateTabLook(self)
end
local function Tab_OnEnter(this)
local self = this.obj
self:Fire("OnTabEnter", self.tabs[this.id].value, this)
end
local function Tab_OnLeave(this)
local self = this.obj
self:Fire("OnTabLeave", self.tabs[this.id].value, this)
end
local function Tab_OnShow(this)
_G[this:GetName().."HighlightTexture"]:SetWidth(this:GetTextWidth() + 30)
end
local function CreateTab(self, id)
local tabname = "AceGUITabGroup"..self.num.."Tab"..id
local tab = CreateFrame("Button",tabname,self.border,"OptionsFrameTabButtonTemplate")
tab.obj = self
tab.id = id
tab.text = _G[tabname .. "Text"]
tab.text:ClearAllPoints()
tab.text:SetPoint("LEFT", tab, "LEFT", 14, -3)
tab.text:SetPoint("RIGHT", tab, "RIGHT", -12, -3)
tab:SetScript("OnClick",Tab_OnClick)
tab:SetScript("OnEnter",Tab_OnEnter)
tab:SetScript("OnLeave",Tab_OnLeave)
tab:SetScript("OnShow", Tab_OnShow)
tab._SetText = tab.SetText
tab.SetText = Tab_SetText
tab.SetSelected = Tab_SetSelected
tab.SetDisabled = Tab_SetDisabled
return tab
end
local function SetTitle(self, text)
self.titletext:SetText(text or "")
if text and text ~= "" then
self.alignoffset = 25
else
self.alignoffset = 18
end
self:BuildTabs()
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 SelectTab(self, value)
local status = self.status or self.localstatus
local found
for i, v in ipairs(self.tabs) do
if v.value == value then
v:SetSelected(true)
found = true
else
v:SetSelected(false)
end
end
status.selected = value
if found then
self:Fire("OnGroupSelected",value)
end
end
local function SetTabs(self, tabs)
self.tablist = tabs
self:BuildTabs()
end
local widths = {}
local rowwidths = {}
local rowends = {}
local function BuildTabs(self)
local hastitle = (self.titletext:GetText() and self.titletext:GetText() ~= "")
local status = self.status or self.localstatus
local tablist = self.tablist
local tabs = self.tabs
if not tablist then return end
local width = self.frame.width or self.frame:GetWidth() or 0
for i = #widths, 1, -1 do
widths[i] = nil
end
for i = #rowwidths, 1, -1 do
rowwidths[i] = nil
end
for i = #rowends, 1, -1 do
rowends[i] = nil
end
--Place Text into tabs and get thier initial width
for i, v in ipairs(tablist) do
local tab = tabs[i]
if not tab then
tab = self:CreateTab(i)
tabs[i] = tab
end
tab:Show()
tab:SetText(v.text)
tab:SetDisabled(v.disabled)
tab.value = v.value
widths[i] = tab:GetWidth() - 6 --tabs are anchored 10 pixels from the right side of the previous one to reduce spacing, but add a fixed 4px padding for the text
end
for i = (#tablist)+1, #tabs, 1 do
tabs[i]:Hide()
end
--First pass, find the minimum number of rows needed to hold all tabs and the initial tab layout
local numtabs = #tablist
local numrows = 1
local usedwidth = 0
for i = 1, #tablist do
--If this is not the first tab of a row and there isn't room for it
if usedwidth ~= 0 and (width - usedwidth - widths[i]) < 0 then
rowwidths[numrows] = usedwidth + 10 --first tab in each row takes up an extra 10px
rowends[numrows] = i - 1
numrows = numrows + 1
usedwidth = 0
end
usedwidth = usedwidth + widths[i]
end
rowwidths[numrows] = usedwidth + 10 --first tab in each row takes up an extra 10px
rowends[numrows] = #tablist
--Fix for single tabs being left on the last row, move a tab from the row above if applicable
if numrows > 1 then
--if the last row has only one tab
if rowends[numrows-1] == numtabs-1 then
--if there are more than 2 tabs in the 2nd last row
if (numrows == 2 and rowends[numrows-1] > 2) or (rowends[numrows] - rowends[numrows-1] > 2) then
--move 1 tab from the second last row to the last, if there is enough space
if (rowwidths[numrows] + widths[numtabs-1]) <= width then
rowends[numrows-1] = rowends[numrows-1] - 1
rowwidths[numrows] = rowwidths[numrows] + widths[numtabs-1]
rowwidths[numrows-1] = rowwidths[numrows-1] - widths[numtabs-1]
end
end
end
end
--anchor the rows as defined and resize tabs to fill thier row
local starttab = 1
for row, endtab in ipairs(rowends) do
local first = true
for tabno = starttab, endtab do
local tab = tabs[tabno]
tab:ClearAllPoints()
if first then
tab:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 0, -(hastitle and 14 or 7)-(row-1)*20 )
first = false
else
tab:SetPoint("LEFT", tabs[tabno-1], "RIGHT", -10, 0)
end
end
-- equal padding for each tab to fill the available width,
-- if the used space is above 75% already
local padding = 0
if not (numrows == 1 and rowwidths[1] < width*0.75) then
padding = (width - rowwidths[row]) / (endtab - starttab+1)
end
for i = starttab, endtab do
PanelTemplates_TabResize(tabs[i], padding + 4, nil, width)
end
starttab = endtab + 1
end
self.borderoffset = (hastitle and 17 or 10)+((numrows)*20)
self.border:SetPoint("TOPLEFT",self.frame,"TOPLEFT",1,-self.borderoffset)
end
local function BuildTabsOnUpdate(this)
BuildTabs(this.obj)
this:SetScript("OnUpdate", nil)
end
local function OnWidthSet(self, width)
local content = self.content
local contentwidth = width - 60
if contentwidth < 0 then
contentwidth = 0
end
content:SetWidth(contentwidth)
content.width = contentwidth
BuildTabs(self)
self.frame:SetScript("OnUpdate", BuildTabsOnUpdate)
end
local function OnHeightSet(self, height)
local content = self.content
local contentheight = height - (self.borderoffset + 23)
if contentheight < 0 then
contentheight = 0
end
content:SetHeight(contentheight)
content.height = contentheight
end
local function LayoutFinished(self, width, height)
if self.noAutoHeight then return end
self:SetHeight((height or 0) + (self.borderoffset + 23))
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
self.num = AceGUI:GetNextWidgetNum(Type)
self.localstatus = {}
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetTitle = SetTitle
self.CreateTab = CreateTab
self.SelectTab = SelectTab
self.BuildTabs = BuildTabs
self.SetStatusTable = SetStatusTable
self.SetTabs = SetTabs
self.LayoutFinished = LayoutFinished
self.frame = frame
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
frame.obj = self
frame:SetHeight(100)
frame:SetWidth(100)
frame:SetFrameStrata("FULLSCREEN_DIALOG")
self.alignoffset = 18
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)
titletext:SetText("")
self.titletext = titletext
local border = CreateFrame("Frame",nil,frame)
self.border = border
self.borderoffset = 27
border:SetPoint("TOPLEFT",frame,"TOPLEFT",1,-27)
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)
self.tabs = {}
--Container Support
local content = CreateFrame("Frame",nil,border)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",border,"TOPLEFT",10,-7)
content:SetPoint("BOTTOMRIGHT",border,"BOTTOMRIGHT",-10,7)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,746 @@
local AceGUI = LibStub("AceGUI-3.0")
-- Lua APIs
local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
local math_min, math_max, floor = math.min, math.max, floor
local select, tremove, unpack = select, table.remove, unpack
-- 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: GameTooltip, FONT_COLOR_CODE_CLOSE
-- Recycling functions
local new, del
do
local pool = setmetatable({},{__mode='k'})
function new()
local t = next(pool)
if t then
pool[t] = nil
return t
else
return {}
end
end
function del(t)
for k in pairs(t) do
t[k] = nil
end
pool[t] = true
end
end
--------------
-- TreeView --
--------------
do
local Type = "TreeGroup"
local Version = 23
local DEFAULT_TREE_WIDTH = 175
local DEFAULT_TREE_SIZABLE = true
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 DraggerBackdrop = {
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = nil,
tile = true, tileSize = 16, edgeSize = 0,
insets = { left = 3, right = 3, top = 7, bottom = 7 }
}
local function OnAcquire(self)
self:SetTreeWidth(DEFAULT_TREE_WIDTH,DEFAULT_TREE_SIZABLE)
self:EnableButtonTooltips(true)
end
local function OnRelease(self)
self.frame:ClearAllPoints()
self.frame:Hide()
self.status = nil
for k, v in pairs(self.localstatus) do
if k == "groups" then
for k2 in pairs(v) do
v[k2] = nil
end
else
self.localstatus[k] = nil
end
end
self.localstatus.scrollvalue = 0
self.localstatus.treewidth = DEFAULT_TREE_WIDTH
self.localstatus.treesizable = DEFAULT_TREE_SIZABLE
end
local function GetButtonParents(line)
local parent = line.parent
if parent and parent.value then
return parent.value, GetButtonParents(parent)
end
end
local function GetButtonUniqueValue(line)
local parent = line.parent
if parent and parent.value then
return GetButtonUniqueValue(parent).."\001"..line.value
else
return line.value
end
end
local function ButtonOnClick(this)
local self = this.obj
self:Fire("OnClick",this.uniquevalue, this.selected)
if not this.selected then
self:SetSelected(this.uniquevalue)
this.selected = true
this:LockHighlight()
self:RefreshTree()
end
AceGUI:ClearFocus()
end
local function ExpandOnClick(this)
local button = this.button
local self = button.obj
local status = (self.status or self.localstatus).groups
status[button.uniquevalue] = not status[button.uniquevalue]
self:RefreshTree()
end
local function ButtonOnDoubleClick(button)
local self = button.obj
local status = self.status or self.localstatus
local status = (self.status or self.localstatus).groups
status[button.uniquevalue] = not status[button.uniquevalue]
self:RefreshTree()
end
local function EnableButtonTooltips(self, enable)
self.enabletooltips = enable
end
local function Button_OnEnter(this)
local self = this.obj
self:Fire("OnButtonEnter", this.uniquevalue, this)
if self.enabletooltips then
GameTooltip:SetOwner(this, "ANCHOR_NONE")
GameTooltip:SetPoint("LEFT",this,"RIGHT")
GameTooltip:SetText(this.text:GetText() or "", 1, .82, 0, 1)
GameTooltip:Show()
end
end
local function Button_OnLeave(this)
local self = this.obj
self:Fire("OnButtonLeave", this.uniquevalue, this)
if self.enabletooltips then
GameTooltip:Hide()
end
end
local buttoncount = 1
local function CreateButton(self)
local button = CreateFrame("Button",("AceGUI30TreeButton%d"):format(buttoncount),self.treeframe, "OptionsListButtonTemplate")
buttoncount = buttoncount + 1
button.obj = self
local icon = button:CreateTexture(nil, "OVERLAY")
icon:SetWidth(14)
icon:SetHeight(14)
button.icon = icon
button:SetScript("OnClick",ButtonOnClick)
button:SetScript("OnDoubleClick", ButtonOnDoubleClick)
button:SetScript("OnEnter",Button_OnEnter)
button:SetScript("OnLeave",Button_OnLeave)
button.toggle.button = button
button.toggle:SetScript("OnClick",ExpandOnClick)
return button
end
local function UpdateButton(button, treeline, selected, canExpand, isExpanded)
local self = button.obj
local toggle = button.toggle
local frame = self.frame
local text = treeline.text or ""
local icon = treeline.icon
local iconCoords = treeline.iconCoords
local level = treeline.level
local value = treeline.value
local uniquevalue = treeline.uniquevalue
local disabled = treeline.disabled
button.treeline = treeline
button.value = value
button.uniquevalue = uniquevalue
if selected then
button:LockHighlight()
button.selected = true
else
button:UnlockHighlight()
button.selected = false
end
local normalTexture = button:GetNormalTexture()
local line = button.line
button.level = level
if ( level == 1 ) then
button:SetNormalFontObject("GameFontNormal")
button:SetHighlightFontObject("GameFontHighlight")
button.text:SetPoint("LEFT", (icon and 16 or 0) + 8, 2)
else
button:SetNormalFontObject("GameFontHighlightSmall")
button:SetHighlightFontObject("GameFontHighlightSmall")
button.text:SetPoint("LEFT", (icon and 16 or 0) + 8 * level, 2)
end
if disabled then
button:EnableMouse(false)
button.text:SetText("|cff808080"..text..FONT_COLOR_CODE_CLOSE)
else
button.text:SetText(text)
button:EnableMouse(true)
end
if icon then
button.icon:SetTexture(icon)
button.icon:SetPoint("LEFT", button, "LEFT", 8 * level, (level == 1) and 0 or 1)
else
button.icon:SetTexture(nil)
end
if iconCoords then
button.icon:SetTexCoord(unpack(iconCoords))
else
button.icon:SetTexCoord(0, 1, 0, 1)
end
if canExpand then
if not isExpanded then
toggle:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-UP")
toggle:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN")
else
toggle:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-UP")
toggle:SetPushedTexture("Interface\\Buttons\\UI-MinusButton-DOWN")
end
toggle:Show()
else
toggle:Hide()
end
end
local function OnScrollValueChanged(this, value)
if this.obj.noupdate then return end
local self = this.obj
local status = self.status or self.localstatus
status.scrollvalue = value
self:RefreshTree()
AceGUI:ClearFocus()
end
-- called to set an external table to store status in
local function SetStatusTable(self, status)
assert(type(status) == "table")
self.status = status
if not status.groups then
status.groups = {}
end
if not status.scrollvalue then
status.scrollvalue = 0
end
if not status.treewidth then
status.treewidth = DEFAULT_TREE_WIDTH
end
if not status.treesizable then
status.treesizable = DEFAULT_TREE_SIZABLE
end
self:SetTreeWidth(status.treewidth,status.treesizable)
self:RefreshTree()
end
--sets the tree to be displayed
--[[
example tree
Alpha
Bravo
-Charlie
-Delta
-Echo
Foxtrot
tree = {
{
value = "A",
text = "Alpha"
},
{
value = "B",
text = "Bravo",
children = {
{
value = "C",
text = "Charlie"
},
{
value = "D",
text = "Delta"
children = {
{
value = "E",
text = "Echo"
}
}
}
}
},
{
value = "F",
text = "Foxtrot"
},
}
]]
local function SetTree(self, tree, filter)
self.filter = filter
if tree then
assert(type(tree) == "table")
end
self.tree = tree
self:RefreshTree()
end
local function ShouldDisplayLevel(tree)
local result = false
for k, v in ipairs(tree) do
if v.children == nil and v.visible ~= false then
result = true
elseif v.children then
result = result or ShouldDisplayLevel(v.children)
end
if result then return result end
end
return false
end
local function addLine(self, v, tree, level, parent)
local line = new()
line.value = v.value
line.text = v.text
line.icon = v.icon
line.iconCoords = v.iconCoords
line.disabled = v.disabled
line.tree = tree
line.level = level
line.parent = parent
line.visible = v.visible
line.uniquevalue = GetButtonUniqueValue(line)
if v.children then
line.hasChildren = true
else
line.hasChildren = nil
end
self.lines[#self.lines+1] = line
return line
end
local function BuildLevel(self, tree, level, parent)
local groups = (self.status or self.localstatus).groups
local hasChildren = self.hasChildren
for i, v in ipairs(tree) do
if v.children then
if not self.filter or ShouldDisplayLevel(v.children) then
local line = addLine(self, v, tree, level, parent)
if groups[line.uniquevalue] then
self:BuildLevel(v.children, level+1, line)
end
end
elseif v.visible ~= false or not self.filter then
addLine(self, v, tree, level, parent)
end
end
end
--fire an update after one frame to catch the treeframes height
local function FirstFrameUpdate(this)
local self = this.obj
this:SetScript("OnUpdate",nil)
self:RefreshTree()
end
local function ResizeUpdate(this)
this.obj:RefreshTree()
end
local function RefreshTree(self)
local buttons = self.buttons
local lines = self.lines
for i, v in ipairs(buttons) do
v:Hide()
end
while lines[1] do
local t = tremove(lines)
for k in pairs(t) do
t[k] = nil
end
del(t)
end
if not self.tree then return end
--Build the list of visible entries from the tree and status tables
local status = self.status or self.localstatus
local groupstatus = status.groups
local tree = self.tree
local treeframe = self.treeframe
self:BuildLevel(tree, 1)
local numlines = #lines
local maxlines = (floor(((self.treeframe:GetHeight()or 0) - 20 ) / 18))
local first, last
if numlines <= maxlines then
--the whole tree fits in the frame
status.scrollvalue = 0
self:ShowScroll(false)
first, last = 1, numlines
else
self:ShowScroll(true)
--scrolling will be needed
self.noupdate = true
self.scrollbar:SetMinMaxValues(0, numlines - maxlines)
--check if we are scrolled down too far
if numlines - status.scrollvalue < maxlines then
status.scrollvalue = numlines - maxlines
self.scrollbar:SetValue(status.scrollvalue)
end
self.noupdate = nil
first, last = status.scrollvalue+1, status.scrollvalue + maxlines
end
local buttonnum = 1
for i = first, last do
local line = lines[i]
local button = buttons[buttonnum]
if not button then
button = self:CreateButton()
buttons[buttonnum] = button
button:SetParent(treeframe)
button:SetFrameLevel(treeframe:GetFrameLevel()+1)
button:ClearAllPoints()
if i == 1 then
if self.showscroll then
button:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",-22,-10)
button:SetPoint("TOPLEFT", self.treeframe, "TOPLEFT", 0, -10)
else
button:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",0,-10)
button:SetPoint("TOPLEFT", self.treeframe, "TOPLEFT", 0, -10)
end
else
button:SetPoint("TOPRIGHT", buttons[buttonnum-1], "BOTTOMRIGHT",0,0)
button:SetPoint("TOPLEFT", buttons[buttonnum-1], "BOTTOMLEFT",0,0)
end
end
UpdateButton(button, line, status.selected == line.uniquevalue, line.hasChildren, groupstatus[line.uniquevalue] )
button:Show()
buttonnum = buttonnum + 1
end
end
local function SetSelected(self, value)
local status = self.status or self.localstatus
if status.selected ~= value then
status.selected = value
self:Fire("OnGroupSelected", value)
end
end
local function BuildUniqueValue(...)
local n = select('#', ...)
if n == 1 then
return ...
else
return (...).."\001"..BuildUniqueValue(select(2,...))
end
end
local function Select(self, uniquevalue, ...)
self.filter = false
local status = self.status or self.localstatus
local groups = status.groups
for i = 1, select('#', ...) do
groups[BuildUniqueValue(select(i, ...))] = true
end
status.selected = uniquevalue
self:RefreshTree()
self:Fire("OnGroupSelected", uniquevalue)
end
local function SelectByPath(self, ...)
self:Select(BuildUniqueValue(...), ...)
end
--Selects a tree node by UniqueValue
local function SelectByValue(self, uniquevalue)
self:Select(uniquevalue, ("\001"):split(uniquevalue))
end
local function ShowScroll(self, show)
self.showscroll = show
if show then
self.scrollbar:Show()
if self.buttons[1] then
self.buttons[1]:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",-22,-10)
end
else
self.scrollbar:Hide()
if self.buttons[1] then
self.buttons[1]:SetPoint("TOPRIGHT", self.treeframe,"TOPRIGHT",0,-10)
end
end
end
local function OnWidthSet(self, width)
local content = self.content
local treeframe = self.treeframe
local status = self.status or self.localstatus
status.fullwidth = width
local contentwidth = width - status.treewidth - 20
if contentwidth < 0 then
contentwidth = 0
end
content:SetWidth(contentwidth)
content.width = contentwidth
local maxtreewidth = math_min(400, width - 50)
if maxtreewidth > 100 and status.treewidth > maxtreewidth then
self:SetTreeWidth(maxtreewidth, status.treesizable)
end
treeframe:SetMaxResize(maxtreewidth,1600)
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 TreeOnMouseWheel(this, delta)
local self = this.obj
if self.showscroll then
local scrollbar = self.scrollbar
local min, max = scrollbar:GetMinMaxValues()
local value = scrollbar:GetValue()
local newvalue = math_min(max,math_max(min,value - delta))
if value ~= newvalue then
scrollbar:SetValue(newvalue)
end
end
end
local function SetTreeWidth(self, treewidth, resizable)
if not resizable then
if type(treewidth) == 'number' then
resizable = false
elseif type(treewidth) == 'boolean' then
resizable = treewidth
treewidth = DEFAULT_TREE_WIDTH
else
resizable = false
treewidth = DEFAULT_TREE_WIDTH
end
end
self.treeframe:SetWidth(treewidth)
self.dragger:EnableMouse(resizable)
local status = self.status or self.localstatus
status.treewidth = treewidth
status.treesizable = resizable
-- recalculate the content width
if status.fullwidth then
self:OnWidthSet(status.fullwidth)
end
end
local function draggerLeave(this)
this:SetBackdropColor(1, 1, 1, 0)
end
local function draggerEnter(this)
this:SetBackdropColor(1, 1, 1, 0.8)
end
local function draggerDown(this)
local treeframe = this:GetParent()
treeframe:StartSizing("RIGHT")
end
local function draggerUp(this)
local treeframe = this:GetParent()
local self = treeframe.obj
local frame = treeframe:GetParent()
treeframe:StopMovingOrSizing()
--treeframe:SetScript("OnUpdate", nil)
treeframe:SetUserPlaced(false)
--Without this :GetHeight will get stuck on the current height, causing the tree contents to not resize
treeframe:SetHeight(0)
treeframe:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
treeframe:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",0,0)
local status = self.status or self.localstatus
status.treewidth = treeframe:GetWidth()
treeframe.obj:Fire("OnTreeResize",treeframe:GetWidth())
-- recalculate the content width
treeframe.obj:OnWidthSet(status.fullwidth)
-- update the layout of the content
treeframe.obj:DoLayout()
end
local function LayoutFinished(self, width, height)
if self.noAutoHeight then return end
self:SetHeight((height or 0) + 20)
end
local createdcount = 0
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = Type
self.lines = {}
self.levels = {}
self.buttons = {}
self.hasChildren = {}
self.localstatus = {}
self.localstatus.groups = {}
self.filter = false
local treeframe = CreateFrame("Frame",nil,frame)
treeframe.obj = self
treeframe:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
treeframe:SetPoint("BOTTOMLEFT",frame,"BOTTOMLEFT",0,0)
treeframe:SetWidth(DEFAULT_TREE_WIDTH)
treeframe:SetScript("OnUpdate",FirstFrameUpdate)
treeframe:SetScript("OnSizeChanged",ResizeUpdate)
treeframe:EnableMouseWheel(true)
treeframe:SetScript("OnMouseWheel", TreeOnMouseWheel)
treeframe:SetBackdrop(PaneBackdrop)
treeframe:SetBackdropColor(0.1,0.1,0.1,0.5)
treeframe:SetBackdropBorderColor(0.4,0.4,0.4)
treeframe:SetResizable(true)
treeframe:SetMinResize(100, 1)
treeframe:SetMaxResize(400,1600)
local dragger = CreateFrame("Frame", nil, treeframe)
dragger:SetWidth(8)
dragger:SetPoint("TOP", treeframe, "TOPRIGHT")
dragger:SetPoint("BOTTOM", treeframe, "BOTTOMRIGHT")
dragger:SetBackdrop(DraggerBackdrop)
dragger:SetBackdropColor(1, 1, 1, 0)
dragger:SetScript("OnMouseDown", draggerDown)
dragger:SetScript("OnMouseUp", draggerUp)
dragger:SetScript("OnEnter", draggerEnter)
dragger:SetScript("OnLeave", draggerLeave)
self.dragger = dragger
self.treeframe = treeframe
self.OnRelease = OnRelease
self.OnAcquire = OnAcquire
self.SetTree = SetTree
self.SetTreeWidth = SetTreeWidth
self.RefreshTree = RefreshTree
self.SetStatusTable = SetStatusTable
self.BuildLevel = BuildLevel
self.CreateButton = CreateButton
self.SetSelected = SetSelected
self.ShowScroll = ShowScroll
self.SetStatusTable = SetStatusTable
self.Select = Select
self.SelectByValue = SelectByValue
self.SelectByPath = SelectByPath
self.OnWidthSet = OnWidthSet
self.OnHeightSet = OnHeightSet
self.EnableButtonTooltips = EnableButtonTooltips
--self.Filter = Filter
self.LayoutFinished = LayoutFinished
self.frame = frame
frame.obj = self
createdcount = createdcount + 1
local scrollbar = CreateFrame("Slider",("AceConfigDialogTreeGroup%dScrollBar"):format(createdcount),treeframe,"UIPanelScrollBarTemplate")
self.scrollbar = scrollbar
local scrollbg = scrollbar:CreateTexture(nil,"BACKGROUND")
scrollbg:SetAllPoints(scrollbar)
scrollbg:SetTexture(0,0,0,0.4)
scrollbar.obj = self
self.noupdate = true
scrollbar:SetPoint("TOPRIGHT",treeframe,"TOPRIGHT",-10,-26)
scrollbar:SetPoint("BOTTOMRIGHT",treeframe,"BOTTOMRIGHT",-10,26)
scrollbar:SetScript("OnValueChanged", OnScrollValueChanged)
scrollbar:SetMinMaxValues(0,0)
self.localstatus.scrollvalue = 0
scrollbar:SetValueStep(1)
scrollbar:SetValue(0)
scrollbar:SetWidth(16)
self.noupdate = nil
local border = CreateFrame("Frame",nil,frame)
self.border = border
border:SetPoint("TOPLEFT",treeframe,"TOPRIGHT", 0,0)
border:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",0,0)
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)
--AceGUI:RegisterAsWidget(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end
@@ -0,0 +1,331 @@
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: GameFontNormal
----------------
-- Main Frame --
----------------
--[[
Events :
OnClose
]]
do
local Type = "Window"
local Version = 4
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 sizerOnMouseUp(this)
this:GetParent():StopMovingOrSizing()
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:EnableResize(true)
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 EnableResize(self, state)
local func = state and "Show" or "Hide"
self.sizer_se[func](self.sizer_se)
self.sizer_s[func](self.sizer_s)
self.sizer_e[func](self.sizer_e)
end
local function Constructor()
local frame = CreateFrame("Frame",nil,UIParent)
local self = {}
self.type = "Window"
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.EnableResize = EnableResize
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:SetScript("OnHide",frameOnClose)
frame:SetMinResize(240,240)
frame:SetToplevel(true)
local titlebg = frame:CreateTexture(nil, "BACKGROUND")
titlebg:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Title-Background]])
titlebg:SetPoint("TOPLEFT", 9, -6)
titlebg:SetPoint("BOTTOMRIGHT", frame, "TOPRIGHT", -28, -24)
local dialogbg = frame:CreateTexture(nil, "BACKGROUND")
dialogbg:SetTexture([[Interface\Tooltips\UI-Tooltip-Background]])
dialogbg:SetPoint("TOPLEFT", 8, -24)
dialogbg:SetPoint("BOTTOMRIGHT", -6, 8)
dialogbg:SetVertexColor(0, 0, 0, .75)
local topleft = frame:CreateTexture(nil, "BORDER")
topleft:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
topleft:SetWidth(64)
topleft:SetHeight(64)
topleft:SetPoint("TOPLEFT")
topleft:SetTexCoord(0.501953125, 0.625, 0, 1)
local topright = frame:CreateTexture(nil, "BORDER")
topright:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
topright:SetWidth(64)
topright:SetHeight(64)
topright:SetPoint("TOPRIGHT")
topright:SetTexCoord(0.625, 0.75, 0, 1)
local top = frame:CreateTexture(nil, "BORDER")
top:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
top:SetHeight(64)
top:SetPoint("TOPLEFT", topleft, "TOPRIGHT")
top:SetPoint("TOPRIGHT", topright, "TOPLEFT")
top:SetTexCoord(0.25, 0.369140625, 0, 1)
local bottomleft = frame:CreateTexture(nil, "BORDER")
bottomleft:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
bottomleft:SetWidth(64)
bottomleft:SetHeight(64)
bottomleft:SetPoint("BOTTOMLEFT")
bottomleft:SetTexCoord(0.751953125, 0.875, 0, 1)
local bottomright = frame:CreateTexture(nil, "BORDER")
bottomright:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
bottomright:SetWidth(64)
bottomright:SetHeight(64)
bottomright:SetPoint("BOTTOMRIGHT")
bottomright:SetTexCoord(0.875, 1, 0, 1)
local bottom = frame:CreateTexture(nil, "BORDER")
bottom:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
bottom:SetHeight(64)
bottom:SetPoint("BOTTOMLEFT", bottomleft, "BOTTOMRIGHT")
bottom:SetPoint("BOTTOMRIGHT", bottomright, "BOTTOMLEFT")
bottom:SetTexCoord(0.376953125, 0.498046875, 0, 1)
local left = frame:CreateTexture(nil, "BORDER")
left:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
left:SetWidth(64)
left:SetPoint("TOPLEFT", topleft, "BOTTOMLEFT")
left:SetPoint("BOTTOMLEFT", bottomleft, "TOPLEFT")
left:SetTexCoord(0.001953125, 0.125, 0, 1)
local right = frame:CreateTexture(nil, "BORDER")
right:SetTexture([[Interface\PaperDollInfoFrame\UI-GearManager-Border]])
right:SetWidth(64)
right:SetPoint("TOPRIGHT", topright, "BOTTOMRIGHT")
right:SetPoint("BOTTOMRIGHT", bottomright, "TOPRIGHT")
right:SetTexCoord(0.1171875, 0.2421875, 0, 1)
local close = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", 2, 1)
close:SetScript("OnClick", closeOnClick)
self.closebutton = close
close.obj = self
local titletext = frame:CreateFontString(nil, "ARTWORK")
titletext:SetFontObject(GameFontNormal)
titletext:SetPoint("TOPLEFT", 12, -8)
titletext:SetPoint("TOPRIGHT", -32, -8)
self.titletext = titletext
local title = CreateFrame("Button", nil, frame)
title:SetPoint("TOPLEFT", titlebg)
title:SetPoint("BOTTOMRIGHT", titlebg)
title:EnableMouse()
title:SetScript("OnMouseDown",titleOnMouseDown)
title:SetScript("OnMouseUp", frameOnMouseUp)
self.title = title
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", sizerOnMouseUp)
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", sizerOnMouseUp)
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", sizerOnMouseUp)
self.sizer_e = sizer_e
--Container Support
local content = CreateFrame("Frame",nil,frame)
self.content = content
content.obj = self
content:SetPoint("TOPLEFT",frame,"TOPLEFT",12,-32)
content:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-12,13)
AceGUI:RegisterAsContainer(self)
return self
end
AceGUI:RegisterWidgetType(Type,Constructor,Version)
end

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