--========================================================-- -- Scorpio Message System -- -- -- -- Author : kurapica125@outlook.com -- -- Create Date : 2021/09/05 -- --========================================================-- --========================================================-- Scorpio "Scorpio.Message" "" --========================================================-- SCORPIO_ADDON_PREFIX = "SCORPIO" MESSAGE_OVERHEAD = 40 MAX_BYTE_SECOND = 2000 MAX_AVAILABLE = 8000 _WorldBeginTime = 0 _Available = 0 _LastUpdate = 0 _Recycle = Recycle() _ProcessQueue = false _AddonMessageQueue = Queue() _ResultMessages = {} _ModulePrefixHandler = {} _NextMessageTasks = {} ------------------------------------------------------------ -- Helpers -- ------------------------------------------------------------ export { min = math.min, ceil = math.ceil, floor = math.floor, random = math.random, format = string.format, char = string.char, byte = string.byte, concat = table.concat, tinsert = table.insert, yield = coroutine.yield, resume = coroutine.resume, running = coroutine.running } function updateAvailable() local now = GetTime() _Available = min(MAX_AVAILABLE, _Available + floor(MAX_BYTE_SECOND * (now - _LastUpdate))) _LastUpdate = now end function toNumber(str) local b1, b2 = byte(str, 1, 2) return (b1 - 65) * 26 + b2 - 65 end function toString(index) return char(65 + floor(index / 26), 65 + index % 26) end __Async__() function nextMessageCall(prefix, callback, timeout) return callback(NextMessage(prefix, timeout)) end function releaseNextMessage(task) if task[2] then local handlers = _NextMessageTasks[prefix] if handlers then local length = #handlers for i = 1, length do if handlers[i] == task then handlers[i] = false if length == 1 then _NextMessageTasks[prefix] = nil end break end end end local ok, err = resume(task[2]) if not ok then geterrorhandler()(err) end task[2] = nil end end ------------------------------------------------------------ -- Message Enumeration -- ------------------------------------------------------------ if Scorpio.IsRetail then __Sealed__() enum "ChatType" { "PARTY", "RAID", "INSTANCE_CHAT", "GUILD", "OFFICER", "WHISPER", "CHANNEL" } else __Sealed__() enum "ChatType" { "PARTY", "RAID", "INSTANCE_CHAT", "GUILD", "OFFICER", "WHISPER", "SAY", "YELL" } end ------------------------------------------------------------ -- Message Event Handler -- ------------------------------------------------------------ function OnEnable() OnEnable = nil C_ChatInfo.RegisterAddonMessagePrefix(SCORPIO_ADDON_PREFIX) end __SystemEvent__() function PLAYER_ENTERING_WORLD() _WorldBeginTime = GetTime() _LastUpdate = GetTime() _Available = 0 end __SecureHook__"SendChatMessage" function Hook_SendChatMessage(message, chatType, language, target) _Available = _Available - (#tostring(message or "") + #tostring(target or "") + MESSAGE_OVERHEAD) end __SecureHook__(_G.C_ChatInfo, "SendAddonMessage") function Hook_SendAddonMessage(prefix, message, chatType, target) _Available = _Available - (#tostring(prefix or "") + #tostring(message or "") + #tostring(target or "") + MESSAGE_OVERHEAD) end __Async__() function DistributeMessages(packet, channel, sender, target) local data = Toolset.parsestring(DeflateDecode(Base64Decode(concat(packet)))) if data then for _, item in ipairs(data) do local prefix = item[1] local message = item[2] and Toolset.parsestring(item[2]) if prefix and message then local handlers = _ModulePrefixHandler[prefix] if handlers then for owner, handler in pairs(handlers) do local ok, err = pcall(handler, message, channel, sender, target) if not ok then geterrorhandler()(err) end end end handlers = _NextMessageTasks[prefix] _NextMessageTasks[prefix] = nil if handlers then Continue() for _, thread in ipairs(handlers) do if type(thread) == "table" then -- With timeout, no recycle, just let it be collected local temp = thread thread = thread[2] temp[2] = nil end if thread then local ok, err = resume(thread, message, channel, sender, target) if not ok then geterrorhandler()(err) end end end _Recycle(wipe(handlers)) end Continue() end end end end __SystemEvent__() function CHAT_MSG_ADDON(prefix, text, channel, sender, target, zoneChannelID, localID, name, instanceID) if prefix ~= SCORPIO_ADDON_PREFIX then return end local pid, count, id, msg = text:match("^(%w+):(%w+):(%w+):(.*)$") pid = pid and tonumber(pid, 16) count = count and toNumber(count) id = id and toNumber(id) if pid and count and id then local now = GetTime() local dist = _ResultMessages[pid] if dist and ((now - dist.time) >= 10 or dist.count ~= count or (#dist + 1 ~= id)) then dist = nil _ResultMessages[pid]= nil end if not dist then if id == 1 then dist = { time = now, count = count, [1] = msg } _ResultMessages[pid] = dist else return -- Abandon, no more check, no ack for now end else dist[#dist + 1] = msg end if id == dist.count then -- All received _ResultMessages[pid]= nil DistributeMessages(dist, channel, sender, target) end end end __Iterator__() function iterdist(packets) local yield = yield for chatType, dist in pairs(packets) do if chatType == "WHISPER" or chatType == "CHANNEL" then for target, sdist in pairs(dist) do yield(sdist, chatType, target) _Recycle(wipe(sdist)) end else yield(dist, chatType) end _Recycle(wipe(dist)) end _Recycle(wipe(packets)) end __Service__(true) function ProcessMessages() while true do while _AddonMessageQueue.Count == 0 do Next() end local packets = _Recycle() local callbacks = _Recycle() local total = 0 -- Build the distribution repeat local chatType, prefix, message, target, callback = _AddonMessageQueue:Dequeue(5) if callback then tasks[callback] = true end local dist = packets[chatType] or _Recycle() packets[chatType] = dist local temp = _Recycle() message = Toolset.tostring(message) total = total + #message temp[1] = prefix temp[2] = message if target ~= -1 then local subDist = dist[target] or _Recycle() dist[target] = subDist subDist[#subDist + 1] = temp else dist[#dist + 1] = temp end -- Not too big if total >= 256 then break end until _AddonMessageQueue.Count <= 0 -- Send the packets for dist, chatType, target in iterdist(packets) do local extra = #SCORPIO_ADDON_PREFIX + (target and #tostring(target) or 0) local maxlen = 238 - extra local message = Base64Encode(DeflateEncode(Toolset.tostring(dist))) local length = #message local count = ceil(length / maxlen) -- 12bit for header local header = format("%06X:", random(0xffffff)) .. toString(count) .. ":" updateAvailable() -- Send the messages for i = 1, count do local msg = header .. toString(i) .. ":" .. message:sub(1 + (i-1) * maxlen, i * maxlen) local len = #msg + extra while _Available < len do local delay = (len - _Available) / MAX_BYTE_SECOND if delay >= 0.1 then Delay(delay) else Next() end updateAvailable() end C_ChatInfo.SendAddonMessage(SCORPIO_ADDON_PREFIX, msg, chatType, target) end end -- Invoke the callback or resume the coroutine for callback in pairs(callbacks) do if type(callback) == "function" then local ok, err = pcall(callback) if not ok then geterrorhandler()(err) end else resume(callback) end end _Recycle(callbacks) Next() end end ------------------------------------------------------------ -- Message Attribute -- ------------------------------------------------------------ --- Register a handler for the prefix message -- @usage -- Scorpio "MyAddon" "v1.0.1" -- -- __Message__() -- function PLAYER_COOLDOWN_UPDATE(cooldown, sender) -- print(cooldown.spellid, cooldown.start, cooldown.duration) -- end -- -- __Message__ "PLAYER_COOLDOWN_START" "PLAYER_COOLDOWN_UPDATE" -- function PLAYER_COOLDOWN(cooldown, sender) -- end -- -- SendAddonMessage("PLAYER_COOLDOWN_UPDATE", { spellid = 1923, start = GetTime(), duration = 3 }) __Sealed__() class "__Message__" (function(_ENV) extend "IAttachAttribute" function AttachAttribute(self, target, targettype, owner, name, stack) if Class.IsObjectType(owner, Scorpio) then if #self > 0 then for _, evt in ipairs(self) do _ModulePrefixHandler[evt] = _ModulePrefixHandler[evt] or {} _ModulePrefixHandler[evt][owner]= target end else _ModulePrefixHandler[name] = _ModulePrefixHandler[name] or {} _ModulePrefixHandler[name][owner] = target end else error("__Message__ can only be applyed to objects of Scorpio.", stack + 1) end end ---------------------------------------------- -- Property -- ---------------------------------------------- property "AttributeTarget" { default = AttributeTargets.Function } ---------------------------------------------- -- Constructor -- ---------------------------------------------- __Arguments__{ NEString * 0 } function __new(cls, ...) return { ... }, true end ---------------------------------------------- -- Meta-Method -- ---------------------------------------------- __Arguments__{ NEString } function __call(self, other) tinsert(self, other) return self end end) ------------------------------------------------------------ -- Message Method -- ------------------------------------------------------------ __Static__() function Scorpio.SendAddonMessage(prefix, message, chatType, target, callback) if not Struct.ValidateValue(NEString, prefix) then error("Usage: SendAddonMessage(prefix, message[, chatType[, target]][, callback]) - The prefix must be a non-empty string", 2) end if not (message and Struct.ValidateValue(Serialization.Serializable, message)) then error("Usage: SendAddonMessage(prefix, message[, chatType[, target]][, callback]) - The message must be serializable", 2) end if type(chatType) == "function" or chatType == true then callback = chatType chatType = "PARTY" elseif type(target) == "function" or target == true then callback = target target = nil end if chatType and not Enum.ValidateValue(ChatType, chatType) then error("Usage: SendAddonMessage(prefix, message[, chatType[, target]][, callback]) - The chatType is not valid", 2) end if chatType == "WHISPER" and not Struct.ValidateValue(NEString, target) then error("Usage: SendAddonMessage(prefix, message[, chatType[, target]][, callback]) - The target must be target user name", 2) end if chatType == "CHANNEL" and not Struct.ValidateValue(NaturalNumber, target) then error("Usage: SendAddonMessage(prefix, message[, chatType[, target]][, callback]) - The target must be a channel id", 2) end if callback and callback ~= true and type(callback) ~= "function" then error("Usage: SendAddonMessage(prefix, message[, chatType[, target]][, callback]) - The callback must be a function", 2) end local thread = callback == true and running() chatType = chatType or "PARTY" if chatType == "WHISPER" or chatType == "CHANNEL" then _AddonMessageQueue:Enqueue(chatType, prefix, message, target, thread or callback or false) else _AddonMessageQueue:Enqueue(chatType, prefix, message, -1, thread or callback or false) end -- Waiting the result if thread then return yield() end end __Arguments__{ NEString, (Number + Callable)/nil, Number/nil } __Static__() function Scorpio.NextMessage(prefix, callback, timeout) if type(callback) == "number" then callback, timeout = nil, callback end if callback then return nextMessageCall(prefix, callback, timeout) end local thread = running() if not thread then error("Usage: NextMessage(prefix[, callback]) - The NextMessage must be used in a coroutine") end _NextMessageTasks[prefix] = _NextMessageTasks[prefix] or _Recycle() if timeout then local task = _Recycle() task[1] = prefix task[2] = thread tinsert(_NextMessageTasks[prefix], task) Delay(timeout, releaseNextMessage, task) else tinsert(_NextMessageTasks[prefix], thread) end return yield() end