This is a function "cheat sheet" for Apollo extension by YellowAfterlife.
The extension can be acquired from GM:Marketplace or itch.io.
For questions/support, use forums (itch.io, GM forums), or send me an email.
A most up-to-date version of the manual is always available online.
The extension is currently available for Windows, Linux, and Mac (experimental).

Click on sections to expand/collapse them.
Quick display controls: Categories · Sections · Everything ·

General

lua_reset()

Destroys all existing Lua states.

This also causes newly made states to have IDs start at 0 again.

lua_error_handler : script(text, state_id)

If you assign a script into this global variable, it will be called whenever an error occurs in Lua code.

So you could, for instance, make a script that displays a message box with error text,

/// scr_handle_lua_error(msg, state)
var state = argument1;
show_message("A Lua error occurred: " + argument0);

and then link it up on game start:

lua_error_handler = scr_handle_lua_error;

Lua states

Introduction to states

A state is a subject to most of the Apollo's functions.

To put it simply, a state is the Lua program along with it's variables and current execution stack.

lua_state_create()

Creates a new Lua state and returns it's ID.

state = lua_state_create();

Lua' standard libraries are included by default.

If you don't want to expose certain API functions to the user, you can use lua_global_set to remove those entries:

lua_global_set(state, "io", undefined);
lua_state_destroy(state_id)

Destroys the given state, freeing up any resources used by it.

lua_state_destroy(state);

It is generally recommended that you clean up your states once they are no longer needed to avoid memory leaks.

lua_thread_create(state_id)

Creates a Lua "thread"-state attached to the given state.

Not to be confused with OS-level threads, Lua "threads" are separate execution states sharing the data from a parent state, coroutine-style.

lua_thread_destroy(state_id)

Destroys a previously created Lua "thread"-state.

Adding Lua code

lua_add_code(state_id, lua_code)

Attempts to compile the given snippet of Lua code, add it to given Lua state, and execute it.
Returns whether all steps succeeded.

lua_add_code(state, "print('Hello!')");

Same as with other things, compilation/runtime errors are forwarded to lua_error_handler if it is defined.

lua_add_file(state_id, path)

Attempts to load and run a snippet of Lua code from the file at the given path.

The function mimics GMS' file handling rules, preferring files in game's save directory over the files in game's installation directory.

It will, however, also accept absolute paths, bypassing sandbox restrictions.

So, if you added an included file called "some.lua", you could then load it with

lua_add_file(state, "some.lua");

Using Lua variables

lua_global_get(state_id, name)

Returns the value of the state's given global variable.

Note that this returns undefined for unsupported types.

lua_add_code(state, "test = 'Hello!'");
show_message(lua_global_get(state, "test"));
lua_global_set(state_id, name, value)

Changes the value of the state's given global variable.

lua_global_set(state, "test", "Hello!");
lua_add_code(state, "print(test) -- 'Hello!'");
lua_global_typeof(state_id, name, value)

Returns the type the state's given global variable as a string.

The usual returned values are as following:

  • "nil": an equivalent of GML's undefined. Any not-yet-set values in Lua are nil.
  • "boolean": a boolean value (true or false).
  • "number": a numeric type, same as GML's real.
  • "string": same as GML' string type.
  • "table": a Lua table. You currently can't do much with these from GML side.
  • "function": a Lua function - as such, a thing that could be called via lua_call.
  • "thread": a Lua "thread"/coroutine (more on these later).

So you could use a snippet like this to check if a state has a function named "test":

if (lua_global_typeof(state, "test") == "function") {
    lua_call(state, "test");
} else show_message("The state does not have a function called `test`!");

Calling Lua code

lua_call(state_id, name, ...arguments)

Attempts to call a Lua function stored in the given global variable of the state.

Returns the first of the function's returned values.

If an error occurs, calls lua_error_handler and returns undefined.

lua_add_code(state, "function greet(s) return 'Hello, ' .. s end");
show_message(lua_call(state, "greet", "GameMaker"));
lua_call_w(state_id, name, arguments:array)

Same as lua_call, but allows to pass in the arguments as an array.

Exposing GML to Lua

lua_add_function(state_id, name, script_id)

Exposes the given GM script to a Lua state as a global function.

For example, if you have some

/// scr_alert(text)
show_message(argument0);

you could expose it to Lua via

lua_add_function(state, "alert", scr_alert);

If you want to organize your functions in Lua-like modules, you can use lua_add_code for that:

