implement CustomBlocks (resolves #19) and InventoryOrder (resolves #43)

This commit is contained in:
Zoey 2024-04-24 23:10:30 -07:00
parent d53f37d664
commit be81b8d581
No known key found for this signature in database
GPG key ID: 8611B896D1AAFAF2
11 changed files with 199 additions and 45 deletions

35
Cargo.lock generated
View file

@ -126,6 +126,7 @@ dependencies = [
"safer-bytes", "safer-bytes",
"serde", "serde",
"serde_json", "serde_json",
"strum",
"tokio", "tokio",
] ]
@ -191,6 +192,12 @@ dependencies = [
"allocator-api2", "allocator-api2",
] ]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -424,6 +431,12 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustversion"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.17" version = "1.0.17"
@ -503,6 +516,28 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "syn" name = "syn"
version = "2.0.60" version = "2.0.60"

View file

@ -17,4 +17,5 @@ rand = "0.8"
safer-bytes = "0.2" safer-bytes = "0.2"
serde = {version = "1", features = ["derive"]} serde = {version = "1", features = ["derive"]}
serde_json = "1" serde_json = "1"
strum = { version = "0.26", features = ["derive"] }
tokio = {version = "1", features = ["full"]} tokio = {version = "1", features = ["full"]}

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
packet::{server::ServerPacket, STRING_LENGTH}, packet::{server::ServerPacket, ExtBitmask, STRING_LENGTH},
player::PlayerType, player::PlayerType,
server::{ server::{
config::{ConfigCoordinatesWithOrientation, ServerProtectionMode}, config::{ConfigCoordinatesWithOrientation, ServerProtectionMode},
network::set_player_inventory,
ServerData, ServerData,
}, },
}; };
@ -87,7 +88,10 @@ impl<'m> Command<'m> {
CMD_SAY => Self::Say { message: arguments }, CMD_SAY => Self::Say { message: arguments },
CMD_SETPERM => Self::SetPermissions { CMD_SETPERM => Self::SetPermissions {
player_username: Self::next_string(&mut arguments)?, 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 => { CMD_KICK => {
let username = Self::next_string(&mut arguments)?; let username = Self::next_string(&mut arguments)?;
@ -297,7 +301,7 @@ impl<'m> Command<'m> {
return messages; 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 let Some(current) = data.config.player_perms.get(player_username) {
if *current >= player_perms { if *current >= player_perms {
@ -329,6 +333,15 @@ impl<'m> Command<'m> {
player_id: p.id, player_id: p.id,
message: format!("Your permissions have been set to {perm_string}"), 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!( messages.push(format!(
"Set permissions for {player_username} to {perm_string}" "Set permissions for {player_username} to {perm_string}"

View file

@ -4,6 +4,9 @@ use internment::Intern;
use crate::player::PlayerType; 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 /// information about all blocks implemented
pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| { pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
[ [
@ -19,7 +22,7 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
), ),
( (
0x07, 0x07,
BlockInfo::new("bedrock").perm(PlayerType::Operator, PlayerType::Operator), BlockInfo::new("bedrock").perm(PlayerType::Moderator, PlayerType::Moderator),
), ),
( (
0x08, 0x08,
@ -28,13 +31,13 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
stationary: 0x09, stationary: 0x09,
ticks_to_spread: 3, ticks_to_spread: 3,
}) })
.perm(PlayerType::Operator, PlayerType::Normal), .perm(PlayerType::Moderator, PlayerType::Normal),
), ),
( (
0x09, 0x09,
BlockInfo::new("water_stationary") BlockInfo::new("water_stationary")
.block_type(BlockType::FluidStationary { moving: 0x08 }) .block_type(BlockType::FluidStationary { moving: 0x08 })
.perm(PlayerType::Operator, PlayerType::Normal), .perm(PlayerType::Moderator, PlayerType::Normal),
), ),
( (
0x0a, 0x0a,
@ -43,13 +46,13 @@ pub static BLOCK_INFO: LazyLock<BTreeMap<u8, BlockInfo>> = LazyLock::new(|| {
stationary: 0x0b, stationary: 0x0b,
ticks_to_spread: 15, ticks_to_spread: 15,
}) })
.perm(PlayerType::Operator, PlayerType::Normal), .perm(PlayerType::Moderator, PlayerType::Normal),
), ),
( (
0x0b, 0x0b,
BlockInfo::new("lava_stationary") BlockInfo::new("lava_stationary")
.block_type(BlockType::FluidStationary { moving: 0x0a }) .block_type(BlockType::FluidStationary { moving: 0x0a })
.perm(PlayerType::Operator, PlayerType::Normal), .perm(PlayerType::Moderator, PlayerType::Normal),
), ),
(0x0c, BlockInfo::new("sand")), (0x0c, BlockInfo::new("sand")),
(0x0d, BlockInfo::new("gravel")), (0x0d, BlockInfo::new("gravel")),

View file

@ -205,6 +205,9 @@ impl ExtBitmask {
Self::EnvWeatherType => { Self::EnvWeatherType => {
ExtInfo::new("EnvWeatherType".to_string(), 1, Self::EnvWeatherType) ExtInfo::new("EnvWeatherType".to_string(), 1, Self::EnvWeatherType)
} }
Self::InventoryOrder => {
ExtInfo::new("InventoryOrder".to_string(), 1, Self::InventoryOrder)
}
_ => return None, _ => return None,
}) })
} }

View file

@ -10,6 +10,8 @@ pub enum ExtendedClientPacket {
}, },
/// packet containing a supported extension name and version /// packet containing a supported extension name and version
ExtEntry { ext_name: String, version: i32 }, ExtEntry { ext_name: String, version: i32 },
/// packet containing the support level for custom blocks from the client
CustomBlockSupportLevel { support_level: u8 },
} }
impl ExtendedClientPacket { impl ExtendedClientPacket {
@ -18,6 +20,7 @@ impl ExtendedClientPacket {
Some(match id { Some(match id {
0x10 => STRING_LENGTH + 2, 0x10 => STRING_LENGTH + 2,
0x11 => STRING_LENGTH + 4, 0x11 => STRING_LENGTH + 4,
0x13 => 1,
_ => return None, _ => return None,
}) })
} }
@ -36,6 +39,9 @@ impl ExtendedClientPacket {
ext_name: buf.try_get_string().ok()?, ext_name: buf.try_get_string().ok()?,
version: buf.try_get_i32().ok()?, version: buf.try_get_i32().ok()?,
}, },
0x13 => Self::CustomBlockSupportLevel {
support_level: buf.try_get_u8().ok()?,
},
_ => return None, _ => return None,
}) })
} }

