Files
Dota-Zombie-Invasion/scripts/vscripts/lib/timers.lua
T
2026-05-29 15:11:31 +07:00

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