Server World API

Reading and placing blocks from server-side Lua, and the per-block interact and update hooks.

Weather hooks and block hooks receive a world object representing the world they run in. It lets scripts inspect and change blocks; every change is saved and broadcast to clients automatically.

All coordinates are block coordinates (integers). layer selects the block layer: 0 = background walls, 1 = foreground (the colliding layer), 2+ = overlay (fluids, effects).


Fields

FieldTypeDescription
world_namestringThe world’s name (read-only).

Reading Blocks

get_block_at(x, y, layer)

Returns a block table describing the block at that position, or an empty table if there is no block there — check for id before using the result:

local block = world:get_block_at(x, y, 1)
if block.id ~= nil then
    log_info("found " .. block.name)
end

Block table keys:

KeyTypeDescription
namestringThe block’s registered name.
idnumberThe block’s numeric ID.
facingstring"Up", "Down", "Left", or "Right".
statenumberThe block’s state value (drives state-based block textures).
biome_idnumberID of the biome this block belongs to.
block_light_levelnumberLight emitted/held from block light sources (0–255).
sky_light_levelnumberLight from the sky (0–255).
fluid_levelnumberFluid fill amount for fluid blocks.
is_surface_blockbooleanWhether this is the top-of-world surface block in its column.
block_matter_typestringThe block’s matter type (solid, liquid, …).
block_shapetable{ type = "Full" }, or { type = "Slab", divisions = n, side = "...", count = n } for partial blocks.

Placing Blocks

set_block_at_from_name(x, y, block_name)

Places a fresh block by name (e.g. "STONE"). This is the simplest and usually best way to place blocks. The layer is determined by the block type itself; the biome is inherited from the block previously at that position. Raises an error if the name is unknown.

set_block_at_from_block_id(x, y, block_id)

Same as above, but takes a numeric block ID — useful together with get_block_variation (see lua-server-globals.md).

set_block_at(x, y, block_table)

Places a fully specified block. Use this only when you need control over facing, state, fluid level, or shape — otherwise prefer the two functions above.

Required keys: id, facing, biome_id, block_light_level, sky_light_level, fluid_level, state, collidable (boolean), and block_shape.

Note: a table returned by get_block_at cannot be passed straight back into set_block_at — it lacks the required collidable key. Copy the fields you want and add collidable yourself:

local old = world:get_block_at(x, y, 1)
if old.id ~= nil then
    world:set_block_at(x, y, {
        id = old.id,
        facing = "Left",                 -- the change we wanted
        biome_id = old.biome_id,
        block_light_level = old.block_light_level,
        sky_light_level = old.sky_light_level,
        fluid_level = old.fluid_level,
        state = old.state,
        collidable = true,
        block_shape = old.block_shape,
    })
end

Damaging Entities

damage_entities_in_radius(x, y, radius, amount, damage_type, attacker_id)

Deals amount damage to every player, mob, and mount whose hitbox overlaps a circle of radius blocks centered on (x, y), and returns the number of entities hit. This is the area counterpart to entity:damage — the delivery mechanism for scripted hazards such as damaging weather, without spawning an AoE zone.

Unlike the block functions above, x, y, and radius are floating-point world positions in blocks (entity positions are not grid-aligned), not integer block coordinates.

  • damage_type is optional — "physical" (the default when omitted), "fire", or "blast" (case-insensitive); an unknown name raises a Lua error.
  • attacker_id is optional. When given, that entity is excluded from the hit and is credited with any kills (pass an attacker’s get_id()); omit it for an ownerless environmental hit.
  • Each hit routes through the target’s normal mitigation (general armor, per-type resistance, the minimum-damage floor) and is not blocked by per-attacker invincibility frames but still respects each victim’s global post-hit window. Kills (drops, on_death hooks) resolve on the same tick.
-- In a weather server-update hook: a Swelter heat pulse that burns everyone near a hot spot.
return function(world, weather_region, delta_ns)
    for _, loc in ipairs(weather_region:select_n_random_locations(3)) do
        local hit = world:damage_entities_in_radius(loc.x, loc.y, 6.0, 2.0, "fire")
        if hit > 0 then log_info("swelter scorched " .. hit .. " target(s)") end
    end
end

Reaching Players

get_players_in_radius(x, y, radius)

Returns a list (1-indexed) of entity handles for every player whose hitbox overlaps a circle of radius blocks centered on (x, y). Mobs and mounts are excluded — for those, damage them with damage_entities_in_radius above.

This is the world-level counterpart to entity:get_closest_player: where damage_entities_in_radius can only hurt an area, this hands you the players themselves, so a hook can push, buff, or check them with any entity method — apply_impulse, apply_effect, has_capability, and so on. It returns every player in range (not just the closest), so effects behave correctly in multiplayer.

As with the damage reach, x, y, and radius are floating-point world positions in blocks (entity positions are not grid-aligned), not integer block coordinates. Returns an empty list if no player is in range.

-- A gust that shoves nearby players away from a point, sparing anyone holding the ward.
return function(world, weather_region, delta_ns)
    for _, loc in ipairs(weather_region:select_n_random_locations(2)) do
        for _, player in ipairs(world:get_players_in_radius(loc.x, loc.y, 8.0)) do
            if not player:has_capability("stormward") then
                player:apply_impulse(18.0, -4.0)
            end
        end
    end
end

Block Hooks

Blocks can run server-side Lua of their own. Both hooks are declared per block entry in your block-config.md file and receive the world object as their first argument.

blocks:
  - name: ritual_stone
    # ...
    scripts:
      server-on-interact: /blocks/ritual_stone_interact.lua
      server-update: /blocks/ritual_stone_update.lua
    interact-cooldown-ms: 500            # optional, default 0

Each value is a bare path to a .lua file that returns its handler function.

scripts: keyWhen CalledSignature
server-on-interactA player interacts with a block of this typefunction(world, x, y, layer, player_entity_id)
server-updateEvery server tick, for each placed block of this typefunction(world, x, y, layer, delta_ns)
  • interact-cooldown-ms sets the minimum time between interactions on a single block cell (0 = no cooldown).
  • Update hooks only run for blocks of types that declare one, so ordinary blocks cost nothing. A block type with an update hook ticks for every placed block of that type in loaded chunks — keep the handler light, and use block state plus early returns to skip idle blocks.
  • delta_ns is the tick delta in nanoseconds (delta_ns * 1e-9 = seconds).
-- Turns the block into stone when a player interacts with it.
return function(world, x, y, layer, player_entity_id)
    world:set_block_at_from_name(x, y, "STONE")
end

See Also

Last updated