Skip to content

World access

Lesson 12.8

world:get_block reads one block. Real mods ask bigger questions: what is the player looking at, what is this room made of, is it dark here, what is in that chest, which blocks just changed? This lesson answers them, for vanilla blocks and your own.

Look along a line

local eye = cl.vec(player:eye_position())
local look = cl.vec(player:look_vector())
local hit = world:raycast(eye, eye + look * 10, { blocks = "outline" })
if hit.type == "block" then
  player:send_message("You look at " .. cl.registry.block_state_to_string(hit.state) .. " (" .. hit.face .. " face)")
end

A ray stops at the first thing in its way and tells you what it hit (type is "miss", "block" or "entity"), where (pos, block_pos, face) and how far (distance). The options choose what counts:

  • blocks = "collider" (the default) stops at what you bump into, "outline" at what the crosshair picks, "visual" lets glass, panes and bars through, "none" ignores blocks.
  • fluids = "any" (or "source", "water") stops at water and lava too.
  • entities = true stops at mobs and players; ignore = player leaves yourself out.

Many blocks in one call

local lo, hi = cl.pos(x - 4, y - 2, z - 4), cl.pos(x + 4, y + 2, z + 4)
local states = world:get_blocks(lo, hi)        -- 9 * 5 * 9 = 405 numbers
local stone = 0
for _, s in ipairs(states) do
  if cl.registry.block_state_is(s, "minecraft:stone") then stone = stone + 1 end
end

The list goes x first, then z, then y. One call is much faster than hundreds of get_block calls. world:snapshot(lo, hi) keeps such a copy for later: snap:get(pos) always answers from the moment you took it.

Doors, crops and other properties

Many blocks have properties: a door is open or not, wheat has an age, stairs face somewhere.

local door = world:get_block(pos)
if cl.registry.state_property(door, "open") then
  world:set_block_property(pos, "open", false)   -- close it
end
world:set_block(pos2, "minecraft:wheat[age=7]") -- fully grown
print(cl.registry.state_property("minecraft:wheat", "age"))   -- 0

state_property gives true / false, a number or a string, and nil when the block has no such property.

What a block is like

local info = cl.registry.block_info(world:get_block(pos))
if info.sound_type == "wood" then
  player:send_message("Creaky!")
end
print(info.hardness, info.light_emission, info.solid)

sound_type ("stone", "wood", "grass", "glass", "wool", …) is the material that decides footsteps and breaking sounds. There is also is_air, is_liquid, friction, map_color and more.

Light and height

local light = world:light(pos)       -- { block = 0..15, sky = 0..15, combined = ... }
if light.combined < 8 then player:send_message("It is dark here") end
print(world:can_see_sky(pos))
local ground = world:heightmap(pos.x, pos.z)   -- the y just above the ground

Chests and other containers

On the server you can look inside containers:

local chest = world:container(pos)
if chest then
  chest.set(0, cl.stack.create("minecraft:diamond", 3))
  local first = chest.get(1)          -- nil for an empty slot
end

Slots start at 0. A double chest is one container with 54 slots. Block entity data comes as SNBT text, like /data get block: world:block_entity(pos).

Hear the world change

cl.on("level.block_changed", function(e)
  if cl.registry.block_state_is(e.new_state, "minecraft:tnt") then
    cl.log("TNT placed at %d %d %d", e.pos.x, e.pos.y, e.pos.z)
  end
end, { filter = "minecraft:tnt" })

The event tells you about every block that changes (players, pistons, water, explosions, commands). The filter keeps only changes to one block, so your function runs rarely.

Exercise

Write a command /scan that counts the ores in a 16 x 16 x 16 box around the player with one get_blocks call and tells the player how many diamond and iron ores it found.

Show a solution
cl.on("server.command_register", function()
  cl.cmd.register(cl.cmd.literal("scan"):executes(function(ctx)
    local p = ctx:player()
    if not p then return 0 end
    local o = p.pos
    local x, y, z = math.floor(o.x), math.floor(o.y), math.floor(o.z)
    local states = ctx:level():get_blocks(cl.pos(x - 8, y - 8, z - 8), cl.pos(x + 7, y + 7, z + 7))
    local diamonds, iron = 0, 0
    for _, s in ipairs(states) do
      if cl.registry.block_state_is(s, "minecraft:diamond_ore") or cl.registry.block_state_is(s, "minecraft:deepslate_diamond_ore") then
        diamonds = diamonds + 1
      elseif cl.registry.block_state_is(s, "minecraft:iron_ore") or cl.registry.block_state_is(s, "minecraft:deepslate_iron_ore") then
        iron = iron + 1
      end
    end
    p:send_message(("%d diamond and %d iron ores nearby"):format(diamonds, iron))
    return 1
  end))
end)

Recap: raycast looks along a line, get_blocks and snapshot read many blocks at once, state_property / state_with / set_block_property handle properties, block_info tells what a block is made of, light, can_see_sky and heightmap measure the world, container and block_entity look inside blocks, and level.block_changed tells you when anything changes. More in the guide World Access.