implement server ticks, block updates, and classic fluid mechanics

This commit is contained in:
Zoey 2024-04-22 16:35:54 -07:00
parent 8dc89d959e
commit 492da31ce0
No known key found for this signature in database
GPG key ID: 8611B896D1AAFAF2
6 changed files with 285 additions and 39 deletions

View file

@ -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 block;
pub mod generation; pub mod generation;
@ -13,6 +19,10 @@ pub struct Level {
/// the blocks which make up the level /// the blocks which make up the level
pub blocks: Vec<u8>, pub blocks: Vec<u8>,
/// index of blocks which need to be updated in the next tick
pub awaiting_update: BTreeSet<usize>,
/// list of updates to apply to the world on the next tick
pub updates: Vec<BlockUpdate>,
} }
impl Level { impl Level {
@ -23,6 +33,8 @@ impl Level {
y_size, y_size,
z_size, z_size,
blocks: vec![0; x_size * 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 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 /// gets the block at the given position
pub fn get_block(&self, x: usize, y: usize, z: usize) -> u8 { pub fn get_block(&self, x: usize, y: usize, z: usize) -> u8 {
self.blocks[self.index(x, y, z)] self.blocks[self.index(x, y, z)]
@ -41,4 +61,40 @@ impl Level {
let index = self.index(x, y, z); let index = self.index(x, y, z);
self.blocks[index] = block; self.blocks[index] = block;
} }
/// applies the level's queued updates
pub fn apply_updates(&mut self) -> Vec<ServerPacket> {
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,
} }

View file

@ -24,7 +24,10 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
( (
0x08, 0x08,
BlockInfo::new("water_flowing") 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), .perm(PlayerType::Operator, PlayerType::Normal),
), ),
( (
@ -36,7 +39,10 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
( (
0x0a, 0x0a,
BlockInfo::new("lava_flowing") 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), .perm(PlayerType::Operator, PlayerType::Normal),
), ),
( (
@ -152,7 +158,30 @@ pub enum BlockType {
/// a slab /// a slab
Slab, Slab,
/// fluid which is actively flowing /// fluid which is actively flowing
FluidFlowing { stationary: u8 }, FluidFlowing {
stationary: u8,
ticks_to_spread: usize,
},
/// fluid which is stationary /// fluid which is stationary
FluidStationary { moving: u8 }, 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,
}
}
}

View file

@ -11,6 +11,7 @@ mod level;
mod packet; mod packet;
mod player; mod player;
mod server; mod server;
mod util;
const CONFIG_FILE: &str = "./server-config.json"; const CONFIG_FILE: &str = "./server-config.json";

View file

@ -5,10 +5,19 @@ use std::sync::Arc;
use tokio::{net::TcpListener, sync::RwLock}; 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; use self::config::ServerConfig;
const TICK_DURATION: std::time::Duration = std::time::Duration::from_millis(50);
/// the server /// the server
#[derive(Debug)] #[derive(Debug)]
pub struct Server { pub struct Server {
@ -64,6 +73,10 @@ impl Server {
/// starts the server /// starts the server
pub async fn run(&mut self) -> std::io::Result<()> { pub async fn run(&mut self) -> std::io::Result<()> {
let data = self.data.clone();
tokio::spawn(async move {
handle_ticks(data).await;
});
loop { loop {
let (stream, addr) = self.listener.accept().await?; let (stream, addr) = self.listener.accept().await?;
println!("connection from {addr}"); println!("connection from {addr}");
@ -76,3 +89,85 @@ impl Server {
} }
} }
} }
/// function to tick the server
async fn handle_ticks(data: Arc<RwLock<ServerData>>) {
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());
}
}
}

View file

@ -10,7 +10,7 @@ use tokio::{
}; };
use crate::{ use crate::{
level::{block::BLOCK_INFO, Level}, level::{block::BLOCK_INFO, BlockUpdate, Level},
packet::{client::ClientPacket, server::ServerPacket, PacketWriter, ARRAY_LENGTH}, packet::{client::ClientPacket, server::ServerPacket, PacketWriter, ARRAY_LENGTH},
player::{Player, PlayerType}, player::{Player, PlayerType},
server::config::ServerProtectionMode, server::config::ServerProtectionMode,
@ -233,7 +233,18 @@ async fn handle_stream_inner(
} }
let new_block_info = BLOCK_INFO.get(&block_type); 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 = let block =
data.level.get_block(x as usize, y as usize, z as usize); data.level.get_block(x as usize, y as usize, z as usize);
let block_info = BLOCK_INFO let block_info = BLOCK_INFO
@ -241,29 +252,23 @@ async fn handle_stream_inner(
.expect("missing block information for block!"); .expect("missing block information for block!");
// check if player has ability to place/break these blocks // check if player has ability to place/break these blocks
if let Some(new_block_info) = new_block_info { let player_type = data
let player_type = data .players
.players .iter()
.iter() .find_map(|p| (p.id == *own_id).then_some(p.player_type))
.find_map(|p| { .unwrap_or_default();
(p.id == *own_id).then_some(p.player_type) if player_type < new_block_info.place_permissions {
}) cancel = true;
.unwrap_or_default(); reply_queue.push_back(ServerPacket::Message {
if player_type < new_block_info.place_permissions { player_id: -1,
cancel = true; message: "Not allowed to place this block.".to_string(),
reply_queue.push_back(ServerPacket::Message { });
player_id: -1, } else if player_type < block_info.break_permissions {
message: "Not allowed to place this block." cancel = true;
.to_string(), reply_queue.push_back(ServerPacket::Message {
}); player_id: -1,
} else if player_type < block_info.break_permissions { message: "Not allowed to break this block.".to_string(),
cancel = true; });
reply_queue.push_back(ServerPacket::Message {
player_id: -1,
message: "Not allowed to break this block."
.to_string(),
});
}
} }
if cancel { if cancel {
@ -275,16 +280,14 @@ async fn handle_stream_inner(
}); });
continue; continue;
} }
let packet = ServerPacket::SetBlock { let (x, y, z) = (x as usize, y as usize, z as usize);
x, let index = data.level.index(x, y, z);
y, data.level.updates.push(BlockUpdate {
z, index,
block_type, block: block_type,
}; });
data.level if new_block_info.block_type.needs_update_on_place() {
.set_block(x as usize, y as usize, z as usize, block_type); data.level.awaiting_update.insert(index);
for player in &mut data.players {
player.packets_to_send.push(packet.clone());
} }
} }
ClientPacket::PositionOrientation { ClientPacket::PositionOrientation {

62
src/util.rs Normal file
View file

@ -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<T>(
level: &Level,
x: usize,
y: usize,
z: usize,
coords: T,
) -> Vec<(usize, usize, usize)>
where
T: IntoIterator<Item = (isize, isize, isize)>,
{
coords
.into_iter()
.filter_map(|(rx, ry, rz)| get_relative_coords(level, x, y, z, rx, ry, rz))
.collect()
}