Build anything the engine can do

In Tiamat the mod API is the only API. Every block, sky and rule in the game is Lua calling the same functions you can call. If something can’t be built through it, that’s treated as an engine bug.

Your first mod

A mod is a folder with two files: a manifest that says who it is, and a Lua script that runs once when the mod loads. This one adds a brick block.

1

Make the folder

The folder name doesn’t matter to the engine. The id in the manifest does.

folder layout
my_mod/
├── mod.toml         # the manifest
├── init.lua         # runs once, at load
└── textures/
    └── brick.png
2

Write the manifest

my_mod/mod.toml
id          =  "my_mod"
name        =  "My Mod"
version     =  "0.1.0"
depends     = [ "core >=0.1" ]
description =  "Adds a brick block."
license     =  "MIT"
3

Register something in init.lua

Your ids are namespaced for you, so "brick" becomes my_mod:brick.

my_mod/init.lua
-- SPDX-License-Identifier: MIT
game.register_block {
    id       =  "brick" ,
    name     =  "Brick" ,
    hardness =  1.2 ,
    textures = { all =  "textures/brick.png"  },
}
game.log ( "my_mod ready" )
4

Drop it in and check it

Put the folder in the server’s mods directory. In the repository that’s game/, next to the reference mods. Then check it without starting the game. The check loads every mod in dependency order, reports any that fail, and lists every registered block.

terminal
cargo run -p server -- --check-mods game
cargo run -p client      # then play it
Not a coder? Let an AI coding assistant write the mod for you. Copy api/AGENTS.md into your mod’s folder. It’s a brief written for the assistant, with the rules it needs to follow.

Official mods

The default mods ship with Tiamat, so there’s nothing extra to download. They’re ordinary mods built on the same public API as yours, which makes them the best examples to read. To change one, write a mod that replaces it. Yours takes precedence automatically.

The manifest

Only three fields are required. The id is your namespace and can use letters, digits and underscores.

Field Required What it does
id
yes
Your namespace. Everything you register is prefixed with it.
name
yes
The name players see.
version
yes
Your mod’s version, such as "0.1.0".
depends
no
Mods that must load first, with version ranges: ["core >=0.1"].
conflicts
no
Mods that can’t load alongside yours. Use it when you replace a feature outright.
description
no
One line about the mod.
license
no
Any license you like. See Your license.
Replacing built-ins The mods in game/ are marked reference = true. Your mod takes precedence over them automatically, with no extra setup.

Six rules

These keep every world identical on every machine, and keep mods from breaking each other.

I

Don’t compute simulation values in Lua

Worlds must be bit-identical everywhere. Avoid math.sin, math.atan and ^ on anything that affects the world. Use the engine’s whole-buffer operations and read measured state such as e.submerged.

II

Quantities are whole units

A block is 27 units. Inventories, drops and costs are integers in units. Show them as units // 27 blocks plus units % 27 nodes, and never mix up count with units.

III

String ids are permanent, numbers aren’t

Save and compare "core:white", never the number from get_block_id. Numeric ids only last for one session.

IV

Register once, then it freezes

Calls like register_block and register_sky work only while init.lua runs. After load they raise an error. Put conditional logic in hooks.

V

Describe terrain, don’t sample it

Build density programs once at load and let the engine evaluate them. Never loop get_block down a column. Use surface_at instead.

VI

Mods name actions, the engine owns keys

Declare an action with register_action and respond in register_on_action. Players choose the key in settings.

Hooks

You register hooks during load, and they run for the rest of the session. Each mod gets one callback per hook.

init.lua
-- answer a chat command, and hide the original message
game.register_on_chat ( function (event)
     if  event.text ~=  "!hello"   then return end
     game.chat_to (event.player,  "Hello from my_mod" )
     return false
end )
Event Fields
Player join / leave
player, name
Chat
player, text
Action
player, id, pressed
Random tick
x, y, z, material
Domain enter / exit
entity, from, to
Dialog
player, form, kind, plus optional name, text, checked, value, index

