[Research] Coroutines and ingame timers

In this forum you will find and post information regarding the modding of Star Wars Battlefront 2. DO NOT POST MOD IDEAS/REQUESTS.

Moderator: Moderators

Post Reply
Sporadia
Corporal
Corporal
Posts: 151
Joined: Thu Jan 24, 2019 11:02 pm
Projects :: No Mod project currently
Games I'm Playing :: None
xbox live or psn: No gamertag set

[Research] Coroutines and ingame timers

Post by Sporadia »

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:
Hidden/Spoiler:
I originally wanted to pause an OnTimerElapse function half way through, reset the timer, and return to the same spot when the timer elapsed the second time (There's an even more complicated reason why I'm trying to do that). And I had the idea of putting the OnTimerElapse function in a lua coroutine and using yield/resume to keep track of where it was. When I was doing this I noticed some unusual and undesirable behaviour with SetTimerValue and GetTimerValue from inside the coroutine. I couldn't think of an explanation for why the timer was acting weird other than that I was calling the functions in a coroutine. So...
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:

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'
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:

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
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:

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.
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:

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.
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.
  1. 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.
  2. 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).
  3. 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.
So here is my last test for now. I created a timer like with test 4, but this timer is designed to loop a few times. On each loop, it resumes a coroutine that was created outside the timer. And, each time the coroutine is resumed, it is supposed to run up until it hits a yield function, then pause and return. (ie it runs from one yield to the next). Once the coroutine reaches the end of its code, it becomes dead so I had to program the timer to check for that and stop looping (because lua doesn't like it when you call a dead coroutine). The objective of the test is just to look for correct coroutine behaviour. Here's the code in ScriptPostLoad:

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
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.
MileHighGuy
Jedi
Jedi
Posts: 1194
Joined: Fri Dec 19, 2008 7:58 pm

Re: [Research] Coroutines and ingame timers

Post by MileHighGuy »

Wow, that's very cool. I didn't even know coroutines existed. What sort of things do you think we can use them for?
Sporadia
Corporal
Corporal
Posts: 151
Joined: Thu Jan 24, 2019 11:02 pm
Projects :: No Mod project currently
Games I'm Playing :: None
xbox live or psn: No gamertag set

Re: [Research] Coroutines and ingame timers

Post by Sporadia »

MileHighGuy wrote:
Wed Mar 06, 2024 4:51 am
Wow, that's very cool. I didn't even know coroutines existed. What sort of things do you think we can use them for?
I came across them when I was reading through Programming in Lua from lua.org. I've read all the way from the beginning up to chapter 16 on OOP. Lua is not designed for threading (without importing libraries). It doesn't have threads at all, it doesn't have anything like semaphores to help you deal with synchronisation (because it assumes coroutines can't do anything where you need them), and the coroutines are very limited: They can't run concurrently and they can't be stopped from the outside. Aside from using them to keep track of where in a function you are, I'm not sure what else to do with them to be honest. I'm not familiar enough with programming to do all the pipe filter stuff that's described in the lua resources. The fact that you can't call ingame timer functions from inside coroutines is also problematic, as I've realised today. You can make a staggered timer the way I tried to do it in test 5, but that staggered timer will not be able to interact with any other timers from inside its elapse coroutine.

On the topic of concurrent programming, the SWBF2 events perform an interrupt when they are triggered (which is something lua isn't designed to handle, because lua coroutines are called; they don't interrupt). Lua doesn't have any helper mechanisms for handling interrupts that I know of. I'm in the process of making some objects that help manage ingame timers/events and synchronisation errors which I'll release as an asset when it's done.
Edit: This is delayed while I do other things

On the topic of helpful things in lua which are easy to miss. Programming in lua chapter 6.1 on closures is good. Along with chapter 5.2 on functions with a variable number of arguments, chapter 6.3 on tail calls (making state machines), all of chapter 7 on how for loops work, and all of chapter 13 on how metatables work.

Edit: Updated the paragraph about interrupts to be more technically accurate.
Post Reply