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(())
+ }
+}