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.
280 lines
8.6 KiB
280 lines
8.6 KiB
local _, private = ...
|
|
|
|
local twipe, tremove = table.wipe, table.remove
|
|
local floor = math.floor
|
|
local GetTime = GetTime
|
|
local pairs, next = pairs, next
|
|
local LastInstanceMapID = -1
|
|
|
|
local schedulerFrame = CreateFrame("Frame", "DBMScheduler")
|
|
schedulerFrame:Hide()
|
|
|
|
local module = private:NewModule("DBMScheduler")
|
|
|
|
--------------------------
|
|
-- OnUpdate/Scheduler --
|
|
--------------------------
|
|
|
|
-- stack that stores a few tables (up to 8) which will be recycled
|
|
local popCachedTable, pushCachedTable
|
|
local numChachedTables = 0
|
|
do
|
|
local tableCache
|
|
|
|
-- gets a table from the stack, it will then be recycled.
|
|
function popCachedTable()
|
|
local t = tableCache
|
|
if t then
|
|
tableCache = t.next
|
|
numChachedTables = numChachedTables - 1
|
|
end
|
|
return t
|
|
end
|
|
|
|
-- tries to push a table on the stack
|
|
-- only tables with <= 4 array entries are accepted as cached tables are only used for tasks with few arguments as we don't want to have big arrays wasting our precious memory space doing nothing...
|
|
-- also, the maximum number of cached tables is limited to 8 as DBM rarely has more than eight scheduled tasks with less than 4 arguments at the same time
|
|
-- this is just to re-use all the tables of the small tasks that are scheduled all the time (like the wipe detection)
|
|
-- note that the cache does not use weak references anywhere for performance reasons, so a cached table will never be deleted by the garbage collector
|
|
function pushCachedTable(t)
|
|
if numChachedTables < 8 and #t <= 4 then
|
|
twipe(t)
|
|
t.next = tableCache
|
|
tableCache = t
|
|
numChachedTables = numChachedTables + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- priority queue (min-heap) that stores all scheduled tasks.
|
|
-- insert: O(log n)
|
|
-- deleteMin: O(log n)
|
|
-- getMin: O(1)
|
|
-- removeAllMatching: O(n)
|
|
local insert, removeAllMatching, getMin, deleteMin
|
|
do
|
|
local heap = {}
|
|
local firstFree = 1
|
|
|
|
-- gets the next task
|
|
function getMin()
|
|
return heap[1]
|
|
end
|
|
|
|
-- restores the heap invariant by moving an item up
|
|
local function siftUp(n)
|
|
local parent = floor(n / 2)
|
|
while n > 1 and heap[parent].time > heap[n].time do -- move the element up until the heap invariant is restored, meaning the element is at the top or the element's parent is <= the element
|
|
heap[n], heap[parent] = heap[parent], heap[n] -- swap the element with its parent
|
|
n = parent
|
|
parent = floor(n / 2)
|
|
end
|
|
end
|
|
|
|
-- restores the heap invariant by moving an item down
|
|
local function siftDown(n)
|
|
local m -- position of the smaller child
|
|
while 2 * n < firstFree do -- #children >= 1
|
|
-- swap the element with its smaller child
|
|
if 2 * n + 1 == firstFree then -- n does not have a right child --> it only has a left child as #children >= 1
|
|
m = 2 * n -- left child
|
|
elseif heap[2 * n].time < heap[2 * n + 1].time then -- #children = 2 and left child < right child
|
|
m = 2 * n -- left child
|
|
else -- #children = 2 and right child is smaller than the left one
|
|
m = 2 * n + 1 -- right
|
|
end
|
|
if heap[n].time <= heap[m].time then -- n is <= its smallest child --> heap invariant restored
|
|
return
|
|
end
|
|
heap[n], heap[m] = heap[m], heap[n]
|
|
n = m
|
|
end
|
|
end
|
|
|
|
-- inserts a new element into the heap
|
|
function insert(ele)
|
|
heap[firstFree] = ele
|
|
siftUp(firstFree)
|
|
firstFree = firstFree + 1
|
|
end
|
|
|
|
-- deletes the min element
|
|
function deleteMin()
|
|
local min = heap[1]
|
|
firstFree = firstFree - 1
|
|
heap[1] = heap[firstFree]
|
|
heap[firstFree] = nil
|
|
siftDown(1)
|
|
return min
|
|
end
|
|
|
|
-- removes multiple scheduled tasks from the heap
|
|
-- note that this function is comparatively slow by design as it has to check all tasks and allows partial matches
|
|
function removeAllMatching(f, mod, ...)
|
|
-- remove all elements that match the signature, this destroyes the heap and leaves a normal array
|
|
local v, match
|
|
local foundMatch = false
|
|
for i = #heap, 1, -1 do -- iterate backwards over the array to allow usage of table.remove
|
|
v = heap[i]
|
|
if (not f or v.func == f) and (not mod or v.mod == mod) then
|
|
match = true
|
|
for j = 1, select("#", ...) do
|
|
if select(j, ...) ~= v[j] then
|
|
match = false
|
|
break
|
|
end
|
|
end
|
|
if match then
|
|
tremove(heap, i)
|
|
firstFree = firstFree - 1
|
|
foundMatch = true
|
|
end
|
|
end
|
|
end
|
|
-- rebuild the heap from the array in O(n)
|
|
if foundMatch then
|
|
for i = floor((firstFree - 1) / 2), 1, -1 do
|
|
siftDown(i)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local wrappers = {}
|
|
local function range(max, cur, ...)
|
|
cur = cur or 1
|
|
if cur > max then
|
|
return ...
|
|
end
|
|
return cur, range(max, cur + 1, select(2, ...))
|
|
end
|
|
local function getWrapper(n)
|
|
wrappers[n] = wrappers[n] or loadstring(([[
|
|
return function(func, tbl)
|
|
return func(]] .. ("tbl[%s], "):rep(n):sub(0, -3) .. [[)
|
|
end
|
|
]]):format(range(n)))()
|
|
return wrappers[n]
|
|
end
|
|
|
|
local nextModSyncSpamUpdate = 0
|
|
--mainFrame:SetScript("OnUpdate", function(self, elapsed)
|
|
local function onUpdate(self, elapsed)
|
|
local time = GetTime()
|
|
|
|
-- execute scheduled tasks
|
|
local nextTask = getMin()
|
|
while nextTask and nextTask.func and nextTask.time <= time do
|
|
deleteMin()
|
|
local n = nextTask.n
|
|
if n == #nextTask then
|
|
nextTask.func(unpack(nextTask))
|
|
else
|
|
-- too many nil values (or a trailing nil)
|
|
-- this is bad because unpack will not work properly
|
|
-- TODO: is there a better solution?
|
|
getWrapper(n)(nextTask.func, nextTask)
|
|
end
|
|
pushCachedTable(nextTask)
|
|
nextTask = getMin()
|
|
end
|
|
|
|
-- execute OnUpdate handlers of all modules
|
|
local foundModFunctions = 0
|
|
for i, v in pairs(private.updateFunctions) do
|
|
foundModFunctions = foundModFunctions + 1
|
|
if i.Options.Enabled and (not i.zones or i.zones[LastInstanceMapID]) then
|
|
i.elapsed = (i.elapsed or 0) + elapsed
|
|
if i.elapsed >= (i.updateInterval or 0) then
|
|
v(i, i.elapsed)
|
|
i.elapsed = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
-- clean up sync spam timers and auto respond spam blockers
|
|
if time > nextModSyncSpamUpdate then
|
|
nextModSyncSpamUpdate = time + 20
|
|
-- TODO: optimize this; using next(t, k) all the time on nearly empty hash tables is not a good idea...doesn't really matter here as modSyncSpam only very rarely contains more than 4 entries...
|
|
-- we now do this just every 20 seconds since the earlier assumption about modSyncSpam isn't true any longer
|
|
-- note that not removing entries at all would be just a small memory leak and not a problem (the sync functions themselves check the timestamp)
|
|
local k, v = next(private.modSyncSpam, nil)
|
|
if v and (time - v > 8) then
|
|
private.modSyncSpam[k] = nil
|
|
end
|
|
end
|
|
if not nextTask and foundModFunctions == 0 then--Nothing left, stop scheduler
|
|
schedulerFrame:SetScript("OnUpdate", nil)
|
|
schedulerFrame:Hide()
|
|
end
|
|
end
|
|
|
|
function module:StartScheduler()
|
|
if not schedulerFrame:IsShown() then
|
|
schedulerFrame:Show()
|
|
schedulerFrame:SetScript("OnUpdate", onUpdate)
|
|
end
|
|
end
|
|
|
|
--For updating zone cache locally
|
|
--without needing to monitor for changes in onupdate functions or registering zone change events
|
|
function module:UpdateZone()
|
|
LastInstanceMapID = DBM and DBM:GetCurrentArea() or -1
|
|
end
|
|
|
|
local function schedule(t, f, mod, ...)
|
|
if type(f) ~= "function" then
|
|
error("usage: schedule(time, func, [mod, args...])", 2)
|
|
end
|
|
module:StartScheduler()
|
|
local v
|
|
if numChachedTables > 0 and select("#", ...) <= 4 then -- a cached table is available and all arguments fit into an array with four slots
|
|
v = popCachedTable()
|
|
v.time = GetTime() + t
|
|
v.func = f
|
|
v.mod = mod
|
|
v.n = select("#", ...)
|
|
for i = 1, v.n do
|
|
v[i] = select(i, ...)
|
|
end
|
|
-- clear slots if necessary
|
|
for i = v.n + 1, 4 do
|
|
v[i] = nil
|
|
end
|
|
else -- create a new table
|
|
v = {time = GetTime() + t, func = f, mod = mod, n = select("#", ...), ...}
|
|
end
|
|
insert(v)
|
|
end
|
|
|
|
--Boss mod prototype usage methods (for announces countdowns and yell scheduling
|
|
function module:ScheduleCountdown(time, numAnnounces, func, mod, prototype, ...)
|
|
time = time or 5
|
|
numAnnounces = numAnnounces or 3
|
|
for i = 1, numAnnounces do
|
|
--In event time is < numbmer of announces (ie 2 second time, with 3 announces)
|
|
local validTime = time - i
|
|
if validTime >= 1 then
|
|
schedule(validTime, func, mod, prototype, i, ...)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function unschedule(f, mod, ...)
|
|
if not f and not mod then
|
|
--Dangerous to use, should only ever happen with a force disable
|
|
removeAllMatching()
|
|
end
|
|
return removeAllMatching(f, mod, ...)
|
|
end
|
|
|
|
function module:Schedule(t, f, mod, ...)
|
|
if type(f) ~= "function" then
|
|
error("usage: DBM:Schedule(time, func, [args...])", 2)
|
|
end
|
|
return schedule(t, f, mod, ...)
|
|
end
|
|
|
|
function module:Unschedule(f, mod, ...)
|
|
return unschedule(f, mod, ...)
|
|
end
|
|
|