Introduction to Mission Scripting - Philbywhizz/pioneer GitHub Wiki
Pioneer is extensible through the Lua scripting language. This document is intended to take the reader through the basics of coding up a mission for Pioneer, with a worked example. The assumption is made that the reader is already aware of basic programming concepts, and in particular has some knowledge of the Lua programming language.
If you are not familiar with Lua, the lua-users wiki: Learning Lua page is a good place to start.
Pioneer's Lua environment
Pioneer contains a full Lua interpreter. There are three instances of the interpreter; one for defining models, one for defining the galaxy, and one for writing mission scripts. This document is focused exclusively on the latter of the three.
When Pioneer is started, the universe is generated. After the universe is generated, Pioneer traverses the data/libs directory and all subdirectories, looking for files ending in the .lua
extension. Each of these files are loaded into the interpreter and run. After these are run, Pioneer traverses the data/modules directory and does exactly the same thing. After this, the main menu is shown, and the player can begin a game.
The order in which the Lua scripts are loaded and run is not strictly defined, other than the fact that scripts in the data/libs directory will be executed before scripts in the data/modules directory.
Any globally defined functions or variables defined in those scripts will be available to any other scripts. We recommend that any functions or variables not explicitly intended for use by other scripts be declared with the local
keyword, which will make them available only within the scope of the current file.
Interacting with the game: Event-based programming
The Lua scripts are all executed at startup. If you were to add a single file named hello_world.lua
to the data/modules directory containing the following:
print("Hello, World!")
you would literally see the words, "Hello, World!" appear in Pioneer's output (if running in a terminal) shortly before the main menu appeared. You would also see it in the Lua console, if you were to open it.
All file-scoped imperative statements in all Lua files are executed at that time. The way to get Lua code to interact with the game itself, beyond that time, is to write functions and to connect them to event handlers. Many events are triggered by Pioneer during the course of play, all of which will cause any functions which are connected to them, to run. Most will provide those functions with arguments.
Here is a quick list of some of the more commonly used events:
onGameStart
is triggered when the player clicks on a new game button in the main menu, or when the player loads a game.onEnterSystem
is triggered whenever any ship arrives in the current star system after a hyperspace journey.onLeaveSystem
is triggered whenever any ship leaves the current star system by hyperspacing.onShipDestroyed
is triggered whenever any ship is destroyed.onShipDocked
is triggered whenever any ship docks at a starport.
There are many more. All are fully documented in the Pioneer Codedoc. Of the five that I have listed, only onGameStart
does not provide the function with any arguments. The other four provide a reference to the ship in question, and the latter two each also provide an additional argument (a reference to the attacker, and the starport, respectively).
Writing a function for an event
An event handling function does not have to return anything. It will be passed any arguments specified in the documentation, which it can either deal with, or ignore. It has access to any variables that are declared in the same file scope, including named functions and tables.
The following function sends a message to the player's ship console, welcoming them to Pioneer:
local welcome = function ()
UI.Message ('Welcome to Pioneer!')
end
The next one expects a ship, and if that ship is the player, it greets them again:
local greetShip = function (ship)
if ship:IsPlayer() then
UI.Message ('Hope you had an enjoyable hyperjump.')
end
end
All that remains is to attach these functions to events. The first, I'm going to attach to the onGameStart
event, the second, I'm going to attach to onEnterSystem
:
EventQueue.onGameStart:Connect(welcome)
EventQueue.onEnterSystem:Connect(greetShip)
EventQueue is a global table containing all of the events, which themselves have methods to connect and disconnect functions as handlers. Connect()
sets the function up as an event handler, and Disconnect()
is its (rarely required) counterpart.
It's actually common practice in Pioneer to name the function after the event:
local onGameStart = function ()
UI.Message ('Welcome to Pioneer!')
end
EventQueue.onGameStart:Connect(onGameStart)
Interacting with the player: BBS forms
The bulletin board system is currently the only place where real dialogue between a script and the player can take place. Bulletin boards can exist within any SpaceStation
in the current system. They are created in a station the first time that a ship is either spawned, or lands, in that station. They continue to exist until the player leaves the system or quits the game. They are not saved in saved games, although a saved game contains information about which ones did exist.
When a bulletin board is created, the onCreateBB
event is triggered, and passes the SpaceStation
body in which that bulletin board was created. There is an exception to this: The event is not triggered after loading a game for those bulletin boards which, having existed at the time of saving, are re-created. The consequences of this will be covered later (see "surviving a reload").
There are two components to any mission's entry on a bulletin board: The advert and the form. The advert is the part that is displayed on the main bulletin board list, along with all the other entries. The form is the part that appears on screen when the player clicks the advert's button.
Internationalization
It is important that scripters are familiar with the Translations library (scroll down to "Lua module translation"). A list of Translate:Add()
calls are usually placed in a file named Languages.lua
and kept in the same subdirectory as the script that needs them.
This is by convention only; all translations, regardless of filename, are available to all scripts, and it would technically be perfectly possible to recycle the translated strings provided by another script. In order to keep the workload as simple as possible for translators, however, we do prefer the convention to be followed.
The script itself makes use of translated strings by getting the translator function and using it to translate strings by token. So, if the Languages.lua
file contained this:
Translate:Add({
English = {
['Please, help me!'] = 'Please, help me!',
},
Deutsch = {
['Please, help me!'] = 'Bitte, hilf mir!',
},
})
then in the main script, you would first get the translator function:
local t = Translate:GetTranslator()
where t
could be any other convenient name, in the event that you already have a variable named t
. Once you have your translator, you can simply use it to translate by giving it the key:
UI.Message(t('Please, help me!'))
This would put "Please, help me!" on the screens of English players, and "Bitter, hilf mir!" on the screens of German players.
The translator is fully documented in the codedoc. For the remainder of the document, I shall be using string literals. This is for clarity only; all custom scripts should be fully translatable if they are intended to be included with the game.
The BBS advert
note: Information in this section is likely to become out of date after the libRocket interface is introduced
Adverts are placed onto a BBS by calling the station's AddAdvert()
method, once the bulletin board has been created. Depending on the nature of your script, you might want to always place one advert on every station (as seen with the Breakdowns & Servicing script), or you might want to place an arbitrary number of adverts on a given station (as seen with deliveries, or assassinations).
The opportunities to add an advert are presented by two events. onCreateBB
is the obvious one; there is also onUpdateBB
, which is called for all existing bulletin boards in the current system, approximately once every hour or two. The actual interval is not particularly predictable.
The AddAdvert()
method takes three arguments. The first is the text that will appear on the advert. The second is the function that will be called when the player clicks the advert. The third is optional, and is a function that can be called when the advert is deleted for any reason.
AddAdvert()
returns a reference number, which can subsequently be used to remove the advert using RemoveAdvert()
.
It's the job of the scripter to decide how many, if any, adverts to add to a bulletin board when onCreateBB
is triggered, and whether to add or remove any when onUpdateBB
is triggered. Tests could include the population of the system, the type of starport or the type of planet. In the future, tests will be able to include the government type of the system.
One important thing to bear in mind is that the script cannot query a bulletin boad to find out what adverts already exist. Each script must carefully track each advert that it has created if it is to have any control over how long they remain, and to be able to re-create them after a reload.
Here is an example of adding an advert. The effect of clicking the advert is simply to send a message to the player console with the name of the station. The advert is only added if the station is in space. There is no mission here; I am simply illustrating the mechanics of adding the advert.
local onCreateBB = function (station)
-- This function can be in any scope that's visible when AddAdvert() is called
local sendStationName = function ()
UI.Message(station.label)
end
if station.type == 'STARPORT_ORBITAL' then
station:AddAdvert('Need the name of this station?',sendStationName)
end
end
EventQueue.onCreateBB:Connect(onCreateBB)
This code will create an advert:
Looking at the image, you will notice that my advert has appeared at a completely arbitrary location on the bulletin board. There is no way to specify the location, and no way to determine it.
Clicking on the advert causes this to happen:
Even though the only thing our function did was to send a message to the console (visible at the bottom), you can see that Pioneer automatically created a form for our advert. The only control is the "Go back" button, and the name and face are defaulted to that which was displayed on the main bulletin board.
The BBS form
note: Information in this section is likely to become out of date after the libRocket interface is introduced
Once the player has clicked on an advert, they are presented with a form. Each advert has only one form. The content of the form is added by the script, and can be modified at any time. It consists of a title, a face, a message and zero or more clickable options.
The form itself is passed to the function specified in the SpaceStation.AddAdvert()
method. In the example above, that function would be sendStationName()
, which simply ignored any parameters sent to it. This resulted in the form being blank.
The form object which is passed to this function has methods for adding the content. SetTitle()
and SetMessage()
each accept a string. SetFace()
takes a table of information which defines the photofit face on the left. AddOption()
adds clickable options with buttons. Clear()
removes the Message and Options, but preserves the Title and Face, while Close()
acts the same way as the "Go back" button.
The following example doesn't have any clickable options, but does have a customized form:
local populateForm = function (form)
local facedata = {
name = "Bob",
female = true,
title = "Lorry driver",
}
form:SetTitle('This appears above the face picture')
form:SetFace(facedata)
form:SetMessage([[This is the main message.
It is normally a multi-line string.]])
end
local onCreateBB = function (station)
station:AddAdvert('This appears in the advert list',populateForm)
end
EventQueue.onCreateBB:Connect(onCreateBB)
As before, an advert was created:
Randomly, it has appeared at the top of the list.
Clicking on the advert causes this to happen:
Now we can see that populateForm
was called, and it successfully filled the form with content. All of the face options were optional; if they aren't provided, Pioneer will choose random values. I have left out a couple of options here; it's wise to specify them all, because after a saved game is loaded, it's the script's job to make sure that the same face appears (see "surviving a reload").
Adding options to the form
In the example above, I created a function named populateForm()
which was run when the advert button was clicked. That's not the only time it can be run; it is also run whenever options on its form are clicked.
To make use of this, it is passed two additional parameters, both of which populateForm()
ignored. The first parameter is the form object, the second is the advert's unique reference and the third is the number of the option that was clicked. Because it handles all chat events, by convention we instead name this function onChat()
, which is how it shall be named from now on.
The previous example did not store or use the value returned by AddAdvert()
. It is this value which is sent as the second parameter; very useful if your script adds several adverts, each of which might have slightly differently worded content.
When the advert was clicked in the main bulletin board list, it was passed the value 0
as the option.
Form options are declared like this:
form:AddOption('I am option one',1)
form:AddOption('I am option two',2)
These options will appear with the specified caption, and will call onChat()
, which will receive the form object, the advert reference and the number that was specified after the caption in AddOption()
.
The codedoc has a brilliant example of a complete onChat, which I will reproduce here:
local onChat = function (form, ref, option)
form:Clear()
-- option 0 is called when the form is first activated from the
-- bulletin board
if option == 0 then
form:SetTitle("Favourite colour")
form:SetMessage("What's your favourite colour?")
form:AddOption("Red", 1)
form:AddOption("Green", 2)
form:AddOption("Yellow", 3)
form:AddOption("Blue", 4)
form:AddOption("Hang up.", -1)
return
end
-- option 1 - red
if option == 1 then
form:SetMessage("Ahh red, the colour of raspberries.")
form:AddOption("Hang up.", -1)
return
end
-- option 2 - green
if option == 2 then
form:SetMessage("Ahh green, the colour of trees.")
form:AddOption("Hang up.", -1)
return
end
-- option 3 - yellow
if option == 3 then
form:SetMessage("Ahh yellow, the colour of the sun.")
form:AddOption("Hang up.", -1)
return
end
-- option 4 - blue
if option == 4 then
form:SetMessage("Ahh blue, the colour of the ocean.")
form:AddOption("Hang up.", -1)
return
end
-- only option left is -1, hang up
form:Close()
end
Here, every time onChat()
is called, regardless of the specified option, the form is cleared. The option is checked, and the relevant content is added to the form. Any other functions can be called from here, and this is how the script gets input from the player.
An alternative format might be this:
local onChat = function (form, ref, option)
form:Clear()
local options = {
[0] = function ()
form:SetTitle("Favourite colour")
form:SetMessage("What's your favourite colour?")
form:AddOption("Red", 1)
form:AddOption("Green", 2)
form:AddOption("Yellow", 3)
form:AddOption("Blue", 4)
form:AddOption("Hang up.", -1)
end,
[1] = function ()
form:SetMessage("Ahh red, the colour of raspberries.")
form:AddOption("Hang up.", -1)
end,
[2] = function ()
form:SetMessage("Ahh green, the colour of trees.")
form:AddOption("Hang up.", -1)
end,
[3] = function ()
form:SetMessage("Ahh yellow, the colour of the sun.")
form:AddOption("Hang up.", -1)
end,
[4] = function ()
form:SetMessage("Ahh blue, the colour of the ocean.")
form:AddOption("Hang up.", -1)
end,
[-1] = function ()
form:Close()
end
}
options[option]()
end
The player's mission list
Once the player has negotiated with your form, there might well be a mission in play. It could be a delivery, an assassination, a rush to tell somebody not to leave because so-and-so loves them... the possibilities are limited only by your creativity.
The player needs a way to keep track of all the missions that they have agreed to undertake. Pioneer provides this through the player's mission screen, which they can access at any time using the F3 button, and looking at the missions tab.
The content of this screen is controlled by some methods on the Player
object, which can always be found at Game.player
, and which inherits from Ship
and Body
. Missions are added to the screen using the AddMission()
method. It takes a table of info, and returns an integer reference to that mission, which should be stored so that it can be updated or removed later. So, it usually looks a little like ref = Game.player:AddMission(table_of_info)
.
In practice, it might look more like this:
local mission_storage={}
table.insert(mission_storage,Game.player:AddMission({
type = "Fetch beer",
client = "Bert Beerbreath",
due = Game.time + 600, -- ten minutes' time
reward = 10,
location = Game.player.frameBody.path, -- here, basically
status = 'ACTIVE'
}))
table.insert(mission_storage,Game.player:AddMission({
type = "Fetch curry",
client = "Curt Curryface",
due = Game.time + 900, -- fifteen minutes' time
reward = 5,
location = Game.player.frameBody.path,
status = 'ACTIVE'
}))
I don't recommend using Game.player.frameBody.path
here. I'm only using it because it always returns something, whether docked or not. A real mission would probably use a space station here.
This creates visible missions on the mission screen:
These missions will remain exactly like that forever, unless a script explicitly makes changes. There is no automatic logic, and no automatic removal. Your script must keep track of them. In this example, I have the references in the mission_storage
table.
The UpdateMission()
method allows a script to alter any detail. The status can be 'ACTIVE'
, 'FAILED'
or 'COMPLETED'
. I'm going to change the status of the first mission to 'COMPLETED'
.
Game.player:UpdateMission(mission_storage[1],{status='COMPLETED'})
The updated information can be a partial table. The unspecified members will remain unchanged:
The GetMission()
method allows a script to read data from a mission, if the script has the reference. It returns a table with that information. Here, I'm going to add ten minutes to the deadline of the second mission:
local due_date = Game.player:GetMission(mission_storage[2]).due
Game.player:UpdateMission(mission_storage[2],{due = due_date + 600})
The result:
The RemoveMission()
method allows a script to remove a mission. Here I remove the completed one:
Game.player:RemoveMission(mission_storage[1])
table.remove(mission_storage,1)
And here it isn't:
Mission flavours
A script can define a mission, and then place many instances of it onto many bulletin boards. A script that introduces many instances should provide some variety; it would harm immersion if all delivery missions were worded identically, for example.
To introduce variety, we can use tables, which contain all of the lines needed for bulletin board forms, messages and so forth. It's then a simple matter to use a similar, but differently worded, table to make the same misison appear different. A flavour might look like this:
{
title = "Shill bidder wanted for auction",
greeting = "Hi there. Want to earn some quick cash?",
yesplease = "Sure. What do you need me to do?",
nothanks = "No, ta - this looks a bit shady.",
}
An alternative flavour might look like this:
{
title = "Help me win an auction.",
greeting = "Hello. I'll pay you to place fake bids for me. Interested?",
yesplease = "Yes, I like to live dangerously.",
nothanks = "No thanks; I'd rather not get arrested for fraud.",
}
Ideally, we just need a table with as many of these as we can be bothered to write, then select one at random.
There are issues with translation, though. Because flavours are often full of colloquial language, and can feature several ways of saying basically the same thing, translating them all word-for-word is not necessarily productive. To this end, the Translate system features a pair of methods which can ease the handling of flavours, especially in different languages.
The Translate:AddFlavour()
method allows a flavour to be added, and marked as being of a specific language. By convention, these statements would appear in the script's Languages.lua
file so that translators could see them and be inspired to write some in other languages. The method takes three arguments. The first is the language of the flavour, as a string. The second is a name, so that the Translate
class can give flavours back to the correct script. The third is the flavour table itself. If my script was named TestModule
, I might define the two flavours above in my Languages.lua
as follows:
Translate:AddFlavour('English','TestModule',{
title = "Shill bidder wanted for auction",
greeting = "Hi there. Want to earn some quick cash?",
yesplease = "Sure. What do you need me to do?",
nothanks = "No, ta - this looks a bit shady.",
})
Translate:AddFlavour('English','TestModule',{
title = "Help me win an auction.",
greeting = "Hello. I'll pay you to place fake bids for me. Interested?",
yesplease = "Yes, I like to live dangerously.",
nothanks = "No thanks; I'd rather not get arrested for fraud.",
})
As an added benefit, Translate:AddFlavour()
checks all flavour tables for uniformity. It does this by comparing all of the table's keys with that of the first English flavour that was specified; a technical consequence of this is that an English flavour needs to come first in your Languages.lua
file, otherwise Pioneer will give you an error.
To fetch the flavours into your script, use Translate.GetFlavours()
. It takes a single argument, which is the module name that was specified in AddFlavour()
's second parameter. It doesn't matter to Translate
how many flavours there are in each language, or whether it's an equal number. If my current language is not English, and there are no flavours in that language, English flavours will be used instead. If there is only one flavour in my language, I will only see that one variety.
The following example will select a random flavour from my flavour tables.
local all_flavours = Translate:GetFlavours('TestModule')
local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)]
GetFlavours()
returns a table of flavours. The second line of code generates a random number between 1 and the number of flavours in that table, and returns that flavour from the table. After this, flavour.title
will either be "Shill bidder wanted for auction"
or "Help me win an auction."
. Adding more flavours means more variety.
Maintaining immersion
Fictionally, of course, this bulletin board is visible to any and all ships that dock at the space station, not just the player. It is important that bulletin board missions are not all scaled to the capabilities of the player. Delivery missions with unreasonable deadlines should not be ruled out. Neither should cargo missions requiring much more cargo space than the player's ship has, or combat missions for which the player is completely unqualified.
These missions should deal with the player gracefully; either allowing them to fail, and providing consequences, or preventing them from being given the mission.
It's also important, if your script serves many instances of a mission, to periodically clear away bulletin board adverts and place new ones. Not just those with obvious time constraints, but any others; the assumption that the player should make is that perhaps some other character has taken these missions.
Surviving a reload
When a game is saved, a script is responsible for informing Pioneer exactly which of its data need to be saved. If nothing is specified, nothing will be saved at all. Similarly, after a game is loaded, a script is responsible for restoring all of its saved data; re-creating bulletin board adverts, the player's mission details, and any run-time state.
Here is a simple form. If a new game is started, you will see this on every bulletin board that you visit:
local onChat = function (form, ref, option)
form:Clear()
form:SetMessage("Hello!")
end
local onCreateBB = function (station)
station:AddAdvert('Click here for a greeting',)
end
EventQueue.onCreateBB:Connect(onCreateBB)
If, however, you save the game, then reload, you will not see it on any bulletin board that had been created in the current system before you saved. onCreateBB
will be triggered for any subsequently created bulletin boards, but not for those that already existed. It isn't appropriate to have scripts create new adverts just because a player has loaded; instead, it's the responsibility of the script to tell Pioneer what to save, and to use those saved data to restore all adverts after the game is loaded. The same goes for player missions, and any working data that the script is using.
There are two things we need to do to achieve all of this.
- We need to track everything that the script is doing.
- We need to be able to pack this away for saving, and bring it back after loading.
Tracking everything that we are doing
Any local table that is declared in file scope is visible to the entire script, without affecting any other scripts. Essential data should be kept in such tables.
Tracking adverts
It's important to be able to re-create an advert, so part of the process of making one should be storing that information. Your script will need to make a note of in which station the advert was placed, which flavour was used (if you have flavours), the face data that was used on the form and any unique information that was used, such as specific mission details, reward, etc. The simplest way to keep all of this together is in a table, gathering up the variables by name into keys of the same name:
local advert = {
station = station,
flavour = flavour, -- This will be a table
facedata = facedata, -- This will be a table
target = target,
destination = destination,
reward = reward,
}
Remember, the AddAdvert()
method takes three arguments: The advert text, the onChat
function and the onDelete
function. It also returns a unique number, which lends itself nicely to storing the information away. That same unique number is passed to the onChat function, allowing onChat to check that stored information.
The onDelete()
function (again, we call it that by convention only) also accepts this reference as its argument, and is called whenever an advert is destroyed, whether that destruction be explicit (RemoveAdvert()
) or implicit (the player hyperspaces away).
So, we can track adverts that have been created, and stop tracking any that have gone away. I'll do that using the reference returned by AddAvert()
as the key to a file-scoped local table:
local all_flavours = Translate:GetFlavours('TestModule')
local all_adverts = {}
local onChat = function (form, ref, option)
form:Clear()
form:SetFace(all_adverts[ref].facedata)
form:SetMessage(all_adverts[ref].flavour.title)
end
local onDelete = function (advert_ref)
all_adverts[advert_ref] = nil
end
local onCreateBB = function (station)
local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)]
advert_ref = station:AddAdvert(flavour.title,onChat,onDelete)
local female = Engine.rand:Integer(1) == 1
all_adverts[advert_ref] = {
station = station,
flavour = flavour,
facedata = {
female = female,
name = NameGen.FullName(female),
seed = Engine.rand:Integer()
}
}
end
EventQueue.onCreateBB:Connect(onCreateBB)
Tracking missions
This is less of a challenge. Whenever Player.AddMission()
is called, it returns a reference to the mission. That reference can be stored in a file-local table, and that table can be iterated through, and Player.GetMission()
called on each one. We can wrap these functions up:
local missionrefs = {}
local AddMission = function(mission)
table.insert(missionrefs,Game.player:AddMission(mission))
return missionrefs[#missionrefs]
end
local RemoveMission = function(mref)
for i,m in ipairs(missionrefs) do
if m == mref then
table.remove(missionrefs,i)
end
end
Game.player:RemoveMission(mref)
end
Now, instead of calling Game.player:AddMission()
and Game.Player:RemoveMission()
, we just directly call our local functions, AddMission()
and RemoveMission()
. They will send their argument on to the instance in Game.player
, and also store or remove the mission ref in the missionrefs table. There is now a record of which missions belong to this script.
Serializing: Packing away for saving and loading
The Serializer
object provides one method: Register
. It takes three arguments. The first is a name; a simple string with which to uniquely identify this script. It's probably sensible to re-use the name that was used for the Translate
object's flavour methods. The second argument is the name of a function which will return a single table. The third argument is a function that will accept a single table.
The second argument is your serializer function. The third is your unserializer function. By convention, we name these serialize
and unserialize
.
serialize
must return a table. This table must contain everything that you need to store to get your script working after a reload. It will be run when the player saves the game.
unserialize
accepts a table, and does something with it. It will be run by the serializer after the game is loaded, immediately before the onGameStart
event is triggered. It is passed the data that serialize
returned at save time.
The most common way to deal with this is as follows (and this is very cut down):
local table_stuff_this_script_uses = {}
local loaded_data
-- The rest of the script goes here!
local onGameStart
if loaded_data then
for k,v in loaded_data.table_stuff_this_script_uses do
table_stuff_this_script_uses[k] = v
end
loaded_data = nil
else
-- New game; do other stuff here perhaps
end
end
local serialize = function ()
return {table_stuff_this_script_uses = table_stuff_this_script_uses}
end
local unserialize = function (data)
loaded_data = data
end
EventQueue.onGameStart:Connect(onGameStart)
Serializer:Register("TestModule", serialize, unserialize)
Now, combining that with the examples above, we get the following, which will save and load both the adverts and the missions that the script created:
local all_flavours = Translate:GetFlavours('TestModule')
local all_adverts = {}
local missionrefs = {}
local onGameStart = function ()
all_adverts = {}
missions = {}
if not loaded_data then return end
for k,ad in pairs(loaded_data.all_adverts) do
local ref = ad.station:AddAdvert(ad.flavour.title, onChat, onDelete)
all_adverts[ref] = ad
end
for k,mission in pairs(loaded_data.missions) do
local mref = Game.player:AddMission(mission)
table.insert(missions,mref)
end
loaded_data = nil
end
local onChat = function (form, ref, option)
form:Clear()
form:SetFace(all_adverts[ref].facedata)
form:SetMessage(all_adverts[ref].flavour.title)
-- some selective code here that will add missions, etc.
end
-- Other event handlers here that might change or remove missions or adverts
local onDelete = function (advert_ref)
all_adverts[advert_ref] = nil
end
local onCreateBB = function (station)
local flavour = all_flavours[Engine.rand:Integer(1,#all_flavours)]
advert_ref = station:AddAdvert(flavour.title,onChat,onDelete)
local female = Engine.rand:Integer(1) == 1
all_adverts[advert_ref] = {
station = station,
flavour = flavour,
facedata = {
female = female,
name = NameGen.FullName(female),
seed = Engine.rand:Integer()
}
}
end
local AddMission = function(mission)
table.insert(missionrefs,Game.player:AddMission(mission))
return missionrefs[#missionrefs]
end
local RemoveMission = function(mref)
for i,m in ipairs(missionrefs) do
if m == mref then
table.remove(missionrefs,i)
end
end
Game.player:RemoveMission(mref)
end
local serialize = function ()
local missions_in_full = {}
for k,mref in ipairs(missions) do
table.insert(missions_in_full,Game.player:GetMission(mref))
end
return { all_adverts = all_adverts, missions = missions_in_full }
end
local unserialize = function (data)
loaded_data = data
end
EventQueue.onGameStart:Connect(onGameStart)
EventQueue.onCreateBB:Connect(onCreateBB)
Serializer:Register("TestModule", serialize, unserialize)
Although the example is functional, it has been coded with brevity alone in mind. I would not recommend using this as the basis for a mission without at least completely rewriting onChat()
, and possibly tidying up the temporary loop variable names.
The codedoc is complete and accurate, and the scripts provided with Pioneer are good examples themselves. The API is extensive, but if you find that there are additonal things you would like to be able to do, or information you would like from Pioneer, the dev team are willing to extend the API to accommodate script writers. Simply create a new issue on the Github issue tracker.
Help is also always available on the Pioneer forum on SpaceSimCentral.com, and many of the dev team can be found on IRC at all hours - simply connect to Freenode (irc.freenode.net channel #pioneer).