5 Commits

678 changed files with 1447 additions and 249471 deletions
+5 -1
View File
@@ -5,4 +5,8 @@ AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
AWS_BUCKET=
AWS_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=
AWS_USE_PATH_STYLE_ENDPOINT=
PRODUCTION_REALMLIST=
PTR_REALMLIST=
LOCAL_REALMLIST=
+5 -1
View File
@@ -3,4 +3,8 @@ __pycache__/
manifest.json
.claude
.vscode
dist
dist
build/
Wow*.exe
*.backup.exe
Logs/
+15
View File
@@ -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
+71
View File
@@ -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()
Binary file not shown.
+115 -62
View File
@@ -1,90 +1,143 @@
# MoonWell Client Sources
Репозиторий с ресурсами и исполняемым файлом модифицированного клиента World of Warcraft 3.3.5a.
Исходники клиентских изменений MoonWell для World of Warcraft 3.3.5a (build 12340).
Основная цель проекта: хранить и сопровождать клиентские изменения поверх стандартных MPQ-пакетов WoW, включая интерфейс, графику, звук и дополнительные данные для кастомных систем сервера.
Клиент переведён на [WarcraftXL](https://github.com/WarcraftXL): `Wow.exe` больше не содержит
MoonWell-патчей. Неподписанный оригинальный executable загружается как обычно, локальный
`d3d9.dll` proxy подхватывает `WarcraftXL.dll`, а модуль MoonWell применяет проверенные изменения
только к памяти запущенного процесса.
## Что находится в репозитории
## Что перенесено из модифицированного Wow.exe
- `Wow.exe` - модифицированный клиентский исполняемый файл.
- Поддерживает использование до 4 ГБ ОЗУ.
- Игнорирует блокирование изменений в исходниках `GlueXML`, что позволяет вносить свои изменения в интерфейс и модели.
- `patch-4/` - наши изменения поверх стандартного `patch-4.MPQ`.
- Содержит графические элементы внутриигрового магазина.
- Основная зона изменений: `Interface/Store_UI`.
- `patch-ruRU-4/` - наши изменения поверх стандартных `patch-ruRU-1.MPQ`, `patch-ruRU-2.MPQ` и `patch-ruRU-3.MPQ`.
- Содержит ресурсы локали `ruRU`, связанные с экраном входа, `GlueXML`, заставками и звуком.
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/LoginScreen`, `Sound/Ambience`, `Sound/Music`.
- `patch-ruRU-5/` - пакет русской локализации и дополнительных интерфейсных ресурсов.
- Содержит перевод на русский язык для glue-интерфейса и связанных экранов клиента.
- Внутри есть `Interface/GlueXML`, `Interface/Glues`, `Interface/GluesVideo`, `Interface/Loginscreen`, `Interface/tooltips`, `Interface/cinematics`.
- `patch-Z/` - кастомный пакет для модуля Mythic+.
- Содержит графические ресурсы Mythic+.
- Также включает связанные клиентские данные в `DBFielsClient/` и иконки в `Interface/Icons/`.
- разблокировка пользовательского `GlueXML`;
- `CreateCharacter(name, modeId)` и поле режима в `CMSG_CHAR_CREATE`;
- одиннадцатое значение `GetCharacterInfo``charFlags`, включая флаг предателя `0x40000000`;
- загрузка WarcraftXL без import-table patch и без отдельного injector.
## Структура по назначению
Large Address Aware намеренно не включён: Windows читает этот PE-флаг до загрузки DLL, поэтому
сохранить его и одновременно оставить файл байт-в-байт оригинальным невозможно. Это означает
стандартный 2-ГБ лимит адресного пространства для 32-битного клиента.
### Внутриигровой магазин
## Структура
Файлы магазина находятся в [`patch-4/Interface/Store_UI`](./patch-4/Interface/Store_UI).
```text
modules/moonwell/ MoonWell runtime-модуль WarcraftXL
vendor/warcraftxl/ закреплённый upstream git submodule
vendor/modules/ закреплённые модули WarcraftXL
src/Data/ исходники MPQ-патчей
tool/ сборщик MPQ
build-warcraftxl.ps1 сборка Win32 runtime/proxy и x64 asset host
run.ps1 полная сборка, установка и запуск клиента
Wow_Original.exe локальный оригинал build 12340 (не хранится в Git)
```
Примеры ресурсов:
Подключены модули `wxl-modern-assets`, `wxl-modern-render`, `wxl-unit-outline` и
`wxl-modern-adt`. Их точные ревизии вместе с ревизией ядра закреплены git submodule.
Для обновления всех зависимостей WarcraftXL:
- `Currencies/Gold.blp`
- `Currencies/Token.blp`
- `Frames/StoreFrame_Main.blp`
```powershell
git submodule update --remote vendor/warcraftxl vendor/modules/*
```
### Экран входа и локализованные ресурсы
После обновления обязательно пересоберите и проверьте вход/создание персонажа: API WarcraftXL
пока развивается.
Файлы экрана входа и связанные ресурсы находятся в [`patch-ruRU-4`](./patch-ruRU-4).
## Требования
Основные каталоги:
- Visual Studio 2022 с C++ toolchain для Win32 и x64;
- CMake 3.20+;
- Rust/Cargo для существующего MPQ-сборщика;
- локальный оригинальный `Wow_Original.exe` с SHA-256
`AA63A5750D60EF16746C686B3D5E26876D98953EAB08B1C026CD0FAF78E88CB8`.
- `Interface/GlueXML` - Lua/XML-исходники интерфейса загрузки и логина
- `Interface/Glues` - графика и модели для glue-экрана
- `Interface/LoginScreen` - фон и связанные изображения
- `Sound/Ambience/GlueScreen` - фоновые звуки
- `Sound/Music/GlueScreenMusic` - музыка экрана входа
После клонирования инициализируйте submodule:
### Русский перевод для `patch-ruRU-5`
```powershell
git submodule update --init --recursive
```
Файлы русской локализации и расширенного glue-интерфейса находятся в [`patch-ruRU-5`](./patch-ruRU-5).
## Сборка WarcraftXL
Основные каталоги:
Быстрое полное развёртывание клиента одной командой:
- `Interface/GlueXML` - Lua/XML-исходники экранов логина, выбора реалма, выбора и создания персонажа
- `Interface/Glues` - текстуры, кнопки, логотипы и элементы интерфейса для `CharacterCreate` и `CharacterSelect`
- `Interface/GluesVideo` - видеоресурсы и ассеты glue-экранов
- `Interface/Loginscreen` - фон и изображения экрана входа
- `Interface/tooltips` - локализованные рамки и оформление тултипов
- `Interface/cinematics` - элементы интерфейса для роликов и заставок
```powershell
.\deploy.ps1
```
Этот пакет используется как слой русификации для дополнительных экранов клиента и их визуальных элементов.
Скрипт инициализирует зависимости, собирает MPQ-патчи, Win32 runtime, D3D9 proxy и x64 Host,
обновляет `dist`, устанавливает комплект в клиент и формирует `manifest.json`. Путь берётся из
`WOW_HOME`/`.env`, а при их отсутствии используется `C:\Program Files (x86)\World of Warcraft`.
### Mythic+
Полезные варианты:
Файлы Mythic+ находятся в [`patch-Z`](./patch-Z).
```powershell
# Явный путь и запуск клиента после установки
.\deploy.ps1 -ClientPath 'D:\Games\World of Warcraft' -StopClient -Launch
Основные каталоги:
# Быстро обновить только WarcraftXL без пересборки MPQ
.\deploy.ps1 -SkipDataBuild
- `Interface/MythicPlus/textures` - текстуры интерфейса Mythic+
- `Interface/MythicPlus/sounds` - звуки таймеров и событий
- `Interface/Icons` - дополнительные иконки
- `DBFielsClient` - клиентские DBC-данные, связанные с отображением предметов и интерфейсом
# Аварийный native D3D9 вместо D3D9On12
.\deploy.ps1 -NativeRenderer
```
## Как воспринимать содержимое репозитория
Без установки в клиент:
- Каталоги `patch-*` представляют содержимое соответствующих MPQ-патчей в распакованном виде.
- Названия каталогов соответствуют именам клиентских патч-пакетов, поверх которых вносятся наши изменения.
- Репозиторий не является полной копией клиента WoW, а только набором модифицированных клиентских ресурсов и исполняемого файла.
```powershell
.\build-warcraftxl.ps1 -Configuration Release
```
## Для чего используется
С установкой:
Проект нужен для сопровождения клиентской части MoonWell:
```powershell
.\build-warcraftxl.ps1 -Configuration Release `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
- кастомизации GlueXML;
- поддержки русского перевода для `patch-ruRU-5`;
- поддержки внутриигрового магазина;
- добавления графики и данных для Mythic+;
- хранения пакетов графических улучшений;
- работы с модифицированным `Wow.exe` для WoW 3.3.5a.
При `-Deploy` скрипт:
1. проверяет хеш `Wow_Original.exe`;
2. сохраняет прежний модифицированный клиент как `Wow.moonwell-patched.backup.exe`;
3. восстанавливает оригинальный `Wow.exe`;
4. устанавливает `WarcraftXL.dll`, D3D9On12 proxy и `Utils\WarcraftXLHost.exe`;
5. монтирует шейдеры modern ADT как loose patch `Data\Patch-WXL.MPQ`;
6. при `-PackagePath` кладёт полный runtime-комплект в `dist` для launcher/S3.
Основной proxy использует D3D9On12, необходимый `wxl-modern-render`. Эффекты постобработки в самом
модуле по умолчанию выключены. Для диагностики или несовместимой видеосистемы можно собрать и
установить native-переходник без modern-render:
```powershell
.\build-warcraftxl.ps1 -Configuration Release -NativeRenderer `
-ClientPath 'C:\Program Files (x86)\World of Warcraft' -Deploy
```
Резервная DLL всегда также сохраняется как `Utils\d3d9-native.dll`.
Полная сборка MPQ, установка WarcraftXL и запуск:
```powershell
$env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
.\run.ps1
```
Диагностика запуска находится в `Logs\wxl-core.log`, `Logs\d3d9proxy.log` и
`Utils\WarcraftXLHost.log` внутри клиента.
## MPQ-пакеты
Распакованные изменяемые слои уже находятся в `src/Data/`; полный набор базовых MPQ для сборки
WarcraftXL не требуется и в объектное хранилище не загружается.
Основные пакеты:
- `patch-4` — ресурсы внутриигрового магазина;
- `patch-ruRU-4` — экран входа и GlueXML;
- `patch-ruRU-5` — русская локализация и интерфейс;
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
## Лицензирование
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный upstream submodule. MoonWell-модуль,
скомпонованный в `WarcraftXL.dll`, должен распространяться с соблюдением GPL-3.0 и доступным
соответствующим исходным кодом. Репозиторий не должен публиковать Blizzard assets или `Wow.exe`.
BIN
View File
Binary file not shown.
+106
View File
@@ -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"
+6
View File
@@ -21,6 +21,10 @@ IGNORED_DIRS_ANYWHERE = {
".git",
".moonwell_launcher",
}
IGNORED_TOP_LEVEL_FILES = {
"Wow.moonwell-patched.backup.exe",
"WarcraftXL_on12.disable",
}
ENV_VAR_NAME = "WOW_HOME"
@@ -42,6 +46,8 @@ def is_ignored(rel_path: Path) -> bool:
return False
if rel_path.parts[0] in IGNORED_TOP_LEVEL_DIRS:
return True
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
return True
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
-276
View File
@@ -1,276 +0,0 @@
--[[----------------------------------------------------------------------------
C_PlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
Клиентское хранилище данных Путеводителя + UI-facing API.
Соответствует контракту из player-guide-requirements.md → "Client API".
Подключается до PlayerGuide.xml/.lua в .toc.
------------------------------------------------------------------------------]]
C_PlayerGuide = C_PlayerGuide or {}
local L = C_PlayerGuide
-- ──────────────────────────────────────────────────────────────────────────
-- Состояние
local currentData = nil -- последний валидный payload от сервера
local lastError = nil -- строка с описанием последней проблемы валидации
-- Состояние UI: "data" | "empty" | "loading"
local uiState = "loading"
-- Подписчики на обновления (PlayerGuide.lua вешает сюда свой Refresh)
local listeners = {}
-- ──────────────────────────────────────────────────────────────────────────
-- Утилиты
local function notify()
for _, fn in ipairs(listeners) do
-- pcall чтобы один сломанный listener не уронил остальные
pcall(fn, uiState, currentData)
end
end
local function isTable(v) return type(v) == "table" end
-- Валидация payload'а. Возвращает true либо false + строка ошибки.
local function validate(data)
if not isTable(data) then return false, "payload is not a table" end
if data.stageID and type(data.stageID) ~= "number" then
return false, "stageID is not a number"
end
if data.progress and type(data.progress) ~= "number" then
return false, "progress is not a number"
end
if data.objectives and not isTable(data.objectives) then
return false, "objectives is not a table"
end
-- Уникальность id целей
if isTable(data.objectives) then
local seen = {}
for i, o in ipairs(data.objectives) do
if not isTable(o) then
return false, "objective #" .. i .. " is not a table"
end
if o.id and seen[o.id] then
return false, "duplicate objective id: " .. tostring(o.id)
end
if o.id then seen[o.id] = true end
end
end
return true
end
-- ──────────────────────────────────────────────────────────────────────────
-- Подписки (для PlayerGuide.lua)
function L.RegisterListener(fn)
table.insert(listeners, fn)
end
-- ──────────────────────────────────────────────────────────────────────────
-- Публичный API (см. требования → Client API)
function L.SetData(data)
local ok, err = validate(data)
if not ok then
lastError = err
uiState = "empty"
notify()
return false, err
end
currentData = data
lastError = nil
uiState = "data"
notify()
return true
end
function L.Clear()
currentData = nil
lastError = nil
uiState = "empty"
notify()
end
function L.SetLoading()
if uiState ~= "data" then
uiState = "loading"
notify()
end
end
function L.GetState()
return uiState
end
function L.GetLastError()
return lastError
end
function L.HasData()
return uiState == "data" and currentData ~= nil
end
function L.GetStageInfo()
if not L.HasData() then return nil end
return currentData.stageID,
currentData.stageName,
currentData.stageDescription,
currentData.progress or 0
end
-- Доп. метод: список всех этапов (для цепочки сверху)
function L.GetStages()
if not L.HasData() then return nil end
return currentData.stages or {} -- сервер опционально шлёт массив этапов
end
function L.GetNumObjectives()
if not L.HasData() or not currentData.objectives then return 0 end
return #currentData.objectives
end
function L.GetObjectiveInfo(index)
if not L.HasData() or not currentData.objectives then return nil end
local o = currentData.objectives[index]
if not o then return nil end
return {
id = o.id,
type = o.type or "custom",
title = o.title or "",
description = o.description or o.subtitle or "",
subtitle = o.subtitle, -- наше расширение поля для UI
completed = o.completed and true or false,
progress = o.progress or 0,
required = o.required or 1,
targetID = o.targetID,
questID = o.questID,
instanceID = o.instanceID,
encounterID = o.encounterID,
achievementID = o.achievementID,
factionID = o.factionID,
itemID = o.itemID,
currencyID = o.currencyID,
order = o.order or index,
}
end
function L.GetNumRecommendations()
if not L.HasData() or not currentData.recommendations then return 0 end
return #currentData.recommendations
end
function L.GetRecommendationInfo(index)
if not L.HasData() or not currentData.recommendations then return nil end
local r = currentData.recommendations[index]
if not r then return nil end
return {
id = r.id,
type = r.type or "dungeon",
title = r.title or r.name or "",
name = r.name or r.title or "",
description = r.description or r.loc or "",
loc = r.loc, -- локация/подзаголовок
level = r.level,
biome = r.biome, -- ключ биом-обложки
instanceID = r.instanceID,
encounterID = r.encounterID,
priority = r.priority or 0,
}
end
function L.GetNumRewards()
if not L.HasData() or not currentData.rewards then return 0 end
return #currentData.rewards
end
function L.GetRewardInfo(index)
if not L.HasData() or not currentData.rewards then return nil end
local r = currentData.rewards[index]
if not r then return nil end
return r
end
-- ──────────────────────────────────────────────────────────────────────────
-- Сетевой слой (AIO)
-- MoonWellPlayerGuide.lua перехватывает SetData() из serverToClient и вызывает
-- L.SetData(payload). RequestRefresh шлёт обратно в обратную сторону.
function L.RequestRefresh()
L.SetLoading()
if AIO and AIO.Handle then
-- name берётся из serverToClient на сервере. Тут — лёгкая обёртка.
AIO.Handle("MoonWellPlayerGuide", "RequestRefresh")
else
-- Без AIO (например, в режиме разработки) — отдаём заглушку через
-- 0.5 секунды, чтобы UI не висел в Loading навечно.
if C_Timer and C_Timer.After then
C_Timer.After(0.5, function() L.Clear() end)
end
end
end
function L.RequestTrackObjective(objectiveID)
if AIO and AIO.Handle then
AIO.Handle("MoonWellPlayerGuide", "RequestTrackObjective", objectiveID)
end
end
function L.RequestOpenTarget(objectiveID)
if AIO and AIO.Handle then
AIO.Handle("MoonWellPlayerGuide", "RequestOpenTarget", objectiveID)
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Статика для разработки (видна, пока сервер не прислал реальный payload).
-- Можно убрать перед релизом.
function L._InjectDevPayload()
L.SetData({
version = 1,
stageID = 5,
stageName = "Подготовка к Нордсколу",
stageDescription =
"Завершите ключевые цепочки заданий в Запределье и докажите готовность к походу на север. "..
"По прибытии в Нордскол вам понадобится снаряжение героического уровня.",
progress = 62,
stages = {
{ id = 1, name = "Начало пути", short = "Начало пути", era = "1—58", status = "done" },
{ id = 2, name = "Огненные Недра", short = "Огненные Недра", era = "60", status = "done" },
{ id = 3, name = "Сквозь Тёмный портал", short = "Тёмный портал", era = "58—60", status = "done" },
{ id = 4, name = "Покорение Запределья", short = "Запределье", era = "60—68", status = "done" },
{ id = 5, name = "Подготовка к Нордсколу", short = "К Нордсколу", era = "68—70", status = "active" },
{ id = 6, name = "Заря Нордскола", short = "Нордскол", era = "70—78", status = "locked" },
{ id = 7, name = "Цитадель Ледяной Короны",short = "Ледяная Корона", era = "78—80", status = "locked" },
},
objectives = {
{ id = 1, type = "level", title = "Достигнуть 68 уровня",
subtitle = "Текущий уровень: 68. Можно отправляться в Нордскол.",
completed = true, progress = 68, required = 68, order = 1 },
{ id = 2, type = "quest_chain", title = "Цепочка «Поиск виверны» в Награнде",
subtitle = "Кисть Шамана • Награнд • награда: «Привязка к виверне»",
completed = false, progress = 5, required = 8, order = 2 },
{ id = 3, type = "dungeon", title = "Пройти Кузню Крови",
subtitle = "Цитадель Адского Пламени • рек. уровень 61—64",
completed = false, progress = 0, required = 1, order = 3, instanceID = 256 },
{ id = 4, type = "dungeon", title = "Пройти Аукиндон: Подземелья Аукенай",
subtitle = "Награнд • рек. уровень 64—66",
completed = false, progress = 0, required = 1, order = 4, instanceID = 257 },
{ id = 5, type = "reputation", title = "Получить «Дружелюбие» с Кенарийской экспедицией",
subtitle = "Текущее положение: Нейтрально • Долина Призрачной Луны",
completed = false, progress = 1200, required = 3000, order = 5, factionID = 942 },
},
recommendations = {
{ id = 1, type = "dungeon", name = "Кузня Крови", loc = "Цитадель Адского Пламени", level = "61—64", biome = "fire" },
{ id = 2, type = "dungeon", name = "Подземелья Аукенай", loc = "Аукиндон", level = "64—66", biome = "arcane" },
{ id = 3, type = "dungeon", name = "Сетеккские залы", loc = "Награнд", level = "67—69", biome = "storm" },
{ id = 4, type = "dungeon", name = "Темный Лабиринт", loc = "Аукиндон", level = "68—70", biome = "shadow" },
{ id = 5, type = "dungeon", name = "Ботаника", loc = "Долина Призрачной Луны", level = "69—70", biome = "nature" },
{ id = 6, type = "dungeon", name = "Расколотые Залы", loc = "Цитадель Адского Пламени", level = "70 H", biome = "fire" },
},
rewards = {}, -- блок наград временно скрыт (см. SHOW_REWARDS в PlayerGuide.lua)
})
end
-78
View File
@@ -1,78 +0,0 @@
--[[----------------------------------------------------------------------------
AIO/MoonWellPlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
AIO-handler (клиентская часть) для namespace "MoonWellPlayerGuide".
Сервер отправляет SetData(payload), клиент кладёт его в C_PlayerGuide.
Клиент шлёт RequestRefresh обратно при нажатии «Обновить».
Контракт описан в player-guide-requirements.md → "AIO Contract".
------------------------------------------------------------------------------]]
local AIO = AIO or require("AIO")
if not AIO then return end -- безопасный no-op без AIO
local handlers = AIO.AddHandlers("MoonWellPlayerGuide", {})
-- ──────────────────────────────────────────────────────────────────────────
-- Server → Client
-- Полная замена payload
function handlers.SetData(player, payload)
if C_PlayerGuide and C_PlayerGuide.SetData then
local ok, err = C_PlayerGuide.SetData(payload)
if not ok and DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage(
"|cffff8080[Путеводитель]|r Ошибка payload: "..tostring(err))
end
end
end
-- Обновление одной цели (для частичных update'ов)
function handlers.UpdateObjective(player, objective)
if not (C_PlayerGuide and C_PlayerGuide.HasData()) then return end
-- Простая стратегия: пересобрать payload локально с заменой цели по id.
-- C_PlayerGuide хранит данные internally — даём прямой доступ через _patch:
local n = C_PlayerGuide.GetNumObjectives()
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if o and o.id == objective.id then
-- Грязный, но рабочий способ для MVP — отдать обратно весь массив.
-- Для прод-уровня лучше выставить C_PlayerGuide.PatchObjective(o).
o.progress = objective.progress or o.progress
o.required = objective.required or o.required
o.completed = objective.completed
if MW_PlayerGuide and MW_PlayerGuide.Refresh then
MW_PlayerGuide.Refresh()
end
break
end
end
end
-- Замена только инфы об этапе (без массива objectives)
function handlers.SetStage(player, stagePayload)
if not stagePayload then return end
-- Объединяем с текущим payload'ом
local _, name, desc, progress = C_PlayerGuide.GetStageInfo()
C_PlayerGuide.SetData({
stageID = stagePayload.stageID,
stageName = stagePayload.stageName or name,
stageDescription = stagePayload.stageDescription or desc,
progress = stagePayload.progress or progress,
stages = stagePayload.stages,
objectives = stagePayload.objectives or {},
recommendations = stagePayload.recommendations or {},
rewards = stagePayload.rewards or {},
})
end
-- Сообщения от сервера (баблы / chat-фрейм)
function handlers.Notify(player, message)
if DEFAULT_CHAT_FRAME and message then
DEFAULT_CHAT_FRAME:AddMessage("|cffffd76a[Путеводитель]|r "..tostring(message))
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Client → Server: ничего регистрировать не нужно — это просто AIO.Handle
-- внутри C_PlayerGuide.RequestRefresh / RequestTrackObjective / RequestOpenTarget.
-402
View File
@@ -1,402 +0,0 @@
--[[----------------------------------------------------------------------------
PlayerGuide.lua
─────────────────────────────────────────────────────────────────────────────
UI-логика вкладки «Путеводитель». Парный файл PlayerGuide.xml.
Зависимости:
* C_PlayerGuide (Utils/C_PlayerGuide.lua) — данные и API
* MW_PLAYER_GUIDE* (Localization/ruRU.lua) — строки
* EncounterJournal (Custom_EncounterJournal) — родительский фрейм
------------------------------------------------------------------------------]]
-- ──────────────────────────────────────────────────────────────────────────
-- Конфигурация фичи
local SHOW_REWARDS = false -- блок наград временно отключён
local SHOW_RECOMMENDATIONS = true
local DENSITY = "compact" -- compact | regular | cozy
local ACCENT = { r = 1.0, g = 0.84, b = 0.42 } -- золото
local ROMAN = { "I","II","III","IV","V","VI","VII","VIII","IX","X" }
-- Биом → цвет «обложки» рекомендации (rgb 0..1).
-- В будущем заменить на текстуры Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*.
local BIOME_COLOR = {
fire = { 0.83, 0.29, 0.10 },
arcane = { 0.48, 0.31, 0.83 },
storm = { 0.24, 0.56, 0.77 },
shadow = { 0.35, 0.16, 0.54 },
nature = { 0.31, 0.63, 0.29 },
holy = { 0.83, 0.69, 0.24 },
}
-- Тип цели → глиф (заменить на иконки из BLP, когда будет атлас).
local TYPE_GLYPH = {
level = "",
quest = "!",
quest_chain = "!",
dungeon = "",
raid = "",
boss = "",
achievement = "",
reputation = "",
profession = "",
item = "",
currency = "",
custom = "",
}
-- Тип цели → плейсхолдер-иконка из встроенных текстур 3.3.5a.
local TYPE_ICON = {
level = "Interface\\Icons\\Ability_Warrior_Challange",
quest = "Interface\\Icons\\INV_Misc_Note_01",
quest_chain = "Interface\\Icons\\INV_Misc_Note_05",
dungeon = "Interface\\Icons\\INV_Misc_Key_06",
raid = "Interface\\Icons\\INV_Misc_Head_Dragon_01",
boss = "Interface\\Icons\\Ability_Warrior_DecisiveStrike",
achievement = "Interface\\Icons\\Achievement_GuildPerk_HonorableMention",
reputation = "Interface\\Icons\\INV_Scroll_03",
profession = "Interface\\Icons\\Trade_Engineering",
item = "Interface\\Icons\\INV_Misc_Gem_Diamond_02",
currency = "Interface\\Icons\\INV_Misc_Coin_17",
custom = "Interface\\Icons\\INV_Misc_QuestionMark",
}
-- ──────────────────────────────────────────────────────────────────────────
-- Хелперы
local function pct(cur, req)
if not req or req <= 0 then return 0 end
return math.floor((cur / req) * 100 + 0.5)
end
local function setRGB(fs, c) fs:SetTextColor(c.r or c[1], c.g or c[2], c.b or c[3]) end
local function showOrHide(frame, show)
if not frame then return end
if show then frame:Show() else frame:Hide() end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Цепочка этапов
local stageBadges = {} -- кэш созданных бейджей по индексу
local function buildStageChain(stages)
if not PlayerGuideFrame then return end
local container = PlayerGuideFrame.StageChain
-- Прячем лишние бейджи, если этапов стало меньше
for i = (#stages + 1), #stageBadges do
stageBadges[i]:Hide()
end
if #stages == 0 then return end
local width = container:GetWidth()
if width == 0 then width = 600 end
local slot = width / #stages
for i, s in ipairs(stages) do
local badge = stageBadges[i]
if not badge then
badge = CreateFrame("Frame", "PlayerGuideStageBadge"..i, container, "MW_PG_StageBadgeTemplate")
stageBadges[i] = badge
end
badge:ClearAllPoints()
badge:SetPoint("LEFT", container, "LEFT", (i - 0.5) * slot - 19, 0)
badge:Show()
-- Текст
badge.Roman:SetText(ROMAN[s.id] or tostring(s.id))
badge.Label:SetText(s.short or s.name or "")
-- Цветовая схема по статусу
local status = s.status or "locked"
if status == "active" then
badge.Roman:SetTextColor(1.0, 0.91, 0.63)
badge.Label:SetTextColor(1.0, 0.84, 0.42)
badge.BadgeBG:SetVertexColor(1.0, 0.84, 0.42, 1)
elseif status == "done" then
badge.Roman:SetTextColor(0.79, 0.65, 0.38)
badge.Label:SetTextColor(0.79, 0.65, 0.38)
badge.BadgeBG:SetVertexColor(0.55, 0.40, 0.18, 1)
-- Можно заменить римскую цифру на «✓» для завершённых:
badge.Roman:SetText("")
else
badge.Roman:SetTextColor(0.40, 0.34, 0.22)
badge.Label:SetTextColor(0.40, 0.34, 0.22)
badge.BadgeBG:SetVertexColor(0.20, 0.15, 0.08, 1)
end
end
end
-- ──────────────────────────────────────────────────────────────────────────
-- Карточки целей
local objectiveCards = {}
local CARD_HEIGHT = { compact = 44, regular = 56, cozy = 72 }
local CARD_GAP = { compact = 4, regular = 6, cozy = 8 }
local function renderObjectives()
local section = PlayerGuideFrame.ObjectivesSection
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumObjectives()
-- Скрыть лишние
for i = (n + 1), #objectiveCards do objectiveCards[i]:Hide() end
local h = CARD_HEIGHT[DENSITY] or 44
local gap = CARD_GAP[DENSITY] or 4
local done = 0
for i = 1, n do
local o = C_PlayerGuide.GetObjectiveInfo(i)
if not o then break end
local card = objectiveCards[i]
if not card then
card = CreateFrame("Frame", "PlayerGuideObjective"..i, parent, "MW_PG_ObjectiveCardTemplate")
objectiveCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, -(i - 1) * (h + gap))
card:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, -(i - 1) * (h + gap))
card:SetHeight(h)
card.objectiveData = o
card:Show()
-- Иконка типа
card.Icon:SetTexture(TYPE_ICON[o.type] or TYPE_ICON.custom)
card.Icon:SetTexCoord(0.08, 0.92, 0.08, 0.92) -- crop borders Blizz-стиль
-- Тип-плашка caps
card.TypeLabel:SetText(_G["MW_PG_TYPE_"..string.upper(o.type)] or string.upper(o.type))
-- Заголовок и подзаголовок
card.Title:SetText(o.title or "")
card.Subtitle:SetText(o.subtitle or o.description or "")
-- Прогресс
card.ProgressBar:SetMinMaxValues(0, o.required or 1)
card.ProgressBar:SetValue(o.progress or 0)
card.Count:SetText(string.format("%d / %d", o.progress or 0, o.required or 1))
-- Цвет по статусу
if o.completed then
done = done + 1
card.Title:SetTextColor(0.62, 0.84, 0.47)
card.TypeLabel:SetTextColor(0.49, 0.65, 0.35)
card.Count:SetTextColor(0.62, 0.84, 0.47)
card.ProgressBar:SetStatusBarColor(0.55, 0.85, 0.40)
card.IconBorder:SetVertexColor(0.40, 0.65, 0.30, 0.7)
else
card.Title:SetTextColor(1.0, 0.91, 0.63)
card.TypeLabel:SetTextColor(0.63, 0.46, 0.19)
card.Count:SetTextColor(0.79, 0.65, 0.38)
card.ProgressBar:SetStatusBarColor(ACCENT.r, ACCENT.g, ACCENT.b)
card.IconBorder:SetVertexColor(ACCENT.r, ACCENT.g, ACCENT.b, 0.55)
end
end
parent:SetHeight(math.max(1, n * (h + gap)))
section.Count:SetText(string.format("· %d / %d", done, n))
end
-- ──────────────────────────────────────────────────────────────────────────
-- Карточки рекомендаций
local recCards = {}
local function renderRecommendations()
local section = PlayerGuideFrame.RecommendationsSection
if not SHOW_RECOMMENDATIONS then section:Hide(); return end
local parent = section.ScrollFrame.ScrollChild
local n = C_PlayerGuide.GetNumRecommendations()
if n == 0 then section:Hide(); return end
section:Show()
for i = (n + 1), #recCards do recCards[i]:Hide() end
local cardW = 148
local gap = 8
for i = 1, n do
local r = C_PlayerGuide.GetRecommendationInfo(i)
if not r then break end
local card = recCards[i]
if not card then
card = CreateFrame("Button", "PlayerGuideRec"..i, parent, "MW_PG_RecCardTemplate")
recCards[i] = card
end
card:ClearAllPoints()
card:SetPoint("TOPLEFT", parent, "TOPLEFT", (i - 1) * (cardW + gap), 0)
card.recData = r
card:Show()
local biome = BIOME_COLOR[r.biome] or BIOME_COLOR.fire
card.Artwork:SetVertexColor(biome[1], biome[2], biome[3], 1)
card.Name:SetText(r.name or "")
card.Loc:SetText(r.loc or "")
card.Level:SetText(r.level or "")
end
parent:SetWidth(math.max(1, n * (cardW + gap)))
section.Count:SetText("· "..n)
end
-- ──────────────────────────────────────────────────────────────────────────
-- Главный Refresh
local function showState(state)
local F = PlayerGuideFrame
local hasData = (state == "data")
showOrHide(F.StageChain, hasData)
showOrHide(F.Header, hasData)
showOrHide(F.ObjectivesSection, hasData)
showOrHide(F.RecommendationsSection, hasData and SHOW_RECOMMENDATIONS)
showOrHide(F.EmptyState, state == "empty")
showOrHide(F.LoadingState, state == "loading")
end
local function refresh()
if not PlayerGuideFrame then return end
local state = C_PlayerGuide.GetState()
showState(state)
if state ~= "data" then return end
-- Заголовок этапа
local id, name, desc, progress = C_PlayerGuide.GetStageInfo()
local stages = C_PlayerGuide.GetStages() or {}
PlayerGuideFrame.Header.Pretitle:SetText(
string.format(MW_PLAYER_GUIDE_STAGE_OF or "Этап %d из %d", id or 0, #stages))
PlayerGuideFrame.Header.Title:SetText(name or "")
PlayerGuideFrame.Header.Description:SetText(desc or "")
PlayerGuideFrame.Header.ProgressLabel:SetText(MW_PLAYER_GUIDE_STAGE_PROGRESS or "ПРОГРЕСС ЭТАПА")
PlayerGuideFrame.Header.ProgressPercent:SetText((progress or 0) .. "%")
PlayerGuideFrame.Header.ProgressBar:SetMinMaxValues(0, 100)
PlayerGuideFrame.Header.ProgressBar:SetValue(progress or 0)
buildStageChain(stages)
renderObjectives()
renderRecommendations()
end
-- ──────────────────────────────────────────────────────────────────────────
-- XML script handlers
function MW_PlayerGuide_OnLoad(self)
-- Подписываемся на изменения C_PlayerGuide
C_PlayerGuide.RegisterListener(function() refresh() end)
end
function MW_PlayerGuide_OnShow(self)
-- Если данных ещё нет — попросим сервер
if not C_PlayerGuide.HasData() then
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
refresh()
end
function MW_PlayerGuide_RefreshButton_OnClick(self)
C_PlayerGuide.SetLoading()
C_PlayerGuide.RequestRefresh()
end
-- Клик по карточке цели — открывает связанный таргет (см. требования)
function MW_PlayerGuide_Objective_OnClick(self)
local o = self.objectiveData
if not o then return end
if o.type == "dungeon" or o.type == "raid" then
if o.instanceID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(o.instanceID)
end
elseif o.type == "boss" then
if o.encounterID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(o.instanceID, o.encounterID)
end
elseif o.type == "achievement" and o.achievementID then
if AchievementFrame_LoadUI then AchievementFrame_LoadUI() end
if AchievementFrame_SelectAchievement then
AchievementFrame_SelectAchievement(o.achievementID)
end
elseif o.type == "item" and o.itemID then
-- Запрос подсказки предмета
if GameTooltip then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetItemByID(o.itemID)
GameTooltip:Show()
end
else
-- Любой нестандартный тип — пытаемся попросить сервер открыть таргет
C_PlayerGuide.RequestOpenTarget(o.id)
end
end
function MW_PlayerGuide_Objective_OnEnter(self)
local o = self.objectiveData
if not o then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(o.title or "", 1, 0.91, 0.63)
if o.subtitle and o.subtitle ~= "" then
GameTooltip:AddLine(o.subtitle, 0.66, 0.59, 0.47, true)
end
if not o.completed then
GameTooltip:AddLine(string.format("Прогресс: %d / %d", o.progress, o.required),
0.79, 0.65, 0.38)
end
GameTooltip:Show()
end
function MW_PlayerGuide_Objective_OnLeave(self) GameTooltip:Hide() end
function MW_PlayerGuide_Rec_OnClick(self)
local r = self.recData
if r and r.instanceID and EncounterJournal_OpenJournal then
EncounterJournal_OpenJournal(r.instanceID)
end
end
function MW_PlayerGuide_Rec_OnEnter(self)
local r = self.recData
if not r then return end
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(r.name or "", 1, 0.91, 0.63)
if r.loc then GameTooltip:AddLine(r.loc, 0.66, 0.59, 0.47) end
if r.level then GameTooltip:AddLine("Уровень: "..r.level, 0.79, 0.65, 0.38) end
GameTooltip:Show()
end
function MW_PlayerGuide_Rec_OnLeave(self) GameTooltip:Hide() end
-- ──────────────────────────────────────────────────────────────────────────
-- Публичные хелперы для Custom_EncounterJournal.lua (routing вкладки)
function PlayerGuide_Show()
if PlayerGuideFrame then
PlayerGuideFrame:Show()
end
end
function PlayerGuide_Hide()
if PlayerGuideFrame then
PlayerGuideFrame:Hide()
end
end
-- Глобальный экспорт для отладки
MW_PlayerGuide = {
Refresh = refresh,
Show = PlayerGuide_Show,
Hide = PlayerGuide_Hide,
InjectDevData = function() C_PlayerGuide._InjectDevPayload() end,
}
-498
View File
@@ -1,498 +0,0 @@
<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">
<!-- ============================================================
PlayerGuide.xml — вкладка «Путеводитель» в Encounter Journal
Канон WotLK: тёмно-синий фон, золотые акценты, Friz Quadrata.
Подключается ПОСЛЕ Custom_EncounterJournal.xml в .toc, чтобы
PlayerGuideFrame мог анкориться к EncounterJournal как parent.
============================================================ -->
<!-- ───────── Шрифты (Friz Quadrata из клиента 3.3.5a) ───────── -->
<Font name="MW_PG_Title" inherits="GameFontNormalHuge" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Section" inherits="QuestFont_Large" virtual="true">
<Color r="1.0" g="0.84" b="0.42"/>
<Shadow><Color r="0" g="0" b="0"/><Offset><AbsDimension x="1" y="-1"/></Offset></Shadow>
</Font>
<Font name="MW_PG_Body" inherits="GameFontHighlight" virtual="true">
<Color r="0.85" g="0.78" b="0.63"/>
</Font>
<Font name="MW_PG_Caps" inherits="GameFontNormalSmall" virtual="true">
<Color r="0.63" g="0.46" b="0.19"/>
</Font>
<Font name="MW_PG_Italic" inherits="QuestFont" virtual="true">
<Color r="0.66" g="0.59" b="0.47"/>
</Font>
<!-- ───────── Backdrop'ы ───────── -->
<!-- Карточка цели (полупрозрачная пергамент-плашка) -->
<Frame name="MW_PG_ObjectiveCardTemplate" virtual="true">
<Size><AbsDimension x="0" y="44"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
edgeFile="Interface\Tooltips\UI-Tooltip-Border"
tile="true">
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="12"/></EdgeSize>
<Color r="0.22" g="0.15" b="0.07" a="0.65"/>
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Иконка типа цели (32×32, золотая рамка применяется отдельной текстурой) -->
<Texture parentKey="Icon" file="Interface\Icons\INV_Misc_QuestionMark">
<Size><AbsDimension x="32" y="32"/></Size>
<Anchors>
<Anchor point="LEFT"><Offset><AbsDimension x="8" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
<Texture parentKey="IconBorder" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="42" y="42"/></Size>
<Anchors><Anchor point="CENTER" relativeKey="$parent.Icon"/></Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
<Color r="1.0" g="0.84" b="0.42" a="0.55"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="TypeLabel" inherits="MW_PG_Caps" justifyH="LEFT">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Icon" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="10" y="-1"/></Offset>
</Anchor>
</Anchors>
</FontString>
<FontString parentKey="Title" inherits="GameFontNormal" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="6" y="0"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Subtitle" inherits="MW_PG_Italic" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.TypeLabel" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-90" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="GameFontNormalSmall" justifyH="RIGHT">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-10" y="-6"/></Offset></Anchor>
</Anchors>
<Color r="0.79" g="0.65" b="0.38"/>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar parentKey="ProgressBar">
<Size><AbsDimension x="0" y="6"/></Size>
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Icon" relativePoint="RIGHT">
<Offset><AbsDimension x="10" y="-12"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-10" y="-12"/></Offset></Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
</Texture>
</Layer>
</Layers>
</StatusBar>
</Frames>
<Scripts>
<OnClick function="MW_PlayerGuide_Objective_OnClick"/>
<OnEnter function="MW_PlayerGuide_Objective_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Objective_OnLeave"/>
</Scripts>
</Frame>
<!-- Карточка рекомендованного подземелья -->
<Button name="MW_PG_RecCardTemplate" virtual="true">
<Size><AbsDimension x="148" y="106"/></Size>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background"
edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets><AbsInset left="3" right="3" top="3" bottom="3"/></BackgroundInsets>
<TileSize><AbsValue val="16"/></TileSize>
<EdgeSize><AbsValue val="12"/></EdgeSize>
<Color r="0.10" g="0.07" b="0.03" a="0.85"/>
<BorderColor r="0.55" g="0.40" b="0.18" a="1.0"/>
</Backdrop>
<Layers>
<Layer level="ARTWORK">
<!-- Биом-плашка (плейсхолдер: цветной градиент) -->
<Texture parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
<Size><AbsDimension x="138" y="58"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Color r="0.55" g="0.16" b="0.07"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="Level" inherits="GameFontNormalSmall" justifyH="CENTER">
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.Artwork" relativePoint="TOPRIGHT">
<Offset><AbsDimension x="-4" y="-4"/></Offset>
</Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="130" y="12"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Artwork" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="2" y="-4"/></Offset>
</Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
<Size><AbsDimension x="130" y="10"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Name" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick function="MW_PlayerGuide_Rec_OnClick"/>
<OnEnter function="MW_PlayerGuide_Rec_OnEnter"/>
<OnLeave function="MW_PlayerGuide_Rec_OnLeave"/>
</Scripts>
</Button>
<!-- Бейдж этапа в цепочке (круглая золотая монета с цифрой) -->
<Frame name="MW_PG_StageBadgeTemplate" virtual="true">
<Size><AbsDimension x="38" y="38"/></Size>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="BadgeBG" file="Interface\Buttons\UI-Quickslot2">
<Size><AbsDimension x="60" y="60"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
<TexCoords left="0.2" right="0.8" top="0.2" bottom="0.8"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString parentKey="Roman" inherits="GameFontNormalLarge" justifyH="CENTER">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="0"/></Offset></Anchor></Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
<FontString parentKey="Label" inherits="MW_PG_Caps" justifyH="CENTER">
<Size><AbsDimension x="100" y="10"/></Size>
<Anchors>
<Anchor point="TOP" relativePoint="BOTTOM"><Offset><AbsDimension x="0" y="-4"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
<!-- ───────── Корневой фрейм ───────── -->
<!-- Парент: EncounterJournal.encounter (см. PlayerGuide.lua → Init) -->
<Frame name="PlayerGuideFrame" hidden="true" parent="EncounterJournal">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="22" y="-58"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
</Anchors>
<Layers>
<!-- «Волна» сверху, как в оригинальном EJ — добавляется через
родительский фон. Здесь — тонкая золотая внутренняя рамка. -->
<Layer level="BORDER">
<Texture parentKey="TopGlow" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="0" y="-58"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.84" b="0.42" a="0.35"/>
</Texture>
</Layer>
</Layers>
<Frames>
<!-- 1) Цепочка этапов сверху -->
<Frame parentKey="StageChain">
<Size><AbsDimension x="0" y="62"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-4"/></Offset></Anchor>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-8" y="-4"/></Offset></Anchor>
</Anchors>
<!-- Бейджи создаются программно в Lua (PlayerGuide_BuildStageChain) -->
</Frame>
<!-- 2) Заголовок текущего этапа -->
<Frame parentKey="Header">
<Size><AbsDimension x="0" y="78"/></Size>
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.StageChain" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="12" y="-12"/></Offset>
</Anchor>
<Anchor point="TOPRIGHT" relativeKey="$parent.StageChain" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-12" y="-12"/></Offset>
</Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Pretitle" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Pretitle" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Description" inherits="MW_PG_Body" justifyH="LEFT">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Title" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="2" y="-6"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-260" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="ProgressLabel" inherits="MW_PG_Caps" justifyH="RIGHT">
<Anchors>
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="-2" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="ProgressPercent" inherits="MW_PG_Title" justifyH="RIGHT">
<Size><AbsDimension x="240" y="28"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressLabel" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<StatusBar parentKey="ProgressBar">
<Size><AbsDimension x="240" y="10"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressPercent" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
</Anchors>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="1.0" g="0.79" b="0.29"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true" file="Interface\Buttons\WHITE8X8">
<Color r="0.05" g="0.04" b="0.02" a="0.85"/>
</Texture>
</Layer>
</Layers>
</StatusBar>
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="100" y="22"/></Size>
<Anchors>
<Anchor point="TOPRIGHT" relativeKey="$parent.ProgressBar" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 3) Секция «Задачи этапа» — заголовок + список карточек -->
<Frame parentKey="ObjectivesSection">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Header" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="-2" y="-10"/></Offset>
</Anchor>
<Anchor point="TOPRIGHT" relativeKey="$parent.Header" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="2" y="-10"/></Offset>
</Anchor>
<Anchor point="BOTTOM"><Offset><AbsDimension x="0" y="142"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_OBJECTIVES">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
<Offset><AbsDimension x="8" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="2"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="600" y="200"/></Size>
<!-- Карточки целей вставляются программно через ObjectiveCardTemplate -->
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 4) Секция «Подходящие подземелья» — горизонтальная карусель -->
<Frame parentKey="RecommendationsSection">
<Size><AbsDimension x="0" y="132"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT"><Offset><AbsDimension x="6" y="6"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-6" y="6"/></Offset></Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Heading" inherits="MW_PG_Section" justifyH="LEFT" text="MW_PLAYER_GUIDE_RECS">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="0"/></Offset></Anchor>
</Anchors>
</FontString>
<FontString parentKey="Count" inherits="MW_PG_Caps" justifyH="LEFT">
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Heading" relativePoint="RIGHT">
<Offset><AbsDimension x="8" y="-2"/></Offset>
</Anchor>
</Anchors>
</FontString>
<Texture parentKey="Divider" file="Interface\Common\UI-TooltipDivider-Transparent">
<Size><AbsDimension x="0" y="2"/></Size>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.Heading" relativePoint="BOTTOMLEFT">
<Offset><AbsDimension x="0" y="-4"/></Offset>
</Anchor>
<Anchor point="RIGHT"><Offset><AbsDimension x="-4" y="0"/></Offset></Anchor>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<ScrollFrame parentKey="ScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="4" y="-26"/></Offset></Anchor>
<Anchor point="BOTTOMRIGHT"><Offset><AbsDimension x="-26" y="4"/></Offset></Anchor>
</Anchors>
<ScrollChild>
<Frame parentKey="ScrollChild">
<Size><AbsDimension x="900" y="110"/></Size>
</Frame>
</ScrollChild>
</ScrollFrame>
</Frames>
</Frame>
<!-- 5) Пустое состояние -->
<Frame parentKey="EmptyState" hidden="true">
<Anchors><Anchor point="CENTER"><Offset><AbsDimension x="0" y="20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="200"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Title" inherits="MW_PG_Title" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_TITLE">
<Anchors><Anchor point="TOP"><Offset><AbsDimension x="0" y="-20"/></Offset></Anchor></Anchors>
<Size><AbsDimension x="460" y="28"/></Size>
</FontString>
<FontString parentKey="Body" inherits="MW_PG_Body" justifyH="CENTER" text="MW_PLAYER_GUIDE_EMPTY_BODY">
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-10"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="420" y="60"/></Size>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="RefreshButton" inherits="UIPanelButtonTemplate2" text="MW_PLAYER_GUIDE_REFRESH">
<Size><AbsDimension x="120" y="24"/></Size>
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Body" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-14"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="MW_PlayerGuide_RefreshButton_OnClick"/>
</Scripts>
</Button>
</Frames>
</Frame>
<!-- 6) Состояние загрузки -->
<Frame parentKey="LoadingState" hidden="true">
<Anchors><Anchor point="CENTER"/></Anchors>
<Size><AbsDimension x="300" y="80"/></Size>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Title" inherits="MW_PG_Section" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_TITLE">
<Anchors><Anchor point="TOP"/></Anchors>
<Size><AbsDimension x="300" y="20"/></Size>
</FontString>
<FontString parentKey="Body" inherits="MW_PG_Italic" justifyH="CENTER" text="MW_PLAYER_GUIDE_LOADING_BODY">
<Anchors>
<Anchor point="TOP" relativeKey="$parent.Title" relativePoint="BOTTOM">
<Offset><AbsDimension x="0" y="-6"/></Offset>
</Anchor>
</Anchors>
<Size><AbsDimension x="300" y="16"/></Size>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
<Scripts>
<OnLoad function="MW_PlayerGuide_OnLoad"/>
<OnShow function="MW_PlayerGuide_OnShow"/>
</Scripts>
</Frame>
</Ui>
-64
View File
@@ -1,64 +0,0 @@
# PlayerGuide — клиентский пакет (3.3.5a / MoonWellClient)
Это материалы для интеграции вкладки **«Путеводитель»** в существующий
ретропортированный Encounter Journal. Реализация соответствует требованиям
из `uploads/player-guide-requirements.md` и опирается на канон-визуал
WotLK (Friz Quadrata, тёмно-синий фон EJ, золотые акценты).
## Структура
```
MoonWellClient/
├── EncounterJournal/
│ ├── Utils/
│ │ └── C_PlayerGuide.lua ← хранилище + API
│ ├── Custom_EncounterJournal/
│ │ ├── Custom_EncounterJournal.xml ← патч: новая вкладка
│ │ ├── Custom_EncounterJournal.lua ← патч: routing вкладки
│ │ ├── PlayerGuide.xml ← фреймы и шаблоны (НОВОЕ)
│ │ └── PlayerGuide.lua ← рендер и логика (НОВОЕ)
│ └── Localization/
│ └── ruRU.lua ← патч: MW_PLAYER_GUIDE и др.
├── AIO/
│ └── MoonWellPlayerGuide.lua ← AIO-handler (клиентская часть)
└── MoonWellClient.toc ← патч: подключение файлов
```
## Порядок интеграции
1. **Скопируйте новые файлы** в проект:
* `client-export/PlayerGuide.xml``EncounterJournal/Custom_EncounterJournal/PlayerGuide.xml`
* `client-export/PlayerGuide.lua``EncounterJournal/Custom_EncounterJournal/PlayerGuide.lua`
* `client-export/C_PlayerGuide.lua``EncounterJournal/Utils/C_PlayerGuide.lua`
* `client-export/MoonWellPlayerGuide.lua``AIO/MoonWellPlayerGuide.lua`
2. **Примените патчи** из `client-export/patches/` к существующим файлам
(см. ниже — `Custom_EncounterJournal.xml`, `Custom_EncounterJournal.lua`,
`MoonWellClient.toc`, `Localization/ruRU.lua`).
3. **Перезагрузите интерфейс** (`/reload`). Если AIO ещё не передал
payload — увидите «пустое» состояние с кнопкой «Обновить».
## Что вошло в MVP (по требованиям)
| MVP-пункт | Где |
| ----------------------------- | ------------------------------------------- |
| вкладка `Путеводитель` | `Custom_EncounterJournal.xml` патч |
| пустой `PlayerGuideFrame` | `PlayerGuide.xml` |
| `C_PlayerGuide.lua` + статика | `C_PlayerGuide.lua` |
| заголовок этапа + цели | `PlayerGuide.lua → PlayerGuide_Refresh` |
| tab switching / hide-show | `Custom_EncounterJournal.lua → EJ_ContentTab_OnClick` хук |
| AIO `SetData` | `MoonWellPlayerGuide.lua → handlers.SetData` |
| Обновить (RequestRefresh) | `PlayerGuide.lua → PlayerGuide_RefreshButton_OnClick` |
## Что отложено (см. «Later UI» в требованиях)
* Фильтры (все / активные / завершённые)
* Pinning отслеживаемой цели
* **Награды** — блок временно отключён (`SHOW_REWARDS = false`)
* Серверные уведомления (`Notify`)
## Сноска по визуалу
Текстуры в XML — это встроенные ресурсы 3.3.5a-клиента. Если у вас
есть атлас MoonWell — замените пути в блоке `-- TEXTURE_ATLAS` на свои.
Биом-обложки рекомендаций (`Interface\AddOns\MoonWellClient\EncounterJournal\Textures\Biome-*`)
надо создать отдельно — это плейсхолдеры (см. таблицу в `PlayerGuide.lua`).
@@ -1,120 +0,0 @@
--[[----------------------------------------------------------------------------
patches/Custom_EncounterJournal.lua.patch
─────────────────────────────────────────────────────────────────────────────
Патч-сниппеты для существующего Custom_EncounterJournal.lua. Эти куски
добавляют routing новой вкладки и hide-show поведение по требованиям:
«при выборе guide tab:
- hide instance cards
- hide Encounter Journal detail panel
- hide suggested content panel
- hide loot journal panel
- hide or disable the expansion/tier dropdown unless explicitly needed
- show PlayerGuideFrame»
------------------------------------------------------------------------------]]
-- ─── ПАТЧ #1 ───────────────────────────────────────────────────────────────
-- НАЙТИ функцию EJ_ContentTab_Select (или ту, что переключает вкладки в
-- Encounter Journal). Обычно она выглядит как switch по id вкладки.
-- ДОБАВИТЬ ветку для нашей вкладки. Пример полностью переписанной функции:
local PLAYERGUIDE_TAB_ID = 1 -- должен совпадать с id="1" в XML патче
function EJ_ContentTab_Select(id)
EncounterJournal.suggestTab:UnlockHighlight()
EncounterJournal.dungeonsTab:UnlockHighlight()
EncounterJournal.raidsTab:UnlockHighlight()
if EncounterJournal.lootJournalTab then
EncounterJournal.lootJournalTab:UnlockHighlight()
end
if EncounterJournal.guideTab then
EncounterJournal.guideTab:UnlockHighlight()
end
-- Прячем «обычные» панели EJ. Этот хелпер уже есть в Custom_EncounterJournal:
-- EncounterJournal_HideAllPanels()
-- Если его нет — выньте тело сюда (Hide instance list, encounter, loot).
EncounterJournal_HideAllPanels()
-- Прячем PlayerGuideFrame — он показывается только на guide-вкладке.
if PlayerGuideFrame then PlayerGuideFrame:Hide() end
if id == PLAYERGUIDE_TAB_ID then
-- НАША ВКЛАДКА -----------------------------------------------------
EncounterJournal.guideTab:LockHighlight()
EncounterJournal.instanceSelect:Hide()
EncounterJournal.encounter:Hide()
if EncounterJournalSuggestFrame then EncounterJournalSuggestFrame:Hide() end
if LootJournal then LootJournal:Hide() end
-- Скрыть/задисейблить дропдаун расширений, как просят требования:
if EJTierDropDown then EJTierDropDown:Hide() end
if PlayerGuideFrame then PlayerGuideFrame:Show() end
return
end
-- ── Остальные вкладки — оригинальная логика ──
if EJTierDropDown then EJTierDropDown:Show() end
if id == 2 then -- SuggestTab (было 1)
EncounterJournal.suggestTab:LockHighlight()
EncounterJournal_ListInstances() -- или ваш Suggest-Show
elseif id == 3 then -- DungeonTab
EncounterJournal.dungeonsTab:LockHighlight()
EJ_HideNonInstancePanels()
EncounterJournal_ListInstances()
elseif id == 4 then -- RaidTab
EncounterJournal.raidsTab:LockHighlight()
EJ_HideNonInstancePanels()
EncounterJournal_ListInstances()
elseif id == 5 then -- LootJournalTab
if EncounterJournal.lootJournalTab then
EncounterJournal.lootJournalTab:LockHighlight()
end
if LootJournal_Show then LootJournal_Show() end
end
end
-- ─── ПАТЧ #2 ───────────────────────────────────────────────────────────────
-- В EJ_ContentTab_OnClick добавить вызов нашего Select для tab id=1
-- (если у вас один централизованный _OnClick — он уже передаёт id и
-- ничего дополнительно делать не нужно):
function EJ_ContentTab_OnClick(self)
EJ_ContentTab_Select(self:GetID())
PlaySound("igCharacterInfoTab")
end
-- ─── ПАТЧ #3 ───────────────────────────────────────────────────────────────
-- Защита: при открытии EJ через EncounterJournal_OpenJournal — переключиться
-- НА dungeon/raid таб, а не оставлять guide-таб выбранным.
-- Найдите вашу EncounterJournal_OpenJournal и убедитесь, что в начале:
local _orig_EncounterJournal_OpenJournal = EncounterJournal_OpenJournal
function EncounterJournal_OpenJournal(instanceID, encounterID, ...)
-- При прямом открытии конкретного instance — переключиться на Dungeon/Raid
if instanceID then
EJ_ContentTab_Select(3) -- Dungeon по умолчанию; ваша логика может выбрать Raid
end
return _orig_EncounterJournal_OpenJournal(instanceID, encounterID, ...)
end
-- ─── ПАТЧ #4 ───────────────────────────────────────────────────────────────
-- Init: статика для разработки (можно убрать перед релизом). Если AIO
-- ещё не успел прислать payload — покажем dev-payload через 1с после
-- первого открытия EJ.
local _devInjected = false
EncounterJournal:HookScript("OnShow", function()
if not _devInjected and not C_PlayerGuide.HasData() then
_devInjected = true
C_Timer.After(1.0, function()
if not C_PlayerGuide.HasData() then
C_PlayerGuide._InjectDevPayload()
end
end)
end
end)
@@ -1,57 +0,0 @@
--[[----------------------------------------------------------------------------
patches/Custom_EncounterJournal.xml.patch
─────────────────────────────────────────────────────────────────────────────
Это НЕ замена файла — это патч-сниппеты для существующего
Custom_EncounterJournal.xml. Вставьте обозначенные блоки.
В оригинальном Custom_EncounterJournal.xml внутри EncounterJournal.bg или
аналогичного контейнера уже есть вкладки SuggestTab / DungeonTab / RaidTab /
LootJournalTab. Нужно вставить GuideTab ПЕРЕД ними (id=1) и сдвинуть
анкоры остальных.
------------------------------------------------------------------------------]]
-- ─── ПАТЧ #1 ───────────────────────────────────────────────────────────────
-- НАЙТИ блок вкладки SuggestTab (или то, что у вас сейчас называется
-- «$parentSuggestTab» / «$parentDungeonTab») и ДОБАВИТЬ перед ним
-- следующую кнопку. id=1, чтобы EJ_ContentTab_OnClick знал об этой вкладке.
<Button name="$parentGuideTab"
inherits="EncounterTierTabTemplate"
text="MW_PLAYER_GUIDE"
parentKey="guideTab"
id="1">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="TOPLEFT">
<Offset><AbsDimension x="40" y="2"/></Offset>
</Anchor>
</Anchors>
<Scripts>
<OnClick function="EJ_ContentTab_OnClick"/>
</Scripts>
</Button>
-- ─── ПАТЧ #2 ───────────────────────────────────────────────────────────────
-- ИЗМЕНИТЬ id существующих вкладок (теперь Путеводитель = 1):
--
-- SuggestTab id="2" (было 1)
-- DungeonTab id="3" (было 2)
-- RaidTab id="4" (было 3)
-- LootJournalTab id="5" (было 4)
--
-- И СДВИНУТЬ анкор первой следующей вкладки (SuggestTab), чтобы она
-- цеплялась за GuideTab:
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentGuideTab" relativePoint="BOTTOMRIGHT">
<Offset><AbsDimension x="-15" y="0"/></Offset>
</Anchor>
</Anchors>
-- ─── ПАТЧ #3 ───────────────────────────────────────────────────────────────
-- В конце <Ui>... </Ui>, ПЕРЕД закрывающим тегом, добавить include
-- для нашего PlayerGuide.xml, чтобы PlayerGuideFrame создавался ВНУТРИ
-- родителя EncounterJournal:
<Include file="PlayerGuide.xml"/>
@@ -1,43 +0,0 @@
--[[----------------------------------------------------------------------------
patches/Localization-ruRU.lua.patch
─────────────────────────────────────────────────────────────────────────────
Строки локализации для вкладки «Путеводитель».
Вставьте этот блок в ваш существующий Localization/ruRU.lua (или
ruRU.lua в корне), либо подключите как отдельный файл и добавьте в .toc.
------------------------------------------------------------------------------]]
-- Имя вкладки
MW_PLAYER_GUIDE = "Путеводитель"
-- Заголовки секций
MW_PLAYER_GUIDE_OBJECTIVES = "Задачи этапа"
MW_PLAYER_GUIDE_RECS = "Подходящие подземелья"
MW_PLAYER_GUIDE_REWARDS = "Награды за этап"
-- Шапка
MW_PLAYER_GUIDE_STAGE_OF = "ЭТАП %d ИЗ %d"
MW_PLAYER_GUIDE_STAGE_PROGRESS = "ПРОГРЕСС ЭТАПА"
MW_PLAYER_GUIDE_REFRESH = "Обновить"
-- Пустое состояние
MW_PLAYER_GUIDE_EMPTY_TITLE = "Путеводитель ещё не открыт"
MW_PLAYER_GUIDE_EMPTY_BODY = "Данные о вашем прогрессе ещё не получены с сервера. "..
"Это может занять несколько секунд после входа в игру."
-- Загрузка
MW_PLAYER_GUIDE_LOADING_TITLE = "ЗАПРАШИВАЕМ ХРОНИКИ"
MW_PLAYER_GUIDE_LOADING_BODY = "Ожидание данных от Путеводителя"
-- Caps-плашки типов целей (используются в карточке цели)
MW_PG_TYPE_LEVEL = "УРОВЕНЬ"
MW_PG_TYPE_QUEST = "ЗАДАНИЕ"
MW_PG_TYPE_QUEST_CHAIN = "ЦЕПОЧКА"
MW_PG_TYPE_DUNGEON = "ПОДЗЕМЕЛЬЕ"
MW_PG_TYPE_RAID = "РЕЙД"
MW_PG_TYPE_BOSS = "БОСС"
MW_PG_TYPE_ACHIEVEMENT = "ДОСТИЖЕНИЕ"
MW_PG_TYPE_REPUTATION = "РЕПУТАЦИЯ"
MW_PG_TYPE_PROFESSION = "ПРОФЕССИЯ"
MW_PG_TYPE_ITEM = "ПРЕДМЕТ"
MW_PG_TYPE_CURRENCY = "ВАЛЮТА"
MW_PG_TYPE_CUSTOM = "ЗАДАЧА"
@@ -1,28 +0,0 @@
# patches/MoonWellClient.toc.patch
#
# Сниппет для добавления в существующий MoonWellClient.toc. ВАЖНО: порядок
# подключения файлов критичен — C_PlayerGuide.lua должен идти ДО PlayerGuide.lua,
# а PlayerGuide.xml — ПОСЛЕ Custom_EncounterJournal.xml (PlayerGuideFrame
# анкорится к EncounterJournal как parent).
#
# Вставьте строки ниже в соответствующие места вашего .toc.
# ─── Локализация ────────────────────────────────────────────────────────────
# В блок Localization, рядом с остальными ruRU.lua:
Localization\ruRU.lua
# ─── Хранилище и API ────────────────────────────────────────────────────────
# В блок EncounterJournal\Utils\, до подключения чего-либо из
# Custom_EncounterJournal\:
EncounterJournal\Utils\C_PlayerGuide.lua
# ─── Сам Encounter Journal (ОБНОВЛЁННЫЙ ПОРЯДОК) ────────────────────────────
# Эти строки заменяют ваш существующий блок Custom_EncounterJournal:
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.lua
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
# ─── AIO-handler ────────────────────────────────────────────────────────────
# Должен подключаться ПОСЛЕ C_PlayerGuide.lua и AIO/AIO_Client.lua:
AIO\MoonWellPlayerGuide.lua
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 B

-125
View File
@@ -1,125 +0,0 @@
# PlayerGuide — текстуры
Все файлы — **PNG с альфа-каналом**, размеры — **степень двойки**
(требование BLP-конвертера). Палитра канон-WotLK: золото (`#ffd76a`)
на тёмно-коричневом (`#1a1208`).
## Что есть
| Файл | Размер | Куда |
|---|---|---|
| `StageBadge-Active.png` | 64×64 | Бейдж текущего этапа в цепочке. |
| `StageBadge-Done.png` | 64×64 | Бейдж завершённого этапа. |
| `StageBadge-Locked.png` | 64×64 | Бейдж недоступного этапа. |
| `ProgressBar-Fill.png` | 128×16 | Заливка прогресс-бара (`BarTexture`). |
| `ProgressBar-BG.png` | 128×16 | Подложка прогресс-бара. |
| `Card-Background.png` | 256×256 | Тайл-фон карточек (`bgFile`, tileable). |
| `Card-Border.png` | 256×32 | Рамка карточки (`edgeFile`, 8 сегментов 32×32: up/down/left/right/TL/TR/BL/BR). |
| `Section-Divider.png` | 256×16 | Декоративный разделитель секций. |
| `Corner-Ornament.png` | 64×64 | Угол декоративной рамки фрейма. |
| `Top-Wave-Glow.png` | 512×64 | «Волна» свечения сверху (как в оригинальном EJ). |
| `Biome-Fire.png` | 256×128 | Обложка рекомендации — огненные локации. |
| `Biome-Arcane.png` | 256×128 | Тайная магия. |
| `Biome-Storm.png` | 256×128 | Буря/вода. |
| `Biome-Shadow.png` | 256×128 | Тьма. |
| `Biome-Nature.png` | 256×128 | Природа. |
| `Biome-Holy.png` | 256×128 | Свет/нежить. |
| `TypeIcon-Level.png` | 64×64 | Иконка цели «уровень». |
| `TypeIcon-Quest.png` | 64×64 | «задание». |
| `TypeIcon-QuestChain.png` | 64×64 | «цепочка». |
| `TypeIcon-Dungeon.png` | 64×64 | «подземелье». |
| `TypeIcon-Raid.png` | 64×64 | «рейд». |
| `TypeIcon-Boss.png` | 64×64 | «босс». |
| `TypeIcon-Achievement.png` | 64×64 | «достижение». |
| `TypeIcon-Reputation.png` | 64×64 | «репутация». |
| `TypeIcon-Profession.png` | 64×64 | «профессия». |
| `TypeIcon-Item.png` | 64×64 | «предмет». |
| `TypeIcon-Currency.png` | 64×64 | «валюта». |
| `TypeIcon-Custom.png` | 64×64 | прочее. |
| `TypeIcon-Done.png` | 64×64 | Цель завершена (зелёная галочка). |
| `TypeIcon-Locked.png` | 64×64 | Цель заблокирована (замок). |
| `Button-Up.png` | 128×32 | Кнопка «Обновить», нормальное. |
| `Button-Down.png` | 128×32 | Кнопка нажата. |
| `Button-Highlight.png` | 128×32 | Hover. |
## Куда положить в проекте
```
MoonWellClient/EncounterJournal/Textures/PlayerGuide/
├── StageBadge-Active.blp
├── StageBadge-Done.blp
├── ...
└── Button-Highlight.blp
```
И в XML использовать пути:
```
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Active"
```
(WoW сам подставит `.blp`.)
## Конвертация PNG → BLP
Любой из:
- **BLPNG Converter** (Windows, GUI) — самый простой, drag-and-drop;
- **BLPLab** — с предпросмотром;
- **CLI**: `blpconverter input.png output.blp` (есть пайплайны на GitHub).
Параметры:
- **Compression:** `DXT5` для всего с альфой (всё, кроме `Card-Background`
для него `DXT1` или `Uncompressed`, если хочется лучше);
- **Mipmaps:** включить (`Generate Mipmaps`), кроме `ProgressBar-Fill/BG`
(тонкие полоски ломаются на мипмапах — там лучше off);
- **Sampling:** `Bicubic` или `Lanczos`.
## Замена биом-обложек
Сейчас биом-обложки — стилизованные плашки с аркой и светящимся центром.
Их можно полностью заменить вашими собственными — главное, оставить размер
**256×128** и имена файлов. Если поменяете список биомов, поправьте таблицу
`BIOME_COLOR` в `PlayerGuide.lua`.
## Замена иконок типов целей
Если у вас есть атлас иконок WoW (например, из `Interface/Icons/`) — карта
`TYPE_ICON` в `PlayerGuide.lua` уже использует встроенные текстуры
клиента. Текстуры `TypeIcon-*` нужны, только если хочется единого
визуального словаря (рамка + глиф).
## Применение в XML (примеры)
```xml
<!-- Бейдж в цепочке этапов: меняем встроенный UI-Quickslot2 на наш -->
<Texture parentKey="BadgeBG"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\StageBadge-Active">
<Size><AbsDimension x="44" y="44"/></Size>
<Anchors><Anchor point="CENTER"/></Anchors>
</Texture>
<!-- Прогресс-бар: BarTexture указывает на Fill, BG ставится в Layer BACKGROUND -->
<StatusBar parentKey="ProgressBar">
<BarTexture file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-Fill"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true"
file="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\ProgressBar-BG"/>
</Layer>
</Layers>
</StatusBar>
<!-- Карточка цели: bgFile = Card-Background, edgeFile = Card-Border -->
<Backdrop bgFile="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Card-Background"
edgeFile="Interface\AddOns\MoonWellClient\EncounterJournal\Textures\PlayerGuide\Card-Border"
tile="true">
<BackgroundInsets><AbsInset left="4" right="4" top="4" bottom="4"/></BackgroundInsets>
<TileSize><AbsValue val="64"/></TileSize>
<EdgeSize><AbsValue val="16"/></EdgeSize>
</Backdrop>
```
## Сноска
Это плейсхолдеры на уровне «лучше, чем встроенные UI-Quickslot2». Если в
проекте есть художник — финальные текстуры стоит делать в этом же
визуальном словаре, чтобы Lua/XML менять не пришлось (только перерисовать
PNG того же размера и сконвертировать).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

-263
View File
@@ -1,263 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<title>PlayerGuide — Texture Sheet</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=EB+Garamond:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet" />
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; min-height: 100%; }
body {
font-family: 'EB Garamond', Georgia, serif;
background:
radial-gradient(ellipse at 50% 0%, #1c2c48 0%, transparent 60%),
radial-gradient(ellipse at 100% 100%, #0d1422 0%, transparent 70%),
linear-gradient(180deg, #0a1018 0%, #050810 100%);
color: #d8c8a0;
padding: 60px 50px 80px;
}
h1 {
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 36px; margin: 0 0 6px;
color: #ffd76a;
text-shadow: 0 0 14px rgba(197,151,46,0.5), 0 2px 0 #000;
letter-spacing: 0.02em;
}
.sub {
font-style: italic; color: #a89878;
margin: 0 0 36px; font-size: 16px;
}
.group {
margin: 0 0 44px;
}
.group-title {
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 13px; letter-spacing: 0.18em; text-transform: uppercase;
color: #ffd76a; text-shadow: 0 1px 0 #000;
display: flex; align-items: center; gap: 12px;
margin: 0 0 18px;
}
.group-title::before {
content: ''; width: 3px; height: 14px;
background: #ffd76a; box-shadow: 0 0 6px #ffd76a88;
}
.group-title::after {
content: ''; flex: 1; height: 1px;
background: linear-gradient(90deg, rgba(138,106,38,0.5), transparent);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 14px;
}
.item {
background: linear-gradient(180deg, rgba(28,44,72,0.4), rgba(10,16,24,0.4));
border: 1px solid rgba(255,215,106,0.18);
border-radius: 3px;
padding: 12px 12px 10px;
display: flex; flex-direction: column; align-items: center;
}
.preview {
width: 100%; aspect-ratio: 1 / 1;
display: flex; align-items: center; justify-content: center;
background:
linear-gradient(45deg, #0d0905 25%, transparent 25%),
linear-gradient(-45deg, #0d0905 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #0d0905 75%),
linear-gradient(-45deg, transparent 75%, #0d0905 75%);
background-size: 16px 16px;
background-position: 0 0, 0 8px, 8px -8px, -8px 0px;
background-color: #18120a;
border: 1px solid rgba(125,90,42,0.4);
margin-bottom: 8px;
overflow: hidden;
}
.preview.wide { aspect-ratio: 2 / 1; }
.preview.bar { aspect-ratio: 4 / 1; }
.preview img {
max-width: 100%; max-height: 100%;
image-rendering: pixelated;
}
.name {
font-family: 'Cinzel', serif; font-weight: 600;
font-size: 11px; color: #ffe9a0;
text-shadow: 0 1px 0 #000;
letter-spacing: 0.04em; text-align: center;
line-height: 1.2;
}
.size {
font-size: 10px; color: #a89878;
font-style: italic; margin-top: 3px;
font-variant-numeric: tabular-nums;
}
/* Showcase row */
.showcase {
margin: 24px 0 40px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.showcase-card {
background: linear-gradient(180deg, rgba(60,40,18,0.35), rgba(25,15,8,0.55));
border: 1px solid #5a4220;
box-shadow: inset 0 1px 0 rgba(255,200,100,0.18), 0 4px 12px rgba(0,0,0,0.5);
border-radius: 2px;
padding: 18px;
display: flex; gap: 14px; align-items: stretch;
}
.showcase-card h3 {
margin: 0 0 4px;
font-family: 'Cinzel', serif; font-weight: 700;
font-size: 14px; color: #ffe9a0; text-shadow: 0 1px 0 #000;
}
.showcase-card p {
margin: 0; font-style: italic; font-size: 12px; color: #a89878;
}
.progress-demo {
margin-top: 10px;
height: 14px; width: 100%; position: relative;
background: url('Card-Background.png');
}
</style>
</head>
<body>
<h1>PlayerGuide — лист текстур</h1>
<p class="sub">33 PNG-файла, готовы к конвертации в BLP. Шахматный фон под превью = прозрачность.</p>
<!-- Demo: показываем как они стыкуются вместе -->
<div class="group">
<div class="group-title">Демонстрация (как это смотрится в карточке)</div>
<div class="showcase">
<div class="showcase-card">
<img src="TypeIcon-Dungeon.png" width="44" height="44" alt="" />
<div style="flex:1;">
<h3>Пройти Кузню Крови</h3>
<p>Цитадель Адского Пламени • рек. уровень 61—64</p>
<div style="margin-top:8px; display:flex; gap:8px; align-items:center;">
<div style="flex:1; height:8px; background: url('ProgressBar-BG.png'); position:relative;">
<div style="width:35%; height:100%; background: url('ProgressBar-Fill.png');"></div>
</div>
<span style="font-family:Cinzel,serif; font-size:11px; color:#caa760;">0 / 1</span>
</div>
</div>
</div>
<div class="showcase-card" style="background: url('Card-Background.png'); border-color: #5a4220;">
<img src="StageBadge-Active.png" width="48" height="48" alt="" />
<div style="flex:1;">
<h3 style="color:#ffd76a;">Подготовка к Нордсколу</h3>
<p>Этап V из VII · 62% завершено</p>
<div style="margin-top:10px; height:14px; position:relative; background: url('ProgressBar-BG.png');">
<div style="width:62%; height:100%; background: url('ProgressBar-Fill.png');"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Группы текстур -->
<div class="group">
<div class="group-title">Бейджи этапов</div>
<div class="grid" id="stage"></div>
</div>
<div class="group">
<div class="group-title">Прогресс-бар</div>
<div class="grid" id="progress"></div>
</div>
<div class="group">
<div class="group-title">Карточка</div>
<div class="grid" id="card"></div>
</div>
<div class="group">
<div class="group-title">Декор</div>
<div class="grid" id="decor"></div>
</div>
<div class="group">
<div class="group-title">Обложки рекомендаций (биомы)</div>
<div class="grid" id="biome"></div>
</div>
<div class="group">
<div class="group-title">Иконки типов целей</div>
<div class="grid" id="types"></div>
</div>
<div class="group">
<div class="group-title">Иконки статусов</div>
<div class="grid" id="status"></div>
</div>
<div class="group">
<div class="group-title">Кнопки</div>
<div class="grid" id="buttons"></div>
</div>
<script>
const groups = {
stage: [
['StageBadge-Active', '64×64'],
['StageBadge-Done', '64×64'],
['StageBadge-Locked', '64×64'],
],
progress: [
['ProgressBar-Fill', '128×16', 'bar'],
['ProgressBar-BG', '128×16', 'bar'],
],
card: [
['Card-Background', '256×256'],
['Card-Border', '256×32', 'bar'],
],
decor: [
['Section-Divider', '256×16', 'bar'],
['Corner-Ornament', '64×64'],
['Top-Wave-Glow', '512×64', 'bar'],
],
biome: [
['Biome-Fire', '256×128', 'wide'],
['Biome-Arcane', '256×128', 'wide'],
['Biome-Storm', '256×128', 'wide'],
['Biome-Shadow', '256×128', 'wide'],
['Biome-Nature', '256×128', 'wide'],
['Biome-Holy', '256×128', 'wide'],
],
types: [
['TypeIcon-Level', '64×64'],
['TypeIcon-Quest', '64×64'],
['TypeIcon-QuestChain', '64×64'],
['TypeIcon-Dungeon', '64×64'],
['TypeIcon-Raid', '64×64'],
['TypeIcon-Boss', '64×64'],
['TypeIcon-Achievement', '64×64'],
['TypeIcon-Reputation', '64×64'],
['TypeIcon-Profession', '64×64'],
['TypeIcon-Item', '64×64'],
['TypeIcon-Currency', '64×64'],
['TypeIcon-Custom', '64×64'],
],
status: [
['TypeIcon-Done', '64×64'],
['TypeIcon-Locked', '64×64'],
],
buttons: [
['Button-Up', '128×32', 'bar'],
['Button-Down', '128×32', 'bar'],
['Button-Highlight', '128×32', 'bar'],
],
};
for (const [groupId, items] of Object.entries(groups)) {
const el = document.getElementById(groupId);
for (const [name, size, shape] of items) {
const item = document.createElement('div');
item.className = 'item';
item.innerHTML = `
<div class="preview ${shape || ''}">
<img src="${name}.png" alt="${name}" />
</div>
<div class="name">${name}</div>
<div class="size">${size}</div>
`;
el.appendChild(item);
}
}
</script>
</body>
</html>
+135
View File
@@ -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
-448
View File
@@ -1,448 +0,0 @@
# Player Guide Requirements
## Goal
Create a `Путеводитель` tab inside the existing Encounter Journal UI.
The guide must show personal character progress calculated by `mod-individual-progress` and recommend the next relevant content: quests, dungeons, raids, bosses, requirements, activities, and rewards.
This feature is not a third Encounter Journal instance type. It is a separate panel inside the same interface.
## High-Level Architecture
```text
AzerothCore / mod-individual-progress
-> calculates player progress
-> builds a guide payload
AIO / MoonWell bridge
-> sends guide data to the client
MoonWellClient / EncounterJournal
-> receives the payload
-> stores it in C_PlayerGuide
-> renders the Player Guide tab
```
## Client Components
Add separate client-side files:
```text
EncounterJournal/Utils/C_PlayerGuide.lua
EncounterJournal/Custom_EncounterJournal/PlayerGuide.lua
EncounterJournal/Custom_EncounterJournal/PlayerGuide.xml
```
Responsibilities:
```text
C_PlayerGuide.lua
- stores current guide data
- exposes a small UI-facing API
- receives server payloads
- requests refreshes from the server
PlayerGuide.lua
- renders guide data
- refreshes objective rows
- handles clicks on objectives/recommendations
PlayerGuide.xml
- defines the guide panel
- defines static frames, headers, scroll area, row templates
```
Register these files in `MoonWellClient.toc` before `Custom_EncounterJournal.lua`.
## Encounter Journal Tab
Add a new top-level tab near `Подземелья` and `Рейды`:
```text
Путеводитель | Подземелья | Рейды | Комплекты
```
The tab must live in:
```text
EncounterJournal/Custom_EncounterJournal/Custom_EncounterJournal.xml
```
Expected XML shape:
```xml
<Button name="$parentGuideTab"
inherits="EncounterTierTabTemplate"
text="MW_PLAYER_GUIDE"
parentKey="guideTab"
id="5">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentSuggestTab" relativePoint="BOTTOMRIGHT" x="35" y="0"/>
</Anchors>
<Scripts>
<OnClick function="EJ_ContentTab_OnClick"/>
</Scripts>
</Button>
```
If the guide tab is inserted before existing tabs, anchors for `DungeonTab`, `RaidTab`, and `LootJournalTab` must be adjusted.
Add a localized string:
```lua
MW_PLAYER_GUIDE = "Путеводитель"
```
## Tab Selection Behavior
When the guide tab is selected:
```text
- hide instance cards
- hide Encounter Journal detail panel
- hide suggested content panel
- hide loot journal panel
- hide or disable the expansion/tier dropdown unless explicitly needed
- show PlayerGuideFrame
```
When any other top-level tab is selected:
```text
- hide PlayerGuideFrame
```
Do not route this tab through:
```lua
EJ_GetInstanceByIndex(index, isRaid)
```
That API is only for dungeon/raid instance lists.
## Client API
Create:
```lua
C_PlayerGuide = C_PlayerGuide or {}
```
Required methods:
```lua
C_PlayerGuide.SetData(data)
C_PlayerGuide.GetStageInfo()
C_PlayerGuide.GetNumObjectives()
C_PlayerGuide.GetObjectiveInfo(index)
C_PlayerGuide.GetNumRecommendations()
C_PlayerGuide.GetRecommendationInfo(index)
C_PlayerGuide.GetNumRewards()
C_PlayerGuide.GetRewardInfo(index)
C_PlayerGuide.RequestRefresh()
```
Minimum behavior:
```text
SetData(data)
- validate payload is a table
- replace current data
- fire/trigger a UI refresh if the guide panel is visible
GetStageInfo()
- returns stageID, stageName, stageDescription, progress
GetObjectiveInfo(index)
- returns a normalized objective record or unpacked objective fields
RequestRefresh()
- asks the server to resend the current payload
```
## Server Payload
The server should send one normalized payload per character.
Minimum shape:
```lua
{
version = 1,
characterGuid = 123,
stageID = 10,
stageName = "Подготовка к Нордсколу",
stageDescription = "Завершите ключевые шаги перед переходом дальше.",
progress = 40,
objectives = {
{
id = 1,
type = "level",
title = "Достигнуть 68 уровня",
description = "",
completed = true,
progress = 68,
required = 68,
targetID = 68,
order = 1,
},
{
id = 2,
type = "quest",
title = "Завершить цепочку Темного портала",
description = "",
completed = false,
progress = 3,
required = 8,
targetID = 10119,
order = 2,
},
{
id = 3,
type = "dungeon",
title = "Пройти Кузню Крови",
description = "",
completed = false,
progress = 0,
required = 1,
targetID = 256,
instanceID = 256,
order = 3,
},
},
recommendations = {
{
id = 1,
type = "dungeon",
title = "Кузня Крови",
description = "Подходит для текущего этапа.",
instanceID = 256,
priority = 100,
},
},
rewards = {
{
type = "item",
itemID = 12345,
count = 1,
},
},
}
```
## Objective Types
Support at least:
```text
level
quest
quest_chain
dungeon
raid
boss
achievement
reputation
profession
item
currency
custom
```
Every objective should use the common fields:
```text
id
type
title
description
completed
progress
required
targetID
order
```
Optional type-specific fields:
```text
questID
questChainID
instanceID
encounterID
achievementID
factionID
professionID
itemID
currencyID
```
## Objective Click Behavior
Recommended behavior:
```text
quest
- show quest link or quest hint if available
dungeon
- open Encounter Journal on instanceID
raid
- open Encounter Journal on instanceID
boss
- open Encounter Journal on encounterID
item
- show item tooltip
achievement
- show achievement UI/link
custom
- show text-only detail or request a server action
```
If a target cannot be opened, the UI should fail quietly and keep the guide visible.
## AIO Contract
Use a separate handler namespace:
```text
MoonWellPlayerGuide
```
Server to client:
```lua
SetData(payload)
UpdateObjective(objective)
SetStage(stagePayload)
Notify(message)
```
Client to server:
```lua
RequestRefresh()
RequestTrackObjective(objectiveID)
RequestOpenTarget(objectiveID)
```
Minimum MVP only needs:
```lua
SetData(payload)
RequestRefresh()
```
## AzerothCore / mod-individual-progress Requirements
`mod-individual-progress` must:
```text
- determine the player's current progression stage
- calculate objective completion state
- calculate objective progress values
- build the guide payload
- send the payload on login
- send updates when progress changes
- respond to explicit client refresh requests
```
Recommended update events:
```text
login
level up
quest accepted
quest completed
quest rewarded
achievement earned
boss kill
dungeon completed
reputation changed
profession changed
item acquired, if item objectives are used
```
## UI Requirements
MVP UI:
```text
- guide tab
- stage title
- stage description
- total progress percentage
- objective list
- completed/incomplete visual state
- recommendation list
- manual refresh button
```
Later UI:
```text
- categories
- filters: all / active / completed
- tracked objective pinning
- reward preview
- requirement tooltips
- direct "Open in Journal" buttons
- server notifications
```
## Non-Goals
Do not:
```text
- store guide progress in JOURNALINSTANCE
- make the guide a third dungeon/raid instance category
- overload EJ_GetInstanceByIndex(index, isRaid)
- depend on Sirus-only globals or realm constants
- block the old dungeon/raid Encounter Journal if guide data is missing
```
## MVP Implementation Order
1. Add the `Путеводитель` tab.
2. Add an empty `PlayerGuideFrame`.
3. Add `C_PlayerGuide.lua` with static test data.
4. Render the stage title and three test objectives.
5. Wire tab switching and hide/show behavior.
6. Add AIO `MoonWellPlayerGuide.SetData`.
7. Send test payload from the server.
8. Replace static data with real `mod-individual-progress` data.
9. Add click behavior for dungeon/raid/boss objectives.
10. Add polish: progress bar, icons, rewards, filters.
## Validation
Client should tolerate missing or malformed fields:
```text
- missing payload -> show empty state
- missing objectives -> show "no active objectives"
- missing recommendations -> hide recommendations block
- unknown objective type -> render as text-only custom objective
- invalid instanceID/encounterID -> do not throw Lua errors
```
Server should validate:
```text
- objective ids are unique within the payload
- order is stable
- type is known or custom
- progress <= required when required is present
- dungeon/raid/boss targets exist in Encounter Journal data when used
```
+65
View File
@@ -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;
}
+9
View File
@@ -0,0 +1,9 @@
LIBRARY d3d9
EXPORTS
Direct3DCreate9
Direct3DCreate9Ex
WxlD3D12Device
WxlD3D12Queue
WxlD3D12DrainDebug
WxlSetSsaaFactor
WxlGetSsaaFactor
-339
View File
@@ -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()
+406
View File
@@ -0,0 +1,406 @@
// 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/Hook.hpp"
#include "core/Mem.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"
#include "offsets/engine/Gx.hpp"
#include <windows.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace moonwell
{
namespace
{
using GxSetProjectionFn = wxl::offsets::engine::gx::GxSetProjectionFn;
GxSetProjectionFn g_nextSetProjection = nullptr;
// FrameXML's stock SetCreature only accepts a creature entry and waits
// for its client cache record. Encounter Journal already has the exact
// CreatureDisplayInfo ID, so accept it as an optional second argument.
constexpr uintptr_t kSetCreature = 0x00597960;
constexpr uintptr_t kGetCurrentModelFrame = 0x004A81B0;
constexpr uintptr_t kModelFrameTypeToken = 0x00C0E4D4;
constexpr uintptr_t kApplyCreatureCacheRecord = 0x00597700;
using SetCreatureFn = int(__cdecl*)(void* state);
using ApplyCreatureCacheRecordFn = void(__fastcall*)(void* frame, void* edx,
const void* cacheRecord);
SetCreatureFn g_nextSetCreature = nullptr;
alignas(4) std::array<uint32_t, 10> g_encounterJournalCreatureRecord{};
void* GetCurrentModelFrame(void* state, uint32_t typeToken)
{
// FrameScript_GetObject takes the type token on the stack, but the
// 3.3.5 client also expects lua_State in ESI (an internal calling
// convention not expressible with a regular C function pointer).
void* frame = nullptr;
__asm
{
mov esi, state
push typeToken
mov eax, kGetCurrentModelFrame
call eax
add esp, 4
mov frame, eax
}
return frame;
}
int __cdecl SetCreatureDisplayInfoHook(void* state)
{
const int result = g_nextSetCreature ? g_nextSetCreature(state) : 0;
if (!state || !wxl::runtime::lua::IsNumber(state, 3))
return result;
const double requestedDisplayInfo = wxl::runtime::lua::ToNumber(state, 3);
if (requestedDisplayInfo <= 0.0 || requestedDisplayInfo > 4294967295.0)
return result;
const auto displayInfo = static_cast<uint32_t>(requestedDisplayInfo);
__try
{
const uint32_t typeToken = *reinterpret_cast<const uint32_t*>(kModelFrameTypeToken);
if (!typeToken)
return result;
void* frame = GetCurrentModelFrame(state, typeToken);
if (!frame)
return result;
// Both the initial loader and the later character-appearance
// pass read displayInfo at +0x24. Keep this record alive and
// attach it to the frame: character models use it asynchronously
// to resolve CreatureDisplayInfoExtra, baked skin and equipment.
g_encounterJournalCreatureRecord.fill(0);
g_encounterJournalCreatureRecord[9] = displayInfo;
*reinterpret_cast<const void**>(static_cast<uint8_t*>(frame) + 0x378) =
g_encounterJournalCreatureRecord.data();
reinterpret_cast<ApplyCreatureCacheRecordFn>(kApplyCreatureCacheRecord)(
frame, nullptr, g_encounterJournalCreatureRecord.data());
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
WLOG_ERROR("moonwell: SetCreature displayInfo bridge failed for %u", displayInfo);
}
return result;
}
void InstallEncounterJournalModelPreview()
{
if (!wxl::core::hook::Install("MoonWellSetCreatureDisplayInfo", kSetCreature,
&SetCreatureDisplayInfoHook, &g_nextSetCreature))
{
WLOG_ERROR("moonwell: encounter journal model hook installation failed");
return;
}
WLOG_INFO("moonwell: encounter journal displayInfo model bridge installed");
}
struct CameraTransition
{
float zoom = 1.0f;
float verticalOffset = 0.0f;
float startZoom = 1.0f;
float startVerticalOffset = 0.0f;
float targetZoom = 1.0f;
float targetVerticalOffset = 0.0f;
DWORD startedAt = 0;
DWORD duration = 0;
} g_characterCreateCamera;
void UpdateCharacterCreateCamera(DWORD now)
{
auto& camera = g_characterCreateCamera;
if (!camera.duration)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
return;
}
const DWORD elapsed = now - camera.startedAt;
float t = static_cast<float>(elapsed) / static_cast<float>(camera.duration);
if (t >= 1.0f)
{
camera.zoom = camera.targetZoom;
camera.verticalOffset = camera.targetVerticalOffset;
camera.duration = 0;
return;
}
t = t * t * (3.0f - 2.0f * t);
camera.zoom = camera.startZoom + (camera.targetZoom - camera.startZoom) * t;
camera.verticalOffset = camera.startVerticalOffset
+ (camera.targetVerticalOffset - camera.startVerticalOffset) * t;
}
int __cdecl SetCharacterCreateCamera(void* state)
{
const bool enabled = state && wxl::runtime::lua::IsNumber(state, 1)
&& wxl::runtime::lua::ToNumber(state, 1) != 0.0;
const float faceZoom = state && wxl::runtime::lua::IsNumber(state, 2)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 2)) : 2.0f;
const float faceVerticalOffset = state && wxl::runtime::lua::IsNumber(state, 3)
? static_cast<float>(wxl::runtime::lua::ToNumber(state, 3)) : -0.65f;
double requestedDuration = state && wxl::runtime::lua::IsNumber(state, 4)
? wxl::runtime::lua::ToNumber(state, 4) : 500.0;
if (requestedDuration < 0.0) requestedDuration = 0.0;
if (requestedDuration > 2000.0) requestedDuration = 2000.0;
const DWORD now = GetTickCount();
UpdateCharacterCreateCamera(now);
auto& camera = g_characterCreateCamera;
camera.startZoom = camera.zoom;
camera.startVerticalOffset = camera.verticalOffset;
camera.targetZoom = enabled && faceZoom > 1.0f ? faceZoom : 1.0f;
camera.targetVerticalOffset = enabled ? faceVerticalOffset : 0.0f;
camera.startedAt = now;
camera.duration = static_cast<DWORD>(requestedDuration);
UpdateCharacterCreateCamera(now);
return 0;
}
void __fastcall CharacterCreateProjectionHook(void* self, void* edx, const void* projection)
{
if (!g_nextSetProjection)
return;
UpdateCharacterCreateCamera(GetTickCount());
const auto& camera = g_characterCreateCamera;
if (projection && (camera.zoom != 1.0f || camera.verticalOffset != 0.0f))
{
const float* source = static_cast<const float*>(projection);
if (source[11] > 0.5f)
{
float adjusted[16];
std::memcpy(adjusted, source, sizeof(adjusted));
adjusted[0] *= camera.zoom;
adjusted[5] *= camera.zoom;
adjusted[9] += camera.verticalOffset;
g_nextSetProjection(self, edx, adjusted);
return;
}
}
g_nextSetProjection(self, edx, projection);
}
void InstallCharacterCreateCamera()
{
namespace gx = wxl::offsets::engine::gx;
void** vtable = reinterpret_cast<void**>(gx::kGxDeviceVTable);
void** slot = &vtable[gx::kGxSetProjectionSlot];
if (*slot == reinterpret_cast<void*>(&CharacterCreateProjectionHook))
return;
g_nextSetProjection = reinterpret_cast<GxSetProjectionFn>(*slot);
void* replacement = reinterpret_cast<void*>(&CharacterCreateProjectionHook);
if (!g_nextSetProjection || !wxl::core::mem::Patch(slot, &replacement, sizeof(replacement)))
{
g_nextSetProjection = nullptr;
WLOG_ERROR("moonwell: character-create projection hook installation failed");
return;
}
WLOG_INFO("moonwell: smooth character-create camera installed");
}
struct Patch
{
const char* name;
uintptr_t address;
const uint8_t* expected;
const uint8_t* replacement;
size_t size;
};
template <size_t N>
bool Apply(const char* name, uintptr_t address,
const std::array<uint8_t, N>& expected,
const std::array<uint8_t, N>& replacement)
{
const auto* current = reinterpret_cast<const uint8_t*>(address);
if (std::memcmp(current, replacement.data(), N) == 0)
return true;
if (std::memcmp(current, expected.data(), N) != 0)
{
WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required",
name, reinterpret_cast<void*>(address));
return false;
}
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(address), replacement.data(), N))
{
WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
reinterpret_cast<void*>(address));
return false;
}
WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast<void*>(address));
return true;
}
bool InstallGlueUnlock()
{
// Same stock-client checks formerly changed in the distributed Wow.exe.
// They allow custom GlueXML while keeping WarcraftXL's callback validator intact.
bool ok = true;
ok &= Apply("glue archive override", 0x005F4DBF,
std::array<uint8_t, 1>{0x74}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback gate", 0x00816625,
std::array<uint8_t, 1>{0x75}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback result A", 0x0081663F,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback result B", 0x00816695,
std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
ok &= Apply("glue callback compare", 0x00816746,
std::array<uint8_t, 1>{0x7F}, std::array<uint8_t, 1>{0xEB});
ok &= Apply("glue callback return", 0x0081675F,
std::array<uint8_t, 7>{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D},
std::array<uint8_t, 7>{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED});
return ok;
}
bool InstallCharacterModePacket()
{
// CreateCharacter(name, modeId): capture Lua argument 2 and place it in
// the final CMSG_CHAR_CREATE byte that is zero in the stock client.
constexpr std::array<uint8_t, 64> expectedCreate = {
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00,
0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4,
0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00,
0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3,
0xCC,0xCC,0xCC,0xCC,0xCC
};
constexpr std::array<uint8_t, 64> replacementCreate = {
0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00,
0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A,
0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4,
0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC,
0x00,0x88,0x55,0xF4,0xC3
};
constexpr std::array<uint8_t, 7> expectedPacket =
{0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00};
constexpr std::array<uint8_t, 7> replacementPacket =
{0xE8,0x5F,0x08,0x00,0x00,0x90,0x90};
return Apply("CreateCharacter mode argument", 0x004E0C60,
expectedCreate, replacementCreate)
&& Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F,
expectedPacket, replacementPacket);
}
bool InstallCharacterFlags()
{
// GetCharacterInfo(index) gains return value 11: the character flags
// from CharacterInfo+0x170. The glue code uses bit 0x40000000.
constexpr uintptr_t patchAddress = 0x004E332D;
constexpr uintptr_t returnAddress = 0x004E3336;
constexpr uintptr_t luaPushNumber = 0x0084E2A0;
constexpr std::array<uint8_t, 5> expected = {0x83,0xC4,0x2C,0x5E,0xB8};
if (*reinterpret_cast<const uint8_t*>(patchAddress) == 0xE9)
return true;
if (std::memcmp(reinterpret_cast<const void*>(patchAddress), expected.data(), expected.size()) != 0)
{
WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p",
reinterpret_cast<void*>(patchAddress));
return false;
}
auto* cave = static_cast<uint8_t*>(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE));
if (!cave)
{
WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError());
return false;
}
const uint8_t prefix[] = {
0x83,0xEC,0x08, // sub esp, 8
0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h]
0xDD,0x1C,0x24, // fstp qword ptr [esp]
0x57, // push edi (lua_State*)
0xE8,0,0,0,0, // call lua_pushnumber
0x83,0xC4,0x38, // cleanup own args + stolen add esp,2Ch
0x5E, // stolen pop esi
0x6A,0x0B,0x58, // eax = 11
0xE9,0,0,0,0 // jump to stock epilogue
};
static_assert(sizeof(prefix) == 30);
std::memcpy(cave, prefix, sizeof(prefix));
const auto callRel = static_cast<int32_t>(luaPushNumber -
(reinterpret_cast<uintptr_t>(cave) + 18));
const auto backRel = static_cast<int32_t>(returnAddress -
(reinterpret_cast<uintptr_t>(cave) + 30));
std::memcpy(cave + 14, &callRel, sizeof(callRel));
std::memcpy(cave + 26, &backRel, sizeof(backRel));
FlushInstructionCache(GetCurrentProcess(), cave, 30);
std::array<uint8_t, 5> jump{0xE9,0,0,0,0};
const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
(patchAddress + jump.size()));
std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
if (!wxl::core::mem::Patch(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
{
VirtualFree(cave, 0, MEM_RELEASE);
WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
return false;
}
WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11");
return true;
}
void InstallBoot()
{
const bool glue = InstallGlueUnlock();
const bool create = InstallCharacterModePacket();
const bool flags = InstallCharacterFlags();
if (!glue || !create || !flags)
WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
else
WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
wxl::core::log::Flush();
}
DWORD WINAPI FlushRuntimeLog(LPVOID)
{
// Let the remainder of RunAll() and the core-ready line reach the
// shared buffered logger, then make startup diagnostics durable.
Sleep(1000);
wxl::core::log::Flush();
return 0;
}
void InstallRuntimeLogFlush()
{
HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
if (thread) CloseHandle(thread);
}
struct Registration
{
Registration()
{
wxl::runtime::lua::RegisterFunction(
"MoonWellSetCharacterCreateCamera", &SetCharacterCreateCamera);
wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
wxl::runtime::modules::Register(
"moonwell-character-create-camera", &InstallCharacterCreateCamera);
wxl::runtime::modules::Register(
"moonwell-encounter-journal-models", &InstallEncounterJournalModelPreview);
wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
}
} g_registration;
}
}
+32
View File
@@ -1,3 +1,9 @@
param(
[Parameter(Mandatory=$false)]
[ValidateSet("production", "ptr", "local")]
[string]$Env = "local"
)
# Stop on errors
$ErrorActionPreference = "Stop"
@@ -36,6 +42,7 @@ $SRC_DIR = Join-Path $ROOT "src"
$DIST_DIR = Join-Path $ROOT "dist"
$TOOL = Join-Path $ROOT "tool\target\release\tool.exe"
$RELOAD_SCRIPT = Join-Path $ROOT "reload_wow.bat"
$WXL_BUILD_SCRIPT = Join-Path $ROOT "build-warcraftxl.ps1"
# --- Ensure tool exists
if (!(Test-Path $TOOL)) {
@@ -59,6 +66,16 @@ if (!(Test-Path (Join-Path $DIST_DIR "Data"))) {
exit 1
}
# --- Build/package/deploy WarcraftXL before the dist sync. The packaged files
# are uploaded by upload_to_s3.py and consumed by the launcher manifest.
Write-Host "Building and packaging WarcraftXL..."
& $WXL_BUILD_SCRIPT -Configuration Release -ClientPath $WOW_HOME `
-PackagePath $DIST_DIR -Deploy
if ($LASTEXITCODE -ne 0) {
Write-Error "WarcraftXL build/deploy failed!"
exit 1
}
# --- Sync dist/ -> WOW_HOME
Write-Host "Syncing dist/ -> WOW_HOME..."
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
@@ -70,6 +87,21 @@ if ($LASTEXITCODE -ge 8) {
Write-Host "MPQ archives deployed to $WOW_HOME"
# --- Write realmlist.wtf based on selected environment
$realmlist = switch ($Env) {
"production" { $env:PRODUCTION_REALMLIST }
"ptr" { $env:PTR_REALMLIST }
"local" { $env:LOCAL_REALMLIST }
}
if ($realmlist) {
$realmlistPath = Join-Path $WOW_HOME "Data\ruRU\realmlist.wtf"
Set-Content -Path $realmlistPath -Value "set realmlist $realmlist" -Encoding ASCII
Write-Host "Realmlist ($Env): $realmlist"
} else {
Write-Warning "REALMLIST for '$Env' is not set in .env -- skipping realmlist.wtf"
}
# --- Run WoW reload script
Write-Host "Launching WoW..."
cmd /c $RELOAD_SCRIPT
@@ -676,6 +676,44 @@ function CharacterCreate_UpdateGameModeVisibility()
end
end
-- The stock creation scene frames each race at a different height. Projection
-- zoom therefore needs a per-race vertical target to keep the face in frame.
-- Values are indexed by GetSelectedSex(): 2 male, 3 female.
local CHARACTER_CREATE_CAMERA_PROFILES = {
HUMAN = { [2] = -0.28, [3] = -0.20 },
ORC = { [2] = -0.50, [3] = -0.30 },
DWARF = { [2] = 0.30, [3] = 0.38 },
NIGHTELF = { [2] = -0.65, [3] = -0.65 },
SCOURGE = { [2] = 0.18, [3] = 0.27 },
TAUREN = { [2] = -0.78, [3] = -0.62 },
GNOME = { [2] = 0.64, [3] = 0.72 },
TROLL = { [2] = -0.70, [3] = -0.55 },
BLOODELF = { [2] = -0.30, [3] = -0.22 },
DRAENEI = { [2] = -0.62, [3] = -0.55 },
};
local function CharacterCreate_GetCameraProfile()
local _, raceFile = GetNameForRace();
local raceProfile = raceFile and CHARACTER_CREATE_CAMERA_PROFILES[strupper(raceFile)];
local sex = GetSelectedSex();
return 2.0, raceProfile and raceProfile[sex] or -0.20;
end
local function CharacterCreate_UpdateCamera(immediate)
local showFace = CharacterCreate.personalizationMode and not CharacterCreate.gameModeSelectionMode;
local mode = showFace and 1 or 0;
if CharacterCreate.cameraMode == mode and not immediate then
return;
end
CharacterCreate.cameraMode = mode;
CharacterCreate:SetCamera(0);
if MoonWellSetCharacterCreateCamera then
local zoom, verticalOffset = CharacterCreate_GetCameraProfile();
MoonWellSetCharacterCreateCamera(mode, zoom, verticalOffset, immediate and 0 or 500);
end
end
local function CharacterCreate_GetEnteredName()
local text = "";
@@ -701,6 +739,8 @@ local function CharacterCreate_UpdatePersonalizationStep()
CharacterCreate.gameModeSelectionMode = false;
end
CharacterCreate_UpdateCamera();
local showGameModes = CharacterCreate.gameModeSelectionMode and not PAID_SERVICE_TYPE;
if showGameModes then
@@ -943,6 +983,7 @@ function CharacterCreate_TogglePersonalization()
CharCreatePersonalizeButton:Show();
CharCreateOkayButton:Hide();
CharacterCreate_UpdateGameModeVisibility();
CharacterCreate_UpdateCamera();
for i=1, NUM_CHAR_CUSTOMIZATIONS do
_G["CharacterCustomizationButtonFrame"..i]:Hide();
@@ -1001,7 +1042,7 @@ function CharacterCreate_OnShow()
CharacterChangeFixup();
CharacterCreate_CreateGenderButtonTextures();
CharacterCreate_UpdateButtonCheckedStates();
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
hideScheduled = true
HideNameEditFrame:Show()
@@ -1013,7 +1054,7 @@ end
function CharacterCreate_OnHide()
PAID_SERVICE_CHARACTER_ID = nil;
PAID_SERVICE_TYPE = nil;
CharacterCreate_ResetState();
CharacterCreate_ResetState(true);
for button, tooltip in pairs(raceTooltips) do
if tooltip then tooltip:Hide() end
@@ -1518,6 +1559,7 @@ end
function CharacterCreate_UpdateModel(self)
UpdateCustomizationScene();
CharacterCreate_UpdateCamera();
self:AdvanceTime();
end
@@ -1644,9 +1686,10 @@ function SetCharacterGender(sex)
CharacterCreate_UpdateButtonCheckedStates();
end
function CharacterCreate_ResetState()
function CharacterCreate_ResetState(immediate)
CharacterCreate.personalizationMode = false;
CharacterCreate.gameModeSelectionMode = false;
CharacterCreate_UpdateCamera(immediate);
CharacterCreateRaceButtonsContainer:Show();
CharacterCreateClassButtonsContainer:Show();
CharacterCreateGenderButtonsContainer:Show();
@@ -49,10 +49,19 @@ local rolesByFlag = {
}
local EJ_Tabs = {};
local EJ_BROKEN_BAKED_MODEL = {};
local EJ_MODEL_CAMERA = {
-- Avatar of Hakkar is unusually small and anchored too low.
[8053] = { scale = 2.0, x = 0, y = 0.16, z = 0 },
-- Gahz'rilla's native UI camera is an extreme facial close-up.
[7271] = { scale = 0.42, x = 0, y = 0.04, z = 0 },
};
local EJ_MODEL_USER_SCALE = {};
EJ_Tabs[1] = { frame = "overviewScroll", button = "overviewTab" };
EJ_Tabs[2] = { frame = "lootScroll", button = "lootTab" };
EJ_Tabs[3] = { frame = "detailsScroll", button = "bossTab" };
EJ_Tabs[4] = { frame = "model", button = "modelTab" };
local EJ_section_openTable = {};
@@ -177,12 +186,75 @@ function EncounterJournal_InitTab(self)
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)
EncounterJournalTitleText:SetText(ADVENTURE_JOURNAL);
SetPortraitToTexture(EncounterJournalPortrait, "Interface\\EncounterJournal\\UI-EJ-PortraitIcon");
self:RegisterCustomEvent("EJ_LOOT_DATA_RECIEVED");
self:RegisterCustomEvent("EJ_DIFFICULTY_UPDATE");
self:RegisterCustomEvent("SEARCH_DB_LOADED");
self:EnableMouseWheel(true);
self:SetScript("OnMouseWheel", EncounterJournal_OnMouseWheel);
do
SetParentFrameLevel(self.inset)
@@ -1061,6 +1133,7 @@ function EncounterJournal_DisplayEncounter(encounterID, noButton, scrollToEncoun
-- Setup Creatures
local id, name, description, displayInfo, iconImage, creatureID;
local hasCreatureModel = false;
for i = 1, MAX_CREATURES_PER_ENCOUNTER do
id, name, description, displayInfo, iconImage, creatureID = EJ_GetCreatureInfo(i);
if id then
@@ -1071,12 +1144,17 @@ function EncounterJournal_DisplayEncounter(encounterID, noButton, scrollToEncoun
button.id = id;
button.description = description;
button.displayInfo = displayInfo;
button.creatureID = creatureID or displayInfo;
button.iconImage = iconImage;
button.creatureID = creatureID;
if creatureID and creatureID > 0 then
hasCreatureModel = true;
end
end
end
--enable abilities tab
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.bossTab, true);
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.modelTab, hasCreatureModel);
if (overviewFound) then
EncounterJournal_ToggleHeaders(self.overviewFrame);
@@ -1110,12 +1188,30 @@ function EncounterJournal_DisplayCreature(self)
local model = EncounterJournal.encounter.info.model;
local displayInfo = self.displayInfo;
if model.ResetFull then
model:ResetFull();
elseif model.ClearModel then
if model.ClearModel then
model:ClearModel();
end
EncounterJournal.creatureDisplayID = displayInfo;
model.displayInfo = displayInfo;
local useStaticPortrait = EJ_BROKEN_BAKED_MODEL[displayInfo];
if model.fallbackPortrait then
if useStaticPortrait then
model.fallbackPortrait:SetTexture(self.iconImage or
(MoonWellEncounterPortraitByDisplayInfo and MoonWellEncounterPortraitByDisplayInfo[displayInfo]) or
QUESTION_MARK_ICON);
model.fallbackPortrait:Show();
else
model.fallbackPortrait:Hide();
end
end
-- WarcraftXL accepts displayInfo as an optional second argument and can
-- therefore render entries which are not present in the creature cache.
-- Stock 3.3.5 safely ignores the extra argument and keeps its old fallback.
if not useStaticPortrait and self.creatureID and self.creatureID > 0 and model.SetCreature then
model:SetCreature(self.creatureID, displayInfo);
EncounterJournal_Model_ApplyCamera(model);
end
model.imageTitle:SetText(self.name)
@@ -1134,6 +1230,74 @@ function EncounterJournal_ShowCreatures()
end
end
function EncounterJournal_Model_OnLoad(self)
self.rotation = 0;
self:SetRotation(self.rotation);
self:EnableMouse(true);
self:EnableMouseWheel(true);
end
function EncounterJournal_Model_ApplyCamera(self)
if not self or not self.displayInfo or EJ_BROKEN_BAKED_MODEL[self.displayInfo] then
return;
end
local camera = EJ_MODEL_CAMERA[self.displayInfo];
local scale = EJ_MODEL_USER_SCALE[self.displayInfo] or (camera and camera.scale) or 1;
if self.SetModelScale then
self:SetModelScale(scale);
end
if self.SetPosition then
self:SetPosition(camera and camera.x or 0, camera and camera.y or 0, camera and camera.z or 0);
end
end
function EncounterJournal_Model_OnMouseWheel(self, delta)
if not self.displayInfo or EJ_BROKEN_BAKED_MODEL[self.displayInfo] then
return;
end
local camera = EJ_MODEL_CAMERA[self.displayInfo];
local scale = EJ_MODEL_USER_SCALE[self.displayInfo] or (camera and camera.scale) or 1;
if delta > 0 then
scale = scale * 1.12;
else
scale = scale / 1.12;
end
scale = math.max(0.15, math.min(4, scale));
EJ_MODEL_USER_SCALE[self.displayInfo] = scale;
EncounterJournal_Model_ApplyCamera(self);
end
function EncounterJournal_Model_OnUpdateModel(self)
EncounterJournal_Model_ApplyCamera(self);
end
function EncounterJournal_Model_OnMouseDown(self, button)
if button == "LeftButton" then
self.dragCursorX = GetCursorPosition();
elseif button == "RightButton" and self.displayInfo then
EJ_MODEL_USER_SCALE[self.displayInfo] = nil;
EncounterJournal_Model_ApplyCamera(self);
end
end
function EncounterJournal_Model_OnMouseUp(self)
self.dragCursorX = nil;
end
function EncounterJournal_Model_OnUpdate(self)
if not self.dragCursorX then
return;
end
local cursorX = GetCursorPosition();
local delta = cursorX - self.dragCursorX;
self.dragCursorX = cursorX;
self.rotation = (self.rotation or 0) + delta * 0.01;
self:SetRotation(self.rotation);
end
function EncounterJournal_HideCreatures(clearDisplayInfo)
for index, creatureButton in ipairs(EncounterJournal.encounter.info.creatureButtons) do
creatureButton:Hide();
@@ -1149,6 +1313,30 @@ local toggleTempList = {};
local headerCount = 0;
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)
local oldtex = self.textures.expanded;
if self:GetParent().expanded then
@@ -1178,6 +1366,22 @@ function EncounterJournal_UpdateButtonState(self)
self.tex.down[3]:Hide();
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)
if IsModifiedClick("CHATLINK") and ChatEdit_GetActiveWindow() then
if self.link then
@@ -1471,6 +1675,7 @@ function EncounterJournal_SetUpOverview(self, role, index)
infoHeader.button.link = link;
infoHeader.sectionID = nextSectionID;
EncounterJournal_NormalizeHeaderButton(infoHeader, EncounterJournal_GetHeaderWidth(self));
infoHeader.overviewDescription:SetWidth(infoHeader:GetWidth() - 20);
EncounterJournal_SetDescriptionWithBullets(infoHeader, description);
infoHeader:Show();
@@ -1479,7 +1684,7 @@ end
function EncounterJournal_ToggleHeaders(self, doNotShift)
local numAdded = 0;
local infoHeader, parentID, _;
local hWidth = self:GetWidth();
local hWidth = EncounterJournal_GetHeaderWidth(self);
local nextSectionID;
local topLevelSection = false;
@@ -1703,7 +1908,7 @@ function EncounterJournal_ToggleHeaders(self, doNotShift)
end
infoHeader.index = nil;
infoHeader:SetWidth(hWidth);
EncounterJournal_NormalizeHeaderButton(infoHeader, hWidth);
EncounterJournal_SetHeaderDescription(infoHeader, description);
-- If this section has not be seen and should start open
@@ -1717,6 +1922,7 @@ function EncounterJournal_ToggleHeaders(self, doNotShift)
numAdded = numAdded + EncounterJournal_ToggleHeaders(infoHeader, true);
end
EncounterJournal_NormalizeHeaderButton(infoHeader, hWidth);
infoHeader:Show();
end -- if not filteredByDifficulty
@@ -1961,6 +2167,7 @@ function EncounterJournal_ClearDetails()
local clearDisplayInfo = true;
EncounterJournal_HideCreatures(clearDisplayInfo);
EncounterJournal_SetTabEnabled(EncounterJournal.encounter.info.modelTab, false);
local bossIndex = 1
local bossButton = _G["EncounterJournalBossButton" .. bossIndex];
@@ -1988,12 +2195,15 @@ function EncounterJournal_SetTab(tabType)
local info = EncounterJournal.encounter.info;
info.tab = tabType;
if info.model then
if info.model and tabType ~= 4 then
info.model:Hide();
end
for key, data in pairs(EJ_Tabs) do
if key == tabType then
info[data.frame]:Show();
if key == 4 then
EncounterJournal_ShowCreatures();
end
info[data.button].selected:Show();
info[data.button].unselected:Hide();
info[data.button]:LockHighlight();
@@ -467,7 +467,7 @@
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentText" parentKey="Text" inherits="PKBT_Font_14" text="UNAVAILABLE">
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormal" text="UNAVAILABLE">
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
@@ -2510,7 +2510,7 @@
<OnClick function="EncounterTabTemplate_OnClick"/>
</Scripts>
</Button>
<Button name="$parentModelTab" inherits="EncounterTabTemplate" parentKey="modelTab" id="4" hidden="true">
<Button name="$parentModelTab" inherits="EncounterTabTemplate" parentKey="modelTab" id="4">
<Anchors>
<Anchor point="TOP" relativeTo="$parentBossTab" relativePoint="BOTTOM" x="0" y="2"/>
</Anchors>
@@ -2532,8 +2532,8 @@
<OnLoad>
self.tooltip = MODEL;
self:Disable();
self:Hide();
</OnLoad>
<OnClick function="EncounterTabTemplate_OnClick"/>
</Scripts>
</Button>
<ScrollFrame name="$parentBossesScrollFrame" parentKey="bossesScroll" inherits="UIPanelScrollFrameCodeTemplate">
@@ -2844,7 +2844,7 @@
</OnShow>
</Scripts>
</ScrollFrame>
<DressUpModel name="$parentModelFrame" parentKey="model" inherits="PKBT_ModelTemplate,ModelTemplate">
<DressUpModel name="$parentModelFrame" parentKey="model" hidden="true" enableMouse="true">
<Size x="390" y="423"/>
<Anchors>
<Anchor point="BOTTOMRIGHT" x="-3" y="1"/>
@@ -2859,6 +2859,14 @@
<TexCoords left="0.76953125" right="0" top="0" bottom="0.830078125"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentFallbackPortrait" parentKey="fallbackPortrait" hidden="true">
<Size x="256" y="256"/>
<Anchors>
<Anchor point="CENTER" x="0" y="8"/>
</Anchors>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture name="$parentShadow" file="Interface\EncounterJournal\UI-EJ-BossModelPaperFrame">
<Size x="393" y="424"/>
@@ -2892,20 +2900,20 @@
</Frames>
<Scripts>
<OnLoad>
Mixin(self, PKBT_ModelMixin)
SharedXML_Model_OnLoad(self);
EncounterJournal_Model_OnLoad(self);
self.imageTitle = self.overlayFrame.imageTitle;
</OnLoad>
<OnShow>
self:OnShow()
EncounterJournal_ShowCreatures();
EncounterJournal.encounter.info.TitleFrame.encounterTitle:Hide();
EncounterJournal.encounter.info.rightShadow:Hide();
EncounterJournal.encounter.info.difficulty:Hide();
</OnShow>
<OnEvent>
self:OnEvent(event, ...)
SharedXML_Model_OnEvent(event, ...)
</OnEvent>
<OnMouseDown function="EncounterJournal_Model_OnMouseDown"/>
<OnMouseUp function="EncounterJournal_Model_OnMouseUp"/>
<OnMouseWheel function="EncounterJournal_Model_OnMouseWheel"/>
<OnUpdateModel function="EncounterJournal_Model_OnUpdateModel"/>
<OnUpdate function="EncounterJournal_Model_OnUpdate"/>
</Scripts>
</DressUpModel>
<Button name="$parentCreatureButton1" inherits="EncounterCreatureButtonTemplate">
@@ -2919,8 +2927,7 @@
SetParentFrameLevel(self.overviewTab, 10)
SetParentFrameLevel(self.lootTab, 10)
SetParentFrameLevel(self.bossTab, 10)
self.modelTab:Hide()
self.model:Hide()
SetParentFrameLevel(self.modelTab, 10)
</OnLoad>
</Scripts>
</Frame>
@@ -1,544 +0,0 @@
UIPanelWindows["ItemBrowser"] = { area = "left", pushable = 0, whileDead = 1, width = 830, xOffset = "15", yOffset = "-10"}
local type = type
local mathmax = math.max
local strfind, strlower, strtrim = string.find, string.lower, string.trim
local tinsert, twipe = table.insert, table.wipe
local debugprofilestop = debugprofilestop
local COLUMNS = {
{
name = "Name",
dataProviderKey = "name",
template = "ItemBrowserListCellNameIconTemplate",
},
{
name = "ItemLevel",
dataProviderKey = "itemLevel",
width = 70,
template = "ItemBrowserListCellItemLevelTemplate",
},
{
name = "ID",
dataProviderKey = "itemID",
width = 90,
template = "ItemBrowserListCellItemIDTemplate",
},
}
ItemBrowserMixin = {}
function ItemBrowserMixin:OnLoad()
self.TitleText:SetText(ITEM_BROWSER)
SetPortraitToTexture(self.portrait, [[Interface\Icons\Achievement_General_ClassicBattles_64]])
self.inset.Bgs:SetAtlas("UI-EJ-BattleforAzeroth")
do -- tabs
self:RegisterCustomEvent("SERVICE_DATA_UPDATE")
self:RegisterCustomEvent("CUSTOM_CHALLENGE_DEACTIVATED")
self.tab1:SetFrameLevel(1)
self.tab2:SetFrameLevel(1)
self.tab3:SetFrameLevel(1)
self.tab4:SetFrameLevel(1)
self.maxTabWidth = (self:GetWidth() - 19) / 4
PanelTemplates_SetNumTabs(self, 4)
end
do -- coroutine
self.COROUTINE_HANDLER = CreateFrame("Frame")
self.COROUTINE_HANDLER:Hide()
self.COROUTINE_HANDLER.FRAMETIME_TARGET = 1 / 55
self.COROUTINE_HANDLER.FRAMETIME_AVAILABLE = 8
self.COROUTINE_HANDLER.FRAMETIME_RESERVE = 4
self.COROUTINE_HANDLER.DEBUG = false
self.COROUTINE_HANDLER:SetScript("OnUpdate", function(this, elapsed)
local frametimeStep = this.FRAMETIME_TARGET - elapsed
if frametimeStep ~= 0 then
frametimeStep = frametimeStep * 1000
this.FRAMETIME_AVAILABLE = mathmax(5, this.FRAMETIME_AVAILABLE + frametimeStep)
end
if this.COROUTINE then
this.COROUTINE_TIMESTAMP = debugprofilestop()
local status, result, progress = coroutine.resume(this.COROUTINE)
if not status then
this.COROUTINE = nil
this:Hide()
self.PROGRESS = 1
error(result, 2)
elseif not result then
if this.DEBUG then print("ITEM_BROWSER_COROUTINE", progress) end
self.PROGRESS = progress
self:OnSearchResultUpdate()
else
if this.DEBUG then print("ITEM_BROWSER_COROUTINE", 1) end
this.COROUTINE = nil
this:Hide()
self.PROGRESS = 1
self:OnSearchResultUpdate()
end
else
this:Hide()
end
end)
end
self.BUTTON_OFFSET_Y = 0
self.PROGRESS = 1
self.results = {}
self.Scroll.ScrollBar:SetBackgroundShown(false)
self.Scroll.update = function(scrollFrame)
self:UpdateResultList()
end
self.Scroll.ScrollBar.doNotHide = true
self.Scroll.scrollBar = self.Scroll.ScrollBar
HybridScrollFrame_CreateButtons(self.Scroll, "ItemBrowserListRowButtonTemplate", 0, 0, nil, nil, nil, -self.BUTTON_OFFSET_Y)
self.SearchBox:ToggleOnCharFilter(true, 0.3)
self.tableBuilder = CreateTableBuilder(HybridScrollFrame_GetButtons(self.Scroll))
self.tableBuilder:SetHeaderContainer(self.HeaderHolder)
self:ConstructTable()
end
function ItemBrowserMixin:OnShow()
SetParentFrameLevel(self.inset)
PanelTemplates_SetTab(self, 4)
self.dirty = true
self.Scroll.ScrollBar:SetValue(0)
self:UpdateResultList()
self.SearchBox:SetFocus()
end
function ItemBrowserMixin:OnEvent(event, ...)
if event == "SERVICE_DATA_UPDATE"
or (event == "CUSTOM_CHALLENGE_DEACTIVATED" and select(2, ...) ~= Enum.HardcoreDeathReason.FAILED_DEATH)
then
if not C_Service.IsRenegadeRealm() then
PanelTemplates_HideTab(self, 2)
self.tab3:SetPoint("LEFT", self.tab1, "RIGHT", -16, 0);
end
if not C_Service.IsHardcoreEnabledOnRealm() then
PanelTemplates_HideTab(self, 3)
end
if not C_Service.IsGMAccount() and not IsInterfaceDevClient() then
PanelTemplates_HideTab(self, 4)
end
end
end
function ItemBrowserMixin:GetRowProvider()
return function(itemIndex)
if itemIndex > #self.results then
return nil
end
local itemID = self.results[itemIndex]
local itemName, itemLink, itemRarity, itemLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(itemID, false, nil, true, true)
return {
itemID = itemID,
name = itemName,
itemLink = itemLink,
itemLevel = itemLevel,
itemType = itemType,
itemSubType = itemSubType,
icon = itemTexture,
}
end
end
function ItemBrowserMixin:ConstructTable()
local textPadding = 1
local cellPadding = 10
self.tableBuilder:Reset()
self.tableBuilder:SetDataProvider(self:GetRowProvider())
self.tableBuilder:SetTableMargins(5)
self.tableBuilder:SetTableWidth(self.Scroll:GetWidth() or select(3, self.Scroll:GetRect()))
for index, columnInfo in ipairs(COLUMNS) do
local column = self.tableBuilder:AddColumn()
column:ConstructHeader("Button", "ItemBrowserListHeaderButtonTemplate", columnInfo.name)
column:ConstrainToHeader(textPadding)
column:ConstructCells("Frame", columnInfo.template)
if columnInfo.width then
column:SetFixedConstraints(columnInfo.width, cellPadding)
else
column:SetFillConstraints(1, cellPadding)
end
end
self.tableBuilder:Arrange()
end
function ItemBrowserMixin:GetNumItems()
if not self.NUM_ITEMS then
self.NUM_ITEMS = tCount(ItemsCache)
end
return self.NUM_ITEMS
end
function ItemBrowserMixin:SearchItem(item)
item = strtrim(item)
if item == "" then
self:ClearResults()
return
end
local itemID = C_Item.GetItemIDFromString(item)
local searchField, searchValue, useStringFind
if itemID then
searchField = Enum.ItemCacheField.ITEM_ID
searchValue = itemID
elseif item ~= "" then
searchField = GetLocale() == "ruRU" and Enum.ItemCacheField.NAME_RURU or Enum.ItemCacheField.NAME_ENGB
searchValue = strlower(item)
useStringFind = true
end
if not searchField then
self:ClearResults()
return
end
self:StartSearch(searchField, searchValue, useStringFind)
end
function ItemBrowserMixin:StartSearch(searchField, searchValue, useStringFind)
if self.SEARCH_FIELD == searchField and self.SEARCH_VALUE == searchValue then
return
end
if self.COROUTINE then
coroutine.close(self.COROUTINE)
self.COROUTINE = nil
self:ClearResults()
end
self.SEARCH_FIELD = searchField
self.SEARCH_VALUE = searchValue
self.USE_STRING_FIND = useStringFind
local framerate = GetFramerate()
self.COROUTINE_HANDLER.FRAMETIME_TARGET = framerate > 63 and (1 / 60) or (1 / 55)
self.COROUTINE_HANDLER.FRAMETIME_BUDGET = 1000 / framerate - self.COROUTINE_HANDLER.FRAMETIME_RESERVE
self.COROUTINE_HANDLER.COROUTINE = coroutine.create(self.SearchProcess)
self.COROUTINE_HANDLER.COROUTINE_TIMESTAMP = debugprofilestop()
local status, result, progress = coroutine.resume(self.COROUTINE_HANDLER.COROUTINE, self)
if not status then
self.COROUTINE_HANDLER.COROUTINE = nil
self.COROUTINE_HANDLER:Hide()
self.PROGRESS = 1
self:ClearResults()
geterrorhandler()(result)
elseif coroutine.status(self.COROUTINE_HANDLER.COROUTINE) == "dead" then
self.COROUTINE_HANDLER.COROUTINE = nil
self.PROGRESS = 1
else
if self.COROUTINE_HANDLER.DEBUG then print("ITEM_BROWSER_COROUTINE", progress) end
self.PROGRESS = progress
self.COROUTINE_HANDLER:Show()
end
self:OnSearchResultUpdate()
end
function ItemBrowserMixin:SearchProcess()
twipe(self.results)
if not ItemsCache then
return true, 1
end
local processedEntries = 0
local fieldValue
for itemID, itemInfo in pairs(ItemsCache) do
processedEntries = processedEntries + 1
fieldValue = itemInfo[self.SEARCH_FIELD]
if type(fieldValue) == "string" then
fieldValue = strlower(fieldValue)
end
if fieldValue == self.SEARCH_VALUE
or (self.USE_STRING_FIND and strfind(fieldValue, self.SEARCH_VALUE, 1, true))
then
tinsert(self.results, itemInfo[Enum.ItemCacheField.ITEM_ID])
end
if (debugprofilestop() - self.COROUTINE_HANDLER.COROUTINE_TIMESTAMP) > self.COROUTINE_HANDLER.FRAMETIME_BUDGET then
coroutine.yield(false, processedEntries / self:GetNumItems())
end
end
return true, 1
end
function ItemBrowserMixin:OnSearchResultUpdate()
self.dirty = true
self:UpdateSearchProgress()
self:UpdateResultList()
end
function ItemBrowserMixin:UpdateSearchProgress()
local progress = Clamp(self.PROGRESS or 1, 0, 1)
self.SearchProgressBar:SetValue(progress)
self.SearchProgressBar:SetAlpha(progress < 1 and 1 or 0)
end
function ItemBrowserMixin:ClearResults()
twipe(self.results)
self.SEARCH_FIELD = nil
self.SEARCH_VALUE = nil
self.USE_STRING_FIND = nil
self.PROGRESS = 1
self.dirty = true
self:UpdateSearchProgress()
self:UpdateResultList()
end
function ItemBrowserMixin:UpdateResultList()
local scrollFrame = self.Scroll
local offset = HybridScrollFrame_GetOffset(scrollFrame)
local numResults = self.results and #self.results or 0
if self.numItems ~= numResults then
self.numItems = numResults
self.dirty = true
end
local populateCount = math.min(#scrollFrame.buttons, numResults)
self.tableBuilder:Populate(offset, populateCount)
local mouseFocus = GetMouseFocus()
for index, button in ipairs(scrollFrame.buttons) do
button:SetOwner(self)
button:SetShown(index <= numResults)
if button == mouseFocus then
button:OnEnter()
end
end
if self.dirty then
local buttonHeight = scrollFrame.buttons[1] and scrollFrame.buttons[1]:GetHeight() or 0
local scrollHeight = buttonHeight * numResults + self.BUTTON_OFFSET_Y * (numResults - 1) + self.BUTTON_OFFSET_Y / 3
HybridScrollFrame_Update(scrollFrame, scrollHeight, scrollFrame:GetHeight())
self.dirty = nil
end
end
ItemBrowserSearchBoxMixin = CreateFromMixins(PKBT_EditBoxMixin)
function ItemBrowserSearchBoxMixin:OnLoad()
PKBT_EditBoxMixin.OnLoad(self)
local l, r, t, b = self:GetTextInsets()
self:SetTextInsets(l + 10, r + 10, t - 1, b)
self:SetInstructions(SEARCH)
self.Instructions:ClearAllPoints()
self.Instructions:SetPoint("LEFT", 19, -2)
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6)
self:SetCustomCharFilter(function(text)
text = string.gsub(text, "%s%s+", " ")
text = utf8.gsub(text, "[^A-zА-я0-9':%- ]+", "")
return text
end)
self.value = ""
end
function ItemBrowserSearchBoxMixin:OnHide()
self:StopOnCharSearchDelay()
end
function ItemBrowserSearchBoxMixin:OnEnterPressed()
self:ClearFocus()
self:DoSearch()
end
function ItemBrowserSearchBoxMixin:OnEditFocusLost()
SearchBoxTemplate_OnEditFocusLost(self)
self:HighlightText(0, 0)
self:DoSearch()
end
function ItemBrowserSearchBoxMixin:OnUpdate(elapsed)
self.searchDelay = self.searchDelay - elapsed
if self.searchDelay <= 0 then
self:SetScript("OnUpdate", nil)
self.searchDelay = nil
self:DoSearch()
end
end
function ItemBrowserSearchBoxMixin:DoSearch()
local newValue = self:GetText()
if self.value ~= newValue and not (self.value == nil and newValue == "") then
self.value = newValue
self:GetParent():SearchItem(newValue)
end
end
function ItemBrowserSearchBoxMixin:ToggleOnCharFilter(state, searchUpdateDelay)
if state then
self.searchUpdateDelay = searchUpdateDelay
self.searchDelay = searchUpdateDelay
else
self:SetScript("OnUpdate", nil)
self.searchUpdateDelay = nil
self.searchDelay = nil
end
end
function ItemBrowserSearchBoxMixin:OnTextChanged(userInput)
PKBT_EditBoxMixin.OnTextChanged(self, userInput)
SearchBoxTemplate_OnTextChanged(self)
end
function ItemBrowserSearchBoxMixin:OnTextValueChanged(value, userInput)
if userInput then
if self.searchUpdateDelay then
self.searchDelay = self.searchUpdateDelay
self:SetScript("OnUpdate", self.OnUpdate)
end
else
self:StopOnCharSearchDelay()
end
end
function ItemBrowserSearchBoxMixin:OnUpdate(elapsed)
self.searchDelay = self.searchDelay - elapsed
if self.searchDelay <= 0 then
self:SetScript("OnUpdate", nil)
self.searchDelay = nil
self:DoSearch()
end
end
function ItemBrowserSearchBoxMixin:StopOnCharSearchDelay()
self:SetScript("OnUpdate", nil)
if self.searchUpdateDelay then
self.searchDelay = self.searchUpdateDelay
end
end
function ItemBrowserSearchBoxMixin:OnClearClick(button)
SearchBoxTemplateClearButton_OnClick(self.clearButton, button)
self:StopOnCharSearchDelay()
self:DoSearch()
end
ItemBrowserListHeaderButtonMixin = CreateFromMixins(TableBuilderElementMixin)
function ItemBrowserListHeaderButtonMixin:Init(name)
self:SetText(name)
end
function ItemBrowserListHeaderButtonMixin:OnClick(button)
end
ItemBrowserListRowButton = CreateFromMixins(PKBT_OwnerMixin, TableBuilderRowMixin)
function ItemBrowserListRowButton:OnLoad()
self.UpdateTooltip = self.OnEnter
end
function ItemBrowserListRowButton:OnEnter()
if self.itemLink then
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:SetHyperlink(self.itemLink)
GameTooltip:Show()
end
end
function ItemBrowserListRowButton:OnLeave()
GameTooltip:Hide()
end
function ItemBrowserListRowButton:OnClick(button)
if self.itemLink then
HandleModifiedItemClick(self.itemLink)
end
end
function ItemBrowserListRowButton:Populate(rowData, dataIndex)
self:SetID(dataIndex)
self.itemID = rowData.itemID
self.itemLink = rowData.itemLink
end
ItemBrowserListCellMixin = {}
function ItemBrowserListCellMixin:OnLoad()
end
function ItemBrowserListCellMixin:Init(dataProviderKey)
self.dataProviderKey = dataProviderKey
end
function ItemBrowserListCellMixin:Populate(rowData, dataIndex)
local value = rowData[self.dataProviderKey]
self.Text:SetJustifyH(self.textAlignment or "CENTER")
self.Text:SetText(value or "?")
end
ItemBrowserListCellNameIconMixin = CreateFromMixins(ItemBrowserListCellMixin)
function ItemBrowserListCellNameIconMixin:Populate(rowData, dataIndex)
local textWidth = self:GetWidth() - 10
self.Text:SetText(rowData.name or UNKNOWN)
self.Text:SetWidth(textWidth)
self.SubText:SetWidth(textWidth)
if rowData.itemType and rowData.itemSubType then
self.SubText:SetFormattedText("%s - %s", rowData.itemType, rowData.itemSubType)
elseif rowData.itemType or rowData.itemSubType then
self.SubText:SetText(rowData.itemType or rowData.itemSubType)
else
self.SubText:SetText("")
end
self.Icon:SetTexture(rowData.icon or QUESTION_MARK_ICON)
local r, g, b = GetItemQualityColor(rowData.rarity or 1)
self.Border:SetVertexColor(r, g, b)
end
ItemBrowserListCellItemLevelMixin = CreateFromMixins(ItemBrowserListCellMixin)
function ItemBrowserListCellItemLevelMixin:Populate(rowData, dataIndex)
self.Text:SetText(rowData.itemLevel and rowData.itemLevel ~= 0 and rowData.itemLevel or "")
end
ItemBrowserListCellItemIDMixin = CreateFromMixins(ItemBrowserListCellMixin)
function ItemBrowserListCellItemIDMixin:Populate(rowData, dataIndex)
self.Text:SetText(rowData.itemID)
end
@@ -1,331 +0,0 @@
<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="Custom_ItemBrowser.lua"/>
<Button name="ItemBrowserListHeaderButtonTemplate" virtual="true">
<Size x="50" y="30"/>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" parentKey="Background" setAllPoints="true">
<Color r="0.2" g="0.2" b="0.2"/>
</Texture>
</Layer>
<!--
<Layer level="HIGHLIGHT">
<Texture name="$parentHighlight" parentKey="Highlight" setAllPoints="true" alpha="0.2" alphaMode="ADD">
<Color r="1" g="1" b="1"/>
</Texture>
</Layer>
-->
</Layers>
<NormalFont style="SystemFont_Shadow_Small"/>
<PushedTextOffset x="0" y="0"/>
<!--
<PushedTextOffset x="1" y="-1"/>
<ButtonText name="$parentButtonText" parentKey="ButtonText">
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
</ButtonText>
-->
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListHeaderButtonMixin)
</OnLoad>
<!--
<OnClick>
self:OnClick(button)
</OnClick>
-->
</Scripts>
</Button>
<Button name="ItemBrowserListRowButtonTemplate" virtual="true">
<Size x="0" y="46"/>
<Anchors>
<Anchor point="LEFT" x="1" y="0"/>
<Anchor point="RIGHT" x="-1" y="0"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBackground" parentKey="Background" setAllPoints="true" inherits="_SearchBarLg"/>
</Layer>
</Layers>
<HighlightTexture file="Interface\Common\Search">
<TexCoords left="0.001953" right="0.501953" top="0.234375" bottom="0.601562"/>
</HighlightTexture>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListRowButton)
self:OnLoad()
</OnLoad>
<OnEvent>
self:OnEvent(event, ...)
</OnEvent>
<OnEnter>
self:OnEnter()
</OnEnter>
<OnLeave>
self:OnLeave()
</OnLeave>
<OnClick>
self:OnClick()
</OnClick>
</Scripts>
</Button>
<Frame name="ItemBrowserListCellNameIconTemplate" virtual="true">
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentBorder" parentKey="Border" file="Interface\Common\Search">
<Size x="40" y="40"/>
<Anchors>
<Anchor point="LEFT" x="10" y="0"/>
</Anchors>
<TexCoords left="0.755859" right="0.830078" top="0.234375" bottom="0.531250"/>
</Texture>
</Layer>
<Layer level="ARTWORK">
<Texture name="$parentIcon" parentKey="Icon">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentBorder" x="1" y="-2"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorder" x="-1" y="1"/>
</Anchors>
</Texture>
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" justifyH="LEFT" maxLines="1">
<Size x="0" y="0"/>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentIcon" relativePoint="RIGHT" x="10" y="8"/>
</Anchors>
<Color r="0.96875" g="0.8984375" b="0.578125" a="1"/>
</FontString>
<FontString name="$parentSubText" parentKey="SubText" inherits="GameFontNormal" justifyH="LEFT">
<Size x="400" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentText" relativePoint="BOTTOMLEFT" x="0" y="-5"/>
</Anchors>
<Color r="0.66796875" g="0.51171875" b="0.3359375" a="1"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListCellNameIconMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
<Frame name="ItemBrowserListCellItemLevelTemplate" virtual="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" maxLines="1">
<Size x="0" y="0"/>
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
<Color r="1" g="1" b="1"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListCellItemLevelMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
<Frame name="ItemBrowserListCellItemIDTemplate" virtual="true">
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentText" parentKey="Text" inherits="GameFontNormalLarge" maxLines="1">
<Size x="0" y="0"/>
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
<Color r="1" g="1" b="1"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserListCellItemIDMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
<Frame name="ItemBrowser" inherits="PortraitFrameTemplate" toplevel="true" enableMouse="true" parent="UIParent" hidden="true">
<Size x="800" y="496"/>
<Anchors>
<Anchor point="CENTER" x="0" y="0"/>
</Anchors>
<Frames>
<Frame name="$parentInset" inherits="InsetFrameTemplate" parentKey="inset">
<Anchors>
<Anchor point="TOPRIGHT" x="-4" y="-60"/>
<Anchor point="BOTTOMLEFT" x="4" y="5"/>
</Anchors>
</Frame>
<EditBox name="$parentSearchBox" parentKey="SearchBox" inherits="PKBT_EditBoxTemplate" letters="64">
<Size x="130" y="36"/>
<Anchors>
<Anchor point="TOPLEFT" x="60" y="-22"/>
<Anchor point="TOPRIGHT" x="-10" y="-22"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentSearchIcon" parentKey="searchIcon" file="Interface\Common\UI-Searchbox-Icon">
<Size x="14" y="14"/>
<Anchors>
<Anchor point="LEFT" x="5" y="-2"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentClearButton" parentKey="clearButton" hidden="true">
<Size x="22" y="22"/>
<Anchors>
<Anchor point="RIGHT" x="-5" y="-1"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture parentKey="texture" file="Interface\FriendsFrame\ClearBroadcastIcon" alpha="0.5" setAllPoints="true"/>
</Layer>
</Layers>
<Scripts>
<OnEnter>
self.texture:SetAlpha(1.0)
</OnEnter>
<OnLeave>
self.texture:SetAlpha(0.5)
</OnLeave>
<OnMouseDown>
if self:IsEnabled() == 1 then
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 1, -1)
end
</OnMouseDown>
<OnMouseUp>
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 0, 0)
</OnMouseUp>
<OnClick>
self:GetParent():OnClearClick(button)
</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserSearchBoxMixin)
self:OnLoad()
</OnLoad>
<OnHide>
self:OnHide()
</OnHide>
<OnTextChanged>
self:OnTextChanged(userInput)
</OnTextChanged>
<OnEnterPressed>
self:OnEnterPressed()
</OnEnterPressed>
<OnEditFocusLost>
self:OnEditFocusLost()
</OnEditFocusLost>
<OnEditFocusGained function="SearchBoxTemplate_OnEditFocusGained"/>
</Scripts>
</EditBox>
<StatusBar name="$parentSearchProgressBar" parentKey="SearchProgressBar" minValue="0" maxValue="1" alpha="0">
<Size x="0" y="6"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentSearchBox" relativePoint="BOTTOMLEFT" x="7" y="1"/>
<Anchor point="TOPRIGHT" relativeTo="$parentSearchBox" relativePoint="BOTTOMRIGHT" x="-7" y="1"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Color r="0" g="0" b="0" a="0.3"/>
</Texture>
</Layer>
</Layers>
<BarTexture file="Interface\TargetingFrame\UI-StatusBar"/>
<BarColor r="0" g="1" b="0"/>
</StatusBar>
<ScrollFrame name="$parentScroll" parentKey="Scroll" inherits="HybridScrollFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentInset" relativePoint="TOPLEFT" x="10" y="-50"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentInset" relativePoint="BOTTOMRIGHT" x="-36" y="10"/>
</Anchors>
<Frames>
<Slider name="$parentScrollBar" parentKey="ScrollBar" inherits="PKBT_HybridScrollBarTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="TOPRIGHT" x="5" y="-25"/>
<Anchor point="BOTTOMLEFT" relativePoint="BOTTOMRIGHT" x="5" y="27"/>
</Anchors>
</Slider>
</Frames>
</ScrollFrame>
<Frame name="$parentHeaderHolder" parentKey="HeaderHolder">
<Size x="0" y="30"/>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentScroll" relativePoint="TOPLEFT" x="0" y="12"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentScroll" relativePoint="TOPRIGHT" x="0" y="12"/>
</Anchors>
</Frame>
<Button name="$parentTab1" inherits="CharacterFrameTabButtonTemplate" text="ADVENTURE_JOURNAL" id="1" parentKey="tab1">
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parent" relativePoint="BOTTOMLEFT" x="19" y="-30"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
<Button name="$parentTab2" inherits="CharacterFrameTabButtonTemplate" text="HEADHUNTING" id="2" parentKey="tab2">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTab1" relativePoint="RIGHT" x="-16" y="0"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
<Button name="$parentTab3" inherits="CharacterFrameTabButtonTemplate" text="HARDCORE_CHALLENGES_TITLE" id="3" parentKey="tab3">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTab2" relativePoint="RIGHT" x="-16" y="0"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
<Button name="$parentTab4" inherits="CharacterFrameTabButtonTemplate" text="ITEM_BROWSER" id="4" parentKey="tab4">
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTab3" relativePoint="RIGHT" x="-16" y="0"/>
</Anchors>
<Scripts>
<OnClick function="Adventure_TabOnClick"/>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, ItemBrowserMixin)
self:OnLoad()
</OnLoad>
<OnShow>
self:OnShow()
</OnShow>
<OnEvent>
self:OnEvent(event, ...)
</OnEvent>
</Scripts>
</Frame>
</Ui>
@@ -1,483 +0,0 @@
do
local tonumber = tonumber
local unpack = unpack
local bitband = bit.band
local strsub = string.sub
local LJ_BUFFER = {}
local NO_CLASS_FILTER = 0
local NO_SPEC_FILTER = 0
local function getPlayerFactionID()
local factionGroup = UnitFactionGroup("player") or "NEUTRAL"
if factionGroup == "Renegade" then
return (C_FactionManager.GetFactionInfoOriginal())
end
return PLAYER_FACTION_GROUP[factionGroup]
end
local function filterByFaction(factionID, playerFactionID)
if factionID == PLAYER_FACTION_GROUP.Neutral then
return true
elseif factionID == playerFactionID then
return true
end
return false
end
local function filterByClassAndSpecFilters(classID, specID, classFilter, specFilter)
if classFilter and specFilter then
if specFilter == 3 then
specFilter = 4
end
if classFilter == NO_CLASS_FILTER and specFilter == NO_SPEC_FILTER then
return true
elseif classFilter == classID and specFilter == NO_SPEC_FILTER then
return true
elseif classFilter == classID and bitband(specFilter, specID) == specFilter then
return true
end
end
return false
end
local function sortItemSets(a, b)
if a[4] == b[4] then
if a[6] ~= b[6] then
return a[6]
end
return (tonumber(strsub(a[3], 3, -1) or 0)) > (tonumber(strsub(b[3], 3, -1) or 0))
end
return a[4] > b[4]
end
C_LootJournal = {}
function C_LootJournal.GenerateLootData(classFilter, specFilter)
table.wipe(LJ_BUFFER)
local serverID = C_Service and C_Service.GetRealmID and C_Service.GetRealmID() or 0
local playerFactionID = getPlayerFactionID()
local itemSetSourceRealm = EJ_ITEMSET_SOURCE_REALM and EJ_ITEMSET_SOURCE_REALM[serverID]
for setIndex, itemSetData in pairs(EJ_ITEMSET_DATA) do
local name, itemlevel, tierName, sourceID, classID, specID, isPVP, itemList, factionID, realmFlag = unpack(itemSetData, 1, 10)
if not factionID then
factionID = 0
end
if filterByClassAndSpecFilters(classID, specID, classFilter or 0, specFilter or 0)
and filterByFaction(factionID, playerFactionID)
then
if itemSetSourceRealm and itemSetSourceRealm[setIndex] then
sourceID = itemSetSourceRealm[setIndex]
end
local sourceText = EJ_ITEMSET_SOURCE[sourceID] or ""
local numItems = type(itemList) == "table" and #itemList or 0
local items = {}
for i = 1, numItems do
local itemID = itemList[i]
items[i] = {
itemID = itemID,
icon = GetItemIcon(itemID) or [[Interface\Icons\INV_Misc_QuestionMark]],
}
end
LJ_BUFFER[#LJ_BUFFER + 1] = {setIndex, name, tierName or "", itemlevel, sourceText, isPVP == 1, items, numItems}
end
end
table.sort(LJ_BUFFER, sortItemSets)
return LJ_BUFFER
end
function C_LootJournal.GetNumItemSets()
return #LJ_BUFFER
end
function C_LootJournal.GetItemSetInfo(index)
if index and LJ_BUFFER[index] then
return unpack(LJ_BUFFER[index])
end
end
end
local NO_SPEC_FILTER = 0;
local NO_CLASS_FILTER = 0;
local NO_INV_TYPE_FILTER = 0;
--===================================================================================================================================
LootJournalItemsMixin = { };
function LootJournalItemsMixin:OnLoad()
self.Background:SetAtlas("loottab-background", true)
self:SetView(LOOT_JOURNAL_ITEM_SETS);
local _, _, classID = UnitClass("player");
local specIndex = C_Talent.GetCurrentSpecTabIndex()
self:SetClassAndSpecFilters(classID, specIndex);
end
function LootJournalItemsMixin:SetClassAndSpecFilters(classID, specID)
self.classFilter = classID;
self.specFilter = specID;
end
function LootJournalItemsMixin:GetClassAndSpecFilters()
return self.classFilter or NO_CLASS_FILTER, self.specFilter or NO_SPEC_FILTER;
end
function LootJournalItemsMixin:SetView(view)
if ( self.view == view ) then
return;
end
self.view = view;
self.ItemSetsFrame:Show();
end
function LootJournalItemsMixin:GetActiveList()
return self.ItemSetsFrame;
end
function LootJournalItemsMixin:Refresh()
self:GetActiveList():Refresh();
end
function LootJournalItemButtonTemplate_OnEnter(self)
local listFrame = self:GetParent();
while ( listFrame and not listFrame.ShowItemTooltip ) do
listFrame = listFrame:GetParent();
end
if ( listFrame ) then
listFrame:ShowItemTooltip(self);
self:SetScript("OnUpdate", LootJournalItemButton_OnUpdate);
self.UpdateTooltip = LootJournalItemButtonTemplate_OnEnter;
end
end
function LootJournalItemButtonTemplate_OnLeave(self)
self.UpdateTooltip = nil;
GameTooltip:Hide();
self:SetScript("OnUpdate", nil);
ResetCursor();
end
function LootJournalItemButton_OnClick(self, button)
if HandleModifiedItemClick(self.itemLink) then
return
end
LootJournal_OpenItemByEntry(self.itemID)
end
--===================================================================================================================================
LootJournalItemSetsMixin = {}
local LJ_ITEMSET_X_OFFSET = 10;
local LJ_ITEMSET_Y_OFFSET = 10;
local LJ_ITEMSET_BUTTON_SPACING = 13;
local LJ_ITEMSET_BOTTOM_BUFFER = 4;
function LootJournalItemSetsMixin:Refresh()
self.dirty = true;
local offset = self.scrollBar:GetValue();
if ( offset == 0 ) then
self:UpdateList();
else
self.scrollBar:SetValue(0);
end
if ( self.ClassButton ) then
self:UpdateClassButtonText();
end
end
function LootJournalItemSetsMixin:GetClassAndSpecFilters()
return self:GetParent():GetClassAndSpecFilters();
end
function LootJournalItemSetsMixin:UpdateClassButtonText()
end
function LootJournalItemSetsMixin:ShowItemTooltip(button)
-- itemLink may not be available
if not button.itemLink then
GameTooltip:Hide()
return
end
GameTooltip:SetOwner(button, "ANCHOR_RIGHT");
GameTooltip:SetHyperlink(button.itemLink);
if LootJournal_CanOpenItemByEntry(button.itemID) then
GameTooltip:AddLine(LOOTJOURNAL_ITEM_CLICK_TO_OPEN_LOOT, 0, 0.8, 1)
end
self.tooltipItemID = button.itemID;
GameTooltip:Show();
end
function LootJournalItemSetsMixin:CheckItemButtonTooltip(button)
if ( GameTooltip:GetOwner() == button and self.tooltipItemID ~= button.itemID ) then
self:ShowItemTooltip(button);
end
end
function LootJournalItemSetsMixin:GetClassFilter()
local classFilter, specFilter = self:GetClassAndSpecFilters();
return classFilter;
end
function LootJournalItemSetsMixin:GetSpecFilter()
local classFilter, specFilter = self:GetClassAndSpecFilters();
return specFilter;
end
function LootJournalItemSetsMixin:SetClassAndSpecFilters(newClassFilter, newSpecFilter)
local classFilter, specFilter = self:GetClassAndSpecFilters();
if not self.classAndSpecFiltersSet or classFilter ~= newClassFilter or specFilter ~= newSpecFilter then
--[[
-- if choosing just a class without a spec, pick a spec
if newClassFilter ~= NO_CLASS_FILTER and newSpecFilter == NO_SPEC_FILTER then
local _, _, classID = UnitClass("player");
-- if player's class, choose active spec
-- otherwise use 1st spec
if classID == newClassFilter then
local specIndex = C_Talent.GetCurrentSpecTabIndex()
newSpecFilter = specIndex
else
newSpecFilter = 1
end
end
--]]
self:GetParent():SetClassAndSpecFilters(newClassFilter, newSpecFilter);
self.scrollBar:SetValue(0);
self:Refresh();
end
CloseDropDownMenus(1);
self.classAndSpecFiltersSet = true;
end
do
function LootJournalItemButton_OnUpdate(self)
if GameTooltip:IsOwned(self) then
if IsModifiedClick("DRESSUP") then
ShowInspectCursor();
else
ResetCursor();
end
end
end
local function OpenClassFilterDropDown(self, level)
if level then
self:GetParent():GetActiveList():OpenClassFilterDropDown(level);
end
end
function LootJournalItemsClassDropDown_OnLoad(self)
UIDropDownMenu_Initialize(self, OpenClassFilterDropDown, "MENU");
end
local CLASS_DROPDOWN = 1;
function LootJournalItemSetsMixin:OpenClassFilterDropDown(level)
local filterClassID = self:GetClassFilter();
local filterSpecID = self:GetSpecFilter();
local classDisplayName, classTag, classID;
local function SetClassAndSpecFilters(_, classFilter, specFilter)
self:SetClassAndSpecFilters(classFilter, specFilter);
end
local info = UIDropDownMenu_CreateInfo();
if UIDROPDOWNMENU_MENU_VALUE == CLASS_DROPDOWN then
info.text = ALL_CLASSES;
info.checked = filterClassID == NO_CLASS_FILTER;
info.arg1 = NO_CLASS_FILTER;
info.arg2 = NO_SPEC_FILTER;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
local numClasses = GetNumClasses();
for i = 1, numClasses do
classDisplayName, classTag, classID = GetClassInfo(i);
if classID ~= 10 then
info.text = classDisplayName;
info.checked = filterClassID == classID;
info.arg1 = classID;
info.arg2 = NO_SPEC_FILTER;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
end
end
end
if level == 1 then
info.text = CLASS;
info.func = nil;
info.notCheckable = true;
info.hasArrow = true;
info.value = CLASS_DROPDOWN;
UIDropDownMenu_AddButton(info, level);
local classDisplayName, classTag, classID;
if filterClassID ~= NO_CLASS_FILTER then
classID = filterClassID;
classDisplayName = GetClassInfo(classID)
else
classDisplayName, classTag, classID = UnitClass("player");
end
info.text = classDisplayName;
info.notCheckable = true;
info.arg1 = nil;
info.arg2 = nil;
info.func = nil;
info.hasArrow = false;
UIDropDownMenu_AddButton(info, level);
info.notCheckable = nil;
for i = 1, GetNumSpecializationsForClassID(classID) do
local _, specName = GetSpecializationInfoForClassID(classID, i)
info.leftPadding = 10;
info.text = specName;
info.checked = filterSpecID == i;
info.arg1 = classID;
info.arg2 = i;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
end
info.text = ALL_SPECS;
info.leftPadding = 10;
info.checked = (classID == filterClassID and filterSpecID == NO_SPEC_FILTER) or (filterClassID == NO_CLASS_FILTER and filterSpecID == NO_SPEC_FILTER);
info.arg1 = classID;
info.arg2 = NO_SPEC_FILTER;
info.func = SetClassAndSpecFilters;
UIDropDownMenu_AddButton(info, level);
end
end
end
function LootJournalItemSetsMixin:OnLoad()
self.scrollBar.trackBG:Hide();
self.update = LootJournalItemSetsMixin.UpdateList;
HybridScrollFrame_CreateButtons(self, "LootJournalItemSetButtonTemplate", LJ_ITEMSET_X_OFFSET, -LJ_ITEMSET_Y_OFFSET, "TOPLEFT", nil, nil, -LJ_ITEMSET_BUTTON_SPACING);
end
function LootJournalItemSetsMixin:OnShow()
if not self.init then
self.init = true;
self:Refresh();
end
end
local function onItemLoaded(itemID)
local self = EncounterJournal.LootJournalItems.ItemSetsFrame
for i = 1, #self.buttons do
local itemButtons = self.buttons[i].ItemButtons;
for j = 1, #itemButtons do
if ( itemButtons[j].itemID == itemID ) then
self:ConfigureItemButton(itemButtons[j]);
return;
end
end
end
end
function LootJournalItemSetsMixin:ConfigureItemButton(button)
local _, itemLink, itemQuality, _, _, _, _, _, _, icon = C_Item.GetItemInfo(button.itemID, false, onItemLoaded, true);
button.itemLink = itemLink;
itemQuality = itemQuality or Enum.ItemQuality.Epic; -- sets are most likely rare
if ( itemQuality == Enum.ItemQuality.Uncommon ) then
button.Border:SetAtlas("loottab-set-itemborder-green", true);
elseif ( itemQuality == Enum.ItemQuality.Rare ) then
button.Border:SetAtlas("loottab-set-itemborder-blue", true);
elseif ( itemQuality == Enum.ItemQuality.Epic ) then
button.Border:SetAtlas("loottab-set-itemborder-purple", true);
end
button.Icon:SetTexture(icon or "Interface\\ICONS\\temp")
button:GetParent().SetName:SetTextColor(GetItemQualityColor(itemQuality));
self:CheckItemButtonTooltip(button);
end
function LootJournalItemSetsMixin:UpdateList()
if ( self.dirty ) then
C_LootJournal.GenerateLootData(self:GetClassAndSpecFilters())
self.dirty = nil;
end
local buttons = self.buttons;
local offset = HybridScrollFrame_GetOffset(self);
local numSets = C_LootJournal.GetNumItemSets()
for i = 1, #buttons do
local button = buttons[i];
local index = offset + i;
if ( index <= numSets ) then
local setIndex, name, tierName, itemlevel, sourceText, isPVP, items, numItems = C_LootJournal.GetItemSetInfo(index)
button:Show();
button.SetName:SetText(name);
button.ItemLevel:SetFormattedText(EJ_SET_ITEM_LEVEL, tierName, itemlevel);
button.PVPIcon:SetShown(isPVP);
button.setIndex = setIndex
button.tooltipText = sourceText
button:CheckItemButtonTooltip()
for j = 1, numItems do
local itemButton = button.ItemButtons[j];
if ( not itemButton ) then
itemButton = CreateFrame("BUTTON", string.format("$parentItemButton%i", j), button, "LootJournalItemSetItemButtonTemplate");
button[string.format("$parentItemButton%i", j)] = itemButton
itemButton:SetPoint("LEFT", button.ItemButtons[j-1], "RIGHT", 5, 0);
end
itemButton.Icon:SetTexture(items[j].icon);
itemButton.itemID = items[j].itemID;
itemButton:Show();
self:ConfigureItemButton(itemButton);
end
for j = numItems + 1, #button.ItemButtons do
button.ItemButtons[j].itemID = nil;
button.ItemButtons[j]:Hide();
end
else
button:Hide();
end
end
local totalHeight = numSets * buttons[1]:GetHeight() + (numSets - 1) * LJ_ITEMSET_BUTTON_SPACING + LJ_ITEMSET_Y_OFFSET + LJ_ITEMSET_BOTTOM_BUFFER
HybridScrollFrame_Update(self, totalHeight, self:GetHeight());
end
LootJournalItemSetButtonMixin = {}
function LootJournalItemSetButtonMixin:OnEnter()
if self.tooltipText then
GameTooltip:SetOwner(self, "ANCHOR_TOP")
GameTooltip:SetText(LOOTJOURNAL_SOURCE_TOOLTIP_HEAD, 1, 1, 1)
GameTooltip:AddLine(self.tooltipText, nil, nil, nil, 1)
self.tooltipSetIndex = self.setIndex
GameTooltip:Show()
else
GameTooltip:Hide()
end
end
function LootJournalItemSetButtonMixin:CheckItemButtonTooltip()
if ( GameTooltip:GetOwner() == self and self.tooltipSetIndex ~= self.setIndex ) then
self:OnEnter();
end
end
@@ -1,164 +0,0 @@
<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="Custom_LootJournalItems.lua"/>
<Button name="LootJournalItemButtonTemplate" virtual="true">
<Scripts>
<OnEnter function="LootJournalItemButtonTemplate_OnEnter"/>
<OnLeave function="LootJournalItemButtonTemplate_OnLeave"/>
<OnClick function="LootJournalItemButton_OnClick"/>
</Scripts>
</Button>
<Button name="LootJournalItemSetItemButtonTemplate" inherits="LootJournalItemButtonTemplate" virtual="true" hidden="true">
<Size x="32" y="32"/>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon" parentKey="Icon">
<Size x="28" y="28"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</Texture>
</Layer>
<Layer level="BORDER">
<Texture parentKey="Border">
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentIcon" relativePoint="CENTER" x="20" y="1"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
self:SetParentArray("ItemButtons")
self.Border:SetAtlas("loottab-set-itemborder-green", true)
</OnLoad>
</Scripts>
</Button>
<Button name="LootJournalItemSetButtonTemplate" virtual="true">
<Size x="744" y="41"/>
<HitRectInsets left="34" right="390" top="1" bottom="1"/>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOPLEFT" x="0" y="0"/>
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentSetName" parentKey="SetName" inherits="GameFontNormalMed2" maxLines="1" justifyH="LEFT">
<Size x="320" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" x="38" y="-6"/>
</Anchors>
</FontString>
<FontString name="$parentItemLevel" parentKey="ItemLevel" inherits="GameFontNormal" text="Item Level" maxLines="1" justifyH="LEFT">
<Size x="320" y="0"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentSetName" relativePoint="BOTTOMLEFT" x="0" y="-2"/>
</Anchors>
<Color r="0.718" g="0.561" b="0.416"/>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="$parentPVPIcon" parentKey="PVPIcon" hidden="true">
<Size x="30" y="30"/>
<Anchors>
<Anchor point="LEFT" relativePoint="LEFT" x="0" y="2"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentIcon" parentKey="Icon" file="Interface\PVPFrame\Icons\prestige-icon-4"/>
</Layer>
</Layers>
<Scripts>
<OnEnter>
GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
GameTooltip:AddLine(LOOTJOURNAL_PVPICON_TOOLTIP_HEAD, 1, 1, 1)
GameTooltip:AddLine(LOOTJOURNAL_PVPICON_TOOLTIP, nil, nil, nil, 1)
GameTooltip:Show()
</OnEnter>
<OnLeave function="GameTooltip_Hide"/>
</Scripts>
</Frame>
<Button name="$parentItemButton1" parentKey="ItemButton1" inherits="LootJournalItemSetItemButtonTemplate">
<Anchors>
<Anchor point="LEFT" x="397" y="0"/>
</Anchors>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, LootJournalItemSetButtonMixin)
self.Background:SetAtlas("loottab-set-background", true)
</OnLoad>
<OnEnter>
self:OnEnter()
</OnEnter>
<OnLeave function="GameTooltip_Hide"/>
</Scripts>
</Button>
<Frame parent="EncounterJournal" name="$parentLootJournalItems" parentKey="LootJournalItems" hidden="true">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentInset" x="4" y="-51" />
<Anchor point="BOTTOMRIGHT" relativeTo="$parentInset" x="-4" y="0"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOP" x="0" y="4"/>
</Anchors>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame name="LootJournalItemsClassDropDown" parentKey="ClassDropDown" inherits="UIDropDownMenuTemplate" hidden="true">
<Scripts>
<OnLoad function="LootJournalItemsClassDropDown_OnLoad"/>
</Scripts>
</Frame>
<ScrollFrame name="$parentItemSetsFrame" parentKey="ItemSetsFrame" inherits="MinimalHybridScrollFrameTemplate" hidden="true">
<Size x="762" y="332"/>
<Anchors>
<Anchor point="BOTTOMLEFT" x="0" y="3"/>
</Anchors>
<Frames>
<Button parentKey="ClassButton" inherits="EJButtonTemplate" text="FILTER">
<Size x="114" y="26"/>
<Anchors>
<Anchor point="TOPRIGHT" x="-12" y="35"/>
</Anchors>
<Scripts>
<OnMouseDown>
EJButtonMixin.OnMouseDown(self, button);
ToggleDropDownMenu(1, nil, self:GetParent():GetParent().ClassDropDown, self, 5, 0);
PlaySound("igMainMenuOptionCheckBoxOn");
</OnMouseDown>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, LootJournalItemSetsMixin)
self:OnLoad()
</OnLoad>
<OnShow>
self:OnShow()
</OnShow>
</Scripts>
</ScrollFrame>
</Frames>
<Scripts>
<OnLoad>
Mixin(self, LootJournalItemsMixin)
self:OnLoad()
</OnLoad>
</Scripts>
</Frame>
</Ui>
@@ -68,6 +68,22 @@ local function updateScrollBar(scrollFrame, contentHeight, contentWidth)
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 scrollBar = getScrollBar(scrollFrame)
if scrollBar then scrollBar:Hide() end
@@ -78,6 +94,24 @@ local function getBodyChild()
return body and body.ScrollChild
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 = {}
@@ -248,7 +282,12 @@ local function renderRecommendations()
card:SetBackdropBorderColor(0.64, 0.45, 0.15, 0.95)
end
card.Artwork:SetTexture(BIOME_TEX[r.biome] or BIOME_TEX.fire)
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)
end
card.Name:SetText(r.name or r.title or "")
card.Loc:SetText(r.loc or r.description or "")
card.Level:SetText(r.level or "")
@@ -368,6 +407,8 @@ end
function MW_PlayerGuide_OnLoad(self)
EncounterJournal.playerGuideFrame = self
self:EnableMouse(true)
self:EnableMouseWheel(true)
local bodyChild = self.BodyScroll and self.BodyScroll.ScrollChild
if bodyChild then
@@ -386,6 +427,12 @@ function MW_PlayerGuide_OnLoad(self)
C_PlayerGuide.RegisterListener(function() refresh() 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()
if not C_PlayerGuide.HasData() then
C_PlayerGuide.SetLoading()
@@ -399,16 +446,94 @@ function MW_PlayerGuide_RefreshButton_OnClick()
C_PlayerGuide.RequestRefresh()
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)
local o = self.objectiveData
if not o then return end
if o.type == "dungeon" or o.type == "raid" then
if o.instanceID and o.instanceID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EJ_ContentTab_SelectAppropriateInstanceTab(o.instanceID)
EncounterJournal_DisplayInstance(o.instanceID)
end
openEncounterJournalInstance(o)
elseif o.type == "boss" then
if o.encounterID and o.encounterID > 0 then
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)
local r = self.recData
if r and r.instanceID and r.instanceID > 0 then
if not EncounterJournal:IsShown() then ShowUIPanel(EncounterJournal) end
EJ_ContentTab_SelectAppropriateInstanceTab(r.instanceID)
EncounterJournal_DisplayInstance(r.instanceID)
end
if r then openEncounterJournalInstance(r) end
end
function MW_PlayerGuide_Rec_OnEnter(self)
@@ -133,16 +133,19 @@
<Layers>
<Layer level="ARTWORK">
<!-- Artwork: 138x56, top offset 5 → bottom at y=61 from card top -->
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8">
<Size><AbsDimension x="168" y="72"/></Size>
<Anchors>
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-5"/></Offset></Anchor>
</Anchors>
<Color r="0.55" g="0.16" b="0.07"/>
<Texture name="$parentArtwork" parentKey="Artwork" file="Interface\Buttons\WHITE8X8" setAllPoints="true">
<Color r="1" g="1" b="1"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<!-- 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">
<Size><AbsDimension x="80" y="12"/></Size>
<Anchors>
@@ -154,7 +157,7 @@
<FontString name="$parentName" parentKey="Name" inherits="GameFontNormal" justifyH="LEFT">
<Size><AbsDimension x="162" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-80"/></Offset></Anchor>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-82"/></Offset></Anchor>
</Anchors>
<Color r="1.0" g="0.91" b="0.63"/>
</FontString>
@@ -162,7 +165,7 @@
<FontString name="$parentLoc" parentKey="Loc" inherits="MW_PG_Italic" justifyH="LEFT">
<Size><AbsDimension x="162" y="16"/></Size>
<Anchors>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-96"/></Offset></Anchor>
<Anchor point="TOPLEFT"><Offset><AbsDimension x="8" y="-98"/></Offset></Anchor>
</Anchors>
</FontString>
</Layer>
@@ -480,6 +483,7 @@
<Scripts>
<OnLoad function="MW_PlayerGuide_OnLoad"/>
<OnShow function="MW_PlayerGuide_OnShow"/>
<OnMouseWheel function="MW_PlayerGuide_OnMouseWheel"/>
</Scripts>
</Frame>
@@ -365,21 +365,6 @@ LOOT_JOURNAL_ITEM_SETS = LOOT_JOURNAL_ITEM_SETS or "Комплекты"
-- compact form on 3.3.5.
BOSS_INFO_STRING = BOSS_INFO_STRING or "Босс: %s"
-- Sirus ships a PKBT model mixin in SharedXML/SharedUIPanelPKBTTemplates;
-- we don't carry it. The EJ DressUpModel OnShow does `self:OnShow()` and
-- OnEvent does `self:OnEvent(...)`, expecting the mixin to provide them.
PKBT_ModelMixin = PKBT_ModelMixin or {}
PKBT_ModelMixin.OnShow = PKBT_ModelMixin.OnShow or function() end
PKBT_ModelMixin.OnEvent = PKBT_ModelMixin.OnEvent or function() end
PKBT_ModelMixin.OnLoad = PKBT_ModelMixin.OnLoad or function() end
if not SharedXML_Model_OnLoad then
function SharedXML_Model_OnLoad() end
end
if not SharedXML_Model_OnEvent then
function SharedXML_Model_OnEvent() end
end
ITEM_CLASS_2 = ITEM_CLASS_2 or 2
ITEM_CLASS_4 = ITEM_CLASS_4 or 4
@@ -119,13 +119,32 @@ end
-- 3.3.5 has no SetPortraitTextureFromCreatureDisplayID equivalent, so leave
-- the texture blank — the static UI-EJ-BOSS-* icon set elsewhere is enough.
do
local portraitByDisplayInfo = {}
local data = MoonWellEncounterJournalData
if data and data.creatures then
for _, creatureList in pairs(data.creatures) do
for _, creature in ipairs(creatureList) do
local displayInfo = creature[3]
local icon = creature[4]
if displayInfo and displayInfo > 0 and icon and icon ~= "" then
portraitByDisplayInfo[displayInfo] = icon
end
end
end
end
-- Ogom is a nested encounter variant and has no top-level portrait row.
portraitByDisplayInfo[6709] = [[Interface\EncounterJournal\UI-EJ-BOSS-Jammalan the Prophet]]
MoonWellEncounterPortraitByDisplayInfo = portraitByDisplayInfo
local frame = CreateFrame("Frame")
local texture = frame:CreateTexture()
local meta = getmetatable(texture)
local methods = meta and meta.__index
if type(methods) == "table" and not methods.SetPortrait then
methods.SetPortrait = function(self)
self:SetTexture(nil)
methods.SetPortrait = function(self, displayInfo)
self:SetTexture(portraitByDisplayInfo[displayInfo] or QUESTION_MARK_ICON)
self:SetTexCoord(0, 1, 0, 1)
end
end
end
@@ -62,7 +62,7 @@ MoonWellEncounterJournalData = {
[281] = { 'Нексус', 'Аспект из рода синих драконов, Малигос, вознамерился вновь обрести контроль над силами магии. Он начал ожесточенную борьбу за то, чтобы разорвать связь смертных с тайной энергией Азерота. Теперь синие драконы направляют потоки энергии всего мира к Нексусу, гигантскому логову Малигоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-thenexus.blp', 'interface\\lfgframe\\lfgicon-thenexus.blp', 'interface\\encounterjournal\\ui-ej-background-thenexus.blp', 'interface\\encounterjournal\\ui-ej-lorebg-thenexus.blp', 576, 0, 11, 1, 281, 520 },
[282] = { 'Окулус', 'Аспект из рода синих драконов, Малигос, вознамерился вновь обрести контроль над силами магии. Он начал ожесточенную борьбу за то, чтобы разорвать связь смертных с тайной энергией Азерота. Теперь синие драконы направляют потоки энергии всего мира к Нексусу, гигантскому логову Малигоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-theoculus.blp', 'interface\\lfgframe\\lfgicon-theoculus.blp', 'interface\\encounterjournal\\ui-ej-background-theoculus.blp', 'interface\\encounterjournal\\ui-ej-lorebg-theoculus.blp', 578, 0, 12, 0, 282, 528 },
[761] = { 'Рубиновое святилище', 'После обнаружения кладки яиц сумеречных драконов в Обсидиановом святилище, Кориалстраз попросил Совет Шести отправить искателей приключений уничтожить их. Это было сделано быстро и без лишнего шума. И хотя все со временем успокоилось, по необъяснимым причинам связь со стражами Рубинового святилища была утеряна. Привратники, стоявшие у портала, оказались убитыми, а магические камни – разрушены. Нынешнее положение Рубинового святилища неизвестно, и ещё меньше известно о напавших на него, так как число чёрных драконов в Нордсколе слишком мало для подобного нападения. Что же за сила посмела напасть на оплот красных драконов, и как они смогли проскользнуть мимо стражников незамеченными?', 'interface\\encounterjournal\\ui-ej-dungeonbutton-rubysanctum.blp', 'interface\\lfgframe\\lfgicon-rubysanctum.blp', 'interface\\encounterjournal\\ui-ej-background-rubysanctum.blp', 'interface\\encounterjournal\\ui-ej-lorebg-rubysanctum.blp', 724, 0, 12, 16, 761, 609 },
[772] = { 'Старые предгорья Хилсбрада', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blpp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 560, 0, 12, 1024, 772, 845 },
[772] = { 'Старые предгорья Хилсбрада', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 560, 0, 12, 1024, 772, 845 },
[253] = { 'Темный лабиринт', 'Дренорским изгнанникам смерть казалась неприятным и неблаговидным последствием жизни. Поэтому дренеи скрывали своих мертвых в подземном некрополе Аукиндоне. Этот чудо-лабиринт раскинулся под самым лесом Тероккар.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-auchindoun.blp', 'interface\\lfgframe\\lfgicon-auchindoun.blp', 'interface\\encounterjournal\\ui-ej-background-auchindoun.blp', 'interface\\encounterjournal\\ui-ej-lorebg-auchindoun.blp', 555, 0, 13, 0, 253, 834 },
[769] = { 'Очищение Стратхольма', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-TheCullingOfStratholme.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-TheCullingOfStratholme.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-TheCullingOfStratholme.blp', 595, 0, 13, 1024, 769, 521 },
[277] = { 'Чертоги Камня', 'Покидая Азерот, титаны доверили лучшим защитникам охрану Ульдуара, таинственного города, укрытого пиками Грозовой гряды. Первым среди своих братьев был назван страж по имени Локен, но осознав, какую мощь оставили после себя титаны, он поддался тьме и вверг город в пучину хаоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-hallsofstone.blp', 'interface\\lfgframe\\lfgicon-hallsofstone.blp', 'interface\\encounterjournal\\ui-ej-background-hallsofstone.blp', 'interface\\encounterjournal\\ui-ej-lorebg-hallsofstone.blp', 599, 0, 14, 0, 277, 526 },
@@ -72,7 +72,7 @@ MoonWellEncounterJournalData = {
[275] = { 'Чертоги Молний', 'Покидая Азерот, титаны доверили лучшим защитникам охрану Ульдуара, таинственного города, укрытого пиками Грозовой гряды. Первым среди своих братьев был назван страж по имени Локен, но осознав, какую мощь оставили после себя титаны, он поддался тьме и вверг город в пучину хаоса.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-hallsoflightning.blp', 'interface\\lfgframe\\lfgicon-hallsoflightning.blp', 'interface\\encounterjournal\\ui-ej-background-hallsoflightning.blp', 'interface\\encounterjournal\\ui-ej-lorebg-hallsoflightning.blp', 602, 0, 15, 1, 275, 525 },
[229] = { 'Нижняя часть пика Черной горы', 'Потрясающая крепость, вырезанная в огненном сердце Черной горы, веками служила образцом мощи клана Черного Железа. Но не так давно черный дракон Нефариан и его род захватили верхние ярусы крепости и развязали жестокую войну с дворфами. Армии драконов нашли союзника в лице вождя Ренда Чернорука и его \'Орды\', и теперь их объединенные силы хозяйничают на Пике Черной горы, проводя эксперименты, призванные расширить их ряды, и строя планы по уничтожению дворфов Черного Железа.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-blackrockspire.blp', 'interface\\lfgframe\\lfgicon-blackrockspire.blp', 'interface\\encounterjournal\\ui-ej-background-blackrockspire.blp', 'interface\\encounterjournal\\ui-ej-lorebg-blackrockspire.blp', 229, 0, 16, 0, 229, 882 },
[278] = { 'Яма Сарона', 'Противостояние защитников Азерота и армии Короля-лича продолжалось долгие годы. За это время пало множество героев, а после смерти они были вынуждены присоединиться к чудовищной армии нежити. В ходе этого безустанного противостояния Тирион Фордринг из Серебряного Авангарда объединил силы с рыцарями Черного Клинка под командованием Дариона Могрейна. Теперь эта объединенная армия, получившая название Пепельный союз, собирается взять штурмом цитадель Ледяной Короны.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-pitofsaron.blp', 'interface\\lfgframe\\lfgicon-pitofsaron.blp', 'interface\\encounterjournal\\ui-ej-background-pitofsaron.blp', 'interface\\encounterjournal\\ui-ej-lorebg-pitofsaron.blp', 658, 0, 16, 1, 278, 602 },
[768] = { 'Черные топи ', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blpp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 269, 0, 16, 1024, 768, 844 },
[768] = { 'Черные топи ', 'В Глубине пещер Времени пробудился погруженный в раздумья дракон Ноздорму. Еще на заре мироздания бронзовые драконы неусыпно хранили запутанный лабиринт временных потоков и следили за хрупким равновесием времени.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-CavernsOfTime.blp', 'interface\\lfgframe\\LFGIcon-CavernsOfTime.BLP', 'interface\\encounterjournal\\UI-EJ-BACKGROUND-CavernsOfTime.blp', 'interface\\encounterjournal\\UI-EJ-LOREBG-CavernsOfTime.blp', 269, 0, 16, 1024, 768, 844 },
[762] = { 'Огненная Пропасть', 'Огненная Пропасть залегает глубоко под Оргриммаром. Ee вулканическими пещерами некогда владели дикие трогги, пока туда не пришли жуткие сектанты из клана Пламенеющего Клинка. Сектанты вызвали в этот мир жуткое создание, имя которому Тарагаман Ненасытный. С помощью его мощи, ходят слухи, что они хотят выбраться из подземелья и захватить Оргриммар. В помощь себе они также призвали к себе в ряды сатира Баззалана, который будет руководить армией сектантов.', 'interface\\encounterjournal\\UI-EJ-DUNGEONBUTTON-RagefireChasm.blp', 'interface\\lfgframe\\lfgicon-RAGEFIRECHASM.blp', 'interface\\encounterjournal\\ui-ej-background-ragefirechasm.blp', 'interface\\encounterjournal\\ui-ej-lorebg-ragefirechasm.blp', 389, 0, 17, 0, 762, 680 },
[240] = { 'Пещеры Стенаний', 'Много лет назад прославленный друид ночных эльфов Наралекс спустился в темные Пещеры Стенаний со своими спутниками. Своим названием пещеры обязаны похожим на плач звукам, сопровождающим вырывающийся на свободу пар. Он намеревался использовать подземные источники, чтобы вернуть Степям их былое плодородие, но стоило ему окунуться в Изумрудный Сон, как его прекрасное видение обернулось жутким кошмаром. С тех самых пор над этим местом нависло страшное проклятие.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-wailingcaverns.blp', 'interface\\lfgframe\\lfgicon-wailingcaverns.blp', 'interface\\encounterjournal\\ui-ej-background-wailingcaverns.blp', 'interface\\encounterjournal\\ui-ej-lorebg-wailingcaverns.blp', 43, 0, 18, 0, 240, 749 },
[236] = { 'Стратхольм', 'Стратхольм был настоящей жемчужиной северного Лордерона, но сейчас все помнят лишь его сокрушительное падение. Именно здесь принц Артас когда-то ослушался своего наставника, благородного паладина Утера Светоносного, и вырезал сотни горожан, якобы зараженных чумой нежити. С тех пор в городе царят смерть, предательство и отчаяние.', 'interface\\encounterjournal\\ui-ej-dungeonbutton-stratholme.blp', 'interface\\lfgframe\\lfgicon-stratholme.blp', 'interface\\encounterjournal\\ui-ej-background-stratholme.blp', 'interface\\encounterjournal\\ui-ej-lorebg-stratholme.blp', 329, 0, 19, 0, 236, 892 },
@@ -1,128 +0,0 @@
local securecall = securecall
local ipairs = ipairs
local next = next
local type = type
local strsplit = string.split
local ExecuteFrameScript = ExecuteFrameScript
local GetFramesRegisteredForEvent = GetFramesRegisteredForEvent
local UnitName = UnitName
local UnitTokenFromGUID = UnitTokenFromGUID
local REGISTERED_CUSTOM_EVENTS = {}
local REGISTERED_CUSTOM_EVENTS_ALL
local function SecureNext(elements, key)
return securecall(next, elements, key)
end
function FireClientEvent(event, ...)
for _, frame in SecureNext, {GetFramesRegisteredForEvent(event)} do
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
end
end
function FireCustomClientEvent(event, ...)
if REGISTERED_CUSTOM_EVENTS[event] then
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS[event] do
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
end
end
if REGISTERED_CUSTOM_EVENTS_ALL then
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS_ALL do
securecall(ExecuteFrameScript, frame, "OnEvent", event, ...)
end
end
end
local eventRegistrar = CreateFrame("Frame")
eventRegistrar:SetScript("OnAttributeChanged", function(this, name, value)
if name == "1" then
if not REGISTERED_CUSTOM_EVENTS[value] then
REGISTERED_CUSTOM_EVENTS[value] = {}
end
REGISTERED_CUSTOM_EVENTS[value][this:GetAttribute("f")] = true
elseif name == "0" then
if REGISTERED_CUSTOM_EVENTS[value] then
REGISTERED_CUSTOM_EVENTS[value][this:GetAttribute("f")] = nil
end
elseif name == "2" then
REGISTERED_CUSTOM_EVENTS_ALL[value] = true
elseif name == "3" then
REGISTERED_CUSTOM_EVENTS_ALL[value] = nil
end
end)
function RegisterCustomEvent(self, event)
eventRegistrar:SetAttribute("f", self)
eventRegistrar:SetAttribute("1", event)
end
function UnregisterCustomEvent(self, event)
eventRegistrar:SetAttribute("f", self)
eventRegistrar:SetAttribute("0", event)
end
function RegisterAllCustomEvents(self)
if not REGISTERED_CUSTOM_EVENTS_ALL then
REGISTERED_CUSTOM_EVENTS_ALL = {}
end
eventRegistrar:SetAttribute("2", self)
end
function UnregisterAllCustomEvents(self)
if not REGISTERED_CUSTOM_EVENTS_ALL then
return
end
eventRegistrar:SetAttribute("3", self)
end
function FireCustomClientUnitGroupEvent(event, unitGUID, ...)
if event and REGISTERED_CUSTOM_EVENTS[event] and unitGUID then
local units = {UnitTokenFromGUID(unitGUID)}
if #units > 0 then
for frame in SecureNext, REGISTERED_CUSTOM_EVENTS[event] do
for _, unit in ipairs(units) do
securecall(ExecuteFrameScript, frame, "OnEvent", event, unit, ...)
end
end
end
end
end
EventHandler = setmetatable(
{
events = {}, -- Original events
listeners = {}, -- New tables for handling events outside og EventHandler
RegisterListener = function(self, listener)
self.listeners[listener] = true
end,
Handle = function(self, opcode, message, unk, sender)
if sender == UnitName("player") then
for listener in SecureNext, self.listeners do
if listener[opcode] then
securecall(listener[opcode], listener, message)
end
end
if self.events[opcode] then
securecall(self.events[opcode], self, message)
end
end
end
},
{
__newindex = function(self, key, value)
if type(value) == "function" then
self.events[key] = value
end
rawset(self, key, value)
end
}
)
local EventHandlerFrame = CreateFrame("Frame")
EventHandlerFrame:RegisterEvent("CHAT_MSG_ADDON")
EventHandlerFrame:SetScript("OnEvent", function(self, event, opcode, message, unk, sender)
EventHandler:Handle(opcode, message, unk, sender)
end)
@@ -1,422 +0,0 @@
local pairs = pairs
local ipairs = ipairs
local next = next
local CreateFrame = CreateFrame
ObjectPoolMixin = {};
function ObjectPoolMixin:OnLoad(creationFunc, resetterFunc)
self.creationFunc = creationFunc;
self.resetterFunc = resetterFunc;
self.activeObjects = {};
self.inactiveObjects = {};
self.numActiveObjects = 0;
end
function ObjectPoolMixin:Acquire()
local numInactiveObjects = #self.inactiveObjects;
if numInactiveObjects > 0 then
local obj = self.inactiveObjects[numInactiveObjects];
self.activeObjects[obj] = true;
self.numActiveObjects = self.numActiveObjects + 1;
self.inactiveObjects[numInactiveObjects] = nil;
return obj, false;
end
local newObj = self.creationFunc(self);
if self.resetterFunc and not self.disallowResetIfNew then
self.resetterFunc(self, newObj);
end
self.activeObjects[newObj] = true;
self.numActiveObjects = self.numActiveObjects + 1;
return newObj, true;
end
function ObjectPoolMixin:Release(obj)
if self:IsActive(obj) then
self.inactiveObjects[#self.inactiveObjects + 1] = obj;
self.activeObjects[obj] = nil;
self.numActiveObjects = self.numActiveObjects - 1;
if self.resetterFunc then
self.resetterFunc(self, obj);
end
return true;
end
return false;
end
function ObjectPoolMixin:ReleaseAll()
for obj in pairs(self.activeObjects) do
self:Release(obj);
end
end
function ObjectPoolMixin:SetResetDisallowedIfNew(disallowed)
self.disallowResetIfNew = disallowed;
end
function ObjectPoolMixin:EnumerateActive()
return pairs(self.activeObjects);
end
function ObjectPoolMixin:GetNextActive(current)
return (next(self.activeObjects, current));
end
function ObjectPoolMixin:GetNextInactive(current)
return (next(self.inactiveObjects, current));
end
function ObjectPoolMixin:IsActive(object)
return (self.activeObjects[object] ~= nil);
end
function ObjectPoolMixin:GetNumActive()
return self.numActiveObjects;
end
function ObjectPoolMixin:EnumerateInactive()
return ipairs(self.inactiveObjects);
end
function CreateObjectPool(creationFunc, resetterFunc)
local objectPool = CreateFromMixins(ObjectPoolMixin);
objectPool:OnLoad(creationFunc, resetterFunc);
return objectPool;
end
FramePoolMixin = CreateFromMixins(ObjectPoolMixin);
local pollFrameIndex = 0
local function FramePoolFactory(framePool)
framePool.createdFrames = framePool.createdFrames + 1
pollFrameIndex = pollFrameIndex + 1
local parentName, frameName = framePool.parent and framePool.parent:GetName()
if parentName then
frameName = string.format("%sPoolFrame%s%i_%i", parentName, framePool.frameTemplate or "NoTemplate", framePool.createdFrames, pollFrameIndex)
elseif framePool.frameTemplate then
frameName = string.format("UnnamedPoolFrame%s%i_%i", framePool.frameTemplate, framePool.createdFrames, pollFrameIndex)
else
frameName = string.format("UnnamedPoolFrameNoTemplate%i_%i", framePool.createdFrames, pollFrameIndex)
end
return CreateFrame(framePool.frameType, frameName, framePool.parent, framePool.frameTemplate);
end
local function ForbiddenFramePoolFactory(framePool)
return FramePoolFactory(framePool)
end
function FramePoolMixin:OnLoad(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
if forbidden then
local creationFunc = ForbiddenFramePoolFactory;
if frameInitFunc ~= nil then
creationFunc = function(framePool)
local frame = ForbiddenFramePoolFactory(framePool);
frameInitFunc(frame);
return frame;
end
end
ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
else
local creationFunc = FramePoolFactory;
if frameInitFunc ~= nil then
creationFunc = function(framePool)
local frame = FramePoolFactory(framePool);
frameInitFunc(frame);
return frame;
end
end
ObjectPoolMixin.OnLoad(self, creationFunc, resetterFunc);
end
self.createdFrames = 0;
self.frameType = frameType;
self.parent = parent;
self.frameTemplate = frameTemplate;
end
function FramePoolMixin:GetTemplate()
return self.frameTemplate;
end
function FramePool_Hide(framePool, frame)
frame:Hide();
end
function FramePool_HideAndClearAnchors(framePool, frame)
frame:Hide();
frame:ClearAllPoints();
end
function CreateFramePool(frameType, parent, frameTemplate, resetterFunc, forbidden, frameInitFunc)
local framePool = CreateFromMixins(FramePoolMixin);
framePool:OnLoad(frameType, parent, frameTemplate, resetterFunc or FramePool_HideAndClearAnchors, forbidden, frameInitFunc);
return framePool;
end
TexturePoolMixin = CreateFromMixins(ObjectPoolMixin);
local function TexturePoolFactory(texturePool)
return texturePool.parent:CreateTexture(nil, texturePool.layer, texturePool.textureTemplate, texturePool.subLayer);
end
function TexturePoolMixin:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc)
ObjectPoolMixin.OnLoad(self, TexturePoolFactory, resetterFunc);
self.parent = parent;
self.layer = layer;
self.subLayer = subLayer;
self.textureTemplate = textureTemplate;
end
TexturePool_Hide = FramePool_Hide;
TexturePool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
function CreateTexturePool(parent, layer, subLayer, textureTemplate, resetterFunc)
local texturePool = CreateFromMixins(TexturePoolMixin);
texturePool:OnLoad(parent, layer, subLayer, textureTemplate, resetterFunc or TexturePool_HideAndClearAnchors);
return texturePool;
end
FontStringPoolMixin = CreateFromMixins(ObjectPoolMixin);
local function FontStringPoolFactory(fontStringPool)
return fontStringPool.parent:CreateFontString(nil, fontStringPool.layer, fontStringPool.fontStringTemplate, fontStringPool.subLayer);
end
function FontStringPoolMixin:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc)
ObjectPoolMixin.OnLoad(self, FontStringPoolFactory, resetterFunc);
self.parent = parent;
self.layer = layer;
self.subLayer = subLayer;
self.fontStringTemplate = fontStringTemplate;
end
FontStringPool_Hide = FramePool_Hide;
FontStringPool_HideAndClearAnchors = FramePool_HideAndClearAnchors;
function CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
local fontStringPool = CreateFromMixins(FontStringPoolMixin);
fontStringPool:OnLoad(parent, layer, subLayer, fontStringTemplate, resetterFunc or FontStringPool_HideAndClearAnchors);
return fontStringPool;
end
ActorPoolMixin = CreateFromMixins(ObjectPoolMixin);
local function ActorPoolFactory(actorPool)
return actorPool.parent:CreateActor(nil, actorPool.actorTemplate);
end
function ActorPoolMixin:OnLoad(parent, actorTemplate, resetterFunc)
ObjectPoolMixin.OnLoad(self, ActorPoolFactory, resetterFunc);
self.parent = parent;
self.actorTemplate = actorTemplate;
end
ActorPool_Hide = FramePool_Hide;
function ActorPool_HideAndClearModel(actorPool, actor)
actor:ClearModel();
actor:Hide();
end
function CreateActorPool(parent, actorTemplate, resetterFunc)
local actorPool = CreateFromMixins(ActorPoolMixin);
actorPool:OnLoad(parent, actorTemplate, resetterFunc or ActorPool_HideAndClearModel);
return actorPool;
end
FramePoolCollectionMixin = {};
function CreateFramePoolCollection()
local poolCollection = CreateFromMixins(FramePoolCollectionMixin);
poolCollection:OnLoad();
return poolCollection;
end
function FramePoolCollectionMixin:OnLoad()
self.pools = {};
end
function FramePoolCollectionMixin:GetNumActive()
local numTotalActive = 0;
for _, pool in pairs(self.pools) do
numTotalActive = numTotalActive + pool:GetNumActive();
end
return numTotalActive;
end
-- Returns the pool, and whether or not the pool needed to be created.
function FramePoolCollectionMixin:GetOrCreatePool(frameType, parent, template, resetterFunc, forbidden)
local pool = self:GetPool(template);
if not pool then
return self:CreatePool(frameType, parent, template, resetterFunc, forbidden), true;
end
return pool, false;
end
function FramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc)
assert(self:GetPool(template) == nil);
local pool = CreateFramePool(frameType, parent, template, resetterFunc, forbidden, frameInitFunc);
self.pools[template] = pool;
return pool;
end
function FramePoolCollectionMixin:GetPool(template)
return self.pools[template];
end
function FramePoolCollectionMixin:Acquire(template)
local pool = self:GetPool(template);
assert(pool);
return pool:Acquire();
end
function FramePoolCollectionMixin:Release(object)
for _, pool in pairs(self.pools) do
if pool:Release(object) then
-- Found it! Just return
return;
end
end
-- Huh, we didn't find that object
assert(false);
end
function FramePoolCollectionMixin:ReleaseAllByTemplate(template)
local pool = self:GetPool(template);
if pool then
pool:ReleaseAll();
end
end
function FramePoolCollectionMixin:ReleaseAll()
for key, pool in pairs(self.pools) do
pool:ReleaseAll();
end
end
function FramePoolCollectionMixin:EnumerateActiveByTemplate(template)
local pool = self:GetPool(template);
if pool then
return pool:EnumerateActive();
end
return nop;
end
function FramePoolCollectionMixin:EnumerateActive()
local currentPoolKey, currentPool = next(self.pools, nil);
local currentObject = nil;
return function()
if currentPool then
currentObject = currentPool:GetNextActive(currentObject);
while not currentObject do
currentPoolKey, currentPool = next(self.pools, currentPoolKey);
if currentPool then
currentObject = currentPool:GetNextActive();
else
break;
end
end
end
return currentObject;
end, nil;
end
function FramePoolCollectionMixin:EnumerateInactiveByTemplate(template)
local pool = self:GetPool(template);
if pool then
return pool:EnumerateInactive();
end
return nop;
end
FixedSizeFramePoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
function CreateFixedSizeFramePoolCollection()
local poolCollection = CreateFromMixins(FixedSizeFramePoolCollectionMixin);
poolCollection:OnLoad();
return poolCollection;
end
function FixedSizeFramePoolCollectionMixin:OnLoad()
FramePoolCollectionMixin.OnLoad(self);
self.sizes = {};
end
function FixedSizeFramePoolCollectionMixin:CreatePool(frameType, parent, template, resetterFunc, forbidden, maxPoolSize, preallocate)
local pool = FramePoolCollectionMixin.CreatePool(self, frameType, parent, template, resetterFunc, forbidden);
if preallocate then
for i = 1, maxPoolSize do
pool:Acquire();
end
pool:ReleaseAll();
end
self.sizes[template] = maxPoolSize;
return pool;
end
function FixedSizeFramePoolCollectionMixin:Acquire(template)
local pool = self:GetPool(template);
assert(pool);
if pool:GetNumActive() < self.sizes[template] then
return pool:Acquire();
end
return nil;
end
FontStringPoolCollectionMixin = CreateFromMixins(FramePoolCollectionMixin);
function CreateFontStringPoolCollection()
local poolCollection = CreateFromMixins(FontStringPoolCollectionMixin);
poolCollection:OnLoad();
return poolCollection;
end
function FontStringPoolCollectionMixin:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
local pool = self:GetPool(fontStringTemplate);
if not pool then
pool = self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
end
return pool;
end
function FontStringPoolCollectionMixin:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc)
assert(self:GetPool(fontStringTemplate) == nil);
local pool = CreateFontStringPool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
self.pools[fontStringTemplate] = pool;
return pool;
end
function FontStringPoolCollectionMixin:CreatePoolIfNeeded(parent, layer, subLayer, fontStringTemplate, resetterFunc)
if not self:GetPool(fontStringTemplate) then
self:CreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
end
end
function FontStringPoolCollectionMixin:Acquire(fontStringTemplate, parent, layer, subLayer, resetterFunc)
local pool = self:GetOrCreatePool(parent, layer, subLayer, fontStringTemplate, resetterFunc);
local newString = pool:Acquire();
if parent then
newString:SetParent(parent);
end
if layer then
newString:SetDrawLayer(layer, subLayer);
end
return newString;
end
@@ -1,660 +0,0 @@
local function getObjTypeMeta(objType)
local obj = CreateFrame(objType)
obj:Hide()
return getmetatable(obj).__index, obj
end
local FrameMeta, FrameObj = getObjTypeMeta("Frame")
local FontStringMeta = getmetatable(FrameObj:CreateFontString()).__index
local TextureMeta = getmetatable(FrameObj:CreateTexture()).__index
local ButtonMeta = getObjTypeMeta("Button")
local CheckButtonMeta = getObjTypeMeta("CheckButton")
local EditBoxMeta = getObjTypeMeta("EditBox")
local SimpleHTMLMeta = getObjTypeMeta("SimpleHTML")
local SliderMeta = getObjTypeMeta("Slider")
local StatusBarMeta = getObjTypeMeta("StatusBar")
local ScrollFrameMeta = getObjTypeMeta("ScrollFrame")
local MessageFrameMeta = getObjTypeMeta("MessageFrame")
local ScrollingMessageFrameMeta = getObjTypeMeta("ScrollingMessageFrame")
local ModelMeta = getObjTypeMeta("Model")
--local ColorSelectMeta = createAndGetMeta("ColorSelect")
--local MovieFrameMeta = createAndGetMeta("MovieFrame")
local AnimationGroup = FrameObj:CreateAnimationGroup()
local AnimationGroupMeta = getmetatable(AnimationGroup).__index
local AnimationAnimationMeta = getmetatable(AnimationGroup:CreateAnimation("Animation")).__index
local AnimationAlphaMeta = getmetatable(AnimationGroup:CreateAnimation("Alpha")).__index
local AnimationRotationMeta = getmetatable(AnimationGroup:CreateAnimation("Rotation")).__index
local AnimationPathMeta = getmetatable(AnimationGroup:CreateAnimation("Path")).__index
local AnimationScaleMeta = getmetatable(AnimationGroup:CreateAnimation("Scale")).__index
local AnimationTranslationMeta = getmetatable(AnimationGroup:CreateAnimation("Translation")).__index
local GameTooltipMeta, PlayerModelMeta, DressUpModelMeta, TabardModelMeta, ModelFFXMeta
if IsOnGlueScreen() then
ModelFFXMeta = getObjTypeMeta("ModelFFX")
else
GameTooltipMeta = getObjTypeMeta("GameTooltip")
PlayerModelMeta = getObjTypeMeta("PlayerModel")
DressUpModelMeta = getObjTypeMeta("DressUpModel")
TabardModelMeta = getObjTypeMeta("TabardModel")
-- local CooldownMeta = createAndGetMeta("Cooldown")
-- local MinimapMeta = createAndGetMeta("Minimap")
-- local TaxiRouteFrameMeta = createAndGetMeta("TaxiRouteFrame")
-- local QuestPOIFrameMeta = createAndGetMeta("QuestPOIFrame")
-- local WorldFrameMeta = createAndGetMeta("WorldFrame")
end
local function nop() end
local function ScriptRegion_SetShown(self, show)
if show then
self:Show()
else
self:Hide()
end
end
local function ScriptRegion_GetScaledRect(self)
local left, bottom, width, height = self:GetRect()
if left and bottom and width and height then
local scale = self:GetEffectiveScale()
return left * scale, bottom * scale, width * scale, height * scale
end
end
local function ScriptRegion_IsMouseOverEx(self, ignoreVisibility, checkMouseFocus)
if ignoreVisibility or self:IsVisible() then
local l, r, t, b = self:GetHitRectInsets()
return self:IsMouseOver(-t, b, l, -r) and (not checkMouseFocus or self == GetMouseFocus())
end
return false
end
local function ScriptRegionResizing_ClearAndSetPoint(self, ...)
self:ClearAllPoints()
self:SetPoint(...)
end
local function Region_GetEffectiveScale(self)
return self:GetParent():GetEffectiveScale()
end
local function LayoutFrame_SetParentArray(self, arrayName, element, setInSelf)
local parent = not setInSelf and self:GetParent() or self
if not parent[arrayName] then
parent[arrayName] = {}
end
table.insert(parent[arrayName], element or self)
end
local RegisterCustomEvent = RegisterCustomEvent
local UnregisterCustomEvent = UnregisterCustomEvent
local function Frame_RegisterCustomEvent(self, event)
RegisterCustomEvent(self, event)
end
local function Frame_UnregisterCustomEvent(self, event)
UnregisterCustomEvent(self, event)
end
local function FontString_SetDesaturated(self, toggle, color)
if toggle then
self:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
else
if type(color) == "table" then
self:SetTextColor(color.r, color.g, color.b)
else
self:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
end
end
end
local SECONDS_PER_DAY = 86400
local function FontString_SetRemainingTime(self, seconds, daysformat)
if type(seconds) ~= "number" then
self:SetText("")
return
end
if daysformat then
if seconds >= SECONDS_PER_DAY then
self:SetFormattedText(D_DAYS_FULL, math.floor(seconds / SECONDS_PER_DAY))
else
self:SetText(date("!%X", seconds))
end
elseif seconds >= SECONDS_PER_DAY then
local days = string.format(DAY_ONELETTER_ABBR_SHORT, math.floor(seconds / SECONDS_PER_DAY))
self:SetFormattedText("%s %s", days, date("!%X", seconds % SECONDS_PER_DAY))
elseif seconds >= 0 then
self:SetText(date("!%X", seconds))
else
self:SetText("")
end
end
local function Texture_SetSubTexCoord(self, left, right, top, bottom)
local ULx, ULy, LLx, LLy, URx, URy, LRx, LRy = self:GetTexCoord()
local leftedge = ULx
local rightedge = URx
local topedge = ULy
local bottomedge = LLy
local width = rightedge - leftedge
local height = bottomedge - topedge
leftedge = ULx + width * left
topedge = ULy + height * top
rightedge = math.max(rightedge * right, ULx)
bottomedge = math.max(bottomedge * bottom, ULy)
ULx = leftedge
ULy = topedge
LLx = leftedge
LLy = bottomedge
URx = rightedge
URy = topedge
LRx = rightedge
LRy = bottomedge
self:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy)
end
local ATLAS_FIELD_WIDTH = 1
local ATLAS_FIELD_HEIGHT = 2
local ATLAS_FIELD_LEFT = 3
local ATLAS_FIELD_RIGHT = 4
local ATLAS_FIELD_TOP = 5
local ATLAS_FIELD_BOTTOM = 6
local ATLAS_FIELD_TILESHORIZ = 7
local ATLAS_FIELD_TILESVERT = 8
local ATLAS_FIELD_TEXTUREPATH = 9
local S_ATLAS_STORAGE = S_ATLAS_STORAGE
local function Texture_SetAtlas(self, atlasName, useAtlasSize, filterMode)
if type(self) ~= "table" then
error("Attempt to find 'this' in non-table object (used '.' instead of ':' ?)", 3)
elseif type(atlasName) ~= "string" then
error(string.format("Usage: %s:SetAtlas(\"atlasName\"[, useAtlasSize])", self:GetName() or tostring(self)), 3)
end
local atlas = S_ATLAS_STORAGE[atlasName]
if not atlas then
error(string.format("%s:SetAtlas: Atlas %s does not exist", self:GetName() or tostring(self), atlasName), 3)
end
local success = self:SetTexture(atlas[ATLAS_FIELD_TEXTUREPATH] or "", atlas[ATLAS_FIELD_TILESHORIZ], atlas[ATLAS_FIELD_TILESVERT])
if success then
if useAtlasSize then
self:SetWidth(atlas[ATLAS_FIELD_WIDTH])
self:SetHeight(atlas[ATLAS_FIELD_HEIGHT])
end
self:SetTexCoord(atlas[ATLAS_FIELD_LEFT], atlas[ATLAS_FIELD_RIGHT], atlas[ATLAS_FIELD_TOP], atlas[ATLAS_FIELD_BOTTOM])
self:SetHorizTile(atlas[ATLAS_FIELD_TILESHORIZ])
self:SetVertTile(atlas[ATLAS_FIELD_TILESVERT])
end
return success
end
local function Texture_SetPortrait(self, displayID)
local portrait = strconcat("Interface\\PORTRAITS\\Portrait_model_", tonumber(displayID))
local success = self:SetTexture(portrait)
return success
end
local function Button_SetEnabled(self, enabled)
if enabled then
self:Enable()
else
self:Disable()
end
end
local function Button_SetNormalAtlas(self, atlasName, useAtlasSize, filterMode)
local texture = self:GetNormalTexture()
if not texture then
self:SetNormalTexture("")
texture = self:GetNormalTexture()
end
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
end
local function Button_SetPushedAtlas(self, atlasName, useAtlasSize, filterMode)
local texture = self:GetPushedTexture()
if not texture then
self:SetPushedTexture("")
texture = self:GetPushedTexture()
end
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
end
local function Button_SetDisabledAtlas(self, atlasName, useAtlasSize, filterMode)
local texture = self:GetDisabledTexture()
if not texture then
self:SetDisabledTexture("")
texture = self:GetDisabledTexture()
end
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
end
local function Button_SetHighlightAtlas(self, atlasName, useAtlasSize, filterMode)
local texture = self:GetHighlightTexture()
if not texture then
self:SetHighlightTexture("")
texture = self:GetHighlightTexture()
end
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
end
local function Button_ClearNormalTexture(self)
self:SetNormalTexture("")
end
local function Button_ClearPushedTexture(self)
self:SetPushedTexture("")
end
local function Button_ClearDisabledTexture(self)
self:SetDisabledTexture("")
end
local function Button_ClearHighlightTexture(self)
self:SetHighlightTexture("")
end
local function CheckButton_SetCheckedAtlas(self, atlasName, useAtlasSize, filterMode)
local texture = self:GetCheckedTexture()
if not texture then
self:SetCheckedTexture("")
texture = self:GetCheckedTexture()
end
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
end
local function CheckButton_SetDisabledCheckedAtlas(self, atlasName, useAtlasSize, filterMode)
local texture = self:GetDisabledCheckedTexture()
if not texture then
self:SetDisabledCheckedTexture("")
texture = self:GetDisabledCheckedTexture()
end
Texture_SetAtlas(texture, atlasName, useAtlasSize, filterMode)
end
local function CheckButton_ClearCheckedTexture(self)
self:SetCheckedTexture("")
end
local function CheckButton_ClearDisabledCheckedTexture(self)
self:SetDisabledCheckedTexture("")
end
local function EditBox_IsEnabled(self)
return self.__ext_Disabled ~= true
end
local function EditBox_Enabled(self)
if self.__ext_mouseEnabled then
self:EnableMouse(true)
end
if self.__ext_autoFocus then
self:SetAutoFocus(true)
self:SetFocus()
self:HighlightText(0, 0)
end
self.SetFocus = nil
self.__ext_Disabled = nil
self.__ext_autoFocus = nil
self.__ext_mouseEnabled = nil
end
local function EditBox_Disable(self)
if not self.__ext_Disabled then
self.__ext_autoFocus = self:IsAutoFocus() == 1
self.__ext_mouseEnabled = self:IsMouseEnabled() == 1
end
self:SetAutoFocus(false)
self:ClearFocus()
self:EnableMouse(false)
self.SetFocus = nop
self.__ext_Disabled = true
end
local function PlayerModel_UpdateModelEffects(self)
self:ResetAllSpells()
do -- 3D armor
local displayIDs = self:GetDisplayIDs()
if displayIDs then
for index, displayID in ipairs(displayIDs) do
if displayID ~= 0 then
local spellID = ITEMS_WITH_3D[displayID]
if spellID then
self:SetSpell(spellID)
end
end
end
end
end
do -- customizations
if not self.__hideCustomizations then
local guid = self.__unitGUID
if guid and guid == UnitGUID("player") then
local activeCustomizationItems = C_CustomizationsCollection.GetActiveCustomizationItems(1)
if activeCustomizationItems then
for index, customizationItemID in ipairs(activeCustomizationItems) do
local spellID = C_CustomizationsCollection.GetCustomizationSpellByItem(customizationItemID)
if spellID then
if spellID == 320799 then
local SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS = {313589, 313590, 313591, 313592, 313593, 313594}
spellID = SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS[math.random(1, #SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS)]
end
self:SetSpell(spellID)
end
end
end
end
end
end
end
local function PlayerModel_SetUnit(self, unit, isCustomPosition, positionData)
if type(unit) ~= "string" then
error("Usage: SetUnit(\"unit\")", 2)
end
local objectType = self:GetObjectType()
if objectType == "PlayerModel" then
self:SetUnitRaw(unit)
return
end
if isCustomPosition then
if objectType == "TabardModel" then
self:SetPosition(0, 0, 0)
else
self:SetPosition(1, 1, 1)
end
end
self:SetUnitRaw(unit)
self.__hideCustomizations = nil
self.__unitGUID = UnitGUID(unit)
PlayerModel_UpdateModelEffects(self)
if isCustomPosition then
local unitSex = UnitSex(unit) or 2
local _, unitRace = UnitRace(unit)
local positionStorage = positionData or (objectType == "TabardModel" and TABARDMODEL_CAMERA_POSITION or DRESSUPMODEL_CAMERA_POSITION)
local data = positionStorage[string.format("%s%d", unitRace or "Human", unitSex)]
if data then
local positionX, positionY, positionZ, facing = unpack(data, 1, 4)
self.basePositionOverrideX = positionX
self.basePositionOverrideY = positionY
self.basePositionOverrideZ = positionZ
self.basePositionOverrideXasZ = true
self:SetPosition(positionX or 0, positionY or 0, positionZ or 0)
if facing then
self:SetFacing(math.rad(facing))
end
return
end
end
self.basePositionOverrideX = nil
self.basePositionOverrideY = nil
self.basePositionOverrideZ = nil
self.basePositionOverrideXasZ = nil
end
local function PlayerModel_RefreshUnit(self)
self:RefreshUnitRaw()
local objectType = self:GetObjectType()
if objectType == "DressUpModel" or objectType == "TabardModel" then
self.__hideCustomizations = nil
PlayerModel_UpdateModelEffects(self)
end
end
local function DressUpModel_Dress(self)
self:DressRaw()
self.__hideCustomizations = nil
PlayerModel_UpdateModelEffects(self)
end
local function DressUpModel_Undress(self)
self:UndressRaw()
self.__hideCustomizations = true
PlayerModel_UpdateModelEffects(self)
end
local function DressUpModel_TryOn(self, item)
if C_CustomizationsCollection.CanPreviewCustomizationInDressUp(item) then
local spellID, category, subcategory = C_CustomizationsCollection.GetCustomizationSpellByItem(item)
if spellID then
PlayerModel_UpdateModelEffects(self)
for index, activeSpellID in ipairs({self:GetAllSpells()}) do
local isCustomizationSpell, activeSpellCategory, activeSpellSubCategory = C_CustomizationsCollection.IsCustomizationSpell(activeSpellID)
if isCustomizationSpell and category == activeSpellCategory and subcategory == activeSpellSubCategory then
self:ResetSpell(activeSpellID)
break
end
end
if spellID == 320799 then
local SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS = {313589, 313590, 313591, 313592, 313593, 313594}
spellID = SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS[math.random(1, #SPELL_EFFECTS_GIFT_OF_MOST_PRECIOUS)]
end
self:SetSpell(spellID)
end
return
end
self:TryOnRaw(item)
PlayerModel_UpdateModelEffects(self)
end
local function Animation_Restart(self)
if self:IsPlaying() then
self:Stop()
end
self:Play()
end
FrameMeta.SetShown = ScriptRegion_SetShown
FrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
FrameMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
FrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
FrameMeta.SetParentArray = LayoutFrame_SetParentArray
FrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
FrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
FontStringMeta.SetShown = ScriptRegion_SetShown
FontStringMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
FontStringMeta.GetEffectiveScale = Region_GetEffectiveScale
FontStringMeta.SetParentArray = LayoutFrame_SetParentArray
FontStringMeta.GetScaledRect = ScriptRegion_GetScaledRect
FontStringMeta.SetDesaturated = FontString_SetDesaturated
FontStringMeta.SetRemainingTime = FontString_SetRemainingTime
TextureMeta.SetShown = ScriptRegion_SetShown
TextureMeta.GetScaledRect = ScriptRegion_GetScaledRect
TextureMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
TextureMeta.GetEffectiveScale = Region_GetEffectiveScale
TextureMeta.SetParentArray = LayoutFrame_SetParentArray
TextureMeta.SetSubTexCoord = Texture_SetSubTexCoord
TextureMeta.SetAtlas = Texture_SetAtlas
TextureMeta.SetPortrait = Texture_SetPortrait
ButtonMeta.SetShown = ScriptRegion_SetShown
ButtonMeta.GetScaledRect = ScriptRegion_GetScaledRect
ButtonMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
ButtonMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
ButtonMeta.SetParentArray = LayoutFrame_SetParentArray
ButtonMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
ButtonMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
ButtonMeta.SetEnabled = Button_SetEnabled
ButtonMeta.SetNormalAtlas = Button_SetNormalAtlas
ButtonMeta.SetPushedAtlas = Button_SetPushedAtlas
ButtonMeta.SetDisabledAtlas = Button_SetDisabledAtlas
ButtonMeta.SetHighlightAtlas = Button_SetHighlightAtlas
ButtonMeta.ClearClearNormalTexture = Button_ClearNormalTexture
ButtonMeta.ClearPushedTexture = Button_ClearPushedTexture
ButtonMeta.ClearDisabledTexture = Button_ClearDisabledTexture
ButtonMeta.ClearHighlightTexture = Button_ClearHighlightTexture
CheckButtonMeta.SetShown = ScriptRegion_SetShown
CheckButtonMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
CheckButtonMeta.GetScaledRect = ScriptRegion_GetScaledRect
CheckButtonMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
CheckButtonMeta.SetParentArray = LayoutFrame_SetParentArray
CheckButtonMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
CheckButtonMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
CheckButtonMeta.SetEnabled = Button_SetEnabled
CheckButtonMeta.SetNormalAtlas = Button_SetNormalAtlas
CheckButtonMeta.SetPushedAtlas = Button_SetPushedAtlas
CheckButtonMeta.SetDisabledAtlas = Button_SetDisabledAtlas
CheckButtonMeta.SetHighlightAtlas = Button_SetHighlightAtlas
CheckButtonMeta.SetCheckedAtlas = CheckButton_SetCheckedAtlas
CheckButtonMeta.SetDisabledCheckedAtlas = CheckButton_SetDisabledCheckedAtlas
CheckButtonMeta.ClearClearNormalTexture = Button_ClearNormalTexture
CheckButtonMeta.ClearPushedTexture = Button_ClearPushedTexture
CheckButtonMeta.ClearDisabledTexture = Button_ClearDisabledTexture
CheckButtonMeta.ClearHighlightTexture = Button_ClearHighlightTexture
CheckButtonMeta.ClearDisabledCheckedTexture = CheckButton_ClearCheckedTexture
CheckButtonMeta.ClearDisabledCheckedTexture = CheckButton_ClearDisabledCheckedTexture
EditBoxMeta.SetShown = ScriptRegion_SetShown
EditBoxMeta.GetScaledRect = ScriptRegion_GetScaledRect
EditBoxMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
EditBoxMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
EditBoxMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
EditBoxMeta.SetEnabled = Button_SetEnabled
EditBoxMeta.IsEnabled = EditBox_IsEnabled
EditBoxMeta.Enable = EditBox_Enabled
EditBoxMeta.Disable = EditBox_Disable
SimpleHTMLMeta.SetShown = ScriptRegion_SetShown
SimpleHTMLMeta.GetScaledRect = ScriptRegion_GetScaledRect
SimpleHTMLMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
SimpleHTMLMeta.SetParentArray = LayoutFrame_SetParentArray
SimpleHTMLMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
SimpleHTMLMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
SliderMeta.SetShown = ScriptRegion_SetShown
SliderMeta.GetScaledRect = ScriptRegion_GetScaledRect
SliderMeta.IsMouseOverEx = ScriptRegion_IsMouseOverEx
SliderMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
SliderMeta.SetParentArray = LayoutFrame_SetParentArray
SliderMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
SliderMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
SliderMeta.SetEnabled = Button_SetEnabled
StatusBarMeta.SetShown = ScriptRegion_SetShown
StatusBarMeta.GetScaledRect = ScriptRegion_GetScaledRect
StatusBarMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
StatusBarMeta.SetParentArray = LayoutFrame_SetParentArray
StatusBarMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
StatusBarMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
ScrollFrameMeta.SetShown = ScriptRegion_SetShown
ScrollFrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
ScrollFrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
ScrollFrameMeta.SetParentArray = LayoutFrame_SetParentArray
ScrollFrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
ScrollFrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
MessageFrameMeta.SetShown = ScriptRegion_SetShown
MessageFrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
MessageFrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
MessageFrameMeta.SetParentArray = LayoutFrame_SetParentArray
MessageFrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
MessageFrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
ScrollingMessageFrameMeta.SetShown = ScriptRegion_SetShown
ScrollingMessageFrameMeta.GetScaledRect = ScriptRegion_GetScaledRect
ScrollingMessageFrameMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
ScrollingMessageFrameMeta.SetParentArray = LayoutFrame_SetParentArray
ScrollingMessageFrameMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
ScrollingMessageFrameMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
ModelMeta.SetShown = ScriptRegion_SetShown
ModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
ModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
ModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
ModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
AnimationGroupMeta.Restart = Animation_Restart
AnimationAnimationMeta.Restart = Animation_Restart
AnimationAlphaMeta.Restart = Animation_Restart
AnimationRotationMeta.Restart = Animation_Restart
AnimationPathMeta.Restart = Animation_Restart
AnimationScaleMeta.Restart = Animation_Restart
AnimationTranslationMeta.Restart = Animation_Restart
if IsOnGlueScreen() then
ModelFFXMeta.SetShown = ScriptRegion_SetShown
ModelFFXMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
ModelFFXMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
else
GameTooltipMeta.GetScaledRect = ScriptRegion_GetScaledRect
GameTooltipMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
GameTooltipMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
PlayerModelMeta.SetShown = ScriptRegion_SetShown
PlayerModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
PlayerModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
PlayerModelMeta.SetParentArray = LayoutFrame_SetParentArray
PlayerModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
PlayerModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
PlayerModelMeta.SetUnitRaw = PlayerModelMeta.SetUnit
PlayerModelMeta.SetUnit = PlayerModel_SetUnit
PlayerModelMeta.RefreshUnitRaw = PlayerModelMeta.RefreshUnit
PlayerModelMeta.RefreshUnit = PlayerModel_RefreshUnit
DressUpModelMeta.SetShown = ScriptRegion_SetShown
DressUpModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
DressUpModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
DressUpModelMeta.SetParentArray = LayoutFrame_SetParentArray
DressUpModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
DressUpModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
DressUpModelMeta.SetUnitRaw = DressUpModelMeta.SetUnit
DressUpModelMeta.SetUnit = PlayerModel_SetUnit
DressUpModelMeta.RefreshUnitRaw = DressUpModelMeta.RefreshUnit
DressUpModelMeta.RefreshUnit = PlayerModel_RefreshUnit
DressUpModelMeta.TryOnRaw = DressUpModelMeta.TryOn
DressUpModelMeta.TryOn = DressUpModel_TryOn
DressUpModelMeta.DressRaw = DressUpModelMeta.Dress
DressUpModelMeta.Dress = DressUpModel_Dress
DressUpModelMeta.UndressRaw = DressUpModelMeta.Undress
DressUpModelMeta.Undress = DressUpModel_Undress
TabardModelMeta.SetShown = ScriptRegion_SetShown
TabardModelMeta.GetScaledRect = ScriptRegion_GetScaledRect
TabardModelMeta.ClearAndSetPoint = ScriptRegionResizing_ClearAndSetPoint
TabardModelMeta.SetParentArray = LayoutFrame_SetParentArray
TabardModelMeta.RegisterCustomEvent = Frame_RegisterCustomEvent
TabardModelMeta.UnregisterCustomEvent = Frame_UnregisterCustomEvent
TabardModelMeta.SetUnitRaw = TabardModelMeta.SetUnit
TabardModelMeta.SetUnit = PlayerModel_SetUnit
TabardModelMeta.RefreshUnitRaw = TabardModelMeta.RefreshUnit
TabardModelMeta.RefreshUnit = PlayerModel_RefreshUnit
TabardModelMeta.SetSpell = function() end
end
@@ -1,196 +0,0 @@
local function SetupTextFont(fontString, fontObject)
if fontString and fontObject then
fontString:SetFontObject(fontObject);
end
end
function SharedTooltip_OnLoad(self)
local style = nil;
local isEmbedded = false;
SharedTooltip_SetBackdropStyle(self, style, isEmbedded);
self:SetClampRectInsets(0, 0, 15, 0);
SetupTextFont(self.TextLeft1, self.textLeft1Font);
SetupTextFont(self.TextRight1, self.textRight1Font);
SetupTextFont(self.TextLeft2, self.textLeft2Font);
SetupTextFont(self.TextRight2, self.textRight2Font);
end
function SharedTooltip_OnHide(self)
self:SetPadding(0, 0, 0, 0);
end
local DEFAULT_TOOLTIP_OFFSET_X = -17;
local DEFAULT_TOOLTIP_OFFSET_Y = 70;
function SharedTooltip_SetDefaultAnchor(tooltip, parent)
tooltip:SetOwner(parent or GetAppropriateTopLevelParent(), "ANCHOR_NONE");
tooltip:SetPoint("BOTTOMRIGHT", GetAppropriateTopLevelParent(), "BOTTOMRIGHT", DEFAULT_TOOLTIP_OFFSET_X, DEFAULT_TOOLTIP_OFFSET_Y);
end
function SharedTooltip_ClearInsertedFrames(self)
if self.insertedFrames then
for i = 1, #self.insertedFrames do
self.insertedFrames[i]:Hide();
end
end
self.insertedFrames = nil;
end
function SharedTooltip_SetBackdropStyle(self, style, embedded)
if embedded or self.IsEmbedded then
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b, 0)
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b, 0);
else
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b, 1);
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b, 1);
end
if self.TopOverlay then
if style and style.overlayAtlasTop then
self.TopOverlay:SetAtlas(style.overlayAtlasTop, true);
self.TopOverlay:SetScale(style.overlayAtlasTopScale or 1.0);
self.TopOverlay:SetPoint("CENTER", self, "TOP", style.overlayAtlasTopXOffset or 0, style.overlayAtlasTopYOffset or 0);
self.TopOverlay:Show();
else
self.TopOverlay:Hide();
end
end
if self.BottomOverlay then
if style and style.overlayAtlasBottom then
self.BottomOverlay:SetAtlas(style.overlayAtlasBottom, true);
self.BottomOverlay:SetScale(style.overlayAtlasBottomScale or 1.0);
self.BottomOverlay:SetPoint("CENTER", self, "BOTTOM", style.overlayAtlasBottomXOffset or 0, style.overlayAtlasBottomYOffset or 0);
self.BottomOverlay:Show();
else
self.BottomOverlay:Hide();
end
end
if style and style.padding then
self:SetPadding(style.padding.right, style.padding.bottom, style.padding.left, style.padding.top);
end
end
function GameTooltip_AddBlankLinesToTooltip(tooltip, numLines)
if numLines ~= nil then
for i = 1, numLines do
tooltip:AddLine(" ");
end
end
end
function GameTooltip_AddBlankLineToTooltip(tooltip)
GameTooltip_AddBlankLinesToTooltip(tooltip, 1);
end
function GameTooltip_SetTitle(tooltip, text, overrideColor, wrap)
local titleColor = overrideColor or HIGHLIGHT_FONT_COLOR;
local r, g, b, a = titleColor:GetRGBA();
if wrap == nil then
wrap = 1;
end
tooltip:SetText(text, r, g, b, a, wrap);
end
function GameTooltip_ShowDisabledTooltip(tooltip, owner, text, tooltipAnchor)
tooltip:SetOwner(owner, tooltipAnchor);
local wrap = 1;
GameTooltip_SetTitle(tooltip, text, RED_FONT_COLOR, wrap);
tooltip:Show();
end
function GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddHighlightLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddInstructionLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, GREEN_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddErrorLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, RED_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddDisabledLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, DISABLED_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset)
local r, g, b = color:GetRGB();
if wrap == nil then
wrap = 1;
end
tooltip:AddLine(text, r, g, b, wrap, leftOffset);
end
function GameTooltip_AddColoredDoubleLine(tooltip, leftText, rightText, leftColor, rightColor, wrap)
local leftR, leftG, leftB = leftColor:GetRGB();
local rightR, rightG, rightB = rightColor:GetRGB();
if wrap == nil then
wrap = 1;
end
tooltip:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB, wrap);
end
function GameTooltip_InsertFrame(tooltipFrame, frame, verticalPadding)
verticalPadding = verticalPadding or 0;
local textSpacing = 2;
local textHeight = Round(_G[tooltipFrame:GetName().."TextLeft2"]:GetLineHeight());
local neededHeight = Round(frame:GetHeight() + verticalPadding);
local numLinesNeeded = math.ceil(neededHeight / (textHeight + textSpacing));
local currentLine = tooltipFrame:NumLines();
GameTooltip_AddBlankLinesToTooltip(tooltipFrame, numLinesNeeded);
frame:SetParent(tooltipFrame);
frame:ClearAllPoints();
frame:SetPoint("TOPLEFT", tooltipFrame:GetName().."TextLeft"..(currentLine + 1), "TOPLEFT", 0, -verticalPadding);
if not tooltipFrame.insertedFrames then
tooltipFrame.insertedFrames = { };
end
local frameWidth = frame:GetWidth();
if tooltipFrame:GetMinimumWidth() < frameWidth then
tooltipFrame:SetMinimumWidth(frameWidth);
end
frame:Show();
tinsert(tooltipFrame.insertedFrames, frame);
-- return space taken so inserted frame can resize if needed
return (numLinesNeeded * textHeight) + (numLinesNeeded - 1) * textSpacing;
end
DisabledTooltipButtonMixin = {};
function DisabledTooltipButtonMixin:OnEnter()
if not self:IsEnabled() then
local disabledTooltip, disabledTooltipAnchor = self:GetDisabledTooltip();
if disabledTooltip ~= nil then
GameTooltip_ShowDisabledTooltip(GetAppropriateTooltip(), self, disabledTooltip, disabledTooltipAnchor);
end
end
end
function DisabledTooltipButtonMixin:OnLeave()
local tooltip = GetAppropriateTooltip();
tooltip:Hide();
end
function DisabledTooltipButtonMixin:SetDisabledTooltip(disabledTooltip, disabledTooltipAnchor)
self.disabledTooltip = disabledTooltip;
self.disabledTooltipAnchor = disabledTooltipAnchor;
end
function DisabledTooltipButtonMixin:GetDisabledTooltip()
return self.disabledTooltip, self.disabledTooltipAnchor;
end
function DisabledTooltipButtonMixin:SetDisabledState(disabled, disabledTooltip, disabledTooltipAnchor)
self:SetEnabled(not disabled);
self:SetDisabledTooltip(disabledTooltip, disabledTooltipAnchor);
end
@@ -1,191 +0,0 @@
<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="SharedTooltipTemplates.lua"/>
<FontString name="TooltipTextLeftTemplate" inherits="GameTooltipText" justifyH="LEFT" hidden="true" virtual="true"/>
<FontString name="TooltipTextRightTemplate" inherits="GameTooltipText" justifyH="RIGHT" hidden="true" virtual="true"/>
<Texture name="TooltipTextureTemplate" hidden="true" virtual="true">
<Size x="12" y="12"/>
</Texture>
<GameTooltip name="SharedTooltipTemplate" clampedToScreen="true" frameStrata="TOOLTIP" hidden="true" virtual="true">
<Attributes>
<Attribute name="textLeft1Font" value="GameTooltipHeaderText" type="string"/>
<Attribute name="textRight1Font" value="GameTooltipHeaderText" type="string"/>
<Attribute name="textLeft2Font" value="GameTooltipText" type="string"/>
<Attribute name="textRight2Font" value="GameTooltipText" type="string"/>
<Attribute name="layoutType" value="TooltipDefaultLayout" type="string"/>
</Attributes>
<Backdrop bgFile="Interface\Tooltips\UI-Tooltip-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<EdgeSize>
<AbsValue val="16"/>
</EdgeSize>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<BackgroundInsets>
<AbsInset left="5" right="5" top="5" bottom="5"/>
</BackgroundInsets>
</Backdrop>
<Layers>
<Layer level="OVERLAY">
<Texture parentKey="TopOverlay" hidden="true">
<Anchors>
<Anchor point="CENTER" relativePoint="TOP" />
</Anchors>
</Texture>
<Texture parentKey="BottomOverlay" hidden="true">
<Anchors>
<Anchor point="CENTER" relativePoint="BOTTOM" />
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentTextLeft1" parentKey="TextLeft1" inherits="TooltipTextLeftTemplate">
<Anchors>
<Anchor point="TOPLEFT" x="10" y="-10"/>
</Anchors>
</FontString>
<FontString name="$parentTextRight1" parentKey="TextRight1" inherits="TooltipTextRightTemplate" justifyH="LEFT">
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentTextLeft1" relativePoint="LEFT" x="40" y="0"/>
</Anchors>
</FontString>
<FontString name="$parentTextLeft2" parentKey="TextLeft2" inherits="TooltipTextLeftTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentTextLeft1" relativePoint="BOTTOMLEFT" x="0" y="-2"/>
</Anchors>
</FontString>
<FontString name="$parentTextRight2" parentKey="TextRight2" inherits="TooltipTextRightTemplate">
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentTextLeft2" relativePoint="LEFT" x="40" y="0"/>
</Anchors>
</FontString>
<Texture name="$parentTexture1" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture2" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture3" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture4" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture5" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture6" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture7" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture8" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture9" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture10" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture11" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture12" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture13" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture14" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture15" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture16" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture17" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture18" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture19" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture20" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture21" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture22" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture23" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture24" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture25" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture26" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture27" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture28" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture29" inherits="TooltipTextureTemplate"/>
<Texture name="$parentTexture30" inherits="TooltipTextureTemplate"/>
</Layer>
</Layers>
<Scripts>
<OnLoad function="SharedTooltip_OnLoad"/>
<OnHide function="SharedTooltip_OnHide"/>
<OnTooltipSetDefaultAnchor function="SharedTooltip_SetDefaultAnchor"/>
<OnTooltipCleared function="SharedTooltip_ClearInsertedFrames"/>
</Scripts>
</GameTooltip>
<GameTooltip name="SharedNoHeaderTooltipTemplate" inherits="SharedTooltipTemplate" virtual="true">
<Attributes>
<Attribute name="textLeft1Font" value="GameTooltipText" type="string"/>
<Attribute name="textRight1Font" value="GameTooltipText" type="string"/>
</Attributes>
</GameTooltip>
<Frame name="TooltipBorderedFrameTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="BorderTopLeft" name="$parentBorderTopLeft" file="Interface\Tooltips\UI-Tooltip-TL">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="TOPLEFT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderTopRight" name="$parentBorderTopRight" file="Interface\Tooltips\UI-Tooltip-TR">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="TOPRIGHT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderBottomRight" name="$parentBorderBottomRight" file="Interface\Tooltips\UI-Tooltip-BR">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderBottomLeft" name="$parentBorderBottomLeft" file="Interface\Tooltips\UI-Tooltip-BL">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="BOTTOMLEFT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderTop" file="Interface\Tooltips\UI-Tooltip-T">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentBorderTopLeft" relativePoint="TOPRIGHT"/>
<Anchor point="TOPRIGHT" relativeTo="$parentBorderTopRight" relativePoint="TOPLEFT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderRight" file="Interface\Tooltips\UI-Tooltip-R">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="TOPRIGHT" relativeTo="$parentBorderTopRight" relativePoint="BOTTOMRIGHT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorderBottomRight" relativePoint="TOPRIGHT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderBottom" file="Interface\Tooltips\UI-Tooltip-B">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="BOTTOMLEFT" relativeTo="$parentBorderBottomLeft" relativePoint="BOTTOMRIGHT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorderBottomRight" relativePoint="BOTTOMLEFT"/>
</Anchors>
</Texture>
<Texture parentKey="BorderLeft" file="Interface\Tooltips\UI-Tooltip-L">
<Size x="8" y="8"/>
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentBorderTopLeft" relativePoint="BOTTOMLEFT"/>
<Anchor point="BOTTOMLEFT" relativeTo="$parentBorderBottomLeft" relativePoint="TOPLEFT"/>
</Anchors>
</Texture>
<Texture parentKey="Background">
<Anchors>
<Anchor point="TOPLEFT" relativeTo="$parentBorderTopLeft" relativePoint="BOTTOMRIGHT"/>
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBorderBottomRight" relativePoint="TOPLEFT"/>
</Anchors>
<Color r="0" g="0" b="0" a="0.8"/>
</Texture>
</Layer>
</Layers>
</Frame>
<Button name="DisabledTooltipButtonTemplate" motionScriptsWhileDisabled="true" virtual="true">
<Scripts>
<OnLoad>
Mixin(self, DisabledTooltipButtonMixin)
</OnLoad>
<OnEnter>
self:OnEnter()
</OnEnter>
<OnLeave>
self:OnLeave()
</OnLeave>
</Scripts>
</Button>
</Ui>
@@ -1,499 +0,0 @@
local ColumnWidthConstraints = {
Fill = 1, -- Width will be distributed by available space.
Fixed = 2, -- Width is specified when initializing the column.
};
-- Any row or cell is expected to initialize itself in terms of the row data. The dataIndex is provided
-- in case the derived mixin needs to make additional CAPI calls involving it's relative index. The row
-- data may also be needed for a tooltip, so it will be assigned to the row and cells on update.
TableBuilderElementMixin = {};
--Derive
function TableBuilderElementMixin:Init(...)
end
--Derive
function TableBuilderElementMixin:Populate(rowData, dataIndex)
end
TableBuilderCellMixin = CreateFromMixins(TableBuilderElementMixin);
--Derive
function TableBuilderCellMixin:OnLineEnter()
end
--Derive
function TableBuilderCellMixin:OnLineLeave()
end
TableBuilderRowMixin = CreateFromMixins(TableBuilderElementMixin);
--Derive
function TableBuilderRowMixin:OnLineEnter()
end
--Derive
function TableBuilderRowMixin:OnLineLeave()
end
function TableBuilderRowMixin:OnEnter()
self:OnLineEnter();
for i, cell in ipairs(self.cells) do
cell:OnLineEnter();
end
end
function TableBuilderRowMixin:OnLeave()
self:OnLineLeave();
for i, cell in ipairs(self.cells) do
cell:OnLineLeave();
end
end
-- Defines an entire column within the table builder, by default a column's sizing constraints are set to fill.
TableBuilderColumnMixin = {};
function TableBuilderColumnMixin:Init(table)
self.cells = {};
self.table = table;
local fillCoefficient = 1.0;
local padding = 0;
self:SetFillConstraints(fillCoefficient, padding);
end
-- Constructs the header frame with an optional initializer.
function TableBuilderColumnMixin:ConstructHeader(templateType, template, ...)
local frame = self.table:ConstructHeader(templateType, template);
self.headerFrame = frame;
if frame.Init then
frame:Init(...);
end
frame:Show();
end
-- Constructs cells corresponding to each row with an optional initializer.
function TableBuilderColumnMixin:ConstructCells(templateType, template, ...)
local cells = self.table:ConstructCells(templateType, template);
self.cells = cells;
for k, cell in pairs(cells) do
if cell.Init then
cell:Init(...);
end
cell:Show();
end
end
function TableBuilderColumnMixin:GetCellByRowIndex(rowIndex)
return self.cells[rowIndex];
end
function TableBuilderColumnMixin:GetFillCoefficient()
return self.fillCoefficient;
end
function TableBuilderColumnMixin:SetFillCoefficient(fillCoefficient)
self.fillCoefficient = fillCoefficient;
end
function TableBuilderColumnMixin:GetPadding()
return self.padding;
end
function TableBuilderColumnMixin:SetPadding(padding)
self.padding = padding;
end
function TableBuilderColumnMixin:GetCellPadding()
return self.leftCellPadding or 0, self.rightCellPadding or 0;
end
function TableBuilderColumnMixin:SetCellPadding(leftCellPadding, rightCellPadding)
self.leftCellPadding = leftCellPadding;
self.rightCellPadding = rightCellPadding;
end
function TableBuilderColumnMixin:GetHeaderFrame()
return self.headerFrame;
end
function TableBuilderColumnMixin:SetHeaderFrame(headerFrame)
self.headerFrame = headerFrame;
end
function TableBuilderColumnMixin:GetWidthConstraints()
return self.widthConstraints;
end
function TableBuilderColumnMixin:GetFixedWidth()
return self.fixedWidth;
end
-- A header frame for the column is expected to be constructed or assigned prior to calling this.
-- See ConstructHeader() or SetHeaderFrame().
function TableBuilderColumnMixin:ConstrainToHeader(padding)
local header = self:GetHeaderFrame();
assert(header, "ConstrainToHeader() called with a nil header frame. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
self:SetFixedConstraints(header:GetWidth(), padding or 0);
end
function TableBuilderColumnMixin:SetFixedConstraints(fixedWidth, padding)
self.widthConstraints = ColumnWidthConstraints.Fixed;
self.fixedWidth = fixedWidth;
self:SetFillCoefficient(0);
self:SetPadding(padding or 0);
end
function TableBuilderColumnMixin:SetFillConstraints(fillCoefficient, padding)
self.widthConstraints = ColumnWidthConstraints.Fill;
self.fixedWidth = 0;
self:SetFillCoefficient(fillCoefficient);
self:SetPadding(padding or 0);
end
function TableBuilderColumnMixin:SetCalculatedWidth(calculatedWidth)
self.calculatedWidth = calculatedWidth;
end
function TableBuilderColumnMixin:GetCalculatedWidth()
return self.calculatedWidth;
end
function TableBuilderColumnMixin:GetCellWidth()
local leftCellPadding, rightCellPadding = self:GetCellPadding();
return (self.calculatedWidth - leftCellPadding) - rightCellPadding;
end
function TableBuilderColumnMixin:GetFullWidth()
return self:GetCalculatedWidth() + self:GetPadding();
end
function TableBuilderColumnMixin:SetDisplayUnderPreviousHeader(displayUnderPreviousHeader)
self.displayUnderPreviousHeader = displayUnderPreviousHeader;
end
function TableBuilderColumnMixin:GetDisplayUnderPreviousHeader()
return self.displayUnderPreviousHeader;
end
-- Constructs a table of frames within an existing set of row frames. These row frames could originate from
-- a hybrid scroll frame or statically fixed set. To populate the table, assign a data provider (CAPI or lua function)
-- that can retrieve an object by index (number).
TableBuilderMixin = {};
function TableBuilderMixin:Init(rows)
self.columns = {};
self.leftMargin = 0;
self.rightMargin = 0;
self.columnHeaderOverlap = 0;
self.tableWidth = 0;
self.headerPoolCollection = CreateFramePoolCollection();
self:SetRows(rows);
end
function TableBuilderMixin:GetDataProvider()
return self.dataProvider;
end
function TableBuilderMixin:SetDataProvider(dataProvider)
self.dataProvider = dataProvider;
end
function TableBuilderMixin:GetDataProviderData(dataIndex)
local dataProvider = self:GetDataProvider();
return dataProvider and dataProvider(dataIndex) or nil;
end
-- Controls the margins of the left-most and right-most columns within the table.
function TableBuilderMixin:SetTableMargins(leftMargin, rightMargin)
rightMargin = rightMargin or leftMargin; -- Use leftMargin as the default for both.
self.leftMargin = leftMargin;
self.rightMargin = rightMargin;
end
-- Column headers overlap to make a consistent display.
function TableBuilderMixin:SetColumnHeaderOverlap(columnHeaderOverlap)
self.columnHeaderOverlap = columnHeaderOverlap;
end
-- Can be used to set the table width, particularly if no header frames are involved.
function TableBuilderMixin:SetTableWidth(tableWidth)
self.tableWidth = tableWidth;
end
function TableBuilderMixin:GetTableWidth()
return self.tableWidth;
end
function TableBuilderMixin:GetTableMargins()
return self.leftMargin, self.rightMargin;
end
function TableBuilderMixin:GetColumnHeaderOverlap()
return self.columnHeaderOverlap;
end
function TableBuilderMixin:GetColumns()
return self.columns;
end
function TableBuilderMixin:GetHeaderContainer()
return self.headerContainer;
end
function TableBuilderMixin:SetHeaderContainer(headerContainer)
assert(headerContainer, "SetHeaderContainer() with a nil header container. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
self.headerContainer = headerContainer;
self:SetTableWidth(headerContainer:GetWidth());
end
function TableBuilderMixin:ReleaseRowPools()
local rows = self.rows;
if rows then
for k, row in pairs(rows) do
local poolCollection = row.poolCollection;
if poolCollection then
poolCollection:ReleaseAll();
end
end
end
end
function TableBuilderMixin:SetRows(rows)
-- Release any previous rows, though I can't imagine a case where the rows
-- are being exchanged.
self:ReleaseRowPools();
self.rows = rows;
for k, row in pairs(rows) do
if not row.poolCollection then
row.poolCollection = CreateFramePoolCollection();
end
end
end
function TableBuilderMixin:GetHeaderPoolCollection()
return self.headerPoolCollection;
end
function TableBuilderMixin:GetRows()
return self.rows;
end
function TableBuilderMixin:ConstructHeader(templateType, template)
local headerContainer = self:GetHeaderContainer();
assert(headerContainer ~= nil, "A header container must be set with TableBuilderMixin:SetHeaderContainer before adding column headers.")
local headerPoolCollection = self:GetHeaderPoolCollection();
local pool = headerPoolCollection:GetOrCreatePool(templateType, headerContainer, template);
return pool:Acquire(template);
end
function TableBuilderMixin:ConstructCells(templateType, template)
local cells = {};
for k, row in pairs(self:GetRows()) do
local pool = row.poolCollection:GetOrCreatePool(templateType, row, template);
local cell = pool:Acquire(template);
tinsert(cells, cell);
end
return cells;
end
function TableBuilderMixin:Arrange()
local columns = self:GetColumns();
if columns and #columns > 0 then
self:CalculateColumnSpacing();
self:ArrangeHeaders();
self:ArrangeCells();
end
end
function TableBuilderMixin:Reset()
self.columns = {};
self:GetHeaderPoolCollection():ReleaseAll();
self:ReleaseRowPools();
end
function TableBuilderMixin:Populate(offset, count)
local dataProvider = self:GetDataProvider();
local columns = self:GetColumns();
for rowIndex = 1, count do
local dataIndex = rowIndex + offset;
local rowData = dataProvider(dataIndex);
if not rowData then
break;
end
local row = self:GetRowByIndex(rowIndex);
if row then
-- Data is assigned to the rows and elements so they can
-- access it later in tooltips.
row.rowData = rowData;
if row.Populate then
row:Populate(rowData, dataIndex);
end
for columnIndex, p in ipairs(columns) do
local cell = self:GetCellByIndex(rowIndex, columnIndex);
if cell.Populate then
cell.rowData = rowData;
cell:Populate(rowData, dataIndex);
end
end
end
end
end
function TableBuilderMixin:GetCellByIndex(rowIndex, index)
local row = self:GetRowByIndex(rowIndex);
return row.cells[index];
end
function TableBuilderMixin:GetRowByIndex(rowIndex)
return self.rows[rowIndex];
end
function TableBuilderMixin:AddColumn()
local column = CreateAndInitFromMixin(TableBuilderColumnMixin, self);
tinsert(self.columns, column);
return column;
end
function TableBuilderMixin:CalculateColumnSpacing()
-- The arrangement of frames is daisy-chained left to right. The margin on the left side
-- is created by adding the margin to it's anchor offset, and the margin on the right side
-- is created by subtracting space from the remaining fill space.
local columns = self:GetColumns();
local paddingTotal = 0;
local fillCoefficientTotal = 0;
local fixedWidthTotal = 0;
for columnIndex, column in ipairs(columns) do
if column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
fillCoefficientTotal = fillCoefficientTotal + column:GetFillCoefficient();
else
fixedWidthTotal = fixedWidthTotal + column:GetFixedWidth();
end
paddingTotal = paddingTotal + column:GetPadding();
end
local tableWidth = self:GetTableWidth();
local leftMargin, rightMargin = self:GetTableMargins();
local fillWidthTotal = tableWidth - paddingTotal - (leftMargin + rightMargin) - fixedWidthTotal;
for k, column in pairs(columns) do
if fillCoefficientTotal > 0 and column:GetWidthConstraints() == ColumnWidthConstraints.Fill then
local fillRatio = column:GetFillCoefficient() / fillCoefficientTotal;
local width = fillRatio * fillWidthTotal;
column:SetCalculatedWidth(width);
else
local width = column:GetFixedWidth();
column:SetCalculatedWidth(width);
end
end
end
function TableBuilderMixin:ArrangeHorizontally(frame, relativeTo, width, pointTop, pointRelativeTop, pointBottom, pointRelativeBottom, xOffset)
frame:SetPoint(pointTop, relativeTo, pointRelativeTop, xOffset, 0);
frame:SetPoint(pointBottom, relativeTo, pointRelativeBottom, xOffset, 0);
frame:SetWidth(width);
end
function TableBuilderMixin:ArrangeHeaders()
local headerOverlap = self:GetColumnHeaderOverlap();
local leftMargin, rightMargin = self:GetTableMargins();
local columns = self:GetColumns();
-- Any trailing columns without headers should add to the width of the last column with a header.
local numColumns = #columns;
local lastActiveHeaderIndex = numColumns;
local trailingWidth = 0;
for i, column in ipairs(columns) do
if column:GetHeaderFrame() then
trailingWidth = 0;
lastActiveHeaderIndex = i;
else
trailingWidth = trailingWidth + column:GetFullWidth();
end
end
local previousHeader = nil;
local accumulatedWidth = 0;
local columnIndex = 1;
while columnIndex <= numColumns do
local column = columns[columnIndex];
accumulatedWidth = accumulatedWidth + column:GetCalculatedWidth();
local isLastIndex = columnIndex == lastActiveHeaderIndex;
local header = column:GetHeaderFrame();
if header then
if isLastIndex then
accumulatedWidth = accumulatedWidth + trailingWidth;
else
for j = columnIndex + 1, #columns do
local nextColumn = columns[j];
if nextColumn:GetDisplayUnderPreviousHeader() then
columnIndex = columnIndex + 1;
accumulatedWidth = accumulatedWidth + nextColumn:GetFullWidth();
else
break;
end
end
end
if previousHeader == nil then
self:ArrangeHorizontally(header, header:GetParent(), accumulatedWidth, "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin);
else
self:ArrangeHorizontally(header, previousHeader, accumulatedWidth + headerOverlap, "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() - headerOverlap);
end
accumulatedWidth = 0;
previousHeader = header;
end
if isLastIndex then
break;
end
columnIndex = columnIndex + 1;
end
end
function TableBuilderMixin:ArrangeCells()
local columns = self:GetColumns();
local leftMargin, rightMargin = self:GetTableMargins();
for rowIndex, row in ipairs(self:GetRows()) do
local cells = {};
local height = row:GetHeight();
local column = columns[1];
local cell = column:GetCellByRowIndex(rowIndex);
tinsert(cells, cell);
cell:SetHeight(height);
local leftCellPadding, rightCellPadding = column:GetCellPadding();
self:ArrangeHorizontally(cell, row, column:GetCellWidth(), "TOPLEFT", "TOPLEFT", "BOTTOMLEFT", "BOTTOMLEFT", leftMargin + leftCellPadding);
local previousCell = cell;
local previousRightCellPadding = rightCellPadding;
for columnIndex = 2, #columns do
column = columns[columnIndex];
cell = column:GetCellByRowIndex(rowIndex);
tinsert(cells, cell);
cell:SetHeight(height);
leftCellPadding, rightCellPadding = column:GetCellPadding();
self:ArrangeHorizontally(cell, previousCell, column:GetCellWidth(), "TOPLEFT", "TOPRIGHT", "BOTTOMLEFT", "BOTTOMRIGHT", column:GetPadding() + leftCellPadding + previousRightCellPadding);
previousCell = cell;
previousRightCellPadding = rightCellPadding;
end
row.cells = cells;
end
end
-- ... are additional mixins
function CreateTableBuilder(rows, ...)
local tableBuilder = CreateAndInitFromMixin(TableBuilderMixin, rows);
Mixin(tableBuilder, ...);
return tableBuilder;
end
@@ -1,697 +0,0 @@
local tRemove = table.remove;
local tInsert = table.insert;
local tWipe = table.wipe;
TableUtil = {};
TableUtil.Constants =
{
AssociativePriorityTable = true,
ArraylikePriorityTable = false,
IsIndexTable = true,
};
function ipairs_reverse(table)
local function Enumerator(table, index)
index = index - 1;
local value = table[index];
if value ~= nil then
return index, value;
end
end
return Enumerator, table, #table + 1;
end
function CreateTableEnumerator(tbl, minIndex, maxIndex)
minIndex = minIndex and (minIndex - 1) or 0;
maxIndex = maxIndex or math.huge;
local function Enumerator(tbl, index)
index = index + 1;
if index <= maxIndex then
local value = tbl[index];
if value ~= nil then
return index, value;
end
end
end
return Enumerator, tbl, minIndex;
end
function CreateTableReverseEnumerator(tbl, minIndex, maxIndex)
minIndex = minIndex or 1;
maxIndex = (maxIndex or #tbl) + 1;
local function Enumerator(tbl, index)
index = index - 1;
if index >= minIndex then
local value = tbl[index];
if value ~= nil then
return index, value;
end
end
end
return Enumerator, tbl, maxIndex;
end
function tCount(t)
if type(t) ~= "table" then
return 0
end
local size = 0
for _ in pairs(t) do
size = size + 1
end
return size
end
function tDeleteItem(tbl, item)
local size = #tbl;
local index = size;
while index > 0 do
if item == tbl[index] then
tRemove(tbl, index);
end
index = index - 1;
end
return size - #tbl;
end
function tIndexOf(tbl, item)
for i, v in ipairs(tbl) do
if item == v then
return i;
end
end
end
function tContains(tbl, item)
local index = 1;
while tbl[index] do
if ( item == tbl[index] ) then
return 1;
end
index = index + 1;
end
return nil;
end
function tContainsValue(tbl, item)
for k, v in pairs(tbl) do
if item == v then
return true;
end
end
return false;
end
function TableUtil.ContainsAllKeys(lhsTable, rhsTable)
for key, _ in pairs(lhsTable) do
if rhsTable[key] == nil then
return false;
end
end
-- Check for any keys that are in rhsTable and not lhsTable.
for key, _ in pairs(rhsTable) do
if lhsTable[key] == nil then
return false;
end
end
return true;
end
function TableUtil.CompareValuesAsKeys(lhsTable, rhsTable, valueToKeyOp)
local lhsKeys = CopyTransformedValuesAsKeys(lhsTable, valueToKeyOp);
local rhsKeys = CopyTransformedValuesAsKeys(rhsTable, valueToKeyOp);
return TableUtil.ContainsAllKeys(lhsKeys, rhsKeys)
end
-- This is a deep compare on the values of the table (based on depth) but not a deep comparison
-- of the keys, as this would be an expensive check and won't be necessary in most cases.
function tCompare(lhsTable, rhsTable, depth)
depth = depth or 1;
for key, value in pairs(lhsTable) do
if type(value) == "table" then
local rhsValue = rhsTable[key];
if type(rhsValue) ~= "table" then
return false;
end
if depth > 1 then
if not tCompare(value, rhsValue, depth - 1) then
return false;
end
end
elseif value ~= rhsTable[key] then
return false;
end
end
-- Check for any keys that are in rhsTable and not lhsTable.
for key, value in pairs(rhsTable) do
if lhsTable[key] == nil then
return false;
end
end
return true;
end
function tInvert(tbl)
local inverted = {};
for k, v in pairs(tbl) do
inverted[v] = k;
end
return inverted;
end
function TableUtil.TrySet(tbl, key)
if not tbl[key] then
tbl[key] = true;
return true;
end
return false;
end
function TableUtil.CopyUnique(tbl, isIndexTable)
local found = {};
local function FilterPredicate(value)
return TableUtil.TrySet(found, value);
end
return tFilter(tbl, FilterPredicate, isIndexTable);
end
function TableUtil.CopyUniqueByPredicate(tbl, isIndexTable, unaryPredicate)
local found = {};
local function FilterPredicate(value)
return TableUtil.TrySet(found, unaryPredicate(value));
end
return tFilter(tbl, FilterPredicate, isIndexTable);
end
function tFilter(tbl, pred, isIndexTable)
local out = {};
if (isIndexTable) then
local currentIndex = 1;
for i, v in ipairs(tbl) do
if (pred(v)) then
out[currentIndex] = v;
currentIndex = currentIndex + 1;
end
end
else
for k, v in pairs(tbl) do
if (pred(v)) then
out[k] = v;
end
end
end
return out;
end
function tAppendAll(table, addedArray)
for i, element in ipairs(addedArray) do
tinsert(table, element);
end
end
function tInsertUnique(tbl, item)
if not tContains(tbl, item) then
table.insert(tbl, item);
return #tbl;
end
return nil;
end
function tUnorderedRemove(tbl, index)
if index ~= #tbl then
tbl[index] = tbl[#tbl];
end
tRemove(tbl);
end
function CopyTable(settings, shallow)
local copy = {};
for k, v in pairs(settings) do
if type(v) == "table" and not shallow then
copy[k] = CopyTable(v);
else
copy[k] = v;
end
end
return copy;
end
function MergeTable(destination, source)
for k, v in pairs(source) do
destination[k] = v;
end
end
-- Useful if there are external references to a table but we want to set
-- that table's key-value pairs to be exactly the same as another table's key-value pairs.
function SetTablePairsToTable(destination, source)
tWipe(destination);
MergeTable(destination, source);
end
function Accumulate(tbl)
local count = 0;
for k, v in pairs(tbl) do
count = count + v;
end
return count;
end
function AccumulateOp(tbl, op)
local count = 0;
for k, v in pairs(tbl) do
count = count + op(v);
end
return count;
end
function TableUtil.Execute(tbl, op)
for k, v in pairs(tbl) do
op(v);
end
end
function TableUtil.ExecuteUntil(tbl, op)
for k, v in pairs(tbl) do
local operationResult = op(v);
if operationResult then
return operationResult;
end
end
return nil;
end
function TableUtil.Transform(tbl, op)
local result = {};
for k, v in pairs(tbl) do
table.insert(result, op(v));
end
return result;
end
function ContainsIf(tbl, pred)
for k, v in pairs(tbl) do
if (pred(v)) then
return true;
end
end
return false;
end
function FindInTableIf(tbl, pred)
for k, v in pairs(tbl) do
if (pred(v)) then
return k, v;
end
end
return nil;
end
function FindValueInTableIf(tbl, pred)
local _, value = FindInTableIf(tbl, pred);
return value;
end
local function FindSortedIndexImplementation(tbl, searchComparison, startIndex, rangeStart, rangeEnd)
local comparisonResult = searchComparison(tbl[startIndex]);
if comparisonResult == 0 then
return startIndex;
end
if comparisonResult > 0 then
if startIndex >= rangeEnd then
return startIndex + 1;
end
rangeStart = startIndex + 1;
return FindSortedIndexImplementation(tbl, searchComparison, startIndex + math.ceil((rangeEnd - startIndex) / 2), rangeStart, rangeEnd);
end
-- comparisonResult < 0
if startIndex <= rangeStart then
return startIndex;
end
rangeEnd = startIndex - 1;
return FindSortedIndexImplementation(tbl, searchComparison, math.floor(startIndex / 2), rangeStart, rangeEnd);
end
function FindSortedIndex(tbl, searchComparison)
local numTable = #tbl;
local startingIndex = math.ceil(numTable / 2);
if startingIndex == 0 then
return 1;
end
return FindSortedIndexImplementation(tbl, searchComparison, startingIndex, 1, numTable);
end
function TableIsEmpty(tbl)
return next(tbl) == nil;
end
function TableHasAnyEntries(tbl)
return next(tbl) ~= nil;
end
function CopyValuesAsKeys(tbl)
local output = {};
for k, v in ipairs(tbl) do
output[v] = v;
end
return output;
end
function CopyTransformedValuesAsKeys(tbl, transformOp)
local output = {};
for _, v in ipairs(tbl) do
output[transformOp(v)] = v;
end
return output;
end
-- Addresses the problem where nil values within a varargs list are not preserved when constructing
-- a table, resulting a table with a smaller size than expected. Should be paired with a call to
-- SafeUnpack when unpacking the table.
function SafePack(...)
local tbl = { ... };
tbl.n = select("#", ...);
return tbl;
end
-- Upacks a table that was constructed using SafePack.
function SafeUnpack(tbl, startIndex)
return unpack(tbl, startIndex or 1, tbl.n);
end
-- Returns the length of a table, accounting for the possibility of a table constructed using SafePack.
function SafeLength(tbl)
if not tbl then
return 0;
end
local operatorCount = #tbl;
local safePackCount = tbl.n;
if safePackCount and operatorCount ~= safePackCount then
return safePackCount;
end
return operatorCount;
end
function GetOrCreateTableEntry(table, key, defaultValue)
local currentValue = table[key];
local isNewValue = (currentValue == nil);
if isNewValue then
if defaultValue ~= nil then
currentValue = defaultValue;
else
currentValue = {};
end
table[key] = currentValue;
end
return currentValue, isNewValue;
end
function GetOrCreateTableEntryByCallback(table, key, callback)
local currentValue = table[key];
local isNewValue = (currentValue == nil);
if isNewValue then
currentValue = callback(key);
table[key] = currentValue;
end
return currentValue, isNewValue;
end
function GetRandomArrayEntry(array)
return array[math.random(1, #array)];
end
function GetRandomTableValue(tbl)
local value;
local n = 0;
for k, v in pairs(tbl) do
n = n + 1;
local r = math.random();
if r <= (1 / n) then
value = v;
end
end
return value;
end
function GetKeysArray(tbl)
local keysArray = {};
for key in pairs(tbl) do
tInsert(keysArray, key);
end
return keysArray;
end
function GetValuesArray(tbl)
local valuesArray = {};
for key, value in pairs(tbl) do
tInsert(valuesArray, value);
end
return valuesArray;
end
function GetPairsArray(tbl)
local pairsArray = {};
for key, value in pairs(tbl) do
tInsert(pairsArray, { key = key, value = value, });
end
return pairsArray;
end
function SwapTableEntries(lhsTable, rhsTable, key)
local lhsValue = lhsTable[key];
lhsTable[key] = rhsTable[key];
rhsTable[key] = lhsValue;
end
function GetKeysArraySortedByValue(tbl)
local keysArray = GetKeysArray(tbl);
table.sort(keysArray, function(a, b)
return tbl[a] < tbl[b];
end);
return keysArray;
end
function TableUtil.OperateOnKeys(tbl, operation)
for key, value in pairs(tbl) do
operation(key);
end
end
function TableUtil.GetTableValueListFromEnumeration(tableKey, ...)
local values = {};
for enumerationKey, tbl in ... do
table.insert(values, tbl[tableKey]);
end
return values;
end
function TableUtil.GetHighestNumericalValueInTable(table)
local highestValue = nil;
for key, value in pairs(table) do
if type(value) == "number" and (not highestValue or value > highestValue) then
highestValue = value;
end
end
return highestValue;
end
--[[
This utility creates and returns a table of elements that are sorted by value "priority".
Arguments:
comparator - comparator(A, B) returns whether A has higher priority than B
isAssociative - whether the table should be associative (true) or array-like (false). Only values have priorities for associative tables, not keys.
Return usage:
t[k]/t:Get(k) - returns the value stored with key/index k
t[k] = v (Associative only) - stores the value v at key k, sorting accordingly
t:Insert(v) (Array-like only) - inserts the value v, sorting accordingly
t:Remove(k) - removes the element at key/index k
t:Iterate(cb) - calls function cb on each key/index value pair in sorted priority order. !!WARNING!! This must be used instead of pairs(t)/ipairs(t)
t:GetTop() - returns the highest priority element
t:Pop() - returns and removes the highest priority element
t:GetBottom() - returns the lowest priority element
t:Size() - returns the number of stored elements. !!WARNING!! This must be used instead of #t
t:Clear() - removes all elements
--]]
function TableUtil.CreatePriorityTable(comparator, isAssociative)
local sortedArray = {};
local ShiftPositionMap;
local keyToPosMap;
if isAssociative then
keyToPosMap = {};
ShiftPositionMap = function(position, shiftUp)
local mod = shiftUp and 1 or -1;
for k, p in pairs(keyToPosMap) do
if p >= position then
keyToPosMap[k] = p + mod;
end
end
end
end
local function SortedInsert(k, v)
if isAssociative then
local prevPos = keyToPosMap[k];
if prevPos then
tRemove(sortedArray, prevPos);
local shiftUp = false;
ShiftPositionMap(prevPos, shiftUp);
end
end
local top = (#sortedArray > 0) and (#sortedArray + 1) or 1;
local bottom = 1;
while top ~= bottom do
local mid = math.floor((top - bottom) / 2) + bottom;
if not comparator(v, sortedArray[mid]) then
bottom = mid + 1;
else
top = mid;
end
end
local idx = bottom;
tInsert(sortedArray, idx, v);
if isAssociative then
local shiftUp = true;
ShiftPositionMap(idx, shiftUp);
keyToPosMap[k] = idx;
end
end
local t = {};
function t:Get(k)
local key = isAssociative and keyToPosMap[k] or k;
return sortedArray[key];
end
if not isAssociative then
function t:Insert(v)
SortedInsert(nil, v)
end
end
function t:Remove(k)
local key = isAssociative and keyToPosMap[k] or k;
tRemove(sortedArray, key);
if isAssociative then
keyToPosMap[k] = nil;
local shiftUp = false;
ShiftPositionMap(key, shiftUp);
end
end
function t:GetTop()
return #sortedArray > 0 and sortedArray[1];
end
function t:GetBottom()
return #sortedArray > 0 and sortedArray[#sortedArray];
end
function t:Pop()
if t:Size() == 0 then
return nil;
end
local top = t:GetTop();
local removalKey = isAssociative and tInvert(keyToPosMap)[1] or 1;
t:Remove(removalKey);
return top;
end
function t:Iterate(callback)
local posToKeyMap = isAssociative and tInvert(keyToPosMap) or nil;
for pos, v in ipairs(sortedArray) do
local key = isAssociative and posToKeyMap[pos] or pos;
local done = callback(key, v);
if done then
return;
end
end
end
function t:Size()
return #sortedArray;
end
function t:Clear()
sortedArray = {};
if isAssociative then
keyToPosMap = {};
end
end
local mt =
{
__index = function(t, k)
return t:Get(k);
end,
};
if isAssociative then
mt.__newindex = function(t, k, v)
if v ~= nil then
SortedInsert(k, v);
else
t:Remove(k);
end
end
else
mt.__newindex = function(t, k, v)
error("Attempted to assign a value to an array-like priority queue index. Use Insert()/Remove() instead.")
end
end
setmetatable(t, mt);
return t;
end
function DeepMergeTable(destination, source)
for k, v in pairs(source) do
if type(v) == "table" then
if type(destination[k]) == "table" then
DeepMergeTable(destination[k], v)
else
destination[k] = CopyTable(v)
end
else
destination[k] = v
end
end
end
@@ -1,850 +0,0 @@
local ADVENTURE_TYPE = {
PRIMARY = 1,
SECONDARY = 2,
}
local ADVENTURE_CATEGORY = {
DEV = 0,
NOVICE_TIPS = 1,
WINTERGRASP = 2,
CATEGORY_BOSSES = 3,
SPEC_INFO = 4,
QUEST_DAILY_CRYSTALS = 5,
BATTLE_PASS_QUEST_DAILY = 6,
QUEST_RENEGADE = 7,
QUEST_SIRUS = 8,
SEASONAL_EVENT = 9,
QUEST_WEEKLY_PVE = 10,
BRAWL = 11,
BATTLE_PASS_QUEST_WEEKLY = 12,
RAID_NEWEST_NORMAL = 13,
RAID_NEWEST_HEROIC = 14,
WEEKEND_EVENT = 15,
QUEST_DAILY_ISLAND = 16,
ZONE_QUEST = 17,
TUTORIAL_COINS = 100,
MISC = 128,
}
local ADVENTURE_CATEGORY_PRIORITY = {
[ADVENTURE_CATEGORY.DEV] = 0,
[ADVENTURE_CATEGORY.NOVICE_TIPS] = 1,
[ADVENTURE_CATEGORY.WINTERGRASP] = 1,
[ADVENTURE_CATEGORY.CATEGORY_BOSSES] = 1,
[ADVENTURE_CATEGORY.SPEC_INFO] = 3,
[ADVENTURE_CATEGORY.QUEST_DAILY_CRYSTALS] = 1,
[ADVENTURE_CATEGORY.BATTLE_PASS_QUEST_DAILY] = 2,
[ADVENTURE_CATEGORY.QUEST_RENEGADE] = 2,
[ADVENTURE_CATEGORY.QUEST_SIRUS] = 2,
[ADVENTURE_CATEGORY.SEASONAL_EVENT] = 2,
[ADVENTURE_CATEGORY.QUEST_WEEKLY_PVE] = 2,
[ADVENTURE_CATEGORY.BRAWL] = 2,
[ADVENTURE_CATEGORY.BATTLE_PASS_QUEST_WEEKLY] = 5,
[ADVENTURE_CATEGORY.RAID_NEWEST_NORMAL] = 2,
[ADVENTURE_CATEGORY.RAID_NEWEST_HEROIC] = 2,
[ADVENTURE_CATEGORY.WEEKEND_EVENT] = 4,
[ADVENTURE_CATEGORY.QUEST_DAILY_ISLAND] = 5,
[ADVENTURE_CATEGORY.ZONE_QUEST] = 5,
[ADVENTURE_CATEGORY.TUTORIAL_COINS] = 6,
[ADVENTURE_CATEGORY.MISC] = 5,
}
local CONDITION_CHECK = {
PLAYER_IN_GM_MODE = 0,
PLAYER_FACTION = 1,
PLAYER_LEVEL = 2,
PLAYER_HAS_BUFF = 3,
PLAYER_HAS_DEBUFF = 4,
PLAYER_HAS_ITEM = 5,
PLAYER_SPELL_KNOWN = 6,
PET_SPELL_KNOWN = 7,
REALM_STAGE = 8,
REALM_DUNGEON_ACTUAL = 9,
REALM_GAME_EVENT_ACTIVE = 10,
PLAYER_QUEST_COMPLETE = 11,
PLAYER_ACHIEVEMENT_COMPLETE = 12,
PLAYER_BATTLEPASS_DAILY_COUNT = 13,
PLAYER_BATTLEPASS_WEEKLY_COUNT = 14,
REALM_WINTERGRASP_REGISTRATION_ACTIVE = 15,
REALM_BRAWL_ACTIVE = 16,
PLAYER_LFG_DUNGEON_JOINABLE = 17,
PLAYER_LFG_MINIGAME_JOINABLE = 18,
PLAYER_CUSTOM_VALUE = 19,
PLAYER_HAS_QUEST_IN_QUESTLOG = 20,
PLAYER_TALENT_GROUPS_COUNT = 21,
REALM_ID = 22,
PLAYER_IN_CHALLENGE_MODE = 23,
}
local CONDITION_OPERATOR = {
COMP_TYPE_EQ = 0,
COMP_TYPE_HIGH = 1,
COMP_TYPE_LOW = 2,
COMP_TYPE_HIGH_EQ = 3,
COMP_TYPE_LOW_EQ = 4,
}
local ACTION_BUTTON_TYPE = {
NONE = 0,
QUEST_START = 1,
UI_PVP_ARENA_RATING = 2,
UI_PVP_HONOR_RANDOM = 3,
UI_PVP_HONOR_WINTERGRASP = 4,
UI_PVP_HONOR_BRAWL = 5,
UI_PVE_LFG_DUNGEON_RANDOM = 6,
UI_PVE_LFG_DUNGEON_ID = 7,
UI_PVE_LFG_MINIGAME_ID = 8,
UI_EJ_DUNGEON_ID = 9,
UI_BATTLEPASS_QUESTS = 10,
UI_HEAD_HUNTING = 11,
}
local error = error
local ipairs = ipairs
local pairs = pairs
local select = select
local tonumber = tonumber
local type = type
local mathmax, mathmin, mathrandom = math.max, math.min, math.random
local strconcat, strformat, strgsub = strconcat, string.format, string.gsub
local tinsert, tsort, twipe, tCompare = table.insert, table.sort, table.wipe, tCompare
local CanQueueForWintergrasp = CanQueueForWintergrasp
local GetAchievementInfo = GetAchievementInfo
local GetFramerate = GetFramerate
local GetItemCount = GetItemCount
local GetQuestLinkByID = GetQuestLinkByID
local GetSpellInfo = GetSpellInfo
local GetTitleForQuestID = GetTitleForQuestID
local IsLFGDungeonJoinable = IsLFGDungeonJoinable
local IsQuestCompleted = IsQuestCompleted
local IsQuestDataCached = IsQuestDataCached
local IsSpellKnown = IsSpellKnown
local QueryQuestStart = QueryQuestStart
local UnitFactionGroup = UnitFactionGroup
local UnitLevel = UnitLevel
local debugprofilestop = debugprofilestop
local FireCustomClientEvent = FireCustomClientEvent
local GetCurrentBrawlID = C_PvP.GetCurrentBrawlID
local IsGameEventActive = IsGameEventActive
local IsGMAccount = IsGMAccount
local IsInterfaceDevClient = IsInterfaceDevClient
--local RequestQuestCacheByID = RequestQuestCacheByID
local AJ_ACTION_TEXT_JOIN_BATTLE = AJ_ACTION_TEXT_JOIN_BATTLE
local AJ_ACTION_TEXT_JOIN_GROUP = AJ_ACTION_TEXT_JOIN_GROUP
local AJ_ACTION_TEXT_OPEN_EJ = AJ_ACTION_TEXT_OPEN_EJ
local AJ_ACTION_TEXT_SHOW_QUEST = AJ_ACTION_TEXT_SHOW_QUEST
local AJ_ACTION_TEXT_START_HUNT = AJ_ACTION_TEXT_START_HUNT
local AJ_ACTION_TEXT_START_QUEST = AJ_ACTION_TEXT_START_QUEST
local AJ_PRIMARY_REWARD_TEXT = AJ_PRIMARY_REWARD_TEXT
local UNKNOWN = UNKNOWN
local COLOR_PATTERN = strformat("^|cff%s|H", string.rep("[0-9A-Fa-f]", 6))
local QUESTION_MARK_ICON = strgsub(QUESTION_MARK_ICON, "%.BLP$", "")
local NUM_SECONDARY_SUGGESTIONS = 2
local MAX_INT = 2^32/2-1
local PRIVATE = {
DIRTY = true,
FRAMETIME_TARGET = 1 / 55,
FRAMETIME_AVAILABLE = 8,
FRAMETIME_RESERVE = 4,
REGISTRY = {},
SUGGESTIONS = {},
PRIMARY_OFFSET_INDEX = 1,
}
PRIVATE.eventHandler = CreateFrame("Frame")
PRIVATE.eventHandler:Hide()
PRIVATE.eventHandler:SetScript("OnUpdate", function(self, elapsed)
local frametimeStep = PRIVATE.FRAMETIME_TARGET - elapsed
if frametimeStep ~= 0 then
frametimeStep = frametimeStep * 1000
PRIVATE.FRAMETIME_AVAILABLE = mathmax(5, PRIVATE.FRAMETIME_AVAILABLE + frametimeStep)
end
if PRIVATE.BUILD_COROUTINE then
PRIVATE.BUILD_COROUTINE_TIMESTAMP = debugprofilestop()
local status, result, progress = coroutine.resume(PRIVATE.BUILD_COROUTINE)
if not status then
PRIVATE.BUILD_COROUTINE = nil
self:Hide()
error(result, 2)
elseif not result then
if PRIVATE.BUILD_COROUTINE_DEBUG then print("AJ_BUILD_COROUTINE", progress) end
else
PRIVATE.BUILD_COROUTINE = nil
self:Hide()
end
else
self:Hide()
end
end)
PRIVATE.Initialize = function()
if PRIVATE.initialized then
return
end
PRIVATE.initialized = true
_G.ADVENTURE_TYPE = nil
_G.ADVENTURE_CATEGORY = nil
_G.ADVENTURE_CATEGORY_PRIORITY = nil
_G.CONDITION_CHECK = nil
_G.ACTION_BUTTON_TYPE = nil
local ADVENTURE_REGISTRY = _G.ADVENTURE_REGISTRY or {}
if not IsInterfaceDevClient() then
_G.ADVENTURE_REGISTRY = nil
end
for _, adventureType in pairs(ADVENTURE_TYPE) do
PRIVATE.REGISTRY[adventureType] = {}
end
for index, entry in ipairs(ADVENTURE_REGISTRY) do
if not entry.rngPriority then
entry.rngPriority = mathrandom(1, MAX_INT)
end
if entry.content then
entry.content = strgsub(entry.content, "\\n", "\n")
end
tinsert(PRIVATE.REGISTRY[entry.type], entry)
end
for adventureType, registry in pairs(PRIVATE.REGISTRY) do
tsort(registry, PRIVATE.SortEntries)
end
end
PRIVATE.SortEntries = function(a, b)
if ADVENTURE_CATEGORY_PRIORITY[a.category] ~= ADVENTURE_CATEGORY_PRIORITY[b.category] then
return ADVENTURE_CATEGORY_PRIORITY[a.category] < ADVENTURE_CATEGORY_PRIORITY[b.category]
elseif a.rngPriority ~= b.rngPriority then
return a.rngPriority < b.rngPriority
elseif a.id ~= b.id then
return a.id < b.id
end
return false
end
PRIVATE.IsMinigameAvailable = function(minigameID)
for index = 1, C_MiniGames.GetNumGames() do
if C_MiniGames.GetGameIDFromIndex(index) == minigameID then
return true
end
end
return false
end
PRIVATE.ApplyConditionOperator = function(operator, conditionValue, value)
if operator == CONDITION_OPERATOR.COMP_TYPE_EQ then
return value == conditionValue
elseif operator == CONDITION_OPERATOR.COMP_TYPE_HIGH then
return value > conditionValue
elseif operator == CONDITION_OPERATOR.COMP_TYPE_LOW then
return value < conditionValue
elseif operator == CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ then
return value >= conditionValue
elseif operator == CONDITION_OPERATOR.COMP_TYPE_LOW_EQ then
return value <= conditionValue
else
return false
end
end
PRIVATE.CheckCondition = function(entry, conditionType, conditionValue, operator, invert)
local isComplete = false
if conditionType == CONDITION_CHECK.PLAYER_QUEST_COMPLETE then
isComplete = IsQuestCompleted(conditionValue)
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_QUEST_IN_QUESTLOG then
isComplete = GetQuestLogIndexByID(conditionValue) ~= nil
elseif conditionType == CONDITION_CHECK.PLAYER_ACHIEVEMENT_COMPLETE then
isComplete = select(4, GetAchievementInfo(conditionValue))
elseif conditionType == CONDITION_CHECK.PLAYER_BATTLEPASS_DAILY_COUNT then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, C_BattlePass.GetNumQuests(Enum.BattlePass.QuestType.Daily))
elseif conditionType == CONDITION_CHECK.PLAYER_BATTLEPASS_WEEKLY_COUNT then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, C_BattlePass.GetNumQuests(Enum.BattlePass.QuestType.Weekly))
elseif conditionType == CONDITION_CHECK.REALM_STAGE then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, C_Service.GetRealmStage())
elseif conditionType == CONDITION_CHECK.PLAYER_LEVEL then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH_EQ, conditionValue, UnitLevel("player"))
elseif conditionType == CONDITION_CHECK.PLAYER_FACTION then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_EQ, conditionValue, SERVER_PLAYER_FACTION_GROUP[UnitFactionGroup("player")])
elseif conditionType == CONDITION_CHECK.PLAYER_CUSTOM_VALUE then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH, 0, C_Service.GetCustomValue(conditionValue) or 0)
elseif conditionType == CONDITION_CHECK.PLAYER_TALENT_GROUPS_COUNT then
isComplete = PRIVATE.ApplyConditionOperator(operator or CONDITION_OPERATOR.COMP_TYPE_HIGH, conditionValue, C_Talent.GetNumTalentGroups() or 1)
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_ITEM then
isComplete = GetItemCount(conditionValue) > 0
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_BUFF then
isComplete = C_Unit.FindAuraBySpell("player", conditionValue, "HELPFUL") ~= nil
elseif conditionType == CONDITION_CHECK.PLAYER_HAS_DEBUFF then
isComplete = C_Unit.FindAuraBySpell("player", conditionValue, "HARMFUL") ~= nil
elseif conditionType == CONDITION_CHECK.PLAYER_SPELL_KNOWN then
isComplete = IsSpellKnown(conditionValue, false)
elseif conditionType == CONDITION_CHECK.PET_SPELL_KNOWN then
isComplete = IsSpellKnown(conditionValue, true)
elseif conditionType == CONDITION_CHECK.REALM_ID then
isComplete = C_Service.GetRealmID() == conditionValue
elseif conditionType == CONDITION_CHECK.REALM_GAME_EVENT_ACTIVE then
isComplete = IsGameEventActive(conditionValue)
elseif conditionType == CONDITION_CHECK.REALM_BRAWL_ACTIVE then
isComplete = GetCurrentBrawlID() == conditionValue
elseif conditionType == CONDITION_CHECK.REALM_WINTERGRASP_REGISTRATION_ACTIVE then
isComplete = CanQueueForWintergrasp() ~= nil
elseif conditionType == CONDITION_CHECK.PLAYER_LFG_DUNGEON_JOINABLE then
isComplete = conditionValue ~= 0 and IsLFGDungeonJoinable(conditionValue)
elseif conditionType == CONDITION_CHECK.PLAYER_LFG_MINIGAME_JOINABLE then
isComplete = conditionValue ~= 0 and PRIVATE.IsMinigameAvailable(conditionValue)
elseif conditionType == CONDITION_CHECK.REALM_DUNGEON_ACTUAL then
isComplete = C_EncounterJournal.IsActualInstance(conditionValue, entry.ej_difficultyID)
elseif conditionType == CONDITION_CHECK.PLAYER_IN_CHALLENGE_MODE then
isComplete = (C_Hardcore.GetActiveChallengeID() or 0) == conditionValue
elseif conditionType == CONDITION_CHECK.PLAYER_IN_GM_MODE then
isComplete = C_Service.IsInGMMode()
else -- unhandled condition
return false
end
if (not invert and isComplete)
or (invert and not isComplete)
then
return true
end
return false
end
PRIVATE.CheckConditions = function(entry)
for index, condition in ipairs(entry.conditions) do
local isComplete = PRIVATE.CheckCondition(entry, condition.type, condition.value, condition.operator, condition.invert)
if not isComplete then
--[[
if condition.altID then
if type(condition.altID) == "table" then
for idIndex, conditionAltID in ipairs(condition.altID) do
isComplete = PRIVATE.CheckCondition(entry, condition.type, conditionAltID, condition.operator, condition.invert)
if isComplete then
break
end
end
else
isComplete = PRIVATE.CheckCondition(entry, condition.type, condition.altID, condition.operator, condition.invert)
end
if not isComplete then
return false
end
else
return false
end
--]]
return false
end
end
return true
end
PRIVATE.UpdateSuggestionsCoroutine = function(isLevelUp)
local oldIDs = PRIVATE.GetUniqueSuggestionIDs()
local numEntries = 0
local processedEntries = 0
for adventureType, registry in ipairs(PRIVATE.REGISTRY) do
numEntries = numEntries + #registry
end
do -- Build Suggestions
twipe(PRIVATE.SUGGESTIONS)
local numPrimary = 0
local numSecondary = 0
for adventureType, registry in ipairs(PRIVATE.REGISTRY) do
local isPrimary = adventureType == ADVENTURE_TYPE.PRIMARY
PRIVATE.SUGGESTIONS[adventureType] = {}
for index, entry in ipairs(registry) do
processedEntries = processedEntries + 1
if PRIVATE.CheckConditions(entry) then
if isPrimary then
numPrimary = numPrimary + 1
tinsert(PRIVATE.SUGGESTIONS[adventureType], entry)
else
if numSecondary >= NUM_SECONDARY_SUGGESTIONS or numPrimary == 0 then
numPrimary = numPrimary + 1
tinsert(PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.PRIMARY], entry)
else
numSecondary = numSecondary + 1
tinsert(PRIVATE.SUGGESTIONS[adventureType], entry)
end
end
end
if (debugprofilestop() - PRIVATE.BUILD_COROUTINE_TIMESTAMP) > PRIVATE.FRAMETIME_BUDGET then
coroutine.yield(false, processedEntries / numEntries)
end
end
end
end
local newIDs = PRIVATE.GetUniqueSuggestionIDs()
local newAdventureNotice = tCompare(oldIDs, newIDs, 2)
local offsetFound
do -- try to select previously displayed primary suggestion
if PRIVATE.PRIMARY_OFFSET_ID then
local suggestion, offsetIndex = PRIVATE.GetSuggestionEntryByID(ADVENTURE_TYPE.PRIMARY, PRIVATE.PRIMARY_OFFSET_ID)
if suggestion then
PRIVATE.PRIMARY_OFFSET_INDEX = offsetIndex
PRIVATE.PRIMARY_OFFSET_ID = suggestion.id
offsetFound = true
end
end
if not offsetFound then
local suggestion = PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.PRIMARY, 1)
if suggestion then
PRIVATE.PRIMARY_OFFSET_ID = suggestion.id
else
PRIVATE.PRIMARY_OFFSET_ID = nil
end
PRIVATE.PRIMARY_OFFSET_INDEX = 1
end
end
FireCustomClientEvent("AJ_REFRESH_DISPLAY", newAdventureNotice)
return true, 1
end
PRIVATE.UpdateSuggestions = function(isLevelUp)
if PRIVATE.BUILD_COROUTINE then
return
end
local framerate = GetFramerate()
PRIVATE.FRAMETIME_TARGET = framerate > 63 and (1 / 60) or (1 / 55)
PRIVATE.FRAMETIME_BUDGET = 1000 / framerate - PRIVATE.FRAMETIME_RESERVE
PRIVATE.BUILD_COROUTINE = coroutine.create(PRIVATE.UpdateSuggestionsCoroutine)
PRIVATE.BUILD_COROUTINE_TIMESTAMP = debugprofilestop()
local status, result, progress = coroutine.resume(PRIVATE.BUILD_COROUTINE, isLevelUp)
if not status then
PRIVATE.BUILD_COROUTINE = nil
PRIVATE.eventHandler:Hide()
error(result, 2)
end
if coroutine.status(PRIVATE.BUILD_COROUTINE) == "dead" then
PRIVATE.BUILD_COROUTINE = nil
else
if PRIVATE.BUILD_COROUTINE_DEBUG then print("AJ_BUILD_COROUTINE", progress) end
PRIVATE.eventHandler:Show()
end
end
PRIVATE.GetNumPrimarySuggestions = function()
if PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.PRIMARY] then
return #PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.PRIMARY]
end
return 0
end
PRIVATE.GetNumSecondarySuggestions = function()
if PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.SECONDARY] then
return #PRIVATE.SUGGESTIONS[ADVENTURE_TYPE.SECONDARY]
end
return 0
end
PRIVATE.GetButtonText = function(suggestion)
if suggestion.actionType == ACTION_BUTTON_TYPE.NONE then
-- skip
elseif suggestion.actionType == ACTION_BUTTON_TYPE.QUEST_START then
return AJ_ACTION_TEXT_START_QUEST
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_ARENA_RATING
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_RANDOM
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_WINTERGRASP
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_BRAWL
then
return AJ_ACTION_TEXT_JOIN_BATTLE
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_RANDOM
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_ID
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_MINIGAME_ID
then
return AJ_ACTION_TEXT_JOIN_GROUP
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_EJ_DUNGEON_ID then
return AJ_ACTION_TEXT_OPEN_EJ
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_BATTLEPASS_QUESTS then
return AJ_ACTION_TEXT_SHOW_QUEST
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_HEAD_HUNTING then
return AJ_ACTION_TEXT_START_HUNT
end
end
local ACTION_TARGET_VIEW = {
[ACTION_BUTTON_TYPE.UI_PVP_ARENA_RATING] = "pvp-arena-rating",
[ACTION_BUTTON_TYPE.UI_PVP_HONOR_RANDOM] = "pvp-honor-random",
[ACTION_BUTTON_TYPE.UI_PVP_HONOR_WINTERGRASP] = "pvp-honor-wintergrasp",
[ACTION_BUTTON_TYPE.UI_PVP_HONOR_BRAWL] = "pvp-honor-brawl",
[ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_RANDOM] = "pve-lfg-dungeon",
[ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_ID] = "pve-lfg-dungeon",
[ACTION_BUTTON_TYPE.UI_PVE_LFG_MINIGAME_ID] = "pve-lfg-minigame",
}
PRIVATE.ActivateEntry = function(suggestion)
if suggestion.actionType == ACTION_BUTTON_TYPE.NONE then
-- skip
elseif suggestion.actionType == ACTION_BUTTON_TYPE.QUEST_START then
local questID = suggestion.actionParam
if questID then
QueryQuestStart(questID, Enum.QueryQuestStartSource.Suggestion, suggestion.id)
-- FireCustomClientEvent("AJ_QUEST_LOG_OPEN", questID or 0, uiMapID or 0)
else
GMError(string.format("[Suggestion::ActivateEntry] no quest id to start [suggestionID=%d]", suggestion.id))
end
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_ARENA_RATING
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_RANDOM
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_WINTERGRASP
or suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVP_HONOR_BRAWL
then
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
FireCustomClientEvent("AJ_ACTION_PVP_LFG", desiredViewType)
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_RANDOM then
for i, lfgDungeonID in ipairs({258, 259, 261}) do
if IsLFGDungeonJoinable(lfgDungeonID) then
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
FireCustomClientEvent("AJ_ACTION_PVE_LFG", desiredViewType, lfgDungeonID)
return
end
end
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_DUNGEON_ID then
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
local lfgDungeonID = suggestion.actionParam or 0
if IsLFGDungeonJoinable(lfgDungeonID) then
FireCustomClientEvent("AJ_ACTION_PVE_LFG", desiredViewType, lfgDungeonID)
end
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_PVE_LFG_MINIGAME_ID then
local desiredViewType = ACTION_TARGET_VIEW[suggestion.actionType]
local minigameID = suggestion.actionParam or 0
FireCustomClientEvent("AJ_ACTION_PVE_LFG", desiredViewType, minigameID)
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_EJ_DUNGEON_ID then
local dungeonID = suggestion.actionParam or 0
local difficultyID = suggestion.actionParam2 or -1
FireCustomClientEvent("AJ_ACTION_EJ_DUNGEON", dungeonID, difficultyID)
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_BATTLEPASS_QUESTS then
local showQuestTab = true
FireCustomClientEvent("AJ_ACTION_BATTLE_PASS", showQuestTab)
elseif suggestion.actionType == ACTION_BUTTON_TYPE.UI_HEAD_HUNTING then
FireCustomClientEvent("AJ_ACTION_HEAD_HUNTING")
end
end
PRIVATE.GetSuggestionEntry = function(suggestionType, index)
if PRIVATE.SUGGESTIONS[suggestionType] then
return PRIVATE.SUGGESTIONS[suggestionType][index]
end
end
PRIVATE.GetSuggestionEntryByID = function(suggestionType, id)
if PRIVATE.SUGGESTIONS[suggestionType] then
for index, suggestion in ipairs(PRIVATE.SUGGESTIONS[suggestionType]) do
if suggestion.id == id then
return suggestion, index
end
end
end
end
PRIVATE.OnItemLoaded = function()
FireCustomClientEvent("AJ_REWARD_DATA_RECEIVED")
end
PRIVATE.OnItemLoadedContent = function()
FireCustomClientEvent("AJ_REFRESH_DISPLAY", false)
end
PRIVATE.OnQuestLoadedContent = function(questID)
FireCustomClientEvent("AJ_REFRESH_DISPLAY", false)
end
PRIVATE.GetQuestLink = function(questID)
questID = tonumber(questID)
if IsQuestDataCached(questID) then
local questLink = GetQuestLinkByID(questID)
if questLink then
questLink = strgsub(questLink, COLOR_PATTERN, "|cff8B0000|H")
return questLink
end
else
RequestQuestCacheByID(questID, PRIVATE.OnQuestLoadedContent)
end
local name = GetTitleForQuestID(questID)
if not name then
name = IsGMAccount() and strformat("%s (quest:%s)", UNKNOWN, questID) or UNKNOWN
end
return strformat("|cff8B0000|Hquest:%d:-1|h[%s]|h|r", questID, name)
end
PRIVATE.GetItemLink = function(itemID)
itemID = tonumber(itemID)
local name, link = C_Item.GetItemInfo(itemID, nil, PRIVATE.OnItemLoadedContent, true)
if not link then
name = IsGMAccount() and strformat("%s (item:%s)", UNKNOWN, itemID) or UNKNOWN
return strformat("|cffffffff[%s]|r", name)
end
return link
end
PRIVATE.GetSpellLink = function(spellID)
spellID = tonumber(spellID)
local name = GetSpellInfo(spellID)
if not name then
name = IsGMAccount() and strformat("%s (spell:%s)", UNKNOWN, spellID) or UNKNOWN
return strformat("|cffffffff[%s]|r", name)
end
return strformat("|cff000080|Hspell:%d|h[%s]|h|r", spellID, name)
end
PRIVATE.GetSuggestionInfo = function(suggestionType, index)
local suggestion = PRIVATE.GetSuggestionEntry(suggestionType, index)
if suggestion then
local description = suggestion.content
if description then
description = strgsub(description, "{quest:(%d+)}", PRIVATE.GetQuestLink)
description = strgsub(description, "{item:(%d+)}", PRIVATE.GetItemLink)
description = strgsub(description, "{spell:(%d+)}", PRIVATE.GetSpellLink)
end
return {
title = suggestion.title,
description = description,
buttonText = PRIVATE.GetButtonText(suggestion),
iconPath = suggestion.icon and strconcat([[Interface\Icons\]], suggestion.icon) or QUESTION_MARK_ICON,
ej_instanceID = suggestion.ej_instanceID,
difficultyID = suggestion.ej_difficultyID or 1,
expansionLevel = suggestion.ej_expansionLevel,
isRandomDungeon = suggestion.ej_isRandomDungeon,
hideDifficulty = suggestion.ej_showDifficulty ~= 1,
}
end
end
PRIVATE.GetSuggestionByIndex = function(suggestionIndex)
if suggestionIndex <= 1 then
return PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.PRIMARY, PRIVATE.PRIMARY_OFFSET_INDEX)
else
return PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.SECONDARY, suggestionIndex - 1)
end
end
PRIVATE.GetReward = function(suggestion)
if not suggestion then
return
end
local numRewards = #suggestion.rewards
if numRewards > 0 then
if numRewards == 1 and suggestion.rewardDesc then
local itemName, itemLink, itemRarity, iLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(suggestion.rewards[1], nil, PRIVATE.OnItemLoaded, true)
local rewardDesc = type(suggestion.rewardDesc) == "string" and suggestion.rewardDesc or AJ_PRIMARY_REWARD_TEXT
return {
isRewardTable = false,
rewardDesc = rewardDesc,
itemLink = itemLink,
itemIcon = itemTexture,
itemQuantity = 1,
}
else
local rewards = {}
for index, itemID in ipairs(suggestion.rewards) do
local itemName, itemLink, itemRarity, iLevel, itemMinLevel, itemType, itemSubType, itemStackCount, itemEquipLoc, itemTexture, vendorPrice = C_Item.GetItemInfo(suggestion.rewards[index], nil, PRIVATE.OnItemLoaded, true)
if itemLink then
tinsert(rewards, {
itemLink = itemLink,
itemIcon = itemTexture,
itemQuantity = 1,
})
end
end
if #rewards > 0 then
rewards.isRewardList = true
return rewards
end
end
elseif false then -- currency item
return {
isRewardTable = false,
rewardDesc = AJ_PRIMARY_REWARD_TEXT,
currencyIcon = nil,
currencyQuantity = nil,
currencyIcon = nil,
}
else
local minItemLevel = suggestion.reward_minItemLevel
local maxItemLevel = suggestion.reward_maxItemLevel
local itemLevel = suggestion.reward_itemLevel
if itemLevel or minItemLevel or maxItemLevel then
if itemLevel then
minItemLevel = nil
maxItemLevel = nil
elseif not minItemLevel or not maxItemLevel then
minItemLevel = nil
maxItemLevel = nil
itemLevel = minItemLevel or maxItemLevel
end
return {
isRewardTable = true,
itemLevel = itemLevel,
minItemLevel = minItemLevel,
maxItemLevel = maxItemLevel,
}
end
end
end
PRIVATE.GetUniqueSuggestionIDs = function()
local suggestionIDs = {}
for adventureType, registry in ipairs(PRIVATE.REGISTRY) do
suggestionIDs[adventureType] = {}
for index, entry in ipairs(registry) do
suggestionIDs[adventureType][entry.id] = true
end
end
return suggestionIDs
end
do
if IsInterfaceDevClient() then
PRIVATE.BUILD_COROUTINE_DEBUG = true
PRIVATE_AJ = PRIVATE
end
PRIVATE.Initialize()
end
C_AdventureJournal = {}
function C_AdventureJournal.ReloadData()
if IsInterfaceDevClient() then
if _G.ADVENTURE_REGISTRY and PRIVATE.initialized then
PRIVATE.initialized = nil
twipe(PRIVATE.REGISTRY)
PRIVATE.Initialize()
end
end
end
function C_AdventureJournal.CanBeShown()
return true
end
function C_AdventureJournal.UpdateSuggestions(isLevelUp)
PRIVATE.UpdateSuggestions(not not isLevelUp)
end
function C_AdventureJournal.SetPrimaryOffset(offset)
if type(offset) ~= "number" then
error(strformat("bad argument #1 to 'C_AdventureJournal.SetPrimaryOffset' (number expected, got %s)", offset ~= nil and type(offset) or "no value"), 2)
end
offset = offset + 1
if offset < 1 or offset > PRIVATE.GetNumPrimarySuggestions() then
error(strformat("bad argument #1 to 'C_AdventureJournal.SetPrimaryOffset' (index %s out of range [1 - %s])", offset, PRIVATE.GetNumPrimarySuggestions()), 2)
end
if offset == PRIVATE.PRIMARY_OFFSET_INDEX then
return
end
local suggestion = PRIVATE.GetSuggestionEntry(ADVENTURE_TYPE.PRIMARY, offset)
if suggestion then
PRIVATE.PRIMARY_OFFSET_INDEX = offset
PRIVATE.PRIMARY_OFFSET_ID = suggestion.id
FireCustomClientEvent("AJ_REFRESH_DISPLAY", false)
end
end
function C_AdventureJournal.GetPrimaryOffset()
return PRIVATE.PRIMARY_OFFSET_INDEX - 1
end
function C_AdventureJournal.GetNumAvailableSuggestions()
return PRIVATE.GetNumPrimarySuggestions()
end
function C_AdventureJournal.GetSuggestions(suggestionTable)
if type(suggestionTable) ~= "table" then
error(strformat("bad argument #1 to 'C_AdventureJournal.GetSuggestions' (table expected, got %s)", suggestionTable ~= nil and type(suggestionTable) or "no value"), 2)
end
twipe(suggestionTable)
if PRIVATE.GetNumPrimarySuggestions() > 0 then
local suggestion = PRIVATE.GetSuggestionInfo(ADVENTURE_TYPE.PRIMARY, PRIVATE.PRIMARY_OFFSET_INDEX)
if suggestion then
tinsert(suggestionTable, suggestion)
end
end
for index = 1, mathmin(NUM_SECONDARY_SUGGESTIONS, PRIVATE.GetNumSecondarySuggestions()) do
local suggestion = PRIVATE.GetSuggestionInfo(ADVENTURE_TYPE.SECONDARY, index)
if suggestion then
tinsert(suggestionTable, suggestion)
end
end
end
function C_AdventureJournal.GetReward(suggestionIndex)
if type(suggestionIndex) ~= "number" then
error(strformat("bad argument #1 to 'C_AdventureJournal.GetReward' (number expected, got %s)", suggestionIndex ~= nil and type(suggestionIndex) or "no value"), 2)
elseif suggestionIndex < 1 or suggestionIndex > NUM_SECONDARY_SUGGESTIONS + 1 then
error(strformat("bad argument #1 to 'C_AdventureJournal.GetReward' (index %s out of range [1 - %s])", suggestionIndex, NUM_SECONDARY_SUGGESTIONS + 1), 2)
end
return PRIVATE.GetReward(PRIVATE.GetSuggestionByIndex(suggestionIndex))
end
function C_AdventureJournal.ActivateEntry(suggestionIndex)
if type(suggestionIndex) ~= "number" then
error(strformat("bad argument #1 to 'C_AdventureJournal.ActivateEntry' (number expected, got %s)", suggestionIndex ~= nil and type(suggestionIndex) or "no value"), 2)
elseif suggestionIndex < 1 or suggestionIndex > NUM_SECONDARY_SUGGESTIONS + 1 then
error(strformat("bad argument #1 to 'C_AdventureJournal.ActivateEntry' (index %s out of range [1 - %s])", suggestionIndex, NUM_SECONDARY_SUGGESTIONS + 1), 2)
end
local suggestion = PRIVATE.GetSuggestionByIndex(suggestionIndex)
if suggestion then
PRIVATE.ActivateEntry(suggestion)
end
end
@@ -1694,6 +1694,11 @@ do -- Section
if not abilityIcon and sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID] > 0 then
abilityIcon = select(3, GetSpellInfo(sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID]))
end
if not abilityIcon and
(sectionInfo[ENCOUNTER_SECTION_FIELD.DESCRIPTION_SPELL_ID] > 0 or
sectionInfo[ENCOUNTER_SECTION_FIELD.ICON_SPELL_ID] > 0) then
abilityIcon = QUESTION_MARK_ICON
end
if sectionInfo[ENCOUNTER_SECTION_FIELD.DIFFCULTY_MASK] ~= -1 then
local difficultyMask = PRIVATE.GetDifficultyMask(PRIVATE.SELECTED_DIFFICULTY)
@@ -313,6 +313,11 @@ function L.GetObjectiveInfo(index)
questID = o.questID,
questChainID = o.questChainID,
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,
achievementID = o.achievementID,
factionID = o.factionID,
@@ -342,7 +347,19 @@ function L.GetRecommendationInfo(index)
loc = r.loc,
level = r.level,
biome = r.biome,
artwork = r.artwork,
cover = r.cover,
coverImage = r.coverImage,
bgImage = r.bgImage,
buttonImage = r.buttonImage,
icon = r.icon,
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,
priority = r.priority or 0,
}
@@ -9,12 +9,7 @@
## X-Hidden: 1
MoonWellClient.lua
EncounterJournal\SharedXML\ClientDataGenerated\Generated_ItemsCache1.lua
EncounterJournal\SharedXML\ClientDataGenerated\Generated_ItemsCache2.lua
EncounterJournal\SharedXML\ClientDataGenerated\Generated_ItemsCache3.lua
EncounterJournal\EncounterJournalCompat.lua
EncounterJournal\SharedXML\Pools.lua
EncounterJournal\SharedXML\TableUtil.lua
EncounterJournal\SharedXML\SoundKitConstants.lua
EncounterJournal\Generated_EncounterJournalData.lua
EncounterJournal\EncounterJournalData.lua
@@ -22,23 +17,15 @@ EncounterJournal\EncounterJournalTierFilter.lua
EncounterJournal\EncounterJournalTierSnapshot.lua
EncounterJournal\GlobalStrings.lua
EncounterJournal\Utils\C_EncounterJournal.lua
EncounterJournal\Utils\C_AdventureJournal.lua
EncounterJournal\Utils\C_PlayerGuide.lua
EncounterJournal\EncounterJournalInit.lua
EncounterJournal\EncounterJournalFrameMethods.lua
EncounterJournal\EncounterJournalFonts.xml
EncounterJournal\SharedXML\SharedTooltipTemplates.xml
EncounterJournal\SharedXML\TableBuilder.lua
EncounterJournal\SharedXML\EventHandler.lua
EncounterJournal\SharedXML\SharedUIPanelTemplates.xml
EncounterJournal\SharedXML\SharedUIPanelPKBTTemplates.xml
EncounterJournal\SharedXML\HybridScrollFrame.xml
EncounterJournal\SharedXML\NavigationBar.xml
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.lua
EncounterJournal\Custom_EncounterJournal\Custom_EncounterJournal.xml
EncounterJournal\Custom_EncounterJournal\PlayerGuide.xml
EncounterJournal\Custom_EncounterJournal\Custom_LootJournalItems.xml
EncounterJournal\Custom_EncounterJournal\Custom_ItemBrowser.xml
EncounterJournal\EncounterJournalScrollFix.lua
EncounterJournal\EncounterJournalDungeonMode.lua
EncounterJournal\EncounterJournalOpen.lua

Some files were not shown because too many files have changed in this diff Show More