Skip to content
Basalt Server

The world from Lua

Blocks, sections, block entities, entity NBT, biomes and light in the native API.

worlds.default(), worlds.by_name(name) and p.pos.world give you a world object. Single blocks use the calls below; edits of many blocks have a bulk form that is orders of magnitude cheaper.

lua
local world = worlds.default()
world:set_block(0, 64, 0, "minecraft:oak_stairs[facing=east]")
print(world:block_state(0, 64, 0))

A position can be given as three integers or as a pos.

Sections#

A section is 16 by 16 by 16 blocks. It travels as a list of 4096 block state ids, ordered x first, then z, then y (index y * 256 + z * 16 + x + 1 for local coordinates from 0). A state id is an integer; worlds.state_id and worlds.state_text convert to and from the text form.

lua
local stone = worlds.state_id("minecraft:stone")
local states = world:section(0, 4, 0)            -- chunk_x, section_y, chunk_z
for i = 1, 4096 do states[i] = stone end
world:set_section(0, 4, 0, states)
Call Does
world:section(cx, sy, cz, load?) The 4096 ids, or nil when the chunk is not loaded and load is false
world:set_section(cx, sy, cz, states, notify?) Writes a section; notify (default true) shows it to the players who see it
world:apply_chunk(cx, cz, sections, options?) Writes several sections of one chunk
world:apply_chunks(chunks, options?) Writes many chunks at once; chunks is a list of { x, z, sections } and the options are the same
world.block_version Changes whenever a block changes or a chunk loads or unloads

In apply_chunk and apply_chunks, sections maps a section y to its 4096 ids, and a negative id keeps the block that is there. options.resend (default true) shows the result to the players; options.previous = true returns a second value with what each section held before, which is what an undo needs. They return the number of blocks that changed, or nil when something was out of range (everything else is still applied).

lua
local changed, before = world:apply_chunk(0, 0, { [4] = states }, { previous = true })

A copy of a section stays good while world.block_version has not moved.

world:resend_chunks({ {0, 0}, {1, 0} }) sends chunks again to the players near them, and world:loaded_chunks() lists the loaded ones as { cx, cz } pairs.

Many positions at once#

world:read_states(positions, load?) returns the state id at each { x, y, z } (-1 where the chunk is not loaded and load is false), and world:write_states(positions, states, notify?) writes them, returning how many changed.

A change of blocks can set off more than the blocks. world:apply_changes(positions, states, effects?) writes the states and then runs effects for the blocks that changed, the way Level.setBlock does in the official server; world:update_neighbors(positions, effects?, previous?) runs them for blocks that are already written, and previous (a state id per position) says what each held before, which the departure of a block needs. effects is a list of names, or their bits added up; without it the effects are neighbors, shapes and clients:

Effect Does
departure The block that left sets off its own hooks on the way out: a piston head takes its base, a dust or a container tells what it fed
place The block that arrived sets off its own hooks: a pumpkin builds its golem, dust settles its power, a piston checks its signal, a sponge drinks
ticks The block that arrived books what it needs: sand its fall, water its flow, fire its spread, frogspawn its hatch
neighbors Tells the neighbors that one of theirs changed, in the official order, so a torch falls off a broken wall and a lamp lights beside a redstone block
shapes Reshapes the blocks around to fit, like fences joining and unsupported blocks breaking
light Works the light out again
entities A container that is replaced spills what it held as items
poi Points of interest (beds, bells, job blocks) follow the change; without it they keep their old state
clients Shows the new state to the players who see the blocks
lua
local at = { {10, 64, 10}, {10, 65, 10} }
local changed = world:apply_changes(at, { stone, stone }, { "neighbors", "shapes", "clients" })
-- sand put down this way falls, water flows, a redstone block lights the lamp next to it
world:apply_changes({ {0, 70, 0} }, { worlds.state_id("minecraft:sand") }, { "place", "ticks", "neighbors" })

The pieces of that aftermath can also be asked for on their own, for blocks already written:

Call Does
world:neighbor_changed(positions, sources?) The block at each position is told that a neighbor changed, and answers as it would in the official server
world:update_shapes(positions, states?) The blocks around each position reshape to fit it; with states, only along dust lines for a block that is or was that state
world:valid_state(where, state) The state a fence, pane, wall or stair would take if it were put there, joined to what is around it; nothing is written
world:schedule_tick(where, delay, kind?) Books a tick of the block there, or of its fluid when kind is "fluid"; false when there is nothing to tick
world:use_item_on(where, face, item, yaw?, pitch?) Uses a block item on a face (down, up, north, south, west, east) as a player holding it would; returns how many blocks were put down

Chunks#

world:chunk_sections(cx, cz, load?) returns every section of a chunk at once, a table from section y to the 4096 ids, which is one call instead of one per section. world:chunk_status(cx, cz) says where a chunk stands: unloaded, queued (asked for and on its way), loaded or ticking (loaded near a player).

request_chunk(world, cx, cz, options?) waits until a chunk is loaded, see Scheduler and coroutines. Look at chunk_status afterwards.

lua
request_chunk("world", 40, -12, { urgent = true })
if world:chunk_status(40, -12) ~= "unloaded" then
    local sections = world:chunk_sections(40, -12, false)
