6 Commits

1335 changed files with 110584 additions and 175 deletions
+1
View File
@@ -5,6 +5,7 @@ manifest.json
.vscode
dist
build/
/new customization mpqs/*.mpq
Wow*.exe
*.backup.exe
Logs/
+2 -1
View File
@@ -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
+9 -8
View File
@@ -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.
+82
View File
@@ -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.
+105
View File
@@ -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.
+38
View File
@@ -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
View File
@@ -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"
}
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
+26
View File
@@ -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"
]
}
+36
View File
@@ -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()
+241
View File
@@ -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
}
+30 -1
View File
@@ -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
@@ -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.
@@ -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>
@@ -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
]]
@@ -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
]]
@@ -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
@@ -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>
@@ -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>
@@ -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>
@@ -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
@@ -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>
@@ -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)
@@ -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
@@ -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
@@ -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