Tutorial:Custom Promises

From FreeSpace Wiki
Revision as of 20:54, 29 May 2020 by M!m (talk | contribs) (Created page with "This tutorial will walk you through using ''async.promise'' for creating promises from Lua code so that you can await custom conditions in ''async.run''. A relatively common...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

This tutorial will walk you through using async.promise for creating promises from Lua code so that you can await custom conditions in async.run.

A relatively common operation in missions is to have an event happen when a ship has arrived in mission. This could be done via very complicated usages of the "On Warp In" hook that involve a lot of global variables. However, here we will create a Lua Module that exposes the function "waitForShipAsync(ship_name)" that returns a promise which resolves to the ship handle when the specified ship has arrived in the mission or errors when the mission has ended. So, let's start with a simple template that exposes exactly that function:

local module = {}

function module.waitForShipAsync(ship_name)
end

return module

Not very useful but a good start nonetheless. If you save this as "missionPromise.lua" in your "scripts" folder it will be available for use with require. For the rest of the tutorial we will be using the same main script so you can save this as tutorial-sct.lua in your "tables" folder.

local mnProm = require("missionPromise")

engine.addHook("On Gameplay Start", function()
	async.run(function()
		mn.sendPlainMessage("Start of the coroutine!")

		local ship = async.await(mnProm.waitForShipAsync("TestShip"))

		mn.sendPlainMessage("Ship has arrived! Name: " .. ship.Name)
	end)
end)

Since you will also need a mission to test this in you can use this simple test mission that has a few ships set up that will arrive when either "1" or "2" is pressed.

If you run this now it will immediately cause a Lua error since waitForShipAsync does not return a value yet which is not accepted by async.await. Since this is about async.promise let's call it and see what happens:

function module.waitForShipAsync(ship_name)
	return async.promise(function(resolve, reject) end)
end

This will now no longer cause an error upon starting a mission. However, you will also never see the "Ship has arrived!" message since the promise that is waited on will never resolve. To see something happen let's make a small change to the module function:

function module.waitForShipAsync(ship_name)
	return async.promise(function(resolve, reject)
		resolve(hv.Player)
	end)
end

When you start the mission now you will immediately see the message "Ship has arrived! Name: Alpha 1". So, we now have a function that returns a promise which will immediately resolve to the player ship handle. However, that is not the handle to the correct ship since we expect to get a handle to "TestShip".

Next, we need to add an engine hook for when a ship has arrived in the mission since that is exactly the event we want to wait for. This can be done with engine.addHook. You pass as the first parameter the name of the hook and a function that will be executed on that hook as the second parameter:

engine.addHook("On Warp In", function()
end)

function module.waitForShipAsync(ship_name)
	return async.promise(function(resolve, reject)
		resolve(hv.Player)
	end)
end

In this hook we can check hv.Self to get a handle of the ship that is arriving. What we want to happen in that function is that if we have created a promise before this point with the ship_name set to the same value as the name of the arriving ship, we want to resolve that promise. The actual way we want to do that is to actually save the resolve callback we got in the promise constructor:

local arrivalListeners = {}
engine.addHook("On Warp In", function()
end)

function module.waitForShipAsync(ship_name)
	return async.promise(function(resolve, reject)
		arrivalListeners[ship_name] = resolve -- <--
	end)
end

We now have arrivalListeners as a table which maps ship name to the desired resolve function. In the warp in hook we can now simply check if we have a pending promise for that ship name and if so, resolve it:

engine.addHook("On Warp In", function()
	local resolver = arrivalListeners[hv.Self.Name]

	if resolver then
		resolver(hv.Self)
	end
end)

Try it out now. If you press "1" in the mission you will see "Ship has arrived! Name: TestShip".

That is it. You just created your first promise!

That code will work reasonably well but will break in subtle ways when used a bit more extensively. For example, if two coroutines want to wait for the same ship, only one will be notified since the previous resolve function will be overwritten. Also, if a ship has already arrived then the function will never resolve since we will never see the "On Warp In" event. Here is a "production ready" version of this script that will handle these edge cases properly. It will also cause the promises to reject when the mission ends so that promises waiting for ships that will never arrive do not stick around forever.

Thank you for joining this tutorial! If you have any questions, don't hesitate to ask them either on discord in the #scp channel or in the scripting subforum.