Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85f80445d1 | |||
| 9fdb7ed718 | |||
| 698de84762 | |||
| cddaf2c515 |
@@ -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.
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
#include "core/Logger.hpp"
|
#include "core/Logger.hpp"
|
||||||
#include "core/Mem.hpp"
|
#include "core/Mem.hpp"
|
||||||
|
#include "runtime/LuaBindings.hpp"
|
||||||
#include "runtime/ModuleInstall.hpp"
|
#include "runtime/ModuleInstall.hpp"
|
||||||
|
#include "offsets/engine/Gx.hpp"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
@@ -20,6 +22,138 @@ namespace moonwell
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr uint32_t kTraitorFlag = 0x40000000u;
|
||||||
|
bool g_loginCharacterIsTraitor = false;
|
||||||
|
|
||||||
|
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
|
||||||
|
GxSetProjectionFn g_nextSetProjection = nullptr;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothstep avoids a visible jerk at both ends of the move.
|
||||||
|
t = t * t * (3.0f - 2.0f * t);
|
||||||
|
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
|
||||||
|
camera.verticalOffset = camera.startVerticalOffset
|
||||||
|
+ (camera.targetVerticalOffset - camera.startVerticalOffset) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __cdecl SetCharacterCreateCamera(void* state)
|
||||||
|
{
|
||||||
|
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
|
||||||
|
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
|
||||||
|
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
|
||||||
|
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
|
||||||
|
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
|
||||||
|
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
|
||||||
|
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
|
||||||
|
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
namespace gx = wxl::offsets::engine::gx;
|
||||||
|
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
|
||||||
|
void** slot = &vtable[gx::kGxSetProjectionSlot];
|
||||||
|
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
|
||||||
|
return;
|
||||||
|
|
||||||
|
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
|
||||||
|
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
|
||||||
|
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
|
||||||
|
{
|
||||||
|
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::runtime::lua::IsNumber(state, 1))
|
||||||
|
flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));
|
||||||
|
|
||||||
|
g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
|
||||||
|
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __cdecl IsTraitor(void* state)
|
||||||
|
{
|
||||||
|
wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
struct Patch
|
struct Patch
|
||||||
{
|
{
|
||||||
const char* name;
|
const char* name;
|
||||||
@@ -196,7 +330,14 @@ namespace moonwell
|
|||||||
{
|
{
|
||||||
Registration()
|
Registration()
|
||||||
{
|
{
|
||||||
|
wxl::runtime::lua::RegisterFunction(
|
||||||
|
"MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
|
||||||
|
wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
|
||||||
|
wxl::runtime::lua::RegisterFunction(
|
||||||
|
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
|
||||||
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
|
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
|
||||||
|
wxl::runtime::modules::Register(
|
||||||
|
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
|
||||||
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
|
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
|
||||||
}
|
}
|
||||||
} g_registration;
|
} g_registration;
|
||||||
|
|||||||
@@ -676,6 +676,44 @@ function CharacterCreate_UpdateGameModeVisibility()
|
|||||||
end
|
end
|
||||||
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
|
||||||
|
|
||||||
local function CharacterCreate_GetEnteredName()
|
local function CharacterCreate_GetEnteredName()
|
||||||
local text = "";
|
local text = "";
|
||||||
|
|
||||||
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
|
|||||||
CharacterCreate.gameModeSelectionMode = false;
|
CharacterCreate.gameModeSelectionMode = false;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
|
|
||||||
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
||||||
|
|
||||||
if showGameModes then
|
if showGameModes then
|
||||||
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
|
|||||||
CharCreatePersonalizeButton:Show();
|
CharCreatePersonalizeButton:Show();
|
||||||
CharCreateOkayButton:Hide();
|
CharCreateOkayButton:Hide();
|
||||||
CharacterCreate_UpdateGameModeVisibility();
|
CharacterCreate_UpdateGameModeVisibility();
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
|
|
||||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||||
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
|
|||||||
CharacterChangeFixup();
|
CharacterChangeFixup();
|
||||||
CharacterCreate_CreateGenderButtonTextures();
|
CharacterCreate_CreateGenderButtonTextures();
|
||||||
CharacterCreate_UpdateButtonCheckedStates();
|
CharacterCreate_UpdateButtonCheckedStates();
|
||||||
CharacterCreate_ResetState();
|
CharacterCreate_ResetState(true);
|
||||||
|
|
||||||
hideScheduled = true
|
hideScheduled = true
|
||||||
HideNameEditFrame:Show()
|
HideNameEditFrame:Show()
|
||||||
@@ -1013,7 +1054,7 @@ end
|
|||||||
function CharacterCreate_OnHide()
|
function CharacterCreate_OnHide()
|
||||||
PAID_SERVICE_CHARACTER_ID = nil;
|
PAID_SERVICE_CHARACTER_ID = nil;
|
||||||
PAID_SERVICE_TYPE = nil;
|
PAID_SERVICE_TYPE = nil;
|
||||||
CharacterCreate_ResetState();
|
CharacterCreate_ResetState(true);
|
||||||
|
|
||||||
for button, tooltip in pairs(raceTooltips) do
|
for button, tooltip in pairs(raceTooltips) do
|
||||||
if tooltip then tooltip:Hide() end
|
if tooltip then tooltip:Hide() end
|
||||||
@@ -1518,6 +1559,7 @@ end
|
|||||||
|
|
||||||
function CharacterCreate_UpdateModel(self)
|
function CharacterCreate_UpdateModel(self)
|
||||||
UpdateCustomizationScene();
|
UpdateCustomizationScene();
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
self:AdvanceTime();
|
self:AdvanceTime();
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
|
|||||||
CharacterCreate_UpdateButtonCheckedStates();
|
CharacterCreate_UpdateButtonCheckedStates();
|
||||||
end
|
end
|
||||||
|
|
||||||
function CharacterCreate_ResetState()
|
function CharacterCreate_ResetState(immediate)
|
||||||
CharacterCreate.personalizationMode = false;
|
CharacterCreate.personalizationMode = false;
|
||||||
CharacterCreate.gameModeSelectionMode = false;
|
CharacterCreate.gameModeSelectionMode = false;
|
||||||
|
CharacterCreate_UpdateCamera(immediate);
|
||||||
CharacterCreateRaceButtonsContainer:Show();
|
CharacterCreateRaceButtonsContainer:Show();
|
||||||
CharacterCreateClassButtonsContainer:Show();
|
CharacterCreateClassButtonsContainer:Show();
|
||||||
CharacterCreateGenderButtonsContainer:Show();
|
CharacterCreateGenderButtonsContainer:Show();
|
||||||
|
|||||||
@@ -1139,6 +1139,12 @@ end
|
|||||||
function CharacterSelect_EnterWorld()
|
function CharacterSelect_EnterWorld()
|
||||||
PlaySound("gsCharacterSelectionEnterWorld");
|
PlaySound("gsCharacterSelectionEnterWorld");
|
||||||
StopGlueAmbience();
|
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();
|
EnterWorld();
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
-- The MoonWell Achievement.dbc intentionally exposes both faction variants.
|
||||||
|
-- Restore the 3.3.5a client-side faction filter and make traitors use the
|
||||||
|
-- opposite faction. IDs come from the stock ruRU build 12340 DBC.
|
||||||
|
|
||||||
|
local achievementFaction = {}
|
||||||
|
|
||||||
|
local function AddFaction(ids, faction)
|
||||||
|
for id in string.gmatch(ids, "%d+") do
|
||||||
|
achievementFaction[tonumber(id)] = faction
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
AddFaction([[
|
||||||
|
33 34 35 37 41 58 202 203 206 220 225 230 246 388 433 434 435 436 437 438
|
||||||
|
439 440 441 442 470 471 472 473 604 610 611 612 613 614 701 707 709 711 713
|
||||||
|
764 899 907 908 942 948 963 966 969 970 1012 1022 1023 1024 1028 1029 1030
|
||||||
|
1034 1035 1038 1040 1151 1167 1169 1172 1184 1189 1191 1192 1255 1262 1279
|
||||||
|
1466 1563 1656 1676 1678 1681 1684 1686 1692 1697 1707 1737 1752 1757 1762
|
||||||
|
1782 2016 2144 2194 2419 2421 2536 2760 2761 2762 2763 2764 2770 2777 2778
|
||||||
|
2779 2780 2781 2782 2797 2817 3356 3478 3556 3576 3580 3596 3676 3846 3851
|
||||||
|
3856 3857 4156 4296 4298 4436 4784 4786
|
||||||
|
]], "Alliance")
|
||||||
|
|
||||||
|
AddFaction([[
|
||||||
|
224 259 443 444 445 446 447 448 449 450 451 452 453 454 468 469 593 603 615
|
||||||
|
616 617 618 619 700 706 708 710 712 714 762 763 873 901 909 926 943 965 967
|
||||||
|
968 971 1005 1006 1011 1025 1026 1027 1031 1032 1033 1036 1037 1039 1041
|
||||||
|
1164 1168 1170 1173 1175 1203 1251 1252 1271 1272 1273 1274 1280 1356 1357
|
||||||
|
1358 1359 1360 1502 1657 1677 1680 1682 1683 1685 1691 1693 1698 1783 1784
|
||||||
|
2017 2145 2192 2195 2200 2420 2476 2497 2537 2765 2766 2767 2768 2769 2771
|
||||||
|
2776 2783 2784 2785 2786 2787 2788 2798 2816 3357 3557 3577 3581 3597 3656
|
||||||
|
3677 3778 3957 4079 4176 4177 4256 4297 4437 4785 4790
|
||||||
|
]], "Horde")
|
||||||
|
|
||||||
|
local NativeGetAchievementInfo = GetAchievementInfo
|
||||||
|
local NativeGetCategoryNumAchievements = GetCategoryNumAchievements
|
||||||
|
local NativeGetLatestCompletedAchievements = GetLatestCompletedAchievements
|
||||||
|
local NativeGetTrackedAchievements = GetTrackedAchievements
|
||||||
|
local NativeGetNumCompletedAchievements = GetNumCompletedAchievements
|
||||||
|
local NativeGetTotalAchievementPoints = GetTotalAchievementPoints
|
||||||
|
|
||||||
|
local categoryCache = {}
|
||||||
|
local summaryCache = {}
|
||||||
|
|
||||||
|
local function GetDisplayFaction()
|
||||||
|
local faction = UnitFactionGroup("player")
|
||||||
|
if MoonWell_IsTraitor then
|
||||||
|
if faction == "Alliance" then
|
||||||
|
return "Horde"
|
||||||
|
elseif faction == "Horde" then
|
||||||
|
return "Alliance"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return faction
|
||||||
|
end
|
||||||
|
|
||||||
|
local function IsAchievementVisible(id, faction)
|
||||||
|
local requiredFaction = achievementFaction[id]
|
||||||
|
return not requiredFaction or not faction or requiredFaction == faction
|
||||||
|
end
|
||||||
|
|
||||||
|
local function BuildCategory(categoryID, showAll)
|
||||||
|
local faction = GetDisplayFaction()
|
||||||
|
local key = tostring(categoryID) .. ":" .. tostring(showAll and 1 or 0)
|
||||||
|
.. ":" .. tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
|
||||||
|
local cached = categoryCache[key]
|
||||||
|
if cached then
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
local nativeCount = NativeGetCategoryNumAchievements(categoryID, showAll)
|
||||||
|
local indices = {}
|
||||||
|
local completed = 0
|
||||||
|
|
||||||
|
for nativeIndex = 1, nativeCount do
|
||||||
|
local id, _, _, isCompleted = NativeGetAchievementInfo(categoryID, nativeIndex)
|
||||||
|
if id and IsAchievementVisible(id, faction) then
|
||||||
|
indices[#indices + 1] = nativeIndex
|
||||||
|
if isCompleted then
|
||||||
|
completed = completed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cached = { indices = indices, completed = completed }
|
||||||
|
categoryCache[key] = cached
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetCategoryNumAchievements(categoryID, showAll)
|
||||||
|
local category = BuildCategory(categoryID, showAll)
|
||||||
|
return #category.indices, category.completed
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetAchievementInfo(idOrCategory, achievementIndex)
|
||||||
|
if achievementIndex == nil then
|
||||||
|
return NativeGetAchievementInfo(idOrCategory)
|
||||||
|
end
|
||||||
|
|
||||||
|
local category = BuildCategory(idOrCategory, false)
|
||||||
|
local nativeIndex = category.indices[achievementIndex]
|
||||||
|
if not nativeIndex then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return NativeGetAchievementInfo(idOrCategory, nativeIndex)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function BuildSummary()
|
||||||
|
local faction = GetDisplayFaction()
|
||||||
|
local key = tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
|
||||||
|
local cached = summaryCache[key]
|
||||||
|
if cached then
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
local total, completed = NativeGetNumCompletedAchievements()
|
||||||
|
local points = NativeGetTotalAchievementPoints()
|
||||||
|
local statisticFlag = ACHIEVEMENT_FLAGS_STATISTIC or 0x00000001
|
||||||
|
local hiddenFlag = ACHIEVEMENT_FLAGS_HIDDEN or 0x00000002
|
||||||
|
|
||||||
|
for id, requiredFaction in pairs(achievementFaction) do
|
||||||
|
if faction and requiredFaction ~= faction then
|
||||||
|
local _, _, achievementPoints, isCompleted, _, _, _, _, flags =
|
||||||
|
NativeGetAchievementInfo(id)
|
||||||
|
if flags and bit.band(flags, statisticFlag) == 0 then
|
||||||
|
local isHidden = bit.band(flags, hiddenFlag) ~= 0
|
||||||
|
if not isHidden or isCompleted then
|
||||||
|
total = total - 1
|
||||||
|
end
|
||||||
|
if isCompleted then
|
||||||
|
completed = completed - 1
|
||||||
|
points = points - (achievementPoints or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cached = { total = total, completed = completed, points = points }
|
||||||
|
summaryCache[key] = cached
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetNumCompletedAchievements()
|
||||||
|
local summary = BuildSummary()
|
||||||
|
return summary.total, summary.completed
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetTotalAchievementPoints()
|
||||||
|
return BuildSummary().points
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FilterAchievementIDs(getter)
|
||||||
|
local faction = GetDisplayFaction()
|
||||||
|
local source = { getter() }
|
||||||
|
local filtered = {}
|
||||||
|
for i = 1, #source do
|
||||||
|
if IsAchievementVisible(source[i], faction) then
|
||||||
|
filtered[#filtered + 1] = source[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return unpack(filtered)
|
||||||
|
end
|
||||||
|
|
||||||
|
if NativeGetLatestCompletedAchievements then
|
||||||
|
function GetLatestCompletedAchievements()
|
||||||
|
return FilterAchievementIDs(NativeGetLatestCompletedAchievements)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if NativeGetTrackedAchievements then
|
||||||
|
function GetTrackedAchievements()
|
||||||
|
return FilterAchievementIDs(NativeGetTrackedAchievements)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function MoonWellAchievementFaction_Refresh()
|
||||||
|
categoryCache = {}
|
||||||
|
summaryCache = {}
|
||||||
|
if AchievementFrame and AchievementFrame:IsShown() then
|
||||||
|
if AchievementFrameCategories_Update then
|
||||||
|
AchievementFrameCategories_Update()
|
||||||
|
end
|
||||||
|
if AchievementFrameAchievements_ForceUpdate then
|
||||||
|
AchievementFrameAchievements_ForceUpdate()
|
||||||
|
elseif AchievementFrameAchievements_Update then
|
||||||
|
AchievementFrameAchievements_Update()
|
||||||
|
end
|
||||||
|
if AchievementFrameSummary_Update then
|
||||||
|
AchievementFrameSummary_Update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local events = CreateFrame("Frame")
|
||||||
|
events:RegisterEvent("ACHIEVEMENT_EARNED")
|
||||||
|
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
events:RegisterEvent("UNIT_FACTION")
|
||||||
|
events:SetScript("OnEvent", function(self, event, unit)
|
||||||
|
if event ~= "UNIT_FACTION" or unit == "player" then
|
||||||
|
MoonWellAchievementFaction_Refresh()
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -1,62 +1,85 @@
|
|||||||
-- ============================================================================
|
-- MoonWell client game-mode integration.
|
||||||
-- MoonWell Client
|
-- WarcraftXL retains the selected character's server-provided charFlags while
|
||||||
-- Server integration: game modes, traitor faction display
|
-- WoW replaces the GlueXML Lua state with the in-world FrameXML state.
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
-- Global flag read by FrameXML files to decide which faction icon to show.
|
|
||||||
-- true = show opposite faction icon (traitor)
|
|
||||||
-- false = show real faction icon (normal)
|
|
||||||
MoonWell_IsTraitor = false
|
MoonWell_IsTraitor = false
|
||||||
|
|
||||||
-- ============================================================================
|
local function RefreshFactionIcons()
|
||||||
-- AIO handler — called by server on login
|
|
||||||
-- Server: AIO:Handle(player, "MoonWellClient", "Init", gameMode)
|
|
||||||
-- gameMode: 0 = normal, 1 = traitor
|
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
if not AIO then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local Handler = AIO.AddHandlers("MoonWellClient", {})
|
|
||||||
|
|
||||||
local function MoonWell_RefreshFactionIcons()
|
|
||||||
-- Portrait PvP icon
|
|
||||||
if PlayerFrame_UpdatePvPStatus then
|
if PlayerFrame_UpdatePvPStatus then
|
||||||
PlayerFrame_UpdatePvPStatus()
|
PlayerFrame_UpdatePvPStatus()
|
||||||
end
|
end
|
||||||
-- DragonUI PvP micro button: SetupPVPButton() runs on load before AIO responds,
|
|
||||||
-- so we refresh its DragonUIPVPIcon texture directly after Init.
|
if PVPFrame and PVPFrame:IsShown() and PVPFrame_Update then
|
||||||
|
PVPFrame_Update()
|
||||||
|
end
|
||||||
|
if BattlefieldFrame and BattlefieldFrame:IsShown() and BattlefieldFrame_Update then
|
||||||
|
BattlefieldFrame_Update()
|
||||||
|
end
|
||||||
|
|
||||||
if PVPMicroButton and PVPMicroButton.DragonUIPVPIcon then
|
if PVPMicroButton and PVPMicroButton.DragonUIPVPIcon then
|
||||||
local factionGroup = UnitFactionGroup("player")
|
local factionGroup = UnitFactionGroup("player")
|
||||||
if factionGroup then
|
if factionGroup then
|
||||||
local displayFaction = MoonWell_IsTraitor
|
local displayFaction = MoonWell_IsTraitor
|
||||||
and (factionGroup == "Alliance" and "Horde" or "Alliance")
|
and (factionGroup == "Alliance" and "Horde" or "Alliance")
|
||||||
or factionGroup
|
or factionGroup
|
||||||
local microTexture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
|
local texture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
|
||||||
local icon = PVPMicroButton.DragonUIPVPIcon
|
local icon = PVPMicroButton.DragonUIPVPIcon
|
||||||
|
local highlight = PVPMicroButton:GetHighlightTexture()
|
||||||
|
|
||||||
if displayFaction == "Alliance" then
|
if displayFaction == "Alliance" then
|
||||||
icon:SetTexture(microTexture)
|
icon:SetTexture(texture)
|
||||||
icon:SetTexCoord(0, 118/256, 0, 151/256)
|
icon:SetTexCoord(0, 118/256, 0, 151/256)
|
||||||
elseif displayFaction == "Horde" then
|
if highlight then
|
||||||
icon:SetTexture(microTexture)
|
highlight:SetTexture(texture)
|
||||||
icon:SetTexCoord(118/256, 236/256, 0, 151/256)
|
highlight:SetTexCoord(0, 118/256, 0, 151/256)
|
||||||
end
|
end
|
||||||
local hl = PVPMicroButton:GetHighlightTexture()
|
|
||||||
if hl then
|
|
||||||
if displayFaction == "Alliance" then
|
|
||||||
hl:SetTexture(microTexture)
|
|
||||||
hl:SetTexCoord(0, 118/256, 0, 151/256)
|
|
||||||
elseif displayFaction == "Horde" then
|
elseif displayFaction == "Horde" then
|
||||||
hl:SetTexture(microTexture)
|
icon:SetTexture(texture)
|
||||||
hl:SetTexCoord(118/256, 236/256, 0, 151/256)
|
icon:SetTexCoord(118/256, 236/256, 0, 151/256)
|
||||||
|
if highlight then
|
||||||
|
highlight:SetTexture(texture)
|
||||||
|
highlight:SetTexCoord(118/256, 236/256, 0, 151/256)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Handler.Init(_, mode)
|
local function ReadNativeGameMode()
|
||||||
MoonWell_IsTraitor = (tonumber(mode) == 1)
|
if type(MoonWellIsTraitor) ~= "function" then
|
||||||
MoonWell_RefreshFactionIcons()
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local wasTraitor = MoonWell_IsTraitor
|
||||||
|
MoonWell_IsTraitor = MoonWellIsTraitor() and true or false
|
||||||
|
if wasTraitor ~= MoonWell_IsTraitor and MoonWellAchievementFaction_Refresh then
|
||||||
|
MoonWellAchievementFaction_Refresh()
|
||||||
|
end
|
||||||
|
RefreshFactionIcons()
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local events = CreateFrame("Frame")
|
||||||
|
events:RegisterEvent("PLAYER_LOGIN")
|
||||||
|
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
events:RegisterEvent("UNIT_FACTION")
|
||||||
|
events:SetScript("OnEvent", function(self, event, unit)
|
||||||
|
if event ~= "UNIT_FACTION" or unit == "player" then
|
||||||
|
ReadNativeGameMode()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- WarcraftXL may publish its Lua globals shortly after addons are loaded.
|
||||||
|
-- Lua 5.1/WotLK has no C_Timer, so retry cheaply until the native API appears.
|
||||||
|
local elapsedTotal = 0
|
||||||
|
events:SetScript("OnUpdate", function(self, elapsed)
|
||||||
|
elapsedTotal = elapsedTotal + elapsed
|
||||||
|
if elapsedTotal < 0.25 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
elapsedTotal = 0
|
||||||
|
|
||||||
|
if ReadNativeGameMode() then
|
||||||
|
self:SetScript("OnUpdate", nil)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
## Interface: 30300
|
## Interface: 30300
|
||||||
## Title: MoonWell Client
|
## Title: MoonWell Client
|
||||||
## Notes: MoonWell server integration — game modes, traitor faction display
|
## Notes: Native WarcraftXL game-mode and traitor faction display
|
||||||
## Version: 1.0.0
|
## Version: 2.0.0
|
||||||
## Author: MoonWell
|
## Author: MoonWell
|
||||||
## SavedVariables: MoonWellClientDB
|
## SavedVariables: MoonWellClientDB
|
||||||
## Dependencies: AIO_Client
|
|
||||||
## DefaultState: Enabled
|
## DefaultState: Enabled
|
||||||
## X-Hidden: 1
|
## X-Hidden: 1
|
||||||
|
|
||||||
|
AchievementFactions.lua
|
||||||
MoonWellClient.lua
|
MoonWellClient.lua
|
||||||
|
|||||||
Reference in New Issue
Block a user