WoWInterface SVN RaidWatch2

[/] [trunk/] [RaidWatch/] [Libs/] [AceComm-3.0/] [AceComm-3.0.lua] - Rev 22

Compare with Previous | Blame | View Log

--- **AceComm-3.0** allows you to send messages of unlimited length over the addon comm channels.
-- It'll automatically split the messages into multiple parts and rebuild them on the receiving end.\\
-- **ChatThrottleLib** is of course being used to avoid being disconnected by the server.
--
-- **AceComm-3.0** can be embeded into your addon, either explicitly by calling AceComm:Embed(MyAddon) or by 
-- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
-- and can be accessed directly, without having to explicitly call AceComm itself.\\
-- It is recommended to embed AceComm, otherwise you'll have to specify a custom `self` on all calls you
-- make into AceComm.
-- @class file
-- @name AceComm-3.0
-- @release $Id: AceComm-3.0.lua 895 2009-12-06 16:28:55Z nevcairiel $

--[[ AceComm-3.0

TODO: Time out old data rotting around from dead senders? Not a HUGE deal since the number of possible sender names is somewhat limited.

]]

local MAJOR, MINOR = "AceComm-3.0", 6

local AceComm,oldminor = LibStub:NewLibrary(MAJOR, MINOR)

if not AceComm then return end

local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
local CTL = assert(ChatThrottleLib, "AceComm-3.0 requires ChatThrottleLib")

-- Lua APIs
local type, next, pairs, tostring = type, next, pairs, tostring
local strsub, strfind = string.sub, string.find
local tinsert, tconcat = table.insert, table.concat
local error, assert = error, assert

-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script
-- GLOBALS: LibStub, DEFAULT_CHAT_FRAME, geterrorhandler

AceComm.embeds = AceComm.embeds or {}

-- for my sanity and yours, let's give the message type bytes some names
local MSG_MULTI_FIRST = "\001"
local MSG_MULTI_NEXT  = "\002"
local MSG_MULTI_LAST  = "\003"

AceComm.multipart_origprefixes = AceComm.multipart_origprefixes or {} -- e.g. "Prefix\001"="Prefix", "Prefix\002"="Prefix"
AceComm.multipart_reassemblers = AceComm.multipart_reassemblers or {} -- e.g. "Prefix\001"="OnReceiveMultipartFirst"

-- the multipart message spool: indexed by a combination of sender+distribution+
AceComm.multipart_spool = AceComm.multipart_spool or {} 

--- Register for Addon Traffic on a specified prefix
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
-- @param method Callback to call on message reception: Function reference, or method name (string) to call on self. Defaults to "OnCommReceived"
function AceComm:RegisterComm(prefix, method)
        if method == nil then
                method = "OnCommReceived"
        end

        return AceComm._RegisterComm(self, prefix, method)      -- created by CallbackHandler
end

local warnedPrefix=false

