significant refactors and implement extension negotiations (resolves #17) and EnvWeatherType (resolves #29)

This commit is contained in:
Zoey 2024-04-24 18:54:19 -07:00
parent 6cb7c5f49a
commit bfb9d62f96
No known key found for this signature in database
GPG key ID: 8611B896D1AAFAF2
15 changed files with 756 additions and 404 deletions

2
.gitignore vendored
View file

@ -1,5 +1,5 @@
/target /target
server-config.json server-config.json
.DS_Store .DS_Store
*.clw /levels
*.cw *.cw

11
Cargo.lock generated
View file

@ -81,6 +81,16 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitmask-enum"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9990737a6d5740ff51cdbbc0f0503015cb30c390f6623968281eb214a520cfc0"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.6.0" version = "1.6.0"
@ -104,6 +114,7 @@ name = "classics"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"bincode", "bincode",
"bitmask-enum",
"bytes", "bytes",
"flate2", "flate2",
"half", "half",

View file

@ -5,6 +5,7 @@ version = "0.1.0"
[dependencies] [dependencies]
bincode = "2.0.0-rc.3" bincode = "2.0.0-rc.3"
bitmask-enum = "2"
bytes = "1" bytes = "1"
flate2 = "1" flate2 = "1"
half = "2" half = "2"

View file

@ -17,6 +17,7 @@ const CMD_BAN: &str = "ban";
const CMD_ALLOWENTRY: &str = "allowentry"; const CMD_ALLOWENTRY: &str = "allowentry";
const CMD_SETPASS: &str = "setpass"; const CMD_SETPASS: &str = "setpass";
const CMD_SETLEVELSPAWN: &str = "setlevelspawn"; const CMD_SETLEVELSPAWN: &str = "setlevelspawn";
const CMD_WEATHER: &str = "weather";
/// list of commands available on the server /// list of commands available on the server
pub const COMMANDS_LIST: &[&str] = &[ pub const COMMANDS_LIST: &[&str] = &[
@ -30,6 +31,7 @@ pub const COMMANDS_LIST: &[&str] = &[
CMD_ALLOWENTRY, CMD_ALLOWENTRY,
CMD_SETPASS, CMD_SETPASS,
CMD_SETLEVELSPAWN, CMD_SETLEVELSPAWN,
CMD_WEATHER,
]; ];
/// enum for possible commands /// enum for possible commands
@ -69,6 +71,8 @@ pub enum Command<'m> {
SetPass { password: &'m str }, SetPass { password: &'m str },
/// sets the level spawn to the player's location /// sets the level spawn to the player's location
SetLevelSpawn, SetLevelSpawn,
/// changes the levels weather
Weather { weather_type: &'m str },
} }
impl<'m> Command<'m> { impl<'m> Command<'m> {
@ -117,6 +121,9 @@ impl<'m> Command<'m> {
password: arguments.trim(), password: arguments.trim(),
}, },
CMD_SETLEVELSPAWN => Self::SetLevelSpawn, CMD_SETLEVELSPAWN => Self::SetLevelSpawn,
CMD_WEATHER => Self::Weather {
weather_type: arguments,
},
_ => return Err(format!("Unknown command: {command_name}")), _ => return Err(format!("Unknown command: {command_name}")),
}) })
} }
@ -134,6 +141,7 @@ impl<'m> Command<'m> {
Self::AllowEntry { .. } => CMD_ALLOWENTRY, Self::AllowEntry { .. } => CMD_ALLOWENTRY,
Self::SetPass { .. } => CMD_SETPASS, Self::SetPass { .. } => CMD_SETPASS,
Self::SetLevelSpawn => CMD_SETLEVELSPAWN, Self::SetLevelSpawn => CMD_SETLEVELSPAWN,
Self::Weather { .. } => CMD_WEATHER,
} }
} }
@ -195,6 +203,10 @@ impl<'m> Command<'m> {
c(""), c(""),
"&fSets the level's spawn to your location.".to_string(), "&fSets the level's spawn to your location.".to_string(),
], ],
CMD_WEATHER => vec![
c("<weather type>"),
"&fSets the level's weather.".to_string(),
],
_ => vec!["&eUnknown command!".to_string()], _ => vec!["&eUnknown command!".to_string()],
} }
} }
@ -459,6 +471,19 @@ impl<'m> Command<'m> {
data.config_needs_saving = true; data.config_needs_saving = true;
messages.push("Level spawn updated!".to_string()); messages.push("Level spawn updated!".to_string());
} }
Command::Weather { weather_type } => {
if let Ok(weather_type) = weather_type.try_into() {
data.level.weather = weather_type;
let packet = ServerPacket::EnvWeatherType { weather_type };
for player in &mut data.players {
player.packets_to_send.push(packet.clone());
}
messages.push("Weather updated!".to_string());
} else {
messages.push(format!("&cUnknown weather type {weather_type}!"));
}
}
} }
messages messages

View file

