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.

2193 lines
85 KiB

5 years ago
--========================================================--
-- Scorpio Addon FrameWork --
-- --
-- Author : kurapica125@outlook.com --
-- Create Date : 2016/12/12 --
-- Update Date : 2019/07/12 --
--========================================================--
PLoop(function(_ENV)
------------------------------------------------------------
-- Scorpio - Addon Class --
------------------------------------------------------------
__Sealed__()
_G.Scorpio = class "Scorpio" (function (_ENV)
inherit "Module"
import "System.Reactive"
----------------------------------------------
-- Prepare --
----------------------------------------------
-------------------- META --------------------
META_WEAKKEY = { __mode = "k" }
META_WEAKVAL = { __mode = "v" }
------------------- Logger -------------------
Log = Logger("Scorpio")
Log.LogLevel = 3
export {
------------------- Math ---------------------
min = math.min,
max = math.max,
------------------- String -------------------
strtrim = strtrim or function(s) return (s:gsub("^%s*(.-)%s*$", "%1")) or "" end,
------------------- Error --------------------
geterrorhandler = geterrorhandler or function() return print end,
errorhandler = errorhandler or function(err) return geterrorhandler()(err) end,
------------------- Table --------------------
tblconcat = table.concat,
tinsert = table.insert,
tremove = table.remove,
wipe = wipe or function(t) for k in pairs(t) do t[k] = nil end return t end,
------------------- Coroutine ----------------
create = coroutine.create,
resume = coroutine.resume,
running = coroutine.running,
status = coroutine.status,
wrap = coroutine.wrap,
yield = coroutine.yield,
DefaultPool = Threading.ThreadPool.Default,
GetSpecialization = GetSpecialization or function() return 1 end,
IsWarModeDesired = C_PvP and C_PvP.IsWarModeDesired or function() return false end,
}
ThreadCall = function(...) return DefaultPool:ThreadCall(...) end
----------------------------------------------
-- Addon Cache --
----------------------------------------------
_RootAddon = setmetatable({}, META_WEAKVAL)
_NotLoaded = setmetatable({}, META_WEAKKEY)
_DisabledModule = setmetatable({}, META_WEAKKEY)
local function callAddonHandlers(map, ...)
if not map then return end
for obj, handler in pairs(map) do
if not _DisabledModule[obj] then
local ok, err = pcall(handler, ...)
if not ok then errorhandler(err) end
end
end
end
----------------------------------------------
-- Cache System --
----------------------------------------------
local t_Cache = {} -- Cache Manager
local _RegisterService = {}
local _ResidentService = setmetatable({}, META_WEAKKEY)
local _ObjectGuidMap = setmetatable({}, META_WEAKKEY)
local _SingleAsync = setmetatable({}, META_WEAKVAL)
local _RunSingleAsync = setmetatable({}, META_WEAKKEY)
local _CancelSingleAsync= setmetatable({}, META_WEAKKEY)
-- For diagnosis
g_CacheGenerated = 0
g_CacheRamain = 1
local function recycleCache(cache)
if cache then
wipe(cache)
if t_Cache then
cache[0] = t_Cache
end
t_Cache = cache
g_CacheRamain = g_CacheRamain + 1
return
end
if t_Cache then
cache = t_Cache
t_Cache = cache[0]
cache[0] = nil
g_CacheRamain = g_CacheRamain - 1
return cache
else
g_CacheGenerated= g_CacheGenerated + 1
return {}
end
end
----------------------------------------------
-- Task System --
----------------------------------------------
-- Phase Settings
PHASE_THRESHOLD = 15 -- The max task operation time per phase
PHASE_TIME_FACTOR = 0.4 -- The factor used to calculate the task operation time per phase
PHASE_OVERTIME_FACTOR = 0.3 -- the fatcor used to calculate the most time for the remain tasks
-- System Task Settings
EVENT_CLEAR_INTERVAL = 100 -- The interval for event task clear
EVENT_CLEAR_DELAY = 10
DIAGNOSE_DELAY = 60
-- Const
HIGH_PRIORITY = 1 -- For Continue
NORMAL_PRIORITY = 2 -- For Event, Next, Wait
LOW_PRIORITY = 3 -- For Delay
-- Global variables
g_Phase = 0 -- The Nth phase based on GetTime()
g_PhaseTime = 0
g_Threshold = 0 -- The threshold based on GetFramerate(), in ms
g_InPhase = false
g_FinishedTask = 0
g_StartTime = 0
g_EndTime = 0
g_AverageTime = 20 -- An useless init value
g_PhaseStartTime = 0 -- Recored the start phase time
g_PhaseStartProfile = 0 -- The start profile time of current phase
-- For diagnosis
g_DelayedTask = 0
g_MaxPhaseTime = 0
-- Task List
t_Tasks = {} -- Core task list
-- Runtime task
r_Tasks = {}
r_Count = 0
-- In Loading Screen
r_InLoadingScreen = true
r_InBattleField = false
r_DelayResumeForBF = 1
-- Phase API
local function processPhase()
if g_InPhase or r_InLoadingScreen then return end
g_InPhase = true
-- Prepare the task list
local now = GetTime()
if now ~= g_Phase then
-- Init the phase
g_Phase = now
g_PhaseTime = r_Count * g_AverageTime * PHASE_OVERTIME_FACTOR
-- For diagnosis
g_DelayedTask = g_DelayedTask + r_Count
-- Calculate the average time per task
if g_FinishedTask > 0 then
local cost = g_EndTime - g_StartTime
-- For diagnosis
if cost > g_MaxPhaseTime then g_MaxPhaseTime = cost end
g_AverageTime = (g_AverageTime + cost / g_FinishedTask) / 2
g_FinishedTask = 0
end
-- Record the start time
g_StartTime = g_PhaseStartProfile -- debugprofilestop()
-- Move task to core based on priority
-- High priority means it should be processed as soon as possible
-- Normal priority means it should be processed in the next phase as high priority
-- Lower priority means it should be processed when there is enough time
local r_Tail = r_Tasks[0]
for i = HIGH_PRIORITY, NORMAL_PRIORITY do
local cache = t_Tasks[i]
if cache then
t_Tasks[i] = nil
if r_Tail then
r_Tail[0] = cache
else
-- Init
r_Tasks[1] = cache
end
while cache do
r_Tail = cache
r_Count = r_Count + #cache
cache = cache[0]
end
end
end
r_Tasks[0] = r_Tail
-- LOW_PRIORITY
if not r_Tasks[LOW_PRIORITY] and t_Tasks[LOW_PRIORITY] then
r_Tasks[LOW_PRIORITY] = t_Tasks[LOW_PRIORITY]
t_Tasks[LOW_PRIORITY] = nil
end
g_PhaseTime = min(PHASE_THRESHOLD, g_PhaseTime + 1000 * PHASE_TIME_FACTOR / max(10, GetFramerate() or 60))
g_Threshold = g_StartTime + g_PhaseTime
-- Check if too much time cost by events(with low cpu), we still need some time to process the high priority tasks
local currStop = debugprofilestop()
if g_Threshold <= currStop then g_Threshold = currStop + g_PhaseTime end
elseif not r_Tasks[1] then
-- Only tasks of high priority can be executed again and again in a phase
local cache = t_Tasks[HIGH_PRIORITY]
if cache then
t_Tasks[HIGH_PRIORITY] = nil
r_Tasks[1] = cache
while cache do
r_Tasks[0] = cache
r_Count = r_Count + #cache
cache = cache[0]
end
end
end
-- It's time to process the task execution
-- Process the high priority tasks
local r_Header = r_Tasks[1]
local runoutIdx = nil
while r_Header do
for i = r_Header[-1] or 1, #r_Header do
-- The phase is out of time, keep the index for next phase
if g_Threshold <= debugprofilestop() then
runoutIdx = i
break
end
local task = r_Header[i]
r_Header[-1]= i + 1
if task then
if _CancelSingleAsync[task] then
_CancelSingleAsync[task] = nil
else
-- Process the task
local ok, msg = resume(task)
if not ok then
pcall(geterrorhandler(), msg)
if _RunSingleAsync[task] then
if type(_RunSingleAsync[task]) == "string" then
_SingleAsync[_RunSingleAsync[task]] = false
else
_RunSingleAsync[task][1] = false
end
_RunSingleAsync[task] = nil
end
if _ResidentService[task] then
ThreadCall(_ResidentService[task], msg)
_ResidentService[task] = nil
end
end
g_FinishedTask = g_FinishedTask + 1
end
end
r_Count = r_Count - 1
end
if runoutIdx and r_Header then
r_Header[-1] = runoutIdx
break
end
local nxt = r_Header[0]
recycleCache(r_Header)
r_Header = nxt
end
r_Tasks[1] = r_Header
if not r_Header then r_Tasks[0] = nil end
-- Process the low priority tasks
if not runoutIdx and r_Tasks[LOW_PRIORITY] then
r_Header = r_Tasks[LOW_PRIORITY]
for i = r_Header[-1] or 1, #r_Header do
if g_Threshold <= debugprofilestop() then
runoutIdx = i
break
end
local task = r_Header[i]
r_Header[-1]= i + 1
if task then
if _CancelSingleAsync[task] then
_CancelSingleAsync[task] = nil
else
-- Process the task
local ok, msg = resume(task)
if not ok then
pcall(geterrorhandler(), msg)
if _RunSingleAsync[task] then
if type(_RunSingleAsync[task]) == "string" then
_SingleAsync[_RunSingleAsync[task]] = false
else
_RunSingleAsync[task][1] = false
end
_RunSingleAsync[task] = nil
end
if _ResidentService[task] then
ThreadCall(_ResidentService[task])
_ResidentService[task] = nil
end
end
g_FinishedTask = g_FinishedTask + 1
end
end
end
if runoutIdx then
r_Header[-1]= runoutIdx
else
recycleCache(r_Header)
r_Tasks[LOW_PRIORITY] = nil
end
end
g_EndTime = debugprofilestop()
g_InPhase = false
-- Try again if have time with high priority tasks
return g_Threshold > g_EndTime and t_Tasks[HIGH_PRIORITY] and processPhase()
end
-- Queue API
local function queueTask(priority, task)
local cache = t_Tasks[priority]
if not cache then
cache = recycleCache()
t_Tasks[priority] = cache
end
tinsert(cache, task)
end
local function queueTaskList(priority, tasklist)
local cache = t_Tasks[priority]
if not cache then
t_Tasks[priority] = tasklist
else
while cache[0] do cache = cache[0] end
cache[0] = tasklist
end
end
----------------------------------------------
-- System Task Driver --
----------------------------------------------
ScorpioManager = CreateFrame("Frame")
_EventDistribution = {} -- System Event
_CombatEventDistribution= {} -- Combat Event
_SecureHookDistribution = setmetatable({}, META_WEAKKEY) -- Secure Hook
t_EventTasks = {} -- Event Task
t_WaitEventTasks = {} -- Wait Event Task
t_SecureHookTasks = setmetatable({}, META_WEAKKEY) -- Secure Hook Task
-- Wait thread token
w_Token = {}
w_Token_INDEX = 1
local t_DelayTasks = nil -- Delayed task
local function queueDelayTask(task, time)
time = floor((GetTime() + time) * 100)
local node, header = t_DelayTasks
while node and node[1] < time do
header = node
node = header[0]
end
if node and node[1] == time then
tinsert(node, task)
else
node = recycleCache()
node[1] = time
node[2] = task
if header then
node[0] = header[0]
header[0] = node
else
node[0] = t_DelayTasks
t_DelayTasks= node
end
end
end
local function queueEventTask(task, event)
if not _EventDistribution[event] then
_EventDistribution[event] = setmetatable({}, META_WEAKKEY)
pcall(ScorpioManager.RegisterEvent, ScorpioManager, event)
end
local cache = t_EventTasks[event]
if not cache then
cache = recycleCache()
t_EventTasks[event] = cache
end
tinsert(cache, task)
end
local function queueWaitTask(task, delay, ...)
local token = w_Token_INDEX
w_Token_INDEX = w_Token_INDEX + 1
if w_Token_INDEX > 2147483647 then w_Token_INDEX = 1 end
w_Token[token] = task
if delay then queueDelayTask(token, delay) end
for i = 1, select("#", ...) do
local event = select(i, ...)
if not _EventDistribution[event] then
_EventDistribution[event] = setmetatable({}, META_WEAKKEY)
pcall(ScorpioManager.RegisterEvent, ScorpioManager, event)
end
local cache = t_WaitEventTasks[event]
if not cache then
cache = recycleCache()
cache[0] = GetTime() + EVENT_CLEAR_INTERVAL
t_WaitEventTasks[event] = cache
end
tinsert(cache, token)
end
end
local function yieldReturn(...)
yield()
return ...
end
local function newSystemTask(func, ...)
if select("#", ...) > 0 then
yieldReturn(yield( running() ))
return func(...)
else
return func(yieldReturn(yield( running() )))
end
end
local function wrapAsSystemTask(func, ...)
return ThreadCall(newSystemTask, func, ...)
end
local function newSimpleTask(func, ...)
yield( running() )
return func(...)
end
local function wrapAsSimpleTask(func, ...)
return ThreadCall(newSimpleTask, func, ...)
end
local function processQueue(priority, queue, ...)
yield( running() )
for _, task in ipairs(queue) do
if task then resume(task, ...) end
end
queueTaskList(priority, queue)
end
local function getSecureHookMap(target, targetFunc)
local map = _SecureHookDistribution[target]
if not map then
map = setmetatable({}, META_WEAKKEY)
_SecureHookDistribution[target] = map
end
map = map[targetFunc]
if not map then
if type(target[targetFunc]) ~= "function" then
error(("No method named '%s' can be found."):format(targetFunc))
end
map = setmetatable({}, META_WEAKKEY)
_SecureHookDistribution[target][targetFunc] = map
hooksecurefunc(target, targetFunc, function(...)
local cache = t_SecureHookTasks[target]
local queue = cache and cache[targetFunc]
if queue then
cache[targetFunc] = nil
queueTask(NORMAL_PRIORITY, ThreadCall(processQueue, HIGH_PRIORITY, queue, ...))
end
return callAddonHandlers(map, ...)
end)
end
return map
end
local function queueNextSecureCall(task, target, targetFunc)
if not getSecureHookMap(target, targetFunc) then return end
local cache = t_SecureHookTasks[target]
if not cache then
cache = setmetatable({}, META_WEAKKEY)
t_SecureHookTasks[target] = cache
end
local queue = cache[targetFunc]
if not queue then
queue = recycleCache()
cache[targetFunc] = queue
end
tinsert(queue, task)
end
local function noCombatCall(callable, ...)
while InCombatLockdown() do Next() end
return callable(...)
end
local function registerService(func, resident)
local wrap
wrap = resident and function()
_ResidentService[running()] = wrap
Next() func()
end or function()
Next() func()
end
tinsert(_RegisterService, wrap)
end
local function processService()
if _RegisterService[1] then
local task = _RegisterService
_RegisterService= {}
for i = 1, #task do ThreadCall(task[i]) end
end
end
local function retSingleAsync(...)
local curr = running()
local guid = _RunSingleAsync[curr]
_RunSingleAsync[curr] = nil
_CancelSingleAsync[curr]= nil
if guid then
if type(guid) == "string" then
_SingleAsync[guid] = false
else
guid[1] = false
end
end
return ...
end
local function registerSingleAsync(func, override, owner, name)
if owner then
-- For method, the guid should be binded to class
-- But we need check if the method is static
local staticguid
local getGuid = function(self)
if staticguid then
-- For static method
return staticguid
elseif staticguid == nil then
-- Check whether is static method
if Interface.IsStaticMethod(owner, name) then
local guid = Guid.New()
while _SingleAsync[guid] ~= nil do guid = Guid.New() end
_SingleAsync[guid] = false
staticguid = guid
return guid
else
staticguid = false
end
else
-- For object method
local map = _ObjectGuidMap[self]
if not map then
map = {}
_ObjectGuidMap[self] = map
end
local guid = map[name]
if not guid then
guid = Guid.New()
while map[guid] ~= nil do guid = Guid.New() end
map[name] = guid
map[guid] = { [0]= guid, [1] = false }
end
return guid, map[guid]
end
end
local wrapper = function(self, ...)
local guid, obmap = getGuid(self)
local curr = running()
if obmap then
_RunSingleAsync[curr] = obmap
_CancelSingleAsync[curr] = nil
obmap[1] = curr
else
_RunSingleAsync[curr] = guid
_CancelSingleAsync[curr] = nil
_SingleAsync[guid] = curr
end
return retSingleAsync(func(self, ...))
end
if override then
return function(self, ...)
local guid, obmap = getGuid(self)
if obmap then
local curr = obmap[1]
if curr then
obmap[1] = false
_RunSingleAsync[curr] = nil
_CancelSingleAsync[curr]= true
end
else
local curr = _SingleAsync[guid]
if curr then
_SingleAsync[guid] = false
_RunSingleAsync[curr] = nil
_CancelSingleAsync[curr]= true
end
end
return ThreadCall(wrapper, self, ...)
end
else
return function(self, ...)
local guid, obmap = getGuid(self)
if obmap then
if obmap[1] then return end
else
if _SingleAsync[guid] then return end
end
return ThreadCall(wrapper, self, ...)
end
end
else
-- For function the guid is binded to function
local guid = Guid.New()
while _SingleAsync[guid] ~= nil do guid = Guid.New() end
_SingleAsync[guid] = false
local wrapper = function(...)
local curr = running()
_RunSingleAsync[curr] = guid
_SingleAsync[guid] = curr
return retSingleAsync(func(...))
end
if override then
return function(...)
local curr = _SingleAsync[guid]
if curr then
_SingleAsync[guid] = false
_RunSingleAsync[curr] = nil
_CancelSingleAsync[curr]= true
end
return ThreadCall(wrapper, ...)
end
else
return function(...)
if _SingleAsync[guid] then return end
return ThreadCall(wrapper, ...)
end
end
end
end
local function callCombatHandlers(timestamp, eventType, ...)
local map = _CombatEventDistribution[eventType]
if map then return callAddonHandlers(map, timestamp, eventType, ...) end
end
local function safeCall(...)
local ok, err = pcall(...)
if not ok then errorhandler(err) end
end
----------------------------------------------
-- Next Observable --
----------------------------------------------
local rycNextObserver = Recycle(Observer)
function rycNextObserver:OnInit(ob)
ob.OnNextCore = function(...)
ob:Unsubscribe()
ob:Resubscribe()
local thread = ob.NextThread
ob.NextThread = nil
rycNextObserver(ob)
if thread then
resume(thread, ...)
queueTask(HIGH_PRIORITY, thread)
end
end
end
----------------------------------------------
-- Addon Helper --
----------------------------------------------
_SlashCmdList = _G.SlashCmdList
_SlashCmdCount = 0
_SlashCmdHandler = {}
-- Whether the player is already logined
_Logined = false
_PlayerSpec = -1
_PlayerWarMode = -1
-- SlashCmd Operation
local function newSlashCmd(slashCmd, map)
-- New Slash Command
_SlashCmdCount = _SlashCmdCount + 1
-- Register it to the system
_G["SLASH_SCORPIOCMD_".._SlashCmdCount.."_1"] = slashCmd
_SlashCmdList["SCORPIOCMD_".._SlashCmdCount.."_"] = function(msg, input)
local option, info
if type(msg) == "string" then
msg = strtrim(msg)
if msg:sub(1, 1) == "\"" and msg:find("\"", 2) then
option, info = msg:match("\"([^\"]+)\"%s*(.*)")
else
option, info = msg:match("(%S+)%s*(.*)")
end
if option then option = option:upper() end
end
if option and map[option] then
if map[option](info, input) == false then
print("--======================--")
print(("%s %s %s"):format(slashCmd:lower(), option:lower(), map[option .. "-desc"] or ""))
print("--======================--")
end
elseif map[0] and map[0](msg, input) ~= false then
-- pass
else
-- Default handler
if next(map) then
print("--======================--")
for opt, m in pairs(map) do
if type(m) == "function" then
print(("%s %s %s"):format(slashCmd:lower(), opt:lower(), map[opt .. "-desc"] or ""))
end
end
print("--======================--")
end
end
end
end
local function loadingWithoutClear(self)
if _NotLoaded[self] then
safeCall(OnLoad, self)
end
for _, mdl in self:GetModules() do loadingWithoutClear(mdl) end
end
local function loading(self)
if _NotLoaded[self] then
_NotLoaded[self] = nil
safeCall(OnLoad, self)
end
for _, mdl in self:GetModules() do loading(mdl) end
end
local function enablingWithCheck(self)
if not _DisabledModule[self] then
if _NotLoaded[self] then
safeCall(OnEnable, self)
end
for _, mdl in self:GetModules() do enablingWithCheck(mdl) end
end
end
local function enabling(self)
if _NotLoaded[self] then loading(self) end
if not _DisabledModule[self] then
safeCall(OnEnable, self)
for _, mdl in self:GetModules() do enabling(mdl) end
end
end
local function disabling(self)
if not _DisabledModule[self] then
_DisabledModule[self] = true
if _Logined then safeCall(OnDisable, self) end
for _, mdl in self:GetModules() do disabling(mdl) end
end
end
local function tryEnable(self)
if _DisabledModule[self] and self._Enabled then
if not self._Parent or (not _DisabledModule[self._Parent]) then
_DisabledModule[self] = nil
safeCall(OnEnable, self)
for _, mdl in self:GetModules() do
if mdl._Enabled then
tryEnable(mdl)
end
end
end
end
end
local function exiting(self)
safeCall(OnQuit, self)
for _, mdl in self:GetModules() do exiting(mdl) end
end
local function specChangedWithCheck(self, spec)
if _NotLoaded[self] then
safeCall(OnSpecChanged, self, spec)
end
for _, mdl in self:GetModules() do specChangedWithCheck(mdl, spec) end
end
local function specChanged(self, spec)
if not _Logined then return end
safeCall(OnSpecChanged, self, spec)
for _, mdl in self:GetModules() do specChanged(mdl, spec) end
end
local function warmodeChangedWithCheck(self, mode)
if _NotLoaded[self] then
safeCall(OnWarModeChanged, self, mode)
end
for _, mdl in self:GetModules() do warmodeChangedWithCheck(mdl, mode) end
end
local function warmodeChanged(self, mode)
if not _Logined then return end
safeCall(OnWarModeChanged, self, mode)
for _, mdl in self:GetModules() do warmodeChanged(mdl, mode) end
end
local function clearNotLoaded(self)
_NotLoaded[self] = nil
for _, mdl in self:GetModules() do clearNotLoaded(mdl) end
end
local function tryloading(self)
if _Logined then
loadingWithoutClear(self)
enablingWithCheck(self)
specChangedWithCheck(self, _PlayerSpec)
warmodeChangedWithCheck(self, _PlayerWarMode)
clearNotLoaded(self)
else
return loading(self)
end
end
----------------------------------------------
-- Scorpio Manager --
----------------------------------------------
function ScorpioManager:OnEvent(evt, ...)
if evt == "COMBAT_LOG_EVENT_UNFILTERED" then
callCombatHandlers( CombatLogGetCurrentEventInfo() )
end
local now = GetTime()
if now > g_PhaseStartTime then
g_PhaseStartTime = now
g_PhaseStartProfile = debugprofilestop()
end
local cache = t_EventTasks[evt]
local wcache = t_WaitEventTasks[evt]
-- event tasks
if cache then
t_EventTasks[evt] = nil
queueTask(NORMAL_PRIORITY, ThreadCall(processQueue, HIGH_PRIORITY, cache, ...))
end
-- wait event tasks
if wcache then
t_WaitEventTasks[evt] = nil
wcache[0] = nil
for i, v in ipairs(wcache) do
local task = w_Token[v]
if task then
w_Token[v] = nil
wcache[i] = task
else
wcache[i] = false
end
end
queueTask(NORMAL_PRIORITY, ThreadCall(processQueue, HIGH_PRIORITY, wcache, evt, ...))
end
-- Call direct handlers
return callAddonHandlers(_EventDistribution[evt], ...)
end
function ScorpioManager:OnUpdate()
local now = GetTime()
if now > g_PhaseStartTime then
g_PhaseStartTime = now
g_PhaseStartProfile = debugprofilestop()
end
-- Make sure unexpected error won't stop the whole task system
if now > g_Phase then g_InPhase = false end
local cache = t_DelayTasks
now = floor(now * 100)
while cache and cache[1] <= now do
local i = 2
local task = cache[i]
repeat
if type(task) == "number" then
local rtask = w_Token[task]
if rtask then
w_Token[task] = nil
end
task = rtask
end
if task then
resume(task, now)
queueTask(LOW_PRIORITY, task)
end
i = i + 1
task = cache[i]
until not task
local ncache = cache[0]
recycleCache(cache)
cache = ncache
end
t_DelayTasks = cache
return processPhase()
end
function ScorpioManager.ADDON_LOADED(name)
local addon = _RootAddon[name]
if addon then
tryloading(addon)
else
name = name:match("%P+")
addon = name and _RootAddon[name]
if addon then tryloading(addon) end
end
processService()
end
function ScorpioManager.PLAYER_LOGIN()
if _Logined then return end
--Log(2, "[START TASK MANAGER]")
--r_InLoadingScreen = false
_PlayerSpec = GetSpecialization() or 1
_PlayerWarMode = IsWarModeDesired() and WarMode.PVP or WarMode.PVE
_Logined = true
for _, addon in pairs(_RootAddon) do
enabling(addon)
specChanged(addon, _PlayerSpec)
warmodeChanged(addon, _PlayerWarMode)
end
processService()
end
function ScorpioManager.PLAYER_LOGOUT()
Log(2, "[STOP TASK MANAGER]")
r_InLoadingScreen = true
for _, addon in pairs(_RootAddon) do
exiting(addon)
end
end
function ScorpioManager.PLAYER_SPECIALIZATION_CHANGED(unit)
if not unit or UnitIsUnit(unit, "player") then
local spec = GetSpecialization() or 1
if _PlayerSpec ~= spec then
_PlayerSpec = spec
for _, addon in pairs(_RootAddon) do
specChanged(addon, spec)
end
end
end
end
function ScorpioManager.PLAYER_ENTERING_WORLD()
Log(2, "[RESUME TASK MANAGER]")
if r_InBattleField then
C_Timer.After(r_DelayResumeForBF, function() r_InLoadingScreen = false r_InBattleField = false end)
else
r_InLoadingScreen = false
end
ScorpioManager.PLAYER_SPECIALIZATION_CHANGED()
end
function ScorpioManager.PLAYER_FLAGS_CHANGED()
local mode = IsWarModeDesired() and WarMode.PVP or WarMode.PVE
if _PlayerWarMode ~= mode then
_PlayerWarMode = mode
for _, addon in pairs(_RootAddon) do
warmodeChanged(addon, mode)
end
end
end
-- Stop the task system when loading screen
function ScorpioManager.LOADING_SCREEN_ENABLED()
Log(2, "[SUSPEND TASK MANAGER]")
r_InLoadingScreen = true
end
function ScorpioManager.LOADING_SCREEN_DISABLED()
Log(2, "[RESUME TASK MANAGER]")
if r_InBattleField then
C_Timer.After(r_DelayResumeForBF, function() r_InLoadingScreen = false r_InBattleField = false end)
else
r_InLoadingScreen = false
end
end
hooksecurefunc(_G, "AcceptBattlefieldPort", function(data, accepted)
if accepted then
Log(2, "[SUSPEND TASK MANAGER]")
r_InBattleField = true
r_InLoadingScreen = true
end
end)
----------------------------------------------
-- System Event Method --
----------------------------------------------
--- Register system event or custom event
-- @param event string, the system|custom event name
-- @param handler string|function, the event handler or its name
__Arguments__{ NEString, (NEString + Function)/nil }:Throwable()
function RegisterEvent(self, evt, handler)
local map = _EventDistribution[evt]
if not map then
pcall(ScorpioManager.RegisterEvent, ScorpioManager, evt)
map = setmetatable({}, META_WEAKKEY)
_EventDistribution[evt] = map
end
handler = handler or evt
if type(handler) == "string" then handler = self[handler] end
if type(handler) ~= "function" then throw("Scorpio:RegisterEvent(event[, handler]) -- handler not existed.") end
map[self] = handler
end
--- Whether the system event or custom event is registered
--@param event string, the system|custom event name
--@return boolean true if the event is registered
__Arguments__{ NEString }
function IsEventRegistered(self, evt)
local map = _EventDistribution[evt]
return map and map[self] and true or false
end
--- Get the registered handler of an event
--@param event string, the system|custom event name
--@return boolean true if the event is registered
__Arguments__{ NEString }
function GetRegisteredEventHandler(self, evt)
local map = _EventDistribution[evt]
return map and map[self]
end
--- Unregister system event or custom event
--@param event string, the system|custom event name
__Arguments__{ NEString }
function UnregisterEvent(self, evt)
local map = _EventDistribution[evt]
if map and map[self] then
map[self] = nil
end
end
--- Unregister all the events
function UnregisterAllEvents(self)
for evt, map in pairs(_EventDistribution) do
if map[self] then
map[self] = nil
end
end
end
--- Fire the system event
--@param event string, the event's name
--@param ... the other arguments
function FireSystemEvent(self, ...)
if type(self) == "string" then
return ScorpioManager:OnEvent(self, ...)
elseif type(self) == "table" and type(select(1, ...)) == "string" then
return ScorpioManager:OnEvent(...)
end
end
----------------------------------------------
-- System Combat Method --
----------------------------------------------
--- Register combat event or custom event
-- @param event string, the combat|custom event name
-- @param handler string|function, the event handler or its name
__Arguments__{ NEString, (NEString + Function)/nil }:Throwable()
function RegisterCombatEvent(self, evt, handler)
local map = _CombatEventDistribution[evt]
if not map then
map = setmetatable({}, META_WEAKKEY)
_CombatEventDistribution[evt] = map
end
handler = handler or evt
if type(handler) == "string" then handler = self[handler] end
if type(handler) ~= "function" then throw("Scorpio:RegisterCombatEvent(event[, handler]) -- handler not existed.") end
map[self] = handler
end
--- Whether the combat event or custom event is registered
--@param event string, the combat|custom event name
--@return boolean true if the event is registered
__Arguments__{ NEString }
function IsCombatEventRegistered(self, evt)
local map = _CombatEventDistribution[evt]
return map and map[self] and true or false
end
--- Get the registered handler of an event
--@param event string, the combat|custom event name
--@return boolean true if the event is registered
__Arguments__{ NEString }
function GetRegisteredCombatEventHandler(self, evt)
local map = _CombatEventDistribution[evt]
return map and map[self]
end
--- Unregister combat event or custom event
--@param event string, the combat|custom event name
__Arguments__{ NEString }
function UnregisterCombatEvent(self, evt)
local map = _CombatEventDistribution[evt]
if map and map[self] then
map[self] = nil
end
end
--- Unregister all the events
function UnregisterAllCombatEvents(self)
for evt, map in pairs(_CombatEventDistribution) do
if map[self] then
map[self] = nil
end
end
end
--- Fire the combat event
--@param event string, the event's name
--@param ... the other arguments
function FireCombatEvent(self, arg1, ...)
if type(self) == "string" then
return ScorpioManager:OnEvent("COMBAT_LOG_EVENT_UNFILTERED", time(), self, arg1, ...)
elseif type(arg1) == "string" then
return ScorpioManager:OnEvent("COMBAT_LOG_EVENT_UNFILTERED", time(), arg1, ...)
end
end
----------------------------------------------
-- Secure Hook System Method --
----------------------------------------------
--- Secure hook a table's function
--@format [target, ]targetFunction[, handler]
--@param target table, the target table, default _G
--@param targetFunction string, the hook function name
--@param handler string, the hook handler
__Arguments__{ Table, NEString, (NEString + Function)/nil }:Throwable()
function SecureHook(self, target, targetFunc, handler)
handler = handler or targetFunc
if type(handler) == "string" then handler = self[handler] end
if type(handler) ~= "function" then throw("Scorpio:SecureHook([target, ]targetFunc[, handler]) -- handler not existed.") end
getSecureHookMap(target, targetFunc)[self] = handler
end
__Arguments__{ NEString, (NEString + Function)/nil }:Throwable()
function SecureHook(self, targetFunc, handler)
if type(_G[targetFunc]) ~= "function" then
throw(("No method named '%s' can be found."):format(targetFunc))
end
handler = handler or targetFunc
if type(handler) == "string" then handler = self[handler] end
if type(handler) ~= "function" then throw("Scorpio:SecureHook([target, ]targetFunc[, handler]) -- handler not existed.") end
getSecureHookMap(_G, targetFunc)[self] = handler
end
--- Un-hook a table's function
--@format [target, ]targetFunction
--@param target table, the target table, default _G
--@param targetFunction string, the hook function name
__Arguments__{ Table, NEString }
function SecureUnHook(self, target, targetFunc)
if _SecureHookDistribution[target] and _SecureHookDistribution[target][targetFunc] then
_SecureHookDistribution[target][targetFunc][self] = nil
end
end
__Arguments__{ NEString }
function SecureUnHook(self, targetFunc) return SecureUnHook(self, _G, targetFunc) end
--- Un-hook all functions
function SecureUnHookAll(self) for _, target in pairs(_SecureHookDistribution) do for _, map in pairs(target) do map[self] = nil end end end
--- Get the secure hook handler
__Arguments__{ Table, NEString }
function GetSecureHookHandler(self, target, targetFunc)
local map = _SecureHookDistribution[target] and _SecureHookDistribution[target][targetFunc]
return map and map[self] or nil
end
__Arguments__{ NEString }
function GetSecureHookHandler(self, targetFunc)
local map = _SecureHookDistribution[_G] and _SecureHookDistribution[_G][targetFunc]
return map and map[self] or nil
end
----------------------------------------------
-- Slash Command Method --
----------------------------------------------
--- Register a slash command with handler
--@param slashCmd string, the slash command, case ignored
--@param handler string|function, handler
__Arguments__{ NEString, NEString + Function }:Throwable()
function RegisterSlashCommand(self, slashCmd, handler)
slashCmd = slashCmd:upper():match("^/?(%w+)")
if slashCmd == "" then throw("The slash command can only be letters and numbers.") end
slashCmd = "/" .. slashCmd
local map = _SlashCmdHandler[slashCmd]
if not map then
map = {}
_SlashCmdHandler[slashCmd] = map
newSlashCmd(slashCmd, map)
end
if type(handler) == "string" then handler = self[handler] end
if type(handler) ~= "function" then throw("Scorpio:RegisterSlashCommand(slashCmd, handler) -- handler not existed.") end
map[0] = handler
end
--- Register a slash command with handler
--@param slashCmd string, the slash command, case ignored
--@param option string, the slash command option, case ignored
--@param handler string|function, handler
__Arguments__{ NEString, NEString, NEString + Function, NEString/nil }:Throwable()
function RegisterSlashCmdOption(self, slashCmd, option, handler, desc)
slashCmd = slashCmd:upper():match("^/?(%w+)")
if slashCmd == "" then throw("The slash command can only be letters and numbers.") end
slashCmd = "/" .. slashCmd
option = option:upper():match("^%w+")
if not option or option == "" then throw("The slash command option can only be letters and numbers.") end
local map = _SlashCmdHandler[slashCmd]
if not map then
map = {}
_SlashCmdHandler[slashCmd] = map
newSlashCmd(slashCmd, map)
end
if type(handler) == "string" then handler = self[handler] end
if type(handler) ~= "function" then throw("Scorpio:RegisterSlashCmdOption(slashCmd, option, handler[, description]) -- handler not existed.") end
map[option] = handler
map[option .. "-desc"] = desc
end
----------------------------------------------
-- Task System Method --
----------------------------------------------
---Call method or continue thread with high priority, the method(thread) should be called(resumed) as soon as possible.
--@format [func[, ...]]
--@param func The function
--@param ... method parameter
__Arguments__{ Function, Any * 0 }
__Static__() function Continue(func, ...)
return queueTask(HIGH_PRIORITY, wrapAsSimpleTask(func, ...))
end
__Arguments__{ }
__Static__() function Continue()
local thread = running()
if not thread then error("Scorpio.Continue() can only be used in a thread.", 2) end
queueTask(HIGH_PRIORITY, thread)
return yield()
end
---Call method or resume thread with normal priority, the method(thread) should be called(resumed) in the next phase.
--@format [func[, ...]]
--@param func The function
--@param ... method parameter
__Arguments__{ Function, Any * 0 }
__Static__() function Next(func, ...)
return queueTask(NORMAL_PRIORITY, wrapAsSimpleTask(func, ...))
end
__Arguments__{ IObservable }
__Static__() function Next(observable)
local thread = running()
if not thread then error("Scorpio.Next(observable) can only be used in a thread.", 2) end
local obNext = rycNextObserver()
obNext.NextThread = thread
observable:Subscribe(obNext)
return yieldReturn(yield())
end
__Arguments__{ }
__Static__() function Next()
local thread = running()
if not thread then error("Scorpio.Next() can only be used in a thread.", 2) end
queueTask(NORMAL_PRIORITY, thread)
return yield()
end
--- Call method|yield current thread and resume it after several seconds
--@format delay[, func[, ...]]
--@param delay the time to delay
--@param func The function
--@param ... method parameter
__Arguments__{ Number, Function, Any * 0 }
__Static__() function Delay(delay, func, ...)
return queueDelayTask(wrapAsSystemTask(func, ...), delay)
end
__Arguments__{ Number }
__Static__() function Delay(delay)
local thread = running()
if not thread then error("Scorpio.Delay(delay) can only be used in a thread.", 2) end
queueDelayTask(thread, delay)
return yieldReturn(yield())
end
--- Call method|yield current thread and resume it after special system event
--@format event[, func[, ...]]
--@param event the system event name
--@param func The function
--@param ... method parameter
__Arguments__{ NEString, Function, Any * 0 }
__Static__() function NextEvent(event, func, ...)
return queueEventTask(wrapAsSystemTask(func, ...), event)
end
__Arguments__{ NEString }
__Static__() function NextEvent(event)
local thread = running()
if not thread then error("Scorpio.NextEvent(event) can only be used in a thread.", 2) end
queueEventTask(thread, event)
return yieldReturn(yield())
end
--- Call method|yield current thread when not in combat
--@format [func[, ...]]
--@param func The function
--@param ... the arguments
__Arguments__{ Function, Any * 0 }
__Static__() function NoCombat(func, ...)
if not InCombatLockdown() then return ThreadCall(func, ...) end
return NextEvent("PLAYER_REGEN_ENABLED", noCombatCall, func, ...)
end
__Arguments__{ }
__Static__() function NoCombat()
if not InCombatLockdown() then return end
if not running() then error("Scorpio.NoCombat() can only be used in a thread.", 2) end
NextEvent("PLAYER_REGEN_ENABLED")
while InCombatLockdown() do Next() end
end
--- Call method|yield current thread and resume it after special system events or time delay
--@format [func, ][waitTime, ][event, ...]
--@param func The function
--@param waitTime the time to wait
--@param event the system event name
__Arguments__{ Function, Number, NEString * 0 }
__Static__() function Wait(func, delay, ...)
return queueWaitTask(wrapAsSystemTask(func), delay, ...)
end
__Arguments__{ Function, NEString * 1 }
__Static__() function Wait(func, ...)
return queueWaitTask(wrapAsSystemTask(func), nil, ...)
end
__Arguments__{ Number, NEString * 0 }
__Static__() function Wait(delay, ...)
local thread = running()
if not thread then error("Scorpio.Wait([waitTime, ][event, ...]) can only be used in a thread.", 2) end
queueWaitTask(thread, delay, ...)
return yieldReturn(yield())
end
__Arguments__{ NEString * 1 }
__Static__() function Wait(...)
local thread = running()
if not thread then error("Scorpio.Wait([waitTime, ][event, ...]) can only be used in a thread.", 2) end
queueWaitTask(thread, nil, ...)
return yieldReturn(yield())
end
--- Call method|yield current thread and resume it after secure object-method call
--@format [func, ][target, ]targetFunction[, ...]
--@param func The function
--@param target table, the target table, default _G
--@param targetFunction string, the target table's method name
--@param ... custom params if you don't need real params of the method-call
__Arguments__{ Function, Table, NEString, Any * 0}
__Static__() function NextSecureCall(func, target, targetFunc, ...)
return queueNextSecureCall(wrapAsSystemTask(func, ...), target, targetFunc)
end
__Arguments__{ Function, NEString, Any * 0}
__Static__() function NextSecureCall(func, targetFunc, ...)
return queueNextSecureCall(wrapAsSystemTask(func, ...), _G, targetFunc)
end
__Arguments__{ Table, NEString }
__Static__() function NextSecureCall(target, targetFunc)
local thread = running()
if not thread then error("Scorpio.NextSecureCall([target, ]targetFunc) can only be used in a thread.", 2) end
queueNextSecureCall(thread, target, targetFunc)
return yieldReturn(yield())
end
__Arguments__{ NEString }
__Static__() function NextSecureCall(targetFunc)
local thread = running()
if not thread then error("Scorpio.NextSecureCall([target, ]targetFunc) can only be used in a thread.", 2) end
queueNextSecureCall(thread, _G, targetFunc)
return yieldReturn(yield())
end
--- Run a method as service
__Arguments__{ Function, Boolean/nil }
__Static__() function RunAsService(func, resident)
registerService(func, resident)
processService()
end
----------------------------------------------
-- Event --
----------------------------------------------
--- Fired when the addon(module) and it's saved variables is loaded
event "OnLoad"
--- Fired when player specialization changed
event "OnSpecChanged"
--- Fired when player toggle the war mode
event "OnWarModeChanged"
--- Fired when the addon(module) is enabled
event "OnEnable"
--- Fired when the addon(module) is disabled
event "OnDisable"
--- Fired when the player log out
event "OnQuit"
----------------------------------------------
-- Static Property --
----------------------------------------------
--- The max task operation time per phase(ms)
__Static__() property "TaskThreshold" { type = Integer, get = function() return PHASE_THRESHOLD end, set = function(self, val) PHASE_THRESHOLD = Clamp(val or 0, 5, 100) end }
--- The factor used to calculate the task operation time per phase
__Static__() property "TaskFactor" { type = Number, get = function() return PHASE_TIME_FACTOR end, set = function(self, val) PHASE_TIME_FACTOR = Clamp(val or 0, 0.1, 1) end }
--- The factor used to calculate the task operation time for remain tasks from the previous phase
__Static__() property "OvertimeFactor" { type = Number, get = function() return PHASE_OVERTIME_FACTOR end, set = function(self, val) PHASE_OVERTIME_FACTOR = Clamp(val or 0, 0.1, 1) end }
--- Whether the task schedule system is suspended
__Static__() property "SystemSuspended" { type = Boolean, get = function() return r_InLoadingScreen end, set = function(self, val) r_InLoadingScreen = val end }
--- Whether the game is retail version
__Static__() property "IsRetail" { type = Boolean, default = function() return _G.WOW_PROJECT_ID == _G.WOW_PROJECT_MAINLINE end }
--- Whether the game is classic version
__Static__() property "IsClassic" { type = Boolean, default = function() return _G.WOW_PROJECT_ID == _G.WOW_PROJECT_CLASSIC end }
--- Whether the game is classic burning crusade
__Static__() property "IsBCC" { type = Boolean, default = function() return _G.WOW_PROJECT_ID == _G.WOW_PROJECT_BURNING_CRUSADE_CLASSIC end }
----------------------------------------------
-- Property --
----------------------------------------------
--- Whether the module is enabled
property "_Enabled" { type = Boolean, default = true, handler = function(self, val) if val then return tryEnable(self) else return disabling(self) end end }
--- Whether the module is disabled by itself or it's parent
property "_Disabled" { get = function (self) return _DisabledModule[self] or false end }
--- Whether the module is already loaded with saved variables
property "_Loaded" { get = function(self) return not _NotLoaded[self] end }
--- The addon of the module
property "_Addon" { get = function(self) while self._Parent do self = self._Parent end return self end }
----------------------------------------------
-- Dispose --
----------------------------------------------
function Dispose(self)
self:UnregisterAllEvents()
self:SecureUnHookAll()
for _, map in pairs(_SlashCmdHandler) do
for k, smap in pairs(map) do
if smap[self] then
map[k] = nil
end
end
end
_RootAddon[self._Name] = nil
_NotLoaded[self] = nil
_DisabledModule[self] = nil
end
----------------------------------------------
-- Constructor --
----------------------------------------------
function Scorpio(self, ...)
super(self, ...)
_NotLoaded[self] = true
if not self._Parent then
-- Means this is an addon
_RootAddon[self._Name] = self
elseif _DisabledModule[self._Parent] then
-- Register disabled modules
_DisabledModule[self] = true
end
end
----------------------------------------------
-- Attributes --
----------------------------------------------
--- Register a system event with a handler, the handler's name is the event name
-- @usage
-- Scorpio "MyAddon" "v1.0.1"
--
-- __SystemEvent__()
-- function ADDON_LOADED(name)
-- print("Addon " .. name .. " is loaded.")
-- end
--
-- __SystemEvent__ "PLAYER_LOGIN" "PLAYER_LOGOUT"
-- function PLAYER_LOGIN(...)
-- -- The event's name won't be passed to it
-- end
__Sealed__()
class "__SystemEvent__" (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
owner:RegisterEvent(evt, target)
end
else
owner:RegisterEvent(name, target)
end
else
error("__SystemEvent__ 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)
--- Register a combat event with a handler, the handler's name is the event name
-- @usage
-- Scorpio "MyAddon" "v1.0.1"
--
-- __CombatEvent__()
-- function SPELL_CAST_START(...)
-- end
--
-- __CombatEvent__ "SPELL_CAST_SUCCESS" "SPELL_MISSED"
-- function SPELL_CAST(...)
-- end
class "__CombatEvent__" (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
owner:RegisterCombatEvent(evt, target)
end
else
owner:RegisterCombatEvent(name, target)
end
else
error("__CombatEvent__ 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)
--- Mark the method as a hook
-- @usage
-- Scorpio "MyAddon" "v1.0.1"
--
-- -- Modify each buff button's texture when they are created
-- __SecureHook__()
-- function BuffButton_OnLoad(self)
--
-- -- We should secure hook the texture's SetTexture to do modifiy
-- -- Normally, these should be done like
-- -- _M:SecureHook(_G[self:GetName() .. "Icon"], "SetTexture", HookSetTexture)
-- -- Just for an example
-- __SecureHook__(_G[self:GetName() .. "Icon"])
-- function SetTexture(self, path)
-- if path then
-- self:SetTexCoord(0.06, 0.94, 0.06, 0.94)
-- end
-- end
-- end
__Sealed__()
class "__SecureHook__" (function(_ENV)
extend "IAttachAttribute"
function AttachAttribute(self, target, targettype, owner, name, stack)
if Class.IsObjectType(owner, Scorpio) then
owner:SecureHook(self.Target, self.TargetFunc or name, target)
else
error("__SecureHook__ can only be applyed to objects of Scorpio.", stack + 1)
end
end
----------------------------------------------
-- Property --
----------------------------------------------
property "AttributeTarget" { default = AttributeTargets.Function }
----------------------------------------------
-- Constructor --
----------------------------------------------
__Arguments__{ Table, NEString/nil }
function __SecureHook__(self, target, targetFunc)
self.Target = target
self.TargetFunc = targetFunc
end
__Arguments__{ NEString/nil }
function __SecureHook__(self, targetFunc)
self.Target = _G
self.TargetFunc = targetFunc
end
end)
--- Register a slash cmd with handler
-- @usage>
-- Scorpio "MyAddon" "v1.0.1"
--
-- Log = Logger("MyAddon")
--
-- -- "/myaddon log 1" used to change the addon's log level
-- __SlashCmd__ "/myaddon" "log"
-- function TurnLog(lvl)
-- Log.LogLevel = tonumber(lvl) or 2
-- end
--
-- -- "/myaddon" default slash command handler, used to display the detail
-- __SlashCmd__ "/myaddon"
-- function SlashCmd(msg)
-- print("/myaddon log N - change the log level")
-- end
__Sealed__()
class "__SlashCmd__" (function(_ENV)
extend "IAttachAttribute"
function AttachAttribute(self, target, targettype, owner, name, stack)
if Class.IsObjectType(owner, Scorpio) then
if not self.SlashOpt then
owner:RegisterSlashCommand(self.SlashCmd, target)
else
owner:RegisterSlashCmdOption(self.SlashCmd, self.SlashOpt, target, self.SlashDesc)
end
else
error("__SlashCmd__ can only be applyed to objects of Scorpio.", stack + 1)
end
end
----------------------------------------------
-- Property --
----------------------------------------------
property "AttributeTarget" { default = AttributeTargets.Function }
property "SlashCmd" { type = NEString }
property "SlashOpt" { type = NEString }
property "SlashDesc" { type = NEString}
----------------------------------------------
-- Constructor --
----------------------------------------------
__Arguments__{ NEString, NEString/nil, NEString/nil }
function __SlashCmd__(self, slashCmd, slashOpt, slashDesc)
self.SlashCmd = slashCmd
self.SlashOpt = slashOpt
self.SlashDesc = slashDesc
end
----------------------------------------------
-- Meta-Method --
----------------------------------------------
__Arguments__{ NEString }
function __call(self, str)
if not self.SlashOpt then
self.SlashOpt = str
return self
elseif not self.SlashDesc then
self.SlashDesc = str
end
end
end)
--- Mark the method so it only be called when the player is not in combat
__Sealed__()
class "__NoCombat__" (function(_ENV)
function __NoCombat__(self)
local del = __Delegate__(NoCombat)
del.Priorty = AttributePriority.Lower
del.SubLevel = -999
end
end)
--- Mark the method as a secure hook when the target's addon is loaded
-- @usage
-- Scorpio "MyAddon" "v1.0.1"
--
-- -- Secure hook 'AuctionFrameTab_OnClick' when Blizzard_AuctionUI loaded
-- __AddonSecureHook__ "Blizzard_AuctionUI"
-- function AuctionFrameTab_OnClick(self, button, down, index)
-- end
--
-- __AddonSecureHook__ "Blizzard_AuctionUI" "AuctionFrameTab_OnClick"
-- function Hook_Blizzard_AuctionUI(self, button, down, index)
-- end
__Sealed__()
class "__AddonSecureHook__" (function(_ENV)
extend "IAttachAttribute"
function AttachAttribute(self, target, targettype, owner, name, stack)
if Class.IsObjectType(owner, Scorpio) then
local addon = self.Addon
if IsAddOnLoaded(addon) then
owner:SecureHook(self.Target, self.TargetFunc or name, target)
else
local targetTbl = self.Target
local targetFunc = self.TargetFunc or name
ThreadCall(function()
while NextEvent("ADDON_LOADED") ~= addon do end
owner:SecureHook(targetTbl, targetFunc, target)
end)
end
else
error("__AddonSecureHook__ can only be applyed to objects of Scorpio.", stack + 1)
end
end
----------------------------------------------
-- Property --
----------------------------------------------
property "AttributeTarget" { default = AttributeTargets.Function }
----------------------------------------------
-- Constructor --
----------------------------------------------
__Arguments__{ NEString, NEString/nil }
function __AddonSecureHook__(self, addon, targetFunc)
self.Addon = addon
self.Target = _G
self.TargetFunc = targetFunc
end
end)
--- Mark the method as an async service so it'd be automatically processed
-- when the addon is loaded, and the system will try to re-process it if the
-- process is dead as required
-- @usage
-- Scorpio "MyAddon" "v1.0.1"
--
-- __Service__(true) -- auto restarted
-- function MyService()
-- while Wait("PLAYER_FLAGS_CHANGED") do
-- print("The pvp mode is " .. (C_PvP.IsWarModeDesired() and "active" or "deactive"))
-- end
-- end
__Sealed__()
class "__Service__" (function(_ENV)
extend "IAttachAttribute"
-----------------------------------------------------------
-- method --
-----------------------------------------------------------
function AttachAttribute(self, target, targettype, owner, name, stack)
registerService(target, self[1])
end
-----------------------------------------------------------
-- property --
-----------------------------------------------------------
--- the attribute target
property "AttributeTarget" { type = AttributeTargets, default = AttributeTargets.Method + AttributeTargets.Function }
-----------------------------------------------------------
-- constructor --
-----------------------------------------------------------
__Arguments__{ Boolean/nil }
function __Service__(self, flag)
self[1] = flag
end
end)
--- Mark the async method that can only be run one copy in the same time.
-- So if the method is still running, another call will be cancelled.
-- We can also change the behavior by set the override flag to true, so
-- when call the method again, the previous call will be cancelled.
__Sealed__()
class "__AsyncSingle__" (function(_ENV)
extend "IInitAttribute"
-----------------------------------------------------------
-- method --
-----------------------------------------------------------
--- modify the target's definition
-- @param target the target
-- @param targettype the target type
-- @param definition the target's definition
-- @param owner the target's owner
-- @param name the target's name in the owner
-- @param stack the stack level
-- @return definition the new definition
function InitDefinition(self, target, targettype, definition, owner, name, stack)
if targettype == AttributeTargets.Method and (Class.Validate(owner) or Inteface.Validate(owner)) then
return registerSingleAsync(definition, self[1], owner, name)
else
return registerSingleAsync(definition, self[1])
end
end
-----------------------------------------------------------
-- property --
-----------------------------------------------------------
--- the attribute target
property "AttributeTarget" { type = AttributeTargets, default = AttributeTargets.Method + AttributeTargets.Function }
--- the attribute's priority
property "Priority" { type = AttributePriority, default = AttributePriority.Lower }
-----------------------------------------------------------
-- constructor --
-----------------------------------------------------------
function __new(_, override) return { override }, true end
end)
----------------------------------------------
-- System Prepare --
----------------------------------------------
Environment.RegisterGlobalNamespace("Scorpio")
ScorpioManager:SetScript("OnEvent", ScorpioManager.OnEvent)
ScorpioManager:SetScript("OnUpdate", ScorpioManager.OnUpdate)
ScorpioManager:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
RegisterEvent(ScorpioManager, "ADDON_LOADED")
RegisterEvent(ScorpioManager, "PLAYER_LOGIN")
RegisterEvent(ScorpioManager, "PLAYER_LOGOUT")
RegisterEvent(ScorpioManager, "PLAYER_SPECIALIZATION_CHANGED")
RegisterEvent(ScorpioManager, "PLAYER_ENTERING_WORLD")
RegisterEvent(ScorpioManager, "PLAYER_FLAGS_CHANGED")
RegisterEvent(ScorpioManager, "LOADING_SCREEN_DISABLED")
RegisterEvent(ScorpioManager, "LOADING_SCREEN_ENABLED")
-- Clear canceld event tasks
ThreadCall(function()
while true do
local now = GetTime()
for evt, cache in pairs(t_WaitEventTasks) do
if cache[0] >= now then
local cnt = 0
for i = #cache, 1, -1 do
if not w_Token[cache[i]] then
cnt = cnt + 1
tremove(cache, i)
end
end
if cnt > 0 then Log(1, "Clear %d tasks for %s", cnt, evt) end
if #cache == 0 then
-- Only clear one cache at one time to avoid un-valid key error
t_WaitEventTasks[evt] = nil
recycleCache(cache)
Log(1, "Recycle %s task cache", evt)
break
else
-- Wait to next cycle
cache[0] = now + EVENT_CLEAR_INTERVAL
end
-- Check if need contine
Continue()
end
end
Delay(EVENT_CLEAR_DELAY)
end
end)
-- Task System Diagnose
ThreadCall(function()
while true do
Log(1, "--======================--")
Log(1, "[Delayed] %d", g_DelayedTask)
Log(1, "[Average] %.2f ms", g_AverageTime)
Log(1, "[Max Phase] %.2f ms", g_MaxPhaseTime)
Log(1, "[Cache][Generated] %d", g_CacheGenerated)
Log(1, "[Cache][Remain] %d", g_CacheRamain)
Log(1, "--======================--")
g_DelayedTask = 0
g_MaxPhaseTime = 0
g_CacheGenerated = 0
Delay(DIAGNOSE_DELAY)
end
end)
----------------------------------------------
-- Locale --
----------------------------------------------
__Sealed__()
class "Localization" (function(_ENV)
_Localizations = {}
enum "Locale" {
"deDE", -- German
--"enGB", -- British English
"enUS", -- American English
"esES", -- Spanish (European)
"esMX", -- Spanish (Latin American)
"itIT", -- Italian (Italy)
"frFR", -- French
"koKR", -- Korean
"ptBR", -- Portuguese (Brazil)
"ruRU", -- Russian
"zhCN", -- Chinese (simplified; mainland China)
"zhTW", -- Chinese (traditional; Taiwan)
}
----------------------------------------------
----------------- Constructor ----------------
----------------------------------------------
__Arguments__{ NEString }
function Localization(self, name)
_Localizations[name] = self
end
----------------------------------------------
----------------- Meta-Method ----------------
----------------------------------------------
__Arguments__{ NEString }
function __exist(_, name)
return _Localizations[name]
end
__Arguments__{ String }
function __index(self, key)
rawset(self, key, key)
return key
end
__Arguments__{ String + Number, String }
function __newindex(self, key, value)
rawset(self, key, value)
end
__Arguments__{ String, Boolean }
function __newindex(self, key, value)
rawset(self, key, key)
end
__Arguments__ { Variable("language", Locale), Variable("asDefault", Boolean, true) }
function __call(self, language, asDefault)
if not asDefault then
local locale = GetLocale()
if locale == "enGB" then locale = "enUS" end
if locale ~= language then return end
end
return self
end
end)
------------------------------------------------------------
-- [Property]Scorpio._Locale --
------------------------------------------------------------
property "_Locale" { set = false, default = function(self) return Localization(self._Addon._Name) end }
end)
end)