Moud

Blocks#

The level is the Minecraft world your place runs inside: the terrain, the buildings, everything made of blocks rather than of instances. game.blocks is how you read it and change it. Positions are vec3 values floored to the block they are inside, so vec3(0.7, 64.2, -3.1) and vec3(0, 64, -3) name the same block.

The server writes the level and every player sees what it writes. A client has its own copy: a LocalScript can read it and can change it too, but those changes are only drawn on that client. Use them for effects and previews. The next time the server changes that block, the server wins.

Reading#

luau
local b = game.blocks
b:get(vec3(0, 64, 0))           -- "minecraft:oak_stairs[facing=north,half=bottom]"
b:isSolid(at)  b:isAir(at)  b:isFluid(at)
b:lightAt(at)                   -- 0 to 15
b:topAt(x, z)                   -- y of the highest block something stands on
b:count("minecraft:stone", from, to)
local ores = b:find("minecraft:diamond_ore", centre, 20, 10)   -- block centres, nearest first, at most 10
  • get(at) returns the block's name along with its state, in the game's own text form.
  • isSolid, isAir and isFluid answer the three questions you ask most often without your having to read the name apart.
  • lightAt(at) is the light level at that block, from 0 to 15.
  • topAt(x, z) gives you the y of the highest block something stands on at that column, which is what you want before dropping a part or a body onto the terrain.
  • count(name, from, to) counts the matching blocks in the box between the two corners.
  • find(name, centre, radius, limit) returns the centre of each matching block within radius of centre, nearest first, and stops at limit of them.

State without the text#

A block's state is the part in brackets: which way a stair faces, whether a door is open. You never have to take the text apart yourself:

luau
local id, state = b:getState(at)       -- "minecraft:oak_door", { facing = "north", open = false, ... }
b:setState(at, id, { open = true })    -- the rest of the state stays the default
local stair = b:withState("minecraft:oak_stairs", { facing = "east", half = "top" })
  • getState(at) returns the name without the brackets and a table of the state. Numbers come back as numbers and true/false as booleans.
  • setState(at, name, state) places name with those values.
  • withState(block, changes) returns a block name with some values changed, ready for set, fill or a palette.

Reading a whole region#

read hands you a box of blocks in one call, as a palette and a buffer:

luau
local palette, cells = b:read(vec3(0, 60, 0), vec3(15, 70, 15))
local size = vec3(16, 11, 16)
local function at(x, y, z)   -- 0-based inside the box
    local i = (y * size.z + z) * size.x + x
    return palette[buffer.readu16(cells, i * 2) + 1]
end
  • Each cell is a 16-bit index into palette, starting at 0. X changes fastest, then z, then y.
  • It is thousands of times faster than calling get in a loop, and the buffer is small enough to keep in a DataStore.

A block name matches with or without its state. minecraft:oak_stairs matches every stair, whichever way it faces, and minecraft:oak_stairs[facing=north,half=bottom] matches only the ones facing north on the bottom half.

A whole area at once#

topsIn reads the surface of every column between two corners in one call, which is what you want before scattering grass, trees or rocks over the ground:

luau
local from, to = vec3(0, 40, 0), vec3(63, 120, 63)
local tops, blocks, names = b:topsIn(from, to, { "minecraft:grass_block" })
local width = to.x - from.x + 1

for column = 0, buffer.len(blocks) // 2 - 1 do
    if buffer.readu16(blocks, column * 2) > 0 then
        local x = from.x + column % width
        local z = from.z + column // width
        local y = buffer.readf32(tops, column * 4)
        -- something stands at x, y, z
    end
end
  • tops holds one 32-bit float a column: the height of the surface, the top of the highest block something stands on, looking down from to.y to from.y. On a slab that is half a block lower than on a full block.
  • blocks holds one 16-bit number a column: which name of names that block is, counting from 1. 0 means the column has nothing there, or its top is not one of the names you asked for.
  • The columns run along x first, then z. Column (x, z) is number (z - from.z) * width + (x - from.x), at byte column * 4 of tops and column * 2 of blocks, where from is the lower corner.
  • The last argument is optional. Given, a column counts only when its top block is one of those names. Leaves are looked through, so the ground under a tree counts, unless you name the leaves.
  • One call reads at most 512 by 512 columns. A client reads the chunks it has, and a column it has not loaded comes back empty.

