Skip to content

World access

Status: Implemented (API 1.11). Ray casts (collision, outline and visual shapes, fluids, entities), bulk block reads and snapshots for background work, block state properties, block facts (sound type, map colour, hardness, light), light levels, sky access and heightmaps, containers and block entity data, and the level.block_changed event, on the server and on the client. Explosions and scheduled ticks are Planned (chunk data attachments arrived in API 1.12: Saved data (attachments on entities, players and chunks, world data)) (World access).

This guide builds the world side of a “sound of the cave” mod: it looks where the player looks, scans the blocks around them in one call, keeps a copy for a worker, reads what the blocks are made of, opens doors, fills a chest and keeps a cache up to date when blocks change. Everything works on vanilla blocks, the same from Lua, C and C++.

Reference: World access, function tables level, registry, events. Players: Guide: Player Control.

1. Ray casts

level:raycast(from, to, opts) finds the first block, fluid or entity on the line from from to to. It is the same code the game uses for the crosshair, projectiles and line of sight (Level#clip), so the answer matches what players see.

Option Values Meaning
blocks "collider" (default), "outline", "visual", "none" which shape stops the ray: the collision box (what you bump into), the selection outline (what the crosshair picks), the visual shape (glass, panes and bars let the ray through) or nothing
fluids "none" (default), "source", "any", "water" which fluids stop it
entities true / false entity hitboxes stop it too (spectators and dead entities never do)
entity_margin blocks grows every hitbox (arrows use 0.3)
ignore an entity never hit (usually the caster; its vehicle and passengers too)
source an entity the entity the shapes are for (scaffolding and powder snow depend on it)
local eye = cl.vec(player:eye_position())
local look = cl.vec(player:look_vector())
local hit = world:raycast(eye, eye + look * 20, { blocks = "outline", fluids = "any", entities = true, ignore = player })
if hit.type == "block" then
  cl.log("looking at %s, face %s, %.1f blocks away", cl.registry.block_state_to_string(hit.state), hit.face, hit.distance)
elseif hit.type == "entity" then
  hit.entity:add_effect("minecraft:glowing", 40)
end

The result: type ("miss", "block" or "entity"), pos (where the ray enters the shape; to on a miss), distance, and for blocks block_pos, state, face and inside (the ray started inside the shape), for entities entity. A ray is at most 1024 blocks long; unloaded chunks read as air.

In C the options are a cl_raycast_opts and the hit a cl_raycast_hit:

cl_raycast_opts o = CL_RAYCAST_OPTS_INIT;
o.blocks = CL_RAY_BLOCKS_VISUAL;
o.fluids = CL_RAY_FLUIDS_ANY;
cl_raycast_hit hit;
if (cl_level_raycast(level, from, to, &o, &hit) == CL_OK && hit.type == CL_HIT_BLOCK) {
    /* hit.block_pos, hit.state, hit.face, hit.distance */
}

2. Many blocks at once

Reading a room block by block costs thousands of calls. level:get_blocks(min, max) returns every state in a box in one call, x fastest, then z, then y (the order chunks store them):

local lo, hi = cl.pos(x - 8, y - 4, z - 8), cl.pos(x + 8, y + 4, z + 8)
local states = world:get_blocks(lo, hi)     -- 17 * 9 * 17 numbers
local sx, sz = 17, 17
local function at(px, py, pz) return states[((py - lo.y) * sz + (pz - lo.z)) * sx + (px - lo.x) + 1] end

The box is at most 128 x 128 x 128 blocks (2,097,152 states). Mod blocks report their own state. Positions in unloaded chunks read as 4294967295 (CL_BLOCK_UNLOADED). C uses the usual two-call array: a buffer smaller than the box gets nothing and the count says how many states it needs.

size_t n = 0;
cl_level_get_blocks(level, lo, hi, NULL, 0, &n);            /* how many */
cl_block_state* buf = malloc(n * sizeof *buf);
cl_level_get_blocks(level, lo, hi, buf, n, &n);

WASM mods read up to 262,144 states per call (one 1 MB transfer); read bigger areas in slices.

Snapshots for background work

level:snapshot(min, max) takes the same copy and keeps it: reading a snapshot is safe from any thread and never sees later changes, so a worker can trace sound paths, plan routes or build a map while the game keeps ticking.

local snap = world:snapshot(lo, hi)
local info = snap:info()                 -- min, max, size_x, size_y, size_z, count, game_time
local s = snap:get(cl.pos(x, y, z))      -- 4294967295 outside the box
local rest = snap:copy(1000)             -- the states from index 1000 on (WASM: slices of 262,144)

C and C++ release snapshots (cl_snapshot_release; clight::Snapshot does it itself, copies share the data).

3. Block states and their properties

A state is a block plus its properties (facing, open, age, …). Read and change them by name, with Lua values:

local door = world:get_block(pos)
if cl.registry.state_property(door, "open") == false then
  world:set_block_property(pos, "open", true)          -- Level#setBlock with the changed state (flags default "all")
end
local ripe = cl.registry.state_with("minecraft:wheat", "age", 7)
cl.registry.state_properties(door)                     -- { "facing", "half", "hinge", "open", "powered" }
cl.registry.state_values(door, "facing")               -- { "north", "south", "west", "east" }
world:set_block(pos, "minecraft:oak_stairs[facing=east,half=top]")   -- the /setblock syntax works wherever a state goes

state_property gives nil when the block has no such property; state_with raises invalid_argument for a value the property does not have. C works with text: cl_block_state_property(state, CL_STR("open"), buf, cap, &len) writes "true", cl_block_state_with(state, CL_STR("age"), CL_STR("7"), &out). cl_block_state_to_string and cl_block_state_parse convert to and from the /setblock syntax. Mod blocks have no properties.

What a block is made of

cl.registry.block_info(state) describes a state like vanilla’s BlockStateBase:

Field Meaning
is_air, is_liquid air; holds water or lava (waterlogged blocks too)
solid, occludes, solid_render, collision_full, has_collision, replaceable the vanilla shape facts (isSolid, canOcclude, isSolidRender, a full collision cube, hasCollision, canBeReplaced)
hardness, requires_tool destroy time (-1 unbreakable), needs the right tool to drop
friction, speed_factor, jump_factor movement on it (ice, soul sand, honey)
light_emission, light_dampening 0…15
map_color, map_color_rgb the MapColor id (0 none) and its colour
sound_volume, sound_pitch of its SoundType
sound_type the SoundType as a name: "stone", "wood", "grass", "gravel", "glass", "wool", "metal", "sand", "snow", …

sound_type is what footstep, acoustics and particle mods need; cl.registry.block_sound_event(state, "step") gives the exact sound event ("minecraft:block.stone.step"). Mod blocks report their own sound_type, hardness, friction and light.

4. Light, sky and height

local l = world:light(pos)              -- { block = 0..15, sky = 0..15, combined, sky_darken }
if l.combined <= 7 then spawn_spooky_thing(pos) end
if world:can_see_sky(pos) then cl.log("outside") end
local ground = world:heightmap(x, z)    -- the y above the highest motion-blocking block ("motion_blocking")
local top = world:heightmap(x, z, "world_surface")

combined is what mobs and crops see (max(block, sky - sky_darken)); sky_darken is 0 by day and up to 11 at night and in storms. Heightmaps are vanilla’s (world_surface, ocean_floor, motion_blocking, motion_blocking_no_leaves); nil / CL_E_NOT_LOADED when the chunk is not loaded. A client only keeps world_surface and motion_blocking.

5. Containers and block entities (server)

local chest = world:container(pos)             -- nil when there is no container there
if chest then
  cl.log("%d slots", chest.size)               -- a double chest is one container of 54 slots
  chest.set(0, cl.stack.create("minecraft:diamond", 5))
  local s = chest.get(4)                       -- a copy with every data component (enchantments, names, ...)
  player:give(s)                                -- the copy keeps them
end

Slots are 0-based. Setting a slot works like Container#setItem: furnaces restart, hoppers and comparators see it, players with the container open see it at once. Chests, barrels, shulker boxes, hoppers, furnaces, dispensers, droppers, brewing stands and crafters are containers.

Block entity data is SNBT, the same as /data:

cl.log(world:block_entity_type(pos))           -- "minecraft:chest"
local data = world:block_entity(pos)            -- what /data get block shows (nil without a block entity)
world:block_entity_merge(pos, '{CustomName:"Loot"}')   -- like /data merge block

6. Block change events

level.block_changed fires for every block that changed: players, mods, pistons, fluids, explosions, growth and commands (not world generation or chunk loading). Use it to keep caches up to date instead of scanning again.

cl.on("level.block_changed", function(e)
  -- e.level, e.pos, e.old_state, e.new_state, e.client
  room_cache:invalidate(e.pos)
end)
cl.on("level.block_changed", on_ore_gone, { filter = "minecraft:air" })   -- only changes to air

On the server the event fires at the end of the level’s tick, for every change of that tick in order, so handlers may change blocks themselves (those changes follow in the same tick). On a client it fires right after the client’s copy changed (client = true), for the server’s updates and the client’s own predictions. The filter is the new state’s block. Without a subscriber it costs nothing.

7. C and C++

clight::Level lv(level);
auto hit = lv.raycast(from, to, &opts);                 // Expected<cl_raycast_hit>
auto states = lv.get_blocks(lo, hi);                    // Expected<std::vector<cl_block_state>>
clight::Snapshot snap = lv.snapshot(lo, hi);            // released when the last copy goes
auto light = lv.light({x, y, z});                       // Expected<cl_light_info>

cl.has("world.raycast"), "world.snapshots", "block.sound_type", "block.containers" and "level.block_changed" tell a mod these exist.