local COMPAT, _, T = select(4,GetBuildInfo()), ... if T.SkipLocalActionBook then return end if T.TenEnv then T.TenEnv() end local EV, AB, KR, RW, IM = T.Evie, T.ActionBook:compatible(2,38), T.ActionBook:compatible("Kindred", 1,26), T.ActionBook:compatible("Rewire", 1,27), T.ActionBook:compatible("Imp", 1,11) assert(EV and AB and KR and RW and IM and 1, "Incompatible library bundle") local MODERN, CI_ERA, CF_CATA = COMPAT >= 10e4, COMPAT < 2e4, COMPAT < 10e4 and COMPAT > 4e4 local playerClass, _, playerRace = UnitClassBase("player"), UnitRace("player") local safequote do local r = {u="\\117", ["{"]="\\123", ["}"]="\\125"} function safequote(s) return (("%q"):format(s):gsub("[{}u]", r)) end end securecall(function() -- weirdly-persistent Druid Incarnations if not MODERN then return end local function checkSpellKnown(id) if not IsSpellKnown(id) then return false, "known-check" end end RW:SetSpellCastableChecker(33891, checkSpellKnown) RW:SetSpellCastableChecker(102543, checkSpellKnown) RW:SetSpellCastableChecker(102558, checkSpellKnown) RW:SetSpellCastableChecker(102560, function() if not (IsSpellKnown(194223) and select(7, GetSpellInfo((GetSpellInfo(194223)))) == 102560) then return false, "known-check" end end) end) securecall(function() -- class/race-locked mounts if not (MODERN or CF_CATA) then return end local classLockedMounts = { [48778]="DEATHKNIGHT", [54729]="DEATHKNIGHT", [229387]="DEATHKNIGHT", [229417]="DEMONHUNTER", [200175]="DEMONHUNTER", [229386]="HUNTER", [229438]="HUNTER", [229439]="HUNTER", [229376]="MAGE", [229385]="MONK", [13819]="PALADIN", [23214]="PALADIN", [34767]="PALADIN", [34769]="PALADIN", [66906]="PALADIN", [69820]="PALADIN", [69826]="PALADIN", [221883]="PALADIN", [205656]="PALADIN", [221885]="PALADIN", [221886]="PALADIN", [231587]="PALADIN", [231588]="PALADIN", [231589]="PALADIN", [231435]="PALADIN", [73629]="PALADIN", [73630]="PALADIN", [270564]="PALADIN", [270562]="PALADIN", [290608]="PALADIN", [363613]="PALADIN", [229377]="PRIEST", [231434]="ROGUE", [231523]="ROGUE", [231524]="ROGUE", [231525]="ROGUE", [231442]="SHAMAN", [5784]="WARLOCK", [23161]="WARLOCK", [232412]="WARLOCK", [238452]="WARLOCK", [238454]="WARLOCK", [229388]="WARRIOR", } local raceLockedMounts = { -- Paladin chargers [73629]="Draenei", [73630]="Draenei", [34769]="BloodElf", [34767]="BloodElf", [69820]="Tauren", [69826]="Tauren", [270564]="Dwarf", [270562]="DarkIronDwarf", [290608]="ZandalariTroll", [363613]="LightforgedDraenei", } local f, m, reason = UnitClass, classLockedMounts, "uncastable-class-lock" for i=1,2 do local function retUncastable() return false, reason, true end local _, me = f("player") for sid, v in pairs(m) do if v ~= me then RW:SetSpellCastableChecker(sid, retUncastable) end end f, m, reason = UnitRace, raceLockedMounts, "uncastable-race-lock" end end) securecall(function() -- Tarecgosa's Visage if not MODERN then return end local SPELL_ID, MOUNT_ID, QUEST_ID = 407555, 1727, 73199 local questOK, mountOK local function addCastEscapes() if InCombatLockdown() then EV.PLAYER_REGEN_ENABLED = addCastEscapes return end local mslot = AB:GetActionSlot("mount", MOUNT_ID) if mslot then local sname = GetSpellInfo(SPELL_ID) RW:SetCastEscapeAction("spell:" .. SPELL_ID, mslot) RW:SetCastEscapeAction(sname, mslot) AB:NotifyObservers("spell") end return "remove" end local function checkVisageConditions() questOK = questOK or C_QuestLog.IsQuestFlaggedCompleted(QUEST_ID) if questOK and not mountOK then local _, _, _, _, _, _, _, _, _, hide, have = C_MountJournal.GetMountInfoByID(MOUNT_ID) mountOK = have and not hide end if questOK and mountOK and addCastEscapes then addCastEscapes() addCastEscapes = nil return true end end RW:SetSpellCastableChecker(SPELL_ID, function(_, castContext) local canMount = questOK and mountOK or checkVisageConditions() local castable = canMount and type(castContext) == "number" and castContext % 4 >= 2 return castable, "visage-check", not castable end) local function cueVisageCheck() return (questOK and mountOK or checkVisageConditions()) and "remove" or nil end EV.QUEST_TURNED_IN = cueVisageCheck EV.PLAYER_ENTERING_WORLD = cueVisageCheck EV.NEW_MOUNT_ADDED = cueVisageCheck end) securecall(function() -- Classic Paladin mounts if not (CF_CATA and playerClass == "PALADIN" and playerRace ~= "Tauren") then -- Only the Sunwalker kodos work with /cast out of the box return end local pendingMounts, noQueue = {}, 1 if playerRace == "BloodElf" then pendingMounts[34767], pendingMounts[34769] = 1, 1 else -- Human, Dwarf, Draenei pendingMounts[13819], pendingMounts[23214] = 1, 1 end local function castableViaEscape(_, castContext) local castable = type(castContext) == "number" and castContext % 4 >= 2 return castable, "pal-catamount", not castable end local function cueMountCheck(ev) if InCombatLockdown() then if noQueue then EV.PLAYER_REGEN_ENABLED, noQueue = cueMountCheck, nil end return end noQueue = 1 local foundNew, fromSpell, getInfo = nil, C_MountJournal.GetMountFromSpell, C_MountJournal.GetMountInfoByID for sid in pairs(pendingMounts) do local mid = fromSpell(sid) if mid and select(11, getInfo(mid)) then RW:SetSpellCastableChecker(sid, castableViaEscape) local mslot = AB:GetActionSlot("mount", mid) if mslot then local sname = GetSpellInfo(sid) RW:SetCastEscapeAction("spell:" .. sid, mslot) RW:SetCastEscapeAction(sname, mslot) end foundNew, pendingMounts[sid] = 1, nil end end if foundNew then AB:NotifyObservers("spell") end return (ev == "PLAYER_REGEN_ENABLED" or next(pendingMounts) == nil) and "remove" end if next(pendingMounts) then EV.SPELLS_CHANGED = cueMountCheck EV.PLAYER_ENTERING_WORLD = cueMountCheck EV.NEW_MOUNT_ADDED = cueMountCheck end end) securecall(function() -- failing ability rank disambiguation if not MODERN then return end local Spell_ForcedID = {126819, 28272, 28271, 161372, 51514, 210873, 211004, 211010, 211015, 783, 126892} local function checkForcedIDCastable(id) return not not FindSpellBookSlotBySpellID(id), "forced-id-cast" end for i=1,#Spell_ForcedID do RW:SetSpellCastableChecker(Spell_ForcedID[i], checkForcedIDCastable) end end) securecall(function() -- failing profession rank disambiguation if not MODERN then return end local activeSet, reserveSet, pendingSync = {}, {} local function procProfessions(n, a, ...) if a then local _, _, _, _, scount, sofs = GetProfessionInfo(a) for i=sofs+1, sofs+scount do local et, eid = GetSpellBookItemInfo(i, "player") if et == "SPELL" and not IsPassiveSpell(eid) then local vid, sn, sr = "spell:" .. eid, GetSpellInfo(eid), GetSpellSubtext(eid) reserveSet[sn], reserveSet[sn .. "()"] = vid, vid if sr and sr ~= "" then reserveSet[sn .. "(" .. sr .. ")"] = vid end end end end if n > 1 then return procProfessions(n-1, ...) end end local function countValues(...) return select("#", ...), ... end local function syncProf(e) if InCombatLockdown() then if not pendingSync then EV.PLAYER_REGEN_ENABLED, pendingSync = syncProf, true end return end pendingSync = false wipe(reserveSet) procProfessions(countValues(GetProfessions())) activeSet, reserveSet = reserveSet, activeSet local changed for k in pairs(reserveSet) do if not activeSet[k] then changed = true RW:SetCastAlias(k, nil) end end for k,v in pairs(activeSet) do if v ~= reserveSet[k] then changed = true RW:SetCastAlias(k, v, true) end end if changed then AB:NotifyObservers("spell") end return e ~= "CHAT_MSG_SKILL" and "remove" end EV.PLAYER_LOGIN, EV.CHAT_MSG_SKILL = syncProf, syncProf end) securecall(function() -- missing usability conditions for Garrison/Dalaran Hearthstone toys if not MODERN then return end local watchedQuests = {} function EV:QUEST_TURNED_IN(qid) local tid = watchedQuests[qid] if tid and PlayerHasToy(tid) then watchedQuests[qid] = nil AB:NotifyObservers("toy") end end local function collectedAndQuestCompleted(id, q1, q2) watchedQuests[q1], watchedQuests[q2 or q1] = id, id return id, function(_id) if PlayerHasToy(id) then local qf = C_QuestLog.IsQuestFlaggedCompleted if qf(q1) or q2 and qf(q2) then AB:SetPlayerHasToyOverride(id, nil) return true end end end end AB:SetPlayerHasToyOverride(collectedAndQuestCompleted(110560, 34378, 34586)) -- Garrison Hearthstone AB:SetPlayerHasToyOverride(collectedAndQuestCompleted(140192, 44184, 44663)) -- Legion Dalaran Hearthstone end) securecall(function() -- /ping's option parsing is silly if not MODERN then return end local f, init = CreateFrame("Frame", nil, nil, "SecureHandlerBaseTemplate"), [[-- AB_PingQuirk_Init PING_COMMAND, TOKENS = %s, newtable() TOKENS.assist, TOKENS.attack, TOKENS.onmyway, TOKENS.warning = %s, %s, %s, %s ]] f:Execute(init:format(safequote(SLASH_PING1 .. " "), safequote(PING_TYPE_ASSIST), safequote(PING_TYPE_ATTACK), safequote(PING_TYPE_ON_MY_WAY), safequote(PING_TYPE_WARNING))) f:SetAttribute("RunSlashCmd", [[-- AB_PingQuirk_Run local cmd, v, target, s = ... if v then target = target and target ~= "cursor" and "[@" .. target .. "] " or "" return PING_COMMAND .. target .. (TOKENS[v and v:lower()] or v) end ]]) RW:RegisterCommand(SLASH_PING1, true, false, f) end) securecall(function() -- /equipset {not a set name} errors if not MODERN then -- Classic lacks the SABT action return end local function uniqueName(prefix) local bni, bn = 1 repeat bn, bni = prefix .. bni, bni + 1 until _G[bn] == nil and GetClickFrame(bn) == nil return bn end local f = CreateFrame("Button", uniqueName("ABEquipSetQuirk!"), nil, "SecureActionButtonTemplate") f:SetAttribute("pressAndHoldAction", 1) f:SetAttribute("type", "equipmentset") f:SetAttribute("typerelease", "equipmentset") f:SetAttribute("DoEquipCommand", SLASH_CLICK1 .. " " .. f:GetName()) f:SetAttribute("RunSlashCmd", [[-- AB_EquipSetQuirk_Run local cmd, v = ... if v and v ~= "" then self:SetAttribute("equipmentset", v) return self:GetAttribute("DoEquipCommand"), "notified-click", v end ]]) f:SetAttribute("RunSlashCmd-PreClick", [[-- AB_EquipSetQuirk_PreRun local cmd, v = ... self:SetAttribute("equipmentset", v) ]]) RW:RegisterCommand(SLASH_EQUIP_SET1, true, false, f) end) securecall(function() -- ClassTalentHelper commands are in SlashCmdList instead of SecureCmdList if not MODERN then return end RW:ImportSlashCmd("TALENT_LOADOUT_BY_NAME", true, false) RW:ImportSlashCmd("TALENT_LOADOUT_BY_INDEX", true, false) RW:ImportSlashCmd("TALENT_SPEC_BY_NAME", true, false) RW:ImportSlashCmd("TALENT_SPEC_BY_INDEX", true, false) end) securecall(function() -- SoD rune ability castable checks if not CI_ERA then return end local function checkRuneSpell(sid) local n1, n2, sid2, _ = IsPlayerSpell(sid) and GetSpellInfo(sid) if n1 then n2, _, _, _, _, _, sid2 = GetSpellInfo(n1) end return n2 and n1 ~= n2 and not IsPassiveSpell(sid2) or false, "rune-ability-spell" end for sid in ("399967 417346 399954 417347 415450 417345 399966 417348 415449"):gmatch("%d+") do RW:SetSpellCastableChecker(sid+0, checkRuneSpell) end end) securecall(function() -- 4.4.0 misplaces secure commands if not CF_CATA then return end RW:ImportSlashCmd("WORLD_MARKER", true, false) RW:ImportSlashCmd("CLEAR_WORLD_MARKER", true, false) RW:ImportSlashCmd("EQUIP_SET", true, false) end) securecall(function() -- Draenic Hologem usability limitation if MODERN and playerRace ~= "Draenei" and playerRace ~= "LightforgedDraenei" then AB:SetPlayerHasToyOverride(210455, false) end end) local MAYBE_FLYABLE, FLIGHT_BLOCKER = true securecall(function() -- FLIGHT_BLOCKER init if not MODERN then return end local f = CreateFrame("Frame", nil, nil, "SecureHandlerAttributeTemplate") f:SetFrameRef("KR", KR:seclib()) f:Execute('KR = self:GetFrameRef("KR")') f:SetAttribute("_onattributechanged", [[-- AB_BlockedFlyable_Driver local sk, v = name:match("^state%-(.+)"), value if v == 1 or v == 0 then KR:RunAttribute("UpdateStateConditional", "blockedflyable", v == 1 and sk or "", v == 0 and sk or "") end ]]) FLIGHT_BLOCKER = f end) securecall(function() -- MAYBE_FLYABLE: [anyflyable] mirror if not (MODERN and (playerClass == "DRUID" or playerRace == "Dracthyr")) then return end local f = CreateFrame("Frame", nil, nil, "SecureFrameTemplate") f:SetScript("OnAttributeChanged", function(_, a, v) if a == "state-anyflyable" then MAYBE_FLYABLE = v == 1 end end) KR:RegisterStateDriver(f, "anyflyable", "[anyflyable] 1;") end) securecall(function() -- Siren Isle flight restrictions if not MODERN then return end local SIREN_ISLE_STORM_SID, inStorm, noPendingUpdate, onSirenIsle = 458069, 0, 1 local checkSIStormTimer local function checkSIStorm(e) if e == "PLAYER_ENTERING_WORLD" then local _,_,_,_, _,_,_,imid = GetInstanceInfo() onSirenIsle = imid == 2127 elseif e == 'RAID_BOSS_EMOTE' then EV.After(0.25, checkSIStormTimer) end local nv = onSirenIsle and C_UnitAuras.GetPlayerAuraBySpellID(SIREN_ISLE_STORM_SID) and 1 or 0 if nv == inStorm then elseif not InCombatLockdown() then inStorm = nv FLIGHT_BLOCKER:SetAttribute("state-sirenislestorm", nv) elseif noPendingUpdate then EV.PLAYER_REGEN_ENABLED, noPendingUpdate = checkSIStorm end if e == "PLAYER_REGEN_ENABLED" then noPendingUpdate = 1 return "remove" end end function checkSIStormTimer() checkSIStorm('TIMER') end local SIREN_ISLE_FLIGHT_QID, disarm = 85657 local function disarmSIFBlock() if not InCombatLockdown() and disarm ~= 2 then KR:RegisterStateDriver(FLIGHT_BLOCKER, "sirenisle", nil) FLIGHT_BLOCKER:SetAttribute("state-sirenisle", 0) disarm = 2 EV.PLAYER_ENTERING_WORLD, EV.RAID_BOSS_EMOTE = checkSIStorm, checkSIStorm checkSIStorm("PLAYER_ENTERING_WORLD") elseif not disarm then disarm, EV.PLAYER_REGEN_ENABLED = 1, disarmSIFBlock end return "remove" end local function checkSIFBlock(ev, qid) if ev == "QUEST_TURNED_IN" and qid == SIREN_ISLE_FLIGHT_QID or ev == "PLAYER_ENTERING_WORLD" and C_QuestLog.IsQuestFlaggedCompletedOnAccount(SIREN_ISLE_FLIGHT_QID) then return disarmSIFBlock() end return disarm and "remove" end EV.QUEST_TURNED_IN = checkSIFBlock EV.PLAYER_ENTERING_WORLD = checkSIFBlock KR:RegisterStateDriver(FLIGHT_BLOCKER, "sirenisle", "[in:siren isle] 1;0") end) securecall(function() -- Oribos/Tazavesh flight restriction if not MODERN then return end local unflyableMaps = {[1670]=1, [1671]=1, [1672]=1, [1673]=1, [2016]=2} local inSL, state, goal, pending = false, false, false local function syncState() if state ~= goal then FLIGHT_BLOCKER:SetAttribute("state-oribos", goal and 1 or 0) state = goal end pending = nil return "remove" end local function onZoneChange() goal = inSL and unflyableMaps[C_Map.GetBestMapForUnit("player")] ~= nil if goal == state then elseif not InCombatLockdown() then syncState() elseif not pending then EV.PLAYER_REGEN_ENABLED, pending = syncState, true end end function EV.PLAYER_ENTERING_WORLD() local f,_,_,_, _,_,_,imid = GetInstanceInfo() if (imid == 2222) ~= inSL then inSL, f = not inSL, EV[inSL and "UnregisterEvent" or "RegisterEvent"] f("ZONE_CHANGED_NEW_AREA", onZoneChange) onZoneChange() end end end) securecall(function() -- Darkmoon Fairegrounds flight restriction if not MODERN then return end KR:RegisterStateDriver(FLIGHT_BLOCKER, "dmf", "[in:darkmoon faire] 1; 0") end) securecall(function() -- TWW dungeon/delve flight restriction if not MODERN then return end KR:RegisterStateDriver(FLIGHT_BLOCKER, "dung", "[in:dungeon,noin:nokhud/dawnbreaker][in:delve] 1; 0") end) securecall(function() -- Travel form outcome feedback if not (MODERN and playerClass == "DRUID") then return end local CAN_FLY, CAN_SWIM = false, false AB:SetSpellIconOverride(783, function() if CAN_SWIM and IsSwimming() then return 132112 end return CAN_FLY and MAYBE_FLYABLE and not InCombatLockdown() and 132128 or 132144, not IsIndoors() end) local function syncLevel(ev) local lv = UnitLevel("player") or 0 CAN_SWIM = CAN_SWIM or lv >= 17 CAN_FLY = CAN_FLY or lv >= 30 return (ev == "PLAYER_LOGIN" or CAN_FLY and CAN_SWIM) and "remove" end EV.PLAYER_LEVEL_UP = syncLevel EV.PLAYER_LOGIN = syncLevel end) securecall(function() -- Soar usability feedback if MODERN and playerRace == "Dracthyr" then AB:SetSpellIconOverride(369536, function() if not MAYBE_FLYABLE then return nil, false end end) end end) securecall(function() -- Siren Isle Research Journal requires pages to use if MODERN then AB:SetItemCountOverride(227405, function() local noteCount = C_Item.GetItemCount(227406) return noteCount, noteCount > 0 end) end end) securecall(function() -- Modern: some mounts aren't castable by spell IDs if not MODERN then return end local BROKEN_SPELL_IDS, lockdown = {366962, 471562} local function pushMountCasts(ev) if InCombatLockdown() then lockdown = 1 elseif ev ~= "PLAYER_REGEN_ENABLED" or lockdown then local gsn, ei = C_Spell.GetSpellName, #BROKEN_SPELL_IDS for i=ei, 1, -1 do local sn = gsn(BROKEN_SPELL_IDS[i]) if sn and gsn(sn) then RW:SetCastAlias("spell:" .. BROKEN_SPELL_IDS[i], sn) BROKEN_SPELL_IDS[i], ei, BROKEN_SPELL_IDS[ei] = i ~= ei and BROKEN_SPELL_IDS[ei] or nil, ei - 1 end end end return (#BROKEN_SPELL_IDS == 0 or ev == "PLAYER_LOGIN") and "remove" end EV.NEW_MOUNT_ADDED, EV.PLAYER_REGEN_ENABLED, EV.PLAYER_LOGIN = pushMountCasts, pushMountCasts, pushMountCasts end) securecall(function() -- Modern: G-99 Breakneck is a fake mount if not MODERN then return end local G99_SPELL_ID, G99_QUEST_ID, questOK = 460013, 84352 local inUndermine, wf = false, CreateFrame("Frame", nil, nil, "SecureFrameTemplate") local function pushG99SpellCastID() RW:SetCastAlias("spell:" .. G99_SPELL_ID, C_Spell.GetSpellName(G99_SPELL_ID)) return "remove" end local function hasUnlockedG99() if not questOK and C_QuestLog.IsQuestFlaggedCompletedOnAccount(G99_QUEST_ID) then questOK = true if InCombatLockdown() then EV.PLAYER_REGEN_ENABLED = pushG99SpellCastID else pushG99SpellCastID() end end return questOK end local function updateGroundMount() IM:SetMountPreference(inUndermine and G99_SPELL_ID or false) AB:NotifyObservers("imptext") end wf:SetScript("OnAttributeChanged", function(_, a, v) if a ~= "state-um" or (v == 1) == inUndermine then return end inUndermine = v == 1 return hasUnlockedG99() and updateGroundMount() end) wf:Hide() KR:RegisterStateDriver(wf, 'um', '[in:undermine] 1; 0') RW:SetSpellCastableChecker(G99_SPELL_ID, function() if hasUnlockedG99() and inUndermine then return true, "g99-quirk" end return false, "g99-quirk" end) AB:AugmentCategory(AB.L"Mounts", function(_, add) if hasUnlockedG99() then add("spell", G99_SPELL_ID) end end) function EV:QUEST_TURNED_IN(qid) if qid == G99_QUEST_ID then questOK = true updateGroundMount() end return questOK and "remove" end function EV:LOADING_SCREEN_ENABLED() -- [11.1/2504] quest completion cache is flushed on PLW; may not repop by PEW. return hasUnlockedG99() and "remove" end end) securecall(function() -- Classic Mists: [spec:1] is stuck if MODERN or COMPAT < 5e4 then return end local function syncSpec() local spec = C_SpecializationInfo.GetSpecialization() local specID, name = C_SpecializationInfo.GetSpecializationInfo(spec or 0) local valid = specID and specID ~= 0 and name and name ~= "" local ex = (C_SpecializationInfo.GetActiveSpecGroup() or 1) == 1 and "/p" or "/s" local v = (spec or 0) .. (valid and "/" .. specID .. "/" .. name .. ex or ex) KR:SetStateConditionalValue("spec", v) end function EV:PLAYER_SPECIALIZATION_CHANGED(u) if u == "player" then syncSpec() end end EV.PLAYER_LOGIN = syncSpec end)