Raycasts#

A block raycast shoots a line through the level and tells you the first block it hits:

luau
local block, at, distance, normal = b:raycast(from, direction, 50, { fluids = true })
  • from is where the line starts and direction is which way it goes. The 50 is how far it looks before giving up.
  • block is the name of what it hit, at is where it hit, distance is how far along the line that was, and normal is the face it came in through.
  • { fluids = true } counts water and lava as something to hit. Leave it out and the ray goes through them.

This ray only sees blocks. To hit parts and bodies as well, use the casts in Queries.

Writing#

On the server every player sees these. On a client they change only what that player sees:

luau
b:set(at, "minecraft:stone")
b:fill(from, to, "minecraft:glass")                    -- returns how many
b:replace("minecraft:dirt", "minecraft:grass_block", from, to)
b:sphere(centre, 5, "minecraft:stone", hollow)
b:cylinder(base, 3, 10, "minecraft:quartz_block", hollow)
b:line(from, to, "minecraft:gold_block")
b:hollowBox(from, to, "minecraft:oak_planks")
b:write(from, to, palette, cells)                      -- the other half of read
  • set(at, name) changes one block, the way a player placing it would: doors, redstone and water react to it.
  • fill(from, to, name) fills the box between two corners and returns how many blocks it wrote.
  • replace(from_name, to_name, from, to) only touches the blocks in that box that already match the first name, so you can turn every dirt block in a region to grass and leave the stone alone.
  • sphere, cylinder, line and hollowBox are the shapes you would otherwise write three nested loops for. hollow on the sphere and the cylinder leaves the inside empty.
  • write(from, to, palette, cells) takes what read gives back, or a buffer you filled yourself. A cell of 0xFFFF leaves that block alone.

Everything except set writes straight into the level. That makes it quick, and it means neighbours don't react: sand doesn't fall and water doesn't flow until something next to it changes.

One call changes at most 4,000,000 blocks, and a client at most 262,144. A bigger call is almost always two corners typed wrong.

A call over about 260,000 blocks lands a slice per step, over a few steps, so the server never stalls. Other writes made in the meantime queue behind it, so they still land in the order you made them.

Copy and paste#

copy reads a region out into a BlockRegion, and paste puts one back:

luau
local house = b:copy(vec3(0, 64, 0), vec3(8, 70, 8))
b:paste(house, vec3(20, 64, 0), { rotate = 1, skipAir = true })
b:paste(house:mirror("x"), vec3(40, 64, 0))
  • copy(from, to) takes the two corners of the region. house.size is how big it is and house:count() is how many blocks it holds.
  • paste(region, at, options) puts it down with its lowest corner at at.
    • rotate is a number of quarter turns clockwise.
    • mirror is "x" or "z".
    • With skipAir, the air in the copy leaves whatever is already there alone, which is what you want when you paste a building onto ground you want to keep.
  • region:rotate(turns) and region:mirror(axis) return turned copies. Stairs, doors and logs turn with the region, so they still face the right way.

A region is not plain data. To keep one, save house:encode(), a buffer, with store (see Saving) or send it through a remote (see Talking across the boundary). Then turn it back with b:decode(data).

Hearing changes#

changed fires once a step for every chunk section (a 16×16×16 cube) where blocks changed, whoever changed them:

luau
b.changed:connect(function(from, to, count)
    if from == to then
        print("now", b:get(from), "at", from)
    else
        print(count, "blocks changed between", from, "and", to)
    end
end)
  • from and to are the corners of the box that holds the changes inside that section. count is how many blocks changed.
  • A single block gives from == to, so read it with get when you need the new block.
  • Changes arrive the same way from anything: a place, a player mining, a set in your own script, a fire spreading.
  • A fill over a large region arrives as one call per section it touched, not one per block.