Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85f80445d1 | |||
| 9fdb7ed718 | |||
| 698de84762 | |||
| cddaf2c515 | |||
| 26ff2b3b94 | |||
| 08c1b9da3f |
+1
-1
@@ -5,4 +5,4 @@ AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=
|
||||
AWS_BUCKET=
|
||||
AWS_ENDPOINT=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||
|
||||
+5
-1
@@ -3,4 +3,8 @@ __pycache__/
|
||||
manifest.json
|
||||
.claude
|
||||
.vscode
|
||||
dist
|
||||
dist
|
||||
build/
|
||||
Wow*.exe
|
||||
*.backup.exe
|
||||
Logs/
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[submodule "vendor/warcraftxl"]
|
||||
path = vendor/warcraftxl
|
||||
url = https://github.com/WarcraftXL/wxl-core.git
|
||||
[submodule "vendor/modules/wxl-modern-assets"]
|
||||
path = vendor/modules/wxl-modern-assets
|
||||
url = https://github.com/WarcraftXL/wxl-modern-assets.git
|
||||
[submodule "vendor/modules/wxl-modern-render"]
|
||||
path = vendor/modules/wxl-modern-render
|
||||
url = https://github.com/WarcraftXL/wxl-modern-render.git
|
||||
[submodule "vendor/modules/wxl-unit-outline"]
|
||||
path = vendor/modules/wxl-unit-outline
|
||||
url = https://github.com/WarcraftXL/wxl-unit-outline.git
|
||||
[submodule "vendor/modules/wxl-modern-adt"]
|
||||
path = vendor/modules/wxl-modern-adt
|
||||
url = https://github.com/WarcraftXL/wxl-modern-adt.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,71 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(MoonWellWarcraftXL LANGUAGES CXX)
|
||||
|
||||
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
|
||||
# its aggregate DLL target here, so updating the framework does not mix our
|
||||
# client-specific offsets and policy into the upstream checkout.
|
||||
add_subdirectory(vendor/warcraftxl)
|
||||
|
||||
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
|
||||
|
||||
function(wxl_collect_sources output)
|
||||
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
|
||||
set(${output} "${collected}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
if(TARGET WarcraftXL)
|
||||
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
|
||||
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||
endif()
|
||||
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
|
||||
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
|
||||
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||
|
||||
target_sources(WarcraftXL PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
|
||||
${WXL_MODERN_ASSETS_RUNTIME}
|
||||
${WXL_MODERN_RENDER_RUNTIME}
|
||||
${WXL_UNIT_OUTLINE_RUNTIME}
|
||||
${WXL_MODERN_ADT_RUNTIME})
|
||||
|
||||
# wxl-modern-render needs its module-root includes and D3D12 import library.
|
||||
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
|
||||
endif()
|
||||
|
||||
if(TARGET WarcraftXLHost)
|
||||
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
|
||||
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||
endif()
|
||||
wxl_collect_sources(WXL_MODERN_ADT_HOST
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
|
||||
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||
|
||||
target_sources(WarcraftXLHost PRIVATE
|
||||
${WXL_MODERN_ASSETS_HOST}
|
||||
${WXL_MODERN_ADT_HOST})
|
||||
endif()
|
||||
|
||||
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
|
||||
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
|
||||
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.
|
||||
@@ -1,90 +1,143 @@
|
||||
# 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): `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 намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому
|
||||
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает
|
||||
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
|
||||
|
||||
### Внутриигровой магазин
|
||||
## Структура
|
||||
|
||||
Файлы магазина находятся в [`patch-4/Interface/Store_UI`](./patch-4/Interface/Store_UI).
|
||||
```text
|
||||
modules/moonwell/ MoonWell runtime-модуль WarcraftXL
|
||||
vendor/warcraftxl/ закреплённый upstream git submodule
|
||||
vendor/modules/ закреплённые модули WarcraftXL
|
||||
src/Data/ исходники MPQ-патчей
|
||||
tool/ сборщик MPQ
|
||||
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host
|
||||
run.ps1 полная сборка, установка и запуск клиента
|
||||
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
|
||||
```
|
||||
|
||||
Примеры ресурсов:
|
||||
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и
|
||||
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule.
|
||||
Для обновления всех зависимостей 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 и x64;
|
||||
- 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, D3D9 proxy и x64 Host,
|
||||
обновляет `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-данные, связанные с отображением предметов и интерфейсом
|
||||
# Аварийный native D3D9 вместо D3D9On12
|
||||
.\deploy.ps1 -NativeRenderer
|
||||
```
|
||||
|
||||
## Как воспринимать содержимое репозитория
|
||||
Без установки в клиент:
|
||||
|
||||
- Каталоги `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`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`;
|
||||
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`;
|
||||
6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
|
||||
|
||||
Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом
|
||||
модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и
|
||||
установить native-переходник без modern-render:
|
||||
|
||||
```powershell
|
||||
.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer `
|
||||
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
|
||||
```
|
||||
|
||||
Резервная 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` и
|
||||
`Utils\WarcraftXLHost.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-модуль,
|
||||
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным
|
||||
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`.
|
||||
|
||||
@@ -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"` | Добавить атрибут |
|
||||
@@ -0,0 +1,106 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Debug', 'Release')]
|
||||
[string]$Configuration = 'Release',
|
||||
[string]$ClientPath = '',
|
||||
[string]$PackagePath = '',
|
||||
[switch]$Deploy,
|
||||
[switch]$NativeRenderer
|
||||
)
|
||||
|
||||
$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'
|
||||
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
|
||||
$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 --target WarcraftXL d3d9 MoonWellLoader
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
|
||||
|
||||
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
|
||||
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
|
||||
|
||||
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
|
||||
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
|
||||
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
|
||||
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
|
||||
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
|
||||
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
|
||||
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
|
||||
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
|
||||
|
||||
function Install-WarcraftXLArtifacts {
|
||||
param([Parameter(Mandatory)][string]$Destination)
|
||||
|
||||
$utils = Join-Path $Destination 'Utils'
|
||||
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
|
||||
New-Item -ItemType Directory -Path $Destination, $utils, $loosePatch -Force | Out-Null
|
||||
|
||||
Copy-Item -LiteralPath $stockExe -Destination (Join-Path $Destination 'Wow.exe') -Force
|
||||
Copy-Item -LiteralPath $warcraftXL -Destination (Join-Path $Destination 'WarcraftXL.dll') -Force
|
||||
Copy-Item -LiteralPath $selectedProxy -Destination (Join-Path $Destination 'd3d9.dll') -Force
|
||||
Copy-Item -LiteralPath $nativeProxy -Destination (Join-Path $utils 'd3d9-native.dll') -Force
|
||||
Copy-Item -LiteralPath $hostExe -Destination (Join-Path $utils 'WarcraftXLHost.exe') -Force
|
||||
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
|
||||
Copy-Item -LiteralPath $_.FullName -Destination $loosePatch -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
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 runtime, host and modules 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"
|
||||
}
|
||||
|
||||
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
|
||||
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
|
||||
@@ -21,6 +21,10 @@ IGNORED_DIRS_ANYWHERE = {
|
||||
".git",
|
||||
".moonwell_launcher",
|
||||
}
|
||||
IGNORED_TOP_LEVEL_FILES = {
|
||||
"Wow.moonwell-patched.backup.exe",
|
||||
"WarcraftXL_on12.disable",
|
||||
}
|
||||
|
||||
ENV_VAR_NAME = "WOW_HOME"
|
||||
|
||||
@@ -42,6 +46,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])
|
||||
|
||||
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
[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
|
||||
Get-Process WarcraftXLHost -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
} | Stop-Process -Force
|
||||
}
|
||||
|
||||
$env:WOW_HOME = $ClientPath
|
||||
Write-Host "MoonWell client: $ClientPath"
|
||||
Write-Host "Package staging: $PackagePath"
|
||||
|
||||
Write-Host '[1/5] Initializing WarcraftXL dependencies...'
|
||||
& git -C $repoRoot submodule update --init --recursive
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Git submodule initialization failed.' }
|
||||
|
||||
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'
|
||||
if (-not (Test-Path -LiteralPath $tool -PathType Leaf)) {
|
||||
$cargo = Get-Command cargo -ErrorAction SilentlyContinue
|
||||
if (-not $cargo) { throw 'Cargo was not found and the MPQ builder is not compiled.' }
|
||||
& $cargo.Source build --release --manifest-path (Join-Path $repoRoot 'tool\Cargo.toml')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'MPQ builder compilation failed.' }
|
||||
}
|
||||
|
||||
& $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 = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12' }
|
||||
Write-Host ''
|
||||
Write-Host 'Deployment complete.'
|
||||
Write-Host " Renderer: $renderer"
|
||||
Write-Host " Wow.exe SHA-256: $wowHash"
|
||||
Write-Host " Host: $(Join-Path $ClientPath 'Utils\WarcraftXLHost.exe')"
|
||||
|
||||
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,65 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Compatibility exports referenced by the current WarcraftXL core. MoonWell
|
||||
// does not enable its D3D9On12 backend, so these intentionally report no D3D12
|
||||
// device and a neutral supersampling factor.
|
||||
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,345 @@
|
||||
// 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 "core/Logger.hpp"
|
||||
#include "core/Mem.hpp"
|
||||
#include "runtime/LuaBindings.hpp"
|
||||
#include "runtime/ModuleInstall.hpp"
|
||||
#include "offsets/engine/Gx.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace moonwell
|
||||
{
|
||||
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
|
||||
{
|
||||
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 (!wxl::core::mem::Patch(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 (!wxl::core::mem::Patch(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)");
|
||||
wxl::core::log::Flush();
|
||||
}
|
||||
|
||||
DWORD WINAPI FlushRuntimeLog(LPVOID)
|
||||
{
|
||||
// Let the remainder of RunAll() and the core-ready line reach the
|
||||
// shared buffered logger, then make startup diagnostics durable.
|
||||
Sleep(1000);
|
||||
wxl::core::log::Flush();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InstallRuntimeLogFlush()
|
||||
{
|
||||
HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
|
||||
if (thread) CloseHandle(thread);
|
||||
}
|
||||
|
||||
struct 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::Register(
|
||||
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
|
||||
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
|
||||
}
|
||||
} g_registration;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ $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"
|
||||
|
||||
# --- Ensure tool exists
|
||||
if (!(Test-Path $TOOL)) {
|
||||
@@ -59,6 +60,16 @@ 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.
|
||||
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
|
||||
|
||||
@@ -676,6 +676,44 @@ 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
|
||||
|
||||
local function CharacterCreate_GetEnteredName()
|
||||
local text = "";
|
||||
|
||||
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
|
||||
CharacterCreate.gameModeSelectionMode = false;
|
||||
end
|
||||
|
||||
CharacterCreate_UpdateCamera();
|
||||
|
||||
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
||||
|
||||
if showGameModes then
|
||||
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
|
||||
CharCreatePersonalizeButton:Show();
|
||||
CharCreateOkayButton:Hide();
|
||||
CharacterCreate_UpdateGameModeVisibility();
|
||||
CharacterCreate_UpdateCamera();
|
||||
|
||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
|
||||
CharacterChangeFixup();
|
||||
CharacterCreate_CreateGenderButtonTextures();
|
||||
CharacterCreate_UpdateButtonCheckedStates();
|
||||
CharacterCreate_ResetState();
|
||||
CharacterCreate_ResetState(true);
|
||||
|
||||
hideScheduled = true
|
||||
HideNameEditFrame:Show()
|
||||
@@ -1013,7 +1054,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
|
||||
@@ -1518,6 +1559,7 @@ end
|
||||
|
||||
function CharacterCreate_UpdateModel(self)
|
||||
UpdateCustomizationScene();
|
||||
CharacterCreate_UpdateCamera();
|
||||
self:AdvanceTime();
|
||||
end
|
||||
|
||||
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
|
||||
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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
-- Server integration: game modes, traitor faction display
|
||||
-- ============================================================================
|
||||
-- MoonWell client game-mode integration.
|
||||
-- WarcraftXL retains the selected character's server-provided charFlags while
|
||||
-- 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
|
||||
|
||||
-- ============================================================================
|
||||
-- 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
|
||||
local function RefreshFactionIcons()
|
||||
if PlayerFrame_UpdatePvPStatus then
|
||||
PlayerFrame_UpdatePvPStatus()
|
||||
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
|
||||
local factionGroup = UnitFactionGroup("player")
|
||||
if factionGroup then
|
||||
local displayFaction = MoonWell_IsTraitor
|
||||
and (factionGroup == "Alliance" and "Horde" or "Alliance")
|
||||
or factionGroup
|
||||
local microTexture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
|
||||
local texture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
|
||||
local icon = PVPMicroButton.DragonUIPVPIcon
|
||||
local highlight = PVPMicroButton:GetHighlightTexture()
|
||||
|
||||
if displayFaction == "Alliance" then
|
||||
icon:SetTexture(microTexture)
|
||||
icon:SetTexture(texture)
|
||||
icon:SetTexCoord(0, 118/256, 0, 151/256)
|
||||
if highlight then
|
||||
highlight:SetTexture(texture)
|
||||
highlight:SetTexCoord(0, 118/256, 0, 151/256)
|
||||
end
|
||||
elseif displayFaction == "Horde" then
|
||||
icon:SetTexture(microTexture)
|
||||
icon:SetTexture(texture)
|
||||
icon:SetTexCoord(118/256, 236/256, 0, 151/256)
|
||||
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
|
||||
hl:SetTexture(microTexture)
|
||||
hl: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
|
||||
|
||||
function Handler.Init(_, mode)
|
||||
MoonWell_IsTraitor = (tonumber(mode) == 1)
|
||||
MoonWell_RefreshFactionIcons()
|
||||
local function ReadNativeGameMode()
|
||||
if type(MoonWellIsTraitor) ~= "function" then
|
||||
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
|
||||
|
||||
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
|
||||
## Title: MoonWell Client
|
||||
## Notes: MoonWell server integration — game modes, traitor faction display
|
||||
## Version: 1.0.0
|
||||
## Notes: Native WarcraftXL game-mode and traitor faction display
|
||||
## Version: 2.0.0
|
||||
## Author: MoonWell
|
||||
## SavedVariables: MoonWellClientDB
|
||||
## Dependencies: AIO_Client
|
||||
## DefaultState: Enabled
|
||||
## X-Hidden: 1
|
||||
|
||||
AchievementFactions.lua
|
||||
MoonWellClient.lua
|
||||
|
||||
+1
Submodule vendor/modules/wxl-modern-adt added at 0412b690c0
+1
Submodule vendor/modules/wxl-modern-assets added at eb7c7c6646
+1
Submodule vendor/modules/wxl-modern-render added at 3f246a5e20
+1
Submodule vendor/modules/wxl-unit-outline added at 29e5e29fa9
+1
Submodule vendor/warcraftxl added at 9d981125e1
Reference in New Issue
Block a user