@ -1,6 +1,10 @@
use std::{collections::BTreeSet, path::Path}; use std::{
collections::BTreeSet,
io::{Read, Write},
path::Path,
};
use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize};
use crate::{packet::server::ServerPacket, util::neighbors}; use crate::{packet::server::ServerPacket, util::neighbors};
@ -9,8 +13,11 @@ use self::block::BLOCK_INFO;
pub mod block; pub mod block;
pub mod generation; pub mod generation;
const LEVEL_INFO_PATH: &str = "info.json";
const LEVEL_DATA_PATH: &str = "level.dat";
/// a classic level /// a classic level
#[derive(Debug, Clone, Encode, Decode)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Level { pub struct Level {
/// the size of the level in the X direction /// the size of the level in the X direction
pub x_size: usize, pub x_size: usize,
@ -20,10 +27,15 @@ pub struct Level {
pub z_size: usize, pub z_size: usize,
/// the blocks which make up the level /// the blocks which make up the level
#[serde(skip)]
pub blocks: Vec<u8>, pub blocks: Vec<u8>,
/// the level's weather
pub weather: WeatherType,
/// index of blocks which need to be updated in the next tick /// index of blocks which need to be updated in the next tick
pub awaiting_update: BTreeSet<usize>, pub awaiting_update: BTreeSet<usize>,
/// list of updates to apply to the world on the next tick /// list of updates to apply to the world on the next tick
#[serde(skip)]
pub updates: Vec<BlockUpdate>, pub updates: Vec<BlockUpdate>,
} }
@ -35,6 +47,7 @@ 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],
weather: WeatherType::Sunny,
awaiting_update: Default::default(), awaiting_update: Default::default(),
updates: Default::default(), updates: Default::default(),
} }
@ -91,32 +104,104 @@ impl Level {
packets packets
} }
pub async fn save<P>(&self, path: P) /// saves the level
pub async fn save<P>(&self, path: P) -> std::io::Result<()>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let path = path.as_ref();
tokio::fs::create_dir_all(path).await?;
tokio::fs::write(
path.join(LEVEL_INFO_PATH),
serde_json::to_string_pretty(self).unwrap(),
)
.await?;
let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best()); let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
bincode::encode_into_std_write(self, &mut encoder, bincode::config::standard()).unwrap(); encoder
tokio::fs::write(path, encoder.finish().unwrap()) .write_all(&self.blocks)
.expect("failed to write blocks");
tokio::fs::write(
path.join(LEVEL_DATA_PATH),
encoder.finish().expect("failed to encode blocks"),
)
.await .await
.unwrap();
} }
pub async fn load<P>(path: P) -> Self /// loads the level
pub async fn load<P>(path: P) -> std::io::Result<Self>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let data = tokio::fs::read(path).await.unwrap(); let path = path.as_ref();
let mut decoder = flate2::read::GzDecoder::new(data.as_slice()); let mut info: Self =
bincode::decode_from_std_read(&mut decoder, bincode::config::standard()).unwrap() serde_json::from_str(&tokio::fs::read_to_string(path.join(LEVEL_INFO_PATH)).await?)
.expect("failed to deserialize level info");
let blocks_data = tokio::fs::read(path.join(LEVEL_DATA_PATH)).await?;
let mut decoder = flate2::read::GzDecoder::new(blocks_data.as_slice());
decoder.read_to_end(&mut info.blocks)?;
let len = info.x_size * info.y_size * info.z_size;
if info.blocks.len() != len {
panic!(
"level data is not correct size! expected {len}, got {}",
info.blocks.len()
);
}
Ok(info)
} }
} }
/// struct describing a block update for the level to handle /// struct describing a block update for the level to handle
#[derive(Debug, Clone, Encode, Decode)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockUpdate { pub struct BlockUpdate {
/// the index of the block to be updated /// the index of the block to be updated
pub index: usize, pub index: usize,
/// the block type to set the block to /// the block type to set the block to
pub block: u8, pub block: u8,
} }
/// weather types for a level
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum WeatherType {
Sunny,
Raining,
Snowing,
}
impl Default for WeatherType {
fn default() -> Self {
Self::Sunny
}
}
impl From<&WeatherType> for u8 {
fn from(value: &WeatherType) -> Self {
match value {
WeatherType::Sunny => 0,
WeatherType::Raining => 1,
WeatherType::Snowing => 2,
}
}
}
impl From<u8> for WeatherType {
fn from(value: u8) -> Self {
match value {
1 => Self::Raining,
2 => Self::Snowing,
_ => Self::Sunny,
}
}
}
impl TryFrom<&str> for WeatherType {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"sunny" => Self::Sunny,
"raining" => Self::Raining,
"snowing" => Self::Snowing,
_ => return Err(()),
})
}
}

View file

@ -14,6 +14,7 @@ mod player;
mod server; mod server;
mod util; mod util;
const SERVER_NAME: &str = "classics";
const CONFIG_FILE: &str = "./server-config.json"; const CONFIG_FILE: &str = "./server-config.json";
#[tokio::main] #[tokio::main]

View file

