diff --git a/Cargo.lock b/Cargo.lock index 62ca920..223d3c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,7 @@ dependencies = [ "safer-bytes", "serde", "serde_json", + "strum", "tokio", ] @@ -191,6 +192,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -424,6 +431,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustversion" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" + [[package]] name = "ryu" version = "1.0.17" @@ -503,6 +516,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.60" diff --git a/Cargo.toml b/Cargo.toml index b5436dc..74e782a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,4 +17,5 @@ rand = "0.8" safer-bytes = "0.2" serde = {version = "1", features = ["derive"]} serde_json = "1" +strum = { version = "0.26", features = ["derive"] } tokio = {version = "1", features = ["full"]} diff --git a/src/command.rs b/src/command.rs index 40c50a8..fa6423d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,8 +1,9 @@ use crate::{ - packet::{server::ServerPacket, STRING_LENGTH}, + packet::{server::ServerPacket, ExtBitmask, STRING_LENGTH}, player::PlayerType, server::{ config::{ConfigCoordinatesWithOrientation, ServerProtectionMode}, + network::set_player_inventory, ServerData, }, }; @@ -87,7 +88,10 @@ impl<'m> Command<'m> { CMD_SAY => Self::Say { message: arguments }, CMD_SETPERM => Self::SetPermissions { player_username: Self::next_string(&mut arguments)?, - permissions: arguments.trim().try_into()?, + permissions: arguments + .trim() + .try_into() + .map_err(|_| format!("&cUnknown permissions type: {arguments}"))?, }, CMD_KICK => { let username = Self::next_string(&mut arguments)?; @@ -297,7 +301,7 @@ impl<'m> Command<'m> { return messages; } - let perm_string = serde_json::to_string(&permissions).expect("should never fail"); + let perm_string: &'static str = permissions.into(); if let Some(current) = data.config.player_perms.get(player_username) { if *current >= player_perms { @@ -329,6 +333,15 @@ impl<'m> Command<'m> { player_id: p.id, message: format!("Your permissions have been set to {perm_string}"), }); + + if p.extensions.contains(ExtBitmask::InventoryOrder) { + set_player_inventory( + p.permissions, + p.extensions, + p.custom_blocks_support_level, + &mut p.packets_to_send, + ); + } } messages.push(format!( "Set permissions for {player_username} to {perm_string}" diff --git a/src/level/block.rs b/src/level/block.rs index b5faae9..35c8997 100644 --- a/src/level/block.rs +++ b/src/level/block.rs @@ -4,6 +4,9 @@ use internment::Intern; use crate::player::PlayerType; +/// the level of custom blocks supported by the server +pub const CUSTOM_BLOCKS_SUPPORT_LEVEL: u8 = 1; + /// information about all blocks implemented pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { [ @@ -19,7 +22,7 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { ), ( 0x07, - BlockInfo::new("bedrock").perm(PlayerType::Operator, PlayerType::Operator), + BlockInfo::new("bedrock").perm(PlayerType::Moderator, PlayerType::Moderator), ), ( 0x08, @@ -28,13 +31,13 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { stationary: 0x09, ticks_to_spread: 3, }) - .perm(PlayerType::Operator, PlayerType::Normal), + .perm(PlayerType::Moderator, PlayerType::Normal), ), ( 0x09, BlockInfo::new("water_stationary") .block_type(BlockType::FluidStationary { moving: 0x08 }) - .perm(PlayerType::Operator, PlayerType::Normal), + .perm(PlayerType::Moderator, PlayerType::Normal), ), ( 0x0a, @@ -43,13 +46,13 @@ pub static BLOCK_INFO: LazyLock> = LazyLock::new(|| { stationary: 0x0b, ticks_to_spread: 15, }) - .perm(PlayerType::Operator, PlayerType::Normal), + .perm(PlayerType::Moderator, PlayerType::Normal), ), ( 0x0b, BlockInfo::new("lava_stationary") .block_type(BlockType::FluidStationary { moving: 0x0a }) - .perm(PlayerType::Operator, PlayerType::Normal), + .perm(PlayerType::Moderator, PlayerType::Normal), ), (0x0c, BlockInfo::new("sand")), (0x0d, BlockInfo::new("gravel")), diff --git a/src/packet.rs b/src/packet.rs index 0160f9c..c57474b 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -205,6 +205,9 @@ impl ExtBitmask { Self::EnvWeatherType => { ExtInfo::new("EnvWeatherType".to_string(), 1, Self::EnvWeatherType) } + Self::InventoryOrder => { + ExtInfo::new("InventoryOrder".to_string(), 1, Self::InventoryOrder) + } _ => return None, }) } diff --git a/src/packet/client_extended.rs b/src/packet/client_extended.rs index 0b1b477..0c67fc7 100644 --- a/src/packet/client_extended.rs +++ b/src/packet/client_extended.rs @@ -10,6 +10,8 @@ pub enum ExtendedClientPacket { }, /// packet containing a supported extension name and version ExtEntry { ext_name: String, version: i32 }, + /// packet containing the support level for custom blocks from the client + CustomBlockSupportLevel { support_level: u8 }, } impl ExtendedClientPacket { @@ -18,6 +20,7 @@ impl ExtendedClientPacket { Some(match id { 0x10 => STRING_LENGTH + 2, 0x11 => STRING_LENGTH + 4, + 0x13 => 1, _ => return None, }) } @@ -36,6 +39,9 @@ impl ExtendedClientPacket { ext_name: buf.try_get_string().ok()?, version: buf.try_get_i32().ok()?, }, + 0x13 => Self::CustomBlockSupportLevel { + support_level: buf.try_get_u8().ok()?, + }, _ => return None, }) } diff --git a/src/packet/server.rs b/src/packet/server.rs index d4d6e24..0e55bad 100644 --- a/src/packet/server.rs +++ b/src/packet/server.rs @@ -1,6 +1,10 @@ use half::f16; -use crate::{level::WeatherType, player::PlayerType, SERVER_NAME}; +use crate::{ + level::{block::CUSTOM_BLOCKS_SUPPORT_LEVEL, WeatherType}, + player::PlayerType, + SERVER_NAME, +}; use super::ExtBitmask; @@ -17,9 +21,9 @@ pub enum ServerPacket { }, /// since clients do not notify the server when leaving, the ping packet is used to check if the client is still connected /// TODO: implement pinging? classicube works fine without it - Ping {}, + Ping, /// informs clients that there is incoming level data - LevelInitialize {}, + LevelInitialize, /// packet to send a chunk (not minecraft chunk) of gzipped level data LevelDataChunk { chunk_length: i16, @@ -96,13 +100,17 @@ pub enum ServerPacket { // extension packets /// packet to send info about the server's extensions - ExtInfo {}, + ExtInfo, /// packet to send info about an extension on the server ExtEntry { ext_name: String, version: i32 }, + /// packet to send the server's supported custom blocks + CustomBlockSupportLevel, /// packet to set a player's currently held block HoldThis { block: u8, prevent_change: bool }, /// informs the client that it should update the current weather EnvWeatherType { weather_type: WeatherType }, + /// packet to set a block's position in the client's inventory + SetInventoryOrder { order: u8, block: u8 }, } impl ServerPacket { @@ -110,8 +118,8 @@ impl ServerPacket { pub fn get_id(&self) -> u8 { match self { Self::ServerIdentification { .. } => 0x00, - Self::Ping {} => 0x01, - Self::LevelInitialize {} => 0x02, + Self::Ping => 0x01, + Self::LevelInitialize => 0x02, Self::LevelDataChunk { .. } => 0x03, Self::LevelFinalize { .. } => 0x04, Self::SetBlock { .. } => 0x06, @@ -125,10 +133,12 @@ impl ServerPacket { Self::DisconnectPlayer { .. } => 0x0e, Self::UpdateUserType { .. } => 0x0f, - Self::ExtInfo {} => 0x10, + Self::ExtInfo => 0x10, Self::ExtEntry { .. } => 0x11, + Self::CustomBlockSupportLevel { .. } => 0x13, Self::HoldThis { .. } => 0x14, Self::EnvWeatherType { .. } => 0x1f, + Self::SetInventoryOrder { .. } => 0x2c, } } @@ -145,8 +155,8 @@ impl ServerPacket { .write_string(server_name) .write_string(server_motd) .write_u8(user_type.into()), - Self::Ping {} => writer, - Self::LevelInitialize {} => writer, + Self::Ping => writer, + Self::LevelInitialize => writer, Self::LevelDataChunk { chunk_length, chunk_data, @@ -239,17 +249,19 @@ impl ServerPacket { Self::DisconnectPlayer { disconnect_reason } => writer.write_string(disconnect_reason), Self::UpdateUserType { user_type } => writer.write_u8(user_type.into()), - Self::ExtInfo {} => writer + 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::CustomBlockSupportLevel => writer.write_u8(CUSTOM_BLOCKS_SUPPORT_LEVEL), Self::HoldThis { block, prevent_change, } => writer.write_u8(*block).write_bool(*prevent_change), Self::EnvWeatherType { weather_type } => writer.write_u8(weather_type.into()), + Self::SetInventoryOrder { order, block } => writer.write_u8(*order).write_u8(*block), } } diff --git a/src/player.rs b/src/player.rs index 7f5ad00..c127f0d 100644 --- a/src/player.rs +++ b/src/player.rs @@ -29,6 +29,8 @@ pub struct Player { pub _addr: SocketAddr, /// the player's supported extensions pub extensions: ExtBitmask, + /// the level of custom blocks this client supports + pub custom_blocks_support_level: u8, /// 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 @@ -36,7 +38,20 @@ pub struct Player { } /// enum describing types of players -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + strum::EnumString, + strum::IntoStaticStr, +)] +#[strum(ascii_case_insensitive)] pub enum PlayerType { /// a normal player Normal, @@ -61,16 +76,3 @@ impl From<&PlayerType> for u8 { } } } - -impl TryFrom<&str> for PlayerType { - type Error = String; - - fn try_from(value: &str) -> Result { - Ok(match value.to_lowercase().as_str() { - "normal" => Self::Normal, - "moderator" => Self::Moderator, - "operator" => Self::Operator, - value => return Err(format!("Unknown permissions type: {value}")), - }) - } -} diff --git a/src/server.rs b/src/server.rs index cf36ec7..0040bf9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,5 @@ pub mod config; -mod network; +pub(crate) mod network; use std::{path::PathBuf, sync::Arc}; diff --git a/src/server/network.rs b/src/server/network.rs index 431d37c..8071dbd 100644 --- a/src/server/network.rs +++ b/src/server/network.rs @@ -18,7 +18,7 @@ use crate::{ client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH, EXTENSION_MAGIC_NUMBER, }, - player::Player, + player::{Player, PlayerType}, server::config::ServerProtectionMode, }; @@ -49,6 +49,32 @@ where Ok(()) } +/// gets the packets needed to update a player's inventory +pub(crate) fn set_player_inventory( + perms: PlayerType, + extensions: ExtBitmask, + custom_blocks_support_level: u8, + packets_queue: &mut Vec, +) { + let custom_blocks = + extensions.contains(ExtBitmask::CustomBlocks) && custom_blocks_support_level == 1; + assert!( + custom_blocks_support_level <= 1, + "support not implemented for additional custom block levels" + ); + for (id, info) in &*BLOCK_INFO { + if !custom_blocks && *id > 49 { + break; + } + let block = if info.place_permissions <= perms { + *id + } else { + 0 + }; + packets_queue.push(ServerPacket::SetInventoryOrder { order: *id, block }); + } +} + pub(super) async fn handle_stream( mut stream: TcpStream, addr: SocketAddr, @@ -185,13 +211,17 @@ async fn handle_stream_inner( pitch: 0, permissions: player_type, extensions: ExtBitmask::none(), + custom_blocks_support_level: 0, packets_to_send: Vec::new(), should_be_kicked: None, }; if magic_number == EXTENSION_MAGIC_NUMBER { - player.extensions = extensions::get_supported_extensions(stream).await?; + (player.extensions, player.custom_blocks_support_level) = + extensions::get_supported_extensions(stream).await?; } + let extensions = player.extensions; + let custom_blocks_support_level = player.custom_blocks_support_level; reply_queue.push(ServerPacket::ServerIdentification { protocol_version: 0x07, @@ -201,9 +231,12 @@ async fn handle_stream_inner( }); println!("generating level packets"); - reply_queue.extend(build_level_packets(&data.level).into_iter()); + reply_queue.extend( + build_level_packets(&data.level, extensions, custom_blocks_support_level) + .into_iter(), + ); - if player.extensions.contains(ExtBitmask::EnvWeatherType) { + if extensions.contains(ExtBitmask::EnvWeatherType) { reply_queue.push(ServerPacket::EnvWeatherType { weather_type: data.level.weather, }); @@ -263,6 +296,15 @@ async fn handle_stream_inner( reply_queue.push(ServerPacket::UpdateUserType { user_type: player_type, }); + + if extensions.contains(ExtBitmask::InventoryOrder) { + set_player_inventory( + player_type, + extensions, + custom_blocks_support_level, + &mut reply_queue, + ); + } } ClientPacket::SetBlock { x, @@ -422,14 +464,30 @@ async fn handle_stream_inner( } /// 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 { +fn build_level_packets( + level: &Level, + extensions: ExtBitmask, + custom_blocks_support_level: u8, +) -> Vec { let mut packets: Vec = vec![ServerPacket::LevelInitialize {}]; - // TODO: the type conversions in here may be weird idk + let custom_blocks = + extensions.contains(ExtBitmask::CustomBlocks) && custom_blocks_support_level >= 1; + let volume = level.x_size * level.y_size * level.z_size; let mut data = Vec::with_capacity(volume + 4); data.extend_from_slice(&(volume as i32).to_be_bytes()); - data.extend_from_slice(&level.blocks); + data.extend(level.blocks.iter().copied().map(|b| { + if custom_blocks || b <= 49 { + b + } else { + BLOCK_INFO + .get(&b) + .expect("missing block") + .fallback + .unwrap_or_default() + } + })); let mut e = GzEncoder::new(Vec::new(), Compression::best()); e.write_all(&data).expect("failed to gzip level data"); diff --git a/src/server/network/extensions.rs b/src/server/network/extensions.rs index 25dcce0..a348f62 100644 --- a/src/server/network/extensions.rs +++ b/src/server/network/extensions.rs @@ -1,13 +1,16 @@ use tokio::net::TcpStream; -use crate::packet::{ - client::ClientPacket, client_extended::ExtendedClientPacket, server::ServerPacket, ExtBitmask, - ExtInfo, +use crate::{ + level::block::CUSTOM_BLOCKS_SUPPORT_LEVEL, + 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 { +pub async fn get_supported_extensions(stream: &mut TcpStream) -> std::io::Result<(ExtBitmask, u8)> { let extensions = ExtBitmask::all().all_contained_info(); write_packets( @@ -61,5 +64,23 @@ pub async fn get_supported_extensions(stream: &mut TcpStream) -> std::io::Result .into_iter() .fold(ExtBitmask::none(), |acc, ext| acc | ext.bitmask); - Ok(final_bitmask) + let custom_blocks_support_level = if final_bitmask.contains(ExtBitmask::CustomBlocks) { + write_packets( + stream, + Some(ServerPacket::CustomBlockSupportLevel).into_iter(), + ) + .await?; + if let Some(ClientPacket::Extended(ExtendedClientPacket::CustomBlockSupportLevel { + support_level, + })) = next_packet(stream).await? + { + support_level.min(CUSTOM_BLOCKS_SUPPORT_LEVEL) + } else { + panic!("expected CustomBlockSupportLevel packet!"); + } + } else { + 0 + }; + + Ok((final_bitmask, custom_blocks_support_level)) }