Lua Modding Overview
An introduction to adding dynamic Lua behavior to mobs, interactables, items, weather, and chat commands.
Mods can use Lua scripts to add dynamic behaviour to the game — custom logic for mobs, interactables, items, weather, and chat commands. Scripts are plain .lua files that live inside your mod folder and are declared in your mod’s YAML config.
Mod Directory Structure
instances/<instance>/mods/<YourMod>/
├── YourMod.yaml — mod metadata and config file references
├── commands/ — Lua scripts for chat commands
│ └── ping.lua
├── mobs/
│ └── ...
├── items/
│ └── ...
└── ...
Your mod’s root YAML file (YourMod.yaml) declares which config files to load. Add a line for each category you use (see the Mod Manifest reference for the full field list and the registered-vs-auto-discovered content map):
mod-info:
name: YourMod
version: 1.0.0
description: A short description.
authors:
- Your Name
block-config: block-config.yaml
item-config: item-config.yaml
mob-config: mob-config.yaml
commands-config: commands-config.yaml # chat commands
# ... other categories
Lua Hooks Overview
Scripts are attached to game objects by declaring them in that object’s YAML config. Every hook is declared inside a scripts: map on the config entry, keyed by the event name, with the value a bare path to a .lua file that returns its handler function — see the categories below.
Paths are relative to your mod’s folder, with or without a leading slash.
/mobs/my_mob/x.luaandmobs/my_mob/x.luaresolve to the same file — both mean<your mod>/mobs/my_mob/x.lua, never a filesystem-absolute path. The same rule applies to every other path a config points at (a chat command’sscript, a sound’ssrc). The shipped configs use the leading-slash form for scripts and the no-slash form for soundsrc; either is fine, so pick one and keep your own mod consistent.
Mob Hooks
Every mob hook lives under a single scripts: block in the mob entry in mob-config.yaml. Each value is just the path to a Lua file that returns its function:
scripts:
server-update: /mobs/my_mob/scripts/server_update.lua
client-on-interact: /mobs/my_mob/scripts/client_interact.lua
-- /mobs/my_mob/scripts/server_update.lua
return function(entity, delta_ns)
-- ...
end
scripts: key | When Called | Signature |
|---|---|---|
server-update | Every server frame | function(entity, delta_ns) |
server-on-create | When the mob spawns | function(entity) |
client-update | Every client frame | function(entity, delta_ns) |
client-on-create | When the mob appears on a client | function(entity) |
client-on-death | When the mob dies (client-side) | function(entity) |
client-on-interact | Player interacts with the mob (client) | function(entity) |
server-on-mounted / server-on-dismounted | A rider mounts/dismounts (needs a mount: block) | function(rider, mount) |
Mobs can also declare NPC dialogue hooks (server-on-dialogue-open, server-on-dialogue-choice, also under scripts:) — see lua-npc-dialogue.md. See mob-config.md for the full details.
Player Hooks
Declared under the scripts: map at the top level of your player config file (referenced from YourMod.yaml as player-config):
scripts: key | When Called | Signature |
|---|---|---|
server-update | Every server frame, for every player | function(player, delta_ns) |
Item Primary-Use Hook
Declared under the item entry’s scripts: map in item-config.yaml:
scripts:
server-on-primary-use: /items/sword_use.lua
Signature: function(entity, target_x, target_y) — entity is the player using the item.
Interactable Hooks
Declared under the interactable entry’s scripts: map in interactable-config.yaml:
scripts: key | When Called | Signature |
|---|---|---|
server-on-create | Placed in the world (server) | function(static_object) |
server-on-interact | Player interacts (server) | function(static_object, player_entity_id, player) |
server-update | Every server frame | function(static_object, delta_ns) |
server-on-inventory-close | Player closes this object’s inventory UI | function(static_object, player_entity_id) |
client-on-create | Received/loaded on a client | function(static_object) |
client-on-interact | Player interacts (client) | function(static_object) |
client-update | Every client frame | function(static_object, delta_ns) |
The player argument of server-on-interact is the interacting player’s entity object. Useful methods on it include player:send_message(text) (private chat reply), player:set_custom_spawn(x, y) (set the player’s respawn point, e.g. a bed), and player:clear_custom_spawn() (revert to the world default spawn). See lua-server-entity.md and lua-server-static-object.md for everything available on the two objects.
An interactable definition can also share its name with an entity (e.g. a mob): when a player interacts with such an entity, the same server-on-interact hook fires but receives just the interacted entity — function(entity). The definition’s interact-range is respected in both cases.
Block Hooks
Declared under the block entry’s scripts: map in block-config.yaml — see lua-server-world.md for details:
scripts: key | When Called | Signature |
|---|---|---|
server-on-interact | Player interacts with a block of this type | function(world, x, y, layer, player_entity_id) |
server-update | Every server frame, per placed block of this type | function(world, x, y, layer, delta_ns) |
Mount Hooks
Declared under the scripts: map inside a mount entry in mount-config.yaml:
scripts: key | When Called | Signature |
|---|---|---|
server-update | Every server frame | function(mount, delta_ns) |
server-on-mounted | An entity mounts (server) | function(rider, mount) |
server-on-dismounted | An entity dismounts (server) | function(rider, mount) |
client-update | Every client frame | function(mount, delta_ns) |
client-on-create | Mount appears on a client | function(mount) |
client-on-death | Mount destroyed (client-side) | function(mount) |
Rideable mobs declare their own server-on-mounted / server-on-dismounted hooks in their scripts: block in mob-config.yaml (return style, like every other mob hook) — not under the mount config.
Weather Hooks
Declared under the weather entry’s scripts: map in weather-config.yaml — see lua-server-weather.md for the weather region API:
scripts: key | When Called | Signature |
|---|---|---|
server-update | Every server frame | function(world, weather_region, delta_ns) |
server-on-start | Weather type begins | function(world, weather_region) |
server-on-end | Weather type ends (before the next begins) | function(world, weather_region) |
Chat Command Hooks
Registered via commands-config.yaml (referenced from YourMod.yaml as commands-config). See lua-chat-commands.md for full details.
Sharing Code Between Files (require)
Hook scripts can share code by pulling in reusable Lua modules with the standard require function. This is the recommended way to factor out helpers (AI logic, math utilities, vendored libraries, etc.) instead of copy-pasting code between hook files or relying on global variables.
Where modules live
Modules are plain .lua files placed in one of two folders at the root of your mod:
instances/<instance>/mods/<YourMod>/
├── scripts/ — your mod's own reusable modules
│ └── mobs/
│ └── glorbo_ai.lua
└── lib/ — vendored third-party Lua libraries
└── json.lua
A module file should follow the standard Lua module idiom — build up a local table and return it:
-- scripts/mobs/glorbo_ai.lua
local M = {}
function M.is_in_attack_range(entity_location, target_location)
-- ...
return true
end
return M
Requiring a module
Modules are namespaced by the mod that owns them, so require calls always look like require("ModName.path.to.module"):
local glorbo_ai = require("Creation.mobs.glorbo_ai")
The dotted path after the mod name maps to a file under that mod’s scripts/ or lib/ folder (. becomes /, with .lua appended). Creation.mobs.glorbo_ai resolves to the first of:
mods/Creation/scripts/mobs/glorbo_ai.luamods/Creation/scripts/mobs/glorbo_ai/init.luamods/Creation/lib/mobs/glorbo_ai.lua
A module is only loaded once per game session — every require of the same path after the first returns the same cached table, so it’s safe to require a module from many different files without worrying about it running its setup code repeatedly.
Requiring across mods
You can require modules from another mod, but only if your mod declares that mod as a dependency in its YAML config:
mod-info:
name: YourMod
version: 1.0.0
dependencies:
- name: Creation
version: 1.0.0
Without that declaration, require("Creation.mobs.glorbo_ai") from YourMod will fail with an error explaining that the dependency needs to be declared. This keeps mod load order and compatibility explicit — you can always require your own mod’s modules freely.
Sandboxing and Available Standard Libraries
Mods are shipped inside worlds, servers, and modpacks that players download and run, so the Lua environment is sandboxed: it cannot touch the operating system or the filesystem. This protects players who load your mod (and server operators running it) from a mod being able to run programs or read and write arbitrary files on their machine.
Both the server and client VMs load only this subset of the Lua 5.4 standard library:
| Library | Available | Notes |
|---|---|---|
string | ✅ | Full |
table | ✅ | Full |
math | ✅ | Full |
utf8 | ✅ | Full |
coroutine | ✅ | Full |
package | ⚠️ Partial | require works through the mod loader (below); package.loadlib and C modules are disabled |
os | ❌ Removed | No os.execute, os.remove, os.getenv, os.rename, etc. os is nil |
io | ❌ Removed | No io.open, io.read, io.write, io.popen, etc. io is nil |
debug | ❌ Removed | Not loaded |
The base-library file loaders dofile and loadfile are also removed (they are nil), and loading pre-compiled Lua bytecode is rejected — chunks must be plain Lua source. load of a text string still works for dynamic code generation.
Loading your files: because os/io are unavailable, you cannot open files directly. Load Lua from your mod with require("ModName.module") (see Sharing Code Between Files above); load data files (images, sounds, configs) by declaring them in your mod’s YAML, which the engine reads for you. require resolves only through the mod loader — the default file-path search is disabled — so it can load code only from your mod’s scripts//lib/ folders or a declared dependency’s, never an arbitrary path on disk.
If you have a use case that genuinely needs a capability outside this set, open an issue rather than trying to work around the sandbox — it is enforced in the engine and cannot be re-enabled from Lua.
Global Functions
Server-side scripts get globals for logging, chat, spawning, and block lookups — see lua-server-globals.md. Client-side scripts get globals for sounds, lights, and UI — see lua-client-globals.md.
API Reference
Server-side (authoritative game state — gameplay logic belongs here):
- Server Global Functions — logging, chat, spawning, locations
- Server Entity API — the entity object: movement, health, mounts, players
- Server Static Object API — the interactable/tree object
- Server World API — reading and placing blocks; block hooks
- Server Weather API — the weather region object
- NPC Dialogue — branching dialogue windows
- Chat Commands — registering
/commandsfrom a mod
Client-side (visuals and audio for the local player only):
- Client Global Functions — sounds, lights, crafting/inventory UI
- Client Entity and Static Object API — animations, lights,
lua_data - Client Sound API — controlling playback, cleanup rules
- Client Light API — dynamic light fields and updates