Block logic
Give a block its own behaviour in Lua. Components keep the state of every block, and batch_tick runs all blocks of a type once per tick. The examples target Evospace 0.22.2; the API reference documents version 0.22.2.
Block hooks
The examples belong to a mod with a block named Kiln. Its logic class is BlockLogic, so everything the block does comes from these fields of its StaticBlock, set in init:
| Field | What it does |
|---|---|
| logic_init | Called with the logic of every new block. Makes its inventories and accessors. |
| components | Components every block keeps, with the values a new block starts with. |
| batch_tick | Called once every sim tick with all placed blocks of the type. |
| window | Builds the window of a block. |
local kiln_tick = require('@self/kiln_tick')
local Kiln = {}
local function kiln_init(logic)
SingleSlotInventory.new(logic, "Input")
SingleSlotInventory.new(logic, "Output")
end
function Kiln.init()
Component.reg("Baking", { fields = { input = "item", progress = "integer" } })
local kiln = StaticBlock.get("Kiln")
kiln.components = { Baking = {} }
kiln.logic_init = kiln_init
kiln.batch_tick = kiln_tick.tick
end
function Kiln.post_init()
local log, coal = StaticItem.get("Log"), StaticItem.get("Coal")
kiln_tick.setup({
bakes = { [log.sim_id] = { output = integer.create(coal.sim_id), ticks = integer.create(200) } },
items = { [log.sim_id] = log, [coal.sim_id] = coal },
})
end
db:mod(Kiln) The kiln has an input slot and an output slot, and bakes a log into coal in 200 ticks. An inventory made with new(logic, name) is saved with the block, and both logic.inventories and batch_tick list it under that name.
Components
A component is a named set of typed fields. Register it in init with Component.reg, then list it in components of every block type that keeps it, with the values a new block starts with.
| Type | Holds |
|---|---|
| integer | A 64-bit whole number. |
| boolean | true or false. |
| item | An item or nil. Saved by the item's name. |
| string | Text. |
- A field left out of the starting values starts at 0, false, nil or "".
- Field names are Lua names. Two names that differ only in case are an error.
- The fields of a component are fixed once a block type lists it.
- A component can be listed by several block types, and a block type can list several components.
Lua reads and changes the components of a placed block through its logic:
| Function | What it does |
|---|---|
| logic:component(name) | A copy of the component's fields, by name. |
| logic:set_component(name, fields) | Changes the fields given. The rest keep their values. |
| logic:get_field(name, field) | One field. |
| logic:set_field(name, field, value) | Changes one field. |
An item field takes an item, its name or nil. A window reads the state it shows with logic:component("Baking").
batch_tick
batch_tick lives in a file that starts with --!sim, written in the sim dialect of Luau: every number is a 64-bit integer. The game calls batch_tick(n, components, slots, inventories) once every sim tick with all placed blocks of the type, in position order. One call covers every kiln in the world.
--!sim
local bakes, items
local function setup(data)
bakes, items = data.bakes, data.items
end
local function tick(n: number, components, slots, inventories)
local input: column, progress: column = components.Baking.input, components.Baking.progress
local slot_item: column, slot_count: column = slots.Input.item, slots.Input.count
local inputs, outputs = inventories.Input, inventories.Output
for i = 1, n do
local item = slot_item[i]
local bake = if slot_count[i] > 0 then bakes[item] else nil
if not bake then
input[i] = 0
progress[i] = 0
elseif input[i] ~= item then
input[i] = item
progress[i] = 0
elseif progress[i] + 1 < bake.ticks then
progress[i] += 1
elseif integer.create(outputs[i]:add(items[bake.output], 1)) == 0 then
inputs[i]:sub(items[item], 1)
progress[i] = 0
end
end
end
return { setup = setup, tick = tick } - n is the number of blocks.
- components.Baking.progress is the column of that field: progress[i] belongs to block i. An integer field holds its value, a boolean field 0 or 1, an item field the item's sim_id or 0 for none. String fields are left out; read them with get_field.
- slots.Input.item and slots.Input.count hold every slot of every block, taken before the call. slots.Input.slots is the number of slots per block, and slot s of block i is at (i - 1) * slots + s. Each kiln inventory has one slot, so the kiln reads its input at i.
- inventories.Input[i] is the Input inventory of block i, as its own class. Change items through it.
- After the call the game writes the columns back into the blocks. Inside batch_tick, change fields through the columns: the write-back replaces what set_field wrote during the call.
- The tables stay the same from tick to tick until a block of the type is placed or removed.
The sim file reaches the game only through the values it is given, so init.lua builds its tables in post_init, once every item has its sim_id.
The --!sim dialect
- Every number is an integer, and a literal with a fraction is an error. Divide with //, which rounds down, and take the remainder with %; / and ^ are errors. Keep fractions in fixed point, such as thousandths in an integer.
- The file sees two globals, integer and buffer. integer has min, max, clamp and the bit operations. Walk a table with for key, value in t do. Reading any other global, or assigning a global, is an error.
- A local or a parameter declared : column reads and writes a column: progress[i] is the value of block i. Take each column into such a local first, and assign it in a statement of its own, as in progress[i] = 0 or progress[i] += 1.
- A number from plain Lua or returned by a game function is a Luau number. Wrap it in integer.create before it meets an integer, as post_init does with sim_id and the kiln tick with the remainder of add. An integer used as a table key looks up the number, so tables keyed by sim_id in plain Lua work as they are.
- sim_id numbers items for one session, and another set of mods shifts it. Keep items in item fields, which the save holds by name.
luau-lsp reads sim files with the definitions from Evospace/lua-api; see Editor for the .luaurc that accepts the directive.
Saves and versions
A block saves the fields that differ from its type's starting values, an item by its name. When a save names an item the game no longer has, the log shows a warning and the field becomes nil. A block replaced by another type on build keeps the components both types list.
To change the fields of a component that saves already hold, raise its version and give it migrate. Loading a block saved by a lower version calls migrate(fields, version) with the fields the save holds, by name with items as their names, and the version that wrote them. It returns the fields for the current version. Version 2 of Baking renames item to input:
Component.reg("Baking", {
fields = { input = "item", progress = "integer" },
version = 2,
migrate = function(fields, version)
if version < 2 then
fields.input, fields.item = fields.item, nil
end
return fields
end,
}) The save holds only the fields that differ from the starting values, so a field can be missing from fields. When migrate fails, the log says so and the block keeps its starting values.
Errors
- A sim file that breaks a rule of the dialect does not load, and the log names the file and the line.
- The first error in the batch_tick of a block type goes to the log. The game keeps calling it every tick.
- An item column holding a number that is no sim_id goes to the log once, and the field becomes nil.