Skip to content

world.blocks

The world's volume: cubic blocks on a one-metre grid, living alongside the terrain rather than instead of it.

luau
local world = require("world")

local stone = world.blocks:find("Stone")
world.blocks:set(4, 1, -7, stone)

Coordinates are block coordinates: whole numbers, one block per metre.

A world where nobody defined a block type still has this table — it just answers air everywhere, which is true.


Reading

Reading is public: everybody in the region is already looking at the wall. These never fail, so you can use them straight in maths and conditions.

get

world.blocks:get(x, y, z) → type id

0 is air, and so is everything outside the world — a script probing past the edge is told "nothing there" rather than given an error.

solid

world.blocks:solid(x, y, z) → boolean

Is that cell something a body collides with?

Not the same as get() ~= 0. A tuft of grass is a block you walk through. Use solid for "can I stand here", get for "what is here".

find

world.blocks:find(name) → type id, or nil

The id the creator's palette gave that name. Case-insensitive.

Always look the id up by name; never write the number. The numbers belong to the creator's palette. Reordering it re-points every hard-coded id at a different material, silently, and the world starts building itself out of glass. A name is the one handle a script and a person share.

luau
local STONE = world.blocks:find("Stone")
if not STONE then
    print("this world has no Stone in its palette")
    return
end

name

world.blocks:name(id) → name, or nil

The other direction. nil for air and for ids nobody defined.

This is what a block event needs: the id arrives as a number, and "they broke Stone" is the sentence you want to write.


raycast

world.blocks:raycast(ox, oy, oz, dx, dy, dz, dist) → hit, or nil

The first solid block along a ray. dist defaults to 32 metres, maximum 512.

This is the same walk the game highlights with and validates against, so a script's idea of "the block in front of you" is the world's.

luau
local eye, dir = shot.eye, shot.dir
local hit = world.blocks:raycast(eye.x, eye.y, eye.z, dir.x, dir.y, dir.z, 6)

if hit then
    world.blocks:set(hit.x, hit.y, hit.z, nil)        -- break it
end
FieldMeaning
x, y, zthe block that was hit
blockits type id — look the name up with name(id)
distmetres from the ray origin
place_x, place_y, place_zthe empty cell against the face it entered through

The place_* fields are where a new block goes if you are building on what you hit — the same cell a player's own right click would use.

luau
if hit then
    world.blocks:set(hit.place_x, hit.place_y, hit.place_z, STONE)
end

This raycast takes loose numbers, not a table. It is the one call in the API that does — origin and direction are six arguments, not { from =, dir = }.


Writing

Both need the block permission and count against the eight-effects-per-event ceiling.

set

world.blocks:set(x, y, z, block)true, or nil, reason

Places one block. nil or 0 breaks it.

Unlike a player's own edit, this has no reach limit: a script is the world's own, and it is already inside the region's budget.

fill

world.blocks:fill(x1, y1, z1, x2, y2, z2, block)true, or nil, reason

Fills the box between two corners, inclusive. This is the verb a generated structure is built with.

luau
-- a hollow room, floor and walls
world.blocks:fill(0, 0, 0, 10, 0, 10, STONE)      -- floor
world.blocks:fill(0, 1, 0, 10, 4, 0, STONE)       -- one wall

Refused whole if the box is too big — nil, "box too big" — and it says so now rather than doing half of it and stopping.

Refusals

ReasonMeaning
permissionthis world has not granted block editing to scripts
invalid positionoutside the world
box too bigfill covering too many cells
too many commandsmore than 8 world effects in this event

A worked example: a pillar that grows

luau
--!strict
local self = require("self")
local world = require("world")

local STONE = world.blocks:find("Stone")

self:on_touch(function()
    if not STONE then
        print("no Stone in this world's palette")
        return
    end

    local here = self:position()
    local bx = math.floor(here.x)
    local bz = math.floor(here.z)
    local by = math.floor(here.y)

    -- find the first free cell above us
    for y = by, by + 12 do
        if world.blocks:get(bx, y, bz) == 0 then
            world.blocks:set(bx, y, bz, STONE)
            return
        end
    end
end)

The loop only reads, which is free — it places exactly one block, so the whole click costs one effect.

See also

Hungrit scripting documentation.