Sporadia wrote: ↑Thu Jan 24, 2019 11:43 pm
If you don’t destroy timers, then your map may crash when it’s run in an instant action playlist. This is something of an invisible bug because the diagnostic tools don’t let you run instant action playlists (to check for the problem) and this crash never effects the first map in a playlist (from what I’ve seen). So, it’s important that DestroyTimer is included in an OnTimerElapse to ensure that all timers are destroyed when they have run to 0. However, it’s also important that you never use a destroyed timer. For single use timers the solution here is easy, you put CreateTimer at the start of ScriptPostLoad and OnTimerElapse at the end of it. Provided you can guarantee that the timer will only ever elapse once from the way you’ve coded its other settings then you’ll be fine.
I'm bumping this because I've never been able to replicate this bug. I vaguely remember that when I wrote this, I had a fairly complicated user script running which was doing these phantom crashes about 3 missions in. And I remember that it stopped when I made the decision to start destroying timers. But in the process of that I could have very easily changed something else. (It wasn't very well tested). Either way, today I've been running code to try to make sure that destroying timers is a necessity:
Code: Select all
local base = "testTimer"
local i = math.random(10000)
for j = i, i + 50 do -- create 50 timers with hopefully different names to the last round
local name = base.."-"..i
local loopEvent = OnTimerElapse(
function(timer)
SetTimerValue(name, 30)
StartTimer(name)
end,
name
)
CreateTimer(name)
SetTimerValue(name, 30)
StartTimer(name)
end
I've tried putting this directly in the mission lua, I've tried loading it via a ScriptCB_DoFile and I've tried injecting it into the missions with a user script. None of them crashed the playlists today so I really have no idea if timer cleanup is a real problem. (It does strike me as odd though that the DestroyTimer function exists).
Edit: I'll need to redo these tests. There is a subtle flaw in this code. My fault for rushing into the main game without checking it works with the debug log first.
Edit2: Ignore last edit, I don't need to retest this. But remember how I said a few years ago that OnTimerElapse needs to be declared after CreateTimer? It turns out that was completely wrong. You can declare them in the opposite order and the TimerElapse event will still exist. (There must be a reason why I thought that didn't work, but I don't remember. It definitely does work though. The test I'm doing now is a lot simpler than the stuff I was doing back then.)
Update to timer tutorial
I'm also bumping this tutorial, because I have some tips which make timers easier that I just didn't know before.
ReleaseTimerElapse
Straight forward, but I didn't know about this when I wrote in this tutorial years ago. If you capture an event like so:
Code: Select all
local event = OnTimerElapse(
function(timer)
-- do something
end,
"Timer1"
)
...then you can delete the OnTimerElapse like so:
It makes timers a bit more flexible. An extra thing to note is that passing local variables into events can be tricky. Global variables are fine, but if I were to try this:
Code: Select all
local event = OnTimerElapse(
function(timer)
ReleaseTimerElapse(event) -- event is nil here
-- do something once
end,
"Timer1"
)
ReleaseTimerElapse is actually acting on
nil here, so it doesn't work. One solution is to get rid of
local and just pass in globals. But what I've actually started doing is passing variables in via tables. For some reason, events can still access the tables that were made outside of them, just not variables. See here:
Code: Select all
local eventStore = {} -- a hack to pass the event in to TimerElapse via this table
eventStore[1] = OnTimerElapse(
function(timer)
ReleaseTimerElapse(eventStore[1])
-- do something once
end,
"Timer1"
)
You can have multiple TimerElapse events for the same timer
I had kind of been working around this for a few years without really understanding it. Every declaration of OnTimerElapse creates a separate event, which triggers when the timer ends. First of all, that means you don't want to be performing OnTimerElapse multiple times (ie putting OnTimerElapse inside an OnCharacterDeath). Because you'll get duplicate TimerElapse events if you do that. (It turns out that's not as big of a deal once you know how to release events).
But on the plus side, this is really handy for making timers loop. You can have one OnTimerElapse which contains the code you're really interested in (but doesn't reset the timer), and then declare a second OnTimerElapse which only contains code to reset the timer and start it again. And then you can quite easily stop the timer looping by using ReleaseTimerElapse just on the event which was looping it. You can turn a timer into a loop timer whenever you want by declaring a new OnTimerElapse which resets it. (Something to note is the order of events here. When the timer runs out, it will first trigger every TimerElapse event related to it. And then code in those events will start to run / interrupt each other in a random order. So every TimerElapse is guaranteed to trigger, even if one of them contains code to restart the timer.)
Here is an example:
Code: Select all
local timerName = "Timer1"
CreateTimer(timerName)
local i = {1}
local loopStore = {}
OnTimerElapse(
function(timer)
print("Elapsed!") -- debug proof that it's working
-- do some really complicated thing
-- this is just something extra I'm adding, to make this timer stoop looping after 5 elapses
local iteration = i[1]
i[1] = iteration + 1
if iteration == 4 then
-- release the looping event on iteration 4, which is 1 lower than you might expect
-- the looping event that creates iteration 5 has already been triggered here, so there will be an iteration 5
-- releasing events will stop them triggering again, but it won't stop them running if they're already triggered
ReleaseTimerElapse(loopStore[1])
end
end,
timerName
)
loopStore[1] = OnTimerElapse(
function(timer)
SetTimerValue(timer, 2)
StartTimer(timer)
end,
timerName
)
SetTimerValue(timerName, 5)
StartTimer(timerName)