API reference

Everything a mod can call lives on the game table. The stub file is the source of truth, and CI keeps it in step with the engine.

11 entries in Registration (load only)
Function Signature What it does
game.register_block
(spec) → integer
Add a block type with its properties and textures.
game.register_item
(spec: {id, name?, texture?, description?}) → integer
Add an item that can’t be placed.
game.register_tool
(spec)
Add a digging or placing tool.
game.register_view
(spec: {id, slots})
Add a named inventory view.
game.register_sky
(spec)
Set the day and night cycle and sky colors.
game.register_action
(spec)
Declare an action players can bind to a key.
game.register_sound
(spec)
Add a sound.
game.register_font
(spec)
Add a typeface for dialogs.
game.register_picture
(spec)
Add an image.
game.register_schematic
(spec) → Schematic
Add a structure template.
game.register_setting
(spec)
Add a per-world setting players can change.

Without code

Some changes need no Lua at all. They live in your manifest.

Themes

Restyle the engine’s own screens (pause, settings, start). Leave out any field to keep the default. If several mods set a theme, the last one in load order wins.

mod.toml
[theme]
font      =  "fonts/Cinzel.ttf"
text_font =  "fonts/Spectral.ttf"
sheet     =  "art/frame_iron.png"
button    =  "art/button_brass.png"
[theme.colours]
text =  "#e8dcc0"

World options

Choices that shape a world, picked before any mod runs.

mod.toml
[[world_option]]
id      =  "biome"
name    =  "Biome"
options = [ "spindle" ,  "savanna" ,  "taiga" ]
default =  1

Settings

Answers are saved per world, not per machine.

init.lua
game.register_setting {
    id      =  "difficulty" ,
    name    =  "How hard" ,
    options = { "gentle" ,  "ordinary" ,  "unfair" },
    default =  1 ,
}

Sharing & building

Share with other mods

Export values and functions once during load. Other mods read them by id. Exports are read-only, functions run inside their owner’s sandbox, and a disabled mod’s exports come back as nil.

exporting mod
game.export {
    version  =  1 ,
    humidity = HUMIDITY,
}
importing mod
local  life =  game.exports ( "tiamat_default_life" )
if  life  then
     -- use life.humidity
end

Plans: capture and stamp buildings

Copy a region and place it somewhere else. Stamping adds to the terrain rather than erasing it, and runs over several ticks. A plan can be up to 64 blocks on a side and 65,536 filled blocks.

init.lua
local  made =  game.plans.capture ( "hut" , {x= 0 ,y= 8 ,z= 0 }, {x= 7 ,y= 12 ,z= 7 })
if  made  then   game.plans.stamp ( "hut" , {x= 40 ,y= 8 ,z= 40 })  end

Containers and machines

Furnaces and other machines are named containers. Create one with make_container, then move items with container_give and container_take. Both work in units.

Checklist

Run through these before you publish a mod.

--check-mods passes
No math.sin, math.atan or ^ on world values
Density fields are built once at load, not per chunk
No numeric block ids are saved or compared to literals
Nothing is registered outside init.lua’s first run
Quantities are in units, and count is never used as units
Hooks return the types the stubs ask for (false, nil and tables mean different things)
You’ve read the limits section of AGENTS.md for anything your design relies on

Your mod is yours

The engine is GPLv3-only, but mods are not covered by the GPL. A mod that talks to the engine only through the Lua API or the network protocol is an independent work. License it however you like, including closed-source or commercial.

Part License Where it’s stated
The engine
GPLv3-only
LICENSE
The mod API (api/)
MIT
api/
Your mod
Your choice
Additional Permission under GPLv3 §7, in LICENSE.EXCEPTION

This is a formal permission, not just a promise in a README. MOD-LICENSING.md explains what it means in practice.

Resources

Scroll to Top