diff --git a/.gitignore b/.gitignore index 3eb7548..b27c5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /target server-config.json .DS_Store -*.clw +/levels *.cw diff --git a/Cargo.lock b/Cargo.lock index c866dcc..62ca920 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,6 +81,16 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "bytes" version = "1.6.0" @@ -104,6 +114,7 @@ name = "classics" version = "0.1.0" dependencies = [ "bincode", + "bitmask-enum", "bytes", "flate2", "half", diff --git a/Cargo.toml b/Cargo.toml index 5a1e361..b5436dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ version = "0.1.0" [dependencies] bincode = "2.0.0-rc.3" +bitmask-enum = "2" bytes = "1" flate2 = "1" half = "2" diff --git a/src/command.rs b/src/command.rs index 7360fe8..40c50a8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -17,6 +17,7 @@ const CMD_BAN: &str = "ban"; const CMD_ALLOWENTRY: &str = "allowentry"; const CMD_SETPASS: &str = "setpass"; const CMD_SETLEVELSPAWN: &str = "setlevelspawn"; +const CMD_WEATHER: &str = "weather"; /// list of commands available on the server pub const COMMANDS_LIST: &[&str] = &[ @@ -30,6 +31,7 @@ pub const COMMANDS_LIST: &[&str] = &[ CMD_ALLOWENTRY, CMD_SETPASS, CMD_SETLEVELSPAWN, + CMD_WEATHER, ]; /// enum for possible commands @@ -69,6 +71,8 @@ pub enum Command<'m> { SetPass { password: &'m str }, /// sets the level spawn to the player's location SetLevelSpawn, + /// changes the levels weather + Weather { weather_type: &'m str }, } impl<'m> Command<'m> { @@ -117,6 +121,9 @@ impl<'m> Command<'m> { password: arguments.trim(), }, CMD_SETLEVELSPAWN => Self::SetLevelSpawn, + CMD_WEATHER => Self::Weather { + weather_type: arguments, + }, _ => return Err(format!("Unknown command: {command_name}")), }) } @@ -134,6 +141,7 @@ impl<'m> Command<'m> { Self::AllowEntry { .. } => CMD_ALLOWENTRY, Self::SetPass { .. } => CMD_SETPASS, Self::SetLevelSpawn => CMD_SETLEVELSPAWN, + Self::Weather { .. } => CMD_WEATHER, } } @@ -195,6 +203,10 @@ impl<'m> Command<'m> { c(""), "&fSets the level's spawn to your location.".to_string(), ], + CMD_WEATHER => vec![ + c(""), + "&fSets the level's weather.".to_string(), + ], _ => vec!["&eUnknown command!".to_string()], } } @@ -459,6 +471,19 @@ impl<'m> Command<'m> { data.config_needs_saving = true; 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 diff --git a/src/level.rs b/src/level.rs index 5c8a953..b595a2d 100644 --- a/src/level.rs +++ b/src/level.rs @@ -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}; @@ -9,8 +13,11 @@ use self::block::BLOCK_INFO; pub mod block; pub mod generation; +const LEVEL_INFO_PATH: &str = "info.json"; +const LEVEL_DATA_PATH: &str = "level.dat"; + /// a classic level -#[derive(Debug, Clone, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Level { /// the size of the level in the X direction pub x_size: usize, @@ -20,10 +27,15 @@ pub struct Level { pub z_size: usize, /// the blocks which make up the level + #[serde(skip)] pub blocks: Vec, + /// the level's weather + pub weather: WeatherType, + /// 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 + #[serde(skip)] pub updates: Vec, } @@ -35,6 +47,7 @@ impl Level { y_size, z_size, blocks: vec![0; x_size * y_size * z_size], + weather: WeatherType::Sunny, awaiting_update: Default::default(), updates: Default::default(), } @@ -91,32 +104,104 @@ impl Level { packets } - pub async fn save

(&self, path: P) + /// saves the level + pub async fn save

(&self, path: P) -> std::io::Result<()> where P: AsRef, { + 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()); - bincode::encode_into_std_write(self, &mut encoder, bincode::config::standard()).unwrap(); - tokio::fs::write(path, encoder.finish().unwrap()) - .await - .unwrap(); + encoder + .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 } - pub async fn load

(path: P) -> Self + /// loads the level + pub async fn load