@ -2,6 +2,7 @@ use half::f16;
use safer_bytes::{error::Truncated, SafeBuf}; use safer_bytes::{error::Truncated, SafeBuf};
pub mod client; pub mod client;
pub mod client_extended;
pub mod server; pub mod server;
/// length of classic strings /// length of classic strings
@ -10,6 +11,30 @@ pub const STRING_LENGTH: usize = 64;
pub const ARRAY_LENGTH: usize = 1024; pub const ARRAY_LENGTH: usize = 1024;
/// units in an f16 unit /// units in an f16 unit
pub const F16_UNITS: f32 = 32.0; pub const F16_UNITS: f32 = 32.0;
/// the magic number to check whether the client supports extensions
pub const EXTENSION_MAGIC_NUMBER: u8 = 0x42;
/// information about a packet extension
#[derive(Debug, PartialEq, Eq)]
pub struct ExtInfo {
/// the extension's name
pub ext_name: String,
/// the extension's version
pub version: i32,
/// the bitmask for the extension
pub bitmask: ExtBitmask,
}
impl ExtInfo {
/// creates new extension info
pub const fn new(ext_name: String, version: i32, bitmask: ExtBitmask) -> Self {
Self {
ext_name,
version,
bitmask,
}
}
}
/// trait extending the `SafeBuf` type /// trait extending the `SafeBuf` type
pub trait SafeBufExtension: SafeBuf { pub trait SafeBufExtension: SafeBuf {
@ -80,6 +105,15 @@ impl PacketWriter {
self.write_i16(r) self.write_i16(r)
} }
/// writes an i32 to the packet
fn write_i32(self, i: i32) -> Self {
let mut s = self;
for b in i.to_be_bytes() {
s = s.write_u8(b);
}
s
}
/// writes a string to the packet /// writes a string to the packet
fn write_string(self, str: &str) -> Self { fn write_string(self, str: &str) -> Self {
let mut s = self; let mut s = self;
@ -109,3 +143,103 @@ impl PacketWriter {
self.write_array_of_length(bytes, ARRAY_LENGTH) self.write_array_of_length(bytes, ARRAY_LENGTH)
} }
} }
/// bitmask for enabled extensions
/// values should not be saved to disk or sent over network! no guarantees on them remaining the same between versions
#[bitmask_enum::bitmask(u64)]
pub enum ExtBitmask {
ClickDistance,
CustomBlocks,
HeldBlock,
EmoteFix,
TextHotKey,
ExtPlayerList,
EnvColors,
SelectionCuboid,
BlockPermissions,
ChangeModel,
EnvMapAppearance,
EnvWeatherType,
HackControl,
MessageTypes,
PlayerClick,
LongerMessages,
FullCP437,
BlockDefinitions,
BlockDefinitionsExt,
BulkBlockUpdate,
TextColors,
EnvMapAspect,
EntityProperty,
ExtEntityPositions,
TwoWayPing,
InventoryOrder,
InstantMOTD,
ExtendedBlocks,
FastMap,
ExtendedTextures,
SetHotbar,
SetSpawnpoint,
VelocityControl,
CustomParticles,
CustomModels_v2,
ExtEntityTeleport,
}
impl ExtBitmask {
/// gets info about a specific extension
fn info(self) -> Option<ExtInfo> {
// TODO: add entries as extensions are supported
Some(match self {
Self::EnvWeatherType => {
ExtInfo::new("EnvWeatherType".to_string(), 1, Self::EnvWeatherType)
}
_ => return None,
})
}
/// gets info about all extensions
pub fn all_contained_info(self) -> Vec<ExtInfo> {
[
Self::ClickDistance,
Self::CustomBlocks,
Self::HeldBlock,
Self::EmoteFix,
Self::TextHotKey,
Self::ExtPlayerList,
Self::EnvColors,
Self::SelectionCuboid,
Self::BlockPermissions,
Self::ChangeModel,
Self::EnvMapAppearance,
Self::EnvWeatherType,
Self::HackControl,
Self::MessageTypes,
Self::PlayerClick,
Self::LongerMessages,
Self::FullCP437,
Self::BlockDefinitions,
Self::BlockDefinitionsExt,
Self::BulkBlockUpdate,
Self::TextColors,
Self::EnvMapAspect,
Self::EntityProperty,
Self::ExtEntityPositions,
Self::TwoWayPing,
Self::InventoryOrder,
Self::InstantMOTD,
Self::ExtendedBlocks,
Self::FastMap,
Self::ExtendedTextures,
Self::SetHotbar,
Self::SetSpawnpoint,
Self::VelocityControl,
Self::CustomParticles,
Self::CustomModels_v2,
Self::ExtEntityTeleport,
]
.into_iter()
.filter_map(|flag| (self & flag).info())
.collect()
}
}

View file

