mirror of https://github.com/zyllian/classics.git synced 2025-05-10 12:16:40 -07:00

implement grass spread, resolves #9

This commit is contained in:
zyl 2025-01-18 10:58:25 -08:00
parent 8c96ecbbbd
commit 7940dbf7d4
Signed by: zyl
SSH key fingerprint: SHA256:uxxbSXbdroP/OnKBGnEDk5q7EKB2razvstC/KmzdXXs
5 changed files with 186 additions and 29 deletions

View file

@ -9,7 +9,8 @@ use bevy_reflect::{PartialReflect, Struct};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ 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; use self::block::BLOCK_INFO;
@ -40,7 +41,11 @@ pub struct Level {
pub level_rules: LevelRules, pub level_rules: LevelRules,
/// index of blocks which need to be updated in the next tick /// index of blocks which need to be updated in the next tick
#[serde(default)]
pub awaiting_update: BTreeSet<usize>, pub awaiting_update: BTreeSet<usize>,
/// index of blocks which are eligible for random tick updates
#[serde(default)]
pub possible_random_updates: Vec<usize>,
/// list of updates to apply to the world on the next tick /// list of updates to apply to the world on the next tick
#[serde(skip)] #[serde(skip)]
pub updates: Vec<BlockUpdate>, pub updates: Vec<BlockUpdate>,
@ -62,6 +67,7 @@ impl Level {
weather: WeatherType::Sunny, weather: WeatherType::Sunny,
level_rules: Default::default(), level_rules: Default::default(),
awaiting_update: Default::default(), awaiting_update: Default::default(),
possible_random_updates: Default::default(),
updates: Default::default(), updates: Default::default(),
save_now: false, save_now: false,
player_data: Default::default(), player_data: Default::default(),
@ -106,11 +112,11 @@ impl Level {
z: z as i16, z: z as i16,
block_type: update.block, 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 let info = BLOCK_INFO
.get(&self.get_block(nx, ny, nz)) .get(&self.get_block(nx, ny, nz))
.expect("missing block"); .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)); self.awaiting_update.insert(self.index(nx, ny, nz));
} }
} }
@ -223,7 +229,14 @@ impl From<u8> for WeatherType {
#[derive(Debug, Clone, Serialize, Deserialize, bevy_reflect::Reflect)] #[derive(Debug, Clone, Serialize, Deserialize, bevy_reflect::Reflect)]
pub struct LevelRules { pub struct LevelRules {
/// whether fluids should spread in the level /// whether fluids should spread in the level
#[serde(default = "level_rules::fluid_spread")]
pub fluid_spread: bool, 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 { impl LevelRules {
@ -251,6 +264,7 @@ impl LevelRules {
pub fn set_rule(&mut self, name: &str, value: &str) -> Result<(), String> { pub fn set_rule(&mut self, name: &str, value: &str) -> Result<(), String> {
let bool_type_id = TypeId::of::<bool>(); let bool_type_id = TypeId::of::<bool>();
let f64_type_id = TypeId::of::<f64>(); let f64_type_id = TypeId::of::<f64>();
let u64_type_id = TypeId::of::<u64>();
let string_type_id = TypeId::of::<String>(); let string_type_id = TypeId::of::<String>();
fn parse_and_apply<T>(value: &str, field_mut: &mut dyn PartialReflect) -> Result<(), String> fn parse_and_apply<T>(value: &str, field_mut: &mut dyn PartialReflect) -> Result<(), String>
@ -278,6 +292,8 @@ impl LevelRules {
parse_and_apply::<bool>(value, field_mut)?; parse_and_apply::<bool>(value, field_mut)?;
} else if id == f64_type_id { } else if id == f64_type_id {
parse_and_apply::<f64>(value, field_mut)?; parse_and_apply::<f64>(value, field_mut)?;
} else if id == u64_type_id {
parse_and_apply::<u64>(value, field_mut)?;
} else if id == string_type_id { } else if id == string_type_id {
parse_and_apply::<String>(value, field_mut)?; parse_and_apply::<String>(value, field_mut)?;
} else { } else {
@ -288,8 +304,27 @@ impl LevelRules {
} }
} }
impl Default for LevelRules { mod level_rules {
fn default() -> Self { pub fn fluid_spread() -> bool {
Self { fluid_spread: true } 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(),
}
} }
} }

View file

@ -7,7 +7,10 @@ use crate::player::PlayerType;
/// the level of custom blocks supported by the server /// the level of custom blocks supported by the server
pub const CUSTOM_BLOCKS_SUPPORT_LEVEL: u8 = 1; pub const CUSTOM_BLOCKS_SUPPORT_LEVEL: u8 = 1;
pub const ID_AIR: u8 = 0x00;
pub const ID_STONE: u8 = 0x01; 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_FLOWING: u8 = 0x08;
pub const ID_WATER_STATIONARY: u8 = 0x09; pub const ID_WATER_STATIONARY: u8 = 0x09;
pub const ID_LAVA_FLOWING: u8 = 0x0a; pub const ID_LAVA_FLOWING: u8 = 0x0a;
@ -16,10 +19,21 @@ pub const ID_LAVA_STATIONARY: u8 = 0x0b;
/// information about all blocks implemented /// information about all blocks implemented
pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| { pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
[ [
(0x00, BlockInfo::new("air").block_type(BlockType::NonSolid)), (
ID_AIR,
BlockInfo::new("air").block_type(BlockType::NonSolid),
),
(ID_STONE, BlockInfo::new("stone")), (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")), (0x04, BlockInfo::new("cobblestone")),
(0x05, BlockInfo::new("planks")), (0x05, BlockInfo::new("planks")),
( (
@ -43,7 +57,8 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
ID_WATER_STATIONARY, ID_WATER_STATIONARY,
BlockInfo::new("water_stationary") BlockInfo::new("water_stationary")
.block_type(BlockType::FluidStationary { moving: 0x08 }) .block_type(BlockType::FluidStationary { moving: 0x08 })
.perm(PlayerType::Moderator, PlayerType::Normal), .perm(PlayerType::Moderator, PlayerType::Normal)
.needs_update_when_neighbor_changed(),
), ),
( (
ID_LAVA_FLOWING, ID_LAVA_FLOWING,
@ -58,7 +73,8 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
ID_LAVA_STATIONARY, ID_LAVA_STATIONARY,
BlockInfo::new("lava_stationary") BlockInfo::new("lava_stationary")
.block_type(BlockType::FluidStationary { moving: 0x0a }) .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")), (0x0c, BlockInfo::new("sand")),
(0x0d, BlockInfo::new("gravel")), (0x0d, BlockInfo::new("gravel")),
@ -169,6 +185,10 @@ pub struct BlockInfo {
pub break_permissions: PlayerType, pub break_permissions: PlayerType,
/// the block used as fallback if the client doesn't support it /// the block used as fallback if the client doesn't support it
pub fallback: Option<u8>, pub fallback: Option<u8>,
/// 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 { impl BlockInfo {
@ -180,6 +200,8 @@ impl BlockInfo {
place_permissions: PlayerType::Normal, place_permissions: PlayerType::Normal,
break_permissions: PlayerType::Normal, break_permissions: PlayerType::Normal,
fallback: None, fallback: None,
needs_update_when_neighbor_changed: false,
may_receive_random_ticks: false,
} }
} }
@ -202,6 +224,18 @@ impl BlockInfo {
self.fallback = Some(fallback); self.fallback = Some(fallback);
self 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 /// types of blocks
@ -233,13 +267,4 @@ impl BlockType {
_ => false, _ => 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,
}
}
} }

View file

@ -3,20 +3,23 @@ pub(crate) mod network;
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use rand::{seq::SliceRandom, Rng};
use tokio::{net::TcpListener, sync::RwLock}; use tokio::{net::TcpListener, sync::RwLock};
use crate::{ use crate::{
error::GeneralError, error::GeneralError,
level::{ level::{
block::{ block::{
BlockType, BLOCK_INFO, ID_LAVA_FLOWING, ID_LAVA_STATIONARY, ID_STONE, ID_WATER_FLOWING, BlockType, BLOCK_INFO, ID_DIRT, ID_GRASS, ID_LAVA_FLOWING, ID_LAVA_STATIONARY,
ID_WATER_STATIONARY, ID_STONE, ID_WATER_FLOWING, ID_WATER_STATIONARY,
}, },
BlockUpdate, Level, BlockUpdate, Level,
}, },
packet::server::ServerPacket, packet::server::ServerPacket,
player::Player, player::Player,
util::neighbors_minus_up, util::{
get_relative_coords, neighbors_full, neighbors_minus_up, neighbors_with_vertical_diagonals,
},
CONFIG_FILE, CONFIG_FILE,
}; };
@ -209,12 +212,68 @@ fn tick(data: &mut ServerData, tick: usize) {
let mut packets = level.apply_updates(); 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); let awaiting_update = std::mem::take(&mut level.awaiting_update);
for index in awaiting_update { for index in awaiting_update {
let (x, y, z) = level.coordinates(index); let (x, y, z) = level.coordinates(index);
let block_id = level.get_block(x, y, z); let block_id = level.get_block(x, y, z);
let block = BLOCK_INFO.get(&block_id).expect("should never fail"); let block = BLOCK_INFO.get(&block_id).expect("should never fail");
match &block.block_type { 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 { BlockType::FluidFlowing {
stationary, stationary,
ticks_to_spread, ticks_to_spread,

View file

@ -14,7 +14,10 @@ use tokio::{
use crate::{ use crate::{
command::Command, command::Command,
error::GeneralError, error::GeneralError,
level::{block::BLOCK_INFO, BlockUpdate, Level}, level::{
block::{BLOCK_INFO, ID_AIR},
BlockUpdate, Level,
},
packet::{ packet::{
client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH, client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH,
EXTENSION_MAGIC_NUMBER, STRING_LENGTH, EXTENSION_MAGIC_NUMBER, STRING_LENGTH,
@ -326,7 +329,7 @@ async fn handle_stream_inner(
mode, mode,
block_type, 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; let mut data = data.write().await;
// kick players if they attempt to place a block out of bounds // 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() { if new_block_info.block_type.needs_update_on_place() {
data.level.awaiting_update.insert(index); data.level.awaiting_update.insert(index);
} }
if new_block_info.may_receive_random_ticks {
data.level.possible_random_updates.push(index);
}
} }
ClientPacket::PositionOrientation { ClientPacket::PositionOrientation {
_player_id_or_held_block: _, _player_id_or_held_block: _,

View file

@ -9,10 +9,10 @@ const NEIGHBORS: &[(isize, isize, isize)] = &[
(0, 0, 1), (0, 0, 1),
]; ];
/// gets a block's direct neighbors which are in the bounds of the level // /// 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)> { // 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()) // 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 /// gets a blocks direct neighbors (excluding above the block) which are in the bounds of the level
pub fn neighbors_minus_up( 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()) 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 /// 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( pub fn get_relative_coords(
level: &Level, level: &Level,