Difference between revisions of "FS2 Open Lua Scripting"

From FreeSpace Wiki
Jump to: navigation, search
(Updated page to reflect changes in newer builds)
(+Override: Make FSO ignore things: clarify syntax)
 
(7 intermediate revisions by 4 users not shown)
Line 1: Line 1:
This page is intended as a crash-course in Lua scripting as it relates to Freespace 2. It's only meant to get you started with Lua scripting - more advanced concepts such as metatables will not be covered.
+
This is an updated tutorial intended to give an introduction and overview into using Lua for scripting in FSO. It is split into three sections. Scripting with FSO, examples for common pitfalls or special situations, and best practices (both for Lua in general and in conjunction with FSO). It is written as of January 2023, for FSO 23.0.0, and some scripting functions or names used may have changed since then. If you spot something that does not work or exist any more, please let us know!
  
You don't need to read the Appendices, but I recommend at least reading the "Optimizing" section if you're doing much scripting.
+
==Requirements and Resources==
 +
One of the most important resources for scripting in FSO is the scripting documentation provided by FSO. To access it, check the FSO launch flag "Output scripting to scripting.html" under "Dev Tool" in Knossos (or pass -output_scripting with the launcher of your choice) and launch the mod. Note that the mod has to actually launch. If FSO throws an error on startup, it'll not generate scripting.html.
 +
This will generate a file called scripting.html. It will probably be located in the same folder where your retail FS2 root.vp is located. If you open the file in your browser, you will see a long list of various different things. It'll be confusing now, but don't worry, we'll get to it in time.
 +
