Scripting API

From FreeSpace Wiki
Jump to: navigation, search

The FS2_Open scripting API is intended to be as cross-platform and as language non-specific as possible. However, it is generally assumed that the most popular language will be Lua.

New System (HEAD)

Introduction

Foreword: Who I Think You Are

For the duration of this document, I will generally assume that you, the reader, are a coder who is interested in Doing Something With Scripting. This means that I will cover the simple stuff first, and mostly focus on how to define and use hooks and variables. However, I will try to make this document also have some value to scripters interested in how things work "under the hood".

What the heck is ADE?

"Ade" is the name of the new scripting backend. I chose this name after I did a complete overhaul of the scripting backend, somewhere around the time that the official fs2_open codebase was split into the "HEAD" (unstable) and "3.6.9" (stable) branches. HEAD uses ADE, 3.6.9 does not.

Ade itself is based on the idea that the most effective way to generate scripting support is via an object-oriented method that mirrors the way that Lua does things. It uses a master array of objects with the type "ade_table_entry", which in turn denote their respective children and parent objects.

Ade itself also defines a number of structs and macros that are used to easily extend functions and variables into Lua.

A little on Lua - Why?

Lua is a crossplatform scripting language that has a syntax similar to Python and C. When adding a scripting component to FS2_Open was discussed, Lua was chosen for the following reasons:

  • Crossplatform support
  • Whitespace-independent syntax
  • Lightweight parser and compact default libraries
  • Support for semi-object-oriented syntax

Rather than define things using traditional constructs such as structs and classes, Lua uses a system of "tables" and "metatables". This is, quite frankly, a pain in the arse to understand. However, I will do my best to give a crash course here.

Tables

Essentially, a Lua table is just a big array. The items stored in the array (or table) can be accessed using either numbers or strings. They can also be accessed using traditional brackets [], or via traditional class syntax (using a period).

myTable[1] = "Macaroni"
myTable["Food"] = "Steak"
print(myTable[1])   --outputs "Macaroni"
print(myTable["Food"])   --outputs "Steak"
print(myTable.Food)   --also outputs "Steak"

Userdata

Userdata is a lua type that consists of user-specified data. All ADE libraries, handles, and objects are defined using userdata and a metatable.

Metatables

The interesting thing about lua tables is that they can also be used as metatables. A metatable is a table that is associated with an object, which provides a series of functions that control how the object or table will be accessed. This may include operators (+ - * / etc) and what happens when you try to index userdata or change the value of an index of userdata. All metatable functions start with "__", and are called when their respective operator is used or respective event occurs.

Diagram showing ADE's use of metatables
Lua Documentation: Metatables

Part One: Conditional Hooks

Introduction

Unlike the old scripting system, the new cool thing in Ade is support for conditional hooks. These mean a little more work for coders just starting out, but they help to improve the efficiency and flexibility of Lua.

Table sytax

See Scripting.tbl#Format_3

Defining conditions

  1. Open up parse/scripting.cpp and parse/scripting.h in your favorite text editor. Find the "CHC_*" defines in parse/scripting.h
  2. Add your define to the end of the list; make sure to use the last #define's number + 1.
  3. Add your condition to the Script_conditions[] array in parse/scripting.cpp. The string will be used for the variable name in the table (The "$" is added automatically).
  4. Scroll down to the ConditionedHook::ConditionsValid() function. In that function:
    1. Add a case for you #define. Put it in the same relative position as you put it in the Script_conditions[] array. (This is not necessary, but it keeps things organized)
    2. The value that the modder specifies after your condition is provided by scp->data.name. (For example, if the modder says "$Version: 3.6.9", then scp->data.name will contain "3.6.9"). If your condition has something to do with a related object, then use the "objp" variable.
    3. Use the value provided by scp->data.name to check your condition
    4. Return false if the condition is not met. Do not return true. This is unnecessary, and will cause any conditions after yours to be ignored. But don't forget to "break".

That's it. _All_ actions will now be able to use your condition to check whether they ought to execute or not.

Defining actions

  1. Open up parse/scripting.cpp and parse/scripting.h in your favorite text editor. Find the "CHA_*" defines in parse/scripting.h
  2. Add your define to the end of the list; make sure to use the last #define's number + 1.
  3. Add your condition to the Script_actions[] array in parse/scripting.cpp. The string will be used for the action name in the table (The "$" is added automatically).

