Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e553d781 | |||
| ae9baeee02 | |||
| a984e705c3 | |||
| 5d82df8926 | |||
| 777c5ddf52 | |||
| a118975317 |
@@ -5,6 +5,7 @@ manifest.json
|
||||
.vscode
|
||||
dist
|
||||
build/
|
||||
/new customization mpqs/*.mpq
|
||||
Wow*.exe
|
||||
*.backup.exe
|
||||
Logs/
|
||||
|
||||
+2
-1
@@ -15,7 +15,8 @@
|
||||
url = https://github.com/WarcraftXL/wxl-db2.git
|
||||
[submodule "vendor/modules/wxl-modern-m2"]
|
||||
path = vendor/modules/wxl-modern-m2
|
||||
url = https://github.com/WarcraftXL/wxl-modern-m2.git
|
||||
url = https://github.com/sindoring/wxl-modern-m2.git
|
||||
branch = agent/guard-oversized-m2-batches
|
||||
[submodule "vendor/modules/wxl-modern-wmo"]
|
||||
path = vendor/modules/wxl-modern-wmo
|
||||
url = https://github.com/WarcraftXL/wxl-modern-wmo.git
|
||||
|
||||
@@ -283,8 +283,7 @@ build/customization-compat/server-dbc/BarberShopStyle.dbc
|
||||
Упаковщик:
|
||||
|
||||
- собирает Z из исходников и сгенерированных MoonWell DBC;
|
||||
- копирует `patch-a-001.mpq` в `dist\Data\ruRU\patch-ruRU-X.MPQ`;
|
||||
- копирует `patch-b-002.mpq` в `dist\Data\ruRU\patch-ruRU-Y.MPQ`;
|
||||
- не копирует внешние графические X/Y в `dist` и не включает их в managed manifest;
|
||||
- не добавляет `patch-k.mpq`.
|
||||
|
||||
Для полной локальной сборки и синхронизации клиента можно использовать:
|
||||
@@ -303,13 +302,15 @@ build/customization-compat/server-dbc/BarberShopStyle.dbc
|
||||
|
||||
## Исходники MPQ и установленные копии
|
||||
|
||||
Файлы в `new customization mpqs` — источники сборки. В готовом клиенте
|
||||
используются их копии X и Y.
|
||||
Файлы в `new customization mpqs` используются только как локальные источники
|
||||
генератора таблиц совместимости. В готовом клиенте X и Y управляются отдельно от
|
||||
репозиторных патчей.
|
||||
|
||||
Технически уже установленный клиент может запускаться без исходников в
|
||||
репозитории, если X/Y остаются в `Data\ruRU`. Однако удаление исходников сломает
|
||||
повторную сборку `dist`, восстановление клиента и генерацию совместимости.
|
||||
Поэтому `patch-a-001.mpq` и `patch-b-002.mpq` нужно хранить.
|
||||
Списки репозиторных и внешних графических патчей находятся в
|
||||
`patch-layout.json`. `deploy.ps1`, `run.ps1`, генератор манифеста и S3 uploader
|
||||
синхронизируют только список `repositoryPatches`. Пути из `graphicsPatches`
|
||||
сохраняются без копирования и удаления. Если один путь указан в обоих списках,
|
||||
`repositoryPatches` имеет приоритет.
|
||||
|
||||
Z не консолидирует многогигабайтные модели и текстуры X/Y. Удаление X или Y из
|
||||
клиента не освобождается наличием текущих DBC в Z.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# DragonUI_NewEra compatibility
|
||||
|
||||
Compatibility was audited against `ghbset/DragonUI_NewEra` commit
|
||||
`37a29d640f97545ff1bb7d4b0ba7a5329cce1b8c` (2026-08-09).
|
||||
|
||||
## Status
|
||||
|
||||
DragonUI_NewEra is bundled in `patch-ruRU-5` as a required MoonWell system
|
||||
component. It is enabled by default and hidden from the AddOns list together
|
||||
with DragonUI and `!!!ClassicAPI`.
|
||||
|
||||
The MoonWell DragonUI exposes the integration seams used by NewEra:
|
||||
|
||||
- global `DragonUI` addon table;
|
||||
- `ModuleRegistry`;
|
||||
- `MoversSystem` and the editable-frame API;
|
||||
- `OptionsPanel` and `PanelControls` after `DragonUI_Options` is loaded;
|
||||
- `Fonts`, `SafeCall`, `CombatQueue`, and global `SetAtlasTexture`.
|
||||
|
||||
NewEra remains a separate addon folder rather than being merged into DragonUI.
|
||||
Its TOC retains:
|
||||
|
||||
```toc
|
||||
## Interface: 30300
|
||||
## Dependencies: DragonUI, !!!ClassicAPI
|
||||
## OptionalDeps: DragonUI_Options
|
||||
```
|
||||
|
||||
`DragonUI_NewEra` is an extension, not a replacement for the HUD modules. It
|
||||
does not need `X-DragonUI-Overrides` unless a future NewEra module explicitly
|
||||
takes ownership of `mainbars`, `minimap`, `buffs`, or another registered HUD
|
||||
module.
|
||||
|
||||
## Bundled sources
|
||||
|
||||
The system package contains:
|
||||
|
||||
```text
|
||||
Interface/AddOns/!!!ClassicAPI/ b3da3000bba23a2c31aa9c9b7c0232d5973789e6
|
||||
Interface/AddOns/DragonUI_NewEra/ 37a29d640f97545ff1bb7d4b0ba7a5329cce1b8c
|
||||
```
|
||||
|
||||
Upstream Git metadata, screenshots, and top-level build tools are excluded.
|
||||
Runtime sources, assets, and the upstream QA harness are retained. The exact
|
||||
revisions are also recorded in addon TOC metadata as `X-Upstream-Commit`.
|
||||
|
||||
Neither audited upstream contains an explicit repository-wide redistribution
|
||||
license. MoonWell maintainers must obtain and archive redistribution permission
|
||||
before a public release. The source revisions above preserve provenance but do
|
||||
not replace that permission.
|
||||
|
||||
## Known ownership conflict
|
||||
|
||||
Both MoonWellClient and NewEra provide an Encounter Journal / Adventure Guide
|
||||
and the `ToggleEncounterJournal` entry point. MoonWell's journal is currently
|
||||
authoritative because it receives server-backed data. NewEra's encounter
|
||||
journal files are intentionally omitted from its system TOC, and its dead
|
||||
options toggle is hidden.
|
||||
|
||||
The NewEra combined-bag module is marked work in progress upstream. Treat it as
|
||||
opt-in and do not use it as the default replacement for DragonUI/Blizzard bags
|
||||
without a separate gameplay test pass.
|
||||
|
||||
NewEra ships 94 power-of-two BLP atlases with one dimension above the project's
|
||||
conservative 512-pixel authoring limit; the largest are 2048x2048. Upstream
|
||||
marks their BLP2 encodings as 3.3.5a-loadable and they package successfully,
|
||||
but every affected panel still requires an in-client render pass on MoonWell's
|
||||
exact executable before release. If that client rejects them, split the atlases
|
||||
without changing their public atlas names instead of silently scaling UI
|
||||
geometry.
|
||||
|
||||
## Release checklist
|
||||
|
||||
1. Update only from reviewed, pinned DragonUI_NewEra and ClassicAPI revisions.
|
||||
2. Obtain and record licenses or permissions for code, fonts, sounds, and BLPs.
|
||||
3. Keep DragonUI_NewEra as its own folder and TOC; do not merge it into
|
||||
DragonUI or FrameXML.
|
||||
4. Keep NewEra Encounter Journal disabled until ownership is deliberately
|
||||
migrated from MoonWellClient.
|
||||
5. Test Character, Spellbook, Talents, Professions, Auction House, LFG, Social,
|
||||
Collections, and the DragonUI options tab on build 12340.
|
||||
6. Test login with NewEra disabled: the default DragonUI must remain complete.
|
||||
@@ -0,0 +1,105 @@
|
||||
# DragonUI: replacement addons
|
||||
|
||||
MoonWell ships DragonUI as the default interface. Players can replace an
|
||||
individual DragonUI module without copying or editing the bundled addon.
|
||||
|
||||
## Replacement addon structure
|
||||
|
||||
The replacement must use its own folder and addon name. Do not create another
|
||||
folder named `DragonUI`; files inside an MPQ and loose files with the same
|
||||
virtual path do not form a supported update mechanism.
|
||||
|
||||
```text
|
||||
MyDragonUIMinimap/
|
||||
|-- MyDragonUIMinimap.toc
|
||||
`-- MyDragonUIMinimap.lua
|
||||
```
|
||||
|
||||
`MyDragonUIMinimap.toc`:
|
||||
|
||||
```toc
|
||||
## Interface: 30300
|
||||
## Title: My DragonUI Minimap
|
||||
## Dependencies: DragonUI
|
||||
## X-DragonUI-Overrides: minimap
|
||||
## X-DragonUI-Override-Priority: 100
|
||||
|
||||
MyDragonUIMinimap.lua
|
||||
```
|
||||
|
||||
The `X-DragonUI-Overrides` declaration is required. DragonUI reads it before
|
||||
dependent addons execute and does not apply the bundled implementation. A list
|
||||
may contain several comma-separated module identifiers.
|
||||
|
||||
`MyDragonUIMinimap.lua`:
|
||||
|
||||
```lua
|
||||
local provider = {}
|
||||
|
||||
function provider:Activate(dragonUI, moduleName)
|
||||
-- Create or show the replacement here.
|
||||
end
|
||||
|
||||
function provider:Deactivate(dragonUI, moduleName)
|
||||
-- Hide frames and unregister events created by this provider.
|
||||
end
|
||||
|
||||
local ok, reason = DragonUI:RegisterModuleOverride(
|
||||
"minimap",
|
||||
"MyDragonUIMinimap",
|
||||
provider,
|
||||
{ priority = 100 }
|
||||
)
|
||||
|
||||
if not ok then
|
||||
DEFAULT_CHAT_FRAME:AddMessage("MyDragonUIMinimap: " .. tostring(reason))
|
||||
end
|
||||
```
|
||||
|
||||
The owner passed to `RegisterModuleOverride` must match the addon folder/TOC
|
||||
name. Supported provider activation names are `Activate`, `Apply`, `Enable`, or
|
||||
`OnEnable`; cleanup names are `Deactivate`, `Restore`, `Disable`, or
|
||||
`OnDisable`.
|
||||
|
||||
## Public API (version 1)
|
||||
|
||||
```lua
|
||||
DragonUI.PUBLIC_API_VERSION
|
||||
DragonUI:RegisterModuleOverride(moduleName, owner, provider[, options])
|
||||
DragonUI:UnregisterModuleOverride(moduleName, owner)
|
||||
DragonUI:IsModuleOverridden(moduleName)
|
||||
DragonUI:GetModuleOverride(moduleName) -- owner, provider
|
||||
```
|
||||
|
||||
DragonUI also sends the Ace message `DRAGONUI_OVERRIDE_CHANGED` with
|
||||
`moduleName, owner`. `owner` is nil after the final provider is removed.
|
||||
|
||||
Registering a replacement at runtime without TOC metadata is supported only
|
||||
for a module that can be restored safely. For a load-once module the call
|
||||
returns `true, "RELOAD_REQUIRED"`; add the metadata and reload the UI.
|
||||
|
||||
## Bundled module identifiers
|
||||
|
||||
The registry currently exposes these identifiers:
|
||||
|
||||
```text
|
||||
bagsort, boss, buffs, buttons, castbar, chatmods, combuctor, cooldowns,
|
||||
darkmode, itemquality, keybinding, mainbars, micromenu, minimap, multicast,
|
||||
noop, petbar, player, questtracker, rage_indicator, stance, tooltip,
|
||||
unitframe_layers, vehicle
|
||||
```
|
||||
|
||||
Target/focus/party frames still contain legacy startup code and will be added
|
||||
to the public registry before they are advertised as independently replaceable.
|
||||
|
||||
## Conflict resolution
|
||||
|
||||
When multiple enabled addons claim the same module, the highest numeric
|
||||
`X-DragonUI-Override-Priority` wins. Equal priorities are resolved by addon
|
||||
name to make the result deterministic. A declaration that never registers a
|
||||
provider is rejected at `PLAYER_LOGIN`, and DragonUI restores its bundled
|
||||
module so the player is not left without that UI element.
|
||||
|
||||
Module suppression is session-only. DragonUI restores the player's saved
|
||||
enabled/disabled choice during `PLAYER_LOGOUT`, so removing a replacement addon
|
||||
automatically brings back the previous DragonUI configuration.
|
||||
@@ -109,6 +109,27 @@ $env:WOW_HOME = 'C:\Program Files (x86)\World of Warcraft'
|
||||
.\run.ps1
|
||||
```
|
||||
|
||||
Для быстрой разработки интерфейса без пересборки MPQ:
|
||||
|
||||
```powershell
|
||||
# Наложить src\Data\...\Interface на клиент и следить за изменениями
|
||||
.\hotreload.ps1
|
||||
|
||||
# То же самое, но запустить клиент и автоматически завершить сессию после его выхода
|
||||
.\hotreload.ps1 -Launch
|
||||
|
||||
# Явно указать клиент, если WOW_HOME не задан
|
||||
.\hotreload.ps1 -ClientPath 'D:\Games\World of Warcraft'
|
||||
|
||||
# Подключиться к другому серверу вместо локального
|
||||
.\hotreload.ps1 -Realmlist 'logon.example.org'
|
||||
```
|
||||
|
||||
Во время сессии изменённые файлы сразу копируются в `WOW_HOME\Interface`; увидеть изменения в игре
|
||||
можно после `/reload`. По `Ctrl+C` или после выхода запущенного через `-Launch` клиента временные
|
||||
файлы удаляются, а существовавшие до запуска файлы восстанавливаются. По умолчанию на время сессии
|
||||
в `Data\ruRU\realmlist.wtf` записывается `127.0.0.1`; исходный realmlist также восстанавливается.
|
||||
|
||||
Диагностика запуска находится в `Logs\wxl-core.log` и `Logs\d3d9proxy.log` внутри клиента.
|
||||
|
||||
## MPQ-пакеты
|
||||
@@ -123,6 +144,23 @@ WarcraftXL не требуется и в объектное хранилище
|
||||
- `patch-ruRU-5` — русская локализация и интерфейс;
|
||||
- `patch-Z` — Mythic+ ресурсы и клиентские данные.
|
||||
|
||||
## DragonUI и пользовательские замены
|
||||
|
||||
DragonUI поставляется как включённый системный интерфейс. Игрок не заменяет
|
||||
файлы из MPQ: отдельный аддон может заранее объявить, какие модули он берёт под
|
||||
свой контроль, и зарегистрировать собственную реализацию после загрузки
|
||||
DragonUI. Формат TOC, Lua API, приоритеты и рабочий пример описаны в
|
||||
[`DRAGONUI_OVERRIDES.md`](DRAGONUI_OVERRIDES.md).
|
||||
|
||||
DragonUI_NewEra и его `!!!ClassicAPI` входят в системный интерфейс MoonWell.
|
||||
Зафиксированные upstream-версии, состав поставки и решение конфликта двух
|
||||
Encounter Journal описаны в [`DRAGONUI_NEWERA.md`](DRAGONUI_NEWERA.md).
|
||||
|
||||
Управляемые репозиторием MPQ и внешние графические патчи перечислены отдельно в
|
||||
`patch-layout.json`. Сборка и deploy синхронизируют только `repositoryPatches` и
|
||||
не изменяют `graphicsPatches`. При пересечении списков репозиторный патч имеет
|
||||
приоритет.
|
||||
|
||||
## Лицензирование
|
||||
|
||||
WarcraftXL распространяется по GPL-3.0 и подключён как отдельный неизменённый upstream submodule.
|
||||
|
||||
+53
-20
@@ -75,8 +75,53 @@ function Enable-LargeAddressAware {
|
||||
}
|
||||
}
|
||||
|
||||
function Initialize-MissingSubmodules {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepositoryPath
|
||||
)
|
||||
|
||||
$modulesFile = Join-Path $RepositoryPath '.gitmodules'
|
||||
if (-not (Test-Path -LiteralPath $modulesFile -PathType Leaf)) {
|
||||
return
|
||||
}
|
||||
|
||||
$configuredModules = @(
|
||||
& git -C $RepositoryPath config --file .gitmodules --get-regexp '^submodule\..*\.path$'
|
||||
)
|
||||
if ($LASTEXITCODE -gt 1) {
|
||||
throw "Failed to read submodules from $modulesFile"
|
||||
}
|
||||
|
||||
foreach ($configuredModule in $configuredModules) {
|
||||
$fields = $configuredModule -split '\s+', 2
|
||||
if ($fields.Count -ne 2) {
|
||||
continue
|
||||
}
|
||||
|
||||
$relativePath = $fields[1].Trim()
|
||||
$modulePath = Join-Path $RepositoryPath $relativePath
|
||||
$gitMarker = Join-Path $modulePath '.git'
|
||||
|
||||
# Updating an initialized submodule checks out the SHA recorded by its parent and detaches
|
||||
# any active development branch. Only initialize genuinely missing worktrees.
|
||||
if (-not (Test-Path -LiteralPath $gitMarker)) {
|
||||
Write-Host "Initializing missing submodule: $modulePath"
|
||||
& git -C $RepositoryPath submodule update --init -- $relativePath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to initialize submodule: $modulePath"
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $modulePath -PathType Container) {
|
||||
Initialize-MissingSubmodules -RepositoryPath $modulePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = $PSScriptRoot
|
||||
. (Join-Path $repoRoot 'pipeline-layout.ps1')
|
||||
$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
|
||||
@@ -84,8 +129,7 @@ if (-not (Test-Path -LiteralPath $cmake)) {
|
||||
$cmake = $cmakeCommand.Source
|
||||
}
|
||||
|
||||
& git -C $repoRoot submodule update --init --recursive
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Failed to initialize WarcraftXL submodules.' }
|
||||
Initialize-MissingSubmodules -RepositoryPath $repoRoot
|
||||
|
||||
$win32BuildDir = Join-Path $repoRoot 'build\warcraftxl-win32'
|
||||
$stockExe = Join-Path $repoRoot 'Wow_Original.exe'
|
||||
@@ -110,18 +154,7 @@ $win32ArtifactDir = Join-Path $win32BuildDir "vendor\warcraftxl\$Configuration"
|
||||
$warcraftXL = Join-Path $win32ArtifactDir 'WarcraftXL.dll'
|
||||
$selectedProxy = Join-Path $win32ArtifactDir 'd3d9.dll'
|
||||
$recoveryProxy = Join-Path $win32BuildDir "artifacts\$Configuration\d3d9-native.dll"
|
||||
$extensionNames = @(
|
||||
'MoonWell',
|
||||
'wxl-db2',
|
||||
'wxl-fdid-moonwell',
|
||||
'wxl-grasswind',
|
||||
'wxl-modern-adt',
|
||||
'wxl-modern-blp',
|
||||
'wxl-modern-m2',
|
||||
'wxl-modern-wmo',
|
||||
'wxl-moonwell-storage-fallback',
|
||||
'wxl-unit-outline'
|
||||
)
|
||||
$extensionNames = Get-MoonWellExtensionNames
|
||||
|
||||
if ($NativeRenderer) {
|
||||
Write-Warning '-NativeRenderer is no longer needed: WarcraftXL 1.1 uses native D3D9 by default.'
|
||||
@@ -133,6 +166,8 @@ function Install-WarcraftXLArtifacts {
|
||||
[string]$Destination
|
||||
)
|
||||
|
||||
Remove-MoonWellRuntimeGarbage -Destination $Destination
|
||||
|
||||
$utils = Join-Path $Destination 'Utils'
|
||||
New-Item `
|
||||
-ItemType Directory `
|
||||
@@ -140,12 +175,6 @@ function Install-WarcraftXLArtifacts {
|
||||
-Force |
|
||||
Out-Null
|
||||
|
||||
$legacyHost = Join-Path $utils 'WarcraftXLHost.exe'
|
||||
if (Test-Path -LiteralPath $legacyHost -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $legacyHost -Force
|
||||
Write-Host "Removed obsolete WarcraftXL 1.0 host: $legacyHost"
|
||||
}
|
||||
|
||||
$destinationExe = Join-Path $Destination 'Wow.exe'
|
||||
|
||||
Copy-Item `
|
||||
@@ -208,6 +237,10 @@ if ($Deploy) {
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($PackagePath)) {
|
||||
$PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $repoRoot `
|
||||
-ClientPath $ClientPath
|
||||
Install-WarcraftXLArtifacts -Destination $PackagePath
|
||||
Write-Host "Packaged stock Wow.exe and WarcraftXL artifacts in $PackagePath"
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from patch_layout import PATCH_LAYOUT
|
||||
|
||||
|
||||
IGNORED_TOP_LEVEL_DIRS = {
|
||||
"Cache",
|
||||
@@ -49,6 +51,8 @@ def is_ignored(rel_path: Path) -> bool:
|
||||
return True
|
||||
if len(rel_path.parts) == 1 and rel_path.name in IGNORED_TOP_LEVEL_FILES:
|
||||
return True
|
||||
if PATCH_LAYOUT.is_external_graphics_patch(rel_path.as_posix()):
|
||||
return True
|
||||
return any(part in IGNORED_DIRS_ANYWHERE for part in rel_path.parts[:-1])
|
||||
|
||||
|
||||
|
||||
+31
-6
@@ -13,6 +13,7 @@ param(
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = $PSScriptRoot
|
||||
. (Join-Path $repoRoot 'pipeline-layout.ps1')
|
||||
|
||||
function Import-ProjectEnvironment {
|
||||
$envFile = Join-Path $repoRoot '.env'
|
||||
@@ -46,6 +47,10 @@ $PackagePath = [System.IO.Path]::GetFullPath($PackagePath)
|
||||
if (-not (Test-Path -LiteralPath $ClientPath -PathType Container)) {
|
||||
throw "Client directory was not found: $ClientPath"
|
||||
}
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $repoRoot `
|
||||
-ClientPath $ClientPath
|
||||
|
||||
$runningClients = @(Get-Process Wow -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.Path -and $_.Path.StartsWith($ClientPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
@@ -64,11 +69,21 @@ $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.' }
|
||||
Write-Host '[1/5] Preserving initialized WarcraftXL worktrees...'
|
||||
|
||||
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||
$previousPackageFiles = @(
|
||||
Get-RelativeFilePaths -Root $PackagePath
|
||||
)
|
||||
if (-not $SkipDataBuild) {
|
||||
Write-Host 'Resetting package staging to prevent stale build artifacts...'
|
||||
Reset-MoonWellPackageStaging `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $repoRoot `
|
||||
-ClientPath $ClientPath
|
||||
} else {
|
||||
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||
Remove-MoonWellRuntimeGarbage -Destination $PackagePath
|
||||
}
|
||||
|
||||
if (-not $SkipDataBuild) {
|
||||
Write-Host '[2/5] Building MPQ patches...'
|
||||
@@ -99,6 +114,18 @@ if ($NativeRenderer) { $buildArguments.NativeRenderer = $true }
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WarcraftXL deployment failed.' }
|
||||
|
||||
Write-Host '[4/5] Synchronizing package files...'
|
||||
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $python) { throw 'Python was not found; package validation cannot run.' }
|
||||
& $python.Source (Join-Path $repoRoot 'validate_package.py') --dir $PackagePath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Package validation failed.' }
|
||||
|
||||
$currentPackageFiles = @(Get-RelativeFilePaths -Root $PackagePath)
|
||||
Remove-StalePackageFilesFromClient `
|
||||
-ClientPath $ClientPath `
|
||||
-PreviousFiles $previousPackageFiles `
|
||||
-CurrentFiles $currentPackageFiles
|
||||
Remove-MoonWellRuntimeGarbage -Destination $ClientPath
|
||||
|
||||
& robocopy $PackagePath $ClientPath /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP
|
||||
$robocopyExitCode = $LASTEXITCODE
|
||||
if ($robocopyExitCode -ge 8) {
|
||||
@@ -107,8 +134,6 @@ if ($robocopyExitCode -ge 8) {
|
||||
|
||||
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.' }
|
||||
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ClientPath = '',
|
||||
[ValidatePattern('^[^\r\n]+$')]
|
||||
[string]$Realmlist = '127.0.0.1',
|
||||
[ValidateRange(100, 10000)]
|
||||
[int]$PollIntervalMilliseconds = 500,
|
||||
[switch]$NoInitialSync,
|
||||
[switch]$Launch
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repoRoot = [System.IO.Path]::GetFullPath($PSScriptRoot).TrimEnd('\')
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InterfaceSourceRoots {
|
||||
param([Parameter(Mandatory)][string]$RepositoryPath)
|
||||
|
||||
$dataRoot = Join-Path $RepositoryPath 'src\Data'
|
||||
if (-not (Test-Path -LiteralPath $dataRoot -PathType Container)) {
|
||||
throw "Patch source directory was not found: $dataRoot"
|
||||
}
|
||||
|
||||
$roots = @()
|
||||
foreach ($dataEntry in Get-ChildItem -LiteralPath $dataRoot -Directory | Sort-Object Name) {
|
||||
$directInterface = Join-Path $dataEntry.FullName 'Interface'
|
||||
if (Test-Path -LiteralPath $directInterface -PathType Container) {
|
||||
$roots += [System.IO.Path]::GetFullPath($directInterface).TrimEnd('\')
|
||||
}
|
||||
|
||||
if ($dataEntry.Name -match '^[a-z]{2}[A-Z]{2}$') {
|
||||
foreach ($localePatch in Get-ChildItem -LiteralPath $dataEntry.FullName -Directory | Sort-Object Name) {
|
||||
$localeInterface = Join-Path $localePatch.FullName 'Interface'
|
||||
if (Test-Path -LiteralPath $localeInterface -PathType Container) {
|
||||
$roots += [System.IO.Path]::GetFullPath($localeInterface).TrimEnd('\')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @($roots)
|
||||
}
|
||||
|
||||
function Get-EffectiveInterfaceFiles {
|
||||
param([Parameter(Mandatory)][string[]]$SourceRoots)
|
||||
|
||||
# Later patch roots win, matching the order in which locale and higher patch
|
||||
# layers override earlier archives in the client.
|
||||
$files = @{}
|
||||
foreach ($sourceRoot in $SourceRoots) {
|
||||
$sourcePrefix = $sourceRoot + '\'
|
||||
foreach ($file in Get-ChildItem -LiteralPath $sourceRoot -Recurse -Force -File) {
|
||||
$relativePath = $file.FullName.Substring($sourcePrefix.Length)
|
||||
if ($relativePath.EndsWith('.manifest.json', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$files[$relativePath] = [pscustomobject]@{
|
||||
RelativePath = $relativePath
|
||||
SourcePath = $file.FullName
|
||||
Signature = '{0}:{1}' -f $file.Length, $file.LastWriteTimeUtc.Ticks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files
|
||||
}
|
||||
|
||||
function Get-SafeTargetPath {
|
||||
param([Parameter(Mandatory)][string]$RelativePath)
|
||||
|
||||
$targetPath = [System.IO.Path]::GetFullPath((Join-Path $interfaceRoot $RelativePath))
|
||||
if (-not $targetPath.StartsWith($interfacePrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Interface path escapes the client Interface directory: $RelativePath"
|
||||
}
|
||||
return $targetPath
|
||||
}
|
||||
|
||||
function Add-CreatedDirectory {
|
||||
param([Parameter(Mandatory)][string]$DirectoryPath)
|
||||
|
||||
if (Test-Path -LiteralPath $DirectoryPath -PathType Container) {
|
||||
return
|
||||
}
|
||||
|
||||
$missing = @()
|
||||
$candidate = $DirectoryPath
|
||||
while ($candidate.StartsWith($interfacePrefix, [System.StringComparison]::OrdinalIgnoreCase) -and
|
||||
-not (Test-Path -LiteralPath $candidate -PathType Container)) {
|
||||
$missing += $candidate
|
||||
$candidate = Split-Path -Parent $candidate
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $DirectoryPath -Force | Out-Null
|
||||
foreach ($created in $missing) {
|
||||
$createdDirectories[$created] = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Install-HotReloadFile {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$RelativePath,
|
||||
[Parameter(Mandatory)][string]$SourcePath,
|
||||
[switch]$Quiet
|
||||
)
|
||||
|
||||
$targetPath = Get-SafeTargetPath -RelativePath $RelativePath
|
||||
if (-not $ownedTargets.ContainsKey($RelativePath)) {
|
||||
$hadOriginal = Test-Path -LiteralPath $targetPath -PathType Leaf
|
||||
$backupPath = $null
|
||||
if ($hadOriginal) {
|
||||
$backupPath = Join-Path $backupRoot $RelativePath
|
||||
Add-CreatedDirectory -DirectoryPath (Split-Path -Parent $backupPath)
|
||||
Copy-Item -LiteralPath $targetPath -Destination $backupPath -Force
|
||||
}
|
||||
|
||||
$ownedTargets[$RelativePath] = [pscustomobject]@{
|
||||
RelativePath = $RelativePath
|
||||
TargetPath = $targetPath
|
||||
HadOriginal = $hadOriginal
|
||||
BackupPath = $backupPath
|
||||
}
|
||||
}
|
||||
|
||||
Add-CreatedDirectory -DirectoryPath (Split-Path -Parent $targetPath)
|
||||
Copy-Item -LiteralPath $SourcePath -Destination $targetPath -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Updated: Interface\$RelativePath" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
function Restore-HotReloadTarget {
|
||||
param(
|
||||
[Parameter(Mandatory)]$State,
|
||||
[switch]$Quiet
|
||||
)
|
||||
|
||||
if ($State.HadOriginal) {
|
||||
Add-CreatedDirectory -DirectoryPath (Split-Path -Parent $State.TargetPath)
|
||||
Copy-Item -LiteralPath $State.BackupPath -Destination $State.TargetPath -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Restored: Interface\$($State.RelativePath)"
|
||||
}
|
||||
} elseif (Test-Path -LiteralPath $State.TargetPath -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $State.TargetPath -Force
|
||||
if (-not $Quiet) {
|
||||
Write-Host "Removed: Interface\$($State.RelativePath)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Import-ProjectEnvironment
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
$ClientPath = $env:WOW_HOME
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
throw 'ClientPath was not supplied and WOW_HOME is not set.'
|
||||
}
|
||||
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
|
||||
$clientPrefix = $clientRoot + '\'
|
||||
$wowExecutable = Join-Path $clientRoot 'Wow.exe'
|
||||
if (-not (Test-Path -LiteralPath $wowExecutable -PathType Leaf)) {
|
||||
throw "Wow.exe was not found in the client directory: $clientRoot"
|
||||
}
|
||||
if ($clientRoot.Equals($repoRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$repoRoot.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw 'The client directory must be separate from the repository.'
|
||||
}
|
||||
|
||||
$sourceRoots = @(Get-InterfaceSourceRoots -RepositoryPath $repoRoot)
|
||||
if (-not $sourceRoots.Count) {
|
||||
throw 'No src\Data\...\Interface patch directories were found.'
|
||||
}
|
||||
|
||||
$interfaceRoot = Join-Path $clientRoot 'Interface'
|
||||
$interfaceRoot = [System.IO.Path]::GetFullPath($interfaceRoot).TrimEnd('\')
|
||||
$interfacePrefix = $interfaceRoot + '\'
|
||||
$systemTempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath()).TrimEnd('\')
|
||||
$systemTempPrefix = $systemTempRoot + '\'
|
||||
$backupRoot = [System.IO.Path]::GetFullPath((
|
||||
Join-Path $systemTempRoot ('MoonWell-hotreload-' + [Guid]::NewGuid().ToString('N'))
|
||||
))
|
||||
if (-not $backupRoot.StartsWith($systemTempPrefix, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not ([System.IO.Path]::GetFileName($backupRoot)).StartsWith(
|
||||
'MoonWell-hotreload-',
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Unsafe hot reload backup path: $backupRoot"
|
||||
}
|
||||
$ownedTargets = @{}
|
||||
$createdDirectories = @{}
|
||||
$createdRealmlistDirectories = @()
|
||||
$realmlistState = $null
|
||||
$cleanupFailed = $false
|
||||
$launchedProcess = $null
|
||||
|
||||
New-Item -ItemType Directory -Path $interfaceRoot, $backupRoot -Force | Out-Null
|
||||
|
||||
Write-Host 'MoonWell Interface hot reload'
|
||||
Write-Host " Client: $clientRoot"
|
||||
Write-Host " Sources: $($sourceRoots.Count) patch directories"
|
||||
|
||||
try {
|
||||
$realmlistPath = [System.IO.Path]::GetFullPath((
|
||||
Join-Path $clientRoot 'Data\ruRU\realmlist.wtf'
|
||||
))
|
||||
if (-not $realmlistPath.StartsWith(
|
||||
$clientPrefix,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Realmlist path escapes the client directory: $realmlistPath"
|
||||
}
|
||||
|
||||
$realmlistDirectory = Split-Path -Parent $realmlistPath
|
||||
$candidateDirectory = $realmlistDirectory
|
||||
while ($candidateDirectory.StartsWith(
|
||||
$clientPrefix,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
) -and
|
||||
-not (Test-Path -LiteralPath $candidateDirectory -PathType Container)) {
|
||||
$createdRealmlistDirectories += $candidateDirectory
|
||||
$candidateDirectory = Split-Path -Parent $candidateDirectory
|
||||
}
|
||||
New-Item -ItemType Directory -Path $realmlistDirectory -Force | Out-Null
|
||||
|
||||
$hadOriginalRealmlist = Test-Path -LiteralPath $realmlistPath -PathType Leaf
|
||||
$realmlistBackupPath = $null
|
||||
if ($hadOriginalRealmlist) {
|
||||
$realmlistBackupPath = Join-Path $backupRoot 'realmlist.wtf.original'
|
||||
Copy-Item -LiteralPath $realmlistPath -Destination $realmlistBackupPath -Force
|
||||
}
|
||||
$realmlistState = [pscustomobject]@{
|
||||
Path = $realmlistPath
|
||||
HadOriginal = $hadOriginalRealmlist
|
||||
BackupPath = $realmlistBackupPath
|
||||
}
|
||||
Set-Content -LiteralPath $realmlistPath -Value "set realmlist $Realmlist" -Encoding ASCII
|
||||
Write-Host " Realmlist: $Realmlist"
|
||||
|
||||
$previousFiles = Get-EffectiveInterfaceFiles -SourceRoots $sourceRoots
|
||||
|
||||
if (-not $NoInitialSync) {
|
||||
Write-Host "Installing $($previousFiles.Count) loose Interface files..."
|
||||
foreach ($entry in $previousFiles.GetEnumerator()) {
|
||||
Install-HotReloadFile `
|
||||
-RelativePath $entry.Value.RelativePath `
|
||||
-SourcePath $entry.Value.SourcePath `
|
||||
-Quiet
|
||||
}
|
||||
Write-Host 'Initial Interface overlay is ready.' -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host 'Initial sync skipped; only subsequent changes will be overlaid.'
|
||||
}
|
||||
|
||||
if ($Launch) {
|
||||
Write-Host 'Launching Wow.exe...'
|
||||
$launchedProcess = Start-Process `
|
||||
-FilePath $wowExecutable `
|
||||
-WorkingDirectory $clientRoot `
|
||||
-PassThru
|
||||
Write-Host 'Watching until the launched client exits. Press Ctrl+C to stop earlier.'
|
||||
} else {
|
||||
Write-Host 'Watching for changes. Press Ctrl+C to stop and clean the client.'
|
||||
}
|
||||
|
||||
while ($true) {
|
||||
Start-Sleep -Milliseconds $PollIntervalMilliseconds
|
||||
|
||||
if ($launchedProcess) {
|
||||
$launchedProcess.Refresh()
|
||||
if ($launchedProcess.HasExited) {
|
||||
Write-Host 'The launched client has exited.'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
$currentFiles = Get-EffectiveInterfaceFiles -SourceRoots $sourceRoots
|
||||
|
||||
foreach ($entry in $currentFiles.GetEnumerator()) {
|
||||
$relativePath = $entry.Key
|
||||
$previous = $previousFiles[$relativePath]
|
||||
if ($null -eq $previous -or $previous.Signature -ne $entry.Value.Signature -or
|
||||
$previous.SourcePath -ne $entry.Value.SourcePath) {
|
||||
Install-HotReloadFile `
|
||||
-RelativePath $entry.Value.RelativePath `
|
||||
-SourcePath $entry.Value.SourcePath
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in $previousFiles.GetEnumerator()) {
|
||||
if (-not $currentFiles.ContainsKey($entry.Key) -and $ownedTargets.ContainsKey($entry.Key)) {
|
||||
Restore-HotReloadTarget -State $ownedTargets[$entry.Key]
|
||||
}
|
||||
}
|
||||
|
||||
$previousFiles = $currentFiles
|
||||
}
|
||||
} finally {
|
||||
Write-Host "Cleaning $($ownedTargets.Count) hot reload files from the client..."
|
||||
foreach ($state in $ownedTargets.Values) {
|
||||
try {
|
||||
Restore-HotReloadTarget -State $state -Quiet
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not clean Interface\$($state.RelativePath): $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $realmlistState) {
|
||||
try {
|
||||
if ($realmlistState.HadOriginal) {
|
||||
Copy-Item `
|
||||
-LiteralPath $realmlistState.BackupPath `
|
||||
-Destination $realmlistState.Path `
|
||||
-Force
|
||||
} elseif (Test-Path -LiteralPath $realmlistState.Path -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $realmlistState.Path -Force
|
||||
}
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not restore realmlist.wtf: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($directoryPath in @($createdRealmlistDirectories | Sort-Object Length -Descending)) {
|
||||
try {
|
||||
if ((Test-Path -LiteralPath $directoryPath -PathType Container) -and
|
||||
-not @(Get-ChildItem -LiteralPath $directoryPath -Force).Count) {
|
||||
Remove-Item -LiteralPath $directoryPath -Force
|
||||
}
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not remove empty directory $directoryPath"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($directoryPath in @($createdDirectories.Keys | Sort-Object Length -Descending)) {
|
||||
try {
|
||||
if ((Test-Path -LiteralPath $directoryPath -PathType Container) -and
|
||||
-not @(Get-ChildItem -LiteralPath $directoryPath -Force).Count) {
|
||||
Remove-Item -LiteralPath $directoryPath -Force
|
||||
}
|
||||
} catch {
|
||||
$cleanupFailed = $true
|
||||
Write-Warning "Could not remove empty directory $directoryPath"
|
||||
}
|
||||
}
|
||||
|
||||
if ($cleanupFailed) {
|
||||
Write-Warning "Cleanup was incomplete. Original-file backups were kept in: $backupRoot"
|
||||
} else {
|
||||
if (Test-Path -LiteralPath $backupRoot) {
|
||||
$resolvedBackupRoot = (Resolve-Path -LiteralPath $backupRoot).Path
|
||||
if (-not $resolvedBackupRoot.Equals(
|
||||
$backupRoot,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
) -or
|
||||
-not $resolvedBackupRoot.StartsWith(
|
||||
$systemTempPrefix,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Refusing to remove unexpected backup path: $resolvedBackupRoot"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolvedBackupRoot -Recurse -Force
|
||||
}
|
||||
Write-Host 'Hot reload overlay removed; original client files restored.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"repositoryPatches": [
|
||||
"Data/patch-4.MPQ",
|
||||
"Data/patch-Z.MPQ",
|
||||
"Data/ruRU/patch-ruRU-4.MPQ",
|
||||
"Data/ruRU/patch-ruRU-5.MPQ",
|
||||
"Data/ruRU/patch-ruRU-Z.MPQ"
|
||||
],
|
||||
"graphicsPatches": [
|
||||
"Data/patch-X.MPQ",
|
||||
"Data/ruRU/backup-ruRU.MPQ",
|
||||
"Data/ruRU/patch-ruRU-6.MPQ",
|
||||
"Data/ruRU/patch-ruRU-A.mpq",
|
||||
"Data/ruRU/patch-ruRU-B.mpq",
|
||||
"Data/ruRU/patch-ruRU-E.mpq",
|
||||
"Data/ruRU/patch-ruRU-H.MPQ",
|
||||
"Data/ruRU/patch-ruRU-I.mpq",
|
||||
"Data/ruRU/patch-ruRU-M.MPQ",
|
||||
"Data/ruRU/patch-ruRU-S.mpq",
|
||||
"Data/ruRU/patch-ruRU-T.mpq",
|
||||
"Data/ruRU/patch-ruRU-U.mpq",
|
||||
"Data/ruRU/patch-ruRU-W.mpq",
|
||||
"Data/ruRU/patch-ruRU-X.MPQ",
|
||||
"Data/ruRU/patch-ruRU-Y.MPQ"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def normalized_path(path: str) -> str:
|
||||
return path.replace("\\", "/").casefold()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatchLayout:
|
||||
repository_patches: frozenset[str]
|
||||
graphics_patches: frozenset[str]
|
||||
|
||||
def is_repository_patch(self, path: str) -> bool:
|
||||
return normalized_path(path) in self.repository_patches
|
||||
|
||||
def is_graphics_patch(self, path: str) -> bool:
|
||||
return normalized_path(path) in self.graphics_patches
|
||||
|
||||
def is_external_graphics_patch(self, path: str) -> bool:
|
||||
key = normalized_path(path)
|
||||
return key in self.graphics_patches and key not in self.repository_patches
|
||||
|
||||
|
||||
def load_patch_layout(path: Path | None = None) -> PatchLayout:
|
||||
config_path = path or Path(__file__).resolve().with_name("patch-layout.json")
|
||||
data = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
repository = frozenset(normalized_path(item) for item in data["repositoryPatches"])
|
||||
graphics = frozenset(normalized_path(item) for item in data["graphicsPatches"])
|
||||
return PatchLayout(repository_patches=repository, graphics_patches=graphics)
|
||||
|
||||
|
||||
PATCH_LAYOUT = load_patch_layout()
|
||||
@@ -0,0 +1,241 @@
|
||||
$script:MoonWellExtensionNames = @(
|
||||
'MoonWell',
|
||||
'wxl-db2',
|
||||
'wxl-fdid-moonwell',
|
||||
'wxl-grasswind',
|
||||
'wxl-modern-adt',
|
||||
'wxl-modern-blp',
|
||||
'wxl-modern-m2',
|
||||
'wxl-modern-wmo',
|
||||
'wxl-moonwell-storage-fallback',
|
||||
'wxl-unit-outline'
|
||||
)
|
||||
|
||||
$patchLayoutPath = Join-Path $PSScriptRoot 'patch-layout.json'
|
||||
if (-not (Test-Path -LiteralPath $patchLayoutPath -PathType Leaf)) {
|
||||
throw "Patch layout was not found: $patchLayoutPath"
|
||||
}
|
||||
$patchLayout = Get-Content -LiteralPath $patchLayoutPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$script:MoonWellRepositoryPatches = @($patchLayout.repositoryPatches)
|
||||
$script:MoonWellGraphicsPatches = @($patchLayout.graphicsPatches)
|
||||
|
||||
function Get-MoonWellExtensionNames {
|
||||
return @($script:MoonWellExtensionNames)
|
||||
}
|
||||
|
||||
function Get-MoonWellRepositoryPatchPaths {
|
||||
return @($script:MoonWellRepositoryPatches)
|
||||
}
|
||||
|
||||
function Get-MoonWellGraphicsPatchPaths {
|
||||
return @($script:MoonWellGraphicsPatches)
|
||||
}
|
||||
|
||||
function Test-MoonWellRepositoryPatch {
|
||||
param([Parameter(Mandatory)][string]$RelativePath)
|
||||
|
||||
foreach ($repositoryPatch in $script:MoonWellRepositoryPatches) {
|
||||
if ($RelativePath.Replace('/', '\').Equals(
|
||||
$repositoryPatch.Replace('/', '\'),
|
||||
[System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-MoonWellGraphicsPatch {
|
||||
param([Parameter(Mandatory)][string]$RelativePath)
|
||||
|
||||
foreach ($graphicsPatch in $script:MoonWellGraphicsPatches) {
|
||||
if ($RelativePath.Replace('/', '\').Equals(
|
||||
$graphicsPatch.Replace('/', '\'),
|
||||
[System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-MoonWellPreservedDataPatch {
|
||||
param([Parameter(Mandatory)][string]$RelativePath)
|
||||
|
||||
if (Test-MoonWellRepositoryPatch -RelativePath $RelativePath) {
|
||||
return $false
|
||||
}
|
||||
return Test-MoonWellGraphicsPatch -RelativePath $RelativePath
|
||||
}
|
||||
|
||||
function Assert-SafePackagePath {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$PackagePath,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$RepositoryPath,
|
||||
[Parameter()]
|
||||
[string]$ClientPath
|
||||
)
|
||||
|
||||
$packageRoot = [System.IO.Path]::GetFullPath($PackagePath).TrimEnd('\')
|
||||
$repositoryRoot = [System.IO.Path]::GetFullPath($RepositoryPath).TrimEnd('\')
|
||||
$pathRoot = [System.IO.Path]::GetPathRoot($packageRoot).TrimEnd('\')
|
||||
|
||||
if (-not $packageRoot -or $packageRoot -eq $pathRoot) {
|
||||
throw "Refusing to use a filesystem root as package staging: $packageRoot"
|
||||
}
|
||||
if ($packageRoot.Equals($repositoryRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$repositoryRoot.StartsWith($packageRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw 'Package staging must not be the repository root or one of its ancestors.'
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($ClientPath)) {
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
|
||||
$packageIsClientOrChild =
|
||||
$packageRoot.Equals($clientRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
$packageRoot.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)
|
||||
$packageContainsClient =
|
||||
$clientRoot.StartsWith($packageRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)
|
||||
if ($packageIsClientOrChild -or $packageContainsClient) {
|
||||
throw 'Package staging must be separate from the installed client tree.'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RelativeFilePaths {
|
||||
param([Parameter(Mandatory)][string]$Root)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Root -PathType Container)) {
|
||||
return @()
|
||||
}
|
||||
|
||||
$absoluteRoot = [System.IO.Path]::GetFullPath($Root).TrimEnd('\')
|
||||
return @(
|
||||
Get-ChildItem -LiteralPath $absoluteRoot -Recurse -Force -File |
|
||||
ForEach-Object { $_.FullName.Substring($absoluteRoot.Length + 1) }
|
||||
)
|
||||
}
|
||||
|
||||
function Remove-EmptyMoonWellDirectories {
|
||||
param([Parameter(Mandatory)][string]$Destination)
|
||||
|
||||
foreach ($relativeRoot in @('Data\Patch-WXL.MPQ', 'Extensions', 'Utils')) {
|
||||
$managedRoot = Join-Path $Destination $relativeRoot
|
||||
if (-not (Test-Path -LiteralPath $managedRoot -PathType Container)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$directories = @(
|
||||
Get-ChildItem -LiteralPath $managedRoot -Recurse -Force -Directory |
|
||||
Sort-Object { $_.FullName.Length } -Descending
|
||||
)
|
||||
$directories += Get-Item -LiteralPath $managedRoot
|
||||
|
||||
foreach ($directory in $directories) {
|
||||
if (-not @(Get-ChildItem -LiteralPath $directory.FullName -Force).Count) {
|
||||
Remove-Item -LiteralPath $directory.FullName -Force
|
||||
Write-Host "Removed empty managed directory: $($directory.FullName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Reset-MoonWellPackageStaging {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$PackagePath,
|
||||
[Parameter(Mandatory)][string]$RepositoryPath,
|
||||
[Parameter(Mandatory)][string]$ClientPath
|
||||
)
|
||||
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $PackagePath `
|
||||
-RepositoryPath $RepositoryPath `
|
||||
-ClientPath $ClientPath
|
||||
|
||||
if (Test-Path -LiteralPath $PackagePath) {
|
||||
Remove-Item -LiteralPath $PackagePath -Recurse -Force
|
||||
}
|
||||
New-Item -ItemType Directory -Path $PackagePath -Force | Out-Null
|
||||
}
|
||||
|
||||
function Remove-MoonWellRuntimeGarbage {
|
||||
param([Parameter(Mandatory)][string]$Destination)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Destination -PathType Container)) {
|
||||
return
|
||||
}
|
||||
|
||||
$allowedExtensions = @{}
|
||||
foreach ($name in $script:MoonWellExtensionNames) {
|
||||
$allowedExtensions[$name.ToLowerInvariant()] = $true
|
||||
}
|
||||
|
||||
$extensionsRoot = Join-Path $Destination 'Extensions'
|
||||
if (Test-Path -LiteralPath $extensionsRoot -PathType Container) {
|
||||
foreach ($entry in Get-ChildItem -LiteralPath $extensionsRoot -Force) {
|
||||
$isAllowedDirectory = $entry.PSIsContainer -and
|
||||
$allowedExtensions.ContainsKey($entry.Name.ToLowerInvariant())
|
||||
if (-not $isAllowedDirectory) {
|
||||
Remove-Item -LiteralPath $entry.FullName -Recurse -Force
|
||||
Write-Host "Removed unmanaged WarcraftXL extension artifact: $($entry.FullName)"
|
||||
continue
|
||||
}
|
||||
|
||||
foreach ($dll in Get-ChildItem -LiteralPath $entry.FullName -Force -File -Filter '*.dll') {
|
||||
$expectedName = "$($entry.Name).dll"
|
||||
if (-not $dll.Name.Equals($expectedName, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
Remove-Item -LiteralPath $dll.FullName -Force
|
||||
Write-Host "Removed stale extension DLL: $($dll.FullName)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dll in Get-ChildItem -LiteralPath $Destination -Force -File -Filter 'WarcraftXL*.dll') {
|
||||
if (-not $dll.Name.Equals('WarcraftXL.dll', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
Remove-Item -LiteralPath $dll.FullName -Force
|
||||
Write-Host "Removed stale WarcraftXL DLL: $($dll.FullName)"
|
||||
}
|
||||
}
|
||||
|
||||
$utilsRoot = Join-Path $Destination 'Utils'
|
||||
if (Test-Path -LiteralPath $utilsRoot -PathType Container) {
|
||||
foreach ($legacyFile in Get-ChildItem -LiteralPath $utilsRoot -Force -File -Filter 'WarcraftXLHost*') {
|
||||
Remove-Item -LiteralPath $legacyFile.FullName -Force
|
||||
Write-Host "Removed obsolete WarcraftXL 1.0 host artifact: $($legacyFile.FullName)"
|
||||
}
|
||||
}
|
||||
|
||||
Remove-EmptyMoonWellDirectories -Destination $Destination
|
||||
}
|
||||
|
||||
function Remove-StalePackageFilesFromClient {
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$ClientPath,
|
||||
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$PreviousFiles,
|
||||
[Parameter(Mandatory)][AllowEmptyCollection()][string[]]$CurrentFiles
|
||||
)
|
||||
|
||||
$current = @{}
|
||||
foreach ($relativePath in $CurrentFiles) {
|
||||
$current[$relativePath.ToLowerInvariant()] = $true
|
||||
}
|
||||
|
||||
$clientRoot = [System.IO.Path]::GetFullPath($ClientPath).TrimEnd('\')
|
||||
foreach ($relativePath in $PreviousFiles) {
|
||||
if ($current.ContainsKey($relativePath.ToLowerInvariant()) -or
|
||||
(Test-MoonWellPreservedDataPatch -RelativePath $relativePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$target = [System.IO.Path]::GetFullPath((Join-Path $clientRoot $relativePath))
|
||||
if (-not $target.StartsWith($clientRoot + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Package inventory contains a path outside the client: $relativePath"
|
||||
}
|
||||
if (Test-Path -LiteralPath $target -PathType Leaf) {
|
||||
Remove-Item -LiteralPath $target -Force
|
||||
Write-Host "Removed stale packaged file from client: $target"
|
||||
}
|
||||
}
|
||||
|
||||
Remove-EmptyMoonWellDirectories -Destination $ClientPath
|
||||
}
|
||||
@@ -9,6 +9,7 @@ $ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Resolve project root (folder where script is located)
|
||||
$ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. (Join-Path $ROOT 'pipeline-layout.ps1')
|
||||
|
||||
# --- Load .env if WOW_HOME not already set
|
||||
if (-not $env:WOW_HOME) {
|
||||
@@ -80,6 +81,11 @@ $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"
|
||||
|
||||
Assert-SafePackagePath `
|
||||
-PackagePath $DIST_DIR `
|
||||
-RepositoryPath $ROOT `
|
||||
-ClientPath $WOW_HOME
|
||||
|
||||
function Stop-WowRuntime {
|
||||
param([Parameter(Mandatory=$true)][string]$ClientPath)
|
||||
|
||||
@@ -118,7 +124,14 @@ if (!(Test-Path $TOOL)) {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# --- Build MPQ archives into dist/
|
||||
# --- Build MPQ archives into a clean dist/
|
||||
$previousPackageFiles = @(
|
||||
Get-RelativeFilePaths -Root $DIST_DIR
|
||||
)
|
||||
Reset-MoonWellPackageStaging `
|
||||
-PackagePath $DIST_DIR `
|
||||
-RepositoryPath $ROOT `
|
||||
-ClientPath $WOW_HOME
|
||||
Write-Host "Building MPQ archives from src/ -> dist/..."
|
||||
& $TOOL $SRC_DIR $DIST_DIR
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
@@ -145,6 +158,22 @@ if ($LASTEXITCODE -ne 0) {
|
||||
|
||||
# --- Sync dist/ -> WOW_HOME
|
||||
Write-Host "Syncing dist/ -> WOW_HOME..."
|
||||
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||
if (-not $python) {
|
||||
Write-Error "Python is required for package validation."
|
||||
exit 1
|
||||
}
|
||||
& $python.Source (Join-Path $ROOT 'validate_package.py') --dir $DIST_DIR
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Package validation failed!"
|
||||
exit 1
|
||||
}
|
||||
$currentPackageFiles = @(Get-RelativeFilePaths -Root $DIST_DIR)
|
||||
Remove-StalePackageFilesFromClient `
|
||||
-ClientPath $WOW_HOME `
|
||||
-PreviousFiles $previousPackageFiles `
|
||||
-CurrentFiles $currentPackageFiles
|
||||
Remove-MoonWellRuntimeGarbage -Destination $WOW_HOME
|
||||
robocopy $DIST_DIR $WOW_HOME /E /NFL /NDL
|
||||
# robocopy exit codes 0-7 are success/warnings; 8+ are errors
|
||||
if ($LASTEXITCODE -ge 8) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
## Interface: 30300
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Blizzard Classic API Emulation
|
||||
## Version: 1.1.64
|
||||
## Author: Tsoukie
|
||||
## DefaultState: Enabled
|
||||
## X-MoonWell-System: 1
|
||||
## X-Upstream-Commit: b3da3000bba23a2c31aa9c9b7c0232d5973789e6
|
||||
Lib\Load.xml
|
||||
Load.xml
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
--[[ $Id: CallbackHandler-1.0.lua 18 2014-10-16 02:52:20Z mikk $ ]]
|
||||
local MAJOR, MINOR = "CallbackHandler-1.0", 7
|
||||
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
|
||||
|
||||
if not CallbackHandler then return end -- No upgrade needed
|
||||
|
||||
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
|
||||
|
||||
-- Lua APIs
|
||||
local tconcat = table.concat
|
||||
local assert, error, loadstring = assert, error, loadstring
|
||||
local setmetatable, rawset, rawget = setmetatable, rawset, rawget
|
||||
local next, select, pairs, type, tostring = next, select, pairs, type, tostring
|
||||
|
||||
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
|
||||
-- List them here for Mikk's FindGlobals script
|
||||
-- GLOBALS: geterrorhandler
|
||||
|
||||
local xpcall = xpcall
|
||||
|
||||
local function errorhandler(err)
|
||||
return geterrorhandler()(err)
|
||||
end
|
||||
|
||||
local function CreateDispatcher(argCount)
|
||||
local code = [[
|
||||
local next, xpcall, eh = ...
|
||||
|
||||
local method, ARGS
|
||||
local function call() method(ARGS) end
|
||||
|
||||
local function dispatch(handlers, ...)
|
||||
local index
|
||||
index, method = next(handlers)
|
||||
if not method then return end
|
||||
local OLD_ARGS = ARGS
|
||||
ARGS = ...
|
||||
repeat
|
||||
xpcall(call, eh)
|
||||
index, method = next(handlers, index)
|
||||
until not method
|
||||
ARGS = OLD_ARGS
|
||||
end
|
||||
|
||||
return dispatch
|
||||
]]
|
||||
|
||||
local ARGS, OLD_ARGS = {}, {}
|
||||
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
|
||||
code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", "))
|
||||
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
|
||||
end
|
||||
|
||||
local Dispatchers = setmetatable({}, {__index=function(self, argCount)
|
||||
local dispatcher = CreateDispatcher(argCount)
|
||||
rawset(self, argCount, dispatcher)
|
||||
return dispatcher
|
||||
end})
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- CallbackHandler:New
|
||||
--
|
||||
-- target - target object to embed public APIs in
|
||||
-- RegisterName - name of the callback registration API, default "RegisterCallback"
|
||||
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
|
||||
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
|
||||
|
||||
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)
|
||||
|
||||
RegisterName = RegisterName or "RegisterCallback"
|
||||
UnregisterName = UnregisterName or "UnregisterCallback"
|
||||
if UnregisterAllName==nil then -- false is used to indicate "don't want this method"
|
||||
UnregisterAllName = "UnregisterAllCallbacks"
|
||||
end
|
||||
|
||||
-- we declare all objects and exported APIs inside this closure to quickly gain access
|
||||
-- to e.g. function names, the "target" parameter, etc
|
||||
|
||||
|
||||
-- Create the registry object
|
||||
local events = setmetatable({}, meta)
|
||||
local registry = { recurse=0, events=events }
|
||||
|
||||
-- registry:Fire() - fires the given event/message into the registry
|
||||
function registry:Fire(eventname, ...)
|
||||
if not rawget(events, eventname) or not next(events[eventname]) then return end
|
||||
local oldrecurse = registry.recurse
|
||||
registry.recurse = oldrecurse + 1
|
||||
|
||||
Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
|
||||
|
||||
registry.recurse = oldrecurse
|
||||
|
||||
if registry.insertQueue and oldrecurse==0 then
|
||||
-- Something in one of our callbacks wanted to register more callbacks; they got queued
|
||||
for eventname,callbacks in pairs(registry.insertQueue) do
|
||||
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
|
||||
for self,func in pairs(callbacks) do
|
||||
events[eventname][self] = func
|
||||
-- fire OnUsed callback?
|
||||
if first and registry.OnUsed then
|
||||
registry.OnUsed(registry, target, eventname)
|
||||
first = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
registry.insertQueue = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Registration of a callback, handles:
|
||||
-- self["method"], leads to self["method"](self, ...)
|
||||
-- self with function ref, leads to functionref(...)
|
||||
-- "addonId" (instead of self) with function ref, leads to functionref(...)
|
||||
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
|
||||
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
|
||||
if type(eventname) ~= "string" then
|
||||
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
|
||||
end
|
||||
|
||||
method = method or eventname
|
||||
|
||||
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten.
|
||||
|
||||
if type(method) ~= "string" and type(method) ~= "function" then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
|
||||
end
|
||||
|
||||
local regfunc
|
||||
|
||||
if type(method) == "string" then
|
||||
-- self["method"] calling style
|
||||
if type(self) ~= "table" then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
|
||||
elseif self==target then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
|
||||
elseif type(self[method]) ~= "function" then
|
||||
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
|
||||
end
|
||||
|
||||
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
|
||||
local arg=select(1,...)
|
||||
regfunc = function(...) self[method](self,arg,...) end
|
||||
else
|
||||
regfunc = function(...) self[method](self,...) end
|
||||
end
|
||||
else
|
||||
-- function ref with self=object or self="addonId" or self=thread
|
||||
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
|
||||
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
|
||||
end
|
||||
|
||||
if select("#",...)>=1 then -- this is not the same as testing for arg==nil!
|
||||
local arg=select(1,...)
|
||||
regfunc = function(...) method(arg,...) end
|
||||
else
|
||||
regfunc = method
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if events[eventname][self] or registry.recurse<1 then
|
||||
-- if registry.recurse<1 then
|
||||
-- we're overwriting an existing entry, or not currently recursing. just set it.
|
||||
events[eventname][self] = regfunc
|
||||
-- fire OnUsed callback?
|
||||
if registry.OnUsed and first then
|
||||
registry.OnUsed(registry, target, eventname)
|
||||
end
|
||||
else
|
||||
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
|
||||
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
|
||||
registry.insertQueue = registry.insertQueue or setmetatable({},meta)
|
||||
registry.insertQueue[eventname][self] = regfunc
|
||||
end
|
||||
end
|
||||
|
||||
-- Unregister a callback
|
||||
target[UnregisterName] = function(self, eventname)
|
||||
if not self or self==target then
|
||||
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
|
||||
end
|
||||
if type(eventname) ~= "string" then
|
||||
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
|
||||
end
|
||||
if rawget(events, eventname) and events[eventname][self] then
|
||||
events[eventname][self] = nil
|
||||
-- Fire OnUnused callback?
|
||||
if registry.OnUnused and not next(events[eventname]) then
|
||||
registry.OnUnused(registry, target, eventname)
|
||||
end
|
||||
end
|
||||
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
|
||||
registry.insertQueue[eventname][self] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
|
||||
if UnregisterAllName then
|
||||
target[UnregisterAllName] = function(...)
|
||||
if select("#",...)<1 then
|
||||
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
|
||||
end
|
||||
if select("#",...)==1 and ...==target then
|
||||
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
|
||||
end
|
||||
|
||||
|
||||
for i=1,select("#",...) do
|
||||
local self = select(i,...)
|
||||
if registry.insertQueue then
|
||||
for eventname, callbacks in pairs(registry.insertQueue) do
|
||||
if callbacks[self] then
|
||||
callbacks[self] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
for eventname, callbacks in pairs(events) do
|
||||
if callbacks[self] then
|
||||
callbacks[self] = nil
|
||||
-- Fire OnUnused callback?
|
||||
if registry.OnUnused and not next(callbacks) then
|
||||
registry.OnUnused(registry, target, eventname)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return registry
|
||||
end
|
||||
|
||||
|
||||
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
|
||||
-- try to upgrade old implicit embeds since the system is selfcontained and
|
||||
-- relies on closures to work.
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<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="CallbackHandler-1.0.lua"/>
|
||||
</Ui>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
-- $Id: LibStub.lua 103 2014-10-16 03:02:50Z mikk $
|
||||
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/addons/libstub/ for more info
|
||||
-- LibStub is hereby placed in the Public Domain
|
||||
-- Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke
|
||||
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 3 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS!
|
||||
local LibStub = _G[LIBSTUB_MAJOR]
|
||||
|
||||
-- Check to see is this version of the stub is obsolete
|
||||
if not LibStub or LibStub.minor < LIBSTUB_MINOR then
|
||||
LibStub = LibStub or {libs = {}, minors = {} }
|
||||
_G[LIBSTUB_MAJOR] = LibStub
|
||||
LibStub.minor = LIBSTUB_MINOR
|
||||
|
||||
-- LibStub:NewLibrary(major, minor)
|
||||
-- major (string) - the major version of the library
|
||||
-- minor (string or number ) - the minor version of the library
|
||||
--
|
||||
-- returns nil if a newer or same version of the lib is already present
|
||||
-- returns empty library object or old library object if upgrade is needed
|
||||
function LibStub:NewLibrary(major, minor)
|
||||
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)")
|
||||
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.")
|
||||
|
||||
local oldminor = self.minors[major]
|
||||
if oldminor and oldminor >= minor then return nil end
|
||||
self.minors[major], self.libs[major] = minor, self.libs[major] or {}
|
||||
return self.libs[major], oldminor
|
||||
end
|
||||
|
||||
-- LibStub:GetLibrary(major, [silent])
|
||||
-- major (string) - the major version of the library
|
||||
-- silent (boolean) - if true, library is optional, silently return nil if its not found
|
||||
--
|
||||
-- throws an error if the library can not be found (except silent is set)
|
||||
-- returns the library object if found
|
||||
function LibStub:GetLibrary(major, silent)
|
||||
if not self.libs[major] and not silent then
|
||||
error(("Cannot find a library instance of %q."):format(tostring(major)), 2)
|
||||
end
|
||||
return self.libs[major], self.minors[major]
|
||||
end
|
||||
|
||||
-- LibStub:IterateLibraries()
|
||||
--
|
||||
-- Returns an iterator for the currently registered libraries
|
||||
function LibStub:IterateLibraries()
|
||||
return pairs(self.libs)
|
||||
end
|
||||
|
||||
setmetatable(LibStub, { __call = LibStub.GetLibrary })
|
||||
end
|
||||
@@ -0,0 +1,523 @@
|
||||
--
|
||||
-- ChatThrottleLib by Mikk
|
||||
--
|
||||
-- Manages AddOn chat output to keep player from getting kicked off.
|
||||
--
|
||||
-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
|
||||
-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
|
||||
--
|
||||
-- Priorities get an equal share of available bandwidth when fully loaded.
|
||||
-- Communication channels are separated on extension+chattype+destination and
|
||||
-- get round-robinned. (Destination only matters for whispers and channels,
|
||||
-- obviously)
|
||||
--
|
||||
-- Will install hooks for SendChatMessage and SendAddonMessage to measure
|
||||
-- bandwidth bypassing the library and use less bandwidth itself.
|
||||
--
|
||||
--
|
||||
-- Fully embeddable library. Just copy this file into your addon directory,
|
||||
-- add it to the .toc, and it's done.
|
||||
--
|
||||
-- Can run as a standalone addon also, but, really, just embed it! :-)
|
||||
--
|
||||
-- LICENSE: ChatThrottleLib is released into the Public Domain
|
||||
--
|
||||
|
||||
local CTL_VERSION = 25
|
||||
|
||||
local _G = _G
|
||||
|
||||
if _G.ChatThrottleLib then
|
||||
if _G.ChatThrottleLib.version >= CTL_VERSION then
|
||||
-- There's already a newer (or same) version loaded. Buh-bye.
|
||||
return
|
||||
elseif not _G.ChatThrottleLib.securelyHooked then
|
||||
print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, <v16) in an addon somewhere. Get the addon updated or copy in a newer ChatThrottleLib.lua (>=v16) in it!")
|
||||
-- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
|
||||
-- ... and if someone has securehooked, they can kiss that goodbye too... >.<
|
||||
_G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
|
||||
if _G.ChatThrottleLib.ORIG_SendAddonMessage then
|
||||
_G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
|
||||
end
|
||||
end
|
||||
_G.ChatThrottleLib.ORIG_SendChatMessage = nil
|
||||
_G.ChatThrottleLib.ORIG_SendAddonMessage = nil
|
||||
end
|
||||
|
||||
if not _G.ChatThrottleLib then
|
||||
_G.ChatThrottleLib = {}
|
||||
end
|
||||
|
||||
ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
|
||||
local ChatThrottleLib = _G.ChatThrottleLib
|
||||
|
||||
ChatThrottleLib.version = CTL_VERSION
|
||||
|
||||
|
||||
|
||||
------------------ TWEAKABLES -----------------
|
||||
|
||||
ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
|
||||
ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
|
||||
|
||||
ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
|
||||
|
||||
ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
|
||||
|
||||
|
||||
local setmetatable = setmetatable
|
||||
local table_remove = table.remove
|
||||
local tostring = tostring
|
||||
local GetTime = GetTime
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local next = next
|
||||
local strlen = string.len
|
||||
local GetFramerate = GetFramerate
|
||||
local strlower = string.lower
|
||||
local unpack,type,pairs,wipe = unpack,type,pairs,wipe
|
||||
local UnitInRaid,GetNumPartyMembers = UnitInRaid,GetNumPartyMembers
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Double-linked ring implementation
|
||||
|
||||
local Ring = {}
|
||||
local RingMeta = { __index = Ring }
|
||||
|
||||
function Ring:New()
|
||||
local ret = {}
|
||||
setmetatable(ret, RingMeta)
|
||||
return ret
|
||||
end
|
||||
|
||||
function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
|
||||
if self.pos then
|
||||
obj.prev = self.pos.prev
|
||||
obj.prev.next = obj
|
||||
obj.next = self.pos
|
||||
obj.next.prev = obj
|
||||
else
|
||||
obj.next = obj
|
||||
obj.prev = obj
|
||||
self.pos = obj
|
||||
end
|
||||
end
|
||||
|
||||
function Ring:Remove(obj)
|
||||
obj.next.prev = obj.prev
|
||||
obj.prev.next = obj.next
|
||||
if self.pos == obj then
|
||||
self.pos = obj.next
|
||||
if self.pos == obj then
|
||||
self.pos = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Recycling bin for pipes
|
||||
-- A pipe is a plain integer-indexed queue of messages
|
||||
-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
|
||||
|
||||
ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
|
||||
local PipeBin = setmetatable({}, {__mode="k"})
|
||||
|
||||
local function DelPipe(pipe)
|
||||
for i = #pipe, 1, -1 do
|
||||
pipe[i] = nil
|
||||
end
|
||||
pipe.prev = nil
|
||||
pipe.next = nil
|
||||
|
||||
PipeBin[pipe] = true
|
||||
end
|
||||
|
||||
local function NewPipe()
|
||||
local pipe = next(PipeBin)
|
||||
if pipe then
|
||||
wipe(pipe)
|
||||
PipeBin[pipe] = nil
|
||||
return pipe
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Recycling bin for messages
|
||||
|
||||
ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
|
||||
local MsgBin = setmetatable({}, {__mode="k"})
|
||||
|
||||
local function DelMsg(msg)
|
||||
msg[1] = nil
|
||||
-- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
|
||||
MsgBin[msg] = true
|
||||
end
|
||||
|
||||
local function NewMsg()
|
||||
local msg = next(MsgBin)
|
||||
if msg then
|
||||
MsgBin[msg] = nil
|
||||
return msg
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib:Init
|
||||
-- Initialize queues, set up frame for OnUpdate, etc
|
||||
|
||||
|
||||
function ChatThrottleLib:Init()
|
||||
|
||||
-- Set up queues
|
||||
if not self.Prio then
|
||||
self.Prio = {}
|
||||
self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
end
|
||||
|
||||
-- v4: total send counters per priority
|
||||
for _, Prio in pairs(self.Prio) do
|
||||
Prio.nTotalSent = Prio.nTotalSent or 0
|
||||
end
|
||||
|
||||
if not self.avail then
|
||||
self.avail = 0 -- v5
|
||||
end
|
||||
if not self.nTotalSent then
|
||||
self.nTotalSent = 0 -- v5
|
||||
end
|
||||
|
||||
|
||||
-- Set up a frame to get OnUpdate events
|
||||
if not self.Frame then
|
||||
self.Frame = CreateFrame("Frame")
|
||||
self.Frame:Hide()
|
||||
end
|
||||
self.Frame:SetScript("OnUpdate", self.OnUpdate)
|
||||
self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
|
||||
self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
self.OnUpdateDelay = 0
|
||||
self.LastAvailUpdate = GetTime()
|
||||
self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
|
||||
|
||||
-- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
|
||||
if not self.securelyHooked then
|
||||
-- Use secure hooks as of v16. Old regular hook support yanked out in v21.
|
||||
self.securelyHooked = true
|
||||
--SendChatMessage
|
||||
hooksecurefunc("SendChatMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendChatMessage(...)
|
||||
end)
|
||||
--SendAddonMessage
|
||||
hooksecurefunc("SendAddonMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendAddonMessage(...)
|
||||
end)
|
||||
end
|
||||
self.nBypass = 0
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
|
||||
|
||||
local bMyTraffic = false
|
||||
|
||||
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = tostring(text or ""):len() + tostring(prefix or ""):len();
|
||||
size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib:UpdateAvail
|
||||
-- Update self.avail with how much bandwidth is currently available
|
||||
|
||||
function ChatThrottleLib:UpdateAvail()
|
||||
local now = GetTime()
|
||||
local MAX_CPS = self.MAX_CPS;
|
||||
local newavail = MAX_CPS * (now - self.LastAvailUpdate)
|
||||
local avail = self.avail
|
||||
|
||||
if now - self.HardThrottlingBeginTime < 5 then
|
||||
-- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
|
||||
avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
|
||||
self.bChoking = true
|
||||
elseif GetFramerate() < self.MIN_FPS then -- GetFramerate call takes ~0.002 secs
|
||||
avail = math_min(MAX_CPS, avail + newavail*0.5)
|
||||
self.bChoking = true -- just a statistic
|
||||
else
|
||||
avail = math_min(self.BURST, avail + newavail)
|
||||
self.bChoking = false
|
||||
end
|
||||
|
||||
avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
|
||||
|
||||
self.avail = avail
|
||||
self.LastAvailUpdate = now
|
||||
|
||||
return avail
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Despooling logic
|
||||
-- Reminder:
|
||||
-- - We have 3 Priorities, each containing a "Ring" construct ...
|
||||
-- - ... made up of N "Pipe"s (1 for each destination/pipename)
|
||||
-- - and each pipe contains messages
|
||||
|
||||
function ChatThrottleLib:Despool(Prio)
|
||||
local ring = Prio.Ring
|
||||
while ring.pos and Prio.avail > ring.pos[1].nSize do
|
||||
local msg = table_remove(ring.pos, 1)
|
||||
if not ring.pos[1] then -- did we remove last msg in this pipe?
|
||||
local pipe = Prio.Ring.pos
|
||||
Prio.Ring:Remove(pipe)
|
||||
Prio.ByName[pipe.name] = nil
|
||||
DelPipe(pipe)
|
||||
else
|
||||
Prio.Ring.pos = Prio.Ring.pos.next
|
||||
end
|
||||
local didSend=false
|
||||
local lowerDest = strlower(msg[3] or "")
|
||||
if lowerDest == "raid" and not UnitInRaid("player") then
|
||||
-- do nothing
|
||||
elseif lowerDest == "party" and GetNumPartyMembers() == 0 then
|
||||
-- do nothing
|
||||
else
|
||||
Prio.avail = Prio.avail - msg.nSize
|
||||
bMyTraffic = true
|
||||
msg.f(unpack(msg, 1, msg.n))
|
||||
bMyTraffic = false
|
||||
Prio.nTotalSent = Prio.nTotalSent + msg.nSize
|
||||
DelMsg(msg)
|
||||
didSend = true
|
||||
end
|
||||
-- notify caller of delivery (even if we didn't send it)
|
||||
if msg.callbackFn then
|
||||
msg.callbackFn (msg.callbackArg, didSend)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib.OnEvent(this,event)
|
||||
-- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
|
||||
local self = ChatThrottleLib
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
|
||||
self.avail = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib.OnUpdate(this,delay)
|
||||
local self = ChatThrottleLib
|
||||
|
||||
self.OnUpdateDelay = self.OnUpdateDelay + delay
|
||||
if self.OnUpdateDelay < 0.08 then
|
||||
return
|
||||
end
|
||||
self.OnUpdateDelay = 0
|
||||
|
||||
self:UpdateAvail()
|
||||
|
||||
if self.avail < 0 then
|
||||
return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
|
||||
end
|
||||
|
||||
-- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
|
||||
local n = 0
|
||||
for prioname,Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Anything queued still?
|
||||
if n<1 then
|
||||
-- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
self.avail = self.avail + Prio.avail
|
||||
Prio.avail = 0
|
||||
end
|
||||
self.bQueueing = false
|
||||
self.Frame:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
|
||||
local avail = self.avail/n
|
||||
self.avail = 0
|
||||
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
Prio.avail = Prio.avail + avail
|
||||
if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
|
||||
self:Despool(Prio)
|
||||
-- Note: We might not get here if the user-supplied callback function errors out! Take care!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Spooling logic
|
||||
|
||||
function ChatThrottleLib:Enqueue(prioname, pipename, msg)
|
||||
local Prio = self.Prio[prioname]
|
||||
local pipe = Prio.ByName[pipename]
|
||||
if not pipe then
|
||||
self.Frame:Show()
|
||||
pipe = NewPipe()
|
||||
pipe.name = pipename
|
||||
Prio.ByName[pipename] = pipe
|
||||
Prio.Ring:Add(pipe)
|
||||
end
|
||||
|
||||
pipe[#pipe + 1] = msg
|
||||
|
||||
self.bQueueing = true
|
||||
end
|
||||
|
||||
function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
|
||||
if not self or not prio or not prefix or not text or not self.Prio[prio] then
|
||||
error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = text:len()
|
||||
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
|
||||
end
|
||||
|
||||
nSize = nSize + self.MSG_OVERHEAD
|
||||
|
||||
-- Check if there's room in the global available bandwidth gauge to send directly
|
||||
if not self.bQueueing and nSize < self:UpdateAvail() then
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendChatMessage(text, chattype, language, destination)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendChatMessage
|
||||
msg[1] = text
|
||||
msg[2] = chattype or "SAY"
|
||||
msg[3] = language
|
||||
msg[4] = destination
|
||||
msg.n = 4
|
||||
msg.nSize = nSize
|
||||
msg.callbackFn = callbackFn
|
||||
msg.callbackArg = callbackArg
|
||||
|
||||
self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
|
||||
if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
|
||||
error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = prefix:len() + 1 + text:len();
|
||||
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
|
||||
end
|
||||
|
||||
nSize = nSize + self.MSG_OVERHEAD;
|
||||
|
||||
-- Check if there's room in the global available bandwidth gauge to send directly
|
||||
if not self.bQueueing and nSize < self:UpdateAvail() then
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendAddonMessage(prefix, text, chattype, target)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendAddonMessage
|
||||
msg[1] = prefix
|
||||
msg[2] = text
|
||||
msg[3] = chattype
|
||||
msg[4] = target
|
||||
msg.n = (target~=nil) and 4 or 3;
|
||||
msg.nSize = nSize
|
||||
msg.callbackFn = callbackFn
|
||||
msg.callbackArg = callbackArg
|
||||
|
||||
self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Get the ball rolling!
|
||||
|
||||
ChatThrottleLib:Init()
|
||||
|
||||
--[[ WoWBench debugging snippet
|
||||
if(WOWB_VER) then
|
||||
local function SayTimer()
|
||||
print("SAY: "..GetTime().." "..arg1)
|
||||
end
|
||||
ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
|
||||
ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
|
||||
end
|
||||
]]
|
||||
|
||||
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
--
|
||||
-- ChatThrottleLib by Mikk
|
||||
--
|
||||
-- Manages AddOn chat output to keep player from getting kicked off.
|
||||
--
|
||||
-- ChatThrottleLib:SendChatMessage/:SendAddonMessage functions that accept
|
||||
-- a Priority ("BULK", "NORMAL", "ALERT") as well as prefix for SendChatMessage.
|
||||
--
|
||||
-- Priorities get an equal share of available bandwidth when fully loaded.
|
||||
-- Communication channels are separated on extension+chattype+destination and
|
||||
-- get round-robinned. (Destination only matters for whispers and channels,
|
||||
-- obviously)
|
||||
--
|
||||
-- Will install hooks for SendChatMessage and SendAddonMessage to measure
|
||||
-- bandwidth bypassing the library and use less bandwidth itself.
|
||||
--
|
||||
--
|
||||
-- Fully embeddable library. Just copy this file into your addon directory,
|
||||
-- add it to the .toc, and it's done.
|
||||
--
|
||||
-- Can run as a standalone addon also, but, really, just embed it! :-)
|
||||
--
|
||||
-- LICENSE: ChatThrottleLib is released into the Public Domain
|
||||
--
|
||||
|
||||
local CTL_VERSION = 25
|
||||
|
||||
local _G = _G
|
||||
|
||||
if _G.ChatThrottleLib then
|
||||
if _G.ChatThrottleLib.version >= CTL_VERSION then
|
||||
-- There's already a newer (or same) version loaded. Buh-bye.
|
||||
return
|
||||
elseif not _G.ChatThrottleLib.securelyHooked then
|
||||
print("ChatThrottleLib: Warning: There's an ANCIENT ChatThrottleLib.lua (pre-wow 2.0, <v16) in an addon somewhere. Get the addon updated or copy in a newer ChatThrottleLib.lua (>=v16) in it!")
|
||||
-- ATTEMPT to unhook; this'll behave badly if someone else has hooked...
|
||||
-- ... and if someone has securehooked, they can kiss that goodbye too... >.<
|
||||
_G.SendChatMessage = _G.ChatThrottleLib.ORIG_SendChatMessage
|
||||
if _G.ChatThrottleLib.ORIG_SendAddonMessage then
|
||||
_G.SendAddonMessage = _G.ChatThrottleLib.ORIG_SendAddonMessage
|
||||
end
|
||||
end
|
||||
_G.ChatThrottleLib.ORIG_SendChatMessage = nil
|
||||
_G.ChatThrottleLib.ORIG_SendAddonMessage = nil
|
||||
end
|
||||
|
||||
if not _G.ChatThrottleLib then
|
||||
_G.ChatThrottleLib = {}
|
||||
end
|
||||
|
||||
ChatThrottleLib = _G.ChatThrottleLib -- in case some addon does "local ChatThrottleLib" above us and we're copypasted (AceComm-2, sigh)
|
||||
local ChatThrottleLib = _G.ChatThrottleLib
|
||||
|
||||
ChatThrottleLib.version = CTL_VERSION
|
||||
|
||||
|
||||
|
||||
------------------ TWEAKABLES -----------------
|
||||
|
||||
ChatThrottleLib.MAX_CPS = 800 -- 2000 seems to be safe if NOTHING ELSE is happening. let's call it 800.
|
||||
ChatThrottleLib.MSG_OVERHEAD = 40 -- Guesstimate overhead for sending a message; source+dest+chattype+protocolstuff
|
||||
|
||||
ChatThrottleLib.BURST = 4000 -- WoW's server buffer seems to be about 32KB. 8KB should be safe, but seen disconnects on _some_ servers. Using 4KB now.
|
||||
|
||||
ChatThrottleLib.MIN_FPS = 20 -- Reduce output CPS to half (and don't burst) if FPS drops below this value
|
||||
|
||||
|
||||
local setmetatable = setmetatable
|
||||
local table_remove = table.remove
|
||||
local tostring = tostring
|
||||
local GetTime = GetTime
|
||||
local math_min = math.min
|
||||
local math_max = math.max
|
||||
local next = next
|
||||
local strlen = string.len
|
||||
local GetFramerate = GetFramerate
|
||||
local strlower = string.lower
|
||||
local unpack,type,pairs,wipe = unpack,type,pairs,wipe
|
||||
local UnitInRaid,GetNumPartyMembers = UnitInRaid,GetNumPartyMembers
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Double-linked ring implementation
|
||||
|
||||
local Ring = {}
|
||||
local RingMeta = { __index = Ring }
|
||||
|
||||
function Ring:New()
|
||||
local ret = {}
|
||||
setmetatable(ret, RingMeta)
|
||||
return ret
|
||||
end
|
||||
|
||||
function Ring:Add(obj) -- Append at the "far end" of the ring (aka just before the current position)
|
||||
if self.pos then
|
||||
obj.prev = self.pos.prev
|
||||
obj.prev.next = obj
|
||||
obj.next = self.pos
|
||||
obj.next.prev = obj
|
||||
else
|
||||
obj.next = obj
|
||||
obj.prev = obj
|
||||
self.pos = obj
|
||||
end
|
||||
end
|
||||
|
||||
function Ring:Remove(obj)
|
||||
obj.next.prev = obj.prev
|
||||
obj.prev.next = obj.next
|
||||
if self.pos == obj then
|
||||
self.pos = obj.next
|
||||
if self.pos == obj then
|
||||
self.pos = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Recycling bin for pipes
|
||||
-- A pipe is a plain integer-indexed queue of messages
|
||||
-- Pipes normally live in Rings of pipes (3 rings total, one per priority)
|
||||
|
||||
ChatThrottleLib.PipeBin = nil -- pre-v19, drastically different
|
||||
local PipeBin = setmetatable({}, {__mode="k"})
|
||||
|
||||
local function DelPipe(pipe)
|
||||
for i = #pipe, 1, -1 do
|
||||
pipe[i] = nil
|
||||
end
|
||||
pipe.prev = nil
|
||||
pipe.next = nil
|
||||
|
||||
PipeBin[pipe] = true
|
||||
end
|
||||
|
||||
local function NewPipe()
|
||||
local pipe = next(PipeBin)
|
||||
if pipe then
|
||||
wipe(pipe)
|
||||
PipeBin[pipe] = nil
|
||||
return pipe
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Recycling bin for messages
|
||||
|
||||
ChatThrottleLib.MsgBin = nil -- pre-v19, drastically different
|
||||
local MsgBin = setmetatable({}, {__mode="k"})
|
||||
|
||||
local function DelMsg(msg)
|
||||
msg[1] = nil
|
||||
-- there's more parameters, but they're very repetetive so the string pool doesn't suffer really, and it's faster to just not delete them.
|
||||
MsgBin[msg] = true
|
||||
end
|
||||
|
||||
local function NewMsg()
|
||||
local msg = next(MsgBin)
|
||||
if msg then
|
||||
MsgBin[msg] = nil
|
||||
return msg
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib:Init
|
||||
-- Initialize queues, set up frame for OnUpdate, etc
|
||||
|
||||
|
||||
function ChatThrottleLib:Init()
|
||||
|
||||
-- Set up queues
|
||||
if not self.Prio then
|
||||
self.Prio = {}
|
||||
self.Prio["ALERT"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
self.Prio["NORMAL"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
self.Prio["BULK"] = { ByName = {}, Ring = Ring:New(), avail = 0 }
|
||||
end
|
||||
|
||||
-- v4: total send counters per priority
|
||||
for _, Prio in pairs(self.Prio) do
|
||||
Prio.nTotalSent = Prio.nTotalSent or 0
|
||||
end
|
||||
|
||||
if not self.avail then
|
||||
self.avail = 0 -- v5
|
||||
end
|
||||
if not self.nTotalSent then
|
||||
self.nTotalSent = 0 -- v5
|
||||
end
|
||||
|
||||
|
||||
-- Set up a frame to get OnUpdate events
|
||||
if not self.Frame then
|
||||
self.Frame = CreateFrame("Frame")
|
||||
self.Frame:Hide()
|
||||
end
|
||||
self.Frame:SetScript("OnUpdate", self.OnUpdate)
|
||||
self.Frame:SetScript("OnEvent", self.OnEvent) -- v11: Monitor P_E_W so we can throttle hard for a few seconds
|
||||
self.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
self.OnUpdateDelay = 0
|
||||
self.LastAvailUpdate = GetTime()
|
||||
self.HardThrottlingBeginTime = GetTime() -- v11: Throttle hard for a few seconds after startup
|
||||
|
||||
-- Hook SendChatMessage and SendAddonMessage so we can measure unpiped traffic and avoid overloads (v7)
|
||||
if not self.securelyHooked then
|
||||
-- Use secure hooks as of v16. Old regular hook support yanked out in v21.
|
||||
self.securelyHooked = true
|
||||
--SendChatMessage
|
||||
hooksecurefunc("SendChatMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendChatMessage(...)
|
||||
end)
|
||||
--SendAddonMessage
|
||||
hooksecurefunc("SendAddonMessage", function(...)
|
||||
return ChatThrottleLib.Hook_SendAddonMessage(...)
|
||||
end)
|
||||
end
|
||||
self.nBypass = 0
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib.Hook_SendChatMessage / .Hook_SendAddonMessage
|
||||
|
||||
local bMyTraffic = false
|
||||
|
||||
function ChatThrottleLib.Hook_SendChatMessage(text, chattype, language, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = strlen(tostring(text or "")) + strlen(tostring(destination or "")) + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
function ChatThrottleLib.Hook_SendAddonMessage(prefix, text, chattype, destination, ...)
|
||||
if bMyTraffic then
|
||||
return
|
||||
end
|
||||
local self = ChatThrottleLib
|
||||
local size = tostring(text or ""):len() + tostring(prefix or ""):len();
|
||||
size = size + tostring(destination or ""):len() + self.MSG_OVERHEAD
|
||||
self.avail = self.avail - size
|
||||
self.nBypass = self.nBypass + size -- just a statistic
|
||||
end
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- ChatThrottleLib:UpdateAvail
|
||||
-- Update self.avail with how much bandwidth is currently available
|
||||
|
||||
function ChatThrottleLib:UpdateAvail()
|
||||
local now = GetTime()
|
||||
local MAX_CPS = self.MAX_CPS;
|
||||
local newavail = MAX_CPS * (now - self.LastAvailUpdate)
|
||||
local avail = self.avail
|
||||
|
||||
if now - self.HardThrottlingBeginTime < 5 then
|
||||
-- First 5 seconds after startup/zoning: VERY hard clamping to avoid irritating the server rate limiter, it seems very cranky then
|
||||
avail = math_min(avail + (newavail*0.1), MAX_CPS*0.5)
|
||||
self.bChoking = true
|
||||
elseif GetFramerate() < self.MIN_FPS then -- GetFramerate call takes ~0.002 secs
|
||||
avail = math_min(MAX_CPS, avail + newavail*0.5)
|
||||
self.bChoking = true -- just a statistic
|
||||
else
|
||||
avail = math_min(self.BURST, avail + newavail)
|
||||
self.bChoking = false
|
||||
end
|
||||
|
||||
avail = math_max(avail, 0-(MAX_CPS*2)) -- Can go negative when someone is eating bandwidth past the lib. but we refuse to stay silent for more than 2 seconds; if they can do it, we can.
|
||||
|
||||
self.avail = avail
|
||||
self.LastAvailUpdate = now
|
||||
|
||||
return avail
|
||||
end
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Despooling logic
|
||||
-- Reminder:
|
||||
-- - We have 3 Priorities, each containing a "Ring" construct ...
|
||||
-- - ... made up of N "Pipe"s (1 for each destination/pipename)
|
||||
-- - and each pipe contains messages
|
||||
|
||||
function ChatThrottleLib:Despool(Prio)
|
||||
local ring = Prio.Ring
|
||||
while ring.pos and Prio.avail > ring.pos[1].nSize do
|
||||
local msg = table_remove(ring.pos, 1)
|
||||
if not ring.pos[1] then -- did we remove last msg in this pipe?
|
||||
local pipe = Prio.Ring.pos
|
||||
Prio.Ring:Remove(pipe)
|
||||
Prio.ByName[pipe.name] = nil
|
||||
DelPipe(pipe)
|
||||
else
|
||||
Prio.Ring.pos = Prio.Ring.pos.next
|
||||
end
|
||||
local didSend=false
|
||||
local lowerDest = strlower(msg[3] or "")
|
||||
if lowerDest == "raid" and not UnitInRaid("player") then
|
||||
-- do nothing
|
||||
elseif lowerDest == "party" and GetNumPartyMembers() == 0 then
|
||||
-- do nothing
|
||||
else
|
||||
Prio.avail = Prio.avail - msg.nSize
|
||||
bMyTraffic = true
|
||||
msg.f(unpack(msg, 1, msg.n))
|
||||
bMyTraffic = false
|
||||
Prio.nTotalSent = Prio.nTotalSent + msg.nSize
|
||||
DelMsg(msg)
|
||||
didSend = true
|
||||
end
|
||||
-- notify caller of delivery (even if we didn't send it)
|
||||
if msg.callbackFn then
|
||||
msg.callbackFn (msg.callbackArg, didSend)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib.OnEvent(this,event)
|
||||
-- v11: We know that the rate limiter is touchy after login. Assume that it's touchy after zoning, too.
|
||||
local self = ChatThrottleLib
|
||||
if event == "PLAYER_ENTERING_WORLD" then
|
||||
self.HardThrottlingBeginTime = GetTime() -- Throttle hard for a few seconds after zoning
|
||||
self.avail = 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib.OnUpdate(this,delay)
|
||||
local self = ChatThrottleLib
|
||||
|
||||
self.OnUpdateDelay = self.OnUpdateDelay + delay
|
||||
if self.OnUpdateDelay < 0.08 then
|
||||
return
|
||||
end
|
||||
self.OnUpdateDelay = 0
|
||||
|
||||
self:UpdateAvail()
|
||||
|
||||
if self.avail < 0 then
|
||||
return -- argh. some bastard is spewing stuff past the lib. just bail early to save cpu.
|
||||
end
|
||||
|
||||
-- See how many of our priorities have queued messages (we only have 3, don't worry about the loop)
|
||||
local n = 0
|
||||
for prioname,Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Anything queued still?
|
||||
if n<1 then
|
||||
-- Nope. Move spillover bandwidth to global availability gauge and clear self.bQueueing
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
self.avail = self.avail + Prio.avail
|
||||
Prio.avail = 0
|
||||
end
|
||||
self.bQueueing = false
|
||||
self.Frame:Hide()
|
||||
return
|
||||
end
|
||||
|
||||
-- There's stuff queued. Hand out available bandwidth to priorities as needed and despool their queues
|
||||
local avail = self.avail/n
|
||||
self.avail = 0
|
||||
|
||||
for prioname, Prio in pairs(self.Prio) do
|
||||
if Prio.Ring.pos or Prio.avail < 0 then
|
||||
Prio.avail = Prio.avail + avail
|
||||
if Prio.Ring.pos and Prio.avail > Prio.Ring.pos[1].nSize then
|
||||
self:Despool(Prio)
|
||||
-- Note: We might not get here if the user-supplied callback function errors out! Take care!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Spooling logic
|
||||
|
||||
function ChatThrottleLib:Enqueue(prioname, pipename, msg)
|
||||
local Prio = self.Prio[prioname]
|
||||
local pipe = Prio.ByName[pipename]
|
||||
if not pipe then
|
||||
self.Frame:Show()
|
||||
pipe = NewPipe()
|
||||
pipe.name = pipename
|
||||
Prio.ByName[pipename] = pipe
|
||||
Prio.Ring:Add(pipe)
|
||||
end
|
||||
|
||||
pipe[#pipe + 1] = msg
|
||||
|
||||
self.bQueueing = true
|
||||
end
|
||||
|
||||
function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, language, destination, queueName, callbackFn, callbackArg)
|
||||
if not self or not prio or not prefix or not text or not self.Prio[prio] then
|
||||
error('Usage: ChatThrottleLib:SendChatMessage("{BULK||NORMAL||ALERT}", "prefix", "text"[, "chattype"[, "language"[, "destination"]]]', 2)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:ChatMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = text:len()
|
||||
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendChatMessage(): message length cannot exceed 255 bytes", 2)
|
||||
end
|
||||
|
||||
nSize = nSize + self.MSG_OVERHEAD
|
||||
|
||||
-- Check if there's room in the global available bandwidth gauge to send directly
|
||||
if not self.bQueueing and nSize < self:UpdateAvail() then
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendChatMessage(text, chattype, language, destination)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendChatMessage
|
||||
msg[1] = text
|
||||
msg[2] = chattype or "SAY"
|
||||
msg[3] = language
|
||||
msg[4] = destination
|
||||
msg.n = 4
|
||||
msg.nSize = nSize
|
||||
msg.callbackFn = callbackFn
|
||||
msg.callbackArg = callbackArg
|
||||
|
||||
self:Enqueue(prio, queueName or (prefix..(chattype or "SAY")..(destination or "")), msg)
|
||||
end
|
||||
|
||||
|
||||
function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
|
||||
if not self or not prio or not prefix or not text or not chattype or not self.Prio[prio] then
|
||||
error('Usage: ChatThrottleLib:SendAddonMessage("{BULK||NORMAL||ALERT}", "prefix", "text", "chattype"[, "target"])', 2)
|
||||
end
|
||||
if callbackFn and type(callbackFn)~="function" then
|
||||
error('ChatThrottleLib:SendAddonMessage(): callbackFn: expected function, got '..type(callbackFn), 2)
|
||||
end
|
||||
|
||||
local nSize = prefix:len() + 1 + text:len();
|
||||
|
||||
if nSize>255 then
|
||||
error("ChatThrottleLib:SendAddonMessage(): prefix + message length cannot exceed 254 bytes", 2)
|
||||
end
|
||||
|
||||
nSize = nSize + self.MSG_OVERHEAD;
|
||||
|
||||
-- Check if there's room in the global available bandwidth gauge to send directly
|
||||
if not self.bQueueing and nSize < self:UpdateAvail() then
|
||||
self.avail = self.avail - nSize
|
||||
bMyTraffic = true
|
||||
_G.SendAddonMessage(prefix, text, chattype, target)
|
||||
bMyTraffic = false
|
||||
self.Prio[prio].nTotalSent = self.Prio[prio].nTotalSent + nSize
|
||||
if callbackFn then
|
||||
callbackFn (callbackArg, true)
|
||||
end
|
||||
-- USER CALLBACK MAY ERROR
|
||||
return
|
||||
end
|
||||
|
||||
-- Message needs to be queued
|
||||
local msg = NewMsg()
|
||||
msg.f = _G.SendAddonMessage
|
||||
msg[1] = prefix
|
||||
msg[2] = text
|
||||
msg[3] = chattype
|
||||
msg[4] = target
|
||||
msg.n = (target~=nil) and 4 or 3;
|
||||
msg.nSize = nSize
|
||||
msg.callbackFn = callbackFn
|
||||
msg.callbackArg = callbackArg
|
||||
|
||||
self:Enqueue(prio, queueName or (prefix..chattype..(target or "")), msg)
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
-- Get the ball rolling!
|
||||
|
||||
ChatThrottleLib:Init()
|
||||
|
||||
--[[ WoWBench debugging snippet
|
||||
if(WOWB_VER) then
|
||||
local function SayTimer()
|
||||
print("SAY: "..GetTime().." "..arg1)
|
||||
end
|
||||
ChatThrottleLib.Frame:SetScript("OnEvent", SayTimer)
|
||||
ChatThrottleLib.Frame:RegisterEvent("CHAT_MSG_SAY")
|
||||
end
|
||||
]]
|
||||
|
||||
|
||||
+2965
File diff suppressed because it is too large
Load Diff
+6
@@ -0,0 +1,6 @@
|
||||
<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="ChatThrottleLib.lua" />
|
||||
<Script file="LibHealComm-4.0.lua" />
|
||||
</Ui>
|
||||
@@ -0,0 +1,3 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/">
|
||||
<Script file="ChatThrottleLib.lua" />
|
||||
</Ui>
|
||||
@@ -0,0 +1,58 @@
|
||||
<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="Util\Constants.lua" />
|
||||
<Script file="Util\SoundKit.lua" />
|
||||
<Script file="Util\C_TimerAugment.lua" />
|
||||
|
||||
<Script file="Util\EventHandler.lua" />
|
||||
<Script file="Util\EventHandlerSupport.lua" />
|
||||
|
||||
<Script file="Util\Unit.lua" />
|
||||
<Script file="Util\Cooldown.lua" />
|
||||
<Script file="Util\Group.lua" />
|
||||
<Script file="Util\Spell.lua" />
|
||||
<Script file="Util\HealPrediction.lua" />
|
||||
|
||||
<Script file="Util\C_CreatureInfo.lua" />
|
||||
<Script file="Util\C_FriendList.lua" />
|
||||
<Script file="Util\C_IncomingSummon.lua" />
|
||||
<Script file="Util\C_Map.lua" />
|
||||
<Script file="Util\C_PvP.lua" />
|
||||
<Script file="Util\C_Texture.lua" />
|
||||
<Script file="Util\C_Talent.lua" />
|
||||
<Script file="Util\C_VoiceChat.lua" />
|
||||
<Script file="Util\C_NamePlate.lua" />
|
||||
<Script file="Util\C_GossipInfo.lua" />
|
||||
<Script file="Util\C_ChatInfo.lua" />
|
||||
<Script file="Util\C_Spell.lua" />
|
||||
<Script file="Util\C_Item.lua" />
|
||||
<Script file="Util\C_UI.lua" />
|
||||
<Script file="Util\C_NewItems.lua" />
|
||||
|
||||
<Script file="Util\Mixin.lua" />
|
||||
<Script file="Util\Pools.lua" />
|
||||
<Script file="Util\Color.lua" />
|
||||
<Script file="Util\ColorUtil.lua" />
|
||||
<Script file="Util\API.lua" />
|
||||
<Script file="Util\TableUtil.lua" />
|
||||
<Script file="Util\MathUtil.lua" />
|
||||
<Script file="Util\EnumUtil.lua" />
|
||||
<Script file="Util\Atlas.lua" />
|
||||
<Script file="Util\SharedExtendedMethods.lua" />
|
||||
<Script file="Util\PartyUtil.lua" />
|
||||
<Script file="Util\PixelUtil.lua" />
|
||||
<Script file="Util\AuraUtil.lua" />
|
||||
<Script file="Util\Aura.lua" />
|
||||
<Script file="Util\NineSlice.lua" />
|
||||
<Script file="Util\TextureUtil.lua" />
|
||||
<Script file="Util\SmoothStatusBar.lua" />
|
||||
<Script file="Util\CallbackRegistry.lua" />
|
||||
<Script file="Util\GlobalCallbackRegistry.lua" />
|
||||
<Script file="Util\SpellUtil.lua" />
|
||||
<Script file="Util\ItemUtil.lua" />
|
||||
|
||||
<Script file="Util\AddOnCompat.lua" />
|
||||
|
||||
<!-- Templates -->
|
||||
<Include file="Templates\Load.xml" />
|
||||
</Ui>
|
||||
@@ -0,0 +1,455 @@
|
||||
BackdropTemplateMixin = BackdropTemplateMixin or {}
|
||||
|
||||
BACKDROP_TOOLTIP_8_8_1111 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 8,
|
||||
edgeSize = 8,
|
||||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||||
};
|
||||
BACKDROP_TOOLTIP_8_12_1111 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 8,
|
||||
edgeSize = 12,
|
||||
insets = { left = 1, right = 1, top = 1, bottom = 1 },
|
||||
};
|
||||
BACKDROP_TOOLTIP_16_16_5555 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 16,
|
||||
edgeSize = 16,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
};
|
||||
BACKDROP_TOOLTIP_12_12_4444 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 12,
|
||||
edgeSize = 12,
|
||||
insets = { left = 4, right = 4, top = 4, bottom = 4 },
|
||||
};
|
||||
BACKDROP_TOOLTIP_0_16 = {
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 16,
|
||||
tileEdge = true,
|
||||
};
|
||||
BACKDROP_TOOLTIP_0_12_0055 = {
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 12,
|
||||
tileEdge = true,
|
||||
insets = { left = 0, right = 0, top = 5, bottom = 5 },
|
||||
};
|
||||
BACKDROP_TOOLTIP_0_16_5555 = {
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 16,
|
||||
tileEdge = true,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
};
|
||||
BACKDROP_ACHIEVEMENTS_0_64 = {
|
||||
edgeFile = "Interface\\AchievementFrame\\UI-Achievement-WoodBorder",
|
||||
edgeSize = 64,
|
||||
tileEdge = true,
|
||||
};
|
||||
BACKDROP_ARENA_32_32 = {
|
||||
bgFile = "Interface\\CharacterFrame\\UI-Party-Background",
|
||||
edgeFile = "Interface\\ArenaEnemyFrame\\UI-Arena-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 32, right = 32, top = 32, bottom = 32 },
|
||||
};
|
||||
BACKDROP_DIALOG_32_32 = {
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
};
|
||||
BACKDROP_GOLD_DIALOG_32_32 = {
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
};
|
||||
BACKDROP_WATERMARK_DIALOG_0_16 = {
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-TestWatermark-Border",
|
||||
tileEdge = true,
|
||||
edgeSize = 16,
|
||||
};
|
||||
BACKDROP_SLIDER_8_8 = {
|
||||
bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
|
||||
edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 8,
|
||||
edgeSize = 8,
|
||||
insets = { left = 3, right = 3, top = 6, bottom = 6 },
|
||||
};
|
||||
BACKDROP_PARTY_32_32 = {
|
||||
bgFile = "Interface\\CharacterFrame\\UI-Party-Background",
|
||||
edgeFile = "Interface\\CharacterFrame\\UI-Party-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 32, right = 32, top = 32, bottom = 32 },
|
||||
};
|
||||
BACKDROP_TOAST_12_12 = {
|
||||
bgFile = "Interface\\FriendsFrame\\UI-Toast-Background",
|
||||
edgeFile = "Interface\\FriendsFrame\\UI-Toast-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 12,
|
||||
edgeSize = 12,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
};
|
||||
BACKDROP_CALLOUT_GLOW_0_16 = {
|
||||
edgeFile = "Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow",
|
||||
edgeSize = 16,
|
||||
tileEdge = true,
|
||||
};
|
||||
BACKDROP_CALLOUT_GLOW_0_20 = {
|
||||
edgeFile = "Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow",
|
||||
edgeSize = 20,
|
||||
tileEdge = true,
|
||||
};
|
||||
BACKDROP_GLUE_TOOLTIP_16_16 = {
|
||||
bgFile = "Interface\\Glues\\Common\\Glue-Tooltip-Background",
|
||||
edgeFile = "Interface\\Glues\\Common\\Glue-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 16,
|
||||
edgeSize = 16,
|
||||
insets = { left = 10, right = 5, top = 4, bottom = 9 },
|
||||
};
|
||||
BACKDROP_GLUE_TOOLTIP_0_16 = {
|
||||
edgeFile = "Interface\\Glues\\Common\\Glue-Tooltip-Border",
|
||||
tileEdge = true,
|
||||
edgeSize = 16,
|
||||
};
|
||||
BACKDROP_MIXED_TOOLTIP_16_16 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Glues\\Common\\Glue-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 16,
|
||||
edgeSize = 16,
|
||||
insets = { left = 10, right = 5, top = 4, bottom = 9 },
|
||||
};
|
||||
BACKDROP_TEXT_PANEL_0_16 = {
|
||||
edgeFile = "Interface\\Glues\\Common\\TextPanel-Border",
|
||||
tileEdge = true,
|
||||
edgeSize = 16,
|
||||
};
|
||||
|
||||
local coordStart = 0.0625;
|
||||
local coordEnd = 1 - coordStart;
|
||||
|
||||
local textureUVs = { -- keys have to match pieceNames in nineSliceSetup table
|
||||
TopLeftCorner = { setWidth = true, setHeight = true, ULx = 0.5078125, ULy = coordStart, LLx = 0.5078125, LLy = coordEnd, URx = 0.6171875, URy = coordStart, LRx = 0.6171875, LRy = coordEnd },
|
||||
TopRightCorner = { setWidth = true, setHeight = true, ULx = 0.6328125, ULy = coordStart, LLx = 0.6328125, LLy = coordEnd, URx = 0.7421875, URy = coordStart, LRx = 0.7421875, LRy = coordEnd },
|
||||
BottomLeftCorner = { setWidth = true, setHeight = true, ULx = 0.7578125, ULy = coordStart, LLx = 0.7578125, LLy = coordEnd, URx = 0.8671875, URy = coordStart, LRx = 0.8671875, LRy = coordEnd },
|
||||
BottomRightCorner = { setWidth = true, setHeight = true, ULx = 0.8828125, ULy = coordStart, LLx = 0.8828125, LLy = coordEnd, URx = 0.9921875, URy = coordStart, LRx = 0.9921875, LRy = coordEnd },
|
||||
TopEdge = { setHeight = true, ULx = 0.2578125, ULy = "repeatX", LLx = 0.3671875, LLy = "repeatX", URx = 0.2578125, URy = coordStart, LRx = 0.3671875, LRy = coordStart },
|
||||
BottomEdge = { setHeight = true, ULx = 0.3828125, ULy = "repeatX", LLx = 0.4921875, LLy = "repeatX", URx = 0.3828125, URy = coordStart, LRx = 0.4921875, LRy = coordStart },
|
||||
LeftEdge = { setWidth = true, ULx = 0.0078125, ULy = coordStart, LLx = 0.0078125, LLy = "repeatY", URx = 0.1171875, URy = coordStart, LRx = 0.1171875, LRy = "repeatY" },
|
||||
RightEdge = { setWidth = true, ULx = 0.1328125, ULy = coordStart, LLx = 0.1328125, LLy = "repeatY", URx = 0.2421875, URy = coordStart, LRx = 0.2421875, LRy = "repeatY" },
|
||||
Center = { ULx = 0, ULy = 0, LLx = 0, LLy = "repeatY", URx = "repeatX", URy = 0, LRx = "repeatX", LRy = "repeatY" },
|
||||
};
|
||||
|
||||
local defaultEdgeSize = 39; -- the old default
|
||||
|
||||
function BackdropTemplateMixin:OnBackdropLoaded()
|
||||
if self.backdropInfo then
|
||||
-- check for invalid info
|
||||
if not self.backdropInfo.edgeFile and not self.backdropInfo.bgFile then
|
||||
self.backdropInfo = nil;
|
||||
return;
|
||||
end
|
||||
self:ApplyBackdrop();
|
||||
do
|
||||
if self.backdropColor then
|
||||
local r, g, b = 1, 1, 1;
|
||||
r, g, b = self.backdropColor:GetRGB();
|
||||
local a = self.backdropColorAlpha or 1;
|
||||
self:SetBackdropColor(r, g, b, a);
|
||||
end
|
||||
end
|
||||
do
|
||||
if self.backdropBorderColor then
|
||||
local r, g, b = 1, 1, 1;
|
||||
r, g, b = self.backdropBorderColor:GetRGB();
|
||||
local a = self.backdropBorderColorAlpha or 1;
|
||||
self:SetBackdropBorderColor(r, g, b, a);
|
||||
end
|
||||
end
|
||||
if self.backdropBorderBlendMode then
|
||||
self:SetBorderBlendMode(self.backdropBorderBlendMode);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:OnBackdropSizeChanged()
|
||||
if self.backdropInfo then
|
||||
self:SetupTextureCoordinates();
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:GetEdgeSize()
|
||||
if self.backdropInfo.edgeSize and self.backdropInfo.edgeSize > 0 then
|
||||
return self.backdropInfo.edgeSize;
|
||||
else
|
||||
return defaultEdgeSize;
|
||||
end
|
||||
end
|
||||
|
||||
local function GetBackdropCoordValue(coord, pieceSetup, repeatX, repeatY)
|
||||
local value = pieceSetup[coord];
|
||||
if value == "repeatX" then
|
||||
return repeatX;
|
||||
elseif value == "repeatY" then
|
||||
return repeatY;
|
||||
else
|
||||
return value;
|
||||
end
|
||||
end
|
||||
|
||||
local function SetupBackdropTextureCoordinates(region, pieceSetup, repeatX, repeatY)
|
||||
region:SetTexCoord( GetBackdropCoordValue("ULx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("ULy", pieceSetup, repeatX, repeatY),
|
||||
GetBackdropCoordValue("LLx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("LLy", pieceSetup, repeatX, repeatY),
|
||||
GetBackdropCoordValue("URx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("URy", pieceSetup, repeatX, repeatY),
|
||||
GetBackdropCoordValue("LRx", pieceSetup, repeatX, repeatY), GetBackdropCoordValue("LRy", pieceSetup, repeatX, repeatY));
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:SetupTextureCoordinates()
|
||||
local width = self:GetWidth();
|
||||
local height = self:GetHeight();
|
||||
local effectiveScale = self:GetEffectiveScale();
|
||||
local edgeSize = self:GetEdgeSize();
|
||||
local edgeRepeatX = max(0, (width / edgeSize) * effectiveScale - 2 - coordStart);
|
||||
local edgeRepeatY = max(0, (height / edgeSize) * effectiveScale - 2 - coordStart);
|
||||
for pieceName, pieceSetup in pairs(textureUVs) do
|
||||
local region = self[pieceName];
|
||||
if region then
|
||||
if pieceName == "Center" then
|
||||
local repeatX = 1;
|
||||
local repeatY = 1;
|
||||
if self.backdropInfo.tile then
|
||||
local divisor = self.backdropInfo.tileSize;
|
||||
if not divisor or divisor == 0 then
|
||||
divisor = edgeSize;
|
||||
end
|
||||
if divisor ~= 0 then
|
||||
repeatX = (width / divisor) * effectiveScale;
|
||||
repeatY = (height / divisor) * effectiveScale;
|
||||
end
|
||||
end
|
||||
SetupBackdropTextureCoordinates(region, pieceSetup, repeatX, repeatY);
|
||||
else
|
||||
SetupBackdropTextureCoordinates(region, pieceSetup, edgeRepeatX, edgeRepeatY);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:SetupPieceVisuals(piece, setupInfo, pieceLayout)
|
||||
local textureInfo = textureUVs[setupInfo.pieceName];
|
||||
local tileVerts = false;
|
||||
local file;
|
||||
if setupInfo.pieceName == "Center" then
|
||||
file = self.backdropInfo.bgFile;
|
||||
tileVerts = self.backdropInfo.tile;
|
||||
else
|
||||
if self.backdropInfo.tileEdge ~= false then
|
||||
tileVerts = true;
|
||||
end
|
||||
file = self.backdropInfo.edgeFile;
|
||||
end
|
||||
piece:SetTexture(file, tileVerts, tileVerts);
|
||||
local cornerWidth = textureInfo.setWidth and self:GetEdgeSize() or 0;
|
||||
local cornerHeight = textureInfo.setHeight and self:GetEdgeSize() or 0;
|
||||
piece:SetSize(cornerWidth, cornerHeight);
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:SetBorderBlendMode(blendMode)
|
||||
if not self.backdropInfo then
|
||||
return;
|
||||
end
|
||||
for pieceName in pairs(textureUVs) do
|
||||
local region = self[pieceName];
|
||||
if region and pieceName ~= "Center" then
|
||||
region:SetBlendMode(blendMode);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:HasBackdropInfo(backdropInfo)
|
||||
return self.backdropInfo == backdropInfo;
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:ClearBackdrop()
|
||||
if self.backdropInfo then
|
||||
for pieceName in pairs(textureUVs) do
|
||||
local region = self[pieceName];
|
||||
if region then
|
||||
region:SetTexture(nil);
|
||||
end
|
||||
end
|
||||
self.backdropInfo = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:ApplyBackdrop()
|
||||
local x, y, x1, y1 = 0, 0, 0, 0;
|
||||
if self.backdropInfo.bgFile then
|
||||
local edgeSize = self:GetEdgeSize();
|
||||
x = -edgeSize;
|
||||
y = edgeSize;
|
||||
x1 = edgeSize;
|
||||
y1 = -edgeSize;
|
||||
local insets = self.backdropInfo.insets;
|
||||
if insets then
|
||||
x = x + (insets.left or 0);
|
||||
y = y - (insets.top or 0);
|
||||
x1 = x1 - (insets.right or 0);
|
||||
y1 = y1 + (insets.bottom or 0);
|
||||
end
|
||||
end
|
||||
|
||||
local layout = {
|
||||
TopLeftCorner = { },
|
||||
TopRightCorner = { },
|
||||
BottomLeftCorner = { },
|
||||
BottomRightCorner = { },
|
||||
TopEdge = { },
|
||||
BottomEdge = { },
|
||||
LeftEdge = { },
|
||||
RightEdge = { },
|
||||
Center = { layer = "BACKGROUND", x = x, y = y, x1 = x1, y1 = y1 },
|
||||
setupPieceVisualsFunction = BackdropTemplateMixin.SetupPieceVisuals,
|
||||
};
|
||||
|
||||
NineSliceUtil.ApplyLayout(self, layout);
|
||||
--self:SetBackdropColor(1, 1, 1, 1);
|
||||
--self:SetBackdropBorderColor(1, 1, 1, 1);
|
||||
self:SetupTextureCoordinates();
|
||||
end
|
||||
|
||||
-- backwards compatibility API starts here
|
||||
function BackdropTemplateMixin:SetBackdrop(backdropInfo)
|
||||
if backdropInfo then
|
||||
if self:HasBackdropInfo(backdropInfo) then
|
||||
return;
|
||||
end
|
||||
if not backdropInfo.edgeFile and not backdropInfo.bgFile then
|
||||
self:ClearBackdrop();
|
||||
return;
|
||||
end
|
||||
self.backdropInfo = backdropInfo;
|
||||
self:ApplyBackdrop();
|
||||
else
|
||||
self:ClearBackdrop();
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:GetBackdrop()
|
||||
if self.backdropInfo then
|
||||
-- make a copy because it will be altered to match old API output
|
||||
local backdropInfo = CopyTable(self.backdropInfo);
|
||||
-- fill in defaults
|
||||
if not backdropInfo.bgFile then
|
||||
backdropInfo.bgFile = "";
|
||||
end
|
||||
if not backdropInfo.edgeFile then
|
||||
backdropInfo.edgeFile = "";
|
||||
end
|
||||
if backdropInfo.tile == nil then
|
||||
backdropInfo.tile = false;
|
||||
end
|
||||
if backdropInfo.tileSize == nil then
|
||||
backdropInfo.tileSize = 0;
|
||||
end
|
||||
if backdropInfo.tileEdge == nil then
|
||||
backdropInfo.tileEdge = true;
|
||||
end
|
||||
if not backdropInfo.edgeSize then
|
||||
backdropInfo.edgeSize = self:GetEdgeSize();
|
||||
end
|
||||
if not backdropInfo.insets then
|
||||
backdropInfo.insets = { };
|
||||
end
|
||||
if not backdropInfo.insets.left then
|
||||
backdropInfo.insets.left = 0;
|
||||
end
|
||||
if not backdropInfo.insets.right then
|
||||
backdropInfo.insets.right = 0;
|
||||
end
|
||||
if not backdropInfo.insets.top then
|
||||
backdropInfo.insets.top = 0;
|
||||
end
|
||||
if not backdropInfo.insets.bottom then
|
||||
backdropInfo.insets.bottom = 0;
|
||||
end
|
||||
return backdropInfo;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:GetBackdropColor()
|
||||
if not self.backdropInfo then
|
||||
return;
|
||||
end
|
||||
if self.Center then
|
||||
return self.Center:GetVertexColor();
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:SetBackdropColor(r, g, b, a)
|
||||
if not self.backdropInfo then
|
||||
-- Ideally this would throw an error here but the old API just failed silently
|
||||
return;
|
||||
end
|
||||
if self.Center then
|
||||
self.Center:SetVertexColor(r, g, b, a or 1);
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:GetBackdropBorderColor()
|
||||
if not self.backdropInfo then
|
||||
return
|
||||
end
|
||||
-- return the vertex color of any valid region
|
||||
for pieceName in pairs(textureUVs) do
|
||||
local region = self[pieceName];
|
||||
if region and pieceName ~= "Center" then
|
||||
return region:GetVertexColor();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:SetBackdropBorderColor(r, g, b, a)
|
||||
if not self.backdropInfo then
|
||||
-- Ideally this would throw an error here but the old API just failed silently
|
||||
return;
|
||||
end
|
||||
for pieceName in pairs(textureUVs) do
|
||||
local region = self[pieceName];
|
||||
if region and pieceName ~= "Center" then
|
||||
region:SetVertexColor(r, g, b, a or 1);
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
<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="Backdrop.lua" />
|
||||
<Frame name="BackdropTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
Mixin(self, BackdropTemplateMixin)
|
||||
</OnLoad>
|
||||
<OnSizeChanged>
|
||||
self:OnBackdropLoaded()
|
||||
self:OnBackdropSizeChanged()
|
||||
</OnSizeChanged>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
-- Simplified Menu Display System
|
||||
-- This is a basic system for displaying a menu from a structure table.
|
||||
--
|
||||
-- See UIDropDownMenu.lua for the menuList details.
|
||||
--
|
||||
-- Args:
|
||||
-- menuList - menu table
|
||||
-- menuFrame - the UI frame to populate
|
||||
-- anchor - where to anchor the frame (e.g. CURSOR)
|
||||
-- x - x offset
|
||||
-- y - y offset
|
||||
-- displayMode - border type
|
||||
-- autoHideDelay - how long until the menu disappears
|
||||
--
|
||||
--
|
||||
function C_EasyMenu(menuList, menuFrame, anchor, x, y, displayMode, autoHideDelay )
|
||||
if ( displayMode == "MENU" ) then
|
||||
menuFrame.displayMode = displayMode;
|
||||
end
|
||||
C_UIDropDownMenu_Initialize(menuFrame, EasyMenu_Initialize, displayMode, nil, menuList);
|
||||
C_ToggleDropDownMenu(1, nil, menuFrame, anchor, x, y, menuList);
|
||||
end
|
||||
|
||||
function C_EasyMenu_Initialize( frame, level, menuList )
|
||||
for index = 1, #menuList do
|
||||
local value = menuList[index]
|
||||
if (value.text) then
|
||||
value.index = index;
|
||||
C_UIDropDownMenu_AddButton( value, level );
|
||||
end
|
||||
end
|
||||
end
|
||||
+1031
File diff suppressed because it is too large
Load Diff
+371
@@ -0,0 +1,371 @@
|
||||
<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">
|
||||
<Button name="C_UIDropDownMenuButtonTemplate" virtual="true">
|
||||
<Size x="100" y="16"/>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentHighlight" file="Interface\QuestFrame\UI-QuestTitleHighlight" alphaMode="ADD" setAllPoints="true" hidden="true"/>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentCheck" file="Interface\Buttons\UI-CheckBox-Check">
|
||||
<Size x="18" y="18"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT">
|
||||
<Offset x="0" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
<Texture name="$parentIcon" hidden="true">
|
||||
<Size>
|
||||
<AbsDimension x="16" y="16"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT">
|
||||
<Offset x="0" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentColorSwatch" hidden="true">
|
||||
<Size>
|
||||
<AbsDimension x="16" y="16"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-6" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentSwatchBg">
|
||||
<Size>
|
||||
<AbsDimension x="14" y="14"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Color r="1.0" g="1.0" b="1.0"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
CloseMenus();
|
||||
C_UIDropDownMenuButton_OpenColorPicker(self:GetParent());
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
C_CloseDropDownMenus(self:GetParent():GetParent():GetID() + 1);
|
||||
_G[self:GetName().."SwatchBg"]:SetVertexColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b);
|
||||
C_UIDropDownMenu_StopCounting(self:GetParent():GetParent());
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
_G[self:GetName().."SwatchBg"]:SetVertexColor(HIGHLIGHT_FONT_COLOR.r, HIGHLIGHT_FONT_COLOR.g, HIGHLIGHT_FONT_COLOR.b);
|
||||
C_UIDropDownMenu_StartCounting(self:GetParent():GetParent());
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
<NormalTexture name="$parentNormalTexture" file="Interface\ChatFrame\ChatFrameColorSwatch"/>
|
||||
</Button>
|
||||
<Button name="$parentExpandArrow" hidden="true">
|
||||
<Size>
|
||||
<AbsDimension x="16" y="16"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
C_ToggleDropDownMenu(self:GetParent():GetParent():GetID() + 1, self:GetParent().value, nil, nil, nil, nil, self:GetParent().menuList, self);
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
local level = self:GetParent():GetParent():GetID() + 1;
|
||||
local listFrame = _G["C_DropDownList"..level];
|
||||
if ( not listFrame or not listFrame:IsShown() or select(2, listFrame:GetPoint()) ~= self ) then
|
||||
C_ToggleDropDownMenu(level, self:GetParent().value, nil, nil, nil, nil, self:GetParent().menuList, self);
|
||||
end
|
||||
C_UIDropDownMenu_StopCounting(self:GetParent():GetParent());
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
C_UIDropDownMenu_StartCounting(self:GetParent():GetParent());
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\ChatFrame\ChatFrameExpandArrow"/>
|
||||
</Button>
|
||||
<Button name="$parentInvisibleButton" hidden="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMLEFT"/>
|
||||
<Anchor point="RIGHT" relativeTo="$parentColorSwatch" relativePoint="LEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
C_UIDropDownMenu_StopCounting(self:GetParent():GetParent());
|
||||
C_CloseDropDownMenus(self:GetParent():GetParent():GetID() + 1);
|
||||
local parent = self:GetParent();
|
||||
if ( parent.tooltipTitle and parent.tooltipWhileDisabled) then
|
||||
if ( parent.tooltipOnButton ) then
|
||||
GameTooltip:SetOwner(parent, "ANCHOR_RIGHT");
|
||||
GameTooltip:AddLine(parent.tooltipTitle, 1.0, 1.0, 1.0);
|
||||
GameTooltip:AddLine(parent.tooltipText);
|
||||
GameTooltip:Show();
|
||||
else
|
||||
GameTooltip_AddNewbieTip(parent, parent.tooltipTitle, 1.0, 1.0, 1.0, parent.tooltipText, 1);
|
||||
end
|
||||
end
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
C_UIDropDownMenu_StartCounting(self:GetParent():GetParent());
|
||||
GameTooltip:Hide();
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self:SetFrameLevel(2);
|
||||
</OnLoad>
|
||||
<OnClick>
|
||||
C_UIDropDownMenuButton_OnClick(self, button, down);
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
if ( self.hasArrow ) then
|
||||
local level = self:GetParent():GetID() + 1;
|
||||
local listFrame = _G["C_DropDownList"..level];
|
||||
if ( not listFrame or not listFrame:IsShown() or select(2, listFrame:GetPoint()) ~= self ) then
|
||||
C_ToggleDropDownMenu(self:GetParent():GetID() + 1, self.value, nil, nil, nil, nil, self.menuList, self);
|
||||
end
|
||||
else
|
||||
C_CloseDropDownMenus(self:GetParent():GetID() + 1);
|
||||
end
|
||||
_G[self:GetName().."Highlight"]:Show();
|
||||
C_UIDropDownMenu_StopCounting(self:GetParent());
|
||||
if ( self.tooltipTitle ) then
|
||||
if ( self.tooltipOnButton ) then
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
|
||||
GameTooltip:AddLine(self.tooltipTitle, 1.0, 1.0, 1.0);
|
||||
GameTooltip:AddLine(self.tooltipText);
|
||||
GameTooltip:Show();
|
||||
else
|
||||
GameTooltip_AddNewbieTip(self, self.tooltipTitle, 1.0, 1.0, 1.0, self.tooltipText, 1);
|
||||
end
|
||||
end
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
_G[self:GetName().."Highlight"]:Hide();
|
||||
C_UIDropDownMenu_StartCounting(self:GetParent());
|
||||
GameTooltip:Hide();
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
<ButtonText name="$parentNormalText">
|
||||
<Anchors>
|
||||
<Anchor point="LEFT">
|
||||
<Offset x="-5" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</ButtonText>
|
||||
<NormalFont style="GameFontHighlightSmallLeft"/>
|
||||
<HighlightFont style="GameFontHighlightSmallLeft"/>
|
||||
<DisabledFont style="GameFontDisableSmallLeft"/>
|
||||
</Button>
|
||||
<Button name="C_UIDropDownListTemplate" hidden="true" frameStrata="DIALOG" enableMouse="true" virtual="true">
|
||||
<Frames>
|
||||
<Frame name="$parentBackdrop" setAllPoints="true">
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background-Dark" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="11" right="12" top="12" bottom="9"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="32"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="32"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
</Frame>
|
||||
<Frame name="$parentMenuBackdrop" setAllPoints="true">
|
||||
<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="4"/>
|
||||
</BackgroundInsets>
|
||||
</Backdrop>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self:SetBackdropBorderColor(TOOLTIP_DEFAULT_COLOR.r, TOOLTIP_DEFAULT_COLOR.g, TOOLTIP_DEFAULT_COLOR.b);
|
||||
self:SetBackdropColor(TOOLTIP_DEFAULT_BACKGROUND_COLOR.r, TOOLTIP_DEFAULT_BACKGROUND_COLOR.g, TOOLTIP_DEFAULT_BACKGROUND_COLOR.b);
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Button name="$parentButton1" inherits="C_UIDropDownMenuButtonTemplate" id="1"/>
|
||||
<Button name="$parentButton2" inherits="C_UIDropDownMenuButtonTemplate" id="2"/>
|
||||
<Button name="$parentButton3" inherits="C_UIDropDownMenuButtonTemplate" id="3"/>
|
||||
<Button name="$parentButton4" inherits="C_UIDropDownMenuButtonTemplate" id="4"/>
|
||||
<Button name="$parentButton5" inherits="C_UIDropDownMenuButtonTemplate" id="5"/>
|
||||
<Button name="$parentButton6" inherits="C_UIDropDownMenuButtonTemplate" id="6"/>
|
||||
<Button name="$parentButton7" inherits="C_UIDropDownMenuButtonTemplate" id="7"/>
|
||||
<Button name="$parentButton8" inherits="C_UIDropDownMenuButtonTemplate" id="8"/>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
self:Hide();
|
||||
</OnClick>
|
||||
<OnEnter>
|
||||
C_UIDropDownMenu_StopCounting(self, motion);
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
C_UIDropDownMenu_StartCounting(self, motion);
|
||||
</OnLeave>
|
||||
<OnUpdate>
|
||||
C_UIDropDownMenu_OnUpdate(self, elapsed);
|
||||
</OnUpdate>
|
||||
<OnShow>
|
||||
for i=1, C_UIDROPDOWNMENU_MAXBUTTONS do
|
||||
if (not self.noResize) then
|
||||
_G[self:GetName().."Button"..i]:SetWidth(self.maxWidth);
|
||||
end
|
||||
end
|
||||
if (not self.noResize) then
|
||||
self:SetWidth(self.maxWidth+25);
|
||||
end
|
||||
self.showTime = nil;
|
||||
if ( self:GetID() > 1 ) then
|
||||
self.parent = _G["C_DropDownList"..(self:GetID() - 1)];
|
||||
end
|
||||
</OnShow>
|
||||
<OnHide>
|
||||
C_UIDropDownMenu_OnHide(self);
|
||||
</OnHide>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Frame name="C_UIDropDownMenuTemplate" virtual="true">
|
||||
<Size>
|
||||
<AbsDimension x="40" y="32"/>
|
||||
</Size>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentLeft" file="Interface\Glues\CharacterCreate\CharacterCreate-LabelFrame">
|
||||
<Size>
|
||||
<AbsDimension x="25" y="64"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="17"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<TexCoords left="0" right="0.1953125" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentMiddle" file="Interface\Glues\CharacterCreate\CharacterCreate-LabelFrame">
|
||||
<Size>
|
||||
<AbsDimension x="115" y="64"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentLeft" relativePoint="RIGHT"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0.1953125" right="0.8046875" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
<Texture name="$parentRight" file="Interface\Glues\CharacterCreate\CharacterCreate-LabelFrame">
|
||||
<Size>
|
||||
<AbsDimension x="25" y="64"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentMiddle" relativePoint="RIGHT"/>
|
||||
</Anchors>
|
||||
<TexCoords left="0.8046875" right="1" top="0" bottom="1"/>
|
||||
</Texture>
|
||||
<FontString name="$parentText" inherits="GameFontHighlightSmall" justifyH="RIGHT">
|
||||
<Size>
|
||||
<AbsDimension x="0" y="10"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" relativeTo="$parentRight">
|
||||
<Offset>
|
||||
<AbsDimension x="-43" y="2"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentButton">
|
||||
<Size>
|
||||
<AbsDimension x="24" y="24"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentRight">
|
||||
<Offset>
|
||||
<AbsDimension x="-16" y="-18"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
C_ToggleDropDownMenu(nil, nil, self:GetParent());
|
||||
PlaySound("igMainMenuOptionCheckBoxOn");
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture name="$parentNormalTexture" file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up">
|
||||
<Size>
|
||||
<AbsDimension x="24" y="24"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT"/>
|
||||
</Anchors>
|
||||
</NormalTexture>
|
||||
<PushedTexture name="$parentPushedTexture" file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Down">
|
||||
<Size>
|
||||
<AbsDimension x="24" y="24"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT"/>
|
||||
</Anchors>
|
||||
</PushedTexture>
|
||||
<DisabledTexture name="$parentDisabledTexture" file="Interface\ChatFrame\UI-ChatIcon-ScrollDown-Disabled">
|
||||
<Size>
|
||||
<AbsDimension x="24" y="24"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT"/>
|
||||
</Anchors>
|
||||
</DisabledTexture>
|
||||
<HighlightTexture name="$parentHighlightTexture" file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD">
|
||||
<Size>
|
||||
<AbsDimension x="24" y="24"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT"/>
|
||||
</Anchors>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnHide>
|
||||
C_CloseDropDownMenus();
|
||||
</OnHide>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Script file="C_UIDropDownMenu.lua" />
|
||||
<Script file="C_EasyMenu.lua" />
|
||||
</Ui>
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
local WIDTH, HEIGHT = 858, 660
|
||||
local IOF = InterfaceOptionsFrame
|
||||
|
||||
IOF:SetPoint("CENTER", UIParent, "CENTER")
|
||||
|
||||
InterfaceOptionsFrameCategories:SetPoint("BOTTOMLEFT", IOF, "BOTTOMLEFT", 22, 50)
|
||||
InterfaceOptionsFrameAddOns:SetPoint("BOTTOMLEFT", IOF, "BOTTOMLEFT", 22, 50)
|
||||
|
||||
IOF:SetSize(WIDTH, HEIGHT)
|
||||
IOF:SetToplevel(true)
|
||||
IOF:HookScript("OnShow", function()
|
||||
IOF:SetSize(WIDTH, HEIGHT)
|
||||
end)
|
||||
@@ -0,0 +1,9 @@
|
||||
<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">
|
||||
<Include file="XML_Helper.lua" />
|
||||
<Include file="Backdrop.xml" />
|
||||
<Include file="UIPanelTemplates.xml" />
|
||||
<Include file="C_UIDropDownMenu.xml" />
|
||||
|
||||
<Script file="InterfaceOptions.lua" />
|
||||
</Ui>
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<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">
|
||||
|
||||
<Button name="UIPanelButtonNoTooltipTemplate" virtual="true">
|
||||
<Size x="40" y="22" />
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture parentKey="Left" file="Interface\Buttons\UI-Panel-Button-Up">
|
||||
<Size x="12" y="22" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" />
|
||||
<Anchor point="BOTTOMLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0" right="0.09375" top="0" bottom="0.6875" />
|
||||
</Texture>
|
||||
<Texture parentKey="Right" file="Interface\Buttons\UI-Panel-Button-Up">
|
||||
<Size x="12" y="22" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" />
|
||||
<Anchor point="BOTTOMRIGHT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.53125" right="0.625" top="0" bottom="0.6875" />
|
||||
</Texture>
|
||||
<Texture parentKey="Middle" file="Interface\Buttons\UI-Panel-Button-Up">
|
||||
<Size x="12" y="22" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeKey="$parent.Left" relativePoint="TOPRIGHT" />
|
||||
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.Right" relativePoint="BOTTOMLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.09375" right="0.53125" top="0" bottom="0.6875" />
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad function="UIPanelButton_OnLoad" />
|
||||
<OnMouseDown function="UIPanelButton_OnMouseDown" />
|
||||
<OnMouseUp function="UIPanelButton_OnMouseUp" />
|
||||
<OnShow function="UIPanelButton_OnShow" />
|
||||
<OnDisable function="UIPanelButton_OnDisable" />
|
||||
<OnEnable function="UIPanelButton_OnEnable" />
|
||||
</Scripts>
|
||||
<ButtonText name="$parentText" parentKey="Text" />
|
||||
<NormalFont style="GameFontNormal" />
|
||||
<HighlightFont style="GameFontHighlight" />
|
||||
<DisabledFont style="GameFontDisable" />
|
||||
<HighlightTexture inherits="UIPanelButtonHighlightTexture" />
|
||||
</Button>
|
||||
|
||||
</Ui>
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
--[[
|
||||
|
||||
Search Box Template
|
||||
|
||||
]]
|
||||
|
||||
function InputBoxInstructions_OnTextChanged(self)
|
||||
self.Instructions:SetShown(self:GetText() == "")
|
||||
end
|
||||
|
||||
function InputBoxInstructions_UpdateColorForEnabledState(self, color)
|
||||
if color then
|
||||
self:SetTextColor(color:GetRGBA());
|
||||
end
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnDisable(self)
|
||||
InputBoxInstructions_UpdateColorForEnabledState(self, self.disabledColor);
|
||||
end
|
||||
|
||||
function InputBoxInstructions_OnEnable(self)
|
||||
InputBoxInstructions_UpdateColorForEnabledState(self, self.enabledColor);
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnLoad(self)
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self:SetTextInsets(16, 20, 0, 0);
|
||||
self.Instructions:SetText(SEARCH);
|
||||
self.Instructions:ClearAllPoints();
|
||||
self.Instructions:SetPoint("TOPLEFT", self, "TOPLEFT", 16, 0);
|
||||
self.Instructions:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -20, 0);
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnEditFocusLost(self)
|
||||
if ( self:GetText() == "" ) then
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self.clearButton:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnEditFocusGained(self)
|
||||
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
|
||||
self.clearButton:Show();
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnTextChanged(self)
|
||||
if ( not self:HasFocus() and self:GetText() == "" ) then
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self.clearButton:Hide();
|
||||
else
|
||||
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
|
||||
self.clearButton:Show();
|
||||
end
|
||||
InputBoxInstructions_OnTextChanged(self);
|
||||
end
|
||||
|
||||
function SearchBoxTemplateClearButton_OnClick(self)
|
||||
--PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
|
||||
local editBox = self:GetParent();
|
||||
editBox:SetText("");
|
||||
editBox:ClearFocus();
|
||||
end
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
<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="SharedUIPanelTemplates.lua" />
|
||||
|
||||
<EditBox name="InputBoxInstructionsTemplate" inherits="InputBoxTemplate" virtual="true">
|
||||
<!--KeyValues>
|
||||
Optional
|
||||
<KeyValue key="disabledColor" value="GRAY_FONT_COLOR" type="global"/>
|
||||
<KeyValue key="enabledColor" value="HIGHLIGHT_FONT_COLOR" type="global"/>
|
||||
</KeyValues-->
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString parentKey="Instructions" justifyH="LEFT" justifyV="MIDDLE" inherits="GameFontDisableSmall" setAllPoints="true">
|
||||
<Color r="0.35" g="0.35" b="0.35"/>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnTextChanged function="InputBoxInstructions_OnTextChanged"/>
|
||||
<OnDisable function="InputBoxInstructions_OnDisable"/>
|
||||
<OnEnable function="InputBoxInstructions_OnEnable"/>
|
||||
</Scripts>
|
||||
<FontString inherits="GameFontHighlightSmall"/>
|
||||
</EditBox>
|
||||
|
||||
<EditBox name="SearchBoxTemplate" inherits="InputBoxInstructionsTemplate" autoFocus="false" virtual="true">
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<Texture name="$parentSearchIcon" atlas="common-search-magnifyingglass" useAtlasSize="false" parentKey="searchIcon">
|
||||
<Size x="10" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" x="1" y="-1"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentClearButton" parentKey="clearButton" hidden="true">
|
||||
<Size x="17" y="17"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT" x="-3" y="0"/>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture atlas="common-search-clearbutton" useAtlasSize="false" alpha="0.5" parentKey="texture">
|
||||
<Size x="10" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="3" y="-3"/>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnEnter>
|
||||
self.texture:SetAlpha(1.0);
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
self.texture:SetAlpha(0.5);
|
||||
</OnLeave>
|
||||
<OnMouseDown>
|
||||
if self:IsEnabled() then
|
||||
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 4, -4);
|
||||
end
|
||||
</OnMouseDown>
|
||||
<OnMouseUp>
|
||||
self.texture:SetPoint("TOPLEFT", self, "TOPLEFT", 3, -3);
|
||||
</OnMouseUp>
|
||||
<OnClick function="SearchBoxTemplateClearButton_OnClick"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad function="SearchBoxTemplate_OnLoad"/>
|
||||
<OnEscapePressed function="EditBox_ClearFocus"/>
|
||||
<OnEnterPressed function="EditBox_ClearFocus"/>
|
||||
<OnEditFocusLost function="SearchBoxTemplate_OnEditFocusLost"/>
|
||||
<OnEditFocusGained function="SearchBoxTemplate_OnEditFocusGained"/>
|
||||
<OnTextChanged function="SearchBoxTemplate_OnTextChanged"/>
|
||||
</Scripts>
|
||||
</EditBox>
|
||||
|
||||
<Slider name="HorizontalSliderTemplate" orientation="HORIZONTAL" enableMouse="true" virtual="true" inherits="BackdropTemplate">
|
||||
<HitRectInsets>
|
||||
<AbsInset left="0" right="0" top="-10" bottom="-10" />
|
||||
</HitRectInsets>
|
||||
<ThumbTexture parentKey="Thumb" name="$parentThumb" file="Interface\Buttons\UI-SliderBar-Button-Horizontal">
|
||||
<Size x="32" y="32" />
|
||||
</ThumbTexture>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self.backdropInfo = _G["BACKDROP_SLIDER_8_8"]
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Slider>
|
||||
|
||||
<Frame name="DialogBorderNoCenterTemplate" useParentLevel="true" frameLevel="15" setAllPoints="true" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
self.layoutType = "Dialog"
|
||||
Mixin(self, NineSlicePanelMixin)
|
||||
self:OnLoad()
|
||||
</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<Frame name="DialogBorderDarkTemplate" inherits="DialogBorderNoCenterTemplate" virtual="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<!-- TODO: Convert this to atlas -->
|
||||
<Texture parentKey="Bg" file="Interface\DialogFrame\UI-DialogBox-Background-Dark" horizTile="true" vertTile="true">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" x="7" y="-7" />
|
||||
<Anchor point="BOTTOMRIGHT" x="-7" y="7" />
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
|
||||
<Button name="UIPanelInfoButton" virtual="true">
|
||||
<Size>
|
||||
<AbsDimension x="16" y="16" />
|
||||
</Size>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTexture" file="Interface\FriendsFrame\InformationIcon" parentKey="texture">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" />
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnMouseDown>
|
||||
self.texture:SetPoint("TOPLEFT", 1, -1);
|
||||
</OnMouseDown>
|
||||
<OnMouseUp>
|
||||
self.texture:SetPoint("TOPLEFT", 0, 0);
|
||||
</OnMouseUp>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\FriendsFrame\InformationIcon-Highlight" alphaMode="ADD">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentTexture" relativePoint="TOPLEFT" />
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1" a="0.4" />
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
|
||||
<Button name="UIMenuButtonStretchTemplate" virtual="true">
|
||||
<Size x="40" y="26" />
|
||||
<Layers>
|
||||
<Layer level="BORDER">
|
||||
<Texture parentKey="TopLeft" name="$parentTopLeft" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="12" y="6" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0" right="0.09375" top="0" bottom="0.1875" />
|
||||
</Texture>
|
||||
<Texture parentKey="TopRight" name="$parentTopRight" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="12" y="6" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.53125" right="0.625" top="0" bottom="0.1875" />
|
||||
</Texture>
|
||||
<Texture parentKey="BottomLeft" name="$parentBottomLeft" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="12" y="6" />
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0" right="0.09375" top="0.625" bottom="0.8125" />
|
||||
</Texture>
|
||||
<Texture parentKey="BottomRight" name="$parentBottomRight" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="12" y="6" />
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.53125" right="0.625" top="0.625" bottom="0.8125" />
|
||||
</Texture>
|
||||
<Texture parentKey="TopMiddle" name="$parentTopMiddle" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="56" y="6" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentTopLeft" relativePoint="TOPRIGHT" />
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentTopRight" relativePoint="BOTTOMLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.09375" right="0.53125" top="0" bottom="0.1875" />
|
||||
</Texture>
|
||||
<Texture parentKey="MiddleLeft" name="$parentMiddleLeft" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="12" y="14" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentTopLeft" relativePoint="BOTTOMRIGHT" />
|
||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentBottomLeft" relativePoint="TOPLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0" right="0.09375" top="0.1875" bottom="0.625" />
|
||||
</Texture>
|
||||
<Texture parentKey="MiddleRight" name="$parentMiddleRight" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="12" y="14" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT" relativeTo="$parentTopRight" relativePoint="BOTTOMRIGHT" />
|
||||
<Anchor point="BOTTOMLEFT" relativeTo="$parentBottomRight" relativePoint="TOPLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.53125" right="0.625" top="0.1875" bottom="0.625" />
|
||||
</Texture>
|
||||
<Texture parentKey="BottomMiddle" name="$parentBottomMiddle" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="56" y="6" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentBottomLeft" relativePoint="TOPRIGHT" />
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBottomRight" relativePoint="BOTTOMLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.09375" right="0.53125" top="0.625" bottom="0.8125" />
|
||||
</Texture>
|
||||
<Texture parentKey="MiddleMiddle" name="$parentMiddleMiddle" file="Interface\Buttons\UI-Silver-Button-Up">
|
||||
<Size x="56" y="14" />
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentTopLeft" relativePoint="BOTTOMRIGHT" />
|
||||
<Anchor point="BOTTOMRIGHT" relativeTo="$parentBottomRight" relativePoint="TOPLEFT" />
|
||||
</Anchors>
|
||||
<TexCoords left="0.09375" right="0.53125" top="0.1875" bottom="0.625" />
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnMouseDown>
|
||||
if self:IsEnabled() ~= 0 then
|
||||
self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Down")
|
||||
if ( self.Icon ) then
|
||||
if ( not self.Icon.oldPoint ) then
|
||||
local point, relativeTo, relativePoint, x, y = self.Icon:GetPoint(1)
|
||||
self.Icon.oldPoint = point
|
||||
self.Icon.oldX = x
|
||||
self.Icon.oldY = y
|
||||
end
|
||||
self.Icon:SetPoint(self.Icon.oldPoint, self.Icon.oldX + 1, self.Icon.oldY - 1)
|
||||
end
|
||||
end
|
||||
</OnMouseDown>
|
||||
<OnMouseUp>
|
||||
if self:IsEnabled() ~= 0 then
|
||||
self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
if ( self.Icon ) then
|
||||
self.Icon:SetPoint(self.Icon.oldPoint, self.Icon.oldX, self.Icon.oldY)
|
||||
end
|
||||
end
|
||||
</OnMouseUp>
|
||||
<OnShow>
|
||||
if self:IsEnabled() ~= 0 then
|
||||
-- we need to reset our textures just in case we were hidden before a mouse up fired
|
||||
self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
end
|
||||
</OnShow>
|
||||
<OnEnable>
|
||||
self.TopLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.TopRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.TopMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleLeft:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleRight:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.BottomMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
self.MiddleMiddle:SetTexture("Interface\\Buttons\\UI-Silver-Button-Up")
|
||||
</OnEnable>
|
||||
<OnEnter>
|
||||
if(self.tooltipText ~= nil) then
|
||||
GameTooltip_AddNewbieTip(self, self.tooltipText, 1.0, 1.0, 1.0, self.newbieText)
|
||||
end
|
||||
</OnEnter>
|
||||
<OnLeave>
|
||||
if(self.tooltipText ~= nil) then
|
||||
GameTooltip:Hide()
|
||||
end
|
||||
</OnLeave>
|
||||
</Scripts>
|
||||
<ButtonText parentKey="Text" name="$parentText">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER" x="0" y="-1" />
|
||||
</Anchors>
|
||||
</ButtonText>
|
||||
<NormalFont style="GameFontHighlightSmall" />
|
||||
<HighlightFont style="GameFontHighlightSmall" />
|
||||
<DisabledFont style="GameFontDisableSmall" />
|
||||
<HighlightTexture file="Interface\Buttons\UI-Silver-Button-Highlight" alphaMode="ADD">
|
||||
<TexCoords left="0" right="1.0" top="0.03" bottom="0.7175" />
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
|
||||
</Ui>
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
local function UIDropDownMenu_CreateUnChecked(object)
|
||||
local uncheck = _G[object:GetName().."UnCheck"];
|
||||
if ( uncheck ) then
|
||||
return;
|
||||
end
|
||||
|
||||
uncheck = object:CreateTexture("$parentUnCheck", "BORDER");
|
||||
_G[object:GetName().."UnCheck"] = uncheck;
|
||||
uncheck:SetSize(16, 16);
|
||||
uncheck:SetPoint("Left");
|
||||
end
|
||||
|
||||
hooksecurefunc("UIDropDownMenu_AddButton", function(info, level)
|
||||
if ( not level ) then
|
||||
level = 1;
|
||||
end
|
||||
|
||||
local listFrame = _G["DropDownList"..level];
|
||||
local index = listFrame and listFrame.numButtons or 1;
|
||||
|
||||
listFrame = listFrame or _G["DropDownList"..level];
|
||||
local listFrameName = listFrame:GetName();
|
||||
|
||||
local button = _G[listFrameName.."Button"..index];
|
||||
UIDropDownMenu_CreateUnChecked(button);
|
||||
|
||||
if ( not info.notCheckable ) then
|
||||
local check = _G[listFrameName.."Button"..index.."Check"];
|
||||
local uncheck = _G[listFrameName.."Button"..index.."UnCheck"];
|
||||
if ( info.disabled ) then
|
||||
check:SetDesaturated(true);
|
||||
check:SetAlpha(0.5);
|
||||
uncheck:SetDesaturated(true);
|
||||
uncheck:SetAlpha(0.5);
|
||||
else
|
||||
check:SetDesaturated(false);
|
||||
check:SetAlpha(1);
|
||||
uncheck:SetDesaturated(false);
|
||||
uncheck:SetAlpha(1);
|
||||
end
|
||||
|
||||
if ( info.isRadio ) then
|
||||
--check:SetTexCoord(0.0, 0.5, 0.5, 1.0);
|
||||
check:SetTexCoord(0.25, 0.5, 0, 1);
|
||||
check:SetTexture("Interface\\Buttons\\UI-RadioButton");
|
||||
--uncheck:SetTexCoord(0.5, 1.0, 0.5, 1.0);
|
||||
uncheck:SetTexCoord(0.75, 1, 0, 1);
|
||||
uncheck:SetTexture("Interface\\Buttons\\UI-RadioButton");
|
||||
else
|
||||
check:SetTexCoord(0, 1, 0, 1);
|
||||
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check");
|
||||
end
|
||||
|
||||
-- Checked can be a function now
|
||||
local checked = info.checked;
|
||||
if ( type(checked) == "function" ) then
|
||||
checked = checked(button);
|
||||
end
|
||||
-- Show the check if checked
|
||||
if ( checked ) then
|
||||
button:LockHighlight();
|
||||
check:Show();
|
||||
uncheck:Hide();
|
||||
else
|
||||
button:UnlockHighlight();
|
||||
check:Hide();
|
||||
uncheck:Show();
|
||||
end
|
||||
else
|
||||
_G[listFrameName.."Button"..index.."Check"]:Hide();
|
||||
_G[listFrameName.."Button"..index.."UnCheck"]:Hide();
|
||||
end
|
||||
|
||||
if ( not info.isRadio ) then
|
||||
_G[listFrameName.."Button"..index.."UnCheck"]:Hide();
|
||||
end
|
||||
|
||||
button.checked = info.checked;
|
||||
end)
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
function SearchBoxTemplate_OnLoad(self)
|
||||
self:SetText(SEARCH);
|
||||
self:SetFontObject("GameFontDisable");
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
self:SetTextInsets(16, 20, 0, 0);
|
||||
end
|
||||
|
||||
function SearchBoxTemplate_OnEditFocusLost(self)
|
||||
self:HighlightText(0, 0);
|
||||
self:SetFontObject("GameFontDisable");
|
||||
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
|
||||
if ( self:GetText() == "" or self:GetText() == SEARCH ) then
|
||||
self:SetText(SEARCH);
|
||||
self.clearButton:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
function SerachBoxTemplate_OnEditFocusGained(self)
|
||||
self:HighlightText();
|
||||
self:SetFontObject("ChatFontSmall");
|
||||
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
|
||||
if ( self:GetText() == SEARCH ) then
|
||||
self:SetText("")
|
||||
end
|
||||
self.clearButton:Show();
|
||||
end
|
||||
|
||||
ITEM_SEARCHBAR_LIST = {
|
||||
"BagItemSearchBox",
|
||||
"GuildItemSearchBox",
|
||||
"VoidItemSearchBox",
|
||||
"BankItemSearchBox",
|
||||
};
|
||||
|
||||
function BagSearch_OnHide(self)
|
||||
local allClosed = true;
|
||||
for _,barName in pairs(ITEM_SEARCHBAR_LIST) do
|
||||
local bar = _G[barName];
|
||||
if bar and bar ~= self and bar:IsVisible() then
|
||||
allClosed = false;
|
||||
end
|
||||
end
|
||||
if ( allClosed ) then
|
||||
self.clearButton:Click();
|
||||
BagSearch_OnTextChanged(self);
|
||||
end
|
||||
end
|
||||
|
||||
function BagSearch_OnTextChanged(self, userChanged)
|
||||
local text = self:GetText();
|
||||
if ( text == SEARCH ) then
|
||||
text = "";
|
||||
end
|
||||
SetItemSearch(text);
|
||||
if (text ~= "") then
|
||||
self.clearButton:Show();
|
||||
else
|
||||
self.clearButton:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
function BagSearch_OnChar(self, text)
|
||||
-- clear focus if the player is repeating keys (ie - trying to move)
|
||||
-- TODO: move into base editbox code?
|
||||
local MIN_REPEAT_CHARACTERS = 3
|
||||
local searchString = self:GetText();
|
||||
if (string.len(searchString) > MIN_REPEAT_CHARACTERS) then
|
||||
local repeatChar = true;
|
||||
for i=1, MIN_REPEAT_CHARACTERS, 1 do
|
||||
if ( string.sub(searchString,(0-i), (0-i)) ~= string.sub(searchString,(-1-i),(-1-i)) ) then
|
||||
repeatChar = false;
|
||||
break;
|
||||
end
|
||||
end
|
||||
if ( repeatChar ) then
|
||||
self:ClearFocus();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BagSearch_OnEditFocusGained(self)
|
||||
SerachBoxTemplate_OnEditFocusGained(self);
|
||||
|
||||
for _,barName in pairs(ITEM_SEARCHBAR_LIST) do
|
||||
local bar = _G[barName];
|
||||
if bar and bar ~= self then
|
||||
bar:SetText(SEARCH);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function BagSearch_OnEditFocusLost(self)
|
||||
SerachBoxTemplate_OnEditFocusGained(self);
|
||||
|
||||
local search = self:GetText();
|
||||
for _,barName in pairs(ITEM_SEARCHBAR_LIST) do
|
||||
local bar = _G[barName];
|
||||
if bar and bar ~= self then
|
||||
bar:SetText(search);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Scrollframe functions
|
||||
function ScrollBar_AdjustAnchors(scrollBar, topAdj, bottomAdj, xAdj)
|
||||
-- assumes default anchoring of topleft-topright, bottomleft-bottomright
|
||||
local topY = 0;
|
||||
local bottomY = 0;
|
||||
local point, parent, refPoint, x, y;
|
||||
for i = 1, 2 do
|
||||
point, parent, refPoint, x, y = scrollBar:GetPoint(i);
|
||||
if ( point == "TOPLEFT" ) then
|
||||
topY = y;
|
||||
elseif ( point == "BOTTOMLEFT" ) then
|
||||
bottomY = y;
|
||||
end
|
||||
end
|
||||
xAdj = xAdj or 0;
|
||||
topAdj = topAdj or 0;
|
||||
bottomAdj = bottomAdj or 0;
|
||||
scrollBar:SetPoint("TOPLEFT", parent, "TOPRIGHT", x + xAdj, topY + topAdj);
|
||||
scrollBar:SetPoint("BOTTOMLEFT", parent, "BOTTOMRIGHT", x + xAdj, bottomY + bottomAdj);
|
||||
end
|
||||
|
||||
-- Truncated Button code
|
||||
|
||||
function TruncatedButton_OnEnter(self)
|
||||
local text = _G[self:GetName().."Text"];
|
||||
if ( text:IsTruncated() ) then
|
||||
GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
|
||||
GameTooltip:SetText(text:GetText());
|
||||
GameTooltip:Show();
|
||||
end
|
||||
end
|
||||
|
||||
function TruncatedButton_OnLeave(self)
|
||||
if ( GameTooltip:GetOwner() == self ) then
|
||||
GameTooltip:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
-- Magic Button code
|
||||
function MagicButton_OnLoad(self)
|
||||
local leftHandled = false;
|
||||
local rightHandled = false;
|
||||
|
||||
-- Find out where this button is anchored and adjust positions/separators as necessary
|
||||
for i=1, self:GetNumPoints() do
|
||||
local point, relativeTo, relativePoint, offsetX, offsetY = self:GetPoint(i);
|
||||
|
||||
if (relativeTo:GetObjectType() == "Button" and (point == "TOPLEFT" or point == "LEFT")) then
|
||||
|
||||
if (offsetX == 0 and offsetY == 0) then
|
||||
self:SetPoint(point, relativeTo, relativePoint, 1, 0);
|
||||
end
|
||||
|
||||
if (relativeTo.RightSeparator) then
|
||||
-- Modify separator to make it a Middle
|
||||
self.LeftSeparator = relativeTo.RightSeparator;
|
||||
else
|
||||
-- Add a Middle separator
|
||||
self.LeftSeparator = self:CreateTexture(self:GetName().."_LeftSeparator", "BORDER");
|
||||
relativeTo.RightSeparator = self.LeftSeparator;
|
||||
end
|
||||
|
||||
self.LeftSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
|
||||
self.LeftSeparator:SetTexCoord("0.00781250", "0.10937500", "0.75781250", "0.95312500");
|
||||
self.LeftSeparator:SetWidth(13);
|
||||
self.LeftSeparator:SetHeight(25);
|
||||
self.LeftSeparator:SetPoint("TOPRIGHT", self, "TOPLEFT", 5, 1);
|
||||
|
||||
leftHandled = true;
|
||||
|
||||
elseif (relativeTo:GetObjectType() == "Button" and (point == "TOPRIGHT" or point == "RIGHT")) then
|
||||
|
||||
if (offsetX == 0 and offsetY == 0) then
|
||||
self:SetPoint(point, relativeTo, relativePoint, -1, 0);
|
||||
end
|
||||
|
||||
if (relativeTo.LeftSeparator) then
|
||||
-- Modify separator to make it a Middle
|
||||
self.RightSeparator = relativeTo.LeftSeparator;
|
||||
else
|
||||
-- Add a Middle separator
|
||||
self.RightSeparator = self:CreateTexture(self:GetName().."_RightSeparator", "BORDER");
|
||||
relativeTo.LeftSeparator = self.RightSeparator;
|
||||
end
|
||||
|
||||
self.RightSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
|
||||
self.RightSeparator:SetTexCoord("0.00781250", "0.10937500", "0.75781250", "0.95312500");
|
||||
self.RightSeparator:SetWidth(13);
|
||||
self.RightSeparator:SetHeight(25);
|
||||
self.RightSeparator:SetPoint("TOPLEFT", self, "TOPRIGHT", -5, 1);
|
||||
|
||||
rightHandled = true;
|
||||
|
||||
elseif (point == "BOTTOMLEFT") then
|
||||
if (offsetX == 0 and offsetY == 0) then
|
||||
self:SetPoint(point, relativeTo, relativePoint, 4, 4);
|
||||
end
|
||||
leftHandled = true;
|
||||
elseif (point == "BOTTOMRIGHT") then
|
||||
if (offsetX == 0 and offsetY == 0) then
|
||||
self:SetPoint(point, relativeTo, relativePoint, -6, 4);
|
||||
end
|
||||
rightHandled = true;
|
||||
elseif (point == "BOTTOM") then
|
||||
if (offsetY == 0) then
|
||||
self:SetPoint(point, relativeTo, relativePoint, 0, 4);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If this button didn't have a left anchor, add the left border texture
|
||||
if (not leftHandled) then
|
||||
if (not self.LeftSeparator) then
|
||||
-- Add a Left border
|
||||
self.LeftSeparator = self:CreateTexture(self:GetName().."_LeftSeparator", "BORDER");
|
||||
self.LeftSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
|
||||
self.LeftSeparator:SetTexCoord("0.24218750", "0.32812500", "0.63281250", "0.82812500");
|
||||
self.LeftSeparator:SetWidth(11);
|
||||
self.LeftSeparator:SetHeight(25);
|
||||
self.LeftSeparator:SetPoint("TOPRIGHT", self, "TOPLEFT", 6, 1);
|
||||
end
|
||||
end
|
||||
|
||||
-- If this button didn't have a right anchor, add the right border texture
|
||||
if (not rightHandled) then
|
||||
if (not self.RightSeparator) then
|
||||
-- Add a Right border
|
||||
self.RightSeparator = self:CreateTexture(self:GetName().."_RightSeparator", "BORDER");
|
||||
self.RightSeparator:SetTexture("Interface\\AddOns\\!!!ClassicAPI\\Texture\\FrameGeneral\\UI-Frame");
|
||||
self.RightSeparator:SetTexCoord("0.90625000", "0.99218750", "0.00781250", "0.20312500");
|
||||
self.RightSeparator:SetWidth(11);
|
||||
self.RightSeparator:SetHeight(25);
|
||||
self.RightSeparator:SetPoint("TOPLEFT", self, "TOPRIGHT", -6, 1);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- ButtonFrameTemplate code
|
||||
function ButtonFrameTemplate_HideButtonBar(self)
|
||||
if self.bottomInset then
|
||||
self.bottomInset:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_OFFSET);
|
||||
else
|
||||
_G[self:GetName() .. "Inset"]:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_OFFSET);
|
||||
end
|
||||
_G[self:GetName() .. "BtnCornerLeft"]:Hide();
|
||||
_G[self:GetName() .. "BtnCornerRight"]:Hide();
|
||||
_G[self:GetName() .. "ButtonBottomBorder"]:Hide();
|
||||
end
|
||||
|
||||
function ButtonFrameTemplate_ShowButtonBar(self)
|
||||
if self.topInset then
|
||||
self.topInset:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_BUTTON_OFFSET);
|
||||
else
|
||||
_G[self:GetName() .. "Inset"]:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", PANEL_INSET_RIGHT_OFFSET, PANEL_INSET_BOTTOM_BUTTON_OFFSET);
|
||||
end
|
||||
_G[self:GetName() .. "BtnCornerLeft"]:Show();
|
||||
_G[self:GetName() .. "BtnCornerRight"]:Show();
|
||||
_G[self:GetName() .. "ButtonBottomBorder"]:Show();
|
||||
end
|
||||
|
||||
function ButtonFrameTemplate_HideAttic(self)
|
||||
if self.topInset then
|
||||
self.topInset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_TOP_OFFSET);
|
||||
else
|
||||
self.Inset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_TOP_OFFSET);
|
||||
end
|
||||
self.TopTileStreaks:Hide();
|
||||
end
|
||||
|
||||
function ButtonFrameTemplate_ShowAttic(self)
|
||||
if self.topInset then
|
||||
self.topInset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_ATTIC_OFFSET);
|
||||
else
|
||||
self.Inset:SetPoint("TOPLEFT", self, "TOPLEFT", PANEL_INSET_LEFT_OFFSET, PANEL_INSET_ATTIC_OFFSET);
|
||||
end
|
||||
self.TopTileStreaks:Show();
|
||||
end
|
||||
|
||||
|
||||
function ButtonFrameTemplate_HidePortrait(self)
|
||||
self.portrait:Hide();
|
||||
self.portraitFrame:Hide();
|
||||
self.topLeftCorner:Show();
|
||||
self.topBorderBar:SetPoint("TOPLEFT", self.topLeftCorner, "TOPRIGHT", 0, 0);
|
||||
self.leftBorderBar:SetPoint("TOPLEFT", self.topLeftCorner, "BOTTOMLEFT", 0, 0);
|
||||
end
|
||||
|
||||
|
||||
function ButtonFrameTemplate_ShowPortrait(self)
|
||||
self.portrait:Show();
|
||||
self.portraitFrame:Show();
|
||||
self.topLeftCorner:Hide();
|
||||
self.topBorderBar:SetPoint("TOPLEFT", self.portraitFrame, "TOPRIGHT", 0, -10);
|
||||
self.leftBorderBar:SetPoint("TOPLEFT", self.portraitFrame, "BOTTOMLEFT", 8, 0);
|
||||
end
|
||||
|
||||
|
||||
-- SquareButton template code
|
||||
SQUARE_BUTTON_TEXCOORDS = {
|
||||
["UP"] = { 0.45312500, 0.64062500, 0.01562500, 0.20312500};
|
||||
["DOWN"] = { 0.45312500, 0.64062500, 0.20312500, 0.01562500};
|
||||
["LEFT"] = { 0.23437500, 0.42187500, 0.01562500, 0.20312500};
|
||||
["RIGHT"] = { 0.42187500, 0.23437500, 0.01562500, 0.20312500};
|
||||
["DELETE"] = { 0.01562500, 0.20312500, 0.01562500, 0.20312500};
|
||||
};
|
||||
|
||||
function SquareButton_SetIcon(self, name)
|
||||
local coords = SQUARE_BUTTON_TEXCOORDS[strupper(name)];
|
||||
if (coords) then
|
||||
self.icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Cap progress bar
|
||||
function CapProgressBar_SetNotches(capBar, count)
|
||||
local barWidth = capBar:GetWidth();
|
||||
local barName = capBar:GetName();
|
||||
|
||||
if ( capBar.notchCount and capBar.notchCount > count ) then
|
||||
for i = count + 1, capBar.notchCount do
|
||||
_G[barName.."Divider"..i]:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
local notchWidth = barWidth / count;
|
||||
|
||||
for i=1, count - 1 do
|
||||
local notch = _G[barName.."Divider"..i];
|
||||
if ( not notch ) then
|
||||
notch = capBar:CreateTexture(barName.."Divider"..i, "BORDER", "CapProgressBarDividerTemplate", -1);
|
||||
end
|
||||
notch:ClearAllPoints();
|
||||
notch:SetPoint("LEFT", capBar, "LEFT", notchWidth * i - 2, 0);
|
||||
end
|
||||
capBar.notchCount = count;
|
||||
end
|
||||
|
||||
function CapProgressBar_Update(capBar, cap1Quantity, cap1Limit, cap2Quantity, cap2Limit, totalQuantity, totalLimit, hasNoSharedStats)
|
||||
if ( totalLimit == 0) then
|
||||
return;
|
||||
end
|
||||
|
||||
local barWidth = capBar:GetWidth();
|
||||
local sizePerPoint = barWidth / totalLimit;
|
||||
local progressWidth = totalQuantity * sizePerPoint;
|
||||
|
||||
local cap1Width, cap2Width;
|
||||
if ( cap2Quantity and cap2Limit ) then
|
||||
cap1Width = min(cap1Limit - cap1Quantity, cap2Limit - cap2Quantity) * sizePerPoint; --cap1 can't go past the cap2 LFG limit either.
|
||||
cap2Width = (cap2Limit - cap2Quantity) * sizePerPoint - cap1Width;
|
||||
else
|
||||
cap1Width = (cap1Limit - cap1Quantity) * sizePerPoint;
|
||||
cap2Width = 0;
|
||||
end
|
||||
|
||||
--Don't let it go past the end.
|
||||
progressWidth = min(progressWidth, barWidth);
|
||||
cap1Width = min(cap1Width, barWidth - progressWidth);
|
||||
cap2Width = min(cap2Width, barWidth - progressWidth - cap1Width);
|
||||
capBar.progress:SetWidth(progressWidth);
|
||||
|
||||
capBar.cap1:SetWidth(cap1Width);
|
||||
capBar.cap2:SetWidth(cap2Width);
|
||||
|
||||
local lastFrame, lastRelativePoint = capBar, "LEFT";
|
||||
|
||||
if ( progressWidth > 0 ) then
|
||||
capBar.progress:Show();
|
||||
capBar.progress:SetPoint("LEFT", lastFrame, lastRelativePoint, 0, 0);
|
||||
lastFrame, lastRelativePoint = capBar.progress, "RIGHT";
|
||||
else
|
||||
capBar.progress:Hide();
|
||||
end
|
||||
|
||||
if ( cap1Width > 0 and not hasNoSharedStats) then
|
||||
capBar.cap1:Show();
|
||||
capBar.cap1Marker:Show();
|
||||
capBar.cap1:SetPoint("LEFT", lastFrame, lastRelativePoint, 0, 0);
|
||||
lastFrame, lastRelativePoint = capBar.cap1, "RIGHT";
|
||||
else
|
||||
capBar.cap1:Hide();
|
||||
capBar.cap1Marker:Hide();
|
||||
end
|
||||
|
||||
if ( cap2Width > 0 and not hasNoSharedStats) then
|
||||
capBar.cap2:Show();
|
||||
capBar.cap2Marker:Show();
|
||||
capBar.cap2:SetPoint("LEFT", lastFrame, lastRelativePoint, 0, 0);
|
||||
lastFrame, lastRelativePoint = capBar.cap2, "RIGHT";
|
||||
else
|
||||
capBar.cap2:Hide();
|
||||
capBar.cap2Marker:Hide();
|
||||
end
|
||||
end
|
||||
+2381
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
function XML_NineSlice(Self, Layout)
|
||||
Self.layoutType = Layout
|
||||
Mixin(Self, NineSlicePanelMixin)
|
||||
Self:OnLoad()
|
||||
end
|
||||
|
||||
function XML_UseParentLevel(Self)
|
||||
Self:SetFrameLevel(Self:GetParent():GetFrameLevel())
|
||||
end
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,166 @@
|
||||
local _G = _G
|
||||
local Mod = mod
|
||||
local Next = next
|
||||
local PCall = pcall
|
||||
local Error = error
|
||||
local Ceil = math.ceil
|
||||
local GetTime = GetTime
|
||||
local GetCVar = GetCVar
|
||||
local Sub = string.gsub
|
||||
local String = tostring
|
||||
local Floor = math.floor
|
||||
local SecureCall = securecall
|
||||
local GetAddOnInfo = GetAddOnInfo
|
||||
local IsAddOnLoaded = IsAddOnLoaded
|
||||
local IsAddOnLoadOnDemand = IsAddOnLoadOnDemand
|
||||
|
||||
function InGlue()
|
||||
return false
|
||||
end
|
||||
|
||||
function AnimateTexCoords(Self, Width, Height, FrameW, FrameH, NumFrames, Elapsed, Throt)
|
||||
local Throt = Throt or Self.throttle or 0.1
|
||||
|
||||
if ( not Self.frame ) then
|
||||
Self.frame = 1
|
||||
Self.Throt = Throt
|
||||
Self.numColumns = Floor(Width / FrameW)
|
||||
Self.numRows = Floor(Height / FrameH)
|
||||
Self.columnWidth = FrameW / Width
|
||||
Self.rowHeight = FrameH / Height
|
||||
end
|
||||
|
||||
if ( not Self.Throt or Self.Throt > Throt ) then
|
||||
local Frame = Self.frame
|
||||
local FramesToAdvance = Floor(Self.Throt / Throt)
|
||||
while Frame + FramesToAdvance > NumFrames do
|
||||
Frame = Frame - NumFrames
|
||||
end
|
||||
|
||||
Frame = Frame + FramesToAdvance
|
||||
Self.Throt = 0
|
||||
local SetLeft = Mod(Frame - 1, Self.numColumns) * Self.columnWidth
|
||||
local SetRight = SetLeft + Self.columnWidth
|
||||
local SetBottom = Ceil(Frame / Self.numColumns) * Self.rowHeight
|
||||
local SetTop = SetBottom - Self.rowHeight
|
||||
Self:SetTexCoord(SetLeft, SetRight, SetTop, SetBottom)
|
||||
Self.frame = Frame
|
||||
else
|
||||
Self.Throt = Self.Throt + Elapsed
|
||||
end
|
||||
end
|
||||
|
||||
function GetTexCoordsForRoleSmallCircle(Role)
|
||||
if ( Role == "TANK" ) then
|
||||
return 0, 19/64, 22/64, 41/64
|
||||
elseif ( Role == "HEALER" ) then
|
||||
return 20/64, 39/64, 1/64, 20/64
|
||||
elseif ( Role == "DAMAGER" ) then
|
||||
return 20/64, 39/64, 22/64, 41/64
|
||||
else
|
||||
Error("Unknown role: "..String(Role))
|
||||
end
|
||||
end
|
||||
|
||||
function PassClickToParent(Self, ...)
|
||||
-- This is def not working on secure frames.
|
||||
local Parent = Self:GetParent()
|
||||
local OnClick = Parent:GetScript("OnClick")
|
||||
if ( OnClick ) then
|
||||
OnClick(Parent, ...)
|
||||
end
|
||||
end
|
||||
|
||||
local function secureexecutenext(Table, Prev, Func, ...)
|
||||
local Key, Value = Next(Table, Prev)
|
||||
if ( Key ~= nil ) then
|
||||
PCall(Func, Key, Value, ...) -- Errors are silently discarded!
|
||||
end
|
||||
return Key
|
||||
end
|
||||
|
||||
function secureexecuterange(Table, Func, ...)
|
||||
local Key = nil
|
||||
repeat
|
||||
Key = SecureCall(secureexecutenext, Table, Key, Func, ...)
|
||||
until Key == nil
|
||||
end
|
||||
|
||||
function securecallfunction(Func, ...)
|
||||
return SecureCall(Func, ...)
|
||||
end
|
||||
|
||||
function GetAddOnEnableState(Character, Index)
|
||||
-- Can't get per-char addons, doing what we can.
|
||||
local _, _, _, Enabled, Loadable = GetAddOnInfo(Index)
|
||||
return (Enabled) and 2 or 0
|
||||
end
|
||||
|
||||
function IsAddonVersionCheckEnabled()
|
||||
return GetCVar("checkAddonVersion") == "1"
|
||||
end
|
||||
|
||||
function C_GetAddOnInfo(Index)
|
||||
local Name, Title, Notes, Enabled, Loadable, Reason, Security = GetAddOnInfo(Index)
|
||||
local NewVersion = nil
|
||||
|
||||
-- Missing "Reason" values: BANNED", "CORRUPT", "DEMAND_LOADED", "INCOMPATIBLE"
|
||||
if ( Loadable and not IsAddOnLoaded(Index) and IsAddOnLoadOnDemand(Index) ) then
|
||||
Reason = "DEMAND_LOADED"
|
||||
Loadable = nil
|
||||
end
|
||||
|
||||
return Name, Title, Notes, Loadable, Reason, Security, NewVersion
|
||||
end
|
||||
|
||||
function HasOverrideActionBar()
|
||||
return _G.BonusActionBarFrame:IsShown()
|
||||
end
|
||||
|
||||
function HasVehicleActionBar()
|
||||
return _G.VehicleMenuBar:IsShown()
|
||||
end
|
||||
|
||||
--[[function GetSchoolString(arg)
|
||||
-- TODO
|
||||
-- Return name of spell school?
|
||||
return "_UNKNOWN_"
|
||||
--print(arg)
|
||||
--SchoolStringTable[school]
|
||||
end]]
|
||||
|
||||
function GetDifficultyInfo(ID)
|
||||
--return "0 Player (Heroic)", "party", false, false, false, false, nil
|
||||
end
|
||||
|
||||
function GetServerTime()
|
||||
-- This will cause problems. We could use game time somehow.
|
||||
return GetTime()
|
||||
end
|
||||
|
||||
function GetNormalizedRealmName()
|
||||
return Sub(GetRealmName(), "[-%s]", "")
|
||||
end
|
||||
|
||||
function Ambiguate(FullName, Context)
|
||||
-- TODO: Make diff context work properly.
|
||||
return FullName
|
||||
end
|
||||
|
||||
function CombatLogGetCurrentEventInfo(Timestamp, SubEvent, SrcGUID, SrcName, SrcFlag, DstGUID, DstName, DstFlag, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12)
|
||||
if ( Timestamp ) then
|
||||
-- Modern payload (Missing)
|
||||
local HideCaster, SrcRaidFlag, DstRaidFlag = false, nil, nil
|
||||
|
||||
-- Note: Blizzard could have changed order of payload from 9th onwards.
|
||||
return Timestamp, SubEvent, HideCaster, SrcGUID, SrcName, SrcFlag, SrcRaidFlag, DstGUID, DstName, DstFlag, DstRaidFlag, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12
|
||||
end
|
||||
end
|
||||
|
||||
function ActionButton_HideOverlayGlow()
|
||||
-- TODO
|
||||
end
|
||||
|
||||
function ActionButton_ShowOverlayGlow()
|
||||
-- TODO
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
local _G = _G
|
||||
local Select = select
|
||||
local Sort = table.sort
|
||||
local Upper = string.upper
|
||||
local GetAddOnInfo = GetAddOnInfo
|
||||
|
||||
local Processor, DB, DBAddOn
|
||||
|
||||
local function Handler(Self, Event, AddOn)
|
||||
if ( Event == "ADDON_LOADED" ) then
|
||||
local Compat = DB[AddOn]
|
||||
if ( Compat ) then
|
||||
for Name, Value in pairs(Compat) do
|
||||
_G[Name] = (Value ~= "nil") and Value or nil
|
||||
end
|
||||
end
|
||||
else
|
||||
DB = nil
|
||||
DBAddOn = nil
|
||||
Self:UnregisterEvent(Event)
|
||||
Self:UnregisterEvent("ADDON_LOADED")
|
||||
Self:SetScript("OnEvent", nil)
|
||||
end
|
||||
end
|
||||
|
||||
local function FindPreviousAddOn(AddOn)
|
||||
if ( not DBAddOn ) then
|
||||
local IsAddOnLoadOnDemand = IsAddOnLoadOnDemand
|
||||
local Count = 1
|
||||
|
||||
DBAddOn = {}
|
||||
|
||||
for i=1,GetNumAddOns() do
|
||||
local Name, _, _, Enabled = GetAddOnInfo(i)
|
||||
if ( Enabled and not IsAddOnLoadOnDemand(i) ) then
|
||||
DBAddOn[Count] = Name
|
||||
Count = Count + 1
|
||||
end
|
||||
end
|
||||
|
||||
Sort(DBAddOn, function (a, b) return Upper(a) < Upper(b) end)
|
||||
end
|
||||
|
||||
for i=1,#DBAddOn do
|
||||
if ( DBAddOn[i] == AddOn and i > 1 ) then
|
||||
return DBAddOn[i-1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function Process(AddOn, Mode, ...)
|
||||
local _, _, _, Enabled = GetAddOnInfo(AddOn)
|
||||
|
||||
if ( Enabled ) then
|
||||
if ( Mode == "ALERT" ) then
|
||||
local Meta, Flag = ...
|
||||
|
||||
if ( GetAddOnMetadata(AddOn, Meta) == Flag ) then
|
||||
local PopupID = "CAPI_ADDONCOMPAT_"..AddOn
|
||||
local URL = "https://gitlab.com/users/Tsoukie/projects"
|
||||
local Width, Height = 540, 200
|
||||
|
||||
StaticPopupDialogs[PopupID] = {
|
||||
text = "ClassicAPI - Incompatible:\n\n|cffFFA500%s (%s)|r\n\nTo avoid issues please use version at:",
|
||||
button1 = CLOSE,
|
||||
timeout = 0,
|
||||
whileDead = true,
|
||||
hideOnEscape = false,
|
||||
showAlert = true,
|
||||
hasEditBox = 1,
|
||||
hasWideEditBox = 1,
|
||||
OnShow = function (Self)
|
||||
Self:SetSize(Width, Height)
|
||||
Self.maxHeightSoFar = Height
|
||||
Self.maxWidthSoFar = Width
|
||||
|
||||
local Edit = Self.wideEditBox
|
||||
Edit:SetWidth(300)
|
||||
Edit:SetText(URL)
|
||||
Edit:SetScript("OnTextChanged", function(S) S:SetText(URL) end)
|
||||
end,
|
||||
}
|
||||
|
||||
StaticPopup_Show(PopupID, AddOn, Flag)
|
||||
end
|
||||
else
|
||||
if ( not Processor ) then
|
||||
DB = {}
|
||||
Processor = CreateFrame("Frame")
|
||||
Processor:SetScript("OnEvent", Handler)
|
||||
Processor:RegisterEvent("ADDON_LOADED")
|
||||
Processor:RegisterEvent("PLAYER_LOGIN")
|
||||
end
|
||||
|
||||
if ( Mode == "NULL" ) then
|
||||
Process(FindPreviousAddOn(AddOn), "NULL_PROCESSED", ...)
|
||||
end
|
||||
|
||||
local Compat = DB[AddOn]
|
||||
if ( not Compat ) then
|
||||
if ( Mode and DB[Mode] ) then
|
||||
DB[AddOn] = DB[Mode]
|
||||
return
|
||||
end
|
||||
|
||||
Compat = {}
|
||||
DB[AddOn] = Compat
|
||||
end
|
||||
|
||||
for i=1, Select("#", ...) do
|
||||
local Obj = Select(i, ...)
|
||||
Compat[Obj] = (Mode == "NULL_PROCESSED") and "nil" or _G[Obj]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Process("WeakAuras", nil, "IsInGroup", "IsInRaid", "GetNumGroupMembers")
|
||||
Process("Details", "WeakAuras", "IsInGroup", "IsInRaid", "GetNumGroupMembers")
|
||||
Process("AI_VoiceOver", "NULL", "WOW_PROJECT_ID")
|
||||
Process("Outfitter", "NULL", "UnitGetIncomingHeals")
|
||||
Process("CompactRaidFrame", "ALERT", "Author", "RomanSpector & Blizzard")
|
||||
@@ -0,0 +1,335 @@
|
||||
|
||||
AnchorMixin = {};
|
||||
|
||||
function AnchorMixin:Init(point, relativeTo, relativePoint, x, y)
|
||||
self:Set(point, relativeTo, relativePoint, x, y);
|
||||
end
|
||||
|
||||
function AnchorMixin:Set(point, relativeTo, relativePoint, x, y)
|
||||
self.point = point;
|
||||
self.relativeTo = relativeTo;
|
||||
self.relativePoint = relativePoint;
|
||||
self.x = x;
|
||||
self.y = y;
|
||||
end
|
||||
|
||||
function AnchorMixin:SetFromPoint(region, pointIndex)
|
||||
-- TODO: Need to check if this has no point set...probably don't want default behavior in some cases, probably
|
||||
-- want to signal something or return an invalid anchor.
|
||||
self:Set(region:GetPoint(pointIndex));
|
||||
end
|
||||
|
||||
function AnchorMixin:Get()
|
||||
local point = self.point or "TOPLEFT";
|
||||
local relativePoint = self.relativePoint or "TOPLEFT";
|
||||
local x = self.x or 0;
|
||||
local y = self.y or 0;
|
||||
return point, self.relativeTo, relativePoint, x, y;
|
||||
end
|
||||
|
||||
function AnchorMixin:SetPoint(region, clearAllPoints)
|
||||
if clearAllPoints then
|
||||
region:ClearAllPoints();
|
||||
end
|
||||
region:SetPoint(self:Get());
|
||||
end
|
||||
|
||||
function AnchorMixin:SetPointWithExtraOffset(region, clearAllPoints, extraOffsetX, extraOffsetY)
|
||||
if clearAllPoints then
|
||||
region:ClearAllPoints();
|
||||
end
|
||||
local point, relativeTo, relativePoint, x, y = self:Get();
|
||||
region:SetPoint(point, relativeTo, relativePoint, x + extraOffsetX, y + extraOffsetY);
|
||||
end
|
||||
|
||||
function CreateAnchor(point, relativeTo, relativePoint, x, y)
|
||||
return CreateAndInitFromMixin(AnchorMixin, point, relativeTo, relativePoint or point, x or 0, y or 0);
|
||||
end
|
||||
|
||||
GridLayoutMixin = {};
|
||||
|
||||
-- If isVertical is true lay out columns first then rows, otherwise we lay out rows then columns
|
||||
-- So for example with a stride of 2 and 6 frames this is how they would look using TopLeftToBottomRight and TopLeftToBottomRightVertical:
|
||||
--
|
||||
-- TopLeftToBottomRight TopLeftToBottomRightVertical
|
||||
-- 1 2 1 3 5
|
||||
-- 3 4 2 4 6
|
||||
-- 5 6
|
||||
GridLayoutMixin.Direction = {
|
||||
TopLeftToBottomRight = { x = 1, y = -1 },
|
||||
TopRightToBottomLeft = { x = -1, y = -1 },
|
||||
BottomRightToTopLeft = { x = -1, y = 1 },
|
||||
TopLeftToBottomRightVertical = { x = 1, y = -1, isVertical = true },
|
||||
TopRightToBottomLeftVertical = { x = -1, y = -1, isVertical = true },
|
||||
};
|
||||
|
||||
function GridLayoutMixin:Init(direction, stride, paddingX, paddingY, horizontalSpacing, verticalSpacing)
|
||||
self.direction = direction or GridLayoutMixin.Direction.TopLeftToBottomRight;
|
||||
self.stride = stride or 1;
|
||||
self.paddingX = paddingX or 0;
|
||||
self.paddingY = paddingY or 0;
|
||||
self.horizontalSpacing = horizontalSpacing;
|
||||
self.verticalSpacing = verticalSpacing;
|
||||
end
|
||||
|
||||
function GridLayoutMixin:SetCustomOffsetFunction(func)
|
||||
self.customOffsetFunction = func;
|
||||
end
|
||||
|
||||
function GridLayoutMixin:GetCustomOffset(row, col)
|
||||
if self.customOffsetFunction then
|
||||
return self.customOffsetFunction(row, col);
|
||||
end
|
||||
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
|
||||
AnchorUtil = {};
|
||||
|
||||
AnchorUtil.CreateAnchor = GenerateClosure(CreateAndInitFromMixin, AnchorMixin);
|
||||
AnchorUtil.CreateGridLayout = GenerateClosure(CreateAndInitFromMixin, GridLayoutMixin);
|
||||
|
||||
|
||||
function AnchorUtil.CreateAnchorFromPoint(region, pointIndex)
|
||||
local anchor = AnchorUtil.CreateAnchor();
|
||||
anchor:SetFromPoint(region, pointIndex);
|
||||
return anchor;
|
||||
end
|
||||
|
||||
-- For initialAnchor and layout, use AnchorUtil.CreateAnchor(...) and AnchorUtil.CreateGridLayout(...)
|
||||
function AnchorUtil.GridLayout(frames, initialAnchor, layout)
|
||||
if #frames <= 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
local width = layout.horizontalSpacing or frames[1]:GetWidth();
|
||||
local height = layout.verticalSpacing or frames[1]:GetHeight();
|
||||
local stride = layout.stride;
|
||||
local paddingX = layout.paddingX;
|
||||
local paddingY = layout.paddingY;
|
||||
local direction = layout.direction;
|
||||
for i, frame in ipairs(frames) do
|
||||
local row = math.floor((i - 1) / stride) + 1;
|
||||
local col = (i - 1) % stride + 1;
|
||||
if direction.isVertical then
|
||||
local tempRow = row;
|
||||
row = col;
|
||||
col = tempRow;
|
||||
end
|
||||
local clearAllPoints = true;
|
||||
local customOffsetX, customOffsetY = layout:GetCustomOffset(row, col);
|
||||
local extraOffsetX = (col - 1) * (width + paddingX) * direction.x + customOffsetX;
|
||||
local extraOffsetY = (row - 1) * (height + paddingY) * direction.y + customOffsetY;
|
||||
initialAnchor:SetPointWithExtraOffset(frame, clearAllPoints, extraOffsetX, extraOffsetY);
|
||||
end
|
||||
end
|
||||
|
||||
local function GetFrameSpacing(totalSize, numElements, elementSize)
|
||||
if numElements <= 1 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
return (totalSize - (numElements * elementSize)) / (numElements - 1);
|
||||
end
|
||||
|
||||
local function SanitizeTotalSize(size)
|
||||
if not size or size == 0 then
|
||||
return math.huge;
|
||||
else
|
||||
return Round(size);
|
||||
end
|
||||
end
|
||||
|
||||
-- For initialAnchor and layout, use AnchorUtil.CreateAnchor(...) and AnchorUtil.CreateGridLayout(...)
|
||||
function AnchorUtil.GridLayoutFactoryByCount(factoryFunction, count, initialAnchor, layout)
|
||||
if count <= 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
local frames = { };
|
||||
while #frames < count do
|
||||
local frame = factoryFunction(#frames + 1);
|
||||
if not frame then
|
||||
break;
|
||||
end
|
||||
|
||||
table.insert(frames, frame);
|
||||
end
|
||||
|
||||
AnchorUtil.GridLayout(frames, initialAnchor, layout);
|
||||
|
||||
return frames;
|
||||
end
|
||||
|
||||
-- For initialAnchor, use AnchorUtil.CreateAnchor(...)
|
||||
function AnchorUtil.GridLayoutFactory(factoryFunction, initialAnchor, totalWidth, totalHeight, overrideDirection, overridePaddingX, overridePaddingY)
|
||||
local frame = factoryFunction(1);
|
||||
if not frame then
|
||||
return;
|
||||
end
|
||||
|
||||
totalWidth = SanitizeTotalSize(totalWidth);
|
||||
totalHeight = SanitizeTotalSize(totalHeight);
|
||||
|
||||
-- If we have an override padding, count it in the frame width. We add a padding to totalWidth/totalHeight to account for the
|
||||
-- extra space we save for the last element which doesn't need padding.
|
||||
local width = Round(frame:GetWidth()) + (overridePaddingX or 0);
|
||||
local height = Round(frame:GetHeight()) + (overridePaddingY or 0);
|
||||
local rowSize = math.floor((totalWidth + (overridePaddingX or 0)) / width);
|
||||
local colSize = math.floor((totalHeight + (overridePaddingY or 0)) / height);
|
||||
|
||||
local spacingX = overridePaddingX or GetFrameSpacing(totalWidth, rowSize, width);
|
||||
local spacingY = overridePaddingY or GetFrameSpacing(totalHeight, colSize, height);
|
||||
|
||||
local frames = { frame };
|
||||
while #frames < rowSize * colSize do
|
||||
local frame = factoryFunction(#frames + 1);
|
||||
if not frame then
|
||||
break;
|
||||
end
|
||||
|
||||
table.insert(frames, frame);
|
||||
end
|
||||
|
||||
local direction = overrideDirection or GridLayoutMixin.Direction.TopLeftToBottomRight;
|
||||
|
||||
AnchorUtil.GridLayout(frames, initialAnchor, AnchorUtil.CreateGridLayout(direction, rowSize, spacingX, spacingY));
|
||||
end
|
||||
|
||||
-- Mirrors an array of regions along the specified axis. For example, if horizontal, a region
|
||||
-- anchored LEFT TOPLEFT 20 20 will become anchored RIGHT TOPRIGHT -20 20.
|
||||
-- Mirror description format: {region = region, mirrorUV = [true, false]}
|
||||
local function MirrorRegionsAlongAxis(mirrorDescriptions, exchangeables, setPointWrapper, setTexCoordsWrapper)
|
||||
for _, description in ipairs(mirrorDescriptions) do
|
||||
local exchanged = {};
|
||||
|
||||
local region = description.region;
|
||||
local mirrorUV = description.mirrorUV;
|
||||
for p in pairs(exchangeables) do
|
||||
if not exchanged[p] then
|
||||
local point1, relative1, relativePoint1, x1, y1 = region:GetPointByName(p);
|
||||
if point1 then
|
||||
-- Retrieve point information for what we're replacing, if any.
|
||||
local mirrorPoint1 = exchangeables[point1];
|
||||
local point2, relative2, relativePoint2, x2, y2 = region:GetPointByName(mirrorPoint1);
|
||||
setPointWrapper(region, point1, relative1, relativePoint1, x1, y1);
|
||||
|
||||
-- If we replaced a point, mirror the information to the original point.
|
||||
if point2 then
|
||||
setPointWrapper(region, point2, relative2, relativePoint2, x2, y2);
|
||||
else
|
||||
-- Otherwise, clear the original point.
|
||||
region:ClearPoint(point1);
|
||||
end
|
||||
|
||||
exchanged[point1] = true;
|
||||
exchanged[mirrorPoint1] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if mirrorUV then
|
||||
setTexCoordsWrapper(region);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local SetPointAlongAxis = function(points, region, point, relative, relativePoint, x, y)
|
||||
local mirrorPoint = points[point];
|
||||
local mirrorRelativePoint = points[relativePoint] or relativePoint;
|
||||
region:SetPoint(mirrorPoint, relative, mirrorRelativePoint, x, y);
|
||||
end
|
||||
|
||||
local VERTICAL_MIRROR_POINTS =
|
||||
{
|
||||
["TOPLEFT"] = "BOTTOMLEFT",
|
||||
["TOP"] = "BOTTOM",
|
||||
["TOPRIGHT"] = "BOTTOMRIGHT",
|
||||
["BOTTOMLEFT"] = "TOPLEFT",
|
||||
["BOTTOM"] = "TOP",
|
||||
["BOTTOMRIGHT"] = "TOPRIGHT",
|
||||
["CENTER"] = "CENTER", -- Mirrored only along x and y offsets.
|
||||
["LEFT"] = "LEFT", -- Mirrored only along x and y offsets.
|
||||
["RIGHT"] = "RIGHT", -- Mirrored only along x and y offsets.
|
||||
};
|
||||
|
||||
local SetPointVertical = function(region, point, relative, relativePoint, x, y)
|
||||
SetPointAlongAxis(VERTICAL_MIRROR_POINTS, region, point, relative, relativePoint, x, -y);
|
||||
end;
|
||||
|
||||
local SetTexCoordVertical = function(region)
|
||||
local x1, y1, x2, y2, x3, y3, x4, y4 = region:GetTexCoord();
|
||||
region:SetTexCoord(x2, y2, x1, y1, x4, y4, x3, y3);
|
||||
end
|
||||
|
||||
function AnchorUtil.MirrorRegionsAlongVerticalAxis(mirrorDescriptions)
|
||||
MirrorRegionsAlongAxis(mirrorDescriptions, VERTICAL_MIRROR_POINTS, SetPointVertical, SetTexCoordVertical);
|
||||
end
|
||||
|
||||
local HORIZONTAL_MIRROR_POINTS =
|
||||
{
|
||||
["TOPLEFT"] = "TOPRIGHT",
|
||||
["LEFT"] = "RIGHT",
|
||||
["BOTTOMLEFT"] = "BOTTOMRIGHT",
|
||||
["TOPRIGHT"] = "TOPLEFT",
|
||||
["RIGHT"] = "LEFT",
|
||||
["BOTTOMRIGHT"] = "BOTTOMLEFT",
|
||||
["CENTER"] = "CENTER", -- Mirrored only along x and y offsets.
|
||||
["TOP"] = "TOP", -- Mirrored only along x and y offsets.
|
||||
["BOTTOM"] = "BOTTOM", -- Mirrored only along x and y offsets.
|
||||
};
|
||||
|
||||
local SetPointHorizontal = function(region, point, relative, relativePoint, x, y)
|
||||
SetPointAlongAxis(HORIZONTAL_MIRROR_POINTS, region, point, relative, relativePoint, -x, y);
|
||||
end
|
||||
|
||||
local SetTexCoordHorizontal = function(region)
|
||||
local x1, y1, x2, y2, x3, y3, x4, y4 = region:GetTexCoord();
|
||||
region:SetTexCoord(x3, y3, x4, y4, x1, y1, x2, y2);
|
||||
end
|
||||
|
||||
function AnchorUtil.MirrorRegionsAlongHorizontalAxis(mirrorDescriptions)
|
||||
MirrorRegionsAlongAxis(mirrorDescriptions, HORIZONTAL_MIRROR_POINTS, SetPointHorizontal, SetTexCoordHorizontal);
|
||||
end
|
||||
|
||||
function AnchorUtil.DebugAnchorGraph(frame, indent, visited, output)
|
||||
local indentString = " ";
|
||||
indent = indent or indentString;
|
||||
output = output or {};
|
||||
visited = visited or {};
|
||||
|
||||
if visited[frame] then
|
||||
return output;
|
||||
end
|
||||
|
||||
visited[frame] = true;
|
||||
|
||||
local function FormatFrame(frame)
|
||||
local color = frame:IsRectValid() and GREEN_FONT_COLOR or RED_FONT_COLOR;
|
||||
local x, y = frame:GetSize();
|
||||
local x2, y2 = frame:GetSize(true);
|
||||
return color:WrapTextInColorCode(frame:GetDebugName() .. (" calculated size <%.2f, %.2f> explicit size <%.2f, %.2f> points <%d>"):format(
|
||||
x, y, x2, y2, frame:GetNumPoints()));
|
||||
end
|
||||
|
||||
table.insert(output, indent .. FormatFrame(frame));
|
||||
|
||||
for i = 1, frame:GetNumPoints() do
|
||||
local point, relativeTo, relativePoint, x, y = frame:GetPoint(i);
|
||||
local anchorString = ("Anchor%d %s to %s at %s offset <%.2f, %.2f>"):format(i, point, relativeTo and relativeTo:GetDebugName() or "?", relativePoint, x, y);
|
||||
table.insert(output, indent .. LIGHTGRAY_FONT_COLOR:WrapTextInColorCode(anchorString));
|
||||
if relativeTo then
|
||||
AnchorUtil.DebugAnchorGraph(relativeTo, indent .. indentString, visited, output);
|
||||
end
|
||||
end
|
||||
|
||||
return output;
|
||||
end
|
||||
|
||||
function AnchorUtil.PrintAnchorGraph(frame)
|
||||
-- Printing to multiple places in case the chat frame isn't visible.
|
||||
local str = table.concat(AnchorUtil.DebugAnchorGraph(frame), "\n");
|
||||
ConsolePrint(str);
|
||||
print(str);
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
ATLAS_INFO_STORAGE = ATLAS_INFO_STORAGE or {}
|
||||
|
||||
local _ = ATLAS_INFO_STORAGE
|
||||
local CAPI_TEXTURE_PATH = "Interface\\AddOns\\!!!ClassicAPI\\Texture\\" -- Make this global in restructure.
|
||||
|
||||
--[[
|
||||
INFORMATION
|
||||
=================================
|
||||
|
||||
Build: 3.4.2 (50664):
|
||||
https://www.townlong-yak.com/framexml/9.2.7/Helix/AtlasInfo.lua
|
||||
https://github.com/Ketho/BlizzardInterfaceResources/blob/wrath/Resources/AtlasInfo.lua
|
||||
https://github.com/Gethe/wow-ui-textures
|
||||
|
||||
Build (Fallback): 10.1.5 (50622):
|
||||
https://github.com/Ketho/BlizzardInterfaceResources/blob/mainline/Resources/AtlasInfo.lua
|
||||
https://wow.tools
|
||||
|
||||
Manual Calc: X/WIDTH || Y/HEIGHT
|
||||
]]
|
||||
|
||||
-- _["atlasName"]={width, height, left, right, top, bottom, tilesHoriz, tilesVert, texturePath}
|
||||
|
||||
-- Interface/FrameGeneral/UIFrameDiamondMetal
|
||||
local FrameGeneral_UIFrameDiamondMetal = CAPI_TEXTURE_PATH.."FrameGeneral\\UIFrameDiamondMetal"
|
||||
_["UI-Frame-DiamondMetal-CornerBottomLeft"]={32, 32, 0.015625, 0.515625, 0.269531, 0.394531, false, false, FrameGeneral_UIFrameDiamondMetal}
|
||||
_["UI-Frame-DiamondMetal-CornerBottomRight"]={32, 32, 0.015625, 0.515625, 0.402344, 0.527344, false, false, FrameGeneral_UIFrameDiamondMetal}
|
||||
_["UI-Frame-DiamondMetal-CornerTopLeft"]={32, 32, 0.015625, 0.515625, 0.535156, 0.660156, false, false, FrameGeneral_UIFrameDiamondMetal}
|
||||
_["UI-Frame-DiamondMetal-CornerTopRight"]={32, 32, 0.015625, 0.515625, 0.667969, 0.792969, false, false, FrameGeneral_UIFrameDiamondMetal}
|
||||
_["_UI-Frame-DiamondMetal-EdgeBottom"]={32, 32, 0, 0.5, 0.00390625, 0.128906, false, false, FrameGeneral_UIFrameDiamondMetal}
|
||||
_["_UI-Frame-DiamondMetal-EdgeTop"]={32, 32, 0, 0.5, 0.136719, 0.261719, false, false, FrameGeneral_UIFrameDiamondMetal}
|
||||
|
||||
-- Interface/FrameGeneral/UIFrameDiamondMetalVertical
|
||||
local FrameGeneral_UIFrameDiamondMetalVertical = CAPI_TEXTURE_PATH.."FrameGeneral\\UIFrameDiamondMetalVertical"
|
||||
_["!UI-Frame-DiamondMetal-EdgeLeft"]={32, 32, 0.0078125, 0.257812, 0, 1, false, false, FrameGeneral_UIFrameDiamondMetalVertical}
|
||||
_["!UI-Frame-DiamondMetal-EdgeRight"]={32, 32, 0.273438, 0.523438, 0, 1, false, false, FrameGeneral_UIFrameDiamondMetalVertical}
|
||||
|
||||
-- Interface/RaidFrame/RaidFrameSummon
|
||||
local RaidFrame_RaidFrameSummon = CAPI_TEXTURE_PATH.."RaidFrame\\RaidFrameSummon"
|
||||
_["Raid-Icon-SummonAccepted"]={32, 32, 0.0078125, 0.257812, 0.015625, 0.515625, false, false, RaidFrame_RaidFrameSummon}
|
||||
_["Raid-Icon-SummonDeclined"]={32, 32, 0.273438, 0.523438, 0.015625, 0.515625, false, false, RaidFrame_RaidFrameSummon}
|
||||
_["Raid-Icon-SummonPending"]={32, 32, 0.539062, 0.789062, 0.015625, 0.515625, false, false, RaidFrame_RaidFrameSummon}
|
||||
|
||||
-- Interface/ContainerFrame/Bags
|
||||
local ContainerFrame_Bags = CAPI_TEXTURE_PATH.."ContainerFrame\\Bags"
|
||||
_["bags-newitem"]={44, 44, 0.363281, 0.535156, 0.00390625, 0.175781, false, false, ContainerFrame_Bags}
|
||||
_["bags-junkcoin"]={20, 18, 0.863281, 0.941406, 0.28125, 0.351562, false, false, ContainerFrame_Bags}
|
||||
_["bags-innerglow"]={36, 36, 0.164062, 0.304688, 0.539062, 0.679688, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-purple"]={39, 39, 0.00390625, 0.15625, 0.539062, 0.691406, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-blue"]={39, 39, 0.542969, 0.695312, 0.164062, 0.316406, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-orange"]={39, 39, 0.707031, 0.859375, 0.363281, 0.515625, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-green"]={39, 39, 0.703125, 0.855469, 0.00390625, 0.15625, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-heirloom"]={39, 39, 0.703125, 0.855469, 0.164062, 0.316406, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-white"]={39, 39, 0.00390625, 0.15625, 0.699219, 0.851562, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-flash"]={90, 90, 0.00390625, 0.355469, 0.00390625, 0.355469, false, false, ContainerFrame_Bags}
|
||||
_["bags-button-autosort-down"]={28, 26, 0.164062, 0.273438, 0.835938, 0.9375, false, false, ContainerFrame_Bags}
|
||||
_["bags-button-autosort-up"]={28, 26, 0.3125, 0.421875, 0.539062, 0.640625, false, false, ContainerFrame_Bags}
|
||||
_["bags-roundhighlight"]={36, 36, 0.164062, 0.304688, 0.6875, 0.828125, false, false, ContainerFrame_Bags}
|
||||
_["bags-icon-consumables"]={28, 28, 0.863281, 0.972656, 0.00390625, 0.113281, false, false, ContainerFrame_Bags}
|
||||
_["bags-icon-equipment"]={28, 28, 0.863281, 0.972656, 0.164062, 0.273438, false, false, ContainerFrame_Bags}
|
||||
_["bags-icon-tradegoods"]={28, 28, 0.867188, 0.976562, 0.363281, 0.472656, false, false, ContainerFrame_Bags}
|
||||
_["bags-glow-artifact"]={39, 39, 0.542969, 0.695312, 0.00390625, 0.15625, false, false, ContainerFrame_Bags}
|
||||
_["bags-greenarrow"]={20, 22, 0.3125, 0.390625, 0.648438, 0.734375, false, false, ContainerFrame_Bags}
|
||||
_["bags-icon-addslots"]={42, 42, 0.363281, 0.527344, 0.183594, 0.347656, false, false, ContainerFrame_Bags}
|
||||
_["bags-static"]={178, 43, 0.00390625, 0.699219, 0.363281, 0.53125, false, false, ContainerFrame_Bags}
|
||||
_["bags-icon-scrappable"]={36, 32, 0.00390625, 0.144531, 0.859375, 0.984375, false, false, ContainerFrame_Bags}
|
||||
@@ -0,0 +1,324 @@
|
||||
local UnitAura = UnitAura
|
||||
local UnitBuff = UnitBuff
|
||||
local UnitDebuff = UnitDebuff
|
||||
local GetSpellInfo = GetSpellInfo
|
||||
local UnitAffectingCombat = UnitAffectingCombat
|
||||
|
||||
local _, CLASS_PLAYER = UnitClass("player")
|
||||
|
||||
local AURA
|
||||
|
||||
function SpellIsPriorityAura(SpellID)
|
||||
return false
|
||||
end
|
||||
|
||||
function SpellCanApplyAura(SpellName)
|
||||
local Info = (SpellName) and AURA[SpellName]
|
||||
return (Info and Info.CanApply)
|
||||
end
|
||||
|
||||
function SpellAppliesOnlyYourself(SpellName)
|
||||
local Info = (SpellName) and AURA[SpellName]
|
||||
return (Info and Info.SelfOnly)
|
||||
end
|
||||
|
||||
function SpellIsSelfBuff(SpellID)
|
||||
if ( SpellID ) then
|
||||
local SpellName = GetSpellInfo(SpellID)
|
||||
local Info = (SpellName) and AURA[SpellName]
|
||||
|
||||
if ( Info ) then
|
||||
return Info.SelfOnly, Info.CanApply
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SpellGetVisibilityInfo(SpellID, Type)
|
||||
if ( SpellID and Type ) then
|
||||
local SpellName = GetSpellInfo(SpellID)
|
||||
local Info = (SpellName) and AURA[SpellName]
|
||||
|
||||
if ( Info ) then
|
||||
local ShowMine, ShowSpec
|
||||
if ( Type == "RAID_INCOMBAT" ) then
|
||||
ShowMine = Info.ShowMine_INCOMBAT
|
||||
ShowSpec = Info.ShowSpec_INCOMBAT
|
||||
elseif ( Type == "RAID_OUTOFCOMBAT" ) then
|
||||
ShowMine = Info.ShowMine_OUTCOMBAT
|
||||
ShowSpec = Info.ShowSpec_OUTCOMBAT
|
||||
end
|
||||
|
||||
return (ShowMine ~= nil or ShowSpec ~= nil), ShowMine, ShowSpec
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
|
||||
Aura Information
|
||||
|
||||
]]
|
||||
|
||||
local function Aura(SpellID, CanApply, SelfOnly, ShowMine_INCOMBAT, ShowMine_OUTCOMBAT, ShowSpec_INCOMBAT, ShowSpec_OUTCOMBAT)
|
||||
AURA = AURA or {}
|
||||
|
||||
AURA[GetSpellInfo(SpellID)] = {
|
||||
CanApply = CanApply, -- Player can cast it.
|
||||
SelfOnly = SelfOnly, -- Spell is not usable on others.
|
||||
|
||||
--[[
|
||||
Combat State Information: RAID_INCOMBAT, RAID_OUTOFCOMBAT
|
||||
ShowMine: whether to show the spell if cast by the player/player's pet/vehicle (e.g. the Paladin Forbearance debuff)
|
||||
ShowSpec: whether to show the spell for the current specialization of the player, it will override all others if set.
|
||||
]]
|
||||
ShowMine_INCOMBAT = ShowMine_INCOMBAT,
|
||||
ShowMine_OUTCOMBAT = ShowMine_OUTCOMBAT,
|
||||
|
||||
ShowSpec_INCOMBAT = ShowSpec_INCOMBAT,
|
||||
ShowSpec_OUTCOMBAT = ShowSpec_OUTCOMBAT,
|
||||
}
|
||||
end
|
||||
|
||||
if ( CLASS_PLAYER == "DEATHKNIGHT" ) then
|
||||
Aura(48265, true, true) -- Unholy Presence
|
||||
Aura(48263, true, true) -- Frost Presence
|
||||
Aura(48266, true, true) -- Blood Presence
|
||||
Aura(45529, true, true) -- Blood Tap
|
||||
Aura(49016, true, false) -- Hysteria
|
||||
Aura(57330, true, false) -- Horn of Winter
|
||||
Aura(49028, true, false) -- Dancing Rune Weapon
|
||||
|
||||
Aura(3714, true, false, false, true) -- Path of Frost
|
||||
elseif ( CLASS_PLAYER == "DRUID" ) then
|
||||
Aura(29166, true, false) -- Innervate
|
||||
Aura(9634, true, true) -- Dire Bear Form
|
||||
Aura(768, true, true) -- Cat Form
|
||||
Aura(48451, true, false) -- Lifebloom
|
||||
Aura(48441, true, false) -- Rejuvenation
|
||||
Aura(48443, true, false) -- Regrowth
|
||||
Aura(53251, true, false) -- Wild Growth
|
||||
Aura(61336, true, true) -- Survival Instincts
|
||||
Aura(50334, true, true) -- Berserk
|
||||
Aura(5229, true, true) -- Enrage
|
||||
Aura(22812, true, true) -- Barkskin
|
||||
Aura(53312, true, true) -- Nature's Grasp
|
||||
Aura(22842, true, true) -- Frenzied Regeneration
|
||||
Aura(2893, true, false) -- Abolish Poison
|
||||
|
||||
Aura(467, true, false, false, true) -- Thorns
|
||||
Aura(1126, true, false, false, true) -- Mark of the Wild
|
||||
Aura(21849, true, false, false, true) -- Gift of the Wild
|
||||
elseif ( CLASS_PLAYER == "HUNTER" ) then
|
||||
Aura(5384, true, true) -- Feign Death
|
||||
Aura(3045, true, true) -- Rapid Fire
|
||||
Aura(53480, true, false) -- Roar of Sacrifice
|
||||
Aura(53271, true, false) -- Master's Call
|
||||
Aura(53476, true, false) -- Intervene
|
||||
|
||||
Aura(19506, true, false, false, true) -- Trueshot Aura
|
||||
elseif ( CLASS_PLAYER == "MAGE" ) then
|
||||
Aura(43039, true, true) -- Ice Barrier
|
||||
Aura(45438, true, true) -- Ice Block
|
||||
Aura(43012, true, true) -- Frost Ward
|
||||
Aura(43008, true, true) -- Ice Armor
|
||||
Aura(7301, true, true) -- Frost Armor
|
||||
Aura(12472, true, true) -- Icy Veins
|
||||
Aura(43010, true, true) -- Fire Ward
|
||||
Aura(43046, true, true) -- Molten Armor
|
||||
Aura(43020, true, true) -- Mana Shield
|
||||
Aura(43024, true, true) -- Mage Armor
|
||||
Aura(66, true, true) -- Invisibility
|
||||
Aura(130, true, false) -- Slow Fall
|
||||
Aura(11213, true, true) -- Arcane Concentration
|
||||
Aura(12043, true, true) -- Presence of Mind
|
||||
Aura(12042, true, true) -- Arcane Power
|
||||
Aura(31579, true, true) -- Arcane Empowerment
|
||||
|
||||
Aura(61024, true, false, false, true) -- Dalaran Intellect
|
||||
Aura(61316, true, false, false, true) -- Dalaran Brilliance
|
||||
Aura(42995, true, false, false, true) -- Arcane Intellect
|
||||
Aura(43002, true, false, false, true) -- Arcane Brilliance
|
||||
Aura(43015, true, false, false, true) -- Dampen Magic
|
||||
Aura(54646, true, false, false, true) -- Focus Magic
|
||||
elseif ( CLASS_PLAYER == "PALADIN" ) then
|
||||
Aura(53601, true, false) -- Sacred Shield
|
||||
Aura(53563, true, false) -- Beacon of Light
|
||||
Aura(6940, true, false) -- Hand of Sacrifice
|
||||
Aura(64205, true, false) -- Divine Sacrifice
|
||||
Aura(31821, true, true) -- Aura Mastery
|
||||
Aura(642, true, true) -- Divine Shield
|
||||
Aura(1022, true, false) -- Hand of Protection
|
||||
Aura(1044, true, false) -- Hand of Freedom
|
||||
Aura(54428, true, true) -- Divine Plea
|
||||
Aura(48952, true, true) -- Holy Shield
|
||||
Aura(48942, true, true) -- Devotion Aura
|
||||
Aura(54043, true, true) -- Retribution Aura
|
||||
Aura(19746, true, true) -- Concentration Aura
|
||||
Aura(48943, true, true) -- Shadow Resistance Aura
|
||||
Aura(48945, true, true) -- Frost Resistance Aura
|
||||
Aura(48947, true, true) -- Fire Resistance Aura
|
||||
Aura(32223, true, true) -- Crusader Aura
|
||||
Aura(31884, true, true) -- Avenging Wrath
|
||||
Aura(54203, true, false) -- Sheath of Light
|
||||
Aura(20053, true, true) -- Vengeance
|
||||
Aura(59578, true, true) -- The Art of War
|
||||
|
||||
Aura(20217, true, false, false, true) -- Blessing of Kings
|
||||
Aura(25898, true, false, false, true) -- Greater Blessing of Kings
|
||||
Aura(48936, true, false, false, true) -- Blessing of Wisdom
|
||||
Aura(48938, true, false, false, true) -- Greater Blessing of Wisdom
|
||||
Aura(48932, true, false, false, true) -- Blessing of Might
|
||||
Aura(48934, true, false, false, true) -- Greater Blessing of Might
|
||||
Aura(25899, true, false, false, true) -- Greater Blessing of Sanctuary
|
||||
Aura(20911, true, false, false, true) -- Blessing of Sanctuary
|
||||
elseif ( CLASS_PLAYER == "PRIEST" ) then
|
||||
Aura(48111, true, false) -- Prayer of Mending
|
||||
Aura(33206, true, false) -- Pain Suppression
|
||||
Aura(48068, true, false) -- Renew
|
||||
Aura(48066, true, false) -- Power Word: Shield
|
||||
Aura(72418, true, true) -- Chilling Knowledge
|
||||
Aura(47930, false, false) -- Grace
|
||||
Aura(10060, true, false) -- Power Infusion
|
||||
Aura(586, true, true) -- Fade
|
||||
Aura(48168, true, true) -- Inner Fire
|
||||
Aura(14751, true, true) -- Inner Focus
|
||||
Aura(6346, true, false) -- Fear Ward
|
||||
Aura(64901, true, false) -- Hymn of Hope
|
||||
Aura(1706, true, false) -- Levitate
|
||||
Aura(64843, false, false) -- Divine Hymn
|
||||
Aura(59891, false, false) -- Borrowed Time
|
||||
Aura(552, true, false) -- Abolish Disease
|
||||
Aura(15473, true, true) -- Shadowform
|
||||
Aura(15286, true, true) -- Vampiric Embrace
|
||||
Aura(49694, true, true) -- Improved Spirit Tap
|
||||
Aura(47788, true, false) -- Guardian Spirit
|
||||
Aura(33151, true, true) -- Surge of Light
|
||||
Aura(33151, true, true) -- Inspiration
|
||||
Aura(7001, true, false) -- Lightwell Renew
|
||||
Aura(27827, true, true) -- Spirit of Redemption
|
||||
Aura(63734, true, true) -- Serendipity
|
||||
Aura(65081, true, false) -- Body and Soul
|
||||
Aura(63944, false, false) -- Renewed Hope
|
||||
|
||||
Aura(48073, true, false, false, true) -- Divine Spirit
|
||||
Aura(48074, true, false, false, true) -- Prayer of Spirit
|
||||
Aura(48169, true, false, false, true) -- Shadow Protection
|
||||
Aura(48170, true, false, false, true) -- Prayer of Shadow Protection
|
||||
Aura(48162, true, false, false, true) -- Prayer of Fortitude
|
||||
Aura(48161, true, false, false, true) -- Power Word: Fortitude
|
||||
elseif ( CLASS_PLAYER == "ROGUE" ) then
|
||||
Aura(1784, true, true) -- Stealth
|
||||
Aura(31665, true, true) -- Master of Subtlety
|
||||
Aura(26669, true, true) -- Evasion
|
||||
Aura(11305, true, true) -- Sprint
|
||||
Aura(26888, true, true) -- Vanish
|
||||
Aura(36554, true, true) -- Shadowstep
|
||||
Aura(48659, true, true) -- Feint
|
||||
Aura(31224, true, true) -- Clock of Shadow
|
||||
Aura(51713, true, true) -- Shadow dance
|
||||
Aura(14177, true, true) -- Cold Blood
|
||||
Aura(57934, true, false) -- Tricks of the Trade
|
||||
elseif ( CLASS_PLAYER == "SHAMAN" ) then
|
||||
Aura(49284, true, false) -- Earth Shield
|
||||
Aura(8515, false, false) -- Windfury Totem
|
||||
Aura(8178, true, false) -- Grounding Totem
|
||||
Aura(32182, true, false) -- Heroism
|
||||
Aura(2825, true, false) -- Bloodlust
|
||||
Aura(61301, true, false) -- Riptide
|
||||
Aura(51466, true, false) -- Elemental Oath
|
||||
elseif ( CLASS_PLAYER == "WARLOCK" ) then
|
||||
Aura(2947, true, false) -- Fire Shield
|
||||
Aura(132, true, false) -- Detect Invisibility
|
||||
Aura(19028, true, false) -- Soul Link
|
||||
Aura(54424, true, false) -- Fel Intelligence
|
||||
elseif ( CLASS_PLAYER == "WARRIOR" ) then
|
||||
Aura(2687, true, true) -- Bloodrage
|
||||
Aura(18499, true, true) -- Berserker Rage
|
||||
Aura(12328, true, true) -- Sweeping Strikes
|
||||
Aura(23920, true, true) -- Spell Reflection
|
||||
Aura(871, true, true) -- Shield Wall
|
||||
Aura(2565, true, true) -- Shield Block
|
||||
Aura(55694, true, true) -- Enraged Regeneration
|
||||
Aura(1719, true, true) -- Recklessness
|
||||
Aura(57522, true, true) -- Enrage
|
||||
Aura(20230, true, true) -- Retaliation
|
||||
Aura(46924, true, true) -- Bladestorm
|
||||
Aura(47440, true, false) -- Commanding Shout
|
||||
Aura(47436, true, false) -- Battle Shout
|
||||
Aura(46913, true, true) -- Bloodsurge
|
||||
Aura(12292, true, true) -- Death Wish
|
||||
Aura(16492, true, true) -- Blood Craze
|
||||
Aura(65156, true, true) -- Juggernaut
|
||||
Aura(3411, true, false) -- Intervene
|
||||
end
|
||||
|
||||
--[[
|
||||
|
||||
Global Auras
|
||||
|
||||
]]
|
||||
|
||||
Aura(69127, nil, nil, nil, nil, false, true) -- Chill of the Throne
|
||||
Aura(26013, nil, nil, nil, nil, false, true) -- Deserter
|
||||
Aura(31694, nil, nil, nil, nil, false, false) -- Strange Feeling
|
||||
Aura(70013, nil, nil, nil, nil, false, false) -- Quel'Delar's Compulsion
|
||||
|
||||
-- TODO: Add PvP flags as a priority buff.
|
||||
|
||||
--[[
|
||||
|
||||
UNITAURA / UNITBUFF / UNITDEBUFF
|
||||
Modern payload.
|
||||
|
||||
]]
|
||||
|
||||
function C_UnitAura(...)
|
||||
local Name, Rank, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID = UnitAura(...)
|
||||
local Aura = AURA[Name]
|
||||
return Name, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID, (Aura and Aura.CanApply)
|
||||
end
|
||||
|
||||
function C_UnitBuff(...)
|
||||
local Name, Rank, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID = UnitBuff(...)
|
||||
local Aura = AURA[Name]
|
||||
return Name, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID, (Aura and Aura.CanApply)
|
||||
end
|
||||
|
||||
function C_UnitDebuff(...)
|
||||
local Name, Rank, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID = UnitDebuff(...)
|
||||
return Name, Icon, Count, Type, Duration, Expire, Caster, Steal, Consolidate, ID
|
||||
end
|
||||
|
||||
--[[
|
||||
|
||||
CompactUnitFrame_UtilShouldDisplayDebuff
|
||||
Note: Support if CompactRaidFrames not installed.
|
||||
|
||||
]]
|
||||
local SpellGetVisibilityInfo = SpellGetVisibilityInfo
|
||||
|
||||
function CompactUnitFrame_UtilShouldDisplayDebuff(...)
|
||||
local _, _, _, _, _, _, _, Caster, _, _, SpellID = UnitDebuff(...)
|
||||
|
||||
if ( SpellID ) then
|
||||
local HasCustom, AlwaysShowMine, ShowForMySpec = SpellGetVisibilityInfo(SpellID, UnitAffectingCombat("player") and "RAID_INCOMBAT" or "RAID_OUTOFCOMBAT")
|
||||
if ( HasCustom ) then
|
||||
return ShowForMySpec or (AlwaysShowMine and (Caster == "player" or Caster == "pet" or Caster == "vehicle"))
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
--[[ TODO
|
||||
function UnitAuraBySlot(unit, slot)
|
||||
if ( unit and LibAura ) then
|
||||
return LibAura:UnitAuraBySlot(unit, slot)
|
||||
end
|
||||
end
|
||||
|
||||
function UnitAuraSlots(unit, filter, maxCount, continuationToken)
|
||||
if ( unit and LibAura ) then
|
||||
return LibAura:UnitAuraSlots(unit, filter, maxCount, continuationToken)
|
||||
end
|
||||
end]]
|
||||
@@ -0,0 +1,67 @@
|
||||
AuraUtil = AuraUtil or {}
|
||||
|
||||
local UnitAura = UnitAura
|
||||
|
||||
local function FindAuraRecurse(predicate, unit, filter, auraIndex, predicateArg1, predicateArg2, predicateArg3, ...)
|
||||
if ... == nil then
|
||||
return nil; -- Not found
|
||||
end
|
||||
if predicate(predicateArg1, predicateArg2, predicateArg3, ...) then
|
||||
return ...;
|
||||
end
|
||||
auraIndex = auraIndex + 1;
|
||||
local name, rank, icon, count, type, duration, expire, caster, steal, consolidate, id = UnitAura(unit, auraIndex, filter);
|
||||
return FindAuraRecurse(predicate, unit, filter, auraIndex, predicateArg1, predicateArg2, predicateArg3, name, icon, count, type, duration, expire, caster, steal, consolidate, id);
|
||||
end
|
||||
-- Find an aura by any predicate, you can pass in up to 3 predicate specific parameters
|
||||
-- The predicate will also receive all aura params, if the aura data matches return true
|
||||
function AuraUtil.FindAura(predicate, unit, filter, predicateArg1, predicateArg2, predicateArg3)
|
||||
local auraIndex = 1;
|
||||
local name, rank, icon, count, type, duration, expire, caster, steal, consolidate, id = UnitAura(unit, auraIndex, filter);
|
||||
return FindAuraRecurse(predicate, unit, filter, auraIndex, predicateArg1, predicateArg2, predicateArg3, name, icon, count, type, duration, expire, caster, steal, consolidate, id);
|
||||
end
|
||||
|
||||
do
|
||||
|
||||
local function NamePredicate(auraNameToFind, _, _, auraName)
|
||||
return auraNameToFind == auraName;
|
||||
end
|
||||
-- Finds the first aura that matches the name
|
||||
-- Notes:
|
||||
-- aura names are not unique!
|
||||
-- aura names are localized, what works in one locale might not work in another
|
||||
-- consider that in English two auras might have different names, but once localized they have the same name, so even using the localized aura name in a search it could result in different behavior
|
||||
-- the unit could have multiple auras with the same name, this will only find the first
|
||||
function AuraUtil.FindAuraByName(auraName, unit, filter)
|
||||
return AuraUtil.FindAura(NamePredicate, unit, filter, auraName);
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
do
|
||||
|
||||
local function ForEachAuraHelper(unit, filter, func, continuationToken, ...)
|
||||
-- continuationToken is the first return value of UnitAuraSlots()
|
||||
local n = select('#', ...);
|
||||
for i=1, n do
|
||||
local slot = select(i, ...);
|
||||
if func(UnitAuraBySlot(unit, slot)) then
|
||||
-- if func returns true then no further slots are needed, so don't return continuationToken
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
return continuationToken;
|
||||
end
|
||||
|
||||
function AuraUtil.ForEachAura(unit, filter, maxCount, func)
|
||||
if maxCount and maxCount <= 0 then
|
||||
return;
|
||||
end
|
||||
local continuationToken;
|
||||
repeat
|
||||
-- continuationToken is the first return value of UnitAuraSltos
|
||||
continuationToken = ForEachAuraHelper(unit, filter, func, UnitAuraSlots(unit, filter, maxCount, continuationToken));
|
||||
until continuationToken == nil;
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
local _G = _G
|
||||
local C_ChatInfo = C_ChatInfo or {}
|
||||
|
||||
local SendAddonMessage = _G.SendAddonMessage
|
||||
|
||||
local CTL = _G.ChatThrottleLib
|
||||
local CTL_SendAddonMessage = CTL.SendAddonMessage
|
||||
CTL.version = 50 -- Force ClassicAPI CTL.
|
||||
CTL.SendAddonMessage = function(...)
|
||||
local _, _, _, _, Type = ...
|
||||
if ( Type ~= "PARTY" and Type ~= "RAID" and Type ~= "GUILD" and Type ~= "BATTLEGROUND" and Type ~= "WHISPER" ) then
|
||||
return
|
||||
end
|
||||
return CTL_SendAddonMessage(...)
|
||||
end
|
||||
|
||||
local function TODO() end
|
||||
|
||||
C_ChatInfo.RegisterAddonMessagePrefix = TODO
|
||||
C_ChatInfo.CanPlayerSpeakLanguage = TODO
|
||||
C_ChatInfo.CanReportPlayer = TODO
|
||||
C_ChatInfo.GetChannelInfoFromIdentifier = TODO
|
||||
C_ChatInfo.GetChannelRosterInfo = TODO
|
||||
C_ChatInfo.GetChannelRuleset = TODO
|
||||
C_ChatInfo.GetChannelShortcut = TODO
|
||||
C_ChatInfo.GetChatLineSenderGUID = TODO
|
||||
C_ChatInfo.GetChatLineSenderName = TODO
|
||||
C_ChatInfo.GetChatLineText = TODO
|
||||
C_ChatInfo.GetChatTypeName = TODO
|
||||
C_ChatInfo.GetClubStreamIDs = TODO
|
||||
C_ChatInfo.GetColorForChatType = TODO
|
||||
C_ChatInfo.GetGeneralChannelID = TODO
|
||||
C_ChatInfo.GetGeneralChannelLocalID = TODO
|
||||
C_ChatInfo.GetMentorChannelID = TODO
|
||||
C_ChatInfo.GetNumActiveChannels = TODO
|
||||
C_ChatInfo.GetNumReservedChatWindows = TODO
|
||||
C_ChatInfo.GetRegisteredAddonMessagePrefixes = TODO
|
||||
|
||||
function C_ChatInfo.IsAddonMessagePrefixRegistered()
|
||||
return true
|
||||
end
|
||||
|
||||
C_ChatInfo.IsChannelRegional = TODO
|
||||
C_ChatInfo.IsChatLineCensored = TODO
|
||||
C_ChatInfo.IsPartyChannelType = TODO
|
||||
C_ChatInfo.IsRegionalServiceAvailable = TODO
|
||||
C_ChatInfo.IsValidChatLine = TODO
|
||||
C_ChatInfo.RegisterAddonMessagePrefix = TODO
|
||||
C_ChatInfo.ReplaceIconAndGroupExpressions = TODO
|
||||
C_ChatInfo.ReportServerLag = TODO
|
||||
C_ChatInfo.RequestCanLocalWhisperTarget = TODO
|
||||
C_ChatInfo.ResetDefaultZoneChannels = TODO
|
||||
|
||||
function C_ChatInfo.SendAddonMessage(...)
|
||||
local _, _, Type = ...
|
||||
if ( Type ~= "PARTY" and Type ~= "RAID" and Type ~= "GUILD" and Type ~= "BATTLEGROUND" and Type ~= "WHISPER" ) then
|
||||
return
|
||||
end
|
||||
return SendAddonMessage(...)
|
||||
end
|
||||
|
||||
C_ChatInfo.SwapChatChannelsByChannelIndex = TODO
|
||||
C_ChatInfo.UncensorChatLine = TODO
|
||||
|
||||
-- Global
|
||||
_G.C_ChatInfo = C_ChatInfo
|
||||
@@ -0,0 +1,108 @@
|
||||
C_CreatureInfo = C_CreatureInfo or {}
|
||||
|
||||
local C_CreatureInfo_RaceData = {
|
||||
[1] = {
|
||||
raceName = "Human",
|
||||
clientFileString = "Human",
|
||||
},
|
||||
[2] = {
|
||||
raceName = "Orc",
|
||||
clientFileString = "Orc",
|
||||
},
|
||||
[3] = {
|
||||
raceName = "Dwarf",
|
||||
clientFileString = "Dwarf",
|
||||
},
|
||||
[4] = {
|
||||
raceName = "Night Elf",
|
||||
clientFileString = "NightElf",
|
||||
},
|
||||
[5] = {
|
||||
raceName = "Undead",
|
||||
clientFileString = "Scourge",
|
||||
},
|
||||
[6] = {
|
||||
raceName = "Tauren",
|
||||
clientFileString = "Tauren",
|
||||
},
|
||||
[7] = {
|
||||
raceName = "Gnome",
|
||||
clientFileString = "Gnome",
|
||||
},
|
||||
[8] = {
|
||||
raceName = "Troll",
|
||||
clientFileString = "Troll",
|
||||
},
|
||||
[9] = {
|
||||
raceName = "Goblin",
|
||||
clientFileString = "Goblin",
|
||||
},
|
||||
[10] = {
|
||||
raceName = "Blood Elf",
|
||||
clientFileString = "BloodElf",
|
||||
},
|
||||
[11] = {
|
||||
raceName = "Draenei",
|
||||
clientFileString = "Draenei",
|
||||
},
|
||||
}
|
||||
|
||||
local C_CreatureInfo_ClassData = {
|
||||
[1] = {
|
||||
className = "Warrior",
|
||||
classFile = "WARRIOR",
|
||||
},
|
||||
[2] = {
|
||||
className = "Paladin",
|
||||
classFile = "PALADIN",
|
||||
},
|
||||
[3] = {
|
||||
className = "Hunter",
|
||||
classFile = "HUNTER",
|
||||
},
|
||||
[4] = {
|
||||
className = "Rogue",
|
||||
classFile = "ROGUE",
|
||||
},
|
||||
[5] = {
|
||||
className = "Priest",
|
||||
classFile = "PRIEST",
|
||||
},
|
||||
[6] = {
|
||||
className = "Death Knight",
|
||||
classFile = "DEATHKNIGHT",
|
||||
},
|
||||
[7] = {
|
||||
className = "Shaman",
|
||||
classFile = "SHAMAN",
|
||||
},
|
||||
[8] = {
|
||||
className = "Mage",
|
||||
classFile = "MAGE",
|
||||
},
|
||||
[9] = {
|
||||
className = "Warlock",
|
||||
classFile = "WARLOCK",
|
||||
},
|
||||
--[10] = { MONK }
|
||||
[11] = {
|
||||
className = "Druid",
|
||||
classFile = "DRUID",
|
||||
},
|
||||
}
|
||||
|
||||
function C_CreatureInfo.GetClassInfo(classID)
|
||||
local classInfo = C_CreatureInfo_ClassData[classID]
|
||||
|
||||
if ( classInfo ) then
|
||||
if ( not classInfo[classID] ) then
|
||||
classInfo.classID = classID
|
||||
end
|
||||
end
|
||||
|
||||
return classInfo
|
||||
end
|
||||
|
||||
function C_CreatureInfo.GetRaceInfo(raceID)
|
||||
return C_CreatureInfo_RaceData[raceID]
|
||||
end
|
||||
@@ -0,0 +1,12 @@
|
||||
local GetNumFriends = GetNumFriends
|
||||
local GetFriendInfo = GetFriendInfo
|
||||
|
||||
C_FriendList = C_FriendList or {}
|
||||
|
||||
C_FriendList.GetNumFriends = function()
|
||||
return GetNumFriends()
|
||||
end
|
||||
|
||||
C_FriendList.GetFriendInfoByIndex = function(index)
|
||||
return GetFriendInfo(index)
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
C_GossipInfo = C_GossipInfo or {}
|
||||
|
||||
function C_GossipInfo.GetText()
|
||||
return GetGossipText()
|
||||
end
|
||||
|
||||
function C_GossipInfo.GetNumActiveQuests()
|
||||
return GetNumGossipActiveQuests()
|
||||
end
|
||||
|
||||
function C_GossipInfo.GetNumAvailableQuests()
|
||||
return GetNumGossipAvailableQuests()
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
C_IncomingSummon = C_IncomingSummon or {}
|
||||
|
||||
function C_IncomingSummon.HasIncomingSummon(unit)
|
||||
return false
|
||||
end
|
||||
|
||||
function C_IncomingSummon.IncomingSummonStatus(unit)
|
||||
return 0
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
local GetItemInfo = GetItemInfo
|
||||
|
||||
C_Item = C_Item or {}
|
||||
|
||||
function C_Item.GetItemInfo(itemInfo)
|
||||
return GetItemInfo(itemInfo)
|
||||
end
|
||||
|
||||
function C_Item.IsItemDataCachedByID(itemID)
|
||||
local itemName, itemLink = GetItemInfo(itemID)
|
||||
return itemLink ~= nil
|
||||
end
|
||||
|
||||
function C_Item.DoesItemExistByID(itemID)
|
||||
return true
|
||||
end
|
||||
|
||||
function C_Item.DoesItemExist(itemInfo)
|
||||
-- This can accept location, id, link or name...
|
||||
return self.IsItemDataCachedByID(itemInfo)
|
||||
end
|
||||
|
||||
function C_Item.GetItemName(itemInfo)
|
||||
return GetItemInfo(itemInfo)
|
||||
end
|
||||
|
||||
function C_Item.GetItemNameByID(itemID)
|
||||
return GetItemInfo(itemID)
|
||||
end
|
||||
|
||||
function C_Item.RequestLoadItemDataByID(itemID)
|
||||
local item = Item:CreateFromItemID(itemID)
|
||||
if item then
|
||||
item:ContinueOnItemLoad(self.DoesItemExistByID)
|
||||
end
|
||||
end
|
||||
|
||||
local TODO = function()end
|
||||
|
||||
C_Item.GetItemGUID = TODO
|
||||
C_Item.IsLocked = TODO
|
||||
C_Item.GetItemID = TODO -- arg: Pass the items location in bags/character etc, offical relies on itemLocationMixin
|
||||
C_Item.LockItem = TODO
|
||||
C_Item.UnlockItem = TODO
|
||||
C_Item.GetItemIconByID = TODO
|
||||
C_Item.GetItemIcon = TODO
|
||||
C_Item.GetItemLink = TODO
|
||||
C_Item.GetItemQualityByID = TODO
|
||||
C_Item.GetItemQuality = TODO
|
||||
C_Item.GetCurrentItemLevel = TODO
|
||||
C_Item.GetItemInventoryTypeByID = TODO
|
||||
C_Item.GetItemInventoryType = TODO
|
||||
C_Item.GetItemGUID = TODO
|
||||
C_Item.IsItemDataCached = TODO
|
||||
C_Item.IsBound = TODO
|
||||
|
||||
|
||||
function GetItemInfoInstant(itemInfo)
|
||||
-- return itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subclassID
|
||||
-- TODO: if it is a link extract the id from the link text and send it back...
|
||||
-- we can't return any of the other information like classic because its not ready
|
||||
-- we have to read the tooltips
|
||||
return itemInfo
|
||||
end
|
||||
|
||||
GetItemSubClassInfo = TODO
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user