diff --git a/src/level.rs b/src/level.rs index f7c0e30..ba938f0 100644 --- a/src/level.rs +++ b/src/level.rs @@ -1,3 +1,9 @@ +use std::collections::BTreeSet; + +use crate::{packet::server::ServerPacket, util::neighbors}; + +use self::block::BLOCK_INFO; + pub mod block; pub mod generation; @@ -13,6 +19,10 @@ pub struct Level { /// the blocks which make up the level pub blocks: Vec, + /// index of blocks which need to be updated in the next tick + pub awaiting_update: BTreeSet, + /// list of updates to apply to the world on the next tick + pub updates: Vec, } impl Level { @@ -23,6 +33,8 @@ impl Level { y_size, z_size, blocks: vec![0; x_size * y_size * z_size], + awaiting_update: Default::default(), + updates: Default::default(), } } @@ -31,6 +43,14 @@ impl Level { x + z * self.x_size + y * self.x_size * self.z_size } + /// gets the coordinates for the given index + pub fn coordinates(&self, index: usize) -> (usize, usize, usize) { + let y = index / (self.x_size * self.z_size); + let z = (index / self.x_size) % self.z_size; + let x = index % self.z_size; + (x, y, z) + } + /// gets the block at the given position pub fn get_block(&self, x: usize, y: usize, z: usize) -> u8 { self.blocks[self.index(x, y, z)] @@ -41,4 +61,40 @@ impl Level { let index = self.index(x, y, z); self.blocks[index] = block; } + + /// applies the level's queued updates + pub fn apply_updates(&mut self) -> Vec { + self.updates.dedup_by(|a, b| a.index == b.index); + let mut packets = Vec::with_capacity(self.updates.len()); + + for update in std::mem::take(&mut self.updates) { + let (x, y, z) = self.coordinates(update.index); + self.blocks[update.index] = update.block; + packets.push(ServerPacket::SetBlock { + x: x as i16, + y: y as i16, + z: z as i16, + block_type: update.block, + }); + for (nx, ny, nz) in neighbors(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() { + self.awaiting_update.insert(self.index(nx, ny, nz)); + } + } + } + + packets + } +} + +/// struct describing a block update for the level to handle +#[derive(Debug, Clone)] +pub struct BlockUpdate { + /// the index of the block to be updated + pub index: usize, + /// the block type to set the block to + pub block: u8, } diff --git a/src/level/block.rs b/src/level/block.rs index 8fa06a4..6fc26df 100644 --- a/src/level/block.rs +++ b/src/level/block.rs @@ -24,7 +24,10 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { ( 0x08, BlockInfo::new("water_flowing") - .block_type(BlockType::FluidFlowing { stationary: 0x09 }) + .block_type(BlockType::FluidFlowing { + stationary: 0x09, + ticks_to_spread: 1, + }) .perm(PlayerType::Operator, PlayerType::Normal), ), ( @@ -36,7 +39,10 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { ( 0x0a, BlockInfo::new("lava_flowing") - .block_type(BlockType::FluidFlowing { stationary: 0x0b }) + .block_type(BlockType::FluidFlowing { + stationary: 0x0b, + ticks_to_spread: 5, + }) .perm(PlayerType::Operator, PlayerType::Normal), ), ( @@ -152,7 +158,30 @@ pub enum BlockType { /// a slab Slab, /// fluid which is actively flowing - FluidFlowing { stationary: u8 }, + FluidFlowing { + stationary: u8, + ticks_to_spread: usize, + }, /// fluid which is stationary FluidStationary { moving: u8 }, } + +impl BlockType { + /// gets whether this block type needs an update after being placed + #[allow(clippy::match_like_matches_macro)] + pub fn needs_update_on_place(&self) -> bool { + match self { + BlockType::FluidFlowing { .. } => true, + _ => 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/main.rs b/src/main.rs index d4e46ae..765b16b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,7 @@ mod level; mod packet; mod player; mod server; +mod util; const CONFIG_FILE: &str = "./server-config.json"; diff --git a/src/server.rs b/src/server.rs index e088072..162995b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -5,10 +5,19 @@ use std::sync::Arc; use tokio::{net::TcpListener, sync::RwLock}; -use crate::{level::Level, player::Player}; +use crate::{ + level::{ + block::{BlockType, BLOCK_INFO}, + BlockUpdate, Level, + }, + player::Player, + util::neighbors_minus_up, +}; use self::config::ServerConfig; +const TICK_DURATION: std::time::Duration = std::time::Duration::from_millis(50); + /// the server #[derive(Debug)] pub struct Server { @@ -64,6 +73,10 @@ impl Server { /// starts the server pub async fn run(&mut self) -> std::io::Result<()> { + let data = self.data.clone(); + tokio::spawn(async move { + handle_ticks(data).await; + }); loop { let (stream, addr) = self.listener.accept().await?; println!("connection from {addr}"); @@ -76,3 +89,85 @@ impl Server { } } } + +/// function to tick the server +async fn handle_ticks(data: Arc>) { + let mut current_tick = 0; + loop { + tick(&mut *data.write().await, current_tick); + current_tick = current_tick.wrapping_add(1); + tokio::time::sleep(TICK_DURATION).await; + } +} + +/// function which ticks the server once +fn tick(data: &mut ServerData, tick: usize) { + let level = &mut data.level; + + let mut packets = level.apply_updates(); + + let awaiting_update = std::mem::take(&mut level.awaiting_update); + if !awaiting_update.is_empty() { + println!("hm"); + } + 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"); + println!("{block:#?}"); + match &block.block_type { + BlockType::FluidFlowing { + stationary, + ticks_to_spread, + } => { + if tick % ticks_to_spread == 0 { + let update = BlockUpdate { + index, + block: *stationary, + }; + level.updates.push(update); + for (nx, ny, nz) in neighbors_minus_up(level, x, y, z) { + let block_at = level.get_block(nx, ny, nz); + let update = if block_at == 0 { + level.awaiting_update.insert(level.index(nx, ny, nz)); + BlockUpdate { + index: level.index(nx, ny, nz), + block: block_id, + } + } else { + continue; + }; + level.updates.push(update); + } + } else { + level.awaiting_update.insert(index); + } + } + BlockType::FluidStationary { moving } => { + let mut needs_update = false; + for (nx, ny, nz) in neighbors_minus_up(level, x, y, z) { + if level.get_block(nx, ny, nz) == 0 { + needs_update = true; + break; + } + } + if needs_update { + let index = level.index(x, y, z); + level.updates.push(BlockUpdate { + index, + block: *moving, + }); + level.awaiting_update.insert(index); + } + } + _ => {} + } + } + + packets.extend(level.apply_updates()); + for packet in packets { + for player in &mut data.players { + player.packets_to_send.push(packet.clone()); + } + } +} diff --git a/src/server/network.rs b/src/server/network.rs index 8027121..8fb8ec4 100644 --- a/src/server/network.rs +++ b/src/server/network.rs @@ -10,7 +10,7 @@ use tokio::{ }; use crate::{ - level::{block::BLOCK_INFO, Level}, + level::{block::BLOCK_INFO, BlockUpdate, Level}, packet::{client::ClientPacket, server::ServerPacket, PacketWriter, ARRAY_LENGTH}, player::{Player, PlayerType}, server::config::ServerProtectionMode, @@ -233,7 +233,18 @@ async fn handle_stream_inner( } let new_block_info = BLOCK_INFO.get(&block_type); - let mut cancel = new_block_info.is_none(); + if new_block_info.is_none() { + reply_queue.push_back(ServerPacket::Message { + player_id: -1, + message: format!( + "Unknown block ID: 0x{:0x}", + block_type + ), + }); + continue; + } + let new_block_info = new_block_info.expect("will never fail"); + let mut cancel = false; let block = data.level.get_block(x as usize, y as usize, z as usize); let block_info = BLOCK_INFO @@ -241,29 +252,23 @@ async fn handle_stream_inner( .expect("missing block information for block!"); // check if player has ability to place/break these blocks - if let Some(new_block_info) = new_block_info { - let player_type = data - .players - .iter() - .find_map(|p| { - (p.id == *own_id).then_some(p.player_type) - }) - .unwrap_or_default(); - if player_type < new_block_info.place_permissions { - cancel = true; - reply_queue.push_back(ServerPacket::Message { - player_id: -1, - message: "Not allowed to place this block." - .to_string(), - }); - } else if player_type < block_info.break_permissions { - cancel = true; - reply_queue.push_back(ServerPacket::Message { - player_id: -1, - message: "Not allowed to break this block." - .to_string(), - }); - } + let player_type = data + .players + .iter() + .find_map(|p| (p.id == *own_id).then_some(p.player_type)) + .unwrap_or_default(); + if player_type < new_block_info.place_permissions { + cancel = true; + reply_queue.push_back(ServerPacket::Message { + player_id: -1, + message: "Not allowed to place this block.".to_string(), + }); + } else if player_type < block_info.break_permissions { + cancel = true; + reply_queue.push_back(ServerPacket::Message { + player_id: -1, + message: "Not allowed to break this block.".to_string(), + }); } if cancel { @@ -275,16 +280,14 @@ async fn handle_stream_inner( }); continue; } - let packet = ServerPacket::SetBlock { - x, - y, - z, - block_type, - }; - data.level - .set_block(x as usize, y as usize, z as usize, block_type); - for player in &mut data.players { - player.packets_to_send.push(packet.clone()); + let (x, y, z) = (x as usize, y as usize, z as usize); + let index = data.level.index(x, y, z); + data.level.updates.push(BlockUpdate { + index, + block: block_type, + }); + if new_block_info.block_type.needs_update_on_place() { + data.level.awaiting_update.insert(index); } } ClientPacket::PositionOrientation { diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..0c06145 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,62 @@ +use crate::level::Level; + +const NEIGHBORS: &[(isize, isize, isize)] = &[ + (0, 1, 0), + (0, -1, 0), + (-1, 0, 0), + (1, 0, 0), + (0, 0, -1), + (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 blocks direct neighbors (excluding above the block) which are in the bounds of the level +pub fn neighbors_minus_up( + level: &Level, + x: usize, + y: usize, + z: usize, +) -> Vec<(usize, usize, usize)> { + get_many_relative_coords(level, x, y, z, NEIGHBORS.iter().skip(1).copied()) +} + +/// 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, + x: usize, + y: usize, + z: usize, + rx: isize, + ry: isize, + rz: isize, +) -> Option<(usize, usize, usize)> { + Some(( + x.checked_add_signed(rx) + .and_then(|x| (x < level.x_size).then_some(x))?, + y.checked_add_signed(ry) + .and_then(|y| (y < level.y_size).then_some(y))?, + z.checked_add_signed(rz) + .and_then(|z| (z < level.z_size).then_some(z))?, + )) +} + +/// takes an iterator of relative changes to apply to the given coordinates and returns a `Vec` of the ones in bounds of the level +pub fn get_many_relative_coords( + level: &Level, + x: usize, + y: usize, + z: usize, + coords: T, +) -> Vec<(usize, usize, usize)> +where + T: IntoIterator, +{ + coords + .into_iter() + .filter_map(|(rx, ry, rz)| get_relative_coords(level, x, y, z, rx, ry, rz)) + .collect() +}