Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85f80445d1 | |||
| 9fdb7ed718 | |||
| 698de84762 | |||
| cddaf2c515 | |||
| 26ff2b3b94 |
+1
-1
@@ -5,4 +5,4 @@ AWS_SECRET_ACCESS_KEY=
|
|||||||
AWS_DEFAULT_REGION=
|
AWS_DEFAULT_REGION=
|
||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_ENDPOINT=
|
AWS_ENDPOINT=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||||
|
|||||||
+5
-1
@@ -3,4 +3,8 @@ __pycache__/
|
|||||||
manifest.json
|
manifest.json
|
||||||
.claude
|
.claude
|
||||||
.vscode
|
.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,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
|
# 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` - модифицированный клиентский исполняемый файл.
|
- разблокировка пользовательского `GlueXML`;
|
||||||
- Поддерживает использование до 4 ГБ ОЗУ.
|
- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`;
|
||||||
- Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели.
|
- одиннадцатое значение `GetCharacterInfo` — `charFlags`, включая флаг предателя `0x40000000`;
|
||||||
- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`.
|
- загрузка WarcraftXL без import-table patch и без отдельного injector.
|
||||||
- Содержит графические элементы внутриигрового магазина.
|
|
||||||
- Основная зона изменений: `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/`.
|
|
||||||
|
|
||||||
## Структура по назначению
|
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`
|
```powershell
|
||||||
- `Currencies/Token.blp`
|
git submodule update --remote vendor/warcraftxl vendor/modules/*
|
||||||
- `Frames/StoreFrame_Main.blp`
|
```
|
||||||
|
|
||||||
### Экран входа и локализованные ресурсы
|
После обновления обязательно пересоберите и проверьте вход/создание персонажа: 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-исходники интерфейса загрузки и логина
|
После клонирования инициализируйте submodule:
|
||||||
- `Interface/Glues` - графика и модели для glue-экрана
|
|
||||||
- `Interface/LoginScreen` - фон и связанные изображения
|
|
||||||
- `Sound/Ambience/GlueScreen` - фоновые звуки
|
|
||||||
- `Sound/Music/GlueScreenMusic` - музыка экрана входа
|
|
||||||
|
|
||||||
### Русский перевод для `patch-ruRU-5`
|
```powershell
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5).
|
## Сборка WarcraftXL
|
||||||
|
|
||||||
Основные каталоги:
|
Быстрое полное развёртывание клиента одной командой:
|
||||||
|
|
||||||
- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа
|
```powershell
|
||||||
- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect`
|
.\deploy.ps1
|
||||||
- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов
|
```
|
||||||
- `Interface/Loginscreen` - фон и изображения экрана входа
|
|
||||||
- `Interface/tooltips` - локализованные рамки и оформление тултипов
|
|
||||||
- `Interface/cinematics` - элементы интерфейса для роликов и заставок
|
|
||||||
|
|
||||||
Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов.
|
Скрипт инициализирует зависимости, собирает 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+
|
# Аварийный native D3D9 вместо D3D9On12
|
||||||
- `Interface/MythicPlus/sounds` - звуки таймеров и событий
|
.\deploy.ps1 -NativeRenderer
|
||||||
- `Interface/Icons` - дополнительные иконки
|
```
|
||||||
- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом
|
|
||||||
|
|
||||||
## Как воспринимать содержимое репозитория
|
Без установки в клиент:
|
||||||
|
|
||||||
- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде.
|
```powershell
|
||||||
- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения.
|
.\build-warcraftxl.ps1 -Configuration Release
|
||||||
- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла.
|
```
|
||||||
|
|
||||||
## Для чего используется
|
С установкой:
|
||||||
|
|
||||||
Проект нужен для сопровождения клиентской части MoonWell:
|
```powershell
|
||||||
|
.\build-warcraftxl.ps1 -Configuration Release `
|
||||||
|
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
|
||||||
|
```
|
||||||
|
|
||||||
- кастомизации GlueXML;
|
При `-Deploy` скрипт:
|
||||||
- поддержки русского перевода для `patch-ruRU-5`;
|
|
||||||
- поддержки внутриигрового магазина;
|
1. проверяет хеш `Wow_Original.exe`;
|
||||||
- добавления графики и данных для Mythic+;
|
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
|
||||||
- хранения пакетов графических улучшений;
|
3. восстанавливает оригинальный `Wow.exe`;
|
||||||
- работы с модифицированным `Wow.exe` для WoW 3.3.5a.
|
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,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",
|
".git",
|
||||||
".moonwell_launcher",
|
".moonwell_launcher",
|
||||||
}
|
}
|
||||||
|
IGNORED_TOP_LEVEL_FILES = {
|
||||||
|
"Wow.moonwell-patched.backup.exe",
|
||||||
|
"WarcraftXL_on12.disable",
|
||||||
|
}
|
||||||
|
|
||||||
ENV_VAR_NAME = "WOW_HOME"
|
ENV_VAR_NAME = "WOW_HOME"
|
||||||
|
|
||||||
@@ -42,6 +46,8 @@ def is_ignored(rel_path: Path) -> bool:
|
|||||||
return False
|
return False
|
||||||
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
|
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
|
||||||
return True
|
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])
|
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"
|
$DIST_DIR = Join-Path $ROOT "dist"
|
||||||
$TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
|
$TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
|
||||||
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
|
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
|
||||||
|
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
|
||||||
|
|
||||||
# --- Ensure tool exists
|
# --- Ensure tool exists
|
||||||
if (!(Test-Path $TOOL)) {
|
if (!(Test-Path $TOOL)) {
|
||||||
@@ -59,6 +60,16 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
|
|||||||
exit 1
|
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
|
# --- Sync dist/ -> WOW_HOME
|
||||||
Write-Host "Syncing dist/ -> WOW_HOME..."
|
Write-Host "Syncing dist/ -> WOW_HOME..."
|
||||||
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
|
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
|
||||||
|
|||||||
@@ -676,6 +676,44 @@ function CharacterCreate_UpdateGameModeVisibility()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- The stock creation scene frames each race at a different height. Projection
|
||||||
|
-- zoom therefore needs a per-race vertical target to keep the face in the same
|
||||||
|
-- part of the screen. Values are indexed by GetSelectedSex(): 2 male, 3 female.
|
||||||
|
local CHARACTER_CREATE_CAMERA_PROFILES = {
|
||||||
|
HUMAN = { [2] = -0.28, [3] = -0.20 },
|
||||||
|
ORC = { [2] = -0.50, [3] = -0.30 },
|
||||||
|
DWARF = { [2] = 0.30, [3] = 0.38 },
|
||||||
|
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
|
||||||
|
SCOURGE = { [2] = 0.18, [3] = 0.27 },
|
||||||
|
TAUREN = { [2] = -0.78, [3] = -0.62 },
|
||||||
|
GNOME = { [2] = 0.64, [3] = 0.72 },
|
||||||
|
TROLL = { [2] = -0.70, [3] = -0.55 },
|
||||||
|
BLOODELF = { [2] = -0.30, [3] = -0.22 },
|
||||||
|
DRAENEI = { [2] = -0.62, [3] = -0.55 },
|
||||||
|
};
|
||||||
|
|
||||||
|
local function CharacterCreate_GetCameraProfile()
|
||||||
|
local _, raceFile = GetNameForRace();
|
||||||
|
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
|
||||||
|
local sex = GetSelectedSex();
|
||||||
|
return 2.0, raceProfile and raceProfile[sex] or -0.20;
|
||||||
|
end
|
||||||
|
|
||||||
|
local function CharacterCreate_UpdateCamera(immediate)
|
||||||
|
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
|
||||||
|
local mode = showFace and 1 or 0;
|
||||||
|
if CharacterCreate.cameraMode == mode and not immediate then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
CharacterCreate.cameraMode = mode;
|
||||||
|
CharacterCreate:SetCamera(0);
|
||||||
|
if MoonWellSetCharacterCreateCamera then
|
||||||
|
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
|
||||||
|
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function CharacterCreate_GetEnteredName()
|
local function CharacterCreate_GetEnteredName()
|
||||||
local text = "";
|
local text = "";
|
||||||
|
|
||||||
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
|
|||||||
CharacterCreate.gameModeSelectionMode = false;
|
CharacterCreate.gameModeSelectionMode = false;
|
||||||
end
|
end
|
||||||
|
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
|
|
||||||
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
|
||||||
|
|
||||||
if showGameModes then
|
if showGameModes then
|
||||||
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
|
|||||||
CharCreatePersonalizeButton:Show();
|
CharCreatePersonalizeButton:Show();
|
||||||
CharCreateOkayButton:Hide();
|
CharCreateOkayButton:Hide();
|
||||||
CharacterCreate_UpdateGameModeVisibility();
|
CharacterCreate_UpdateGameModeVisibility();
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
|
|
||||||
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
for i=1, NUM_CHAR_CUSTOMIZATIONS do
|
||||||
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
_G["CharacterCustomizationButtonFrame"..i]:Hide();
|
||||||
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
|
|||||||
CharacterChangeFixup();
|
CharacterChangeFixup();
|
||||||
CharacterCreate_CreateGenderButtonTextures();
|
CharacterCreate_CreateGenderButtonTextures();
|
||||||
CharacterCreate_UpdateButtonCheckedStates();
|
CharacterCreate_UpdateButtonCheckedStates();
|
||||||
CharacterCreate_ResetState();
|
CharacterCreate_ResetState(true);
|
||||||
|
|
||||||
hideScheduled = true
|
hideScheduled = true
|
||||||
HideNameEditFrame:Show()
|
HideNameEditFrame:Show()
|
||||||
@@ -1013,7 +1054,7 @@ end
|
|||||||
function CharacterCreate_OnHide()
|
function CharacterCreate_OnHide()
|
||||||
PAID_SERVICE_CHARACTER_ID = nil;
|
PAID_SERVICE_CHARACTER_ID = nil;
|
||||||
PAID_SERVICE_TYPE = nil;
|
PAID_SERVICE_TYPE = nil;
|
||||||
CharacterCreate_ResetState();
|
CharacterCreate_ResetState(true);
|
||||||
|
|
||||||
for button, tooltip in pairs(raceTooltips) do
|
for button, tooltip in pairs(raceTooltips) do
|
||||||
if tooltip then tooltip:Hide() end
|
if tooltip then tooltip:Hide() end
|
||||||
@@ -1518,6 +1559,7 @@ end
|
|||||||
|
|
||||||
function CharacterCreate_UpdateModel(self)
|
function CharacterCreate_UpdateModel(self)
|
||||||
UpdateCustomizationScene();
|
UpdateCustomizationScene();
|
||||||
|
CharacterCreate_UpdateCamera();
|
||||||
self:AdvanceTime();
|
self:AdvanceTime();
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
|
|||||||
CharacterCreate_UpdateButtonCheckedStates();
|
CharacterCreate_UpdateButtonCheckedStates();
|
||||||
end
|
end
|
||||||
|
|
||||||
function CharacterCreate_ResetState()
|
function CharacterCreate_ResetState(immediate)
|
||||||
CharacterCreate.personalizationMode = false;
|
CharacterCreate.personalizationMode = false;
|
||||||
CharacterCreate.gameModeSelectionMode = false;
|
CharacterCreate.gameModeSelectionMode = false;
|
||||||
|
CharacterCreate_UpdateCamera(immediate);
|
||||||
CharacterCreateRaceButtonsContainer:Show();
|
CharacterCreateRaceButtonsContainer:Show();
|
||||||
CharacterCreateClassButtonsContainer:Show();
|
CharacterCreateClassButtonsContainer:Show();
|
||||||
CharacterCreateGenderButtonsContainer:Show();
|
CharacterCreateGenderButtonsContainer:Show();
|
||||||
|
|||||||
@@ -1139,6 +1139,12 @@ end
|
|||||||
function CharacterSelect_EnterWorld()
|
function CharacterSelect_EnterWorld()
|
||||||
PlaySound("gsCharacterSelectionEnterWorld");
|
PlaySound("gsCharacterSelectionEnterWorld");
|
||||||
StopGlueAmbience();
|
StopGlueAmbience();
|
||||||
|
-- Persist the server-provided game-mode bit across the GlueXML -> FrameXML
|
||||||
|
-- Lua-state transition. WarcraftXL owns the value for this Wow.exe process.
|
||||||
|
if ( type(MoonWellSetLoginCharacterFlags) == "function" ) then
|
||||||
|
local info = { GetCharacterInfo(CharacterSelect.selectedIndex) };
|
||||||
|
MoonWellSetLoginCharacterFlags(info[11] or 0);
|
||||||
|
end
|
||||||
EnterWorld();
|
EnterWorld();
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
-- The MoonWell Achievement.dbc intentionally exposes both faction variants.
|
||||||
|
-- Restore the 3.3.5a client-side faction filter and make traitors use the
|
||||||
|
-- opposite faction. IDs come from the stock ruRU build 12340 DBC.
|
||||||
|
|
||||||
|
local achievementFaction = {}
|
||||||
|
|
||||||
|
local function AddFaction(ids, faction)
|
||||||
|
for id in string.gmatch(ids, "%d+") do
|
||||||
|
achievementFaction[tonumber(id)] = faction
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
AddFaction([[
|
||||||
|
33 34 35 37 41 58 202 203 206 220 225 230 246 388 433 434 435 436 437 438
|
||||||
|
439 440 441 442 470 471 472 473 604 610 611 612 613 614 701 707 709 711 713
|
||||||
|
764 899 907 908 942 948 963 966 969 970 1012 1022 1023 1024 1028 1029 1030
|
||||||
|
1034 1035 1038 1040 1151 1167 1169 1172 1184 1189 1191 1192 1255 1262 1279
|
||||||
|
1466 1563 1656 1676 1678 1681 1684 1686 1692 1697 1707 1737 1752 1757 1762
|
||||||
|
1782 2016 2144 2194 2419 2421 2536 2760 2761 2762 2763 2764 2770 2777 2778
|
||||||
|
2779 2780 2781 2782 2797 2817 3356 3478 3556 3576 3580 3596 3676 3846 3851
|
||||||
|
3856 3857 4156 4296 4298 4436 4784 4786
|
||||||
|
]], "Alliance")
|
||||||
|
|
||||||
|
AddFaction([[
|
||||||
|
224 259 443 444 445 446 447 448 449 450 451 452 453 454 468 469 593 603 615
|
||||||
|
616 617 618 619 700 706 708 710 712 714 762 763 873 901 909 926 943 965 967
|
||||||
|
968 971 1005 1006 1011 1025 1026 1027 1031 1032 1033 1036 1037 1039 1041
|
||||||
|
1164 1168 1170 1173 1175 1203 1251 1252 1271 1272 1273 1274 1280 1356 1357
|
||||||
|
1358 1359 1360 1502 1657 1677 1680 1682 1683 1685 1691 1693 1698 1783 1784
|
||||||
|
2017 2145 2192 2195 2200 2420 2476 2497 2537 2765 2766 2767 2768 2769 2771
|
||||||
|
2776 2783 2784 2785 2786 2787 2788 2798 2816 3357 3557 3577 3581 3597 3656
|
||||||
|
3677 3778 3957 4079 4176 4177 4256 4297 4437 4785 4790
|
||||||
|
]], "Horde")
|
||||||
|
|
||||||
|
local NativeGetAchievementInfo = GetAchievementInfo
|
||||||
|
local NativeGetCategoryNumAchievements = GetCategoryNumAchievements
|
||||||
|
local NativeGetLatestCompletedAchievements = GetLatestCompletedAchievements
|
||||||
|
local NativeGetTrackedAchievements = GetTrackedAchievements
|
||||||
|
local NativeGetNumCompletedAchievements = GetNumCompletedAchievements
|
||||||
|
local NativeGetTotalAchievementPoints = GetTotalAchievementPoints
|
||||||
|
|
||||||
|
local categoryCache = {}
|
||||||
|
local summaryCache = {}
|
||||||
|
|
||||||
|
local function GetDisplayFaction()
|
||||||
|
local faction = UnitFactionGroup("player")
|
||||||
|
if MoonWell_IsTraitor then
|
||||||
|
if faction == "Alliance" then
|
||||||
|
return "Horde"
|
||||||
|
elseif faction == "Horde" then
|
||||||
|
return "Alliance"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return faction
|
||||||
|
end
|
||||||
|
|
||||||
|
local function IsAchievementVisible(id, faction)
|
||||||
|
local requiredFaction = achievementFaction[id]
|
||||||
|
return not requiredFaction or not faction or requiredFaction == faction
|
||||||
|
end
|
||||||
|
|
||||||
|
local function BuildCategory(categoryID, showAll)
|
||||||
|
local faction = GetDisplayFaction()
|
||||||
|
local key = tostring(categoryID) .. ":" .. tostring(showAll and 1 or 0)
|
||||||
|
.. ":" .. tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
|
||||||
|
local cached = categoryCache[key]
|
||||||
|
if cached then
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
local nativeCount = NativeGetCategoryNumAchievements(categoryID, showAll)
|
||||||
|
local indices = {}
|
||||||
|
local completed = 0
|
||||||
|
|
||||||
|
for nativeIndex = 1, nativeCount do
|
||||||
|
local id, _, _, isCompleted = NativeGetAchievementInfo(categoryID, nativeIndex)
|
||||||
|
if id and IsAchievementVisible(id, faction) then
|
||||||
|
indices[#indices + 1] = nativeIndex
|
||||||
|
if isCompleted then
|
||||||
|
completed = completed + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cached = { indices = indices, completed = completed }
|
||||||
|
categoryCache[key] = cached
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetCategoryNumAchievements(categoryID, showAll)
|
||||||
|
local category = BuildCategory(categoryID, showAll)
|
||||||
|
return #category.indices, category.completed
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetAchievementInfo(idOrCategory, achievementIndex)
|
||||||
|
if achievementIndex == nil then
|
||||||
|
return NativeGetAchievementInfo(idOrCategory)
|
||||||
|
end
|
||||||
|
|
||||||
|
local category = BuildCategory(idOrCategory, false)
|
||||||
|
local nativeIndex = category.indices[achievementIndex]
|
||||||
|
if not nativeIndex then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return NativeGetAchievementInfo(idOrCategory, nativeIndex)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function BuildSummary()
|
||||||
|
local faction = GetDisplayFaction()
|
||||||
|
local key = tostring(faction) .. ":" .. tostring(MoonWell_IsTraitor)
|
||||||
|
local cached = summaryCache[key]
|
||||||
|
if cached then
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
local total, completed = NativeGetNumCompletedAchievements()
|
||||||
|
local points = NativeGetTotalAchievementPoints()
|
||||||
|
local statisticFlag = ACHIEVEMENT_FLAGS_STATISTIC or 0x00000001
|
||||||
|
local hiddenFlag = ACHIEVEMENT_FLAGS_HIDDEN or 0x00000002
|
||||||
|
|
||||||
|
for id, requiredFaction in pairs(achievementFaction) do
|
||||||
|
if faction and requiredFaction ~= faction then
|
||||||
|
local _, _, achievementPoints, isCompleted, _, _, _, _, flags =
|
||||||
|
NativeGetAchievementInfo(id)
|
||||||
|
if flags and bit.band(flags, statisticFlag) == 0 then
|
||||||
|
local isHidden = bit.band(flags, hiddenFlag) ~= 0
|
||||||
|
if not isHidden or isCompleted then
|
||||||
|
total = total - 1
|
||||||
|
end
|
||||||
|
if isCompleted then
|
||||||
|
completed = completed - 1
|
||||||
|
points = points - (achievementPoints or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cached = { total = total, completed = completed, points = points }
|
||||||
|
summaryCache[key] = cached
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetNumCompletedAchievements()
|
||||||
|
local summary = BuildSummary()
|
||||||
|
return summary.total, summary.completed
|
||||||
|
end
|
||||||
|
|
||||||
|
function GetTotalAchievementPoints()
|
||||||
|
return BuildSummary().points
|
||||||
|
end
|
||||||
|
|
||||||
|
local function FilterAchievementIDs(getter)
|
||||||
|
local faction = GetDisplayFaction()
|
||||||
|
local source = { getter() }
|
||||||
|
local filtered = {}
|
||||||
|
for i = 1, #source do
|
||||||
|
if IsAchievementVisible(source[i], faction) then
|
||||||
|
filtered[#filtered + 1] = source[i]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return unpack(filtered)
|
||||||
|
end
|
||||||
|
|
||||||
|
if NativeGetLatestCompletedAchievements then
|
||||||
|
function GetLatestCompletedAchievements()
|
||||||
|
return FilterAchievementIDs(NativeGetLatestCompletedAchievements)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if NativeGetTrackedAchievements then
|
||||||
|
function GetTrackedAchievements()
|
||||||
|
return FilterAchievementIDs(NativeGetTrackedAchievements)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function MoonWellAchievementFaction_Refresh()
|
||||||
|
categoryCache = {}
|
||||||
|
summaryCache = {}
|
||||||
|
if AchievementFrame and AchievementFrame:IsShown() then
|
||||||
|
if AchievementFrameCategories_Update then
|
||||||
|
AchievementFrameCategories_Update()
|
||||||
|
end
|
||||||
|
if AchievementFrameAchievements_ForceUpdate then
|
||||||
|
AchievementFrameAchievements_ForceUpdate()
|
||||||
|
elseif AchievementFrameAchievements_Update then
|
||||||
|
AchievementFrameAchievements_Update()
|
||||||
|
end
|
||||||
|
if AchievementFrameSummary_Update then
|
||||||
|
AchievementFrameSummary_Update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local events = CreateFrame("Frame")
|
||||||
|
events:RegisterEvent("ACHIEVEMENT_EARNED")
|
||||||
|
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
events:RegisterEvent("UNIT_FACTION")
|
||||||
|
events:SetScript("OnEvent", function(self, event, unit)
|
||||||
|
if event ~= "UNIT_FACTION" or unit == "player" then
|
||||||
|
MoonWellAchievementFaction_Refresh()
|
||||||
|
end
|
||||||
|
end)
|
||||||
@@ -1,62 +1,85 @@
|
|||||||
-- ============================================================================
|
-- MoonWell client game-mode integration.
|
||||||
-- MoonWell Client
|
-- WarcraftXL retains the selected character's server-provided charFlags while
|
||||||
-- Server integration: game modes, traitor faction display
|
-- WoW replaces the GlueXML Lua state with the in-world FrameXML state.
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
-- Global flag read by FrameXML files to decide which faction icon to show.
|
|
||||||
-- true = show opposite faction icon (traitor)
|
|
||||||
-- false = show real faction icon (normal)
|
|
||||||
MoonWell_IsTraitor = false
|
MoonWell_IsTraitor = false
|
||||||
|
|
||||||
-- ============================================================================
|
local function RefreshFactionIcons()
|
||||||
-- AIO handler — called by server on login
|
|
||||||
-- Server: AIO:Handle(player, "MoonWellClient", "Init", gameMode)
|
|
||||||
-- gameMode: 0 = normal, 1 = traitor
|
|
||||||
-- ============================================================================
|
|
||||||
|
|
||||||
if not AIO then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local Handler = AIO.AddHandlers("MoonWellClient", {})
|
|
||||||
|
|
||||||
local function MoonWell_RefreshFactionIcons()
|
|
||||||
-- Portrait PvP icon
|
|
||||||
if PlayerFrame_UpdatePvPStatus then
|
if PlayerFrame_UpdatePvPStatus then
|
||||||
PlayerFrame_UpdatePvPStatus()
|
PlayerFrame_UpdatePvPStatus()
|
||||||
end
|
end
|
||||||
-- DragonUI PvP micro button: SetupPVPButton() runs on load before AIO responds,
|
|
||||||
-- so we refresh its DragonUIPVPIcon texture directly after Init.
|
if PVPFrame and PVPFrame:IsShown() and PVPFrame_Update then
|
||||||
|
PVPFrame_Update()
|
||||||
|
end
|
||||||
|
if BattlefieldFrame and BattlefieldFrame:IsShown() and BattlefieldFrame_Update then
|
||||||
|
BattlefieldFrame_Update()
|
||||||
|
end
|
||||||
|
|
||||||
if PVPMicroButton and PVPMicroButton.DragonUIPVPIcon then
|
if PVPMicroButton and PVPMicroButton.DragonUIPVPIcon then
|
||||||
local factionGroup = UnitFactionGroup("player")
|
local factionGroup = UnitFactionGroup("player")
|
||||||
if factionGroup then
|
if factionGroup then
|
||||||
local displayFaction = MoonWell_IsTraitor
|
local displayFaction = MoonWell_IsTraitor
|
||||||
and (factionGroup == "Alliance" and "Horde" or "Alliance")
|
and (factionGroup == "Alliance" and "Horde" or "Alliance")
|
||||||
or factionGroup
|
or factionGroup
|
||||||
local microTexture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
|
local texture = "Interface\\AddOns\\DragonUI\\Textures\\Micromenu\\micropvp"
|
||||||
local icon = PVPMicroButton.DragonUIPVPIcon
|
local icon = PVPMicroButton.DragonUIPVPIcon
|
||||||
|
local highlight = PVPMicroButton:GetHighlightTexture()
|
||||||
|
|
||||||
if displayFaction == "Alliance" then
|
if displayFaction == "Alliance" then
|
||||||
icon:SetTexture(microTexture)
|
icon:SetTexture(texture)
|
||||||
icon:SetTexCoord(0, 118/256, 0, 151/256)
|
icon:SetTexCoord(0, 118/256, 0, 151/256)
|
||||||
|
if highlight then
|
||||||
|
highlight:SetTexture(texture)
|
||||||
|
highlight:SetTexCoord(0, 118/256, 0, 151/256)
|
||||||
|
end
|
||||||
elseif displayFaction == "Horde" then
|
elseif displayFaction == "Horde" then
|
||||||
icon:SetTexture(microTexture)
|
icon:SetTexture(texture)
|
||||||
icon:SetTexCoord(118/256, 236/256, 0, 151/256)
|
icon:SetTexCoord(118/256, 236/256, 0, 151/256)
|
||||||
end
|
if highlight then
|
||||||
local hl = PVPMicroButton:GetHighlightTexture()
|
highlight:SetTexture(texture)
|
||||||
if hl then
|
highlight:SetTexCoord(118/256, 236/256, 0, 151/256)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function Handler.Init(_, mode)
|
local function ReadNativeGameMode()
|
||||||
MoonWell_IsTraitor = (tonumber(mode) == 1)
|
if type(MoonWellIsTraitor) ~= "function" then
|
||||||
MoonWell_RefreshFactionIcons()
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local wasTraitor = MoonWell_IsTraitor
|
||||||
|
MoonWell_IsTraitor = MoonWellIsTraitor() and true or false
|
||||||
|
if wasTraitor ~= MoonWell_IsTraitor and MoonWellAchievementFaction_Refresh then
|
||||||
|
MoonWellAchievementFaction_Refresh()
|
||||||
|
end
|
||||||
|
RefreshFactionIcons()
|
||||||
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local events = CreateFrame("Frame")
|
||||||
|
events:RegisterEvent("PLAYER_LOGIN")
|
||||||
|
events:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
events:RegisterEvent("UNIT_FACTION")
|
||||||
|
events:SetScript("OnEvent", function(self, event, unit)
|
||||||
|
if event ~= "UNIT_FACTION" or unit == "player" then
|
||||||
|
ReadNativeGameMode()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- WarcraftXL may publish its Lua globals shortly after addons are loaded.
|
||||||
|
-- Lua 5.1/WotLK has no C_Timer, so retry cheaply until the native API appears.
|
||||||
|
local elapsedTotal = 0
|
||||||
|
events:SetScript("OnUpdate", function(self, elapsed)
|
||||||
|
elapsedTotal = elapsedTotal + elapsed
|
||||||
|
if elapsedTotal < 0.25 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
elapsedTotal = 0
|
||||||
|
|
||||||
|
if ReadNativeGameMode() then
|
||||||
|
self:SetScript("OnUpdate", nil)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
## Interface: 30300
|
## Interface: 30300
|
||||||
## Title: MoonWell Client
|
## Title: MoonWell Client
|
||||||
## Notes: MoonWell server integration — game modes, traitor faction display
|
## Notes: Native WarcraftXL game-mode and traitor faction display
|
||||||
## Version: 1.0.0
|
## Version: 2.0.0
|
||||||
## Author: MoonWell
|
## Author: MoonWell
|
||||||
## SavedVariables: MoonWellClientDB
|
## SavedVariables: MoonWellClientDB
|
||||||
## Dependencies: AIO_Client
|
|
||||||
## DefaultState: Enabled
|
## DefaultState: Enabled
|
||||||
## X-Hidden: 1
|
## X-Hidden: 1
|
||||||
|
|
||||||
|
AchievementFactions.lua
|
||||||
MoonWellClient.lua
|
MoonWellClient.lua
|
||||||
|
|||||||
+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