mirror of
https://github.com/zyllian/classics.git
synced 2025-01-17 19:22:37 -08:00
implement server ticks, block updates, and classic fluid mechanics
This commit is contained in:
parent
8dc89d959e
commit
492da31ce0
6 changed files with 285 additions and 39 deletions
56
src/level.rs
56
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<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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ mod level;
|
|||
mod packet;
|
||||
mod player;
|
||||
mod server;
|
||||
mod util;
|
||||
|
||||
const CONFIG_FILE: &str = "./server-config.json";
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
62
src/util.rs
Normal file
62
src/util.rs
Normal 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()
|
||||
}
|
Loading…
Add table
Reference in a new issue