Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2291ace9f7 | |||
| 26ff2b3b94 | |||
| d5e2dce359 | |||
| 22ae941c4a | |||
| 08c1b9da3f |
@@ -6,3 +6,7 @@ AWS_DEFAULT_REGION=
|
|||||||
AWS_BUCKET=
|
AWS_BUCKET=
|
||||||
AWS_ENDPOINT=
|
AWS_ENDPOINT=
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT=
|
AWS_USE_PATH_STYLE_ENDPOINT=
|
||||||
|
|
||||||
|
PRODUCTION_REALMLIST=
|
||||||
|
PTR_REALMLIST=
|
||||||
|
LOCAL_REALMLIST=
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ 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,46 @@
|
|||||||
|
# MoonWell Client Sources — Agent Guide
|
||||||
|
|
||||||
|
## Проект
|
||||||
|
|
||||||
|
WoW-клиент на базе патча **3.3.5a (build 12340, WotLK)**. Репозиторий содержит UI-аддоны и
|
||||||
|
кастомные интерфейсы для приватного сервера MoonWell.
|
||||||
|
|
||||||
|
Стек: Lua 5.1, FrameXML (XML + Lua), WoW Addon API 3.3.5.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Навыки (Skills)
|
||||||
|
|
||||||
|
Перед тем как работать с UI — **прочитай соответствующий файл из `ai-skills/`**.
|
||||||
|
|
||||||
|
| Задача | Файл |
|
||||||
|
|--------|------|
|
||||||
|
| Верстка фреймов, XML, Lua CreateFrame, pixel-perfect из Figma, текстуры, шрифты, якоря | [`ai-skills/ui/SKILL.md`](ai-skills/ui/SKILL.md) |
|
||||||
|
| Виджеты, типы фреймов, Button/EditBox/ScrollFrame, события, SavedVariables, Blizzard API | [`ai-skills/ui/widgets-and-api.md`](ai-skills/ui/widgets-and-api.md) |
|
||||||
|
|
||||||
|
Читай **весь файл** скилла, а не отдельные секции — там есть ограничения API, типичные ошибки
|
||||||
|
и готовые шаблоны.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ключевые ограничения (всегда держи в голове)
|
||||||
|
|
||||||
|
- `## Interface: 30300` — никаких retail-API
|
||||||
|
- Нет `SetSize()` → только `SetWidth()` / `SetHeight()`
|
||||||
|
- Нет `C_Timer`, `CreateFramePool`, `PixelUtil`
|
||||||
|
- Текстуры: только степени двойки, максимум 512×512 px, формат `.tga` / `.blp`
|
||||||
|
- XML-атрибуты: `relativeTo` (не `relativeKey`), `$parentX` (не `$parent.X`)
|
||||||
|
- Кнопки и ScrollFrame в XML должны иметь `name`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Структура аддона
|
||||||
|
|
||||||
|
```
|
||||||
|
AddonName/
|
||||||
|
├── AddonName.toc ← обязательно, имя файла = имя папки
|
||||||
|
├── AddonName.xml ← верстка (загружается первой)
|
||||||
|
└── AddonName.lua ← логика
|
||||||
|
```
|
||||||
|
|
||||||
|
Порядок файлов в TOC важен: XML раньше Lua.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MoonWellWarcraftXL LANGUAGES CXX)
|
||||||
|
|
||||||
|
# WarcraftXL remains an upstream submodule. MoonWell modules are attached to
|
||||||
|
# its aggregate DLL target here, so updating the framework does not mix our
|
||||||
|
# client-specific offsets and policy into the upstream checkout.
|
||||||
|
add_subdirectory(vendor/warcraftxl)
|
||||||
|
|
||||||
|
set(WXL_EXTERNAL_MODULES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/vendor/modules")
|
||||||
|
|
||||||
|
function(wxl_collect_sources output)
|
||||||
|
file(GLOB_RECURSE collected CONFIGURE_DEPENDS ${ARGN})
|
||||||
|
set(${output} "${collected}" PARENT_SCOPE)
|
||||||
|
endfunction()
|
||||||
|
|
||||||
|
if(TARGET WarcraftXL)
|
||||||
|
wxl_collect_sources(WXL_MODERN_ASSETS_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/src/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||||
|
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||||
|
list(FILTER WXL_MODERN_ASSETS_RUNTIME EXCLUDE REGEX
|
||||||
|
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||||
|
endif()
|
||||||
|
wxl_collect_sources(WXL_MODERN_RENDER_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/src/*.cpp")
|
||||||
|
wxl_collect_sources(WXL_UNIT_OUTLINE_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-unit-outline/src/*.cpp")
|
||||||
|
wxl_collect_sources(WXL_MODERN_ADT_RUNTIME
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/src/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||||
|
|
||||||
|
target_sources(WarcraftXL PRIVATE
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/modules/moonwell/src/MoonWell.cpp"
|
||||||
|
${WXL_MODERN_ASSETS_RUNTIME}
|
||||||
|
${WXL_MODERN_RENDER_RUNTIME}
|
||||||
|
${WXL_UNIT_OUTLINE_RUNTIME}
|
||||||
|
${WXL_MODERN_ADT_RUNTIME})
|
||||||
|
|
||||||
|
# wxl-modern-render needs its module-root includes and D3D12 import library.
|
||||||
|
include("${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-render/module.cmake")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET WarcraftXLHost)
|
||||||
|
wxl_collect_sources(WXL_MODERN_ASSETS_HOST
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/host/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-assets/shared/*.cpp")
|
||||||
|
if(WXL_MODERN_ASSETS_EXCLUDE_REGEX)
|
||||||
|
list(FILTER WXL_MODERN_ASSETS_HOST EXCLUDE REGEX
|
||||||
|
"${WXL_MODERN_ASSETS_EXCLUDE_REGEX}")
|
||||||
|
endif()
|
||||||
|
wxl_collect_sources(WXL_MODERN_ADT_HOST
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/host/*.cpp"
|
||||||
|
"${WXL_EXTERNAL_MODULES_DIR}/wxl-modern-adt/shared/*.cpp")
|
||||||
|
|
||||||
|
target_sources(WarcraftXLHost PRIVATE
|
||||||
|
${WXL_MODERN_ASSETS_HOST}
|
||||||
|
${WXL_MODERN_ADT_HOST})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Minimal native-D3D9 proxy retained as a recovery renderer. The regular
|
||||||
|
# package uses WarcraftXL's D3D9On12 proxy required by wxl-modern-render.
|
||||||
|
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||||
|
add_library(MoonWellLoader SHARED
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/loader/d3d9.def")
|
||||||
|
set_target_properties(MoonWellLoader PROPERTIES
|
||||||
|
OUTPUT_NAME "d3d9-native"
|
||||||
|
PREFIX ""
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/artifacts")
|
||||||
|
target_compile_definitions(MoonWellLoader PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||||
|
endif()
|
||||||
@@ -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,535 @@
|
|||||||
|
---
|
||||||
|
name: wow-335a-ui
|
||||||
|
description: >
|
||||||
|
Верстка и разработка UI-аддонов для World of Warcraft 3.3.5a (WotLK, build 12340).
|
||||||
|
Используй этот скилл при любой работе с интерфейсом WoW 3.3.5: создание фреймов
|
||||||
|
через XML или Lua CreateFrame(), позиционирование через SetPoint/якоря, слои
|
||||||
|
(BACKGROUND/ARTWORK/OVERLAY/HIGHLIGHT), страты (frameStrata), текстуры, шрифты,
|
||||||
|
кнопки, ScrollFrame, скрипты-события, структура аддона (TOC + XML + Lua),
|
||||||
|
SavedVariables, работа с Blizzard API патча 3.3.5. Также применяй когда речь идёт
|
||||||
|
об AzerothCore, кастомных интерфейсах для WoW-прайват серверов 3.3.5a, Eluna
|
||||||
|
или модификации FrameXML. ОБЯЗАТЕЛЬНО применяй при задачах pixel-perfect переноса
|
||||||
|
дизайна (Figma/PSD/скриншот) в WoW UI: пересчёт координат с учётом UIParent scale,
|
||||||
|
конвертация цветов HEX→RGBA, подбор шрифтов, подготовка текстур (степени двойки,
|
||||||
|
TexCoord-кроп), устранение расхождений между макетом и игровым интерфейсом.
|
||||||
|
---
|
||||||
|
|
||||||
|
# WoW 3.3.5a — Верстка UI (FrameXML / AddOn Development)
|
||||||
|
|
||||||
|
## Ключевые ограничения патча 3.3.5a
|
||||||
|
|
||||||
|
- **API версии**: `## Interface: 30300` (build 12340)
|
||||||
|
- **Lua версия**: Lua 5.1
|
||||||
|
- **Текстуры**: только степени двойки по ширине/высоте (8, 16, 32, 64, 128, 256, 512 px); максимум 512×512
|
||||||
|
- **Нет** `:SetSize()` — только `:SetWidth()` / `:SetHeight()` (или через XML `<Size>`)
|
||||||
|
- **Нет** `hooksecurefunc` для защищённых фреймов без taint
|
||||||
|
- Отсутствуют многие retail-API: нет `C_Timer`, нет `CreateFramePool`, нет `PixelUtil`
|
||||||
|
- Таймеры — через `OnUpdate` с накоплением `elapsed`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pixel-Perfect: перенос дизайна из Figma/PSD в WoW UI
|
||||||
|
|
||||||
|
### Главная проблема: система координат WoW ≠ пиксели экрана
|
||||||
|
|
||||||
|
WoW рисует UI в **виртуальных единицах**, а не в экранных пикселях. Размер виртуального экрана зафиксирован движком на **768 единиц по высоте**, ширина зависит от соотношения сторон:
|
||||||
|
|
||||||
|
| Разрешение | Виртуальная ширина | Виртуальная высота |
|
||||||
|
|-----------|-------------------|-------------------|
|
||||||
|
| 1024×768 | 1024 | 768 |
|
||||||
|
| 1280×960 | 1024 | 768 |
|
||||||
|
| 1920×1080 | 1365 | 768 |
|
||||||
|
| 2560×1440 | 1365 | 768 |
|
||||||
|
|
||||||
|
> **Вывод**: высота UIParent всегда = 768. Всё, что сделано в Figma на 768px по высоте, переносится 1:1 без пересчёта. При другой высоте холста в Figma — нужен коэффициент.
|
||||||
|
|
||||||
|
### Пересчёт координат из Figma в WoW
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Формула: wow_value = figma_value * (768 / figma_canvas_height)
|
||||||
|
-- Если макет сделан на 1080px:
|
||||||
|
local SCALE = 768 / 1080 -- ≈ 0.711
|
||||||
|
|
||||||
|
local function px(figma_px)
|
||||||
|
return math.floor(figma_px * SCALE + 0.5) -- округление к ближайшему
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Использование:
|
||||||
|
frame:SetWidth(px(400)) -- figma: 400px → wow: 284
|
||||||
|
frame:SetHeight(px(200)) -- figma: 200px → wow: 142
|
||||||
|
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
||||||
|
```
|
||||||
|
|
||||||
|
> Если макет 768px в высоту — `SCALE = 1.0`, пересчёт не нужен.
|
||||||
|
|
||||||
|
### UIParent scale и как он ломает пиксели
|
||||||
|
|
||||||
|
Игрок может изменить масштаб UI в настройках. Это сдвигает все координаты.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Получить текущий масштаб UIParent
|
||||||
|
local uiScale = UIParent:GetScale() -- обычно 0.64–1.0
|
||||||
|
|
||||||
|
-- Перевести экранные пиксели в виртуальные единицы WoW
|
||||||
|
local function screenPxToWow(px)
|
||||||
|
local screenH = select(2, GetPhysicalScreenSize()) -- реальный размер экрана
|
||||||
|
return px * (768 / screenH) / uiScale
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Принудительно зафиксировать scale (использовать с умом!)
|
||||||
|
-- UIParent:SetScale(1.0) -- сломает все стандартные фреймы
|
||||||
|
```
|
||||||
|
|
||||||
|
**Рекомендация**: не трогай `UIParent:SetScale()`. Вместо этого рисуй в виртуальных единицах и принимай, что у разных игроков масштаб разный. Для абсолютного pixel-perfect на конкретном разрешении — дай опцию ручного масштабирования фрейма в настройках.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Масштабируй только свой фрейм
|
||||||
|
frame:SetScale(0.85)
|
||||||
|
-- Тогда все дочерние элементы тоже масштабируются
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Цвета: Figma HEX/RGBA → WoW RGBA
|
||||||
|
|
||||||
|
WoW принимает цвета в диапазоне **0.0–1.0** (не 0–255).
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Конвертер HEX → WoW RGBA
|
||||||
|
local function hex(str, a)
|
||||||
|
str = str:gsub("#", "")
|
||||||
|
local r = tonumber(str:sub(1,2), 16) / 255
|
||||||
|
local g = tonumber(str:sub(3,4), 16) / 255
|
||||||
|
local b = tonumber(str:sub(5,6), 16) / 255
|
||||||
|
return r, g, b, a or 1.0
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Использование:
|
||||||
|
tex:SetVertexColor(hex("#FF6A00")) -- непрозрачный
|
||||||
|
tex:SetVertexColor(hex("#FF6A00", 0.8)) -- 80% непрозрачность
|
||||||
|
fs:SetTextColor(hex("#FFD100"))
|
||||||
|
frame:SetBackdropColor(hex("#1A1A2E", 0.95))
|
||||||
|
frame:SetBackdropBorderColor(hex("#4A90D9"))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Figma opacity → WoW alpha**: значение из Figma (например, 75%) делится на 100 → `0.75`.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Figma: opacity 60%
|
||||||
|
frame:SetAlpha(0.60)
|
||||||
|
tex:SetAlpha(0.60)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Шрифты: как приблизить к дизайну
|
||||||
|
|
||||||
|
WoW 3.3.5a поддерживает только `.ttf` шрифты, помещённые в папку аддона.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Кастомный шрифт из аддона
|
||||||
|
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", 14, "OUTLINE")
|
||||||
|
-- Флаги: "", "OUTLINE", "THICKOUTLINE", "MONOCHROME", "OUTLINE,MONOCHROME"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Соответствие Figma → WoW размер шрифта**:
|
||||||
|
|
||||||
|
| Figma (макет 1080px) | WoW pt (при SCALE=0.711) |
|
||||||
|
|---------------------|--------------------------|
|
||||||
|
| 12 px | 9 |
|
||||||
|
| 14 px | 10 |
|
||||||
|
| 16 px | 11 |
|
||||||
|
| 18 px | 13 |
|
||||||
|
| 24 px | 17 |
|
||||||
|
| 32 px | 23 |
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Автоматический пересчёт размера шрифта
|
||||||
|
local function fontSize(figma_pt)
|
||||||
|
return math.max(8, math.floor(figma_pt * SCALE + 0.5))
|
||||||
|
end
|
||||||
|
fs:SetFont("Fonts\\FRIZQT__.TTF", fontSize(18), "OUTLINE")
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Минимальный читаемый размер в WoW**: 8 pt. Ниже — нечитаемо.
|
||||||
|
|
||||||
|
**Встроенные шрифты WoW 3.3.5a**:
|
||||||
|
```
|
||||||
|
Fonts\FRIZQT__.TTF — основной интерфейсный (без кириллицы!)
|
||||||
|
Fonts\MORPHEUS.ttf — декоративный (заголовки)
|
||||||
|
Fonts\ARIALN.TTF — Arial Narrow
|
||||||
|
Fonts\skurri.ttf — шрифт повреждений
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Кириллица**: встроенные шрифты WoW не поддерживают кириллицу. Положи в аддон `.ttf` с кириллицей (например, PT Sans, Roboto) и подключи явно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Текстуры: подготовка под ограничения движка
|
||||||
|
|
||||||
|
#### Требования к текстурам WoW 3.3.5a
|
||||||
|
- Ширина и высота — **степени двойки**: 2, 4, 8, 16, 32, 64, 128, 256, 512
|
||||||
|
- Максимум: **512×512 px**
|
||||||
|
- Формат: `.tga` (RGBA, 32-bit) или `.blp`
|
||||||
|
- Текстура не обязана быть квадратной: 512×64 — ок, 256×128 — ок
|
||||||
|
|
||||||
|
#### Как уместить большой элемент дизайна
|
||||||
|
|
||||||
|
Если элемент из Figma больше 512px — разрезай на части:
|
||||||
|
|
||||||
|
```
|
||||||
|
[512×256] [512×256] ← верхняя половина (левая + правая)
|
||||||
|
[512×256] [512×256] ← нижняя половина
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Сборка из 4 текстур
|
||||||
|
local tl = frame:CreateTexture(nil, "BACKGROUND")
|
||||||
|
tl:SetTexture("Interface\\AddOns\\MyAddon\\bg_tl")
|
||||||
|
tl:SetPoint("TOPLEFT", frame, "TOPLEFT")
|
||||||
|
tl:SetWidth(256); tl:SetHeight(128)
|
||||||
|
|
||||||
|
local tr = frame:CreateTexture(nil, "BACKGROUND")
|
||||||
|
tr:SetTexture("Interface\\AddOns\\MyAddon\\bg_tr")
|
||||||
|
tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT")
|
||||||
|
tr:SetWidth(256); tr:SetHeight(128)
|
||||||
|
-- ... и так далее
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TexCoord — показать только часть текстуры
|
||||||
|
|
||||||
|
Если в одном файле несколько элементов (спрайт-лист), используй `SetTexCoord`:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- SetTexCoord(left, right, top, bottom) — от 0.0 до 1.0
|
||||||
|
-- Пример: иконка занимает правый нижний квадрант 256×256 атласа
|
||||||
|
tex:SetTexCoord(0.5, 1.0, 0.5, 1.0)
|
||||||
|
|
||||||
|
-- Формула для ячейки в сетке (col/row с нуля):
|
||||||
|
local cols, rows = 4, 4
|
||||||
|
local col, row = 2, 1 -- третья колонка, вторая строка
|
||||||
|
tex:SetTexCoord(
|
||||||
|
col/cols, -- left
|
||||||
|
(col+1)/cols, -- right
|
||||||
|
row/rows, -- top
|
||||||
|
(row+1)/rows -- bottom
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Важно**: при SetTexCoord текстура тайлится внутри заданного Size фрейма. SetAllPoints растягивает — SetWidth/SetHeight + SetPoint = точный размер.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Позиционирование: от Figma к SetPoint
|
||||||
|
|
||||||
|
В Figma позиция элемента — это X,Y от левого верхнего угла холста (или родителя). В WoW — якорная система.
|
||||||
|
|
||||||
|
**Алгоритм переноса**:
|
||||||
|
|
||||||
|
1. Определи ближайший угол/сторону в Figma (от чего считать)
|
||||||
|
2. Вычисли отступ от этой стороны
|
||||||
|
3. Пересчитай через `px()`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Figma (холст 1365×768):
|
||||||
|
-- Элемент: X=50, Y=30, W=200, H=100
|
||||||
|
-- → привязка от TOPLEFT UIParent
|
||||||
|
|
||||||
|
frame:SetWidth(px(200))
|
||||||
|
frame:SetHeight(px(100))
|
||||||
|
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
||||||
|
-- ^^^ ^^^
|
||||||
|
-- X offset Y offset (отрицательный! вниз)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Знаки осей WoW**: X растёт вправо (+), Y растёт вверх (+). Отступ вниз от TOPLEFT — **отрицательный**.
|
||||||
|
|
||||||
|
| Figma anchor | WoW SetPoint |
|
||||||
|
|-------------|-------------|
|
||||||
|
| Top-Left | `"TOPLEFT", UIParent, "TOPLEFT", x, -y` |
|
||||||
|
| Top-Right | `"TOPRIGHT", UIParent, "TOPRIGHT", -x, -y` |
|
||||||
|
| Bottom-Left | `"BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y` |
|
||||||
|
| Bottom-Right | `"BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -x, y` |
|
||||||
|
| Center | `"CENTER", UIParent, "CENTER", x - W/2_canvas, y` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Вспомогательный модуль: px-утилиты (вставь в начало Lua)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- PixelHelper — pixel-perfect вёрстка из Figma
|
||||||
|
local PH = {}
|
||||||
|
|
||||||
|
PH.CANVAS_HEIGHT = 768 -- высота холста в Figma (измени если другая!)
|
||||||
|
|
||||||
|
function PH.scale()
|
||||||
|
return 768 / PH.CANVAS_HEIGHT
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Пересчёт px из Figma → WoW единицы
|
||||||
|
function PH.px(v)
|
||||||
|
return math.floor(v * PH.scale() + 0.5)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Цвет из HEX (#RRGGBB или #RRGGBBAA)
|
||||||
|
function PH.color(hex, alpha)
|
||||||
|
hex = hex:gsub("#","")
|
||||||
|
local r = tonumber(hex:sub(1,2),16)/255
|
||||||
|
local g = tonumber(hex:sub(3,4),16)/255
|
||||||
|
local b = tonumber(hex:sub(5,6),16)/255
|
||||||
|
local a = alpha or (hex:len() == 8 and tonumber(hex:sub(7,8),16)/255) or 1
|
||||||
|
return r, g, b, a
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Размер шрифта из Figma → WoW
|
||||||
|
function PH.font(pt)
|
||||||
|
return math.max(8, math.floor(pt * PH.scale() + 0.5))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Установка якоря с автопересчётом
|
||||||
|
function PH.point(frame, point, relativeTo, relPoint, x, y)
|
||||||
|
frame:SetPoint(point, relativeTo, relPoint, PH.px(x or 0), PH.px(y or 0))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Размер фрейма из Figma
|
||||||
|
function PH.size(frame, w, h)
|
||||||
|
frame:SetWidth(PH.px(w))
|
||||||
|
frame:SetHeight(PH.px(h))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Экспорт глобально (или оставь локальным)
|
||||||
|
MyAddon_PH = PH
|
||||||
|
```
|
||||||
|
|
||||||
|
**Использование**:
|
||||||
|
```lua
|
||||||
|
local px = MyAddon_PH.px
|
||||||
|
local color = MyAddon_PH.color
|
||||||
|
local pxfont = MyAddon_PH.font
|
||||||
|
|
||||||
|
MyAddon_PH.size(frame, 400, 250)
|
||||||
|
MyAddon_PH.point(frame, "TOPLEFT", UIParent, "TOPLEFT", 50, -30)
|
||||||
|
frame:SetBackdropColor(color("#1A1A2E", 0.95))
|
||||||
|
fs:SetTextColor(color("#FFD100"))
|
||||||
|
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", pxfont(16), "OUTLINE")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Чеклист pixel-perfect переноса
|
||||||
|
|
||||||
|
- [ ] Холст Figma = 768px в высоту (или задан `PH.CANVAS_HEIGHT`)
|
||||||
|
- [ ] Все размеры и отступы пересчитаны через `px()`
|
||||||
|
- [ ] Цвета конвертированы через `color()`
|
||||||
|
- [ ] Текстуры нарезаны до 512×512, размеры кратны степени двойки
|
||||||
|
- [ ] Шрифты `.ttf` подключены из папки аддона (если кириллица или кастомный шрифт)
|
||||||
|
- [ ] Размеры шрифтов пересчитаны через `font()`
|
||||||
|
- [ ] Y-отступы от верхних якорей — **отрицательные**
|
||||||
|
- [ ] Проверено на scale UIParent ≠ 1.0 (изменить в настройках игры для теста)
|
||||||
|
- [ ] Текстуры без SetAllPoints — точный SetWidth/SetHeight + SetPoint
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```
|
||||||
|
MyAddon/
|
||||||
|
├── MyAddon.toc ← обязателен, имя файла = имя папки
|
||||||
|
├── MyAddon.xml ← верстка фреймов (опционально)
|
||||||
|
└── MyAddon.lua ← логика
|
||||||
|
```
|
||||||
|
|
||||||
|
### TOC-файл (3.3.5a)
|
||||||
|
|
||||||
|
```toc
|
||||||
|
## Interface: 30300
|
||||||
|
## Title: My Addon
|
||||||
|
## Notes: Описание аддона
|
||||||
|
## Author: Ilyas
|
||||||
|
## Version: 1.0
|
||||||
|
## SavedVariables: MyAddonDB
|
||||||
|
|
||||||
|
MyAddon.xml
|
||||||
|
MyAddon.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
> Файлы загружаются **в порядке перечисления**. XML раньше Lua — фреймы существуют к моменту выполнения скриптов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## XML-верстка
|
||||||
|
|
||||||
|
### Минимальный шаблон XML-файла
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Ui xmlns="http://www.blizzard.com/wow/ui/"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||||
|
|
||||||
|
<Script file="MyAddon.lua"/>
|
||||||
|
|
||||||
|
<Frame name="MyAddon_Frame" parent="UIParent" toplevel="true" enableMouse="true" movable="true">
|
||||||
|
<Size>
|
||||||
|
<AbsDimension x="300" y="200"/>
|
||||||
|
</Size>
|
||||||
|
<Anchors>
|
||||||
|
<Anchor point="CENTER"/>
|
||||||
|
</Anchors>
|
||||||
|
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
|
||||||
|
edgeFile="Interface\DialogFrame\UI-DialogBox-Border"
|
||||||
|
tile="true">
|
||||||
|
<BackgroundInsets>
|
||||||
|
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
||||||
|
</BackgroundInsets>
|
||||||
|
<TileSize><AbsValue val="32"/></TileSize>
|
||||||
|
<EdgeSize><AbsValue val="32"/></EdgeSize>
|
||||||
|
</Backdrop>
|
||||||
|
<Layers>
|
||||||
|
<Layer level="ARTWORK">
|
||||||
|
<FontString name="$parentTitle" inherits="GameFontNormalLarge" text="Заголовок">
|
||||||
|
<Anchors>
|
||||||
|
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
|
||||||
|
</Anchors>
|
||||||
|
</FontString>
|
||||||
|
</Layer>
|
||||||
|
</Layers>
|
||||||
|
<Frames>
|
||||||
|
<Button name="$parentClose" inherits="UIPanelCloseButton">
|
||||||
|
<Anchors>
|
||||||
|
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor>
|
||||||
|
</Anchors>
|
||||||
|
</Button>
|
||||||
|
</Frames>
|
||||||
|
<Scripts>
|
||||||
|
<OnLoad>MyAddon_Frame_OnLoad(self)</OnLoad>
|
||||||
|
<OnMouseDown>self:StartMoving()</OnMouseDown>
|
||||||
|
<OnMouseUp>self:StopMovingOrSizing()</OnMouseUp>
|
||||||
|
</Scripts>
|
||||||
|
</Frame>
|
||||||
|
|
||||||
|
</Ui>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Точки якоря (FRAMEPOINT)
|
||||||
|
|
||||||
|
| Точка | Расположение |
|
||||||
|
|-------|-------------|
|
||||||
|
| `TOPLEFT` | Верхний левый угол |
|
||||||
|
| `TOP` | Верхний центр |
|
||||||
|
| `TOPRIGHT` | Верхний правый угол |
|
||||||
|
| `LEFT` | Левый центр |
|
||||||
|
| `CENTER` | Центр |
|
||||||
|
| `RIGHT` | Правый центр |
|
||||||
|
| `BOTTOMLEFT` | Нижний левый угол |
|
||||||
|
| `BOTTOM` | Нижний центр |
|
||||||
|
| `BOTTOMRIGHT` | Нижний правый угол |
|
||||||
|
|
||||||
|
### SetPoint через Lua
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
|
||||||
|
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, -10)
|
||||||
|
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||||
|
frame:SetPoint("BOTTOMRIGHT", anotherFrame, "TOPRIGHT", 0, 5)
|
||||||
|
|
||||||
|
-- Сброс якорей перед переустановкой
|
||||||
|
frame:ClearAllPoints()
|
||||||
|
frame:SetPoint("CENTER", UIParent, "CENTER")
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Правило**: фрейм должен иметь достаточно якорей для однозначного определения позиции **и** размера. Либо Size + один якорь, либо два якоря задающих оба измерения.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Слои и страты
|
||||||
|
|
||||||
|
### Draw Layers (внутри фрейма, сверху вниз по видимости)
|
||||||
|
|
||||||
|
```
|
||||||
|
BACKGROUND (0) → BORDER (1) → ARTWORK (2) → OVERLAY (3) → HIGHLIGHT (4)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `HIGHLIGHT` — автоматически показывается при наведении мыши (требует `enableMouse="true"`)
|
||||||
|
- FontString всегда рисуется **поверх** Texture в том же слое
|
||||||
|
- Порядок текстур в одном слое **не гарантирован** → разноси по слоям
|
||||||
|
|
||||||
|
### Frame Strata (между фреймами)
|
||||||
|
|
||||||
|
```
|
||||||
|
WORLD → BACKGROUND → LOW → MEDIUM → HIGH → DIALOG → FULLSCREEN → FULLSCREEN_DIALOG → TOOLTIP
|
||||||
|
```
|
||||||
|
|
||||||
|
```lua
|
||||||
|
frame:SetFrameStrata("DIALOG")
|
||||||
|
frame:SetFrameLevel(5) -- внутри страты (0–255)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Виджеты, события, Blizzard API
|
||||||
|
|
||||||
|
> Полный справочник — в `references/widgets-and-api.md`.
|
||||||
|
> Читай его когда нужны: типы фреймов, Button/EditBox/ScrollFrame/StatusBar, события OnEvent/OnUpdate, SavedVariables, пути к текстурам, типичные ошибки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Шаблонный аддон «с нуля» (минимальный рабочий пример)
|
||||||
|
|
||||||
|
**MyAddon.toc**
|
||||||
|
```toc
|
||||||
|
## Interface: 30300
|
||||||
|
## Title: MyAddon
|
||||||
|
## SavedVariables: MyAddonDB
|
||||||
|
MyAddon.lua
|
||||||
|
```
|
||||||
|
|
||||||
|
**MyAddon.lua**
|
||||||
|
```lua
|
||||||
|
local ADDON_NAME = "MyAddon"
|
||||||
|
|
||||||
|
local f = CreateFrame("Frame", ADDON_NAME.."_Frame", UIParent)
|
||||||
|
f:SetWidth(250)
|
||||||
|
f:SetHeight(150)
|
||||||
|
f:SetPoint("CENTER")
|
||||||
|
f:SetFrameStrata("DIALOG")
|
||||||
|
f:SetMovable(true)
|
||||||
|
f:EnableMouse(true)
|
||||||
|
f:SetBackdrop({
|
||||||
|
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||||
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||||
|
tile = true, tileSize = 32, edgeSize = 32,
|
||||||
|
insets = {left=11, right=12, top=12, bottom=11}
|
||||||
|
})
|
||||||
|
f:Hide()
|
||||||
|
|
||||||
|
-- Заголовок
|
||||||
|
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||||
|
title:SetPoint("TOP", f, "TOP", 0, -12)
|
||||||
|
title:SetText("My Addon")
|
||||||
|
|
||||||
|
-- Закрытие
|
||||||
|
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
|
||||||
|
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 4, 4)
|
||||||
|
close:SetScript("OnClick", function() f:Hide() end)
|
||||||
|
|
||||||
|
-- Перетаскивание
|
||||||
|
f:SetScript("OnMouseDown", function(self, btn)
|
||||||
|
if btn == "LeftButton" then self:StartMoving() end
|
||||||
|
end)
|
||||||
|
f:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)
|
||||||
|
|
||||||
|
-- Инициализация
|
||||||
|
f:RegisterEvent("ADDON_LOADED")
|
||||||
|
f:SetScript("OnEvent", function(self, event, name)
|
||||||
|
if event == "ADDON_LOADED" and name == ADDON_NAME then
|
||||||
|
MyAddonDB = MyAddonDB or {}
|
||||||
|
print("|cff00ff00"..ADDON_NAME.."|r загружен. /myaddon")
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Слэш-команда
|
||||||
|
SLASH_MYADDON1 = "/myaddon"
|
||||||
|
SlashCmdList["MYADDON"] = function()
|
||||||
|
if f:IsShown() then f:Hide() else f:Show() end
|
||||||
|
end
|
||||||
|
```
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
# WoW 3.3.5a — Виджеты, события и Blizzard API
|
||||||
|
|
||||||
|
## Создание фреймов через Lua
|
||||||
|
|
||||||
|
### CreateFrame
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- CreateFrame(frameType, name, parent, template)
|
||||||
|
local f = CreateFrame("Frame", "MyAddon_MainFrame", UIParent)
|
||||||
|
f:SetWidth(300)
|
||||||
|
f:SetHeight(200)
|
||||||
|
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||||
|
f:SetFrameStrata("DIALOG")
|
||||||
|
f:SetBackdrop({
|
||||||
|
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||||
|
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||||
|
tile = true, tileSize = 32, edgeSize = 32,
|
||||||
|
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
||||||
|
})
|
||||||
|
f:SetBackdropColor(0, 0, 0, 0.8)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Типы фреймов (frameType)
|
||||||
|
|
||||||
|
| Тип | Назначение |
|
||||||
|
|-----|-----------|
|
||||||
|
| `Frame` | Базовый контейнер |
|
||||||
|
| `Button` | Кнопка |
|
||||||
|
| `EditBox` | Поле ввода текста |
|
||||||
|
| `ScrollFrame` | Прокручиваемая область |
|
||||||
|
| `StatusBar` | Полоска (HP, мана, XP) |
|
||||||
|
| `Cooldown` | Кулдаун-свип на иконке |
|
||||||
|
| `GameTooltip` | Тултип |
|
||||||
|
| `MessageFrame` | Прокручиваемые сообщения |
|
||||||
|
| `Model` | 3D-модель |
|
||||||
|
| `MovieFrame` | Видео |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Текстуры
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local tex = frame:CreateTexture(nil, "ARTWORK")
|
||||||
|
tex:SetTexture("Interface\\AddOns\\MyAddon\\images\\icon")
|
||||||
|
tex:SetAllPoints(frame)
|
||||||
|
tex:SetWidth(32)
|
||||||
|
tex:SetHeight(32)
|
||||||
|
tex:SetPoint("CENTER", frame, "CENTER")
|
||||||
|
tex:SetVertexColor(1, 0.5, 0, 1)
|
||||||
|
tex:SetAlpha(0.8)
|
||||||
|
|
||||||
|
-- Вырезка части текстуры
|
||||||
|
tex:SetTexCoord(0, 0.5, 0, 0.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
## FontString (текст)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||||
|
fs:SetPoint("CENTER", frame, "CENTER")
|
||||||
|
fs:SetText("Привет, Азерот!")
|
||||||
|
fs:SetTextColor(1, 1, 0, 1)
|
||||||
|
fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Стандартные шаблоны шрифтов
|
||||||
|
|
||||||
|
| Шаблон | Применение |
|
||||||
|
|--------|-----------|
|
||||||
|
| `GameFontNormal` | Обычный текст интерфейса |
|
||||||
|
| `GameFontNormalLarge` | Заголовки |
|
||||||
|
| `GameFontNormalSmall` | Мелкий текст |
|
||||||
|
| `GameFontHighlight` | Подсвеченный |
|
||||||
|
| `GameFontDisable` | Неактивный (серый) |
|
||||||
|
| `NumberFontNormal` | Числа |
|
||||||
|
| `ChatFontNormal` | Чат |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Button
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local btn = CreateFrame("Button", "MyAddon_Btn", frame, "UIPanelButtonTemplate")
|
||||||
|
btn:SetWidth(80)
|
||||||
|
btn:SetHeight(22)
|
||||||
|
btn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 10)
|
||||||
|
btn:SetText("Нажми")
|
||||||
|
btn:SetScript("OnClick", function(self)
|
||||||
|
print("Нажато!")
|
||||||
|
end)
|
||||||
|
|
||||||
|
btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-Button-Up")
|
||||||
|
btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-Button-Down")
|
||||||
|
btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаблоны кнопок
|
||||||
|
|
||||||
|
| Шаблон | Вид |
|
||||||
|
|--------|-----|
|
||||||
|
| `UIPanelButtonTemplate` | Стандартная кнопка интерфейса |
|
||||||
|
| `UIPanelCloseButton` | Крестик закрытия окна |
|
||||||
|
| `UIMenuButtonStretchTemplate` | Растягиваемая кнопка меню |
|
||||||
|
| `SecureActionButtonTemplate` | Для действий в бою (protected) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ScrollFrame
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local scroll = CreateFrame("ScrollFrame", "MyAddon_Scroll", frame, "UIPanelScrollFrameTemplate")
|
||||||
|
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
|
||||||
|
scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 8)
|
||||||
|
|
||||||
|
local child = CreateFrame("Frame", "MyAddon_ScrollChild", scroll)
|
||||||
|
child:SetWidth(scroll:GetWidth())
|
||||||
|
child:SetHeight(400)
|
||||||
|
scroll:SetScrollChild(child)
|
||||||
|
```
|
||||||
|
|
||||||
|
## EditBox
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local eb = CreateFrame("EditBox", "MyAddon_Input", frame, "InputBoxTemplate")
|
||||||
|
eb:SetWidth(200)
|
||||||
|
eb:SetHeight(20)
|
||||||
|
eb:SetPoint("CENTER", frame, "CENTER")
|
||||||
|
eb:SetAutoFocus(false)
|
||||||
|
eb:SetMaxLetters(255)
|
||||||
|
eb:SetScript("OnEnterPressed", function(self)
|
||||||
|
local text = self:GetText()
|
||||||
|
self:ClearFocus()
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
## StatusBar
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local bar = CreateFrame("StatusBar", nil, frame)
|
||||||
|
bar:SetWidth(200)
|
||||||
|
bar:SetHeight(16)
|
||||||
|
bar:SetPoint("CENTER", frame, "CENTER")
|
||||||
|
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
||||||
|
bar:SetStatusBarColor(0, 1, 0, 1)
|
||||||
|
bar:SetMinMaxValues(0, 100)
|
||||||
|
bar:SetValue(75)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Скрипты-события
|
||||||
|
|
||||||
|
```lua
|
||||||
|
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||||
|
frame:RegisterEvent("CHAT_MSG_SAY")
|
||||||
|
|
||||||
|
frame:SetScript("OnEvent", function(self, event, ...)
|
||||||
|
if event == "PLAYER_ENTERING_WORLD" then
|
||||||
|
-- вход в мир
|
||||||
|
elseif event == "CHAT_MSG_SAY" then
|
||||||
|
local msg, author = ...
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ключевые события
|
||||||
|
|
||||||
|
| Событие | Когда |
|
||||||
|
|---------|-------|
|
||||||
|
| `PLAYER_ENTERING_WORLD` | Вход в мир / reload |
|
||||||
|
| `ADDON_LOADED` | Аддон загружен (arg1 == имя) |
|
||||||
|
| `PLAYER_LOGIN` | После входа |
|
||||||
|
| `PLAYER_LOGOUT` | При выходе |
|
||||||
|
| `VARIABLES_LOADED` | SavedVariables загружены |
|
||||||
|
| `UI_ERROR_MESSAGE` | Ошибка интерфейса |
|
||||||
|
|
||||||
|
### OnUpdate — таймер
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local elapsed_total = 0
|
||||||
|
frame:SetScript("OnUpdate", function(self, elapsed)
|
||||||
|
elapsed_total = elapsed_total + elapsed
|
||||||
|
if elapsed_total >= 1.0 then
|
||||||
|
elapsed_total = 0
|
||||||
|
-- действие каждую секунду
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SavedVariables
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- В TOC: ## SavedVariables: MyAddonDB
|
||||||
|
frame:RegisterEvent("ADDON_LOADED")
|
||||||
|
frame:SetScript("OnEvent", function(self, event, addonName)
|
||||||
|
if event == "ADDON_LOADED" and addonName == "MyAddon" then
|
||||||
|
MyAddonDB = MyAddonDB or { position = { "CENTER", 0, 0 } }
|
||||||
|
local p = MyAddonDB.position
|
||||||
|
self:ClearAllPoints()
|
||||||
|
self:SetPoint(p[1], UIParent, p[1], p[2], p[3])
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
frame:SetScript("OnMouseUp", function(self)
|
||||||
|
self:StopMovingOrSizing()
|
||||||
|
local point, _, _, x, y = self:GetPoint()
|
||||||
|
MyAddonDB.position = { point, x, y }
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Полезные Blizzard-утилиты (3.3.5a)
|
||||||
|
|
||||||
|
```lua
|
||||||
|
_G["MyAddon_Frame"]:Show()
|
||||||
|
IsAddOnLoaded("MyAddon")
|
||||||
|
UnitName("player")
|
||||||
|
UnitClass("target")
|
||||||
|
UnitHealth("player"), UnitHealthMax("player")
|
||||||
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Красный текст|r")
|
||||||
|
print("Белый текст")
|
||||||
|
UnitAffectingCombat("player")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Часто используемые пути к текстурам Blizzard
|
||||||
|
|
||||||
|
```
|
||||||
|
Interface\DialogFrame\UI-DialogBox-Background
|
||||||
|
Interface\DialogFrame\UI-DialogBox-Border
|
||||||
|
Interface\DialogFrame\UI-DialogBox-Gold-Background
|
||||||
|
Interface\Tooltips\UI-Tooltip-Background
|
||||||
|
Interface\Tooltips\UI-Tooltip-Border
|
||||||
|
Interface\Buttons\UI-Panel-Button-Up
|
||||||
|
Interface\Buttons\UI-Panel-Button-Down
|
||||||
|
Interface\Icons\INV_Misc_QuestionMark
|
||||||
|
Interface\TargetingFrame\UI-StatusBar
|
||||||
|
Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Типичные ошибки и решения
|
||||||
|
|
||||||
|
| Ошибка | Причина | Решение |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| `attempt to index global 'X' (nil)` | Фрейм не создан | Проверь порядок файлов в TOC |
|
||||||
|
| `SetPoint anchor family connection` | Циклические якоря | Переписать цепочку якорей |
|
||||||
|
| `Script ran too long` | Бесконечный цикл OnUpdate | Early return или счётчик |
|
||||||
|
| Текстура не отображается | Размер не кратен 2 / >512px | Пересохрани в TGA/BLP |
|
||||||
|
| `bad argument #1 (string expected)` | nil вместо строки | `if val then` |
|
||||||
|
| Frame не кликается | Нет `enableMouse="true"` | Добавить атрибут |
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[ValidateSet('Debug', 'Release')]
|
||||||
|
[string]$Configuration = 'Release',
|
||||||
|
[string]$ClientPath = '',
|
||||||
|
[string]$PackagePath = '',
|
||||||
|
[switch]$Deploy,
|
||||||
|
[switch]$NativeRenderer
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$repoRoot = $PSScriptRoot
|
||||||
|
$cmake = 'C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $cmake)) {
|
||||||
|
$cmakeCommand = Get-Command cmake -ErrorAction SilentlyContinue
|
||||||
|
if (-not $cmakeCommand) { throw 'CMake 3.20+ was not found.' }
|
||||||
|
$cmake = $cmakeCommand.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
& git -C $repoRoot submodule update --init --recursive
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
|
||||||
|
|
||||||
|
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
|
||||||
|
$hostBuildDir = Join-Path $repoRoot 'build\warcraftxl-host-x64'
|
||||||
|
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
|
||||||
|
|
||||||
|
if ($Deploy -or -not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||||
|
if (-not (Test-Path -LiteralPath $stockExe -PathType Leaf)) {
|
||||||
|
throw "Stock executable was not found: $stockExe"
|
||||||
|
}
|
||||||
|
$expectedStockHash = 'AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8'
|
||||||
|
$stockHash = (Get-FileHash -LiteralPath $stockExe -Algorithm SHA256).Hash
|
||||||
|
if ($stockHash -ne $expectedStockHash) {
|
||||||
|
throw "Wow_Original.exe is not the verified MoonWell 3.3.5a stock executable (SHA-256 $stockHash)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& $cmake -S $repoRoot -B $win32BuildDir -A Win32 '-DCLIENT_PATH='
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 configure failed.' }
|
||||||
|
& $cmake --build $win32BuildDir --config $Configuration --target WarcraftXL d3d9 MoonWellLoader
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Win32 build failed.' }
|
||||||
|
|
||||||
|
& $cmake -S $repoRoot -B $hostBuildDir -A x64 '-DWXL_BUILD_HOST=ON' '-DCLIENT_PATH='
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host configure failed.' }
|
||||||
|
& $cmake --build $hostBuildDir --config $Configuration --target WarcraftXLHost
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL Host build failed.' }
|
||||||
|
|
||||||
|
$win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
|
||||||
|
$hostArtifactDir = Join-Path $hostBuildDir "vendor\warcraftxl\$Configuration"
|
||||||
|
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
|
||||||
|
$modernProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
|
||||||
|
$nativeProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
|
||||||
|
$hostExe = Join-Path $hostArtifactDir 'WarcraftXLHost.exe'
|
||||||
|
$selectedProxy = if ($NativeRenderer) { $nativeProxy } else { $modernProxy }
|
||||||
|
$adtResources = Join-Path $repoRoot 'vendor\modules\wxl-modern-adt\_resources'
|
||||||
|
|
||||||
|
function Install-WarcraftXLArtifacts {
|
||||||
|
param([Parameter(Mandatory)][string]$Destination)
|
||||||
|
|
||||||
|
$utils = Join-Path $Destination 'Utils'
|
||||||
|
$loosePatch = Join-Path $Destination 'Data\Patch-WXL.MPQ'
|
||||||
|
New-Item -ItemType Directory -Path $Destination, $utils, $loosePatch -Force | Out-Null
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $stockExe -Destination (Join-Path $Destination 'Wow.exe') -Force
|
||||||
|
Copy-Item -LiteralPath $warcraftXL -Destination (Join-Path $Destination 'WarcraftXL.dll') -Force
|
||||||
|
Copy-Item -LiteralPath $selectedProxy -Destination (Join-Path $Destination 'd3d9.dll') -Force
|
||||||
|
Copy-Item -LiteralPath $nativeProxy -Destination (Join-Path $utils 'd3d9-native.dll') -Force
|
||||||
|
Copy-Item -LiteralPath $hostExe -Destination (Join-Path $utils 'WarcraftXLHost.exe') -Force
|
||||||
|
Get-ChildItem -LiteralPath $adtResources | ForEach-Object {
|
||||||
|
Copy-Item -LiteralPath $_.FullName -Destination $loosePatch -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Deploy) {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||||
|
$ClientPath = 'C:\Program Files (x86)\World of Warcraft'
|
||||||
|
}
|
||||||
|
$ClientPath = [System.IO.Path]::GetFullPath($ClientPath)
|
||||||
|
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
|
||||||
|
throw "Client directory was not found: $ClientPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientExe = Join-Path $ClientPath 'Wow.exe'
|
||||||
|
if (Test-Path -LiteralPath $clientExe -PathType Leaf) {
|
||||||
|
$clientHash = (Get-FileHash -LiteralPath $clientExe -Algorithm SHA256).Hash
|
||||||
|
if ($clientHash -ne $stockHash) {
|
||||||
|
$legacyBackup = Join-Path $ClientPath 'Wow.moonwell-patched.backup.exe'
|
||||||
|
if (-not (Test-Path -LiteralPath $legacyBackup)) {
|
||||||
|
Copy-Item -LiteralPath $clientExe -Destination $legacyBackup
|
||||||
|
}
|
||||||
|
Write-Host "Legacy executable saved as $legacyBackup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Install-WarcraftXLArtifacts -Destination $ClientPath
|
||||||
|
Write-Host "Installed WarcraftXL runtime, host and modules in $ClientPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||||
|
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||||
|
Install-WarcraftXLArtifacts -Destination $PackagePath
|
||||||
|
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderer = if ($NativeRenderer) { 'native D3D9 recovery proxy' } else { 'WarcraftXL D3D9On12 proxy' }
|
||||||
|
Write-Host "WarcraftXL build complete ($renderer): $win32ArtifactDir"
|
||||||
@@ -21,6 +21,10 @@ IGNORED_DIRS_ANYWHERE = {
|
|||||||
".git",
|
".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,204 @@
|
|||||||
|
// 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/ModuleInstall.hpp"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace moonwell
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
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::modules::RegisterBoot("moonwell", &InstallBoot);
|
||||||
|
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
|
||||||
|
}
|
||||||
|
} g_registration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[ValidateSet("production", "ptr", "local")]
|
||||||
|
[string]$Env = "local"
|
||||||
|
)
|
||||||
|
|
||||||
# Stop on errors
|
# Stop on errors
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
@@ -36,6 +42,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 +66,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
|
||||||
@@ -70,6 +87,21 @@ if ($LASTEXITCODE -ge 8) {
|
|||||||
|
|
||||||
Write-Host "MPQ archives deployed to $WOW_HOME"
|
Write-Host "MPQ archives deployed to $WOW_HOME"
|
||||||
|
|
||||||
|
# --- Write realmlist.wtf based on selected environment
|
||||||
|
$realmlist = switch ($Env) {
|
||||||
|
"production" { $env:PRODUCTION_REALMLIST }
|
||||||
|
"ptr" { $env:PTR_REALMLIST }
|
||||||
|
"local" { $env:LOCAL_REALMLIST }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($realmlist) {
|
||||||
|
$realmlistPath = Join-Path $WOW_HOME "Data\ruRU\realmlist.wtf"
|
||||||
|
Set-Content -Path $realmlistPath -Value "set realmlist $realmlist" -Encoding ASCII
|
||||||
|
Write-Host "Realmlist ($Env): $realmlist"
|
||||||
|
} else {
|
||||||
|
Write-Warning "REALMLIST for '$Env' is not set in .env -- skipping realmlist.wtf"
|
||||||
|
}
|
||||||
|
|
||||||
# --- Run WoW reload script
|
# --- Run WoW reload script
|
||||||
Write-Host "Launching WoW..."
|
Write-Host "Launching WoW..."
|
||||||
cmd /c $RELOAD_SCRIPT
|
cmd /c $RELOAD_SCRIPT
|
||||||
|
|||||||
+107
-2
@@ -177,12 +177,75 @@ function EncounterJournal_InitTab(self)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function EncounterJournal_ScrollFrame_OnMouseWheel(scrollFrame, delta, isHybrid)
|
||||||
|
if not scrollFrame then
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
if isHybrid and HybridScrollFrame_OnMouseWheel then
|
||||||
|
HybridScrollFrame_OnMouseWheel(scrollFrame, delta);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
if ScrollFrameTemplate_OnMouseWheel then
|
||||||
|
ScrollFrameTemplate_OnMouseWheel(scrollFrame, delta);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local scrollBar = scrollFrame.ScrollBar or scrollFrame.scrollBar or _G[scrollFrame:GetName() .. "ScrollBar"];
|
||||||
|
if scrollBar then
|
||||||
|
local minValue, maxValue = scrollBar:GetMinMaxValues();
|
||||||
|
local step = scrollBar:GetValueStep() or 20;
|
||||||
|
scrollBar:SetValue(math.min(maxValue, math.max(minValue, scrollBar:GetValue() - delta * step)));
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
|
function EncounterJournal_OnMouseWheel(self, delta)
|
||||||
|
if PlayerGuideFrame and PlayerGuideFrame:IsVisible() and PlayerGuideFrame.BodyScroll and PlayerGuideFrame.BodyScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(PlayerGuideFrame.BodyScroll, delta);
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.searchResults and self.searchResults:IsVisible() and self.searchResults.scrollFrame and self.searchResults.scrollFrame:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(self.searchResults.scrollFrame, delta, true);
|
||||||
|
end
|
||||||
|
|
||||||
|
if self.suggestFrame and self.suggestFrame:IsVisible() then
|
||||||
|
EJSuggestFrame_OnMouseWheel(self.suggestFrame, delta);
|
||||||
|
return true;
|
||||||
|
end
|
||||||
|
|
||||||
|
local instanceSelect = self.instanceSelect;
|
||||||
|
if instanceSelect and instanceSelect:IsVisible() and instanceSelect.scroll and instanceSelect.scroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(instanceSelect.scroll, delta);
|
||||||
|
end
|
||||||
|
|
||||||
|
local info = self.encounter and self.encounter.info;
|
||||||
|
if info and info:IsVisible() then
|
||||||
|
if info.lootScroll and info.lootScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.lootScroll, delta, true);
|
||||||
|
elseif info.detailsScroll and info.detailsScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.detailsScroll, delta);
|
||||||
|
elseif info.overviewScroll and info.overviewScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.overviewScroll, delta);
|
||||||
|
elseif info.bossesScroll and info.bossesScroll:IsShown() then
|
||||||
|
return EncounterJournal_ScrollFrame_OnMouseWheel(info.bossesScroll, delta);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return false;
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_OnLoad(self)
|
function EncounterJournal_OnLoad(self)
|
||||||
EncounterJournalTitleText:SetText(ADVENTURE_JOURNAL);
|
EncounterJournalTitleText:SetText(ADVENTURE_JOURNAL);
|
||||||
SetPortraitToTexture(EncounterJournalPortrait, "Interface\\EncounterJournal\\UI-EJ-PortraitIcon");
|
SetPortraitToTexture(EncounterJournalPortrait, "Interface\\EncounterJournal\\UI-EJ-PortraitIcon");
|
||||||
self:RegisterCustomEvent("EJ_LOOT_DATA_RECIEVED");
|
self:RegisterCustomEvent("EJ_LOOT_DATA_RECIEVED");
|
||||||
self:RegisterCustomEvent("EJ_DIFFICULTY_UPDATE");
|
self:RegisterCustomEvent("EJ_DIFFICULTY_UPDATE");
|
||||||
self:RegisterCustomEvent("SEARCH_DB_LOADED");
|
self:RegisterCustomEvent("SEARCH_DB_LOADED");
|
||||||
|
self:EnableMouseWheel(true);
|
||||||
|
self:SetScript("OnMouseWheel", EncounterJournal_OnMouseWheel);
|
||||||
|
|
||||||
do
|
do
|
||||||
SetParentFrameLevel(self.inset)
|
SetParentFrameLevel(self.inset)
|
||||||
@@ -1149,6 +1212,30 @@ local toggleTempList = {};
|
|||||||
local headerCount = 0;
|
local headerCount = 0;
|
||||||
local loopedSections = {};
|
local loopedSections = {};
|
||||||
|
|
||||||
|
local function EncounterJournal_GetHeaderWidth(sourceFrame)
|
||||||
|
local width = sourceFrame and sourceFrame:GetWidth() or 0;
|
||||||
|
if width and width > 20 then
|
||||||
|
return width;
|
||||||
|
end
|
||||||
|
|
||||||
|
local info = EncounterJournal and EncounterJournal.encounter and EncounterJournal.encounter.info;
|
||||||
|
local scrollFrame = info and ((info.detailsScroll and info.detailsScroll:IsShown() and info.detailsScroll) or
|
||||||
|
(info.overviewScroll and info.overviewScroll:IsShown() and info.overviewScroll) or info.detailsScroll or info.overviewScroll);
|
||||||
|
local child = scrollFrame and (scrollFrame.child or scrollFrame.ScrollChild);
|
||||||
|
|
||||||
|
width = child and child:GetWidth() or 0;
|
||||||
|
if width and width > 20 then
|
||||||
|
return width;
|
||||||
|
end
|
||||||
|
|
||||||
|
width = scrollFrame and scrollFrame:GetWidth() or 0;
|
||||||
|
if width and width > 20 then
|
||||||
|
return math.max(1, width - 30);
|
||||||
|
end
|
||||||
|
|
||||||
|
return 320;
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_UpdateButtonState(self)
|
function EncounterJournal_UpdateButtonState(self)
|
||||||
local oldtex = self.textures.expanded;
|
local oldtex = self.textures.expanded;
|
||||||
if self:GetParent().expanded then
|
if self:GetParent().expanded then
|
||||||
@@ -1178,6 +1265,22 @@ function EncounterJournal_UpdateButtonState(self)
|
|||||||
self.tex.down[3]:Hide();
|
self.tex.down[3]:Hide();
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function EncounterJournal_NormalizeHeaderButton(infoHeader, width)
|
||||||
|
if not infoHeader or not infoHeader.button then
|
||||||
|
return;
|
||||||
|
end
|
||||||
|
|
||||||
|
width = width or EncounterJournal_GetHeaderWidth(infoHeader);
|
||||||
|
infoHeader:SetWidth(width);
|
||||||
|
infoHeader.button:SetWidth(width);
|
||||||
|
|
||||||
|
if infoHeader.button:GetButtonState() ~= "NORMAL" then
|
||||||
|
infoHeader.button:SetButtonState("NORMAL");
|
||||||
|
end
|
||||||
|
|
||||||
|
EncounterJournal_UpdateButtonState(infoHeader.button);
|
||||||
|
end
|
||||||
|
|
||||||
function EncounterJournal_OnClick(self)
|
function EncounterJournal_OnClick(self)
|
||||||
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow() then
|
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow() then
|
||||||
if self.link then
|
if self.link then
|
||||||
@@ -1471,6 +1574,7 @@ function EncounterJournal_SetUpOverview(self, role, index)
|
|||||||
infoHeader.button.link = link;
|
infoHeader.button.link = link;
|
||||||
infoHeader.sectionID = nextSectionID;
|
infoHeader.sectionID = nextSectionID;
|
||||||
|
|
||||||
|
EncounterJournal_NormalizeHeaderButton(infoHeader, EncounterJournal_GetHeaderWidth(self));
|
||||||
infoHeader.overviewDescription:SetWidth(infoHeader:GetWidth() - 20);
|
infoHeader.overviewDescription:SetWidth(infoHeader:GetWidth() - 20);
|
||||||
EncounterJournal_SetDescriptionWithBullets(infoHeader, description);
|
EncounterJournal_SetDescriptionWithBullets(infoHeader, description);
|
||||||
infoHeader:Show();
|
infoHeader:Show();
|
||||||
@@ -1479,7 +1583,7 @@ end
|
|||||||
function EncounterJournal_ToggleHeaders(self, doNotShift)
|
function EncounterJournal_ToggleHeaders(self, doNotShift)
|
||||||
local numAdded = 0;
|
local numAdded = 0;
|
||||||
local infoHeader, parentID, _;
|
local infoHeader, parentID, _;
|
||||||
local hWidth = self:GetWidth();
|
local hWidth = EncounterJournal_GetHeaderWidth(self);
|
||||||
local nextSectionID;
|
local nextSectionID;
|
||||||
local topLevelSection = false;
|
local topLevelSection = false;
|
||||||
|
|
||||||
@@ -1703,7 +1807,7 @@ function EncounterJournal_ToggleHeaders(self, doNotShift)
|
|||||||
end
|
end
|
||||||
|
|
||||||
infoHeader.index = nil;
|
infoHeader.index = nil;
|
||||||
infoHeader:SetWidth(hWidth);
|
EncounterJournal_NormalizeHeaderButton(infoHeader, hWidth);
|
||||||
EncounterJournal_SetHeaderDescription(infoHeader, description);
|
EncounterJournal_SetHeaderDescription(infoHeader, description);
|
||||||
|
|
||||||
-- If this section has not be seen and should start open
|
-- If this section has not be seen and should start open
|
||||||
@@ -1717,6 +1821,7 @@ function EncounterJournal_ToggleHeaders(self, doNotShift)
|
|||||||
numAdded = numAdded + EncounterJournal_ToggleHeaders(infoHeader, true);
|
numAdded = numAdded + EncounterJournal_ToggleHeaders(infoHeader, true);
|
||||||
end
|
end
|
||||||
|
|
||||||
|
EncounterJournal_NormalizeHeaderButton(infoHeader, hWidth);
|
||||||
infoHeader:Show();
|
infoHeader:Show();
|
||||||
end -- if not filteredByDifficulty
|
end -- if not filteredByDifficulty
|
||||||
|
|
||||||
|
|||||||
+131
-10
@@ -68,6 +68,22 @@ local function updateScrollBar(scrollFrame, contentHeight, contentWidth)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function scrollFrameOnMouseWheel(scrollFrame, delta)
|
||||||
|
if not scrollFrame then return end
|
||||||
|
|
||||||
|
if ScrollFrameTemplate_OnMouseWheel then
|
||||||
|
ScrollFrameTemplate_OnMouseWheel(scrollFrame, delta)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local scrollBar = getScrollBar(scrollFrame)
|
||||||
|
if scrollBar then
|
||||||
|
local minValue, maxValue = scrollBar:GetMinMaxValues()
|
||||||
|
local step = scrollBar:GetValueStep() or 20
|
||||||
|
scrollBar:SetValue(math.min(maxValue, math.max(minValue, scrollBar:GetValue() - delta * step)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function hideScrollBar(scrollFrame)
|
local function hideScrollBar(scrollFrame)
|
||||||
local scrollBar = getScrollBar(scrollFrame)
|
local scrollBar = getScrollBar(scrollFrame)
|
||||||
if scrollBar then scrollBar:Hide() end
|
if scrollBar then scrollBar:Hide() end
|
||||||
@@ -78,6 +94,24 @@ local function getBodyChild()
|
|||||||
return body and body.ScrollChild
|
return body and body.ScrollChild
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local resolveEncounterJournalInstanceID
|
||||||
|
|
||||||
|
local function getRecommendationArtwork(data)
|
||||||
|
if type(data) ~= "table" then return nil end
|
||||||
|
|
||||||
|
local artwork = data.artwork or data.cover or data.coverImage or data.buttonImage or data.bgImage or data.icon
|
||||||
|
if artwork and artwork ~= "" then return artwork end
|
||||||
|
|
||||||
|
if not resolveEncounterJournalInstanceID or not EJ_GetInstanceInfo then return nil end
|
||||||
|
local instanceID = resolveEncounterJournalInstanceID(data)
|
||||||
|
if not instanceID then return nil end
|
||||||
|
|
||||||
|
local ok, _, _, bgImage, buttonImage, loreImage, buttonSmallImage = pcall(EJ_GetInstanceInfo, instanceID)
|
||||||
|
if ok then
|
||||||
|
return buttonImage or bgImage or loreImage or buttonSmallImage
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- ─────────────────────────────────── Цепочка этапов ──
|
-- ─────────────────────────────────── Цепочка этапов ──
|
||||||
|
|
||||||
local stageBadges = {}
|
local stageBadges = {}
|
||||||
@@ -248,7 +282,12 @@ local function renderRecommendations()
|
|||||||
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
|
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local artwork = getRecommendationArtwork(r)
|
||||||
|
card.Artwork:SetTexCoord(0, 0.68359375, 0, 0.7421875)
|
||||||
|
card.Artwork:SetVertexColor(1, 1, 1, 1)
|
||||||
|
if not artwork or not card.Artwork:SetTexture(artwork) then
|
||||||
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
|
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
|
||||||
|
end
|
||||||
card.Name:SetText(r.name or r.title or "")
|
card.Name:SetText(r.name or r.title or "")
|
||||||
card.Loc:SetText(r.loc or r.description or "")
|
card.Loc:SetText(r.loc or r.description or "")
|
||||||
card.Level:SetText(r.level or "")
|
card.Level:SetText(r.level or "")
|
||||||
@@ -368,6 +407,8 @@ end
|
|||||||
|
|
||||||
function MW_PlayerGuide_OnLoad(self)
|
function MW_PlayerGuide_OnLoad(self)
|
||||||
EncounterJournal.playerGuideFrame = self
|
EncounterJournal.playerGuideFrame = self
|
||||||
|
self:EnableMouse(true)
|
||||||
|
self:EnableMouseWheel(true)
|
||||||
|
|
||||||
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
|
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
|
||||||
if bodyChild then
|
if bodyChild then
|
||||||
@@ -386,6 +427,12 @@ function MW_PlayerGuide_OnLoad(self)
|
|||||||
C_PlayerGuide.RegisterListener(function() refresh() end)
|
C_PlayerGuide.RegisterListener(function() refresh() end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function MW_PlayerGuide_OnMouseWheel(self, delta)
|
||||||
|
if self.BodyScroll and self.BodyScroll:IsShown() then
|
||||||
|
scrollFrameOnMouseWheel(self.BodyScroll, delta)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function MW_PlayerGuide_OnShow()
|
function MW_PlayerGuide_OnShow()
|
||||||
if not C_PlayerGuide.HasData() then
|
if not C_PlayerGuide.HasData() then
|
||||||
C_PlayerGuide.SetLoading()
|
C_PlayerGuide.SetLoading()
|
||||||
@@ -399,16 +446,94 @@ function MW_PlayerGuide_RefreshButton_OnClick()
|
|||||||
C_PlayerGuide.RequestRefresh()
|
C_PlayerGuide.RequestRefresh()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function validEncounterJournalInstanceID(value)
|
||||||
|
local instanceID = tonumber(value)
|
||||||
|
if not instanceID or instanceID <= 0 or not EJ_GetInstanceInfo then return nil end
|
||||||
|
|
||||||
|
local ok, instanceName = pcall(EJ_GetInstanceInfo, instanceID)
|
||||||
|
if ok and instanceName then return instanceID end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function mapIDToEncounterJournalInstanceID(value)
|
||||||
|
local mapID = tonumber(value)
|
||||||
|
if not mapID or mapID <= 0 then return nil end
|
||||||
|
if not C_EncounterJournal or not C_EncounterJournal.GetInstanceIDByMapID then return nil end
|
||||||
|
|
||||||
|
local ok, instanceID = pcall(C_EncounterJournal.GetInstanceIDByMapID, mapID)
|
||||||
|
if ok then
|
||||||
|
return validEncounterJournalInstanceID(instanceID)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function resolveEncounterJournalInstanceID(data)
|
||||||
|
if type(data) == "number" or type(data) == "string" then
|
||||||
|
return validEncounterJournalInstanceID(data) or mapIDToEncounterJournalInstanceID(data)
|
||||||
|
end
|
||||||
|
if type(data) ~= "table" then return nil end
|
||||||
|
|
||||||
|
local instanceID =
|
||||||
|
validEncounterJournalInstanceID(data.ej_instanceID) or
|
||||||
|
validEncounterJournalInstanceID(data.ejInstanceID) or
|
||||||
|
validEncounterJournalInstanceID(data.journalInstanceID) or
|
||||||
|
validEncounterJournalInstanceID(data.instanceID) or
|
||||||
|
validEncounterJournalInstanceID(data.dungeonID) or
|
||||||
|
validEncounterJournalInstanceID(data.targetID)
|
||||||
|
if instanceID then return instanceID end
|
||||||
|
|
||||||
|
return mapIDToEncounterJournalInstanceID(data.mapID) or
|
||||||
|
mapIDToEncounterJournalInstanceID(data.worldMapAreaID) or
|
||||||
|
mapIDToEncounterJournalInstanceID(data.instanceID) or
|
||||||
|
mapIDToEncounterJournalInstanceID(data.dungeonID) or
|
||||||
|
mapIDToEncounterJournalInstanceID(data.targetID)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function requestOpenTarget(data)
|
||||||
|
if type(data) == "table" and data.id and C_PlayerGuide and C_PlayerGuide.RequestOpenTarget then
|
||||||
|
C_PlayerGuide.RequestOpenTarget(data.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function openEncounterJournalInstance(data, encounterID)
|
||||||
|
if not EncounterJournal then return nil end
|
||||||
|
|
||||||
|
local instanceID = resolveEncounterJournalInstanceID(data)
|
||||||
|
if not instanceID then
|
||||||
|
requestOpenTarget(data)
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if type(data) == "table" and not encounterID then
|
||||||
|
encounterID = data.encounterID
|
||||||
|
end
|
||||||
|
|
||||||
|
if not EncounterJournal:IsShown() then
|
||||||
|
ShowUIPanel(EncounterJournal)
|
||||||
|
end
|
||||||
|
|
||||||
|
if EncounterJournal.encounter then
|
||||||
|
EncounterJournal.encounter:Show()
|
||||||
|
end
|
||||||
|
if EncounterJournal.instanceSelect then
|
||||||
|
EJ_ContentTab_SelectAppropriateInstanceTab(instanceID)
|
||||||
|
end
|
||||||
|
|
||||||
|
if NavBar_Reset and EncounterJournal.navBar then
|
||||||
|
NavBar_Reset(EncounterJournal.navBar)
|
||||||
|
end
|
||||||
|
|
||||||
|
EncounterJournal_DisplayInstance(instanceID)
|
||||||
|
|
||||||
|
if encounterID and encounterID > 0 then
|
||||||
|
EncounterJournal_DisplayEncounter(encounterID)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function MW_PlayerGuide_Objective_OnClick(self)
|
function MW_PlayerGuide_Objective_OnClick(self)
|
||||||
local o = self.objectiveData
|
local o = self.objectiveData
|
||||||
if not o then return end
|
if not o then return end
|
||||||
|
|
||||||
if o.type == "dungeon" or o.type == "raid" then
|
if o.type == "dungeon" or o.type == "raid" then
|
||||||
if o.instanceID and o.instanceID > 0 then
|
openEncounterJournalInstance(o)
|
||||||
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
|
||||||
EJ_ContentTab_SelectAppropriateInstanceTab(o.instanceID)
|
|
||||||
EncounterJournal_DisplayInstance(o.instanceID)
|
|
||||||
end
|
|
||||||
elseif o.type == "boss" then
|
elseif o.type == "boss" then
|
||||||
if o.encounterID and o.encounterID > 0 then
|
if o.encounterID and o.encounterID > 0 then
|
||||||
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
||||||
@@ -457,11 +582,7 @@ function MW_PlayerGuide_Objective_OnLeave() GameTooltip:Hide() end
|
|||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnClick(self)
|
function MW_PlayerGuide_Rec_OnClick(self)
|
||||||
local r = self.recData
|
local r = self.recData
|
||||||
if r and r.instanceID and r.instanceID > 0 then
|
if r then openEncounterJournalInstance(r) end
|
||||||
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
|
|
||||||
EJ_ContentTab_SelectAppropriateInstanceTab(r.instanceID)
|
|
||||||
EncounterJournal_DisplayInstance(r.instanceID)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function MW_PlayerGuide_Rec_OnEnter(self)
|
function MW_PlayerGuide_Rec_OnEnter(self)
|
||||||
|
|||||||
+12
-8
@@ -133,16 +133,19 @@
|
|||||||
<Layers>
|
<Layers>
|
||||||
<Layer level="ARTWORK">
|
<Layer level="ARTWORK">
|
||||||
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
|
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
|
||||||
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
|
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8" setAllPoints="true">
|
||||||
<Size><AbsDimension x="168" y="72"/></Size>
|
<Color r="1" g="1" b="1"/>
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
<Color r="0.55" g="0.16" b="0.07"/>
|
|
||||||
</Texture>
|
</Texture>
|
||||||
</Layer>
|
</Layer>
|
||||||
<Layer level="OVERLAY">
|
<Layer level="OVERLAY">
|
||||||
<!-- Level: top-right corner of artwork (artwork starts at y=-5 → level at TOPRIGHT+(-4,-9)) -->
|
<!-- Level: top-right corner of artwork (artwork starts at y=-5 → level at TOPRIGHT+(-4,-9)) -->
|
||||||
|
<Texture name="$parentTextBackdrop" parentKey="TextBackdrop" file="Interface\Buttons\WHITE8X8">
|
||||||
|
<Size><AbsDimension x="176" y="42"/></Size>
|
||||||
|
<Anchors>
|
||||||
|
<Anchor point="BOTTOM"/>
|
||||||
|
</Anchors>
|
||||||
|
<Color r="0" g="0" b="0" a="0.72"/>
|
||||||
|
</Texture>
|
||||||
<FontString name="$parentLevel" parentKey="Level" inherits="GameFontNormalSmall" justifyH="RIGHT">
|
<FontString name="$parentLevel" parentKey="Level" inherits="GameFontNormalSmall" justifyH="RIGHT">
|
||||||
<Size><AbsDimension x="80" y="12"/></Size>
|
<Size><AbsDimension x="80" y="12"/></Size>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
@@ -154,7 +157,7 @@
|
|||||||
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
|
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
|
||||||
<Size><AbsDimension x="162" y="16"/></Size>
|
<Size><AbsDimension x="162" y="16"/></Size>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-80"/></Offset></Anchor>
|
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-82"/></Offset></Anchor>
|
||||||
</Anchors>
|
</Anchors>
|
||||||
<Color r="1.0" g="0.91" b="0.63"/>
|
<Color r="1.0" g="0.91" b="0.63"/>
|
||||||
</FontString>
|
</FontString>
|
||||||
@@ -162,7 +165,7 @@
|
|||||||
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
|
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
|
||||||
<Size><AbsDimension x="162" y="16"/></Size>
|
<Size><AbsDimension x="162" y="16"/></Size>
|
||||||
<Anchors>
|
<Anchors>
|
||||||
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-96"/></Offset></Anchor>
|
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-98"/></Offset></Anchor>
|
||||||
</Anchors>
|
</Anchors>
|
||||||
</FontString>
|
</FontString>
|
||||||
</Layer>
|
</Layer>
|
||||||
@@ -480,6 +483,7 @@
|
|||||||
<Scripts>
|
<Scripts>
|
||||||
<OnLoad function="MW_PlayerGuide_OnLoad"/>
|
<OnLoad function="MW_PlayerGuide_OnLoad"/>
|
||||||
<OnShow function="MW_PlayerGuide_OnShow"/>
|
<OnShow function="MW_PlayerGuide_OnShow"/>
|
||||||
|
<OnMouseWheel function="MW_PlayerGuide_OnMouseWheel"/>
|
||||||
</Scripts>
|
</Scripts>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
|
|||||||
+17
@@ -313,6 +313,11 @@ function L.GetObjectiveInfo(index)
|
|||||||
questID = o.questID,
|
questID = o.questID,
|
||||||
questChainID = o.questChainID,
|
questChainID = o.questChainID,
|
||||||
instanceID = o.instanceID,
|
instanceID = o.instanceID,
|
||||||
|
ej_instanceID = o.ej_instanceID or o.ejInstanceID,
|
||||||
|
journalInstanceID = o.journalInstanceID,
|
||||||
|
mapID = o.mapID,
|
||||||
|
worldMapAreaID = o.worldMapAreaID,
|
||||||
|
dungeonID = o.dungeonID,
|
||||||
encounterID = o.encounterID,
|
encounterID = o.encounterID,
|
||||||
achievementID = o.achievementID,
|
achievementID = o.achievementID,
|
||||||
factionID = o.factionID,
|
factionID = o.factionID,
|
||||||
@@ -342,7 +347,19 @@ function L.GetRecommendationInfo(index)
|
|||||||
loc = r.loc,
|
loc = r.loc,
|
||||||
level = r.level,
|
level = r.level,
|
||||||
biome = r.biome,
|
biome = r.biome,
|
||||||
|
artwork = r.artwork,
|
||||||
|
cover = r.cover,
|
||||||
|
coverImage = r.coverImage,
|
||||||
|
bgImage = r.bgImage,
|
||||||
|
buttonImage = r.buttonImage,
|
||||||
|
icon = r.icon,
|
||||||
instanceID = r.instanceID,
|
instanceID = r.instanceID,
|
||||||
|
ej_instanceID = r.ej_instanceID or r.ejInstanceID,
|
||||||
|
journalInstanceID = r.journalInstanceID,
|
||||||
|
mapID = r.mapID,
|
||||||
|
worldMapAreaID = r.worldMapAreaID,
|
||||||
|
targetID = r.targetID,
|
||||||
|
dungeonID = r.dungeonID,
|
||||||
encounterID = r.encounterID,
|
encounterID = r.encounterID,
|
||||||
priority = r.priority or 0,
|
priority = r.priority or 0,
|
||||||
}
|
}
|
||||||
|
|||||||
+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