View file

@ -1,6 +1,10 @@
use half::f16; 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; 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 /// 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 /// TODO: implement pinging? classicube works fine without it
Ping {}, Ping,
/// informs clients that there is incoming level data /// informs clients that there is incoming level data
LevelInitialize {}, LevelInitialize,
/// packet to send a chunk (not minecraft chunk) of gzipped level data /// packet to send a chunk (not minecraft chunk) of gzipped level data
LevelDataChunk { LevelDataChunk {
chunk_length: i16, chunk_length: i16,
@ -96,13 +100,17 @@ pub enum ServerPacket {
// extension packets // extension packets
/// packet to send info about the server's extensions /// packet to send info about the server's extensions
ExtInfo {}, ExtInfo,
/// packet to send info about an extension on the server /// packet to send info about an extension on the server
ExtEntry { ext_name: String, version: i32 }, 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 /// packet to set a player's currently held block
HoldThis { block: u8, prevent_change: bool }, HoldThis { block: u8, prevent_change: bool },
/// informs the client that it should update the current weather /// informs the client that it should update the current weather
EnvWeatherType { weather_type: WeatherType }, EnvWeatherType { weather_type: WeatherType },
/// packet to set a block's position in the client's inventory
SetInventoryOrder { order: u8, block: u8 },
} }
impl ServerPacket { impl ServerPacket {
@ -110,8 +118,8 @@ impl ServerPacket {
pub fn get_id(&self) -> u8 { pub fn get_id(&self) -> u8 {
match self { match self {
Self::ServerIdentification { .. } => 0x00, Self::ServerIdentification { .. } => 0x00,
Self::Ping {} => 0x01, Self::Ping => 0x01,
Self::LevelInitialize {} => 0x02, Self::LevelInitialize => 0x02,
Self::LevelDataChunk { .. } => 0x03, Self::LevelDataChunk { .. } => 0x03,
Self::LevelFinalize { .. } => 0x04, Self::LevelFinalize { .. } => 0x04,
Self::SetBlock { .. } => 0x06, Self::SetBlock { .. } => 0x06,
@ -125,10 +133,12 @@ impl ServerPacket {
Self::DisconnectPlayer { .. } => 0x0e, Self::DisconnectPlayer { .. } => 0x0e,
Self::UpdateUserType { .. } => 0x0f, Self::UpdateUserType { .. } => 0x0f,
Self::ExtInfo {} => 0x10, Self::ExtInfo => 0x10,
Self::ExtEntry { .. } => 0x11, Self::ExtEntry { .. } => 0x11,
Self::CustomBlockSupportLevel { .. } => 0x13,
Self::HoldThis { .. } => 0x14, Self::HoldThis { .. } => 0x14,
Self::EnvWeatherType { .. } => 0x1f, Self::EnvWeatherType { .. } => 0x1f,
Self::SetInventoryOrder { .. } => 0x2c,
} }
} }
@ -145,8 +155,8 @@ impl ServerPacket {
.write_string(server_name) .write_string(server_name)
.write_string(server_motd) .write_string(server_motd)
.write_u8(user_type.into()), .write_u8(user_type.into()),
Self::Ping {} => writer, Self::Ping => writer,
Self::LevelInitialize {} => writer, Self::LevelInitialize => writer,
Self::LevelDataChunk { Self::LevelDataChunk {
chunk_length, chunk_length,
chunk_data, chunk_data,
@ -239,17 +249,19 @@ 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 Self::ExtInfo => writer
.write_string(SERVER_NAME) .write_string(SERVER_NAME)
.write_i16(ExtBitmask::all().all_contained_info().len() as i16), .write_i16(ExtBitmask::all().all_contained_info().len() as i16),
Self::ExtEntry { ext_name, version } => { Self::ExtEntry { ext_name, version } => {
writer.write_string(ext_name).write_i32(*version) writer.write_string(ext_name).write_i32(*version)
} }
Self::CustomBlockSupportLevel => writer.write_u8(CUSTOM_BLOCKS_SUPPORT_LEVEL),
Self::HoldThis { Self::HoldThis {
block, block,
prevent_change, prevent_change,
} => writer.write_u8(*block).write_bool(*prevent_change), } => writer.write_u8(*block).write_bool(*prevent_change),
Self::EnvWeatherType { weather_type } => writer.write_u8(weather_type.into()), Self::EnvWeatherType { weather_type } => writer.write_u8(weather_type.into()),
Self::SetInventoryOrder { order, block } => writer.write_u8(*order).write_u8(*block),
} }
} }

View file

@ -29,6 +29,8 @@ pub struct Player {
pub _addr: SocketAddr, pub _addr: SocketAddr,
/// the player's supported extensions /// the player's supported extensions
pub extensions: ExtBitmask, 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 /// 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
@ -36,7 +38,20 @@ pub struct Player {
} }
/// enum describing types of players /// 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 { pub enum PlayerType {
/// a normal player /// a normal player
Normal, Normal,
@ -61,16 +76,3 @@ impl From<&PlayerType> for u8 {
} }
} }
} }
impl TryFrom<&str> for PlayerType {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value.to_lowercase().as_str() {
"normal" => Self::Normal,
"moderator" => Self::Moderator,
"operator" => Self::Operator,
value => return Err(format!("Unknown permissions type: {value}")),
})
}
}

