Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87ec8c2ce9 | |||
| b4a2eb8bc2 | |||
| ae178d4a18 | |||
| e5ad71bc59 | |||
| b0ac94509a | |||
| 2e98622454 | |||
| e48bb6fc03 | |||
| a3b7b834b7 | |||
| f82bbf252c | |||
| a3fc195f14 | |||
| c1c2b4e8bb | |||
| 83f5e08689 | |||
| 76ca0cc851 | |||
| 398245d27d | |||
| 887ab7879b | |||
| e3b8d2649e | |||
| e916cdea19 | |||
| befdaa456d | |||
| 7291fff664 | |||
| 790506fc7a | |||
| 87329f2633 | |||
| 8375b95f7b | |||
| 01f476ca1b | |||
| 63c9af7846 | |||
| f02f4d73f0 | |||
| 721833bb50 | |||
| b30a3554cf | |||
| 595ed5091d | |||
| 85f80445d1 | |||
| 9fdb7ed718 | |||
| 698de84762 | |||
| cddaf2c515 | |||
| 2291ace9f7 | |||
| 26ff2b3b94 | |||
| d5e2dce359 | |||
| 22ae941c4a | |||
| 08c1b9da3f | |||
| b0da9ec10b | |||
| d8d94f10b0 | |||
| 255485daca |
+5
-1
@@ -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
@@ -3,4 +3,10 @@ __pycache__/
|
||||
manifest.json
|
||||
.claude
|
||||
.vscode
|
||||
dist
|
||||
dist
|
||||
build/
|
||||
Wow*.exe
|
||||
*.backup.exe
|
||||
Logs/
|
||||
local-assets/
|
||||
!Wow_Original.exe
|
||||
|
||||
+24
@@ -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
|
||||
@@ -0,0 +1,46 @@
|
||||
# MoonWell Client Sources — Agent Guide
|
||||
|
||||
## Проект
|
||||
|
||||
WoW-клиент на базе патча **3.3.5a (build 12340, WotLK)**. Репозиторий содержит UI-аддоны и
|
||||
кастомные интерфейсы для приватного сервера MoonWell.
|
||||
|
||||
Стек: Lua 5.1, FrameXML (XML + Lua), WoW Addon API 3.3.5.
|
||||
|
||||
---
|
||||
|
||||
## Навыки (Skills)
|
||||
|
||||
Перед тем как работать с UI — **прочитай соответствующий файл из `ai-skills/`**.
|
||||
|
||||
| Задача | Файл |
|
||||
|--------|------|
|
||||
| Верстка фреймов, XML, Lua CreateFrame, pixel-perfect из Figma, текстуры, шрифты, якоря | [`ai-skills/ui/SKILL.md`](ai-skills/ui/SKILL.md) |
|
||||
| Виджеты, типы фреймов, Button/EditBox/ScrollFrame, события, SavedVariables, Blizzard API | [`ai-skills/ui/widgets-and-api.md`](ai-skills/ui/widgets-and-api.md) |
|
||||
|
||||
Читай **весь файл** скилла, а не отдельные секции — там есть ограничения API, типичные ошибки
|
||||
и готовые шаблоны.
|
||||
|
||||
---
|
||||
|
||||
## Ключевые ограничения (всегда держи в голове)
|
||||
|
||||
- `## Interface: 30300` — никаких retail-API
|
||||
- Нет `SetSize()` → только `SetWidth()` / `SetHeight()`
|
||||
- Нет `C_Timer`, `CreateFramePool`, `PixelUtil`
|
||||
- Текстуры: только степени двойки, максимум 512×512 px, формат `.tga` / `.blp`
|
||||
- XML-атрибуты: `relativeTo` (не `relativeKey`), `$parentX` (не `$parent.X`)
|
||||
- Кнопки и ScrollFrame в XML должны иметь `name`
|
||||
|
||||
---
|
||||
|
||||
## Структура аддона
|
||||
|
||||
```
|
||||
AddonName/
|
||||
├── AddonName.toc ← обязательно, имя файла = имя папки
|
||||
├── AddonName.xml ← верстка (загружается первой)
|
||||
└── AddonName.lua ← логика
|
||||
```
|
||||
|
||||
Порядок файлов в TOC важен: XML раньше Lua.
|
||||
@@ -0,0 +1,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()
|
||||
@@ -0,0 +1,788 @@
|
||||
# План расширенной кастомизации персонажей MoonWell
|
||||
|
||||
## 1. Цель
|
||||
|
||||
Создать для клиента WoW 3.3.5a (build 12340) расширенную систему внешности, визуально и по удобству близкую к retail, сохранив оригинальный `Wow.exe` и используя WarcraftXL для всех клиентских расширений.
|
||||
|
||||
Система должна:
|
||||
|
||||
- поддерживать стандартные параметры 3.3.5a и дополнительные параметры MoonWell;
|
||||
- показывать одинаковую внешность при создании, выборе персонажа, входе в мир и другим игрокам;
|
||||
- хранить дополнительные параметры на сервере и считать сервер источником истины;
|
||||
- поддерживать текстурные слои, geoset базовой модели и отдельные прикрепляемые M2-модели;
|
||||
- работать без Eluna/AIO в качестве постоянного транспорта состояния;
|
||||
- позволять добавлять новые варианты преимущественно данными и ассетами;
|
||||
- иметь версионирование формата и безопасный откат.
|
||||
|
||||
## 2. Не входит в первую версию
|
||||
|
||||
- полное копирование внутренних `ChrCustomization*` DB2 и протокола актуального retail-клиента;
|
||||
- поддержка всех retail-рас, которых нет в MoonWell;
|
||||
- динамическое изменение скелета или пропорций тела;
|
||||
- автоматический импорт всех retail-ассетов без ручной проверки;
|
||||
- публикация полного дампа современных файлов Blizzard;
|
||||
- изменение бинарника `Wow.exe` на диске.
|
||||
|
||||
## 3. Базовые ограничения
|
||||
|
||||
### Клиент
|
||||
|
||||
- WoW 3.3.5a, build 12340, Lua 5.1, `## Interface: 30300`.
|
||||
- `Wow.exe` должен оставаться оригинальным с контролируемым SHA-256.
|
||||
- Новое нативное поведение реализуется модулем MoonWell внутри WarcraftXL.
|
||||
- GlueXML и FrameXML не используют retail API (`C_Timer`, `CreateFramePool`, `PixelUtil` и т. п.).
|
||||
- Для анимаций интерфейса используется `OnUpdate`.
|
||||
- UI-текстуры: `.blp`/`.tga`, размеры по степеням двойки, не более 512×512 на отдельный UI-тайл.
|
||||
- В Lua/XML использовать `SetWidth`/`SetHeight`, а не `SetSize`.
|
||||
|
||||
### Ассеты и рендер
|
||||
|
||||
- Исходные современные M2/MD21 обрабатываются `wxl-modern-assets`.
|
||||
- Каждая модель проверяется на поддерживаемую версию формата, лимит костей, материалы, анимации и attachment points.
|
||||
- Не использовать OBJ/FBX/glTF как вход для игрового клиента: нужны исходные `.m2`, `.skin`, `.skel`, `.anim` и `.blp`.
|
||||
- UI-превью и модель в мире должны использовать один каталог вариантов и одинаковые идентификаторы.
|
||||
|
||||
### Сервер
|
||||
|
||||
- Сервер валидирует расу, пол, доступность и совместимость каждого выбора.
|
||||
- Клиент не может назначить себе недоступный вариант внешности.
|
||||
- Дополнительные параметры хранятся отдельно от стандартных байтов внешности 3.3.5a.
|
||||
- Изменения схемы БД поставляются миграциями и имеют обратимый путь.
|
||||
|
||||
## 4. Целевая архитектура
|
||||
|
||||
```text
|
||||
Retail CDN (выбранные FileDataID)
|
||||
│
|
||||
▼
|
||||
Asset manifest + downloader + validation
|
||||
│
|
||||
▼
|
||||
Patch*.MPQ / loose Patch-WXL assets
|
||||
│
|
||||
├── wxl-modern-assets
|
||||
├── адаптированный wxl-equip-extension
|
||||
└── MoonWell customization runtime
|
||||
│
|
||||
Lua API / GlueXML UI
|
||||
│
|
||||
▼
|
||||
MoonWell customization protocol
|
||||
│
|
||||
▼
|
||||
AzerothCore storage + validation + broadcast
|
||||
```
|
||||
|
||||
### Компоненты
|
||||
|
||||
1. **Asset pipeline** — получает только перечисленные зависимости по `FileDataID`, сохраняет манифест и проверяет комплектность.
|
||||
2. **`wxl-modern-assets`** — загружает и приводит современные M2/MD21 и текстуры к контракту клиента 3.3.5a.
|
||||
3. **Адаптированный `wxl-equip-extension`** — прикрепляет M2, поддерживает collection M2, два модельных канала, attachment points и фильтрацию geoset.
|
||||
4. **MoonWell customization runtime** — управляет независимыми косметическими каналами, текстурными слоями, geoset базовой модели и Lua API.
|
||||
5. **GlueXML UI** — категории, список вариантов, камера, случайный выбор, сброс и предварительный просмотр.
|
||||
6. **Серверный модуль** — хранение, валидация, загрузка, изменение и распространение внешности.
|
||||
|
||||
## 5. Типы параметров внешности
|
||||
|
||||
### Стандартные параметры 3.3.5a
|
||||
|
||||
- пол;
|
||||
- цвет кожи;
|
||||
- лицо;
|
||||
- причёска;
|
||||
- цвет волос;
|
||||
- стандартная особенность лица/борода.
|
||||
|
||||
Они продолжают использовать штатные поля персонажа и стандартный пакет создания.
|
||||
|
||||
### Дополнительные текстурные параметры
|
||||
|
||||
- шрамы;
|
||||
- татуировки тела;
|
||||
- татуировки лица;
|
||||
- макияж;
|
||||
- рисунок/отметины;
|
||||
- дополнительные оттенки кожи;
|
||||
- дополнительные слои лица.
|
||||
|
||||
Реализация: расширение `CharSections`, каталог слоёв и модификация/перехват клиентского композитора текстуры персонажа.
|
||||
|
||||
### Geoset базовой модели
|
||||
|
||||
- дополнительные причёски;
|
||||
- части бороды и усов;
|
||||
- форма ушей/бровей;
|
||||
- варианты клыков;
|
||||
- встроенные рога;
|
||||
- скрытие частей базовой модели.
|
||||
|
||||
Реализация: таблица выбора geoset по расе, полу, категории и варианту.
|
||||
|
||||
### Прикрепляемые M2-элементы
|
||||
|
||||
- серьги и пирсинг;
|
||||
- украшения для волос;
|
||||
- объёмные бороды;
|
||||
- рога и дополнительные клыки;
|
||||
- протезы;
|
||||
- венцы, диадемы и прочие аксессуары;
|
||||
- независимые объёмные элементы тела.
|
||||
|
||||
Реализация: косметические каналы поверх механизма `wxl-equip-extension`.
|
||||
|
||||
## 6. Косметические каналы
|
||||
|
||||
Кастомизация не должна занимать реальные слоты экипировки. Нужен отдельный реестр каналов:
|
||||
|
||||
```text
|
||||
hair_extra
|
||||
beard_extra
|
||||
horns
|
||||
ears
|
||||
tusks
|
||||
piercing_1
|
||||
piercing_2
|
||||
accessory_head
|
||||
accessory_face
|
||||
accessory_body_1
|
||||
accessory_body_2
|
||||
```
|
||||
|
||||
Для каждого канала задаются:
|
||||
|
||||
- стабильный числовой `option_id`;
|
||||
- допустимые расы и пол;
|
||||
- `display_id` или прямое описание M2;
|
||||
- Model1/Model2;
|
||||
- Texture1/Texture2;
|
||||
- attachment point для каждого модельного канала;
|
||||
- список разрешённых geoset collection M2;
|
||||
- флаги суффиксов расы/пола и структуры каталогов;
|
||||
- правила конфликта со шлемом и настоящей экипировкой;
|
||||
- приоритет и взаимоисключающие каналы;
|
||||
- значение сброса (`choice_id = 0`).
|
||||
|
||||
## 7. Адаптация `wxl-equip-extension`
|
||||
|
||||
### Обязательные работы
|
||||
|
||||
- закрепить точную ревизию репозитория как submodule;
|
||||
- адаптировать сборочную структуру к текущему WarcraftXL MoonWell;
|
||||
- проверить пересечение хуков с `wxl-modern-assets` и существующими `OnItemSlotChange`, `OnItemSlotClear`, `OnM2PerFrameUpdate`;
|
||||
- вынести разбор Model1/Model2, `Icon2`, attachment points и collection geoset в переиспользуемый слой;
|
||||
- добавить API независимых косметических каналов, не привязанных к надетому предмету;
|
||||
- устранить утечку/остаток collection M2 после снятия или смены варианта;
|
||||
- обеспечить корректное уничтожение дочерних моделей при смене персонажа, карты, формы и пола;
|
||||
- исключить двойное прикрепление модели при повторной синхронизации;
|
||||
- очистить мусорные Model1/Model2 в старых строках `ItemDisplayInfo.dbc`;
|
||||
- добавить диагностический лог разрешения пути, модели, текстуры, attachment point и geoset;
|
||||
- реализовать безопасный fallback при отсутствии ассета.
|
||||
|
||||
### Проверки совместимости
|
||||
|
||||
- персонаж игрока;
|
||||
- другие игроки;
|
||||
- NPC с тем же `display_id`;
|
||||
- экран создания;
|
||||
- экран выбора;
|
||||
- парикмахерская;
|
||||
- смена экипировки;
|
||||
- превращения и shapeshift;
|
||||
- транспорт;
|
||||
- телепорт и смена карты;
|
||||
- повторный вход и `/reload`.
|
||||
|
||||
## 8. Каталог данных кастомизации
|
||||
|
||||
Нужен единый, версионируемый источник данных, из которого генерируются клиентское и серверное представления.
|
||||
|
||||
Предлагаемая запись:
|
||||
|
||||
```text
|
||||
option_id
|
||||
choice_id
|
||||
race_mask
|
||||
gender_mask
|
||||
category
|
||||
label_key
|
||||
render_type
|
||||
char_section_id
|
||||
geoset_ids
|
||||
model_1
|
||||
model_2
|
||||
texture_1
|
||||
texture_2
|
||||
attachment_1
|
||||
attachment_2
|
||||
flags
|
||||
conflict_group
|
||||
unlock_condition
|
||||
asset_manifest_id
|
||||
enabled
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- `(option_id, choice_id)` никогда не переиспользуется с другим смыслом;
|
||||
- удалённый вариант остаётся распознаваемым для миграции старых персонажей;
|
||||
- порядок отображения UI хранится отдельно от стабильного идентификатора;
|
||||
- клиент и сервер собираются из одной версии каталога;
|
||||
- версия каталога входит в сетевое рукопожатие.
|
||||
|
||||
## 9. Серверное хранение
|
||||
|
||||
### Предлагаемая таблица
|
||||
|
||||
```sql
|
||||
character_customization (
|
||||
guid INT UNSIGNED NOT NULL,
|
||||
option_id SMALLINT UNSIGNED NOT NULL,
|
||||
choice_id INT UNSIGNED NOT NULL,
|
||||
updated_at TIMESTAMP NOT NULL,
|
||||
PRIMARY KEY (guid, option_id)
|
||||
)
|
||||
```
|
||||
|
||||
Дополнительно:
|
||||
|
||||
- индекс по `guid`;
|
||||
- внешний ключ или очистка записей при удалении персонажа;
|
||||
- миграция с начальными значениями для существующих персонажей;
|
||||
- сервис сброса неизвестных/отключённых вариантов;
|
||||
- аудит изменения внешности при необходимости.
|
||||
|
||||
### Серверная логика
|
||||
|
||||
- загрузить набор при `Character::LoadFromDB` или эквивалентном этапе;
|
||||
- валидировать выбор по расе, полу, режиму игры и доступности;
|
||||
- сохранять транзакционно;
|
||||
- включать состояние в данные экрана выбора персонажа;
|
||||
- отправлять полный snapshot при входе в мир;
|
||||
- отправлять delta при изменении;
|
||||
- рассылать актуальное состояние игрокам в зоне видимости;
|
||||
- отправлять snapshot при появлении объекта в зоне видимости нового клиента;
|
||||
- ограничить частоту изменений;
|
||||
- запретить недоступные комбинации независимо от клиентского UI.
|
||||
|
||||
## 10. Сетевой протокол MoonWell
|
||||
|
||||
### Общие требования
|
||||
|
||||
- собственный namespace/opcode или надёжный WarcraftXL transport;
|
||||
- версия протокола и версия каталога;
|
||||
- явная длина сообщения и лимит числа записей;
|
||||
- little-endian поля с фиксированной шириной;
|
||||
- сервер является источником истины;
|
||||
- неизвестные поля пропускаются или отклоняются согласно версии;
|
||||
- отсутствие расширения не ломает стандартный вход в игру.
|
||||
|
||||
### Сообщения
|
||||
|
||||
```text
|
||||
CMSG_MOONWELL_CUSTOMIZATION_CREATE
|
||||
protocol_version
|
||||
catalog_version
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
|
||||
SMSG_MOONWELL_CUSTOMIZATION_RESULT
|
||||
result_code
|
||||
normalized choices
|
||||
|
||||
SMSG_MOONWELL_CUSTOMIZATION_SNAPSHOT
|
||||
player_guid
|
||||
catalog_version
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
|
||||
CMSG_MOONWELL_CUSTOMIZATION_UPDATE
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
|
||||
SMSG_MOONWELL_CUSTOMIZATION_DELTA
|
||||
player_guid
|
||||
option_count
|
||||
repeated(option_id, choice_id)
|
||||
```
|
||||
|
||||
### Экран выбора персонажа
|
||||
|
||||
Предпочтительный вариант — отдельный snapshot после получения списка персонажей. Не расширять штатный пакет без строгой проверки длины и совместимости.
|
||||
|
||||
Клиент должен:
|
||||
|
||||
- сопоставить snapshot с GUID персонажа;
|
||||
- применить его к модели выбранного персонажа;
|
||||
- хранить данные только до обновления от сервера;
|
||||
- показывать стандартную внешность, если snapshot отсутствует или версия несовместима.
|
||||
|
||||
## 11. Клиентский runtime API
|
||||
|
||||
Планируемый API WarcraftXL:
|
||||
|
||||
```text
|
||||
MoonWellCustomizationGetCatalogVersion()
|
||||
MoonWellCustomizationGetOptions(race, gender)
|
||||
MoonWellCustomizationGetChoices(optionId)
|
||||
MoonWellCustomizationGetCurrent(optionId)
|
||||
MoonWellCustomizationPreview(optionId, choiceId)
|
||||
MoonWellCustomizationResetPreview()
|
||||
MoonWellCustomizationCommitPreview()
|
||||
MoonWellCustomizationRandomize()
|
||||
MoonWellCustomizationApplySnapshot(guid, data)
|
||||
MoonWellCustomizationClear(guid)
|
||||
```
|
||||
|
||||
Требования:
|
||||
|
||||
- Lua получает только безопасные операции высокого уровня;
|
||||
- нативный слой владеет указателями моделей и их жизненным циклом;
|
||||
- повторное применение snapshot идемпотентно;
|
||||
- preview не меняет серверное состояние;
|
||||
- при ошибке модель возвращается к последнему подтверждённому snapshot;
|
||||
- смена расы или пола пересчитывает допустимость всех параметров;
|
||||
- отсутствие M2/текстуры отключает только конкретный вариант, а не весь экран.
|
||||
|
||||
## 12. Применение внешности
|
||||
|
||||
Порядок применения должен быть фиксированным:
|
||||
|
||||
1. базовая раса и пол;
|
||||
2. стандартные параметры 3.3.5a;
|
||||
3. `CharSections` и текстурные слои;
|
||||
4. geoset базовой модели;
|
||||
5. скрытие/конфликты с экипировкой;
|
||||
6. независимые косметические M2-каналы;
|
||||
7. обновление материалов и композитной текстуры;
|
||||
8. окончательное обновление позы и камеры превью.
|
||||
|
||||
При смене одного параметра обновляется только затронутый слой, если это безопасно. Полная пересборка модели используется как fallback.
|
||||
|
||||
## 13. Интерфейс создания персонажа
|
||||
|
||||
### Структура
|
||||
|
||||
- категории слева или справа от модели;
|
||||
- прокручиваемая сетка вариантов;
|
||||
- текстовое название активной категории;
|
||||
- состояние выбранного варианта;
|
||||
- кнопки случайного выбора и сброса;
|
||||
- возврат к выбору расы/класса без потери валидных параметров;
|
||||
- подтверждение создания только после серверной валидации;
|
||||
- плавное приближение камеры к зоне редактирования.
|
||||
|
||||
### Категории первой версии
|
||||
|
||||
- кожа;
|
||||
- лицо;
|
||||
- причёска;
|
||||
- цвет волос;
|
||||
- борода/особенность лица;
|
||||
- шрамы;
|
||||
- татуировки;
|
||||
- серьги/пирсинг;
|
||||
- расовый аксессуар.
|
||||
|
||||
Категории скрываются, если для текущей расы/пола нет вариантов.
|
||||
|
||||
### Камера
|
||||
|
||||
- профиль камеры задаётся по расе, полу и категории;
|
||||
- отдельно настраиваются distance, вертикальная цель, горизонтальный сдвиг и FOV;
|
||||
- переход использует easing через `OnUpdate`;
|
||||
- короткие и средние расы имеют отдельные профили;
|
||||
- при выходе из кастомизации камера плавно возвращается к полному росту;
|
||||
- вращение персонажа доступно во всех режимах;
|
||||
- UI не перекрывает лицо и активный аксессуар на 4:3, 16:9 и 16:10.
|
||||
|
||||
### Ограничения UI 3.3.5a
|
||||
|
||||
- XML загружается раньше Lua;
|
||||
- именованные Button и ScrollFrame;
|
||||
- `relativeTo`, а не `relativeKey`;
|
||||
- `$parentX`, а не `$parent.X`;
|
||||
- `SetWidth`/`SetHeight` вместо `SetSize`;
|
||||
- таймеры и анимации через `OnUpdate`;
|
||||
- UI-атласы режутся на тайлы не более 512×512.
|
||||
|
||||
## 14. Парикмахерская и изменение существующего персонажа
|
||||
|
||||
- использовать тот же каталог и preview API;
|
||||
- получать текущий серверный snapshot при открытии;
|
||||
- считать стоимость на сервере;
|
||||
- подтверждать изменение одним атомарным запросом;
|
||||
- откатывать preview при закрытии или отказе сервера;
|
||||
- блокировать недоступные варианты;
|
||||
- поддерживать бесплатные/платные категории отдельными правилами;
|
||||
- обновлять внешность окружающим только после подтверждения.
|
||||
|
||||
## 15. Получение retail-ассетов без установки клиента
|
||||
|
||||
### Инструменты
|
||||
|
||||
- `wow.export` в online-режиме — поиск и предварительный просмотр;
|
||||
- Wago Tools/wow-listfile — поиск пути и `FileDataID`;
|
||||
- TACTSharp/TACTTool — точечная загрузка исходных файлов с CDN Blizzard.
|
||||
|
||||
### Манифест ассета
|
||||
|
||||
```text
|
||||
asset_id
|
||||
source_product
|
||||
source_build
|
||||
file_data_id
|
||||
original_path
|
||||
target_path
|
||||
sha256
|
||||
kind
|
||||
parent_asset_id
|
||||
license_note
|
||||
```
|
||||
|
||||
### Пайплайн
|
||||
|
||||
1. Выбрать модель и исходный retail build.
|
||||
2. Зафиксировать корневой `FileDataID`.
|
||||
3. Выгрузить M2 и определить зависимости.
|
||||
4. Загрузить `.skin`, `.skel`, `.anim` и текстуры.
|
||||
5. Проверить хэши и комплектность.
|
||||
6. Проверить модель в изолированном preview.
|
||||
7. Проверить downport через `wxl-modern-assets`.
|
||||
8. Добавить запись в каталог кастомизации.
|
||||
9. Упаковать только выбранные файлы.
|
||||
10. Не помещать массовый дамп retail-клиента в Git.
|
||||
|
||||
### Автоматические проверки ассета
|
||||
|
||||
- поддерживаемая версия M2/MD21;
|
||||
- наличие всех skin-профилей;
|
||||
- разрешимые FileDataID и пути текстур;
|
||||
- наличие внешнего скелета/анимаций;
|
||||
- attachment points для целевой расы;
|
||||
- лимит костей на draw после разбиения;
|
||||
- валидные материалы и blend modes;
|
||||
- отсутствие коллизий целевых путей;
|
||||
- размер итогового MPQ и память после загрузки;
|
||||
- корректное освобождение ресурсов.
|
||||
|
||||
## 16. Этапы реализации
|
||||
|
||||
### Этап 0. Исследование и фиксация контрактов
|
||||
|
||||
- [ ] Зафиксировать ревизии WarcraftXL и всех модулей.
|
||||
- [ ] Зафиксировать исходный retail build для ассетов.
|
||||
- [ ] Составить карту существующих хуков персонажа и DBC.
|
||||
- [ ] Проверить структуру штатных пакетов создания/выбора персонажа.
|
||||
- [ ] Зафиксировать каталог option/choice ID.
|
||||
- [ ] Утвердить правила доступности и монетизации.
|
||||
- [ ] Выбрать первую расу для вертикального среза: человек.
|
||||
|
||||
**Результат:** техническая спецификация без конфликтующих точек расширения.
|
||||
|
||||
### Этап 1. Интеграция equip-extension
|
||||
|
||||
- [ ] Добавить репозиторий как закреплённый submodule.
|
||||
- [ ] Адаптировать CMake/module layout.
|
||||
- [ ] Собрать Win32 runtime и x64 host.
|
||||
- [ ] Устранить конфликт хуков с текущим WarcraftXL.
|
||||
- [ ] Исправить снятие collection M2.
|
||||
- [ ] Очистить мусорные поля `ItemDisplayInfo.dbc`.
|
||||
- [ ] Добавить smoke-тест Model1/Model2 на каждом поддержанном слоте.
|
||||
- [ ] Добавить диагностическое логирование.
|
||||
|
||||
**Результат:** современная объёмная экипировка работает и корректно снимается.
|
||||
|
||||
### Этап 2. Независимые косметические каналы
|
||||
|
||||
- [ ] Отделить attachment runtime от реальных слотов экипировки.
|
||||
- [ ] Реализовать реестр косметических каналов.
|
||||
- [ ] Реализовать add/replace/remove/clear.
|
||||
- [ ] Реализовать conflict groups и приоритеты.
|
||||
- [ ] Реализовать скрытие под шлемом.
|
||||
- [ ] Добавить API preview.
|
||||
- [ ] Проверить смену карты, формы и пола.
|
||||
|
||||
**Результат:** аксессуар можно показать без создания фиктивного предмета.
|
||||
|
||||
### Этап 3. Asset pipeline
|
||||
|
||||
- [ ] Добавить входной manifest с FileDataID.
|
||||
- [ ] Реализовать пакетную загрузку через CDN.
|
||||
- [ ] Добавить локальный кэш по хэшу.
|
||||
- [ ] Автоматически проверять зависимости.
|
||||
- [ ] Формировать отчёт о пропущенных файлах.
|
||||
- [ ] Интегрировать выбранные ассеты в MPQ build.
|
||||
- [ ] Добавить контроль размера и дубликатов.
|
||||
|
||||
**Результат:** воспроизводимая сборка выбранных retail-ассетов без полного клиента.
|
||||
|
||||
### Этап 4. Серверное хранение и протокол
|
||||
|
||||
- [ ] Добавить миграцию `character_customization`.
|
||||
- [ ] Реализовать серверный каталог допустимых вариантов.
|
||||
- [ ] Реализовать create/update validation.
|
||||
- [ ] Реализовать snapshot и delta.
|
||||
- [ ] Подключить данные к character enum/select flow.
|
||||
- [ ] Рассылать состояние в visibility range.
|
||||
- [ ] Добавить rate limit и журнал ошибок.
|
||||
- [ ] Добавить fallback для клиента без совместимой версии.
|
||||
|
||||
**Результат:** параметры сохраняются и одинаково видны всем клиентам.
|
||||
|
||||
### Этап 5. Текстурные слои и geoset
|
||||
|
||||
- [ ] Расширить/переопределить `CharSections` данными MoonWell.
|
||||
- [ ] Реализовать слои шрамов, татуировок и макияжа.
|
||||
- [ ] Реализовать таблицу geoset базовой модели.
|
||||
- [ ] Синхронизировать цветовые зависимости.
|
||||
- [ ] Обеспечить совместимость с надетой бронёй.
|
||||
- [ ] Добавить полную пересборку как fallback.
|
||||
|
||||
**Результат:** работают не только прикрепляемые M2, но и полноценные параметры тела/лица.
|
||||
|
||||
### Этап 6. Новый UI создания персонажа
|
||||
|
||||
- [ ] Создать адаптивную панель категорий.
|
||||
- [ ] Создать прокручиваемую сетку choices.
|
||||
- [ ] Подключить preview API.
|
||||
- [ ] Добавить race/gender/category camera profiles.
|
||||
- [ ] Добавить randomize и reset.
|
||||
- [ ] Добавить обработку недоступного ассета.
|
||||
- [ ] Добавить серверное подтверждение создания.
|
||||
- [ ] Проверить 4:3, 16:9, 16:10 и UI scale.
|
||||
|
||||
**Результат:** законченный retail-подобный пользовательский сценарий.
|
||||
|
||||
### Этап 7. Вертикальный срез — человек
|
||||
|
||||
- [ ] Мужская и женская модели.
|
||||
- [ ] Стандартные категории.
|
||||
- [ ] Минимум одна новая текстурная категория.
|
||||
- [ ] Минимум два M2-косметических канала.
|
||||
- [ ] Сохранение, выбор персонажа и отображение в мире.
|
||||
- [ ] Видимость другим игрокам.
|
||||
- [ ] Парикмахерская/изменение существующего персонажа.
|
||||
- [ ] Полный regression-тест экипировки.
|
||||
|
||||
**Результат:** одна раса полностью проходит production-сценарий.
|
||||
|
||||
### Этап 8. Масштабирование на все расы WotLK
|
||||
|
||||
- [ ] Дворфы.
|
||||
- [ ] Ночные эльфы.
|
||||
- [ ] Гномы.
|
||||
- [ ] Дренеи.
|
||||
- [ ] Орки.
|
||||
- [ ] Нежить.
|
||||
- [ ] Таурены.
|
||||
- [ ] Тролли.
|
||||
- [ ] Эльфы крови.
|
||||
- [ ] Отдельная настройка камер для каждого пола.
|
||||
- [ ] Матрица конфликтов расовых элементов с экипировкой.
|
||||
|
||||
**Результат:** единый уровень качества для всех доступных рас.
|
||||
|
||||
### Этап 9. Стабилизация и выпуск
|
||||
|
||||
- [ ] Нагрузочный тест большого скопления игроков.
|
||||
- [ ] Проверка утечек модели/текстур при многократной смене вариантов.
|
||||
- [ ] Проверка холодного кэша WarcraftXLHost.
|
||||
- [ ] Проверка отсутствующих/повреждённых ассетов.
|
||||
- [ ] Версионирование manifest, каталога и протокола.
|
||||
- [ ] Миграция существующих персонажей.
|
||||
- [ ] Rollback-пакет.
|
||||
- [ ] Документация для добавления новой категории/расы.
|
||||
- [ ] Обновление deploy-скрипта и launcher manifest.
|
||||
|
||||
**Результат:** воспроизводимый production-релиз.
|
||||
|
||||
## 17. Матрица тестирования
|
||||
|
||||
### Персонажи
|
||||
|
||||
- каждая раса × каждый пол;
|
||||
- стандартный режим × предатель;
|
||||
- новый персонаж × существующий персонаж;
|
||||
- минимальное × максимальное число выбранных параметров;
|
||||
- случайная комбинация × преднамеренно конфликтная комбинация.
|
||||
|
||||
### Состояния клиента
|
||||
|
||||
- создание;
|
||||
- выбор;
|
||||
- загрузочный экран;
|
||||
- вход в мир;
|
||||
- появление другого игрока;
|
||||
- телепорт;
|
||||
- смерть/дух;
|
||||
- транспорт;
|
||||
- shapeshift/превращение;
|
||||
- парикмахерская;
|
||||
- смена экипировки;
|
||||
- скрытие шлема/плаща;
|
||||
- `/reload`;
|
||||
- переподключение;
|
||||
- выход и повторный вход.
|
||||
|
||||
### Графика
|
||||
|
||||
- native renderer и основной D3D9On12 путь;
|
||||
- разные уровни component texture quality;
|
||||
- включённые/выключенные SSAA, SSAO и AA;
|
||||
- дальние LOD;
|
||||
- тени;
|
||||
- прозрачные и emissive материалы;
|
||||
- несколько одинаковых моделей одновременно;
|
||||
- 4:3, 16:9, 16:10 и разные UI scale.
|
||||
|
||||
### Сеть и сервер
|
||||
|
||||
- потеря/повтор snapshot;
|
||||
- snapshot до появления модели и после неё;
|
||||
- неизвестная версия каталога;
|
||||
- неизвестный option/choice;
|
||||
- запрещённый выбор;
|
||||
- слишком большое сообщение;
|
||||
- быстрое повторное изменение;
|
||||
- одновременное изменение и телепорт;
|
||||
- сохранение при logout/crash;
|
||||
- удаление и восстановление персонажа.
|
||||
|
||||
## 18. Производительность
|
||||
|
||||
Целевые ограничения:
|
||||
|
||||
- не выполнять разбор каталога каждый кадр;
|
||||
- не пересобирать всю модель при изменении независимого аксессуара;
|
||||
- кэшировать разрешённые пути и обработанные modern assets;
|
||||
- не создавать дубликаты одной и той же дочерней M2-модели;
|
||||
- освобождать attachment при удалении/скрытии объекта;
|
||||
- ограничить число одновременных косметических M2 на персонажа;
|
||||
- иметь настройку отключения дальних косметических attachment;
|
||||
- измерять время первой загрузки, потребление RAM/VRAM и frame time;
|
||||
- логирование hot path отключено в release-сборке.
|
||||
|
||||
Предварительный бюджет первой версии:
|
||||
|
||||
- до 6 независимых M2-каналов на персонажа;
|
||||
- не более двух моделей на канал;
|
||||
- отсутствие постоянной работы Lua `OnUpdate`, кроме активной UI-анимации;
|
||||
- пересборка полной внешности только при snapshot, смене расы/пола или восстановлении после ошибки.
|
||||
|
||||
## 19. Безопасность и целостность
|
||||
|
||||
- сервер проверяет все ID и комбинации;
|
||||
- клиент не передаёт пути файлов серверу;
|
||||
- протокол содержит только числовые option/choice ID;
|
||||
- сообщения имеют максимальный размер и максимальное число записей;
|
||||
- сервер нормализует дубликаты option ID;
|
||||
- неизвестные значения не записываются в БД;
|
||||
- стоимость изменения рассчитывается сервером;
|
||||
- unlock condition проверяется сервером;
|
||||
- клиентский каталог не считается доказательством доступности;
|
||||
- журналировать несовпадение версии и попытки недопустимых изменений.
|
||||
|
||||
## 20. Сборка, доставка и откат
|
||||
|
||||
### Сборка
|
||||
|
||||
- закреплённые submodule revisions;
|
||||
- asset manifest с SHA-256;
|
||||
- генерация клиентского и серверного каталога из одного источника;
|
||||
- сборка WarcraftXL Win32 и Host x64;
|
||||
- сборка MPQ;
|
||||
- автоматическая проверка оригинального `Wow.exe`;
|
||||
- формирование launcher manifest.
|
||||
|
||||
### Версионирование
|
||||
|
||||
- `customization_protocol_version`;
|
||||
- `customization_catalog_version`;
|
||||
- `customization_asset_version`;
|
||||
- совместимая версия WarcraftXL runtime.
|
||||
|
||||
### Откат
|
||||
|
||||
- сервер умеет не отправлять расширенные сообщения;
|
||||
- клиент при отсутствии данных показывает стандартную внешность;
|
||||
- отключение модуля не блокирует вход персонажа;
|
||||
- миграция БД не удаляет стандартные параметры;
|
||||
- предыдущие MPQ/runtime хранятся как отдельный release artifact;
|
||||
- неизвестные сохранённые choices не удаляются автоматически без миграции.
|
||||
|
||||
## 21. Оценка трудоёмкости
|
||||
|
||||
Для одного разработчика при наличии подготовленных ассетов:
|
||||
|
||||
| Работа | Оценка |
|
||||
|---|---:|
|
||||
| Адаптация и исправление equip-extension | 4–7 дней |
|
||||
| Независимые косметические каналы | 4–7 дней |
|
||||
| Asset pipeline | 3–5 дней |
|
||||
| Серверное хранение и протокол | 4–7 дней |
|
||||
| Текстурные слои и geoset | 5–10 дней |
|
||||
| Новый UI создания/изменения | 5–8 дней |
|
||||
| Полный вертикальный срез одной расы | 5–10 дней |
|
||||
| Каждая следующая раса | 2–5 дней |
|
||||
| Стабилизация и regression | 7–12 дней |
|
||||
|
||||
Ориентиры:
|
||||
|
||||
- технический прототип одной расы: 1–2 недели;
|
||||
- production-вертикаль людей: 3–5 недель;
|
||||
- все десять рас WotLK: 2–3 месяца;
|
||||
- насыщенность категориями, близкая к retail: 3–6 месяцев.
|
||||
|
||||
Главный переменный фактор — подготовка и проверка ассетов, а не серверная схема.
|
||||
|
||||
## 22. Рекомендуемый первый milestone
|
||||
|
||||
Вертикальный срез для людей обоих полов:
|
||||
|
||||
- штатные пять категорий;
|
||||
- одна текстурная категория: шрамы;
|
||||
- два независимых M2-канала: серьги и аксессуар для волос;
|
||||
- серверное сохранение;
|
||||
- snapshot на экране выбора;
|
||||
- видимость другим игрокам;
|
||||
- изменение через парикмахерскую;
|
||||
- камера по категориям;
|
||||
- корректная работа со шлемом;
|
||||
- один полный набор automated/manual acceptance checks.
|
||||
|
||||
Этот milestone должен подтвердить всю архитектуру до масштабирования на остальные расы.
|
||||
|
||||
## 23. Критерии готовности production-версии
|
||||
|
||||
- [ ] `Wow.exe` идентичен оригиналу по SHA-256.
|
||||
- [ ] Все расширения поставляются через WarcraftXL и MPQ.
|
||||
- [ ] Клиент и сервер используют одинаковую версию каталога.
|
||||
- [ ] Внешность сохраняется после выхода и перезапуска клиента.
|
||||
- [ ] Экран создания, экран выбора и модель в мире совпадают.
|
||||
- [ ] Другие игроки видят подтверждённую сервером внешность.
|
||||
- [ ] Недоступные варианты невозможно применить изменённым клиентом.
|
||||
- [ ] Смена/снятие варианта не оставляет collection M2.
|
||||
- [ ] Нет placeholder-моделей из мусорных строк `ItemDisplayInfo.dbc`.
|
||||
- [ ] Нет заметных утечек памяти при многократном preview.
|
||||
- [ ] Отсутствующий ассет не приводит к падению клиента.
|
||||
- [ ] Все расы и оба пола проходят тестовую матрицу.
|
||||
- [ ] Камера корректно кадрирует низких, средних и высоких персонажей.
|
||||
- [ ] UI работает на 4:3, 16:9, 16:10 и разных UI scale.
|
||||
- [ ] Реализован и проверен rollback.
|
||||
- [ ] Описан процесс добавления новой категории, choice и ассета.
|
||||
|
||||
## 24. Открытые решения перед началом реализации
|
||||
|
||||
- точная ревизия `wxl-equip-extension` для закрепления;
|
||||
- список категорий первого релиза;
|
||||
- исходный retail build для моделей;
|
||||
- набор моделей для вертикального среза людей;
|
||||
- формат единого каталога: JSON, CSV или генератор из SQL/YAML;
|
||||
- транспорт custom packets между WarcraftXL и AzerothCore;
|
||||
- способ передачи snapshots на экране выбора;
|
||||
- политика скрытия аксессуаров под шлемом;
|
||||
- правила доступности и разблокировки вариантов;
|
||||
- использование парикмахерской или отдельного UI изменения внешности;
|
||||
- лимит косметических M2 на одного персонажа;
|
||||
- политика хранения выбранных Blizzard-ассетов и доступа к ним;
|
||||
- минимальная поддерживаемая версия launcher/runtime.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Автоматический импорт mount и pet
|
||||
|
||||
Список современных моделей находится в `custom-creatures.json`. В нём есть две независимые
|
||||
секции:
|
||||
|
||||
```json
|
||||
{
|
||||
"mounts": [
|
||||
"assets/creatures/my_mount"
|
||||
],
|
||||
"pets": [
|
||||
"assets/creatures/my_pet"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Папка должна быть результатом **Raw M2 export** из wow.export и содержать:
|
||||
|
||||
- один основной `.m2`;
|
||||
- `*.manifest.json`;
|
||||
- все упомянутые в manifest текстуры, skin, skeleton, bone и animation-файлы.
|
||||
|
||||
Разрешены абсолютные пути, например `C:/Users/sindo/wow.export/creature/catslime`, но для
|
||||
воспроизводимой сборки папку лучше скопировать внутрь репозитория и указать относительный путь.
|
||||
|
||||
## Что делает сборщик
|
||||
|
||||
При обычном запуске `tool.exe`, `run.ps1` или `deploy.ps1` сборщик:
|
||||
|
||||
1. читает manifest каждой модели и размещает retail M2 со всеми зависимостями в loose-каталоге
|
||||
`Data/Patch-WXL.MPQ`, создавая рядом `.mdx`-алиас для загрузчика WoW 3.3.5, чтобы WarcraftXL всегда
|
||||
преобразовывал MD21 до загрузки клиентом;
|
||||
2. генерирует `WXLFileData.csv` для WarcraftXL;
|
||||
3. дописывает модели в `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc`;
|
||||
4. сохраняет стабильные ID в `custom-creatures.lock.json`;
|
||||
5. для `pets` создаёт companion summon spell и строку skill line `778`;
|
||||
6. кладёт серверные DBC, SQL и таблицу ID в `build/server-dbc`.
|
||||
|
||||
Полный `Spell.dbc` в патч не копируется. WarcraftXL читает компактный
|
||||
`WXLSpellOverrides.tsv` и при открытии DBC добавляет новые spell-записи к штатному файлу клиента.
|
||||
|
||||
Перестановка моделей в списке не меняет уже выданные ID. Lock-файл необходимо хранить в Git.
|
||||
|
||||
## Дополнительные параметры
|
||||
|
||||
Если значений по умолчанию недостаточно, вместо строки используется объект:
|
||||
|
||||
```json
|
||||
{
|
||||
"folder": "assets/creatures/my_mount",
|
||||
"key": "my_mount",
|
||||
"name": "My Mount",
|
||||
"name_ru": "Мой транспорт",
|
||||
"archive": "Creature/MyMount",
|
||||
"model_file": "my_mount.m2",
|
||||
"template_display_id": 2404,
|
||||
"display_scale": 1.0,
|
||||
"model_scale": 1.0,
|
||||
"collision_width": 0.6111,
|
||||
"collision_height": 2.031,
|
||||
"mount_height": 1.8657,
|
||||
"flags": 0
|
||||
}
|
||||
```
|
||||
|
||||
Для `mounts` по умолчанию клонируется верховая лошадь с display ID `2404`, для `pets` —
|
||||
компаньон-кот с display ID `5448`. Шаблон задаёт физические размеры, звук и прочие поля DBC;
|
||||
параметры объекта позволяют их переопределить.
|
||||
|
||||
## Установка на сервер
|
||||
|
||||
Для каждого элемента `pets` сборщик автоматически выдаёт `creatureEntry`, `summonSpellID` и
|
||||
`skillLineAbilityID`. Значения находятся в `build/server-dbc/custom-creatures.csv`.
|
||||
|
||||
1. Скопировать `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` из `build/server-dbc` в
|
||||
серверный каталог `data/dbc`.
|
||||
2. Применить `build/server-dbc/custom-creatures.sql` к `acore_world`.
|
||||
3. Перезапустить worldserver: DBC и таблицы `spell_dbc` загружаются только при старте.
|
||||
4. Для проверки изучить выданный spell командой `.learn <summonSpellID>`.
|
||||
|
||||
SQL создаёт `creature_template`, `creature_model_info`, `creature_template_model`, companion spell и строку skill line.
|
||||
Генерация полноценного mount spell для секции `mounts` остаётся отдельным этапом: mount требует
|
||||
другой spell-шаблон с аурами скорости и `SPELL_AURA_MOUNTED`.
|
||||
@@ -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.
@@ -0,0 +1,535 @@
|
||||
---
|
||||
name: wow-335a-ui
|
||||
description: >
|
||||
Верстка и разработка UI-аддонов для World of Warcraft 3.3.5a (WotLK, build 12340).
|
||||
Используй этот скилл при любой работе с интерфейсом WoW 3.3.5: создание фреймов
|
||||
через XML или Lua CreateFrame(), позиционирование через SetPoint/якоря, слои
|
||||
(BACKGROUND/ARTWORK/OVERLAY/HIGHLIGHT), страты (frameStrata), текстуры, шрифты,
|
||||
кнопки, ScrollFrame, скрипты-события, структура аддона (TOC + XML + Lua),
|
||||
SavedVariables, работа с Blizzard API патча 3.3.5. Также применяй когда речь идёт
|
||||
об AzerothCore, кастомных интерфейсах для WoW-прайват серверов 3.3.5a, Eluna
|
||||
или модификации FrameXML. ОБЯЗАТЕЛЬНО применяй при задачах pixel-perfect переноса
|
||||
дизайна (Figma/PSD/скриншот) в WoW UI: пересчёт координат с учётом UIParent scale,
|
||||
конвертация цветов HEX→RGBA, подбор шрифтов, подготовка текстур (степени двойки,
|
||||
TexCoord-кроп), устранение расхождений между макетом и игровым интерфейсом.
|
||||
---
|
||||
|
||||
# WoW 3.3.5a — Верстка UI (FrameXML / AddOn Development)
|
||||
|
||||
## Ключевые ограничения патча 3.3.5a
|
||||
|
||||
- **API версии**: `## Interface: 30300` (build 12340)
|
||||
- **Lua версия**: Lua 5.1
|
||||
- **Текстуры**: только степени двойки по ширине/высоте (8, 16, 32, 64, 128, 256, 512 px); максимум 512×512
|
||||
- **Нет** `:SetSize()` — только `:SetWidth()` / `:SetHeight()` (или через XML `<Size>`)
|
||||
- **Нет** `hooksecurefunc` для защищённых фреймов без taint
|
||||
- Отсутствуют многие retail-API: нет `C_Timer`, нет `CreateFramePool`, нет `PixelUtil`
|
||||
- Таймеры — через `OnUpdate` с накоплением `elapsed`
|
||||
|
||||
---
|
||||
|
||||
## Pixel-Perfect: перенос дизайна из Figma/PSD в WoW UI
|
||||
|
||||
### Главная проблема: система координат WoW ≠ пиксели экрана
|
||||
|
||||
WoW рисует UI в **виртуальных единицах**, а не в экранных пикселях. Размер виртуального экрана зафиксирован движком на **768 единиц по высоте**, ширина зависит от соотношения сторон:
|
||||
|
||||
| Разрешение | Виртуальная ширина | Виртуальная высота |
|
||||
|-----------|-------------------|-------------------|
|
||||
| 1024×768 | 1024 | 768 |
|
||||
| 1280×960 | 1024 | 768 |
|
||||
| 1920×1080 | 1365 | 768 |
|
||||
| 2560×1440 | 1365 | 768 |
|
||||
|
||||
> **Вывод**: высота UIParent всегда = 768. Всё, что сделано в Figma на 768px по высоте, переносится 1:1 без пересчёта. При другой высоте холста в Figma — нужен коэффициент.
|
||||
|
||||
### Пересчёт координат из Figma в WoW
|
||||
|
||||
```lua
|
||||
-- Формула: wow_value = figma_value * (768 / figma_canvas_height)
|
||||
-- Если макет сделан на 1080px:
|
||||
local SCALE = 768 / 1080 -- ≈ 0.711
|
||||
|
||||
local function px(figma_px)
|
||||
return math.floor(figma_px * SCALE + 0.5) -- округление к ближайшему
|
||||
end
|
||||
|
||||
-- Использование:
|
||||
frame:SetWidth(px(400)) -- figma: 400px → wow: 284
|
||||
frame:SetHeight(px(200)) -- figma: 200px → wow: 142
|
||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
||||
```
|
||||
|
||||
> Если макет 768px в высоту — `SCALE = 1.0`, пересчёт не нужен.
|
||||
|
||||
### UIParent scale и как он ломает пиксели
|
||||
|
||||
Игрок может изменить масштаб UI в настройках. Это сдвигает все координаты.
|
||||
|
||||
```lua
|
||||
-- Получить текущий масштаб UIParent
|
||||
local uiScale = UIParent:GetScale() -- обычно 0.64–1.0
|
||||
|
||||
-- Перевести экранные пиксели в виртуальные единицы WoW
|
||||
local function screenPxToWow(px)
|
||||
local screenH = select(2, GetPhysicalScreenSize()) -- реальный размер экрана
|
||||
return px * (768 / screenH) / uiScale
|
||||
end
|
||||
|
||||
-- Принудительно зафиксировать scale (использовать с умом!)
|
||||
-- UIParent:SetScale(1.0) -- сломает все стандартные фреймы
|
||||
```
|
||||
|
||||
**Рекомендация**: не трогай `UIParent:SetScale()`. Вместо этого рисуй в виртуальных единицах и принимай, что у разных игроков масштаб разный. Для абсолютного pixel-perfect на конкретном разрешении — дай опцию ручного масштабирования фрейма в настройках.
|
||||
|
||||
```lua
|
||||
-- Масштабируй только свой фрейм
|
||||
frame:SetScale(0.85)
|
||||
-- Тогда все дочерние элементы тоже масштабируются
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Цвета: Figma HEX/RGBA → WoW RGBA
|
||||
|
||||
WoW принимает цвета в диапазоне **0.0–1.0** (не 0–255).
|
||||
|
||||
```lua
|
||||
-- Конвертер HEX → WoW RGBA
|
||||
local function hex(str, a)
|
||||
str = str:gsub("#", "")
|
||||
local r = tonumber(str:sub(1,2), 16) / 255
|
||||
local g = tonumber(str:sub(3,4), 16) / 255
|
||||
local b = tonumber(str:sub(5,6), 16) / 255
|
||||
return r, g, b, a or 1.0
|
||||
end
|
||||
|
||||
-- Использование:
|
||||
tex:SetVertexColor(hex("#FF6A00")) -- непрозрачный
|
||||
tex:SetVertexColor(hex("#FF6A00", 0.8)) -- 80% непрозрачность
|
||||
fs:SetTextColor(hex("#FFD100"))
|
||||
frame:SetBackdropColor(hex("#1A1A2E", 0.95))
|
||||
frame:SetBackdropBorderColor(hex("#4A90D9"))
|
||||
```
|
||||
|
||||
**Figma opacity → WoW alpha**: значение из Figma (например, 75%) делится на 100 → `0.75`.
|
||||
|
||||
```lua
|
||||
-- Figma: opacity 60%
|
||||
frame:SetAlpha(0.60)
|
||||
tex:SetAlpha(0.60)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Шрифты: как приблизить к дизайну
|
||||
|
||||
WoW 3.3.5a поддерживает только `.ttf` шрифты, помещённые в папку аддона.
|
||||
|
||||
```lua
|
||||
-- Кастомный шрифт из аддона
|
||||
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", 14, "OUTLINE")
|
||||
-- Флаги: "", "OUTLINE", "THICKOUTLINE", "MONOCHROME", "OUTLINE,MONOCHROME"
|
||||
```
|
||||
|
||||
**Соответствие Figma → WoW размер шрифта**:
|
||||
|
||||
| Figma (макет 1080px) | WoW pt (при SCALE=0.711) |
|
||||
|---------------------|--------------------------|
|
||||
| 12 px | 9 |
|
||||
| 14 px | 10 |
|
||||
| 16 px | 11 |
|
||||
| 18 px | 13 |
|
||||
| 24 px | 17 |
|
||||
| 32 px | 23 |
|
||||
|
||||
```lua
|
||||
-- Автоматический пересчёт размера шрифта
|
||||
local function fontSize(figma_pt)
|
||||
return math.max(8, math.floor(figma_pt * SCALE + 0.5))
|
||||
end
|
||||
fs:SetFont("Fonts\\FRIZQT__.TTF", fontSize(18), "OUTLINE")
|
||||
```
|
||||
|
||||
> **Минимальный читаемый размер в WoW**: 8 pt. Ниже — нечитаемо.
|
||||
|
||||
**Встроенные шрифты WoW 3.3.5a**:
|
||||
```
|
||||
Fonts\FRIZQT__.TTF — основной интерфейсный (без кириллицы!)
|
||||
Fonts\MORPHEUS.ttf — декоративный (заголовки)
|
||||
Fonts\ARIALN.TTF — Arial Narrow
|
||||
Fonts\skurri.ttf — шрифт повреждений
|
||||
```
|
||||
|
||||
> **Кириллица**: встроенные шрифты WoW не поддерживают кириллицу. Положи в аддон `.ttf` с кириллицей (например, PT Sans, Roboto) и подключи явно.
|
||||
|
||||
---
|
||||
|
||||
### Текстуры: подготовка под ограничения движка
|
||||
|
||||
#### Требования к текстурам WoW 3.3.5a
|
||||
- Ширина и высота — **степени двойки**: 2, 4, 8, 16, 32, 64, 128, 256, 512
|
||||
- Максимум: **512×512 px**
|
||||
- Формат: `.tga` (RGBA, 32-bit) или `.blp`
|
||||
- Текстура не обязана быть квадратной: 512×64 — ок, 256×128 — ок
|
||||
|
||||
#### Как уместить большой элемент дизайна
|
||||
|
||||
Если элемент из Figma больше 512px — разрезай на части:
|
||||
|
||||
```
|
||||
[512×256] [512×256] ← верхняя половина (левая + правая)
|
||||
[512×256] [512×256] ← нижняя половина
|
||||
```
|
||||
|
||||
```lua
|
||||
-- Сборка из 4 текстур
|
||||
local tl = frame:CreateTexture(nil, "BACKGROUND")
|
||||
tl:SetTexture("Interface\\AddOns\\MyAddon\\bg_tl")
|
||||
tl:SetPoint("TOPLEFT", frame, "TOPLEFT")
|
||||
tl:SetWidth(256); tl:SetHeight(128)
|
||||
|
||||
local tr = frame:CreateTexture(nil, "BACKGROUND")
|
||||
tr:SetTexture("Interface\\AddOns\\MyAddon\\bg_tr")
|
||||
tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT")
|
||||
tr:SetWidth(256); tr:SetHeight(128)
|
||||
-- ... и так далее
|
||||
```
|
||||
|
||||
#### TexCoord — показать только часть текстуры
|
||||
|
||||
Если в одном файле несколько элементов (спрайт-лист), используй `SetTexCoord`:
|
||||
|
||||
```lua
|
||||
-- SetTexCoord(left, right, top, bottom) — от 0.0 до 1.0
|
||||
-- Пример: иконка занимает правый нижний квадрант 256×256 атласа
|
||||
tex:SetTexCoord(0.5, 1.0, 0.5, 1.0)
|
||||
|
||||
-- Формула для ячейки в сетке (col/row с нуля):
|
||||
local cols, rows = 4, 4
|
||||
local col, row = 2, 1 -- третья колонка, вторая строка
|
||||
tex:SetTexCoord(
|
||||
col/cols, -- left
|
||||
(col+1)/cols, -- right
|
||||
row/rows, -- top
|
||||
(row+1)/rows -- bottom
|
||||
)
|
||||
```
|
||||
|
||||
> **Важно**: при SetTexCoord текстура тайлится внутри заданного Size фрейма. SetAllPoints растягивает — SetWidth/SetHeight + SetPoint = точный размер.
|
||||
|
||||
---
|
||||
|
||||
### Позиционирование: от Figma к SetPoint
|
||||
|
||||
В Figma позиция элемента — это X,Y от левого верхнего угла холста (или родителя). В WoW — якорная система.
|
||||
|
||||
**Алгоритм переноса**:
|
||||
|
||||
1. Определи ближайший угол/сторону в Figma (от чего считать)
|
||||
2. Вычисли отступ от этой стороны
|
||||
3. Пересчитай через `px()`
|
||||
|
||||
```lua
|
||||
-- Figma (холст 1365×768):
|
||||
-- Элемент: X=50, Y=30, W=200, H=100
|
||||
-- → привязка от TOPLEFT UIParent
|
||||
|
||||
frame:SetWidth(px(200))
|
||||
frame:SetHeight(px(100))
|
||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
||||
-- ^^^ ^^^
|
||||
-- X offset Y offset (отрицательный! вниз)
|
||||
```
|
||||
|
||||
> **Знаки осей WoW**: X растёт вправо (+), Y растёт вверх (+). Отступ вниз от TOPLEFT — **отрицательный**.
|
||||
|
||||
| Figma anchor | WoW SetPoint |
|
||||
|-------------|-------------|
|
||||
| Top-Left | `"TOPLEFT", UIParent, "TOPLEFT", x, -y` |
|
||||
| Top-Right | `"TOPRIGHT", UIParent, "TOPRIGHT", -x, -y` |
|
||||
| Bottom-Left | `"BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y` |
|
||||
| Bottom-Right | `"BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -x, y` |
|
||||
| Center | `"CENTER", UIParent, "CENTER", x - W/2_canvas, y` |
|
||||
|
||||
---
|
||||
|
||||
### Вспомогательный модуль: px-утилиты (вставь в начало Lua)
|
||||
|
||||
```lua
|
||||
-- PixelHelper — pixel-perfect вёрстка из Figma
|
||||
local PH = {}
|
||||
|
||||
PH.CANVAS_HEIGHT = 768 -- высота холста в Figma (измени если другая!)
|
||||
|
||||
function PH.scale()
|
||||
return 768 / PH.CANVAS_HEIGHT
|
||||
end
|
||||
|
||||
-- Пересчёт px из Figma → WoW единицы
|
||||
function PH.px(v)
|
||||
return math.floor(v * PH.scale() + 0.5)
|
||||
end
|
||||
|
||||
-- Цвет из HEX (#RRGGBB или #RRGGBBAA)
|
||||
function PH.color(hex, alpha)
|
||||
hex = hex:gsub("#","")
|
||||
local r = tonumber(hex:sub(1,2),16)/255
|
||||
local g = tonumber(hex:sub(3,4),16)/255
|
||||
local b = tonumber(hex:sub(5,6),16)/255
|
||||
local a = alpha or (hex:len() == 8 and tonumber(hex:sub(7,8),16)/255) or 1
|
||||
return r, g, b, a
|
||||
end
|
||||
|
||||
-- Размер шрифта из Figma → WoW
|
||||
function PH.font(pt)
|
||||
return math.max(8, math.floor(pt * PH.scale() + 0.5))
|
||||
end
|
||||
|
||||
-- Установка якоря с автопересчётом
|
||||
function PH.point(frame, point, relativeTo, relPoint, x, y)
|
||||
frame:SetPoint(point, relativeTo, relPoint, PH.px(x or 0), PH.px(y or 0))
|
||||
end
|
||||
|
||||
-- Размер фрейма из Figma
|
||||
function PH.size(frame, w, h)
|
||||
frame:SetWidth(PH.px(w))
|
||||
frame:SetHeight(PH.px(h))
|
||||
end
|
||||
|
||||
-- Экспорт глобально (или оставь локальным)
|
||||
MyAddon_PH = PH
|
||||
```
|
||||
|
||||
**Использование**:
|
||||
```lua
|
||||
local px = MyAddon_PH.px
|
||||
local color = MyAddon_PH.color
|
||||
local pxfont = MyAddon_PH.font
|
||||
|
||||
MyAddon_PH.size(frame, 400, 250)
|
||||
MyAddon_PH.point(frame, "TOPLEFT", UIParent, "TOPLEFT", 50, -30)
|
||||
frame:SetBackdropColor(color("#1A1A2E", 0.95))
|
||||
fs:SetTextColor(color("#FFD100"))
|
||||
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", pxfont(16), "OUTLINE")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Чеклист pixel-perfect переноса
|
||||
|
||||
- [ ] Холст Figma = 768px в высоту (или задан `PH.CANVAS_HEIGHT`)
|
||||
- [ ] Все размеры и отступы пересчитаны через `px()`
|
||||
- [ ] Цвета конвертированы через `color()`
|
||||
- [ ] Текстуры нарезаны до 512×512, размеры кратны степени двойки
|
||||
- [ ] Шрифты `.ttf` подключены из папки аддона (если кириллица или кастомный шрифт)
|
||||
- [ ] Размеры шрифтов пересчитаны через `font()`
|
||||
- [ ] Y-отступы от верхних якорей — **отрицательные**
|
||||
- [ ] Проверено на scale UIParent ≠ 1.0 (изменить в настройках игры для теста)
|
||||
- [ ] Текстуры без SetAllPoints — точный SetWidth/SetHeight + SetPoint
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
MyAddon/
|
||||
├── MyAddon.toc ← обязателен, имя файла = имя папки
|
||||
├── MyAddon.xml ← верстка фреймов (опционально)
|
||||
└── MyAddon.lua ← логика
|
||||
```
|
||||
|
||||
### TOC-файл (3.3.5a)
|
||||
|
||||
```toc
|
||||
## Interface: 30300
|
||||
## Title: My Addon
|
||||
## Notes: Описание аддона
|
||||
## Author: Ilyas
|
||||
## Version: 1.0
|
||||
## SavedVariables: MyAddonDB
|
||||
|
||||
MyAddon.xml
|
||||
MyAddon.lua
|
||||
```
|
||||
|
||||
> Файлы загружаются **в порядке перечисления**. XML раньше Lua — фреймы существуют к моменту выполнения скриптов.
|
||||
|
||||
---
|
||||
|
||||
## XML-верстка
|
||||
|
||||
### Минимальный шаблон XML-файла
|
||||
|
||||
```xml
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
|
||||
<Script file="MyAddon.lua"/>
|
||||
|
||||
<Frame name="MyAddon_Frame" parent="UIParent" toplevel="true" enableMouse="true" movable="true">
|
||||
<Size>
|
||||
<AbsDimension x="300" y="200"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
|
||||
edgeFile="Interface\DialogFrame\UI-DialogBox-Border"
|
||||
tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize><AbsValue val="32"/></TileSize>
|
||||
<EdgeSize><AbsValue val="32"/></EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormalLarge" text="Заголовок">
|
||||
<Anchors>
|
||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentClose" inherits="UIPanelCloseButton">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor>
|
||||
</Anchors>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>MyAddon_Frame_OnLoad(self)</OnLoad>
|
||||
<OnMouseDown>self:StartMoving()</OnMouseDown>
|
||||
<OnMouseUp>self:StopMovingOrSizing()</OnMouseUp>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
</Ui>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Точки якоря (FRAMEPOINT)
|
||||
|
||||
| Точка | Расположение |
|
||||
|-------|-------------|
|
||||
| `TOPLEFT` | Верхний левый угол |
|
||||
| `TOP` | Верхний центр |
|
||||
| `TOPRIGHT` | Верхний правый угол |
|
||||
| `LEFT` | Левый центр |
|
||||
| `CENTER` | Центр |
|
||||
| `RIGHT` | Правый центр |
|
||||
| `BOTTOMLEFT` | Нижний левый угол |
|
||||
| `BOTTOM` | Нижний центр |
|
||||
| `BOTTOMRIGHT` | Нижний правый угол |
|
||||
|
||||
### SetPoint через Lua
|
||||
|
||||
```lua
|
||||
-- frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
|
||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, -10)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetPoint("BOTTOMRIGHT", anotherFrame, "TOPRIGHT", 0, 5)
|
||||
|
||||
-- Сброс якорей перед переустановкой
|
||||
frame:ClearAllPoints()
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER")
|
||||
```
|
||||
|
||||
> **Правило**: фрейм должен иметь достаточно якорей для однозначного определения позиции **и** размера. Либо Size + один якорь, либо два якоря задающих оба измерения.
|
||||
|
||||
---
|
||||
|
||||
## Слои и страты
|
||||
|
||||
### Draw Layers (внутри фрейма, сверху вниз по видимости)
|
||||
|
||||
```
|
||||
BACKGROUND (0) → BORDER (1) → ARTWORK (2) → OVERLAY (3) → HIGHLIGHT (4)
|
||||
```
|
||||
|
||||
- `HIGHLIGHT` — автоматически показывается при наведении мыши (требует `enableMouse="true"`)
|
||||
- FontString всегда рисуется **поверх** Texture в том же слое
|
||||
- Порядок текстур в одном слое **не гарантирован** → разноси по слоям
|
||||
|
||||
### Frame Strata (между фреймами)
|
||||
|
||||
```
|
||||
WORLD → BACKGROUND → LOW → MEDIUM → HIGH → DIALOG → FULLSCREEN → FULLSCREEN_DIALOG → TOOLTIP
|
||||
```
|
||||
|
||||
```lua
|
||||
frame:SetFrameStrata("DIALOG")
|
||||
frame:SetFrameLevel(5) -- внутри страты (0–255)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Виджеты, события, Blizzard API
|
||||
|
||||
> Полный справочник — в `references/widgets-and-api.md`.
|
||||
> Читай его когда нужны: типы фреймов, Button/EditBox/ScrollFrame/StatusBar, события OnEvent/OnUpdate, SavedVariables, пути к текстурам, типичные ошибки.
|
||||
|
||||
---
|
||||
|
||||
## Шаблонный аддон «с нуля» (минимальный рабочий пример)
|
||||
|
||||
**MyAddon.toc**
|
||||
```toc
|
||||
## Interface: 30300
|
||||
## Title: MyAddon
|
||||
## SavedVariables: MyAddonDB
|
||||
MyAddon.lua
|
||||
```
|
||||
|
||||
**MyAddon.lua**
|
||||
```lua
|
||||
local ADDON_NAME = "MyAddon"
|
||||
|
||||
local f = CreateFrame("Frame", ADDON_NAME.."_Frame", UIParent)
|
||||
f:SetWidth(250)
|
||||
f:SetHeight(150)
|
||||
f:SetPoint("CENTER")
|
||||
f:SetFrameStrata("DIALOG")
|
||||
f:SetMovable(true)
|
||||
f:EnableMouse(true)
|
||||
f:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = {left=11, right=12, top=12, bottom=11}
|
||||
})
|
||||
f:Hide()
|
||||
|
||||
-- Заголовок
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||
title:SetPoint("TOP", f, "TOP", 0, -12)
|
||||
title:SetText("My Addon")
|
||||
|
||||
-- Закрытие
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
|
||||
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 4, 4)
|
||||
close:SetScript("OnClick", function() f:Hide() end)
|
||||
|
||||
-- Перетаскивание
|
||||
f:SetScript("OnMouseDown", function(self, btn)
|
||||
if btn == "LeftButton" then self:StartMoving() end
|
||||
end)
|
||||
f:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)
|
||||
|
||||
-- Инициализация
|
||||
f:RegisterEvent("ADDON_LOADED")
|
||||
f:SetScript("OnEvent", function(self, event, name)
|
||||
if event == "ADDON_LOADED" and name == ADDON_NAME then
|
||||
MyAddonDB = MyAddonDB or {}
|
||||
print("|cff00ff00"..ADDON_NAME.."|r загружен. /myaddon")
|
||||
end
|
||||
end)
|
||||
|
||||
-- Слэш-команда
|
||||
SLASH_MYADDON1 = "/myaddon"
|
||||
SlashCmdList["MYADDON"] = function()
|
||||
if f:IsShown() then f:Hide() else f:Show() end
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,256 @@
|
||||
# WoW 3.3.5a — Виджеты, события и Blizzard API
|
||||
|
||||
## Создание фреймов через Lua
|
||||
|
||||
### CreateFrame
|
||||
|
||||
```lua
|
||||
-- CreateFrame(frameType, name, parent, template)
|
||||
local f = CreateFrame("Frame", "MyAddon_MainFrame", UIParent)
|
||||
f:SetWidth(300)
|
||||
f:SetHeight(200)
|
||||
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
f:SetFrameStrata("DIALOG")
|
||||
f:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
||||
})
|
||||
f:SetBackdropColor(0, 0, 0, 0.8)
|
||||
```
|
||||
|
||||
### Типы фреймов (frameType)
|
||||
|
||||
| Тип | Назначение |
|
||||
|-----|-----------|
|
||||
| `Frame` | Базовый контейнер |
|
||||
| `Button` | Кнопка |
|
||||
| `EditBox` | Поле ввода текста |
|
||||
| `ScrollFrame` | Прокручиваемая область |
|
||||
| `StatusBar` | Полоска (HP, мана, XP) |
|
||||
| `Cooldown` | Кулдаун-свип на иконке |
|
||||
| `GameTooltip` | Тултип |
|
||||
| `MessageFrame` | Прокручиваемые сообщения |
|
||||
| `Model` | 3D-модель |
|
||||
| `MovieFrame` | Видео |
|
||||
|
||||
---
|
||||
|
||||
## Текстуры
|
||||
|
||||
```lua
|
||||
local tex = frame:CreateTexture(nil, "ARTWORK")
|
||||
tex:SetTexture("Interface\\AddOns\\MyAddon\\images\\icon")
|
||||
tex:SetAllPoints(frame)
|
||||
tex:SetWidth(32)
|
||||
tex:SetHeight(32)
|
||||
tex:SetPoint("CENTER", frame, "CENTER")
|
||||
tex:SetVertexColor(1, 0.5, 0, 1)
|
||||
tex:SetAlpha(0.8)
|
||||
|
||||
-- Вырезка части текстуры
|
||||
tex:SetTexCoord(0, 0.5, 0, 0.5)
|
||||
```
|
||||
|
||||
## FontString (текст)
|
||||
|
||||
```lua
|
||||
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
fs:SetPoint("CENTER", frame, "CENTER")
|
||||
fs:SetText("Привет, Азерот!")
|
||||
fs:SetTextColor(1, 1, 0, 1)
|
||||
fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
|
||||
```
|
||||
|
||||
### Стандартные шаблоны шрифтов
|
||||
|
||||
| Шаблон | Применение |
|
||||
|--------|-----------|
|
||||
| `GameFontNormal` | Обычный текст интерфейса |
|
||||
| `GameFontNormalLarge` | Заголовки |
|
||||
| `GameFontNormalSmall` | Мелкий текст |
|
||||
| `GameFontHighlight` | Подсвеченный |
|
||||
| `GameFontDisable` | Неактивный (серый) |
|
||||
| `NumberFontNormal` | Числа |
|
||||
| `ChatFontNormal` | Чат |
|
||||
|
||||
---
|
||||
|
||||
## Button
|
||||
|
||||
```lua
|
||||
local btn = CreateFrame("Button", "MyAddon_Btn", frame, "UIPanelButtonTemplate")
|
||||
btn:SetWidth(80)
|
||||
btn:SetHeight(22)
|
||||
btn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 10)
|
||||
btn:SetText("Нажми")
|
||||
btn:SetScript("OnClick", function(self)
|
||||
print("Нажато!")
|
||||
end)
|
||||
|
||||
btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-Button-Up")
|
||||
btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-Button-Down")
|
||||
btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
|
||||
```
|
||||
|
||||
### Шаблоны кнопок
|
||||
|
||||
| Шаблон | Вид |
|
||||
|--------|-----|
|
||||
| `UIPanelButtonTemplate` | Стандартная кнопка интерфейса |
|
||||
| `UIPanelCloseButton` | Крестик закрытия окна |
|
||||
| `UIMenuButtonStretchTemplate` | Растягиваемая кнопка меню |
|
||||
| `SecureActionButtonTemplate` | Для действий в бою (protected) |
|
||||
|
||||
---
|
||||
|
||||
## ScrollFrame
|
||||
|
||||
```lua
|
||||
local scroll = CreateFrame("ScrollFrame", "MyAddon_Scroll", frame, "UIPanelScrollFrameTemplate")
|
||||
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
|
||||
scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 8)
|
||||
|
||||
local child = CreateFrame("Frame", "MyAddon_ScrollChild", scroll)
|
||||
child:SetWidth(scroll:GetWidth())
|
||||
child:SetHeight(400)
|
||||
scroll:SetScrollChild(child)
|
||||
```
|
||||
|
||||
## EditBox
|
||||
|
||||
```lua
|
||||
local eb = CreateFrame("EditBox", "MyAddon_Input", frame, "InputBoxTemplate")
|
||||
eb:SetWidth(200)
|
||||
eb:SetHeight(20)
|
||||
eb:SetPoint("CENTER", frame, "CENTER")
|
||||
eb:SetAutoFocus(false)
|
||||
eb:SetMaxLetters(255)
|
||||
eb:SetScript("OnEnterPressed", function(self)
|
||||
local text = self:GetText()
|
||||
self:ClearFocus()
|
||||
end)
|
||||
```
|
||||
|
||||
## StatusBar
|
||||
|
||||
```lua
|
||||
local bar = CreateFrame("StatusBar", nil, frame)
|
||||
bar:SetWidth(200)
|
||||
bar:SetHeight(16)
|
||||
bar:SetPoint("CENTER", frame, "CENTER")
|
||||
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
||||
bar:SetStatusBarColor(0, 1, 0, 1)
|
||||
bar:SetMinMaxValues(0, 100)
|
||||
bar:SetValue(75)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Скрипты-события
|
||||
|
||||
```lua
|
||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
frame:RegisterEvent("CHAT_MSG_SAY")
|
||||
|
||||
frame:SetScript("OnEvent", function(self, event, ...)
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
-- вход в мир
|
||||
elseif event == "CHAT_MSG_SAY" then
|
||||
local msg, author = ...
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
### Ключевые события
|
||||
|
||||
| Событие | Когда |
|
||||
|---------|-------|
|
||||
| `PLAYER_ENTERING_WORLD` | Вход в мир / reload |
|
||||
| `ADDON_LOADED` | Аддон загружен (arg1 == имя) |
|
||||
| `PLAYER_LOGIN` | После входа |
|
||||
| `PLAYER_LOGOUT` | При выходе |
|
||||
| `VARIABLES_LOADED` | SavedVariables загружены |
|
||||
| `UI_ERROR_MESSAGE` | Ошибка интерфейса |
|
||||
|
||||
### OnUpdate — таймер
|
||||
|
||||
```lua
|
||||
local elapsed_total = 0
|
||||
frame:SetScript("OnUpdate", function(self, elapsed)
|
||||
elapsed_total = elapsed_total + elapsed
|
||||
if elapsed_total >= 1.0 then
|
||||
elapsed_total = 0
|
||||
-- действие каждую секунду
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SavedVariables
|
||||
|
||||
```lua
|
||||
-- В TOC: ## SavedVariables: MyAddonDB
|
||||
frame:RegisterEvent("ADDON_LOADED")
|
||||
frame:SetScript("OnEvent", function(self, event, addonName)
|
||||
if event == "ADDON_LOADED" and addonName == "MyAddon" then
|
||||
MyAddonDB = MyAddonDB or { position = { "CENTER", 0, 0 } }
|
||||
local p = MyAddonDB.position
|
||||
self:ClearAllPoints()
|
||||
self:SetPoint(p[1], UIParent, p[1], p[2], p[3])
|
||||
end
|
||||
end)
|
||||
|
||||
frame:SetScript("OnMouseUp", function(self)
|
||||
self:StopMovingOrSizing()
|
||||
local point, _, _, x, y = self:GetPoint()
|
||||
MyAddonDB.position = { point, x, y }
|
||||
end)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Полезные Blizzard-утилиты (3.3.5a)
|
||||
|
||||
```lua
|
||||
_G["MyAddon_Frame"]:Show()
|
||||
IsAddOnLoaded("MyAddon")
|
||||
UnitName("player")
|
||||
UnitClass("target")
|
||||
UnitHealth("player"), UnitHealthMax("player")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Красный текст|r")
|
||||
print("Белый текст")
|
||||
UnitAffectingCombat("player")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Часто используемые пути к текстурам Blizzard
|
||||
|
||||
```
|
||||
Interface\DialogFrame\UI-DialogBox-Background
|
||||
Interface\DialogFrame\UI-DialogBox-Border
|
||||
Interface\DialogFrame\UI-DialogBox-Gold-Background
|
||||
Interface\Tooltips\UI-Tooltip-Background
|
||||
Interface\Tooltips\UI-Tooltip-Border
|
||||
Interface\Buttons\UI-Panel-Button-Up
|
||||
Interface\Buttons\UI-Panel-Button-Down
|
||||
Interface\Icons\INV_Misc_QuestionMark
|
||||
Interface\TargetingFrame\UI-StatusBar
|
||||
Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Типичные ошибки и решения
|
||||
|
||||
| Ошибка | Причина | Решение |
|
||||
|--------|---------|---------|
|
||||
| `attempt to index global 'X' (nil)` | Фрейм не создан | Проверь порядок файлов в TOC |
|
||||
| `SetPoint anchor family connection` | Циклические якоря | Переписать цепочку якорей |
|
||||
| `Script ran too long` | Бесконечный цикл OnUpdate | Early return или счётчик |
|
||||
| Текстура не отображается | Размер не кратен 2 / >512px | Пересохрани в TGA/BLP |
|
||||
| `bad argument #1 (string expected)` | nil вместо строки | `if val then` |
|
||||
| Frame не кликается | Нет `enableMouse="true"` | Добавить атрибут |
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,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"
|
||||
@@ -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])
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"base_dbc_dir": "assets/dbc/3.3.5a",
|
||||
"model_data_id_start": 50000,
|
||||
"display_info_id_start": 50000,
|
||||
"creature_entry_id_start": 5000000,
|
||||
"summon_spell_id_start": 110000,
|
||||
"skill_line_ability_id_start": 30000,
|
||||
"defaults": {
|
||||
"mount_template_display_id": 2404,
|
||||
"pet_template_display_id": 5448
|
||||
},
|
||||
"mounts": [],
|
||||
"pets": []
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"models": {
|
||||
"catslime": {
|
||||
"model_data_id": 50000,
|
||||
"display_info_id": 50000,
|
||||
"creature_entry": 5000000,
|
||||
"summon_spell_id": 110000,
|
||||
"skill_line_ability_id": 30000
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,64 @@
|
||||
// Minimal D3D9 forwarding proxy for loading WarcraftXL without editing Wow.exe.
|
||||
|
||||
#include <windows.h>
|
||||
#include <d3d9.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
using Create9Fn = IDirect3D9* (WINAPI*)(UINT);
|
||||
using Create9ExFn = HRESULT (WINAPI*)(UINT, IDirect3D9Ex**);
|
||||
|
||||
HMODULE g_systemD3D9 = nullptr;
|
||||
Create9Fn g_create9 = nullptr;
|
||||
Create9ExFn g_create9Ex = nullptr;
|
||||
|
||||
void ResolveSystemD3D9()
|
||||
{
|
||||
if (g_systemD3D9) return;
|
||||
char path[MAX_PATH] = {};
|
||||
const UINT length = GetSystemDirectoryA(path, MAX_PATH);
|
||||
if (!length || length >= MAX_PATH - 10) return;
|
||||
lstrcatA(path, "\\d3d9.dll");
|
||||
g_systemD3D9 = LoadLibraryA(path);
|
||||
if (!g_systemD3D9) return;
|
||||
g_create9 = reinterpret_cast<Create9Fn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9"));
|
||||
g_create9Ex = reinterpret_cast<Create9ExFn>(GetProcAddress(g_systemD3D9, "Direct3DCreate9Ex"));
|
||||
}
|
||||
|
||||
void LoadWarcraftXL()
|
||||
{
|
||||
static bool attempted = false;
|
||||
if (attempted) return;
|
||||
attempted = true;
|
||||
LoadLibraryA("WarcraftXL.dll");
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" IDirect3D9* WINAPI Direct3DCreate9(UINT sdkVersion)
|
||||
{
|
||||
ResolveSystemD3D9();
|
||||
LoadWarcraftXL();
|
||||
return g_create9 ? g_create9(sdkVersion) : nullptr;
|
||||
}
|
||||
|
||||
extern "C" HRESULT WINAPI Direct3DCreate9Ex(UINT sdkVersion, IDirect3D9Ex** output)
|
||||
{
|
||||
ResolveSystemD3D9();
|
||||
LoadWarcraftXL();
|
||||
if (g_create9Ex) return g_create9Ex(sdkVersion, output);
|
||||
if (output) *output = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
// Legacy compatibility exports retained for the recovery proxy. WarcraftXL 1.1
|
||||
// uses native D3D9 and ignores the former D3D9On12 bridge.
|
||||
extern "C" void* WxlD3D12Device() { return nullptr; }
|
||||
extern "C" void* WxlD3D12Queue() { return nullptr; }
|
||||
extern "C" void WxlD3D12DrainDebug() {}
|
||||
extern "C" void WxlSetSsaaFactor(float) {}
|
||||
extern "C" float WxlGetSsaaFactor() { return 1.0f; }
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID)
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
LIBRARY d3d9
|
||||
EXPORTS
|
||||
Direct3DCreate9
|
||||
Direct3DCreate9Ex
|
||||
WxlD3D12Device
|
||||
WxlD3D12Queue
|
||||
WxlD3D12DrainDebug
|
||||
WxlSetSsaaFactor
|
||||
WxlGetSsaaFactor
|
||||
@@ -1,339 +0,0 @@
|
||||
"""
|
||||
Wow.exe binary patcher for MoonWell custom features.
|
||||
|
||||
Patches:
|
||||
1. Script_CreateCharacter: reads lua_tonumber(L, 2) as outfitId,
|
||||
stores to global, passes to CMSG_CHAR_CREATE packet instead of 0.
|
||||
2. GetCharacterInfo: adds 11th return value — charFlags as a Lua number,
|
||||
read from struct_offset +0x170 (same field as ghost flag 0x2000).
|
||||
Lua checks bit 0x40000000 (CHARACTER_FLAG_UNK31) for traitor status.
|
||||
|
||||
Usage:
|
||||
python make_modes_in_exe.py <path_to_Wow.exe>
|
||||
|
||||
A backup (Wow.exe.bak) is created before patching.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PE helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _u16(data, off):
|
||||
return struct.unpack_from("<H", data, off)[0]
|
||||
|
||||
def _u32(data, off):
|
||||
return struct.unpack_from("<I", data, off)[0]
|
||||
|
||||
|
||||
def parse_pe_sections(data):
|
||||
if data[0:2] != b"MZ":
|
||||
raise ValueError("Not a valid PE file (missing MZ)")
|
||||
pe_off = _u32(data, 0x3C)
|
||||
if data[pe_off:pe_off + 4] != b"PE\x00\x00":
|
||||
raise ValueError("Not a valid PE file (missing PE signature)")
|
||||
coff = pe_off + 4
|
||||
num_secs = _u16(data, coff + 2)
|
||||
opt_size = _u16(data, coff + 16)
|
||||
opt = coff + 20
|
||||
magic = _u16(data, opt)
|
||||
if magic == 0x10b:
|
||||
image_base = _u32(data, opt + 28)
|
||||
elif magic == 0x20b:
|
||||
image_base = struct.unpack_from("<Q", data, opt + 24)[0]
|
||||
else:
|
||||
raise ValueError(f"Unknown PE magic: 0x{magic:04x}")
|
||||
sec_table = opt + opt_size
|
||||
sections = []
|
||||
for i in range(num_secs):
|
||||
o = sec_table + i * 40
|
||||
name = data[o:o + 8].rstrip(b"\x00").decode("ascii", errors="replace")
|
||||
virt_size = _u32(data, o + 8)
|
||||
virt_addr = _u32(data, o + 12)
|
||||
raw_size = _u32(data, o + 16)
|
||||
raw_off = _u32(data, o + 20)
|
||||
sections.append((name, virt_addr, virt_size, raw_off, raw_size))
|
||||
return image_base, sections
|
||||
|
||||
|
||||
def va_to_offset(va, sections, image_base):
|
||||
rva = va - image_base
|
||||
for name, va0, vs, ro, _ in sections:
|
||||
if va0 <= rva < va0 + vs:
|
||||
return ro + (rva - va0)
|
||||
raise ValueError(f"VA 0x{va:08x} not in any section")
|
||||
|
||||
|
||||
def offset_to_va(off, sections, image_base):
|
||||
for name, va0, _, ro, rs in sections:
|
||||
if ro <= off < ro + rs:
|
||||
return image_base + va0 + (off - ro)
|
||||
raise ValueError(f"File offset 0x{off:08x} not in any section")
|
||||
|
||||
|
||||
def find_cave(data, sections, image_base, size, search_from_va):
|
||||
"""Return VA of the first run of >= size consecutive CC bytes after search_from_va."""
|
||||
text = next((s for s in sections if s[0] == ".text"), None)
|
||||
if not text is None:
|
||||
_, va0, vs, ro, rs = text
|
||||
else:
|
||||
raise ValueError("No .text section")
|
||||
|
||||
rva_start = max(search_from_va - image_base, va0)
|
||||
off_start = ro + (rva_start - va0)
|
||||
off_end = ro + min(vs, rs)
|
||||
|
||||
i = off_start
|
||||
while i <= off_end - size:
|
||||
if data[i] != 0xcc:
|
||||
i += 1
|
||||
continue
|
||||
j = i
|
||||
while j < off_end and data[j] == 0xcc:
|
||||
j += 1
|
||||
if j - i >= size:
|
||||
return offset_to_va(i, sections, image_base)
|
||||
i = j
|
||||
raise ValueError(f"No {size}-byte code cave found from VA 0x{search_from_va:08x}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch 1 & 2: outfitId in CMSG_CHAR_CREATE (fixed addresses)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_FIXED_PATCHES = [
|
||||
(
|
||||
0x004e0c60,
|
||||
bytes.fromhex(
|
||||
"558bec568b75086a0156e8f1d23600"
|
||||
"83c40885c0740d6a006a0156e860d4"
|
||||
"360083c40c6a006a0156e853d43600"
|
||||
"50e8edf6ffff83c41033c05e5dc3cc"
|
||||
"cccccccc"
|
||||
),
|
||||
bytes.fromhex(
|
||||
"558bec568b75086a0256e8c1d33600"
|
||||
"db1c245883c404a221 42ac006a006a"
|
||||
"0156e85bd4360050e8f5f6ffff83c4"
|
||||
"1033c05e5dc38855f08a1521 42ac00"
|
||||
"8855f4c3"
|
||||
).replace(b" ", b""), # spaces stripped for readability
|
||||
"Script_CreateCharacter: read outfitId from Lua arg 2 -> global",
|
||||
),
|
||||
(
|
||||
0x004e042f,
|
||||
bytes.fromhex("8855f0c645f400"),
|
||||
bytes.fromhex("e85f08000090 90").replace(b" ", b""),
|
||||
"Packet builder: CALL helper -> load face + outfitId from global",
|
||||
),
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Patch 3: GetCharacterInfo — add 11th return value (charFlags as number)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Assembly context (FUN_004e3170 epilogue, valid branch):
|
||||
#
|
||||
# 004e31cd 56 PUSH ESI ← saves caller's ESI
|
||||
# 004e31ce 8b f0 MOV ESI, EAX ← ESI = char struct ptr
|
||||
# ... [function body — ESI used throughout for char struct] ...
|
||||
# 004e32e5 8b 8e 70010000 MOV ECX, [ESI+0x170] ← charFlags (ghost flag check)
|
||||
# ...
|
||||
# 004e332d 83 c4 2c ADD ESP, 0x2c ← clean accumulated args ← PATCH HERE
|
||||
# 004e3330 5e POP ESI ← restores caller's ESI (ESI valid until here)
|
||||
# 004e3331 b8 0a 00 00 00 MOV EAX, 0xa (dead after patch)
|
||||
# 004e3336 5f POP EDI ← JMP back target
|
||||
# 004e3337 8b e5 MOV ESP, EBP
|
||||
# 004e3339 5d POP EBP
|
||||
# 004e333a c3 RET
|
||||
#
|
||||
# KEY: ESI = char struct ptr up to and including ADD ESP,0x2c (004e332d).
|
||||
# After POP ESI (004e3330) it is caller's ESI — NOT char struct.
|
||||
# Previous approach: CALL cave at 004e3331 (after POP ESI) -> ESI already wrong.
|
||||
# Fix: JMP cave at 004e332d (before POP ESI) -> ESI still valid in cave.
|
||||
#
|
||||
# New approach: patch 5 bytes at 004e332d with JMP cave.
|
||||
# Old bytes: 83 c4 2c (ADD ESP,0x2c) + 5e (POP ESI) + b8 (MOV EAX first byte)
|
||||
# New bytes: e9 XX XX XX XX JMP cave
|
||||
# The 4 dead bytes at 004e3332-004e3335 are unreachable.
|
||||
#
|
||||
# Cave (30 bytes, found automatically via CC scan over entire .text):
|
||||
# 83 ec 08 SUB ESP, 8 ; make room for double
|
||||
# db 86 70 01 00 00 FILD [ESI+0x170] ; load charFlags into FPU (ESI valid here!)
|
||||
# dd 1c 24 FSTP [ESP] ; store as double
|
||||
# 57 PUSH EDI ; lua_State* L
|
||||
# e8 XX XX XX XX CALL FUN_0084e2a0 ; lua_pushnumber(L, charFlags)
|
||||
# 83 c4 38 ADD ESP, 0x38 ; merged: own 0xc + stolen 0x2c
|
||||
# 5e POP ESI ; stolen: restore caller's ESI
|
||||
# 6a 0b PUSH 0xb ; \ EAX = 11 in 3 bytes
|
||||
# 58 POP EAX ; / (vs MOV EAX,0xb = 5 bytes)
|
||||
# e9 XX XX XX XX JMP 004e3336 ; continue epilogue (POP EDI ...)
|
||||
#
|
||||
# Lua code reads info[11] and checks bit 0x40000000 (CHARACTER_FLAG_UNK31 = traitor).
|
||||
# charFlags confirmed at [ESI+0x170]: same field used for ghost flag (AND ECX,0x2000).
|
||||
|
||||
_CHARINFO_PATCH_VA = 0x004e332d # ADD ESP,0x2c (5 bytes including POP ESI + MOV EAX[0])
|
||||
_CHARINFO_JMPBACK_VA = 0x004e3336 # POP EDI — resume epilogue after cave
|
||||
_CHARINFO_CAVE_SIZE = 30
|
||||
_CHARINFO_CAVE_SEARCH_FROM = 0x00401000 # search entire .text for first 35-byte CC block
|
||||
_LUA_PUSHNUMBER_VA = 0x0084e2a0 # lua_pushnumber
|
||||
|
||||
|
||||
def _build_charinfo_patches(data, sections, image_base):
|
||||
"""
|
||||
Build the dynamic GetCharacterInfo patches.
|
||||
Returns list of (va, old_bytes, new_bytes, description), [] if already applied,
|
||||
or None on unrecoverable error.
|
||||
"""
|
||||
patch_off = va_to_offset(_CHARINFO_PATCH_VA, sections, image_base)
|
||||
current = bytes(data[patch_off:patch_off + 5])
|
||||
|
||||
# Already patched with new JMP approach?
|
||||
if current[0:1] == b"\xe9":
|
||||
return []
|
||||
|
||||
# Verify first 4 bytes look like the epilogue (ADD ESP,0x2c + POP ESI).
|
||||
# Accept both unpatched (b8 at [4]) and old-CALL-patched (e8 at [4]) states.
|
||||
if current[0:4] != bytes.fromhex("83c42c5e"):
|
||||
print(f"[WARN] 0x{_CHARINFO_PATCH_VA:08x}: unexpected bytes {current.hex(' ')}")
|
||||
print(" Expected ADD ESP,0x2c + POP ESI at this address.")
|
||||
print(" Restore Wow.exe.bak and re-run.")
|
||||
return None
|
||||
|
||||
old_b = current # exact 5 bytes currently in file (b8 or e8 at index 4)
|
||||
|
||||
# Find a 35-byte code cave in .text
|
||||
cave_va = find_cave(data, sections, image_base,
|
||||
_CHARINFO_CAVE_SIZE, _CHARINFO_CAVE_SEARCH_FROM)
|
||||
|
||||
# Cave layout offsets (30 bytes total):
|
||||
# 0 SUB ESP,8 (3)
|
||||
# 3 FILD [ESI+0x170] (6)
|
||||
# 9 FSTP [ESP] (3)
|
||||
# 12 PUSH EDI (1)
|
||||
# 13 CALL lua_pushnumber (5) — next instr at 18
|
||||
# 18 ADD ESP, 0x38 (3) merged: own 0xc + stolen 0x2c
|
||||
# 21 POP ESI (1) stolen
|
||||
# 22 PUSH 0xb (2) \ shorter than MOV EAX,0xb (5 bytes)
|
||||
# 24 POP EAX (1) /
|
||||
# 25 JMP 004e3336 (5) — next instr at cave_va+30
|
||||
call_rel = _LUA_PUSHNUMBER_VA - (cave_va + 18)
|
||||
jmp_back_rel = _CHARINFO_JMPBACK_VA - (cave_va + 30)
|
||||
|
||||
cave_bytes = (
|
||||
bytes.fromhex("83ec08") # SUB ESP, 8 (3)
|
||||
+ bytes.fromhex("db8670010000") # FILD [ESI+0x170] (6)
|
||||
+ bytes.fromhex("dd1c24") # FSTP [ESP] (3)
|
||||
+ bytes.fromhex("57") # PUSH EDI (1)
|
||||
+ b"\xe8" + struct.pack("<i", call_rel) # CALL lua_pushnumber (5)
|
||||
+ bytes.fromhex("83c438") # ADD ESP, 0x38 (0xc+0x2c) (3)
|
||||
+ bytes.fromhex("5e") # POP ESI [stolen] (1)
|
||||
+ bytes.fromhex("6a0b") # PUSH 0xb (2)
|
||||
+ bytes.fromhex("58") # POP EAX -> EAX=11 (1)
|
||||
+ b"\xe9" + struct.pack("<i", jmp_back_rel) # JMP 004e3336 (5)
|
||||
)
|
||||
assert len(cave_bytes) == _CHARINFO_CAVE_SIZE, \
|
||||
f"Cave size {len(cave_bytes)} != {_CHARINFO_CAVE_SIZE}"
|
||||
|
||||
# Patch: JMP cave (5 bytes at 004e332d)
|
||||
jmp_rel = cave_va - (_CHARINFO_PATCH_VA + 5)
|
||||
patch_new = b"\xe9" + struct.pack("<i", jmp_rel)
|
||||
|
||||
return [
|
||||
(
|
||||
cave_va,
|
||||
bytes([0xcc] * _CHARINFO_CAVE_SIZE),
|
||||
cave_bytes,
|
||||
f"GetCharacterInfo cave @ 0x{cave_va:08x}: push charFlags, stolen epilogue",
|
||||
),
|
||||
(
|
||||
_CHARINFO_PATCH_VA,
|
||||
old_b,
|
||||
patch_new,
|
||||
"GetCharacterInfo: JMP to cave before POP ESI (ESI=char struct), return 11",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(f"Usage: python {Path(sys.argv[0]).name} <path_to_Wow.exe>")
|
||||
sys.exit(1)
|
||||
|
||||
exe_path = Path(sys.argv[1])
|
||||
if not exe_path.exists():
|
||||
print(f"Error: file not found: {exe_path}")
|
||||
sys.exit(1)
|
||||
|
||||
data = bytearray(exe_path.read_bytes())
|
||||
image_base, sections = parse_pe_sections(data)
|
||||
|
||||
print(f"Image base: 0x{image_base:08x}")
|
||||
print(f"Sections: {len(sections)}")
|
||||
for n, va, vs, ro, rs in sections:
|
||||
print(f" {n:8s} VA=0x{va:08x} VSize=0x{vs:08x} Raw=0x{ro:08x} RSize=0x{rs:08x}")
|
||||
print()
|
||||
|
||||
charinfo_patches = _build_charinfo_patches(data, sections, image_base)
|
||||
if charinfo_patches is None:
|
||||
sys.exit(1)
|
||||
all_patches = list(_FIXED_PATCHES) + charinfo_patches
|
||||
|
||||
# Verify all first
|
||||
ok = True
|
||||
for va, old_b, new_b, desc in all_patches:
|
||||
off = va_to_offset(va, sections, image_base)
|
||||
cur = bytes(data[off:off + len(old_b)])
|
||||
if cur == new_b:
|
||||
print(f"[SKIP] 0x{va:08x}: already patched — {desc}")
|
||||
elif cur == old_b:
|
||||
print(f"[ OK ] 0x{va:08x}: verified — {desc}")
|
||||
else:
|
||||
print(f"[FAIL] 0x{va:08x}: {desc}")
|
||||
print(f" Expected: {old_b.hex(' ')}")
|
||||
print(f" Found: {cur.hex(' ')}")
|
||||
ok = False
|
||||
|
||||
if not ok:
|
||||
print("\nByte mismatch — aborting. Is this the correct Wow.exe (3.3.5a build 12340)?")
|
||||
sys.exit(1)
|
||||
|
||||
# Backup
|
||||
bak = exe_path.with_suffix(".exe.bak")
|
||||
if not bak.exists():
|
||||
shutil.copy2(exe_path, bak)
|
||||
print(f"\nBackup: {bak}")
|
||||
else:
|
||||
print(f"\nBackup already exists: {bak}")
|
||||
|
||||
# Apply
|
||||
applied = 0
|
||||
for va, old_b, new_b, desc in all_patches:
|
||||
off = va_to_offset(va, sections, image_base)
|
||||
cur = bytes(data[off:off + len(old_b)])
|
||||
if cur == new_b:
|
||||
continue
|
||||
data[off:off + len(old_b)] = new_b
|
||||
applied += 1
|
||||
print(f"[PATCH] 0x{va:08x}: {desc}")
|
||||
|
||||
if applied == 0:
|
||||
print("\nNothing to patch.")
|
||||
else:
|
||||
exe_path.write_bytes(data)
|
||||
print(f"\n{applied} patch(es) written to {exe_path}")
|
||||
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// FileDataID overlay for selected retail assets shipped in MoonWell patches.
|
||||
|
||||
#include "game/Io.hpp"
|
||||
#include "wxl/FdidApi.h"
|
||||
#include "wxl/PluginApi.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::fdid
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view kFileDataMap = "WXLFileData.csv";
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
std::unordered_map<uint32_t, std::string> g_paths;
|
||||
const WXL_Api* g_api = nullptr;
|
||||
const char*(__cdecl* g_resolveTexture)(uint32_t) = nullptr;
|
||||
const char*(__cdecl* g_resolveModel)(uint32_t) = nullptr;
|
||||
|
||||
bool ReadAll(const char* path, std::vector<uint8_t>& out)
|
||||
{
|
||||
void* handle = nullptr;
|
||||
if (!wxl::game::io::FileOpen(path, wxl::game::io::kOpenWholeFile, &handle) || !handle)
|
||||
return false;
|
||||
uint32_t high = 0;
|
||||
const uint32_t size = wxl::game::io::FileSize(handle, &high);
|
||||
if (high != 0)
|
||||
{
|
||||
wxl::game::io::FileClose(handle);
|
||||
return false;
|
||||
}
|
||||
out.resize(size);
|
||||
uint32_t read = 0;
|
||||
const bool ok = (size == 0 || wxl::game::io::FileRead(handle, out.data(), size, &read))
|
||||
&& read == size;
|
||||
wxl::game::io::FileClose(handle);
|
||||
if (!ok) out.clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::string_view Trim(std::string_view value)
|
||||
{
|
||||
while (!value.empty() && (value.front() == ' ' || value.front() == '\t' ||
|
||||
value.front() == '\r' || value.front() == '\n'))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && (value.back() == ' ' || value.back() == '\t' ||
|
||||
value.back() == '\r' || value.back() == '\n'))
|
||||
value.remove_suffix(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
std::vector<uint8_t> bytes;
|
||||
if (!ReadAll(kFileDataMap.data(), bytes))
|
||||
{
|
||||
if (g_api && g_api->Log)
|
||||
g_api->Log(WXL_LOG_WARN, "MoonWellFdid", "FileDataID map '%s' was not found",
|
||||
kFileDataMap.data());
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < text.size())
|
||||
{
|
||||
size_t lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == std::string_view::npos) lineEnd = text.size();
|
||||
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
|
||||
lineStart = lineEnd + 1;
|
||||
|
||||
if (line.empty() || line.front() == '#') continue;
|
||||
const size_t comma = line.find(',');
|
||||
if (comma == std::string_view::npos) continue;
|
||||
|
||||
const std::string_view idText = Trim(line.substr(0, comma));
|
||||
std::string_view pathText = Trim(line.substr(comma + 1));
|
||||
uint32_t fileDataId = 0;
|
||||
const auto parsed = std::from_chars(idText.data(), idText.data() + idText.size(), fileDataId);
|
||||
if (parsed.ec != std::errc{} || parsed.ptr != idText.data() + idText.size() ||
|
||||
fileDataId == 0 || pathText.empty())
|
||||
continue;
|
||||
|
||||
std::string path(pathText);
|
||||
for (char& c : path) if (c == '/') c = '\\';
|
||||
g_paths[fileDataId] = std::move(path);
|
||||
}
|
||||
|
||||
if (g_api && g_api->Log)
|
||||
g_api->Log(WXL_LOG_INFO, "MoonWellFdid", "loaded %zu custom FileDataID path(s)",
|
||||
g_paths.size());
|
||||
}
|
||||
|
||||
const char* ResolveCustom(uint32_t fileDataId)
|
||||
{
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
const auto found = g_paths.find(fileDataId);
|
||||
return found == g_paths.end() ? nullptr : found->second.c_str();
|
||||
}
|
||||
|
||||
const char* __cdecl ResolveTexture(uint32_t fileDataId)
|
||||
{
|
||||
if (const char* custom = ResolveCustom(fileDataId)) return custom;
|
||||
return g_resolveTexture ? g_resolveTexture(fileDataId) : nullptr;
|
||||
}
|
||||
|
||||
const char* __cdecl ResolveModel(uint32_t fileDataId)
|
||||
{
|
||||
if (const char* custom = ResolveCustom(fileDataId)) return custom;
|
||||
return g_resolveModel ? g_resolveModel(fileDataId) : nullptr;
|
||||
}
|
||||
|
||||
bool PatchResolver(WXL_FdidApi* fdid)
|
||||
{
|
||||
if (!fdid || fdid->apiVersion != WXL_FDID_API_VERSION) return false;
|
||||
g_resolveTexture = fdid->ResolveTexture;
|
||||
g_resolveModel = fdid->ResolveModel;
|
||||
|
||||
DWORD oldProtection = 0;
|
||||
if (!VirtualProtect(fdid, sizeof(*fdid), PAGE_READWRITE, &oldProtection)) return false;
|
||||
fdid->ResolveTexture = &ResolveTexture;
|
||||
fdid->ResolveModel = &ResolveModel;
|
||||
DWORD ignored = 0;
|
||||
VirtualProtect(fdid, sizeof(*fdid), oldProtection, &ignored);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const WXL_PluginInfo* __cdecl WXL_Query(void)
|
||||
{
|
||||
static const WXL_PluginInfo info = {
|
||||
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWellFdid", 1, WXL_CLIENT_BUILD,
|
||||
};
|
||||
return &info;
|
||||
}
|
||||
|
||||
int __cdecl WXL_Load(const WXL_Api* api)
|
||||
{
|
||||
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
|
||||
moonwell::fdid::g_api = api;
|
||||
auto* fdid = static_cast<WXL_FdidApi*>(api->GetInterface("wxl.fdid", WXL_FDID_API_VERSION));
|
||||
if (!moonwell::fdid::PatchResolver(fdid))
|
||||
{
|
||||
api->Log(WXL_LOG_ERROR, "MoonWellFdid", "wxl.fdid v1 is unavailable");
|
||||
return 0;
|
||||
}
|
||||
api->Log(WXL_LOG_INFO, "MoonWellFdid", "custom FileDataID overlay installed");
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// MoonWell's specific-archive fallback, kept outside the WarcraftXL core.
|
||||
|
||||
#include "wxl/PluginApi.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace
|
||||
{
|
||||
const WXL_Api* g_api = nullptr;
|
||||
using FileOpenFn = int(__stdcall*)(void* archive, const char* name, uint32_t flags, void** out);
|
||||
FileOpenFn g_nextOpen = nullptr;
|
||||
std::atomic<uint32_t> g_logCount{0};
|
||||
|
||||
int __stdcall FileOpenHook(void* archive, const char* name, uint32_t flags, void** out)
|
||||
{
|
||||
if (!g_nextOpen) return 0;
|
||||
|
||||
const int nativeResult = g_nextOpen(archive, name, flags, out);
|
||||
if (nativeResult || archive == nullptr || !name || !*name) return nativeResult;
|
||||
|
||||
// A dependency requested through one concrete MPQ may live in a higher-priority loose
|
||||
// Patch-*.MPQ directory. Retry only after that exact archive misses, using the client's
|
||||
// normal global search path. This is the MoonWell-only behavior that used to patch core.
|
||||
if (out) *out = nullptr;
|
||||
const int fallbackResult = g_nextOpen(nullptr, name, flags, out);
|
||||
if (fallbackResult && g_api && g_api->Log)
|
||||
{
|
||||
const uint32_t index = g_logCount.fetch_add(1, std::memory_order_relaxed);
|
||||
if (index < 32)
|
||||
g_api->Log(WXL_LOG_INFO, "MoonWellStorageFallback",
|
||||
"specific archive miss resolved globally: '%s'", name);
|
||||
}
|
||||
return fallbackResult;
|
||||
}
|
||||
}
|
||||
|
||||
const WXL_PluginInfo* __cdecl WXL_Query(void)
|
||||
{
|
||||
static const WXL_PluginInfo info = {
|
||||
sizeof(WXL_PluginInfo), WXL_API_VERSION, "MoonWellStorageFallback", 1,
|
||||
WXL_CLIENT_BUILD,
|
||||
};
|
||||
return &info;
|
||||
}
|
||||
|
||||
int __cdecl WXL_Load(const WXL_Api* api)
|
||||
{
|
||||
if (!api || api->apiVersion != WXL_API_VERSION || !api->HookAttachByName) return 0;
|
||||
g_api = api;
|
||||
const int installed = api->HookAttachByName(
|
||||
"Io.FileOpen", reinterpret_cast<void*>(&FileOpenHook),
|
||||
reinterpret_cast<void**>(&g_nextOpen), WXL_HOOK_DEFAULT_PRIORITY);
|
||||
api->Log(installed ? WXL_LOG_INFO : WXL_LOG_ERROR, "MoonWellStorageFallback", "%s",
|
||||
installed ? "specific-archive fallback installed" : "Io.FileOpen hook failed");
|
||||
return installed;
|
||||
}
|
||||
@@ -0,0 +1,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;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Appends compact MoonWell spell overrides to stock 3.3.5 DBC bytes.
|
||||
|
||||
#include "game/Io.hpp"
|
||||
#include "wxl/PluginApi.h"
|
||||
#include "wxl/StorageApi.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
namespace moonwell::spells
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view kOverridesPath = "WXLSpellOverrides.tsv";
|
||||
constexpr std::string_view kSpellPath = "DBFilesClient\\Spell.dbc";
|
||||
constexpr std::string_view kSkillPath = "DBFilesClient\\SkillLineAbility.dbc";
|
||||
constexpr uint32_t kSpellFields = 234;
|
||||
constexpr uint32_t kSpellRecordSize = kSpellFields * 4;
|
||||
constexpr uint32_t kSkillFields = 14;
|
||||
constexpr uint32_t kSkillRecordSize = kSkillFields * 4;
|
||||
|
||||
struct Override
|
||||
{
|
||||
uint32_t spellId = 0;
|
||||
uint32_t templateSpellId = 0;
|
||||
uint32_t creatureEntry = 0;
|
||||
uint32_t skillLineAbilityId = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
std::once_flag g_loadOnce;
|
||||
bool g_ready = false;
|
||||
const WXL_Api* g_api = nullptr;
|
||||
std::vector<Override> g_overrides;
|
||||
|
||||
void Log(int level, const char* message)
|
||||
{
|
||||
if (g_api && g_api->Log) g_api->Log(level, "MoonWellSpells", "%s", message);
|
||||
}
|
||||
|
||||
bool ReadAll(const char* path, std::vector<uint8_t>& out)
|
||||
{
|
||||
void* handle = nullptr;
|
||||
if (!wxl::game::io::FileOpen(path, wxl::game::io::kOpenWholeFile, &handle) || !handle)
|
||||
return false;
|
||||
uint32_t high = 0;
|
||||
const uint32_t size = wxl::game::io::FileSize(handle, &high);
|
||||
if (high != 0)
|
||||
{
|
||||
wxl::game::io::FileClose(handle);
|
||||
return false;
|
||||
}
|
||||
out.resize(size);
|
||||
uint32_t read = 0;
|
||||
const bool ok = (size == 0 || wxl::game::io::FileRead(handle, out.data(), size, &read))
|
||||
&& read == size;
|
||||
wxl::game::io::FileClose(handle);
|
||||
if (!ok) out.clear();
|
||||
return ok;
|
||||
}
|
||||
|
||||
uint32_t ReadU32(const uint8_t* data)
|
||||
{
|
||||
uint32_t value = 0;
|
||||
std::memcpy(&value, data, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
void WriteU32(uint8_t* data, uint32_t value)
|
||||
{
|
||||
std::memcpy(data, &value, sizeof(value));
|
||||
}
|
||||
|
||||
std::string_view Trim(std::string_view value)
|
||||
{
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front())))
|
||||
value.remove_prefix(1);
|
||||
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back())))
|
||||
value.remove_suffix(1);
|
||||
return value;
|
||||
}
|
||||
|
||||
bool ParseU32(std::string_view text, uint32_t& value)
|
||||
{
|
||||
text = Trim(text);
|
||||
const auto parsed = std::from_chars(text.data(), text.data() + text.size(), value);
|
||||
return parsed.ec == std::errc{} && parsed.ptr == text.data() + text.size();
|
||||
}
|
||||
|
||||
bool SamePath(std::string_view left, std::string_view right)
|
||||
{
|
||||
if (left.size() != right.size()) return false;
|
||||
for (size_t i = 0; i < left.size(); ++i)
|
||||
{
|
||||
const unsigned char a = static_cast<unsigned char>(left[i] == '/' ? '\\' : left[i]);
|
||||
const unsigned char b = static_cast<unsigned char>(right[i] == '/' ? '\\' : right[i]);
|
||||
if (std::tolower(a) != std::tolower(b)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseOverrides(const std::vector<uint8_t>& bytes, std::vector<Override>& out)
|
||||
{
|
||||
const std::string_view text(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
size_t lineStart = 0;
|
||||
while (lineStart < text.size())
|
||||
{
|
||||
size_t lineEnd = text.find('\n', lineStart);
|
||||
if (lineEnd == std::string_view::npos) lineEnd = text.size();
|
||||
std::string_view line = Trim(text.substr(lineStart, lineEnd - lineStart));
|
||||
lineStart = lineEnd + 1;
|
||||
if (line.empty() || line.front() == '#') continue;
|
||||
|
||||
std::string_view fields[5];
|
||||
size_t fieldStart = 0;
|
||||
bool valid = true;
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
{
|
||||
const size_t tab = line.find('\t', fieldStart);
|
||||
if (tab == std::string_view::npos) { valid = false; break; }
|
||||
fields[i] = line.substr(fieldStart, tab - fieldStart);
|
||||
fieldStart = tab + 1;
|
||||
}
|
||||
if (!valid) return false;
|
||||
fields[4] = Trim(line.substr(fieldStart));
|
||||
|
||||
Override item;
|
||||
if (!ParseU32(fields[0], item.spellId) ||
|
||||
!ParseU32(fields[1], item.templateSpellId) ||
|
||||
!ParseU32(fields[2], item.creatureEntry) ||
|
||||
!ParseU32(fields[3], item.skillLineAbilityId) || fields[4].empty())
|
||||
return false;
|
||||
item.name.assign(fields[4]);
|
||||
out.push_back(std::move(item));
|
||||
}
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
bool ValidateWdbc(const std::vector<uint8_t>& bytes, uint32_t fields, uint32_t recordSize,
|
||||
uint32_t& count, uint32_t& stringSize, size_t& recordsEnd)
|
||||
{
|
||||
if (bytes.size() < 20 || std::memcmp(bytes.data(), "WDBC", 4) != 0) return false;
|
||||
count = ReadU32(bytes.data() + 4);
|
||||
if (ReadU32(bytes.data() + 8) != fields || ReadU32(bytes.data() + 12) != recordSize)
|
||||
return false;
|
||||
stringSize = ReadU32(bytes.data() + 16);
|
||||
recordsEnd = 20ull + static_cast<size_t>(count) * recordSize;
|
||||
return recordsEnd <= bytes.size() && recordsEnd + stringSize == bytes.size();
|
||||
}
|
||||
|
||||
const uint8_t* FindSpell(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t id)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSpellRecordSize;
|
||||
if (ReadU32(row) == id) return row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* FindSkillRow(const std::vector<uint8_t>& bytes, uint32_t count, uint32_t spellId)
|
||||
{
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
const uint8_t* row = bytes.data() + 20 + static_cast<size_t>(i) * kSkillRecordSize;
|
||||
if (ReadU32(row + 4) == 778 && ReadU32(row + 8) == spellId) return row;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool BuildSpellDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
uint32_t count = 0, stringSize = 0;
|
||||
size_t recordsEnd = 0;
|
||||
if (!ValidateWdbc(base, kSpellFields, kSpellRecordSize, count, stringSize, recordsEnd))
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> records;
|
||||
records.reserve(overrides.size() * kSpellRecordSize);
|
||||
std::vector<uint8_t> strings;
|
||||
for (const Override& item : overrides)
|
||||
{
|
||||
const uint8_t* source = FindSpell(base, count, item.templateSpellId);
|
||||
if (!source) return false;
|
||||
const size_t recordStart = records.size();
|
||||
records.insert(records.end(), source, source + kSpellRecordSize);
|
||||
uint8_t* row = records.data() + recordStart;
|
||||
WriteU32(row, item.spellId);
|
||||
WriteU32(row + 110 * 4, item.creatureEntry);
|
||||
|
||||
const uint32_t nameOffset = stringSize + static_cast<uint32_t>(strings.size());
|
||||
strings.insert(strings.end(), item.name.begin(), item.name.end());
|
||||
strings.push_back(0);
|
||||
const std::string description = "Призывает или отпускает спутника: " + item.name + ".";
|
||||
const uint32_t descriptionOffset = stringSize + static_cast<uint32_t>(strings.size());
|
||||
strings.insert(strings.end(), description.begin(), description.end());
|
||||
strings.push_back(0);
|
||||
for (uint32_t field = 136; field <= 151; ++field) WriteU32(row + field * 4, nameOffset);
|
||||
for (uint32_t field = 170; field <= 185; ++field) WriteU32(row + field * 4, descriptionOffset);
|
||||
for (uint32_t field = 187; field <= 202; ++field) WriteU32(row + field * 4, 0);
|
||||
}
|
||||
|
||||
out.reserve(base.size() + records.size() + strings.size());
|
||||
out.insert(out.end(), base.begin(), base.begin() + 20);
|
||||
WriteU32(out.data() + 4, count + static_cast<uint32_t>(overrides.size()));
|
||||
WriteU32(out.data() + 16, stringSize + static_cast<uint32_t>(strings.size()));
|
||||
out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd);
|
||||
out.insert(out.end(), records.begin(), records.end());
|
||||
out.insert(out.end(), base.begin() + recordsEnd, base.end());
|
||||
out.insert(out.end(), strings.begin(), strings.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BuildSkillDbc(const std::vector<uint8_t>& base, const std::vector<Override>& overrides,
|
||||
std::vector<uint8_t>& out)
|
||||
{
|
||||
uint32_t count = 0, stringSize = 0;
|
||||
size_t recordsEnd = 0;
|
||||
if (!ValidateWdbc(base, kSkillFields, kSkillRecordSize, count, stringSize, recordsEnd))
|
||||
return false;
|
||||
|
||||
std::vector<uint8_t> records;
|
||||
records.reserve(overrides.size() * kSkillRecordSize);
|
||||
for (const Override& item : overrides)
|
||||
{
|
||||
const uint8_t* source = FindSkillRow(base, count, item.templateSpellId);
|
||||
if (!source) return false;
|
||||
const size_t recordStart = records.size();
|
||||
records.insert(records.end(), source, source + kSkillRecordSize);
|
||||
uint8_t* row = records.data() + recordStart;
|
||||
WriteU32(row, item.skillLineAbilityId);
|
||||
WriteU32(row + 8, item.spellId);
|
||||
}
|
||||
|
||||
out.reserve(base.size() + records.size());
|
||||
out.insert(out.end(), base.begin(), base.begin() + 20);
|
||||
WriteU32(out.data() + 4, count + static_cast<uint32_t>(overrides.size()));
|
||||
out.insert(out.end(), base.begin() + 20, base.begin() + recordsEnd);
|
||||
out.insert(out.end(), records.begin(), records.end());
|
||||
out.insert(out.end(), base.begin() + recordsEnd, base.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Load()
|
||||
{
|
||||
std::vector<uint8_t> overrideBytes;
|
||||
if (!ReadAll(kOverridesPath.data(), overrideBytes))
|
||||
return;
|
||||
|
||||
if (!ParseOverrides(overrideBytes, g_overrides))
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "failed to parse WXLSpellOverrides.tsv");
|
||||
return;
|
||||
}
|
||||
g_ready = true;
|
||||
if (g_api && g_api->Log)
|
||||
g_api->Log(WXL_LOG_INFO, "MoonWellSpells", "loaded %zu companion spell override(s)",
|
||||
g_overrides.size());
|
||||
}
|
||||
|
||||
int __cdecl Transform(const char* rawName, const uint8_t* raw, uint32_t rawLen,
|
||||
const WXL_ByteSink* sink)
|
||||
{
|
||||
if (!rawName || !raw || !sink || !sink->Write) return 0;
|
||||
const std::string_view name(rawName);
|
||||
if (!SamePath(name, kSpellPath) && !SamePath(name, kSkillPath)) return false;
|
||||
std::call_once(g_loadOnce, &Load);
|
||||
if (!g_ready) return false;
|
||||
|
||||
const std::vector<uint8_t> base(raw, raw + rawLen);
|
||||
std::vector<uint8_t> out;
|
||||
const bool built = SamePath(name, kSpellPath)
|
||||
? BuildSpellDbc(base, g_overrides, out)
|
||||
: BuildSkillDbc(base, g_overrides, out);
|
||||
if (!built)
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "failed to build custom companion DBC");
|
||||
return 0;
|
||||
}
|
||||
sink->Write(sink->ctx, out.data(), static_cast<uint32_t>(out.size()));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool Install(const WXL_Api* api)
|
||||
{
|
||||
g_api = api;
|
||||
if (!api || !api->GetInterface) return false;
|
||||
auto* storage = static_cast<WXL_StorageApi*>(
|
||||
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
|
||||
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
|
||||
!storage->RegisterClientTransform)
|
||||
{
|
||||
Log(WXL_LOG_ERROR, "wxl.storage v1 is unavailable");
|
||||
return false;
|
||||
}
|
||||
storage->RegisterClientTransform(".dbc", &Transform);
|
||||
Log(WXL_LOG_INFO, "DBC override transform registered");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
struct WXL_Api;
|
||||
|
||||
namespace moonwell::spells
|
||||
{
|
||||
bool Install(const WXL_Api* api);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GLOB_RECURSE WXL_EXT_SHARED_SRC CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/engine/assets/shared/textures/blp/*.cpp")
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// WarcraftXL 1.1 BLP transform extension.
|
||||
|
||||
#include "engine/assets/shared/textures/blp/BlpTranscode.hpp"
|
||||
#include "wxl/ModernBlpApi.h"
|
||||
#include "wxl/PluginApi.h"
|
||||
#include "wxl/StorageApi.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
const WXL_Api* g_api = nullptr;
|
||||
constexpr uint32_t kMaxTextureEdge = 1024;
|
||||
|
||||
int __cdecl Transcode(const char* name, const uint8_t* raw, uint32_t rawLen,
|
||||
const WXL_ByteSink* sink)
|
||||
{
|
||||
if (!raw || !sink || !sink->Write) return 0;
|
||||
namespace blp = wxl::modern::assets::textures::blp;
|
||||
|
||||
const std::span<const uint8_t> input(raw, rawLen);
|
||||
std::vector<uint8_t> capped;
|
||||
const bool didCap = blp::CapBlpMips(input, capped, kMaxTextureEdge);
|
||||
const std::span<const uint8_t> source = didCap
|
||||
? std::span<const uint8_t>(capped.data(), capped.size()) : input;
|
||||
|
||||
std::vector<uint8_t> transcoded;
|
||||
if (blp::TranscodeBlp(source, transcoded))
|
||||
{
|
||||
sink->Write(sink->ctx, transcoded.data(), static_cast<uint32_t>(transcoded.size()));
|
||||
if (g_api && g_api->Log)
|
||||
g_api->Log(WXL_LOG_DEBUG, "wxl-modern-blp", "%s%s BGRA->DXT5",
|
||||
name ? name : "?", didCap ? " capped" : "");
|
||||
return 1;
|
||||
}
|
||||
if (!didCap) return 0;
|
||||
|
||||
sink->Write(sink->ctx, capped.data(), static_cast<uint32_t>(capped.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const WXL_ModernBlpApi g_blpApi = {
|
||||
sizeof(WXL_ModernBlpApi), WXL_MODERN_BLP_API_VERSION, &Transcode,
|
||||
};
|
||||
}
|
||||
|
||||
const WXL_PluginInfo* __cdecl WXL_Query(void)
|
||||
{
|
||||
static const WXL_PluginInfo info = {
|
||||
sizeof(WXL_PluginInfo), WXL_API_VERSION, "wxl-modern-blp", 1, WXL_CLIENT_BUILD,
|
||||
};
|
||||
return &info;
|
||||
}
|
||||
|
||||
int __cdecl WXL_Load(const WXL_Api* api)
|
||||
{
|
||||
if (!api || api->apiVersion != WXL_API_VERSION) return 0;
|
||||
g_api = api;
|
||||
auto* storage = static_cast<WXL_StorageApi*>(
|
||||
api->GetInterface("wxl.storage", WXL_STORAGE_API_VERSION));
|
||||
if (!storage || storage->apiVersion != WXL_STORAGE_API_VERSION ||
|
||||
!storage->RegisterClientTransform)
|
||||
{
|
||||
api->Log(WXL_LOG_ERROR, "wxl-modern-blp", "wxl.storage v1 is unavailable");
|
||||
return 0;
|
||||
}
|
||||
|
||||
storage->RegisterClientTransform(".blp", &Transcode);
|
||||
api->PublishInterface("wxl.modern-blp", WXL_MODERN_BLP_API_VERSION,
|
||||
const_cast<WXL_ModernBlpApi*>(&g_blpApi));
|
||||
api->Log(WXL_LOG_INFO, "wxl-modern-blp", "BLP transcode and mip cap active");
|
||||
return 1;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
Binary file not shown.
@@ -4,6 +4,7 @@
|
||||
## Author: Rochet2 <https://github.com/Rochet2>
|
||||
## SavedVariables: AIO_sv, AIO_sv_Addons
|
||||
## SavedVariablesPerCharacter: AIO_sv_char
|
||||
## X-Hidden: 1
|
||||
|
||||
#dependencies
|
||||
Dep_LibWindow-1.1\LibStub.lua
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
|
||||
-- This file is only there in standalone Ace3 and provides handy dev tool stuff I guess
|
||||
-- for now only /rl to reload your UI :)
|
||||
-- note the complete overkill use of AceAddon and console, ain't it cool?
|
||||
|
||||
-- GLOBALS: next, loadstring, ReloadUI, geterrorhandler
|
||||
-- GLOBALS: BINDING_HEADER_ACE3, BINDING_NAME_RELOADUI, Ace3, LibStub
|
||||
|
||||
-- BINDINGs labels
|
||||
BINDING_HEADER_ACE3 = "Ace3"
|
||||
BINDING_NAME_RELOADUI = "ReloadUI"
|
||||
--
|
||||
|
||||
local gui = LibStub("AceGUI-3.0")
|
||||
local reg = LibStub("AceConfigRegistry-3.0")
|
||||
local dialog = LibStub("AceConfigDialog-3.0")
|
||||
|
||||
Ace3 = LibStub("AceAddon-3.0"):NewAddon("Ace3", "AceConsole-3.0")
|
||||
local Ace3 = Ace3
|
||||
|
||||
local selectedgroup
|
||||
local frame
|
||||
local select
|
||||
local status = {}
|
||||
local configs = {}
|
||||
|
||||
local function frameOnClose()
|
||||
gui:Release(frame)
|
||||
frame = nil
|
||||
end
|
||||
|
||||
local function RefreshConfigs()
|
||||
for name in reg:IterateOptionsTables() do
|
||||
configs[name] = name
|
||||
end
|
||||
end
|
||||
|
||||
local function ConfigSelected(widget, event, value)
|
||||
selectedgroup = value
|
||||
dialog:Open(value, widget)
|
||||
end
|
||||
|
||||
local old_CloseSpecialWindows
|
||||
|
||||
-- GLOBALS: CloseSpecialWindows, next
|
||||
function Ace3:Open()
|
||||
if not old_CloseSpecialWindows then
|
||||
old_CloseSpecialWindows = CloseSpecialWindows
|
||||
CloseSpecialWindows = function()
|
||||
local found = old_CloseSpecialWindows()
|
||||
if frame then
|
||||
frame:Hide()
|
||||
return true
|
||||
end
|
||||
return found
|
||||
end
|
||||
end
|
||||
RefreshConfigs()
|
||||
if next(configs) == nil then
|
||||
self:Print("No Configs are Registered")
|
||||
return
|
||||
end
|
||||
|
||||
if not frame then
|
||||
frame = gui:Create("Frame")
|
||||
frame:ReleaseChildren()
|
||||
frame:SetTitle("Ace3 Options")
|
||||
frame:SetLayout("FILL")
|
||||
frame:SetCallback("OnClose", frameOnClose)
|
||||
|
||||
select = gui:Create("DropdownGroup")
|
||||
select:SetGroupList(configs)
|
||||
select:SetCallback("OnGroupSelected", ConfigSelected)
|
||||
frame:AddChild(select)
|
||||
end
|
||||
if not selectedgroup then
|
||||
selectedgroup = next(configs)
|
||||
end
|
||||
select:SetGroup(selectedgroup)
|
||||
frame:Show()
|
||||
end
|
||||
|
||||
local function RefreshOnUpdate(this)
|
||||
select:SetGroup(selectedgroup)
|
||||
this:SetScript("OnUpdate", nil)
|
||||
end
|
||||
|
||||
function Ace3:ConfigTableChanged(event, appName)
|
||||
if selectedgroup == appName and frame then
|
||||
frame.frame:SetScript("OnUpdate", RefreshOnUpdate)
|
||||
end
|
||||
end
|
||||
|
||||
reg.RegisterCallback(Ace3, "ConfigTableChange", "ConfigTableChanged")
|
||||
|
||||
function Ace3:PrintCmd(input)
|
||||
input = input:trim():match("^(.-);*$")
|
||||
local func, err = loadstring("LibStub(\"AceConsole-3.0\"):Print(" .. input .. ")")
|
||||
if not func then
|
||||
LibStub("AceConsole-3.0"):Print("Error: " .. err)
|
||||
else
|
||||
func()
|
||||
end
|
||||
end
|
||||
|
||||
function Ace3:OnInitialize()
|
||||
self:RegisterChatCommand("ace3", function() self:Open() end)
|
||||
self:RegisterChatCommand("rl", function() ReloadUI() end)
|
||||
self:RegisterChatCommand("print", "PrintCmd")
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
## Interface: 30300
|
||||
## X-Curse-Packaged-Version: r922
|
||||
## X-Curse-Project-Name: Ace3
|
||||
## X-Curse-Project-ID: ace3
|
||||
## X-Curse-Repository-ID: wow/ace3/mainline
|
||||
|
||||
## Title: Lib: Ace3
|
||||
## Notes: AddOn development framework
|
||||
## Author: Ace3 Development Team
|
||||
## Dependencies: LibStub, CallbackHandler-1.0
|
||||
## X-Website: http://www.wowace.com
|
||||
## X-Category: Library
|
||||
## X-License: Limited BSD
|
||||
|
||||
AceAddon-3.0\AceAddon-3.0.xml
|
||||
AceEvent-3.0\AceEvent-3.0.xml
|
||||
AceTimer-3.0\AceTimer-3.0.xml
|
||||
AceBucket-3.0\AceBucket-3.0.xml
|
||||
AceHook-3.0\AceHook-3.0.xml
|
||||
AceDB-3.0\AceDB-3.0.xml
|
||||
AceDBOptions-3.0\AceDBOptions-3.0.xml
|
||||
AceLocale-3.0\AceLocale-3.0.xml
|
||||
AceConsole-3.0\AceConsole-3.0.xml
|
||||
AceGUI-3.0\AceGUI-3.0.xml
|
||||
AceConfig-3.0\AceConfig-3.0.xml
|
||||
AceComm-3.0\AceComm-3.0.xml
|
||||
AceTab-3.0\AceTab-3.0.xml
|
||||
AceSerializer-3.0\AceSerializer-3.0.xml
|
||||
|
||||
Ace3.lua
|
||||
@@ -0,0 +1,57 @@
|
||||
--- AceConfig-3.0 wrapper library.
|
||||
-- Provides an API to register an options table with the config registry,
|
||||
-- as well as associate it with a slash command.
|
||||
-- @class file
|
||||
-- @name AceConfig-3.0
|
||||
-- @release $Id: AceConfig-3.0.lua 877 2009-11-02 15:56:50Z nevcairiel $
|
||||
|
||||
--[[
|
||||
AceConfig-3.0
|
||||
|
||||
Very light wrapper library that combines all the AceConfig subcomponents into one more easily used whole.
|
||||
|
||||
]]
|
||||
|
||||
local MAJOR, MINOR = "AceConfig-3.0", 2
|
||||
local AceConfig = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfig then return end
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
local cfgcmd = LibStub("AceConfigCmd-3.0")
|
||||
local cfgdlg = LibStub("AceConfigDialog-3.0")
|
||||
--TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local pcall, error, type, pairs = pcall, error, type, pairs
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- :RegisterOptionsTable(appName, options, slashcmd, persist)
|
||||
--
|
||||
-- - appName - (string) application name
|
||||
-- - options - table or function ref, see AceConfigRegistry
|
||||
-- - slashcmd - slash command (string) or table with commands, or nil to NOT create a slash command
|
||||
|
||||
--- Register a option table with the AceConfig registry.
|
||||
-- You can supply a slash command (or a table of slash commands) to register with AceConfigCmd directly.
|
||||
-- @paramsig appName, options [, slashcmd]
|
||||
-- @param appName The application name for the config table.
|
||||
-- @param options The option table (or a function to generate one on demand)
|
||||
-- @param slashcmd A slash command to register for the option table, or a table of slash commands.
|
||||
-- @usage
|
||||
-- local AceConfig = LibStub("AceConfig-3.0")
|
||||
-- AceConfig:RegisterOptionsTable("MyAddon", myOptions, {"/myslash", "/my"})
|
||||
function AceConfig:RegisterOptionsTable(appName, options, slashcmd)
|
||||
local ok,msg = pcall(cfgreg.RegisterOptionsTable, self, appName, options)
|
||||
if not ok then error(msg, 2) end
|
||||
|
||||
if slashcmd then
|
||||
if type(slashcmd) == "table" then
|
||||
for _,cmd in pairs(slashcmd) do
|
||||
cfgcmd:CreateChatCommand(cmd, appName)
|
||||
end
|
||||
else
|
||||
cfgcmd:CreateChatCommand(slashcmd, appName)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Include file="AceConfigRegistry-3.0\AceConfigRegistry-3.0.xml"/>
|
||||
<Include file="AceConfigCmd-3.0\AceConfigCmd-3.0.xml"/>
|
||||
<Include file="AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
|
||||
<!--<Include file="AceConfigDropdown-3.0\AceConfigDropdown-3.0.xml"/>-->
|
||||
<Script file="AceConfig-3.0.lua"/>
|
||||
</Ui>
|
||||
+787
@@ -0,0 +1,787 @@
|
||||
--- AceConfigCmd-3.0 handles access to an options table through the "command line" interface via the ChatFrames.
|
||||
-- @class file
|
||||
-- @name AceConfigCmd-3.0
|
||||
-- @release $Id: AceConfigCmd-3.0.lua 904 2009-12-13 11:56:37Z nevcairiel $
|
||||
|
||||
--[[
|
||||
AceConfigCmd-3.0
|
||||
|
||||
Handles commandline optionstable access
|
||||
|
||||
REQUIRES: AceConsole-3.0 for command registration (loaded on demand)
|
||||
|
||||
]]
|
||||
|
||||
-- TODO: plugin args
|
||||
|
||||
|
||||
local MAJOR, MINOR = "AceConfigCmd-3.0", 12
|
||||
local AceConfigCmd = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigCmd then return end
|
||||
|
||||
AceConfigCmd.commands = AceConfigCmd.commands or {}
|
||||
local commands = AceConfigCmd.commands
|
||||
|
||||
local cfgreg = LibStub("AceConfigRegistry-3.0")
|
||||
local AceConsole -- LoD
|
||||
local AceConsoleName = "AceConsole-3.0"
|
||||
|
||||
-- Lua APIs
|
||||
local strsub, strsplit, strlower, strmatch, strtrim = string.sub, string.split, string.lower, string.match, string.trim
|
||||
local format, tonumber, tostring = string.format, tonumber, tostring
|
||||
local tsort, tinsert = table.sort, table.insert
|
||||
local select, pairs, next, type = select, pairs, next, type
|
||||
local error, assert = error, assert
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: LibStub, SELECTED_CHAT_FRAME, DEFAULT_CHAT_FRAME
|
||||
|
||||
|
||||
local L = setmetatable({}, { -- TODO: replace with proper locale
|
||||
__index = function(self,k) return k end
|
||||
})
|
||||
|
||||
|
||||
|
||||
local function print(msg)
|
||||
(SELECTED_CHAT_FRAME or DEFAULT_CHAT_FRAME):AddMessage(msg)
|
||||
end
|
||||
|
||||
-- constants used by getparam() calls below
|
||||
|
||||
local handlertypes = {["table"]=true}
|
||||
local handlermsg = "expected a table"
|
||||
|
||||
local functypes = {["function"]=true, ["string"]=true}
|
||||
local funcmsg = "expected function or member name"
|
||||
|
||||
|
||||
-- pickfirstset() - picks the first non-nil value and returns it
|
||||
|
||||
local function pickfirstset(...)
|
||||
for i=1,select("#",...) do
|
||||
if select(i,...)~=nil then
|
||||
return select(i,...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- err() - produce real error() regarding malformed options tables etc
|
||||
|
||||
local function err(info,inputpos,msg )
|
||||
local cmdstr=" "..strsub(info.input, 1, inputpos-1)
|
||||
error(MAJOR..": /" ..info[0] ..cmdstr ..": "..(msg or "malformed options table"), 2)
|
||||
end
|
||||
|
||||
|
||||
-- usererr() - produce chatframe message regarding bad slash syntax etc
|
||||
|
||||
local function usererr(info,inputpos,msg )
|
||||
local cmdstr=strsub(info.input, 1, inputpos-1);
|
||||
print("/" ..info[0] .. " "..cmdstr ..": "..(msg or "malformed options table"))
|
||||
end
|
||||
|
||||
|
||||
-- callmethod() - call a given named method (e.g. "get", "set") with given arguments
|
||||
|
||||
local function callmethod(info, inputpos, tab, methodtype, ...)
|
||||
local method = info[methodtype]
|
||||
if not method then
|
||||
err(info, inputpos, "'"..methodtype.."': not set")
|
||||
end
|
||||
|
||||
info.arg = tab.arg
|
||||
info.option = tab
|
||||
info.type = tab.type
|
||||
|
||||
if type(method)=="function" then
|
||||
return method(info, ...)
|
||||
elseif type(method)=="string" then
|
||||
if type(info.handler[method])~="function" then
|
||||
err(info, inputpos, "'"..methodtype.."': '"..method.."' is not a member function of "..tostring(info.handler))
|
||||
end
|
||||
return info.handler[method](info.handler, info, ...)
|
||||
else
|
||||
assert(false) -- type should have already been checked on read
|
||||
end
|
||||
end
|
||||
|
||||
-- callfunction() - call a given named function (e.g. "name", "desc") with given arguments
|
||||
|
||||
local function callfunction(info, tab, methodtype, ...)
|
||||
local method = tab[methodtype]
|
||||
|
||||
info.arg = tab.arg
|
||||
info.option = tab
|
||||
info.type = tab.type
|
||||
|
||||
if type(method)=="function" then
|
||||
return method(info, ...)
|
||||
else
|
||||
assert(false) -- type should have already been checked on read
|
||||
end
|
||||
end
|
||||
|
||||
-- do_final() - do the final step (set/execute) along with validation and confirmation
|
||||
|
||||
local function do_final(info, inputpos, tab, methodtype, ...)
|
||||
if info.validate then
|
||||
local res = callmethod(info,inputpos,tab,"validate",...)
|
||||
if type(res)=="string" then
|
||||
usererr(info, inputpos, "'"..strsub(info.input, inputpos).."' - "..res)
|
||||
return
|
||||
end
|
||||
end
|
||||
-- console ignores .confirm
|
||||
|
||||
callmethod(info,inputpos,tab,methodtype, ...)
|
||||
end
|
||||
|
||||
|
||||
-- getparam() - used by handle() to retreive and store "handler", "get", "set", etc
|
||||
|
||||
local function getparam(info, inputpos, tab, depth, paramname, types, errormsg)
|
||||
local old,oldat = info[paramname], info[paramname.."_at"]
|
||||
local val=tab[paramname]
|
||||
if val~=nil then
|
||||
if val==false then
|
||||
val=nil
|
||||
elseif not types[type(val)] then
|
||||
err(info, inputpos, "'" .. paramname.. "' - "..errormsg)
|
||||
end
|
||||
info[paramname] = val
|
||||
info[paramname.."_at"] = depth
|
||||
end
|
||||
return old,oldat
|
||||
end
|
||||
|
||||
|
||||
-- iterateargs(tab) - custom iterator that iterates both t.args and t.plugins.*
|
||||
local dummytable={}
|
||||
|
||||
local function iterateargs(tab)
|
||||
if not tab.plugins then
|
||||
return pairs(tab.args)
|
||||
end
|
||||
|
||||
local argtabkey,argtab=next(tab.plugins)
|
||||
local v
|
||||
|
||||
return function(_, k)
|
||||
while argtab do
|
||||
k,v = next(argtab, k)
|
||||
if k then return k,v end
|
||||
if argtab==tab.args then
|
||||
argtab=nil
|
||||
else
|
||||
argtabkey,argtab = next(tab.plugins, argtabkey)
|
||||
if not argtabkey then
|
||||
argtab=tab.args
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function checkhidden(info, inputpos, tab)
|
||||
if tab.cmdHidden~=nil then
|
||||
return tab.cmdHidden
|
||||
end
|
||||
local hidden = tab.hidden
|
||||
if type(hidden) == "function" or type(hidden) == "string" then
|
||||
info.hidden = hidden
|
||||
hidden = callmethod(info, inputpos, tab, 'hidden')
|
||||
info.hidden = nil
|
||||
end
|
||||
return hidden
|
||||
end
|
||||
|
||||
local function showhelp(info, inputpos, tab, depth, noHead)
|
||||
if not noHead then
|
||||
print("|cff33ff99"..info.appName.."|r: Arguments to |cffffff78/"..info[0].."|r "..strsub(info.input,1,inputpos-1)..":")
|
||||
end
|
||||
|
||||
local sortTbl = {} -- [1..n]=name
|
||||
local refTbl = {} -- [name]=tableref
|
||||
|
||||
for k,v in iterateargs(tab) do
|
||||
if not refTbl[k] then -- a plugin overriding something in .args
|
||||
tinsert(sortTbl, k)
|
||||
refTbl[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
tsort(sortTbl, function(one, two)
|
||||
local o1 = refTbl[one].order or 100
|
||||
local o2 = refTbl[two].order or 100
|
||||
if type(o1) == "function" or type(o1) == "string" then
|
||||
info.order = o1
|
||||
info[#info+1] = one
|
||||
o1 = callmethod(info, inputpos, refTbl[one], "order")
|
||||
info[#info] = nil
|
||||
info.order = nil
|
||||
end
|
||||
if type(o2) == "function" or type(o1) == "string" then
|
||||
info.order = o2
|
||||
info[#info+1] = two
|
||||
o2 = callmethod(info, inputpos, refTbl[two], "order")
|
||||
info[#info] = nil
|
||||
info.order = nil
|
||||
end
|
||||
if o1<0 and o2<0 then return o1<o2 end
|
||||
if o2<0 then return true end
|
||||
if o1<0 then return false end
|
||||
if o1==o2 then return tostring(one)<tostring(two) end -- compare names
|
||||
return o1<o2
|
||||
end)
|
||||
|
||||
for i = 1, #sortTbl do
|
||||
local k = sortTbl[i]
|
||||
local v = refTbl[k]
|
||||
if not checkhidden(info, inputpos, v) then
|
||||
if v.type ~= "description" and v.type ~= "header" then
|
||||
-- recursively show all inline groups
|
||||
local name, desc = v.name, v.desc
|
||||
if type(name) == "function" then
|
||||
name = callfunction(info, v, 'name')
|
||||
end
|
||||
if type(desc) == "function" then
|
||||
desc = callfunction(info, v, 'desc')
|
||||
end
|
||||
if v.type == "group" and pickfirstset(v.cmdInline, v.inline, false) then
|
||||
print(" "..(desc or name)..":")
|
||||
local oldhandler,oldhandler_at = getparam(info, inputpos, v, depth, "handler", handlertypes, handlermsg)
|
||||
showhelp(info, inputpos, v, depth, true)
|
||||
info.handler,info.handler_at = oldhandler,oldhandler_at
|
||||
else
|
||||
local key = k:gsub(" ", "_")
|
||||
print(" |cffffff78"..key.."|r - "..(desc or name or ""))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function keybindingValidateFunc(text)
|
||||
if text == nil or text == "NONE" then
|
||||
return nil
|
||||
end
|
||||
text = text:upper()
|
||||
local shift, ctrl, alt
|
||||
local modifier
|
||||
while true do
|
||||
if text == "-" then
|
||||
break
|
||||
end
|
||||
modifier, text = strsplit('-', text, 2)
|
||||
if text then
|
||||
if modifier ~= "SHIFT" and modifier ~= "CTRL" and modifier ~= "ALT" then
|
||||
return false
|
||||
end
|
||||
if modifier == "SHIFT" then
|
||||
if shift then
|
||||
return false
|
||||
end
|
||||
shift = true
|
||||
end
|
||||
if modifier == "CTRL" then
|
||||
if ctrl then
|
||||
return false
|
||||
end
|
||||
ctrl = true
|
||||
end
|
||||
if modifier == "ALT" then
|
||||
if alt then
|
||||
return false
|
||||
end
|
||||
alt = true
|
||||
end
|
||||
else
|
||||
text = modifier
|
||||
break
|
||||
end
|
||||
end
|
||||
if text == "" then
|
||||
return false
|
||||
end
|
||||
if not text:find("^F%d+$") and text ~= "CAPSLOCK" and text:len() ~= 1 and (text:byte() < 128 or text:len() > 4) and not _G["KEY_" .. text] then
|
||||
return false
|
||||
end
|
||||
local s = text
|
||||
if shift then
|
||||
s = "SHIFT-" .. s
|
||||
end
|
||||
if ctrl then
|
||||
s = "CTRL-" .. s
|
||||
end
|
||||
if alt then
|
||||
s = "ALT-" .. s
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- handle() - selfrecursing function that processes input->optiontable
|
||||
-- - depth - starts at 0
|
||||
-- - retfalse - return false rather than produce error if a match is not found (used by inlined groups)
|
||||
|
||||
local function handle(info, inputpos, tab, depth, retfalse)
|
||||
|
||||
if not(type(tab)=="table" and type(tab.type)=="string") then err(info,inputpos) end
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Grab hold of handler,set,get,func,etc if set (and remember old ones)
|
||||
-- Note that we do NOT validate if method names are correct at this stage,
|
||||
-- the handler may change before they're actually used!
|
||||
|
||||
local oldhandler,oldhandler_at = getparam(info,inputpos,tab,depth,"handler",handlertypes,handlermsg)
|
||||
local oldset,oldset_at = getparam(info,inputpos,tab,depth,"set",functypes,funcmsg)
|
||||
local oldget,oldget_at = getparam(info,inputpos,tab,depth,"get",functypes,funcmsg)
|
||||
local oldfunc,oldfunc_at = getparam(info,inputpos,tab,depth,"func",functypes,funcmsg)
|
||||
local oldvalidate,oldvalidate_at = getparam(info,inputpos,tab,depth,"validate",functypes,funcmsg)
|
||||
--local oldconfirm,oldconfirm_at = getparam(info,inputpos,tab,depth,"confirm",functypes,funcmsg)
|
||||
|
||||
-------------------------------------------------------------------
|
||||
-- Act according to .type of this table
|
||||
|
||||
if tab.type=="group" then
|
||||
------------ group --------------------------------------------
|
||||
|
||||
if type(tab.args)~="table" then err(info, inputpos) end
|
||||
if tab.plugins and type(tab.plugins)~="table" then err(info,inputpos) end
|
||||
|
||||
-- grab next arg from input
|
||||
local _,nextpos,arg = (info.input):find(" *([^ ]+) *", inputpos)
|
||||
if not arg then
|
||||
showhelp(info, inputpos, tab, depth)
|
||||
return
|
||||
end
|
||||
nextpos=nextpos+1
|
||||
|
||||
-- loop .args and try to find a key with a matching name
|
||||
for k,v in iterateargs(tab) do
|
||||
if not(type(k)=="string" and type(v)=="table" and type(v.type)=="string") then err(info,inputpos, "options table child '"..tostring(k).."' is malformed") end
|
||||
|
||||
-- is this child an inline group? if so, traverse into it
|
||||
if v.type=="group" and pickfirstset(v.cmdInline, v.inline, false) then
|
||||
info[depth+1] = k
|
||||
if handle(info, inputpos, v, depth+1, true)==false then
|
||||
info[depth+1] = nil
|
||||
-- wasn't found in there, but that's ok, we just keep looking down here
|
||||
else
|
||||
return -- done, name was found in inline group
|
||||
end
|
||||
-- matching name and not a inline group
|
||||
elseif strlower(arg)==strlower(k:gsub(" ", "_")) then
|
||||
info[depth+1] = k
|
||||
return handle(info,nextpos,v,depth+1)
|
||||
end
|
||||
end
|
||||
|
||||
-- no match
|
||||
if retfalse then
|
||||
-- restore old infotable members and return false to indicate failure
|
||||
info.handler,info.handler_at = oldhandler,oldhandler_at
|
||||
info.set,info.set_at = oldset,oldset_at
|
||||
info.get,info.get_at = oldget,oldget_at
|
||||
info.func,info.func_at = oldfunc,oldfunc_at
|
||||
info.validate,info.validate_at = oldvalidate,oldvalidate_at
|
||||
--info.confirm,info.confirm_at = oldconfirm,oldconfirm_at
|
||||
return false
|
||||
end
|
||||
|
||||
-- couldn't find the command, display error
|
||||
usererr(info, inputpos, "'"..arg.."' - " .. L["unknown argument"])
|
||||
return
|
||||
end
|
||||
|
||||
local str = strsub(info.input,inputpos);
|
||||
|
||||
if tab.type=="execute" then
|
||||
------------ execute --------------------------------------------
|
||||
do_final(info, inputpos, tab, "func")
|
||||
|
||||
|
||||
|
||||
elseif tab.type=="input" then
|
||||
------------ input --------------------------------------------
|
||||
|
||||
local res = true
|
||||
if tab.pattern then
|
||||
if not(type(tab.pattern)=="string") then err(info, inputpos, "'pattern' - expected a string") end
|
||||
if not strmatch(str, tab.pattern) then
|
||||
usererr(info, inputpos, "'"..str.."' - " .. L["invalid input"])
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", str)
|
||||
|
||||
|
||||
|
||||
elseif tab.type=="toggle" then
|
||||
------------ toggle --------------------------------------------
|
||||
local b
|
||||
local str = strtrim(strlower(str))
|
||||
if str=="" then
|
||||
b = callmethod(info, inputpos, tab, "get")
|
||||
|
||||
if tab.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if b then
|
||||
b = nil
|
||||
elseif b == nil then
|
||||
b = false
|
||||
else
|
||||
b = true
|
||||
end
|
||||
else
|
||||
b = not b
|
||||
end
|
||||
|
||||
elseif str==L["on"] then
|
||||
b = true
|
||||
elseif str==L["off"] then
|
||||
b = false
|
||||
elseif tab.tristate and str==L["default"] then
|
||||
b = nil
|
||||
else
|
||||
if tab.tristate then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'on', 'off' or 'default', or no argument to toggle."], str))
|
||||
else
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'on' or 'off', or no argument to toggle."], str))
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", b)
|
||||
|
||||
|
||||
elseif tab.type=="range" then
|
||||
------------ range --------------------------------------------
|
||||
local val = tonumber(str)
|
||||
if not val then
|
||||
usererr(info, inputpos, "'"..str.."' - "..L["expected number"])
|
||||
return
|
||||
end
|
||||
if type(info.step)=="number" then
|
||||
val = val- (val % info.step)
|
||||
end
|
||||
if type(info.min)=="number" and val<info.min then
|
||||
usererr(info, inputpos, val.." - "..format(L["must be equal to or higher than %s"], tostring(info.min)) )
|
||||
return
|
||||
end
|
||||
if type(info.max)=="number" and val>info.max then
|
||||
usererr(info, inputpos, val.." - "..format(L["must be equal to or lower than %s"], tostring(info.max)) )
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", val)
|
||||
|
||||
|
||||
elseif tab.type=="select" then
|
||||
------------ select ------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
|
||||
local values = tab.values
|
||||
if type(values) == "function" or type(values) == "string" then
|
||||
info.values = values
|
||||
values = callmethod(info, inputpos, tab, "values")
|
||||
info.values = nil
|
||||
end
|
||||
|
||||
if str == "" then
|
||||
local b = callmethod(info, inputpos, tab, "get")
|
||||
local fmt = "|cffffff78- [%s]|r %s"
|
||||
local fmt_sel = "|cffffff78- [%s]|r %s |cffff0000*|r"
|
||||
print(L["Options for |cffffff78"..info[#info].."|r:"])
|
||||
for k, v in pairs(values) do
|
||||
if b == k then
|
||||
print(fmt_sel:format(k, v))
|
||||
else
|
||||
print(fmt:format(k, v))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local ok
|
||||
for k,v in pairs(values) do
|
||||
if strlower(k)==str then
|
||||
str = k -- overwrite with key (in case of case mismatches)
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not ok then
|
||||
usererr(info, inputpos, "'"..str.."' - "..L["unknown selection"])
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", str)
|
||||
|
||||
elseif tab.type=="multiselect" then
|
||||
------------ multiselect -------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
|
||||
local values = tab.values
|
||||
if type(values) == "function" or type(values) == "string" then
|
||||
info.values = values
|
||||
values = callmethod(info, inputpos, tab, "values")
|
||||
info.values = nil
|
||||
end
|
||||
|
||||
if str == "" then
|
||||
local fmt = "|cffffff78- [%s]|r %s"
|
||||
local fmt_sel = "|cffffff78- [%s]|r %s |cffff0000*|r"
|
||||
print(L["Options for |cffffff78"..info[#info].."|r (multiple possible):"])
|
||||
for k, v in pairs(values) do
|
||||
if callmethod(info, inputpos, tab, "get", k) then
|
||||
print(fmt_sel:format(k, v))
|
||||
else
|
||||
print(fmt:format(k, v))
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
--build a table of the selections, checking that they exist
|
||||
--parse for =on =off =default in the process
|
||||
--table will be key = true for options that should toggle, key = [on|off|default] for options to be set
|
||||
local sels = {}
|
||||
for v in str:gmatch("[^ ]+") do
|
||||
--parse option=on etc
|
||||
local opt, val = v:match('(.+)=(.+)')
|
||||
--get option if toggling
|
||||
if not opt then
|
||||
opt = v
|
||||
end
|
||||
|
||||
--check that the opt is valid
|
||||
local ok
|
||||
for k,v in pairs(values) do
|
||||
if strlower(k)==opt then
|
||||
opt = k -- overwrite with key (in case of case mismatches)
|
||||
ok = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not ok then
|
||||
usererr(info, inputpos, "'"..opt.."' - "..L["unknown selection"])
|
||||
return
|
||||
end
|
||||
|
||||
--check that if val was supplied it is valid
|
||||
if val then
|
||||
if val == L["on"] or val == L["off"] or (tab.tristate and val == L["default"]) then
|
||||
--val is valid insert it
|
||||
sels[opt] = val
|
||||
else
|
||||
if tab.tristate then
|
||||
usererr(info, inputpos, format(L["'%s' '%s' - expected 'on', 'off' or 'default', or no argument to toggle."], v, val))
|
||||
else
|
||||
usererr(info, inputpos, format(L["'%s' '%s' - expected 'on' or 'off', or no argument to toggle."], v, val))
|
||||
end
|
||||
return
|
||||
end
|
||||
else
|
||||
-- no val supplied, toggle
|
||||
sels[opt] = true
|
||||
end
|
||||
end
|
||||
|
||||
for opt, val in pairs(sels) do
|
||||
local newval
|
||||
|
||||
if (val == true) then
|
||||
--toggle the option
|
||||
local b = callmethod(info, inputpos, tab, "get", opt)
|
||||
|
||||
if tab.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if b then
|
||||
b = nil
|
||||
elseif b == nil then
|
||||
b = false
|
||||
else
|
||||
b = true
|
||||
end
|
||||
else
|
||||
b = not b
|
||||
end
|
||||
newval = b
|
||||
else
|
||||
--set the option as specified
|
||||
if val==L["on"] then
|
||||
newval = true
|
||||
elseif val==L["off"] then
|
||||
newval = false
|
||||
elseif val==L["default"] then
|
||||
newval = nil
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", opt, newval)
|
||||
end
|
||||
|
||||
|
||||
elseif tab.type=="color" then
|
||||
------------ color --------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
if str == "" then
|
||||
--TODO: Show current value
|
||||
return
|
||||
end
|
||||
|
||||
local r, g, b, a
|
||||
|
||||
if tab.hasAlpha then
|
||||
if str:len() == 8 and str:find("^%x*$") then
|
||||
--parse a hex string
|
||||
r,g,b,a = tonumber(str:sub(1, 2), 16) / 255, tonumber(str:sub(3, 4), 16) / 255, tonumber(str:sub(5, 6), 16) / 255, tonumber(str:sub(7, 8), 16) / 255
|
||||
else
|
||||
--parse seperate values
|
||||
r,g,b,a = str:match("^([%d%.]+) ([%d%.]+) ([%d%.]+) ([%d%.]+)$")
|
||||
r,g,b,a = tonumber(r), tonumber(g), tonumber(b), tonumber(a)
|
||||
end
|
||||
if not (r and g and b and a) then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'RRGGBBAA' or 'r g b a'."], str))
|
||||
return
|
||||
end
|
||||
|
||||
if r >= 0.0 and r <= 1.0 and g >= 0.0 and g <= 1.0 and b >= 0.0 and b <= 1.0 and a >= 0.0 and a <= 1.0 then
|
||||
--values are valid
|
||||
elseif r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255 and a >= 0 and a <= 255 then
|
||||
--values are valid 0..255, convert to 0..1
|
||||
r = r / 255
|
||||
g = g / 255
|
||||
b = b / 255
|
||||
a = a / 255
|
||||
else
|
||||
--values are invalid
|
||||
usererr(info, inputpos, format(L["'%s' - values must all be either in the range 0..1 or 0..255."], str))
|
||||
end
|
||||
else
|
||||
a = 1.0
|
||||
if str:len() == 6 and str:find("^%x*$") then
|
||||
--parse a hex string
|
||||
r,g,b = tonumber(str:sub(1, 2), 16) / 255, tonumber(str:sub(3, 4), 16) / 255, tonumber(str:sub(5, 6), 16) / 255
|
||||
else
|
||||
--parse seperate values
|
||||
r,g,b = str:match("^([%d%.]+) ([%d%.]+) ([%d%.]+)$")
|
||||
r,g,b = tonumber(r), tonumber(g), tonumber(b)
|
||||
end
|
||||
if not (r and g and b) then
|
||||
usererr(info, inputpos, format(L["'%s' - expected 'RRGGBB' or 'r g b'."], str))
|
||||
return
|
||||
end
|
||||
if r >= 0.0 and r <= 1.0 and g >= 0.0 and g <= 1.0 and b >= 0.0 and b <= 1.0 then
|
||||
--values are valid
|
||||
elseif r >= 0 and r <= 255 and g >= 0 and g <= 255 and b >= 0 and b <= 255 then
|
||||
--values are valid 0..255, convert to 0..1
|
||||
r = r / 255
|
||||
g = g / 255
|
||||
b = b / 255
|
||||
else
|
||||
--values are invalid
|
||||
usererr(info, inputpos, format(L["'%s' - values must all be either in the range 0-1 or 0-255."], str))
|
||||
end
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", r,g,b,a)
|
||||
|
||||
elseif tab.type=="keybinding" then
|
||||
------------ keybinding --------------------------------------------
|
||||
local str = strtrim(strlower(str))
|
||||
if str == "" then
|
||||
--TODO: Show current value
|
||||
return
|
||||
end
|
||||
local value = keybindingValidateFunc(str:upper())
|
||||
if value == false then
|
||||
usererr(info, inputpos, format(L["'%s' - Invalid Keybinding."], str))
|
||||
return
|
||||
end
|
||||
|
||||
do_final(info, inputpos, tab, "set", value)
|
||||
|
||||
elseif tab.type=="description" then
|
||||
------------ description --------------------
|
||||
-- ignore description, GUI config only
|
||||
else
|
||||
err(info, inputpos, "unknown options table item type '"..tostring(tab.type).."'")
|
||||
end
|
||||
end
|
||||
|
||||
--- Handle the chat command.
|
||||
-- This is usually called from a chat command handler to parse the command input as operations on an aceoptions table.\\
|
||||
-- AceConfigCmd uses this function internally when a slash command is registered with `:CreateChatCommand`
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param input The commandline input (as given by the WoW handler, i.e. without the command itself)
|
||||
-- @usage
|
||||
-- MyAddon = LibStub("AceAddon-3.0"):NewAddon("MyAddon", "AceConsole-3.0")
|
||||
-- -- Use AceConsole-3.0 to register a Chat Command
|
||||
-- MyAddon:RegisterChatCommand("mychat", "ChatCommand")
|
||||
--
|
||||
-- -- Show the GUI if no input is supplied, otherwise handle the chat input.
|
||||
-- function MyAddon:ChatCommand(input)
|
||||
-- -- Assuming "MyOptions" is the appName of a valid options table
|
||||
-- if not input or input:trim() == "" then
|
||||
-- LibStub("AceConfigDialog-3.0"):Open("MyOptions")
|
||||
-- else
|
||||
-- LibStub("AceConfigCmd-3.0").HandleCommand(MyAddon, "mychat", "MyOptions", input)
|
||||
-- end
|
||||
-- end
|
||||
function AceConfigCmd:HandleCommand(slashcmd, appName, input)
|
||||
|
||||
local optgetter = cfgreg:GetOptionsTable(appName)
|
||||
if not optgetter then
|
||||
error([[Usage: HandleCommand("slashcmd", "appName", "input"): 'appName' - no options table "]]..tostring(appName)..[[" has been registered]], 2)
|
||||
end
|
||||
local options = assert( optgetter("cmd", MAJOR) )
|
||||
|
||||
local info = { -- Don't try to recycle this, it gets handed off to callbacks and whatnot
|
||||
[0] = slashcmd,
|
||||
appName = appName,
|
||||
options = options,
|
||||
input = input,
|
||||
self = self,
|
||||
handler = self,
|
||||
uiType = "cmd",
|
||||
uiName = MAJOR,
|
||||
}
|
||||
|
||||
handle(info, 1, options, 0) -- (info, inputpos, table, depth)
|
||||
end
|
||||
|
||||
--- Utility function to create a slash command handler.
|
||||
-- Also registers tab completion with AceTab
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
function AceConfigCmd:CreateChatCommand(slashcmd, appName)
|
||||
if not AceConsole then
|
||||
AceConsole = LibStub(AceConsoleName)
|
||||
end
|
||||
if AceConsole.RegisterChatCommand(self, slashcmd, function(input)
|
||||
AceConfigCmd.HandleCommand(self, slashcmd, appName, input) -- upgradable
|
||||
end,
|
||||
true) then -- succesfully registered so lets get the command -> app table in
|
||||
commands[slashcmd] = appName
|
||||
end
|
||||
end
|
||||
|
||||
--- Utility function that returns the options table that belongs to a slashcommand.
|
||||
-- Designed to be used for the AceTab interface.
|
||||
-- @param slashcmd The slash command WITHOUT leading slash (only used for error output)
|
||||
-- @return The options table associated with the slash command (or nil if the slash command was not registered)
|
||||
function AceConfigCmd:GetChatCommandOptions(slashcmd)
|
||||
return commands[slashcmd]
|
||||
end
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConfigCmd-3.0.lua"/>
|
||||
</Ui>
|
||||
+1889
File diff suppressed because it is too large
Load Diff
+4
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConfigDialog-3.0.lua"/>
|
||||
</Ui>
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
--- AceConfigRegistry-3.0 handles central registration of options tables in use by addons and modules.\\
|
||||
-- Options tables can be registered as raw tables, OR as function refs that return a table.\\
|
||||
-- Such functions receive three arguments: "uiType", "uiName", "appName". \\
|
||||
-- * Valid **uiTypes**: "cmd", "dropdown", "dialog". This is verified by the library at call time. \\
|
||||
-- * The **uiName** field is expected to contain the full name of the calling addon, including version, e.g. "FooBar-1.0". This is verified by the library at call time.\\
|
||||
-- * The **appName** field is the options table name as given at registration time \\
|
||||
--
|
||||
-- :IterateOptionsTables() (and :GetOptionsTable() if only given one argument) return a function reference that the requesting config handling addon must call with valid "uiType", "uiName".
|
||||
-- @class file
|
||||
-- @name AceConfigRegistry-3.0
|
||||
-- @release $Id: AceConfigRegistry-3.0.lua 921 2010-05-09 15:49:14Z nevcairiel $
|
||||
local MAJOR, MINOR = "AceConfigRegistry-3.0", 12
|
||||
local AceConfigRegistry = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not AceConfigRegistry then return end
|
||||
|
||||
AceConfigRegistry.tables = AceConfigRegistry.tables or {}
|
||||
|
||||
local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
|
||||
|
||||
if not AceConfigRegistry.callbacks then
|
||||
AceConfigRegistry.callbacks = CallbackHandler:New(AceConfigRegistry)
|
||||
end
|
||||
|
||||
-- Lua APIs
|
||||
local tinsert, tconcat = table.insert, table.concat
|
||||
local strfind, strmatch = string.find, string.match
|
||||
local type, tostring, select, pairs = type, tostring, select, pairs
|
||||
local error, assert = error, assert
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Validating options table consistency:
|
||||
|
||||
|
||||
AceConfigRegistry.validated = {
|
||||
-- list of options table names ran through :ValidateOptionsTable automatically.
|
||||
-- CLEARED ON PURPOSE, since newer versions may have newer validators
|
||||
cmd = {},
|
||||
dropdown = {},
|
||||
dialog = {},
|
||||
}
|
||||
|
||||
|
||||
|
||||
local function err(msg, errlvl, ...)
|
||||
local t = {}
|
||||
for i=select("#",...),1,-1 do
|
||||
tinsert(t, (select(i, ...)))
|
||||
end
|
||||
error(MAJOR..":ValidateOptionsTable(): "..tconcat(t,".")..msg, errlvl+2)
|
||||
end
|
||||
|
||||
|
||||
local isstring={["string"]=true, _="string"}
|
||||
local isstringfunc={["string"]=true,["function"]=true, _="string or funcref"}
|
||||
local istable={["table"]=true, _="table"}
|
||||
local ismethodtable={["table"]=true,["string"]=true,["function"]=true, _="methodname, funcref or table"}
|
||||
local optstring={["nil"]=true,["string"]=true, _="string"}
|
||||
local optstringfunc={["nil"]=true,["string"]=true,["function"]=true, _="string or funcref"}
|
||||
local optnumber={["nil"]=true,["number"]=true, _="number"}
|
||||
local optmethod={["nil"]=true,["string"]=true,["function"]=true, _="methodname or funcref"}
|
||||
local optmethodfalse={["nil"]=true,["string"]=true,["function"]=true,["boolean"]={[false]=true}, _="methodname, funcref or false"}
|
||||
local optmethodnumber={["nil"]=true,["string"]=true,["function"]=true,["number"]=true, _="methodname, funcref or number"}
|
||||
local optmethodtable={["nil"]=true,["string"]=true,["function"]=true,["table"]=true, _="methodname, funcref or table"}
|
||||
local optmethodbool={["nil"]=true,["string"]=true,["function"]=true,["boolean"]=true, _="methodname, funcref or boolean"}
|
||||
local opttable={["nil"]=true,["table"]=true, _="table"}
|
||||
local optbool={["nil"]=true,["boolean"]=true, _="boolean"}
|
||||
local optboolnumber={["nil"]=true,["boolean"]=true,["number"]=true, _="boolean or number"}
|
||||
|
||||
local basekeys={
|
||||
type=isstring,
|
||||
name=isstringfunc,
|
||||
desc=optstringfunc,
|
||||
descStyle=optstring,
|
||||
order=optmethodnumber,
|
||||
validate=optmethodfalse,
|
||||
confirm=optmethodbool,
|
||||
confirmText=optstring,
|
||||
disabled=optmethodbool,
|
||||
hidden=optmethodbool,
|
||||
guiHidden=optmethodbool,
|
||||
dialogHidden=optmethodbool,
|
||||
dropdownHidden=optmethodbool,
|
||||
cmdHidden=optmethodbool,
|
||||
icon=optstringfunc,
|
||||
iconCoords=optmethodtable,
|
||||
handler=opttable,
|
||||
get=optmethodfalse,
|
||||
set=optmethodfalse,
|
||||
func=optmethodfalse,
|
||||
arg={["*"]=true},
|
||||
width=optstring,
|
||||
}
|
||||
|
||||
local typedkeys={
|
||||
header={},
|
||||
description={
|
||||
image=optstringfunc,
|
||||
imageCoords=optmethodtable,
|
||||
imageHeight=optnumber,
|
||||
imageWidth=optnumber,
|
||||
fontSize=optstringfunc,
|
||||
},
|
||||
group={
|
||||
args=istable,
|
||||
plugins=opttable,
|
||||
inline=optbool,
|
||||
cmdInline=optbool,
|
||||
guiInline=optbool,
|
||||
dropdownInline=optbool,
|
||||
dialogInline=optbool,
|
||||
childGroups=optstring,
|
||||
},
|
||||
execute={
|
||||
image=optstringfunc,
|
||||
imageCoords=optmethodtable,
|
||||
imageHeight=optnumber,
|
||||
imageWidth=optnumber,
|
||||
},
|
||||
input={
|
||||
pattern=optstring,
|
||||
usage=optstring,
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
multiline=optboolnumber,
|
||||
},
|
||||
toggle={
|
||||
tristate=optbool,
|
||||
image=optstringfunc,
|
||||
imageCoords=optmethodtable,
|
||||
},
|
||||
tristate={
|
||||
},
|
||||
range={
|
||||
min=optnumber,
|
||||
softMin=optnumber,
|
||||
max=optnumber,
|
||||
softMax=optnumber,
|
||||
step=optnumber,
|
||||
bigStep=optnumber,
|
||||
isPercent=optbool,
|
||||
},
|
||||
select={
|
||||
values=ismethodtable,
|
||||
style={
|
||||
["nil"]=true,
|
||||
["string"]={dropdown=true,radio=true},
|
||||
_="string: 'dropdown' or 'radio'"
|
||||
},
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
},
|
||||
multiselect={
|
||||
values=ismethodtable,
|
||||
style=optstring,
|
||||
tristate=optbool,
|
||||
control=optstring,
|
||||
dialogControl=optstring,
|
||||
dropdownControl=optstring,
|
||||
},
|
||||
color={
|
||||
hasAlpha=optbool,
|
||||
},
|
||||
keybinding={
|
||||
-- TODO
|
||||
},
|
||||
}
|
||||
|
||||
local function validateKey(k,errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
if type(k)~="string" then
|
||||
err("["..tostring(k).."] - key is not a string", errlvl,...)
|
||||
end
|
||||
if strfind(k, "[%c\127]") then
|
||||
err("["..tostring(k).."] - key name contained control characters", errlvl,...)
|
||||
end
|
||||
end
|
||||
|
||||
local function validateVal(v, oktypes, errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
local isok=oktypes[type(v)] or oktypes["*"]
|
||||
|
||||
if not isok then
|
||||
err(": expected a "..oktypes._..", got '"..tostring(v).."'", errlvl,...)
|
||||
end
|
||||
if type(isok)=="table" then -- isok was a table containing specific values to be tested for!
|
||||
if not isok[v] then
|
||||
err(": did not expect "..type(v).." value '"..tostring(v).."'", errlvl,...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function validate(options,errlvl,...)
|
||||
errlvl=(errlvl or 0)+1
|
||||
-- basic consistency
|
||||
if type(options)~="table" then
|
||||
err(": expected a table, got a "..type(options), errlvl,...)
|
||||
end
|
||||
if type(options.type)~="string" then
|
||||
err(".type: expected a string, got a "..type(options.type), errlvl,...)
|
||||
end
|
||||
|
||||
-- get type and 'typedkeys' member
|
||||
local tk = typedkeys[options.type]
|
||||
if not tk then
|
||||
err(".type: unknown type '"..options.type.."'", errlvl,...)
|
||||
end
|
||||
|
||||
-- make sure that all options[] are known parameters
|
||||
for k,v in pairs(options) do
|
||||
if not (tk[k] or basekeys[k]) then
|
||||
err(": unknown parameter", errlvl,tostring(k),...)
|
||||
end
|
||||
end
|
||||
|
||||
-- verify that required params are there, and that everything is the right type
|
||||
for k,oktypes in pairs(basekeys) do
|
||||
validateVal(options[k], oktypes, errlvl,k,...)
|
||||
end
|
||||
for k,oktypes in pairs(tk) do
|
||||
validateVal(options[k], oktypes, errlvl,k,...)
|
||||
end
|
||||
|
||||
-- extra logic for groups
|
||||
if options.type=="group" then
|
||||
for k,v in pairs(options.args) do
|
||||
validateKey(k,errlvl,"args",...)
|
||||
validate(v, errlvl,k,"args",...)
|
||||
end
|
||||
if options.plugins then
|
||||
for plugname,plugin in pairs(options.plugins) do
|
||||
if type(plugin)~="table" then
|
||||
err(": expected a table, got '"..tostring(plugin).."'", errlvl,tostring(plugname),"plugins",...)
|
||||
end
|
||||
for k,v in pairs(plugin) do
|
||||
validateKey(k,errlvl,tostring(plugname),"plugins",...)
|
||||
validate(v, errlvl,k,tostring(plugname),"plugins",...)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Validates basic structure and integrity of an options table \\
|
||||
-- Does NOT verify that get/set etc actually exist, since they can be defined at any depth
|
||||
-- @param options The table to be validated
|
||||
-- @param name The name of the table to be validated (shown in any error message)
|
||||
-- @param errlvl (optional number) error level offset, default 0 (=errors point to the function calling :ValidateOptionsTable)
|
||||
function AceConfigRegistry:ValidateOptionsTable(options,name,errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
name = name or "Optionstable"
|
||||
if not options.name then
|
||||
options.name=name -- bit of a hack, the root level doesn't really need a .name :-/
|
||||
end
|
||||
validate(options,errlvl,name)
|
||||
end
|
||||
|
||||
--- Fires a "ConfigTableChange" callback for those listening in on it, allowing config GUIs to refresh.
|
||||
-- You should call this function if your options table changed from any outside event, like a game event
|
||||
-- or a timer.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
function AceConfigRegistry:NotifyChange(appName)
|
||||
if not AceConfigRegistry.tables[appName] then return end
|
||||
AceConfigRegistry.callbacks:Fire("ConfigTableChange", appName)
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Registering and retreiving options tables:
|
||||
|
||||
|
||||
-- validateGetterArgs: helper function for :GetOptionsTable (or, rather, the getter functions returned by it)
|
||||
|
||||
local function validateGetterArgs(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+2
|
||||
if uiType~="cmd" and uiType~="dropdown" and uiType~="dialog" then
|
||||
error(MAJOR..": Requesting options table: 'uiType' - invalid configuration UI type, expected 'cmd', 'dropdown' or 'dialog'", errlvl)
|
||||
end
|
||||
if not strmatch(uiName, "[A-Za-z]%-[0-9]") then -- Expecting e.g. "MyLib-1.2"
|
||||
error(MAJOR..": Requesting options table: 'uiName' - badly formatted or missing version number. Expected e.g. 'MyLib-1.2'", errlvl)
|
||||
end
|
||||
end
|
||||
|
||||
--- Register an options table with the config registry.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param options The options table, OR a function reference that generates it on demand. \\
|
||||
-- See the top of the page for info on arguments passed to such functions.
|
||||
function AceConfigRegistry:RegisterOptionsTable(appName, options)
|
||||
if type(options)=="table" then
|
||||
if options.type~="group" then -- quick sanity checker
|
||||
error(MAJOR..": RegisterOptionsTable(appName, options): 'options' - missing type='group' member in root group", 2)
|
||||
end
|
||||
AceConfigRegistry.tables[appName] = function(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
validateGetterArgs(uiType, uiName, errlvl)
|
||||
if not AceConfigRegistry.validated[uiType][appName] then
|
||||
AceConfigRegistry:ValidateOptionsTable(options, appName, errlvl) -- upgradable
|
||||
AceConfigRegistry.validated[uiType][appName] = true
|
||||
end
|
||||
return options
|
||||
end
|
||||
elseif type(options)=="function" then
|
||||
AceConfigRegistry.tables[appName] = function(uiType, uiName, errlvl)
|
||||
errlvl=(errlvl or 0)+1
|
||||
validateGetterArgs(uiType, uiName, errlvl)
|
||||
local tab = assert(options(uiType, uiName, appName))
|
||||
if not AceConfigRegistry.validated[uiType][appName] then
|
||||
AceConfigRegistry:ValidateOptionsTable(tab, appName, errlvl) -- upgradable
|
||||
AceConfigRegistry.validated[uiType][appName] = true
|
||||
end
|
||||
return tab
|
||||
end
|
||||
else
|
||||
error(MAJOR..": RegisterOptionsTable(appName, options): 'options' - expected table or function reference", 2)
|
||||
end
|
||||
end
|
||||
|
||||
--- Returns an iterator of ["appName"]=funcref pairs
|
||||
function AceConfigRegistry:IterateOptionsTables()
|
||||
return pairs(AceConfigRegistry.tables)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
--- Query the registry for a specific options table.
|
||||
-- If only appName is given, a function is returned which you
|
||||
-- can call with (uiType,uiName) to get the table.\\
|
||||
-- If uiType&uiName are given, the table is returned.
|
||||
-- @param appName The application name as given to `:RegisterOptionsTable()`
|
||||
-- @param uiType The type of UI to get the table for, one of "cmd", "dropdown", "dialog"
|
||||
-- @param uiName The name of the library/addon querying for the table, e.g. "MyLib-1.0"
|
||||
function AceConfigRegistry:GetOptionsTable(appName, uiType, uiName)
|
||||
local f = AceConfigRegistry.tables[appName]
|
||||
if not f then
|
||||
return nil
|
||||
end
|
||||
|
||||
if uiType then
|
||||
return f(uiType,uiName,1) -- get the table for us
|
||||
else
|
||||
return f -- return the function
|
||||
end
|
||||
end
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceConfigRegistry-3.0.lua"/>
|
||||
</Ui>
|
||||
+6
-6
@@ -39,8 +39,8 @@
|
||||
-- end
|
||||
-- @class file
|
||||
-- @name AceDB-3.0.lua
|
||||
-- @release $Id: AceDB-3.0.lua 940 2010-06-19 08:01:47Z nevcairiel $
|
||||
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 21
|
||||
-- @release $Id: AceDB-3.0.lua 914 2010-03-08 12:09:22Z nevcairiel $
|
||||
local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 20
|
||||
local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)
|
||||
|
||||
if not AceDB then return end -- No upgrade needed
|
||||
@@ -507,11 +507,11 @@ function DBObjectLib:DeleteProfile(name, silent)
|
||||
error("Cannot delete the active profile in an AceDBObject.", 2)
|
||||
end
|
||||
|
||||
if not rawget(self.profiles, name) and not silent then
|
||||
if not rawget(self.sv.profiles, name) and not silent then
|
||||
error("Cannot delete profile '" .. name .. "'. It does not exist.", 2)
|
||||
end
|
||||
|
||||
self.profiles[name] = nil
|
||||
self.sv.profiles[name] = nil
|
||||
|
||||
-- populate to child namespaces
|
||||
if self.children then
|
||||
@@ -537,7 +537,7 @@ function DBObjectLib:CopyProfile(name, silent)
|
||||
error("Cannot have the same source and destination profiles.", 2)
|
||||
end
|
||||
|
||||
if not rawget(self.profiles, name) and not silent then
|
||||
if not rawget(self.sv.profiles, name) and not silent then
|
||||
error("Cannot copy profile '" .. name .. "'. It does not exist.", 2)
|
||||
end
|
||||
|
||||
@@ -545,7 +545,7 @@ function DBObjectLib:CopyProfile(name, silent)
|
||||
DBObjectLib.ResetProfile(self, nil, true)
|
||||
|
||||
local profile = self.profile
|
||||
local source = self.profiles[name]
|
||||
local source = self.sv.profiles[name]
|
||||
|
||||
copyTable(source, profile)
|
||||
|
||||
+420
@@ -0,0 +1,420 @@
|
||||
--- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles.
|
||||
-- @class file
|
||||
-- @name AceDBOptions-3.0
|
||||
-- @release $Id: AceDBOptions-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $
|
||||
local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 11
|
||||
local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR)
|
||||
|
||||
if not AceDBOptions then return end -- No upgrade needed
|
||||
|
||||
-- Lua APIs
|
||||
local pairs, next = pairs, next
|
||||
|
||||
-- WoW APIs
|
||||
local UnitClass = UnitClass
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: NORMAL_FONT_COLOR_CODE, FONT_COLOR_CODE_CLOSE
|
||||
|
||||
AceDBOptions.optionTables = AceDBOptions.optionTables or {}
|
||||
AceDBOptions.handlers = AceDBOptions.handlers or {}
|
||||
|
||||
--[[
|
||||
Localization of AceDBOptions-3.0
|
||||
]]
|
||||
|
||||
local L = {
|
||||
default = "Default",
|
||||
intro = "You can change the active database profile, so you can have different settings for every character.",
|
||||
reset_desc = "Reset the current profile back to its default values, in case your configuration is broken, or you simply want to start over.",
|
||||
reset = "Reset Profile",
|
||||
reset_sub = "Reset the current profile to the default",
|
||||
choose_desc = "You can either create a new profile by entering a name in the editbox, or choose one of the already exisiting profiles.",
|
||||
new = "New",
|
||||
new_sub = "Create a new empty profile.",
|
||||
choose = "Existing Profiles",
|
||||
choose_sub = "Select one of your currently available profiles.",
|
||||
copy_desc = "Copy the settings from one existing profile into the currently active profile.",
|
||||
copy = "Copy From",
|
||||
delete_desc = "Delete existing and unused profiles from the database to save space, and cleanup the SavedVariables file.",
|
||||
delete = "Delete a Profile",
|
||||
delete_sub = "Deletes a profile from the database.",
|
||||
delete_confirm = "Are you sure you want to delete the selected profile?",
|
||||
profiles = "Profiles",
|
||||
profiles_sub = "Manage Profiles",
|
||||
current = "Current Profile:",
|
||||
}
|
||||
|
||||
local LOCALE = GetLocale()
|
||||
if LOCALE == "deDE" then
|
||||
L["default"] = "Standard"
|
||||
L["intro"] = "Hier kannst du das aktive Datenbankprofile \195\164ndern, damit du verschiedene Einstellungen f\195\188r jeden Charakter erstellen kannst, wodurch eine sehr flexible Konfiguration m\195\182glich wird."
|
||||
L["reset_desc"] = "Setzt das momentane Profil auf Standardwerte zur\195\188ck, f\195\188r den Fall das mit der Konfiguration etwas schief lief oder weil du einfach neu starten willst."
|
||||
L["reset"] = "Profil zur\195\188cksetzen"
|
||||
L["reset_sub"] = "Das aktuelle Profil auf Standard zur\195\188cksetzen."
|
||||
L["choose_desc"] = "Du kannst ein neues Profil erstellen, indem du einen neuen Namen in der Eingabebox 'Neu' eingibst, oder w\195\164hle eines der vorhandenen Profile aus."
|
||||
L["new"] = "Neu"
|
||||
L["new_sub"] = "Ein neues Profil erstellen."
|
||||
L["choose"] = "Vorhandene Profile"
|
||||
L["choose_sub"] = "W\195\164hlt ein bereits vorhandenes Profil aus."
|
||||
L["copy_desc"] = "Kopiere die Einstellungen von einem vorhandenen Profil in das aktive Profil."
|
||||
L["copy"] = "Kopieren von..."
|
||||
L["delete_desc"] = "L\195\182sche vorhandene oder unbenutzte Profile aus der Datenbank um Platz zu sparen und um die SavedVariables Datei 'sauber' zu halten."
|
||||
L["delete"] = "Profil l\195\182schen"
|
||||
L["delete_sub"] = "L\195\182scht ein Profil aus der Datenbank."
|
||||
L["delete_confirm"] = "Willst du das ausgew\195\164hlte Profil wirklich l\195\182schen?"
|
||||
L["profiles"] = "Profile"
|
||||
L["profiles_sub"] = "Profile verwalten"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "frFR" then
|
||||
L["default"] = "D\195\169faut"
|
||||
L["intro"] = "Vous pouvez changer le profil actuel afin d'avoir des param\195\168tres diff\195\169rents pour chaque personnage, permettant ainsi d'avoir une configuration tr\195\168s flexible."
|
||||
L["reset_desc"] = "R\195\169initialise le profil actuel au cas o\195\185 votre configuration est corrompue ou si vous voulez tout simplement faire table rase."
|
||||
L["reset"] = "R\195\169initialiser le profil"
|
||||
L["reset_sub"] = "R\195\169initialise le profil actuel avec les param\195\168tres par d\195\169faut."
|
||||
L["choose_desc"] = "Vous pouvez cr\195\169er un nouveau profil en entrant un nouveau nom dans la bo\195\174te de saisie, ou en choississant un des profils d\195\169j\195\160 existants."
|
||||
L["new"] = "Nouveau"
|
||||
L["new_sub"] = "Cr\195\169\195\169e un nouveau profil vierge."
|
||||
L["choose"] = "Profils existants"
|
||||
L["choose_sub"] = "Permet de choisir un des profils d\195\169j\195\160 disponibles."
|
||||
L["copy_desc"] = "Copie les param\195\168tres d'un profil d\195\169j\195\160 existant dans le profil actuellement actif."
|
||||
L["copy"] = "Copier \195\160 partir de"
|
||||
L["delete_desc"] = "Supprime les profils existants inutilis\195\169s de la base de donn\195\169es afin de gagner de la place et de nettoyer le fichier SavedVariables."
|
||||
L["delete"] = "Supprimer un profil"
|
||||
L["delete_sub"] = "Supprime un profil de la base de donn\195\169es."
|
||||
L["delete_confirm"] = "Etes-vous s\195\187r de vouloir supprimer le profil s\195\169lectionn\195\169 ?"
|
||||
L["profiles"] = "Profils"
|
||||
L["profiles_sub"] = "Gestion des profils"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "koKR" then
|
||||
L["default"] = "기본값"
|
||||
L["intro"] = "모든 캐릭터의 다양한 설정과 사용중인 데이터베이스 프로필, 어느것이던지 매우 다루기 쉽게 바꿀수 있습니다."
|
||||
L["reset_desc"] = "단순히 다시 새롭게 구성을 원하는 경우, 현재 프로필을 기본값으로 초기화 합니다."
|
||||
L["reset"] = "프로필 초기화"
|
||||
L["reset_sub"] = "현재의 프로필을 기본값으로 초기화 합니다"
|
||||
L["choose_desc"] = "새로운 이름을 입력하거나, 이미 있는 프로필중 하나를 선택하여 새로운 프로필을 만들 수 있습니다."
|
||||
L["new"] = "새로운 프로필"
|
||||
L["new_sub"] = "새로운 프로필을 만듭니다."
|
||||
L["choose"] = "프로필 선택"
|
||||
L["choose_sub"] = "당신이 현재 이용할수 있는 프로필을 선택합니다."
|
||||
L["copy_desc"] = "현재 사용중인 프로필에, 선택한 프로필의 설정을 복사합니다."
|
||||
L["copy"] = "복사"
|
||||
L["delete_desc"] = "데이터베이스에 사용중이거나 저장된 프로파일 삭제로 SavedVariables 파일의 정리와 공간 절약이 됩니다."
|
||||
L["delete"] = "프로필 삭제"
|
||||
L["delete_sub"] = "데이터베이스의 프로필을 삭제합니다."
|
||||
L["delete_confirm"] = "정말로 선택한 프로필의 삭제를 원하십니까?"
|
||||
L["profiles"] = "프로필"
|
||||
L["profiles_sub"] = "프로필 설정"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "esES" or LOCALE == "esMX" then
|
||||
L["default"] = "Por defecto"
|
||||
L["intro"] = "Puedes cambiar el perfil activo de tal manera que cada personaje tenga diferentes configuraciones."
|
||||
L["reset_desc"] = "Reinicia el perfil actual a los valores por defectos, en caso de que se haya estropeado la configuración o quieras volver a empezar de nuevo."
|
||||
L["reset"] = "Reiniciar Perfil"
|
||||
L["reset_sub"] = "Reinicar el perfil actual al de por defecto"
|
||||
L["choose_desc"] = "Puedes crear un nuevo perfil introduciendo un nombre en el recuadro o puedes seleccionar un perfil de los ya existentes."
|
||||
L["new"] = "Nuevo"
|
||||
L["new_sub"] = "Crear un nuevo perfil vacio."
|
||||
L["choose"] = "Perfiles existentes"
|
||||
L["choose_sub"] = "Selecciona uno de los perfiles disponibles."
|
||||
L["copy_desc"] = "Copia los ajustes de un perfil existente al perfil actual."
|
||||
L["copy"] = "Copiar de"
|
||||
L["delete_desc"] = "Borra los perfiles existentes y sin uso de la base de datos para ganar espacio y limpiar el archivo SavedVariables."
|
||||
L["delete"] = "Borrar un Perfil"
|
||||
L["delete_sub"] = "Borra un perfil de la base de datos."
|
||||
L["delete_confirm"] = "¿Estas seguro que quieres borrar el perfil seleccionado?"
|
||||
L["profiles"] = "Perfiles"
|
||||
L["profiles_sub"] = "Manejar Perfiles"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "zhTW" then
|
||||
L["default"] = "預設"
|
||||
L["intro"] = "你可以選擇一個活動的資料設定檔,這樣你的每個角色就可以擁有不同的設定值,可以給你的插件設定帶來極大的靈活性。"
|
||||
L["reset_desc"] = "將當前的設定檔恢復到它的預設值,用於你的設定檔損壞,或者你只是想重來的情況。"
|
||||
L["reset"] = "重置設定檔"
|
||||
L["reset_sub"] = "將當前的設定檔恢復為預設值"
|
||||
L["choose_desc"] = "你可以通過在文本框內輸入一個名字創立一個新的設定檔,也可以選擇一個已經存在的設定檔。"
|
||||
L["new"] = "新建"
|
||||
L["new_sub"] = "新建一個空的設定檔。"
|
||||
L["choose"] = "現有的設定檔"
|
||||
L["choose_sub"] = "從當前可用的設定檔裏面選擇一個。"
|
||||
L["copy_desc"] = "從當前某個已保存的設定檔複製到當前正使用的設定檔。"
|
||||
L["copy"] = "複製自"
|
||||
L["delete_desc"] = "從資料庫裏刪除不再使用的設定檔,以節省空間,並且清理SavedVariables檔。"
|
||||
L["delete"] = "刪除一個設定檔"
|
||||
L["delete_sub"] = "從資料庫裏刪除一個設定檔。"
|
||||
L["delete_confirm"] = "你確定要刪除所選擇的設定檔嗎?"
|
||||
L["profiles"] = "設定檔"
|
||||
L["profiles_sub"] = "管理設定檔"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "zhCN" then
|
||||
L["default"] = "默认"
|
||||
L["intro"] = "你可以选择一个活动的数据配置文件,这样你的每个角色就可以拥有不同的设置值,可以给你的插件配置带来极大的灵活性。"
|
||||
L["reset_desc"] = "将当前的配置文件恢复到它的默认值,用于你的配置文件损坏,或者你只是想重来的情况。"
|
||||
L["reset"] = "重置配置文件"
|
||||
L["reset_sub"] = "将当前的配置文件恢复为默认值"
|
||||
L["choose_desc"] = "你可以通过在文本框内输入一个名字创立一个新的配置文件,也可以选择一个已经存在的配置文件。"
|
||||
L["new"] = "新建"
|
||||
L["new_sub"] = "新建一个空的配置文件。"
|
||||
L["choose"] = "现有的配置文件"
|
||||
L["choose_sub"] = "从当前可用的配置文件里面选择一个。"
|
||||
L["copy_desc"] = "从当前某个已保存的配置文件复制到当前正使用的配置文件。"
|
||||
L["copy"] = "复制自"
|
||||
L["delete_desc"] = "从数据库里删除不再使用的配置文件,以节省空间,并且清理SavedVariables文件。"
|
||||
L["delete"] = "删除一个配置文件"
|
||||
L["delete_sub"] = "从数据库里删除一个配置文件。"
|
||||
L["delete_confirm"] = "你确定要删除所选择的配置文件么?"
|
||||
L["profiles"] = "配置文件"
|
||||
L["profiles_sub"] = "管理配置文件"
|
||||
--L["current"] = "Current Profile:"
|
||||
elseif LOCALE == "ruRU" then
|
||||
L["default"] = "По умолчанию"
|
||||
L["intro"] = "Изменяя активный профиль, вы можете задать различные настройки модификаций для каждого персонажа."
|
||||
L["reset_desc"] = "Если ваша конфигурации испорчена или если вы хотите настроить всё заново - сбросьте текущий профиль на стандартные значения."
|
||||
L["reset"] = "Сброс профиля"
|
||||
L["reset_sub"] = "Сброс текущего профиля на стандартный"
|
||||
L["choose_desc"] = "Вы можете создать новый профиль, введя название в поле ввода, или выбрать один из уже существующих профилей."
|
||||
L["new"] = "Новый"
|
||||
L["new_sub"] = "Создать новый чистый профиль"
|
||||
L["choose"] = "Существующие профили"
|
||||
L["choose_sub"] = "Выбор одиного из уже доступных профилей"
|
||||
L["copy_desc"] = "Скопировать настройки из выбранного профиля в активный."
|
||||
L["copy"] = "Скопировать из"
|
||||
L["delete_desc"] = "Удалить существующий и неиспользуемый профиль из БД для сохранения места, и очистить SavedVariables файл."
|
||||
L["delete"] = "Удалить профиль"
|
||||
L["delete_sub"] = "Удаление профиля из БД"
|
||||
L["delete_confirm"] = "Вы уверены, что вы хотите удалить выбранный профиль?"
|
||||
L["profiles"] = "Профили"
|
||||
L["profiles_sub"] = "Управление профилями"
|
||||
--L["current"] = "Current Profile:"
|
||||
end
|
||||
|
||||
local defaultProfiles
|
||||
local tmpprofiles = {}
|
||||
|
||||
-- Get a list of available profiles for the specified database.
|
||||
-- You can specify which profiles to include/exclude in the list using the two boolean parameters listed below.
|
||||
-- @param db The db object to retrieve the profiles from
|
||||
-- @param common If true, getProfileList will add the default profiles to the return list, even if they have not been created yet
|
||||
-- @param nocurrent If true, then getProfileList will not display the current profile in the list
|
||||
-- @return Hashtable of all profiles with the internal name as keys and the display name as value.
|
||||
local function getProfileList(db, common, nocurrent)
|
||||
local profiles = {}
|
||||
|
||||
-- copy existing profiles into the table
|
||||
local currentProfile = db:GetCurrentProfile()
|
||||
for i,v in pairs(db:GetProfiles(tmpprofiles)) do
|
||||
if not (nocurrent and v == currentProfile) then
|
||||
profiles[v] = v
|
||||
end
|
||||
end
|
||||
|
||||
-- add our default profiles to choose from ( or rename existing profiles)
|
||||
for k,v in pairs(defaultProfiles) do
|
||||
if (common or profiles[k]) and not (nocurrent and k == currentProfile) then
|
||||
profiles[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
return profiles
|
||||
end
|
||||
|
||||
--[[
|
||||
OptionsHandlerPrototype
|
||||
prototype class for handling the options in a sane way
|
||||
]]
|
||||
local OptionsHandlerPrototype = {}
|
||||
|
||||
--[[ Reset the profile ]]
|
||||
function OptionsHandlerPrototype:Reset()
|
||||
self.db:ResetProfile()
|
||||
end
|
||||
|
||||
--[[ Set the profile to value ]]
|
||||
function OptionsHandlerPrototype:SetProfile(info, value)
|
||||
self.db:SetProfile(value)
|
||||
end
|
||||
|
||||
--[[ returns the currently active profile ]]
|
||||
function OptionsHandlerPrototype:GetCurrentProfile()
|
||||
return self.db:GetCurrentProfile()
|
||||
end
|
||||
|
||||
--[[
|
||||
List all active profiles
|
||||
you can control the output with the .arg variable
|
||||
currently four modes are supported
|
||||
|
||||
(empty) - return all available profiles
|
||||
"nocurrent" - returns all available profiles except the currently active profile
|
||||
"common" - returns all avaialble profiles + some commonly used profiles ("char - realm", "realm", "class", "Default")
|
||||
"both" - common except the active profile
|
||||
]]
|
||||
function OptionsHandlerPrototype:ListProfiles(info)
|
||||
local arg = info.arg
|
||||
local profiles
|
||||
if arg == "common" and not self.noDefaultProfiles then
|
||||
profiles = getProfileList(self.db, true, nil)
|
||||
elseif arg == "nocurrent" then
|
||||
profiles = getProfileList(self.db, nil, true)
|
||||
elseif arg == "both" then -- currently not used
|
||||
profiles = getProfileList(self.db, (not self.noDefaultProfiles) and true, true)
|
||||
else
|
||||
profiles = getProfileList(self.db)
|
||||
end
|
||||
|
||||
return profiles
|
||||
end
|
||||
|
||||
function OptionsHandlerPrototype:HasNoProfiles(info)
|
||||
local profiles = self:ListProfiles(info)
|
||||
return ((not next(profiles)) and true or false)
|
||||
end
|
||||
|
||||
--[[ Copy a profile ]]
|
||||
function OptionsHandlerPrototype:CopyProfile(info, value)
|
||||
self.db:CopyProfile(value)
|
||||
end
|
||||
|
||||
--[[ Delete a profile from the db ]]
|
||||
function OptionsHandlerPrototype:DeleteProfile(info, value)
|
||||
self.db:DeleteProfile(value)
|
||||
end
|
||||
|
||||
--[[ fill defaultProfiles with some generic values ]]
|
||||
local function generateDefaultProfiles(db)
|
||||
defaultProfiles = {
|
||||
["Default"] = L["default"],
|
||||
[db.keys.char] = db.keys.char,
|
||||
[db.keys.realm] = db.keys.realm,
|
||||
[db.keys.class] = UnitClass("player")
|
||||
}
|
||||
end
|
||||
|
||||
--[[ create and return a handler object for the db, or upgrade it if it already existed ]]
|
||||
local function getOptionsHandler(db, noDefaultProfiles)
|
||||
if not defaultProfiles then
|
||||
generateDefaultProfiles(db)
|
||||
end
|
||||
|
||||
local handler = AceDBOptions.handlers[db] or { db = db, noDefaultProfiles = noDefaultProfiles }
|
||||
|
||||
for k,v in pairs(OptionsHandlerPrototype) do
|
||||
handler[k] = v
|
||||
end
|
||||
|
||||
AceDBOptions.handlers[db] = handler
|
||||
return handler
|
||||
end
|
||||
|
||||
--[[
|
||||
the real options table
|
||||
]]
|
||||
local optionsTable = {
|
||||
desc = {
|
||||
order = 1,
|
||||
type = "description",
|
||||
name = L["intro"] .. "\n",
|
||||
},
|
||||
descreset = {
|
||||
order = 9,
|
||||
type = "description",
|
||||
name = L["reset_desc"],
|
||||
},
|
||||
reset = {
|
||||
order = 10,
|
||||
type = "execute",
|
||||
name = L["reset"],
|
||||
desc = L["reset_sub"],
|
||||
func = "Reset",
|
||||
},
|
||||
current = {
|
||||
order = 11,
|
||||
type = "description",
|
||||
name = function(info) return L["current"] .. " " .. NORMAL_FONT_COLOR_CODE .. info.handler:GetCurrentProfile() .. FONT_COLOR_CODE_CLOSE end,
|
||||
width = "default",
|
||||
},
|
||||
choosedesc = {
|
||||
order = 20,
|
||||
type = "description",
|
||||
name = "\n" .. L["choose_desc"],
|
||||
},
|
||||
new = {
|
||||
name = L["new"],
|
||||
desc = L["new_sub"],
|
||||
type = "input",
|
||||
order = 30,
|
||||
get = false,
|
||||
set = "SetProfile",
|
||||
},
|
||||
choose = {
|
||||
name = L["choose"],
|
||||
desc = L["choose_sub"],
|
||||
type = "select",
|
||||
order = 40,
|
||||
get = "GetCurrentProfile",
|
||||
set = "SetProfile",
|
||||
values = "ListProfiles",
|
||||
arg = "common",
|
||||
},
|
||||
copydesc = {
|
||||
order = 50,
|
||||
type = "description",
|
||||
name = "\n" .. L["copy_desc"],
|
||||
},
|
||||
copyfrom = {
|
||||
order = 60,
|
||||
type = "select",
|
||||
name = L["copy"],
|
||||
desc = L["copy_desc"],
|
||||
get = false,
|
||||
set = "CopyProfile",
|
||||
values = "ListProfiles",
|
||||
disabled = "HasNoProfiles",
|
||||
arg = "nocurrent",
|
||||
},
|
||||
deldesc = {
|
||||
order = 70,
|
||||
type = "description",
|
||||
name = "\n" .. L["delete_desc"],
|
||||
},
|
||||
delete = {
|
||||
order = 80,
|
||||
type = "select",
|
||||
name = L["delete"],
|
||||
desc = L["delete_sub"],
|
||||
get = false,
|
||||
set = "DeleteProfile",
|
||||
values = "ListProfiles",
|
||||
disabled = "HasNoProfiles",
|
||||
arg = "nocurrent",
|
||||
confirm = true,
|
||||
confirmText = L["delete_confirm"],
|
||||
},
|
||||
}
|
||||
|
||||
--- Get/Create a option table that you can use in your addon to control the profiles of AceDB-3.0.
|
||||
-- @param db The database object to create the options table for.
|
||||
-- @return The options table to be used in AceConfig-3.0
|
||||
-- @usage
|
||||
-- -- Assuming `options` is your top-level options table and `self.db` is your database:
|
||||
-- options.args.profiles = LibStub("AceDBOptions-3.0"):GetOptionsTable(self.db)
|
||||
function AceDBOptions:GetOptionsTable(db, noDefaultProfiles)
|
||||
local tbl = AceDBOptions.optionTables[db] or {
|
||||
type = "group",
|
||||
name = L["profiles"],
|
||||
desc = L["profiles_sub"],
|
||||
}
|
||||
|
||||
tbl.handler = getOptionsHandler(db, noDefaultProfiles)
|
||||
tbl.args = optionsTable
|
||||
|
||||
AceDBOptions.optionTables[db] = tbl
|
||||
return tbl
|
||||
end
|
||||
|
||||
-- upgrade existing tables
|
||||
for db,tbl in pairs(AceDBOptions.optionTables) do
|
||||
tbl.handler = getOptionsHandler(db)
|
||||
tbl.args = optionsTable
|
||||
end
|
||||
@@ -0,0 +1,4 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceDBOptions-3.0.lua"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,876 @@
|
||||
--- **AceGUI-3.0** provides access to numerous widgets which can be used to create GUIs.
|
||||
-- AceGUI is used by AceConfigDialog to create the option GUIs, but you can use it by itself
|
||||
-- to create any custom GUI. There are more extensive examples in the test suite in the Ace3
|
||||
-- stand-alone distribution.
|
||||
--
|
||||
-- **Note**: When using AceGUI-3.0 directly, please do not modify the frames of the widgets directly,
|
||||
-- as any "unknown" change to the widgets will cause addons that get your widget out of the widget pool
|
||||
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we'll
|
||||
-- implement a proper API to modify it.
|
||||
-- @usage
|
||||
-- local AceGUI = LibStub("AceGUI-3.0")
|
||||
-- -- Create a container frame
|
||||
-- local f = AceGUI:Create("Frame")
|
||||
-- f:SetCallback("OnClose",function(widget) AceGUI:Release(widget) end)
|
||||
-- f:SetTitle("AceGUI-3.0 Example")
|
||||
-- f:SetStatusText("Status Bar")
|
||||
-- f:SetLayout("Flow")
|
||||
-- -- Create a button
|
||||
-- local btn = AceGUI:Create("Button")
|
||||
-- btn:SetWidth(170)
|
||||
-- btn:SetText("Button !")
|
||||
-- btn:SetCallback("OnClick", function() print("Click!") end)
|
||||
-- -- Add the button to the container
|
||||
-- f:AddChild(btn)
|
||||
-- @class file
|
||||
-- @name AceGUI-3.0
|
||||
-- @release $Id: AceGUI-3.0.lua 919 2010-05-09 11:36:03Z nevcairiel $
|
||||
local ACEGUI_MAJOR, ACEGUI_MINOR = "AceGUI-3.0", 31
|
||||
local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR)
|
||||
|
||||
if not AceGUI then return end -- No upgrade needed
|
||||
|
||||
-- Lua APIs
|
||||
local tconcat, tremove, tinsert = table.concat, table.remove, table.insert
|
||||
local select, pairs, next, type = select, pairs, next, type
|
||||
local error, assert, loadstring = error, assert, loadstring
|
||||
local setmetatable, rawget, rawset = setmetatable, rawget, rawset
|
||||
local math_max = math.max
|
||||
|
||||
-- WoW APIs
|
||||
local UIParent = UIParent
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: geterrorhandler, LibStub
|
||||
|
||||
--local con = LibStub("AceConsole-3.0",true)
|
||||
|
||||
AceGUI.WidgetRegistry = AceGUI.WidgetRegistry or {}
|
||||
AceGUI.LayoutRegistry = AceGUI.LayoutRegistry or {}
|
||||
AceGUI.WidgetBase = AceGUI.WidgetBase or {}
|
||||
AceGUI.WidgetContainerBase = AceGUI.WidgetContainerBase or {}
|
||||
AceGUI.WidgetVersions = AceGUI.WidgetVersions or {}
|
||||
|
||||
-- local upvalues
|
||||
local WidgetRegistry = AceGUI.WidgetRegistry
|
||||
local LayoutRegistry = AceGUI.LayoutRegistry
|
||||
local WidgetVersions = AceGUI.WidgetVersions
|
||||
|
||||
--[[
|
||||
xpcall safecall implementation
|
||||
]]
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local xpcall, eh = ...
|
||||
local method, ARGS
|
||||
local function call() return method(ARGS) end
|
||||
|
||||
local function dispatch(func, ...)
|
||||
method = func
|
||||
if not method then return end
|
||||
ARGS = ...
|
||||
return xpcall(call, eh)
|
||||
end
|
||||
|
||||
return dispatch
|
||||
]]
|
||||
|
||||
local ARGS = {}
|
||||
for i = 1, argCount do ARGS[i] = "arg"..i end
|
||||
code = code:gsub("ARGS", tconcat(ARGS, ", "))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(xpcall, errorhandler)
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
Dispatchers[0] = function(func)
|
||||
return xpcall(func, errorhandler)
|
||||
end
|
||||
|
||||
local function safecall(func, ...)
|
||||
return Dispatchers[select('#', ...)](func, ...)
|
||||
end
|
||||
|
||||
-- Recycling functions
|
||||
local newWidget, delWidget
|
||||
do
|
||||
-- Version Upgrade in Minor 29
|
||||
-- Internal Storage of the objects changed, from an array table
|
||||
-- to a hash table, and additionally we introduced versioning on
|
||||
-- the widgets which would discard all widgets from a pre-29 version
|
||||
-- anyway, so we just clear the storage now, and don't try to
|
||||
-- convert the storage tables to the new format.
|
||||
-- This should generally not cause *many* widgets to end up in trash,
|
||||
-- since once dialogs are opened, all addons should be loaded already
|
||||
-- and AceGUI should be on the latest version available on the users
|
||||
-- setup.
|
||||
-- -- nevcairiel - Nov 2nd, 2009
|
||||
if oldminor and oldminor < 29 and AceGUI.objPools then
|
||||
AceGUI.objPools = nil
|
||||
end
|
||||
|
||||
AceGUI.objPools = AceGUI.objPools or {}
|
||||
local objPools = AceGUI.objPools
|
||||
--Returns a new instance, if none are available either returns a new table or calls the given contructor
|
||||
function newWidget(type)
|
||||
if not WidgetRegistry[type] then
|
||||
error("Attempt to instantiate unknown widget type", 2)
|
||||
end
|
||||
|
||||
if not objPools[type] then
|
||||
objPools[type] = {}
|
||||
end
|
||||
|
||||
local newObj = next(objPools[type])
|
||||
if not newObj then
|
||||
newObj = WidgetRegistry[type]()
|
||||
newObj.AceGUIWidgetVersion = WidgetVersions[type]
|
||||
else
|
||||
objPools[type][newObj] = nil
|
||||
-- if the widget is older then the latest, don't even try to reuse it
|
||||
-- just forget about it, and grab a new one.
|
||||
if not newObj.AceGUIWidgetVersion or newObj.AceGUIWidgetVersion < WidgetVersions[type] then
|
||||
return newWidget(type)
|
||||
end
|
||||
end
|
||||
return newObj
|
||||
end
|
||||
-- Releases an instance to the Pool
|
||||
function delWidget(obj,type)
|
||||
if not objPools[type] then
|
||||
objPools[type] = {}
|
||||
end
|
||||
if objPools[type][obj] then
|
||||
error("Attempt to Release Widget that is already released", 2)
|
||||
end
|
||||
objPools[type][obj] = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-------------------
|
||||
-- API Functions --
|
||||
-------------------
|
||||
|
||||
-- Gets a widget Object
|
||||
|
||||
--- Create a new Widget of the given type.
|
||||
-- This function will instantiate a new widget (or use one from the widget pool), and call the
|
||||
-- OnAcquire function on it, before returning.
|
||||
-- @param type The type of the widget.
|
||||
-- @return The newly created widget.
|
||||
function AceGUI:Create(type)
|
||||
if WidgetRegistry[type] then
|
||||
local widget = newWidget(type)
|
||||
|
||||
if rawget(widget,'Acquire') then
|
||||
widget.OnAcquire = widget.Acquire
|
||||
widget.Acquire = nil
|
||||
elseif rawget(widget,'Aquire') then
|
||||
widget.OnAcquire = widget.Aquire
|
||||
widget.Aquire = nil
|
||||
end
|
||||
|
||||
if rawget(widget,'Release') then
|
||||
widget.OnRelease = rawget(widget,'Release')
|
||||
widget.Release = nil
|
||||
end
|
||||
|
||||
if widget.OnAcquire then
|
||||
widget:OnAcquire()
|
||||
else
|
||||
error(("Widget type %s doesn't supply an OnAcquire Function"):format(type))
|
||||
end
|
||||
-- Set the default Layout ('List')
|
||||
safecall(widget.SetLayout, widget, 'List')
|
||||
safecall(widget.ResumeLayout, widget)
|
||||
return widget
|
||||
end
|
||||
end
|
||||
|
||||
--- Releases a widget Object.
|
||||
-- This function calls OnRelease on the widget and places it back in the widget pool.
|
||||
-- Any data on the widget is being erased, and the widget will be hidden.\\
|
||||
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
|
||||
-- @param widget The widget to release
|
||||
function AceGUI:Release(widget)
|
||||
safecall( widget.PauseLayout, widget )
|
||||
widget:Fire("OnRelease")
|
||||
safecall( widget.ReleaseChildren, widget )
|
||||
|
||||
if widget.OnRelease then
|
||||
widget:OnRelease()
|
||||
else
|
||||
error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
|
||||
end
|
||||
for k in pairs(widget.userdata) do
|
||||
widget.userdata[k] = nil
|
||||
end
|
||||
for k in pairs(widget.events) do
|
||||
widget.events[k] = nil
|
||||
end
|
||||
widget.width = nil
|
||||
widget.relWidth = nil
|
||||
widget.height = nil
|
||||
widget.relHeight = nil
|
||||
widget.noAutoHeight = nil
|
||||
widget.frame:ClearAllPoints()
|
||||
widget.frame:Hide()
|
||||
widget.frame:SetParent(UIParent)
|
||||
widget.frame.width = nil
|
||||
widget.frame.height = nil
|
||||
if widget.content then
|
||||
widget.content.width = nil
|
||||
widget.content.height = nil
|
||||
end
|
||||
delWidget(widget, widget.type)
|
||||
end
|
||||
|
||||
-----------
|
||||
-- Focus --
|
||||
-----------
|
||||
|
||||
|
||||
--- Called when a widget has taken focus.
|
||||
-- e.g. Dropdowns opening, Editboxes gaining kb focus
|
||||
-- @param widget The widget that should be focused
|
||||
function AceGUI:SetFocus(widget)
|
||||
if self.FocusedWidget and self.FocusedWidget ~= widget then
|
||||
safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
|
||||
end
|
||||
self.FocusedWidget = widget
|
||||
end
|
||||
|
||||
|
||||
--- Called when something has happened that could cause widgets with focus to drop it
|
||||
-- e.g. titlebar of a frame being clicked
|
||||
function AceGUI:ClearFocus()
|
||||
if self.FocusedWidget then
|
||||
safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
|
||||
self.FocusedWidget = nil
|
||||
end
|
||||
end
|
||||
|
||||
-------------
|
||||
-- Widgets --
|
||||
-------------
|
||||
--[[
|
||||
Widgets must provide the following functions
|
||||
OnAcquire() - Called when the object is acquired, should set everything to a default hidden state
|
||||
OnRelease() - Called when the object is Released, should remove any anchors and hide the Widget
|
||||
|
||||
And the following members
|
||||
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
|
||||
type - the type of the object, same as the name given to :RegisterWidget()
|
||||
|
||||
Widgets contain a table called userdata, this is a safe place to store data associated with the wigdet
|
||||
It will be cleared automatically when a widget is released
|
||||
Placing values directly into a widget object should be avoided
|
||||
|
||||
If the Widget can act as a container for other Widgets the following
|
||||
content - frame or derivitive that children will be anchored to
|
||||
|
||||
The Widget can supply the following Optional Members
|
||||
:OnWidthSet(width) - Called when the width of the widget is changed
|
||||
:OnHeightSet(height) - Called when the height of the widget is changed
|
||||
Widgets should not use the OnSizeChanged events of thier frame or content members, use these methods instead
|
||||
AceGUI already sets a handler to the event
|
||||
:LayoutFinished(width, height) - called after a layout has finished, the width and height will be the width and height of the
|
||||
area used for controls. These can be nil if the layout used the existing size to layout the controls.
|
||||
|
||||
]]
|
||||
|
||||
--------------------------
|
||||
-- Widget Base Template --
|
||||
--------------------------
|
||||
do
|
||||
local function fixlevels(parent,...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
while child do
|
||||
child:SetFrameLevel(parent:GetFrameLevel()+1)
|
||||
fixlevels(child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local WidgetBase = AceGUI.WidgetBase
|
||||
|
||||
WidgetBase.SetParent = function(self, parent)
|
||||
local frame = self.frame
|
||||
frame:SetParent(nil)
|
||||
frame:SetParent(parent.content)
|
||||
self.parent = parent
|
||||
--fixlevels(parent.frame,parent.frame:GetChildren())
|
||||
end
|
||||
|
||||
WidgetBase.SetCallback = function(self, name, func)
|
||||
if type(func) == "function" then
|
||||
self.events[name] = func
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.Fire = function(self, name, ...)
|
||||
if self.events[name] then
|
||||
local success, ret = safecall(self.events[name], self, name, ...)
|
||||
if success then
|
||||
return ret
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.SetWidth = function(self, width)
|
||||
self.frame:SetWidth(width)
|
||||
self.frame.width = width
|
||||
if self.OnWidthSet then
|
||||
self:OnWidthSet(width)
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.SetRelativeWidth = function(self, width)
|
||||
if width <= 0 or width > 1 then
|
||||
error(":SetRelativeWidth(width): Invalid relative width.", 2)
|
||||
end
|
||||
self.relWidth = width
|
||||
self.width = "relative"
|
||||
end
|
||||
|
||||
WidgetBase.SetHeight = function(self, height)
|
||||
self.frame:SetHeight(height)
|
||||
self.frame.height = height
|
||||
if self.OnHeightSet then
|
||||
self:OnHeightSet(height)
|
||||
end
|
||||
end
|
||||
|
||||
--[[ WidgetBase.SetRelativeHeight = function(self, height)
|
||||
if height <= 0 or height > 1 then
|
||||
error(":SetRelativeHeight(height): Invalid relative height.", 2)
|
||||
end
|
||||
self.relHeight = height
|
||||
self.height = "relative"
|
||||
end ]]
|
||||
|
||||
WidgetBase.IsVisible = function(self)
|
||||
return self.frame:IsVisible()
|
||||
end
|
||||
|
||||
WidgetBase.IsShown= function(self)
|
||||
return self.frame:IsShown()
|
||||
end
|
||||
|
||||
WidgetBase.Release = function(self)
|
||||
AceGUI:Release(self)
|
||||
end
|
||||
|
||||
WidgetBase.SetPoint = function(self, ...)
|
||||
return self.frame:SetPoint(...)
|
||||
end
|
||||
|
||||
WidgetBase.ClearAllPoints = function(self)
|
||||
return self.frame:ClearAllPoints()
|
||||
end
|
||||
|
||||
WidgetBase.GetNumPoints = function(self)
|
||||
return self.frame:GetNumPoints()
|
||||
end
|
||||
|
||||
WidgetBase.GetPoint = function(self, ...)
|
||||
return self.frame:GetPoint(...)
|
||||
end
|
||||
|
||||
WidgetBase.GetUserDataTable = function(self)
|
||||
return self.userdata
|
||||
end
|
||||
|
||||
WidgetBase.SetUserData = function(self, key, value)
|
||||
self.userdata[key] = value
|
||||
end
|
||||
|
||||
WidgetBase.GetUserData = function(self, key)
|
||||
return self.userdata[key]
|
||||
end
|
||||
|
||||
WidgetBase.IsFullHeight = function(self)
|
||||
return self.height == "fill"
|
||||
end
|
||||
|
||||
WidgetBase.SetFullHeight = function(self, isFull)
|
||||
if isFull then
|
||||
self.height = "fill"
|
||||
else
|
||||
self.height = nil
|
||||
end
|
||||
end
|
||||
|
||||
WidgetBase.IsFullWidth = function(self)
|
||||
return self.width == "fill"
|
||||
end
|
||||
|
||||
WidgetBase.SetFullWidth = function(self, isFull)
|
||||
if isFull then
|
||||
self.width = "fill"
|
||||
else
|
||||
self.width = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- local function LayoutOnUpdate(this)
|
||||
-- this:SetScript("OnUpdate",nil)
|
||||
-- this.obj:PerformLayout()
|
||||
-- end
|
||||
|
||||
local WidgetContainerBase = AceGUI.WidgetContainerBase
|
||||
|
||||
WidgetContainerBase.PauseLayout = function(self)
|
||||
self.LayoutPaused = true
|
||||
end
|
||||
|
||||
WidgetContainerBase.ResumeLayout = function(self)
|
||||
self.LayoutPaused = nil
|
||||
end
|
||||
|
||||
WidgetContainerBase.PerformLayout = function(self)
|
||||
if self.LayoutPaused then
|
||||
return
|
||||
end
|
||||
safecall(self.LayoutFunc,self.content, self.children)
|
||||
end
|
||||
|
||||
--call this function to layout, makes sure layed out objects get a frame to get sizes etc
|
||||
WidgetContainerBase.DoLayout = function(self)
|
||||
self:PerformLayout()
|
||||
-- if not self.parent then
|
||||
-- self.frame:SetScript("OnUpdate", LayoutOnUpdate)
|
||||
-- end
|
||||
end
|
||||
|
||||
WidgetContainerBase.AddChild = function(self, child, beforeWidget)
|
||||
if beforeWidget then
|
||||
local siblingIndex = 1
|
||||
for _, widget in pairs(self.children) do
|
||||
if widget == beforeWidget then
|
||||
break
|
||||
end
|
||||
siblingIndex = siblingIndex + 1
|
||||
end
|
||||
tinsert(self.children, siblingIndex, child)
|
||||
else
|
||||
tinsert(self.children, child)
|
||||
end
|
||||
child:SetParent(self)
|
||||
child.frame:Show()
|
||||
self:DoLayout()
|
||||
end
|
||||
|
||||
WidgetContainerBase.AddChildren = function(self, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
local child = select(i, ...)
|
||||
tinsert(self.children, child)
|
||||
child:SetParent(self)
|
||||
child.frame:Show()
|
||||
end
|
||||
self:DoLayout()
|
||||
end
|
||||
|
||||
WidgetContainerBase.ReleaseChildren = function(self)
|
||||
local children = self.children
|
||||
for i = 1,#children do
|
||||
AceGUI:Release(children[i])
|
||||
children[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetLayout = function(self, Layout)
|
||||
self.LayoutFunc = AceGUI:GetLayout(Layout)
|
||||
end
|
||||
|
||||
WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
|
||||
if adjust then
|
||||
self.noAutoHeight = nil
|
||||
else
|
||||
self.noAutoHeight = true
|
||||
end
|
||||
end
|
||||
|
||||
local function FrameResize(this)
|
||||
local self = this.obj
|
||||
if this:GetWidth() and this:GetHeight() then
|
||||
if self.OnWidthSet then
|
||||
self:OnWidthSet(this:GetWidth())
|
||||
end
|
||||
if self.OnHeightSet then
|
||||
self:OnHeightSet(this:GetHeight())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ContentResize(this)
|
||||
if this:GetWidth() and this:GetHeight() then
|
||||
this.width = this:GetWidth()
|
||||
this.height = this:GetHeight()
|
||||
this.obj:DoLayout()
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(WidgetContainerBase,{__index=WidgetBase})
|
||||
|
||||
--One of these function should be called on each Widget Instance as part of its creation process
|
||||
|
||||
--- Register a widget-class as a container for newly created widgets.
|
||||
-- @param widget The widget class
|
||||
function AceGUI:RegisterAsContainer(widget)
|
||||
widget.children = {}
|
||||
widget.userdata = {}
|
||||
widget.events = {}
|
||||
widget.base = WidgetContainerBase
|
||||
widget.content.obj = widget
|
||||
widget.frame.obj = widget
|
||||
widget.content:SetScript("OnSizeChanged",ContentResize)
|
||||
widget.frame:SetScript("OnSizeChanged",FrameResize)
|
||||
setmetatable(widget,{__index=WidgetContainerBase})
|
||||
widget:SetLayout("List")
|
||||
end
|
||||
|
||||
--- Register a widget-class as a widget.
|
||||
-- @param widget The widget class
|
||||
function AceGUI:RegisterAsWidget(widget)
|
||||
widget.userdata = {}
|
||||
widget.events = {}
|
||||
widget.base = WidgetBase
|
||||
widget.frame.obj = widget
|
||||
widget.frame:SetScript("OnSizeChanged",FrameResize)
|
||||
setmetatable(widget,{__index=WidgetBase})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
------------------
|
||||
-- Widget API --
|
||||
------------------
|
||||
|
||||
--- Registers a widget Constructor, this function returns a new instance of the Widget
|
||||
-- @param Name The name of the widget
|
||||
-- @param Constructor The widget constructor function
|
||||
-- @param Version The version of the widget
|
||||
function AceGUI:RegisterWidgetType(Name, Constructor, Version)
|
||||
assert(type(Constructor) == "function")
|
||||
assert(type(Version) == "number")
|
||||
|
||||
local oldVersion = WidgetVersions[Name]
|
||||
if oldVersion and oldVersion >= Version then return end
|
||||
|
||||
WidgetVersions[Name] = Version
|
||||
WidgetRegistry[Name] = Constructor
|
||||
end
|
||||
|
||||
--- Registers a Layout Function
|
||||
-- @param Name The name of the layout
|
||||
-- @param LayoutFunc Reference to the layout function
|
||||
function AceGUI:RegisterLayout(Name, LayoutFunc)
|
||||
assert(type(LayoutFunc) == "function")
|
||||
if type(Name) == "string" then
|
||||
Name = Name:upper()
|
||||
end
|
||||
LayoutRegistry[Name] = LayoutFunc
|
||||
end
|
||||
|
||||
--- Get a Layout Function from the registry
|
||||
-- @param Name The name of the layout
|
||||
function AceGUI:GetLayout(Name)
|
||||
if type(Name) == "string" then
|
||||
Name = Name:upper()
|
||||
end
|
||||
return LayoutRegistry[Name]
|
||||
end
|
||||
|
||||
AceGUI.counts = AceGUI.counts or {}
|
||||
|
||||
--- A type-based counter to count the number of widgets created.
|
||||
-- This is used by widgets that require a named frame, e.g. when a Blizzard
|
||||
-- Template requires it.
|
||||
-- @param type The widget type
|
||||
function AceGUI:GetNextWidgetNum(type)
|
||||
if not self.counts[type] then
|
||||
self.counts[type] = 0
|
||||
end
|
||||
self.counts[type] = self.counts[type] + 1
|
||||
return self.counts[type]
|
||||
end
|
||||
|
||||
--- Return the number of created widgets for this type.
|
||||
-- In contrast to GetNextWidgetNum, the number is not incremented.
|
||||
-- @param type The widget type
|
||||
function AceGUI:GetWidgetCount(type)
|
||||
return self.counts[type] or 0
|
||||
end
|
||||
|
||||
--- Return the version of the currently registered widget type.
|
||||
-- @param type The widget type
|
||||
function AceGUI:GetWidgetVersion(type)
|
||||
return WidgetVersions[type]
|
||||
end
|
||||
|
||||
--[[ Widget Template
|
||||
|
||||
--------------------------
|
||||
-- Widget Name --
|
||||
--------------------------
|
||||
do
|
||||
local Type = "Type"
|
||||
|
||||
local function OnAcquire(self)
|
||||
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Frame",nil,UIParent)
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
--Container Support
|
||||
--local content = CreateFrame("Frame",nil,frame)
|
||||
--self.content = content
|
||||
|
||||
--AceGUI:RegisterAsContainer(self)
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor)
|
||||
end
|
||||
|
||||
|
||||
]]
|
||||
|
||||
-------------
|
||||
-- Layouts --
|
||||
-------------
|
||||
|
||||
--[[
|
||||
A Layout is a func that takes 2 parameters
|
||||
content - the frame that widgets will be placed inside
|
||||
children - a table containing the widgets to layout
|
||||
|
||||
]]
|
||||
|
||||
-- Very simple Layout, Children are stacked on top of each other down the left side
|
||||
AceGUI:RegisterLayout("List",
|
||||
function(content, children)
|
||||
|
||||
local height = 0
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
|
||||
local frame = child.frame
|
||||
frame:ClearAllPoints()
|
||||
frame:Show()
|
||||
if i == 1 then
|
||||
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT",children[i-1].frame,"BOTTOMLEFT",0,0)
|
||||
end
|
||||
|
||||
if child.width == "fill" then
|
||||
child:SetWidth(width)
|
||||
frame:SetPoint("RIGHT",content,"RIGHT")
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(content.width or content:GetWidth())
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
elseif child.width == "relative" then
|
||||
child:SetWidth(width * child.relWidth)
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(content.width or content:GetWidth())
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
end
|
||||
|
||||
height = height + (frame.height or frame:GetHeight() or 0)
|
||||
end
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, height )
|
||||
end
|
||||
)
|
||||
|
||||
-- A single control fills the whole content area
|
||||
AceGUI:RegisterLayout("Fill",
|
||||
function(content, children)
|
||||
if children[1] then
|
||||
children[1]:SetWidth(content:GetWidth() or 0)
|
||||
children[1]:SetHeight(content:GetHeight() or 0)
|
||||
children[1].frame:SetAllPoints(content)
|
||||
children[1].frame:Show()
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight() )
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
AceGUI:RegisterLayout("Flow",
|
||||
function(content, children)
|
||||
--used height so far
|
||||
local height = 0
|
||||
--width used in the current row
|
||||
local usedwidth = 0
|
||||
--height of the current row
|
||||
local rowheight = 0
|
||||
local rowoffset = 0
|
||||
local lastrowoffset
|
||||
|
||||
local width = content.width or content:GetWidth() or 0
|
||||
|
||||
--control at the start of the row
|
||||
local rowstart
|
||||
local rowstartoffset
|
||||
local lastrowstart
|
||||
local isfullheight
|
||||
|
||||
local frameoffset
|
||||
local lastframeoffset
|
||||
local oversize
|
||||
for i = 1, #children do
|
||||
local child = children[i]
|
||||
oversize = nil
|
||||
local frame = child.frame
|
||||
local frameheight = frame.height or frame:GetHeight() or 0
|
||||
local framewidth = frame.width or frame:GetWidth() or 0
|
||||
lastframeoffset = frameoffset
|
||||
-- HACK: Why did we set a frameoffset of (frameheight / 2) ?
|
||||
-- That was moving all widgets half the widgets size down, is that intended?
|
||||
-- Actually, it seems to be neccessary for many cases, we'll leave it in for now.
|
||||
-- If widgets seem to anchor weirdly with this, provide a valid alignoffset for them.
|
||||
-- TODO: Investigate moar!
|
||||
frameoffset = child.alignoffset or (frameheight / 2)
|
||||
|
||||
if child.width == "relative" then
|
||||
framewidth = width * child.relWidth
|
||||
end
|
||||
|
||||
frame:Show()
|
||||
frame:ClearAllPoints()
|
||||
if i == 1 then
|
||||
-- anchor the first control to the top left
|
||||
frame:SetPoint("TOPLEFT",content,"TOPLEFT",0,0)
|
||||
rowheight = frameheight
|
||||
rowoffset = frameoffset
|
||||
rowstart = frame
|
||||
rowstartoffset = frameoffset
|
||||
usedwidth = framewidth
|
||||
if usedwidth > width then
|
||||
oversize = true
|
||||
end
|
||||
else
|
||||
-- if there isn't available width for the control start a new row
|
||||
-- if a control is "fill" it will be on a row of its own full width
|
||||
if usedwidth == 0 or ((framewidth) + usedwidth > width) or child.width == "fill" then
|
||||
if isfullheight then
|
||||
-- a previous row has already filled the entire height, there's nothing we can usefully do anymore
|
||||
-- (maybe error/warn about this?)
|
||||
break
|
||||
end
|
||||
--anchor the previous row, we will now know its height and offset
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
|
||||
height = height + rowheight + 3
|
||||
--save this as the rowstart so we can anchor it after the row is complete and we have the max height and offset of controls in it
|
||||
rowstart = frame
|
||||
rowstartoffset = frameoffset
|
||||
rowheight = frameheight
|
||||
rowoffset = frameoffset
|
||||
usedwidth = framewidth
|
||||
if usedwidth > width then
|
||||
oversize = true
|
||||
end
|
||||
-- put the control on the current row, adding it to the width and checking if the height needs to be increased
|
||||
else
|
||||
--handles cases where the new height is higher than either control because of the offsets
|
||||
--math.max(rowheight-rowoffset+frameoffset, frameheight-frameoffset+rowoffset)
|
||||
|
||||
--offset is always the larger of the two offsets
|
||||
rowoffset = math_max(rowoffset, frameoffset)
|
||||
|
||||
rowheight = math_max(rowheight,rowoffset+(frameheight/2))
|
||||
--print("type:", child.type, "offset:",frameoffset-lastframeoffset)
|
||||
frame:SetPoint("TOPLEFT",children[i-1].frame,"TOPRIGHT",0,frameoffset-lastframeoffset)
|
||||
usedwidth = framewidth + usedwidth
|
||||
end
|
||||
end
|
||||
|
||||
if child.width == "fill" then
|
||||
child:SetWidth(width)
|
||||
frame:SetPoint("RIGHT",content,"RIGHT",0,0)
|
||||
|
||||
usedwidth = 0
|
||||
rowstart = frame
|
||||
rowstartoffset = frameoffset
|
||||
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(width)
|
||||
end
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
rowheight = frame.height or frame:GetHeight() or 0
|
||||
rowoffset = child.alignoffset or (rowheight / 2)
|
||||
rowstartoffset = rowoffset
|
||||
elseif child.width == "relative" then
|
||||
child:SetWidth(width * child.relWidth)
|
||||
|
||||
if child.OnWidthSet then
|
||||
child:OnWidthSet(width)
|
||||
end
|
||||
|
||||
if child.DoLayout then
|
||||
child:DoLayout()
|
||||
end
|
||||
elseif oversize then
|
||||
if width > 1 then
|
||||
frame:SetPoint("RIGHT",content,"RIGHT",0,0)
|
||||
end
|
||||
end
|
||||
|
||||
if child.height == "fill" then
|
||||
frame:SetPoint("BOTTOM",content,"BOTTOM")
|
||||
isfullheight = true
|
||||
end
|
||||
end
|
||||
|
||||
--anchor the last row, if its full height needs a special case since its height has just been changed by the anchor
|
||||
if isfullheight then
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-height)
|
||||
elseif rowstart then
|
||||
rowstart:SetPoint("TOPLEFT",content,"TOPLEFT",0,-(height+(rowoffset-rowstartoffset)+3))
|
||||
end
|
||||
|
||||
height = height + rowheight + 3
|
||||
safecall( content.obj.LayoutFinished, content.obj, nil, height )
|
||||
end
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="AceGUI-3.0.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Button.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-CheckBox.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-ColorPicker.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-DropDownGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-DropDown.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-DropDown-Items.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-EditBox.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Frame.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Window.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Heading.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-InlineGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Keybinding.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-ScrollFrame.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-SimpleGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Slider.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-TabGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-TreeGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Label.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-MultiLineEditBox.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-BlizOptionsGroup.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-InteractiveLabel.lua"/>
|
||||
<Script file="widgets\AceGUIWidget-Icon.lua"/>
|
||||
</Ui>
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
|
||||
-------------
|
||||
-- Widgets --
|
||||
-------------
|
||||
--[[
|
||||
Widgets must provide the following functions
|
||||
Acquire() - Called when the object is aquired, should set everything to a default hidden state
|
||||
Release() - Called when the object is Released, should remove any anchors and hide the Widget
|
||||
|
||||
And the following members
|
||||
frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
|
||||
type - the type of the object, same as the name given to :RegisterWidget()
|
||||
|
||||
Widgets contain a table called userdata, this is a safe place to store data associated with the wigdet
|
||||
It will be cleared automatically when a widget is released
|
||||
Placing values directly into a widget object should be avoided
|
||||
|
||||
If the Widget can act as a container for other Widgets the following
|
||||
content - frame or derivitive that children will be anchored to
|
||||
|
||||
The Widget can supply the following Optional Members
|
||||
|
||||
|
||||
]]
|
||||
|
||||
----------------------------------
|
||||
-- Blizzard Options Group --
|
||||
----------------------------------
|
||||
--[[
|
||||
Group Designed to be added to the bliz interface options panel
|
||||
]]
|
||||
|
||||
-- WoW APIs
|
||||
local CreateFrame = CreateFrame
|
||||
|
||||
do
|
||||
local Type = "BlizOptionsGroup"
|
||||
local Version = 10
|
||||
|
||||
local function OnAcquire(self)
|
||||
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
self:SetName()
|
||||
end
|
||||
|
||||
local function okay(this)
|
||||
this.obj:Fire("okay")
|
||||
end
|
||||
|
||||
local function cancel(this)
|
||||
this.obj:Fire("cancel")
|
||||
end
|
||||
|
||||
local function defaults(this)
|
||||
this.obj:Fire("defaults")
|
||||
end
|
||||
|
||||
local function SetName(self, name, parent)
|
||||
self.frame.name = name
|
||||
self.frame.parent = parent
|
||||
end
|
||||
|
||||
local function OnShow(this)
|
||||
this.obj:Fire("OnShow")
|
||||
end
|
||||
|
||||
local function OnHide(this)
|
||||
this.obj:Fire("OnHide")
|
||||
end
|
||||
|
||||
local function OnWidthSet(self, width)
|
||||
local content = self.content
|
||||
local contentwidth = width - 63
|
||||
if contentwidth < 0 then
|
||||
contentwidth = 0
|
||||
end
|
||||
content:SetWidth(contentwidth)
|
||||
content.width = contentwidth
|
||||
end
|
||||
|
||||
|
||||
local function OnHeightSet(self, height)
|
||||
local content = self.content
|
||||
local contentheight = height - 26
|
||||
if contentheight < 0 then
|
||||
contentheight = 0
|
||||
end
|
||||
content:SetHeight(contentheight)
|
||||
content.height = contentheight
|
||||
end
|
||||
|
||||
local function SetTitle(self, title)
|
||||
local content = self.content
|
||||
content:ClearAllPoints()
|
||||
if not title or title == "" then
|
||||
content:SetPoint("TOPLEFT",self.frame,"TOPLEFT",10,-10)
|
||||
self.label:SetText("")
|
||||
else
|
||||
content:SetPoint("TOPLEFT",self.frame,"TOPLEFT",10,-40)
|
||||
self.label:SetText(title)
|
||||
end
|
||||
content:SetPoint("BOTTOMRIGHT",self.frame,"BOTTOMRIGHT",-10,10)
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Frame")
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
self.frame = frame
|
||||
self.SetName = SetName
|
||||
|
||||
self.OnWidthSet = OnWidthSet
|
||||
self.OnHeightSet = OnHeightSet
|
||||
self.SetTitle = SetTitle
|
||||
|
||||
frame.obj = self
|
||||
frame.okay = okay
|
||||
frame.cancel = cancel
|
||||
frame.defaults = defaults
|
||||
|
||||
frame:Hide()
|
||||
frame:SetScript("OnHide",OnHide)
|
||||
frame:SetScript("OnShow",OnShow)
|
||||
|
||||
local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormalLarge")
|
||||
self.label = label
|
||||
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -15)
|
||||
label:SetPoint("BOTTOMRIGHT", frame, "TOPRIGHT", 10, -45)
|
||||
label:SetJustifyH("LEFT")
|
||||
label:SetJustifyV("TOP")
|
||||
|
||||
--Container Support
|
||||
local content = CreateFrame("Frame",nil,frame)
|
||||
self.content = content
|
||||
content.obj = self
|
||||
content:SetPoint("TOPLEFT",frame,"TOPLEFT",15,-10)
|
||||
content:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-10,10)
|
||||
|
||||
AceGUI:RegisterAsContainer(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- WoW APIs
|
||||
local _G = _G
|
||||
local PlaySound = PlaySound
|
||||
local CreateFrame, UIParent = CreateFrame, UIParent
|
||||
|
||||
--------------------------
|
||||
-- Button --
|
||||
--------------------------
|
||||
do
|
||||
local Type = "Button"
|
||||
local Version = 13
|
||||
|
||||
local function OnAcquire(self)
|
||||
-- restore default values
|
||||
self:SetHeight(24)
|
||||
self:SetWidth(200)
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
self:SetDisabled(false)
|
||||
end
|
||||
|
||||
local function Button_OnClick(this, ...)
|
||||
PlaySound("igMainMenuOption")
|
||||
this.obj:Fire("OnClick", ...)
|
||||
AceGUI:ClearFocus()
|
||||
end
|
||||
|
||||
local function Button_OnEnter(this)
|
||||
this.obj:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function Button_OnLeave(this)
|
||||
this.obj:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function SetText(self, text)
|
||||
self.text:SetText(text or "")
|
||||
end
|
||||
|
||||
local function SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.frame:Disable()
|
||||
else
|
||||
self.frame:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local num = AceGUI:GetNextWidgetNum(Type)
|
||||
local name = "AceGUI30Button"..num
|
||||
local frame = CreateFrame("Button",name,UIParent,"UIPanelButtonTemplate2")
|
||||
local self = {}
|
||||
self.num = num
|
||||
self.type = Type
|
||||
self.frame = frame
|
||||
|
||||
local left = _G[name .. "Left"]
|
||||
local right = _G[name .. "Right"]
|
||||
local middle = _G[name .. "Middle"]
|
||||
|
||||
left:SetPoint("TOP", frame, "TOP", 0, 0)
|
||||
left:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0)
|
||||
|
||||
right:SetPoint("TOP", frame, "TOP", 0, 0)
|
||||
right:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0)
|
||||
|
||||
middle:SetPoint("TOP", frame, "TOP", 0, 0)
|
||||
middle:SetPoint("BOTTOM", frame, "BOTTOM", 0, 0)
|
||||
|
||||
local text = frame:GetFontString()
|
||||
self.text = text
|
||||
text:ClearAllPoints()
|
||||
text:SetPoint("TOPLEFT",frame,"TOPLEFT", 15, -1)
|
||||
text:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT", -15, 1)
|
||||
text:SetJustifyV("MIDDLE")
|
||||
|
||||
frame:SetScript("OnClick",Button_OnClick)
|
||||
frame:SetScript("OnEnter",Button_OnEnter)
|
||||
frame:SetScript("OnLeave",Button_OnLeave)
|
||||
|
||||
self.SetText = SetText
|
||||
self.SetDisabled = SetDisabled
|
||||
|
||||
frame:EnableMouse(true)
|
||||
|
||||
frame:SetHeight(24)
|
||||
frame:SetWidth(200)
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+309
@@ -0,0 +1,309 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local select = select
|
||||
|
||||
-- WoW APIs
|
||||
local PlaySound = PlaySound
|
||||
local CreateFrame, UIParent = CreateFrame, UIParent
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: SetDesaturation, GameFontHighlight
|
||||
|
||||
--------------------------
|
||||
-- Check Box --
|
||||
--------------------------
|
||||
--[[
|
||||
Events :
|
||||
OnValueChanged
|
||||
|
||||
]]
|
||||
do
|
||||
local Type = "CheckBox"
|
||||
local Version = 16
|
||||
|
||||
local function OnAcquire(self)
|
||||
self:SetValue(false)
|
||||
self.tristate = nil
|
||||
self:SetHeight(24)
|
||||
self:SetWidth(200)
|
||||
self:SetImage()
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
self.check:Hide()
|
||||
self.highlight:Hide()
|
||||
self.down = nil
|
||||
self.checked = nil
|
||||
self:SetType()
|
||||
self:SetDisabled(false)
|
||||
self:SetDescription(nil)
|
||||
end
|
||||
|
||||
local function CheckBox_OnEnter(this)
|
||||
local self = this.obj
|
||||
self.highlight:Show()
|
||||
self:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function CheckBox_OnLeave(this)
|
||||
local self = this.obj
|
||||
self.highlight:Hide()
|
||||
self:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function CheckBox_OnMouseUp(this)
|
||||
local self = this.obj
|
||||
if not self.disabled then
|
||||
self:ToggleChecked()
|
||||
|
||||
if self.checked then
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
else -- for both nil and false (tristate)
|
||||
PlaySound("igMainMenuOptionCheckBoxOff")
|
||||
end
|
||||
|
||||
self:Fire("OnValueChanged",self.checked)
|
||||
self.text:SetPoint("LEFT",self.check,"RIGHT",0,0)
|
||||
end
|
||||
self.down = nil
|
||||
end
|
||||
|
||||
local function CheckBox_OnMouseDown(this)
|
||||
local self = this.obj
|
||||
if not self.disabled then
|
||||
self.text:SetPoint("LEFT",self.check,"RIGHT",1,-1)
|
||||
self.down = true
|
||||
end
|
||||
AceGUI:ClearFocus()
|
||||
end
|
||||
|
||||
local function SetDisabled(self,disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.frame:Disable()
|
||||
self.text:SetTextColor(0.5,0.5,0.5)
|
||||
SetDesaturation(self.check, true)
|
||||
else
|
||||
self.frame:Enable()
|
||||
self.text:SetTextColor(1,1,1)
|
||||
if self.tristate and self.checked == nil then
|
||||
SetDesaturation(self.check, true)
|
||||
else
|
||||
SetDesaturation(self.check, false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SetValue(self,value)
|
||||
local check = self.check
|
||||
self.checked = value
|
||||
if value then
|
||||
SetDesaturation(self.check, false)
|
||||
self.check:Show()
|
||||
else
|
||||
--Nil is the unknown tristate value
|
||||
if self.tristate and value == nil then
|
||||
SetDesaturation(self.check, true)
|
||||
self.check:Show()
|
||||
else
|
||||
SetDesaturation(self.check, false)
|
||||
self.check:Hide()
|
||||
end
|
||||
end
|
||||
SetDisabled(self, self.disabled)
|
||||
end
|
||||
|
||||
local function SetTriState(self, enabled)
|
||||
self.tristate = enabled
|
||||
self:SetValue(self:GetValue())
|
||||
end
|
||||
|
||||
local function GetValue(self)
|
||||
return self.checked
|
||||
end
|
||||
|
||||
local function SetType(self, type)
|
||||
local checkbg = self.checkbg
|
||||
local check = self.check
|
||||
local highlight = self.highlight
|
||||
|
||||
if type == "radio" then
|
||||
checkbg:SetHeight(16)
|
||||
checkbg:SetWidth(16)
|
||||
checkbg:SetTexture("Interface\\Buttons\\UI-RadioButton")
|
||||
checkbg:SetTexCoord(0,0.25,0,1)
|
||||
check:SetHeight(16)
|
||||
check:SetWidth(16)
|
||||
check:SetTexture("Interface\\Buttons\\UI-RadioButton")
|
||||
check:SetTexCoord(0.25,0.5,0,1)
|
||||
check:SetBlendMode("ADD")
|
||||
highlight:SetTexture("Interface\\Buttons\\UI-RadioButton")
|
||||
highlight:SetTexCoord(0.5,0.75,0,1)
|
||||
else
|
||||
checkbg:SetHeight(24)
|
||||
checkbg:SetWidth(24)
|
||||
checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
|
||||
checkbg:SetTexCoord(0,1,0,1)
|
||||
check:SetHeight(24)
|
||||
check:SetWidth(24)
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
check:SetTexCoord(0,1,0,1)
|
||||
check:SetBlendMode("BLEND")
|
||||
highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
|
||||
highlight:SetTexCoord(0,1,0,1)
|
||||
end
|
||||
end
|
||||
|
||||
local function ToggleChecked(self)
|
||||
local value = self:GetValue()
|
||||
if self.tristate then
|
||||
--cycle in true, nil, false order
|
||||
if value then
|
||||
self:SetValue(nil)
|
||||
elseif value == nil then
|
||||
self:SetValue(false)
|
||||
else
|
||||
self:SetValue(true)
|
||||
end
|
||||
else
|
||||
self:SetValue(not self:GetValue())
|
||||
end
|
||||
end
|
||||
|
||||
local function SetLabel(self, label)
|
||||
self.text:SetText(label)
|
||||
end
|
||||
|
||||
local function SetDescription(self, desc)
|
||||
if desc then
|
||||
if not self.desc then
|
||||
local desc = self.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
desc:ClearAllPoints()
|
||||
desc:SetPoint("TOPLEFT", self.check, "TOPRIGHT", 5, -21)
|
||||
desc:SetWidth(self.frame.width - 30)
|
||||
desc:SetJustifyH("LEFT")
|
||||
desc:SetJustifyV("TOP")
|
||||
self.desc = desc
|
||||
end
|
||||
self.desc:Show()
|
||||
--self.text:SetFontObject(GameFontNormal)
|
||||
self.desc:SetText(desc)
|
||||
self:SetHeight(28 + self.desc:GetHeight())
|
||||
else
|
||||
if self.desc then
|
||||
self.desc:SetText("")
|
||||
self.desc:Hide()
|
||||
end
|
||||
self.text:SetFontObject(GameFontHighlight)
|
||||
self:SetHeight(24)
|
||||
end
|
||||
end
|
||||
|
||||
local function SetImage(self, path, ...)
|
||||
local image = self.image
|
||||
image:SetTexture(path)
|
||||
|
||||
if image:GetTexture() then
|
||||
local n = select('#', ...)
|
||||
if n == 4 or n == 8 then
|
||||
image:SetTexCoord(...)
|
||||
else
|
||||
image:SetTexCoord(0, 1, 0, 1)
|
||||
end
|
||||
end
|
||||
self:AlignImage()
|
||||
end
|
||||
|
||||
local function AlignImage(self)
|
||||
local img = self.image:GetTexture()
|
||||
self.text:ClearAllPoints()
|
||||
if not img then
|
||||
self.text:SetPoint("LEFT", self.check, "RIGHT", 0, 0)
|
||||
self.text:SetPoint("RIGHT", self.frame, "RIGHT", 0, 0)
|
||||
else
|
||||
self.text:SetPoint("LEFT", self.image,"RIGHT", 1, 0)
|
||||
self.text:SetPoint("RIGHT", self.frame,"RIGHT", 0, 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnWidthSet(self, width)
|
||||
if self.desc and self.desc:GetText() ~= "" and self.desc:GetText() ~= nil then
|
||||
self.desc:SetWidth(width - 30)
|
||||
self:SetHeight(28 + self.desc:GetHeight())
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Button",nil,UIParent)
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.SetValue = SetValue
|
||||
self.GetValue = GetValue
|
||||
self.SetDisabled = SetDisabled
|
||||
self.SetType = SetType
|
||||
self.ToggleChecked = ToggleChecked
|
||||
self.SetLabel = SetLabel
|
||||
self.SetTriState = SetTriState
|
||||
self.SetDescription = SetDescription
|
||||
self.OnWidthSet = OnWidthSet
|
||||
self.SetImage = SetImage
|
||||
self.AlignImage = AlignImage
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
local text = frame:CreateFontString(nil,"OVERLAY","GameFontHighlight")
|
||||
self.text = text
|
||||
|
||||
frame:SetScript("OnEnter",CheckBox_OnEnter)
|
||||
frame:SetScript("OnLeave",CheckBox_OnLeave)
|
||||
frame:SetScript("OnMouseUp",CheckBox_OnMouseUp)
|
||||
frame:SetScript("OnMouseDown",CheckBox_OnMouseDown)
|
||||
frame:EnableMouse()
|
||||
local checkbg = frame:CreateTexture(nil,"ARTWORK")
|
||||
self.checkbg = checkbg
|
||||
checkbg:SetWidth(24)
|
||||
checkbg:SetHeight(24)
|
||||
checkbg:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
|
||||
checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
|
||||
local check = frame:CreateTexture(nil,"OVERLAY")
|
||||
self.check = check
|
||||
check:SetWidth(24)
|
||||
check:SetHeight(24)
|
||||
check:SetPoint("CENTER",checkbg,"CENTER",0,0)
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
|
||||
local highlight = frame:CreateTexture(nil, "OVERLAY")
|
||||
self.highlight = highlight
|
||||
highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetAllPoints(checkbg)
|
||||
highlight:Hide()
|
||||
|
||||
local image = frame:CreateTexture(nil, "OVERLAY")
|
||||
self.image = image
|
||||
image:SetHeight(16)
|
||||
image:SetWidth(16)
|
||||
image:SetPoint("LEFT", check, "RIGHT", 1, 0)
|
||||
|
||||
text:SetJustifyH("LEFT")
|
||||
frame:SetHeight(24)
|
||||
frame:SetWidth(200)
|
||||
text:SetHeight(18)
|
||||
text:SetPoint("LEFT",check,"RIGHT",0,0)
|
||||
text:SetPoint("RIGHT",frame,"RIGHT",0,0)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- WoW APIs
|
||||
local CreateFrame, UIParent = CreateFrame, UIParent
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: ShowUIPanel, HideUIPanel, ColorPickerFrame, OpacitySliderFrame
|
||||
|
||||
--------------------------
|
||||
-- ColorPicker --
|
||||
--------------------------
|
||||
do
|
||||
local Type = "ColorPicker"
|
||||
local Version = 11
|
||||
|
||||
local function OnAcquire(self)
|
||||
self.HasAlpha = false
|
||||
self:SetColor(0,0,0,1)
|
||||
self:SetHeight(24)
|
||||
self:SetWidth(200)
|
||||
end
|
||||
|
||||
local function SetLabel(self, text)
|
||||
self.text:SetText(text)
|
||||
end
|
||||
|
||||
local function SetColor(self,r,g,b,a)
|
||||
self.r = r
|
||||
self.g = g
|
||||
self.b = b
|
||||
self.a = a or 1
|
||||
self.colorSwatch:SetVertexColor(r,g,b,a)
|
||||
end
|
||||
|
||||
local function Control_OnEnter(this)
|
||||
this.obj:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function Control_OnLeave(this)
|
||||
this.obj:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function SetHasAlpha(self, HasAlpha)
|
||||
self.HasAlpha = HasAlpha
|
||||
end
|
||||
|
||||
local function ColorCallback(self,r,g,b,a,isAlpha)
|
||||
if not self.HasAlpha then
|
||||
a = 1
|
||||
end
|
||||
self:SetColor(r,g,b,a)
|
||||
if ColorPickerFrame:IsVisible() then
|
||||
--colorpicker is still open
|
||||
|
||||
self:Fire("OnValueChanged",r,g,b,a)
|
||||
else
|
||||
--colorpicker is closed, color callback is first, ignore it,
|
||||
--alpha callback is the final call after it closes so confirm now
|
||||
if isAlpha then
|
||||
self:Fire("OnValueConfirmed",r,g,b,a)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function ColorSwatch_OnClick(this)
|
||||
HideUIPanel(ColorPickerFrame)
|
||||
local self = this.obj
|
||||
if not self.disabled then
|
||||
ColorPickerFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
ColorPickerFrame.func = function()
|
||||
local r,g,b = ColorPickerFrame:GetColorRGB()
|
||||
local a = 1 - OpacitySliderFrame:GetValue()
|
||||
ColorCallback(self,r,g,b,a)
|
||||
end
|
||||
|
||||
ColorPickerFrame.hasOpacity = self.HasAlpha
|
||||
ColorPickerFrame.opacityFunc = function()
|
||||
local r,g,b = ColorPickerFrame:GetColorRGB()
|
||||
local a = 1 - OpacitySliderFrame:GetValue()
|
||||
ColorCallback(self,r,g,b,a,true)
|
||||
end
|
||||
local r, g, b, a = self.r, self.g, self.b, self.a
|
||||
if self.HasAlpha then
|
||||
ColorPickerFrame.opacity = 1 - (a or 0)
|
||||
end
|
||||
ColorPickerFrame:SetColorRGB(r, g, b)
|
||||
|
||||
ColorPickerFrame.cancelFunc = function()
|
||||
ColorCallback(self,r,g,b,a,true)
|
||||
end
|
||||
ShowUIPanel(ColorPickerFrame)
|
||||
end
|
||||
AceGUI:ClearFocus()
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
local function SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if self.disabled then
|
||||
self.frame:Disable()
|
||||
self.text:SetTextColor(0.5,0.5,0.5)
|
||||
else
|
||||
self.frame:Enable()
|
||||
self.text:SetTextColor(1,1,1)
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local frame = CreateFrame("Button",nil,UIParent)
|
||||
local self = {}
|
||||
self.type = Type
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.SetLabel = SetLabel
|
||||
self.SetColor = SetColor
|
||||
self.SetDisabled = SetDisabled
|
||||
self.SetHasAlpha = SetHasAlpha
|
||||
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
local text = frame:CreateFontString(nil,"OVERLAY","GameFontHighlight")
|
||||
self.text = text
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetTextColor(1,1,1)
|
||||
frame:SetHeight(24)
|
||||
frame:SetWidth(200)
|
||||
text:SetHeight(24)
|
||||
frame:SetScript("OnClick", ColorSwatch_OnClick)
|
||||
frame:SetScript("OnEnter",Control_OnEnter)
|
||||
frame:SetScript("OnLeave",Control_OnLeave)
|
||||
|
||||
local colorSwatch = frame:CreateTexture(nil, "OVERLAY")
|
||||
self.colorSwatch = colorSwatch
|
||||
colorSwatch:SetWidth(19)
|
||||
colorSwatch:SetHeight(19)
|
||||
colorSwatch:SetTexture("Interface\\ChatFrame\\ChatFrameColorSwatch")
|
||||
local texture = frame:CreateTexture(nil, "BACKGROUND")
|
||||
colorSwatch.texture = texture
|
||||
texture:SetWidth(16)
|
||||
texture:SetHeight(16)
|
||||
texture:SetTexture(1,1,1)
|
||||
texture:Show()
|
||||
|
||||
local checkers = frame:CreateTexture(nil, "BACKGROUND")
|
||||
colorSwatch.checkers = checkers
|
||||
checkers:SetTexture("Tileset\\Generic\\Checkers")
|
||||
checkers:SetDesaturated(true)
|
||||
checkers:SetVertexColor(1,1,1,0.75)
|
||||
checkers:SetTexCoord(.25,0,0.5,.25)
|
||||
checkers:SetWidth(14)
|
||||
checkers:SetHeight(14)
|
||||
checkers:Show()
|
||||
|
||||
local highlight = frame:CreateTexture(nil, "BACKGROUND")
|
||||
self.highlight = highlight
|
||||
highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetAllPoints(frame)
|
||||
highlight:Hide()
|
||||
|
||||
texture:SetPoint("CENTER", colorSwatch, "CENTER")
|
||||
checkers:SetPoint("CENTER", colorSwatch, "CENTER")
|
||||
colorSwatch:SetPoint("LEFT", frame, "LEFT", 0, 0)
|
||||
text:SetPoint("LEFT",colorSwatch,"RIGHT",2,0)
|
||||
text:SetPoint("RIGHT",frame,"RIGHT")
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(Type,Constructor,Version)
|
||||
end
|
||||
+465
@@ -0,0 +1,465 @@
|
||||
--[[ $Id: AceGUIWidget-DropDown-Items.lua 916 2010-03-15 12:24:36Z nevcairiel $ ]]--
|
||||
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local select, assert = select, assert
|
||||
|
||||
-- WoW APIs
|
||||
local PlaySound = PlaySound
|
||||
local CreateFrame = CreateFrame
|
||||
|
||||
local function fixlevels(parent,...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
while child do
|
||||
child:SetFrameLevel(parent:GetFrameLevel()+1)
|
||||
fixlevels(child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function fixstrata(strata, parent, ...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
parent:SetFrameStrata(strata)
|
||||
while child do
|
||||
fixstrata(strata, child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- ItemBase is the base "class" for all dropdown items.
|
||||
-- Each item has to use ItemBase.Create(widgetType) to
|
||||
-- create an initial 'self' value.
|
||||
-- ItemBase will add common functions and ui event handlers.
|
||||
-- Be sure to keep basic usage when you override functions.
|
||||
|
||||
local ItemBase = {
|
||||
-- NOTE: The ItemBase version is added to each item's version number
|
||||
-- to ensure proper updates on ItemBase changes.
|
||||
-- Use at least 1000er steps.
|
||||
version = 1000,
|
||||
counter = 0,
|
||||
}
|
||||
|
||||
function ItemBase.Frame_OnEnter(this)
|
||||
local self = this.obj
|
||||
|
||||
if self.useHighlight then
|
||||
self.highlight:Show()
|
||||
end
|
||||
self:Fire("OnEnter")
|
||||
|
||||
if self.specialOnEnter then
|
||||
self.specialOnEnter(self)
|
||||
end
|
||||
end
|
||||
|
||||
function ItemBase.Frame_OnLeave(this)
|
||||
local self = this.obj
|
||||
|
||||
self.highlight:Hide()
|
||||
self:Fire("OnLeave")
|
||||
|
||||
if self.specialOnLeave then
|
||||
self.specialOnLeave(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
function ItemBase.OnAcquire(self)
|
||||
self.frame:SetToplevel(true)
|
||||
self.frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
function ItemBase.OnRelease(self)
|
||||
self:SetDisabled(false)
|
||||
self.pullout = nil
|
||||
self.frame:SetParent(nil)
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
-- NOTE: this is called by a Dropdown-Pullout.
|
||||
-- Do not call this method directly
|
||||
function ItemBase.SetPullout(self, pullout)
|
||||
self.pullout = pullout
|
||||
|
||||
self.frame:SetParent(nil)
|
||||
self.frame:SetParent(pullout.itemFrame)
|
||||
self.parent = pullout.itemFrame
|
||||
fixlevels(pullout.itemFrame, pullout.itemFrame:GetChildren())
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.SetText(self, text)
|
||||
self.text:SetText(text or "")
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.GetText(self)
|
||||
return self.text:GetText()
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.SetPoint(self, ...)
|
||||
self.frame:SetPoint(...)
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.Show(self)
|
||||
self.frame:Show()
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.Hide(self)
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
function ItemBase.SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.useHighlight = false
|
||||
self.text:SetTextColor(.5, .5, .5)
|
||||
else
|
||||
self.useHighlight = true
|
||||
self.text:SetTextColor(1, 1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
-- NOTE: this is called by a Dropdown-Pullout.
|
||||
-- Do not call this method directly
|
||||
function ItemBase.SetOnLeave(self, func)
|
||||
self.specialOnLeave = func
|
||||
end
|
||||
|
||||
-- exported
|
||||
-- NOTE: this is called by a Dropdown-Pullout.
|
||||
-- Do not call this method directly
|
||||
function ItemBase.SetOnEnter(self, func)
|
||||
self.specialOnEnter = func
|
||||
end
|
||||
|
||||
function ItemBase.Create(type)
|
||||
-- NOTE: Most of the following code is copied from AceGUI-3.0/Dropdown widget
|
||||
local count = AceGUI:GetNextWidgetNum(type)
|
||||
local frame = CreateFrame("Button", "AceGUI30DropDownItem"..count)
|
||||
local self = {}
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
self.type = type
|
||||
|
||||
self.useHighlight = true
|
||||
|
||||
frame:SetHeight(17)
|
||||
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
local text = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
|
||||
text:SetTextColor(1,1,1)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetPoint("TOPLEFT",frame,"TOPLEFT",18,0)
|
||||
text:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",-8,0)
|
||||
self.text = text
|
||||
|
||||
local highlight = frame:CreateTexture(nil, "OVERLAY")
|
||||
highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
highlight:SetBlendMode("ADD")
|
||||
highlight:SetHeight(14)
|
||||
highlight:ClearAllPoints()
|
||||
highlight:SetPoint("RIGHT",frame,"RIGHT",-3,0)
|
||||
highlight:SetPoint("LEFT",frame,"LEFT",5,0)
|
||||
highlight:Hide()
|
||||
self.highlight = highlight
|
||||
|
||||
local check = frame:CreateTexture("OVERLAY")
|
||||
check:SetWidth(16)
|
||||
check:SetHeight(16)
|
||||
check:SetPoint("LEFT",frame,"LEFT",3,-1)
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
|
||||
check:Hide()
|
||||
self.check = check
|
||||
|
||||
local sub = frame:CreateTexture("OVERLAY")
|
||||
sub:SetWidth(16)
|
||||
sub:SetHeight(16)
|
||||
sub:SetPoint("RIGHT",frame,"RIGHT",-3,-1)
|
||||
sub:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
|
||||
sub:Hide()
|
||||
self.sub = sub
|
||||
|
||||
frame:SetScript("OnEnter", ItemBase.Frame_OnEnter)
|
||||
frame:SetScript("OnLeave", ItemBase.Frame_OnLeave)
|
||||
|
||||
self.OnAcquire = ItemBase.OnAcquire
|
||||
self.OnRelease = ItemBase.OnRelease
|
||||
|
||||
self.SetPullout = ItemBase.SetPullout
|
||||
self.GetText = ItemBase.GetText
|
||||
self.SetText = ItemBase.SetText
|
||||
self.SetDisabled = ItemBase.SetDisabled
|
||||
|
||||
self.SetPoint = ItemBase.SetPoint
|
||||
self.Show = ItemBase.Show
|
||||
self.Hide = ItemBase.Hide
|
||||
|
||||
self.SetOnLeave = ItemBase.SetOnLeave
|
||||
self.SetOnEnter = ItemBase.SetOnEnter
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--[[
|
||||
Template for items:
|
||||
|
||||
-- Item:
|
||||
--
|
||||
do
|
||||
local widgetType = "Dropdown-Item-"
|
||||
local widgetVersion = 1
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
--]]
|
||||
|
||||
-- Item: Header
|
||||
-- A single text entry.
|
||||
-- Special: Different text color and no highlight
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Header"
|
||||
local widgetVersion = 1
|
||||
|
||||
local function OnEnter(this)
|
||||
local self = this.obj
|
||||
self:Fire("OnEnter")
|
||||
|
||||
if self.specialOnEnter then
|
||||
self.specialOnEnter(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnLeave(this)
|
||||
local self = this.obj
|
||||
self:Fire("OnLeave")
|
||||
|
||||
if self.specialOnLeave then
|
||||
self.specialOnLeave(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported, override
|
||||
local function SetDisabled(self, disabled)
|
||||
ItemBase.SetDisabled(self, disabled)
|
||||
if not disabled then
|
||||
self.text:SetTextColor(1, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.SetDisabled = SetDisabled
|
||||
|
||||
self.frame:SetScript("OnEnter", OnEnter)
|
||||
self.frame:SetScript("OnLeave", OnLeave)
|
||||
|
||||
self.text:SetTextColor(1, 1, 0)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Execute
|
||||
-- A simple button
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Execute"
|
||||
local widgetVersion = 1
|
||||
|
||||
local function Frame_OnClick(this, button)
|
||||
local self = this.obj
|
||||
if self.disabled then return end
|
||||
self:Fire("OnClick")
|
||||
if self.pullout then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.frame:SetScript("OnClick", Frame_OnClick)
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Toggle
|
||||
-- Some sort of checkbox for dropdown menus.
|
||||
-- Does not close the pullout on click.
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Toggle"
|
||||
local widgetVersion = 3
|
||||
|
||||
local function UpdateToggle(self)
|
||||
if self.value then
|
||||
self.check:Show()
|
||||
else
|
||||
self.check:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
local function OnRelease(self)
|
||||
ItemBase.OnRelease(self)
|
||||
self:SetValue(nil)
|
||||
end
|
||||
|
||||
local function Frame_OnClick(this, button)
|
||||
local self = this.obj
|
||||
if self.disabled then return end
|
||||
self.value = not self.value
|
||||
if self.value then
|
||||
PlaySound("igMainMenuOptionCheckBoxOn")
|
||||
else
|
||||
PlaySound("igMainMenuOptionCheckBoxOff")
|
||||
end
|
||||
UpdateToggle(self)
|
||||
self:Fire("OnValueChanged", self.value)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetValue(self, value)
|
||||
self.value = value
|
||||
UpdateToggle(self)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetValue(self)
|
||||
return self.value
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.frame:SetScript("OnClick", Frame_OnClick)
|
||||
|
||||
self.SetValue = SetValue
|
||||
self.GetValue = GetValue
|
||||
self.OnRelease = OnRelease
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Menu
|
||||
-- Shows a submenu on mouse over
|
||||
-- Does not close the pullout on click
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Menu"
|
||||
local widgetVersion = 2
|
||||
|
||||
local function OnEnter(this)
|
||||
local self = this.obj
|
||||
self:Fire("OnEnter")
|
||||
|
||||
if self.specialOnEnter then
|
||||
self.specialOnEnter(self)
|
||||
end
|
||||
|
||||
self.highlight:Show()
|
||||
|
||||
if not self.disabled and self.submenu then
|
||||
self.submenu:Open("TOPLEFT", self.frame, "TOPRIGHT", self.pullout:GetRightBorderWidth(), 0, self.frame:GetFrameLevel() + 100)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnHide(this)
|
||||
local self = this.obj
|
||||
if self.submenu then
|
||||
self.submenu:Close()
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetMenu(self, menu)
|
||||
assert(menu.type == "Dropdown-Pullout")
|
||||
self.submenu = menu
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function CloseMenu(self)
|
||||
self.submenu:Close()
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.sub:Show()
|
||||
|
||||
self.frame:SetScript("OnEnter", OnEnter)
|
||||
self.frame:SetScript("OnHide", OnHide)
|
||||
|
||||
self.SetMenu = SetMenu
|
||||
self.CloseMenu = CloseMenu
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
|
||||
-- Item: Separator
|
||||
-- A single line to separate items
|
||||
do
|
||||
local widgetType = "Dropdown-Item-Separator"
|
||||
local widgetVersion = 1
|
||||
|
||||
-- exported, override
|
||||
local function SetDisabled(self, disabled)
|
||||
ItemBase.SetDisabled(self, disabled)
|
||||
self.useHighlight = false
|
||||
end
|
||||
|
||||
local function Constructor()
|
||||
local self = ItemBase.Create(widgetType)
|
||||
|
||||
self.SetDisabled = SetDisabled
|
||||
|
||||
local line = self.frame:CreateTexture(nil, "OVERLAY")
|
||||
line:SetHeight(1)
|
||||
line:SetTexture(.5, .5, .5)
|
||||
line:SetPoint("LEFT", self.frame, "LEFT", 10, 0)
|
||||
line:SetPoint("RIGHT", self.frame, "RIGHT", -10, 0)
|
||||
|
||||
self.text:Hide()
|
||||
|
||||
self.useHighlight = false
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion + ItemBase.version)
|
||||
end
|
||||
+707
@@ -0,0 +1,707 @@
|
||||
--[[ $Id: AceGUIWidget-DropDown.lua 916 2010-03-15 12:24:36Z nevcairiel $ ]]--
|
||||
local AceGUI = LibStub("AceGUI-3.0")
|
||||
|
||||
-- Lua APIs
|
||||
local min, max, floor = math.min, math.max, math.floor
|
||||
local select, pairs, ipairs = select, pairs, ipairs
|
||||
local tsort = table.sort
|
||||
|
||||
-- WoW APIs
|
||||
local PlaySound = PlaySound
|
||||
local UIParent, CreateFrame = UIParent, CreateFrame
|
||||
local _G = _G
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: CLOSE
|
||||
|
||||
local function fixlevels(parent,...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
while child do
|
||||
child:SetFrameLevel(parent:GetFrameLevel()+1)
|
||||
fixlevels(child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function fixstrata(strata, parent, ...)
|
||||
local i = 1
|
||||
local child = select(i, ...)
|
||||
parent:SetFrameStrata(strata)
|
||||
while child do
|
||||
fixstrata(strata, child, child:GetChildren())
|
||||
i = i + 1
|
||||
child = select(i, ...)
|
||||
end
|
||||
end
|
||||
|
||||
do
|
||||
local widgetType = "Dropdown-Pullout"
|
||||
local widgetVersion = 3
|
||||
|
||||
--[[ Static data ]]--
|
||||
|
||||
local backdrop = {
|
||||
bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
edgeSize = 32,
|
||||
tileSize = 32,
|
||||
tile = true,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
}
|
||||
local sliderBackdrop = {
|
||||
bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
|
||||
edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
|
||||
tile = true, tileSize = 8, edgeSize = 8,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 }
|
||||
}
|
||||
|
||||
local defaultWidth = 200
|
||||
local defaultMaxHeight = 600
|
||||
|
||||
--[[ UI Event Handlers ]]--
|
||||
|
||||
-- HACK: This should be no part of the pullout, but there
|
||||
-- is no other 'clean' way to response to any item-OnEnter
|
||||
-- Used to close Submenus when an other item is entered
|
||||
local function OnEnter(item)
|
||||
local self = item.pullout
|
||||
for k, v in ipairs(self.items) do
|
||||
if v.CloseMenu and v ~= item then
|
||||
v:CloseMenu()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- See the note in Constructor() for each scroll related function
|
||||
local function OnMouseWheel(this, value)
|
||||
this.obj:MoveScroll(value)
|
||||
end
|
||||
|
||||
local function OnScrollValueChanged(this, value)
|
||||
this.obj:SetScroll(value)
|
||||
end
|
||||
|
||||
local function OnSizeChanged(this)
|
||||
this.obj:FixScroll()
|
||||
end
|
||||
|
||||
--[[ Exported methods ]]--
|
||||
|
||||
-- exported
|
||||
local function SetScroll(self, value)
|
||||
local status = self.scrollStatus
|
||||
local frame, child = self.scrollFrame, self.itemFrame
|
||||
local height, viewheight = frame:GetHeight(), child:GetHeight()
|
||||
|
||||
local offset
|
||||
if height > viewheight then
|
||||
offset = 0
|
||||
else
|
||||
offset = floor((viewheight - height) / 1000 * value)
|
||||
end
|
||||
child:ClearAllPoints()
|
||||
child:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, offset)
|
||||
child:SetPoint("TOPRIGHT", frame, "TOPRIGHT", self.slider:IsShown() and -12 or 0, offset)
|
||||
status.offset = offset
|
||||
status.scrollvalue = value
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function MoveScroll(self, value)
|
||||
local status = self.scrollStatus
|
||||
local frame, child = self.scrollFrame, self.itemFrame
|
||||
local height, viewheight = frame:GetHeight(), child:GetHeight()
|
||||
|
||||
if height > viewheight then
|
||||
self.slider:Hide()
|
||||
else
|
||||
self.slider:Show()
|
||||
local diff = height - viewheight
|
||||
local delta = 1
|
||||
if value < 0 then
|
||||
delta = -1
|
||||
end
|
||||
self.slider:SetValue(min(max(status.scrollvalue + delta*(1000/(diff/45)),0), 1000))
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function FixScroll(self)
|
||||
local status = self.scrollStatus
|
||||
local frame, child = self.scrollFrame, self.itemFrame
|
||||
local height, viewheight = frame:GetHeight(), child:GetHeight()
|
||||
local offset = status.offset or 0
|
||||
|
||||
if viewheight < height then
|
||||
self.slider:Hide()
|
||||
child:SetPoint("TOPRIGHT", frame, "TOPRIGHT", 0, offset)
|
||||
self.slider:SetValue(0)
|
||||
else
|
||||
self.slider:Show()
|
||||
local value = (offset / (viewheight - height) * 1000)
|
||||
if value > 1000 then value = 1000 end
|
||||
self.slider:SetValue(value)
|
||||
self:SetScroll(value)
|
||||
if value < 1000 then
|
||||
child:ClearAllPoints()
|
||||
child:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, offset)
|
||||
child:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, offset)
|
||||
status.offset = offset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnAcquire(self)
|
||||
self.frame:SetParent(UIParent)
|
||||
--self.itemFrame:SetToplevel(true)
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnRelease(self)
|
||||
self:Clear()
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function AddItem(self, item)
|
||||
self.items[#self.items + 1] = item
|
||||
|
||||
local h = #self.items * 16
|
||||
self.itemFrame:SetHeight(h)
|
||||
self.frame:SetHeight(min(h + 34, self.maxHeight)) -- +34: 20 for scrollFrame placement (10 offset) and +14 for item placement
|
||||
|
||||
item.frame:SetPoint("LEFT", self.itemFrame, "LEFT")
|
||||
item.frame:SetPoint("RIGHT", self.itemFrame, "RIGHT")
|
||||
|
||||
item:SetPullout(self)
|
||||
item:SetOnEnter(OnEnter)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function Open(self, point, relFrame, relPoint, x, y)
|
||||
local items = self.items
|
||||
local frame = self.frame
|
||||
local itemFrame = self.itemFrame
|
||||
|
||||
frame:SetPoint(point, relFrame, relPoint, x, y)
|
||||
|
||||
|
||||
local height = 8
|
||||
for i, item in pairs(items) do
|
||||
if i == 1 then
|
||||
item:SetPoint("TOP", itemFrame, "TOP", 0, -2)
|
||||
else
|
||||
item:SetPoint("TOP", items[i-1].frame, "BOTTOM", 0, 1)
|
||||
end
|
||||
|
||||
item:Show()
|
||||
|
||||
height = height + 16
|
||||
end
|
||||
itemFrame:SetHeight(height)
|
||||
fixstrata("TOOLTIP", frame, frame:GetChildren())
|
||||
frame:Show()
|
||||
self:Fire("OnOpen")
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function Close(self)
|
||||
self.frame:Hide()
|
||||
self:Fire("OnClose")
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function Clear(self)
|
||||
local items = self.items
|
||||
for i, item in pairs(items) do
|
||||
AceGUI:Release(item)
|
||||
items[i] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function IterateItems(self)
|
||||
return ipairs(self.items)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetHideOnLeave(self, val)
|
||||
self.hideOnLeave = val
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetMaxHeight(self, height)
|
||||
self.maxHeight = height or defaultMaxHeight
|
||||
if self.frame:GetHeight() > height then
|
||||
self.frame:SetHeight(height)
|
||||
elseif (self.itemFrame:GetHeight() + 34) < height then
|
||||
self.frame:SetHeight(self.itemFrame:GetHeight() + 34) -- see :AddItem
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetRightBorderWidth(self)
|
||||
return 6 + (self.slider:IsShown() and 12 or 0)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetLeftBorderWidth(self)
|
||||
return 6
|
||||
end
|
||||
|
||||
--[[ Constructor ]]--
|
||||
|
||||
local function Constructor()
|
||||
local count = AceGUI:GetNextWidgetNum(widgetType)
|
||||
local frame = CreateFrame("Frame", "AceGUI30Pullout"..count, UIParent)
|
||||
local self = {}
|
||||
self.count = count
|
||||
self.type = widgetType
|
||||
self.frame = frame
|
||||
frame.obj = self
|
||||
|
||||
self.OnAcquire = OnAcquire
|
||||
self.OnRelease = OnRelease
|
||||
|
||||
self.AddItem = AddItem
|
||||
self.Open = Open
|
||||
self.Close = Close
|
||||
self.Clear = Clear
|
||||
self.IterateItems = IterateItems
|
||||
self.SetHideOnLeave = SetHideOnLeave
|
||||
|
||||
self.SetScroll = SetScroll
|
||||
self.MoveScroll = MoveScroll
|
||||
self.FixScroll = FixScroll
|
||||
|
||||
self.SetMaxHeight = SetMaxHeight
|
||||
self.GetRightBorderWidth = GetRightBorderWidth
|
||||
self.GetLeftBorderWidth = GetLeftBorderWidth
|
||||
|
||||
self.items = {}
|
||||
|
||||
self.scrollStatus = {
|
||||
scrollvalue = 0,
|
||||
}
|
||||
|
||||
self.maxHeight = defaultMaxHeight
|
||||
|
||||
frame:SetBackdrop(backdrop)
|
||||
frame:SetBackdropColor(0, 0, 0)
|
||||
frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
frame:SetClampedToScreen(true)
|
||||
frame:SetWidth(defaultWidth)
|
||||
frame:SetHeight(self.maxHeight)
|
||||
--frame:SetToplevel(true)
|
||||
|
||||
-- NOTE: The whole scroll frame code is copied from the AceGUI-3.0 widget ScrollFrame
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, frame)
|
||||
local itemFrame = CreateFrame("Frame", nil, scrollFrame)
|
||||
|
||||
self.scrollFrame = scrollFrame
|
||||
self.itemFrame = itemFrame
|
||||
|
||||
scrollFrame.obj = self
|
||||
itemFrame.obj = self
|
||||
|
||||
local slider = CreateFrame("Slider", "AceGUI30PulloutScrollbar"..count, scrollFrame)
|
||||
slider:SetOrientation("VERTICAL")
|
||||
slider:SetHitRectInsets(0, 0, -10, 0)
|
||||
slider:SetBackdrop(sliderBackdrop)
|
||||
slider:SetWidth(8)
|
||||
slider:SetThumbTexture("Interface\\Buttons\\UI-SliderBar-Button-Vertical")
|
||||
slider:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
self.slider = slider
|
||||
slider.obj = self
|
||||
|
||||
scrollFrame:SetScrollChild(itemFrame)
|
||||
scrollFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 6, -12)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -6, 12)
|
||||
scrollFrame:EnableMouseWheel(true)
|
||||
scrollFrame:SetScript("OnMouseWheel", OnMouseWheel)
|
||||
scrollFrame:SetScript("OnSizeChanged", OnSizeChanged)
|
||||
scrollFrame:SetToplevel(true)
|
||||
scrollFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
itemFrame:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
|
||||
itemFrame:SetPoint("TOPRIGHT", scrollFrame, "TOPRIGHT", -12, 0)
|
||||
itemFrame:SetHeight(400)
|
||||
itemFrame:SetToplevel(true)
|
||||
itemFrame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
|
||||
slider:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", -16, 0)
|
||||
slider:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", -16, 0)
|
||||
slider:SetScript("OnValueChanged", OnScrollValueChanged)
|
||||
slider:SetMinMaxValues(0, 1000)
|
||||
slider:SetValueStep(1)
|
||||
slider:SetValue(0)
|
||||
|
||||
scrollFrame:Show()
|
||||
itemFrame:Show()
|
||||
slider:Hide()
|
||||
|
||||
self:FixScroll()
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion)
|
||||
end
|
||||
|
||||
do
|
||||
local widgetType = "Dropdown"
|
||||
local widgetVersion = 22
|
||||
|
||||
--[[ Static data ]]--
|
||||
|
||||
--[[ UI event handler ]]--
|
||||
|
||||
local function Control_OnEnter(this)
|
||||
this.obj:Fire("OnEnter")
|
||||
end
|
||||
|
||||
local function Control_OnLeave(this)
|
||||
this.obj:Fire("OnLeave")
|
||||
end
|
||||
|
||||
local function Dropdown_OnHide(this)
|
||||
local self = this.obj
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
|
||||
local function Dropdown_TogglePullout(this)
|
||||
local self = this.obj
|
||||
PlaySound("igMainMenuOptionCheckBoxOn") -- missleading name, but the Blizzard code uses this sound
|
||||
if self.open then
|
||||
self.open = nil
|
||||
self.pullout:Close()
|
||||
AceGUI:ClearFocus()
|
||||
else
|
||||
self.open = true
|
||||
self.pullout:SetWidth(self.frame:GetWidth())
|
||||
self.pullout:Open("TOPLEFT", self.frame, "BOTTOMLEFT", 0, self.label:IsShown() and -2 or 0)
|
||||
AceGUI:SetFocus(self)
|
||||
end
|
||||
end
|
||||
|
||||
local function OnPulloutOpen(this)
|
||||
local self = this.userdata.obj
|
||||
local value = self.value
|
||||
|
||||
if not self.multiselect then
|
||||
for i, item in this:IterateItems() do
|
||||
item:SetValue(item.userdata.value == value)
|
||||
end
|
||||
end
|
||||
|
||||
self.open = true
|
||||
end
|
||||
|
||||
local function OnPulloutClose(this)
|
||||
local self = this.userdata.obj
|
||||
self.open = nil
|
||||
self:Fire("OnClosed")
|
||||
end
|
||||
|
||||
local function ShowMultiText(self)
|
||||
local text
|
||||
for i, widget in self.pullout:IterateItems() do
|
||||
if widget.type == "Dropdown-Item-Toggle" then
|
||||
if widget:GetValue() then
|
||||
if text then
|
||||
text = text..", "..widget:GetText()
|
||||
else
|
||||
text = widget:GetText()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
self:SetText(text)
|
||||
end
|
||||
|
||||
local function OnItemValueChanged(this, event, checked)
|
||||
local self = this.userdata.obj
|
||||
|
||||
if self.multiselect then
|
||||
self:Fire("OnValueChanged", this.userdata.value, checked)
|
||||
ShowMultiText(self)
|
||||
else
|
||||
if checked then
|
||||
self:SetValue(this.userdata.value)
|
||||
self:Fire("OnValueChanged", this.userdata.value)
|
||||
else
|
||||
this:SetValue(true)
|
||||
end
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Exported methods ]]--
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnAcquire(self)
|
||||
local pullout = AceGUI:Create("Dropdown-Pullout")
|
||||
self.pullout = pullout
|
||||
pullout.userdata.obj = self
|
||||
pullout:SetCallback("OnClose", OnPulloutClose)
|
||||
pullout:SetCallback("OnOpen", OnPulloutOpen)
|
||||
self.pullout.frame:SetFrameLevel(self.frame:GetFrameLevel() + 1)
|
||||
fixlevels(self.pullout.frame, self.pullout.frame:GetChildren())
|
||||
|
||||
self:SetHeight(44)
|
||||
self:SetWidth(200)
|
||||
end
|
||||
|
||||
-- exported, AceGUI callback
|
||||
local function OnRelease(self)
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
AceGUI:Release(self.pullout)
|
||||
self.pullout = nil
|
||||
|
||||
self:SetText("")
|
||||
self:SetLabel("")
|
||||
self:SetDisabled(false)
|
||||
self:SetMultiselect(false)
|
||||
|
||||
self.value = nil
|
||||
self.list = nil
|
||||
self.open = nil
|
||||
self.hasClose = nil
|
||||
|
||||
self.frame:ClearAllPoints()
|
||||
self.frame:Hide()
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetDisabled(self, disabled)
|
||||
self.disabled = disabled
|
||||
if disabled then
|
||||
self.text:SetTextColor(0.5,0.5,0.5)
|
||||
self.button:Disable()
|
||||
self.label:SetTextColor(0.5,0.5,0.5)
|
||||
else
|
||||
self.button:Enable()
|
||||
self.label:SetTextColor(1,.82,0)
|
||||
self.text:SetTextColor(1,1,1)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function ClearFocus(self)
|
||||
if self.open then
|
||||
self.pullout:Close()
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetText(self, text)
|
||||
self.text:SetText(text or "")
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetLabel(self, text)
|
||||
if text and text ~= "" then
|
||||
self.label:SetText(text)
|
||||
self.label:Show()
|
||||
self.dropdown:SetPoint("TOPLEFT",self.frame,"TOPLEFT",-15,-18)
|
||||
self.frame:SetHeight(44)
|
||||
else
|
||||
self.label:SetText("")
|
||||
self.label:Hide()
|
||||
self.dropdown:SetPoint("TOPLEFT",self.frame,"TOPLEFT",-15,0)
|
||||
self.frame:SetHeight(26)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetValue(self, value)
|
||||
if self.list then
|
||||
self:SetText(self.list[value] or "")
|
||||
end
|
||||
self.value = value
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetValue(self)
|
||||
return self.value
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetItemValue(self, item, value)
|
||||
if not self.multiselect then return end
|
||||
for i, widget in self.pullout:IterateItems() do
|
||||
if widget.userdata.value == item then
|
||||
if widget.SetValue then
|
||||
widget:SetValue(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
ShowMultiText(self)
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetItemDisabled(self, item, disabled)
|
||||
for i, widget in self.pullout:IterateItems() do
|
||||
if widget.userdata.value == item then
|
||||
widget:SetDisabled(disabled)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function AddListItem(self, value, text)
|
||||
local item = AceGUI:Create("Dropdown-Item-Toggle")
|
||||
item:SetText(text)
|
||||
item.userdata.obj = self
|
||||
item.userdata.value = value
|
||||
item:SetCallback("OnValueChanged", OnItemValueChanged)
|
||||
self.pullout:AddItem(item)
|
||||
end
|
||||
|
||||
local function AddCloseButton(self)
|
||||
if not self.hasClose then
|
||||
local close = AceGUI:Create("Dropdown-Item-Execute")
|
||||
close:SetText(CLOSE)
|
||||
self.pullout:AddItem(close)
|
||||
self.hasClose = true
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local sortlist = {}
|
||||
local function SetList(self, list)
|
||||
self.list = list
|
||||
self.pullout:Clear()
|
||||
self.hasClose = nil
|
||||
if not list then return end
|
||||
|
||||
for v in pairs(list) do
|
||||
sortlist[#sortlist + 1] = v
|
||||
end
|
||||
tsort(sortlist)
|
||||
|
||||
for i, value in pairs(sortlist) do
|
||||
AddListItem(self, value, list[value])
|
||||
sortlist[i] = nil
|
||||
end
|
||||
if self.multiselect then
|
||||
ShowMultiText(self)
|
||||
AddCloseButton(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function AddItem(self, value, text)
|
||||
if self.list then
|
||||
self.list[value] = text
|
||||
AddListItem(self, value, text)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function SetMultiselect(self, multi)
|
||||
self.multiselect = multi
|
||||
if multi then
|
||||
ShowMultiText(self)
|
||||
AddCloseButton(self)
|
||||
end
|
||||
end
|
||||
|
||||
-- exported
|
||||
local function GetMultiselect(self)
|
||||
return self.multiselect
|
||||
end
|
||||
|
||||
--[[ Constructor ]]--
|
||||
|
||||
local function Constructor()
|
||||
local count = AceGUI:GetNextWidgetNum(widgetType)
|
||||
local frame = CreateFrame("Frame", nil, UIParent)
|
||||
local dropdown = CreateFrame("Frame", "AceGUI30DropDown"..count, frame, "UIDropDownMenuTemplate")
|
||||
|
||||
local self = {}
|
||||
self.type = widgetType
|
||||
self.frame = frame
|
||||
self.dropdown = dropdown
|
||||
self.count = count
|
||||
frame.obj = self
|
||||
dropdown.obj = self
|
||||
|
||||
self.OnRelease = OnRelease
|
||||
self.OnAcquire = OnAcquire
|
||||
|
||||
self.ClearFocus = ClearFocus
|
||||
|
||||
self.SetText = SetText
|
||||
self.SetValue = SetValue
|
||||
self.GetValue = GetValue
|
||||
self.SetList = SetList
|
||||
self.SetLabel = SetLabel
|
||||
self.SetDisabled = SetDisabled
|
||||
self.AddItem = AddItem
|
||||
self.SetMultiselect = SetMultiselect
|
||||
self.GetMultiselect = GetMultiselect
|
||||
self.SetItemValue = SetItemValue
|
||||
self.SetItemDisabled = SetItemDisabled
|
||||
|
||||
self.alignoffset = 31
|
||||
|
||||
frame:SetHeight(44)
|
||||
frame:SetWidth(200)
|
||||
frame:SetScript("OnHide",Dropdown_OnHide)
|
||||
|
||||
dropdown:ClearAllPoints()
|
||||
dropdown:SetPoint("TOPLEFT",frame,"TOPLEFT",-15,0)
|
||||
dropdown:SetPoint("BOTTOMRIGHT",frame,"BOTTOMRIGHT",17,0)
|
||||
dropdown:SetScript("OnHide", nil)
|
||||
|
||||
local left = _G[dropdown:GetName() .. "Left"]
|
||||
local middle = _G[dropdown:GetName() .. "Middle"]
|
||||
local right = _G[dropdown:GetName() .. "Right"]
|
||||
|
||||
middle:ClearAllPoints()
|
||||
right:ClearAllPoints()
|
||||
|
||||
middle:SetPoint("LEFT", left, "RIGHT", 0, 0)
|
||||
middle:SetPoint("RIGHT", right, "LEFT", 0, 0)
|
||||
right:SetPoint("TOPRIGHT", dropdown, "TOPRIGHT", 0, 17)
|
||||
|
||||
local button = _G[dropdown:GetName() .. "Button"]
|
||||
self.button = button
|
||||
button.obj = self
|
||||
button:SetScript("OnEnter",Control_OnEnter)
|
||||
button:SetScript("OnLeave",Control_OnLeave)
|
||||
button:SetScript("OnClick",Dropdown_TogglePullout)
|
||||
|
||||
local text = _G[dropdown:GetName() .. "Text"]
|
||||
self.text = text
|
||||
text.obj = self
|
||||
text:ClearAllPoints()
|
||||
text:SetPoint("RIGHT", right, "RIGHT" ,-43, 2)
|
||||
text:SetPoint("LEFT", left, "LEFT", 25, 2)
|
||||
|
||||
local label = frame:CreateFontString(nil,"OVERLAY","GameFontNormalSmall")
|
||||
label:SetPoint("TOPLEFT",frame,"TOPLEFT",0,0)
|
||||
label:SetPoint("TOPRIGHT",frame,"TOPRIGHT",0,0)
|
||||
label:SetJustifyH("LEFT")
|
||||
label:SetHeight(18)
|
||||
label:Hide()
|
||||
self.label = label
|
||||
|
||||
AceGUI:RegisterAsWidget(self)
|
||||
return self
|
||||
end
|
||||
|
||||
AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion)
|
||||
end
|
||||
+178
@@ -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
|
||||
+235
@@ -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
|
||||
+308
@@ -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
|
||||
+76
@@ -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
|
||||
+149
@@ -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
|
||||
|
||||
+138
@@ -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
|
||||
+219
@@ -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
|
||||
|
||||
+230
@@ -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
|
||||
+158
@@ -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)
|
||||
+266
@@ -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)
|
||||
+241
@@ -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
|
||||
+99
@@ -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
|
||||
+284
@@ -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
|
||||
+389
@@ -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
|
||||
+746
@@ -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
|
||||
+331
@@ -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
Reference in New Issue
Block a user