Lua scripting

Sapp is using LuaJIT-2.1.0-beta2.

Important notes:

First of all, please don't use any Lua script that you don't know to be 100% safe.

Visit the forum for example scripts that are also safe to use.

Check out the new Lua API summary page by 002.

Sapp Lua API's current version is 1.11.0.0

Lua scripts for Sapp are not compatible with Phasor scripts (or any Lua script that wasn't made for Sapp) at all, so please don't try to load them and then wonder why it's not working.

Please read everything carefully here before you ask a question.

This feature is for users who already have experience with Lua, or for people who want to learn it by themselves. I can't give any support about scripting in Lua, so please only talk to me about this if you found a bug or have an idea what else I should add.

Sapp will try to process every .lua file from the gametypes\sapp\lua folder and compile them. You will likely to get an error message here if you made a syntax mistake in your script and it won't be available.

To enable lua callbacks, use the lua 1 command. This is the general switch to enable\disable calling any lua scripts.

However, to make a lua script active, you have to use the lua_load <script_name> command. If you want to disable a script, use the lua_unload <script_name> command.

Use the lua_list command to see the current state of the scripts.

Things that must be in your lua script:

API Version: This is the version of the Lua API of Sapp that your script is compatible with.
So when you writing your script, put the actual version from the latest Sapp into your script file like:
api_version = "1.10.0.0"

You can get the current version from Sapp with the lua_api_v command.

If the mayor API version is different in your script that the one in Sapp that tries to load it, it means they are not compatible and your script won't be loaded.
If the minor version is different, you will get a warning message, but Sapp will still load your script, but it might won't work correctly.

There are only 3 basic functions whose names can't be changed:

function OnScriptLoad()

function OnScriptUnload()
function OnError()

The first two functions are necessary, they will be called when you use the lua_load and lua_unload commands.
In the first one, you have to register the callbacks that you want Sapp to call your function at an event.
For the best performance, only register the callbacks that you actually need and using it.

The third function will be called if an error happens in your Lua script (if you declared it). Just add the following code to (possible the top) of your script and you can get stack trace information that will might help a lot:
function OnError(Message)
    print(debug.traceback())
end
You can also use StackTracePlus which gives even more information.

For Example:
function OnScriptLoad()
    register_callback(cb['EVENT_SPAWN'], "OnPlayerSpawn")
    register_callback(cb['EVENT_DIE'], "OnPlayerDeath")
    register_callback(cb['EVENT_CHAT'], "OnChatMessage")
end

function OnScriptUnload()
    -- Not really necessary to fill this if you have nothing to reset.
end

And your functions will be like:

function OnEventDie(PlayerIndex)
...
end

function OnEventDie(PlayerIndex, Killer)
...
end

function OnWeaponPickup(PlayerIndex, Index, Type)
...
end

etc.

Special events for Lua callbacks only (registered the same way):

function OnChatMessage(PlayerIndex, Message, Type)

This is the only event that has a return value, a boolean. If you return true, the chat message will be visible, if false, the chat message will be blocked.
Message is just a copy of the original chat message, therefore you can't modify that. Type is where the chat message was sent 0 - global, 1- team, 2- vehicle.

function OnCommand(PlayerIndex, Command, Enviroment, Password)

It runs whenever a player executes a command. PlayerIndex and Command are obvious, Enviroment: 0: console, 1: rcon, 2: chat, Password is the password used in the rcon, otherwise a nil. This event is executed before Sapp checks for admin permissions, and the command will be blocked if the function returns false (same as EVENT_CHAT).

function OnEcho(PlayerIndex, Echo)

It will return the outputs of the command in the Echo parameter if the execute_command were used with the true third parameter. Note that this is called once for every line.

function OnObjectSpawn (PlayerIndex, MapID, ParentID, ObjectID)

Called when an object is being created. PlayerIndex is the Index value of the associated player (or 0), MapID is the "MetaIndex" aka the tags table ID, ParentID is the ObjectID of the parent object or 0xFFFFFFFF if there is no parent, ObjectID will be the ID of the object being created, but it's not valid until the callback returns. If you return false, the object creation will be blocked, otherwise you can return a second parameter which is the MapID of the object that you want to change the object to which is being created. Note that it must have the same type, otherwise this parameter is ignored.

function OnDamageApplication(PlayerIndex, Causer, MetaID, Damage, HitString, Backtap)

Called when a damage is being applied to a player. PlayerIndex is the index of the player who suffers the damage, Causer is the index of the player who caused the damage, MetaID is the Tag-Index of the damage object, Damage is the amount of damage made, HitString can be "head", "body" or "legs", Backtap is true if the damage was a backtap. It has two optional return values, first is a boolean if the damage should be enabled, the second is the new damage amount you want to apply. Note: Blocking ("return false") fall damage (Causer == "0") will result the players stuck in the server when they quit. To block fall damage, use "return true, 0" instead.

cb is a table of the possible callbacks, which are the following:

EVENT_TICK, EVENT_ALIVE, EVENT_AREA_ENTER, EVENT_AREA_EXIT, EVENT_PRESPAWN, EVENT_SPAWN, EVENT_KILL, EVENT_DIE, EVENT_SNAP, EVENT_WARP,
EVENT_WEAPON_PICKUP, EVENT_WEAPON_DROP, EVENT_VEHICLE_ENTER, EVENT_VEHICLE_EXIT, EVENT_BETRAY, EVENT_SUICIDE, EVENT_SCORE,
EVENT_TEAM_SWITCH, EVENT_PREJOIN, EVENT_JOIN, EVENT_LEAVE, EVENT_CAMP, EVENT_LOGIN, EVENT_GAME_START, EVENT_GAME_END, EVENT_MAP_RESET, EVENT_CHAT, EVENT_COMMAND, EVENT_ECHO, EVENT_OBJECT_SPAWN, EVENT_DAMAGE_APPLICATION

You can give any name to your functions. The first parameter is always the player index (1-16) which is an integer and it's used for Halo and Sapp commands.
If there are special arguments, they will be the second, third, etc. parameters and they will be all strings.

Callbacks can be also unregistered with
unregister_callback(cb[<EVENT_ID>])

You can also call a Lua script function from Sapp:
lua_call <script name> <function name> [arguments]

For example if your "test.lua" script has a function like:
function TestFunc(index, msg)
    say(tonumber(index), msg)
end

Then it can be called from Sapp like lua_call test TestFunc 1 Hello!

Note that every argument of the function is passed as a string.

Sapp functions that you can call:

execute_command(string, PlayerIndex, Echo)
Executes any Halo or Sapp command (as the PlayerIndex player, if specified), if the Echo parameter is true, it will call the EVENT_ECHO callback for every line the command outputs. The last two parameter is optional. It also returns a boolean, false if the command was unknown, wrong or "can't be used now", otherwise true.
Example:
execute_command("k "..PlayerIndex.." 'no reason'")

execute_command_sequence(string, PlayerIndex, Echo)
Executes multiple commands separated by semicolons (like the events and the custom commands), it works in the same way as the execute_command except it has no return value.

get_var(player_index, var)
Returns any default or custom sapp variable.
Example:
local hp = get_var(PlayerIndex, "$hp")
local warnings = get_var(PlayerIndex, "$warnings")

say(PlayerIndex, message)
Sends a chat message to the given player (equals to say $n "message").

say_all(message)
Sends a chat message to every player in the server (equals to say * "message")

cprint(message, color)
Prints a message to the console, the color argument is a number and optional, if not specified the default console color will be used.

rand(min, max)
Returns a cryptographically secure pseudo-random number.
If no arguments are specified, the number will be between 0 and 2^31, if only 1 argument is specified, the number will be between 0 and the specified argument, if both, then the random number will be between the two arguments. Note that the random number can be the minimum value, but the maximum will be always one less than the max argument. For example: rand(1, 5) will return either 1, 2, 3 or 4.

The following functions are only for advanced users:

to_real_index(PlayerIndex)
Returns the real index (0-15) that Halo uses for player tables and some other score tables, -1 if the player is not present.

to_player_index(RealPlayerIndex)
Converts back to PlayerIndex (1-16) from real player index (0-15), 0 means that the player is not in the server.

player_present(PlayerIndex)
Returns true if the player is present in the server, false if not.

player_alive(PlayerIndex)
Returns true if the player is alive, false if player is dead or not in the server.

get_player(PlayerIndex)
Returns the memory address of the player's player table entry.

get_dynamic_player(PlayerIndex)
Returns the memory address of the dynamic player object. 0 if the player is not present or dead.

get_object_memory(ObjectID)
Returns the dynamic memory of an object. 0 if the object doesn't exist anymore.

lookup_tag(MetaID) and lookup_tag(type, name)
Returns the address of the tag entry. The tag entry has the following structure (Class0 is the 4 letter type):

struct Tag_Entry
{
    DWORD Class0;
    DWORD Class1;
    DWORD Class2;
    DWORD MetaID;
    char* TagName;
    void* TagStruct;
    BYTE Unknown[8];
};

spawn_object(type, name, x, y, z, rot, MetaID)
Spawns an object to the given coordinates and returns it's ObjectID that you can use in the other commands.
Example:
local ball = spawn_object("weap", "weapons\\ball\\ball", 12.34, -20.5, 10)
Note that here you have to use double backslash in the object names.
or in a protected map:
spawn("vehi", "thatvehicleyoualwayswanted", 10, 20, 30, 3.14, 0xE25E00EA)
If the MetaID is specified, you don't have to give a real path to the object, only should give the type correctly. Return value is the ObjectID of the spawned object, or 0xFFFFFFFF if the function failed.

destroy_object(ObjectID)
Destroys the given object.

sync_ammo(ObjectID)
Synchronizes the ammo for the given ObjectID (weapon).

powerup_interact(ObjectID, PlayerIndex)
Makes the player interact with a powerup equipment. It returns false if the player was dead, the ObjectID was invalid, or the object wasn't an equipment, otherwise true, even if the player couldn't interact with the object, for example because he had already full HP or Over Shield.

assign_weapon(ObjectID, PlayerIndex)
Assigns the weapon with the ObjectID to the given player. Returns a boolean if it was successful or not.

drop_weapon(PlayerIndex)
Forces the player to drop the current weapon from his/her hand.

enter_vehicle(ObjectID, PlayerIndex, Seat)
Enters the player to the vehicle with the given ObjectID into the specified seat.

exit_vehicle(PlayerIndex)
Ejects the given player from the vehicle.

kill(PlayerIndex)
Kills the given player.

camo(PlayerIndex, duration)
Makes the given player invisible for the duration specified in ticks (1 second = 30 ticks), or until death if duration is 0.

system_status()
Returns the system load values, in percent.
Example: local CurrentCPULoad, TotalCPULoad, MemoryLoad = system_status()

Timers:

With timers, you can call functions delayed or repeatedly;

timer(<milliseconds>, <callback>, [arguments]...)

This means that the "callback" function will be called with the given argument(s) after the specified "milliseconds" passed. If the callback function returns true, the timer will be called again with the same arguments, if returns false or nothing then it won't be called again.

For example if you want a function to run once, after 5 seconds a player joined, then it looks like:

function OnPlayerJoin(PlayerIndex)
    timer(5000, "hello", "PlayerIndex")
end

function hello(PlayerIndex)
    execute_command("say $n Hello $name!", tonumber(PlayerIndex))
    return false
end

This equals to the following:
event_join 'w8 5;say $n "Hello $name!"'

An example for a "ticking" timer:

stop = false
ticks = 0
function OnGameStart()
    stop = false
    ticks = 0
    timer(1000, "test")
end

function OnGameEnd()
    stop = true
end

function test()
    ticks = ticks + 1
    say_all(ticks.."s passed since game start")
    if stop == false then
        return true
    else
        return false
    end
end

Note that doing anything from the scripts is fully thread safe.

Memory management functions:

Note that by default these functions have no security check if you pass them a valid address or not, and if you don't you will probably crash the whole server, so be careful when using these.
To safely read/write memory locations, use
safe_read (boolean)
and
safe_write (boolean)
functions to turn on\off safety checks.
Note that enabling these can cause performance drop with lot of read/write, so only enable them if you really need, for example patching Halo's code, then disable when you don't need them, anymore.

Read functions take one argument, the address to read from.
Write functions take two arguments, the address to write to, and the value to write.

Return values:

Safe read OFF (default):
Read functions return the value they have read or crashes the server if the address is invalid.
Safe write OFF (default):
Write functions return true if succeed, or crashes the server if the address is invalid.

Safe read ON (slower):
Read functions return the value they have read, nil if the address is invalid.
Safe write ON (slower):
Write functions return true if succeed, false if the address is invalid.

Exception is read_vector3d which returns 3 floats if successful.
Example usage:
local DynPlayer = get_dynamic_player(PlayerIndex)
if DynPlayer ~= 0 then
    local x, y, z = read_vector3d(DynPlayer + 0x5c)
    ...

This will return the coordinates of the given player into the x, y, z variables.

Functions to read memory:

    read_bit(address, bit)
    read_char(address)

    read_byte(address)
    read_short(address)
    read_word(address)
    read_int(address)
    read_dword(address)
    read_float(address)
    read_double(address)
    read_vector3d(address)

    read_string(address)

Functions to write memory:

    write_bit(address, bit, value)
    write_char(address, value)
    write_byte(address, value)
    write_short(address, value)
    write_word(address, value)
    write_int(address, value)
    write_dword(address, value)
    write_float(address, value)
    write_double(address, value)
    write_vector3d(address, value1, value2, value3)
    write_string(address, value)

With the read/write_bit function the address always belongs to a byte, therefore the bit parameter should be always between 0 and 7, and the value parameter will be always converted to 0 or 1.

byte is unsigned char, word is unsigned short, dword is unsigned int, float is 4, double is 8 byte floating point numbers, vector3d is 3 floats in a row, and strings are array of 1 byte chars (and not wide-char strings with 2 byte characters such as player names.

sig_scan (signature)
Scans for the given (masked) signature in Halo's code, returns the address if found or 0 if didn't.
Signature can't have spaces and can't start with masked byte.
Example:
local addr = sig_scan("83EC??568BF0A0????????84C00F84")

Map Management

Command Effect

map_load <map_name>

Loads a new map from the maps folder, therefore there is no need to restart the server anymore if you want to install new maps.

map_query <string>

Query the list of the maps from the server that are available for download and has the "string" in the name.

Example: map_query cold

Returns:
ID  Name
 0  coldaphobia
 1  coldaphobia_2
 2  coldrush
 3  coldsnap
 4  icecold
 5  [h3]cold_v1

map_download <map_ID>

Downloads and install the map that you choose from the query list. Note that you don't have to use the map_load command for the maps you downloaded, just be patient while it gets downloaded, extracted and installed.

Example: After you executed the map_query cold command, use map_download 3 to download and load Coldsnap.

Licensing

Sapp Mapcycle

A map cycle puts the server on a continuous loop of games. Using the SAPP map cycle is advantageous in that it’s non-volatile (the map cycle is saved into a file), you can edit the map cycle as it’s being played, you can skip the map cycle, and you can automatically skip map cycle entries if there are too many or not enough players.

Note: Make sure your map cycle covers your entire possible range of players. If SAPP cannot find a game that can be loaded with the current number of players, the minimum or maximum player count may be ignored when picking the next game.

Note: Enabling the map cycle will disable map voting.

To set up your map cycle, you can edit your mapcycle.txt, or use SAPP’s map cycle commands. This is how each line in mapcycle.txt file is formatted:
map:game variant:minimum players:maximum players

Setting up the SAPP mapcycle:
changing from Halo's default mapcycle to SAPP's mapcycle:

There is a mapcycle.txt (ANSI encoding) in the gametypes\sapp folder.
Move the mapcycle options from your Halo's  init.txt to your mapcycle.txt file. Make sure to remove the "sv_mapcycle_add" part, so taht your map's are copied in the "map:mode" format.
Now you can specify the minimum and maximum players.

Finally put "mapcycle_begin" command (without the sv_) after the load command in your init.txt.
Add sapp_mapcycle 1 to Sapp's init.txt.


Example:
Halo format:
init.txt:

...
sv_mapcycle_add bloodgulch ctf
sv_mapcycle_add ratrace slayer
sv_mapcycle_add sidewinder ctf
sv_mapcycle_add carousel slayer

sv_mapcycle_begin
load


Sapp format:
mapcycle.txt:
bloodgulch:ctf:0:16
ratrace:slayer:0:16
sidewinder:ctf:0:16
carousel:slayer:0:16


init.txt:
...
load
mapcycle_begin

Sapp's init.txt:
...
sapp_mapcycle 1


Sapp Mapcycle Commands:

Command Usage Effect

map_next

Start the next game in the mapcycle.

map_prev

Start the previous game in the mapcycle.

map_spec <index>

Skip the mapcycle to a certain point.

mapcycle

Get a list of games in the map cycle and their indices.

mapcycle_add <map> <game variant> [minimum players] [maximum players] [index]

Insert a new mapcycle entry with a map and gametype variant.

Optionally, the minimum players and maximum players can be added (by default they’re 0 and 16, respectively), and the game can also be inserted at a certain index, moving games at the index and afterwards after the index.

mapcycle_begin

Begin the mapcycle from the beginning.

mapcycle_del <index>

Remove a mapcycle entry.

sapp_mapcycle [enabled]

This will enable SAPP’s mapcycle. mapcycle_begin will also automatically enable this if it isn’t already enabled.

Default: false

 

Copyright © 2025 SAPP: Halo and Halo Custom Edition Server App. All Rights Reserved.
SAPP is sponsored by Elite Game Servers