├── img ├── config.tga ├── target-left.tga └── target-right.tga ├── sounds ├── resist.mp3 └── expiring.mp3 ├── libs ├── AceAddon-2.0 │ └── AceAddon-2.0.lua ├── AceDebug │ └── AceDebug-2.0.lua ├── AceModuleCore-2.0 │ └── AceModuleCore-2.0.lua ├── AceLocale-2.2 │ └── AceLocale-2.2.lua ├── AceHook-2.1 │ └── AceHook-2.1.lua └── AceLibrary │ └── AceLibrary.lua ├── spells ├── warrior.lua ├── mage.lua ├── shaman.lua ├── shared_debuffs.lua ├── hunter.lua ├── priest.lua ├── rogue.lua ├── warlock.lua └── druid.lua ├── global.lua ├── LICENSE ├── Cursive.toc ├── core.lua ├── filter.lua ├── utils.lua ├── README.md ├── settings.lua ├── Localization.lua ├── commands.lua └── curses.lua /img/config.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepopo978/Cursive/HEAD/img/config.tga -------------------------------------------------------------------------------- /sounds/resist.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepopo978/Cursive/HEAD/sounds/resist.mp3 -------------------------------------------------------------------------------- /img/target-left.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepopo978/Cursive/HEAD/img/target-left.tga -------------------------------------------------------------------------------- /img/target-right.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepopo978/Cursive/HEAD/img/target-right.tga -------------------------------------------------------------------------------- /sounds/expiring.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepopo978/Cursive/HEAD/sounds/expiring.mp3 -------------------------------------------------------------------------------- /libs/AceAddon-2.0/AceAddon-2.0.lua: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepopo978/Cursive/HEAD/libs/AceAddon-2.0/AceAddon-2.0.lua -------------------------------------------------------------------------------- /spells/warrior.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getWarriorSpells() 3 | return { 4 | [772] = { name = L["rend"], rank = 1, duration = 9 }, 5 | [6546] = { name = L["rend"], rank = 2, duration = 12 }, 6 | [6547] = { name = L["rend"], rank = 3, duration = 15 }, 7 | [6548] = { name = L["rend"], rank = 4, duration = 18 }, 8 | [11572] = { name = L["rend"], rank = 5, duration = 21 }, 9 | [11573] = { name = L["rend"], rank = 6, duration = 21 }, 10 | [11574] = { name = L["rend"], rank = 7, duration = 21 }, 11 | } 12 | end 13 | -------------------------------------------------------------------------------- /spells/mage.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getMageSpells() 3 | return { 4 | [118] = { name = L["polymorph"], rank = 1, duration = 20 }, 5 | [12824] = { name = L["polymorph"], rank = 2, duration = 30 }, 6 | [12825] = { name = L["polymorph"], rank = 3, duration = 40 }, 7 | [12826] = { name = L["polymorph"], rank = 4, duration = 50 }, 8 | 9 | [28270] = { name = L["polymorph: cow"], rank = 1, duration = 50 }, 10 | [28271] = { name = L["polymorph: turtle"], rank =1, duration = 50 }, 11 | [28272] = { name = L["polymorph: pig"], rank = 1, duration = 50 }, 12 | } 13 | end 14 | -------------------------------------------------------------------------------- /spells/shaman.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getShamanSpells() 3 | return { 4 | [8050] = { name = L["flame shock"], rank = 1, duration = 12, variableDuration = true }, 5 | [8052] = { name = L["flame shock"], rank = 2, duration = 12, variableDuration = true }, 6 | [8053] = { name = L["flame shock"], rank = 3, duration = 12, variableDuration = true }, 7 | [10447] = { name = L["flame shock"], rank = 4, duration = 12, variableDuration = true }, 8 | [10448] = { name = L["flame shock"], rank = 5, duration = 12, variableDuration = true }, 9 | [29228] = { name = L["flame shock"], rank = 6, duration = 12, variableDuration = true }, 10 | } 11 | end 12 | -------------------------------------------------------------------------------- /global.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | Cursive = AceLibrary("AceAddon-2.0"):new("AceEvent-2.0", "AceDebug-2.0", "AceModuleCore-2.0", "AceConsole-2.0", "AceDB-2.0", "AceHook-2.1") 3 | Cursive.superwow = true 4 | 5 | if not GetPlayerBuffID or not CombatLogAdd or not SpellInfo then 6 | local notify = CreateFrame("Frame", "CursiveNoSuperwow", UIParent) 7 | notify:SetScript("OnUpdate", function() 8 | DEFAULT_CHAT_FRAME:AddMessage(L["|cffffcc00Cursive:|cffffaaaa Couldn't detect SuperWoW."]) 9 | this:Hide() 10 | end) 11 | 12 | Cursive.superwow = false 13 | end 14 | 15 | function Cursive:OnEnable() 16 | if not Cursive.superwow then 17 | return 18 | end 19 | 20 | DEFAULT_CHAT_FRAME:AddMessage(L["|cffffcc00Cursive:|cffffaaaa Loaded. /cursive for commands and minimap icon for options."]) 21 | 22 | Cursive.curses:LoadCurses() 23 | if Cursive.db.profile.enabled then 24 | Cursive.core.enable() 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spells/shared_debuffs.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getSharedDebuffs() 3 | return { 4 | faeriefire = { 5 | [770] = { name = L["faerie fire"], rank = 1, duration = 40 }, 6 | [778] = { name = L["faerie fire"], rank = 2, duration = 40 }, 7 | [9749] = { name = L["faerie fire"], rank = 3, duration = 40 }, 8 | [9907] = { name = L["faerie fire"], rank = 4, duration = 40 }, 9 | 10 | [16855] = { name = L["faerie fire"], rank = 1, duration = 40 }, -- use faerie fire instead of (bear) version so they block each other 11 | [17387] = { name = L["faerie fire"], rank = 2, duration = 40 }, 12 | [17388] = { name = L["faerie fire"], rank = 3, duration = 40 }, 13 | [17389] = { name = L["faerie fire"], rank = 4, duration = 40 }, 14 | 15 | [16857] = { name = L["faerie fire"], rank = 1, duration = 40 }, -- use faerie fire instead of (feral) version so they block each other 16 | [17390] = { name = L["faerie fire"], rank = 2, duration = 40 }, 17 | [17391] = { name = L["faerie fire"], rank = 3, duration = 40 }, 18 | [17392] = { name = L["faerie fire"], rank = 4, duration = 40 }, 19 | } 20 | } 21 | end 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Eric Mauser (Shagu) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cursive.toc: -------------------------------------------------------------------------------- 1 | ## Interface: 11200 2 | ## Title: [|CFF33AAFFSUPERWOW|R] Cursive 2.6.0 3 | ## Title-zhCN: [|CFF33AAFFSUPERWOW|R] Cursive 2.6.0 4 | ## Author: Pepopo 5 | ## Notes: Uses SuperWoW GUIDs to show units in combat and track curses 6 | ## Notes-zhCN: 使用SuperWoW的GUID功能来显示战斗中的单位并监视诅咒等 7 | ## Version: 1.4.0 8 | ## SavedVariablesPerCharacter: CursiveDB 9 | ## X-Embeds: Ace2, FuBarPlugin-2.0, CompostLib, DewDropLib, TabletLib 10 | 11 | Libs\AceLibrary\AceLibrary.lua 12 | 13 | Libs\AceOO-2.0\AceOO-2.0.lua 14 | Libs\AceAddon-2.0\AceAddon-2.0.lua 15 | Libs\AceConsole\AceConsole-2.0.lua 16 | Libs\AceHook-2.1\AceHook-2.1.lua 17 | Libs\AceDB-2.0\AceDB-2.0.lua 18 | Libs\AceDebug\AceDebug-2.0.lua 19 | Libs\AceEvent-2.0\AceEvent-2.0.lua 20 | Libs\AceLocale-2.2\AceLocale-2.2.lua 21 | Libs\AceModuleCore-2.0\AceModuleCore-2.0.lua 22 | Libs\Dewdrop\Dewdrop-2.0.lua 23 | Libs\Tablet\Tablet-2.0.lua 24 | Libs\FuBarPlugin\FuBarPlugin-2.0.lua 25 | 26 | Localization.lua 27 | global.lua 28 | utils.lua 29 | settings.lua 30 | core.lua 31 | spells\shared_debuffs.lua 32 | spells\druid.lua 33 | spells\hunter.lua 34 | spells\mage.lua 35 | spells\priest.lua 36 | spells\rogue.lua 37 | spells\shaman.lua 38 | spells\warlock.lua 39 | spells\warrior.lua 40 | curses.lua 41 | filter.lua 42 | ui.lua 43 | commands.lua 44 | -------------------------------------------------------------------------------- /core.lua: -------------------------------------------------------------------------------- 1 | if not Cursive.superwow then 2 | return 3 | end 4 | 5 | -- add (1) for first stack of buffs/debuffs 6 | -- other addons already do this, avoid having to parse both formats 7 | AURAADDEDOTHERHELPFUL = "%s gains %s (1)." 8 | AURAADDEDOTHERHARMFUL = "%s is afflicted by %s (1)." 9 | AURAADDEDSELFHARMFUL = "You are afflicted by %s (1)." 10 | AURAADDEDSELFHELPFUL = "You gain %s (1)." 11 | 12 | Cursive.core = CreateFrame("Frame", "Cursive", UIParent) 13 | 14 | Cursive.core.tooltipScan = CreateFrame("GameTooltip", "CursiveTooltipScan", UIParent, "GameTooltipTemplate") 15 | 16 | Cursive.core.guids = {} 17 | 18 | Cursive.core.add = function(unit) 19 | local _, guid = UnitExists(unit) 20 | 21 | if guid and not UnitIsDead(unit) then 22 | Cursive.core.guids[guid] = GetTime() 23 | end 24 | end 25 | 26 | Cursive.core.addGuid = function(guid) 27 | -- check if first two characters are 0x 28 | if string.sub(guid, 1, 2) ~= "0x" then 29 | return 30 | end 31 | if UnitExists(guid) and not UnitIsDead(guid) then 32 | Cursive.core.guids[guid] = GetTime() 33 | end 34 | end 35 | 36 | Cursive.core.remove = function(guid) 37 | Cursive.core.guids[guid] = nil 38 | end 39 | 40 | Cursive.core.enable = function() 41 | -- unitstr 42 | Cursive.core:RegisterEvent("PLAYER_TARGET_CHANGED") 43 | -- arg1 44 | Cursive.core:RegisterEvent("UNIT_COMBAT") -- this can get called with player/target/raid1 etc 45 | Cursive.core:RegisterEvent("UNIT_MODEL_CHANGED") 46 | end 47 | 48 | Cursive.core.disable = function() 49 | Cursive.core:UnregisterAllEvents() 50 | Cursive.core.guids = {} 51 | end 52 | 53 | Cursive.core:SetScript("OnEvent", function() 54 | if event == "PLAYER_TARGET_CHANGED" then 55 | this.add("target") 56 | else 57 | -- arg1 is guid 58 | this.addGuid(arg1) 59 | end 60 | end) 61 | -------------------------------------------------------------------------------- /spells/hunter.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getHunterSpells() 3 | return { 4 | [3043] = { name = L["scorpid sting"], rank = 1, duration = 20 }, 5 | [14275] = { name = L["scorpid sting"], rank = 2, duration = 20 }, 6 | [14276] = { name = L["scorpid sting"], rank = 3, duration = 20 }, 7 | [14277] = { name = L["scorpid sting"], rank = 4, duration = 20 }, 8 | 9 | [1978] = { name = L["serpent sting"], rank = 1, duration = 15 }, 10 | [13549] = { name = L["serpent sting"], rank = 2, duration = 15 }, 11 | [13550] = { name = L["serpent sting"], rank = 3, duration = 15 }, 12 | [13551] = { name = L["serpent sting"], rank = 4, duration = 15 }, 13 | [13552] = { name = L["serpent sting"], rank = 5, duration = 15 }, 14 | [13553] = { name = L["serpent sting"], rank = 6, duration = 15 }, 15 | [13554] = { name = L["serpent sting"], rank = 7, duration = 15 }, 16 | [13555] = { name = L["serpent sting"], rank = 8, duration = 15 }, 17 | [25295] = { name = L["serpent sting"], rank = 9, duration = 15 }, 18 | 19 | [3034] = { name = L["viper sting"], rank = 1, duration = 8 }, 20 | [14279] = { name = L["viper sting"], rank = 2, duration = 8 }, 21 | [14280] = { name = L["viper sting"], rank = 3, duration = 8 }, 22 | 23 | [2974] = { name = L["wing clip"], rank = 1, duration = 10 }, 24 | [14267] = { name = L["wing clip"], rank = 2, duration = 10 }, 25 | [14268] = { name = L["wing clip"], rank = 3, duration = 10 }, 26 | 27 | [5116] = { name = L["concussive shot"], rank = 1, duration = 4 }, 28 | 29 | [19386] = { name = L["wyvern sting"], rank = 1, duration = 12 }, 30 | [24132] = { name = L["wyvern sting"], rank = 2, duration = 12 }, 31 | [24133] = { name = L["wyvern sting"], rank = 3, duration = 12 }, 32 | 33 | [19306] = { name = L["counterattack"], rank = 1, duration = 5 }, 34 | [20909] = { name = L["counterattack"], rank = 2, duration = 5 }, 35 | [20910] = { name = L["counterattack"], rank = 3, duration = 5 }, 36 | 37 | [1130] = { name = L["hunter's mark"], rank = 1, duration = 120 }, 38 | [14323] = { name = L["hunter's mark"], rank = 2, duration = 120 }, 39 | [14324] = { name = L["hunter's mark"], rank = 3, duration = 120 }, 40 | [14325] = { name = L["hunter's mark"], rank = 4, duration = 120 }, 41 | } 42 | end 43 | -------------------------------------------------------------------------------- /spells/priest.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getPriestSpells() 3 | return { 4 | [1425] = { name = L["shackle undead"], rank = 1, duration = 30 }, 5 | [9486] = { name = L["shackle undead"], rank = 2, duration = 40 }, 6 | [10956] = { name = L["shackle undead"], rank = 3, duration = 50 }, 7 | 8 | [453] = { name = L["mind soothe"], rank = 1, duration = 15 }, 9 | [8192] = { name = L["mind soothe"], rank = 2, duration = 15 }, 10 | [10953] = { name = L["mind soothe"], rank = 3, duration = 15 }, 11 | 12 | [605] = { name = L["mind control"], rank = 1, duration = 60 }, 13 | [10911] = { name = L["mind control"], rank = 2, duration = 30 }, 14 | [10912] = { name = L["mind control"], rank = 3, duration = 30 }, 15 | 16 | [2944] = { name = L["devouring plague"], rank = 1, duration = 24 }, 17 | [19276] = { name = L["devouring plague"], rank = 2, duration = 24 }, 18 | [19277] = { name = L["devouring plague"], rank = 3, duration = 24 }, 19 | [19278] = { name = L["devouring plague"], rank = 4, duration = 24 }, 20 | [19279] = { name = L["devouring plague"], rank = 5, duration = 24 }, 21 | [19280] = { name = L["devouring plague"], rank = 6, duration = 24 }, 22 | 23 | [9035] = { name = L["hex of weakness"], rank = 1, duration = 120 }, 24 | [19281] = { name = L["hex of weakness"], rank = 2, duration = 120 }, 25 | [19282] = { name = L["hex of weakness"], rank = 3, duration = 120 }, 26 | [19283] = { name = L["hex of weakness"], rank = 4, duration = 120 }, 27 | [19284] = { name = L["hex of weakness"], rank = 5, duration = 120 }, 28 | [19285] = { name = L["hex of weakness"], rank = 6, duration = 120 }, 29 | 30 | [589] = { name = L["shadow word: pain"], rank = 1, duration = 24, variableDuration = true }, -- assume they have talent 31 | [594] = { name = L["shadow word: pain"], rank = 2, duration = 24, variableDuration = true }, 32 | [970] = { name = L["shadow word: pain"], rank = 3, duration = 24, variableDuration = true }, 33 | [992] = { name = L["shadow word: pain"], rank = 4, duration = 24, variableDuration = true }, 34 | [2767] = { name = L["shadow word: pain"], rank = 5, duration = 24, variableDuration = true }, 35 | [10892] = { name = L["shadow word: pain"], rank = 6, duration = 24, variableDuration = true }, 36 | [10893] = { name = L["shadow word: pain"], rank = 7, duration = 24, variableDuration = true }, 37 | [10894] = { name = L["shadow word: pain"], rank = 8, duration = 24, variableDuration = true }, 38 | 39 | [15286] = { name = L["vampiric embrace"], rank = 1, duration = 60 }, 40 | 41 | [14914] = { name = L["holy fire"], rank = 1, duration = 10 }, 42 | [15262] = { name = L["holy fire"], rank = 2, duration = 10 }, 43 | [15263] = { name = L["holy fire"], rank = 3, duration = 10 }, 44 | [15264] = { name = L["holy fire"], rank = 4, duration = 10 }, 45 | [15265] = { name = L["holy fire"], rank = 5, duration = 10 }, 46 | [15266] = { name = L["holy fire"], rank = 6, duration = 10 }, 47 | [15267] = { name = L["holy fire"], rank = 7, duration = 10 }, 48 | [15261] = { name = L["holy fire"], rank = 8, duration = 10 }, 49 | } 50 | end 51 | -------------------------------------------------------------------------------- /spells/rogue.lua: -------------------------------------------------------------------------------- 1 | function getGougeDuration() 2 | -- Improved Gouge: +.5s per talent 3 | local _, _, _, _, count = GetTalentInfo(2, 1) 4 | if count and count > 0 then 5 | return 4 + (count * .5) 6 | end 7 | 8 | return 4 9 | end 10 | 11 | function getRuptureDuration() 12 | local duration = 6 + Cursive.curses.GetComboPointsUsed() * 2 13 | local _, _, _, _, count = GetTalentInfo(1, 10) 14 | if count and count > 0 then 15 | return duration + (count * 2) 16 | end 17 | 18 | return duration 19 | end 20 | 21 | function getKidneyShotDuration() 22 | return 1 + Cursive.curses.GetComboPointsUsed() 23 | end 24 | 25 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 26 | function getRogueSpells() 27 | return { 28 | [2094] = { name = L["blind"], rank = 1, duration = 10 }, 29 | [21060] = { name = L["blind"], rank = 1, duration = 10 }, 30 | 31 | [6770] = { name = L["sap"], rank = 1, duration = 25 }, 32 | [2070] = { name = L["sap"], rank = 2, duration = 35 }, 33 | [11297] = { name = L["sap"], rank = 3, duration = 45 }, 34 | 35 | [1776] = { name = L["gouge"], rank = 1, duration = 4, calculateDuration = getGougeDuration }, 36 | [1777] = { name = L["gouge"], rank = 2, duration = 4, calculateDuration = getGougeDuration }, 37 | [8629] = { name = L["gouge"], rank = 3, duration = 4, calculateDuration = getGougeDuration }, 38 | [11285] = { name = L["gouge"], rank = 4, duration = 4, calculateDuration = getGougeDuration }, 39 | [11286] = { name = L["gouge"], rank = 5, duration = 4, calculateDuration = getGougeDuration }, 40 | 41 | [1943] = { name = L["rupture"], rank = 1, duration = 6, calculateDuration = getRuptureDuration }, 42 | [8639] = { name = L["rupture"], rank = 2, duration = 6, calculateDuration = getRuptureDuration }, 43 | [8640] = { name = L["rupture"], rank = 3, duration = 6, calculateDuration = getRuptureDuration }, 44 | [11273] = { name = L["rupture"], rank = 4, duration = 6, calculateDuration = getRuptureDuration }, 45 | [11274] = { name = L["rupture"], rank = 5, duration = 6, calculateDuration = getRuptureDuration }, 46 | [11275] = { name = L["rupture"], rank = 6, duration = 6, calculateDuration = getRuptureDuration }, 47 | 48 | [408] = { name = L["kidney shot"], rank = 1, duration = 1, calculateDuration = getKidneyShotDuration }, 49 | [8643] = { name = L["kidney shot"], rank = 2, duration = 1, calculateDuration = getKidneyShotDuration }, 50 | 51 | [8647] = { name = L["expose armor"], rank = 1, duration = 30 }, -- this is a shared debuff but almost always only done by 1 rogue 52 | [8649] = { name = L["expose armor"], rank = 2, duration = 30 }, 53 | [8650] = { name = L["expose armor"], rank = 3, duration = 30 }, 54 | [11197] = { name = L["expose armor"], rank = 4, duration = 30 }, 55 | [11198] = { name = L["expose armor"], rank = 5, duration = 30 }, 56 | 57 | [703] = { name = L["garrote"], rank = 1, duration = 18 }, 58 | [8631] = { name = L["garrote"], rank = 2, duration = 18 }, 59 | [8632] = { name = L["garrote"], rank = 3, duration = 18 }, 60 | [8633] = { name = L["garrote"], rank = 4, duration = 18 }, 61 | [11289] = { name = L["garrote"], rank = 5, duration = 18 }, 62 | [11290] = { name = L["garrote"], rank = 6, duration = 18 }, 63 | 64 | [2818] = { name = L["deadly poison"], rank = 1, duration = 12 }, 65 | [2819] = { name = L["deadly poison II"], rank = 2, duration = 12 }, 66 | [11353] = { name = L["deadly poison III"], rank = 3, duration = 12 }, 67 | [11354] = { name = L["deadly poison IV"], rank = 4, duration = 12 }, 68 | [25349] = { name = L["deadly poison V"], rank = 5, duration = 12 }, 69 | 70 | [16511] = { name = L["hemorrhage"], rank = 1, duration = 15 }, 71 | 72 | [1833] = { name = L["cheap shot"], rank = 1, duration = 4 }, 73 | [14902] = { name = L["cheap shot"], rank = 1, duration = 4 }, 74 | } 75 | end 76 | -------------------------------------------------------------------------------- /filter.lua: -------------------------------------------------------------------------------- 1 | if not Cursive.superwow then 2 | return 3 | end 4 | 5 | local filter = { 6 | } 7 | 8 | filter.attackable = function(unit) 9 | return UnitCanAttack("player", unit) and true or false 10 | end 11 | 12 | filter.player = function(unit) 13 | return UnitIsPlayer(unit) and true or false 14 | end 15 | 16 | filter.notplayer = function(unit) 17 | return not UnitIsPlayer(unit) and true or false 18 | end 19 | 20 | filter.infight = function(unit) 21 | return UnitAffectingCombat(unit) and true or false 22 | end 23 | 24 | filter.hascurse = function(unit) 25 | return Cursive.curses:HasAnyCurse(unit) and true or false 26 | end 27 | 28 | filter.alive = function(unit) 29 | return not UnitIsDead(unit) and true or false 30 | end 31 | 32 | filter.range = function(unit) 33 | if IsSpellInRange then 34 | -- 16707 is hex which has 45 yd range 35 | return IsSpellInRange(16707, unit) == 1 and true or false 36 | else 37 | return CheckInteractDistance(unit, 4) and true or false 38 | end 39 | end 40 | 41 | filter.icon = function(unit) 42 | return GetRaidTargetIndex(unit) and true or false 43 | end 44 | 45 | filter.normal = function(unit) 46 | local elite = UnitClassification(unit) 47 | return elite == "normal" and true or false 48 | end 49 | 50 | filter.elite = function(unit) 51 | local elite = UnitClassification(unit) 52 | return (elite == "elite" or elite == "rareelite") and true or false 53 | end 54 | 55 | filter.hostile = function(unit) 56 | return UnitIsEnemy("player", unit) and true or false 57 | end 58 | 59 | filter.notignored = function(unit) 60 | if not Cursive.db.profile.ignorelist or table.getn(Cursive.db.profile.ignorelist) == 0 then 61 | return true 62 | end 63 | 64 | local unitName = UnitName(unit) 65 | if not unitName then 66 | return true 67 | end 68 | for _, str in ipairs(Cursive.db.profile.ignorelist) do 69 | if string.find(string.lower(unitName), string.lower(str), nil, not Cursive.db.profile.ignorelistuseregex) then 70 | return false 71 | end 72 | end 73 | return true 74 | end 75 | 76 | Cursive.filter = filter 77 | 78 | function Cursive:ShouldDisplayGuid(guid) 79 | -- never display units that don't exist 80 | if not UnitExists(guid) then 81 | return false 82 | end 83 | 84 | -- never display dead units 85 | if not Cursive.filter.alive(guid) then 86 | return false 87 | end 88 | 89 | local _, targetGuid = UnitExists("target") 90 | 91 | -- always show target if attackable 92 | if (targetGuid == guid) and filter.attackable(guid) then 93 | return true 94 | end 95 | 96 | -- always show raid marks if attackable and not in combat or this guid is affecting combat 97 | if filter.icon(guid) and filter.attackable(guid) and (not UnitAffectingCombat("player") or UnitAffectingCombat(guid)) then 98 | return true 99 | end 100 | 101 | if Cursive.db.profile.filterincombat and not filter.infight(guid) then 102 | return false 103 | end 104 | 105 | if Cursive.db.profile.filterhascurse and not filter.hascurse(guid) then 106 | return false 107 | end 108 | 109 | if Cursive.db.profile.filterhostile and not filter.hostile(guid) then 110 | return false 111 | end 112 | 113 | if Cursive.db.profile.filterattackable and not filter.attackable(guid) then 114 | return false 115 | end 116 | 117 | if Cursive.db.profile.filterrange and not filter.range(guid) then 118 | return false 119 | end 120 | 121 | if Cursive.db.profile.filterraidmark and not filter.icon(guid) then 122 | return false 123 | end 124 | 125 | if Cursive.db.profile.filterplayer and not filter.player(guid) then 126 | return false 127 | end 128 | 129 | if Cursive.db.profile.filternotplayer and not filter.notplayer(guid) then 130 | return false 131 | end 132 | 133 | if Cursive.db.profile.filterignored and not filter.notignored(guid) then 134 | return false 135 | end 136 | 137 | return true 138 | end 139 | -------------------------------------------------------------------------------- /spells/warlock.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | function getWarlockSpells() 3 | return { 4 | [172] = { name = L["corruption"], rank = 1, duration = 12, variableDuration = true, darkHarvest = true, numTicks=4}, 5 | [6222] = { name = L["corruption"], rank = 2, duration = 15, variableDuration = true, darkHarvest = true, numTicks=5 }, 6 | [6223] = { name = L["corruption"], rank = 3, duration = 18, variableDuration = true, darkHarvest = true, numTicks=6 }, 7 | [7648] = { name = L["corruption"], rank = 4, duration = 18, variableDuration = true, darkHarvest = true, numTicks=6 }, 8 | [11671] = { name = L["corruption"], rank = 5, duration = 18, variableDuration = true, darkHarvest = true, numTicks=6 }, 9 | [11672] = { name = L["corruption"], rank = 6, duration = 18, variableDuration = true, darkHarvest = true, numTicks=6 }, 10 | [25311] = { name = L["corruption"], rank = 7, duration = 18, variableDuration = true, darkHarvest = true, numTicks=6 }, 11 | 12 | [980] = { name = L["curse of agony"], rank = 1, duration = 24, variableDuration = true, darkHarvest = true, numTicks=12 }, 13 | [1014] = { name = L["curse of agony"], rank = 2, duration = 24, variableDuration = true, darkHarvest = true, numTicks=12 }, 14 | [6217] = { name = L["curse of agony"], rank = 3, duration = 24, variableDuration = true, darkHarvest = true, numTicks=12 }, 15 | [11711] = { name = L["curse of agony"], rank = 4, duration = 24, variableDuration = true, darkHarvest = true, numTicks=12 }, 16 | [11712] = { name = L["curse of agony"], rank = 5, duration = 24, variableDuration = true, darkHarvest = true, numTicks=12 }, 17 | [11713] = { name = L["curse of agony"], rank = 6, duration = 24, variableDuration = true, darkHarvest = true, numTicks=12 }, 18 | 19 | [18265] = { name = L["siphon life"], rank = 1, duration = 30, variableDuration = true, darkHarvest = true, numTicks=10 }, 20 | [18879] = { name = L["siphon life"], rank = 2, duration = 30, variableDuration = true, darkHarvest = true, numTicks=10 }, 21 | [18880] = { name = L["siphon life"], rank = 3, duration = 30, variableDuration = true, darkHarvest = true, numTicks=10 }, 22 | [18881] = { name = L["siphon life"], rank = 4, duration = 30, variableDuration = true, darkHarvest = true, numTicks=10 }, 23 | 24 | [52550] = { name = L["dark harvest"], rank = 1, duration = 8, variableDuration = true }, 25 | [52551] = { name = L["dark harvest"], rank = 2, duration = 8, variableDuration = true }, 26 | [52552] = { name = L["dark harvest"], rank = 3, duration = 8, variableDuration = true }, 27 | 28 | [603] = { name = L["curse of doom"], rank = 1, duration = 60 }, 29 | 30 | [704] = { name = L["curse of recklessness"], rank = 1, duration = 120 }, 31 | [7658] = { name = L["curse of recklessness"], rank = 2, duration = 120 }, 32 | [7659] = { name = L["curse of recklessness"], rank = 3, duration = 120 }, 33 | [11717] = { name = L["curse of recklessness"], rank = 4, duration = 120 }, 34 | 35 | [17862] = { name = L["curse of shadow"], rank = 1, duration = 300 }, 36 | [17937] = { name = L["curse of shadow"], rank = 2, duration = 300 }, 37 | 38 | [1490] = { name = L["curse of the elements"], rank = 1, duration = 300 }, 39 | [11721] = { name = L["curse of the elements"], rank = 2, duration = 300 }, 40 | [11722] = { name = L["curse of the elements"], rank = 3, duration = 300 }, 41 | 42 | [1714] = { name = L["curse of tongues"], rank = 1, duration = 30 }, 43 | [11719] = { name = L["curse of tongues"], rank = 2, duration = 30 }, 44 | 45 | [702] = { name = L["curse of weakness"], rank = 1, duration = 120 }, 46 | [1108] = { name = L["curse of weakness"], rank = 2, duration = 120 }, 47 | [6205] = { name = L["curse of weakness"], rank = 3, duration = 120 }, 48 | [7646] = { name = L["curse of weakness"], rank = 4, duration = 120 }, 49 | [11707] = { name = L["curse of weakness"], rank = 5, duration = 120 }, 50 | [11708] = { name = L["curse of weakness"], rank = 6, duration = 120 }, 51 | 52 | [18223] = { name = L["curse of exhaustion"], rank = 1, duration = 12 }, 53 | 54 | [348] = { name = L["immolate"], rank = 1, duration = 15 }, 55 | [707] = { name = L["immolate"], rank = 2, duration = 15 }, 56 | [1094] = { name = L["immolate"], rank = 3, duration = 15 }, 57 | [2941] = { name = L["immolate"], rank = 4, duration = 15 }, 58 | [11665] = { name = L["immolate"], rank = 5, duration = 15 }, 59 | [11667] = { name = L["immolate"], rank = 6, duration = 15 }, 60 | [11668] = { name = L["immolate"], rank = 7, duration = 15 }, 61 | [25309] = { name = L["immolate"], rank = 8, duration = 15 }, 62 | 63 | [6789] = { name = L["death coil"], rank = 1, duration = 3 }, 64 | [17925] = { name = L["death coil"], rank = 2, duration = 3 }, 65 | [17926] = { name = L["death coil"], rank = 3, duration = 3 }, 66 | 67 | [710] = { name = L["banish"], rank = 1, duration = 20 }, 68 | [18647] = { name = L["banish"], rank = 2, duration = 30 }, 69 | 70 | [5782] = { name = L["fear"], rank = 1, duration = 10 }, 71 | [6213] = { name = L["fear"], rank = 2, duration = 15 }, 72 | [6215] = { name = L["fear"], rank = 3, duration = 20 }, 73 | } 74 | end 75 | -------------------------------------------------------------------------------- /spells/druid.lua: -------------------------------------------------------------------------------- 1 | function getRipDuration() 2 | -- check if using black morass idol 3 | local _, _, id = strfind(GetInventoryItemLink("player", 18) or "", "item:(%d+)") 4 | if id == "61699" then 5 | -- 10% less duration 6 | return (8 + Cursive.curses.GetComboPointsUsed() * 2) * 0.9 7 | end 8 | 9 | return 8 + Cursive.curses.GetComboPointsUsed() * 2 10 | end 11 | 12 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 13 | function getDruidSpells() 14 | return { 15 | [339] = { name = L["entangling roots"], rank = 1, duration = 12 }, 16 | [1062] = { name = L["entangling roots"], rank = 2, duration = 15 }, 17 | [5195] = { name = L["entangling roots"], rank = 3, duration = 18 }, 18 | [5196] = { name = L["entangling roots"], rank = 4, duration = 21 }, 19 | [9852] = { name = L["entangling roots"], rank = 5, duration = 24 }, 20 | [9853] = { name = L["entangling roots"], rank = 6, duration = 27 }, 21 | 22 | [700] = { name = L["sleep"], rank = 1, duration = 20 }, 23 | [1090] = { name = L["sleep"], rank = 2, duration = 30 }, 24 | [2937] = { name = L["sleep"], rank = 3, duration = 40 }, 25 | 26 | [770] = { name = L["faerie fire"], rank = 1, duration = 40 }, 27 | [778] = { name = L["faerie fire"], rank = 2, duration = 40 }, 28 | [9749] = { name = L["faerie fire"], rank = 3, duration = 40 }, 29 | [9907] = { name = L["faerie fire"], rank = 4, duration = 40 }, 30 | 31 | [16855] = { name = L["faerie fire"], rank = 1, duration = 40 }, -- use faerie fire instead of (bear) version so they block each other 32 | [17387] = { name = L["faerie fire"], rank = 2, duration = 40 }, 33 | [17388] = { name = L["faerie fire"], rank = 3, duration = 40 }, 34 | [17389] = { name = L["faerie fire"], rank = 4, duration = 40 }, 35 | 36 | [16857] = { name = L["faerie fire"], rank = 1, duration = 40 }, -- use faerie fire instead of (feral) version so they block each other 37 | [17390] = { name = L["faerie fire"], rank = 2, duration = 40 }, 38 | [17391] = { name = L["faerie fire"], rank = 3, duration = 40 }, 39 | [17392] = { name = L["faerie fire"], rank = 4, duration = 40 }, 40 | 41 | [2637] = { name = L["hibernate"], rank = 1, duration = 20 }, 42 | [18657] = { name = L["hibernate"], rank = 2, duration = 30 }, 43 | [18658] = { name = L["hibernate"], rank = 3, duration = 40 }, 44 | 45 | [5570] = { name = L["insect swarm"], rank = 1, duration = 18, variableDuration = true }, 46 | [24974] = { name = L["insect swarm"], rank = 2, duration = 18, variableDuration = true }, 47 | [24975] = { name = L["insect swarm"], rank = 3, duration = 18, variableDuration = true }, 48 | [24976] = { name = L["insect swarm"], rank = 4, duration = 18, variableDuration = true }, 49 | [24977] = { name = L["insect swarm"], rank = 5, duration = 18, variableDuration = true }, 50 | 51 | [8921] = { name = L["moonfire"], rank = 1, duration = 9, variableDuration = true }, 52 | [8924] = { name = L["moonfire"], rank = 2, duration = 18, variableDuration = true }, 53 | [8925] = { name = L["moonfire"], rank = 3, duration = 18, variableDuration = true }, 54 | [8926] = { name = L["moonfire"], rank = 4, duration = 18, variableDuration = true }, 55 | [8927] = { name = L["moonfire"], rank = 5, duration = 18, variableDuration = true }, 56 | [8928] = { name = L["moonfire"], rank = 6, duration = 18, variableDuration = true }, 57 | [8929] = { name = L["moonfire"], rank = 7, duration = 18, variableDuration = true }, 58 | [9833] = { name = L["moonfire"], rank = 8, duration = 18, variableDuration = true }, 59 | [9834] = { name = L["moonfire"], rank = 9, duration = 18, variableDuration = true }, 60 | [9835] = { name = L["moonfire"], rank = 10, duration = 18, variableDuration = true }, 61 | 62 | [1822] = { name = L["rake"], rank = 1, duration = 9, variableDuration = true }, 63 | [1823] = { name = L["rake"], rank = 2, duration = 9, variableDuration = true }, 64 | [1824] = { name = L["rake"], rank = 3, duration = 9, variableDuration = true }, 65 | [9904] = { name = L["rake"], rank = 4, duration = 9, variableDuration = true }, 66 | 67 | [1079] = { name = L["rip"], rank = 1, duration = 8, calculateDuration = getRipDuration }, 68 | [9492] = { name = L["rip"], rank = 2, duration = 8, calculateDuration = getRipDuration }, 69 | [9493] = { name = L["rip"], rank = 3, duration = 8, calculateDuration = getRipDuration }, 70 | [9752] = { name = L["rip"], rank = 4, duration = 8, calculateDuration = getRipDuration }, 71 | [9894] = { name = L["rip"], rank = 5, duration = 8, calculateDuration = getRipDuration }, 72 | [9896] = { name = L["rip"], rank = 6, duration = 8, calculateDuration = getRipDuration }, 73 | 74 | [2908] = { name = L["soothe animal"], rank = 1, duration = 15 }, 75 | [8955] = { name = L["soothe animal"], rank = 2, duration = 15 }, 76 | [9901] = { name = L["soothe animal"], rank = 3, duration = 15 }, 77 | 78 | [5211] = { name = L["bash"], rank = 1, duration = 2 }, 79 | [6798] = { name = L["bash"], rank = 2, duration = 3 }, 80 | [8983] = { name = L["bash"], rank = 3, duration = 4 }, 81 | 82 | [99] = { name = L["demoralizing roar"], rank = 1, duration = 30 }, 83 | [1735] = { name = L["demoralizing roar"], rank = 2, duration = 30 }, 84 | [9490] = { name = L["demoralizing roar"], rank = 3, duration = 30 }, 85 | [9747] = { name = L["demoralizing roar"], rank = 4, duration = 30 }, 86 | [9898] = { name = L["demoralizing roar"], rank = 5, duration = 30 }, 87 | 88 | [5209] = { name = L["challenging roar"], rank = 1, duration = 6 }, 89 | 90 | [9005] = { name = L["pounce bleed"], rank = 1, duration = 18 }, 91 | [9823] = { name = L["pounce bleed"], rank = 2, duration = 18 }, 92 | [9827] = { name = L["pounce bleed"], rank = 3, duration = 18 }, 93 | } 94 | end 95 | -------------------------------------------------------------------------------- /utils.lua: -------------------------------------------------------------------------------- 1 | if not Cursive.superwow then 2 | return 3 | end 4 | 5 | local utils = {} 6 | 7 | utils.strsplit = function(delimiter, subject) 8 | if not subject then 9 | return nil 10 | end 11 | local delimiter, fields = delimiter or ":", {} 12 | local pattern = string.format("([^%s]+)", delimiter) 13 | string.gsub(subject, pattern, function(c) 14 | fields[table.getn(fields) + 1] = c 15 | end) 16 | return unpack(fields) 17 | end 18 | 19 | utils.round = function(input, places) 20 | if not places then 21 | places = 0 22 | end 23 | if type(input) == "number" and type(places) == "number" then 24 | local pow = 1 25 | for i = 1, places do 26 | pow = pow * 10 27 | end 28 | return floor(input * pow + 0.5) / pow 29 | end 30 | end 31 | 32 | utils.IsValidAnchor = function(anchor) 33 | if anchor == "TOP" then 34 | return true 35 | end 36 | if anchor == "TOPLEFT" then 37 | return true 38 | end 39 | if anchor == "TOPRIGHT" then 40 | return true 41 | end 42 | if anchor == "CENTER" then 43 | return true 44 | end 45 | if anchor == "LEFT" then 46 | return true 47 | end 48 | if anchor == "RIGHT" then 49 | return true 50 | end 51 | if anchor == "BOTTOM" then 52 | return true 53 | end 54 | if anchor == "BOTTOMLEFT" then 55 | return true 56 | end 57 | if anchor == "BOTTOMRIGHT" then 58 | return true 59 | end 60 | return false 61 | end 62 | 63 | utils.GetBestAnchor = function(self) 64 | local scale = self:GetScale() 65 | local x, y = self:GetCenter() 66 | local a = GetScreenWidth() / scale / 3 67 | local b = GetScreenWidth() / scale / 3 * 2 68 | local c = GetScreenHeight() / scale / 3 * 2 69 | local d = GetScreenHeight() / scale / 3 70 | if not x or not y then 71 | return 72 | end 73 | 74 | if x < a and y > c then 75 | return "TOPLEFT" 76 | elseif x > a and x < b and y > c then 77 | return "TOP" 78 | elseif x > b and y > c then 79 | return "TOPRIGHT" 80 | elseif x < a and y > d and y < c then 81 | return "LEFT" 82 | elseif x > a and x < b and y > d and y < c then 83 | return "CENTER" 84 | elseif x > b and y > d and y < c then 85 | return "RIGHT" 86 | elseif x < a and y < d then 87 | return "BOTTOMLEFT" 88 | elseif x > a and x < b and y < d then 89 | return "BOTTOM" 90 | elseif x > b and y < d then 91 | return "BOTTOMRIGHT" 92 | end 93 | end 94 | 95 | utils.ConvertFrameAnchor = function(self, anchor) 96 | local scale, x, y, _ = self:GetScale(), nil, nil, nil 97 | 98 | if anchor == "CENTER" then 99 | x, y = self:GetCenter() 100 | x, y = x - GetScreenWidth() / 2 / scale, y - GetScreenHeight() / 2 / scale 101 | elseif anchor == "TOPLEFT" then 102 | x, y = self:GetLeft(), self:GetTop() - GetScreenHeight() / scale 103 | elseif anchor == "TOP" then 104 | x, _ = self:GetCenter() 105 | x, y = x - GetScreenWidth() / 2 / scale, self:GetTop() - GetScreenHeight() / scale 106 | elseif anchor == "TOPRIGHT" then 107 | x, y = self:GetRight() - GetScreenWidth() / scale, self:GetTop() - GetScreenHeight() / scale 108 | elseif anchor == "RIGHT" then 109 | _, y = self:GetCenter() 110 | x, y = self:GetRight() - GetScreenWidth() / scale, y - GetScreenHeight() / 2 / scale 111 | elseif anchor == "BOTTOMRIGHT" then 112 | x, y = self:GetRight() - GetScreenWidth() / scale, self:GetBottom() 113 | elseif anchor == "BOTTOM" then 114 | x, _ = self:GetCenter() 115 | x, y = x - GetScreenWidth() / 2 / scale, self:GetBottom() 116 | elseif anchor == "BOTTOMLEFT" then 117 | x, y = self:GetLeft(), self:GetBottom() 118 | elseif anchor == "LEFT" then 119 | _, y = self:GetCenter() 120 | x, y = self:GetLeft(), y - GetScreenHeight() / 2 / scale 121 | end 122 | 123 | return anchor, utils.round(x, 2), utils.round(y, 2) 124 | end 125 | 126 | local _r, _g, _b, _a 127 | utils.rgbhex = function(r, g, b, a) 128 | if type(r) == "table" then 129 | if r.r then 130 | _r, _g, _b, _a = r.r, r.g, r.b, (r.a or 1) 131 | elseif table.getn(r) >= 3 then 132 | _r, _g, _b, _a = r[1], r[2], r[3], (r[4] or 1) 133 | end 134 | elseif tonumber(r) then 135 | _r, _g, _b, _a = r, g, b, (a or 1) 136 | end 137 | 138 | if _r and _g and _b and _a then 139 | -- limit values to 0-1 140 | _r = _r + 0 > 1 and 1 or _r + 0 141 | _g = _g + 0 > 1 and 1 or _g + 0 142 | _b = _b + 0 > 1 and 1 or _b + 0 143 | _a = _a + 0 > 1 and 1 or _a + 0 144 | return string.format("|c%02x%02x%02x%02x", _a * 255, _r * 255, _g * 255, _b * 255) 145 | end 146 | 147 | return "" 148 | end 149 | 150 | utils.GetReactionColor = function(unitstr) 151 | local color = UnitReactionColor[UnitReaction(unitstr, "player")] 152 | local r, g, b = .8, .8, .8 153 | 154 | if color then 155 | r, g, b = color.r, color.g, color.b 156 | end 157 | 158 | return utils.rgbhex(r, g, b), r, g, b 159 | end 160 | 161 | utils.GetUnitColor = function(unitstr) 162 | local r, g, b = .8, .8, .8 163 | 164 | if UnitIsPlayer(unitstr) then 165 | local _, class = UnitClass(unitstr) 166 | 167 | if RAID_CLASS_COLORS[class] then 168 | r, g, b = RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b 169 | end 170 | else 171 | return utils.GetReactionColor(unitstr) 172 | end 173 | 174 | return utils.rgbhex(r, g, b), r, g, b 175 | end 176 | 177 | utils.GetLevelColor = function(unitstr) 178 | local color = GetDifficultyColor(UnitLevel(unitstr)) 179 | local r, g, b = .8, .8, .8 180 | 181 | if color then 182 | r, g, b = color.r, color.g, color.b 183 | end 184 | 185 | return utils.rgbhex(r, g, b), r, g, b 186 | end 187 | 188 | utils.GetLevelString = function(unitstr) 189 | local level = UnitLevel(unitstr) 190 | if level == -1 then 191 | level = "??" 192 | end 193 | 194 | local elite = UnitClassification(unitstr) 195 | if elite == "worldboss" then 196 | level = level .. "B" 197 | elseif elite == "rareelite" then 198 | level = level .. "R+" 199 | elseif elite == "elite" then 200 | level = level .. "+" 201 | elseif elite == "rare" then 202 | level = level .. "R" 203 | end 204 | 205 | return level 206 | end 207 | 208 | utils.GetLowercaseSpellNameNoRank = function(spellName) 209 | return string.lower(string.gsub(spellName, "%([rR]ank %d%)", "")) 210 | end 211 | 212 | Cursive.utils = utils 213 | -------------------------------------------------------------------------------- /libs/AceDebug/AceDebug-2.0.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Name: AceDebug-2.0 3 | Revision: $Rev: 15024 $ 4 | Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) 5 | Inspired By: Ace 1.x by Turan (turan@gryphon.com) 6 | Website: http://www.wowace.com/ 7 | Documentation: http://www.wowace.com/index.php/AceDebug-2.0 8 | SVN: http://svn.wowace.com/root/trunk/Ace2/AceDebug-2.0 9 | Description: Mixin to allow for simple debugging capabilities. 10 | Dependencies: AceLibrary, AceOO-2.0 11 | ]] 12 | 13 | local MAJOR_VERSION = "AceDebug-2.0" 14 | local MINOR_VERSION = "$Revision: 15024 $" 15 | 16 | if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end 17 | if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end 18 | 19 | if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end 20 | 21 | local DEBUGGING, TOGGLE_DEBUGGING 22 | 23 | if GetLocale() == "frFR" then 24 | DEBUGGING = "D\195\169boguage" 25 | TOGGLE_DEBUGGING = "Activer/d\195\169sactiver le d\195\169boguage" 26 | elseif GetLocale() == "deDE" then 27 | DEBUGGING = "Debuggen" 28 | TOGGLE_DEBUGGING = "Aktiviert/Deaktiviert Debugging" 29 | elseif GetLocale() == "koKR" then 30 | DEBUGGING = "디버깅" 31 | TOGGLE_DEBUGGING = "디버깅 기능 사용함/사용안함" 32 | elseif GetLocale() == "zhTW" then 33 | DEBUGGING = "除錯" 34 | TOGGLE_DEBUGGING = "啟用/停用除錯功能" 35 | elseif GetLocale() == "zhCN" then 36 | DEBUGGING = "\232\176\131\232\175\149" 37 | TOGGLE_DEBUGGING = "\229\144\175\231\148\168/\231\166\129\231\148\168 \232\176\131\232\175\149" 38 | else -- enUS 39 | DEBUGGING = "Debugging" 40 | TOGGLE_DEBUGGING = "Enable/disable debugging" 41 | end 42 | 43 | local table_setn 44 | do 45 | local version = GetBuildInfo() 46 | if string.find(version, "^2%.") then 47 | -- 2.0.0 48 | table_setn = function() end 49 | else 50 | table_setn = table.setn 51 | end 52 | end 53 | 54 | local math_mod = math.mod or math.fmod 55 | 56 | local AceOO = AceLibrary:GetInstance("AceOO-2.0") 57 | local AceDebug = AceOO.Mixin {"Debug", "CustomDebug", "IsDebugging", "SetDebugging", "SetDebugLevel", "LevelDebug", "CustomLevelDebug", "GetDebugLevel"} 58 | 59 | local function print(text, r, g, b, frame, delay) 60 | (frame or DEFAULT_CHAT_FRAME):AddMessage(text, r, g, b, 1, delay or 5) 61 | end 62 | 63 | local tmp 64 | 65 | 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) 66 | if not self.debugging then return end 67 | 68 | local output = string.format("|cff7fff7f(DEBUG) %s:[%s%3d]|r", tostring(self), date("%H:%M:%S"), math_mod(GetTime(), 1) * 1000) 69 | 70 | if string.find(tostring(a1), "%%") then 71 | 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)) 72 | else 73 | if not tmp then 74 | tmp = {} 75 | end 76 | 77 | -- This block dynamically rebuilds the tmp array stopping on the first nil. 78 | table.insert(tmp, output) 79 | 80 | table.insert(tmp, tostring(a1)) 81 | table.insert(tmp, a2) 82 | table.insert(tmp, a3) 83 | table.insert(tmp, a4) 84 | table.insert(tmp, a5) 85 | table.insert(tmp, a6) 86 | table.insert(tmp, a7) 87 | table.insert(tmp, a8) 88 | table.insert(tmp, a9) 89 | table.insert(tmp, a10) 90 | table.insert(tmp, a11) 91 | table.insert(tmp, a12) 92 | table.insert(tmp, a13) 93 | table.insert(tmp, a14) 94 | table.insert(tmp, a15) 95 | table.insert(tmp, a16) 96 | table.insert(tmp, a17) 97 | table.insert(tmp, a18) 98 | table.insert(tmp, a19) 99 | table.insert(tmp, a20) 100 | while tmp[table.getn(tmp)] == nil do 101 | table.remove(tmp) 102 | end 103 | for k = 1, table.getn(tmp) do 104 | tmp[k] = tostring(tmp[k]) 105 | end 106 | 107 | output = table.concat(tmp, " ") 108 | 109 | for k,v in pairs(tmp) do 110 | tmp[k] = nil 111 | end 112 | table_setn(tmp, 0) 113 | end 114 | 115 | print(output, r, g, b, frame or self.debugFrame, delay) 116 | end 117 | 118 | function AceDebug:Debug(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) 119 | 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) 120 | end 121 | 122 | function AceDebug:IsDebugging() 123 | return self.debugging 124 | end 125 | 126 | function AceDebug:SetDebugging(debugging) 127 | self.debugging = debugging 128 | end 129 | 130 | -- Takes a number 1-3 131 | -- Level 1: Critical messages that every user should receive 132 | -- Level 2: Should be used for local debugging (function calls, etc) 133 | -- Level 3: Very verbose debugging, will dump everything and anything 134 | -- If set to nil, you will receive no debug information 135 | function AceDebug:SetDebugLevel(level) 136 | AceDebug:argCheck(level, 1, "number", "nil") 137 | if not level then 138 | self.debuglevel = nil 139 | return 140 | end 141 | if level < 1 or level > 3 then 142 | AceDebug:error("Bad argument #1 to `SetDebugLevel`, must be a number 1-3") 143 | end 144 | self.debuglevel = level 145 | end 146 | 147 | function AceDebug:GetDebugLevel() 148 | return self.debuglevel 149 | end 150 | 151 | 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) 152 | if not self.debugging or not self.debuglevel then return end 153 | AceDebug:argCheck(level, 1, "number") 154 | if level < 1 or level > 3 then 155 | AceDebug:error("Bad argument #1 to `LevelDebug`, must be a number 1-3") 156 | end 157 | if level > self.debuglevel then return end 158 | 159 | local output = string.format("|cff7fff7f(DEBUG) %s:[%s.%3d]|r", tostring(self), date("%H:%M:%S"), math_mod(GetTime(), 1) * 1000) 160 | 161 | if string.find(tostring(a1), "%%") then 162 | 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)) 163 | else 164 | if not tmp then 165 | tmp = {} 166 | end 167 | 168 | -- This block dynamically rebuilds the tmp array stopping on the first nil. 169 | table.insert(tmp, output) 170 | 171 | table.insert(tmp, tostring(a1)) 172 | table.insert(tmp, a2) 173 | table.insert(tmp, a3) 174 | table.insert(tmp, a4) 175 | table.insert(tmp, a5) 176 | table.insert(tmp, a6) 177 | table.insert(tmp, a7) 178 | table.insert(tmp, a8) 179 | table.insert(tmp, a9) 180 | table.insert(tmp, a10) 181 | table.insert(tmp, a11) 182 | table.insert(tmp, a12) 183 | table.insert(tmp, a13) 184 | table.insert(tmp, a14) 185 | table.insert(tmp, a15) 186 | table.insert(tmp, a16) 187 | table.insert(tmp, a17) 188 | table.insert(tmp, a18) 189 | table.insert(tmp, a19) 190 | table.insert(tmp, a20) 191 | while tmp[table.getn(tmp)] == nil do 192 | table.remove(tmp) 193 | end 194 | for k = 1, table.getn(tmp) do 195 | tmp[k] = tostring(tmp[k]) 196 | end 197 | 198 | output = table.concat(tmp, " ") 199 | 200 | for k,v in pairs(tmp) do 201 | tmp[k] = nil 202 | end 203 | table_setn(tmp, 0) 204 | end 205 | 206 | print(output, r, g, b, frame or self.debugFrame, delay) 207 | end 208 | 209 | function AceDebug:LevelDebug(level, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) 210 | if not self.debugging or not self.debuglevel then return end 211 | AceDebug:argCheck(level, 1, "number") 212 | if level < 1 or level > 3 then 213 | AceDebug:error("Bad argument #1 to `LevelDebug`, must be a number 1-3") 214 | end 215 | if level > self.debuglevel then return end 216 | 217 | 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) 218 | end 219 | 220 | 221 | local options 222 | function AceDebug:GetAceOptionsDataTable(target) 223 | if not options then 224 | options = { 225 | debug = { 226 | name = DEBUGGING, 227 | desc = TOGGLE_DEBUGGING, 228 | type = "toggle", 229 | get = "IsDebugging", 230 | set = "SetDebugging", 231 | order = -2, 232 | } 233 | } 234 | end 235 | return options 236 | end 237 | AceLibrary:Register(AceDebug, MAJOR_VERSION, MINOR_VERSION, AceDebug.activate) 238 | AceDebug = AceLibrary(MAJOR_VERSION) 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cursive 2 | 3 | > [!IMPORTANT] 4 | > 5 | > **This addon requires you to have [SuperWoW](https://github.com/balakethelock/SuperWoW) installed.** 6 | > 7 | > It won't work without it. Really. 8 | 9 | ![image](https://github.com/pepopo978/Cursive/assets/149287158/801511af-29c7-4baf-b1ac-5e8c52f0f846) 10 | 11 | Cursive combines ShaguScan unit scanning with curse tracking similar to DotTimer and the ability to automatically curse 12 | targets similar to Decursive. 13 | 14 | ### Recommended setup 15 | 16 | Move the Cursive label to the desired position on the screen. You can turn on 'Show Frame Background' in the settings to 17 | help with this. Once you have it where you want it, turn off 'Show Frame Background' and turn on 'Allow clickthrough' 18 | so that it doesn't block your clicks when it's not displaying mobs. 19 | 20 | ## Commands 21 | 22 | `/cursive` for commands, minimap icon to edit options. 23 | 24 | ### Curse 25 | 26 | `/cursive curse ||`: Casts spell if not already on target/guid 27 | 28 | EXAMPLE: `/cursive curse Corruption|target` will attempt to cast Corruption on your target if it's not already on them 29 | and they aren't cc'ed. 30 | 31 | ### Multicurse 32 | 33 | `/cursive multicurse ||`: Picks target based on priority and 34 | casts spell if not already on target and they aren't cc'ed. 35 | 36 | EXAMPLE: `/cursive multicurse Corruption|HIGHEST_HP` will attempt to cast Corruption picking the target with the highest 37 | HP that doesn't already have it and will warn you if it does nothing. 38 | 39 | ### Target 40 | 41 | `/cursive target ||`: Targets unit based on priority if spell in range and not already on target 42 | 43 | EXAMPLE: `/cursive target Icicles|HIGHEST_HP` will target the enemy with the highest HP in range of the spell Icicles. 44 | 45 | can also do it only if you don't have a target already: 46 | `/run if not UnitName("target") then SlashCmdList.CURSIVE("target Icicles|HIGHEST_HP") end` 47 | 48 | There is also 49 | `/script targetGuid = Cursive:GetTarget("str", "str", {})`: Returns guid of the unit that would be targeted based on priority and options. 50 | 51 | EXAMPLE: `/script targetGuid = Cursive:GetTarget("Corruption", "HIGHEST_HP", {})` will return guid of the enemy with the highest HP in range of the spell Corruption that doesn't already have Corruption. 52 | 53 | ## Priority Options 54 | 55 | - HIGHEST_HP - Target highest HP enemy without a curse first. 56 | - LOWEST_HP - Target lowest HP enemy without a curse first. 57 | - RAID_MARK - Target largest raid mark enemy without a curse first. Priority is as follows: 58 | - Skull = 8 (1st priority) 59 | - Cross = 7 60 | - Square = 6 61 | - Moon = 5 62 | - Triangle = 4 63 | - Diamond = 3 64 | - Circle = 2 65 | - Star = 1 (8th priority) 66 | - No mark = 0 67 | - RAID_MARK_SQUARE - Target largest raid mark but ignore Skull and Cross. Priority is as follows: 68 | - Square = 6 (1st priority) 69 | - Moon = 5 70 | - Triangle = 4 71 | - Diamond = 3 72 | - Circle = 2 73 | - Star = 1 74 | - No mark = 0 75 | - Cross = -1 76 | - Skull = -2 (9th priority) 77 | - INVERSE_RAID_MARK - Target lowest raid mark enemy without a curse first. (reverse of RAID_MARK) 78 | 79 | - HIGHEST_HP_RAID_MARK - Target highest HP enemy with a raid (use raid mark priorities for identical hp), then highest 80 | HP enemy without a mark. 81 | - HIGHEST_HP_RAID_MARK_SQUARE - Same as HIGHEST_HP_RAID_MARK but with RAID_MARK_SQUARE priority for marked enemies. 82 | - HIGHEST_HP_INVERSE_RAID_MARK - Same as HIGHEST_HP_RAID_MARK but with INVERSE_RAID_MARK priority for marked enemies. 83 | 84 | ## Command Options 85 | 86 | All commands can take the following options separated by commas: 87 | 88 | - `warnings` : "Display text warnings when a curse fails to cast.", 89 | - `resistsound` : "Play a sound when a curse is resisted.", 90 | - `expiringsound` : "Play a sound when a curse is about to expire.", 91 | - `allowooc` : "Allow out of combat targets to be multicursed. Would only consider using this solo to avoid potentially 92 | griefing raids/dungeons by pulling unintended mobs.", 93 | - `priotarget` : "Always prioritize current target when choosing target for multicurse. Does not affect 'curse' 94 | command.", 95 | - `ignoretarget` : "Ignore the current target when choosing target for multicurse. Does not affect 'curse' command.", 96 | - `playeronly` : "Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' 97 | command.", 98 | - `minhp=` : "Minimum HP for a target to be considered.", 99 | - `refreshtime=` : "Time threshold at which to allow refreshing a curse. Default is 0 seconds.", 100 | - `name=` : "Filter targets by name. Can be a partial match. If no match is found, the command will do nothing.", 101 | - `ignorespellid=` : "Ignore targets with the specified spell id already on them. Useful for ignoring targets 102 | that already have a shared debuff.", 103 | - `ignorespelltexture=` : "Ignore targets with the specified spell texture already on them. Useful for ignoring 104 | targets that already have a shared debuff.", 105 | 106 | EXAMPLE: `/cursive multicurse Corruption|HIGHEST_HP|warnings,resistsound,expiringsound,minhp=10000,refreshtime=2` 107 | 108 | EXAMPLE: 109 | `/cursive multicurse Curse of Recklessness|RAID_MARK|name=Touched Warrior,ignorespelltexture=Spell_Shadow_UnholyStrength,resistsound,expiringsound` 110 | 111 | ## Macro examples 112 | 113 | You can just put multiple commands in a macro like this 114 | ``` 115 | /cursive curse Curse of Recklessness|target|refreshtime=1 116 | /cursive curse Corruption|target|refreshtime=3 117 | /cursive curse Siphon Life|target|refreshtime=1 118 | ``` 119 | but the game won't always execute them in the order you want 120 | 121 | if you want more control of the order you can do something like 122 | ``` 123 | /script if not Cursive:Curse("Curse of Recklessness", "target", {refreshtime=1}) then if not Cursive:Curse("Corruption", "target", {refreshtime=3}) then Cursive:Curse("Siphon Life", "target", {refreshtime=1}) end end 124 | ``` 125 | This also works for Multicurse: 126 | ``` 127 | /script if not Cursive:Multicurse("Curse of Recklessness", "HIGHEST_HP", {refreshtime=1}) then if not Cursive:Multicurse("Corruption", "HIGHEST_HP", {refreshtime=3}) then Cursive:Multicurse("Siphon Life", "HIGHEST_HP", {refreshtime=1}) end end 128 | ``` 129 | 130 | Example with more options 131 | `/script Cursive:Multicurse("Curse of Recklessness", "HIGHEST_HP", {warnings=1,resistsound=1,expiringsound=1,refreshtime=2})` 132 | 133 | All cursive commands will return true only if it attempted to cast or it found a target. 134 | 135 | ## Shared Debuffs 136 | 137 | Shared debuffs applied by other players will appear greyed out on targets. 138 | 139 | Currently only Faerie Fire is supported as I felt it warranted special handling. It works by looking for other players casting faerie fire and then checking if the mob has the debuff, so if someone refreshes faerie fire and gets resisted it will incorrectly restart the timer. However, it should still correctly remove faerie fire if it falls off that target. I didn't want to impact performance to handle this edge case. 140 | 141 | ## Accessing curse data in other addons 142 | 143 | Cursive data can be accessed in other addons 144 | 145 | You can check if curse is active with 146 | `Cursive.curses:HasCurse(lowercaseSpellNameNoRank, targetGuid, minRemaining)` 147 | 148 | You can get raw curse data using 149 | `Cursive.curses:GetCurseData(spellName, guid)` 150 | 151 | Curse data is a table with the following structure: 152 | ``` 153 | { 154 | rank = int, 155 | duration = float, 156 | start = float, 157 | spellID = int, 158 | targetGuid = int, 159 | currentPlayer = bool, 160 | } 161 | ``` 162 | 163 | Here's an example that gets the time left on Corruption on the current target: 164 | ``` 165 | /run _, guid = UnitExists("target"); local data = Cursive.curses:GetCurseData("Corruption", guid); print(Cursive.curses:TimeRemaining(data)) 166 | ``` 167 | 168 | ## Important info 169 | 170 | If you have my latest nampower, it will use the SpellInRange function from that to provide improved range checking. 171 | 172 | Otherwise, all commands will prioritize targets within 28 yards of you first to have a better chance of being in range. 173 | 174 | All commands will ignore targets with the following CCs on them: 175 | 176 | - Sleep 177 | - Entangling Roots 178 | - Shackle Undead 179 | - Polymorph 180 | - Turn Undead 181 | - Blind 182 | - Sap 183 | - Gouge 184 | - Freezing Trap 185 | - Banish 186 | 187 | Multicurse will only ever target enemies that are already in combat (except if you target a mob directly first) to 188 | prevent pulling things you didn't intend like marked patrols. 189 | 190 | Mobs with raid marks will be displayed first. 191 | 192 | Mobs will the top 3 max hps will always display next. I may make this configurable in the future. 193 | 194 | There is an option "always show current target" that will display your current target in the last slot if they aren't already being displayed. 195 | 196 | You can ignore mobs based on their unit name using the ignored mob list filter. It is comma separated and you need to press enter to get it to save. For example can do: 197 | `whelp,scarab` to ignore all mobs with those strings in their name. 198 | 199 | -------------------------------------------------------------------------------- /libs/AceModuleCore-2.0/AceModuleCore-2.0.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Name: AceModuleCore-2.0 3 | Revision: $Rev: 17998 $ 4 | Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) 5 | Inspired By: Ace 1.x by Turan (turan@gryphon.com) 6 | Website: http://www.wowace.com/ 7 | Documentation: http://www.wowace.com/index.php/AceModuleCore-2.0 8 | SVN: http://svn.wowace.com/root/trunk/Ace2/AceModuleCore-2.0 9 | Description: Mixin to provide a module system so that modules or plugins can 10 | use an addon as its core. 11 | Dependencies: AceLibrary, AceOO-2.0, AceAddon-2.0, AceEvent-2.0 (optional), Compost-2.0 (optional) 12 | ]] 13 | 14 | local MAJOR_VERSION = "AceModuleCore-2.0" 15 | local MINOR_VERSION = "$Revision: 17998 $" 16 | 17 | if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary") end 18 | if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end 19 | 20 | if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check 21 | if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end 22 | 23 | local function safecall(func,a,b,c,d,e,f,g) 24 | local success, err = pcall(func,a,b,c,d,e,f,g) 25 | if not success then geterrorhandler()(err) end 26 | end 27 | 28 | local table_setn 29 | do 30 | local version = GetBuildInfo() 31 | if string.find(version, "^2%.") then 32 | -- 2.0.0 33 | table_setn = function() end 34 | else 35 | table_setn = table.setn 36 | end 37 | end 38 | 39 | local new, del 40 | do 41 | local list = setmetatable({}, {__mode = 'k'}) 42 | function new() 43 | local t = next(list) 44 | if t then 45 | list[t] = nil 46 | return t 47 | else 48 | return {} 49 | end 50 | end 51 | function del(t) 52 | for k in pairs(t) do 53 | t[k] = nil 54 | end 55 | table_setn(t, 0) 56 | list[t] = true 57 | return nil 58 | end 59 | end 60 | 61 | local AceOO = AceLibrary:GetInstance("AceOO-2.0") 62 | local AceModuleCore = AceOO.Mixin { 63 | "NewModule", 64 | "HasModule", 65 | "GetModule", 66 | "IsModule", 67 | "IterateModules", 68 | "SetModuleMixins", 69 | "SetModuleClass", 70 | "IsModuleActive", 71 | "ToggleModuleActive" 72 | } 73 | local AceEvent 74 | 75 | local Compost = AceLibrary:HasInstance("Compost-2.0") and AceLibrary("Compost-2.0") 76 | 77 | local function getlibrary(lib) 78 | if type(lib) == "string" then 79 | return AceLibrary(lib) 80 | else 81 | return lib 82 | end 83 | end 84 | 85 | local tmp 86 | function AceModuleCore:NewModule(name, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) 87 | if not self.modules then 88 | AceModuleCore:error("CreatePrototype() must be called before attempting to create a new module.", 2) 89 | end 90 | AceModuleCore:argCheck(name, 2, "string") 91 | if string.len(name) == 0 then 92 | AceModuleCore:error("Bad argument #2 to `NewModule`, string must not be empty") 93 | end 94 | if self.modules[name] then 95 | AceModuleCore:error("The module %q has already been registered", name) 96 | end 97 | 98 | if not tmp then 99 | tmp = {} 100 | end 101 | if a1 then table.insert(tmp, a1) 102 | if a2 then table.insert(tmp, a2) 103 | if a3 then table.insert(tmp, a3) 104 | if a4 then table.insert(tmp, a4) 105 | if a5 then table.insert(tmp, a5) 106 | if a6 then table.insert(tmp, a6) 107 | if a7 then table.insert(tmp, a7) 108 | if a8 then table.insert(tmp, a8) 109 | if a9 then table.insert(tmp, a9) 110 | if a10 then table.insert(tmp, a10) 111 | if a11 then table.insert(tmp, a11) 112 | if a12 then table.insert(tmp, a12) 113 | if a13 then table.insert(tmp, a13) 114 | if a14 then table.insert(tmp, a14) 115 | if a15 then table.insert(tmp, a15) 116 | if a16 then table.insert(tmp, a16) 117 | if a17 then table.insert(tmp, a17) 118 | if a18 then table.insert(tmp, a18) 119 | if a19 then table.insert(tmp, a19) 120 | if a20 then table.insert(tmp, a20) 121 | end end end end end end end end end end end end end end end end end end end end 122 | for k,v in ipairs(tmp) do 123 | tmp[k] = getlibrary(v) 124 | end 125 | 126 | if self.moduleMixins then 127 | for _,mixin in ipairs(self.moduleMixins) do 128 | local exists = false 129 | for _,v in ipairs(tmp) do 130 | if mixin == v then 131 | exists = true 132 | break 133 | end 134 | end 135 | if not exists then 136 | table.insert(tmp, mixin) 137 | end 138 | end 139 | end 140 | 141 | local module = AceOO.Classpool(self.moduleClass, unpack(tmp)):new(name) 142 | self.modules[name] = module 143 | module.name = name 144 | module.title = name 145 | 146 | AceModuleCore.totalModules[module] = self 147 | 148 | if AceEvent then 149 | AceEvent:TriggerEvent("Ace2_ModuleCreated", module) 150 | end 151 | 152 | for k in pairs(tmp) do 153 | tmp[k] = nil 154 | end 155 | table_setn(tmp, 0) 156 | return module 157 | end 158 | 159 | function AceModuleCore:HasModule(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) 160 | if a1 then if not self.modules[a1] then return false end 161 | if a2 then if not self.modules[a2] then return false end 162 | if a3 then if not self.modules[a3] then return false end 163 | if a4 then if not self.modules[a4] then return false end 164 | if a5 then if not self.modules[a5] then return false end 165 | if a6 then if not self.modules[a6] then return false end 166 | if a7 then if not self.modules[a7] then return false end 167 | if a8 then if not self.modules[a8] then return false end 168 | if a9 then if not self.modules[a9] then return false end 169 | if a10 then if not self.modules[a10] then return false end 170 | if a11 then if not self.modules[a11] then return false end 171 | if a12 then if not self.modules[a12] then return false end 172 | if a13 then if not self.modules[a13] then return false end 173 | if a14 then if not self.modules[a14] then return false end 174 | if a15 then if not self.modules[a15] then return false end 175 | if a16 then if not self.modules[a16] then return false end 176 | if a17 then if not self.modules[a17] then return false end 177 | if a18 then if not self.modules[a18] then return false end 178 | if a19 then if not self.modules[a19] then return false end 179 | if a20 then if not self.modules[a20] then return false end 180 | end end end end end end end end end end end end end end end end end end end end 181 | 182 | return true 183 | end 184 | 185 | function AceModuleCore:GetModule(name) 186 | if not self.modules then 187 | AceModuleCore:error("Error initializing class. Please report error.") 188 | end 189 | if not self.modules[name] then 190 | AceModuleCore:error("Cannot find module %q.", name) 191 | end 192 | return self.modules[name] 193 | end 194 | 195 | function AceModuleCore:IsModule(module) 196 | if self == AceModuleCore then 197 | return AceModuleCore.totalModules[module] 198 | else 199 | for k,v in pairs(self.modules) do 200 | if v == module then 201 | return true 202 | end 203 | end 204 | return false 205 | end 206 | end 207 | 208 | function AceModuleCore:IterateModules() 209 | local t = new() 210 | for k in pairs(self.modules) do 211 | table.insert(t, k) 212 | end 213 | table.sort(t) 214 | local i = 0 215 | return function() 216 | i = i + 1 217 | local x = t[i] 218 | if x then 219 | return x, self.modules[x] 220 | else 221 | t = del(t) 222 | return nil 223 | end 224 | end, nil, nil 225 | end 226 | 227 | function AceModuleCore:SetModuleMixins(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) 228 | if self.moduleMixins then 229 | AceModuleCore:error('Cannot call "SetModuleMixins" twice') 230 | elseif not self.modules then 231 | AceModuleCore:error("Error initializing class. Please report error.") 232 | elseif next(self.modules) then 233 | AceModuleCore:error('Cannot call "SetModuleMixins" after "NewModule" has been called.') 234 | end 235 | 236 | self.moduleMixins = Compost and Compost:Acquire(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) or {a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20} 237 | for k,v in ipairs(self.moduleMixins) do 238 | self.moduleMixins[k] = getlibrary(v) 239 | end 240 | end 241 | 242 | function AceModuleCore:SetModuleClass(class) 243 | class = getlibrary(class) 244 | AceModuleCore:assert(AceOO.inherits(class, AceOO.Class), "Bad argument #2 to `SetModuleClass' (Class expected)") 245 | if not self.modules then 246 | AceModuleCore:error("Error initializing class. Please report error.") 247 | end 248 | if self.customModuleClass then 249 | AceModuleCore:error("Cannot call `SetModuleClass' twice.") 250 | end 251 | self.customModuleClass = true 252 | self.moduleClass = class 253 | self.modulePrototype = class.prototype 254 | end 255 | 256 | function AceModuleCore:ToggleModuleActive(module, state) 257 | AceModuleCore:argCheck(module, 2, "table", "string") 258 | AceModuleCore:argCheck(state, 3, "nil", "boolean") 259 | 260 | if type(module) == "string" then 261 | if not self:HasModule(module) then 262 | AceModuleCore:error("Cannot find module %q", module) 263 | end 264 | module = self:GetModule(module) 265 | else 266 | if not self:IsModule(module) then 267 | AceModuleCore:error("%q is not a module", module) 268 | end 269 | end 270 | 271 | local disable 272 | if state == nil then 273 | disable = self:IsModuleActive(module) 274 | else 275 | disable = not state 276 | if disable ~= self:IsModuleActive(module) then 277 | return 278 | end 279 | end 280 | 281 | if type(module.ToggleActive) == "function" then 282 | return module:ToggleActive(not disable) 283 | elseif AceOO.inherits(self, "AceDB-2.0") then 284 | if not self.db or not self.db.raw then 285 | AceModuleCore:error("Cannot toggle a module until `RegisterDB' has been called and `ADDON_LOADED' has been fireed.") 286 | end 287 | if type(self.db.raw.disabledModules) ~= "table" then 288 | self.db.raw.disabledModules = Compost and Compost:Acquire() or {} 289 | end 290 | local _,profile = self:GetProfile() 291 | if type(self.db.raw.disabledModules[profile]) ~= "table" then 292 | self.db.raw.disabledModules[profile] = Compost and Compost:Acquire() or {} 293 | end 294 | if type(self.db.raw.disabledModules[profile][module.name]) ~= "table" then 295 | self.db.raw.disabledModules[profile][module.name] = disable or nil 296 | end 297 | if not disable then 298 | if not next(self.db.raw.disabledModules[profile]) then 299 | if Compost then 300 | Compost:Reclaim(self.db.raw.disabledModules[profile]) 301 | end 302 | self.db.raw.disabledModules[profile] = nil 303 | end 304 | if not next(self.db.raw.disabledModules) then 305 | if Compost then 306 | Compost:Reclaim(self.db.raw.disabledModules) 307 | end 308 | self.db.raw.disabledModules = nil 309 | end 310 | end 311 | else 312 | if type(self.disabledModules) ~= "table" then 313 | self.disabledModules = Compost and Compost:Acquire() or {} 314 | end 315 | self.disabledModules[module.name] = disable or nil 316 | end 317 | if AceOO.inherits(module, "AceAddon-2.0") then 318 | local AceAddon = AceLibrary("AceAddon-2.0") 319 | if not AceAddon.addonsStarted[module] then 320 | return 321 | end 322 | end 323 | if not disable then 324 | local current = module.class 325 | while true do 326 | if current == AceOO.Class then 327 | break 328 | end 329 | if current.mixins then 330 | for mixin in pairs(current.mixins) do 331 | if type(mixin.OnEmbedEnable) == "function" then 332 | safecall(mixin.OnEmbedEnable, mixin, module) 333 | end 334 | end 335 | end 336 | current = current.super 337 | end 338 | if type(module.OnEnable) == "function" then 339 | safecall(module.OnEnable, module) 340 | end 341 | if AceEvent then 342 | AceEvent:TriggerEvent("Ace2_AddonEnabled", module) 343 | end 344 | else 345 | local current = module.class 346 | while true do 347 | if current == AceOO.Class then 348 | break 349 | end 350 | if current.mixins then 351 | for mixin in pairs(current.mixins) do 352 | if type(mixin.OnEmbedDisable) == "function" then 353 | safecall(mixin.OnEmbedDisable, mixin, module) 354 | end 355 | end 356 | end 357 | current = current.super 358 | end 359 | if type(module.OnDisable) == "function" then 360 | safecall(module.OnDisable, module) 361 | end 362 | if AceEvent then 363 | AceEvent:TriggerEvent("Ace2_AddonDisabled", module) 364 | end 365 | end 366 | return not disable 367 | end 368 | 369 | function AceModuleCore:IsModuleActive(module) 370 | AceModuleCore:argCheck(module, 2, "table", "string") 371 | 372 | if AceModuleCore == self then 373 | self:argCheck(module, 2, "table") 374 | 375 | local core = AceModuleCore.totalModules[module] 376 | if not core then 377 | self:error("Bad argument #2 to `IsModuleActive'. Not a module") 378 | end 379 | return core:IsModuleActive(module) 380 | end 381 | 382 | if type(module) == "string" then 383 | if not self:HasModule(module) then 384 | AceModuleCore:error("Cannot find module %q", module) 385 | end 386 | module = self:GetModule(module) 387 | else 388 | if not self:IsModule(module) then 389 | AceModuleCore:error("%q is not a module", module) 390 | end 391 | end 392 | 393 | if type(module.IsActive) == "function" then 394 | return module:IsActive() 395 | elseif AceOO.inherits(self, "AceDB-2.0") then 396 | local _,profile = self:GetProfile() 397 | return not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[profile] or not self.db.raw.disabledModules[profile][module.name] 398 | else 399 | return not self.disabledModules or not self.disabledModules[module.name] 400 | end 401 | end 402 | 403 | function AceModuleCore:OnInstanceInit(target) 404 | if target.modules then 405 | AceModuleCore:error("OnInstanceInit cannot be called twice") 406 | end 407 | target.modules = Compost and Compost:Acquire() or {} 408 | 409 | target.moduleClass = AceOO.Class("AceAddon-2.0") 410 | target.modulePrototype = target.moduleClass.prototype 411 | end 412 | 413 | AceModuleCore.OnManualEmbed = AceModuleCore.OnInstanceInit 414 | 415 | function AceModuleCore.OnEmbedProfileDisable(AceModuleCore, self, newProfile) 416 | if not AceOO.inherits(self, "AceDB-2.0") then 417 | return 418 | end 419 | local _,currentProfile = self:GetProfile() 420 | for k, module in pairs(self.modules) do 421 | if type(module.IsActive) == "function" or type(module.ToggleActive) == "function" then 422 | -- continue 423 | else 424 | local currentActive = not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[currentProfile] or not self.db.raw.disabledModules[currentProfile][module.name] 425 | local newActive = not self.db or not self.db.raw or not self.db.raw.disabledModules or not self.db.raw.disabledModules[newProfile] or not self.db.raw.disabledModules[newProfile][module.name] 426 | if currentActive ~= newActive then 427 | self:ToggleModuleActive(module) 428 | if not self.db.raw.disabledModules then 429 | self.db.raw.disabledModules = {} 430 | end 431 | if not self.db.raw.disabledModules[currentProfile] then 432 | self.db.raw.disabledModules[currentProfile] = {} 433 | end 434 | self.db.raw.disabledModules[currentProfile][module.name] = not currentActive or nil 435 | end 436 | end 437 | end 438 | end 439 | 440 | local function activate(self, oldLib, oldDeactivate) 441 | AceModuleCore = self 442 | 443 | if oldLib then 444 | self.totalModules = oldLib.totalModules 445 | end 446 | if not self.totalModules then 447 | self.totalModules = {} 448 | end 449 | 450 | self.super.activate(self, oldLib, oldDeactivate) 451 | end 452 | 453 | local function external(self, major, instance) 454 | if major == "Compost-2.0" then 455 | Compost = instance 456 | elseif major == "AceEvent-2.0" then 457 | AceEvent = instance 458 | end 459 | end 460 | 461 | AceLibrary:Register(AceModuleCore, MAJOR_VERSION, MINOR_VERSION, activate, nil, external) 462 | AceModuleCore = AceLibrary(MAJOR_VERSION) 463 | -------------------------------------------------------------------------------- /libs/AceLocale-2.2/AceLocale-2.2.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Name: AceLocale-2.2 3 | Revision: $Rev: 17638 $ 4 | Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) 5 | Inspired By: Ace 1.x by Turan (turan@gryphon.com) 6 | Website: http://www.wowace.com/ 7 | Documentation: http://www.wowace.com/index.php/AceLocale-2.2 8 | SVN: http://svn.wowace.com/root/trunk/Ace2/AceLocale-2.2 9 | Description: Localization library for addons to use to handle proper 10 | localization and internationalization. 11 | Dependencies: AceLibrary 12 | ]] 13 | 14 | local MAJOR_VERSION = "AceLocale-2.2" 15 | local MINOR_VERSION = "$Revision: 17639 $" 16 | 17 | if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end 18 | if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end 19 | 20 | if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check 21 | local AceLocale = {} 22 | 23 | local DEFAULT_LOCALE = "enUS" 24 | local _G = getfenv(0) 25 | 26 | local BASE_TRANSLATIONS, DEBUGGING, TRANSLATIONS, BASE_LOCALE, TRANSLATION_TABLES, REVERSE_TRANSLATIONS, STRICTNESS, DYNAMIC_LOCALES, CURRENT_LOCALE, NAME 27 | 28 | local rawget = rawget 29 | local rawset = rawset 30 | local type = type 31 | 32 | local newRegistries = {} 33 | local scheduleClear 34 | 35 | local lastSelf 36 | local __index = function(self, key) 37 | lastSelf = self 38 | local value = (rawget(self, TRANSLATIONS) or AceLocale.prototype)[key] 39 | rawset(self, key, value) 40 | return value 41 | end 42 | 43 | local __newindex = function(self, k, v) 44 | if type(v) ~= "function" and type(k) ~= "table" then 45 | AceLocale.error(self, "Cannot change the values of an AceLocale instance.") 46 | end 47 | rawset(self, k, v) 48 | end 49 | 50 | local __tostring = function(self) 51 | if type(rawget(self, 'GetLibraryVersion')) == "function" then 52 | return self:GetLibraryVersion() 53 | else 54 | return "AceLocale(" .. self[NAME] .. ")" 55 | end 56 | end 57 | 58 | local function clearCache(self) 59 | if not rawget(self, BASE_TRANSLATIONS) then 60 | return 61 | end 62 | 63 | local cache = self[BASE_TRANSLATIONS] 64 | rawset(self, REVERSE_TRANSLATIONS, nil) 65 | 66 | for k in pairs(self) do 67 | if rawget(cache, k) ~= nil then 68 | self[k] = nil 69 | end 70 | end 71 | rawset(self, 'tmp', true) 72 | self.tmp = nil 73 | end 74 | 75 | local function refixInstance(instance) 76 | if getmetatable(instance) then 77 | setmetatable(instance, nil) 78 | end 79 | local translations = instance[TRANSLATIONS] 80 | if translations then 81 | if getmetatable(translations) then 82 | setmetatable(translations, nil) 83 | end 84 | local baseTranslations = instance[BASE_TRANSLATIONS] 85 | if getmetatable(baseTranslations) then 86 | setmetatable(baseTranslations, nil) 87 | end 88 | if translations == baseTranslations or instance[STRICTNESS] then 89 | setmetatable(instance, { 90 | __index = __index, 91 | __newindex = __newindex, 92 | __tostring = __tostring 93 | }) 94 | 95 | setmetatable(translations, { 96 | __index = AceLocale.prototype 97 | }) 98 | else 99 | setmetatable(instance, { 100 | __index = __index, 101 | __newindex = __newindex, 102 | __tostring = __tostring 103 | }) 104 | 105 | setmetatable(translations, { 106 | __index = baseTranslations, 107 | }) 108 | 109 | setmetatable(baseTranslations, { 110 | __index = AceLocale.prototype, 111 | }) 112 | end 113 | else 114 | setmetatable(instance, { 115 | __index = __index, 116 | __newindex = __newindex, 117 | __tostring = __tostring, 118 | }) 119 | end 120 | clearCache(instance) 121 | newRegistries[instance] = true 122 | scheduleClear() 123 | return instance 124 | end 125 | 126 | function AceLocale:new(name) 127 | self:argCheck(name, 2, "string") 128 | 129 | if self.registry[name] and type(rawget(self.registry[name], 'GetLibraryVersion')) ~= "function" then 130 | return self.registry[name] 131 | end 132 | 133 | AceLocale.registry[name] = refixInstance({ 134 | [STRICTNESS] = false, 135 | [NAME] = name, 136 | }) 137 | newRegistries[AceLocale.registry[name]] = true 138 | return AceLocale.registry[name] 139 | end 140 | 141 | AceLocale.prototype = { class = AceLocale } 142 | 143 | function AceLocale.prototype:EnableDebugging() 144 | if rawget(self, BASE_TRANSLATIONS) then 145 | AceLocale.error(self, "Cannot enable debugging after a translation has been registered.") 146 | end 147 | rawset(self, DEBUGGING, true) 148 | end 149 | 150 | function AceLocale.prototype:EnableDynamicLocales(override) 151 | AceLocale.argCheck(self, override, 2, "boolean", "nil") 152 | if not override and rawget(self, BASE_TRANSLATIONS) then 153 | AceLocale.error(self, "Cannot enable dynamic locales after a translation has been registered.") 154 | end 155 | if not rawget(self, DYNAMIC_LOCALES) then 156 | rawset(self, DYNAMIC_LOCALES, true) 157 | if rawget(self, BASE_LOCALE) then 158 | if not rawget(self, TRANSLATION_TABLES) then 159 | rawset(self, TRANSLATION_TABLES, {}) 160 | end 161 | self[TRANSLATION_TABLES][self[BASE_LOCALE]] = self[BASE_TRANSLATIONS] 162 | self[TRANSLATION_TABLES][self[CURRENT_LOCALE]] = self[TRANSLATIONS] 163 | end 164 | end 165 | end 166 | 167 | function AceLocale.prototype:RegisterTranslations(locale, func) 168 | AceLocale.argCheck(self, locale, 2, "string") 169 | AceLocale.argCheck(self, func, 3, "function") 170 | 171 | if locale == rawget(self, BASE_LOCALE) then 172 | AceLocale.error(self, "Cannot provide the same locale more than once. %q provided twice.", locale) 173 | end 174 | 175 | if rawget(self, BASE_TRANSLATIONS) and GetLocale() ~= locale then 176 | if rawget(self, DEBUGGING) or rawget(self, DYNAMIC_LOCALES) then 177 | if not rawget(self, TRANSLATION_TABLES) then 178 | rawset(self, TRANSLATION_TABLES, {}) 179 | end 180 | if self[TRANSLATION_TABLES][locale] then 181 | AceLocale.error(self, "Cannot provide the same locale more than once. %q provided twice.", locale) 182 | end 183 | local t = func() 184 | func = nil 185 | if type(t) ~= "table" then 186 | AceLocale.error(self, "Bad argument #3 to `RegisterTranslations'. function did not return a table. (expected table, got %s)", type(t)) 187 | end 188 | self[TRANSLATION_TABLES][locale] = t 189 | t = nil 190 | end 191 | func = nil 192 | return 193 | end 194 | local t = func() 195 | func = nil 196 | if type(t) ~= "table" then 197 | AceLocale.error(self, "Bad argument #3 to `RegisterTranslations'. function did not return a table. (expected table, got %s)", type(t)) 198 | end 199 | 200 | rawset(self, TRANSLATIONS, t) 201 | if not rawget(self, BASE_TRANSLATIONS) then 202 | rawset(self, BASE_TRANSLATIONS, t) 203 | rawset(self, BASE_LOCALE, locale) 204 | for key,value in pairs(t) do 205 | if value == true then 206 | t[key] = key 207 | end 208 | end 209 | else 210 | for key, value in pairs(self[TRANSLATIONS]) do 211 | if not rawget(self[BASE_TRANSLATIONS], key) then 212 | AceLocale.error(self, "Improper translation exists. %q is likely misspelled for locale %s.", key, locale) 213 | end 214 | if value == true then 215 | 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) 216 | end 217 | end 218 | end 219 | rawset(self, CURRENT_LOCALE, locale) 220 | refixInstance(self) 221 | if rawget(self, DEBUGGING) or rawget(self, DYNAMIC_LOCALES) then 222 | if not rawget(self, TRANSLATION_TABLES) then 223 | rawset(self, TRANSLATION_TABLES, {}) 224 | end 225 | self[TRANSLATION_TABLES][locale] = t 226 | end 227 | t = nil 228 | end 229 | 230 | function AceLocale.prototype:SetLocale(locale) 231 | AceLocale.argCheck(self, locale, 2, "string", "boolean") 232 | if not rawget(self, DYNAMIC_LOCALES) then 233 | AceLocale.error(self, "Cannot call `SetLocale' without first calling `EnableDynamicLocales'.") 234 | end 235 | if not rawget(self, TRANSLATION_TABLES) then 236 | AceLocale.error(self, "Cannot call `SetLocale' without first calling `RegisterTranslations'.") 237 | end 238 | if locale == true then 239 | locale = GetLocale() 240 | if not self[TRANSLATION_TABLES][locale] then 241 | locale = self[BASE_LOCALE] 242 | end 243 | end 244 | 245 | if self[CURRENT_LOCALE] == locale then 246 | return 247 | end 248 | 249 | if not self[TRANSLATION_TABLES][locale] then 250 | AceLocale.error(self, "Locale %q not registered.", locale) 251 | end 252 | 253 | self[TRANSLATIONS] = self[TRANSLATION_TABLES][locale] 254 | self[CURRENT_LOCALE] = locale 255 | refixInstance(self) 256 | end 257 | 258 | function AceLocale.prototype:GetLocale() 259 | if not rawget(self, TRANSLATION_TABLES) then 260 | AceLocale.error(self, "Cannot call `GetLocale' without first calling `RegisterTranslations'.") 261 | end 262 | return self[CURRENT_LOCALE] 263 | end 264 | 265 | local function iter(t, position) 266 | return (next(t, position)) 267 | end 268 | 269 | function AceLocale.prototype:IterateAvailableLocales() 270 | if not rawget(self, DYNAMIC_LOCALES) then 271 | AceLocale.error(self, "Cannot call `IterateAvailableLocales' without first calling `EnableDynamicLocales'.") 272 | end 273 | if not rawget(self, TRANSLATION_TABLES) then 274 | AceLocale.error(self, "Cannot call `IterateAvailableLocales' without first calling `RegisterTranslations'.") 275 | end 276 | return iter, self[TRANSLATION_TABLES], nil 277 | end 278 | 279 | function AceLocale.prototype:HasLocale(locale) 280 | if not rawget(self, DYNAMIC_LOCALES) then 281 | AceLocale.error(self, "Cannot call `HasLocale' without first calling `EnableDynamicLocales'.") 282 | end 283 | AceLocale.argCheck(self, locale, 2, "string") 284 | return rawget(self, TRANSLATION_TABLES) and self[TRANSLATION_TABLES][locale] ~= nil 285 | end 286 | 287 | function AceLocale.prototype:SetStrictness(strict) 288 | AceLocale.argCheck(self, strict, 2, "boolean") 289 | local mt = getmetatable(self) 290 | if not mt then 291 | AceLocale.error(self, "Cannot call `SetStrictness' without a metatable.") 292 | end 293 | if not rawget(self, TRANSLATIONS) then 294 | AceLocale.error(self, "No translations registered.") 295 | end 296 | rawset(self, STRICTNESS, strict) 297 | refixInstance(self) 298 | end 299 | 300 | local function initReverse(self) 301 | rawset(self, REVERSE_TRANSLATIONS, {}) 302 | local alpha = self[TRANSLATIONS] 303 | local bravo = self[REVERSE_TRANSLATIONS] 304 | for base, localized in pairs(alpha) do 305 | bravo[localized] = base 306 | end 307 | end 308 | 309 | function AceLocale.prototype:GetTranslation(text) 310 | AceLocale.argCheck(self, text, 1, "string", "number") 311 | if not rawget(self, TRANSLATIONS) then 312 | AceLocale.error(self, "No translations registered") 313 | end 314 | return self[text] 315 | end 316 | 317 | function AceLocale.prototype:GetStrictTranslation(text) 318 | AceLocale.argCheck(self, text, 1, "string", "number") 319 | local x = rawget(self, TRANSLATIONS) 320 | if not x then 321 | AceLocale.error(self, "No translations registered") 322 | end 323 | local value = rawget(x, text) 324 | if value == nil then 325 | AceLocale.error(self, "Translation %q does not exist for locale %s", text, self[CURRENT_LOCALE]) 326 | end 327 | return value 328 | end 329 | 330 | function AceLocale.prototype:GetReverseTranslation(text) 331 | local x = rawget(self, REVERSE_TRANSLATIONS) 332 | if not x then 333 | if not rawget(self, TRANSLATIONS) then 334 | AceLocale.error(self, "No translations registered") 335 | end 336 | initReverse(self) 337 | x = self[REVERSE_TRANSLATIONS] 338 | end 339 | local translation = x[text] 340 | if not translation then 341 | AceLocale.error(self, "Reverse translation for %q does not exist", text) 342 | end 343 | return translation 344 | end 345 | 346 | function AceLocale.prototype:GetIterator() 347 | local x = rawget(self, TRANSLATIONS) 348 | if not x then 349 | AceLocale.error(self, "No translations registered") 350 | end 351 | return next, x, nil 352 | end 353 | 354 | function AceLocale.prototype:GetReverseIterator() 355 | local x = rawget(self, REVERSE_TRANSLATIONS) 356 | if not x then 357 | if not rawget(self, TRANSLATIONS) then 358 | AceLocale.error(self, "No translations registered") 359 | end 360 | initReverse(self) 361 | x = self[REVERSE_TRANSLATIONS] 362 | end 363 | return next, x, nil 364 | end 365 | 366 | function AceLocale.prototype:HasTranslation(text) 367 | AceLocale.argCheck(self, text, 1, "string", "number") 368 | local x = rawget(self, TRANSLATIONS) 369 | if not x then 370 | AceLocale.error(self, "No translations registered") 371 | end 372 | return rawget(x, text) and true 373 | end 374 | 375 | function AceLocale.prototype:HasReverseTranslation(text) 376 | local x = rawget(self, REVERSE_TRANSLATIONS) 377 | if not x then 378 | if not rawget(self, TRANSLATIONS) then 379 | AceLocale.error(self, "No translations registered") 380 | end 381 | initReverse(self) 382 | x = self[REVERSE_TRANSLATIONS] 383 | end 384 | return x[text] and true 385 | end 386 | 387 | function AceLocale.prototype:Debug() 388 | if not rawget(self, DEBUGGING) then 389 | return 390 | end 391 | local words = {} 392 | local locales = {"enUS", "ruRU", "deDE", "frFR", "koKR", "zhCN", "zhTW", "esES"} 393 | local localizations = {} 394 | DEFAULT_CHAT_FRAME:AddMessage("--- AceLocale Debug ---") 395 | for _,locale in ipairs(locales) do 396 | if not self[TRANSLATION_TABLES][locale] then 397 | DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q not found", locale)) 398 | else 399 | localizations[locale] = self[TRANSLATION_TABLES][locale] 400 | end 401 | end 402 | local localeDebug = {} 403 | for locale, localization in pairs(localizations) do 404 | localeDebug[locale] = {} 405 | for word in pairs(localization) do 406 | if type(localization[word]) == "table" then 407 | if type(words[word]) ~= "table" then 408 | words[word] = {} 409 | end 410 | for bit in pairs(localization[word]) do 411 | if type(localization[word][bit]) == "string" then 412 | words[word][bit] = true 413 | end 414 | end 415 | elseif type(localization[word]) == "string" then 416 | words[word] = true 417 | end 418 | end 419 | end 420 | for word in pairs(words) do 421 | if type(words[word]) == "table" then 422 | for bit in pairs(words[word]) do 423 | for locale, localization in pairs(localizations) do 424 | if not rawget(localization, word) or not localization[word][bit] then 425 | localeDebug[locale][word .. "::" .. bit] = true 426 | end 427 | end 428 | end 429 | else 430 | for locale, localization in pairs(localizations) do 431 | if not rawget(localization, word) then 432 | localeDebug[locale][word] = true 433 | end 434 | end 435 | end 436 | end 437 | for locale, t in pairs(localeDebug) do 438 | if not next(t) then 439 | DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q complete", locale)) 440 | else 441 | DEFAULT_CHAT_FRAME:AddMessage(string.format("Locale %q missing:", locale)) 442 | for word in pairs(t) do 443 | DEFAULT_CHAT_FRAME:AddMessage(string.format(" %q", word)) 444 | end 445 | end 446 | end 447 | DEFAULT_CHAT_FRAME:AddMessage("--- End AceLocale Debug ---") 448 | end 449 | 450 | setmetatable(AceLocale.prototype, { 451 | __index = function(self, k) 452 | 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. 453 | AceLocale.error(lastSelf or self, "Translation %q does not exist.", k) 454 | end 455 | return nil 456 | end 457 | }) 458 | 459 | local function activate(self, oldLib, oldDeactivate) 460 | AceLocale = self 461 | 462 | self.frame = oldLib and oldLib.frame or CreateFrame("Frame") 463 | self.registry = oldLib and oldLib.registry or {} 464 | self.BASE_TRANSLATIONS = oldLib and oldLib.BASE_TRANSLATIONS or {} 465 | self.DEBUGGING = oldLib and oldLib.DEBUGGING or {} 466 | self.TRANSLATIONS = oldLib and oldLib.TRANSLATIONS or {} 467 | self.BASE_LOCALE = oldLib and oldLib.BASE_LOCALE or {} 468 | self.TRANSLATION_TABLES = oldLib and oldLib.TRANSLATION_TABLES or {} 469 | self.REVERSE_TRANSLATIONS = oldLib and oldLib.REVERSE_TRANSLATIONS or {} 470 | self.STRICTNESS = oldLib and oldLib.STRICTNESS or {} 471 | self.NAME = oldLib and oldLib.NAME or {} 472 | self.DYNAMIC_LOCALES = oldLib and oldLib.DYNAMIC_LOCALES or {} 473 | self.CURRENT_LOCALE = oldLib and oldLib.CURRENT_LOCALE or {} 474 | 475 | BASE_TRANSLATIONS = self.BASE_TRANSLATIONS 476 | DEBUGGING = self.DEBUGGING 477 | TRANSLATIONS = self.TRANSLATIONS 478 | BASE_LOCALE = self.BASE_LOCALE 479 | TRANSLATION_TABLES = self.TRANSLATION_TABLES 480 | REVERSE_TRANSLATIONS = self.REVERSE_TRANSLATIONS 481 | STRICTNESS = self.STRICTNESS 482 | NAME = self.NAME 483 | DYNAMIC_LOCALES = self.DYNAMIC_LOCALES 484 | CURRENT_LOCALE = self.CURRENT_LOCALE 485 | 486 | 487 | local GetTime = GetTime 488 | local timeUntilClear = GetTime() + 5 489 | scheduleClear = function() 490 | if next(newRegistries) then 491 | self.frame:Show() 492 | timeUntilClear = GetTime() + 5 493 | end 494 | end 495 | 496 | if not self.registry then 497 | self.registry = {} 498 | else 499 | for name, instance in pairs(self.registry) do 500 | local name = name 501 | local mt = getmetatable(instance) 502 | setmetatable(instance, nil) 503 | instance[NAME] = name 504 | local strict 505 | if instance[STRICTNESS] ~= nil then 506 | strict = instance[STRICTNESS] 507 | elseif instance[TRANSLATIONS] ~= instance[BASE_TRANSLATIONS] then 508 | if getmetatable(instance[TRANSLATIONS]).__index == oldLib.prototype then 509 | strict = true 510 | end 511 | end 512 | instance[STRICTNESS] = strict and true or false 513 | refixInstance(instance) 514 | end 515 | end 516 | 517 | self.frame:SetScript("OnEvent", scheduleClear) 518 | self.frame:SetScript("OnUpdate", function() -- (this, elapsed) 519 | if timeUntilClear - GetTime() <= 0 then 520 | self.frame:Hide() 521 | for k in pairs(newRegistries) do 522 | clearCache(k) 523 | newRegistries[k] = nil 524 | k = nil 525 | end 526 | end 527 | end) 528 | self.frame:UnregisterAllEvents() 529 | self.frame:RegisterEvent("ADDON_LOADED") 530 | self.frame:RegisterEvent("PLAYER_ENTERING_WORLD") 531 | self.frame:Show() 532 | 533 | if oldDeactivate then 534 | oldDeactivate(oldLib) 535 | end 536 | end 537 | 538 | AceLibrary:Register(AceLocale, MAJOR_VERSION, MINOR_VERSION, activate) 539 | -------------------------------------------------------------------------------- /settings.lua: -------------------------------------------------------------------------------- 1 | if not Cursive.superwow then 2 | return 3 | end 4 | 5 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 6 | Cursive:RegisterDB("CursiveDB") 7 | Cursive:RegisterDefaults("profile", { 8 | caption = L["Cursive"], 9 | anchor = "CENTER", 10 | x = -240, 11 | y = 120, 12 | 13 | -- editable 14 | enabled = true, 15 | clickthrough = false, 16 | showbackdrop = false, 17 | showtitle = true, 18 | showtargetindicator = true, 19 | showraidicons = true, 20 | showhealthbar = true, 21 | showunitname = true, 22 | 23 | shareddebuffs = { 24 | faeriefire = false, 25 | }, 26 | 27 | alwaysshowcurrenttarget = true, 28 | 29 | scale = 1, 30 | healthwidth = 100, 31 | height = 16, 32 | bartexture = "Interface\\TargetingFrame\\UI-StatusBar", 33 | 34 | raidiconsize = 16, 35 | curseiconsize = 16, 36 | maxcurses = 5, 37 | spacing = 4, 38 | maxrow = 10, 39 | maxcol = 1, 40 | textsize = 9, 41 | cursetimersize = 11, 42 | 43 | curseordering = L["Expiring soonest -> latest"], 44 | curseshowdecimals = false, 45 | invertbars = false, 46 | expandupwards = false, 47 | 48 | filterincombat = true, 49 | filterhostile = true, 50 | filterattackable = true, 51 | filterrange = false, 52 | filterraidmark = false, 53 | filterhascurse = false, 54 | filterignored = true, 55 | 56 | ignorelist = {}, 57 | ignorelistuseregex = false, 58 | }) 59 | 60 | local function splitString(str, delimiter) 61 | local result = {} 62 | local from = 1 63 | local delim_from, delim_to = string.find(str, delimiter, from) 64 | while delim_from do 65 | table.insert(result, string.sub(str, from, delim_from - 1)) 66 | from = delim_to + 1 67 | delim_from, delim_to = string.find(str, delimiter, from) 68 | end 69 | table.insert(result, string.sub(str, from)) 70 | return result 71 | end 72 | 73 | local barOptions = { 74 | ["invertbars"] = { 75 | type = "toggle", 76 | name = "Invert Bar Display", 77 | desc = "Show sections in order 3-2-1 and reverse element order in sections 1 and 3", 78 | order = 1, 79 | get = function() 80 | return Cursive.db.profile.invertbars 81 | end, 82 | set = function(v) 83 | Cursive.db.profile.invertbars = v 84 | Cursive.UpdateFramesFromConfig() 85 | end, 86 | }, 87 | ["expandupwards"] = { 88 | type = "toggle", 89 | name = "Expand Bars Upwards", 90 | desc = "Make bars expand upwards instead of downwards", 91 | order = 2, 92 | get = function() 93 | return Cursive.db.profile.expandupwards 94 | end, 95 | set = function(v) 96 | Cursive.db.profile.expandupwards = v 97 | Cursive.UpdateFramesFromConfig() 98 | end, 99 | }, 100 | ["spacer1"] = { 101 | type = "header", 102 | name = "Section Display", 103 | order = 5, 104 | }, 105 | ["showtargetindicator"] = { 106 | type = "toggle", 107 | name = L["Show Targeting Arrow"], 108 | desc = L["Show Targeting Arrow"], 109 | order = 10, 110 | get = function() 111 | return Cursive.db.profile.showtargetindicator 112 | end, 113 | set = function(v) 114 | Cursive.db.profile.showtargetindicator = v 115 | Cursive.UpdateFramesFromConfig() 116 | end, 117 | }, 118 | ["showraidicons"] = { 119 | type = "toggle", 120 | name = L["Show Raid Icons"], 121 | desc = L["Show Raid Icons"], 122 | order = 15, 123 | get = function() 124 | return Cursive.db.profile.showraidicons 125 | end, 126 | set = function(v) 127 | Cursive.db.profile.showraidicons = v 128 | Cursive.UpdateFramesFromConfig() 129 | end, 130 | }, 131 | ["showhealthbar"] = { 132 | type = "toggle", 133 | name = L["Show Health Bar"], 134 | desc = L["Show Health Bar"], 135 | order = 20, 136 | get = function() 137 | return Cursive.db.profile.showhealthbar 138 | end, 139 | set = function(v) 140 | Cursive.db.profile.showhealthbar = v 141 | Cursive.UpdateFramesFromConfig() 142 | end, 143 | }, 144 | ["showunitname"] = { 145 | type = "toggle", 146 | name = L["Show Unit Name"], 147 | desc = L["Show Unit Name"], 148 | order = 25, 149 | get = function() 150 | return Cursive.db.profile.showunitname 151 | end, 152 | set = function(v) 153 | Cursive.db.profile.showunitname = v 154 | Cursive.UpdateFramesFromConfig() 155 | end, 156 | }, 157 | ["alwaysshowcurrenttarget"] = { 158 | type = "toggle", 159 | name = "Always Show Current Target", 160 | desc = "Always show current target at the bottom of the mob list if it is not already shown", 161 | order = 30, 162 | get = function() 163 | return Cursive.db.profile.alwaysshowcurrenttarget 164 | end, 165 | set = function(v) 166 | Cursive.db.profile.alwaysshowcurrenttarget = v 167 | end, 168 | }, 169 | ["spacer2"] = { 170 | type = "header", 171 | name = "Size & Appearance", 172 | order = 35, 173 | }, 174 | ["barwidth"] = { 175 | type = "range", 176 | name = L["Health Bar/Unit Name Width"], 177 | desc = L["Health Bar/Unit Name Width"], 178 | order = 40, 179 | min = 30, 180 | max = 150, 181 | step = 5, 182 | get = function() 183 | return Cursive.db.profile.healthwidth 184 | end, 185 | set = function(v) 186 | if v ~= Cursive.db.profile.healthwidth then 187 | Cursive.db.profile.healthwidth = v 188 | Cursive.UpdateFramesFromConfig() 189 | end 190 | end, 191 | }, 192 | ["barheight"] = { 193 | type = "range", 194 | name = L["Health Bar/Unit Name Height"], 195 | desc = L["Health Bar/Unit Name Height"], 196 | order = 50, 197 | min = 10, 198 | max = 30, 199 | step = 2, 200 | get = function() 201 | return Cursive.db.profile.height 202 | end, 203 | set = function(v) 204 | if v ~= Cursive.db.profile.height then 205 | Cursive.db.profile.height = v 206 | Cursive.UpdateFramesFromConfig() 207 | end 208 | end, 209 | }, 210 | ["bartexture"] = { 211 | type = "text", 212 | name = L["Health Bar Texture"], 213 | desc = L["Health Bar Texture Desc"], 214 | order = 55, 215 | usage = "Interface\\TargetingFrame\\UI-StatusBar", 216 | get = function() 217 | return Cursive.db.profile.bartexture 218 | end, 219 | set = function(v) 220 | if v ~= Cursive.db.profile.bartexture then 221 | Cursive.db.profile.bartexture = v 222 | Cursive.UpdateFramesFromConfig() 223 | end 224 | end, 225 | }, 226 | ["raidiconsize"] = { 227 | type = "range", 228 | name = L["Raid Icon Size"], 229 | desc = L["Raid Icon Size"], 230 | order = 60, 231 | min = 10, 232 | max = 30, 233 | step = 1, 234 | get = function() 235 | return Cursive.db.profile.raidiconsize 236 | end, 237 | set = function(v) 238 | if v ~= Cursive.db.profile.raidiconsize then 239 | Cursive.db.profile.raidiconsize = v 240 | Cursive.UpdateFramesFromConfig() 241 | end 242 | end, 243 | }, 244 | ["curseiconsize"] = { 245 | type = "range", 246 | name = L["Curse Icon Size"], 247 | desc = L["Curse Icon Size"], 248 | order = 70, 249 | min = 10, 250 | max = 30, 251 | step = 1, 252 | get = function() 253 | return Cursive.db.profile.curseiconsize 254 | end, 255 | set = function(v) 256 | if v ~= Cursive.db.profile.curseiconsize then 257 | Cursive.db.profile.curseiconsize = v 258 | Cursive.UpdateFramesFromConfig() 259 | end 260 | end, 261 | }, 262 | ["curseordering"] = { 263 | type = "text", 264 | name = L["Curse Ordering"], 265 | desc = L["Curse Ordering"], 266 | order = 72, 267 | get = function() 268 | return Cursive.db.profile.curseordering 269 | end, 270 | validate = { L["Order applied"], L["Expiring soonest -> latest"], L["Expiring latest -> soonest"] }, 271 | set = function(v) 272 | Cursive.db.profile.curseordering = v 273 | end, 274 | }, 275 | ["curseshowdecimals"] = { 276 | type = "toggle", 277 | name = L["Decimal Duration"], 278 | desc = L["Decimal Duration Desc"], 279 | order = 74, 280 | get = function() 281 | return Cursive.db.profile.curseshowdecimals 282 | end, 283 | set = function(v) 284 | Cursive.db.profile.curseshowdecimals = v 285 | Cursive.UpdateFramesFromConfig() 286 | end, 287 | }, 288 | ["spacing"] = { 289 | type = "range", 290 | name = L["Spacing"], 291 | desc = L["Spacing"], 292 | order = 80, 293 | min = 0, 294 | max = 10, 295 | step = 1, 296 | get = function() 297 | return Cursive.db.profile.spacing 298 | end, 299 | set = function(v) 300 | if v ~= Cursive.db.profile.spacing then 301 | Cursive.db.profile.spacing = v 302 | Cursive.UpdateFramesFromConfig() 303 | end 304 | end, 305 | }, 306 | ["textsize"] = { 307 | type = "range", 308 | name = L["Name/Hp Text Size"], 309 | desc = L["Name/Hp Text Size"], 310 | order = 90, 311 | min = 8, 312 | max = 20, 313 | step = 1, 314 | get = function() 315 | return Cursive.db.profile.textsize 316 | end, 317 | set = function(v) 318 | if v ~= Cursive.db.profile.textsize then 319 | Cursive.db.profile.textsize = v 320 | Cursive.UpdateFramesFromConfig() 321 | end 322 | end, 323 | }, 324 | ["cursetimersize"] = { 325 | type = "range", 326 | name = L["Curse Timer Text Size"], 327 | desc = L["Curse Timer Text Size"], 328 | order = 95, 329 | min = 6, 330 | max = 20, 331 | step = 1, 332 | get = function() 333 | return Cursive.db.profile.cursetimersize 334 | end, 335 | set = function(v) 336 | if v ~= Cursive.db.profile.cursetimersize then 337 | Cursive.db.profile.cursetimersize = v 338 | Cursive.UpdateFramesFromConfig() 339 | end 340 | end, 341 | }, 342 | ["scale"] = { 343 | type = "range", 344 | name = L["Scale"], 345 | desc = L["Scale"], 346 | order = 100, 347 | min = 0.5, 348 | max = 2, 349 | step = 0.1, 350 | get = function() 351 | return Cursive.db.profile.scale 352 | end, 353 | set = function(v) 354 | if v ~= Cursive.db.profile.scale then 355 | Cursive.db.profile.scale = v 356 | Cursive.UpdateFramesFromConfig() 357 | end 358 | end, 359 | }, 360 | } 361 | 362 | local mobFilters = { 363 | ["incombat"] = { 364 | type = "toggle", 365 | name = L["In Combat"], 366 | desc = L["In Combat"], 367 | order = 1, 368 | get = function() 369 | return Cursive.db.profile.filterincombat 370 | end, 371 | set = function(v) 372 | Cursive.db.profile.filterincombat = v 373 | end, 374 | }, 375 | ["hostile"] = { 376 | type = "toggle", 377 | name = L["Hostile"], 378 | desc = L["Hostile"], 379 | order = 11, 380 | get = function() 381 | return Cursive.db.profile.filterhostile 382 | end, 383 | set = function(v) 384 | Cursive.db.profile.filterhostile = v 385 | end, 386 | }, 387 | ["attackable"] = { 388 | type = "toggle", 389 | name = L["Attackable"], 390 | desc = L["Attackable"], 391 | order = 22, 392 | get = function() 393 | return Cursive.db.profile.filterattackable 394 | end, 395 | set = function(v) 396 | Cursive.db.profile.filterattackable = v 397 | end, 398 | }, 399 | ["player"] = { 400 | type = "toggle", 401 | name = L["Player"], 402 | desc = L["Player Desc"], 403 | order = 33, 404 | get = function() 405 | return Cursive.db.profile.filterplayer 406 | end, 407 | set = function(v) 408 | Cursive.db.profile.filterplayer = v 409 | end, 410 | }, 411 | ["notplayer"] = { 412 | type = "toggle", 413 | name = L["Not Player"], 414 | desc = L["Not Player Desc"], 415 | order = 33, 416 | get = function() 417 | return Cursive.db.profile.filternotplayer 418 | end, 419 | set = function(v) 420 | Cursive.db.profile.filternotplayer = v 421 | end, 422 | }, 423 | ["range"] = { 424 | type = "toggle", 425 | name = IsSpellInRange and L["Within 45 Range"] or L["Within 28 Range"], 426 | desc = IsSpellInRange and L["Within 45 Range"] or L["Within 28 Range"], 427 | order = 44, 428 | get = function() 429 | return Cursive.db.profile.filterrange 430 | end, 431 | set = function(v) 432 | Cursive.db.profile.filterrange = v 433 | end, 434 | }, 435 | ["raidmark"] = { 436 | type = "toggle", 437 | name = L["Has Raid Mark"], 438 | desc = L["Has Raid Mark"], 439 | order = 55, 440 | get = function() 441 | return Cursive.db.profile.filterraidmark 442 | end, 443 | set = function(v) 444 | Cursive.db.profile.filterraidmark = v 445 | end, 446 | }, 447 | ["hascurse"] = { 448 | type = "toggle", 449 | name = L["Has Curse"], 450 | desc = L["Only show units you have cursed"], 451 | order = 66, 452 | get = function() 453 | return Cursive.db.profile.filterhascurse 454 | end, 455 | set = function(v) 456 | Cursive.db.profile.filterhascurse = v 457 | end, 458 | }, 459 | ["notignored"] = { 460 | type = "toggle", 461 | name = L["Not ignored"], 462 | desc = L["Not ignored"], 463 | order = 67, 464 | get = function() 465 | return Cursive.db.profile.filterignored 466 | end, 467 | set = function(v) 468 | Cursive.db.profile.filterignored = v 469 | end, 470 | }, 471 | ["ignorelist"] = { 472 | type = "text", 473 | name = L["Ignored Mobs List (Enter to save)"], 474 | desc = L["Ignored Mobs Desc"], 475 | usage = "whelp, black dragonkin, player3", 476 | order = 68, 477 | get = function() 478 | if Cursive.db.profile.ignorelist and table.getn(Cursive.db.profile.ignorelist) > 0 then 479 | return table.concat(Cursive.db.profile.ignorelist, ",") or "" 480 | end 481 | return "" 482 | end, 483 | set = function(v) 484 | if not v or v == "" then 485 | Cursive.db.profile.ignorelist = {} 486 | else 487 | Cursive.db.profile.ignorelist = splitString(v, ","); 488 | end 489 | -- check for common lua regex patterns 490 | Cursive.db.profile.ignorelistuseregex = string.find(v, "[*+%%?]") ~= nil 491 | end, 492 | }, 493 | } 494 | 495 | local sharedDebuffs = { 496 | ["sharedFaerieFire"] = { 497 | type = "toggle", 498 | name = L["Shared Faerie Fire"], 499 | desc = L["This will show other player's Faerie Fires and avoid trying to cast Faerie Fire on those mobs"], 500 | order = 10, 501 | get = function() 502 | return Cursive.db.profile.shareddebuffs.faeriefire 503 | end, 504 | set = function(v) 505 | Cursive.db.profile.shareddebuffs.faeriefire = v 506 | Cursive.UpdateFramesFromConfig() 507 | end, 508 | }, 509 | } 510 | 511 | Cursive.cmdtable = { 512 | type = "group", 513 | handler = Cursive, 514 | args = { 515 | ["enabled"] = { 516 | type = "toggle", 517 | name = L["Enabled"], 518 | desc = L["Enable/Disable Cursive"], 519 | order = 1, 520 | get = function() 521 | return Cursive.db.profile.enabled 522 | end, 523 | set = function(v) 524 | Cursive.db.profile.enabled = v 525 | if v == true then 526 | Cursive.core.enable() 527 | else 528 | Cursive.core.disable() 529 | end 530 | end, 531 | }, 532 | ["showtitle"] = { 533 | type = "toggle", 534 | name = L["Show Title"], 535 | desc = L["Show the title of the frame"], 536 | order = 3, 537 | get = function() 538 | return Cursive.db.profile.showtitle 539 | end, 540 | set = function(v) 541 | Cursive.db.profile.showtitle = v 542 | Cursive.UpdateFramesFromConfig() 543 | end, 544 | }, 545 | ["clickthrough"] = { 546 | type = "toggle", 547 | name = L["Allow clickthrough"], 548 | desc = L["This will allow you to click through the frame to target mobs behind it, but prevents dragging the frame."], 549 | order = 5, 550 | get = function() 551 | return Cursive.db.profile.clickthrough 552 | end, 553 | set = function(v) 554 | Cursive.db.profile.clickthrough = v 555 | Cursive.UpdateFramesFromConfig() 556 | end, 557 | }, 558 | ["showbackdrop"] = { 559 | type = "toggle", 560 | name = L["Show Frame Background"], 561 | desc = L["Toggle the frame background to help with positioning"], 562 | order = 7, 563 | get = function() 564 | return Cursive.db.profile.showbackdrop 565 | end, 566 | set = function(v) 567 | Cursive.db.profile.showbackdrop = v 568 | Cursive.UpdateFramesFromConfig() 569 | end, 570 | }, 571 | ["resetframe"] = { 572 | type = "execute", 573 | name = L["Reset Frame"], 574 | desc = L["Move the frame back to the default position"], 575 | order = 9, 576 | func = function() 577 | Cursive.db.profile.anchor = "CENTER" 578 | Cursive.db.profile.x = -100 579 | Cursive.db.profile.y = -100 580 | Cursive.UpdateFramesFromConfig() 581 | end, 582 | }, 583 | ["spacer"] = { 584 | type = "header", 585 | name = " ", 586 | order = 11, 587 | }, 588 | ["bardisplay"] = { 589 | type = "group", 590 | name = L["Bar Display Settings"], 591 | desc = L["Bar Display Settings"], 592 | order = 13, 593 | args = barOptions 594 | }, 595 | ["filters"] = { 596 | type = "group", 597 | name = L["Mob filters"], 598 | desc = L["Target and Raid Marks always shown"], 599 | order = 19, 600 | args = mobFilters 601 | }, 602 | ["shareddebuffs"] = { 603 | type = "group", 604 | name = L["Shared Debuffs"], 605 | desc = L["Shared Debuffs"], 606 | order = 20, 607 | args = sharedDebuffs 608 | }, 609 | ["spacer2"] = { 610 | type = "header", 611 | name = " ", 612 | order = 21, 613 | }, 614 | ["maxcurses"] = { 615 | type = "range", 616 | name = L["Max Curses"], 617 | desc = L["Max Curses"], 618 | order = 22, 619 | min = 1, 620 | max = 8, 621 | step = 1, 622 | get = function() 623 | return Cursive.db.profile.maxcurses 624 | end, 625 | set = function(v) 626 | if v ~= Cursive.db.profile.maxcurses then 627 | Cursive.db.profile.maxcurses = v 628 | Cursive.UpdateFramesFromConfig() 629 | end 630 | end, 631 | }, 632 | ["maxrow"] = { 633 | type = "range", 634 | name = L["Max Rows"], 635 | desc = L["Max Rows"], 636 | order = 30, 637 | min = 1, 638 | max = 20, 639 | step = 1, 640 | get = function() 641 | return Cursive.db.profile.maxrow 642 | end, 643 | set = function(v) 644 | if v ~= Cursive.db.profile.maxrow then 645 | Cursive.db.profile.maxrow = v 646 | Cursive.UpdateFramesFromConfig() 647 | end 648 | end, 649 | }, 650 | ["maxcol"] = { 651 | type = "range", 652 | name = L["Max Columns"], 653 | desc = L["Max Columns"], 654 | order = 40, 655 | min = 1, 656 | max = 20, 657 | step = 1, 658 | get = function() 659 | return Cursive.db.profile.maxcol 660 | end, 661 | set = function(v) 662 | if v ~= Cursive.db.profile.maxcol then 663 | Cursive.db.profile.maxcol = v 664 | Cursive.UpdateFramesFromConfig() 665 | end 666 | end, 667 | }, 668 | } 669 | } 670 | 671 | local deuce = Cursive:NewModule("Options Menu") 672 | deuce.hasFuBar = IsAddOnLoaded("FuBar") and FuBar 673 | deuce.consoleCmd = not deuce.hasFuBar 674 | 675 | CursiveOptions = AceLibrary("AceAddon-2.0"):new("AceDB-2.0", "FuBarPlugin-2.0") 676 | CursiveOptions.name = "FuBar - Cursive" 677 | CursiveOptions:RegisterDB("CursiveDB") 678 | CursiveOptions.hasIcon = "Interface\\Icons\\spell_shadow_deathcoil" 679 | CursiveOptions.defaultMinimapPosition = 180 680 | CursiveOptions.independentProfile = true 681 | CursiveOptions.hideWithoutStandby = false 682 | 683 | -- XXX total hack 684 | CursiveOptions.OnMenuRequest = Cursive.cmdtable 685 | local args = AceLibrary("FuBarPlugin-2.0"):GetAceOptionsDataTable(CursiveOptions) 686 | for k, v in pairs(args) do 687 | if CursiveOptions.OnMenuRequest.args[k] == nil then 688 | CursiveOptions.OnMenuRequest.args[k] = v 689 | end 690 | end 691 | -- XXX end hack 692 | -------------------------------------------------------------------------------- /Localization.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | 3 | L:RegisterTranslations("enUS", function() 4 | return { 5 | ["|cffffcc00Cursive:|cffffaaaa Commands:"] = true, 6 | ["|cffffcc00Priority choices:"] = true, 7 | ["|cffffcc00Options (separate with ,):"] = true, 8 | ["Display text warnings when a curse fails to cast."] = true, 9 | ["Play a sound when a curse is resisted."] = true, 10 | ["Play a sound when a curse is about to expire."] = true, 11 | ["Allow out of combat targets to be multicursed. Would only consider using this solo to avoid potentially griefing raids/dungeons by pulling unintended mobs."] = true, 12 | ["Minimum HP for a target to be considered. Example usage minhp=10000. "] = true, 13 | ["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."] = true, 14 | ["Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff."] = true, 15 | ["Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff."] = true, 16 | ["Time threshold at which to allow refreshing a curse. Default is 0 seconds."] = true, 17 | ["Always prioritize current target when choosing target for multicurse. Does not affect 'curse' command."] = true, 18 | ["Ignore the current target when choosing target for multicurse. Does not affect 'curse' command."] = true, 19 | ["Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command."] = true, 20 | ["/cursive curse ||>: Casts spell if not already on target/guid"] = true, 21 | ["/cursive multicurse ||>: Picks target based on priority and casts spell if not already on target"] = true, 22 | ["/cursive target ||>: Targets unit based on priority if spell in range and not already on target"] = true, 23 | ["Target with the highest HP."] = true, 24 | ["Target with the lowest HP."] = true, 25 | ["Target with the highest raid mark."] = true, 26 | ["Target with the highest raid mark with Cross set to -1 and Skull set to -2 (Square highest prio at 6)."] = true, 27 | ["Target with the lowest raid mark."] = true, 28 | ["Target with the highest HP and raid mark."] = true, 29 | ["Same as HIGHEST_HP_RAID_MARK but with RAID_MARK_SQUARE mark prio."] = true, 30 | ["Same as HIGHEST_HP_RAID_MARK but with INVERSE_RAID_MARK mark prio."] = true, 31 | ["|cffffcc00Cursive:|cffffaaaa Couldn't find a target to curse."] = true, 32 | ["curse_duration_format"] = ".*over ([%d.]+) sec.", 33 | ["Shared Debuffs"] = true, 34 | ["|cffffcc00Cursive:|cffffaaaa Unknown command."] = true, 35 | 36 | -- curses 37 | ["(.+) fades from (.+)"] = true, 38 | ["Your (.+) was resisted by (.+)"] = true, 39 | ["Your (.+) missed (.+)"] = true, 40 | ["Your (.+) is parried by (.+)"] = true, 41 | ["Your (.+) fail.+\. (.+) is immune"] = true, 42 | ["Your (.+) was blocked by (.+)"] = true, 43 | ["Your (.+) was dodged by (.+)"] = true, 44 | ["Your Molten Blast(.+)for .+ Fire damage"] = true, 45 | 46 | -- global 47 | ["|cffffcc00Cursive:|cffffaaaa Couldn't detect SuperWoW."] = true, 48 | ["|cffffcc00Cursive:|cffffaaaa Loaded. /cursive for commands and minimap icon for options."] = true, 49 | 50 | 51 | -- settings 52 | ["Cursive"] = true, 53 | ["Show Targeting Arrow"] = true, 54 | ["Show Raid Icons"] = true, 55 | ["Show Health Bar"] = true, 56 | ["Show Unit Name"] = true, 57 | ["Health Bar/Unit Name Width"] = true, 58 | ["Health Bar/Unit Name Height"] = true, 59 | ["Health Bar Texture"] = true, 60 | ["Health Bar Texture Desc"] = "Full path to the texture file like the default: Interface\\TargetingFrame\\UI-StatusBar", 61 | ["Raid Icon Size"] = true, 62 | ["Curse Icon Size"] = true, 63 | ["Spacing"] = true, 64 | ["Name/Hp Text Size"] = true, 65 | ["Curse Timer Text Size"] = true, 66 | ["Scale"] = true, 67 | ["In Combat"] = true, 68 | ["Player"] = true, 69 | ["Player Desc"] = "Only show player units", 70 | ["Not Player"] = true, 71 | ["Not Player Desc"] = "Only show NON player units", 72 | ["Hostile"] = true, 73 | ["Attackable"] = true, 74 | ["Within 28 Range"] = true, 75 | ["Within 45 Range"] = true, 76 | ["Has Raid Mark"] = true, 77 | ["Has Curse"] = true, 78 | ["Only show units you have cursed"] = true, 79 | ["Enabled"] = true, 80 | ["Enable/Disable Cursive"] = true, 81 | ["Show Title"] = true, 82 | ["Show the title of the frame"] = true, 83 | ["Allow clickthrough"] = true, 84 | ["This will allow you to click through the frame to target mobs behind it, but prevents dragging the frame."] = true, 85 | ["Show Frame Background"] = true, 86 | ["Toggle the frame background to help with positioning"] = true, 87 | ["Reset Frame"] = true, 88 | ["Move the frame back to the default position"] = true, 89 | ["Bar Display Settings"] = true, 90 | ["Mob filters"] = true, 91 | ["Target and Raid Marks always shown"] = true, 92 | ["Max Curses"] = true, 93 | ["Max Rows"] = true, 94 | ["Max Columns"] = true, 95 | ["Curse Ordering"] = true, 96 | ["Decimal Duration"] = true, 97 | ["Decimal Duration Desc"] = "Show 1 decimal place for curse durations when less than 10 seconds remaining.", 98 | ["Order applied"] = true, 99 | ["Expiring soonest -> latest"] = true, 100 | ["Expiring latest -> soonest"] = true, 101 | ["Order applied"] = true, 102 | ["Expiring soonest -> latest"] = true, 103 | ["Expiring latest -> soonest"] = true, 104 | ["Not ignored"] = true, 105 | ["Ignored Mobs List (Enter to save)"] = true, 106 | ["Ignored Mobs Desc"] = "Comma separated list of strings to ignore if found in the unit name. If you use any of these regex characters (*+%?) it will do a regex search otherwise it will do a plain text search. So both Rift-Lost and Rift%-Lost should work.", 107 | ["Shared Faerie Fire"] = true, 108 | ["This will show other player's Faerie Fires and avoid trying to cast Faerie Fire on those mobs"] = true, 109 | 110 | -- spells 111 | ["Rank"] = true, 112 | ["Rank 1"] = true, 113 | ["Rank 2"] = true, 114 | ["Rank 3"] = true, 115 | ["Rank 4"] = true, 116 | 117 | -- druid 118 | ["entangling roots"] = true, 119 | ["sleep"] = true, 120 | ["faerie fire"] = true, 121 | ["faerie fire (bear)()"] = true, 122 | ["faerie fire (feral)()"] = true, 123 | ["hibernate"] = true, 124 | ["insect swarm"] = true, 125 | ["moonfire"] = true, 126 | ["rake"] = true, 127 | ["rip"] = true, 128 | ["soothe animal"] = true, 129 | ["bash"] = true, 130 | ["Maim"] = true, 131 | ["demoralizing roar"] = true, 132 | ["challenging roar"] = true, 133 | ["ferocious bite"] = true, 134 | ["pounce bleed"] = true, 135 | 136 | -- hunter 137 | ["scorpid sting"] = true, 138 | ["serpent sting"] = true, 139 | ["viper sting"] = true, 140 | ["wing clip"] = true, 141 | ["concussive shot"] = true, 142 | ["wyvern sting"] = true, 143 | ["counterattack"] = true, 144 | ["hunter's mark"] = true, 145 | ["freezing trap"] = true, 146 | 147 | -- mage 148 | ["polymorph"] = true, 149 | ["polymorph: cow"] = true, 150 | ["polymorph: turtle"] = true, 151 | ["polymorph: pig"] = true, 152 | 153 | -- priest 154 | ["shackle undead"] = true, 155 | ["mind soothe"] = true, 156 | ["mind control"] = true, 157 | ["devouring plague"] = true, 158 | ["hex of weakness"] = true, 159 | ["shadow word: pain"] = true, 160 | ["vampiric embrace"] = true, 161 | ["holy fire"] = true, 162 | 163 | -- Paladin 164 | ["turn undead"] = true, 165 | 166 | -- rogue 167 | ["blind"] = true, 168 | ["sap"] = true, 169 | ["gouge"] = true, 170 | ["rupture"] = true, 171 | ["kidney shot"] = true, 172 | ["expose armor"] = true, 173 | ["garrote"] = true, 174 | ["deadly poison"] = true, 175 | ["deadly poison II"] = true, 176 | ["deadly poison III"] = true, 177 | ["deadly poison IV"] = true, 178 | ["deadly poison V"] = true, 179 | ["hemorrhage"] = true, 180 | ["cheap shot"] = true, 181 | 182 | -- shaman 183 | ["flame shock"] = true, 184 | ["molten blast"] = true, 185 | 186 | -- warlock 187 | ["conflagrate"] = true, 188 | ["corruption"] = true, 189 | ["curse of agony"] = true, 190 | ["curse of doom"] = true, 191 | ["curse of recklessness"] = true, 192 | ["curse of shadow"] = true, 193 | ["curse of the elements"] = true, 194 | ["curse of tongues"] = true, 195 | ["curse of weakness"] = true, 196 | ["curse of exhaustion"] = true, 197 | ["dark harvest"] = true, 198 | ["immolate"] = true, 199 | ["death coil"] = true, 200 | ["banish"] = true, 201 | ["siphon life"] = true, 202 | ["fear"] = true, 203 | 204 | -- warrior 205 | ["rend"] = true, 206 | } 207 | end) 208 | 209 | L:RegisterTranslations("zhCN", function() 210 | return { 211 | ["|cffffcc00Cursive:|cffffaaaa Commands:"] = "|cffffcc00Cursive:|cffffaaaa 命令:", 212 | ["|cffffcc00Priority choices:"] = "|cffffcc00优先级选择:", 213 | ["|cffffcc00Options (separate with ,):"] = "|cffffcc00选项 (用半角逗号分隔):", 214 | ["Display text warnings when a curse fails to cast."] = "当诅咒施放失败时显示文本警告.", 215 | ["Play a sound when a curse is resisted."] = "当诅咒被抵抗时播放声音.", 216 | ["Play a sound when a curse is about to expire."] = "当诅咒即将到期时播放声音.\n 详细使用说明及示例请查看readme文件", 217 | ["Allow out of combat targets to be multicursed. Would only consider using this solo to avoid potentially griefing raids/dungeons by pulling unintended mobs."] = "允许战斗外目标被多重诅咒。只有在单人游戏中才会考虑使用这个选项,以避免在副本中意外拉到怪物而造成困扰.", 218 | ["Minimum HP for a target to be considered. Example usage minhp=10000. "] = "目标考虑的最小生命值。例如:minhp=10000.", 219 | ["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."] = "通过名称过滤目标。可以是部分匹配。如果没有找到匹配项,命令将不执行.", 220 | ["Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff."] = "忽略已经在目标身上的指定法术ID。对于忽略已经有共享debuff的目标很有用.", 221 | ["Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff."] = "忽略已经在目标身上的指定法术纹理。对于忽略已经有共享debuff的目标很有用.", 222 | ["Time threshold at which to allow refreshing a curse. Default is 0 seconds."] = "允许刷新诅咒的时间阈值。默认是0秒.例如:refreshtime=2", 223 | ["Always prioritize current target when choosing target for multicurse. Does not affect 'curse' command."] = "选择多重诅咒目标时总是优先当前目标。不影响‘诅咒’命令。", 224 | ["Ignore the current target when choosing target for multicurse. Does not affect 'curse' command."] = "选择多重诅咒目标时忽略当前目标。不影响‘诅咒’命令。", 225 | ["Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command."] = "选择多重诅咒目标时只选择玩家,忽略NPC。不影响‘诅咒’命令。", 226 | ["/cursive curse ||>: Casts spell if not already on target/guid"] = "/cursive curse <法术名称>|<目标ID>|<选项列表,用半角逗号分隔>: 如果目标/ID身上没有此法术,目标ID可以是以下内容(mouseover), (player), (pet), (party)(%d), (partypet)(%d), (raid)(%d+), (raidpet)(%d+), (target), (targettarget)", 227 | ["/cursive multicurse ||>: Picks target based on priority and casts spell if not already on target"] = "/cursive multicurse <法术名称>|<优先级>|<选项列表,用半角逗号分隔>: 根据优先级选择目标并施放法术,如果目标上没有", 228 | ["/cursive target ||>: Targets unit based on priority if spell in range and not already on target"] = "/cursive target <法术名称>|<优先级>|<选项列表,用半角逗号分隔>:如果法术在范围内且目标上没有此法术,则根据优先级选择目标", 229 | ["Target with the highest HP."] = "选择生命值最高的目标.", 230 | ["Target with the lowest HP."] = "选择生命值最低的目标.", 231 | ["Target with the highest raid mark."] = "选择团队标记最高的目标.", 232 | ["Target with the highest raid mark with Cross set to -1 and Skull set to -2 (Square highest prio at 6)."] = "选择团队标记最高的目标,但是将红叉设为-1,骷髅设为-2(方块标记最高优先级为6).", 233 | ["Target with the lowest raid mark."] = "选择团队标记优先级最低的目标.(与RAID_MARK顺序相反)", 234 | ["Target with the highest HP and raid mark."] = "选择生命值和团队标记最高的目标.", 235 | ["Same as HIGHEST_HP_RAID_MARK but with RAID_MARK_SQUARE mark prio."] = "与HIGHEST_HP_RAID_MARK相同,但是RAID_MARK_SQUARE标记优先.", 236 | ["Same as HIGHEST_HP_RAID_MARK but with INVERSE_RAID_MARK mark prio."] = "与HIGHEST_HP_RAID_MARK相同,但是INVERSE_RAID_MARK标记优先.", 237 | ["|cffffcc00Cursive:|cffffaaaa Couldn't find a target to curse."] = "|cffffcc00Cursive:|cffffaaaa 找不到要诅咒的目标.", 238 | -- 法术描述样本 239 | -- 腐蚀术:腐蚀目标,在18.69秒内造成累计828到834点伤害。 240 | -- 精灵之火:使目标的护甲降低175点,持续40秒。在效果持续期间,目标无法潜行或隐形。 241 | -- 扫击:对目标造成19点伤害,并在9秒内造成总计39点额外伤害。伤害受到你的攻击强度加成,奖励1个连击点数。 242 | -- 虫群:敌人被飞虫围绕,攻击命中率降低2%,在18秒内受到总计99点自然伤害。 243 | -- 月火术:灼烧敌人,对其造成40到48点伤害,并在18秒内造成额外的120点奥术伤害。 244 | ["curse_duration_format"] = "(%d+%.?%d*)秒", 245 | ["Shared Debuffs"] = "共享debuff", 246 | ["|cffffcc00Cursive:|cffffaaaa Unknown command."] = "|cffffcc00Cursive:|cffffaaaa 未知命令.", 247 | 248 | -- curses 249 | ["(.+) fades from (.+)"] = "(.+)效果从(.+)身上消失", 250 | ["Your (.+) was resisted by (.+)"] = "你的(.+)被(.+)抵抗了", 251 | ["Your (.+) is parried by (.+)"] = "你的(.+)被(.+)招架了", 252 | ["Your (.+) missed (.+)"] = "你的(.+)没有击中(.+)", 253 | ["Your (.+) was dodged by (.+)"] = "你的(.+)被(.+)躲闪过去了", 254 | ["Your (.+) was blocked by (.+)"] = "你的(.+)被(.+)格挡了", 255 | ["Your (.+) fail.+\. (.+) is immune"] = "你的(.+)施放失败。(.+)对此免疫", 256 | ["Your Molten Blast(.+)for .+ Fire damage"] = "你的熔岩爆裂(.+)造成了.+点火焰伤害", 257 | 258 | -- global 259 | ["|cffffcc00Cursive:|cffffaaaa Couldn't detect SuperWoW."] = "|cffffcc00Cursive:|cffffaaaa 无法检测到SuperWoW.", 260 | ["|cffffcc00Cursive:|cffffaaaa Loaded. /cursive for commands and minimap icon for options."] = "|cffffcc00Cursive:|cffffaaaa 已加载。/cursive 用于查看帮助命令,小地图图标打开设置选项.", 261 | 262 | -- settings 263 | ["Cursive"] = "Cursive", 264 | ["Show Targeting Arrow"] = "显示目标箭头", 265 | ["Show Raid Icons"] = "显示团队标记", 266 | ["Show Health Bar"] = "显示生命条", 267 | ["Show Unit Name"] = "显示单位名称", 268 | ["Health Bar/Unit Name Width"] = "生命条宽度", 269 | ["Health Bar/Unit Name Height"] = "生命条高度", 270 | ["Health Bar Texture"] = "生命条纹理", 271 | ["Health Bar Texture Desc"] = "生命条纹理的完整路径,例如默认值:Interface\\TargetingFrame\\UI-StatusBar", 272 | ["Raid Icon Size"] = "团队标记大小", 273 | ["Curse Icon Size"] = "诅咒图标大小", 274 | ["Spacing"] = "间距", 275 | ["Name/Hp Text Size"] = "名称/生命值文字大小", 276 | ["Curse Timer Text Size"] = "诅咒计时器文字大小", 277 | ["Scale"] = "缩放", 278 | ["In Combat"] = "战斗中", 279 | ["Player"] = "玩家", 280 | ["Player Desc"] = "只显示玩家单位", 281 | ["Not Player"] = "非玩家", 282 | ["Not Player Desc"] = "只显示非玩家单位", 283 | ["Hostile"] = "敌对", 284 | ["Attackable"] = "可攻击", 285 | ["Within 28 Range"] = "在28范围内", 286 | ["Within 45 Range"] = "在45范围内", 287 | ["Has Raid Mark"] = "有团队标记", 288 | ["Has Curse"] = "有诅咒", 289 | ["Only show units you have cursed"] = "只显示你诅咒过的单位", 290 | ["Enabled"] = "启用", 291 | ["Enable/Disable Cursive"] = "启用/禁用 Cursive", 292 | ["Show Title"] = "显示标题", 293 | ["Show the title of the frame"] = "显示框架的标题", 294 | ["Allow clickthrough"] = "允许点击穿透", 295 | ["This will allow you to click through the frame to target mobs behind it, but prevents dragging the frame."] = "这将允许你点击穿透框架选中其背后的怪物,但会禁用拖动框架.", 296 | ["Show Frame Background"] = "显示框架背景", 297 | ["Toggle the frame background to help with positioning"] = "切换框架背景以帮助定位", 298 | ["Reset Frame"] = "重置框架", 299 | ["Move the frame back to the default position"] = "将框架移回默认位置", 300 | ["Bar Display Settings"] = "条形显示设置", 301 | ["Mob filters"] = "怪物过滤器", 302 | ["Target and Raid Marks always shown"] = "目标和团队标记始终显示", 303 | ["Max Curses"] = "最大诅咒数", 304 | ["Max Rows"] = "最大行数", 305 | ["Max Columns"] = "最大列数", 306 | ["Curse Ordering"] = "诅咒排序", 307 | ["Decimal Duration"] = "小数持续时间", 308 | ["Decimal Duration Desc"] = "当剩余时间少于10秒时,显示1位小数.", 309 | ["Order applied"] = "应用顺序", 310 | ["Expiring soonest -> latest"] = "即将到期->最新", 311 | ["Expiring latest -> soonest"] = "最新->即将到期", 312 | ["Not ignored"] = "不被忽略", 313 | ["Ignored Mobs List (Enter to save)"] = "忽略的怪物列表(回车保存)", 314 | ["Ignored Mobs Desc"] = "用逗号分隔的字符串列表,如果在单位名称中找到则忽略。如果你使用了这些正则表达式字符(*+%?),它将进行正则表达式搜索,否则将进行纯文本搜索。所以Rift-Lost和Rift%-Lost都应该有效.", 315 | ["Shared Faerie Fire"] = "共享精灵之火", 316 | ["This will show other player's Faerie Fires and avoid trying to cast Faerie Fire on those mobs"] = "这将显示其他玩家的精灵之火,并避免尝试在这些怪物上施放精灵之火", 317 | 318 | -- spells 319 | ["Rank"] = "等级", 320 | ["Rank 1"] = "等级 1", 321 | ["Rank 2"] = "等级 2", 322 | ["Rank 3"] = "等级 3", 323 | ["Rank 4"] = "等级 4", 324 | 325 | -- druid 326 | ["entangling roots"] = "纠缠根须", 327 | ["sleep"] = "沉睡", 328 | ["faerie fire"] = "精灵之火", 329 | ["faerie fire (bear)()"] = "精灵之火(熊形态)", 330 | ["faerie fire (feral)()"] = "精灵之火(野性形态)", 331 | ["hibernate"] = "休眠", 332 | ["insect swarm"] = "虫群", 333 | ["moonfire"] = "月火术", 334 | ["rake"] = "扫击", 335 | ["rip"] = "撕扯", 336 | ["soothe animal"] = "安抚动物", 337 | ["bash"] = "重击", 338 | ["Maim"] = "致残", 339 | ["demoralizing roar"] = "挫志咆哮", 340 | ["challenging roar"] = "挑战咆哮", 341 | ["ferocious bite"] = "凶猛撕咬", 342 | ["pounce bleed"] = "突袭流血", 343 | 344 | -- hunter 345 | ["scorpid sting"] = "毒蝎钉刺", 346 | ["serpent sting"] = "毒蛇钉刺", 347 | ["viper sting"] = "蝰蛇钉刺", 348 | ["wing clip"] = "摔绊", 349 | ["concussive shot"] = "震荡射击", 350 | ["wyvern sting"] = "翼龙钉刺", 351 | ["counterattack"] = "反击", 352 | ["hunter's mark"] = "猎人印记", 353 | ["freezing trap"] = "冰冻陷阱", 354 | 355 | -- mage 356 | ["polymorph"] = "变形术", 357 | ["polymorph: cow"] = "变形术:奶牛", 358 | ["polymorph: turtle"] = "变形术:龟", 359 | ["polymorph: pig"] = "变形术:猪", 360 | 361 | -- priest 362 | ["shackle undead"] = "束缚亡灵", 363 | ["mind soothe"] = "安抚心灵", 364 | ["mind control"] = "心灵控制", 365 | ["devouring plague"] = "吞噬瘟疫", 366 | ["hex of weakness"] = "虚弱妖术", 367 | ["shadow word: pain"] = "暗言术:痛", 368 | ["vampiric embrace"] = "吸血鬼的拥抱", 369 | ["holy fire"] = "神圣之火", 370 | 371 | -- Paladin 372 | ["turn undead"] = "超度亡灵", 373 | 374 | -- rogue 375 | ["blind"] = "致盲", 376 | ["sap"] = "闷棍", 377 | ["gouge"] = "凿击", 378 | ["rupture"] = "割裂", -- no idea if right 379 | ["kidney shot"] = "肾击", -- no idea if right 380 | ["expose armor"] = "破甲", -- no idea if right 381 | ["garrote"] = "锁喉", -- no idea if right 382 | ["deadly poison"] = "致命毒药", -- no idea if right 383 | ["deadly poison II"] = "致命毒药 II", 384 | ["deadly poison III"] = "致命毒药 III", 385 | ["deadly poison IV"] = "致命毒药 IV", 386 | ["deadly poison V"] = "致命毒药 V", 387 | ["hemorrhage"] = "出血", -- no idea if right 388 | ["cheap shot"] = "肮脏的打击", -- no idea if right 389 | 390 | -- shaman 391 | ["flame shock"] = "烈焰震击", -- no idea if right 392 | ["molten blast"] = "熔岩爆裂", -- no idea if right 393 | 394 | -- warlock 395 | ["conflagrate"] = "引燃", -- no idea if right 396 | ["corruption"] = "腐蚀术", 397 | ["curse of agony"] = "痛苦诅咒", 398 | ["curse of doom"] = "末日诅咒", 399 | ["curse of recklessness"] = "鲁莽诅咒", 400 | ["curse of shadow"] = "暗影诅咒", 401 | ["curse of the elements"] = "元素诅咒", 402 | ["curse of tongues"] = "语言诅咒", 403 | ["curse of weakness"] = "虚弱诅咒", 404 | ["curse of exhaustion"] = "疲劳诅咒", 405 | ["dark harvest"] = "黑暗收割", 406 | ["immolate"] = "献祭", 407 | ["death coil"] = "死亡缠绕", 408 | ["banish"] = "放逐", 409 | ["siphon life"] = "生命虹吸", 410 | ["fear"] = "恐惧", 411 | 412 | -- warrior 413 | ["rend"] = '撕裂', -- no idea if right 414 | } 415 | end) 416 | -------------------------------------------------------------------------------- /libs/AceHook-2.1/AceHook-2.1.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Name: AceHook-2.1 3 | Revision: $Rev: 17638 $ 4 | Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) 5 | Inspired By: Ace 1.x by Turan (turan@gryphon.com) 6 | Website: http://www.wowace.com/ 7 | Documentation: http://www.wowace.com/index.php/AceHook-2.1 8 | SVN: http://svn.wowace.com/root/trunk/Ace2/AceHook-2.1 9 | Description: Mixin to allow for safe hooking of functions, methods, and scripts. 10 | Dependencies: AceLibrary, AceOO-2.0 11 | ]] 12 | 13 | local MAJOR_VERSION = "AceHook-2.1" 14 | local MINOR_VERSION = "$Revision: 17638 $" 15 | 16 | -- This ensures the code is only executed if the libary doesn't already exist, or is a newer version 17 | if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end 18 | if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end 19 | 20 | if loadstring("return function(...) return ... end") and AceLibrary:HasInstance(MAJOR_VERSION) then return end -- lua51 check 21 | if not AceLibrary:HasInstance("AceOO-2.0") then error(MAJOR_VERSION .. " requires AceOO-2.0") end 22 | 23 | --[[--------------------------------------------------------------------------------- 24 | Create the library object 25 | ----------------------------------------------------------------------------------]] 26 | 27 | local AceOO = AceLibrary:GetInstance("AceOO-2.0") 28 | local AceHook = AceOO.Mixin { 29 | "Hook", 30 | "HookScript", 31 | "SecureHook", 32 | "Unhook", 33 | "UnhookAll", 34 | "HookReport", 35 | "IsHooked", 36 | } 37 | 38 | --[[--------------------------------------------------------------------------------- 39 | Library Definitions 40 | ----------------------------------------------------------------------------------]] 41 | 42 | local protFuncs = { 43 | CameraOrSelectOrMoveStart = true, CameraOrSelectOrMoveStop = true, 44 | TurnOrActionStart = true, TurnOrActionStop = true, 45 | PitchUpStart = true, PitchUpStop = true, 46 | PitchDownStart = true, PitchDownStop = true, 47 | MoveBackwardStart = true, MoveBackwardStop = true, 48 | MoveForwardStart = true, MoveForwardStop = true, 49 | Jump = true, StrafeLeftStart = true, 50 | StrafeLeftStop = true, StrafeRightStart = true, 51 | StrafeRightStop = true, ToggleMouseMove = true, 52 | ToggleRun = true, TurnLeftStart = true, 53 | TurnLeftStop = true, TurnRightStart = true, 54 | TurnRightStop = true, 55 | } 56 | 57 | local function issecurevariable(x) 58 | if protFuncs[x] then 59 | return 1 60 | else 61 | return nil 62 | end 63 | end 64 | 65 | local _G = getfenv(0) 66 | 67 | local function hooksecurefunc(arg1, arg2, arg3) 68 | if type(arg1) == "string" then 69 | arg1, arg2, arg3 = _G, arg1, arg2 70 | end 71 | local orig = arg1[arg2] 72 | arg1[arg2] = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 73 | 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) 74 | 75 | arg3(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 76 | 77 | return x1,x2,x3,x4,x5,x6,x7,x8,x9,x10,x11,x12,x13,x14,x15,x16,x17,x18,x19,x20 78 | end 79 | end 80 | 81 | local protectedScripts = { 82 | OnClick = true, 83 | } 84 | 85 | 86 | local handlers, scripts, actives, registry 87 | 88 | --[[--------------------------------------------------------------------------------- 89 | Private definitions (Not exposed) 90 | ----------------------------------------------------------------------------------]] 91 | 92 | local new, del 93 | do 94 | local list = setmetatable({}, {__mode = "k"}) 95 | function new() 96 | local t = next(list) 97 | if not t then 98 | return {} 99 | end 100 | list[t] = nil 101 | return t 102 | end 103 | 104 | function del(t) 105 | setmetatable(t, nil) 106 | for k in pairs(t) do 107 | t[k] = nil 108 | end 109 | list[t] = true 110 | end 111 | end 112 | 113 | local function createFunctionHook(self, func, handler, orig, secure) 114 | if not secure then 115 | if type(handler) == "string" then 116 | -- The handler is a method, need to self it 117 | local uid 118 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 119 | if actives[uid] then 120 | return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 121 | else 122 | return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 123 | end 124 | end 125 | return uid 126 | else 127 | -- The handler is a function, just call it 128 | local uid 129 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 130 | if actives[uid] then 131 | return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 132 | else 133 | return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 134 | end 135 | end 136 | return uid 137 | end 138 | else 139 | -- secure hooks don't call the original method 140 | if type(handler) == "string" then 141 | -- The handler is a method, need to self it 142 | local uid 143 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 144 | if actives[uid] then 145 | return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 146 | end 147 | end 148 | return uid 149 | else 150 | -- The handler is a function, just call it 151 | local uid 152 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 153 | if actives[uid] then 154 | return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 155 | end 156 | end 157 | return uid 158 | end 159 | end 160 | end 161 | 162 | local function createMethodHook(self, object, method, handler, orig, secure, script) 163 | if script then 164 | if type(handler) == "string" then 165 | local uid 166 | uid = function() 167 | if actives[uid] then 168 | return self[handler](self, object) 169 | else 170 | return orig() 171 | end 172 | end 173 | return uid 174 | else 175 | -- The handler is a function, just call it 176 | local uid 177 | uid = function() 178 | if actives[uid] then 179 | return handler(object) 180 | else 181 | return orig() 182 | end 183 | end 184 | return uid 185 | end 186 | elseif not secure then 187 | if type(handler) == "string" then 188 | local uid 189 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 190 | if actives[uid] then 191 | return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 192 | else 193 | return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 194 | end 195 | end 196 | return uid 197 | else 198 | -- The handler is a function, just call it 199 | local uid 200 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 201 | if actives[uid] then 202 | return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 203 | else 204 | return orig(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 205 | end 206 | end 207 | return uid 208 | end 209 | else 210 | -- secure hooks don't call the original method 211 | if type(handler) == "string" then 212 | local uid 213 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 214 | if actives[uid] then 215 | return self[handler](self, a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 216 | end 217 | end 218 | return uid 219 | else 220 | -- The handler is a function, just call it 221 | local uid 222 | uid = function(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 223 | if actives[uid] then 224 | return handler(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20) 225 | end 226 | end 227 | return uid 228 | end 229 | end 230 | end 231 | 232 | local function hookFunction(self, func, handler, secure) 233 | local orig = _G[func] 234 | 235 | if not orig or type(orig) ~= "function" then 236 | AceHook:error("Attempt to hook a non-existant function %q", func) 237 | end 238 | 239 | if not handler then 240 | handler = func 241 | end 242 | 243 | if registry[self][func] then 244 | local uid = registry[self][func] 245 | 246 | if actives[uid] then 247 | -- We have an active hook from this source. Don't multi-hook 248 | AceHook:error("%q already has an active hook from this source.", func) 249 | end 250 | 251 | if handlers[uid] == handler then 252 | -- The hook is inactive, so reactivate it 253 | actives[uid] = true 254 | return 255 | else 256 | AceHook:error("There is a stale hook for %q can't hook or reactivate.", func) 257 | end 258 | end 259 | 260 | if type(handler) == "string" then 261 | if type(self[handler]) ~= "function" then 262 | AceHook:error("Could not find the the handler %q when hooking function %q", handler, func) 263 | end 264 | elseif type(handler) ~= "function" then 265 | AceHook:error("Could not find the handler you supplied when hooking %q", func) 266 | end 267 | 268 | local uid = createFunctionHook(self, func, handler, orig, secure) 269 | registry[self][func] = uid 270 | actives[uid] = true 271 | handlers[uid] = handler 272 | 273 | if not secure then 274 | _G[func] = uid 275 | self.hooks[func] = orig 276 | else 277 | hooksecurefunc(func, uid) 278 | end 279 | end 280 | 281 | local function unhookFunction(self, func) 282 | if not registry[self][func] then 283 | AceHook:error("Tried to unhook %q which is not currently hooked.", func) 284 | end 285 | 286 | local uid = registry[self][func] 287 | 288 | if actives[uid] then 289 | -- See if we own the global function 290 | if self.hooks[func] and _G[func] == uid then 291 | _G[func] = self.hooks[func] 292 | self.hooks[func] = nil 293 | registry[self][func] = nil 294 | handlers[uid] = nil 295 | scripts[uid] = nil 296 | actives[uid] = nil 297 | -- Magically all-done 298 | else 299 | actives[uid] = nil 300 | end 301 | end 302 | end 303 | 304 | local function hookMethod(self, obj, method, handler, script, secure) 305 | if not handler then 306 | handler = method 307 | end 308 | 309 | if not obj or type(obj) ~= "table" then 310 | AceHook:error("The object you supplied could not be found, or isn't a table.") 311 | end 312 | 313 | local uid = registry[self][obj] and registry[self][obj][method] 314 | if uid then 315 | if actives[uid] then 316 | -- We have an active hook from this source. Don't multi-hook 317 | AceHook:error("%q already has an active hook from this source.", method) 318 | end 319 | 320 | if handlers[uid] == handler then 321 | -- The hook is inactive, reactivate it. 322 | actives[uid] = true 323 | return 324 | else 325 | AceHook:error("There is a stale hook for %q can't hook or reactivate.", method) 326 | end 327 | end 328 | 329 | if type(handler) == "string" then 330 | if type(self[handler]) ~= "function" then 331 | AceHook:error("Could not find the handler %q you supplied when hooking method %q", handler, method) 332 | end 333 | elseif type(handler) ~= "function" then 334 | AceHook:error("Could not find the handler you supplied when hooking method %q", method) 335 | end 336 | 337 | local orig 338 | if script then 339 | if not obj.GetScript then 340 | AceHook:error("The object you supplied does not have a GetScript method.") 341 | end 342 | if not obj:HasScript(method) then 343 | AceHook:error("The object you supplied doesn't allow the %q method.", method) 344 | end 345 | 346 | orig = obj:GetScript(method) 347 | if type(orig) ~= "function" then 348 | -- Sometimes there is not a original function for a script. 349 | orig = function() end 350 | end 351 | else 352 | orig = obj[method] 353 | end 354 | if not orig then 355 | AceHook:error("Could not find the method or script %q you are trying to hook.", method) 356 | end 357 | 358 | if not registry[self][obj] then 359 | registry[self][obj] = new() 360 | end 361 | 362 | if not self.hooks[obj] then 363 | self.hooks[obj] = new() 364 | end 365 | 366 | local uid = createMethodHook(self, obj, method, handler, orig, secure, script) 367 | registry[self][obj][method] = uid 368 | actives[uid] = true 369 | handlers[uid] = handler 370 | scripts[uid] = script and true or nil 371 | 372 | if script then 373 | obj:SetScript(method, uid) 374 | self.hooks[obj][method] = orig 375 | elseif not secure then 376 | obj[method] = uid 377 | self.hooks[obj][method] = orig 378 | else 379 | hooksecurefunc(obj, method, uid) 380 | end 381 | end 382 | 383 | local function unhookMethod(self, obj, method) 384 | if not registry[self][obj] or not registry[self][obj][method] then 385 | AceHook:error("Attempt to unhook a method %q that is not currently hooked.", method) 386 | return 387 | end 388 | 389 | local uid = registry[self][obj][method] 390 | 391 | if actives[uid] then 392 | if scripts[uid] then -- If this is a script 393 | if obj:GetScript(method) == uid then 394 | -- We own the script. Revert to normal. 395 | obj:SetScript(method, self.hooks[obj][method]) 396 | self.hooks[obj][method] = nil 397 | registry[self][obj][method] = nil 398 | handlers[uid] = nil 399 | scripts[uid] = nil 400 | actives[uid] = nil 401 | else 402 | actives[uid] = nil 403 | end 404 | else 405 | if self.hooks[obj] and self.hooks[obj][method] and obj[method] == uid then 406 | -- We own the method. Revert to normal. 407 | obj[method] = self.hooks[obj][method] 408 | self.hooks[obj][method] = nil 409 | registry[self][obj][method] = nil 410 | handlers[uid] = nil 411 | actives[uid] = nil 412 | else 413 | actives[uid] = nil 414 | end 415 | end 416 | end 417 | if self.hooks[obj] and not next(self.hooks[obj]) then 418 | self.hooks[obj] = del(self.hooks[obj]) 419 | end 420 | if not next(registry[self][obj]) then 421 | registry[self][obj] = del(registry[self][obj]) 422 | end 423 | end 424 | 425 | -- ("function" [, handler] [, hookSecure]) or (object, "method" [, handler] [, hookSecure]) 426 | function AceHook:Hook(object, method, handler, hookSecure) 427 | if type(object) == "string" then 428 | method, handler, hookSecure = object, method, handler 429 | if handler == true then 430 | handler, hookSecure = nil, true 431 | end 432 | if not hookSecure and issecurevariable(method) then 433 | AceHook:error("Attempt to hook secure function %q. Use `SecureHook' or add `true' to the argument list to override.", method) 434 | end 435 | AceHook:argCheck(handler, 3, "function", "string", "nil") 436 | AceHook:argCheck(hookSecure, 4, "boolean", "nil") 437 | hookFunction(self, method, handler, false) 438 | else 439 | if handler == true then 440 | handler, hookSecure = nil, true 441 | end 442 | if not hookSecure and issecurevariable(object, method) then 443 | AceHook:error("Attempt to hook secure method %q. Use `SecureHook' or add `true' to the argument list to override.", method) 444 | end 445 | AceHook:argCheck(object, 2, "table") 446 | AceHook:argCheck(method, 3, "string") 447 | AceHook:argCheck(handler, 4, "function", "string", "nil") 448 | AceHook:argCheck(hookSecure, 5, "boolean", "nil") 449 | hookMethod(self, object, method, handler, false, false) 450 | end 451 | end 452 | 453 | -- ("function", handler) or (object, "method", handler) 454 | function AceHook:SecureHook(object, method, handler) 455 | if type(object) == "string" then 456 | method, handler = object, method 457 | AceHook:argCheck(handler, 3, "function", "string", "nil") 458 | hookFunction(self, method, handler, true) 459 | else 460 | AceHook:argCheck(object, 2, "table") 461 | AceHook:argCheck(method, 3, "string") 462 | AceHook:argCheck(handler, 4, "function", "string", "nil") 463 | hookMethod(self, object, method, handler, false, true) 464 | end 465 | end 466 | 467 | function AceHook:HookScript(frame, script, handler) 468 | AceHook:argCheck(frame, 2, "table") 469 | if not frame[0] or type(frame.IsFrameType) ~= "function" then 470 | AceHook:error("Bad argument #2 to `HookScript'. Expected frame.") 471 | end 472 | AceHook:argCheck(script, 3, "string") 473 | AceHook:argCheck(handler, 4, "function", "string", "nil") 474 | hookMethod(self, frame, script, handler, true, false) 475 | end 476 | 477 | -- ("function") or (object, "method") 478 | function AceHook:IsHooked(obj, method) 479 | if type(obj) == "string" then 480 | if registry[self][obj] and actives[registry[self][obj]] then 481 | return true, handlers[registry[self][obj]] 482 | end 483 | else 484 | AceHook:argCheck(obj, 2, "string", "table") 485 | AceHook:argCheck(method, 3, "string") 486 | if registry[self][obj] and registry[self][obj][method] and actives[registry[self][obj][method]] then 487 | return true, handlers[registry[self][obj][method]] 488 | end 489 | end 490 | 491 | return false, nil 492 | end 493 | 494 | -- ("function") or (object, "method") 495 | function AceHook:Unhook(obj, method) 496 | if type(obj) == "string" then 497 | unhookFunction(self, obj) 498 | else 499 | AceHook:argCheck(obj, 2, "string", "table") 500 | AceHook:argCheck(method, 3, "string") 501 | unhookMethod(self, obj, method) 502 | end 503 | end 504 | 505 | function AceHook:UnhookAll() 506 | for key, value in pairs(registry[self]) do 507 | if type(key) == "table" then 508 | for method in pairs(value) do 509 | self:Unhook(key, method) 510 | end 511 | else 512 | self:Unhook(key) 513 | end 514 | end 515 | end 516 | 517 | function AceHook:HookReport() 518 | DEFAULT_CHAT_FRAME:AddMessage("This is a list of all active hooks for this object:") 519 | if not next(registry[self]) then 520 | DEFAULT_CHAT_FRAME:AddMessage("No hooks") 521 | end 522 | 523 | for key, value in pairs(registry[self]) do 524 | if type(value) == "table" then 525 | for method, uid in pairs(value) do 526 | 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 "")) 527 | end 528 | else 529 | 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 "")) 530 | end 531 | end 532 | end 533 | 534 | function AceHook:OnInstanceInit(object) 535 | if not object.hooks then 536 | object.hooks = new() 537 | end 538 | if not registry[object] then 539 | registry[object] = new() 540 | end 541 | end 542 | 543 | AceHook.OnManualEmbed = AceHook.OnInstanceInit 544 | 545 | function AceHook:OnEmbedDisable(target) 546 | self.UnhookAll(target) 547 | end 548 | 549 | local function activate(self, oldLib, oldDeactivate) 550 | AceHook = self 551 | 552 | self.handlers = oldLib and oldLib.handlers or {} 553 | self.registry = oldLib and oldLib.registry or {} 554 | self.scripts = oldLib and oldLib.scripts or {} 555 | self.actives = oldLib and oldLib.actives or {} 556 | 557 | handlers = self.handlers 558 | registry = self.registry 559 | scripts = self.scripts 560 | actives = self.actives 561 | 562 | AceHook.super.activate(self, oldLib, oldDeactivate) 563 | 564 | if oldDeactivate then 565 | oldDeactivate(oldLib) 566 | end 567 | end 568 | 569 | AceLibrary:Register(AceHook, MAJOR_VERSION, MINOR_VERSION, activate) 570 | -------------------------------------------------------------------------------- /commands.lua: -------------------------------------------------------------------------------- 1 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 2 | local curseCommands = L["|cffffcc00Cursive:|cffffaaaa Commands:"] 3 | local priorityChoices = L["|cffffcc00Priority choices:"] 4 | local curseOptions = L["|cffffcc00Options (separate with ,):"] 5 | 6 | local commandOptions = { 7 | warnings = L["Display text warnings when a curse fails to cast."], 8 | resistsound = L["Play a sound when a curse is resisted."], 9 | expiringsound = L["Play a sound when a curse is about to expire."], 10 | allowooc = L["Allow out of combat targets to be multicursed. Would only consider using this solo to avoid potentially griefing raids/dungeons by pulling unintended mobs."], 11 | minhp = L["Minimum HP for a target to be considered. Example usage minhp=10000. "], 12 | refreshtime = L["Time threshold at which to allow refreshing a curse. Default is 0 seconds."], 13 | priotarget = L["Always prioritize current target when choosing target for multicurse. Does not affect 'curse' command."], 14 | ignoretarget = L["Ignore the current target when choosing target for multicurse. Does not affect 'curse' command."], 15 | playeronly = L["Only choose players and ignore npcs when choosing target for multicurse. Does not affect 'curse' command."], 16 | name = L["Filter targets by name. Can be a partial match. If no match is found, the command will do nothing."], 17 | ignorespellid = L["Ignore targets with the specified spell id already on them. Useful for ignoring targets that already have a shared debuff."], 18 | ignorespelltexture = L["Ignore targets with the specified spell texture already on them. Useful for ignoring targets that already have a shared debuff."], 19 | } 20 | 21 | local commands = { 22 | ["curse"] = L["/cursive curse ||>: Casts spell if not already on target/guid"], 23 | ["multicurse"] = L["/cursive multicurse ||>: Picks target based on priority and casts spell if not already on target"], 24 | ["target"] = L["/cursive target ||>: Targets unit based on priority if spell in range and not already on target"], 25 | } 26 | 27 | local PRIORITY_HIGHEST_HP = "HIGHEST_HP" 28 | local PRIORITY_LOWEST_HP = "LOWEST_HP" 29 | local PRIORITY_RAID_MARK = "RAID_MARK" 30 | local PRIORITY_RAID_MARK_SQUARE = "RAID_MARK_SQUARE" 31 | local PRIORITY_INVERSE_RAID_MARK = "INVERSE_RAID_MARK" 32 | local PRIORITY_HIGHEST_HP_RAID_MARK = "HIGHEST_HP_RAID_MARK" 33 | local PRIORITY_HIGHEST_HP_RAID_MARK_SQUARE = "HIGHEST_HP_RAID_MARK_SQUARE" 34 | local PRIORITY_HIGHEST_HP_INVERSE_RAID_MARK = "HIGHEST_HP_INVERSE_RAID_MARK" 35 | 36 | local priorities = { 37 | [PRIORITY_HIGHEST_HP] = L["Target with the highest HP."], 38 | [PRIORITY_LOWEST_HP] = L["Target with the lowest HP."], 39 | [PRIORITY_RAID_MARK] = L["Target with the highest raid mark."], 40 | [PRIORITY_RAID_MARK_SQUARE] = L["Target with the highest raid mark with Cross set to -1 and Skull set to -2 (Square highest prio at 6)."], 41 | [PRIORITY_INVERSE_RAID_MARK] = L["Target with the lowest raid mark."], 42 | [PRIORITY_HIGHEST_HP_RAID_MARK] = L["Target with the highest HP and raid mark."], 43 | [PRIORITY_HIGHEST_HP_RAID_MARK_SQUARE] = L["Same as HIGHEST_HP_RAID_MARK but with RAID_MARK_SQUARE mark prio."], 44 | [PRIORITY_HIGHEST_HP_INVERSE_RAID_MARK] = L["Same as HIGHEST_HP_RAID_MARK but with INVERSE_RAID_MARK mark prio."] 45 | } 46 | 47 | local curseNoTarget = L["|cffffcc00Cursive:|cffffaaaa Couldn't find a target to curse."] 48 | 49 | local function parseOptions(optionsStr) 50 | local options = { } 51 | 52 | if optionsStr then 53 | for option, _ in pairs(commandOptions) do 54 | -- special case for minhp as it takes a param 55 | if option == "minhp" then 56 | local _, _, minHp = string.find(optionsStr, "minhp=(%d+)") 57 | if minHp then 58 | options["minhp"] = tonumber(minHp) 59 | end 60 | elseif option == "refreshtime" then 61 | local _, _, refreshTime = string.find(optionsStr, "refreshtime=(%d+)") 62 | if refreshTime then 63 | options["refreshtime"] = tonumber(refreshTime) 64 | end 65 | elseif option == "name" then 66 | local _, _, name = string.find(optionsStr, "name=([%w%s]+)") 67 | if name then 68 | options["name"] = name 69 | end 70 | elseif option == "ignorespellid" then 71 | local _, _, spellId = string.find(optionsStr, "ignorespellid=(%d+)") 72 | if spellId then 73 | options["ignorespellid"] = tonumber(spellId) 74 | end 75 | elseif option == "ignorespelltexture" then 76 | local _, _, texture = string.find(optionsStr, "ignorespelltexture=([%w_]+)") 77 | if texture then 78 | options["ignorespelltexture"] = texture 79 | end 80 | elseif string.find(optionsStr, option) then 81 | options[option] = true 82 | end 83 | end 84 | end 85 | 86 | return options 87 | end 88 | 89 | local function handleSlashCommands(msg, editbox) 90 | if not msg or msg == "" then 91 | DEFAULT_CHAT_FRAME:AddMessage(curseCommands) 92 | for _, description in pairs(commands) do 93 | DEFAULT_CHAT_FRAME:AddMessage(description) 94 | end 95 | DEFAULT_CHAT_FRAME:AddMessage(priorityChoices) 96 | for priority, description in pairs(priorities) do 97 | DEFAULT_CHAT_FRAME:AddMessage("|CFFFFFF00" .. priority .. "|R: " .. description) 98 | end 99 | 100 | DEFAULT_CHAT_FRAME:AddMessage(curseOptions) 101 | for option, description in pairs(commandOptions) do 102 | DEFAULT_CHAT_FRAME:AddMessage("|CFFFFFF00" .. option .. "|R: " .. description) 103 | end 104 | return 105 | end 106 | -- get first word in string 107 | local _, _, command, args = string.find(msg, "(%w+) (.*)") 108 | if command == "curse" then 109 | local spellName, targetedGuid, optionsStr = Cursive.utils.strsplit("|", args) 110 | local options = parseOptions(optionsStr) 111 | Cursive:Curse(spellName, targetedGuid, options) 112 | elseif command == "multicurse" then 113 | local spellName, priority, optionsStr = Cursive.utils.strsplit("|", args) 114 | local options = parseOptions(optionsStr) 115 | Cursive:Multicurse(spellName, priority, options) 116 | elseif command == "target" then 117 | local spellName, priority, optionsStr = Cursive.utils.strsplit("|", args) 118 | local options = parseOptions(optionsStr) 119 | Cursive:Target(spellName, priority, options) 120 | else 121 | DEFAULT_CHAT_FRAME:AddMessage(L["|cffffcc00Cursive:|cffffaaaa Unknown command."]) 122 | DEFAULT_CHAT_FRAME:AddMessage(curseCommands) 123 | for _, description in pairs(commands) do 124 | DEFAULT_CHAT_FRAME:AddMessage(description) 125 | end 126 | end 127 | end 128 | 129 | local crowdControlledSpellIds = { 130 | [700] = { name = L["sleep"], rank = 1, duration = 20 }, 131 | [1090] = { name = L["sleep"], rank = 2, duration = 30 }, 132 | [2937] = { name = L["sleep"], rank = 3, duration = 40 }, 133 | 134 | [339] = { name = L["entangling roots"], rank = 1, duration = 12 }, 135 | [1062] = { name = L["entangling roots"], rank = 2, duration = 15 }, 136 | [5195] = { name = L["entangling roots"], rank = 3, duration = 18 }, 137 | [5196] = { name = L["entangling roots"], rank = 4, duration = 21 }, 138 | [9852] = { name = L["entangling roots"], rank = 5, duration = 24 }, 139 | [9853] = { name = L["entangling roots"], rank = 6, duration = 27 }, 140 | 141 | [2637] = { name = L["hibernate"], rank = 1, duration = 20 }, 142 | [18657] = { name = L["hibernate"], rank = 2, duration = 30 }, 143 | [18658] = { name = L["hibernate"], rank = 3, duration = 40 }, 144 | 145 | [1425] = { name = L["shackle undead"], rank = 1, duration = 30 }, 146 | [9486] = { name = L["shackle undead"], rank = 2, duration = 40 }, 147 | [10956] = { name = L["shackle undead"], rank = 3, duration = 50 }, 148 | 149 | -- polymorph 150 | [118] = { name = L["polymorph"], rank = L["Rank 1"], duration = 20 }, 151 | [12824] = { name = L["polymorph"], rank = L["Rank 2"], duration = 30 }, 152 | [12825] = { name = L["polymorph"], rank = L["Rank 3"], duration = 40 }, 153 | [12826] = { name = L["polymorph"], rank = L["Rank 4"], duration = 50 }, 154 | 155 | [28270] = { name = L["polymorph: cow"], rank = L["Rank 1"], duration = 50 }, 156 | [28271] = { name = L["polymorph: turtle"], rank = L["Rank 1"], duration = 50 }, 157 | [28272] = { name = L["polymorph: pig"], rank = L["Rank 1"], duration = 50 }, 158 | 159 | [2878] = { name = L["turn undead"], rank = 1, duration = 10 }, 160 | [5627] = { name = L["turn undead"], rank = 2, duration = 15 }, 161 | [10326] = { name = L["turn undead"], rank = 3, duration = 20 }, 162 | 163 | [2094] = { name = L["blind"], rank = 1, duration = 10 }, 164 | [21060] = { name = L["blind"], rank = 1, duration = 10 }, 165 | 166 | [6770] = { name = L["sap"], rank = 1, duration = 25 }, 167 | [2070] = { name = L["sap"], rank = 2, duration = 35 }, 168 | [11297] = { name = L["sap"], rank = 3, duration = 45 }, 169 | 170 | [1776] = { name = L["gouge"], rank = 1, duration = 4 }, 171 | [1777] = { name = L["gouge"], rank = 2, duration = 4 }, 172 | [8629] = { name = L["gouge"], rank = 3, duration = 4 }, 173 | [11285] = { name = L["gouge"], rank = 4, duration = 4 }, 174 | [11286] = { name = L["gouge"], rank = 5, duration = 4 }, 175 | 176 | [3355] = { name = L["freezing trap"], rank = 1, duration = 10 }, 177 | [14308] = { name = L["freezing trap"], rank = 2, duration = 15 }, 178 | [14309] = { name = L["freezing trap"], rank = 3, duration = 20 }, 179 | 180 | [710] = { name = L["banish"], rank = 1, duration = 30 }, 181 | [18647] = { name = L["banish"], rank = 2, duration = 30 }, 182 | 183 | -- mind control effects 184 | [28410] = { name = "Chains of Kel'Thuzad" }, -- we aren't casting these, name doesn't matter 185 | [7621] = { name = "Arugal's Curse" }, 186 | [24261] = { name = "Brain Wash" }, 187 | [12888] = { name = "Cause Insanity" }, 188 | [24327] = { name = "Cause Insanity" }, 189 | [26079] = { name = "Cause Insanity" }, 190 | [24327] = { name = "Cause Insanity" }, 191 | [23174] = { name = "Chromatic Mutation" }, 192 | [25806] = { name = "Creature of Nightmare" }, 193 | [23298] = { name = "Demonic Doom" }, 194 | [7645] = { name = "Dominate Mind" }, 195 | [14515] = { name = "Dominate Mind" }, 196 | [15859] = { name = "Dominate Mind" }, 197 | [20604] = { name = "Dominate Mind" }, 198 | [20740] = { name = "Dominate Mind" }, 199 | [17405] = { name = "Domination" }, 200 | [3442] = { name = "Enslave" }, 201 | [13181] = { name = "Gnomish Mind Control Cap" }, 202 | [26740] = { name = "Gnomish Mind Control Cap" }, 203 | [12483] = { name = "Hex of Jammal'an" }, 204 | [25772] = { name = "Mental Domination" }, 205 | [7967] = { name = "Naralex's Nightmare" }, 206 | [19469] = { name = "Poison Mind" }, 207 | [17244] = { name = "Possess" }, 208 | [22667] = { name = "Shadow Command" }, 209 | [20668] = { name = "Sleepwalk" }, 210 | [785] = { name = "True Fulfillment" }, 211 | [26195] = { name = "Whisperings of C'Thun" }, 212 | [26197] = { name = "Whisperings of C'Thun" }, 213 | [26198] = { name = "Whisperings of C'Thun" }, 214 | [26258] = { name = "Whisperings of C'Thun" }, 215 | [26259] = { name = "Whisperings of C'Thun" }, 216 | [24178] = { name = "Will of Hakkar" }, 217 | 218 | -- immunity effects 219 | [642] = { name = "Divine Shield" }, 220 | [1020] = { name = "Divine Shield" }, 221 | [13874] = { name = "Divine Shield" }, 222 | [5573] = { name = "Divine Protection" }, 223 | [13007] = { name = "Divine Protection" }, 224 | [6356] = { name = "Spell Immunity" }, 225 | [6724] = { name = "Light of Elune" }, 226 | [7121] = { name = "Anti-Magic Shield" }, 227 | [19645] = { name = "Anti-Magic Shield" }, 228 | [24021] = { name = "Anti-Magic Shield" }, 229 | [8361] = { name = "Purity" }, 230 | [8611] = { name = "Phase Shift" }, 231 | [45713] = { name = "Phase Shift" }, 232 | [9438] = { name = "Arcane Bubble" }, 233 | [11958] = { name = "Ice Block" }, 234 | [12843] = { name = "Mordresh's Shield" }, 235 | [21892] = { name = "Arcane Protection" }, 236 | [51096] = { name = "Worgen Dimension" }, 237 | [51228] = { name = "Invulnerability" }, 238 | [52010] = { name = "Pending Detonation" }, 239 | [53225] = { name = "Ward of Vorgendor" }, 240 | [57644] = { name = "Veil of Vorgendor" }, 241 | } 242 | 243 | local function isMobCrowdControlled(guid) 244 | -- check if mob is CC'ed 245 | 246 | -- check debuffs 247 | for i = 1, 16 do 248 | local _, _, _, spellId = UnitDebuff(guid, i) 249 | if spellId then 250 | if crowdControlledSpellIds[spellId] then 251 | return true 252 | end 253 | else 254 | break 255 | end 256 | end 257 | 258 | -- check buffs 259 | for i = 1, 32 do 260 | local _, _, spellId = UnitBuff(guid, i) 261 | if spellId then 262 | if crowdControlledSpellIds[spellId] then 263 | return true 264 | end 265 | else 266 | break 267 | end 268 | end 269 | 270 | return false 271 | end 272 | 273 | local function GetSquarePrioRaidTargetIndex(guid) 274 | local index = GetRaidTargetIndex(guid) 275 | if index == 7 then 276 | return 0 -- cross becomes 0 277 | elseif index == 8 then 278 | return -1 -- skull becomes -1 279 | elseif index == 0 then 280 | return -2 -- nomark becomes -2 281 | end 282 | return index or -2 283 | end 284 | 285 | local function hasSpellId(guid, ignoreSpellId) 286 | for i = 1, 16 do 287 | local texture, stacks, spellSchool, spellId = UnitDebuff(guid, i); 288 | if not spellId then 289 | break 290 | end 291 | if spellId == ignoreSpellId then 292 | return true 293 | end 294 | end 295 | 296 | for i = 1, 32 do 297 | local texture, stacks, spellId = UnitBuff(guid, i); 298 | if not spellId then 299 | break 300 | end 301 | if spellId == ignoreSpellId then 302 | return true 303 | end 304 | end 305 | 306 | return false 307 | end 308 | 309 | local function hasSpellTexture(guid, ignoreTexture) 310 | for i = 1, 16 do 311 | local texture = UnitDebuff(guid, i); 312 | if not texture then 313 | break 314 | end 315 | if string.find(texture, ignoreTexture) then 316 | return true 317 | end 318 | end 319 | 320 | for i = 1, 32 do 321 | local texture = UnitBuff(guid, i); 322 | if not texture then 323 | break 324 | end 325 | if string.find(texture, ignoreTexture) then 326 | return true 327 | end 328 | end 329 | 330 | return false 331 | end 332 | 333 | local function passedOptionFilters(guid, options) 334 | if options["name"] then 335 | local name = UnitName(guid) 336 | if not string.find(name, options["name"]) then 337 | return false 338 | end 339 | end 340 | if options["ignorespellid"] then 341 | if hasSpellId(guid, options["ignorespellid"]) then 342 | return false 343 | end 344 | end 345 | if options["ignorespelltexture"] then 346 | if hasSpellTexture(guid, options["ignorespelltexture"]) then 347 | return false 348 | end 349 | end 350 | if options["playeronly"] and not UnitIsPlayer(guid) then 351 | return false 352 | end 353 | return true 354 | end 355 | 356 | local function pickTarget(selectedPriority, lowercaseSpellNameNoRank, checkRange, options) 357 | -- Curse the target that best matches the selected priority 358 | local highestPrimaryValue = -10 359 | local highestSecondaryValue = -10 360 | local targetedGuid = nil 361 | 362 | if selectedPriority == PRIORITY_LOWEST_HP then 363 | highestPrimaryValue = 999999999999 -- should be bigger than any mob hp 364 | end 365 | 366 | local minHp = options["minhp"] 367 | local ignoreInFight = options["allowooc"] 368 | local refreshTime = options["refreshtime"] 369 | 370 | local _, currentTargetGuid = UnitExists("target") 371 | 372 | local seenRaidMark = nil -- if we have seen a raid mark 373 | 374 | for guid, time in pairs(Cursive.core.guids) do 375 | -- apply filters 376 | local shouldDisplay = Cursive:ShouldDisplayGuid(guid) 377 | -- check if target displayed 378 | if shouldDisplay then 379 | if not options["ignoretarget"] or guid ~= currentTargetGuid then 380 | -- check if in combat already or player is actively targeting the mob 381 | if ignoreInFight or Cursive.filter.infight(guid) or guid == currentTargetGuid then 382 | if passedOptionFilters(guid, options) then 383 | local passedRangeCheck = false 384 | if IsSpellInRange then 385 | local result 386 | if Cursive.curses.isDruid and string.find(lowercaseSpellNameNoRank, "faerie fire %(feral%)") then 387 | -- IsSpellInRange doesn't work with Faerie Fire (Feral), use spellid instead 388 | result = IsSpellInRange(17392, guid) 389 | else 390 | result = IsSpellInRange(lowercaseSpellNameNoRank, guid) 391 | end 392 | if result == -1 then 393 | passedRangeCheck = checkRange == false or CheckInteractDistance(guid, 4) -- fallback to old range check 394 | else 395 | -- 0 or 1 396 | passedRangeCheck = result == 1 397 | end 398 | else 399 | -- prioritize targets within 28 yards first to improve chances of being in range 400 | passedRangeCheck = checkRange == false or CheckInteractDistance(guid, 4) 401 | end 402 | if passedRangeCheck then 403 | -- check if the target has the curse 404 | if not Cursive.curses:HasCurse(lowercaseSpellNameNoRank, guid, refreshTime) and not isMobCrowdControlled(guid) then 405 | local mobHp = UnitHealth(guid) 406 | if not minHp or mobHp >= minHp then 407 | local primaryValue = -1 408 | local secondaryValue = -1 409 | if options["priotarget"] and guid == currentTargetGuid then 410 | seenRaidMark = true 411 | primaryValue = 999999999999 -- should be bigger than any mob hp 412 | elseif selectedPriority == PRIORITY_HIGHEST_HP then 413 | primaryValue = UnitHealth(guid) or 0 414 | elseif selectedPriority == PRIORITY_LOWEST_HP then 415 | primaryValue = UnitHealth(guid) or 999999999999 416 | elseif selectedPriority == PRIORITY_RAID_MARK then 417 | primaryValue = GetRaidTargetIndex(guid) or 0 418 | elseif selectedPriority == PRIORITY_RAID_MARK_SQUARE then 419 | primaryValue = GetSquarePrioRaidTargetIndex(guid) 420 | elseif selectedPriority == PRIORITY_INVERSE_RAID_MARK then 421 | primaryValue = -1 * (GetRaidTargetIndex(guid) or 9) 422 | elseif selectedPriority == PRIORITY_HIGHEST_HP_RAID_MARK then 423 | secondaryValue = GetRaidTargetIndex(guid) or 0 424 | if secondaryValue > 0 and not seenRaidMark then 425 | highestPrimaryValue = -10 -- reset highestPriorityValue if this is the first raid mark we've seen 426 | seenRaidMark = true 427 | end 428 | primaryValue = UnitHealth(guid) or 0 429 | elseif selectedPriority == PRIORITY_HIGHEST_HP_RAID_MARK_SQUARE then 430 | secondaryValue = GetSquarePrioRaidTargetIndex(guid) 431 | if secondaryValue > -2 and not seenRaidMark then 432 | highestPrimaryValue = -10 -- reset highestPriorityValue if this is the first raid mark we've seen 433 | seenRaidMark = true 434 | end 435 | primaryValue = UnitHealth(guid) or 0 436 | elseif selectedPriority == PRIORITY_HIGHEST_HP_INVERSE_RAID_MARK then 437 | secondaryValue = -1 * (GetRaidTargetIndex(guid) or 9) 438 | if secondaryValue > -9 and not seenRaidMark then 439 | highestPrimaryValue = -10 -- reset highestPriorityValue if this is the first raid mark we've seen 440 | seenRaidMark = true 441 | end 442 | primaryValue = UnitHealth(guid) or 0 443 | end 444 | 445 | if selectedPriority == PRIORITY_LOWEST_HP then 446 | if primaryValue < highestPrimaryValue then 447 | highestPrimaryValue = primaryValue 448 | targetedGuid = guid 449 | end 450 | elseif primaryValue > highestPrimaryValue then 451 | highestPrimaryValue = primaryValue 452 | highestSecondaryValue = secondaryValue 453 | targetedGuid = guid 454 | elseif primaryValue == highestPrimaryValue and secondaryValue > highestSecondaryValue then 455 | highestSecondaryValue = secondaryValue 456 | targetedGuid = guid 457 | end 458 | end 459 | end 460 | end 461 | end 462 | end 463 | end 464 | end 465 | end 466 | 467 | -- run again if no target found ignoring range (only if IsSpellInRange is not available) 468 | if not targetedGuid and checkRange == true and not IsSpellInRange then 469 | targetedGuid = pickTarget(selectedPriority, lowercaseSpellNameNoRank, false, options) 470 | end 471 | 472 | return targetedGuid 473 | end 474 | 475 | local function castSpellWithOptions(spellName, lowercaseSpellNameNoRank, targetedGuid, options) 476 | if options["resistsound"] then 477 | Cursive.curses:EnableResistSound(targetedGuid) 478 | end 479 | if options["expiringsound"] then 480 | Cursive.curses:RequestExpiringSound(lowercaseSpellNameNoRank, targetedGuid) 481 | end 482 | CastSpellByName(spellName, targetedGuid) 483 | end 484 | 485 | function Cursive:Curse(spellName, targetedGuid, options) 486 | if not spellName or not targetedGuid then 487 | DEFAULT_CHAT_FRAME:AddMessage(commands["curse"]) 488 | return false 489 | end 490 | 491 | if targetedGuid and string.sub(targetedGuid, 1, 2) ~= "0x" then 492 | _, targetedGuid = UnitExists(targetedGuid) 493 | 494 | if not targetedGuid then 495 | if options["warnings"] then 496 | DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) 497 | end 498 | return false 499 | end 500 | end 501 | 502 | if targetedGuid then 503 | -- check for options 504 | if not passedOptionFilters(targetedGuid, options) then 505 | if options["warnings"] then 506 | DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) 507 | end 508 | return false 509 | end 510 | end 511 | 512 | -- remove (Rank x) from spellName if it exists 513 | local lowercaseSpellNameNoRank = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) 514 | 515 | if targetedGuid and not Cursive.curses:HasCurse(lowercaseSpellNameNoRank, targetedGuid, options["refreshtime"]) and not isMobCrowdControlled(targetedGuid) then 516 | castSpellWithOptions(string.lower(spellName), lowercaseSpellNameNoRank, targetedGuid, options) 517 | return true 518 | elseif options["warnings"] then 519 | DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) 520 | end 521 | 522 | return false 523 | end 524 | 525 | local function getSpellTarget(spellName, priority, options) 526 | if not spellName then 527 | DEFAULT_CHAT_FRAME:AddMessage(commands["multicurse"]) 528 | return 529 | end 530 | 531 | if priority and not priorities[priority] then 532 | DEFAULT_CHAT_FRAME:AddMessage(priorityChoices) 533 | for choice, description in pairs(priorities) do 534 | DEFAULT_CHAT_FRAME:AddMessage("|CFFFFFF00" .. choice .. "|R: " .. description) 535 | end 536 | return 537 | end 538 | 539 | local selectedPriority = priority or PRIORITY_HIGHEST_HP 540 | 541 | -- remove (Rank x) from spellName if it exists 542 | local lowercaseSpellNameNoRank = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) 543 | 544 | return pickTarget(selectedPriority, lowercaseSpellNameNoRank, true, options) 545 | end 546 | 547 | function Cursive:Multicurse(spellName, priority, options) 548 | local targetedGuid = getSpellTarget(spellName, priority, options) 549 | if targetedGuid then 550 | local lowercaseSpellNameNoRank = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) 551 | castSpellWithOptions(string.lower(spellName), lowercaseSpellNameNoRank, targetedGuid, options) 552 | return true 553 | elseif options["warnings"] then 554 | DEFAULT_CHAT_FRAME:AddMessage(curseNoTarget) 555 | end 556 | return false 557 | end 558 | 559 | function Cursive:GetTarget(spellName, priority, options) 560 | return getSpellTarget(spellName, priority, options) 561 | end 562 | 563 | function Cursive:Target(spellName, priority, options) 564 | local targetedGuid = getSpellTarget(spellName, priority, options) 565 | if targetedGuid then 566 | TargetUnit(targetedGuid) 567 | return true 568 | end 569 | return false 570 | end 571 | 572 | SLASH_CURSIVE1 = "/cursive" --creating the slash command 573 | SlashCmdList["CURSIVE"] = handleSlashCommands --associating the function with the slash command 574 | -------------------------------------------------------------------------------- /curses.lua: -------------------------------------------------------------------------------- 1 | if not Cursive.superwow then 2 | return 3 | end 4 | local L = AceLibrary("AceLocale-2.2"):new("Cursive") 5 | 6 | local _, playerClassName = UnitClass("player") 7 | 8 | local curses = { 9 | trackedCurseIds = {}, 10 | trackedCurseNamesToTextures = {}, 11 | trackedCurseNameRanksToSpellSlots = {}, 12 | conflagrateSpellIds = { 13 | [17962] = true, 14 | [18930] = true, 15 | [18931] = true, 16 | [18932] = true, 17 | }, 18 | darkHarvestSpellIds = { 19 | [52550] = true, 20 | [52551] = true, 21 | [52552] = true, 22 | }, 23 | darkHarvestData = {}, 24 | guids = {}, 25 | isChanneling = false, 26 | pendingCast = {}, 27 | resistSoundGuids = {}, 28 | expiringSoundGuids = {}, 29 | requestedExpiringSoundGuids = {}, -- guid added on spellcast, moved to expiringSoundGuids once rendered by ui 30 | previousComboPoints = 0, 31 | comboPoints = 0, 32 | lastFerociousBiteTime = 0, 33 | lastFerociousBiteTargetGuid = 0, 34 | lastMoltenBlastTargetGuid = 0, 35 | 36 | sharedDebuffs = { 37 | faeriefire = {}, 38 | }, 39 | sharedDebuffGuids = { 40 | faeriefire = {}, -- used for scanning for shared debuffs like faerie fire 41 | }, -- used for scanning for shared debuffs like faerie fire 42 | 43 | -- Whitelist of mobs that can bleed (for rake tracking at client debuff cap) 44 | mobsThatBleed = { 45 | ["0xF13000F1F3276A33"] = true, -- Keeper Gnarlmoon 46 | ["0xF13000F1FA276A32"] = true, -- Ley-Watcher Incantagos 47 | ["0xF13000EA3F276C05"] = true, -- King 48 | ["0xF13000EA31279058"] = true, -- Queen 49 | ["0xF13000EA43276C06"] = true, -- Bishop 50 | ["0xF13000EA44279044"] = true, -- Pawn 51 | ["0xF13000EA42276C07"] = true, -- Rook 52 | ["0xF13000EA4D05DA44"] = true, -- Sanv Tas'dal 53 | ["0xF13000EA57276C04"] = true, -- Kruul 54 | ["0xF130016C95276DAF"] = true, -- Mephistroth 55 | 56 | ["0xF130003E5401591A"] = true, --Anub'Rekhan 57 | ["0xF130003E510159B0"] = true, --Grand Widow Faerlina 58 | ["0xF130003E500159A3"] = true, --Maexxna 59 | ["0xF130003EBD01598C"] = true, --Razuvious 60 | ["0xF130003EBC01599F"] = true, --Gothik 61 | ["0xF130003EBF015AB2"] = true, --Zeliek 62 | ["0xF130003EBE015AB3"] = true, --Mograine 63 | ["0xF130003EC1015AB1"] = true, --Blaumeux 64 | ["0xF130003EC0015AB0"] = true, --Thane 65 | ["0xF130003E52015824"] = true, --Noth 66 | ["0xF130003E4001588D"] = true, --Heigan 67 | ["0xF130003E8B0158A2"] = true, --Loatheb 68 | ["0xF130003E9C0158EA"] = true, --Patchwerk 69 | ["0xF130003E3B0158EF"] = true, --Grobbulus 70 | ["0xF130003E3C0158F0"] = true, --Gluth 71 | ["0xF130003E380159A0"] = true, --Thaddius 72 | ["0xF130003E75015AB4"] = true, --Sapphiron 73 | ["0xF130003E76015AED"] = true, --Kel'Thuzad 74 | }, 75 | } 76 | 77 | -- combat events for curses 78 | local fades_test = L["(.+) fades from (.+)"] 79 | local resist_test = L["Your (.+) was resisted by (.+)"] 80 | local missed_test = L["Your (.+) missed (.+)"] 81 | local parry_test = L["Your (.+) is parried by (.+)"] 82 | local immune_test = L["Your (.+) fail.+\. (.+) is immune"] 83 | local block_test = L["Your (.+) was blocked by (.+)"] 84 | local dodge_test = L["Your (.+) was dodged by (.+)"] 85 | 86 | local molten_blast_test = L["Your Molten Blast(.+)for .+ Fire damage"] 87 | 88 | local lastGuid = nil 89 | 90 | -- I think depending on ping the combo point used event can fire either before or after your ability cast 91 | function curses:GetComboPointsUsed() 92 | if curses.comboPoints == 0 then 93 | return curses.previousComboPoints 94 | else 95 | return curses.comboPoints 96 | end 97 | end 98 | 99 | 100 | function curses:LoadCurses() 101 | -- reset dicts 102 | curses.trackedCurseIds = {} 103 | curses.trackedCurseNamesToTextures = {} 104 | curses.trackedCurseNameRanksToSpellSlots = {} 105 | 106 | curses.isWarlock = playerClassName == "WARLOCK" 107 | curses.isPriest = playerClassName == "PRIEST" 108 | curses.isMage = playerClassName == "MAGE" 109 | curses.isDruid = playerClassName == "DRUID" 110 | curses.isHunter = playerClassName == "HUNTER" 111 | curses.isRogue = playerClassName == "ROGUE" 112 | curses.isShaman = playerClassName == "SHAMAN" 113 | curses.isWarrior = playerClassName == "WARRIOR" 114 | 115 | -- curses to track 116 | if curses.isWarlock then 117 | curses.trackedCurseIds = getWarlockSpells() 118 | elseif curses.isPriest then 119 | curses.trackedCurseIds = getPriestSpells() 120 | elseif curses.isMage then 121 | curses.trackedCurseIds = getMageSpells() 122 | elseif curses.isDruid then 123 | curses.trackedCurseIds = getDruidSpells() 124 | elseif curses.isHunter then 125 | curses.trackedCurseIds = getHunterSpells() 126 | elseif curses.isRogue then 127 | curses.trackedCurseIds = getRogueSpells() 128 | elseif curses.isShaman then 129 | curses.trackedCurseIds = getShamanSpells() 130 | elseif curses.isWarrior then 131 | curses.trackedCurseIds = getWarriorSpells() 132 | end 133 | 134 | -- load shared debuffs 135 | curses.sharedDebuffs = getSharedDebuffs() 136 | 137 | -- go through spell slots and 138 | local i = 1 139 | while true do 140 | local spellname, spellrank = GetSpellName(i, BOOKTYPE_SPELL) 141 | if not spellname then 142 | break 143 | end 144 | 145 | if spellrank == "" then 146 | spellrank = L["Rank 1"] 147 | end 148 | 149 | curses.trackedCurseNameRanksToSpellSlots[string.lower(spellname) .. spellrank] = i 150 | i = i + 1 151 | end 152 | 153 | for id, data in pairs(curses.trackedCurseIds) do 154 | -- get the texture 155 | local name, rank, texture = SpellInfo(id) 156 | -- update trackedCurseNamesToTextures 157 | curses.trackedCurseNamesToTextures[data.name] = texture 158 | -- update trackedCurseIds 159 | curses.trackedCurseIds[id].texture = texture 160 | end 161 | 162 | if curses.isDruid or curses.isRogue then 163 | Cursive:RegisterEvent("PLAYER_COMBO_POINTS", function() 164 | local currentComboPoints = GetComboPoints() 165 | if curses.isDruid and currentComboPoints >= curses.comboPoints then 166 | -- combo points did not decrease, check if Ferocious Bite was used is the last .5 sec 167 | if GetTime() - curses.lastFerociousBiteTime < 0.5 and 168 | curses.lastFerociousBiteTargetGuid and 169 | curses.lastFerociousBiteTargetGuid ~= 0 then 170 | 171 | -- check if Rip active 172 | local rip = L["rip"] 173 | if curses:HasCurse(rip, curses.lastFerociousBiteTargetGuid, 0) then 174 | curses.guids[curses.lastFerociousBiteTargetGuid][rip]["start"] = GetTime() -- reset start time to current time 175 | end 176 | 177 | -- check if Rake active 178 | local rake = L["rake"] 179 | if curses:HasCurse(rake, curses.lastFerociousBiteTargetGuid, 0) then 180 | curses.guids[curses.lastFerociousBiteTargetGuid][rake]["start"] = GetTime() -- reset start time to current time 181 | end 182 | end 183 | end 184 | curses.previousComboPoints = curses.comboPoints 185 | curses.comboPoints = currentComboPoints 186 | end) 187 | end 188 | end 189 | 190 | function curses:ScanTooltipForDuration(curseSpellID) 191 | -- scan spellbook for duration in case they have haste talent 192 | local nameRank = curses.trackedCurseIds[curseSpellID].name .. L["Rank"] .. " " .. curses.trackedCurseIds[curseSpellID].rank 193 | local spellSlot = curses.trackedCurseNameRanksToSpellSlots[nameRank] 194 | 195 | if spellSlot then 196 | Cursive.core.tooltipScan:SetOwner(Cursive.core.tooltipScan, "ANCHOR_NONE") 197 | Cursive.core.tooltipScan:ClearLines() 198 | Cursive.core.tooltipScan:SetSpell(spellSlot, BOOKTYPE_SPELL) 199 | local numLines = Cursive.core.tooltipScan:NumLines() 200 | if numLines and numLines > 0 then 201 | -- get the last line 202 | local text = getglobal("CursiveTooltipScan" .. "TextLeft" .. numLines):GetText() 203 | if text then 204 | local _, _, duration = string.find(text, L["curse_duration_format"]) 205 | if duration then 206 | return tonumber(duration) 207 | end 208 | end 209 | end 210 | end 211 | 212 | return curses.trackedCurseIds[curseSpellID].duration 213 | end 214 | 215 | function curses:GetCurseDuration(curseSpellID) 216 | if curses.trackedCurseIds[curseSpellID].variableDuration then 217 | return curses:ScanTooltipForDuration(curseSpellID) 218 | elseif curses.trackedCurseIds[curseSpellID].calculateDuration then 219 | return curses.trackedCurseIds[curseSpellID].calculateDuration() 220 | end 221 | 222 | return curses.trackedCurseIds[curseSpellID].duration 223 | end 224 | 225 | function curses:ScanGuidForCurse(guid, curseSpellID) 226 | for i = 1, 64 do 227 | local _, _, _, spellID = UnitDebuff(guid, i) 228 | if spellID then 229 | if spellID == curseSpellID then 230 | return true 231 | end 232 | else 233 | break 234 | end 235 | end 236 | for i = 1, 64 do 237 | local _, _, spellID = UnitBuff(guid, i) 238 | if spellID then 239 | if spellID == curseSpellID then 240 | return true 241 | end 242 | else 243 | break 244 | end 245 | end 246 | 247 | return nil 248 | end 249 | 250 | function curses:GetLowercaseSpellName(spellName) 251 | spellName = string.lower(spellName) 252 | 253 | -- handle faerie fire special case 254 | if curses.isDruid and string.find(spellName, L["faerie fire"]) then 255 | return L["faerie fire"] 256 | end 257 | 258 | return spellName 259 | end 260 | 261 | Cursive:RegisterEvent("LEARNED_SPELL_IN_TAB", function() 262 | -- reload curses in case spell slots changed 263 | curses:LoadCurses() 264 | end) 265 | 266 | local function StopChanneling() 267 | curses.isChanneling = false 268 | end 269 | 270 | Cursive:RegisterEvent("SPELLCAST_CHANNEL_START", function() 271 | curses.isChanneling = true 272 | end); 273 | Cursive:RegisterEvent("SPELLCAST_CHANNEL_STOP", StopChanneling); 274 | Cursive:RegisterEvent("SPELLCAST_INTERRUPTED", StopChanneling); 275 | Cursive:RegisterEvent("SPELLCAST_FAILED", StopChanneling); 276 | 277 | Cursive:RegisterEvent("UNIT_CASTEVENT", function(casterGuid, targetGuid, event, spellID, castDuration) 278 | -- immolate will fire both start and cast 279 | if event == "CAST" then 280 | local _, guid = UnitExists("player") 281 | if casterGuid ~= guid then 282 | -- check for faeriefire 283 | if Cursive.db.profile.shareddebuffs.faeriefire and curses.sharedDebuffs.faeriefire[spellID] then 284 | curses.sharedDebuffGuids.faeriefire[targetGuid] = GetTime() 285 | end 286 | 287 | return 288 | end 289 | 290 | -- store pending cast 291 | curses.pendingCast = { 292 | spellID = spellID, 293 | targetGuid = targetGuid, 294 | castDuration = castDuration 295 | } 296 | 297 | if curses.isDruid then 298 | -- track ferocious bite cast time and target 299 | if spellID == 22557 or 300 | spellID == 22568 or 301 | spellID == 22827 or 302 | spellID == 22828 or 303 | spellID == 22829 or 304 | spellID == 31018 then 305 | curses.lastFerociousBiteTime = GetTime() 306 | curses.lastFerociousBiteTargetGuid = targetGuid 307 | end 308 | end 309 | 310 | if curses.isShaman then 311 | if spellID >= 36916 and spellID <= 36921 then 312 | curses.lastMoltenBlastTargetGuid = targetGuid 313 | end 314 | end 315 | 316 | -- delay to check for resists/failures 317 | local delay = 0.2 318 | 319 | local _, _, nping = GetNetStats() 320 | -- ignore extreme pings 321 | if nping and nping > 0 and nping < 500 then 322 | delay = 0.05 + (nping / 1000.0) -- convert to seconds 323 | end 324 | 325 | if curses.trackedCurseIds[spellID] then 326 | lastGuid = targetGuid 327 | local duration = curses:GetCurseDuration(spellID) - delay 328 | Cursive:ScheduleEvent("addCurse" .. targetGuid .. curses.trackedCurseIds[spellID].name, curses.ApplyCurse, delay, self, spellID, targetGuid, GetTime(), duration) 329 | elseif curses.conflagrateSpellIds[spellID] then 330 | Cursive:ScheduleEvent("updateCurse" .. targetGuid .. L["conflagrate"], curses.UpdateCurse, delay, self, spellID, targetGuid, GetTime()) 331 | end 332 | elseif event == "START" then 333 | if curses.trackedCurseIds[spellID] then 334 | local _, guid = UnitExists("player") 335 | if casterGuid ~= guid then 336 | return 337 | end 338 | 339 | -- store pending cast 340 | curses.pendingCast = { 341 | spellID = spellID, 342 | targetGuid = targetGuid, 343 | castDuration = castDuration 344 | } 345 | end 346 | elseif event == "FAIL" then 347 | if curses.trackedCurseIds[spellID] then 348 | local _, guid = UnitExists("player") 349 | if casterGuid ~= guid then 350 | return 351 | end 352 | -- clear pending cast 353 | curses.pendingCast = {} 354 | end 355 | elseif event == "CHANNEL" then 356 | -- dark harvest 357 | if curses.darkHarvestSpellIds[spellID] then 358 | local _, guid = UnitExists("player") 359 | if casterGuid ~= guid then 360 | return 361 | end 362 | 363 | curses.darkHarvestData = { 364 | spellID = spellID, 365 | targetGuid = targetGuid, 366 | castDuration = curses:ScanTooltipForDuration(spellID), 367 | start = GetTime() 368 | } 369 | end 370 | end 371 | end) 372 | 373 | Cursive:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE", 374 | function(message) 375 | local spell_failed_tests = { 376 | resist_test, 377 | immune_test 378 | } 379 | -- only some classes have melee spells that need to check for dodge, parry, miss, block 380 | if playerClassName == "DRUID" or playerClassName == "HUNTER" or playerClassName == "ROGUE" then 381 | spell_failed_tests = { 382 | resist_test, 383 | immune_test, 384 | missed_test, 385 | parry_test, 386 | block_test, 387 | dodge_test 388 | } 389 | end 390 | 391 | local spellName, failedTarget 392 | for _, test in pairs(spell_failed_tests) do 393 | local _, _, foundSpell, foundTarget = string.find(message, test) 394 | if foundSpell and foundTarget then 395 | spellName = foundSpell 396 | failedTarget = foundTarget 397 | break 398 | end 399 | end 400 | 401 | if spellName and failedTarget then 402 | spellName = curses:GetLowercaseSpellName(spellName) 403 | 404 | curses.pendingCast = {} 405 | 406 | if curses.trackedCurseNamesToTextures[spellName] and lastGuid then 407 | Cursive:CancelScheduledEvent("addCurse" .. lastGuid .. spellName) 408 | -- check if sound should be played 409 | if curses:ShouldPlayResistSound(lastGuid) then 410 | PlaySoundFile("Interface\\AddOns\\Cursive\\Sounds\\resist.mp3") 411 | end 412 | elseif spellName == L["conflagrate"] and lastGuid then 413 | Cursive:CancelScheduledEvent("updateCurse" .. lastGuid .. spellName) 414 | end 415 | return 416 | end 417 | 418 | if curses.isShaman and string.find(message, molten_blast_test) then 419 | local flame_shock = L["flame shock"] 420 | if curses:HasCurse(flame_shock, curses.lastMoltenBlastTargetGuid, 0) then 421 | curses.guids[curses.lastMoltenBlastTargetGuid][flame_shock]["start"] = GetTime() -- reset start time to current time 422 | end 423 | end 424 | end 425 | ) -- resists 426 | 427 | Cursive:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER", function(message) 428 | -- check if spell that faded is relevant 429 | local _, _, spellName, target = string.find(message, fades_test) 430 | if spellName and target then 431 | spellName = curses:GetLowercaseSpellName(spellName) 432 | if curses.trackedCurseNamesToTextures[spellName] then 433 | -- loop through targets with active curses 434 | for guid, data in pairs(curses.guids) do 435 | for curseName, curseData in pairs(data) do 436 | if curseName == spellName then 437 | -- see if target still has that curse 438 | if not curses:ScanGuidForCurse(guid, curseData.spellID) then 439 | -- remove curse 440 | curses:RemoveCurse(guid, curseName) 441 | end 442 | end 443 | end 444 | end 445 | end 446 | end 447 | end 448 | ) 449 | 450 | function curses:GetLastTickTime(curseData) 451 | local ticks = curses.trackedCurseIds[curseData.spellID].numTicks 452 | if not ticks then 453 | return GetTime() 454 | end 455 | 456 | local tickTime = curseData.duration / ticks 457 | local currentTime = GetTime() + tickTime * .2 -- dh won't apply to previous tick if within 20% of tick time 458 | 459 | return math.floor((currentTime - curseData.start) / tickTime) * tickTime + curseData.start 460 | end 461 | 462 | function curses:TrackDarkHarvest(curseData) 463 | if curses.darkHarvestData["targetGuid"] and curses.darkHarvestData["targetGuid"] == curseData["targetGuid"] then 464 | local dhActive = false 465 | -- check if still channeling 466 | if curses.isChanneling then 467 | local dhTimeRemaining = curses.darkHarvestData.castDuration - (GetTime() - curses.darkHarvestData.start) 468 | -- check if dh still active based on cast duration 469 | if dhTimeRemaining > 0 then 470 | dhActive = true 471 | -- dh is active 472 | if not curseData["dhStartTime"] then 473 | curseData["dhStartTime"] = curses:GetLastTickTime(curseData) -- dh will reduce full tick duration 474 | end 475 | end 476 | end 477 | if curseData["dhStartTime"] and dhActive == false and not curseData["dhEndTime"] then 478 | -- if dh no longer active, store end time if not already stored 479 | curseData["dhEndTime"] = GetTime() 480 | end 481 | end 482 | end 483 | 484 | function curses:GetDarkHarvestReduction(curseData) 485 | if curseData["dhStartTime"] then 486 | local endTime = curseData["dhEndTime"] or GetTime() 487 | local dhActiveTime = endTime - curseData["dhStartTime"] 488 | if dhActiveTime > 0 then 489 | return dhActiveTime * .3 -- 30% reduction 490 | end 491 | end 492 | return 0 493 | end 494 | 495 | function curses:TimeRemaining(curseData) 496 | local dhReduction = 0 497 | 498 | if curses.trackedCurseIds[curseData.spellID].darkHarvest then 499 | curses:TrackDarkHarvest(curseData) 500 | 501 | dhReduction = curses:GetDarkHarvestReduction(curseData) 502 | end 503 | 504 | local remaining = curseData.duration - (GetTime() - curseData.start) - dhReduction 505 | if Cursive.db.profile.curseshowdecimals and remaining < 10 then 506 | -- round to 1 decimal point 507 | remaining = math.floor(remaining * 10) / 10 508 | else 509 | remaining = math.ceil(remaining) 510 | end 511 | 512 | return remaining 513 | end 514 | 515 | function curses:EnableResistSound(guid) 516 | curses.resistSoundGuids[guid] = true 517 | end 518 | 519 | function curses:EnableExpiringSound(lowercaseSpellNameNoRank, guid) 520 | if curses.requestedExpiringSoundGuids[guid] and curses.requestedExpiringSoundGuids[guid][lowercaseSpellNameNoRank] then 521 | curses.requestedExpiringSoundGuids[guid][lowercaseSpellNameNoRank] = nil 522 | end 523 | 524 | if not curses.expiringSoundGuids[guid] then 525 | curses.expiringSoundGuids[guid] = {} 526 | end 527 | curses.expiringSoundGuids[guid][lowercaseSpellNameNoRank] = true 528 | end 529 | 530 | function curses:RequestExpiringSound(lowercaseSpellNameNoRank, guid) 531 | if not curses.requestedExpiringSoundGuids[guid] then 532 | curses.requestedExpiringSoundGuids[guid] = {} 533 | end 534 | curses.requestedExpiringSoundGuids[guid][lowercaseSpellNameNoRank] = true 535 | end 536 | 537 | function curses:HasRequestedExpiringSound(lowercaseSpellNameNoRank, guid) 538 | return curses.requestedExpiringSoundGuids[guid] and curses.requestedExpiringSoundGuids[guid][lowercaseSpellNameNoRank] 539 | end 540 | 541 | function curses:ShouldPlayExpiringSound(lowercaseSpellNameNoRank, guid) 542 | if curses.expiringSoundGuids[guid] and curses.expiringSoundGuids[guid][lowercaseSpellNameNoRank] then 543 | curses.expiringSoundGuids[guid][lowercaseSpellNameNoRank] = nil -- remove entry to avoid playing sound multiple times 544 | return true 545 | end 546 | 547 | return false 548 | end 549 | 550 | function curses:ShouldPlayResistSound(guid) 551 | if curses.resistSoundGuids[guid] then 552 | curses.resistSoundGuids[guid] = nil -- remove entry to avoid playing sound multiple times 553 | return true 554 | end 555 | 556 | return false 557 | end 558 | 559 | function curses:HasAnyCurse(guid) 560 | if curses.guids[guid] and next(curses.guids[guid]) then 561 | return true 562 | end 563 | return nil 564 | end 565 | 566 | function curses:GetCurseData(spellName, guid) 567 | -- convert to lowercase and remove rank 568 | local lowercaseSpellNameNoRank = Cursive.utils.GetLowercaseSpellNameNoRank(spellName) 569 | 570 | if curses.guids[guid] and curses.guids[guid][lowercaseSpellNameNoRank] then 571 | return curses.guids[guid][lowercaseSpellNameNoRank] 572 | end 573 | 574 | return nil 575 | end 576 | 577 | function curses:HasCurse(lowercaseSpellNameNoRank, targetGuid, minRemaining) 578 | if not minRemaining then 579 | minRemaining = 0 -- default to 0 580 | end 581 | 582 | -- handle faerie fire special case 583 | if curses.isDruid and string.find(lowercaseSpellNameNoRank, L["faerie fire"]) then 584 | -- remove (feral) or (bear) from spell name 585 | lowercaseSpellNameNoRank = L["faerie fire"] 586 | end 587 | 588 | if curses.guids[targetGuid] and curses.guids[targetGuid][lowercaseSpellNameNoRank] then 589 | local remaining = Cursive.curses:TimeRemaining(curses.guids[targetGuid][lowercaseSpellNameNoRank]) 590 | if remaining >= minRemaining then 591 | return true 592 | end 593 | end 594 | 595 | -- check pending cast 596 | if curses.pendingCast and 597 | curses.pendingCast.targetGuid == targetGuid and 598 | curses.pendingCast.spellID and 599 | curses.trackedCurseIds[curses.pendingCast.spellID] and 600 | curses.trackedCurseIds[curses.pendingCast.spellID].name == lowercaseSpellNameNoRank then 601 | return true 602 | end 603 | 604 | return nil 605 | end 606 | 607 | -- Apply shared curse from another player 608 | function curses:ApplySharedCurse(sharedDebuffKey, spellID, targetGuid, startTime) 609 | local name = curses.sharedDebuffs[sharedDebuffKey][spellID].name 610 | local rank = curses.sharedDebuffs[sharedDebuffKey][spellID].rank 611 | local duration = curses.sharedDebuffs[sharedDebuffKey][spellID].duration 612 | 613 | if not curses.guids[targetGuid] then 614 | curses.guids[targetGuid] = {} 615 | end 616 | 617 | curses.guids[targetGuid][name] = { 618 | rank = rank, 619 | duration = duration, 620 | start = startTime, 621 | spellID = spellID, 622 | targetGuid = targetGuid, 623 | currentPlayer = false, 624 | } 625 | end 626 | 627 | -- Apply curse from player 628 | function curses:ApplyCurse(spellID, targetGuid, startTime, duration) 629 | -- clear pending cast 630 | curses.pendingCast = {} 631 | 632 | local name = curses.trackedCurseIds[spellID].name 633 | local rank = curses.trackedCurseIds[spellID].rank 634 | 635 | if curses.isDruid and name == L["rake"] then 636 | -- check if mob is in bleed whitelist first 637 | -- these bosses are most likely to hit 48 client debuff cap 638 | if not curses.mobsThatBleed[targetGuid] then 639 | if not curses:ScanGuidForCurse(targetGuid, spellID) then 640 | -- rake not found on target, do not apply 641 | return 642 | end 643 | end 644 | end 645 | 646 | if not curses.guids[targetGuid] then 647 | curses.guids[targetGuid] = {} 648 | end 649 | 650 | curses.guids[targetGuid][name] = { 651 | rank = rank, 652 | duration = duration, 653 | start = startTime, 654 | spellID = spellID, 655 | targetGuid = targetGuid, 656 | currentPlayer = true, 657 | } 658 | end 659 | 660 | function curses:UpdateCurse(spellID, targetGuid, startTime) 661 | -- clear pending cast 662 | curses.pendingCast = {} 663 | 664 | if curses.conflagrateSpellIds[spellID] then 665 | -- check if target has immolate 666 | if curses:HasCurse(L["immolate"], targetGuid) then 667 | -- reduce duration by 3 sec 668 | curses.guids[targetGuid][L["immolate"]].duration = curses.guids[targetGuid][L["immolate"]].duration - 3 669 | end 670 | end 671 | end 672 | 673 | function curses:RemoveCurse(guid, curseName) 674 | if curses.guids[guid] and curses.guids[guid][curseName] then 675 | curses.guids[guid][curseName] = nil 676 | end 677 | if curses.expiringSoundGuids[guid] and curses.expiringSoundGuids[guid][curseName] then 678 | curses.expiringSoundGuids[guid][curseName] = nil 679 | end 680 | end 681 | 682 | function curses:RemoveGuid(guid) 683 | curses.guids[guid] = nil 684 | curses.resistSoundGuids[guid] = nil 685 | curses.expiringSoundGuids[guid] = nil 686 | curses.requestedExpiringSoundGuids[guid] = nil 687 | end 688 | 689 | Cursive.curses = curses 690 | -------------------------------------------------------------------------------- /libs/AceLibrary/AceLibrary.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Name: AceLibrary 3 | Revision: $Rev: 17722 $ 4 | Developed by: The Ace Development Team (http://www.wowace.com/index.php/The_Ace_Development_Team) 5 | Inspired By: Iriel (iriel@vigilance-committee.org) 6 | Tekkub (tekkub@gmail.com) 7 | Revision: $Rev: 17722 $ 8 | Website: http://www.wowace.com/ 9 | Documentation: http://www.wowace.com/index.php/AceLibrary 10 | SVN: http://svn.wowace.com/root/trunk/Ace2/AceLibrary 11 | Description: Versioning library to handle other library instances, upgrading, 12 | and proper access. 13 | It also provides a base for libraries to work off of, providing 14 | proper error tools. It is handy because all the errors occur in the 15 | file that called it, not in the library file itself. 16 | Dependencies: None 17 | ]] 18 | 19 | local ACELIBRARY_MAJOR = "AceLibrary" 20 | local ACELIBRARY_MINOR = "$Revision: 17722 $" 21 | 22 | if loadstring("return function(...) return ... end") and AceLibrary and AceLibrary:HasInstance(ACELIBRARY_MAJOR) then return end -- lua51 check 23 | local table_setn 24 | do 25 | local version = GetBuildInfo() 26 | if string.find(version, "^2%.") then 27 | -- 2.0.0 28 | table_setn = function() end 29 | else 30 | table_setn = table.setn 31 | end 32 | end 33 | 34 | local string_gfind = string.gmatch or string.gfind 35 | 36 | local _G = getfenv(0) 37 | local previous = _G[ACELIBRARY_MAJOR] 38 | if previous and not previous:IsNewVersion(ACELIBRARY_MAJOR, ACELIBRARY_MINOR) then return end 39 | 40 | local function safecall(func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) 41 | local success, err = pcall(func,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10) 42 | if not success then geterrorhandler()(err) end 43 | end 44 | 45 | -- @table AceLibrary 46 | -- @brief System to handle all versioning of libraries. 47 | local AceLibrary = {} 48 | local AceLibrary_mt = {} 49 | setmetatable(AceLibrary, AceLibrary_mt) 50 | 51 | local tmp 52 | 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) 53 | if type(self) ~= "table" then 54 | _G.error(string.format("Bad argument #1 to `error' (table expected, got %s)", type(self)), 2) 55 | end 56 | if not tmp then 57 | tmp = {} 58 | else 59 | for k in pairs(tmp) do tmp[k] = nil end 60 | table_setn(tmp, 0) 61 | end 62 | 63 | table.insert(tmp, a1) 64 | table.insert(tmp, a2) 65 | table.insert(tmp, a3) 66 | table.insert(tmp, a4) 67 | table.insert(tmp, a5) 68 | table.insert(tmp, a6) 69 | table.insert(tmp, a7) 70 | table.insert(tmp, a8) 71 | table.insert(tmp, a9) 72 | table.insert(tmp, a10) 73 | table.insert(tmp, a11) 74 | table.insert(tmp, a12) 75 | table.insert(tmp, a13) 76 | table.insert(tmp, a14) 77 | table.insert(tmp, a15) 78 | table.insert(tmp, a16) 79 | table.insert(tmp, a17) 80 | table.insert(tmp, a18) 81 | table.insert(tmp, a19) 82 | table.insert(tmp, a20) 83 | 84 | local stack = debugstack() 85 | if not message then 86 | local _,_,second = string.find(stack, "\n(.-)\n") 87 | message = "error raised! " .. second 88 | else 89 | for i = 1,table.getn(tmp) do 90 | tmp[i] = tostring(tmp[i]) 91 | end 92 | for i = 1,10 do 93 | table.insert(tmp, "nil") 94 | end 95 | message = string.format(message, unpack(tmp)) 96 | end 97 | 98 | if getmetatable(self) and getmetatable(self).__tostring then 99 | message = string.format("%s: %s", tostring(self), message) 100 | elseif type(rawget(self, 'GetLibraryVersion')) == "function" and AceLibrary:HasInstance(self:GetLibraryVersion()) then 101 | message = string.format("%s: %s", self:GetLibraryVersion(), message) 102 | elseif type(rawget(self, 'class')) == "table" and type(rawget(self.class, 'GetLibraryVersion')) == "function" and AceLibrary:HasInstance(self.class:GetLibraryVersion()) then 103 | message = string.format("%s: %s", self.class:GetLibraryVersion(), message) 104 | end 105 | 106 | local first = string.gsub(stack, "\n.*", "") 107 | local file = string.gsub(first, ".*\\(.*).lua:%d+: .*", "%1") 108 | file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") 109 | 110 | local i = 0 111 | for s in string_gfind(stack, "\n([^\n]*)") do 112 | i = i + 1 113 | if not string.find(s, file .. "%.lua:%d+:") then 114 | file = string.gsub(s, "^.*\\(.*).lua:%d+: .*", "%1") 115 | file = string.gsub(file, "([%(%)%.%*%+%-%[%]%?%^%$%%])", "%%%1") 116 | break 117 | end 118 | end 119 | local j = 0 120 | for s in string_gfind(stack, "\n([^\n]*)") do 121 | j = j + 1 122 | if j > i and not string.find(s, file .. "%.lua:%d+:") then 123 | _G.error(message, j + 1) 124 | return 125 | end 126 | end 127 | _G.error(message, 2) 128 | return 129 | end 130 | 131 | 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) 132 | if not condition then 133 | if not message then 134 | local stack = debugstack() 135 | local _,_,second = string.find(stack, "\n(.-)\n") 136 | message = "assertion failed! " .. second 137 | end 138 | error(self, message, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20) 139 | return 140 | end 141 | return condition 142 | end 143 | 144 | local function argCheck(self, arg, num, kind, kind2, kind3, kind4, kind5) 145 | if type(num) ~= "number" then 146 | error(self, "Bad argument #3 to `argCheck' (number expected, got %s)", type(num)) 147 | elseif type(kind) ~= "string" then 148 | error(self, "Bad argument #4 to `argCheck' (string expected, got %s)", type(kind)) 149 | end 150 | local errored = false 151 | arg = type(arg) 152 | if arg ~= kind and arg ~= kind2 and arg ~= kind3 and arg ~= kind4 and arg ~= kind5 then 153 | local _,_,func = string.find(debugstack(), "`argCheck'.-([`<].-['>])") 154 | if not func then 155 | _,_,func = string.find(debugstack(), "([`<].-['>])") 156 | end 157 | if kind5 then 158 | 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) 159 | elseif kind4 then 160 | 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) 161 | elseif kind3 then 162 | error(self, "Bad argument #%s to %s (%s, %s, or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, kind3, arg) 163 | elseif kind2 then 164 | error(self, "Bad argument #%s to %s (%s or %s expected, got %s)", tonumber(num) or 0/0, func, kind, kind2, arg) 165 | else 166 | error(self, "Bad argument #%s to %s (%s expected, got %s)", tonumber(num) or 0/0, func, kind, arg) 167 | end 168 | end 169 | end 170 | 171 | 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) 172 | 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) 173 | if not a1 then 174 | error(self, string.gsub(a2, ".-%.lua:%d-: ", "")) 175 | else 176 | return a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20 177 | end 178 | end 179 | 180 | local recurse = {} 181 | local function addToPositions(t, major) 182 | if not AceLibrary.positions[t] or AceLibrary.positions[t] == major then 183 | rawset(t, recurse, true) 184 | AceLibrary.positions[t] = major 185 | for k,v in pairs(t) do 186 | if type(v) == "table" and not rawget(v, recurse) then 187 | addToPositions(v, major) 188 | end 189 | if type(k) == "table" and not rawget(k, recurse) then 190 | addToPositions(k, major) 191 | end 192 | end 193 | local mt = getmetatable(t) 194 | if mt and not rawget(mt, recurse) then 195 | addToPositions(mt, major) 196 | end 197 | rawset(t, recurse, nil) 198 | end 199 | end 200 | 201 | local function svnRevisionToNumber(text) 202 | if type(text) == "string" then 203 | if string.find(text, "^%$Revision: (%d+) %$$") then 204 | return tonumber((string.gsub(text, "^%$Revision: (%d+) %$$", "%1"))) 205 | elseif string.find(text, "^%$Rev: (%d+) %$$") then 206 | return tonumber((string.gsub(text, "^%$Rev: (%d+) %$$", "%1"))) 207 | elseif string.find(text, "^%$LastChangedRevision: (%d+) %$$") then 208 | return tonumber((string.gsub(text, "^%$LastChangedRevision: (%d+) %$$", "%1"))) 209 | end 210 | elseif type(text) == "number" then 211 | return text 212 | end 213 | return nil 214 | end 215 | 216 | local crawlReplace 217 | do 218 | local recurse = {} 219 | local function func(t, to, from) 220 | if recurse[t] then 221 | return 222 | end 223 | recurse[t] = true 224 | local mt = getmetatable(t) 225 | setmetatable(t, nil) 226 | rawset(t, to, rawget(t, from)) 227 | rawset(t, from, nil) 228 | for k,v in pairs(t) do 229 | if v == from then 230 | t[k] = to 231 | elseif type(v) == "table" then 232 | if not recurse[v] then 233 | func(v, to, from) 234 | end 235 | end 236 | 237 | if type(k) == "table" then 238 | if not recurse[k] then 239 | func(k, to, from) 240 | end 241 | end 242 | end 243 | setmetatable(t, mt) 244 | if mt then 245 | if mt == from then 246 | setmetatable(t, to) 247 | elseif not recurse[mt] then 248 | func(mt, to, from) 249 | end 250 | end 251 | end 252 | function crawlReplace(t, to, from) 253 | func(t, to, from) 254 | for k in pairs(recurse) do 255 | recurse[k] = nil 256 | end 257 | end 258 | end 259 | 260 | -- @function destroyTable 261 | -- @brief remove all the contents of a table 262 | -- @param t table to destroy 263 | local function destroyTable(t) 264 | setmetatable(t, nil) 265 | for k,v in pairs(t) do t[k] = nil end 266 | table_setn(t, 0) 267 | end 268 | 269 | local function isFrame(frame) 270 | 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" 271 | end 272 | 273 | local new, del 274 | do 275 | local tables = setmetatable({}, {__mode = "k"}) 276 | 277 | function new() 278 | local t = next(tables) 279 | if t then 280 | tables[t] = nil 281 | return t 282 | else 283 | return {} 284 | end 285 | end 286 | 287 | function del(t, depth) 288 | if depth and depth > 0 then 289 | for k,v in pairs(t) do 290 | if type(v) == "table" and not isFrame(v) then 291 | del(v, depth - 1) 292 | end 293 | end 294 | end 295 | destroyTable(t) 296 | tables[t] = true 297 | end 298 | end 299 | 300 | -- @function copyTable 301 | -- @brief Create a shallow copy of a table and return it. 302 | -- @param from The table to copy from 303 | -- @return A shallow copy of the table 304 | local function copyTable(from) 305 | local to = new() 306 | for k,v in pairs(from) do to[k] = v end 307 | table_setn(to, table.getn(from)) 308 | setmetatable(to, getmetatable(from)) 309 | return to 310 | end 311 | 312 | -- @function deepTransfer 313 | -- @brief Fully transfer all data, keeping proper previous table 314 | -- backreferences stable. 315 | -- @param to The table with which data is to be injected into 316 | -- @param from The table whose data will be injected into the first 317 | -- @param saveFields If available, a shallow copy of the basic data is saved 318 | -- in here. 319 | -- @param list The account of table references 320 | -- @param list2 The current status on which tables have been traversed. 321 | local deepTransfer 322 | do 323 | -- @function examine 324 | -- @brief Take account of all the table references to be shared 325 | -- between the to and from tables. 326 | -- @param to The table with which data is to be injected into 327 | -- @param from The table whose data will be injected into the first 328 | -- @param list An account of the table references 329 | local function examine(to, from, list, major) 330 | list[from] = to 331 | for k,v in pairs(from) do 332 | if rawget(to, k) and type(from[k]) == "table" and type(to[k]) == "table" and not list[from[k]] then 333 | if from[k] == to[k] then 334 | list[from[k]] = to[k] 335 | elseif AceLibrary.positions[from[v]] ~= major and AceLibrary.positions[from[v]] then 336 | list[from[k]] = from[k] 337 | elseif not list[from[k]] then 338 | examine(to[k], from[k], list, major) 339 | end 340 | end 341 | end 342 | return list 343 | end 344 | 345 | function deepTransfer(to, from, saveFields, major, list, list2) 346 | setmetatable(to, nil) 347 | local createdList 348 | if not list then 349 | createdList = true 350 | list = new() 351 | list2 = new() 352 | examine(to, from, list, major) 353 | end 354 | list2[to] = to 355 | for k,v in pairs(to) do 356 | if type(rawget(from, k)) ~= "table" or type(v) ~= "table" or isFrame(v) then 357 | if saveFields then 358 | saveFields[k] = v 359 | end 360 | to[k] = nil 361 | elseif v ~= _G then 362 | if saveFields then 363 | saveFields[k] = copyTable(v) 364 | end 365 | end 366 | end 367 | for k in pairs(from) do 368 | if rawget(to, k) and to[k] ~= from[k] and AceLibrary.positions[to[k]] == major and from[k] ~= _G then 369 | if not list2[to[k]] then 370 | deepTransfer(to[k], from[k], nil, major, list, list2) 371 | end 372 | to[k] = list[to[k]] or list2[to[k]] 373 | else 374 | rawset(to, k, from[k]) 375 | end 376 | end 377 | table_setn(to, table.getn(from)) 378 | setmetatable(to, getmetatable(from)) 379 | local mt = getmetatable(to) 380 | if mt then 381 | if list[mt] then 382 | setmetatable(to, list[mt]) 383 | elseif mt.__index and list[mt.__index] then 384 | mt.__index = list[mt.__index] 385 | end 386 | end 387 | destroyTable(from) 388 | if createdList then 389 | del(list) 390 | del(list2) 391 | end 392 | end 393 | end 394 | 395 | -- @method TryToLoadStandalone 396 | -- @brief Attempt to find and load a standalone version of the requested library 397 | -- @param major A string representing the major version 398 | -- @return If library is found, return values from the call to LoadAddOn are returned 399 | -- If the library has been requested previously, nil is returned. 400 | local function TryToLoadStandalone(major) 401 | if not AceLibrary.scannedlibs then AceLibrary.scannedlibs = {} end 402 | if AceLibrary.scannedlibs[major] then return end 403 | 404 | AceLibrary.scannedlibs[major] = true 405 | 406 | local name, _, _, enabled, loadable = GetAddOnInfo(major) 407 | if loadable then 408 | return LoadAddOn(name) 409 | end 410 | 411 | for i=1,GetNumAddOns() do 412 | if GetAddOnMetadata(i, "X-AceLibrary-"..major) then 413 | local name, _, _, enabled, loadable = GetAddOnInfo(i) 414 | if loadable then 415 | return LoadAddOn(name) 416 | end 417 | end 418 | end 419 | end 420 | 421 | -- @method IsNewVersion 422 | -- @brief Obtain whether the supplied version would be an upgrade to the 423 | -- current version. This allows for bypass code in library 424 | -- declaration. 425 | -- @param major A string representing the major version 426 | -- @param minor An integer or an svn revision string representing the minor version 427 | -- @return whether the supplied version would be newer than what is 428 | -- currently available. 429 | function AceLibrary:IsNewVersion(major, minor) 430 | argCheck(self, major, 2, "string") 431 | TryToLoadStandalone(major) 432 | 433 | if type(minor) == "string" then 434 | local m = svnRevisionToNumber(minor) 435 | if m then 436 | minor = m 437 | else 438 | _G.error(string.format("Bad argument #3 to `IsNewVersion'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) 439 | end 440 | end 441 | argCheck(self, minor, 3, "number") 442 | local data = self.libs[major] 443 | if not data then 444 | return true 445 | end 446 | return data.minor < minor 447 | end 448 | 449 | -- @method HasInstance 450 | -- @brief Returns whether an instance exists. This allows for optional support of a library. 451 | -- @param major A string representing the major version. 452 | -- @param minor (optional) An integer or an svn revision string representing the minor version. 453 | -- @return Whether an instance exists. 454 | function AceLibrary:HasInstance(major, minor) 455 | argCheck(self, major, 2, "string") 456 | TryToLoadStandalone(major) 457 | 458 | if minor then 459 | if type(minor) == "string" then 460 | local m = svnRevisionToNumber(minor) 461 | if m then 462 | minor = m 463 | else 464 | _G.error(string.format("Bad argument #3 to `HasInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) 465 | end 466 | end 467 | argCheck(self, minor, 3, "number") 468 | if not self.libs[major] then 469 | return 470 | end 471 | return self.libs[major].minor == minor 472 | end 473 | return self.libs[major] and true 474 | end 475 | 476 | -- @method GetInstance 477 | -- @brief Returns the library with the given major/minor version. 478 | -- @param major A string representing the major version. 479 | -- @param minor (optional) An integer or an svn revision string representing the minor version. 480 | -- @return The library with the given major/minor version. 481 | function AceLibrary:GetInstance(major, minor) 482 | argCheck(self, major, 2, "string") 483 | TryToLoadStandalone(major) 484 | 485 | local data = self.libs[major] 486 | if not data then 487 | _G.error(string.format("Cannot find a library instance of %s.", major), 2) 488 | return 489 | end 490 | if minor then 491 | if type(minor) == "string" then 492 | local m = svnRevisionToNumber(minor) 493 | if m then 494 | minor = m 495 | else 496 | _G.error(string.format("Bad argument #3 to `GetInstance'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) 497 | end 498 | end 499 | argCheck(self, minor, 2, "number") 500 | if data.minor ~= minor then 501 | _G.error(string.format("Cannot find a library instance of %s, minor version %d.", major, minor), 2) 502 | return 503 | end 504 | end 505 | return data.instance 506 | end 507 | 508 | -- Syntax sugar. AceLibrary("FooBar-1.0") 509 | AceLibrary_mt.__call = AceLibrary.GetInstance 510 | 511 | local donothing 512 | 513 | local AceEvent 514 | 515 | -- @method Register 516 | -- @brief Registers a new version of a given library. 517 | -- @param newInstance the library to register 518 | -- @param major the major version of the library 519 | -- @param minor the minor version of the library 520 | -- @param activateFunc (optional) A function to be called when the library is 521 | -- fully activated. Takes the arguments 522 | -- (newInstance [, oldInstance, oldDeactivateFunc]). If 523 | -- oldInstance is given, you should probably call 524 | -- oldDeactivateFunc(oldInstance). 525 | -- @param deactivateFunc (optional) A function to be called by a newer library's 526 | -- activateFunc. 527 | -- @param externalFunc (optional) A function to be called whenever a new 528 | -- library is registered. 529 | function AceLibrary:Register(newInstance, major, minor, activateFunc, deactivateFunc, externalFunc) 530 | argCheck(self, newInstance, 2, "table") 531 | argCheck(self, major, 3, "string") 532 | if type(minor) == "string" then 533 | local m = svnRevisionToNumber(minor) 534 | if m then 535 | minor = m 536 | else 537 | _G.error(string.format("Bad argument #4 to `Register'. Must be a number or SVN revision string. %q is not appropriate", minor), 2) 538 | end 539 | end 540 | argCheck(self, minor, 4, "number") 541 | if math.floor(minor) ~= minor or minor < 0 then 542 | error(self, "Bad argument #4 to `Register' (integer >= 0 expected, got %s)", minor) 543 | end 544 | argCheck(self, activateFunc, 5, "function", "nil") 545 | argCheck(self, deactivateFunc, 6, "function", "nil") 546 | argCheck(self, externalFunc, 7, "function", "nil") 547 | if not deactivateFunc then 548 | if not donothing then 549 | donothing = function() end 550 | end 551 | deactivateFunc = donothing 552 | end 553 | local data = self.libs[major] 554 | if not data then 555 | -- This is new 556 | local instance = copyTable(newInstance) 557 | crawlReplace(instance, instance, newInstance) 558 | destroyTable(newInstance) 559 | if AceLibrary == newInstance then 560 | self = instance 561 | AceLibrary = instance 562 | end 563 | self.libs[major] = { 564 | instance = instance, 565 | minor = minor, 566 | deactivateFunc = deactivateFunc, 567 | externalFunc = externalFunc, 568 | } 569 | rawset(instance, 'GetLibraryVersion', function(self) 570 | return major, minor 571 | end) 572 | if not rawget(instance, 'error') then 573 | rawset(instance, 'error', error) 574 | end 575 | if not rawget(instance, 'assert') then 576 | rawset(instance, 'assert', assert) 577 | end 578 | if not rawget(instance, 'argCheck') then 579 | rawset(instance, 'argCheck', argCheck) 580 | end 581 | if not rawget(instance, 'pcall') then 582 | rawset(instance, 'pcall', pcall) 583 | end 584 | addToPositions(instance, major) 585 | if activateFunc then 586 | safecall(activateFunc, instance, nil, nil) -- no old version, so explicit nil 587 | end 588 | 589 | if externalFunc then 590 | for k,data in pairs(self.libs) do 591 | if k ~= major then 592 | safecall(externalFunc, instance, k, data.instance) 593 | end 594 | end 595 | end 596 | 597 | for k,data in pairs(self.libs) do 598 | if k ~= major and data.externalFunc then 599 | safecall(data.externalFunc, data.instance, major, instance) 600 | end 601 | end 602 | if major == "AceEvent-2.0" then 603 | AceEvent = instance 604 | end 605 | if AceEvent then 606 | AceEvent.TriggerEvent(self, "AceLibrary_Register", major, instance) 607 | end 608 | 609 | return instance 610 | end 611 | local instance = data.instance 612 | if minor <= data.minor then 613 | -- This one is already obsolete, raise an error. 614 | _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) 615 | return 616 | end 617 | -- This is an update 618 | local oldInstance = new() 619 | 620 | addToPositions(newInstance, major) 621 | local isAceLibrary = (AceLibrary == newInstance) 622 | local old_error, old_assert, old_argCheck, old_pcall 623 | if isAceLibrary then 624 | self = instance 625 | AceLibrary = instance 626 | 627 | old_error = instance.error 628 | old_assert = instance.assert 629 | old_argCheck = instance.argCheck 630 | old_pcall = instance.pcall 631 | 632 | self.error = error 633 | self.assert = assert 634 | self.argCheck = argCheck 635 | self.pcall = pcall 636 | end 637 | deepTransfer(instance, newInstance, oldInstance, major) 638 | crawlReplace(instance, instance, newInstance) 639 | local oldDeactivateFunc = data.deactivateFunc 640 | data.minor = minor 641 | data.deactivateFunc = deactivateFunc 642 | data.externalFunc = externalFunc 643 | rawset(instance, 'GetLibraryVersion', function(self) 644 | return major, minor 645 | end) 646 | if not rawget(instance, 'error') then 647 | rawset(instance, 'error', error) 648 | end 649 | if not rawget(instance, 'assert') then 650 | rawset(instance, 'assert', assert) 651 | end 652 | if not rawget(instance, 'argCheck') then 653 | rawset(instance, 'argCheck', argCheck) 654 | end 655 | if not rawget(instance, 'pcall') then 656 | rawset(instance, 'pcall', pcall) 657 | end 658 | if isAceLibrary then 659 | for _,v in pairs(self.libs) do 660 | local i = type(v) == "table" and v.instance 661 | if type(i) == "table" then 662 | if not rawget(i, 'error') or i.error == old_error then 663 | rawset(i, 'error', error) 664 | end 665 | if not rawget(i, 'assert') or i.assert == old_assert then 666 | rawset(i, 'assert', assert) 667 | end 668 | if not rawget(i, 'argCheck') or i.argCheck == old_argCheck then 669 | rawset(i, 'argCheck', argCheck) 670 | end 671 | if not rawget(i, 'pcall') or i.pcall == old_pcall then 672 | rawset(i, 'pcall', pcall) 673 | end 674 | end 675 | end 676 | end 677 | if activateFunc then 678 | safecall(activateFunc, instance, oldInstance, oldDeactivateFunc) 679 | else 680 | safecall(oldDeactivateFunc, oldInstance) 681 | end 682 | del(oldInstance) 683 | 684 | if externalFunc then 685 | for k,data in pairs(self.libs) do 686 | if k ~= major then 687 | safecall(externalFunc, instance, k, data.instance) 688 | end 689 | end 690 | end 691 | 692 | return instance 693 | end 694 | 695 | local iter 696 | function AceLibrary:IterateLibraries() 697 | if not iter then 698 | local function iter(t, k) 699 | k = next(t, k) 700 | if not k then 701 | return nil 702 | else 703 | return k, t[k].instance 704 | end 705 | end 706 | end 707 | return iter, self.libs, nil 708 | end 709 | 710 | -- @function Activate 711 | -- @brief The activateFunc for AceLibrary itself. Called when 712 | -- AceLibrary properly registers. 713 | -- @param self Reference to AceLibrary 714 | -- @param oldLib (optional) Reference to an old version of AceLibrary 715 | -- @param oldDeactivate (optional) Function to deactivate the old lib 716 | local function activate(self, oldLib, oldDeactivate) 717 | if not self.libs then 718 | if oldLib then 719 | self.libs = oldLib.libs 720 | self.scannedlibs = oldLib.scannedlibs 721 | end 722 | if not self.libs then 723 | self.libs = {} 724 | end 725 | if not self.scannedlibs then 726 | self.scannedlibs = {} 727 | end 728 | end 729 | if not self.positions then 730 | if oldLib then 731 | self.positions = oldLib.positions 732 | end 733 | if not self.positions then 734 | self.positions = setmetatable({}, { __mode = "k" }) 735 | end 736 | end 737 | 738 | -- Expose the library in the global environment 739 | _G[ACELIBRARY_MAJOR] = self 740 | 741 | if oldDeactivate then 742 | oldDeactivate(oldLib) 743 | end 744 | end 745 | 746 | if not previous then 747 | previous = AceLibrary 748 | end 749 | if not previous.libs then 750 | previous.libs = {} 751 | end 752 | AceLibrary.libs = previous.libs 753 | if not previous.positions then 754 | previous.positions = setmetatable({}, { __mode = "k" }) 755 | end 756 | AceLibrary.positions = previous.positions 757 | AceLibrary:Register(AceLibrary, ACELIBRARY_MAJOR, ACELIBRARY_MINOR, activate) 758 | --------------------------------------------------------------------------------