Skip to content

server.liquid

Constants #

const max_cells_per_tick = 4096

max_cells_per_tick caps how many pending cells the manager drains in a single tick so a large flood can never stall the actor thread. Cells left over stay queued and are processed on later ticks - the flow just spreads a bit slower under heavy load instead of blocking the whole server. 4096 is generous: a flat pool spreading 7 cells in every direction is well under this.

const source_depth = 8

Water depth model, adapted from dragonfly and PocketMine.

depth runs 1..8 internally: 8 is a source, 1..7 are flowing levels where a higher number is a fuller (taller) cell, and a falling cell behaves like a full column. Each horizontal step loses spread_decay levels, so a source reaches at most 7 cells away before the flow is exhausted.

Bedrock encodes this in the liquid_depth block state as liquid_depth = 8 - depth, with +8 added when the cell is falling. A source is minecraft:water (still); anything flowing is minecraft:flowing_water.

const max_flow_depth = 7
const spread_decay = 1
const min_adjacent_sources = 2

fn new_falling #

fn new_falling() WaterState

new_falling is a falling column - it appears full but spreads like flowing.

fn new_flowing #

fn new_flowing(depth int) WaterState

new_flowing builds a flowing cell at the given depth.

fn new_manager #

fn new_manager(host Host) &LiquidManager

new_manager builds a LiquidManager bound to host and precomputes the water id table (all source, flowing and falling states) once.

fn new_source #

fn new_source() WaterState

new_source is the full still water source.

interface BlockSink #

interface BlockSink {
mut:
	set_block_id(id int, x int, y int, z int)
}

BlockSink writes a block by network id and broadcasts the change to viewers. Every liquid write routes through this so a spread looks like a normal edit.

interface BlockSource #

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

BlockSource reads block network ids from a world by absolute coordinates. Hub satisfies it structurally; tests use an in-memory grid.

interface Host #

interface Host {
	BlockSource
	BlockSink
}

Host is what the LiquidManager needs from its world: read and write blocks. Hub satisfies it via get_block/set_block_id, so this module never imports session and stays unit-testable against a fake grid.

struct LiquidManager #

@[heap]
struct LiquidManager {
mut:
	host    Host
	pending map[string]Pos
	// water_states maps every known water network id to its resolved cell so the
	// manager can tell water apart from other blocks when it reads the world.
	water_states map[int]WaterState
	air_id       int
}

LiquidManager owns the set of block positions that still need a liquid update and processes them on the actor thread each tick. It holds no world of its own - the Host it is handed reads and writes blocks.

fn (LiquidManager) place_source #

fn (mut m LiquidManager) place_source(x int, y int, z int)

place_source sets a water source at the position and queues it for spreading.

fn (LiquidManager) enqueue #

fn (mut m LiquidManager) enqueue(x int, y int, z int)

enqueue marks a cell as needing a liquid update on a later tick.

fn (LiquidManager) on_block_changed #

fn (mut m LiquidManager) on_block_changed(x int, y int, z int)

on_block_changed re-evaluates a cell and its neighbours after a block edit - placing water starts its flow, breaking a block frees a path for adjacent water to spread into. Only water cells actually do anything when processed.

fn (LiquidManager) pending_count #

fn (m &LiquidManager) pending_count() int

pending_count is the number of cells still queued. Used by tests.

fn (LiquidManager) tick #

fn (mut m LiquidManager) tick()

tick drains up to max_cells_per_tick queued cells and processes each. Cells a processed cell touches (neighbours it fills, or itself when it changes) are re-queued for the next tick, so the flow advances one ring per tick like vanilla. Runs on the actor thread from TickJob.

struct WaterState #

struct WaterState {
pub:
	depth   int
	falling bool
}

WaterState is a resolved water cell: its internal depth and whether it is falling. It knows how to encode itself into a Bedrock network id.

fn (WaterState) is_source #

fn (w WaterState) is_source() bool

is_source reports whether this cell is a full still source.

fn (WaterState) network_id #

fn (w WaterState) network_id() int

network_id resolves this water cell to its Bedrock runtime id. Source cells use minecraft:water, flowing/falling cells use minecraft:flowing_water.