(path: P) -> std::io::Result where P: AsRef, { - let data = tokio::fs::read(path).await.unwrap(); - let mut decoder = flate2::read::GzDecoder::new(data.as_slice()); - bincode::decode_from_std_read(&mut decoder, bincode::config::standard()).unwrap() + let path = path.as_ref(); + let mut info: Self = + 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 -#[derive(Debug, Clone, Encode, Decode)] +#[derive(Debug, Clone, Serialize, Deserialize)] 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, } + +/// 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 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 { + Ok(match value { + "sunny" => Self::Sunny, + "raining" => Self::Raining, + "snowing" => Self::Snowing, + _ => return Err(()), + }) + } +} diff --git a/src/main.rs b/src/main.rs index 64222ed..e8ff4ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod player; mod server; mod util; +const SERVER_NAME: &str = "classics"; const CONFIG_FILE: &str = "./server-config.json"; #[tokio::main] diff --git a/src/packet.rs b/src/packet.rs index 856412c..7e49e95 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -2,6 +2,7 @@ use half::f16; use safer_bytes::{error::Truncated, SafeBuf}; pub mod client; +pub mod client_extended; pub mod server; /// length of classic strings @@ -10,6 +11,30 @@ pub const STRING_LENGTH: usize = 64; pub const ARRAY_LENGTH: usize = 1024; /// units in an f16 unit 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 pub trait SafeBufExtension: SafeBuf { @@ -80,6 +105,15 @@ impl PacketWriter { 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 fn write_string(self, str: &str) -> Self { let mut s = self; @@ -109,3 +143,103 @@ impl PacketWriter { 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 { + // 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 { + [ + 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() + } +} diff --git a/src/packet/client.rs b/src/packet/client.rs index 22e338e..618438a 100644 --- a/src/packet/client.rs +++ b/src/packet/client.rs @@ -1,6 +1,6 @@ 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 #[derive(Debug, Clone)] @@ -10,10 +10,10 @@ pub enum ClientPacket { /// should always be 0x07 for classic clients >= 0.28 protocol_version: u8, username: String, - /// currently unverified, original minecraft auth for classic is gone anyway - /// TODO: use verification key field as password protection? investigate + /// used as the password the client sends to the server 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 /// 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, message: String, }, + + // extension packets + Extended(ExtendedClientPacket), } 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) pub const fn get_size_from_id(id: u8) -> Option { Some(match id { @@ -62,7 +54,7 @@ impl ClientPacket { 0x05 => 2 + 2 + 2 + 1 + 1, 0x08 => 1 + 2 + 2 + 2 + 1 + 1, 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()?, username: 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 { x: buf.try_get_i16().ok()?, @@ -97,56 +89,8 @@ impl ClientPacket { player_id: buf.try_get_i8().ok()?, message: buf.try_get_string().ok()?, }, - id => { - println!("unknown packet id: {id:0x}"); - return None; - } + + id => Self::Extended(ExtendedClientPacket::read(id, buf)?), }) } - - // 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) - // } - // } - // } } diff --git a/src/packet/client_extended.rs b/src/packet/client_extended.rs new file mode 100644 index 0000000..0b1b477 --- /dev/null +++ b/src/packet/client_extended.rs @@ -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 { + Some(match id { + 0x10 => STRING_LENGTH + 2, + 0x11 => STRING_LENGTH + 4, + _ => return None, + }) + } + + /// reads the packet + pub fn read(id: u8, buf: &mut B) -> Option + 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, + }) + } +} diff --git a/src/packet/server.rs b/src/packet/server.rs index 578bd48..f24b719 100644 --- a/src/packet/server.rs +++ b/src/packet/server.rs @@ -1,6 +1,8 @@ use half::f16; -use crate::player::PlayerType; +use crate::{level::WeatherType, player::PlayerType, SERVER_NAME}; + +use super::ExtBitmask; #[derive(Debug, Clone)] #[allow(unused)] @@ -91,6 +93,14 @@ pub enum ServerPacket { /// 0x00 for normal, 0x64 for op 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 { @@ -112,6 +122,10 @@ impl ServerPacket { Self::Message { .. } => 0x0d, Self::DisconnectPlayer { .. } => 0x0e, 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::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()), } } diff --git a/src/player.rs b/src/player.rs index 00ca727..7f5ad00 100644 --- a/src/player.rs +++ b/src/player.rs @@ -3,7 +3,7 @@ use std::net::SocketAddr; use half::f16; use serde::{Deserialize, Serialize}; -use crate::packet::server::ServerPacket; +use crate::packet::{server::ServerPacket, ExtBitmask}; /// struct for players #[derive(Debug)] @@ -27,6 +27,8 @@ pub struct Player { /// the player's IP address pub _addr: SocketAddr, + /// the player's supported extensions + pub extensions: ExtBitmask, /// queue of packets to be sent to this player pub packets_to_send: Vec, /// whether this player should be kicked and the message to give diff --git a/src/server.rs b/src/server.rs index 4f51b7a..cf36ec7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -19,7 +19,7 @@ use crate::{ use self::config::ServerConfig; 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 #[derive(Debug)] @@ -59,9 +59,13 @@ impl ServerData { impl Server { /// creates a new server with a generated level pub async fn new(config: ServerConfig) -> std::io::Result { - 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() { - Level::load(level_path).await + Level::load(level_path).await? } else { println!("generating level"); let mut rng = rand::thread_rng(); @@ -71,6 +75,7 @@ impl Server { config.level_size.z, ); config.generation.generate(&mut level, &mut rng); + level.save(level_path).await?; println!("done!"); level }; @@ -104,9 +109,7 @@ impl Server { println!("connection from {addr}"); let data = data.clone(); tokio::spawn(async move { - network::handle_stream(stream, addr, data) - .await - .expect("failed to handle client stream"); + network::handle_stream(stream, addr, data).await; }); } }); @@ -116,7 +119,10 @@ impl Server { // TODO: cancel pending tasks/send out "Server is stopping" messages *here* instead of elsewhere // 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(()) } @@ -154,7 +160,10 @@ async fn handle_ticks(data: Arc>) { if data.config.auto_save_minutes != 0 && 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(); } } diff --git a/src/server/config.rs b/src/server/config.rs index 60fe4bf..c226153 100644 --- a/src/server/config.rs +++ b/src/server/config.rs @@ -18,6 +18,8 @@ pub struct ServerConfig { pub protection_mode: ServerProtectionMode, /// map of user permissions pub player_perms: BTreeMap, + /// the level's name + pub level_name: String, /// the level's size pub level_size: ConfigCoordinates, /// the level's spawn point @@ -42,6 +44,7 @@ impl Default for ServerConfig { motd: "here's the default server motd".to_string(), protection_mode: ServerProtectionMode::None, player_perms: Default::default(), + level_name: "default".to_string(), level_size: ConfigCoordinates { x: 256, y: 64, diff --git a/src/server/network.rs b/src/server/network.rs index a398b18..40728b4 100644 --- a/src/server/network.rs +++ b/src/server/network.rs @@ -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 flate2::{write::GzEncoder, Compression}; use half::f16; use tokio::{ - io::{AsyncReadExt, AsyncWriteExt, Interest}, + io::{AsyncReadExt, AsyncWriteExt}, net::TcpStream, sync::RwLock, }; @@ -12,21 +14,50 @@ use tokio::{ use crate::{ command::Command, 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}, server::config::ServerProtectionMode, }; use super::ServerData; +async fn next_packet(stream: &mut TcpStream) -> std::io::Result> { + 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(stream: &mut TcpStream, packets: I) -> std::io::Result<()> +where + I: Iterator, +{ + 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( mut stream: TcpStream, addr: SocketAddr, data: Arc>, -) -> std::io::Result<()> { +) { let mut own_id: i8 = -1; let r = handle_stream_inner(&mut stream, addr, data.clone(), &mut own_id).await; + println!("{addr} is no longer connected"); match r { Ok(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 { @@ -60,8 +96,6 @@ pub(super) async fn handle_stream( player.packets_to_send.push(message_packet.clone()); } } - - Ok(()) } async fn handle_stream_inner( @@ -70,13 +104,11 @@ async fn handle_stream_inner( data: Arc>, own_id: &mut i8, ) -> std::io::Result> { - let mut reply_queue: VecDeque = VecDeque::new(); - let mut read_buf; - let mut id_buf; + let mut reply_queue: Vec = Vec::new(); macro_rules! msg { ($message:expr) => { - reply_queue.push_back(ServerPacket::Message { + reply_queue.push(ServerPacket::Message { player_id: -1, message: $message, }); @@ -90,327 +122,303 @@ async fn handle_stream_inner( } } - let ready = stream - .ready(Interest::READABLE | Interest::WRITABLE) - .await?; + if let Some(packet) = next_packet(stream).await? { + match packet { + ClientPacket::PlayerIdentification { + protocol_version, + username, + verification_key, + magic_number, + } => { + if protocol_version != 0x07 { + return Ok(Some("Unknown protocol version! Please connect with a classic 0.30-compatible client.".to_string())); + } - if ready.is_read_closed() { - println!("disconnecting {addr}"); - break; - } + let zero = f16::from_f32(0.0); - 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); + let mut data = data.write().await; - stream.read_exact(&mut read_buf).await?; - - match ClientPacket::read(id_buf[0], &mut read_buf) - .expect("should never fail: id already checked") + match &data.config.protection_mode { + ServerProtectionMode::None => {} + ServerProtectionMode::Password(password) => { + if verification_key != *password { + return Ok(Some("Incorrect password!".to_string())); + } + } + ServerProtectionMode::PasswordsByUser(passwords) => { + if !passwords + .get(&username) + .map(|password| verification_key == *password) + .unwrap_or_default() { - ClientPacket::PlayerIdentification { - protocol_version, - username, - verification_key, - _unused, - } => { - if protocol_version != 0x07 { - return Ok(Some("Unknown protocol version! Please connect with a classic 0.30-compatible client.".to_string())); - } - - let zero = f16::from_f32(0.0); - - let mut data = data.write().await; - - match &data.config.protection_mode { - ServerProtectionMode::None => {} - ServerProtectionMode::Password(password) => { - if verification_key != *password { - return Ok(Some("Incorrect password!".to_string())); - } - } - ServerProtectionMode::PasswordsByUser(passwords) => { - if !passwords - .get(&username) - .map(|password| verification_key == *password) - .unwrap_or_default() - { - return Ok(Some("Incorrect password!".to_string())); - } - } - } - - for player in &data.players { - if player.username == username { - return Ok(Some( - "Player with username already connected!" - .to_string(), - )); - } - } - - *own_id = data - .free_player_ids - .pop() - .unwrap_or_else(|| data.players.len() as i8); - - let player_type = data - .config - .player_perms - .get(&username) - .copied() - .unwrap_or_default(); - - let mut player = Player { - _addr: addr, - id: *own_id, // TODO: actually assign user ids - username, - x: zero, - y: zero, - z: zero, - yaw: 0, - pitch: 0, - permissions: player_type, - packets_to_send: Vec::new(), - should_be_kicked: None, - }; - - reply_queue.push_back(ServerPacket::ServerIdentification { - protocol_version: 0x07, - server_name: data.config.name.clone(), - server_motd: data.config.motd.clone(), - user_type: player_type, - }); - - println!("generating level packets"); - reply_queue - .extend(build_level_packets(&data.level).into_iter()); - - let username = player.username.clone(); - - let (spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) = - if let Some(spawn) = &data.config.spawn { - (spawn.x, spawn.y, spawn.z, spawn.yaw, spawn.pitch) - } else { - (16.5, (data.level.y_size / 2 + 2) as f32, 16.5, 0, 0) - }; - - let (spawn_x, spawn_y, spawn_z) = ( - f16::from_f32(spawn_x), - f16::from_f32(spawn_y), - f16::from_f32(spawn_z), - ); - - player.x = spawn_x; - player.y = spawn_y; - player.z = spawn_z; - player.yaw = spawn_yaw; - player.pitch = spawn_pitch; - data.players.push(player); - - let spawn_packet = ServerPacket::SpawnPlayer { - player_id: *own_id, - player_name: username.clone(), - x: spawn_x, - y: spawn_y, - z: spawn_z, - yaw: spawn_yaw, - pitch: spawn_pitch, - }; - let message_packet = ServerPacket::Message { - player_id: *own_id, - message: format!("&e{} has joined the server.", username), - }; - for player in &mut data.players { - player.packets_to_send.push(spawn_packet.clone()); - if player.id != *own_id { - reply_queue.push_back(ServerPacket::SpawnPlayer { - player_id: player.id, - player_name: player.username.clone(), - x: player.x, - y: player.y, - z: player.z, - yaw: player.yaw, - pitch: player.pitch, - }); - player.packets_to_send.push(message_packet.clone()); - } - } - msg!("&dWelcome to the server! Enjoyyyyyy".to_string()); - reply_queue.push_back(ServerPacket::UpdateUserType { - user_type: PlayerType::Operator, - }); - } - ClientPacket::SetBlock { - x, - y, - z, - mode, - block_type, - } => { - let block_type = if mode == 0x00 { 0 } else { block_type }; - let mut data = data.write().await; - - // kick players if they attempt to place a block out of bounds - if x.clamp(0, data.level.x_size as i16 - 1) != x - || y.clamp(0, data.level.y_size as i16 - 1) != y - || z.clamp(0, data.level.z_size as i16 - 1) != z - { - return Ok(Some( - "Attempt to place block out of bounds".to_string(), - )); - } - - let new_block_info = BLOCK_INFO.get(&block_type); - if new_block_info.is_none() { - msg!(format!("&cUnknown 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 - .get(&block) - .expect("missing block information for block!"); - - // check if player has ability to place/break these blocks - let player_type = data - .players - .iter() - .find_map(|p| (p.id == *own_id).then_some(p.permissions)) - .unwrap_or_default(); - if player_type < new_block_info.place_permissions { - cancel = true; - msg!("&cNot allow to place this block.".to_string()); - } else if player_type < block_info.break_permissions { - cancel = true; - msg!("&cNot allowed to break this block.".to_string()); - } - - if cancel { - reply_queue.push_back(ServerPacket::SetBlock { - x, - y, - z, - block_type: block, - }); - continue; - } - 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 { - _player_id: _, - x, - y, - z, - yaw, - pitch, - } => { - let mut data = data.write().await; - - let player = data - .players - .iter_mut() - .find(|p| p.id == *own_id) - .expect("missing player"); - player.x = x; - player.y = y; - player.z = z; - player.yaw = yaw; - player.pitch = pitch; - - data.spread_packet(ServerPacket::SetPositionOrientation { - player_id: *own_id, - x, - y, - z, - yaw, - pitch, - }); - } - ClientPacket::Message { player_id, message } => { - let mut data = data.write().await; - - if let Some(message) = message.strip_prefix(Command::PREFIX) { - match Command::parse(message) { - Ok(cmd) => { - for message in cmd.process(&mut data, *own_id) { - msg!(message); - } - } - Err(msg) => { - msg!(format!("&c{msg}")); - } - } - } else { - println!("{message}"); - let message = format!( - "&f<{}> {message}", - data.players - .iter() - .find(|p| p.id == *own_id) - .expect("should never fail") - .username - ); - data.spread_packet(ServerPacket::Message { - player_id, - message, - }); - } - } + return Ok(Some("Incorrect password!".to_string())); } + } + } + + for player in &data.players { + if player.username == username { + return Ok(Some("Player with username already connected!".to_string())); + } + } + + *own_id = data + .free_player_ids + .pop() + .unwrap_or_else(|| data.players.len() as i8); + + let player_type = data + .config + .player_perms + .get(&username) + .copied() + .unwrap_or_default(); + + let mut player = Player { + _addr: addr, + id: *own_id, // TODO: actually assign user ids + username, + x: zero, + y: zero, + z: zero, + yaw: 0, + pitch: 0, + permissions: player_type, + extensions: ExtBitmask::none(), + packets_to_send: Vec::new(), + should_be_kicked: None, + }; + + if magic_number == EXTENSION_MAGIC_NUMBER { + player.extensions = extensions::get_supported_extensions(stream).await?; + } + + reply_queue.push(ServerPacket::ServerIdentification { + protocol_version: 0x07, + server_name: data.config.name.clone(), + server_motd: data.config.motd.clone(), + user_type: player_type, + }); + + println!("generating level packets"); + reply_queue.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 (spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) = + if let Some(spawn) = &data.config.spawn { + (spawn.x, spawn.y, spawn.z, spawn.yaw, spawn.pitch) } else { - println!("unknown packet id: {}", id_buf[0]); + (16.5, (data.level.y_size / 2 + 2) as f32, 16.5, 0, 0) + }; + + let (spawn_x, spawn_y, spawn_z) = ( + f16::from_f32(spawn_x), + f16::from_f32(spawn_y), + f16::from_f32(spawn_z), + ); + + player.x = spawn_x; + player.y = spawn_y; + player.z = spawn_z; + player.yaw = spawn_yaw; + player.pitch = spawn_pitch; + data.players.push(player); + + let spawn_packet = ServerPacket::SpawnPlayer { + player_id: *own_id, + player_name: username.clone(), + x: spawn_x, + y: spawn_y, + z: spawn_z, + yaw: spawn_yaw, + pitch: spawn_pitch, + }; + let message_packet = ServerPacket::Message { + player_id: *own_id, + message: format!("&e{} has joined the server.", username), + }; + for player in &mut data.players { + player.packets_to_send.push(spawn_packet.clone()); + if player.id != *own_id { + reply_queue.push(ServerPacket::SpawnPlayer { + player_id: player.id, + player_name: player.username.clone(), + x: player.x, + y: player.y, + z: player.z, + yaw: player.yaw, + pitch: player.pitch, + }); + player.packets_to_send.push(message_packet.clone()); } } + msg!("&dWelcome to the server! Enjoyyyyyy".to_string()); + reply_queue.push(ServerPacket::UpdateUserType { + user_type: PlayerType::Operator, + }); } - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue, - Err(e) => return Err(e), - } - } + ClientPacket::SetBlock { + x, + y, + z, + mode, + block_type, + } => { + let block_type = if mode == 0x00 { 0 } else { block_type }; + let mut data = data.write().await; - if ready.is_writable() { - { - let mut data = data.write().await; - if let Some(player) = data.players.iter_mut().find(|p| p.id == *own_id) { - for mut packet in player.packets_to_send.drain(..) { - if let Some(id) = packet.get_player_id() { - if id == *own_id { - if !packet.should_echo() { - continue; + // kick players if they attempt to place a block out of bounds + if x.clamp(0, data.level.x_size as i16 - 1) != x + || y.clamp(0, data.level.y_size as i16 - 1) != y + || z.clamp(0, data.level.z_size as i16 - 1) != z + { + return Ok(Some("Attempt to place block out of bounds".to_string())); + } + + let new_block_info = BLOCK_INFO.get(&block_type); + if new_block_info.is_none() { + msg!(format!("&cUnknown 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 + .get(&block) + .expect("missing block information for block!"); + + // check if player has ability to place/break these blocks + let player_type = data + .players + .iter() + .find_map(|p| (p.id == *own_id).then_some(p.permissions)) + .unwrap_or_default(); + if player_type < new_block_info.place_permissions { + cancel = true; + msg!("&cNot allow to place this block.".to_string()); + } else if player_type < block_info.break_permissions { + cancel = true; + msg!("&cNot allowed to break this block.".to_string()); + } + + if cancel { + reply_queue.push(ServerPacket::SetBlock { + x, + y, + z, + block_type: block, + }); + continue; + } + 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 { + _player_id: _, + x, + y, + z, + yaw, + pitch, + } => { + let mut data = data.write().await; + + let player = data + .players + .iter_mut() + .find(|p| p.id == *own_id) + .expect("missing player"); + player.x = x; + player.y = y; + player.z = z; + player.yaw = yaw; + player.pitch = pitch; + + data.spread_packet(ServerPacket::SetPositionOrientation { + player_id: *own_id, + x, + y, + z, + yaw, + pitch, + }); + } + ClientPacket::Message { player_id, message } => { + let mut data = data.write().await; + + if let Some(message) = message.strip_prefix(Command::PREFIX) { + match Command::parse(message) { + Ok(cmd) => { + for message in cmd.process(&mut data, *own_id) { + msg!(message); } - packet.set_player_id(-1); + } + Err(msg) => { + msg!(format!("&c{msg}")); } } - reply_queue.push_back(packet); + } else { + println!("{message}"); + let message = format!( + "&f<{}> {message}", + data.players + .iter() + .find(|p| p.id == *own_id) + .expect("should never fail") + .username + ); + data.spread_packet(ServerPacket::Message { player_id, message }); } } - } - while let Some(packet) = reply_queue.pop_front() { - let writer = PacketWriter::default().write_u8(packet.get_id()); - let msg = packet.write(writer).into_raw_packet(); - stream.write_all(&msg).await?; + ClientPacket::Extended(_packet) => { + // extended packets! + return Ok(Some( + "Unexpected extension packet in this phase!".to_string(), + )); + // match packet { + // packet => { + // println!("improper client packet for this phase!: {packet:#?}"); + // return Ok(Some( + // "Client sent invalid packet for this phase".to_string(), + // )); + // } + // } + } } } + + let mut data = data.write().await; + if let Some(player) = data.players.iter_mut().find(|p| p.id == *own_id) { + for mut packet in player.packets_to_send.drain(..) { + if let Some(id) = packet.get_player_id() { + if id == *own_id { + if !packet.should_echo() { + continue; + } + packet.set_player_id(-1); + } + } + reply_queue.push(packet); + } + } + + write_packets(stream, reply_queue.drain(..)).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 diff --git a/src/server/network/extensions.rs b/src/server/network/extensions.rs new file mode 100644 index 0000000..25dcce0 --- /dev/null +++ b/src/server/network/extensions.rs @@ -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 { + 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) +}