diff --git a/server/block/liquid.v b/server/block/liquid.v new file mode 100644 index 0000000..e8bb5a2 --- /dev/null +++ b/server/block/liquid.v @@ -0,0 +1,161 @@ +module block + +import server.world + +// 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 the liquid's spread decay, so water +// reaches at most 7 cells from its source and overworld lava only 3. +// +// 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 +// or minecraft:lava (still); anything flowing is the flowing_ form. +pub const source_depth = 8 +pub const max_flow_depth = 7 +pub const spread_decay = 1 +pub const lava_spread_decay = 2 +pub const min_adjacent_sources = 2 + +// LiquidKind is which fluid a cell holds. The two behave the same way apart +// from how far they reach, how fast they move and what they do to each other. +pub enum LiquidKind { + water + lava +} + +// LiquidState is a resolved liquid cell: which fluid, its internal depth and +// whether it is falling. It knows how to encode itself into a Bedrock network +// id. +pub struct LiquidState { +pub: + kind LiquidKind + depth int + falling bool +} + +// is_source reports whether this cell is a full still source. +pub fn (l LiquidState) is_source() bool { + return l.depth == source_depth && !l.falling +} + +// spread_decay is how much depth this liquid loses per horizontal step. +pub fn (l LiquidState) spread_decay() int { + return if l.kind == .lava { lava_spread_decay } else { spread_decay } +} + +// liquid_depth_value returns the Bedrock liquid_depth state value for this cell. +fn (l LiquidState) liquid_depth_value() int { + mut v := source_depth - l.depth + if l.falling { + v += 8 + } + return v +} + +fn (l LiquidState) block_name() string { + return match l.kind { + .water { + if l.is_source() { + 'minecraft:water' + } else { + 'minecraft:flowing_water' + } + } + .lava { + if l.is_source() { + 'minecraft:lava' + } else { + 'minecraft:flowing_lava' + } + } + } +} + +// network_id resolves this liquid cell to its Bedrock runtime id. +pub fn (l LiquidState) network_id() int { + return world.new_block_with_states(l.block_name(), [ + world.BlockState{ + key: 'liquid_depth' + kind: world.state_kind_int + int_value: l.liquid_depth_value() + }, + ]).network_id +} + +// new_source is the full still water source. +pub fn new_source() LiquidState { + return new_liquid_source(.water) +} + +// new_flowing builds a flowing water cell at the given depth. +pub fn new_flowing(depth int) LiquidState { + return new_liquid_flowing(.water, depth) +} + +// new_falling is a falling water column - it appears full but spreads like +// flowing. +pub fn new_falling() LiquidState { + return new_liquid_falling(.water) +} + +pub fn new_liquid_source(kind LiquidKind) LiquidState { + return LiquidState{ + kind: kind + depth: source_depth + falling: false + } +} + +pub fn new_liquid_flowing(kind LiquidKind, depth int) LiquidState { + return LiquidState{ + kind: kind + depth: depth + falling: false + } +} + +pub fn new_liquid_falling(kind LiquidKind) LiquidState { + return LiquidState{ + kind: kind + depth: source_depth + falling: true + } +} + +// liquid_states maps every liquid network id to the cell it stands for. It is +// built once so both the spread engine and the gameplay code that only wants +// to ask "is this lava?" read the same table. +pub const liquid_states = build_liquid_states() + +fn build_liquid_states() map[int]LiquidState { + mut states := map[int]LiquidState{} + for kind in [LiquidKind.water, .lava] { + source := new_liquid_source(kind) + states[source.network_id()] = source + falling := new_liquid_falling(kind) + states[falling.network_id()] = falling + for depth := 1; depth <= max_flow_depth; depth++ { + flowing := new_liquid_flowing(kind, depth) + states[flowing.network_id()] = flowing + } + } + return states +} + +// liquid_at_id returns the liquid cell a network id stands for, or none when +// the id is not a liquid at all. +pub fn liquid_at_id(id int) ?LiquidState { + return liquid_states[id] or { return none } +} + +// is_lava_id and is_water_id answer what a block id is without the caller +// having to know how many states each fluid has. +pub fn is_lava_id(id int) bool { + state := liquid_at_id(id) or { return false } + return state.kind == .lava +} + +pub fn is_water_id(id int) bool { + state := liquid_at_id(id) or { return false } + return state.kind == .water +} diff --git a/server/block/liquid_manager.v b/server/block/liquid_manager.v index a5baeb3..848d721 100644 --- a/server/block/liquid_manager.v +++ b/server/block/liquid_manager.v @@ -38,6 +38,10 @@ struct Pos { z int } +// lava_tick_divisor is how many liquid ticks pass between two steps of a lava +// cell. Lava crawls where water runs, and this is the whole difference. +pub const lava_tick_divisor = 6 + // 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. @@ -46,28 +50,17 @@ pub 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 + air_id int + // ticks counts the liquid ticks this manager has run, so lava can be held + // back to its own slower rate without a second queue. + ticks i64 } -// new_manager builds a LiquidManager bound to host and precomputes the water id -// table (all source, flowing and falling states) once. +// new_manager builds a LiquidManager bound to host. pub fn new_manager(host Host) &LiquidManager { - mut states := map[int]WaterState{} - src := new_source() - states[src.network_id()] = src - fall := new_falling() - states[fall.network_id()] = fall - for depth := 1; depth <= max_flow_depth; depth++ { - w := new_flowing(depth) - states[w.network_id()] = w - } return &LiquidManager{ - host: host - water_states: states - air_id: world.air.network_id + host: host + air_id: world.air.network_id } } @@ -77,7 +70,13 @@ fn key(x int, y int, z int) string { // place_source sets a water source at the position and queues it for spreading. pub fn (mut m LiquidManager) place_source(x int, y int, z int) { - m.host.set_block_id(new_source().network_id(), x, y, z) + m.place_liquid_source(.water, x, y, z) +} + +// place_liquid_source sets a source of the given fluid and queues it for +// spreading. +pub fn (mut m LiquidManager) place_liquid_source(kind LiquidKind, x int, y int, z int) { + m.host.set_block_id(new_liquid_source(kind).network_id(), x, y, z) m.enqueue(x, y, z) } @@ -99,37 +98,48 @@ pub fn (m &LiquidManager) pending_count() int { return m.pending.len } -// water_at returns the resolved water cell at a position, or none if the block -// there is not water. -fn (mut m LiquidManager) water_at(x int, y int, z int) ?WaterState { - id := m.host.get_block(x, y, z) - return m.water_states[id] or { return none } +// liquid_at returns the resolved liquid cell at a position, or none if the +// block there is not a liquid. +fn (mut m LiquidManager) liquid_at(x int, y int, z int) ?LiquidState { + return liquid_at_id(m.host.get_block(x, y, z)) +} + +// liquid_of_kind_at returns the cell at a position only when it holds the +// fluid asked for. +fn (mut m LiquidManager) liquid_of_kind_at(kind LiquidKind, x int, y int, z int) ?LiquidState { + state := m.liquid_at(x, y, z) or { return none } + if state.kind != kind { + return none + } + return state } -// is_water reports whether the block at a position is any water cell. -fn (mut m LiquidManager) is_water(x int, y int, z int) bool { - if _ := m.water_at(x, y, z) { +// is_liquid_of_kind reports whether the block at a position is that fluid. +fn (mut m LiquidManager) is_liquid_of_kind(kind LiquidKind, x int, y int, z int) bool { + if _ := m.liquid_of_kind_at(kind, x, y, z) { return true } return false } -// can_flow_into reports whether water may spread into the cell. For now only -// air and existing water are replaceable - solid terrain blocks the flow. +// can_flow_into reports whether a liquid may spread into the cell. Air and any +// liquid are replaceable - solid terrain blocks the flow. fn (mut m LiquidManager) can_flow_into(x int, y int, z int) bool { id := m.host.get_block(x, y, z) if id == m.air_id { return true } - return id in m.water_states + return id in liquid_states } // 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 call, so the flow advances one ring per call. The // world runtime only calls this every few ticks, which is what sets the vanilla -// flow rate. Runs on the owning world's actor thread. +// flow rate. Lava is held back further, to its own slower rate. Runs on the +// owning world's actor thread. pub fn (mut m LiquidManager) tick() { + m.ticks++ if m.pending.len == 0 { return } @@ -143,7 +153,14 @@ pub fn (mut m LiquidManager) tick() { for p in batch { m.pending.delete(key(p.x, p.y, p.z)) } + lava_moves := m.ticks % lava_tick_divisor == 0 for p in batch { + if !lava_moves && m.is_liquid_of_kind(.lava, p.x, p.y, p.z) { + // Hold the cell over rather than dropping it: lava still has to + // move, just not on this tick. + m.enqueue(p.x, p.y, p.z) + continue + } m.process(p.x, p.y, p.z) } } @@ -151,83 +168,129 @@ pub fn (mut m LiquidManager) tick() { // process runs one cell's flow step: dry up if unfed, fall straight down, then // spread outwards with decayed depth. fn (mut m LiquidManager) process(x int, y int, z int) { - cur := m.water_at(x, y, z) or { return } + cur := m.liquid_at(x, y, z) or { return } + if m.solidified_by_contact(cur, x, y, z) { + return + } // A flowing cell with no feeding source nearby dries up toward air. if !cur.is_source() && !m.source_around(cur, x, y, z) { + decay := cur.spread_decay() mut next_depth := 0 - if cur.depth - 2 * spread_decay > 0 { - next_depth = cur.depth - 2 * spread_decay + if cur.depth - 2 * decay > 0 { + next_depth = cur.depth - 2 * decay } if next_depth <= 0 { m.set_air(x, y, z) } else { - m.set_water(new_flowing(next_depth), x, y, z) + m.set_liquid(new_liquid_flowing(cur.kind, next_depth), x, y, z) m.enqueue_neighbours(x, y, z) } return } - // Falling: pour straight down into air/water below. + // Falling: pour straight down into air/liquid below. below_falls := m.can_flow_into(x, y - 1, z) if below_falls { - m.flow_into(new_falling(), x, y - 1, z) + m.flow_into(new_liquid_falling(cur.kind), x, y - 1, z) } // Once resting on ground (or a source), spread outwards with decayed depth. if cur.is_source() || !below_falls { - spread_depth := cur.depth - spread_decay + spread_depth := cur.depth - cur.spread_decay() if spread_depth <= 0 { return } - m.spread_outwards(new_flowing(spread_depth), x, y, z) + m.spread_outwards(new_liquid_flowing(cur.kind, spread_depth), x, y, z) } } -// spread_outwards flows the decayed water into each horizontal neighbour. -fn (mut m LiquidManager) spread_outwards(w WaterState, x int, y int, z int) { - m.flow_into(w, x + 1, y, z) - m.flow_into(w, x - 1, y, z) - m.flow_into(w, x, y, z + 1) - m.flow_into(w, x, y, z - 1) +// solidified_by_contact turns a lava cell that has met water into the rock the +// contact makes, and reports whether it did. A lava source becomes obsidian, a +// flowing one cobblestone - the same two outcomes as in game, decided by which +// of the two the lava was. +fn (mut m LiquidManager) solidified_by_contact(cur LiquidState, x int, y int, z int) bool { + if cur.kind != .lava || !m.water_touching(x, y, z) { + return false + } + solid := if cur.is_source() { world.obsidian } else { world.cobblestone } + m.host.set_block_id(solid.network_id, x, y, z) + m.enqueue_neighbours(x, y, z) + return true +} + +// water_touching reports whether any face neighbour of a cell holds water. +fn (mut m LiquidManager) water_touching(x int, y int, z int) bool { + return m.is_liquid_of_kind(.water, x + 1, y, z) || m.is_liquid_of_kind(.water, x - 1, y, z) + || m.is_liquid_of_kind(.water, x, y, z + 1) || m.is_liquid_of_kind(.water, x, y, z - 1) + || m.is_liquid_of_kind(.water, x, y + 1, z) || m.is_liquid_of_kind(.water, x, y - 1, z) } -// flow_into writes w into the target cell if it may flow there and the target -// isn't already an equal-or-fuller water cell, then queues the target. -fn (mut m LiquidManager) flow_into(w WaterState, x int, y int, z int) { +// spread_outwards flows the decayed liquid into each horizontal neighbour. +fn (mut m LiquidManager) spread_outwards(l LiquidState, x int, y int, z int) { + m.flow_into(l, x + 1, y, z) + m.flow_into(l, x - 1, y, z) + m.flow_into(l, x, y, z + 1) + m.flow_into(l, x, y, z - 1) +} + +// flow_into writes l into the target cell if it may flow there and the target +// isn't already an equal-or-fuller cell of the same fluid, then queues the +// target. Flowing into the opposite fluid makes rock instead. +fn (mut m LiquidManager) flow_into(l LiquidState, x int, y int, z int) { if !m.can_flow_into(x, y, z) { return } - if existing := m.water_at(x, y, z) { + if existing := m.liquid_at(x, y, z) { + if existing.kind != l.kind { + m.mix_into(l, existing, x, y, z) + return + } // Don't overwrite an equal or fuller cell and never demote a source or // a falling column if existing.is_source() { return } - if existing.falling && !w.falling { + if existing.falling && !l.falling { return } - if !w.falling && existing.depth >= w.depth && !existing.falling { + if !l.falling && existing.depth >= l.depth && !existing.falling { return } } - m.set_water(w, x, y, z) + m.set_liquid(l, x, y, z) m.enqueue(x, y, z) } -// source_around reports whether a horizontally adjacent or overhead water cell -// feeds this one. A cell fed from above (falling) or by a fuller neighbour stays -// wet; otherwise it dries up. Also counts adjacent sources for source-forming. -fn (mut m LiquidManager) source_around(cur WaterState, x int, y int, z int) bool { - // Water directly above feeds this cell (it is falling into it). - if m.is_water(x, y + 1, z) { +// mix_into resolves one fluid arriving where the other already is. Lava +// arriving in water sets into stone; water arriving in lava makes obsidian of +// a source and cobblestone of anything flowing. +fn (mut m LiquidManager) mix_into(arriving LiquidState, existing LiquidState, x int, y int, z int) { + solid := if arriving.kind == .lava { + world.stone + } else if existing.is_source() { + world.obsidian + } else { + world.cobblestone + } + m.host.set_block_id(solid.network_id, x, y, z) + m.enqueue_neighbours(x, y, z) +} + +// source_around reports whether a horizontally adjacent or overhead cell of the +// same fluid feeds this one. A cell fed from above (falling) or by a fuller +// neighbour stays wet; otherwise it dries up. Also counts adjacent sources for +// source-forming, which only water does. +fn (mut m LiquidManager) source_around(cur LiquidState, x int, y int, z int) bool { + // Liquid directly above feeds this cell (it is falling into it). + if m.is_liquid_of_kind(cur.kind, x, y + 1, z) { return true } mut adjacent_sources := 0 mut fed := false offsets := [[1, 0], [-1, 0], [0, 1], [0, -1]] for o in offsets { - side := m.water_at(x + o[0], y, z + o[1]) or { continue } + side := m.liquid_of_kind_at(cur.kind, x + o[0], y, z + o[1]) or { continue } if side.is_source() { adjacent_sources++ } @@ -235,9 +298,11 @@ fn (mut m LiquidManager) source_around(cur WaterState, x int, y int, z int) bool fed = true } } - // Two adjacent sources form a new source here if there is solid ground below. - if adjacent_sources >= min_adjacent_sources && !m.can_flow_into(x, y - 1, z) { - m.set_water(new_source(), x, y, z) + // Two adjacent water sources form a new source here if there is solid + // ground below. Lava never does this. + if cur.kind == .water && adjacent_sources >= min_adjacent_sources + && !m.can_flow_into(x, y - 1, z) { + m.set_liquid(new_liquid_source(cur.kind), x, y, z) m.enqueue_neighbours(x, y, z) return true } @@ -254,8 +319,8 @@ fn (mut m LiquidManager) enqueue_neighbours(x int, y int, z int) { m.enqueue(x, y - 1, z) } -fn (mut m LiquidManager) set_water(w WaterState, x int, y int, z int) { - m.host.set_block_id(w.network_id(), x, y, z) +fn (mut m LiquidManager) set_liquid(l LiquidState, x int, y int, z int) { + m.host.set_block_id(l.network_id(), x, y, z) } fn (mut m LiquidManager) set_air(x int, y int, z int) { diff --git a/server/block/liquid_manager_test.v b/server/block/liquid_manager_test.v index b25575a..79fec7d 100644 --- a/server/block/liquid_manager_test.v +++ b/server/block/liquid_manager_test.v @@ -34,7 +34,7 @@ fn run_ticks(mut m LiquidManager, n int) { // water_depth returns the internal depth at a cell, or 0 if it isn't water. fn depth_at(mut m LiquidManager, x int, y int, z int) int { - w := m.water_at(x, y, z) or { return 0 } + w := m.liquid_of_kind_at(.water, x, y, z) or { return 0 } return w.depth } @@ -50,11 +50,11 @@ fn test_source_spreads_to_horizontal_neighbours() { m.place_source(0, 0, 0) run_ticks(mut m, 20) - assert m.is_water(0, 0, 0) - assert m.is_water(1, 0, 0) - assert m.is_water(-1, 0, 0) - assert m.is_water(0, 0, 1) - assert m.is_water(0, 0, -1) + assert m.is_liquid_of_kind(.water, 0, 0, 0) + assert m.is_liquid_of_kind(.water, 1, 0, 0) + assert m.is_liquid_of_kind(.water, -1, 0, 0) + assert m.is_liquid_of_kind(.water, 0, 0, 1) + assert m.is_liquid_of_kind(.water, 0, 0, -1) } fn test_flowing_depth_decays_with_distance() { @@ -84,9 +84,9 @@ fn test_water_flows_down() { m.place_source(0, 5, 0) run_ticks(mut m, 20) - assert m.is_water(0, 4, 0) - assert m.is_water(0, 3, 0) - below := m.water_at(0, 4, 0) or { WaterState{} } + assert m.is_liquid_of_kind(.water, 0, 4, 0) + assert m.is_liquid_of_kind(.water, 0, 3, 0) + below := m.liquid_of_kind_at(.water, 0, 4, 0) or { LiquidState{} } assert below.falling } @@ -100,16 +100,16 @@ fn test_flowing_dries_up_when_source_removed() { mut m := new_manager(wld) m.place_source(0, 0, 0) run_ticks(mut m, 30) - assert m.is_water(1, 0, 0) + assert m.is_liquid_of_kind(.water, 1, 0, 0) // Remove the source and re-notify the region. wld.set_block_id(world.air.network_id, 0, 0, 0) m.on_block_changed(0, 0, 0) run_ticks(mut m, 60) - assert !m.is_water(1, 0, 0) - assert !m.is_water(2, 0, 0) - assert !m.is_water(0, 0, 0) + assert !m.is_liquid_of_kind(.water, 1, 0, 0) + assert !m.is_liquid_of_kind(.water, 2, 0, 0) + assert !m.is_liquid_of_kind(.water, 0, 0, 0) } fn test_two_sources_form_new_source() { @@ -126,7 +126,7 @@ fn test_two_sources_form_new_source() { m.place_source(2, 0, 0) run_ticks(mut m, 40) - mid := m.water_at(1, 0, 0) or { WaterState{} } + mid := m.liquid_of_kind_at(.water, 1, 0, 0) or { LiquidState{} } assert mid.is_source() } @@ -147,11 +147,121 @@ fn test_per_tick_cap_is_respected() { fn test_falling_column_survives_weaker_horizontal_spread() { mut wld := &FakeWorld{} mut m := new_manager(wld) - m.set_water(new_falling(), 0, 0, 0) + m.set_liquid(new_falling(), 0, 0, 0) m.flow_into(new_flowing(3), 0, 0, 0) - after := m.water_at(0, 0, 0) or { panic('expected water') } + after := m.liquid_of_kind_at(.water, 0, 0, 0) or { panic('expected water') } assert after.falling assert after.depth == source_depth } + +// lava_depth_at returns the internal depth of a lava cell, or 0 if the block +// there is not lava. +fn lava_depth_at(mut m LiquidManager, x int, y int, z int) int { + l := m.liquid_of_kind_at(.lava, x, y, z) or { return 0 } + return l.depth +} + +fn solid_floor(mut wld FakeWorld) { + for dx in -8 .. 9 { + for dz in -8 .. 9 { + wld.set_solid(dx, -1, dz) + } + } +} + +fn test_lava_spreads_less_far_than_water() { + mut wld := &FakeWorld{} + solid_floor(mut wld) + mut m := new_manager(wld) + m.place_liquid_source(.lava, 0, 0, 0) + run_ticks(mut m, 200) + + // Decay is 2 per step, so a source at depth 8 reaches three cells out. + assert lava_depth_at(mut m, 1, 0, 0) == 6 + assert lava_depth_at(mut m, 2, 0, 0) == 4 + assert lava_depth_at(mut m, 3, 0, 0) == 2 + assert lava_depth_at(mut m, 4, 0, 0) == 0 +} + +fn test_lava_moves_slower_than_water() { + mut wld := &FakeWorld{} + solid_floor(mut wld) + mut m := new_manager(wld) + m.place_liquid_source(.lava, 0, 0, 0) + // One tick is not enough for lava: it is held over until its own slower + // rate comes round. + m.tick() + assert !m.is_liquid_of_kind(.lava, 1, 0, 0) + assert m.pending_count() > 0 + for _ in 0 .. lava_tick_divisor { + m.tick() + } + assert m.is_liquid_of_kind(.lava, 1, 0, 0) +} + +fn test_lava_arriving_in_water_makes_stone() { + mut wld := &FakeWorld{} + mut m := new_manager(wld) + m.set_liquid(new_liquid_source(.water), 1, 0, 0) + m.flow_into(new_liquid_flowing(.lava, 6), 1, 0, 0) + + assert wld.get_block(1, 0, 0) == world.stone.network_id +} + +fn test_water_pouring_onto_a_lava_source_makes_obsidian() { + mut wld := &FakeWorld{} + mut m := new_manager(wld) + m.set_liquid(new_liquid_source(.lava), 0, 0, 0) + m.set_liquid(new_liquid_falling(.water), 0, 1, 0) + m.process(0, 0, 0) + + assert wld.get_block(0, 0, 0) == world.obsidian.network_id +} + +fn test_water_reaching_a_lava_source_makes_obsidian() { + mut wld := &FakeWorld{} + solid_floor(mut wld) + mut m := new_manager(wld) + m.set_liquid(new_liquid_source(.lava), 1, 0, 0) + m.place_source(0, 0, 0) + run_ticks(mut m, 200) + + assert wld.get_block(1, 0, 0) == world.obsidian.network_id +} + +fn test_water_reaching_flowing_lava_makes_cobblestone() { + mut wld := &FakeWorld{} + solid_floor(mut wld) + mut m := new_manager(wld) + m.set_liquid(new_liquid_flowing(.lava, 4), 1, 0, 0) + m.place_source(0, 0, 0) + run_ticks(mut m, 200) + + assert wld.get_block(1, 0, 0) == world.cobblestone.network_id +} + +fn test_lava_never_forms_a_new_source_between_two_of_them() { + mut wld := &FakeWorld{} + solid_floor(mut wld) + mut m := new_manager(wld) + m.place_liquid_source(.lava, -1, 0, 0) + m.place_liquid_source(.lava, 1, 0, 0) + run_ticks(mut m, 200) + + middle := m.liquid_of_kind_at(.lava, 0, 0, 0) or { + assert false, 'lava did not reach the gap' + return + } + assert !middle.is_source() +} + +fn test_fluid_ids_are_recognised_by_kind() { + assert is_lava_id(new_liquid_source(.lava).network_id()) + assert is_lava_id(new_liquid_flowing(.lava, 3).network_id()) + assert !is_lava_id(new_source().network_id()) + assert is_water_id(new_flowing(3).network_id()) + assert !is_water_id(world.stone.network_id) + assert liquid_at_id(world.stone.network_id) == none +} diff --git a/server/block/liquid_water.v b/server/block/liquid_water.v deleted file mode 100644 index de9a0a4..0000000 --- a/server/block/liquid_water.v +++ /dev/null @@ -1,75 +0,0 @@ -module block - -import server.world - -// 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. -pub const source_depth = 8 -pub const max_flow_depth = 7 -pub const spread_decay = 1 -pub const min_adjacent_sources = 2 - -// 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. -pub struct WaterState { -pub: - depth int - falling bool -} - -// is_source reports whether this cell is a full still source. -pub fn (w WaterState) is_source() bool { - return w.depth == source_depth && !w.falling -} - -// liquid_depth_value returns the Bedrock liquid_depth state value for this cell. -fn (w WaterState) liquid_depth_value() int { - mut v := source_depth - w.depth - if w.falling { - v += 8 - } - return v -} - -// network_id resolves this water cell to its Bedrock runtime id. Source cells -// use minecraft:water, flowing/falling cells use minecraft:flowing_water. -pub fn (w WaterState) network_id() int { - name := if w.is_source() { 'minecraft:water' } else { 'minecraft:flowing_water' } - return world.new_block_with_states(name, [ - world.BlockState{ - key: 'liquid_depth' - kind: world.state_kind_int - int_value: w.liquid_depth_value() - }, - ]).network_id -} - -// new_source is the full still water source. -pub fn new_source() WaterState { - return WaterState{ - depth: source_depth - falling: false - } -} - -// new_flowing builds a flowing cell at the given depth. -pub fn new_flowing(depth int) WaterState { - return WaterState{ - depth: depth - falling: false - } -} - -// new_falling is a falling column - it appears full but spreads like flowing. -pub fn new_falling() WaterState { - return WaterState{ - depth: source_depth - falling: true - } -} diff --git a/server/session/blocks_api.v b/server/session/blocks_api.v index 20eb88a..8545be6 100644 --- a/server/session/blocks_api.v +++ b/server/session/blocks_api.v @@ -1,5 +1,6 @@ module session +import server.block import server.world import server.worldrt @@ -93,31 +94,45 @@ fn (mut h Hub) set_block_id(id int, x int, y int, z int) { h.write_block(id, x, y, z) } -// PlaceWaterTask is place_water's actual per world work. The liquid manager -// interaction only ever happens on the owning world's own actor thread, -// through the worldrt.WorldTx it's handed. -struct PlaceWaterTask { - x int - y int - z int +// PlaceLiquidTask is place_water/place_lava's actual per world work. The +// liquid manager interaction only ever happens on the owning world's own actor +// thread, through the worldrt.WorldTx it's handed. +struct PlaceLiquidTask { + kind block.LiquidKind + x int + y int + z int } -fn (t PlaceWaterTask) name() string { - return 'PlaceWaterTask' +fn (t PlaceLiquidTask) name() string { + return 'PlaceLiquidTask' } -fn (t PlaceWaterTask) run(mut tx worldrt.WorldTx) { - tx.place_water(t.x, t.y, t.z) +fn (t PlaceLiquidTask) run(mut tx worldrt.WorldTx) { + match t.kind { + .water { tx.place_water(t.x, t.y, t.z) } + .lava { tx.place_lava(t.x, t.y, t.z) } + } } // place_water sets a water source in the default world and lets that world's // runtime own the liquid update. fn (mut h Hub) place_water(x int, y int, z int) { + h.place_liquid(.water, x, y, z) +} + +// place_lava does the same for a lava source. +fn (mut h Hub) place_lava(x int, y int, z int) { + h.place_liquid(.lava, x, y, z) +} + +fn (mut h Hub) place_liquid(kind block.LiquidKind, x int, y int, z int) { mut wr := h.default_world_runtime() or { return } - wr.submit(PlaceWaterTask{ - x: x - y: y - z: z + wr.submit(PlaceLiquidTask{ + kind: kind + x: x + y: y + z: z }) } diff --git a/server/session/breaking.v b/server/session/breaking.v index d046cc0..d42a0fa 100644 --- a/server/session/breaking.v +++ b/server/session/breaking.v @@ -366,13 +366,13 @@ fn (s &NetworkSession) supported_by_ground() bool { pos := s.current_position() below := s.block_at(int(math.floor(pos.x)), int(math.floor(pos.y - ground_probe_depth)), int(math.floor(pos.z))) - return below != world.air.network_id && below != world.water.network_id + return below != world.air.network_id && block.liquid_at_id(below) == none } // head_submerged reports whether the player's eyes are inside water, which // slows mining down the same way vanilla does. fn (s &NetworkSession) head_submerged() bool { pos := s.current_position() - return s.block_at(int(math.floor(pos.x)), int(math.floor(pos.y + player_eye_height)), - int(math.floor(pos.z))) == world.water.network_id + return block.is_water_id(s.block_at(int(math.floor(pos.x)), int(math.floor(pos.y + + player_eye_height)), int(math.floor(pos.z)))) } diff --git a/server/session/environment.v b/server/session/environment.v index a094b10..8f722c4 100644 --- a/server/session/environment.v +++ b/server/session/environment.v @@ -1,6 +1,7 @@ module session import math +import server.block import server.player import server.world import bedrock_v.protocol.current as proto @@ -37,8 +38,11 @@ fn (mut s NetworkSession) tick_environmental_damage(mut tx worldrt.WorldTx) { } block_id := block_at(tx, int(math.floor(pos.x)), int(math.floor(pos.y)), int(math.floor(pos.z))) - s.tick_breath(mut tx, block_id == world.water.network_id, tick) - s.tick_burning(mut tx, block_id == world.lava.network_id, block_id == world.water.network_id) + // Flowing lava burns and flowing water drowns exactly like their sources, + // so both are matched by fluid rather than by the source block id. + in_water := block.is_water_id(block_id) + s.tick_breath(mut tx, in_water, tick) + s.tick_burning(mut tx, block.is_lava_id(block_id), in_water) } // tick_breath drains or refills the underwater breath meter and applies diff --git a/server/session/spawn.v b/server/session/spawn.v index 9569ef7..d6c7a61 100644 --- a/server/session/spawn.v +++ b/server/session/spawn.v @@ -6,6 +6,7 @@ import bedrock_v.protocol import bedrock_v.protocol.types import bedrock_v.nbt import server.event +import server.block import server.world import server.world.db import server.internal.logger @@ -48,7 +49,7 @@ fn saved_body_clear(id int) bool { } fn saved_floor_solid(id int) bool { - return id != world.air.network_id && id != world.water.network_id && id != world.lava.network_id + return id != world.air.network_id && block.liquid_at_id(id) == none } fn safe_player_position(gen world.Generator, pos types.Vector3) bool { diff --git a/server/session/world_concurrency_test.v b/server/session/world_concurrency_test.v index d6cfb7c..04f0da7 100644 --- a/server/session/world_concurrency_test.v +++ b/server/session/world_concurrency_test.v @@ -49,10 +49,11 @@ fn test_stalled_world_does_not_stall_another_worlds_ticks_or_liquids() { // Give B's liquid manager real, ongoing work so "B keeps progressing" // means something more than just the tick counter moving. - ok := wr_b.submit(PlaceWaterTask{ - x: 0 - y: 60 - z: 0 + ok := wr_b.submit(PlaceLiquidTask{ + kind: .water + x: 0 + y: 60 + z: 0 }) assert ok worldrt.world_call[bool]('test', mut wr_b, fn (mut tx worldrt.WorldTx) bool { diff --git a/server/session/world_metrics_test.v b/server/session/world_metrics_test.v index 5a45943..a0577fb 100644 --- a/server/session/world_metrics_test.v +++ b/server/session/world_metrics_test.v @@ -134,7 +134,9 @@ fn test_metrics_reports_player_and_entity_counts() { mut s := &NetworkSession{ player: pl runtime_id: hub.allocate_runtime_id() - conn: &Conn{ transport: transport } + conn: &Conn{ + transport: transport + } hub: hub world: wr.world world_runtime: wr @@ -203,10 +205,11 @@ fn test_metrics_liquid_backlog_matches_actor_owned_state_after_a_tick() { hub.close_worlds() } - assert wr.submit(PlaceWaterTask{ - x: 0 - y: 60 - z: 0 + assert wr.submit(PlaceLiquidTask{ + kind: .water + x: 0 + y: 60 + z: 0 }) worldrt.world_call[bool]('test', mut wr, fn (mut tx worldrt.WorldTx) bool { return true @@ -286,7 +289,9 @@ fn test_metrics_tracks_outbound_overflow_and_peak_depth() { mut s := &NetworkSession{ player: pl runtime_id: hub.allocate_runtime_id() - conn: &Conn{ transport: transport } + conn: &Conn{ + transport: transport + } hub: hub world: wr.world world_runtime: wr diff --git a/server/worldrt/world_runtime.v b/server/worldrt/world_runtime.v index 6a36737..18b9ba5 100644 --- a/server/worldrt/world_runtime.v +++ b/server/worldrt/world_runtime.v @@ -47,9 +47,10 @@ pub interface WorldTask { // WorldRuntime owns one world's actor and serializes its simulation state. // External callers submit WorldTasks; task code accesses the world through // WorldTx. Actor owned fields must not be accessed directly from other threads. -@[heap] + // block_update_flags is the UpdateBlockPacket flag set every block change is // sent with: neighbours plus network. +@[heap] pub const block_update_flags = 11 @[heap] @@ -58,10 +59,10 @@ pub mut: // The shared substrate a world task works on. Public because gameplay code // outside this module runs on the actor and needs them; everything below // belongs to the actor alone and stays private to it. - world &db.World = unsafe { nil } - entities &entity.Manager = unsafe { nil } + world &db.World = unsafe { nil } + entities &entity.Manager = unsafe { nil } chunk_service &WorldChunkService = unsafe { nil } - task_scheduler &WorldScheduler = unsafe { nil } + task_scheduler &WorldScheduler = unsafe { nil } // services is the slice of the surrounding server a world task may need. // The actor's own work never touches it. services Services @@ -70,7 +71,7 @@ pub mut: // sessions through it. generators GeneratorFactory // handler receives what happens in this world without a player causing it. - handler Handler = NopHandler{} + handler Handler = NopHandler{} liquids &block.LiquidManager = unsafe { nil } mut: // players advances the players in this world once per simulated step. The @@ -115,7 +116,6 @@ mut: // simulation debt (requested - simulated) without taking tick_mutex. published_latest_tick &stdatomic.AtomicVal[i64] = stdatomic.new_atomic[i64](0) - // Cross thread metric snapshots. The world thread publishes simulation // values, session threads publish outbound values and other threads only // read them. @@ -133,13 +133,13 @@ mut: // actor owned and must not be read from another thread. The list is // deliberately unbounded (see its own comment), so depth is the only // signal that a task is rescheduling itself without making progress. - published_continuation_depth &stdatomic.AtomicVal[i64] = stdatomic.new_atomic[i64](0) - published_continuation_peak &stdatomic.AtomicVal[i64] = stdatomic.new_atomic[i64](0) + published_continuation_depth &stdatomic.AtomicVal[i64] = stdatomic.new_atomic[i64](0) + published_continuation_peak &stdatomic.AtomicVal[i64] = stdatomic.new_atomic[i64](0) // actor_thread identifies the thread running run_jobs, published once // before the loop starts. Other threads read it to detect a call that // would block waiting for the actor it is already running on. It reads as // 0 until the actor starts, so the check fails open rather than wrong. - actor_thread &stdatomic.AtomicVal[u64] = stdatomic.new_atomic[u64](0) + actor_thread &stdatomic.AtomicVal[u64] = stdatomic.new_atomic[u64](0) // longest_task_name uses the runtime mutex because V atomics can't store // strings. It is updated only when a task sets a new duration record. longest_task_name string @@ -242,7 +242,11 @@ pub fn (mut tx WorldTx) set_block(x int, y int, z int, id int) { } pub fn (mut tx WorldTx) place_water(x int, y int, z int) { - tx.wr.liquids.place_source(x, y, z) + tx.wr.liquids.place_liquid_source(.water, x, y, z) +} + +pub fn (mut tx WorldTx) place_lava(x int, y int, z int) { + tx.wr.liquids.place_liquid_source(.lava, x, y, z) } pub fn (mut tx WorldTx) on_block_changed(x int, y int, z int) { @@ -648,8 +652,8 @@ pub: // Continuation backlog now and at its high water mark. Continuations are // actor owned follow up work for tasks that yielded; a depth that keeps // climbing means something is rescheduling itself faster than it retires. - continuation_depth i64 - continuation_peak i64 + continuation_depth i64 + continuation_peak i64 } pub fn (mut wr WorldRuntime) metrics() WorldMetrics {