diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index d4f5ba990..aa143dd13 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -77,7 +77,6 @@ fn main() { fn entry(start_time: Instant) -> Result<(), BinaryError> { let state = launch::create_state(start_time)?; let global_state = Arc::new(state); - create_whitelist(); if !global_state .world diff --git a/src/bin/src/packet_handlers/play_packets/command.rs b/src/bin/src/packet_handlers/play_packets/command.rs index 0896a5295..2bfa5eec8 100644 --- a/src/bin/src/packet_handlers/play_packets/command.rs +++ b/src/bin/src/packet_handlers/play_packets/command.rs @@ -8,11 +8,13 @@ use ferrumc_commands::{ }; use ferrumc_core::mq; use ferrumc_net::ChatCommandPacketReceiver; +use ferrumc_state::{GlobalState, GlobalStateResource}; use ferrumc_text::{NamedColor, TextComponent, TextComponentBuilder}; fn resolve( input: String, sender: Sender, + state: GlobalState, ) -> Result<(Arc, CommandContext), Box> { let command = infrastructure::find_command(&input); if command.is_none() { @@ -33,6 +35,7 @@ fn resolve( input: input.clone(), command: command.clone(), sender, + state, }; Ok((command, ctx)) @@ -42,6 +45,7 @@ pub fn handle( receiver: Res, mut dispatch_msgs: MessageWriter, mut resolved_dispatch_msgs: MessageWriter, + state: Res, ) { for (event, entity) in receiver.0.try_iter() { let sender = Sender::Player(entity); @@ -50,7 +54,7 @@ pub fn handle( sender, }); - let resolved = resolve(event.command, sender); + let resolved = resolve(event.command, sender, state.0.clone()); match resolved { Err(err) => { mq::queue(*err, false, entity); diff --git a/src/bin/src/packet_handlers/play_packets/command_suggestions.rs b/src/bin/src/packet_handlers/play_packets/command_suggestions.rs index 8934e3f70..299636f22 100644 --- a/src/bin/src/packet_handlers/play_packets/command_suggestions.rs +++ b/src/bin/src/packet_handlers/play_packets/command_suggestions.rs @@ -10,7 +10,7 @@ use ferrumc_net::{ use ferrumc_net_codec::net_types::{ length_prefixed_vec::LengthPrefixedVec, prefixed_optional::PrefixedOptional, var_int::VarInt, }; -use ferrumc_state::GlobalStateResource; +use ferrumc_state::{GlobalState, GlobalStateResource}; use tracing::error; fn find_command(input: String) -> Option> { @@ -47,7 +47,12 @@ fn find_command(input: String) -> Option> { None } -fn create_ctx(input: String, command: Option>, sender: Sender) -> CommandContext { +fn create_ctx( + input: String, + command: Option>, + sender: Sender, + state: GlobalState, +) -> CommandContext { let input = input .strip_prefix(command.clone().map(|c| c.name).unwrap_or_default()) .unwrap_or(&input) @@ -58,6 +63,7 @@ fn create_ctx(input: String, command: Option>, sender: Sender) -> C input: input.clone(), command: command.unwrap_or(ROOT_COMMAND.clone()), sender, + state, } } @@ -82,7 +88,12 @@ pub fn handle( )) .unwrap_or(&input) .to_string(); - let mut ctx = create_ctx(command_arg.clone(), command.clone(), Sender::Player(entity)); + let mut ctx = create_ctx( + command_arg.clone(), + command.clone(), + Sender::Player(entity), + state.0.clone(), + ); let command_arg = command_arg.clone(); // ok borrow checker let tokens = command_arg.split(" ").collect::>(); let Some(current_token) = tokens.last() else { diff --git a/src/bin/src/packet_handlers/play_packets/confirm_player_teleport.rs b/src/bin/src/packet_handlers/play_packets/confirm_player_teleport.rs index 4e9554647..111686ab4 100644 --- a/src/bin/src/packet_handlers/play_packets/confirm_player_teleport.rs +++ b/src/bin/src/packet_handlers/play_packets/confirm_player_teleport.rs @@ -1,3 +1,15 @@ -pub fn handle() { - // TODO +use bevy_ecs::prelude::{Query, Res}; +use ferrumc_components::player::teleport_tracker::TeleportTracker; +use ferrumc_net::ConfirmPlayerTeleportReceiver; + +pub fn handle( + receiver: Res, + mut query: Query<&mut TeleportTracker>, +) { + for (_, eid) in receiver.0.try_iter() { + let Ok(mut tracker) = query.get_mut(eid) else { + continue; + }; + tracker.waiting_for_confirm = false; + } } diff --git a/src/bin/src/packet_handlers/play_packets/player_abilities.rs b/src/bin/src/packet_handlers/play_packets/player_abilities.rs index e9e593734..7713b2bce 100644 --- a/src/bin/src/packet_handlers/play_packets/player_abilities.rs +++ b/src/bin/src/packet_handlers/play_packets/player_abilities.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::{Entity, Query, Res}; use ferrumc_net::connection::StreamWriter; -use tracing::{debug, error, warn}; +use tracing::{error, trace, warn}; use ferrumc_net::PlayerAbilitiesReceiver; @@ -42,7 +42,7 @@ pub fn handle( // --- Validation --- if abilities.may_fly { // Player is allowed to fly. Update the server's state. - debug!( + trace!( "Player {} toggled flying to: {}", trigger_eid.index(), client_is_flying diff --git a/src/bin/src/packet_handlers/play_packets/player_command.rs b/src/bin/src/packet_handlers/play_packets/player_command.rs index d5c790c7e..74635ce23 100644 --- a/src/bin/src/packet_handlers/play_packets/player_command.rs +++ b/src/bin/src/packet_handlers/play_packets/player_command.rs @@ -6,7 +6,7 @@ use ferrumc_net::packets::incoming::player_command::PlayerCommandAction; use ferrumc_net::packets::outgoing::entity_metadata::{EntityMetadata, EntityMetadataPacket}; use ferrumc_net::PlayerCommandPacketReceiver; use ferrumc_net_codec::net_types::var_int::VarInt; -use tracing::debug; +use tracing::log::trace; /// Handles PlayerCommand packets (sprinting, leave bed, etc.) /// Note: Sneaking is handled via PlayerInput packet, NOT here @@ -23,9 +23,11 @@ pub fn handle( let entity_id = VarInt::new(identity.short_uuid); - debug!( + trace!( "PlayerCommand: {:?} from {} (entity_id={})", - event.action, identity.username, identity.short_uuid + event.action, + identity.username, + identity.short_uuid ); match event.action { diff --git a/src/bin/src/packet_handlers/play_packets/set_held_item.rs b/src/bin/src/packet_handlers/play_packets/set_held_item.rs index 917fef101..8e36e384d 100644 --- a/src/bin/src/packet_handlers/play_packets/set_held_item.rs +++ b/src/bin/src/packet_handlers/play_packets/set_held_item.rs @@ -2,7 +2,7 @@ use bevy_ecs::prelude::{Query, Res}; use ferrumc_inventories::hotbar::Hotbar; use ferrumc_net::SetHeldItemReceiver; use ferrumc_state::GlobalStateResource; -use tracing::{debug, error}; +use tracing::{error, trace}; pub fn handle( receiver: Res, @@ -14,9 +14,10 @@ pub fn handle( if 0 <= event.slot_index && event.slot_index < 9 { if let Ok(mut hotbar) = query.get_mut(entity) { hotbar.selected_slot = event.slot_index as u8; - debug!( + trace!( "Set held item for player {} to slot {}", - entity, event.slot_index + entity, + event.slot_index ); } else { error!("Could not find hotbar for player {}", entity); diff --git a/src/bin/src/packet_handlers/play_packets/set_player_position.rs b/src/bin/src/packet_handlers/play_packets/set_player_position.rs index cec33f5bd..8c9120df4 100644 --- a/src/bin/src/packet_handlers/play_packets/set_player_position.rs +++ b/src/bin/src/packet_handlers/play_packets/set_player_position.rs @@ -1,5 +1,6 @@ use bevy_ecs::prelude::{MessageWriter, Query, Res}; +use ferrumc_components::player::teleport_tracker::TeleportTracker; use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::position::Position; use ferrumc_messages::chunk_calc::ChunkCalc; @@ -9,12 +10,16 @@ use tracing::trace; pub fn handle( receiver: Res, - mut query: Query<(&mut Position, &mut OnGround)>, + mut query: Query<(&mut Position, &mut OnGround, &TeleportTracker)>, mut movement_messages: MessageWriter, mut chunk_calc_messages: MessageWriter, ) { for (event, eid) in receiver.0.try_iter() { - if let Ok((mut pos, mut ground)) = query.get_mut(eid) { + if let Ok((mut pos, mut ground, tracker)) = query.get_mut(eid) { + if tracker.waiting_for_confirm { + // Ignore position updates while waiting for teleport confirmation + continue; + } let new_pos = Position::new(event.x, event.feet_y, event.z); // Check if chunk changed diff --git a/src/bin/src/packet_handlers/play_packets/set_player_position_and_rotation.rs b/src/bin/src/packet_handlers/play_packets/set_player_position_and_rotation.rs index ac195c47d..87637c65c 100644 --- a/src/bin/src/packet_handlers/play_packets/set_player_position_and_rotation.rs +++ b/src/bin/src/packet_handlers/play_packets/set_player_position_and_rotation.rs @@ -1,5 +1,6 @@ use bevy_ecs::prelude::Query; use bevy_ecs::prelude::{MessageWriter, Res}; +use ferrumc_components::player::teleport_tracker::TeleportTracker; use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; @@ -11,10 +12,19 @@ pub fn handle( receiver: Res, mut movement_messages: MessageWriter, mut chunk_calc_messages: MessageWriter, - mut query: Query<(&mut Position, &mut Rotation, &mut OnGround)>, + mut query: Query<( + &mut Position, + &mut Rotation, + &mut OnGround, + &mut TeleportTracker, + )>, ) { for (event, eid) in receiver.0.try_iter() { - if let Ok((mut pos, mut rot, mut ground)) = query.get_mut(eid) { + if let Ok((mut pos, mut rot, mut ground, tracker)) = query.get_mut(eid) { + if tracker.waiting_for_confirm { + // Ignore position updates while waiting for teleport confirmation + continue; + } let new_pos = Position::new(event.x, event.feet_y, event.z); let new_rot = Rotation::new(event.yaw, event.pitch); let on_ground = event.flags & 0x01 != 0; diff --git a/src/bin/src/packet_handlers/play_packets/set_player_rotation.rs b/src/bin/src/packet_handlers/play_packets/set_player_rotation.rs index 81cbba9dc..c7162a425 100644 --- a/src/bin/src/packet_handlers/play_packets/set_player_rotation.rs +++ b/src/bin/src/packet_handlers/play_packets/set_player_rotation.rs @@ -1,4 +1,5 @@ use bevy_ecs::prelude::{MessageWriter, Query, Res}; +use ferrumc_components::player::teleport_tracker::TeleportTracker; use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::rotation::Rotation; use ferrumc_net::packets::packet_messages::Movement; @@ -7,10 +8,14 @@ use ferrumc_net::SetPlayerRotationPacketReceiver; pub fn handle( receiver: Res, mut movement_messages: MessageWriter, - mut query: Query<(&mut Rotation, &mut OnGround)>, + mut query: Query<(&mut Rotation, &mut OnGround, &mut TeleportTracker)>, ) { for (event, eid) in receiver.0.try_iter() { - if let Ok((mut rot, mut ground)) = query.get_mut(eid) { + if let Ok((mut rot, mut ground, tracker)) = query.get_mut(eid) { + if tracker.waiting_for_confirm { + // Ignore rotation updates while waiting for teleport confirmation + continue; + } let new_rot = Rotation::new(event.yaw, event.pitch); let on_ground = event.flags & 0x01 != 0; diff --git a/src/bin/src/systems/chunk_calculator.rs b/src/bin/src/systems/chunk_calculator.rs index bb6281170..56a7679f9 100644 --- a/src/bin/src/systems/chunk_calculator.rs +++ b/src/bin/src/systems/chunk_calculator.rs @@ -1,5 +1,6 @@ use bevy_ecs::prelude::{MessageReader, Query}; use bevy_math::IVec2; +use ferrumc_components::player::client_information::ClientInformation; use ferrumc_config::server_config::get_global_config; use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; use ferrumc_core::transform::position::Position; @@ -8,15 +9,17 @@ use ferrumc_world::pos::ChunkPos; pub fn handle( mut messages: MessageReader, - mut query: Query<(&Position, &mut ChunkReceiver)>, + mut query: Query<(&Position, &mut ChunkReceiver, &ClientInformation)>, ) { for message in messages.read() { - let (position, mut chunk_receiver) = match query.get_mut(message.0) { + let (position, mut chunk_receiver, client_info) = match query.get_mut(message.0) { Ok(data) => data, Err(_) => continue, // Skip if the player does not exist }; - let radius = get_global_config().chunk_render_distance as i32; + let server_render_distance = get_global_config().chunk_render_distance as i32; + let client_view_distance = client_info.view_distance as i32; + let radius = server_render_distance.min(client_view_distance); let player_chunk = ChunkPos::from(position.coords); let mut queued_chunks = Vec::new(); diff --git a/src/bin/src/systems/listeners/player_tp.rs b/src/bin/src/systems/listeners/player_tp.rs index b761c3ec5..3c3d0949c 100644 --- a/src/bin/src/systems/listeners/player_tp.rs +++ b/src/bin/src/systems/listeners/player_tp.rs @@ -1,47 +1,82 @@ -use bevy_ecs::prelude::{MessageReader, MessageWriter, Query}; +use bevy_ecs::prelude::{Entity, MessageReader, MessageWriter, Query}; +use ferrumc_components::player::teleport_tracker::TeleportTracker; +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::position::Position; -use ferrumc_core::transform::rotation::Rotation; use ferrumc_messages::chunk_calc::ChunkCalc; use ferrumc_messages::entity_update::SendEntityUpdate; use ferrumc_messages::teleport_player::TeleportPlayer; use ferrumc_net::connection::StreamWriter; +use ferrumc_net::packets::outgoing::entity_position_sync::TeleportEntityPacket; use ferrumc_net::packets::outgoing::synchronize_player_position::SynchronizePlayerPositionPacket; use tracing::error; pub fn teleport_player( - mut query: Query<(&StreamWriter, &mut Position, &Rotation)>, + mut query: Query<(Entity, &StreamWriter, &mut Position, &mut TeleportTracker)>, + id_query: Query<&PlayerIdentity>, mut message_reader: MessageReader, mut chunk_calc_msg: MessageWriter, mut player_update_msg: MessageWriter, ) { for message in message_reader.read() { - let entity = message.entity; - - // Notify the chunk calculation system to recalculate chunks for this player - chunk_calc_msg.write(ChunkCalc(entity)); - - // Notify the player update system to send the new position to the client - player_update_msg.write(SendEntityUpdate(entity)); - let Ok((conn, mut pos, rot)) = query.get_mut(entity) else { - continue; + let message_entity = message.entity; + let id = match id_query.get(message_entity) { + Ok(id) => id, + Err(err) => { + error!( + "Failed to get PlayerIdentity for entity {:?}: {}", + message_entity, err + ); + continue; + } }; - pos.x = message.x; - pos.y = message.y; - pos.z = message.z; - if let Err(err) = conn.send_packet(SynchronizePlayerPositionPacket { - teleport_id: rand::random::().into(), - x: message.x, - y: message.y, - z: message.z, - vel_x: message.vel_x, - vel_y: message.vel_y, - vel_z: message.vel_z, - yaw: rot.yaw, - pitch: rot.pitch, - flags: 0, - }) { - error!("Failed to send teleport packet: {}", err); - continue; + for (entity, conn, mut pos, mut tracker) in query.iter_mut() { + if entity == message_entity { + // Block movement tracking until the player has been teleported + tracker.waiting_for_confirm = true; + pos.x = message.x; + pos.y = message.y; + pos.z = message.z; + // If it's the entity we are trying to teleport, send the sync player pos packet + if let Err(err) = conn.send_packet(SynchronizePlayerPositionPacket { + teleport_id: rand::random::().into(), + x: message.x, + y: message.y, + z: message.z, + vel_x: message.vel_x, + vel_y: message.vel_y, + vel_z: message.vel_z, + yaw: message.yaw, + pitch: message.pitch, + flags: 0, + }) { + error!("Failed to send teleport packet: {}", err); + continue; + } + } else { + // Otherwise send teleport entity packet. This ideally should be handled by the send + // entity updates system, but it seems to be a bit buggy + if let Err(err) = conn.send_packet(TeleportEntityPacket { + entity_id: id.short_uuid.into(), + x: message.x, + y: message.y, + z: message.z, + vel_x: 0.0, + vel_y: 0.0, + vel_z: 0.0, + yaw: message.yaw, + pitch: message.pitch, + on_ground: false, + }) { + error!("Failed to send teleport packet: {}", err); + continue; + } + } } + + // Notify the player update system to send the new position to the client + player_update_msg.write(SendEntityUpdate(message_entity)); + + // Notify the chunk calculation system to recalculate chunks for this player + chunk_calc_msg.write(ChunkCalc(message_entity)); } } diff --git a/src/bin/src/systems/new_connections.rs b/src/bin/src/systems/new_connections.rs index efc1fa16e..580f38f79 100644 --- a/src/bin/src/systems/new_connections.rs +++ b/src/bin/src/systems/new_connections.rs @@ -1,5 +1,6 @@ use bevy_ecs::prelude::{Commands, Res, Resource}; use crossbeam_channel::Receiver; +use ferrumc_components::player::teleport_tracker::TeleportTracker; use ferrumc_components::player::{ gamemode::GameModeComponent, offline_player_data::OfflinePlayerData, pending_events::PendingPlayerJoin, player_bundle::PlayerBundle, sneak::SneakState, @@ -83,6 +84,9 @@ pub fn accept_new_connections( last_sent_keep_alive: Instant::now(), }, PendingPlayerJoin(new_connection.player_identity.clone()), + TeleportTracker { + waiting_for_confirm: false, + }, )); let entity_id = entity_commands.id(); diff --git a/src/bin/src/systems/send_entity_updates.rs b/src/bin/src/systems/send_entity_updates.rs index e570c5b5f..5bbb6015a 100644 --- a/src/bin/src/systems/send_entity_updates.rs +++ b/src/bin/src/systems/send_entity_updates.rs @@ -1,5 +1,7 @@ +#![expect(clippy::type_complexity)] use bevy_ecs::prelude::{MessageReader, Query}; use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_core::transform::grounded::OnGround; use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; @@ -18,7 +20,8 @@ pub fn handle( &Velocity, &Rotation, &mut LastSyncedPosition, - &EntityIdentity, + Option<&EntityIdentity>, + Option<&PlayerIdentity>, &OnGround, )>, mut conn_query: Query<&StreamWriter>, @@ -28,12 +31,24 @@ pub fn handle( for msg in reader.read() { entities_to_update.push(msg.0); } - entities_to_update.dedup(); for entity in entities_to_update { - if let Ok((pos, vel, rot, mut last_synced, id, grounded)) = query.get_mut(entity) { - if last_synced.0.distance(pos.coords) > 8.0 { + if let Ok((pos, vel, rot, mut last_synced, entity_id_opt, player_id_opt, grounded)) = + query.get_mut(entity) + { + let id = if let Some(entity_id) = entity_id_opt { + entity_id.entity_id + } else if let Some(player_id) = player_id_opt { + player_id.short_uuid + } else { + warn!( + "Tried to send entity update for entity without identity: {:?}", + entity + ); + continue; + }; + if last_synced.0.distance(pos.coords) >= 8.0 { let packet = TeleportEntityPacket { - entity_id: id.entity_id.into(), + entity_id: id.into(), x: pos.x, y: pos.y, z: pos.z, @@ -63,7 +78,7 @@ pub fn handle( ) }; let packet = UpdateEntityPositionAndRotationPacket { - entity_id: id.entity_id.into(), + entity_id: id.into(), delta_x, delta_y, delta_z, diff --git a/src/lib/commands/Cargo.toml b/src/lib/commands/Cargo.toml index ef4dbd143..c17663171 100644 --- a/src/lib/commands/Cargo.toml +++ b/src/lib/commands/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ferrumc-commands" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] thiserror = { workspace = true } @@ -15,8 +15,11 @@ ferrumc-macros = { workspace = true } bevy_ecs = { workspace = true } ferrumc-net-codec = { workspace = true } regex = { workspace = true } -ferrumc-components = {workspace = true } +ferrumc-components = { workspace = true } ferrumc-nbt = { workspace = true } +ferrumc-state = { workspace = true } +uuid = { workspace = true } +rand = { workspace = true } [dev-dependencies] # Needed for the ServerState mock... :concern: ferrumc-world = { workspace = true } diff --git a/src/lib/commands/src/arg/duration.rs b/src/lib/commands/src/arg/duration.rs index 361641389..a1292b9f0 100644 --- a/src/lib/commands/src/arg/duration.rs +++ b/src/lib/commands/src/arg/duration.rs @@ -4,7 +4,7 @@ use regex::Regex; use crate::{CommandContext, Suggestion}; -use super::{primitive::PrimitiveArgument, utils::parser_error, CommandArgument, ParserResult}; +use super::{CommandArgument, ParserResult, primitive::PrimitiveArgument, utils::parser_error}; static PATTERN: LazyLock = LazyLock::new(|| Regex::new("(([1-9][0-9]+|[1-9])[dhms])").unwrap()); diff --git a/src/lib/commands/src/arg/entities/any_entity.rs b/src/lib/commands/src/arg/entities/any_entity.rs new file mode 100644 index 000000000..b90e9e761 --- /dev/null +++ b/src/lib/commands/src/arg/entities/any_entity.rs @@ -0,0 +1,9 @@ +use bevy_ecs::prelude::Entity; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; + +pub(crate) fn resolve_any_entity( + iter: impl Iterator, Option<&PlayerIdentity>)>, +) -> Vec { + iter.map(|(entity, _, _)| entity).collect() +} diff --git a/src/lib/commands/src/arg/entities/any_player.rs b/src/lib/commands/src/arg/entities/any_player.rs new file mode 100644 index 000000000..543fc1c20 --- /dev/null +++ b/src/lib/commands/src/arg/entities/any_player.rs @@ -0,0 +1,15 @@ +use bevy_ecs::entity::Entity; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; + +pub(crate) fn resolve_any_player( + iter: impl Iterator, Option<&PlayerIdentity>)>, +) -> Vec { + let mut players = Vec::new(); + for (entity, _, player_id) in iter { + if player_id.is_some() { + players.push(entity); + } + } + players +} diff --git a/src/lib/commands/src/arg/entities/entity_uuid.rs b/src/lib/commands/src/arg/entities/entity_uuid.rs new file mode 100644 index 000000000..e58336015 --- /dev/null +++ b/src/lib/commands/src/arg/entities/entity_uuid.rs @@ -0,0 +1,18 @@ +use bevy_ecs::entity::Entity; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use uuid::Uuid; + +pub(crate) fn resolve_uuid( + uuid: Uuid, + iter: impl Iterator, Option<&PlayerIdentity>)>, +) -> Option { + for (entity, entity_id_opt, player_id_opt) in iter { + match (player_id_opt, entity_id_opt) { + (Some(player_id), _) if player_id.uuid == uuid => return Some(entity), + (_, Some(entity_id)) if entity_id.uuid == uuid => return Some(entity), + _ => {} + } + } + None +} diff --git a/src/lib/commands/src/arg/entities/mod.rs b/src/lib/commands/src/arg/entities/mod.rs new file mode 100644 index 000000000..c69c90794 --- /dev/null +++ b/src/lib/commands/src/arg/entities/mod.rs @@ -0,0 +1,357 @@ +mod any_entity; +mod any_player; +mod entity_uuid; +mod player; +mod random_player; + +use crate::arg::primitive::PrimitiveArgument; +use crate::arg::{CommandArgument, ParserResult}; +use crate::{CommandContext, Suggestion}; +use ::uuid::Uuid; +use bevy_ecs::prelude::Entity; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; + +/// Represents an entity argument in a command. +/// It can be a player name, UUID, or special selectors like @e, @p, @r, @a. +/// This won't get you an entity directly, use `resolve()` to get the entities. +/// +/// # Example +/// ```ignore +/// # use ferrumc_commands::arg::entities::EntityArgument; +/// # use ferrumc_core::identity::entity_identity::EntityIdentity; +/// # use ferrumc_core::identity::player_identity::PlayerIdentity; +/// # use bevy_ecs::prelude::World; +/// +/// fn my_command(query: Query<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>) { +/// let arg = EntityArgument::PlayerName("Steve".to_string()); +/// let result = arg.resolve(query.iter()); +/// assert_eq!(result, vec![entity]); +/// } +/// ``` +#[derive(Clone, Debug, PartialEq)] +pub enum EntityArgument { + PlayerName(String), + Uuid(Uuid), + AnyEntity, + AnyPlayer, + // NearestPlayer, + RandomPlayer, +} + +impl CommandArgument for EntityArgument { + fn parse(ctx: &mut CommandContext) -> ParserResult { + const PREFIXES: &[(&str, EntityArgument)] = &[ + ("@e", EntityArgument::AnyEntity), + // ("@p", EntityArgument::NearestPlayer), + ("@r", EntityArgument::RandomPlayer), + ("@a", EntityArgument::AnyPlayer), + ]; + let input = ctx.input.read_string(); + for (prefix, entity_type) in PREFIXES { + if input == *prefix { + return Ok(entity_type.clone()); + } + } + if input.len() == 36 && input.chars().all(|c| c.is_ascii_hexdigit() || c == '-') { + let uuid = Uuid::parse_str(&input) + .map_err(|_| crate::arg::utils::parser_error("invalid UUID format"))?; + Ok(EntityArgument::Uuid(uuid)) + } else { + Ok(EntityArgument::PlayerName(input)) + } + } + + fn primitive() -> PrimitiveArgument { + PrimitiveArgument::word() + } + + fn suggest(ctx: &mut CommandContext) -> Vec { + let mut suggestions = vec![ + Suggestion { + content: "@e".to_string(), + tooltip: Some(ferrumc_nbt::NBT::new("Any Entity".into())), + }, + // Suggestion { + // content: "@p".to_string(), + // tooltip: Some(ferrumc_nbt::NBT::new("Nearest Player".into())), + // }, + Suggestion { + content: "@r".to_string(), + tooltip: Some(ferrumc_nbt::NBT::new("Random Player".into())), + }, + Suggestion { + content: "@a".to_string(), + tooltip: Some(ferrumc_nbt::NBT::new("All Players".into())), + }, + ]; + let state = ctx.state.clone(); + for kv in &state.clone().players.player_list { + let (_, (uuid, name)) = kv.pair(); + suggestions.push(Suggestion { + content: name.clone(), + tooltip: Some(ferrumc_nbt::NBT::new( + Uuid::from_u128(*uuid) + .as_hyphenated() + .to_string() + .to_uppercase() + .into(), + )), + }); + } + suggestions + } +} + +impl EntityArgument { + pub fn resolve( + &self, + iter: impl Iterator, Option<&PlayerIdentity>)>, + ) -> Vec { + match self { + EntityArgument::PlayerName(name) => player::resolve_player_name(name.clone(), iter) + .map(|e| vec![e]) + .unwrap_or_default(), + EntityArgument::Uuid(uuid) => entity_uuid::resolve_uuid(*uuid, iter) + .map(|e| vec![e]) + .unwrap_or_default(), + EntityArgument::AnyEntity => any_entity::resolve_any_entity(iter), + EntityArgument::AnyPlayer => any_player::resolve_any_player(iter), + // EntityArgument::NearestPlayer => { + // // TODO: Figure this out + // vec![] + // } + EntityArgument::RandomPlayer => random_player::resolve_random_player(iter) + .map(|e| vec![e]) + .unwrap_or_default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Command, CommandInput, Sender}; + use bevy_ecs::prelude::World; + use ferrumc_core::identity::entity_identity::EntityIdentity; + use ferrumc_core::identity::player_identity::PlayerIdentity; + use ferrumc_state::create_test_state; + use std::sync::Arc; + + #[test] + fn test_parse_entity_argument() { + let mut ctx = CommandContext { + input: CommandInput { + input: "Steve".to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let arg = EntityArgument::parse(&mut ctx).unwrap(); + assert_eq!(arg, EntityArgument::PlayerName("Steve".to_string())); + + let mut ctx = CommandContext { + input: CommandInput { + input: "@e".to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let arg = EntityArgument::parse(&mut ctx).unwrap(); + assert_eq!(arg, EntityArgument::AnyEntity); + + // let mut ctx = CommandContext { + // input: CommandInput { + // input: "@p".to_string(), + // cursor: 0, + // }, + // command: Arc::new(Command { + // name: "", + // args: vec![], + // }), + // sender: Sender::Server, + // }; + // let arg = EntityArgument::parse(&mut ctx).unwrap(); + // assert_eq!(arg, EntityArgument::NearestPlayer); + + let mut ctx = CommandContext { + input: CommandInput { + input: "@r".to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let arg = EntityArgument::parse(&mut ctx).unwrap(); + assert_eq!(arg, EntityArgument::RandomPlayer); + + let mut ctx = CommandContext { + input: CommandInput { + input: "@a".to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let arg = EntityArgument::parse(&mut ctx).unwrap(); + assert_eq!(arg, EntityArgument::AnyPlayer); + + let uuid_str = "123e4567-e89b-12d3-a456-426614174000"; + let mut ctx = CommandContext { + input: CommandInput { + input: uuid_str.to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let arg = EntityArgument::parse(&mut ctx).unwrap(); + assert_eq!( + arg, + EntityArgument::Uuid(Uuid::parse_str(uuid_str).unwrap()) + ); + } + + #[test] + fn test_resolves_name() { + let mut world = World::new(); + let entity = world + .spawn(( + EntityIdentity { + entity_id: 0, + uuid: Uuid::new_v4(), + }, + PlayerIdentity { + username: "Steve".to_string(), + uuid: Default::default(), + short_uuid: 0, + properties: vec![], + }, + )) + .id(); + + let arg = EntityArgument::PlayerName("Steve".to_string()); + let result = arg.resolve( + world + .query::<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>() + .iter(&world), + ); + assert_eq!(result, vec![entity]); + } + + #[test] + fn test_resolves_uuid() { + let mut world = World::new(); + let test_uuid = Uuid::new_v4(); + let entity = world + .spawn((EntityIdentity { + entity_id: 0, + uuid: test_uuid, + },)) + .id(); + let arg = EntityArgument::Uuid(test_uuid); + let result = arg.resolve( + world + .query::<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>() + .iter(&world), + ); + assert_eq!(result, vec![entity]); + } + + #[test] + fn test_resolves_any_entity() { + let mut world = World::new(); + let entity1 = world + .spawn(EntityIdentity { + entity_id: 0, + uuid: Uuid::new_v4(), + }) + .id(); + let entity2 = world + .spawn(EntityIdentity { + entity_id: 1, + uuid: Uuid::new_v4(), + }) + .id(); + let arg = EntityArgument::AnyEntity; + let result = arg.resolve( + world + .query::<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>() + .iter(&world), + ); + assert_eq!(result.len(), 2); + assert!(result.contains(&entity1)); + assert!(result.contains(&entity2)); + } + + #[test] + fn test_resolves_any_player() { + let mut world = World::new(); + let entity1 = world + .spawn(( + EntityIdentity { + entity_id: 0, + uuid: Uuid::new_v4(), + }, + PlayerIdentity { + username: "Steve".to_string(), + uuid: Uuid::new_v4(), + short_uuid: 0, + properties: vec![], + }, + )) + .id(); + let entity2 = world + .spawn(( + EntityIdentity { + entity_id: 1, + uuid: Uuid::new_v4(), + }, + PlayerIdentity { + username: "Alex".to_string(), + uuid: Uuid::new_v4(), + short_uuid: 1, + properties: vec![], + }, + )) + .id(); + let non_player_entity = world + .spawn(EntityIdentity { + entity_id: 2, + uuid: Uuid::new_v4(), + }) + .id(); + let arg = EntityArgument::AnyPlayer; + let result = arg.resolve( + world + .query::<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>() + .iter(&world), + ); + assert_eq!(result.len(), 2); + assert!(result.contains(&entity1)); + assert!(result.contains(&entity2)); + assert!(!result.contains(&non_player_entity)); + } +} diff --git a/src/lib/commands/src/arg/entities/player.rs b/src/lib/commands/src/arg/entities/player.rs new file mode 100644 index 000000000..d7a8c7bd4 --- /dev/null +++ b/src/lib/commands/src/arg/entities/player.rs @@ -0,0 +1,17 @@ +use bevy_ecs::prelude::Entity; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; + +pub(crate) fn resolve_player_name( + name: String, + iter: impl Iterator, Option<&PlayerIdentity>)>, +) -> Option { + for (entity, _, player_id) in iter { + if let Some(identity) = player_id + && identity.username == name + { + return Some(entity); + } + } + None +} diff --git a/src/lib/commands/src/arg/entities/random_player.rs b/src/lib/commands/src/arg/entities/random_player.rs new file mode 100644 index 000000000..c93684b4b --- /dev/null +++ b/src/lib/commands/src/arg/entities/random_player.rs @@ -0,0 +1,18 @@ +use bevy_ecs::entity::Entity; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use rand::prelude::IteratorRandom; + +pub(crate) fn resolve_random_player( + iter: impl Iterator, Option<&PlayerIdentity>)>, +) -> Option { + let mut rng = rand::thread_rng(); + iter.filter_map(|(entity, _, player_id)| { + if player_id.is_some() { + Some(entity) + } else { + None + } + }) + .choose(&mut rng) +} diff --git a/src/lib/commands/src/arg/gamemode.rs b/src/lib/commands/src/arg/gamemode.rs index 34a112fb3..0ff23aa55 100644 --- a/src/lib/commands/src/arg/gamemode.rs +++ b/src/lib/commands/src/arg/gamemode.rs @@ -1,6 +1,6 @@ use crate::{ - arg::{utils::parser_error, CommandArgument, ParserResult}, CommandContext, Suggestion, + arg::{CommandArgument, ParserResult, utils::parser_error}, }; use super::PrimitiveArgument; diff --git a/src/lib/commands/src/arg/mod.rs b/src/lib/commands/src/arg/mod.rs index aeb875e6f..9f7d89dad 100644 --- a/src/lib/commands/src/arg/mod.rs +++ b/src/lib/commands/src/arg/mod.rs @@ -3,10 +3,12 @@ use ferrumc_text::TextComponent; use primitive::PrimitiveArgument; -use crate::{ctx::CommandContext, Suggestion}; +use crate::{Suggestion, ctx::CommandContext}; pub mod duration; +pub mod entities; pub mod gamemode; +pub mod position; pub mod primitive; pub type ParserResult = Result>; diff --git a/src/lib/commands/src/arg/position.rs b/src/lib/commands/src/arg/position.rs new file mode 100644 index 000000000..737c008ee --- /dev/null +++ b/src/lib/commands/src/arg/position.rs @@ -0,0 +1,305 @@ +use crate::arg::primitive::PrimitiveArgument; +use crate::arg::utils::parser_error; +use crate::arg::{CommandArgument, ParserResult}; +use crate::{CommandContext, Suggestion}; +use ferrumc_core::transform::position::Position; + +/// Represents a position argument in a command, which can be absolute or relative. +/// For example: "100 64 -200" (absolute) or "~10 ~ ~-5" (relative). +/// +/// The coordinates are initially opaque, in order to get an actual world position you must pass in +/// a base position to resolve against, usually the player calling the command. You can pass in a +/// 0,0,0 position if you want absolute coordinates only. +/// +/// # Example +/// ```ignore +/// use ferrumc_commands::arg::position::CommandPosition; +/// use ferrumc_core::transform::position::Position; +/// +/// let cmd_pos = CommandPosition::parse("~10 64 ~-5").unwrap; +/// let base_position = Position::new(100.0, 64.0, 100.0); +/// let resolved_position = cmd_pos.resolve(&base_position); +/// assert_eq!(resolved_position, Position::new(110.0, 64.0, 95.0)); +/// ``` +pub struct CommandPosition { + x: PositionType, + y: PositionType, + z: PositionType, +} + +enum PositionType { + Absolute(f64), + Relative(f64), +} + +impl CommandArgument for CommandPosition { + fn parse(ctx: &mut CommandContext) -> ParserResult { + let mut string_input = String::new(); + while ctx.input.has_remaining_input() { + if !string_input.is_empty() { + string_input.push(' '); + } + string_input.push_str(&ctx.input.read_string()); + } + let mut parts = string_input.split_whitespace(); + let x_str = parts + .next() + .ok_or_else(|| parser_error("missing x coordinate"))?; + let y_str = parts + .next() + .ok_or_else(|| parser_error("missing y coordinate"))?; + let z_str = parts + .next() + .ok_or_else(|| parser_error("missing z coordinate"))?; + let x = if let Some(x_str) = x_str.strip_prefix('~') { + if x_str.is_empty() { + PositionType::Relative(0.0) + } else { + let offset = x_str + .parse::() + .map_err(|_| parser_error("invalid x coordinate"))?; + PositionType::Relative(offset) + } + } else { + let value = x_str + .parse::() + .map_err(|_| parser_error("invalid x coordinate"))?; + PositionType::Absolute(value) + }; + let y = if let Some(y_str) = y_str.strip_prefix('~') { + if y_str.is_empty() { + PositionType::Relative(0.0) + } else { + let offset = y_str + .parse::() + .map_err(|_| parser_error("invalid y coordinate"))?; + PositionType::Relative(offset) + } + } else { + let value = y_str + .parse::() + .map_err(|_| parser_error("invalid y coordinate"))?; + PositionType::Absolute(value) + }; + let z = if let Some(z_str) = z_str.strip_prefix('~') { + if z_str.is_empty() { + PositionType::Relative(0.0) + } else { + let offset = z_str + .parse::() + .map_err(|_| parser_error("invalid z coordinate"))?; + PositionType::Relative(offset) + } + } else { + let value = z_str + .parse::() + .map_err(|_| parser_error("invalid z coordinate"))?; + PositionType::Absolute(value) + }; + if parts.next().is_some() { + return Err(parser_error("too many coordinates provided")); + } + Ok(CommandPosition { x, y, z }) + } + + fn primitive() -> PrimitiveArgument { + PrimitiveArgument::greedy() + } + + fn suggest(_ctx: &mut CommandContext) -> Vec { + vec![Suggestion::of("~ ~ ~")] + } +} + +impl CommandPosition { + pub fn resolve(&self, position: &Position) -> Position { + let x = match self.x { + PositionType::Absolute(val) => val, + PositionType::Relative(offset) => position.x + offset, + }; + let y = match self.y { + PositionType::Absolute(val) => val, + PositionType::Relative(offset) => position.y + offset, + }; + let z = match self.z { + PositionType::Absolute(val) => val, + PositionType::Relative(offset) => position.z + offset, + }; + Position::new(x, y, z) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Command, CommandInput, Sender}; + use ferrumc_state::create_test_state; + use std::sync::Arc; + + #[test] + fn test_parse() { + let mut ctx = CommandContext { + input: CommandInput { + input: "~10 5 ~-10".to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let cmd_pos = CommandPosition::parse(&mut ctx).unwrap(); + match cmd_pos.x { + PositionType::Relative(offset) => assert_eq!(offset, 10.0), + _ => panic!("Expected relative x"), + } + match cmd_pos.y { + PositionType::Absolute(value) => assert_eq!(value, 5.0), + _ => panic!("Expected absolute y"), + } + match cmd_pos.z { + PositionType::Relative(offset) => assert_eq!(offset, -10.0), + _ => panic!("Expected relative z"), + } + } + + #[test] + fn test_resolve() { + let cmd_pos = CommandPosition { + x: PositionType::Relative(10.0), + y: PositionType::Absolute(5.0), + z: PositionType::Relative(-10.0), + }; + let base_position = Position::new(100.0, 64.0, 100.0); + let resolved = cmd_pos.resolve(&base_position); + assert_eq!(resolved.x, 110.0); + assert_eq!(resolved.y, 5.0); + assert_eq!(resolved.z, 90.0); + } + + #[test] + fn parse_valid_inputs() { + let cases = vec![ + ( + "100 64 -200", + ( + PositionType::Absolute(100.0), + PositionType::Absolute(64.0), + PositionType::Absolute(-200.0), + ), + ), + ( + "~10 ~ ~-5", + ( + PositionType::Relative(10.0), + PositionType::Relative(0.0), + PositionType::Relative(-5.0), + ), + ), + ( + "50 ~20 30", + ( + PositionType::Absolute(50.0), + PositionType::Relative(20.0), + PositionType::Absolute(30.0), + ), + ), + ( + "~ ~ ~", + ( + PositionType::Relative(0.0), + PositionType::Relative(0.0), + PositionType::Relative(0.0), + ), + ), + ( + "1 2 ~", + ( + PositionType::Absolute(1.0), + PositionType::Absolute(2.0), + PositionType::Relative(0.0), + ), + ), + ( + "~-0 ~0 ~+0", + ( + PositionType::Relative(-0.0), + PositionType::Relative(0.0), + PositionType::Relative(0.0), + ), + ), + ]; + for (input, expected) in cases { + let mut ctx = CommandContext { + input: CommandInput { + input: input.to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + let cmd_pos = CommandPosition::parse(&mut ctx) + .unwrap_or_else(|_| panic!("input `{}` should be valid", input)); + match cmd_pos.x { + PositionType::Absolute(val) => match expected.0 { + PositionType::Absolute(exp_val) => assert_eq!(val, exp_val), + _ => panic!("Expected relative x for input `{}`", input), + }, + PositionType::Relative(offset) => match expected.0 { + PositionType::Relative(exp_offset) => assert_eq!(offset, exp_offset), + _ => panic!("Expected absolute x for input `{}`", input), + }, + } + match cmd_pos.y { + PositionType::Absolute(val) => match expected.1 { + PositionType::Absolute(exp_val) => assert_eq!(val, exp_val), + _ => panic!("Expected relative y for input `{}`", input), + }, + PositionType::Relative(offset) => match expected.1 { + PositionType::Relative(exp_offset) => assert_eq!(offset, exp_offset), + _ => panic!("Expected absolute y for input `{}`", input), + }, + } + match cmd_pos.z { + PositionType::Absolute(val) => match expected.2 { + PositionType::Absolute(exp_val) => assert_eq!(val, exp_val), + _ => panic!("Expected relative z for input `{}`", input), + }, + PositionType::Relative(offset) => match expected.2 { + PositionType::Relative(exp_offset) => assert_eq!(offset, exp_offset), + _ => panic!("Expected absolute z for input `{}`", input), + }, + } + } + } + + #[test] + fn test_parse_invalid_inputs() { + let cases = vec!["", "1 2", "1 two 3", "not_a_number 5 6", "~ ~", "1 2 3 4"]; + for input in cases { + let mut ctx = CommandContext { + input: CommandInput { + input: input.to_string(), + cursor: 0, + }, + command: Arc::new(Command { + name: "", + args: vec![], + }), + sender: Sender::Server, + state: create_test_state().0.0, + }; + assert!( + CommandPosition::parse(&mut ctx).is_err(), + "input `{}` should be invalid", + input + ); + } + } +} diff --git a/src/lib/commands/src/arg/primitive/bool.rs b/src/lib/commands/src/arg/primitive/bool.rs index da05036d1..7a1694bab 100644 --- a/src/lib/commands/src/arg/primitive/bool.rs +++ b/src/lib/commands/src/arg/primitive/bool.rs @@ -1,6 +1,6 @@ use crate::{ - arg::{utils::parser_error, CommandArgument, ParserResult}, CommandContext, Suggestion, + arg::{CommandArgument, ParserResult, utils::parser_error}, }; use super::PrimitiveArgument; diff --git a/src/lib/commands/src/arg/primitive/char.rs b/src/lib/commands/src/arg/primitive/char.rs index 6cefe9733..6d8dac9b4 100644 --- a/src/lib/commands/src/arg/primitive/char.rs +++ b/src/lib/commands/src/arg/primitive/char.rs @@ -1,6 +1,6 @@ use crate::{ - arg::{utils::parser_error, CommandArgument, ParserResult}, CommandContext, Suggestion, + arg::{CommandArgument, ParserResult, utils::parser_error}, }; use super::PrimitiveArgument; diff --git a/src/lib/commands/src/arg/primitive/float.rs b/src/lib/commands/src/arg/primitive/float.rs index 51a637690..d61cb09df 100644 --- a/src/lib/commands/src/arg/primitive/float.rs +++ b/src/lib/commands/src/arg/primitive/float.rs @@ -1,10 +1,10 @@ use std::io::Write; -use ferrumc_net_codec::encode::{errors::NetEncodeError, NetEncode, NetEncodeOpts}; +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, errors::NetEncodeError}; use tokio::io::AsyncWrite; use crate::{ - arg::{utils::error, CommandArgument, ParserResult}, + arg::{CommandArgument, ParserResult, utils::error}, ctx::CommandContext, wrapper, }; diff --git a/src/lib/commands/src/arg/primitive/int.rs b/src/lib/commands/src/arg/primitive/int.rs index 6c4ab39e8..48a87b7d2 100644 --- a/src/lib/commands/src/arg/primitive/int.rs +++ b/src/lib/commands/src/arg/primitive/int.rs @@ -1,12 +1,12 @@ use std::{io::Write, ops::Deref}; -use ferrumc_net_codec::encode::{errors::NetEncodeError, NetEncode, NetEncodeOpts}; +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, errors::NetEncodeError}; use tokio::io::AsyncWrite; use crate::{ arg::{ - utils::{error, parser_error}, CommandArgument, ParserResult, + utils::{error, parser_error}, }, ctx::CommandContext, }; diff --git a/src/lib/commands/src/arg/primitive/long.rs b/src/lib/commands/src/arg/primitive/long.rs index 6857b0946..90e79780e 100644 --- a/src/lib/commands/src/arg/primitive/long.rs +++ b/src/lib/commands/src/arg/primitive/long.rs @@ -1,12 +1,12 @@ use std::{io::Write, ops::Deref}; -use ferrumc_net_codec::encode::{errors::NetEncodeError, NetEncode, NetEncodeOpts}; +use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts, errors::NetEncodeError}; use tokio::io::AsyncWrite; use crate::{ arg::{ - utils::{error, parser_error}, CommandArgument, ParserResult, + utils::{error, parser_error}, }, ctx::CommandContext, }; diff --git a/src/lib/commands/src/arg/primitive/mod.rs b/src/lib/commands/src/arg/primitive/mod.rs index a48eb89b1..41d04893c 100644 --- a/src/lib/commands/src/arg/primitive/mod.rs +++ b/src/lib/commands/src/arg/primitive/mod.rs @@ -13,7 +13,7 @@ use std::io::Write; use enum_ordinalize::Ordinalize; use ferrumc_macros::NetEncode; use ferrumc_net_codec::{ - encode::{errors::NetEncodeError, NetEncode, NetEncodeOpts}, + encode::{NetEncode, NetEncodeOpts, errors::NetEncodeError}, net_types::var_int::VarInt, }; use float::FloatArgumentFlags; @@ -149,6 +149,7 @@ pub enum PrimitiveArgumentType { TemplateRotation, Heightmap, UUID, + Position, } impl NetEncode for PrimitiveArgumentType { diff --git a/src/lib/commands/src/arg/primitive/string.rs b/src/lib/commands/src/arg/primitive/string.rs index cac1bf0c7..f43028312 100644 --- a/src/lib/commands/src/arg/primitive/string.rs +++ b/src/lib/commands/src/arg/primitive/string.rs @@ -2,13 +2,13 @@ use std::io::Write; use enum_ordinalize::Ordinalize; use ferrumc_net_codec::{ - encode::{errors::NetEncodeError, NetEncode, NetEncodeOpts}, + encode::{NetEncode, NetEncodeOpts, errors::NetEncodeError}, net_types::var_int::VarInt, }; use tokio::io::AsyncWrite; use crate::{ - arg::{utils::parser_error, CommandArgument, ParserResult}, + arg::{CommandArgument, ParserResult, utils::parser_error}, ctx::CommandContext, wrapper, }; diff --git a/src/lib/commands/src/ctx.rs b/src/lib/commands/src/ctx.rs index 1db8c501f..8a665fac3 100644 --- a/src/lib/commands/src/ctx.rs +++ b/src/lib/commands/src/ctx.rs @@ -2,14 +2,14 @@ use std::sync::Arc; -use tracing::error; - use crate::{ - arg::{utils::parser_error, CommandArgument, ParserResult}, + Command, + arg::{CommandArgument, ParserResult, utils::parser_error}, input::CommandInput, sender::Sender, - Command, }; +use ferrumc_state::GlobalState; +use tracing::error; /// Context of the execution of a command. pub struct CommandContext { @@ -21,6 +21,8 @@ pub struct CommandContext { /// The sender of the command. pub sender: Sender, + + pub state: GlobalState, } impl CommandContext { diff --git a/src/lib/commands/src/graph/mod.rs b/src/lib/commands/src/graph/mod.rs index a52eabe0c..87997920c 100644 --- a/src/lib/commands/src/graph/mod.rs +++ b/src/lib/commands/src/graph/mod.rs @@ -179,24 +179,25 @@ impl CommandGraph { } CommandNodeType::Literal => { // for literal nodes, everything must match exactly. - if let Some(name) = ¤t_node.name { - if !input_words.is_empty() && input_words[0] == name { - // we found a match, we continue with the remaining input. - let remaining = if input_words.len() > 1 { - remaining_input[name.len()..].trim_start() - } else { - "" - }; - - // once we found a node that is executable and the remaining input is empty, we've found something. - if remaining.is_empty() && current_node.is_executable() { - matches.push((node_idx, remaining)); - } - - // we continue checking the other children. - for child_idx in current_node.children.data.iter() { - self.find_command_recursive(child_idx.0 as u32, remaining, matches); - } + if let Some(name) = ¤t_node.name + && !input_words.is_empty() + && input_words[0] == name + { + // we found a match, we continue with the remaining input. + let remaining = if input_words.len() > 1 { + remaining_input[name.len()..].trim_start() + } else { + "" + }; + + // once we found a node that is executable and the remaining input is empty, we've found something. + if remaining.is_empty() && current_node.is_executable() { + matches.push((node_idx, remaining)); + } + + // we continue checking the other children. + for child_idx in current_node.children.data.iter() { + self.find_command_recursive(child_idx.0 as u32, remaining, matches); } } } @@ -224,10 +225,10 @@ impl CommandGraph { fn collect_command_parts(&self, node_idx: u32, parts: &mut Vec) { let node = &self.nodes[node_idx as usize]; - if let Some(name) = &node.name { - if node.node_type() == CommandNodeType::Literal { - parts.push(name.clone()); - } + if let Some(name) = &node.name + && node.node_type() == CommandNodeType::Literal + { + parts.push(name.clone()); } // find the parent diff --git a/src/lib/commands/src/infrastructure.rs b/src/lib/commands/src/infrastructure.rs index d9616cabd..66cc84ef0 100644 --- a/src/lib/commands/src/infrastructure.rs +++ b/src/lib/commands/src/infrastructure.rs @@ -7,7 +7,7 @@ use std::{ sync::{Arc, LazyLock, RwLock}, }; -use crate::{graph::CommandGraph, Command}; +use crate::{Command, graph::CommandGraph}; static COMMANDS: LazyLock>> = LazyLock::new(DashMap::new); static COMMAND_GRAPH: LazyLock> = @@ -15,6 +15,7 @@ static COMMAND_GRAPH: LazyLock> = thread_local! { static SYSTEMS_TO_BE_REGISTERED: RefCell>> = RefCell::new(Vec::new()); + static EXCLUSIVE_SYSTEMS_TO_BE_REGISTERED: RefCell>> = RefCell::new(Vec::new()); } /// Internal function. Adds a command system. diff --git a/src/lib/commands/src/lib.rs b/src/lib/commands/src/lib.rs index 348f5ad1f..fa88c6a37 100644 --- a/src/lib/commands/src/lib.rs +++ b/src/lib/commands/src/lib.rs @@ -1,5 +1,6 @@ //! FerrumC's Command API. #![feature(duration_constructors)] +#![feature(anonymous_lifetime_in_impl_trait)] use std::sync::{Arc, LazyLock}; diff --git a/src/lib/commands/src/messages.rs b/src/lib/commands/src/messages.rs index 4764b0c9f..8dc331448 100644 --- a/src/lib/commands/src/messages.rs +++ b/src/lib/commands/src/messages.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use bevy_ecs::message::Message; -use crate::{ctx::CommandContext, sender::Sender, Command}; +use crate::{Command, ctx::CommandContext, sender::Sender}; /// A command has been dispatched #[derive(Message)] diff --git a/src/lib/components/src/player/mod.rs b/src/lib/components/src/player/mod.rs index 0e18e0557..29da6d85a 100644 --- a/src/lib/components/src/player/mod.rs +++ b/src/lib/components/src/player/mod.rs @@ -9,4 +9,5 @@ pub mod pending_events; pub mod player_bundle; pub mod sneak; pub mod swimming; +pub mod teleport_tracker; pub mod view_distance; diff --git a/src/lib/components/src/player/teleport_tracker.rs b/src/lib/components/src/player/teleport_tracker.rs new file mode 100644 index 000000000..f379236e6 --- /dev/null +++ b/src/lib/components/src/player/teleport_tracker.rs @@ -0,0 +1,6 @@ +use bevy_ecs::prelude::Component; + +#[derive(Component)] +pub struct TeleportTracker { + pub waiting_for_confirm: bool, +} diff --git a/src/lib/default_commands/Cargo.toml b/src/lib/default_commands/Cargo.toml index be3edbd1b..dba51493f 100644 --- a/src/lib/default_commands/Cargo.toml +++ b/src/lib/default_commands/Cargo.toml @@ -15,6 +15,7 @@ ferrumc-performance = { workspace = true } ferrumc-net-codec = { workspace = true } lazy_static = { workspace = true } bimap = { workspace = true } +ferrumc-nbt = { workspace = true } ctor = { workspace = true } tracing = { workspace = true } diff --git a/src/lib/default_commands/src/kill.rs b/src/lib/default_commands/src/kill.rs index a8919d01e..fa6f83ecd 100644 --- a/src/lib/default_commands/src/kill.rs +++ b/src/lib/default_commands/src/kill.rs @@ -1,31 +1,62 @@ +#![expect(clippy::type_complexity)] use bevy_ecs::prelude::{Commands, Entity, Query}; +use ferrumc_commands::arg::entities::EntityArgument; use ferrumc_commands::Sender; use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_macros::command; use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::remove_entities::RemoveEntitiesPacket; +use ferrumc_net::packets::outgoing::system_message::SystemMessagePacket; use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; +use ferrumc_text::{Color, NamedColor, TextComponentBuilder}; #[command("kill")] fn kill_command( #[sender] sender: Sender, + #[arg] entity_argument: EntityArgument, args: ( - Query<(Entity, &EntityIdentity)>, + Query<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>, Commands, Query<&StreamWriter>, ), ) { let (query, mut cmd, conn_query) = args; + let selected_entities = entity_argument.resolve(query.iter()); + let mut removed_entities = Vec::new(); - for (entity, entity_id) in query.iter() { - removed_entities.push(entity_id.entity_id.into()); - cmd.entity(entity).despawn(); + let mut removed_count = 0; + let killed_message = SystemMessagePacket { + message: ferrumc_nbt::NBT::new( + TextComponentBuilder::new("You have been killed. How sad :(") + .bold() + .color(Color::Named(NamedColor::Red)) + .build(), + ), + overlay: false, + }; + for entity in selected_entities { + if let Ok((ent, entity_id_opt, player_id_opt)) = query.get(entity) { + if let Some(entity_id) = entity_id_opt { + removed_entities.push(entity_id.entity_id.into()); + cmd.entity(ent).despawn(); + removed_count += 1; + } else if let Some(_player_id) = player_id_opt { + // Don't remove players, just send them a killed message + if let Ok(conn) = conn_query.get(ent) { + if let Err(err) = conn.send_packet_ref(&killed_message) { + sender.send_message( + format!("Failed to send killed message: {}", err).into(), + false, + ); + } + } + } + } } - let removed_count = removed_entities.len(); - let packet = RemoveEntitiesPacket { entity_ids: LengthPrefixedVec::new(removed_entities), }; diff --git a/src/lib/default_commands/src/tp.rs b/src/lib/default_commands/src/tp.rs index 2ef29d1a9..f8f43ebf4 100644 --- a/src/lib/default_commands/src/tp.rs +++ b/src/lib/default_commands/src/tp.rs @@ -1,18 +1,22 @@ +#![expect(clippy::type_complexity)] +use bevy_ecs::entity::Entity; use bevy_ecs::prelude::{MessageWriter, Query}; -use ferrumc_commands::arg::primitive::float::Float; +use ferrumc_commands::arg::entities::EntityArgument; +use ferrumc_commands::arg::position::CommandPosition; use ferrumc_commands::Sender; use ferrumc_commands::Sender::Player; +use ferrumc_core::identity::entity_identity::EntityIdentity; +use ferrumc_core::identity::player_identity::PlayerIdentity; +use ferrumc_core::transform::position::Position; use ferrumc_core::transform::rotation::Rotation; use ferrumc_macros::command; use ferrumc_messages::teleport_player::TeleportPlayer; -#[command("tp")] +#[command("tp pos")] fn tp_command( #[sender] sender: Sender, - #[arg] x: Float, - #[arg] y: Float, - #[arg] z: Float, - args: (Query<&Rotation>, MessageWriter), + #[arg] pos: CommandPosition, + args: (Query<(&Rotation, &Position)>, MessageWriter), ) { let (mut query, mut tp_player_msg) = args; let Player(entity) = sender else { @@ -20,16 +24,17 @@ fn tp_command( return; }; - let Ok(rot) = query.get_mut(entity) else { + let Ok((rot, position)) = query.get_mut(entity) else { sender.send_message("Could not find your player entity.".into(), false); return; }; + let resolved_pos = pos.resolve(position); tp_player_msg.write(TeleportPlayer { entity, - x: *x as f64, - y: *y as f64, - z: *z as f64, + x: resolved_pos.x, + y: resolved_pos.y, + z: resolved_pos.z, vel_x: 0.0, vel_y: 0.0, vel_z: 0.0, @@ -37,8 +42,59 @@ fn tp_command( pitch: rot.pitch, }); + sender.send_message(format!("Teleported to ({}).", resolved_pos).into(), false); +} + +#[command("tp entity")] +fn tp_to_command( + #[sender] sender: Sender, + #[arg] target: EntityArgument, + args: ( + Query<(&Rotation, &Position)>, + MessageWriter, + Query<(Entity, Option<&EntityIdentity>, Option<&PlayerIdentity>)>, + ), +) { + let (query, mut tp_player_msg, resolve_q) = args; + + let resolved_targets = target.resolve(resolve_q.iter()); + + if resolved_targets.len() != 1 { + sender.send_message( + "You must specify exactly one target to teleport to.".into(), + false, + ); + return; + } else if matches!(sender, Sender::Server) { + sender.send_message("This command can only be used by players.".into(), false); + return; + } + + let target_entity = resolved_targets.first().expect("Checked above; qed"); + + let Player(sender_e) = sender else { + unreachable!(); + }; + + let Ok([(sender_rot, _), (_, target_pos)]) = query.get_many([sender_e, *target_entity]) else { + sender.send_message("Could not find player entities.".into(), false); + return; + }; + + tp_player_msg.write(TeleportPlayer { + entity: sender_e, + x: target_pos.x, + y: target_pos.y, + z: target_pos.z, + vel_x: 0.0, + vel_y: 0.0, + vel_z: 0.0, + yaw: sender_rot.yaw, + pitch: sender_rot.pitch, + }); + sender.send_message( - format!("Teleported to ({}, {}, {}).", *x, *y, *z).into(), + format!("Teleported to the entity at {}.", target_pos).into(), false, ); } diff --git a/src/lib/net/src/conn_init/login.rs b/src/lib/net/src/conn_init/login.rs index 220e85deb..2628f1c30 100644 --- a/src/lib/net/src/conn_init/login.rs +++ b/src/lib/net/src/conn_init/login.rs @@ -466,6 +466,8 @@ fn send_initial_chunks( let client_view_distance = client_view_distance as i32; let radius = server_render_distance.min(client_view_distance); + let start = std::time::Instant::now(); + // Generate/load chunks in parallel let mut batch = state.thread_pool.batch(); @@ -500,6 +502,13 @@ fn send_initial_chunks( } } + let end = std::time::Instant::now(); + let duration = end.duration_since(start); + debug!( + "Sent initial chunks in {:?} milliseconds", + duration.as_millis() + ); + Ok(()) } diff --git a/src/lib/net/src/packets/outgoing/mod.rs b/src/lib/net/src/packets/outgoing/mod.rs index 5e49cc859..02b1bfbba 100644 --- a/src/lib/net/src/packets/outgoing/mod.rs +++ b/src/lib/net/src/packets/outgoing/mod.rs @@ -60,3 +60,5 @@ pub mod hurt_animation; pub mod respawn; pub mod set_health; pub mod update_time; + +pub mod synchronise_vehicle_position; diff --git a/src/lib/net/src/packets/outgoing/synchronise_vehicle_position.rs b/src/lib/net/src/packets/outgoing/synchronise_vehicle_position.rs new file mode 100644 index 000000000..0b861455d --- /dev/null +++ b/src/lib/net/src/packets/outgoing/synchronise_vehicle_position.rs @@ -0,0 +1,18 @@ +use ferrumc_macros::{packet, NetEncode}; +use ferrumc_net_codec::net_types::var_int::VarInt; + +#[derive(NetEncode)] +#[packet(packet_id = "teleport_entity", state = "play")] +pub struct SynchroniseVehiclePosition { + pub entity_id: VarInt, + pub x: f64, + pub y: f64, + pub z: f64, + pub vel_x: f64, + pub vel_y: f64, + pub vel_z: f64, + pub yaw: f32, + pub pitch: f32, + pub flags: i32, + pub on_ground: bool, +} diff --git a/src/lib/utils/logging/src/lib.rs b/src/lib/utils/logging/src/lib.rs index 047dda9af..85f3fed69 100644 --- a/src/lib/utils/logging/src/lib.rs +++ b/src/lib/utils/logging/src/lib.rs @@ -17,6 +17,8 @@ pub fn init_logging(trace_level: Level) { // Disallow request and hyper-util debug prints since they spam the console let env_filter = env_filter .add_directive("reqwest=warn".parse().unwrap()) + .add_directive("h2=warn".parse().unwrap()) + .add_directive("rustls=warn".parse().unwrap()) .add_directive("hyper_util=warn".parse().unwrap()); let file_appender = tracing_appender::rolling::Builder::new() diff --git a/src/lib/utils/src/maths/step.rs b/src/lib/utils/src/maths/step.rs index cbb68eced..2904fcd68 100644 --- a/src/lib/utils/src/maths/step.rs +++ b/src/lib/utils/src/maths/step.rs @@ -13,12 +13,13 @@ /// # Example /// /// ``` +/// # use crate::ferrumc_utils::maths::step::step_between; /// use bevy_math::Vec3A; /// let start = Vec3A::new(0.0, 0.0, 0.0); /// let end = Vec3A::new(1.0, 1.0, 1.0); /// let step = 0.5; /// let points = step_between(start, end, step); -/// assert_eq!(points.len(), 3); +/// assert_eq!(points.len(), 5); /// ``` pub fn step_between( start: bevy_math::Vec3A, @@ -41,3 +42,21 @@ pub fn step_between( points.push(end); // Ensure the end point is included in the list. points } + +#[cfg(test)] +mod tests { + use super::*; + use bevy_math::Vec3A; + + #[test] + fn test_step_between() { + let start = Vec3A::new(0.0, 0.0, 0.0); + let end = Vec3A::new(0.0, 0.0, 1.0); + let step = 0.5; + let points = step_between(start, end, step); + assert_eq!(points.len(), 3); + assert_eq!(points[0], Vec3A::new(0.0, 0.0, 0.0)); + assert_eq!(points[1], Vec3A::new(0.0, 0.0, 0.5)); + assert_eq!(points[2], Vec3A::new(0.0, 0.0, 1.0)); + } +}