mirror of
https://github.com/zyllian/classics.git
synced 2025-05-10 08:06:40 -07:00
implement grass spread, resolves #9
This commit is contained in:
parent
8c96ecbbbd
commit
7940dbf7d4
5 changed files with 186 additions and 29 deletions
47
src/level.rs
47
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<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
|
||||
#[serde(skip)]
|
||||
pub updates: Vec<BlockUpdate>,
|
||||
|
@ -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<u8> 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::<bool>();
|
||||
let f64_type_id = TypeId::of::<f64>();
|
||||
let u64_type_id = TypeId::of::<u64>();
|
||||
let string_type_id = TypeId::of::<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)?;
|
||||
} else if id == f64_type_id {
|
||||
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 {
|
||||
parse_and_apply::<String>(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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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")),
|
||||
(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<BTreeMap<u8, BlockInfo>> = 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<BTreeMap<u8, BlockInfo>> = 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<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 {
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: _,
|
||||
|
|
40
src/util.rs
40
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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue