From 92e171266ea01b7eacbf678cf64061b31a2f23d5 Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Fri, 30 Oct 2026 16:27:05 +0000 Subject: [PATCH] Apply Patch --- Bindings.xml | 74 + CHANGELOG-lowgc.md | 107 + MODERN-BLIZZARD.txt | 47 + PTUnitFrame.lua | 1932 +++++++++++++++++ PTUnitFrameGroup.lua | 803 +++++++ Profile.lua | 91 + ProfileManager.lua | 780 +++++++ Puppeteer.lua | 867 ++++++++ Puppeteer.toc | 98 + PuppeteerSettings.lua | 626 ++++++ assets/textures/arrow.tga | Bin 0 -> 220404 bytes .../textures/bars/Blizzard-Raid-Sideless.tga | Bin 0 -> 667 bytes assets/textures/bars/Blizzard-Raid.tga | Bin 0 -> 935 bytes assets/textures/bars/Blizzard-Smooth.tga | Bin 0 -> 571 bytes assets/textures/bars/BlizzardRetailBar.tga | Bin 0 -> 12134 bytes assets/textures/bars/MODERN-POWER-NOTICES.md | 163 ++ assets/textures/bars/ModernPowerEnergy.tga | Bin 0 -> 8236 bytes .../textures/bars/ModernPowerEnergyClass.tga | Bin 0 -> 8236 bytes assets/textures/bars/ModernPowerFocus.tga | Bin 0 -> 8236 bytes assets/textures/bars/ModernPowerManaClass.tga | Bin 0 -> 8236 bytes assets/textures/bars/ModernPowerRage.tga | Bin 0 -> 8236 bytes assets/textures/bars/Puppeteer-Borderless.tga | Bin 0 -> 3223 bytes .../bars/Puppeteer-Flat-Borderless.tga | Bin 0 -> 556 bytes assets/textures/bars/Puppeteer-Flat.tga | Bin 0 -> 556 bytes .../bars/Puppeteer-Shineless-Borderless.tga | Bin 0 -> 795 bytes assets/textures/bars/Puppeteer-Shineless.tga | Bin 0 -> 795 bytes assets/textures/bars/Puppeteer.tga | Bin 0 -> 3223 bytes assets/textures/roles/Damage.tga | Bin 0 -> 11976 bytes assets/textures/roles/Healer.tga | Bin 0 -> 11614 bytes assets/textures/roles/Tank.tga | Bin 0 -> 12724 bytes core/ActionBindings.lua | 162 ++ core/Bindings.lua | 731 +++++++ core/Command.lua | 127 ++ core/EnemyTracker.lua | 111 + core/EventHandler.lua | 164 ++ core/OverrideBindings.lua | 211 ++ core/Roles.lua | 402 ++++ core/SpellsTooltip.lua | 364 ++++ core/UnitTracker.lua | 143 ++ gui/GuiUtil.lua | 20 + gui/Settings.lua | 1358 ++++++++++++ gui/component/BindingOptions.lua | 255 +++ gui/component/BindingScriptEditor.lua | 161 ++ gui/component/ButtonEditor.lua | 415 ++++ gui/component/InputBlock.lua | 22 + gui/component/LoadScriptEditor.lua | 59 + gui/component/MultiEditor.lua | 155 ++ gui/component/NewLoadout.lua | 101 + gui/component/SpellBindInterface.lua | 162 ++ gui/component/SpellLine.lua | 424 ++++ gui/component/uf/Arrow.lua | 51 + gui/component/uf/CastIcon.lua | 242 +++ gui/component/uf/HighlightBorder.lua | 128 ++ libs/AuraTracker.lua | 263 +++ libs/GuidRoster.lua | 100 + libs/HealPredict.lua | 852 ++++++++ libs/PTUnit.lua | 553 +++++ libs/Pool.lua | 134 ++ libs/ShaguUtil.lua | 377 ++++ libs/UnitProxy.lua | 471 ++++ libs/Util.lua | 1661 ++++++++++++++ libs/ace/AceAddon-2.0/AceAddon-2.0.lua | 1001 +++++++++ libs/ace/AceConsole-2.0/AceConsole-2.0.lua | 1898 ++++++++++++++++ libs/ace/AceDebug-2.0/AceDebug-2.0.lua | 242 +++ libs/ace/AceEvent-2.0/AceEvent-2.0.lua | 970 +++++++++ libs/ace/AceHook-2.1/AceHook-2.1.lua | 569 +++++ libs/ace/AceLibrary/AceLibrary.lua | 752 +++++++ libs/ace/AceLocale-2.2/AceLocale-2.2.lua | 538 +++++ libs/ace/AceOO-2.0/AceOO-2.0.lua | 1004 +++++++++ libs/ace/Banzai-1.0/Banzai-1.0.lua | 168 ++ libs/ace/Compost-2.0/Compost-2.0.lua | 277 +++ libs/ace/Deformat-2.0/Deformat-2.0.lua | 343 +++ libs/ace/Gratuity-2.0/Gratuity-2.0.lua | 220 ++ libs/ace/HealComm-1.0/HealComm-1.0.lua | 1849 ++++++++++++++++ .../ace/ItemBonusLib-1.0/ItemBonusLib-1.0.lua | 482 ++++ libs/ace/ItemBonusLib-1.0/Locales/enUS.lua | 226 ++ libs/ace/RosterLib-2.0/RosterLib-2.0.lua | 393 ++++ libs/gui/Component.lua | 419 ++++ libs/gui/PTGuiLib.lua | 51 + libs/gui/abstract/TabComponent.lua | 133 ++ libs/gui/component/Button.lua | 175 ++ libs/gui/component/Checkbox.lua | 43 + libs/gui/component/ColorSelect.lua | 89 + libs/gui/component/Container.lua | 10 + libs/gui/component/Dropdown.lua | 342 +++ libs/gui/component/EditBox.lua | 29 + libs/gui/component/EditBoxSlider.lua | 81 + libs/gui/component/MultiLineEditBox.lua | 72 + libs/gui/component/ScrollFrame.lua | 138 ++ libs/gui/component/Slider.lua | 56 + libs/gui/component/TabPanel.lua | 12 + libs/gui/component/Text.lua | 65 + libs/gui/component/Title.lua | 20 + libs/gui/frame/SimpleDialog.lua | 100 + libs/gui/frame/TabFrame.lua | 33 + locale/Locale.lua | 102 + locale/deDE.lua | 183 ++ locale/esES.lua | 183 ++ locale/ptBR.lua | 183 ++ locale/ruRU.lua | 183 ++ locale/zhCN.lua | 183 ++ 101 files changed, 29784 insertions(+) create mode 100644 Bindings.xml create mode 100644 CHANGELOG-lowgc.md create mode 100644 MODERN-BLIZZARD.txt create mode 100644 PTUnitFrame.lua create mode 100644 PTUnitFrameGroup.lua create mode 100644 Profile.lua create mode 100644 ProfileManager.lua create mode 100644 Puppeteer.lua create mode 100644 Puppeteer.toc create mode 100644 PuppeteerSettings.lua create mode 100644 assets/textures/arrow.tga create mode 100644 assets/textures/bars/Blizzard-Raid-Sideless.tga create mode 100644 assets/textures/bars/Blizzard-Raid.tga create mode 100644 assets/textures/bars/Blizzard-Smooth.tga create mode 100644 assets/textures/bars/BlizzardRetailBar.tga create mode 100644 assets/textures/bars/MODERN-POWER-NOTICES.md create mode 100644 assets/textures/bars/ModernPowerEnergy.tga create mode 100644 assets/textures/bars/ModernPowerEnergyClass.tga create mode 100644 assets/textures/bars/ModernPowerFocus.tga create mode 100644 assets/textures/bars/ModernPowerManaClass.tga create mode 100644 assets/textures/bars/ModernPowerRage.tga create mode 100644 assets/textures/bars/Puppeteer-Borderless.tga create mode 100644 assets/textures/bars/Puppeteer-Flat-Borderless.tga create mode 100644 assets/textures/bars/Puppeteer-Flat.tga create mode 100644 assets/textures/bars/Puppeteer-Shineless-Borderless.tga create mode 100644 assets/textures/bars/Puppeteer-Shineless.tga create mode 100644 assets/textures/bars/Puppeteer.tga create mode 100644 assets/textures/roles/Damage.tga create mode 100644 assets/textures/roles/Healer.tga create mode 100644 assets/textures/roles/Tank.tga create mode 100644 core/ActionBindings.lua create mode 100644 core/Bindings.lua create mode 100644 core/Command.lua create mode 100644 core/EnemyTracker.lua create mode 100644 core/EventHandler.lua create mode 100644 core/OverrideBindings.lua create mode 100644 core/Roles.lua create mode 100644 core/SpellsTooltip.lua create mode 100644 core/UnitTracker.lua create mode 100644 gui/GuiUtil.lua create mode 100644 gui/Settings.lua create mode 100644 gui/component/BindingOptions.lua create mode 100644 gui/component/BindingScriptEditor.lua create mode 100644 gui/component/ButtonEditor.lua create mode 100644 gui/component/InputBlock.lua create mode 100644 gui/component/LoadScriptEditor.lua create mode 100644 gui/component/MultiEditor.lua create mode 100644 gui/component/NewLoadout.lua create mode 100644 gui/component/SpellBindInterface.lua create mode 100644 gui/component/SpellLine.lua create mode 100644 gui/component/uf/Arrow.lua create mode 100644 gui/component/uf/CastIcon.lua create mode 100644 gui/component/uf/HighlightBorder.lua create mode 100644 libs/AuraTracker.lua create mode 100644 libs/GuidRoster.lua create mode 100644 libs/HealPredict.lua create mode 100644 libs/PTUnit.lua create mode 100644 libs/Pool.lua create mode 100644 libs/ShaguUtil.lua create mode 100644 libs/UnitProxy.lua create mode 100644 libs/Util.lua create mode 100644 libs/ace/AceAddon-2.0/AceAddon-2.0.lua create mode 100644 libs/ace/AceConsole-2.0/AceConsole-2.0.lua create mode 100644 libs/ace/AceDebug-2.0/AceDebug-2.0.lua create mode 100644 libs/ace/AceEvent-2.0/AceEvent-2.0.lua create mode 100644 libs/ace/AceHook-2.1/AceHook-2.1.lua create mode 100644 libs/ace/AceLibrary/AceLibrary.lua create mode 100644 libs/ace/AceLocale-2.2/AceLocale-2.2.lua create mode 100644 libs/ace/AceOO-2.0/AceOO-2.0.lua create mode 100644 libs/ace/Banzai-1.0/Banzai-1.0.lua create mode 100644 libs/ace/Compost-2.0/Compost-2.0.lua create mode 100644 libs/ace/Deformat-2.0/Deformat-2.0.lua create mode 100644 libs/ace/Gratuity-2.0/Gratuity-2.0.lua create mode 100644 libs/ace/HealComm-1.0/HealComm-1.0.lua create mode 100644 libs/ace/ItemBonusLib-1.0/ItemBonusLib-1.0.lua create mode 100644 libs/ace/ItemBonusLib-1.0/Locales/enUS.lua create mode 100644 libs/ace/RosterLib-2.0/RosterLib-2.0.lua create mode 100644 libs/gui/Component.lua create mode 100644 libs/gui/PTGuiLib.lua create mode 100644 libs/gui/abstract/TabComponent.lua create mode 100644 libs/gui/component/Button.lua create mode 100644 libs/gui/component/Checkbox.lua create mode 100644 libs/gui/component/ColorSelect.lua create mode 100644 libs/gui/component/Container.lua create mode 100644 libs/gui/component/Dropdown.lua create mode 100644 libs/gui/component/EditBox.lua create mode 100644 libs/gui/component/EditBoxSlider.lua create mode 100644 libs/gui/component/MultiLineEditBox.lua create mode 100644 libs/gui/component/ScrollFrame.lua create mode 100644 libs/gui/component/Slider.lua create mode 100644 libs/gui/component/TabPanel.lua create mode 100644 libs/gui/component/Text.lua create mode 100644 libs/gui/component/Title.lua create mode 100644 libs/gui/frame/SimpleDialog.lua create mode 100644 libs/gui/frame/TabFrame.lua create mode 100644 locale/Locale.lua create mode 100644 locale/deDE.lua create mode 100644 locale/esES.lua create mode 100644 locale/ptBR.lua create mode 100644 locale/ruRU.lua create mode 100644 locale/zhCN.lua diff --git a/Bindings.xml b/Bindings.xml new file mode 100644 index 0000000..2f53759 --- /dev/null +++ b/Bindings.xml @@ -0,0 +1,74 @@ + + + Puppeteer.HandleKeyPress(1) + + + Puppeteer.HandleKeyPress(2) + + + Puppeteer.HandleKeyPress(3) + + + Puppeteer.HandleKeyPress(4) + + + Puppeteer.HandleKeyPress(5) + + + Puppeteer.HandleKeyPress(6) + + + Puppeteer.HandleKeyPress(7) + + + Puppeteer.HandleKeyPress(8) + + + Puppeteer.HandleKeyPress(9) + + + Puppeteer.HandleKeyPress(10) + + + Puppeteer.HandleKeyPress(11) + + + Puppeteer.HandleKeyPress(12) + + + Puppeteer.HandleKeyPress(13) + + + Puppeteer.HandleKeyPress(14) + + + Puppeteer.HandleKeyPress(15) + + + Puppeteer.HandleKeyPress(16) + + + Puppeteer.HandleKeyPress(17) + + + Puppeteer.HandleKeyPress(18) + + + Puppeteer.HandleKeyPress(19) + + + Puppeteer.HandleKeyPress(20) + + + Puppeteer.HandleKeyPress(21) + + + Puppeteer.HandleKeyPress(22) + + + Puppeteer.HandleKeyPress(23) + + + Puppeteer.HandleKeyPress(24) + + \ No newline at end of file diff --git a/CHANGELOG-lowgc.md b/CHANGELOG-lowgc.md new file mode 100644 index 0000000..20deb17 --- /dev/null +++ b/CHANGELOG-lowgc.md @@ -0,0 +1,107 @@ +# Puppeteer 1.1.3-lowgc + +A low-garbage build of [OldManAlpha/Puppeteer](https://github.com/OldManAlpha/Puppeteer), based on its latest `dev` +branch. That branch includes the Nampower cast-fail fix for heal predictions and the "Vanilla Shaman Color" option. + +## Additions from skrymsli/Puppeteer + +Each [skrymsli](https://github.com/skrymsli/Puppeteer) commit was only kept if it doesn't cost combat performance: + +- **Variable test UI frame sizes (`/pt testui [count]`)**: kept. It only does work while test mode is on. Its + per-call pattern match was replaced with a value cached when each frame is created. +- **Healer mana display and report**: kept, but changed. As shipped, it rescanned all 40 raid frames and redrew the + raid header on every mana, rage or energy event in the raid. That made each event about 20x more expensive in CPU + (111 µs vs 5.6 µs) and a raid-wide mana tick about twice as slow. It now updates at most twice per second. That + brings the cost back to the OldManAlpha level (5.3 µs per event) and adds about 2 bytes of garbage per raid-wide + tick. + +## Why + +WoW 1.12 runs Lua 5.0, whose collector stops the game and walks the whole heap whenever enough garbage piles up. +That is the combat hitch. The only fix an addon can make is to allocate less, so every change below removes +allocations from code that runs during fights. Nothing about how the frames look or behave was changed. + +## Garbage per event (bytes), 40-man raid + +| Event | OldManAlpha | This build | +|---|---:|---:| +| Full group check (RAID_ROSTER_UPDATE) | 17,657 | 237 | +| Target change | 4,789 | 1.5 | +| HealComm message from another healer | 436 | 4.3 | +| Heal cast (SuperWoW START + CAST) | 409 | 1.8 | +| HoT cast + Nampower aura event | 419 | 0.4 | +| Heal cast + its combat log line | 134 | 5.6 | +| UNIT_HEALTH | 50 | 21 | + +Simulated Naxxramas-style fight: 21.1 KB/s → 8.6 KB/s of garbage. The SuperWoW + Nampower + UnitXP client and the +plain 1.12 client gave the same results. + +## Changes + +- **Private table pool (`libs/Pool.lua`).** Puppeteer used Compost-2.0, which strongly keeps only 10 tables and puts + the rest in a weak table that every GC empties. Its erase also shrinks tables, so every reuse has to grow them + again. Puppeteer's own code now uses a pool that survives GC and keeps table capacity. The shared Ace libraries + still use their own Compost. +- **Aura scanning (`PTUnit`).** Each unit reuses one entry table per aura slot and its name/type arrays, instead of + rebuilding everything on every UNIT_AURA. +- **Target changes and group checks.** + - The heal-prediction GUID set is now refilled in place; it used to be a new array plus a new set every time. + - Frame sorting and layout use reusable scratch tables and static comparators. + - Role ranks are computed once per frame instead of inside the comparator, which also removes an O(n²) + lookup during the sort. +- **Heal prediction.** + - Fixed tables that were orphaned on every cast: last cast, overwritten HoTs, overwritten resurrections. + - Debug messages are only built when debug output is on. They were built for every heal even with debug off. +- **Aura tracking.** + - Nampower aura events reuse one table instead of creating `{target}` each time. + - AoE buff target lists are now released after use. +- **Unit frame text.** + - Health, missing health, power, incoming heal and range text are each built with one `string.format`, so no + intermediate strings are created. + - Color code prefixes are cached. + - Range text is only rebuilt when the shown distance or its color changes. + - Aura minute strings ("5m") are cached. +- **HealComm-1.0 (bundled).** The message split table and heal entries are reused. HealComm is shared between addons, + so this only applies when Puppeteer's copy is the one loaded. + +## Verification + +- All files compile under a real Lua 5.0 VM. +- OldManAlpha's build, skrymsli's build and this build were run through the same deterministic simulated fight. Every frame's texts, bar values, + colors, alpha, positions, aura icons, heal predictions and aura caches were compared at checkpoints across 10 + settings and client combinations. Against OldManAlpha's build, every unit frame matched exactly; the only + differences were in the raid header, where skrymsli's mana labels live. Against skrymsli's build, the only + difference was: a stale aura icon reference now holds that slot's current + aura instead of an erased table, which only affects what a hover tooltip would show. + +## About ClassicAPI + +ClassicAPI backports the modern API and Lua 5.1 syntax, but it still runs on 1.12's Lua 5.0 VM and documents no +collector changes. It would not reduce GC pauses, so this build does not depend on it. + +## Flat bar textures + +Two new bar textures, **Puppeteer Flat** and **Puppeteer Flat Borderless**, are available in the Health Bar Texture +and Power Bar Texture dropdowns. The original Puppeteer textures are brighter in the top half than the bottom +(about 188 vs 137–120 brightness), which makes bar colors look darker halfway down. The flat textures use the +top-half brightness all the way down. The bordered one keeps Puppeteer's dark top and bottom edge. + +## Shorter frame titles + +The title bar above each frame group ("Party", "Raid", ...) is 16 px tall instead of 20 px. The height is the +`HEADER_HEIGHT` constant at the top of `PTUnitFrameGroup.lua`. + +## Pets use their owner's class color + +When health bars or names use the Class color, pets now take their owner's class color, so a hunter's pet is hunter +green and a warlock's pet is warlock purple. It is on by default and can be turned off with "Pets Use Owner's Class +Color" in the options. The owner of each pet slot (pet, partypet1-4, raidpet1-40) is looked up in a table built at +load, so this adds no garbage. + +## Move frames with hidden titles + +A frame group whose title is hidden had nothing left to grab, so it couldn't be moved. Now, while a group's title is +hidden, holding Shift+Ctrl+Alt and dragging any of its unit frames with the left mouse button moves the group. It uses +the normal drag code, so frame locking, "move all frames" and saved positions behave as with the title. That click +never casts, even if the keys are released before the mouse button. Groups with a visible title or a locked position +keep the old behavior, including any Shift+Ctrl+Alt left-click binding. diff --git a/MODERN-BLIZZARD.txt b/MODERN-BLIZZARD.txt new file mode 100644 index 0000000..0c4cd84 --- /dev/null +++ b/MODERN-BLIZZARD.txt @@ -0,0 +1,47 @@ +Blizzard Modern customization + +Based on OEVG2/Puppeteer commit 502e151f755c99f6f74005f9fd3489ea7519e6a3. + +Installation: +1. Exit WoW completely so the client can discover the new texture. +2. Back up your existing Interface\AddOns\Puppeteer folder. +3. Extract the Puppeteer folder in this ZIP into Interface\AddOns, + replacing the existing addon files. Keep your WTF saved settings. +4. Start WoW, type /pt, and open Customize. +5. Select the style to customize (or the default override). +6. Set Health Bar Texture to Blizzard Modern. +7. Set Health Bar Color to Class to use the supplied nine-class palette. + Enemy health bars use their separate enemy color setting. +8. Use the Reload UI button if the changes are not yet visible. + +Set Power Bar Texture to Blizzard Modern to use the exact supplied colored +resource artwork with white tint and an opaque black backing. Puppeteer's +generic bars use the reference's Player family: mana always uses ManaClass; +rogue/druid energy uses EnergyClass; rage, focus and other energy use the +standard Player variants. Actual resource type determines the artwork, +including shapeshifts. Other power textures use the normal resource colors. +Names retain their existing colors. +Health Numbers Color and Power Numbers Color under Customize each offer +White. Use Style Default restores the original gold +font color. Select your style and click Reload UI after changing settings. +These options work with any bar texture. Health hover highlights, debuff +indicators, missing-health and status messages keep their existing colors. +Debuff colors, charm indicators and incoming-heal effects retain their priority. +Choosing another texture restores the existing health class palette, including +the Vanilla Class Colors preference. Blizzard Modern uses the exact supplied +palette even when that preference is enabled. + +Assets: +BlizzardRetailBar.tga copied unchanged from the user's supplied reference ZIP. +Reference source: BetterBlizzFrames by Bodify, media/blizzTex/BlizzardRetailBar.tga +https://github.com/Bodify/BetterBlizzFrames +The supplied reference identifies this asset as All Rights Reserved, +Copyright (c) 2023-2026 Bodify; no new redistribution rights are granted. +Class palette copied from the supplied ModernClassColors.lua reference. +Power assets copied byte for byte from Modern-Style-Handoff, frozen source +commit 2933f2fce1d3ac4aa72bfc6e9b0e24bd673b78ae of +https://github.com/OEVG2/ShaguTweaks-ClassicAPI +See assets/textures/bars/MODERN-POWER-NOTICES.md for their attribution. + +Validation: Lua syntax and isolated health-color behavior checked locally. +An in-game visual check is still required. diff --git a/PTUnitFrame.lua b/PTUnitFrame.lua new file mode 100644 index 0000000..445549c --- /dev/null +++ b/PTUnitFrame.lua @@ -0,0 +1,1932 @@ +PTUnitFrame = {} +PTUtil.SetEnvironment(PTUnitFrame) +PTUnitFrame.__index = PTUnitFrame +local _G = getfenv(0) +local PT = Puppeteer +local util = PTUtil +local colorize = util.Colorize +local GetColorPrefix = util.GetColorPrefix +local format = string.format +local floor = math.floor +local ceil = math.ceil +local compost = PTPool + +PTUnitFrame.owningGroup = nil + +PTUnitFrame.unit = nil +PTUnitFrame.isCustomUnit = false +PTUnitFrame.guidUnit = nil -- Used for custom units + +PTUnitFrame.rootContainer = nil -- Contains the main container and the overlay +PTUnitFrame.overlayContainer = nil -- Contains elements that should not be affected by opacity +PTUnitFrame.container = nil -- Most elements are contained in this +PTUnitFrame.nameText = nil +PTUnitFrame.healthBar = nil +PTUnitFrame.incomingHealthBar = nil +PTUnitFrame.incomingDirectHealthBar = nil +PTUnitFrame.healthText = nil +PTUnitFrame.missingHealthText = nil +PTUnitFrame.incomingHealText = nil +PTUnitFrame.powerBar = nil +PTUnitFrame.powerText = nil +PTUnitFrame.roleIcon = nil +PTUnitFrame.button = nil +PTUnitFrame.auraPanel = nil +PTUnitFrame.scrollingDamageFrame = nil -- Unimplemented +PTUnitFrame.scrollingHealFrame = nil -- Unimplemented +PTUnitFrame.auraIconPool = {} -- array: {"frame", "icon", "stackText"} +PTUnitFrame.auraButtonPool = {} +PTUnitFrame.auraButtons = {} +PTUnitFrame.auraIcons = {} -- array: {"frame", "icon", "stackText", "button"} + +PTUnitFrame.targetOutline = nil + +PTUnitFrame.targeted = false + +PTUnitFrame.flashTexture = nil -- {"frame", "texture"} +PTUnitFrame.flashTime = 0 +PTUnitFrame.lastHealthPercent = 0 + +PTUnitFrame.incomingHealing = 0 +PTUnitFrame.incomingDirectHealing = 0 + +PTUnitFrame.hovered = false +PTUnitFrame.pressed = false + +PTUnitFrame.distanceText = nil +PTUnitFrame.lineOfSightIcon = nil -- map: {"frame", "icon"} + +PTUnitFrame.inRange = true +PTUnitFrame.distance = 0 +PTUnitFrame.inSight = true + +PTUnitFrame.fakeStats = {} -- Used for displaying a fake party/raid + +function PTUnitFrame:New(unit, isCustomUnit) + local _, _, num = string.find(unit, "(%d+)$") + local obj = setmetatable({unit = unit, isCustomUnit = isCustomUnit, auraIconPool = {}, auraButtonPool = {}, + auraButtons = {}, auraIcons = {}, fakeStats = PTUnitFrame.GenerateFakeStats(), + unitNumber = num and tonumber(num) or false}, self) + return obj +end + +local fakeNames = {"Leeroyjenkins", "Realsigred", "Appledog", "Exdraclespy", "Dieghostt", "Olascoli", "Yaijin", + "Geroya", "Artemyz", "Nomeon", "Orinnberry", "Hoppetosse", "Deathell", "Jackbob", "Luscita", "Healpiggies", + "Pamara", "Merauder", "Onetwofree", "Biggly", "Drexx", "Grassguzzler", "Thebackup", "Steaktank", "Fshoo", + "Bovinebill", "Rawtee", "Aylin", "Sneeziesnorf", "Dreak", "Jordin", "Evilkillers", "Xathas", "Linkado", + "Smiteknight", "Rollnmbqs", "Viniss", "Rinnegon", "Elfdefense", "Foxtau", "Tombdeath", "Myhawk", "Numnumcat", + "Laudead", "Esatto", "Boffin", "Tikomo", "Huddletree", "Butterboy", "Bolgrand", "Ginius", "Exulthiuss", + "Xplol", "Wheeliebear", "Pimenton", "Meditating", "Qyroth", "Lazhar", "Rookon", "Eiris", "Padren", + "Erazergus", "Scarlatina", "Holdrim", "Soulbane", "Debilitated", "Doorooid", "Palefire", "Tellarna", + "Breathofwing", "Chillaf", "Hulena", "Hyperiann", "Bluebeam", "Daevana", "Adriena", "Aeywynn", "Bluaa", + "Chadd", "Leutry", "Mouzer", "Qiner"} +function PTUnitFrame.GenerateFakeStats() + + local name = fakeNames[math.random(table.getn(fakeNames))] + + local class = util.GetRandomClass() + + local currentHealth + local maxHealth = math.random(100, 5000) + if math.random(10) > 3 then + currentHealth = math.random(1, maxHealth) + elseif math.random(8) == 1 then + currentHealth = 0 + else + currentHealth = maxHealth + end + + local maxPower = math.random(100, 5000) + if util.ClassPowerTypes[class] ~= "mana" then + maxPower = 100 + end + local currentPower = math.random(1, maxPower) + + local debuffType + local trackedDebuffCount = table.getn(PuppeteerSettings.TrackedDebuffTypes) + if trackedDebuffCount > 0 then + if math.random(1, 10) == 1 then + debuffType = PuppeteerSettings.TrackedDebuffTypes[math.random(trackedDebuffCount)] + end + end + + local raidMark + if math.random(10) == 1 then + raidMark = math.random(8) + end + + local online = not (math.random(12) == 1) + + local fakeStats = { + name = name, + class = class, + currentHealth = currentHealth, + maxHealth = maxHealth, + currentPower = currentPower, + maxPower = maxPower, + debuffType = debuffType, + raidMark = raidMark, + online = online} + return fakeStats +end + +function PTUnitFrame:GetUnit() + return self.unit +end + +function PTUnitFrame:GetResolvedUnit() + return not self.isCustomUnit and self.unit or self.guidUnit +end + +function PTUnitFrame:GetRootContainer() + return self.rootContainer +end + +function PTUnitFrame:GetContainer() + return self.container +end + +function PTUnitFrame:Show() + self.container:Show() + self.rootContainer:Show() + self:UpdateAll() +end + +function PTUnitFrame:Hide() + if not self:IsFake() then + self.container:Hide() + self.rootContainer:Hide() + end +end + +function PTUnitFrame:IsShown() + return self.rootContainer:IsShown() +end + +function PTUnitFrame:SetOwningGroup(group) + self.owningGroup = group + self:Initialize() + self:GetRootContainer():SetParent(group:GetContainer()) +end + +function PTUnitFrame:RegisterClicks() + local buttons = PTOptions.CastWhen == "Mouse Up" and util.GetUpButtons() or util.GetDownButtons() + self.button:RegisterForClicks(unpack(buttons)) + for _, button in ipairs(self.auraButtons) do + button:RegisterForClicks(unpack(buttons)) + end +end + +function PTUnitFrame:UpdateAll() + self:UpdateAuras() + self:UpdateHealth() + self:UpdatePower() + self:UpdateRange() + self:UpdateSight() + self:EvaluateTarget() + self:UpdateOutline() + self:UpdateRaidMark() + self:UpdatePVP() +end + +function PTUnitFrame:GetShowDistanceThreshold() + local threshold = self:GetProfile().ShowDistanceThreshold + return self:IsEnemy() and threshold.Hostile or threshold.Friendly +end + +function PTUnitFrame:GetOutOfRangeThreshold() + local threshold = self:GetProfile().OutOfRangeThreshold + return self:IsEnemy() and threshold.Hostile or threshold.Friendly +end + +function PTUnitFrame:UpdateRange() + local wasInRange = self.inRange + self.distance = self:GetCache():GetDistance() + self.inRange = math.ceil(self.distance) < self:GetOutOfRangeThreshold() + if wasInRange ~= self.inRange then + self:UpdateOpacity() + end + + self:UpdateRangeText() +end + +function PTUnitFrame:UpdateSight() + self.inSight = self:GetCache():IsInSight() + local frame = self.lineOfSightIcon.frame + if frame:IsShown() ~= self.inSight then + local dist = math.ceil(self.distance) + if not self.inSight and (dist < 80 or UnitIsUnit(self.unit, "target")) then + frame:Show() + else + frame:Hide() + end + end +end + +local preciseDistance = util.CanClientGetPreciseDistance() +local RANGE_PREFIX_FAR = GetColorPrefix(0.75, 0.75, 0.75) +local RANGE_PREFIX_OUT = GetColorPrefix(1, 0.3, 0.3) +local RANGE_PREFIX_NEAR = GetColorPrefix(1, 0.6, 0) +function PTUnitFrame:UpdateRangeText() + local dist = ceil(self.distance) + local prefix + if dist >= (preciseDistance and self:GetShowDistanceThreshold() or 28) and dist < 9999 then + if dist > 80 then + prefix = RANGE_PREFIX_FAR + elseif dist >= self:GetOutOfRangeThreshold() then + prefix = RANGE_PREFIX_OUT + else + prefix = RANGE_PREFIX_NEAR + end + end + -- Skip rebuilding the text if what's displayed wouldn't change + if self.lastRangeDist and prefix == self.lastRangePrefix and (not prefix or dist == self.lastRangeDist) then + return + end + self.lastRangePrefix = prefix + self.lastRangeDist = dist + + local text = "" + if prefix then + if preciseDistance then + text = format("%s%.14g yd|r", prefix, dist) + elseif dist < 28 then + text = format("%s<%.14g yd|r", prefix, dist) + else + text = prefix.."28+ yd|r" + end + end + self.distanceText:SetText(text) +end + +function PTUnitFrame:UpdateOpacity() + local profile = self:GetProfile() + + local alpha = 1 + if not self.inRange then + alpha = alpha * (profile.OutOfRangeOpacity / 100) + end + if profile.AlertPercent < math.ceil((self:GetCurrentHealth() / self:GetMaxHealth()) * 100) and + table.getn(self:GetAfflictedDebuffTypes()) == 0 then + alpha = alpha * (profile.NotAlertedOpacity / 100) + end + self.container:SetAlpha(alpha) +end + +-- Evaluate if the unit of this frame is the target and update the target outline if the state has changed +function PTUnitFrame:EvaluateTarget() + if self.unit == "target" then -- "target" frames should not show a border since it's obvious they're the target + return + end + local wasTargeted = self.targeted + self.targeted = UnitIsUnit(self.unit, "target") + if self.targeted ~= wasTargeted then + self:UpdateOutline() + end +end + +function PTUnitFrame:UpdateOutline() + local aggro = self:HasAggro() + local targeted = self.targeted + + local r, g, b + if aggro and targeted then + r, g, b = 1, 0.6, 0.5 + elseif aggro then + r, g, b = 1, 0, 0 + elseif targeted then + r, g, b = 1, 1, 0.85 + end + + self:SetOutlineColor(r, g, b) +end + +function PTUnitFrame:SetOutlineColor(r, g, b) + if r then + self.targetOutline:Show() + self.targetOutline:SetBackdropBorderColor(r, g, b, 0.75) + else + self.targetOutline:Hide() + end +end + +function PTUnitFrame:UpdateRaidMark() + local unit = self:GetResolvedUnit() + local fake = self:IsFake() + if not unit and not fake then + self.raidMarkIcon.frame:Hide() + return + end + + if unit == "target" and not UnitExists("target") then + self.raidMarkIcon.frame:Hide() + return + end + + local markIndex + if fake then + markIndex = self.fakeStats.raidMark + else + markIndex = GetRaidTargetIndex(unit) + end + if not markIndex then + self.raidMarkIcon.frame:Hide() + return + end + SetRaidTargetIconTexture(self.raidMarkIcon.icon, markIndex) + self.raidMarkIcon.frame:Show() +end + +function PTUnitFrame:UpdatePVP() + if UnitIsPVP(self.unit) and (not util.IsReallyInInstance() or not UnitIsVisible(self.unit)) then + local faction = UnitFactionGroup(self.unit) + if faction == "Alliance" then + self.pvpIcon.icon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Alliance") + elseif faction == "Horde" then + self.pvpIcon.icon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Horde") + else + self.pvpIcon.icon:SetTexture("Interface\\TargetingFrame\\UI-PVP-FFA") + end + self.pvpIcon.frame:Show() + else + self.pvpIcon.frame:Hide() + end +end + +function PTUnitFrame:Flash() + local FLASH_TIME = 0.15 + local START_OPACITY = self:GetProfile().FlashOpacity / 100 + + self.flashTime = FLASH_TIME + local frame = self.flashTexture.frame + frame:Show() + frame:SetAlpha(START_OPACITY) + + frame.flashTime = FLASH_TIME + frame.startOpacity = START_OPACITY + + if not frame:GetScript("OnUpdate") then + frame:SetScript("OnUpdate", PTUnitFrame.Flash_OnUpdate) + end +end + +function PTUnitFrame.Flash_OnUpdate() + local frame = this + local self = frame.unitFrame + local FLASH_TIME = frame.flashTime + local START_OPACITY = frame.startOpacity + self.flashTime = self.flashTime - arg1 + frame:SetAlpha(START_OPACITY - (((FLASH_TIME - self.flashTime) / FLASH_TIME) * START_OPACITY)) + + if self.flashTime <= 0 then + frame:Hide() + frame:SetScript("OnUpdate", nil) + end +end + +-- If direct healing is nil, it will be assumed that all the incoming healing is direct healing +function PTUnitFrame:SetIncomingHealing(incomingHealing, incomingDirectHealing) + self.incomingHealing = incomingHealing + self.incomingDirectHealing = incomingDirectHealing or incomingHealing + self:UpdateHealth() +end + +function PTUnitFrame:UpdateIncomingHealing() + if PTHealPredict then + local _, guid = UnitExists(self:GetUnit()) + self:SetIncomingHealing(PTHealPredict.GetIncomingHealing(guid)) + else + local name = UnitName(self:GetUnit()) + self:SetIncomingHealing(Puppeteer.HealComm:getHeal(name)) + end +end + +function PTUnitFrame:GetCurrentHealth() + if self:IsFake() then + if not self.fakeStats.online then + return 0 + end + return self.fakeStats.currentHealth + end + return UnitHealth(self.unit) +end + +function PTUnitFrame:GetMaxHealth() + if self:IsFake() then + return self.fakeStats.maxHealth + end + return UnitHealthMax(self.unit) +end + +function PTUnitFrame:GetCurrentPower() + if self:IsFake() then + return self.fakeStats.currentPower + end + return UnitMana(self.unit) +end + +function PTUnitFrame:GetMaxPower() + if self:IsFake() then + return self.fakeStats.maxPower + end + return UnitManaMax(self.unit) +end + +function PTUnitFrame:ShouldShowMissingHealth() + local profile = self:GetProfile() + local currentHealth = self:GetCurrentHealth() + if currentHealth == 0 then + return false + end + local missingHealth = self:GetMaxHealth() - currentHealth + return (missingHealth > 0 or profile.AlwaysShowMissingHealth) and profile.MissingHealthDisplay ~= "Hidden" + and (profile.ShowEnemyMissingHealth or not self:IsEnemy()) + and not UnitIsGhost(self.unit) and (UnitIsConnected(self.unit) or self:IsFake()) +end + +-- Used for custom colors in profiles. +-- Valid Colors: +-- "Class" - Uses the unit's class color +-- "Default" - Does not color the text +-- Array RGB - Colors the text as the RGB values +-- Returns the color code prefix for a profile text color, or nil if the text isn't colored +function PTUnitFrame:GetTextColorPrefix(color) + if color == "Class" then + local r, g, b = util.GetClassColor(self:GetColorClass()) + return GetColorPrefix(r, g, b) + elseif type(color) == "table" then + return GetColorPrefix(color[1], color[2], color[3]) + end +end + +function PTUnitFrame:ColorizeText(inputText, color) + if color == "Class" then + local r, g, b = util.GetClassColor(self:GetColorClass()) + return colorize(inputText, r, g, b) + elseif type(color) == "table" then + return colorize(inputText, color) + end + return inputText +end + +function PTUnitFrame:ApplyNumberColor(text, choice) + if choice == "White" then + text:SetTextColor(1, 1, 1) + else + text:SetTextColor(GameFontNormal:GetTextColor()) + end +end + +local HOVER_PREFIX = GetColorPrefix(1, 1, 1) +local MISSING_HEALTH_FORMATS = {Plain = "-%.14g", Colored = "%s-%.14g|r", + Inline = "%s (-%.14g)", InlineColored = "%s%s (-%.14g)|r"} +local MISSING_PERCENT_FORMATS = {Plain = "-%.14g%%", Colored = "%s-%.14g%%|r", + Inline = "%s (-%.14g%%)", InlineColored = "%s%s (-%.14g%%)|r"} +function PTUnitFrame:UpdateHealth() + local fake = self:IsFake() + if not UnitExists(self.unit) and not fake then + if self.isCustomUnit or self.unit == "target" then + self.healthText:SetText(colorize(self.unit ~= "target" and "Too Far" or "", 0.7, 0.7, 0.7)) + self.missingHealthText:SetText("") + self:SetHealthBarValue(0) + self.powerBar:SetValue(0) + self:UpdateOpacity() + self:AdjustHealthPosition() + end + if self.unit == "target" then + self.nameText:SetText("") + end + return + end + local profile = self:GetProfile() + local unit = self.unit + + local fakeOnline = self.fakeStats.online + self:ApplyNumberColor(self.healthText, profile.HealthNumbersColor) + + local currentHealth = self:GetCurrentHealth() + local maxHealth = self:GetMaxHealth() + + local unitName = self:GetName() + + self:UpdateOpacity() -- May be changed further down + + if not UnitIsConnected(unit) and (not fake or not fakeOnline) then + self.nameText:SetText(self:ColorizeText(unitName, profile.NameText.Color)) + self.healthText:SetText(colorize("Offline", 0.7, 0.7, 0.7)) + self.missingHealthText:SetText("") + self:SetHealthBarValue(0) + self.powerBar:SetValue(0) + self:UpdateOpacity() + self:AdjustHealthPosition() + return + end + + local enemy = self:IsEnemy() + + -- Set Name and its colors + local nameText + if self:IsPlayer() or fake then + local r, g, b + if profile.ShowDebuffColorsOn == "Name" and not enemy then + r, g, b = self:GetDebuffColor() + end + if r then + nameText = colorize(unitName, r, g, b) + else + nameText = self:ColorizeText(unitName, profile.NameText.Color) + end + else -- Unit is not a player + if enemy then + nameText = colorize(unitName, 1, 0.3, 0.3) + else -- Unit is not an enemy + local r, g, b + if profile.ShowDebuffColorsOn == "Name" then + r, g, b = self:GetDebuffColor() + end + if r then + nameText = colorize(unitName, r, g, b) + else + nameText = unitName + end + end + end + self.nameText:SetText(nameText) + + local healthText = self.healthText + local missingHealthText = self.missingHealthText + + -- Set Health Status + if currentHealth <= 0 then -- Unit Dead + local cache = self:GetCache() + local text + if cache:IsBeingResurrected() then + if cache:GetResurrectionCasts() > 1 then + text = colorize("DEAD", 0.8, 1, 0.8) + else + text = colorize("DEAD", 0.3, 1, 0.3) + end + else + text = colorize("DEAD", 1, 0.3, 0.3) + end + + -- Check for Feign Death so the healer doesn't get alarmed + local feign = self:GetCache():HasBuffIDOrName(5384, "Feign Death") + if feign then + text = "Feign" + end + + healthText:SetText(text) + missingHealthText:SetText("") + self:SetHealthBarValue(0) + self.powerBar:SetValue(0) + if self.lastHealthPercent > 0 and not self:IsEnemy() then + if not feign then + self:Flash() + end + self.lastHealthPercent = 0 + end + elseif UnitIsGhost(unit) then + healthText:SetText(colorize("Ghost", 1, 0.3, 0.3)) + missingHealthText:SetText("") + self:SetHealthBarValue(0) + self.powerBar:SetValue(0) + else -- Unit Not Dead + -- Each displayed string is built by a single format call, so no intermediate strings are created. + -- A debuff color takes priority over the white hover highlight, as it did when the colors were nested. + local prefix + if profile.ShowDebuffColorsOn == "Health" then + local r, g, b = self:GetDebuffColor() + if r then + prefix = GetColorPrefix(r, g, b) + end + end + if not prefix and self.hovered then + prefix = HOVER_PREFIX + end + + local text = "" + local missingText = "" + local healthDisplay = profile.HealthDisplay + if healthDisplay == "Health/Max Health" then + if prefix then + text = format("%s%.14g/%.14g|r", prefix, currentHealth, maxHealth) + else + text = format("%.14g/%.14g", currentHealth, maxHealth) + end + elseif healthDisplay == "Health" then + if prefix then + text = format("%s%.14g|r", prefix, currentHealth) + else + text = currentHealth + end + elseif healthDisplay == "% Health" then + local percent = floor((currentHealth / maxHealth) * 100) + if prefix then + text = format("%s%.14g%%|r", prefix, percent) + else + text = format("%.14g%%", percent) + end + elseif prefix then + text = prefix.."|r" + end + + if self:ShouldShowMissingHealth() then + local missingHealthDisplay = profile.MissingHealthDisplay + local missingValue + local formats + if missingHealthDisplay == "-Health" then + missingValue = floor(maxHealth - currentHealth) + formats = MISSING_HEALTH_FORMATS + elseif missingHealthDisplay == "-% Health" then + missingValue = ceil((floor(maxHealth - currentHealth) / maxHealth) * 100) + formats = MISSING_PERCENT_FORMATS + end + + if missingValue then + local missingPrefix = self:GetTextColorPrefix(profile.HealthTexts.Missing.Color) + if profile.MissingHealthInline then + if text ~= "" then + if missingPrefix then + text = format(formats.InlineColored, text, missingPrefix, missingValue) + else + text = format(formats.Inline, text, missingValue) + end + end + elseif missingPrefix then + missingText = format(formats.Colored, missingPrefix, missingValue) + else + missingText = format(formats.Plain, missingValue) + end + end + end + + healthText:SetText(text) + missingHealthText:SetText(missingText) + + self:SetHealthBarValue(currentHealth / maxHealth) + + local healthPercent = (currentHealth / maxHealth) * 100 + if healthPercent < self.lastHealthPercent - profile.FlashThreshold and not self:IsEnemy() then + self:Flash() + end + self.lastHealthPercent = healthPercent + + if self:GetCache():HasBuffIDOrName(27827, "Spirit of Redemption") then + healthText:SetText(colorize("Spirit", 1, 0.3, 0.3)) + end + end + + self:UpdateOpacity() + self:AdjustHealthPosition() +end + +local greenToRedColors = {{1, 0, 0}, {1, 0.3, 0}, {1, 1, 0}, {0.6, 0.92, 0}, {0, 0.8, 0}} +function PTUnitFrame:SetHealthBarValue(value) + local unit = self.unit + local healthBar = self.healthBar + local incomingHealthBar = self.incomingHealthBar + local incomingDirectHealthBar = self.incomingDirectHealthBar + local incomingHealText = self.incomingHealText + local incomingHealing = self.incomingHealing + local incomingDirectHealing = self.incomingDirectHealing + local profile = self:GetProfile() + local enemy = self:IsEnemy() + + healthBar:SetValue(value) + + local healthIncMaxRatio = 0 + local healthIncDirectMaxRatio = 0 + + if incomingHealing > 0 then + healthIncMaxRatio = value + (incomingHealing / self:GetMaxHealth()) + healthIncDirectMaxRatio = value + (incomingDirectHealing / self:GetMaxHealth()) + incomingHealthBar:SetValue(healthIncMaxRatio) + incomingDirectHealthBar:SetValue(healthIncDirectMaxRatio) + if profile.IncomingHealDisplay == "Overheal" then + if healthIncMaxRatio > 1 then + incomingHealText:SetText(format("+%.14g", ceil(self:GetCurrentHealth() + incomingHealing - self:GetMaxHealth()))) + local rgb = incomingDirectHealing > 0 and profile.IncomingHealText.Color or + profile.IncomingHealText.IndirectColor + if incomingDirectHealing > 0 then + incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3]) + else + incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3]) + end + else + incomingHealText:SetText("") + end + elseif profile.IncomingHealDisplay == "Heal" then + incomingHealText:SetText(format("+%.14g", ceil(incomingHealing))) + local rgb = incomingDirectHealing > 0 and profile.IncomingHealText.Color or + profile.IncomingHealText.IndirectColor + if incomingDirectHealing > 0 then + incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3]) + else + incomingHealText:SetTextColor(rgb[1], rgb[2], rgb[3]) + end + else + incomingHealText:SetText("") + end + else -- No incoming healing + incomingHealthBar:SetValue(0) + incomingDirectHealthBar:SetValue(0) + incomingHealText:SetText("") + end + incomingHealthBar:SetAlpha(0.35) + incomingDirectHealthBar:SetAlpha(0.4) + + local r, g, b + + if profile.ShowDebuffColorsOn == "Health Bar" then + r, g, b = self:GetDebuffColor() + end + + if UnitIsCharmed(unit) and enemy then + r, g, b = 0.25, 0.25, 0.25 + end + + if r == nil then -- If there's no debuff color, proceed to normal colors + local hbc = enemy and profile.EnemyHealthBarColor or profile.HealthBarColor + if hbc == "Class" then + local class = self:GetColorClass() + if class == nil then + class = self.fakeStats.class + end + r, g, b = util.GetClassColor(class, false, profile.HealthBarStyle == "Blizzard Modern") + elseif hbc == "Green" then + r, g, b = 0, 0.8, 0 + elseif hbc == "Green To Red" then + r, g, b = util.InterpolateColorsNoTable(greenToRedColors, value) + end + + if healthIncMaxRatio > 1 then + local brightenFactor = math.min(((healthIncMaxRatio - 1) / 4) + 1, 1.25) + r = math.min(r * brightenFactor, 1) + g = math.min(g * brightenFactor, 1) + b = math.min(b * brightenFactor, 1) + end + end + healthBar:SetStatusBarColor(r, g, b) + incomingHealthBar:SetStatusBarColor(0, 0.8, 0) + incomingDirectHealthBar:SetStatusBarColor(0, 0.8, 0) + + local feign = self:GetCache():HasBuffIDOrName(5384, "Feign Death") + local bg = healthBar.background + if value == 0 and not feign then + bg:SetTexture(0.5, 0.5, 0.5, 0.5) + elseif value < 0.3 and not enemy and not feign then + bg:SetTexture(1, 0.4, 0.4, 0.25) + else + bg:SetTexture(0.5, 0.5, 0.5, 0.25) + end +end + +-- Returns the r, g, b of the current dispellable debuff color, or nil if none +function PTUnitFrame:GetDebuffColor() + local enemy = self:IsEnemy() + if not enemy then -- Do not display debuff colors for enemies + for _, trackedDebuffType in ipairs(PuppeteerSettings.TrackedDebuffTypes) do + if self:GetTrackedDebuffTypes()[trackedDebuffType] then + local debuffTypeColor = PuppeteerSettings.DebuffTypeColors[trackedDebuffType] + return debuffTypeColor[1], debuffTypeColor[2], debuffTypeColor[3] + end + end + end + + local fake = self:IsFake() + if fake and self.fakeStats.debuffType then + local debuffTypeColor = PuppeteerSettings.DebuffTypeColors[self.fakeStats.debuffType] + return debuffTypeColor[1], debuffTypeColor[2], debuffTypeColor[3] + end +end + +function PTUnitFrame:UpdatePower() + local profile = self:GetProfile() + self:ApplyNumberColor(self.powerText, profile.PowerNumbersColor) + local unit = self.unit + local powerBar = self.powerBar + local fake = self:IsFake() + local class = fake and self.fakeStats.class or util.GetClass(unit) + local currentPower = self:GetCurrentPower() + local maxPower = self:GetMaxPower() + + if not UnitExists(self.unit) and not fake then + powerBar:SetValue(0) + powerBar.modernBackground:Hide() + self.powerText:SetText("") + return + end + + local powerType = fake and util.ClassPowerTypes[class or "WARRIOR"] or util.GetPowerType(unit) + local powerColor = util.PowerColors[powerType] or util.PowerColors.mana + local modernTexture = profile.PowerBarStyle == "Blizzard Modern" and PT.GetModernPowerTexture(powerType, class) + local texture = modernTexture or PT.BarStyles[profile.PowerBarStyle] + if powerBar.currentPowerTexture ~= texture then + powerBar:SetStatusBarTexture(texture) + powerBar.currentPowerTexture = texture + end + + powerBar:SetValue(maxPower > 0 and currentPower / maxPower or 0) + if modernTexture then + powerBar:SetStatusBarColor(1, 1, 1, 1) + powerBar.modernBackground:Show() + else + powerBar:SetStatusBarColor(powerColor[1], powerColor[2], powerColor[3]) + powerBar.modernBackground:Hide() + end + local text = "" + if profile.PowerDisplay == "Power" then + text = currentPower + elseif profile.PowerDisplay == "Power/Max Power" then + text = format("%.14g/%.14g", currentPower, maxPower) + elseif profile.PowerDisplay == "% Power" then + if maxPower == 0 then + maxPower = 1 + end + text = format("%.14g%%", floor((currentPower / maxPower) * 100)) + end + self.powerText:SetText(text) +end + +AuraTooltip = CreateFrame("GameTooltip", "PTAuraTooltip", UIParent, "GameTooltipTemplate") + +local AURA_DURATION_TEXT_FLASH_THRESHOLD = 5 +local AURA_DURATION_TEXT_LOW_THRESHOLD = 30 +-- A map of all seconds below the flash threshold to an array of colors to interpolate +local durationTextFlashColorsRange +if util.IsSuperWowPresent() then + local flashColorsReset = {{1, 1, 0.75}, {1, 0.7, 0.55}, {1, 0.6, 0.6}} + local flashColors = {{1, 1, 0.25}, {1, 0.6, 0.35}, {1, 0.4, 0.4}} + + local textFlashColors = {} + for i = 0, AURA_DURATION_TEXT_FLASH_THRESHOLD + 1 do + textFlashColors[i] = {} + textFlashColors[i][1] = util.InterpolateColors(flashColors, + (AURA_DURATION_TEXT_FLASH_THRESHOLD - i) / AURA_DURATION_TEXT_FLASH_THRESHOLD) + textFlashColors[i][2] = util.InterpolateColors(flashColorsReset, + (AURA_DURATION_TEXT_FLASH_THRESHOLD - i) / AURA_DURATION_TEXT_FLASH_THRESHOLD) + end + durationTextFlashColorsRange = {} + for seconds, colors in pairs(textFlashColors) do + durationTextFlashColorsRange[seconds] = {colors[2], colors[1]} + end +end +-- "Nm" duration strings, cached so they aren't rebuilt every time a timer ticks +local minutesTexts = {} +function GetMinutesText(minutes) + local text = minutesTexts[minutes] + if not text then + text = minutes.."m" + minutesTexts[minutes] = text + end + return text +end + +function PTUnitFrame:AllocateAura() + local frame = CreateFrame("Frame", nil, self.auraPanel) + frame.unitFrame = self + + local icon = frame:CreateTexture(nil, "ARTWORK") + local border = frame:CreateTexture(nil, "OVERLAY") + border:SetTexture("Interface\\Buttons\\UI-Debuff-Overlays") + border:SetTexCoord(0.296875, 0.5703125, 0, 0.515625) + border:SetPoint("TOPLEFT", icon, "TOPLEFT", -0.5, 0.5) + border:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0.5, -0.5) + local stackText = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + stackText:SetTextColor(1, 1, 1) + + -- Duration display, only used when SuperWoW is present + if util.IsSuperWowPresent() then + local duration = CreateFrame("Model", nil, frame, "CooldownFrameTemplate") + duration.noCooldownCount = true + duration:SetAlpha(0.8) + local durationOverlayFrame = CreateFrame("Frame", nil, frame) + durationOverlayFrame:SetFrameLevel(durationOverlayFrame:GetFrameLevel() + 1) + local durationText = durationOverlayFrame:CreateFontString(nil, "ARTWORK", "GameFontNormal") + durationText:SetPoint("BOTTOMLEFT", durationOverlayFrame, "BOTTOMLEFT", 0, 0) + durationText.SetSeconds = function(self, seconds) + self.seconds = seconds + if seconds == nil then + self:SetText("") + return + end + self:SetText(seconds <= 60 and seconds or GetMinutesText(ceil(seconds / 60))) + local fontSize = ceil(frame:GetHeight() * (seconds < 540 and (seconds < 10 and 0.6 or 0.45) or 0.35)) + if fontSize ~= self.fontSize then + self.fontSize = fontSize + self:SetFont("Fonts\\FRIZQT__.TTF", fontSize, "OUTLINE") + end + end + duration.UpdateText = function() + local seconds = duration.seconds + local secondsPrecise = duration.secondsPrecise + durationText:SetSeconds(seconds) + if seconds <= AURA_DURATION_TEXT_FLASH_THRESHOLD then + durationText:SetTextColor( + util.InterpolateColorsNoTable(durationTextFlashColorsRange[seconds], + secondsPrecise - seconds)) + elseif seconds <= AURA_DURATION_TEXT_LOW_THRESHOLD then + durationText:SetTextColor(1, 1, 0.25) + else + durationText:SetTextColor(1, 1, 1) + end + duration:SetScript("OnUpdate", nil) + end + local SetSequenceTime = duration.SetSequenceTime + local GetTime = GetTime + duration:SetScript("OnUpdateModel", function() + if duration.stopping == 0 then + duration:SetAlpha(0.8) + local time = GetTime() + local progress = (time - duration.start) / duration.duration + if progress < 1.0 then + SetSequenceTime(duration, 0, 1000 - (progress * 1000)) + local secondsPrecise = duration.start - time + duration.duration + local seconds = math.floor(secondsPrecise) + if seconds <= (duration.displayAt or AURA_DURATION_TEXT_FLASH_THRESHOLD) then + if durationText.seconds ~= seconds or seconds <= AURA_DURATION_TEXT_FLASH_THRESHOLD then + -- You don't want to know why it's gotta be done like this.. + -- (If you're insane and you do, it's because otherwise the text will disappear for one frame otherwise) + duration.seconds = seconds + duration.secondsPrecise = secondsPrecise + duration:SetScript("OnUpdate", duration.UpdateText) + end + elseif durationText.seconds ~= nil then + durationText:SetSeconds(nil) + end + return + end + durationText:SetSeconds(nil) + SetSequenceTime(duration, 0, 0) + end + end) + return {["frame"] = frame, ["icon"] = icon, ["border"] = border, ["stackText"] = stackText, ["overlay"] = durationOverlayFrame, + ["durationText"] = durationText, ["duration"] = duration} + end + return {["frame"] = frame, ["icon"] = icon, ["border"] = border, ["stackText"] = stackText} +end + +-- Get an icon from the available pool. Automatically inserts into the used pool. +function PTUnitFrame:GetUnusedAura() + local aura + if table.getn(self.auraIconPool) > 0 then + aura = table.remove(self.auraIconPool, table.getn(self.auraIconPool)) + else + aura = self:AllocateAura() + end + aura.frame:SetAlpha(aura.frame:GetParent():GetAlpha()) + aura.button = self:GetUnusedAuraButton() + aura.button.AuraOwner = aura + aura.button:Show() + table.insert(self.auraIcons, aura) + return aura +end + +function PTUnitFrame:GetUnusedAuraButton() + local button + if table.getn(self.auraButtonPool) > 0 then + button = table.remove(self.auraButtonPool, table.getn(self.auraButtonPool)) + else + button = CreateFrame("Button", nil, self.button) + button:RegisterForClicks(unpack(PTOptions.CastWhen == "Mouse Up" and util.GetUpButtons() or util.GetDownButtons())) + button.unitFrame = self + button:SetScript("OnClick", PTUnitFrame.AuraButton_OnClick) + button:SetScript("OnMouseDown", PTUnitFrame.AuraButton_OnMouseDown) + button:SetScript("OnMouseUp", PTUnitFrame.AuraButton_OnMouseUp) + button:SetScript("OnEnter", PTUnitFrame.AuraButton_OnEnter) + button:SetScript("OnLeave", PTUnitFrame.AuraButton_OnLeave) + table.insert(self.auraButtons, button) + end + return button +end + +function PTUnitFrame.AuraButton_OnClick() + local ufButton = this.unitFrame.button + if MouseIsOver(ufButton) then + ufButton:GetScript("OnClick")() + end +end + +function PTUnitFrame.AuraButton_OnMouseDown() + local ufButton = this.unitFrame.button + if MouseIsOver(ufButton) then + this:ClearAllPoints() + this:SetAllPoints(ufButton) + this.IsHeld = true + ufButton:GetScript("OnMouseDown")() + end +end + +function PTUnitFrame.AuraButton_OnMouseUp() + this:ClearAllPoints() + this.IsHeld = nil + if this.AuraOwner then + this:SetAllPoints(this.AuraOwner.frame) + else + local this = this + PTUtil.RunLater(function() + this:Hide() + + table.insert(this.unitFrame.auraButtonPool, this) + end) + end + this.unitFrame.button:GetScript("OnMouseUp")() +end + +function PTUnitFrame.AuraButton_OnEnter() + local ufButton = this.unitFrame.button + if this.AuraOwner then + this.unitFrame:ApplyAuraTooltip(this.AuraOwner.frame) + end + if MouseIsOver(ufButton) then + ufButton:GetScript("OnEnter")() + end +end + +function PTUnitFrame.AuraButton_OnLeave() + AuraTooltip:Hide() + this.unitFrame.button:GetScript("OnLeave")() +end + +function PTUnitFrame:ReleaseAuras() + if table.getn(self.auraIcons) == 0 then + return + end + -- Release all icons back to the icon pool + for _, aura in ipairs(self.auraIcons) do + local frame = aura.frame + frame:Hide() + frame:ClearAllPoints() + + local icon = aura.icon + icon:ClearAllPoints() + + local stackText = aura.stackText + stackText:ClearAllPoints() + stackText:SetText("") + + local button = aura.button + aura.button = nil + button.AuraOwner = nil + if not button.IsHeld then -- Don't release button if the user is currently holding it + button:ClearAllPoints() + button:Hide() + table.insert(self.auraButtonPool, button) + end + + if util.IsSuperWowPresent() then + aura.durationText:SetSeconds(nil) + CooldownFrame_SetTimer(aura.duration, 0, 0, 0) + end + + table.insert(self.auraIconPool, aura) + end + self.auraIcons = compost:Erase(self.auraIcons) +end + +do + local trackedBuffs = PuppeteerSettings.TrackedBuffs + function PTUnitFrame.BuffSorter(a, b) + return trackedBuffs[a.name] < trackedBuffs[b.name] + end +end + +do + local trackedDebuffs = PuppeteerSettings.TrackedDebuffs + function PTUnitFrame.DebuffSorter(a, b) + return trackedDebuffs[a.name] < trackedDebuffs[b.name] + end +end +local enemyDebuffSorter = function(a, b) + if a.time and not b.time then + return true + end + if not a.time and b.time then + return false + end + if not a.time and not b.time then + return a.name > b.name + end + if UnitIsUnit(a.time.owner, "player") and not UnitIsUnit(b.time.owner, "player") then + return true + end + if not UnitIsUnit(a.time.owner, "player") and UnitIsUnit(b.time.owner, "player") then + return false + end + return (a.time.startTime + a.time.duration) < (b.time.startTime + b.time.duration) +end +function PTUnitFrame:UpdateAuras() + Puppeteer.StartTiming("UFUpdateAuras") + local profile = self:GetProfile() + local unit = self.unit + local enemy = self:IsEnemy() + + self:ReleaseAuras() + + if profile.AuraTracker.Height < 1 then + return + end + + local cache = self:GetCache() + + if cache.HasImportantDebuff then + if not self.highlightBorder then + self.highlightBorder = PTGuiLib.Get("puppeteer_highlight_border") + self.highlightBorder:AttachToUnitFrame(self) + end + else + if self.highlightBorder then + self.highlightBorder:Dispose() + self.highlightBorder = nil + end + end + + local trackedBuffs = PuppeteerSettings.TrackedBuffs + + local buffs = compost:GetTable() -- Buffs that are tracked because of matching name + for name, array in pairs(cache.BuffsMap) do + if trackedBuffs[name] or enemy then + util.AppendArrayElements(buffs, array) + end + end + + if not enemy then + table.sort(buffs, self.BuffSorter) + end + + + local trackedDebuffs = PuppeteerSettings.TrackedDebuffs + local trackedDebuffTypes = PuppeteerSettings.TrackedDebuffTypesSet + + local debuffs = compost:GetTable() -- Debuffs that are tracked because of matching name, later combined with typed debuffs + local typedDebuffs = compost:GetTable() -- Debuffs that are tracked because it's a tracked type (like "Magic" or "Disease") + for name, array in pairs(cache.DebuffsMap) do + if trackedDebuffs[name] or enemy then + util.AppendArrayElements(debuffs, array) + else + -- Check if debuff is a tracked type + for _, debuff in ipairs(array) do + if trackedDebuffTypes[debuff.type] then + table.insert(typedDebuffs, debuff) + end + end + end + end + + if not enemy then + table.sort(debuffs, self.DebuffSorter) + util.AppendArrayElements(debuffs, typedDebuffs) + elseif util.IsSuperWowPresent() then + table.sort(debuffs, enemyDebuffSorter) + end + + local auraTrackerProps = profile.AuraTracker + local width = auraTrackerProps.Width == "Anchor" and auraTrackerProps:GetAnchorComponent(self):GetWidth() or auraTrackerProps.Width + local auraSize = auraTrackerProps.Height + local origSize = auraSize + local spacing = profile.TrackedAurasSpacing + local origSpacing = spacing + local auraCount = table.getn(buffs) + table.getn(debuffs) + + -- If there's not enough space, shrink until all auras fit + while ((auraSize * auraCount) + (spacing * (auraCount - 1)) > width) and auraSize >= 1 do + auraSize = auraSize - 1 + spacing = (auraSize / origSize) * origSpacing + end + + local xOffset = 0 + local yOffset = profile.TrackedAurasAlignment == "TOP" and 0 or origSize - auraSize + for _, buff in ipairs(buffs) do + local aura = self:GetUnusedAura() + self:CreateAura(aura, buff, xOffset, -yOffset, "Buff", auraSize) + xOffset = xOffset + auraSize + spacing + end + xOffset = 0 + for _, debuff in ipairs(debuffs) do + local aura = self:GetUnusedAura() + self:CreateAura(aura, debuff, xOffset, -yOffset, "Debuff", auraSize) + xOffset = xOffset - auraSize - spacing + end + compost:Reclaim(buffs) + compost:Reclaim(debuffs) + compost:Reclaim(typedDebuffs) + Puppeteer.EndTiming("UFUpdateAuras") +end + +function PTUnitFrame:ApplyAuraTooltip(auraFrame) + local index = auraFrame.auraIndex + local type = auraFrame.auraType + local auraData = auraFrame.auraData + + local tooltip = AuraTooltip + local cache = self:GetCache() + tooltip:SetOwner(auraFrame, "ANCHOR_BOTTOMLEFT") + local unit = self:GetResolvedUnit() + if auraData then + if type == "Buff" then + tooltip:SetUnitBuff(unit, index) + else + tooltip:SetUnitDebuff(unit, index) + end + local auraTime = auraData.time + if auraTime then + local seconds = math.floor(auraTime.startTime - GetTime() + auraTime.duration) + local time + local format + if seconds < 60 then + time = seconds + if time == 1 then + format = SPELL_TIME_REMAINING_SEC + else + format = SPELL_TIME_REMAINING_SEC_P1 + end + else + time = math.ceil(seconds / 60) + format = SPELL_TIME_REMAINING_MIN_P1 + end + if auraTime.ownerName and seconds > -10 then + tooltip:AddDoubleLine(string.format(format, time), "Caster: "..auraTime.ownerName) + --if not auraTime.nampower then + -- tooltip:AddLine("Not From Nampower") + --end + else + tooltip:AddLine(string.format(format, time)) + end + end + tooltip:Show() + else + Puppeteer.print(colorize("Hovered over phantom aura!", 1, 0.3, 0.3)) + Puppeteer.print("Unit: "..self:GetUnit()) + Puppeteer.print("Aura: "..index.." ("..type..")") + local lastIndex = 0 + for i = 1, 33 do + local aura = (type == "Buff" and cache.Buffs or cache.Debuffs)[i] + if not aura then + lastIndex = i - 1 + break + end + end + Puppeteer.print("Final index: "..lastIndex) + end +end + +local debuffTypeBorderColors = { + ["Magic"] = {0.2, 0.6, 1.0}, + ["Curse"] = {0.6, 0.0, 1.0}, + ["Disease"] = {0.6, 0.4, 0}, + ["Poison"] = {0.0, 0.6, 0}, + ["Other"] = {1, 0, 0} +} +local selfBorderedBuffs = util.ToSet({ + "Renew", + "Greater Heal", + "Rejuvenation", + "Regrowth", + "Blessing of Sacrifice", "Hand of Sacrifice" +}) +function PTUnitFrame:CreateAura(component, aura, xOffset, yOffset, type, size) + local stacks = aura.stacks + local name = aura.name + local frame = component.frame + frame:SetWidth(size) + frame:SetHeight(size) + frame:SetPoint(type == "Buff" and "TOPLEFT" or "TOPRIGHT", xOffset, yOffset) + frame:Show() + frame.unitFrame = self + frame.aura = component + frame.auraData = aura + frame.auraIndex = aura.index + frame.auraType = type + + local icon = component.icon + icon:SetAllPoints(frame) + icon:SetTexture(aura.texture) + + local button = component.button + button:SetAllPoints(frame) + + if util.IsSuperWowPresent() then + local overlay = component.overlay + overlay:SetAllPoints() + + local duration = component.duration + duration:SetAllPoints() + duration:SetScale(size * 0.0275) + end + + if stacks > 1 then + local stackText = component.stackText + stackText:SetPoint("CENTER", frame, "CENTER", 0, 0) + stackText:SetFont("Fonts\\FRIZQT__.TTF", math.ceil(size * (stacks < 10 and 0.75 or 0.6))) + stackText:SetText(stacks) + end + + if type == "Buff" then + if selfBorderedBuffs[aura.name] and aura.time and UnitIsUnit(aura.time.owner, "player") then + local border = component.border + border:Show() + border:SetVertexColor(1, 1, 0) + else + component.border:Hide() + end + else + local border = component.border + border:Show() + local color = debuffTypeBorderColors[aura.type] or debuffTypeBorderColors["Other"] + if aura.time and UnitIsUnit(aura.time.owner, "player") and self:IsEnemy() then + border:SetVertexColor(1, 1, 0) + else + border:SetVertexColor(color[1], color[2], color[3]) + end + end + + if aura.time then + local startTime = aura.time.startTime + local endTime = aura.time.endTime + local duration = aura.time.duration + if endTime - GetTime() > -4 then -- Don't display duration if the predicted time has lapsed + local durationUI = component.duration + + CooldownFrame_SetTimer(durationUI, startTime, duration, 1) + + if duration < 60 then + durationUI.displayAt = PTOptions.ShowAuraTimesAt.Short + elseif duration <= 60 * 2 then + durationUI.displayAt = PTOptions.ShowAuraTimesAt.Medium + else + durationUI.displayAt = PTOptions.ShowAuraTimesAt.Long + end + + -- To prevent having a frame where the duration is not updated + component.durationText:SetSeconds(nil) + util.CallWithThis(durationUI, durationUI:GetScript("OnUpdateModel")) + end + end +end + +function PTUnitFrame:Initialize() + local unit = self.unit + + local profile = self:GetProfile() + + -- Container Elements + + local rootContainer = CreateFrame("Frame", "PTUnitFrame_"..unit, UIParent) + self.rootContainer = rootContainer + rootContainer:SetPoint("CENTER", 0, 0) + + local container = CreateFrame("Frame", "$parentContainer", rootContainer) + self.container = container + container:SetAllPoints(rootContainer) + + local overlayContainer = CreateFrame("Frame", "$parentOverlayContainer", rootContainer) + self.overlayContainer = overlayContainer + overlayContainer:SetFrameLevel(container:GetFrameLevel() + 5) + overlayContainer:SetAllPoints(rootContainer) + + -- Distance Text + + local distanceText = overlayContainer:CreateFontString(nil, "OVERLAY", "GameFontNormal") + self.distanceText = distanceText + distanceText:SetAlpha(profile.RangeText:GetAlpha()) + + local losFrame = CreateFrame("Frame", nil, container) + losFrame:SetFrameLevel(container:GetFrameLevel() + 3) + local losIcon = losFrame:CreateTexture(nil, "OVERLAY") + self.lineOfSightIcon = {frame = losFrame, icon = losIcon} + losIcon:SetTexture("Interface\\Icons\\Spell_nature_sleep") + losIcon:SetAlpha(profile.LineOfSightIcon:GetAlpha()) + losFrame:Hide() + + -- Role Icon + + local roleFrame = CreateFrame("Frame", nil, container) + roleFrame:SetFrameLevel(container:GetFrameLevel() + 3) + local roleIcon = roleFrame:CreateTexture(nil, "OVERLAY") + self.roleIcon = {frame = roleFrame, icon = roleIcon} + roleIcon:SetAlpha(profile.RoleIcon:GetAlpha()) + roleFrame:Hide() + + -- Raid Mark Icon + + local raidMarkFrame = CreateFrame("Frame", nil, container) + raidMarkFrame:SetFrameLevel(container:GetFrameLevel() + 3) + local raidMarkIcon = raidMarkFrame:CreateTexture(nil, "OVERLAY") + self.raidMarkIcon = {frame = raidMarkFrame, icon = raidMarkIcon} + raidMarkIcon:SetAlpha(profile.RaidMarkIcon:GetAlpha()) + raidMarkIcon:SetTexture("Interface\\TARGETINGFRAME\\UI-RaidTargetingIcons") + raidMarkFrame:Hide() + + -- PVP Icon + + local pvpFrame = CreateFrame("Frame", nil, container) + pvpFrame:SetFrameLevel(container:GetFrameLevel() + 4) + local pvpIcon = pvpFrame:CreateTexture(nil, "OVERLAY") + self.pvpIcon = {frame = pvpFrame, icon = pvpIcon} + pvpIcon:SetAlpha(profile.PVPIcon:GetAlpha()) + pvpIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Alliance") + pvpIcon:SetTexCoord(3 / 64, 39 / 64, 2 / 64, 38 / 64) + pvpFrame:Hide() + + -- Health Bar Element + + local healthBar = CreateFrame("StatusBar", "$parentHealthBar", container) + self.healthBar = healthBar + healthBar:SetStatusBarTexture(PT.BarStyles[profile.HealthBarStyle]) + healthBar:SetMinMaxValues(0, 1) + + local incomingHealthBar = CreateFrame("StatusBar", "$parentIncomingHealthBar", container) + self.incomingHealthBar = incomingHealthBar + incomingHealthBar:SetStatusBarTexture(PT.BarStyles[profile.HealthBarStyle]) + incomingHealthBar:SetMinMaxValues(0, 1) + incomingHealthBar:SetFrameLevel(healthBar:GetFrameLevel() - 1) + + local incomingDirectHealthBar = CreateFrame("StatusBar", "$parentIncomingDirectHealthBar", container) + self.incomingDirectHealthBar = incomingDirectHealthBar + incomingDirectHealthBar:SetStatusBarTexture(PT.BarStyles[profile.HealthBarStyle]) + incomingDirectHealthBar:SetMinMaxValues(0, 1) + incomingDirectHealthBar:SetFrameLevel(healthBar:GetFrameLevel() - 1) + + -- Name Element + + local name = healthBar:CreateFontString(nil, "ARTWORK", "GameFontNormal") + self.nameText = name + name:SetAlpha(profile.NameText:GetAlpha()) + + local bg = container:CreateTexture(nil, "BACKGROUND") + healthBar.background = bg + bg:SetAllPoints(healthBar) + bg:SetTexture(0.5, 0.5, 0.5, 0.25) + + -- Incoming Text + local incomingHealText = overlayContainer:CreateFontString(nil, "ARTWORK", "GameFontNormal") + self.incomingHealText = incomingHealText + + -- Missing Health Text + + local missingHealthText = healthBar:CreateFontString(nil, "ARTWORK", "GameFontNormal") + self.missingHealthText = missingHealthText + missingHealthText:SetAlpha(profile.HealthTexts.Missing:GetAlpha()) + + -- Power Bar Element + + local powerBar = CreateFrame("StatusBar", "$parentPowerStatusBar", container) + self.powerBar = powerBar + powerBar:SetStatusBarTexture(PT.BarStyles[profile.PowerBarStyle]) + powerBar:SetMinMaxValues(0, 1) + powerBar:SetValue(1) + powerBar:SetStatusBarColor(0, 0, 1) + powerBar:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background"}) + local modernBackground = powerBar:CreateTexture(nil, "BACKGROUND") + modernBackground:SetAllPoints(powerBar) + modernBackground:SetTexture(0, 0, 0, 1) + modernBackground:Hide() + powerBar.modernBackground = modernBackground + local powerText = powerBar:CreateFontString(nil, "ARTWORK", "GameFontNormal") + self.powerText = powerText + powerText:SetAlpha(profile.PowerText:GetAlpha()) + + -- Button Element + + local button = CreateFrame("Button", "$parentButton", healthBar, "UIPanelButtonTemplate") + self.button = button + local healthText = button:GetFontString() + self.healthText = healthText + healthText:ClearAllPoints() + + + healthText = healthBar:CreateFontString(nil, "ARTWORK", "GameFontNormal") + self.healthText = healthText + + self:RegisterClicks() + button:SetScript("OnClick", function() + local buttonType = arg1 + -- Clicks that move the frame group never cast + if self.suppressClick then + self.suppressClick = nil + return + end + if self:IsFrameDragClick(buttonType) then + return + end + PT.UnitFrame_OnClick(buttonType, unit, self) + end) + button:SetScript("OnMouseDown", function() + local buttonType = arg1 + self.suppressClick = nil + if self:IsFrameDragClick(buttonType) then + -- Hand the drag to the group container, which handles locking, moving all frames and saving positions + self.frameDragging = true + if PTOptions.CastWhen == "Mouse Up" then + self.suppressClick = true -- The click comes after the release, even if the keys were let go first + end + util.CallWithThis(self.owningGroup:GetContainer(), self.owningGroup:GetContainer():GetScript("OnMouseDown")) + return + end + PT.CurrentlyHeldButton = buttonType + PT.ReapplySpellsTooltip() + self.pressed = true + self:AdjustHealthPosition() + end) + button:SetScript("OnMouseUp", function() + if self.frameDragging then + self.frameDragging = nil + util.CallWithThis(self.owningGroup:GetContainer(), self.owningGroup:GetContainer():GetScript("OnMouseUp")) + return + end + PT.CurrentlyHeldButton = nil + PT.ReapplySpellsTooltip() + self.pressed = false + self:AdjustHealthPosition() + end) + button:SetScript("OnEnter", function() + local attachTooltipTo + if PTOptions.SpellsTooltip.AttachTo == "Frame" then + attachTooltipTo = self.rootContainer + elseif PTOptions.SpellsTooltip.AttachTo == "Group" then + attachTooltipTo = self.owningGroup:GetContainer() + elseif PTOptions.SpellsTooltip.AttachTo == "Screen" then + attachTooltipTo = UIParent + else + attachTooltipTo = self.button + end + PT.ApplySpellsTooltip(attachTooltipTo, unit, self.button) + self.hovered = true + self:UpdateHealth() + if PTOptions.SetMouseover and util.IsSuperWowPresent() then + SetMouseoverUnit(self:GetResolvedUnit()) + end + PT.Mouseover = self:GetUnit() + PT.MouseoverFrame = self + PT.ApplyOverrideBindings() + end) + button:SetScript("OnLeave", function() + PT.HideSpellsTooltip() + self.hovered = false + self:UpdateHealth() + if PTOptions.SetMouseover and util.IsSuperWowPresent() then + SetMouseoverUnit(nil) + end + PT.Mouseover = nil + PT.MouseoverFrame = nil + if PT.CurrentlyHeldButton and not util.GetAllButtonsSet()[PT.CurrentlyHeldButton] then + self.pressed = false + self:AdjustHealthPosition() + PT.CurrentlyHeldButton = nil + PT.ReapplySpellsTooltip() + end + PT.RemoveOverrideBindings() + end) + button:EnableMouse(true) + + button:SetNormalTexture(nil) + button:SetHighlightTexture(nil) + button:SetPushedTexture(nil) + + -- Buff Panel Element + + local buffPanel = CreateFrame("Frame", "$parentBuffPanel", container) + self.auraPanel = buffPanel + buffPanel:SetFrameLevel(container:GetFrameLevel() + 2) + + local targetOutline = CreateFrame("Frame", "$parentTargetOutline", rootContainer) + self.targetOutline = targetOutline + targetOutline:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = profile.TargetOutline.Thickness}) + targetOutline:SetFrameLevel(container:GetFrameLevel() + 10) + targetOutline:Hide() + + local flashFrame = CreateFrame("Frame", "$parentFlash", container) + flashFrame:SetFrameLevel(container:GetFrameLevel() + 9) + flashFrame.unitFrame = self + local flashTexture = flashFrame:CreateTexture(nil, "OVERLAY") + self.flashTexture = {frame = flashFrame, texture = flashTexture} + flashTexture:SetTexture(1, 1, 1) + flashFrame:Hide() + + self:SetHealthBarValue(0) + self:SizeElements() +end + +function PTUnitFrame:SizeElements() + local profile = self:GetProfile() + local width = profile.Width + local healthBarHeight = profile.HealthBarHeight + local powerBarHeight = profile.PowerBarHeight + + local rootContainer = self.rootContainer + rootContainer:SetWidth(width) + + local overlayContainer = self.overlayContainer + overlayContainer:SetWidth(width) + + local container = self.container + container:SetWidth(width) + + local healthBar = self.healthBar + healthBar:SetWidth(width) + healthBar:SetHeight(healthBarHeight) + healthBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -profile.PaddingTop) + + local incomingHealthBar = self.incomingHealthBar + incomingHealthBar:SetWidth(width) + incomingHealthBar:SetHeight(healthBarHeight) + incomingHealthBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -profile.PaddingTop) + + local directIncomingHealthBar = self.incomingDirectHealthBar + directIncomingHealthBar:SetWidth(width) + directIncomingHealthBar:SetHeight(healthBarHeight) + directIncomingHealthBar:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -profile.PaddingTop) + + local powerBar = self.powerBar + powerBar:SetWidth(width) + powerBar:SetHeight(powerBarHeight) + powerBar:SetPoint("TOPLEFT", healthBar, "BOTTOMLEFT", 0, 0) + + local button = self.button + button:SetWidth(healthBar:GetWidth()) + button:SetHeight(healthBar:GetHeight() + powerBar:GetHeight()) + button:SetPoint("TOP", 0, 0) + + local name = self.nameText + self:UpdateComponent(name, profile.NameText) + + self:AdjustHealthPosition() + + local powerText = self.powerText + self:UpdateComponent(powerText, profile.PowerText) + + local incomingHealText = self.incomingHealText + self:UpdateComponent(incomingHealText, profile.IncomingHealText) + + local distanceText = self.distanceText + self:UpdateComponent(distanceText, profile.RangeText) + + local losFrame = self.lineOfSightIcon.frame + self:UpdateComponent(self.lineOfSightIcon.frame, profile.LineOfSightIcon) + + local losIcon = self.lineOfSightIcon.icon + losIcon:SetAllPoints(losFrame) + + local roleFrame = self.roleIcon.frame + self:UpdateComponent(roleFrame, profile.RoleIcon) + + local roleIcon = self.roleIcon.icon + roleIcon:SetAllPoints(roleFrame) + + local raidMarkFrame = self.raidMarkIcon.frame + self:UpdateComponent(raidMarkFrame, profile.RaidMarkIcon) + + local raidMarkIcon = self.raidMarkIcon.icon + raidMarkIcon:SetAllPoints(raidMarkFrame) + + local pvpFrame = self.pvpIcon.frame + self:UpdateComponent(pvpFrame, profile.PVPIcon) + + local pvpIcon = self.pvpIcon.icon + pvpIcon:SetAllPoints(pvpFrame) + + local auraPanel = self.auraPanel + self:UpdateComponent(auraPanel, profile.AuraTracker) + + self:UpdateComponent(self.targetOutline, profile.TargetOutline) + + self:UpdateComponent(self.flashTexture.frame, profile.Flash) + self.flashTexture.texture:SetAllPoints(self.flashTexture.frame) + + rootContainer:SetHeight(self:GetHeight()) + overlayContainer:SetHeight(self:GetHeight()) + container:SetHeight(self:GetHeight()) +end + +function PTUnitFrame:AdjustHealthPosition() + local profile = self:GetProfile() + + local healthTexts = profile.HealthTexts + local healthTextProps = (self:ShouldShowMissingHealth() and not profile.MissingHealthInline) and + healthTexts.WithMissing or healthTexts.Normal + local missingHealthTextProps = healthTexts.Missing + + local xOffset, yOffset + if self.pressed then + xOffset, yOffset = 1, -1 + end + self:UpdateComponent(self.healthText, healthTextProps, xOffset, yOffset) + self:UpdateComponent(self.missingHealthText, missingHealthTextProps, xOffset, yOffset) +end + +local alignmentAnchorMap = { + ["LEFT"] = { + ["TOP"] = "TOPLEFT", + ["CENTER"] = "LEFT", + ["BOTTOM"] = "BOTTOMLEFT", + }, + ["CENTER"] = { + ["TOP"] = "TOP", + ["CENTER"] = "CENTER", + ["BOTTOM"] = "BOTTOM", + }, + ["RIGHT"] = { + ["TOP"] = "TOPRIGHT", + ["CENTER"] = "RIGHT", + ["BOTTOM"] = "BOTTOMRIGHT", + } +} +function PTUnitFrame:UpdateComponent(component, props, xOffset, yOffset) + xOffset = xOffset or 0 + yOffset = yOffset or 0 + + local anchor = props:GetAnchorComponent(self) + + component:ClearAllPoints() + if component.SetFont then -- Must be a FontString + component:SetWidth(math.min(props:GetMaxWidth(), anchor:GetWidth())) + component:SetHeight(props.FontSize * 1.25) + component:SetFont("Fonts\\FRIZQT__.TTF", props.FontSize, props.Outline and "OUTLINE" or nil) + if props.Outline then + component:SetShadowOffset(0, 0) + end + component:SetJustifyH(props.AlignmentH) + component:SetJustifyV(props.AlignmentV) + else + component:SetWidth(props:GetWidth(self)) + component:SetHeight(props:GetHeight(self)) + end + local alignment = alignmentAnchorMap[props.AlignmentH][props.AlignmentV] + component:SetPoint(alignment, anchor, alignment, props:GetOffsetX() + xOffset, props:GetOffsetY() + yOffset) +end + +function PTUnitFrame:GetCache() + return PTUnit.Get(self.unit) +end + +function PTUnitFrame:GetAfflictedDebuffTypes() + return self:GetCache().AfflictedDebuffTypes +end + +function PTUnitFrame:GetTrackedDebuffTypes() + return self:GetCache().TrackedDebuffTypes +end + +function PTUnitFrame:GetWidth() + return self:GetProfile().Width +end + +function PTUnitFrame:GetHeight() + return self:GetProfile():GetHeight() +end + +function PTUnitFrame:GetName() + if self:IsFake() then + return self.fakeStats.name + end + return UnitName(self.unit) +end + +function PTUnitFrame:GetClass() + if self:IsFake() then + return self.fakeStats.class + end + return util.GetClass(self.unit) +end + +-- Key: Pet unit | Value: The unit that owns it. Built once so looking up an owner creates no strings. +local petOwners = {["pet"] = "player"} +for i = 1, 4 do + petOwners["partypet"..i] = "party"..i +end +for i = 1, 40 do + petOwners["raidpet"..i] = "raid"..i +end + +-- The class used for "Class" colors. With the PetOwnerClassColors option, pets take their owner's class +-- (a hunter's pet is colored like a hunter), falling back to the pet's own class if the owner is unknown. +function PTUnitFrame:GetColorClass() + if self:IsFake() then + return self.fakeStats.class + end + local unit = self.unit + local owner = petOwners[unit] + if owner and PTOptions.PetOwnerClassColors then + local ownerClass = util.GetClass(owner) + if ownerClass then + return ownerClass + end + end + return util.GetClass(unit) +end + +function PTUnitFrame:IsPlayer() + return UnitIsPlayer(self.unit) +end + +function PTUnitFrame:IsEnemy() + return UnitCanAttack("player", self.unit) +end + +function PTUnitFrame:IsFake() + if not Puppeteer.TestUI or UnitExists(self.unit) then + return false + end + -- The unit's trailing number is extracted once when the frame is created + local num = self.unitNumber + if num and num > Puppeteer.TestUICount then + return false + end + return true +end + +function PTUnitFrame:GetRole() + return Puppeteer.GetUnitAssignedRole(self:GetUnit()) +end + +function PTUnitFrame:HasAggro() + local unit = self:GetUnit() + if self.isCustomUnit then + if not self.guidUnit then + return false + end + unit = PTUnitProxy.ResolveCustomUnit(self.guidUnit) + if not unit then + return false + end + end + return Puppeteer.Banzai:GetUnitAggroByUnitId(unit) +end + +local roleTexturesPath = PTUtil.GetAssetsPath().."textures\\roles\\" +local roleTextures = { + ["Tank"] = roleTexturesPath.."Tank", + ["Healer"] = roleTexturesPath.."Healer", + ["Damage"] = roleTexturesPath.."Damage" +} +function PTUnitFrame:UpdateRole() + local role = self:GetRole() + self.roleIcon.icon:SetTexture(roleTextures[role]) + if role then + self.roleIcon.frame:Show() + else + self.roleIcon.frame:Hide() + end +end + +-- With the group's title hidden there is nothing to grab, so Shift+Ctrl+Alt + left-drag on a unit frame moves the +-- group instead (unless the group is locked) +function PTUnitFrame:IsFrameDragClick(buttonType) + if buttonType ~= "LeftButton" or not (IsShiftKeyDown() and IsControlKeyDown() and IsAltKeyDown()) then + return false + end + local groupName = self.owningGroup.name + return PuppeteerSettings.IsTitleHidden(groupName) and not PuppeteerSettings.IsFrameLocked(groupName) +end + +function PTUnitFrame:GetProfile() + return self.owningGroup:GetProfile() +end + + +-- Out of Range Arrow +local deadArrowDistance = 30 +local arrow = PTGuiLib.Get("puppeteer_arrow") +arrow:SetGradientColors({{1, 0, 0}, {1, 1, 0}, {0, 1, 0}}) +arrow:SetSize(28, 21) +arrow:Hide() +local function PTArrowUpdater_OnUpdate() + local unit = Puppeteer.Mouseover + if not UnitExists(unit) then + arrow:Hide() + return + end + local enemy = UnitCanAttack(unit, "player") + local unitFrame = Puppeteer.MouseoverFrame + local outOfRange = PTUnit.Get(unit):GetDistance() > + (enemy and -1 or (UnitIsDead(unit) and deadArrowDistance or (unitFrame:GetOutOfRangeThreshold() - 1))) + if outOfRange and util.GetUnitPosition(unitFrame:GetResolvedUnit()) then + local overlay = unitFrame.overlayContainer + if arrow:GetParent() ~= overlay then + arrow:SetParent(overlay) + arrow:SetPoint("CENTER", unitFrame.button, "CENTER") + arrow:SetFrameLevel(overlay:GetFrameLevel() + 50) + end + + if enemy then + local facingAngle = util.GetFacingAngle(unit) + if not (PTUnit.Get(unit):GetDistance() > unitFrame:GetOutOfRangeThreshold() - 1) and + (facingAngle < (math.pi * 0.5) or facingAngle > (math.pi * 1.5)) then + arrow:Hide() + return + end + end + arrow:Show() + local cameraAngle = util.GetCameraFacingAngle(unit) + arrow:SetDirection(cameraAngle) + else + arrow:Hide() + end +end +local updater = CreateFrame("Frame", "PTArrowUpdater") + +function Puppeteer.SetOutOfRangeArrowEnabled(enabled) + updater:SetScript("OnUpdate", (enabled and util.EnabledModFeatures["Friendly Position"]) and PTArrowUpdater_OnUpdate or nil) + arrow:Hide() +end \ No newline at end of file diff --git a/PTUnitFrameGroup.lua b/PTUnitFrameGroup.lua new file mode 100644 index 0000000..47a34b6 --- /dev/null +++ b/PTUnitFrameGroup.lua @@ -0,0 +1,803 @@ +PTUnitFrameGroup = {} +PTUtil.SetEnvironment(PTUnitFrameGroup) +PTUnitFrameGroup.__index = PTUnitFrameGroup +local _G = getfenv(0) +local PT = Puppeteer +local util = PTUtil + +-- Height of the title bar above each frame group ("Party", "Raid", ...). The original was 20. +HEADER_HEIGHT = 16 + +PTUnitFrameGroup.name = "???" +PTUnitFrameGroup.raidmana = "??" +PTUnitFrameGroup.healermana = "??" + +PTUnitFrameGroup.profile = nil + +PTUnitFrameGroup.container = nil +PTUnitFrameGroup.borderFrame = nil +PTUnitFrameGroup.header = nil +PTUnitFrameGroup.label = nil +PTUnitFrameGroup.manalabel = nil +PTUnitFrameGroup.manaTooltipFrame = nil +PTUnitFrameGroup.healermanalabel = nil +PTUnitFrameGroup.healerManaTooltipFrame = nil +PTUnitFrameGroup.reportManaButton = nil +PTUnitFrameGroup.uis = nil +PTUnitFrameGroup.units = nil + +PTUnitFrameGroup.petGroup = false +PTUnitFrameGroup.environment = "all" -- party, raid, or all +PTUnitFrameGroup.sortByRole = true + +PTUnitFrameGroup.moveContainer = CreateFrame("Frame", "PTUnitFrameGroupBulkMoveContainer", UIParent) +PTUnitFrameGroup.moveContainer:EnableMouse(true) +PTUnitFrameGroup.moveContainer:SetMovable(true) + +ContextMenu = PTGuiLib.Get("dropdown", UIParent) +ContextMenu:SetDynamicOptions(function(addOption, level, args) + addOption("text", "Lock Position", + "func", function(self, gui) + PuppeteerSettings.SetFrameLocked(gui.FrameGroup.name, not self.checked) + end, + "initFunc", function(self, gui) + self.checked = PuppeteerSettings.IsFrameLocked(gui.FrameGroup.name) + end) + + addOption("notCheckable", true, + "disabled", true) + + addOption("text", "Open Settings", + "notCheckable", true, + "func", function(self, gui) + PTSettingsGui.TabFrame:Show() + end) +end) + +function PTUnitFrameGroup:New(name, environment, units, petGroup, profile, sortByRole) + local obj = setmetatable({name = name, environment = environment, uis = {}, units = units, petGroup = petGroup, + profile = profile, sortByRole = sortByRole, + -- Scratch tables reused every time the frames are sorted and positioned + sortScratch = {groups = {}, sorted = {}, idSorted = {}, groupSet = {}, columns = {}}}, self) + obj:Initialize() + return obj +end + +function PTUnitFrameGroup:EvaluateShown() + if self:CanShowInEnvironment(Puppeteer.CurrentlyInRaid and "raid" or "party") and self:ShowCondition() then + self:Show() + self:UpdateUIPositions() + else + self:Hide() + end +end + +function PTUnitFrameGroup:ShowCondition() + if PTOptions.Hidden or PuppeteerSettings.IsFrameHidden(self.name) then + return false + end + + if PTOptions.HideWhileSolo and (GetNumPartyMembers() == 0 and GetNumRaidMembers() == 0) then + return false + end + + for _, ui in pairs(self.uis) do + if ui:IsShown() then + return true + end + end + return false + end + +function PTUnitFrameGroup:AddUI(ui, noUpdate) + self.uis[ui:GetUnit()] = ui + ui:SetOwningGroup(self) + if not noUpdate then + self:UpdateUIPositions() + end +end + +function PTUnitFrameGroup:GetContainer() + return self.container +end + +function PTUnitFrameGroup:ResetFrameLevel() + self.container:SetFrameLevel(0) + self.borderFrame:SetFrameLevel(1) +end + +function PTUnitFrameGroup:GetEnvironment() + return self.environment +end + +function PTUnitFrameGroup:CanShowInEnvironment(environment) + return self.environment == "all" or self.environment == environment +end + +function PTUnitFrameGroup:Show() + self.container:Show() + for _, ui in pairs(self.uis) do + ui:UpdateAll() + end + self:UpdateReportManaButton() + self:UpdateRaidMana() +end + +function PTUnitFrameGroup:UpdateRaidMana() + if self.name ~= "Raid" then + return + end + if not PTOptions.ShowRaidMana then + self.raidmana = "" + self.healermana = "" + self:SetFrameTitle() + self:UpdateReportManaButton() + return + end + local totalManaPct = 0 + local totalManaUnits = 0 + local healerManaPct = 0 + local healerManaUnits = 0 + for _, ui in pairs(self.uis) do + if ui:IsShown() and UnitIsConnected(ui:GetUnit()) then + local powerType, _, _, _, _ = UnitPowerType(ui:GetUnit()) + if powerType == 0 then -- Mana + local mana = ui:GetCurrentPower() + local manaMax = ui:GetMaxPower() + local manaPct = manaMax > 0 and ((mana / manaMax) * 100) or 0 + totalManaPct = manaPct + totalManaPct + totalManaUnits = totalManaUnits + 1 + if ui:GetRole() == "Healer" then + healerManaPct = manaPct + healerManaPct + healerManaUnits = healerManaUnits + 1 + end + end + end + end + if totalManaUnits > 0 then + self.raidmana = string.format("%d%%", math.floor((totalManaPct / totalManaUnits))) + else + self.raidmana = "" + end + if healerManaUnits > 0 then + self.healermana = string.format("%d%%", math.floor((healerManaPct / healerManaUnits))) + else + self.healermana = "" + end + + self:UpdateReportManaButton() + self:SetFrameTitle() +end + +-- Power events fire constantly in a raid, so raid mana recalculation is coalesced to at most every 0.5 seconds +local RAID_MANA_UPDATE_INTERVAL = 0.5 +local raidManaUpdater = CreateFrame("Frame", "PTRaidManaUpdater") +local pendingRaidManaGroup = nil +local nextRaidManaUpdate = 0 +local function RaidManaUpdater_OnUpdate() + local time = GetTime() + if time < nextRaidManaUpdate then + return + end + raidManaUpdater:SetScript("OnUpdate", nil) + nextRaidManaUpdate = time + RAID_MANA_UPDATE_INTERVAL + local group = pendingRaidManaGroup + pendingRaidManaGroup = nil + if group then + group:UpdateRaidMana() + end +end + +function PTUnitFrameGroup:RequestRaidManaUpdate() + if self.name ~= "Raid" or not PTOptions.ShowRaidMana then + return + end + pendingRaidManaGroup = self + if not raidManaUpdater:GetScript("OnUpdate") then + raidManaUpdater:SetScript("OnUpdate", RaidManaUpdater_OnUpdate) + end +end + +function PTUnitFrameGroup:ReportRaidMana() + if not UnitInRaid("player") or not (IsRaidLeader() or IsRaidOfficer()) then + return + end + + if self.raidmana == "" then + DEFAULT_CHAT_FRAME:AddMessage("No mana users in raid.") + return + end + + local msg = "Raid Mana: ".. self.raidmana + if self.healermana ~= "" then + msg = msg .. " || Healer Mana: " .. self.healermana + end + SendChatMessage(msg, "RAID_WARNING", nil, nil); +end + +function PTUnitFrameGroup:Hide() + self.container:Hide() +end + +-- Used while moving frames to avoid the lag while moving over other toplevel frames +function PTUnitFrameGroup:RemoveToplevel() + self.container:SetToplevel(false) + self.container:SetFrameStrata("HIGH") +end + +function PTUnitFrameGroup:ApplyToplevel() + self.container:SetToplevel(true) + self.container:SetFrameStrata("MEDIUM") +end + +function PTUnitFrameGroup:UpdateHeaderColor() + local opacity = PuppeteerSettings.IsFrameLocked(self.name) and 0.1 or 0.5 + self.header:SetBackdropColor(0, 0, 0, opacity) +end + +function PTUnitFrameGroup:Initialize() + local container = CreateFrame("Frame", "PTUnitFrameGroupContainer_"..self.name, UIParent) + self.container = container + container:EnableMouse(true) + container:SetMovable(true) + container:SetUserPlaced(false) + self:ApplyToplevel() + container:ClearAllPoints() + local anchor, x, y = PuppeteerSettings.GetFramePosition(self.name) + container:SetPoint(anchor or "TOPLEFT", UIParent, "TOPLEFT", x or 100, y or -100) + container:SetBackdrop({bgFile = "Interface\\Buttons\\WHITE8X8"}) + container:SetBackdropColor(0, 0, 0, self:GetProfile().BackgroundOpacity / 100) + + container:SetScript("OnMouseDown", function() + local button = arg1 + + if button ~= "LeftButton" or container.isMoving then + return + end + + if PuppeteerSettings.IsFrameLocked(self.name) then + return + end + + container.isMoving = true + + if (util.GetKeyModifier() == PTOptions.FrameDrag.AltMoveKey) == PTOptions.FrameDrag.MoveAll then + container:StartMoving() + container:SetUserPlaced(false) -- StartMoving sets this and needs to be reverted + self:RemoveToplevel() + return + end + + container.bulkMovement = true + + local moveContainer = PTUnitFrameGroup.moveContainer + moveContainer:ClearAllPoints() + moveContainer:SetPoint("TOPLEFT", 0, 0) + -- If the container doesn't have a size, it doesn't move + moveContainer:SetWidth(1) + moveContainer:SetHeight(1) + local movingGroups = {} + for _, group in pairs(Puppeteer.UnitFrameGroups) do + if not PuppeteerSettings.IsFrameLocked(group.name) and group:GetContainer():IsVisible() then + group:RemoveToplevel() + local gc = group:GetContainer() + local xOffset = gc:GetLeft() + local yOffset = gc:GetTop() - GetScreenHeight() + gc:ClearAllPoints() + gc:SetPoint("TOPLEFT", moveContainer, "TOPLEFT", xOffset, yOffset) + table.insert(movingGroups, group) + end + end + moveContainer.groups = movingGroups + moveContainer:StartMoving() + end) + + container:SetScript("OnMouseUp", function() + local button = arg1 + + if button == "RightButton" and MouseIsOver(self.header) and self.header:IsVisible() then + ContextMenu.FrameGroup = self + ContextMenu:SetToggleState(false) + ContextMenu:SetToggleState(true, container, container:GetWidth(), container:GetHeight()) + PlaySound("igMainMenuOpen") + return + end + + if (button ~= "LeftButton" or not container.isMoving) then + return + end + + container.isMoving = false + + if not container.bulkMovement then + container:StopMovingOrSizing() + self:ApplyToplevel() + util.ConvertAnchor(container, PuppeteerSettings.GetFramePosition(self.name)) + return + end + + container.bulkMovement = false + + local moveContainer = PTUnitFrameGroup.moveContainer + moveContainer:StopMovingOrSizing() + for _, group in pairs(moveContainer.groups) do + group:ApplyToplevel() + local gc = group:GetContainer() + util.ConvertAnchor(gc, PuppeteerSettings.GetFramePosition(group.name)) + end + -- Prevent container from potentially blocking mouse by setting it back to 0 size + moveContainer:SetWidth(0) + moveContainer:SetHeight(0) + end) + + container:SetScript("OnHide", function() + if not container.isMoving then + return + end + local prevArg = arg1 + arg1 = "LeftButton" + container:GetScript("OnMouseUp")() + arg1 = prevArg + end) + + local header = CreateFrame("Frame", "$parentHeader", container) + self.header = header + header:SetPoint("TOPLEFT", container, 0, 0) + header:SetBackdrop({bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background"}) + self:UpdateHeaderColor() + + local label = header:CreateFontString(header, "OVERLAY", "GameFontNormal") + self.label = label + label:SetPoint("CENTER", header, "CENTER", 0, 0) + + + + local mana = header:CreateFontString(header, "OVERLAY", "GameFontNormal") + self.manalabel = mana + mana:SetTextColor(0, 0.7, 1, 1) + + local manaTooltip = CreateFrame("Frame", nil, header) + self.manaTooltipFrame = manaTooltip + manaTooltip:SetAllPoints(mana) + manaTooltip:EnableMouse(true) + manaTooltip:RegisterForDrag("LeftButton") + manaTooltip:SetScript("OnEnter", function() + GameTooltip:SetOwner(manaTooltip, "ANCHOR_BOTTOM") + GameTooltip:SetText("Raid Mana") + GameTooltip:Show() + end) + manaTooltip:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + manaTooltip:SetScript("OnMouseDown", function() + container:GetScript("OnMouseDown")() + end) + manaTooltip:SetScript("OnMouseUp", function() + container:GetScript("OnMouseUp")() + end) + + local healerMana = header:CreateFontString(header, "OVERLAY", "GameFontNormal") + self.healermanalabel = healerMana + healerMana:SetTextColor(0.2, 1, 0.4, 1) + + local healerManaTooltip = CreateFrame("Frame", nil, header) + self.healerManaTooltipFrame = healerManaTooltip + healerManaTooltip:SetAllPoints(healerMana) + healerManaTooltip:EnableMouse(true) + healerManaTooltip:RegisterForDrag("LeftButton") + healerManaTooltip:SetScript("OnEnter", function() + GameTooltip:SetOwner(healerManaTooltip, "ANCHOR_BOTTOM") + GameTooltip:SetText("Healer Mana") + GameTooltip:Show() + end) + healerManaTooltip:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + healerManaTooltip:SetScript("OnMouseDown", function() + container:GetScript("OnMouseDown")() + end) + healerManaTooltip:SetScript("OnMouseUp", function() + container:GetScript("OnMouseUp")() + end) + + local reportBtn = CreateFrame("Button", "$parentReportMana", header) + self.reportManaButton = reportBtn + reportBtn:SetWidth(HEADER_HEIGHT - 2) + reportBtn:SetHeight(HEADER_HEIGHT - 2) + reportBtn:SetPoint("RIGHT", header, "RIGHT", -2, 0) + reportBtn:SetNormalTexture("Interface\\Buttons\\UI-GuildButton-PublicNote-Up") + reportBtn:SetHighlightTexture("Interface\\Buttons\\UI-GuildButton-PublicNote-Up") + reportBtn:GetHighlightTexture():SetAlpha(0.3) + reportBtn:SetScript("OnClick", function() + self:ReportRaidMana() + end) + reportBtn:SetScript("OnEnter", function() + GameTooltip:SetOwner(reportBtn, "ANCHOR_RIGHT") + GameTooltip:SetText("Report raid & healer mana") + GameTooltip:Show() + end) + reportBtn:SetScript("OnLeave", function() + GameTooltip:Hide() + end) + reportBtn:Hide() + + self:SetFrameTitle() + + local borderFrame = CreateFrame("Frame", "$parentBorder", container) + self.borderFrame = borderFrame + borderFrame:SetPoint("CENTER", container, 0, 0) + + self:ApplyProfile() +end + +function PTUnitFrameGroup:UpdateManaLabelLayout(headerWidth) + if self.name ~= "Raid" then + return + end + local hasButton = self.reportManaButton:IsShown() + local btnSpace = hasButton and 20 or 0 + local totalWidth = headerWidth or self.header:GetWidth() + local availableWidth = totalWidth - btnSpace - 4 + local fontSize = 12 + if self.healermana ~= "" then + -- Two labels sharing space need more aggressive sizing + if availableWidth < 50 then + fontSize = 7 + elseif availableWidth < 90 then + fontSize = 8 + elseif availableWidth < 130 then + fontSize = 9 + elseif availableWidth < 170 then + fontSize = 10 + end + else + if availableWidth < 40 then + fontSize = 7 + elseif availableWidth < 60 then + fontSize = 9 + end + end + self.manalabel:SetFont("Fonts\\FRIZQT__.TTF", fontSize) + self.healermanalabel:SetFont("Fonts\\FRIZQT__.TTF", fontSize) + + self.manalabel:ClearAllPoints() + self.healermanalabel:ClearAllPoints() + local healerVisible = self.healermanalabel:GetText() and self.healermanalabel:GetText() ~= "" + if healerVisible then + local halfWidth = (availableWidth - 4) / 2 + self.manalabel:SetWidth(halfWidth) + self.healermanalabel:SetWidth(halfWidth) + self.manalabel:SetPoint("LEFT", self.header, "LEFT", 2, 0) + self.healermanalabel:SetPoint("RIGHT", self.header, "RIGHT", -btnSpace - 2, 0) + else + self.manalabel:SetWidth(availableWidth) + self.manalabel:SetPoint("CENTER", self.header, "CENTER", -btnSpace / 2, 0) + end +end + +function PTUnitFrameGroup:UpdateReportManaButton() + if self.name ~= "Raid" or not self.reportManaButton then + return + end + if UnitInRaid("player") and (IsRaidLeader() or IsRaidOfficer()) and PTOptions.ShowRaidMana then + self.reportManaButton:Show() + else + self.reportManaButton:Hide() + end +end + +function PTUnitFrameGroup:UpdateManaLabels() + if self.name ~= "Raid" or not PTOptions.ShowRaidMana then + return + end + local hasButton = self.reportManaButton and self.reportManaButton:IsShown() + local btnSpace = hasButton and 20 or 0 + local availableWidth = self.header:GetWidth() - btnSpace - 4 + if self.healermana ~= "" then + if availableWidth >= 280 then + self.manalabel:SetText("Raid: "..self.raidmana) + self.healermanalabel:SetText("Heal: "..self.healermana) + elseif availableWidth >= 130 then + self.manalabel:SetText("R:"..self.raidmana) + self.healermanalabel:SetText("H:"..self.healermana) + elseif availableWidth >= 80 then + self.manalabel:SetText(self.raidmana) + self.healermanalabel:SetText(self.healermana) + elseif availableWidth >= 50 then + local r = string.gsub(self.raidmana, "%%", "") + local h = string.gsub(self.healermana, "%%", "") + self.manalabel:SetText(r) + self.healermanalabel:SetText(h) + else + local r = string.gsub(self.raidmana, "%%", "") + self.manalabel:SetText(r) + self.healermanalabel:SetText("") + end + else + if availableWidth >= 200 then + self.manalabel:SetText("Raid: "..self.raidmana) + elseif availableWidth >= 40 then + self.manalabel:SetText(self.raidmana) + else + self.manalabel:SetText("") + end + self.healermanalabel:SetText("") + end + -- Show tooltips only when labels lack descriptive prefixes + local needsTooltip = availableWidth < 130 + self.manaTooltipFrame:EnableMouse(needsTooltip) + self.healerManaTooltipFrame:EnableMouse(needsTooltip) + self:UpdateManaLabelLayout() +end + +function PTUnitFrameGroup:SetFrameTitle() + if(self.name == "Raid") then + if PTOptions.ShowRaidMana then + self.label:SetText("") + self:UpdateManaLabels() + else + self.label:SetText("Raid") + self.manalabel:SetText("") + self.healermanalabel:SetText("") + end + else + self.label:SetText(self.name) + self.manalabel:SetText("") + self.healermanalabel:SetText("") + end +end + +function PTUnitFrameGroup:ApplyProfile() + local profile = self:GetProfile() + + local borderFrame = self.borderFrame + if profile.BorderStyle == "Tooltip" then + borderFrame:SetBackdrop({edgeFile="Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 17, + tile = true, tileSize = 17}) + elseif profile.BorderStyle == "Dialog Box" then + borderFrame:SetBackdrop({edgeFile="Interface\\DialogFrame\\UI-DialogBox-Border", edgeSize = 24, + tile = true, tileSize = 24}) + else + borderFrame:SetBackdrop({}) + end +end + +local getn, setn = table.getn, table.setn +local function clearArray(array) + for i = getn(array), 1, -1 do + array[i] = nil + end + setn(array, 0) +end + +local function getScratchArray(arrays, index) + local array = arrays[index] + if array then + clearArray(array) + else + array = {} + arrays[index] = array + end + return array +end + +function PTUnitFrameGroup:UpdateUIPositions() + local profile = self:GetProfile() + local profileWidth = profile.Width + local profileHeight = profile:GetHeight() + local maxUnitsInAxis = profile.MaxUnitsInAxis + local orientation = profile.Orientation + local headerEnabled = not PuppeteerSettings.IsTitleHidden(self.name) + local headerHeight = headerEnabled and HEADER_HEIGHT or 0 + + local sortedUIs, numSorted = self:GetSortedUIs() + -- Split the sorted groups into columns of at most maxUnitsInAxis units + local columns = self.sortScratch.columns + local numColumns = 0 + for groupIndex = 1, numSorted do + local column + local unitsUntilShift = maxUnitsInAxis + for _, ui in ipairs(sortedUIs[groupIndex]) do + if not column or unitsUntilShift == 0 then + numColumns = numColumns + 1 + column = getScratchArray(columns, numColumns) + unitsUntilShift = maxUnitsInAxis + end + table.insert(column, ui) + unitsUntilShift = unitsUntilShift - 1 + end + end + + -- IMPORTANT: "Column" does not necessarily mean vertical! + local largestColumn = orientation == "Vertical" and profile.MinUnitsY or profile.MinUnitsX + for columnIndex = 1, numColumns do + largestColumn = math.max(largestColumn, getn(columns[columnIndex])) + end + + local xSpacing = profile.HorizontalSpacing + local ySpacing = profile.VerticalSpacing + for columnIndex = 1, numColumns do + for i, ui in ipairs(columns[columnIndex]) do -- Column is guaranteed to be less than max units + local container = ui:GetRootContainer() + local x = orientation == "Vertical" and ((profileWidth + xSpacing) * (columnIndex - 1)) or ((profileWidth + xSpacing) * (i - 1)) + local y = orientation == "Vertical" and ((profileHeight + ySpacing) * (i - 1)) or ((profileHeight + ySpacing) * (columnIndex - 1)) + container:SetPoint("TOPLEFT", self.container, "TOPLEFT", x, -y - headerHeight) + end + end + + local largestRow = math.max(numColumns, orientation == "Vertical" and profile.MinUnitsX or profile.MinUnitsY) + + + --largestRow = math.max(largestRow, 1) + --largestColumn = math.max(largestColumn, 1) + + local width = orientation == "Vertical" and (profileWidth * largestRow + (xSpacing * (largestRow - 1))) or (profileWidth * largestColumn + (xSpacing * (largestColumn - 1))) + width = math.max(width, profileWidth) -- Prevent width from being 0 + local height = orientation == "Vertical" and (profileHeight * largestColumn + (ySpacing * (largestColumn - 1))) or (profileHeight * largestRow + (ySpacing * (largestRow - 1))) + height = height + headerHeight + self.container:SetWidth(width) + self.container:SetHeight(height) + + local header = self.header + if headerEnabled then + header:Show() + header:SetWidth(width) + header:SetHeight(headerHeight) + else + header:Hide() + end + + local borderPadding = 0 + if profile.BorderStyle == "Tooltip" then + borderPadding = 10 + elseif profile.BorderStyle == "Dialog Box" then + borderPadding = 18 + end + self.borderFrame:SetWidth(width + borderPadding) + self.borderFrame:SetHeight(height + borderPadding) + + local label = self.label + label:SetPoint("CENTER", header, "CENTER", 0, 0) + + self:UpdateManaLabels() +end + +-- Comparators are defined once instead of creating new closures on every sort +local function compareByName(a, b) + local aName = UnitName(a:GetUnit()) or a.fakeStats.name or a:GetUnit() + local bName = UnitName(b:GetUnit()) or b.fakeStats.name or b:GetUnit() + return aName < bName +end + +local function compareByClass(a, b) + local aName = util.GetClass(a:GetUnit()) or a.fakeStats.class + local bName = util.GetClass(b:GetUnit()) or b.fakeStats.class + return aName < bName +end + +local rolePriority = { + ["Tank"] = 1, + ["Healer"] = 2, + ["Damage"] = 3 +} +-- Key: Unit frame | Value: Sort rank. Ranks are computed once per frame before sorting, rather than inside the +-- comparator (which previously also needed a copy of the group to look up each frame's original position). +local roleRanks = {} +local function compareByRoleRank(a, b) + return roleRanks[a] < roleRanks[b] +end + +-- Returns an array with the index being the group number, and the value being an array of units, +-- along with the number of groups in the array. The returned tables are reused by the next call. +function PTUnitFrameGroup:GetSortedUIs() + local profile = self:GetProfile() + local uis = self.uis + local scratch = self.sortScratch + local groups = scratch.groups + local numGroups + + if self.environment == "raid" and profile.SplitRaidIntoGroups and not self.petGroup then + numGroups = 8 + for i = 1, 8 do + getScratchArray(groups, i) + end + local raidUnits = PTUtil.RaidUnits + local numRaidMembers = GetNumRaidMembers() + for i = 1, numRaidMembers do + local _, _, subgroup = GetRaidRosterInfo(i) + local group = groups[subgroup] + table.insert(group, uis[raidUnits[i]]) + end + -- If testing, fill empty slots with fake players + if Puppeteer.TestUI then + local maxFake = Puppeteer.TestUICount + for groupNumber = 1, 8 do + local group = groups[groupNumber] + for frameNumber = 1, 5 do + if not group[frameNumber] then + numRaidMembers = numRaidMembers + 1 + if numRaidMembers <= maxFake then + table.insert(group, uis[raidUnits[numRaidMembers]]) + end + end + end + end + end + else + numGroups = 1 + local group = getScratchArray(groups, 1) + for _, ui in pairs(uis) do + if ui:IsShown() then + table.insert(group, ui) + end + end + end + + local sortedGroups = scratch.sorted + clearArray(sortedGroups) + local numSorted = 0 + local sortUnitsBy = profile.SortUnitsBy + if sortUnitsBy == "ID" then + local groupSet = scratch.groupSet + for groupNumber = 1, numGroups do + local group = groups[groupNumber] + if getn(group) > 0 then + if self.environment == "raid" and not self.petGroup then + -- Should already be sorted if we're not dealing with the pets + numSorted = numSorted + 1 + sortedGroups[numSorted] = group + else -- Sort by the order of the unit IDs this UI group can handle + for _, ui in ipairs(group) do -- Convert group UI array to a set with the key as the Unit ID + groupSet[ui:GetUnit()] = ui + end + local sortedGroup = getScratchArray(scratch.idSorted, groupNumber) + for _, unit in ipairs(self.units) do + if groupSet[unit] then + table.insert(sortedGroup, groupSet[unit]) + end + end + for unit in pairs(groupSet) do + groupSet[unit] = nil + end + numSorted = numSorted + 1 + sortedGroups[numSorted] = sortedGroup + end + end + end + elseif sortUnitsBy == "Name" or sortUnitsBy == "Class Name" then + local comparator = sortUnitsBy == "Name" and compareByName or compareByClass + for groupNumber = 1, numGroups do + local group = groups[groupNumber] + if getn(group) > 0 then + table.sort(group, comparator) + numSorted = numSorted + 1 + sortedGroups[numSorted] = group + end + end + end + setn(sortedGroups, numSorted) + + local sortByRole = self.sortByRole + for groupIndex = 1, numSorted do + local group = sortedGroups[groupIndex] + if sortByRole then + for i, ui in ipairs(group) do + local unit = ui:GetUnit() + roleRanks[ui] = ((rolePriority[ui:GetRole()] or 4) * 1000) + i + + (UnitIsUnit("player", unit) and 0 or 100) + (UnitIsUnit("pet", unit) and 0 or 99) + end + table.sort(group, compareByRoleRank) + for _, ui in ipairs(group) do + roleRanks[ui] = nil + end + end + for _, ui in ipairs(group) do + ui:UpdateRole() + end + end + return sortedGroups, numSorted +end + +function PTUnitFrameGroup:GetProfile() + return self.profile +end \ No newline at end of file diff --git a/Profile.lua b/Profile.lua new file mode 100644 index 0000000..3c7b48f --- /dev/null +++ b/Profile.lua @@ -0,0 +1,91 @@ +-- DEPRECATION NOTICE +-- UI Profiles as they currently stand will be retired in a future update for a more modular system + +PTUIProfile = {} +PTUIProfile.__index = PTUIProfile +local util = PTUtil + +local positionedMethods = { + GetWidth = function(self, ui) + return (self.Width ~= "Anchor" and self.Width or self:GetAnchorComponent(ui):GetWidth()) + (self.Width2 or 0) + end, + GetHeight = function(self, ui) + return (self.Height ~= "Anchor" and self.Height or self:GetAnchorComponent(ui):GetHeight()) + (self.Height2 or 0) + end, + GetOffsetX = function(self) + if self.AlignmentH == "LEFT" then + return self.PaddingH + self.OffsetX + elseif self.AlignmentH == "RIGHT" then + return -self.PaddingH + self.OffsetX + end + return self.OffsetX + end, + GetOffsetY = function(self) + if self.AlignmentV == "TOP" then + return -self.PaddingV + self.OffsetY + elseif self.AlignmentV == "BOTTOM" then + return self.PaddingV + self.OffsetY + end + return self.OffsetY + end, + GetAlpha = function(self) + return self.Opacity / 100 + end, + GetAnchorComponent = function(self, ui) + local anchorName = self.Anchor + if anchorName == "Health Bar" then + return ui.healthBar + elseif anchorName == "Power Bar" then + return ui.powerBar + elseif anchorName == "Button" then + return ui.button + elseif anchorName == "Container" then + return ui.container + end + end +} +local textMethods = util.CloneTable(positionedMethods) +textMethods.GetMaxWidth = function(self) + return self.MaxWidth or 1000 +end +local objectMethods = { + Sized = positionedMethods, + Text = textMethods +} + +local function InjectObjectMethods(t) + if t.ObjectType then + for k, f in pairs(objectMethods[t.ObjectType]) do + t[k] = f + end + return + end + for k, v in pairs(t) do + if type(v) == "table" then + InjectObjectMethods(v) + end + end +end + +function PTUIProfile:New(base, diff) + local obj = setmetatable(util.CloneTable(base or PTProfileManager.GetDefaultProfile(), true), self) + if diff then + util.ApplyTableDiffs(obj, diff) + end + InjectObjectMethods(obj) + return obj +end + +function PTUIProfile:Deserialize(serialized) + local diff = loadstring(serialized)() + return PTUIProfile:New(PTProfileManager.GetDefaultProfile(), diff) +end + +function PTUIProfile:Serialize(base) + return "return "..PTUtil.SerializeTable(self, nil, base) +end + +function PTUIProfile:GetHeight() + local totalHeight = self.HealthBarHeight + self.PowerBarHeight + self.PaddingTop + self.PaddingBottom + return totalHeight +end \ No newline at end of file diff --git a/ProfileManager.lua b/ProfileManager.lua new file mode 100644 index 0000000..0064b88 --- /dev/null +++ b/ProfileManager.lua @@ -0,0 +1,780 @@ +-- DEPRECATION NOTICE +-- UI Profiles as they currently stand will be retired in a future update for a more modular system + +PTProfileManager = {} + +PTUtil.SetEnvironment(PTProfileManager) +local _G = getfenv(0) + +local util = PTUtil + +-- The profile name used for fallback +DEFAULT_PROFILE_NAME = "Default" + +-- Profiles that have not had style overrides applied +DefaultProfiles = {} +-- Profiles that have had style overrides applied +Profiles = {} + +DefaultProfileOrder = { + "Default", "Default (Short Bar)", "Small", "Very Small", "Very Small (Horizontal)", "Long", "Long (Small)", + "Long (Integrated)", "Enemy", "Enemy (Small)", "Legacy" +} +DefaultProfileOrder = util.ToSet(DefaultProfileOrder, true) + +-- Tries to get profile with overrides applied, or the default one if none is found +function GetProfile(name) + return Profiles[name or DEFAULT_PROFILE_NAME] or DefaultProfiles[name or DEFAULT_PROFILE_NAME] +end + +function GetDefaultProfile(name) + return DefaultProfiles[name or DEFAULT_PROFILE_NAME] +end + +function GetProfileNames() + local names = util.ToArray(DefaultProfiles) + table.sort(names, function(a, b) + return (DefaultProfileOrder[a] or 1000) < (DefaultProfileOrder[b] or 1000) + end) + return names +end + +function CreateProfile(name, baseName, diff, useDefault) + local profileGetter = (useDefault ~= false) and GetDefaultProfile or GetProfile + DefaultProfiles[name] = PTUIProfile:New(profileGetter(baseName or DEFAULT_PROFILE_NAME), diff) + if useDefault ~= false then + ApplyOverrides(name) + end + return DefaultProfiles[name] +end + +function ApplyOverrides(profileName) + local overrides = PTOptions.StyleOverrides[profileName] + local profile = GetDefaultProfile(profileName) + + if overrides and profile then + profile = PTUIProfile:New(profile, overrides) + Profiles[profileName] = profile + end +end + +function InitializeDefaultProfiles() + -- Preserve saved texture selections from before the display-name change. + for _, overrides in pairs(PTOptions.StyleOverrides) do + if overrides.HealthBarStyle == "Modern Blizzard" then + overrides.HealthBarStyle = "Blizzard Modern" + end + if overrides.PowerBarStyle == "Modern Blizzard" then + overrides.PowerBarStyle = "Blizzard Modern" + end + end + DefaultProfiles["Default"] = PTUIProfile:New({ + ["HorizontalSpacing"] = 1, + ["AlertPercent"] = 100, + ["PVPIcon"] = { + ["PaddingH"] = 0, + ["Opacity"] = 100, + ["OffsetX"] = -6, + ["Anchor"] = "Container", + ["PaddingV"] = 0, + ["AlignmentH"] = "LEFT", + ["Width"] = 14, + ["ObjectType"] = "Sized", + ["Height"] = 14, + ["OffsetY"] = 2, + ["AlignmentV"] = "TOP", + }, + ["Flash"] = { + ["PaddingH"] = 4, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 4, + ["AlignmentH"] = "CENTER", + ["Width"] = "Anchor", + ["ObjectType"] = "Sized", + ["Height"] = "Anchor", + ["OffsetY"] = 0, + ["AlignmentV"] = "CENTER", + }, + ["FlashOpacity"] = 70, + ["OutOfRangeOpacity"] = 50, + ["AuraTracker"] = { + ["PaddingH"] = 0, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 0, + ["AlignmentH"] = "LEFT", + ["Width"] = "Anchor", + ["ObjectType"] = "Sized", + ["Height"] = 14, + ["OffsetY"] = 0, + ["AlignmentV"] = "BOTTOM", + }, + ["AlwaysShowMissingHealth"] = false, + ["PowerText"] = { + ["FontSize"] = 8, + ["PaddingH"] = 4, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Power Bar", + ["PaddingV"] = 4, + ["AlignmentH"] = "CENTER", + ["ObjectType"] = "Text", + ["OffsetY"] = 0, + ["AlignmentV"] = "CENTER", + }, + ["RoleIcon"] = { + ["PaddingH"] = 1, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Container", + ["PaddingV"] = 1, + ["AlignmentH"] = "LEFT", + ["Width"] = 14, + ["ObjectType"] = "Sized", + ["Height"] = 14, + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + ["SortUnitsBy"] = "ID", -- "ID", "Name", "Class Name" + ["IncomingHealText"] = { + ["Outline"] = true, + ["FontSize"] = 9, + ["PaddingH"] = 2, + ["Color"] = { + 0.5, + 1, + 0.5, + }, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 4, + ["AlignmentH"] = "LEFT", + ["IndirectColor"] = { + 0.3, + 0.8, + 0.3, + }, + ["ObjectType"] = "Text", + ["OffsetY"] = 2, + ["AlignmentV"] = "CENTER", + }, + ["NameText"] = { + ["FontSize"] = 11, + ["PaddingH"] = 4, + ["Color"] = "Class", + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 1, + ["AlignmentH"] = "CENTER", + ["MaxWidth"] = 80, + ["ObjectType"] = "Text", + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + ["HealthBarColor"] = "Green To Red", -- "Class", "Green", "Green To Red" + ["Width"] = 100, + ["HealthDisplay"] = "Health", -- "Health", "Health/Max Health", "% Health", "Hidden" + ["ShowDistanceThreshold"] = { + ["Friendly"] = 30, + ["Hostile"] = 30, + }, + ["MissingHealthDisplay"] = "-Health", -- "Hidden", "-Health", "-% Health" + ["IncomingHealDisplay"] = "Overheal", -- "Overheal", "Heal", "Hidden" + ["PowerDisplay"] = "Power", -- "Power", "Power/Max Power", "% Power", "Hidden" + ["ShowEnemyMissingHealth"] = false, + ["RangeText"] = { + ["FontSize"] = 9, + ["PaddingH"] = 4, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 0, + ["AlignmentH"] = "CENTER", + ["ObjectType"] = "Text", + ["OffsetY"] = -7, + ["AlignmentV"] = "CENTER", + }, + ["NameDisplay"] = "Name", -- Unimplemented + ["TargetOutline"] = { + ["PaddingH"] = 4, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Button", + ["PaddingV"] = 4, + ["AlignmentH"] = "CENTER", + ["Height2"] = 2, + ["Width"] = "Anchor", + ["Thickness"] = 2, + ["ObjectType"] = "Sized", + ["Height"] = "Anchor", + ["Width2"] = 2, + ["OffsetY"] = 0, + ["AlignmentV"] = "CENTER", + }, + ["HealthTexts"] = { + ["Normal"] = { + ["FontSize"] = 11, + ["PaddingH"] = 4, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 4, + ["AlignmentH"] = "CENTER", + ["ObjectType"] = "Text", + ["OffsetY"] = 2, + ["AlignmentV"] = "CENTER", + }, + ["Missing"] = { + ["FontSize"] = 11, + ["PaddingH"] = 2, + ["Color"] = { + 1, + 0.4, + 0.4, + }, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 0, + ["AlignmentH"] = "RIGHT", + ["ObjectType"] = "Text", + ["OffsetY"] = 2, + ["AlignmentV"] = "CENTER", + }, + ["WithMissing"] = { + ["FontSize"] = 11, + ["PaddingH"] = 8, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 0, + ["AlignmentH"] = "CENTER", + ["ObjectType"] = "Text", + ["OffsetY"] = 2, + ["AlignmentV"] = "CENTER", + }, + }, + ["OutOfRangeThreshold"] = { + ["Friendly"] = 41, + ["Hostile"] = 41, + }, + ["HealthBarStyle"] = "Puppeteer", -- "Blizzard", "Blizzard Raid", "Puppeteer" + ["Orientation"] = "Vertical", --"Vertical", "Horizontal" + ["NotAlertedOpacity"] = 60, + ["MinUnitsX"] = 0, + ["PaddingBottom"] = 0, + ["TrackedAurasSpacing"] = 1, + ["HealthBarHeight"] = 36, + ["EnemyHealthBarColor"] = "Green", + ["BorderStyle"] = "Tooltip", -- "Tooltip", "Dialog Box", "Borderless" + ["PaddingTop"] = 0, + ["PowerBarHeight"] = 9, + ["SplitRaidIntoGroups"] = true, + ["MaxUnitsInAxis"] = 5, + ["PowerBarStyle"] = "Puppeteer Borderless", + ["FlashThreshold"] = 25, + ["TrackAuras"] = true, + ["TrackedAurasAlignment"] = "BOTTOM", + ["ShowDebuffColorsOn"] = "Health Bar", -- "Health Bar", "Name", "Health", "Hidden" + ["MinUnitsY"] = 0, + ["MissingHealthInline"] = false, + ["BarsOffsetY"] = 0, + ["VerticalSpacing"] = 0, + ["BackgroundOpacity"] = 25, + ["LineOfSightIcon"] = { + ["PaddingH"] = 4, + ["Opacity"] = 80, + ["OffsetX"] = 0, + ["Anchor"] = "Health Bar", + ["PaddingV"] = 4, + ["AlignmentH"] = "CENTER", + ["Width"] = 20, + ["ObjectType"] = "Sized", + ["Height"] = 20, + ["OffsetY"] = 0, + ["AlignmentV"] = "CENTER", + }, + ["RaidMarkIcon"] = { + ["PaddingH"] = 1, + ["Opacity"] = 100, + ["OffsetX"] = 0, + ["Anchor"] = "Container", + ["PaddingV"] = 1, + ["AlignmentH"] = "RIGHT", + ["Width"] = 12, + ["ObjectType"] = "Sized", + ["Height"] = 12, + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + }) + ApplyOverrides("Default") + + CreateProfile("Default (Short Bar)", "Default", { + ["AuraTracker"] = { + ["Height"] = 13, + }, + ["IncomingHealText"] = { + ["PaddingV"] = 2, + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + ["NameText"] = { + ["Anchor"] = "Container", + ["PaddingV"] = 0, + }, + ["RangeText"] = { + ["OffsetY"] = -4, + }, + ["HealthTexts"] = { + ["Missing"] = { + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + ["Normal"] = { + ["OffsetY"] = 0, + ["PaddingV"] = 0, + ["AlignmentV"] = "TOP", + }, + ["WithMissing"] = { + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + }, + ["PaddingTop"] = 12, + ["HealthBarHeight"] = 24, + }) + + CreateProfile("Small", "Default", { + ["AuraTracker"] = { + ["Height"] = 12, + }, + ["PowerText"] = { + ["AlignmentH"] = "RIGHT", + }, + ["RoleIcon"] = { + ["Width"] = 12, + ["Height"] = 12, + }, + ["IncomingHealText"] = { + ["FontSize"] = 7, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = -6, + }, + ["NameText"] = { + ["MaxWidth"] = 47, + }, + ["Width"] = 67, + ["PowerDisplay"] = "Hidden", + ["RangeText"] = { + ["FontSize"] = 8, + ["OffsetY"] = -6, + }, + ["HealthTexts"] = { + ["Normal"] = { + ["FontSize"] = 9, + }, + ["Missing"] = { + ["FontSize"] = 9, + ["PaddingH"] = 4, + }, + ["WithMissing"] = { + ["FontSize"] = 9, + ["PaddingH"] = 4, + ["AlignmentH"] = "LEFT", + }, + }, + ["PowerBarHeight"] = 6, + ["LineOfSightIcon"] = { + ["Opacity"] = 70, + }, + ["RaidMarkIcon"] = { + ["PaddingV"] = 0, + }, + }) + + CreateProfile("Very Small", "Default", { + ["PVPIcon"] = { + ["Width"] = 12, + ["Height"] = 12, + }, + ["AuraTracker"] = { + ["Height"] = 11, + }, + ["PowerText"] = { + ["AlignmentH"] = "RIGHT", + }, + ["RoleIcon"] = { + ["Width"] = 10, + ["Height"] = 10, + }, + ["IncomingHealText"] = { + ["FontSize"] = 7, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = -6, + }, + ["NameText"] = { + ["FontSize"] = 9, + ["OffsetX"] = 6, + ["AlignmentH"] = "LEFT", + ["MaxWidth"] = 34, + }, + ["Width"] = 50, + ["MissingHealthDisplay"] = "Hidden", + ["IncomingHealDisplay"] = "Hidden", + ["PowerDisplay"] = "Hidden", + ["RangeText"] = { + ["FontSize"] = 7, + ["OffsetY"] = -6, + }, + ["HealthTexts"] = { + ["Normal"] = { + ["FontSize"] = 8, + ["OffsetY"] = 1, + }, + ["Missing"] = { + ["FontSize"] = 13, + ["PaddingH"] = 4, + ["OffsetY"] = 0, + ["AlignmentV"] = "BOTTOM", + }, + ["WithMissing"] = { + ["PaddingH"] = 4, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + }, + ["HealthBarHeight"] = 29, + ["PowerBarHeight"] = 5, + ["LineOfSightIcon"] = { + ["Opacity"] = 70, + ["Width"] = 16, + ["Height"] = 16, + }, + ["RaidMarkIcon"] = { + ["PaddingV"] = 0, + ["AlignmentH"] = "CENTER", + ["Width"] = 10, + ["Height"] = 10, + ["OffsetY"] = 4, + }, + }) + + CreateProfile("Very Small (Horizontal)", "Very Small", { + ["Orientation"] = "Horizontal" + }) + + CreateProfile("Long", "Default", { + ["PVPIcon"] = { + ["OffsetY"] = -5, + }, + ["AuraTracker"] = { + ["Anchor"] = "Container", + ["AlignmentH"] = "CENTER", + ["Height"] = 20, + }, + ["PowerText"] = { + ["FontSize"] = 10, + ["AlignmentH"] = "RIGHT", + }, + ["RoleIcon"] = { + ["OffsetX"] = -5, + ["PaddingV"] = 0, + ["OffsetY"] = 5, + }, + ["IncomingHealText"] = { + ["PaddingH"] = 4, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = 0, + ["AlignmentV"] = "BOTTOM", + }, + ["NameText"] = { + ["FontSize"] = 12, + ["PaddingV"] = 4, + ["AlignmentH"] = "LEFT", + ["MaxWidth"] = 105, + ["AlignmentV"] = "CENTER", + }, + ["Width"] = 150, + ["IncomingHealDisplay"] = "Hidden", + ["RangeText"] = { + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + ["HealthTexts"] = { + ["Normal"] = { + ["FontSize"] = 12, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = 0, + }, + ["Missing"] = { + ["FontSize"] = 13, + ["PaddingH"] = 4, + ["OffsetY"] = 0, + ["AlignmentV"] = "BOTTOM", + }, + ["WithMissing"] = { + ["PaddingH"] = 4, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + }, + ["PaddingBottom"] = 20, + ["TrackedAurasSpacing"] = 2, + ["HealthBarHeight"] = 24, + ["PowerBarHeight"] = 10, + ["TrackedAurasAlignment"] = "TOP", + ["LineOfSightIcon"] = { + ["Anchor"] = "Button", + ["Width"] = 24, + ["Height"] = 24, + }, + ["RaidMarkIcon"] = { + ["PaddingV"] = 0, + ["AlignmentH"] = "CENTER", + ["Width"] = 14, + ["Height"] = 14, + ["OffsetY"] = 5, + }, + }) + + CreateProfile("Long (Small)", "Long", { + ["AuraTracker"] = { + ["Height"] = 16, + }, + ["PowerText"] = { + ["FontSize"] = 8, + }, + ["RoleIcon"] = { + ["OffsetX"] = -4, + ["Width"] = 12, + ["Height"] = 12, + }, + ["NameText"] = { + ["FontSize"] = 10, + ["MaxWidth"] = 80, + }, + ["Width"] = 120, + ["HealthTexts"] = { + ["Missing"] = { + ["FontSize"] = 9, + }, + ["Normal"] = { + ["FontSize"] = 10, + }, + ["WithMissing"] = { + ["FontSize"] = 8, + }, + }, + ["PaddingBottom"] = 16, + ["PowerBarHeight"] = 8, + ["HealthBarHeight"] = 16, + ["RaidMarkIcon"] = { + ["Width"] = 12, + ["Height"] = 12, + }, + }) + + CreateProfile("Long (Integrated)", "Long", { + ["AuraTracker"] = { + ["Anchor"] = "Health Bar", + ["AlignmentH"] = "LEFT", + ["Width"] = 105, + ["Height"] = 17, + }, + ["RoleIcon"] = { + ["OffsetY"] = 6, + }, + ["NameText"] = { + ["AlignmentV"] = "TOP", + }, + ["HealthTexts"] = { + ["Missing"] = { + ["PaddingV"] = 4, + }, + ["Normal"] = { + ["AlignmentV"] = "TOP", + }, + ["WithMissing"] = { + ["FontSize"] = 12, + ["PaddingV"] = 4, + }, + }, + ["PaddingBottom"] = 0, + ["TrackedAurasAlignment"] = "BOTTOM", + ["HealthBarHeight"] = 35, + }) + + CreateProfile("Enemy", "Default", { + ["PVPIcon"] = { + ["OffsetX"] = -8, + ["OffsetY"] = -4, + }, + ["OutOfRangeThreshold"] = { + ["Hostile"] = 31, + }, + ["AuraTracker"] = { + ["Height"] = 13, + }, + ["RoleIcon"] = { + ["OffsetX"] = -5, + ["Width"] = 10, + ["Height"] = 10, + ["OffsetY"] = 5, + }, + ["IncomingHealText"] = { + ["FontSize"] = 7, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = -6, + }, + ["NameText"] = { + ["PaddingH"] = 1, + ["AlignmentH"] = "LEFT", + ["MaxWidth"] = 75, + ["PaddingV"] = 2, + }, + ["Width"] = 120, + ["ShowDistanceThreshold"] = { + ["Hostile"] = 20, + }, + ["MissingHealthDisplay"] = "Hidden", + ["PowerDisplay"] = "Hidden", + ["RangeText"] = { + ["FontSize"] = 10, + ["OffsetY"] = 2, + ["AlignmentV"] = "BOTTOM", + }, + ["HealthTexts"] = { + ["Missing"] = { + ["FontSize"] = 9, + ["PaddingH"] = 4, + }, + ["Normal"] = { + ["PaddingH"] = 1, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = 0, + ["PaddingV"] = 2, + ["AlignmentV"] = "TOP", + }, + ["WithMissing"] = { + ["FontSize"] = 9, + ["PaddingH"] = 4, + ["AlignmentH"] = "LEFT", + }, + }, + ["PowerText"] = { + ["AlignmentH"] = "RIGHT", + }, + ["LineOfSightIcon"] = { + ["Opacity"] = 70, + }, + ["MaxUnitsInAxis"] = 10, + ["PowerBarHeight"] = 0, + ["HealthBarHeight"] = 29, + ["RaidMarkIcon"] = { + ["AlignmentH"] = "CENTER", + ["OffsetY"] = 2, + ["PaddingV"] = 0, + }, + }) + + CreateProfile("Enemy (Small)", "Enemy", { + ["AuraTracker"] = { + ["Height"] = 12, + }, + ["NameText"] = { + ["FontSize"] = 9, + ["MaxWidth"] = 60, + }, + ["Width"] = 100, + ["HealthTexts"] = { + ["Normal"] = { + ["FontSize"] = 9, + }, + }, + ["HealthBarHeight"] = 24, + }) + + CreateProfile("Legacy", "Default", { + ["PVPIcon"] = { + ["OffsetY"] = -4, + }, + ["AuraTracker"] = { + ["Anchor"] = "Container", + ["AlignmentH"] = "CENTER", + ["Height"] = 20, + }, + ["PowerText"] = { + ["FontSize"] = 10, + ["AlignmentH"] = "RIGHT", + }, + ["RoleIcon"] = { + ["AlignmentH"] = "RIGHT", + ["Width"] = 16, + ["Height"] = 16, + }, + ["IncomingHealText"] = { + ["PaddingH"] = 4, + ["OffsetY"] = 0, + }, + ["NameText"] = { + ["FontSize"] = 12, + ["Anchor"] = "Container", + ["PaddingV"] = 4, + ["AlignmentH"] = "LEFT", + ["MaxWidth"] = 200, + }, + ["Width"] = 200, + ["HealthDisplay"] = "Health/Max Health", + ["PowerDisplay"] = "Hidden", + ["RangeText"] = { + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + ["HealthTexts"] = { + ["Normal"] = { + ["FontSize"] = 12, + ["OffsetY"] = 0, + }, + ["Missing"] = { + ["FontSize"] = 13, + ["PaddingH"] = 4, + ["OffsetY"] = 0, + ["AlignmentV"] = "BOTTOM", + }, + ["WithMissing"] = { + ["PaddingH"] = 4, + ["AlignmentH"] = "RIGHT", + ["OffsetY"] = 0, + ["AlignmentV"] = "TOP", + }, + }, + ["HealthBarStyle"] = "Blizzard", + ["PaddingBottom"] = 20, + ["TrackedAurasSpacing"] = 2, + ["HealthBarHeight"] = 25, + ["BorderStyle"] = "Hidden", + ["PaddingTop"] = 20, + ["PowerBarHeight"] = 5, + ["PowerBarStyle"] = "Blizzard", + ["TrackedAurasAlignment"] = "TOP", + ["MissingHealthInline"] = true, + ["LineOfSightIcon"] = { + ["Anchor"] = "Button", + ["Width"] = 24, + ["Height"] = 24, + }, + ["RaidMarkIcon"] = { + ["OffsetX"] = -18, + ["Width"] = 16, + ["Height"] = 16, + }, + }) +end \ No newline at end of file diff --git a/Puppeteer.lua b/Puppeteer.lua new file mode 100644 index 0000000..ee6e034 --- /dev/null +++ b/Puppeteer.lua @@ -0,0 +1,867 @@ +Puppeteer = {} +PTUtil.SetEnvironment(Puppeteer) +local _G = getfenv(0) +_G.PuppeteerLib = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0") + +VERSION = GetAddOnMetadata("Puppeteer", "version") + +TestUI = false +TestUICount = 40 + +Banzai = AceLibrary("Banzai-1.0") +HealComm = AceLibrary("HealComm-1.0") +GuidRoster = nil -- Will be nil if SuperWoW isn't present + +local compost = PTPool +local util = PTUtil +local colorize = util.Colorize +local GetKeyModifier = util.GetKeyModifier +local GetClass = util.GetClass +local GetPowerType = util.GetPowerType +local UseItem = util.UseItem +local GetItemCount = util.GetItemCount + +PartyUnits = util.PartyUnits +PetUnits = util.PetUnits +TargetUnits = util.TargetUnits +RaidUnits = util.RaidUnits +RaidPetUnits = util.RaidPetUnits +AllUnits = util.AllUnits +AllUnitsSet = util.AllUnitsSet +AllCustomUnits = util.CustomUnits +AllCustomUnitsSet = util.CustomUnitsSet + +local ptBarsPath = util.GetAssetsPath().."textures\\bars\\" +BarStyles = { + ["Blizzard Modern"] = ptBarsPath.."BlizzardRetailBar", + ["Blizzard"] = "Interface\\TargetingFrame\\UI-StatusBar", + ["Blizzard Smooth"] = ptBarsPath.."Blizzard-Smooth", + ["Blizzard Raid"] = ptBarsPath.."Blizzard-Raid", + ["Blizzard Raid Sideless"] = ptBarsPath.."Blizzard-Raid-Sideless", + ["Puppeteer"] = ptBarsPath.."Puppeteer", + ["Puppeteer Borderless"] = ptBarsPath.."Puppeteer-Borderless", + ["Puppeteer Shineless"] = ptBarsPath.."Puppeteer-Shineless", + ["Puppeteer Shineless Borderless"] = ptBarsPath.."Puppeteer-Shineless-Borderless", + -- Uniform fill: no darker lower half, so the bar color looks the same from top to bottom + ["Puppeteer Flat"] = ptBarsPath.."Puppeteer-Flat", + ["Puppeteer Flat Borderless"] = ptBarsPath.."Puppeteer-Flat-Borderless" +} + +CurrentlyHeldButton = nil + +-- Puppeteer's generic bars use the reference's Player family artwork. +-- These resource textures already contain their colors and need a white tint. +local modernPowerTextures = { + mana = ptBarsPath.."ModernPowerManaClass", + rage = ptBarsPath.."ModernPowerRage", + focus = ptBarsPath.."ModernPowerFocus", + energy = ptBarsPath.."ModernPowerEnergy" +} +local modernClassEnergyTexture = ptBarsPath.."ModernPowerEnergyClass" +function GetModernPowerTexture(powerType, class) + if powerType == "energy" and (class == "ROGUE" or class == "DRUID") then + return modernClassEnergyTexture + end + return modernPowerTextures[powerType] +end + +-- An unmapped array of all unit frames +AllUnitFrames = {} +-- A map of units to an array of unit frames associated with the unit +PTUnitFrames = {} + +-- Key: Unit frame group name | Value: The group +UnitFrameGroups = {} + +CustomUnitGUIDMap = PTUnitProxy and PTUnitProxy.CustomUnitGUIDMap or {} +GUIDCustomUnitMap = PTUnitProxy and PTUnitProxy.GUIDCustomUnitMap or {} + + +CurrentlyInRaid = false + +Mouseover = nil + +-- Returns the array of unit frames of the unit +function GetUnitFrames(unit) + return PTUnitFrames[unit] +end + +-- A temporary dummy function while the addon initializes. See below for the real iterator. +function UnitFrames(unit) + return function() end +end + +local function OpenUnitFramesIterator() + -- UnitFrames function definition. + -- Returns an iterator for the unit frames of the unit. + -- These iterators have a serious problem in that they do not support concurrent iteration. + if util.IsSuperWowPresent() then + local EMPTY_UIS = {} + local PTUnitFrames = PTUnitFrames + local GuidUnitMap = PTGuidRoster.GuidUnitMap + local iterTable = {} -- The table reused for iteration over GUID units + local uis + local i = 0 + local len = 0 + local iterFunc = function() + i = i + 1 + if i <= len then + return uis[i] + end + end + function UnitFrames(unit) + if i < len then + print("Collision: "..i.."/"..len) + end + if GuidUnitMap[unit] then -- If a GUID is provided, ALL UIs associated with that GUID will be iterated + uis = iterTable + for i = 1, table.getn(uis) do + uis[i] = nil + end + table.setn(uis, 0) + for _, unit in pairs(GuidUnitMap[unit]) do + for _, frame in ipairs(PTUnitFrames[unit] or EMPTY_UIS) do + table.insert(uis, frame) + end + end + else + uis = PTUnitFrames[unit] or EMPTY_UIS + end + len = table.getn(uis) + i = 0 + return iterFunc + end + else -- Optimized version for vanilla + local PTUnitFrames = PTUnitFrames + local uis + local i = 0 + local len = 0 + local iterFunc = function() + i = i + 1 + if i <= len then + return uis[i] + end + end + function UnitFrames(unit) + i = 0 + uis = PTUnitFrames[unit] + len = table.getn(uis) + return iterFunc + end + end +end + +function Debug(msg) + DEFAULT_CHAT_FRAME:AddMessage(msg) +end + +function UpdateUnitFrameGroups() + for _, group in pairs(UnitFrameGroups) do + group:UpdateRaidMana() + group:UpdateUIPositions() + end +end + +function UpdateAllIncomingHealing() + if PTHealPredict then + for _, ui in ipairs(AllUnitFrames) do + if PTOptions.UseHealPredictions then + local _, guid = UnitExists(ui:GetUnit()) + ui:SetIncomingHealing(PTHealPredict.GetIncomingHealing(guid)) + else + ui:SetIncomingHealing(0) + end + end + else + for _, ui in ipairs(AllUnitFrames) do + if PTOptions.UseHealPredictions then + ui:UpdateIncomingHealing() + else + ui:SetIncomingHealing(0) + end + end + end +end + +function UpdateAllOutlines() + for _, ui in ipairs(AllUnitFrames) do + ui:UpdateOutline() + end +end + +function CreateUnitFrameGroup(groupName, environment, units, petGroup, profile, sortByRole) + if UnitFrameGroups[groupName] then + error("[Puppeteer] Tried to create a unit frame group using existing name! \""..groupName.."\"") + return + end + local uiGroup = PTUnitFrameGroup:New(groupName, environment, units, petGroup, profile, sortByRole) + for _, unit in ipairs(units) do + local ui = PTUnitFrame:New(unit, AllCustomUnitsSet[unit] ~= nil) + if not PTUnitFrames[unit] then + PTUnitFrames[unit] = {} + end + table.insert(PTUnitFrames[unit], ui) + table.insert(AllUnitFrames, ui) + uiGroup:AddUI(ui, true) + if unit ~= "target" then + ui:Hide() + end + end + UnitFrameGroups[groupName] = uiGroup + uiGroup:UpdateUIPositions() + return uiGroup +end + +local function initUnitFrames() + local getSelectedProfile = PuppeteerSettings.GetSelectedProfile + CreateUnitFrameGroup("Party", "party", PartyUnits, false, getSelectedProfile("Party")) + CreateUnitFrameGroup("Pets", "party", PetUnits, true, getSelectedProfile("Pets")) + CreateUnitFrameGroup("Raid", "raid", RaidUnits, false, getSelectedProfile("Raid")) + CreateUnitFrameGroup("Raid Pets", "raid", RaidPetUnits, true, getSelectedProfile("Raid Pets")) + CreateUnitFrameGroup("Target", "all", TargetUnits, false, getSelectedProfile("Target"), false) + if util.IsSuperWowPresent() then + CreateUnitFrameGroup("Focus", "all", PTUnitProxy.CustomUnitsMap["focus"], false, getSelectedProfile("Focus"), false) + CreateUnitFrameGroup("Enemy", "all", PTUnitProxy.CustomUnitsMap["enemy"], false, getSelectedProfile("Enemy"), false) + end + + local baseCondition = UnitFrameGroups["Target"].ShowCondition + UnitFrameGroups["Target"].ShowCondition = function(self) + local friendly = not UnitCanAttack("player", "target") + return (PTOptions.AlwaysShowTargetFrame or (UnitExists("target") and + (friendly and PTOptions.ShowTargets.Friendly) or (not friendly and PTOptions.ShowTargets.Hostile))) + and baseCondition(self) + end + + OpenUnitFramesIterator() +end + +function OnAddonLoaded() + PuppeteerSettings.SetDefaults() + + if PTBindings == nil then + GenerateDefaultBindings() + end + + InitOverrideBindingsMapping() + InitBindingDisplayCache() + + if util.HasModVersion("Nampower", util.Nampower_v3_0) then + SetCVar("NP_EnableAuraCastEvents", 1) + SetCVar("NP_EnableSpellHealEvents", 1) + end + + if util.IsSuperWowPresent() then + -- In case other addons override unit functions, we want to make sure we're using their functions + PTUnitProxy.CreateUnitProxies() + + -- Do it again after all addons have loaded + local frame = CreateFrame("Frame") + local reapply = GetTime() + 0.1 + frame:SetScript("OnUpdate", function() + if GetTime() > reapply then + PTUnitProxy.CreateUnitProxies() + frame:SetScript("OnUpdate", nil) + end + end) + end + + if not _G.PTRoleCache then + _G.PTRoleCache = {} + end + if not _G.PTRoleCache[GetRealmName()] then + _G.PTRoleCache[GetRealmName()] = {} + end + AssignedRoles = _G.PTRoleCache[GetRealmName()] + PruneAssignedRoles() + + if util.IsSuperWowPresent() then + PTUnit.UpdateGuidCaches() + + local customUnitUpdater = CreateFrame("Frame", "PTCustomUnitUpdater") + local nextUpdate = GetTime() + 0.25 + -- Older versions of SuperWoW had an issue where units that aren't part of normal units wouldn't receive events, + -- so updates are done manually + local needsManualUpdates = util.SuperWoWFeatureLevel < util.SuperWoW_v1_4 + local existing = {} + local nextCleanup = GetTime() + 10 + customUnitUpdater:SetScript("OnUpdate", function() + if GetTime() > nextUpdate then + nextUpdate = GetTime() + 0.25 + + for guid, units in pairs(GUIDCustomUnitMap) do + local exists = UnitExists(guid) == 1 + local needsUpdate = false + if (existing[guid] ~= nil) ~= exists then + existing[guid] = exists or nil + needsUpdate = true + end + + if needsManualUpdates or needsUpdate then + PTUnit.Get(guid):UpdateAuras() + for _, unit in ipairs(units) do + for ui in UnitFrames(unit) do + ui:UpdateAll() + ui:UpdateIncomingHealing() + end + end + end + end + + if GetTime() > nextCleanup then + nextCleanup = GetTime() + 10 + for guid in pairs(existing) do + if not GUIDCustomUnitMap[guid] then + existing[guid] = nil + end + end + end + end + end) + else + PTUnit.CreateCaches() + end + PuppeteerSettings.UpdateTrackedDebuffTypes() + PTProfileManager.InitializeDefaultProfiles() + + do + if PTOptions.Scripts.OnLoad then + local scriptString = "local GetProfile = PTProfileManager.GetProfile ".. + "local CreateProfile = PTProfileManager.CreateProfile "..PTOptions.Scripts.OnLoad + local script, err = loadstring(scriptString) + if script then + local ok, result = pcall(script) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 1, 0.4, 0.4)..colorize("ERROR: ", 1, 0.2, 0.2) + ..colorize("The Load Script produced an error! If this causes Puppeteer to fail to load, ".. + "you will need to manually edit the script in your game files.", 1, 0.4, 0.4)) + DEFAULT_CHAT_FRAME:AddMessage(colorize("OnLoad Script Error: "..tostring(result), 1, 0, 0)) + end + else + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 1, 0.4, 0.4)..colorize("ERROR: ", 1, 0.2, 0.2) + ..colorize("The Load Script failed to load: "..err, 1, 0.4, 0.4)) + end + end + end + PTSettingsGui.Init() + if PTHealPredict then + PTHealPredict.OnLoad() + + PTHealPredict.HookUpdates(function(guid, incomingHealing, incomingDirectHealing) + if not PTOptions.UseHealPredictions then + return + end + local units = GuidRoster.GetUnits(guid) + if not units then + return + end + for _, unit in ipairs(units) do + for ui in UnitFrames(unit) do + ui:SetIncomingHealing(incomingHealing, incomingDirectHealing) + end + end + end) + else + local roster = AceLibrary("RosterLib-2.0") + PuppeteerLib:RegisterEvent("HealComm_Healupdate", function(name) + if not PTOptions.UseHealPredictions then + return + end + local unit = roster:GetUnitIDFromName(name) + if unit then + for ui in UnitFrames(unit) do + ui:UpdateIncomingHealing() + end + end + if UnitName("target") == name then + for ui in UnitFrames("target") do + ui:UpdateIncomingHealing() + end + end + end) + PuppeteerLib:RegisterEvent("HealComm_Ressupdate", function(name) + local unit = roster:GetUnitIDFromName(name) + if unit then + for ui in UnitFrames(unit) do + ui:UpdateHealth() + end + end + if UnitName("target") == name then + for ui in UnitFrames("target") do + ui:UpdateHealth() + end + end + end) + end + + InitRoleDropdown() + + SetLFTAutoRoleEnabled(PTOptions.LFTAutoRole) + + SetOutOfRangeArrowEnabled(PTOptions.OutOfRangeArrow) + + if util.IsSuperWowPresent() then + SetEnemyTrackingEnabled(PuppeteerSettings.IsExperimentEnabled("Enemy")) + end + + TestUI = PTOptions.TestUI + + if TestUI then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] UI Testing is enabled. Use /pt testui to disable.", 1, 0.6, 0.6)) + end + + initUnitFrames() + StartUnitTracker() + + PuppeteerLib:RegisterEvent("Banzai_UnitGainedAggro", function(unit) + if PTGuidRoster then + unit = PTGuidRoster.GetUnitGuid(unit) + end + for ui in UnitFrames(unit) do + ui:UpdateOutline() + end + end) + PuppeteerLib:RegisterEvent("Banzai_UnitLostAggro", function(unit) + if PTGuidRoster then + unit = PTGuidRoster.GetUnitGuid(unit) + end + for ui in UnitFrames(unit) do + ui:UpdateOutline() + end + end) + + if PTOnLoadInfoDisabled == nil then + PTOnLoadInfoDisabled = false + end + + do + local INFO_SEND_TIME = GetTime() + 0.5 + local infoFrame = CreateFrame("Frame") + infoFrame:SetScript("OnUpdate", function() + if GetTime() < INFO_SEND_TIME then + return + end + infoFrame:SetScript("OnUpdate", nil) + if PTGlobalOptions.ShowLoadMessage then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] Use ", 0.5, 1, 0.5)..colorize("/pt help", 0, 1, 0) + ..colorize(" to see commands.", 0.5, 1, 0.5)) + end + + if not util.IsSuperWowPresent() and util.IsNampowerPresent() then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 1, 0.4, 0.4)..colorize("WARNING: ", 1, 0.2, 0.2) + ..colorize("You are using Nampower without SuperWoW, which will cause heal predictions to be wildly inaccurate ".. + "for you and your raid members! It is highly recommended to install SuperWoW.", 1, 0.4, 0.4)) + end + + if util.IsSuperWowPresent() and not HealComm:IsEventRegistered("UNIT_CASTEVENT") then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 1, 0.4, 0.4)..colorize("WARNING: ", 1, 0.2, 0.2) + ..colorize("You have another addon that uses a HealComm version that is incompatible with SuperWoW! ".. + "This will cause wildly inaccurate heal predictions to be shown to your raid members. It is ".. + "recommended to either unload the offending addon or copy Puppeteer's HealComm ".. + "into the other addon.", 1, 0.4, 0.4)) + end + end) + end + + do + if PTOptions.Scripts.OnPostLoad then + local scriptString = "local GetProfile = PTProfileManager.GetProfile ".. + "local CreateProfile = PTProfileManager.CreateProfile "..PTOptions.Scripts.OnPostLoad + local script, err = loadstring(scriptString) + if script then + local ok, result = pcall(script) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 1, 0.4, 0.4)..colorize("ERROR: ", 1, 0.2, 0.2) + ..colorize("The Postload Script produced an error! If this causes Puppeteer to fail to operate, ".. + "you may need to manually edit the script in your game files.", 1, 0.4, 0.4)) + DEFAULT_CHAT_FRAME:AddMessage(colorize("OnPostLoad Script Error: "..tostring(result), 1, 0, 0)) + end + else + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 1, 0.4, 0.4)..colorize("ERROR: ", 1, 0.2, 0.2) + ..colorize("The Postload Script failed to load: "..err, 1, 0.4, 0.4)) + end + end + end +end + +function PromptHealersMateImport() + if IsAddOnLoaded("HealersMate") and HealersMate and PTOptions["ImportedFromHM"] == nil then + local dialog = PTGuiLib.Get("simple_dialog", UIParent) + :SetWidth(350) + :SetPoint("CENTER") + :SetTitle("Puppeteer HealersMate Import") + :SetText("Puppeteer has detected HealersMate. Would you like to import your character's data from HealersMate? ".. + "This will overwrite your settings in Puppeteer!") + :SetMovable(true) + dialog:GetHandle():SetFrameStrata("HIGH") + dialog:SetDisposeHandler(function(self) + self:GetHandle():SetFrameStrata("MEDIUM") + end) + + dialog:AddButton("Import & Disable HM", function() + DisableAddOn("HealersMate") + ImportHealersMateSettings() + end) + dialog:AddButton("Import & Don't Disable HM", function() + ImportHealersMateSettings() + end) + dialog:AddButton("Don't Import & Disable HM", function() + PTOptions["ImportedFromHM"] = false + DisableAddOn("HealersMate") + ReloadUI() + dialog:Dispose() + end) + dialog:AddButton("Don't Import & Don't Disable HM", function() + PTOptions["ImportedFromHM"] = false + dialog:Dispose() + end) + dialog:AddButton("Ask Me Later", function() + dialog:Dispose() + end) + end +end + +function ImportHealersMateSettings() + if _G.HMOptions then + _G.PTOptions = util.CloneTable(_G.HMOptions, true) + _G.PTOptions["ImportedFromHM"] = true + end + if _G.HMSpells then + local loadout = ConvertSpellsToBindings(_G.HMSpells) + GetBindingLoadouts()["Imported"] = loadout + PTBindings.SelectedLoadout = "Imported" + end + + if _G.PTGlobalOptions["ImportedFromHM"] == nil then + -- Import global fields if HealersMate has a larger heal cache + if _G.PTHealCache and (util.GetTableSize(_G.PTHealCache) < util.GetTableSize(_G.HMHealCache)) then + _G.PTHealCache = _G.HMHealCache + _G.PTPlayerHealCache = _G.HMPlayerHealCache + _G.PTRoleCache = _G.HMRoleCache + end + _G.PTGlobalOptions["ImportedFromHM"] = true + end + + ReloadUI() +end + +-- Converts legacy HealersMate spells to a Puppeteer loadout +function ConvertSpellsToBindings(spells) + local specialToActionMap = { + ["TARGET"] = "Target", + ["ASSIST"] = "Assist", + ["FOLLOW"] = "Follow", + ["CONTEXT"] = "Menu", + ["ROLE"] = "Role", + ["SET ROLE"] = "Role", + ["ROLE: TANK"] = "Role: Tank", + ["ROLE: HEALER"] = "Role: Healer", + ["ROLE: DAMAGE"] = "Role: Damage", + ["ROLE: NONE"] = "Role: None", + ["FOCUS"] = "Focus", + ["PROMOTE FOCUS"] = "Promote Focus" + } + local loadout = CreateEmptyBindingsLoadout() + for target, modifiers in pairs(spells) do + for modifier, buttons in pairs(modifiers) do + loadout.Bindings[target][modifier] = {} + for button, spell in pairs(buttons) do + local binding + if util.StartsWith(spell, "Item: ") then + binding = { + Type = "ITEM", + Data = string.sub(spell, string.len("Item: ") + 1) + } + elseif util.StartsWith(spell, "Macro: ") then + binding = { + Type = "MACRO", + Data = string.sub(spell, string.len("Macro: ") + 1) + } + elseif specialToActionMap[string.upper(spell)] then + binding = { + Type = "ACTION", + Data = specialToActionMap[string.upper(spell)] + } + else + binding = { + Type = "SPELL", + Data = spell + } + end + + loadout.Bindings[target][modifier][button] = binding + end + end + end + PruneLoadout(loadout) + return loadout +end + +function CheckPartyFramesEnabled() + local shouldBeDisabled = (CurrentlyInRaid and PTOptions.DisablePartyFrames.InRaid) or + (not CurrentlyInRaid and PTOptions.DisablePartyFrames.InParty) + SetPartyFramesEnabled(not shouldBeDisabled) +end + +function SetPartyFramesEnabled(enabled) + if enabled then + for i = 1, MAX_PARTY_MEMBERS do + local frame = getglobal("PartyMemberFrame"..i) + if frame and frame.PTRealShow then + frame.Show = frame.PTRealShow + frame.PTRealShow = nil + + if UnitExists("party"..i) then + frame:Show() + end + local prevThis = _G.this + _G.this = frame + PartyMemberFrame_OnLoad() + _G.this = prevThis + end + end + else + for i = 1, MAX_PARTY_MEMBERS do + local frame = getglobal("PartyMemberFrame"..i) + if frame and not frame.PTRealShow then + frame:UnregisterAllEvents() + frame.PTRealShow = frame.Show + frame.Show = function() end + frame:Hide() + end + end + end +end + +function ToggleFocusUnit(unit) + if PTUnitProxy.IsUnitUnitType(unit, "focus") then + if not PTUnitProxy.CustomUnitsSetMap["focus"][unit] then + return -- Do not toggle focus if user is clicking on a UI that isn't the focus UI + end + UnfocusUnit(unit) + else + FocusUnit(unit) + end +end + +function FocusUnit(unit) + local guid = PTGuidRoster.ResolveUnitGuid(unit) + if not guid or PTUnitProxy.IsGuidUnitType(guid, "focus") then + return + end + + PTUnitProxy.SetGuidUnitType(guid, "focus") + PlaySound("GAMETARGETHOSTILEUNIT") +end + +function UnfocusUnit(unit) + local guid = PTGuidRoster.ResolveUnitGuid(unit) + if not guid then + return + end + local focusUnit = PTUnitProxy.GetCurrentUnitOfType(guid, "focus") + if not focusUnit then + return + end + PTUnitProxy.SetCustomUnitGuid(focusUnit, nil) + PlaySound("INTERFACESOUND_LOSTTARGETUNIT") +end + +function PromoteFocus(unit) + local guid = PTGuidRoster.ResolveUnitGuid(unit) + if not guid then + return + end + PTUnitProxy.PromoteGuidUnitType(guid, "focus") +end + +function CycleFocus(onlyAttackable) + PTUnitProxy.CycleUnitType("focus", onlyAttackable) +end + +local emptySpell = {} +function UnitFrame_OnClick(button, unit, unitFrame) + local binding = GetBindingFor(unit, GetKeyModifier(), button) + if not UnitExists(unit) then + if binding and binding.Type == "ACTION" then + RunBinding(binding, unit, unitFrame) + end + return + end + if not binding then + RunBinding_Spell(emptySpell, unit) + return + end + + RunBinding(binding, unit, unitFrame) +end + +local CheckGroupThrottler = CreateFrame("Frame", "PTCheckGroupThrottler") +local MAX_GROUP_UPDATE_TOKENS = 2 +local GROUP_UPDATE_RECOVERY = 0.2 +local groupUpdateTokens = 2 +local nextGroupUpdateToken = 0 +local groupUpdateWaiting = false +local CheckGroupThrottler_OnUpdate = function() + if nextGroupUpdateToken <= GetTime() then + groupUpdateTokens = groupUpdateTokens + 1 + nextGroupUpdateToken = GetTime() + GROUP_UPDATE_RECOVERY + if groupUpdateWaiting then + groupUpdateWaiting = false + CheckGroupThrottled() + end + if groupUpdateTokens == MAX_GROUP_UPDATE_TOKENS then + CheckGroupThrottler:SetScript("OnUpdate", nil) + end + end +end +function UseGroupUpdateToken() + if groupUpdateTokens > 0 then + groupUpdateTokens = groupUpdateTokens - 1 + if CheckGroupThrottler:GetScript("OnUpdate") == nil then + nextGroupUpdateToken = GetTime() + GROUP_UPDATE_RECOVERY + CheckGroupThrottler:SetScript("OnUpdate", CheckGroupThrottler_OnUpdate) + end + return true + end + groupUpdateWaiting = true + return false +end + +-- Same as CheckGroup, but may delay and consolidate checks if there's been many calls +function CheckGroupThrottled() + if UseGroupUpdateToken() then + CheckGroup() + if PTOptions and PTOptions["Debug"] then + print("Group Update "..groupUpdateTokens) + end + end +end + +-- Reevaluates what UI frames should be shown and updates the roster if using SuperWoW +function CheckGroup() + --StartTiming("CheckGroup") + if GetNumRaidMembers() > 0 then + if not CurrentlyInRaid then + CurrentlyInRaid = true + SetPartyFramesEnabled(not PTOptions.DisablePartyFrames.InRaid) + end + else + if CurrentlyInRaid then + CurrentlyInRaid = false + SetPartyFramesEnabled(not PTOptions.DisablePartyFrames.InParty) + end + end + local superwow = util.IsSuperWowPresent() + if superwow then + GuidRoster.ResetRoster() + GuidRoster.PopulateRoster() + PTUnit.UpdateGuidCaches() + end + for _, unit in ipairs(util.AllRealUnits) do + local exists, guid = UnitExists(unit) + if unit ~= "target" then + if exists then + for ui in UnitFrames(unit) do + ui:Show() + end + else + for ui in UnitFrames(unit) do + ui:Hide() + end + end + end + end + for _, group in pairs(UnitFrameGroups) do + group:EvaluateShown() + group:UpdateReportManaButton() + end + if not superwow then -- If SuperWoW isn't present, the units may have shifted and thus require a full scan + PTUnit.UpdateAllUnits() + end + for _, ui in ipairs(AllUnitFrames) do + if ui:IsShown() then + ui:UpdateRange() + ui:UpdateSight() + ui:UpdateAuras() + ui:UpdateIncomingHealing() + ui:UpdateOutline() + end + end + if superwow then + PTHealPredict.SetRelevantGUIDsFromMap(GuidRoster.GuidUnitMap) + end + RunTrackingScan() + --EndTiming("CheckGroup") +end + +function CheckTarget() + local exists, guid = UnitExists("target") + if exists then + local friendly = not UnitCanAttack("player", "target") + if (friendly and PTOptions.ShowTargets.Friendly) or (not friendly and PTOptions.ShowTargets.Hostile) then + for ui in UnitFrames("target") do + ui.lastHealthPercent = (ui:GetCurrentHealth() / ui:GetMaxHealth()) * 100 + ui:UpdateRange() + ui:UpdateSight() + ui:UpdateRole() + ui:UpdateIncomingHealing() + end + end + else + for ui in UnitFrames("target") do + ui.lastHealthPercent = (ui:GetCurrentHealth() / ui:GetMaxHealth()) * 100 + ui:UpdateAll() + ui:UpdateRole() + ui:UpdateIncomingHealing() + end + end + UnitFrameGroups["Target"]:EvaluateShown() +end + +function IsRelevantUnit(unit) + return AllUnitsSet[unit] ~= nil or GUIDCustomUnitMap[unit] +end + +function Info(msg) + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] ", 0.5, 1, 0.5)..colorize(msg, 1, 1, 0.4)) +end + +function print(msg) + if not PTOptions or not PTOptions["Debug"] then + return + end + local window + local i = 1 + while not window do + local name = GetChatWindowInfo(i) + if not name then + break + end + if name == "Debug" then + window = getglobal("ChatFrame"..i) + break + end + i = i + 1 + end + if window then + window:AddMessage(tostring(msg)) + end +end + +function StartTiming(name) + if not pfDebug_StartTiming then + return + end + pfDebug_StartTiming(name) +end + +function EndTiming(name) + if not pfDebug_EndTiming then + return + end + pfDebug_EndTiming(name) +end + +function PrintStack() + print(debugstack(2)) +end \ No newline at end of file diff --git a/Puppeteer.toc b/Puppeteer.toc new file mode 100644 index 0000000..f28429c --- /dev/null +++ b/Puppeteer.toc @@ -0,0 +1,98 @@ +## Interface: 11200 +## Version: 1.1.3-lowgc +## Title: Puppeteer +## Notes: Unit frames addon tailored for healers +## Author: OldManAlpha, Richard Truax(i2ichardt) +## SavedVariables: PTGlobalOptions, PTHealCache, PTPlayerHealCache, PTRoleCache +## SavedVariablesPerCharacter: PTBindings, PTOptions + +libs\ace\AceLibrary\AceLibrary.lua +libs\ace\AceLocale-2.2\AceLocale-2.2.lua +libs\ace\AceOO-2.0\AceOO-2.0.lua +libs\ace\AceDebug-2.0\AceDebug-2.0.lua +libs\ace\AceAddon-2.0\AceAddon-2.0.lua +libs\ace\AceConsole-2.0\AceConsole-2.0.lua +libs\ace\AceEvent-2.0\AceEvent-2.0.lua +libs\ace\AceHook-2.1\AceHook-2.1.lua +libs\ace\Compost-2.0\Compost-2.0.lua +libs\ace\Gratuity-2.0\Gratuity-2.0.lua +libs\ace\RosterLib-2.0\RosterLib-2.0.lua +libs\ace\Deformat-2.0\Deformat-2.0.lua +libs\ace\ItemBonusLib-1.0\Locales\enUS.lua +libs\ace\ItemBonusLib-1.0\ItemBonusLib-1.0.lua +libs\ace\Banzai-1.0\Banzai-1.0.lua +libs\ace\HealComm-1.0\HealComm-1.0.lua + +libs\Pool.lua + +libs\UnitProxy.lua +libs\Util.lua + +locale\Locale.lua +locale\esES.lua +locale\ptBR.lua +locale\deDE.lua +locale\ruRU.lua +locale\zhCN.lua + +libs\gui\PTGuiLib.lua +libs\gui\Component.lua + +libs\gui\abstract\TabComponent.lua + +libs\gui\component\Container.lua +libs\gui\component\ScrollFrame.lua +libs\gui\component\Button.lua +libs\gui\component\Checkbox.lua +libs\gui\component\Text.lua +libs\gui\component\Title.lua +libs\gui\component\Slider.lua +libs\gui\component\Dropdown.lua +libs\gui\component\EditBox.lua +libs\gui\component\MultiLineEditBox.lua +libs\gui\component\TabPanel.lua +libs\gui\component\EditBoxSlider.lua +libs\gui\component\ColorSelect.lua + +libs\gui\frame\TabFrame.lua +libs\gui\frame\SimpleDialog.lua + +gui\component\uf\Arrow.lua + +Puppeteer.lua +core\SpellsTooltip.lua +core\EventHandler.lua +core\UnitTracker.lua +core\Command.lua +core\OverrideBindings.lua +core\Bindings.lua +core\ActionBindings.lua +core\Roles.lua +core\EnemyTracker.lua + +PuppeteerSettings.lua +PTUnitFrame.lua +PTUnitFrameGroup.lua +Profile.lua +ProfileManager.lua + +gui\GuiUtil.lua +gui\component\SpellLine.lua +gui\component\InputBlock.lua +gui\component\SpellBindInterface.lua +gui\component\BindingScriptEditor.lua +gui\component\MultiEditor.lua +gui\component\BindingOptions.lua +gui\component\ButtonEditor.lua +gui\component\NewLoadout.lua +gui\component\LoadScriptEditor.lua +gui\Settings.lua + +gui\component\uf\CastIcon.lua +gui\component\uf\HighlightBorder.lua + +libs\GuidRoster.lua +libs\AuraTracker.lua +libs\PTUnit.lua +libs\ShaguUtil.lua +libs\HealPredict.lua diff --git a/PuppeteerSettings.lua b/PuppeteerSettings.lua new file mode 100644 index 0000000..903b17c --- /dev/null +++ b/PuppeteerSettings.lua @@ -0,0 +1,626 @@ +PuppeteerSettings = {} +PTUtil.SetEnvironment(PuppeteerSettings) + +local _G = getfenv(0) +local util = PTUtil + +local _, playerClass = UnitClass("player") + +local debuffTypeCureSpells = { + ["PALADIN"] = { + ["Purify"] = {"Poison", "Disease"}, + ["Cleanse"] = {"Poison", "Disease", "Magic"} + }, + ["PRIEST"] = { + ["Cure Disease"] = {"Disease"}, + ["Abolish Disease"] = {"Disease"}, + ["Dispel Magic"] = {"Magic"} + }, + ["DRUID"] = { + ["Cure Poison"] = {"Poison"}, + ["Abolish Poison"] = {"Poison"}, + ["Remove Curse"] = {"Curse"} + }, + ["SHAMAN"] = { + ["Cure Poison"] = {"Poison"}, + ["Cure Disease"] = {"Disease"} + }, + ["MAGE"] = { + ["Remove Lesser Curse"] = {"Curse"} + } +} +for _, spells in pairs(debuffTypeCureSpells) do + PTLocale.Keys(spells) +end +function UpdateTrackedDebuffTypes() + for _, class in ipairs(util.GetClasses()) do + if not debuffTypeCureSpells[class] then + debuffTypeCureSpells[class] = {} + end + end + + local trackedDebuffTypes = {} + do + local id = 1; + for i = 1, GetNumSpellTabs() do + local _, _, _, numSpells = GetSpellTabInfo(i); + for j = 1, numSpells do + local spellName = GetSpellName(id, "spell"); + local types = debuffTypeCureSpells[playerClass][spellName] + if types then + for _, type in ipairs(types) do + trackedDebuffTypes[type] = 1 + end + end + id = id + 1 + end + end + end + TrackedDebuffTypesSet = trackedDebuffTypes + trackedDebuffTypes = util.ToArray(trackedDebuffTypes) + + TrackedDebuffTypes = trackedDebuffTypes +end + +function LetsCrashOut() + local dialog + dialog = PTGuiLib.Get("simple_dialog", UIParent) + :SetPoint("CENTER") + :SetTitle("Puppeteer") + :SetText("You are using a newer settings version with an older version of Puppeteer! Either update the addon or ".. + "manually delete its data.") + :AddButton("Okay", function() + dialog:Dispose() + end) + Puppeteer.UnregisterEventHandlers() + SeeYaImCrashingOut() -- im crashing out +end + +function SetDefaults() + if not PTOptions then + _G.PTOptions = {} + end + + if not PTGlobalOptions then + _G.PTGlobalOptions = {} + end + + local OPTIONS_VERSION = 4 + + if PTOptions.OptionsVersion and (PTOptions.OptionsVersion > OPTIONS_VERSION) then + LetsCrashOut() + end + + if PTGlobalOptions.OptionsVersion and (PTGlobalOptions.OptionsVersion > OPTIONS_VERSION) then + LetsCrashOut() + end + + local isHealer = util.IsHealerClass("player") + local isManaUser = util.ClassPowerTypes[util.GetClass("player")] == "mana" + do + local defaults = { + ["ShowTargets"] = { + ["Friendly"] = isHealer, + ["Hostile"] = false + }, + ["AlwaysShowTargetFrame"] = false, + ["TargetWhileCasting"] = false, + ["TargetAfterCasting"] = false, + ["FrameDrag"] = { + ["MoveAll"] = false, + ["AltMoveKey"] = "Shift" + }, + ["DisablePartyFrames"] = { + ["InParty"] = false, + ["InRaid"] = false + }, + ["SpellsTooltip"] = { + ["Enabled"] = isHealer, + ["AttachTo"] = "Button", -- "Button", "Frame", "Group", "Screen" + ["OffsetX"] = 0, + ["OffsetY"] = 0, + ["Anchor"] = "Top Right", -- "Top Left", "Top Right", "Bottom Left", "Bottom Right" + ["ShowManaCost"] = false, + ["ShowManaPercentCost"] = true, + ["HideCastsAbove"] = 3, + ["CriticalCastsLevel"] = 3, + ["AbbreviatedKeys"] = false, + ["ColoredKeys"] = true, + ["ShowPowerBar"] = true, + ["ShowPowerAs"] = isManaUser and "Power %" or "Power", -- "Power", "Power/Max Power", "Power %" + ["ShowItemCount"] = false + }, + ["ShowAuraTimesAt"] = { + ["Short"] = 5, -- <1 min + ["Medium"] = 10, -- <=2 min + ["Long"] = 60 * 2 -- >2 min + }, + ["Tracking"] = { + ["EvaluateInterval"] = 1.25, -- How often everyone is fully scanned to determine if they should be closely tracked + ["DistanceUpdateInterval"] = 0.1, -- How often distance tracked units are updated + ["SightUpdateInterval"] = 0.1, -- How often sight tracked units are updated + ["MinDistanceTracking"] = 0, -- The minimum distance to start closely tracking distance + ["MaxDistanceTracking"] = 80, -- The maxmimum distance to start closely tracking distance + ["MaxSightTracking"] = 80 -- The maximum distance to closely track sight + }, + ["CastWhen"] = "Mouse Up", -- Mouse Up, Mouse Down + ["CastWhenKey"] = "Key Up", -- Key Up, Key Down + ["AutoResurrect"] = util.ResurrectionSpells[util.GetClass("player")] ~= nil, + ["UseHealPredictions"] = true, + ["PVPFlagProtection"] = true, + ["SetMouseover"] = true, + ["ShowRaidMana"] = true, + ["LFTAutoRole"] = true, -- WoW 1.18.1 Clients + ["TestUI"] = false, + ["Hidden"] = false, + ["HideWhileSolo"] = false, + ["OutOfRangeArrow"] = true, + ["VanillaClassColors"] = false, + ["PetOwnerClassColors"] = true, + ["ChosenProfiles"] = { + ["Party"] = PTProfileManager.DEFAULT_PROFILE_NAME, + ["Pets"] = PTProfileManager.DEFAULT_PROFILE_NAME, + ["Raid"] = "Small", + ["Raid Pets"] = "Small", + ["Target"] = "Long", + ["Focus"] = PTProfileManager.DEFAULT_PROFILE_NAME, + ["Enemy"] = "Enemy" + }, + ["StyleOverrides"] = {}, + ["FrameOptions"] = {}, + ["Scripts"] = { + ["OnLoad"] = "", + ["OnPostLoad"] = "" + }, + ["Experiments"] = { + ["Enemy"] = false, + ["CastIcons"] = false + }, + ["OptionsVersion"] = OPTIONS_VERSION + } + local specialDefaults = { + ["Buttons"] = { + "LeftButton", + "MiddleButton", + "RightButton", + "Button5", + "Button4", + "MOUSEWHEELUP", + "MOUSEWHEELDOWN" + }, + ["ButtonInfo"] = { + ["LeftButton"] = { + ["Name"] = "Left", + ["ShowUnbound"] = true + }, + ["MiddleButton"] = { + ["Name"] = "Middle", + ["ShowUnbound"] = true + }, + ["RightButton"] = { + ["Name"] = "Right", + ["ShowUnbound"] = true + }, + ["Button5"] = { + ["Name"] = "Forward", + ["ShowUnbound"] = true + }, + ["Button4"] = { + ["Name"] = "Back", + ["ShowUnbound"] = true + }, + ["MOUSEWHEELUP"] = { + ["Name"] = "Wheel Up", + ["ShowUnbound"] = false + }, + ["MOUSEWHEELDOWN"] = { + ["Name"] = "Wheel Down", + ["ShowUnbound"] = false + }, + } + } + + if PTOptions.OptionsVersion and PTOptions.OptionsVersion < OPTIONS_VERSION then + local optionsUpgrades = { + { + version = 2, + upgrade = function(self, options) + local upgraded = util.CloneTable(options, true) + if options["ShowSpellsTooltip"] ~= nil then + if not options["SpellsTooltip"] then + upgraded["SpellsTooltip"] = {} + end + upgraded["SpellsTooltip"]["Enabled"] = options["ShowSpellsTooltip"] + upgraded["ShowSpellsTooltip"] = nil + end + if options["ChosenProfiles"] ~= nil then + local groupNames = {"Party", "Pets", "Raid", "Raid Pets", "Target"} + local changedProfileNames = { + ["Compact"] = "Default", + ["Compact (Small)"] = "Small", + ["Compact (Short Bar)"] = "Default (Short Bar)" + } + for _, name in ipairs(groupNames) do + local currentlySelected = options["ChosenProfiles"][name] + if changedProfileNames[currentlySelected] then + upgraded["ChosenProfiles"][name] = changedProfileNames[currentlySelected] + end + end + end + upgraded["OptionsVersion"] = self.version + return upgraded + end, + shouldUpgrade = function(self, options) + return options.OptionsVersion < self.version + end + }, + { -- HealersMate -> Puppeteer 1.0.0 + version = 3, + upgrade = function(self, options) + local upgraded = util.CloneTable(options, true) + if options["AutoTarget"] then + upgraded["TargetWhileCasting"] = true + upgraded["TargetAfterCasting"] = true + end + if options["Scripts"] then + local guard = "-- Auto-generated guard to prevent errors in new addon version, remove if you're sure ".. + "your script won't produce errors\nif true then return end\n\n" + if options["Scripts"]["OnLoad"] ~= nil and options["Scripts"]["OnLoad"] ~= "" then + upgraded["Scripts"]["OnLoad"] = guard..options["Scripts"]["OnLoad"] + end + if options["Scripts"]["OnPostLoad"] ~= nil and options["Scripts"]["OnPostLoad"] ~= "" then + upgraded["Scripts"]["OnPostLoad"] = guard..options["Scripts"]["OnPostLoad"] + end + end + upgraded["OptionsVersion"] = self.version + return upgraded + end, + shouldUpgrade = function(self, options) + return options.OptionsVersion < self.version + end + }, + { -- Puppeteer 1.0.5 -> 1.1.0 + version = 4, + upgrade = function(self, options) + local upgraded = util.CloneTable(options, true) + if options["Tracking"] then + if options["Tracking"]["MinDistanceTracking"] == 20 then + upgraded["Tracking"]["MinDistanceTracking"] = 0 + end + if options["Tracking"]["MaxDistanceTracking"] == 60 then + upgraded["Tracking"]["MaxDistanceTracking"] = 80 + end + end + if options["ChosenProfiles"] and not options["ChosenProfiles"]["Enemy"] then + upgraded["ChosenProfiles"]["Enemy"] = "Enemy" + end + upgraded["OptionsVersion"] = self.version + return upgraded + end, + shouldUpgrade = function(self, options) + return options.OptionsVersion < self.version + end + } + } + for _, upgrade in ipairs(optionsUpgrades) do + if upgrade:shouldUpgrade(PTOptions) then + local prevVersion = PTOptions.OptionsVersion + _G.PTOptions = upgrade:upgrade(PTOptions) + Puppeteer.Info("Upgraded options from version "..prevVersion.." to "..upgrade.version) + end + end + end + + ApplyDefaults(PTOptions, defaults) + -- Special defaults + if not PTOptions["Buttons"] then + PTOptions["Buttons"] = util.CloneTable(specialDefaults["Buttons"]) + end + if not PTOptions["ButtonInfo"] then + PTOptions["ButtonInfo"] = util.CloneTable(specialDefaults["ButtonInfo"]) + end + end + + do + local defaults = { + ["ShowLoadMessage"] = true, + ["Experiments"] = { + ["AutoRole"] = false + }, + ["OptionsVersion"] = OPTIONS_VERSION + } + ApplyDefaults(PTGlobalOptions, defaults) + end +end + +function ApplyDefaults(t, defaults) + for field, value in pairs(defaults) do + if type(value) == "table" then + t[field] = t[field] or {} + ApplyDefaults(t[field], value) + elseif t[field] == nil then + t[field] = value + end + end +end + +function TraverseOptions(location) + local path = util.SplitString(location, ".") + local currentTable = PTOptions + if path[1] == "Global" then + currentTable = PTGlobalOptions + table.remove(path, 1) + end + for i = 1, table.getn(path) - 1 do + currentTable = currentTable[path[i]] + end + return currentTable, path[table.getn(path)] +end + +function GetOption(location) + local optionTable, location = TraverseOptions(location) + return optionTable[location] +end + +function SetOption(location, value) + local optionTable, location = TraverseOptions(location) + optionTable[location] = value +end + +-- Buffs/debuffs that significantly modify healing +DefaultTrackedHealingBuffs = {"Amplify Magic", "Dampen Magic", "Master Demonologist", "Apotheosis"} +DefaultTrackedHealingDebuffs = {"Mortal Strike", "Wound Poison", "Curse of the Deadwood", "Veil of Shadow", "Mortal Wound", + "Gehennas' Curse", "Necrotic Poison", "Blood Fury", "Necrotic Aura", "Brood Affliction: Green", + "Shadowbane Curse", "Leeching Bite", "Foulfire Bolt", "Spores of Corruption" -- WoW 1.18.1 Clients +} +-- Tracked buffs for all classes +DefaultTrackedBuffs = { + "Blessing of Protection", "Hand of Protection", "Divine Protection", "Divine Shield", "Divine Intervention", -- Paladin + "Bulwark of the Righteous", "Blessing of Sacrifice", "Hand of Sacrifice", + "Power Infusion", "Ascendance", "Spirit of Redemption", "Inner Focus", "Abolish Disease", "Power Word: Shield", "Mind Control", -- Priest + "Shield Wall", "Recklessness", "Last Stand", -- Warrior + "Evasion", "Vanish", -- Rogue + "Deterrence", "Feign Death", "Mend Pet", -- Hunter + "Tranquility", "Frenzied Regeneration", "Berserk", "Barkskin", "Barkskin (Feral)", "Innervate", "Abolish Poison", -- Druid + "Soulstone Resurrection", "Sacrifice", "Hellfire", "Health Funnel", -- Warlock + "Ice Block", "Evocation", "Ice Barrier", "Mana Shield", -- Mage + "Quel'dorei Meditation", "Grace of the Sunwell", "Cannibalize", -- Racial + "First Aid", "Food", "Drink", "Invulnerability", "Living Free Action", "Persistent Shield", "Rapid Healing" -- Generic +} +-- Tracked buffs for specific classes +DefaultClassTrackedBuffs = { + ["PALADIN"] = {"Blessing of Wisdom", "Blessing of Might", "Blessing of Salvation", "Blessing of Sanctuary", + "Blessing of Kings", "Blessing of Light", "Greater Blessing of Wisdom", "Greater Blessing of Might", + "Greater Blessing of Salvation", "Greater Blessing of Sanctuary", "Greater Blessing of Kings", + "Greater Blessing of Light", "Daybreak", "Blessing of Freedom", "Hand of Freedom", "Redoubt", "Holy Shield"}, + ["PRIEST"] = {"Prayer of Fortitude", "Power Word: Fortitude", "Prayer of Spirit", "Divine Spirit", + "Prayer of Shadow Protection", "Shadow Protection", "Lightwell", "Holy Champion", "Champion's Grace", "Empower Champion", + "Champion's Bond", "Fear Ward", "Inner Fire", "Renew", "Greater Heal", "Lightwell Renew", "Inspiration", + "Fade", "Reactive Fade", "Spirit Tap", "Enlighten", "Enlightened"}, + ["WARRIOR"] = {"Battle Shout"}, + ["DRUID"] = {"Gift of the Wild", "Mark of the Wild", "Thorns", "Rejuvenation", "Regrowth", "Dash", "Blooming Bud"}, + ["SHAMAN"] = {"Water Walking", "Healing Way", "Ancestral Fortitude"}, + ["MAGE"] = {"Arcane Brilliance", "Arcane Intellect", "Frost Armor", "Ice Armor", "Mage Armor", "Fire Ward", "Frost Ward", + "Arcane Power"}, + ["WARLOCK"] = {"Demon Armor", "Demon Skin", "Unending Breath", "Shadow Ward", "Fire Shield", "Consume Shadows"}, + ["HUNTER"] = {"Rapid Fire", "Quick Shots", "Quick Strikes", "Aspect of the Pack", + "Aspect of the Wild", "Bestial Wrath", "Feed Pet Effect"} +} + +-- Tracked debuffs for all classes +DefaultTrackedDebuffs = { + "Forbearance", "Hammer of Justice", -- Paladin + "Silence", "Mind Control", "Psychic Scream", -- Priest + "Blind", "Sap", "Cheap Shot", -- Rogue + "Death Wish", "Intimidating Shout", -- Warrior + "Enrage", "Entangling Roots", -- Druid + "Frost Nova", "Polymorph", -- Mage + "Fear", "Seduction", "Enslave Demon", -- Warlock + "Recently Bandaged", "Greater Dreamless Sleep", "Dreamless Sleep", "Resurrection Sickness", "Ghost", "Net", -- Generic + "Deafening Screech", "Hooked Net", "Web Explosion", "Chains of Ice", "Acid Spit", -- Applied by mobs + "Gust of Wind", -- Windhorn Canyon + "Fungal Spores", -- Stormwrought Castle + "Sonic Burst", -- Uldaman + "Hex", "Freeze Solid", "Petrify", -- Zul'Farrak + "Howl of Terror", -- Gilneas City + "Living Bomb", "Ancient Dread", "Withering Heat", -- Molten Core + "Bellowing Roar", -- Onyxia/Nefarian + "Intimidating Roar", "Curse of Blood", "Wyvern Sting", -- ZG + "Brood Affliction: Bronze", "Corrupted Healing", "Shadow of Ebonroc", -- BWL + "Frost Breath", "Death Coil", -- Karazhan Crypts + "Phantom Scream", "Smoke Bomb", "Terrifying Presence", "Moroes Curse", -- Kara10 + "Acid Breath", "Call of Nightmare", "Dreamstate", "Fever Dream", -- Emerald Sanctum + "Withered Defiling", "Paralyzing Poison", "Seed of Corruption", "Fearful Roar", "Poison Cloud", "Phasebound", -- Timbermaw Hold + "Lucid Nightmare", "Dream Fever", "Mind-Shattering Rumble", "Fixate", + "Poison Bolt", "Plague", -- AQ40 + "Corrupted Mind", "Frost Blast", "Veil of Darkness", "Web Wrap", "Poison Charge", "Life Drain", "Intercept Stun", -- Naxxramas + "Red Moon", "Blue Moon", "Blood Siphon", "Invoked Silence", "Dragon Roar", "Surge of Mana", "Gaze of Incantagos" -- Kara40 +} +-- Tracked debuffs for specific classes +DefaultClassTrackedDebuffs = { + ["PRIEST"] = {"Weakened Soul", "Holy Attenuation"} +} + +-- Debuffs which should highlight the unit frame +DefaultImportantDebuffs = { + "Living Bomb", "Plague", "Web Wrap", "Frost Blast", "Poison Charge", "Withered Defiling", "Seed of Corruption", + "Blood Siphon", "Surge of Mana" +} + +-- Debuffs which are dispellable but should not change the health bar color +DefaultIgnoredDispellableDebuffs = { + "Frostbolt", "Icicles", -- Mage + "Greater Dreamless Sleep", "Dreamless Sleep", -- Generic + "Emerald Rot", "Sanctum Mind Decay", "Emerald Instability", -- Emerald Sanctum + "Frost Shock", -- Kara10 + "Arcane Overload", "Doom of Medivh" -- Kara40 +} + +PTLocale.Array(DefaultTrackedHealingBuffs) +PTLocale.Array(DefaultTrackedHealingDebuffs) +PTLocale.Array(DefaultTrackedBuffs) +PTLocale.Array(DefaultTrackedDebuffs) +for _, buffs in pairs(DefaultClassTrackedBuffs) do + PTLocale.Array(buffs) +end +for _, debuffs in pairs(DefaultClassTrackedDebuffs) do + PTLocale.Array(debuffs) +end +PTLocale.Array(DefaultImportantDebuffs) +PTLocale.Array(DefaultIgnoredDispellableDebuffs) + +-- The baked aura sets +TrackedBuffs = {} +TrackedDebuffs = {} +TrackedDebuffTypes = {} +ImportantDebuffs = {} +IgnoredDispellableDebuffs = {} +TrackedHealingBuffs = {} +TrackedHealingDebuffs = {} + +function BakeTrackedAuras() + util.ClearTable(TrackedBuffs) + util.ClearTable(TrackedDebuffs) + util.ClearTable(TrackedHealingBuffs) + util.ClearTable(TrackedHealingDebuffs) + util.ClearTable(ImportantDebuffs) + util.ClearTable(IgnoredDispellableDebuffs) + + local trackedBuffsArray = {} + if DefaultClassTrackedBuffs[playerClass] then + util.AppendArrayElements(trackedBuffsArray, DefaultClassTrackedBuffs[playerClass]) + end + util.AppendArrayElements(trackedBuffsArray, DefaultTrackedHealingBuffs) + util.AppendArrayElements(trackedBuffsArray, DefaultTrackedBuffs) + util.ToSet(trackedBuffsArray, true, TrackedBuffs) + + local trackedDebuffsArray = {} + if DefaultClassTrackedDebuffs[playerClass] then + util.AppendArrayElements(trackedDebuffsArray, DefaultClassTrackedDebuffs[playerClass]) + end + util.AppendArrayElements(trackedDebuffsArray, DefaultTrackedHealingDebuffs) + util.AppendArrayElements(trackedDebuffsArray, DefaultTrackedDebuffs) + util.ToSet(trackedDebuffsArray, true, TrackedDebuffs) + + util.ToSet(DefaultTrackedHealingBuffs, false, TrackedHealingBuffs) + util.ToSet(DefaultTrackedHealingDebuffs, false, TrackedHealingDebuffs) + + util.ToSet(DefaultImportantDebuffs, false, ImportantDebuffs) + util.ToSet(DefaultIgnoredDispellableDebuffs, false, IgnoredDispellableDebuffs) +end + +BakeTrackedAuras() + +function AddTrackedBuffs(...) + for _, buff in ipairs(arg) do + table.insert(DefaultTrackedBuffs, buff) + end + BakeTrackedAuras() +end + +function RemoveTrackedBuffs(...) + for _, buff in ipairs(arg) do + util.RemoveElement(DefaultTrackedBuffs, buff) + util.RemoveElement(DefaultClassTrackedBuffs[playerClass], buff) + end + BakeTrackedAuras() +end + +function AddTrackedDebuffs(...) + for _, debuff in ipairs(arg) do + table.insert(DefaultTrackedDebuffs, debuff) + end + BakeTrackedAuras() +end + +function RemoveTrackedDebuffs(...) + for _, debuff in ipairs(arg) do + util.RemoveElement(DefaultTrackedDebuffs, debuff) + util.RemoveElement(DefaultClassTrackedDebuffs[playerClass], debuff) + end + BakeTrackedAuras() +end + +DebuffTypeColors = { + ["Magic"] = {0.35, 0.35, 1}, + ["Curse"] = {0.5, 0, 1}, + ["Disease"] = {0.45, 0.35, 0.16}, + ["Poison"] = {0.6, 0.7, 0} +} + +function GetSelectedProfileName(frame) + local selected = PTOptions.ChosenProfiles[frame] + if not PTProfileManager.GetProfile(selected) then + selected = "Default" + end + return selected +end + +function GetSelectedProfile(frame) + return PTProfileManager.GetProfile(GetSelectedProfileName(frame)) +end + +local function validateFrameOptionsExistence(frameName) + if not PTOptions.FrameOptions[frameName] then + PTOptions.FrameOptions[frameName] = {} + end +end + +function IsFrameHidden(frameName) + return PTOptions.FrameOptions[frameName] and PTOptions.FrameOptions[frameName].Hidden +end + +function SetFrameHidden(frameName, hidden) + validateFrameOptionsExistence(frameName) + PTOptions.FrameOptions[frameName].Hidden = hidden + PTSettingsGui.UpdateFrameOptions() +end + +function IsFrameLocked(frameName) + return PTOptions.FrameOptions[frameName] and PTOptions.FrameOptions[frameName].Locked +end + +function SetFrameLocked(frameName, locked) + validateFrameOptionsExistence(frameName) + PTOptions.FrameOptions[frameName].Locked = locked + local group = Puppeteer.UnitFrameGroups[frameName] + if group then + group:UpdateHeaderColor() + end + PTSettingsGui.UpdateFrameOptions() +end + +function IsTitleHidden(frameName) + return PTOptions.FrameOptions[frameName] and PTOptions.FrameOptions[frameName].TitleHidden +end + +function SetTitleHidden(frameName, hidden) + validateFrameOptionsExistence(frameName) + PTOptions.FrameOptions[frameName].TitleHidden = hidden + local group = Puppeteer.UnitFrameGroups[frameName] + if group then + group:UpdateUIPositions() + end + PTSettingsGui.UpdateFrameOptions() +end + +function GetFramePosition(frameName) + if not (PTOptions.FrameOptions[frameName] and PTOptions.FrameOptions[frameName].Position) then + return "TOPLEFT", (GetScreenWidth() / 2), -(GetScreenHeight() / 2) + end + return unpack(PTOptions.FrameOptions[frameName].Position) +end + +function SaveFramePositions() + for frameName, group in pairs(Puppeteer.UnitFrameGroups) do + if not PTOptions.FrameOptions[frameName] then + PTOptions.FrameOptions[frameName] = {} + end + local anchor, _, _, x, y = group:GetContainer():GetPoint(1) + PTOptions.FrameOptions[frameName].Position = {anchor, x, y} + end +end + +function IsExperimentEnabled(experiment) + return PTOptions.Experiments[experiment] or PTGlobalOptions.Experiments[experiment] +end \ No newline at end of file diff --git a/assets/textures/arrow.tga b/assets/textures/arrow.tga new file mode 100644 index 0000000000000000000000000000000000000000..55c466933a43f8e2435c4fc9cc5824c678626953 GIT binary patch literal 220404 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 Q{Iid~`Q-Z3S0A7K4F%ZTW&i*H literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Blizzard-Raid-Sideless.tga b/assets/textures/bars/Blizzard-Raid-Sideless.tga new file mode 100644 index 0000000000000000000000000000000000000000..c685af9eb26b58135022a144bc7475c0d2e081f3 GIT binary patch literal 667 zcmZQz;9`IQ1qKBM35NMWfkE?&3ybHsG_`KHnzjfPIFb1(fVjww?8jxC$9*|y;86dMjW`fLa zYipZdU0V&N=ggY(o)PArQJM}Rz{nuT@RD5!81dqy+E$R5nOCBhms+A=s%M~QXoN?< g0!WS#3Q^Qeg7QLwLS4f=gFXHIbR*n6eO&bz0MJ)jF8}}l literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Blizzard-Raid.tga b/assets/textures/bars/Blizzard-Raid.tga new file mode 100644 index 0000000000000000000000000000000000000000..4065a9d88cfde844cb29783a584a0827b8d7c2a3 GIT binary patch literal 935 zcmZQz;9`IQ1qKBM2?mC&oUD04fk6z-&CPR*3yT?&l9Cwa%$hT|rKtrgyQiy%0i+J3 zrmwFL%wD=^Y15QRQ>IUxG_h&Qv?(yLxgd63Z5_j!)obR?nKy?4WG0BcaM40Ad+VmH zbC)k$4ri}jwHD0YzGM5`wd>Y0^z`*G?A^C_?)nYuVeDC(Hg9SI>Vxs;Zrz5R4O9nL z1JjReF3^l7pj&3a>_m18vKx`z4)iD7Pf$P40{R&vJV0Rs3#W;bC(dnaYh%dH%w_rIsj|>KW)68sX8e0Fq;bLKJl{T_HiCu3?_Rp8kHi5pJG7 Hu6hgr@Twtl literal 0 HcmV?d00001 diff --git a/assets/textures/bars/BlizzardRetailBar.tga b/assets/textures/bars/BlizzardRetailBar.tga new file mode 100644 index 0000000000000000000000000000000000000000..18e973c235793487dba08a2ab0dc4e48dfa37705 GIT binary patch literal 12134 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 literal 0 HcmV?d00001 diff --git a/assets/textures/bars/MODERN-POWER-NOTICES.md b/assets/textures/bars/MODERN-POWER-NOTICES.md new file mode 100644 index 0000000..cab9228 --- /dev/null +++ b/assets/textures/bars/MODERN-POWER-NOTICES.md @@ -0,0 +1,163 @@ +# ShaguTweaks-ClassicAPI third-party notices + +This file records third-party material, upstream sources, external runtime +dependencies, and development references known to ShaguTweaks-ClassicAPI. + +It is intentionally additive: source comments, Git history, upstream license +notices, and README credits remain part of the provenance trail and should not +be removed merely because this summary exists. + +## ShaguTweaks + +ShaguTweaks-ClassicAPI is a fork of ShaguTweaks: + +- Source: https://github.com/shagu/ShaguTweaks +- Copyright (c) 2021 Eric Mauser (Shagu) +- License: MIT + +The original MIT license remains at the repository root in `LICENSE` and an +additional preserved copy is stored in `LICENSES/ShaguTweaks-MIT.txt`. + +The MIT grant applies to material for which the upstream copyright holder had +the right to grant those permissions. It does not by itself establish ownership +or relicensing authority over unrelated third-party or game-derived assets that +may have been present in an upstream package. + +## TokensWorth/ShaguTweaks-mods + +Extended frame support in `mods/move-unitframes.lua` contains code adapted +from `TokensWorth/ShaguTweaks-mods`: + +- Upstream module: `mods/move-unitframes-extended.lua` +- Source: https://github.com/TokensWorth/ShaguTweaks-mods +- Copyright (c) 2022 GryllsAddons +- License: MIT + +The preserved license text is stored in +`LICENSES/ShaguTweaks-mods-MIT.txt`. + +## ClassicAPI + +ClassicAPI is a required external runtime dependency used by +ShaguTweaks-ClassicAPI. + +- Source: https://github.com/brues-code/ClassicAPI +- The upstream repository includes the GNU General Public License version 3. +- ClassicAPI itself is **not bundled** in this addon repository. + +A verbatim copy of the upstream license document is preserved in +`LICENSES/ClassicAPI-GPL-3.0.txt` for reference. Including that license text +does not relicense ShaguTweaks-ClassicAPI under the GPL and does not imply that +ClassicAPI binaries or source are distributed as part of this addon. + +## AoELoot + +`mods/aoe-loot.lua` is adapted from the standalone AoELoot addon provided to +the project by Sandrea: + +- Original addon title: `AoELoot` +- Original metadata credit: Sandrea / ChatGPT +- Adapted module: `mods/aoe-loot.lua` + +The supplied package did not include an upstream URL or a separate license +file. This notice records the known provenance and preserves attribution; it +does not infer additional licensing terms. + +## SuperWoW + +SuperWoW is an optional external runtime dependency/fallback used by some +compatibility paths. + +- Source: https://github.com/balakethelock/SuperWoW +- SuperWoW is **not bundled** in this addon repository. +- Its upstream license is preserved verbatim in + `LICENSES/SuperWoW-LICENSE.txt`. + +The upstream SuperWoW license has its own restrictions. This notice records the +dependency and preserves its license; it does not extend those terms to +ShaguTweaks-ClassicAPI or grant additional rights in SuperWoW. + +## pfUI and zUI + +pfUI and zUI are credited in the project README as sources of additional code +ideas and development reference. + +This notice does **not** make a broader claim that either project is bundled in +whole, nor does it infer a license for unspecified material. Any specific +future copied or substantially adapted component should be documented with its +exact source, affected files, and applicable license at the time it is added. + +## Turtle WoW-like environments + +ShaguTweaks-ClassicAPI contains compatibility logic intended for Turtle WoW-like +server/client environments. Compatibility, testing, naming, or behavioral +reference does not create an affiliation, endorsement, partnership, or +ownership relationship with those projects or their maintainers. + +## Artwork and screenshots + +Artwork under `img/` and screenshots under `screenshots/` are tracked +separately in `Docs/ASSET_PROVENANCE.md`. + +A project-level software license does not automatically prove ownership or +relicensing authority for every visual asset in an upstream project. The asset +manifest therefore records exact upstream matches without making broader +ownership claims. + +## Project identity and trademarks + +The canonical repository maintained by Dusk-92 is: + +- https://github.com/Dusk-92/ShaguTweaks-ClassicAPI + +Forks, mirrors, package caches, and modified copies hosted elsewhere are +independent unless this canonical project explicitly states otherwise. + +World of Warcraft and Blizzard Entertainment names, marks, and game assets +remain the property of their respective rights holders. Other project names and +trademarks remain the property of their respective owners. + +See `PROJECT_IDENTITY.md` for the full project-identity notice. + +## Preservation rule + +Do not remove historical source comments, attribution notes, license notices, +compatibility explanations, or provenance records simply because newer +documentation summarizes them. + +When replacing or substantially rewriting third-party-derived material, update +the provenance record rather than erasing the earlier history. + + +## OEVG2 customization and BetterBlizzFrames texture + +This repository imports a source snapshot of Dusk/ShaguTweaks-ClassicAPI downloaded on 2026-10-24 and adds English-only loading and unit-frame customizations described in README.md. + +`img/BlizzardRetailBar.tga` was obtained from: +https://github.com/Bodify/BetterBlizzFrames/blob/main/media/blizzTex/BlizzardRetailBar.tga + +BetterBlizzFrames' repository license states **All Rights Reserved**, copyright (c) 2023-2026 Bodify. This asset is not covered by this repository's original ShaguTweaks MIT license. The source project's notice does not establish separate provenance or permissions for any underlying game artwork. + + +## Modern mana bar texture + +`img/BlizzardRetailBarCrop2.tga` is copied unchanged from BetterBlizzFrames: +https://github.com/Bodify/BetterBlizzFrames/blob/main/media/blizzTex/BlizzardRetailBarCrop2.tga + +Source Git blob: `bb412b3fa538a6c91671d06ccbfd1e820f4c2876`. +BetterBlizzFrames: All Rights Reserved, Copyright (c) 2023-2026 Bodify. +This asset retains its original rights and is not relicensed under MIT. +The mana tint follows BetterBlizzFrames' default `PowerBarColor.MANA` lookup: +RGB `(0, 0, 1)`. The selected texture is its `Blizzard Retail Bar Crop 2` preset. + + +## Native retail resource artwork + +`img/ModernPowerMana.tga`, `img/ModernPowerRage.tga`, `img/ModernPowerFocus.tga` and `img/ModernPowerEnergy.tga` are cropped/resized exports of Blizzard's `interface/hud/uiunitframe2x.blp` (FileDataID 4642466), client build 12.1.5.70448, retrieved from https://wago.tools/api/casc/4642466?version=12.1.5.70448 . + +Matching atlas metadata and conversion details are recorded in `Docs/MODERN_MANA.md`. These are Blizzard game assets, not original artwork or assets relicensed under the software's MIT license. The previous BetterBlizzFrames grayscale asset attribution remains part of the history. + + +### Frame-specific resource exports + +Additional img/ModernPower*Class.tga, *Target.tga, *Small.tga and *Party.tga variants use the matching Blizzard atlas crops documented in Docs/MODERN_MANA.md. Player/target/small variants come from FileDataID 4642466; party variants come from FileDataID 4681512, interface/hud/uipartyframe.blp. Both were retrieved for build 12.1.5.70448 from wago.tools. All resource exports now use bilinear resizing to compact power-of-two TGA dimensions. These remain Blizzard game artwork; no MIT relicensing is asserted. diff --git a/assets/textures/bars/ModernPowerEnergy.tga b/assets/textures/bars/ModernPowerEnergy.tga new file mode 100644 index 0000000000000000000000000000000000000000..a5a601eba727ee704c9b7e29309866341437f67c GIT binary patch literal 8236 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 AYybcN literal 0 HcmV?d00001 diff --git a/assets/textures/bars/ModernPowerFocus.tga b/assets/textures/bars/ModernPowerFocus.tga new file mode 100644 index 0000000000000000000000000000000000000000..51e6f9ff781d3c70124b644d189a02f8b85ef2db GIT binary patch literal 8236 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 B+SmX9 literal 0 HcmV?d00001 diff --git a/assets/textures/bars/ModernPowerManaClass.tga b/assets/textures/bars/ModernPowerManaClass.tga new file mode 100644 index 0000000000000000000000000000000000000000..ec75f776feeb54ef2039fe87fcbfaf73703a3c79 GIT binary patch literal 8236 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 CsZOf^ literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Puppeteer-Borderless.tga b/assets/textures/bars/Puppeteer-Borderless.tga new file mode 100644 index 0000000000000000000000000000000000000000..93386e37f7047ed82a648e9fe58a2fd99c8b0df1 GIT binary patch literal 3223 zcmeH~xo#9e5Qf_k5+Wi%knsXofJ9CLk|hKZmq-Z+NGt;6`7&`>5D9v*J7k&%(CXrAX~vQnwkV!rQZ zMGJx;lZ9c}Vo?-jMT_G&lO;*gV%FLWvokX@@!Z^;H$OigEi5efi;Ii?($Z4Ayu4gm zSy>5JS67p@wY6}4eLdLN*a$Z_H{-3Xtz>(9JKfpYNq2X5n|phEqj;^guXDxa@#MwI z2S<1bxWX4t2%mVxZ)Rs_BeeW;deCLi7o*cdZ-nkZ`l%zKo)UG1)E84{@`e6b`tp?i z$RF)bOnjogk}n(!o^bR2!(-qRZ_ba7GCC6VjI$UGfnwEDa)sf8Wra!at-GT{cLMh` z8sx3JqeSP5BVKvRx8Mj*xDoi`5nlrDU*%R0qys%AbVbtVdT-IK*$k;8rXJO0nL*X5 znN;2KTIc(mD^9Z+sb8-5!V|9W#go99!f9FSPV#g5QzE`h-r-$Gj!$=6g{KOix4*wH zUt|5HJ6FDNV({`hz!#5k{r+pb@O$X6=+PWR=+m=%I9Dls`;mIfh56DuM0mm#z7Y?6 z6@1E`@3`{kfxEHfrt~%~yk{D&w_|v{chuYboH)(0?$pxTTr+rOa1C5#IQ|$9d>*{1 zYSkU1Cq-AFUbuQj-E2;{dY-s?@_p*n3@cuF%Ga|gJR@B2D}R8arryCh3U{EMqNReS z;=5n-J?UzYuNUTBbE0@XC(2L3@q`C%UA3^Tupg;L;%dZ~-CYZJ96ZZ&B!7sj^D1$= zYd!DE7fuzt-+|eXW;m)&qwb}ShC25rjdip&(I_vC`M$e9X{?Z^{2Dk-yvMrEzUCk% zZVa3;@-)}e;ApNJthKN6GI2HHmDeEuS73~T<%3xVy8?%vA!&c+G)&>OF0-%m25}|g zo8(R6eb!^_H@}i6?M==VH$nW**ly?t3t%@LIoN|66CxVar19AwzXw$Fcy z{{Qg%J8;~bF&~bdGAGT6A0>Nq|6%9j_Ra^7&t7Uj-+tlZH}cQwtj+zl#8ut@9=C7a VxpKGTR>zI&=k8tYxOS!8d;&%a`$7N! literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Puppeteer-Flat-Borderless.tga b/assets/textures/bars/Puppeteer-Flat-Borderless.tga new file mode 100644 index 0000000000000000000000000000000000000000..39cf226305e46024f4a3c74deccd5a917feede39 GIT binary patch literal 556 vcmZQz;9`ISMh6B7hW~r^>>0%)Hw<8@AtWf&HOw>E)89`w!p+miRgVDxH_x8l literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Puppeteer-Flat.tga b/assets/textures/bars/Puppeteer-Flat.tga new file mode 100644 index 0000000000000000000000000000000000000000..8bdb8b93b3ba1bcab173462c9188e3bcf2f26738 GIT binary patch literal 556 zcmZQz;9`ISMh6B7hW~+qfnc1Lmi8Zn_w3m-iidF+z~UJi@37Pm5)|qh<{9ki@24B# K=IP_A#{d980hQ7K literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Puppeteer-Shineless-Borderless.tga b/assets/textures/bars/Puppeteer-Shineless-Borderless.tga new file mode 100644 index 0000000000000000000000000000000000000000..82507c684b521fc2df3ed1f3c5bbac024d57628a GIT binary patch literal 795 zcmeIvxlRIM5C`y&%7oHJ?RWt*=!I50SUtpQ52Clj*%n@*>1N;)0AXc1}@KY6h%SGvP4x?AGQ5G{_7uH zpw3R%`@s>bebrw^_vNkA6FRTG#-(s3oL~H~-r&93F2Btk?%w@y_t<)B-?wjXPM@#Z I*DZm40ed%9s35LgDilPCU$}NK;8Gk4N!olgNt>jP(l%+Eq)D1& z_`i;uub}tP4laU=ljri|4EJ6R=N!iBY!^+|WJe65C_<7X$g&JYQHZLl==b|%Fc?77 zG@|P|IKwbtnkE?zhu|#Bf^FNxaU5`?(Fo)5n7FPB&htF@zE38T38vF2nayUH&*vlv z0)$~m7K;Uz%OzQ@R^Xy2LLA2=NfNBrYm%lZvMeKco@2AwkfJESm1T*lstE7-tM`xE zZXW;n4<=A&$LxLYknOX}-&T9;ymos+_odgk5Ke`&^B?{<_+4##zP&rxuKVHcvGvrx PZ{OaWJYTi1TLSw8GjFV* literal 0 HcmV?d00001 diff --git a/assets/textures/bars/Puppeteer.tga b/assets/textures/bars/Puppeteer.tga new file mode 100644 index 0000000000000000000000000000000000000000..dc474f79320d87e838ff56ad86d4f4f6f285a452 GIT binary patch literal 3223 zcmeH~$!Zl*5Qa}c5Zt*_-1q_p72LZ~2#69V+`2H}MqCJ)hrT_Y+qqLNiHT97uFO2o zc?vqD!^15$GBT1C&GWoWRw|WR z%=i7QXh9HUvM>x=EQ+G6XmK28vLs1b%vzgac6N3)o}Zuh78Vww#l=N`X=%w{US5t@ zR#r-@tE=JK+FG){z8-FDYy_K|o8i{hR=mBvo$T!Fq`SMj>E7O6bANw-6tA`Rb*{KP zp1fH3;0P}PSNP%y;S;a;&D`8vgqD9!54sHcVsv`wjnExPKXoM3Q=+br`eN!#zR({_ zUnc2~{L%iz#3$-2`NFZ_2{-RQJO)1T=KSa=qa#tzIE&E`C{{fsR~SB6R+#kOx;sjA zCveZ8LEgGMN_4I`;+3a-3y$!F8-Xt#@g?y7Rc`e_I?z)>S0sI|_ZHon&5$}`>QP;m z8C0E`N!2Z{b-vHJ;xwC)`sI2rJmCsoJPDjBoR+ojBtNG=CF0BE9o}{1_;j~bc&hMu z2L}i8HP&CcbL9&s1~0DzeDN69@4vK&Y;a0lus zS}JHNzWX)bQ?3U2dSTu*CyLi|qWlybPk7+gRSWA1`;lrSu10*>-L-JX!LvL^@`t!O zuM(%b*7L4>;Z(u<9hePihNJ2<>R#$-sB?ePSVvnEjq=i%@4Nex#tM1LuYuFVd#vm1 zYYt-K#=sdPPjfv3j^?_-TKhUL6IUZ%c@6S^1;#j7KA3f|D{$x;lJ;j#!!%y&GW$Aj z5LY6;N!}FRXFbM#^DBAM-sD_y6U6^a&I@t_Fvh`peni_e>k0OC{*1WS_?Yz+`!CVH zNgrR~Ct2%n-wfy96EjC_g`DT)z6WC(uk{=Dzjd~ABxj5DJBRTWU;LbZztne3_nq@& z+vo41|3Ccx4xBKj&4=U1%t>?RN68-FfB5K0`=bX>&R%Li-+tlZH}cQwtj+zl#8ut@ Z9=C7axpKGTR>zI&=k8tYxOS!8d;$)+^=<$F literal 0 HcmV?d00001 diff --git a/assets/textures/roles/Damage.tga b/assets/textures/roles/Damage.tga new file mode 100644 index 0000000000000000000000000000000000000000..f7504a2d686f6ebc64d387f181736fdbf7e20f86 GIT binary patch literal 11976 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 literal 0 HcmV?d00001 diff --git a/assets/textures/roles/Tank.tga b/assets/textures/roles/Tank.tga new file mode 100644 index 0000000000000000000000000000000000000000..91f98e59b47acdbad4af5a9ad498f2ab4e41d4e7 GIT binary patch literal 12724 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 i_pkW>`M 0 and tostring(arg[1]) or "" + if table.getn(arg) > 1 then + for i = 2, table.getn(arg) do + str = str..tostring(arg[i]) + end + end + DEFAULT_CHAT_FRAME:AddMessage(str) + end +}, {__index = PTUtil}) +local targetedScript +local function targetedScriptFunc() + local ok, result = pcall(targetedScript) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] Error occurred while running custom script binding:", 1, 0.5, 0.5)) + DEFAULT_CHAT_FRAME:AddMessage(colorize(result, 1, 0.5, 0.5)) + end +end +local function setupTargetedScript(script) + targetedScript = script + return targetedScriptFunc +end +function RunBinding_Script(binding, unit, unitFrame) + local scriptString = binding.Data + if not BindingScriptCache[scriptString] then + local func, err = loadstring(preScript..scriptString, scriptString) + if not func then + DEFAULT_CHAT_FRAME:AddMessage(colorize("[Puppeteer] Failed to load binding function:", 1, 0.5, 0.5)) + DEFAULT_CHAT_FRAME:AddMessage(colorize(err, 1, 0.5, 0.5)) + return + end + setfenv(func, BindingEnvironment) + BindingScriptCache[scriptString] = func + end + BindingEnvironment.PTScriptUnit = PTUnitProxy and PTUnitProxy.CustomUnitGUIDMap[unit] or unit + BindingEnvironment.PTScriptUnitUnresolved = unit + BindingEnvironment.PTScriptUnitFrame = unitFrame + RunTargetedAction(binding, unit, setupTargetedScript(BindingScriptCache[scriptString])) +end + +MultiMenu = PTGuiLib.Get("dropdown", UIParent) + +local function RunMultiBindingElement(self) + local binding = self.binding + this.checked = not this.checked + self.checked = not self.checked -- Done to counter keepShownOnClick check toggle + if not binding.Data.KeepOpen then + MultiMenu:SetToggleState(false) + end + + + -- Hacks to make sure any dropdown menus that open while running the binding stay open + -- The root issue is that since the click function isn't finished running when another dropdown opens, + -- it tries to close the newly opened dropdown + local button + local buttonKeepShownOnClick -- The intended value + local realAddButton = _G.UIDropDownMenu_AddButton + if binding.Type ~= "SPELL" and binding.Type ~= "ITEM" then -- Spells and items can't possibly run into an issue + local selfIndex = util.IndexOf(self.siblings, self) + _G.UIDropDownMenu_AddButton = function(info, level) + realAddButton(info, level) + level = level or 1 + if level ~= 1 then + return + end + local listFrame = _G["DropDownList"..level] + local index = listFrame.numButtons + + if index == selfIndex then + button = _G[listFrame:GetName().."Button"..index] + buttonKeepShownOnClick = button.keepShownOnClick + button.keepShownOnClick = 1 + end + end + end + + -- It's rather important that we don't raise an error here + local ok, result = pcall(RunBinding, self.subBinding, self.unit, self.unitFrame) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("Error while running binding: "..result) + end + + -- Restore state to what it should be + _G.UIDropDownMenu_AddButton = realAddButton + if button then + util.RunLater(function() + button.keepShownOnClick = buttonKeepShownOnClick + if button.checked then + _G[button:GetName().."Check"]:Hide() + button.checked = nil + else + _G[button:GetName().."Check"]:Show() + button.checked = 1 + end + end) + end +end + +function RunBinding_Multi(binding, unit, unitFrame) + if MultiMenu.Options ~= nil then + compost:Reclaim(MultiMenu.Options, 1) + end + MultiMenu:SetToggleState(false) + local options = compost:GetTable() + local title = binding.Data.Title ~= "" and binding.Data.Title -- or GetBindingTooltipText(binding) + if title then + local titleColor = binding.Data.TitleColor or BindTypeTooltipColors[binding.Type] + table.insert(options, compost:AcquireHash( + "text", colorize(title, titleColor), + "isTitle", true, + "notCheckable", true, + "textHeight", 12 + )) + end + local list = binding.Data.Bindings + for _, subBinding in ipairs(list) do + local subBinding = subBinding + local display = compost:GetTable() + _UpdateBindingDisplay(subBinding, display) + table.insert(options, compost:AcquireHash( + "text", display.Normal, + "notCheckable", true, + "keepShownOnClick", true, -- This is used so that dropdowns shown during the func call don't get immediately hidden + "func", RunMultiBindingElement, + "binding", binding, + "subBinding", subBinding, + "unit", unit, + "unitFrame", unitFrame + )) + compost:Reclaim(display) + end + if binding.Data.KeepOpen then + table.insert(options, compost:AcquireHash( + "notCheckable", true, + "disabled", true + )) + table.insert(options, compost:AcquireHash( + "text", colorize("Close Menu", NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b), + "notCheckable", true, + "keepShownOnClick", true, + "func", function(self) + MultiMenu:SetToggleState(false) + end + )) + end + MultiMenu:SetOptions(options) + local container = unitFrame:GetRootContainer() + MultiMenu:SetToggleState(true, container, container:GetWidth(), container:GetHeight()) + MultiMenu:SetKeepOpen(true) + PlaySound("GAMESPELLBUTTONMOUSEDOWN") +end + +function RunBinding(binding, unit, unitFrame) + if binding.Data == nil then + return + end + local targetCastable = UnitExists(unit) and UnitIsConnected(unit) and UnitIsVisible(unit) + local bindingType = binding.Type + if bindingType == "SPELL" then + if targetCastable then + RunBinding_Spell(binding, unit) + end + elseif bindingType == "ACTION" then + RunBinding_Action(binding, unit, unitFrame) + elseif bindingType == "ITEM" then + if targetCastable then + RunBinding_Item(binding, unit) + end + elseif bindingType == "MACRO" then + RunBinding_Macro(binding, unit) + elseif bindingType == "SCRIPT" then + RunBinding_Script(binding, unit, unitFrame) + elseif bindingType == "MULTI" then + RunBinding_Multi(binding, unit, unitFrame) + end +end \ No newline at end of file diff --git a/core/Command.lua b/core/Command.lua new file mode 100644 index 0000000..7936a80 --- /dev/null +++ b/core/Command.lua @@ -0,0 +1,127 @@ +SLASH_PUPPETEER1 = "/puppeteer" +SLASH_PUPPETEER2 = "/pt" +if not IsAddOnLoaded("HealersMate") then + SLASH_PUPPETEER3 = "/hm" +end +SlashCmdList["PUPPETEER"] = function(args) + if args == "reset" then + for _, group in pairs(Puppeteer.UnitFrameGroups) do + local gc = group:GetContainer() + gc:ClearAllPoints() + gc:SetPoint(PTUtil.GetCenterScreenPoint(gc:GetWidth(), gc:GetHeight())) + end + PTSettingsGui.TabFrame:ClearAllPoints() + PTSettingsGui.TabFrame:SetPoint("CENTER", 0, 0) + Puppeteer.Info("Reset all frame positions.") + elseif args == "check" then + Puppeteer.CheckGroup() + elseif args == "update" then + for _, ui in ipairs(Puppeteer.AllUnitFrames) do + ui:SizeElements() + ui:UpdateAll() + end + for _, group in pairs(Puppeteer.UnitFrameGroups) do + group:ApplyProfile() + group:UpdateUIPositions() + end + elseif args == "testui" or string.find(args, "^testui %d") then + local _, _, countStr = string.find(args, "testui (%d+)") + local count = tonumber(countStr) or 40 + if count < 1 then count = 1 end + if count > 40 then count = 40 end + -- If a count is given while already on, just update the count + if countStr and PTOptions.TestUI then + Puppeteer.TestUICount = count + else + PTOptions.TestUI = not PTOptions.TestUI + Puppeteer.TestUI = PTOptions.TestUI + Puppeteer.TestUICount = count + end + if PTOptions.TestUI then + for _, ui in ipairs(Puppeteer.AllUnitFrames) do + ui.fakeStats = ui.GenerateFakeStats() + if ui:IsFake() then + ui:Show() + else + ui.container:Hide() + ui.rootContainer:Hide() + end + end + end + Puppeteer.CheckGroup() + if not PTOptions.TestUI and PTUnitProxy then + for _, type in ipairs(PTUnitProxy.CustomUnitTypes) do + PTUnitProxy.UpdateUnitTypeFrames(type) + end + end + Puppeteer.Info("UI Testing is now "..(not PTOptions.TestUI and + PTUtil.Colorize("off", 1, 0.6, 0.6) or PTUtil.Colorize("on", 0.6, 1, 0.6))..".") + elseif args == "toggle" then + PTOptions.Hidden = not PTOptions.Hidden + Puppeteer.CheckGroup() + Puppeteer.Info("The Puppeteer UI is now "..(PTOptions.Hidden and + PTUtil.Colorize("hidden", 1, 0.6, 0.6) or PTUtil.Colorize("shown", 0.6, 1, 0.6))..".") + elseif args == "show" then + PTOptions.Hidden = false + Puppeteer.CheckGroup() + Puppeteer.Info("The Puppeteer UI is now "..(PTOptions.Hidden and + PTUtil.Colorize("hidden", 1, 0.6, 0.6) or PTUtil.Colorize("shown", 0.6, 1, 0.6))..".") + elseif args == "hide" then + PTOptions.Hidden = true + Puppeteer.CheckGroup() + Puppeteer.Info("The Puppeteer UI is now "..(PTOptions.Hidden and + PTUtil.Colorize("hidden", 1, 0.6, 0.6) or PTUtil.Colorize("shown", 0.6, 1, 0.6))..".") + elseif args == "roles" then + local group = Puppeteer.UnitFrameGroups[Puppeteer.CurrentlyInRaid and "Raid" or "Party"] + local chatType = Puppeteer.CurrentlyInRaid and "RAID" or "PARTY" + local tanks = {} + local healers = {} + for _, ui in group.uis do + if UnitIsPlayer(ui:GetUnit()) then + local role = ui:GetRole() + local name = UnitName(ui:GetUnit()) + if role == "Tank" then + table.insert(tanks, name) + elseif role == "Healer" then + table.insert(healers, name) + end + end + end + SendChatMessage("Puppeteer -- Assigned Roles", chatType) + SendChatMessage("Tanks("..table.getn(tanks).."): "..table.concat(tanks, ", "), chatType) + SendChatMessage("Healers("..table.getn(healers).."): "..table.concat(healers, ", "), chatType) + elseif args == "silent" then + PTGlobalOptions.ShowLoadMessage = not PTGlobalOptions.ShowLoadMessage + Puppeteer.Info("Load message is now "..(PTGlobalOptions.ShowLoadMessage and + PTUtil.Colorize("on", 0.6, 1, 0.6) or PTUtil.Colorize("off", 1, 0.6, 0.6))..".") + elseif args == "help" or args == "?" then + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt", 0, 0.8, 0).." -- Opens the addon configuration") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt reset", 0, 0.8, 0).." -- Resets all frame positions") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt testui [count]", 0, 0.8, 0).. + " -- Toggles fake players (1-40, default 40)") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt toggle", 0, 0.8, 0).." -- Shows/hides the UI") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt show", 0, 0.8, 0).." -- Shows the UI") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt hide", 0, 0.8, 0).." -- Hides the UI") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt roles", 0, 0.8, 0).." -- Broadcast the roles you have assigned to chat") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt silent", 0, 0.8, 0).." -- Turns off/on message when addon loads") + DEFAULT_CHAT_FRAME:AddMessage(PTUtil.Colorize("/pt mana", 0, 0.8, 0).." -- Reports raid mana when in a raid. You must be raid leader") + elseif args == "importhm" then + Puppeteer.ImportHealersMateSettings() + Puppeteer.Info("Imported HealersMate settings") + elseif args == "mana" then + if not PTOptions.ShowRaidMana then + DEFAULT_CHAT_FRAME:AddMessage("Raid mana is disabled. Enable it in Puppeteer -> Options -> Other -> Show Raid Mana") + return + end + if UnitInRaid("player") and (IsRaidLeader() or IsRaidOfficer()) then + local raidFrameGroup = Puppeteer.UnitFrameGroups["Raid"] + raidFrameGroup:ReportRaidMana() + else + DEFAULT_CHAT_FRAME:AddMessage("You must be the raid leader to use this command.") + end + elseif args == "" then + PTSettingsGui.TabFrame:Show() + else + Puppeteer.Info("Unknown subcommand. See usage with /pt help") + end +end \ No newline at end of file diff --git a/core/EnemyTracker.lua b/core/EnemyTracker.lua new file mode 100644 index 0000000..a1fe360 --- /dev/null +++ b/core/EnemyTracker.lua @@ -0,0 +1,111 @@ +-- An experimental feature that tracks enemies + +local util = PTUtil + +if not util.IsSuperWowPresent() then + return +end + +util.SetEnvironment(Puppeteer) + +local function isUnitRelevant(unit) + return UnitCanAttack("player", unit) and UnitHealth(unit) > 0 and UnitIsConnected(unit) and UnitExists(unit) + and (UnitAffectingCombat(unit) or UnitIsUnit(unit, "target") or UnitIsUnit(unit, "pettarget")) +end + +local isUnitNearby = util.CanClientGetPreciseDistance(true) and +function(unit) + return util.GetDistanceTo(unit) < 60 +end +or +function() return true end + +local f = CreateFrame("Frame", "PTEnemyUpdater") + +f:SetScript("OnEvent", function() + local unit = arg1 + if event == "PLAYER_TARGET_CHANGED" then + unit = "target" + elseif event == "UPDATE_MOUSEOVER_UNIT" then + unit = UnitGUID("mouseover") + end + + if UnitCanAttack("player", unit) and not UnitIsCharmed("player") and (UnitAffectingCombat(unit) or (unit == "target" and UnitHealth(unit) > 0)) then + if not CustomUnitsMap["enemy"] then + return + end + + local guid = UnitGUID(unit) + if IsGuidUnitType(guid, "enemy") then -- Already enemy + return + end + + local enemyUnits = CustomUnitsMap["enemy"] + if not enemyUnits then + return + end + + if not util.IsReallyInInstance() and (not UnitAffectingCombat("player") or not isUnitNearby(unit)) + and (unit ~= "target" or not UnitCanAttack("player", unit)) then + return + end + + for _, enemyUnit in ipairs(enemyUnits) do + if CustomUnitGUIDMap[enemyUnit] then + if not isUnitRelevant(enemyUnit) then + SetCustomUnitGuid(enemyUnit, nil) + SetCustomUnitGuid(enemyUnit, guid) + return + end + end + end + + SetGuidUnitType(guid, "enemy") + end +end) + +local nextUpdate = GetTime() + 0.25 +local EnemyTracker_OnUpdate = function() + if GetTime() > nextUpdate then + nextUpdate = nextUpdate + 0.25 + + local enemyUnits = CustomUnitsMap["enemy"] + if not enemyUnits then + return + end + local maxEnemies = table.getn(enemyUnits) + local lastIndex = 0 + for i, unit in ipairs(enemyUnits) do + if CustomUnitGUIDMap[unit] then + if isUnitRelevant(unit) then + lastIndex = i + end + end + end + + for i = lastIndex + 1, maxEnemies - lastIndex do + local enemyUnit = enemyUnits[i] + if CustomUnitGUIDMap[enemyUnit] then + SetCustomUnitGuid(enemyUnit, nil) + end + end + end +end + +function SetEnemyTrackingEnabled(trackEnemies) + if trackEnemies then + f:RegisterEvent("UNIT_COMBAT") + f:RegisterEvent("UNIT_FLAGS") + f:RegisterEvent("PLAYER_TARGET_CHANGED") + f:RegisterEvent("UPDATE_MOUSEOVER_UNIT") + f:SetScript("OnUpdate", EnemyTracker_OnUpdate) + else + f:UnregisterAllEvents() + f:SetScript("OnUpdate", nil) + for _, unit in ipairs(CustomUnitsMap["enemy"]) do + SetCustomUnitGuid(unit, nil) + end + end +end + +RegisterCustomUnits("enemy", 40) \ No newline at end of file diff --git a/core/EventHandler.lua b/core/EventHandler.lua new file mode 100644 index 0000000..4d8eb2d --- /dev/null +++ b/core/EventHandler.lua @@ -0,0 +1,164 @@ +PTUtil.SetEnvironment(Puppeteer) +local _G = getfenv(0) +local util = PTUtil + +EventHandlerFrames = {} +local function RegisterEventHandler(events, handler) + if type(events) == "string" then + events = {events} + end + local frame = CreateFrame("Frame", "PTEventHandler_"..events[1]) + for _, event in ipairs(events) do + frame:RegisterEvent(event) + end + frame:SetScript("OnEvent", handler) + table.insert(EventHandlerFrames, frame) +end + +function UnregisterEventHandlers() + for _, frame in ipairs(EventHandlerFrames) do + frame:UnregisterAllEvents() + end +end + +RegisterEventHandler("ADDON_LOADED", function() + if arg1 ~= "Puppeteer" then + return + end + GuidRoster = PTGuidRoster + OnAddonLoaded() +end) +RegisterEventHandler("PLAYER_ENTERING_WORLD", function() + CheckGroup() + + if PTOptions.DisablePartyFrames.InParty then + SetPartyFramesEnabled(false) + end + + PromptHealersMateImport() +end) +RegisterEventHandler({"UNIT_HEALTH", "UNIT_MAXHEALTH"}, function() + local unit = arg1 + if not IsRelevantUnit(unit) then + return + end + for ui in UnitFrames(unit) do + ui:UpdateHealth() + end +end) +RegisterEventHandler({"UNIT_MANA", "UNIT_RAGE", "UNIT_ENERGY", "UNIT_FOCUS", "UNIT_MAXMANA", "UNIT_DISPLAYPOWER"}, function() + local unit = arg1 + if not IsRelevantUnit(unit) then + return + end + + for ui in UnitFrames(unit) do + ui:UpdatePower() + end + + if unit == "player" then + ReapplySpellsTooltip() + end + + local raidFrameGroup = UnitFrameGroups["Raid"] + if raidFrameGroup then + raidFrameGroup:RequestRaidManaUpdate() + end +end) +RegisterEventHandler("UNIT_AURA", function() + local unit = arg1 + if not IsRelevantUnit(unit) then + return + end + if unit == "player" then + util.MarkSpellCostCacheDirty() + end + PTUnit.Get(unit):UpdateAuras() + for ui in UnitFrames(unit) do + ui:UpdateAuras() + ui:UpdateHealth() -- Update health because there may be an aura that changes health bar color + end +end) +RegisterEventHandler({"PARTY_MEMBERS_CHANGED", "RAID_ROSTER_UPDATE"}, function() + CheckGroupThrottled() +end) +RegisterEventHandler({"UNIT_PET", "PLAYER_PET_CHANGED"}, function() + local unit = arg1 + if IsRelevantUnit(unit) then + CheckGroupThrottled() + end +end) +RegisterEventHandler("PLAYER_TARGET_CHANGED", function() + for _, ui in ipairs(AllUnitFrames) do + ui:EvaluateTarget() + end + local exists, guid = UnitExists("target") + if util.IsSuperWowPresent() then + PTUnit.UpdateGuidCaches() + end + + PTUnit.Get("target"):UpdateAll() + if util.IsSuperWowPresent() then + GuidRoster.SetUnitGuid("target", guid) + PTHealPredict.SetRelevantGUIDsFromMap(GuidRoster.GuidUnitMap) + end + + if exists then + EvaluateTracking("target", true) + end + + if PTOptions.Hidden then + return + end + + CheckTarget() +end) +RegisterEventHandler("SPELLS_CHANGED", function() + PuppeteerSettings.UpdateTrackedDebuffTypes() + util.MarkSpellCostCacheDirty() +end) +RegisterEventHandler("CHARACTER_POINTS_CHANGED", function() + util.MarkSpellCostCacheDirty() +end) +RegisterEventHandler("RAID_TARGET_UPDATE", function() + for _, ui in ipairs(AllUnitFrames) do + ui:UpdateRaidMark() + end +end) +RegisterEventHandler("UNIT_FACTION", function() + if not IsRelevantUnit(arg1) then + return + end + for ui in UnitFrames(arg1) do + ui:UpdatePVP() + end +end) +RegisterEventHandler("PLAYER_LOGOUT", function() + RemoveOverrideBindings() + PuppeteerSettings.SaveFramePositions() +end) + +local GetKeyModifier = util.GetKeyModifier +local keyListener = CreateFrame("Frame", "PTTooltipKeyListener") +local lastModifier = "None" +local function PTTooltipKeyListener_OnUpdate() + local modifier = GetKeyModifier() + if lastModifier ~= modifier then + lastModifier = modifier + if SpellsTooltip:IsVisible() then + ReapplySpellsTooltip() + end + end +end + +function SetTooltipKeyListenerEnabled(enabled) + if keyListener:GetScript("OnUpdate") ~= nil == enabled then -- Don't set the script if the state is unchanged + return + end + if enabled then + lastModifier = GetKeyModifier() + keyListener:SetScript("OnUpdate", PTTooltipKeyListener_OnUpdate) + else + keyListener:SetScript("OnUpdate", nil) + end +end \ No newline at end of file diff --git a/core/OverrideBindings.lua b/core/OverrideBindings.lua new file mode 100644 index 0000000..2ea48b9 --- /dev/null +++ b/core/OverrideBindings.lua @@ -0,0 +1,211 @@ +-- Handles mouse wheel and key bindings + +PTUtil.SetEnvironment(Puppeteer) +local _G = getfenv(0) +local util = PTUtil + +_G.BINDING_HEADER_PUPPETEER = "Puppeteer (Don't Touch These)" +local bindingsNames = {} +local MAX_BINDINGS = 24 +for i = 1, MAX_BINDINGS do + bindingsNames[i] = "PUPPETEERBINDING"..i + _G["BINDING_NAME_"..bindingsNames[i]] = "Dynamic Binding "..i +end +local bindingsNamesSet = util.ToSet(bindingsNames) + +local BindingPrefixes = { + "", + "SHIFT-", + "CTRL-", + "ALT-", + "ALT-CTRL-SHIFT-", + "ALT-CTRL-", + "ALT-SHIFT-", + "CTRL-SHIFT-" +} + +StoredBindings = {} -- Key: Baked keybind key(like "ALT-SHIFT-R") | Value: The stored value of the binding +IndexButtonMap = {} -- Key: Index corresponding to the binding | Value: Button name +KeybindIndexMap = {} -- Key: Baked keybind key | Value: Index corresponding to the binding + +function InitOverrideBindingsMapping() + util.ClearTable(IndexButtonMap) + util.ClearTable(KeybindIndexMap) + local index = 1 + for _, button in ipairs(PTOptions.Buttons) do + if not util.GetAllButtonsSet()[button] then + for _, prefix in ipairs(BindingPrefixes) do + KeybindIndexMap[prefix..button] = index + end + IndexButtonMap[index] = button + index = index + 1 + end + end + + for _, bindingName in ipairs(bindingsNames) do + local k1, k2, k3, k4, k5, k6, k7, k8 = GetBindingKey(bindingName) + for _, key in ipairs({k1, k2, k3, k4, k5, k6, k7, k8}) do + if key then + SetBinding(key, nil) + end + end + end +end + + +-- Stuff to stop expensive UPDATE_BINDINGS events as much as possible + +local noOp = function() end +local updateBindingsFunctions = {} +local updateBindingsFrames = {} +local holdingFunctionsHostage = false +function AddUpdateBindingsFunction(funcName) + table.insert(updateBindingsFunctions, { + funcName = funcName + }) +end +function AddUpdateBindingsFrame(frame) + frame = type(frame) ~= "string" and frame or _G[frame] + if frame then + table.insert(updateBindingsFrames, { + frame = frame + }) + end +end +-- Alternative method +function HookUpdateBindingsFrame(frameName) + local frame = _G[frameName] + local realScript = frame:GetScript("OnEvent") + frame:SetScript("OnEvent", function() + if holdingFunctionsHostage then + return + end + realScript() + end) +end +AddUpdateBindingsFunction("ActionButton_OnEvent") +AddUpdateBindingsFunction("BonusActionButton_OnEvent") +AddUpdateBindingsFunction("PetActionButton_OnEvent") +AddUpdateBindingsFunction("CharacterMicroButton_OnEvent") +AddUpdateBindingsFunction("TalentMicroButton_OnEvent") +AddUpdateBindingsFrame("MainMenuMicroButton") +AddUpdateBindingsFrame("QuestLogMicroButton") +AddUpdateBindingsFrame("SocialsMicroButton") +AddUpdateBindingsFrame("WorldMapMicroButton") +util.RunLater(function() + AddUpdateBindingsFrame("pfActionBar") -- pfUI + AddUpdateBindingsFrame("DFRL_HotkeyBinding") -- Dragonflight Reloaded + if DRAGONFLIGHT then -- DF3 + AddUpdateBindingsFrame((function() DRAGONFLIGHT() return DF.setups.actionbars.eventFrame end)()) + end +end) + +local function StopUpdateBindingsUpdates() + if holdingFunctionsHostage then + return + end + holdingFunctionsHostage = true + for _, entry in ipairs(updateBindingsFunctions) do + entry.func = _G[entry.funcName] + _G[entry.funcName] = noOp + end + for _, entry in ipairs(updateBindingsFrames) do + entry.func = entry.frame:GetScript("OnEvent") + entry.frame:SetScript("OnEvent", nil) + end +end + +local function StartUpdateBindingsUpdates() + if not holdingFunctionsHostage then + return + end + for _, entry in ipairs(updateBindingsFunctions) do + _G[entry.funcName] = entry.func + end + for _, entry in ipairs(updateBindingsFrames) do + entry.frame:SetScript("OnEvent", entry.func) + end + holdingFunctionsHostage = false +end + +-- End of UPDATE_BINDINGS mitigation + +function ApplyOverrideBindings() + RemoveOverrideBindings() + StopUpdateBindingsUpdates() + for fullButton, index in pairs(KeybindIndexMap) do + local binding = GetBindingAction(fullButton) + + if binding == "" then -- Lazy set the binding to our binding and don't touch until setting up again + SetBinding(fullButton, bindingsNames[index]) + elseif not bindingsNamesSet[binding] then -- If it's not our binding, it must be stored and replaced + StoredBindings[fullButton] = binding + SetBinding(fullButton, bindingsNames[index]) + end + end + StartUpdateBindingsUpdates() +end + +function RemoveOverrideBindings() + if util.IsTableEmpty(StoredBindings) then + return + end + StopUpdateBindingsUpdates() + for button, binding in pairs(StoredBindings) do + SetBinding(button, binding) + end + StartUpdateBindingsUpdates() + util.ClearTable(StoredBindings) +end + +function HandleKeyPress(index) + if not Mouseover then + return + end + local button = IndexButtonMap[index] + if keystate == "down" then + CurrentlyHeldButton = button + MouseoverFrame.pressed = true + MouseoverFrame:AdjustHealthPosition() + if button == "MOUSEWHEELUP" or button == "MOUSEWHEELDOWN" then + FakeMouseWheelHold(button) + end + ReapplySpellsTooltip() + if PTOptions.CastWhenKey == "Key Down" then + UnitFrame_OnClick(button, Mouseover, MouseoverFrame) + end + else + if button ~= "MOUSEWHEELUP" and button ~= "MOUSEWHEELDOWN" then + CurrentlyHeldButton = nil + MouseoverFrame.pressed = false + MouseoverFrame:AdjustHealthPosition() + ReapplySpellsTooltip() + end + if PTOptions.CastWhenKey == "Key Up" then + UnitFrame_OnClick(button, Mouseover, MouseoverFrame) + end + end +end + +MouseWheelHeldFaker = CreateFrame("Frame", "PTMouseWheelHeldFaker") + +local unholdAt +local unholdButton +local MouseWheelHeldFaker_OnUpdate = function() + if GetTime() >= unholdAt then + MouseWheelHeldFaker:SetScript("OnUpdate", nil) + if CurrentlyHeldButton == unholdButton then + CurrentlyHeldButton = nil + if MouseoverFrame then + MouseoverFrame.pressed = false + MouseoverFrame:AdjustHealthPosition() + end + ReapplySpellsTooltip() + end + end +end +function FakeMouseWheelHold(button) + unholdAt = GetTime() + 0.08 + unholdButton = button + MouseWheelHeldFaker:SetScript("OnUpdate", MouseWheelHeldFaker_OnUpdate) +end \ No newline at end of file diff --git a/core/Roles.lua b/core/Roles.lua new file mode 100644 index 0000000..bac0779 --- /dev/null +++ b/core/Roles.lua @@ -0,0 +1,402 @@ +PTUtil.SetEnvironment(Puppeteer) +local _G = getfenv(0) +local util = PTUtil +local colorize = util.Colorize +local GetColoredRoleText = util.GetColoredRoleText +local SplitString = util.SplitString + +AssignedRoles = nil + +LFTAutoRoleFrame = CreateFrame("Frame", "PT_LFTAutoRoleFrame") +function SetLFTAutoRoleEnabled(enabled) + if not util.IsClient1181() then + return + end + + if not enabled then + LFTAutoRoleFrame:SetScript("OnEvent", nil) + LFTAutoRoleFrame:SetScript("OnUpdate", nil) + LFTAutoRoleFrame:UnregisterAllEvents() + return + end + + if LFTAutoRoleFrame:GetScript("OnEvent") ~= nil then -- Already enabled + return + end + + local roleMap = { + ["t"] = "Tank", + ["h"] = "Healer", + ["d"] = "Damage" + } + local offerCompleteTime = -1 -- The time the offer is completed, used to verify rolecheck info is valid + local alreadyAssigned = {} -- The Tank & Healer names + local scanDPSTime = -1 + local AutoRoleFrame_OnUpdate = function() + if scanDPSTime > GetTime() then + return + end + + for _, unit in ipairs(PartyUnits) do + local name = UnitName(unit) + if name ~= "Unknown" and not util.ArrayContains(alreadyAssigned, name) then + print("Assuming "..name.." is Damage") + SetAssignedRole(name, "Damage") + end + end + UpdateUnitFrameGroups() + LFTAutoRoleFrame:SetScript("OnUpdate", nil) + end + LFTAutoRoleFrame:RegisterEvent("CHAT_MSG_ADDON") + LFTAutoRoleFrame:SetScript("OnEvent", function() + if arg1 == (LFT_ADDON_PREFIX or "TW_LFG") then + -- After an offer is complete, it is immediately followed by rolecheck info for the tank and healer + if strfind(arg2, "S2C_ROLECHECK_INFO") then + if GetNumPartyMembers() == 4 then + local params = util.SplitString(arg2, ";") + local member = params[2] + params = util.SplitString(params[3], ":") -- get confirmed roles + if offerCompleteTime + 0.5 > GetTime() and table.getn(params) == 1 then + local role = roleMap[params[1]] + table.insert(alreadyAssigned, member) + SetAssignedRole(member, role) -- Tank & Healer is sent after the offer is complete + print("Assigning "..member.." to "..role) + if table.getn(alreadyAssigned) == 2 then -- Set rest as Damage after Tank & Healer is sent + -- Delay scanning party members because their names might not be loaded yet + scanDPSTime = GetTime() + 1.5 + LFTAutoRoleFrame:SetScript("OnUpdate", AutoRoleFrame_OnUpdate) + end + UpdateUnitFrameGroups() + end + end + elseif strfind(arg2, "S2C_OFFER_COMPLETE") then + alreadyAssigned = {} + offerCompleteTime = GetTime() + end + end + end) +end + +function GetAssignedRole(name) + if not AssignedRoles or not AssignedRoles[name] then + return + end + AssignedRoles[name]["lastSeen"] = time() + return AssignedRoles[name]["role"] +end + +function GetUnitAssignedRole(unit) + if not UnitIsPlayer(unit) then + return + end + return GetAssignedRole(UnitName(unit)) +end + +function SetAssignedRole(name, role) + if role == nil or role == "No Role" then + AssignedRoles[name] = nil + return + end + AssignedRoles[name] = { + ["role"] = role, + ["lastSeen"] = time() + } +end + +-- Returns true if role assignment failed +function SetUnitAssignedRole(unit, role) + if not UnitIsPlayer(unit) then + return true + end + SetAssignedRole(UnitName(unit), role) +end + +function PruneAssignedRoles() + local currentTime = time() + for name, data in pairs(AssignedRoles) do + if not data["lastSeen"] or data["lastSeen"] < currentTime - (24 * 60 * 60) then + AssignedRoles[name] = nil + --print("Pruned "..name.."'s role") + end + end +end + +function SetRoleAndUpdate(name, role) + SetAssignedRole(name, role) + UpdateUnitFrameGroups() +end + +function SetUnitRoleAndUpdate(unit, role) + if not SetUnitAssignedRole(unit, role) then + UpdateUnitFrameGroups() + end +end + +-- Players will be considered as the role in the index if they have the highest talent points in said index. +-- Classes not listed have only DPS specs and are not bothered to be scanned. +TalentCountRoleMap = { + PRIEST = { + "Damage", "Healer", "Damage" + }, + PALADIN = { + "Healer", "Tank", "Damage" + }, + WARRIOR = { + "Damage", "Damage", "Tank" + }, + SHAMAN = { + "Damage", "Damage", "Healer" + }, + DRUID = { -- Spec #2(Feral) will be swapped to Tank if Thick Hide talent is found + "Damage", "Damage", "Healer" + } +} + + +local processingRoles = false +local isHooked = false +local function hookTalentMessageHandler() + if not isHooked and InspectTalentsFrame_HandleMessage then + isHooked = true + InspectTalentsFrame_HandleMessage_Original = InspectTalentsFrame_HandleMessage + _G.InspectTalentsFrame_HandleMessage = function(message, sender) + -- Only pass through talent requests while scanning talents + if not processingRoles or strfind(message, "INSTalentShow", 1, true) then + InspectTalentsFrame_HandleMessage_Original(message, sender) + end + end + end +end + +local function disableTalentMessageProcessing() + if not IsAddOnLoaded("Blizzard_InspectUI") or not IsAddOnLoaded("Blizzard_TalentUI") then + LoadAddOn("Blizzard_InspectUI") + LoadAddOn("Blizzard_TalentUI") + end + hookTalentMessageHandler() + processingRoles = true +end + +local function enableTalentMessageProcessing() + processingRoles = false +end + +local PlayerTalentData = {} +local SCAN_TIMEOUT = 4 +local scanTimeoutAt +local talentScanner = CreateFrame("Frame", "PTTalentScanner") +local function TalentScanner_OnUpdate() + if GetTime() >= scanTimeoutAt then + talentScanner:SetScript("OnUpdate", nil) + util.ClearTable(PlayerTalentData) + enableTalentMessageProcessing() + end +end +talentScanner:RegisterEvent("CHAT_MSG_ADDON") +talentScanner:SetScript("OnEvent", function() + if arg1 == "TW_CHAT_MSG_WHISPER" then + local message = arg2 + local sender = arg4 + + if not PlayerTalentData[sender] then + return + end + + if string.find(message, "INSTalentTabInfo;", 1, true) then + -- This is sent right before receiving info for individual talents in the tree + local split = SplitString(message, ';') + local index = tonumber(split[2]) + local pointsSpent = tonumber(split[4]) + + PlayerTalentData[sender].trees[index] = {points = pointsSpent, talents = {}} + elseif string.find(message, "INSTalentInfo;", 1, true) then + local split = SplitString(message, ';') + + local tree = tonumber(split[2]) + local tier = tonumber(split[5]) + local column = tonumber(split[6]) + local currRank = tonumber(split[7]) + + local cache = PlayerTalentData[sender] + local talents = cache.trees[tree].talents + talents[tier.."-"..column] = currRank + elseif string.find(message, "INSTalentEND;", 1, true) then + local data = PlayerTalentData[sender] + local trees = data.trees + local mostPoints = 0 + local mostIndex = 0 + for i = 1, 3 do + if trees[i] and trees[i].points > mostPoints then -- TODO: Error + mostPoints = trees[i].points + mostIndex = i + end + end + local class = data.class + if mostIndex > 0 then + -- Check for Druid Thick Hide talent, set as tank if they have it + if class == "DRUID" and mostIndex == 2 and (trees[2].talents["2-3"] or 0) > 0 then + SetRoleAndUpdate(sender, "Tank") + else + SetRoleAndUpdate(sender, mostPoints > 0 and TalentCountRoleMap[class][mostIndex] or "Damage") + end + end + PlayerTalentData[sender] = nil + if util.IsTableEmpty(PlayerTalentData) then + scanTimeoutAt = 0 -- Re-enable inspect comm next frame + end + end + end +end) + +local function requestTalents(name) + if name == UnitName("player") then + if InspectTalentsFrame_HandleMessage then + -- Send our own talents to ourself (lol) + InspectTalentsFrame_HandleMessage("INSTalentShow", UnitName("player")) + return + end + end + SendAddonMessage("TW_CHAT_MSG_WHISPER<"..name..">", "INSTalentShow", "GUILD") +end + +local function startTalentScan(name, class) + PlayerTalentData[name] = {class = class, trees = {}} + talentScanner:SetScript("OnUpdate", TalentScanner_OnUpdate) + scanTimeoutAt = GetTime() + SCAN_TIMEOUT + disableTalentMessageProcessing() + requestTalents(name) +end + +function AutoRole(unit) + local class = util.GetClass(unit) + if not TalentCountRoleMap[class] then + SetUnitRoleAndUpdate(unit, "Damage") + return + end + if not UnitIsConnected(unit) then -- Can't request offline player's talents + return + end + startTalentScan(UnitName(unit), class) +end + +function AutoRoleByNameClass(name, class) + if not TalentCountRoleMap[class] then + SetRoleAndUpdate(name, "Damage") + return + end + startTalentScan(name, class) +end + +RoleAssignInfo = {} + +RoleDropdown = PTGuiLib.Get("dropdown", UIParent) + +function InitRoleDropdown() + local initFunc = function(self) + self.checked = (GetAssignedRole(RoleAssignInfo.Name) or "No Role") == self.role + end + + local genRole = function(role) + return { + text = GetColoredRoleText(role), + role = role, + initFunc = initFunc, + func = function(info) + SetAssignedRole(RoleAssignInfo.Name, info.role) + UpdateUnitFrameGroups() + end + } + end + local massRoleFunc = function(info) + if not RoleAssignInfo.FrameGroup then + return + end + for _, ui in pairs(RoleAssignInfo.FrameGroup.uis) do + if (not ui:GetRole() or not info.role) and UnitIsPlayer(ui:GetUnit()) then + SetAssignedRole(UnitName(ui:GetUnit()), info.role) + end + end + UpdateUnitFrameGroups() + RoleDropdown:SetToggleState(false) + end + local genMassRole = function(role) + return { + text = GetColoredRoleText(role), + role = role, + notCheckable = true, + func = massRoleFunc + } + end + + local options = { + { + initFunc = function(self) + self.text = colorize(RoleAssignInfo.Name.."'s Role", RoleAssignInfo.ClassColor) + end, + notCheckable = true, + disabled = true, + textHeight = 12 + }, + genRole("Tank"), + genRole("Healer"), + genRole("Damage"), + genRole("No Role"), + { + notCheckable = true, + disabled = true + }, { + text = "Set Unassigned As", + tooltipTitle = "Set Unassigned As", + tooltipText = "Mass-set the roles of unassigned players. Only applies to players contained in this UI group.", + notCheckable = true, + textHeight = 11, + children = { + genMassRole("Tank"), + genMassRole("Healer"), + genMassRole("Damage") + } + }, { + text = "Clear Roles", + tooltipTitle = "Clear Roles", + tooltipText = "Clear all players' roles. Only applies to players contained in this UI group.", + notCheckable = true, + textHeight = 11, + func = massRoleFunc + } + } + if PuppeteerSettings.IsExperimentEnabled("AutoRole") then + table.insert(options, 6, { + text = colorize("Auto Detect", 1, 0.6, 0), + func = function() + if not RoleAssignInfo.FrameGroup then + return + end + AutoRoleByNameClass(RoleAssignInfo.Name, RoleAssignInfo.Class) + end + }) + local lastMassRole = 0 + table.insert(options, 9, { + text = "Auto Detect Unassigned", + tooltipTitle = "Auto Detect Unassigned", + tooltipText = "Automatically detect the roles of unassigned players. Only applies to players contained in this UI group.", + notCheckable = true, + textHeight = 11, + func = function() + if not RoleAssignInfo.FrameGroup then + return + end + if lastMassRole + 6 > GetTime() then + Puppeteer.Info("Please wait a moment before requesting roles again") + return + end + lastMassRole = GetTime() + for _, ui in pairs(RoleAssignInfo.FrameGroup.uis) do + if UnitIsPlayer(ui:GetUnit()) and not GetUnitAssignedRole(ui:GetUnit()) then + AutoRole(ui:GetUnit()) + end + end + end + }) + end + RoleDropdown:SetOptions(options) +end \ No newline at end of file diff --git a/core/SpellsTooltip.lua b/core/SpellsTooltip.lua new file mode 100644 index 0000000..12ab492 --- /dev/null +++ b/core/SpellsTooltip.lua @@ -0,0 +1,364 @@ +PTUtil.SetEnvironment(Puppeteer) +local _G = getfenv(0) +local compost = PTPool +local util = PTUtil +local colorize = util.Colorize +local GetKeyModifier = util.GetKeyModifier +local GetClass = util.GetClass +local GetItemCount = util.GetItemCount +local IsValidMacro = util.IsValidMacro +local GetResourceCost = util.GetResourceCost +local T = PTLocale.T + +local lowToHighColors = { + {1, 0, 0}, + {1, 0.9, 0}, + {0.35, 1, 0.35} +} +local tooltipPowerColors = { + ["mana"] = {0.5, 0.7, 1}, -- Not the accurate color, but more readable + ["rage"] = {1, 0, 0}, + ["energy"] = {1, 1, 0} +} +local tooltipAnchorMap = {["Top Left"] = "ANCHOR_LEFT", ["Top Right"] = "ANCHOR_RIGHT", + ["Bottom Left"] = "ANCHOR_BOTTOMLEFT", ["Bottom Right"] = "ANCHOR_BOTTOMRIGHT"} + +local tooltipCastsColors = { + ["Normal"] = {0.6, 1, 0.6}, + ["Critical"] = {1, 1, 0}, + ["Zero"] = {1, 0.5, 0.5} +} +local normalFontColor = {NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b} +local unboundText = colorize("Unbound", 0.6, 0.6, 0.6) + +local targetTypes = {"Friendly", "Hostile"} +ResurrectDisplayCache = {} +ButtonDisplayCache = nil +BindingDisplayCache = nil +DirtyDisplayModifiers = {} + +SpecialSpellBindings = {} +for class, spell in pairs(util.ResurrectionSpells) do + local binding = { + ["Type"] = "SPELL", + ["Data"] = spell + } + SpecialSpellBindings[spell] = binding +end +SpecialSpellBindings[T("Revive Champion")] = { + ["Type"] = "SPELL", + ["Data"] = T("Revive Champion") +} + +BindTypeTooltipColors = { + ["SPELL"] = normalFontColor, + ["ACTION"] = normalFontColor, + ["MACRO"] = {1, 0.6, 1}, + ["ITEM"] = {1, 1, 1}, + ["SCRIPT"] = {1, 0.5, 0.3}, + ["MULTI"] = normalFontColor +} + + +SpellsTooltip = CreateFrame("GameTooltip", "PTSpellsTooltip", UIParent, "GameTooltipTemplate") +SpellsTooltipOwner = nil +SpellsTooltipUnit = nil +SpellsTooltipAttach = nil +SpellsTooltipPowerBar = nil +do + local manaBar = CreateFrame("StatusBar", "PTSpellsTooltipManaBar", SpellsTooltip) + SpellsTooltipPowerBar = manaBar + manaBar:SetStatusBarTexture(BarStyles["Puppeteer"]) + manaBar:SetMinMaxValues(0, 1) + manaBar:SetWidth(100) + manaBar:SetHeight(12) + manaBar:SetPoint("TOPRIGHT", SpellsTooltip, "TOPRIGHT", -10, -12) + + local bg = manaBar:CreateTexture(nil, "BACKGROUND") + manaBar.background = bg + bg:SetAllPoints(true) + bg:SetTexture(0.3, 0.3, 0.3, 0.8) + + local text = manaBar:CreateFontString(nil, "ARTWORK", "GameFontNormal") + manaBar.text = text + text:SetWidth(manaBar:GetWidth()) + text:SetHeight(manaBar:GetHeight()) + text:SetPoint("CENTER", manaBar, "CENTER") + text:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE") + text:SetShadowOffset(0, 0) + text:SetJustifyH("CENTER") + text:SetJustifyV("CENTER") +end + + +local currentPower +local currentMaxPower +local currentPowerType + + +function InitBindingDisplayCache() + ButtonDisplayCache = {} + for _, button in ipairs(PTOptions.Buttons) do + ButtonDisplayCache[button] = {} + ButtonDisplayCache[button]["Normal"] = PTOptions.ButtonInfo[button].Name or button + ButtonDisplayCache[button]["Unfocused"] = colorize(PTOptions.ButtonInfo[button].Name or button, 0.3, 0.3, 0.3) + end + + BindingDisplayCache = {} + for _, targetType in ipairs(targetTypes) do + local levelTargetType = {} + BindingDisplayCache[targetType] = levelTargetType + for _, modifier in ipairs(util.GetKeyModifiers()) do + local levelModifier = {} + levelTargetType[modifier] = levelModifier + for _, button in ipairs(PTOptions.Buttons) do + local entry = {} + levelModifier[button] = entry + end + end + end +end + +function MarkDisplayCacheDirty() + +end + +function UpdateBindingDisplays(targetType, modifier, forceUpdate) + currentPower = UnitMana("player") + currentMaxPower = UnitManaMax("player") + currentPowerType = util.GetPowerType("player") + local entries = BindingDisplayCache[targetType][modifier] + for button, entry in pairs(entries) do + UpdateBindingDisplay(GetBinding(targetType, modifier, button), entry, forceUpdate) + end + return entries +end + +function UpdateBindingDisplay(binding, entry, fullUpdate) + if fullUpdate then + entry.Normal = nil + end + local prevNormal = entry.Normal + _UpdateBindingDisplay(binding, entry) + if prevNormal ~= entry.Normal then + entry.Unfocused = colorize(util.StripColors(entry.Normal), 0.3, 0.3, 0.3) + end + return entry +end + +function _UpdateBindingDisplay(binding, entry) + if binding == nil or binding.Type == nil or binding.Data == nil then + entry.Normal = unboundText + return + end + local options = PTOptions.SpellsTooltip + local currentPower = currentPower or UnitMana("player") + local maxPower = currentMaxPower or UnitManaMax("player") + local powerType = currentPowerType or util.GetPowerType("player") + local needsUpdate = not entry.Normal + local textColor = binding.Tooltip and binding.Tooltip.TextColor or BindTypeTooltipColors[binding.Type] + local text = binding.Tooltip and binding.Tooltip.Type == "CUSTOM" and binding.Tooltip.Data + if binding.Type == "SPELL" then + local spell = binding.Data + if spell == "" then + if needsUpdate then + entry.Normal = unboundText + entry.Casts = -1 + end + return + end + local cost, resource = GetResourceCost(spell) + if cost == "unknown" then + if needsUpdate then + entry.Normal = colorize(spell.." (Unknown)", 1, 0.4, 0.4) + entry.Casts = -1 + end + return + elseif cost == 0 then + entry.Normal = text or spell + entry.Casts = -1 + return + end + + local casts = math.floor(currentPower / cost) + local resourceColor = tooltipPowerColors[resource] + if resource ~= powerType then -- A druid can't cast a spell that requires a different power type + casts = 0 + end + if entry.Casts == casts and not needsUpdate then + return + end + local spellText = colorize(text or spell, textColor) + local costText + if powerType == "mana" and resource == powerType then + if options.ShowManaCost then + costText = cost + end + if options.ShowManaPercentCost then + costText = (costText and (costText.." ") or "")..util.RoundNumber((cost / maxPower) * 100, 1).."%" + end + else + costText = cost + end + if casts == 0 then + spellText = colorize(util.StripColors(spellText), 0.5, 0.5, 0.5) + end + if costText then + spellText = spellText.." "..colorize(costText, resourceColor) + end + if casts <= options.HideCastsAbove then + local castsColor + if casts == 0 then + castsColor = tooltipCastsColors["Zero"] + elseif casts <= options.CriticalCastsLevel then + castsColor = tooltipCastsColors["Critical"] + else + castsColor = tooltipCastsColors["Normal"] + end + spellText = spellText..colorize(" ("..casts..")", castsColor) + end + entry.Normal = spellText + entry.Casts = casts + elseif binding.Type == "ACTION" then + entry.Normal = colorize(text or binding.Data, textColor) + elseif binding.Type == "ITEM" then + local item = binding.Data + + text = colorize(text or item, textColor) + + if PTOptions.SpellsTooltip.ShowItemCount then + if not entry.NextUpdate or GetTime() > entry.NextUpdate then + entry.Casts = GetItemCount(item) -- This is giga expensive + entry.NextUpdate = GetTime() + 2 + end + local casts = entry.Casts + if casts <= options.HideCastsAbove then + local castsColor + if casts == 0 then + castsColor = tooltipCastsColors["Zero"] + elseif casts <= options.CriticalCastsLevel then + castsColor = tooltipCastsColors["Critical"] + else + castsColor = tooltipCastsColors["Normal"] + end + text = text..colorize(" ("..casts..")", castsColor) + end + end + entry.Normal = text + elseif binding.Type == "MACRO" then + text = text or binding.Data + if IsValidMacro(binding.Data) then + text = colorize(text, textColor) + else + text = colorize(text.." (Invalid Macro)", 1, 0.4, 0.4) + end + entry.Normal = text + elseif binding.Type == "SCRIPT" then + entry.Normal = colorize(text or "Script", textColor) + elseif binding.Type == "MULTI" then + entry.Normal = colorize(text or (binding.Data.Title ~= "" and binding.Data.Title) or "Multi", textColor) + else + entry.Normal = "Unhandled" + end +end + +function ApplySpellsTooltip(attachTo, unit, owner) + if not PTOptions.SpellsTooltip.Enabled then + return + end + --StartTiming("SpellsTooltip") + SetTooltipKeyListenerEnabled(true) + SpellsTooltipOwner = owner + SpellsTooltipAttach = attachTo + SpellsTooltipUnit = unit + SpellsTooltip:SetOwner(attachTo, tooltipAnchorMap[PTOptions.SpellsTooltip.Anchor], + PTOptions.SpellsTooltip.OffsetX, PTOptions.SpellsTooltip.OffsetY) + local options = PTOptions.SpellsTooltip + local currentPower = UnitMana("player") + local maxPower = UnitManaMax("player") + local powerType = util.GetPowerType("player") + local powerColor = tooltipPowerColors[powerType] + local powerText = "" + local showPowerBar = options.ShowPowerBar + if options.ShowPowerAs == "Power" then + powerText = tostring(currentPower) + elseif options.ShowPowerAs == "Power/Max Power" then + powerText = currentPower.."/"..maxPower + elseif options.ShowPowerAs == "Power %" then + powerText = util.RoundNumber((currentPower / maxPower) * 100).."%" + end + + if showPowerBar then + local r, g, b = util.InterpolateColorsNoTable(lowToHighColors, (currentPower / maxPower)) + powerText = colorize(powerText, r, g, b) + SpellsTooltipPowerBar:SetStatusBarColor(powerColor[1], powerColor[2], powerColor[3]) + SpellsTooltipPowerBar:SetValue(currentPower / maxPower) + SpellsTooltipPowerBar.text:SetText(powerText) + else + powerText = colorize(powerText, powerColor) + end + + local modifier = GetKeyModifier() + local displayModifier = modifier ~= "None" and + util.GetKeyModifierTypeByID(1 + (options.AbbreviatedKeys and 2 or 0) + (options.ColoredKeys and 1 or 0)) or " " + SpellsTooltip:AddDoubleLine(displayModifier, showPowerBar and " " or powerText, 1, 1, 1) + + local friendly = not UnitCanAttack("player", unit) + + local deadFriend = util.IsDeadFriend(unit) + local selfClass = GetClass("player") + local canResurrect = PTOptions.AutoResurrect and deadFriend and util.ResurrectionSpells[selfClass] + local canReviveChampion = canResurrect and util.GetSpellID(T("Revive Champion")) and + PTUnit.Get(unit):HasBuffIDOrName(45568, T("Holy Champion")) and UnitAffectingCombat("player") + local resEntry + if canReviveChampion then + resEntry = UpdateBindingDisplay(SpecialSpellBindings[T("Revive Champion")], compost:GetTable()) + elseif canResurrect then + resEntry = UpdateBindingDisplay(SpecialSpellBindings[util.ResurrectionSpells[selfClass]], compost:GetTable()) + end + + --StartTiming("BindingDisplays") + local entries = UpdateBindingDisplays(friendly and "Friendly" or "Hostile", modifier) + --EndTiming("BindingDisplays") + for _, button in ipairs(PTOptions.Buttons) do + local focused = not CurrentlyHeldButton or button == CurrentlyHeldButton + local displayCache = entries[button] + + local leftText = focused and ButtonDisplayCache[button].Normal or ButtonDisplayCache[button].Unfocused + + local rightText + local usingRes + if resEntry then + local binding = GetBindingFor(unit, modifier, button) + if not binding or binding.Type == "SPELL" then + rightText = focused and resEntry.Normal or resEntry.Unfocused + usingRes = true + end + end + rightText = rightText or (focused and displayCache.Normal or displayCache.Unfocused) + + if displayCache.Normal ~= unboundText or usingRes or PTOptions.ButtonInfo[button].ShowUnbound then + SpellsTooltip:AddDoubleLine(leftText, rightText) + end + end + if resEntry then + compost:Reclaim(resEntry) + end + SpellsTooltip:Show() + --EndTiming("SpellsTooltip") +end + +function HideSpellsTooltip() + SpellsTooltip:Hide() + SpellsTooltipOwner = nil + SpellsTooltipAttach = nil + SpellsTooltipUnit = nil + SetTooltipKeyListenerEnabled(false) +end + +function ReapplySpellsTooltip() + if SpellsTooltipOwner ~= nil then + SpellsTooltip:Hide() + ApplySpellsTooltip(SpellsTooltipAttach, SpellsTooltipUnit, SpellsTooltipAttach) + end +end \ No newline at end of file diff --git a/core/UnitTracker.lua b/core/UnitTracker.lua new file mode 100644 index 0000000..5db8b1c --- /dev/null +++ b/core/UnitTracker.lua @@ -0,0 +1,143 @@ +PTUtil.SetEnvironment(Puppeteer) +local _G = getfenv(0) + +UnitTrackerFrame = CreateFrame("Frame", "PTUnitTrackerFrame", UIParent) + +local util = PTUtil +local GetTime = GetTime +local compost = PTPool + +local TRACKING_EVAL_INTERVAL = 1.25 +local RANGE_MIN_DIST = 20 +local RANGE_MAX_DIST = 60 +local SIGHT_MAX_DIST = 80 +local DISTANCE_UPDATE_INTERVAL = 0.1 +local SIGHT_UPDATE_INTERVAL = 0.1 + +local AllUnits = util.AllUnits + +local distanceTrackedUnits = {} +local sightTrackedUnits = {} +local prevSightTrackedUnits = {} -- Swapped with sightTrackedUnits on every evaluation, so no tables are created +local preciseDistance = util.CanClientGetPreciseDistance() +local sightTrackingEnabled = util.CanClientSightCheck() +local nextEval = GetTime() + 0.5 +local nextRangeUpdate = GetTime() + 0.6 +local nextSightUpdate = GetTime() + 0.6 +if not preciseDistance then + nextRangeUpdate = nextRangeUpdate + 99999999 -- Effectively disable updates +end +if not sightTrackingEnabled then + nextSightUpdate = nextSightUpdate + 99999999 +end + +function LoadTrackingOptions() + local opts = PTOptions.Tracking + TRACKING_EVAL_INTERVAL = opts.EvaluateInterval + RANGE_MIN_DIST = opts.MinDistanceTracking + RANGE_MAX_DIST = opts.MaxDistanceTracking + SIGHT_MAX_DIST = opts.MaxSightTracking + DISTANCE_UPDATE_INTERVAL = opts.DistanceUpdateInterval + SIGHT_UPDATE_INTERVAL = opts.SightUpdateInterval +end + +function RunTrackingScan() + local UnitFrames = UnitFrames + local time = GetTime() + if time > nextEval then + --StartTiming("TrackingEval") + nextEval = time + TRACKING_EVAL_INTERVAL + + + compost:Erase(distanceTrackedUnits) + local previous = sightTrackedUnits + sightTrackedUnits = compost:Erase(prevSightTrackedUnits) + prevSightTrackedUnits = previous + if PTGuidRoster then + for guid, cache in pairs(PTUnit.GetAllUnits()) do + EvaluateTracking(guid) + end + else + for _, unit in ipairs(AllUnits) do + EvaluateTracking(unit) + end + end + + for _, unit in ipairs(prevSightTrackedUnits) do + for ui in UnitFrames(unit) do + ui:UpdateSight() + end + end + --EndTiming("TrackingEval") + end + + if time > nextRangeUpdate then + nextRangeUpdate = time + DISTANCE_UPDATE_INTERVAL + for _, unit in ipairs(distanceTrackedUnits) do + local cache = PTUnit.Get(unit) + if cache and cache:UpdateDistance() then + for ui in UnitFrames(unit) do + ui:UpdateRange() + end + end + end + end + + if time > nextSightUpdate then + nextSightUpdate = time + SIGHT_UPDATE_INTERVAL + for _, unit in ipairs(sightTrackedUnits) do + local cache = PTUnit.Get(unit) + --StartTiming("SightScan") + if cache and cache:UpdateSight() then + for ui in UnitFrames(unit) do + ui:UpdateSight() + end + end + --EndTiming("SightScan") + end + end +end + +function EvaluateTracking(unit, update) + local UnitFrames = UnitFrames + local cache = PTUnit.Get(unit) + local distanceChanged = cache:UpdateDistance() + local sightChanged = cache:UpdateSight() + local new = cache:CheckNew() + local dist = cache:GetDistance() + if distanceChanged or sightChanged or new then + for ui in UnitFrames(unit) do + if distanceChanged or new then + ui:UpdateRange() + end + if sightChanged or new then + ui:UpdateSight() + end + end + end + if cache:UpdatePVP() then + for ui in UnitFrames(unit) do + ui:UpdatePVP() + end + end + local isTarget = UnitIsUnit(unit, "target") + if PTGuidRoster then + unit = PTGuidRoster.ResolveUnitGuid(unit) + end + -- Only closely track units that are close to the range threshold + if isTarget or (dist <= RANGE_MAX_DIST and dist >= RANGE_MIN_DIST) then + if not update or not util.ArrayContains(distanceTrackedUnits, unit) then + table.insert(distanceTrackedUnits, unit) + end + end + if sightTrackingEnabled and (isTarget or (dist > 0 and dist < SIGHT_MAX_DIST)) then + if not update or not util.ArrayContains(sightTrackedUnits, unit) then + table.insert(sightTrackedUnits, unit) + end + end +end + +function StartUnitTracker() + LoadTrackingOptions() + UnitTrackerFrame:SetScript("OnUpdate", RunTrackingScan) +end \ No newline at end of file diff --git a/gui/GuiUtil.lua b/gui/GuiUtil.lua new file mode 100644 index 0000000..e00f5e9 --- /dev/null +++ b/gui/GuiUtil.lua @@ -0,0 +1,20 @@ +PTGuiUtil = {} +PTUtil.SetEnvironment(PTGuiUtil) +local _G = getfenv(0) +local compost = PTPool +local util = PTUtil +local colorize = util.Colorize + +function CreateColorUpdater(checkboxGetter, colorSelectGetter, colorGetter) + return function(self) + local color = colorGetter(self) + if color then + checkboxGetter(self):SetChecked(true) + colorSelectGetter(self):Show() + colorSelectGetter(self):SetColor(color) + else + checkboxGetter(self):SetChecked(false) + colorSelectGetter(self):Hide() + end + end +end \ No newline at end of file diff --git a/gui/Settings.lua b/gui/Settings.lua new file mode 100644 index 0000000..63cb0e9 --- /dev/null +++ b/gui/Settings.lua @@ -0,0 +1,1358 @@ +PTSettingsGui = {} +PTUtil.SetEnvironment(PTSettingsGui, PuppeteerSettings) +local util = PTUtil +local colorize = util.Colorize +local compost = PTPool +local GetOption = PuppeteerSettings.GetOption +local SetOption = PuppeteerSettings.SetOption + +TabFrame = PTGuiLib.Get("tab_frame"):Hide() + +function Init() + TabFrame:SetPoint("CENTER") + :SetSize(425, 475) + :SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + :SetSpecial() + + local origHide = TabFrame:GetHandle().Hide + TabFrame:GetHandle().Hide = function(self) + origHide(self) + if EditedBindings then + if Puppeteer.GetNumLoadoutChanges(Puppeteer.GetBindings(), EditedBindings) > 0 then + Puppeteer.Info("You have unsaved changes to your bindings") + end + end + end + + if PTOptions.Debug2 then + TabFrame:Show() + end + + local title = PTGuiLib.Get("title", TabFrame) + title:SetPoint("TOP", TabFrame, "TOP", 0, 22) + title:SetHeight(38) + title:SetWidth(170) + title:SetText("Puppeteer Settings") + + local closeButton = CreateFrame("Button", nil, TabFrame:GetHandle(), "UIPanelCloseButton") + closeButton:SetPoint("TOPRIGHT", TabFrame:GetHandle(), "TOPRIGHT", 0, 0) + closeButton:SetScript("OnClick", function() TabFrame:Hide() end) + + CreateTab_Bindings() + CreateTab_Options() + CreateTab_Customize() + CreateTab_About() +end + +OverlayStack = {} +OverlayBlockInputs = {} +function AddOverlayFrame(overlayFrame) + table.insert(OverlayStack, overlayFrame) + local block = PTGuiLib.Get("puppeteer_input_block", TabFrame) + block:SetScript("OnMouseDown", TabFrame:GetScript("OnMouseDown"), true) + block:SetScript("OnMouseUp", TabFrame:GetScript("OnMouseUp"), true) + table.insert(OverlayBlockInputs, block) + block:SetFrameLevel(overlayFrame:GetFrameLevel() + (table.getn(OverlayStack) * 200) - 100) + overlayFrame:SetFrameLevel(overlayFrame:GetFrameLevel() + (table.getn(OverlayStack) * 200)) + PTUtil.FixFrameLevels(overlayFrame:GetHandle()) +end + +function PopOverlayFrame() + local index = table.getn(OverlayStack) + table.remove(OverlayStack, index) + local block = table.remove(OverlayBlockInputs, index) + block:Dispose() +end + +EditedBindings = {} +BindingsContext = {Target = "Friendly", Modifier = "None"} +function CreateTab_Bindings() + local container = TabFrame:CreateTab("Bindings") + + local selectLoadoutLabel = CreateLabel(container, "Select Loadout") + :SetPoint("TOPLEFT", container, "TOPLEFT", 30, -40) + local selectLoadoutDropdown = CreateDropdown(container, 130) + :SetPoint("LEFT", selectLoadoutLabel, "RIGHT", 5, 0) + :SetDynamicOptions(function(addOption, level, args) + for _, name in ipairs(Puppeteer.GetBindingLoadoutNames()) do + addOption("text", name, + "checked", Puppeteer.GetSelectedBindingsLoadoutName() == name, + "func", args.func) + end + end, { + func = function(self) + local loadoutName = self.text + if Puppeteer.LoadoutEquals(Puppeteer.GetBindings(), EditedBindings) then + Puppeteer.SetSelectedBindingsLoadout(loadoutName) + else + local dialog + dialog = PTGuiLib.Get("simple_dialog", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + :SetTitle("Unsaved Changes") + :SetText("You have unsaved changes to your bindings. What would you like to do?") + :AddButton("Save changes & switch", function() + SaveBindings() + Puppeteer.SetSelectedBindingsLoadout(loadoutName) + PopOverlayFrame() + dialog:Dispose() + end) + :AddButton("Discard changes & switch", function() + Puppeteer.SetSelectedBindingsLoadout(loadoutName) + PopOverlayFrame() + dialog:Dispose() + end) + :AddButton("Carry changes over", function() + local editedBindings = EditedBindings + Puppeteer.SetSelectedBindingsLoadout(loadoutName) + EditedBindings = editedBindings + UpdateBindingsInterface() + UpdateUnsavedChanges() + PopOverlayFrame() + dialog:Dispose() + end) + :AddButton("Cancel", function() + PopOverlayFrame() + dialog:Dispose() + end) + AddOverlayFrame(dialog) + PlaySound("igMainMenuOpen") + end + end + }) + :SetTextUpdater(function(self) + self:SetText(Puppeteer.GetSelectedBindingsLoadoutName()) + end) + LoadoutsDropdown = selectLoadoutDropdown + local newLoadout = PTGuiLib.Get("button", container) + :SetPoint("LEFT", selectLoadoutDropdown, "RIGHT", 5, 0) + :SetSize(60, 22) + :SetText("New") + :OnClick(function(self) + if util.GetTableSize(Puppeteer.GetBindingLoadouts()) >= 20 then + Puppeteer.Info("You cannot create any more loadouts!") + return + end + + if Puppeteer.LoadoutEquals(Puppeteer.GetBindings(), EditedBindings) then + PromptNewLoadout() + else + local dialog + dialog = PTGuiLib.Get("simple_dialog", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + :SetTitle("Unsaved Changes") + :SetText("You have unsaved changes to your bindings. What would you like to do?") + :AddButton("Save changes", function() + SaveBindings() + PopOverlayFrame() + dialog:Dispose() + PromptNewLoadout() + end) + :AddButton("Discard changes", function() + LoadBindings() + PopOverlayFrame() + dialog:Dispose() + PromptNewLoadout() + end) + :AddButton("Cancel", function() + PopOverlayFrame() + dialog:Dispose() + end) + AddOverlayFrame(dialog) + PlaySound("igMainMenuOpen") + end + end) + local deleteLoadout = PTGuiLib.Get("button", container) + :SetPoint("LEFT", newLoadout, "RIGHT", 5, 0) + :SetSize(60, 22) + :SetText("Delete") + :OnClick(function() + local loadouts = Puppeteer.GetBindingLoadouts() + local currentLoadoutName = Puppeteer.GetSelectedBindingsLoadoutName() + local anotherLoadoutName + for k, v in pairs(loadouts) do + if k ~= Puppeteer.GetSelectedBindingsLoadoutName() then + anotherLoadoutName = k + break + end + end + if not anotherLoadoutName then + Puppeteer.Info("Cannot delete the only loadout") + return + end + local dialog = PTGuiLib.Get("simple_dialog", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER", 0, 40) + dialog:SetTitle("Confirm Delete") + dialog:SetText("Are you sure you want to delete binding loadout '"..currentLoadoutName.."'?") + dialog:AddButton("Yes, delete loadout", function() + dialog:Dispose() + PopOverlayFrame() + Puppeteer.SetSelectedBindingsLoadout(anotherLoadoutName) + loadouts[currentLoadoutName] = nil + end) + dialog:AddButton("No, keep loadout", function() + dialog:Dispose() + PopOverlayFrame() + end) + dialog:PlayOpenSound() + AddOverlayFrame(dialog) + end) + + local bindingsForLabel = CreateLabel(container, "Bindings For") + :SetPoint("TOPLEFT", container, "TOPLEFT", 40, -95) + + local bindingsForDropdown = CreateDropdown(container, 100) + :SetPoint("LEFT", bindingsForLabel, "RIGHT", 5, 0) + :SetDynamicOptions(function(addOption, level, args) + if not EditedBindings.UseFriendlyForHostile then + for _, option in ipairs(args.options) do + addOption("text", option, + "dropdownText", option, + "initFunc", args.initFunc, + "func", args.func) + end + end + end, { + options = {"Friendly", "Hostile"}, + initFunc = function(self, gui) + self.checked = self.text == gui:GetText() + end, + func = function(self) + SetTargetContext(self.text) + UpdateBindingsInterface() + end + }) + :SetTextUpdater(function(self) + self:SetText(EditedBindings.UseFriendlyForHostile and "All Targets" or BindingsContext.Target) + end) + BindingsForDropdown = bindingsForDropdown + + local useSame = CreateLabel(container, "Universal Bindings") + :SetPoint("LEFT", bindingsForDropdown, "RIGHT", 10, 0) + :ApplyTooltip("Use the same bindings for both friendly and hostile targets") + local universalBindingsCheckbox = CreateCheckbox(container, 20, 20) + :SetPoint("LEFT", useSame, "RIGHT", 5, 0) + :ApplyTooltip("Use the same bindings for both friendly and hostile targets") + :OnClick(function(self) + EditedBindings.UseFriendlyForHostile = self:GetChecked() == 1 + SetTargetContext("Friendly") + UpdateBindingsInterface() + UpdateUnsavedChanges() + end) + UniversalBindingsCheckbox = universalBindingsCheckbox + + local keyLabel = CreateLabel(container, "Key Modifier") + :SetPoint("TOPLEFT", container, "TOPLEFT", 95, -125) + + local keyDropdown = CreateDropdown(container, 150) + :SetPoint("LEFT", keyLabel, "RIGHT", 5, 0) + :SetSimpleOptions(util.GetKeyModifiers(), function(modifier) + return { + text = modifier, + initFunc = function(self, gui) + self.checked = self.text == gui:GetText() + end, + func = function(self, gui) + gui:SetText(self.text) + SetModifierContext(self.text) + UpdateBindingsInterface() + end + } + end, "None") + + local interface = PTGuiLib.Get("puppeteer_spell_bind_interface", container) + :SetPoint("TOPLEFT", container, "TOPLEFT", 5, -160) + :SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -5, 80) + + SpellBindInterface = interface + + UnsavedChangesText = PTGuiLib.GetText(container, "", 12) + :SetPoint("TOP", interface, "BOTTOM", 0, -3) + --:SetTextColor(1, 0.2, 0.2) + + + DiscardButton = PTGuiLib.Get("button", container) + :SetPoint("BOTTOMLEFT", container, "BOTTOMLEFT", 10, 35) + :SetSize(125, 25) + :SetText("Discard Changes") + :OnClick(function() + LoadBindings() + end) + SaveAndCloseButton = PTGuiLib.Get("button", container) + :SetPoint("BOTTOM", container, "BOTTOM", 0, 35) + :SetSize(125, 25) + :SetText("Save & Close") + :OnClick(function() + SaveBindings() + TabFrame:Hide() + end) + SaveButton = PTGuiLib.Get("button", container) + :SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -10, 35) + :SetSize(125, 25) + :SetText("Save Changes") + :OnClick(function() + SaveBindings() + end) + + local addButton = PTGuiLib.Get("button", container) + :SetPoint("TOP", container, "TOP", 0, -445) + :SetSize(200, 25) + :SetText("Add or Remove Buttons") + :ApplyTooltip("Edit what buttons you can bind spells to") + :OnClick(function() + local editor = PTGuiLib.Get("puppeteer_button_editor", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + AddOverlayFrame(editor) + end) + + LoadBindings() +end + +function PromptNewLoadout() + local newLoadout = PTGuiLib.Get("puppeteer_new_loadout", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + AddOverlayFrame(newLoadout) + PlaySound("igMainMenuOpen") +end + +function SetTargetContext(friendlyOrHostile) + BindingsContext.Target = friendlyOrHostile + BindingsForDropdown:UpdateText() +end + +function SetModifierContext(modifier) + BindingsContext.Modifier = modifier + +end + +function SetBindingsContext(friendlyOrHostile, modifier) + SetTargetContext(friendlyOrHostile) + SetModifierContext(modifier) +end + +function GetBindingsContext() + local targetBindings = EditedBindings.Bindings[BindingsContext.Target] + if not targetBindings then + targetBindings = {} + EditedBindings.Bindings[BindingsContext.Target] = targetBindings + end + local bindings = EditedBindings.Bindings[BindingsContext.Target][BindingsContext.Modifier] + if not bindings then + bindings = {} + EditedBindings.Bindings[BindingsContext.Target][BindingsContext.Modifier] = bindings + end + return bindings +end + +function UpdateBindingsInterface() + local bindings = GetBindingsContext() + for _, button in ipairs(PTOptions.Buttons) do + if not bindings[button] then + bindings[button] = {} + end + end + SpellBindInterface:SetBindings(bindings) +end + +function ReloadBindingLines() + SpellBindInterface:ClearSpellLines() + for _, button in ipairs(PTOptions.Buttons) do + SpellBindInterface:AddSpellLine(button, PTOptions.ButtonInfo[button].Name or button) + end +end + +function LoadBindings() + ReloadBindingLines() + LoadoutsDropdown:UpdateText() + EditedBindings = util.CloneTable(Puppeteer.GetBindings(), true) + UniversalBindingsCheckbox:SetChecked(EditedBindings.UseFriendlyForHostile) + if EditedBindings.UseFriendlyForHostile then + SetTargetContext("Friendly") + end + BindingsForDropdown:UpdateText() + UpdateBindingsInterface() + UpdateUnsavedChanges() +end + +function SaveBindings() + Puppeteer.GetBindingLoadouts()[Puppeteer.GetSelectedBindingsLoadoutName()] = Puppeteer.PruneLoadout(EditedBindings) + LoadBindings() +end + +function UpdateUnsavedChanges() + local ok, changes = pcall(Puppeteer.GetNumLoadoutChanges, Puppeteer.GetBindings(), EditedBindings) + if not ok then + Puppeteer.print(changes) + changes = "ERROR" + end + if changes == 0 then + UnsavedChangesText:Hide() + DiscardButton:SetEnabled(false) + SaveAndCloseButton:SetEnabled(false) + SaveButton:SetEnabled(false) + else + UnsavedChangesText:Show() + UnsavedChangesText:SetText("You have "..changes.." unsaved change"..(changes ~= 1 and "s" or "").." to your bindings") + DiscardButton:SetEnabled(true) + SaveAndCloseButton:SetEnabled(true) + SaveButton:SetEnabled(true) + end +end + +function CreateTab_Options() + local container = TabFrame:CreateTab("Options") + + local tabPanel = PTGuiLib.Get("tab_panel", container) + tabPanel:SetPoint("TOPLEFT", container, "TOPLEFT", 5, -28 - 50) + tabPanel:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -5, 5) + tabPanel:SetSimpleBackground() + container.TabPanel = tabPanel + + CreateTab_Options_Casting(tabPanel) + CreateTab_Options_SpellsTooltip(tabPanel) + CreateTab_Options_Other(tabPanel) + CreateTab_Options_Advanced(tabPanel) + CreateTab_Options_Mods(tabPanel) +end + +function CreateTab_Options_Casting(panel) + local container = panel:CreateTab("Casting") + local layout = NewLabeledColumnLayout(container, {150, 310}, -20, 10) + local factory = NewComponentFactory(container, layout) + container.factory = factory + + factory:dropdown("Cast When (Mouse)", "What mouse button state to start casting spells at", "CastWhen", {"Mouse Up", "Mouse Down"}, function() + for _, ui in ipairs(Puppeteer.AllUnitFrames) do + ui:RegisterClicks() + end + end) + factory:dropdown("Cast When (Keys)", "What key state to start casting spells at", "CastWhenKey", {"Key Up", "Key Down"}) + local resSpell = util.ResurrectionSpells[util.GetClass("player")] + local autoResInfo = not resSpell and "This does nothing for your class" or {"Replaces your bound spells with "..resSpell.. + " when clicking on a dead ally", "All other types of binds, such as Actions, will not be replaced"} + factory:checkbox("Auto Resurrect", autoResInfo, "AutoResurrect") + factory:checkbox("PVP Flag Protection", {"Stops you from casting spells on PVP flagged players if you're not flagged", + "Attempting to cast will prompt you to make an exception", + "Only stops you from using Spell bindings"}, "PVPFlagProtection") + factory:checkbox("Target While Casting", {"Target the unit while most bindings run", + "Note that these binding types override this rule:", + "Spell - Always targets unless using SuperWoW", + "Action - Never targets unless specified by action", + "Item - Always targets", + "Multi - Never targets"}, "TargetWhileCasting") + factory:checkbox("Target After Casting", {"Target the unit after most bindings run", + "Note that these binding types override this rule:", + "Multi - Never targets"}, "TargetAfterCasting") +end + +function CreateTab_Options_SpellsTooltip(panel) + local container = panel:CreateTab("Spells Tooltip") + local layout = NewLabeledColumnLayout(container, {150, 310}, -20, 10) + local factory = NewComponentFactory(container, layout) + container.factory = factory + factory:checkbox("Enable Spells Tooltip", {"Show the spells tooltip when hovering over unit frames"}, "SpellsTooltip.Enabled") + factory:checkbox("Show % Mana Cost", {"Show the percent mana cost in the spells tooltip", + "Does nothing for non-mana users"}, "SpellsTooltip.ShowManaPercentCost") + layout:column(2):levelAt(1) + factory:checkbox("Show # Mana Cost", {"Show the number mana cost in the spells tooltip", + "Does nothing for non-mana users"}, "SpellsTooltip.ShowManaCost") + layout:column(1) + factory:slider("Hide Casts Above", "Hide cast count if above this threshold", "SpellsTooltip.HideCastsAbove", 0, 50) + factory:slider("Critical Casts Level", "Show yellow text at this threshold", "SpellsTooltip.CriticalCastsLevel", 0, 50) + factory:checkbox("Shortened Keys", "Shortens keys to 1 letter", "SpellsTooltip.AbbreviatedKeys") + layout:column(2):levelAt(1) + factory:checkbox("Colored Keys", "Color code the keys as opposed to all being white", "SpellsTooltip.ColoredKeys") + layout:column(1) + factory:checkbox("Show Power Bar", "Show a power bar in the spells tooltip", "SpellsTooltip.ShowPowerBar", function() + if PTOptions.SpellsTooltip.ShowPowerBar then + Puppeteer.SpellsTooltipPowerBar:Show() + else + Puppeteer.SpellsTooltipPowerBar:Hide() + end + end) + factory:dropdown("Show Power As", "What type of information to show for power amounts", "SpellsTooltip.ShowPowerAs", + {"Power", "Power/Max Power", "Power %"}) + factory:dropdown("Attach To", "What the tooltip should be attached to", "SpellsTooltip.AttachTo", + {"Button", "Frame", "Group", "Screen"}) + layout:offset(0, 10) + factory:dropdown("Anchor", "Where the tooltip should be anchored", "SpellsTooltip.Anchor", + {"Top Left", "Top Right", "Bottom Left", "Bottom Right"}) + factory:checkbox("Show Item Count", {"Show the amount of your bound items", colorize("Warning: This causes lag!", 1, 0.4, 0.4)}, + "SpellsTooltip.ShowItemCount") +end + +function CreateTab_Options_Other(panel) + local container = panel:CreateTab("Other") + local layout = NewLabeledColumnLayout(container, {150, 220, 300}, -20, 10) + local factory = NewComponentFactory(container, layout) + container.factory = factory + factory:checkbox("Always Show Target", "Always show the target frame, regardless of whether you have a target or not", + "AlwaysShowTargetFrame", function() Puppeteer.CheckTarget() end) + layout:offset(0, 10) + factory:label("Show Targets:") + layout:column(2):levelAt(1) + factory:checkbox("Friendly", {"Show the Target frame when targeting friendlies", "No effect if Always Show Target is checked"}, + "ShowTargets.Friendly", function() Puppeteer.CheckTarget() end) + layout:column(3):levelAt(2) + factory:checkbox("Hostile", {"Show the Target frame when targeting hostiles", "No effect if Always Show Target is checked"}, + "ShowTargets.Hostile", function() Puppeteer.CheckTarget() end) + layout:column(1) + factory:label("Hide Party Frames:") + layout:column(2):levelAt(1) + factory:checkbox("In Party", {"Hide default party frames while in party", "This may cause issues with other addons"}, + "DisablePartyFrames.InParty", function() Puppeteer.CheckPartyFramesEnabled() end) + layout:column(3):levelAt(2) + factory:checkbox("In Raid", {"Hide default party frames while in raid", "This may cause issues with other addons"}, + "DisablePartyFrames.InRaid", function() Puppeteer.CheckPartyFramesEnabled() end) + layout:column(1) + factory:checkbox("Hide While Solo", "If enabled, all Puppeteer frames will be hidden when not in a party or raid", + "HideWhileSolo", function() Puppeteer.CheckGroup() end) + local dragAllCheckbox = factory:checkbox("Drag All Frames", {"If enabled, all frames will be moved when dragging", + "Use the inverse key to move a single frame; Opposite effect if disabled"}, "FrameDrag.MoveAll") + layout:ignoreNext() + local inverseDropdown = factory:dropdown("Inverse Key", {"This key will be used to do the opposite of the default drag operation"}, + "FrameDrag.AltMoveKey", {"Shift", "Control", "Alt"}) + inverseDropdown:SetWidth(80) + inverseDropdown:SetPoint("LEFT", dragAllCheckbox, "RIGHT", 90, 0) + factory:checkbox("Show Heal Predictions", {"See predictions on incoming healing", "Improved predictions if using SuperWoW"}, + "UseHealPredictions", function() Puppeteer.UpdateAllIncomingHealing() end) + + factory:checkbox("Out of Range Arrow", {"See an arrow when hovering over an out of range player"}, + "OutOfRangeArrow", function() Puppeteer.SetOutOfRangeArrowEnabled(PTOptions.OutOfRangeArrow) end) + + factory:checkbox("Pets Use Owner's Class Color", {"Pets are colored with their owner's class color", + "Applies wherever the Class color is chosen (health bars and names)"}, "PetOwnerClassColors", + function() + for _, ui in ipairs(Puppeteer.AllUnitFrames) do + ui:UpdateHealth() + end + end) + + factory:checkbox("Vanilla Shaman Color", {"Use paladin pink for shamans, as they originally were in vanilla", + "This is not recommended to use on cross-faction servers"}, "VanillaClassColors", + function() + for _, ui in ipairs(Puppeteer.AllUnitFrames) do + ui:UpdateHealth() + end + end) + + factory:checkbox("(1.18.1) LFT Auto Role", {"Automatically assign roles when joining LFT groups", + "This functionality was tested for 1.18.0 and may break in future updates"}, "LFTAutoRole", + function() Puppeteer.SetLFTAutoRoleEnabled(PTOptions.LFTAutoRole) end) + factory:checkbox("Show Raid Mana", {"Show average raid and healer mana in the raid frame header", + "Healer mana is shown separately for members with the Healer role"}, "ShowRaidMana", + function() + local raidGroup = Puppeteer.UnitFrameGroups["Raid"] + if raidGroup then raidGroup:UpdateRaidMana() end + end) +end + +function CreateTab_Options_Advanced(panel) + local container = panel:CreateTab("Advanced") + local layout = NewLabeledColumnLayout(container, {150, 220, 300}, 0, 10) + local factory = NewComponentFactory(container, layout) + container.factory = factory + + local TEXT_WIDTH = 370 + + local experimentsLabel = CreateLabel(container, "Experiments") + :SetPoint("TOP", container, "TOP", 0, -20) + :SetFontSize(14) + local experimentsInfo = CreateLabel(container, "Features which are not complete and/or need more testing. Use at your own risk.") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", experimentsLabel, "BOTTOM", 0, -5) + layout:offset(0, -70) + factory:checkbox("(1.18.1)\nAuto Role", {"If enabled, the Role Action menu shows auto role detection options", "Enabled Account-Wide"}, + "Global.Experiments.AutoRole", + Puppeteer.InitRoleDropdown) + factory:checkbox("(SuperWoW & UnitXP)\nEnemy Frames", + {"If enabled, you will see an additional frame group with in-combat enemies", "Requires SuperWoW and UnitXP SP3", + "Enabled Per-Character"}, + "Experiments.Enemy", + function() + Puppeteer.SetEnemyTrackingEnabled(PuppeteerSettings.IsExperimentEnabled("Enemy")) + end) + factory:checkbox("(SuperWoW)\nCast Icons", {"If enabled, you will see incoming casts over unit frames", + "This feature is highly subject to changes", "Requires SuperWoW!", "Enabled Per-Character"}, + "Experiments.CastIcons", + function() + if PTHealPredict then + PTHealPredict.RemoveAllCastIcons() + end + end) + + local scriptsLabel = CreateLabel(container, "Load & Postload Scripts") + :SetPoint("TOP", container, "TOP", 0, -160) + :SetFontSize(14) + + local loadScriptInfo = CreateLabel(container, "The Load Script runs after profiles are initialized, but before UIs are created, ".. + "making it good for editing profile attributes. GetProfile and CreateProfile are defined locals.") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", scriptsLabel, "BOTTOM", 0, -10) + local loadScriptButton = PTGuiLib.Get("button", container) + :SetPoint("TOP", loadScriptInfo, "BOTTOM", 0, -5) + :SetSize(150, 20) + :SetText("Edit Load Script") + :OnClick(function() + local editor + editor = PTGuiLib.Get("puppeteer_load_script_editor", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + :SetTitle("Edit Load Script") + editor:GetEditbox():SetText(PTOptions.Scripts.OnLoad or "") + editor:SetCallback(function(save, data) + if save then + PTOptions.Scripts.OnLoad = data + end + editor:Dispose() + PopOverlayFrame() + end) + editor:GetEditbox():SetFocus() + AddOverlayFrame(editor) + end) + local postLoadScriptInfo = CreateLabel(container, "The Postload Script runs after everything is initialized. ".. + "GetProfile and CreateProfile are defined locals.") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", loadScriptButton, "BOTTOM", 0, -10) + local postLoadScriptButton = PTGuiLib.Get("button", container) + :SetPoint("TOP", postLoadScriptInfo, "BOTTOM", 0, -5) + :SetSize(150, 20) + :SetText("Edit Postload Script") + :OnClick(function() + local editor + editor = PTGuiLib.Get("puppeteer_load_script_editor", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + :SetTitle("Edit Postload Script") + editor:GetEditbox():SetText(PTOptions.Scripts.OnPostLoad or "") + editor:SetCallback(function(save, data) + if save then + PTOptions.Scripts.OnPostLoad = data + end + editor:Dispose() + PopOverlayFrame() + end) + editor:GetEditbox():SetFocus() + AddOverlayFrame(editor) + end) + local reloadInfo = CreateLabel(container, "A reload or relog is required for any script changes to take effect.") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", postLoadScriptButton, "BOTTOM", 0, -20) + local reloadButton = PTGuiLib.Get("button", container) + :SetPoint("TOP", reloadInfo, "BOTTOM", 0, -5) + :SetSize(120, 20) + :SetText("Reload UI") + :OnClick(function() + ReloadUI() + end) +end + +function CreateTab_Options_Mods(panel) + local container, scrollFrame = panel:CreateTab("Mods", true) + local layout = NewLabeledColumnLayout(container, {150, 310}, -20, 10) + local factory = NewComponentFactory(container, layout) + container.factory = factory + + local TEXT_WIDTH = 370 + + local generalInfo = CreateLabel(container, "Some client mods enhance your experience with Puppeteer by enabling additional functionality.") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", container, "TOP", 0, -10) + + local modColors = { + ["SuperWoW"] = {1, 0.4, 0.4}, + ["UnitXP SP3"] = {0.4, 0.4, 1}, + ["Nampower"] = {0.4, 1, 0.4}, + ["VanillaUtils"] = {1, 1, 1} + } + local detectedTexts = { + ["Detected"] = colorize("Mod Detected", 0.2, 1, 0.2).." - %s", + ["Not Detected"] = colorize("Mod Not Detected", 1, 0.2, 0.2), + ["Outdated"] = colorize("Mod Detected (Out of Date)", 1, 1, 0).." - %s" + } + local detectedLabelTexts = {} + for _, mod in ipairs({"SuperWoW", "UnitXP SP3", "Nampower"}) do + detectedLabelTexts[mod] = string.format(detectedTexts[util.GetModVersionState(mod)], util.GetModVersionText(mod)) + end + local superWowLabel = CreateLabel(container, colorize("SuperWoW", modColors["SuperWoW"])) + :SetPoint("TOP", generalInfo, "BOTTOM", 0, -20) + :SetFontSize(14) + local superWowDetectedLabel = CreateLabel(container, detectedLabelTexts["SuperWoW"]) + :SetPoint("TOP", superWowLabel, "BOTTOM", 0, -5) + :SetFontSize(10) + :SetFontFlags("OUTLINE") + + local superWowInfo = CreateLabel(container, "SuperWoW provides the following enhancements:\n\n".. + "• Enables tracking of many class buff and debuff timers\n".. + "• Enhances spell casting by directly casting on targets rather than split-second target switching tricks\n".. + "• Allows you to see accurate distance to other friendly players and NPCs\n".. + "• Mousing over unit frames properly sets your mouseover target\n".. + "• Shows incoming healing from players that do not have HealComm and predicts more accurate numbers\n".. + "• Add players/mobs to a separate Focus frame (By using the Focus action bind)") + :SetJustifyH("LEFT") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", superWowDetectedLabel, "BOTTOM", 0, -10) + local superWowLink = CreateLinkEditbox(container, "https://github.com/balakethelock/SuperWoW") + :SetPoint("TOP", superWowInfo, "BOTTOM", 0, -5) + :SetSize(300, 20) + local superWowLinkLabel = CreateLabel(container, "Link:") + :SetPoint("RIGHT", superWowLink, "LEFT", -5, 0) + + layout:ignoreNext() + local setMouseoverCheckbox = factory:checkbox("Set Mouseover", {"Requires SuperWoW Mod To Work", + "If enabled, hovering over frames will set your mouseover target"}, "SetMouseover") + :SetPoint("TOP", superWowLink, "BOTTOM", 0, -10) + if not util.IsSuperWowPresent() then + setMouseoverCheckbox:Disable() + end + + -- UnitXP SP3 + + local unitXPLabel = CreateLabel(container, colorize("UnitXP SP3", modColors["UnitXP SP3"])) + :SetPoint("TOP", setMouseoverCheckbox, "BOTTOM", 0, -20) + :SetFontSize(14) + local unitXPDetectedLabel = CreateLabel(container, detectedLabelTexts["UnitXP SP3"]) + :SetPoint("TOP", unitXPLabel, "BOTTOM", 0, -5) + :SetFontSize(10) + :SetFontFlags("OUTLINE") + + local unitXPInfo = CreateLabel(container, "UnitXP SP3 provides the following enhancements:\n\n".. + "• Displays when units are out of line-of-sight\n".. + "• Allows you to see more accurate distance than SuperWoW and also see distance to enemies") + :SetJustifyH("LEFT") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", unitXPDetectedLabel, "BOTTOM", 0, -10) + local unitXPLink = CreateLinkEditbox(container, "https://codeberg.org/konaka/UnitXP_SP3/wiki") + :SetPoint("TOP", unitXPInfo, "BOTTOM", 0, -5) + :SetSize(300, 20) + local unitXPLinkLabel = CreateLabel(container, "Link:") + :SetPoint("RIGHT", unitXPLink, "LEFT", -5, 0) + + -- Nampower + + local nampowerLabel = CreateLabel(container, colorize("Nampower", modColors["Nampower"])) + :SetPoint("TOP", unitXPLink, "BOTTOM", 0, -20) + :SetFontSize(14) + local nampowerDetectedLabel = CreateLabel(container, detectedLabelTexts["Nampower"]) + :SetPoint("TOP", nampowerLabel, "BOTTOM", 0, -5) + :SetFontSize(10) + :SetFontFlags("OUTLINE") + + local nampowerInfo = CreateLabel(container, "Nampower provides the following enhancements:\n\n".. + "• Allows you to queue spell casts like in modern versions of WoW, drastically increasing casting efficiency\n".. + "• (Newer Versions) Enables tracking of additional buff/debuff timers") + :SetJustifyH("LEFT") + :SetWidth(TEXT_WIDTH) + :SetPoint("TOP", nampowerDetectedLabel, "BOTTOM", 0, -10) + local nampowerLink = CreateLinkEditbox(container, "https://gitea.com/avitasia/nampower") + :SetPoint("TOP", nampowerInfo, "BOTTOM", 0, -5) + :SetSize(300, 20) + local nampowerLinkLabel = CreateLabel(container, "Link:") + :SetPoint("RIGHT", nampowerLink, "LEFT", -5, 0) + + + -- local modFeaturesLabel = CreateLabel(container, "Mod Enhancements") + -- :SetPoint("TOP", nampowerLink, "BOTTOM", 0, -25) + -- :SetFontSize(14) + -- local lastLabel = modFeaturesLabel + -- for _, feature in ipairs(util.ModFeatures) do + -- if not feature.Hidden then + -- local modPrefix = "" + -- for _, provider in ipairs(feature.Providers) do + -- if string.len(modPrefix) > 0 then + -- modPrefix = modPrefix.." or " + -- end + -- modPrefix = modPrefix..colorize(provider[1], modColors[provider[1]]) + -- end + -- local desc = feature.Description + -- if not feature.ChosenProvider then + -- desc = colorize(desc, 0.5, 0.5, 0.5) + -- end + -- local label = CreateLabel(container, "• ["..modPrefix.."] "..desc) + -- :SetJustifyH("LEFT") + -- :SetWidth(TEXT_WIDTH) + -- :SetPoint("TOP", lastLabel, "BOTTOM", 0, -8) + -- lastLabel = label + -- end + -- end + + + scrollFrame:UpdateScrollRange() +end + +function CreateTab_Customize() + local container = TabFrame:CreateTab("Customize") + + local frameStyleContainer = PTGuiLib.Get("container", container) + :SetSimpleBackground() + :SetPoint("TOPLEFT", container, "TOPLEFT", 5, -26) + :SetPoint("BOTTOMRIGHT", container, "TOPRIGHT", -5, -155) + local layout = NewLabeledColumnLayout(frameStyleContainer, {100, 340, 175}, -25, 5) + + local frameSettingsText = CreateLabel(frameStyleContainer, "Frame Group Settings") + :SetPoint("TOP", frameStyleContainer, "TOP", 0, -5) + :SetFontSize(14) + + + local preferredFrameOrder = {"Party", "Pets", "Raid", "Raid Pets", "Target", "Focus"} + local frameDropdown = CreateLabeledDropdown(frameStyleContainer, "Configure Frame", "The frame to edit the attributes of") + :SetWidth(150) + :SetDynamicOptions(function(addOption, level, args) + for _, name in ipairs(preferredFrameOrder) do + if Puppeteer.UnitFrameGroups[name] then + addOption("text", name, + "dropdownText", name, + "initFunc", args.initFunc, + "func", args.func) + end + end + for name, group in pairs(Puppeteer.UnitFrameGroups) do + if not util.ArrayContains(preferredFrameOrder, name) then + addOption("text", name, + "dropdownText", name, + "initFunc", args.initFunc, + "func", args.func) + end + end + end, { + initFunc = function(self, gui) + self.checked = self.text == gui:GetText() + end, + func = function(self, gui) + StyleDropdown:UpdateText() + AnchorDropdown:UpdateText() + UpdateFrameOptions() + end + }) + :SetText("Party") + FrameDropdown = frameDropdown + layout:column(3):layoutComponent(frameDropdown) + local GetSelectedProfileName = PuppeteerSettings.GetSelectedProfileName + local styleDropdown = CreateLabeledDropdown(frameStyleContainer, "Choose Style", "The style of the frame") + :SetWidth(150) + :SetDynamicOptions(function(addOption, level, args) + local profiles = PTProfileManager.GetProfileNames() + for _, profile in ipairs(profiles) do + addOption("text", profile, + "checked", GetSelectedProfileName(frameDropdown:GetText()) == profile, + "func", args.func) + end + end, { + func = function(self, gui) + local selectedFrame = frameDropdown:GetText() + PTOptions.ChosenProfiles[selectedFrame] = self.text + + if selectedFrame == "Focus" and not util.IsSuperWowPresent() then + return + end + + -- Here's some probably buggy profile hotswapping + local group = Puppeteer.UnitFrameGroups[selectedFrame] + group.profile = GetSelectedProfile(selectedFrame) + local oldUIs = group.uis + group.uis = {} + group:ResetFrameLevel() -- Need to lower frame or the added UIs are somehow under it + for unit, ui in pairs(oldUIs) do + ui:GetRootContainer():SetParent(nil) + -- Forget about the old UI, and cause a fat memory leak why not + ui:GetRootContainer():Hide() + local newUI = PTUnitFrame:New(unit, ui.isCustomUnit) + util.RemoveElement(Puppeteer.AllUnitFrames, ui) + table.insert(Puppeteer.AllUnitFrames, newUI) + local unitUIs = Puppeteer.GetUnitFrames(unit) + util.RemoveElement(unitUIs, ui) + table.insert(unitUIs, newUI) + group:AddUI(newUI, true) + if ui.guidUnit then + newUI.guidUnit = ui.guidUnit + elseif unit ~= "target" then + newUI:Hide() + end + end + Puppeteer.CheckGroup() + group:UpdateUIPositions() + group:ApplyProfile() + + gui:UpdateText() + end + }) + :SetTextUpdater(function(self) + self:SetText(GetSelectedProfileName(frameDropdown:GetText())) + end) + StyleDropdown = styleDropdown + layout:column(1):offset(0, -30):layoutComponent(styleDropdown) + + local anchors = {"TOPLEFT", "TOP", "TOPRIGHT", "LEFT", "CENTER", "RIGHT", "BOTTOMLEFT", "BOTTOM", "BOTTOMRIGHT"} + local readableAnchorMap = { + TOPLEFT = "Top Left", + TOP = "Top", + TOPRIGHT = "Top Right", + LEFT = "Left", + CENTER = "Center", + RIGHT = "Right", + BOTTOMLEFT = "Bottom Left", + BOTTOM = "Bottom", + BOTTOMRIGHT = "Bottom Right" + } + local anchorDropdown = CreateLabeledDropdown(frameStyleContainer, "Anchor", + {"The point the frame is anchored to, affecting the direction it expands/retracts", + "Top Left: Expands right and down", + "Top: Expands equally left & right and down", + "Top Right: Expands left and down", + "Left: Expands right and equally up & down", + "Center: Expands equally in all directions", + "Right: Expands left and equally up & down", + "Bottom Left: Expands right and up", + "Bottom: Expands equally left & right and up", + "Bottom Right: Expands left and up",}) + :SetWidth(150) + :SetSimpleOptions(anchors, function(option) + return { + initFunc = function(self) + self.checked = PuppeteerSettings.GetFramePosition(frameDropdown:GetText()) == option + end, + func = function(self, gui) + local group = Puppeteer.UnitFrameGroups[frameDropdown:GetText()] + util.ConvertAnchor(group:GetContainer(), option) + PuppeteerSettings.SaveFramePositions() + gui:UpdateText() + end, + text = readableAnchorMap[option] + } + end) + :SetTextUpdater(function(self) + self:SetText(readableAnchorMap[PuppeteerSettings.GetFramePosition(frameDropdown:GetText())]) + end) + layout:layoutComponent(anchorDropdown) + AnchorDropdown = anchorDropdown + + local lockFrameCheckbox = CreateLabeledCheckbox(frameStyleContainer, "Lock Frame", {"If checked, this frame will not be movable", + "Note: This setting is also accessible by right-clicking the group title bar"}) + :OnClick(function(self) + local frameName = frameDropdown:GetText() + PuppeteerSettings.SetFrameLocked(frameName, self:GetChecked() == 1) + end) + layout:column(2):offset(0, -30):layoutComponent(lockFrameCheckbox) + LockFrameCheckbox = lockFrameCheckbox + + local hideTitleCheckbox = CreateLabeledCheckbox(frameStyleContainer, "Hide Title", {"If checked, the title of this frame will be hidden", + colorize("To move the frame while its title is hidden, hold Shift+Ctrl+Alt and drag it with the left mouse button", 1, 0.82, 0)}) + :OnClick(function(self) + local frameName = frameDropdown:GetText() + PuppeteerSettings.SetTitleHidden(frameName, self:GetChecked() == 1) + end) + layout:layoutComponent(hideTitleCheckbox) + HideTitleCheckbox = hideTitleCheckbox + + local hideFrameCheckbox = CreateLabeledCheckbox(frameStyleContainer, "Hide Frame", "If checked, this frame will not be visible") + :OnClick(function(self) + local frameName = frameDropdown:GetText() + PuppeteerSettings.SetFrameHidden(frameName, self:GetChecked() == 1) + Puppeteer.CheckGroup() + end) + layout:layoutComponent(hideFrameCheckbox) + HideFrameCheckbox = hideFrameCheckbox + + UpdateFrameOptions() + + local overrideContainer = PTGuiLib.Get("scroll_frame", container) + :SetPoint("TOPLEFT", frameStyleContainer, "BOTTOMLEFT", 0, -5) + :SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -5, 5) + :SetSimpleBackground() + StyleOverrideContainer = overrideContainer + + local container = overrideContainer + + local styleOverridesText = PTGuiLib.GetText(container, "Style Overrides") + :SetFontSize(14) + :SetPoint("TOP", container, "TOP", 0, -5) + local explainer = PTGuiLib.GetText(container, "Style Overrides are an inelegant, temporary measure to enable customization ".. + "of commonly requested unit frame edits. Some settings may be visually incompatible with one another. ".. + "This will be replaced in a future date with a system that is much more robust.\n\n".. + colorize("You must reload or relog for any changes to take effect!", 1, 0.5, 0.5)) + :SetPoint("TOP", styleOverridesText, "TOP", 0, -20) + :SetWidth(360) + + local layout = NewLabeledColumnLayout(container, {160}, -120, 5) + + local styleDropdown = CreateLabeledDropdown(container, "Edit Style", "The style to edit the overrides of") + :SetWidth(160) + :SetText("Default") + :SetDynamicOptions(function(addOption, level, args) + local profiles = PTProfileManager.GetProfileNames() + for _, profile in ipairs(profiles) do + addOption("text", profile, + "initFunc", args.initFunc, + "func", args.func) + end + end, { + initFunc = function(self, gui) + self.checked = gui:GetText() == self.text + end, + func = function(self) + SetSelectedStyleOverride(self.text) + end + }) + layout:offset(-65, 0):layoutComponent(styleDropdown) + layout:offset(65, 0) + StyleOverrideDropdown = styleDropdown + SetSelectedStyleOverride(PTProfileManager.DEFAULT_PROFILE_NAME) + + local reloadUI = PTGuiLib.Get("button", container) + :SetPoint("LEFT", styleDropdown, "RIGHT", 10, 0) + :SetSize(100, 20) + :SetText("Reload UI") + :OnClick(ReloadUI) + + local function add(component, offsetY) + if component:Type() == "checkbox" then + offsetY = (offsetY or 0) - 12 + end + layout:offset(0, offsetY or 0):layoutComponent(component) + end + local createDropdown = CreateStyleOverrideDropdown + local createSlider = CreateStyleOverrideSlider + + local barStyles = util.ToArray(Puppeteer.BarStyles) + table.sort(barStyles) + + add(createDropdown("Health Bar Color", nil, "HealthBarColor", {"Green To Red", "Green", "Class"}), -10) + add(createDropdown("Health Bar Texture", "Blizzard Modern uses the reference class palette when Health Bar Color is Class", "HealthBarStyle", barStyles)) + add(createDropdown("Health Display", "What kind of text is displayed as health", "HealthDisplay", {"Health", "Health/Max Health", "% Health", "Hidden"})) + add(createDropdown("Health Numbers Color", "White overrides the default gold health numbers", "HealthNumbersColor", {"White"})) + add(createDropdown("Missing Health Display", "What kind of text is displayed as missing health", "MissingHealthDisplay", {"-Health", "-% Health", "Hidden"})) + add(createDropdown("Incoming Heal Display", nil, "IncomingHealDisplay", {"Overheal", "Heal", "Hidden"})) + add(createDropdown("Power Bar Texture", "Blizzard Modern uses the reference's colored mana, rage, focus and energy artwork", "PowerBarStyle", barStyles)) + add(createDropdown("Power Display", "What kind of text is displayed as power", "PowerDisplay", {"Power", "Power/Max Power", "% Power", "Hidden"})) + add(createDropdown("Power Numbers Color", "White overrides the default gold mana, rage, energy and focus numbers", "PowerNumbersColor", {"White"})) + add(createDropdown("Name Text Color", "'Default' is default Blizzard yellow text", "NameText.Color", {"Class", "Default"})) + add(createDropdown("Show Debuff Colors On", nil, "ShowDebuffColorsOn", {"Health Bar", "Name", "Health", "Hidden"})) + add(createDropdown("Sort Units By", "The sorting algorithm for units in a group", "SortUnitsBy", {"ID", "Name", "Class Name"})) + add(createDropdown("Growth Orientation", "Vertical grows units up and down, Horizontal grows units left and right", "Orientation", {"Vertical", "Horizontal"})) + add(createDropdown("Split Raid Into Groups", "If enabled, raids will be split based on the groups", "SplitRaidIntoGroups", {true, false})) + add(createDropdown("Border Style", "The border of the group", "BorderStyle", {"Tooltip", "Dialog Box", "Borderless"})) + add(createSlider("Background Opacity", "How opaque the background of the frame group is in %", "BackgroundOpacity", 0, 100)) + add(createSlider("Max Units In Axis", "The maximum number of units in the growth axis until it must shift down", "MaxUnitsInAxis", 1, 20)) + add(createSlider("Min Units X", "The minimum amount of unit space to take on the X-axis", "MinUnitsX", 0, 20)) + add(createSlider("Min Units Y", "The minimum amount of unit space to take on the Y-axis", "MinUnitsY", 0, 20)) + add(createSlider("Horizontal Spacing", "The number of pixels between units", "HorizontalSpacing", 0, 20)) + add(createSlider("Vertical Spacing", "The number of pixels between units", "VerticalSpacing", 0, 20)) + add(createSlider("Show Distance At\n(Friendly)", "How many yards to start showing friendly distance at", "ShowDistanceThreshold.Friendly", 0, 100)) + add(createSlider("Show Distance At\n(Hostile)", "How many yards to start showing hostile distance at", "ShowDistanceThreshold.Hostile", 0, 100)) + add(createSlider("Out of Range At\n(Friendly)", "How many yards to fade out friendly units", "OutOfRangeThreshold.Friendly", 0, 100)) + add(createSlider("Out of Range At\n(Hostile)", "How many yards to fade out hostile units", "OutOfRangeThreshold.Hostile", 0, 100)) + add(createSlider("Out of Range Opacity", "How opaque out of range players appear in %", "OutOfRangeOpacity", 0, 100)) + local visualIssues = colorize("Adjusting this value may cause visual issues!", 1, 0.4, 0.4) + add(createSlider("Unit Frame Width", {"The width of each unit frame", visualIssues}, "Width", 20, 250)) + add(createSlider("Health Bar Height", {"The height of the health bar", visualIssues}, "HealthBarHeight", 1, 100)) + add(createSlider("Power Bar Height", {"The height of the power bar", visualIssues}, "PowerBarHeight", 1, 100)) + add(createSlider("Aura Tracker Height", {"The height of tracked auras", visualIssues}, "AuraTracker.Height", 0, 50)) + PopulateStyleOverrides() +end + +function UpdateFrameOptions() + LockFrameCheckbox:SetChecked(PuppeteerSettings.IsFrameLocked(FrameDropdown:GetText())) + HideTitleCheckbox:SetChecked(PuppeteerSettings.IsTitleHidden(FrameDropdown:GetText())) + HideFrameCheckbox:SetChecked(PuppeteerSettings.IsFrameHidden(FrameDropdown:GetText())) +end + +StyleOverrideComponents = {} + +CurrentStyleOverride = nil + +function CreateStyleOverrideDropdown(text, tooltip, optionLoc, options) + local dropdown, label = CreateLabeledDropdown(StyleOverrideContainer, text, tooltip) + dropdown:SetWidth(200) + dropdown:SetDynamicOptions(function(addOption, level, args) + addOption("text", "Use Style Default", + "initFunc", args.initFunc, + "func", args.func) + for _, option in ipairs(options) do + addOption("text", tostring(option), + "option", option, + "initFunc", args.initFunc, + "func", args.func) + end + end, { + initFunc = function(self, gui) + self.checked = gui:GetText() == self.text + end, + func = function(self, gui) + SetStyleOverride(GetSelectedStyleOverride(), optionLoc, self.option) + gui:UpdateText() + end + }) + :SetTextUpdater(function(gui) + local value = GetStyleOverride(GetSelectedStyleOverride(), optionLoc) + gui:SetText(value ~= nil and tostring(value) or "Use Style Default") + end) + StyleOverrideComponents[optionLoc] = {Updater = function() + dropdown:UpdateText() + end} + return dropdown +end + +function CreateStyleOverrideSlider(text, tooltip, optionLoc, min, max) + local checkbox = CreateLabeledCheckbox(StyleOverrideContainer, text, tooltip) + checkbox:ApplyTooltip(tooltip, colorize("Check to override style default", 1, 0.4, 1)) + local slider = CreateSlider(StyleOverrideContainer) + slider:SetMinMaxValues(min, max) + slider:GetSlider():SetNumberedText() + slider:GetSlider():HookScript("OnValueChanged", function() + SetStyleOverride(GetSelectedStyleOverride(), optionLoc, slider:GetSlider():GetValue()) + end) + slider:SetPoint("LEFT", checkbox, "RIGHT", 5, 0) + slider:ApplyTooltip(tooltip) + local function Checkbox_OnClick() + local value = GetStyleOverride(GetSelectedStyleOverride(), optionLoc) + if checkbox:GetChecked() then + slider:Show() + if value == nil then + value = GetStyleOverride(PTProfileManager.GetDefaultProfile(CurrentStyleOverride), optionLoc) + SetStyleOverride(GetSelectedStyleOverride(), optionLoc, value) + end + slider:SetValue(value) + else + slider:Hide() + SetStyleOverride(GetSelectedStyleOverride(), optionLoc, nil) + end + end + checkbox:SetScript("OnClick", Checkbox_OnClick) + StyleOverrideComponents[optionLoc] = {Updater = function() + local hasOverride = GetStyleOverride(GetSelectedStyleOverride(), optionLoc) ~= nil + checkbox:SetChecked(hasOverride) + Checkbox_OnClick() + end} + return checkbox +end + +function GetSelectedStyleOverride() + return PTOptions.StyleOverrides[CurrentStyleOverride] +end + +function SetSelectedStyleOverride(style) + CurrentStyleOverride = style + if not PTOptions.StyleOverrides[CurrentStyleOverride] then + PTOptions.StyleOverrides[CurrentStyleOverride] = {} + end + StyleOverrideDropdown:SetText(style) + PopulateStyleOverrides() +end + +function PopulateStyleOverrides() + for _, component in pairs(StyleOverrideComponents) do + component.Updater() + end +end + +function TraverseOverride(style, location) + local path = util.SplitString(location, ".") + local currentTable = style + for i = 1, table.getn(path) - 1 do + if not currentTable[path[i]] then + currentTable[path[i]] = {} + end + currentTable = currentTable[path[i]] + end + return currentTable, path[table.getn(path)] +end + +function GetStyleOverride(style, location) + local optionTable, location = TraverseOverride(style, location) + return optionTable[location] +end + +function SetStyleOverride(style, location, value) + local optionTable, location = TraverseOverride(style, location) + optionTable[location] = value +end + +function CreateTab_About() + local container = TabFrame:CreateTab("About") + + local text = PTGuiLib.GetText(container, + "Puppeteer Version "..Puppeteer.VERSION.. + "\n\n\nPuppeteer Author: OldManAlpha\nNordanaar IGN: Oldmana, Lowall, Jmdruid".. + "\n\nHealersMate Original Author: i2ichardt\nEmail: rj299@yahoo.com".. + "\n\nAdditional Contributors".. + "\nWoW 1.18.1 Clients community: Answers to addon development questions".. + "\nShagu: Utility functions & providing a wealth of research material".. + "\nChatGPT: Utility functions".. + "\n\n\nCheck For Updates, Report Issues, Make Suggestions:\n", + 12) + :SetPoint("TOP", container, "TOP", 0, -80) + + CreateLinkEditbox(container, "https://github.com/OldManAlpha/Puppeteer") + :SetPoint("TOP", text, "BOTTOM", 0, -10) + :SetSize(300, 20) +end + + +-- Factory-related functions + +function NewLabeledColumnLayout(container, columns, startY, spacing) + local layout = {} + layout.lastAdded = {} + layout.params = {} + layout.selectedColumn = 1 + function layout:getNextPoint(columnIndex) + local offsetX, offsetY = self.params.offsetX or 0, self.params.offsetY or 0 + if self.lastAdded[columnIndex] then + return "TOPLEFT", self.lastAdded[columnIndex], "BOTTOMLEFT", offsetX, -spacing + offsetY + end + return "TOPLEFT", container, "TOPLEFT", columns[columnIndex] + offsetX, startY + offsetY + end + function layout:layoutComponent(component) + if not self.params.ignoreNext then + local columnIndex = self.selectedColumn or 1 + component:SetPoint(self:getNextPoint(columnIndex)) + self.lastAdded[columnIndex] = component + end + util.ClearTable(self.params) + end + function layout:column(columnIndex) + self.selectedColumn = columnIndex + return self + end + function layout:offset(offsetX, offsetY) + self.params.offsetX = (self.params.offsetX or 0) + offsetX + self.params.offsetY = (self.params.offsetY or 0) + offsetY + return self + end + function layout:levelAt(columnIndex) + local lastAdded = self.lastAdded[columnIndex] + self.lastAdded[self.selectedColumn] = lastAdded + self:offset(columns[self.selectedColumn] - columns[columnIndex], spacing + lastAdded:GetHeight()) + return self + end + function layout:setLastAdded(columnIndex, component) + self.lastAdded[columnIndex] = component + return self + end + function layout:ignoreNext() + self.params.ignoreNext = true + return self + end + return layout +end + +function NewComponentFactory(container, layout) + return { + ["layout"] = layout, + ["doLayout"] = function(self, component) + if self.layout then + self.layout:layoutComponent(component) + end + end, + ["checkbox"] = function(self, text, tooltipText, optionLoc, clickFunc) + local checkbox, label = CreateLabeledCheckbox(container, text, tooltipText) + self:doLayout(checkbox) + checkbox:SetScript("OnClick", function(self) + SetOption(optionLoc, this:GetChecked() == 1) + if clickFunc then + clickFunc(self) + end + end) + checkbox:SetChecked(GetOption(optionLoc)) + return checkbox, label + end, + ["dropdown"] = function(self, text, tooltipText, optionLoc, options, selectFunc) + local dropdown, label = CreateLabeledDropdown(container, text, tooltipText) + self:doLayout(dropdown) + dropdown:SetSimpleOptions(options, function(option) + return { + text = option, + dropdownText = option, + initFunc = function(self) + self.checked = GetOption(optionLoc) == self.text + end, + func = function(self) + SetOption(optionLoc, self.text) + if selectFunc then + selectFunc(self) + end + end + } + end, GetOption(optionLoc)) + return dropdown, label + end, + ["slider"] = function(self, text, tooltipText, optionLoc, minValue, maxValue) + local slider, label = CreateLabeledSlider(container, text, tooltipText) + slider:SetMinMaxValues(minValue, maxValue) + slider:SetValue(GetOption(optionLoc)) + slider:GetSlider():SetNumberedText() + local script = slider:GetSlider():GetScript("OnValueChanged") + slider:GetSlider():SetScript("OnValueChanged", function(self) + script() + SetOption(optionLoc, self:GetValue()) + end) + self:doLayout(slider) + return slider, label + end, + ["label"] = function(self, text, tooltipText) + -- Dummy frame + local frame = PTGuiLib.Get("container", container) + :SetSize(20, 20) + local label = CreateLabel(container, text) + :SetPoint(GetLabelPoint(frame)) + self:doLayout(frame) + return label, frame + end + } +end + +function GetLabelPoint(relative) + return "RIGHT", relative, "LEFT", -5, 0 +end + +function CreateLabeledCheckbox(parent, text, tooltipText) + local checkbox = CreateCheckbox(parent) + local label = CreateLabel(parent, text) + label:SetPoint(GetLabelPoint(checkbox)) + checkbox:ApplyTooltip(tooltipText) + label:ApplyTooltip(tooltipText) + return checkbox, label +end + +function CreateLabeledDropdown(parent, text, tooltipText) + local dropdown = CreateDropdown(parent) + local label = CreateLabel(parent, text) + label:SetPoint(GetLabelPoint(dropdown)) + dropdown:ApplyTooltip(tooltipText) + label:ApplyTooltip(tooltipText) + return dropdown, label +end + +function CreateLabeledSlider(parent, text, tooltipText) + local slider = CreateSlider(parent) + local label = CreateLabel(parent, text) + label:SetPoint(GetLabelPoint(slider)) + slider:ApplyTooltip(tooltipText) + label:ApplyTooltip(tooltipText) + return slider, label +end + +function CreateLinkEditbox(parent, site) + return PTGuiLib.Get("editbox", parent) + :SetText(site) + :SetJustifyH("CENTER") + :SetScript("OnTextChanged", function(self) + self:SetText(site) + end) +end + +function CreateDropdown(parent, width) + return PTGuiLib.Get("dropdown", parent) + :SetSize(width or 140, 25) +end + +function CreateSlider(parent, width, height) + return PTGuiLib.Get("editbox_slider", parent) + :SetSize(width or 160, height or 36) +end + +function CreateCheckbox(parent, width, height) + return PTGuiLib.Get("checkbox", parent) + :SetSize(width or 20, height or 20) +end + +function CreateLabel(parent, text) + return PTGuiLib.GetText(parent, text) +end \ No newline at end of file diff --git a/gui/component/BindingOptions.lua b/gui/component/BindingOptions.lua new file mode 100644 index 0000000..96b3981 --- /dev/null +++ b/gui/component/BindingOptions.lua @@ -0,0 +1,255 @@ +PTBindingOptions = PTGuiComponent:Extend("puppeteer_binding_options") + +function PTBindingOptions:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + + local title = PTGuiLib.Get("title", frame) + :SetSize(192, 36) + :SetPoint("CENTER", frame, "TOP") + :SetText("Binding Options") + obj:AddComponent("title", title) + + + local margin = 150 + local yPos = -40 + + local tooltipTypeTooltip = "What type of text to show in the tooltip" + local tooltipTypeLabel = PTGuiLib.GetText(frame, "Tooltip Text") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(tooltipTypeTooltip) + local tooltipTypeDropdown = PTGuiLib.Get("dropdown", frame) + :SetPoint("LEFT", tooltipTypeLabel, "RIGHT", 5, 0) + :SetWidth(120) + :ApplyTooltip(tooltipTypeTooltip) + obj:AddComponent("TooltipTypeDropdown", tooltipTypeDropdown) + local initFunc = function(self, gui) + self.checked = self.text == gui:GetText() + end + local func = function(self) + obj.Binding.Tooltip.Type = self.type + obj.Binding.Tooltip.Data = nil + obj:UpdateTooltipType() + end + tooltipTypeDropdown:SetOptions({ + { + text = "Default", + type = "DEFAULT", + initFunc = initFunc, + func = func + }, + { + text = "Custom Text", + type = "CUSTOM", + initFunc = initFunc, + func = func + } + }) + + yPos = yPos - 30 + + local tooltipCustomTextTooltip = "The text shown in the tooltip" + local tooltipTextLabel = PTGuiLib.GetText(frame, "Custom Text") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(tooltipCustomTextTooltip) + obj:AddComponent("TooltipTextLabel", tooltipTextLabel) + local tooltipTextEditbox = PTGuiLib.Get("editbox", frame) + :SetPoint("LEFT", tooltipTextLabel, "RIGHT", 5, 0) + :SetSize(150, 20) + :ApplyTooltip(tooltipCustomTextTooltip) + :SetScript("OnTextChanged", function(self) + if not obj.Binding then + return + end + obj.Binding.Tooltip.Data = self:GetText() ~= "" and self:GetText() or nil + end) + obj:AddComponent("TooltipTextEditbox", tooltipTextEditbox) + + yPos = yPos - 30 + + local textColorTooltip = "Use a custom color for this binding in the tooltip" + local tooltipColorLabel = PTGuiLib.GetText(frame, "Custom Text Color") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(textColorTooltip) + local tooltipColorCheckbox = PTGuiLib.Get("checkbox", frame) + :SetPoint("LEFT", tooltipColorLabel, "RIGHT", 5, 0) + :SetSize(20, 20) + :ApplyTooltip(textColorTooltip) + :OnClick(function(self) + if self:GetChecked() then + local color = Puppeteer.BindTypeTooltipColors[obj.Binding.Type] + obj:GetTooltipColorSelect():SetColorRGB(color[1], color[2], color[3]) + obj.Binding.Tooltip.TextColor = {color[1], color[2], color[3]} + else + obj.Binding.Tooltip.TextColor = nil + end + obj:UpdateTooltipTextColor() + end) + obj:AddComponent("TooltipColorCheckbox", tooltipColorCheckbox) + + local colorSelect = PTGuiLib.Get("color_select", frame) + :SetPoint("LEFT", tooltipColorCheckbox, "RIGHT", 5, 0) + :SetSize(30, 20) + :SetColorSelectHandler(function(colorSelect) + local r, g, b = colorSelect:GetColorRGB() + obj.Binding.Tooltip.TextColor = {r, g, b} + end) + obj:AddComponent("TooltipColorSelect", colorSelect) + + yPos = yPos - 30 + + local targetWhileCastingTooltip = {"Target the unit while this binding runs", + "Note that these binding types override this rule:", + "Spell - Always targets unless using SuperWoW", + "Action - Never targets unless specified by action", + "Item - Always targets", + "Multi - Never targets"} + local targetWhileCastingLabel = PTGuiLib.GetText(frame, "Target While Casting") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(targetWhileCastingTooltip) + local targetWhileCastingDropdown = PTGuiLib.Get("dropdown", frame) + :SetPoint("LEFT", targetWhileCastingLabel, "RIGHT", 5, 0) + :SetWidth(120) + :ApplyTooltip(targetWhileCastingTooltip) + obj:AddComponent("TargetWhileCastingDropdown", targetWhileCastingDropdown) + + yPos = yPos - 30 + + local targetAfterCastingTooltip = "Target the unit after this binding runs" + local targetAfterCastingLabel = PTGuiLib.GetText(frame, "Target After Casting") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(targetAfterCastingTooltip) + local targetAfterCastingDropdown = PTGuiLib.Get("dropdown", frame) + :SetPoint("LEFT", targetAfterCastingLabel, "RIGHT", 5, 0) + :SetWidth(120) + :ApplyTooltip(targetAfterCastingTooltip) + obj:AddComponent("TargetAfterCastingDropdown", targetAfterCastingDropdown) + + local copy = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 30, 40) + :SetSize(120, 20) + :SetText("Copy") + :ApplyTooltip("Copy this binding in its current state to the clipboard") + :OnClick(function() + Puppeteer.SetBindingClipboard(obj.Binding) + obj:GetPasteButton():SetEnabled(true) + PlaySound("igSpellBookOpen") + end) + self:AddComponent("CopyButton", copy) + local paste = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -30, 40) + :SetSize(120, 20) + :SetText("Paste") + :ApplyTooltip("Overwrite this binding with your clipboard") + :SetEnabled(false) + :OnClick(function() + local binding = obj.Binding + PTUtil.ClearTable(binding) + for k, v in pairs(Puppeteer.GetBindingClipboard()) do + binding[k] = v + end + PlaySound("igSpellBookClose") + obj:Dispose() + end) + obj:AddComponent("PasteButton", paste) + + local done = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 5, 5) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 5) + :SetHeight(20) + :SetText("Done") + :OnClick(function() + Puppeteer.PruneBinding(obj.Binding) + obj:Dispose() + end) + return obj +end + +function PTBindingOptions:OnAcquire() + self.super.OnAcquire(self) + self:SetSize(350, 300) + local populateDropdown = function(dropdown, settingName) + local initFunc = function(option) + option.checked = self.Binding[settingName] == option.setting + end + local func = function(option, gui) + gui:SetText(option.text) + self.Binding[settingName] = option.setting + end + dropdown:SetTextUpdater(function(gui) + gui:SetTextBy("setting", self.Binding[settingName]) + end, true) + dropdown:SetOptions({ + { + text = "Use Default ("..(PTOptions[settingName] and "Yes" or "No")..")", + setting = nil, + initFunc = initFunc, + func = func + }, + { + text = "Yes", + setting = true, + initFunc = initFunc, + func = func + }, + { + text = "No", + setting = false, + initFunc = initFunc, + func = func + } + }) + end + populateDropdown(self:GetTargetWhileCastingDropdown(), "TargetWhileCasting") + populateDropdown(self:GetTargetAfterCastingDropdown(), "TargetAfterCasting") + + self:GetPasteButton():SetEnabled(Puppeteer.HasBindingClipboard()) +end + +function PTBindingOptions:OnDispose() + self.super.OnDispose(self) + self.Binding = nil +end + +PTBindingOptions:CreateGetter("TooltipTypeDropdown") +PTBindingOptions:CreateGetter("TooltipTextLabel") +PTBindingOptions:CreateGetter("TooltipTextEditbox") +PTBindingOptions:CreateGetter("TooltipColorCheckbox") +PTBindingOptions:CreateGetter("TooltipColorSelect") +PTBindingOptions:CreateGetter("TargetWhileCastingDropdown") +PTBindingOptions:CreateGetter("TargetAfterCastingDropdown") +PTBindingOptions:CreateGetter("CopyButton") +PTBindingOptions:CreateGetter("PasteButton") + +function PTBindingOptions:UpdateTooltipType() + local type = self.Binding and self.Binding.Tooltip.Type or "DEFAULT" + if type == "DEFAULT" then + self:GetTooltipTextLabel():Hide() + self:GetTooltipTextEditbox():Hide() + elseif type == "CUSTOM" then + self:GetTooltipTextLabel():Show() + self:GetTooltipTextEditbox():Show() + self:GetTooltipTextEditbox():SetText(self.Binding.Tooltip.Data or "") + end + self:GetTooltipTypeDropdown():SetTextBy("type", type) +end + +PTBindingOptions.UpdateTooltipTextColor = PTGuiUtil.CreateColorUpdater( + PTBindingOptions.GetTooltipColorCheckbox, + PTBindingOptions.GetTooltipColorSelect, + function(self) return self.Binding and self.Binding.Tooltip.TextColor end) + +function PTBindingOptions:SetBinding(binding) + self.Binding = binding + if not binding.Tooltip then + binding.Tooltip = {} + end + self:GetTargetWhileCastingDropdown():UpdateText() + self:GetTargetAfterCastingDropdown():UpdateText() + self:UpdateTooltipType() + self:UpdateTooltipTextColor() +end + +PTGuiLib.RegisterComponent(PTBindingOptions) \ No newline at end of file diff --git a/gui/component/BindingScriptEditor.lua b/gui/component/BindingScriptEditor.lua new file mode 100644 index 0000000..61614d0 --- /dev/null +++ b/gui/component/BindingScriptEditor.lua @@ -0,0 +1,161 @@ +PTBindingScriptEditor = PTGuiComponent:Extend("puppeteer_binding_script_editor") +local colorize = PTUtil.Colorize + +function PTBindingScriptEditor:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + local title = PTGuiLib.Get("title", frame) + :SetSize(192, 36) + :SetPoint("CENTER", frame, "TOP") + :SetText("Edit Binding Script") + obj:AddComponent("Title", title) + local editbox = PTGuiLib.Get("multi_line_editbox", frame) + :SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -155) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 30) + :SetScript("OnTabPressed", function(self) + self:GetHandle():Insert(" ") + end) + editbox:GetScrollFrame():SetSimpleBackground() + obj:AddComponent("Editbox", editbox) + + local margin = 130 + local yPos = -30 + + local customTextTooltip = "The text shown in the spells tooltip for this binding" + local name = PTGuiLib.GetText(frame, "Custom Tooltip Text") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(customTextTooltip) + local nameEditbox = PTGuiLib.Get("editbox", frame) + :SetPoint("LEFT", name, "RIGHT", 5, 0) + :SetSize(150, 20) + :ApplyTooltip(customTextTooltip) + :SetScript("OnTextChanged", function(self) + if not obj.Binding then + return + end + obj.Binding.Tooltip.Data = self:GetText() ~= "" and self:GetText() or nil + obj.Binding.Tooltip.Type = obj.Binding.Tooltip.Data and "CUSTOM" or "DEFAULT" + end) + obj:AddComponent("TooltipTextEditbox", nameEditbox) + + yPos = yPos - 30 + + local textColorTooltip = "Use a custom color for this binding in the tooltip" + local tooltipColorLabel = PTGuiLib.GetText(frame, "Custom Text Color") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(textColorTooltip) + local tooltipColorCheckbox = PTGuiLib.Get("checkbox", frame) + :SetPoint("LEFT", tooltipColorLabel, "RIGHT", 5, 0) + :SetSize(20, 20) + :ApplyTooltip(textColorTooltip) + :OnClick(function(self) + if self:GetChecked() then + local color = Puppeteer.BindTypeTooltipColors[obj.Binding.Type] + obj:GetTooltipColorSelect():SetColorRGB(color[1], color[2], color[3]) + obj.Binding.Tooltip.TextColor = {color[1], color[2], color[3]} + else + obj.Binding.Tooltip.TextColor = nil + end + obj:UpdateTooltipTextColor() + end) + obj:AddComponent("TooltipColorCheckbox", tooltipColorCheckbox) + + local colorSelect = PTGuiLib.Get("color_select", frame) + :SetPoint("LEFT", tooltipColorCheckbox, "RIGHT", 5, 0) + :SetSize(30, 20) + :SetColorSelectHandler(function(colorSelect) + local r, g, b = colorSelect:GetColorRGB() + obj.Binding.Tooltip.TextColor = {r, g, b} + end) + obj:AddComponent("TooltipColorSelect", colorSelect) + + yPos = yPos - 25 + + local predefined = PTGuiLib.GetText(frame, "Predefined local variables (hover for more info):") + :SetPoint("TOPLEFT", frame, "TOPLEFT", 5, yPos) + :SetJustifyH("LEFT") + + local local1 = PTGuiLib.GetText(frame, "local unit") + :SetTextColor(0.4, 1, 0.4) + :SetPoint("TOPLEFT", predefined, "BOTTOMLEFT", 0, 0) + :SetJustifyH("LEFT") + :ApplyTooltip("The unit being clicked", "This will be resolved to a GUID if clicking on a focus") + local local2 = PTGuiLib.GetText(frame, "local unresolvedUnit") + :SetTextColor(0.4, 1, 0.4) + :SetPoint("TOPLEFT", local1, "BOTTOMLEFT") + :SetJustifyH("LEFT") + :ApplyTooltip("The unresolved unit being clicked", + "If a focus is being clicked, this will be a phony", "unit, such as 'focus1'") + local local3 = PTGuiLib.GetText(frame, "local unitData") + :SetTextColor(0.4, 1, 0.4) + :SetPoint("TOPLEFT", local2, "BOTTOMLEFT") + :SetJustifyH("LEFT") + :ApplyTooltip("Cache stored by Puppeteer about the unit", + "Some useful APIs:", + "unitData:HasBuff(buffName) -- Returns true if the unit has buffName", + "unitData:HasDebuff(debuffName) -- Returns true if the unit has debuffName", + "unitData:HasDebuffType(typeName) -- Returns true if the unit has the debuff type(such as \"Magic\")", + "unitData:GetAuraTimeRemaining(auraName) -- Returns the time remaining on an aura, or nil if unknown(SuperWoW required)") + local local4 = PTGuiLib.GetText(frame, "local unitFrame") + :SetTextColor(0.4, 1, 0.4) + :SetPoint("TOPLEFT", local3, "BOTTOMLEFT") + :SetJustifyH("LEFT") + :ApplyTooltip("The PTUnitFrame instance that was clicked") + + local saveButton = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 5) + :SetSize(150, 20) + :SetText("Save") + :OnClick(function() + obj.Callback(true, editbox:GetText(), nameEditbox:GetText()) + end, true) + local cancelButton = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 5, 5) + :SetSize(150, 20) + :SetText("Cancel") + :OnClick(function() + obj.Callback(false) + end, true) + return obj +end + +function PTBindingScriptEditor:OnAcquire() + self.super.OnAcquire(self) + self:GetEditbox():SetText("") + self:GetEditbox():GetScrollFrame():FixNextUpdate() + self:SetSize(375, 400) +end + +function PTBindingScriptEditor:OnDispose() + self.super.OnDispose(self) +end + +PTBindingScriptEditor:CreateGetter("Editbox") +PTBindingScriptEditor:CreateGetter("TooltipTextEditbox") +PTBindingScriptEditor:CreateGetter("TooltipColorCheckbox") +PTBindingScriptEditor:CreateGetter("TooltipColorSelect") + +PTBindingScriptEditor.UpdateTooltipTextColor = PTGuiUtil.CreateColorUpdater( + PTBindingScriptEditor.GetTooltipColorCheckbox, + PTBindingScriptEditor.GetTooltipColorSelect, + function(self) return self.Binding and self.Binding.Tooltip.TextColor end) + +function PTBindingScriptEditor:SetCallback(func) + self.Callback = func + return self +end + +function PTBindingScriptEditor:SetBinding(binding) + self.Binding = binding + if not binding.Tooltip then + binding.Tooltip = {} + end + self:GetEditbox():SetText(binding.Data or "") + self:UpdateTooltipTextColor() + self:GetTooltipTextEditbox():SetText(binding.Tooltip.Type == "CUSTOM" and binding.Tooltip.Data or "") + return self +end + +PTGuiLib.RegisterComponent(PTBindingScriptEditor) \ No newline at end of file diff --git a/gui/component/ButtonEditor.lua b/gui/component/ButtonEditor.lua new file mode 100644 index 0000000..ab3ea99 --- /dev/null +++ b/gui/component/ButtonEditor.lua @@ -0,0 +1,415 @@ +PTButtonEditor = PTGuiComponent:Extend("puppeteer_button_editor") +local util = PTUtil +local RotateTexture = util.RotateTexture + +function PTButtonEditor:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + + local title = PTGuiLib.Get("title", frame) + :SetSize(192, 36) + :SetPoint("CENTER", frame, "TOP") + :SetText("Edit Buttons") + obj:AddComponent("Title", title) + + local scrollFrame = PTGuiLib.Get("scroll_frame", frame) + :SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -50) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 100) + :SetSimpleBackground() + obj:AddComponent("ScrollFrame", scrollFrame) + + local controlsLabel = PTGuiLib.Get("text", frame) + :SetText("Controls") + :SetWidth(66) + :SetHeight(30) + :SetPoint("TOPLEFT", scrollFrame:GetHandle(), "TOPLEFT", 5, 33) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + :SetJustifyV("BOTTOM") + local buttonIDLabel = PTGuiLib.Get("text", frame) + :SetText("Button ID") + :SetWidth(100) + :SetHeight(30) + :SetPoint("LEFT", controlsLabel, "RIGHT", 0, 0) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + :SetJustifyV("BOTTOM") + local customNameLabel = PTGuiLib.Get("text", frame) + :SetText("Custom Name") + :SetWidth(130) + :SetHeight(30) + :SetPoint("LEFT", buttonIDLabel, "RIGHT", 0, 0) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + :SetJustifyV("BOTTOM") + local showUnboundLabel = PTGuiLib.Get("text", frame) + :SetText("Show Unbound") + :SetWidth(82) + :SetHeight(30) + :SetPoint("LEFT", customNameLabel, "RIGHT", 0, 0) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + :SetJustifyV("BOTTOM") + + local done = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOM", frame, "BOTTOM", 0, 5) + :SetText("Done") + :SetSize(200, 20) + :OnClick(function() + local Buttons = PTOptions.Buttons + local buttonInfo = PTOptions.ButtonInfo + PTUtil.ClearTable(Buttons) + for _, line in ipairs(obj.Lines) do + local button = line.Button + table.insert(Buttons, button) + if not buttonInfo[button] then + buttonInfo[button] = {} + end + local info = buttonInfo[button] + info.Name = line:GetCustomButtonName() + info.ShowUnbound = line:IsUnboundShown() + end + PTSettingsGui.ReloadBindingLines() + PTSettingsGui.UpdateBindingsInterface() + Puppeteer.InitBindingDisplayCache() + Puppeteer.InitOverrideBindingsMapping() + obj:Dispose() + PTSettingsGui.PopOverlayFrame() + end) + + local addButton = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOM", done, "TOP", 0, 10) + :SetSize(200, 50) + :SetText("Add a Button") + :OnClick(function(self) + if not obj.AddMode then + obj:SetAddMode(true) + end + end) + + addButton:HookScript("OnMouseUp", function(self) + if not obj.AddMode then + return + end + if not obj:ContainsButton(arg1) then + obj:AddButtonLine(arg1, arg1) + PlaySound("igSpellBookOpen") + end + obj:SetAddMode(false) + local onClick = addButton:GetScript("OnClick") + addButton:SetScript("OnClick", nil) + PTUtil.RunLater(function() + addButton:SetScript("OnClick", onClick, true) + end) + end) + addButton:SetScript("OnKeyUp", function(self) + if not obj.AddMode then + return + end + if arg1 == "ESCAPE" then + obj:SetAddMode(false) + return + end + if not obj:ContainsButton(arg1) then + if not obj:AreBindingsFull() then + obj:AddButtonLine(arg1, arg1) + PlaySound("igSpellBookOpen") + else + Puppeteer.Info("You cannot add any more non-mouse buttons!") + end + end + obj:SetAddMode(false) + end) + addButton:SetScript("OnMouseWheel", function(self) + if not obj.AddMode then + return + end + if arg1 > 0 then + if not obj:ContainsButton("MOUSEWHEELUP") then + if not obj:AreBindingsFull() then + obj:AddButtonLine("MOUSEWHEELUP", "MOUSEWHEELUP") + PlaySound("igSpellBookOpen") + else + Puppeteer.Info("You cannot add any more non-mouse buttons!") + end + end + elseif arg1 < 0 then + if not obj:ContainsButton("MOUSEWHEELDOWN") then + if not obj:AreBindingsFull() then + obj:AddButtonLine("MOUSEWHEELDOWN", "MOUSEWHEELDOWN") + PlaySound("igSpellBookOpen") + else + Puppeteer.Info("You cannot add any more non-mouse buttons!") + end + end + end + obj:SetAddMode(false) + end) + obj:AddComponent("AddButton", addButton) + obj:SetAddMode(false) + + obj.Lines = {} + obj.LineMap = {} + return obj +end + +function PTButtonEditor:OnAcquire() + self.super.OnAcquire(self) + self:SetSize(425, 400) + self:GetScrollFrame():FixNextUpdate() + for _, button in ipairs(PTOptions.Buttons) do + self:AddButtonLine(button, button) + end +end + +function PTButtonEditor:OnDispose() + self.super.OnDispose(self) + self:ClearButtonLines() + self:SetAddMode(false) +end + +PTButtonEditor:CreateGetter("ScrollFrame") +PTButtonEditor:CreateGetter("AddButton") + +function PTButtonEditor:SetAddMode(addMode) + self.AddMode = addMode + self:GetAddButton():GetHandle():EnableKeyboard(self.AddMode) + self:GetAddButton():GetHandle():EnableMouseWheel(self.AddMode) + self:GetAddButton():SetText(self.AddMode and "Press any button, key,\nor mouse wheel to bind\nPress Esc to Cancel" + or "Left click to start\nbinding a button") +end + +function PTButtonEditor:ContainsButton(button) + return self.LineMap[button] ~= nil +end + +function PTButtonEditor:GetNumUsedBindings() + local nonBindingButtons = util.GetAllButtonsSet() + local used = 0 + for button, _ in pairs(self.LineMap) do + if not nonBindingButtons[button] then + used = used + 1 + end + end + return used +end + +function PTButtonEditor:AreBindingsFull() + return self:GetNumUsedBindings() >= 24 +end + +function PTButtonEditor:SwapIndexes(index1, index2) + self.Lines[index1], self.Lines[index2] = self.Lines[index2], self.Lines[index1] + self:UpdateLinePositions() +end + +function PTButtonEditor:AddButtonLine(id, button) + local line = PTGuiLib.Get("puppeteer_button_line", self:GetScrollFrame()) + :SetSize(380, 25) + :SetButton(button) + :SetControlHandler(self) + + table.insert(self.Lines, line) + if id then + self.LineMap[id] = line + end + + self:UpdateLinePositions() + return line +end + +function PTButtonEditor:RemoveButtonLine(line) + for id, l in pairs(self.LineMap) do + if line == l then + self.LineMap[id] = nil + end + end + PTUtil.RemoveElement(self.Lines, line) + line:Dispose() + + self:UpdateLinePositions() +end + +function PTButtonEditor:ClearButtonLines() + for _, line in ipairs(self.Lines) do + line:Dispose() + end + PTUtil.ClearTable(self.Lines) + PTUtil.ClearTable(self.LineMap) +end + +function PTButtonEditor:UpdateLinePositions() + local INTERVAL = 26 + for i, line in ipairs(self.Lines) do + line:SetPoint("TOPLEFT", self:GetScrollFrame(), "TOPLEFT", 5, -5 - (INTERVAL * (i - 1))) + end + self:GetScrollFrame():UpdateScrollChildRect() +end + +-- Control handler functions + +function PTButtonEditor:OnLineDelete(line) + self:RemoveButtonLine(line) +end + +function PTButtonEditor:OnLineUp(line) + local index = PTUtil.IndexOf(self.Lines, line) + if IsShiftKeyDown() then + for i = index, 2, -1 do + self:SwapIndexes(i, i - 1) + end + elseif index > 1 and table.getn(self.Lines) > 1 then + self:SwapIndexes(index, index - 1) + end +end + +function PTButtonEditor:OnLineDown(line) + local index = PTUtil.IndexOf(self.Lines, line) + local lineCount = table.getn(self.Lines) + if IsShiftKeyDown() then + for i = index, lineCount - 1 do + self:SwapIndexes(i, i + 1) + end + elseif index < table.getn(self.Lines) then + self:SwapIndexes(index, index + 1) + end +end + +-- End of control handler functions + +PTGuiLib.RegisterComponent(PTButtonEditor) + +-- Button line component + +PTButtonLine = PTGuiComponent:Extend("puppeteer_button_line") + +function PTButtonLine:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + + local delete = PTGuiLib.Get("button", frame) + :SetPoint("LEFT", frame, "LEFT", 0, 0) + :SetSize(20, 20) + :ApplyTooltip("Delete this button") + :OnClick(function() + if obj.ControlHandler then + obj.ControlHandler:OnLineDelete(obj) + end + end) + delete.NormalTexture:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up") + RotateTexture(delete.NormalTexture, 180, 0.7) + delete.PushedTexture:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Down") + RotateTexture(delete.PushedTexture, 180, 0.7) + delete:SetUseDefaultTextures(true) + + local up = PTGuiLib.Get("button", frame) + :SetPoint("LEFT", delete, "RIGHT", 6, 0) + :SetSize(20, 20) + :ApplyTooltip("Move this button up", "Hold shift to move to top") + :OnClick(function() + if obj.ControlHandler then + obj.ControlHandler:OnLineUp(obj) + end + end) + up.NormalTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up") + RotateTexture(up.NormalTexture, -90, 0.75) + up.PushedTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down") + RotateTexture(up.PushedTexture, -90, 0.75) + up.HighlightTexture:SetTexture("Interface\\Buttons\\UI-Common-MouseHilight") + up.HighlightTexture:SetTexCoord(0, 1, 0, 1) + up:SetUseDefaultTextures(true) + + local down = PTGuiLib.Get("button", frame) + :SetPoint("LEFT", up, "RIGHT", 1, 0) + :SetSize(20, 20) + :ApplyTooltip("Move this button down", "Hold shift to move to bottom") + :OnClick(function() + if obj.ControlHandler then + obj.ControlHandler:OnLineDown(obj) + end + end, true) + down.NormalTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up") + RotateTexture(down.NormalTexture, 90, 0.75) + down.PushedTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down") + RotateTexture(down.PushedTexture, 90, 0.75) + down.HighlightTexture:SetTexture("Interface\\Buttons\\UI-Common-MouseHilight") + down.HighlightTexture:SetTexCoord(0, 1, 0, 1) + down:SetUseDefaultTextures(true) + + + local buttonNameLabel = PTGuiLib.Get("text", frame) + :SetPoint("LEFT", down, "RIGHT", 5, 0) + :SetText("Left") + :SetWidth(90) + :SetHeight(30) + :SetJustifyH("RIGHT") + :SetNonSpaceWrap(true) + obj:AddComponent("ButtonNameLabel", buttonNameLabel) + + local nameEditbox = PTGuiLib.Get("editbox", frame) + :SetPoint("LEFT", buttonNameLabel, "RIGHT", 5, 0) + :SetSize(130, 20) + :SetScript("OnEnterPressed", function(self) + self:ClearFocus() + end) + :ApplyTooltip("This name will be shown on the binding tooltip") + obj:AddComponent("NameEditbox", nameEditbox) + + local showUnboundCheckbox = PTGuiLib.Get("checkbox", frame) + :SetPoint("LEFT", nameEditbox, "RIGHT", 30, 0) + :SetSize(20, 20) + :ApplyTooltip("If enabled, the binding tooltip will show this key when unbound") + obj:AddComponent("ShowUnboundCheckbox", showUnboundCheckbox) + + + return obj +end + +function PTButtonLine:OnAcquire() + self.super.OnAcquire(self) + self:SetSize(380, 25) +end + +function PTButtonLine:OnDispose() + self.super.OnDispose(self) + self.Button = nil +end + +PTButtonLine:CreateGetter("ButtonNameLabel") +PTButtonLine:CreateGetter("NameEditbox") +PTButtonLine:CreateGetter("ShowUnboundCheckbox") + +function PTButtonLine:SetButton(button) + self.Button = button + self:GetButtonNameLabel():SetText(util.GetButtonName(button)) + local buttonInfo = PTOptions.ButtonInfo + if not buttonInfo[button] then + buttonInfo[button] = {Name = util.GetButtonName(button), ShowUnbound = true} + end + self:GetNameEditbox():SetText(buttonInfo[button].Name or util.GetButtonName(button)) + self:GetShowUnboundCheckbox():SetChecked(buttonInfo[button].ShowUnbound) + return self +end + +-- The handler is expected to have functions OnLineDelete, OnLineUp, OnLineDown +function PTButtonLine:SetControlHandler(handler) + self.ControlHandler = handler + return self +end + +function PTButtonLine:GetCustomButtonName() + local text = self:GetNameEditbox():GetText() + if text == "" then + text = util.GetButtonName(self.Button) + end + return text +end + +function PTButtonLine:IsUnboundShown() + return self:GetShowUnboundCheckbox():GetChecked() == 1 +end + +PTGuiLib.RegisterComponent(PTButtonLine) \ No newline at end of file diff --git a/gui/component/InputBlock.lua b/gui/component/InputBlock.lua new file mode 100644 index 0000000..d0d4e58 --- /dev/null +++ b/gui/component/InputBlock.lua @@ -0,0 +1,22 @@ +PTInputBlock = PTGuiComponent:Extend("puppeteer_input_block") + +function PTInputBlock:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetBackground({frameBackdrop = { + bgFile = "Interface\\Buttons\\WHITE8X8", + edgeSize = 11, + tile = true + }, frameBackdropColor = {0, 0, 0, 0.5}}) + frame:EnableMouse(true) + return obj +end + +function PTInputBlock:SetParent(parent) + self.super.SetParent(self, parent) + self:SetPoint("TOPLEFT", parent, "TOPLEFT") + self:SetPoint("BOTTOMRIGHT", parent, "BOTTOMRIGHT") +end + +PTGuiLib.RegisterComponent(PTInputBlock) \ No newline at end of file diff --git a/gui/component/LoadScriptEditor.lua b/gui/component/LoadScriptEditor.lua new file mode 100644 index 0000000..465aa2f --- /dev/null +++ b/gui/component/LoadScriptEditor.lua @@ -0,0 +1,59 @@ +PTLoadScriptEditor = PTGuiComponent:Extend("puppeteer_load_script_editor") +local colorize = PTUtil.Colorize + +function PTLoadScriptEditor:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + local title = PTGuiLib.Get("title", frame) + :SetSize(192, 36) + :SetPoint("CENTER", frame, "TOP") + :SetText("Edit Script") + obj:AddComponent("Title", title) + local editbox = PTGuiLib.Get("multi_line_editbox", frame) + :SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -30) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 30) + :SetScript("OnTabPressed", function(self) + self:GetHandle():Insert(" ") + end) + editbox:GetScrollFrame():SetSimpleBackground() + obj:AddComponent("Editbox", editbox) + + local saveButton = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 5) + :SetSize(150, 20) + :SetText("Save") + :OnClick(function() + obj.Callback(true, editbox:GetText()) + end, true) + local cancelButton = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 5, 5) + :SetSize(150, 20) + :SetText("Cancel") + :OnClick(function() + obj.Callback(false) + end, true) + return obj +end + +function PTLoadScriptEditor:OnAcquire() + self.super.OnAcquire(self) + self:GetEditbox():SetText("") + self:GetEditbox():GetScrollFrame():FixNextUpdate() + self:SetSize(375, 325) +end + +PTLoadScriptEditor:CreateGetter("Editbox") + +function PTLoadScriptEditor:SetCallback(func) + self.Callback = func + return self +end + +function PTLoadScriptEditor:SetTitle(title) + self:GetComponent("Title"):SetText(title) + return self +end + +PTGuiLib.RegisterComponent(PTLoadScriptEditor) \ No newline at end of file diff --git a/gui/component/MultiEditor.lua b/gui/component/MultiEditor.lua new file mode 100644 index 0000000..ca977af --- /dev/null +++ b/gui/component/MultiEditor.lua @@ -0,0 +1,155 @@ +PTMultiEditor = PTGuiComponent:Extend("puppeteer_multi_editor") + +function PTMultiEditor:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + + local title = PTGuiLib.Get("title", frame) + :SetSize(192, 36) + :SetPoint("CENTER", frame, "TOP") + :SetText("Edit Multi Binding") + obj:AddComponent("title", title) + + local interface = PTGuiLib.Get("puppeteer_spell_bind_interface", frame) + :SetPoint("TOPLEFT", frame, "TOPLEFT", 5, -75) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -5, 60) + :SetLeftLabel("Controls") + obj:AddComponent("BindInterface", interface) + + local margin = 120 + local yPos = -30 + + local titleTooltip = {"The title of the Multi-menu, leave blank for no title", + "If there's no custom tooltip text set, this title will be", "used in the spells tooltip"} + local titleLabel = PTGuiLib.GetText(frame, "Menu Title") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(titleTooltip) + local titleEditbox = PTGuiLib.Get("editbox", frame) + :SetPoint("LEFT", titleLabel, "RIGHT", 5, 0) + :SetSize(140, 20) + :ApplyTooltip(titleTooltip) + :SetScript("OnTextChanged", function(self) + obj.Binding.Data.Title = self:GetText() ~= "" and self:GetText() or nil + end) + obj:AddComponent("TitleEditbox", titleEditbox) + + yPos = yPos - 25 + + local titleColorTooltip = "Use a custom color for the title" + local titleColorLabel = PTGuiLib.GetText(frame, "Title Color") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + :ApplyTooltip(titleColorTooltip) + local titleColorCheckbox = PTGuiLib.Get("checkbox", frame) + :SetPoint("LEFT", titleColorLabel, "RIGHT", 5, 0) + :SetSize(20, 20) + :ApplyTooltip(titleColorTooltip) + :OnClick(function(self) + if self:GetChecked() then + local color = Puppeteer.BindTypeTooltipColors[obj.Binding.Type] + obj:GetTitleColorSelect():SetColorRGB(color[1], color[2], color[3]) + obj.Binding.Data.TitleColor = {color[1], color[2], color[3]} + else + obj.Binding.Data.TitleColor = nil + end + obj:UpdateTitleTextColor() + end) + obj:AddComponent("TitleColorCheckbox", titleColorCheckbox) + local titleColorSelect = PTGuiLib.Get("color_select", frame) + :SetPoint("LEFT", titleColorCheckbox, "RIGHT", 5, 0) + :SetSize(30, 20) + :SetColorSelectHandler(function(self) + obj.Binding.Data.TitleColor = self:GetColor() + end) + obj:AddComponent("TitleColorSelect", titleColorSelect) + + + + + local multiKeepOpenTooltip = {"If enabled, the Multi-menu will stay open after clicking a binding", + "It will still close if a different menu is triggered"} + local multiKeepOpenLabel = PTGuiLib.GetText(frame, "Keep Open") + :SetPoint("LEFT", titleEditbox, "RIGHT", 30, 0) + :ApplyTooltip(multiKeepOpenTooltip) + local multiKeepOpenCheckbox = PTGuiLib.Get("checkbox", frame) + :SetPoint("LEFT", multiKeepOpenLabel, "RIGHT", 5, 0) + :SetSize(20, 20) + :ApplyTooltip(multiKeepOpenTooltip) + :OnClick(function(self) + if self:GetChecked() then + obj.Binding.Data.KeepOpen = true + else + obj.Binding.Data.KeepOpen = nil + end + end) + obj:AddComponent("KeepOpenCheckbox", multiKeepOpenCheckbox) + + + local done = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOM", frame, "BOTTOM", 0, 5) + :SetSize(200, 20) + :SetText("Done") + :OnClick(function() + obj:Dispose() + end) + + local add = PTGuiLib.Get("button", frame) + :SetPoint("BOTTOM", done, "TOP", 0, 5) + :SetSize(200, 20) + :SetText("Add Binding") + :OnClick(function() + if table.getn(interface.Bindings) >= 20 then + Puppeteer.Info("Cannot add any more bindings!") + return + end + local binding = {} + table.insert(interface.Bindings, binding) + local line = interface:AddSpellLine() + line:SetMode(PTSpellLine.MODE_ORDERED) + line:SetBinding(binding) + interface:UpdateLinePositions() + end) + return obj +end + +function PTMultiEditor:OnAcquire() + self.super.OnAcquire(self) + self:SetSize(425, 360) + self:GetBindInterface():GetScrollFrame():FixNextUpdate() +end + +function PTMultiEditor:OnDispose() + self.super.OnDispose(self) + self:GetBindInterface():ClearSpellLines() +end + +PTMultiEditor:CreateGetter("BindInterface") +PTMultiEditor:CreateGetter("TitleEditbox") +PTMultiEditor:CreateGetter("TitleColorCheckbox") +PTMultiEditor:CreateGetter("TitleColorSelect") +PTMultiEditor:CreateGetter("KeepOpenCheckbox") + +PTMultiEditor.UpdateTitleTextColor = PTGuiUtil.CreateColorUpdater( + PTMultiEditor.GetTitleColorCheckbox, + PTMultiEditor.GetTitleColorSelect, + function(self) return self.Binding.Data.TitleColor end) + +function PTMultiEditor:SetBinding(multiBinding) + self.Binding = multiBinding + local interface = self:GetBindInterface() + if not multiBinding.Data then + multiBinding.Data = {Bindings = {}} + end + interface:SetBindings(multiBinding.Data.Bindings) + for i, binding in ipairs(multiBinding.Data.Bindings) do + local line = interface:AddSpellLine(i, "") + line:SetMode(PTSpellLine.MODE_ORDERED) + line:SetBinding(binding) + end + self:GetTitleEditbox():SetText(multiBinding.Data.Title or "") + self:UpdateTitleTextColor() + self:GetKeepOpenCheckbox():SetChecked(multiBinding.Data.KeepOpen) +end + +PTGuiLib.RegisterComponent(PTMultiEditor) \ No newline at end of file diff --git a/gui/component/NewLoadout.lua b/gui/component/NewLoadout.lua new file mode 100644 index 0000000..94019a0 --- /dev/null +++ b/gui/component/NewLoadout.lua @@ -0,0 +1,101 @@ +PTNewLoadout = PTGuiComponent:Extend("puppeteer_new_loadout") + +function PTNewLoadout:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + + local title = PTGuiLib.Get("title", frame) + :SetSize(192, 36) + :SetPoint("CENTER", frame, "TOP") + :SetText("New Bindings Loadout") + obj:AddComponent("Title", title) + + local margin = 115 + local yPos = -35 + + local inheritLabel = PTGuiLib.GetText(frame, "Copy From") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + local inheritDropdown = PTGuiLib.Get("dropdown", frame) + :SetPoint("LEFT", inheritLabel, "RIGHT", 5, 0) + :SetWidth(150) + :SetDynamicOptions(function(addOption, level, args) + addOption("text", "", + "dropdownText", "", + "initFunc", args.initFunc) + for _, name in ipairs(Puppeteer.GetBindingLoadoutNames()) do + addOption("text", name, + "dropdownText", name, + "initFunc", args.initFunc) + end + end, { + initFunc = function(self, gui) + self.checked = self.text == gui:GetText() + end + }) + :SetText("") + obj:AddComponent("InheritDropdown", inheritDropdown) + + yPos = yPos - 30 + + local nameLabel = PTGuiLib.GetText(frame, "Loadout Name") + :SetPoint("RIGHT", frame, "TOPLEFT", margin, yPos) + local nameEditbox = PTGuiLib.Get("editbox", frame) + :SetPoint("LEFT", nameLabel, "RIGHT", 5, 0) + :SetSize(150, 20) + :SetScript("OnEnterPressed", function(self) + self:ClearFocus() + end) + obj:AddComponent("NameEditbox", nameEditbox) + + local cancelButton = PTGuiLib.Get("button", frame) + :SetText("Cancel") + :SetPoint("BOTTOM", frame, "BOTTOM", 0, 5) + :SetSize(150, 20) + :OnClick(function() + PTSettingsGui.PopOverlayFrame() + obj:Dispose() + end) + cancelButton:SetClickSound(PlaySound, "igMainMenuClose") + + local createLoadoutButton = PTGuiLib.Get("button", frame) + :SetText("Create Loadout") + :SetPoint("BOTTOM", cancelButton, "TOP", 0, 5) + :SetSize(150, 20) + :OnClick(function() + local inheritFrom = inheritDropdown:GetText() + local name = nameEditbox:GetText() + if name == "" or name == "" then + return + end + local loadouts = Puppeteer.GetBindingLoadouts() + if loadouts[name] then + DEFAULT_CHAT_FRAME:AddMessage("Loadout with that name already exists!") + return + end + local loadout = Puppeteer.NewBindingsLoadout(name, loadouts[inheritFrom]) + Puppeteer.SetSelectedBindingsLoadout(name) + PTSettingsGui.PopOverlayFrame() + obj:Dispose() + end) + createLoadoutButton:SetClickSound(PlaySound, "igMainMenuClose") + + return obj +end + +function PTNewLoadout:OnAcquire() + self.super.OnAcquire(self) + self:SetSize(300, 150) + self:GetInheritDropdown():SetText("") + self:GetNameEditbox():SetText("") +end + +function PTNewLoadout:OnDispose() + self.super.OnDispose(self) +end + +PTNewLoadout:CreateGetter("InheritDropdown") +PTNewLoadout:CreateGetter("NameEditbox") + +PTGuiLib.RegisterComponent(PTNewLoadout) \ No newline at end of file diff --git a/gui/component/SpellBindInterface.lua b/gui/component/SpellBindInterface.lua new file mode 100644 index 0000000..53fee36 --- /dev/null +++ b/gui/component/SpellBindInterface.lua @@ -0,0 +1,162 @@ +PTSpellBindInterface = PTGuiComponent:Extend("puppeteer_spell_bind_interface") + +function PTSpellBindInterface:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + + local buttonLabel = PTGuiLib.Get("text", frame) + :SetText("Button") + :SetWidth(65) + :SetPoint("TOPLEFT", frame, "TOPLEFT", 5, 0) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + local typeLabel = PTGuiLib.Get("text", frame) + :SetText("Type") + :SetWidth(40) + :SetPoint("LEFT", buttonLabel, "RIGHT", 20, 0) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + local bindingLabel = PTGuiLib.Get("text", frame) + :SetText("Binding") + :SetWidth(210) + :SetPoint("LEFT", typeLabel, "RIGHT", 20, 0) + :SetFontSize(14) + :SetFontFlags("OUTLINE") + + local bindingScrollFrame = PTGuiLib.Get("scroll_frame", frame) + :SetPoint("TOPLEFT", frame, "TOPLEFT", 0, -20) + :SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0) + :SetSimpleBackground() + obj:AddComponent("ScrollFrame", bindingScrollFrame) + obj:AddComponent("LeftLabel", buttonLabel) + + obj.Lines = {} + obj.LineMap = {} + + return obj +end + +function PTSpellBindInterface:OnAcquire() + self.super.OnAcquire(self) + self:GetScrollFrame():FixNextUpdate() +end + +function PTSpellBindInterface:OnDispose() + self:ClearSpellLines() + self.super.OnDispose(self) +end + +PTSpellBindInterface:CreateGetter("ScrollFrame") +PTSpellBindInterface:CreateGetter("LeftLabel") + +function PTSpellBindInterface:SetLeftLabel(text) + self:GetLeftLabel():SetText(text) + return self +end + +function PTSpellBindInterface:SetBindings(bindings) + self.Bindings = bindings + for id, line in pairs(self.LineMap) do + line:SetBinding(bindings[id]) + end +end + +function PTSpellBindInterface:GetSpellLine(id) + return self.LineMap[id] +end + +function PTSpellBindInterface:SwapIndexes(index1, index2) + self.Lines[index1], self.Lines[index2] = self.Lines[index2], self.Lines[index1] + self.Bindings[index1], self.Bindings[index2] = self.Bindings[index2], self.Bindings[index1] + self:UpdateLinePositions() +end + +function PTSpellBindInterface:AddSpellLine(id, label) + local line = PTGuiLib.Get("puppeteer_spell_line", self:GetScrollFrame()) + :SetSize(380, 25) + :SetLabel(label) + :SetControlHandler(self) + + table.insert(self.Lines, line) + if id then + self.LineMap[id] = line + end + + self:UpdateLinePositions() + return line +end + +function PTSpellBindInterface:RemoveSpellLineByID(id, noDispose) + local line = self.LineMap[id] + self.LineMap[id] = nil + PTUtil.RemoveElement(self.Lines, line) + if not noDispose then + line:Dispose() + end + + self:UpdateLinePositions() +end + +function PTSpellBindInterface:RemoveSpellLine(line) + for id, l in pairs(self.LineMap) do + if line == l then + self.LineMap[id] = nil + end + end + PTUtil.RemoveElement(self.Lines, line) + line:Dispose() + + self:UpdateLinePositions() +end + +function PTSpellBindInterface:ClearSpellLines() + for _, line in ipairs(self.Lines) do + line:Dispose() + end + PTUtil.ClearTable(self.Lines) + PTUtil.ClearTable(self.LineMap) +end + +function PTSpellBindInterface:UpdateLinePositions() + local INTERVAL = 26 + for i, line in ipairs(self.Lines) do + line:SetPoint("TOPLEFT", self:GetScrollFrame(), "TOPLEFT", 5, -5 - (INTERVAL * (i - 1))) + end + self:GetScrollFrame():UpdateScrollChildRect() +end + +-- Control handler functions + +function PTSpellBindInterface:OnLineDelete(line) + local index = PTUtil.IndexOf(self.Lines, line) + self:RemoveSpellLine(line) + table.remove(self.Bindings, index) +end + +function PTSpellBindInterface:OnLineUp(line) + local index = PTUtil.IndexOf(self.Lines, line) + if IsShiftKeyDown() then + for i = index, 2, -1 do + self:SwapIndexes(i, i - 1) + end + elseif index > 1 and table.getn(self.Lines) > 1 then + self:SwapIndexes(index, index - 1) + end +end + +function PTSpellBindInterface:OnLineDown(line) + local index = PTUtil.IndexOf(self.Lines, line) + local lineCount = table.getn(self.Lines) + if IsShiftKeyDown() then + for i = index, lineCount - 1 do + self:SwapIndexes(i, i + 1) + end + elseif index < table.getn(self.Lines) then + self:SwapIndexes(index, index + 1) + end +end + +-- End of control handler functions + +PTGuiLib.RegisterComponent(PTSpellBindInterface) \ No newline at end of file diff --git a/gui/component/SpellLine.lua b/gui/component/SpellLine.lua new file mode 100644 index 0000000..158310d --- /dev/null +++ b/gui/component/SpellLine.lua @@ -0,0 +1,424 @@ +PTSpellLine = PTGuiComponent:Extend("puppeteer_spell_line") +local compost = PTPool +local util = PTUtil +local RotateTexture = util.RotateTexture +local colorize = util.Colorize +local SearchSpells = util.SearchSpells +local SearchMacros = util.SearchMacros +local SearchItems = util.SearchItems + +local bindHelper = PTGuiLib.Get("dropdown", UIParent) + +function PTSpellLine:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + + local initFunc = function(self, gui) + self.checked = self.text == gui:GetText() + end + local func = function(self, gui) + gui:SetText(self.text) + obj:SetBindType(self.type) + end + local createTypeEntry = function(type, text, tooltip) + return { + type = type, + text = text, + tooltipTitle = text, + tooltipText = tooltip, + initFunc = initFunc, + func = func + } + end + local typeDropdown = PTGuiLib.Get("dropdown", frame) + :SetShowShadow(false) + :SetPoint("LEFT", frame, "LEFT", 70, 0) + :SetWidth(70) + :SetText("Spell") + :SetOptions({ + createTypeEntry("SPELL", "Spell", "Bind a spell from your spellbook"), + createTypeEntry("ACTION", "Action", "Bind a special action"), + createTypeEntry("ITEM", "Item", "Bind an item from your inventory"), + createTypeEntry("MACRO", "Macro", "Bind one of your macros"), + createTypeEntry("SCRIPT", "Script", "Run a custom Lua script"), + createTypeEntry("MULTI", "Multi", "Open an interface with multiple bindings") + }) + + local buttonText = PTGuiLib.Get("text", frame) + :SetPoint("RIGHT", typeDropdown, "LEFT", -5, 0) + :SetText("Left") + :SetWidth(65) + :SetHeight(30) + :SetJustifyH("RIGHT") + :SetNonSpaceWrap(true) + + buttonText:Hide() + + local down = PTGuiLib.Get("button", frame) + :SetPoint("RIGHT", typeDropdown, "LEFT", -3, 0) + :SetSize(20, 20) + :ApplyTooltip("Move this binding down", "Hold shift to move to bottom") + :OnClick(function() + if obj.ControlHandler then + obj.ControlHandler:OnLineDown(obj) + end + end) + down.NormalTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up") + RotateTexture(down.NormalTexture, 90, 0.75) + down.PushedTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down") + RotateTexture(down.PushedTexture, 90, 0.75) + down.HighlightTexture:SetTexture("Interface\\Buttons\\UI-Common-MouseHilight") + down.HighlightTexture:SetTexCoord(0, 1, 0, 1) + down:SetUseDefaultTextures(true) + + local up = PTGuiLib.Get("button", frame) + :SetPoint("RIGHT", down, "LEFT", -1, 0) + :SetSize(20, 20) + :ApplyTooltip("Move this binding up", "Hold shift to move to top") + :OnClick(function() + if obj.ControlHandler then + obj.ControlHandler:OnLineUp(obj) + end + end) + up.NormalTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up") + RotateTexture(up.NormalTexture, -90, 0.75) + up.PushedTexture:SetTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down") + RotateTexture(up.PushedTexture, -90, 0.75) + up.HighlightTexture:SetTexture("Interface\\Buttons\\UI-Common-MouseHilight") + up.HighlightTexture:SetTexCoord(0, 1, 0, 1) + up:SetUseDefaultTextures(true) + + local delete = PTGuiLib.Get("button", frame) + :SetPoint("RIGHT", up, "LEFT", -6, 0) + :SetSize(20, 20) + :ApplyTooltip("Delete this binding") + :OnClick(function() + if obj.ControlHandler then + obj.ControlHandler:OnLineDelete(obj) + end + end) + delete.NormalTexture:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up") + RotateTexture(delete.NormalTexture, 180, 0.7) + delete.PushedTexture:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Down") + RotateTexture(delete.PushedTexture, 180, 0.7) + delete:SetUseDefaultTextures(true) + + local options = PTGuiLib.Get("button", frame) + :SetPoint("RIGHT", frame, "RIGHT") + :SetSize(20, 20) + :ApplyTooltip("More options for this binding") + :OnClick(function() + local TabFrame = PTSettingsGui.TabFrame + local editor = PTGuiLib.Get("puppeteer_binding_options", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + editor:SetBinding(obj.Binding) + PTSettingsGui.AddOverlayFrame(editor) + editor:SetDisposeHandler(function() + PTSettingsGui.PopOverlayFrame() + PTSettingsGui.UpdateUnsavedChanges() + obj:Update() + end) + end) + options:SetClickSound(PlaySound, "igMainMenuOption") + options.NormalTexture:SetTexture("Interface\\ICONS\\INV_Misc_Gear_01") + options.NormalTexture:SetTexCoord(0, 1, 0, 1) + options.PushedTexture:SetTexture("Interface\\ICONS\\INV_Misc_Gear_01") + options.PushedTexture:SetTexCoord(0.1, 0.9, 0.1, 0.9) + options.HighlightTexture:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight") + RotateTexture(options.HighlightTexture, 0, 0.5) + options:SetUseDefaultTextures(true) + + -- Depending on the selected type, one of these three components is displayed + local editbox = PTGuiLib.Get("editbox", frame) + :SetJustifyH("CENTER") + :SetScript("OnEnterPressed", function(self) + self:ClearFocus() + end) + local dropdown = PTGuiLib.Get("dropdown", frame) + :SetShowShadow(false) + local t = getglobal(dropdown:GetName().."Text") + t:ClearAllPoints() + t:SetPoint("CENTER", dropdown:GetHandle(), "CENTER") + t:SetJustifyH("CENTER") + t:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE") + local button = PTGuiLib.Get("button", frame) + for _, f in ipairs({editbox, dropdown, button}) do + f:SetPoint("LEFT", typeDropdown, "RIGHT", 5, 0) + f:SetPoint("RIGHT", options, "LEFT", -5, 0) + f:SetHeight(20) + f:Hide() + end + + obj:AddComponent("Label", buttonText) + obj:AddComponent("UpButton", up) + obj:AddComponent("DownButton", down) + obj:AddComponent("DeleteButton", delete) + obj:AddComponent("TypeDropdown", typeDropdown) + obj:AddComponent("OptionsButton", options) + obj:AddComponent("ContentEditbox", editbox) + obj:AddComponent("ContentDropdown", dropdown) + obj:AddComponent("ContentButton", button) + return obj +end + +function PTSpellLine:OnAcquire() + self.super.OnAcquire(self) + + self:SetMode(PTSpellLine.MODE_LABELED) +end + +function PTSpellLine:OnDispose() + self.super.OnDispose(self) + + self.ControlHandler = nil +end + +PTSpellLine:CreateGetter("Label") +PTSpellLine:CreateGetter("UpButton") +PTSpellLine:CreateGetter("DownButton") +PTSpellLine:CreateGetter("DeleteButton") +PTSpellLine:CreateGetter("TypeDropdown") +PTSpellLine:CreateGetter("OptionsButton") +PTSpellLine:CreateGetter("ContentEditbox") +PTSpellLine:CreateGetter("ContentDropdown") +PTSpellLine:CreateGetter("ContentButton") + +function PTSpellLine:SetLabel(text) + self:GetLabel():SetText(text):ApplyTooltip(text) + return self +end + +PTSpellLine.MODE_LABELED = 1 +PTSpellLine.MODE_ORDERED = 2 +function PTSpellLine:SetMode(mode) + if mode == PTSpellLine.MODE_LABELED then + self:GetLabel():Show() + self:GetUpButton():Hide() + self:GetDownButton():Hide() + self:GetDeleteButton():Hide() + elseif mode == PTSpellLine.MODE_ORDERED then + self:GetLabel():Hide() + self:GetUpButton():Show() + self:GetDownButton():Show() + self:GetDeleteButton():Show() + end + return self +end + +function PTSpellLine:SetBindType(bindType) + self:GetTypeDropdown():SetText(bindType) + util.ClearTable(self.Binding) + self.Binding.Type = bindType + PTSettingsGui.UpdateUnsavedChanges() + self:Update() + return self +end + +function PTSpellLine:SetBinding(binding) + self.Binding = binding + Puppeteer.ExpandBinding(binding) + self:Update() + return self +end + +-- The handler is expected to have functions OnLineDelete, OnLineUp, OnLineDown +function PTSpellLine:SetControlHandler(handler) + self.ControlHandler = handler + return self +end + +function PTSpellLine:SetEnabledContent(contentType) + self:GetContentEditbox():Hide() + self:GetContentDropdown():Hide() + self:GetContentButton():Hide() + if contentType == "editbox" then + self:GetContentEditbox():Show() + elseif contentType == "dropdown" then + self:GetContentDropdown():Show() + elseif contentType == "button" then + self:GetContentButton():Show() + end +end + +function PTSpellLine:ApplySearchableEditbox(bindType, searchFunc, searchAtLength) + local binding = self.Binding + self:GetContentEditbox():SetScript("OnTextChanged", function(editbox) + if not editbox.hasFocus then + return + end + binding.Data = self:GetContentEditbox():GetText() + PTSettingsGui.UpdateUnsavedChanges() + if not binding.Type then + binding.Type = bindType + end + if string.len(binding.Data) < (searchAtLength or 2) then + return + end + local searchResults = searchFunc(binding.Data) + if table.getn(searchResults) == 1 and searchResults[1] == binding.Data then + bindHelper:SetToggleState(false) + return + end + bindHelper:SetSimpleOptions(searchResults, function(option) + return { + text = option, + notCheckable = true, + func = function(option) + binding.Data = option.text + PTSettingsGui.UpdateUnsavedChanges() + self:Update() + end + } + end) + bindHelper:SetToggleState(false) + bindHelper:SetToggleState(true, self:GetContentEditbox():GetHandle(), 0, 0) + bindHelper:SetKeepOpen(true) + end) + self:GetContentEditbox():SetScript("OnEditFocusGained", function(self) + self:HighlightText() + self.hasFocus = true + end) + self:GetContentEditbox():SetScript("OnEditFocusLost", function(self) + bindHelper:SetToggleState(false) + self:HighlightText(0, 0) + self.hasFocus = nil + end) + self:GetContentEditbox():SetScript("OnTabPressed", function(editbox) + if string.len(binding.Data) < (searchAtLength or 2) then + return + end + local searchResults = searchFunc(binding.Data) + if table.getn(searchResults) > 0 then + editbox:SetText(searchResults[1]) + PTSettingsGui.UpdateUnsavedChanges() + end + end) +end + +function PTSpellLine:Update() + local binding = self.Binding + if binding.Type == "SPELL" or not binding.Type then + self:SetEnabledContent("editbox") + self:GetTypeDropdown():SetText("Spell") + self:GetContentEditbox():SetText(binding.Data or "") + self:GetContentEditbox():ApplyTooltip("Enter the name of a spell", "To downrank, suffix '("..RANK.." #)'", "For example, 'Heal("..RANK.." 2)'") + self:ApplySearchableEditbox("SPELL", SearchSpells, 2) + elseif binding.Type == "ACTION" then + self:SetEnabledContent("dropdown") + self:GetTypeDropdown():SetText("Action") + self:GetContentDropdown():ApplyTooltip("Choose from a list of special actions") + self:GetContentDropdown():SetText(binding.Data or "") + + self:GetContentDropdown():SetDynamicOptions(function(addOption, level, args) + for _, action in ipairs(Puppeteer.ActionBinds) do + addOption("text", action.Name, + "checked", binding.Data == action.Name, + "func", args.func, + "tooltipTitle", action.Name, + "tooltipText", action.Description) + end + end, { + func = function(self, gui) + binding.Data = self.text + gui:SetText(self.text) + PTSettingsGui.UpdateUnsavedChanges() + end + }) + elseif binding.Type == "MACRO" then + self:SetEnabledContent("editbox") + self:GetTypeDropdown():SetText("Macro") + self:GetContentEditbox():SetText(binding.Data or "") + self:GetContentEditbox():ApplyTooltip("Enter the name of one of your macros", + "Note that macro binds won't target unless you enable", "\"Target While Casting\" in this binding's settings or globally") + self:ApplySearchableEditbox("MACRO", SearchMacros, 1) + elseif binding.Type == "ITEM" then + self:SetEnabledContent("editbox") + self:GetTypeDropdown():SetText("Item") + self:GetContentEditbox():SetText(binding.Data or "") + self:GetContentEditbox():ApplyTooltip("Enter the name of an item in your bags") + self:ApplySearchableEditbox("ITEM", SearchItems, 1) + elseif binding.Type == "SCRIPT" then + self:SetEnabledContent("button") + self:GetTypeDropdown():SetText("Script") + local script = binding.Data or "" + local info + local _, lines = nil, 0 + if script ~= "" then + _, lines = string.gsub(script, "\n", "") + lines = lines + 1 + info = lines.." Line"..(lines ~= 1 and "s" or "") + else + info = "Empty" + end + self:GetContentButton():SetText("Edit Script ("..info..")") + local maxLines = 16 + if lines > maxLines then + local lineCount = 0 + local pos = 0 + local nl + while lineCount < maxLines do + nl = string.find(script, "\n", pos, true) + if not nl then + break + end + pos = nl + 1 + lineCount = lineCount + 1 + end + script = string.sub(script, 1, nl - 1).."\n"..colorize((lines - maxLines).." More...", 0.7, 0.7, 0.7) + end + if lines > 0 then + self:GetContentButton():ApplyTooltip("Script Preview", script) + else + self:GetContentButton():ApplyTooltip("Blank Script") + end + self:GetContentButton():OnClick(function() + local TabFrame = PTSettingsGui.TabFrame + local editor + editor = PTGuiLib.Get("puppeteer_binding_script_editor", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + :SetBinding(binding) + :SetCallback(function(save, scriptData, nameData) + if save then + binding.Data = scriptData + PTSettingsGui.UpdateUnsavedChanges() + end + editor:Dispose() + + self:Update() + end) + PTSettingsGui.AddOverlayFrame(editor) + editor:SetDisposeHandler(PTSettingsGui.PopOverlayFrame) + + editor:GetEditbox():SetFocus() + end, true) + elseif binding.Type == "MULTI" then + self:SetEnabledContent("button") + self:GetTypeDropdown():SetText("Multi") + local count = (binding.Data and binding.Data.Bindings and table.getn(binding.Data.Bindings)) or 0 + self:GetContentButton():SetText("Edit Bindings ("..count..")") + local tooltip = {} + for i = 1, count do + local subBinding = binding.Data.Bindings[i] + tooltip[i] = subBinding.Type + if subBinding.Type ~= "SCRIPT" and subBinding.Type ~= "MULTI" and subBinding.Data then + tooltip[i] = tooltip[i].." - "..subBinding.Data + end + end + self:GetContentButton():ApplyTooltip("Contents", tooltip) + self:GetContentButton():OnClick(function() + local TabFrame = PTSettingsGui.TabFrame + local editor = PTGuiLib.Get("puppeteer_multi_editor", TabFrame) + :SetPoint("CENTER", TabFrame, "CENTER") + editor:SetBinding(binding) + PTSettingsGui.AddOverlayFrame(editor) + editor:SetDisposeHandler(function() + PTSettingsGui.PopOverlayFrame() + PTSettingsGui.UpdateUnsavedChanges() + self:Update() + end) + end, true) + end +end + +PTGuiLib.RegisterComponent(PTSpellLine) \ No newline at end of file diff --git a/gui/component/uf/Arrow.lua b/gui/component/uf/Arrow.lua new file mode 100644 index 0000000..1ce69d9 --- /dev/null +++ b/gui/component/uf/Arrow.lua @@ -0,0 +1,51 @@ +PTArrow = PTGuiComponent:Extend("puppeteer_arrow") +local util = PTUtil +local interpolateColors = util.InterpolateColorsNoTable + +function PTArrow:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + local arrow = frame:CreateTexture("PTUnitFrameArrow", "MEDIUM") + arrow:SetTexture(util.GetAssetsPath().."textures\\arrow") + arrow:SetAllPoints() + obj.arrow = arrow + return obj +end + +function PTArrow:OnAcquire() + self.super.OnAcquire(self) + self.colors = nil + self.direction = nil +end + +function PTArrow:SetColor(r, g, b) + self.arrow:SetVertexColor(r, g, b) +end + +function PTArrow:SetGradientColors(colors) + self.colors = colors +end + +function PTArrow:GetDirection() + return self.direction +end + +function PTArrow:SetDirection(dir) + self.direction = dir + local arrow = self.arrow + local cell = util.modulo(math.floor(dir / (math.pi*2) * 108 + 0.5), 108) + local column = util.modulo(cell, 9) + local row = math.floor(cell / 9) + local xstart = (column * 56) / 512 + local ystart = (row * 42) / 512 + local xend = ((column + 1) * 56) / 512 + local yend = ((row + 1) * 42) / 512 + arrow:SetTexCoord(xstart,xend,ystart,yend) + if self.colors then + local perc = math.abs(((math.pi - math.abs(dir)) / math.pi)) + self:SetColor(interpolateColors(self.colors, perc)) + end +end + +PTGuiLib.RegisterComponent(PTArrow) \ No newline at end of file diff --git a/gui/component/uf/CastIcon.lua b/gui/component/uf/CastIcon.lua new file mode 100644 index 0000000..0e323af --- /dev/null +++ b/gui/component/uf/CastIcon.lua @@ -0,0 +1,242 @@ +-- Proof-of-concept spaghetti code for showing incoming spell casts + +PTCastIcon = PTGuiComponent:Extend("puppeteer_cast_icon") +local compost = PTPool +local activeIcons = {} + +function PTCastIcon:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + local icon = frame:CreateTexture(nil, "BACKGROUND") + icon:SetAllPoints(frame) + local border = CreateFrame("Frame", nil, frame) + border:SetBackdrop({edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1}) + border:SetPoint("TOPLEFT", icon, "TOPLEFT", -0.5, 0.5) + border:SetPoint("BOTTOMRIGHT", icon, "BOTTOMRIGHT", 0.5, -0.5) + local progressOverlay = frame:CreateTexture(nil, "ARTWORK") + progressOverlay:SetTexture(0, 0, 0, 0.5) + local progressEdge = frame:CreateTexture(nil, "ARTWORK") + progressEdge:SetTexture(1, 1, 0.4, 0.75) + progressEdge:SetPoint("BOTTOMLEFT", progressOverlay, 0, -0.5) + progressEdge:SetPoint("BOTTOMRIGHT", progressOverlay, 0, -0.5) + progressEdge:SetHeight(1) + local castOrder = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + castOrder:SetTextColor(1, 1, 1) + castOrder:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE") + castOrder:SetShadowOffset(0, 0) + castOrder:ClearAllPoints() + castOrder:SetPoint("CENTER", frame) + local overhealIndicator = border:CreateTexture(nil, "OVERLAY") + overhealIndicator:SetPoint("TOPLEFT", frame, "TOPLEFT") + overhealIndicator:SetWidth(4) + overhealIndicator:SetHeight(4) + obj.icon = icon + obj.border = border + obj.castOrder = castOrder + obj.overhealIndicator = overhealIndicator + obj.progressOverlay = progressOverlay + obj.progressEdge = progressEdge + return obj +end + +function PTCastIcon:OnAcquire() + self.super.OnAcquire(self) +end + +function PTCastIcon:OnDispose() + self.super.OnDispose(self) + self.icon:SetVertexColor(1, 1, 1) + self.progressOverlay:Show() + self.progressEdge:Show() + self.castOrder:Show() + self.overhealIndicator:Show() + self:GetHandle():SetAlpha(1) + self:SetScript("OnUpdate", nil) + local index = PTUtil.KeyOf(self.castIcons, self) + if index ~= nil then + self.castIcons[index] = nil + end +end + +function PTCastIcon:GetPosition(index) + local isEven = index / 2 == math.floor(index / 2) + return (math.floor(index / 2) * 15 * (isEven and 1 or -1)), 0 +end + +function PTCastIcon:GetProgress() + return math.min((GetTime() - self.startTime) / self.time, 1) +end + +function PTCastIcon:GetOvertime() + return GetTime() - self.endTime +end + +local importantCasts = { + "Soulstone Resurrection", + "Proclaim Champion", + "Revive Champion", + "Shackle Undead", "Mind Control", "Fear", "Polymorph", "Polymorph: Turtle", "Polymorph: Cow", "Polymorph: Rodent" +} +importantCasts = PTUtil.ToSet(importantCasts) +PTLocale.Keys(importantCasts) + +local T = PTLocale.Translate +function PTCastIcon:Start(spellName, spellTexture, time, unit, healAmount, unitFrame) + local class = PTUtil.GetClass(unit) + self:SetScript("OnUpdate", function() + Puppeteer.StartTiming("CastIcon") + self:Update() + Puppeteer.EndTiming("CastIcon") + end, false) + self.icon:SetTexture(spellTexture) + --Puppeteer.print(spellTexture) + self.border:SetBackdropBorderColor(PTUtil.GetClassColor(class)) + self.startTime = GetTime() + self.endTime = self.startTime + time + self.time = time + self.spellName = spellName + self.class = class + self.unit = unit + self.healAmount = healAmount + self.unitFrame = unitFrame + self.state = "CASTING" + activeIcons[unit] = self + local isCasterSelf = PTUnitProxy.UnitIsUnit("player", unit) + local isSelfHealer = Puppeteer.GetUnitAssignedRole("player") == "Healer" + local shouldBeCenter = isCasterSelf and isSelfHealer + local canBeCenter = isCasterSelf or not isSelfHealer + self:SetParent(unitFrame.overlayContainer) + local group = unitFrame.owningGroup + local totalHealth = 0 + local totalUnits = 0 + for _, unit in ipairs(group.units) do + if PTUnitProxy.UnitExists(unit) and PTUnitProxy.UnitIsConnected(unit) then + totalHealth = totalHealth + PTUnitProxy.UnitHealthMax(unit) + totalUnits = totalUnits + 1 + end + end + local avgHealth = totalHealth / totalUnits + local healPower = healAmount / avgHealth + Puppeteer.print("Heal Power: "..healPower) + local startSize = 9 + local maxSize = 15 + local healPowerSizeMult = 9.2 + local size = math.max(math.min(startSize + (healPower * healPowerSizeMult), maxSize), startSize) + if PTUtil.ResurrectionSpellsSet[spellName] or importantCasts[spellName] then + size = 14 + self.overhealIndicator:Hide() + end + Puppeteer.print("Size: "..size) + self:SetSize(size, size) + local order = self.castOrder + if isCasterSelf then + order:SetFont("Fonts\\FRIZQT__.TTF", size, "OUTLINE") + order:SetTextColor(1, 1, 0) + else + order:SetFont("Fonts\\FRIZQT__.TTF", size - 1, "OUTLINE") + order:SetTextColor(0.9, 0.9, 0.9) + end + if not unitFrame.castIcons then + unitFrame.castIcons = {} + end + self.castIcons = unitFrame.castIcons + -- Remove our own icon in case it's fading still + if shouldBeCenter and unitFrame.castIcons[1] and PTUnitProxy.UnitIsUnit("player", unitFrame.castIcons[1].unit) then + unitFrame.castIcons[1]:Dispose() + end + for i = (canBeCenter and 1 or 2), 40 do + if not unitFrame.castIcons[i] then + unitFrame.castIcons[i] = self + self.point = {"CENTER", unitFrame.overlayContainer, "CENTER", self:GetPosition(i)} + self:SetPoint(unpack(self.point)) + break + end + end +end + +local endColors = { + [true] = {0.6, 1, 0.6}, + [false] = {1, 0.6, 0.6} +} +function PTCastIcon:End(successful) + self.state = "FADING" + self.startTime = GetTime() + self.time = successful and 0.2 or 0.4 + self.endTime = self.startTime + self.endTime + self.icon:SetVertexColor(unpack(endColors[successful])) + self.progressOverlay:Hide() + if successful then + self.progressEdge:Hide() + end + self.overhealIndicator:Hide() + self.successful = successful + self.castOrder:Hide() + activeIcons[self.unit] = nil +end + +local iconSorter = function(a, b) + return (a.endTime + (a.state == "CASTING" and 0 or 1000)) < (b.endTime + (b.state == "CASTING" and 0 or 1000)) +end +local effectiveHealColors = { + {1, 0, 0}, + {1, 1, 0}, + {0, 1, 0} +} +function PTCastIcon:Update() + if self.state == "CASTING" then + local overlay = self.progressOverlay + overlay:ClearAllPoints() + overlay:SetPoint("TOPLEFT", self:GetHandle(), "TOPLEFT") + overlay:SetPoint("BOTTOMRIGHT", self:GetHandle(), "BOTTOMRIGHT", 0, self:GetHandle():GetHeight() * self:GetProgress()) + + local castOrder = self.castOrder + local icons = self.castIcons + local rankedCasts = compost:GetTable() + for _, icon in pairs(icons) do + table.insert(rankedCasts, icon) + end + table.sort(rankedCasts, iconSorter) + local castRank = PTUtil.IndexOf(rankedCasts, self) + local missingHealth = self.unitFrame:GetMaxHealth() - self.unitFrame:GetCurrentHealth() + for i, icon in ipairs(rankedCasts) do + local healAmount = icon.healAmount + if i == castRank then + local effectiveHealProportion = missingHealth / healAmount + local r, g, b = PTUtil.InterpolateColorsNoTable(effectiveHealColors, effectiveHealProportion) + self.overhealIndicator:SetTexture(r, g, b) + break + end + missingHealth = missingHealth - healAmount + end + compost:Reclaim(rankedCasts) + castOrder:SetText(tostring(castRank)) + elseif self.state == "FADING" then + local progress = self:GetProgress() + if progress >= 1 then + self:Dispose() + return + end + self:GetHandle():SetAlpha(1 - self:GetProgress()) + if not self.successful then + self:ClearAllPoints() + local p = self.point + self:SetPoint(p[1], p[2], p[3], p[4] + (-math.sin(4 * math.pi * progress) * 1.5), p[5]) + end + end +end + +PTGuiLib.RegisterComponent(PTCastIcon) + +local f = CreateFrame("Frame", "PTPushbackTracker") +f:RegisterEvent("SPELL_DELAYED_SELF") +f:RegisterEvent("SPELL_DELAYED_OTHER") +f:SetScript("OnEvent", function() + local caster, pushback = arg1, (arg2 / 1000) + if activeIcons[caster] then + Puppeteer.print("Pushback: "..pushback) + local icon = activeIcons[caster] + icon.startTime = icon.startTime + pushback + icon.endTime = icon.endTime + pushback + end +end) \ No newline at end of file diff --git a/gui/component/uf/HighlightBorder.lua b/gui/component/uf/HighlightBorder.lua new file mode 100644 index 0000000..bfae969 --- /dev/null +++ b/gui/component/uf/HighlightBorder.lua @@ -0,0 +1,128 @@ +PTHighlightBorder = PTGuiComponent:Extend("puppeteer_highlight_border") +local compost = PTPool +local util = PTUtil +local dashes = {} + +function PTHighlightBorder:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj.BorderWidth = 4 + obj.Dashes = {} + return obj +end + +function PTHighlightBorder:OnAcquire() + self.super.OnAcquire(self) + self.State = 0 + self.Speed = 1.5 + self.Interval = 20 + self.Length = 10 + self.Thickness = 2 +end + +function PTHighlightBorder:OnDispose() + self.super.OnDispose(self) + self:DisposeDashes() + self:SetScript("OnUpdate", nil) +end + +function PTHighlightBorder:AcquireDash() + local dash + if table.getn(dashes) > 0 then + dash = table.remove(dashes, table.getn(dashes)) + else + dash = CreateFrame("Frame") + local tex = dash:CreateTexture(nil, "ARTWORK") + tex:SetAllPoints() + tex:SetTexture(1, 1, 0) + end + dash:SetParent(self:GetHandle()) + dash:Show() + table.insert(self.Dashes, dash) + return dash +end + +function PTHighlightBorder:DisposeDashes() + for _, dash in ipairs(self.Dashes) do + dash:ClearAllPoints() + dash:Hide() + table.insert(dashes, dash) + end + util.ClearTable(self.Dashes) +end + +local lines = { + {anchor = "BOTTOMLEFT", heightwise = true, reversed = true}, + {anchor = "TOPLEFT", heightwise = false, reversed = false}, + {anchor = "TOPRIGHT", heightwise = true, reversed = false}, + {anchor = "BOTTOMRIGHT", heightwise = false, reversed = true} +} + +function PTHighlightBorder:Draw() + self:DisposeDashes() + + self.State = self.State + self.Speed + if self.State >= self.Interval then + self.State = self.State - self.Interval + end + + local width = self:GetWidth() + local height = self:GetHeight() + + local state = self.State + local length = self.Length + local interval = self.Interval + local thickness = self.Thickness + + for _, line in ipairs(lines) do + local anchor = line.anchor + local heightwise = line.heightwise + local reversed = line.reversed + local point = 0 + local sideLength = heightwise and height or width + while point < sideLength do + if point == 0 and state ~= 0 then + point = point + state + if point > (interval - length) then + local dash = self:AcquireDash() + local dashLength = length - (interval - state) + dash:SetWidth(heightwise and thickness or dashLength) + dash:SetHeight(heightwise and dashLength or thickness) + dash:SetPoint(anchor, self:GetHandle(), anchor, 0, 0) + end + else + local dash = self:AcquireDash() + local dashLength = length + if point + length > sideLength then + dashLength = sideLength - point + end + dash:SetWidth(heightwise and thickness or dashLength) + dash:SetHeight(heightwise and dashLength or thickness) + dash:SetPoint(anchor, self:GetHandle(), anchor, + (heightwise and 0 or point) * (reversed and -1 or 1), + (heightwise and point or 0) * (reversed and 1 or -1)) + point = point + interval + end + end + end +end + +function PTHighlightBorder:SetBorderParent(borderParent) + self.BorderParent = borderParent + self:SetParent(borderParent) + self:GetHandle():SetAllPoints(borderParent) + local nextUpdate = 0 + self:SetScript("OnUpdate", function(self) + if nextUpdate < GetTime() then + nextUpdate = GetTime() + 0.033 + self:Draw() + end + end) +end + +function PTHighlightBorder:AttachToUnitFrame(unitFrame) + self:SetBorderParent(unitFrame.overlayContainer) +end + +PTGuiLib.RegisterComponent(PTHighlightBorder) \ No newline at end of file diff --git a/libs/AuraTracker.lua b/libs/AuraTracker.lua new file mode 100644 index 0000000..b859597 --- /dev/null +++ b/libs/AuraTracker.lua @@ -0,0 +1,263 @@ +-- Detects casts and records the aura times in PTUnit + +if not PTUtil.IsSuperWowPresent() then + return +end + +local client1181 = PTUtil.IsClient1181() +-- Value is duration +local trackedCastedAuras = { + -- Druid + ["Rejuvenation"] = 12, + ["Regrowth"] = client1181 and 20 or 21, + ["Mark of the Wild"] = 30 * 60, + ["Gift of the Wild"] = 60 * 60, + ["Thorns"] = 10 * 60, + ["Abolish Poison"] = 8, + ["Innervate"] = 20, + ["Barkskin"] = 15, + ["Barkskin (Feral)"] = 12, + -- Druid: Offensive + ["Moonfire"] = 18, + ["Insect Swarm"] = 18, + ["Faerie Fire"] = 40, + ["Faerie Fire (Feral)"] = 40, + -- Priest + ["Power Word: Fortitude"] = 30 * 60, + ["Divine Spirit"] = 30 * 60, + ["Shadow Protection"] = 10 * 60, + ["Prayer of Fortitude"] = 60 * 60, + ["Prayer of Spirit"] = 60 * 60, + ["Prayer of Shadow Protection"] = 20 * 60, + ["Renew"] = 15, + ["Power Word: Shield"] = 30, + ["Weakened Soul"] = 15, + ["Fade"] = 10, + ["Fear Ward"] = 10 * 60, + ["Champion's Grace"] = 30 * 60, + ["Empower Champion"] = 10 * 60, + ["Champion's Bond"] = 10 * 60, + ["Spirit of Redemption"] = 10, + ["Abolish Disease"] = 20, + ["Inner Fire"] = 10 * 60, + -- Priest: Offensive + ["Shadow Word: Pain"] = 18, + ["Holy Fire"] = 10, + ["Mind Control"] = 60, + -- Paladin + ["Blessing of Protection"] = 10, + ["Hand of Protection"] = 10, + ["Divine Protection"] = 8, + ["Divine Shield"] = 12, + ["Holy Shield"] = 10, + ["Bulwark of the Righteous"] = 12, + ["Forbearance"] = 60, + ["Blessing of Sacrifice"] = 30, + ["Hand of Sacrifice"] = 30, + ["Divine Intervention"] = 3 * 60, + ["Blessing of Wisdom"] = (client1181 and 10 or 5) * 60, + ["Blessing of Might"] = (client1181 and 10 or 5) * 60, + ["Blessing of Salvation"] = (client1181 and 10 or 5) * 60, + ["Blessing of Sanctuary"] = (client1181 and 10 or 5) * 60, + ["Blessing of Kings"] = (client1181 and 10 or 5) * 60, + ["Blessing of Light"] = (client1181 and 10 or 5) * 60, + ["Greater Blessing of Wisdom"] = (client1181 and 30 or 15) * 60, + ["Greater Blessing of Might"] = (client1181 and 30 or 15) * 60, + ["Greater Blessing of Salvation"] = (client1181 and 30 or 15) * 60, + ["Greater Blessing of Sanctuary"] = (client1181 and 30 or 15) * 60, + ["Greater Blessing of Kings"] = (client1181 and 30 or 15) * 60, + ["Greater Blessing of Light"] = (client1181 and 30 or 15) * 60, + -- Shaman + ["Water Walking"] = 10 * 60, + -- Mage + ["Arcane Intellect"] = 30 * 60, + ["Arcane Brilliance"] = 60 * 60, + ["Frost Armor"] = 30 * 60, + ["Ice Armor"] = 30 * 60, + ["Mage Armor"] = 30 * 60, + ["Ice Barrier"] = 60, + ["Mana Shield"] = 60, + ["Fire Ward"] = 30, + ["Frost Ward"] = 30, + ["Ice Block"] = 10, + ["Arcane Power"] = 20, + -- Mage: Offensive + ["Polymorph"] = 50, + -- Warlock + ["Soulstone Resurrection"] = 30 * 60, + ["Unending Breath"] = 10 * 60, + ["Demon Skin"] = 30 * 60, + ["Demon Armor"] = 30 * 60, + ["Fire Shield"] = 3 * 60, + -- Warlock: Offsensive + ["Corruption"] = 18, + ["Immolate"] = 15, + ["Curse of Agony"] = 24, + ["Curse of Tongues"] = 30, + ["Curse of Recklessness"] = 2 * 60, + -- Rogue + ["Evasion"] = 15, + -- Hunter + ["Feign Death"] = 6 * 60, + ["Deterrence"] = 10, + ["Rapid Fire"] = 15, + -- Hunter: Offensive + ["Hunter's Mark"] = 2 * 60, + ["Serpent Sting"] = 15, + ["Scorpid Sting"] = 20, + ["Viper Sting"] = 8, + -- Warrior + ["Battle Shout"] = 2 * 60, + ["Shield Wall"] = 10, -- 12 with talent + ["Last Stand"] = 20, + ["Death Wish"] = 30, + -- Warrior: Offensive + ["Rend"] = 21, + ["Mortal Strike"] = 10, + -- Racial + ["Quel'dorei Meditation"] = 5, + ["Grace of the Sunwell"] = 15, + ["Blood Fury"] = 25, + -- Generic + ["First Aid"] = 8, + ["Recently Bandaged"] = 60, + ["Rapid Healing"] = 15, + -- Emerald Sanctum + ["Call of Nightmare"] = 10, + ["Dreamstate"] = 10, + -- Naxxramas + ["Veil of Darkness"] = 7, + -- Misc + ["Curse of the Deadwood"] = 2 * 60 +} +PTLocale.Keys(trackedCastedAuras) +PuppeteerSettings.TrackedCastedAuras = trackedCastedAuras + +-- Auras to start the timer for even though they weren't directly casted +local additionalAuras = { + ["Divine Protection"] = {{"Forbearance", 25771}}, + ["Divine Shield"] = {{"Forbearance", 25771}}, + ["Blessing of Protection"] = {{"Forbearance", 25771}}, + ["Hand of Protection"] = {{"Forbearance", 25771}}, + ["First Aid"] = {{"Recently Bandaged", 11196}}, + ["Power Word: Shield"] = {{"Weakened Soul", 6788}} +} +PTLocale.Keys(additionalAuras) +for _, array in pairs(additionalAuras) do + for _, entry in ipairs(array) do + entry[1] = PTLocale.T(entry[1]) + end +end + +-- Value is range +local aoeAuras = { + ["Prayer of Fortitude"] = 100, + ["Prayer of Spirit"] = 100, + ["Prayer of Shadow Protection"] = 100, + ["Arcane Brilliance"] = 100, + ["Gift of the Wild"] = 100, + ["Battle Shout"] = 20 +} +PTLocale.Keys(aoeAuras) + +-- Paladins always get their own special stuff.. +-- Their buffs are aoe but apply to the whole raid for a specific class +local aoeClassAuras = PTUtil.ToSet({ + "Greater Blessing of Wisdom", "Greater Blessing of Might", "Greater Blessing of Salvation", + "Greater Blessing of Sanctuary", "Greater Blessing of Kings", "Greater Blessing of Light" +}) +PTLocale.Keys(aoeClassAuras) + +local function applyTimedAura(spellName, id, owner, units, duration) + for _, unit in ipairs(units) do + if PTUnit.Get(unit) then + PTUnit.Get(unit):ApplyTimedAura(spellName, id, owner, duration or trackedCastedAuras[spellName], duration ~= nil) + end + end +end + +local castEventFrame = CreateFrame("Frame", "PTAuraTrackingCasts") +castEventFrame:RegisterEvent("UNIT_CASTEVENT") +castEventFrame:SetScript("OnEvent", function() + local caster, target, event, spellID, duration = arg1, arg2, arg3, arg4, arg5 + + if event == "CAST" or event == "CHANNEL" then + local spellName = SpellInfo(spellID) + if trackedCastedAuras[spellName] then + if target == "" then + target = caster + end + local units = PTGuidRoster.GetUnits(target) + if not units then + return + end + if aoeAuras[spellName] then + local targets = PTUtil.GetSurroundingPartyMembers(target, aoeAuras[spellName]) + for _, unit in ipairs(targets) do + local units = PTGuidRoster.GetAllUnits(unit) + if units then + applyTimedAura(spellName, spellID, caster, units) + end + end + PTPool.Release(targets) + elseif aoeClassAuras[spellName] then + local class = PTUtil.GetClass(target) + local targets = PTUtil.GetSurroundingRaidMembers(target, 100) + for _, unit in ipairs(targets) do + if PTUtil.GetClass(unit) == class then + local units = PTGuidRoster.GetAllUnits(unit) + if units then + applyTimedAura(spellName, spellID, caster, units) + end + end + end + PTPool.Release(targets) + else + applyTimedAura(spellName, spellID, caster, units) + end + + if additionalAuras[spellName] then + for _, aura in ipairs(additionalAuras[spellName]) do + applyTimedAura(aura[1], aura[2], caster, units) + end + end + end + end +end) + +if not PTUtil.HasModVersion("Nampower", PTUtil.Nampower_v3_0) then + return +end + +local singleTarget = {} -- Reused instead of creating a new single-element table for every aura event +local nampowerAuraFrame = CreateFrame("Frame", "PTNampowerAuras") +nampowerAuraFrame:RegisterEvent("AURA_CAST_ON_SELF") +nampowerAuraFrame:RegisterEvent("AURA_CAST_ON_OTHER") +nampowerAuraFrame:SetScript("OnEvent", function() + local spellID, caster, target, _, _, _, _, duration = arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 + if not caster or not target then + return + end + if duration <= 0 then + return + end + singleTarget[1] = target + applyTimedAura(GetSpellRecField(spellID, "name"), spellID, caster, singleTarget, duration / 1000) + singleTarget[1] = nil +end) + +local inspirationTracker = CreateFrame("Frame", "PTInspirationTracker") +inspirationTracker:RegisterEvent("SPELL_HEAL_BY_SELF") +inspirationTracker:RegisterEvent("SPELL_HEAL_BY_OTHER") +local inspirationProcSpells = PTUtil.ToSet({"Heal", "Greater Heal", "Flash Heal", "Prayer of Healing"}) +inspirationTracker:SetScript("OnEvent", function() + local target, caster, spellID, amount, crit, periodic = arg1, arg2, arg3, arg4, arg5, arg6 + + local spellName = GetSpellRecField(spellID, "name") + + if crit == 1 and inspirationProcSpells[spellName] then + if PTUnit.Get(target) then + PTUnit.Get(target):ApplyTimedAura(spellName, 15359, caster, 15, true) + end + end +end) \ No newline at end of file diff --git a/libs/GuidRoster.lua b/libs/GuidRoster.lua new file mode 100644 index 0000000..cead536 --- /dev/null +++ b/libs/GuidRoster.lua @@ -0,0 +1,100 @@ +-- A utility to map guids to units + +if not PTUtil.IsSuperWowPresent() then + return +end + +PTGuidRoster = {} +PTUtil.SetEnvironment(PTGuidRoster) +local _G = getfenv(0) + +local compost = PTPool + +local util = PTUtil + +GuidUnitMap = {} +GuidFrameMap = {} + +function ResetRoster() + local roster = GuidUnitMap + for k, v in pairs(roster) do + compost:Reclaim(v) + roster[k] = nil + end +end + +function PopulateRoster() + for _, unit in ipairs(util.AllRealUnits) do + local exists, guid = UnitExists(unit) + if exists then + AddUnit(guid, unit) + end + end + for guid, units in pairs(PTUnitProxy.GUIDCustomUnitMap) do + for _, unit in ipairs(units) do + AddUnit(guid, unit) + end + end +end + +function AddUnit(guid, unit) + if not GuidUnitMap[guid] then + GuidUnitMap[guid] = compost:GetTable() + end + table.insert(GuidUnitMap[guid], unit) +end + +function SetUnitGuid(unit, guid) + for guidInMap, units in pairs(GuidUnitMap) do + if util.ArrayContains(units, unit) then + util.RemoveElement(units, unit) + if table.getn(units) == 0 then + compost:Reclaim(units) + GuidUnitMap[guidInMap] = nil + end + break + end + end + + if not guid then + return + end + + if not GuidUnitMap[guid] then + GuidUnitMap[guid] = compost:GetTable() + end + table.insert(GuidUnitMap[guid], unit) +end + +function GetUnitGuid(unit) + local _, guid = UnitExists(unit) + return guid +end + +-- Resolves the GUID of the real unit, custom unit, or returns the unit itself if it's already a GUID. +-- If the unit is "target" and there's no target, this returns nil. +function ResolveUnitGuid(unit) + local guid = GetUnitGuid(unit) or PTUnitProxy.CustomUnitGUIDMap[unit] or unit + if guid ~= "target" then + return guid + end +end + +-- Returns an array of all the units the guid is, or nil if none +function GetUnits(guid) + return GuidUnitMap[guid] +end + +function HasUnits(guid) + return GuidUnitMap[guid] ~= nil +end + +-- Returns an array of units this unit is +function GetAllUnits(unit) + return GetUnits(GetUnitGuid(unit)) +end + +-- Returns an array of all guids that have units +function GetTrackedGuids() + return util.ToArray(GuidUnitMap) +end \ No newline at end of file diff --git a/libs/HealPredict.lua b/libs/HealPredict.lua new file mode 100644 index 0000000..645511a --- /dev/null +++ b/libs/HealPredict.lua @@ -0,0 +1,852 @@ +-- Predicts healing based on previously seen healing. +-- This library currently is not independent and relies on some Puppeteer code. + +if not PTUtil.IsSuperWowPresent() then + return +end + +PTHealPredict = {} +PTUtil.SetEnvironment(PTHealPredict) +local _G = getfenv(0) + +local compost = PTPool + +RelevantGUIDs = {} -- A set of GUIDs to listen to + +IncomingHeals = {} -- Key: Receiver | Value: List of incoming casts +IncomingHots = {} -- Key: Receiver | Value: {HoT Name: {"caster", "id", "heal"}} +Casts = {} -- Key: Caster | Value: {"targets", "spellID", "startTime"} +LastCastedSpells = {} + +-- A cache of expected heal values for spells. These values are saved. +-- Key: Spell ID | Value: Typical heal value for the spell ID +HealCache = {} + +-- Since every healer heals for different values based on talents and gear, +-- everyone's heal values are cached +-- Key: Name | Value: Array: {Spell ID: Heal value} +PlayerHealCache = {} + +ResurrectionTargets = {} -- Key: Receiver | Value: {Caster: {"startTime", "castTime"}} + +-- An array of functions that listen to changes to incoming healing +Listeners = {} + +local print +local colorize = PTUtil.Colorize + +-- Debug messages are only built when debug output is enabled, since building them creates garbage +local function debugEnabled() + return PTOptions and PTOptions["Debug"] +end + +local PRAYER_OF_HEALING_IDS = PTUtil.ToSet({596, 996, 10960, 10961, 25316}) +local ResurrectionSpells = PTUtil.ToSet({ + "Resurrection", "Revive Champion", "Redemption", "Ancestral Spirit", "Rebirth" +}) +PTLocale.Keys(ResurrectionSpells) + +local TRACKED_HOTS = PTUtil.ToSet({ + "Rejuvenation", "Regrowth", -- Druid + "Renew", "Greater Heal", -- Priest + "Mend Pet", -- Hunter + "First Aid" -- Generic +}) +PTLocale.Keys(TRACKED_HOTS) + +function OnLoad() + print = Puppeteer.print + if not PTHealCache then + setglobal("PTHealCache", {}) + end + HealCache = PTHealCache + + local hardcodedHots = { + -- Bandages + [746] = 11, -- Linen + [1159] = 19, -- Heavy Linen + [3267] = 23, -- Wool + [3268] = 43, -- Heavy Wool + [7926] = 50, -- Silk + [7927] = 80, -- Heavy Silk + [10838] = 100, -- Mageweave + [10839] = 138, -- Heavy Mageweave + [18608] = 170, -- Runecloth + [18610] = 250, -- Heavy Runecloth + -- Mend Pet + [136] = 20, + [3111] = 38, + [3661] = 68, + [3662] = 103, + [13542] = 142, + [13543] = 189, + [13544] = 245 + } + for k, v in pairs(hardcodedHots) do + HealCache[k.."-HoT"] = v + end + + if not PTPlayerHealCache then + setglobal("PTPlayerHealCache", {}) + end + if not PTPlayerHealCache[GetRealmName()] then + PTPlayerHealCache[GetRealmName()] = {} + end + PlayerHealCache = PTPlayerHealCache[GetRealmName()] +end + +-- Get the expected heal of a player's spell +function GetExpectedHeal(playerName, spellID) + local playerCache = PlayerHealCache[playerName] + if not playerCache or not playerCache[spellID] then + return GetGenericExpectedHeal(spellID) + end + return playerCache[spellID] +end + +function GetGenericExpectedHeal(spellID) + return HealCache[spellID] or 0 +end + +-- Returns all incoming healing and incoming direct healing +function GetIncomingHealing(guid) + local healing = 0 + local casts = IncomingHeals[guid] + if casts then + for _, cast in pairs(casts) do + healing = healing + cast["heal"] + end + end + local directHealing = healing + local hots = IncomingHots[guid] + if hots then + for _, hot in pairs(hots) do + healing = healing + hot["heal"] + end + end + return healing, directHealing +end + +-- Returns non-HoT incoming healing +function GetIncomingDirectHealing(guid) + local healing = 0 + local casts = IncomingHeals[guid] + if casts then + for _, cast in pairs(casts) do + healing = healing + cast["heal"] + end + end + return healing +end + +-- To mimick HealComm, but this currently only accepts GUIDs +function getHeal(guid) + return GetIncomingHealing(guid) +end + +function IsBeingResurrected(guid) + return ResurrectionTargets[guid] ~= nil +end + +function GetResurrectionCount(guid) + local resses = ResurrectionTargets[guid] + if not resses then + return 0 + end + local count = 0 + for _ in pairs(resses) do + count = count + 1 + end + return count +end + +-- Used for Prayer of Healing to add incoming healing to multiple players +function AddIncomingMultiCast(targets, caster, spellID, healAmount, castTime) + Casts[caster] = compost:AcquireHash( + "targets", targets, + "spellID", spellID, + "startTime", GetTime() + ) + for _, target in ipairs(targets) do + AddIncomingCast(target, caster, spellID, healAmount, castTime, true) + end +end + +local castIcons = {} +function RemoveAllCastIcons() + for caster, icons in pairs(castIcons) do + for _, icon in ipairs(castIcons[caster]) do + icon:End(false) + end + compost:Reclaim(icons) + castIcons[caster] = nil + end +end + +function AddIncomingCast(target, caster, spellID, healAmount, castTime, multi) + if not multi then + Casts[caster] = compost:AcquireHash( + "targets", compost:Acquire(target), + "spellID", spellID, + "startTime", GetTime() + ) + end + local targetTable = IncomingHeals[target] + if not targetTable then + targetTable = {} + IncomingHeals[target] = targetTable + end + if targetTable[caster] then + compost:Reclaim(targetTable[caster]) + end + targetTable[caster] = compost:AcquireHash( + "spellID", spellID, + "heal", healAmount, + "castTime", castTime, + "startTime", GetTime() + ) + + UpdateTarget(target) +end + +function RemoveIncomingCast(caster, successful) + local cast = Casts[caster] + if cast then + for _, target in ipairs(cast["targets"]) do + local incomingHeals = IncomingHeals[target] + compost:Reclaim(incomingHeals[caster]) + incomingHeals[caster] = nil + UpdateTarget(target) + end + compost:Reclaim(cast["targets"]) + compost:Reclaim(cast) + Casts[caster] = nil + end +end + +function GetCurrentCast(caster) + local cast = Casts[caster] + if cast then + return table.getn(cast["targets"]) > 0 and IncomingHeals[cast["targets"][1]][caster] or nil + end +end + +function AddHot(target, caster, spellID, spellName, healAmount) + local hot = compost:AcquireHash( + "caster", caster, + "id", spellID, + "heal", healAmount, + "startTime", GetTime() + ) + local targetTable = IncomingHots[target] + if not targetTable then + targetTable = {} + IncomingHots[target] = targetTable + end + if targetTable[spellName] then + compost:Reclaim(targetTable[spellName]) + end + targetTable[spellName] = hot + + UpdateTarget(target) +end + +function UpdateTarget(target) + for _, listener in ipairs(Listeners) do + listener(target, GetIncomingHealing(target)) + end +end + +local function trimDecimal(number, places) + local factor = 10 ^ places + return math.floor(number * factor) / factor +end + +local GENERIC_CHANGE_FACTOR = 0.05 +local PLAYER_CHANGE_FACTOR = 0.25 +function UpdateCache(heal, name) + name = name or UnitName("player") + local lastCastedSpell = LastCastedSpells[name] + + if not lastCastedSpell then + return + end + LastCastedSpells[name] = nil + + local spellID = lastCastedSpell["spellID"] + local castTarget = lastCastedSpell["target"] + compost:Reclaim(lastCastedSpell) + + if not PRAYER_OF_HEALING_IDS[spellID] then + if castTarget == "" then + --print(colorize("Don't have a target of spell cast for "..name.."'s "..spellID, 1, 0, 0)) + return + end + local cache = PTUnit.Get(castTarget) + if not cache or cache == PTUnit then + --print(colorize("Could not find "..name.."'s unit while updating cache!", 1, 0, 0)) + return + end + if cache.HasHealingModifier then + --print(colorize("Not updating cache for "..name.."'s "..spellID.." because of healing modifier", 0.5, 0.5, 0.5)) + return + end + end + + -- Update the generic cache + if not HealCache[spellID] then + HealCache[spellID] = heal + else + local prevHeal = HealCache[spellID] + local adjustedHeal = trimDecimal(prevHeal + ((heal - prevHeal) * GENERIC_CHANGE_FACTOR), 2) + HealCache[spellID] = adjustedHeal + if debugEnabled() then + print(colorize("Generic "..spellID..": "..prevHeal.." -> "..adjustedHeal, 0, 0.8, 0.8)) + end + end + + if not PlayerHealCache[name] then + PlayerHealCache[name] = {} + end + -- Update the player-specific cache + local playerCache = PlayerHealCache[name] + if not playerCache[spellID] then + playerCache[spellID] = heal + if debugEnabled() then + print(colorize("Created cache for "..name.."'s "..spellID, 1, 0.5, 1)) + end + end + local prevHeal = playerCache[spellID] + local adjustedHeal = trimDecimal(prevHeal + ((heal - prevHeal) * PLAYER_CHANGE_FACTOR), 2) + playerCache[spellID] = adjustedHeal + playerCache["lastSeen"] = time() + + if debugEnabled() then + print(colorize(name.."'s "..spellID..": "..prevHeal.." -> "..adjustedHeal, 0, 0.8, 0.2)) + end +end + +function UpdateCacheHot(spellName, heal, targetGuid, targetName, casterGuid, casterName) + if not IncomingHots[targetGuid] then + return + end + local hots = IncomingHots[targetGuid] + if not hots[spellName] then + return + end + heal = tonumber(heal) or 0 + local hot = hots[spellName] + if hot["heal"] ~= heal then + local prevHeal = hot["heal"] + hot["heal"] = heal + UpdateTarget(targetGuid) + if not PlayerHealCache[casterName] then + PlayerHealCache[casterName] = {} + end + local spellID = hot["id"] + + local cache = PTUnit.Get(targetGuid) + if not cache or cache == PTUnit then + if debugEnabled() then + print(colorize("Could not find "..targetName.."'s unit while updating cache!", 1, 0, 0)) + end + return + end + if cache.HasHealingModifier then + if debugEnabled() then + print(colorize("Not updating cache for "..casterName.."'s "..spellID.. + " because of healing modifier", 0.5, 0.5, 0.5)) + end + return + end + -- Update the player-specific cache + local playerCache = PlayerHealCache[casterName] + spellID = spellID.."-HoT" + if not playerCache[spellID] then + playerCache[spellID] = heal + if debugEnabled() then + print(colorize("Created cache for "..casterName.."'s "..spellID.." ("..spellName..")", 1, 0.5, 1)) + end + end + PlayerHealCache[casterName][spellID] = heal + if debugEnabled() then + print(colorize(casterName.."'s "..spellID.." ("..spellName..")"..": "..prevHeal.." -> "..heal, 0, 0.8, 0.2)) + end + end +end + +function RemoveHoT(spellName, targetGuid) + if not IncomingHots[targetGuid] then + return + end + if not IncomingHots[targetGuid][spellName] then + return + end + local hot = IncomingHots[targetGuid][spellName] + -- A hack needed because overwritten HoTs cause the previous HoT to be removed, + -- which happens after the UNIT_CASTEVENT + if hot["startTime"] + 0.5 > GetTime() and not hot["swiftmend"] then + return + end + compost:Reclaim(hot) + IncomingHots[targetGuid][spellName] = nil + UpdateTarget(targetGuid) +end + +local roster = AceLibrary("RosterLib-2.0") +local function getGuidFromLogName(name) + local petName, owner = PTUtil.cmatch(name, "%s (%s)") + local unit + if owner then -- A pet is being healed + local ownerUnit = roster:GetUnitIDFromName(owner) + if not ownerUnit then + return + end + unit = roster:GetPetFromOwner(ownerUnit) + else + unit = roster:GetUnitIDFromName(name) + end + if not unit then + -- Check custom units + for _, guid in pairs(PTUnitProxy.CustomUnitGUIDMap) do + if UnitName(guid) == name then + return guid + end + end + end + if unit then + local _, guid = UnitExists(unit) + return guid + end +end + +local function getSelfGuid() + local _, guid = UnitExists("player") + return guid +end + +local autoShotName = PTLocale.Translate("Auto Shot") + +local eventFrame = CreateFrame("Frame", "PTHealPredictCasts") +eventFrame:RegisterEvent("UNIT_CASTEVENT") +eventFrame:SetScript("OnEvent", function() + local caster, target, event, spellID, duration = arg1, arg2, arg3, arg4, arg5 + + if not RelevantGUIDs[caster] then + return + end + + if not UnitIsPlayer(caster) then + return + end + + local spellName = SpellInfo(spellID) + + if ResurrectionSpells[spellName] then + if event == "START" then + if target then + if not ResurrectionTargets[target] then + ResurrectionTargets[target] = compost:GetTable() + end + local resses = ResurrectionTargets[target] + if resses[caster] then + compost:Reclaim(resses[caster]) + end + resses[caster] = compost:AcquireHash("startTime", GetTime(), "castTime", duration) + end + elseif event == "CAST" or event == "FAIL" then + local cast = Casts[caster] + if cast then + local target = cast["targets"][1] + local resses = ResurrectionTargets[target] + if resses and resses[caster] then + compost:Reclaim(resses[caster]) + resses[caster] = nil + + if not next(resses) then + compost:Reclaim(resses) + ResurrectionTargets[target] = nil + end + end + end + end + UpdateTarget(target) + end + + if event == "CAST" or event == "CHANNEL" then + -- Swiftmend could cause Rejuvenation or Regrowth to end very quickly, + -- so there needs to be a flag to allow the removal of the HoT + if spellName == "Swiftmend" then + if IncomingHots[target] then + local hots = IncomingHots[target] + if hots["Rejuvenation"] then + hots["Rejuvenation"]["swiftmend"] = true + end + if hots["Regrowth"] then + hots["Regrowth"]["swiftmend"] = true + end + end + end + + if TRACKED_HOTS[spellName] then + if spellName == "Mend Pet" then -- Mend pet doesn't "target" the pet, so we have to acquire the pet + local units = PTGuidRoster.GetUnits(caster) + if not units then + return + end + local casterUnit = units[1] + if not casterUnit then + return + end + local petUnit = roster:GetPetFromOwner(casterUnit) + if not petUnit then + return + end + local _, guid = UnitExists(petUnit) + target = guid + end + if spellName ~= "Greater Heal" or spellID == 22009 then + AddHot(target, caster, spellID, spellName, GetExpectedHeal(UnitName(caster), spellID.."-HoT")) + end + end + end + + -- Check started cast spell ID to prevent instant mid-cast spells from removing incoming healing + local currentCast = GetCurrentCast(caster) + if event == "CAST" and currentCast and currentCast["spellID"] == spellID then + RemoveIncomingCast(caster, true) + local casterName = UnitName(caster) + local last = LastCastedSpells[casterName] + if not last then + last = compost:GetTable() + LastCastedSpells[casterName] = last + end + last["unit"] = caster + last["target"] = target + last["spellID"] = spellID + return + end + + if event == "START" or event == "FAIL" then + if spellName ~= autoShotName then -- Don't remove the cast when Auto Shot fails + RemoveIncomingCast(caster, event == "START") + end + end + + if event == "START" and target then + if UnitCanAssist(caster, target ~= "" and target or caster) and duration > 0 then + if PRAYER_OF_HEALING_IDS[spellID] then + local inRange = PTUtil.GetSurroundingPartyMembers(target ~= "" and target or caster, 28) + local casterName = UnitName(caster) + local expectedHeal = GetExpectedHeal(casterName, spellID) + AddIncomingMultiCast(inRange, caster, spellID, expectedHeal, duration) + elseif target ~= "" then + local casterName = UnitName(caster) + local expectedHeal = GetExpectedHeal(casterName, spellID) + AddIncomingCast(target, caster, spellID, expectedHeal, duration) + end + end + end +end) + +-- UNIT_CASTEVENT does not fire "FAIL" events on plain 1.12 servers, need to fallback on Nampower +local f = CreateFrame("Frame", "PTTempHealPredictFix") +f:RegisterEvent("SPELL_FAILED_SELF") +f:RegisterEvent("SPELL_FAILED_OTHER") +f:SetScript("OnEvent", function() + local caster = event == "SPELL_FAILED_SELF" and getSelfGuid() or arg1 + local spellID = event == "SPELL_FAILED_SELF" and arg1 or arg2 + + local spellName = SpellInfo(spellID) + + if ResurrectionSpells[spellName] then + local cast = Casts[caster] + if cast then + local target = cast["targets"][1] + local resses = ResurrectionTargets[target] + if resses and resses[caster] then + compost:Reclaim(resses[caster]) + resses[caster] = nil + + if not next(resses) then + compost:Reclaim(resses) + ResurrectionTargets[target] = nil + end + end + end + end + + if spellName ~= autoShotName then -- Don't remove the cast when Auto Shot fails + RemoveIncomingCast(caster, false) + + if castIcons[caster] then + for _, icon in ipairs(castIcons[caster]) do + icon:End(false) + end + compost:Reclaim(castIcons[caster]) + castIcons[caster] = nil + end + end +end) + +local trackedHostileSpells = PTUtil.ToSet({"Shackle Undead", "Mind Control", "Fear", "Polymorph", "Polymorph: Turtle", "Polymorph: Cow"}) +local castIconFrame = CreateFrame("Frame", "PTCastIcons") +castIconFrame:RegisterEvent("UNIT_CASTEVENT") +castIconFrame:SetScript("OnEvent", function() + local caster, target, event, spellID, duration = arg1, arg2, arg3, arg4, arg5 + + local spellName = SpellInfo(spellID) + if event == "CAST" then + if castIcons[caster] and castIcons[caster][1].spellName == spellName then + for _, icon in ipairs(castIcons[caster]) do + icon:End(true) + end + compost:Reclaim(castIcons[caster]) + castIcons[caster] = nil + end + end + if event == "START" or event == "FAIL" then + if spellName ~= autoShotName then -- Don't remove the cast when Auto Shot fails + if castIcons[caster] then + for _, icon in ipairs(castIcons[caster]) do + icon:End(false) + end + compost:Reclaim(castIcons[caster]) + castIcons[caster] = nil + end + end + end + + if event == "START" then + if not PuppeteerSettings.IsExperimentEnabled("CastIcons") or (not UnitCanAssist(caster, target) + and not trackedHostileSpells[spellName] and not PRAYER_OF_HEALING_IDS[spellID]) then + return + end + local inRange + if PRAYER_OF_HEALING_IDS[spellID] then + inRange = PTUtil.GetSurroundingPartyMembers(target ~= "" and target or caster, 28) + end + inRange = inRange or compost:Acquire(target) + + for _, target in ipairs(inRange) do + local spellName, _, tex = SpellInfo(spellID) + local targetFrame + for f in Puppeteer.UnitFrames(target) do + if f.owningGroup:GetContainer():IsShown() then + targetFrame = f + break + end + end + if targetFrame then + local icon = PTGuiLib.Get("puppeteer_cast_icon") + local healAmount = UnitCanAssist(caster, target) and GetExpectedHeal(UnitName(caster), spellID) or 0 + icon:Start(spellName, tex, duration / 1000, caster, healAmount, targetFrame) + if not castIcons[caster] then + castIcons[caster] = compost:GetTable() + end + table.insert(castIcons[caster], icon) + end + end + compost:Reclaim(inRange) + end +end) + + +-- Because the prediction code is not currently bullet-proof to infinite incoming heals, we're checking once a while for old casts +local GARBAGE_CHECK_INTERVAL = 10 +local nextGarbageCheck = GetTime() + GARBAGE_CHECK_INTERVAL +eventFrame:SetScript("OnUpdate", function() + if GetTime() > nextGarbageCheck then + local time = GetTime() + nextGarbageCheck = time + GARBAGE_CHECK_INTERVAL + + for receiver, casts in pairs(IncomingHeals) do + for caster, cast in pairs(casts) do + if cast["startTime"] + 15 < time then + if debugEnabled() then + print(colorize("Removed "..caster.."'s heal on "..receiver.." for taking too long", 1, 0, 0)) + end + compost:Reclaim(cast) + casts[caster] = nil + UpdateTarget(receiver) + end + end + end + + for receiver, hots in pairs(IncomingHots) do + for name, hot in pairs(hots) do + if hot["startTime"] + 25 < time then + if debugEnabled() then + print(colorize("Removed "..hot["caster"].."'s "..name.." (HoT) on ".. + receiver.." for taking too long", 1, 0, 0)) + end + compost:Reclaim(hot) + hots[name] = nil + UpdateTarget(receiver) + end + end + end + + for target, resses in pairs(ResurrectionTargets) do + for caster, res in pairs(resses) do + if res["startTime"] + 20 < time then + if debugEnabled() then + print(colorize("Removed "..caster.."'s resurrection on ".. + target.." for taking too long", 1, 0, 0)) + end + compost:Reclaim(res) + resses[caster] = nil + ResurrectionTargets[target] = nil + UpdateTarget(target) + end + end + if not ResurrectionTargets[target] then -- Must've been removed + compost:Reclaim(resses) + end + end + + for caster, icons in pairs(castIcons) do + local icon = icons[1] + if icon:GetOvertime() > 10 then + for _, icon in ipairs(castIcons[caster]) do + icon:End(false) + end + compost:Reclaim(castIcons[caster]) + castIcons[caster] = nil + end + end + end +end) + +local cmatch = PTUtil.cmatch + +local combatLogFrame = CreateFrame("Frame", "PTHealPredictCombatLog") +combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_BUFF") +combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF") +combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF") -- Needed to see casts coming from other players to yourself +combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF") +combatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PET_BUFF") +combatLogFrame:SetScript("OnEvent", function() + if string.find(arg1, "critically") then + return + end + + if string.find(arg1, "Bonus Healing") then + return + end + + local spell, targetName, heal = cmatch(arg1, HEALEDSELFOTHER) -- "Your %s heals %s for %d." + if spell and targetName and heal then + UpdateCache(tonumber(heal)) + return + end + + local spell, heal = cmatch(arg1, HEALEDSELFSELF) -- "Your %s heals you for %d." + if spell and heal then + UpdateCache(tonumber(heal)) + return + end + + local name, spell, heal = cmatch(arg1, HEALEDOTHERSELF) -- "%s's %s heals you for %d." + if name and spell and heal then + UpdateCache(tonumber(heal), name) + return + end + + local name, spell, targetName, heal = cmatch(arg1, HEALEDOTHEROTHER) -- "%s's %s heals %s for %d." + if name and spell and targetName and heal then + UpdateCache(tonumber(heal), name) + return + end +end) + +local periodicCombatLogFrame = CreateFrame("Frame", "PTHealPredictPerCombatLog") +periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS") +periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS") +periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS") +periodicCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS") +periodicCombatLogFrame:SetScript("OnEvent", function() + local heal, spell = cmatch(arg1, PERIODICAURAHEALSELFSELF) -- "You gain %d health from %s." + if heal and spell then + local selfName = UnitName("player") + local selfGuid = getSelfGuid() + UpdateCacheHot(spell, heal, selfGuid, selfName, selfGuid, selfName) + return + end + + local name, heal, spell = cmatch(arg1, PERIODICAURAHEALSELFOTHER) -- "%s gains %d health from your %s." + if name and heal and spell then + local casterGuid = getSelfGuid() + local targetGuid = getGuidFromLogName(name) + UpdateCacheHot(spell, heal, targetGuid, name, casterGuid, UnitName(casterGuid)) + return + end + + local heal, name, spell = cmatch(arg1, PERIODICAURAHEALOTHERSELF) -- "You gain %d health from %s's %s." + if heal and name and spell then + local casterGuid = getGuidFromLogName(name) + local targetGuid = getSelfGuid() + UpdateCacheHot(spell, heal, targetGuid, UnitName("player"), casterGuid, name) + return + end + + local targetName, heal, name, spell = cmatch(arg1, PERIODICAURAHEALOTHEROTHER) -- "%s gains %d health from %s's %s." + if targetName and heal and name and spell then + local casterGuid = getGuidFromLogName(name) + local targetGuid = getGuidFromLogName(targetName) + UpdateCacheHot(spell, heal, targetGuid, targetName, casterGuid, name) + return + end +end) + +local auraCombatLogFrame = CreateFrame("Frame", "PTHealPredictAuraCombatLog") +auraCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER") +auraCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_PARTY") +auraCombatLogFrame:SetScript("OnEvent", function() + local spell, name = cmatch(arg1, AURAREMOVEDOTHER) -- "%s fades from %s." + if spell and name then + local guid = getGuidFromLogName(name) + if not guid then + return + end + RemoveHoT(spell, guid) + return + end +end) + +local selfAuraCombatLogFrame = CreateFrame("Frame", "PTHealPredictSelfAuraCombatLog") +selfAuraCombatLogFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_SELF") +selfAuraCombatLogFrame:SetScript("OnEvent", function() + local spell = cmatch(arg1, AURAREMOVEDSELF) -- "%s fades from you." + if spell then + RemoveHoT(spell, getSelfGuid()) + return + end +end) + +-- Set the GUIDs to listen to +function SetRelevantGUIDs(guidArray) + local relevant = RelevantGUIDs + for guid in pairs(relevant) do + relevant[guid] = nil + end + for _, guid in ipairs(guidArray) do + relevant[guid] = 1 + end +end + +-- Same as SetRelevantGUIDs, but takes a map with GUIDs as keys (like the GUID roster) to avoid building an array +function SetRelevantGUIDsFromMap(guidMap) + local relevant = RelevantGUIDs + for guid in pairs(relevant) do + relevant[guid] = nil + end + for guid in pairs(guidMap) do + relevant[guid] = 1 + end +end + +-- Provided listener function will receive the arguments: Updated GUID, Updated Incoming Healing +function HookUpdates(listener) + table.insert(Listeners, listener) +end diff --git a/libs/PTUnit.lua b/libs/PTUnit.lua new file mode 100644 index 0000000..3fb8e57 --- /dev/null +++ b/libs/PTUnit.lua @@ -0,0 +1,553 @@ +-- Caches important information about units and makes the data easily readable at any time. +-- If using SuperWoW, the cache map will have GUIDs as the key instead of unit IDs. + +PTUnit = {} +PTUtil.SetEnvironment(PTUnit) +local _G = getfenv(0) + +local util = PTUtil +local GetAuraInfo = util.GetAuraInfo +local AllUnits = util.AllUnits +local AllRealUnits = util.AllRealUnits +local AllUnitsSet = util.AllUnitsSet +local superwow = util.IsSuperWowPresent() +local canGetAuraIDs = util.CanClientGetAuraIDs() + +local compost = PTPool + +-- Non-instance variable +-- Key: Unit ID(Unmodded) or GUID(SuperWoW) | Value: PTUnit Instance +PTUnit.Cached = {} + +PTUnit.Unit = nil + +PTUnit.AurasPopulated = false +-- Buff/debuff entry contents: {"name", "stacks", "texture", "index", "type", "id"(SuperWoW only)} +PTUnit.Buffs = {} -- Array of all buffs +PTUnit.BuffsMap = {} -- Key: Name | Value: Array of buffs with key's name +PTUnit.BuffsIDSet = {} -- Set of currently applied buff IDs +PTUnit.Debuffs = {} -- Array of all debuffs +PTUnit.DebuffsMap = {} -- Key: Name | Value: Array of debuffs with key's name +PTUnit.DebuffsIDSet = {} -- Set of currently applied debuff IDs +PTUnit.TypedDebuffs = {} -- Key: Type | Value: Array of debuffs that are the type +PTUnit.AfflictedDebuffTypes = {} -- Set of the afflicted debuff types +PTUnit.TrackedDebuffTypes = {} -- Set of debuff types that exclude frivilous debuffs + +PTUnit.HasHealingModifier = false +PTUnit.HasImportantDebuff = false + +-- Only used with SuperWoW, managed in AuraTracker.lua +PTUnit.AuraTimes = {} -- Key: Aura Name | Value: {"startTime", "duration"} +PTUnit.AuraTimesByName = {} + +PTUnit.DisplayPVP = false -- This is not the real PVP status of the unit, this is affected by other conditions + +PTUnit.Distance = 0 +PTUnit.InSight = true +PTUnit.IsNew = false + +-- Non-GUID function +function CreateCaches() + if superwow then + Puppeteer.print("Tried to create non-SuperWoW caches while using SuperWoW!") + return + end + for _, unit in ipairs(AllUnits) do + PTUnit:New(unit) + end +end + +function UpdateGuidCaches() + local cached = PTUnit.Cached + local prevCached = PTUtil.CloneTableCompost(cached) + for _, unit in ipairs(AllRealUnits) do + local exists, guid = UnitExists(unit) + if exists then + if not cached[guid] then + PTUnit:New(guid) + Puppeteer.EvaluateTracking(unit, true) + end + prevCached[guid] = nil + end + end + for guid, units in pairs(PTUnitProxy.GUIDCustomUnitMap) do + if not cached[guid] then + PTUnit:New(guid) + for _, unit in ipairs(units) do + Puppeteer.EvaluateTracking(unit, true) + end + end + prevCached[guid] = nil + end + for garbageGuid, cache in pairs(prevCached) do + cache:Dispose() + compost:Reclaim(cache) + cached[garbageGuid] = nil + end + compost:Reclaim(prevCached) +end + +-- Likely never needed to be called when using GUIDs +function UpdateAllUnits() + for _, cache in pairs(PTUnit.Cached) do + cache:UpdateAll() + end +end + +-- Get the PTUnit by unit ID. If using SuperWoW, GUID or unit ID is accepted. +function Get(unit) + if superwow and AllUnitsSet[unit] then + return PTUnit.Cached[PTGuidRoster.GetUnitGuid(unit)] or PTUnit + end + return PTUnit.Cached[unit] or PTUnit +end + +function GetAllUnits() + return PTUnit.Cached +end + +function PTUnit:New(unit) + local obj = compost:AcquireHash("Unit", unit) + setmetatable(obj, self) + self.__index = self + PTUnit.Cached[unit] = obj + obj:AllocateAuras() + obj.AurasPopulated = true -- To force aura fields to generate + obj.IsNew = true + obj.IsSelf = UnitIsUnit(unit, "player") + if superwow then + obj.AuraTimes = compost:GetTable() + obj.AuraTimesByName = compost:GetTable() + end + obj:UpdateAll() + return obj +end + +function PTUnit:Dispose() + ReleaseArrayMap(self.BuffsMap) + ReleaseArrayMap(self.DebuffsMap) + ReleaseArrayMap(self.TypedDebuffs) + compost:Reclaim(self.Buffs) + compost:Reclaim(self.BuffsMap) + compost:Reclaim(self.BuffsIDSet) + compost:Reclaim(self.Debuffs) + compost:Reclaim(self.DebuffsMap) + compost:Reclaim(self.DebuffsIDSet) + compost:Reclaim(self.TypedDebuffs) + compost:Reclaim(self.AfflictedDebuffTypes) + compost:Reclaim(self.TrackedDebuffTypes) + compost:Reclaim(self.BuffEntries, 1) + compost:Reclaim(self.DebuffEntries, 1) + util.CompostReclaim(self.AuraTimes) + compost:Reclaim(self.AuraTimesByName) +end + +function PTUnit:UpdateAll() + self:UpdateAuras() + self:UpdatePVP() + self:UpdateDistance() + self:UpdateSight() +end + +-- Returns true if this unit is new, clearing its new status. +function PTUnit:CheckNew() + if self.IsNew then + self.IsNew = false + return true + end +end + +function PTUnit:UpdatePVP() + if not self.Unit then + return + end + local shouldDisplay = UnitIsPVP(self.Unit) and (not util.IsReallyInInstance() or not UnitIsVisible(self.Unit)) + if self.DisplayPVP ~= (shouldDisplay == 1 or shouldDisplay == true) then + self.DisplayPVP = shouldDisplay + return true + end + return false +end + +function PTUnit:ShouldDisplayPVP() + return self.DisplayPVP +end + +-- Returns true if the distance changed +function PTUnit:UpdateDistance() + if not self.Unit then + return + end + local prevDist = self.Distance + self.Distance = self.IsSelf and 0 or util.GetDistanceTo(self.Unit) + + return self.Distance ~= prevDist +end + +function PTUnit:GetDistance() + return self.Distance +end + +-- Returns true if the sight state has changed +function PTUnit:UpdateSight() + if not self.Unit then + return + end + local wasInSight = self.InSight + self.InSight = self.IsSelf or util.IsInSight(self.Unit) + + return self.InSight ~= wasInSight +end + +function PTUnit:IsInSight() + return self.InSight +end + +function PTUnit:IsBeingResurrected() + if not self.Unit then + return false + end + if PTHealPredict then + return PTHealPredict.IsBeingResurrected(self.Unit) + end + return Puppeteer.HealComm:UnitisResurrecting(UnitName(self.Unit)) +end + +function PTUnit:GetResurrectionCasts() + if not self.Unit then + return 0 + end + if PTHealPredict then + return PTHealPredict.GetResurrectionCount(self.Unit) + end + return Puppeteer.HealComm:UnitisResurrecting(UnitName(self.Unit)) and 1 or 0 +end + +-- Aura storage is reused across rescans so that UNIT_AURA produces no garbage: +-- * Each unit keeps one entry table per aura slot (BuffEntries/DebuffEntries) that is overwritten in place. +-- * The name/type -> array maps hand their arrays back to a dedicated free list when cleared. +-- * Buffs, Debuffs and the sets are emptied in place rather than swapped for new tables. +local freeArrays = {} +local freeArrayCount = 0 +local MAX_FREE_ARRAYS = 1024 +local getn, setn = table.getn, table.setn + +local function GetArray() + if freeArrayCount > 0 then + local array = freeArrays[freeArrayCount] + freeArrays[freeArrayCount] = nil + freeArrayCount = freeArrayCount - 1 + return array + end + return {} +end + +local function ClearArray(array) + for i = getn(array), 1, -1 do + array[i] = nil + end + setn(array, 0) +end + +-- Empties a map whose values are arrays, keeping the arrays for reuse +function ReleaseArrayMap(map) + for key, array in pairs(map) do + map[key] = nil + ClearArray(array) + if freeArrayCount < MAX_FREE_ARRAYS then + freeArrayCount = freeArrayCount + 1 + freeArrays[freeArrayCount] = array + end + end +end +local ReleaseArrayMap = ReleaseArrayMap + +local function ClearSet(set) + for key in pairs(set) do + set[key] = nil + end +end + +local function GetEntry(entries, slot) + local entry = entries[slot] + if not entry then + entry = {} + entries[slot] = entry + end + return entry +end + +function PTUnit:AllocateAuras() + self.Buffs = compost:GetTable() + self.BuffsMap = compost:GetTable() + self.BuffsIDSet = compost:GetTable() + self.Debuffs = compost:GetTable() + self.DebuffsMap = compost:GetTable() + self.DebuffsIDSet = compost:GetTable() + self.TypedDebuffs = compost:GetTable() + self.AfflictedDebuffTypes = compost:GetTable() + self.TrackedDebuffTypes = compost:GetTable() + self.BuffEntries = compost:GetTable() + self.DebuffEntries = compost:GetTable() +end + +function PTUnit:ClearAuras() + if not self.AurasPopulated or self == PTUnit then + return + end + ClearArray(self.Buffs) + ReleaseArrayMap(self.BuffsMap) + ClearSet(self.BuffsIDSet) + ClearArray(self.Debuffs) + ReleaseArrayMap(self.DebuffsMap) + ClearSet(self.DebuffsIDSet) + ReleaseArrayMap(self.TypedDebuffs) + ClearSet(self.AfflictedDebuffTypes) + ClearSet(self.TrackedDebuffTypes) + self.HasHealingModifier = false + self.AurasPopulated = false + self.HasImportantDebuff = false +end + +local claimedAuras = {} +function PTUnit:UpdateAuras() + local unit = self.Unit + + if not unit then + return + end + + Puppeteer.StartTiming("PTUnitAuraUpdate") + + self:ClearAuras() + + if not UnitExists(unit) then + Puppeteer.EndTiming("PTUnitAuraUpdate") + return + end + + local buffs = self.Buffs + local buffsMap = self.BuffsMap + local buffsIDSet = self.BuffsIDSet + local buffEntries = self.BuffEntries + local time = GetTime() + for index = 1, 32 do + local texture, stacks, id = UnitBuff(unit, index) + if not texture then + break + end + local name, type = GetAuraInfo(unit, index, "Buff", id) + if PuppeteerSettings.TrackedHealingBuffs[name] then + self.HasHealingModifier = true + end + local auraTime = nil + if id and self.AuraTimes[id] then + local auraTimes = self.AuraTimes[id] + local highestDuration = -100 + for owner, aura in pairs(auraTimes) do + local timeLeft = aura.endTime - time + if not claimedAuras[aura] and timeLeft > highestDuration then + auraTime = aura + highestDuration = timeLeft + end + end + if auraTime then + claimedAuras[auraTime] = 1 + end + end + local buff = GetEntry(buffEntries, index) + buff.name = name + buff.index = index + buff.texture = texture + buff.stacks = stacks + buff.type = type + buff.id = id + buff.time = auraTime + local nameArray = buffsMap[name] + if not nameArray then + nameArray = GetArray() + buffsMap[name] = nameArray + end + if id ~= nil then + buffsIDSet[id] = true + end + table.insert(nameArray, buff) + table.insert(buffs, buff) + end + + local afflictedDebuffTypes = self.AfflictedDebuffTypes + local trackedDebuffTypes = self.TrackedDebuffTypes + local debuffs = self.Debuffs + local debuffsMap = self.DebuffsMap + local debuffsIDSet = self.DebuffsIDSet + local typedDebuffs = self.TypedDebuffs -- Dispellable debuffs + local debuffEntries = self.DebuffEntries + for index = 1, 16 do + local texture, stacks, type, id = UnitDebuff(unit, index) + if not texture then + break + end + type = type or "" + local name = GetAuraInfo(unit, index, "Debuff", id) + if PuppeteerSettings.TrackedHealingDebuffs[name] then + self.HasHealingModifier = true + end + if PuppeteerSettings.ImportantDebuffs[name] then + self.HasImportantDebuff = true + end + local auraTime = nil + if id and self.AuraTimes[id] then + local auraTimes = self.AuraTimes[id] + local highestDuration = -100 + for owner, aura in pairs(auraTimes) do + local timeLeft = aura.endTime - time + if not claimedAuras[aura] and timeLeft > highestDuration then + auraTime = aura + highestDuration = timeLeft + end + end + if auraTime then + claimedAuras[auraTime] = 1 + end + end + local debuff = GetEntry(debuffEntries, index) + debuff.name = name + debuff.index = index + debuff.texture = texture + debuff.stacks = stacks + debuff.type = type + debuff.id = id + debuff.time = auraTime + local nameArray = debuffsMap[name] + if not nameArray then + nameArray = GetArray() + debuffsMap[name] = nameArray + end + if id ~= nil then + debuffsIDSet[id] = true + end + table.insert(nameArray, debuff) + if type ~= "" then + afflictedDebuffTypes[type] = 1 + if not PuppeteerSettings.IgnoredDispellableDebuffs[name] then + trackedDebuffTypes[type] = 1 + end + local typeArray = typedDebuffs[type] + if not typeArray then + typeArray = GetArray() + typedDebuffs[type] = typeArray + end + table.insert(typeArray, debuff) + end + table.insert(debuffs, debuff) + end + self.AurasPopulated = true + util.ClearTable(claimedAuras) + Puppeteer.EndTiming("PTUnitAuraUpdate") +end + +function PTUnit:HasBuff(name) + return self.BuffsMap[name] ~= nil +end + +-- SuperWoW/WoW 1.18.1 Clients only +function PTUnit:HasBuffID(id) + return self.BuffsIDSet[id] ~= nil +end + +-- Looks for ID if SuperWoW/WoW 1.18.1 Clients is present, otherwise searches by name +function PTUnit:HasBuffIDOrName(id, name) + if canGetAuraIDs then + return self:HasBuffID(id) + end + return self:HasBuff(name) +end + +function PTUnit:HasDebuff(name) + return self.DebuffsMap[name] ~= nil +end + +-- SuperWoW/WoW 1.18.1 Clients only +function PTUnit:HasDebuffID(id) + return self.DebuffsIDSet[id] ~= nil +end + +-- Looks for ID if SuperWoW/WoW 1.18.1 Clients is present, otherwise searches by name +function PTUnit:HasDebuffIDOrName(id, name) + if canGetAuraIDs then + return self:HasDebuffID(id) + end + return self:HasDebuff(name) +end + +function PTUnit:HasDebuffType(type) + return self.AfflictedDebuffTypes[type] +end + +function PTUnit:HasTrackedDebuffType(type) + return self.TrackedDebuffTypes[type] +end + +-- Returns the first buff with the provided name +function PTUnit:GetBuff(name) + if not self:HasBuff(name) then + return + end + return self.BuffsMap[name][1] +end + +-- Returns the table of all buffs with the provided name +function PTUnit:GetBuffs(name) + return self.BuffsMap[name] +end + +function PTUnit:GetDebuff(name) + if not self:HasDebuff(name) then + return + end + return self.DebuffsMap[name][1] +end + +function PTUnit:GetDebuffs(name) + return self.DebuffsMap[name] +end + +function PTUnit:ApplyTimedAura(spellName, spellID, owner, duration, isNampower) + if not self.AuraTimes[spellID] then + self.AuraTimes[spellID] = compost:GetTable() + end + if not self.AuraTimesByName[spellName] then + self.AuraTimesByName[spellName] = compost:GetTable() + end + duration = duration or 0 + local time = GetTime() + local entry = compost:AcquireHash( + "startTime", time, + "endTime", time + duration, + "duration", duration, + "owner", owner, + "ownerName", UnitName(owner), + "nampower", isNampower ~= nil and isNampower ~= false + ) + if self.AuraTimes[spellID][owner] then + compost:Reclaim(self.AuraTimes[spellID][owner]) + end + self.AuraTimes[spellID][owner] = entry + self.AuraTimesByName[spellName] = entry + + self:UpdateAuras() + for ui in Puppeteer.UnitFrames(self.Unit) do + ui:UpdateAuras() + end +end + +function PTUnit:GetAuraTimeRemaining(name) + if not superwow then + return + end + local auraTime = self.AuraTimesByName[name] + if not auraTime then + return + end + return auraTime.startTime - GetTime() + auraTime.duration +end \ No newline at end of file diff --git a/libs/Pool.lua b/libs/Pool.lua new file mode 100644 index 0000000..ca60384 --- /dev/null +++ b/libs/Pool.lua @@ -0,0 +1,134 @@ +-- PTPool: a private, garbage-free table pool for Puppeteer. +-- +-- Drop-in replacement for the parts of Compost-2.0 that Puppeteer uses (GetTable, Acquire, AcquireHash, Erase, +-- Reclaim), written for Lua 5.0's stop-the-world collector: +-- * Pooled tables are held by strong references, so a garbage collection never throws the pool away +-- (Compost keeps only 10 tables strongly and parks the rest in a weak table that every GC empties). +-- * Erasing a table keeps its allocated array/hash capacity, so a reused table does not have to grow again +-- (Compost's Erase forces a rehash that shrinks the table to nothing). +-- * No statistics bookkeeping, no gcinfo() calls, no varargs, no closures. +-- The pool is bounded; tables released beyond the cap are simply left to the collector. + +PTPool = {} +local PTPool = PTPool + +local MAX_POOLED = 3000 + +local pool = {} +local pooled = 0 +local inPool = {} -- Set of tables currently sitting in the pool, used to ignore double releases + +local next = next +local pairs = pairs +local type = type +local setmetatable = setmetatable +local getmetatable = getmetatable +local table_setn = table.setn + +-- Clears every key of the table without shrinking it, and resets its table.getn size. +local function Wipe(t) + if getmetatable(t) then + setmetatable(t, nil) + end + for k in pairs(t) do + t[k] = nil + end + table_setn(t, 0) + return t +end +PTPool.Wipe = Wipe + +local function Get() + if pooled > 0 then + local t = pool[pooled] + pool[pooled] = nil + pooled = pooled - 1 + inPool[t] = nil + if next(t) then -- Something wrote to it after releasing it; never hand out a dirty table + Wipe(t) + end + return t + end + return {} +end +PTPool.Get = Get + +local function Release(t) + if inPool[t] then -- Already released; pooling it twice would hand the same table to two owners + return + end + Wipe(t) + if pooled < MAX_POOLED then + pooled = pooled + 1 + pool[pooled] = t + inPool[t] = true + end +end +PTPool.Release = Release + +-- Releases the table and, down to the given depth, every table it contains. +local function ReleaseDeep(t, depth) + if depth and depth > 0 then + for _, v in pairs(t) do + if type(v) == "table" then + ReleaseDeep(v, depth - 1) + end + end + end + Release(t) +end +PTPool.ReleaseDeep = ReleaseDeep + +-- Compost-compatible method interface, so existing "compost:Method(...)" call sites work unchanged. + +function PTPool:GetTable() + return Get() +end + +function PTPool:Reclaim(t, depth) + if type(t) ~= "table" then + return + end + ReleaseDeep(t, depth) +end + +function PTPool:Erase(t) + if type(t) ~= "table" then + return + end + return Wipe(t) +end + +function PTPool:Acquire(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10) + local t = Get() + local n = 0 + if a1 ~= nil then n = n + 1 t[n] = a1 end + if a2 ~= nil then n = n + 1 t[n] = a2 end + if a3 ~= nil then n = n + 1 t[n] = a3 end + if a4 ~= nil then n = n + 1 t[n] = a4 end + if a5 ~= nil then n = n + 1 t[n] = a5 end + if a6 ~= nil then n = n + 1 t[n] = a6 end + if a7 ~= nil then n = n + 1 t[n] = a7 end + if a8 ~= nil then n = n + 1 t[n] = a8 end + if a9 ~= nil then n = n + 1 t[n] = a9 end + if a10 ~= nil then n = n + 1 t[n] = a10 end + table_setn(t, n) + return t +end + +function PTPool:AcquireHash(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8) + local t = Get() + if k1 ~= nil then t[k1] = v1 end + if k2 ~= nil then t[k2] = v2 end + if k3 ~= nil then t[k3] = v3 end + if k4 ~= nil then t[k4] = v4 end + if k5 ~= nil then t[k5] = v5 end + if k6 ~= nil then t[k6] = v6 end + if k7 ~= nil then t[k7] = v7 end + if k8 ~= nil then t[k8] = v8 end + return t +end + +function PTPool.GetPooledCount() + return pooled +end diff --git a/libs/ShaguUtil.lua b/libs/ShaguUtil.lua new file mode 100644 index 0000000..64a565f --- /dev/null +++ b/libs/ShaguUtil.lua @@ -0,0 +1,377 @@ +--[[ + +The MIT License (MIT) + +Copyright (c) 2016-2021 Eric Mauser (Shagu) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +]] + +-- I'm too lazy to implement this myself, for now.. + +PTUtil.SetEnvironment(PTUtil) +local _G = getfenv(0) + + +local gfind = string.gmatch or string.gfind + +-- [ SanitizePattern ] +-- Sanitizes and convert patterns into gfind compatible ones. +-- 'pattern' [string] unformatted pattern +-- returns: [string] simplified gfind compatible pattern +local sanitize_cache = {} +function SanitizePattern(pattern) + if not sanitize_cache[pattern] then + local ret = pattern + -- escape magic characters + ret = gsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1") + -- remove capture indexes + ret = gsub(ret, "%d%$","") + -- catch all characters + ret = gsub(ret, "(%%%a)","%(%1+%)") + -- convert all %s to .+ + ret = gsub(ret, "%%s%+",".+") + -- set priority to numbers over strings + ret = gsub(ret, "%(.%+%)%(%%d%+%)","%(.-%)%(%%d%+%)") + -- cache it + sanitize_cache[pattern] = ret + end + + return sanitize_cache[pattern] +end + +-- [ GetCaptures ] +-- Returns the indexes of a given regex pattern +-- 'pat' [string] unformatted pattern +-- returns: [numbers] capture indexes +local capture_cache = {} +function GetCaptures(pat) + local r = capture_cache + if not r[pat] then + for a, b, c, d, e in gfind(gsub(pat, "%((.+)%)", "%1"), gsub(pat, "%d%$", "%%(.-)$")) do + r[pat] = { a, b, c, d, e} + end + + r[pat] = r[pat] or {} + end + + return r[pat][1], r[pat][2], r[pat][3], r[pat][4], r[pat][5] +end + +-- [ cmatch ] +-- Same as string.match but aware of capture indexes (up to 5) +-- 'str' [string] input string that should be matched +-- 'pat' [string] unformatted pattern +-- returns: [strings] matched string in capture order +local a, b, c, d, e +local _, va, vb, vc, vd, ve +local ra, rb, rc, rd, re +function cmatch(str, pat) + -- read capture indexes + a, b, c, d, e = GetCaptures(pat) + _, _, va, vb, vc, vd, ve = string.find(str, SanitizePattern(pat)) + + -- put entries into the proper return values + ra = e == 1 and ve or d == 1 and vd or c == 1 and vc or b == 1 and vb or va + rb = e == 2 and ve or d == 2 and vd or c == 2 and vc or a == 2 and va or vb + rc = e == 3 and ve or d == 3 and vd or a == 3 and va or b == 3 and vb or vc + rd = e == 4 and ve or a == 4 and va or c == 4 and vc or b == 4 and vb or vd + re = a == 5 and va or d == 5 and vd or c == 5 and vc or b == 5 and vb or ve + + return ra, rb, rc, rd, re +end + +-- pfQuest +function modulo(val, by) + return val - math.floor(val/by)*by; +end + + +-- Modified serialization and compression functions from pfUI/modules/share.lua + +local indentMap = {[0] = ""} +for i = 1, 8 do + indentMap[i] = indentMap[i - 1].." " +end +-- Parameters: +-- tbl: The table to serialize +-- name: Name of the table, or nil for a nameless table +-- compare: A table to compare equality to, reducing the output if values are equal, can be nil +-- ignored: A set of ignored keys at all depths, can be nil +-- indentation: The number of spaces to indent in each nest, 0-8, default 4 +-- spacing (Internal): String of current indentation +function SerializeTable(tbl, name, compare, ignored, indentation, spacing) + indentation = indentation or 4 + spacing = spacing or "" + local match = nil + local str = spacing .. (name and (( spacing == "" and "" or "[\"" ) .. name .. ( spacing == "" and "" or "\"]" ) .. " = {\n") + or "{\n") + + local isArray = GetTableSize(tbl) == table.getn(tbl) + for k, v in (isArray and ipairs or pairs)(tbl) do + local serializedK = type(k) == "number" and k or ("\""..k.."\"") + local start = str .. spacing .. indentMap[indentation] .. (not isArray and "["..serializedK.."] = " or "") + local vType = type(v) + if not ( ignored ~= nil and (ignored[k] and spacing == "") ) and ( compare == nil or compare[k] == nil or compare[k] ~= tbl[k] ) then + if vType == "table" then + local result = SerializeTable(tbl[k], k, compare and compare[k], ignored, indentation, spacing .. indentMap[indentation]) + if result then + match = true + str = str .. result + end + elseif vType == "string" then + match = true + str = start.."\""..string.gsub(v, "\\", "\\\\").."\",\n" + elseif vType == "number" or vType == "boolean" then + match = true + str = start..tostring(v)..",\n" + end + end + end + + if not match then + return nil + end + + str = str .. spacing .. "}" .. ( spacing == "" and "" or "," ) .. "\n" + return str + end + + function DeserializeTable(str) + return loadstring("return "..str)() + end + + function Compress(input) + -- based on Rochet2's lzw compression + if type(input) ~= "string" then + return nil + end + local len = strlen(input) + if len <= 1 then + return "u"..input + end + + local dict = {} + for i = 0, 255 do + local ic, iic = strchar(i), strchar(i, 0) + dict[ic] = iic + end + local a, b = 0, 1 + + local result = {"c"} + local resultlen = 1 + local n = 2 + local word = "" + for i = 1, len do + local c = strsub(input, i, i) + local wc = word..c + if not dict[wc] then + local write = dict[word] + if not write then + return nil + end + result[n] = write + resultlen = resultlen + strlen(write) + n = n+1 + if len <= resultlen then + return "u"..input + end + local str = wc + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[str] = strchar(a,b) + a = a+1 + word = c + else + word = wc + end + end + result[n] = dict[word] + resultlen = resultlen+strlen(result[n]) + n = n+1 + if len <= resultlen then + return "u"..input + end + return table.concat(result) + end + + function Decompress(input) + -- based on Rochet2's lzw compression + if type(input) ~= "string" or strlen(input) < 1 then + return nil + end + + local control = strsub(input, 1, 1) + if control == "u" then + return strsub(input, 2) + elseif control ~= "c" then + return nil + end + input = strsub(input, 2) + local len = strlen(input) + + if len < 2 then + return nil + end + + local dict = {} + for i = 0, 255 do + local ic, iic = strchar(i), strchar(i, 0) + dict[iic] = ic + end + + local a, b = 0, 1 + + local result = {} + local n = 1 + local last = strsub(input, 1, 2) + result[n] = dict[last] + n = n+1 + for i = 3, len, 2 do + local code = strsub(input, i, i+1) + local lastStr = dict[last] + if not lastStr then + return nil + end + local toAdd = dict[code] + if toAdd then + result[n] = toAdd + n = n+1 + local str = lastStr..strsub(toAdd, 1, 1) + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[strchar(a,b)] = str + a = a+1 + else + local str = lastStr..strsub(lastStr, 1, 1) + result[n] = str + n = n+1 + if a >= 256 then + a, b = 0, b+1 + if b >= 256 then + dict = {} + b = 1 + end + end + dict[strchar(a,b)] = str + a = a+1 + end + last = code + end + return table.concat(result) + end + +function Encode(to_encode) + local index_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + local bit_pattern = '' + local encoded = '' + local trailing = '' + + for i = 1, string.len(to_encode) do + local remaining = tonumber(string.byte(string.sub(to_encode, i, i))) + local bin_bits = '' + for i = 7, 0, -1 do + local current_power = math.pow(2, i) + if remaining >= current_power then + bin_bits = bin_bits .. '1' + remaining = remaining - current_power + else + bin_bits = bin_bits .. '0' + end + end + bit_pattern = bit_pattern .. bin_bits + end + + if mod(string.len(bit_pattern), 3) == 2 then + trailing = '==' + bit_pattern = bit_pattern .. '0000000000000000' + elseif mod(string.len(bit_pattern), 3) == 1 then + trailing = '=' + bit_pattern = bit_pattern .. '00000000' + end + + local count = 0 + for i = 1, string.len(bit_pattern), 6 do + local byte = string.sub(bit_pattern, i, i+5) + local offset = tonumber(tonumber(byte, 2)) + encoded = encoded .. string.sub(index_table, offset+1, offset+1) + count = count + 1 + if count >= 92 then + encoded = encoded .. "\n" + count = 0 + end + end + + return string.sub(encoded, 1, -1 - string.len(trailing)) .. trailing + end + + function Decode(to_decode) + local index_table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + local padded = gsub(to_decode,"%s", "") + local unpadded = gsub(padded,"=", "") + local bit_pattern = '' + local decoded = '' + + to_decode = gsub(to_decode,"\n", "") + to_decode = gsub(to_decode," ", "") + + for i = 1, string.len(unpadded) do + local char = string.sub(to_decode, i, i) + local offset, _ = string.find(index_table, char) + if offset == nil then return nil end + + local remaining = tonumber(offset-1) + local bin_bits = '' + for i = 7, 0, -1 do + local current_power = math.pow(2, i) + if remaining >= current_power then + bin_bits = bin_bits .. '1' + remaining = remaining - current_power + else + bin_bits = bin_bits .. '0' + end + end + + bit_pattern = bit_pattern .. string.sub(bin_bits, 3) + end + + for i = 1, string.len(bit_pattern), 8 do + local byte = string.sub(bit_pattern, i, i+7) + decoded = decoded .. strchar(tonumber(byte, 2)) + end + + local padding_length = string.len(padded)-string.len(unpadded) + + if (padding_length == 1 or padding_length == 2) then + decoded = string.sub(decoded,1,-2) + end + + return decoded + end \ No newline at end of file diff --git a/libs/UnitProxy.lua b/libs/UnitProxy.lua new file mode 100644 index 0000000..0e3bba5 --- /dev/null +++ b/libs/UnitProxy.lua @@ -0,0 +1,471 @@ +-- Proxy unit functions that resolve custom units to their GUIDs before doing what they do. +-- One might say you'd be insane to do this just to have custom units, and you'd be correct. + +-- SuperWoW check, can't rely on PTUtil because it depends on this file +if SpellInfo == nil then + return +end + +PTUnitProxy = {} +local _G = getfenv(0) +setmetatable(PTUnitProxy, {__index = getfenv(1)}) +setfenv(1, PTUnitProxy) + +local compost = PTPool + +-- Unit data + +CustomUnitTypes = {} -- Array of custom unit types(ex: "focus") +CustomUnitTypesSet = {} -- Set of custom unit types + +CustomUnitsTypeMap = {} -- Key: Custom unit(ex: "focus5") | Value: Unit type(ex: "focus") + +CustomUnitsMap = {} -- Key: Type(ex: "focus") | Value: Array of all units +CustomUnitsSetMap = {} -- Key: Type(ex: "focus") | Value: Set of all units + +AllCustomUnits = {} -- Array of all custom units +AllCustomUnitsSet = {} -- Set of all custom units + +-- End of unit data + + +GUIDCustomUnitMap = {} -- Key: GUID | Value: Array of custom units +--UnitTypeGUIDUnitMap = {} -- Key: Unit type | Value: {Key: GUID | Value: Custom unit of type} +CustomUnitGUIDMap = {} -- Key: Custom unit | Value: GUID + +DEFAULT_CUSTOM_UNITS = {["focus"] = 40} + +UpdateListeners = {} + +function RegisterCustomUnits(unitName, maxUnits) + table.insert(CustomUnitTypes, unitName) + CustomUnitTypesSet[unitName] = 1 + local units = {} + local unitsSet = {} + for i = 1, maxUnits do + local unit = unitName..i + units[i] = unit + unitsSet[unit] = 1 + CustomUnitsTypeMap[unit] = unitName + table.insert(AllCustomUnits, unit) + AllCustomUnitsSet[unit] = 1 + end + CustomUnitsMap[unitName] = units + CustomUnitsSetMap[unitName] = unitsSet + + for _, listener in ipairs(UpdateListeners) do + listener() + end +end + +for unitName, maxUnits in pairs(DEFAULT_CUSTOM_UNITS) do + RegisterCustomUnits(unitName, maxUnits) +end + +-- Listen for new custom unit registries +function RegisterUpdateListener(listener) + table.insert(UpdateListeners, listener) +end + +function UnregisterUpdateListener(listener) + PTUtil.RemoveElement(UpdateListeners, listener) +end + +ImportedEnvironments = {} +UnitFunctions = {} + + +function UpdateUnitTypeFrames(unitType) + local groups = compost:GetTable() + for _, unit in ipairs(CustomUnitsMap[unitType]) do + local guid = CustomUnitGUIDMap[unit] + for ui in Puppeteer.UnitFrames(unit) do + if guid then + ui:Show() + ui.guidUnit = guid + ui:UpdateAll() + ui:UpdateIncomingHealing() + else + ui:Hide() + ui.guidUnit = nil + end + groups[ui.owningGroup] = 1 + end + end + + for group, _ in pairs(groups) do + group:EvaluateShown() + end + compost:Reclaim(groups) +end + +function UpdateUnitFrames(unit) + local groups = compost:GetTable() + local guid = CustomUnitGUIDMap[unit] + for ui in Puppeteer.UnitFrames(unit) do + if guid then + ui:Show() + ui.guidUnit = guid + ui:UpdateAll() + else + ui:Hide() + ui.guidUnit = nil + end + groups[ui.owningGroup] = 1 + end + + for group, _ in pairs(groups) do + group:EvaluateShown() + end + compost:Reclaim(groups) +end + +function GetCurrentUnitOfType(guid, unitType) + local unitArray = GUIDCustomUnitMap[guid] + if not unitArray then + return + end + for _, unit in ipairs(unitArray) do + if CustomUnitsTypeMap[unit] == unitType then + return unit + end + end +end + +-- Sets the GUID of the custom unit. If the GUID already has a unit of the type, this will do nothing. +-- If the GUID is nil, the unit will be freed. Returns true if state was changed. +function SetCustomUnitGuid(unit, guid, skipUpdate) + if guid then + local unitType = CustomUnitsTypeMap[unit] + local currentUnit = GetCurrentUnitOfType(guid, unitType) + if currentUnit then + return + end + CustomUnitGUIDMap[unit] = guid + local unitArray = GUIDCustomUnitMap[guid] + if not unitArray then + unitArray = compost:GetTable() + GUIDCustomUnitMap[guid] = unitArray + end + table.insert(unitArray, unit) + PTGuidRoster.SetUnitGuid(unit, guid) + PTUnit.UpdateGuidCaches() + if not skipUpdate then + UpdateUnitFrames(unit) + end + return true + else + local guid = CustomUnitGUIDMap[unit] + if guid then + CustomUnitGUIDMap[unit] = nil + local unitArray = GUIDCustomUnitMap[guid] + if table.getn(unitArray) == 1 then + compost:Reclaim(unitArray) + GUIDCustomUnitMap[guid] = nil + else + PTUtil.RemoveElement(unitArray, unit) + end + PTGuidRoster.SetUnitGuid(unit, nil) + PTUnit.UpdateGuidCaches() + if not skipUpdate then + UpdateUnitFrames(unit) + end + return true + end + end +end + +function ClearGuidUnitType(guid, unitType) + for _, unit in ipairs(CustomUnitsMap[unitType]) do + if CustomUnitGUIDMap[unit] then + SetCustomUnitGuid(unit, nil) + return unit + end + end +end + +-- Finds a free unit of the unit type to assign for the GUID. Returns the unit if successful, otherwise nil. +function SetGuidUnitType(guid, unitType, skipUpdate) + if GetCurrentUnitOfType(guid, unitType) then + return + end + local selectedUnit + for _, unit in ipairs(CustomUnitsMap[unitType]) do + if not CustomUnitGUIDMap[unit] then + selectedUnit = unit + break + end + end + + if not selectedUnit then + return + end + + SetCustomUnitGuid(selectedUnit, guid, skipUpdate) + return selectedUnit +end + +-- Promotes a GUID from a lower unit number of the type to the first unit number. For example, if the GUID +-- is "focus5", it would be moved to "focus1" and all units in between would be shifted down and/or compressed. +-- TODO: Probably should just handle promotion in unit frame sorting rather than actually changing their IDs. +function PromoteGuidUnitType(guid, unitType) + local unit = GetCurrentUnitOfType(guid, unitType) + if not unit then + return + end + local units = CustomUnitsMap[unitType] + local index = PTUtil.IndexOf(units, unit) + + SetCustomUnitGuid(unit, nil) + -- Remove all units before this unit + local reacquire = compost:GetTable() + for i = 1, index do + local moveUnit = units[i] + if CustomUnitGUIDMap[moveUnit] then + local moveGuid = CustomUnitGUIDMap[moveUnit] + SetCustomUnitGuid(moveUnit, nil, true) + table.insert(reacquire, moveGuid) + end + end + -- Set the promotion first so they get the first spot + SetGuidUnitType(guid, unitType, true) + -- Reacquire the removed units + for _, reacquireGuid in ipairs(reacquire) do + SetGuidUnitType(reacquireGuid, unitType, true) + end + compost:Reclaim(reacquire) + UpdateUnitTypeFrames(unitType) +end + +function IsGuidCustomUnit(guid, unit) + local unitArray = GUIDCustomUnitMap[guid] + for _, customUnit in ipairs(unitArray) do + if customUnit == unit then + return true + end + end + return false +end + +function IsUnitUnitType(unit, unitType) + local guid = PTGuidRoster.ResolveUnitGuid(unit) + return IsGuidUnitType(guid, unitType) +end + +function IsGuidUnitType(guid, unitType) + local unitArray = GUIDCustomUnitMap[guid] + if not unitArray then + return false + end + for _, customUnit in ipairs(unitArray) do + if CustomUnitsTypeMap[customUnit] == unitType then + return true + end + end + return false +end + +-- Returns a normal unit associated with the custom unit or GUID, or nil if there is none +function ResolveCustomUnit(customUnit) + local guid = CustomUnitGUIDMap[customUnit] or customUnit + local units = PTGuidRoster.GetUnits(guid) + if units and table.getn(units) > 1 then + for _, rosterUnit in ipairs(units) do + if not AllCustomUnitsSet[rosterUnit] then + return rosterUnit + end + end + end +end + +function CycleUnitType(unitType, onlyAttackable) + local targetGuid = PTGuidRoster.GetUnitGuid("target") + local typeTarget = GetCurrentUnitOfType(targetGuid, unitType) + local typeUnits = CustomUnitsMap[unitType] + if not typeTarget then -- Not targeting this unit type + for _, typeUnit in ipairs(typeUnits) do + if UnitExists(typeUnit) then + TargetUnit(typeUnit) + return + end + end + return + end + local maxUnits = table.getn(typeUnits) + local targetIndex = PTUtil.IndexOf(typeUnits, typeTarget) + local i = targetIndex + for e = 1, maxUnits - 1 do + i = i + 1 + if i > maxUnits then + i = i - maxUnits + end + local guid = CustomUnitGUIDMap[typeUnits[i]] + if guid and UnitExists(guid) and (not onlyAttackable or UnitCanAttack("player", guid)) then + TargetUnit(guid) + break + end + end +end + +function ImportFunctions(env) + for name, func in pairs(UnitFunctions) do + env[name] = func + end + table.insert(ImportedEnvironments, env) +end + +function UpdateImports() + for _, env in ipairs(ImportedEnvironments) do + for name, func in pairs(UnitFunctions) do + env[name] = func + end + end +end + +function UnitProxy(name, func, defaultValue) + local proxy = function(unit) + if AllCustomUnitsSet[unit] then + unit = CustomUnitGUIDMap[unit] + if not unit then + return defaultValue + end + end + if not unit then + return + end + return func(unit) + end + PTUnitProxy[name] = proxy + UnitFunctions[name] = proxy +end + +function DoubleUnitProxy(name, func, defaultValue) + local proxy = function(unit1, unit2) + if AllCustomUnitsSet[unit1] then + unit1 = CustomUnitGUIDMap[unit1] + if not unit1 then + return defaultValue + end + end + if AllCustomUnitsSet[unit2] then + unit2 = CustomUnitGUIDMap[unit2] + if not unit2 then + return defaultValue + end + end + return func(unit1, unit2) + end + PTUnitProxy[name] = proxy + UnitFunctions[name] = proxy +end + +function AuraProxy(name, func, defaultValue) + local proxy = function(unit, index) + if AllCustomUnitsSet[unit] then + unit = CustomUnitGUIDMap[unit] + if not unit then + return defaultValue + end + end + return func(unit, index) + end + PTUnitProxy[name] = proxy + UnitFunctions[name] = proxy +end + +function CustomProxy(name, constructor) + local proxy = constructor() + PTUnitProxy[name] = proxy + UnitFunctions[name] = proxy +end + +function CustomFunction(name, func) + PTUnitProxy[name] = func + UnitFunctions[name] = func +end + +function CreateUnitProxies() + AuraProxy("UnitBuff", _G.UnitBuff, nil) + AuraProxy("UnitDebuff", _G.UnitDebuff, nil) + UnitProxy("UnitExists", _G.UnitExists, false) + UnitProxy("UnitHealth", _G.UnitHealth, 0) + UnitProxy("UnitHealthMax", _G.UnitHealthMax, 0) + UnitProxy("UnitMana", _G.UnitMana, 0) + UnitProxy("UnitManaMax", _G.UnitManaMax, 0) + UnitProxy("UnitIsPlayer", _G.UnitIsPlayer, false) + UnitProxy("UnitIsConnected", _G.UnitIsConnected, false) + UnitProxy("UnitIsDead", _G.UnitIsDead, false) + UnitProxy("UnitIsGhost", _G.UnitIsGhost, false) + UnitProxy("UnitIsDeadOrGhost", _G.UnitIsDeadOrGhost, false) + UnitProxy("UnitIsCorpse", _G.UnitIsCorpse, false) + UnitProxy("UnitClass", _G.UnitClass, "") + UnitProxy("UnitName", _G.UnitName, "Unknown") + UnitProxy("UnitPowerType", _G.UnitPowerType, 0) + UnitProxy("UnitIsVisible", _G.UnitIsVisible, false) + UnitProxy("UnitIsCharmed", _G.UnitIsCharmed, false) + UnitProxy("TargetUnit", _G.TargetUnit, nil) + UnitProxy("FollowUnit", _G.FollowUnit, nil) + UnitProxy("AssistUnit", _G.AssistUnit, nil) + UnitProxy("UnitAffectingCombat", _G.UnitAffectingCombat, false) + UnitProxy("UnitIsPVP", _G.UnitIsPVP, false) + UnitProxy("UnitFactionGroup", _G.UnitFactionGroup, nil) + UnitProxy("UnitPosition", _G.UnitPosition, 0) + DoubleUnitProxy("UnitIsFriend", _G.UnitIsFriend, false) + DoubleUnitProxy("UnitIsEnemy", _G.UnitIsEnemy, false) + DoubleUnitProxy("UnitIsUnit", _G.UnitIsUnit, false) + DoubleUnitProxy("UnitCanAttack", _G.UnitCanAttack, false) + DoubleUnitProxy("CheckInteractDistance", _G.CheckInteractDistance, false) + CustomProxy("CastSpellByName", function() + return function(spell, unit) + if AllCustomUnitsSet[unit] then + unit = CustomUnitGUIDMap[unit] + if not unit then + return + end + end + return _G.CastSpellByName(spell, unit) + end + end) + -- UnitXP SP3 compatibility + CustomProxy("UnitXP", function() + local UnitXP = _G.UnitXP + return function(a1, a2, a3, a4, a5) + if a1 then + a1 = AllCustomUnitsSet[a1] and CustomUnitGUIDMap[a1] or a1 + if not a1 then return 0 end + end + if a2 then + a2 = AllCustomUnitsSet[a2] and CustomUnitGUIDMap[a2] or a2 + if not a2 then return 0 end + end + if a3 then + a3 = AllCustomUnitsSet[a3] and CustomUnitGUIDMap[a3] or a3 + if not a3 then return 0 end + end + if a4 then + a4 = AllCustomUnitsSet[a4] and CustomUnitGUIDMap[a4] or a4 + if not a4 then return 0 end + end + if a5 then + a5 = AllCustomUnitsSet[a5] and CustomUnitGUIDMap[a5] or a5 + if not a5 then return 0 end + end + return UnitXP(a1, a2, a3, a4, a5) + end + end) + if _G.GetUnitGUID then + UnitProxy("UnitGUID", _G.GetUnitGUID, nil) + UnitProxy("GetUnitGUID", _G.GetUnitGUID, nil) + else + CustomProxy("UnitGUID", function() + return function(unit) + local _, guid = UnitExists(unit) + return guid + end + end) + end + + UpdateImports() +end + +CreateUnitProxies() \ No newline at end of file diff --git a/libs/Util.lua b/libs/Util.lua new file mode 100644 index 0000000..812eed3 --- /dev/null +++ b/libs/Util.lua @@ -0,0 +1,1661 @@ +-- Contains standalone utility functions that cause no side effects and don't require data from other files, other than the unit proxy + +PTUtil = {} + +function PTUtil.SetEnvironment(t, index) + setmetatable(t, {__index = index or PTUnitProxy or getfenv(1)}) + setfenv(2, t) +end + +local _G = getfenv(0) +PTUtil.SetEnvironment(PTUtil) +local getn = table.getn + +Classes = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", "SHAMAN", "MAGE", "WARLOCK", "DRUID"} +HealerClasses = {"PRIEST", "DRUID", "SHAMAN", "PALADIN"} + +UnitXPSP3 = pcall(UnitXP, "inSight", "player", "player") -- WTB better way to check for UnitXP SP3 +UnitXPSP3_Version = -1 +if UnitXPSP3 and pcall(UnitXP, "version", "coffTimeDateStamp") then + UnitXPSP3_Version = UnitXP("version", "coffTimeDateStamp") or -1 +end +UnitXPSP3Latest = 0 +SuperWoW = SpellInfo ~= nil +SuperWoWFeatureLevel = 0 +SuperWoW_v1_2 = 1 +SuperWoW_v1_3 = 2 +SuperWoW_v1_4 = 3 +SuperWoWLatest = SuperWoW_v1_4 +if SUPERWOW_VERSION then + if SUPERWOW_VERSION == "1.2" then + SuperWoWFeatureLevel = SuperWoW_v1_2 + elseif SUPERWOW_VERSION == "1.3" then + SuperWoWFeatureLevel = SuperWoW_v1_3 + else -- Anything newer than 1.3 is considered as 1.4 feature set + SuperWoWFeatureLevel = SuperWoW_v1_4 + end +end +Nampower = QueueSpellByName ~= nil +NampowerFeatureLevel = 0 +Nampower_v3_0 = 1 +NampowerLatest = Nampower_v3_0 +if GetNampowerVersion then + local major, minor, patch = GetNampowerVersion() + if major >= 3 then + NampowerFeatureLevel = Nampower_v3_0 + end +end +VanillaUtils = pcall(UnitXP, "unitPosition", "player") + +EnabledMods = { + ["SuperWoW"] = SuperWoW and SuperWoWFeatureLevel, + ["Nampower"] = Nampower and NampowerFeatureLevel, + ["UnitXP SP3"] = UnitXPSP3 and 0, + ["VanillaUtils"] = VanillaUtils and 0 +} + +-- Providers are ordered by preference +ModFeatures = { + { + Name = "GUID Units", + Description = "Better tracking of players/mobs. Grants the ability to add players/mobs to a separate Focus frame.", + Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}} + }, + { + Name = "Aura Timers", + Description = "See timers on buffs and debuffs, with Nampower providing many more than SuperWoW.", + Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}} + }, + { + Name = "Friendly Distance", + Description = "See precise distance to friendly players, with UnitXP SP3 being more accurate.", + Providers = {{"UnitXP SP3"}, {"SuperWoW"}} + }, + { + Name = "Enemy Distance", + Description = "See precise distance to enemies.", + Providers = {{"UnitXP SP3"}} + }, + { + Name = "Friendly Position", + Hidden = true, + Providers = {{"SuperWoW"}, {"VanillaUtils"}} + }, + { + Name = "Enemy Position", + Hidden = true, + Providers = {{"VanillaUtils"}} + }, + { + Name = "Heal Predictions", + Description = "See incoming healing from players that do not have HealComm and predict more accurate numbers.", + Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}} + }, + { + Name = "Mouseover", + Description = "Set your actual mouseover target when hovering over unit frames.", + Providers = {{"SuperWoW"}} + }, + { + Name = "Direct Casting", + Description = "Directly cast spells rather than using split-second target switching tricks.", + Providers = {{"Nampower", Nampower_v3_0}, {"SuperWoW"}} + }, + { + Name = "LOS", + Description = "See when someone is out of your line-of-sight.", + Providers = {{"UnitXP SP3"}} + }, + { + Name = "Spell Queue", + Description = "Queue spell casts like in modern versions of WoW, drastically increasing casting efficiency.", + Providers = {{"Nampower"}} + } +} +EnabledModFeatures = {} +for _, feature in ipairs(ModFeatures) do + for _, providerEntry in ipairs(feature.Providers) do + local modInfo = EnabledMods[providerEntry[1]] + if modInfo and (not providerEntry[2] or modInfo >= providerEntry[2]) then + feature.ChosenProvider = providerEntry[1] + EnabledModFeatures[feature.Name] = providerEntry[1] + break + end + end +end + +function GetModState(present, upToDate) + if not present then + return "Not Detected" + end + if not upToDate then + return "Outdated" + end + return "Detected" +end + +function IsSuperWowPresent() + return SuperWoW +end + +function GetSuperWoWVersionText() + if not SuperWoW then + return "" + end + return "Version "..(SUPERWOW_VERSION or "1.1 or older") +end + +function IsSuperWoWUpToDate() + return SuperWoWFeatureLevel >= SuperWoWLatest +end + +function GetSuperWoWVersionState() + return GetModState(IsSuperWowPresent(), IsSuperWoWUpToDate()) +end + +function IsUnitXPSP3Present() + return UnitXPSP3 +end + +function GetUnitXPSP3VersionText() + if not UnitXPSP3 then + return "" + end + return UnitXPSP3_Version > -1 and "Version "..date("%x", UnitXPSP3_Version) or "Old Version" +end + +function IsUnitXPSP3UpToDate() + return UnitXPSP3_Version >= UnitXPSP3Latest +end + +function GetUnitXPSP3VersionState() + return GetModState(IsUnitXPSP3Present(), IsUnitXPSP3UpToDate()) +end + +-- Doesn't detect Namreeb's Nampower +function IsNampowerPresent() + return Nampower +end + +function GetNampowerVersionText() + if not Nampower then + return "" + end + if not GetNampowerVersion then + return "Old Version" + end + local major, minor, patch = GetNampowerVersion() + return "Version "..major.."."..minor.."."..patch +end + +function IsNampowerUpToDate() + return NampowerFeatureLevel >= NampowerLatest +end + +function GetNampowerVersionState() + return GetModState(IsNampowerPresent(), IsNampowerUpToDate()) +end + +function GetModVersionText(mod) + if mod == "SuperWoW" then + return GetSuperWoWVersionText() + elseif mod == "UnitXP SP3" then + return GetUnitXPSP3VersionText() + elseif mod == "Nampower" then + return GetNampowerVersionText() + elseif mod == "VanillaUtils" then + return "Unknown" + end + return "Invalid Mod: "..tostring(mod) +end + +function GetModVersionState(mod) + if mod == "SuperWoW" then + return GetSuperWoWVersionState() + elseif mod == "UnitXP SP3" then + return GetUnitXPSP3VersionState() + elseif mod == "Nampower" then + return GetNampowerVersionState() + elseif mod == "VanillaUtils" then + return GetModState(true, true) + end + return "Invalid Mod: "..tostring(mod) +end + +function HasModVersion(mod, version) + return EnabledMods[mod] and EnabledMods[mod] >= version +end + +-- TURTLE_WOW_VERSION is the global the WoW 1.18.1 client defines, so it keeps its name +Client1181 = TURTLE_WOW_VERSION ~= nil + +PowerColors = { + ["mana"] = {0.1, 0.25, 1}, --{r = 0, g = 0, b = 0.882}, Not accurate, changed color to make brighter + ["rage"] = {1, 0, 0}, + ["focus"] = {1, 0.5, 0.25}, + ["energy"] = {1, 1, 0} +} + +ClassPowerTypes = { + ["WARRIOR"] = "rage", + ["PALADIN"] = "mana", + ["HUNTER"] = "mana", + ["ROGUE"] = "energy", + ["PRIEST"] = "mana", + ["SHAMAN"] = "mana", + ["MAGE"] = "mana", + ["WARLOCK"] = "mana", + ["DRUID"] = "mana" +} + +-- The power types IDs mapped in accordance to UnitPowerType +PowerTypeMap = { + [0] = "mana", + [1] = "rage", + [2] = "focus", + [3] = "energy" +} + +ResurrectionSpells = { + ["PRIEST"] = "Resurrection", + ["PALADIN"] = "Redemption", + ["SHAMAN"] = "Ancestral Spirit", + ["DRUID"] = "Rebirth" +} +ResurrectionSpellsSet = { + ["Resurrection"] = "PRIEST", + ["Redemption"] = "PALADIN", + ["Ancestral Spirit"] = "SHAMAN", + ["Rebirth"] = "DRUID" +} + +-- The default color Blizzard uses for text +DefaultTextColor = {1, 0.82, 0} + +PartyUnits = {"player", "party1", "party2", "party3", "party4"} +PetUnits = {"pet", "partypet1", "partypet2", "partypet3", "partypet4"} +TargetUnits = {"target"} +RaidUnits = {} +for i = 1, MAX_RAID_MEMBERS do + RaidUnits[i] = "raid"..i +end +RaidPetUnits = {} +for i = 1, MAX_RAID_MEMBERS do + RaidPetUnits[i] = "raidpet"..i +end +CustomUnits = PTUnitProxy and PTUnitProxy.AllCustomUnits or {} +CustomUnitsSet = PTUnitProxy and PTUnitProxy.AllCustomUnitsSet or {} +FocusUnits = PTUnitProxy and PTUnitProxy.CustomUnitsMap["focus"] or {} + +local unitArrays = {PartyUnits, PetUnits, RaidUnits, RaidPetUnits, TargetUnits} +AllUnits = {} +for _, unitArray in ipairs(unitArrays) do + for _, unit in ipairs(unitArray) do + table.insert(AllUnits, unit) + end +end +AllRealUnits = {} +for i, unit in ipairs(AllUnits) do + AllRealUnits[i] = unit +end +if PTUnitProxy then + for _, unit in ipairs(CustomUnits) do + table.insert(AllUnits, unit) + end + PTUnitProxy.RegisterUpdateListener(function() + local i = 1 + for _, unit in ipairs(AllRealUnits) do + AllUnits[i] = unit + i = i + 1 + end + for _, unit in ipairs(CustomUnits) do + AllUnits[i] = unit + i = i + 1 + end + ClearTable(AllUnitsSet) + for k, v in pairs(ToSet(AllUnits)) do + AllUnitsSet[k] = v + end + end) +end + +local assetsPath = "Interface\\AddOns\\Puppeteer\\assets\\" +function GetAssetsPath() + return assetsPath +end + +-- Returns a new table with the elements of the given array being the keys with 1 being the value of all keys, +-- or the index if indexValue is true +function ToSet(array, indexValue, to) + local set = to or {} + for index, value in ipairs(array) do + set[value] = indexValue and index or 1 + end + return set +end + +-- Returns a new table with the keys of the given set being the values of the array +function ToArray(set) + local array = {} + for value, _ in pairs(set) do + table.insert(array, value) + end + return array +end + +-- Adds the elements of otherArray to the array +function AppendArrayElements(array, otherArray) + for _, v in ipairs(otherArray) do + table.insert(array, v) + end +end + +function IndexOf(table, value) + for i, v in ipairs(table) do + if v == value then + return i + end + end + return -1 +end + +function KeyOf(table, value) + for k, v in pairs(table) do + if v == value then + return k + end + end +end + +function ArrayContains(table, value) + for _, v in ipairs(table) do + if v == value then + return true + end + end + return false +end + +function RemoveElement(t, value) + table.remove(t, IndexOf(t, value)) +end + +function ReverseArray(t) + for i = 1, math.floor(getn(t) / 2) do + t[i], t[getn(t) - i + 1] = t[getn(t) - i + 1], t[i] + end +end + +function CloneTable(table, deep) + local clone = {} + for k, v in pairs(table) do + if deep and type(v) == "table" then + clone[k] = CloneTable(v, true) + else + clone[k] = v + end + end + return clone +end + +function ApplyTableDiffs(t, overrides) + for k, v in pairs(overrides) do + if t[k] ~= nil then + if type(v) == "table" then + if type(t[k]) == "table" then + ApplyTableDiffs(t[k], v) + else + t[k] = CloneTable(v, true) + end + else + t[k] = v + end + else + t[k] = type(v) == "table" and CloneTable(v, true) or v + end + end +end + +local compost = PTPool + +-- Recursively reclaims all tables this table contains +function CompostReclaim(t) + for k, v in pairs(t) do + if type(v) == "table" then + CompostReclaim(v) + end + end + compost:Reclaim(t) +end + +function CloneTableCompost(t, deep) + local clone = compost:GetTable() + local n = 0 + for k, v in pairs(t) do + if deep and type(v) == "table" then + clone[k] = CloneTableCompost(v, true) + else + clone[k] = v + end + n = n + 1 + end + table.setn(clone, n) + return clone +end + +function ClearTable(t) + for k, v in pairs(t) do + t[k] = nil + end + table.setn(t, 0) +end + +function GetTableSize(t) + local size = 0 + for _ in pairs(t) do + size = size + 1 + end + return size +end + +function IsTableEmpty(t) + for _ in pairs(t) do + return false + end + return true +end + +-- Recursion not supported +function TableEquals(t1, t2) + -- Verify the tables have the same keys + for k, v in pairs(t1) do + if t2[k] == nil then + return false + end + end + for k, v in pairs(t2) do + if t1[k] == nil then + return false + end + end + -- Verify the tables have equal values + for k, v in pairs(t1) do + if type(v) == "table" then + if type(t2[k]) ~= "table" or not TableEquals(v, t2[k]) then + return false + end + else + if v ~= t2[k] then + return false + end + end + end + return true +end + +function TraverseTable(v, k1, k2, k3, k4, k5) + -- Walks the keys directly rather than packing them into a temporary table + if k1 == nil then return v end + if type(v) ~= "table" then return nil, k1 end + v = v[k1] + if k2 == nil then return v end + if type(v) ~= "table" then return nil, k2 end + v = v[k2] + if k3 == nil then return v end + if type(v) ~= "table" then return nil, k3 end + v = v[k3] + if k4 == nil then return v end + if type(v) ~= "table" then return nil, k4 end + v = v[k4] + if k5 == nil then return v end + if type(v) ~= "table" then return nil, k5 end + return v[k5] +end + +-- Courtesy of ChatGPT +function SplitString(str, delimiter) + local result = {} + local start_pos = 1 + + while true do + local end_pos = string.find(str, delimiter, start_pos, true) + + if not end_pos then + table.insert(result, string.sub(str, start_pos)) + break + end + + table.insert(result, string.sub(str, start_pos, end_pos - 1)) + start_pos = end_pos + string.len(delimiter) + end + + return result +end + +function StartsWith(str, starts) + return string.sub(str, 1, string.len(starts)) == starts +end + +function RoundNumber(number, decimalPlaces) + decimalPlaces = decimalPlaces or 0 + return math.floor(number * 10^decimalPlaces + 0.5) / 10^decimalPlaces +end + +-- Courtesy of ChatGPT +function InterpolateColors(colors, t) + local r, g, b = InterpolateColorsNoTable(colors, t) + return {r, g, b} +end + +function InterpolateColorsNoTable(colors, t) + local numColors = getn(colors) + + -- Ensure t is between 0 and 1 + t = math.max(0, math.min(1, t)) + + -- If there are fewer than 2 colors, just return the single color + if numColors < 2 then + local c = colors[1] + return c[1], c[2], c[3] + end + + -- Determine the segment in which t falls + local scaledT = t * (numColors - 1) -- Scale t to cover the range of indices + local index = math.floor(scaledT) + local fraction = scaledT - index + + -- Handle edge cases where index is out of bounds + if index >= numColors - 1 then + local c = colors[numColors] + return c[1], c[2], c[3] + end + + local color1 = colors[index + 1] + local color2 = colors[index + 2] + + -- Linear interpolation between color1 and color2 + local r = color1[1] + (color2[1] - color1[1]) * fraction + local g = color1[2] + (color2[2] - color1[2]) * fraction + local b = color1[3] + (color2[3] - color1[3]) * fraction + + return r, g, b +end + +-- Color code prefixes ("|cFFrrggbb") are cached so colorizing doesn't format a new prefix string every call +local colorPrefixCache = {} +local colorPrefixCacheSize = 0 +local MAX_COLOR_PREFIX_CACHE = 1024 +local floor = math.floor +local format = string.format +function GetColorPrefix(r, g, b) + local ri, gi, bi = floor(r * 255), floor(g * 255), floor(b * 255) + local key = ri * 65536 + gi * 256 + bi + local prefix = colorPrefixCache[key] + if not prefix then + prefix = format("|cFF%02x%02x%02x", ri, gi, bi) + if colorPrefixCacheSize < MAX_COLOR_PREFIX_CACHE then + colorPrefixCache[key] = prefix + colorPrefixCacheSize = colorPrefixCacheSize + 1 + end + end + return prefix +end +local GetColorPrefix = GetColorPrefix + +function Colorize(text, r, g, b) + if type(r) == "table" then + local rgb = r + r = rgb[1] + g = rgb[2] + b = rgb[3] + end + return GetColorPrefix(r, g, b) .. text .. "|r" +end + +function StripColors(text) + text = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "") + text = string.gsub(text, "|r", "") + return text +end + +local coloredRoles = { + ["Tank"] = Colorize("Tank", 0.3, 0.6, 1), + ["Healer"] = Colorize("Healer", 0.2, 1, 0.2), + ["Damage"] = Colorize("Damage", 1, 0.4, 0.4), + ["No Role"] = "No Role" +} +function GetColoredRoleText(role) + if not role then + return coloredRoles["No Role"] + end + return coloredRoles[role] +end + +function IsFeigning(unit) + local cache = PTUnit.Get(unit) + if not cache then + local unitClass = GetClass(unit) + if unitClass == "HUNTER" then + local superwow = IsSuperWowPresent() + for i = 1, 32 do + local texture, _, id = UnitBuff(unit, i) + if superwow then -- Use the ID if SuperWoW is present + if id == 5384 then -- 5384 is Feign Death + return true + end + else -- Use the texture otherwise + if texture == "Interface\\Icons\\Ability_Rogue_FeignDeath" then + return true + end + end + end + end + return false + end + return cache:HasBuffIDOrName(5384, "Feign Death") +end + +function HasAura(unit, auraType, auraTexture, auraID) + local auraFunc = auraType == "Buff" and UnitBuff or UnitDebuff + local checkCount = auraType == "Buff" and 32 or 16 + + local superwow = IsSuperWowPresent() + for i = 1, checkCount do + local texture, _, id = auraFunc(unit, i) + if superwow and auraID then + if auraID == id then + return true + end + else + if texture == auraTexture then + return true + end + end + end + return false +end + +function GetBagSlotInfo(bag, slot) + local link = GetContainerItemLink(bag, slot) + if not link then + return + end + local _, _, name = string.find(link, "%[(.*)%]") + local _, count = GetContainerItemInfo(bag, slot) + return name, count +end + +-- Returns: Bag index, Slot index +function FindBagSlot(itemName) + local bestBag, bestSlot, lowestStackSize + for bag = 0, NUM_BAG_FRAMES do + for slot = 1, GetContainerNumSlots(bag) do + local name, count = GetBagSlotInfo(bag, slot) + if itemName == name then + if not lowestStackSize or lowestStackSize > count then + bestBag = bag + bestSlot = slot + lowestStackSize = count + end + end + end + end + return bestBag, bestSlot +end + +-- Returns true if an item was found and attempted to be used +function UseItem(itemName) + local bag, slot = FindBagSlot(itemName) + if not bag then + return + end + UseContainerItem(bag, slot) + return true +end + +function GetItemCount(itemName) + local total = 0 + for bag = 0, NUM_BAG_FRAMES do + for slot = 1, GetContainerNumSlots(bag) do + local name, count = GetBagSlotInfo(bag, slot) + if itemName == name then + total = total + count + end + end + end + return total +end + +function IsValidMacro(name) + return GetMacroIndexByName(name) ~= 0 +end + +function RunMacro(name, target) + if not IsValidMacro(name) then + return + end + if target then + _G.PT_MacroTarget = target + end + local _, _, body = GetMacroInfo(GetMacroIndexByName(name)) + local commands = SplitString(body, "\n") + for i = 1, getn(commands) do + ChatFrameEditBox:SetText(commands[i]) + ChatEdit_SendText(ChatFrameEditBox) + end + if target then + _G.PT_MacroTarget = nil + end +end + +local ScanningTooltip = CreateFrame("GameTooltip", "PTScanningTooltip", nil, "GameTooltipTemplate"); +ScanningTooltip:SetOwner(WorldFrame, "ANCHOR_NONE"); +-- Allow tooltip SetX() methods to dynamically add new lines based on these +ScanningTooltip:AddFontStrings( + ScanningTooltip:CreateFontString( "$parentTextLeft1", nil, "GameTooltipText" ), + ScanningTooltip:CreateFontString( "$parentTextRight1", nil, "GameTooltipText" ) ); + +local spellRankString = RANK.." " +function ExtractSpellRank(spellname) + -- Find the starting position of "Rank " + local start_pos = string.find(spellname, spellRankString) + + -- Check if "Rank " was found + if start_pos then + -- Adjust start_pos to point to the first digit + --start_pos = start_pos + 5 -- Move past "Rank " + + -- Find the ending parenthesis + local end_pos = string.find(spellname, ")", start_pos) + + -- Extract the number substring + if end_pos then + local number_str = string.sub(spellname, start_pos, end_pos - 1) + --local number = tonumber(number_str) -- Convert to a number + + return number_str + end + end + return nil +end + +local resourceCostPatterns = {[MANA_COST] = "mana", [RAGE_COST] = "rage", [ENERGY_COST] = "energy", [FOCUS_COST] = "focus"} +function ExtractResourceCost(costText) + for costPattern, resourceName in pairs(resourceCostPatterns) do + local cost = cmatch(costText, costPattern) + if cost then + return tonumber(cost), resourceName + end + end + return 0 +end + +function GetSpellID(spellname) + local id = 1 + local matchingSpells = compost:GetTable() + local spellRank = ExtractSpellRank(spellname) + + if spellRank ~= nil then + spellname = string.gsub(spellname, "%b()", "") + end + + for i = 1, GetNumSpellTabs() do + local _, _, _, numSpells = GetSpellTabInfo(i) + for j = 1, numSpells do + local spellName, rank, realID = GetSpellName(id, "spell") + if spellName == spellname then + if rank == spellRank then -- If the rank is specified, then we can check if this is the right spell + return id + else + table.insert(matchingSpells, id) + end + end + id = id + 1 + end + end + local foundID = matchingSpells[getn(matchingSpells)] + compost:Reclaim(matchingSpells) + return foundID +end + +local costCache = {} +local costTypeCache = {} +local costCacheDirty = false +function MarkSpellCostCacheDirty() + costCacheDirty = true +end +-- Returns the numerical cost and the resource name; "unknown" if the spell is unknown; 0 if the spell is free +function GetResourceCost(spellName) + if costCacheDirty then + ClearTable(costCache) + ClearTable(costTypeCache) + costCacheDirty = false + end + if costCache[spellName] then + return costCache[spellName], costTypeCache[spellName] + end + + ScanningTooltip:SetOwner(UIParent, "ANCHOR_NONE"); + + local spellID, bookType + if GetSpellSlotTypeIdForName then -- Nampower 2.6.0 function + spellID, bookType = GetSpellSlotTypeIdForName(spellName) + if bookType == "unknown" then + return "unknown" + end + if bookType ~= "spell" then + return 0 + end + else + spellID = GetSpellID(spellName) + end + if not spellID then + return "unknown" + end + + ScanningTooltip:SetSpell(spellID, "spell") + + local leftText = _G["PTScanningTooltipTextLeft2"] + + if leftText:GetText() then + costCache[spellName], costTypeCache[spellName] = ExtractResourceCost(leftText:GetText()) + return costCache[spellName], costTypeCache[spellName] + end + costCache[spellName] = 0 + return 0 +end + +if HasModVersion("Nampower", Nampower_v3_0) then + function GetResourceCost(spellName) + local spellID = GetSpellIdForName(spellName) + local cost = GetSpellRecField(spellID, "manaCost") + local powerType = GetSpellRecField(spellID, "powerType") + if powerType == 1 then -- Rage is 10x for some reason + cost = cost / 10 + end + return cost or "unknown", PowerTypeMap[powerType] + end +end + +-- Returns the aura's name and its school type +function ScanAuraInfo(unit, index, type) + -- Make these texts blank since they don't clear otherwise + local leftText = _G["PTScanningTooltipTextLeft1"] + leftText:SetText("") + local rightText = _G["PTScanningTooltipTextRight1"] + rightText:SetText("") + if type == "Buff" then + ScanningTooltip:SetUnitBuff(unit, index) + else + ScanningTooltip:SetUnitDebuff(unit, index) + end + return leftText:GetText() or "", rightText:GetText() or "" +end + +if SuperWoW or Client1181 then + local auraNameCache = {} + local auraTypeCache = {} + + function GetAuraInfo(unit, index, type, id) + if not id then + if type == "Buff" then + local _, _, i = UnitBuff(unit, index) + id = i + else + local _, _, _, i = UnitDebuff(unit, index) + id = i + end + if not id then -- Uh oh, the 1.18.1 client lost the ID + return ScanAuraInfo(unit, index, type) + end + end + if not auraNameCache[id] then + auraNameCache[id], auraTypeCache[id] = ScanAuraInfo(unit, index, type) + end + return auraNameCache[id], auraTypeCache[id] + end + + function GetCachedAuraInfo(id) + return auraNameCache[id], auraTypeCache[id] + end +else + GetAuraInfo = ScanAuraInfo +end + +function GetActionSlotName(slot) + _G["PTScanningTooltipTextLeft1"]:SetText("") + ScanningTooltip:SetAction(slot) + return _G["PTScanningTooltipTextLeft1"]:GetText() or "" +end + +local actionCache = {} +function FindAction(name) + if actionCache[name] then + local data = actionCache[name] + if GetActionTexture(data.slot) == data.texture then + return data.slot + end + actionCache[name] = nil + end + for i = 1, 120 do + if GetActionTexture(i) then + local slotName = GetActionSlotName(i) + if slotName == name then + actionCache[name] = { + slot = i, + texture = GetActionTexture(i) + } + return i + end + end + end +end + +function IsCurrentActionByName(name) + local slot = FindAction(name) + if slot then + return IsCurrentAction(slot) + end +end + +function IsAutoRepeatActionByName(name) + local slot = FindAction(name) + if slot then + return IsAutoRepeatAction(slot) + end +end + +-- Casts an action if it's not already being used. Very useful for auto attack abilities. They must be somewhere on your bars. +function CastActionByName(name, target) + if not (IsAutoRepeatActionByName(name) or IsCurrentActionByName(name)) then + CastSpellByName(name, target) + end +end + +-- Returns an array of the units in the party number or the unit's raid group +function GetRaidPartyMembers(partyNumberOrUnit) + if not RAID_SUBGROUP_LISTS then + return compost:GetTable() + end + if type(partyNumberOrUnit) == "string" then + partyNumberOrUnit = FindUnitRaidGroup(partyNumberOrUnit) + end + local members = compost:GetTable() + if RAID_SUBGROUP_LISTS[partyNumberOrUnit] then + local n = 0 + for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[partyNumberOrUnit]) do + n = n + 1 + members[n] = RaidUnits[raidNumber] + end + table.setn(members, n) + end + return members +end + +-- Returns the raid unit that this unit is, or nil if the unit is not in the raid +function FindRaidUnit(unit) + if not RAID_SUBGROUP_LISTS then + return nil + end + for party = 1, 8 do + if RAID_SUBGROUP_LISTS[party] then + for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[party]) do + local raidUnit = RaidUnits[raidNumber] + if UnitIsUnit(unit, raidUnit) then + return raidUnit + end + end + end + end +end + +-- Returns the raid group number the unit is part of, or nil if the unit is not in the raid +function FindUnitRaidGroup(unit) + for party = 1, 8 do + if RAID_SUBGROUP_LISTS[party] then + for frameNumber, raidNumber in pairs(RAID_SUBGROUP_LISTS[party]) do + local raidUnit = RaidUnits[raidNumber] + if UnitIsUnit(unit, raidUnit) then + return party + end + end + end + end +end + +-- Requires SuperWoW +function GetSurroundingPartyMembers(player, range) + local units + if UnitInRaid("player") then + units = GetRaidPartyMembers(player) + else + units = CloneTableCompost(PartyUnits) + AppendArrayElements(units, PetUnits) + end + + local inRange = GetUnitsInRange(player, units, range or 30) + compost:Reclaim(units) + return inRange +end + +function GetSurroundingRaidMembers(player, range, checkPets) + local units + if UnitInRaid("player") then + units = CloneTableCompost(RaidUnits) + if checkPets then + AppendArrayElements(units, RaidPetUnits) + end + else + units = CloneTableCompost(PartyUnits) + if checkPets then + AppendArrayElements(units, PetUnits) + end + end + + local inRange = GetUnitsInRange(player, units, range or 30) + compost:Reclaim(units) + return inRange +end + +function GetUnitsInRange(center, units, range) + local inRange = compost:GetTable() + for _, unit in ipairs(units) do + local exists, guid = UnitExists(unit) + if exists and UnitIsConnected(unit) and not UnitIsDeadOrGhost(unit) and + GetDistanceBetween(center, unit) <= (range or 30) then + table.insert(inRange, guid) + end + end + return inRange +end + +-- Blizzard's UI functions seem to get called referring to a global called "this" referring to the UI object. +-- This function calls a function on the object, emulating the "this" variable. +function CallWithThis(object, func) + local prevThis = _G.this + _G.this = object + func() + _G.this = prevThis +end + +local function _FixFrameLevels(parent, ...) + local level = parent:GetFrameLevel() + 1 + for i = 1, getn(arg) do + local child = arg[i] + -- Children of scroll frames can block components outside if they're layered above the scroll pane + if parent.GetScrollChild and parent:GetScrollChild() == child then + child:SetFrameLevel(level - 1) + else + child:SetFrameLevel(level) + end + _FixFrameLevels(child, child:GetChildren()) + end +end + +function FixFrameLevels(frame) + return _FixFrameLevels(frame, frame:GetChildren()) +end + +-- Modified ChatGPT function +function RotateTexture(texture, degrees, extent) + local angleRadians = math.rad(degrees) + local cos = math.cos(angleRadians) + local sin = math.sin(angleRadians) + + extent = (extent or 1) / 2 + + local x1, y1 = -extent, extent -- UL + local x2, y2 = -extent, -extent -- LL + local x3, y3 = extent, extent -- UR + local x4, y4 = extent, -extent -- LR + + local ULx, ULy = x1 * cos - y1 * sin + 0.5, x1 * sin + y1 * cos + 0.5 + local LLx, LLy = x2 * cos - y2 * sin + 0.5, x2 * sin + y2 * cos + 0.5 + local URx, URy = x3 * cos - y3 * sin + 0.5, x3 * sin + y3 * cos + 0.5 + local LRx, LRy = x4 * cos - y4 * sin + 0.5, x4 * sin + y4 * cos + 0.5 + + texture:SetTexCoord(ULx, ULy, LLx, LLy, URx, URy, LRx, LRy) +end + +local PTTaskExecutor = CreateFrame("Frame", "PTTaskExecutor") +local taskQueue = {} +local offTaskQueue = {} +local PTTaskExecutor_OnUpdate = function() + local runningQueue = taskQueue + taskQueue = offTaskQueue + offTaskQueue = runningQueue + for _, task in ipairs(runningQueue) do + local ok, result = pcall(task) + if not ok then + DEFAULT_CHAT_FRAME:AddMessage("Puppeteer Task Error: "..result) + end + end + ClearTable(runningQueue) + if getn(taskQueue) == 0 then + PTTaskExecutor:SetScript("OnUpdate", nil) + end +end +function RunLater(func) + table.insert(taskQueue, func) + if PTTaskExecutor:GetScript("OnUpdate") == nil then + PTTaskExecutor:SetScript("OnUpdate", PTTaskExecutor_OnUpdate) + end +end + +-- Returns the class without the first return variable fluff +function GetClass(unit) + local _, class = UnitClass(unit) + return class +end + +function GetClasses() + return Classes +end + +function GetRandomClass() + return Classes[math.random(1, 9)] +end + +local healerClassesSet = ToSet(HealerClasses) +function IsHealerClass(unit) + return healerClassesSet[GetClass(unit)] == 1 +end + +local classColors = { + ["DRUID"] = {1.0, 0.49, 0.04}, + ["HUNTER"] = {0.67, 0.83, 0.45}, + ["MAGE"] = {0.41, 0.8, 0.94}, + ["PALADIN"] = {0.96, 0.55, 0.73}, + ["PRIEST"] = {1.0, 1.0, 1.0}, + ["ROGUE"] = {1.0, 0.96, 0.41}, + ["SHAMAN"] = {0.14, 0.35, 1.0}, + ["WARLOCK"] = {0.58, 0.51, 0.79}, + ["WARRIOR"] = {0.78, 0.61, 0.43} +} +-- Shamans used the Paladin pink color in vanilla +local vanillaClassColors = { + ["SHAMAN"] = {0.96, 0.55, 0.73} +} +local unknownClassColor = {0.7, 0.7, 0.7} +-- Exact palette from the supplied ModernClassColors reference. Keep this local +-- so choosing a health bar texture does not change names or other addons. +local modernClassColors = { + ["DRUID"] = {1, 0.49, 0.04}, + ["HUNTER"] = {0.67, 0.83, 0.45}, + ["MAGE"] = {0.25, 0.78, 0.92}, + ["PALADIN"] = {0.96, 0.55, 0.73}, + ["PRIEST"] = {1, 1, 1}, + ["ROGUE"] = {1, 0.96, 0.41}, + ["SHAMAN"] = {0, 0.44, 0.87}, + ["WARLOCK"] = {0.53, 0.53, 0.93}, + ["WARRIOR"] = {0.78, 0.61, 0.43} +} +function GetClassColor(class, asArray, modern) + local color + if modern then + color = modernClassColors[class] + elseif PTOptions and PTOptions.VanillaClassColors and vanillaClassColors[class] then + color = vanillaClassColors[class] + else + color = classColors[class] + end + if not color then -- Unknown class + color = unknownClassColor + end + if asArray then + return color + end + return color[1], color[2], color[3] +end + +-- Returns an array of spells starting with the string, ordered from highest rank to lowest. +-- Limit is 20 if nil, non-ranks not included. Adds non-ranked name unless specified otherwise. +function SearchSpells(startStr, limit, noNonRank) + startStr = string.upper(startStr) + limit = limit or 20 + local matchingSpells = {} + local id = 1 + for i = 1, GetNumSpellTabs() do + local breakOut + local tabName, _, _, numSpells = GetSpellTabInfo(i) + if tabName == "ZMounts" or tabName == "ZzCompanions" then -- No 1.18.1 client "spell" tabs + id = id + numSpells + else + for j = 1, numSpells do + local spellName, rank, realID = GetSpellName(id, "spell") + if not IsSpellPassive(id, "spell") then + local fullName = spellName + if rank ~= "" then + fullName = fullName.."("..rank..")" + end + if StartsWith(string.upper(fullName), startStr) then + table.insert(matchingSpells, fullName) + if getn(matchingSpells) >= limit then + breakOut = true + break + end + end + end + id = id + 1 + end + end + if breakOut then + break + end + end + + ReverseArray(matchingSpells) + + if not noNonRank then + local alreadyFound = compost:GetTable() + local toInsert = compost:GetTable() + for i = 1, getn(matchingSpells) do + local rank = ExtractSpellRank(matchingSpells[i]) + -- Don't add non rank if the user is explicitly typing out the rank already + if (string.len(matchingSpells[i]) - string.len(rank or "") - 2) < (string.len(startStr)) then + break + end + if rank then + local baseSpell = string.sub(matchingSpells[i], 1, string.len(matchingSpells[i]) - string.len(rank) - 2) + if not alreadyFound[baseSpell] then + alreadyFound[baseSpell] = true + table.insert(toInsert, compost:Acquire(i, baseSpell)) + end + end + end + local offset = 0 + for _, insertion in ipairs(toInsert) do + table.insert(matchingSpells, insertion[1] + offset, insertion[2]) + offset = offset + 1 + end + compost:Reclaim(alreadyFound) + compost:Reclaim(toInsert, 1) + end + + return matchingSpells +end + +function SearchMacros(startStr, limit) + startStr = string.upper(startStr) + limit = limit or 20 + local matchingMacros = {} + for i = 1, GetNumMacros() do + local name = GetMacroInfo(i) + if StartsWith(string.upper(name), startStr) then + table.insert(matchingMacros, name) + if getn(matchingMacros) >= limit then + break + end + end + end + return matchingMacros +end + +function SearchItems(startStr, limit) + startStr = string.upper(startStr) + limit = limit or 20 + local alreadyFound = compost:GetTable() + local matchingItems = {} + for bag = 0, NUM_BAG_FRAMES do + for slot = 1, GetContainerNumSlots(bag) do + local name = GetBagSlotInfo(bag, slot) + if name and not alreadyFound[name] and StartsWith(string.upper(name), startStr) then + table.insert(matchingItems, name) + alreadyFound[name] = true + if getn(matchingItems) >= limit then + break + end + end + end + end + compost:Reclaim(alreadyFound) + return matchingItems +end + +-- Checks for feign death as well +function IsDeadFriend(unit) + return (UnitIsDead(unit) or UnitIsCorpse(unit)) and UnitIsFriend("player", unit) and not IsFeigning(unit) +end + +local keyModifiers = {"None", "Shift", "Control", "Alt", "Shift+Control", "Shift+Alt", "Control+Alt", "Shift+Control+Alt"} +function GetKeyModifiers() + return keyModifiers +end + +-- L1: Shift +-- L2: Control +-- L3: Alt +KeyModifierMap = {} +do + -- That moment when trying to be more concise goes terribly wrong + local keys = {{"Shift", "S", {0.4, 1, 0.4}}, {"Control", "C", {0.4, 0.4, 1}}, {"Alt", "A", {1, 0.4, 0.4}}} + local states = {true, false} + for _, l1State in ipairs(states) do + local l1 = {} + KeyModifierMap[l1State] = l1 + for _, l2State in ipairs(states) do + local l2 = {} + l1[l2State] = l2 + for _, l3State in ipairs(states) do + local keyStr = "" + local keyStrColored = "" + local keyAbbStr = "" + local keyAbbStrColored = "" + for i = 1, 3 do + if (i == 1 and l1State) or (i == 2 and l2State) or (i == 3 and l3State) then + local key = keys[i] + if keyStr ~= "" then + keyStr = keyStr.."+" + keyStrColored = keyStrColored.."+" + keyAbbStr = keyAbbStr.."+" + keyAbbStrColored = keyAbbStrColored.."+" + end + keyStr = keyStr..key[1] + keyStrColored = keyStrColored..Colorize(key[1], key[3]) + keyAbbStr = keyAbbStr..key[2] + keyAbbStrColored = keyAbbStrColored..Colorize(key[2], key[3]) + end + end + + if keyStr == "" then + keyStr = "None" + keyStrColored = "None" + keyAbbStr = "None" + keyAbbStrColored = "None" + end + + l2[l3State] = {keyStr, keyStrColored, keyAbbStr, keyAbbStrColored} + end + end + end +end +function GetKeyModifier() + return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][1] +end + +function GetColoredKeyModifier() + return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][2] +end + +function GetAbbreviatedKeyModifier() + return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][3] +end + +function GetColoredAbbreviatedKeyModifier() + return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][4] +end + +function GetKeyModifierTypeByID(id) + return KeyModifierMap[IsShiftKeyDown() == 1][IsControlKeyDown() == 1][IsAltKeyDown() == 1][id] +end + +local nameOverrides = { + ["LeftButton"] = "Left Button", + ["RightButton"] = "Right Button", + ["MiddleButton"] = "Middle Button" +} +function GetButtonName(rawButton) + return nameOverrides[rawButton] or GetBindingText(rawButton, "KEY_") +end + +local buttons = {"LeftButton", "MiddleButton", "RightButton", "Button4", "Button5"} +function GetAllButtons() + return buttons +end + +local buttonsSet = ToSet(buttons, true) +function GetAllButtonsSet() + return buttonsSet +end + +local upButtons = {} +for _, button in ipairs(buttons) do + table.insert(upButtons, button.."Up") +end +function GetUpButtons() + return upButtons +end + +local downButtons = {} +for _, button in ipairs(buttons) do + table.insert(downButtons, button.."Down") +end +function GetDownButtons() + return downButtons +end + +function GetCenterScreenPoint(componentWidth, componentHeight) + return "TOPLEFT", (GetScreenWidth() / 2) - (componentWidth / 2), -((GetScreenHeight() / 2) - (componentHeight / 2)) +end + +-- Keeps the frame at the current position, while modifying the anchor point +function ConvertAnchor(frame, anchor) + local leftX, rightX, topY, bottomY = frame:GetLeft(), frame:GetRight(), frame:GetTop(), frame:GetBottom() + local centerX, centerY = frame:GetCenter() + local x, y + if anchor == "TOPLEFT" then + x, y = leftX, topY + elseif anchor == "TOPRIGHT" then + x, y = rightX, topY + elseif anchor == "BOTTOMLEFT" then + x, y = leftX, bottomY + elseif anchor == "BOTTOMRIGHT" then + x, y = rightX, bottomY + elseif anchor == "TOP" then + x, y = centerX, topY + elseif anchor == "BOTTOM" then + x, y = centerX, bottomY + elseif anchor == "LEFT" then + x, y = leftX, centerY + elseif anchor == "RIGHT" then + x, y = rightX, centerY + elseif anchor == "CENTER" then + x, y = centerX, centerY + end + frame:ClearAllPoints() + frame:SetPoint(anchor, UIParent, "TOPLEFT", x, y - GetScreenHeight()) +end + +function GetPowerType(unit) + return PowerTypeMap[UnitPowerType(unit)] +end + +function GetPowerColor(unit) + return PowerColors[GetPowerType(unit)] +end + +-- You never really know these days +function IsReallyInInstance() + return IsInInstance() and not InstanceWorldZones[GetRealZoneText()] +end + +-- Returns distance if UnitXP SP3 or SuperWoW is present; +-- 0 if unit is offline, or unit is enemy and SuperWoW is the distance provider; +-- 9999 if unit is not visible or UnitXP SP3 is not present. +-- Might try to do hacky stuff for people without mods later on. +function GetDistanceTo(unit) + return GetDistanceBetween("player", unit) +end + +function GetDistanceBetween_SuperWow(unit1, unit2) + if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then + return 0 + end + + if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then + return 9999 + end + + local x1, z1, y1 = UnitPosition(unit1) + local x2, z2, y2 = UnitPosition(unit2) + + if not x1 or not x2 then + return 0 + end + local dx = x2 - x1 + local dz = z2 - z1 + local dy = y2 - y1 + return math.sqrt(dx*dx + dz*dz + dy*dy) +end + +function GetDistanceBetween_UnitXPSP3_Legacy(unit1, unit2) + if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then + return 0 + end + + if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then + return 9999 + end + + return math.max((UnitXP("distanceBetween", unit1, unit2) or (9999 + 3)) - 3, 0) -- UnitXP SP3 modded function +end + +function GetDistanceBetween_UnitXPSP3(unit1, unit2) + if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then + return 0 + end + + if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then + return 9999 + end + + return math.max(UnitXP("distanceBetween", unit1, unit2) or 9999, 0) -- UnitXP SP3 modded function +end + +function GetDistanceBetween_Vanilla(unit1, unit2) + if not UnitIsConnected(unit1) or not UnitIsConnected(unit2) then + return 0 + end + + if not UnitIsVisible(unit1) or not UnitIsVisible(unit2) then + return 9999 + end + + if unit1 == "player" then + if CheckInteractDistance(unit2, 3) then + return 9 + end + if CheckInteractDistance(unit2, 4) then + return 27 + end + end + + return 28 +end + +if UnitXPSP3 then + if UnitXPSP3_Version > -1 then -- Newer versions have more accurate distances + GetDistanceBetween = GetDistanceBetween_UnitXPSP3 + else -- Fall back to old distance calculation + GetDistanceBetween = GetDistanceBetween_UnitXPSP3_Legacy + end +elseif SuperWoW then + GetDistanceBetween = GetDistanceBetween_SuperWow +else -- sad + GetDistanceBetween = GetDistanceBetween_Vanilla +end + +-- SuperWoW cannot provide precise distance for enemies +function CanClientGetPreciseDistance(alsoEnemies) + return UnitXPSP3 or (SuperWoW and not alsoEnemies) +end + +function GetUnitPosition(unit) + return nil +end + +if VanillaUtils then + GetUnitPosition = function(unit) + return UnitXP("unitPosition", unit) + end +elseif SuperWoW then + GetUnitPosition = UnitPosition +end + +-- Returns whether unit is in sight if UnitXP SP3 is present, otherwise always true. +IsInSight = function() + return true +end + +do -- This is done to prevent crashes from checking sight too early + local sightEnableFrame = CreateFrame("Frame") + sightEnableFrame:RegisterEvent("ADDON_LOADED") + sightEnableFrame:SetScript("OnEvent", function() + if arg1 == "Puppeteer" then + if UnitXPSP3 then + IsInSight = function(unit) + return UnitXP("inSight", "player", unit) -- UnitXP SP3 modded function + end + end + sightEnableFrame:SetScript("OnEvent", nil) + end + end) +end + +function CanClientSightCheck() + return UnitXPSP3 +end + +-- From pfQuest +local minimaparrow = ({Minimap:GetChildren()})[9] +for k, v in pairs({Minimap:GetChildren()}) do + if v:IsObjectType("Model") and not v:GetName() then + if string.find(strlower(v:GetModel()), "interface\\minimap\\minimaparrow") then + minimaparrow = v + break + end + end +end +function GetPlayerFacing() + return minimaparrow:GetFacing() +end + +function GetUnitDirection(from, to) + local sx, sz = GetUnitPosition(from) + local tx, tz = GetUnitPosition(to) + + local dx = tx - sx + local dz = tz - sz + + return math.atan2(dz, dx) +end + +function GetFacingAngle(unit) + local angle = GetUnitDirection("player", unit) - GetPlayerFacing() + angle = modulo(angle, math.pi * 2) + return angle +end + +if VanillaUtils then + function GetCameraFacing() + local sx, sy = GetUnitPosition("player") + local cx, cy = UnitXP("cameraPosition") + return math.atan2(cy - sy, cx - sx) + math.pi + end +else + GetCameraFacing = GetPlayerFacing -- Sad +end + +function GetCameraFacingAngle(unit) + local cameraAngle = GetUnitDirection("player", unit) - GetCameraFacing() + cameraAngle = modulo(cameraAngle, math.pi * 2) + return cameraAngle +end + + +function CanClientGetAuraIDs() + return SuperWoW-- or Client1181 -- 1.18.1 client ID fetching is not reliable +end + +function IsClient1181() + return Client1181 +end + +AllUnitsSet = ToSet(AllUnits) +FocusUnitsSet = ToSet(FocusUnits) +RunLater(function() + PTLocale.Values(ResurrectionSpells) + PTLocale.Keys(ResurrectionSpellsSet) +end) + +InstanceWorldZones = ToSet({"Winter Veil Vale"}) -- Why is this an instance?? \ No newline at end of file diff --git a/libs/ace/AceAddon-2.0/AceAddon-2.0.lua b/libs/ace/AceAddon-2.0/AceAddon-2.0.lua new file mode 100644 index 0000000..94846b6 --- /dev/null +++ b/libs/ace/AceAddon-2.0/AceAddon-2.0.lua @@ -0,0 +1,1001 @@ +--[[ +Name: AceAddon-2.0 +Revision: $Rev: 17644 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Ace 1.x by Turan (turan@gryphon.com) +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceAddon-2.0 +SVN: http://svn.wowace.com/root/trunk/Ace2/AceAddon-2.0 +Description: Base for all Ace addons to inherit from. +Dependencies: AceLibrary, AceOO-2.0, AceEvent-2.0, (optional) AceConsole-2.0 +]] + +local MAJOR_VERSION = "AceAddon-2.0" +local MINOR_VERSION = "$Revision: 17644 $" + +-- This ensures the code is only executed if the libary doesn't already exist, or is a newer version +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0.") end + +local function safecall(func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + local success, err = pcall(func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) + if not success then geterrorhandler()(err) end +end + +-- Localization +local STANDBY, TITLE, NOTES, VERSION, AUTHOR, DATE, CATEGORY, EMAIL, CREDITS, WEBSITE, CATEGORIES, ABOUT, PRINT_ADDON_INFO +if GetLocale() == "deDE" then + STANDBY = "|cffff5050(Standby)|r" -- capitalized + + TITLE = "Titel" + NOTES = "Anmerkung" + VERSION = "Version" + AUTHOR = "Autor" + DATE = "Datum" + CATEGORY = "Kategorie" + EMAIL = "E-mail" + WEBSITE = "Webseite" + CREDITS = "Credits" -- fix + + ABOUT = "\195\188ber" + PRINT_ADDON_INFO = "Gibt Addondaten aus" + + CATEGORIES = { + ["Action Bars"] = "Aktionsleisten", + ["Auction"] = "Auktion", + ["Audio"] = "Audio", + ["Battlegrounds/PvP"] = "Schlachtfeld/PvP", + ["Buffs"] = "Buffs", + ["Chat/Communication"] = "Chat/Kommunikation", + ["Druid"] = "Druide", + ["Hunter"] = "Jäger", + ["Mage"] = "Magier", + ["Paladin"] = "Paladin", + ["Priest"] = "Priester", + ["Rogue"] = "Schurke", + ["Shaman"] = "Schamane", + ["Warlock"] = "Hexenmeister", + ["Warrior"] = "Krieger", + ["Healer"] = "Heiler", + ["Tank"] = "Tank", -- noone use "Brecher"... + ["Caster"] = "Caster", + ["Combat"] = "Kampf", + ["Compilations"] = "Compilations", -- whats that o_O + ["Data Export"] = "Datenexport", + ["Development Tools"] = "Entwicklungs Tools", + ["Guild"] = "Gilde", + ["Frame Modification"] = "Frame Modifikation", + ["Interface Enhancements"] = "Interface Verbesserungen", + ["Inventory"] = "Inventar", + ["Library"] = "Library", + ["Map"] = "Map", + ["Mail"] = "Mail", + ["Miscellaneous"] = "Diverses", + ["Quest"] = "Quest", + ["Raid"] = "Schlachtzug", + ["Tradeskill"] = "Handelsf\195\164higkeit", + ["UnitFrame"] = "UnitFrame", + } +elseif GetLocale() == "frFR" then + STANDBY = "|cffff5050(attente)|r" + + TITLE = "Titre" + NOTES = "Notes" + VERSION = "Version" + AUTHOR = "Auteur" + DATE = "Date" + CATEGORY = "Cat\195\169gorie" + EMAIL = "E-mail" + WEBSITE = "Site web" + CREDITS = "Credits" -- fix + + ABOUT = "A propos" + PRINT_ADDON_INFO = "Afficher les informations sur l'addon" + + CATEGORIES = { + ["Action Bars"] = "Barres d'action", + ["Auction"] = "H\195\180tel des ventes", + ["Audio"] = "Audio", + ["Battlegrounds/PvP"] = "Champs de bataille/JcJ", + ["Buffs"] = "Buffs", + ["Chat/Communication"] = "Chat/Communication", + ["Druid"] = "Druide", + ["Hunter"] = "Chasseur", + ["Mage"] = "Mage", + ["Paladin"] = "Paladin", + ["Priest"] = "Pr\195\170tre", + ["Rogue"] = "Voleur", + ["Shaman"] = "Chaman", + ["Warlock"] = "D\195\169moniste", + ["Warrior"] = "Guerrier", + ["Healer"] = "Soigneur", + ["Tank"] = "Tank", + ["Caster"] = "Casteur", + ["Combat"] = "Combat", + ["Compilations"] = "Compilations", + ["Data Export"] = "Exportation de donn\195\169es", + ["Development Tools"] = "Outils de d\195\169veloppement", + ["Guild"] = "Guilde", + ["Frame Modification"] = "Modification des fen\195\170tres", + ["Interface Enhancements"] = "Am\195\169liorations de l'interface", + ["Inventory"] = "Inventaire", + ["Library"] = "Biblioth\195\168ques", + ["Map"] = "Carte", + ["Mail"] = "Courrier", + ["Miscellaneous"] = "Divers", + ["Quest"] = "Qu\195\170tes", + ["Raid"] = "Raid", + ["Tradeskill"] = "M\195\169tiers", + ["UnitFrame"] = "Fen\195\170tres d'unit\195\169", + } +elseif GetLocale() == "koKR" then + STANDBY = "|cffff5050(사용가능)|r" + + TITLE = "제목" + NOTES = "노트" + VERSION = "버전" + AUTHOR = "저작자" + DATE = "날짜" + CATEGORY = "분류" + EMAIL = "E-mail" + WEBSITE = "웹사이트" + CREDITS = "Credits" -- fix + + ABOUT = "정보" + PRINT_ADDON_INFO = "애드온 정보 출력" + + CATEGORIES = { + ["Action Bars"] = "액션바", + ["Auction"] = "경매", + ["Audio"] = "음향", + ["Battlegrounds/PvP"] = "전장/PvP", + ["Buffs"] = "버프", + ["Chat/Communication"] = "대화/의사소통", + ["Druid"] = "드루이드", + ["Hunter"] = "사냥꾼", + ["Mage"] = "마법사", + ["Paladin"] = "성기사", + ["Priest"] = "사제", + ["Rogue"] = "도적", + ["Shaman"] = "주술사", + ["Warlock"] = "흑마법사", + ["Warrior"] = "전사", + ["Healer"] = "힐러", + ["Tank"] = "탱커", + ["Caster"] = "캐스터", + ["Combat"] = "전투", + ["Compilations"] = "복합", + ["Data Export"] = "자료 출력", + ["Development Tools"] = "개발 도구", + ["Guild"] = "길드", + ["Frame Modification"] = "구조 변경", + ["Interface Enhancements"] = "인터페이스 강화", + ["Inventory"] = "인벤토리", + ["Library"] = "라이브러리", + ["Map"] = "지도", + ["Mail"] = "우편", + ["Miscellaneous"] = "기타", + ["Quest"] = "퀘스트", + ["Raid"] = "공격대", + ["Tradeskill"] = "전문기술", + ["UnitFrame"] = "유닛 프레임", + } +elseif GetLocale() == "zhTW" then + STANDBY = "|cffff5050(待命)|r" + + TITLE = "標題" + NOTES = "註記" + VERSION = "版本" + AUTHOR = "作者" + DATE = "日期" + CATEGORY = "類別" + EMAIL = "E-mail" + WEBSITE = "網站" + CREDITS = "Credits" -- fix + + ABOUT = "關於" + PRINT_ADDON_INFO = "顯示插件資訊" + + CATEGORIES = { + ["Action Bars"] = "動作列", + ["Auction"] = "拍賣", + ["Audio"] = "音樂", + ["Battlegrounds/PvP"] = "戰場/PvP", + ["Buffs"] = "增益", + ["Chat/Communication"] = "聊天/通訊", + ["Druid"] = "德魯伊", + ["Hunter"] = "獵人", + ["Mage"] = "法師", + ["Paladin"] = "聖騎士", + ["Priest"] = "牧師", + ["Rogue"] = "盜賊", + ["Shaman"] = "薩滿", + ["Warlock"] = "術士", + ["Warrior"] = "戰士", + ["Healer"] = "治療者", + ["Tank"] = "坦克", + ["Caster"] = "施法者", + ["Combat"] = "戰鬥", + ["Compilations"] = "編輯", + ["Data Export"] = "資料匯出", + ["Development Tools"] = "開發工具", + ["Guild"] = "公會", + ["Frame Modification"] = "框架修改", + ["Interface Enhancements"] = "介面增強", + ["Inventory"] = "背包", + ["Library"] = "資料庫", + ["Map"] = "地圖", + ["Mail"] = "郵件", + ["Miscellaneous"] = "綜合", + ["Quest"] = "任務", + ["Raid"] = "團隊", + ["Tradeskill"] = "商業技能", + ["UnitFrame"] = "單位框架", + } +elseif GetLocale() == "zhCN" then + STANDBY = "|cffff5050(\230\154\130\230\140\130)|r" + + TITLE = "\230\160\135\233\162\152" + NOTES = "\233\153\132\230\179\168" + VERSION = "\231\137\136\230\156\172" + AUTHOR = "\228\189\156\232\128\133" + DATE = "\230\151\165\230\156\159" + CATEGORY = "\229\136\134\231\177\187" + EMAIL = "\231\148\181\229\173\144\233\130\174\228\187\182" + WEBSITE = "\231\189\145\231\171\153" + CREDITS = "Credits" -- fix + + ABOUT = "\229\133\179\228\186\142" + PRINT_ADDON_INFO = "\229\141\176\229\136\151\229\135\186\230\143\146\228\187\182\228\191\161\230\129\175" + + CATEGORIES = { + ["Action Bars"] = "\229\138\168\228\189\156\230\157\161", + ["Auction"] = "\230\139\141\229\141\150", + ["Audio"] = "\233\159\179\233\162\145", + ["Battlegrounds/PvP"] = "\230\136\152\229\156\186/PvP", + ["Buffs"] = "\229\162\158\231\155\138\233\173\148\230\179\149", + ["Chat/Communication"] = "\232\129\138\229\164\169/\228\186\164\230\181\129", + ["Druid"] = "\229\190\183\233\178\129\228\188\138", + ["Hunter"] = "\231\140\142\228\186\186", + ["Mage"] = "\230\179\149\229\184\136", + ["Paladin"] = "\229\156\163\233\170\145\229\163\171", + ["Priest"] = "\231\137\167\229\184\136", + ["Rogue"] = "\231\155\151\232\180\188", + ["Shaman"] = "\232\144\168\230\187\161\231\165\173\229\143\184", + ["Warlock"] = "\230\156\175\229\163\171", + ["Warrior"] = "\230\136\152\229\163\171", +-- ["Healer"] = "\230\178\187\231\150\151\228\191\157\233\154\156", +-- ["Tank"] = "\232\191\145\230\136\152\230\142\167\229\136\182", +-- ["Caster"] = "\232\191\156\231\168\139\232\190\147\229\135\186", + ["Combat"] = "\230\136\152\230\150\151", + ["Compilations"] = "\231\188\150\232\175\145", + ["Data Export"] = "\230\149\176\230\141\174\229\175\188\229\135\186", + ["Development Tools"] = "\229\188\128\229\143\145\229\183\165\229\133\183", + ["Guild"] = "\229\133\172\228\188\154", + ["Frame Modification"] = "\230\161\134\230\158\182\228\191\174\230\148\185", + ["Interface Enhancements"] = "\231\149\140\233\157\162\229\162\158\229\188\186", + ["Inventory"] = "\232\131\140\229\140\133", + ["Library"] = "\229\186\147", + ["Map"] = "\229\156\176\229\155\190", + ["Mail"] = "\233\130\174\228\187\182", + ["Miscellaneous"] = "\230\157\130\233\161\185", + ["Quest"] = "\228\187\187\229\138\161", + ["Raid"] = "\229\155\162\233\152\159", + ["Tradeskill"] = "\229\149\134\228\184\154\230\138\128\232\131\189", + ["UnitFrame"] = "\229\164\180\229\131\143\230\161\134\230\158\182", + } +else -- enUS + STANDBY = "|cffff5050(standby)|r" + + TITLE = "Title" + NOTES = "Notes" + VERSION = "Version" + AUTHOR = "Author" + DATE = "Date" + CATEGORY = "Category" + EMAIL = "E-mail" + WEBSITE = "Website" + CREDITS = "Credits" + + ABOUT = "About" + PRINT_ADDON_INFO = "Print out addon info" + + CATEGORIES = { + ["Action Bars"] = "Action Bars", + ["Auction"] = "Auction", + ["Audio"] = "Audio", + ["Battlegrounds/PvP"] = "Battlegrounds/PvP", + ["Buffs"] = "Buffs", + ["Chat/Communication"] = "Chat/Communication", + ["Druid"] = "Druid", + ["Hunter"] = "Hunter", + ["Mage"] = "Mage", + ["Paladin"] = "Paladin", + ["Priest"] = "Priest", + ["Rogue"] = "Rogue", + ["Shaman"] = "Shaman", + ["Warlock"] = "Warlock", + ["Warrior"] = "Warrior", + ["Healer"] = "Healer", + ["Tank"] = "Tank", + ["Caster"] = "Caster", + ["Combat"] = "Combat", + ["Compilations"] = "Compilations", + ["Data Export"] = "Data Export", + ["Development Tools"] = "Development Tools", + ["Guild"] = "Guild", + ["Frame Modification"] = "Frame Modification", + ["Interface Enhancements"] = "Interface Enhancements", + ["Inventory"] = "Inventory", + ["Library"] = "Library", + ["Map"] = "Map", + ["Mail"] = "Mail", + ["Miscellaneous"] = "Miscellaneous", + ["Quest"] = "Quest", + ["Raid"] = "Raid", + ["Tradeskill"] = "Tradeskill", + ["UnitFrame"] = "UnitFrame", + } +end + +setmetatable(CATEGORIES, { __index = function(self, key) -- case-insensitive + local lowerKey = string.lower(key) + for k,v in pairs(CATEGORIES) do + if string.lower(k) == lowerKey then + return v + end + end +end }) + +-- Create the library object + +local AceOO = AceLibrary("AceOO-2.0") +local AceAddon = AceOO.Class() +local AceEvent +local AceConsole +local AceModuleCore + +function AceAddon:ToString() + return "AceAddon" +end + +local function print(text) + DEFAULT_CHAT_FRAME:AddMessage(text) +end + +function AceAddon:ADDON_LOADED(name) + while table.getn(self.nextAddon) > 0 do + local addon = table.remove(self.nextAddon, 1) + table.insert(self.addons, addon) + if not self.addons[name] then + self.addons[name] = addon + end + self:InitializeAddon(addon, name) + end +end + +local function RegisterOnEnable(self) + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.defaultLanguage then -- HACK + AceAddon.playerLoginFired = true + end + if AceAddon.playerLoginFired then + AceAddon.addonsStarted[self] = true + if (type(self.IsActive) ~= "function" or self:IsActive()) and (not AceModuleCore or not AceModuleCore:IsModule(self) or AceModuleCore:IsModuleActive(self)) then + local current = self.class + while true do + if current == AceOO.Class then + break + end + if current.mixins then + for mixin in pairs(current.mixins) do + if type(mixin.OnEmbedEnable) == "function" then + safecall(mixin.OnEmbedEnable,mixin,self) + end + end + end + current = current.super + end + if type(self.OnEnable) == "function" then + safecall(self.OnEnable,self) + end + end + else + if not AceAddon.addonsToOnEnable then + AceAddon.addonsToOnEnable = {} + end + table.insert(AceAddon.addonsToOnEnable, self) + end +end + +local function stripSpaces(text) + if type(text) == "string" then + return (string.gsub(string.gsub(text, "^%s*(.-)%s*$", "%1"), "%s%s+", " ")) + end + return text +end + +function AceAddon:InitializeAddon(addon, name) + if addon.name == nil then + addon.name = name + end + if GetAddOnMetadata then + -- TOC checks + if addon.title == nil then + addon.title = GetAddOnMetadata(name, "Title") + if addon.title then + local num = string.find(addon.title, " |cff7fff7f %-Ace2%-|r$") + if num then + addon.title = string.sub(addon.title, 1, num - 1) + end + addon.title = stripSpaces(addon.title) + end + end + if addon.notes == nil then + addon.notes = GetAddOnMetadata(name, "Notes") + addon.notes = stripSpaces(addon.notes) + end + if addon.version == nil then + addon.version = GetAddOnMetadata(name, "Version") + if addon.version then + if string.find(addon.version, "%$Revision: (%d+) %$") then + addon.version = string.gsub(addon.version, "%$Revision: (%d+) %$", "%1") + elseif string.find(addon.version, "%$Rev: (%d+) %$") then + addon.version = string.gsub(addon.version, "%$Rev: (%d+) %$", "%1") + elseif string.find(addon.version, "%$LastChangedRevision: (%d+) %$") then + addon.version = string.gsub(addon.version, "%$LastChangedRevision: (%d+) %$", "%1") + end + end + addon.version = stripSpaces(addon.version) + end + if addon.author == nil then + addon.author = GetAddOnMetadata(name, "Author") + addon.author = stripSpaces(addon.author) + end + if addon.credits == nil then + addon.credits = GetAddOnMetadata(name, "X-Credits") + addon.credits = stripSpaces(addon.credits) + end + if addon.date == nil then + addon.date = GetAddOnMetadata(name, "X-Date") or GetAddOnMetadata(name, "X-ReleaseDate") + if addon.date then + if string.find(addon.date, "%$Date: (.-) %$") then + addon.date = string.gsub(addon.date, "%$Date: (.-) %$", "%1") + elseif string.find(addon.date, "%$LastChangedDate: (.-) %$") then + addon.date = string.gsub(addon.date, "%$LastChangedDate: (.-) %$", "%1") + end + end + addon.date = stripSpaces(addon.date) + end + if addon.category == nil then + addon.category = GetAddOnMetadata(name, "X-Category") + addon.category = stripSpaces(addon.category) + end + if addon.email == nil then + addon.email = GetAddOnMetadata(name, "X-eMail") or GetAddOnMetadata(name, "X-Email") + addon.email = stripSpaces(addon.email) + end + if addon.website == nil then + addon.website = GetAddOnMetadata(name, "X-Website") + addon.website = stripSpaces(addon.website) + end + end + local current = addon.class + while true do + if current == AceOO.Class then + break + end + if current.mixins then + for mixin in pairs(current.mixins) do + if type(mixin.OnEmbedInitialize) == "function" then + mixin:OnEmbedInitialize(addon, name) + end + end + end + current = current.super + end + if type(addon.OnInitialize) == "function" then + safecall(addon.OnInitialize, addon, name) + end + RegisterOnEnable(addon) +end + +function AceAddon.prototype:PrintAddonInfo() + local x + if self.title then + x = "|cffffff7f" .. tostring(self.title) .. "|r" + elseif self.name then + x = "|cffffff7f" .. tostring(self.name) .. "|r" + else + x = "|cffffff7f<" .. tostring(self.class) .. " instance>|r" + end + if type(self.IsActive) == "function" then + if not self:IsActive() then + x = x .. " " .. STANDBY + end + end + if self.version then + x = x .. " - |cffffff7f" .. tostring(self.version) .. "|r" + end + if self.notes then + x = x .. " - " .. tostring(self.notes) + end + print(x) + if self.author then + print(" - |cffffff7f" .. AUTHOR .. ":|r " .. tostring(self.author)) + end + if self.credits then + print(" - |cffffff7f" .. CREDITS .. ":|r " .. tostring(self.credits)) + end + if self.date then + print(" - |cffffff7f" .. DATE .. ":|r " .. tostring(self.date)) + end + if self.category then + local category = CATEGORIES[self.category] + if category then + print(" - |cffffff7f" .. CATEGORY .. ":|r " .. category) + end + end + if self.email then + print(" - |cffffff7f" .. EMAIL .. ":|r " .. tostring(self.email)) + end + if self.website then + print(" - |cffffff7f" .. WEBSITE .. ":|r " .. tostring(self.website)) + end +end + +local options +function AceAddon:GetAceOptionsDataTable(target) + if not options then + options = { + about = { + name = ABOUT, + desc = PRINT_ADDON_INFO, + type = "execute", + func = "PrintAddonInfo", + order = -1, + } + } + end + return options +end + +function AceAddon:PLAYER_LOGIN() + self.playerLoginFired = true + if self.addonsToOnEnable then + while table.getn(self.addonsToOnEnable) > 0 do + local addon = table.remove(self.addonsToOnEnable, 1) + self.addonsStarted[addon] = true + if (type(addon.IsActive) ~= "function" or addon:IsActive()) and (not AceModuleCore or not AceModuleCore:IsModule(addon) or AceModuleCore:IsModuleActive(addon)) then + local current = addon.class + while true do + if current == AceOO.Class then + break + end + if current.mixins then + for mixin in pairs(current.mixins) do + if type(mixin.OnEmbedEnable) == "function" then + safecall(mixin.OnEmbedEnable,mixin,addon) + end + end + end + current = current.super + end + if type(addon.OnEnable) == "function" then + safecall(addon.OnEnable,addon) + end + end + end + self.addonsToOnEnable = nil + end +end + +function AceAddon.prototype:Inject(t) + AceAddon:argCheck(t, 2, "table") + for k,v in pairs(t) do + self[k] = v + end +end + +function AceAddon.prototype:init() + if not AceEvent then + error(MAJOR_VERSION .. " requires AceEvent-2.0", 4) + end + AceAddon.super.prototype.init(self) + + self.super = self.class.prototype + + AceAddon:RegisterEvent("ADDON_LOADED", "ADDON_LOADED", true) + table.insert(AceAddon.nextAddon, self) +end + +function AceAddon.prototype:ToString() + local x + if type(self.title) == "string" then + x = self.title + elseif type(self.name) == "string" then + x = self.name + else + x = "<" .. tostring(self.class) .. " instance>" + end + if (type(self.IsActive) == "function" and not self:IsActive()) or (AceModuleCore and AceModuleCore:IsModule(addon) and AceModuleCore:IsModuleActive(addon)) then + x = x .. " " .. STANDBY + end + return x +end + +AceAddon.new = function(self, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20) + local class = AceAddon:pcall(AceOO.Classpool, self, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20) + return class:new() +end + +local function external(self, major, instance) + if major == "AceEvent-2.0" then + AceEvent = instance + + AceEvent:embed(self) + + self:RegisterEvent("PLAYER_LOGIN", "PLAYER_LOGIN", true) + elseif major == "AceConsole-2.0" then + AceConsole = instance + + local slashCommands = { "/ace2" } + local _,_,_,enabled,loadable = GetAddOnInfo("Ace") + if not enabled or not loadable then + table.insert(slashCommands, "/ace") + end + local function listAddon(addon, depth) + if not depth then + depth = 0 + end + + local s = string.rep(" ", depth) .. " - " .. tostring(addon) + if rawget(addon, 'version') then + s = s .. " - |cffffff7f" .. tostring(addon.version) .. "|r" + end + if rawget(addon, 'slashCommand') then + s = s .. " |cffffff7f(" .. tostring(addon.slashCommand) .. ")|r" + end + print(s) + if type(rawget(addon, 'modules')) == "table" then + local i = 0 + for k,v in pairs(addon.modules) do + i = i + 1 + if i == 6 then + print(string.rep(" ", depth + 1) .. " - more...") + break + else + listAddon(v, depth + 1) + end + end + end + end + local function listNormalAddon(i) + local name,_,_,enabled,loadable = GetAddOnInfo(i) + if not loadable then + enabled = false + end + if self.addons[name] then + local addon = self.addons[name] + if not AceCoreAddon or not AceCoreAddon:IsModule(addon) then + listAddon(addon) + end + else + local s = " - " .. tostring(GetAddOnMetadata(i, "Title") or name) + local version = GetAddOnMetadata(i, "Version") + if version then + if string.find(version, "%$Revision: (%d+) %$") then + version = string.gsub(version, "%$Revision: (%d+) %$", "%1") + elseif string.find(version, "%$Rev: (%d+) %$") then + version = string.gsub(version, "%$Rev: (%d+) %$", "%1") + elseif string.find(version, "%$LastChangedRevision: (%d+) %$") then + version = string.gsub(version, "%$LastChangedRevision: (%d+) %$", "%1") + end + s = s .. " - |cffffff7f" .. version .. "|r" + end + if not enabled then + s = s .. " |cffff0000(disabled)|r" + end + if IsAddOnLoadOnDemand(i) then + s = s .. " |cff00ff00[LoD]|r" + end + print(s) + end + end + local function mySort(alpha, bravo) + return tostring(alpha) < tostring(bravo) + end + AceConsole.RegisterChatCommand(self, slashCommands, { + desc = "AddOn development framework", + name = "Ace2", + type = "group", + args = { + about = { + desc = "Get information about Ace2", + name = "About", + type = "execute", + func = function() + print("|cffffff7fAce2|r - |cffffff7f2.0." .. string.gsub(MINOR_VERSION, "%$Revision: (%d+) %$", "%1") .. "|r - AddOn development framework") + print(" - |cffffff7f" .. AUTHOR .. ":|r Ace Development Team") + print(" - |cffffff7f" .. WEBSITE .. ":|r http://www.wowace.com/") + end + }, + list = { + desc = "List addons", + name = "List", + type = "group", + args = { + ace2 = { + desc = "List addons using Ace2", + name = "Ace2", + type = "execute", + func = function() + print("|cffffff7fAddon list:|r") + local AceCoreAddon = AceLibrary:HasInstance("AceCoreAddon-2.0") and AceLibrary("AceCoreAddon-2.0") + table.sort(self.addons, mySort) + for _,v in ipairs(self.addons) do + if not AceCoreAddon or not AceCoreAddon:IsModule(v) then + listAddon(v) + end + end + end + }, + all = { + desc = "List all addons", + name = "All", + type = "execute", + func = function() + print("|cffffff7fAddon list:|r") + local AceCoreAddon = AceLibrary:HasInstance("AceCoreAddon-2.0") and AceLibrary("AceCoreAddon-2.0") + local count = GetNumAddOns() + for i = 1, count do + listNormalAddon(i) + end + end + }, + enabled = { + desc = "List all enabled addons", + name = "Enabled", + type = "execute", + func = function() + print("|cffffff7fAddon list:|r") + local AceCoreAddon = AceLibrary:HasInstance("AceCoreAddon-2.0") and AceLibrary("AceCoreAddon-2.0") + local count = GetNumAddOns() + for i = 1, count do + local _,_,_,enabled,loadable = GetAddOnInfo(i) + if enabled and loadable then + listNormalAddon(i) + end + end + end + }, + disabled = { + desc = "List all disabled addons", + name = "Disabled", + type = "execute", + func = function() + print("|cffffff7fAddon list:|r") + local AceCoreAddon = AceLibrary:HasInstance("AceCoreAddon-2.0") and AceLibrary("AceCoreAddon-2.0") + local count = GetNumAddOns() + for i = 1, count do + local _,_,_,enabled,loadable = GetAddOnInfo(i) + if not enabled or not loadable then + listNormalAddon(i) + end + end + end + }, + lod = { + desc = "List all LoadOnDemand addons", + name = "LoadOnDemand", + type = "execute", + func = function() + print("|cffffff7fAddon list:|r") + local AceCoreAddon = AceLibrary:HasInstance("AceCoreAddon-2.0") and AceLibrary("AceCoreAddon-2.0") + local count = GetNumAddOns() + for i = 1, count do + if IsAddOnLoadOnDemand(i) then + listNormalAddon(i) + end + end + end + }, + ace1 = { + desc = "List all addons using Ace1", + name = "Ace 1.x", + type = "execute", + func = function() + print("|cffffff7fAddon list:|r") + local count = GetNumAddOns() + for i = 1, count do + local dep1, dep2, dep3, dep4 = GetAddOnDependencies(i) + if dep1 == "Ace" or dep2 == "Ace" or dep3 == "Ace" or dep4 == "Ace" then + listNormalAddon(i) + end + end + end + }, + libs = { + desc = "List all libraries using AceLibrary", + name = "Libraries", + type = "execute", + func = function() + if type(AceLibrary) == "table" and type(AceLibrary.libs) == "table" then + print("|cffffff7fLibrary list:|r") + for name, data in pairs(AceLibrary.libs) do + local s + if data.minor then + s = " - " .. tostring(name) .. "." .. tostring(data.minor) + else + s = " - " .. tostring(name) + end + if AceLibrary(name).slashCommand then + s = s .. " |cffffff7f(" .. tostring(AceLibrary(name).slashCommand) .. "|cffffff7f)" + end + print(s) + end + end + end + }, + search = { + desc = "Search by name", + name = "Search", + type = "text", + usage = "", + input = true, + get = false, + set = function(...) + for i,v in ipairs(arg) do + arg[i] = string.lower(string.gsub(string.gsub(v, '%*', '.*'), '%%', '%%%%')) + end + local count = GetNumAddOns() + for i = 1, count do + local name = GetAddOnInfo(i) + local good = true + for _,v in ipairs(arg) do + if not string.find(string.lower(name), v) then + good = false + break + end + end + if good then + listNormalAddon(i) + end + end + end + } + }, + }, + enable = { + desc = "Enable addon", + name = "Enable", + type = "text", + usage = "", + get = false, + set = function(text) + local name,title,_,_,_,reason = GetAddOnInfo(text) + if reason == "MISSING" then + print(string.format("|cffffff7fAce2:|r AddOn %q does not exist", text)) + else + EnableAddOn(text) + print(string.format("|cffffff7fAce2:|r %s is now enabled", title or name)) + end + end, + }, + disable = { + desc = "Disable addon", + name = "Disable", + type = "text", + usage = "", + get = false, + set = function(text) + local name,title,_,_,_,reason = GetAddOnInfo(text) + if reason == "MISSING" then + print(string.format("|cffffff7fAce2:|r AddOn %q does not exist", text)) + else + DisableAddOn(text) + print(string.format("|cffffff7fAce2:|r %s is now disabled", title or name)) + end + end, + }, + load = { + desc = "Load addon", + name = "Load", + type = "text", + usage = "", + get = false, + set = function(text) + local name,title,_,_,loadable,reason = GetAddOnInfo(text) + if reason == "MISSING" then + print(string.format("|cffffff7fAce2:|r AddOn %q does not exist.", text)) + elseif not loadable then + print(string.format("|cffffff7fAce2:|r AddOn %q is not loadable. Reason: %s", text, reason)) + else + LoadAddOn(text) + print(string.format("|cffffff7fAce2:|r %s is now loaded", title or name)) + end + end + }, + info = { + desc = "Display information", + name = "Information", + type = "execute", + func = function() + local mem, threshold = gcinfo() + print(string.format(" - |cffffff7fMemory usage [|r%.3f MiB|cffffff7f]|r", mem / 1024)) + print(string.format(" - |cffffff7fThreshold [|r%.3f MiB|cffffff7f]|r", threshold / 1024)) + print(string.format(" - |cffffff7fFramerate [|r%.0f fps|cffffff7f]|r", GetFramerate())) + local bandwidthIn, bandwidthOut, latency = GetNetStats() + bandwidthIn, bandwidthOut = floor(bandwidthIn * 1024), floor(bandwidthOut * 1024) + print(string.format(" - |cffffff7fLatency [|r%.0f ms|cffffff7f]|r", latency)) + print(string.format(" - |cffffff7fBandwidth in [|r%.0f B/s|cffffff7f]|r", bandwidthIn)) + print(string.format(" - |cffffff7fBandwidth out [|r%.0f B/s|cffffff7f]|r", bandwidthOut)) + print(string.format(" - |cffffff7fTotal addons [|r%d|cffffff7f]|r", GetNumAddOns())) + print(string.format(" - |cffffff7fAce2 addons [|r%d|cffffff7f]|r", table.getn(self.addons))) + local ace = 0 + local enabled = 0 + local disabled = 0 + local lod = 0 + for i = 1, GetNumAddOns() do + local dep1, dep2, dep3, dep4 = GetAddOnDependencies(i) + if dep1 == "Ace" or dep2 == "Ace" or dep3 == "Ace" or dep4 == "Ace" then + ace = ace + 1 + end + if IsAddOnLoadOnDemand(i) then + lod = lod + 1 + end + local _,_,_,IsActive,loadable = GetAddOnInfo(i) + if not IsActive or not loadable then + disabled = disabled + 1 + else + enabled = enabled + 1 + end + end + print(string.format(" - |cffffff7fAce 1.x addons [|r%d|cffffff7f]|r", ace)) + print(string.format(" - |cffffff7fLoadOnDemand addons [|r%d|cffffff7f]|r", lod)) + print(string.format(" - |cffffff7fenabled addons [|r%d|cffffff7f]|r", enabled)) + print(string.format(" - |cffffff7fdisabled addons [|r%d|cffffff7f]|r", disabled)) + local libs = 0 + if type(AceLibrary) == "table" and type(AceLibrary.libs) == "table" then + for _ in pairs(AceLibrary.libs) do + libs = libs + 1 + end + end + print(string.format(" - |cffffff7fAceLibrary instances [|r%d|cffffff7f]|r", libs)) + end + } + } + }) + elseif major == "AceModuleCore-2.0" then + AceModuleCore = instance + end +end + +local function activate(self, oldLib, oldDeactivate) + AceAddon = self + + if oldLib then + self.playerLoginFired = oldLib.playerLoginFired or DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.defaultLanguage + self.addonsToOnEnable = oldLib.addonsToOnEnable + self.addons = oldLib.addons + self.nextAddon = oldLib.nextAddon + self.addonsStarted = oldLib.addonsStarted + end + if not self.addons then + self.addons = {} + end + if not self.nextAddon then + self.nextAddon = {} + end + if not self.addonsStarted then + self.addonsStarted = {} + end + if oldDeactivate then + oldDeactivate(oldLib) + end +end + +AceLibrary:Register(AceAddon, MAJOR_VERSION, MINOR_VERSION, activate, nil, external) +AceAddon = AceLibrary(MAJOR_VERSION) diff --git a/libs/ace/AceConsole-2.0/AceConsole-2.0.lua b/libs/ace/AceConsole-2.0/AceConsole-2.0.lua new file mode 100644 index 0000000..6700cee --- /dev/null +++ b/libs/ace/AceConsole-2.0/AceConsole-2.0.lua @@ -0,0 +1,1898 @@ +--[[ +Name: AceConsole-2.0 +Revision: $Rev: 17638 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Ace 1.x by Turan (turan@gryphon.com) +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceConsole-2.0 +SVN: http://svn.wowace.com/root/trunk/Ace2/AceConsole-2.0 +Description: Mixin to allow for input/output capabilities. This uses the + AceOptions data table format to determine input. + http://wiki.wowace.com/index.php/AceOptions_data_table +Dependencies: AceLibrary, AceOO-2.0 +]] + +local MAJOR_VERSION = "AceConsole-2.0" +local MINOR_VERSION = "$Revision: 17638 $" + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0.") end + +local MAP_ONOFF, USAGE, IS_CURRENTLY_SET_TO, IS_NOW_SET_TO, IS_NOT_A_VALID_OPTION_FOR, IS_NOT_A_VALID_VALUE_FOR, NO_OPTIONS_AVAILABLE, OPTION_HANDLER_NOT_FOUND, OPTION_HANDLER_NOT_VALID, OPTION_IS_DISABLED +if GetLocale() == "deDE" then + MAP_ONOFF = { [false] = "|cffff0000Aus|r", [true] = "|cff00ff00An|r" } + USAGE = "Benutzung" + IS_CURRENTLY_SET_TO = "|cffffff7f%s|r steht momentan auf |cffffff7f[|r%s|cffffff7f]|r" + IS_NOW_SET_TO = "|cffffff7f%s|r ist nun auf |cffffff7f[|r%s|cffffff7f]|r gesetzt" + IS_NOT_A_VALID_OPTION_FOR = "[|cffffff7f%s|r] ist keine g\195\188ltige Option f\195\188r |cffffff7f%s|r" + IS_NOT_A_VALID_VALUE_FOR = "[|cffffff7f%s|r] ist kein g\195\188ltiger Wert f\195\188r |cffffff7f%s|r" + NO_OPTIONS_AVAILABLE = "Keine Optionen verfügbar" + OPTION_HANDLER_NOT_FOUND = "Optionen handler |cffffff7f%q|r nicht gefunden." + OPTION_HANDLER_NOT_VALID = "Optionen handler nicht g\195\188ltig." + OPTION_IS_DISABLED = "Option |cffffff7f%s|r deaktiviert." +elseif GetLocale() == "frFR" then + MAP_ONOFF = { [false] = "|cffff0000Inactif|r", [true] = "|cff00ff00Actif|r" } + USAGE = "Utilisation" + IS_CURRENTLY_SET_TO = "|cffffff7f%s|r est actuellement positionn\195\169 sur |cffffff7f[|r%s|cffffff7f]|r" + IS_NOW_SET_TO = "|cffffff7f%s|r est maintenant positionn\195\169 sur |cffffff7f[|r%s|cffffff7f]|r" + IS_NOT_A_VALID_OPTION_FOR = "[|cffffff7f%s|r] n'est pas une option valide pour |cffffff7f%s|r" + IS_NOT_A_VALID_VALUE_FOR = "[|cffffff7f%s|r] n'est pas une valeur valide pour |cffffff7f%s|r" + NO_OPTIONS_AVAILABLE = "Pas d'options disponibles" + OPTION_HANDLER_NOT_FOUND = "Le gestionnaire d'option |cffffff7f%q|r n'a pas \195\169t\195\169 trouv\195\169." + OPTION_HANDLER_NOT_VALID = "Le gestionnaire d'option n'est pas valide." + OPTION_IS_DISABLED = "L'option |cffffff7f%s|r est d\195\169sactiv\195\169e." +elseif GetLocale() == "koKR" then + MAP_ONOFF = { [false] = "|cffff0000끔|r", [true] = "|cff00ff00켬|r" } + USAGE = "사용법" + IS_CURRENTLY_SET_TO = "|cffffff7f%s|r|1은;는; 현재 상태는 |cffffff7f[|r%s|cffffff7f]|r|1으로;로; 설정되어 있습니다" + IS_NOW_SET_TO = "|cffffff7f%s|r|1을;를; |cffffff7f[|r%s|cffffff7f]|r 상태로 변경합니다" + IS_NOT_A_VALID_OPTION_FOR = "[|cffffff7f%s|r]|1은;는; |cffffff7f%s|r에서 사용불가능한 설정입니다" + IS_NOT_A_VALID_VALUE_FOR = "[|cffffff7f%s|r]|1은;는; |cffffff7f%s|r에서 사용불가능한 설정값입니다" + NO_OPTIONS_AVAILABLE = "가능한 설정이 없습니다" + OPTION_HANDLER_NOT_FOUND = "설정 조정값인 |cffffff7f%q|r|1을;를; 찾지 못했습니다." + OPTION_HANDLER_NOT_VALID = "설정 조정값이 올바르지 않습니다." + OPTION_IS_DISABLED = "|cffffff7f%s|r 설정은 사용할 수 없습니다." +elseif GetLocale() == "zhCN" then + MAP_ONOFF = { [false] = "|cffff0000\229\133\179\233\151\173|r", [true] = "|cff00ff00\229\188\128\229\144\175|r" } + USAGE = "\231\148\168\230\179\149" + IS_CURRENTLY_SET_TO = "|cffffff7f%s|r \229\189\147\229\137\141\232\162\171\232\174\190\231\189\174 |cffffff7f[|r%s|cffffff7f]|r" + IS_NOW_SET_TO = "|cffffff7f%s|r \231\142\176\229\156\168\232\162\171\232\174\190\231\189\174\228\184\186 |cffffff7f[|r%s|cffffff7f]|r" + IS_NOT_A_VALID_OPTION_FOR = "[|cffffff7f%s|r] \228\184\141\230\152\175\228\184\128\228\184\170\230\156\137\230\149\136\231\154\132\233\128\137\233\161\185 \228\184\186 |cffffff7f%s|r" + IS_NOT_A_VALID_VALUE_FOR = "[|cffffff7f%s|r] \228\184\141\230\152\175\228\184\128\228\184\170\230\156\137\230\149\136\229\128\188 \228\184\186 |cffffff7f%s|r" + NO_OPTIONS_AVAILABLE = "\230\178\161\230\156\137\233\128\137\233\161\185\229\143\175\231\148\168" + OPTION_HANDLER_NOT_FOUND = "\233\128\137\233\161\185\229\164\132\231\144\134\231\168\139\229\186\143 |cffffff7f%q|r \230\178\161\230\159\165\230\137\190." + OPTION_HANDLER_NOT_VALID = "\233\128\137\233\161\185\229\164\132\231\144\134\231\168\139\229\186\143 \230\151\160\230\149\136." + OPTION_IS_DISABLED = "\233\128\137\233\161\185 |cffffff7f%s|r \228\184\141\229\174\140\230\149\180." +elseif GetLocale() == "zhTW" then + MAP_ONOFF = { [false] = "|cffff0000關閉|r", [true] = "|cff00ff00開啟|r" } + USAGE = "用法" + IS_CURRENTLY_SET_TO = "|cffffff7f%s|r 目前的設定為 |cffffff7f[|r%s|cffffff7f]|r" + IS_NOW_SET_TO = "|cffffff7f%s|r 現在被設定為 |cffffff7f[|r%s|cffffff7f]|r" + IS_NOT_A_VALID_OPTION_FOR = "[|cffffff7f%s|r] 是一個不符合規定的選項,對 |cffffff7f%s|r" + IS_NOT_A_VALID_VALUE_FOR = "[|cffffff7f%s|r] 是一個不符合規定的數值,對 |cffffff7f%s|r" + NO_OPTIONS_AVAILABLE = "沒有可用的選項處理器。" + OPTION_HANDLER_NOT_FOUND = "找不到 |cffffff7f%q|r 選項處理器。" + OPTION_HANDLER_NOT_VALID = "選項處理器不符合規定。" + OPTION_IS_DISABLED = "|cffffff7f%s|r 已被停用。" +else -- enUS + MAP_ONOFF = { [false] = "|cffff0000Off|r", [true] = "|cff00ff00On|r" } + USAGE = "Usage" + IS_CURRENTLY_SET_TO = "|cffffff7f%s|r is currently set to |cffffff7f[|r%s|cffffff7f]|r" + IS_NOW_SET_TO = "|cffffff7f%s|r is now set to |cffffff7f[|r%s|cffffff7f]|r" + IS_NOT_A_VALID_OPTION_FOR = "[|cffffff7f%s|r] is not a valid option for |cffffff7f%s|r" + IS_NOT_A_VALID_VALUE_FOR = "[|cffffff7f%s|r] is not a valid value for |cffffff7f%s|r" + NO_OPTIONS_AVAILABLE = "No options available" + OPTION_HANDLER_NOT_FOUND = "Option handler |cffffff7f%q|r not found." + OPTION_HANDLER_NOT_VALID = "Option handler not valid." + OPTION_IS_DISABLED = "Option |cffffff7f%s|r is disabled." +end + +local NONE = NONE or "None" + +local AceOO = AceLibrary("AceOO-2.0") +local AceEvent + +local AceConsole = AceOO.Mixin { "Print", "PrintComma", "CustomPrint", "RegisterChatCommand" } +local Dewdrop + +local _G = getfenv(0) + +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +local function print(text, name, r, g, b, frame, delay) + if not text or string.len(text) == 0 then + text = " " + end + if not name or name == AceConsole then + (frame or DEFAULT_CHAT_FRAME):AddMessage(text, r, g, b, nil, delay or 5) + else + (frame or DEFAULT_CHAT_FRAME):AddMessage("|cffffff78" .. tostring(name) .. ":|r " .. text, r, g, b, nil, delay or 5) + end +end + +local tmp +function AceConsole:CustomPrint(r, g, b, frame, delay, connector, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + a1 = tostring(a1) + if string.find(a1, "%%") then + print(string.format(a1, tostring(a2), tostring(a3), tostring(a4), tostring(a5), tostring(a6), tostring(a7), tostring(a8), tostring(a9), tostring(a10), tostring(a11), tostring(a12), tostring(a13), tostring(a14), tostring(a15), tostring(a16), tostring(a17), tostring(a18), tostring(a19), tostring(a20)), self, r, g, b, frame or self.printFrame, delay) + else + if not tmp then + tmp = {} + end + tmp[1] = a1 + tmp[2] = a2 + tmp[3] = a3 + tmp[4] = a4 + tmp[5] = a5 + tmp[6] = a6 + tmp[7] = a7 + tmp[8] = a8 + tmp[9] = a9 + tmp[10] = a10 + tmp[11] = a11 + tmp[12] = a12 + tmp[13] = a13 + tmp[14] = a14 + tmp[15] = a15 + tmp[16] = a16 + tmp[17] = a17 + tmp[18] = a18 + tmp[19] = a19 + tmp[20] = a20 + local n = 20 + while tmp[n] == nil do + n = n - 1 + end + table_setn(tmp, n) + for k = 1, n do + tmp[k] = tostring(tmp[k]) + end + print(table.concat(tmp, connector or " "), self, r, g, b, frame or self.printFrame, delay) + for k,v in pairs(tmp) do + tmp[k] = nil + end + table_setn(tmp, 0) + end +end + +function AceConsole:Print(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + return AceConsole.CustomPrint(self, nil, nil, nil, nil, nil, " ", a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) +end + +function AceConsole:PrintComma(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + return AceConsole.CustomPrint(self, nil, nil, nil, nil, nil, ", ", a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) +end + +local work +local argwork + +local string_gfind = string.gmatch or string.gfind + +local function findTableLevel(self, options, chat, text, index, passTable) + if not index then + index = 1 + if work then + for k,v in pairs(work) do + work[k] = nil + end + table_setn(work, 0) + for k,v in pairs(argwork) do + argwork[k] = nil + end + table_setn(argwork, 0) + else + work = {} + argwork = {} + end + local len = string.len(text) + local count + repeat + text, count = string.gsub(text, "(|cff%x%x%x%x%x%x|Hitem:%d-:%d-:%d-:%d-|h%[[^%]]-) (.-%]|h|r)", "%1\001%2") + until count == 0 + text = string.gsub(text, "(%]|h|r)(|cff%x%x%x%x%x%x|Hitem:%d-:%d-:%d-:%d-|h%[)", "%1 %2") + for token in string_gfind(text, "([^%s]+)") do + local token = token + local num = tonumber(token) + if num then + token = num + else + token = string.gsub(token, "\001", " ") + end + table.insert(work, token) + end + end + + local path = chat + for i = 1, index - 1 do + path = path .. " " .. tostring(work[i]) + end + + if type(options.args) == "table" then + local disabled, hidden = options.disabled, options.cmdHidden or options.hidden + if hidden then + if type(hidden) == "function" then + hidden = hidden() + elseif type(hidden) == "string" then + local handler = options.handler or self + if type(handler[hidden]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, hidden)) + end + hidden = handler[hidden](handler) + end + end + if hidden then + disabled = true + elseif disabled then + if type(disabled) == "function" then + disabled = disabled() + elseif type(disabled) == "string" then + local handler = options.handler or self + if type(handler[disabled]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, disabled)) + end + disabled = handler[disabled](handler) + end + end + if not disabled then + local next = work[index] and string.lower(work[index]) + if next then + for k,v in pairs(options.args) do + local good = false + if string.lower(k) == next then + good = true + elseif type(v.aliases) == "table" then + for _,alias in ipairs(v.aliases) do + if string.lower(alias) == next then + good = true + break + end + end + elseif type(v.aliases) == "string" and string.lower(v.aliases) == next then + good = true + end + if good then + return findTableLevel(options.handler or self, v, chat, text, index + 1, options.pass and options or nil) + end + end + end + end + end + for i = index, table.getn(work) do + table.insert(argwork, work[i]) + end + return options, path, argwork, options.handler or self, passTable, passTable and work[index - 1] +end + +local function validateOptionsMethods(self, options, position) + if type(options) ~= "table" then + return "Options must be a table.", position + end + self = options.handler or self + if options.type == "execute" then + if options.func and type(options.func) ~= "string" and type(options.func) ~= "function" then + return "func must be a string or function", position + end + if options.func and type(options.func) == "string" and type(self[options.func]) ~= "function" then + return string.format("%q is not a proper function", options.func), position + end + else + if options.get then + if type(options.get) ~= "string" and type(options.get) ~= "function" then + return "get must be a string or function", position + end + if type(options.get) == "string" and type(self[options.get]) ~= "function" then + return string.format("%q is not a proper function", options.get), position + end + end + if options.set then + if type(options.set) ~= "string" and type(options.set) ~= "function" then + return "set must be a string or function", position + end + if type(options.set) == "string" and type(self[options.set]) ~= "function" then + return string.format("%q is not a proper function", options.set), position + end + end + if options.validate and type(options.validate) ~= "table" then + if type(options.validate) ~= "string" and type(options.validate) ~= "function" then + return "validate must be a string or function", position + end + if type(options.validate) == "string" and type(self[options.validate]) ~= "function" then + return string.format("%q is not a proper function", options.validate), position + end + end + end + if options.disabled and type(options.disabled) == "string" and type(self[options.disabled]) ~= "function" then + return string.format("%q is not a proper function", options.disabled), position + end + if options.cmdHidden and type(options.cmdHidden) == "string" and type(self[options.cmdHidden]) ~= "function" then + return string.format("%q is not a proper function", options.cmdHidden), position + end + if options.guiHidden and type(options.guiHidden) == "string" and type(self[options.guiHidden]) ~= "function" then + return string.format("%q is not a proper function", options.guiHidden), position + end + if options.hidden and type(options.hidden) == "string" and type(self[options.hidden]) ~= "function" then + return string.format("%q is not a proper function", options.hidden), position + end + if options.type == "group" and type(options.args) == "table" then + for k,v in pairs(options.args) do + if type(v) == "table" then + local newposition + if position then + newposition = position .. ".args." .. k + else + newposition = "args." .. k + end + local err, pos = validateOptionsMethods(self, v, newposition) + if err then + return err, pos + end + end + end + end +end + +local function validateOptions(options, position, baseOptions, fromPass) + if not baseOptions then + baseOptions = options + end + if type(options) ~= "table" then + return "Options must be a table.", position + end + local kind = options.type + if type(kind) ~= "string" then + return '"type" must be a string.', position + elseif kind ~= "group" and kind ~= "range" and kind ~= "text" and kind ~= "execute" and kind ~= "toggle" and kind ~= "color" and kind ~= "header" then + return '"type" must either be "range", "text", "group", "toggle", "execute", "color", or "header".', position + end + if options.aliases then + if type(options.aliases) ~= "table" and type(options.aliases) ~= "string" then + return '"alias" must be a table or string', position + end + end + if not fromPass then + if kind == "execute" then + if type(options.func) ~= "string" and type(options.func) ~= "function" then + return '"func" must be a string or function', position + end + elseif kind == "range" or kind == "text" or kind == "toggle" then + if type(options.set) ~= "string" and type(options.set) ~= "function" then + return '"set" must be a string or function', position + end + if kind == "text" and options.get == false then + elseif type(options.get) ~= "string" and type(options.get) ~= "function" then + return '"get" must be a string or function', position + end + elseif kind == "group" and options.pass then + if options.pass ~= true then + return '"pass" must be either nil, true, or false', position + end + if not options.func then + if type(options.set) ~= "string" and type(options.set) ~= "function" then + return '"set" must be a string or function', position + end + if type(options.get) ~= "string" and type(options.get) ~= "function" then + return '"get" must be a string or function', position + end + elseif type(options.func) ~= "string" and type(options.func) ~= "function" then + return '"func" must be a string or function', position + end + end + else + if kind == "group" then + return 'cannot have "type" = "group" as a subgroup of a passing group', position + end + end + if options ~= baseOptions then + if kind == "header" then + elseif type(options.desc) ~= "string" then + return '"desc" must be a string', position + elseif string.len(options.desc) == 0 then + return '"desc" cannot be a 0-length string', position + end + end + + if options ~= baseOptions or kind == "range" or kind == "text" or kind == "toggle" or kind == "color" then + if options.type == "header" and not options.cmdName and not options.name then + elseif options.cmdName then + if type(options.cmdName) ~= "string" then + return '"cmdName" must be a string or nil', position + elseif string.len(options.cmdName) == 0 then + return '"cmdName" cannot be a 0-length string', position + end + if type(options.guiName) ~= "string" then + if not options.guiNameIsMap then + return '"guiName" must be a string or nil', position + end + elseif string.len(options.guiName) == 0 then + return '"guiName" cannot be a 0-length string', position + end + else + if type(options.name) ~= "string" then + return '"name" must be a string', position + elseif string.len(options.name) == 0 then + return '"name" cannot be a 0-length string', position + end + end + end + if options.guiNameIsMap then + if type(options.guiNameIsMap) ~= "boolean" then + return '"guiNameIsMap" must be a boolean or nil', position + elseif options.type ~= "toggle" then + return 'if "guiNameIsMap" is true, then "type" must be set to \'toggle\'', position + elseif type(options.map) ~= "table" then + return '"map" must be a table', position + end + end + if options.message and type(options.message) ~= "string" then + return '"message" must be a string or nil', position + end + if options.error and type(options.error) ~= "string" then + return '"error" must be a string or nil', position + end + if options.current and type(options.current) ~= "string" then + return '"current" must be a string or nil', position + end + if options.order then + if type(options.order) ~= "number" or (-1 < options.order and options.order < 0.999) then + return '"order" must be a non-zero number or nil', position + end + end + if options.disabled then + if type(options.disabled) ~= "function" and type(options.disabled) ~= "string" and options.disabled ~= true then + return '"disabled" must be a function, string, or boolean', position + end + end + if options.cmdHidden then + if type(options.cmdHidden) ~= "function" and type(options.cmdHidden) ~= "string" and options.cmdHidden ~= true then + return '"cmdHidden" must be a function, string, or boolean', position + end + end + if options.guiHidden then + if type(options.guiHidden) ~= "function" and type(options.guiHidden) ~= "string" and options.guiHidden ~= true then + return '"guiHidden" must be a function, string, or boolean', position + end + end + if options.hidden then + if type(options.hidden) ~= "function" and type(options.hidden) ~= "string" and options.hidden ~= true then + return '"hidden" must be a function, string, or boolean', position + end + end + if kind == "text" then + if type(options.validate) == "table" then + local t = options.validate + local iTable = nil + for k,v in pairs(t) do + if type(k) == "number" then + if iTable == nil then + iTable = true + elseif not iTable then + return '"validate" must either have all keys be indexed numbers or strings', position + elseif k < 1 or k > table.getn(t) then + return '"validate" numeric keys must be indexed properly. >= 1 and <= table.getn', position + end + else + if iTable == nil then + iTable = false + elseif iTable then + return '"validate" must either have all keys be indexed numbers or strings', position + end + end + if type(v) ~= "string" then + return '"validate" values must all be strings', position + end + end + else + if type(options.usage) ~= "string" then + return '"usage" must be a string', position + elseif options.validate and type(options.validate) ~= "string" and type(options.validate) ~= "function" then + return '"validate" must be a string, function, or table', position + end + end + elseif kind == "range" then + if options.min or options.max then + if type(options.min) ~= "number" then + return '"min" must be a number', position + elseif type(options.max) ~= "number" then + return '"max" must be a number', position + elseif options.min >= options.max then + return '"min" must be less than "max"', position + end + end + if options.step then + if type(options.step) ~= "number" then + return '"step" must be a number', position + elseif options.step < 0 then + return '"step" must be nonnegative', position + end + end + if options.isPercent and options.isPercent ~= true then + return '"isPercent" must either be nil, true, or false', position + end + elseif kind == "toggle" then + if options.map then + if type(options.map) ~= "table" then + return '"map" must be a table', position + elseif type(options.map[true]) ~= "string" then + return '"map[true]" must be a string', position + elseif type(options.map[false]) ~= "string" then + return '"map[false]" must be a string', position + end + end + elseif kind == "color" then + if options.hasAlpha and options.hasAlpha ~= true then + return '"hasAlpha" must be nil, true, or false', position + end + elseif kind == "group" then + if options.pass and options.pass ~= true then + return '"pass" must be nil, true, or false', position + end + if type(options.args) ~= "table" then + return '"args" must be a table', position + end + for k,v in pairs(options.args) do + if type(k) ~= "string" then + return '"args" keys must be strings', position + elseif string.find(k, "%s") then + return string.format('"args" keys must not include spaces. %q is not appropriate.', k), position + elseif string.len(k) == 0 then + return '"args" keys must not be 0-length strings.', position + end + if type(v) ~= "table" then + return '"args" values must be tables', position and position .. "." .. k or k + end + local newposition + if position then + newposition = position .. ".args." .. k + else + newposition = "args." .. k + end + local err, pos = validateOptions(v, newposition, baseOptions, options.pass) + if err then + return err, pos + end + end + end +end + +local colorTable +local colorFunc +local colorCancelFunc + +local order + +local mysort_args +local mysort + +local function printUsage(self, handler, realOptions, options, path, args, quiet, filter) + if filter then + filter = "^" .. string.gsub(filter, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") + end + local hidden, disabled = options.cmdHidden or options.hidden, options.disabled + if hidden then + if type(hidden) == "function" then + hidden = hidden() + elseif type(hidden) == "string" then + if type(handler[hidden]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, hidden)) + end + hidden = handler[hidden](handler) + end + end + if hidden then + disabled = true + elseif disabled then + if type(disabled) == "function" then + disabled = disabled() + elseif type(disabled) == "string" then + if type(handler[disabled]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, disabled)) + end + disabled = handler[disabled](handler) + end + end + local kind = string.lower(options.type or "group") + if disabled then + print(string.format(OPTION_IS_DISABLED, path), realOptions.cmdName or realOptions.name or self) + elseif kind == "text" then + local var + if passTable then + if not passTable.get then + elseif type(passTable.get) == "function" then + var = passTable.get(passValue) + else + local handler = passTable.handler or handler + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + else + if not options.get then + elseif type(options.get) == "function" then + var = options.get() + else + local handler = options.handler or handler + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + end + + local usage + if type(options.validate) == "table" then + if filter then + if not order then + order = {} + end + for k,v in pairs(options.validate) do + if string.find(v, filter) then + table.insert(order, v) + end + end + usage = "{" .. table.concat(order, " || ") .. "}" + for k in pairs(order) do + order[k] = nil + end + table_setn(order, 0) + else + if not order then + order = {} + end + for k,v in pairs(options.validate) do + table.insert(order, v) + end + usage = "{" .. table.concat(order, " || ") .. "}" + for k in pairs(order) do + order[k] = nil + end + table_setn(order, 0) + end + var = options.validate[var] or var + else + usage = options.usage or "" + end + if not quiet then + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, usage), realOptions.cmdName or realOptions.name or self) + end + if (passTable and passTable.get) or options.get then + print(string.format(options.current or IS_CURRENTLY_SET_TO, tostring(options.cmdName or options.name), tostring(var or NONE))) + end + elseif kind == "range" then + local var + if passTable then + if type(passTable.get) == "function" then + var = passTable.get(passValue) + else + local handler = passTable.handler or handler + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + else + if type(options.get) == "function" then + var = options.get() + else + local handler = options.handler or handler + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + end + + local usage + local min = options.min or 0 + local max = options.max or 1 + if options.isPercent then + min, max = min * 100, max * 100 + var = tostring(var * 100) .. "%" + end + local bit = "-" + if min < 0 or max < 0 then + bit = " - " + end + usage = string.format("(%s%s%s)", min, bit, max) + if not quiet then + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, usage), realOptions.cmdName or realOptions.name or self) + end + print(string.format(options.current or IS_CURRENTLY_SET_TO, tostring(options.cmdName or options.name), tostring(var or NONE))) + elseif kind == "group" then + local usage + if next(options.args) then + if not order then + order = {} + end + for k,v in pairs(options.args) do + if v.type ~= "header" then + local hidden = v.cmdHidden or v.hidden + if hidden then + if type(hidden) == "function" then + hidden = hidden() + elseif type(hidden) == "string" then + local handler = v.handler or handler + if type(handler[hidden]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, hidden)) + end + hidden = handler[hidden](handler) + end + end + if not hidden then + if filter then + if string.find(k, filter) then + table.insert(order, k) + elseif type(v.aliases) == "table" then + for _,bit in ipairs(v.aliases) do + if string.find(v.aliases, filter) then + table.insert(order, k) + break + end + end + elseif type(v.aliases) == "string" then + if string.find(v.aliases, filter) then + table.insert(order, k) + end + end + else + table.insert(order, k) + end + end + end + end + if not mysort then + mysort = function(a, b) + local alpha, bravo = mysort_args[a], mysort_args[b] + local alpha_order = alpha and alpha.order or 100 + local bravo_order = bravo and bravo.order or 100 + if alpha_order == bravo_order then + return tostring(a) < tostring(b) + else + if alpha_order < 0 then + if bravo_order > 0 then + return false + end + else + if bravo_order < 0 then + return true + end + end + if alpha_order > 0 and bravo_order > 0 then + return tostring(a) < tostring(b) + end + return alpha_order < bravo_order + end + end + end + mysort_args = options.args + table.sort(order, mysort) + mysort_args = nil + if not quiet then + if options == realOptions then + if options.desc then + print(tostring(options.desc), realOptions.cmdName or realOptions.name or self) + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, "{" .. table.concat(order, " || ") .. "}")) + elseif self.description or self.notes then + print(tostring(self.description or self.notes), realOptions.cmdName or realOptions.name or self) + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, "{" .. table.concat(order, " || ") .. "}")) + else + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, "{" .. table.concat(order, " || ") .. "}"), realOptions.cmdName or realOptions.name or self) + end + else + if options.desc then + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, "{" .. table.concat(order, " || ") .. "}"), realOptions.cmdName or realOptions.name or self) + print(tostring(options.desc)) + else + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, "{" .. table.concat(order, " || ") .. "}"), realOptions.cmdName or realOptions.name or self) + end + end + end + for _,k in ipairs(order) do + local v = options.args[k] + if v then + local disabled = v.disabled + if disabled then + if type(disabled) == "function" then + disabled = disabled() + elseif type(disabled) == "string" then + local handler = v.handler or handler + if type(handler[disabled]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, disabled)) + end + disabled = handler[disabled](handler) + end + end + if type(v.aliases) == "table" then + k = k .. " || " .. table.concat(v.aliases, " || ") + elseif type(v.aliases) == "string" then + k = k .. " || " .. v.aliases + end + if v.get then + local a1,a2,a3,a4 + if type(v.get) == "function" then + if options.pass then + a1,a2,a3,a4 = v.get(k) + else + a1,a2,a3,a4 = v.get() + end + else + local handler = v.handler or handler + if type(handler[v.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, v.get)) + end + if options.pass then + a1,a2,a3,a4 = handler[v.get](handler, k) + else + a1,a2,a3,a4 = handler[v.get](handler) + end + end + if v.type == "color" then + if v.hasAlpha then + if not a1 or not a2 or not a3 or not a4 then + s = NONE + else + s = string.format("|c%02x%02x%02x%02x%02x%02x%02x%02x|r", a4*255, a1*255, a2*255, a3*255, a4*255, a1*255, a2*255, a3*255) + end + else + if not a1 or not a2 or not a3 then + s = NONE + else + s = string.format("|cff%02x%02x%02x%02x%02x%02x|r", a1*255, a2*255, a3*255, a1*255, a2*255, a3*255) + end + end + elseif v.type == "toggle" then + if v.map then + s = tostring(v.map[a1 and true or false] or NONE) + else + s = tostring(MAP_ONOFF[a1 and true or false] or NONE) + end + elseif v.type == "range" then + if v.isPercent then + s = tostring(a1 * 100) .. "%" + else + s = tostring(a1) + end + elseif v.type == "text" and type(v.validate) == "table" then + s = tostring(v.validate[a1] or a1) + else + s = tostring(a1 or NONE) + end + if disabled then + local s = string.gsub(s, "|cff%x%x%x%x%x%x(.-)|r", "%1") + local desc = string.gsub(v.desc or NONE, "|cff%x%x%x%x%x%x(.-)|r", "%1") + print(string.format("|cffcfcfcf - %s: [%s] %s|r", k, s, desc)) + else + print(string.format(" - |cffffff7f%s: [|r%s|cffffff7f]|r %s", k, s, v.desc or NONE)) + end + else + if disabled then + local desc = string.gsub(v.desc or NONE, "|cff%x%x%x%x%x%x(.-)|r", "%1") + print(string.format("|cffcfcfcf - %s: %s", k, desc)) + else + print(string.format(" - |cffffff7f%s:|r %s", k, v.desc or NONE)) + end + end + end + end + for k in pairs(order) do + order[k] = nil + end + table_setn(order, 0) + else + if options.desc then + desc = options.desc + print(string.format("|cffffff7f%s:|r %s", USAGE, path), realOptions.cmdName or realOptions.name or self) + print(tostring(options.desc)) + elseif options == realOptions and (self.description or self.notes) then + print(tostring(self.description or self.notes), realOptions.cmdName or realOptions.name or self) + print(string.format("|cffffff7f%s:|r %s", USAGE, path)) + else + print(string.format("|cffffff7f%s:|r %s", USAGE, path), realOptions.cmdName or realOptions.name or self) + end + print(NO_OPTIONS_AVAILABLE) + end + end +end + +local function handlerFunc(self, chat, msg, options) + if not msg then + msg = "" + else + msg = string.gsub(msg, "^%s*(.-)%s*$", "%1") + msg = string.gsub(msg, "%s+", " ") + end + + local realOptions = options + local options, path, args, handler, passTable, passValue = findTableLevel(self, options, chat, msg) + + local hidden, disabled = options.cmdHidden or options.hidden, options.disabled + if hidden then + if type(hidden) == "function" then + hidden = hidden() + elseif type(hidden) == "string" then + if type(handler[hidden]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, hidden)) + end + hidden = handler[hidden](handler) + end + end + if hidden then + disabled = true + elseif disabled then + if type(disabled) == "function" then + disabled = disabled() + elseif type(disabled) == "string" then + if type(handler[disabled]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, disabled)) + end + disabled = handler[disabled](handler) + end + end + local _G_this = this + local kind = string.lower(options.type or "group") + if disabled then + print(string.format(OPTION_IS_DISABLED, path), realOptions.cmdName or realOptions.name or self) + elseif kind == "text" then + if table.getn(args) > 0 then + if (type(options.validate) == "table" and table.getn(args) > 1) or (type(options.validate) ~= "table" and not options.input) then + local arg = table.concat(args, " ") + for k,v in pairs(args) do + args[k] = nil + end + table_setn(args, 0) + table.insert(args, arg) + end + if options.validate then + local good + if type(options.validate) == "function" then + good = options.validate(unpack(args)) + elseif type(options.validate) == "table" then + local arg = args[1] + arg = string.lower(tostring(arg)) + for k,v in pairs(options.validate) do + if string.lower(v) == arg then + args[1] = type(k) == "string" and k or v + good = true + break + end + end + if not good and type((next(options.validate))) == "string" then + for k,v in pairs(options.validate) do + if type(k) == "string" and string.lower(k) == arg then + args[1] = k + good = true + break + end + end + end + else + if type(handler[options.validate]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.validate)) + end + good = handler[options.validate](handler, unpack(args)) + end + if not good then + local usage + if type(options.validate) == "table" then + if not order then + order = {} + end + for k,v in pairs(options.validate) do + table.insert(order, v) + end + usage = "{" .. table.concat(order, " || ") .. "}" + for k in pairs(order) do + order[k] = nil + end + table_setn(order, 0) + else + usage = options.usage or "" + end + print(string.format(options.error or IS_NOT_A_VALID_OPTION_FOR, tostring(table.concat(args, " ")), path), realOptions.cmdName or realOptions.name or self) + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, usage)) + return + end + end + + local var + if passTable then + if not passTable.get then + elseif type(passTable.get) == "function" then + var = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + else + if not options.get then + elseif type(options.get) == "function" then + var = options.get() + else + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + end + + if var ~= args[1] then + if passTable then + if type(passTable.set) == "function" then + passTable.set(passValue, unpack(args)) + else + if type(handler[passTable.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.set)) + end + handler[passTable.set](handler, passTable.set, unpack(args)) + end + else + if type(options.set) == "function" then + options.set(unpack(args)) + else + if type(handler[options.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.set)) + end + handler[options.set](handler, unpack(args)) + end + end + end + end + + if table.getn(args) > 0 then + local var + if passTable then + if not passTable.get then + elseif type(passTable.get) == "function" then + var = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + else + if not options.get then + elseif type(options.get) == "function" then + var = options.get() + else + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + end + if type(options.validate) == "table" then + var = options.validate[var] or var + end + if (passTable and passTable.get) or options.get then + print(string.format(options.message or IS_NOW_SET_TO, tostring(options.cmdName or options.name), tostring(var or NONE)), realOptions.cmdName or realOptions.name or self) + end + if var == args[1] then + return + end + else + printUsage(self, handler, realOptions, options, path, args) + return + end + elseif kind == "execute" then + if passTable then + if type(passFunc) == "function" then + set(passValue) + else + if type(handler[passFunc]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passFunc)) + end + handler[passFunc](handler, passValue) + end + else + local ret, msg + if type(options.func) == "function" then + options.func() + else + local handler = options.handler or self + if type(handler[options.func]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.func)) + end + handler[options.func](handler) + end + end + elseif kind == "toggle" then + local var + if passTable then + if type(passTable.get) == "function" then + var = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + if type(passTable.set) == "function" then + passTable.set(passValue, not var) + else + if type(handler[passTable.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.set)) + end + handler[passTable.set](handler, passValue, not var) + end + if type(passTable.get) == "function" then + var = passTable.get(passValue) + else + var = handler[passTable.get](handler, passValue) + end + else + local handler = options.handler or self + if type(options.get) == "function" then + var = options.get() + else + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + if type(options.set) == "function" then + options.set(not var) + else + if type(handler[options.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.set)) + end + handler[options.set](handler, not var) + end + if type(options.get) == "function" then + var = options.get() + else + var = handler[options.get](handler) + end + end + + print(string.format(options.message or IS_NOW_SET_TO, tostring(options.cmdName or options.name), (options.map or MAP_ONOFF)[var and true or false] or NONE), realOptions.cmdName or realOptions.name or self) + elseif kind == "range" then + local arg + if table.getn(args) <= 1 then + arg = args[1] + else + arg = table.concat(args, " ") + end + + if arg then + local min = options.min or 0 + local max = options.max or 1 + local good = false + if type(arg) == "number" then + if options.isPercent then + arg = arg / 100 + end + + if arg >= min and arg <= max then + good = true + end + + if good and type(options.step) == "number" and options.step > 0 then + local step = options.step + arg = math.floor((arg - min) / step + 0.5) * step + min + if arg > max then + arg = max + elseif arg < min then + arg = min + end + end + end + if not good then + local usage + local min = options.min or 0 + local max = options.max or 1 + if options.isPercent then + min, max = min * 100, max * 100 + end + local bit = "-" + if min < 0 or max < 0 then + bit = " - " + end + usage = string.format("(%s%s%s)", min, bit, max) + print(string.format(options.error or IS_NOT_A_VALID_VALUE_FOR, tostring(arg), path), realOptions.cmdName or realOptions.name or self) + print(string.format("|cffffff7f%s:|r %s %s", USAGE, path, usage)) + return + end + + local var + if passTable then + if type(passTable.get) == "function" then + var = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + else + if type(options.get) == "function" then + var = options.get() + else + local handler = options.handler or self + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + end + + if var ~= arg then + if passTable then + if type(passTable.set) == "function" then + passTable.set(passValue, arg) + else + if type(handler[passTable.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.set)) + end + handler[passTable.set](handler, passValue, arg) + end + else + if type(options.set) == "function" then + options.set(arg) + else + local handler = options.handler or self + if type(handler[options.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.set)) + end + handler[options.set](handler, arg) + end + end + end + end + + if arg then + local var + if passTable then + if type(passTable.get) == "function" then + var = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + var = handler[passTable.get](handler, passValue) + end + else + if type(options.get) == "function" then + var = options.get() + else + local handler = options.handler or self + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + var = handler[options.get](handler) + end + end + + if var and options.isPercent then + var = tostring(var * 100) .. "%" + end + print(string.format(options.message or IS_NOW_SET_TO, tostring(options.cmdName or options.name), tostring(var or NONE)), realOptions.cmdName or realOptions.name or self) + if var == arg then + return + end + else + printUsage(self, handler, realOptions, options, path, args) + return + end + elseif kind == "color" then + if table.getn(args) > 0 then + local r,g,b,a + if table.getn(args) == 1 then + local arg = tostring(args[1]) + if options.hasAlpha then + if string.len(arg) == 8 and string.find(arg, "^%x*$") then + r,g,b,a = tonumber(string.sub(arg, 1, 2), 16) / 255, tonumber(string.sub(arg, 3, 4), 16) / 255, tonumber(string.sub(arg, 5, 6), 16) / 255, tonumber(string.sub(arg, 7, 8), 16) / 255 + end + else + if string.len(arg) == 6 and string.find(arg, "^%x*$") then + r,g,b = tonumber(string.sub(arg, 1, 2), 16) / 255, tonumber(string.sub(arg, 3, 4), 16) / 255, tonumber(string.sub(arg, 5, 6), 16) / 255 + end + end + elseif table.getn(args) == 4 and options.hasAlpha then + local a1,a2,a3,a4 = args[1], args[2], args[3], args[4] + if type(a1) == "number" and type(a2) == "number" and type(a3) == "number" and type(a4) == "number" and a1 <= 1 and a2 <= 1 and a3 <= 1 and a4 <= 1 then + r,g,b,a = a1,a2,a3,a4 + elseif (type(a1) == "number" or string.len(a1) == 2) and string.find(a1, "^%x*$") and (type(a2) == "number" or string.len(a2) == 2) and string.find(a2, "^%x*$") and (type(a3) == "number" or string.len(a3) == 2) and string.find(a3, "^%x*$") and (type(a4) == "number" or string.len(a4) == 2) and string.find(a4, "^%x*$") then + r,g,b,a = tonumber(a1, 16) / 255, tonumber(a2, 16) / 255, tonumber(a3, 16) / 255, tonumber(a4, 16) / 255 + end + elseif table.getn(args) == 3 and not options.hasAlpha then + local a1,a2,a3 = args[1], args[2], args[3] + if type(a1) == "number" and type(a2) == "number" and type(a3) == "number" and a1 <= 1 and a2 <= 1 and a3 <= 1 then + r,g,b = a1,a2,a3 + elseif (type(a1) == "number" or string.len(a1) == 2) and string.find(a1, "^%x*$") and (type(a2) == "number" or string.len(a2) == 2) and string.find(a2, "^%x*$") and (type(a3) == "number" or string.len(a3) == 2) and string.find(a3, "^%x*$") then + r,g,b = tonumber(a1, 16) / 255, tonumber(a2, 16) / 255, tonumber(a3, 16) / 255 + end + end + if not r then + print(string.format(options.error or IS_NOT_A_VALID_OPTION_FOR, table.concat(args, ' '), path), realOptions.cmdName or realOptions.name or self) + print(string.format("|cffffff7f%s:|r %s {0-1} {0-1} {0-1}%s", USAGE, path, options.hasAlpha and " {0-1}" or "")) + return + end + if passTable then + if type(passTable.set) == "function" then + passTable.set(passValue, r,g,b,a) + else + if type(handler[passTable.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.set)) + end + handler[passTable.set](handler, passValue, r,g,b,a) + end + else + if type(options.set) == "function" then + options.set(r,g,b,a) + else + if type(handler[options.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.set)) + end + handler[options.set](handler, r,g,b,a) + end + end + + local r,g,b,a + if passTable then + if type(passTable.get) == "function" then + r,g,b,a = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + r,g,b,a = handler[passTable.get](handler, passValue) + end + else + if type(options.get) == "function" then + r,g,b,a = options.get() + else + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + r,g,b,a = handler[options.get](handler) + end + end + + local s + if type(r) == "number" and type(g) == "number" and type(b) == "number" then + if options.hasAlpha and type(a) == "number" then + s = string.format("|c%02x%02x%02x%02x%02x%02x%02x%02x|r", a*255, r*255, g*255, b*255, r*255, g*255, b*255, a*255) + else + s = string.format("|cff%02x%02x%02x%02x%02x%02x|r", r*255, g*255, b*255, r*255, g*255, b*255) + end + else + s = NONE + end + print(string.format(options.message or IS_NOW_SET_TO, tostring(options.cmdName or options.name), s), realOptions.cmdName or realOptions.name or self) + else + local r,g,b,a + if passTable then + if type(passTable.get) == "function" then + r,g,b,a = passTable.get(passValue) + else + if type(handler[passTable.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, passTable.get)) + end + r,g,b,a = handler[passTable.get](handler, passValue) + end + else + if type(options.get) == "function" then + r,g,b,a = options.get() + else + if type(handler[options.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, options.get)) + end + r,g,b,a = handler[options.get](handler) + end + end + + if not colorTable then + colorTable = {} + local t = colorTable + + if ColorPickerOkayButton then + local ColorPickerOkayButton_OnClick = ColorPickerOkayButton:GetScript("OnClick") + ColorPickerOkayButton:SetScript("OnClick", function() + if ColorPickerOkayButton_OnClick then + ColorPickerOkayButton_OnClick() + end + if t.active then + ColorPickerFrame.cancelFunc = nil + ColorPickerFrame.func = nil + ColorPickerFrame.opacityFunc = nil + local r,g,b,a + if t.passValue then + if type(t.get) == "function" then + r,g,b,a = t.get(t.passValue) + else + if type(t.handler[t.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, t.get)) + end + r,g,b,a = t.handler[t.get](t.handler, t.passValue) + end + else + if type(t.get) == "function" then + r,g,b,a = t.get() + else + if type(t.handler[t.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, t.get)) + end + r,g,b,a = t.handler[t.get](t.handler) + end + end + if r ~= t.r or g ~= t.g or b ~= t.b or (t.hasAlpha and a ~= t.a) then + local s + if type(r) == "number" and type(g) == "number" and type(b) == "number" then + if t.hasAlpha and type(a) == "number" then + s = string.format("|c%02x%02x%02x%02x%02x%02x%02x%02x|r", a*255, r*255, g*255, b*255, r*255, g*255, b*255, a*255) + else + s = string.format("|cff%02x%02x%02x%02x%02x%02x|r", r*255, g*255, b*255, r*255, g*255, b*255) + end + else + s = NONE + end + print(string.format(t.message, tostring(t.name), s), t.realOptions.cmdName or t.realOptions.name or self) + end + for k,v in pairs(t) do + t[k] = nil + end + end + end) + end + else + for k,v in pairs(colorTable) do + colorTable[k] = nil + end + end + + if type(r) ~= "number" or type(g) ~= "number" or type(b) ~= "number" then + r,g,b = 1, 1, 1 + end + if type(a) ~= "number" then + a = 1 + end + local t = colorTable + t.r = r + t.g = g + t.b = b + if hasAlpha then + t.a = a + end + t.realOptions = realOptions + t.hasAlpha = options.hasAlpha + t.handler = handler + t.set = passTable and passTable.set or options.set + t.get = passTable and passTable.get or options.get + t.name = options.cmdName or options.name + t.message = options.message or IS_NOW_SET_TO + t.passValue = passValue + t.active = true + + if not colorFunc then + colorFunc = function() + local r,g,b = ColorPickerFrame:GetColorRGB() + if t.hasAlpha then + local a = 1 - OpacitySliderFrame:GetValue() + if type(t.set) == "function" then + if t.passValue then + t.set(t.passValue, r,g,b,a) + else + t.set(r,g,b,a) + end + else + if type(t.handler[t.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, t.set)) + end + if t.passValue then + t.handler[t.set](t.handler, t.passValue, r,g,b,a) + else + t.handler[t.set](t.handler, r,g,b,a) + end + end + else + if type(t.set) == "function" then + if t.passValue then + t.set(t.passValue, r,g,b) + else + t.set(r,g,b) + end + else + if type(t.handler[t.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, t.set)) + end + if t.passValue then + t.handler[t.set](t.handler, t.passValue, r,g,b) + else + t.handler[t.set](t.handler, r,g,b) + end + end + end + end + end + + ColorPickerFrame.func = colorFunc + ColorPickerFrame.hasOpacity = options.hasAlpha + if options.hasAlpha then + ColorPickerFrame.opacityFunc = ColorPickerFrame.func + ColorPickerFrame.opacity = 1 - a + end + ColorPickerFrame:SetColorRGB(r,g,b) + + if not colorCancelFunc then + colorCancelFunc = function() + if t.hasAlpha then + if type(t.set) == "function" then + if t.passValue then + t.set(t.passValue, t.r,t.g,t.b,t.a) + else + t.set(t.r,t.g,t.b,t.a) + end + else + if type(t.handler[t.get]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, t.get)) + end + if t.passValue then + t.handler[t.set](t.handler, t.passValue, t.r,t.g,t.b,t.a) + else + t.handler[t.set](t.handler, t.r,t.g,t.b,t.a) + end + end + else + if type(t.set) == "function" then + if t.passValue then + t.set(t.passValue, t.r,t.g,t.b) + else + t.set(t.r,t.g,t.b) + end + else + if type(t.handler[t.set]) ~= "function" then + AceConsole:error("%s: %s", tostring(handler), string.format(OPTION_HANDLER_NOT_FOUND, t.set)) + end + if t.passValue then + t.handler[t.set](t.handler, t.passValue, t.r,t.g,t.b) + else + t.handler[t.set](t.handler, t.r,t.g,t.b) + end + end + end + for k,v in pairs(t) do + t[k] = nil + end + ColorPickerFrame.cancelFunc = nil + ColorPickerFrame.func = nil + ColorPickerFrame.opacityFunc = nil + end + end + + ColorPickerFrame.cancelFunc = colorCancelFunc + + ShowUIPanel(ColorPickerFrame) + end + return + elseif kind == "group" then + if table.getn(args) == 0 then + printUsage(self, handler, realOptions, options, path, args) + else + -- invalid argument + print(string.format(options.error or IS_NOT_A_VALID_OPTION_FOR, args[1], path), realOptions.cmdName or realOptions.name or self) + end + return + end + this = _G_this + if Dewdrop then + Dewdrop:Refresh(1) + Dewdrop:Refresh(2) + Dewdrop:Refresh(3) + Dewdrop:Refresh(4) + Dewdrop:Refresh(5) + end +end + +local external +function AceConsole:RegisterChatCommand(slashCommands, options, name) + if type(slashCommands) ~= "table" and slashCommands ~= false then + AceConsole:error("Bad argument #2 to `RegisterChatCommand' (expected table, got %s)", type(slashCommands)) + end + if not slashCommands and type(name) ~= "string" then + AceConsole:error("Bad argument #4 to `RegisterChatCommand' (expected string, got %s)", type(name)) + end + if type(options) ~= "table" and type(options) ~= "function" and options ~= nil then + AceConsole:error("Bad argument #3 to `RegisterChatCommand' (expected table, function, or nil, got %s)", type(options)) + end + if name then + if type(name) ~= "string" then + AceConsole:error("Bad argument #4 to `RegisterChatCommand' (expected string or nil, got %s)", type(name)) + elseif not string.find(name, "^%w+$") or string.upper(name) ~= name or string.len(name) == 0 then + AceConsole:error("Argument #4 must be an uppercase, letters-only string with at least 1 character") + end + end + if slashCommands then + if table.getn(slashCommands) == 0 then + AceConsole:error("Argument #2 to `RegisterChatCommand' must include at least one string") + end + + for k,v in pairs(slashCommands) do + if type(k) ~= "number" then + AceConsole:error("All keys in argument #2 to `RegisterChatCommand' must be numbers") + end + if type(v) ~= "string" then + AceConsole:error("All values in argument #2 to `RegisterChatCommand' must be strings") + elseif not string.find(v, "^/[A-Za-z][A-Za-z0-9_]*$") then + AceConsole:error("All values in argument #2 to `RegisterChatCommand' must be in the form of \"/word\"") + end + end + end + + if not options then + options = { + type = 'group', + args = {}, + handler = self + } + end + + if type(options) == "table" then + local err, position = validateOptions(options) + if err then + if position then + AceConsole:error(position .. ": " .. err) + else + AceConsole:error(err) + end + end + + if not options.handler then + options.handler = self + end + + if options.handler == self and string.lower(options.type) == "group" and self.class then + AceConsole:InjectAceOptionsTable(self, options) + end + end + + local chat + if slashCommands then + chat = slashCommands[1] + else + chat = _G["SLASH_"..name..1] + end + + local handler + if type(options) == "function" then + handler = options + for k,v in pairs(_G) do + if handler == v then + local k = k + handler = function(msg) + return _G[k](msg) + end + end + end + else + function handler(msg) + handlerFunc(self, chat, msg, options) + end + end + + if not _G.SlashCmdList then + _G.SlashCmdList = {} + end + + if not name then + repeat + name = string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) .. string.char(math.random(26) + string.byte('A') - 1) + until not _G.SlashCmdList[name] + end + + if slashCommands then + if _G.SlashCmdList[name] then + local i = 0 + while true do + i = i + 1 + if _G["SLASH_"..name..i] then + _G["SLASH_"..name..i] = nil + else + break + end + end + end + + local i = 0 + for _,command in ipairs(slashCommands) do + i = i + 1 + _G["SLASH_"..name..i] = command + if string.lower(command) ~= command then + i = i + 1 + _G["SLASH_"..name..i] = string.lower(command) + end + end + end + _G.SlashCmdList[name] = handler + if self ~= AceConsole and self.slashCommand == nil then + self.slashCommand = chat + end + + if not AceEvent and AceLibrary:HasInstance("AceEvent-2.0") then + external(AceConsole, "AceEvent-2.0", AceLibrary("AceEvent-2.0")) + end + if AceEvent then + if not AceConsole.nextAddon then + AceConsole.nextAddon = {} + end + if type(options) == "table" then + AceConsole.nextAddon[self] = options + if not self.playerLogin then + AceConsole:RegisterEvent("PLAYER_LOGIN", "PLAYER_LOGIN", true) + end + end + end + + AceConsole.registry[name] = options +end + +function AceConsole:InjectAceOptionsTable(handler, options) + self:argCheck(handler, 2, "table") + self:argCheck(options, 3, "table") + if string.lower(options.type) ~= "group" then + self:error('Cannot inject into options table argument #3 if its type is not "group"') + end + if options.handler ~= nil and options.handler ~= handler then + self:error("Cannot inject into options table argument #3 if it has a different handler than argument #2") + end + options.handler = handler + local class = handler.class + if not class then + self:error("Cannot retrieve AceOptions tables from a non-object argument #2") + end + while class and class ~= AceOO.Class do + if type(class.GetAceOptionsDataTable) == "function" then + local t = class:GetAceOptionsDataTable(handler) + for k,v in pairs(t) do + if type(options.args) ~= "table" then + options.args = {} + end + if options.args[k] == nil then + options.args[k] = v + end + end + end + local mixins = class.mixins + if mixins then + for mixin in pairs(mixins) do + if type(mixin.GetAceOptionsDataTable) == "function" then + local t = mixin:GetAceOptionsDataTable(handler) + for k,v in pairs(t) do + if type(options.args) ~= "table" then + options.args = {} + end + if options.args[k] == nil then + options.args[k] = v + end + end + end + end + end + class = class.super + end + return options +end + +function AceConsole:PLAYER_LOGIN() + self.playerLogin = true + for addon, options in pairs(self.nextAddon) do + local err, position = validateOptionsMethods(addon, options) + if err then + if position then + error(tostring(addon) .. ": AceConsole: " .. position .. ": " .. err) + else + error(tostring(addon) .. ": AceConsole: " .. err) + end + end + self.nextAddon[addon] = nil + end + + self:RegisterChatCommand({ "/reload", "/rl", "/reloadui" }, ReloadUI, "RELOAD") + + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + self:RegisterChatCommand({ "/print" }, function(text) + RunScript("local function func(...) local arg = {...}; for k = 1,select('#', ...) do arg[k] = tostring(arg[k]) end DEFAULT_CHAT_FRAME:AddMessage(table.concat(arg, ' ')) end func(" .. text .. ")") + end, "PRINT") + else + self:RegisterChatCommand({ "/print" }, function(text) + RunScript("local function func(...) for k = 1,table.getn(arg) do arg[k] = tostring(arg[k]) end DEFAULT_CHAT_FRAME:AddMessage(table.concat(arg, ' ')) end func(" .. text .. ")") + end, "PRINT") + end +end + +function AceConsole:TabCompleteInfo(cmdpath) + local _, _, cmd = string.find(cmdpath, "(/%S+)") + if not cmd then + return + end + local path = string.sub(cmdpath, string.len(cmd) + 2) + for name in pairs(SlashCmdList) do --global + if AceConsole.registry[name] then + local i = 0 + while true do + i = i + 1 + local scmd = _G["SLASH_"..name..i] + if not scmd then break end + if cmd == scmd then + return name, cmd, path + end + end + end + end +end + +function external(self, major, instance) + if major == "AceEvent-2.0" then + if not AceEvent then + AceEvent = instance + + AceEvent:embed(self) + end + elseif major == "AceTab-2.0" then + instance:RegisterTabCompletion("AceConsole", "%/.*", function(t, cmdpath, pos) + local ac = AceLibrary("AceConsole-2.0") + local name, cmd, path = ac:TabCompleteInfo(string.sub(cmdpath, 1, pos)) + + if not ac.registry[name] then + return false + else + local validArgs = findTableLevel(ac, ac.registry[name], cmd, path or "") + if validArgs.args then + for arg in pairs(validArgs.args) do + table.insert(t, arg) + end + end + end + end, function(u, matches, gcs, cmdpath) + local ac = AceLibrary("AceConsole-2.0") + local name, cmd, path = ac:TabCompleteInfo(cmdpath) + if ac.registry[name] then + local validArgs, path2, argwork = findTableLevel(ac, ac.registry[name], cmd, path) + printUsage(ac, validArgs.handler, ac.registry[name], validArgs, path2, argwork, not gcs or gcs ~= "", gcs) + end + end) + elseif major == "Dewdrop-2.0" then + Dewdrop = instance + end +end + +local function activate(self, oldLib, oldDeactivate) + AceConsole = self + + self.super.activate(self, oldLib, oldDeactivate) + + if oldLib then + self.registry = oldLib.registry + self.nextAddon = oldLib.nextAddon + end + if not self.registry then + self.registry = {} + else + for name,options in pairs(self.registry) do + self:RegisterChatCommand(false, options, name) + end + end + + if oldDeactivate then + oldDeactivate(oldLib) + end +end + +AceLibrary:Register(AceConsole, MAJOR_VERSION, MINOR_VERSION, activate, nil, external) +AceConsole = AceLibrary(MAJOR_VERSION) diff --git a/libs/ace/AceDebug-2.0/AceDebug-2.0.lua b/libs/ace/AceDebug-2.0/AceDebug-2.0.lua new file mode 100644 index 0000000..b4cb266 --- /dev/null +++ b/libs/ace/AceDebug-2.0/AceDebug-2.0.lua @@ -0,0 +1,242 @@ +--[[ + Name: AceDebug-2.0 + Revision: $Rev: 99999 $ + Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) + Inspired By: Ace 1.x by Turan (turan@gryphon.com) + Website: http://www.wowace.com/ + Documentation: http://www.wowace.com/index.php/AceDebug-2.0 + SVN: http://svn.wowace.com/root/trunk/Ace2/AceDebug-2.0 + Description: Mixin to allow for simple debugging capabilities. + Dependencies: AceLibrary, AceOO-2.0 +]] + +local MAJOR_VERSION = "AceDebug-2.0" +local MINOR_VERSION = "$Revision: 99999 $" + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end + +local DEBUGGING, TOGGLE_DEBUGGING + +if GetLocale() == "frFR" then + DEBUGGING = "D\195\169boguage" + TOGGLE_DEBUGGING = "Activer/d\195\169sactiver le d\195\169boguage" +elseif GetLocale() == "deDE" then + DEBUGGING = "Debuggen" + TOGGLE_DEBUGGING = "Aktiviert/Deaktiviert Debugging" +elseif GetLocale() == "koKR" then + DEBUGGING = "디버깅" + TOGGLE_DEBUGGING = "디버깅 기능 사용함/사용안함" +elseif GetLocale() == "zhTW" then + DEBUGGING = "除錯" + TOGGLE_DEBUGGING = "啟用/停用除錯功能" +elseif GetLocale() == "zhCN" then + DEBUGGING = "\232\176\131\232\175\149" + TOGGLE_DEBUGGING = "\229\144\175\231\148\168/\231\166\129\231\148\168 \232\176\131\232\175\149" +elseif GetLocale() == "ruRU" then + DEBUGGING = "Отладка" + TOGGLE_DEBUGGING = "Вкл./Выкл. отладку для этого аддона." +else -- enUS + DEBUGGING = "Debugging" + TOGGLE_DEBUGGING = "Enable/disable debugging" +end + +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +local math_mod = math.mod or math.fmod + +local AceOO = AceLibrary:GetInstance("AceOO-2.0") +local AceDebug = AceOO.Mixin {"Debug", "CustomDebug", "IsDebugging", "SetDebugging", "SetDebugLevel", "LevelDebug", "CustomLevelDebug", "GetDebugLevel"} + +local function print(text, r, g, b, frame, delay) + (frame or DEFAULT_CHAT_FRAME):AddMessage(text, r, g, b, 1, delay or 5) +end + +local tmp + +function AceDebug:CustomDebug(r, g, b, frame, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if not self.debugging then return end + + local output = string.format("|cff7fff7f(DEBUG) %s:[%s%3d]|r", tostring(self), date("%H:%M:%S"), math_mod(GetTime(), 1) * 1000) + + if string.find(tostring(a1), "%%") then + output = output .. " " .. string.format(tostring(a1), tostring(a2), tostring(a3), tostring(a4), tostring(a5), tostring(a6), tostring(a7), tostring(a8), tostring(a9), tostring(a10), tostring(a11), tostring(a12), tostring(a13), tostring(a14), tostring(a15), tostring(a16), tostring(a17), tostring(a18), tostring(a19), tostring(a20)) + else + if not tmp then + tmp = {} + end + + -- This block dynamically rebuilds the tmp array stopping on the first nil. + table.insert(tmp, output) + + table.insert(tmp, tostring(a1)) + table.insert(tmp, a2) + table.insert(tmp, a3) + table.insert(tmp, a4) + table.insert(tmp, a5) + table.insert(tmp, a6) + table.insert(tmp, a7) + table.insert(tmp, a8) + table.insert(tmp, a9) + table.insert(tmp, a10) + table.insert(tmp, a11) + table.insert(tmp, a12) + table.insert(tmp, a13) + table.insert(tmp, a14) + table.insert(tmp, a15) + table.insert(tmp, a16) + table.insert(tmp, a17) + table.insert(tmp, a18) + table.insert(tmp, a19) + table.insert(tmp, a20) + while tmp[table.getn(tmp)] == nil do + table.remove(tmp) + end + for k = 1, table.getn(tmp) do + tmp[k] = tostring(tmp[k]) + end + + output = table.concat(tmp, " ") + + for k,v in pairs(tmp) do + tmp[k] = nil + end + table_setn(tmp, 0) + end + + print(output, r, g, b, frame or self.debugFrame, delay) +end + +function AceDebug:Debug(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + AceDebug.CustomDebug(self, nil, nil, nil, nil, nil, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) +end + +function AceDebug:IsDebugging() + return self.debugging +end + +function AceDebug:SetDebugging(debugging) + self.debugging = debugging +end + +-- Takes a number 1-3 +-- Level 1: Critical messages that every user should receive +-- Level 2: Should be used for local debugging (function calls, etc) +-- Level 3: Very verbose debugging, will dump everything and anything +-- If set to nil, you will receive no debug information +function AceDebug:SetDebugLevel(level) + AceDebug:argCheck(level, 1, "number", "nil") + if not level then + self.debuglevel = nil + return + end + if level < 1 or level > 3 then + AceDebug:error("Bad argument #1 to `SetDebugLevel`, must be a number 1-3") + end + self.debuglevel = level +end + +function AceDebug:GetDebugLevel() + return self.debuglevel +end + +function AceDebug:CustomLevelDebug(level, r, g, b, frame, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if not self.debugging or not self.debuglevel then return end + AceDebug:argCheck(level, 1, "number") + if level < 1 or level > 3 then + AceDebug:error("Bad argument #1 to `LevelDebug`, must be a number 1-3") + end + if level > self.debuglevel then return end + + local output = string.format("|cff7fff7f(DEBUG) %s:[%s.%3d]|r", tostring(self), date("%H:%M:%S"), math_mod(GetTime(), 1) * 1000) + + if string.find(tostring(a1), "%%") then + output = output .. " " .. string.format(tostring(a1), tostring(a2), tostring(a3), tostring(a4), tostring(a5), tostring(a6), tostring(a7), tostring(a8), tostring(a9), tostring(a10), tostring(a11), tostring(a12), tostring(a13), tostring(a14), tostring(a15), tostring(a16), tostring(a17), tostring(a18), tostring(a19), tostring(a20)) + else + if not tmp then + tmp = {} + end + + -- This block dynamically rebuilds the tmp array stopping on the first nil. + table.insert(tmp, output) + + table.insert(tmp, tostring(a1)) + table.insert(tmp, a2) + table.insert(tmp, a3) + table.insert(tmp, a4) + table.insert(tmp, a5) + table.insert(tmp, a6) + table.insert(tmp, a7) + table.insert(tmp, a8) + table.insert(tmp, a9) + table.insert(tmp, a10) + table.insert(tmp, a11) + table.insert(tmp, a12) + table.insert(tmp, a13) + table.insert(tmp, a14) + table.insert(tmp, a15) + table.insert(tmp, a16) + table.insert(tmp, a17) + table.insert(tmp, a18) + table.insert(tmp, a19) + table.insert(tmp, a20) + while tmp[table.getn(tmp)] == nil do + table.remove(tmp) + end + for k = 1, table.getn(tmp) do + tmp[k] = tostring(tmp[k]) + end + + output = table.concat(tmp, " ") + + for k,v in pairs(tmp) do + tmp[k] = nil + end + table_setn(tmp, 0) + end + + print(output, r, g, b, frame or self.debugFrame, delay) +end + +function AceDebug:LevelDebug(level, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if not self.debugging or not self.debuglevel then return end + AceDebug:argCheck(level, 1, "number") + if level < 1 or level > 3 then + AceDebug:error("Bad argument #1 to `LevelDebug`, must be a number 1-3") + end + if level > self.debuglevel then return end + + AceDebug.CustomLevelDebug(self, level, nil, nil, nil, nil, nil, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) +end + + +local options +function AceDebug:GetAceOptionsDataTable(target) + if not options then + options = { + debug = { + name = DEBUGGING, + desc = TOGGLE_DEBUGGING, + type = "toggle", + get = "IsDebugging", + set = "SetDebugging", + order = -2, + } + } + end + return options +end +AceLibrary:Register(AceDebug, MAJOR_VERSION, MINOR_VERSION, AceDebug.activate) +AceDebug = AceLibrary(MAJOR_VERSION) \ No newline at end of file diff --git a/libs/ace/AceEvent-2.0/AceEvent-2.0.lua b/libs/ace/AceEvent-2.0/AceEvent-2.0.lua new file mode 100644 index 0000000..e0e71d7 --- /dev/null +++ b/libs/ace/AceEvent-2.0/AceEvent-2.0.lua @@ -0,0 +1,970 @@ +--[[ +Name: AceEvent-2.0 +Revision: $Rev: 17638 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Ace 1.x by Turan (turan@gryphon.com) +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceEvent-2.0 +SVN: http://svn.wowace.com/root/trunk/Ace2/AceEvent-2.0 +Description: Mixin to allow for event handling, scheduling, and inter-addon + communication. +Dependencies: AceLibrary, AceOO-2.0 +]] + +local MAJOR_VERSION = "AceEvent-2.0" +local MINOR_VERSION = "$Revision: 17638 $" + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end + +local AceOO = AceLibrary:GetInstance("AceOO-2.0") +local Mixin = AceOO.Mixin +local AceEvent = Mixin { + "RegisterEvent", + "RegisterAllEvents", + "UnregisterEvent", + "UnregisterAllEvents", + "TriggerEvent", + "ScheduleEvent", + "ScheduleRepeatingEvent", + "CancelScheduledEvent", + "CancelAllScheduledEvents", + "IsEventRegistered", + "IsEventScheduled", + "RegisterBucketEvent", + "UnregisterBucketEvent", + "UnregisterAllBucketEvents", + "IsBucketEventRegistered", + } + +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +local weakKey = {__mode="k"} +local new, del +do + local list = setmetatable({}, weakKey) + function new() + local t = next(list) + if t then + list[t] = nil + return t + else + return {} + end + end + + function del(t) + setmetatable(t, nil) + for k in pairs(t) do + t[k] = nil + end + list[t] = true + end +end + +local FAKE_NIL +local RATE + +local eventsWhichHappenOnce = { + PLAYER_LOGIN = true, + AceEvent_FullyInitialized = true, + VARIABLES_LOADED = true, + PLAYER_LOGOUT = true, +} + +local registeringFromAceEvent +function AceEvent:RegisterEvent(event, method, once) + AceEvent:argCheck(event, 2, "string") + if self == AceEvent and not registeringFromAceEvent then + AceEvent:argCheck(method, 3, "function") + self = method + else + AceEvent:argCheck(method, 3, "string", "function", "nil", "boolean", "number") + if type(method) == "boolean" or type(method) == "number" then + AceEvent:argCheck(once, 4, "nil") + once, method = method, event + end + end + AceEvent:argCheck(once, 4, "number", "boolean", "nil") + if eventsWhichHappenOnce[event] then + once = true + end + local throttleRate + if type(once) == "number" then + throttleRate, once = once + end + if not method then + method = event + end + if type(method) == "string" and type(self[method]) ~= "function" then + AceEvent:error("Cannot register event %q to method %q, it does not exist", event, method) + else + assert(type(method) == "function" or type(method) == "string") + end + + local AceEvent_registry = AceEvent.registry + if not AceEvent_registry[event] then + AceEvent_registry[event] = new() + AceEvent.frame:RegisterEvent(event) + end + + local remember = true + if AceEvent_registry[event][self] then + remember = false + end + AceEvent_registry[event][self] = method + + local AceEvent_onceRegistry = AceEvent.onceRegistry + if once then + if not AceEvent_onceRegistry then + AceEvent.onceRegistry = new() + AceEvent_onceRegistry = AceEvent.onceRegistry + end + if not AceEvent_onceRegistry[event] then + AceEvent_onceRegistry[event] = new() + end + AceEvent_onceRegistry[event][self] = true + else + if AceEvent_onceRegistry and AceEvent_onceRegistry[event] then + AceEvent_onceRegistry[event][self] = nil + if not next(AceEvent_onceRegistry[event]) then + AceEvent_onceRegistry[event] = del(AceEvent_onceRegistry[event]) + end + end + end + + local AceEvent_throttleRegistry = AceEvent.throttleRegistry + if throttleRate then + if not AceEvent_throttleRegistry then + AceEvent.throttleRegistry = new() + AceEvent_throttleRegistry = AceEvent.throttleRegistry + end + if not AceEvent_throttleRegistry[event] then + AceEvent_throttleRegistry[event] = new() + end + if AceEvent_throttleRegistry[event][self] then + AceEvent_throttleRegistry[event][self] = del(AceEvent_throttleRegistry[event][self]) + end + AceEvent_throttleRegistry[event][self] = setmetatable(new(), weakKey) + local t = AceEvent_throttleRegistry[event][self] + t[RATE] = throttleRate + else + if AceEvent_throttleRegistry and AceEvent_throttleRegistry[event] then + if AceEvent_throttleRegistry[event][self] then + AceEvent_throttleRegistry[event][self] = del(AceEvent_throttleRegistry[event][self]) + end + if not next(AceEvent_throttleRegistry[event]) then + AceEvent_throttleRegistry[event] = del(AceEvent_throttleRegistry[event]) + end + end + end + + if remember then + AceEvent:TriggerEvent("AceEvent_EventRegistered", self, event) + end +end + +local ALL_EVENTS + +function AceEvent:RegisterAllEvents(method) + if self == AceEvent then + AceEvent:argCheck(method, 1, "function") + self = method + else + AceEvent:argCheck(method, 1, "string", "function") + if type(method) == "string" and type(self[method]) ~= "function" then + AceEvent:error("Cannot register all events to method %q, it does not exist", method) + end + end + + local AceEvent_registry = AceEvent.registry + if not AceEvent_registry[ALL_EVENTS] then + AceEvent_registry[ALL_EVENTS] = new() + AceEvent.frame:RegisterAllEvents() + end + + AceEvent_registry[ALL_EVENTS][self] = method +end + +local _G = getfenv(0) +local memstack, timestack = {}, {} +local memdiff, timediff +function AceEvent:TriggerEvent(event, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + AceEvent:argCheck(event, 2, "string") + local AceEvent_registry = AceEvent.registry + if (not AceEvent_registry[event] or not next(AceEvent_registry[event])) and (not AceEvent_registry[ALL_EVENTS] or not next(AceEvent_registry[ALL_EVENTS])) then + return + end + local _G_event = _G.event + _G.event = event + local lastEvent = AceEvent.currentEvent + AceEvent.currentEvent = event + + local AceEvent_onceRegistry = AceEvent.onceRegistry + local AceEvent_debugTable = AceEvent.debugTable + if AceEvent_onceRegistry and AceEvent_onceRegistry[event] then + local tmp = new() + for obj, method in pairs(AceEvent_onceRegistry[event]) do + tmp[obj] = AceEvent_registry[event] and AceEvent_registry[event][obj] or nil + end + local obj = next(tmp) + while obj do + local mem, time + if AceEvent_debugTable then + if not AceEvent_debugTable[event] then + AceEvent_debugTable[event] = new() + end + if not AceEvent_debugTable[event][obj] then + AceEvent_debugTable[event][obj] = new() + AceEvent_debugTable[event][obj].mem = 0 + AceEvent_debugTable[event][obj].time = 0 + AceEvent_debugTable[event][obj].count = 0 + end + if memdiff then + table.insert(memstack, memdiff) + table.insert(timestack, timediff) + end + memdiff, timediff = 0, 0 + mem, time = gcinfo(), GetTime() + end + local method = tmp[obj] + AceEvent.UnregisterEvent(obj, event) + if type(method) == "string" then + local obj_method = obj[method] + if obj_method then + obj_method(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + end + elseif method then -- function + method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + end + if AceEvent_debugTable then + local dmem, dtime = memdiff, timediff + mem, time = gcinfo() - mem - memdiff, GetTime() - time - timediff + AceEvent_debugTable[event][obj].mem = AceEvent_debugTable[event][obj].mem + mem + AceEvent_debugTable[event][obj].time = AceEvent_debugTable[event][obj].time + time + AceEvent_debugTable[event][obj].count = AceEvent_debugTable[event][obj].count + 1 + + memdiff, timediff = table.remove(memstack), table.remove(timestack) + if memdiff then + memdiff = memdiff + mem + dmem + timediff = timediff + time + dtime + end + end + tmp[obj] = nil + obj = next(tmp) + end + del(tmp) + end + + local AceEvent_throttleRegistry = AceEvent.throttleRegistry + local throttleTable = AceEvent_throttleRegistry and AceEvent_throttleRegistry[event] + if AceEvent_registry[event] then + local tmp = new() + for obj, method in pairs(AceEvent_registry[event]) do + tmp[obj] = method + end + local obj = next(tmp) + while obj do + local method = tmp[obj] + local continue = false + if throttleTable and throttleTable[obj] then + local a1 = a1 + if a1 == nil then + a1 = FAKE_NIL + end + if not throttleTable[obj][a1] or GetTime() - throttleTable[obj][a1] >= throttleTable[obj][RATE] then + throttleTable[obj][a1] = GetTime() + else + continue = true + end + end + if not continue then + local mem, time + if AceEvent_debugTable then + if not AceEvent_debugTable[event] then + AceEvent_debugTable[event] = new() + end + if not AceEvent_debugTable[event][obj] then + AceEvent_debugTable[event][obj] = new() + AceEvent_debugTable[event][obj].mem = 0 + AceEvent_debugTable[event][obj].time = 0 + AceEvent_debugTable[event][obj].count = 0 + end + if memdiff then + table.insert(memstack, memdiff) + table.insert(timestack, timediff) + end + memdiff, timediff = 0, 0 + mem, time = gcinfo(), GetTime() + end + if type(method) == "string" then + local obj_method = obj[method] + if obj_method then + obj_method(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + end + elseif method then -- function + method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + end + if AceEvent_debugTable then + local dmem, dtime = memdiff, timediff + mem, time = gcinfo() - mem - memdiff, GetTime() - time - timediff + AceEvent_debugTable[event][obj].mem = AceEvent_debugTable[event][obj].mem + mem + AceEvent_debugTable[event][obj].time = AceEvent_debugTable[event][obj].time + time + AceEvent_debugTable[event][obj].count = AceEvent_debugTable[event][obj].count + 1 + + memdiff, timediff = table.remove(memstack), table.remove(timestack) + if memdiff then + memdiff = memdiff + mem + dmem + timediff = timediff + time + dtime + end + end + end + tmp[obj] = nil + obj = next(tmp) + end + del(tmp) + end + if AceEvent_registry[ALL_EVENTS] then + local tmp = new() + for obj, method in pairs(AceEvent_registry[ALL_EVENTS]) do + tmp[obj] = method + end + local obj = next(tmp) + while obj do + local method = tmp[obj] + local mem, time + if AceEvent_debugTable then + if not AceEvent_debugTable[event] then + AceEvent_debugTable[event] = new() + end + if not AceEvent_debugTable[event][obj] then + AceEvent_debugTable[event][obj] = new() + AceEvent_debugTable[event][obj].mem = 0 + AceEvent_debugTable[event][obj].time = 0 + AceEvent_debugTable[event][obj].count = 0 + end + if memdiff then + table.insert(memstack, memdiff) + table.insert(timestack, timediff) + end + memdiff, timediff = 0, 0 + mem, time = gcinfo(), GetTime() + end + if type(method) == "string" then + local obj_method = obj[method] + if obj_method then + obj_method(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + end + elseif method then -- function + method(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + end + if AceEvent_debugTable then + local dmem, dtime = memdiff, timediff + mem, time = gcinfo() - mem - memdiff, GetTime() - time - timediff + AceEvent_debugTable[event][obj].mem = AceEvent_debugTable[event][obj].mem + mem + AceEvent_debugTable[event][obj].time = AceEvent_debugTable[event][obj].time + time + AceEvent_debugTable[event][obj].count = AceEvent_debugTable[event][obj].count + 1 + + memdiff, timediff = table.remove(memstack), table.remove(timestack) + if memdiff then + memdiff = memdiff + mem + dmem + timediff = timediff + time + dtime + end + end + tmp[obj] = nil + obj = next(tmp) + end + del(tmp) + end + _G.event = _G_event + AceEvent.currentEvent = lastEvent +end + +-- local accessors +local getn = table.getn +local tinsert = table.insert +local tremove = table.remove +local floor = math.floor +local GetTime = GetTime +local next = next +local pairs = pairs +local unpack = unpack + +local delayRegistry +local tmp = {} +local function OnUpdate() + local t = GetTime() + for k,v in pairs(delayRegistry) do + tmp[k] = true + end + for k in pairs(tmp) do + local v = delayRegistry[k] + if v then + local v_time = v.time + if not v_time then + delayRegistry[k] = del(v) + elseif v_time <= t then + local v_repeatDelay = v.repeatDelay + if v_repeatDelay then + -- use the event time, not the current time, else timing inaccuracies add up over time + v.time = v_time + v_repeatDelay + end + local event = v.event + local mem, time + if AceEvent_debugTable then + mem, time = gcinfo(), GetTime() + end + if type(event) == "function" then + event(unpack(v)) + else + AceEvent:TriggerEvent(event, unpack(v)) + end + if AceEvent_debugTable then + mem, time = gcinfo() - mem, GetTime() - time + v.mem = v.mem + mem + v.timeSpent = v.timeSpent + time + v.count = v.count + 1 + end + if not v_repeatDelay then + local x = delayRegistry[k] + if x and x.time == v_time then -- check if it was manually reset + delayRegistry[k] = del(v) + end + end + end + end + end + for k in pairs(tmp) do + tmp[k] = nil + end + if not next(delayRegistry) then + AceEvent.frame:Hide() + end +end + +local function ScheduleEvent(self, repeating, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + local id + if type(event) == "string" or type(event) == "table" then + if type(event) == "table" then + if not delayRegistry or not delayRegistry[event] then + AceEvent:error("Bad argument #2 to `ScheduleEvent'. Improper id table fed in.") + end + end + if type(delay) ~= "number" then + id, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 = event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 + AceEvent:argCheck(event, 3, "string", "function", --[[ so message is right ]] "number") + AceEvent:argCheck(delay, 4, "number") + self:CancelScheduledEvent(id) + end + else + AceEvent:argCheck(event, 2, "string", "function") + AceEvent:argCheck(delay, 3, "number") + end + + if not delayRegistry then + AceEvent.delayRegistry = new() + delayRegistry = AceEvent.delayRegistry + AceEvent.frame:SetScript("OnUpdate", OnUpdate) + end + local t + if type(id) == "table" then + for k in pairs(id) do + id[k] = nil + end + t = id + else + t = new() + end + t[1] = a1 + t[2] = a2 + t[3] = a3 + t[4] = a4 + t[5] = a5 + t[6] = a6 + t[7] = a7 + t[8] = a8 + t[9] = a9 + t[10] = a10 + t[11] = a11 + t[12] = a12 + t[13] = a13 + t[14] = a14 + t[15] = a15 + t[16] = a16 + t[17] = a17 + t[18] = a18 + t[19] = a19 + t[20] = a20 + table_setn(t, 20) + t.event = event + t.time = GetTime() + delay + t.self = self + t.id = id or t + t.repeatDelay = repeating and delay + if AceEvent_debugTable then + t.mem = 0 + t.count = 0 + t.timeSpent = 0 + end + delayRegistry[t.id] = t + AceEvent.frame:Show() + return t.id +end + +function AceEvent:ScheduleEvent(event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if type(event) == "string" or type(event) == "table" then + if type(event) == "table" then + if not delayRegistry or not delayRegistry[event] then + AceEvent:error("Bad argument #2 to `ScheduleEvent'. Improper id table fed in.") + end + end + if type(delay) ~= "number" then + AceEvent:argCheck(delay, 3, "string", "function", --[[ so message is right ]] "number") + AceEvent:argCheck(a1, 4, "number") + end + else + AceEvent:argCheck(event, 2, "string", "function") + AceEvent:argCheck(delay, 3, "number") + end + + return ScheduleEvent(self, false, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) +end + +function AceEvent:ScheduleRepeatingEvent(event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if type(event) == "string" or type(event) == "table" then + if type(event) == "table" then + if not delayRegistry or not delayRegistry[event] then + AceEvent:error("Bad argument #2 to `ScheduleEvent'. Improper id table fed in.") + end + end + if type(delay) ~= "number" then + AceEvent:argCheck(delay, 3, "string", "function", --[[ so message is right ]] "number") + AceEvent:argCheck(a1, 4, "number") + end + else + AceEvent:argCheck(event, 2, "string", "function") + AceEvent:argCheck(delay, 3, "number") + end + + return ScheduleEvent(self, true, event, delay, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) +end + +function AceEvent:CancelScheduledEvent(t) + AceEvent:argCheck(t, 2, "string", "table") + if delayRegistry then + local v = delayRegistry[t] + if v then + delayRegistry[t] = del(v) + if not next(delayRegistry) then + AceEvent.frame:Hide() + end + return true + end + end + return false +end + +function AceEvent:IsEventScheduled(t) + AceEvent:argCheck(t, 2, "string", "table") + if delayRegistry then + local v = delayRegistry[t] + if v then + return true, v.time - GetTime() + end + end + return false, nil +end + +function AceEvent:UnregisterEvent(event) + AceEvent:argCheck(event, 2, "string") + local AceEvent_registry = AceEvent.registry + if AceEvent_registry[event] and AceEvent_registry[event][self] then + AceEvent_registry[event][self] = nil + local AceEvent_onceRegistry = AceEvent.onceRegistry + if AceEvent_onceRegistry and AceEvent_onceRegistry[event] and AceEvent_onceRegistry[event][self] then + AceEvent_onceRegistry[event][self] = nil + if not next(AceEvent_onceRegistry[event]) then + AceEvent_onceRegistry[event] = del(AceEvent_onceRegistry[event]) + end + end + local AceEvent_throttleRegistry = AceEvent.throttleRegistry + if AceEvent_throttleRegistry and AceEvent_throttleRegistry[event] and AceEvent_throttleRegistry[event][self] then + AceEvent_throttleRegistry[event][self] = del(AceEvent_throttleRegistry[event][self]) + if not next(AceEvent_throttleRegistry[event]) then + AceEvent_throttleRegistry[event] = del(AceEvent_throttleRegistry[event]) + end + end + if not next(AceEvent_registry[event]) then + AceEvent_registry[event] = del(AceEvent_registry[event]) + if not AceEvent_registry[ALL_EVENTS] or not next(AceEvent_registry[ALL_EVENTS]) then + AceEvent.frame:UnregisterEvent(event) + end + end + else + if self == AceEvent then + error(string.format("Cannot unregister event %q. Improperly unregistering from AceEvent-2.0.", event), 2) + else + AceEvent:error("Cannot unregister event %q. %q is not registered with it.", event, self) + end + end + AceEvent:TriggerEvent("AceEvent_EventUnregistered", self, event) +end + +function AceEvent:UnregisterAllEvents() + local AceEvent_registry = AceEvent.registry + if AceEvent_registry[ALL_EVENTS] and AceEvent_registry[ALL_EVENTS][self] then + AceEvent_registry[ALL_EVENTS][self] = nil + if not next(AceEvent_registry[ALL_EVENTS]) then + del(AceEvent_registry[ALL_EVENTS]) + AceEvent.frame:UnregisterAllEvents() + for k,v in pairs(AceEvent_registry) do + if k ~= ALL_EVENTS then + AceEvent.frame:RegisterEvent(k) + end + end + AceEvent_registry[event] = nil + end + end + local first = true + for event, data in pairs(AceEvent_registry) do + if first then + if AceEvent_registry.AceEvent_EventUnregistered then + event = "AceEvent_EventUnregistered" + else + first = false + end + end + local x = data[self] + data[self] = nil + if x and event ~= ALL_EVENTS then + if not next(data) then + del(data) + if not AceEvent_registry[ALL_EVENTS] or not next(AceEvent_registry[ALL_EVENTS]) then + AceEvent.frame:UnregisterEvent(event) + end + AceEvent_registry[event] = nil + end + AceEvent:TriggerEvent("AceEvent_EventUnregistered", self, event) + end + if first then + event = nil + end + end + if AceEvent.onceRegistry then + for event, data in pairs(AceEvent.onceRegistry) do + data[self] = nil + end + end +end + +function AceEvent:CancelAllScheduledEvents() + if delayRegistry then + for k,v in pairs(delayRegistry) do + if v.self == self then + delayRegistry[k] = del(v) + end + end + if not next(delayRegistry) then + AceEvent.frame:Hide() + end + end +end + +function AceEvent:IsEventRegistered(event) + AceEvent:argCheck(event, 2, "string") + local AceEvent_registry = AceEvent.registry + if self == AceEvent then + return AceEvent_registry[event] and next(AceEvent_registry[event]) and true or false + end + if AceEvent_registry[event] and AceEvent_registry[event][self] then + return true, AceEvent_registry[event][self] + end + return false, nil +end + +local bucketfunc +function AceEvent:RegisterBucketEvent(event, delay, method) + AceEvent:argCheck(event, 2, "string", "table") + if type(event) == "table" then + for k,v in pairs(event) do + if type(k) ~= "number" then + AceEvent:error("All keys to argument #2 to `RegisterBucketEvent' must be numbers.") + elseif type(v) ~= "string" then + AceEvent:error("All values to argument #2 to `RegisterBucketEvent' must be strings.") + end + end + end + AceEvent:argCheck(delay, 3, "number") + if AceEvent == self then + AceEvent:argCheck(method, 4, "function") + self = method + else + if type(event) == "string" then + AceEvent:argCheck(method, 4, "string", "function", "nil") + if not method then + method = event + end + else + AceEvent:argCheck(method, 4, "string", "function") + end + + if type(method) == "string" and type(self[method]) ~= "function" then + AceEvent:error("Cannot register event %q to method %q, it does not exist", event, method) + end + end + if not AceEvent.buckets then + AceEvent.buckets = new() + end + if not AceEvent.buckets[event] then + AceEvent.buckets[event] = new() + end + if not AceEvent.buckets[event][self] then + AceEvent.buckets[event][self] = new() + AceEvent.buckets[event][self].current = new() + AceEvent.buckets[event][self].self = self + else + AceEvent.CancelScheduledEvent(self, AceEvent.buckets[event][self].id) + end + local bucket = AceEvent.buckets[event][self] + bucket.method = method + + local func = function(arg1) + bucket.run = true + if arg1 then + bucket.current[arg1] = true + end + end + AceEvent.buckets[event][self].func = func + if type(event) == "string" then + AceEvent.RegisterEvent(self, event, func) + else + for _,v in ipairs(event) do + AceEvent.RegisterEvent(self, v, func) + end + end + if not bucketfunc then + bucketfunc = function(bucket) + local current = bucket.current + local method = bucket.method + local self = bucket.self + if bucket.run then + if type(method) == "string" then + self[method](self, current) + elseif method then -- function + method(current) + end + for k in pairs(current) do + current[k] = nil + k = nil + end + bucket.run = false + end + end + end + bucket.id = AceEvent.ScheduleRepeatingEvent(self, bucketfunc, delay, bucket) +end + +function AceEvent:IsBucketEventRegistered(event) + AceEvent:argCheck(event, 2, "string", "table") + return AceEvent.buckets and AceEvent.buckets[event] and AceEvent.buckets[event][self] +end + +function AceEvent:UnregisterBucketEvent(event) + AceEvent:argCheck(event, 2, "string", "table") + if not AceEvent.buckets or not AceEvent.buckets[event] or not AceEvent.buckets[event][self] then + AceEvent:error("Cannot unregister bucket event %q. %q is not registered with it.", event, self) + end + + local bucket = AceEvent.buckets[event][self] + + if type(event) == "string" then + AceEvent.UnregisterEvent(self, event) + else + for _,v in ipairs(event) do + AceEvent.UnregisterEvent(self, v) + end + end + AceEvent:CancelScheduledEvent(bucket.id) + + del(bucket.current) + AceEvent.buckets[event][self] = del(AceEvent.buckets[event][self]) + if not next(AceEvent.buckets[event]) then + AceEvent.buckets[event] = del(AceEvent.buckets[event]) + end +end + +function AceEvent:UnregisterAllBucketEvents() + if not AceEvent.buckets or not next(AceEvent.buckets) then + return + end + for k,v in pairs(AceEvent.buckets) do + if v == self then + AceEvent.UnregisterBucketEvent(self, k) + k = nil + end + end +end + +function AceEvent:OnEmbedDisable(target) + self.UnregisterAllEvents(target) + + self.CancelAllScheduledEvents(target) + + self.UnregisterAllBucketEvents(target) +end + +function AceEvent:EnableDebugging() + if not self.debugTable then + self.debugTable = new() + + if delayRegistry then + for k,v in pairs(self.delayRegistry) do + if not v.mem then + v.mem = 0 + v.count = 0 + v.timeSpent = 0 + end + end + end + end +end + +function AceEvent:IsFullyInitialized() + return self.postInit or false +end + +function AceEvent:IsPostPlayerLogin() + return self.playerLogin or false +end + +function AceEvent:activate(oldLib, oldDeactivate) + AceEvent = self + + if oldLib then + self.onceRegistry = oldLib.onceRegistry + self.throttleRegistry = oldLib.throttleRegistry + self.delayRegistry = oldLib.delayRegistry + self.buckets = oldLib.buckets + self.registry = oldLib.registry + self.frame = oldLib.frame + self.debugTable = oldLib.debugTable + self.playerLogin = oldLib.pew or DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.defaultLanguage and true + self.postInit = oldLib.postInit or self.playerLogin and ChatTypeInfo and ChatTypeInfo.WHISPER and ChatTypeInfo.WHISPER.r and true + self.ALL_EVENTS = oldLib.ALL_EVENTS + self.FAKE_NIL = oldLib.FAKE_NIL + self.RATE = oldLib.RATE + end + if not self.registry then + self.registry = {} + end + if not self.frame then + self.frame = CreateFrame("Frame", "AceEvent20Frame") + end + if not self.ALL_EVENTS then + self.ALL_EVENTS = {} + end + if not self.FAKE_NIL then + self.FAKE_NIL = {} + end + if not self.RATE then + self.RATE = {} + end + ALL_EVENTS = self.ALL_EVENTS + FAKE_NIL = self.FAKE_NIL + RATE = self.RATE + local inPlw = false + local blacklist = { + UNIT_INVENTORY_CHANGED = true, + BAG_UPDATE = true, + ITEM_LOCK_CHANGED = true, + ACTIONBAR_SLOT_CHANGED = true, + } + self.frame:SetScript("OnEvent", function() + local event = event + if event == "PLAYER_ENTERING_WORLD" then + inPlw = false + elseif event == "PLAYER_LEAVING_WORLD" then + inPlw = true + end + if event and (not inPlw or not blacklist[event]) then + self:TriggerEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) + end + end) + if self.delayRegistry then + delayRegistry = self.delayRegistry + self.frame:SetScript("OnUpdate", OnUpdate) + end + + self:UnregisterAllEvents() + self:CancelAllScheduledEvents() + + registeringFromAceEvent = true + self:RegisterEvent("LOOT_OPENED", function() + SendAddonMessage("LOOT_OPENED", "", "RAID") + end) + registeringFromAceEvent = nil + + if not self.playerLogin then + registeringFromAceEvent = true + self:RegisterEvent("PLAYER_LOGIN", function() + self.playerLogin = true + end, true) + registeringFromAceEvent = nil + end + + if not self.postInit then + local isReload = true + local function func() + self.postInit = true + self:TriggerEvent("AceEvent_FullyInitialized") + if self.registry["CHAT_MSG_CHANNEL_NOTICE"] and self.registry["CHAT_MSG_CHANNEL_NOTICE"][self] then + self:UnregisterEvent("CHAT_MSG_CHANNEL_NOTICE") + end + if self.registry["MEETINGSTONE_CHANGED"] and self.registry["MEETINGSTONE_CHANGED"][self] then + self:UnregisterEvent("MEETINGSTONE_CHANGED") + end + if self.registry["MINIMAP_ZONE_CHANGED"] and self.registry["MINIMAP_ZONE_CHANGED"][self] then + self:UnregisterEvent("MINIMAP_ZONE_CHANGED") + end + if self.registry["LANGUAGE_LIST_CHANGED"] and self.registry["LANGUAGE_LIST_CHANGED"][self] then + self:UnregisterEvent("LANGUAGE_LIST_CHANGED") + end + end + registeringFromAceEvent = true + local f = function() + self.playerLogin = true + self:ScheduleEvent("AceEvent_FullyInitialized", func, 1) + end + self:RegisterEvent("MEETINGSTONE_CHANGED", f, true) + self:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE", function() + self:ScheduleEvent("AceEvent_FullyInitialized", func, 0.05) + end) + self:RegisterEvent("LANGUAGE_LIST_CHANGED", function() + if self.registry["MEETINGSTONE_CHANGED"] and self.registry["MEETINGSTONE_CHANGED"][self] then + self:UnregisterEvent("MEETINGSTONE_CHANGED") + self:RegisterEvent("MINIMAP_ZONE_CHANGED", f, true) + end + end) + registeringFromAceEvent = nil + end + + self.super.activate(self, oldLib, oldDeactivate) + if oldLib then + oldDeactivate(oldLib) + end +end + +AceLibrary:Register(AceEvent, MAJOR_VERSION, MINOR_VERSION, AceEvent.activate) +AceEvent = AceLibrary(MAJOR_VERSION) diff --git a/libs/ace/AceHook-2.1/AceHook-2.1.lua b/libs/ace/AceHook-2.1/AceHook-2.1.lua new file mode 100644 index 0000000..deb4a73 --- /dev/null +++ b/libs/ace/AceHook-2.1/AceHook-2.1.lua @@ -0,0 +1,569 @@ +--[[ +Name: AceHook-2.1 +Revision: $Rev: 17638 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Ace 1.x by Turan (turan@gryphon.com) +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceHook-2.1 +SVN: http://svn.wowace.com/root/trunk/Ace2/AceHook-2.1 +Description: Mixin to allow for safe hooking of functions, methods, and scripts. +Dependencies: AceLibrary, AceOO-2.0 +]] + +local MAJOR_VERSION = "AceHook-2.1" +local MINOR_VERSION = "$Revision: 17638 $" + +-- This ensures the code is only executed if the libary doesn't already exist, or is a newer version +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end + +--[[--------------------------------------------------------------------------------- + Create the library object +----------------------------------------------------------------------------------]] + +local AceOO = AceLibrary:GetInstance("AceOO-2.0") +local AceHook = AceOO.Mixin { + "Hook", + "HookScript", + "SecureHook", + "Unhook", + "UnhookAll", + "HookReport", + "IsHooked", + } + +--[[--------------------------------------------------------------------------------- + Library Definitions +----------------------------------------------------------------------------------]] + +local protFuncs = { + CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true, + TurnOrActionStart = true, TurnOrActionStop = true, + PitchUpStart = true, PitchUpStop = true, + PitchDownStart = true, PitchDownStop = true, + MoveBackwardStart = true, MoveBackwardStop = true, + MoveForwardStart = true, MoveForwardStop = true, + Jump = true, StrafeLeftStart = true, + StrafeLeftStop = true, StrafeRightStart = true, + StrafeRightStop = true, ToggleMouseMove = true, + ToggleRun = true, TurnLeftStart = true, + TurnLeftStop = true, TurnRightStart = true, + TurnRightStop = true, +} + +local function issecurevariable(x) + if protFuncs[x] then + return 1 + else + return nil + end +end + +local _G = getfenv(0) + +local function hooksecurefunc(arg1, arg2, arg3) + if type(arg1) == "string" then + arg1, arg2, arg3 = _G, arg1, arg2 + end + local orig = arg1[arg2] + arg1[arg2] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + local x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20 = orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + + arg3(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + + return x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20 + end +end + +local protectedScripts = { + OnClick = true, +} + + +local handlers, scripts, actives, registry + +--[[--------------------------------------------------------------------------------- + Private definitions (Not exposed) +----------------------------------------------------------------------------------]] + +local new, del +do + local list = setmetatable({}, {__mode = "k"}) + function new() + local t = next(list) + if not t then + return {} + end + list[t] = nil + return t + end + + function del(t) + setmetatable(t, nil) + for k in pairs(t) do + t[k] = nil + end + list[t] = true + end +end + +local function createFunctionHook(self, func, handler, orig, secure) + if not secure then + if type(handler) == "string" then + -- The handler is a method, need to self it + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + else + return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + else + -- The handler is a function, just call it + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + else + return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + end + else + -- secure hooks don't call the original method + if type(handler) == "string" then + -- The handler is a method, need to self it + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + else + -- The handler is a function, just call it + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + end + end +end + +local function createMethodHook(self, object, method, handler, orig, secure, script) + if script then + if type(handler) == "string" then + local uid + uid = function() + if actives[uid] then + return self[handler](self, object) + else + return orig() + end + end + return uid + else + -- The handler is a function, just call it + local uid + uid = function() + if actives[uid] then + return handler(object) + else + return orig() + end + end + return uid + end + elseif not secure then + if type(handler) == "string" then + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + else + return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + else + -- The handler is a function, just call it + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + else + return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + end + else + -- secure hooks don't call the original method + if type(handler) == "string" then + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + else + -- The handler is a function, just call it + local uid + uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if actives[uid] then + return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end + end + return uid + end + end +end + +local function hookFunction(self, func, handler, secure) + local orig = _G[func] + + if not orig or type(orig) ~= "function" then + AceHook:error("Attempt to hook a non-existant function %q", func) + end + + if not handler then + handler = func + end + + if registry[self][func] then + local uid = registry[self][func] + + if actives[uid] then + -- We have an active hook from this source. Don't multi-hook + AceHook:error("%q already has an active hook from this source.", func) + end + + if handlers[uid] == handler then + -- The hook is inactive, so reactivate it + actives[uid] = true + return + else + AceHook:error("There is a stale hook for %q can't hook or reactivate.", func) + end + end + + if type(handler) == "string" then + if type(self[handler]) ~= "function" then + AceHook:error("Could not find the the handler %q when hooking function %q", handler, func) + end + elseif type(handler) ~= "function" then + AceHook:error("Could not find the handler you supplied when hooking %q", func) + end + + local uid = createFunctionHook(self, func, handler, orig, secure) + registry[self][func] = uid + actives[uid] = true + handlers[uid] = handler + + if not secure then + _G[func] = uid + self.hooks[func] = orig + else + hooksecurefunc(func, uid) + end +end + +local function unhookFunction(self, func) + if not registry[self][func] then + AceHook:error("Tried to unhook %q which is not currently hooked.", func) + end + + local uid = registry[self][func] + + if actives[uid] then + -- See if we own the global function + if self.hooks[func] and _G[func] == uid then + _G[func] = self.hooks[func] + self.hooks[func] = nil + registry[self][func] = nil + handlers[uid] = nil + scripts[uid] = nil + actives[uid] = nil + -- Magically all-done + else + actives[uid] = nil + end + end +end + +local function hookMethod(self, obj, method, handler, script, secure) + if not handler then + handler = method + end + + if not obj or type(obj) ~= "table" then + AceHook:error("The object you supplied could not be found, or isn't a table.") + end + + local uid = registry[self][obj] and registry[self][obj][method] + if uid then + if actives[uid] then + -- We have an active hook from this source. Don't multi-hook + AceHook:error("%q already has an active hook from this source.", method) + end + + if handlers[uid] == handler then + -- The hook is inactive, reactivate it. + actives[uid] = true + return + else + AceHook:error("There is a stale hook for %q can't hook or reactivate.", method) + end + end + + if type(handler) == "string" then + if type(self[handler]) ~= "function" then + AceHook:error("Could not find the handler %q you supplied when hooking method %q", handler, method) + end + elseif type(handler) ~= "function" then + AceHook:error("Could not find the handler you supplied when hooking method %q", method) + end + + local orig + if script then + if not obj.GetScript then + AceHook:error("The object you supplied does not have a GetScript method.") + end + if not obj:HasScript(method) then + AceHook:error("The object you supplied doesn't allow the %q method.", method) + end + + orig = obj:GetScript(method) + if type(orig) ~= "function" then + -- Sometimes there is not a original function for a script. + orig = function() end + end + else + orig = obj[method] + end + if not orig then + AceHook:error("Could not find the method or script %q you are trying to hook.", method) + end + + if not registry[self][obj] then + registry[self][obj] = new() + end + + if not self.hooks[obj] then + self.hooks[obj] = new() + end + + local uid = createMethodHook(self, obj, method, handler, orig, secure, script) + registry[self][obj][method] = uid + actives[uid] = true + handlers[uid] = handler + scripts[uid] = script and true or nil + + if script then + obj:SetScript(method, uid) + self.hooks[obj][method] = orig + elseif not secure then + obj[method] = uid + self.hooks[obj][method] = orig + else + hooksecurefunc(obj, method, uid) + end +end + +local function unhookMethod(self, obj, method) + if not registry[self][obj] or not registry[self][obj][method] then + AceHook:error("Attempt to unhook a method %q that is not currently hooked.", method) + return + end + + local uid = registry[self][obj][method] + + if actives[uid] then + if scripts[uid] then -- If this is a script + if obj:GetScript(method) == uid then + -- We own the script. Revert to normal. + obj:SetScript(method, self.hooks[obj][method]) + self.hooks[obj][method] = nil + registry[self][obj][method] = nil + handlers[uid] = nil + scripts[uid] = nil + actives[uid] = nil + else + actives[uid] = nil + end + else + if self.hooks[obj] and self.hooks[obj][method] and obj[method] == uid then + -- We own the method. Revert to normal. + obj[method] = self.hooks[obj][method] + self.hooks[obj][method] = nil + registry[self][obj][method] = nil + handlers[uid] = nil + actives[uid] = nil + else + actives[uid] = nil + end + end + end + if self.hooks[obj] and not next(self.hooks[obj]) then + self.hooks[obj] = del(self.hooks[obj]) + end + if not next(registry[self][obj]) then + registry[self][obj] = del(registry[self][obj]) + end +end + +-- ("function" [, handler] [, hookSecure]) or (object, "method" [, handler] [, hookSecure]) +function AceHook:Hook(object, method, handler, hookSecure) + if type(object) == "string" then + method, handler, hookSecure = object, method, handler + if handler == true then + handler, hookSecure = nil, true + end + if not hookSecure and issecurevariable(method) then + AceHook:error("Attempt to hook secure function %q. Use `SecureHook' or add `true' to the argument list to override.", method) + end + AceHook:argCheck(handler, 3, "function", "string", "nil") + AceHook:argCheck(hookSecure, 4, "boolean", "nil") + hookFunction(self, method, handler, false) + else + if handler == true then + handler, hookSecure = nil, true + end + if not hookSecure and issecurevariable(object, method) then + AceHook:error("Attempt to hook secure method %q. Use `SecureHook' or add `true' to the argument list to override.", method) + end + AceHook:argCheck(object, 2, "table") + AceHook:argCheck(method, 3, "string") + AceHook:argCheck(handler, 4, "function", "string", "nil") + AceHook:argCheck(hookSecure, 5, "boolean", "nil") + hookMethod(self, object, method, handler, false, false) + end +end + +-- ("function", handler) or (object, "method", handler) +function AceHook:SecureHook(object, method, handler) + if type(object) == "string" then + method, handler = object, method + AceHook:argCheck(handler, 3, "function", "string", "nil") + hookFunction(self, method, handler, true) + else + AceHook:argCheck(object, 2, "table") + AceHook:argCheck(method, 3, "string") + AceHook:argCheck(handler, 4, "function", "string", "nil") + hookMethod(self, object, method, handler, false, true) + end +end + +function AceHook:HookScript(frame, script, handler) + AceHook:argCheck(frame, 2, "table") + if not frame[0] or type(frame.IsFrameType) ~= "function" then + AceHook:error("Bad argument #2 to `HookScript'. Expected frame.") + end + AceHook:argCheck(script, 3, "string") + AceHook:argCheck(handler, 4, "function", "string", "nil") + hookMethod(self, frame, script, handler, true, false) +end + +-- ("function") or (object, "method") +function AceHook:IsHooked(obj, method) + if type(obj) == "string" then + if registry[self][obj] and actives[registry[self][obj]] then + return true, handlers[registry[self][obj]] + end + else + AceHook:argCheck(obj, 2, "string", "table") + AceHook:argCheck(method, 3, "string") + if registry[self][obj] and registry[self][obj][method] and actives[registry[self][obj][method]] then + return true, handlers[registry[self][obj][method]] + end + end + + return false, nil +end + +-- ("function") or (object, "method") +function AceHook:Unhook(obj, method) + if type(obj) == "string" then + unhookFunction(self, obj) + else + AceHook:argCheck(obj, 2, "string", "table") + AceHook:argCheck(method, 3, "string") + unhookMethod(self, obj, method) + end +end + +function AceHook:UnhookAll() + for key, value in pairs(registry[self]) do + if type(key) == "table" then + for method in pairs(value) do + self:Unhook(key, method) + end + else + self:Unhook(key) + end + end +end + +function AceHook:HookReport() + DEFAULT_CHAT_FRAME:AddMessage("This is a list of all active hooks for this object:") + if not next(registry[self]) then + DEFAULT_CHAT_FRAME:AddMessage("No hooks") + end + + for key, value in pairs(registry[self]) do + if type(value) == "table" then + for method, uid in pairs(value) do + DEFAULT_CHAT_FRAME:AddMessage(string.format("object: %s method: %q |cff%s|r%s", tostring(key), method, actives[uid] and "00ff00Active" or "ffff00Inactive", not self.hooks[key][method] and " |cff7f7fff-Secure-|r" or "")) + end + else + DEFAULT_CHAT_FRAME:AddMessage(string.format("function: %q |cff%s|r%s", tostring(key), actives[value] and "00ff00Active" or "ffff00Inactive", not self.hooks[key] and " |cff7f7fff-Secure-|r" or "")) + end + end +end + +function AceHook:OnInstanceInit(object) + if not object.hooks then + object.hooks = new() + end + if not registry[object] then + registry[object] = new() + end +end + +AceHook.OnManualEmbed = AceHook.OnInstanceInit + +function AceHook:OnEmbedDisable(target) + self.UnhookAll(target) +end + +local function activate(self, oldLib, oldDeactivate) + AceHook = self + + self.handlers = oldLib and oldLib.handlers or {} + self.registry = oldLib and oldLib.registry or {} + self.scripts = oldLib and oldLib.scripts or {} + self.actives = oldLib and oldLib.actives or {} + + handlers = self.handlers + registry = self.registry + scripts = self.scripts + actives = self.actives + + AceHook.super.activate(self, oldLib, oldDeactivate) + + if oldDeactivate then + oldDeactivate(oldLib) + end +end + +AceLibrary:Register(AceHook, MAJOR_VERSION, MINOR_VERSION, activate) diff --git a/libs/ace/AceLibrary/AceLibrary.lua b/libs/ace/AceLibrary/AceLibrary.lua new file mode 100644 index 0000000..9c7d93b --- /dev/null +++ b/libs/ace/AceLibrary/AceLibrary.lua @@ -0,0 +1,752 @@ +--[[ +Name: AceLibrary +Revision: $Rev: 17638 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Iriel (iriel@vigilance-committee.org) + Tekkub (tekkub@gmail.com) + Revision: $Rev: 17638 $ +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceLibrary +SVN: http://svn.wowace.com/root/trunk/Ace2/AceLibrary +Description: Versioning library to handle other library instances, upgrading, + and proper access. + It also provides a base for libraries to work off of, providing + proper error tools. It is handy because all the errors occur in the + file that called it, not in the library file itself. +Dependencies: None +]] + +local ACELIBRARY_MAJOR = "AceLibrary" +local ACELIBRARY_MINOR = "$Revision: 17638 $" + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(ACELIBRARY_MINOR) then return end -- lua51 check +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +local string_gfind = string.gmatch or string.gfind + +local _G = getfenv(0) +local previous = _G[ACELIBRARY_MAJOR] +if previous and not previous:IsNewVersion(ACELIBRARY_MAJOR, ACELIBRARY_MINOR) then return end + +-- @table AceLibrary +-- @brief System to handle all versioning of libraries. +local AceLibrary = {} +local AceLibrary_mt = {} +setmetatable(AceLibrary, AceLibrary_mt) + +local tmp +local function error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if type(self) ~= "table" then + _G.error(string.format("Bad argument #1 to `error' (table expected, got %s)", type(self)), 2) + end + if not tmp then + tmp = {} + else + for k in pairs(tmp) do tmp[k] = nil end + table_setn(tmp, 0) + end + + table.insert(tmp, a1) + table.insert(tmp, a2) + table.insert(tmp, a3) + table.insert(tmp, a4) + table.insert(tmp, a5) + table.insert(tmp, a6) + table.insert(tmp, a7) + table.insert(tmp, a8) + table.insert(tmp, a9) + table.insert(tmp, a10) + table.insert(tmp, a11) + table.insert(tmp, a12) + table.insert(tmp, a13) + table.insert(tmp, a14) + table.insert(tmp, a15) + table.insert(tmp, a16) + table.insert(tmp, a17) + table.insert(tmp, a18) + table.insert(tmp, a19) + table.insert(tmp, a20) + + local stack = debugstack() + if not message then + local _,_,second = string.find(stack, "\n(.-)\n") + message = "error raised! " .. second + else + for i = 1,table.getn(tmp) do + tmp[i] = tostring(tmp[i]) + end + for i = 1,10 do + table.insert(tmp, "nil") + end + message = string.format(message, unpack(tmp)) + end + + if getmetatable(self) and getmetatable(self).__tostring then + message = string.format("%s: %s", tostring(self), message) + elseif type(rawget(self, 'GetLibraryVersion')) == "function" and AceLibrary:HasInstance(self:GetLibraryVersion()) then + message = string.format("%s: %s", self:GetLibraryVersion(), message) + elseif type(rawget(self, 'class')) == "table" and type(rawget(self.class, 'GetLibraryVersion')) == "function" and AceLibrary:HasInstance(self.class:GetLibraryVersion()) then + message = string.format("%s: %s", self.class:GetLibraryVersion(), message) + end + + local first = string.gsub(stack, "\n.*", "") + local file = string.gsub(first, ".*\\(.*).lua:%d+: .*", "%1") + file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") + + local i = 0 + for s in string_gfind(stack, "\n([^\n]*)") do + i = i + 1 + if not string.find(s, file .. "%.lua:%d+:") then + file = string.gsub(s, "^.*\\(.*).lua:%d+: .*", "%1") + file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") + break + end + end + local j = 0 + for s in string_gfind(stack, "\n([^\n]*)") do + j = j + 1 + if j > i and not string.find(s, file .. "%.lua:%d+:") then + _G.error(message, j + 1) + return + end + end + _G.error(message, 2) + return +end + +local function assert(self, condition, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if not condition then + if not message then + local stack = debugstack() + local _,_,second = string.find(stack, "\n(.-)\n") + message = "assertion failed! " .. second + end + error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + return + end + return condition +end + +local function argCheck(self, arg, num, kind, kind2, kind3, kind4, kind5) + if type(num) ~= "number" then + error(self, "Bad argument #3 to `argCheck' (number expected, got %s)", type(num)) + elseif type(kind) ~= "string" then + error(self, "Bad argument #4 to `argCheck' (string expected, got %s)", type(kind)) + end + local errored = false + arg = type(arg) + if arg ~= kind and arg ~= kind2 and arg ~= kind3 and arg ~= kind4 and arg ~= kind5 then + local _,_,func = string.find(debugstack(), "`argCheck'.-([`<].-['>])") + if not func then + _,_,func = string.find(debugstack(), "([`<].-['>])") + end + if kind5 then + error(self, "Bad argument #%s to %s (%s, %s, %s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, kind4, kind5, arg) + elseif kind4 then + error(self, "Bad argument #%s to %s (%s, %s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, kind4, arg) + elseif kind3 then + error(self, "Bad argument #%s to %s (%s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, arg) + elseif kind2 then + error(self, "Bad argument #%s to %s (%s or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, arg) + else + error(self, "Bad argument #%s to %s (%s expected, got %s)", tonumber(num) or 0/0, func, kind, arg) + end + end +end + +local function pcall(self, func, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 = _G.pcall(func, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + if not a1 then + error(self, string.gsub(a2, ".-%.lua:%d-: ", "")) + else + return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 + end +end + +local recurse = {} +local function addToPositions(t, major) + if not AceLibrary.positions[t] or AceLibrary.positions[t] == major then + rawset(t, recurse, true) + AceLibrary.positions[t] = major + for k,v in pairs(t) do + if type(v) == "table" and not rawget(v, recurse) then + addToPositions(v, major) + end + if type(k) == "table" and not rawget(k, recurse) then + addToPositions(k, major) + end + end + local mt = getmetatable(t) + if mt and not rawget(mt, recurse) then + addToPositions(mt, major) + end + rawset(t, recurse, nil) + end +end + +local function svnRevisionToNumber(text) + if type(text) == "string" then + if string.find(text, "^%$Revision: (%d+) %$$") then + return tonumber((string.gsub(text, "^%$Revision: (%d+) %$$", "%1"))) + elseif string.find(text, "^%$Rev: (%d+) %$$") then + return tonumber((string.gsub(text, "^%$Rev: (%d+) %$$", "%1"))) + elseif string.find(text, "^%$LastChangedRevision: (%d+) %$$") then + return tonumber((string.gsub(text, "^%$LastChangedRevision: (%d+) %$$", "%1"))) + end + elseif type(text) == "number" then + return text + end + return nil +end + +local crawlReplace +do + local recurse = {} + local function func(t, to, from) + if recurse[t] then + return + end + recurse[t] = true + local mt = getmetatable(t) + setmetatable(t, nil) + rawset(t, to, rawget(t, from)) + rawset(t, from, nil) + for k,v in pairs(t) do + if v == from then + t[k] = to + elseif type(v) == "table" then + if not recurse[v] then + func(v, to, from) + end + end + + if type(k) == "table" then + if not recurse[k] then + func(k, to, from) + end + end + end + setmetatable(t, mt) + if mt then + if mt == from then + setmetatable(t, to) + elseif not recurse[mt] then + func(mt, to, from) + end + end + end + function crawlReplace(t, to, from) + func(t, to, from) + for k in pairs(recurse) do + recurse[k] = nil + end + end +end + +-- @function destroyTable +-- @brief remove all the contents of a table +-- @param t table to destroy +local function destroyTable(t) + setmetatable(t, nil) + for k,v in pairs(t) do t[k] = nil end + table_setn(t, 0) +end + +local function isFrame(frame) + return type(frame) == "table" and type(rawget(frame, 0)) == "userdata" and type(rawget(frame, 'IsFrameType')) == "function" and getmetatable(frame) and type(rawget(getmetatable(frame), '__index')) == "function" +end + +local new, del +do + local tables = setmetatable({}, {__mode = "k"}) + + function new() + local t = next(tables) + if t then + tables[t] = nil + return t + else + return {} + end + end + + function del(t, depth) + if depth and depth > 0 then + for k,v in pairs(t) do + if type(v) == "table" and not isFrame(v) then + del(v, depth - 1) + end + end + end + destroyTable(t) + tables[t] = true + end +end + +-- @function copyTable +-- @brief Create a shallow copy of a table and return it. +-- @param from The table to copy from +-- @return A shallow copy of the table +local function copyTable(from) + local to = new() + for k,v in pairs(from) do to[k] = v end + table_setn(to, table.getn(from)) + setmetatable(to, getmetatable(from)) + return to +end + +-- @function deepTransfer +-- @brief Fully transfer all data, keeping proper previous table +-- backreferences stable. +-- @param to The table with which data is to be injected into +-- @param from The table whose data will be injected into the first +-- @param saveFields If available, a shallow copy of the basic data is saved +-- in here. +-- @param list The account of table references +-- @param list2 The current status on which tables have been traversed. +local deepTransfer +do + -- @function examine + -- @brief Take account of all the table references to be shared + -- between the to and from tables. + -- @param to The table with which data is to be injected into + -- @param from The table whose data will be injected into the first + -- @param list An account of the table references + local function examine(to, from, list, major) + list[from] = to + for k,v in pairs(from) do + if rawget(to, k) and type(from[k]) == "table" and type(to[k]) == "table" and not list[from[k]] then + if from[k] == to[k] then + list[from[k]] = to[k] + elseif AceLibrary.positions[from[v]] ~= major and AceLibrary.positions[from[v]] then + list[from[k]] = from[k] + elseif not list[from[k]] then + examine(to[k], from[k], list, major) + end + end + end + return list + end + + function deepTransfer(to, from, saveFields, major, list, list2) + setmetatable(to, nil) + local createdList + if not list then + createdList = true + list = new() + list2 = new() + examine(to, from, list, major) + end + list2[to] = to + for k,v in pairs(to) do + if type(rawget(from, k)) ~= "table" or type(v) ~= "table" or isFrame(v) then + if saveFields then + saveFields[k] = v + end + to[k] = nil + elseif v ~= _G then + if saveFields then + saveFields[k] = copyTable(v) + end + end + end + for k in pairs(from) do + if rawget(to, k) and to[k] ~= from[k] and AceLibrary.positions[to[k]] == major and from[k] ~= _G then + if not list2[to[k]] then + deepTransfer(to[k], from[k], nil, major, list, list2) + end + to[k] = list[to[k]] or list2[to[k]] + else + rawset(to, k, from[k]) + end + end + table_setn(to, table.getn(from)) + setmetatable(to, getmetatable(from)) + local mt = getmetatable(to) + if mt then + if list[mt] then + setmetatable(to, list[mt]) + elseif mt.__index and list[mt.__index] then + mt.__index = list[mt.__index] + end + end + destroyTable(from) + if createdList then + del(list) + del(list2) + end + end +end + +-- @method TryToLoadStandalone +-- @brief Attempt to find and load a standalone version of the requested library +-- @param major A string representing the major version +-- @return If library is found, return values from the call to LoadAddOn are returned +-- If the library has been requested previously, nil is returned. +local function TryToLoadStandalone(major) + if not AceLibrary.scannedlibs then AceLibrary.scannedlibs = {} end + if AceLibrary.scannedlibs[major] then return end + + AceLibrary.scannedlibs[major] = true + + local name, _, _, enabled, loadable = GetAddOnInfo(major) + if loadable then + return LoadAddOn(name) + end + + for i=1,GetNumAddOns() do + if GetAddOnMetadata(i, "X-AceLibrary-"..major) then + local name, _, _, enabled, loadable = GetAddOnInfo(i) + if loadable then + return LoadAddOn(name) + end + end + end +end + +-- @method IsNewVersion +-- @brief Obtain whether the supplied version would be an upgrade to the +-- current version. This allows for bypass code in library +-- declaration. +-- @param major A string representing the major version +-- @param minor An integer or an svn revision string representing the minor version +-- @return whether the supplied version would be newer than what is +-- currently available. +function AceLibrary:IsNewVersion(major, minor) + argCheck(self, major, 2, "string") + TryToLoadStandalone(major) + + if type(minor) == "string" then + local m = svnRevisionToNumber(minor) + if m then + minor = m + else + _G.error(string.format("Bad argument #3 to `IsNewVersion'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) + end + end + argCheck(self, minor, 3, "number") + local data = self.libs[major] + if not data then + return true + end + return data.minor < minor +end + +-- @method HasInstance +-- @brief Returns whether an instance exists. This allows for optional support of a library. +-- @param major A string representing the major version. +-- @param minor (optional) An integer or an svn revision string representing the minor version. +-- @return Whether an instance exists. +function AceLibrary:HasInstance(major, minor) + argCheck(self, major, 2, "string") + TryToLoadStandalone(major) + + if minor then + if type(minor) == "string" then + local m = svnRevisionToNumber(minor) + if m then + minor = m + else + _G.error(string.format("Bad argument #3 to `HasInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) + end + end + argCheck(self, minor, 3, "number") + if not self.libs[major] then + return + end + return self.libs[major].minor == minor + end + return self.libs[major] and true +end + +-- @method GetInstance +-- @brief Returns the library with the given major/minor version. +-- @param major A string representing the major version. +-- @param minor (optional) An integer or an svn revision string representing the minor version. +-- @return The library with the given major/minor version. +function AceLibrary:GetInstance(major, minor) + argCheck(self, major, 2, "string") + TryToLoadStandalone(major) + + local data = self.libs[major] + if not data then + _G.error(string.format("Cannot find a library instance of %s.", major), 2) + return + end + if minor then + if type(minor) == "string" then + local m = svnRevisionToNumber(minor) + if m then + minor = m + else + _G.error(string.format("Bad argument #3 to `GetInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) + end + end + argCheck(self, minor, 2, "number") + if data.minor ~= minor then + _G.error(string.format("Cannot find a library instance of %s, minor version %d.", major, minor), 2) + return + end + end + return data.instance +end + +-- Syntax sugar. AceLibrary("FooBar-1.0") +AceLibrary_mt.__call = AceLibrary.GetInstance + +local donothing + +local AceEvent + +-- @method Register +-- @brief Registers a new version of a given library. +-- @param newInstance the library to register +-- @param major the major version of the library +-- @param minor the minor version of the library +-- @param activateFunc (optional) A function to be called when the library is +-- fully activated. Takes the arguments +-- (newInstance [, oldInstance, oldDeactivateFunc]). If +-- oldInstance is given, you should probably call +-- oldDeactivateFunc(oldInstance). +-- @param deactivateFunc (optional) A function to be called by a newer library's +-- activateFunc. +-- @param externalFunc (optional) A function to be called whenever a new +-- library is registered. +function AceLibrary:Register(newInstance, major, minor, activateFunc, deactivateFunc, externalFunc) + argCheck(self, newInstance, 2, "table") + argCheck(self, major, 3, "string") + if type(minor) == "string" then + local m = svnRevisionToNumber(minor) + if m then + minor = m + else + _G.error(string.format("Bad argument #4 to `Register'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) + end + end + argCheck(self, minor, 4, "number") + if math.floor(minor) ~= minor or minor < 0 then + error(self, "Bad argument #4 to `Register' (integer >= 0 expected, got %s)", minor) + end + argCheck(self, activateFunc, 5, "function", "nil") + argCheck(self, deactivateFunc, 6, "function", "nil") + argCheck(self, externalFunc, 7, "function", "nil") + if not deactivateFunc then + if not donothing then + donothing = function() end + end + deactivateFunc = donothing + end + local data = self.libs[major] + if not data then + -- This is new + local instance = copyTable(newInstance) + crawlReplace(instance, instance, newInstance) + destroyTable(newInstance) + if AceLibrary == newInstance then + self = instance + AceLibrary = instance + end + self.libs[major] = { + instance = instance, + minor = minor, + deactivateFunc = deactivateFunc, + externalFunc = externalFunc, + } + rawset(instance, 'GetLibraryVersion', function(self) + return major, minor + end) + if not rawget(instance, 'error') then + rawset(instance, 'error', error) + end + if not rawget(instance, 'assert') then + rawset(instance, 'assert', assert) + end + if not rawget(instance, 'argCheck') then + rawset(instance, 'argCheck', argCheck) + end + if not rawget(instance, 'pcall') then + rawset(instance, 'pcall', pcall) + end + addToPositions(instance, major) + if activateFunc then + activateFunc(instance, nil, nil) -- no old version, so explicit nil + end + + if externalFunc then + for k,data in pairs(self.libs) do + if k ~= major then + externalFunc(instance, k, data.instance) + end + end + end + + for k,data in pairs(self.libs) do + if k ~= major and data.externalFunc then + data.externalFunc(data.instance, major, instance) + end + end + if major == "AceEvent-2.0" then + AceEvent = instance + end + if AceEvent then + AceEvent.TriggerEvent(self, "AceLibrary_Register", major, instance) + end + + return instance + end + local instance = data.instance + if minor <= data.minor then + -- This one is already obsolete, raise an error. + _G.error(string.format("Obsolete library registered. %s is already registered at version %d. You are trying to register version %d. Hint: if not AceLibrary:IsNewVersion(%q, %d) then return end", major, data.minor, minor, major, minor), 2) + return + end + -- This is an update + local oldInstance = new() + + addToPositions(newInstance, major) + local isAceLibrary = (AceLibrary == newInstance) + local old_error, old_assert, old_argCheck, old_pcall + if isAceLibrary then + self = instance + AceLibrary = instance + + old_error = instance.error + old_assert = instance.assert + old_argCheck = instance.argCheck + old_pcall = instance.pcall + + self.error = error + self.assert = assert + self.argCheck = argCheck + self.pcall = pcall + end + deepTransfer(instance, newInstance, oldInstance, major) + crawlReplace(instance, instance, newInstance) + local oldDeactivateFunc = data.deactivateFunc + data.minor = minor + data.deactivateFunc = deactivateFunc + data.externalFunc = externalFunc + rawset(instance, 'GetLibraryVersion', function(self) + return major, minor + end) + if not rawget(instance, 'error') then + rawset(instance, 'error', error) + end + if not rawget(instance, 'assert') then + rawset(instance, 'assert', assert) + end + if not rawget(instance, 'argCheck') then + rawset(instance, 'argCheck', argCheck) + end + if not rawget(instance, 'pcall') then + rawset(instance, 'pcall', pcall) + end + if isAceLibrary then + for _,v in pairs(self.libs) do + local i = type(v) == "table" and v.instance + if type(i) == "table" then + if not rawget(i, 'error') or i.error == old_error then + rawset(i, 'error', error) + end + if not rawget(i, 'assert') or i.assert == old_assert then + rawset(i, 'assert', assert) + end + if not rawget(i, 'argCheck') or i.argCheck == old_argCheck then + rawset(i, 'argCheck', argCheck) + end + if not rawget(i, 'pcall') or i.pcall == old_pcall then + rawset(i, 'pcall', pcall) + end + end + end + end + if activateFunc then + activateFunc(instance, oldInstance, oldDeactivateFunc) + else + oldDeactivateFunc(oldInstance) + end + del(oldInstance) + + if externalFunc then + for k,data in pairs(self.libs) do + if k ~= major then + externalFunc(instance, k, data.instance) + end + end + end + + return instance +end + +local iter +function AceLibrary:IterateLibraries() + if not iter then + local function iter(t, k) + k = next(t, k) + if not k then + return nil + else + return k, t[k].instance + end + end + end + return iter, self.libs, nil +end + +-- @function Activate +-- @brief The activateFunc for AceLibrary itself. Called when +-- AceLibrary properly registers. +-- @param self Reference to AceLibrary +-- @param oldLib (optional) Reference to an old version of AceLibrary +-- @param oldDeactivate (optional) Function to deactivate the old lib +local function activate(self, oldLib, oldDeactivate) + if not self.libs then + if oldLib then + self.libs = oldLib.libs + self.scannedlibs = oldLib.scannedlibs + end + if not self.libs then + self.libs = {} + end + if not self.scannedlibs then + self.scannedlibs = {} + end + end + if not self.positions then + if oldLib then + self.positions = oldLib.positions + end + if not self.positions then + self.positions = setmetatable({}, { __mode = "k" }) + end + end + + -- Expose the library in the global environment + _G[ACELIBRARY_MAJOR] = self + + if oldDeactivate then + oldDeactivate(oldLib) + end +end + +if not previous then + previous = AceLibrary +end +if not previous.libs then + previous.libs = {} +end +AceLibrary.libs = previous.libs +if not previous.positions then + previous.positions = setmetatable({}, { __mode = "k" }) +end +AceLibrary.positions = previous.positions +AceLibrary:Register(AceLibrary, ACELIBRARY_MAJOR, ACELIBRARY_MINOR, activate) diff --git a/libs/ace/AceLocale-2.2/AceLocale-2.2.lua b/libs/ace/AceLocale-2.2/AceLocale-2.2.lua new file mode 100644 index 0000000..3ff0a3f --- /dev/null +++ b/libs/ace/AceLocale-2.2/AceLocale-2.2.lua @@ -0,0 +1,538 @@ +--[[ +Name: AceLocale-2.2 +Revision: $Rev: 17638 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Ace 1.x by Turan (turan@gryphon.com) +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceLocale-2.2 +SVN: http://svn.wowace.com/root/trunk/Ace2/AceLocale-2.2 +Description: Localization library for addons to use to handle proper + localization and internationalization. +Dependencies: AceLibrary +]] + +local MAJOR_VERSION = "AceLocale-2.2" +local MINOR_VERSION = "$Revision: 17638 $" + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +local AceLocale = {} + +local DEFAULT_LOCALE = "enUS" +local _G = getfenv(0) + +local BASE_TRANSLATIONS, DEBUGGING, TRANSLATIONS, BASE_LOCALE, TRANSLATION_TABLES, REVERSE_TRANSLATIONS, STRICTNESS, DYNAMIC_LOCALES, CURRENT_LOCALE, NAME + +local rawget = rawget +local rawset = rawset +local type = type + +local newRegistries = {} +local scheduleClear + +local lastSelf +local __index = function(self, key) + lastSelf = self + local value = (rawget(self, TRANSLATIONS) or AceLocale.prototype)[key] + rawset(self, key, value) + return value +end + +local __newindex = function(self, k, v) + if type(v) ~= "function" and type(k) ~= "table" then + AceLocale.error(self, "Cannot change the values of an AceLocale instance.") + end + rawset(self, k, v) +end + +local __tostring = function(self) + if type(rawget(self, 'GetLibraryVersion')) == "function" then + return self:GetLibraryVersion() + else + return "AceLocale(" .. self[NAME] .. ")" + end +end + +local function clearCache(self) + if not rawget(self, BASE_TRANSLATIONS) then + return + end + + local cache = self[BASE_TRANSLATIONS] + rawset(self, REVERSE_TRANSLATIONS, nil) + + for k in pairs(self) do + if rawget(cache, k) ~= nil then + self[k] = nil + end + end + rawset(self, 'tmp', true) + self.tmp = nil +end + +local function refixInstance(instance) + if getmetatable(instance) then + setmetatable(instance, nil) + end + local translations = instance[TRANSLATIONS] + if translations then + if getmetatable(translations) then + setmetatable(translations, nil) + end + local baseTranslations = instance[BASE_TRANSLATIONS] + if getmetatable(baseTranslations) then + setmetatable(baseTranslations, nil) + end + if translations == baseTranslations or instance[STRICTNESS] then + setmetatable(instance, { + __index = __index, + __newindex = __newindex, + __tostring = __tostring + }) + + setmetatable(translations, { + __index = AceLocale.prototype + }) + else + setmetatable(instance, { + __index = __index, + __newindex = __newindex, + __tostring = __tostring + }) + + setmetatable(translations, { + __index = baseTranslations, + }) + + setmetatable(baseTranslations, { + __index = AceLocale.prototype, + }) + end + else + setmetatable(instance, { + __index = __index, + __newindex = __newindex, + __tostring = __tostring, + }) + end + clearCache(instance) + newRegistries[instance] = true + scheduleClear() + return instance +end + +function AceLocale:new(name) + self:argCheck(name, 2, "string") + + if self.registry[name] and type(rawget(self.registry[name], 'GetLibraryVersion')) ~= "function" then + return self.registry[name] + end + + AceLocale.registry[name] = refixInstance({ + [STRICTNESS] = false, + [NAME] = name, + }) + newRegistries[AceLocale.registry[name]] = true + return AceLocale.registry[name] +end + +AceLocale.prototype = { class = AceLocale } + +function AceLocale.prototype:EnableDebugging() + if rawget(self, BASE_TRANSLATIONS) then + AceLocale.error(self, "Cannot enable debugging after a translation has been registered.") + end + rawset(self, DEBUGGING, true) +end + +function AceLocale.prototype:EnableDynamicLocales(override) + AceLocale.argCheck(self, override, 2, "boolean", "nil") + if not override and rawget(self, BASE_TRANSLATIONS) then + AceLocale.error(self, "Cannot enable dynamic locales after a translation has been registered.") + end + if not rawget(self, DYNAMIC_LOCALES) then + rawset(self, DYNAMIC_LOCALES, true) + if rawget(self, BASE_LOCALE) then + if not rawget(self, TRANSLATION_TABLES) then + rawset(self, TRANSLATION_TABLES, {}) + end + self[TRANSLATION_TABLES][self[BASE_LOCALE]] = self[BASE_TRANSLATIONS] + self[TRANSLATION_TABLES][self[CURRENT_LOCALE]] = self[TRANSLATIONS] + end + end +end + +function AceLocale.prototype:RegisterTranslations(locale, func) + AceLocale.argCheck(self, locale, 2, "string") + AceLocale.argCheck(self, func, 3, "function") + + if locale == rawget(self, BASE_LOCALE) then + AceLocale.error(self, "Cannot provide the same locale more than once. %q provided twice.", locale) + end + + if rawget(self, BASE_TRANSLATIONS) and GetLocale() ~= locale then + if rawget(self, DEBUGGING) or rawget(self, DYNAMIC_LOCALES) then + if not rawget(self, TRANSLATION_TABLES) then + rawset(self, TRANSLATION_TABLES, {}) + end + if self[TRANSLATION_TABLES][locale] then + AceLocale.error(self, "Cannot provide the same locale more than once. %q provided twice.", locale) + end + local t = func() + func = nil + if type(t) ~= "table" then + AceLocale.error(self, "Bad argument #3 to `RegisterTranslations'. function did not return a table. (expected table, got %s)", type(t)) + end + self[TRANSLATION_TABLES][locale] = t + t = nil + end + func = nil + return + end + local t = func() + func = nil + if type(t) ~= "table" then + AceLocale.error(self, "Bad argument #3 to `RegisterTranslations'. function did not return a table. (expected table, got %s)", type(t)) + end + + rawset(self, TRANSLATIONS, t) + if not rawget(self, BASE_TRANSLATIONS) then + rawset(self, BASE_TRANSLATIONS, t) + rawset(self, BASE_LOCALE, locale) + for key,value in pairs(t) do + if value == true then + t[key] = key + end + end + else + for key, value in pairs(self[TRANSLATIONS]) do + if not rawget(self[BASE_TRANSLATIONS], key) then + AceLocale.error(self, "Improper translation exists. %q is likely misspelled for locale %s.", key, locale) + end + if value == true then + AceLocale.error(self, "Can only accept true as a value on the base locale. %q is the base locale, %q is not.", rawget(self, BASE_LOCALE), locale) + end + end + end + rawset(self, CURRENT_LOCALE, locale) + refixInstance(self) + if rawget(self, DEBUGGING) or rawget(self, DYNAMIC_LOCALES) then + if not rawget(self, TRANSLATION_TABLES) then + rawset(self, TRANSLATION_TABLES, {}) + end + self[TRANSLATION_TABLES][locale] = t + end + t = nil +end + +function AceLocale.prototype:SetLocale(locale) + AceLocale.argCheck(self, locale, 2, "string", "boolean") + if not rawget(self, DYNAMIC_LOCALES) then + AceLocale.error(self, "Cannot call `SetLocale' without first calling `EnableDynamicLocales'.") + end + if not rawget(self, TRANSLATION_TABLES) then + AceLocale.error(self, "Cannot call `SetLocale' without first calling `RegisterTranslations'.") + end + if locale == true then + locale = GetLocale() + if not self[TRANSLATION_TABLES][locale] then + locale = self[BASE_LOCALE] + end + end + + if self[CURRENT_LOCALE] == locale then + return + end + + if not self[TRANSLATION_TABLES][locale] then + AceLocale.error(self, "Locale %q not registered.", locale) + end + + self[TRANSLATIONS] = self[TRANSLATION_TABLES][locale] + self[CURRENT_LOCALE] = locale + refixInstance(self) +end + +function AceLocale.prototype:GetLocale() + if not rawget(self, TRANSLATION_TABLES) then + AceLocale.error(self, "Cannot call `GetLocale' without first calling `RegisterTranslations'.") + end + return self[CURRENT_LOCALE] +end + +local function iter(t, position) + return (next(t, position)) +end + +function AceLocale.prototype:IterateAvailableLocales() + if not rawget(self, DYNAMIC_LOCALES) then + AceLocale.error(self, "Cannot call `IterateAvailableLocales' without first calling `EnableDynamicLocales'.") + end + if not rawget(self, TRANSLATION_TABLES) then + AceLocale.error(self, "Cannot call `IterateAvailableLocales' without first calling `RegisterTranslations'.") + end + return iter, self[TRANSLATION_TABLES], nil +end + +function AceLocale.prototype:HasLocale(locale) + if not rawget(self, DYNAMIC_LOCALES) then + AceLocale.error(self, "Cannot call `HasLocale' without first calling `EnableDynamicLocales'.") + end + AceLocale.argCheck(self, locale, 2, "string") + return rawget(self, TRANSLATION_TABLES) and self[TRANSLATION_TABLES][locale] ~= nil +end + +function AceLocale.prototype:SetStrictness(strict) + AceLocale.argCheck(self, strict, 2, "boolean") + local mt = getmetatable(self) + if not mt then + AceLocale.error(self, "Cannot call `SetStrictness' without a metatable.") + end + if not rawget(self, TRANSLATIONS) then + AceLocale.error(self, "No translations registered.") + end + rawset(self, STRICTNESS, strict) + refixInstance(self) +end + +local function initReverse(self) + rawset(self, REVERSE_TRANSLATIONS, {}) + local alpha = self[TRANSLATIONS] + local bravo = self[REVERSE_TRANSLATIONS] + for base, localized in pairs(alpha) do + bravo[localized] = base + end +end + +function AceLocale.prototype:GetTranslation(text) + AceLocale.argCheck(self, text, 1, "string", "number") + if not rawget(self, TRANSLATIONS) then + AceLocale.error(self, "No translations registered") + end + return self[text] +end + +function AceLocale.prototype:GetStrictTranslation(text) + AceLocale.argCheck(self, text, 1, "string", "number") + local x = rawget(self, TRANSLATIONS) + if not x then + AceLocale.error(self, "No translations registered") + end + local value = rawget(x, text) + if value == nil then + AceLocale.error(self, "Translation %q does not exist for locale %s", text, self[CURRENT_LOCALE]) + end + return value +end + +function AceLocale.prototype:GetReverseTranslation(text) + local x = rawget(self, REVERSE_TRANSLATIONS) + if not x then + if not rawget(self, TRANSLATIONS) then + AceLocale.error(self, "No translations registered") + end + initReverse(self) + x = self[REVERSE_TRANSLATIONS] + end + local translation = x[text] + if not translation then + AceLocale.error(self, "Reverse translation for %q does not exist", text) + end + return translation +end + +function AceLocale.prototype:GetIterator() + local x = rawget(self, TRANSLATIONS) + if not x then + AceLocale.error(self, "No translations registered") + end + return next, x, nil +end + +function AceLocale.prototype:GetReverseIterator() + local x = rawget(self, REVERSE_TRANSLATIONS) + if not x then + if not rawget(self, TRANSLATIONS) then + AceLocale.error(self, "No translations registered") + end + initReverse(self) + x = self[REVERSE_TRANSLATIONS] + end + return next, x, nil +end + +function AceLocale.prototype:HasTranslation(text) + AceLocale.argCheck(self, text, 1, "string", "number") + local x = rawget(self, TRANSLATIONS) + if not x then + AceLocale.error(self, "No translations registered") + end + return rawget(x, text) and true +end + +function AceLocale.prototype:HasReverseTranslation(text) + local x = rawget(self, REVERSE_TRANSLATIONS) + if not x then + if not rawget(self, TRANSLATIONS) then + AceLocale.error(self, "No translations registered") + end + initReverse(self) + x = self[REVERSE_TRANSLATIONS] + end + return x[text] and true +end + +function AceLocale.prototype:Debug() + if not rawget(self, DEBUGGING) then + return + end + local words = {} + local locales = {"enUS", "deDE", "frFR", "koKR", "zhCN", "zhTW", "esES"} + local localizations = {} + DEFAULT_CHAT_FRAME:AddMessage("--- AceLocale Debug ---") + for _,locale in ipairs(locales) do + if not self[TRANSLATION_TABLES][locale] then + DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q not found", locale)) + else + localizations[locale] = self[TRANSLATION_TABLES][locale] + end + end + local localeDebug = {} + for locale, localization in pairs(localizations) do + localeDebug[locale] = {} + for word in pairs(localization) do + if type(localization[word]) == "table" then + if type(words[word]) ~= "table" then + words[word] = {} + end + for bit in pairs(localization[word]) do + if type(localization[word][bit]) == "string" then + words[word][bit] = true + end + end + elseif type(localization[word]) == "string" then + words[word] = true + end + end + end + for word in pairs(words) do + if type(words[word]) == "table" then + for bit in pairs(words[word]) do + for locale, localization in pairs(localizations) do + if not rawget(localization, word) or not localization[word][bit] then + localeDebug[locale][word .. "::" .. bit] = true + end + end + end + else + for locale, localization in pairs(localizations) do + if not rawget(localization, word) then + localeDebug[locale][word] = true + end + end + end + end + for locale, t in pairs(localeDebug) do + if not next(t) then + DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q complete", locale)) + else + DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q missing:", locale)) + for word in pairs(t) do + DEFAULT_CHAT_FRAME:AddMessage(string.format(" %q", word)) + end + end + end + DEFAULT_CHAT_FRAME:AddMessage("--- End AceLocale Debug ---") +end + +setmetatable(AceLocale.prototype, { + __index = function(self, k) + if type(k) ~= "table" and k ~= 0 and k ~= "GetLibraryVersion" and k ~= "error" and k ~= "assert" and k ~= "argCheck" and k ~= "pcall" then -- HACK: remove "GetLibraryVersion" and such later. + AceLocale.error(lastSelf or self, "Translation %q does not exist.", k) + end + return nil + end +}) + +local function activate(self, oldLib, oldDeactivate) + AceLocale = self + + self.frame = oldLib and oldLib.frame or CreateFrame("Frame") + self.registry = oldLib and oldLib.registry or {} + self.BASE_TRANSLATIONS = oldLib and oldLib.BASE_TRANSLATIONS or {} + self.DEBUGGING = oldLib and oldLib.DEBUGGING or {} + self.TRANSLATIONS = oldLib and oldLib.TRANSLATIONS or {} + self.BASE_LOCALE = oldLib and oldLib.BASE_LOCALE or {} + self.TRANSLATION_TABLES = oldLib and oldLib.TRANSLATION_TABLES or {} + self.REVERSE_TRANSLATIONS = oldLib and oldLib.REVERSE_TRANSLATIONS or {} + self.STRICTNESS = oldLib and oldLib.STRICTNESS or {} + self.NAME = oldLib and oldLib.NAME or {} + self.DYNAMIC_LOCALES = oldLib and oldLib.DYNAMIC_LOCALES or {} + self.CURRENT_LOCALE = oldLib and oldLib.CURRENT_LOCALE or {} + + BASE_TRANSLATIONS = self.BASE_TRANSLATIONS + DEBUGGING = self.DEBUGGING + TRANSLATIONS = self.TRANSLATIONS + BASE_LOCALE = self.BASE_LOCALE + TRANSLATION_TABLES = self.TRANSLATION_TABLES + REVERSE_TRANSLATIONS = self.REVERSE_TRANSLATIONS + STRICTNESS = self.STRICTNESS + NAME = self.NAME + DYNAMIC_LOCALES = self.DYNAMIC_LOCALES + CURRENT_LOCALE = self.CURRENT_LOCALE + + + local GetTime = GetTime + local timeUntilClear = GetTime() + 5 + scheduleClear = function() + if next(newRegistries) then + self.frame:Show() + timeUntilClear = GetTime() + 5 + end + end + + if not self.registry then + self.registry = {} + else + for name, instance in pairs(self.registry) do + local name = name + local mt = getmetatable(instance) + setmetatable(instance, nil) + instance[NAME] = name + local strict + if instance[STRICTNESS] ~= nil then + strict = instance[STRICTNESS] + elseif instance[TRANSLATIONS] ~= instance[BASE_TRANSLATIONS] then + if getmetatable(instance[TRANSLATIONS]).__index == oldLib.prototype then + strict = true + end + end + instance[STRICTNESS] = strict and true or false + refixInstance(instance) + end + end + + self.frame:SetScript("OnEvent", scheduleClear) + self.frame:SetScript("OnUpdate", function() -- (this, elapsed) + if timeUntilClear - GetTime() <= 0 then + self.frame:Hide() + for k in pairs(newRegistries) do + clearCache(k) + newRegistries[k] = nil + k = nil + end + end + end) + self.frame:UnregisterAllEvents() + self.frame:RegisterEvent("ADDON_LOADED") + self.frame:RegisterEvent("PLAYER_ENTERING_WORLD") + self.frame:Show() + + if oldDeactivate then + oldDeactivate(oldLib) + end +end + +AceLibrary:Register(AceLocale, MAJOR_VERSION, MINOR_VERSION, activate) diff --git a/libs/ace/AceOO-2.0/AceOO-2.0.lua b/libs/ace/AceOO-2.0/AceOO-2.0.lua new file mode 100644 index 0000000..ca1f070 --- /dev/null +++ b/libs/ace/AceOO-2.0/AceOO-2.0.lua @@ -0,0 +1,1004 @@ +--[[ +Name: AceOO-2.0 +Revision: $Rev: 17638 $ +Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) +Inspired By: Ace 1.x by Turan (turan@gryphon.com) +Website: http://www.wowace.com/ +Documentation: http://www.wowace.com/index.php/AceOO-2.0 +SVN: http://svn.wowace.com/root/trunk/Ace2/AceOO-2.0 +Description: Library to provide an object-orientation framework. +Dependencies: AceLibrary +]] + +local MAJOR_VERSION = "AceOO-2.0" +local MINOR_VERSION = "$Revision: 17638 $" + +-- This ensures the code is only executed if the libary doesn't already exist, or is a newer version +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +local AceOO = { + error = AceLibrary.error, + argCheck = AceLibrary.argCheck +} + +-- @function getuid +-- @brief Obtain a unique string identifier for the object in question. +-- @param t The object to obtain the uid for. +-- @return The uid string. +local function pad(cap) + return string.rep('0', 8 - string.len(cap)) .. cap +end +local function getuid(t) + local mt = getmetatable(t) + setmetatable(t, nil) + local str = tostring(t) + setmetatable(t, mt) + local _,_,cap = string.find(str, '[^:]*: 0x(.*)$') + if cap then return pad(cap) end + _,_,cap = string.find(str, '[^:]*: (.*)$') + if cap then return pad(cap) end +end + +local function getlibrary(o) + if type(o) == "table" then + return o + elseif type(o) == "string" then + if not AceLibrary:HasInstance(o) then + AceOO:error("Library %q does not exist.", o) + end + return AceLibrary(o) + end +end + +-- @function Factory +-- @brief Construct a factory for the creation of objects. +-- @param obj The object whose init method will be called on the new factory +-- object. +-- @param newobj The object whose init method will be called on the new +-- objects that the Factory creates, to initialize them. +-- @param (a1..a20) Arguments which will be passed to obj.init() in addition +-- to the Factory object. +-- @return The new factory which creates a newobj when its new method is called, +-- or when it is called directly (__call metamethod). +local Factory +do + local function new(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, + a13, a14, a15, a16, a17, a18, a19, a20) + local t = {} + local uid = getuid(t) + local l = getlibrary + obj:init(t, l(a1), l(a2), l(a3), l(a4), l(a5), l(a6), l(a7), + l(a8), l(a9), l(a10), l(a11), l(a12), l(a13), + l(a14), l(a15), l(a16), l(a17), l(a18), l(a19), + l(a20)) + t.uid = uid + return t + end + + local function createnew(self, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, + a19, a20) + local o = self.prototype + local l = getlibrary + return new(o, l(a1), l(a2), l(a3), l(a4), l(a5), l(a6), l(a7), + l(a8), l(a9), l(a10), l(a11), l(a12), l(a13), + l(a14), l(a15), l(a16), l(a17), l(a18), l(a19), + l(a20)) + end + + function Factory(obj, newobj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, + a11, a12, a13, a14, a15, a16, a17, a18, + a19, a20) + local t = new(obj, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, + a13, a14, a15, a16, a17, a18, a19, a20) + t.prototype = newobj + t.new = createnew + getmetatable(t).__call = t.new + return t + end +end + + +local function objtostring(self) + if self.ToString then + return self:ToString() + elseif self.GetLibraryVersion then + return (self:GetLibraryVersion()) + elseif self.super then + local s = "Sub-" .. tostring(self.super) + local first = true + if self.interfaces then + for interface in pairs(self.interfaces) do + if first then + s = s .. "(" .. tostring(interface) + first = false + else + s = s .. ", " .. tostring(interface) + end + end + end + if self.mixins then + for mixin in pairs(self.mixins) do + if first then + s = s .. tostring(mixin) + first = false + else + s = s .. ", " .. tostring(mixin) + end + end + end + if first then + if self.uid then + return s .. ":" .. self.uid + else + return s + end + else + return s .. ")" + end + else + return self.uid and 'Subclass:' .. self.uid or 'Subclass' + end +end + +-- @table Object +-- @brief Base of all objects, including Class. +-- +-- @method init +-- @brief Initialize a new object. +-- @param newobject The object to initialize +-- @param class The class to make newobject inherit from +local Object +do + Object = {} + function Object:init(newobject, class) + local parent = class or self + if not rawget(newobject, 'uid') then + newobject.uid = getuid(newobject) + end + local mt = { + __index = parent, + __tostring = objtostring, + } + setmetatable(newobject, mt) + end + Object.uid = getuid(Object) + setmetatable(Object, { __tostring = function() return 'Object' end }) +end + +local Interface + +local function validateInterface(object, interface) + if not object.class and object.prototype then + object = object.prototype + end + for k,v in pairs(interface.interface) do + if tostring(type(object[k])) ~= v then + return false + end + end + if interface.superinterfaces then + for superinterface in pairs(interface.superinterfaces) do + if not validateInterface(object, superinterface) then + return false + end + end + end + if type(object.class) == "table" and rawequal(object.class.prototype, object) then + if not object.class.interfaces then + rawset(object.class, 'interfaces', {}) + end + object.class.interfaces[interface] = true + elseif type(object.class) == "table" and type(object.class.prototype) == "table" then + validateInterface(object.class.prototype, interface) + -- check if class is proper, thus preventing future checks. + end + return true +end + +-- @function inherits +-- @brief Return whether an Object or Class inherits from a given +-- parent. +-- @param object Object or Class to check +-- @param parent Parent to test inheritance from +-- @return whether an Object or Class inherits from a given +-- parent. +local function inherits(object, parent) + object = getlibrary(object) + if type(parent) == "string" then + if not AceLibrary:HasInstance(parent) then + return false + else + parent = AceLibrary(parent) + end + end + AceOO:argCheck(parent, 2, "table") + if type(object) ~= "table" then + return false + end + local current + if object.class then + current = object.class + else + current = object + end + if type(current) ~= "table" then + return false + end + if rawequal(current, parent) then + return true + end + if parent.class then + while true do + if rawequal(current, Object) then + break + end + if current.mixins then + for mixin in pairs(current.mixins) do + if rawequal(mixin, parent) then + return true + end + end + end + if current.interfaces then + for interface in pairs(current.interfaces) do + if rawequal(interface, parent) then + return true + end + end + end + current = current.super + if type(current) ~= "table" then + break + end + end + + local isInterface = false + local curr = parent.class + while true do + if rawequal(curr, Object) then + break + elseif rawequal(curr, Interface) then + isInterface = true + break + end + curr = curr.super + if type(curr) ~= "table" then + break + end + end + return isInterface and validateInterface(object, parent) + else + while true do + if rawequal(current, parent) then + return true + elseif rawequal(current, Object) then + return false + end + current = current.super + if type(current) ~= "table" then + return false + end + end + end +end + +-- @table Class +-- @brief An object factory which sets up inheritence and supports +-- 'mixins'. +-- +-- @metamethod Class call +-- @brief Call ClassFactory:new() to create a new class. +-- +-- @method Class new +-- @brief Construct a new object. +-- @param (a1..a20) Arguments to pass to the object init function. +-- @return The new object. +-- +-- @method Class init +-- @brief Initialize a new class. +-- @param parent Superclass. +-- @param (a1..a20) Mixins. +-- +-- @method Class ToString +-- @return A string representing the object, in this case 'Class'. +local initStatus +local Class +local Mixin +local autoEmbed = false +local function traverseInterfaces(bit, total) + if bit.superinterfaces then + for interface in pairs(bit.superinterfaces) do + if not total[interface] then + total[interface] = true + traverseInterfaces(interface, total) + end + end + end +end +local class_new +do + Class = Factory(Object, setmetatable({}, {__index = Object}), Object) + Class.super = Object + + local function protostring(t) + return '<' .. tostring(t.class) .. ' prototype>' + end + local function classobjectstring(t) + if t.ToString then + return t:ToString() + elseif t.GetLibraryVersion then + return (t:GetLibraryVersion()) + else + return '<' .. tostring(t.class) .. ' instance>' + end + end + local function classobjectequal(self, other) + if type(self) == "table" and self.Equals then + return self:Equals(other) + elseif type(other) == "table" and other.Equals then + return other:Equals(self) + elseif type(self) == "table" and self.CompareTo then + return self:CompareTo(other) == 0 + elseif type(other) == "table" and other.CompareTo then + return other:CompareTo(self) == 0 + else + return rawequal(self, other) + end + end + local function classobjectlessthan(self, other) + if type(self) == "table" and self.IsLessThan then + return self:IsLessThan(other) + elseif type(other) == "table" and other.IsLessThanOrEqualTo then + return not other:IsLessThanOrEqualTo(self) + elseif type(self) == "table" and self.CompareTo then + return self:CompareTo(other) < 0 + elseif type(other) == "table" and other.CompareTo then + return other:CompareTo(self) > 0 + elseif type(other) == "table" and other.IsLessThan and other.Equals then + return other:Equals(self) or other:IsLessThan(self) + else + AceOO:error("cannot compare two objects") + end + end + local function classobjectlessthanequal(self, other) + if type(self) == "table" and self.IsLessThanOrEqualTo then + return self:IsLessThanOrEqualTo(other) + elseif type(other) == "table" and other.IsLessThan then + return not other:IsLessThan(self) + elseif type(self) == "table" and self.CompareTo then + return self:CompareTo(other) <= 0 + elseif type(other) == "table" and other.CompareTo then + return other:CompareTo(self) >= 0 + elseif type(self) == "table" and self.IsLessThan and self.Equals then + return self:Equals(other) or self:IsLessThan(other) + else + AceOO:error("cannot compare two incompatible objects") + end + end + local function classobjectadd(self, other) + if type(self) == "table" and self.Add then + return self:Add(other) + else + AceOO:error("cannot add two incompatible objects") + end + end + local function classobjectsub(self, other) + if type(self) == "table" and self.Subtract then + return self:Subtract(other) + else + AceOO:error("cannot subtract two incompatible objects") + end + end + local function classobjectunm(self, other) + if type(self) == "table" and self.UnaryNegation then + return self:UnaryNegation(other) + else + AceOO:error("attempt to negate an incompatible object") + end + end + local function classobjectmul(self, other) + if type(self) == "table" and self.Multiply then + return self:Multiply(other) + else + AceOO:error("cannot multiply two incompatible objects") + end + end + local function classobjectdiv(self, other) + if type(self) == "table" and self.Divide then + return self:Divide(other) + else + AceOO:error("cannot divide two incompatible objects") + end + end + local function classobjectpow(self, other) + if type(self) == "table" and self.Exponent then + return self:Exponent(other) + else + AceOO:error("cannot exponentiate two incompatible objects") + end + end + local function classobjectconcat(self, other) + if type(self) == "table" and self.Concatenate then + return self:Concatenate(other) + else + AceOO:error("cannot concatenate two incompatible objects") + end + end + function class_new(self, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, + a13, a14, a15, a16, a17, a18, a19, a20) + if self.virtual then + AceOO:error("Cannot instantiate a virtual class.") + end + + local o = self.prototype + local newobj = {} + if o.class and o.class.instancemeta then + setmetatable(newobj, o.class.instancemeta) + else + Object:init(newobj, o) + end + + if self.interfaces and not self.interfacesVerified then + -- Verify the interfaces + + for interface in pairs(self.interfaces) do + for field,kind in pairs(interface.interface) do + if tostring(type(newobj[field])) ~= kind then + AceOO:error("Class did not satisfy all interfaces. %q is required to be a %s. It is a %s", field, kind, tostring(type(newobj[field]))) + end + end + end + self.interfacesVerified = true + end + local tmp = initStatus + initStatus = newobj + newobj:init(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, + a13, a14, a15, a16, a17, a18, a19, a20) + if initStatus then + initStatus = tmp + AceOO:error("Initialization not completed, be sure to call the superclass's init method.") + return + end + initStatus = tmp + return newobj + end + local classmeta = { + __tostring = objtostring, + __call = function(self, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, + a13, a14, a15, a16, a17, a18, a19, a20) + return self:new(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, + a13, a14, a15, a16, a17, a18, a19, a20) + end, + } + function Class:init(newclass, parent, a1, a2, a3, a4, a5, a6, a7, a8, a9, + a10, a11, a12, a13, a14, a15, a16, + a17, a18, a19, a20) + parent = parent or self + + local total + + if parent.class then + total = { + parent, a1, a2, a3, a4, a5, a6, a7, a8, a9, + a10, a11, a12, a13, a14, a15, a16, + a17, a18, a19, a20 + } + parent = self + else + total = { + a1, a2, a3, a4, a5, a6, a7, a8, a9, + a10, a11, a12, a13, a14, a15, a16, + a17, a18, a19, a20 + } + end + if not inherits(parent, Class) then + AceOO:error("Classes must inherit from a proper class") + end + if parent.sealed then + AceOO:error("Cannot inherit from a sealed class") + end + for i,v in ipairs(total) do + if inherits(v, Mixin) and v.class then + if not newclass.mixins then + newclass.mixins = {} + end + if newclass.mixins[v] then + AceOO:error("Cannot explicitly inherit from the same mixin twice") + end + newclass.mixins[v] = true + elseif inherits(v, Interface) and v.class then + if not newclass.interfaces then + newclass.interfaces = {} + end + if newclass.interfaces[v] then + AceOO:error("Cannot explicitly inherit from the same interface twice") + end + newclass.interfaces[v] = true + else + AceOO:error("Classes can only inherit from one or zero classes and any number of mixins or interfaces") + end + end + if parent.interfaces then + if newclass.interfaces then + for interface in pairs(parent.interfaces) do + newclass.interfaces[interface] = true + end + else + newclass.interfaces = parent.interfaces + end + end + for k in pairs(total) do + total[k] = nil + end + table_setn(total, 0) + + newclass.super = parent + + newclass.prototype = setmetatable(total, { + __index = parent.prototype, + __tostring = protostring, + }) + total = nil + + newclass.instancemeta = { + __index = newclass.prototype, + __tostring = classobjectstring, + __eq = classobjectequal, + __lt = classobjectlessthan, + __le = classobjectlessthanequal, + __add = classobjectadd, + __sub = classobjectsub, + __unm = classobjectunm, + __mul = classobjectmul, + __div = classobjectdiv, + __pow = classobjectpow, + __concat = classobjectconcat, + } + + setmetatable(newclass, classmeta) + + newclass.new = class_new + + if newclass.mixins then + -- Fold in the mixins + local err, msg + for mixin in pairs(newclass.mixins) do + local ret + autoEmbed = true + ret, msg = pcall(mixin.embed, mixin, newclass.prototype) + autoEmbed = false + if not ret then + err = true + break + end + end + + if err then + local pt = newclass.prototype + for k,v in pairs(pt) do + pt[k] = nil + end + + -- method conflict + AceOO:error(msg) + end + end + + newclass.prototype.class = newclass + + if newclass.interfaces then + for interface in pairs(newclass.interfaces) do + traverseInterfaces(interface, newclass.interfaces) + end + end + if newclass.mixins then + for mixin in pairs(newclass.mixins) do + if mixin.interfaces then + if not newclass.interfaces then + newclass.interfaces = {} + end + for interface in pairs(mixin.interfaces) do + newclass.interfaces[interface] = true + end + end + end + end + end + function Class:ToString() + if type(self.GetLibraryVersion) == "function" then + return (self:GetLibraryVersion()) + else + return "Class" + end + end + + local tmp + function Class.prototype:init() + if rawequal(self, initStatus) then + initStatus = nil + else + AceOO:error("Improper self passed to init. You must do MyClass.super.prototype.init(self, ...)", 2) + end + self.uid = getuid(self) + local current = self.class + while true do + if current == Class then + break + end + if current.mixins then + for mixin in pairs(current.mixins) do + if type(mixin.OnInstanceInit) == "function" then + mixin:OnInstanceInit(self) + end + end + end + current = current.super + end + end +end + + +-- @object ClassFactory +-- @brief A factory for creating classes. Rarely used directly. +local ClassFactory = Factory(Object, Class, Object) + +function Class:new(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, + a12, a13, a14, a15, a16, a17, a18, a19, a20) + local x = ClassFactory:new(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, + a12, a13, a14, a15, a16, a17, a18, a19, a20) + if AceOO.classes then + AceOO.classes[x] = true + end + return x +end +getmetatable(Class).__call = Class.new + +-- @class Mixin +-- @brief A class to create mixin objects, which contain methods that get +-- "mixed in" to class prototypes. +-- +-- @object Mixin prototype +-- @brief The prototype that mixin objects inherit their methods from. +-- +-- @method Mixin prototype embed +-- @brief Mix in the methods of our object which are listed in our interface +-- to the supplied target table. +-- +-- @method Mixin prototype init +-- @brief Initialize the mixin object. +-- @param newobj The new object we're initializing. +-- @param interface The interface we implement (the list of methods our +-- prototype provides which should be mixed into the target +-- table by embed). +do + Mixin = Class() + function Mixin:ToString() + if self.GetLibraryVersion then + return (self:GetLibraryVersion()) + else + return 'Mixin' + end + end + local function _Embed(state, field, target) + field = next(state.export, field) + if field == nil then + return + end + + if rawget(target, field) or (target[field] and target[field] ~= state[field]) then + AceOO:error("Method conflict in attempt to mixin. Field %q", field) + end + + target[field] = state[field] + + local ret,msg = pcall(_Embed, state, field, target) + if not ret then + -- Mix in the next method according to the defined interface. If that + -- fails due to a conflict, re-raise to back out the previous mixed + -- methods. + + target[field] = nil + AceOO:error(msg) + end + end + function Mixin.prototype:embed(target) + local mt = getmetatable(target) + setmetatable(target, nil) + local err, msg = pcall(_Embed, self, nil, target) + if not err then + setmetatable(target, mt) + AceOO:error(msg) + return + end + if type(self.embedList) == "table" then + self.embedList[target] = true + end + if type(target.class) ~= "table" then + target[self] = true + end + if not autoEmbed and type(self.OnManualEmbed) == "function" then + self:OnManualEmbed(target) + end + setmetatable(target, mt) + end + + function Mixin.prototype:activate(oldLib, oldDeactivate) + if oldLib and oldLib.embedList then + for target in pairs(oldLib.embedList) do + local mt = getmetatable(target) + setmetatable(target, nil) + for field in pairs(oldLib.export) do + target[field] = nil + end + setmetatable(target, mt) + end + self.embedList = oldLib.embedList + for target in pairs(self.embedList) do + self:embed(target) + end + else + self.embedList = setmetatable({}, {__mode="k"}) + end + end + + function Mixin.prototype:init(export, a1, a2, a3, a4, a5, a6, a7, a8, a9, + a10, a11, a12, a13, a14, a15, a16, + a17, a18, a19, a20) + AceOO:argCheck(export, 2, "table") + for k,v in pairs(export) do + if type(k) ~= "number" then + AceOO:error("All keys to argument #2 must be numbers.") + elseif type(v) ~= "string" then + AceOO:error("All values to argument #2 must be strings.") + end + end + local num = table.getn(export) + for i = 1, num do + local v = export[i] + export[i] = nil + export[v] = true + end + table_setn(export, 0) + local interfaces + if a1 then + local l = getlibrary + interfaces = { l(a1), l(a2), l(a3), l(a4), l(a5), l(a6), l(a7), l(a8), + l(a9), l(a10), l(a11), l(a12), l(a13), l(a14), l(a15), l(a16), + l(a17), l(a18), l(a19), l(a20) } + for _,v in ipairs(interfaces) do + if not v.class or not inherits(v, Interface) then + AceOO:error("Mixins can inherit only from interfaces") + end + end + local num = table.getn(interfaces) + for i = 1, num do + local v = interfaces[i] + interfaces[i] = nil + interfaces[v] = true + end + table_setn(interfaces, 0) + for interface in pairs(interfaces) do + traverseInterfaces(interface, interfaces) + end + for interface in pairs(interfaces) do + for field,kind in pairs(interface.interface) do + if kind ~= "nil" then + local good = false + for bit in pairs(export) do + if bit == field then + good = true + break + end + end + if not good then + AceOO:error("Mixin does not fully accommodate field %q", field) + end + end + end + end + end + self.super = Mixin.prototype + Mixin.super.prototype.init(self) + self.export = export + self.interfaces = interfaces + end +end + +-- @class Interface +-- @brief A class to create interfaces, which contain contracts that classes +-- which inherit from this must comply with. +-- +-- @object Interface prototype +-- @brief The prototype that interface objects must adhere to. +-- +-- @method Interface prototype init +-- @brief Initialize the mixin object. +-- @param interface The interface we contract (the hash of fields forced). +-- @param (a1..a20) Superinterfaces +do + Interface = Class() + function Interface:ToString() + if self.GetLibraryVersion then + return (self:GetLibraryVersion()) + else + return 'Instance' + end + end + function Interface.prototype:init(interface, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) + Interface.super.prototype.init(self) + AceOO:argCheck(interface, 2, "table") + for k,v in pairs(interface) do + if type(k) ~= "string" then + AceOO:error("All keys to argument #2 must be numbers.") + elseif type(v) ~= "string" then + AceOO:error("All values to argument #2 must be strings.") + elseif v ~= "nil" and v ~= "string" and v ~= "number" and v ~= "table" and v ~= "function" then + AceOO:error('All values to argument #2 must either be "nil", "string", "number", "table", or "function".') + end + end + local l = getlibrary + a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 = l(a1), l(a2), l(a3), l(a4), l(a5), l(a6), l(a7), l(a8), l(a9), l(a10), l(a11), l(a12), l(a13), l(a14), l(a15), l(a16), l(a17), l(a18), l(a19), l(a20) + if a1 then + self.superinterfaces = {a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20} + for k,v in ipairs(self.superinterfaces) do + if not inherits(v, Interface) or not v.class then + AceOO:error('Cannot provide a non-Interface to inherit from') + end + end + local num = table.getn(self.superinterfaces) + for i = 1, num do + local v = self.superinterfaces[i] + self.superinterfaces[i] = nil + self.superinterfaces[v] = true + end + table_setn(self.superinterfaces, 0) + end + self.interface = interface + end +end + +-- @function Classpool +-- @brief Obtain a read only class from our pool of classes, indexed by the +-- superclass and mixins. +-- @param sc The superclass of the class we want. +-- @param (m1..m20) Mixins of the class we want's objects. +-- @return A read only class from the class pool. +local Classpool +do + local pool = setmetatable({}, {__mode = 'v'}) + local function newindex(k, v) + AceOO:error('Attempt to modify a read-only class.') + end + local function protonewindex(k, v) + AceOO:error('Attempt to modify a read-only class prototype.') + end + local function ts(bit) + if type(bit) ~= "table" then + return tostring(bit) + elseif getmetatable(bit) and bit.__tostring then + return tostring(bit) + elseif type(bit.GetLibraryVersion) == "function" then + return bit:GetLibraryVersion() + else + return tostring(bit) + end + end + local t + local function getcomplexuid(sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, + m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20) + if not t then t = {} end + if sc then if sc.uid then table.insert(t, sc.uid) else AceOO:error("%s is not an appropriate class/mixin", ts(sc)) end + if m1 then if m1.uid then table.insert(t, m1.uid) else AceOO:error("%s is not an appropriate mixin", ts(m1)) end + if m2 then if m2.uid then table.insert(t, m2.uid) else AceOO:error("%s is not an appropriate mixin", ts(m2)) end + if m3 then if m3.uid then table.insert(t, m3.uid) else AceOO:error("%s is not an appropriate mixin", ts(m3)) end + if m4 then if m4.uid then table.insert(t, m4.uid) else AceOO:error("%s is not an appropriate mixin", ts(m4)) end + if m5 then if m5.uid then table.insert(t, m5.uid) else AceOO:error("%s is not an appropriate mixin", ts(m5)) end + if m6 then if m6.uid then table.insert(t, m6.uid) else AceOO:error("%s is not an appropriate mixin", ts(m6)) end + if m7 then if m7.uid then table.insert(t, m7.uid) else AceOO:error("%s is not an appropriate mixin", ts(m7)) end + if m8 then if m8.uid then table.insert(t, m8.uid) else AceOO:error("%s is not an appropriate mixin", ts(m8)) end + if m9 then if m9.uid then table.insert(t, m9.uid) else AceOO:error("%s is not an appropriate mixin", ts(m9)) end + if m10 then if m10.uid then table.insert(t, m10.uid) else AceOO:error("%s is not an appropriate mixin", ts(m10)) end + if m11 then if m11.uid then table.insert(t, m11.uid) else AceOO:error("%s is not an appropriate mixin", ts(m11)) end + if m12 then if m12.uid then table.insert(t, m12.uid) else AceOO:error("%s is not an appropriate mixin", ts(m12)) end + if m13 then if m13.uid then table.insert(t, m13.uid) else AceOO:error("%s is not an appropriate mixin", ts(m13)) end + if m14 then if m14.uid then table.insert(t, m14.uid) else AceOO:error("%s is not an appropriate mixin", ts(m14)) end + if m15 then if m15.uid then table.insert(t, m15.uid) else AceOO:error("%s is not an appropriate mixin", ts(m15)) end + if m16 then if m16.uid then table.insert(t, m16.uid) else AceOO:error("%s is not an appropriate mixin", ts(m16)) end + if m17 then if m17.uid then table.insert(t, m17.uid) else AceOO:error("%s is not an appropriate mixin", ts(m17)) end + if m18 then if m18.uid then table.insert(t, m18.uid) else AceOO:error("%s is not an appropriate mixin", ts(m18)) end + if m19 then if m19.uid then table.insert(t, m19.uid) else AceOO:error("%s is not an appropriate mixin", ts(m19)) end + if m20 then if m20.uid then table.insert(t, m20.uid) else AceOO:error("%s is not an appropriate mixin", ts(m20)) end + end end end end end end end end end end end end end end end end end end end end end + table.sort(t) + local uid = table.concat(t, '') + for k in pairs(t) do t[k] = nil end + table_setn(t, 0) + return uid + end + local classmeta + function Classpool(sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, + m10, m11, m12, m13, m14, m15, m16, + m17, m18, m19, m20) + local l = getlibrary + sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20 = l(sc), l(m1), l(m2), l(m3), l(m4), l(m5), l(m6), l(m7), l(m8), l(m9), l(m10), l(m11), l(m12), l(m13), l(m14), l(m15), l(m16), l(m17), l(m18), l(m19), l(m20) + if sc and sc.class then + sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20 = Class, sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19 + end + sc = sc or Class + local key = getcomplexuid(sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17, m18, m19, m20) + if not pool[key] then + local class = Class(sc, m1, m2, m3, m4, m5, m6, m7, m8, m9, + m10, m11, m12, m13, m14, m15, m16, m17, + m18, m19, m20) + if not classmeta then + classmeta = {} + local mt = getmetatable(class) + for k,v in pairs(mt) do + classmeta[k] = v + end + classmeta.__newindex = newindex + end + -- Prevent the user from adding methods to this class. + -- NOTE: I'm not preventing modifications of existing class members, + -- but it's likely that only a truly malicious user will be doing so. + class.sealed = true + setmetatable(class, classmeta) + getmetatable(class.prototype).__newindex = protonewindex + pool[key] = class + end + return pool[key] + end +end + +AceOO.Factory = Factory +AceOO.Object = Object +AceOO.Class = Class +AceOO.Mixin = Mixin +AceOO.Interface = Interface +AceOO.Classpool = Classpool +AceOO.inherits = inherits + +-- Library handling bits + +local function activate(self, oldLib, oldDeactivate) + AceOO = self + Factory = self.Factory + Object = self.Object + Class = self.Class + ClassFactory.prototype = Class + Mixin = self.Mixin + Interface = self.Interface + Classpool = self.Classpool + + if oldLib then + self.classes = oldLib.classes + end + if not self.classes then + self.classes = setmetatable({}, {__mode="k"}) + else + for class in pairs(self.classes) do + class.new = class_new + end + end + + if oldDeactivate then + oldDeactivate(oldLib) + end +end + +AceLibrary:Register(AceOO, MAJOR_VERSION, MINOR_VERSION, activate) +AceOO = AceLibrary(MAJOR_VERSION) diff --git a/libs/ace/Banzai-1.0/Banzai-1.0.lua b/libs/ace/Banzai-1.0/Banzai-1.0.lua new file mode 100644 index 0000000..3cfd276 --- /dev/null +++ b/libs/ace/Banzai-1.0/Banzai-1.0.lua @@ -0,0 +1,168 @@ +--[[ +Name: Banzai-1.0 +Revision: $Rev: 14544 $ +Author(s): Rabbit (rabbit.magtheridon@gmail.com), maia +Documentation: http://www.wowace.com/index.php/Banzai-1.0_API_Documentation +SVN: http://svn.wowace.com/root/trunk/BanzaiLib/Banzai-1.0 +Description: Aggro notification library. +Dependencies: AceLibrary, AceEvent-2.0, RosterLib-2.0 +]] + +------------------------------------------------------------------------------- +-- Locals +------------------------------------------------------------------------------- + +local MAJOR_VERSION = "Banzai-1.0" +local MINOR_VERSION = "$Revision: 14544 $" + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end +if not AceLibrary:HasInstance("RosterLib-2.0") then error(MAJOR_VERSION .. " requires RosterLib-2.0.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +local lib = {} +AceLibrary("AceEvent-2.0"):embed(lib) + +local RL = nil +local roster = nil +local playerName = nil + +------------------------------------------------------------------------------- +-- Compost Heap, courtesy of Tekkub/SEEA. +------------------------------------------------------------------------------- + +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +local heap = {} +setmetatable(heap, {__mode = "kv"}) + +local function acquire() + local t = next(heap) + if t then + heap[t] = nil + assert(not next(t), "A table in the compost heap has been modified!") + end + t = t or {} + return t +end + + +local function reclaim(t, d) + if type(t) ~= "table" then return end + if d and d > 0 then + for i in pairs(t) do + if type(t[i]) == "table" then reclaim(t[i], d - 1) end + end + end + for i in pairs(t) do t[i] = nil end + table_setn(t, 0) + heap[t] = true +end + +------------------------------------------------------------------------------- +-- Initialization +------------------------------------------------------------------------------- + +-- Activate a new instance of this library +function activate(self, oldLib, oldDeactivate) + if oldLib then + self.vars = oldLib.vars + if oldLib:IsEventScheduled("UpdateAggroList") then + oldLib:CancelScheduledEvent("UpdateAggroList") + end + end + + RL = AceLibrary("RosterLib-2.0") + roster = RL.roster + playerName = UnitName("player") + self:ScheduleRepeatingEvent("UpdateAggroList", self.UpdateAggroList, 0.2, self) + + if oldDeactivate then oldDeactivate(oldLib) end +end + +------------------------------------------------------------------------------- +-- Library +------------------------------------------------------------------------------- + +function lib:UpdateAggroList() + local oldBanzai = acquire() + + if not roster then return end + for i, unit in pairs(roster) do + oldBanzai[unit.unitid] = unit.banzai + + -- deduct aggro for all, increase it later for everyone with aggro + if not unit.banzaiModifier then unit.banzaiModifier = 0 end + unit.banzaiModifier = math.max(0, unit.banzaiModifier - 5) + + -- check for aggro + local targetId = unit.unitid .. "target" + local targetName = UnitName(targetId .. "target") + if roster[targetName] and UnitCanAttack("player", targetId) and UnitCanAttack(targetId, "player") then + if not roster[targetName].banzaiModifier then roster[targetName].banzaiModifier = 0 end + roster[targetName].banzaiModifier = roster[targetName].banzaiModifier + 10 + if not roster[targetName].banzaiTarget then roster[targetName].banzaiTarget = acquire() end + table.insert(roster[targetName].banzaiTarget, targetId) + end + + -- cleanup + unit.banzaiModifier = math.max(0, unit.banzaiModifier) + unit.banzaiModifier = math.min(25, unit.banzaiModifier) + + -- set aggro + unit.banzai = (unit.banzaiModifier > 15) + end + + for i, unit in pairs(roster) do + if oldBanzai[unit.unitid] ~= nil and oldBanzai[unit.unitid] ~= unit.banzai then + -- Aggro status has changed. + if unit.banzai == true and unit.banzaiTarget then + -- Unit has aggro + self:TriggerEvent("Banzai_UnitGainedAggro", unit.unitid, unit.banzaiTarget) + if unit.name == playerName then + self:TriggerEvent("Banzai_PlayerGainedAggro", unit.banzaiTarget) + end + elseif unit.banzai == false then + -- Unit lost aggro + self:TriggerEvent("Banzai_UnitLostAggro", unit.unitid) + if unit.name == playerName then + self:TriggerEvent("Banzai_PlayerLostAggro", unit.unitid) + end + end + end + reclaim(unit.banzaiTarget) + unit.banzaiTarget = nil + end + + reclaim(oldBanzai) + oldBanzai = nil +end + +------------------------------------------------------------------------------- +-- API +------------------------------------------------------------------------------- + +function lib:GetUnitAggroByUnitId( unitId ) + local rosterUnit = RL:GetUnitObjectFromUnit(unitId) + if not rosterUnit then return nil end + return rosterUnit.banzai +end + +function lib:GetUnitAggroByUnitName( unitName ) + local rosterUnit = RL:GetUnitObjectFromName(unitName) + if not rosterUnit then return nil end + return rosterUnit.banzai +end + +------------------------------------------------------------------------------- +-- Register +------------------------------------------------------------------------------- +AceLibrary:Register(lib, MAJOR_VERSION, MINOR_VERSION, activate) diff --git a/libs/ace/Compost-2.0/Compost-2.0.lua b/libs/ace/Compost-2.0/Compost-2.0.lua new file mode 100644 index 0000000..3f2a000 --- /dev/null +++ b/libs/ace/Compost-2.0/Compost-2.0.lua @@ -0,0 +1,277 @@ + +--[[ +Name: Compost-2.0 +Revision: $Rev: 11579 $ +Author: Tekkub Stoutwrithe (tekkub@gmail.com) +Website: http://wiki.wowace.com/index.php/CompostLib +Documentation: http://wiki.wowace.com/index.php/Compost-2.0_API_Documentation +SVN: svn://svn.wowace.com/root/trunk/CompostLib/Compost-2.0 +Description: Recycle tables to reduce garbage generation +Dependencies: AceLibrary +]] + +local vmajor, vminor = "Compost-2.0", "$Revision: 11579 $" + +if not AceLibrary then error(vmajor .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(vmajor, vminor) then return end + +local lib = {} + +local table_setn +do + local version = GetBuildInfo() + if string.find(version, "^2%.") then + -- 2.0.0 + table_setn = function() end + else + table_setn = table.setn + end +end + +-- Activate a new instance of this library +local function activate(self, oldLib, oldDeactivate) + if oldLib then -- if upgrading + self.var, self.k = oldLib.var, oldLib.k + else + self.k = { -- Constants go here + maxcache = 10, -- I think this is a good number, I'll change it later if necessary + } + self.var = { -- "Local" variables go here + cache = {}, + secondarycache = {}, + } + + -- This makes the secondary cache table a weak table, any values in it will be reclaimed + -- during a GC if there are no other references to them + setmetatable(self.var.secondarycache, {__mode = "v"}) + end + if not self.var.tablechecks then + self.var.tablechecks = {} + setmetatable(self.var.tablechecks, {__mode = "kv"}) + for i,v in ipairs(self.var.cache) do self.var.tablechecks[v] = true end + for i,v in ipairs(self.var.secondarycache) do self.var.tablechecks[v] = true end + end + if oldDeactivate then oldDeactivate(oldLib) end +end + + +-- Removes an empty table from the cache and returns it +-- or generates a new table if none available +function lib:GetTable() + if self.var.disabled then return {} end + + if table.getn(self.var.cache) > 0 then + for i in pairs(self.var.cache) do + local t = table.remove(self.var.cache, i) + self.var.tablechecks[t] = nil + if next(t) then -- Table has been modified, someone holds a ref still, discard it + error("Someone is modifying tables reclaimed by Compost!") + self:IncDec("numdiscarded", 1) + else -- It's clean, we think... return it. + self:IncDec("totn", -1) + self:IncDec("numrecycled", 1) + return t + end + end + end + + if next(self.var.secondarycache) then + for i in pairs(self.var.secondarycache) do + local t = table.remove(self.var.secondarycache, i) + self.var.tablechecks[t] = nil + if next(t) then -- Table has been modified, someone holds a ref still, discard it + error("Someone is modifying tables reclaimed by Compost!") + self:IncDec("numdiscarded", 1) + else -- It's clean, we think... return it. + self:IncDec("totn", -1) + self:IncDec("numrecycled", 1) + return t + end + end + end + + self:IncDec("numnew", 1) + return {} +end + + +-- Returns a table, populated with any variables passed +-- basically: return {a1, a2, ... a20} +function lib:Acquire(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + local t = self:GetTable() + return self:Populate(t,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) +end + + +-- Acquires a table and fills it with values, hash style +-- basically: return {k1 = v1, k2 = v2, ... k10 = v10} +function lib:AcquireHash(k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) + local t = self:GetTable() + return self:PopulateHash(t,k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) +end + + +-- Erases the table passed, fills it with the args passed, and returns it +-- Essentially the same as doing Reclaim then Acquire, except the same table is reused +function lib:Recycle(t,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + t = self:Erase(t) + return self:Populate(t,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) +end + + +-- Erases the table passed, fills it with the args passed, and returns it +-- Essentially the same as doing Reclaim then AcquireHash, except the same table is reused +function lib:RecycleHash(t,k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) + t = self:Erase(t) + return self:PopulateHash(t,k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) +end + + +-- Returns a table to the cache +-- All tables referenced inside the passed table will be reclaimed also +-- If a depth is passed, Reclaim will call itsself recursivly +-- to reclaim all tables contained in t to the depth specified +function lib:Reclaim(t, depth) + if type(t) ~= "table" or self.var.disabled then return end + self:assert(not self.var.tablechecks[t], "Cannot reclaim a table twice") + + if not self:ItemsInSecondaryCache() then self.var.totn = table.getn(self.var.cache) end + + if depth and depth > 0 then + for i in pairs(t) do + if type(t[i]) == "table" then self:Reclaim(t[i], depth - 1) end + end + end + self:Erase(t) + if self.k.maxcache and table.getn(self.var.cache) >= self.k.maxcache then + table.insert(self.var.secondarycache, t) + else + table.insert(self.var.cache, t) + end + self:IncDec("numreclaim", 1) + self:IncDec("totn", 1) + self.var.maxn = math.max(self.var.maxn or 0, self.var.totn) + self.var.tablechecks[t] = true +end + + +-- Reclaims multiple tables, can take 10 recursive sets or 20 non-recursives, +-- or any combination of the two. Pass args in the following manner: +-- table1, depth1, tabl2, depth2, table3, table4, table5, depth5, ... +function lib:ReclaimMulti(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if not a1 then return end + if type(a2) == "number" then + self:Reclaim(a1, a2) + self:ReclaimMulti(a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + else + self:Reclaim(a1) + self:ReclaimMulti(a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + end +end + + +-- Erases the table passed, nothing more nothing less :) +-- Tables referenced inside the passed table are NOT erased +function lib:Erase(t) + if type(t) ~= "table" then return end + if self.var.disabled then return {} end + local mem = gcinfo() + setmetatable(t, nil) + for i in pairs(t) do + t[i] = nil + end + t.reset = 1 + t.reset = nil + table_setn(t, 0) + self:IncDec("memfreed", math.abs(gcinfo() - mem)) + self:IncDec("numerased", 1) + return t +end + + +-- Fills the table passed with the args passed +function lib:Populate(t,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) + if not t then return end + if a1 ~= nil then table.insert(t, a1) end + if a2 ~= nil then table.insert(t, a2) end + if a3 ~= nil then table.insert(t, a3) end + if a4 ~= nil then table.insert(t, a4) end + if a5 ~= nil then table.insert(t, a5) end + if a6 ~= nil then table.insert(t, a6) end + if a7 ~= nil then table.insert(t, a7) end + if a8 ~= nil then table.insert(t, a8) end + if a9 ~= nil then table.insert(t, a9) end + if a10 ~= nil then table.insert(t, a10) end + if a11 ~= nil then table.insert(t, a11) end + if a12 ~= nil then table.insert(t, a12) end + if a13 ~= nil then table.insert(t, a13) end + if a14 ~= nil then table.insert(t, a14) end + if a15 ~= nil then table.insert(t, a15) end + if a16 ~= nil then table.insert(t, a16) end + if a17 ~= nil then table.insert(t, a17) end + if a18 ~= nil then table.insert(t, a18) end + if a19 ~= nil then table.insert(t, a19) end + if a20 ~= nil then table.insert(t, a20) end + return t +end + + +-- Same as Populate, but takes 10 key-value pairs instead +function lib:PopulateHash(t,k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) + if not t then return end + if k1 ~= nil then t[k1] = v1 end + if k2 ~= nil then t[k2] = v2 end + if k3 ~= nil then t[k3] = v3 end + if k4 ~= nil then t[k4] = v4 end + if k5 ~= nil then t[k5] = v5 end + if k6 ~= nil then t[k6] = v6 end + if k7 ~= nil then t[k7] = v7 end + if k8 ~= nil then t[k8] = v8 end + if k9 ~= nil then t[k9] = v9 end + if k10 ~= nil then t[k10] = v10 end + return t +end + + +function lib:IncDec(variable, diff) + self.var[variable] = (self.var[variable] or 0) + diff +end + + +function lib:ItemsInSecondaryCache() + for i in pairs(self.var.secondarycache) do return true end +end + + +function lib:GetSecondaryCacheSize() + local n = 0 + for i in pairs(self.var.secondarycache) do n = n + 1 end + return n +end + + +-- Prints out statistics on table recycling +-- /script CompostLib:GetInstance("compost-1"):Stats() +function lib:Stats() + if self.var.disabled then ChatFrame1:AddMessage("CompostLib is disabled!") + else ChatFrame1:AddMessage( + string.format( + "|cff00ff00New: %d|r | |cffffff00Recycled: %d|r | |cff00ffffMain: %d|r | |cffff0000Secondary: %d|r | |cffff8800Max %d|r | |cff888888Erases: %d|r | |cffff00ffMem Saved: %d KiB|r | |cffff0088Lost to GC: %d", + self.var.numnew or 0, + self.var.numrecycled or 0, + table.getn(self.var.cache), + self:GetSecondaryCacheSize(), + self.var.maxn or 0, + (self.var.numerased or 0) - (self.var.numreclaim or 0), + (self.var.memfreed or 0) + 32/1024*(self.var.numrecycled or 0), + (self.var.numreclaim or 0) - (self.var.numrecycled or 0) - table.getn(self.var.cache))) + end +end + +setmetatable(lib, { __call = lib.Acquire }) + +-------------------------------- +-- Load this bitch! -- +-------------------------------- +AceLibrary:Register(lib, vmajor, vminor, activate) +lib = nil diff --git a/libs/ace/Deformat-2.0/Deformat-2.0.lua b/libs/ace/Deformat-2.0/Deformat-2.0.lua new file mode 100644 index 0000000..857a8e3 --- /dev/null +++ b/libs/ace/Deformat-2.0/Deformat-2.0.lua @@ -0,0 +1,343 @@ +--[[ +Name: Deformat-2.0 +Revision: $Rev: 6804 $ +Author(s): ckknight (ckknight@gmail.com) +Website: http://ckknight.wowinterface.com/ +Documentation: http://wiki.wowace.com/index.php/Deformat-2.0 +SVN: http://svn.wowace.com/root/trunk/Deformat/Deformat-2.0 +Description: A library to deformat format strings. +Dependencies: AceLibrary +]] + +local MAJOR_VERSION = "Deformat-2.0" +local MINOR_VERSION = "$Revision: 6804 $" + + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +local Deformat = {} + +do + local sequences = { + ["%d*d"] = "%%-?%%d+", + ["s"] = ".+", + ["[fg]"] = "%%-?%%d+%%.%%d+", + ["%%%.%d[fg]"] = "%%-?%%d+%%.?%%d*", + ["c"] = ".", + } + local curries = {} + + local function doNothing(item) + return item + end + local v = {} + + local function concat(a1, a2, a3, a4, a5) + local left, right + if not a2 then + return a1 + elseif not a3 then + left, right = a1, a2 + elseif not a4 then + return concat(concat(a1, a2), a3) + elseif not a5 then + return concat(concat(concat(a1, a2), a3), a4) + else + return concat(concat(concat(concat(a1, a2), a3), a4), a5) + end + if not string.find(left, "%%1%$") and not string.find(right, "%%1%$") then + return left .. right + elseif not string.find(right, "%%1%$") then + local i + for j = 9, 1, -1 do + if string.find(left, "%%" .. j .. "%$") then + i = j + break + end + end + while true do + local first + local firstPat + for x, y in pairs(sequences) do + local i = string.find(right, "%%" .. x) + if not first or (i and i < first) then + first = i + firstPat = x + end + end + if not first then + break + end + i = i + 1 + right = string.gsub(right, "%%(" .. firstPat .. ")", "%%" .. i .. "$%1") + end + return left .. right + elseif not string.find(left, "%%1%$") then + local i = 1 + while true do + local first + local firstPat + for x, y in pairs(sequences) do + local i = string.find(left, "%%" .. x) + if not first or (i and i < first) then + first = i + firstPat = x + end + end + if not first then + break + end + i = i + 1 + left = string.gsub(left, "%%(" .. firstPat .. ")", "%%" .. i .. "$%1") + end + return concat(left, right) + else + local i + for j = 9, 1, -1 do + if string.find(left, "%%" .. j .. "%$") then + i = j + break + end + end + local j + for k = 9, 1, -1 do + if string.find(right, "%%" .. k .. "%$") then + j = k + break + end + end + for k = j, 1, -1 do + right = string.gsub(right, "%%" .. k .. "%$", "%%" .. k + i .. "%$") + end + return left .. right + end + end + + local function Curry(a1, a2, a3, a4, a5) + local pattern = concat(a1, a2, a3, a4, a5) + if not string.find(pattern, "%%1%$") then + local unpattern = string.gsub(pattern, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") + local f = {} + local i = 0 + while true do + local first + local firstPat + for x, y in pairs(sequences) do + local i = string.find(unpattern, "%%%%" .. x) + if not first or (i and i < first) then + first = i + firstPat = x + end + end + if not first then + break + end + unpattern = string.gsub(unpattern, "%%%%" .. firstPat, "(" .. sequences[firstPat] .. ")", 1) + i = i + 1 + if firstPat == "c" or firstPat == "s" then + table.insert(f, doNothing) + else + table.insert(f, tonumber) + end + end + unpattern = "^" .. unpattern .. "$" + local _,alpha, bravo, charlie, delta, echo, foxtrot, golf, hotel, india + if i == 0 then + return + elseif i == 1 then + return function(text) + _,_,alpha = string.find(text, unpattern) + if alpha then + return f[1](alpha) + end + end + elseif i == 2 then + return function(text) + _,_,alpha, bravo = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo) + end + end + elseif i == 3 then + return function(text) + _,_,alpha, bravo, charlie = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie) + end + end + elseif i == 4 then + return function(text) + _,_,alpha, bravo, charlie, delta = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie), f[4](delta) + end + end + elseif i == 5 then + return function(text) + _,_,alpha, bravo, charlie, delta, echo = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie), f[4](delta), f[5](echo) + end + end + elseif i == 6 then + return function(text) + _,_,alpha, bravo, charlie, delta, echo, foxtrot = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie), f[4](delta), f[5](echo), f[6](foxtrot) + end + end + elseif i == 7 then + return function(text) + _,_,alpha, bravo, charlie, delta, echo, foxtrot, golf = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie), f[4](delta), f[5](echo), f[6](foxtrot), f[7](golf) + end + end + elseif i == 8 then + return function(text) + _,_,alpha, bravo, charlie, delta, echo, foxtrot, golf, hotel = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie), f[4](delta), f[5](echo), f[6](foxtrot), f[7](golf), f[8](hotel) + end + end + else + return function(text) + _,_,alpha, bravo, charlie, delta, echo, foxtrot, golf, hotel, india = string.find(text, unpattern) + if alpha then + return f[1](alpha), f[2](bravo), f[3](charlie), f[4](delta), f[5](echo), f[6](foxtrot), f[7](golf), f[8](hotel), f[9](india) + end + end + end + else + local o = {} + local f = {} + local unpattern = string.gsub(pattern, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") + local i = 1 + while true do + local pat + for x, y in pairs(sequences) do + if not pat and string.find(unpattern, "%%%%" .. i .. "%%%$" .. x) then + pat = x + break + end + end + if not pat then + break + end + unpattern = string.gsub(unpattern, "%%%%" .. i .. "%%%$" .. pat, "(" .. sequences[pat] .. ")", 1) + if pat == "c" or pat == "s" then + table.insert(f, doNothing) + else + table.insert(f, tonumber) + end + i = i + 1 + end + i = 1 + string.gsub(pattern, "%%(%d)%$", function(w) o[i] = tonumber(w); i = i + 1; end) + v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8],v[9] = nil + for x, y in pairs(f) do + v[x] = f[y] + end + for x, y in pairs(v) do + f[x] = v[x] + end + unpattern = "^" .. unpattern .. "$" + i = i - 1 + if i == 0 then + return function(text) + return + end + elseif i == 1 then + return function(text) + _,_,v[1] = string.find(text, unpattern) + if v[1] then + return f[1](v[1]) + end + end + elseif i == 2 then + return function(text) + _,_,v[1],v[2] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]) + end + end + elseif i == 3 then + return function(text) + _,_,v[1],v[2],v[3] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]) + end + end + elseif i == 4 then + return function(text) + _,_,v[1],v[2],v[3],v[4] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]), f[4](v[o[4]]) + end + end + elseif i == 5 then + return function(text) + _,_,v[1],v[2],v[3],v[4],v[5] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]), f[4](v[o[4]]), f[5](v[o[5]]) + end + end + elseif i == 6 then + return function(text) + _,_,v[1],v[2],v[3],v[4],v[5],v[6] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]), f[4](v[o[4]]), f[5](v[o[5]]), f[6](v[o[6]]) + end + end + elseif i == 7 then + return function(text) + _,_,v[1],v[2],v[3],v[4],v[5],v[6],v[7] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]), f[4](v[o[4]]), f[5](v[o[5]]), f[6](v[o[6]]), f[7](v[o[7]]) + end + end + elseif i == 8 then + return function(text) + _,_,v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]), f[4](v[o[4]]), f[5](v[o[5]]), f[6](v[o[6]]), f[7](v[o[7]]), f[8](v[o[8]]) + end + end + else + return function(text) + _,_,v[1],v[2],v[3],v[4],v[5],v[6],v[7],v[8],v[9] = string.find(text, unpattern) + if v[1] then + return f[1](v[o[1]]), f[2](v[o[2]]), f[3](v[o[3]]), f[4](v[o[4]]), f[5](v[o[5]]), f[6](v[o[6]]), f[7](v[o[7]]), f[8](v[o[8]]), f[9](v[o[9]]) + end + end + end + end + end + + function Deformat:Deformat(text, a1, a2, a3, a4, a5) + self:argCheck(text, 2, "string") + self:argCheck(a1, 3, "string") + local pattern = a1 + if a5 then + pattern = a1 .. a2 .. a3 .. a4 .. a5 + elseif a4 then + pattern = a1 .. a2 .. a3 .. a4 + elseif a3 then + pattern = a1 .. a2 .. a3 + elseif a2 then + pattern = a1 .. a2 + end + if curries[pattern] == nil then + curries[pattern] = Curry(a1, a2, a3, a4, a5) + end + return curries[pattern](text) + end +end + +local mt = getmetatable(Deformat) or {} +mt.__call = Deformat.Deformat +setmetatable(Deformat, mt) + +AceLibrary:Register(Deformat, MAJOR_VERSION, MINOR_VERSION) +Deformat = nil diff --git a/libs/ace/Gratuity-2.0/Gratuity-2.0.lua b/libs/ace/Gratuity-2.0/Gratuity-2.0.lua new file mode 100644 index 0000000..0cf3782 --- /dev/null +++ b/libs/ace/Gratuity-2.0/Gratuity-2.0.lua @@ -0,0 +1,220 @@ +--[[ +Name: Gratuity-2.0 +Revision: $Rev: 11053 $ +Author: Tekkub Stoutwrithe (tekkub@gmail.com) +Website: http://wiki.wowace.com/index.php/GratuityLib +Documentation: http://wiki.wowace.com/index.php/Gratuity-2.0_API_Documentation +SVN: svn://svn.wowace.com/root/trunk/GratuityLib/Gratuity-2.0 +Description: Tooltip parsing library +Dependencies: AceLibrary, (optional) Compost-2.0, (optional) Deformat-2.0 +]] + +local vmajor, vminor = "Gratuity-2.0", "$Revision: 11053 $" + +if not AceLibrary then error(vmajor .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(vmajor, vminor) then return end + +local lib = {} +local methods = { + "SetBagItem", "SetAction", "SetAuctionItem", "SetAuctionSellItem", "SetBuybackItem", + "SetCraftItem", "SetCraftSpell", "SetHyperlink", "SetInboxItem", "SetInventoryItem", + "SetLootItem", "SetLootRollItem", "SetMerchantItem", "SetPetAction", "SetPlayerBuff", + "SetQuestItem", "SetQuestLogItem", "SetQuestRewardSpell", "SetSendMailItem", "SetShapeshift", + "SetSpell", "SetTalent", "SetTrackingSpell", "SetTradePlayerItem", "SetTradeSkillItem", "SetTradeTargetItem", + "SetTrainerService", "SetUnit", "SetUnitBuff", "SetUnitDebuff", +} + + +-- Activate a new instance of this library +local function activate(self, oldLib, oldDeactivate) + if oldLib then self.vars = oldLib.vars + else + self.vars = {} + self:CreateTooltip() + end + + self:CreateSetMethods() + + if oldDeactivate then oldDeactivate(oldLib) end +end + + +function lib:InitCompost() + if not self.vars.compost and AceLibrary:HasInstance("Compost-2.0") then self.vars.compost = AceLibrary("Compost-2.0") end +end + + +function lib:CreateTooltip() + local tt = CreateFrame("GameTooltip") + + self.vars.tooltip = tt + tt:SetOwner(tt, "ANCHOR_NONE") +-- tooltip:SetParent() + + self.vars.Llines, self.vars.Rlines = {}, {} + for i=1,30 do + self.vars.Llines[i], self.vars.Rlines[i] = tt:CreateFontString(), tt:CreateFontString() + self.vars.Llines[i]:SetFontObject(GameFontNormal) + self.vars.Rlines[i]:SetFontObject(GameFontNormal) + tt:AddFontStrings(self.vars.Llines[i], self.vars.Rlines[i]) + end +end + + +-- Clears the tooltip completely, none of this "erase left, hide right" crap blizzard does +function lib:Erase() + self.vars.tooltip:ClearLines() -- Ensures tooltip's NumLines is reset + for i=1,30 do self.vars.Rlines[i]:SetText() end -- Clear text from right side (ClearLines only hides them) + if not self.vars.tooltip:IsOwned(self.vars.tooltip) then self.vars.tooltip:SetOwner(self.vars.tooltip, "ANCHOR_NONE") end + self:assert(self.vars.tooltip:IsOwned(self.vars.tooltip), "Gratuity's tooltip is not scanable") +end + + +-- Get the number of lines +-- Arg: endln - If passed and tooltip's NumLines is higher, endln is returned back +function lib:NumLines(endln) + local num = self.vars.tooltip:NumLines() + return endln and num > endln and endln or num or 0 +end + +local FindDefault = function(str, pattern) + return string.find(str, pattern); +end; + +local FindExact = function(str, pattern) + if (str == pattern) then + return string.find(str, pattern); + end; +end; + +-- If text is found on tooltip then results of string.find are returned +-- Args: +-- txt - The text string to find +-- startln - First tooltip line to check, default 1 +-- endln - Last line to test, default 30 +-- ignoreleft / ignoreright - Causes text on one side of the tooltip to be ignored +-- exact - the compare will be an exact match vs the default behaviour of +function lib:Find(txt, startln, endln, ignoreleft, ignoreright, exact) + local searchFunction = FindDefault; + if (exact == true) then + searchFunction = FindExact; + end; + self:argCheck(txt, 2, "string", "number") + local t1, t2 = type(startln or 1), type(self:NumLines(endln)) + if (t1 ~= "number" or t2 ~= "number") then print(t1, t2, (startln or 1),self:NumLines(endln)) end + for i=(startln or 1),self:NumLines(endln) do + if not ignoreleft then + local txtl = self.vars.Llines[i]:GetText() + if (txtl and searchFunction(txtl, txt)) then return string.find(txtl, txt) end + end + + if not ignoreright then + local txtr = self.vars.Rlines[i]:GetText() + if (txtr and searchFunction(txtr, txt)) then return string.find(txtr, txt) end + end + end +end + + +-- Calls Find many times. +-- Args are passed directly to Find, t1-t10 replace the txt arg +-- Returns Find results for the first match found, if any +function lib:MultiFind(startln, endln, ignoreleft, ignoreright, t1,t2,t3,t4,t5,t6,t7,t8,t9,t10) + self:argCheck(t1, 6, "string", "number") + if t1 and self:Find(t1, startln, endln, ignoreleft, ignoreright) then return self:Find(t1, startln, endln, ignoreleft, ignoreright) + elseif t2 then return self:MultiFind(startln, endln, ignoreleft, ignoreright, t2,t3,t4,t5,t6,t7,t8,t9,t10) end +end + + +local deformat +-- If text is found on tooltip then results of deformat:Deformat are returned +-- Args: +-- txt - The text string to deformat and serach for +-- startln - First tooltip line to check, default 1 +-- endln - Last line to test, default 30 +-- ignoreleft / ignoreright - Causes text on one side of the tooltip to be ignored +function lib:FindDeformat(txt, startln, endln, ignoreleft, ignoreright) + self:argCheck(txt, 2, "string", "number") + if not deformat then + self:assert(AceLibrary:HasInstance("Deformat-2.0"), "FindDeformat requires Deformat-2.0 to be available") + deformat = AceLibrary("Deformat-2.0") + end + + for i=(startln or 1),self:NumLines(endln) do + if not ignoreleft then + local txtl = self.vars.Llines[i]:GetText() + if (txtl and deformat(txtl, txt)) then return deformat(txtl, txt) end + end + + if not ignoreright then + local txtr = self.vars.Rlines[i]:GetText() + if (txtr and deformat(txtr, txt)) then return deformat(txtr, txt) end + end + end +end + + +-- Returns a table of strings pulled from the tooltip, or nil if no strings in tooltip +-- Args: +-- startln - First tooltip line to check, default 1 +-- endln - Last line to test, default 30 +-- ignoreleft / ignoreright - Causes text on one side of the tooltip to be ignored +function lib:GetText(startln, endln, ignoreleft, ignoreright) + self:InitCompost() + local retval + + for i=(startln or 1),(endln or 30) do + local txtl, txtr + if not ignoreleft then txtl = self.vars.Llines[i]:GetText() end + if not ignoreright then txtr = self.vars.Rlines[i]:GetText() end + if txtl or txtr then + if not retval then retval = self.vars.compost and self.vars.compost:Acquire() or {} end + local t = self.vars.compost and self.vars.compost:Acquire(txtl, txtr) or {txtl, txtr} + table.insert(retval, t) + end + end + + return retval +end + + +-- Returns the text from a specific line (both left and right unless second arg is true) +-- Args: +-- line - the line number you wish to retrieve +-- getright - if passed the right line will be returned, if not the left will be returned +function lib:GetLine(line, getright) + self:argCheck(line, 2, "number") + if self.vars.tooltip:NumLines() < line then return end + if getright then return self.vars.Rlines[line] and self.vars.Rlines[line]:GetText() + elseif self.vars.Llines[line] then + return self.vars.Llines[line]:GetText(), self.vars.Rlines[line]:GetText() + end +end + + +----------------------------------- +-- Set tooltip methods -- +----------------------------------- + +-- These methods are designed to immitate the GameTooltip API +local testmethods = { + SetAction = function(id) return HasAction(id) end, +} +local gettrue = function() return true end +function lib:CreateSetMethods() + for _,m in pairs(methods) do + local meth = m + local func = testmethods[meth] or gettrue + self[meth] = function(self,a1,a2,a3,a4) + self:Erase() + if not func(a1,a2,a3,a4) then return end + return self:pcall(self.vars.tooltip[meth], self.vars.tooltip,a1,a2,a3,a4) end + end +end + + +-------------------------------- +-- Load this bitch! -- +-------------------------------- +AceLibrary:Register(lib, vmajor, vminor, activate) +lib = nil diff --git a/libs/ace/HealComm-1.0/HealComm-1.0.lua b/libs/ace/HealComm-1.0/HealComm-1.0.lua new file mode 100644 index 0000000..9326036 --- /dev/null +++ b/libs/ace/HealComm-1.0/HealComm-1.0.lua @@ -0,0 +1,1849 @@ +--[[ +Name: HealComm-1.0 +Revision: $Rev: 11732 $ +Author(s): aviana +Website: https://github.com/Aviana +Description: A library to provide communication of heals and resurrections. +Dependencies: AceLibrary, AceEvent-2.0, RosterLib-2.0, ItemBonusLib-1.0 +]] + +-- This HealComm was modified by MarcelineVQ and sourced from https://github.com/MarcelineVQ/LunaUnitFrames/ + +local MAJOR_VERSION = "HealComm-1.0" +local MINOR_VERSION = "$Revision: 14732 $" + +if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end +if not AceLibrary:HasInstance("RosterLib-2.0") then error(MAJOR_VERSION .. " requires RosterLib-2.0") end +if not AceLibrary:HasInstance("AceEvent-2.0") then error(MAJOR_VERSION .. " requires AceEvent-2.0") end +if not AceLibrary:HasInstance("AceHook-2.1") then error(MAJOR_VERSION .. " requires AceHook-2.1") end +if not AceLibrary:HasInstance("ItemBonusLib-1.0") then error(MAJOR_VERSION .. " requires ItemBonusLib-1.0") end + +local roster = AceLibrary("RosterLib-2.0") +local itemBonus = AceLibrary("ItemBonusLib-1.0") +local L = AceLibrary("AceLocale-2.2"):new("HealComm-1.0") +local HealComm = {} +local has_superwow = SetAutoloot and true or false +local player_guid +------------------------------------------------ +-- Locales +------------------------------------------------ + +L:RegisterTranslations("enUS", function() return { + ["Libram of Divinity"] = true, + ["Libram of Light"] = true, + ["Set: Increases the duration of your Rejuvenation spell by 3 sec."] = true, + ["Set: Increases the duration of your Renew spell by 3 sec."] = true, + ["Totem of Life"] = true, + ["Totem of Sustaining"] = true, + ["^Corpse of (.+)$"] = true, + ["Holy Light"] = true, + ["Flash of Light"] = true, + ["Lesser Heal"] = true, + ["Heal"] = true, + ["Greater Heal"] = true, + ["Flash Heal"] = true, + ["Prayer of Healing"] = true, + ["Lesser Healing Wave"] = true, + ["Healing Wave"] = true, + ["Chain Heal"] = true, + ["Healing Touch"] = true, + ["Regrowth"] = true, + ["Resurrection"] = true; + ["Rebirth"] = true; + ["Redemption"] = true; + ["Ancestral Spirit"] = true; + ["Renew"] = true; + ["Rejuvenation"] = true; + ["Power Infusion"] = true, + ["Divine Favor"] = true, + ["Nature Aligned"] = true, + ["Crusader's Wrath"] = true, + ["The Furious Storm"] = true, + ["Holy Power"] = true, + ["Prayer Beads Blessing"] = true, + ["Chromatic Infusion"] = true, + ["Ascendance"] = true, + ["Ephemeral Power"] = true, + ["Unstable Power"] = true, + ["Healing of the Ages"] = true, + ["Essence of Sapphiron"] = true, + ["The Eye of the Dead"] = true, + ["Mortal Strike"] = true, + ["Wound Poison"] = true, + ["Curse of the Deadwood"] = true, + ["Veil of Shadow"] = true, + ["Gehennas' Curse"] = true, + ["Mortal Wound"] = true, + ["Necrotic Poison"] = true, + ["Blood Fury"] = true, + ["Necrotic Aura"] = true, + ["Blessing of Light"] = true, + ["Healing Way"] = true, + ["Warsong Gulch"] = true, + ["Arathi Basin"] = true, + ["Alterac Valley"] = true, +} end) +L:RegisterTranslations("ruRU", function() return { + ["Libram of Divinity"] = "Манускрипт божественности", + ["Libram of Light"] = "Манускрипт света", + ["Set: Increases the duration of your Rejuvenation spell by 3 sec."] = "Комплект: Увеличение длительности заклинания \"Омоложение\" на 3 сек.", -- T2 + ["Set: Increases the duration of your Renew spell by 3 sec."] = "Комплект: Увеличение длительности заклинания \"Обновление\" на 3 сек.", -- T2.5 + ["Totem of Life"] = "Тотем жизни", + ["Totem of Sustaining"] = "Тотем воодушевления", + ["^Corpse of (.+)$"] = "^Труп (.+)$", + ["Holy Light"] = "Свет небес", + ["Flash of Light"] = "Улучшенная вспышка света", + ["Lesser Heal"] = "Малое исцеление", + ["Heal"] = "Исцеление", + ["Greater Heal"] = "Великое исцеление", + ["Flash Heal"] = "Быстрое исцеление", + ["Prayer of Healing"] = "Молитва исцеления", + ["Lesser Healing Wave"] = "Малая волна исцеления", + ["Healing Wave"] = "Волна исцеления", + ["Chain Heal"] = "Цепное исцеление", + ["Healing Touch"] = "Целительное прикосновение", + ["Regrowth"] = "Восстановление", + ["Resurrection"] = "Воскрешение", + ["Rebirth"] = "Возрождение", + ["Redemption"] = "Искупление", + ["Ancestral Spirit"] = "Дух предков", + ["Renew"] = "Обновление", + ["Rejuvenation"] = "Омоложение", + ["Power Infusion"] = "Придание сил", + ["Divine Favor"] = "Божественное одобрение", + ["Nature Aligned"] = "Упорядочение Природы", + ["Crusader's Wrath"] = "Гнев рыцаря Света", + ["The Furious Storm"] = "Яростный шторм", + ["Holy Power"] = "Священная сила", + ["Prayer Beads Blessing"] = "Благословение четок", + ["Chromatic Infusion"] = "Цветной настой", + ["Ascendance"] = "Господство", + ["Ephemeral Power"] = "Эфемерная Власть", + ["Unstable Power"] = "Изменчивая сила", + ["Healing of the Ages"] = "Исцеление Эпох", + ["Essence of Sapphiron"] = "Сущность Сапфирона", + ["The Eye of the Dead"] = "Глаз Мертвого", + ["Mortal Strike"] = "Смертельный удар", + ["Wound Poison"] = "Нейтрализующий яд", + ["Curse of the Deadwood"] = "Проклятие Мертвого Леса", + ["Veil of Shadow"] = "Пелена Тени", + ["Gehennas' Curse"] = "Проклятие Гееннаса", + ["Mortal Wound"] = "Смертоносная рана", + ["Necrotic Poison"] = "Некротический яд", + ["Blood Fury"] = "Кровавое неистовство", + ["Necrotic Aura"] = "Мертвенная аура", + ["Blessing of Light"] = "Благословение Света", + ["Healing Way"] = "Путь исцеления", + ["Warsong Gulch"] = "Ущелье Песни Войны", + ["Arathi Basin"] = "Низина Арати", + ["Alterac Valley"] = "Альтеракская долина", +} end) +L:RegisterTranslations("deDE", function() return { + ["Libram of Divinity"] = "Buchband der Offenbarung", + ["Libram of Light"] = "Buchband des Lichts", + ["Set: Increases the duration of your Rejuvenation spell by 3 sec."] = "Set: Erh\195\182ht die Dauer Eures Zaubers \'Verj\195\188ngung\' um 3 Sek.", + ["Set: Increases the duration of your Renew spell by 3 sec."] = "Set: Erh\195\182ht die Dauer Eures Zaubers 'Erneuerung' um 3 Sek.", + ["Totem of Life"] = "Totem des Lebens", + ["Totem of Sustaining"] = "Totem der Erhaltung", + ["^Corpse of (.+)$"] = "^Leichnam von (.+)$", + ["Holy Light"] = "Heiliges Licht", + ["Flash of Light"] = "Lichtblitz", + ["Lesser Heal"] = "Geringes Heilen", + ["Heal"] = "Heilen", + ["Greater Heal"] = "Große Heilung", + ["Flash Heal"] = "Blitzheilung", + ["Prayer of Healing"] = "Gebet der Heilung", + ["Lesser Healing Wave"] = "Geringe Welle der Heilung", + ["Healing Wave"] = "Welle der Heilung", + ["Chain Heal"] = "Kettenheilung", + ["Healing Touch"] = "Heilende Ber\195\188hrung", + ["Regrowth"] = "Nachwachsen", + ["Resurrection"] = "Auferstehung", + ["Rebirth"] = "Wiedergeburt", + ["Redemption"] = "Erl\195\182sung", + ["Ancestral Spirit"] = "Geist der Ahnen", + ["Renew"] = "Erneuerung", + ["Rejuvenation"] = "Verj\195\188ngung", + ["Power Infusion"] = "Seele der Macht", + ["Divine Favor"] = "G\195\182ttliche Gunst", + ["Nature Aligned"] = "Naturverbundenheit", + ["Crusader's Wrath"] = "Zorn des Kreuzfahrers", + ["The Furious Storm"] = "Der wilde Sturm", + ["Holy Power"] = "Heilige Kraft", + ["Prayer Beads Blessing"] = "Segen der Gebetsperlen", + ["Chromatic Infusion"] = "Erf\195\188llt mit chromatischer Macht", + ["Ascendance"] = "Überlegenheit", + ["Ephemeral Power"] = "Ephemere Macht", + ["Unstable Power"] = "Instabile Macht", + ["Healing of the Ages"] = "Heilung der Urzeiten", + ["Essence of Sapphiron"] = "Essenz Saphirons", + ["The Eye of the Dead"] = "Das Auge des Todes", + ["Mortal Strike"] = "T\195\182dlicher Stoß", + ["Wound Poison"] = "Wundgift", + ["Curse of the Deadwood"] = "Fluch der Totenwaldfelle", + ["Veil of Shadow"] = "Schattenschleier", + ["Gehennas' Curse"] = "Gehennas' Fluch", + ["Mortal Wound"] = "Trauma", + ["Necrotic Poison"] = "Nekrotisches Gift", + ["Blood Fury"] = "Kochendes Blut", + ["Necrotic Aura"] = "Nekrotische Aura", + ["Blessing of Light"] = "Segen des Lichts", + ["Healing Way"] = "Pfad der Heilung", + ["Warsong Gulch"] = "Warsongschlucht", + ["Arathi Basin"] = "Arathibecken", + ["Alterac Valley"] = "Alteractal", +} end) +L:RegisterTranslations("frFR", function() return { + ["Libram of Divinity"] = "Libram de divinit\195\169", + ["Libram of Light"] = "Libram de lumi\195\168re", + ["Set: Increases the duration of your Rejuvenation spell by 3 sec."] = "Set: Augmente la dur\195\169e de votre sort R\195\169cup\195\169ration de 3 s.", + ["Set: Increases the duration of your Renew spell by 3 sec."] = "Set: Augmente la dur\195\169e de votre sort R\195\169novation de 3 s.", + ["Totem of Life"] = "Totem de vie", + ["Totem of Sustaining"] = "Totem de soutien", + ["^Corpse of (.+)$"] = "^Cadavre |2 (.+)$", + ["Holy Light"] = "Lumi\195\168re sacr\195\169e", + ["Flash of Light"] = "Eclair lumineux", + ["Lesser Heal"] = "Soins inf\195\169rieurs", + ["Heal"] = "Soins", + ["Greater Heal"] = "Soins sup\195\169rieurs", + ["Flash Heal"] = "Soins rapides", + ["Prayer of Healing"] = "Pri\195\168re de soins", + ["Lesser Healing Wave"] = "Vague de soins inf\195\169rieurs", + ["Healing Wave"] = "Vague de soins", + ["Chain Heal"] = "Salve de gu\195\169rison", + ["Healing Touch"] = "Toucher gu\195\169risseur", + ["Regrowth"] = "R\195\169tablissement", + ["Resurrection"] = "R\195\169surrection", + ["Rebirth"] = "Renaissance", + ["Redemption"] = "R\195\169demption", + ["Ancestral Spirit"] = "Esprit ancestral", + ["Renew"] = "R\195\169novation", + ["Rejuvenation"] = "R\195\169cup\195\169ration", + ["Power Infusion"] = "Infusion de puissance", + ["Divine Favor"] = "Faveur divine", + ["Nature Aligned"] = "Alignement sur la nature", + ["Crusader's Wrath"] = "Col\195\168re du crois\195\169", + ["The Furious Storm"] = "La temp\195\170te furieuse", + ["Holy Power"] = "Puissance sacr\195\169e", + ["Prayer Beads Blessing"] = "B\195\169n\195\169diction du chapelet", + ["Chromatic Infusion"] = "Infusion chromatique", + ["Ascendance"] = "Ascendance", + ["Ephemeral Power"] = "Puissance \195\169ph\195\169m\195\168re", + ["Unstable Power"] = "Puissance instable", + ["Healing of the Ages"] = "Soins des \195\162ges", + ["Essence of Sapphiron"] = "Essence de Saphiron", + ["The Eye of the Dead"] = "L'Oeil du mort", + ["Mortal Strike"] = "Frappe mortelle", + ["Wound Poison"] = "Poison douloureux", + ["Curse of the Deadwood"] = "Mal\195\169diction des Mort-bois", + ["Veil of Shadow"] = "Voile de l'ombre", + ["Gehennas' Curse"] = "Mal\195\169diction de Gehennas", + ["Mortal Wound"] = "Blessures mortelles", + ["Necrotic Poison"] = "Poison n\195\169crotique", + ["Blood Fury"] = "Fureur sanguinaire", + ["Necrotic Aura"] = "Aura n\195\169crotique", + ["Blessing of Light"] = "B\195\169n\195\169diction de lumi\195\168re", + ["Healing Way"] = "Flots de soins", + ["Warsong Gulch"] = "Goulet des Warsong", + ["Arathi Basin"] = "Bassin d'Arathi", + ["Alterac Valley"] = "Vall\195\169e d'Alterac", +} end) +L:RegisterTranslations("zhCN", function() return { + ["Libram of Divinity"] = "神性圣契", + ["Libram of Light"] = "光明圣契", + ["Set: Increases the duration of your Rejuvenation spell by 3 sec."] = "套装:使你的回春术的持续时间延长3秒。", -- T2 + ["Set: Increases the duration of your Renew spell by 3 sec."] = "套装:使你的恢复术的持续时间延长3秒。", -- T2.5 + ["Totem of Life"] = "生命图腾", + ["Totem of Sustaining"] = "持久图腾", + ["^Corpse of (.+)$"] = "(.+)的尸体", + ["Holy Light"] = "圣光术", + ["Flash of Light"] = "圣光闪现", + ["Lesser Heal"] = "次级治疗术", + ["Heal"] = "治疗术", + ["Greater Heal"] = "强效治疗术", + ["Flash Heal"] = "快速治疗", + ["Prayer of Healing"] = "治疗祷言", + ["Lesser Healing Wave"] = "次级治疗波", + ["Healing Wave"] = "治疗波", + ["Chain Heal"] = "治疗链", + ["Healing Touch"] = "治疗之触", + ["Regrowth"] = "愈合", + ["Resurrection"] = "复活", + ["Rebirth"] = "复生", + ["Redemption"] = "救赎", + ["Ancestral Spirit"] = "先祖之魂", + ["Renew"] = "恢复", + ["Rejuvenation"] = "回春术", + ["Power Infusion"] = "能量灌注", + ["Divine Favor"] = "神恩术", + ["Nature Aligned"] = "自然之盟", + ["Crusader's Wrath"] = "十字军之怒", + ["The Furious Storm"] = "狂野风暴", + ["Holy Power"] = "神圣强化", + ["Prayer Beads Blessing"] = "祈祷之珠", + ["Chromatic Infusion"] = "多彩能量", + ["Ascendance"] = "优越", + ["Ephemeral Power"] = "短暂强力", + ["Unstable Power"] = "能量无常", + ["Healing of the Ages"] = "远古治疗", + ["Essence of Sapphiron"] = "萨菲隆的精华", + ["The Eye of the Dead"] = "亡者之眼", + ["Mortal Strike"] = "致死打击", + ["Wound Poison"] = "致伤毒药", + ["Curse of the Deadwood"] = "死木诅咒", + ["Veil of Shadow"] = "暗影之雾", -- 存在多个不同名技能,暗影之雾/暗影迷雾/幽影之雾 + ["Gehennas' Curse"] = "基赫纳斯的诅咒", + ["Mortal Wound"] = "重伤", + ["Necrotic Poison"] = "死灵之毒", + ["Blood Fury"] = "血性狂暴", + ["Necrotic Aura"] = "死灵光环", + ["Blessing of Light"] = "光明祝福", + ["Healing Way"] = "治疗之道", + ["Warsong Gulch"] = "战歌峡谷", + ["Arathi Basin"] = "阿拉希盆地", + ["Alterac Valley"] = "奥特兰克山谷", +} end) +L:RegisterTranslations("koKR", function() return { + ["Libram of Divinity"] = "신앙의 성서", + ["Libram of Light"] = "빛의 성서", + ["Set: Increases the duration of your Rejuvenation spell by 3 sec."] = true, --needs translation + ["Set: Increases the duration of your Renew spell by 3 sec."] = true, --needs translation + ["Totem of Life"] = "생명의 토템", + ["Totem of Sustaining"] = "지탱의 토템", + ["^Corpse of (.+)$"] = true, --needs translation + ["Holy Light"] = "성스러운 빛", + ["Flash of Light"] = "빛의 섬광", + ["Lesser Heal"] = "하급 치유", + ["Heal"] = "치유", + ["Greater Heal"] = "상급 치유", + ["Flash Heal"] = "순간 치유", + ["Prayer of Healing"] = "치유의 기원", + ["Lesser Healing Wave"] = "하급 치유의 물결", + ["Healing Wave"] = "치유의 물결", + ["Chain Heal"] = "연쇄 치유", + ["Healing Touch"] = "치유의 손길", + ["Regrowth"] = "재생", + ["Resurrection"] = "부활", + ["Rebirth"] = "환생", + ["Redemption"] = "구원", + ["Ancestral Spirit"] = "고대의 영혼", + ["Renew"] = "소생", + ["Rejuvenation"] = "회복", + ["Power Infusion"] = "마력 주입", + ["Divine Favor"] = "신의 은총", + ["Nature Aligned"] = "자연 동화", + ["Crusader's Wrath"] = "성전사의 격노", + ["The Furious Storm"] = "휘몰아치는 폭풍", + ["Holy Power"] = "신성 마법 강화", + ["Prayer Beads Blessing"] = "기원의 묵주의 축복", + ["Chromatic Infusion"] = "오색 용력", + ["Ascendance"] = "승리의 기세", + ["Ephemeral Power"] = "마력의 힘", + ["Unstable Power"] = "불안정한 마력", + ["Healing of the Ages"] = "세월의 치유", + ["Essence of Sapphiron"] = "사피론의 정수", + ["The Eye of the Dead"] = "사자의 눈", + ["Mortal Strike"] = "죽음의 일격", + ["Wound Poison"] = "상처 감염 독", + ["Curse of the Deadwood"] = "마른가지의 저주", + ["Veil of Shadow"] = "암흑의 장막", + ["Gehennas' Curse"] = "게헨나스의 저주", + ["Mortal Wound"] = "죽음의 상처", + ["Necrotic Poison"] = "부패의 독", + ["Blood Fury"] = "피의 격노", + ["Necrotic Aura"] = "괴저 오라", + ["Blessing of Light"] = "빛의 축복", + ["Healing Way"] = "치유의 길", + ["Warsong Gulch"] = "전쟁노래 협곡", + ["Arathi Basin"] = "아라시 분지", + ["Alterac Valley"] = "알터랙 계곡", +} end) +------------------------------------------------ +-- activate, enable, disable +------------------------------------------------ + +local function activate(self, oldLib, oldDeactivate) + HealComm = self + if oldLib then + self.Heals = oldLib.Heals + self.GrpHeals = oldLib.GrpHeals + self.Lookup = oldLib.Lookup + self.pendingResurrections = oldLib.pendingResurrections + self.Hots = oldLib.Hots + self.SpellCastInfo = oldLib.SpellCastInfo + oldLib:UnregisterAllEvents() + oldLib:CancelAllScheduledEvents() + end + if not self.Heals then + self.Heals = {} + end + if not self.GrpHeals then + self.GrpHeals = {} + end + if not self.Lookup then + self.Lookup = {} + end + if not self.pendingResurrections then + self.pendingResurrections = {} + end + if not self.Hots then + self.Hots = {} + end + if not self.SpellCastInfo then + self.SpellCastInfo = {} + end + if oldDeactivate then oldDeactivate(oldLib) end +end + +local function external(self, major, instance) + if major == "AceEvent-2.0" then + local AceEvent = instance + AceEvent:embed(self) + if has_superwow then + self:RegisterEvent("UNIT_CASTEVENT") + else + self:RegisterEvent("SPELLCAST_START") + self:RegisterEvent("SPELLCAST_STOP") + end + self:RegisterEvent("SPELLCAST_INTERRUPTED") + self:RegisterEvent("SPELLCAST_FAILED") + self:RegisterEvent("SPELLCAST_DELAYED") + self:RegisterEvent("CHAT_MSG_ADDON") + self:RegisterEvent("UNIT_AURA") + self:RegisterEvent("UNIT_HEALTH") + self:RegisterEvent("PLAYER_LOGIN") + self:TriggerEvent("HealComm_Enabled") + end + if major == "AceHook-2.1" then + local AceHook = instance + AceHook:embed(self) + end +end + +function HealComm:PLAYER_LOGIN() + self:HookScript(WorldFrame, "OnMouseDown", "OnMouseDown") + if not has_superwow then + self:Hook("CastSpell") + self:Hook("CastSpellByName") + self:Hook("UseAction") + end + self:Hook("SpellTargetUnit") + self:Hook("SpellStopTargeting") + self:Hook("TargetUnit") + + if has_superwow then + _,player_guid = UnitExists("player") + end +end + +function HealComm:Enable() +-- not used anymore, but as addons still might be calling this method, we're keeping it. +end + + +function HealComm:Disable() +-- not used anymore, but as addons still might be calling this method, we're keeping it. +end + +------------------------------------------------ +-- Addon Code +------------------------------------------------ + +function strmatch(str, pat, init) + local a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a13,a14,a15,a16,a17,a18,a19,a20 = string.find(str, pat, init) + return a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a13,a14,a15,a16,a17,a18,a19,a20 +end + +HealComm.Spells = { + [L["Holy Light"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (44*hlMod+(((2.5/3.5) * SpellPower)*0.1)) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (88*hlMod+(((2.5/3.5) * SpellPower)*0.224)) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (174*hlMod+(((2.5/3.5) * SpellPower)*0.476)) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (334*hlMod+((2.5/3.5) * SpellPower)) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (522*hlMod+((2.5/3.5) * SpellPower)) + end; + [6] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (740*hlMod+((2.5/3.5) * SpellPower)) + end; + [7] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (1000*hlMod+((2.5/3.5) * SpellPower)) + end; + [8] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (1318*hlMod+((2.5/3.5) * SpellPower)) + end; + [9] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (1681*hlMod+((2.5/3.5) * SpellPower)) + end; + }; + [L["Flash of Light"]] = { + [1] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (68*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + [2] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (104*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + [3] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (155*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + [4] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (210*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + [5] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (284*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + [6] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (364*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + [7] = function (SpellPower) + local lp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Libram of Divinity"] then + lp = 53 + elseif name == L["Libram of Light"] then + lp = 83 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(1,5) + local hlMod = 4*talentRank/100 + 1 + return (481*hlMod+lp+((1.5/3.5) * SpellPower)) + end; + }; + [L["Healing Wave"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (40*pMod+(((1.5/3.5) * SpellPower)*0.22)) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (72*pMod+(((2/3.5) * SpellPower)*0.38)) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (143*pMod+(((2.5/3.5) * SpellPower)*0.446)) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (293*pMod+(((3/3.5) * SpellPower)*0.7)) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (409*pMod+((3/3.5) * SpellPower)) + end; + [6] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (580*pMod+((3/3.5) * SpellPower)) + end; + [7] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (798*pMod+((3/3.5) * SpellPower)) + end; + [8] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (1093*pMod+((3/3.5) * SpellPower)) + end; + [9] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (1465*pMod+((3/3.5) * SpellPower)) + end; + [10] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (1736*pMod+((3/3.5) * SpellPower)) + end; + }; + [L["Lesser Healing Wave"]] = { + [1] = function (SpellPower) + local tp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Totem of Sustaining"] then + tp = 53 + elseif name == L["Totem of Life"] then + tp = 80 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (175*pMod+tp+((1.5/3.5) * SpellPower)) + end; + [2] = function (SpellPower) + local tp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Totem of Sustaining"] then + tp = 53 + elseif name == L["Totem of Life"] then + tp = 80 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (265*pMod+tp+((1.5/3.5) * SpellPower)) + end; + [3] = function (SpellPower) + local tp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Totem of Sustaining"] then + tp = 53 + elseif name == L["Totem of Life"] then + tp = 80 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (360*pMod+tp+((1.5/3.5) * SpellPower)) + end; + [4] = function (SpellPower) + local tp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Totem of Sustaining"] then + tp = 53 + elseif name == L["Totem of Life"] then + tp = 80 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (487*pMod+tp+((1.5/3.5) * SpellPower)) + end; + [5] = function (SpellPower) + local tp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Totem of Sustaining"] then + tp = 53 + elseif name == L["Totem of Life"] then + tp = 80 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (669*pMod+tp+((1.5/3.5) * SpellPower)) + end; + [6] = function (SpellPower) + local tp = 0 + if GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")) then + local _,_,itemstring = string.find(GetInventoryItemLink("player",GetInventorySlotInfo("RangedSlot")), "|H(.+)|h") + local name = GetItemInfo(itemstring) + if name == L["Totem of Sustaining"] then + tp = 53 + elseif name == L["Totem of Life"] then + tp = 80 + end + end + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (881*pMod+tp+((1.5/3.5) * SpellPower)) + end; + }; + [L["Chain Heal"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (344*pMod+((2.5/3.5) * SpellPower)) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (435*pMod+((2.5/3.5) * SpellPower)) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,14) + local pMod = 2*talentRank/100 + 1 + return (591*pMod+((2.5/3.5) * SpellPower)) + end; + }; + [L["Lesser Heal"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (52*shMod+((1.5/3.5) * (SpellPower+sgMod))*0.19) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (79*shMod+((2/3.5) * (SpellPower+sgMod))*0.34) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (147*shMod+((2.5/3.5) * (SpellPower+sgMod))*0.6) + end; + }; + [L["Heal"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (319*shMod+((3/3.5) * (SpellPower+sgMod))*0.586) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (471*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (610*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (759*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + }; + [L["Flash Heal"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (216*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (287*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (361*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (440*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (568*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + [6] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (705*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + [7] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (886*shMod+((1.5/3.5) * (SpellPower+sgMod))) + end; + }; + [L["Greater Heal"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (957*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (1220*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (1524*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (1903*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (2081*shMod+((3/3.5) * (SpellPower+sgMod))) + end; + }; + [L["Prayer of Healing"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (311*shMod+((3/3.5/3) * (SpellPower+sgMod))) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (460*shMod+((3/3.5/3) * (SpellPower+sgMod))) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (676*shMod+((3/3.5/3) * (SpellPower+sgMod))) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (965*shMod+((3/3.5/3) * (SpellPower+sgMod))) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(2,14) + local _,Spirit,_,_ = UnitStat("player",5) + local sgMod = Spirit * 5*talentRank/100 + local _,_,_,_,talentRank2,_ = GetTalentInfo(2,15) + local shMod = 2*talentRank2/100 + 1 + return (1070*shMod+((3/3.5/3) * (SpellPower+sgMod))) + end; + }; + [L["Healing Touch"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return (43*gnMod+((1.5/3.5) * SpellPower * (1-((20-4)*0.0375)))) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return (101*gnMod+((2/3.5) * SpellPower * (1-((20-13)*0.0375)))) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return (220*gnMod+((2.5/3.5) * SpellPower)) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return (435*gnMod+((3/3.5) * SpellPower)) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((634*gnMod)+SpellPower) + end; + [6] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((819*gnMod)+SpellPower) + end; + [7] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((1029*gnMod)+SpellPower) + end; + [8] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((1314*gnMod)+SpellPower) + end; + [9] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((1657*gnMod)+SpellPower) + end; + [10] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((2061*gnMod)+SpellPower) + end; + [11] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((2473*gnMod)+SpellPower) + end; + }; + [L["Regrowth"]] = { + [1] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((91*gnMod)+(((2/3.5)*SpellPower)*0.5*0.38)) + end; + [2] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((177*gnMod)+(((2/3.5)*SpellPower)*0.5*0.513)) + end; + [3] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((258*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + [4] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((340*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + [5] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((432*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + [6] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((544*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + [7] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((686*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + [8] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((858*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + [9] = function (SpellPower) + local _,_,_,_,talentRank,_ = GetTalentInfo(3,12) + local gnMod = 2*talentRank/100 + 1 + return ((1062*gnMod)+(((2/3.5)*SpellPower)*0.5)) + end; + }; +} + +local Resurrections = { + [L["Resurrection"]] = true; + [L["Rebirth"]] = true; + [L["Redemption"]] = true; + [L["Ancestral Spirit"]] = true; +} + +local Hots = { + [L["Renew"]] = true; + [L["Rejuvenation"]] = true; +} + +-- Puppeteer: the result table is reused between calls (its only caller reads it immediately) so that each +-- received addon message doesn't create a new table +local strsplitResult = {} +local function strsplit(pString, pPattern) + local Table = strsplitResult + for i = table.getn(Table), 1, -1 do + Table[i] = nil + end + table.setn(Table, 0) + local fpat = "(.-)" .. pPattern + local last_end = 1 + local s, e, cap = strfind(pString, fpat, 1) + while s do + if s ~= 1 or cap ~= "" then + table.insert(Table,cap) + end + last_end = e+1 + s, e, cap = strfind(pString, fpat, last_end) + end + if last_end <= strlen(pString) then + cap = strfind(pString, last_end) + table.insert(Table, cap) + end + return Table +end + +local healcommTip = CreateFrame("GameTooltip", "healcommTip", nil, "GameTooltipTemplate") +healcommTip:SetOwner(WorldFrame, "ANCHOR_NONE") + +HealComm.Buffs = { + [L["Power Infusion"]] = {amount = 0, mod = 0.2, icon = "Interface\\Icons\\Spell_Holy_PowerInfusion"}; + [L["Divine Favor"]] = {amount = 0, mod = 0.5, icon = "Interface\\Icons\\Spell_Holy_Heal"}; + [L["Nature Aligned"]] = {amount = 0, mod = 0.2, icon = "Interface\\Icons\\Spell_Nature_SpiritArmor"}; + [L["Crusader's Wrath"]] = {amount = 95, mod = 0, icon = "Interface\\Icons\\Spell_Nature_GroundingTotem"}; + [L["The Furious Storm"]] = {amount = 95, mod = 0, icon = "Interface\\Icons\\Spell_Nature_CallStorm"}; + [L["Holy Power"]] = {amount = 80, mod = 0, icon = "Interface\\Icons\\Spell_Holy_HolyNova"}; + [L["Prayer Beads Blessing"]] = {amount = 190, mod = 0, icon = "Interface\\Icons\\Inv_Jewelry_Necklace_11"}; + [L["Chromatic Infusion"]] = {amount = 190, mod = 0, icon = "Interface\\Icons\\Spell_Holy_MindVision"}; + [L["Ascendance"]] = {amount = 75, mod = 0, icon = "Interface\\Icons\\Spell_Lightning_LightningBolt01"}; + [L["Ephemeral Power"]] = {amount = 175, mod = 0, icon = "Interface\\Icons\\Spell_Holy_MindVision"}; + [L["Unstable Power"]] = {amount = 34, mod = 0, icon = "Interface\\Icons\\Spell_Lightning_LightningBolt01"}; + [L["Healing of the Ages"]] = {amount = 350, mod = 0, icon = "Interface\\Icons\\Spell_Nature_HealingWaveGreater"}; + [L["Essence of Sapphiron"]] = {amount = 130, mod = 0, icon = "Interface\\Icons\\Inv_Trinket_Naxxramas06"}; + [L["The Eye of the Dead"]] = {amount = 450, mod = 0, icon = "Interface\\Icons\\Inv_Trinket_Naxxramas01"} +} + +HealComm.Debuffs = { + [L["Mortal Strike"]] = {amount = 0, mod = 0.5, icon = "Interface\\Icons\\Ability_Warrior_SavageBlow"}; + [L["Wound Poison"]] = {amount = -135, mod = 0, icon = "Interface\\Icons\\Inv_Misc_Herb_16"}; + [L["Curse of the Deadwood"]] = {amount = 0, mod = 0.5, icon = "Interface\\Icons\\Spell_Shadow_GatherShadows"}; + [L["Veil of Shadow"]] = {amount = 0, mod = 0.75, icon = "Interface\\Icons\\Spell_Shadow_GatherShadows"}; + [L["Gehennas' Curse"]] = {amount = 0, mod = 0.75, icon = "Interface\\Icons\\Spell_Shadow_GatherShadows"}; + [L["Mortal Wound"]] = {amount = 0, mod = 0.1, icon = "Interface\\Icons\\Ability_CriticalStrike"}; + [L["Necrotic Poison"]] = {amount = 0, mod = 0.9, icon = "Interface\\Icons\\Ability_Creature_Poison_03"}; + [L["Blood Fury"]] = {amount = 0, mod = 0.5, icon = "Interface\\Icons\\Ability_Rogue_FeignDeath"}; + [L["Necrotic Aura"]] = {amount = 0, mod = 1, icon = "Interface\\Icons\\Ability_Creature_Disease_05"} +} + +local function getSetBonus() + healcommTip:SetInventoryItem("player", 1) + local text = "healcommTipTextLeft"..(healcommTip:NumLines() or 1) + local text = getglobal(text) + if text then + text = text:GetText() + else + return nil + end + if text == L["Set: Increases the duration of your Rejuvenation spell by 3 sec."] or text == L["Set: Increases the duration of your Renew spell by 3 sec."] then + return true + else + return nil + end +end + +function HealComm:GetBuffSpellPower() + local Spellpower = 0 + local healmod = 1 + for i=1, 32 do + local buffTexture, buffApplications = UnitBuff("player", i) + if not buffTexture then + return Spellpower, healmod + end + healcommTip:SetUnitBuff("player", i) + local buffName = healcommTipTextLeft1:GetText() + if self.Buffs[buffName] and self.Buffs[buffName].icon == buffTexture then + Spellpower = (self.Buffs[buffName].amount * buffApplications) + Spellpower + healmod = (self.Buffs[buffName].mod * buffApplications) + healmod + end + end + return Spellpower, healmod +end + +function HealComm:GetUnitSpellPower(unit, spell) + local targetpower = 0 + local targetmod = 1 + local buffTexture, buffApplications + local debuffTexture, debuffApplications + local buffName + for i=1, 32 do + if UnitIsVisible(unit) and UnitIsConnected(unit) and UnitCanAssist("player", unit) then + buffTexture, buffApplications = UnitBuff(unit, i) + healcommTip:SetUnitBuff(unit, i) + else + buffTexture, buffApplications = UnitBuff("player", i) + healcommTip:SetUnitBuff("player", i) + end + if not buffTexture then + break + end + buffName = healcommTipTextLeft1:GetText() + if buffName == L["Blessing of Light"] then + local HLBonus, FoLBonus = strmatch(healcommTipTextLeft2:GetText(),"(%d+).-(%d+)") + if (spell == L["Flash of Light"]) then + targetpower = FoLBonus + targetpower + elseif spell == L["Holy Light"] then + targetpower = HLBonus + targetpower + end + end + if buffName == L["Healing Way"] and spell == L["Healing Wave"] then + targetmod = targetmod * ((buffApplications * 0.06) + 1) + end + end + for i=1, 16 do + if UnitIsVisible(unit) and UnitIsConnected(unit) and UnitCanAssist("player", unit) then + debuffTexture, debuffApplications = UnitDebuff(unit, i) + healcommTip:SetUnitDebuff(unit, i) + else + debuffTexture, debuffApplications = UnitDebuff("player", i) + healcommTip:SetUnitDebuff("player", i) + end + if not debuffTexture then + break + end + local debuffName = healcommTipTextLeft1:GetText() + if self.Debuffs[debuffName] then + targetpower = (self.Debuffs[debuffName].amount * debuffApplications) + targetpower + targetmod = (1-(self.Debuffs[debuffName].mod * debuffApplications)) * targetmod + end + end + return targetpower, targetmod +end + +function HealComm:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time) + if caster ~= player_guid then return end + if action == "MAINHAND" or action == "OFFHAND" then return end + + local spellName, rank = SpellInfo(spell_id) + _,_,rank = string.find(rank,"(%d+)") + + if not (self.Spells[spellName] or Resurrections[spellName] or Hots[spellName]) then return end + + self.CurrentSpellName = spellName + self.CurrentSpellRank = rank + self:ProcessSpellCast(target) + + if action == "START" then + self:SPELLCAST_START(spellName,cast_time) + elseif action== "CAST" then + self:SPELLCAST_STOP() + end +end + +function HealComm:UNIT_HEALTH() + local name = UnitName(arg1) + if self.pendingResurrections[name] then + for k,v in pairs(self.pendingResurrections[name]) do + self.pendingResurrections[name][k] = nil + end + self:TriggerEvent("HealComm_Ressupdate", name) + end +end + +-- Puppeteer: heal entries are recycled rather than creating a new table for every heal that starts +local freeHealEntries = {} +local function releaseHealEntry(entry) + if entry and table.getn(freeHealEntries) < 100 then + entry.amount = nil + entry.ctime = nil + table.insert(freeHealEntries, entry) + end +end + +function HealComm:stopHeal(caster) + if self:IsEventScheduled("Healcomm_"..caster) then + self:CancelScheduledEvent("Healcomm_"..caster) + end + if self.Lookup[caster] then + releaseHealEntry(self.Heals[self.Lookup[caster]][caster]) + self.Heals[self.Lookup[caster]][caster] = nil + self:TriggerEvent("HealComm_Healupdate", self.Lookup[caster]) + self.Lookup[caster] = nil + end +end + +function HealComm:startHeal(caster, target, size, casttime) + self:ScheduleEvent("Healcomm_"..caster, self.stopHeal, (casttime/1000), self, caster) + if not self.Heals[target] then + self.Heals[target] = {} + end + if self.Lookup[caster] then + releaseHealEntry(self.Heals[self.Lookup[caster]][caster]) + self.Heals[self.Lookup[caster]][caster] = nil + self.Lookup[caster] = nil + end + local entry = table.remove(freeHealEntries) or {} + entry.amount = math.floor(size) + entry.ctime = (casttime/1000)+GetTime() + if self.Heals[target][caster] then + releaseHealEntry(self.Heals[target][caster]) + end + self.Heals[target][caster] = entry + self.Lookup[caster] = target + self:TriggerEvent("HealComm_Healupdate", target) +end + +function HealComm:delayHeal(caster, delay) + self:CancelScheduledEvent("Healcomm_"..caster) + if self.Lookup[caster] and self.Heals[self.Lookup[caster]] then + self.Heals[self.Lookup[caster]][caster].ctime = self.Heals[self.Lookup[caster]][caster].ctime + (delay/1000) + self:ScheduleEvent("Healcomm_"..caster, self.stopHeal, (self.Heals[self.Lookup[caster]][caster].ctime-GetTime()), self, caster) + end +end + +function HealComm:startGrpHeal(caster, size, casttime, party1, party2, party3, party4, party5) + self:ScheduleEvent("Healcomm_"..caster, self.stopGrpHeal, (casttime/1000), self, caster) + self.GrpHeals[caster] = {amount = math.floor(size), ctime = (casttime/1000)+GetTime(), targets = {party1, party2, party3, party4, party5}} + for i=1,getn(self.GrpHeals[caster].targets) do + self:TriggerEvent("HealComm_Healupdate", self.GrpHeals[caster].targets[i]) + end +end + +function HealComm:stopGrpHeal(caster) + if self:IsEventScheduled("Healcomm_"..caster) then + self:CancelScheduledEvent("Healcomm_"..caster) + end + local targets + if self.GrpHeals[caster] then + targets = self.GrpHeals[caster].targets + end + self.GrpHeals[caster] = nil + if targets then + for i=1,getn(targets) do + self:TriggerEvent("HealComm_Healupdate", targets[i]) + end + end +end + +function HealComm:delayGrpHeal(caster, delay) + self:CancelScheduledEvent("Healcomm_"..caster) + if self.GrpHeals[caster] then + self.GrpHeals[caster].ctime = self.GrpHeals[caster].ctime + (delay/1000) + self:ScheduleEvent("Healcomm_"..caster, self.stopGrpHeal, (self.GrpHeals[caster].ctime-GetTime()), self, caster) + end +end + +function HealComm:startResurrection(caster, target) + if not self.pendingResurrections[target] then + self.pendingResurrections[target] = {} + end + self.pendingResurrections[target][caster] = GetTime()+70 + self:ScheduleEvent("Healcomm_"..caster..target, self.RessExpire, 70, self, caster, target) + self:TriggerEvent("HealComm_Ressupdate", target) +end + +function HealComm:cancelResurrection(caster) + for k,v in pairs(self.pendingResurrections) do + if v[caster] and (v[caster]-GetTime()) > 60 then + self.pendingResurrections[k][caster] = nil + self:TriggerEvent("HealComm_Ressupdate", k) + end + end +end + +function HealComm:RessExpire(caster, target) + self.pendingResurrections[target][caster] = nil + self:TriggerEvent("HealComm_Ressupdate", target) +end + +function HealComm:SendAddonMessage(msg) + local zone = GetRealZoneText() + if zone == L["Warsong Gulch"] or zone == L["Arathi Basin"] or zone == L["Alterac Valley"] then + SendAddonMessage("HealComm", msg, "BATTLEGROUND") + else + SendAddonMessage("HealComm", msg, "RAID") + end +end + +function HealComm:SPELLCAST_START(spell,cast_time) + local spell = spell or arg1 + local cast_time = cast_time or arg2 + if ( self.SpellCastInfo and self.SpellCastInfo[1] == spell and self.Spells[spell] ) then + local Bonus = itemBonus:GetBonus("HEAL") + local buffpower, buffmod = self:GetBuffSpellPower() + local targetpower, targetmod = self.SpellCastInfo[4], self.SpellCastInfo[5] + local Bonus = Bonus + buffpower + local amount = ((math.floor(self.Spells[self.SpellCastInfo[1]][tonumber(self.SpellCastInfo[2])](Bonus))+targetpower)*buffmod*targetmod) + if spell == L["Prayer of Healing"] then + local targets = {UnitName("player")} + local targetsstring = UnitName("player").."/" + for i=1,4 do + if CheckInteractDistance("party"..i, 4) then + table.insert(targets, i ,UnitName("party"..i)) + targetsstring = targetsstring..UnitName("party"..i).."/" + end + end + self:SendAddonMessage("GrpHeal/"..amount.."/"..cast_time.."/"..targetsstring) + self:startGrpHeal(UnitName("player"), amount, cast_time, targets[1], targets[2], targets[3], targets[4], targets[5]) + else + self:SendAddonMessage("Heal/"..self.SpellCastInfo[3].."/"..amount.."/"..cast_time.."/") + self:startHeal(UnitName("player"), self.SpellCastInfo[3], amount, cast_time) + end + elseif ( self.SpellCastInfo and self.SpellCastInfo[1] == spell and Resurrections[spell] ) then + self:SendAddonMessage("Resurrection/"..self.SpellCastInfo[3].."/start/") + self:startResurrection(UnitName("player"), self.SpellCastInfo[3]) + end + self.spellIsCasting = spell +end + +function HealComm:SPELLCAST_INTERRUPTED() + if self:IsEventScheduled("TriggerRegrowthHot") then + self:CancelScheduledEvent("TriggerRegrowthHot") + end + + if self.Spells[self.spellIsCasting] then + if self.spellIsCasting == L["Prayer of Healing"] then + self:SendAddonMessage("GrpHealstop") + self:stopGrpHeal(UnitName("player")) + else + self:SendAddonMessage("Healstop") + self:stopHeal(UnitName("player")) + end + elseif Resurrections[self.spellIsCasting] then + self:SendAddonMessage("Resurrection/stop/") + self:cancelResurrection(UnitName("player")) + end + self.CurrentSpellRank = nil + self.CurrentSpellName = nil + self.spellIsCasting = nil + for key in pairs(self.SpellCastInfo) do + self.SpellCastInfo[key] = nil + end +end + +function HealComm:SPELLCAST_FAILED() + self.failed = true +end + +function HealComm:SPELLCAST_DELAYED() + if self.spellIsCasting == L["Prayer of Healing"] then + self:SendAddonMessage("GrpHealdelay/"..arg1.."/") + self:delayGrpHeal(UnitName("player"), arg1) + else + self:SendAddonMessage("Healdelay/"..arg1.."/") + self:delayHeal(UnitName("player"), arg1) + end +end + +function HealComm:TriggerRegrowthHot() + local dur = 21 + self:SendAddonMessage("Regr/"..self.savetarget.."/"..dur.."/") + if not self.Hots[self.savetarget] then + self.Hots[self.savetarget] = {} + end + if not self.Hots[self.savetarget]["Regr"] then + self.Hots[self.savetarget]["Regr"]= {} + end + self.Hots[self.savetarget]["Regr"].start = GetTime() + self.Hots[self.savetarget]["Regr"].dur = dur + self:TriggerEvent("HealComm_Hotupdate", roster:GetUnitIDFromName(self.savetarget), "Regrowth") +end + +function HealComm:SPELLCAST_STOP() + if not self.SpellCastInfo then return end + local targetUnit = roster:GetUnitIDFromName(self.SpellCastInfo[3]) + if targetUnit then + if self.SpellCastInfo[1] == L["Renew"] then + local dur = getSetBonus() and 18 or 15 + self:SendAddonMessage("Renew/"..self.SpellCastInfo[3].."/"..dur.."/") + if not self.Hots[self.SpellCastInfo[3]] then + self.Hots[self.SpellCastInfo[3]] = {} + end + if not self.Hots[self.SpellCastInfo[3]]["Renew"] then + self.Hots[self.SpellCastInfo[3]]["Renew"]= {} + end + self.Hots[self.SpellCastInfo[3]]["Renew"].start = GetTime() + self.Hots[self.SpellCastInfo[3]]["Renew"].dur = dur + self:TriggerEvent("HealComm_Hotupdate", targetUnit, "Renew") + elseif self.SpellCastInfo[1] == L["Rejuvenation"] then + local dur = getSetBonus() and 15 or 12 + self:SendAddonMessage("Reju/"..self.SpellCastInfo[3].."/"..dur.."/") + if not self.Hots[self.SpellCastInfo[3]] then + self.Hots[self.SpellCastInfo[3]] = {} + end + if not self.Hots[self.SpellCastInfo[3]]["Reju"] then + self.Hots[self.SpellCastInfo[3]]["Reju"]= {} + end + self.Hots[self.SpellCastInfo[3]]["Reju"].start = GetTime() + self.Hots[self.SpellCastInfo[3]]["Reju"].dur = dur + self:TriggerEvent("HealComm_Hotupdate", targetUnit, "Rejuvenation") + elseif self.SpellCastInfo[1] == L["Regrowth"] then + self.savetarget = self.SpellCastInfo[3] + self:ScheduleEvent("TriggerRegrowthHot", self.TriggerRegrowthHot, 0.3, self) + end + end + self.CurrentSpellRank = nil + self.CurrentSpellName = nil + for key in pairs(self.SpellCastInfo) do + self.SpellCastInfo[key] = nil + end +end + +function HealComm:CHAT_MSG_ADDON() + if arg1 == "HealComm" and arg4 ~= UnitName("player") then + local result = strsplit(arg2,"/") + if result[1] == "Heal" then + self:startHeal(arg4, result[2], result[3], result[4]) + elseif arg2 == "Healstop" then + self:stopHeal(arg4) + elseif result[1] == "Healdelay" then + self:delayHeal(arg4, result[2]) + elseif result[1] == "Resurrection" and result[2] == "stop" then + self:cancelResurrection(arg4) + elseif result[1] == "Resurrection" and result[3] == "start" then + self:startResurrection(arg4, result[2]) + elseif result[1] == "GrpHeal" then + self:startGrpHeal(arg4, result[2], result[3], result[4], result[5], result[6], result[7], result[8]) + elseif arg2 == "GrpHealstop" then + self:stopGrpHeal(arg4) + elseif result[1] == "GrpHealdelay" then + self:delayGrpHeal(arg4, result[2]) + elseif result[1] == "Renew" then + if not self.Hots[result[2]] then + self.Hots[result[2]] = {} + end + if not self.Hots[result[2]]["Renew"] then + self.Hots[result[2]]["Renew"]= {} + end + self.Hots[result[2]]["Renew"].dur = result[3] + self.Hots[result[2]]["Renew"].start = GetTime() + local targetUnit = roster:GetUnitIDFromName(result[2]) + self:TriggerEvent("HealComm_Hotupdate", targetUnit, "Renew") + elseif result[1] == "Reju" then + if not self.Hots[result[2]] then + self.Hots[result[2]] = {} + end + if not self.Hots[result[2]]["Reju"] then + self.Hots[result[2]]["Reju"]= {} + end + self.Hots[result[2]]["Reju"].dur = result[3] + self.Hots[result[2]]["Reju"].start = GetTime() + local targetUnit = roster:GetUnitIDFromName(result[2]) + self:TriggerEvent("HealComm_Hotupdate", targetUnit, "Rejuvenation") + elseif result[1] == "Regr" then + if not self.Hots[result[2]] then + self.Hots[result[2]] = {} + end + if not self.Hots[result[2]]["Regr"] then + self.Hots[result[2]]["Regr"]= {} + end + self.Hots[result[2]]["Regr"].dur = result[3] + self.Hots[result[2]]["Regr"].start = GetTime() + local targetUnit = roster:GetUnitIDFromName(result[2]) + self:TriggerEvent("HealComm_Hotupdate", targetUnit, "Regrowth") + end + end +end + +function HealComm:UNIT_AURA() + local name = UnitName(arg1) + if self.Hots[name] and (self.Hots[name]["Regr"] or self.Hots[name]["Reju"] or self.Hots[name]["Renew"]) then + local regr,reju,renew + for i=1,32 do + if not UnitBuff(arg1,i) then + break + end + healcommTip:ClearLines() + healcommTip:SetUnitBuff(arg1,i) + regr = regr or healcommTipTextLeft1:GetText() == L["Regrowth"] + reju = reju or healcommTipTextLeft1:GetText() == L["Rejuvenation"] + renew = renew or healcommTipTextLeft1:GetText() == L["Renew"] + end + if not regr then + self.Hots[name]["Regr"] = nil + self:TriggerEvent("HealComm_Hotupdate", arg1, "Regrowth") + end + if not reju then + self.Hots[name]["Reju"] = nil + self:TriggerEvent("HealComm_Hotupdate", arg1, "Rejuvenation") + end + if not renew then + self.Hots[name]["Renew"] = nil + self:TriggerEvent("HealComm_Hotupdate", arg1, "Renew") + end + end +end + +function HealComm:getRegrTime(unit) + if unit == UNKNOWNOBJECT or unit == UKNOWNBEING then + return + end + local dbUnit = self.Hots[UnitName(unit)] + if dbUnit and dbUnit["Regr"] and (dbUnit["Regr"].start + dbUnit["Regr"].dur) > GetTime() then + return dbUnit["Regr"].start, dbUnit["Regr"].dur + else + return + end +end + +function HealComm:getRejuTime(unit) + if unit == UNKNOWNOBJECT or unit == UKNOWNBEING then + return + end + local dbUnit = self.Hots[UnitName(unit)] + if dbUnit and dbUnit["Reju"] and (dbUnit["Reju"].start + dbUnit["Reju"].dur) > GetTime() then + return dbUnit["Reju"].start, dbUnit["Reju"].dur + else + return + end +end + +function HealComm:getRenewTime(unit) + if unit == UNKNOWNOBJECT or unit == UKNOWNBEING then + return + end + local dbUnit = self.Hots[UnitName(unit)] + if dbUnit and dbUnit["Renew"] and (dbUnit["Renew"].start + dbUnit["Renew"].dur) > GetTime() then + return dbUnit["Renew"].start, dbUnit["Renew"].dur + else + return + end +end + +function HealComm:getHeal(unit) + if unit == UNKNOWNOBJECT or unit == UKNOWNBEING then + return 0 + end + local healamount = 0 + if self.Heals[unit] then + for k,v in self.Heals[unit] do + healamount = healamount+v.amount + end + end + for k,v in pairs(self.GrpHeals) do + for j,c in pairs(v.targets) do + if unit == c then + healamount = healamount+v.amount + end + end + end + return healamount +end + +function HealComm:UnitisResurrecting(unit) + local resstime + if self.pendingResurrections[unit] then + for k,v in pairs(self.pendingResurrections[unit]) do + if v < GetTime() then + self.pendingResurrections[unit][k] = nil + elseif not resstime or resstime > v then + resstime = v + end + end + end + return resstime +end + +function HealComm:getNumHeals(unit) + if unit == UNKNOWNOBJECT or unit == UKNOWNBEING then + return 0 + end + local heals = 0 + if self.Heals[unit] then + for _ in self.Heals[unit] do + heals = heals + 1 + end + end + for _,v in pairs(self.GrpHeals) do + for _,c in pairs(v.targets) do + if unit == c then + heals = heals + 1 + end + end + end + return heals +end + + +function HealComm:CastSpell(spellId, spellbookTabNum) + self.hooks.CastSpell(spellId, spellbookTabNum) + + if self.failed or (self.CurrentSpellName and not SpellIsTargeting()) then + self.failed = nil + return + end + + local spellName, rank = GetSpellName(spellId, spellbookTabNum) + _,_,rank = string.find(rank,"(%d+)") + + if not (self.Spells[spellName] or Resurrections[spellName] or Hots[spellName]) then return end + + self.CurrentSpellName = spellName + self.CurrentSpellRank = rank + if not SpellIsTargeting() then + if ( UnitIsVisible("target") and UnitIsConnected("target") and UnitCanAssist("player", "target") ) then + -- Spell is being cast on the current target. + if UnitIsPlayer("target") then + self:ProcessSpellCast("target") + end + else + self:ProcessSpellCast("player") + end + end +end + +function HealComm:CastSpellByName(spellName, onSelf) + self.hooks.CastSpellByName(spellName, onSelf) + + if self.failed then + self.failed = nil + return + end + + if (self.CurrentSpellName and not SpellIsTargeting()) or (GetCVar("AutoSelfCast") == "0" and onSelf ~= 1 and not SpellIsTargeting() and not (UnitExists("target") and UnitCanAssist("player", "target"))) then return end + + local _,_,rank = string.find(spellName,"(%d+)") + local _, _, spellName = string.find(spellName, "^([^%(]+)") + spellName = string.lower(spellName) + local i = 1 + while GetSpellName(i, BOOKTYPE_SPELL) do + local s, r = GetSpellName(i, BOOKTYPE_SPELL) + if string.lower(s) == spellName then + spellName = s + if rank then + break + else + while s == spellName do + rank = r + i = i+1 + s, r = GetSpellName(i, BOOKTYPE_SPELL) + end + break + end + end + i = i+1 + end + if rank then + _,_,rank = string.find(rank,"(%d+)") + end + if spellName then + if not (self.Spells[spellName] or Resurrections[spellName] or Hots[spellName]) then return end + self.CurrentSpellName = spellName + self.CurrentSpellRank = rank + + if not SpellIsTargeting() then + if UnitIsVisible("target") and UnitIsConnected("target") and UnitCanAssist("player", "target") and onSelf ~= 1 then + if UnitIsPlayer("target") then + self:ProcessSpellCast("target") + end + else + self:ProcessSpellCast("player") + end + end + end +end + +function HealComm:OnMouseDown(object) + local unit = "mouseover" + if ( self.CurrentSpellName and GameTooltipTextLeft1:IsVisible() ) then + local _, _, name = string.find(GameTooltipTextLeft1:GetText(), L["^Corpse of (.+)$"]) + if ( name ) then + unit = roster:GetUnitIDFromName(name) + end + end + if ( self.CurrentSpellName and SpellIsTargeting() and UnitExists(unit) ) then + self:ProcessSpellCast(unit) + end + if ( self.hooks[object]["OnMouseDown"] ) then + self.hooks[object]["OnMouseDown"]() + end +end + +function HealComm:UseAction(slot, checkCursor, onSelf) + healcommTip:ClearLines() + healcommTip:SetAction(slot) + local spellName = healcommTipTextLeft1:GetText() + + self.hooks.UseAction(slot, checkCursor, onSelf) + + -- Test to see if this is a macro + if self.failed or GetActionText(slot) or (self.CurrentSpellName and not SpellIsTargeting()) or not (self.Spells[spellName] or Resurrections[spellName] or Hots[spellName]) then + self.failed = nil + return + end + + self.CurrentSpellName = spellName + local rank = healcommTipTextRight1:GetText() + if rank then + _,_,rank = string.find(rank,"(%d+)") + end + self.CurrentSpellRank = rank or 1 + + if not SpellIsTargeting() then + if ( UnitIsVisible("target") and UnitIsConnected("target") and UnitCanAssist("player", "target") and onSelf ~= 1) then + -- Spell is being cast on the current target + if UnitIsPlayer("target") then + self:ProcessSpellCast("target") + end + else + -- Spell is being cast on the player + self:ProcessSpellCast("player") + end + end +end + +function HealComm:SpellTargetUnit(unit) + local shallTargetUnit + if ( SpellIsTargeting() ) then + shallTargetUnit = true + end + self.hooks.SpellTargetUnit(unit) + + if ( shallTargetUnit and self.CurrentSpellName and not SpellIsTargeting() ) then + if UnitIsPlayer(unit) then + self:ProcessSpellCast(unit) + end + self.CurrentSpellName = nil + self.CurrentSpellRank = nil + end +end + +function HealComm:SpellStopTargeting() + self.hooks.SpellStopTargeting() + self.CurrentSpellName = nil + self.CurrentSpellRank = nil +end + +function HealComm:TargetUnit(unit) + -- Look to see if we're currently waiting for a target internally + -- If we are, then well glean the target info here. + if ( self.CurrentSpellName and UnitExists(unit) ) and UnitIsPlayer(unit) then + self:ProcessSpellCast(unit) + end + self.hooks.TargetUnit(unit) +end + +function HealComm:ProcessSpellCast(unit) + local power, mod = self:GetUnitSpellPower(unit, self.CurrentSpellName) + self.SpellCastInfo[1] = (self.SpellCastInfo[1] or self.CurrentSpellName) + self.SpellCastInfo[2] = (self.SpellCastInfo[2] or self.CurrentSpellRank) + self.SpellCastInfo[3] = (self.SpellCastInfo[3] or (unit and UnitName(unit) or "Unknown")) + self.SpellCastInfo[4] = (self.SpellCastInfo[4] or power) + self.SpellCastInfo[5] = (self.SpellCastInfo[5] or mod) +end + +AceLibrary:Register(HealComm, MAJOR_VERSION, MINOR_VERSION, activate, nil, external) +HealComm = nil diff --git a/libs/ace/ItemBonusLib-1.0/ItemBonusLib-1.0.lua b/libs/ace/ItemBonusLib-1.0/ItemBonusLib-1.0.lua new file mode 100644 index 0000000..8d104b5 --- /dev/null +++ b/libs/ace/ItemBonusLib-1.0/ItemBonusLib-1.0.lua @@ -0,0 +1,482 @@ +local MAJOR_VERSION = "ItemBonusLib-1.0" +local MINOR_VERSION = "$Revision: 17465 $" + +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end + +if not AceLibrary:HasInstance("AceLocale-2.2") then error(MAJOR_VERSION .. " requires AceLocale-2.2") end +if not AceLibrary:HasInstance("Gratuity-2.0") then error(MAJOR_VERSION .. " requires Gratuity-2.0") end +if not AceLibrary:HasInstance("Deformat-2.0") then error(MAJOR_VERSION .. " requires Deformat-2.0") end + +local DEBUG = false + +local ItemBonusLib = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceConsole-2.0", "AceDebug-2.0") +ItemBonusLib:SetDebugging(DEBUG) + +local Gratuity = AceLibrary("Gratuity-2.0") +local Deformat = AceLibrary("Deformat-2.0") + +local L = AceLibrary("AceLocale-2.2"):new("ItemBonusLib") + +-- bonuses[BONUS] = VALUE +local bonuses = {} + +-- details[BONUS][SLOT] = VALUE +local details = {} + +-- items[LINK].bonuses[BONUS] = VALUE +-- items[LINK].set = SETNAME +-- items[LINK].set_line = number +local items = {} + +-- sets[SETNAME].count = COUNT +-- sets[SETNAME].bonuses[NUM][BONUS] = VALUE +-- sets[SETNAME].scan_count = COUNT +-- sets[SETNAME].scan_bonuses = COUNT +local sets = {} + +local slots = { + ["Head"] = true, + ["Neck"] = true, + ["Shoulder"] = true, + ["Shirt"] = true, + ["Chest"] = true, + ["Waist"] = true, + ["Legs"] = true, + ["Feet"] = true, + ["Wrist"] = true, + ["Hands"] = true, + ["Finger0"] = true, + ["Finger1"] = true, + ["Trinket0"] = true, + ["Trinket1"] = true, + ["Back"] = true, + ["MainHand"] = true, + ["SecondaryHand"] = true, + ["Ranged"] = true, + ["Tabard"] = true, +} + +function ItemBonusLib:OnInitialize() + self:RegisterEvent("PLAYER_ENTERING_WORLD") + self:RegisterEvent("PLAYER_LEAVING_WORLD") + for s in pairs(slots) do + slots[s] = GetInventorySlotInfo (s.."Slot") + end + + local options = { + type = "group", + desc = L["An addon to get information about bonus from equipped items"], + args = { + show = { + type = "execute", + name = L["show"], + desc = L["Show all bonuses from the current equipment"], + func = function () + self:Print(L["Current equipment bonuses:"]) + for bonus, value in pairs(bonuses) do + self:Print("%s : %d", self:GetBonusFriendlyName(bonus), value) + end + end + }, + details = { + type = "execute", + name = L["details"], + desc = L["Shows bonuses with slot distribution"], + func = function () + self:Print(L["Current equipment bonus details:"]) + for bonus, detail in pairs(details) do + local s = {} + for slot, value in pairs(detail) do + table.insert(s, string.format("%s : %d", slot, value)) + end + self:Print("%s : %d (%s)", self:GetBonusFriendlyName(bonus), bonuses[bonus], table.concat(s, ", ")) + end + end + }, + item = { + type = "text", + name = L["item"], + desc = L["show bonuses of given itemlink"], + usage = L[""], + get = false, + set = function (link) + local info = self:ScanItemLink(link) + self:Print(L["Bonuses for %s:"], link) + for bonus, value in pairs(info.bonuses) do + self:Print("%s : %d", self:GetBonusFriendlyName(bonus), value) + end + if info.set then + self:Print(L["Item is part of set [%s]"], info.set) + local set = sets[info.set] + for number, bonuses in pairs(set.bonuses) do + local has_bonus = number <= set.count and "*" or " " + self:Print(L[" %sBonus for %d pieces :"], has_bonus, number) + for bonus, value in pairs(bonuses) do + self:Print(" %s : %d", self:GetBonusFriendlyName(bonus), value) + end + end + end + end + }, + slot = { + type = "text", + name = L["slot"], + desc = L["show bonuses of given slot"], + usage = L[""], + get = false, + set = function (slot) + self:Print(L["Bonuses of slot %s:"], slot) + for bonus, detail in pairs(details) do + if detail[slot] then + self:Print("%s : %d", self:GetBonusFriendlyName(bonus), detail[slot]) + end + end + end + }, + }, + } + + self:RegisterChatCommand(L.CHAT_COMMANDS, options) + +end + +function ItemBonusLib:PLAYER_ENTERING_WORLD() + self:RegisterBucketEvent("UNIT_INVENTORY_CHANGED", 0.5) + self:ScheduleEvent(function() self:ScanEquipment() end, 1) +end + +function ItemBonusLib:PLAYER_LEAVING_WORLD() + self:UnregisterBucketEvent("UNIT_INVENTORY_CHANGED") +end + +function ItemBonusLib:UNIT_INVENTORY_CHANGED(units) + if units.player then + self:ScanEquipment() + end +end + +local cleanItemLink +do + local s = string + local trim = function (str) + local gsub = s.gsub + str = gsub (str, "^%s+", "" ) + str = gsub (str, "%s+$", "" ) + str = gsub (str, "%.$", "" ) + return str + end + + local equip = ITEM_SPELL_TRIGGER_ONEQUIP + local l_equip = s.len(equip) + + function cleanItemLink(itemLink) + local _, _, link = s.find(itemLink, "|c%x+|H(item:%d+:%d+:%d+:%d+)|h%[.-%]|h|r") + return link or itemLink + end + + function ItemBonusLib:AddValue(bonuses, effect, value) + if type(effect) == "string" then + bonuses[effect] = (bonuses[effect] or 0) + value + elseif type(value) == "table" then + for i, e in ipairs(effect) do + self:AddValue (bonuses, e, value[i]) + end + else + for _, e in ipairs(effect) do + self:AddValue (bonuses, e, value) + end + end + end + + function ItemBonusLib:CheckPassive(bonuses, line) + for _, p in pairs(L.PATTERNS_PASSIVE) do + local _, _, value = s.find (line, "^" .. p.pattern) + if value then + self:AddValue (bonuses, p.effect, value) + return true + end + end + end + + function ItemBonusLib:CheckToken(bonuses, token, value) + local t = L.PATTERNS_GENERIC_LOOKUP[token] + if t then + self:AddValue (bonuses, t, value) + return true + else + local s1, s2 + + for _, p in ipairs(L.PATTERNS_GENERIC_STAGE1) do + if s.find (token, p.pattern, 1, 1) then + s1 = p.effect + break + end + end + for _, p in ipairs(L.PATTERNS_GENERIC_STAGE2) do + if s.find(token, p.pattern, 1, 1) then + s2 = p.effect + break + end + end + if s1 and s2 then + self:AddValue (bonuses, s1..s2, value) + return true + end + end + self:Debug("CheckToken failed for \"%s\" (%d)", token, value) + end + + function ItemBonusLib:CheckGeneric(bonuses, line) + local found + + while s.len(line) > 0 do + local tmpStr + local pos = s.find (line, "/", 1, true) + if pos then + tmpStr = s.sub (line, 1, pos-1) + line = s.sub (line, pos+1) + else + tmpStr = line + line = "" + end + + -- trim line + tmpStr = trim (tmpStr) + + local _, _, value, token = s.find(tmpStr, "^%+(%d+)%%?(.*)$") + if not value then + _, _, token, value = s.find(tmpStr, "^(.*)%+(%d+)%%?$") + end + if token and value then + -- trim token + token = trim (token) + if self:CheckToken (bonuses, token, value) then + found = true + end + end + end + return found + end + + function ItemBonusLib:CheckOther(bonuses, line) + for _, p in ipairs(L.PATTERNS_OTHER) do + local start, _, value = s.find (line, "^" .. p.pattern) + if start then + if p.value then + self:AddValue(bonuses, p.effect, p.value) + elseif value then + self:AddValue (bonuses, p.effect, value) + end + return true + end + end + end + + function ItemBonusLib:AddBonusInfo(bonuses, line, no_prefix) + local found + if no_prefix then + found = self:CheckPassive(bonuses, line) + elseif s.sub (line, 0, l_equip) == equip then + found = self:CheckPassive (bonuses, s.sub(line, l_equip + 2)) + end + if not found then + found = self:CheckGeneric(bonuses, line) + if not found then + found = self:CheckOther(bonuses, line) + if not found then + self:Debug("Unmatched bonus line \"%s\"", line) + end + end + end + end +end + +do + local ITEM_SET_NAME = ITEM_SET_NAME + local ITEM_SET_BONUS = ITEM_SET_BONUS + local ITEM_SET_BONUS_GRAY = ITEM_SET_BONUS_GRAY + + function ItemBonusLib:ScanItemLink(link) + link = cleanItemLink(link) + local info = items[link] + local scan_set + local set_name, set_count, set_total + if not info then + info = { bonuses = {} } + Gratuity:SetHyperlink(link) + for i = 2, Gratuity:NumLines() do + local line = Gratuity:GetLine(i) + set_name, set_count, set_total = Deformat(line, ITEM_SET_NAME) + if set_name then + info.set = set_name + info.set_line = i + local set = sets[set_name] + if not set or set.scan_count > set_count and set.scan_bonuses > 1 then + scan_set = true + end + break + end + self:AddBonusInfo(info.bonuses, line) + end + items[link] = info + elseif info.set then + Gratuity:SetHyperlink(link) + set_name, set_count, set_total = Deformat(Gratuity:GetLine(info.set_line), ITEM_SET_NAME) + local set = sets[set_name] + if set.scan_count > set_count and set.scan_bonuses > 1 then + scan_set = true + end + end + if scan_set then + self:Debug("Scanning set \"%s\"", set_name) + local set = { count = 0, bonuses = {}, scan_count = set_count, scan_bonuses = 0 } + for i = info.set_line + set_total + 2, Gratuity:NumLines() do + local line = Gratuity:GetLine(i) + local count, bonus + local bonus = Deformat(line, ITEM_SET_BONUS) + if bonus then + set.scan_bonuses = set.scan_bonuses + 1 + count = set_count + else + count, bonus = Deformat( + line, ITEM_SET_BONUS_GRAY) + end + if not bonus then + self:Debug("Invalid set line \"%s\"", line) + -- break + else + local bonuses = set.bonuses[count] or {} + self:AddBonusInfo(bonuses, bonus, true) + set.bonuses[count] = bonuses + end + end + sets[set_name] = set + end + return info + end +end + +function ItemBonusLib:ScanEquipment() + -- clean bonus information + for bonus in pairs(bonuses) do + bonuses[bonus] = nil + end + for bonus, detail in pairs(details) do + for slot in pairs(detail) do + detail[slot] = nil + end + end + for _, set in pairs(sets) do + set.count = 0 + end + + for slot, id in pairs(slots) do + local link = GetInventoryItemLink("player", id) + if link then + self:Debug("Scanning item %s", link) + local info = self:ScanItemLink(link) + local set = info.set + if set then + sets[set].count = sets[set].count + 1 + end + for bonus, value in pairs(info.bonuses) do + bonuses[bonus] = (bonuses[bonus] or 0) + value + if not details[bonus] then + details[bonus] = {} + end + details[bonus][slot] = (details[bonus][slot] or 0) + value + end + end + end + for _, set in pairs(sets) do + for i = 2, set.count do + if set.bonuses[i] then + for bonus, value in pairs(set.bonuses[i]) do + bonuses[bonus] = (bonuses[bonus] or 0) + value + if not details[bonus] then + details[bonus] = {} + end + details[bonus].Set = (details[bonus].Set or 0) + value + end + end + end + end + self:TriggerEvent("ItemBonusLib_Update") +end + +-- DEBUG +if DEBUG then + function ItemBonusLib:DumpCachedItems(clear) + DevTools_Dump(items) + if clear then + items = {} + end + end + + function ItemBonusLib:DumpCachedSets(clear) + DevTools_Dump(sets) + end + + function ItemBonusLib:DumpBonuses() + DevTools_Dump(bonuses) + end + + function ItemBonusLib:DumpDetails() + DevTools_Dump(details) + end + + function ItemBonusLib:Reload() + items = {} + sets = {} + self:ScanEquipment() + end +end + +-- BonusScanner compatible API +function ItemBonusLib:GetBonus(bonus) + return bonuses[bonus] or 0 +end + +function ItemBonusLib:GetSlotBonuses (slotname) + local bonuses = {} + for bonus, detail in pairs(details) do + if detail[slotname] then + bonuses[bonus] = detail[slotname] + end + end + return bonuses +end + +function ItemBonusLib:GetBonusDetails (bonus) + return details[bonus] or {} +end + +function ItemBonusLib:GetSlotBonus (bonus, slotname) + local detail = details[bonus] + return detail and detail[slotname] or 0 +end + +function ItemBonusLib:GetBonusFriendlyName (bonus) + return L.NAMES[bonus] or bonus +end + +function ItemBonusLib:IsActive () + return true +end + +function ItemBonusLib:ScanItem (itemlink, excludeSet) + if not excludeSet then + self:error("excludeSet can't be false on BonusScanner compatible API") + end + local name, link = GetItemInfo(itemlink) + if not name then + return + end + return self:ScanItemLink(link).bonuses +end + +function ItemBonusLib:ScanTooltipFrame (frame, excludeSet) + self:error("BonusScanner:ScanTooltipFrame() is not available") +end + +AceLibrary:Register(ItemBonusLib, MAJOR_VERSION, MINOR_VERSION) diff --git a/libs/ace/ItemBonusLib-1.0/Locales/enUS.lua b/libs/ace/ItemBonusLib-1.0/Locales/enUS.lua new file mode 100644 index 0000000..5714bd4 --- /dev/null +++ b/libs/ace/ItemBonusLib-1.0/Locales/enUS.lua @@ -0,0 +1,226 @@ +if AceLibrary:HasInstance("ItemBonusLib-1.0") then return end + +local L = AceLibrary("AceLocale-2.2"):new("ItemBonusLib") + +L:RegisterTranslations("enUS", function () return { + CHAT_COMMANDS = { "/abonus" }, + ["An addon to get information about bonus from equipped items"] = true, + ["show"] = true, + ["Show all bonuses from the current equipment"] = true, + ["Current equipment bonuses:"] = true, + ["details"] = true, + ["Shows bonuses with slot distribution"] = true, + ["Current equipment bonus details:"] = true, + ["item"] = true, + ["show bonuses of given itemlink"] = true, + [""] = true, + ["Bonuses for %s:"] = true, + ["Item is part of set [%s]"] = true, + [" %sBonus for %d pieces :"] = true, + ["slot"] = true, + ["show bonuses of given slot"] = true, + [""] = true, + ["Bonuses of slot %s:"] = true, + + -- bonus names + NAMES = { + STR = "Strength", + AGI = "Agility", + STA = "Stamina", + INT = "Intellect", + SPI = "Spirit", + ARMOR = "Reinforced Armor", + + ARCANERES = "Arcane Resistance", + FIRERES = "Fire Resistance", + NATURERES = "Nature Resistance", + FROSTRES = "Frost Resistance", + SHADOWRES = "Shadow Resistance", + + FISHING = "Fishing", + MINING = "Mining", + HERBALISM = "Herbalism", + SKINNING = "Skinning", + DEFENSE = "Defense", + + BLOCK = "Chance to Block", + BLOCKVALUE = "Block value", + DODGE = "Dodge", + PARRY = "Parry", + ATTACKPOWER = "Attack Power", + ATTACKPOWERUNDEAD = "Attack Power against Undead", + ATTACKPOWERFERAL = "Attack Power in feral form", + CRIT = "Crit. hits", + RANGEDATTACKPOWER = "Ranged Attack Power", + RANGEDCRIT = "Crit. Shots", + TOHIT = "Chance to Hit", + + DMG = "Spell Damage", + DMGUNDEAD = "Spell Damage against Undead", + ARCANEDMG = "Arcane Damage", + FIREDMG = "Fire Damage", + FROSTDMG = "Frost Damage", + HOLYDMG = "Holy Damage", + NATUREDMG = "Nature Damage", + SHADOWDMG = "Shadow Damage", + SPELLCRIT = "Crit. Spell", + SPELLTOHIT = "Chance to Hit with spells", + SPELLPEN = "Spell Penetration", + HEAL = "Healing", + HOLYCRIT = "Crit. Holy Spell", + + HEALTHREG = "Life Regeneration", + MANAREG = "Mana Regeneration", + HEALTH = "Life Points", + MANA = "Mana Points" + }; + + + -- passive bonus patterns. checked against lines which start with above prefixes + PATTERNS_PASSIVE = { + { pattern = "+(%d+) ranged Attack Power%.", effect = "RANGEDATTACKPOWER" }, + { pattern = "Increases your chance to block attacks with a shield by (%d+)%%%.", effect = "BLOCK" }, + { pattern = "Increases the block value of your shield by (%d+)%.", effect = "BLOCKVALUE" }, + { pattern = "Increases your chance to dodge an attack by (%d+)%%%.", effect = "DODGE" }, + { pattern = "Increases your chance to parry an attack by (%d+)%%%.", effect = "PARRY" }, + { pattern = "Improves your chance to get a critical strike with spells by (%d+)%%%.", effect = "SPELLCRIT" }, + { pattern = "Improves your chance to get a critical strike with Holy spells by (%d+)%%%.", effect = "HOLYCRIT" }, + { pattern = "Increases the critical effect chance of your Holy spells by (%d+)%%%.", effect = "HOLYCRIT" }, + { pattern = "Improves your chance to get a critical strike by (%d+)%%%.", effect = "CRIT" }, + { pattern = "Improves your chance to get a critical strike with missile weapons by (%d+)%%%.", effect = "RANGEDCRIT" }, + { pattern = "Increases damage done by Arcane spells and effects by up to (%d+)%.", effect = "ARCANEDMG" }, + { pattern = "Increases damage done by Fire spells and effects by up to (%d+)%.", effect = "FIREDMG" }, + { pattern = "Increases damage done by Frost spells and effects by up to (%d+)%.", effect = "FROSTDMG" }, + { pattern = "Increases damage done by Holy spells and effects by up to (%d+)%.", effect = "HOLYDMG" }, + { pattern = "Increases damage done by Nature spells and effects by up to (%d+)%.", effect = "NATUREDMG" }, + { pattern = "Increases damage done by Shadow spells and effects by up to (%d+)%.", effect = "SHADOWDMG" }, + { pattern = "Increases healing done by spells and effects by up to (%d+)%.", effect = "HEAL" }, + { pattern = "Increases damage and healing done by magical spells and effects by up to (%d+)%.", effect = {"HEAL", "DMG"} }, + { pattern = "Increases damage done to Undead by magical spells and effects by up to (%d+)", effect = "DMGUNDEAD" }, + { pattern = "+(%d+) Attack Power when fighting Undead.", effect = "ATTACKPOWERUNDEAD" }, + { pattern = "Restores (%d+) health per 5 sec%.", effect = "HEALTHREG" }, + { pattern = "Restores (%d+) health every 5 sec%.", effect = "HEALTHREG" }, -- both versions ('per' and 'every') seem to be used + { pattern = "Restores (%d+) mana per 5 sec%.", effect = "MANAREG" }, + { pattern = "Restores (%d+) mana every 5 sec%.", effect = "MANAREG" }, + { pattern = "Improves your chance to hit by (%d+)%%%.", effect = "TOHIT" }, + { pattern = "Improves your chance to hit with spells by (%d+)%%%.", effect = "SPELLTOHIT" }, + { pattern = "Decreases the magical resistances of your spell targets by (%d+).", effect = "SPELLPEN" }, + + -- Added for HealPoints + { pattern = "Allows (%d+)%% of your Mana regeneration to continue while casting%.", effect = "CASTINGREG"}, + { pattern = "Improves your chance to get a critical strike with Nature spells by (%d+)%%%.", effect = "NATURECRIT"}, + { pattern = "Reduces the casting time of your Regrowth spell by 0%.(%d+) sec%.", effect = "CASTINGREGROWTH"}, + { pattern = "Reduces the casting time of your Holy Light spell by 0%.(%d+) sec%.", effect = "CASTINGHOLYLIGHT"}, + { pattern = "Reduces the casting time of your Healing Touch spell by 0%.(%d+) sec%.", effect = "CASTINGHEALINGTOUCH"}, + { pattern = "%-0%.(%d+) sec to the casting time of your Flash Heal spell%.", effect = "CASTINGFLASHHEAL"}, + { pattern = "%-0%.(%d+) seconds on the casting time of your Chain Heal spell%.", effect = "CASTINGCHAINHEAL"}, + { pattern = "Increases the duration of your Rejuvenation spell by (%d+) sec%.", effect = "DURATIONREJUV"}, + { pattern = "Increases the duration of your Renew spell by (%d+) sec%.", effect = "DURATIONRENEW"}, + { pattern = "Increases your normal health and mana regeneration by (%d+)%.", effect = "MANAREGNORMAL"}, + { pattern = "Increases the amount healed by Chain Heal to targets beyond the first by (%d+)%%%.", effect = "IMPCHAINHEAL"}, + { pattern = "Increases healing done by Rejuvenation by up to (%d+)%.", effect = "IMPREJUVENATION"}, + { pattern = "Increases healing done by Lesser Healing Wave by up to (%d+)%.", effect = "IMPLESSERHEALINGWAVE"}, + { pattern = "Increases healing done by Flash of Light by up to (%d+)%.", effect = "IMPFLASHOFLIGHT"}, + { pattern = "After casting your Healing Wave or Lesser Healing Wave spell%, gives you a 25%% chance to gain Mana equal to (%d+)%% of the base cost of the spell%.", effect = "REFUNDHEALINGWAVE"}, + { pattern = "Your Healing Wave will now jump to additional nearby targets%. Each jump reduces the effectiveness of the heal by (%d+)%%%, and the spell will jump to up to two additional targets%.", effect = "JUMPHEALINGWAVE"}, + { pattern = "Reduces the mana cost of your Healing Touch%, Regrowth%, Rejuvenation and Tranquility spells by (%d+)%%%.", effect = "CHEAPERDRUID"}, + { pattern = "On Healing Touch critical hits%, you regain (%d+)%% of the mana cost of the spell%.", effect = "REFUNDHTCRIT"}, + { pattern = "Reduces the mana cost of your Renew spell by (%d+)%%%.", effect = "CHEAPERRENEW"}, + + }; + + -- generic patterns have the form "+xx bonus" or "bonus +xx" with an optional % sign after the value. + + -- first the generic bonus string is looked up in the following table + PATTERNS_GENERIC_LOOKUP = { + ["All Stats"] = {"STR", "AGI", "STA", "INT", "SPI"}, + ["Strength"] = "STR", + ["Agility"] = "AGI", + ["Stamina"] = "STA", + ["Intellect"] = "INT", + ["Spirit"] = "SPI", + + ["All Resistances"] = { "ARCANERES", "FIRERES", "FROSTRES", "NATURERES", "SHADOWRES"}, + + ["Fishing"] = "FISHING", + ["Fishing Lure"] = "FISHING", + ["Increased Fishing"] = "FISHING", + ["Mining"] = "MINING", + ["Herbalism"] = "HERBALISM", + ["Skinning"] = "SKINNING", + ["Defense"] = "DEFENSE", + ["Increased Defense"] = "DEFENSE", + + ["Attack Power"] = "ATTACKPOWER", + ["Attack Power when fighting Undead"] = "ATTACKPOWERUNDEAD", + ["Attack Power in Cat, Bear, and Dire Bear forms only"] = "ATTACKPOWERFERAL", + + ["Dodge"] = "DODGE", + ["Block"] = "BLOCK", + ["Block Value"] = "BLOCKVALUE", + ["Hit"] = "TOHIT", + ["Spell Hit"] = "SPELLTOHIT", + ["Blocking"] = "BLOCK", + ["Ranged Attack Power"] = "RANGEDATTACKPOWER", + ["health every 5 sec"] = "HEALTHREG", + ["Healing Spells"] = "HEAL", + ["Increases Healing"] = "HEAL", + ["Healing and Spell Damage"] = {"HEAL", "DMG"}, + ["Damage and Healing Spells"] = {"HEAL", "DMG"}, + ["Spell Damage and Healing"] = {"HEAL", "DMG"}, + ["mana every 5 sec"] = "MANAREG", + ["Mana Regen"] = "MANAREG", + ["Spell Damage"] = {"HEAL", "DMG"}, + ["Critical"] = "CRIT", + ["Critical Hit"] = "CRIT", + ["Damage"] = "DMG", + ["Health"] = "HEALTH", + ["HP"] = "HEALTH", + ["Mana"] = "MANA", + ["Armor"] = "ARMOR", + ["Reinforced Armor"] = "ARMOR", + }; + + -- next we try to match against one pattern of stage 1 and one pattern of stage 2 and concatenate the effect strings + PATTERNS_GENERIC_STAGE1 = { + { pattern = "Arcane", effect = "ARCANE" }, + { pattern = "Fire", effect = "FIRE" }, + { pattern = "Frost", effect = "FROST" }, + { pattern = "Holy", effect = "HOLY" }, + { pattern = "Shadow", effect = "SHADOW" }, + { pattern = "Nature", effect = "NATURE" } + }; + + PATTERNS_GENERIC_STAGE2 = { + { pattern = "Resist", effect = "RES" }, + { pattern = "Damage", effect = "DMG" }, + { pattern = "Effects", effect = "DMG" }, + }; + + -- finally if we got no match, we match against some special enchantment patterns. + PATTERNS_OTHER = { + { pattern = "Mana Regen (%d+) per 5 sec%.", effect = "MANAREG" }, + + { pattern = "Minor Wizard Oil", effect = {"DMG", "HEAL"}, value = 8 }, + { pattern = "Lesser Wizard Oil", effect = {"DMG", "HEAL"}, value = 16 }, + { pattern = "Wizard Oil", effect = {"DMG", "HEAL"}, value = 24 }, + { pattern = "Brilliant Wizard Oil", effect = {"DMG", "HEAL", "SPELLCRIT"}, value = {36, 36, 1} }, + + { pattern = "Minor Mana Oil", effect = "MANAREG", value = 4 }, + { pattern = "Lesser Mana Oil", effect = "MANAREG", value = 8 }, + { pattern = "Brilliant Mana Oil", effect = { "MANAREG", "HEAL"}, value = {12, 25} }, + + { pattern = "Eternium Line", effect = "FISHING", value = 5 }, + + { pattern = "Healing %+31 and 5 mana per 5 sec%.", effect = { "MANAREG", "HEAL"}, value = {5, 31} }, + { pattern = "Stamina %+16 and Armor %+100", effect = { "STA", "ARMOR"}, value = {16, 100} }, + { pattern = "Attack Power %+26 and %+1%% Critical Strike", effect = { "ATTACKPOWER", "CRIT"}, value = {26, 1} }, + { pattern = "Spell Damage %+15 and %+1%% Spell Critical Strike", effect = { "DMG", "HEAL", "SPELLCRIT"}, value = {15, 15, 1} }, + + --WoW 1.18.1 Clients: new ring enchants + { pattern = "Healing %+12", effect = { "HEAL"}, value = {12} }, + + --WoW 1.18.1 Clients: new T3 Flash of Light set bonus + { pattern = "Increases the amount healed by your Judgement of Light by 20 and Flash of Light by 60.", effect = { "IMPFLASHOFLIGHT"}, value = {60}}, + } +} end) diff --git a/libs/ace/RosterLib-2.0/RosterLib-2.0.lua b/libs/ace/RosterLib-2.0/RosterLib-2.0.lua new file mode 100644 index 0000000..ce020c0 --- /dev/null +++ b/libs/ace/RosterLib-2.0/RosterLib-2.0.lua @@ -0,0 +1,393 @@ +--[[ +Name: RosterLib-2.0 +Revision: $Revision: 16213 $ +X-ReleaseDate: $Date: 2006-08-10 08:55:29 +0200 (Thu, 10 Aug 2006) $ +Author: Maia (maia.proudmoore@gmail.com) +Website: http://wiki.wowace.com/index.php/RosterLib-2.0 +Documentation: http://wiki.wowace.com/index.php/RosterLib-2.0_API_Documentation +SVN: http://svn.wowace.com/root/trunk/RosterLib-2.0/ +Description: party/raid roster management +Dependencies: AceLibrary, AceOO-2.0, AceEvent-2.0 +]] + +local MAJOR_VERSION = "RosterLib-2.0" +local MINOR_VERSION = "$Revision: 16213 $" + +if not AceLibrary then error(vmajor .. " requires AceLibrary.") end +if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end +if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end +if not AceLibrary:HasInstance("AceEvent-2.0") then error(MAJOR_VERSION .. " requires AceEvent-2.0") end + +local updatedUnits = {} +local unknownUnits = {} +local RosterLib = {} +local roster + +------------------------------------------------ +-- activate, enable, disable +------------------------------------------------ + +local function print(text) + ChatFrame3:AddMessage(text) +end + +local function activate(self, oldLib, oldDeactivate) + RosterLib = self + if oldLib then + self.roster = oldLib.roster + oldLib:UnregisterAllEvents() + oldLib:CancelAllScheduledEvents() + end + if not self.roster then self.roster = {} end + if oldDeactivate then oldDeactivate(oldLib) end + roster = self.roster +end + + +local function external(self, major, instance) + if major == "AceEvent-2.0" then + AceEvent = instance + AceEvent:embed(self) + self:UnregisterAllEvents() + self:CancelAllScheduledEvents() + if AceEvent:IsFullyInitialized() then + self:AceEvent_FullyInitialized() + else + self:RegisterEvent("AceEvent_FullyInitialized", "AceEvent_FullyInitialized", true) + end + elseif major == "Compost-2.0" then + Compost = instance + end +end + + +function RosterLib:Enable() +-- not used anymore, but as addons still might be calling this method, we're keeping it. +end + + +function RosterLib:Disable() +-- not used anymore, but as addons still might be calling this method, we're keeping it. +end + +------------------------------------------------ +-- Internal functions +------------------------------------------------ + +function RosterLib:AceEvent_FullyInitialized() + self:TriggerEvent("RosterLib_Enabled") + self:RegisterEvent("RAID_ROSTER_UPDATE","ScanFullRoster") + self:RegisterEvent("PARTY_MEMBERS_CHANGED","ScanFullRoster") + self:RegisterEvent("UNIT_PET","ScanPet") + self:ScanFullRoster() +end + + +------------------------------------------------ +-- Unit iterator +------------------------------------------------ + +local playersent, petsent, unitcount, petcount, pmem, rmem, unit + +local function NextUnit() + -- STEP 1: pet + if not petsent then + petsent = true + if rmem == 0 then + unit = "pet" + if UnitExists(unit) then return unit end + end + end + -- STEP 2: player + if not playersent then + playersent = true + if rmem == 0 then + unit = "player" + if UnitExists(unit) then return unit end + end + end + -- STEP 3: raid units + if rmem > 0 then + -- STEP 3a: pet units + for i = petcount, rmem do + unit = string.format("raidpet%d", i) + petcount = petcount + 1 + if UnitExists(unit) then return unit end + end + -- STEP 3b: player units + for i = unitcount, rmem do + unit = string.format("raid%d", i) + unitcount = unitcount + 1 + if UnitExists(unit) then return unit end + end + -- STEP 4: party units + elseif pmem > 0 then + -- STEP 3a: pet units + for i = petcount, pmem do + unit = string.format("partypet%d", i) + petcount = petcount + 1 + if UnitExists(unit) then return unit end + end + -- STEP 3b: player units + for i = unitcount, pmem do + unit = string.format("party%d", i) + unitcount = unitcount + 1 + if UnitExists(unit) then return unit end + end + end +end + +local function UnitIterator() + playersent, petsent, unitcount, petcount, pmem, rmem = false, false, 1, 1, GetNumPartyMembers(), GetNumRaidMembers() + return NextUnit +end + +------------------------------------------------ +-- Roster code +------------------------------------------------ + + +function RosterLib:ScanFullRoster() + -- save all units we currently have, this way we can check who to remove from roster later. + local temp = Compost and Compost:Acquire() or {} + for name in pairs(roster) do + temp[name] = true + end + -- update data + for unitid in UnitIterator() do + local name = self:CreateOrUpdateUnit(unitid) + -- we successfully added a unit, so we don't need to remove it next step + if name then temp[name] = nil end + end + -- clear units we had in roster that either left the raid or are unknown for some reason. + for name in pairs(temp) do + self:RemoveUnit(name) + end + if Compost then Compost:Reclaim(temp) end + self:ProcessRoster() +end + + +function RosterLib:ScanPet(owner) + local unitid = self:GetPetFromOwner(owner) + if not unitid then + return + elseif not UnitExists(unitid) then + unknownUnits[unitid] = nil + -- find the pet in the roster we need to delete + for _,u in pairs(roster) do + if u.unitid == unitid then + self:RemoveUnit(u.name) + end + end + else + self:CreateOrUpdateUnit(unitid) + end + self:ProcessRoster() +end + + +function RosterLib:GetPetFromOwner(id) + -- convert party3 crap to raid IDs when in raid. + local owner = self:GetUnitIDFromUnit(id) + if not owner then + return + end + -- get ID + if string.find(owner,"raid") then + return string.gsub(owner, "raid", "raidpet") + elseif string.find(owner,"party") then + return string.gsub(owner, "party", "partypet") + elseif owner == "player" then + return "pet" + else + return nil + end +end + + +function RosterLib:ScanUnknownUnits() + local name + for unitid in pairs(unknownUnits) do + if UnitExists(unitid) then + name = self:CreateOrUpdateUnit(unitid) + else + unknownUnits[unitid] = nil + end + -- some pets never have a name. too bad for them, farewell! + if not name and string.find(unitid,"pet") then + unknownUnits[unitid] = nil + end + end + self:ProcessRoster() +end + + +function RosterLib:ProcessRoster() + if next(updatedUnits, nil) then + self:TriggerEvent("RosterLib_RosterChanged", updatedUnits) + for name in pairs(updatedUnits) do + local u = updatedUnits[name] + self:TriggerEvent("RosterLib_UnitChanged", u.unitid, u.name, u.class, u.subgroup, u.rank, u.oldname, u.oldunitid, u.oldclass, u.oldsubgroup, u.oldrank) + if Compost then Compost:Reclaim(updatedUnits[name]) end + updatedUnits[name] = nil + end + end + if next(unknownUnits, nil) then + self:CancelScheduledEvent("ScanUnknownUnits") + self:ScheduleEvent("ScanUnknownUnits",self.ScanUnknownUnits, 1, self) + end +end + + +function RosterLib:CreateOrUpdateUnit(unitid) + local old = nil + -- check for name + local name = UnitName(unitid) + if name and name ~= UNKNOWNOBJECT and name ~= UKNOWNBEING and not UnitIsCharmed(unitid) then + -- clear stuff + unknownUnits[unitid] = nil + -- return if a pet attempts to replace a player name + -- this doesnt fix the problem with 2 pets overwriting each other FIXME + if string.find(unitid,"pet") then + if roster[name] and roster[name].class ~= "pet" then + return name + end + end + -- save old data if existing + if roster[name] then + old = Compost and Compost:Acquire() or {} + old.name = roster[name].name + old.unitid = roster[name].unitid + old.class = roster[name].class + old.rank = roster[name].rank + old.subgroup = roster[name].subgroup + old.online = roster[name].online + end + -- object + if not roster[name] then + roster[name] = Compost and Compost:Acquire() or {} + end + -- name + roster[name].name = name + -- unitid + roster[name].unitid = unitid + -- class + if string.find(unitid,"pet") then + roster[name].class = "PET" + else + _,roster[name].class = UnitClass(unitid) + end + -- subgroup and rank + if GetNumRaidMembers() > 0 then + local _,_,num = string.find(unitid, "(%d+)") + _,roster[name].rank,roster[name].subgroup = GetRaidRosterInfo(num) + else + roster[name].subgroup = 1 + roster[name].rank = 0 + end + -- online/offline status + roster[name].online = UnitIsConnected(unitid) + + -- compare data + if not old + or roster[name].name ~= old.name + or roster[name].unitid ~= old.unitid + or roster[name].class ~= old.class + or roster[name].subgroup ~= old.subgroup + or roster[name].rank ~= old.rank + or roster[name].online ~= old.online + then + updatedUnits[name] = Compost and Compost:Acquire() or {} + updatedUnits[name].oldname = (old and old.name) or nil + updatedUnits[name].oldunitid = (old and old.unitid) or nil + updatedUnits[name].oldclass = (old and old.class) or nil + updatedUnits[name].oldsubgroup = (old and old.subgroup) or nil + updatedUnits[name].oldrank = (old and old.rank) or nil + updatedUnits[name].oldonline = (old and old.online) or nil + updatedUnits[name].name = roster[name].name + updatedUnits[name].unitid = roster[name].unitid + updatedUnits[name].class = roster[name].class + updatedUnits[name].subgroup = roster[name].subgroup + updatedUnits[name].rank = roster[name].rank + updatedUnits[name].online = roster[name].online + end + -- compost our table + if old and Compost then + Compost:Reclaim(old) + end + return name + else + unknownUnits[unitid] = true + return false + end +end + + +function RosterLib:RemoveUnit(name) + updatedUnits[name] = Compost and Compost:Acquire() or {} + updatedUnits[name].oldname = roster[name].name + updatedUnits[name].oldunitid = roster[name].unitid + updatedUnits[name].oldclass = roster[name].class + updatedUnits[name].oldsubgroup = roster[name].subgroup + updatedUnits[name].oldrank = roster[name].rank + if Compost then Compost:Reclaim(roster[name]) end + roster[name] = nil +end + + +------------------------------------------------ +-- API +------------------------------------------------ + +function RosterLib:GetUnitIDFromName(name) + if roster[name] then + return roster[name].unitid + else + return nil + end +end + + +function RosterLib:GetUnitIDFromUnit(unit) + local name = UnitName(unit) + if name and roster[name] then + return roster[name].unitid + else + return nil + end +end + + +function RosterLib:GetUnitObjectFromName(name) + if roster[name] then + return roster[name] + else + return nil + end +end + + +function RosterLib:GetUnitObjectFromUnit(unit) + local name = UnitName(unit) + if roster[name] then + return roster[name] + else + return nil + end +end + + +function RosterLib:IterateRoster(pets) + local key + return function() + repeat + key = next(roster, key) + until (roster[key] == nil or pets or roster[key].class ~= "PET") + + return roster[key] + end +end + + +AceLibrary:Register(RosterLib, MAJOR_VERSION, MINOR_VERSION, activate, nil, external) diff --git a/libs/gui/Component.lua b/libs/gui/Component.lua new file mode 100644 index 0000000..735b7f6 --- /dev/null +++ b/libs/gui/Component.lua @@ -0,0 +1,419 @@ +PTGuiComponent = {} +PTUtil.SetEnvironment(PTGuiComponent) +local _G = getfenv(0) +PTGuiComponent.__index = PTGuiComponent + +-- A static flag to verify if this is a gui component +PTGuiComponent.IsPuppeteerGui = true + +PTGuiComponent.TypeName = "BASE_COMPONENT" + +-- The primary frame associated with the set of components +PTGuiComponent.Primary = nil +-- The raw frame associated with the component +PTGuiComponent.Frame = nil +-- All gui component instances associated with the component, including itself(named "Frame") +PTGuiComponent.Components = {} + +-- Permanent frames cannot be recycled +PTGuiComponent.Permanent = false + +PTGuiComponent.FrameCount = 0 + +function PTGuiComponent:Extend(typeName) + local obj = {} + setmetatable(obj, self) + obj.TypeName = typeName + obj.super = self + obj.__index = obj + return obj +end + +-- Called when a component is acquired +function PTGuiComponent:OnAcquire() + self:Show() +end + +-- Responsible for cleaning up a component, putting it in a default state +function PTGuiComponent:OnDispose() + self:Hide() + self:ClearAllPoints() + self:SetSize(0, 0) + self:SetParent(nil) + if self.HasScript and self:HasScript("OnClick") then + self:OnClick(nil) + end + + if self.DisposeHandler then + self:DisposeHandler() + self.DisposeHandler = nil + end +end + +function PTGuiComponent:Dispose() + if not self:IsPermanent() then -- Permanent components aren't recycled, but can try to hide themselves + PTGuiLib.Recycle(self) + end + + self:OnDispose() +end + +-- Set a special handler that is called when this component is being disposed. +-- Should be used for cleaning up things components don't normally clean up. +function PTGuiComponent:SetDisposeHandler(handler) + self.DisposeHandler = handler + return self +end + +function PTGuiComponent:SetPermanent() + self.Permanent = true + return self +end + +function PTGuiComponent:IsPermanent() + return self.Permanent +end + +function PTGuiComponent:Type() + return self.TypeName +end + +function PTGuiComponent:GenerateName() + self.FrameCount = self.FrameCount + 1 + return "PTGui_"..self:Type()..self.FrameCount +end + +function PTGuiComponent:SetHandle(frame) + self.Frame = frame + self:AddComponent("Frame", self) + frame.GuiInstance = self + self:SetPrimary() + return self +end + +function PTGuiComponent:GetHandle() + return self.Frame +end + +function PTGuiComponent:IsInstance() + return self.Frame ~= nil +end + +function PTGuiComponent:IsPrimary() + return self.Primary == self.Frame +end + +function PTGuiComponent:GetPrimary() + return self.Primary +end + +-- Sets this component as the primary for all associated components +function PTGuiComponent:SetPrimary() + for _, component in pairs(self.Components) do + component.Primary = self + end +end + +function PTGuiComponent:Import(returnSelf, ...) + self:ImportComponent(nil, returnSelf, arg) +end + +function PTGuiComponent:ImportComponent(frameName, returnSelf, ...) + frameName = frameName or "Frame" + for _, import in ipairs(arg) do + if type(import) == "table" then + for _, import in ipairs(import) do + self:DoImport(frameName, returnSelf, import) + end + else + self:DoImport(frameName, returnSelf, import) + end + end +end + +-- Instance imports are faster and static imports consume less memory. +-- Instance importing must be done after the component is added. +function PTGuiComponent:DoImport(frameName, returnSelf, import) + if self:IsInstance() then -- Prefer an instance import + local frame = self:GetComponent(frameName):GetHandle() + local func = frame[import] + if returnSelf then + self[import] = function(self, a1, a2, a3, a4, a5) + func(frame, a1, a2, a3, a4, a5) + return self + end + else + self[import] = function(self, a1, a2, a3, a4, a5) + return func(frame, a1, a2, a3, a4, a5) + end + end + elseif frameName == "Frame" then -- Optimized static import for the handle + if returnSelf then + self[import] = function(self, a1, a2, a3, a4, a5) + local frame = self.Frame + frame[import](frame, a1, a2, a3, a4, a5) + return self + end + else + self[import] = function(self, a1, a2, a3, a4, a5) + local frame = self.Frame + return frame[import](frame, a1, a2, a3, a4, a5) + end + end + else -- Static import for other components + if returnSelf then + self[import] = function(self, a1, a2, a3, a4, a5) + local frame = self.Components[frameName]:GetHandle() + frame[import](frame, a1, a2, a3, a4, a5) + return self + end + else + self[import] = function(self, a1, a2, a3, a4, a5) + local frame = self.Components[frameName]:GetHandle() + return frame[import](frame, a1, a2, a3, a4, a5) + end + end + end +end + +function PTGuiComponent:CreateGetter(componentName, funcName) + self[funcName or ("Get"..componentName)] = function(self) + return self:GetComponent(componentName) + end +end + +function PTGuiComponent:GetContainer() + return self.Frame +end + +function PTGuiComponent:GetAnchor() + return self.Frame +end + +function PTGuiComponent:AddComponent(name, component) + if self.Components == PTGuiComponent.Components then + self.Components = {} + end + self.Components[name] = component +end + +function PTGuiComponent:GetComponent(name) + return self.Components[name] +end + +function PTGuiComponent:GetComponents() + return self.Components +end + +function PTGuiComponent:SetParent(obj) + if obj and obj.IsPuppeteerGui then + obj = obj:GetContainer() + end + self:GetHandle():SetParent(obj) + PTUtil.FixFrameLevels(self:GetHandle()) + return self +end + +function PTGuiComponent:SetSize(width, height) + self:SetWidth(width) + self:SetHeight(height) + return self +end + +function PTGuiComponent:SetScript(scriptName, script, noSelf) + self:GetHandle():SetScript(scriptName, script and (noSelf and script or function() script(self) end) or nil) + return self +end + +function PTGuiComponent:HookScript(scriptName, script) + local prevScript = self:GetScript(scriptName) + self:GetHandle():SetScript(scriptName, function() + if prevScript then + prevScript(self) + end + script(self) + end) + return self +end + +function PTGuiComponent:OnClick(script, noSelf) + return self:SetScript("OnClick", script, noSelf) +end + +function PTGuiComponent:SetPoint(point, relativeFrame, relativePoint, xOffset, yOffset) + if type(relativeFrame) == "table" and relativeFrame.IsPuppeteerGui then + relativeFrame = relativeFrame:GetAnchor() + end + self:GetHandle():SetPoint(point, relativeFrame, relativePoint, xOffset, yOffset) + return self +end + +PTGuiComponent.BACKGROUND_TOOLTIP = 1 +PTGuiComponent.BACKGROUND_DIALOG = 2 +function PTGuiComponent:SetSimpleBackground(type, diff) + type = type or PTGuiComponent.BACKGROUND_TOOLTIP + local background + if type == PTGuiComponent.BACKGROUND_TOOLTIP then + background = { + frameBackdrop = { + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeSize = 11, + tile = true + }, + frameBackdropColor = {0.09, 0.09, 0.19}, + frameBorderColor = nil, + borderBackdrop = { + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 11, + tileSize = 11, + tile = true + }, + borderBackdropColor = nil, + borderBorderColor = {0.8, 0.8, 0.8}, + borderPadding = 2 + } + elseif type == PTGuiComponent.BACKGROUND_DIALOG then + background = { + frameBackdrop = { + bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", + edgeSize = 32, + tile = true, + tileSize = 32 + }, + borderBackdrop = { + edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", + edgeSize = 32, + tile = true, + tileSize = 32 + }, + borderPadding = 11 + } + end + if diff then + for k, v in pairs(diff) do + background[k] = v + end + end + return self:SetBackground(background) +end + +function PTGuiComponent:SetBackground(params) + local frame = self:GetHandle() + + frame:SetBackdrop(nil) + frame:SetBackdropColor() + frame:SetBackdropBorderColor() + if frame.guiBorder then + frame.guiBorder:Hide() + frame.guiBorder:SetBackdrop(nil) + frame.guiBorder:SetBackdropColor() + frame.guiBorder:SetBackdropBorderColor() + end + + if not params then + return self + end + + if params.frameBackdrop then + frame:SetBackdrop(params.frameBackdrop) + if params.frameBackdropColor then + frame:SetBackdropColor(unpack(params.frameBackdropColor)) + end + if params.frameBorderColor then + frame:SetBackdropColor(unpack(params.frameBorderColor)) + end + end + + if params.borderBackdrop then + local border = frame.guiBorder or CreateFrame("Frame", nil, frame) + frame.guiBorder = border + frame.guiBorder:Show() + local padding = params.borderPadding or 0 + border:SetPoint("TOPLEFT", frame, "TOPLEFT", -padding, padding) + border:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", padding, -padding) + border:SetBackdrop(params.borderBackdrop) + border:SetFrameLevel(frame:GetFrameLevel()) + if params.borderBackdropColor then + border:SetBackdropColor(unpack(params.borderBackdropColor)) + end + if params.borderBorderColor then + border:SetBackdropBorderColor(unpack(params.borderBorderColor)) + end + end + return self +end + +-- TODO: Handle cleanup of tooltips for disposed components +local infoTooltip = CreateFrame("GameTooltip", "PTGuiInfoTooltip", UIParent, "GameTooltipTemplate") + +local singleTextArray = {} +local function ShowTooltip(attachTo, texts) + infoTooltip:SetOwner(attachTo, "ANCHOR_RIGHT") + infoTooltip:SetPoint("RIGHT", attachTo, "LEFT", 0, 0) + + if type(texts) == "string" then + singleTextArray[1] = texts + texts = singleTextArray + end + + for i, text in ipairs(texts) do + if i ~= 1 then + text = PTUtil.Colorize(text, 1.0, 0.88, 0.3) + end + infoTooltip:AddLine(text) + _G["PTGuiInfoTooltipTextLeft"..i]:SetFont("Fonts\\FRIZQT__.TTF", i == 1 and 14 or 12, "OUTLINE") + end + + infoTooltip:Show() +end + +function PTGuiComponent:ApplyTooltip(...) + self:ApplyTooltipToAll(arg) + return self +end + +function PTGuiComponent:ApplyTooltipTo(component, texts) + local primary = component:GetPrimary():GetHandle() + local frame = component:GetHandle() + frame:EnableMouse(true) + frame:SetScript("OnEnter", function() + ShowTooltip(primary, texts) + end) + frame:SetScript("OnLeave", function() + infoTooltip:Hide() + end) +end + +local function flattenTexts(element, t) + if type(element) == "table" then + for _, e in ipairs(element) do + flattenTexts(e, t) + end + elseif type(element) == "string" then + table.insert(t, element) + end + return t +end + +function PTGuiComponent:ApplyTooltipToAll(texts) + local primary = self:GetPrimary():GetHandle() + local texts = flattenTexts(texts, {}) + local onEnter = function() + ShowTooltip(primary, texts) + end + local onLeave = function() + infoTooltip:Hide() + end + for _, component in pairs(self:GetComponents()) do + local frame = component:GetHandle() + if frame.EnableMouse and frame.HasScript and frame:HasScript("OnEnter") then + frame:EnableMouse(true) + frame:SetScript("OnEnter", onEnter) + frame:SetScript("OnLeave", onLeave) + end + end +end + + +PTGuiComponent:Import(true, "ClearAllPoints", "SetAllPoints", "SetWidth", "SetHeight", "Show", "Hide", "SetFrameLevel") +PTGuiComponent:Import(false, "GetWidth", "GetHeight", "GetName", "HasScript", "GetScript", "GetParent", "GetFrameLevel", "IsVisible", "IsShown") \ No newline at end of file diff --git a/libs/gui/PTGuiLib.lua b/libs/gui/PTGuiLib.lua new file mode 100644 index 0000000..7e1f35d --- /dev/null +++ b/libs/gui/PTGuiLib.lua @@ -0,0 +1,51 @@ +-- A GUI library that wraps frames, making complex functionality easier, and facilitates frame aquisition and disposal. +-- It is currently only intended for use in Puppeteer because it's specific to what I want and I'm too lazy to implement +-- more things devs would want in a GUI library. +PTGuiLib = {} +PTUtil.SetEnvironment(PTGuiLib) +local getn = table.getn + +ComponentRegistry = {} +ComponentPool = {} + +local function _Get(componentType) + local pool = ComponentPool[componentType] + local component + if getn(pool) > 0 then + component = table.remove(pool, getn(pool)) + else + component = ComponentRegistry[componentType]:New() + end + component:OnAcquire() + return component +end + +function Get(componentType, parent) + local component = _Get(componentType) + if parent then + component:SetParent(parent) + end + return component +end + +function GetText(parent, text, fontSize) + local component = Get("text", parent) + component:SetText(text) + if fontSize then + component:SetFontSize(fontSize) + end + return component +end + +function Recycle(component) + table.insert(ComponentPool[component:Type()], component) +end + +function RegisterComponent(class) + ComponentRegistry[class:Type()] = class + ComponentPool[class:Type()] = {} +end + +function GetClass(componentType) + return ComponentRegistry[componentType] +end \ No newline at end of file diff --git a/libs/gui/abstract/TabComponent.lua b/libs/gui/abstract/TabComponent.lua new file mode 100644 index 0000000..15fd86d --- /dev/null +++ b/libs/gui/abstract/TabComponent.lua @@ -0,0 +1,133 @@ +PTGuiTabComponent = PTGuiComponent:Extend("tab_component") + +PTGuiTabComponent.TabPosition = "TOP" -- "TOP", "BOTTOM" +PTGuiTabComponent.TabOffsetX = 0 +PTGuiTabComponent.TabOffsetY = 0 + +function PTGuiTabComponent:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + obj.Tabs = {} + obj:RegisterSizeListener() + return obj +end + +function PTGuiTabComponent:RegisterSizeListener() + self:SetScript("OnSizeChanged", function() + self:PositionTabs() + end, true) +end + +function PTGuiTabComponent:CreateTab(name, useScrollFrame) + local tabIndex = self:GetTabCount() + 1 + local tabLabel = "Tab"..tabIndex + local container + local scrollFrame + if not useScrollFrame then + container = PTGuiContainer:New("$parent"..tabLabel.."Container", self:GetHandle()) + container:SetAllPoints(self:GetHandle()) + container:Hide() + else + scrollFrame = PTGuiLib.Get("scroll_frame", self:GetHandle()) + scrollFrame:SetAllPoints(self:GetHandle()) + scrollFrame:Hide() + container = scrollFrame:GetComponent("content") + end + self:AddComponent(tabLabel, container) + self:SetPrimary() + local tab = CreateFrame("Button", "$parent"..tabLabel, self:GetHandle(), self.TabPosition == "TOP" and "TabButtonTemplate" or "CharacterFrameTabButtonTemplate") + if self.TabPosition == "TOP" then + tab:SetHeight(22) + end + tab:SetText(name) + tab:RegisterForClicks("LeftButtonUp") + + tab:SetScript("OnClick", function() + self:SetSelectedTab(tabIndex) + self:PlayTabSound() + end) + local tabInfo = {tab = tab, container = container, scrollFrame = scrollFrame, root = scrollFrame or container, name = name} + table.insert(self.Tabs, tabInfo) + self:UpdateTabCount() + PanelTemplates_TabResize(0, tab) + self:PositionTabs() + return container, scrollFrame +end + +function PTGuiTabComponent:GetTabCount() + return table.getn(self.Tabs) +end + +function PTGuiTabComponent:GetTab(indexOrName) + if type(indexOrName) == "string" then + for _, tab in ipairs(self.Tabs) do + if tab.name == indexOrName then + return tab + end + end + end + return self.Tabs[indexOrName] +end + +-- Returns the GUI instance of the tab container +function PTGuiTabComponent:GetTabContainer(indexOrName) + return self:GetTab(indexOrName).container +end + +-- Returns the Blizzard frame instance of the tab container +function PTGuiTabComponent:GetTabFrame(indexOrName) + return self:GetTab(indexOrName).container:GetHandle() +end + +function PTGuiTabComponent:GetSelectedTab() + return self:GetTabContainer(PanelTemplates_GetSelectedTab(self:GetHandle())) +end + +function PTGuiTabComponent:UpdateTabCount() + PanelTemplates_SetNumTabs(self:GetHandle(), self:GetTabCount()) + self:SetSelectedTab(1) +end + +function PTGuiTabComponent:SetSelectedTab(index) + PanelTemplates_SetTab(self:GetHandle(), index) + for i, tab in ipairs(self.Tabs) do + if i == index then + tab.root:Show() + else + tab.root:Hide() + end + end +end + +function PTGuiTabComponent:PlayTabSound() + PlaySoundFile("Sound\\Interface\\uCharacterSheetTab.wav") +end + +function PTGuiTabComponent:PositionTabs() + local space = self:GetWidth() - self.TabOffsetX + local usedSpace = 0 + local row = 0 + for i, tab in ipairs(self.Tabs) do + tab = tab.tab + tab:ClearAllPoints() + usedSpace = usedSpace + tab:GetWidth() + if usedSpace < space and row > 0 then + tab:SetPoint("LEFT", self:GetTab(i - 1).tab, "RIGHT") + else + if self.TabPosition == "TOP" then + tab:SetPoint("TOPLEFT", self:GetHandle(), "TOPLEFT", self.TabOffsetX, self.TabOffsetY + (tab:GetHeight() * (row + 1))) + else + tab:SetPoint("TOPLEFT", self:GetHandle(), "BOTTOMLEFT", self.TabOffsetX, self.TabOffsetY - (tab:GetHeight() * row)) + end + row = row + 1 + usedSpace = tab:GetWidth() + end + end +end + +function PTGuiTabComponent:SetTabPosition(pos, xOffset, yOffset) + self.TabPosition = pos + self.TabOffsetX = xOffset + self.TabOffsetY = yOffset +end \ No newline at end of file diff --git a/libs/gui/component/Button.lua b/libs/gui/component/Button.lua new file mode 100644 index 0000000..2403734 --- /dev/null +++ b/libs/gui/component/Button.lua @@ -0,0 +1,175 @@ +-- Buttons created by this are modified to not stretch at the corners, making for cleaner looking buttons. +PTGuiButton = PTGuiComponent:Extend("button") +PTGuiButton:Import(true, "SetText", "Enable", "Disable") +PTGuiButton:Import(false, "GetText", "IsEnabled") +local _G = getfenv(0) + +function PTGuiButton:New() + local obj = setmetatable({}, self) + obj:SetHandle(CreateFrame("Button", self:GenerateName(), nil, "UIPanelButtonTemplate")) + obj:SetupTextures() + return obj +end + +function PTGuiButton:OnAcquire() + self.super.OnAcquire(self) + self:SetClickSound(_G.PlaySound, "GAMESPELLBUTTONMOUSEDOWN") +end + +function PTGuiButton:OnDispose() + self.super.OnDispose(self) + self:SetText("") + self:SetEnabled(true) + self:SetUseDefaultTextures(false) + -- TODO: Restore default textures to default state +end + +function PTGuiButton:SetupTextures() + local button = self:GetHandle() + + self.NormalTexture = button:GetNormalTexture() + self.NormalTexture:SetTexture(nil) + self.PushedTexture = button:GetPushedTexture() + self.PushedTexture:SetTexture(nil) + self.DisabledTexture = button:GetDisabledTexture() + self.DisabledTexture:SetTexture(nil) + self.HighlightTexture = button:GetHighlightTexture() + + local topLeft = button:CreateTexture(button:GetName().."TopLeft", "BACKGROUND") + topLeft:SetWidth(7) + topLeft:SetHeight(7) + topLeft:SetPoint("TOPLEFT", button, "TOPLEFT") + topLeft:SetTexCoord(1 / 128, 8 / 128, 1 / 32, 8 / 32) + + local topRight = button:CreateTexture(button:GetName().."TopRight", "BACKGROUND") + topRight:SetWidth(7) + topRight:SetHeight(7) + topRight:SetPoint("TOPRIGHT", button, "TOPRIGHT") + topRight:SetTexCoord(72 / 128, 79 / 128, 1 / 32, 8 / 32) + + local topMid = button:CreateTexture(button:GetName().."TopMid", "BACKGROUND") + topMid:SetPoint("TOPLEFT", topLeft, "TOPRIGHT") + topMid:SetPoint("BOTTOMRIGHT", topRight, "BOTTOMLEFT") + topMid:SetTexCoord(9 / 128, 70 / 128, 1 / 32, 8 / 32) + + local botLeft = button:CreateTexture(button:GetName().."BotLeft", "BACKGROUND") + botLeft:SetWidth(7) + botLeft:SetHeight(7) + botLeft:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT") + botLeft:SetTexCoord(1 / 128, 8 / 128, 15 / 32, 22 / 32) + + local botRight = button:CreateTexture(button:GetName().."BotRight", "BACKGROUND") + botRight:SetWidth(7) + botRight:SetHeight(7) + botRight:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT") + botRight:SetTexCoord(72 / 128, 79 / 128, 15 / 32, 22 / 32) + + local botMid = button:CreateTexture(button:GetName().."BotMid", "BACKGROUND") + botMid:SetPoint("TOPLEFT", botLeft, "TOPRIGHT") + botMid:SetPoint("BOTTOMRIGHT", botRight, "BOTTOMLEFT") + botMid:SetTexCoord(9 / 128, 70 / 128, 15 / 32, 22 / 32) + + local midLeft = button:CreateTexture(button:GetName().."MidLeft", "BACKGROUND") + midLeft:SetPoint("TOPLEFT", topLeft, "BOTTOMLEFT") + midLeft:SetPoint("BOTTOMRIGHT", botLeft, "TOPRIGHT") + midLeft:SetTexCoord(1 / 128, 8 / 128, 9 / 32, 13 / 32) + + local midRight = button:CreateTexture(button:GetName().."MidRight", "BACKGROUND") + midRight:SetPoint("TOPLEFT", topRight, "BOTTOMLEFT") + midRight:SetPoint("BOTTOMRIGHT", botRight, "TOPRIGHT") + midRight:SetTexCoord(72 / 128, 79 / 128, 9 / 32, 13 / 32) + + local center = button:CreateTexture(button:GetName().."Center", "BACKGROUND") + center:SetPoint("TOPLEFT", topLeft, "BOTTOMRIGHT") + center:SetPoint("BOTTOMRIGHT", botRight, "TOPLEFT") + center:SetTexCoord(9 / 128, 70 / 128, 9 / 32, 13 / 32) + + button.textures = {topLeft, topMid, topRight, botLeft, botMid, botRight, midLeft, center, midRight} + + button:SetScript("OnMouseDown", function() + if button:IsEnabled() == 1 then + self:SetTexture("PUSHED") + end + end) + + button:SetScript("OnMouseUp", function() + if button:IsEnabled() == 1 then + self:SetTexture("NORMAL") + end + end) + + local realEnable = button.Enable + button.Enable = function(buttonSelf) + realEnable(buttonSelf) + self:SetTexture("NORMAL") + end + local realDisable = button.Disable + button.Disable = function(buttonSelf) + realDisable(buttonSelf) + self:SetTexture("DISABLED") + end + + self:SetTexture("NORMAL") +end + +local buttonTextures = {["NORMAL"] = "Interface\\Buttons\\UI-Panel-Button-Up", + ["PUSHED"] = "Interface\\Buttons\\UI-Panel-Button-Down", + ["DISABLED"] = "Interface\\Buttons\\UI-Panel-Button-Disabled" +} +function PTGuiButton:SetTexture(type) -- "NORMAL", "PUSHED", "DISABLED" + local texLoc = buttonTextures[type] + for _, tex in ipairs(self:GetHandle().textures) do + tex:SetTexture(texLoc) + end +end + +function PTGuiButton:SetUseDefaultTextures(useDefault) + if not useDefault and self.UsingDefaultTextures then + local button = self:GetHandle() + for _, tex in ipairs(button.textures) do + tex:Show() + end + self.NormalTexture:SetTexture(nil) + self.PushedTexture:SetTexture(nil) + self.DisabledTexture:SetTexture(nil) + self.HighlightTexture:SetTexture("Interface\\Buttons\\UI-Panel-Button-Highlight") + self.UsingDefaultTextures = nil + elseif useDefault then + local button = self:GetHandle() + for _, tex in ipairs(button.textures) do + tex:Hide() + end + self.UsingDefaultTextures = true + end + return self +end + +function PTGuiButton:SetEnabled(enabled) + (enabled and self.Enable or self.Disable)(self) + return self +end + +function PTGuiButton:SetScript(scriptName, script, noSelf) + if scriptName ~= "OnClick" then + return self.super.SetScript(self, scriptName, script, noSelf) + end + self:GetHandle():SetScript(scriptName, function() + self:PlayClickSound() + if script then + script(self) + end + end) + return self +end + +function PTGuiButton:PlayClickSound() + self.ClickSoundFunc(self.ClickSoundName) +end + +function PTGuiButton:SetClickSound(soundFunc, soundName) + self.ClickSoundFunc = soundFunc + self.ClickSoundName = soundName + return self +end + +PTGuiLib.RegisterComponent(PTGuiButton) \ No newline at end of file diff --git a/libs/gui/component/Checkbox.lua b/libs/gui/component/Checkbox.lua new file mode 100644 index 0000000..f9d1df0 --- /dev/null +++ b/libs/gui/component/Checkbox.lua @@ -0,0 +1,43 @@ +PTGuiCheckbox = PTGuiComponent:Extend("checkbox") +PTGuiCheckbox:Import(true, "SetChecked", "Enable", "Disable") +PTGuiCheckbox:Import(false, "GetChecked") + +-- Texture coords that makes the checkbox larger and more centered +local minX, maxX, minY, maxY = 2 / 32, 29 / 32, 2 / 32, 28 / 32 + +function PTGuiCheckbox:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("CheckButton", self:GenerateName(), nil, "UICheckButtonTemplate") + obj:SetHandle(frame) + frame:GetNormalTexture():SetTexCoord(minX, maxX, minY, maxY) + frame:GetHighlightTexture():SetTexCoord(minX, maxX, minY, maxY) + frame:GetPushedTexture():SetTexCoord(minX, maxX, minY, maxY) + frame:GetCheckedTexture():SetTexCoord(minX, maxX, minY, maxY) + frame:GetDisabledCheckedTexture():SetTexCoord(minX, maxX, minY, maxY) + return obj +end + +function PTGuiCheckbox:OnDispose() + self.super.OnDispose(self) + + self:SetChecked(false) +end + +function PTGuiCheckbox:SetScript(scriptName, script, noSelf) + if scriptName ~= "OnClick" then + return self.super.SetScript(self, scriptName, script, noSelf) + end + self:GetHandle():SetScript(scriptName, function() + if self:GetChecked() then + PlaySound("igMainMenuOptionCheckBoxOn") + else + PlaySound("igMainMenuOptionCheckBoxOff") + end + if script then + script(self) + end + end) + return self +end + +PTGuiLib.RegisterComponent(PTGuiCheckbox) \ No newline at end of file diff --git a/libs/gui/component/ColorSelect.lua b/libs/gui/component/ColorSelect.lua new file mode 100644 index 0000000..45e9c8f --- /dev/null +++ b/libs/gui/component/ColorSelect.lua @@ -0,0 +1,89 @@ +PTGuiColorSelect = PTGuiComponent:Extend("color_select") + +function PTGuiColorSelect:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + obj:SetHandle(frame) + + local texture = frame:CreateTexture(nil, "BACKGROUND") + texture:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1) + texture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1) + obj.ColorTexture = texture + + obj.Color = {1, 1, 1} + + texture:SetTexture(obj.Color[1], obj.Color[2], obj.Color[3]) + + obj:SetBackground({ + borderBackdrop = { + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + edgeSize = 11, + tileSize = 11, + tile = true + }, + borderPadding = 2 + }) + + frame:EnableMouse(true) + frame:SetScript("OnMouseUp", function() + ColorPickerFrame.func = function() + obj:SetColorRGB(ColorPickerFrame:GetColorRGB()) + if not obj:IsVisible() then + return + end + if obj.ColorSelectHandler then + obj.ColorSelectHandler(obj) + end + end + local color = obj:GetColor() + ColorPickerFrame.hasOpacity = false + ColorPickerFrame.opacityFunc = nil + ColorPickerFrame.opacity = nil + ColorPickerFrame:SetColorRGB(color[1], color[2], color[3]); + ColorPickerFrame.previousValues = {r = color[1], g = color[2], b = color[3]}; + ColorPickerFrame.cancelFunc = function() + local prevColor = ColorPickerFrame.previousValues + obj:SetColorRGB(prevColor.r, prevColor.g, prevColor.b) + ColorPickerFrame.func = nil + ColorPickerFrame.cancelFunc = nil + end + ShowUIPanel(ColorPickerFrame) + end) + + return obj +end + +function PTGuiColorSelect:OnDispose() + self.super.OnDispose(self) + local color = self.Color + color[1], color[2], color[3], color[4] = 1, 1, 1, 1 +end + +function PTGuiColorSelect:GetColor() + local color = self.Color + return {color[1], color[2], color[3], color[4]} +end + +function PTGuiColorSelect:GetColorRGB() + return self.Color[1], self.Color[2], self.Color[3], self.Color[4] +end + +function PTGuiColorSelect:SetColorRGB(r, g, b, a) + local color = self.Color + color[1], color[2], color[3], color[4] = r, g, b, a + self.ColorTexture:SetTexture(r, g, b, a or 1) + return self +end + +function PTGuiColorSelect:SetColor(rgb) + self.Color[1], self.Color[2], self.Color[3], self.Color[4] = rgb[1], rgb[2], rgb[3], rgb[4] + self.ColorTexture:SetTexture(self.Color[1], self.Color[2], self.Color[3], self.Color[4] or 1) + return self +end + +function PTGuiColorSelect:SetColorSelectHandler(handler) + self.ColorSelectHandler = handler + return self +end + +PTGuiLib.RegisterComponent(PTGuiColorSelect) \ No newline at end of file diff --git a/libs/gui/component/Container.lua b/libs/gui/component/Container.lua new file mode 100644 index 0000000..4a64039 --- /dev/null +++ b/libs/gui/component/Container.lua @@ -0,0 +1,10 @@ +PTGuiContainer = PTGuiComponent:Extend("container") + +function PTGuiContainer:New(name, parent) + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", name or self:GenerateName(), parent) + obj:SetHandle(frame) + return obj +end + +PTGuiLib.RegisterComponent(PTGuiContainer) \ No newline at end of file diff --git a/libs/gui/component/Dropdown.lua b/libs/gui/component/Dropdown.lua new file mode 100644 index 0000000..f944aaa --- /dev/null +++ b/libs/gui/component/Dropdown.lua @@ -0,0 +1,342 @@ +-- This can either be used as a normal dropdown or a context menu. If the parent is UIParent, it will be a context menu. +-- Otherwise, it is a dropdown. This can be changed during runtime simply by calling SetParent through the GUI component. +-- The positioning of the dropdown's components are modified to behave like you'd expect. SetWidth actually sets the width. +-- SetHeight does nothing, and will always be 25 px. + +-- Initialization is handled by this component. Use SetOptions to set the options table. +-- === New Fields === +-- initFunc(self, gui) - Runs on level initialization before the button is added +-- dropdownText - The text the dropdown will be set to when this option is clicked, runs before "func" +-- closeOnClick - If true, closes all menus regardless of level, runs after "func" +-- children - Array for a next level menu; If present, "hasArrow" is automatically set to true +-- parent [Automatically Assigned] - Reference to the previous level element, or nil if root +-- siblings [Automatically Assigned] - Reference to the array of elements in the level +-- gui [Automatically Assigned] - Reference to the GUI instance +-- === Adjusted Existing Fields === +-- func(self, gui) - Always passes self as the first argument and the GUI instance as the second argument +-- arg1, arg2 - Do not use, now internally used to pass self and gui to "func" +-- value - Do not use, now internally used to reference self + +PTGuiDropdown = PTGuiComponent:Extend("dropdown") +local _G = getfenv(0) +local compost = PTPool + +PTGuiDropdown.Options = nil + +function PTGuiDropdown:New() + local obj = setmetatable({}, self) + local dropdown = CreateFrame("Frame", self:GenerateName(), nil, "UIDropDownMenuTemplate") + obj:SetHandle(dropdown) + -- Make SetWidth actually set the width, like one would hope + local dropdownSetWidth = dropdown.SetWidth + dropdownSetWidth(dropdown, 0) -- Allows points to size the dropdown + dropdown.SetWidth = function(self, width) + _G[dropdown:GetName().."Middle"]:SetWidth(width - 15) + dropdownSetWidth(self, width) + _G[dropdown:GetName().."Text"]:SetWidth(width - 25) + dropdown.noResize = 1 + end + -- The dropdown doesn't stretch vertically, so the height is always 25 + local dropdownSetHeight = dropdown.SetHeight + dropdown.SetHeight = function(self, height) + dropdownSetHeight(self, 25) + end + obj:SetShowShadow(true) -- Need to do this on creation to fix the visual position + -- Make clicking anywhere on the dropdown open the menu + dropdown:EnableMouse(true) + dropdown:SetScript("OnMouseUp", function() + if MouseIsOver(this) then + PTUtil.CallWithThis(_G[this:GetName().."Button"], _G[this:GetName().."Button"]:GetScript("OnClick")) + end + end) + dropdown:SetScript("OnSizeChanged", function() + local width = this:GetWidth() + _G[this:GetName().."Middle"]:SetWidth(width - 15) + dropdownSetWidth(this, width) + _G[this:GetName().."Text"]:SetWidth(width - 25) + end) + return obj +end + +local partProps = { + Default = { + Left = { + Width = 25, Height = 64, + Coords = {0, 0.1953125, 0, 1}, + Point = {"TOPLEFT", "$parent", "TOPLEFT", -18, 18} + }, + Middle = { + Width = 115, Height = 64, + Coords = {0.1953125, 0.8046875, 0, 1}, + Point = {"LEFT", "$parentLeft", "RIGHT"} + }, + Right = { + Width = 25, Height = 64, + Coords = {0.8046875, 1, 0, 1}, + Point = {"LEFT", "$parentMiddle", "RIGHT"} + }, + Text = { + Point = {"RIGHT", "$parentRight", -43, 2} + }, + Button = { + Point = {"TOPRIGHT", "$parentRight", -16, -18} + } + }, + Shadowless = { + Left = { + Width = 6, Height = 24, + Coords = {18 / 128, (18 + 6) / 128, 19 / 64, (19 + 24) / 64}, + Point = {"TOPLEFT", 0, -1} + }, + Middle = { + Width = 115, Height = 24, + Coords = {0.1953125, 0.8046875, 19 / 64, (19 + 24) / 64}, + Point = {"LEFT", "$parentLeft", "RIGHT", 0, 0} + }, + Right = { + Width = 9, Height = 24, + Coords = {102 / 128, (102 + 9) / 128, 19 / 64, (19 + 24) / 64}, + Point = {"LEFT", "$parentMiddle", "RIGHT", 0, 0} + }, + Text = { + Point = {"RIGHT", "$parentRight", -26, 1} + }, + Button = { + Point = {"TOPRIGHT", "$parentRight", 1, 1} + } + } +} +function PTGuiDropdown:SetShowShadow(showShadow) + local frameName = self:GetHandle():GetName() + for partName, props in pairs(showShadow and partProps.Default or partProps.Shadowless) do + local part = _G[frameName..partName] + if props.Coords then + part:SetTexCoord(unpack(props.Coords)) + end + if props.Width then + part:SetWidth(props.Width) + part:SetHeight(props.Height) + end + part:ClearAllPoints() + part:SetPoint(unpack(props.Point)) + end + self:SetWidth(self:GetWidth()) + UIDropDownMenu_SetAnchor(showShadow and 18 or 0, showShadow and 17 or 0, self:GetHandle(), "TOPLEFT", + _G[frameName.."Left"], "BOTTOMLEFT") + return self +end + +local defaultTextures = { + ButtonNormalTexture = "Interface\\ChatFrame\\UI-ChatIcon-ScrollDown-Up", + ButtonDisabledTexture = "Interface\\ChatFrame\\UI-ChatIcon-ScrollDown-Disabled", + ButtonPushedTexture = "Interface\\ChatFrame\\UI-ChatIcon-ScrollDown-Down", + ButtonHighlightTexture = "Interface\\Buttons\\UI-Common-MouseHilight" +} +function PTGuiDropdown:SetParent(obj) + local menuType = obj == UIParent and "MENU" or nil + if menuType ~= "MENU" and self:GetHandle().displayMode == "MENU" then + -- Show the dropdown if we're switching from a context menu to a dropdown + -- BROKEN: When put back as a context menu, the button shows forever??? + local frameName = self:GetHandle():GetName() + _G[frameName.."Left"]:Show() + _G[frameName.."Middle"]:Show() + _G[frameName.."Right"]:Show() + for texName, texPath in pairs(defaultTextures) do + _G[frameName..texName]:SetTexture(texPath) + end + self:SetShowShadow(true) + self:GetHandle().displayMode = nil + end + UIDropDownMenu_Initialize(self:GetHandle(), function(level) + self:Initialize(level) + end, menuType) + self.super.SetParent(self, obj) +end + +function PTGuiDropdown:OnDispose() + self:SetToggleState(false) + self:SetShowShadow(true) + self.super.OnDispose(self) + + self.Options = nil + if self.DynamicOptions then + compost:Reclaim(self.DynamicOptions, 1) + self.DynamicOptionsArgs = nil + end + self.TextUpdater = nil + self:SetText("") +end + +local dynamicOptions +local function addDynamicOption(k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) + local option = compost:AcquireHash(k1,v1,k2,v2,k3,v3,k4,v4,k5,v5,k6,v6,k7,v7,k8,v8,k9,v9,k10,v10) + table.insert(dynamicOptions, option) +end + +function PTGuiDropdown:Initialize(level) + if not self.Options then + return + end + level = level or 1 + local options = self.Options + if type(options) == "function" then + if self.DynamicOptions then + compost:Reclaim(self.DynamicOptions, 1) + end + self.DynamicOptions = compost:GetTable() + dynamicOptions = self.DynamicOptions + options(addDynamicOption, level, self.DynamicOptionsArgs) + options = dynamicOptions + self:BakeOptions(options) + end + local value = UIDROPDOWNMENU_MENU_VALUE + options = level == 1 and options or value.children + for _, option in ipairs(options) do + if option.initFunc then + option:initFunc(self) + end + UIDropDownMenu_AddButton(option, level) + end +end + +-- Similar to normal dropdown initialization. Really only should be used for single level options. +-- The initFunc receives a function to add options, use in the style of Compost:AcquireHash +-- args are passed into the initFunc +function PTGuiDropdown:SetDynamicOptions(initFunc, args) + self.Options = initFunc + self.DynamicOptionsArgs = args + return self +end + +function PTGuiDropdown:SetSimpleOptions(options, createFunc, dropdownText) + local optTable = {} + for _, option in ipairs(options) do + table.insert(optTable, createFunc(option)) + end + self:SetOptions(optTable) + if dropdownText then + self:SetText(dropdownText) + end + return self +end + +-- Sets the options for this dropdown. If elements are added afterwards, it is mandatory that this function is called again. +function PTGuiDropdown:SetOptions(options) + self.Options = options + self:BakeOptions(options) + return self +end + +function PTGuiDropdown:BakeOptions(options, parent) + for _, opt in ipairs(options) do + opt.parent = parent + opt.siblings = options + opt.value = opt + opt.arg1 = opt + opt.arg2 = self + opt.gui = self + if opt.dropdownText or opt.closeOnClick then + local func = opt.func + opt.func = function(self, gui) + if self.dropdownText then + gui:SetText(self.dropdownText) + end + if func then + func(self, gui) + end + if self.closeOnClick then + gui:SetToggleState(false) + end + end + end + if opt.children then + opt.hasArrow = true + self:BakeOptions(opt.children, opt) + end + end +end + +function PTGuiDropdown:SetTextBy(field, value, textField) + for _, option in ipairs(self.Options) do + if option[field] == value then + self:SetText(option[textField or "text"]) + return self + end + end + return self +end + +function PTGuiDropdown:SetTextUpdater(updater, noImmediateUpdate) + self.TextUpdater = updater + if not noImmediateUpdate then + self:TextUpdater() + end + return self +end + +function PTGuiDropdown:UpdateText() + if self.TextUpdater then + self:TextUpdater() + end + return self +end + +function PTGuiDropdown:SetText(text) + UIDropDownMenu_SetText(text, self:GetHandle()) + return self +end + +function PTGuiDropdown:GetText() + return UIDropDownMenu_GetText(self:GetHandle()) +end + +local dropdownList1 = _G["DropDownList1"] +function PTGuiDropdown:SetTimer(time) + dropdownList1.showTimer = time +end + +function PTGuiDropdown:StopCounting() + dropdownList1.isCounting = nil +end + +local keepOpenFrame = CreateFrame("Frame", "PTGuiDropdownKeepOpen") +local keepOpenDropdown +local checkNext +local keepOpenScript = function() + if GetTime() > checkNext then + if not keepOpenDropdown:IsToggledOn() then + keepOpenFrame:SetScript("OnUpdate", nil) + return + end + checkNext = GetTime() + 1 + dropdownList1.showTimer = 10 + dropdownList1.isCounting = nil + end +end +-- Keeps the dropdown open until it is forcibly hidden +function PTGuiDropdown:SetKeepOpen(keepOpen) + if not self:IsToggledOn() then + return self + end + if keepOpen then + checkNext = 0 + keepOpenDropdown = self + keepOpenFrame:SetScript("OnUpdate", keepOpenScript) + else + keepOpenFrame:SetScript("OnUpdate", nil) + end + return self +end + +function PTGuiDropdown:IsToggledOn() + return (dropdownList1:IsVisible() ~= nil) and (UIDROPDOWNMENU_OPEN_MENU == self:GetName()) +end + +function PTGuiDropdown:SetToggleState(shown, anchor, xOffset, yOffset) + if self:IsToggledOn() ~= shown then + ToggleDropDownMenu(1, nil, self:GetHandle(), anchor and anchor:GetName() or nil, xOffset, yOffset) + end + return self +end + +PTGuiLib.RegisterComponent(PTGuiDropdown) \ No newline at end of file diff --git a/libs/gui/component/EditBox.lua b/libs/gui/component/EditBox.lua new file mode 100644 index 0000000..bd13511 --- /dev/null +++ b/libs/gui/component/EditBox.lua @@ -0,0 +1,29 @@ +PTGuiEditbox = PTGuiComponent:Extend("editbox") +local _G = getfenv(0) +PTGuiEditbox:Import(true, "SetText", "SetMultiLine", "SetAutoFocus", "SetFocus", "ClearFocus", "SetNumeric", "SetJustifyH", "HighlightText") +PTGuiEditbox:Import(false, "GetText", "IsMultiLine", "IsAutoFocus", "IsNumeric") + +function PTGuiEditbox:New() + local obj = setmetatable({}, self) + obj:SetHandle(CreateFrame("Editbox", self:GenerateName(), nil, "InputBoxTemplate")) + obj:SetAutoFocus(false) -- Who actually wants this on by default?? + + -- Fix edit box texture starting 5 px to the left nonsense + local left = _G[obj:GetName().."Left"] + local a,b,c,d,e = left:GetPoint(1) + left:ClearAllPoints() + left:SetPoint(a,b,c,0,e) + obj:GetHandle():SetTextInsets(4, 4, 0, 0) + + return obj +end + +function PTGuiEditbox:OnDispose() + self.super.OnDispose(self) + + self:SetText("") + self:SetMultiLine(false) + self:SetAutoFocus(false) +end + +PTGuiLib.RegisterComponent(PTGuiEditbox) \ No newline at end of file diff --git a/libs/gui/component/EditBoxSlider.lua b/libs/gui/component/EditBoxSlider.lua new file mode 100644 index 0000000..dd152d1 --- /dev/null +++ b/libs/gui/component/EditBoxSlider.lua @@ -0,0 +1,81 @@ +-- Recommended width: 100+ px / Recommended height: 36-40 px + +PTGuiEditBoxSlider = PTGuiComponent:Extend("editbox_slider") +PTGuiEditBoxSlider:ImportComponent("slider", true, "SetMinMaxValues", "SetValue", "SetValueStep") +PTGuiEditBoxSlider:ImportComponent("slider", false, "GetMinMaxValues", "GetValue", "GetValueStep") +PTGuiEditBoxSlider:ImportComponent("editbox", true, "SetText") +PTGuiEditBoxSlider:ImportComponent("editbox", false, "GetText") + +function PTGuiEditBoxSlider:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName(), nil) + obj:SetHandle(frame) + local slider = PTGuiLib.Get("slider", frame) + obj:AddComponent("slider", slider) + local editbox = PTGuiLib.Get("editbox", frame) + obj:AddComponent("editbox", editbox) + return obj +end + +function PTGuiEditBoxSlider:OnAcquire() + self.super.OnAcquire(self) + + local slider = self:GetSlider() + local editbox = self:GetEditbox() + self:SetPrimary() + + slider:SetPoint("TOPLEFT", self, "TOPLEFT") + slider:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", 0, 20) + editbox:SetJustifyH("CENTER") + self:SetEditboxPadding(35) + + slider:GetHandle():SetHitRectInsets(0, 0, 0, 3) + + slider:SetScript("OnValueChanged", function() + if not editbox.hasFocus then + editbox:SetText(slider:GetValue()) + end + end, true) + editbox:SetText(slider:GetValue()) + + editbox:SetScript("OnTextChanged", function() + slider:SetValue(editbox:GetHandle():GetNumber()) + end, true) + + editbox:SetScript("OnEditFocusGained", function() + editbox.hasFocus = true + editbox:GetHandle():HighlightText() + end, true) + + editbox:SetScript("OnEditFocusLost", function() + editbox:SetText(slider:GetValue()) + editbox.hasFocus = nil + end, true) + + editbox:SetScript("OnEnterPressed", function() + editbox:GetScript("OnTextChanged")() + editbox:ClearFocus() + end, true) +end + +-- TODO: Handle disposal +function PTGuiEditBoxSlider:OnDispose() + self.super.OnDispose(self) +end + +function PTGuiEditBoxSlider:GetSlider() + return self:GetComponent("slider") +end + +function PTGuiEditBoxSlider:GetEditbox() + return self:GetComponent("editbox") +end + +function PTGuiEditBoxSlider:SetEditboxPadding(padding) + local editbox = self:GetEditbox() + editbox:ClearAllPoints() + editbox:SetPoint("TOPLEFT", self:GetSlider(), "BOTTOMLEFT", padding, 0) + editbox:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -padding, 0) +end + +PTGuiLib.RegisterComponent(PTGuiEditBoxSlider) \ No newline at end of file diff --git a/libs/gui/component/MultiLineEditBox.lua b/libs/gui/component/MultiLineEditBox.lua new file mode 100644 index 0000000..7fbe1fc --- /dev/null +++ b/libs/gui/component/MultiLineEditBox.lua @@ -0,0 +1,72 @@ +PTGuiMultiLineEditBox = PTGuiComponent:Extend("multi_line_editbox") +PTGuiMultiLineEditBox:Import(true, "SetText", "SetMultiLine", "SetAutoFocus", "SetFocus", "ClearFocus", "HighlightText") +PTGuiMultiLineEditBox:Import(false, "GetText", "IsMultiLine", "IsAutoFocus") +PTGuiMultiLineEditBox:ImportComponent("scroll_frame", true, "Show", "Hide", "ClearAllPoints") + +function PTGuiMultiLineEditBox:New() + local obj = setmetatable({}, self) + obj:SetHandle(CreateFrame("Editbox", self:GenerateName(), nil)) + obj:GetHandle():SetFontObject(ChatFontNormal) + obj:SetAutoFocus(false) -- Who actually wants this on by default?? + obj:SetMultiLine(true) + local scrollFrame = PTGuiScrollFrame:New(obj) + obj:GetHandle():SetPoint("TOPLEFT", 0, 0) + obj:AddComponent("scroll_frame", scrollFrame) + obj:SetPrimary() + obj:SetScript("OnTextChanged", function() + -- Stolen from SuperMacro + local scrollBar = getglobal(this:GetParent():GetName().."ScrollBar") + this:GetParent():UpdateScrollChildRect() + + local _, max = scrollBar:GetMinMaxValues() + scrollBar.prevMaxValue = scrollBar.prevMaxValue or max + + if math.abs(scrollBar.prevMaxValue - scrollBar:GetValue()) <= 1 then + -- if scroll is down and add new line then move scroll + scrollBar:SetValue(max); + end + if max ~= scrollBar.prevMaxValue then + -- save max value + scrollBar.prevMaxValue = max + end + end, true) + obj:SetScript("OnEscapePressed", function() + this:ClearFocus() + end, true) + return obj +end + +function PTGuiMultiLineEditBox:OnDispose() + self.super.OnDispose(self) + + self:SetText("") + self:SetAutoFocus(false) +end + +function PTGuiMultiLineEditBox:GetScrollFrame() + return self:GetComponent("scroll_frame") +end + +function PTGuiMultiLineEditBox:SetParent(obj) + self:GetScrollFrame():SetParent(obj) +end + +function PTGuiMultiLineEditBox:SetWidth(width) + self:GetScrollFrame():GetHandle():SetWidth(width) + return self +end + +function PTGuiMultiLineEditBox:SetHeight(height) + self:GetScrollFrame():GetHandle():SetHeight(height) + return self +end + +function PTGuiMultiLineEditBox:SetPoint(point, relativeFrame, relativePoint, xOffset, yOffset) + if type(relativeFrame) == "table" and relativeFrame.IsPuppeteerGui then + relativeFrame = relativeFrame:GetAnchor() + end + self:GetScrollFrame():SetPoint(point, relativeFrame, relativePoint, xOffset, yOffset) + return self +end + +PTGuiLib.RegisterComponent(PTGuiMultiLineEditBox) \ No newline at end of file diff --git a/libs/gui/component/ScrollFrame.lua b/libs/gui/component/ScrollFrame.lua new file mode 100644 index 0000000..c1fad46 --- /dev/null +++ b/libs/gui/component/ScrollFrame.lua @@ -0,0 +1,138 @@ +PTGuiScrollFrame = PTGuiComponent:Extend("scroll_frame") +local _G = getfenv(0) +local util = PTUtil + +PTGuiScrollFrame.ApplyScrollbarPadding = true + +function PTGuiScrollFrame:New(content) + local obj = setmetatable({}, self) + local scrollFrame = CreateFrame("ScrollFrame", self:GenerateName(), nil, "UIPanelScrollFrameTemplate") + if content then + content:GetHandle():SetParent(scrollFrame) + else + content = PTGuiLib.Get("container", scrollFrame) + end + scrollFrame:SetScrollChild(content:GetHandle()) + scrollFrame.scrollBarHideable = 1 + obj:SetHandle(scrollFrame) + obj:AddComponent("content", content) + obj:SetPrimary() + obj:SetupScrollbar() + -- It seems that scroll frames require their child to have a manually set width to function properly, + -- and I can't think of a better way to ensure the width is always up-to-date other than using OnSizeChanged + scrollFrame:SetScript("OnSizeChanged", function() + obj:GetHandle():SetScrollChild(obj:GetContainer()) + obj:GetContainer():SetWidth(math.max(this:GetWidth() - (obj.ApplyScrollbarPadding and 30 or 0), 0)) + obj:GetContainer():SetHeight(this:GetHeight()) + end) + return obj +end + +-- You better be disposing everything contained in this scroll frame before disposing the frame +function PTGuiScrollFrame:OnDispose() + self.super.OnDispose(self) +end + +function PTGuiScrollFrame:SetParent(parent) + self.super.SetParent(self, parent) + -- If this isn't done after setting a new parent, the contents go off the scroll frame + self:GetHandle():SetScrollChild(self:GetContainer()) +end + +function PTGuiScrollFrame:GetContainer() + return self:GetComponent("content"):GetHandle() +end + +function PTGuiScrollFrame:GetAnchor() + return self:GetComponent("content"):GetHandle() +end + +function PTGuiScrollFrame:SetWidth(width) + self.super.SetWidth(self, width) + return self +end + +function PTGuiScrollFrame:SetHeight(height) + self.super.SetHeight(self, height) + return self +end + +function PTGuiScrollFrame:SetApplyScrollbarPadding(applyPadding) + self.ApplyScrollbarPadding = applyPadding + return self +end + +function PTGuiScrollFrame:UpdateScrollRange() + util.CallWithThis(self:GetHandle(), ScrollFrame_OnScrollRangeChanged) + return self +end + +function PTGuiScrollFrame:UpdateScrollChildRect() + self:GetHandle():UpdateScrollChildRect() + return self +end + +-- I'm going insane +function PTGuiScrollFrame:FixNextUpdate() + PTUtil.RunLater(function() + self:GetHandle():SetScrollChild(self:GetContainer()) + self:GetContainer():SetWidth(math.max(self:GetWidth() - (self.ApplyScrollbarPadding and 30 or 0), 0)) + self:GetContainer():SetHeight(self:GetHeight()) + end) +end + +-- TODO: Reduce the minimum size and make maximum size infinite +function PTGuiScrollFrame:SetupScrollbar() + local scrollFrame = self:GetHandle() + local scrollBar = _G[scrollFrame:GetName().."ScrollBar"] + local scrollUpButton = _G[scrollBar:GetName().."ScrollUpButton"] + local scrollDownButton = _G[scrollBar:GetName().."ScrollDownButton"] + + local defaultPoints = {} + for i = 1, scrollBar:GetNumPoints() do + local a,b,c,d,e = scrollBar:GetPoint(i) + defaultPoints[i] = {a, b, c, d, e} + end + self.defaultScrollbarPoints = defaultPoints + + scrollBar:ClearAllPoints() + scrollBar:SetPoint("TOPRIGHT", scrollFrame, -7, -20) + scrollBar:SetPoint("BOTTOMRIGHT", scrollFrame, -7, 18) + + local backgroundTop = scrollBar:CreateTexture(scrollBar:GetName().."BgTop", "LOW") + backgroundTop:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ScrollBar") + backgroundTop:SetWidth(31) + backgroundTop:SetHeight(128) + backgroundTop:SetPoint("BOTTOM", scrollUpButton, "TOP", 0, -128 + 4) + backgroundTop:SetTexCoord(0, 0.484375, 0, 0.5) + + local backgroundBottom = scrollBar:CreateTexture(scrollBar:GetName().."BottomTop", "LOW") + backgroundBottom:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ScrollBar") + backgroundBottom:SetWidth(31) + backgroundBottom:SetHeight(106) + backgroundBottom:SetPoint("TOP", scrollDownButton, "BOTTOM", 0, 106 - 2) + backgroundBottom:SetTexCoord(0.515625, 1.0, 0, 0.4140625) + + local backgroundMiddle = scrollBar:CreateTexture(scrollBar:GetName().."BgMiddle", "LOW") + backgroundMiddle:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-ScrollBar") + backgroundMiddle:SetWidth(31) + backgroundMiddle:SetPoint("TOP", backgroundTop, "BOTTOM", 0, 0) + backgroundMiddle:SetTexCoord(0, 0.484375, 0.125, 0.875) + backgroundMiddle:Hide() + + scrollBar:SetScript("OnSizeChanged", function() + -- Scrollbar height value is scaled, but the button heights aren't.. + local missingSpace = (scrollBar:GetHeight() / scrollBar:GetEffectiveScale()) + + scrollUpButton:GetHeight() + scrollDownButton:GetHeight() + 10 - + (backgroundTop:GetHeight() + backgroundBottom:GetHeight()) + if missingSpace > 0 then + backgroundMiddle:SetHeight(missingSpace) + backgroundMiddle:SetTexCoord(0, 0.484375, 0.125, 0.125 + (missingSpace / 256)) + backgroundMiddle:Show() + else + backgroundMiddle:Hide() + end + end) +end + +PTGuiLib.RegisterComponent(PTGuiScrollFrame) \ No newline at end of file diff --git a/libs/gui/component/Slider.lua b/libs/gui/component/Slider.lua new file mode 100644 index 0000000..dce0a91 --- /dev/null +++ b/libs/gui/component/Slider.lua @@ -0,0 +1,56 @@ +PTGuiSlider = PTGuiComponent:Extend("slider") +local _G = getfenv(0) +PTGuiSlider:Import(true, "Enable", "Disable", "SetMinMaxValues", "SetOrientation", "SetValue", "SetValueStep") +PTGuiSlider:Import(false, "GetMinMaxValues", "GetOrientation", "GetValue", "GetValueStep") + +function PTGuiSlider:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Slider", self:GenerateName(), nil, "OptionsSliderTemplate") + obj:SetHandle(frame) + return obj +end + +function PTGuiSlider:OnAcquire() + self.super.OnAcquire(self) + self:SetMinMaxValues(1, 10) + self:SetValueStep(1) + self:SetValue(1) +end + +function PTGuiSlider:OnDispose() + self.super.OnDispose(self) + self:SetText("") + self:SetLowText("Low") + self:SetHighText("High") + self:SetFontSize(10) + self:SetOrientation("HORIZONTAL") +end + +function PTGuiSlider:SetText(text) + _G[self:GetName().."Text"]:SetText(text) +end + +function PTGuiSlider:SetLowText(text) + _G[self:GetName().."Low"]:SetText(text) +end + +function PTGuiSlider:SetHighText(text) + _G[self:GetName().."High"]:SetText(text) +end + +function PTGuiSlider:SetNumberedText() + local min, max = self:GetMinMaxValues() + self:SetLowText(min) + self:SetHighText(max) +end + +function PTGuiSlider:SetFontSize(fontSize) + local font, _, flags = _G[self:GetName().."Text"]:GetFont() + _G[self:GetName().."Text"]:SetFont(font, fontSize, flags) + local font, _, flags = _G[self:GetName().."Low"]:GetFont() + _G[self:GetName().."Low"]:SetFont(font, fontSize, flags) + local font, _, flags = _G[self:GetName().."High"]:GetFont() + _G[self:GetName().."High"]:SetFont(font, fontSize, flags) +end + +PTGuiLib.RegisterComponent(PTGuiSlider) \ No newline at end of file diff --git a/libs/gui/component/TabPanel.lua b/libs/gui/component/TabPanel.lua new file mode 100644 index 0000000..8f9b51e --- /dev/null +++ b/libs/gui/component/TabPanel.lua @@ -0,0 +1,12 @@ +PTGuiTabPanel = PTGuiTabComponent:Extend("tab_panel") +PTGuiTabPanel:SetTabPosition("TOP", 5, 0) + +function PTGuiTabPanel:New() + return self.super.New(self) +end + +function PTGuiTabPanel:PlayTabSound() + PlaySound("GAMESPELLBUTTONMOUSEDOWN") +end + +PTGuiLib.RegisterComponent(PTGuiTabPanel) \ No newline at end of file diff --git a/libs/gui/component/Text.lua b/libs/gui/component/Text.lua new file mode 100644 index 0000000..f5a626f --- /dev/null +++ b/libs/gui/component/Text.lua @@ -0,0 +1,65 @@ +PTGuiText = PTGuiComponent:Extend("text") +PTGuiText:Import(true, "SetText", "SetFont", "SetNonSpaceWrap", "SetJustifyH", "SetJustifyV", "SetTextColor") +PTGuiText:Import(false, "GetText", "GetFont", "GetStringWidth", "CanNonSpaceWrap", "GetJustifyH", "GetJustifyV", "GetTextColor") + +function PTGuiText:New() + local obj = setmetatable({}, self) + local container = PTGuiLib.Get("container") + obj:AddComponent("container", container) + obj:SetHandle(container:GetHandle():CreateFontString(nil, "MEDIUM", "GameFontNormal")) + obj:SetFont("Fonts\\FRIZQT__.TTF", 12) + container:GetHandle():SetAllPoints(obj:GetHandle()) + return obj +end + +function PTGuiText:OnAcquire() + self.super.OnAcquire(self) +end + +function PTGuiText:OnDispose() + self.super.OnDispose(self) + + self:SetText("") + self:SetFont("Fonts\\FRIZQT__.TTF", 12) + self:SetJustifyH("CENTER") + self:SetJustifyV("MIDDLE") + self:SetNonSpaceWrap(false) + self:SetTextColor(1, 0.82, 0) +end + +function PTGuiText:GetContainer() + return self:GetComponent("container") +end + +function PTGuiText:SetBackground(params) + return self:GetContainer():SetBackground(params) +end + +function PTGuiText:Show() + self:GetContainer():Show() + return self +end + +function PTGuiText:Hide() + self:GetContainer():Hide() + return self +end + +function PTGuiText:SetParent(frame) + self:GetContainer():SetParent(frame) + return self +end + +function PTGuiText:SetFontSize(size) + local fontName, _, flags = self:GetFont() + self:SetFont(fontName, size, flags) + return self +end + +function PTGuiText:SetFontFlags(flags) + local fontName, size, _ = self:GetFont() + self:SetFont(fontName, size, flags) + return self +end + +PTGuiLib.RegisterComponent(PTGuiText) \ No newline at end of file diff --git a/libs/gui/component/Title.lua b/libs/gui/component/Title.lua new file mode 100644 index 0000000..c5d4faa --- /dev/null +++ b/libs/gui/component/Title.lua @@ -0,0 +1,20 @@ +PTGuiTitle = PTGuiComponent:Extend("title") +PTGuiTitle:ImportComponent("text", true, "SetText", "SetFont", "SetNonSpaceWrap", "SetJustifyH", "SetJustifyV", "SetTextColor") +PTGuiTitle:ImportComponent("text", false, "GetText", "GetFont", "GetStringWidth", "CanNonSpaceWrap", "GetJustifyH", "GetJustifyV", "GetTextColor") + +function PTGuiTitle:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName(), nil) + obj:SetHandle(frame) + local tex = frame:CreateTexture(nil, "LOW") + tex:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header") + tex:SetTexCoord(58 / 256, 197 / 256, 0 / 64, 40 / 64) + tex:SetAllPoints() + local text = PTGuiLib.Get("text", frame) + obj:AddComponent("text", text) + obj:SetPrimary() + text:SetPoint("CENTER", frame, "CENTER") + return obj +end + +PTGuiLib.RegisterComponent(PTGuiTitle) \ No newline at end of file diff --git a/libs/gui/frame/SimpleDialog.lua b/libs/gui/frame/SimpleDialog.lua new file mode 100644 index 0000000..79e7239 --- /dev/null +++ b/libs/gui/frame/SimpleDialog.lua @@ -0,0 +1,100 @@ +PTGuiSimpleDialog = PTGuiComponent:Extend("simple_dialog") +local getn = table.getn + +function PTGuiSimpleDialog:New() + local obj = setmetatable({}, self) + local frame = CreateFrame("Frame", self:GenerateName()) + frame:SetWidth(300) + frame:SetHeight(200) + frame:EnableMouse(true) + obj:SetHandle(frame) + obj:SetSimpleBackground(PTGuiComponent.BACKGROUND_DIALOG) + local title = PTGuiLib.Get("title", frame) + :SetPoint("LEFT", frame, "TOPLEFT", 50, 0) + :SetPoint("RIGHT", frame, "TOPRIGHT", -50, 0) + :SetHeight(36) + :SetPoint("CENTER", frame, "TOP") + obj:AddComponent("title", title) + local text = PTGuiLib.Get("text", frame) + :SetWidth(290) + :SetPoint("TOP", title, "BOTTOM", 0, -5) + :SetTextColor(1, 1, 1) + obj:AddComponent("text", text) + obj.Buttons = {} + return obj +end + +function PTGuiSimpleDialog:OnAcquire() + self.super.OnAcquire(self) + self:SetWidth(300) + self:SetHeight(200) +end + +function PTGuiSimpleDialog:OnDispose() + self.super.OnDispose(self) + + for _, button in ipairs(self.Buttons) do + button:Dispose() + end + PTUtil.ClearTable(self.Buttons) + self:SetMovable(false) +end + +function PTGuiSimpleDialog:SetMovable(movable) + self:GetHandle():SetMovable(movable) + self:GetHandle():SetScript("OnMouseDown", movable and function() + if arg1 == "LeftButton" then + self:GetHandle():StartMoving() + end + end or nil) + self:GetHandle():SetScript("OnMouseUp", movable and function() + if arg1 == "LeftButton" then + self:GetHandle():StopMovingOrSizing() + end + end or nil) + return self +end + +function PTGuiSimpleDialog:PlayOpenSound() + PlaySound("igMainMenuOpen") + return self +end + +function PTGuiSimpleDialog:UpdateHeight() + self:SetScript("OnUpdate", function() + self:SetScript("OnUpdate", nil) + if getn(self.Buttons) > 0 then + local lastButtonBottom = self.Buttons[getn(self.Buttons)]:GetHandle():GetBottom() + if not lastButtonBottom then + return + end + self:GetHandle():SetHeight(self:GetHandle():GetTop() - lastButtonBottom + 10) + end + end, true) + return self +end + +function PTGuiSimpleDialog:SetTitle(text) + self:GetComponent("title"):SetText(text) + return self +end + +function PTGuiSimpleDialog:SetText(text) + self:GetComponent("text"):SetText(text) + return self +end + +function PTGuiSimpleDialog:AddButton(text, onClick) + local anchor = getn(self.Buttons) == 0 and self:GetComponent("text") or self.Buttons[getn(self.Buttons)] + local button = PTGuiLib.Get("button", self) + :SetText(text) + :SetSize(self:GetWidth() - 80, 20) + :SetPoint("TOP", anchor, "BOTTOM", 0, anchor == self:GetComponent("text") and -10 or -5) + :OnClick(onClick) + :SetClickSound(PlaySound, "igMainMenuClose") + table.insert(self.Buttons, button) + self:UpdateHeight() + return self, button +end + +PTGuiLib.RegisterComponent(PTGuiSimpleDialog) \ No newline at end of file diff --git a/libs/gui/frame/TabFrame.lua b/libs/gui/frame/TabFrame.lua new file mode 100644 index 0000000..7087801 --- /dev/null +++ b/libs/gui/frame/TabFrame.lua @@ -0,0 +1,33 @@ +PTGuiTabFrame = PTGuiTabComponent:Extend("tab_frame") +PTGuiTabFrame:SetTabPosition("BOTTOM", 0, -5) + +function PTGuiTabFrame:New() + local obj = self.super.New(self) + local frame = obj:GetHandle() + frame:SetToplevel(true) + frame:EnableMouse(true) + frame:SetMovable(true) + frame:SetScript("OnMouseDown", function() + local button = arg1 + + if button == "LeftButton" then + frame:StartMoving() + end + end) + frame:SetScript("OnMouseUp", function() + local button = arg1 + + if button == "LeftButton" then + frame:StopMovingOrSizing() + end + end) + return obj +end + +-- Allows frame to the closed with escape +function PTGuiTabFrame:SetSpecial() + table.insert(UISpecialFrames, self:GetName()) + return self +end + +PTGuiLib.RegisterComponent(PTGuiTabFrame) \ No newline at end of file diff --git a/locale/Locale.lua b/locale/Locale.lua new file mode 100644 index 0000000..f08c68b --- /dev/null +++ b/locale/Locale.lua @@ -0,0 +1,102 @@ +PTLocale = {} +PTUtil.SetEnvironment(PTLocale) +local compost = PTPool + +Translations = nil + +-- Set to true to enable the ability to dump all things that need translations +local EXPORT_MODE = false + +function SetTranslations(translations) + Translations = translations + + if not EXPORT_MODE then + -- Non-English version + function Translate(str) + return Translations[str] or str + end + T = Translate + end +end + +function IsUsingTranslations() + return Translations ~= nil or EXPORT_MODE +end + +-- English version +function Translate(str) + return str +end + +if EXPORT_MODE then + NeedsTranslation = {} + + function Translate(str) + if not PTUtil.ArrayContains(NeedsTranslation, str) then + table.insert(NeedsTranslation, str) + end + return Translations and Translations[str] or str + end + + -- /run PTLocale.ExportNeedsTranslate() + function ExportNeedsTranslate() + local needsTranslate = table.concat(NeedsTranslation, "\n") + ExportFile("NeedsTranslate", needsTranslate) + end +end + +T = Translate + +function Keys(set) + if not IsUsingTranslations() then + return + end + local toInsert = compost:GetTable() + for key, value in pairs(set) do + local translated = Translate(key) + if translated then + set[key] = nil + toInsert[Translate(key)] = value + end + end + for key, value in pairs(toInsert) do + set[key] = value + end + compost:Reclaim(toInsert) +end + +function Values(table) + if not IsUsingTranslations() then + return + end + for key, value in pairs(table) do + table[key] = Translate(value) + end +end + +function KeysValues(table) + if not IsUsingTranslations() then + return + end + local toInsert = compost:GetTable() + for key, value in pairs(table) do + local translated = Translate(key) + if translated then + table[key] = nil + toInsert[translated] = Translate(value) + end + end + for key, value in pairs(toInsert) do + table[key] = value + end + compost:Reclaim(toInsert) +end + +function Array(array) + if not IsUsingTranslations() then + return + end + for i, value in ipairs(array) do + array[i] = Translate(value) + end +end \ No newline at end of file diff --git a/locale/deDE.lua b/locale/deDE.lua new file mode 100644 index 0000000..095c548 --- /dev/null +++ b/locale/deDE.lua @@ -0,0 +1,183 @@ +if GetLocale() ~= "deDE" then + return +end +local translations = { + ["Abolish Disease"] = "Krankheit aufheben", + ["Abolish Poison"] = "Vergiftung aufheben", + ["Acid Spit"] = "Säurespucke", + ["Amplify Magic"] = "Magie verstärken", + ["Ancestral Fortitude"] = "Seelenstärke der Ahnen", + ["Ancestral Spirit"] = "Geist der Ahnen", + ["Arcane Brilliance"] = "Arkane Brillanz", + ["Arcane Intellect"] = "Arkane Intelligenz", + ["Aspect of the Pack"] = "Aspekt des Rudels", + ["Aspect of the Wild"] = "Aspekt der Wildnis", + ["Auto Shot"] = "Automatischer Schuss", + ["Battle Shout"] = "Schlachtruf", + ["Bellowing Roar"] = "Dröhnendes Gebrüll", + ["Bestial Wrath"] = "Zorn des Wildtiers", + ["Blessing of Kings"] = "Segen der Könige", + ["Blessing of Light"] = "Segen des Lichts", + ["Blessing of Might"] = "Segen der Macht", + ["Blessing of Salvation"] = "Segen der Rettung", + ["Blessing of Sanctuary"] = "Segen des Refugiums", + ["Blessing of Wisdom"] = "Segen der Weisheit", + ["Blood Fury"] = "Kochendes Blut", + ["Blooming Bud"] = "Blühende Knospen", + ["Brood Affliction: Bronze"] = "Brutgebrechen: Bronze", + ["Brood Affliction: Green"] = "Brutgebrechen: Grün", + ["Bulwark of the Righteous"] = "Bollwerk der Rechtschaffenen", + ["Call of Nightmare"] = "Ruf des Alptraums", + ["Chains of Ice"] = "Eisketten", + ["Champion's Bond"] = "Band des Champions", + ["Champion's Grace"] = "Gnade des Champions", + ["Cleanse"] = "Reinigung des Glaubens", + ["Consume Shadows"] = "Schatten verzehren", + ["Corrupted Healing"] = "Verdorbene Heilung", + ["Corrupted Mind"] = "Gedankenverneblung", + ["Corruption"] = "Verderbnis", + ["Cure Disease"] = "Krankheit heilen", + ["Cure Poison"] = "Vergiftung heilen", + ["Curse of Agony"] = "Fluch der Pein", + ["Curse of Recklessness"] = "Fluch der Tollkühnheit", + ["Curse of Tongues"] = "Fluch der Sprachen", + ["Curse of the Deadwood"] = "Fluch der Totenwaldfelle", + ["Dampen Magic"] = "Magie dämpfen", + ["Daybreak"] = "Morgendämmerung", + ["Deafening Screech"] = "Ohrenbetäubendes Kreischen", + ["Death Wish"] = "Todeswunsch", + ["Demon Armor"] = "Dämonenrüstung", + ["Demon Skin"] = "Dämonenhaut", + ["Deterrence"] = "Abschreckung", + ["Dispel Magic"] = "Magiebannung", + ["Divine Intervention"] = "Göttliches Eingreifen", + ["Divine Protection"] = "Göttlicher Schutz", + ["Divine Shield"] = "Gottesschild", + ["Divine Spirit"] = "Göttlicher Willen", + ["Drink"] = "Trinken", + ["Empower Champion"] = "Champion stärken", + ["Enlighten"] = "Erleuchtung", + ["Enlightened"] = "Erleuchtet", + ["Enrage"] = "Wutanfall", + ["Entangling Roots"] = "Wucherwurzeln", + ["Evasion"] = "Entrinnen", + ["Evocation"] = "Hervorrufung", + ["Fade"] = "Verblassen", + ["Faerie Fire"] = "Feenfeuer", + ["Fear"] = "Furcht", + ["Fear Ward"] = "Furchtzauberschutz", + ["Feed Pet Effect"] = "Tier füttern", + ["Feign Death"] = "Totstellen", + ["Fire Shield"] = "Feuerschild", + ["First Aid"] = "Erste Hilfe", + ["Food"] = "Essen", + ["Forbearance"] = "Vorahnung", + ["Frenzied Regeneration"] = "Rasende Regeneration", + ["Frost Armor"] = "Frostrüstung", + ["Frost Blast"] = "Frostschlag", + ["Frost Nova"] = "Frostnova", + ["Fungal Spores"] = "Pilzsporen", + ["Gehennas' Curse"] = "Gehennas Fluch", + ["Ghost"] = "Geistererscheinung", + ["Gift of the Wild"] = "Gabe der Wildnis", + ["Grace of the Sunwell"] = "Anmut des Sonnenbrunnens", + ["Greater Blessing of Kings"] = "Großer Segen der Könige", + ["Greater Blessing of Light"] = "Großer Segen des Lichts", + ["Greater Blessing of Might"] = "Großer Segen der Macht", + ["Greater Blessing of Salvation"] = "Großer Segen der Rettung", + ["Greater Blessing of Sanctuary"] = "Großer Segen des Refugiums", + ["Greater Blessing of Wisdom"] = "Großer Segen der Weisheit", + ["Greater Heal"] = "Große Heilung", + ["Hammer of Justice"] = "Hammer der Gerechtigkeit", + ["Hand of Freedom"] = "Hand der Freiheit", + ["Hand of Protection"] = "Hand des Schutzes", + ["Hand of Sacrifice"] = "Hand der Opferung", + ["Healing Way"] = "Pfad der Heilung", + ["Health Funnel"] = "Lebenslinie", + ["Hellfire"] = "Höllenfeuer", + ["Hex"] = "Verhexung", + ["Holy Champion"] = "Heiliger Champion", + ["Holy Fire"] = "Heiliges Feuer", + ["Holy Shield"] = "Heiliger Schild", + ["Hooked Net"] = "Hakennetz", + ["Hunter's Mark"] = "Mal des Jägers", + ["Ice Armor"] = "Eisrüstung", + ["Ice Barrier"] = "Eisbarriere", + ["Ice Block"] = "Eisblock", + ["Immolate"] = "Feuerbrand", + ["Inner Fire"] = "Inneres Feuer", + ["Inner Focus"] = "Innerer Fokus", + ["Innervate"] = "Anregen", + ["Insect Swarm"] = "Insektenschwarm", + ["Inspiration"] = "Inspiration", + ["Intimidating Shout"] = "Drohruf", + ["Invulnerability"] = "Unverwundbarkeit", + ["Last Stand"] = "Letztes Gefecht", + ["Lightwell Renew"] = "Erneuerung des Lichtbrunnens", + ["Living Free Action"] = "Lebhafte freie Aktion", + ["Mage Armor"] = "Magische Rüstung", + ["Mana Shield"] = "Manaschild", + ["Mark of the Wild"] = "Mal der Wildnis", + ["Master Demonologist"] = "Meister der Dämonologie", + ["Mend Pet"] = "Tier heilen", + ["Mind Control"] = "Gedankenkontrolle", + ["Moonfire"] = "Mondfeuer", + ["Mortal Strike"] = "Tödlicher Stoß", + ["Necrotic Poison"] = "Nekrotisches Gift", + ["Net"] = "Netz", + ["Phantom Scream"] = "Phantomschrei", + ["Polymorph"] = "Verwandlung", + ["Power Infusion"] = "Seele der Macht", + ["Power Word: Fortitude"] = "Machtwort: Seelenstärke", + ["Power Word: Shield"] = "Machtwort: Schild", + ["Prayer of Fortitude"] = "Gebet der Seelenstärke", + ["Prayer of Shadow Protection"] = "Gebet des Schattenschutzes", + ["Prayer of Spirit"] = "Gebet der Willenskraft", + ["Proclaim Champion"] = "Champion ausrufen", + ["Purify"] = "Läutern", + ["Quel'dorei Meditation"] = "Meditation der Quel'dorei", + ["Quick Shots"] = "Schnelle Schüsse", + ["Quick Strikes"] = "Rasanter Schlag", + ["Rapid Fire"] = "Schnellfeuer", + ["Rapid Healing"] = "Rasche Heilung", + ["Reactive Fade"] = "Reaktives Verblassen", + ["Rebirth"] = "Wiedergeburt", + ["Recently Bandaged"] = "Kürzlich bandagiert", + ["Recklessness"] = "Tollkühnheit", + ["Redemption"] = "Erlösung", + ["Redoubt"] = "Verschanzen", + ["Regrowth"] = "Nachwachsen", + ["Rejuvenation"] = "Verjüngung", + ["Remove Curse"] = "Fluch aufheben", + ["Remove Lesser Curse"] = "Geringen Fluch aufheben", + ["Rend"] = "Verwunden", + ["Renew"] = "Erneuerung", + ["Resurrection"] = "Auferstehung", + ["Resurrection Sickness"] = "Wiederbelebungsnachwirkungen", + ["Revive Champion"] = "Champion wiederbeleben", + ["Sacrifice"] = "Opferung", + ["Scorpid Sting"] = "Skorpidstich", + ["Seduction"] = "Verführung", + ["Serpent Sting"] = "Schlangenbiss", + ["Shadow Protection"] = "Schattenschutz", + ["Shadow Ward"] = "Schattenzauberschutz", + ["Shadow Word: Pain"] = "Schattenwort: Schmerz", + ["Shadowbane Curse"] = "Schattenbann Fluch", + ["Shield Wall"] = "Schildwall", + ["Silence"] = "Stille", + ["Smoke Bomb"] = "Rauchbombe", + ["Soulstone Resurrection"] = "Seelenstein-Auferstehung", + ["Spirit Tap"] = "Willensentzug", + ["Spirit of Redemption"] = "Geist der Erlösung", + ["Thorns"] = "Dornen", + ["Unending Breath"] = "Unendlicher Atem", + ["Vanish"] = "Verschwinden", + ["Veil of Darkness"] = "Schleier der Dunkelheit", + ["Veil of Shadow"] = "Schleier des Schattens", + ["Viper Sting"] = "Vipernbiss", + ["Water Walking"] = "Wasserwandeln", + ["Weakened Soul"] = "Geschwächte Seele", + ["Web Explosion"] = "Gespinstexplosion", + ["Wound Poison"] = "Wundgift", +} +PTLocale.SetTranslations(translations) \ No newline at end of file diff --git a/locale/esES.lua b/locale/esES.lua new file mode 100644 index 0000000..bd6568e --- /dev/null +++ b/locale/esES.lua @@ -0,0 +1,183 @@ +if GetLocale() ~= "esES" then + return +end +local translations = { + ["Abolish Disease"] = "Suprimir Enfermedad", + ["Abolish Poison"] = "Suprimir Veneno", + ["Acid Spit"] = "Flema de Ácido", + ["Amplify Magic"] = "Amplificar Magia", + ["Ancestral Fortitude"] = "Entereza Ancestral", + ["Ancestral Spirit"] = "Espíritu Ancestral", + ["Arcane Brilliance"] = "Luminosidad Arcana", + ["Arcane Intellect"] = "Intelecto Arcano", + ["Aspect of the Pack"] = "Aspecto de la Manada", + ["Aspect of the Wild"] = "Aspecto Salvaje", + ["Auto Shot"] = "Disparo Automático", + ["Battle Shout"] = "Grito de Batalla", + ["Bellowing Roar"] = "Rugido Bramante", + ["Bestial Wrath"] = "Cólera de las Bestias", + ["Blessing of Kings"] = "Bendición de Reyes", + ["Blessing of Light"] = "Bendición de Luz", + ["Blessing of Might"] = "Bendición de Poderío", + ["Blessing of Salvation"] = "Bendición de Salvación", + ["Blessing of Sanctuary"] = "Bendición de Salvaguardia", + ["Blessing of Wisdom"] = "Bendición de Sabiduría", + ["Blood Fury"] = "Furia Sangrienta", + ["Blooming Bud"] = "Brote Florido", + ["Brood Affliction: Bronze"] = "Aflicción del Linaje: Bronce", + ["Brood Affliction: Green"] = "Aflicción del Linaje: Verde", + ["Bulwark of the Righteous"] = "Murallas del Justo", + ["Call of Nightmare"] = "Llamado de la Pesadilla", + ["Chains of Ice"] = "Cadenas de Hielo", + ["Champion's Bond"] = "Vínculo del Campeón", + ["Champion's Grace"] = "Gracia del Campeón", + ["Cleanse"] = "Limpiar", + ["Consume Shadows"] = "Consumir Sombras", + ["Corrupted Healing"] = "Sanación Corrupta", + ["Corrupted Mind"] = "Mente Corrupta", + ["Corruption"] = "Corrupción", + ["Cure Disease"] = "Curar Enfermedad", + ["Cure Poison"] = "Curar Envenenamiento", + ["Curse of Agony"] = "Maldición de Agonía", + ["Curse of Recklessness"] = "Maldición de Temeridad", + ["Curse of Tongues"] = "Maldición de las Lenguas", + ["Curse of the Deadwood"] = "Maldición de los Muertobosque", + ["Dampen Magic"] = "Atenuar Magia", + ["Daybreak"] = "Alborada", + ["Deafening Screech"] = "Chirrido Ensordecedor", + ["Death Wish"] = "Deseo de Muerte", + ["Demon Armor"] = "Armadura Demoníaca", + ["Demon Skin"] = "Piel de Demonio", + ["Deterrence"] = "Disuasión", + ["Dispel Magic"] = "Disipar Magia", + ["Divine Intervention"] = "Intervención Divina", + ["Divine Protection"] = "Protección Divina", + ["Divine Shield"] = "Escudo Divino", + ["Divine Spirit"] = "Espíritu Divino", + ["Drink"] = "Beber", + ["Empower Champion"] = "Empoderar Campeón", + ["Enlighten"] = "Iluminar", + ["Enlightened"] = "Iluminado", + ["Enrage"] = "Enfurecer", + ["Entangling Roots"] = "Raíces Enredadoras", + ["Evasion"] = "Evasión", + ["Evocation"] = "Evocación", + ["Fade"] = "Desvanecerse", + ["Faerie Fire"] = "Fuego Feérico", + ["Fear"] = "Miedo", + ["Fear Ward"] = "Resguardo de Miedo", + ["Feed Pet Effect"] = "Efecto de Alimentar Mascota", + ["Feign Death"] = "Fingir Muerte", + ["Fire Shield"] = "Escudo de Fuego", + ["First Aid"] = "Primeros Auxilios", + ["Food"] = "Comida", + ["Forbearance"] = "Abstinencia", + ["Frenzied Regeneration"] = "Regeneración Frenética", + ["Frost Armor"] = "Armadura de Escarcha", + ["Frost Blast"] = "Explosión de Escarcha", + ["Frost Nova"] = "Nova de Escarcha", + ["Fungal Spores"] = "Esporas Fungales", + ["Gehennas' Curse"] = "Maldición de Gehennas", + ["Ghost"] = "Fantasma", + ["Gift of the Wild"] = "Don de Lo Salvaje", + ["Grace of the Sunwell"] = "Gracia del Sol", + ["Greater Blessing of Kings"] = "Bendición de Reyes Superior", + ["Greater Blessing of Light"] = "Bendición de Luz Superior", + ["Greater Blessing of Might"] = "Bendición de Poderío Superior", + ["Greater Blessing of Salvation"] = "Bendición de Salvación Superior", + ["Greater Blessing of Sanctuary"] = "Bendición de Salvaguardia Superior", + ["Greater Blessing of Wisdom"] = "Bendición de Sabiduría Superior", + ["Greater Heal"] = "Sanación Superior", + ["Hammer of Justice"] = "Martillo de Justicia", + ["Hand of Freedom"] = "Mano de la Libertad", + ["Hand of Protection"] = "Mano de Protección", + ["Hand of Sacrifice"] = "Mano de Sacrificio", + ["Healing Way"] = "Camino de Sanación", + ["Health Funnel"] = "Cauce de Salud", + ["Hellfire"] = "Llamas Infernales", + ["Hex"] = "Maleficio", + ["Holy Champion"] = "Campeón Sagrado", + ["Holy Fire"] = "Fuego Sagrado", + ["Holy Shield"] = "Escudo Sagrado", + ["Hooked Net"] = "Red con Ganchos", + ["Hunter's Mark"] = "Marca del Cazador", + ["Ice Armor"] = "Armadura de Hielo", + ["Ice Barrier"] = "Barrera de Hielo", + ["Ice Block"] = "Bloque de Hielo", + ["Immolate"] = "Inmolar", + ["Inner Fire"] = "Fuego Interno", + ["Inner Focus"] = "Enfoque Interno", + ["Innervate"] = "Estimular", + ["Insect Swarm"] = "Enjambre de Insectos", + ["Inspiration"] = "Inspiración", + ["Intimidating Shout"] = "Grito Intimidador", + ["Invulnerability"] = "Invulnerabilidad", + ["Last Stand"] = "Última Carga", + ["Lightwell Renew"] = "Renovar Pozo de Luz", + ["Living Free Action"] = "Acción Libre Viva", + ["Mage Armor"] = "Armadura de Mago", + ["Mana Shield"] = "Escudo de Maná", + ["Mark of the Wild"] = "Marca de Lo Salvaje", + ["Master Demonologist"] = "Maestro Demonólogo", + ["Mend Pet"] = "Aliviar Mascota", + ["Mind Control"] = "Control Mental", + ["Moonfire"] = "Fuego Lunar", + ["Mortal Strike"] = "Golpe Mortal", + ["Necrotic Poison"] = "Veneno Necrótico", + ["Net"] = "Red", + ["Phantom Scream"] = "Grito Fantasmagórico", + ["Polymorph"] = "Polimorfia", + ["Power Infusion"] = "Infusión de Poder", + ["Power Word: Fortitude"] = "Palabra de Poder: Entereza", + ["Power Word: Shield"] = "Palabra de Poder: Escudo", + ["Prayer of Fortitude"] = "Rezo de Entereza", + ["Prayer of Shadow Protection"] = "Rezo de Protección Contra las Sombras", + ["Prayer of Spirit"] = "Rezo de Espíritu", + ["Proclaim Champion"] = "Proclamar Campeón", + ["Purify"] = "Purificar", + ["Quel'dorei Meditation"] = "Meditación Quel'dorei", + ["Quick Shots"] = "Disparos Presurosos", + ["Quick Strikes"] = "Golpes Rápidos", + ["Rapid Fire"] = "Fuego Rápido", + ["Rapid Healing"] = "Sanación Rápida", + ["Reactive Fade"] = "Desvanecimiento Reactivo", + ["Rebirth"] = "Renacer", + ["Recently Bandaged"] = "Vendaje Reciente", + ["Recklessness"] = "Temeridad", + ["Redemption"] = "Redención", + ["Redoubt"] = "Baluarte", + ["Regrowth"] = "Recrecimiento", + ["Rejuvenation"] = "Rejuvenecimiento", + ["Remove Curse"] = "Eliminar Maldición", + ["Remove Lesser Curse"] = "Eliminar Maldición Inferior", + ["Rend"] = "Desgarrar", + ["Renew"] = "Renovar", + ["Resurrection"] = "Resurrección", + ["Resurrection Sickness"] = "Dolencia de Resurrección", + ["Revive Champion"] = "Revivir Campeón", + ["Sacrifice"] = "Sacrificio", + ["Scorpid Sting"] = "Picadura de Escórpido", + ["Seduction"] = "Seducción", + ["Serpent Sting"] = "Picadura de Serpiente", + ["Shadow Protection"] = "Protección Contra las Sombras", + ["Shadow Ward"] = "Resguardo Contra las Sombras", + ["Shadow Word: Pain"] = "Palabra de las Sombras: Dolor", + ["Shadowbane Curse"] = "Maldición de Shadowbane", + ["Shield Wall"] = "Muro de Escudo", + ["Silence"] = "Silencio", + ["Smoke Bomb"] = "Bomba de Humo", + ["Soulstone Resurrection"] = "Resurrección con Piedra de Alma", + ["Spirit Tap"] = "Transfusión de Espíritu", + ["Spirit of Redemption"] = "Espíritu Redentor", + ["Thorns"] = "Espinas", + ["Unending Breath"] = "Aliento Inagotable", + ["Vanish"] = "Esfumarse", + ["Veil of Darkness"] = "Velo de Oscuridad", + ["Veil of Shadow"] = "Velo de Sombras", + ["Viper Sting"] = "Picadura de Víbora", + ["Water Walking"] = "Caminar Sobre el Agua", + ["Weakened Soul"] = "Alma Debilitada", + ["Web Explosion"] = "Deflagración de Telaraña", + ["Wound Poison"] = "Veneno Hiriente", +} +PTLocale.SetTranslations(translations) \ No newline at end of file diff --git a/locale/ptBR.lua b/locale/ptBR.lua new file mode 100644 index 0000000..00124f1 --- /dev/null +++ b/locale/ptBR.lua @@ -0,0 +1,183 @@ +if GetLocale() ~= "ptBR" then + return +end +local translations = { + ["Abolish Disease"] = "Abolir Doença", + ["Abolish Poison"] = "Abolir Veneno", + ["Acid Spit"] = "Cuspe Ácido", + ["Amplify Magic"] = "Amplificar Magia", + ["Ancestral Fortitude"] = "Fortitude Ancestral", + ["Ancestral Spirit"] = "Espírito Ancestral", + ["Arcane Brilliance"] = "Inteligência Arcana", + ["Arcane Intellect"] = "Inteligência Arcana", + ["Aspect of the Pack"] = "Aspecto da Matilha", + ["Aspect of the Wild"] = "Aspecto do Indomado", + ["Auto Shot"] = "Tiro Automático", + ["Battle Shout"] = "Brado de Batalha", + ["Bellowing Roar"] = "Rugido Retumbante", + ["Bestial Wrath"] = "Ira Bestial", + ["Blessing of Kings"] = "Bênção dos Reis", + ["Blessing of Light"] = "Bênção de Luz", + ["Blessing of Might"] = "Bênção do Poder", + ["Blessing of Salvation"] = "Bênção da Salvação", + ["Blessing of Sanctuary"] = "Bênção do Santuário", + ["Blessing of Wisdom"] = "Bênção de Sabedoria", + ["Blood Fury"] = "Fúria Sangrenta", + ["Blooming Bud"] = "Botão florescendo", + ["Brood Affliction: Bronze"] = "Suplício da Linhagem: Bronze", + ["Brood Affliction: Green"] = "Suplício da Linhagem: Verde", + ["Bulwark of the Righteous"] = "Baluarte dos Justos", + ["Call of Nightmare"] = "Chamado do Pesadelo", + ["Chains of Ice"] = "Correntes de Gelo", + ["Champion's Bond"] = "Vínculo do Campeão", + ["Champion's Grace"] = "Graça do Campeão", + ["Cleanse"] = "Purificação", + ["Consume Shadows"] = "Consumir Sombras", + ["Corrupted Healing"] = "Cura Corrompida", + ["Corrupted Mind"] = "Mente Corrompida", + ["Corruption"] = "Corrupção", + ["Cure Disease"] = "Curar Doença", + ["Cure Poison"] = "Curar Veneno", + ["Curse of Agony"] = "Maldição da Agonia", + ["Curse of Recklessness"] = "Maldição da Temeridade", + ["Curse of Tongues"] = "Maldição de Línguas", + ["Curse of the Deadwood"] = "Maldição de Lenha Morta", + ["Dampen Magic"] = "Atenuar Magia", + ["Daybreak"] = "Aurora", + ["Deafening Screech"] = "Guinchado Ensurdecedor", + ["Death Wish"] = "Desejo de Matar", + ["Demon Armor"] = "Armadura Demoníaca", + ["Demon Skin"] = "Pele de Demônio", + ["Deterrence"] = "Coibição", + ["Dispel Magic"] = "Dissipar Magia", + ["Divine Intervention"] = "Intervenção Divina", + ["Divine Protection"] = "Proteção Divina", + ["Divine Shield"] = "Escudo Divino", + ["Divine Spirit"] = "Espírito Divino", + ["Drink"] = "Bebida", + ["Empower Champion"] = "Capacite o Campeão", + ["Enlighten"] = "Iluminar", + ["Enlightened"] = "Iluminado", + ["Enrage"] = "Enfurecer", + ["Entangling Roots"] = "Raízes Enredantes", + ["Evasion"] = "Evasão", + ["Evocation"] = "Evocação", + ["Fade"] = "Desvanecer", + ["Faerie Fire"] = "Fogo Feérico", + ["Fear"] = "Medo", + ["Fear Ward"] = "Proteção Contra Medo", + ["Feed Pet Effect"] = "Efeito de Alimentar Ajudante", + ["Feign Death"] = "Fingir de Morto", + ["Fire Shield"] = "Escudo do Fogo", + ["First Aid"] = "Primeiros Socorros", + ["Food"] = "Comida", + ["Forbearance"] = "Abstinência", + ["Frenzied Regeneration"] = "Regeneração Frenética", + ["Frost Armor"] = "Armadura Gélida", + ["Frost Blast"] = "Impacto Gélido", + ["Frost Nova"] = "Nova Congelante", + ["Fungal Spores"] = "Esporos Fúngicos", + ["Gehennas' Curse"] = "Maldição de Geena", + ["Ghost"] = "Fantasma", + ["Gift of the Wild"] = "Dádiva do Indomado", + ["Grace of the Sunwell"] = "Graça da Nascente do Sol", + ["Greater Blessing of Kings"] = "Bênção Maior dos Reis", + ["Greater Blessing of Light"] = "Bênção Maior de Luz", + ["Greater Blessing of Might"] = "Bênção Maior de Poder", + ["Greater Blessing of Salvation"] = "Bênção Maior da Salvação", + ["Greater Blessing of Sanctuary"] = "Bênção Maior do Santuário", + ["Greater Blessing of Wisdom"] = "Bênção Maior de Sabedoria", + ["Greater Heal"] = "Cura Maior", + ["Hammer of Justice"] = "Martelo da Justiça", + ["Hand of Freedom"] = "Mão da Liberdade", + ["Hand of Protection"] = "Mão de Proteção", + ["Hand of Sacrifice"] = "Mão do Sacrifício", + ["Healing Way"] = "Caminho da Cura", + ["Health Funnel"] = "Funil de Vida", + ["Hellfire"] = "Fogo do Inferno", + ["Hex"] = "Bagata", + ["Holy Champion"] = "Santo Campeão", + ["Holy Fire"] = "Fogo Sagrado", + ["Holy Shield"] = "Escudo Sagrado", + ["Hooked Net"] = "Rede com Ganchos", + ["Hunter's Mark"] = "Marca do Caçador", + ["Ice Armor"] = "Armadura de Gelo", + ["Ice Barrier"] = "Barreira de Gelo", + ["Ice Block"] = "Bloco de Gelo", + ["Immolate"] = "Imolação", + ["Inner Fire"] = "Fogo Interior", + ["Inner Focus"] = "Concentração Interior", + ["Innervate"] = "Avivar", + ["Insect Swarm"] = "Nuvem de Insetos", + ["Inspiration"] = "Inspiração", + ["Intimidating Shout"] = "Brado Intimidador", + ["Invulnerability"] = "Invulnerabilidade", + ["Last Stand"] = "Último Recurso", + ["Lightwell Renew"] = "Renovação da Fonte de Luz", + ["Living Free Action"] = "Ação Desimpedida Ativa", + ["Mage Armor"] = "Armadura de Mago", + ["Mana Shield"] = "Escudo de Mana", + ["Mark of the Wild"] = "Marca do Indomado", + ["Master Demonologist"] = "Mestre Demonologista", + ["Mend Pet"] = "Curar Ajudante", + ["Mind Control"] = "Controle Mental", + ["Moonfire"] = "Fogo Lunar", + ["Mortal Strike"] = "Golpe Mortal", + ["Necrotic Poison"] = "Veneno Necrótico", + ["Net"] = "Rede", + ["Phantom Scream"] = "Grito Fantasma", + ["Polymorph"] = "Polimorfia", + ["Power Infusion"] = "Infusão de Poder", + ["Power Word: Fortitude"] = "Palavra de Poder: Fortitude", + ["Power Word: Shield"] = "Palavra de Poder: Escudo", + ["Prayer of Fortitude"] = "Prece da Fortitude", + ["Prayer of Shadow Protection"] = "Prece de Proteção Sombria", + ["Prayer of Spirit"] = "Prece do Espírito", + ["Proclaim Champion"] = "Proclamar Campeão", + ["Purify"] = "Purificar", + ["Quel'dorei Meditation"] = "Meditação Quel'dorei", + ["Quick Shots"] = "Disparos Velozes", + ["Quick Strikes"] = "Ataques Rápidos", + ["Rapid Fire"] = "Tiro Rápido", + ["Rapid Healing"] = "Cura Rápida", + ["Reactive Fade"] = "Desvanecer Reativo", + ["Rebirth"] = "Renascimento", + ["Recently Bandaged"] = "Tratado Recentemente", + ["Recklessness"] = "Temeridade", + ["Redemption"] = "Redenção", + ["Redoubt"] = "Reduto", + ["Regrowth"] = "Recrescimento", + ["Rejuvenation"] = "Rejuvenescer", + ["Remove Curse"] = "Remover Maldição", + ["Remove Lesser Curse"] = "Remover Maldição Inferior", + ["Rend"] = "Dilacerar", + ["Renew"] = "Renovar", + ["Resurrection"] = "Ressurreição", + ["Resurrection Sickness"] = "Enjoo de Ressurreição", + ["Revive Champion"] = "Reviver o Campeão", + ["Sacrifice"] = "Sacrificar", + ["Scorpid Sting"] = "Picada de Escorpídeo", + ["Seduction"] = "Sedução", + ["Serpent Sting"] = "Picada de Serpente", + ["Shadow Protection"] = "Proteção Sombria", + ["Shadow Ward"] = "Proteção Contra Sombra", + ["Shadow Word: Pain"] = "Palavra Sombria: Dor", + ["Shadowbane Curse"] = "Maldição Sombria", + ["Shield Wall"] = "Muralha de Escudos", + ["Silence"] = "Silêncio", + ["Smoke Bomb"] = "Bomba de Fumaça", + ["Soulstone Resurrection"] = "Ressurreição com Pedra da Alma", + ["Spirit Tap"] = "Transfusão de Espírito", + ["Spirit of Redemption"] = "Espírito da Redenção", + ["Thorns"] = "Espinhos", + ["Unending Breath"] = "Fôlego Interminável", + ["Vanish"] = "Sumir", + ["Veil of Darkness"] = "Véu de Trevas", + ["Veil of Shadow"] = "Véu de Sombras", + ["Viper Sting"] = "Picada de Víbora", + ["Water Walking"] = "Andar Sobre a Água", + ["Weakened Soul"] = "Alma Enfraquecida", + ["Web Explosion"] = "Explosão de Teia", + ["Wound Poison"] = "Veneno Ferino", +} +PTLocale.SetTranslations(translations) \ No newline at end of file diff --git a/locale/ruRU.lua b/locale/ruRU.lua new file mode 100644 index 0000000..7ac5163 --- /dev/null +++ b/locale/ruRU.lua @@ -0,0 +1,183 @@ +if GetLocale() ~= "ruRU" then + return +end +local translations = { + ["Abolish Disease"] = "Устранение болезни", + ["Abolish Poison"] = "Устранение яда", + ["Acid Spit"] = "Кислотный плевок", + ["Amplify Magic"] = "Усиление магии", + ["Ancestral Fortitude"] = "Стойкость предков", + ["Ancestral Spirit"] = "Дух предков", + ["Arcane Brilliance"] = "Чародейская гениальность", + ["Arcane Intellect"] = "Чародейский интеллект", + ["Aspect of the Pack"] = "Дух стаи", + ["Aspect of the Wild"] = "Дух дикой природы", + ["Auto Shot"] = "Автоматическая стрельба", + ["Battle Shout"] = "Боевой крик", + ["Bellowing Roar"] = "Раскатистый рев", + ["Bestial Wrath"] = "Звериный гнев", + ["Blessing of Kings"] = "Благословение королей", + ["Blessing of Light"] = "Благословение Света", + ["Blessing of Might"] = "Благословение могущества", + ["Blessing of Salvation"] = "Благословение спасения", + ["Blessing of Sanctuary"] = "Благословение неприкосновенности", + ["Blessing of Wisdom"] = "Благословение мудрости", + ["Blood Fury"] = "Кровавое неистовство", + ["Blooming Bud"] = "Blooming Bud", + ["Brood Affliction: Bronze"] = "Колдовство бронзовых драконов", + ["Brood Affliction: Green"] = "Колдовство зеленых драконов", + ["Bulwark of the Righteous"] = "Bulwark of the Righteous", + ["Call of Nightmare"] = "Call of Nightmare", + ["Chains of Ice"] = "Ледяные оковы", + ["Champion's Bond"] = "Champion's Bond", + ["Champion's Grace"] = "Champion's Grace", + ["Cleanse"] = "Очищение", + ["Consume Shadows"] = "Поглощение теней", + ["Corrupted Healing"] = "Порочное лечение", + ["Corrupted Mind"] = "Испорченный разум", + ["Corruption"] = "Порча", + ["Cure Disease"] = "Излечение болезни", + ["Cure Poison"] = "Выведение яда", + ["Curse of Agony"] = "Проклятие агонии", + ["Curse of Recklessness"] = "Проклятие безрассудства", + ["Curse of Tongues"] = "Проклятие косноязычия", + ["Curse of the Deadwood"] = "Проклятие Мертвого Леса", + ["Dampen Magic"] = "Ослабление магии", + ["Daybreak"] = "Daybreak", + ["Deafening Screech"] = "Оглушительный визг", + ["Death Wish"] = "Жажда смерти", + ["Demon Armor"] = "Шкура демона", + ["Demon Skin"] = "Шкура демона", + ["Deterrence"] = "Сдерживание", + ["Dispel Magic"] = "Рассеивание заклинаний", + ["Divine Intervention"] = "Божественное вмешательство", + ["Divine Protection"] = "Божественная защита", + ["Divine Shield"] = "Божественный щит", + ["Divine Spirit"] = "Божественный дух", + ["Drink"] = "Питье", + ["Empower Champion"] = "Empower Champion", + ["Enlighten"] = "Enlighten", + ["Enlightened"] = "Enlightened", + ["Enrage"] = "Исступление", + ["Entangling Roots"] = "Гнев деревьев", + ["Evasion"] = "Ускользание", + ["Evocation"] = "Прилив сил", + ["Fade"] = "Уход в тень", + ["Faerie Fire"] = "Волшебный огонь", + ["Fear"] = "Страх", + ["Fear Ward"] = "Защита от страха", + ["Feed Pet Effect"] = "Эффект кормления питомца", + ["Feign Death"] = "Притвориться мертвым", + ["Fire Shield"] = "Огненный щит", + ["First Aid"] = "Первая помощь", + ["Food"] = "Пища", + ["Forbearance"] = "Воздержанность", + ["Frenzied Regeneration"] = "Неистовое восстановление", + ["Frost Armor"] = "Морозный доспех", + ["Frost Blast"] = "Ледяной взрыв", + ["Frost Nova"] = "Кольцо льда", + ["Fungal Spores"] = "Fungal Spores", + ["Gehennas' Curse"] = "Проклятие Гееннаса", + ["Ghost"] = "Призрак", + ["Gift of the Wild"] = "Дар дикой природы", + ["Grace of the Sunwell"] = "Grace of the Sunwell", + ["Greater Blessing of Kings"] = "Великое благословение королей", + ["Greater Blessing of Light"] = "Великое благословение Света", + ["Greater Blessing of Might"] = "Великое благословение могущества", + ["Greater Blessing of Salvation"] = "Великое благословение спасения", + ["Greater Blessing of Sanctuary"] = "Великое благословение неприкосновенности", + ["Greater Blessing of Wisdom"] = "Великое благословение мудрости", + ["Greater Heal"] = "Великое исцеление", + ["Hammer of Justice"] = "Молот правосудия", + ["Hand of Freedom"] = "Благословение свободы", + ["Hand of Protection"] = "Благословение защиты", + ["Hand of Sacrifice"] = "Благословение жертвенности", + ["Healing Way"] = "Путь исцеления", + ["Health Funnel"] = "Канал здоровья", + ["Hellfire"] = "Адское Пламя", + ["Hex"] = "Сглаз", + ["Holy Champion"] = "Holy Champion", + ["Holy Fire"] = "Священный огонь", + ["Holy Shield"] = "Щит небес", + ["Hooked Net"] = "Сеть с крючьями", + ["Hunter's Mark"] = "Метка охотника", + ["Ice Armor"] = "Ледяной доспех", + ["Ice Barrier"] = "Ледяная преграда", + ["Ice Block"] = "Ледяная глыба", + ["Immolate"] = "Жертвенный огонь", + ["Inner Fire"] = "Внутренний огонь", + ["Inner Focus"] = "Внутреннее сосредоточение", + ["Innervate"] = "Озарение", + ["Insect Swarm"] = "Рой насекомых", + ["Inspiration"] = "Вдохновение", + ["Intimidating Shout"] = "Устрашающий крик", + ["Invulnerability"] = "Неуязвимость", + ["Last Stand"] = "Ни шагу назад", + ["Lightwell Renew"] = "Обновление колодца Света", + ["Living Free Action"] = "Живая свобода", + ["Mage Armor"] = "Магический доспех", + ["Mana Shield"] = "Щит маны", + ["Mark of the Wild"] = "Знак дикой природы", + ["Master Demonologist"] = "Мастер-демонолог", + ["Mend Pet"] = "Лечение питомца", + ["Mind Control"] = "Контроль над разумом", + ["Moonfire"] = "Лунный огонь", + ["Mortal Strike"] = "Смертельный удар", + ["Necrotic Poison"] = "Некротический яд", + ["Net"] = "Сеть", + ["Phantom Scream"] = "Phantom Scream", + ["Polymorph"] = "Превращение", + ["Power Infusion"] = "Придание сил", + ["Power Word: Fortitude"] = "Слово силы: Стойкость", + ["Power Word: Shield"] = "Слово силы: Щит", + ["Prayer of Fortitude"] = "Молитва стойкости", + ["Prayer of Shadow Protection"] = "Молитва защиты от темной магии", + ["Prayer of Spirit"] = "Молитва духа", + ["Proclaim Champion"] = "Proclaim Champion", + ["Purify"] = "Омовение", + ["Quel'dorei Meditation"] = "Quel'dorei Meditation", + ["Quick Shots"] = "Скорострельность", + ["Quick Strikes"] = "Quick Strikes", + ["Rapid Fire"] = "Быстрая стрельба", + ["Rapid Healing"] = "Быстрое исцеление", + ["Reactive Fade"] = "Резкий уход в тень", + ["Rebirth"] = "Возрождение", + ["Recently Bandaged"] = "Недавняя перевязка", + ["Recklessness"] = "Безрассудство", + ["Redemption"] = "Искупление", + ["Redoubt"] = "Оплот", + ["Regrowth"] = "Восстановление", + ["Rejuvenation"] = "Омоложение", + ["Remove Curse"] = "Снятие проклятия", + ["Remove Lesser Curse"] = "Снятие малого проклятия", + ["Rend"] = "Кровопускание", + ["Renew"] = "Обновление", + ["Resurrection"] = "Воскрешение", + ["Resurrection Sickness"] = "Слабость после воскрешения", + ["Revive Champion"] = "Revive Champion", + ["Sacrifice"] = "Жертвоприношение", + ["Scorpid Sting"] = "Укус скорпида", + ["Seduction"] = "Соблазн", + ["Serpent Sting"] = "Укус змеи", + ["Shadow Protection"] = "Защита от темной магии", + ["Shadow Ward"] = "Защита от темной магии", + ["Shadow Word: Pain"] = "Слово Тьмы: Боль", + ["Shadowbane Curse"] = "Shadowbane Curse", + ["Shield Wall"] = "Глухая оборона", + ["Silence"] = "Безмолвие", + ["Smoke Bomb"] = "Дымовая шашка", + ["Soulstone Resurrection"] = "Воскрешение камнем души", + ["Spirit Tap"] = "Захват духа", + ["Spirit of Redemption"] = "Дух воздаяния", + ["Thorns"] = "Шипы", + ["Unending Breath"] = "Бесконечное дыхание", + ["Vanish"] = "Исчезновение", + ["Veil of Darkness"] = "Завеса тьмы", + ["Veil of Shadow"] = "Пелена Тени", + ["Viper Sting"] = "Укус гадюки", + ["Water Walking"] = "Хождение по воде", + ["Weakened Soul"] = "Ослабленная душа", + ["Web Explosion"] = "Паутинный взрыв", + ["Wound Poison"] = "Нейтрализующий яд", +} +PTLocale.SetTranslations(translations) \ No newline at end of file diff --git a/locale/zhCN.lua b/locale/zhCN.lua new file mode 100644 index 0000000..ea18033 --- /dev/null +++ b/locale/zhCN.lua @@ -0,0 +1,183 @@ +if GetLocale() ~= "zhCN" then + return +end +local translations = { + ["Abolish Disease"] = "驱除疾病", + ["Abolish Poison"] = "驱毒术", + ["Acid Spit"] = "酸液喷射", + ["Amplify Magic"] = "魔法增效", + ["Ancestral Fortitude"] = "先祖坚韧", + ["Ancestral Spirit"] = "先祖之魂", + ["Arcane Brilliance"] = "奥术光辉", + ["Arcane Intellect"] = "奥术智慧", + ["Aspect of the Pack"] = "豹群守护", + ["Aspect of the Wild"] = "野性守护", + ["Auto Shot"] = "自动射击", + ["Battle Shout"] = "战斗怒吼", + ["Bellowing Roar"] = "低沉咆哮", + ["Bestial Wrath"] = "狂野怒火", + ["Blessing of Kings"] = "王者祝福", + ["Blessing of Light"] = "光明祝福", + ["Blessing of Might"] = "力量祝福", + ["Blessing of Salvation"] = "拯救祝福", + ["Blessing of Sanctuary"] = "庇护祝福", + ["Blessing of Wisdom"] = "智慧祝福", + ["Blood Fury"] = "血性狂怒", + ["Blooming Bud"] = "绽放之芽", + ["Brood Affliction: Bronze"] = "龙血之痛:青铜", + ["Brood Affliction: Green"] = "龙血之痛:绿", + ["Bulwark of the Righteous"] = "正义壁垒", + ["Call of Nightmare"] = "噩梦的召唤", + ["Chains of Ice"] = "冰链术", + ["Champion's Bond"] = "勇士之义", + ["Champion's Grace"] = "勇士之赐", + ["Cleanse"] = "清洁术", + ["Consume Shadows"] = "吞噬暗影", + ["Corrupted Healing"] = "堕落治疗", + ["Corrupted Mind"] = "堕落心灵", + ["Corruption"] = "腐蚀术", + ["Cure Disease"] = "祛病术", + ["Cure Poison"] = "消毒术", + ["Curse of Agony"] = "痛苦诅咒", + ["Curse of Recklessness"] = "鲁莽诅咒", + ["Curse of Tongues"] = "语言诅咒", + ["Curse of the Deadwood"] = "死木诅咒", + ["Dampen Magic"] = "魔法抑制", + ["Daybreak"] = "晨辉", + ["Deafening Screech"] = "震耳尖啸", + ["Death Wish"] = "死亡之愿", + ["Demon Armor"] = "魔甲术", + ["Demon Skin"] = "恶魔皮肤", + ["Deterrence"] = "威慑", + ["Dispel Magic"] = "驱散魔法", + ["Divine Intervention"] = "神圣干涉", + ["Divine Protection"] = "圣佑术", + ["Divine Shield"] = "圣盾术", + ["Divine Spirit"] = "神圣之灵", + ["Drink"] = "喝水", + ["Empower Champion"] = "赋能勇士", + ["Enlighten"] = "启发", + ["Enlightened"] = "启发", + ["Enrage"] = "激怒", + ["Entangling Roots"] = "纠缠根须", + ["Evasion"] = "闪避", + ["Evocation"] = "唤醒", + ["Fade"] = "渐隐术", + ["Faerie Fire"] = "精灵之火", + ["Fear"] = "恐惧术", + ["Fear Ward"] = "防护恐惧结界", + ["Feed Pet Effect"] = "喂养宠物效果", + ["Feign Death"] = "假死", + ["Fire Shield"] = "火焰之盾", + ["First Aid"] = "急救", + ["Food"] = "进食", + ["Forbearance"] = "自律", + ["Frenzied Regeneration"] = "狂暴回复", + ["Frost Armor"] = "霜甲术", + ["Frost Blast"] = "冰霜冲击", + ["Frost Nova"] = "冰霜新星", + ["Fungal Spores"] = "菌类孢子", + ["Gehennas' Curse"] = "基赫纳斯的诅咒", + ["Ghost"] = "鬼魂", + ["Gift of the Wild"] = "野性赐福", + ["Grace of the Sunwell"] = "太阳之井的恩典", + ["Greater Blessing of Kings"] = "强效王者祝福", + ["Greater Blessing of Light"] = "强效光明祝福", + ["Greater Blessing of Might"] = "强效力量祝福", + ["Greater Blessing of Salvation"] = "强效拯救祝福", + ["Greater Blessing of Sanctuary"] = "强效庇护祝福", + ["Greater Blessing of Wisdom"] = "强效智慧祝福", + ["Greater Heal"] = "强效治疗术", + ["Hammer of Justice"] = "制裁之锤", + ["Hand of Freedom"] = "自由之手", + ["Hand of Protection"] = "保护之手", + ["Hand of Sacrifice"] = "牺牲之手", + ["Healing Way"] = "治疗之道", + ["Health Funnel"] = "生命通道", + ["Hellfire"] = "地狱烈焰", + ["Hex"] = "妖术", + ["Holy Champion"] = "神圣勇士", + ["Holy Fire"] = "神圣之火", + ["Holy Shield"] = "神圣之盾", + ["Hooked Net"] = "钩网", + ["Hunter's Mark"] = "猎人印记", + ["Ice Armor"] = "冰甲术", + ["Ice Barrier"] = "寒冰护体", + ["Ice Block"] = "寒冰屏障", + ["Immolate"] = "献祭", + ["Inner Fire"] = "心灵之火", + ["Inner Focus"] = "心灵专注", + ["Innervate"] = "激活", + ["Insect Swarm"] = "虫群", + ["Inspiration"] = "灵感", + ["Intimidating Shout"] = "破胆怒吼", + ["Invulnerability"] = "无敌", + ["Last Stand"] = "破釜沉舟", + ["Lightwell Renew"] = "光明之泉回复", + ["Living Free Action"] = "活力自由行动", + ["Mage Armor"] = "魔甲术", + ["Mana Shield"] = "法力护盾", + ["Mark of the Wild"] = "野性印记", + ["Master Demonologist"] = "恶魔学识大师", + ["Mend Pet"] = "治疗宠物", + ["Mind Control"] = "精神控制", + ["Moonfire"] = "月火术", + ["Mortal Strike"] = "致死打击", + ["Necrotic Poison"] = "死灵之毒", + ["Net"] = "投网", + ["Phantom Scream"] = "幻影尖叫", + ["Polymorph"] = "变形术", + ["Power Infusion"] = "能量灌注", + ["Power Word: Fortitude"] = "真言术:韧", + ["Power Word: Shield"] = "真言术:盾", + ["Prayer of Fortitude"] = "坚韧祷言", + ["Prayer of Shadow Protection"] = "暗影防护祷言", + ["Prayer of Spirit"] = "精神祷言", + ["Proclaim Champion"] = "勇士誓约", + ["Purify"] = "纯净术", + ["Quel'dorei Meditation"] = "奎尔多雷冥想", + ["Quick Shots"] = "快速射击", + ["Quick Strikes"] = "快速打击", + ["Rapid Fire"] = "急速射击", + ["Rapid Healing"] = "疾速治疗", + ["Reactive Fade"] = "活性消隐", + ["Rebirth"] = "复生", + ["Recently Bandaged"] = "新近包扎", + ["Recklessness"] = "鲁莽", + ["Redemption"] = "救赎", + ["Redoubt"] = "盾牌壁垒", + ["Regrowth"] = "愈合", + ["Rejuvenation"] = "回春术", + ["Remove Curse"] = "解除诅咒", + ["Remove Lesser Curse"] = "解除次级诅咒", + ["Rend"] = "撕裂", + ["Renew"] = "恢复", + ["Resurrection"] = "复活术", + ["Resurrection Sickness"] = "复活虚弱", + ["Revive Champion"] = "复活勇士", + ["Sacrifice"] = "牺牲", + ["Scorpid Sting"] = "毒蝎钉刺", + ["Seduction"] = "诱惑", + ["Serpent Sting"] = "毒蛇钉刺", + ["Shadow Protection"] = "防护暗影", + ["Shadow Ward"] = "防护暗影结界", + ["Shadow Word: Pain"] = "暗言术:痛", + ["Shadowbane Curse"] = "魔剑诅咒", + ["Shield Wall"] = "盾墙", + ["Silence"] = "沉默", + ["Smoke Bomb"] = "烟雾弹", + ["Soulstone Resurrection"] = "灵魂石复活", + ["Spirit Tap"] = "精神分流", + ["Spirit of Redemption"] = "救赎之魂", + ["Thorns"] = "荆棘术", + ["Unending Breath"] = "魔息术", + ["Vanish"] = "消失", + ["Veil of Darkness"] = "黑暗之雾", + ["Veil of Shadow"] = "暗影迷雾", + ["Viper Sting"] = "蝰蛇钉刺", + ["Water Walking"] = "水上行走", + ["Weakened Soul"] = "虚弱灵魂", + ["Web Explosion"] = "蛛网爆炸", + ["Wound Poison"] = "致伤毒药", +} +PTLocale.SetTranslations(translations) \ No newline at end of file