From 7940dbf7d4c763f3ab61a2d531ee48664f5ecdb9 Mon Sep 17 00:00:00 2001 From: zyl Date: Sat, 18 Jan 2025 10:58:25 -0800 Subject: [PATCH] implement grass spread, resolves #9 --- src/level.rs | 47 +++++++++++++++++++++++++++---- src/level/block.rs | 53 +++++++++++++++++++++++++---------- src/server.rs | 65 +++++++++++++++++++++++++++++++++++++++++-- src/server/network.rs | 10 +++++-- src/util.rs | 40 +++++++++++++++++++++++--- 5 files changed, 186 insertions(+), 29 deletions(-) diff --git a/src/level.rs b/src/level.rs index 439ed52..d32127e 100644 --- a/src/level.rs +++ b/src/level.rs @@ -9,7 +9,8 @@ use bevy_reflect::{PartialReflect, Struct}; use serde::{Deserialize, Serialize}; use crate::{ - error::GeneralError, packet::server::ServerPacket, player::SavablePlayerData, util::neighbors, + error::GeneralError, packet::server::ServerPacket, player::SavablePlayerData, + util::neighbors_full, }; use self::block::BLOCK_INFO; @@ -40,7 +41,11 @@ pub struct Level { pub level_rules: LevelRules, /// index of blocks which need to be updated in the next tick + #[serde(default)] pub awaiting_update: BTreeSet, + /// index of blocks which are eligible for random tick updates + #[serde(default)] + pub possible_random_updates: Vec, /// list of updates to apply to the world on the next tick #[serde(skip)] pub updates: Vec, @@ -62,6 +67,7 @@ impl Level { weather: WeatherType::Sunny, level_rules: Default::default(), awaiting_update: Default::default(), + possible_random_updates: Default::default(), updates: Default::default(), save_now: false, player_data: Default::default(), @@ -106,11 +112,11 @@ impl Level { z: z as i16, block_type: update.block, }); - for (nx, ny, nz) in neighbors(self, x, y, z) { + for (nx, ny, nz) in neighbors_full(self, x, y, z) { let info = BLOCK_INFO .get(&self.get_block(nx, ny, nz)) .expect("missing block"); - if info.block_type.needs_update_when_neighbor_changed() { + if info.needs_update_when_neighbor_changed { self.awaiting_update.insert(self.index(nx, ny, nz)); } } @@ -223,7 +229,14 @@ impl From for WeatherType { #[derive(Debug, Clone, Serialize, Deserialize, bevy_reflect::Reflect)] pub struct LevelRules { /// whether fluids should spread in the level + #[serde(default = "level_rules::fluid_spread")] pub fluid_spread: bool, + /// the number of blocks which should receive random tick updates + #[serde(default = "level_rules::random_tick_updates")] + pub random_tick_updates: u64, + /// the chance that grass will spread to an adjacent dirt block when randomly updated + #[serde(default = "level_rules::grass_spread_chance")] + pub grass_spread_chance: u64, } impl LevelRules { @@ -251,6 +264,7 @@ impl LevelRules { pub fn set_rule(&mut self, name: &str, value: &str) -> Result<(), String> { let bool_type_id = TypeId::of::(); let f64_type_id = TypeId::of::(); + let u64_type_id = TypeId::of::(); let string_type_id = TypeId::of::(); fn parse_and_apply(value: &str, field_mut: &mut dyn PartialReflect) -> Result<(), String> @@ -278,6 +292,8 @@ impl LevelRules { parse_and_apply::(value, field_mut)?; } else if id == f64_type_id { parse_and_apply::(value, field_mut)?; + } else if id == u64_type_id { + parse_and_apply::(value, field_mut)?; } else if id == string_type_id { parse_and_apply::(value, field_mut)?; } else { @@ -288,8 +304,27 @@ impl LevelRules { } } -impl Default for LevelRules { - fn default() -> Self { - Self { fluid_spread: true } +mod level_rules { + pub fn fluid_spread() -> bool { + true + } + + pub fn random_tick_updates() -> u64 { + 1000 + } + + pub fn grass_spread_chance() -> u64 { + 2048 + } +} + +impl Default for LevelRules { + fn default() -> Self { + use level_rules::*; + Self { + fluid_spread: fluid_spread(), + random_tick_updates: random_tick_updates(), + grass_spread_chance: grass_spread_chance(), + } } } diff --git a/src/level/block.rs b/src/level/block.rs index 9afe27c..6e22e32 100644 --- a/src/level/block.rs +++ b/src/level/block.rs @@ -7,7 +7,10 @@ use crate::player::PlayerType; /// the level of custom blocks supported by the server pub const CUSTOM_BLOCKS_SUPPORT_LEVEL: u8 = 1; +pub const ID_AIR: u8 = 0x00; pub const ID_STONE: u8 = 0x01; +pub const ID_GRASS: u8 = 0x02; +pub const ID_DIRT: u8 = 0x03; pub const ID_WATER_FLOWING: u8 = 0x08; pub const ID_WATER_STATIONARY: u8 = 0x09; pub const ID_LAVA_FLOWING: u8 = 0x0a; @@ -16,10 +19,21 @@ pub const ID_LAVA_STATIONARY: u8 = 0x0b; /// information about all blocks implemented pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { [ - (0x00, BlockInfo::new("air").block_type(BlockType::NonSolid)), + ( + ID_AIR, + BlockInfo::new("air").block_type(BlockType::NonSolid), + ), (ID_STONE, BlockInfo::new("stone")), - (0x02, BlockInfo::new("grass")), - (0x03, BlockInfo::new("dirt")), + ( + ID_GRASS, + BlockInfo::new("grass") + .needs_update_when_neighbor_changed() + .may_receive_random_ticks(), + ), + ( + ID_DIRT, + BlockInfo::new("dirt").needs_update_when_neighbor_changed(), + ), (0x04, BlockInfo::new("cobblestone")), (0x05, BlockInfo::new("planks")), ( @@ -43,7 +57,8 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { ID_WATER_STATIONARY, BlockInfo::new("water_stationary") .block_type(BlockType::FluidStationary { moving: 0x08 }) - .perm(PlayerType::Moderator, PlayerType::Normal), + .perm(PlayerType::Moderator, PlayerType::Normal) + .needs_update_when_neighbor_changed(), ), ( ID_LAVA_FLOWING, @@ -58,7 +73,8 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { ID_LAVA_STATIONARY, BlockInfo::new("lava_stationary") .block_type(BlockType::FluidStationary { moving: 0x0a }) - .perm(PlayerType::Moderator, PlayerType::Normal), + .perm(PlayerType::Moderator, PlayerType::Normal) + .needs_update_when_neighbor_changed(), ), (0x0c, BlockInfo::new("sand")), (0x0d, BlockInfo::new("gravel")), @@ -169,6 +185,10 @@ pub struct BlockInfo { pub break_permissions: PlayerType, /// the block used as fallback if the client doesn't support it pub fallback: Option, + /// whether this block needs an update when its neighbor is changed + pub needs_update_when_neighbor_changed: bool, + /// whether this block may receive random ticks + pub may_receive_random_ticks: bool, } impl BlockInfo { @@ -180,6 +200,8 @@ impl BlockInfo { place_permissions: PlayerType::Normal, break_permissions: PlayerType::Normal, fallback: None, + needs_update_when_neighbor_changed: false, + may_receive_random_ticks: false, } } @@ -202,6 +224,18 @@ impl BlockInfo { self.fallback = Some(fallback); self } + + /// marks this block as needing updates when its neighbor is changed + pub const fn needs_update_when_neighbor_changed(mut self) -> Self { + self.needs_update_when_neighbor_changed = true; + self + } + + /// makes this block capable of receiving random ticks and marks it as eligible when placed + pub const fn may_receive_random_ticks(mut self) -> Self { + self.may_receive_random_ticks = true; + self + } } /// types of blocks @@ -233,13 +267,4 @@ impl BlockType { _ => false, } } - - /// gets whether this block type needs an update when one of it's direct neighbors changes - #[allow(clippy::match_like_matches_macro)] - pub fn needs_update_when_neighbor_changed(&self) -> bool { - match self { - BlockType::FluidStationary { .. } => true, - _ => false, - } - } } diff --git a/src/server.rs b/src/server.rs index 8837944..d442dbe 100644 --- a/src/server.rs +++ b/src/server.rs @@ -3,20 +3,23 @@ pub(crate) mod network; use std::{path::PathBuf, sync::Arc}; +use rand::{seq::SliceRandom, Rng}; use tokio::{net::TcpListener, sync::RwLock}; use crate::{ error::GeneralError, level::{ block::{ - BlockType, BLOCK_INFO, ID_LAVA_FLOWING, ID_LAVA_STATIONARY, ID_STONE, ID_WATER_FLOWING, - ID_WATER_STATIONARY, + BlockType, BLOCK_INFO, ID_DIRT, ID_GRASS, ID_LAVA_FLOWING, ID_LAVA_STATIONARY, + ID_STONE, ID_WATER_FLOWING, ID_WATER_STATIONARY, }, BlockUpdate, Level, }, packet::server::ServerPacket, player::Player, - util::neighbors_minus_up, + util::{ + get_relative_coords, neighbors_full, neighbors_minus_up, neighbors_with_vertical_diagonals, + }, CONFIG_FILE, }; @@ -209,12 +212,68 @@ fn tick(data: &mut ServerData, tick: usize) { let mut packets = level.apply_updates(); + // apply random tick updates + let mut rng = rand::thread_rng(); + level.possible_random_updates.shuffle(&mut rng); + for _ in 0..level.level_rules.random_tick_updates { + if let Some(index) = level.possible_random_updates.pop() { + level.awaiting_update.insert(index); + } else { + break; + } + } + let awaiting_update = std::mem::take(&mut level.awaiting_update); for index in awaiting_update { let (x, y, z) = level.coordinates(index); let block_id = level.get_block(x, y, z); let block = BLOCK_INFO.get(&block_id).expect("should never fail"); match &block.block_type { + BlockType::Solid => { + if block_id == ID_GRASS { + let mut dirt_count = 0; + for (nx, ny, nz) in neighbors_with_vertical_diagonals(level, x, y, z) { + if level.get_block(nx, ny, nz) == ID_DIRT { + // only turn dirt into grass if there's empty space above it + if get_relative_coords(level, nx, ny, nz, 0, 1, 0) + .map(|(x, y, z)| level.get_block(x, y, z)) + .is_none_or(|id| id == 0x00) + { + dirt_count += 1; + if rng.gen_range(0..level.level_rules.grass_spread_chance) == 0 { + dirt_count -= 1; + level.updates.push(BlockUpdate { + index: level.index(nx, ny, nz), + block: ID_GRASS, + }); + } + } + } + } + if get_relative_coords(level, x, y, z, 0, 1, 0) + .map(|(x, y, z)| level.get_block(x, y, z)) + .is_some_and(|id| id != 0x00) + { + dirt_count += 1; + if rng.gen_range(0..level.level_rules.grass_spread_chance) == 0 { + dirt_count -= 1; + level.updates.push(BlockUpdate { + index: level.index(x, y, z), + block: ID_DIRT, + }); + } + } + if dirt_count > 0 { + level.possible_random_updates.push(level.index(x, y, z)); + } + } else if block_id == ID_DIRT { + for (nx, ny, nz) in neighbors_full(level, x, y, z) { + if level.get_block(nx, ny, nz) == ID_GRASS { + level.possible_random_updates.push(level.index(nx, ny, nz)); + } + } + } + } BlockType::FluidFlowing { stationary, ticks_to_spread, diff --git a/src/server/network.rs b/src/server/network.rs index 55b1ef3..2941404 100644 --- a/src/server/network.rs +++ b/src/server/network.rs @@ -14,7 +14,10 @@ use tokio::{ use crate::{ command::Command, error::GeneralError, - level::{block::BLOCK_INFO, BlockUpdate, Level}, + level::{ + block::{BLOCK_INFO, ID_AIR}, + BlockUpdate, Level, + }, packet::{ client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH, EXTENSION_MAGIC_NUMBER, STRING_LENGTH, @@ -326,7 +329,7 @@ async fn handle_stream_inner( mode, block_type, } => { - let block_type = if mode == 0x00 { 0 } else { block_type }; + let block_type = if mode == 0x00 { ID_AIR } else { block_type }; let mut data = data.write().await; // kick players if they attempt to place a block out of bounds @@ -383,6 +386,9 @@ async fn handle_stream_inner( if new_block_info.block_type.needs_update_on_place() { data.level.awaiting_update.insert(index); } + if new_block_info.may_receive_random_ticks { + data.level.possible_random_updates.push(index); + } } ClientPacket::PositionOrientation { _player_id_or_held_block: _, diff --git a/src/util.rs b/src/util.rs index 0c06145..f170e10 100644 --- a/src/util.rs +++ b/src/util.rs @@ -9,10 +9,10 @@ const NEIGHBORS: &[(isize, isize, isize)] = &[ (0, 0, 1), ]; -/// gets a block's direct neighbors which are in the bounds of the level -pub fn neighbors(level: &Level, x: usize, y: usize, z: usize) -> Vec<(usize, usize, usize)> { - get_many_relative_coords(level, x, y, z, NEIGHBORS.iter().copied()) -} +// /// gets a block's direct neighbors which are in the bounds of the level +// pub fn neighbors(level: &Level, x: usize, y: usize, z: usize) -> Vec<(usize, usize, usize)> { +// get_many_relative_coords(level, x, y, z, NEIGHBORS.iter().copied()) +// } /// gets a blocks direct neighbors (excluding above the block) which are in the bounds of the level pub fn neighbors_minus_up( @@ -24,6 +24,38 @@ pub fn neighbors_minus_up( get_many_relative_coords(level, x, y, z, NEIGHBORS.iter().skip(1).copied()) } +/// gets a block's neighbors (including vertical diagonals) which are in the bounds of the level, i.e. for grass spread +pub fn neighbors_with_vertical_diagonals( + level: &Level, + x: usize, + y: usize, + z: usize, +) -> Vec<(usize, usize, usize)> { + let down = NEIGHBORS + .iter() + .skip(2) + .copied() + .map(|(x, _, z)| (x, -1isize, z)); + let up = NEIGHBORS + .iter() + .skip(2) + .copied() + .map(|(x, _, z)| (x, 1isize, z)); + get_many_relative_coords( + level, + x, + y, + z, + NEIGHBORS.iter().skip(2).copied().chain(down).chain(up), + ) +} + +/// gets a block's neighbors, including all diagonals +pub fn neighbors_full(level: &Level, x: usize, y: usize, z: usize) -> Vec<(usize, usize, usize)> { + let iter = (-1..=1).flat_map(|x| (-1..=1).flat_map(move |y| (-1..=1).map(move |z| (x, y, z)))); + get_many_relative_coords(level, x, y, z, iter) +} + /// adds relative coordinates to the given ones, returning `None` if the coordinates would be out of bounds for hte level pub fn get_relative_coords( level: &Level,