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 0000000..55c4669
Binary files /dev/null and b/assets/textures/arrow.tga differ
diff --git a/assets/textures/bars/Blizzard-Raid-Sideless.tga b/assets/textures/bars/Blizzard-Raid-Sideless.tga
new file mode 100644
index 0000000..c685af9
Binary files /dev/null and b/assets/textures/bars/Blizzard-Raid-Sideless.tga differ
diff --git a/assets/textures/bars/Blizzard-Raid.tga b/assets/textures/bars/Blizzard-Raid.tga
new file mode 100644
index 0000000..4065a9d
Binary files /dev/null and b/assets/textures/bars/Blizzard-Raid.tga differ
diff --git a/assets/textures/bars/Blizzard-Smooth.tga b/assets/textures/bars/Blizzard-Smooth.tga
new file mode 100644
index 0000000..d85a872
Binary files /dev/null and b/assets/textures/bars/Blizzard-Smooth.tga differ
diff --git a/assets/textures/bars/BlizzardRetailBar.tga b/assets/textures/bars/BlizzardRetailBar.tga
new file mode 100644
index 0000000..18e973c
Binary files /dev/null and b/assets/textures/bars/BlizzardRetailBar.tga differ
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 0000000..a5a601e
Binary files /dev/null and b/assets/textures/bars/ModernPowerEnergy.tga differ
diff --git a/assets/textures/bars/ModernPowerEnergyClass.tga b/assets/textures/bars/ModernPowerEnergyClass.tga
new file mode 100644
index 0000000..71eee9e
Binary files /dev/null and b/assets/textures/bars/ModernPowerEnergyClass.tga differ
diff --git a/assets/textures/bars/ModernPowerFocus.tga b/assets/textures/bars/ModernPowerFocus.tga
new file mode 100644
index 0000000..51e6f9f
Binary files /dev/null and b/assets/textures/bars/ModernPowerFocus.tga differ
diff --git a/assets/textures/bars/ModernPowerManaClass.tga b/assets/textures/bars/ModernPowerManaClass.tga
new file mode 100644
index 0000000..ec75f77
Binary files /dev/null and b/assets/textures/bars/ModernPowerManaClass.tga differ
diff --git a/assets/textures/bars/ModernPowerRage.tga b/assets/textures/bars/ModernPowerRage.tga
new file mode 100644
index 0000000..77b4813
Binary files /dev/null and b/assets/textures/bars/ModernPowerRage.tga differ
diff --git a/assets/textures/bars/Puppeteer-Borderless.tga b/assets/textures/bars/Puppeteer-Borderless.tga
new file mode 100644
index 0000000..93386e3
Binary files /dev/null and b/assets/textures/bars/Puppeteer-Borderless.tga differ
diff --git a/assets/textures/bars/Puppeteer-Flat-Borderless.tga b/assets/textures/bars/Puppeteer-Flat-Borderless.tga
new file mode 100644
index 0000000..39cf226
Binary files /dev/null and b/assets/textures/bars/Puppeteer-Flat-Borderless.tga differ
diff --git a/assets/textures/bars/Puppeteer-Flat.tga b/assets/textures/bars/Puppeteer-Flat.tga
new file mode 100644
index 0000000..8bdb8b9
Binary files /dev/null and b/assets/textures/bars/Puppeteer-Flat.tga differ
diff --git a/assets/textures/bars/Puppeteer-Shineless-Borderless.tga b/assets/textures/bars/Puppeteer-Shineless-Borderless.tga
new file mode 100644
index 0000000..82507c6
Binary files /dev/null and b/assets/textures/bars/Puppeteer-Shineless-Borderless.tga differ
diff --git a/assets/textures/bars/Puppeteer-Shineless.tga b/assets/textures/bars/Puppeteer-Shineless.tga
new file mode 100644
index 0000000..573a466
Binary files /dev/null and b/assets/textures/bars/Puppeteer-Shineless.tga differ
diff --git a/assets/textures/bars/Puppeteer.tga b/assets/textures/bars/Puppeteer.tga
new file mode 100644
index 0000000..dc474f7
Binary files /dev/null and b/assets/textures/bars/Puppeteer.tga differ
diff --git a/assets/textures/roles/Damage.tga b/assets/textures/roles/Damage.tga
new file mode 100644
index 0000000..f7504a2
Binary files /dev/null and b/assets/textures/roles/Damage.tga differ
diff --git a/assets/textures/roles/Healer.tga b/assets/textures/roles/Healer.tga
new file mode 100644
index 0000000..804a7b6
Binary files /dev/null and b/assets/textures/roles/Healer.tga differ
diff --git a/assets/textures/roles/Tank.tga b/assets/textures/roles/Tank.tga
new file mode 100644
index 0000000..91f98e5
Binary files /dev/null and b/assets/textures/roles/Tank.tga differ
diff --git a/core/ActionBindings.lua b/core/ActionBindings.lua
new file mode 100644
index 0000000..74e6b45
--- /dev/null
+++ b/core/ActionBindings.lua
@@ -0,0 +1,162 @@
+PTUtil.SetEnvironment(Puppeteer)
+local _G = getfenv(0)
+local util = PTUtil
+local colorize = util.Colorize
+
+
+ActionBinds = {}
+ActionBindsMap = {}
+ActionBindNames = {}
+
+function RegisterActionBind(bindTable)
+ table.insert(ActionBinds, bindTable)
+ ActionBindsMap[bindTable.Name] = bindTable
+ table.insert(ActionBindNames, bindTable.Name)
+end
+
+RegisterActionBind({
+ Name = "Target",
+ Description = "Target the unit",
+ Script = function(unit, unitFrame)
+ TargetUnit(unit)
+ end
+})
+RegisterActionBind({
+ Name = "Assist",
+ Description = "Target the unit's target",
+ Script = function(unit, unitFrame)
+ AssistUnit(unit)
+ end
+})
+RegisterActionBind({
+ Name = "Follow",
+ Description = "Follow the unit",
+ Script = function(unit, unitFrame)
+ FollowUnit(unit)
+ end
+})
+RegisterActionBind({
+ Name = "Menu",
+ Description = "Opens the context menu for the unit, if applicable",
+ Script = function(unit, unitFrame)
+ -- Resolve focus to a proper unit if possible
+ if AllCustomUnitsSet[unit] then
+ unit = PTUnitProxy.ResolveCustomUnit(unit)
+ if not unit then
+ return
+ end
+ end
+
+ local dropdown
+
+ local specialContexts = {
+ ["player"] = _G["PlayerFrameDropDown"],
+ ["target"] = _G["TargetFrameDropDown"],
+ ["pet"] = _G["PetFrameDropDown"]
+ }
+ if specialContexts[unit] then
+ dropdown = specialContexts[unit]
+ elseif util.StartsWith(unit, "raid") and not util.StartsWith(unit, "raidpet") then
+ FriendsDropDown.displayMode = "MENU"
+ FriendsDropDown.initialize = function()
+ UnitPopup_ShowMenu(_G[UIDROPDOWNMENU_OPEN_MENU], "PARTY", unit, nil, string.sub(unit, 5))
+ end
+ dropdown = FriendsDropDown
+ elseif util.StartsWith(unit, "party") and not util.StartsWith(unit, "partypet") then
+ dropdown = _G["PartyMemberFrame"..string.sub(unit, 6).."DropDown"]
+ end
+
+
+ if dropdown then
+ local frame = unitFrame:GetRootContainer()
+ ToggleDropDownMenu(1, nil, dropdown, frame:GetName(), frame:GetWidth(), 0)
+ end
+ end
+})
+RegisterActionBind({
+ Name = "Role",
+ Description = "Open a menu to assign a player's role",
+ Script = function(unit, unitFrame)
+ if not UnitIsPlayer(unit) then
+ return
+ end
+ RoleAssignInfo.Name = UnitName(unit)
+ RoleAssignInfo.Class = util.GetClass(unit)
+ RoleAssignInfo.ClassColor = util.GetClassColor(util.GetClass(unit), true)
+ RoleAssignInfo.FrameGroup = unitFrame.owningGroup
+ local frame = unitFrame:GetRootContainer()
+ RoleDropdown:SetToggleState(false)
+ RoleDropdown:SetToggleState(true, frame, frame:GetWidth(), frame:GetHeight())
+ RoleDropdown:SetKeepOpen(true)
+ PlaySound("igMainMenuOpen")
+ end
+})
+RegisterActionBind({
+ Name = "Role: Tank",
+ Description = "Set the player's role as Tank",
+ Script = function(unit, unitFrame)
+ SetUnitRoleAndUpdate(unit, "Tank")
+ end
+})
+RegisterActionBind({
+ Name = "Role: Healer",
+ Description = "Set the player's role as Healer",
+ Script = function(unit, unitFrame)
+ SetUnitRoleAndUpdate(unit, "Healer")
+ end
+})
+RegisterActionBind({
+ Name = "Role: Damage",
+ Description = "Set the player's role as Damage",
+ Script = function(unit, unitFrame)
+ SetUnitRoleAndUpdate(unit, "Damage")
+ end
+})
+RegisterActionBind({
+ Name = "Role: None",
+ Description = "Remove the player's role",
+ Script = function(unit, unitFrame)
+ SetUnitRoleAndUpdate(unit, nil)
+ end
+})
+RegisterActionBind({
+ Name = "Focus",
+ Description = "Add/remove a unit to your Focus frames",
+ Script = function(unit, unitFrame)
+ if not util.IsSuperWowPresent() then
+ DEFAULT_CHAT_FRAME:AddMessage(colorize("You need SuperWoW to focus targets.", 1, 0.5, 0.5))
+ return
+ end
+
+ ToggleFocusUnit(unit)
+ end
+})
+RegisterActionBind({
+ Name = "Promote Focus",
+ Description = "Move a focus to the top",
+ Script = function(unit, unitFrame)
+ if not util.IsSuperWowPresent() then
+ DEFAULT_CHAT_FRAME:AddMessage(colorize("You need SuperWoW to focus targets.", 1, 0.5, 0.5))
+ return
+ end
+
+ PromoteFocus(unit)
+ end
+})
+--[[
+RegisterActionBind({
+ Name = "Demote Focus",
+ Description = "Move a focus to the bottom",
+ Script = function(unit, unitFrame)
+ if not util.IsSuperWowPresent() then
+ DEFAULT_CHAT_FRAME:AddMessage(colorize("You need SuperWoW to focus targets.", 1, 0.5, 0.5))
+ return
+ end
+ -- TODO: Logic flawed
+ local guid = PTGuidRoster.GetUnitGuid(unit)
+
+ PT_UnfocusUnit(unit)
+ PT_FocusUnit(guid)
+ end
+})
+]]
\ No newline at end of file
diff --git a/core/Bindings.lua b/core/Bindings.lua
new file mode 100644
index 0000000..264d7e4
--- /dev/null
+++ b/core/Bindings.lua
@@ -0,0 +1,731 @@
+PTUtil.SetEnvironment(Puppeteer)
+local _G = getfenv(0)
+local compost = PTPool
+local util = PTUtil
+local colorize = util.Colorize
+local GetSpellID = util.GetSpellID
+local GetClass = util.GetClass
+local CompostReclaim = util.CompostReclaim
+
+BindingClipboard = nil
+
+function GetBindings()
+ return PTBindings.Loadouts[PTBindings.SelectedLoadout]
+end
+
+function GetBindingsFor(unit)
+ local bindings = GetBindings().Bindings
+ if not UnitCanAttack("player", unit) or bindings.UseFriendlyForHostile then
+ return bindings.Friendly
+ end
+ return bindings.Hostile
+end
+
+function GetBinding(friendlyOrHostile, modifier, button)
+ local bindings = GetBindings()
+ if bindings.UseFriendlyForHostile and friendlyOrHostile == "Hostile" then
+ friendlyOrHostile = "Friendly"
+ end
+ local l1 = bindings.Bindings[friendlyOrHostile]
+ if not l1 then
+ return
+ end
+ local l2 = l1[modifier]
+ if not l2 then
+ return
+ end
+ return l2[button]
+end
+
+function GetBindingFor(unit, modifier, button)
+ return GetBinding(not UnitCanAttack("player", unit) and "Friendly" or "Hostile", modifier, button)
+end
+
+function GetSelectedBindingsLoadoutName()
+ return PTBindings.SelectedLoadout
+end
+
+function SetSelectedBindingsLoadout(name)
+ PTBindings.SelectedLoadout = name
+ PTSettingsGui.LoadBindings()
+ InitBindingDisplayCache()
+end
+
+function GetBindingLoadouts()
+ return PTBindings.Loadouts
+end
+
+function NewBindingsLoadout(name, baseLoadout)
+ local loadouts = GetBindingLoadouts()
+ if loadouts[name] then
+ return
+ end
+ local loadout = baseLoadout and util.CloneTable(baseLoadout, true) or CreateEmptyBindingsLoadout()
+ loadouts[name] = loadout
+end
+
+function CreateEmptyBindingsLoadout()
+ return {
+ UseFriendlyForHostile = false,
+ Bindings = {
+ Friendly = {},
+ Hostile = {}
+ }
+ }
+end
+
+local SORT_DESCENDING = function(a, b) return a < b end
+function GetBindingLoadoutNames()
+ local loadouts = GetBindingLoadouts()
+ local names = {}
+ for name, _ in pairs(loadouts) do
+ table.insert(names, name)
+ end
+ table.sort(names, SORT_DESCENDING)
+ return names
+end
+
+-- Returns a pruned copy of the loadout using composted tables
+function PruneLoadoutCompost(loadout)
+ loadout = util.CloneTableCompost(loadout, true)
+ for targetName, target in pairs(loadout.Bindings) do
+ for modifierName, modifier in pairs(target) do
+ for button, binding in pairs(modifier) do
+ local shouldRemove = PruneBinding(binding, true)
+ if shouldRemove then
+ CompostReclaim(binding)
+ modifier[button] = nil
+ end
+ end
+ if util.IsTableEmpty(modifier) then
+ CompostReclaim(modifier)
+ target[modifierName] = nil
+ end
+ end
+ end
+ return loadout
+end
+
+function PruneLoadout(loadout, copy)
+ if copy then
+ loadout = util.CloneTable(loadout, true)
+ end
+ for targetName, target in pairs(loadout.Bindings) do
+ for modifierName, modifier in pairs(target) do
+ for button, binding in pairs(modifier) do
+ local shouldRemove = PruneBinding(binding)
+ if shouldRemove then
+ modifier[button] = nil
+ end
+ end
+ if util.IsTableEmpty(modifier) then
+ target[modifierName] = nil
+ end
+ end
+ end
+ return loadout
+end
+
+function PruneBinding(binding, useCompost)
+ if not binding.Type or not binding.Data then
+ return true
+ end
+ if binding.Tooltip then
+ local tooltip = binding.Tooltip
+ if tooltip.Type ~= "DEFAULT" and (tooltip.Data == nil or tooltip.Data == "") then
+ tooltip.Type = nil
+ tooltip.Data = nil
+ end
+
+ if util.IsTableEmpty(tooltip) then
+ if useCompost then
+ CompostReclaim(binding.Tooltip)
+ end
+ binding.Tooltip = nil
+ end
+ end
+ -- Empty spells are default and don't need to be stored
+ if binding.Type == "SPELL" and (not binding.Data or binding.Data == "") then
+ return true
+ end
+ if binding.Type == "MULTI" then
+ if binding.Data.Title == "" then
+ binding.Data.Title = nil
+ end
+ for _, subBinding in ipairs(binding.Data.Bindings) do
+ PruneBinding(subBinding)
+ end
+ end
+end
+
+local stringDataBindings = util.ToSet({"SPELL", "ACTION", "ITEM", "MACRO", "SCRIPT"})
+function ExpandBinding(binding)
+ if not binding.Type then
+ binding.Type = "SPELL"
+ end
+ if not binding.Data then
+ if stringDataBindings[binding.Type] then
+ binding.Data = ""
+ elseif binding.Type == "MULTI" then
+ binding.Data = {Bindings = {}}
+ end
+ end
+ if not binding.Tooltip then
+ binding.Tooltip = {}
+ end
+end
+
+function LoadoutEquals(loadout1, loadout2)
+ loadout1 = PruneLoadoutCompost(loadout1)
+ loadout2 = PruneLoadoutCompost(loadout2)
+ local equals = util.TableEquals(loadout1, loadout2)
+ CompostReclaim(loadout1)
+ CompostReclaim(loadout2)
+ return equals
+end
+
+function GetNumLoadoutChanges(origLoadout, modifiedLoadout)
+ origLoadout = PruneLoadoutCompost(origLoadout)
+ modifiedLoadout = PruneLoadoutCompost(modifiedLoadout)
+ local numChanges = 0
+ local alreadyChecked = compost:GetTable()
+ for targetName, target in pairs(modifiedLoadout.Bindings) do
+ for modifierName, modifier in pairs(target) do
+ for button, binding in pairs(modifier) do
+ local otherBinding = util.TraverseTable(origLoadout.Bindings, targetName, modifierName, button)
+ if otherBinding == nil or not util.TableEquals(binding, otherBinding) then
+ numChanges = numChanges + 1
+ alreadyChecked[binding] = true
+ end
+ end
+ end
+ end
+ for targetName, target in pairs(origLoadout.Bindings) do
+ for modifierName, modifier in pairs(target) do
+ for button, binding in pairs(modifier) do
+ local otherBinding = util.TraverseTable(modifiedLoadout.Bindings, targetName, modifierName, button)
+ if not alreadyChecked[otherBinding] and (otherBinding == nil or not util.TableEquals(binding, otherBinding)) then
+ numChanges = numChanges + 1
+ end
+ end
+ end
+ end
+ if origLoadout.UseFriendlyForHostile ~= modifiedLoadout.UseFriendlyForHostile then
+ numChanges = numChanges + 1
+ end
+ CompostReclaim(origLoadout)
+ CompostReclaim(modifiedLoadout)
+ compost:Reclaim(alreadyChecked)
+ return numChanges
+end
+
+-- Returns a copy of the clipboard
+function GetBindingClipboard()
+ return util.CloneTable(BindingClipboard, true)
+end
+
+function HasBindingClipboard()
+ return BindingClipboard ~= nil
+end
+
+-- Copies the binding to the clipboard
+function SetBindingClipboard(binding)
+ BindingClipboard = util.CloneTable(binding, true)
+end
+
+
+function GetBindingTooltipText(binding)
+ if binding.Tooltip and binding.Tooltip.Type == "CUSTOM" then
+ return binding.Tooltip.Data
+ end
+end
+
+function GenerateDefaultBindings()
+ _G.PTBindings = {}
+ PTBindings["SelectedLoadout"] = "Primary"
+ local loadouts = {}
+ PTBindings["Loadouts"] = loadouts
+ loadouts["Primary"] = CreateEmptyBindingsLoadout()
+
+ local queuedBestSpellTasks = {}
+ local frame = CreateFrame("Frame")
+ frame:RegisterEvent("SPELLS_CHANGED")
+ frame:SetScript("OnEvent", function()
+ frame:UnregisterAllEvents()
+ for _, task in ipairs(queuedBestSpellTasks) do
+ task()
+ end
+ end)
+
+ local friendlyOrHostile = "Friendly"
+ local modifier
+ local function setContext(a, b)
+ friendlyOrHostile = a
+ modifier = b
+ end
+ local function setBinding(button, binding)
+ local bindings = GetBindings()
+ local l1 = bindings.Bindings[friendlyOrHostile]
+ if not l1 then
+ l1 = {}
+ bindings.Bindings[friendlyOrHostile] = l1
+ end
+ local l2 = l1[modifier]
+ if not l2 then
+ l2 = {}
+ l1[modifier] = l2
+ end
+ l2[button] = binding
+ end
+ local function setSpell(button, spell)
+ setBinding(button, {Type = "SPELL", Data = spell})
+ end
+ local function setBestSpell(button, spells)
+ local friendlyOrHostile = friendlyOrHostile
+ local modifier = modifier
+ table.insert(queuedBestSpellTasks, function()
+ for _, spell in ipairs(spells) do
+ if util.GetSpellID(spell) then
+ setContext(friendlyOrHostile, modifier)
+ setSpell(button, spell)
+ SetSelectedBindingsLoadout(GetSelectedBindingsLoadoutName())
+ return
+ end
+ end
+ end)
+ end
+ local function setMulti(button, tooltip, spells)
+ local bindings = {}
+ for i, spell in ipairs(spells) do
+ bindings[i] = {Type = "SPELL", Data = spell}
+ end
+ setBinding(button, {
+ Type = "MULTI",
+ Tooltip = {
+ Type = "CUSTOM",
+ Data = tooltip
+ },
+ Data = {
+ Bindings = bindings
+ }
+ })
+ end
+ local function setAction(button, action)
+ setBinding(button, {Type = "ACTION", Data = action})
+ end
+ local function addHealerControls()
+ setContext("Friendly", "Shift")
+ setAction("LeftButton", "Target")
+ setAction("MiddleButton", "Role")
+ setAction("RightButton", "Menu")
+ end
+
+ local class = GetClass("player")
+ if class == "PRIEST" then
+ setContext("Friendly", "None")
+ setSpell("LeftButton", "Power Word: Shield")
+ setSpell("MiddleButton", "Renew")
+ setBestSpell("RightButton", {"Greater Heal", "Heal", "Lesser Heal"})
+
+ addHealerControls()
+
+ setContext("Friendly", "Control")
+ setMulti("LeftButton", "Buffs", {"Power Word: Fortitude", "Divine Spirit", "Shadow Protection"})
+ setSpell("RightButton", "Dispel Magic")
+
+ setContext("Hostile", "None")
+ setSpell("RightButton", "Dispel Magic")
+
+ setContext("Hostile", "Control")
+ setSpell("RightButton", "Dispel Magic")
+ elseif class == "DRUID" then
+ setContext("Friendly", "None")
+ setSpell("LeftButton", "Rejuvenation")
+ setSpell("RightButton", "Healing Touch")
+
+ addHealerControls()
+
+ setContext("Friendly", "Control")
+ setMulti("LeftButton", "Buffs", {"Mark of the Wild", "Thorns"})
+ setSpell("RightButton", "Remove Curse")
+ elseif class == "PALADIN" then
+ setContext("Friendly", "None")
+ setSpell("LeftButton", "Flash of Light")
+ setSpell("RightButton", "Holy Light")
+
+ addHealerControls()
+
+ setContext("Friendly", "Control")
+ setMulti("LeftButton", "Blessings", {"Blessing of Might", "Blessing of Wisdom", "Blessing of Salvation", "Blessing of Kings"})
+ setBestSpell("RightButton", {"Cleanse", "Purify"})
+ elseif class == "SHAMAN" then
+ setContext("Friendly", "None")
+ setBestSpell("LeftButton", {"Healing Wave", "Lesser Healing Wave"})
+ setSpell("RightButton", "Chain Heal")
+
+ addHealerControls()
+
+ setContext("Friendly", "Control")
+ setSpell("RightButton", "Cure Disease")
+ else
+ -- Non-healer classes can use this addon like traditional raid frames
+ setContext("Friendly", "None")
+ setAction("LeftButton", "Target")
+ setAction("MiddleButton", "Role")
+ setAction("RightButton", "Menu")
+
+ setContext("Hostile", "None")
+ setAction("LeftButton", "Target")
+ setAction("RightButton", "Menu")
+ end
+end
+
+PVPProtectOverrideTime = 0
+
+PVPProtectMenu = PTGuiLib.Get("dropdown", UIParent)
+PVPProtectMenu:SetOptions({
+ {
+ text = colorize("PVP Flag Protection", 0.3, 1, 0.3),
+ textHeight = 11,
+ notCheckable = true,
+ disabled = true
+ },
+ {
+ notCheckable = true,
+ disabled = true
+ },
+ {
+ text = colorize("Whoops, thanks for the save!", 0.8, 1, 0.8),
+ notCheckable = true,
+ func = function()
+ if UIErrorsFrame then
+ UIErrorsFrame:AddMessage(colorize("No problem, stay safe", 0.2, 1, 0.2))
+ end
+ end
+ },
+ {
+ notCheckable = true,
+ disabled = true
+ },
+ {
+ text = colorize("Disable protection for 5 min", 1, 0.8, 0.8),
+ notCheckable = true,
+ func = function()
+ PVPProtectOverrideTime = GetTime() + (5 * 60)
+ if UIErrorsFrame then
+ UIErrorsFrame:AddMessage(colorize("Protection disabled for 5 min", 1, 0, 0))
+ end
+ end
+ },
+ {
+ text = colorize("Disable protection this session", 1, 0.6, 0.6),
+ notCheckable = true,
+ func = function()
+ PVPProtectOverrideTime = GetTime() + (1000 * 60)
+ if UIErrorsFrame then
+ UIErrorsFrame:AddMessage(colorize("Protection disabled this session", 1, 0, 0))
+ end
+ end
+ }
+})
+
+function ShouldTriggerPVPFlagProtection(unit, spell)
+ return PTOptions.PVPFlagProtection and not util.IsReallyInInstance() and UnitIsPVP(unit) and UnitIsPlayer(unit)
+ and not UnitIsPVP("player") and PVPProtectOverrideTime < GetTime() and not util.ResurrectionSpellsSet[spell]
+end
+
+local Sound_Disabled = function() end
+
+function RunTargetedAction(binding, unit, actionFunc, mustTempTarget)
+ local hasTarget = UnitExists("target")
+ local changeTarget = (mustTempTarget or binding.TargetWhileCasting or PTOptions.TargetWhileCasting)
+ and not UnitIsUnit("target", unit)
+
+ if changeTarget then
+ local Sound_Enabled = PlaySound
+ _G.PlaySound = Sound_Disabled
+ TargetUnit(unit)
+ _G.PlaySound = Sound_Enabled
+ end
+
+ actionFunc()
+
+ local targetAfterCasting = binding.TargetAfterCasting or
+ (binding.TargetAfterCasting == nil and PTOptions.TargetAfterCasting)
+ if targetAfterCasting then
+ TargetUnit(unit)
+ elseif changeTarget then
+ if hasTarget then
+ TargetLastTarget()
+ else
+ local Sound_Enabled = PlaySound
+ _G.PlaySound = Sound_Disabled
+ ClearTarget()
+ _G.PlaySound = Sound_Enabled
+ end
+ end
+end
+
+local targetedSpell
+local targetedSpellUnit
+local function targetedCastFunc()
+ if util.IsSuperWowPresent() then
+ CastSpellByName(targetedSpell, targetedSpellUnit)
+ else
+ CastSpellByName(targetedSpell)
+ end
+end
+local function setupTargetedCast(spell, unit)
+ targetedSpell = spell
+ targetedSpellUnit = unit
+ return targetedCastFunc
+end
+function RunBinding_Spell(binding, unit)
+ local spell = binding.Data
+
+ if PTOptions.AutoResurrect and util.IsDeadFriend(unit) then
+ if PTUnit.Get(unit):HasBuffIDOrName(45568, "Holy Champion") and GetSpellID("Revive Champion")
+ and UnitAffectingCombat("player") then
+ spell = "Revive Champion"
+ else
+ spell = util.ResurrectionSpells[GetClass("player")] or spell
+ end
+ end
+
+ if spell == nil or spell == "" then
+ return
+ end
+
+ if ShouldTriggerPVPFlagProtection(unit, spell) then
+ if MouseoverFrame then
+ PVPProtectMenu:SetToggleState(false)
+ local frame = MouseoverFrame:GetRootContainer()
+ PVPProtectMenu:SetToggleState(true, frame, frame:GetWidth(), frame:GetHeight())
+ PVPProtectMenu:SetKeepOpen(true)
+ end
+ PlaySound("igMainMenuOpen")
+ return
+ end
+
+ RunTargetedAction(binding, unit, setupTargetedCast(spell, unit), not util.IsSuperWowPresent())
+end
+
+function RunBinding_Action(binding, unit, unitFrame)
+ local action = ActionBindsMap[binding.Data]
+ if not action then
+ return
+ end
+ action.Script(unit, unitFrame)
+end
+
+function RunBinding_Item(binding, unit)
+ RunTargetedAction(binding, unit, function()
+ util.UseItem(binding.Data)
+ end, true)
+end
+
+function RunBinding_Macro(binding, unit)
+ RunTargetedAction(binding, unit, function()
+ util.RunMacro(binding.Data, unit)
+ end)
+end
+
+BindingScriptAPI = {
+ EnsureBuffs = function(unit, ...)
+ local unitData = PTUnit.Get(unit)
+ for _, buff in ipairs(arg) do
+ if not unitData:HasBuff(buff) then
+ if util.IsSuperWowPresent() then
+ CastSpellByName(buff, unit)
+ else
+ CastSpellByName(buff)
+ end
+ return buff
+ end
+ end
+ end
+}
+
+local preScript = "local unit = PTScriptUnit;"..
+ "local unresolvedUnit = PTScriptUnitUnresolved;"..
+ "local unitData = PTUnit.Get(unit);"..
+ "local unitFrame = PTScriptUnitFrame;"
+BindingScriptCache = {}
+BindingEnvironment = setmetatable({_G = _G,
+ api = BindingScriptAPI,
+ print = function(...)
+ local str = table.getn(arg) > 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