diff --git a/Cargo.lock b/Cargo.lock index a020d543..87f1d780 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3858,17 +3858,6 @@ dependencies = [ "valence_protocol 0.2.0-alpha.1+mc.1.20.1 (git+https://github.com/TestingPlant/valence?branch=feat-bytes)", ] -[[package]] -name = "hyperion-respawn" -version = "0.1.0" -dependencies = [ - "bevy", - "hyperion", - "tracing", - "valence_protocol 0.2.0-alpha.1+mc.1.20.1 (git+https://github.com/TestingPlant/valence?branch=feat-bytes)", - "valence_server", -] - [[package]] name = "hyperion-scheduled" version = "0.1.0" @@ -6729,7 +6718,6 @@ dependencies = [ "hyperion-permission", "hyperion-proxy-module", "hyperion-rank-tree", - "hyperion-respawn", "hyperion-scheduled", "hyperion-text", "hyperion-utils", diff --git a/Cargo.toml b/Cargo.toml index 74155a70..b20d4dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ members = [ 'crates/hyperion-proxy', 'crates/hyperion-proxy-module', 'crates/hyperion-rank-tree', - 'crates/hyperion-respawn', 'crates/hyperion-scheduled', 'crates/hyperion-stats', 'crates/hyperion-text', @@ -227,9 +226,6 @@ version = '0.3.6' features = ['rustls-tls', 'stream'] version = '0.12.12' -[workspace.dependencies.hyperion-respawn] -path = 'crates/hyperion-respawn' - [workspace.dependencies.roaring] features = ['simd'] version = '0.10.12' @@ -322,6 +318,7 @@ missing_panics_doc = 'allow' module_name_repetitions = 'allow' print_stdout = 'deny' single_match_else = 'allow' +struct_excessive_bools = 'allow' too_long_first_doc_paragraph = 'allow' too_many_lines = 'allow' needless_pass_by_value = 'allow' diff --git a/crates/hyperion-respawn/.gitignore b/crates/hyperion-respawn/.gitignore deleted file mode 100644 index ea8c4bf7..00000000 --- a/crates/hyperion-respawn/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/crates/hyperion-respawn/Cargo.toml b/crates/hyperion-respawn/Cargo.toml deleted file mode 100644 index a9b8014a..00000000 --- a/crates/hyperion-respawn/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "hyperion-respawn" -version = "0.1.0" -edition = "2021" -authors = ["Andrew Gazelka "] -readme = "README.md" -publish = false - -[dependencies] -bevy = {workspace = true} -hyperion = {workspace = true} -tracing = {workspace = true} -valence_protocol = {workspace = true} -valence_server = {workspace = true} - -[lints] -workspace = true diff --git a/crates/hyperion-respawn/README.md b/crates/hyperion-respawn/README.md deleted file mode 100644 index cb2e8806..00000000 --- a/crates/hyperion-respawn/README.md +++ /dev/null @@ -1 +0,0 @@ -# respawn \ No newline at end of file diff --git a/crates/hyperion-respawn/src/lib.rs b/crates/hyperion-respawn/src/lib.rs deleted file mode 100644 index 48dee2cd..00000000 --- a/crates/hyperion-respawn/src/lib.rs +++ /dev/null @@ -1,124 +0,0 @@ -use bevy::prelude::*; -use hyperion::{ - ingress, - net::{Compose, DataBundle}, - simulation::{ - metadata::{entity::Pose, living_entity::Health}, - packet::play, - Flight, FlyingSpeed, Pitch, Position, Uuid, Xp, Yaw, - }, -}; -use tracing::error; -use valence_protocol::{ - game_mode::OptGameMode, - packets::play::{ - player_abilities_s2c::{PlayerAbilitiesFlags, PlayerAbilitiesS2c}, - ClientStatusC2s, ExperienceBarUpdateS2c, HealthUpdateS2c, PlayerRespawnS2c, PlayerSpawnS2c, - }, - BlockPos, ByteAngle, GlobalPos, VarInt, -}; -use valence_server::{ident, GameMode}; - -fn handle_respawn( - mut packets: EventReader<'_, '_, play::ClientStatus>, - mut query: Query< - '_, - '_, - ( - &mut Health, - &mut Pose, - &Uuid, - &Position, - &Yaw, - &Pitch, - &Xp, - &Flight, - &FlyingSpeed, - ), - >, - compose: Res<'_, Compose>, -) { - for packet in packets.read() { - if !matches!(**packet, ClientStatusC2s::PerformRespawn) { - continue; - } - - let (mut health, mut pose, uuid, position, yaw, pitch, xp, flight, flying_speed) = - match query.get_mut(packet.sender()) { - Ok(data) => data, - Err(e) => { - error!("failed to handle respawn: query failed: {e}"); - continue; - } - }; - - health.heal(20.); - - *pose = Pose::Standing; - - let pkt_health = HealthUpdateS2c { - health: health.abs(), - food: VarInt(20), - food_saturation: 5.0, - }; - - let pkt_respawn = PlayerRespawnS2c { - dimension_type_name: ident!("minecraft:overworld"), - dimension_name: ident!("minecraft:overworld"), - hashed_seed: 0, - game_mode: GameMode::Survival, - previous_game_mode: OptGameMode::default(), - is_debug: false, - is_flat: false, - copy_metadata: false, - last_death_location: Option::from(GlobalPos { - dimension_name: ident!("minecraft:overworld"), - position: BlockPos::from(position.as_dvec3()), - }), - portal_cooldown: VarInt::default(), - }; - - let pkt_xp = ExperienceBarUpdateS2c { - bar: xp.get_visual().prop, - level: VarInt(i32::from(xp.get_visual().level)), - total_xp: VarInt::default(), - }; - - let pkt_abilities = PlayerAbilitiesS2c { - flags: PlayerAbilitiesFlags::default() - .with_flying(flight.is_flying) - .with_allow_flying(flight.allow), - flying_speed: flying_speed.speed, - fov_modifier: 0.0, - }; - - let mut bundle = DataBundle::new(&compose); - bundle.add_packet(&pkt_health).unwrap(); - bundle.add_packet(&pkt_respawn).unwrap(); - bundle.add_packet(&pkt_xp).unwrap(); - bundle.add_packet(&pkt_abilities).unwrap(); - bundle.unicast(packet.connection_id()).unwrap(); - - let pkt_add_player = PlayerSpawnS2c { - entity_id: VarInt(packet.minecraft_id()), - player_uuid: uuid.0, - position: position.as_dvec3(), - yaw: ByteAngle::from_degrees(**yaw), - pitch: ByteAngle::from_degrees(**pitch), - }; - - compose - .broadcast(&pkt_add_player) - .exclude(packet.connection_id()) - .send() - .unwrap(); - } -} - -pub struct RespawnPlugin; - -impl Plugin for RespawnPlugin { - fn build(&self, app: &mut App) { - app.add_systems(FixedUpdate, handle_respawn.after(ingress::decode::play)); - } -} diff --git a/crates/hyperion/src/egress/player_join/mod.rs b/crates/hyperion/src/egress/player_join/mod.rs index 5333841e..f756cd10 100644 --- a/crates/hyperion/src/egress/player_join/mod.rs +++ b/crates/hyperion/src/egress/player_join/mod.rs @@ -20,7 +20,7 @@ use valence_registry::{BiomeRegistry, RegistryCodec}; use valence_server::entity::EntityKind; use valence_text::IntoText; -use crate::simulation::{MovementTracking, Pitch, packet_state}; +use crate::simulation::{MovementTracking, Pitch, TeleportEvent, packet_state}; mod list; pub use list::*; @@ -29,9 +29,7 @@ use crate::{ config::Config, egress::metadata::show_all, net::{Compose, ConnectionId, DataBundle}, - simulation::{ - PendingTeleportation, Position, Uuid, Yaw, skin::PlayerSkin, util::registry_codec_raw, - }, + simulation::{Position, Uuid, Yaw, skin::PlayerSkin, util::registry_codec_raw}, }; #[derive(Event)] @@ -345,10 +343,15 @@ fn process_player_join( server_velocity: DVec3::ZERO, sprinting: false, was_on_ground: false, + skip_next_check: false, }, - PendingTeleportation::new(position), packet_state::Play(()), )); + commands.send_event(TeleportEvent { + player: entity_id, + destination: position, + reset_velocity: false, + }); }); info!("{name} joined the world"); diff --git a/crates/hyperion/src/egress/sync_entity_state.rs b/crates/hyperion/src/egress/sync_entity_state.rs index ac251a55..f1d2f1fb 100644 --- a/crates/hyperion/src/egress/sync_entity_state.rs +++ b/crates/hyperion/src/egress/sync_entity_state.rs @@ -14,10 +14,11 @@ use crate::{ net::{Compose, ConnectionId, DataBundle}, simulation::{ EntitySize, Flight, MovementTracking, Owner, PendingTeleportation, Pitch, Position, - Velocity, Xp, Yaw, + TeleportEvent, Velocity, Xp, Yaw, animation::ActiveAnimation, event, event::HitGroundEvent, + handle_teleports, handlers::is_grounded, metadata::{MetadataChanges, get_and_clear_metadata}, }, @@ -102,15 +103,16 @@ fn sync_player_entity( &mut Velocity, &Yaw, &Pitch, - Option<&mut PendingTeleportation>, + &mut PendingTeleportation, &mut MovementTracking, &Flight, ), >, - mut event_writer: EventWriter<'_, HitGroundEvent>, - commands: ParallelCommands<'_, '_>, + mut hit_ground_writer: EventWriter<'_, HitGroundEvent>, + mut teleport_writer: EventWriter<'_, TeleportEvent>, ) { - let events = boxcar::Vec::new(); + let hit_ground_events = boxcar::Vec::new(); + let teleport_events = boxcar::Vec::new(); query .par_iter_mut() .batching_strategy(BatchingStrategy { @@ -126,117 +128,100 @@ fn sync_player_entity( mut velocity, yaw, pitch, - pending_teleport, + mut pending_teleport, mut tracking, flight, )| { let entity_id = VarInt(entity.minecraft_id()); - if let Some(mut pending_teleport) = pending_teleport { + if !pending_teleport.confirmed { if pending_teleport.ttl == 0 { - // This needs to trigger OnInsert, so pending_teleport cannot be modified directly - commands.command_scope(|mut commands| { - commands - .entity(entity) - .insert(PendingTeleportation::new(pending_teleport.destination)); + // Resend the teleport. The player is teleported to their current position + // in case the server updated their position while pending teleport + // confirmation from the client + teleport_events.push(TeleportEvent { + player: entity, + destination: **position, + reset_velocity: false, }); } else { pending_teleport.ttl -= 1; } - } else { - let chunk_pos = position.to_chunk(); + } - let position_delta = **position - tracking.last_tick_position; - let needs_teleport = position_delta.abs().max_element() >= 8.0; - let changed_position = **position != tracking.last_tick_position; + let chunk_pos = position.to_chunk(); - let look_changed = (**yaw - ***prev_yaw).abs() >= 0.01 - || (**pitch - ***prev_pitch).abs() >= 0.01; + let position_delta = **position - tracking.last_tick_position; + let needs_teleport = position_delta.abs().max_element() >= 8.0; + let changed_position = **position != tracking.last_tick_position; - let mut bundle = DataBundle::new(&compose); + let look_changed = + (**yaw - ***prev_yaw).abs() >= 0.01 || (**pitch - ***prev_pitch).abs() >= 0.01; - // Maximum number of movement packets allowed during 1 tick is 5 - if tracking.received_movement_packets > 5 { - tracking.received_movement_packets = 1; - } + let mut bundle = DataBundle::new(&compose); + + // Maximum number of movement packets allowed during 1 tick is 5 + if tracking.received_movement_packets > 5 { + tracking.received_movement_packets = 1; + } - // Replace 100 by 300 if fall flying (aka elytra) - if f64::from(position_delta.length_squared()) + // Replace 100 by 300 if fall flying (aka elytra) + if !tracking.skip_next_check + && f64::from(position_delta.length_squared()) - tracking.server_velocity.length_squared() > 100f64 * f64::from(tracking.received_movement_packets) - { - commands.command_scope(|mut commands| { - commands - .entity(entity) - .insert(PendingTeleportation::new(tracking.last_tick_position)); - }); - tracking.received_movement_packets = 0; - return; - } + { + teleport_events.push(TeleportEvent { + player: entity, + destination: tracking.last_tick_position, + reset_velocity: false, + }); + tracking.received_movement_packets = 0; + return; + } - let grounded = is_grounded(position, &blocks); - tracking.was_on_ground = grounded; - if grounded - && !tracking.last_tick_flying - && tracking.fall_start_y - position.y > 3. - { - let event = HitGroundEvent { - client: entity, - fall_distance: tracking.fall_start_y - position.y, - }; - events.push(event); - tracking.fall_start_y = position.y; - } + let grounded = is_grounded(position, &blocks); + tracking.was_on_ground = grounded; + if grounded && !tracking.last_tick_flying && tracking.fall_start_y - position.y > 3. + { + let event = HitGroundEvent { + client: entity, + fall_distance: tracking.fall_start_y - position.y, + }; + hit_ground_events.push(event); + tracking.fall_start_y = position.y; + } - if (tracking.last_tick_flying && flight.allow) || position_delta.y >= 0. { - tracking.fall_start_y = position.y; - } + if (tracking.last_tick_flying && flight.allow) || position_delta.y >= 0. { + tracking.fall_start_y = position.y; + } - if changed_position && !needs_teleport && look_changed { - let packet = play::RotateAndMoveRelativeS2c { + if changed_position && !needs_teleport && look_changed { + let packet = play::RotateAndMoveRelativeS2c { + entity_id, + #[allow(clippy::cast_possible_truncation)] + delta: (position_delta * 4096.0).to_array().map(|x| x as i16), + yaw: ByteAngle::from_degrees(**yaw), + pitch: ByteAngle::from_degrees(**pitch), + on_ground: grounded, + }; + + bundle.add_packet(&packet).unwrap(); + } else { + if changed_position && !needs_teleport { + let packet = play::MoveRelativeS2c { entity_id, #[allow(clippy::cast_possible_truncation)] delta: (position_delta * 4096.0).to_array().map(|x| x as i16), - yaw: ByteAngle::from_degrees(**yaw), - pitch: ByteAngle::from_degrees(**pitch), on_ground: grounded, }; - bundle.add_packet(&packet).unwrap(); - } else { - if changed_position && !needs_teleport { - let packet = play::MoveRelativeS2c { - entity_id, - #[allow(clippy::cast_possible_truncation)] - delta: (position_delta * 4096.0).to_array().map(|x| x as i16), - on_ground: grounded, - }; - - bundle.add_packet(&packet).unwrap(); - } - - if look_changed { - let packet = play::RotateS2c { - entity_id, - yaw: ByteAngle::from_degrees(**yaw), - pitch: ByteAngle::from_degrees(**pitch), - on_ground: grounded, - }; - - bundle.add_packet(&packet).unwrap(); - } - let packet = play::EntitySetHeadYawS2c { - entity_id, - head_yaw: ByteAngle::from_degrees(**yaw), - }; - bundle.add_packet(&packet).unwrap(); } - if needs_teleport { - let packet = play::EntityPositionS2c { + if look_changed { + let packet = play::RotateS2c { entity_id, - position: position.as_dvec3(), yaw: ByteAngle::from_degrees(**yaw), pitch: ByteAngle::from_degrees(**pitch), on_ground: grounded, @@ -244,20 +229,39 @@ fn sync_player_entity( bundle.add_packet(&packet).unwrap(); } + let packet = play::EntitySetHeadYawS2c { + entity_id, + head_yaw: ByteAngle::from_degrees(**yaw), + }; - if velocity.0 != Vec3::ZERO { - let packet = play::EntityVelocityUpdateS2c { - entity_id, - velocity: velocity.to_packet_units(), - }; + bundle.add_packet(&packet).unwrap(); + } - bundle.add_packet(&packet).unwrap(); - velocity.0 = Vec3::ZERO; - } + if needs_teleport { + let packet = play::EntityPositionS2c { + entity_id, + position: position.as_dvec3(), + yaw: ByteAngle::from_degrees(**yaw), + pitch: ByteAngle::from_degrees(**pitch), + on_ground: grounded, + }; - bundle.broadcast_local(chunk_pos).unwrap(); + bundle.add_packet(&packet).unwrap(); } + if velocity.0 != Vec3::ZERO { + let packet = play::EntityVelocityUpdateS2c { + entity_id, + velocity: velocity.to_packet_units(), + }; + + bundle.add_packet(&packet).unwrap(); + velocity.0 = Vec3::ZERO; + } + + bundle.broadcast_local(chunk_pos).unwrap(); + + tracking.skip_next_check = false; tracking.received_movement_packets = 0; tracking.last_tick_position = **position; tracking.last_tick_flying = flight.is_flying; @@ -295,7 +299,8 @@ fn sync_player_entity( }, ); - event_writer.write_batch(events); + hit_ground_writer.write_batch(hit_ground_events); + teleport_writer.write_batch(teleport_events); } fn update_projectile_positions( @@ -385,7 +390,7 @@ impl Plugin for EntityStateSyncPlugin { entity_xp_sync, entity_metadata_sync, active_animation_sync, - sync_player_entity, + sync_player_entity.after(handle_teleports), update_projectile_positions, ), ); diff --git a/crates/hyperion/src/simulation/handlers.rs b/crates/hyperion/src/simulation/handlers.rs index f7a86346..6ab43e5d 100644 --- a/crates/hyperion/src/simulation/handlers.rs +++ b/crates/hyperion/src/simulation/handlers.rs @@ -21,11 +21,11 @@ use crate::{ net::{Compose, ConnectionId}, simulation::{ Aabb, ConfirmBlockSequences, EntitySize, Flight, MovementTracking, PendingTeleportation, - Pitch, Position, Yaw, aabb, + Pitch, Position, TeleportEvent, Yaw, aabb, animation::{self, ActiveAnimation}, block_bounds, blocks::Blocks, - event, + event, handle_teleports, metadata::{entity::Pose, living_entity::HandStates}, packet::{OrderedPacketRef, play}, }, @@ -38,7 +38,7 @@ use crate::{ functions would add complexity because most parameters will still need to be passed \ manually." )] -fn position_and_look_updates( +pub fn position_and_look_updates( mut full_reader: EventReader<'_, '_, play::Full>, mut position_reader: EventReader<'_, '_, play::PositionAndOnGround>, mut look_reader: EventReader<'_, '_, play::LookAndOnGround>, @@ -47,15 +47,24 @@ fn position_and_look_updates( '_, '_, ( - Query<'_, '_, (&EntitySize, &mut MovementTracking, &mut Position, &Yaw)>, + Query< + '_, + '_, + ( + &PendingTeleportation, + &EntitySize, + &mut MovementTracking, + &mut Position, + &Yaw, + ), + >, Query<'_, '_, (&mut Yaw, &mut Pitch)>, - Query<'_, '_, &mut Position>, + Query<'_, '_, &mut PendingTeleportation>, ), >, - teleport_query: Query<'_, '_, &PendingTeleportation>, + mut teleport_writer: EventWriter<'_, TeleportEvent>, blocks: Res<'_, Blocks>, compose: Res<'_, Compose>, - mut commands: Commands<'_, '_>, ) { let mut full_reader = full_reader.read().map(OrderedPacketRef::from).peekable(); let mut position_reader = position_reader @@ -80,9 +89,9 @@ fn position_and_look_updates( packet.sender(), packet.connection_id(), queries.p0(), + &mut teleport_writer, blocks, compose, - &mut commands, packet.position.as_vec3(), packet.on_ground, ); @@ -104,9 +113,9 @@ fn position_and_look_updates( packet.sender(), packet.connection_id(), queries.p0(), + &mut teleport_writer, blocks, compose, - &mut commands, packet.position.as_vec3(), packet.on_ground, ); @@ -125,12 +134,20 @@ fn position_and_look_updates( pitch.pitch = packet.pitch; }, packet in teleport_reader => { - let client = packet.sender(); - let Ok(pending_teleport) = teleport_query.get(client) else { - warn!("failed to confirm teleportation: client is not pending teleportation, so there is nothing to confirm"); - continue; + let mut query = queries.p2(); + let mut pending_teleport = match query.get_mut(packet.sender()) { + Ok(pending_teleport) => pending_teleport, + Err(e) => { + error!("failed to confirm teleportation: query failed: {e}"); + continue; + } }; + if pending_teleport.confirmed { + warn!("failed to confirm teleportation: client is not pending teleportation or the teleport has already been confirmed, so there is nothing to confirm"); + continue; + } + let pending_teleport_id = pending_teleport.teleport_id; if VarInt(pending_teleport_id) != packet.teleport_id { @@ -142,40 +159,7 @@ fn position_and_look_updates( continue; } - let mut query = queries.p2(); - let mut position = match query.get_mut(client) { - Ok(position) => position, - Err(e) => { - error!("failed to confirm teleportation: query failed: {e}"); - continue; - } - }; - - **position = pending_teleport.destination; - - commands.queue(move |world: &mut World| { - let Ok(mut entity) = world.get_entity_mut(client) else { - error!("failed to confirm teleportation: client entity has despawned"); - return; - }; - - let Some(pending_teleport) = entity.get::() else { - error!( - "failed to confirm teleportation: client is missing PendingTeleportation \ - component" - ); - return; - }; - - if pending_teleport.teleport_id != pending_teleport_id { - // A new pending teleport must have started between the time that this - // command was queued and the time that this command was ran. Therefore, - // this should not remove the PendingTeleportation component. - return; - } - - entity.remove::(); - }); + pending_teleport.confirmed = true; } }; if result.is_none() { @@ -187,14 +171,24 @@ fn position_and_look_updates( fn change_position_or_correct_client( client: Entity, connection_id: ConnectionId, - mut query: Query<'_, '_, (&EntitySize, &mut MovementTracking, &mut Position, &Yaw)>, + mut query: Query< + '_, + '_, + ( + &PendingTeleportation, + &EntitySize, + &mut MovementTracking, + &mut Position, + &Yaw, + ), + >, + teleport_writer: &mut EventWriter<'_, TeleportEvent>, blocks: &Blocks, compose: &Compose, - commands: &mut Commands<'_, '_>, proposed: Vec3, on_ground: bool, ) { - let (&size, mut tracking, mut pose, yaw) = match query.get_mut(client) { + let (teleport, &size, mut tracking, mut pose, yaw) = match query.get_mut(client) { Ok(data) => data, Err(e) => { error!("change_position_or_correct_client failed: query failed: {e}"); @@ -202,6 +196,12 @@ fn change_position_or_correct_client( } }; + if !teleport.confirmed { + // The client does not yet know that it has been teleported, so any position change packets + // are not useful + return; + } + if let Err(e) = try_change_position(proposed, &pose, size, blocks) { // Send error message to player let msg = format!("§c{e}"); @@ -214,9 +214,11 @@ fn change_position_or_correct_client( warn!("Failed to send error message to player: {e}"); } - commands - .entity(client) - .insert(PendingTeleportation::new(pose.position)); + teleport_writer.write(TeleportEvent { + player: client, + destination: pose.position, + reset_velocity: false, + }); } tracking.received_movement_packets = tracking.received_movement_packets.saturating_add(1); @@ -622,7 +624,7 @@ impl Plugin for HandlersPlugin { app.add_systems( FixedUpdate, ( - position_and_look_updates, + position_and_look_updates.before(handle_teleports), hand_swing, player_action, client_command, diff --git a/crates/hyperion/src/simulation/mod.rs b/crates/hyperion/src/simulation/mod.rs index d40efe1a..d4d9dc68 100644 --- a/crates/hyperion/src/simulation/mod.rs +++ b/crates/hyperion/src/simulation/mod.rs @@ -491,20 +491,43 @@ impl Velocity { } } -#[derive(Component, Default, Debug, Copy, Clone, PartialEq)] +#[derive(Event, Debug, Copy, Clone, PartialEq)] +pub struct TeleportEvent { + pub player: Entity, + pub destination: Vec3, + pub reset_velocity: bool, +} + +/// Component used to mark a pending teleportation. To teleport a player, send [`TeleportEvent`] +/// instead of modifying this directly. +#[derive(Component, Debug, Copy, Clone, PartialEq)] pub struct PendingTeleportation { pub teleport_id: i32, pub destination: Vec3, pub ttl: u8, + + /// Whether this pending teleportation has been confirmed by the client + pub confirmed: bool, } impl PendingTeleportation { #[must_use] - pub fn new(destination: Vec3) -> Self { + fn new(destination: Vec3) -> Self { Self { teleport_id: fastrand::i32(..), destination, ttl: 20, + confirmed: false, + } + } + + #[must_use] + const fn none() -> Self { + Self { + teleport_id: 0, + destination: Vec3::ZERO, + ttl: 0, + confirmed: true, } } } @@ -536,6 +559,7 @@ pub struct MovementTracking { pub server_velocity: DVec3, pub sprinting: bool, pub was_on_ground: bool, + pub skip_next_check: bool, } #[derive(Component, Default, Debug, Copy, Clone)] @@ -557,6 +581,7 @@ fn initialize_player( EntitySize::default(), Flight::default(), FlyingSpeed::default(), + PendingTeleportation::none(), hyperion_inventory::CursorItem::default(), )); @@ -638,28 +663,63 @@ fn initialize_uuid(trigger: Trigger<'_, OnAdd, EntityKind>, mut commands: Comman }); } -fn send_pending_teleportation( - trigger: Trigger<'_, OnInsert, PendingTeleportation>, - query: Query<'_, '_, (&PendingTeleportation, &Yaw, &Pitch, &ConnectionId)>, +pub fn handle_teleports( + mut reader: EventReader<'_, '_, TeleportEvent>, + mut query: Query< + '_, + '_, + ( + &mut PendingTeleportation, + &mut Position, + &mut Velocity, + &mut MovementTracking, + &Yaw, + &Pitch, + &ConnectionId, + ), + >, compose: Res<'_, Compose>, ) { - let (pending_teleportation, yaw, pitch, &connection) = match query.get(trigger.target()) { - Ok(data) => data, - Err(e) => { - error!("failed to send pending teleportation: query failed: {e}"); - return; - } - }; + for event in reader.read() { + let ( + mut pending_teleportation, + mut position, + mut velocity, + mut tracking, + yaw, + pitch, + &connection, + ) = match query.get_mut(event.player) { + Ok(data) => data, + Err(e) => { + error!("failed to handle teleport: query failed: {e}"); + return; + } + }; - let pkt = play::PlayerPositionLookS2c { - position: pending_teleportation.destination.as_dvec3(), - yaw: **yaw, - pitch: **pitch, - flags: PlayerPositionLookFlags::default(), - teleport_id: VarInt(pending_teleportation.teleport_id), - }; + *pending_teleportation = PendingTeleportation::new(event.destination); - compose.unicast(&pkt, connection).unwrap(); + let pkt = play::PlayerPositionLookS2c { + position: pending_teleportation.destination.as_dvec3(), + yaw: **yaw, + pitch: **pitch, + flags: PlayerPositionLookFlags::default(), + teleport_id: VarInt(pending_teleportation.teleport_id), + }; + + compose.unicast(&pkt, connection).unwrap(); + + // Perform teleportation + **position = pending_teleportation.destination; + tracking.received_movement_packets = 0; + tracking.skip_next_check = true; + + if event.reset_velocity { + tracking.fall_start_y = position.y; + tracking.server_velocity = DVec3::ZERO; + velocity.0 = Vec3::ZERO; + } + } } fn spawn_entities( @@ -739,7 +799,6 @@ impl Plugin for SimPlugin { fn build(&self, app: &mut App) { app.add_observer(initialize_player); app.add_observer(remove_player); - app.add_observer(send_pending_teleportation); app.add_observer(update_flight); app.add_observer(initialize_uuid); @@ -750,9 +809,10 @@ impl Plugin for SimPlugin { InventoryPlugin, MetadataPlugin, )); - app.add_systems(FixedUpdate, spawn_entities); + app.add_systems(FixedUpdate, (handle_teleports, spawn_entities)); app.add_event::(); + app.add_event::(); app.add_event::(); app.add_event::(); app.add_event::(); diff --git a/events/tag/Cargo.toml b/events/tag/Cargo.toml index b45e046f..ab31ea63 100644 --- a/events/tag/Cargo.toml +++ b/events/tag/Cargo.toml @@ -18,7 +18,6 @@ hyperion-item = { workspace = true } hyperion-permission = { workspace = true } hyperion-proxy-module = { workspace = true } hyperion-rank-tree = { workspace = true } -hyperion-respawn = { workspace = true } hyperion-scheduled = { workspace = true } hyperion-text = { workspace = true } hyperion-utils = { workspace = true } diff --git a/events/tag/src/lib.rs b/events/tag/src/lib.rs index 26e7ebc9..1801c37e 100644 --- a/events/tag/src/lib.rs +++ b/events/tag/src/lib.rs @@ -131,7 +131,6 @@ impl Plugin for TagPlugin { hyperion_item::ItemPlugin, hyperion_permission::PermissionPlugin, hyperion_rank_tree::RankTreePlugin, - hyperion_respawn::RespawnPlugin, hyperion_proxy_module::HyperionProxyPlugin, )); app.add_observer(initialize_player); diff --git a/events/tag/src/plugin/attack.rs b/events/tag/src/plugin/attack.rs index e8176067..e36197cd 100644 --- a/events/tag/src/plugin/attack.rs +++ b/events/tag/src/plugin/attack.rs @@ -7,13 +7,17 @@ use glam::IVec3; use hyperion::{ BlockKind, ingress, net::{ - Compose, ConnectionId, agnostic, + Compose, ConnectionId, DataBundle, agnostic, packets::{BossBarAction, BossBarS2c}, }, runtime::AsyncRuntime, simulation::{ - PendingTeleportation, Position, Velocity, Yaw, blocks::Blocks, event, - metadata::living_entity::Health, packet::play, packet_state, + Flight, FlyingSpeed, Pitch, Position, TeleportEvent, Velocity, Xp, Yaw, + blocks::Blocks, + event, + metadata::{entity::Pose, living_entity::Health}, + packet::play, + packet_state, }, uuid::Uuid, }; @@ -22,12 +26,16 @@ use hyperion_rank_tree::Team; use hyperion_utils::{EntityExt, Prev}; use tracing::error; use valence_protocol::{ - ItemKind, ItemStack, Particle, VarInt, ident, + BlockPos, ByteAngle, GameMode, GlobalPos, ItemKind, ItemStack, Particle, VarInt, + game_mode::OptGameMode, + ident, math::{DVec3, Vec3}, packets::play::{ - DamageTiltS2c, DeathMessageS2c, EntityDamageS2c, GameMessageS2c, ParticleS2c, + DamageTiltS2c, DeathMessageS2c, EntityDamageS2c, ExperienceBarUpdateS2c, GameMessageS2c, + ParticleS2c, PlayerRespawnS2c, PlayerSpawnS2c, boss_bar_s2c::{BossBarColor, BossBarDivision, BossBarFlags}, client_status_c2s::ClientStatusC2s, + player_abilities_s2c::{PlayerAbilitiesFlags, PlayerAbilitiesS2c}, player_interact_entity_c2s::EntityInteraction, }, text::IntoText, @@ -310,18 +318,45 @@ fn handle_attacks( fn handle_respawn( mut packets: EventReader<'_, '_, play::ClientStatus>, - query: Query<'_, '_, &Team>, + mut query: Query< + '_, + '_, + ( + &hyperion::simulation::Uuid, + &Xp, + &Flight, + &FlyingSpeed, + &Team, + &Position, + &Yaw, + &Pitch, + &mut Health, + &mut Pose, + ), + >, candidates_query: Query<'_, '_, (Entity, &Position, &Team)>, mut blocks: ResMut<'_, Blocks>, runtime: Res<'_, AsyncRuntime>, - mut commands: Commands<'_, '_>, + compose: Res<'_, Compose>, + mut teleport_writer: EventWriter<'_, TeleportEvent>, ) { for packet in packets.read() { if !matches!(**packet, ClientStatusC2s::PerformRespawn) { continue; } - let team = match query.get(packet.sender()) { + let ( + uuid, + xp, + flight, + flying_speed, + team, + last_death_location, + yaw, + pitch, + mut health, + mut pose, + ) = match query.get_mut(packet.sender()) { Ok(team) => team, Err(e) => { error!("handle respawn failed: query failed: {e}"); @@ -329,6 +364,14 @@ fn handle_respawn( } }; + if !health.is_dead() { + continue; + } + + health.heal(20.); + + *pose = Pose::Standing; + let pos_vec = candidates_query .iter() .filter(|(candidate_entity, _, candidate_team)| { @@ -345,9 +388,61 @@ fn handle_respawn( find_spawn_position(&mut blocks, &runtime, &avoid_blocks()) }; - commands - .entity(packet.sender()) - .insert(PendingTeleportation::new(respawn_pos)); + teleport_writer.write(TeleportEvent { + player: packet.sender(), + destination: respawn_pos, + reset_velocity: true, + }); + + let pkt_respawn = PlayerRespawnS2c { + dimension_type_name: ident!("minecraft:overworld"), + dimension_name: ident!("minecraft:overworld"), + hashed_seed: 0, + game_mode: GameMode::Survival, + previous_game_mode: OptGameMode::default(), + is_debug: false, + is_flat: false, + copy_metadata: false, + last_death_location: Option::from(GlobalPos { + dimension_name: ident!("minecraft:overworld"), + position: BlockPos::from(last_death_location.as_dvec3()), + }), + portal_cooldown: VarInt::default(), + }; + + let pkt_xp = ExperienceBarUpdateS2c { + bar: xp.get_visual().prop, + level: VarInt(i32::from(xp.get_visual().level)), + total_xp: VarInt::default(), + }; + + let pkt_abilities = PlayerAbilitiesS2c { + flags: PlayerAbilitiesFlags::default() + .with_flying(flight.is_flying) + .with_allow_flying(flight.allow), + flying_speed: flying_speed.speed, + fov_modifier: 0.0, + }; + + let mut bundle = DataBundle::new(&compose); + bundle.add_packet(&pkt_respawn).unwrap(); + bundle.add_packet(&pkt_xp).unwrap(); + bundle.add_packet(&pkt_abilities).unwrap(); + bundle.unicast(packet.connection_id()).unwrap(); + + let pkt_add_player = PlayerSpawnS2c { + entity_id: VarInt(packet.minecraft_id()), + player_uuid: uuid.0, + position: respawn_pos.as_dvec3(), + yaw: ByteAngle::from_degrees(**yaw), + pitch: ByteAngle::from_degrees(**pitch), + }; + + compose + .broadcast(&pkt_add_player) + .exclude(packet.connection_id()) + .send() + .unwrap(); } }