--- Send a message over the Addon Channel
-- @param prefix A printable character (\032-\255) classification of the message (typically AddonName or AddonNameEvent)
-- @param text Data to send, nils (\000) not allowed. Any length.
-- @param distribution Addon channel, e.g. "RAID", "GUILD", etc; see SendAddonMessage API
-- @param target Destination for some distributions; see SendAddonMessage API
-- @param prio OPTIONAL: ChatThrottleLib priority, "BULK", "NORMAL" or "ALERT". Defaults to "NORMAL".
-- @param callbackFn OPTIONAL: callback function to be called as each chunk is sent. receives 3 args: the user supplied arg (see next), the number of bytes sent so far, and the number of bytes total to send.
-- @param callbackArg: OPTIONAL: first arg to the callback function. nil will be passed if not specified.
function AceComm:SendCommMessage(prefix, text, distribution, target, prio, callbackFn, callbackArg)
        prio = prio or "NORMAL" -- pasta's reference implementation had different prio for singlepart and multipart, but that's a very bad idea since that can easily lead to out-of-sequence delivery!
        if not( type(prefix)=="string" and
                        type(text)=="string" and
                        type(distribution)=="string" and
                        (target==nil or type(target)=="string") and
                        (prio=="BULK" or prio=="NORMAL" or prio=="ALERT") 
                ) then
                error('Usage: SendCommMessage(addon, "prefix", "text", "distribution"[, "target"[, "prio"[, callbackFn, callbackarg]]])', 2)
        end
        
        if strfind(prefix, "[\001-\009]") then
                if strfind(prefix, "[\001-\003]") then
                        error("SendCommMessage: Characters \\001--\\003 in prefix are reserved for AceComm metadata", 2)
                elseif not warnedPrefix then
                        -- I have some ideas about future extensions that require more control characters /mikk, 20090808
                        geterrorhandler()("SendCommMessage: Heads-up developers: Characters \\004--\\009 in prefix are reserved for AceComm future extension")
                        warnedPrefix = true
                end
        end


        local textlen = #text
        local maxtextlen = 254 - #prefix        -- 254 is the max length of prefix + text that can be sent in one message
        local queueName = prefix..distribution..(target or "")

        local ctlCallback = nil
        if callbackFn then
                ctlCallback = function(sent)
                        return callbackFn(callbackArg, sent, textlen)
                end
        end

        if textlen <= maxtextlen then
                -- fits all in one message
                CTL:SendAddonMessage(prio, prefix, text, distribution, target, queueName, ctlCallback, textlen)
        else
                maxtextlen = maxtextlen - 1     -- 1 extra byte for part indicator in prefix

                -- first part
                local chunk = strsub(text, 1, maxtextlen)
                CTL:SendAddonMessage(prio, prefix..MSG_MULTI_FIRST, chunk, distribution, target, queueName, ctlCallback, maxtextlen)

                -- continuation
                local pos = 1+maxtextlen
                local prefix2 = prefix..MSG_MULTI_NEXT

                while pos+maxtextlen <= textlen do
                        chunk = strsub(text, pos, pos+maxtextlen-1)
                        CTL:SendAddonMessage(prio, prefix2, chunk, distribution, target, queueName, ctlCallback, pos+maxtextlen-1)
                        pos = pos + maxtextlen
                end

                -- final part
                chunk = strsub(text, pos)
                CTL:SendAddonMessage(prio, prefix..MSG_MULTI_LAST, chunk, distribution, target, queueName, ctlCallback, textlen)
        end
end


----------------------------------------
-- Message receiving
----------------------------------------

do
        local compost = setmetatable({}, {__mode = "k"})
        local function new()
                local t = next(compost)
                if t then 
                        compost[t]=nil
                        for i=#t,3,-1 do        -- faster than pairs loop. don't even nil out 1/2 since they'll be overwritten
                                t[i]=nil
                        end
                        return t
                end
                
                return {}
        end
        
        local function lostdatawarning(prefix,sender,where)
                DEFAULT_CHAT_FRAME:AddMessage(MAJOR..": Warning: lost network data regarding '"..tostring(prefix).."' from '"..tostring(sender).."' (in "..where..")")
        end

        function AceComm:OnReceiveMultipartFirst(prefix, message, distribution, sender)
                local key = prefix.."\t"..distribution.."\t"..sender    -- a unique stream is defined by the prefix + distribution + sender
                local spool = AceComm.multipart_spool
                
                --[[
                if spool[key] then 
                        lostdatawarning(prefix,sender,"First")
                        -- continue and overwrite
                end
                --]]
                
                spool[key] = message  -- plain string for now
        end

        function AceComm:OnReceiveMultipartNext(prefix, message, distribution, sender)
                local key = prefix.."\t"..distribution.."\t"..sender    -- a unique stream is defined by the prefix + distribution + sender
                local spool = AceComm.multipart_spool
                local olddata = spool[key]
                
                if not olddata then
                        --lostdatawarning(prefix,sender,"Next")
                        return
                end

                if type(olddata)~="table" then
                        -- ... but what we have is not a table. So make it one. (Pull a composted one if available)
                        local t = new()
                        t[1] = olddata    -- add old data as first string
                        t[2] = message    -- and new message as second string
                        spool[key] = t    -- and put the table in the spool instead of the old string
                else
                        tinsert(olddata, message)
                end
        end

        function AceComm:OnReceiveMultipartLast(prefix, message, distribution, sender)
                local key = prefix.."\t"..distribution.."\t"..sender    -- a unique stream is defined by the prefix + distribution + sender
                local spool = AceComm.multipart_spool
                local olddata = spool[key]
                
                if not olddata then
                        --lostdatawarning(prefix,sender,"End")
                        return
                end

                spool[key] = nil
                
                if type(olddata) == "table" then
                        -- if we've received a "next", the spooled data will be a table for rapid & garbage-free tconcat
                        tinsert(olddata, message)
                        AceComm.callbacks:Fire(prefix, tconcat(olddata, ""), distribution, sender)
                        compost[olddata] = true
                else
                        -- if we've only received a "first", the spooled data will still only be a string
                        AceComm.callbacks:Fire(prefix, olddata..message, distribution, sender)
                end
        end
