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