Difference between revisions of "FS2 Open Lua Scripting"

From FreeSpace Wiki
Jump to: navigation, search
 
(+Override: Make FSO ignore things: clarify syntax)
 
(14 intermediate revisions by 7 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!
  
== What You Need ==
+
==Requirements and Resources==
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.
+
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
  
Here we go!
+
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.
  
== Comments ==
+
Similarly to if, there are loops to repeat things.
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)
+
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>.
  
For example, this:
+
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.
  --This is a comment
+
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:
  mn.loadMission("sm2-10.fs2") --Load a mission
+
table1 = {
  --Greetings!
+
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
  
Will do exactly the same thing as this:
+
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.
  mn.loadMission("sm2-10.fs2")
+
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
  
== Variables ==
+
There is one more helpful use of tables. You can also use them to store values for a certain name or index.
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 is a variable, as it may be seen in the wild:
+
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
  
  x = 42
+
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.
--x is 42
+
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.
  y = x
+
  function multiply(a, b)
  --y, copycat that it is, is now also equal to 42
+
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 <tt>function</tt>, 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 <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
  
=== Types ===
+
For organization, functions can also be part of tables. For this, just put a colon between the table name and the function name:
However, variables can hold different types of data. These basic types are:
+
table = {}
 +
function table:functionInTable()
 +
--Do something
 +
end
  
==== Boolean ====
+
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:
A boolean is a simple variable - it can be true, or false.
+
table = {
--It's Monday, and Dilbert is at the office
+
test = 5
  working = true
+
}
  --Oops, he just opened Solitaire
+
function table:functionInTable()
  working = false
+
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.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>.
 +
 
 +
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
 +
  ]
  
==== Numbers ====
+
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 number is any variable that holds (As you might've guessed) a number. A number will automatically convert into a string, if needed.
+
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.
--A variable can be a whole number, like this
 
x = 5
 
--Or it can be a decimal value
 
x = 3.141592654
 
--Or you can divide two numbers for a fraction
 
x = 3/4
 
  
==== Strings ====
+
===Making FSO do Stuff / Library Calls and Variables===
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.
+
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.
--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 ====
+
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.
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.
 
  
  --Today I got a new dog
+
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.
  dog = "Spot"
+
  $Application: FS2_Open
  --But then he ran away
+
  $On Game Init: [
dog = nil
+
ba.warning("The script successfully ran!")
 +
  ]
 +
When running this in a debug build, we can now see our message at startup!
  
Any time an error says that a variable is nil, it usually means that it doesn't exist (Double-check your spelling).
+
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.
  
==== Userdata/objects ====
+
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.
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".
+
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"
  
--Assuming we have a my_car object, this will copy it to your_car
+
===Custom SEXPs===
your_car = my_car
+
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.
--Congratulations! But maybe you want to customize it?
 
--Note how I refer to the member variable, color
 
your_car.color = "Red"
 
  
==== Handles ====
+
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:
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.
+
  $Operator: example-script-sexp
  --Get the "GTF Ulysses" ship type
+
$Category: Change
  --For this, I use the function 'getShipclassByName'
+
$Subcategory: Scripted
  ulysses = tb.getShipclassByName("GTF Ulysses")
+
$Minimum Arguments: 1
--Now get rid of the handle
+
$Maximum Arguments: 1
ulysses = nil
+
$Description: Prints out the string passed.
  --Even though we've gotten rid of the handle, the Ulysses will still exist.
+
  $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
  
=== Scope ===
+
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.
The '''''scope''''' of a variable is the area where it exists. By default, all variables exist everywhere; however, if you define your own function, writing 'local' in front of the variable name will tell Lua that the variable should only exist within that function.
 
  
Note that this means that if you make a variable called 'x' in one hook, and a variable called 'x' in another hook, they will actually be the same value.
+
==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.
  
== Functions ==
+
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>.
A function is an instruction to Lua to do something. It may change variables, or it may call (activate) other functions.
+
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 <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:
 +
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>:
 +
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)
 +
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)
 +
]
  
