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, …).
collidablebooleanWhether the block collides with entities.
block_shapetable{ type = "Full" }, { type = "Slab", divisions = n, side = "...", count = n }, or { type = "FloorProfile", left = n, right = n } for a sloped floor (left and right are the face heights at each edge in quarter blocks, 0 to 4).

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.

local block = world:get_block_at(x, y, 1)
if block.id ~= nil then
    block.facing = "Left"
    world:set_block_at(x, y, block)
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

Time of Day

get_time_phase()

Returns the world’s current time-of-day phase as a lower-case string — one of "midnight", "dawn", "morning", "noon", "afternoon", "dusk", "evening", "night". These are the same spellings the /time command and every time-of-day config field use.

is_night()

Returns true while it is night in this world (the dusk → midnight half of the cycle), false during the day half (dawn → afternoon). The complement of the day/night split that rendering and mob spawning agree on.

if world:is_night() then
    -- ... something that only happens after dark
end

Resting

try_rest(player_entity_id)

Rests a player to dawn at a bed. The engine applies every gate itself, so a hook only has to relay the result:

  • Night only — refused during the day (a daytime rest would fast-forward almost a whole cycle).
  • No monsters nearby — refused while any contact-damaging mob is within the server’s rest: hostile-radius: (default 20 blocks) of the resting player.
  • Co-op consensus — the world clock only advances once every present player has rested; each rest is a short-lived “vote”, so in multiplayer everyone must bed down within a few seconds of each other. Singleplayer advances immediately.

On success it jumps the world clock to the top of morning — full daylight — and broadcasts the change to every client. (Morning, not the top of dawn: at the very top of dawn the sun is still on the horizon, so it looks no brighter than night; morning is the first instant the sun has fully risen.) Returns status, resting, present:

  • status — one of "advanced" (the world moved to dawn), "waiting" (your rest is recorded; still waiting on other players), "blocked_hostile", "blocked_day", or "unavailable" (an internal error; the player was not found).
  • resting, present — the current rest-vote count and the number of players present, for a “2 / 3 resting” style message.
-- A bed's server-on-interact hook. The world is the fourth argument.
return function(entity, player_entity_id, player, world)
    local loc = entity:get_location()
    player:set_custom_spawn(loc.x, loc.y)

    local status, resting, present = world:try_rest(player_entity_id)
    if status == "advanced" then
        player:send_message("You rest until dawn.")
    elseif status == "waiting" then
        player:send_message("Resting… (" .. resting .. "/" .. present .. ")")
    elseif status == "blocked_hostile" then
        player:send_message("You cannot rest — monsters are near.")
    elseif status == "blocked_day" then
        player:send_message("You can only rest at night.")
    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