
# `world`

Two globals hold everything live in the game:

| Global | Is |
|--------|-----|
| `player` | Your player as an [entity handle](entity.md), or `nil` outside a world |
| `world` | The loaded world handle, or `nil` outside a world |

Both re-resolve on every access, so a respawn or dimension change already shows in the next
read. Guard for `nil` on the title screen:

```lua
module:event("tick", function()
    if not player then return end
    if player:health() < 8 then
        hud.notify("Low HP", ("%.1f left"):format(player:health()), "error")
    end
end)
```

Handles are live: every colon-call re-reads the game, so one kept from last tick answers with
this tick's state. The exception is [text](text.md), which always comes as a snapshot.

| Handle | Comes from |
|--------|------------|
| [Entity](entity.md) | `player`, `world:players()`, `world:entities()`, `world:entity(id)` |
| [Item](item.md) | `e:held_item()`, `e:equipped(slot)`, `inventory.item`, `game.item` |
| [Block state](blocks.md) | `world:block_state(pos)`, a block hit's `state` |
| [Raycast hit](blocks.md#raycast-hits) | `world:raycast(...)`, `world:raycast_entity(...)`, `player:raycast()` |
| [Chunk](#chunks) | `world:chunk(x, z)`, `world:loaded_chunks()` |

Positions are [vec3](vec3.md) everywhere. Data that never changes at runtime (items,
translations) lives on [`game`](game.md).

## Entities

| Method | Returns |
|--------|---------|
| `world:players()` | Every player as an [entity handle](entity.md), yourself included |
| `world:player(name)` | One player by name, case-insensitive, or `nil` |
| `world:entities(type?)` | Every entity, optionally filtered by exact type id (`"minecraft:item"`) |
| `world:entity(id)` | Entity by numeric id, or `nil` |

```lua
for _, p in ipairs(world:players()) do
    if not p:is_self() and p:distance() < 8 then
        print(p:name() .. " is close")
    end
end
```

## Blocks

| Method | Returns |
|--------|---------|
| `world:block(pos)` | Block id at a [vec3](vec3.md), e.g. `"minecraft:air"` |
| `world:block_state(pos)` | Full [block state handle](blocks.md) |
| `world:find_blocks(ids, center, radius)` | Positions of matching blocks in a cube around `center` |
| `world:sign_text(pos)` | A sign's four front lines, four back lines and waxed flag, or `nil` |
| `world:top_y(x, z[, heightmap])` | Surface y of a column |

Heightmaps: `"motion_blocking"` (default) · `"motion_blocking_no_leaves"` · `"world_surface"`
· `"ocean_floor"`.

`find_blocks` takes one block id or a list, scans `radius` blocks out along each axis
(capped at `64`) and returns the matches as [vec3](vec3.md) block positions:

```lua
local chests = world:find_blocks({ "minecraft:chest", "minecraft:trapped_chest" },
    player:position(), 6)
print(#chests .. " chests nearby")
```

## Raycasts

| Method | Returns |
|--------|---------|
| `world:raycast(from, to[, fluids[, shape]])` | First [block hit](blocks.md#raycast-hits) between two points, or `nil` |
| `world:raycast_entity(from, to)` | Closest [entity hit](blocks.md#raycast-hits), or `nil`. Skips spectators and yourself |

`fluids = true` also hits fluids. `shape` picks the collision shape to test: `"outline"`
(default, what the crosshair uses) · `"collider"` (physics shape) · `"visual"`.

Both need a local player and return `nil` without one. For the crosshair ray use
`player:raycast(max_distance?)`, which tests blocks and entities at once.

## Environment

| Method | Returns |
|--------|---------|
| `world:time()` | Time of day in ticks |
| `world:dimension()` | Dimension id, e.g. `"minecraft:overworld"` |
| `world:raining()`, `world:thundering()` | Weather flags |
| `world:rain_gradient()`, `world:thunder_gradient()` | Weather strength, `0..1` |
| `world:biome(pos)` | Biome id, e.g. `"minecraft:plains"` |
| `world:light(pos)` | Block light and sky light, `0–15`, as two returns |

```lua
local pos = player:position()
print(world:biome(pos), world:light(pos))
if world:raining() then
    print("rain strength", world:rain_gradient())
end
```

## Chunks

| Method | Returns |
|--------|---------|
| `world:chunk(x, z)` | Loaded chunk by chunk coordinates, or `nil` |
| `world:is_chunk_loaded(x, z)` | Whether that chunk is loaded |
| `world:loaded_chunks()` | Every loaded chunk |
| `world:min_section()` | Section y of the lowest section, `-4` in the overworld |
| `world:section_count()` | Sections per chunk column, `24` in the overworld |

Chunk and section coordinates are block coordinates divided by 16, rounded down. A chunk
handle has `x()`, `z()` and `section(y)`, which takes a section y from `world:min_section()`
to `world:min_section() + world:section_count() - 1` and returns `nil` outside it.

A section is a snapshot: the 16×16×16 blocks copied at the `section(y)` call.

| Method | Returns |
|--------|---------|
| `s:y()` | Section y; its blocks run from `y * 16` to `y * 16 + 15` |
| `s:empty()` | `true` when every block is air |
| `s:palette()` | The stored palette as `{id, properties, count}` entries |
| `s:blocks()` | 4096 palette indices; local block `x, y, z` sits at `y * 256 + z * 16 + x + 1` |

The palette is the one the server sent, so it can hold states no block uses (`count` is `0`).
A section whose palette overflowed (over 256 states) lists only the states in use. Reading the
palette is cheap; check it before walking the blocks:

```lua
local function full_hives(chunk)
    local found = {}
    local bottom = world:min_section()
    for sy = bottom, bottom + world:section_count() - 1 do
        local section = chunk:section(sy)
        local wanted = {}
        for i, entry in ipairs(section:palette()) do
            if entry.count > 0 and entry.properties.honey_level == "5" then wanted[i] = true end
        end
        if next(wanted) then
            local blocks = section:blocks()
            for i = 0, 4095 do
                if wanted[blocks[i + 1]] then
                    found[#found + 1] = vec3(chunk:x() * 16 + i % 16, sy * 16 + math.floor(i / 256),
                        chunk:z() * 16 + math.floor(i / 16) % 16)
                end
            end
        end
    end
    return found
end

module:event("chunk_load", function(e)
    local chunk = world:chunk(e.x, e.z)
    if chunk then
        for _, pos in ipairs(full_hives(chunk)) do print(tostring(pos)) end
    end
end)
```

Light sections stretch one past each end of the column, so bit `i` of a light mask from the
[`light_update` and `level_chunk_with_light`](packets.md#light) packets is section
`world:min_section() - 1 + i`.
