-- $Id$ --[[ LICENSE Copyright (c) 2006-2007, Kyle Smith All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --]] --[[ DOCUMENTATION Description: CastCommLib is an embedded library for communicating spell casts and their targets using the AddonMessage channel. Addons can register with CastCommLib to receive notification of these messages. CastCommLib will use ChatThrottleLib if it is available. API: CastCommLib:RegisterCallback(key, func) -- Adds a callback funciton to be invoked when a spell event is received -- -- If key is a table, then func must be a string and the callback will be -- invoked as: -- key[func](key, sender, senderUnit, action, target, channel, spell, rank, -- displayName, icon, startTime, endTime, isTradeSkill) -- otherwise func must be a function and will be invoked as: -- func(sender, senderUnit, action, target, channel, spell, rank, -- displayName, icon, startTime, endTime, isTradeSkill) -- -- string sender: -- the name of the character that sent the event -- string senderUnit: -- the unitid of the character that sent the event -- string action: -- one of "START", "DELAYED", "INTERRUPTED", "FAILED", "SUCCEEDED", -- "CHANNEL_START", "CHANNEL_UPDATE", or "CHANNEL_STOP". -- depending on the event that caused the message to be sent -- string target: -- the name of the target for the spell -- boolean channel: -- if the spell is channeled -- string spell: -- the name of the spell -- string rank: -- the rank of the spell -- string displayName: -- the name of the spell (not sure how this differes from spell, but it's -- from UnitCastingInfo()) -- string icon: -- the path to the texture for the spell -- number startTime: -- the time the spell started casting/channeling -- number endTime: -- the time the spell will finish casting/channeling -- boolean isTradeSkill: -- if the spell is a tradeskill -- -- Usage: -- CastCommLib:RegisterCallback(MyAddon, "CastCommLib") -- or -- CastCommLib:RegisterCallback("MyAddon", OnCastCommLibReceived) CastCommLib:UnregisterCallback(key) -- Removes the callback function specified by key -- -- Usage: -- CastCommLib:UnregisterCallback(MyAddon) -- or -- CastCommLib:UnregisterCallback("MyAddon") CastCommLib:GetUnitId(name) -- returns the cached unitid for the specified name CastCommLib:IterateGroupMembers(name) -- returns an iterator suitable for use with for -- return values for each iteration is the name of the next person in the -- specified person's group CastCommLib:GetCastingInfo(name) -- returns target, channel, spell, rank, displayName, icon, startTime, endTime, -- isTradeSkill -- -- See CastCommLib:RegisterCallback for details CastCommLib:Enable() -- Enables the library. You probably shouldn't be calling this. CastCommLib:Disable() -- Disables the library. You probably shouldn't be calling this. CastCommLib:ToggleDebugging(on) -- Enables, disables, or toggles the debugging state of CastCommLib -- -- If on is nil, the debugging state will be toggled, otherwise it will be set -- to the value of on. --]] local name = "CastCommLib" local version = tonumber(("$Revision$"):match("(%d+)")) or 1 local oldLib = getglobal(name) if oldLib and oldLib.version >= version then return end local lib = {} lib.name = name lib.version = version --[[ LOCALIZATION ]]---------------------------------------------------------- local L local currentLocale = GetLocale() local L_defaults = { -- error messages ERROR_CALLBACK_REGISTERED = "a callback has already been registered with that key", ERROR_CALLBACK_USAGE = string.format("%s version %d usage: RegisterCallback(table, string) or RegisterCallback(key, function)", name, version), } if currentLocale == "enUS" then L = {} elseif currentLocale == "deDE" then L = {} elseif currentLocale == "frFR" then L = {} elseif currentLocale == "zhCN" then L = {} elseif currentLocale == "zhTW" then L = {} elseif currentLocale == "koKR" then L = {} elseif currentLocale == "esES" then L = {} else L = {} end -- provide default translations setmetatable(L, { __index = L_defaults }) --[[ HERE'S THE REAL CODE ]]-------------------------------------------------- function lib:Initialize (old) self:Debug("Initialize()") self.callbacks = old and old.callbacks or {} self:RegisterEvent("ADDON_LOADED") end function lib:Enable () self:Debug("Enable()") -- spell self:RegisterEvent("UNIT_SPELLCAST_SENT") self:RegisterEvent("UNIT_SPELLCAST_START") self:RegisterEvent("UNIT_SPELLCAST_DELAYED") self:RegisterEvent("UNIT_SPELLCAST_INTERRUPTED") self:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED") self:RegisterEvent("UNIT_SPELLCAST_FAILED") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_START") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_UPDATE") self:RegisterEvent("UNIT_SPELLCAST_CHANNEL_STOP") -- comm self:RegisterEvent("CHAT_MSG_ADDON") -- roster self:UpdateRoster() self:RegisterEvent("PARTY_MEMBERS_CHANGED", "UpdateRoster") self:RegisterEvent("RAID_ROSTER_UPDATE", "UpdateRoster") end function lib:Disable () self:Debug("Disable()") self:UnregisterAllEvents() end --[[ CALLBACK FUNCTIONS ]]---------------------------------------------------- function lib:RegisterCallback (k, f) if self.callbacks[k] then error(L["ERROR_CALLBACK_REGISTERED"], 2) end if type(k) == "table" then if type(f) ~= "string" then error(L["ERROR_CALLBACK_USAGE"], 2) end elseif type(f) ~= "function" then error(L["ERROR_CALLBACK_USAGE"], 2) end self.callbacks[k] = f end function lib:UnregisterCallback (k) self.callbacks[k] = nil end --[[ SPELL FUNCTIONS ]]------------------------------------------------------- --[[ Notes on event orders The following are the order that these events have been known to arrive in: Casting an instant spell successfully: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_SUCCEEDED Attempting to cast on an in-range target with obstructed line-of-sight: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_FAILED Attempting to cast a non-instant spell while moving: UNIT_SPELLCAST_FAILED Casting a non-instant spell and then moving: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_START UNIT_SPELLCAST_INTERRUPTED Casting a non-instant spell successfully: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_START UNIT_SPELLCAST_SUCCEEDED UNIT_SPELLCAST_SUCCEEDED Casting a non-instant spell while being attacked: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_START UNIT_SPELLCAST_DELAYED UNIT_SPELLCAST_SUCCEEDED UNIT_SPELLCAST_SUCCEEDED Casting a channeled spell and then moving: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_CHANNEL_START UNIT_SPELLCAST_SUCCEEDED UNIT_SPELLCAST_CHANNEL_STOP Casting a channeled spell successfully: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_CHANNEL_START UNIT_SPELLCAST_SUCCEEDED UNIT_SPELLCAST_CHANNEL_STOP Casting a channeled spell while being attacked: UNIT_SPELLCAST_SENT UNIT_SPELLCAST_CHANNEL_START UNIT_SPELLCAST_SUCCEEDED UNIT_SPELLCAST_CHANNEL_UPDATE UNIT_SPELLCAST_CHANNEL_STOP --]] do local lastSpell local lastTarget local isChanneling local isInstant -- only fired for player function lib:UNIT_SPELLCAST_SENT (unit, spell, rank, target) if unit ~= "player" then return end lastSpell = spell lastTarget = target isChanneling = nil isInstant = true end -- only fired for player function lib:UNIT_SPELLCAST_START (unit) if unit ~= "player" then return end if not lastSpell then return end isInstant = false self:SendMessage("START", lastTarget) end function lib:UNIT_SPELLCAST_DELAYED (unit) if unit ~= "player" then return end if not lastSpell then return end self:SendMessage("DELAYED") end function lib:UNIT_SPELLCAST_INTERRUPTED (unit) if unit ~= "player" then return end if not lastSpell then return end self:SendMessage("INTERRUPTED") lastSpell = nil end -- only fired for player function lib:UNIT_SPELLCAST_SUCCEEDED (unit, spell, rank) if unit ~= "player" then return end if not lastSpell then return end if isChanneling then return end if not isInstant then self:SendMessage("SUCCEEDED") end lastSpell = nil end function lib:UNIT_SPELLCAST_FAILED (unit) if unit ~= "player" then return end if not lastSpell then return end self:SendMessage("FAILED") lastSpell = nil end function lib:UNIT_SPELLCAST_CHANNEL_START (unit) if unit ~= "player" then return end if not lastSpell then return end isChanneling = true isInstant = false self:SendMessage("CHANNEL_START", lastTarget) end function lib:UNIT_SPELLCAST_CHANNEL_UPDATE (unit) if unit ~= "player" then return end if not lastSpell then return end if not isChanneling then return end self:SendMessage("CHANNEL_UPDATE") end function lib:UNIT_SPELLCAST_CHANNEL_STOP (unit) if unit ~= "player" then return end if not lastSpell then return end if not isChanneling then return end self:SendMessage("CHANNEL_STOP") lastSpell = nil isChanneling = nil end end --[[ ROSTER FUNCTIONS ]]------------------------------------------------------ do local roster = {} local roster_subgroup = {} local party_units = { "player", "party1", "party2", "party3", "party4", "party5", } local raid_units = { "raid1", "raid2", "raid3", "raid4", "raid5", "raid6", "raid7", "raid8", "raid9", "raid10", "raid11", "raid12", "raid13", "raid14", "raid15", "raid16", "raid17", "raid18", "raid19", "raid20", "raid21", "raid22", "raid23", "raid24", "raid25", "raid26", "raid27", "raid28", "raid29", "raid30", "raid31", "raid32", "raid33", "raid34", "raid35", "raid36", "raid37", "raid38", "raid39", "raid40", } local unitIndex local function ExistingUnits (units) unitIndex = unitIndex + 1 local unit = units[unitIndex] if unit and UnitExists(unit) then return unit else return nil end end local function UnitIterator (units) unitIndex = 0 return ExistingUnits, units end local unitsToRemove = {} function lib:UpdateRoster () self:Debug("UpdateRoster()") local units, inRaid if GetNumRaidMembers() > 0 then units = raid_units inRaid = true else units = party_units inRaid = false end for name, unit in pairs(roster) do unitsToRemove[name] = true end for unitid in UnitIterator(units) do local name = UnitName(unitid) local raidIndex = unitid:match("raid(%d+)") local subgroup = raidIndex and select(3, GetRaidRosterInfo(raidIndex)) or 1 self:Debug("Saw", unitid, name, subgroup) roster[name] = unitid roster_subgroup[name] = subgroup unitsToRemove[name] = nil end for name in pairs(unitsToRemove) do self:Debug("Removed", name) roster[name] = nil roster_subgroup[name] = nil unitsToRemove[name] = nil end end function lib:GetUnitId (name) return roster[name] end local igm_index local function SubgroupIterator (group) igm_index = next(roster_subgroup, igm_index) while igm_index and roster_subgroup[igm_index] do if roster_subgroup[igm_index] == group then return igm_index, roster[igm_index] end igm_index = next(roster_subgroup, igm_index) end return nil end function lib:IterateGroupMembers (name) igm_index = nil return SubgroupIterator, roster_subgroup[name] end end --[[ COMM FUNCTIONS ]]-------------------------------------------------------- -- GetSpellInfo() is in here too do function lib:CHAT_MSG_ADDON (prefix, message, distribution, sender) if prefix ~= self.name then return end self:ReceiveMessage(sender, message) end -- ChatThrottleLib support local SendAddonMessage = SendAddonMessage local CTL function lib:ADDON_LOADED (name) if ChatThrottleLib == CTL then return end CTL = ChatThrottleLib self:Debug("ChatThrottleLib", CTL.version, "detected") local function CTL_SendAddonMessage (prefix, message, distribution) return CTL:SendAddonMessage("ALERT", prefix, message, distribution) end SendAddonMessage = CTL_SendAddonMessage end -- action abbreviations for the addon message -- all abbreviations are 2 letters so we can use string.sub instead of -- string.match -- lookup action -> abbreviation local actionAbbr = {} -- lookup abbreviation -> action local abbrAction = {} -- keep the forward and backup lookups consistent -- also, sanity-check the input local function actionAbbr__newindex (t, k, v) error("don't modify this table!") end setmetatable(actionAbbr, { __newindex = actionAbbr__newindex }) local function abbrAction__newindex (t, k, v) -- sanity-check our arguments if type(k) ~= "string" then error("key must be string") end if type(v) ~= "string" then error("value must be string") end if k:len() ~= 2 then error("key must be 2 characters long") end rawset(t, k, v) rawset(actionAbbr, v, k) end setmetatable(abbrAction, { __newindex = abbrAction__newindex }) abbrAction["C "] = "START" abbrAction["D "] = "DELAYED" abbrAction["I "] = "INTERRUPTED" abbrAction["F "] = "FAILED" abbrAction["S "] = "SUCCEEDED" abbrAction["CC"] = "CHANNEL_START" abbrAction["CU"] = "CHANNEL_UPDATE" abbrAction["CS"] = "CHANNEL_STOP" -- these actions mean a new spell has been cast local actionSpellBegin = { ["START"] = true, ["CHANNEL_START"] = true, } -- these actions mean we need to update the information about the spell local actionSpellContinue = { ["DELAYED"] = true, ["CHANNEL_UPDATE"] = true, } -- these actions mean the spell is no longer being cast local actionSpellEnd = { ["INTERRUPTED"] = true, ["FAILED"] = true, ["SUCCEEDED"] = true, ["CHANNEL_STOP"] = true, } -- message functions function lib:SendMessage(action, target) local message if actionSpellBegin[action] then message = ("%-2s%s"):format(actionAbbr[action], target) else message = ("%-2s"):format(actionAbbr[action]) end local distribution if select(2, IsInInstance()) == "pvp" then distribution = "BATTLEGROUND" elseif GetNumRaidMembers() > 0 then distribution = "RAID" elseif GetNumPartyMembers() > 0 then distribution = "PARTY" end self:Debug("SendAddonMessage", self.name, message, distribution) if distribution then SendAddonMessage(self.name, message, distribution) else self:ReceiveMessage(UnitName("player"), message) end end -- cache all the spell info because it isn't available after the spell -- is finished casting local cache_target = {} local cache_channel = {} local cache_spell = {} local cache_rank = {} local cache_displayName = {} local cache_icon = {} local cache_startTime = {} local cache_endTime = {} local cache_isTradeSkill = {} function lib:ReceiveMessage(sender, message) local action, target, channel, senderUnit local spell, rank, displayName, icon, startTime, endTime, isTradeSkill action = message:sub(1, 2) target = message:sub(3, -1) action = abbrAction[action] senderUnit = self:GetUnitId(sender) if not senderUnit then self:Debug("Unable to determine unit for", sender) return end if action == "START" then cache_target[sender] = target elseif action == "CHANNEL_START" then cache_target[sender] = target cache_channel[sender] = true channel = true else target = cache_target[sender] channel = cache_channel[sender] end if not actionSpellEnd[action] then if channel then spell, rank, displayName, icon, startTime, endTime, isTradeSkill = UnitChannelInfo(senderUnit) else spell, rank, displayName, icon, startTime, endTime, isTradeSkill = UnitCastingInfo(senderUnit) end cache_spell[sender] = spell cache_rank[sender] = rank cache_displayName[sender] = displayName cache_icon[sender] = icon cache_startTime[sender] = startTime cache_endTime[sender] = endTime cache_isTradeSkill[sender] = isTradeSkill else spell = cache_spell[sender] rank = cache_rank[sender] displayName = cache_displayName[sender] icon = cache_icon[sender] startTime = cache_startTime[sender] endTime = cache_endTime[sender] isTradeSkill = cache_isTradeSkill[sender] end self:Debug("ReceiveMessage", sender, senderUnit, action, target, channel, spell, rank, displayName, icon, startTime, endTime, isTradeSkill) for k, f in pairs(self.callbacks) do if type(k) == "table" then k[f](k, sender, senderUnit, action, target, channel, spell, rank, displayName, icon, startTime, endTime, isTradeSkill) else f(sender, senderUnit, action, target, channel, spell, rank, displayName, icon, startTime, endTime, isTradeSkill) end end if actionSpellEnd[action] then cache_target[sender] = nil cache_channel[sender] = nil cache_spell[sender] = nil cache_rank[sender] = nil cache_displayName[sender] = nil cache_icon[sender] = nil cache_startTime[sender] = nil cache_endTime[sender] = nil cache_isTradeSkill[sender] = nil end end function lib:GetCastingInfo (name) return cache_target[name], cache_channel[name], cache_spell[name], cache_rank[name], cache_displayName[name], cache_icon[name], cache_startTime[name], cache_endTime[name], cache_isTradeskill[name] end end --[[ LIBRARY CORE ]]---------------------------------------------------------- -- everything below here is just basic library stuff -- inspired by Dongle and Ace2 --[[ DEBUGGING ]]------------------------------------------------------------- do function lib:InitializeDebugging (old) if old then self.debug = old.debug self.debugFrame = old.debugFrame else self.debug = false self.debugFrame = ChatFrame1 end end local debugArgs = {} function lib:DebugPrint (...) for i = #debugArgs, 1, -1 do debugArgs[i] = nil end table.insert(debugArgs, string.format("|cfffcc000||%s ||%d|||r", self.name, self.version)) for i = 1, select("#", ...) do table.insert(debugArgs, tostring(select(i, ...))) end local message = table.concat(debugArgs, " ") self.debugFrame:AddMessage(message) end function lib:ToggleDebugging (on) if on == nil then self.debug = not self.debug else self.debug = on end self:DebugPrint("Debugging", self.debug and "|cff00cf00enabled|r" or "|cffcf0000disabled|r") return self.debug end function lib:Debug (...) if not self.debug then return end return self:DebugPrint(...) end end --[[ EVENT HANDLING ]]-------------------------------------------------------- do -- event dispatch table local events = {} -- event dispatch function local function OnEvent (frame, event, ...) local handler = events[event] lib.event = event if handler and type(lib[handler]) == "function" then lib:Debug("|cffcccc00Event:|r", event, ...) return lib[handler](lib, ...) end end -- I'm sure we'll use this for a timer eventually local time_since_last_schedule = 0 local time_until_next_schedule local next_scheduled_function local function OnUpdate (frame, elapsed) -- nothing to do if not time_until_next_schedule then return end time_since_last_schedule = time_since_last_schedule + elapsed -- nothing to do yet if time_since_last_schedule < time_until_next_schedule then return end next_scheduled_function() -- reset time_since_last_schedule = 0 time_until_next_schedule = nil next_scheduled_function = nil end function lib:InitializeEvents (old) if old then old:UnregisterAllEvents() self.eventFrame = old.eventFrame self.eventFrame:SetScript("OnEvent", nil) self.eventFrame:SetScript("OnUpdate", nil) else local frameName = self.name .. "_EVENT_FRAME" self.eventFrame = CreateFrame("Frame", frameName) end self.eventFrame:SetScript("OnEvent", OnEvent) -- self.eventFrame:SetScript("OnUpdate", OnUpdate) end function lib:RegisterEvent (event, handler) events[event] = handler or event self.eventFrame:RegisterEvent(event) end function lib:UnregisterEvent(event) self.eventFrame:UnregisterEvent(event) events[event] = nil end function lib:UnregisterAllEvents () for event in pairs(events) do self:UnregisterEvent(event) end end function lib:TriggerEvent (event, ...) return OnEvent(nil, event, ...) end end --[[ LIBRARY MANAGEMENT ]]---------------------------------------------------- function lib:Install () local old = getglobal(self.name) if old then old:Disable() end -- debugging self:InitializeDebugging(old) -- event handling self:InitializeEvents(old) -- install the library in the global namespace setglobal(self.name, self) -- register important event handlers self:RegisterEvent("LIB_INSTALLED", "Initialize") self:RegisterEvent("PLAYER_LOGIN", "Enable") self:RegisterEvent("PLAYER_LOGOUT", "Disable") -- tell the library to do it's thing self:TriggerEvent("LIB_INSTALLED", old) end lib:Install()