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