You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

422 lines
16 KiB

--========================================================--
-- 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