@ -1,6 +1,6 @@
use half::f16; use half::f16;
use super::{SafeBufExtension, STRING_LENGTH}; use super::{client_extended::ExtendedClientPacket, SafeBufExtension, STRING_LENGTH};
/// enum for a packet which can be received by the client /// enum for a packet which can be received by the client
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -10,10 +10,10 @@ pub enum ClientPacket {
/// should always be 0x07 for classic clients >= 0.28 /// should always be 0x07 for classic clients >= 0.28
protocol_version: u8, protocol_version: u8,
username: String, username: String,
/// currently unverified, original minecraft auth for classic is gone anyway /// used as the password the client sends to the server
/// TODO: use verification key field as password protection? investigate
verification_key: String, verification_key: String,
_unused: u8, /// unused in vanilla but used to check the magic number for extension support
magic_number: u8,
}, },
/// packet sent when a client changes a block /// packet sent when a client changes a block
/// because changes are reflected immediately, to restrict changes, server must send back its own SetBlock packet with the original block /// because changes are reflected immediately, to restrict changes, server must send back its own SetBlock packet with the original block
@ -41,20 +41,12 @@ pub enum ClientPacket {
player_id: i8, player_id: i8,
message: String, message: String,
}, },
// extension packets
Extended(ExtendedClientPacket),
} }
impl ClientPacket { impl ClientPacket {
// unused currently, so disabled
// /// gets the packet's id
// pub fn get_id(&self) -> u8 {
// match self {
// Self::PlayerIdentification { .. } => 0x00,
// Self::SetBlock { .. } => 0x05,
// Self::PositionOrientation { .. } => 0x08,
// Self::Message { .. } => 0x0d,
// }
// }
/// gets the size of the packet from the given id (minus one byte for the id) /// gets the size of the packet from the given id (minus one byte for the id)
pub const fn get_size_from_id(id: u8) -> Option<usize> { pub const fn get_size_from_id(id: u8) -> Option<usize> {
Some(match id { Some(match id {
@ -62,7 +54,7 @@ impl ClientPacket {
0x05 => 2 + 2 + 2 + 1 + 1, 0x05 => 2 + 2 + 2 + 1 + 1,
0x08 => 1 + 2 + 2 + 2 + 1 + 1, 0x08 => 1 + 2 + 2 + 2 + 1 + 1,
0x0d => 1 + STRING_LENGTH, 0x0d => 1 + STRING_LENGTH,
_ => return None, _ => return ExtendedClientPacket::get_size_from_id(id),
}) })
} }
@ -76,7 +68,7 @@ impl ClientPacket {
protocol_version: buf.try_get_u8().ok()?, protocol_version: buf.try_get_u8().ok()?,
username: buf.try_get_string().ok()?, username: buf.try_get_string().ok()?,
verification_key: buf.try_get_string().ok()?, verification_key: buf.try_get_string().ok()?,
_unused: buf.try_get_u8().ok()?, magic_number: buf.try_get_u8().ok()?,
}, },
0x05 => Self::SetBlock { 0x05 => Self::SetBlock {
x: buf.try_get_i16().ok()?, x: buf.try_get_i16().ok()?,
@ -97,56 +89,8 @@ impl ClientPacket {
player_id: buf.try_get_i8().ok()?, player_id: buf.try_get_i8().ok()?,
message: buf.try_get_string().ok()?, message: buf.try_get_string().ok()?,
}, },
id => {
println!("unknown packet id: {id:0x}"); id => Self::Extended(ExtendedClientPacket::read(id, buf)?),
return None;
}
}) })
} }
// only needed on the client, so disabled for now
// /// writes the packet
// pub fn write(&self, writer: super::PacketWriter) -> super::PacketWriter {
// match self {
// Self::PlayerIdentification {
// protocol_version,
// username,
// verification_key,
// _unused,
// } => writer
// .write_u8(*protocol_version)
// .write_string(username)
// .write_string(verification_key)
// .write_u8(*_unused),
// Self::SetBlock {
// x,
// y,
// z,
// mode,
// block_type,
// } => writer
// .write_i16(*x)
// .write_i16(*y)
// .write_i16(*z)
// .write_u8(*mode)
// .write_u8(*block_type),
// Self::PositionOrientation {
// player_id,
// x,
// y,
// z,
// yaw,
// pitch,
// } => writer
// .write_i8(*player_id)
// .write_f16(*x)
// .write_f16(*y)
// .write_f16(*z)
// .write_u8(*yaw)
// .write_u8(*pitch),
// Self::Message { player_id, message } => {
// writer.write_i8(*player_id).write_string(message)
// }
// }
// }
} }

View file

@ -0,0 +1,42 @@
use super::{SafeBufExtension, STRING_LENGTH};
/// extended client packets
#[derive(Debug, Clone)]
pub enum ExtendedClientPacket {
/// packet containing the client name and the number of extensions it supports
ExtInfo {
app_name: String,
extension_count: i16,
},
/// packet containing a supported extension name and version
ExtEntry { ext_name: String, version: i32 },
}
impl ExtendedClientPacket {
/// gets the size of the packet from the given id (minus one byte for the id)
pub const fn get_size_from_id(id: u8) -> Option<usize> {
Some(match id {
0x10 => STRING_LENGTH + 2,
0x11 => STRING_LENGTH + 4,
_ => return None,
})
}
/// reads the packet
pub fn read<B>(id: u8, buf: &mut B) -> Option<Self>
where
B: SafeBufExtension,
{
Some(match id {
0x10 => Self::ExtInfo {
app_name: buf.try_get_string().ok()?,
extension_count: buf.try_get_i16().ok()?,
},
0x11 => Self::ExtEntry {
ext_name: buf.try_get_string().ok()?,
version: buf.try_get_i32().ok()?,
},
_ => return None,
})
}
}