lua_add_function(state, "game_alert", scr_alert);
lua_add_code(state, '
    game = { alert: game_alert }
');

which would then allow you to do

game.alert("Hello!")

on Lua side of things.

lua_return(...values)

Lua allows to return multiple values from a function call at once.

This function helps to do that in scripts exposed to Lua via lua_add_function.

So, you could have

/// lengthdir_xy(len, dir)
var len = argument0, dir = argument1;
return lua_return(lengthdir_x(len, dir), lengthdir_y(len, dir));

expose it via

lua_add_function(state, "lengthdir_xy", lengthdir_xy);

and use it from Lua side like

local x, y = lengthdir_xy(30, 45)
print(x, y) -- 21.21, -21.21
lua_return_w(values:array)

Same as aforementioned lua_return, but returns the contents of an array as a value list instead.
Note this will not work for nested arrays, however.

lua_return_add(...values)

Add zero or more values to the list of returned values.

This is particularly handy for any GML operations that are done in a loop, e.g.

/// instance_find_all(obj)
with (argument0) lua_return_add(id);
return lua_return_add();

The last line with an empty lua_return_add is needed to return 0 values if loop matches no instances (as runtime would otherwise assume that you are going to return something with a regular return).

lua_bool(value)

While Lua has a separate boolean type, GameMaker uses 1 as true-value and 0 as false-value.
This makes it hard to tell whether you were meaning to send 1 or true.

So there's this function, which returns either a lua_true or a lua_false depending on argument, which can be told apart by the extension explicitly, and will become the according Lua values once sent to Lua.

lua_current

When a Lua state calls the exposed GML script, this variable holds the ID of the "caller" state. Can be used if you want to do anything aside of just returning value(s).

lua_show_error(text)

Sends an error message to the currently executing Lua state.

This should only be used inside scripts exposed via lua_add_function.

/// scr_variable_global_get(name)
if (is_string(argument0)) {
	return variable_global_get(argument0);
} else lua_show_error("Expected a variable name to be a string.");
Using GM instance variables from Lua

If you are using GameMaker Studio 2 or an Early Access version of GameMaker: Studio 1, you can have Lua directly read and write variables on GameMaker instances.

To do so, you would add three scripts to your project:

/// ref_variable_instance_get(context, name)
var q = argument0, s = argument1;
with (q) return variable_instance_get(id, s);
if (q < 100000) {
    lua_show_error("Couldn't find any instances of " + string(q)
        + " (" + object_get_name(q) + ")");
} else lua_show_error("Couldn't find instance " + string(q));
return undefined;

(reads a variable from an instance),

/// ref_variable_instance_set(context, name, value)
var q = argument0, s = argument1, v = argument2, n = 0;
with (q) { variable_instance_set(id, s, v); n++; }
if (n) exit;
if (q < 100000) {
    lua_show_error("Couldn't find any instances of " + string(q)
        + " (" + object_get_name(q) + ")");
} else lua_show_error("Couldn't find instance " + string(q));

(writes a variable to an instance(s)),

/// ref_variable_instance_init(lua_state)
var q = argument0;
lua_add_function(q, "variable_instance_get", ref_variable_instance_get);
lua_add_function(q, "variable_instance_set", ref_variable_instance_set);
lua_add_code(q, '-- ref_variable_instance_init()
    __idfields = __idfields or { };
    debug.setmetatable(0, {
        __index = function(self, name)
            if (__idfields[name]) then
                return _G[name];
            else
                return variable_instance_get(self, name);
            end
        end,
        __newindex = variable_instance_set,
    })
');

(exposes the above scripts to a Lua state and sets it up to use them when trying to read/write a field on a numeric value (id)).

Then you can use them as following:

// create a Lua state:
state = lua_state_create();
// allow the state to work with GM instances:
ref_variable_instance_init(state);
// add a test function to the state -
// function takes an instance and modifies it's `result` variable.
lua_add_code(state, "function test(q) q.result = 'Hello!' end");
// call the test-function for the current instance and display the result:
result = "";
lua_call(state, "test", id);
show_message(result);

Automatically exposing scripts/functions

If you are building a medium-scale scripting API, you may find yourself needing to expose a large number of scripts (and/or built-in functions), as well as introducing argument type checking to prevent GML-side errors.

To save you from having to deal with that, Apollo includes a small utility that generates wrapper and loader scripts.

It accepts function definitions in funcname(arg1:type1, arg2:type2, ...):rtype,

  • arg1, arg2, ...: argument names. Will be shown in errors returned to Lua.
  • type1, type2, ...: argument types. Optional.
    If defined, code will be added to ensure that each argument matches it's type.
    Known types are real, bool, string;
    color, int, index, id can also be used, but are treated same as real.
  • rtype: returned type, if the function returns a specific one.
    If set to bool, return-statement will be wrapped in lua_bool call.
  • If prefixed with :, function will be marked as "instance function" and will accept an instance ID as first argument, also allowing to call it as inst.func(...) if instance access scripts are set up.

Constants can be defined either as name# (uses the value of same-named constant/variable) or name = value (computes the given GML value at inclusion time).

The tool is included with the extension as ApolloGen.exe;

A web-based version is available below:

Whenever the contents of above field are changed, updated loader script will be output into the field below:

You can then save them into a .gml file and import it to your project.

Writing Lua code

Learning Lua

"getting started" page on the Lua' website houses a large collection of links to tutorials, wikis, and other learning materials.

Lua Manual provides detailed explanations on how internal elements and all standard functions lf the language work.

Translating GML to Lua

If you have pre-existing GML code that you'd like to quickly port for use with Apollo, I have also developed an online GML->Lua compiler.

While automatic conversion won't make extensive use of Lua-specific language features, it produces functional code in vast majority of cases and the output is clean enough to tweak it manually if needed.

Lua coroutines

A summary on Lua coroutines

A coroutine, in short, is a function that can pause/resume execution at arbitrary points. These can be used for iterators, cutscenes (pausing/resuming allows to write timing in an intuitive way), tweening, AI, or anything else that benefits from maintaining the state across multi-call execution.

lua_thread_create(state_id)

Creates a "thread" state for the given Lua state and returns it's ID.

Such "threads" share the global context (variables, functions, etc.) with their parent state, but have their own clal stack, meaning that they can do their own thing (namely, executing coroutines) while the parent state does something else.

thread = lua_thread_create(state);
lua_thread_destroy(state_id)

Destroys a previously created "thread" state.

Does not free resources of the parent state, only what was owned by the thread itself.
Is a convenience function and is interchangeable with lua_state_destroy.

lua_call_start(state_id, name, ...arguments)

Starts a coroutine call on the given sate, returns whether the operation succeeded.

Note that some functions will work oddly (or not work at all) on a state that is currently amidst the coroutine call, which is why you should generally create a thread for the coroutine call.

lua_call_start_w(state_id, name, arguments:array)

Same as lua_call_start, but takes arguments as an array.

lua_call_next(state_id)

Executes the next iteration on the given state and returns whether the coroutine call is ongoing (as opposed to finishing or encountering a runtime error).

The general scheme of performing coroutine calls is thus as following:

lua_add_code(state, "
    function test(num)
        for i = 1, num do
            coroutine.yield(i)
        end
        return 'rad!'
    end
");
th = lua_thread_create(state);
if (lua_call_start(th, "test", 4)) {
    while (lua_call_next(th)) {
        show_debug_message("yield: " + string(lua_call_result));
    }
    show_debug_message("result: " + string(lua_call_result));
}
lua_thread_destroy(th);
lua_call_result

Holds the result of the last lua_call_next - yielded value when the execution continues, and final returned value when the execution stops.

FAQ

What Lua version is used?

Apollo uses Lua 5.3.4 (current version).

What platforms does it run on?

The extension currently runs on Windows, Mac, and Linux.

Lua is linked statically on Mac/Linux.

Mac may require additional tinkering (via install_name_tool - example), as library inclusion paths may vary depending on whether the game is running from IDE, whether YYC is enabled, and GMS version. If you are familiar with Mac extension development yourself, feel free to get in touch about better ways of handling this.

Any additional platforms will be investigated later, given sufficient demand.

Does it use LuaJIT?

The extension currently uses the "regular" version of Lua (no additional libraries).

If LuaJIT will reveal to not cause any additional issues on Windows/Mac/Linux, the extension will be modified to make use of it.

Limitations

Lua tables cannot be transmitted to GML automatically

While these are roughly equivalent to GM's ds_maps, the two work very differently -
ds_maps are passed by-index and managed manually (ds_map_destroy),
Lua' tables are passed by-reference and managed by garbage collector.

While future iterations on GML should make it possible to automatically convert between tables and lightweight data structures, this would only allow to return a new table/structure rather than modifying an existing one.

The issue can be approached in several ways:

  • Expose ds_maps to Lua code and use them instead of tables for transfer.
  • Have a wrapper function on Lua side to expand the table into multiple values prior to calling the GML script and/or wrap multiple returned values from GML back into a table.
  • If you do not need to read data from the table (but store/retrieve it), you can convert it to index and back on Lua side via lookup tables (see below).
Lua-specific reference types cannot be transmitted to GML automatically

Lua supports several additional reference types (such as Lua function references),
but these cannot be safely sent to GML as pointers as they are garbage-collected,
and thus may get recycled while still referenced on the GML side of things
(resulting in hard crash when trying to use the passed back value).

A good way to deal with this is to make a pair of lookup tables - since Lua allows table indexes to be of any type, you can do something like the following:

ref = {
    __r2i = { },
    __i2r = { },
    __next = 0
}
function ref.toid(fn)
    local id = ref.__r2i[fn]
    if (id == nil) then
        id = ref.__next
        ref.__next = id + 1
        ref.__r2i[fn] = id
        ref.__i2r[id] = fn
    end
    return id
end
function ref.fromid(id)
    return ref.__i2r[id]
end
function ref.free(fn)
    local id
    if (type(fn) == "number") then
        id = fn
        fn = ref.__i2r[id]
    else
        id = ref.__r2i[fn]
    end
    ref.__r2i[fn] = nil
    ref.__i2r[id] = nil
end

Which allow you to use ref.toid(some_reference) to return/create a numeric ID for a reference, ref.fromid(index) to convert one of those back to a reference, and ref.free(index_or_reference) to remove the lookup pairs (allowing Lua to safely recycle the reference when it is no longer used).