diff --git a/.etc/example-config.toml b/.etc/example-config.toml index a0cef8e2..2ea9d290 100644 --- a/.etc/example-config.toml +++ b/.etc/example-config.toml @@ -37,4 +37,8 @@ cache_ttl = 60 # How big the cache can be in kb. cache_capacity = 20_000 -whitelist = false \ No newline at end of file +# Velocity configuration +[velocity] +enabled = false +# The key from forwarding.secret +secret = "" diff --git a/Cargo.toml b/Cargo.toml index a007e09e..d4252af8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,15 +126,18 @@ thiserror = "2.0.3" rand = "0.9.0-beta.0" fnv = "1.0.7" wyhash = "0.5.0" +sha2 = "=0.10.8" +hmac = "0.12.1" # Encoding/Serialization +toml = "0.8.19" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" serde_derive = "1.0.210" base64 = "0.22.1" bitcode = "0.6.3" bitcode_derive = "0.6.3" -toml = "0.8.19" +#bitmask-enum = "2.2.5" # Bit manipulation byteorder = "1.5.0" diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index 1451fff7..beaf750b 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -26,6 +26,7 @@ ferrumc-macros = { workspace = true } ferrumc-nbt = { workspace = true } ferrumc-general-purpose = { workspace = true } ferrumc-state = { workspace = true } +ferrumc-text = { workspace = true } parking_lot = { workspace = true, features = ["deadlock_detection"] } tracing = { workspace = true } @@ -33,10 +34,12 @@ tokio = { workspace = true } futures = { workspace = true } async-trait = { workspace = true } clap = { workspace = true, features = ["derive"] } +hmac = { workspace = true } +sha2 = { workspace = true } +rand = { workspace = true } flate2 = { workspace = true } ctor = { workspace = true } - [[bin]] name = "ferrumc" path = "src/main.rs" diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 1a3c9b7c..18600981 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -2,6 +2,7 @@ #![forbid(unsafe_code)] extern crate core; +use crate::cli::{CLIArgs, Command, ImportArgs}; use crate::errors::BinaryError; use clap::Parser; use ferrumc_config::statics::get_global_config; @@ -16,9 +17,8 @@ use systems::definition; use tokio::runtime::Handle; use tracing::{error, info}; -pub(crate) mod errors; -use crate::cli::{CLIArgs, Command, ImportArgs}; mod cli; +pub(crate) mod errors; mod packet_handlers; mod systems; diff --git a/src/bin/src/packet_handlers/login_process.rs b/src/bin/src/packet_handlers/login_process.rs index ccf82297..3d70eeed 100644 --- a/src/bin/src/packet_handlers/login_process.rs +++ b/src/bin/src/packet_handlers/login_process.rs @@ -1,4 +1,3 @@ -use ferrumc_config::statics::{get_global_config, get_whitelist}; use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::grounded::OnGround; @@ -6,8 +5,9 @@ use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; use ferrumc_ecs::components::storage::ComponentRefMut; use ferrumc_ecs::entities::Entity; +use ferrumc_events::infrastructure::Event; use ferrumc_macros::event_handler; -use ferrumc_net::connection::{ConnectionState, StreamWriter}; +use ferrumc_net::connection::{ConnectionState, PlayerStartLoginEvent, StreamWriter}; use ferrumc_net::errors::NetError; use ferrumc_net::packets::incoming::ack_finish_configuration::AckFinishConfigurationEvent; use ferrumc_net::packets::incoming::keep_alive::IncomingKeepAlivePacket; @@ -18,9 +18,7 @@ use ferrumc_net::packets::outgoing::client_bound_known_packs::ClientBoundKnownPa use ferrumc_net::packets::outgoing::finish_configuration::FinishConfigurationPacket; use ferrumc_net::packets::outgoing::game_event::GameEventPacket; use ferrumc_net::packets::outgoing::keep_alive::OutgoingKeepAlivePacket; -use ferrumc_net::packets::outgoing::login_disconnect::LoginDisconnectPacket; use ferrumc_net::packets::outgoing::login_play::LoginPlayPacket; -use ferrumc_net::packets::outgoing::login_success::LoginSuccessPacket; use ferrumc_net::packets::outgoing::player_info_update::PlayerInfoUpdatePacket; use ferrumc_net::packets::outgoing::registry_data::get_registry_packets; use ferrumc_net::packets::outgoing::set_center_chunk::SetCenterChunk; @@ -43,49 +41,30 @@ async fn handle_login_start( ) -> Result { let uuid = login_start_event.login_start_packet.uuid; let username = login_start_event.login_start_packet.username.as_str(); - let player_identity = PlayerIdentity::new(username.to_string(), uuid); debug!("Handling login start event for user: {username}, uuid: {uuid}"); - // Add the player identity component to the ECS for the entity. - state - .universe - .add_component::( - login_start_event.conn_id, - PlayerIdentity::new(username.to_string(), uuid), - )? - /*.add_component::(login_start_event.conn_id, ChunkReceiver::default())?*/; - - //Send a Login Success Response to further the login sequence - let mut writer = state - .universe - .get_mut::(login_start_event.conn_id)?; - - if get_global_config().whitelist { - let whitelist = get_whitelist(); - - if whitelist.get(&uuid).is_none() { - writer.send_packet( - LoginDisconnectPacket::new( - "{\"translate\":\"multiplayer.disconnect.not_whitelisted\"}", - ), - &NetEncodeOpts::WithLength, - )?; - return Ok(login_start_event); + let event = PlayerStartLoginEvent { + entity: login_start_event.conn_id, + profile: PlayerIdentity::new(username.to_string(), uuid), + cancelled: false, + }; + + match PlayerStartLoginEvent::trigger(event, state.clone()).await { + Err(NetError::Kick(msg)) => Err(NetError::Kick(msg)), + Ok(event) => { + if !event.is_cancelled() { + // Add the player identity component to the ECS for the entity. + ferrumc_net::connection::send_login_success( + state, + login_start_event.conn_id, + event.profile, + ) + .await?; + } + Ok(login_start_event) } + e => e.map(|_| login_start_event), } - - // Add the player identity component to the ECS for the entity. - state - .universe - .add_component::(login_start_event.conn_id, player_identity)?; - - //Send a Login Success Response to further the login sequence - writer.send_packet( - LoginSuccessPacket::new(uuid, username), - &NetEncodeOpts::WithLength, - )?; - - Ok(login_start_event) } #[event_handler] @@ -145,58 +124,66 @@ async fn handle_ack_finish_configuration( *conn_state = ConnectionState::Play; + let chunk = state.world.load_chunk(0, 0, "overworld").await.ok(); + + let y = if let Some(ref chunk) = chunk { + (chunk.heightmaps.motion_blocking_height(0, 0)) as f64 + } else { + 256.0 + }; + // add components to the entity after the connection state has been set to play. // to avoid wasting resources on entities that are fetching stuff like server status etc. state .universe - .add_component::(entity_id, Position::default())? - .add_component::(entity_id, Rotation::default())? + .add_component::(entity_id, Position::new(0.0, y, 0.0))? + .add_component::(entity_id, Rotation::new(0.0, 0.0))? .add_component::(entity_id, OnGround::default())? .add_component::(entity_id, ChunkReceiver::default())?; let mut writer = state.universe.get_mut::(entity_id)?; - writer // 21 - .send_packet(LoginPlayPacket::new(entity_id), &NetEncodeOpts::WithLength)?; - writer // 29 - .send_packet( - SynchronizePlayerPositionPacket::default(), // The coordinates here should be used for the center chunk. - &NetEncodeOpts::WithLength, - )?; - writer // 37 - .send_packet( - SetDefaultSpawnPositionPacket::default(), // Player specific, aka. home, bed, where it would respawn. - &NetEncodeOpts::WithLength, - )?; - writer // 38 - .send_packet( - GameEventPacket::start_waiting_for_level_chunks(), - &NetEncodeOpts::WithLength, - )?; - writer // 41 - .send_packet( - SetCenterChunk::new(0, 0), // TODO - Dependent on the player spawn position. - &NetEncodeOpts::WithLength, - )?; - writer // other - .send_packet( - SetRenderDistance::new(5), // TODO - &NetEncodeOpts::WithLength, - )?; + writer.send_packet(LoginPlayPacket::new(entity_id), &NetEncodeOpts::WithLength)?; + writer.send_packet( + SynchronizePlayerPositionPacket::from_player(entity_id, state.clone())?, // The coordinates here should be used for the center chunk. + &NetEncodeOpts::WithLength, + )?; + writer.send_packet( + SetDefaultSpawnPositionPacket::default(), // Player specific, aka. home, bed, where it would respawn. + &NetEncodeOpts::WithLength, + )?; + writer.send_packet( + GameEventPacket::start_waiting_for_level_chunks(), + &NetEncodeOpts::WithLength, + )?; + writer.send_packet( + SetCenterChunk::new(0, 0), // TODO - Dependent on the player spawn position. + &NetEncodeOpts::WithLength, + )?; + writer.send_packet( + SetRenderDistance::new(5), // TODO + &NetEncodeOpts::WithLength, + )?; send_keep_alive(entity_id, &state, &mut writer).await?; - let pos = state.universe.get_mut::(entity_id)?; - let mut chunk_recv = state.universe.get_mut::(entity_id)?; - chunk_recv.last_chunk = Some((pos.x as i32, pos.z as i32, String::from("overworld"))); - chunk_recv.calculate_chunks().await; + if let Some(ref chunk) = chunk { + writer.send_packet(ferrumc_net::packets::outgoing::chunk_and_light_data::ChunkAndLightData::from_chunk(chunk)?, &NetEncodeOpts::WithLength)?; + } } + let pos = state.universe.get::(entity_id)?; + let mut chunk_recv = state.universe.get_mut::(entity_id)?; + chunk_recv.last_chunk = Some((pos.x as i32, pos.z as i32, String::from("overworld"))); + chunk_recv.calculate_chunks().await; + drop(chunk_recv); + player_info_update_packets(entity_id, &state).await?; broadcast_spawn_entity_packet(entity_id, &state).await?; Ok(ack_finish_configuration_event) } + async fn send_keep_alive( conn_id: usize, state: &GlobalState, @@ -223,12 +210,7 @@ async fn player_info_update_packets(entity_id: Entity, state: &GlobalState) -> N let packet = PlayerInfoUpdatePacket::new_player_join_packet(entity_id, state); let start = Instant::now(); - broadcast( - &packet, - state, - BroadcastOptions::default().except([entity_id]), - ) - .await?; + broadcast(&packet, state, BroadcastOptions::default().all()).await?; trace!( "Broadcasting player info update took: {:?}", start.elapsed() @@ -263,8 +245,10 @@ async fn broadcast_spawn_entity_packet(entity_id: Entity, state: &GlobalState) - let writer = state.universe.get_mut::(entity_id)?; futures::stream::iter(get_all_play_players(state)) .fold(writer, |mut writer, entity| async move { - if let Ok(packet) = SpawnEntityPacket::player(entity, state) { - let _ = writer.send_packet(packet, &NetEncodeOpts::WithLength); + if entity != entity_id { + if let Ok(packet) = SpawnEntityPacket::player(entity, state) { + let _ = writer.send_packet(packet, &NetEncodeOpts::WithLength); + } } writer }) diff --git a/src/bin/src/packet_handlers/mod.rs b/src/bin/src/packet_handlers/mod.rs index 21fee1f7..7c2d6380 100644 --- a/src/bin/src/packet_handlers/mod.rs +++ b/src/bin/src/packet_handlers/mod.rs @@ -4,3 +4,6 @@ mod login_process; mod player; mod player_leave; mod tick_handler; + +mod velocity; +mod whitelist; diff --git a/src/bin/src/packet_handlers/player/do_action.rs b/src/bin/src/packet_handlers/player/do_action.rs index de89aac9..14c7e7ca 100644 --- a/src/bin/src/packet_handlers/player/do_action.rs +++ b/src/bin/src/packet_handlers/player/do_action.rs @@ -13,6 +13,7 @@ async fn handle_player_do_action( ) -> Result { trace!("player just did: {:?}", event.action); + // TODO: replace this with a better system to support multiple actions match event.action { PlayerCommandAction::StartSneaking => { let packet = EntityMetadataPacket::new( @@ -26,8 +27,13 @@ async fn handle_player_do_action( broadcast(&packet, &state, Default::default()).await?; } PlayerCommandAction::StopSneaking => { - let packet = - EntityMetadataPacket::new(event.entity_id, [EntityMetadata::entity_standing()]); + let packet = EntityMetadataPacket::new( + event.entity_id, + [ + EntityMetadata::entity_state_none(), + EntityMetadata::entity_standing(), + ], + ); broadcast(&packet, &state, Default::default()).await?; } diff --git a/src/bin/src/packet_handlers/player_leave.rs b/src/bin/src/packet_handlers/player_leave.rs index 8acf2442..f2fc0409 100644 --- a/src/bin/src/packet_handlers/player_leave.rs +++ b/src/bin/src/packet_handlers/player_leave.rs @@ -1,6 +1,8 @@ +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_macros::event_handler; use ferrumc_net::connection::PlayerDisconnectEvent; use ferrumc_net::errors::NetError; +use ferrumc_net::packets::outgoing::player_info_remove::PlayerInfoRemovePacket; use ferrumc_net::packets::outgoing::remove_entities::RemoveEntitiesPacket; use ferrumc_net::utils::broadcast::{broadcast, BroadcastOptions}; use ferrumc_state::GlobalState; @@ -15,10 +17,18 @@ async fn handle_player_disconnect( info!("Player disconnected: {:?}", entity_id); - let remove_entity_packet = RemoveEntitiesPacket::from_entities([entity_id]); + { + let profile = state.universe.get::(entity_id)?; + broadcast( + &PlayerInfoRemovePacket::new(vec![profile.uuid]), + &state, + BroadcastOptions::default().all(), + ) + .await?; + } broadcast( - &remove_entity_packet, + &RemoveEntitiesPacket::from_entities([entity_id]), &state, BroadcastOptions::default().all(), ) diff --git a/src/bin/src/packet_handlers/velocity.rs b/src/bin/src/packet_handlers/velocity.rs new file mode 100644 index 00000000..a47f9cc6 --- /dev/null +++ b/src/bin/src/packet_handlers/velocity.rs @@ -0,0 +1,147 @@ +use ferrumc_config::statics::get_global_config; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::event_handler; +use ferrumc_net::packets::incoming::server_bound_plugin_message::*; +use ferrumc_net::packets::outgoing::client_bound_plugin_message::*; +use ferrumc_net::packets::outgoing::disconnect::DISCONNECT_STRING; +use ferrumc_net::utils::ecs_helpers::EntityExt; +use ferrumc_net::{ + connection::{PlayerStartLoginEvent, StreamWriter}, + errors::NetError, + NetResult, +}; +use ferrumc_net_codec::decode::NetDecode; +use ferrumc_net_codec::{decode::NetDecodeOpts, encode::NetEncodeOpts, net_types::var_int::VarInt}; +use ferrumc_state::GlobalState; +use ferrumc_text::*; +use hmac::{Hmac, Mac}; +use sha2::Sha256; +use std::io::Cursor; +use tokio::io::AsyncReadExt; + +type HmacSha256 = Hmac; + +struct VelocityMessageId(u32); + +#[event_handler] +async fn handle_login_start( + mut event: PlayerStartLoginEvent, + state: GlobalState, +) -> NetResult { + if get_global_config().velocity.enabled { + let entity = event.entity; + if entity.get::(&state).is_ok() { + return Ok(event); + } + + let id = rand::random::(); + let mut writer = entity.get_mut::(&state.clone())?; + writer.send_packet( + LoginPluginMessagePacket::<()>::new(id, String::from("velocity:player_info"), ()), + &NetEncodeOpts::WithLength, + )?; + state + .universe + .add_component(entity, VelocityMessageId(id))?; + + // this stops the packet handler from doing login success + event.cancel(true); + + Ok(event) + } else { + Ok(event) + } +} + +#[event_handler] +async fn handle_velocity_response( + event: LoginPluginResponseEvent, + state: GlobalState, +) -> NetResult { + let message = &event.packet; + if message.message_id.val as u32 == event.entity.get::(&state.clone())?.0 { + let len = message.data.len(); + + let mut signature = vec![0u8; 32]; + let mut data = Vec::with_capacity(256); + let mut buf = Cursor::new(&message.data); + + if len > 0 && message.success { + buf.read_exact(&mut signature).await?; + + let index = buf.position(); + buf.read_to_end(&mut data).await?; + buf.set_position(index); + + let version = VarInt::decode(&mut buf, &NetDecodeOpts::None)?; + let _addr = String::decode(&mut buf, &NetDecodeOpts::None)?; + + if version != 1 { + return Err(NetError::kick( + TextComponentBuilder::new("[FerrumC]") + .color(NamedColor::Blue) + .space() + .extra( + ComponentBuilder::text( + "This velocity modern forwarding version is not supported!", + ) + .color(NamedColor::Red), + ) + .build(), + )); + } + } else { + return Err(NetError::kick( + ComponentBuilder::text("[FerrumC]") + .color(NamedColor::Blue) + .space() + .extra( + ComponentBuilder::text( + "The velocity proxy did not send forwarding information!", + ) + .color(NamedColor::Red), + ) + .build(), + )); + } + + let mut key = HmacSha256::new_from_slice(get_global_config().velocity.secret.as_bytes()) + .expect("Failed to create HmacSha256 for velocity secret"); + key.update(&data); + + if key.verify_slice(&signature[..]).is_ok() { + let e = PlayerStartLoginEvent { + entity: event.entity, + profile: PlayerIdentity::decode(&mut buf, &NetDecodeOpts::None)?, + cancelled: false, + }; + + match PlayerStartLoginEvent::trigger(e, state.clone()).await { + Ok(e) => { + if e.is_cancelled() { + return Err(NetError::kick(DISCONNECT_STRING.to_string())); + } + + state + .universe + .remove_component::(event.entity)?; + + ferrumc_net::connection::send_login_success( + state.clone(), + event.entity, + e.profile, + ) + .await?; + + Ok(event) + } + e => e.map(|_| event), + } + } else { + Err(NetError::kick("Invalid proxy response!".to_string())) + } + } else { + Ok(event) + } +} diff --git a/src/bin/src/packet_handlers/whitelist.rs b/src/bin/src/packet_handlers/whitelist.rs new file mode 100644 index 00000000..87631f72 --- /dev/null +++ b/src/bin/src/packet_handlers/whitelist.rs @@ -0,0 +1,26 @@ +use ferrumc_config::statics::{get_global_config, get_whitelist}; +use ferrumc_macros::event_handler; +use ferrumc_net::{connection::PlayerStartLoginEvent, errors::NetError, NetResult}; +use ferrumc_state::GlobalState; +use ferrumc_text::*; + +#[event_handler] +async fn handle_login_start( + event: PlayerStartLoginEvent, + _state: GlobalState, +) -> NetResult { + if get_global_config().whitelist { + let whitelist = get_whitelist(); + + if whitelist.get(&event.profile.uuid).is_none() { + Err(NetError::Kick(ComponentBuilder::translate( + "multiplayer.disconnect.not_whitelisted", + Vec::new(), + ))) + } else { + Ok(event) + } + } else { + Ok(event) + } +} diff --git a/src/bin/src/systems/keep_alive_system.rs b/src/bin/src/systems/keep_alive_system.rs index 7fbb6df7..f8705fc6 100644 --- a/src/bin/src/systems/keep_alive_system.rs +++ b/src/bin/src/systems/keep_alive_system.rs @@ -5,7 +5,7 @@ use ferrumc_net::connection::{ConnectionState, StreamWriter}; use ferrumc_net::packets::incoming::keep_alive::IncomingKeepAlivePacket; use ferrumc_net::packets::outgoing::keep_alive::OutgoingKeepAlivePacket; use ferrumc_net::utils::broadcast::{BroadcastOptions, BroadcastToAll}; -use ferrumc_net::utils::state::terminate_connection; +use ferrumc_net::utils::state::TerminateConnectionPlayerExt; use ferrumc_state::GlobalState; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -76,12 +76,9 @@ impl System for KeepAliveSystem { if (current_time - keep_alive.timestamp) >= 30000 { // two iterations missed - if let Err(e) = terminate_connection( - state.clone(), - *entity, - "Keep alive timeout".to_string(), - ) - .await + if let Err(e) = entity + .terminate_connection(state.clone(), "Keep alive timeout".to_string()) + .await { warn!( "Failed to terminate connection for entity {:?} , Err : {:?}", diff --git a/src/bin/src/systems/ticking_system.rs b/src/bin/src/systems/ticking_system.rs index 6e72394f..80c19d0e 100644 --- a/src/bin/src/systems/ticking_system.rs +++ b/src/bin/src/systems/ticking_system.rs @@ -8,6 +8,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; use tracing::{debug, info, trace}; + pub struct TickingSystem; static KILLED: AtomicBool = AtomicBool::new(false); @@ -26,8 +27,8 @@ impl System for TickingSystem { res }; - if res.is_err() { - debug!("error handling tick event: {:?}", res); + if let Err(err) = res { + debug!("error handling tick event: {:?}", err); } let now = Instant::now(); diff --git a/src/lib/core/Cargo.toml b/src/lib/core/Cargo.toml index 435b049c..1180a7c7 100644 --- a/src/lib/core/Cargo.toml +++ b/src/lib/core/Cargo.toml @@ -6,9 +6,14 @@ edition = "2021" [dependencies] thiserror = { workspace = true } -tokio = { workspace = true } -ferrumc-ecs = { workspace = true } +tokio = { workspace = true} dashmap = { workspace = true } -ferrumc-world = { workspace = true } tracing = { workspace = true } log = "0.4.22" + +ferrumc-ecs = { workspace = true } +ferrumc-world = { workspace = true } +ferrumc-net-codec = { workspace = true } +ferrumc-macros = { workspace = true } +ferrumc-config = { workspace = true } +flate2 = { workspace = true } diff --git a/src/lib/core/src/identity/player_identity.rs b/src/lib/core/src/identity/player_identity.rs index a8b33e9d..062c5eb4 100644 --- a/src/lib/core/src/identity/player_identity.rs +++ b/src/lib/core/src/identity/player_identity.rs @@ -1,11 +1,96 @@ -#[derive(Debug)] +use ferrumc_macros::{NetDecode, NetEncode}; +use ferrumc_net_codec::{ + decode::{NetDecode, NetDecodeOpts, NetDecodeResult}, + encode::{NetEncode, NetEncodeOpts, NetEncodeResult}, + net_types::length_prefixed_vec::LengthPrefixedVec, +}; +use std::io::{Read, Write}; +use tokio::io::AsyncWrite; + +#[derive(Eq, PartialEq, Clone, Debug, NetEncode, NetDecode)] +/// The PlayerIdentity holds information about a player. +/// +/// Fields: +/// `uuid`: The uuid of the PlayerIdentity. +/// `username`: The username of the PlayerIdentity. +/// `properties`: The properties of the PlayerIdentity for example textures. +/// +/// ```ignore +/// PlayerIdentity { +/// uuid: Uuid::new_v4().as_u128(), +/// username: String::from("Name"), +/// properties: vec![IdentityProperty { +/// name: String::from("textures"), +/// value: String::from("ewogICJ0aW1lc3RhbXAiIDog..."), +/// signature: None, +/// }], +/// } +/// ``` +/// pub struct PlayerIdentity { - pub username: String, + /// The uuid of this Identity pub uuid: u128, + /// The username of this Identity + pub username: String, + /// The properties of this Identity + pub properties: LengthPrefixedVec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Signature(pub Option); + +impl NetEncode for Signature { + fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { + (self.0.is_some()).encode(writer, opts)?; + (self.0).encode(writer, opts)?; + Ok(()) + } + + async fn encode_async( + &self, + writer: &mut W, + opts: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + (self.0.is_some()).encode_async(writer, opts).await?; + (self.0).encode_async(writer, opts).await?; + Ok(()) + } +} + +impl NetDecode for Signature { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + if bool::decode(reader, opts)? { + Ok(Signature(Some(::decode(reader, opts)?))) + } else { + Ok(Signature(None)) + } + } +} + +#[derive(Eq, PartialEq, Clone, Debug, NetEncode, NetDecode)] +/// A property of a PlayerIdentity. +/// +/// Fields: +/// `name`: The name of the Property. +/// `value`: The value of the Property. +/// `signature`: The signature of the Property +/// +pub struct IdentityProperty { + /// The name of this Property. + pub name: String, + /// The value of this Property. + pub value: String, + /// The signature of this Property. + pub signature: Signature, } impl PlayerIdentity { + /// Create a new PlayerIdentity from uuid and username. pub fn new(username: String, uuid: u128) -> Self { - Self { username, uuid } + Self { + username, + uuid, + properties: LengthPrefixedVec::new(vec![]), + } } } diff --git a/src/lib/core/src/transform/position.rs b/src/lib/core/src/transform/position.rs index 3b1f0fdc..bcabafc1 100644 --- a/src/lib/core/src/transform/position.rs +++ b/src/lib/core/src/transform/position.rs @@ -1,5 +1,8 @@ +use ferrumc_macros::NetEncode; use std::fmt::{Debug, Display, Formatter}; +use std::io::Write; +#[derive(Copy, Clone, NetEncode)] pub struct Position { pub x: f64, pub y: f64, diff --git a/src/lib/derive_macros/src/events/mod.rs b/src/lib/derive_macros/src/events/mod.rs index ac4e0f95..77019bc4 100644 --- a/src/lib/derive_macros/src/events/mod.rs +++ b/src/lib/derive_macros/src/events/mod.rs @@ -123,10 +123,37 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { quote! {crate} } FoundCrate::Name(name) => { + let name = format_ident!("{}", name); quote! {::#name} } }; + let mut cancellable = false; + + for attr in crate::helpers::get_derive_attributes(&input, "event") { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("cancellable") { + cancellable = true; + return Ok(()); + } + + Err(meta.error("unrecognized attributes")) + }) + .unwrap(); + } + + let cancellable_impl = cancellable.then(|| { + quote! { + fn is_cancelled(&self) -> bool { + self.cancelled + } + + fn cancel(&mut self, value: bool) { + self.cancelled = value; + } + } + }); + let output = quote! { impl ::ferrumc_events::infrastructure::Event for #name { type Data = Self; @@ -136,6 +163,8 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { fn name() -> &'static str { stringify!(#name) } + + #cancellable_impl } }; diff --git a/src/lib/events/src/errors.rs b/src/lib/events/src/errors.rs index 7d48b763..f1529cd3 100644 --- a/src/lib/events/src/errors.rs +++ b/src/lib/events/src/errors.rs @@ -9,4 +9,6 @@ pub enum EventsError { }, #[error("A listener failed")] ListenerFailed, + #[error("{0}")] + Other(String), } diff --git a/src/lib/events/src/infrastructure.rs b/src/lib/events/src/infrastructure.rs index 16774b21..839d7c4b 100644 --- a/src/lib/events/src/infrastructure.rs +++ b/src/lib/events/src/infrastructure.rs @@ -1,7 +1,7 @@ use std::{any::Any, future::Future, pin::Pin, sync::LazyLock}; use dashmap::DashMap; -use futures::{stream, StreamExt}; +use futures::{stream, StreamExt, TryStreamExt}; /// A Lazily initialized HashMap wrapped in a ShardedLock optimized for reads. type LazyRwListenerMap = LazyLock>; @@ -52,10 +52,15 @@ impl Priority for EventListener { } } +enum EventTryError { + Acc(Data), + Err(Error), +} + #[allow(async_fn_in_trait)] pub trait Event: Sized + Send + Sync + 'static { /// Event data structure - type Data: Send + Sync; + type Data: Event; /// State type State: Send + Sync + Clone; @@ -66,43 +71,63 @@ pub trait Event: Sized + Send + Sync + 'static { /// Stringified name of the event fn name() -> &'static str; + fn is_cancelled(&self) -> bool { + false + } + + fn cancel(&mut self, _value: bool) { + unimplemented!(); + } + /// Trigger an event execution /// /// This method will pass the data to the listener with the highest priority which /// will give its result to the next one with a lesser priority and so on. /// - /// Returns `Ok(())` if the execution succeeded. `Err(EventsError)` ifa listener failed. - async fn trigger(event: Self::Data, state: Self::State) -> Result<(), Self::Error> { + /// Returns `Ok(Self::Data)` if the execution succeeded. `Err(EventsError)` if a listener failed. + async fn trigger(event: Self::Data, state: Self::State) -> Result { #[cfg(debug_assertions)] let start = std::time::Instant::now(); - let listeners = EVENTS_LISTENERS - .get(Self::name()) - .expect("Failed to find event listeners. Impossible;"); + let listeners = match EVENTS_LISTENERS.get(Self::name()) { + Some(listeners) => listeners, + None => { + return Ok(event); + } + }; // Convert listeners iterator into Stream - stream::iter(listeners.iter()) + let res = stream::iter(listeners.iter()) // TODO: Remove this since it's not possible to have a wrong type in the map of the event??? // Maybe some speedup? // Filter only listeners we can downcast into the correct type .filter_map(|dyn_list| async { dyn_list.downcast_ref::>() }) + .map(Ok) // Trigger listeners in a row - .fold(Ok(event), |intercepted, listener| { + .try_fold(Ok(event), |intercepted, listener| { let state = state.clone(); async move { - if intercepted.is_err() { - intercepted - } else { - (listener.listener)(intercepted.unwrap(), state).await + match intercepted { + Ok(event) => { + if !event.is_cancelled() { + Ok((listener.listener)(event, state).await) + } else { + Err(EventTryError::Acc(event)) + } + } + Err(e) => Err(EventTryError::Err(e)), } } }) - .await?; + .await; #[cfg(debug_assertions)] tracing::trace!("Event {} took {:?}", Self::name(), start.elapsed()); - Ok(()) + match res { + Ok(Ok(event)) | Err(EventTryError::Acc(event)) => Ok(event), + Ok(Err(e)) | Err(EventTryError::Err(e)) => Err(e), + } } /// Register a new event listener for this event diff --git a/src/lib/net/Cargo.toml b/src/lib/net/Cargo.toml index dd1ad654..f4f77287 100644 --- a/src/lib/net/Cargo.toml +++ b/src/lib/net/Cargo.toml @@ -31,3 +31,4 @@ uuid = { workspace = true, features = ["v4"] } async-trait = { workspace = true } byteorder = { workspace = true } ferrumc-state = { workspace = true } +#bitmask-enum = { workspace = true } diff --git a/src/lib/net/crates/codec/src/decode/primitives.rs b/src/lib/net/crates/codec/src/decode/primitives.rs index 651a262f..8afea78f 100644 --- a/src/lib/net/crates/codec/src/decode/primitives.rs +++ b/src/lib/net/crates/codec/src/decode/primitives.rs @@ -40,6 +40,12 @@ impl_for_primitives!( f64 ); +impl NetDecode for () { + fn decode(_reader: &mut R, _: &NetDecodeOpts) -> NetDecodeResult { + Ok(()) + } +} + impl NetDecode for bool { fn decode(reader: &mut R, _: &NetDecodeOpts) -> NetDecodeResult { Ok(::decode(reader, &NetDecodeOpts::None)? != 0) diff --git a/src/lib/net/crates/codec/src/encode/primitives.rs b/src/lib/net/crates/codec/src/encode/primitives.rs index 86931626..d0078784 100644 --- a/src/lib/net/crates/codec/src/encode/primitives.rs +++ b/src/lib/net/crates/codec/src/encode/primitives.rs @@ -45,6 +45,20 @@ impl_for_primitives!( f64 ); +impl NetEncode for () { + fn encode(&self, _writer: &mut W, _: &NetEncodeOpts) -> NetEncodeResult<()> { + Ok(()) + } + + async fn encode_async( + &self, + _writer: &mut W, + _: &NetEncodeOpts, + ) -> NetEncodeResult<()> { + Ok(()) + } +} + impl NetEncode for bool { fn encode(&self, writer: &mut W, _: &NetEncodeOpts) -> NetEncodeResult<()> { (*self as u8).encode(writer, &NetEncodeOpts::None) diff --git a/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs b/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs index 68a53b4e..bc1d4591 100644 --- a/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs +++ b/src/lib/net/crates/codec/src/net_types/length_prefixed_vec.rs @@ -2,29 +2,37 @@ use crate::decode::{NetDecode, NetDecodeOpts, NetDecodeResult}; use crate::encode::{NetEncode, NetEncodeOpts, NetEncodeResult}; use crate::net_types::var_int::VarInt; use std::io::{Read, Write}; +use std::ops::{Deref, DerefMut}; use tokio::io::AsyncWrite; -#[derive(Debug)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct LengthPrefixedVec { - pub length: VarInt, pub data: Vec, } impl Default for LengthPrefixedVec { fn default() -> Self { - Self { - length: VarInt::new(0), - data: Vec::new(), - } + Self { data: Vec::new() } } } impl LengthPrefixedVec { pub fn new(data: Vec) -> Self { - Self { - length: VarInt::new(data.len() as i32), - data, - } + Self { data } + } +} + +impl Deref for LengthPrefixedVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.data + } +} + +impl DerefMut for LengthPrefixedVec { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.data } } @@ -33,7 +41,7 @@ where T: NetEncode, { fn encode(&self, writer: &mut W, opts: &NetEncodeOpts) -> NetEncodeResult<()> { - self.length.encode(writer, opts)?; + VarInt::from(self.len()).encode(writer, opts)?; for item in &self.data { item.encode(writer, opts)?; @@ -47,7 +55,7 @@ where writer: &mut W, opts: &NetEncodeOpts, ) -> NetEncodeResult<()> { - self.length.encode_async(writer, opts).await?; + VarInt::from(self.len()).encode_async(writer, opts).await?; for item in &self.data { item.encode_async(writer, opts).await?; @@ -56,6 +64,7 @@ where Ok(()) } } + impl NetDecode for LengthPrefixedVec where T: NetDecode, @@ -68,6 +77,6 @@ where data.push(T::decode(reader, opts)?); } - Ok(Self { length, data }) + Ok(Self { data }) } } diff --git a/src/lib/net/crates/codec/src/net_types/var_int.rs b/src/lib/net/crates/codec/src/net_types/var_int.rs index 33231791..42010c81 100644 --- a/src/lib/net/crates/codec/src/net_types/var_int.rs +++ b/src/lib/net/crates/codec/src/net_types/var_int.rs @@ -8,7 +8,7 @@ use deepsize::DeepSizeOf; use std::io::{Read, Write}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -#[derive(Debug, Encode, Decode, Clone, DeepSizeOf)] +#[derive(Debug, Encode, Decode, Clone, DeepSizeOf, Eq)] pub struct VarInt { /// The value of the VarInt. pub val: i32, diff --git a/src/lib/net/src/connection.rs b/src/lib/net/src/connection.rs index 34df47bf..8489fd56 100644 --- a/src/lib/net/src/connection.rs +++ b/src/lib/net/src/connection.rs @@ -1,6 +1,10 @@ +use crate::errors::NetError; use crate::packets::incoming::packet_skeleton::PacketSkeleton; -use crate::utils::state::terminate_connection; -use crate::{handle_packet, NetResult}; +use crate::packets::outgoing::login_success::LoginSuccessPacket; +use crate::utils::state::TerminateConnectionPlayerExt; +use crate::{handle_packet, packets::outgoing::disconnect::DISCONNECT_STRING, NetResult}; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_ecs::entities::Entity; use ferrumc_events::infrastructure::Event; use ferrumc_macros::Event; use ferrumc_net_codec::encode::NetEncode; @@ -33,6 +37,7 @@ impl Default for ConnectionControl { Self::new() } } + #[derive(Clone)] pub enum ConnectionState { Handshaking, @@ -41,6 +46,7 @@ pub enum ConnectionState { Play, Configuration, } + impl ConnectionState { pub fn as_str(&self) -> &'static str { match self { @@ -131,6 +137,25 @@ impl Default for CompressionStatus { } } +pub async fn send_login_success( + state: Arc, + conn_id: usize, + identity: PlayerIdentity, +) -> NetResult<()> { + let mut writer = state.universe.get_mut::(conn_id)?; + + writer.send_packet( + LoginSuccessPacket::new(identity.clone()), + &NetEncodeOpts::WithLength, + )?; + + state + .universe + .add_component::(conn_id, identity)?; + + Ok(()) +} + pub async fn handle_connection(state: Arc, tcp_stream: TcpStream) -> NetResult<()> { let (mut reader, writer) = tcp_stream.into_split(); @@ -194,15 +219,24 @@ pub async fn handle_connection(state: Arc, tcp_stream: TcpStream) - .instrument(debug_span!("eid", %entity)) .inner() { - warn!( - "Failed to handle packet: {:?}. packet_id: {:02X}; conn_state: {}", - e, - packet_skele.id, - conn_state.as_str() - ); - // Kick the player (when implemented). - terminate_connection(state.clone(), entity, "Failed to handle packet".to_string()) - .await?; + match e { + NetError::Kick(msg) => { + entity + .terminate_connection(state.clone(), msg.clone()) + .await?; + } + _ => { + warn!( + "Failed to handle packet: {:?}. packet_id: {:02X}; conn_state: {}", + e, + packet_skele.id, + conn_state.as_str() + ); + entity + .terminate_connection(state.clone(), DISCONNECT_STRING.to_string()) + .await?; + } + } break 'recv; }; } @@ -231,6 +265,23 @@ pub struct PlayerDisconnectEvent { pub entity_id: usize, } +/// This event is triggered when the player attempts to log on to the server. +/// +/// Beware that not all components on the entity may be set yet this event is mostly for: +/// a custom handshaking protocol before the player logs in using login plugin messages/etc. +/// +#[derive(Event, Clone)] +#[event(cancellable)] +pub struct PlayerStartLoginEvent { + /// The entity that this event was fired for. + pub entity: Entity, + + /// This profile can be changed and after the event is finished this will be the new profile. + pub profile: PlayerIdentity, + + pub cancelled: bool, +} + /// Since parking_lot is single-threaded, we use spawn_blocking to remove all components from the entity asynchronously (on another thread). async fn remove_all_components_blocking(state: Arc, entity: usize) -> NetResult<()> { let res = diff --git a/src/lib/net/src/errors.rs b/src/lib/net/src/errors.rs index d24d3684..bf412191 100644 --- a/src/lib/net/src/errors.rs +++ b/src/lib/net/src/errors.rs @@ -43,6 +43,15 @@ pub enum NetError { #[error("{0}")] Chunk(#[from] ChunkError), + + #[error("{0}")] + Kick(ferrumc_text::TextComponent), +} + +impl NetError { + pub fn kick(reason: impl Into) -> Self { + Self::Kick(reason.into()) + } } #[derive(Debug, Error)] diff --git a/src/lib/net/src/packets/incoming/keep_alive.rs b/src/lib/net/src/packets/incoming/keep_alive.rs index df1f9425..fc6a1c1e 100644 --- a/src/lib/net/src/packets/incoming/keep_alive.rs +++ b/src/lib/net/src/packets/incoming/keep_alive.rs @@ -1,6 +1,6 @@ use crate::packets::outgoing::keep_alive::OutgoingKeepAlivePacket; use crate::packets::IncomingPacket; -use crate::utils::state::terminate_connection; +use crate::utils::state::TerminateConnectionPlayerExt; use crate::NetResult; use ferrumc_macros::{packet, NetDecode}; use ferrumc_state::ServerState; @@ -21,8 +21,9 @@ impl IncomingPacket for IncomingKeepAlivePacket { "Invalid keep alive packet received from {:?} with id {:?} (expected {:?})", conn_id, self.timestamp, last_sent_keep_alive.timestamp ); - if let Err(e) = - terminate_connection(state, conn_id, "Invalid keep alive packet".to_string()).await + if let Err(e) = conn_id + .terminate_connection(state, "Invalid keep alive packet".to_string()) + .await { debug!("Error terminating connection: {:?}", e); } diff --git a/src/lib/net/src/packets/incoming/server_bound_plugin_message.rs b/src/lib/net/src/packets/incoming/server_bound_plugin_message.rs index 2917488c..0eb1bde1 100644 --- a/src/lib/net/src/packets/incoming/server_bound_plugin_message.rs +++ b/src/lib/net/src/packets/incoming/server_bound_plugin_message.rs @@ -1,17 +1,38 @@ use crate::packets::IncomingPacket; use crate::NetResult; -use ferrumc_macros::packet; +use ferrumc_events::infrastructure::Event; +use ferrumc_macros::{packet, Event}; use ferrumc_net_codec::decode::{NetDecode, NetDecodeOpts, NetDecodeResult}; +use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_state::ServerState; +use std::fmt::Debug; use std::io::Read; use std::sync::Arc; -use tracing::debug; +use tracing::trace; + +/// This event triggers when a [LoginPluginResponse] is received. +/// +#[derive(Event, Debug)] +pub struct LoginPluginResponseEvent { + /// The entity that the event was triggered for + pub entity: usize, + /// The [LoginPluginResponse] packet received. + pub packet: LoginPluginResponse, +} #[derive(Debug)] #[packet(packet_id = "custom_payload", state = "configuration")] pub struct ServerBoundPluginMessage { - channel: String, - data: Vec, + pub channel: String, + pub data: Vec, +} + +#[derive(Debug, Clone)] +#[packet(packet_id = "custom_query_answer", state = "login")] +pub struct LoginPluginResponse { + pub message_id: VarInt, + pub success: bool, + pub data: Vec, } pub struct ClientMinecraftBrand { @@ -30,11 +51,11 @@ impl NetDecode for ServerBoundPluginMessage { impl IncomingPacket for ServerBoundPluginMessage { async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { - debug!("Received plugin message: {:?}", self); + trace!("Received plugin message: {:?}", self); if self.channel == "minecraft:brand" { let brand = String::from_utf8(self.data)?; - debug!("Client brand: {}", brand); + trace!("Client brand: {}", brand); state .universe @@ -44,3 +65,36 @@ impl IncomingPacket for ServerBoundPluginMessage { Ok(()) } } + +impl NetDecode for LoginPluginResponse { + fn decode(reader: &mut R, opts: &NetDecodeOpts) -> NetDecodeResult { + let message_id = ::decode(reader, opts)?; + let success = ::decode(reader, opts)?; + + let mut buf = Vec::::new(); + if success { + reader.read_to_end(&mut buf)?; + } + + Ok(Self { + message_id, + success, + data: buf, + }) + } +} + +impl IncomingPacket for LoginPluginResponse { + async fn handle(self, conn_id: usize, state: Arc) -> NetResult<()> { + LoginPluginResponseEvent::trigger( + LoginPluginResponseEvent { + entity: conn_id, + packet: self, + }, + Arc::clone(&state), + ) + .await?; + + Ok(()) + } +} diff --git a/src/lib/net/src/packets/outgoing/client_bound_plugin_message.rs b/src/lib/net/src/packets/outgoing/client_bound_plugin_message.rs new file mode 100644 index 00000000..0b23ce5e --- /dev/null +++ b/src/lib/net/src/packets/outgoing/client_bound_plugin_message.rs @@ -0,0 +1,65 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::{encode::NetEncode, net_types::var_int::VarInt}; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = "custom_payload", state = "configuration")] +pub struct ConfigurationPluginMessagePacket +where + T: NetEncode, +{ + pub channel: String, + pub data: T, +} + +#[derive(NetEncode)] +#[packet(packet_id = "custom_payload", state = "play")] +pub struct PlayPluginMessagePacket +where + T: NetEncode, +{ + pub channel: String, + pub data: T, +} + +#[derive(NetEncode, Clone)] +#[packet(packet_id = "custom_query", state = "login")] +pub struct LoginPluginMessagePacket +where + T: NetEncode, +{ + pub message_id: VarInt, + pub channel: String, + pub data: T, +} + +impl ConfigurationPluginMessagePacket +where + T: NetEncode, +{ + pub fn new(channel: String, data: T) -> Self { + Self { channel, data } + } +} + +impl PlayPluginMessagePacket +where + T: NetEncode, +{ + pub fn new(channel: String, data: T) -> Self { + Self { channel, data } + } +} + +impl LoginPluginMessagePacket +where + T: NetEncode, +{ + pub fn new(id: u32, channel: String, data: T) -> Self { + Self { + message_id: VarInt::new(id as i32), + channel, + data, + } + } +} diff --git a/src/lib/net/src/packets/outgoing/disconnect.rs b/src/lib/net/src/packets/outgoing/disconnect.rs index 77da9b29..1895276f 100644 --- a/src/lib/net/src/packets/outgoing/disconnect.rs +++ b/src/lib/net/src/packets/outgoing/disconnect.rs @@ -1,27 +1,53 @@ +use crate::connection::ConnectionState; +use crate::{errors::NetError, NetResult}; use ferrumc_macros::{packet, NetEncode}; -use ferrumc_text::{ComponentBuilder, TextComponent}; +use ferrumc_text::*; use std::io::Write; +pub const DISCONNECT_STRING: &str = "§cDisconnected"; + +#[derive(NetEncode)] +pub enum DisconnectPacket { + Login(LoginDisconnectPacket), + Play(PlayDisconnectPacket), +} + +#[derive(NetEncode)] +#[packet(packet_id = "login_disconnect", state = "login")] +pub struct LoginDisconnectPacket { + pub reason: JsonTextComponent, +} + #[derive(NetEncode)] #[packet(packet_id = "disconnect", state = "play")] -pub struct DisconnectPacket { +pub struct PlayDisconnectPacket { pub reason: TextComponent, } impl DisconnectPacket { - pub fn new(reason: TextComponent) -> Self { - Self { reason } + pub fn from>(state: &ConnectionState, reason: C) -> NetResult { + match state { + ConnectionState::Login => Ok(DisconnectPacket::Login(LoginDisconnectPacket::new( + reason.into(), + ))), + ConnectionState::Play => Ok(DisconnectPacket::Play(PlayDisconnectPacket::new(reason))), + _ => Err(NetError::InvalidState(state.clone() as u8)), + } } - pub fn from_string(reason: String) -> Self { - let reason = ComponentBuilder::text(reason); +} + +impl LoginDisconnectPacket { + pub fn new>(reason: C) -> Self { Self { - reason: reason.build(), + reason: reason.into(), } } } -impl Default for DisconnectPacket { - fn default() -> Self { - Self::from_string("FERRUMC-DISCONNECTED".to_string()) +impl PlayDisconnectPacket { + pub fn new>(reason: C) -> Self { + Self { + reason: reason.into(), + } } } diff --git a/src/lib/net/src/packets/outgoing/entity_metadata.rs b/src/lib/net/src/packets/outgoing/entity_metadata.rs index 1bf51732..39ae7152 100644 --- a/src/lib/net/src/packets/outgoing/entity_metadata.rs +++ b/src/lib/net/src/packets/outgoing/entity_metadata.rs @@ -67,6 +67,14 @@ pub mod constructors { value, } } + + pub fn entity_state_none() -> Self { + Self::new( + EntityMetadataIndexType::Byte, + EntityMetadataValue::Entity0(EntityStateMask::new()), + ) + } + /// To hide the name tag and stuff pub fn entity_sneaking_pressed() -> Self { Self::new( @@ -76,6 +84,7 @@ pub mod constructors { )), ) } + /// Actual sneaking visual, so you can see the player sneaking pub fn entity_sneaking_visual() -> Self { Self::new( diff --git a/src/lib/net/src/packets/outgoing/login_disconnect.rs b/src/lib/net/src/packets/outgoing/login_disconnect.rs deleted file mode 100644 index bb3c3fdd..00000000 --- a/src/lib/net/src/packets/outgoing/login_disconnect.rs +++ /dev/null @@ -1,14 +0,0 @@ -use ferrumc_macros::{packet, NetEncode}; -use std::io::Write; - -#[derive(NetEncode)] -#[packet(packet_id = "login_disconnect", state = "login")] -pub struct LoginDisconnectPacket<'a> { - pub reason: &'a str, -} - -impl<'a> LoginDisconnectPacket<'a> { - pub fn new(reason: &'a str) -> Self { - Self { reason } - } -} diff --git a/src/lib/net/src/packets/outgoing/login_success.rs b/src/lib/net/src/packets/outgoing/login_success.rs index f7d040d2..fe9976e0 100644 --- a/src/lib/net/src/packets/outgoing/login_success.rs +++ b/src/lib/net/src/packets/outgoing/login_success.rs @@ -1,22 +1,18 @@ +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_macros::{packet, NetEncode}; -use ferrumc_net_codec::net_types::var_int::VarInt; use std::io::Write; #[derive(NetEncode)] #[packet(packet_id = "game_profile", state = "login")] -pub struct LoginSuccessPacket<'a> { - pub uuid: u128, - pub username: &'a str, - pub number_of_properties: VarInt, +pub struct LoginSuccessPacket { + pub identity: PlayerIdentity, pub strict_error_handling: bool, } -impl<'a> LoginSuccessPacket<'a> { - pub fn new(uuid: u128, username: &'a str) -> Self { +impl LoginSuccessPacket { + pub fn new(identity: PlayerIdentity) -> Self { Self { - uuid, - username, - number_of_properties: VarInt::from(0), + identity, strict_error_handling: false, } } diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index ce630555..897279a9 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -2,29 +2,28 @@ pub mod chunk_and_light_data; pub mod chunk_batch_finish; pub mod chunk_batch_start; pub mod client_bound_known_packs; +pub mod client_bound_plugin_message; pub mod disconnect; +pub mod entity_animation; +pub mod entity_metadata; pub mod finish_configuration; pub mod game_event; pub mod keep_alive; -pub mod login_disconnect; pub mod login_play; pub mod login_success; pub mod ping_response; +pub mod player_info_remove; +pub mod player_info_update; pub mod registry_data; +pub mod remove_entities; pub mod set_center_chunk; pub mod set_default_spawn_position; pub mod set_render_distance; +pub mod spawn_entity; pub mod status_response; pub mod synchronize_player_position; pub mod update_time; -pub mod remove_entities; -pub mod spawn_entity; - -pub mod entity_animation; -pub mod entity_metadata; -pub mod player_info_update; - // --------- Movement ---------- pub mod set_head_rotation; pub mod teleport_entity; diff --git a/src/lib/net/src/packets/outgoing/player_info_remove.rs b/src/lib/net/src/packets/outgoing/player_info_remove.rs new file mode 100644 index 00000000..1bec3774 --- /dev/null +++ b/src/lib/net/src/packets/outgoing/player_info_remove.rs @@ -0,0 +1,17 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use std::io::Write; + +#[derive(NetEncode)] +#[packet(packet_id = "player_info_remove", state = "play")] +pub struct PlayerInfoRemovePacket { + pub player_uuids: LengthPrefixedVec, +} + +impl PlayerInfoRemovePacket { + pub fn new(uuids: Vec) -> Self { + Self { + player_uuids: LengthPrefixedVec::new(uuids), + } + } +} diff --git a/src/lib/net/src/packets/outgoing/player_info_update.rs b/src/lib/net/src/packets/outgoing/player_info_update.rs index 65f6e9db..72c01bcd 100644 --- a/src/lib/net/src/packets/outgoing/player_info_update.rs +++ b/src/lib/net/src/packets/outgoing/player_info_update.rs @@ -1,50 +1,79 @@ -use crate::utils::broadcast::get_all_play_players; -use ferrumc_core::identity::player_identity::PlayerIdentity; +use crate::utils::{broadcast::get_all_play_players, ecs_helpers::EntityExt}; +use ferrumc_core::identity::player_identity::*; use ferrumc_ecs::entities::Entity; use ferrumc_macros::{packet, NetEncode}; -use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; -use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_net_codec::net_types::{length_prefixed_vec::LengthPrefixedVec, var_int::VarInt}; use ferrumc_state::GlobalState; use std::io::Write; use tracing::debug; +#[derive(NetEncode, Debug, Eq, PartialEq, Clone)] +pub enum PlayerAction { + AddPlayer { + username: String, + properties: LengthPrefixedVec, + }, + InitializeChat { + // TODO + }, + UpdateGameMode(VarInt), + UpdateListed(bool), + UpdateLatency(VarInt), + UpdateDisplayName { + // TODO + }, +} + +impl PlayerAction { + pub fn flags(&self) -> u8 { + match self { + Self::AddPlayer { .. } => 0x01, + Self::InitializeChat { .. } => 0x02, + Self::UpdateGameMode(..) => 0x04, + Self::UpdateListed(..) => 0x08, + Self::UpdateLatency(..) => 0x10, + Self::UpdateDisplayName { .. } => 0x20, + } + } +} + +#[derive(NetEncode, Debug, Eq, PartialEq)] +pub struct PlayerInfo { + pub uuid: u128, + pub actions: Vec, +} + #[derive(NetEncode)] #[packet(packet_id = "player_info_update", state = "play")] pub struct PlayerInfoUpdatePacket { - pub actions: u8, - pub numbers_of_players: VarInt, - pub players: Vec, + actions: u8, + infos: LengthPrefixedVec, } impl PlayerInfoUpdatePacket { pub fn with_players(players: T) -> Self where - T: IntoIterator, + T: IntoIterator, { - let players: Vec = players.into_iter().collect(); + let players: Vec<_> = players.into_iter().collect(); Self { actions: players .iter() - .map(|player| player.get_actions_mask()) + .map(|player| { + player + .actions + .iter() + .fold(0, |acc, action| acc | action.flags()) + }) .fold(0, |acc, x| acc | x), - numbers_of_players: VarInt::new(players.len() as i32), - players, + infos: LengthPrefixedVec::new(players), } } /// The packet to be sent to all already connected players when a new player joins the server pub fn new_player_join_packet(new_player_id: Entity, state: &GlobalState) -> Self { - let identity = state - .universe - .get_component_manager() - .get::(new_player_id) - .unwrap(); - let uuid = identity.uuid; - let name = identity.username.clone(); - - let player = PlayerWithActions::add_player(uuid, name); - - Self::with_players(vec![player]) + let identity = new_player_id.get::(state).unwrap(); + Self::with_players(vec![PlayerInfo::from(&identity)]) } /// The packet to be sent to a new player when they join the server, @@ -53,24 +82,13 @@ impl PlayerInfoUpdatePacket { let players = { let mut players = get_all_play_players(state); players.retain(|&player| player != new_player_id); - players }; let players = players .into_iter() - .filter_map(|player| { - let identity = state - .universe - .get_component_manager() - .get::(player) - .ok()?; - let uuid = identity.uuid; - let name = identity.username.clone(); - - Some((uuid, name)) - }) - .map(|(uuid, name)| PlayerWithActions::add_player(uuid, name)) + .filter_map(|player| state.universe.get::(player).ok()) + .map(|identity| PlayerInfo::from(&identity)) .collect::>(); debug!("Sending PlayerInfoUpdatePacket with {:?} players", players); @@ -79,46 +97,17 @@ impl PlayerInfoUpdatePacket { } } -#[derive(NetEncode, Debug)] -pub struct PlayerWithActions { - pub uuid: u128, - pub actions: Vec, -} - -impl PlayerWithActions { - pub fn get_actions_mask(&self) -> u8 { - let mut mask = 0; - for action in &self.actions { - mask |= match action { - PlayerAction::AddPlayer { .. } => 0x01, - } - } - mask - } - - pub fn add_player(uuid: impl Into, name: impl Into) -> Self { +impl PlayerInfo { + pub fn from(profile: &PlayerIdentity) -> Self { Self { - uuid: uuid.into(), - actions: vec![PlayerAction::AddPlayer { - name: name.into(), - properties: LengthPrefixedVec::default(), - }], + uuid: profile.uuid, + actions: vec![ + PlayerAction::AddPlayer { + username: profile.username.clone(), + properties: profile.properties.clone(), + }, + PlayerAction::UpdateListed(true), + ], } } } - -#[derive(NetEncode, Debug)] -pub enum PlayerAction { - AddPlayer { - name: String, - properties: LengthPrefixedVec, - }, -} - -#[derive(NetEncode, Debug)] -pub struct PlayerProperty { - pub name: String, - pub value: String, - pub is_signed: bool, - pub signature: Option, -} diff --git a/src/lib/net/src/packets/outgoing/synchronize_player_position.rs b/src/lib/net/src/packets/outgoing/synchronize_player_position.rs index e36a447a..96880c5c 100644 --- a/src/lib/net/src/packets/outgoing/synchronize_player_position.rs +++ b/src/lib/net/src/packets/outgoing/synchronize_player_position.rs @@ -1,6 +1,9 @@ use crate::packets::outgoing::set_default_spawn_position::DEFAULT_SPAWN_POSITION; +use crate::{utils::ecs_helpers::EntityExt, NetResult}; +use ferrumc_core::transform::{position::Position, rotation::Rotation}; use ferrumc_macros::{packet, NetEncode}; use ferrumc_net_codec::net_types::var_int::VarInt; +use ferrumc_state::GlobalState; use std::io::Write; #[derive(NetEncode)] @@ -50,4 +53,18 @@ impl SynchronizePlayerPositionPacket { teleport_id, } } + + pub fn from_player(id: usize, state: GlobalState) -> NetResult { + let pos = id.get::(&state.clone())?; + let rot = id.get::(&state.clone())?; + Ok(Self::new( + pos.x, + pos.y, + pos.z, + rot.yaw, + rot.pitch, + 0, + VarInt::new(0), + )) + } } diff --git a/src/lib/net/src/utils/broadcast.rs b/src/lib/net/src/utils/broadcast.rs index fcbcc266..92f0e9c9 100644 --- a/src/lib/net/src/utils/broadcast.rs +++ b/src/lib/net/src/utils/broadcast.rs @@ -107,7 +107,7 @@ pub async fn broadcast( let (state, packet, async_callback, sync_callback) = (state, packet, opts.async_callback, opts.sync_callback); - futures::stream::iter(entities.into_iter()) + futures::stream::iter(entities) .fold( (state, packet, async_callback, sync_callback), move |(state, packet, async_callback, sync_callback), entity| { diff --git a/src/lib/net/src/utils/state.rs b/src/lib/net/src/utils/state.rs index fa32aa5a..14a31a86 100644 --- a/src/lib/net/src/utils/state.rs +++ b/src/lib/net/src/utils/state.rs @@ -1,7 +1,7 @@ use crate::{ - connection::{ConnectionControl, StreamWriter}, + connection::{ConnectionControl, ConnectionState, StreamWriter}, errors::NetError, - packets::outgoing::disconnect::DisconnectPacket, + packets::outgoing::disconnect::*, NetResult, }; use ferrumc_net_codec::encode::NetEncodeOpts; @@ -10,56 +10,66 @@ use tracing::{trace, warn}; use super::ecs_helpers::EntityExt; -// used codium for this function comment, very useful +pub trait TerminateConnectionPlayerExt { + #[allow(async_fn_in_trait)] + async fn terminate_connection( + &self, + state: GlobalState, + reason: impl Into, + ) -> NetResult<()>; +} -/// Terminates the connection of an entity with the given `conn_id`. -/// -/// Sends a disconnect packet with the given `reason` to the client, and marks the connection as -/// terminated. This will cause the connection to be dropped on the next tick of the -/// `ConnectionSystem`. -/// -/// # Errors -/// -/// Returns an error if the stream writer or connection control component cannot be accessed for -/// the given `conn_id`. -pub async fn terminate_connection( - state: GlobalState, - conn_id: usize, - reason: String, -) -> NetResult<()> { - let mut writer = match conn_id.get_mut::(&state.clone()) { - Ok(writer) => writer, - Err(e) => { - warn!("Failed to get stream writer for entity {}: {}", conn_id, e); - return Err(NetError::ECSError(e)); - } - }; +impl TerminateConnectionPlayerExt for usize { + // used codium for this function comment, very useful - if let Err(e) = writer.send_packet( - DisconnectPacket::from_string(reason), - &NetEncodeOpts::WithLength, - ) { - warn!( - "Failed to send disconnect packet to entity {}: {}", - conn_id, e - ); - return Err(e); - } + /// Terminates the connection of an entity with the given `conn_id`. + /// + /// Sends a disconnect packet with the given `reason` to the client, and marks the connection as + /// terminated. This will cause the connection to be dropped on the next tick of the + /// `ConnectionSystem`. + /// + /// # Errors + /// + /// Returns an error if the stream writer or connection control component cannot be accessed for + /// the given `conn_id`. + async fn terminate_connection( + &self, + state: GlobalState, + reason: impl Into, + ) -> NetResult<()> { + let mut writer = match self.get_mut::(&state.clone()) { + Ok(writer) => writer, + Err(e) => { + warn!("Failed to get stream writer for entity {}: {}", self, e); + return Err(NetError::ECSError(e)); + } + }; - match conn_id.get_mut::(&state.clone()) { - Ok(mut control) => { - control.should_disconnect = true; + let conn_state = self.get::(&state.clone())?; - trace!("Set should_disconnect to true for entity {}", conn_id); + if let Err(e) = writer.send_packet( + DisconnectPacket::from(&conn_state, reason)?, + &NetEncodeOpts::WithLength, + ) { + warn!("Failed to send disconnect packet to entity {}: {}", self, e); + return Err(e); } - Err(e) => { - warn!( - "Failed to get connection control for entity {}: {}", - conn_id, e - ); - return Err(NetError::ECSError(e)); + + match self.get_mut::(&state.clone()) { + Ok(mut control) => { + control.should_disconnect = true; + + trace!("Set should_disconnect to true for entity {}", self); + } + Err(e) => { + warn!( + "Failed to get connection control for entity {}: {}", + self, e + ); + return Err(NetError::ECSError(e)); + } } - } - Ok(()) + Ok(()) + } } diff --git a/src/lib/utils/config/src/server_config.rs b/src/lib/utils/config/src/server_config.rs index 127e2433..d45c7f5b 100644 --- a/src/lib/utils/config/src/server_config.rs +++ b/src/lib/utils/config/src/server_config.rs @@ -15,6 +15,7 @@ use serde_derive::{Deserialize, Serialize}; /// - `database` - [DatabaseConfig]: The configuration for the database. /// - `world`: The name of the world that the server will load. /// - `network_compression_threshold`: The threshold at which the server will compress network packets. +/// - `velocity`: Velocity settings. /// - `whitelist`: Whether the server whitelist is enabled or not. #[derive(Debug, Deserialize, Serialize)] pub struct ServerConfig { @@ -26,9 +27,24 @@ pub struct ServerConfig { pub database: DatabaseConfig, pub world: String, pub network_compression_threshold: i32, // Can be negative + #[serde(default)] + pub velocity: VelocityConfig, + #[serde(default)] pub whitelist: bool, } +/// The velocity configuration section from [ServerConfig]. +/// +/// Fields: +/// - `enabled`: If velocity support should be enabled. +/// - `secret`: The velocity secret used for modern forwarding. +#[derive(Debug, Deserialize, Serialize, Default)] +pub struct VelocityConfig { + /// see [velocity_secret](VelocityConfig::secret) + pub enabled: bool, + pub secret: String, +} + /// The database configuration section from [ServerConfig]. /// /// Fields: diff --git a/src/lib/world/src/chunk_format.rs b/src/lib/world/src/chunk_format.rs index 49d9fd26..681dac5c 100644 --- a/src/lib/world/src/chunk_format.rs +++ b/src/lib/world/src/chunk_format.rs @@ -92,6 +92,15 @@ impl Heightmaps { world_surface: vec![], } } + + pub fn motion_blocking_height(&self, x: usize, z: usize) -> i64 { + let bits_per_value = 9; // ceil(log2(383 + 1)) + let index = ((z % 16) * 16) + (x % 16); + let start = (index * bits_per_value) / 64; + let offset = (index * bits_per_value) % 64; + let value = self.motion_blocking[start] >> offset; + (value & ((1 << bits_per_value) - 1)) - 64 + } } impl Default for Heightmaps {