Furthermore, there exists a [https://github.com/FSO-Scripters/fso-scripts public repository of available scripts] for FSO. It may be worth checking what is already available, or how certain problems have been solved by other people if you are stuck. Also, if you have completed and tested a script, you are welcome to add it to the scripting repository.
 +
==Lua Basics==
 +
For a more professional, long term look at Lua I recommend having a look at [https://www.lua.org/pil/contents.html Programming in Lua], but it certainly isn’t a required read to do scripting. For a slightly shorter text, see the following:
 +
==Lua Basics==
 +
So, in order to be able to script with Lua, the first question obviously has to be "how does one even Lua?".
 +
At its very basic steps, programming, in most languages, is a combination of calculating things, and making decisions from it. And Lua is no different from that.
 +
So to start off, let’s learn to calculate a simple addition and to save that.
 +
Now, the calculation part is pretty straightforward.
 +
1 + 1
 +
That is enough to tell Lua to calculate 1 + 1. But we don’t save it, so it’s not very useful to us.
 +
calculation = 1 + 1
 +
But that is also not too hard. This has now created something called a variable. If we later say something like <tt>calculation + 1</tt> it will yield 3, because <tt>calculation</tt> is now 2. This variable we have now called "calculation", but any name consisting of letters and underscores are allowed. Other special characters, especially spaces, must not be part of a variable name.
 +
Something you should remember is that in Lua, variables can be local and global. What we did above is make a global variable. That means that it can be accessed from everywhere, including other scripts.
 +
But we could have also written the following
 +
local calculation = 1 + 1
 +
That would have made calculation a local variable. The difference is, that a local variable is only available where you created it. It cannot be accessed by another script.
 +
And if you created the variable for example in a loop, or in an if (we will learn what these are shortly), then you cannot access the variable after this loop or if as well. As a rule of thumb, try to make something a local variable if possible.
 +
Now, variables are called variable for a reason: You can also change them after you have created them:
 +
calculation = calculation + 1
 +
This increases the calculation variable by one. Unpacking what this line of code says one by one, we can also describe it as follows: Set the value of calculation to the what the value of calculation was plus one.
 +
Variables can store more than numbers, however. Everything you can do stuff with in Lua can be made into a variable. Some examples below:
 +
local number = 1
 +
local number_with_decimal_point = 1.5
 +
local string = "Can be any text"
 +
local boolean = true --true or false, used for logical operations
  
<p>'''IMPORTANT: This page was last updated on 5/27/2007 and assumes you are using the most up-to-date build at that time, [http://www.hard-light.net/forums/index.php/topic,47201.0.html C05202007]. There may be slight syntax differences with significantly newer or older builds. You should also make sure you are using a build from the Unstable/HEAD CVS branch.'''</p>
+
But what do we do now with these variables?
 +
One of the most important things in programming is having our program make decisions. In Lua, one way of doing this is an if statement. The way it is programmed is almost like you would phrase a conditional sentence in English: If some condition is true, then do something, else do something different. Let’s say we want to add 1 to <tt>calculation</tt> if it is smaller than 2, and remove 1 otherwise. In Lua, it looks like this:
 +
if calculation < 2 then
 +
calculation = calculation + 1
 +
else
 +
calculation = calculation - 1
 +
end
 +
Basically the same, except that after the thing to do when the condition is not fulfilled must be an <tt>end</tt>. The condition can be anything really, from comparing if two things are the same (using ==, with two equals signs), or if something is smaller as something else like in the example. You can even combine multiple conditions using <tt>and</tt> or <tt>or</tt> if it is needed.
  
== What You Need ==
+
Similarly to if, there are loops to repeat things.
You will need a basic understanding of how to open and edit computer files, a copy of Freespace 2, and an open mind. Any kind of prior scripting experience will be immensely helpful, but you shouldn't need any to understand this document.
+
If you write <tt>while</tt> instead of <tt>if</tt>, Lua will repeat the content of the while loop as long as the condition is true.
 +
a = 5
 +
b = 5
 +
result = 0
 +
while a > 0 do
 +
a = a - 1
 +
result = result + b
 +
end
 +
This is a very simple multiplication program that uses a while loop and basic arithmetics to calculate the product of <tt>a</tt> and <tt>b</tt> and saves it in <tt>result</tt>.
  
Here we go!
+
Often, you will end up in a situation where you want a variable to be global. But if you have many variables, at some point it becomes hard to keep track what is what.
 
+
One tool to help you organize your variables are tables. These can serve as a container to keep multiple variables together. The following example shows how you can create a table and store a variable inside it:
<b>Note:</b> Although all the examples should not cause syntax errors anywhere, they may not work in certain places. For example, you have to load a mission before you can get handles to ships in the mission.
+
table1 = {
 
+
variable1 = 1
== Scripting hooks ==
+
}
A scripting hook is a segment of Lua code, that is run at a specific place in FS2_Open. Some scripting hooks, such as $GameInit, are executed only once. Others, such as state hooks, are executed every frame. Others may be executed only when a specific action occurs (such as a keypress or a ship warping out).
+
table2 = {
 
+
variable1 = 2,
There are three different types of hooks. All hooks are parsed and loaded into memory when they are read, so there is no speed difference. Also, the optional field "+Override: YES" can be used to make a scripting hook completely override fs2_open's usual behavior. (For example, for the HUD, this would completely disable the default fs2_open HUD and use the scripting version instead)
+
variable2 = 3
 
+
}
For the sake of organization, information on different scripting hooks can be found in the wiki page for the table that they are actually in.
+
table1.variable2 = 4
 +
--Both tables now have entries called variable1 and variable2, both with different values
 +
 +
--Access for reading works the same as for writing:
 +
table1.variable1 = table1.variable2 + table2.variable1
  
=== Return hook ===
+
But that is not the only think tables can do. They can also help you to keep a list of values, even if you don't know how many values you will have.
$Hook: 2 + #Mission.Ships
+
We can make a table that is basically a list, and then do something for all elements in the list.
This works like a normal table variable, except that you can use scripting functions as well.
+
list = {}
 
+
  --Now the list is still empty
=== Normal hook ===
+
table.insert(list, 5)
  $Hook: [
+
table.insert(list, 2)
gr.setColor(255, 255, 255)
 
gr.drawString("Hello, world!")
 
 
   
 
   
return 2 + #Mission.Ships
+
--Now we have two elements in our list, 5 and 2
  ]
+
  for i, value in ipairs(list) do
A normal hook is denoted by [ and ]. Whatever is within the brackets is passed to the Lua interpreter. This means that you cannot use table-style ;; comments within brackets; you must use Lua's comments.
+
--This piece of code will be called once for each element in the list, no matter how many elements are in the list.
 
+
--i is the number of the element in the list, and value is the value of the element
However, you can still use it with a .tbl field that takes a value, using the "return" command. In this case, even though the other lines are still run, the $Hook: variable would be set to the same value as in the 'Return hook' example.
+
--In this example, this piece of code will be called with i = 1 and value = 5 as well as with i = 2 and value = 2
 
+
end
=== File hook ===
 
<pre>$Hook: [[script.lua]]</pre>
 
A file hook loads the scripting from the file in the data/scripts directory. In all other respects, it is the same as a normal hook.
 
 
 
== Comments ==
 
A "comment" (in any programming or scripting language) is a block of a script that is ignored. In Lua there is only one kind of comment, which starts with double-dashes. Any time you put two dashes in a Lua script outside of a string, the rest of the line will be ignored. (This is the same as putting a double-semicolon in a Freespace table)
 
 
 
For example, this:
 
--Load a mission for use in the mainhall
 
Mission.loadMission("sm2-10.fs2") --Use 'King's Gambit' for now.
 
 
 
Will do exactly the same thing as this:
 
Mission.loadMission("sm2-10.fs2")
 
 
 
== Variables ==
 
A variable is the simplest kind of thing that there is in Lua. All it really does is hold data; no more, no less. They really aren't that scary. Here are some different types of variables
 
  
  --Number variable
+
There is one more helpful use of tables. You can also use them to store values for a certain name or index.
  life = 42
+
Consider the use case that we want to store the balance of multiple people. For this, we could use the following:
 +
accounts = {}
 +
  --Now the list is still empty
 +
accounts["John"] = 5
 +
  accounts["Frank"] = 20
 +
--Now the list has two elements, one stored for the name John and one for Frank.
 
   
 
   
  --String variable
+
  --We can also access them again:
  name = "Bob"
+
  local johnsMoney = accounts["John"]
 
   
 
   
  --Boolean variables
+
  --using a loop for all elements works too, but a bit differently:
  smoking = true
+
  for name, value in pairs(list) do
  healthy = false
+
--Name will be the name, and value the value stored for this name
+
  end
  --Userdata (Handle or object) variable
+
 
player = Mission.Ships['Alpha 1']
+
Finally, we need to have a look at one last tool: Functions. In many cases, when you write Lua code, there will be parts of your code that you want to reuse somewhere else.
+
But instead of copying the code, we can make it a function. Let us consider the multiplication example from before. Instead of copying that to everywhere we would need it, we can convert it to a function.
--Nil variable
+
  function multiply(a, b)
--Erases a previously set variable
+
result = 0
  life = nil
+
while a > 0 do
 +
a = a - 1
 +
result = result + b
 +
end
 +
return result
 +
  end
 
   
 
   
--Member variable of userdata. Can be any of the above types.
+
As you can see, the function consist of three main parts. First, the function declaration. After <tt>function</tt>, you write the name you want to use for the function, followed by its parameters in parenthesis.
  player.Name = "Ship"
+
This list of parameters can be as long as you want, including having no parameters. The names you use for the parameters are then available to use in your functions like a variable would be.
 +
The second part is the actual code of the function. This code can contain the word <tt>return</tt>. Return means that the function will be quit, and if there is a value or a variable named after <tt>return</tt>, it will be returned to the function caller as the reuslt.
 +
Finally, the function is terminated with an <tt>end</tt>
 +
To use the function, simply call its name and fill in any arguments you may have added:
 +
  local multiplicationResult = multiply(2, 4)
 +
--multiplicationResult will be 8
  
<b>Note:</b> Although technically objects and handles are all userdata, it's easier to just think of every object and handle as being different types of variables.
+
For organization, functions can also be part of tables. For this, just put a colon between the table name and the function name:
 +
table = {}
 +
function table:functionInTable()
 +
--Do something
 +
end
  
=== Arrays ===
+
One special thing when using functions in tables is that the function has access to other values in the table by using the name <tt>self</tt>. It can be used like any other variable, and will be the table that contains the function. See the following example:
 +
table = {
 +
test = 5
 +
}
 +
function table:functionInTable()
 +
return self.test --This will return 5
 +
end
  
An array is a special type of variable, that holds multiple variables. The '''''index''''' of all Lua arrays starts at 1.
+
==Interactions with FSO==
 +
Knowing Lua we can now consider how we can use that to make FSO do things.
 +
===Running Code / Conditional Hooks===
 +
Now, we know how to write very basic code. But how do we actually run it?
 +
For this, FSO uses conditional hooks, specified in the [[Scripting.tbl|scripting table (*-sct.tbm)]].
 +
At the basic level, conditional hooks are made up of three parts, action, condition and code.
 +
* First, the action: An action is effectively an event in FSO at which we can run our custom code. The very basic example is <tt>On Game Init</tt>. This action is run once when FSO starts up, and can be used by us to run scripts that need to happen during this start up. A list of all available actions and when they trigger can be found in the scripting.html, reasonably far up, under the section Conditional Hooks and then Actions.
 +
* Second, one or more conditions: The condition is a tool to have more fine-grain control over ''when exactly'' hooks are run. Imagine you want to script a special afterburner effect using the <tt>On Afterburner Engage</tt> action. But since this new effect should only apply to one ship class, we don't want our script to run for other ship classes. In this case, we could use the <tt>Ship class</tt> condition to limit the hook to a specific ship class. Similarly, since most scripts should run just in FSO and not in FRED, setting the <tt>Application</tt> condition to <tt>FS2_Open</tt> is a good idea in most cases as well. A list of conditions that can be used for all hooks is listed in scripting.html under Conditional Hooks and then Conditions, with further explanation  [[Scripting.tbl#Conditions|here]]. Some conditions are only available for some specific actions, and may mean slightly different things for different actions. These are documented for each action individually in scripting.html.
 +
* Third, scripting code that is run. This can be specified in two different styles. Either, just write a block of Lua code in between two brackets <tt>[ like this ]</tt>, or specify a file in the scripts folder that contains the code in double brackets <tt>&#91;&#91;sample.lua&#93;&#93;</tt>.
  
--Set the fifth element of array to 1
+
When specifying these conditional hooks in the scripting table, you first specify one or more conditions. Then you can specify one or more pairs of an action, and the code to be run. Until you specify the next set of conditions, all actions will have the last condition.
a[5] = 12
 
--Set n to the fifth element (tehe)
 
n = a[5]
 
--n will be 12
 
  
The size of arrays may also be determined using the '''''#''''' sign
+
Now we can use this to run some code:
 +
$Application: FS2_Open
 +
$On Game Init: [
 +
--This is now Lua code that is run.
 +
local variable = 1 + 1
 +
]
  
--Create an array
+
One thing to note is that all hooks share the same global space. This effectively means that all Lua functions we define and all variables we write are shared among all hooks.
a = {2, 4, 6, 8}
+
This is why it is a very common pattern to define the bulk of any script's code in a function in the <tt>On Game Init</tt> action, and then just call these functions from the specific conditional hooks for a cleaner script style.
--Set s to the size of the array
 
s = #a
 
--s will be 4
 
  
== Libraries ==
+
===Making FSO do Stuff / Library Calls and Variables===
In order to gain access to the FS2_Open engine through scripting, you must use the FS2_Open libraries. Each library represents a different section of the engine. In addition, libraries may be referenced one of two ways: the full name, or a shorthand version of the name made up of two letters. Here, the same setColor '''''function''''' is used twice.
+
In order to actually have any useful effect through our scripts, we need to be able to both tell FSO to do certain things, as well as be able to check on the state of certain gameplay elements.
  
Graphics.setColor(255, 255, 255)
+
In order to know what is possible, we need to reference scripting.html. Below the section listing the hooks is a listing of libraries. These libraries are (usually) the first point of interfacing with FSO. You can click the library names to jump to the point in scripting.html where they are documented. Searching through these functions and variables will likely take a significant amount of time during scripting, so it’s best to get familiar with how these functions are documented.
gr.setColor(255, 255, 255)
 
  
For the sake of easy reading, I've used the longhand version of names in the wiki. However, I recommend you learn the shorthand versions as they're generally faster to work with when scripting.
+
For a very basic script, we will just want to output some message to the screen to make sure we were able to correctly run code. Since that is a very basic operation, it’s located in the base library. We’ll use the <tt>warning</tt> function, so that we can see the message when using a debug build. The documentation tells us that the function takes a message as a string, and will return nothing.
 +
$Application: FS2_Open
 +
$On Game Init: [
 +
ba.warning("The script successfully ran!")
 +
]
 +
When running this in a debug build, we can now see our message at startup!
  
To get a full list of library functions, use the "-output_scripting" command line parameter. This should create a file called scripting.html, with a list of the different libraries, functions, and types supported by that build. (Use a web browser to open it)
+
Some things are exposed to Lua not as functions, but as variables. See for example XAxisInverted in the controls (io) library. You can access io.XAxisInverted like any other boolean with Lua. Reading this boolean will give you the current state, and writing to it will change it. Some of these values, notably those that are loaded from tables, cannot be written to and changed. In that case the documentation will be missing the <tt>= &lt;typename&gt;</tt> after the variable name.
  
 +
Many of the values you will access from FSO will not be standard Lua types, but special FSO types, like for example a ship. For example, <tt>mn.Ships[1]</tt> will return the first ship in a mission, with the <tt>ship</tt> type. To learn what the ship type is, you can click it in scripting.html to see the documentation. We can see that it inherits object (which means that you can use any ship the same way you can also use an object), and that it has a number of properties and functions, just as the libraries had. One thing that is important, is that if you call functions on ''FSO types'', you must use <tt>:</tt> before the function name, while you need to use <tt>.</tt> for functions directly within a library.
 +
Let's say that <tt>ship</tt> were an object of the type <tt>ship</tt>. We can use the following code to clear its orders and call it a new name:
 +
ship:clearOrders()
 +
ship.Name = "Renamed Ship"
  
== Functions ==
+
===Custom SEXPs===
A function is an instruction to Lua to do something. It may change variables, or it may call (activate) other functions. Functions are the meat of scripting; they get everything done. Essentially, they are much like an individual SEXP.
+
In a lot of cases, we don’t actually want to define our code in hooks. Often we want much more fine grain control when things are run, depending on events in FRED and so on. For this, we can make custom SEXPs that call scripting code.
  
As a result, this section is pretty tough, but covers almost all of the specific knowledge you'll need to work within Lua.
+
To define the SEXP, first we need an entry in a <tt>*-sexp.tbm</tt>. See [[Dynamic_SEXPs|this page]] for details on how the table is formatted. For our very simple example, we will use this as the SEXP table:
 +
$Operator: example-script-sexp
 +
$Category: Change
 +
$Subcategory: Scripted
 +
$Minimum Arguments: 1
 +
$Maximum Arguments: 1
 +
$Description: Prints out the string passed.
 +
$Parameter:
 +
+Description: The string that is printed
 +
+Type: string
 +
This table basically means that we now have a new SEXP called <tt>example-script-sexp</tt> which takes a single string as an argument.
 +
To actually run scripting code, we now need to register a function with the SEXP. This is done by assigning <tt>mn.LuaSEXPs[&lt;SEXP name&gt;]</tt> a function that takes the same parameters as specified in the SEXP table. For this example, this would be:
 +
mn.LuaSEXPs["example-script-sexp"] = function(message)
 +
--message is a string
 +
ba.warning(message)
 +
end
  
=== Function vocabulary ===
+
Now, if we call the SEXP from an event in FRED, we should see a warning pop up, with the message that we specified in the event.
* '''''Return value''''' - Data that a function returns. Any data that a function returns may be stored in a variable.
 
* '''''Arguments''''' - Data that is given to a function. May be data itself, a variable containing the data, or even the return value of another function.
 
* '''''Call''''' - To run a function.
 
  
 +
==Examples and Common Situations / Issues==
 +
FSO scripting has a few peculiarities that you should know about if you want to script certain things. This chapter aims to highlight most of them. But first, let us build one complete example.
 +
===A full example===
 +
The goal for this small example is to make a weapon that saps afterburner energy from a target it hits. For this, I assume that you know how to make a custom weapon in the weapons table, which I assume we call "ABDrain" for the purposes of this.
  
=== Typical function use ===
+
Since this is something that should happen on a trigger, and not from an event, it’s more suited for a conditional hook than for a custom SEXP. Looking at scripting.html, a suited action would seem to be <tt>On Weapon Collision</tt>, since that triggers every time that a weapon collides with something. But we don’t want our script to be draining the afterburner every time ''any'' weapon collides with something, but only when our new weapon does so. So let us add the <tt>Weapon class: ABDrain</tt> condition. In addition, since only ships have afterburners, we need to specify the condition <tt>Object type: Ship</tt>.
<b>Example:</b> This will write "Alpha 1" in the upper-left corner of the screen.
+
All in all, these conditions now result in this hook running every time our weapon collides with (aka hits) a ship. Now we need to write the code to be executed. For this, we first need to consider what our weapon should actually do. Let's define it as "drains a third of the ship’s maximum afterburner fuel".
player = Mission.Ships['Alpha 1']
+
The steps to then actually perform this action are thus: Find out what a third of the ships maximum afterburner actually is, subtract that from the current afterburner level, but don’t decrease the afterburner below 0 (since after all, we cannot have negative afterburner)
Graphics.setColor(255, 255, 255)
+
Checking scripting.html, we find that the conditional hook will provide the hook variable <tt>Ship</tt>. We also find that its type, <tt>ship</tt>, has a variable called <tt>AfterburnerFuelMax</tt>. That means a third of the ship’s afterburner fuel can be calculated with the following:
Graphics.drawString(player.Name, 5, 5)
+
local fuel_third = hv.Ship.AfterburnerFuelMax / 3.0
 
+
The next step is to subtract it from the current fuel that the ship has. In scripting.h, we see that next to <tt>AfterburnerFuelMax</tt> there is the variable <tt>AfterburnerFuelLeft</tt>:
* <b>Line 1:</b> <br>In the first line, the "Ships" array of the mission library is indexed. Because 'Alpha 1' is specified, it will set either the handle to the ship named 'Alpha 1', or it will return an invalid handle if Alpha 1 is dead or there was no Alpha 1 in the mission to begin with. Because the player variable is set to the result of the Array index, it will receive whatever handle is accessed by the array. <br><br><b>Result</b><br>After the first line, the player variable is a ship handle to the ship named "Alpha 1" in the current mission.
+
local fuel_new = hv.Ship.AfterburnerFuelLeft - fuel_third
 
+
Finally, we want to set <tt>AfterburnerFuelLeft</tt> to the result of that, but not lower than 0:
 
+
hv.Ship.AfterburnerFuelLeft = math.max(fuel_new, 0)
* <b>Line 2</b><br>In the second line, the "setColor" function is used to set the current drawing color to pure white. Unlike getShipByName, it takes at least three arguments, all numbers - the red, green, and blue scale of the current color, from 0 to 255. It's always a good idea to use the setColor function before doing any drawing in scripting, as the engine can and will change the current color between scripting hooks.<br><br><b>Result</b> <br>Current drawing color is set to pure white
+
Putting it all together and in the correct syntax for a scripting table then gives:
 
+
$Weapon class: ABDrain
 
+
$Object type: Ship
* <b>Line 3</b><br>In the third line, the drawString function takes three arguments - a string, followed by two numbers. The string specifies the text to write, the numbers specify the x and y position of the upper-left corner of the written text. As you can see, we've directly used a '''''member variable''''' of the player variable to fill the first argument. Because we got the ship handle by its name, the Name variable will be the same as the name that we got it by ("Alpha 1").<br><br><b>Result</b><br>After the third line, "Alpha 1" will appear in the upper-left corner of the string in white text.
+
  $On Weapon Collision: [
 
+
  local fuel_third = hv.Ship.AfterburnerFuelMax / 3.0
<b>Note:</b> Numbers and certain types of userdata will automatically convert to strings. In all other cases, functions will give an error if you try to use the wrong type of argument.
+
  local fuel_new = hv.Ship.AfterburnerFuelLeft - fuel_third
 
+
  hv.Ship.AfterburnerFuelLeft = math.max(fuel_new, 0)
<b>Debugging note:</b> If Alpha 1 were dead in the above example, you would get an error about 'trying to index a nil value' because it would not be able to run the getName() function.
+
  ]
 
+
While this script would work, it’s not very pretty or well optimized. Let’s clean it up a bit. First, we index into hv.Ship three separate times. That means it’s probably worth saving as a local variable. Then, we don’t actually need to save fuel_third and fuel_new in variables. Since we use them only once, there is no harm in just copying the code to where it’s used. That gives us the following script:
 
+
  $Weapon class: ABDrain
=== Userdata functions ===
+
  $Object type: Ship
Handles and objects may also have functions.
+
  $On Weapon Collision: [
 
+
  local ship = hv.Ship
<b>Example:</b> This will write the number of subsystem's on Alpha 1's ship in the upper-left corner.
+
  ship.AfterburnerFuelLeft = math.max(ship.AfterburnerFuelLeft - ship.AfterburnerFuelMax / 3.0, 0)
  player = Mission.Ships['Alpha 1']
 
Graphics.drawString(#player, 5, 5)
 
 
 
As before, we get the handle to the Alpha 1 ship with the Ships array.
 
 
 
 
 
=== Optional arguments ===
 
 
 
Sometimes you don't have to use an argument. For example, the setColor function has an optional fourth argument - how opaque the color should be. If the argument isn't used, the color is totally opaque. These two lines do the same thing:
 
 
 
Graphics.setColor(255, 255, 255)
 
  Graphics.setColor(255, 255, 255, 255)
 
 
 
 
 
== Quick, a full example! ==
 
By this point, you may be bored of all this exposition. So here's your typical "Hello, World!" example.
 
 
 
For this amazing trick, we'll make our very own splash screen. Turn off all Freespace 2 mods, and create a file called "scripting.tbl" in your data/tables directory. Open that up in a text editor, and write:
 
  #Conditional Hooks
 
  $Version: 3.7
 
  $On Splash Screen: [
 
  --Set the color to white
 
Graphics.setColor(255, 255, 255)
 
--Write "Hello, world!"
 
Graphics.drawString("Hello, world!", 5, 10)
 
 
  ]
 
  ]
+Override: YES
 
#End
 
  
Now, when you start up FS2_Open, you should see "Hello, world!" in the upper-left, as it loads.
+
===Common Situations with Special Handling Required===
 +
This section details situations where certain FSO-specific tricks are required or very helpful to complete a certain task.
 +
====+Override: Make FSO ignore things====
 +
As you may have noticed, the hooks in scripting.html specify whether they are ''overridable'' or not. In this context, overridable means that we can tell FSO to pretend that whatever caused the action to activate didn’t actually occur. In the example with the afterburner weapon, an override would cause the weapon to ''not actually collide'' with the ship it hit, just passing through it. To override a hook, after your normal script block, add <tt>+Override:</tt>, and then add another block of code. This block of code should return a boolean. If it returns true, the hook will be overridden. If it returns false, FSO proceeds as normal. Here is an example from Blighted:
 +
$On Ship Collision: [ HUD_OnShipCollision() ]
 +
+Override: [ return ENEMIES_OnCollisionOverride() ]
 +
Note that these are two different blocks of code: <code>HUD_OnShipCollision()</code> is the normal behavior for this hook, but <code>return ENEMIES_OnCollisionOverride()</code> determines whether the hook actually runs.
  
== Control structures ==
+
Incidentally, '''+Override:''' does not require the normal script block to have any code in it. The following will randomly disable ship collisions 50% of the time:
A control structure is a series of statements that let you change how a script runs, based on some condition - say, a ship is dead, or the life of a certain ship has fallen below a certain amount.
+
$On Ship Collision: []
 +
+Override: [ return ba.rand32f() < 0.5 ]
  
=== If statements ===
+
====Repeating SEXP Arguments====
If statements are a way to check if something is some way or not. All if statements have the form:
+
The SEXP table can contain the <tt>$Repeat</tt> tag. It implies that all arguments following the tag can be repeated in FRED up to the argument maximum. If that is the case, special care needs to be taken on the Lua side to accommodate the repeating arguments.
  if (something) then
+
The function supplied to the custom SEXP needs to take the non-repeating arguments as usual, and then use Lua’s <tt>ipairs</tt> function to iterate through a list of tuples containing the repeating arguments. For an example, take this function which has the non-repeating arguments arg1 and arg2, and then two repeating arguments rep1 and rep2:
  (execute)
+
  function(arg1, arg2, …)
 +
  --arg1 and arg2 can already be used as usual
 +
for number, repeating in ipairs(arg) do
 +
local rep1 = repeating[1]
 +
local rep2 = repeating[2]
 +
end
 
  end
 
  end
 
+
The three dots (...) in the parameter list indicate that the function has a variable number of arguments. When this function is called, all its arguments are collected in a single table, which the function accesses as a hidden parameter named arg. Besides those arguments, the arg table has an extra field, n, with the actual number of arguments collected.
(something) can be true, or false. If it's true, then (execute) will be run. If it's false, then the script will skip down to "end" and go on from there.
+
====Version-Dependent Scripting====
 
+
When you script within a -sct.tbm, and not in a .lua file, the script is processed as a table before it is processed as Lua. This gives the scripter the ability to use FSO’s version-dependent comments in Lua. This can be used to enable certain behaviour of the script only when a sufficient FSO version is used, while allowing older versions of FSO to also load the script without the behaviour. An example of this is a version dependent function which chooses whether to use a newly-introduced random function of FSO’s provided libraries, or whether to emulate a slower, less well randomized version using Lua’s math functions if that is unavailable:
To check that a handle is valid:
+
  ;;FSO 20.1.0.20200727;; rand32 = ba.rand32
  ship = Mission.Ships['Alpha 2']
+
;;FSO 20.1.0.20200727;; !*
  if ship:isValid() then
+
  rand32 = function(a, b)
--This will always happen while Alpha 2 is alive
+
if a and b then
  Graphics.drawString("MACKIE LIVES!", 5, 10)
+
return math.random(a, b)
 +
elseif a then
 +
return math.random(a) - 1
 +
  else
 +
return math.random(0, 0x7fffffff)
 +
end
 
  end
 
  end
 
+
;;FSO 20.1.0.20200727;; *!
To check a variable value
+
See [[Version-specific_commenting]] for details.
x = 42
+
====Doing something for all Ships====
y = 23
+
Performing some sort of operation for all ships is a reasonably common occurrence. It might be tempting to just build a for loop and access <tt>mn.Ships[i]</tt>, but this is ''very'' slow for technical reasons. A much faster alternative is to use the ship iterator, and build a for each loop:
if x < y or x is y then
+
for ship in mn.getShipList() do
  --We never get here, because x is bigger than y
+
  --use ship as desired
 
  end
 
  end
 +
The same kind of iterator is also available for all missiles, and all parse objects (yet-to-spawn ships)
  
=== For statements ===
+
====Making things take Time / Waiting for Stuff====
A for statement is generally used to iterate through a list.
+
A few situations require code along the lines of "keep this state for two seconds". Or, "wait four seconds and then do something". Naively, we’d be adding a global variable, saving the time that the wait started, then repeatedly checking if we’ve passed our wait time since then. This is very tedious to write and easy to mess up. FSO provides asynchronous coroutines to make this a very easy process.
 +
First, start up an asynchronous function using <tt>async.run</tt>, then wait in that function using <tt>async.await</tt> on <tt>mn.awaitAsync</tt>, and do whatever you had to wait for.
 +
An example:
 +
async.run(function()
 +
--This code will run immediately
 +
async.await(mn.awaitAsync(4))
 +
--This code will run 4 seconds later
 +
end, async.OnFrameExecutor) --Use OnFrameExecutor for things that need to draw, OnSimulationExecutor for things that just calculate physics
 +
--This code will run immediately
 +
Similarly, it’s reasonably easy to wait for certain things to happen. For this, you can use a loop to wait for a condition, and then yield to the game in your loop. If you don’t yield in the loop, the game cannot process any new changes, and your code will be stuck in the loop. See the example:
 +
async.run(function()
 +
--This code will run immediately
 +
while mn.Ships["Alpha 1"].HitpointsLeft > 10 do
 +
async.await(async.yield())
 +
end
 +
--This code will run once Alpha 1 has less or equal to 10 hitpoints left
 +
end, async.OnFrameExecutor)
 +
It is also possible to do other things in addition to yielding within loops in which you wait for certain things, such as drawing to the screen. This can be used to make certain visual effects that are available only for a certain time or until certain conditions are fulfilled.  
  
<b>Example:</b>
+
====Listening to Input====
This bit of scripting will run through all of Alpha 2's subsystems if she isn't dead, and display a list of them in the upper-left.
+
Listening to control input is closer to making custom SEXPs than to writing scripting hooks. While the <tt>On Action</tt> hook action exists, it is limited for certain types of controls. For listening to player-rebindable controls, the <tt>io</tt> library should be used.
 +
Call <tt>io.Keybinding[&lt;Keybind name&gt;].Bind:registerHook(...)</tt> with a function which will be called when the action is pressed. Certain types of controls have special things that need to be considered. If you registered the function for an axis control, it will be called every time the axis is changed. In addition, the <tt>Value</tt> hook variable will be set to the value of the axis. If it is a continuous button (such as holding down primary fire), the function will be called up to once per frame, with the hook variable <tt>Pressed</tt> set to whether or not the button currently is pressed down.
 +
An example of how that can be used:
 +
pressed = false
 +
io.Keybinding["CUSTOM_CONTROL_1"].Bind:registerHook(function()
 +
if hv.Pressed ~= pressed then
 +
pressed = hv.Pressed
 +
if hv.Pressed then
 +
--Button is pressed
 +
else
 +
--Button is released
 +
end
 +
end
 +
end, true)
  
ship = Mission.Ships['Alpha 2']
+
====Parsing Config Files====
if ship then
+
Oftentimes, scripts that are made to be generic and not developed for one mod in particular want configurable settings without the need to change the scripts code. For this, loading configs is desirable. The current accepted standard to do so is to use the [https://github.com/FSO-Scripters/fso-scripts/tree/master/AxBase functions built by Axem]. Merge the data folder of AxBase with your data folder, and you are good to go.
num_subsys = #ship
+
Then, just call axemParse’s ReadJSON method to load JSON-formatted .cfg files from the config folder without issue. An example how to use it to load a file called config.cfg into a variable:
x = 5
+
if cf.fileExists("config.cfg", "data/config", true) then
y = 5
+
config = axemParse:ReadJSON("config.cfg")
for i=1,num_subsys do
+
else
--Use the i variable to go through the ship's subsystems,
+
ba.error("Config file missing! Cannot proceed!\n")
--and to change the y position of the text.
+
  return
gr.drawString(ship[i].Name, x, y + i*10)
 
  end
 
 
  end
 
  end
  
=== More information ===
+
====Scripted Ship AI====
For other keywords you can use with control structures, see the [http://www.lua.org/manual/5.0/manual.html#2.5.2 Lua Documentation]
+
It is also possible to script ship AI using Lua. It is very similar to how SEXPs are scripted, except that it needs two functions supplied instead of just one. One that is executed once the AI mode is started, and one that is called each frame the AI mode is active. In comparison to a normal SEXP, these AI functions are also passed an additional parameter in the form of an <tt>ai_helper</tt> object, which can be used to perform several actions with the ship the AI mode is active on. Additionally, both functions need to return a boolean. If they return true, the AI mode is completed and the ship will proceed with the next highest order, if false the ship will continue with this AI mode. A minimal example for the scripting side of LuaAI is the following:
 
+
mn.LuaAISEXPs["lua-ai-sexp"].ActionFrame = function(ai_helper)
For more detailed information on if statements and other types of control structures, see the [http://lua-users.org/wiki/ControlStructureTutorial Lua-users wiki]
+
ai_helper.ForwardThrust = 1
 
+
return false
 
+
end
== Appendices ==
+
  mn.LuaAISEXPs["lua-ai-sexp"].ActionEnter = function(ai_helper)
 
+
return false
There's more depth to scripting than what's covered above. To make the rest of the article shorter, I've put the topics still within the scope of this article, but not necessary to know in most cases, down here.
 
 
 
However, I HIGHLY recommend you read the optimizing scripting section.
 
 
 
=== Appendix: Optimizing scripting ===
 
 
 
Because fs2_open is written in C, which is much faster than Lua, it is much better to store variables in C and provide Lua functions to work with them. In addition, to keep Lua scripting from messing with C scripting and to make it as friendly as possible, there are a number of safeguards to catch problems, in most of the Lua functions.
 
 
 
As a result, whenever you're working with a value provided by a library or object function, you should set it to a Lua variable if you're going to use it more than 1-2 times due to the large overhead resulting from the interface.
 
 
 
Thus, even though it may look like "Name" is a member variable of the ship handle, it is actually a function that performs 3-4 validity checks before returning the value, which must also be converted from C to Lua. Setting a variable to the return value of the function means that you can use that variable wherever you want to use the ship name using the native Lua environment.
 
 
 
=== Appendix: Debugging scripting ===
 
 
 
Unfortunately, the Lua interpreter provides only limited debugging information about hooks. (Supposedly, it will provide more information about scripting within user functions, but I haven't tested that claim yet)
 
 
 
Generally, error messages complaining about referencing or indexing a 'nil value' occur because a function could not return a handle, so it returned nil. Some functions also return false to indicate that the handle is correct, but due to some kind of wrong information being passed, it couldn't complete.
 
 
 
Generally, you should check all handles before using them:
 
  player = Mission.Ships['Alpha 1']
 
if player:isValid() then
 
 
 
  end
 
  end
  
With all uses of the player handle falling in the "if" statement.
+
For the SEXP-Table side, see [[Dynamic_SEXPs#.23Lua_AI|here]].
  
=== Appendix: Function types ===
+
===Common Pitfalls===
 
+
In this section we will take a very brief look at several common errors people make while scripting.
Here's all the different kinds of functions you can have:
+
====Removing Elements during Iteration====
--A function provided by itself
+
When you are using a for loop to iterate through a table, you must not remove elements from it. This will invalidate the iterators and cause unexpected behaviour. Instead, you must keep a list of elements to remove and then remove the elements from the table after you have completed iterating the table.
x = cos(y)
+
====Object Validity====
--A function provided by a library
+
Most types of values provided by FSO's API have a function akin to <tt>isValid</tt> or similar. Especially for objects that are kept around for multiple frames, and then especially ships or weapons, it is prudent to check whether isValid is still true before operating on the values. It can happen quickly that a value becomes invalid (such as a ship dying and getting cleaned up), either causing an error or returning an invalid or unexpected value when using it.
x = Math.cos(y)
+
====Function Calling Syntax====
--A function provided by a userdata variable.
+
Lua has two ways of calling functions. Using a colon before the function name, and using a dot before the function name. For things provided by FSO’s API, ''all'' functions that are available through a library must be called with a dot, while all functions available through an object must be called using a colon. For custom functions, the call syntax should match the syntax used when declaring the function. If you keep to the recommendations made in the Lua explanation chapter and the style guide of this document, custom functions should almost all be called using a colon.
x = y:cos()
+
====Calling functions with Arguments: When will it copy?====
In every case, x would be set to the cosine of y, which is 1.
+
If you call a function in Lua that has arguments, some of these arguments will be copied to the function, while others will not be. To demonstrate the difference, see these two code snippets:
 
+
  function add1(value)
<b>Note:</b> You must use a ":" with userdata functions, as in the example. This tells Lua that it needs to give the function the value of the variable that it's being called from.
+
  value = value + 1
 
 
=== Appendix: Making your own functions ===
 
So, let's make the doSomething() function do, well, something.
 
  doSomething = function()
 
  color = "red"
 
 
  end
 
  end
As you can see, creating a function is much like creating a variable; you just use function() instead of the value, then write what you want the function to do. To end the function, you just write "end"
+
 
+
  local value = 1
Now, let's use our new doSomething() function.
+
  add1(value)
  color = "black"
+
  --value will be 1
  doSomething()
+
and
  --After using doSomething(), color is now red
+
  function add1(value)
 
+
  value[1] = value[1] + 1
Functions can also '''''return''''' values.
 
--Make a new function
 
  doSomethingElse = function()
 
  return 10
 
 
  end
 
  end
x = 0
 
doSomething()
 
--x is now 27
 
x = doSomethingElse()
 
--x is now 10
 
 
=== Appendix: Variable Scope ===
 
The '''''scope''''' of a variable is the area where it exists. Writing <tt>local</tt> in front of the variable name will tell Lua that the variable should only exist within that function or hook.
 
 
Note that this means that if you use the same variable name in two different hooks without using local, the same variable will be used.
 
 
<b>Note:</b>
 
 
You may find it useful to have some way of distinguishing global variables from local ones, to remind yourself that the value does carry over. For example, starting global variable names with "g_".
 
 
<b>Example:</b> The following will draw the text "Media VP" in the upper-left corner of the main hall, even though the variable is set in a different hook than it is used in.
 
 
#Conditional Hooks
 
$Version: 3.7
 
$On Game Init: [g_mod = "Media VP"]
 
 
   
 
   
  $State: GS_STATE_MAIN_MENU
+
  local value = 1
  $On Frame: [Graphics.drawString(g_mod, 0, 0)]
+
  add1( {value} )
  #End
+
  --value will be 2
 
+
In the first code snippet, <tt>value</tt> was copied when it was passed to the function, so the function added 1 to the copy and not the original value. The second code snippet however, passed a list containing <tt>value</tt> to the function. Lists (and all other tables) are passed by reference, so Lua did not make a copy and the function modified the original value.
=== Appendix: Variable Types ===
+
In Lua, only numbers, booleans and strings (and some other technicalities unimportant to us) are copied when calling a function. Tables, functions, and all of FSO's custom data types are passed by reference and will ''not'' be copied.
Variables can hold different types of data. These basic types are:
+
The exception to this is usually values taken from FSO's custom data types. To illustrate, see the following example:
 
+
  local ship = mn.Ships["Alpha 1"]
==== Boolean ====
+
A boolean is a simple variable - it can be true, or false.
+
  --This will change the hitpoints of Alpha 1
--It's Monday, and Dilbert is at the office
+
  ship.HitpointsLeft = 1
  working = true
+
  --Oops, he just opened Solitaire
+
  --Accessing a value of a FSO type will create a copy
  working = false
+
  local position = ship.Position
 
+
  --This will create a reference to position
==== Numbers ====
+
  local position2 = position
A number is any variable that holds (As you might've guessed) a number. A number will automatically convert into a string, if needed.
+
  --A variable can be a whole number, like this
+
  --This will NOT change the ships position, but it will change position2
  x = 5
+
  position[1] = 1
  --Or it can be a decimal value
 
  x = 3.141592654
 
  --Or you can divide two numbers for a fraction
 
  x = 3/4
 
  
==== Strings ====
+
In general, it is most safe to assume that FSO's data types are processed as references, but to push them back to FSO, they need to be assigned back to the API.
A string is a variable that holds a series of characters, and a character is any number, letter, or symbol on your keyboard (and then some). Basically, strings are the way that words and sentences are passed around in Lua.
 
--A cow says moo!
 
cow = "moo"
 
--A friend's name
 
friend = "Bob"
 
Note that actual string content is contained within double quotation marks, to mark it as a string.
 
  
==== nil ====
+
==Style Guide==
nil isn't really a variable type, but it is important. nil means that a variable doesn't hold anything. If you want to get rid of a variable, you just set it to be nil.
+
This is a short section on how we prefer to style our scripts for FSO. Some of these are performance relevant, while others are more for a better level of organization.
 
+
===Avoid runSEXP===
--Today I got a new dog
+
If you can, avoid evaluateSEXP, evaluateNumericSEXP and runSEXP. These are comparably slow, as they require both text processing and the assembling of a SEXP node tree. They can also be more error prone than just doing everything in a script. Using them occasionally is not necessarily a bad thing (and many existing scripts use them without a problem), but if you find you are using them frequently or in high-performance situations, consider adding a feature request on the SCP's GitHub page to add the missing functionality to the scripting API.
dog = "Spot"
 
--But then he ran away
 
dog = nil
 
 
 
Any time an error says that a variable is nil, it usually means that it doesn't exist (Double-check your spelling).
 
 
 
==== Userdata/objects ====
 
Objects (or "userdata", as they are known in Lua) is a variable that can contain other variables, or functions. Variables or functions within an object are known as "member variables" or "member functions".
 
 
 
--Assuming we have a my_car object, this will copy it to your_car
 
your_car = my_car
 
--Congratulations! But maybe you want to customize it?
 
--Note how I refer to the member variable, color
 
your_car.color = "Red"
 
 
 
==== Handles ====
 
A handle isn't technically a Lua type, but handles are used a lot in FS2_Open Lua scripting. Essentially, a handle is a variable that refers to a specific object; but doesn't actually contain that object's data. An example is the "shipclass" type; it doesn't actually contain the data in ships.tbl, it just serves to reference the ship class so that you can get and set data.
 
--Get the "GTF Ulysses" ship type
 
--For this, I use the ShipClasses array
 
ulysses = tb.ShipClasses['GTF Ulysses']
 
--Now get rid of the handle
 
ulysses = nil
 
--Even though we've gotten rid of the handle, the Ulysses will still exist.
 
 
 
In situations where a handle variable is an object, you must set the entire variable at once; you cannot change elements of the array individually.
 
--So intead of this...
 
ship = mn.Ships[1]
 
ship.Position["y"] = 0
 
 
--You would go...
 
ship = mn.Ships[1]
 
pos = ship.Position["y"]
 
pos["y"] = 0
 
ship.Position = pos
 
  
=== Appendix: External Lua references ===
+
===Make everything a local===
* [http://www.devmaster.net/articles/lua/lua1.php DevMaster.net - Lua Scripting 101]
+
If you have a function or a piece of code that regularly accesses the same variable, consider making a <tt>local</tt> copy of it. For things such as access to FSO’s API, this can be a lot faster since calls to the API are generally accompanied by safety checks or even expensive calculations. But even when accessing a subtable of a Lua object or a global variable, it can often be faster to copy it to a local and then use that local
* [http://www.lua.org/manual/5.1/ Official Lua 5.1 Reference Manual]
+
===Setup functions in On Game Init===
* [http://www.lua.org/pil/ Programming in Lua]
+
This one is less performance relevant, but it has been somewhat established that scripts try to keep most of their code in function definitions that are run in On Game Init, which are then just called in each hook’s code, instead of having all the code in the hook itself.
* [http://www.lua-users.org/ lua-users.org]
+
===Keep your Script’s Code in a Lua Table===
 +
Try to avoid saving a lot of variables or functions in the global scope. Since the global namespace is shared among all scripts, it is easy to accidentally overwrite another script’s variable or function otherwise. It is recommended that all scripts have exactly one overarching Lua table, and then declare functions and variables for the script as objects of this table.

Latest revision as of 03:13, 11 March 2024

This is an updated tutorial intended to give an introduction and overview into using Lua for scripting in FSO. It is split into three sections. Scripting with FSO, examples for common pitfalls or special situations, and best practices (both for Lua in general and in conjunction with FSO). It is written as of January 2023, for FSO 23.0.0, and some scripting functions or names used may have changed since then. If you spot something that does not work or exist any more, please let us know!

Requirements and Resources

One of the most important resources for scripting in FSO is the scripting documentation provided by FSO. To access it, check the FSO launch flag "Output scripting to scripting.html" under "Dev Tool" in Knossos (or pass -output_scripting with the launcher of your choice) and launch the mod. Note that the mod has to actually launch. If FSO throws an error on startup, it'll not generate scripting.html. This will generate a file called scripting.html. It will probably be located in the same folder where your retail FS2 root.vp is located. If you open the file in your browser, you will see a long list of various different things. It'll be confusing now, but don't worry, we'll get to it in time. Furthermore, there exists a public repository of available scripts for FSO. It may be worth checking what is already available, or how certain problems have been solved by other people if you are stuck. Also, if you have completed and tested a script, you are welcome to add it to the scripting repository.

Lua Basics

For a more professional, long term look at Lua I recommend having a look at Programming in Lua, but it certainly isn’t a required read to do scripting. For a slightly shorter text, see the following:

Lua Basics

So, in order to be able to script with Lua, the first question obviously has to be "how does one even Lua?". At its very basic steps, programming, in most languages, is a combination of calculating things, and making decisions from it. And Lua is no different from that. So to start off, let’s learn to calculate a simple addition and to save that. Now, the calculation part is pretty straightforward.

1 + 1

That is enough to tell Lua to calculate 1 + 1. But we don’t save it, so it’s not very useful to us.

calculation = 1 + 1

But that is also not too hard. This has now created something called a variable. If we later say something like calculation + 1 it will yield 3, because calculation is now 2. This variable we have now called "calculation", but any name consisting of letters and underscores are allowed. Other special characters, especially spaces, must not be part of a variable name. Something you should remember is that in Lua, variables can be local and global. What we did above is make a global variable. That means that it can be accessed from everywhere, including other scripts. But we could have also written the following

local calculation = 1 + 1

That would have made calculation a local variable. The difference is, that a local variable is only available where you created it. It cannot be accessed by another script. And if you created the variable for example in a loop, or in an if (we will learn what these are shortly), then you cannot access the variable after this loop or if as well. As a rule of thumb, try to make something a local variable if possible. Now, variables are called variable for a reason: You can also change them after you have created them:

calculation = calculation + 1

This increases the calculation variable by one. Unpacking what this line of code says one by one, we can also describe it as follows: Set the value of calculation to the what the value of calculation was plus one. Variables can store more than numbers, however. Everything you can do stuff with in Lua can be made into a variable. Some examples below:

local number = 1
local number_with_decimal_point = 1.5
local string = "Can be any text"
local boolean = true --true or false, used for logical operations

But what do we do now with these variables? One of the most important things in programming is having our program make decisions. In Lua, one way of doing this is an if statement. The way it is programmed is almost like you would phrase a conditional sentence in English: If some condition is true, then do something, else do something different. Let’s say we want to add 1 to calculation if it is smaller than 2, and remove 1 otherwise. In Lua, it looks like this:

if calculation < 2 then
	calculation = calculation + 1
else
	calculation = calculation - 1
end

Basically the same, except that after the thing to do when the condition is not fulfilled must be an end. The condition can be anything really, from comparing if two things are the same (using ==, with two equals signs), or if something is smaller as something else like in the example. You can even combine multiple conditions using and or or if it is needed.

Similarly to if, there are loops to repeat things. If you write while instead of if, Lua will repeat the content of the while loop as long as the condition is true.

a = 5
b = 5
result = 0
while a > 0 do
	a = a - 1
	result = result + b
end

This is a very simple multiplication program that uses a while loop and basic arithmetics to calculate the product of a and b and saves it in result.

Often, you will end up in a situation where you want a variable to be global. But if you have many variables, at some point it becomes hard to keep track what is what. One tool to help you organize your variables are tables. These can serve as a container to keep multiple variables together. The following example shows how you can create a table and store a variable inside it:

table1 = {
	variable1 = 1
}
table2 = {
	variable1 = 2,
	variable2 = 3
}
table1.variable2 = 4
--Both tables now have entries called variable1 and variable2, both with different values

--Access for reading works the same as for writing:
table1.variable1 = table1.variable2 + table2.variable1

But that is not the only think tables can do. They can also help you to keep a list of values, even if you don't know how many values you will have. We can make a table that is basically a list, and then do something for all elements in the list.

list = {}
--Now the list is still empty
table.insert(list, 5)
table.insert(list, 2)

--Now we have two elements in our list, 5 and 2
for i, value in ipairs(list) do
	--This piece of code will be called once for each element in the list, no matter how many elements are in the list.
	--i is the number of the element in the list, and value is the value of the element
	--In this example, this piece of code will be called with i = 1 and value = 5 as well as with i = 2 and value = 2
end

There is one more helpful use of tables. You can also use them to store values for a certain name or index. Consider the use case that we want to store the balance of multiple people. For this, we could use the following:

accounts = {}
--Now the list is still empty
accounts["John"] = 5
accounts["Frank"] = 20
--Now the list has two elements, one stored for the name John and one for Frank.

--We can also access them again:
local johnsMoney = accounts["John"]

--using a loop for all elements works too, but a bit differently:
for name, value in pairs(list) do
	--Name will be the name, and value the value stored for this name
end

Finally, we need to have a look at one last tool: Functions. In many cases, when you write Lua code, there will be parts of your code that you want to reuse somewhere else. But instead of copying the code, we can make it a function. Let us consider the multiplication example from before. Instead of copying that to everywhere we would need it, we can convert it to a function.

function multiply(a, b)
	result = 0
	while a > 0 do
		a = a - 1
		result = result + b
	end
	return result
end

As you can see, the function consist of three main parts. First, the function declaration. After function, you write the name you want to use for the function, followed by its parameters in parenthesis. This list of parameters can be as long as you want, including having no parameters. The names you use for the parameters are then available to use in your functions like a variable would be. The second part is the actual code of the function. This code can contain the word return. Return means that the function will be quit, and if there is a value or a variable named after return, it will be returned to the function caller as the reuslt. Finally, the function is terminated with an end To use the function, simply call its name and fill in any arguments you may have added:

local multiplicationResult = multiply(2, 4)
--multiplicationResult will be 8

For organization, functions can also be part of tables. For this, just put a colon between the table name and the function name:

table = {}
function table:functionInTable()
	--Do something
end

One special thing when using functions in tables is that the function has access to other values in the table by using the name self. It can be used like any other variable, and will be the table that contains the function. See the following example:

table = {
	test = 5
}
function table:functionInTable()
	return self.test --This will return 5
end

Interactions with FSO

Knowing Lua we can now consider how we can use that to make FSO do things.

Running Code / Conditional Hooks

Now, we know how to write very basic code. But how do we actually run it? For this, FSO uses conditional hooks, specified in the scripting table (*-sct.tbm). At the basic level, conditional hooks are made up of three parts, action, condition and code.

  • First, the action: An action is effectively an event in FSO at which we can run our custom code. The very basic example is On Game Init. This action is run once when FSO starts up, and can be used by us to run scripts that need to happen during this start up. A list of all available actions and when they trigger can be found in the scripting.html, reasonably far up, under the section Conditional Hooks and then Actions.
  • Second, one or more conditions: The condition is a tool to have more fine-grain control over when exactly hooks are run. Imagine you want to script a special afterburner effect using the On Afterburner Engage action. But since this new effect should only apply to one ship class, we don't want our script to run for other ship classes. In this case, we could use the Ship class condition to limit the hook to a specific ship class. Similarly, since most scripts should run just in FSO and not in FRED, setting the Application condition to FS2_Open is a good idea in most cases as well. A list of conditions that can be used for all hooks is listed in scripting.html under Conditional Hooks and then Conditions, with further explanation here. Some conditions are only available for some specific actions, and may mean slightly different things for different actions. These are documented for each action individually in scripting.html.
  • Third, scripting code that is run. This can be specified in two different styles. Either, just write a block of Lua code in between two brackets [ like this ], or specify a file in the scripts folder that contains the code in double brackets [[sample.lua]].

When specifying these conditional hooks in the scripting table, you first specify one or more conditions. Then you can specify one or more pairs of an action, and the code to be run. Until you specify the next set of conditions, all actions will have the last condition.

Now we can use this to run some code:

$Application: FS2_Open
$On Game Init: [
	--This is now Lua code that is run.
	local variable = 1 + 1
]

One thing to note is that all hooks share the same global space. This effectively means that all Lua functions we define and all variables we write are shared among all hooks. This is why it is a very common pattern to define the bulk of any script's code in a function in the On Game Init action, and then just call these functions from the specific conditional hooks for a cleaner script style.

Making FSO do Stuff / Library Calls and Variables

In order to actually have any useful effect through our scripts, we need to be able to both tell FSO to do certain things, as well as be able to check on the state of certain gameplay elements.

In order to know what is possible, we need to reference scripting.html. Below the section listing the hooks is a listing of libraries. These libraries are (usually) the first point of interfacing with FSO. You can click the library names to jump to the point in scripting.html where they are documented. Searching through these functions and variables will likely take a significant amount of time during scripting, so it’s best to get familiar with how these functions are documented.

For a very basic script, we will just want to output some message to the screen to make sure we were able to correctly run code. Since that is a very basic operation, it’s located in the base library. We’ll use the warning function, so that we can see the message when using a debug build. The documentation tells us that the function takes a message as a string, and will return nothing.

$Application: FS2_Open
$On Game Init: [
	ba.warning("The script successfully ran!")
]

When running this in a debug build, we can now see our message at startup!

Some things are exposed to Lua not as functions, but as variables. See for example XAxisInverted in the controls (io) library. You can access io.XAxisInverted like any other boolean with Lua. Reading this boolean will give you the current state, and writing to it will change it. Some of these values, notably those that are loaded from tables, cannot be written to and changed. In that case the documentation will be missing the = <typename> after the variable name.

Many of the values you will access from FSO will not be standard Lua types, but special FSO types, like for example a ship. For example, mn.Ships[1] will return the first ship in a mission, with the ship type. To learn what the ship type is, you can click it in scripting.html to see the documentation. We can see that it inherits object (which means that you can use any ship the same way you can also use an object), and that it has a number of properties and functions, just as the libraries had. One thing that is important, is that if you call functions on FSO types, you must use : before the function name, while you need to use . for functions directly within a library. Let's say that ship were an object of the type ship. We can use the following code to clear its orders and call it a new name:

ship:clearOrders()
ship.Name = "Renamed Ship"

Custom SEXPs

In a lot of cases, we don’t actually want to define our code in hooks. Often we want much more fine grain control when things are run, depending on events in FRED and so on. For this, we can make custom SEXPs that call scripting code.

To define the SEXP, first we need an entry in a *-sexp.tbm. See this page for details on how the table is formatted. For our very simple example, we will use this as the SEXP table:

$Operator: example-script-sexp
$Category: Change
$Subcategory: Scripted
$Minimum Arguments: 1
$Maximum Arguments: 1
$Description: Prints out the string passed.
$Parameter:
	+Description: The string that is printed
	+Type: string

This table basically means that we now have a new SEXP called example-script-sexp which takes a single string as an argument. To actually run scripting code, we now need to register a function with the SEXP. This is done by assigning mn.LuaSEXPs[<SEXP name>] a function that takes the same parameters as specified in the SEXP table. For this example, this would be:

mn.LuaSEXPs["example-script-sexp"] = function(message)
	--message is a string
	ba.warning(message)
end

Now, if we call the SEXP from an event in FRED, we should see a warning pop up, with the message that we specified in the event.

Examples and Common Situations / Issues

FSO scripting has a few peculiarities that you should know about if you want to script certain things. This chapter aims to highlight most of them. But first, let us build one complete example.

A full example

The goal for this small example is to make a weapon that saps afterburner energy from a target it hits. For this, I assume that you know how to make a custom weapon in the weapons table, which I assume we call "ABDrain" for the purposes of this.

Since this is something that should happen on a trigger, and not from an event, it’s more suited for a conditional hook than for a custom SEXP. Looking at scripting.html, a suited action would seem to be On Weapon Collision, since that triggers every time that a weapon collides with something. But we don’t want our script to be draining the afterburner every time any weapon collides with something, but only when our new weapon does so. So let us add the Weapon class: ABDrain condition. In addition, since only ships have afterburners, we need to specify the condition Object type: Ship. All in all, these conditions now result in this hook running every time our weapon collides with (aka hits) a ship. Now we need to write the code to be executed. For this, we first need to consider what our weapon should actually do. Let's define it as "drains a third of the ship’s maximum afterburner fuel". The steps to then actually perform this action are thus: Find out what a third of the ships maximum afterburner actually is, subtract that from the current afterburner level, but don’t decrease the afterburner below 0 (since after all, we cannot have negative afterburner) Checking scripting.html, we find that the conditional hook will provide the hook variable Ship. We also find that its type, ship, has a variable called AfterburnerFuelMax. That means a third of the ship’s afterburner fuel can be calculated with the following:

local fuel_third = hv.Ship.AfterburnerFuelMax / 3.0

The next step is to subtract it from the current fuel that the ship has. In scripting.h, we see that next to AfterburnerFuelMax there is the variable AfterburnerFuelLeft:

local fuel_new = hv.Ship.AfterburnerFuelLeft - fuel_third

Finally, we want to set AfterburnerFuelLeft to the result of that, but not lower than 0:

hv.Ship.AfterburnerFuelLeft = math.max(fuel_new, 0)

Putting it all together and in the correct syntax for a scripting table then gives:

$Weapon class: ABDrain
$Object type: Ship
$On Weapon Collision: [
 	local fuel_third = hv.Ship.AfterburnerFuelMax / 3.0
 	local fuel_new = hv.Ship.AfterburnerFuelLeft - fuel_third
 	hv.Ship.AfterburnerFuelLeft = math.max(fuel_new, 0)
]

While this script would work, it’s not very pretty or well optimized. Let’s clean it up a bit. First, we index into hv.Ship three separate times. That means it’s probably worth saving as a local variable. Then, we don’t actually need to save fuel_third and fuel_new in variables. Since we use them only once, there is no harm in just copying the code to where it’s used. That gives us the following script:

$Weapon class: ABDrain
$Object type: Ship
$On Weapon Collision: [
	local ship = hv.Ship
 	ship.AfterburnerFuelLeft = math.max(ship.AfterburnerFuelLeft - ship.AfterburnerFuelMax / 3.0, 0)
]

Common Situations with Special Handling Required

This section details situations where certain FSO-specific tricks are required or very helpful to complete a certain task.

+Override: Make FSO ignore things

As you may have noticed, the hooks in scripting.html specify whether they are overridable or not. In this context, overridable means that we can tell FSO to pretend that whatever caused the action to activate didn’t actually occur. In the example with the afterburner weapon, an override would cause the weapon to not actually collide with the ship it hit, just passing through it. To override a hook, after your normal script block, add +Override:, and then add another block of code. This block of code should return a boolean. If it returns true, the hook will be overridden. If it returns false, FSO proceeds as normal. Here is an example from Blighted:

$On Ship Collision: [ HUD_OnShipCollision() ]
+Override: [ return ENEMIES_OnCollisionOverride() ]

Note that these are two different blocks of code: HUD_OnShipCollision() is the normal behavior for this hook, but return ENEMIES_OnCollisionOverride() determines whether the hook actually runs.

Incidentally, +Override: does not require the normal script block to have any code in it. The following will randomly disable ship collisions 50% of the time:

$On Ship Collision: []
+Override: [ return ba.rand32f() < 0.5 ]

Repeating SEXP Arguments

The SEXP table can contain the $Repeat tag. It implies that all arguments following the tag can be repeated in FRED up to the argument maximum. If that is the case, special care needs to be taken on the Lua side to accommodate the repeating arguments. The function supplied to the custom SEXP needs to take the non-repeating arguments as usual, and then use Lua’s ipairs function to iterate through a list of tuples containing the repeating arguments. For an example, take this function which has the non-repeating arguments arg1 and arg2, and then two repeating arguments rep1 and rep2:

function(arg1, arg2, …)
	--arg1 and arg2 can already be used as usual
	for number, repeating in ipairs(arg) do
		local rep1 = repeating[1]
		local rep2 = repeating[2]
	end
end

The three dots (...) in the parameter list indicate that the function has a variable number of arguments. When this function is called, all its arguments are collected in a single table, which the function accesses as a hidden parameter named arg. Besides those arguments, the arg table has an extra field, n, with the actual number of arguments collected.

Version-Dependent Scripting

When you script within a -sct.tbm, and not in a .lua file, the script is processed as a table before it is processed as Lua. This gives the scripter the ability to use FSO’s version-dependent comments in Lua. This can be used to enable certain behaviour of the script only when a sufficient FSO version is used, while allowing older versions of FSO to also load the script without the behaviour. An example of this is a version dependent function which chooses whether to use a newly-introduced random function of FSO’s provided libraries, or whether to emulate a slower, less well randomized version using Lua’s math functions if that is unavailable:

;;FSO 20.1.0.20200727;; rand32 = ba.rand32
;;FSO 20.1.0.20200727;; !*
rand32 = function(a, b)
	if a and b then
		return math.random(a, b)
	elseif a then
		return math.random(a) - 1
	else
		return math.random(0, 0x7fffffff)
	end
end
;;FSO 20.1.0.20200727;; *!

See Version-specific_commenting for details.

Doing something for all Ships

Performing some sort of operation for all ships is a reasonably common occurrence. It might be tempting to just build a for loop and access mn.Ships[i], but this is very slow for technical reasons. A much faster alternative is to use the ship iterator, and build a for each loop:

for ship in mn.getShipList() do
	--use ship as desired
end

The same kind of iterator is also available for all missiles, and all parse objects (yet-to-spawn ships)

Making things take Time / Waiting for Stuff

A few situations require code along the lines of "keep this state for two seconds". Or, "wait four seconds and then do something". Naively, we’d be adding a global variable, saving the time that the wait started, then repeatedly checking if we’ve passed our wait time since then. This is very tedious to write and easy to mess up. FSO provides asynchronous coroutines to make this a very easy process. First, start up an asynchronous function using async.run, then wait in that function using async.await on mn.awaitAsync, and do whatever you had to wait for. An example:

async.run(function()
	--This code will run immediately
	async.await(mn.awaitAsync(4))
	--This code will run 4 seconds later
end, async.OnFrameExecutor) --Use OnFrameExecutor for things that need to draw, OnSimulationExecutor for things that just calculate physics
--This code will run immediately

Similarly, it’s reasonably easy to wait for certain things to happen. For this, you can use a loop to wait for a condition, and then yield to the game in your loop. If you don’t yield in the loop, the game cannot process any new changes, and your code will be stuck in the loop. See the example:

async.run(function()
	--This code will run immediately
	while mn.Ships["Alpha 1"].HitpointsLeft > 10 do
		async.await(async.yield())
	end
	--This code will run once Alpha 1 has less or equal to 10 hitpoints left
end, async.OnFrameExecutor)

It is also possible to do other things in addition to yielding within loops in which you wait for certain things, such as drawing to the screen. This can be used to make certain visual effects that are available only for a certain time or until certain conditions are fulfilled.

Listening to Input

Listening to control input is closer to making custom SEXPs than to writing scripting hooks. While the On Action hook action exists, it is limited for certain types of controls. For listening to player-rebindable controls, the io library should be used. Call io.Keybinding[<Keybind name>].Bind:registerHook(...) with a function which will be called when the action is pressed. Certain types of controls have special things that need to be considered. If you registered the function for an axis control, it will be called every time the axis is changed. In addition, the Value hook variable will be set to the value of the axis. If it is a continuous button (such as holding down primary fire), the function will be called up to once per frame, with the hook variable Pressed set to whether or not the button currently is pressed down. An example of how that can be used:

pressed = false
io.Keybinding["CUSTOM_CONTROL_1"].Bind:registerHook(function() 
	if hv.Pressed ~= pressed then
		pressed = hv.Pressed
		if hv.Pressed then
			--Button is pressed
		else
			--Button is released
		end
	end
end, true)

Parsing Config Files

Oftentimes, scripts that are made to be generic and not developed for one mod in particular want configurable settings without the need to change the scripts code. For this, loading configs is desirable. The current accepted standard to do so is to use the functions built by Axem. Merge the data folder of AxBase with your data folder, and you are good to go. Then, just call axemParse’s ReadJSON method to load JSON-formatted .cfg files from the config folder without issue. An example how to use it to load a file called config.cfg into a variable:

if cf.fileExists("config.cfg", "data/config", true) then
	config = axemParse:ReadJSON("config.cfg")
else
	ba.error("Config file missing! Cannot proceed!\n")
	return
end

Scripted Ship AI

It is also possible to script ship AI using Lua. It is very similar to how SEXPs are scripted, except that it needs two functions supplied instead of just one. One that is executed once the AI mode is started, and one that is called each frame the AI mode is active. In comparison to a normal SEXP, these AI functions are also passed an additional parameter in the form of an ai_helper object, which can be used to perform several actions with the ship the AI mode is active on. Additionally, both functions need to return a boolean. If they return true, the AI mode is completed and the ship will proceed with the next highest order, if false the ship will continue with this AI mode. A minimal example for the scripting side of LuaAI is the following:

mn.LuaAISEXPs["lua-ai-sexp"].ActionFrame = function(ai_helper)
	ai_helper.ForwardThrust = 1
	return false
end
mn.LuaAISEXPs["lua-ai-sexp"].ActionEnter = function(ai_helper)
	return false
end

For the SEXP-Table side, see here.

Common Pitfalls

In this section we will take a very brief look at several common errors people make while scripting.

Removing Elements during Iteration

When you are using a for loop to iterate through a table, you must not remove elements from it. This will invalidate the iterators and cause unexpected behaviour. Instead, you must keep a list of elements to remove and then remove the elements from the table after you have completed iterating the table.

Object Validity

Most types of values provided by FSO's API have a function akin to isValid or similar. Especially for objects that are kept around for multiple frames, and then especially ships or weapons, it is prudent to check whether isValid is still true before operating on the values. It can happen quickly that a value becomes invalid (such as a ship dying and getting cleaned up), either causing an error or returning an invalid or unexpected value when using it.

Function Calling Syntax

Lua has two ways of calling functions. Using a colon before the function name, and using a dot before the function name. For things provided by FSO’s API, all functions that are available through a library must be called with a dot, while all functions available through an object must be called using a colon. For custom functions, the call syntax should match the syntax used when declaring the function. If you keep to the recommendations made in the Lua explanation chapter and the style guide of this document, custom functions should almost all be called using a colon.

Calling functions with Arguments: When will it copy?

If you call a function in Lua that has arguments, some of these arguments will be copied to the function, while others will not be. To demonstrate the difference, see these two code snippets:

function add1(value)
	value = value + 1
end

local value = 1
add1(value)
--value will be 1

and

function add1(value)
	value[1] = value[1] + 1
end

local value = 1
add1( {value} )
--value will be 2

In the first code snippet, value was copied when it was passed to the function, so the function added 1 to the copy and not the original value. The second code snippet however, passed a list containing value to the function. Lists (and all other tables) are passed by reference, so Lua did not make a copy and the function modified the original value. In Lua, only numbers, booleans and strings (and some other technicalities unimportant to us) are copied when calling a function. Tables, functions, and all of FSO's custom data types are passed by reference and will not be copied. The exception to this is usually values taken from FSO's custom data types. To illustrate, see the following example:

local ship = mn.Ships["Alpha 1"]

--This will change the hitpoints of Alpha 1
ship.HitpointsLeft = 1

--Accessing a value of a FSO type will create a copy
local position = ship.Position
--This will create a reference to position
local position2 = position

--This will NOT change the ships position, but it will change position2
position[1] = 1

In general, it is most safe to assume that FSO's data types are processed as references, but to push them back to FSO, they need to be assigned back to the API.

Style Guide

This is a short section on how we prefer to style our scripts for FSO. Some of these are performance relevant, while others are more for a better level of organization.

Avoid runSEXP

If you can, avoid evaluateSEXP, evaluateNumericSEXP and runSEXP. These are comparably slow, as they require both text processing and the assembling of a SEXP node tree. They can also be more error prone than just doing everything in a script. Using them occasionally is not necessarily a bad thing (and many existing scripts use them without a problem), but if you find you are using them frequently or in high-performance situations, consider adding a feature request on the SCP's GitHub page to add the missing functionality to the scripting API.

Make everything a local

If you have a function or a piece of code that regularly accesses the same variable, consider making a local copy of it. For things such as access to FSO’s API, this can be a lot faster since calls to the API are generally accompanied by safety checks or even expensive calculations. But even when accessing a subtable of a Lua object or a global variable, it can often be faster to copy it to a local and then use that local

Setup functions in On Game Init

This one is less performance relevant, but it has been somewhat established that scripts try to keep most of their code in function definitions that are run in On Game Init, which are then just called in each hook’s code, instead of having all the code in the hook itself.

Keep your Script’s Code in a Lua Table

Try to avoid saving a lot of variables or functions in the global scope. Since the global namespace is shared among all scripts, it is easy to accidentally overwrite another script’s variable or function otherwise. It is recommended that all scripts have exactly one overarching Lua table, and then declare functions and variables for the script as objects of this table.