View file

@ -1,5 +1,5 @@
pub mod config; pub mod config;
mod network; pub(crate) mod network;
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};

View file

@ -18,7 +18,7 @@ use crate::{
client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH, client::ClientPacket, server::ServerPacket, ExtBitmask, PacketWriter, ARRAY_LENGTH,
EXTENSION_MAGIC_NUMBER, EXTENSION_MAGIC_NUMBER,
}, },
player::Player, player::{Player, PlayerType},
server::config::ServerProtectionMode, server::config::ServerProtectionMode,
}; };
@ -49,6 +49,32 @@ where
Ok(()) 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<ServerPacket>,
) {
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( pub(super) async fn handle_stream(
mut stream: TcpStream, mut stream: TcpStream,
addr: SocketAddr, addr: SocketAddr,
@ -185,13 +211,17 @@ async fn handle_stream_inner(
pitch: 0, pitch: 0,
permissions: player_type, permissions: player_type,
extensions: ExtBitmask::none(), extensions: ExtBitmask::none(),
custom_blocks_support_level: 0,
packets_to_send: Vec::new(), packets_to_send: Vec::new(),
should_be_kicked: None, should_be_kicked: None,
}; };
if magic_number == EXTENSION_MAGIC_NUMBER { 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 { reply_queue.push(ServerPacket::ServerIdentification {
protocol_version: 0x07, protocol_version: 0x07,
@ -201,9 +231,12 @@ async fn handle_stream_inner(
}); });
println!("generating level packets"); 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 { reply_queue.push(ServerPacket::EnvWeatherType {
weather_type: data.level.weather, weather_type: data.level.weather,
}); });
@ -263,6 +296,15 @@ async fn handle_stream_inner(
reply_queue.push(ServerPacket::UpdateUserType { reply_queue.push(ServerPacket::UpdateUserType {
user_type: player_type, user_type: player_type,
}); });
if extensions.contains(ExtBitmask::InventoryOrder) {
set_player_inventory(
player_type,
extensions,
custom_blocks_support_level,
&mut reply_queue,
);
}
} }
ClientPacket::SetBlock { ClientPacket::SetBlock {
x, 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 /// 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,
extensions: ExtBitmask,
custom_blocks_support_level: u8,
) -> Vec<ServerPacket> {
let mut packets: Vec<ServerPacket> = vec![ServerPacket::LevelInitialize {}]; let mut packets: Vec<ServerPacket> = 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 volume = level.x_size * level.y_size * level.z_size;
let mut data = Vec::with_capacity(volume + 4); let mut data = Vec::with_capacity(volume + 4);
data.extend_from_slice(&(volume as i32).to_be_bytes()); 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()); let mut e = GzEncoder::new(Vec::new(), Compression::best());
e.write_all(&data).expect("failed to gzip level data"); e.write_all(&data).expect("failed to gzip level data");

View file

@ -1,13 +1,16 @@
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::packet::{ use crate::{
client::ClientPacket, client_extended::ExtendedClientPacket, server::ServerPacket, ExtBitmask, level::block::CUSTOM_BLOCKS_SUPPORT_LEVEL,
ExtInfo, packet::{
client::ClientPacket, client_extended::ExtendedClientPacket, server::ServerPacket,
ExtBitmask, ExtInfo,
},
}; };
use super::{next_packet, write_packets}; use super::{next_packet, write_packets};
pub async fn get_supported_extensions(stream: &mut TcpStream) -> std::io::Result<ExtBitmask> { pub async fn get_supported_extensions(stream: &mut TcpStream) -> std::io::Result<(ExtBitmask, u8)> {
let extensions = ExtBitmask::all().all_contained_info(); let extensions = ExtBitmask::all().all_contained_info();
write_packets( write_packets(
@ -61,5 +64,23 @@ pub async fn get_supported_extensions(stream: &mut TcpStream) -> std::io::Result
.into_iter() .into_iter()
.fold(ExtBitmask::none(), |acc, ext| acc | ext.bitmask); .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))
} }