end






----------------------------------------
-- Embed CallbackHandler
----------------------------------------

if not AceComm.callbacks then
        -- ensure that 'prefix to watch' table is consistent with registered
        -- callbacks
        AceComm.__prefixes = {}

        AceComm.callbacks = CallbackHandler:New(AceComm,
                                                "_RegisterComm",
                                                "UnregisterComm",
                                                "UnregisterAllComm")
end

function AceComm.callbacks:OnUsed(target, prefix)
        AceComm.multipart_origprefixes[prefix..MSG_MULTI_FIRST] = prefix
        AceComm.multipart_reassemblers[prefix..MSG_MULTI_FIRST] = "OnReceiveMultipartFirst"
        
        AceComm.multipart_origprefixes[prefix..MSG_MULTI_NEXT] = prefix
        AceComm.multipart_reassemblers[prefix..MSG_MULTI_NEXT] = "OnReceiveMultipartNext"
        
        AceComm.multipart_origprefixes[prefix..MSG_MULTI_LAST] = prefix
        AceComm.multipart_reassemblers[prefix..MSG_MULTI_LAST] = "OnReceiveMultipartLast"
end

function AceComm.callbacks:OnUnused(target, prefix)
        AceComm.multipart_origprefixes[prefix..MSG_MULTI_FIRST] = nil
        AceComm.multipart_reassemblers[prefix..MSG_MULTI_FIRST] = nil
        
        AceComm.multipart_origprefixes[prefix..MSG_MULTI_NEXT] = nil
        AceComm.multipart_reassemblers[prefix..MSG_MULTI_NEXT] = nil
        
        AceComm.multipart_origprefixes[prefix..MSG_MULTI_LAST] = nil
        AceComm.multipart_reassemblers[prefix..MSG_MULTI_LAST] = nil
end

local function OnEvent(this, event, ...)
        if event == "CHAT_MSG_ADDON" then
                local prefix,message,distribution,sender = ...
                local reassemblername = AceComm.multipart_reassemblers[prefix]
                if reassemblername then
                        -- multipart: reassemble
                        local aceCommReassemblerFunc = AceComm[reassemblername]
                        local origprefix = AceComm.multipart_origprefixes[prefix]
                        aceCommReassemblerFunc(AceComm, origprefix, message, distribution, sender)
                else
                        -- single part: fire it off immediately and let CallbackHandler decide if it's registered or not
                        AceComm.callbacks:Fire(prefix, message, distribution, sender)
                end
        else
                assert(false, "Received "..tostring(event).." event?!")
        end
end

AceComm.frame = AceComm.frame or CreateFrame("Frame", "AceComm30Frame")
AceComm.frame:SetScript("OnEvent", OnEvent)
AceComm.frame:UnregisterAllEvents()
AceComm.frame:RegisterEvent("CHAT_MSG_ADDON")


----------------------------------------
-- Base library stuff
----------------------------------------

local mixins = {
        "RegisterComm",
        "UnregisterComm",
        "UnregisterAllComm",
        "SendCommMessage",
}

-- Embeds AceComm-3.0 into the target object making the functions from the mixins list available on target:..
-- @param target target object to embed AceComm-3.0 in
function AceComm:Embed(target)
        for k, v in pairs(mixins) do
                target[v] = self[v]
        end
        self.embeds[target] = true
        return target
end

function AceComm:OnEmbedDisable(target)
        target:UnregisterAllComm()
end

-- Update embeds
for target, v in pairs(AceComm.embeds) do
        AceComm:Embed(target)
end

Compare with Previous | Blame