View file

@ -1,6 +1,8 @@
use half::f16; use half::f16;
use crate::player::PlayerType; use crate::{level::WeatherType, player::PlayerType, SERVER_NAME};
use super::ExtBitmask;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(unused)] #[allow(unused)]
@ -91,6 +93,14 @@ pub enum ServerPacket {
/// 0x00 for normal, 0x64 for op /// 0x00 for normal, 0x64 for op
user_type: PlayerType, user_type: PlayerType,
}, },
// extension packets
/// packet to send info about the server's extensions
ExtInfo {},
/// packet to send info about an extension on the server
ExtEntry { ext_name: String, version: i32 },
/// informs the client that it should update the current weather
EnvWeatherType { weather_type: WeatherType },
} }
impl ServerPacket { impl ServerPacket {
@ -112,6 +122,10 @@ impl ServerPacket {
Self::Message { .. } => 0x0d, Self::Message { .. } => 0x0d,
Self::DisconnectPlayer { .. } => 0x0e, Self::DisconnectPlayer { .. } => 0x0e,
Self::UpdateUserType { .. } => 0x0f, Self::UpdateUserType { .. } => 0x0f,
Self::ExtInfo {} => 0x10,
Self::ExtEntry { .. } => 0x11,
Self::EnvWeatherType { .. } => 0x1f,
} }
} }
@ -221,6 +235,14 @@ impl ServerPacket {
} }
Self::DisconnectPlayer { disconnect_reason } => writer.write_string(disconnect_reason), Self::DisconnectPlayer { disconnect_reason } => writer.write_string(disconnect_reason),
Self::UpdateUserType { user_type } => writer.write_u8(user_type.into()), Self::UpdateUserType { user_type } => writer.write_u8(user_type.into()),
Self::ExtInfo {} => writer
.write_string(SERVER_NAME)
.write_i16(ExtBitmask::all().all_contained_info().len() as i16),
Self::ExtEntry { ext_name, version } => {
writer.write_string(ext_name).write_i32(*version)
}
Self::EnvWeatherType { weather_type } => writer.write_u8(weather_type.into()),
} }
} }

View file

@ -3,7 +3,7 @@ use std::net::SocketAddr;
use half::f16; use half::f16;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::packet::server::ServerPacket; use crate::packet::{server::ServerPacket, ExtBitmask};
/// struct for players /// struct for players
#[derive(Debug)] #[derive(Debug)]
@ -27,6 +27,8 @@ pub struct Player {
/// the player's IP address /// the player's IP address
pub _addr: SocketAddr, pub _addr: SocketAddr,
/// the player's supported extensions
pub extensions: ExtBitmask,
/// queue of packets to be sent to this player /// queue of packets to be sent to this player
pub packets_to_send: Vec<ServerPacket>, pub packets_to_send: Vec<ServerPacket>,
/// whether this player should be kicked and the message to give /// whether this player should be kicked and the message to give

View file

@ -19,7 +19,7 @@ use crate::{
use self::config::ServerConfig; use self::config::ServerConfig;
const TICK_DURATION: std::time::Duration = std::time::Duration::from_millis(50); const TICK_DURATION: std::time::Duration = std::time::Duration::from_millis(50);
const LEVEL_PATH: &str = "level.clw"; const LEVELS_PATH: &str = "levels";
/// the server /// the server
#[derive(Debug)] #[derive(Debug)]
@ -59,9 +59,13 @@ impl ServerData {
impl Server { impl Server {
/// creates a new server with a generated level /// creates a new server with a generated level
pub async fn new(config: ServerConfig) -> std::io::Result<Self> { pub async fn new(config: ServerConfig) -> std::io::Result<Self> {
let level_path = PathBuf::from(LEVEL_PATH); let levels_path = PathBuf::from(LEVELS_PATH);
if !levels_path.exists() {
std::fs::create_dir_all(&levels_path)?;
}
let level_path = levels_path.join(&config.level_name);
let level = if level_path.exists() { let level = if level_path.exists() {
Level::load(level_path).await Level::load(level_path).await?
} else { } else {
println!("generating level"); println!("generating level");
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
@ -71,6 +75,7 @@ impl Server {
config.level_size.z, config.level_size.z,
); );
config.generation.generate(&mut level, &mut rng); config.generation.generate(&mut level, &mut rng);
level.save(level_path).await?;
println!("done!"); println!("done!");
level level
}; };
@ -104,9 +109,7 @@ impl Server {
println!("connection from {addr}"); println!("connection from {addr}");
let data = data.clone(); let data = data.clone();
tokio::spawn(async move { tokio::spawn(async move {
network::handle_stream(stream, addr, data) network::handle_stream(stream, addr, data).await;
.await
.expect("failed to handle client stream");
}); });
} }
}); });
@ -116,7 +119,10 @@ impl Server {
// TODO: cancel pending tasks/send out "Server is stopping" messages *here* instead of elsewhere // TODO: cancel pending tasks/send out "Server is stopping" messages *here* instead of elsewhere
// rn the message isn't guaranteed to actually go out........ // rn the message isn't guaranteed to actually go out........
self.data.read().await.level.save(LEVEL_PATH).await; let data = self.data.read().await;
data.level
.save(PathBuf::from(LEVELS_PATH).join(&data.config.level_name))
.await?;
Ok(()) Ok(())
} }
@ -154,7 +160,10 @@ async fn handle_ticks(data: Arc<RwLock<ServerData>>) {
if data.config.auto_save_minutes != 0 if data.config.auto_save_minutes != 0
&& last_auto_save.elapsed().as_secs() / 60 >= data.config.auto_save_minutes && last_auto_save.elapsed().as_secs() / 60 >= data.config.auto_save_minutes
{ {
data.level.save(LEVEL_PATH).await; data.level
.save(PathBuf::from(LEVELS_PATH).join(&data.config.level_name))
.await
.expect("failed to autosave level");
last_auto_save = std::time::Instant::now(); last_auto_save = std::time::Instant::now();
} }
} }

