[Research] Coroutines and ingame timers
Posted: Tue Mar 05, 2024 9:46 am
This is about lua coroutines, and the SWBF2 ingame timers. This started with me trying to do something complicated, but I'm going to break it down into simple tests.
Why I'm doing this:
This thread will be a series of tests to see how compatible lua coroutines are with the ingame timer.
Test 1: Can you create ingame timers inside lua coroutines?
Answer: No.
For this test, I wanted to see if lua coroutines work at all, so I ran this code inside of the ScriptPostLoad function:
This is the relevant section of the resulting debug log:
This result shows that the existence of ingame timers in general doesn't break coroutines (testco2 is after a CreateTimer), but putting CreateTimer inside of a coroutine does break it. It also shows that coroutines can be nested (to rule out that problem).
Test 2: Setting and reading timer values inside a lua coroutine
Result: This also doesn't work as intended.
For this test, I've created a timer outside of a coroutine, but I attempted to set and read the timer values from inside the coroutine. This is the test code from the ScriptPostLoad function:
And this is the test result from the debug log:
This is the behaviour I was seeing originally (before the tests) where the GetTimerValue function returns the name of the timer when you call it inside a coroutine. It looks like neither SetTimerValue nor GetTimerValue have worked correctly, because the GetTimerValue from outside the coroutine returned 0, when the timer should be set at 4. (A more robust test would first set the timer value before the coroutine is run, then set the timer value in the coroutine and see if any change is applied. But this test was sufficient to satisfy in my mind that SetTimerValue didn't work inside the coroutine.)
Test 3: Can you start ingame timers inside lua coroutines?
Answer: No.
Another straightforward test. I created a 1 second timer, tried to start it from inside a coroutine and made it print to debug if it ever elapsed. I made a separate timer to print when 5 seconds had passed. This is the test code from the ScriptPostLoad function:
And this is the result:
The test timer never elapsed, which suggests that it was never correctly started. At this point, I find it unlikely that any other timer functions will work inside coroutines either.
Test 4: Can you create and run lua coroutines inside an OnTimerElapse event?
Answer: Yes.
Since coroutines shouldn't contain/interact with ingame timers, the next question is what about the reverse? I ran this test to see if coroutines can be created and run from an OnTimerElapse event. This is the test code from the ScriptPostLoad function:
And this is the result:
It appears to have functioned without a problem.
Test 5: Can you yield a coroutine that was resumed inside the OnTimerElapse event?
Answer: Yes. Also this is a proof of concept for a staggered timer elapse.
This might look like an oddly specific test but it doubles as a proof of concept for something I'm trying to achieve. Notice a few things.
And this is the result:
If you're not familiar with coroutines, this is exactly what should happen. I'm very pleased with this one.
PS You may be asking why I don't just make multiple timers with different OnTimerElapse events and jump from one to the other. It's because I'm doing OOP, and want to be able to pass the object a custom timer elapse function. I can hide the coroutine's yield calls inside the object's member functions. This method is just better for what I'm doing.
Why I'm doing this:
Hidden/Spoiler:
For this test, I wanted to see if lua coroutines work at all, so I ran this code inside of the ScriptPostLoad function:
Code: Select all
local testco = coroutine.create(
function()
print("ABC: Coroutines work as expected (1).")
end
)
coroutine.resume(testco)
-- First, see if creating a timer impacts coroutines
local test_timer = "breaktimer"
CreateTimer(test_timer)
local testco2 = coroutine.create(
function()
print("ABC: Coroutines work as expected (2).")
end
)
coroutine.resume(testco2)
-- Test nested coroutines
local testco3 = coroutine.create(
function()
print("ABC: Coroutines work as expected (3).")
local testco4 = coroutine.create(
function()
print("ABC: Coroutines work as expected (4).")
end
)
coroutine.resume(testco4)
end
)
coroutine.resume(testco3)
-- In it's simplest form (I've had this glitch before), show that you can't create timers inside coroutines
local testco5 = coroutine.create(
function()
local co_timer = "cotimer"
CreateTimer(co_timer)
print("ABC: Coroutines work as expected (5).")
end
)
coroutine.resume(testco5)
This is the relevant section of the resulting debug log:
Code: Select all
ABC: Coroutines work as expected (1).
ABC: Coroutines work as expected (2).
ABC: Coroutines work as expected (3).
ABC: Coroutines work as expected (4).
Message Severity: 3
C:\Battlefront2\main\Battlefront2\Source\LuaHelper.cpp(312)
CallProc failed: bad argument #1 to `resume' (string expected, got thread)
stack traceback:
[C]: in function `resume'
(none): in function `ScriptPostLoad'
For this test, I've created a timer outside of a coroutine, but I attempted to set and read the timer values from inside the coroutine. This is the test code from the ScriptPostLoad function:
Code: Select all
local control = "controltimer"
CreateTimer(control)
SetTimerValue(control, 4)
print("ABC: Control Timer Value: "..GetTimerValue(control))
local t1 = "testtimer"
CreateTimer(t1)
local co = coroutine.create(
function()
print("ABC: Coroutine was resumed successfully.")
SetTimerValue(t1, 4)
print("ABC: Timer Value reading from inside the coroutine: "..GetTimerValue(t1))
end
)
coroutine.resume(co)
print("ABC: Timer Value reading from outside the coroutine (after resume): "..GetTimerValue(t1))
And this is the test result from the debug log:
Code: Select all
ABC: Control Timer Value: 4
ABC: Coroutine was resumed successfully.
ABC: Timer Value reading from inside the coroutine: testtimer
ABC: Timer Value reading from outside the coroutine (after resume): 0
Another straightforward test. I created a 1 second timer, tried to start it from inside a coroutine and made it print to debug if it ever elapsed. I made a separate timer to print when 5 seconds had passed. This is the test code from the ScriptPostLoad function:
Code: Select all
-- setup
local test_timer = "testtimer"
CreateTimer(test_timer)
OnTimerElapse(
function(timer)
print("ABC: The timer elapsed successfully.")
end,
test_timer
)
-- test
SetTimerValue(test_timer, 1)
local co = coroutine.create(
function()
print("ABC: The coroutine was resumed successfully.")
StartTimer(test_timer)
end
)
coroutine.resume(co)
-- timeout
local t2 = "timeout"
CreateTimer(t2)
SetTimerValue(t2, 5)
OnTimerElapse(
function(timer)
print("ABC: 5 seconds have now passed so the test timer should have elapsed by now.")
end,
t2
)
StartTimer(t2)
And this is the result:
Code: Select all
ABC: The coroutine was resumed successfully.
ifs_sideselect_fnEnter(): Map does not support custom era teams
ifs_sideselect_fnEnter(): The award settings file does not exist
ABC: 5 seconds have now passed so the test timer should have elapsed by now.
Since coroutines shouldn't contain/interact with ingame timers, the next question is what about the reverse? I ran this test to see if coroutines can be created and run from an OnTimerElapse event. This is the test code from the ScriptPostLoad function:
Code: Select all
local t1 = "t1"
CreateTimer(t1)
SetTimerValue(t1, 3)
OnTimerElapse(
function(timer)
print("ABC: Timer elapsed.")
local testco = coroutine.create(
function()
print("ABC: Coroutine running.")
end
)
print("ABC: Starting coroutine.")
coroutine.resume(testco)
print("ABC: Coroutine finished.")
end,
t1
)
StartTimer(t1)
And this is the result:
Code: Select all
ABC: Timer elapsed.
ABC: Starting coroutine.
ABC: Coroutine running.
ABC: Coroutine finished.
This might look like an oddly specific test but it doubles as a proof of concept for something I'm trying to achieve. Notice a few things.
- I haven't created the coroutine inside the OnTimerElapse event, so yielding a coroutine that was created inside OnTimerElapse is still untested for the time being.
- This is a looping timer I created just for testing purposes. The way I've programmed this is bad practice because this test timer will never be destroyed at the end of the match. I've known there to be a problem with timers crashing instant action playlists either when they're not destroyed or never stopped (one of those things, but I don't know which).
- The coroutine in this test is a stand in for an actually useful timer elapse function, where meaningful code can be placed in the same spots as the print functions. The OnTimerElapse event is just behaving like a coroutine handler.
Code: Select all
-- setup
local t1 = "t1"
CreateTimer(t1)
SetTimerValue(t1, 3)
-- the code to be run inside OnTimerElapse
local co = coroutine.create(
function()
print("ABC: First section of the desired OnTimerElapse code.")
coroutine.yield()
print("ABC: Second section of the desired OnTimerElapse code.")
coroutine.yield()
print("ABC: Third section of the desired OnTimerElapse code.")
end
)
OnTimerElapse(
function(timer)
print("ABC: Timer elapsed.")
if coroutine.status(co) == "dead" then
print("ABC: Coroutine has already finished")
return
end
print("ABC: Resuming the coroutine.")
coroutine.resume(co)
print("ABC: Restarting the timer.")
SetTimerValue(timer, 3)
StartTimer(timer)
end,
t1
)
-- test begins
print("ABC: Starting the timer for the first time.")
StartTimer(t1)
And this is the result:
Code: Select all
ABC: Starting the timer for the first time.
ifs_sideselect_fnEnter(): Map does not support custom era teams
ifs_sideselect_fnEnter(): The award settings file does not exist
ABC: Timer elapsed.
ABC: Resuming the coroutine.
ABC: First section of the desired OnTimerElapse code.
ABC: Restarting the timer.
ABC: Timer elapsed.
ABC: Resuming the coroutine.
ABC: Second section of the desired OnTimerElapse code.
ABC: Restarting the timer.
ABC: Timer elapsed.
ABC: Resuming the coroutine.
ABC: Third section of the desired OnTimerElapse code.
ABC: Restarting the timer.
ABC: Timer elapsed.
ABC: Coroutine has already finished
PS You may be asking why I don't just make multiple timers with different OnTimerElapse events and jump from one to the other. It's because I'm doing OOP, and want to be able to pass the object a custom timer elapse function. I can hide the coroutine's yield calls inside the object's member functions. This method is just better for what I'm doing.