diff --git a/game/scripts/vscripts/libraries/timers.lua b/game/scripts/vscripts/libraries/timers.lua index a17cddc615..abac81e75c 100644 --- a/game/scripts/vscripts/libraries/timers.lua +++ b/game/scripts/vscripts/libraries/timers.lua @@ -1,4 +1,4 @@ -TIMERS_VERSION = "1.05" +TIMERS_VERSION = "1.06" --[[ @@ -82,85 +82,229 @@ if Timers == nil then --Timers.__index = Timers end +-- Min-heap helper functions for O(log n) timer management +-- Heap entries are {endTime, name} pairs, ordered by endTime + +local function heap_parent(i) + return math.floor(i / 2) +end + +local function heap_left(i) + return 2 * i +end + +local function heap_right(i) + return 2 * i + 1 +end + +local function heap_swap(heap, i, j) + heap[i], heap[j] = heap[j], heap[i] +end + +local function heap_bubble_up(heap, i) + while i > 1 do + local p = heap_parent(i) + if heap[p][1] <= heap[i][1] then + break + end + heap_swap(heap, i, p) + i = p + end +end + +local function heap_bubble_down(heap, i) + local n = #heap + while true do + local smallest = i + local l = heap_left(i) + local r = heap_right(i) + + if l <= n and heap[l][1] < heap[smallest][1] then + smallest = l + end + if r <= n and heap[r][1] < heap[smallest][1] then + smallest = r + end + + if smallest == i then + break + end + + heap_swap(heap, i, smallest) + i = smallest + end +end + +local function heap_push(heap, endTime, name) + local entry = {endTime, name} + heap[#heap + 1] = entry + heap_bubble_up(heap, #heap) +end + +local function heap_pop(heap) + local n = #heap + if n == 0 then + return nil + end + + local top = heap[1] + if n == 1 then + heap[1] = nil + else + heap[1] = heap[n] + heap[n] = nil + heap_bubble_down(heap, 1) + end + + return top +end + +local function heap_peek(heap) + return heap[1] +end + function Timers:start() Timers = self self.timers = {} + -- Initialize heaps for efficient timer management + self.gameTimeHeap = {} + self.realTimeHeap = {} + + -- Lazy deletion sets for removed timers still in heaps + self.gameTimeRemoved = {} + self.realTimeRemoved = {} + + -- Counter for periodic heap cleanup + self.thinkCount = 0 + --local ent = Entities:CreateByClassname("info_target") -- Entities:FindByClassname(nil, 'CWorld') local ent = SpawnEntityFromTableSynchronous("info_target", {targetname="timers_lua_thinker"}) ent:SetThink("Think", self, "timers", TIMERS_THINK) end -function Timers:Think() - --if GameRules:State_Get() >= DOTA_GAMERULES_STATE_POST_GAME then - --return - --end +-- Process a single heap, executing all ready timers +local function ProcessHeap(heap, removedSet, now) + while true do + local top = heap_peek(heap) + if not top then + break + end - -- Track game time, since the dt passed in to think is actually wall-clock time not simulation time. - local pre_loop_now = GameRules:GetGameTime() + local endTime = top[1] + local name = top[2] - -- Process timers - for k,v in pairs(Timers.timers) do - local bUseGameTime = true - if v.useGameTime ~= nil and v.useGameTime == false then - bUseGameTime = false - end - local bOldStyle = false - if v.useOldStyle ~= nil and v.useOldStyle == true then - bOldStyle = true + -- Early exit: if the smallest endTime is still in the future, nothing is ready + if endTime > now then + break end - local now = pre_loop_now - if not bUseGameTime then - now = Time() + -- Pop this timer from the heap + heap_pop(heap) + + -- Check for lazy deletion + if removedSet[name] then + removedSet[name] = nil + else + -- Get the timer data + local v = Timers.timers[name] + + -- Timer might have been removed or replaced since it was pushed + if v and v.endTime == endTime then + -- Remove from timers lookup + Timers.timers[name] = nil + + local bOldStyle = v.useOldStyle == true + + Timers.runningTimer = name + Timers.removeSelf = false + + -- Run the callback + local status, nextCall + if v.context then + status, nextCall = xpcall(function() return v.callback(v.context, v) end, function (msg) + return msg..'\n'..debug.traceback()..'\n' + end) + else + status, nextCall = xpcall(function() return v.callback(v) end, function (msg) + return msg..'\n'..debug.traceback()..'\n' + end) + end + + Timers.runningTimer = nil + + -- Make sure it worked + if status then + -- Check if it needs to loop + if nextCall and not Timers.removeSelf then + -- Change its end time + if bOldStyle then + v.endTime = v.endTime + nextCall - now + else + v.endTime = v.endTime + nextCall + end + + -- Re-add to timers table and heap + Timers.timers[name] = v + heap_push(heap, v.endTime, name) + end + else + -- Nope, handle the error + Timers:HandleEventError('Timer', name, nextCall) + end + end end + end +end - if v.endTime == nil then - v.endTime = now +-- Rebuild a heap, filtering out removed and stale entries +local function RebuildHeap(oldHeap, removedSet, timersTable) + local newHeap = {} + for i = 1, #oldHeap do + local entry = oldHeap[i] + local endTime = entry[1] + local name = entry[2] + local timer = timersTable[name] + -- Only keep entries that aren't marked removed, exist in timers table, + -- and have matching endTime (filters out stale entries from replaced timers) + if not removedSet[name] and timer and timer.endTime == endTime then + heap_push(newHeap, endTime, name) end - -- Check if the timer has finished - if now >= v.endTime then - -- Remove from timers list - Timers.timers[k] = nil - - Timers.runningTimer = k - Timers.removeSelf = false - - -- Run the callback - local status, nextCall - if v.context then - status, nextCall = xpcall(function() return v.callback(v.context, v) end, function (msg) - return msg..'\n'..debug.traceback()..'\n' - end) - else - status, nextCall = xpcall(function() return v.callback(v) end, function (msg) - return msg..'\n'..debug.traceback()..'\n' - end) - end + end + return newHeap +end - Timers.runningTimer = nil +-- How often to run cleanup (in think ticks, ~10 seconds at 0.01s per tick) +local CLEANUP_INTERVAL = 1000 - -- Make sure it worked - if status then - -- Check if it needs to loop - if nextCall and not Timers.removeSelf then - -- Change its end time +function Timers:Think() + --if GameRules:State_Get() >= DOTA_GAMERULES_STATE_POST_GAME then + --return + --end - if bOldStyle then - v.endTime = v.endTime + nextCall - now - else - v.endTime = v.endTime + nextCall - end + -- Track game time, since the dt passed in to think is actually wall-clock time not simulation time. + local gameTimeNow = GameRules:GetGameTime() + local realTimeNow = Time() - Timers.timers[k] = v - end + -- Process game time timers + ProcessHeap(self.gameTimeHeap, self.gameTimeRemoved, gameTimeNow) - -- Update timer data - --self:UpdateTimerData() - else - -- Nope, handle the error - Timers:HandleEventError('Timer', k, nextCall) - end + -- Process real time timers + ProcessHeap(self.realTimeHeap, self.realTimeRemoved, realTimeNow) + + -- Periodic cleanup to remove stale entries from heaps + self.thinkCount = self.thinkCount + 1 + if self.thinkCount >= CLEANUP_INTERVAL then + self.thinkCount = 0 + + -- Only rebuild if there are removed entries to clean up + if next(self.gameTimeRemoved) then + self.gameTimeHeap = RebuildHeap(self.gameTimeHeap, self.gameTimeRemoved, self.timers) + self.gameTimeRemoved = {} + end + if next(self.realTimeRemoved) then + self.realTimeHeap = RebuildHeap(self.realTimeHeap, self.realTimeRemoved, self.timers) + self.realTimeRemoved = {} end end @@ -187,25 +331,14 @@ function Timers:HandleEventError(name, event, err) end function Timers:RemainingTime(name) - --Calculates Remaining Time on a given timer local v = Timers.timers[name] - local bUseGameTime = true - if v.useGameTime ~= nil and v.useGameTime == false then - bUseGameTime = false - end - local bOldStyle = false - if v.useOldStyle ~= nil and v.useOldStyle == true then - bOldStyle = true - end + local bUseGameTime = v.useGameTime == nil or v.useGameTime ~= false + local now = GameRules:GetGameTime() if not bUseGameTime then now = Time() end - if v.endTime == nil then - v.endTime = now - end - return v.endTime - now end @@ -228,9 +361,10 @@ function Timers:CreateTimer(name, args, context) return end + local bUseGameTime = args.useGameTime == nil or args.useGameTime ~= false local now = GameRules:GetGameTime() - if args.useGameTime ~= nil and args.useGameTime == false then + if not bUseGameTime then now = Time() end @@ -244,10 +378,34 @@ function Timers:CreateTimer(name, args, context) Timers.timers[name] = args + -- Push onto the appropriate heap and clear any lazy deletion mark + if bUseGameTime then + Timers.gameTimeRemoved[name] = nil + heap_push(Timers.gameTimeHeap, args.endTime, name) + else + Timers.realTimeRemoved[name] = nil + heap_push(Timers.realTimeHeap, args.endTime, name) + end + return name end function Timers:RemoveTimer(name) + local timer = Timers.timers[name] + if timer then + -- Mark for lazy deletion in the appropriate heap + local bUseGameTime = timer.useGameTime == nil or timer.useGameTime ~= false + if bUseGameTime then + Timers.gameTimeRemoved[name] = true + else + Timers.realTimeRemoved[name] = true + end + else + -- Timer already removed from table, mark in both sets to be safe + Timers.gameTimeRemoved[name] = true + Timers.realTimeRemoved[name] = true + end + Timers.timers[name] = nil if Timers.runningTimer == name then Timers.removeSelf = true @@ -255,18 +413,39 @@ function Timers:RemoveTimer(name) end function Timers:RemoveTimers(killAll) - local timers = {} Timers.removeSelf = true - if not killAll then - for k,v in pairs(Timers.timers) do + if killAll then + -- Clear everything + Timers.timers = {} + Timers.gameTimeHeap = {} + Timers.realTimeHeap = {} + Timers.gameTimeRemoved = {} + Timers.realTimeRemoved = {} + else + -- Keep only persistent timers, rebuild heaps + local timers = {} + local gameTimeHeap = {} + local realTimeHeap = {} + + for k, v in pairs(Timers.timers) do if v.persist then timers[k] = v + local bUseGameTime = v.useGameTime == nil or v.useGameTime ~= false + if bUseGameTime then + heap_push(gameTimeHeap, v.endTime, k) + else + heap_push(realTimeHeap, v.endTime, k) + end end end - end - Timers.timers = timers + Timers.timers = timers + Timers.gameTimeHeap = gameTimeHeap + Timers.realTimeHeap = realTimeHeap + Timers.gameTimeRemoved = {} + Timers.realTimeRemoved = {} + end end if not Timers.timers then Timers:start() end