mirror of
https://github.com/fuffc/WeakestAuras.git
synced 2026-11-10 20:07:35 +00:00
ClassicAPI 1.18 is now the only requirement Requirements - Needs ClassicAPI 1.18.0 or newer. SuperWoW is no longer used, and Nampower is optional. - On an older ClassicAPI the addon stays off, and a popup links the download. - Custom triggers on SuperWoW or Nampower events only fire while that mod is installed. Triggers - New Combat Log trigger, with lists of spell IDs, spell names and NPC IDs. WeakAuras2 auras that use one now import. - Custom triggers can listen to the combat log with CLEU or CLEU:SUBEVENT. - Crowd Control matches a specific type (stun, fear, root, silence, disarm and more) or a school lockout, with the effect's real timer. - Saved Crowd Control auras: Root, Silence and Disarm filters carry over; CC and Snare become "any type", and snares are no longer detected. - Cast bars follow pushback and channel updates. Displays - Icon cooldown swipes are drawn by the client's own cooldown frame. - Sound actions play on a chosen channel, and a new Stop sound type ends a looping sound. Fixes - Cooldown auras no longer sweep on every wand shot. - The Global Cooldown trigger ignores wand and Auto Shot shots.
706 lines
34 KiB
Lua
706 lines
34 KiB
Lua
-- WeakestAuras -- action dispatch and compiled custom action functions.
|
|
-- Upstream section refs (§7, §13, §16) point at design/architecture/weakauras2-reference.md
|
|
|
|
if WeakestAuras.disabled then return end
|
|
|
|
local WA = WeakestAuras
|
|
|
|
WA.customActionsFunctions = {}
|
|
|
|
WA.send_chat_message_types = {
|
|
WHISPER = "Whisper", SAY = "Say", EMOTE = "Emote", YELL = "Yell",
|
|
PARTY = "Party", GUILD = "Guild", OFFICER = "Officer", RAID = "Raid",
|
|
SMARTRAID = "BG > Raid > Party > Say", RAID_WARNING = "Raid Warning",
|
|
COMBAT = "Blizzard Combat Text", PRINT = "Chat Frame", ERROR = "Error Frame",
|
|
TTS = "Text-to-speech",
|
|
}
|
|
|
|
WA.sound_types = {
|
|
[" custom"] = "Custom Sound File",
|
|
[" KitID"] = "Sound by Kit ID",
|
|
["Sound\\interface\\RaidWarning.wav"] = "Raid Warning",
|
|
["Sound\\interface\\levelup2.wav"] = "Ready Check",
|
|
["Sound\\interface\\iQuestComplete.wav"] = "Quest Complete",
|
|
["Sound\\interface\\iTellMessage.wav"] = "Whisper",
|
|
["Sound\\Spells\\LevelUp.wav"] = "Level Up",
|
|
["Sound\\Spells\\ReputationLevelUp.wav"] = "Reputation Up",
|
|
["Sound\\Spells\\PVPThroughQueue.wav"] = "Battleground Queue",
|
|
["Sound\\Spells\\ShaysBell.wav"] = "Shay's Bell",
|
|
["Sound\\Doodad\\BellTollNightElf.wav"] = "Bell Toll (Night Elf)",
|
|
["Sound\\Doodad\\BellTollAlliance.wav"] = "Bell Toll (Alliance)",
|
|
["Sound\\Doodad\\BellTollHorde.wav"] = "Bell Toll (Horde)",
|
|
["Sound\\Doodad\\BellTollTribal.wav"] = "Bell Toll (Tribal)",
|
|
["Sound\\Doodad\\DwarfHorn.wav"] = "Dwarf Horn",
|
|
["Sound\\Doodad\\G_GongTroll01.wav"] = "Troll Gong",
|
|
["Sound\\Doodad\\LightHouseFogHorn.wav"] = "Fog Horn",
|
|
["Sound\\Doodad\\HornGoober.wav"] = "Horn",
|
|
["Sound\\interface\\AuctionWindowOpen.wav"] = "Auction Window",
|
|
["Sound\\Interface\\igQuestFailed.wav"] = "Quest Failed",
|
|
}
|
|
|
|
-- The bundled set, under sounds\ and sounds\powerauras\. Paths and labels are
|
|
-- upstream's LibSharedMedia registrations (WA2 Types.lua) carried over verbatim,
|
|
-- because WA2Import matches an incoming path against these to land it on our
|
|
-- copy -- a renamed file or a renamed label would break that lookup silently.
|
|
-- Upstream's "Cartoon Hop" is not here: it registers a name whose file WA2 does
|
|
-- not actually ship, which is exactly the dead reference this table avoids.
|
|
local BUNDLED_SOUNDS = {
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\AcousticGuitar.ogg"] = "Acoustic Guitar",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\aggro.ogg"] = "Aggro",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\AirHorn.ogg"] = "Air Horn",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Applause.ogg"] = "Applause",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\Arrow_Swoosh.ogg"] = "Arrow Swoosh",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\bam.ogg"] = "Bam",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\BananaPeelSlip.ogg"] = "Banana Peel Slip",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\BatmanPunch.ogg"] = "Batman Punch",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\bigkiss.ogg"] = "Big Kiss",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\BikeHorn.ogg"] = "Bike Horn",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\BITE.ogg"] = "Bite",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Blast.ogg"] = "Blast",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Bleat.ogg"] = "Bleat",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\BoxingArenaSound.ogg"] = "Boxing Arena Gong",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Brass.mp3"] = "Brass",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\burp4.ogg"] = "Burp",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\CartoonVoiceBaritone.ogg"] = "Cartoon Voice Baritone",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\CartoonWalking.ogg"] = "Cartoon Walking",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\cat2.ogg"] = "Cat",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\CatMeow2.ogg"] = "Cat Meow",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\chant2.ogg"] = "Chant Major 2nd",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\chant4.ogg"] = "Chant Minor 3rd",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\ChickenAlarm.ogg"] = "Chicken Alarm",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\chimes.ogg"] = "Chimes",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\cookie.ogg"] = "Cookie Monster",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\CowMooing.ogg"] = "Cow Mooing",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\DoubleWhoosh.ogg"] = "Double Whoosh",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Drums.ogg"] = "Drums",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\ESPARK1.ogg"] = "Electrical Spark",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\ErrorBeep.ogg"] = "Error Beep",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\Fireball.ogg"] = "Fireball",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\Gasp.ogg"] = "Gasp",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Glass.mp3"] = "Glass",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\GoatBleating.ogg"] = "Goat Bleeting",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\shot.ogg"] = "Gunshot",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\heartbeat.ogg"] = "Heartbeat",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\HeartbeatSingle.ogg"] = "Heartbeat Single",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\hic3.ogg"] = "Hiccup",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\huh_1.ogg"] = "Huh?",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\hurricane.ogg"] = "Hurricane",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\hyena.ogg"] = "Hyena",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\kaching.ogg"] = "Kaching",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\KittenMeow.ogg"] = "Kitten Meow",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\moan.ogg"] = "Moan",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\OhNo.ogg"] = "Oh No",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\panther1.ogg"] = "Panther",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\phone.ogg"] = "Phone",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\bear_polar.ogg"] = "Polar Bear",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\PUNCH.ogg"] = "Punch",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\rainroof.ogg"] = "Rain",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\RingingPhone.ogg"] = "Ringing Phone",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\RoaringLion.ogg"] = "Roaring Lion",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\RobotBlip.ogg"] = "Robot Blip",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\rocket.ogg"] = "Rocket",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\RoosterChickenCalls.ogg"] = "Rooster Chicken Call",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\SharpPunch.ogg"] = "Sharp Punch",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\SheepBleat.ogg"] = "Sheep Blerping",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\shipswhistle.ogg"] = "Ship's Whistle",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Shotgun.ogg"] = "Shotgun",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\snakeatt.ogg"] = "Snake Attack",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\sneeze.ogg"] = "Sneeze",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\sonar.ogg"] = "Sonar",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\splash.ogg"] = "Splash",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\Squeakypig.ogg"] = "Squeaky Toy",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\SqueakyToyShort.ogg"] = "Squeaky Toy Short",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\SquishFart.ogg"] = "Squish Fart",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\swordecho.ogg"] = "Sword Ring",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\SynthChord.ogg"] = "Synth Chord",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\TadaFanfare.ogg"] = "Tada Fanfare",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\TempleBellHuge.ogg"] = "Temple Bell",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\throwknife.ogg"] = "Throwing Knife",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\thunder.ogg"] = "Thunder",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Torch.ogg"] = "Torch",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Adds.ogg"] = "Voice: Adds",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Boss.ogg"] = "Voice: Boss",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Circle.ogg"] = "Voice: Circle",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Cross.ogg"] = "Voice: Cross",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Diamond.ogg"] = "Voice: Diamond",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\DontRelease.ogg"] = "Voice: Don't Release",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Empowered.ogg"] = "Voice: Empowered",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Focus.ogg"] = "Voice: Focus",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Idiot.ogg"] = "Voice: Idiot",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Left.ogg"] = "Voice: Left",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Moon.ogg"] = "Voice: Moon",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Next.ogg"] = "Voice: Next",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Portal.ogg"] = "Voice: Portal",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Protected.ogg"] = "Voice: Protected",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Release.ogg"] = "Voice: Release",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Right.ogg"] = "Voice: Right",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\RunAway.ogg"] = "Voice: Run Away",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Skull.ogg"] = "Voice: Skull",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Spread.ogg"] = "Voice: Spread",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Square.ogg"] = "Voice: Square",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Stack.ogg"] = "Voice: Stack",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Star.ogg"] = "Voice: Star",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Switch.ogg"] = "Voice: Switch",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Taunt.ogg"] = "Voice: Taunt",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Triangle.ogg"] = "Voice: Triangle",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\WarningSiren.ogg"] = "Warning Siren",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\WaterDrop.ogg"] = "Water Drop",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\wlaugh.ogg"] = "Wicked Female Laugh",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\wickedmalelaugh1.ogg"] = "Wicked Male Laugh",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\wilhelm.ogg"] = "Wilhelm Scream",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\wolf5.ogg"] = "Wolf Howl",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\Xylophone.ogg"] = "Xylophone",
|
|
["Interface\\AddOns\\WeakestAuras\\sounds\\powerauras\\yeehaw.ogg"] = "Yeehaw",
|
|
}
|
|
WA.bundled_sound_types = BUNDLED_SOUNDS
|
|
for path, label in pairs(BUNDLED_SOUNDS) do WA.sound_types[path] = label end
|
|
|
|
local MESSAGE_TYPES = { "WHISPER", "SAY", "EMOTE", "YELL", "PARTY", "GUILD", "OFFICER", "RAID", "SMARTRAID", "RAID_WARNING", "COMBAT", "PRINT", "ERROR", "TTS" }
|
|
local MESSAGE_LABELS = WA.send_chat_message_types
|
|
|
|
local ACTION_SPECS = {
|
|
{ key = "init", block = "init", field = "custom", enabled = "do_custom", body = true },
|
|
{ key = "load", block = "init", field = "customOnLoad", enabled = "do_custom_load", body = true },
|
|
{ key = "unload", block = "init", field = "customOnUnload", enabled = "do_custom_unload", body = true },
|
|
{ key = "start", block = "start", field = "custom", enabled = "do_custom", body = true },
|
|
{ key = "finish", block = "finish", field = "custom", enabled = "do_custom", body = true },
|
|
{ key = "start_message", block = "start", field = "message_custom", enabled = "do_message" },
|
|
{ key = "finish_message", block = "finish", field = "message_custom", enabled = "do_message" },
|
|
}
|
|
|
|
local function sourceFor(data, spec)
|
|
local actions = data.actions and data.actions[spec.block]
|
|
if not actions or not actions[spec.enabled] then return nil end
|
|
local source = actions[spec.field]
|
|
if type(source) ~= "string" or source == "" then return nil end
|
|
return source
|
|
end
|
|
|
|
local function hasSound(options)
|
|
if options.sound_type == "Stop" then return true end
|
|
if options.sound == " custom" then return options.sound_path and options.sound_path ~= "" end
|
|
if options.sound == " KitID" then return options.sound_kit_id and options.sound_kit_id ~= "" end
|
|
return options.sound and options.sound ~= ""
|
|
end
|
|
|
|
-- A kit id plays through C_Sound, which takes only numbers; anything else is a
|
|
-- sound entry name, which only PlaySound takes, and which yields no handle.
|
|
local function playKit(kit, channel)
|
|
local id = tonumber(kit)
|
|
if id then return C_Sound.PlaySound(id, channel) end
|
|
PlaySound(kit)
|
|
end
|
|
|
|
local function playSound(region, options)
|
|
region.soundHandle = nil
|
|
if options.sound_type == "Stop" then return end
|
|
local channel = options.sound_channel or "Master"
|
|
local ok, _, handle
|
|
if options.sound == " KitID" then
|
|
ok, _, handle = pcall(playKit, options.sound_kit_id, channel)
|
|
else
|
|
local path = options.sound
|
|
if path == " custom" then path = options.sound_path end
|
|
ok, _, handle = pcall(PlaySoundFile, path, channel)
|
|
end
|
|
if ok then region.soundHandle = handle end
|
|
end
|
|
|
|
function WA.SoundStop(region, fadeoutTime)
|
|
if region.soundHandle then StopSound(region.soundHandle, fadeoutTime) end
|
|
region.soundHandle = nil
|
|
end
|
|
|
|
function WA.SoundRepeatStop(region)
|
|
if region.soundRepeatTicker then region.soundRepeatTicker:Cancel() end
|
|
region.soundRepeatTicker = nil
|
|
end
|
|
|
|
function WA.SoundPlay(region, options)
|
|
if not options or WA.optionsOpen or WA.forced[region.id] or WA.SquelchingActions() then return end
|
|
if not hasSound(options) then return end
|
|
WA.SoundStop(region)
|
|
WA.SoundRepeatStop(region)
|
|
playSound(region, options)
|
|
local loop = options.do_loop or options.sound_type == "Loop"
|
|
if loop and tonumber(options.sound_repeat) and options.sound_repeat > 0 then
|
|
region.soundRepeatTicker = C_Timer.NewTicker(options.sound_repeat, function()
|
|
if not region.toShow then WA.SoundRepeatStop(region); return end
|
|
playSound(region, options)
|
|
end)
|
|
end
|
|
end
|
|
|
|
local lastSoundPreviewTime
|
|
function WA.PreviewSound(value, isKit)
|
|
if lastSoundPreviewTime == GetTime() then return end
|
|
lastSoundPreviewTime = GetTime()
|
|
if isKit then
|
|
pcall(playKit, value, "Master")
|
|
elseif value and value ~= "" then
|
|
pcall(PlaySoundFile, value, "Master")
|
|
end
|
|
end
|
|
|
|
function WA.SendChat(region, options)
|
|
if not options or WA.optionsOpen or WA.SquelchingActions() then return end
|
|
WA.HandleChatAction(options.message_type, options.message, options.message_dest,
|
|
options.message_dest_isunit, options.r, options.g, options.b, region, nil, nil, nil)
|
|
end
|
|
|
|
local function startGlowOn(region, frame, options)
|
|
region.activeExternalGlows = region.activeExternalGlows or {}
|
|
local glow = region.activeExternalGlows[frame]
|
|
if not glow then
|
|
glow = WA.CreateExternalGlow and WA.CreateExternalGlow(frame)
|
|
region.activeExternalGlows[frame] = glow
|
|
end
|
|
if glow and glow.StartGlow then glow:StartGlow(options) end
|
|
end
|
|
|
|
local function stopGlowOn(region, frame)
|
|
if not frame or not region.activeExternalGlows then return end
|
|
local glow = region.activeExternalGlows[frame]
|
|
if glow and glow.StopGlow then glow:StopGlow() end
|
|
region.activeExternalGlows[frame] = nil
|
|
end
|
|
|
|
-- A glow the user aimed at a unit rather than at a named frame (§16). Both
|
|
-- targets are recycled -- a nameplate to whatever creature the engine next
|
|
-- needs one for, a unit frame to whatever the token points at -- so a lit glow
|
|
-- has to follow its unit or go out. Left alone it becomes a glow on a stranger,
|
|
-- which is worse than no feature. Keyed by region because one region glows one
|
|
-- unit at a time; the entry outlives a missing frame so the glow returns with
|
|
-- the plate.
|
|
local unitGlowMonitor = {}
|
|
local unitGlowWatching
|
|
|
|
local function glowUnitFrame(frameType, unit)
|
|
if not unit then return nil end
|
|
if frameType == "NAMEPLATE" then
|
|
return WA.GetUnitNameplate and WA.GetUnitNameplate(unit) or nil
|
|
end
|
|
return WA.GetUnitFrame and WA.GetUnitFrame(unit) or nil
|
|
end
|
|
|
|
-- Re-resolves every monitored glow, on every wake the unit watcher has: the
|
|
-- token moved, a plate was recycled, or a frame that was missing turned up.
|
|
local function refreshUnitGlows()
|
|
for region, entry in pairs(unitGlowMonitor) do
|
|
local frame = glowUnitFrame(entry.frameType, region.state and region.state.unit)
|
|
if frame ~= entry.frame then
|
|
stopGlowOn(region, entry.frame)
|
|
entry.frame = frame
|
|
if frame then startGlowOn(region, frame, entry.options) end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function ensureUnitGlowWatch()
|
|
if unitGlowWatching or not WA.RegisterUnitWatchCallback then return end
|
|
unitGlowWatching = true
|
|
WA.RegisterUnitWatchCallback(refreshUnitGlows)
|
|
end
|
|
|
|
-- A named glow target, which is either a global frame or another display -- the
|
|
-- same two spellings the SELECTFRAME anchor takes, because upstream's frame
|
|
-- selector writes one field for both and an imported glow can carry either.
|
|
local function namedGlowFrame(name)
|
|
if type(name) ~= "string" or name == "" then return nil end
|
|
local _, _, id = string.find(name, "^" .. WA.ANCHOR_AURA_PREFIX .. "(.+)$")
|
|
if id then return WA.PeekRegion(id, "") end
|
|
return getglobal(name)
|
|
end
|
|
|
|
function WA.GlowExternal(region, options)
|
|
if not region or not options or not options.glow_frame_type then return end
|
|
local frameType = options.glow_frame_type
|
|
local unitAimed = frameType == "UNITFRAME" or frameType == "NAMEPLATE"
|
|
local frame = region
|
|
if frameType == "FRAMESELECTOR" and options.glow_frame then
|
|
frame = namedGlowFrame(options.glow_frame)
|
|
elseif unitAimed then
|
|
frame = glowUnitFrame(frameType, region.state and region.state.unit)
|
|
end
|
|
if options.glow_action == "hide" then
|
|
local entry = unitAimed and unitGlowMonitor[region]
|
|
if entry then
|
|
-- The lit frame, not the one the token resolves to now: the unit may
|
|
-- have moved since, and the glow is on the frame it was started on.
|
|
frame = entry.frame
|
|
unitGlowMonitor[region] = nil
|
|
end
|
|
stopGlowOn(region, frame)
|
|
return
|
|
end
|
|
if unitAimed then
|
|
unitGlowMonitor[region] = { frameType = frameType, options = options, frame = frame }
|
|
ensureUnitGlowWatch()
|
|
end
|
|
if not frame then return end
|
|
startGlowOn(region, frame, options)
|
|
end
|
|
|
|
function WA.StopExternalGlows(region)
|
|
unitGlowMonitor[region] = nil
|
|
for frame, glow in pairs(region.activeExternalGlows or {}) do
|
|
if glow.StopGlow then glow:StopGlow() end
|
|
region.activeExternalGlows[frame] = nil
|
|
end
|
|
end
|
|
|
|
function WA.CompileActions(data)
|
|
if not data or WA.IsGroup(data) then return end
|
|
local id = data.id
|
|
local cache = WA.customActionsFunctions[id] or {}
|
|
local sources = cache.sources or {}
|
|
cache.sources = sources
|
|
WA.customActionsFunctions[id] = cache
|
|
for i = 1, #ACTION_SPECS do
|
|
local spec = ACTION_SPECS[i]
|
|
local source = sourceFor(data, spec)
|
|
if sources[spec.key] ~= source then
|
|
sources[spec.key] = source
|
|
cache[spec.key] = nil
|
|
WA.ClearAuraEnv(data.id)
|
|
if source then
|
|
cache[spec.key] = WA.LoadFunction(source, id .. ": " .. spec.key, spec.body and true or false)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function WA.ClearActionFunctions(id)
|
|
WA.customActionsFunctions[id] = nil
|
|
end
|
|
|
|
function WA.RenameActionFunctions(oldId, newId)
|
|
WA.customActionsFunctions[newId] = WA.customActionsFunctions[oldId]
|
|
WA.customActionsFunctions[oldId] = nil
|
|
end
|
|
|
|
function WA.RunActionCode(data, key, region)
|
|
local cache = WA.customActionsFunctions[data.id]
|
|
local fn = cache and cache[key]
|
|
if not fn then return end
|
|
if region then
|
|
return WA.RunAuraFunc(region, data.id .. ": " .. key, fn)
|
|
end
|
|
return WA.RunAuraFunc(data.id, data.id .. ": " .. key, fn)
|
|
end
|
|
|
|
-- Whether this aura will make a noise, and how: four keyed warnings recomputed
|
|
-- from `data` on every WA.Add, since that is when any of the four fields can have
|
|
-- changed. Neither severity is a fault -- they exist so a user hunting a stray
|
|
-- sound can find which aura is making it, and they pair with the login squelch
|
|
-- (WA.SquelchingActions): the strip says which aura, the squelch buys time to fix
|
|
-- it. Both fold under a `warning` or an `error`, which is what their rank means.
|
|
--
|
|
-- A condition change is matched on its property *name*, as upstream does, not on
|
|
-- the resolved type: `sound` and `chat` are injected once per region type by
|
|
-- proto.AddProperties and are never sub-region properties, so nothing prefixes
|
|
-- them.
|
|
function WA.UpdateSoundWarnings(data)
|
|
if not data or not data.uid or not WA.UpdateWarning then return end
|
|
local uid = data.uid
|
|
local actions = data.actions or {}
|
|
local start, finish = actions.start or {}, actions.finish or {}
|
|
|
|
local soundCondition, ttsCondition
|
|
local conditions = data.conditions or {}
|
|
for i = 1, #conditions do
|
|
local changes = conditions[i].changes or {}
|
|
for c = 1, #changes do
|
|
local change = changes[c]
|
|
if change.property == "sound" then
|
|
soundCondition = true
|
|
elseif change.property == "chat" and type(change.value) == "table"
|
|
and change.value.message_type == "TTS" then
|
|
ttsCondition = true
|
|
end
|
|
end
|
|
end
|
|
|
|
WA.UpdateWarning(uid, "sound_action",
|
|
(start.do_sound or finish.do_sound) and "sound" or nil,
|
|
"This aura plays a sound via an action.")
|
|
WA.UpdateWarning(uid, "sound_condition", soundCondition and "sound" or nil,
|
|
"This aura plays a sound via a condition.")
|
|
WA.UpdateWarning(uid, "tts_action",
|
|
((start.do_message and start.message_type == "TTS")
|
|
or (finish.do_message and finish.message_type == "TTS")) and "tts" or nil,
|
|
"This aura speaks via an action.")
|
|
WA.UpdateWarning(uid, "tts_condition", ttsCondition and "tts" or nil,
|
|
"This aura speaks via a condition.")
|
|
end
|
|
|
|
function WA.SquelchingActions()
|
|
local untilTime = WA.actionSquelchUntil
|
|
return untilTime and GetTime() < untilTime or false
|
|
end
|
|
|
|
local actionEventFrame = CreateFrame("Frame")
|
|
actionEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
actionEventFrame:SetScript("OnEvent", function()
|
|
local seconds = tonumber(WA.Options().login_squelch_time)
|
|
if seconds == nil then seconds = 10 end
|
|
WA.actionSquelchUntil = GetTime() + seconds
|
|
end)
|
|
|
|
local function actionFormatGet(action, key, default)
|
|
local value = action["message_format_" .. key]
|
|
if value == nil then return default end
|
|
return value
|
|
end
|
|
|
|
local function actionFormatSet(data, action, key, value)
|
|
action["message_format_" .. key] = value
|
|
WA.Add(data)
|
|
end
|
|
|
|
local function messageCustomNeeded(action)
|
|
return WA.ContainsCustomPlaceHolder(action.message)
|
|
or (action.message_type == "WHISPER" and WA.ContainsCustomPlaceHolder(action.message_dest))
|
|
end
|
|
|
|
local function messageFields(fields, data, action, when, condition)
|
|
local prefix = "actions:" .. when .. ":"
|
|
if not condition then
|
|
table.insert(fields, { type = "toggle", name = "Chat Message", key = "do_message",
|
|
get = function() return action.do_message and true or false end,
|
|
set = function(v) action.do_message = v and true or false; WA.Add(data); WA.RefreshOptions() end })
|
|
end
|
|
table.insert(fields, { type = "select", name = "Message Type", key = "message_type", values = MESSAGE_TYPES, labels = MESSAGE_LABELS,
|
|
get = function() return action.message_type or "PRINT" end,
|
|
set = function(v) action.message_type = v; WA.Add(data); WA.RefreshOptions() end })
|
|
table.insert(fields, { type = "input", name = "Message", key = "message",
|
|
get = function() return action.message end,
|
|
set = function(v) action.message = v; WA.SetDefaultFormatters(v, function(k, d) return actionFormatGet(action, k, d) end,
|
|
function(k, x) actionFormatSet(data, action, k, x) end, data); WA.Add(data); WA.RefreshOptions() end })
|
|
if action.message_type == "WHISPER" then
|
|
table.insert(fields, { type = "input", name = "Send To", key = "message_dest",
|
|
get = function() return action.message_dest end,
|
|
set = function(v) action.message_dest = v; WA.Add(data); WA.RefreshOptions() end })
|
|
table.insert(fields, { type = "toggle", name = "Is Unit", key = "message_dest_isunit",
|
|
get = function() return action.message_dest_isunit and true or false end,
|
|
set = function(v) action.message_dest_isunit = v and true or false; WA.Add(data) end })
|
|
end
|
|
if action.message_type == "PRINT" or action.message_type == "ERROR" or action.message_type == "COMBAT" then
|
|
-- One field writing action.r/g/b as a unit, so the key is a coined name
|
|
-- rather than any one of the three properties.
|
|
table.insert(fields, { type = "color", name = "Color", key = "message_color",
|
|
get = function() return { action.r or 1, action.g or 1, action.b or 1, 1 } end,
|
|
set = function(v) action.r, action.g, action.b = v[1], v[2], v[3]; WA.Add(data) end })
|
|
end
|
|
if action.do_message and messageCustomNeeded(action) then
|
|
table.insert(fields, { type = "code", height = 80, name = "Message Custom Code", key = "message_custom",
|
|
get = function() return action.message_custom end,
|
|
set = function(v) action.message_custom = v; WA.Add(data) end })
|
|
end
|
|
if action.message and action.message ~= "" then
|
|
local formatFields = WA.FormatOptionFields(action.message,
|
|
function(k, d) return actionFormatGet(action, k, d) end,
|
|
function(k, v) actionFormatSet(data, action, k, v) end, data, prefix .. "message_format_")
|
|
for i = 1, #formatFields do table.insert(fields, formatFields[i]) end
|
|
end
|
|
end
|
|
|
|
function WA.ActionMessageFields(fields, data, action, when, condition)
|
|
messageFields(fields, data, action, when, condition)
|
|
end
|
|
|
|
local SOUND_CHANNELS = { "Master", "SFX", "Ambience", "Music" }
|
|
local SOUND_CHANNEL_LABELS = { Master = "Master", SFX = "Sound", Ambience = "Ambience", Music = "Music" }
|
|
|
|
-- Upstream offers the stop on hide only: a show sound has nothing earlier of
|
|
-- this aura's to cut off.
|
|
local function stopSoundFields(fields, data, action)
|
|
table.insert(fields, { type = "toggle", name = "Stop Sound", key = "stop_sound",
|
|
get = function() return action.stop_sound and true or false end,
|
|
set = function(v) action.stop_sound = v and true or false; WA.Add(data); WA.RefreshOptions() end })
|
|
if not action.stop_sound then return end
|
|
table.insert(fields, { type = "toggle", name = "Fade Out", key = "do_sound_fade",
|
|
get = function() return action.do_sound_fade and true or false end,
|
|
set = function(v) action.do_sound_fade = v and true or false; WA.Add(data); WA.RefreshOptions() end })
|
|
if action.do_sound_fade then
|
|
table.insert(fields, { type = "range", name = "Fade Out Time (seconds)", key = "stop_sound_fade", min = 0, max = 10, step = 0.1,
|
|
get = function() return action.stop_sound_fade or 0 end,
|
|
set = function(v) action.stop_sound_fade = v; WA.Add(data) end })
|
|
end
|
|
end
|
|
|
|
local function soundFields(fields, data, action, condition, when)
|
|
local previews = {}
|
|
for key in pairs(WA.sound_types) do
|
|
if key ~= " custom" and key ~= " KitID" then previews[key] = true end
|
|
end
|
|
if condition then
|
|
table.insert(fields, { type = "select", name = "Sound Type", key = "sound_type", values = { "Play", "Loop", "Stop" },
|
|
get = function() return action.sound_type or "Play" end,
|
|
set = function(v) action.sound_type = v; WA.Add(data); WA.RefreshOptions() end })
|
|
if action.sound_type == "Stop" then return end
|
|
else
|
|
if when == "finish" then stopSoundFields(fields, data, action) end
|
|
table.insert(fields, { type = "toggle", name = "Play Sound", key = "do_sound",
|
|
get = function() return action.do_sound and true or false end,
|
|
set = function(v) action.do_sound = v and true or false; WA.Add(data); WA.RefreshOptions() end })
|
|
table.insert(fields, { type = "toggle", name = "Loop", key = "do_loop",
|
|
get = function() return action.do_loop and true or false end,
|
|
set = function(v) action.do_loop = v and true or false; WA.Add(data); WA.RefreshOptions() end })
|
|
end
|
|
if (condition and action.sound_type == "Loop") or (not condition and action.do_loop) then
|
|
table.insert(fields, { type = "range", name = "Repeat After", key = "sound_repeat", min = 0.1, max = 60, step = 0.1,
|
|
get = function() return action.sound_repeat or 1 end,
|
|
set = function(v) action.sound_repeat = v; WA.Add(data) end })
|
|
end
|
|
table.insert(fields, { type = "select", name = "Sound", key = "sound", values = WA.SoundValues, labels = WA.sound_types,
|
|
previews = previews, onPreview = function(v) if v ~= " custom" and v ~= " KitID" then WA.PreviewSound(v) end end,
|
|
get = function() return action.sound or "" end,
|
|
set = function(v) action.sound = v; WA.Add(data); WA.RefreshOptions() end })
|
|
if action.sound == " custom" then
|
|
table.insert(fields, { type = "input", name = "Sound File Path", key = "sound_path",
|
|
get = function() return action.sound_path end,
|
|
set = function(v) action.sound_path = v; WA.PreviewSound(v); WA.Add(data) end })
|
|
elseif action.sound == " KitID" then
|
|
table.insert(fields, { type = "input", name = "Sound Kit ID or Name", key = "sound_kit_id",
|
|
get = function() return action.sound_kit_id end,
|
|
set = function(v) action.sound_kit_id = v; WA.PreviewSound(v, true); WA.Add(data) end })
|
|
end
|
|
table.insert(fields, { type = "select", name = "Sound Channel", key = "sound_channel",
|
|
values = SOUND_CHANNELS, labels = SOUND_CHANNEL_LABELS,
|
|
get = function() return action.sound_channel or "Master" end,
|
|
set = function(v) action.sound_channel = v; WA.Add(data) end })
|
|
end
|
|
|
|
-- Ordered by *label*, not by path: the list is long enough to need scrolling and
|
|
-- a path sort would group it by folder, scattering the names the reader is
|
|
-- actually scanning for. The two pseudo-entries keep their leading space and are
|
|
-- pinned to the top rather than sorted in among the real files.
|
|
WA.SoundValues = {}
|
|
for key in pairs(WA.sound_types) do table.insert(WA.SoundValues, key) end
|
|
table.sort(WA.SoundValues, function(a, b)
|
|
local pinnedA, pinnedB = string.sub(a, 1, 1) == " ", string.sub(b, 1, 1) == " "
|
|
if pinnedA ~= pinnedB then return pinnedA end
|
|
return WA.sound_types[a] < WA.sound_types[b]
|
|
end)
|
|
|
|
function WA.ActionSoundFields(fields, data, action, condition, when)
|
|
soundFields(fields, data, action, condition, when)
|
|
end
|
|
|
|
function WA.PerformActions(data, when, region)
|
|
if not data or WA.optionsOpen or WA.forced[data.id] then return end
|
|
local actions = data.actions and data.actions[when]
|
|
if not actions then return end
|
|
if not WA.SquelchingActions() and actions.do_message and actions.message_type and actions.message then
|
|
local cache = WA.customActionsFunctions[data.id]
|
|
local customValues
|
|
local customFn = cache and cache[when .. "_message"]
|
|
if customFn then
|
|
local ok, values = WA.RunAuraFuncPacked(region, data.id .. ": " .. when .. " message", customFn)
|
|
if ok then customValues = values end
|
|
end
|
|
WA.HandleChatAction(actions.message_type, actions.message, actions.message_dest,
|
|
actions.message_dest_isunit, actions.r, actions.g, actions.b, region,
|
|
customValues, when, region[when .. "Formatters"])
|
|
end
|
|
-- Upstream's fade is in seconds; StopSound takes milliseconds.
|
|
if actions.stop_sound then
|
|
WA.SoundStop(region, actions.do_sound_fade and actions.stop_sound_fade and actions.stop_sound_fade * 1000)
|
|
end
|
|
if actions.do_sound then WA.SoundPlay(region, actions) end
|
|
if actions.do_custom and actions.custom then
|
|
local cache = WA.customActionsFunctions[data.id]
|
|
local fn = cache and cache[when]
|
|
if fn then region:RunCode(fn) end
|
|
end
|
|
-- Deliberately outside the squelch above, as upstream's is: a glow is a state
|
|
-- the aura leaves behind rather than a notification, so suppressing it at
|
|
-- login would leave the display lit with nothing to put it out.
|
|
if actions.do_glow then WA.GlowExternal(region, actions) end
|
|
-- The escape hatch for a glow whose own "hide" never runs -- a show aimed at
|
|
-- a frame the aura no longer resolves, most often. Finish only: on show there
|
|
-- is nothing yet to clear.
|
|
if when == "finish" and actions.hide_all_glows then WA.StopExternalGlows(region) end
|
|
end
|
|
|
|
local function runInitIfNeeded(id, env)
|
|
if env._waActionInit then return end
|
|
env._waActionInit = true
|
|
local cache = WA.customActionsFunctions[id]
|
|
if cache and cache.init then
|
|
WA.RunAuraFunc(id, id .. ": init", cache.init)
|
|
end
|
|
end
|
|
|
|
-- `onlyConfig` is an environment built solely so somebody else can read its
|
|
-- config -- a group collecting aura_env.child_envs. Running init there would run
|
|
-- an aura's own code at a moment that aura had nothing to do with, so this is
|
|
-- the enforcement point for the distinction AuraEnv.lua draws.
|
|
local originalActivateAuraEnv = WA.ActivateAuraEnv
|
|
function WA.ActivateAuraEnv(id, cloneId, state, states, onlyConfig)
|
|
local env = originalActivateAuraEnv(id, cloneId, state, states, onlyConfig)
|
|
if id and env and not onlyConfig then runInitIfNeeded(id, env) end
|
|
return env
|
|
end
|
|
|
|
function WA.HandleChatAction(messageType, message, messageDest, messageDestIsUnit, r, g, b,
|
|
region, customValues, when, formatters)
|
|
if not message then return end
|
|
message = WA.ReplacePlaceHolders(message, region, formatters, customValues)
|
|
if messageType == "PRINT" then
|
|
if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then
|
|
pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, message, r or 1, g or 1, b or 1)
|
|
end
|
|
elseif messageType == "ERROR" then
|
|
if UIErrorsFrame and UIErrorsFrame.AddMessage then
|
|
pcall(UIErrorsFrame.AddMessage, UIErrorsFrame, message, r or 1, g or 1, b or 1)
|
|
end
|
|
elseif messageType == "COMBAT" then
|
|
if not CombatText_AddMessage and LoadAddOn then pcall(LoadAddOn, "Blizzard_CombatText") end
|
|
if CombatText_AddMessage then
|
|
pcall(CombatText_AddMessage, message, COMBAT_TEXT_SCROLL_FUNCTION, r or 1, g or 1, b or 1)
|
|
end
|
|
elseif messageType == "TTS" then
|
|
pcall(C_VoiceChat.SpeakText, C_TTSSettings.GetSpeechVoiceID(), message)
|
|
else
|
|
if messageType == "WHISPER" and messageDest then
|
|
messageDest = WA.ReplacePlaceHolders(messageDest, region, formatters, customValues)
|
|
if messageDestIsUnit and UnitName then messageDest = UnitName(messageDest) end
|
|
pcall(SendChatMessage, message, "WHISPER", nil, messageDest)
|
|
elseif messageType == "SMARTRAID" then
|
|
local channel = "SAY"
|
|
if IsInRaid() then channel = "RAID"
|
|
elseif IsInGroup() then channel = "PARTY" end
|
|
pcall(SendChatMessage, message, channel)
|
|
else
|
|
pcall(SendChatMessage, message, messageType)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function noAction()
|
|
end
|
|
|
|
local function addRegionMethods(region)
|
|
function region:RunCode(fn)
|
|
if fn and not WA.optionsOpen and not WA.forced[self.id] then
|
|
return WA.RunAuraFunc(self, self.id .. ": custom action", fn)
|
|
end
|
|
end
|
|
region.SoundPlay = function(self, options) WA.SoundPlay(self, options) end
|
|
region.SoundStop = function(self, fadeoutTime) WA.SoundStop(self, fadeoutTime) end
|
|
region.SoundRepeatStop = function(self) WA.SoundRepeatStop(self) end
|
|
region.SendChat = function(self, options) WA.SendChat(self, options) end
|
|
region.GlowExternal = function(self, options) WA.GlowExternal(self, options) end
|
|
region.StopExternalGlows = function(self) WA.StopExternalGlows(self) end
|
|
end
|
|
|
|
WA.AttachActionMethods = addRegionMethods
|
|
|
|
function WA.RunActionCodeForLoad(data, key)
|
|
local cache = WA.customActionsFunctions[data.id]
|
|
local fn = cache and cache[key]
|
|
if fn then return WA.RunAuraFunc(data.id, data.id .. ": " .. key, fn) end
|
|
end
|