Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 213 additions & 74 deletions game/scripts/vscripts/libraries/timers.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
TIMERS_VERSION = "1.05"
TIMERS_VERSION = "1.06"

--[[

Expand Down Expand Up @@ -82,85 +82,198 @@ if Timers == nil then
--Timers.__index = Timers
end

-- lightweight binary min-heap for timers by endTime
local function heap_new()
return { items = {}, size = 0 }
end

local function heap_swap(h, i, j)
local a, b = h.items[i], h.items[j]
h.items[i], h.items[j] = b, a
if a then a._heapIndex = j end
if b then b._heapIndex = i end
end

local function heap_sift_up(h, i)
while i > 1 do
local parent = math.floor(i / 2)
if h.items[parent].endTime <= h.items[i].endTime then break end
heap_swap(h, parent, i)
i = parent
end
end

local function heap_sift_down(h, i)
local size = h.size
while true do
local left = i * 2
if left > size then break end
local right = left + 1
local smallest = left
if right <= size and h.items[right].endTime < h.items[left].endTime then
smallest = right
end
if h.items[i].endTime <= h.items[smallest].endTime then break end
heap_swap(h, i, smallest)
i = smallest
end
end

local function heap_push(h, item)
h.size = h.size + 1
h.items[h.size] = item
item._heapIndex = h.size
heap_sift_up(h, h.size)
end

local function heap_peek(h)
if h.size == 0 then return nil end
return h.items[1]
end

local function heap_pop(h)
local top = heap_peek(h)
if not top then return nil end
local last = h.items[h.size]
h.items[1] = last
h.items[h.size] = nil
h.size = h.size - 1
if last then last._heapIndex = 1 end
if h.size > 0 then
heap_sift_down(h, 1)
end
if top then top._heapIndex = nil end
return top
end

local function heap_remove(h, item)
local idx = item and item._heapIndex
if not idx or idx < 1 or idx > h.size then return end
if idx == h.size then
h.items[idx] = nil
h.size = h.size - 1
item._heapIndex = nil
return
end
local last = h.items[h.size]
h.items[idx] = last
h.items[h.size] = nil
h.size = h.size - 1
last._heapIndex = idx
item._heapIndex = nil
-- decide direction
if last.endTime < (h.items[math.floor(idx/2)] and h.items[math.floor(idx/2)].endTime or last.endTime) then
heap_sift_up(h, idx)
else
heap_sift_down(h, idx)
end
end

local function heap_update(h, item)
local idx = item and item._heapIndex
if not idx or idx < 1 or idx > h.size then return end
-- try both ways cheaply
local parent = math.floor(idx/2)
if parent >= 1 and h.items[parent].endTime > item.endTime then
heap_sift_up(h, idx)
else
heap_sift_down(h, idx)
end
end

function Timers:start()
Timers = self
self.timers = {}
-- two heaps: game time and real time
self._gameHeap = heap_new()
self._realHeap = heap_new()

--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:_ProcessTimer(name, v, now)
Timers.runningTimer = name
Timers.removeSelf = false

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

if status then
if nextCall and not Timers.removeSelf then
local bOldStyle = v.useOldStyle ~= nil and v.useOldStyle == true
if bOldStyle then
v.endTime = v.endTime + nextCall - now
else
v.endTime = v.endTime + nextCall
end
if v._heapType == 'game' then
if v._heapIndex then
heap_update(Timers._gameHeap, v)
else
heap_push(Timers._gameHeap, v)
end
Timers.timers[name] = v
else
if v._heapIndex then
heap_update(Timers._realHeap, v)
else
heap_push(Timers._realHeap, v)
end
Timers.timers[name] = v
end
end
else
Timers:HandleEventError('Timer', name, nextCall)
end
end

function Timers:Think()
--if GameRules:State_Get() >= DOTA_GAMERULES_STATE_POST_GAME then
--return
--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()

-- Process timers
for k,v in pairs(Timers.timers) do
local bUseGameTime = true
if v.useGameTime ~= nil and v.useGameTime == false then
bUseGameTime = false
local nowGame = GameRules:GetGameTime()
local nowReal = Time()

-- Process game-time timers
while true do
local top = heap_peek(Timers._gameHeap)
if not top or top.endTime > nowGame then break end
heap_pop(Timers._gameHeap)
local name = top._name
-- Remove from map; guard against stale entries
local v = Timers.timers[name]
if v == top then
Timers.timers[name] = nil
top._heapIndex = nil
Timers:_ProcessTimer(name, top, nowGame)
end
local bOldStyle = false
if v.useOldStyle ~= nil and v.useOldStyle == true then
bOldStyle = true
end

local now = pre_loop_now
if not bUseGameTime then
now = Time()
end

if v.endTime == nil then
v.endTime = now
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

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

Timers.timers[k] = v
end
end

-- Update timer data
--self:UpdateTimerData()
else
-- Nope, handle the error
Timers:HandleEventError('Timer', k, nextCall)
end
-- Process real-time timers
while true do
local top = heap_peek(Timers._realHeap)
if not top or top.endTime > nowReal then break end
heap_pop(Timers._realHeap)
local name = top._name
local v = Timers.timers[name]
if v == top then
Timers.timers[name] = nil
top._heapIndex = nil
Timers:_ProcessTimer(name, top, nowReal)
end
end

Expand Down Expand Up @@ -228,11 +341,8 @@ function Timers:CreateTimer(name, args, context)
return
end


local now = GameRules:GetGameTime()
if args.useGameTime ~= nil and args.useGameTime == false then
now = Time()
end
local useGameTime = not (args.useGameTime ~= nil and args.useGameTime == false)
local now = useGameTime and GameRules:GetGameTime() or Time()

if args.endTime == nil then
args.endTime = now
Expand All @@ -241,32 +351,61 @@ function Timers:CreateTimer(name, args, context)
end

args.context = context
args._name = name
args._heapType = useGameTime and 'game' or 'real'

Timers.timers[name] = args
if useGameTime then
heap_push(Timers._gameHeap, args)
else
heap_push(Timers._realHeap, args)
end

return name
end

function Timers:RemoveTimer(name)
Timers.timers[name] = nil
local v = Timers.timers[name]
if v then
Timers.timers[name] = nil
if v._heapType == 'game' then
heap_remove(Timers._gameHeap, v)
else
heap_remove(Timers._realHeap, v)
end
end
if Timers.runningTimer == name then
Timers.removeSelf = true
end
end

function Timers:RemoveTimers(killAll)
local timers = {}
local newMap = {}
Timers.removeSelf = true

if not killAll then
for k,v in pairs(Timers.timers) do
if killAll then
-- clear heaps
self._gameHeap = heap_new()
self._realHeap = heap_new()
else
-- retain only persist timers, rebuild heaps
local gameHeap = heap_new()
local realHeap = heap_new()
for k, v in pairs(Timers.timers) do
if v.persist then
timers[k] = v
newMap[k] = v
if v._heapType == 'game' then
heap_push(gameHeap, v)
else
heap_push(realHeap, v)
end
end
end
self._gameHeap = gameHeap
self._realHeap = realHeap
end

Timers.timers = timers
Timers.timers = newMap
end

if not Timers.timers then Timers:start() end
Expand Down