Getting started

Scripting with Lua needs:

  • A TR1 or TR2 TRX build from the latest develop branch that adds Lua scripting.
  • Familiarity with the game flow JSON format.

Quick steps

A level's script is named after the level it belongs to. For a level that loads level1.phd, create scripts/level1.lua in the game's directory and put the following content in it:

trx.events.on_game_start(function()
  trx.log.info("hello from level 1!")
end)

Nothing declares the file: the game flow names the level, and the script beside it under that name runs as the level loads.

Start the game. The logs should show the following:

INF | 2025-10-04 12:12:23.155 [scripts/level1.lua:2:?] hello from level 1!

The game's own script

A game can also ship one script that belongs to the game rather than to any of its levels. Create scripts/_game.lua in the game's directory and it runs once as the game starts, with the game flow, the strings and the settings all in place. Nothing declares it: the file being there is what runs it. It is where a game declares settings of its own, with trx.config.declare.

An expansion that has nothing of its own to set up needs no file: the script of the game it extends runs instead. Shipping one replaces that script rather than adding to it, so anything worth keeping goes in a module both require.

Scripts in a directory of their own

A script that belongs to no single game goes in the scripts/ directory beside the engine. It runs when a game starts. A script made of several files goes in its own directory, with init.lua as its entry point:

scripts/my_scripts/init.lua

The engine runs each init.lua it finds. A script takes effect when it is present. A directory without one does nothing. The scripts that ship with the engine run first, so a directory script can use what they have declared.

The engine looks for a file beside the script first. For example, trx.inject.declare naming my.bin finds scripts/my_scripts/my.bin before the game's own injections. The script does not need to name its directory.

Sharing code between scripts

A script can put what it has in common with another in a file of its own and require it. A name carries the directory it lives in, so a call site says which file it means:

local mine = require("tr1.my_module")            -- games/tr1/modules/my_module.lua
local nested = require("tr1.my_group.my_module") -- games/tr1/modules/my_group/my_module.lua
local other = require("tr1-ub.my_module")        -- any installed game, by its directory name
local pooled = require("common.my_module")       -- modules/my_module.lua, beside the engine
local own = require(".my_module")                -- the directory it runs from

A module lives in modules/, alongside the scripts/ the engine runs, and a name reaches modules/ alone: there is no name for a level script or for _game.lua. common is the pool every game can reach, in modules/ next to the executable; create the directory if it is not there. A game's own directory is named the way it appears in games/, so a script says which game it is reaching into and gets the same file whichever game is running. trx is reserved for the engine's own modules, which are the global trx table rather than something to require.

A name that starts with . refers to the directory of the entry point. Renaming the directory does not change its require calls. Use this form only while init.lua is loading.

A required script runs once, and every later call is handed what the first one returned:

-- modules/my_module.lua
local M = {}
function M.greet() trx.log.info("hello from my_module") end
return M

Because each form names one directory, the same name in two of them is two separate modules, and a game's own script is never mistaken for one it extends. A script required by a level script belongs to that level: it runs again for the next one, so anything it attaches is there for each level in turn. One required by _game.lua runs once for the game. The first require of a name decides which of the two it is, and every require after it is handed that module, so the game and a level never hold two copies of one name.

Names can contain letters, digits, _, - and . between directories. A relative name has one leading .. Case does not count, so two spellings name the same module. A name cannot reach outside its directory: .. is not valid.

Interactive commands

The /lua console command also runs short Lua commands in-game:

/lua