View file

@ -18,6 +18,8 @@ pub struct ServerConfig {
pub protection_mode: ServerProtectionMode, pub protection_mode: ServerProtectionMode,
/// map of user permissions /// map of user permissions
pub player_perms: BTreeMap<String, PlayerType>, pub player_perms: BTreeMap<String, PlayerType>,
/// the level's name
pub level_name: String,
/// the level's size /// the level's size
pub level_size: ConfigCoordinates, pub level_size: ConfigCoordinates,
/// the level's spawn point /// the level's spawn point
@ -42,6 +44,7 @@ impl Default for ServerConfig {
motd: "here's the default server motd".to_string(), motd: "here's the default server motd".to_string(),
protection_mode: ServerProtectionMode::None, protection_mode: ServerProtectionMode::None,
player_perms: Default::default(), player_perms: Default::default(),
level_name: "default".to_string(),
level_size: ConfigCoordinates { level_size: ConfigCoordinates {
x: 256, x: 256,
y: 64, y: 64,

View file

@ -1,10 +1,12 @@
use std::{collections::VecDeque, io::Write, net::SocketAddr, sync::Arc}; mod extensions;
use std::{io::Write, net::SocketAddr, sync::Arc};
use bytes::BytesMut; use bytes::BytesMut;
use flate2::{write::GzEncoder, Compression}; use flate2::{write::GzEncoder, Compression};
use half::f16; use half::f16;
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt, Interest}, io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream, net::TcpStream,
sync::RwLock, sync::RwLock,
}; };
@ -12,21 +14,50 @@ use tokio::{
use crate::{ use crate::{
command::Command, command::Command,
level::{block::BLOCK_INFO, BlockUpdate, Level}, level::{block::BLOCK_INFO, BlockUpdate, Level},
packet::{client::ClientPacket, server::ServerPacket, PacketWriter, ARRAY_LENGTH}, packet::{
client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH,
EXTENSION_MAGIC_NUMBER,
},
player::{Player, PlayerType}, player::{Player, PlayerType},
server::config::ServerProtectionMode, server::config::ServerProtectionMode,
}; };
use super::ServerData; use super::ServerData;
async fn next_packet(stream: &mut TcpStream) -> std::io::Result<Option<ClientPacket>> {
let id = stream.read_u8().await?;
if let Some(size) = ClientPacket::get_size_from_id(id) {
let mut buf = BytesMut::zeroed(size);
stream.read_exact(&mut buf).await?;
Ok(ClientPacket::read(id, &mut buf))
} else {
println!("unknown packet id: {id:0x}");
Ok(None)
}
}
async fn write_packets<I>(stream: &mut TcpStream, packets: I) -> std::io::Result<()>
where
I: Iterator<Item = ServerPacket>,
{
for packet in packets {
let writer = PacketWriter::default().write_u8(packet.get_id());
let msg = packet.write(writer).into_raw_packet();
stream.write_all(&msg).await?;
}
Ok(())
}
pub(super) async fn handle_stream( pub(super) async fn handle_stream(
mut stream: TcpStream, mut stream: TcpStream,
addr: SocketAddr, addr: SocketAddr,
data: Arc<RwLock<ServerData>>, data: Arc<RwLock<ServerData>>,
) -> std::io::Result<()> { ) {
let mut own_id: i8 = -1; let mut own_id: i8 = -1;
let r = handle_stream_inner(&mut stream, addr, data.clone(), &mut own_id).await; let r = handle_stream_inner(&mut stream, addr, data.clone(), &mut own_id).await;
println!("{addr} is no longer connected");
match r { match r {
Ok(disconnect_reason) => { Ok(disconnect_reason) => {
if let Some(disconnect_reason) = disconnect_reason { if let Some(disconnect_reason) = disconnect_reason {
@ -38,7 +69,12 @@ pub(super) async fn handle_stream(
} }
} }
} }
Err(e) => eprintln!("Error in stream handler for <{addr}>: {e}"), Err(e) => {
// unexpected eof is expected when clients disconnect
if e.kind() != std::io::ErrorKind::UnexpectedEof {
eprintln!("Error in stream handler for <{addr}>: {e}")
}
}
} }
if let Err(e) = stream.shutdown().await { if let Err(e) = stream.shutdown().await {
@ -60,8 +96,6 @@ pub(super) async fn handle_stream(
player.packets_to_send.push(message_packet.clone()); player.packets_to_send.push(message_packet.clone());
} }
} }
Ok(())
} }
async fn handle_stream_inner( async fn handle_stream_inner(
@ -70,13 +104,11 @@ async fn handle_stream_inner(
data: Arc<RwLock<ServerData>>, data: Arc<RwLock<ServerData>>,
own_id: &mut i8, own_id: &mut i8,
) -> std::io::Result<Option<String>> { ) -> std::io::Result<Option<String>> {
let mut reply_queue: VecDeque<ServerPacket> = VecDeque::new(); let mut reply_queue: Vec<ServerPacket> = Vec::new();
let mut read_buf;
let mut id_buf;
macro_rules! msg { macro_rules! msg {
($message:expr) => { ($message:expr) => {
reply_queue.push_back(ServerPacket::Message { reply_queue.push(ServerPacket::Message {
player_id: -1, player_id: -1,
message: $message, message: $message,
}); });
@ -90,33 +122,13 @@ async fn handle_stream_inner(
} }
} }
let ready = stream if let Some(packet) = next_packet(stream).await? {
.ready(Interest::READABLE | Interest::WRITABLE) match packet {
.await?;
if ready.is_read_closed() {
println!("disconnecting {addr}");
break;
}
if ready.is_readable() {
id_buf = [0u8];
match stream.try_read(&mut id_buf) {
Ok(n) => {
if n == 1 {
if let Some(size) = ClientPacket::get_size_from_id(id_buf[0]) {
read_buf = BytesMut::zeroed(size);
stream.read_exact(&mut read_buf).await?;
match ClientPacket::read(id_buf[0], &mut read_buf)
.expect("should never fail: id already checked")
{
ClientPacket::PlayerIdentification { ClientPacket::PlayerIdentification {
protocol_version, protocol_version,
username, username,
verification_key, verification_key,
_unused, magic_number,
} => { } => {
if protocol_version != 0x07 { if protocol_version != 0x07 {
return Ok(Some("Unknown protocol version! Please connect with a classic 0.30-compatible client.".to_string())); return Ok(Some("Unknown protocol version! Please connect with a classic 0.30-compatible client.".to_string()));
@ -146,10 +158,7 @@ async fn handle_stream_inner(
for player in &data.players { for player in &data.players {
if player.username == username { if player.username == username {
return Ok(Some( return Ok(Some("Player with username already connected!".to_string()));
"Player with username already connected!"
.to_string(),
));
} }
} }
@ -175,11 +184,16 @@ async fn handle_stream_inner(
yaw: 0, yaw: 0,
pitch: 0, pitch: 0,
permissions: player_type, permissions: player_type,
extensions: ExtBitmask::none(),
packets_to_send: Vec::new(), packets_to_send: Vec::new(),
should_be_kicked: None, should_be_kicked: None,
}; };
reply_queue.push_back(ServerPacket::ServerIdentification { if magic_number == EXTENSION_MAGIC_NUMBER {
player.extensions = extensions::get_supported_extensions(stream).await?;
}
reply_queue.push(ServerPacket::ServerIdentification {
protocol_version: 0x07, protocol_version: 0x07,
server_name: data.config.name.clone(), server_name: data.config.name.clone(),
server_motd: data.config.motd.clone(), server_motd: data.config.motd.clone(),
@ -187,8 +201,13 @@ async fn handle_stream_inner(
}); });
println!("generating level packets"); println!("generating level packets");
reply_queue reply_queue.extend(build_level_packets(&data.level).into_iter());
.extend(build_level_packets(&data.level).into_iter());
if player.extensions.contains(ExtBitmask::EnvWeatherType) {
reply_queue.push(ServerPacket::EnvWeatherType {
weather_type: data.level.weather,
});
}
let username = player.username.clone(); let username = player.username.clone();
@ -228,7 +247,7 @@ async fn handle_stream_inner(
for player in &mut data.players { for player in &mut data.players {
player.packets_to_send.push(spawn_packet.clone()); player.packets_to_send.push(spawn_packet.clone());
if player.id != *own_id { if player.id != *own_id {
reply_queue.push_back(ServerPacket::SpawnPlayer { reply_queue.push(ServerPacket::SpawnPlayer {
player_id: player.id, player_id: player.id,
player_name: player.username.clone(), player_name: player.username.clone(),
x: player.x, x: player.x,
@ -241,7 +260,7 @@ async fn handle_stream_inner(
} }
} }
msg!("&dWelcome to the server! Enjoyyyyyy".to_string()); msg!("&dWelcome to the server! Enjoyyyyyy".to_string());
reply_queue.push_back(ServerPacket::UpdateUserType { reply_queue.push(ServerPacket::UpdateUserType {
user_type: PlayerType::Operator, user_type: PlayerType::Operator,
}); });
} }
@ -260,9 +279,7 @@ async fn handle_stream_inner(
|| y.clamp(0, data.level.y_size as i16 - 1) != y || y.clamp(0, data.level.y_size as i16 - 1) != y
|| z.clamp(0, data.level.z_size as i16 - 1) != z || z.clamp(0, data.level.z_size as i16 - 1) != z
{ {
return Ok(Some( return Ok(Some("Attempt to place block out of bounds".to_string()));
"Attempt to place block out of bounds".to_string(),
));
} }
let new_block_info = BLOCK_INFO.get(&block_type); let new_block_info = BLOCK_INFO.get(&block_type);
@ -272,8 +289,7 @@ async fn handle_stream_inner(
} }
let new_block_info = new_block_info.expect("will never fail"); let new_block_info = new_block_info.expect("will never fail");
let mut cancel = false; 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
.get(&block) .get(&block)
.expect("missing block information for block!"); .expect("missing block information for block!");
@ -293,7 +309,7 @@ async fn handle_stream_inner(
} }
if cancel { if cancel {
reply_queue.push_back(ServerPacket::SetBlock { reply_queue.push(ServerPacket::SetBlock {
x, x,
y, y,
z, z,
@ -365,25 +381,27 @@ async fn handle_stream_inner(
.expect("should never fail") .expect("should never fail")
.username .username
); );
data.spread_packet(ServerPacket::Message { data.spread_packet(ServerPacket::Message { player_id, message });
player_id, }
message, }
});
} ClientPacket::Extended(_packet) => {
} // extended packets!
} return Ok(Some(
} else { "Unexpected extension packet in this phase!".to_string(),
println!("unknown packet id: {}", id_buf[0]); ));
} // match packet {
} // packet => {
} // println!("improper client packet for this phase!: {packet:#?}");
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, // return Ok(Some(
Err(e) => return Err(e), // "Client sent invalid packet for this phase".to_string(),
// ));
// }
// }
}
} }
} }
if ready.is_writable() {
{
let mut data = data.write().await; let mut data = data.write().await;
if let Some(player) = data.players.iter_mut().find(|p| p.id == *own_id) { if let Some(player) = data.players.iter_mut().find(|p| p.id == *own_id) {
for mut packet in player.packets_to_send.drain(..) { for mut packet in player.packets_to_send.drain(..) {
@ -395,23 +413,13 @@ async fn handle_stream_inner(
packet.set_player_id(-1); packet.set_player_id(-1);
} }
} }
reply_queue.push_back(packet); reply_queue.push(packet);
}
} }
} }
while let Some(packet) = reply_queue.pop_front() { write_packets(stream, reply_queue.drain(..)).await?;
let writer = PacketWriter::default().write_u8(packet.get_id());
let msg = packet.write(writer).into_raw_packet();
stream.write_all(&msg).await?;
} }
} }
}
println!("remaining packets: {}", reply_queue.len());
Ok(None)
}
/// helper to put together packets that need to be sent to send full level data for the given level /// helper to put together packets that need to be sent to send full level data for the given level
fn build_level_packets(level: &Level) -> Vec<ServerPacket> { fn build_level_packets(level: &Level) -> Vec<ServerPacket> {

View file

@ -0,0 +1,65 @@
use tokio::net::TcpStream;
use crate::packet::{
client::ClientPacket, client_extended::ExtendedClientPacket, server::ServerPacket, ExtBitmask,
ExtInfo,
};
use super::{next_packet, write_packets};
pub async fn get_supported_extensions(stream: &mut TcpStream) -> std::io::Result<ExtBitmask> {
let extensions = ExtBitmask::all().all_contained_info();
write_packets(
stream,
Some(ServerPacket::ExtInfo {})
.into_iter()
.chain(extensions.iter().map(|info| ServerPacket::ExtEntry {
ext_name: info.ext_name.to_string(),
version: info.version,
})),
)
.await?;
let client_extensions = if let Some(ClientPacket::Extended(ExtendedClientPacket::ExtInfo {
app_name,
extension_count,
})) = next_packet(stream).await?
{
println!("client name: {app_name}");
let mut client_extensions = Vec::with_capacity(extension_count as usize);
for _ in 0..extension_count {
if let Some(ClientPacket::Extended(ExtendedClientPacket::ExtEntry {
ext_name,
version,
})) = next_packet(stream).await?
{
client_extensions.push(ExtInfo::new(ext_name, version, ExtBitmask::none()));
} else {
panic!("expected ExtEntry packet!");
}
}
client_extensions.retain_mut(|cext| {
if let Some(sext) = extensions
.iter()
.find(|sext| sext.ext_name == cext.ext_name && sext.version == cext.version)
{
cext.bitmask = sext.bitmask;
true
} else {
false
}
});
client_extensions
} else {
Vec::new()
};
println!("mutual extensions: {client_extensions:?}");
let final_bitmask = client_extensions
.into_iter()
.fold(ExtBitmask::none(), |acc, ext| acc | ext.bitmask);
Ok(final_bitmask)
}