For example:
+
===Common Situations with Special Handling Required===
doSomething()
+
This section details situations where certain FSO-specific tricks are required or very helpful to complete a certain task.
This single line of code would, presumeably tell Lua to do 'something'. What that something is, depends on what the function is supposed to do. Note the two parentheses after the function name; these tell Lua to run the function. (There are reasons to use function names without the parentheses, but that is one of those 'advanced topics')
+
====+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.
  
Here's all the different kinds of functions you can have:
+
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 function provided by itself
+
  $On Ship Collision: []
  x = cos(y)
+
  +Override: [ return ba.rand32f() < 0.5 ]
--A function provided by a library
 
x = ma.cos(y)
 
--A function provided by an object
 
x = y:cos()
 
In every case, x would be set to the cosine of y, which is 1.
 
  
Functions may also have multiple arguments:
+
====Repeating SEXP Arguments====
  gr.drawString("Hello!", 5, 10)
+
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.
This tells Freespace 2 to draw the string "Hello!" on-screen, starting at the point 5 pixels down from the top of the screen, and 10 pixels from the left of the screen.
+
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:
 +
  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 <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:
 +
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)
  
=== Finally, a full example! ===
+
====Making things take Time / Waiting for Stuff====
By this point, you may be bored of all this exposition. So let's actually use variables and functions to do something in Freespace!
+
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.
  
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:
+
====Listening to Input====
  #Global Hooks
+
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.
  $Splash: [
+
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.
--Set the color to white
+
An example of how that can be used:
gr.setColor(255, 255, 255)
+
  pressed = false
--Write "Hello, world!"
+
  io.Keybinding["CUSTOM_CONTROL_1"].Bind:registerHook(function()
  gr.drawString("Hello, world!", 5, 10)
+
if hv.Pressed ~= pressed then
]
+
pressed = hv.Pressed
#End
+
if hv.Pressed then
 +
--Button is pressed
 +
else
 +
--Button is released
 +
end
 +
  end
 +
end, true)
  
Now, when you start up FS2_Open, you should see "Hello, world!" in the upper-left, as it loads.
+
====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 [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.
 +
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
  
=== Making your own functions (Optional) ===
+
====Scripted Ship AI====
So, let's make the doSomething() function do, well, something.
+
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:
  doSomething = function()
+
  mn.LuaAISEXPs["lua-ai-sexp"].ActionFrame = function(ai_helper)
  color = "red"
+
  ai_helper.ForwardThrust = 1
 +
return false
 +
end
 +
mn.LuaAISEXPs["lua-ai-sexp"].ActionEnter = function(ai_helper)
 +
return false
 
  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"
 
  
Now, let's use our new doSomething() function.
+
For the SEXP-Table side, see [[Dynamic_SEXPs#.23Lua_AI|here]].
color = "black"
 
doSomething()
 
--After using doSomething(), color is now red
 
  
Functions can also '''''return''''' values.
+
===Common Pitfalls===
  --Make a new function
+
In this section we will take a very brief look at several common errors people make while scripting.
  doSomethingElse = function()
+
====Removing Elements during Iteration====
  return 10
+
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 <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.
 +
====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
 
  end
  x = 0
+
   
  doSomething()
+
local value = 1
  --x is now 27
+
  add1( {value} )
  x = doSomethingElse()
+
  --value will be 2
  --x is now 10
+
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.
 +
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
  
== Libraries ==
+
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 library provides various functions, which may in turn provide various objects or handles with their own functions. All Freespace 2 libraries are named using two letters. "Graphics" is "gr", "Mouse" is "ms", and so on.
 
  
You can see how functions in a graphics are referenced in the "Hello, world!" example above.
+
==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.
  
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 and functions supported by that build.
+
===Make everything a local===
 +
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
 +
===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.

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.