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.lua and mobs/my_mob/x.lua resolve 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’s script, a sound’s src). The shipped configs use the leading-slash form for scripts and the no-slash form for sound src; 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: keyWhen CalledSignature
server-updateEvery server framefunction(entity, delta_ns)
server-on-createWhen the mob spawnsfunction(entity)
client-updateEvery client framefunction(entity, delta_ns)
client-on-createWhen the mob appears on a clientfunction(entity)
client-on-deathWhen the mob dies (client-side)function(entity)
client-on-interactPlayer interacts with the mob (client)function(entity)
server-on-mounted / server-on-dismountedA 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: keyWhen CalledSignature
server-updateEvery server frame, for every playerfunction(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: keyWhen CalledSignature
server-on-createPlaced in the world (server)function(static_object)
server-on-interactPlayer interacts (server)function(static_object, player_entity_id, player)
server-updateEvery server framefunction(static_object, delta_ns)
server-on-inventory-closePlayer closes this object’s inventory UIfunction(static_object, player_entity_id)
client-on-createReceived/loaded on a clientfunction(static_object)
client-on-interactPlayer interacts (client)function(static_object)
client-updateEvery client framefunction(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: keyWhen CalledSignature
server-on-interactPlayer interacts with a block of this typefunction(world, x, y, layer, player_entity_id)
server-updateEvery server frame, per placed block of this typefunction(world, x, y, layer, delta_ns)

Mount Hooks

Declared under the scripts: map inside a mount entry in mount-config.yaml:

scripts: keyWhen CalledSignature
server-updateEvery server framefunction(mount, delta_ns)
server-on-mountedAn entity mounts (server)function(rider, mount)
server-on-dismountedAn entity dismounts (server)function(rider, mount)
client-updateEvery client framefunction(mount, delta_ns)
client-on-createMount appears on a clientfunction(mount)
client-on-deathMount 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: keyWhen CalledSignature
server-updateEvery server framefunction(world, weather_region, delta_ns)
server-on-startWeather type beginsfunction(world, weather_region)
server-on-endWeather 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.lua
  • mods/Creation/scripts/mobs/glorbo_ai/init.lua
  • mods/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:

LibraryAvailableNotes
stringFull
tableFull
mathFull
utf8Full
coroutineFull
package⚠️ Partialrequire works through the mod loader (below); package.loadlib and C modules are disabled
os❌ RemovedNo os.execute, os.remove, os.getenv, os.rename, etc. os is nil
io❌ RemovedNo io.open, io.read, io.write, io.popen, etc. io is nil
debug❌ RemovedNot 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):

Client-side (visuals and audio for the local player only):