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

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