end

A chunk another plugin needs can be kept loaded with world:hold_chunks(token, { {40, -12} }). The chunks stay until world:release_chunks(token) is called with the same token, whether or not a player is near.

Block entities#

Chests, signs, banners and the like carry data besides their state. It is read and written as SNBT text, the same notation /data uses.

lua
local chest = world:block_entity(10, 64, 10)       -- nil when the block holds none
world:set_block_entity(10, 64, 10, chest)          -- id, x, y and z in the text are ignored
for _, at in ipairs(world:block_entities(0, 0)) do -- { x, y, z } for every one in a loaded chunk
    print(at[1], at[2], at[3])
end

set_block_entity returns false when the block there owns no block entity, and shows the change to the players who see it. create_block_entity(x, y, z, snbt?) gives the block a block entity (the block's defaults when the text is left out) in place of the one there, and remove_block_entity(x, y, z) takes it away and leaves the block; both return false when they have nothing to do.

Entities as NBT#

lua
local snbt = world:entity_nbt(entity)    -- an entity or its id
local copy = world:spawn_nbt(snbt)       -- edit Pos in the text first to place it elsewhere
world:remove_entity(copy.uuid)           -- takes an entity out by its uuid

spawn_nbt(snbt, cause?) asks the entity.spawn event first, with cause as its reason (plugin unless given), and returns nil when a handler cancels it.

Entities also expose uuid, yaw, pitch, velocity, on_ground, age, width, height and custom_name. world:entity(id_or_uuid) finds one and world:entities_in(cx, cz) lists the entities a chunk would save.

Biomes and light#

Biomes are stored per 4 by 4 by 4 cell. A chunk column has world.height / 4 * 16 of them, ordered (cell_y * 4 + cell_z) * 4 + cell_x from the lowest block up.

lua
local biomes = world:biomes(0, 0)                 -- one name per cell, nil when not loaded
biomes[1] = "minecraft:desert"
world:set_biomes(0, 0, biomes)                    -- shows the chunk again

A cell in set_biomes that is not a string keeps its biome. worlds.biomes() lists every biome name.

The server works the light out itself when blocks change, off the tick thread. These calls are for tools that edit many blocks and want to read the result or take control of it.

Call Does
world:light_at(where, layer) The level (0 to 15) of "block" or "sky" light at a position, up to date; nil when the chunk is not loaded
world:light_section(cx, sy, cz, layer, load?, fresh?) The light of a section as 2048 bytes, two levels each (index y * 256 + z * 16 + x, low nibble first), or nil; fresh (default true) works the light out first when blocks around changed
world:set_light_section(cx, sy, cz, layer, bytes) Replaces a section's light; it stands until blocks around change and the light is worked out again
world:check_light(positions) Queues the light around those blocks to be worked out again
world:relight_chunks(chunks) Works the light of those chunks out again off the tick thread, then shows them again to the players near them
lua
world:relight_chunks({ {0, 0} })
print(world:light_at(0, 64, 0, "sky"))

What blocks are#

The tables the server uses for blocks are readable, so a plugin does not need its own copy.

Call Gives
worlds.blocks() { name, first_state, state_count } for every block
worlds.block_properties(block) { name, values } for each property, in the order their values step the state id
worlds.state_flags(id) blocks_motion, occludes, full_cube, replaceable, has_fluid, has_collision, conducts_redstone, ticks_randomly, air, has_block_entity, light_emission, light_opacity
worlds.block_hardness(block) destroy_time, explosion_resistance, needs_tool
worlds.block_info(id) friction, speed_factor, jump_factor, push_reaction, map_color, ignite_odds, burn_odds
worlds.block_entity_types() Block id to the id of its block entity type, for every block that owns one
worlds.items(), worlds.entity_types() The ids, in registry order
worlds.registry(name) The ids of a registry by name: damage_type, particle_type, worldgen/biome and the other synchronized ones
worlds.registry_data(registry, id) The data a synchronized registry holds for one entry as SNBT, or nil: damage_type, jukebox_song, instrument, wolf_variant and the rest
worlds.tags(registry) Tag id to member ids, for blocks, items, fluids or entity_types
lua
local info = worlds.block_info(worlds.state_id("minecraft:ice"))
print(info.friction, info.push_reaction)   -- 0.98   normal

Old saved data#

Data written by an older version can be brought up to date with the same fixes the server applies to its own worlds. Block states and biome ids are only fixed from data version 2832 (1.18) on; older text comes back unchanged.

Call Gives
worlds.data_version() The data version the server writes
worlds.fix_nbt(kind, snbt, from_version) SNBT upgraded from from_version; kind is chunk, block_entity, entity or item
worlds.fix_name(kind, text, from_version) A block_state (minecraft:oak_stairs[facing=east]), item id or biome id upgraded
lua
print(worlds.fix_name("block_state", "minecraft:grass", 3000))   -- minecraft:short_grass

Threads#

These calls follow the rules of the rest of the API: a plugin runs in its own lane, and the world hands the work to the thread that owns it. Reading a loaded section is cheap from any lane; a write is applied before the call returns.

Edit this page