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 generation;
@ -13,6 +19,10 @@ pub struct Level {
/// the blocks which make up the level
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 {
@ -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<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,
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<BTreeMap<u8, BlockInfo>> = 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,
}
}
}

View file

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

View file

@ -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<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::{
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,30 +252,24 @@ 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)
})
.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(),
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(),
message: "Not allowed to break this block.".to_string(),
});
}
}
if cancel {
reply_queue.push_back(ServerPacket::SetBlock {
@ -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 {

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()
}