From c9fbd1a24e86e6df4698640ef08ae59303ab728a Mon Sep 17 00:00:00 2001 From: qualterz <38355785+qualterz@users.noreply.github.com> Date: Thu, 16 Mar 2023 14:43:26 +0200 Subject: [PATCH] Weather implementation (#260) ## Description An implementation of basic weather systems. The weather component attached to a world instance would be handled for all clients, except those that have their own weather component; these clients would be handled separately. ## Test Plan
Playground ```rust fn handle_command_events( instances: Query>, mut exec_cmds: EventReader, mut commands: Commands, ) { for cmd in exec_cmds.iter() { let msg = cmd.command.to_string(); let ent = instances.single(); match msg.as_str() { "ar" => { commands.entity(ent).insert(Rain(WEATHER_LEVEL.end)); } "rr" => { commands.entity(ent).remove::(); } "at" => { commands.entity(ent).insert(Thunder(WEATHER_LEVEL.end)); } "rt" => { commands.entity(ent).remove::(); } _ => (), }; } } ```
Steps: 1. Run `cargo test --package valence --lib -- weather::test` #### Related Part of #210 Past approach #106 --- crates/valence/src/lib.rs | 1 + crates/valence/src/server.rs | 4 +- crates/valence/src/weather.rs | 417 ++++++++++++++++++++++++++++++++++ 3 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 crates/valence/src/weather.rs diff --git a/crates/valence/src/lib.rs b/crates/valence/src/lib.rs index f1e1990..8d56e0a 100644 --- a/crates/valence/src/lib.rs +++ b/crates/valence/src/lib.rs @@ -43,6 +43,7 @@ pub mod server; mod unit_test; pub mod util; pub mod view; +pub mod weather; pub mod prelude { pub use async_trait::async_trait; diff --git a/crates/valence/src/server.rs b/crates/valence/src/server.rs index a8b9e78..079ebf8 100644 --- a/crates/valence/src/server.rs +++ b/crates/valence/src/server.rs @@ -30,6 +30,7 @@ use crate::player_list::PlayerListPlugin; use crate::prelude::event::ClientEventPlugin; use crate::prelude::ComponentPlugin; use crate::server::connect::do_accept_loop; +use crate::weather::WeatherPlugin; mod byte_channel; mod connect; @@ -335,7 +336,8 @@ pub fn build_plugin( .add_plugin(EntityPlugin) .add_plugin(InstancePlugin) .add_plugin(InventoryPlugin) - .add_plugin(PlayerListPlugin); + .add_plugin(PlayerListPlugin) + .add_plugin(WeatherPlugin); /* println!( diff --git a/crates/valence/src/weather.rs b/crates/valence/src/weather.rs new file mode 100644 index 0000000..3463cb2 --- /dev/null +++ b/crates/valence/src/weather.rs @@ -0,0 +1,417 @@ +//! The weather system. +//! +//! This module contains the systems and components needed to handle +//! weather. +//! +//! # Components +//! +//! The components may be attached to clients or instances. +//! +//! - [`Rain`]: When attached, raining begin and rain level set events are +//! emitted. When removed, the end raining event is emitted. +//! - [`Thunder`]: When attached, thunder level set event is emitted. When +//! removed, the thunder level set to zero event is emitted. +//! +//! New joined players are handled, so that they are get weather events from +//! the instance. + +use std::ops::Range; + +use bevy_ecs::prelude::*; +use valence_protocol::packet::s2c::play::game_state_change::GameEventKind; +use valence_protocol::packet::s2c::play::GameStateChangeS2c; + +use crate::instance::UpdateInstancesPreClientSet; +use crate::packet::WritePacket; +use crate::prelude::*; + +pub const WEATHER_LEVEL: Range = 0_f32..1_f32; + +/// Contains the rain level. +/// Valid value is a value within the [WEATHER_LEVEL] range. +/// Invalid value would be clamped. +#[derive(Component)] +pub struct Rain(pub f32); + +/// Contains the thunder level. +/// Valid value is a value within the [WEATHER_LEVEL] range. +/// Invalid value would be clamped. +#[derive(Component)] +pub struct Thunder(pub f32); + +impl Instance { + /// Sends the begin rain event to all players in the instance. + fn begin_raining(&mut self) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::BeginRaining, + value: f32::default(), + }); + } + + /// Sends the end rain event to all players in the instance. + fn end_raining(&mut self) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::EndRaining, + value: f32::default(), + }); + } + + /// Sends the set rain level event to all players in the instance. + fn set_rain_level(&mut self, level: f32) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end), + }); + } + + /// Sends the set thunder level event to all players in the instance. + fn set_thunder_level(&mut self, level: f32) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::ThunderLevelChange, + value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end), + }); + } +} + +impl Client { + /// Sends the begin rain event to the client. + fn begin_raining(&mut self) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::BeginRaining, + value: f32::default(), + }); + } + + /// Sends the end rain event to the client. + fn end_raining(&mut self) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::EndRaining, + value: f32::default(), + }); + } + + /// Sends the set rain level event to the client. + fn set_rain_level(&mut self, level: f32) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end), + }); + } + + /// Sends the set thunder level event to the client. + fn set_thunder_level(&mut self, level: f32) { + self.write_packet(&GameStateChangeS2c { + kind: GameEventKind::ThunderLevelChange, + value: level.clamp(WEATHER_LEVEL.start, WEATHER_LEVEL.end), + }); + } +} + +fn handle_weather_for_joined_player( + mut clients: Query<(&mut Client, &Location), Added>, + weathers: Query<(Option<&Rain>, Option<&Thunder>), With>, +) { + clients.for_each_mut(|(mut client, loc)| { + if let Ok((rain, thunder)) = weathers.get(loc.0) { + if let Some(level) = rain { + client.begin_raining(); + client.set_rain_level(level.0); + } + + if let Some(level) = thunder { + client.set_thunder_level(level.0); + } + } + }) +} + +fn handle_rain_begin_per_instance(mut query: Query<&mut Instance, Added>) { + query.for_each_mut(|mut instance| { + instance.begin_raining(); + }); +} + +fn handle_rain_change_per_instance(mut query: Query<(&mut Instance, &Rain), Changed>) { + query.for_each_mut(|(mut instance, rain)| instance.set_rain_level(rain.0)); +} + +fn handle_rain_end_per_instance( + mut query: Query<&mut Instance>, + mut removed: RemovedComponents, +) { + removed.iter().for_each(|entity| { + if let Ok(mut instance) = query.get_mut(entity) { + instance.end_raining(); + } + }) +} + +fn handle_thunder_change_per_instance( + mut query: Query<(&mut Instance, &Thunder), Changed>, +) { + query.for_each_mut(|(mut instance, thunder)| instance.set_thunder_level(thunder.0)); +} + +fn handle_thunder_end_per_instance( + mut query: Query<&mut Instance>, + mut removed: RemovedComponents, +) { + removed.iter().for_each(|entity| { + if let Ok(mut instance) = query.get_mut(entity) { + instance.set_thunder_level(WEATHER_LEVEL.start); + } + }) +} + +fn handle_rain_begin_per_client(mut query: Query<&mut Client, (Added, Without)>) { + query.for_each_mut(|mut client| { + client.begin_raining(); + }); +} + +fn handle_rain_change_per_client( + mut query: Query<(&mut Client, &Rain), (Changed, Without)>, +) { + query.for_each_mut(|(mut client, rain)| { + client.set_rain_level(rain.0); + }); +} + +fn handle_rain_end_per_client(mut query: Query<&mut Client>, mut removed: RemovedComponents) { + removed.iter().for_each(|entity| { + if let Ok(mut client) = query.get_mut(entity) { + client.end_raining(); + } + }) +} + +fn handle_thunder_change_per_client( + mut query: Query<(&mut Client, &Thunder), (Changed, Without)>, +) { + query.for_each_mut(|(mut client, thunder)| { + client.set_thunder_level(thunder.0); + }); +} + +fn handle_thunder_end_per_client( + mut query: Query<&mut Client, Without>, + mut removed: RemovedComponents, +) { + removed.iter().for_each(|entity| { + if let Ok(mut client) = query.get_mut(entity) { + client.set_thunder_level(WEATHER_LEVEL.start); + } + }) +} + +pub(crate) struct WeatherPlugin; + +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub(crate) struct UpdateWeatherPerInstanceSet; + +#[derive(SystemSet, Copy, Clone, PartialEq, Eq, Hash, Debug)] +pub(crate) struct UpdateWeatherPerClientSet; + +impl Plugin for WeatherPlugin { + fn build(&self, app: &mut App) { + app.configure_set( + UpdateWeatherPerInstanceSet + .in_base_set(CoreSet::PostUpdate) + .before(UpdateInstancesPreClientSet), + ); + + app.configure_set( + UpdateWeatherPerClientSet + .in_base_set(CoreSet::PostUpdate) + .before(FlushPacketsSet), + ); + + app.add_systems( + ( + handle_rain_begin_per_instance, + handle_rain_change_per_instance, + handle_rain_end_per_instance, + handle_thunder_change_per_instance, + handle_thunder_end_per_instance, + ) + .chain() + .in_set(UpdateWeatherPerInstanceSet) + .before(UpdateWeatherPerClientSet), + ); + + app.add_systems( + ( + handle_rain_begin_per_client, + handle_rain_change_per_client, + handle_rain_end_per_client, + handle_thunder_change_per_client, + handle_thunder_end_per_client, + ) + .chain() + .in_set(UpdateWeatherPerClientSet), + ); + + app.add_system(handle_weather_for_joined_player.before(UpdateWeatherPerClientSet)); + } +} + +#[cfg(test)] +mod test { + use anyhow::Ok; + use bevy_app::App; + use valence_protocol::packet::S2cPlayPacket; + + use super::*; + use crate::unit_test::util::scenario_single_client; + use crate::{assert_packet_count, assert_packet_order}; + + fn assert_weather_packets(sent_packets: Vec) { + assert_packet_count!(sent_packets, 6, S2cPlayPacket::GameStateChangeS2c(_)); + + assert_packet_order!( + sent_packets, + S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c { + kind: GameEventKind::BeginRaining, + value: _ + }), + S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c { + kind: GameEventKind::RainLevelChange, + value: _ + }), + S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c { + kind: GameEventKind::ThunderLevelChange, + value: _ + }), + S2cPlayPacket::GameStateChangeS2c(GameStateChangeS2c { + kind: GameEventKind::EndRaining, + value: _ + }) + ); + + if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[1] { + assert_eq!(pkt.value, 0.5f32); + } + + if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[2] { + assert_eq!(pkt.value, WEATHER_LEVEL.end); + } + + if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[3] { + assert_eq!(pkt.value, 0.5f32); + } + + if let S2cPlayPacket::GameStateChangeS2c(pkt) = sent_packets[4] { + assert_eq!(pkt.value, WEATHER_LEVEL.end); + } + } + + #[test] + fn test_weather_instance() -> anyhow::Result<()> { + let mut app = App::new(); + let (_, mut client_helper) = scenario_single_client(&mut app); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Get the instance entity. + let instance_ent = app + .world + .iter_entities() + .find(|e| e.contains::()) + .expect("could not find instance") + .id(); + + // Insert a rain component to the instance. + app.world.entity_mut(instance_ent).insert(Rain(0.5f32)); + for _ in 0..2 { + app.update(); + } + + // Alter a rain component of the instance. + app.world.entity_mut(instance_ent).insert(Rain( + // Invalid value to assert it is clamped. + WEATHER_LEVEL.end + 1_f32, + )); + app.update(); + + // Insert a thunder component to the instance. + app.world.entity_mut(instance_ent).insert(Thunder(0.5f32)); + app.update(); + + // Alter a thunder component of the instance. + app.world.entity_mut(instance_ent).insert(Thunder( + // Invalid value to assert it is clamped. + WEATHER_LEVEL.end + 1_f32, + )); + app.update(); + + // Remove the rain component from the instance. + app.world.entity_mut(instance_ent).remove::(); + for _ in 0..2 { + app.update(); + } + + // Make assertions. + let sent_packets = client_helper.collect_sent()?; + + assert_weather_packets(sent_packets); + + Ok(()) + } + + #[test] + fn test_weather_client() -> anyhow::Result<()> { + let mut app = App::new(); + let (_, mut client_helper) = scenario_single_client(&mut app); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Get the client entity. + let client_ent = app + .world + .iter_entities() + .find(|e| e.contains::()) + .expect("could not find client") + .id(); + + // Insert a rain component to the client. + app.world.entity_mut(client_ent).insert(Rain(0.5f32)); + for _ in 0..2 { + app.update(); + } + + // Alter a rain component of the client. + app.world.entity_mut(client_ent).insert(Rain( + // Invalid value to assert it is clamped. + WEATHER_LEVEL.end + 1_f32, + )); + app.update(); + + // Insert a thunder component to the client. + app.world.entity_mut(client_ent).insert(Thunder(0.5f32)); + app.update(); + + // Alter a thunder component of the client. + app.world.entity_mut(client_ent).insert(Thunder( + // Invalid value to assert it is clamped. + WEATHER_LEVEL.end + 1_f32, + )); + app.update(); + + // Remove the rain component from the client. + app.world.entity_mut(client_ent).remove::(); + for _ in 0..2 { + app.update(); + } + + // Make assertions. + let sent_packets = client_helper.collect_sent()?; + + assert_weather_packets(sent_packets); + + Ok(()) + } +}