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#
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 10get(at)returns the block's name along with its state, in the game's own text form.isSolid,isAirandisFluidanswer 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 withinradiusofcentre, nearest first, and stops atlimitof 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:
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 andtrue/falseas booleans.setState(at, name, state)placesnamewith those values.withState(block, changes)returns a block name with some values changed, ready forset,fillor a palette.
Reading a whole region#
read hands you a box of blocks in one call, as a palette and a buffer:
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
getin 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:
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
endtopsholds one 32-bit float a column: the height of the surface, the top of the highest block something stands on, looking down fromto.ytofrom.y. On a slab that is half a block lower than on a full block.blocksholds one 16-bit number a column: which name ofnamesthat block is, counting from 1.0means 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 bytecolumn * 4oftopsandcolumn * 2ofblocks, wherefromis 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:
local block, at, distance, normal = b:raycast(from, direction, 50, { fluids = true })fromis where the line starts anddirectionis which way it goes. The50is how far it looks before giving up.blockis the name of what it hit,atis where it hit,distanceis how far along the line that was, andnormalis 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:
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 readset(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,lineandhollowBoxare the shapes you would otherwise write three nested loops for.hollowon the sphere and the cylinder leaves the inside empty.write(from, to, palette, cells)takes whatreadgives back, or a buffer you filled yourself. A cell of0xFFFFleaves 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:
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.sizeis how big it is andhouse:count()is how many blocks it holds.paste(region, at, options)puts it down with its lowest corner atat.rotateis a number of quarter turns clockwise.mirroris"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)andregion: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:
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)fromandtoare the corners of the box that holds the changes inside that section.countis how many blocks changed.- A single block gives
from == to, so read it withgetwhen you need the new block. - Changes arrive the same way from anything: a place, a player mining, a
setin your own script, a fire spreading. - A
fillover a large region arrives as one call per section it touched, not one per block.
