initial commit
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
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
|
||||
Reference in New Issue
Block a user