Skip to content

server.world.light #

Module light is a server-side light-LEVEL engine (block light + sky light).

IMPORTANT - Bedrock rendering light is CLIENT-side. In Minecraft: Bedrock Edition the client computes block/sky light for rendering itself, and the server does NOT send light in the chunk packet (see server/world/chunk.v serialize() - no light data, deliberately). So this engine is NOT needed for the client to render light.

Its only purpose is future GAMEPLAY logic that has to know the light level at a position - mob-spawn eligibility, crop/tree growth, phantom spawning, etc. Vedrock has no such consumer yet, so this module is self-contained, unit-tested infrastructure that a gameplay system can query later. It is deliberately NOT wired into the chunk network packet.

The propagation model mirrors the inspiration engines (dragonfly server/world/chunk/light*.go and PocketMine-MP src/world/light/): light spreads by a breadth-first flood fill, losing 1 level per block travelled and being attenuated/blocked by the opacity of the block it enters.

Constants #

const max_light = u8(15)

max_light is the brightest a light level can be, matching vanilla's 0-15.

const max_volume = 128 * 128 * 128

max_volume caps how many blocks a single light computation may touch so a huge region query cannot allocate unbounded memory. 128^3 = ~2M blocks, one u8 per block per light type, so a full block+sky computation over the cap costs about 4 MB of light storage plus the source grid. Callers that need more must tile.

const air = 0

Known block ids. This is a LOCAL, self-contained table - it does not touch the Block interface or server/block/*.v. The ids are arbitrary but stable within this module; a real BlockSource maps its own blocks onto these. It is a starting set meant to be extended as gameplay needs more blocks.

const stone = 1 // stand-in for any fully opaque, non-emitting block
const water = 2 // transparent but attenuates light beyond the normal falloff
const leaves = 3 // same idea as water - diffuses without fully blocking
const glowstone = 100
const sea_lantern = 101
const lava = 102
const torch = 103
const jack_o_lantern = 104
const redstone_torch = 105
const beacon = 106

fn compute #

fn compute(region Region, src BlockSource) ?&LightGrid

compute runs a full block-light and sky-light computation over the region, reading blocks from src. It returns none when the region exceeds max_volume so a bad query can't blow up memory - tile the region and merge instead.

fn emission #

fn emission(block_id int) u8

emission returns the block-light level a block id emits, 0-15. Only the known emitters glow - everything else is dark. Extend this table as needed.

fn filter #

fn filter(block_id int) u8

filter returns how many extra light levels a block id removes from light that passes THROUGH it, on top of the base 1-per-block falloff. 0 means fully transparent (light only loses the normal 1 level), max_light means fully opaque (light cannot pass at all). Water and leaves diffuse light - they let it through but eat a couple of extra levels. Unknown non-air ids are treated as opaque so the engine errs on the side of blocking, like a solid block.

fn new_region #

fn new_region(x1 int, y1 int, z1 int, x2 int, y2 int, z2 int) Region

new_region normalizes the two corners so callers need not order them.

fn opaque #

fn opaque(block_id int) bool

opaque reports whether a block fully blocks light (a filter of max_light). Sky light stops descending at 15 the moment it hits an opaque block.

fn LightKind.from #

fn LightKind.from[W](input W) !LightKind

interface BlockSource #

interface BlockSource {
	get_block(x int, y int, z int) int
}

BlockSource reads abstract block ids from a world by absolute coordinates. The engine talks only to this interface so it never imports session/world/ block internals and stays unit-testable against an in-memory grid. The ids it returns are looked up in this module's local emission/opacity table - see air, glowstone and friends below. Hub (or a chunk view) can satisfy this later by mapping its own block ids onto these constants.

enum LightKind #

enum LightKind {
	block
	sky
}

LightKind selects which of the two light channels an operation acts on.

struct LightGrid #

struct LightGrid {
pub:
	region Region
mut:
	block_light []u8
	sky_light   []u8
}

LightGrid holds the computed light levels for one Region. It carries both the block-light and sky-light channels so a single computation fills both. Levels are queried by absolute coordinate through light_at; a position outside the region reads as 0.

fn (LightGrid) light_at #

fn (g &LightGrid) light_at(kind LightKind, x int, y int, z int) u8

light_at returns the light level of the given kind at an absolute coordinate, or 0 when the position lies outside the computed region.

fn (LightGrid) block_light_at #

fn (g &LightGrid) block_light_at(x int, y int, z int) u8

block_light_at and sky_light_at are convenience wrappers over light_at.

fn (LightGrid) sky_light_at #

fn (g &LightGrid) sky_light_at(x int, y int, z int) u8

fn (LightGrid) add_light #

fn (mut g LightGrid) add_light(src BlockSource, x int, y int, z int)

add_light re-floods block light from a single position after an emitter is placed there. src must already report the new block. Only additive - use remove_light first if a brighter emitter was replaced by a dimmer one.

fn (LightGrid) remove_light #

fn (mut g LightGrid) remove_light(src BlockSource, x int, y int, z int)

remove_light clears block light originating from an emitter that was removed at the given position and re-propagates any surrounding light back into the hole. This is the classic two-pass BFS removal (dragonfly/PocketMine): first tear down every cell that was lit by the removed source, collecting brighter edge cells, then flood those edges back in. src must already report the block AFTER removal.

struct Region #

struct Region {
pub:
	min_x int
	min_y int
	min_z int
	max_x int
	max_y int
	max_z int
}

Region is the axis-aligned box the engine computes light for. Light is only stored and propagated inside the box; anything outside is treated as unlit and non-blocking so an emitter near the edge simply loses its light at the border.

fn (Region) width #

fn (r Region) width() int

width, height, depth are the span of the region on each axis, inclusive.

fn (Region) height #

fn (r Region) height() int

fn (Region) depth #

fn (r Region) depth() int

fn (Region) volume #

fn (r Region) volume() i64

volume is the number of blocks the region covers, both corners inclusive. Computed in i64 so a huge region can't overflow int32 and slip past the cap.

fn (Region) contains #

fn (r Region) contains(x int, y int, z int) bool

contains reports whether an absolute coordinate falls inside the region.