FS2 Open Lua Scripting

From FreeSpace Wiki
Revision as of 18:09, 24 September 2008 by TopAce (talk | contribs) (If it's scripting, it must be SCP-related. Haven't read the article)
Jump to: navigation, search

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.

You don't need to read the Appendices, but I recommend at least reading the "Optimizing" section if you're doing much scripting.

IMPORTANT: This page was last updated on 5/27/2007 and assumes you are using the most up-to-date build at that time, 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.

What You Need

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.

Here we go!

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

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

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)

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.

Return hook

$Hook: 2 + #Mission.Ships

This works like a normal table variable, except that you can use scripting functions as well.

Normal hook

$Hook: [
	gr.setColor(255, 255, 255)
	gr.drawString("Hello, world!")

	return 2 + #Mission.Ships
]

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.

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.

File hook

$Hook: [[script.lua]]

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
life = 42

--String variable
name = "Bob"

--Boolean variables
smoking = true
healthy = false

--Userdata (Handle or object) variable
player = Mission.Ships['Alpha 1']

--Nil variable
--Erases a previously set variable
life = nil

--Member variable of userdata. Can be any of the above types.
player.Name = "Ship"

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

Arrays

An array is a special type of variable, that holds multiple variables. The index of all Lua arrays starts at 1.

--Set the fifth element of array to 1
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

--Create an array
a = {2, 4, 6, 8}
--Set s to the size of the array
s = #a
--s will be 4

Libraries

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.

Graphics.setColor(255, 255, 255)
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.

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)


Functions

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.

As a result, this section is pretty tough, but covers almost all of the specific knowledge you'll need to work within Lua.

Function vocabulary

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


Typical function use

Example: This will write "Alpha 1" in the upper-left corner of the screen.

player = Mission.Ships['Alpha 1']
Graphics.setColor(255, 255, 255)
Graphics.drawString(player.Name, 5, 5)
  • Line 1:
    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.

    Result
    After the first line, the player variable is a ship handle to the ship named "Alpha 1" in the current mission.


  • Line 2
    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.

    Result
    Current drawing color is set to pure white


  • Line 3
    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").

    Result
    After the third line, "Alpha 1" will appear in the upper-left corner of the string in white text.

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

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


Userdata functions

Handles and objects may also have functions.

Example: This will write the number of subsystem's on Alpha 1's ship in the upper-left corner.

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.

Control structures

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.

If statements

If statements are a way to check if something is some way or not. All if statements have the form:

if (something) then
	(execute)
end

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

To check that a handle is valid:

ship = Mission.Ships['Alpha 2']
if ship:isValid() then
	--This will always happen while Alpha 2 is alive
	Graphics.drawString("MACKIE LIVES!", 5, 10)
end

To check a variable value

x = 42
y = 23
if x < y or x is y then
	--We never get here, because x is bigger than y
end

For statements

A for statement is generally used to iterate through a list.

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

ship = Mission.Ships['Alpha 2']
if ship then
	num_subsys = #ship
	x = 5
	y = 5
	for i=1,num_subsys do
		--Use the i variable to go through the ship's subsystems,
		--and to change the y position of the text.
		gr.drawString(ship[i].Name, x, y + i*10)
	end
end

More information

For other keywords you can use with control structures, see the Lua Documentation

For more detailed information on if statements and other types of control structures, see the Lua-users wiki


Appendices

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

With all uses of the player handle falling in the "if" statement.

Appendix: Function types

Here's all the different kinds of functions you can have:

--A function provided by itself
x = cos(y)
--A function provided by a library
x = Math.cos(y)
--A function provided by a userdata variable.
x = y:cos()

In every case, x would be set to the cosine of y, which is 1.

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

Appendix: Making your own functions

So, let's make the doSomething() function do, well, something.

doSomething = function()
	color = "red"
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.

color = "black"
doSomething()
--After using doSomething(), color is now red

Functions can also return values.

--Make a new function
doSomethingElse = function()
	return 10
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 local 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.

Note:

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_".

Example: 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
$On Frame: [Graphics.drawString(g_mod, 0, 0)]
#End

Appendix: Variable Types

Variables can hold different types of data. These basic types are:

Boolean

A boolean is a simple variable - it can be true, or false.

--It's Monday, and Dilbert is at the office
working = true
--Oops, he just opened Solitaire
working = false

Numbers

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

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

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