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
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.
18 entries in Hooks
Function
Signature
What it does
game.register_on_load
(fn())
Runs once, after every mod has loaded.
game.register_on_generate
(fn(buf, pos))
Generate terrain. Runs in a worker VM.
game.register_on_tick
(fn(dt_ticks))
Runs every simulation tick.
game.register_random_tick
(material, fn(event))
Runs on random blocks of one material.
game.register_on_player_join
(fn(event))
A player enters the world.
game.register_on_player_leave
(fn(event))
A player disconnects.
game.register_on_action
(fn(event))
A player presses or releases one of your actions.
game.register_on_chat
(fn(event)) → boolean | string | nil
Filter, rewrite or block a chat message.
game.register_on_dialog_event
(fn(event))
A player uses a dialog widget.
game.register_on_domain_enter
(fn(event)) → boolean?
Before an entity enters a space. Can refuse it.
game.register_on_domain_exit
(fn(event)) → boolean?
Before an entity leaves a space. Can refuse it.
game.register_on_punch
(fn(event))
An entity is hit with a tool.
game.register_on_place
(fn(event))
A block is about to be placed.
game.register_on_dig
(fn(event))
A block is about to be removed.
game.register_on_use
(fn(event))
A player right-clicks to use something.
game.register_on_fluid_flow
(fn(event))
Fluid moves or meets an obstacle.
game.register_chunk_tint
(fn(pos)) → r, g, b
Color a chunk, for biome tints.
game.register_chunk_fog
(fn(pos)) → {visibility, …} | nil
Set fog for a column.
11 entries in World
Function
Signature
What it does
game.get_block
(pos) → {material, occupancy, cells?} | nil
Read the block at a position.
game.set_block
(pos, material)
Write a whole block.
game.surface_at
({x, z, from, depth?}) → {y, material} | nil
Find the surface in a column.
game.get_light
(pos) → {sun, r, g, b}
Light levels at a block.
game.line_of_sight
(from, to) → boolean | nil
Whether one point can see another.
game.find_path
(from, to, options?) → {x,y,z}[] | nil
Find a walkable path.
game.get_fluid
(pos) → {fluid, volume} | nil
Read fluid at a position.
game.set_fluid
(pos, fluid, volume)
Place fluid.
game.noise_heightmap
(chunk_pos, options?) → Heightmap
Build terrain heights from noise.
game.density
(spec) → Density
Build a terrain density field.
game.schematic
(spec) → Schematic
Build a structure template.
11 entries in Inventory
Function
Signature
What it does
game.inventory
(player, view?) → {material, units, count}[]
Read a player’s inventory.
game.give
(player, spec) → boolean
Add to a player’s inventory.
game.take
(player, spec) → integer
Remove from a player’s inventory.
game.held
(player) → {material, …} | nil
The item in the selected hotbar slot.
game.make_container
(name, slots) → boolean
Create a container in the world.
game.open_container
(name, player) → boolean
Show a container to a player.
game.close_container
(name, player) → boolean
Close and save a container.
game.container
(name) → slots[]
Read a container’s contents.
game.container_give
(name, spec) → integer
Put items into a container.
game.container_take
(name, spec) → integer
Take items out of a container.
game.break_container
(name) → items[]
Destroy a container and return its items.
8 entries in Players & entities
Function
Signature
What it does
game.get_tool
(player) → string | nil
The player’s current digging tool.
game.set_tool
(player, tool | nil) → boolean
Equip a tool, or nil for bare hands.
game.looking_at
(player) → {x, y, z, material, face} | nil
The block under the crosshair.
game.look_direction
(player) → {x, y, z, domain} | nil
Which way the player is looking.
game.entity
(id) → {pos, vel, facing, …}
Read an entity’s state.
game.spawn_entity
(spec: {material, pos, …}) → integer
Create an entity and return its id.
game.steer_entity
(id, {pos?, vel?, facing?})
Move or turn an entity.
game.heading
(dx, dz) → number
Turn a direction into an angle in radians.
9 entries in Messages & effects
Function
Signature
What it does
game.log
(message)
Write to the server log.
game.chat_to
(player, text)
Send a chat message to one player.
game.show_dialog
(spec) → boolean
Show a UI dialog.
game.update_dialog
(spec) → boolean
Change an open dialog.
game.close_dialog
({player, form}) → boolean
Close a dialog.
game.emit_particles
({pos, rate, …})
Spawn a burst of particles.
game.flash
({pos, radius?, intensity?}) → integer
A lightning flash.
game.set_sky_modifier
(player, modifier | nil) → boolean
Overlay weather on a player’s sky.
game.set_precipitation
(player, spec | nil) → boolean
Start or stop rain or snow for a player.
4 entries in Universe
Function
Signature
What it does
game.stars
() → {id, x, y, z, magnitude, warmth}[] | nil
The star catalog for this seed.
game.world_position
() → {x, y, z} | nil
Where this world sits in the universe.
game.star_in_view
(player) → {id, alignment} | nil
The star a player is looking at.
game.content_hash
(data) → string
SHA-256 of some content, as hex.
4 entries in Lookups & sharing
Function
Signature
What it does
game.get_block_id
(qualified_name) → integer
Turn a block name into this session’s number.
game.get_material_name
(id) → string
Turn a block number back into its name.
game.export
(table)
Share values with other mods. Load only.
game.exports
(mod_id) → table | nil
Read another mod’s exports.
7 entries in Constants
Function
Signature
What it does
game.UNITS_PER_BLOCK
27
Sub-block units in one block (3×3×3).
game.CHUNK_BLOCKS
16
Chunk size, in blocks.
game.ITEMS_PER_STACK
90
Items in a full stack.
game.OCCUPANCY_FULL
integer
A 27-bit mask with every unit filled.
game.AIR
0
The empty material.
game.mod_id
string
Your namespace.
game.world_seed
integer?
The seed. nil while registering.
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.
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.