Weather implementation (#260)

<!-- Please make sure that your PR is aligned with the guidelines in
CONTRIBUTING.md to the best of your ability. -->
<!-- Good PRs have tests! Make sure you have sufficient test coverage.
-->

## Description

<!-- Describe the changes you've made. You may include any justification
you want here. -->

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

<!-- Explain how you tested your changes, and include any code that you
used to test this. -->
<!-- If there is an example that is sufficient to use in place of a
playground, replace the playground section with a note that indicates
this. -->

<details>

<summary>Playground</summary>

```rust
fn handle_command_events(
    instances: Query<Entity, With<Instance>>,
    mut exec_cmds: EventReader<CommandExecution>,
    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::<Rain>();
            }
            "at" => {
                commands.entity(ent).insert(Thunder(WEATHER_LEVEL.end));
            }
            "rt" => {
                commands.entity(ent).remove::<Thunder>();
            }
            _ => (),
        };
    }
}
```

</details>

<!-- You need to include steps regardless of whether or not you are
using a playground. -->
Steps:
1. Run `cargo test --package valence --lib -- weather::test`

#### Related
Part of #210
Past approach #106

<!-- Link to any issues that have context for this or that this PR
fixes. -->
This commit is contained in:
qualterz 2023-03-16 14:43:26 +02:00 committed by GitHub
parent 2c0fb2d8c4
commit c9fbd1a24e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 421 additions and 1 deletions

View file

@ -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;

View file

@ -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!(

View file

@ -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<f32> = 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<Client>>,
weathers: Query<(Option<&Rain>, Option<&Thunder>), With<Instance>>,
) {
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<Rain>>) {
query.for_each_mut(|mut instance| {
instance.begin_raining();
});
}
fn handle_rain_change_per_instance(mut query: Query<(&mut Instance, &Rain), Changed<Rain>>) {
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<Rain>,
) {
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<Thunder>>,
) {
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<Thunder>,
) {
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<Rain>, Without<Instance>)>) {
query.for_each_mut(|mut client| {
client.begin_raining();
});
}
fn handle_rain_change_per_client(
mut query: Query<(&mut Client, &Rain), (Changed<Rain>, Without<Instance>)>,
) {
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<Rain>) {
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<Thunder>, Without<Instance>)>,
) {
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<Instance>>,
mut removed: RemovedComponents<Thunder>,
) {
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<S2cPlayPacket>) {
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::<Instance>())
.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::<Rain>();
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::<Client>())
.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::<Rain>();
for _ in 0..2 {
app.update();
}
// Make assertions.
let sent_packets = client_helper.collect_sent()?;
assert_weather_packets(sent_packets);
Ok(())
}
}