Now you're done...with parse/scripting.*, anyway.

  1. Go to the place in the code you want the action to be triggered.
  2. Call IsConditionOverride(action #define) to determine if the modder wants to override the retail FreeSpace 2 action. By convention, conditional hooks also override any other types of hooks, and IsConditionOverride should come before everything else.
    1. If this action involves an object (Like rendering a ship), pass the pointer to the object using the second argument to IsConditionOverride(). This will be used to check any conditions specified.
  3. Call Script_system.RunCondition(action #define) to actually run the method. By convention, this should be called after the retail FreeSpace 2 action or other types of hooks, so the modder can change anything he doesn't like.
    1. If you want the modder to be able to return a value, define a type using the format character (2nd argument) and a destination buffer with the data variable (3rd argument). If you want to pass an object, but don't want to involve a return value, set these fields to '\0' and NULL, respectively.
    2. If this action involves an object (Like rendering a ship), don't forget to pass the pointer to the objp argument (4th argument).
    3. A typical call might look like this:
  4. Use Script_system.SetHookObject() or Script_system.SetHookVar() to define hook variables (Hook variables are contained within the "hv" library).
  5. Don't forget to use Script_system.RemHookVars() when you're done.

Congratulations! Now anyone will be able to use your hook.

Example:

Script_system.SetHookObject(2, "Self", warping_object, "SomeOtherObject", some_other_object);
if(!Script_System.IsConditionOverride(CHC_WARPIN, warping_object))
{
   //Handle the warpin normally
}
Script_system.RunCondition(CHC_WARPIN, '\0', NULL, warping_object)
Script_system.RemHookVar(2, "Self", "SomeOtherObject");

In the table, you would use this hook like so:

#Conditional Hooks
$Shipclass: SJ Sathanas
$On Warp In: [
   gr.setColor(255, 0, 0)
   gr.drawString("Danger! Supernova imminent! Sathanas detected at " .. tostring(Self.Position), 200, 200)
]
#End

Old System (3.6.9)

Part One: Basic API and Scripting Hooks

Things To Know

Script_system
This object abstracts a scripting session. It is generally used to allow for easy construction/destruction of whatever is necessary to initialize scripting system objects, and make sure that the right language session data is passed. Its constructor takes one argument, the name of the scripting session (for easy debugging)
Script_system.CreateLuaSession(libraries)
Initializes a Lua session. Until this is called, you will not be able to use Lua at all and fs2_open should ignore code chunks that use it.
Script_system.ParseChunk(debugname)
Parses a 'code chunk'. These are snippets of code encapsulated by symbols such as square brackets or parentheses. It stores a handle to the parsed script in memory (Probably bytecode).
script_system.RunBytecode(handle):Actually executes a code chunk that has been parsed with ParseChunk

Example

This snippet of code (using FS2_Open functions) will parse a file that has scripting after each "$Global:" variable identifier. After all the scripting has been parsed, it will evaluate every hook, once.

//Create script system object
script_system Script_system;

//Create array that holds a series of scripting hooks
std::vector<script_hook> Script_globalhooks;

//Initialize Lua
Script_system.CreateLuaState(Lua_libraries);

//Parse file
read_file_text("scripting.tbl");
reset_parse();
while(optional_string("$Global:"))
{
	Script_globalhooks.push_back(st->ParseChunk());
}

//Execute hooks
for(uint i = 0; i < Script_globalhooks.size(); i++)
{
	Script_system.RunBytecode(Script_globalhooks[i]);
}

Part Two: The Higher-Level Lua Library

The Lua library is probably what you'll want to be the most familiar with, as it's where you'll edit to add functions, classes, libraries, and variables.


Limitations

  • Lbraries can only contain functions, and not variables or arrays.
  • All "arrays" must be an object with an indexer (see LUA_INDEXER define).
    I was not able to get this working originally, but I've got another idea on how to try this so if you really feel an array class would be best in a given situation, tell me.


General conventions

For the internal Lua API:

  • All internal lua functions are prefixed with lua_
  • All Lua library and object variables are prefixed with l_, so "l_Graphics".
  • When failure occurs due to a bad type being passed, or a subsystem not being ready, LUA_RETURN_NIL is usually used.
  • When failure lies with the content of data, rather than the circumstances or type, LUA_RETURN_FALSE is generally used where appropriate.

For Scripting:

  • All variable names have the form of "EverySomethingIsCapitalized"
  • All function names have the form of "doSomethingFast()"
  • All object names have the form of "somethingneat"
  • All library names are one capitalized word. For more than one word, the same syntax as variables(?)
  • All library shortnames are two noncapitalized letters relating to the name. "Sound" -> "sd"
  • So far, I've used three-letter noncapitalized variables for globals. But I may decide to switch to the normal variable convention soon, as it's MUCH clearer about what the variable is.


Things To Know

lua_lib
Defines a Lua library.
lua_obj
Defines a Lua object type (userdata).
LUA_FUNC
Defines a Lua member function. Used with both lua_lib and lua_obj.
LUA_VAR
Defines a Lua member variable. Only used with lua_obj.
LUA_INDEXER
Defines Lua activity when brackets are used. Only used with lua_obj.

Once you invoke any of the above, it's not necessary to do anything else for it to be present in Lua.

lua_lib

lua_lib l_Base("Base", "ba", "Base FreeSpace 2 functions");

Defines a Lua library, which can contain member functions (But not any kind of variables).

Probably the simplest and most obvious to work with. The first field is the library name; the second is the shorthand version. The last is merely the description, which is only used for -ouput_scripting


lua_obj

lua_obj<object_h> l_Ship("ship", "Ship object", &l_Object);

Defines a lua object, via a C++ template. The <> defines what type of C data the object will hold. In general, it's better to pass a handle to FreeSpace data (such as the ship_info struct) than to try and copy the entire thing. For 'static' data - such as image handles or species indices - a simple <int> with the index of the item will suffice. For perishable data, you may want to store a unique identifier - say, an object's signature. In the above example, I use a helper struct that holds both a pointer to the object, and the object's signature. That way, all I need to do is check that the object at the pointer has the same signature to make sure that the handle is still valid.

The next two parameters are fairly obvious - the name of the object (becomes the metatable name), and the description in output_scripting.

The last parameter warrants some explanation - it is the parent of the object. Multiple levels of derived objects aren't supported, and a derived object must have the same type as its parent object, since all of the parent object's functions will generally only support the parent object's data type, and it's just needlessly complicating.


LUA_FUNC

LUA_FUNC(newVector, l_Math, "[x, y, z]", "Vector object", "Creates a vector object")

One of the two most used of the Big 5. The same tactics used for a LUA_FUNC are virtually the same for LUA_VAR and LUA_INDEXER.

There are two components to LUA_FUNC. The first is the arguments (above); the second is the actual body.

The arguments are, the function name (As it will appear in Lua). This is also used internally for the C compiler, so it is not actually a string. The second argument is the library or object that the function is a part of. So, the above example is the "newVector" function of the "Math" library, and would be accessed by calling "Math.newVector()"

The third, fourth, and fifth arguments are all purely descriptive, but you should always fill them in if applicable to ensure good documentation. The first is the arguments that the function will take; they may be in any form you please, but I have generally used the "required, required, [optional, optional]" form. As you can see, in the example all arguments are optional. The second argument is the return value. If the function returns different types, you should note it here. The last argument is the basic description. Because it can be pretty long, I've sometimes split it up across multiple lines.

Now, for the body of the define:

{
	vec3d v3 = vmd_zero_vector;
	lua_get_args(L, "|fff", &v3.xyz.x, &v3.xyz.y, &v3.xyz.z);
	return lua_set_args(L, "o", l_Vector.Set(v3));
}

Defining function variables

In the first line, we create the C vector object that we're going to return, and initialize it to 0 (since all arguments are optional).

Parsing arguments passed to your function in Lua

In the second line, lua_get_args is used to parse the arguments from Lua. This function is big and messy and takes care of all the warnings and errors that may be caused by invalid types and such. The first argument, L, is just the lua state data that must be passed along; ignore it but don't forget it. The second argument is a list of characters that specify the variable types. These characters represent the following C types:

  • b - Boolean
  • d - Double
  • f - Float
  • i - Int
  • s - String
  • x - Fix
  • o - lua_obj (See later)

Finally,

  • | - Denotes all variables after this as being optional.

All arguments after the formatting string should be pointers to the variables you want lua_get_args to set. Be careful; compilers will not catch if you use the a boolean where you have specified a double, for example. Optional variables will only be changed if a value is specified. Note: If a wrong type is passed to an optional variable of your function, that variable will be skipped, although variables after it may be changed. Note: You do pass pointers to (char *), so you end up passing a char **. Lua will take care of all memory concerns; don't try to deallocate the pointer yourself.

When using the 'o' parameter, you can get objects in one of two ways:

vec3d v3;
vec3d *v3p;
lua_get_args(L, "oo", l_Vector.GetPtr(&v3p), l_Vector.Get(&v3));

"v3p" would be a pointer to the first object's data, while "v3" would merely hold a copy of the data in the second object. Because it's not necessary to allocate or copy as much memory, getting pointers may be faster; but at the same time, you should be careful not to change the data in the pointer unless you really want to, as it will change the actual object's data. When you define a member function, the object will be passed as the first argument. As a result, this lua_get_args call would be suitable for, say, a function that copies the data passed in the argument to the calling object - "vectora:copyData(vectorb)".

lua_get_args will return the number of arguments parsed, or 0 if the required variables requirement was not met. So when working with required variables (or an object), you should generally use the form:

if(!lua_get_args(...))
	return LUA_RETURN_NIL;

The exception being when you want to actually know the number of arguments, in which case you should handle 0 appropriately. (More on LUA_RETURN_NIL later)

Return values

Return values are set via the lua_set_args function, which works pretty much exactly the same as lua_get_args, except in reverse. The differences:

  • Actual variables, rather than pointers, are passed.
  • Objects use their .Set() function (and there is no SetPtr() function).
  • No | character

Always use "return lua_set_args", or one of the following:

  • LUA_RETURN_NIL - Function returns nothing.
  • LUA_RETURN_FALSE - Function returns false.
  • LUA_RETURN_TRUE - Function returns true.

Yes, you can have multiple return values. The syntax in Lua is:

a, b = Library.theFunction() --In Lua

Operators

Definitely an advanced topic, which I will write when someone has need for it. These are slightly crazy as you can reverse objects and I don't fully understand what happens when you try to use an operator on two different objects that have different actions defined for that one operator.

Look at the vector class for what I've done with operators; if you got the LUA_FUNC section, you should be able to understand it. Note that the lua_isnumber() functions take the almighty L, and the argument index to check (Starting from 1).

Look up the lua_Operators[] array for the full list of operators, as well as a bit of descriptive text.


LUA_VAR

LUA_VAR(Name, l_Species, "String", "Species name")

Arguments are variable name, object that this variable will be a member of, the type that this variable is, and a brief description of what the variable is.

Variables defined by fs2_open are functions. This is generally the easiest and fastest way to go about doing things, since Lua is only a part of the much-larger fs2_open base, which cannot be going through the API to get at variables - not without causing massive code rewrites and slowdowns.

So, if you want an object to have actual data, you'll have to rely on the internal C dataset to do this. See the vector class for a good example.

Actually doing stuff with LUA_VAR

The two big differences with LUA_VAR are the LUA_SETTING_VAR macro, and how lua_get_args is used. Much like with functions, the first argument of lua_get_args will be the object that the variable is a member of. The second argument is optional, and is the value that the variable may be set to.

Here's the body for the above example, minus some safeguarding to make it easier to read:

	char *s = NULL;
	int idx;
	if(!lua_get_args(L, "o|s", l_Species.Get(&idx), &s))
		return LUA_RETURN_NIL;

	if(LUA_SETTING_VAR && s != NULL) {
		strncpy(Species_info[idx].species_name, s, sizeof(Species_info[idx].species_name)-1);
	}

	return lua_set_args(L, "s", Species_info[idx].species_name);

You should consider LUA_SETTING_VAR to be the safest way of determining whether or not a variable is actually being set, because it uses Lua's own internal guidelines to determine this.

It is good practice to return the value of the variable after changing it, so the scripter can check the value afterwards.


LUA_INDEXER

LUA_INDEXER(l_ShipTextures, "Texture name or index", "Texture", "Ship textures")

First argument is the object, second is what can be placed in the brackets ([]), third is the type returned, and fourth is a description of what the array is.

The first argument passed to the indexer will be the parent object; the second, the index (This can be whatever you like - a number, a string, even an image handle, just as long as lua_get_args can handle it). The third, the value that the scripter wants the current index to be set to.

In all other respects, it is exactly like LUA_VAR; use LUA_SETTING_VAR to determine whether you are in setting mode or not.