333 lines
8.8 KiB
Lua
333 lines
8.8 KiB
Lua
TIMERS_VERSION = "1.07"
|
|
|
|
--[[
|
|
1.07 modified by Perry (fixed stack overflow if lots of timers finish at the same time and removed HandleErrors throwing error outside tools mode)
|
|
1.06 modified by Celireor (now uses binary heap priority queue instead of iteration to determine timer of shortest duration)
|
|
|
|
DO NOT MODIFY A REALTIME TIMER TO USE GAMETIME OR VICE VERSA MIDWAY WITHOUT FIRST REMOVING AND RE-ADDING THE TIMER
|
|
|
|
-- A timer running every second that starts immediately on the next frame, respects pauses
|
|
Timers:CreateTimer(function()
|
|
print ("Hello. I'm running immediately and then every second thereafter.")
|
|
return 1.0
|
|
end
|
|
)
|
|
|
|
-- The same timer as above with a shorthand call
|
|
Timers(function()
|
|
print ("Hello. I'm running immediately and then every second thereafter.")
|
|
return 1.0
|
|
end)
|
|
|
|
|
|
-- A timer which calls a function with a table context
|
|
Timers:CreateTimer(GameMode.someFunction, GameMode)
|
|
|
|
-- A timer running every second that starts 5 seconds in the future, respects pauses
|
|
Timers:CreateTimer(5, function()
|
|
print ("Hello. I'm running 5 seconds after you called me and then every second thereafter.")
|
|
return 1.0
|
|
end
|
|
)
|
|
|
|
-- 10 second delayed, run once using gametime (respect pauses)
|
|
Timers:CreateTimer({
|
|
endTime = 10, -- when this timer should first execute, you can omit this if you want it to run first on the next frame
|
|
callback = function()
|
|
print ("Hello. I'm running 10 seconds after when I was started.")
|
|
end
|
|
})
|
|
|
|
-- 10 second delayed, run once regardless of pauses
|
|
Timers:CreateTimer({
|
|
useGameTime = false,
|
|
endTime = 10, -- when this timer should first execute, you can omit this if you want it to run first on the next frame
|
|
callback = function()
|
|
print ("Hello. I'm running 10 seconds after I was started even if someone paused the game.")
|
|
end
|
|
})
|
|
|
|
|
|
-- A timer running every second that starts after 2 minutes regardless of pauses
|
|
Timers:CreateTimer("uniqueTimerString3", {
|
|
useGameTime = false,
|
|
endTime = 120,
|
|
callback = function()
|
|
print ("Hello. I'm running after 2 minutes and then every second thereafter.")
|
|
return 1
|
|
end
|
|
})
|
|
|
|
]]
|
|
|
|
-- Binary Heap implementation copy-pasted from https://gist.github.com/starwing/1757443a1bd295653c39
|
|
-- BinaryHeap[1] always points to the element with the lowest "key" variable
|
|
-- API
|
|
-- BinaryHeap(key) - Creates a new BinaryHeap with key. The key is the name of the integer variable used to sort objects.
|
|
-- BinaryHeap:Insert - Inserts an object into BinaryHeap
|
|
-- BinaryHeap:Remove - Removes an object from BinaryHeap
|
|
|
|
BinaryHeap = BinaryHeap or {}
|
|
BinaryHeap.__index = BinaryHeap
|
|
|
|
function BinaryHeap:Insert(item)
|
|
local index = #self + 1
|
|
local key = self.key
|
|
item.index = index
|
|
self[index] = item
|
|
while index > 1 do
|
|
local parent = math.floor(index/2)
|
|
if self[parent][key] <= item[key] then
|
|
break
|
|
end
|
|
self[index], self[parent] = self[parent], self[index]
|
|
self[index].index = index
|
|
self[parent].index = parent
|
|
index = parent
|
|
end
|
|
return item
|
|
end
|
|
|
|
function BinaryHeap:Remove(item)
|
|
local index = item.index
|
|
if self[index] ~= item then return end
|
|
local key = self.key
|
|
local heap_size = #self
|
|
if index == heap_size then
|
|
self[heap_size] = nil
|
|
return
|
|
end
|
|
self[index] = self[heap_size]
|
|
self[index].index = index
|
|
self[heap_size] = nil
|
|
while true do
|
|
local left = index*2
|
|
local right = left + 1
|
|
if not self[left] then break end
|
|
local newindex = right
|
|
if self[index][key] >= self[left][key] then
|
|
if not self[right] or self[left][key] < self[right][key] then
|
|
newindex = left
|
|
end
|
|
elseif not self[right] or self[index][key] <= self[right][key] then
|
|
break
|
|
end
|
|
self[index], self[newindex] = self[newindex], self[index]
|
|
self[index].index = index
|
|
self[newindex].index = newindex
|
|
index = newindex
|
|
end
|
|
end
|
|
|
|
setmetatable(BinaryHeap, {__call = function(self, key) return setmetatable({key=key}, self) end})
|
|
|
|
function table.merge(input1, input2)
|
|
for i,v in pairs(input2) do
|
|
input1[i] = v
|
|
end
|
|
return input1
|
|
end
|
|
|
|
|
|
TIMERS_THINK = 0.01
|
|
|
|
if Timers == nil then
|
|
print ( '[Timers] creating Timers ['..TIMERS_VERSION..']' )
|
|
Timers = {}
|
|
setmetatable(Timers, {
|
|
__call = function(t, ...)
|
|
return t:CreateTimer(...)
|
|
end
|
|
})
|
|
--Timers.__index = Timers
|
|
end
|
|
|
|
function Timers:start()
|
|
self.started = true
|
|
Timers = self
|
|
self:InitializeTimers()
|
|
self.nextTickCallbacks = {}
|
|
|
|
local ent = SpawnEntityFromTableSynchronous("info_target", {targetname="timers_lua_thinker"})
|
|
ent:SetThink("Think", self, "timers", TIMERS_THINK)
|
|
end
|
|
|
|
function Timers:Think()
|
|
local nextTickCallbacks = table.merge({}, Timers.nextTickCallbacks)
|
|
Timers.nextTickCallbacks = {}
|
|
for _, cb in ipairs(nextTickCallbacks) do
|
|
local status, result = xpcall(cb, function(err)
|
|
return tostring(err)
|
|
end)
|
|
if not status then
|
|
Timers:HandleEventError(result)
|
|
end
|
|
end
|
|
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 now = GameRules:GetGameTime()
|
|
|
|
-- Process timers
|
|
self:ExecuteTimers(self.realTimeHeap, Time())
|
|
self:ExecuteTimers(self.gameTimeHeap, GameRules:GetGameTime())
|
|
|
|
return TIMERS_THINK
|
|
end
|
|
|
|
function Timers:ExecuteTimers(timerList, now)
|
|
--Timers are alr. sorted by end time upon insertion
|
|
local currentTimer = timerList[1]
|
|
|
|
--Check if timer has finished
|
|
while currentTimer and (now >= currentTimer.endTime) do
|
|
-- Remove from timers list
|
|
timerList:Remove(currentTimer)
|
|
Timers.runningTimer = currentTimer
|
|
Timers.removeSelf = false
|
|
|
|
-- Run the callback
|
|
local status, timerResult
|
|
if currentTimer.context then
|
|
status, timerResult = xpcall(function()
|
|
return currentTimer.callback(currentTimer.context, currentTimer)
|
|
end, function(err)
|
|
return tostring(err)
|
|
end)
|
|
else
|
|
status, timerResult = xpcall(function()
|
|
return currentTimer.callback(currentTimer)
|
|
end, function(err)
|
|
return tostring(err)
|
|
end)
|
|
end
|
|
|
|
Timers.runningTimer = nil
|
|
|
|
-- Make sure it worked
|
|
if status then
|
|
-- Check if it needs to loop
|
|
if timerResult and not Timers.removeSelf then
|
|
-- Change its end time
|
|
|
|
currentTimer.endTime = currentTimer.endTime + timerResult
|
|
|
|
timerList:Insert(currentTimer)
|
|
end
|
|
|
|
-- Update timer data
|
|
--self:UpdateTimerData()
|
|
else
|
|
-- Nope, handle the error
|
|
Timers:HandleEventError(timerResult)
|
|
end
|
|
--Check next timer in heap
|
|
currentTimer = timerList[1]
|
|
end
|
|
end
|
|
|
|
function Timers:HandleEventError(err)
|
|
-- Защита от спама "nil" без контекста
|
|
if err == nil or err == "" then
|
|
print("[Timers] Timer error: <nil>")
|
|
return
|
|
end
|
|
|
|
print("[Timers] Timer error:", err)
|
|
--if not IsInToolsMode() then
|
|
-- If you want to send errors from inside timers on live servers to your own webserver, do it here
|
|
--end
|
|
end
|
|
|
|
function Timers:CreateTimer(arg1, arg2, context)
|
|
local timer
|
|
if type(arg1) == "function" then
|
|
if arg2 ~= nil then
|
|
context = arg2
|
|
end
|
|
timer = {callback = arg1}
|
|
elseif type(arg1) == "table" then
|
|
timer = arg1
|
|
elseif type(arg1) == "number" then
|
|
timer = {endTime = arg1, callback = arg2}
|
|
elseif type(arg1) == "string" then
|
|
-- First argument is timer name, second is table with timer config
|
|
timer = arg2
|
|
if timer then
|
|
timer.name = arg1
|
|
end
|
|
end
|
|
if not timer or not timer.callback then
|
|
print("Invalid timer created: timer is nil or callback is missing")
|
|
return
|
|
end
|
|
|
|
local now = GameRules:GetGameTime()
|
|
local timerHeap = self.gameTimeHeap
|
|
if timer.useGameTime ~= nil and timer.useGameTime == false then
|
|
now = Time()
|
|
timerHeap = self.realTimeHeap
|
|
end
|
|
|
|
if timer.endTime == nil then
|
|
timer.endTime = now
|
|
else
|
|
timer.endTime = now + timer.endTime
|
|
end
|
|
|
|
timer.context = context
|
|
|
|
timerHeap:Insert(timer)
|
|
|
|
return timer
|
|
end
|
|
|
|
function Timers:NextTick(callback)
|
|
table.insert(Timers.nextTickCallbacks, callback)
|
|
end
|
|
|
|
function Timers:RemoveTimer(nameOrTimer)
|
|
-- Разрешаем передавать либо сам таймер, либо строковое имя
|
|
if not nameOrTimer then return end
|
|
|
|
-- Если передали строку – ищем таймер с таким name в кучах
|
|
if type(nameOrTimer) == "string" then
|
|
local name = nameOrTimer
|
|
local function findByName(heap)
|
|
for _, t in ipairs(heap) do
|
|
if t.name == name then
|
|
return t
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local timer = findByName(self.gameTimeHeap) or findByName(self.realTimeHeap)
|
|
if not timer then
|
|
return
|
|
end
|
|
nameOrTimer = timer
|
|
end
|
|
|
|
local timer = nameOrTimer
|
|
local timerHeap = self.gameTimeHeap
|
|
if timer.useGameTime ~= nil and timer.useGameTime == false then
|
|
timerHeap = self.realTimeHeap
|
|
end
|
|
|
|
timerHeap:Remove(timer)
|
|
if Timers.runningTimer == timer then
|
|
Timers.removeSelf = true
|
|
end
|
|
end
|
|
|
|
function Timers:InitializeTimers()
|
|
self.realTimeHeap = BinaryHeap("endTime")
|
|
self.gameTimeHeap = BinaryHeap("endTime")
|
|
end
|
|
|
|
if not Timers.started then Timers:start() end
|
|
|
|
GameRules.Timers = Timers |