From 8759a2163210403821c64a9e18f248a44ad71034 Mon Sep 17 00:00:00 2001 From: Bafbi Date: Tue, 8 Aug 2023 09:00:39 +0200 Subject: [PATCH] Add abilities components on the player (#439) # Objective see #438 # Solution The client is updated when a change is detected on one of the player abilities component. --- crates/valence_client/src/abilities.rs | 162 ++++++++++++++++++ crates/valence_client/src/lib.rs | 8 + .../src/packets/play/player_abilities_s2c.rs | 5 +- .../play/update_player_abilities_c2s.rs | 2 +- examples/anvil_loading.rs | 14 +- examples/cow_sphere.rs | 29 +++- src/tests/client.rs | 43 +++++ 7 files changed, 259 insertions(+), 4 deletions(-) create mode 100644 crates/valence_client/src/abilities.rs diff --git a/crates/valence_client/src/abilities.rs b/crates/valence_client/src/abilities.rs new file mode 100644 index 000000000..827ea94bd --- /dev/null +++ b/crates/valence_client/src/abilities.rs @@ -0,0 +1,162 @@ +pub use valence_packet::packets::play::player_abilities_s2c::PlayerAbilitiesFlags; +use valence_packet::packets::play::{PlayerAbilitiesS2c, UpdatePlayerAbilitiesC2s}; + +use super::*; +use crate::event_loop::{EventLoopPreUpdate, PacketEvent}; + +/// [`Component`] that stores the player's flying speed ability. +/// +/// [`Default`] value: `0.05`. +#[derive(Component)] +pub struct FlyingSpeed(pub f32); + +impl Default for FlyingSpeed { + fn default() -> Self { + Self(0.05) + } +} + +/// [`Component`] that stores the player's field of view modifier ability. +/// The lower the value, the higher the field of view. +/// +/// [`Default`] value: `0.1`. +#[derive(Component)] +pub struct FovModifier(pub f32); + +impl Default for FovModifier { + fn default() -> Self { + Self(0.1) + } +} + +/// Send if the client sends [`UpdatePlayerAbilitiesC2s::StartFlying`] +#[derive(Event)] +pub struct PlayerStartFlyingEvent { + pub client: Entity, +} + +/// Send if the client sends [`UpdatePlayerAbilitiesC2s::StopFlying`] +#[derive(Event)] +pub struct PlayerStopFlyingEvent { + pub client: Entity, +} + +/// Order of execution : +/// 1. [`update_game_mode`] : Watch [`GameMode`] changes => Send +/// [`GameStateChangeS2c`] to update the client's gamemode +/// 2. [`update_client_player_abilities`] : Watch [`PlayerAbilitiesFlags`], +/// [`FlyingSpeed`] and [`FovModifier`] changes => Send [`PlayerAbilitiesS2c`] +/// to update the client's abilities 3. [`update_player_abilities`] : Watch +/// [`GameMode`] changes => Update [`PlayerAbilitiesFlags`] according to the +/// [`GameMode`] 4. [`update_server_player_abilities`] : Watch +/// [`UpdatePlayerAbilitiesC2s`] packets => Update [`PlayerAbilitiesFlags`] +/// according to the packet +pub(super) fn build(app: &mut App) { + app.add_event::() + .add_event::() + .add_systems( + PostUpdate, + ( + update_client_player_abilities, + update_player_abilities.before(update_client_player_abilities), + ) + .in_set(UpdateClientsSet) + .after(update_game_mode), + ) + .add_systems(EventLoopPreUpdate, update_server_player_abilities); +} + +fn update_client_player_abilities( + mut clients_query: Query< + ( + &mut Client, + &PlayerAbilitiesFlags, + &FlyingSpeed, + &FovModifier, + ), + Or<( + Changed, + Changed, + Changed, + )>, + >, +) { + for (mut client, flags, flying_speed, fov_modifier) in clients_query.iter_mut() { + client.write_packet(&PlayerAbilitiesS2c { + flags: *flags, + flying_speed: flying_speed.0, + fov_modifier: fov_modifier.0, + }) + } +} + +/// /!\ This system does not trigger change detection on +/// [`PlayerAbilitiesFlags`] +fn update_player_abilities( + mut player_start_flying_event_writer: EventWriter, + mut player_stop_flying_event_writer: EventWriter, + mut client_query: Query<(Entity, &mut PlayerAbilitiesFlags, &GameMode), Changed>, +) { + for (entity, mut mut_flags, gamemode) in client_query.iter_mut() { + let flags = mut_flags.bypass_change_detection(); + match gamemode { + GameMode::Creative => { + flags.set_invulnerable(true); + flags.set_allow_flying(true); + flags.set_instant_break(true); + } + GameMode::Spectator => { + flags.set_invulnerable(true); + flags.set_allow_flying(true); + flags.set_instant_break(false); + flags.set_flying(true); + player_start_flying_event_writer.send(PlayerStartFlyingEvent { client: entity }); + } + GameMode::Survival => { + flags.set_invulnerable(false); + flags.set_allow_flying(false); + flags.set_instant_break(false); + flags.set_flying(false); + player_stop_flying_event_writer.send(PlayerStopFlyingEvent { client: entity }); + } + GameMode::Adventure => { + flags.set_invulnerable(false); + flags.set_allow_flying(false); + flags.set_instant_break(false); + flags.set_flying(false); + player_stop_flying_event_writer.send(PlayerStopFlyingEvent { client: entity }); + } + } + } +} + +/// /!\ This system does not trigger change detection on +/// [`PlayerAbilitiesFlags`] +fn update_server_player_abilities( + mut packet_events: EventReader, + mut player_start_flying_event_writer: EventWriter, + mut player_stop_flying_event_writer: EventWriter, + mut client_query: Query<&mut PlayerAbilitiesFlags>, +) { + for packets in packet_events.iter() { + if let Some(pkt) = packets.decode::() { + if let Ok(mut mut_flags) = client_query.get_mut(packets.client) { + let flags = mut_flags.bypass_change_detection(); + match pkt { + UpdatePlayerAbilitiesC2s::StartFlying => { + flags.set_flying(true); + player_start_flying_event_writer.send(PlayerStartFlyingEvent { + client: packets.client, + }); + } + UpdatePlayerAbilitiesC2s::StopFlying => { + flags.set_flying(false); + player_stop_flying_event_writer.send(PlayerStopFlyingEvent { + client: packets.client, + }); + } + } + } + } + } +} diff --git a/crates/valence_client/src/lib.rs b/crates/valence_client/src/lib.rs index b0b730acd..c468f69b9 100644 --- a/crates/valence_client/src/lib.rs +++ b/crates/valence_client/src/lib.rs @@ -68,6 +68,7 @@ use valence_packet::protocol::encode::{PacketEncoder, WritePacket}; use valence_packet::protocol::Packet; use valence_registry::RegistrySet; +pub mod abilities; pub mod action; pub mod command; pub mod custom_payload; @@ -160,6 +161,7 @@ impl Plugin for ClientPlugin { op_level::build(app); resource_pack::build(app); status::build(app); + abilities::build(app); } } @@ -196,6 +198,9 @@ pub struct ClientBundle { pub is_debug: spawn::IsDebug, pub is_flat: spawn::IsFlat, pub portal_cooldown: spawn::PortalCooldown, + pub player_abilities_flags: abilities::PlayerAbilitiesFlags, + pub flying_speed: abilities::FlyingSpeed, + pub fov_modifier: abilities::FovModifier, pub player: PlayerEntityBundle, } @@ -234,6 +239,9 @@ impl ClientBundle { reduced_debug_info: spawn::ReducedDebugInfo::default(), is_debug: spawn::IsDebug::default(), portal_cooldown: spawn::PortalCooldown::default(), + player_abilities_flags: abilities::PlayerAbilitiesFlags::default(), + flying_speed: abilities::FlyingSpeed::default(), + fov_modifier: abilities::FovModifier::default(), player: PlayerEntityBundle { uuid: UniqueId(args.uuid), ..Default::default() diff --git a/crates/valence_packet/src/packets/play/player_abilities_s2c.rs b/crates/valence_packet/src/packets/play/player_abilities_s2c.rs index bc8621f94..783d71721 100644 --- a/crates/valence_packet/src/packets/play/player_abilities_s2c.rs +++ b/crates/valence_packet/src/packets/play/player_abilities_s2c.rs @@ -1,3 +1,5 @@ +use bevy_ecs::prelude::Component; + use super::*; #[derive(Clone, Debug, Encode, Decode, Packet)] @@ -8,8 +10,9 @@ pub struct PlayerAbilitiesS2c { pub fov_modifier: f32, } +/// [`Component`] that stores the player's abilities flags. #[bitfield(u8)] -#[derive(PartialEq, Eq, Encode, Decode)] +#[derive(PartialEq, Eq, Encode, Decode, Component, Default)] pub struct PlayerAbilitiesFlags { pub invulnerable: bool, pub flying: bool, diff --git a/crates/valence_packet/src/packets/play/update_player_abilities_c2s.rs b/crates/valence_packet/src/packets/play/update_player_abilities_c2s.rs index 979e14280..c3de90d28 100644 --- a/crates/valence_packet/src/packets/play/update_player_abilities_c2s.rs +++ b/crates/valence_packet/src/packets/play/update_player_abilities_c2s.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Copy, Clone, Debug, Encode, Decode, Packet)] +#[derive(Copy, Clone, Debug, Encode, Decode, Packet, PartialEq, Eq)] #[packet(id = packet_id::UPDATE_PLAYER_ABILITIES_C2S)] pub enum UpdatePlayerAbilitiesC2s { #[packet(tag = 0b00)] diff --git a/examples/anvil_loading.rs b/examples/anvil_loading.rs index 86f96c8b2..f5efc9555 100644 --- a/examples/anvil_loading.rs +++ b/examples/anvil_loading.rs @@ -1,8 +1,11 @@ +#![allow(clippy::type_complexity)] + use std::path::PathBuf; use clap::Parser; use valence::prelude::*; use valence_anvil::{AnvilLevel, ChunkLoadEvent, ChunkLoadStatus}; +use valence_client::abilities::{FlyingSpeed, FovModifier, PlayerAbilitiesFlags}; use valence_client::message::SendMessage; const SPAWN_POS: DVec3 = DVec3::new(0.0, 256.0, 0.0); @@ -78,6 +81,9 @@ fn init_clients( &mut VisibleEntityLayers, &mut Position, &mut GameMode, + &mut PlayerAbilitiesFlags, + &mut FlyingSpeed, + &mut FovModifier, ), Added, >, @@ -89,6 +95,9 @@ fn init_clients( mut visible_entity_layers, mut pos, mut game_mode, + mut abilities, + mut flying_speed, + mut fov_modifier, ) in &mut clients { let layer = layers.single(); @@ -97,7 +106,10 @@ fn init_clients( visible_chunk_layer.0 = layer; visible_entity_layers.0.insert(layer); pos.set(SPAWN_POS); - *game_mode = GameMode::Spectator; + *game_mode = GameMode::Adventure; + abilities.set_allow_flying(true); + flying_speed.0 = 0.1; + fov_modifier.0 = 0.05; } } diff --git a/examples/cow_sphere.rs b/examples/cow_sphere.rs index 90e9e1707..621d9e3a0 100644 --- a/examples/cow_sphere.rs +++ b/examples/cow_sphere.rs @@ -4,6 +4,9 @@ use std::f64::consts::TAU; use glam::{DQuat, EulerRot}; use valence::prelude::*; +use valence_client::abilities::{PlayerStartFlyingEvent, PlayerStopFlyingEvent}; +use valence_client::message::SendMessage; +use valence_core::text::color::NamedColor; type SpherePartBundle = valence::entity::cow::CowEntityBundle; @@ -25,7 +28,12 @@ fn main() { .add_systems(Startup, setup) .add_systems( Update, - (init_clients, update_sphere, despawn_disconnected_clients), + ( + init_clients, + update_sphere, + despawn_disconnected_clients, + display_is_flying, + ), ) .run(); } @@ -142,3 +150,22 @@ fn fibonacci_spiral(n: usize) -> impl Iterator { fn lerp(a: f64, b: f64, t: f64) -> f64 { a * (1.0 - t) + b * t } + +// Send an actionbar message to all clients when their flying state changes. +fn display_is_flying( + mut player_start_flying_events: EventReader, + mut player_stop_flying_events: EventReader, + mut clients: Query<&mut Client>, +) { + for event in player_start_flying_events.iter() { + if let Ok(mut client) = clients.get_mut(event.client) { + client.send_action_bar_message("You are flying!".color(NamedColor::Green)); + } + } + + for event in player_stop_flying_events.iter() { + if let Ok(mut client) = clients.get_mut(event.client) { + client.send_action_bar_message("You are no longer flying!".color(NamedColor::Red)); + } + } +} diff --git a/src/tests/client.rs b/src/tests/client.rs index 1690c7b5d..f9e8c87aa 100644 --- a/src/tests/client.rs +++ b/src/tests/client.rs @@ -1,5 +1,7 @@ use glam::DVec3; +use valence_client::abilities::PlayerAbilitiesFlags; use valence_core::chunk_pos::ChunkPos; +use valence_core::game_mode::GameMode; use valence_layer::chunk::UnloadedChunk; use valence_layer::ChunkLayer; use valence_packet::packets::play::{ @@ -60,3 +62,44 @@ fn client_teleport_and_move() { .collect_received() .assert_count::(1); } + +#[test] +fn client_gamemode_changed_ability() { + let mut senario = ScenarioSingleClient::new(); + + *senario + .app + .world + .get_mut::(senario.client) + .unwrap() = GameMode::Creative; + + senario.app.update(); + + let abilities = senario + .app + .world + .get::(senario.client) + .unwrap(); + + assert!(abilities.allow_flying()); + assert!(abilities.instant_break()); + assert!(abilities.invulnerable()); + + *senario + .app + .world + .get_mut::(senario.client) + .unwrap() = GameMode::Adventure; + + senario.app.update(); + + let abilities = senario + .app + .world + .get::(senario.client) + .unwrap(); + + assert!(!abilities.allow_flying()); + assert!(!abilities.instant_break()); + assert!(!abilities.invulnerable()); +}