From 7fab7fe57edea6c30cef1867517a02b41281a760 Mon Sep 17 00:00:00 2001 From: SelfMadeSystem Date: Wed, 18 Oct 2023 19:49:00 -0400 Subject: [PATCH] Added food and saturation (#568) # Objective - Food and saturation currently don't exist for the player in valence - Closes #565 # Solution Adds food and saturation to the `PlayerEntity` and tells the client about any changes. The only problem right now is that the default value for `Health` is the same for ALL entities, so the server tells the client that their health is at 14 when it should be at 20. ## Note Uncertain if best approach. Please lmk if another approach would be better --- crates/valence_entity/build.rs | 118 ++++++++++++++++++---------- crates/valence_server/src/client.rs | 23 +++++- src/tests.rs | 1 + src/tests/hunger.rs | 38 +++++++++ 4 files changed, 135 insertions(+), 45 deletions(-) create mode 100644 src/tests/hunger.rs diff --git a/crates/valence_entity/build.rs b/crates/valence_entity/build.rs index a50aab54f..4f916da85 100644 --- a/crates/valence_entity/build.rs +++ b/crates/valence_entity/build.rs @@ -366,45 +366,59 @@ fn build() -> anyhow::Result { #snake_entity_name_ident: Default::default(), }]); - if entity_name == "LivingEntity" { - bundle_fields.extend([quote! { - pub living_absorption: super::living::Absorption, - }]); - - bundle_init_fields.extend([quote! { - living_absorption: Default::default(), - }]); - - bundle_fields.extend([quote! { - pub living_attributes: super::attributes::EntityAttributes, - }]); - - // Get the default values of the attributes. - let mut attribute_default_values = TokenStream::new(); - - if let Some(attributes) = &entity.attributes { - for attribute in attributes { - let name = ident(attribute.name.to_pascal_case()); - let base_value = attribute.base_value; - attribute_default_values.extend([quote! { - .with_attribute_and_value( - super::EntityAttribute::#name, - #base_value, - ) - }]); + match entity_name { + "LivingEntity" => { + bundle_fields.extend([quote! { + pub living_absorption: super::living::Absorption, + }]); + + bundle_init_fields.extend([quote! { + living_absorption: Default::default(), + }]); + + bundle_fields.extend([quote! { + pub living_attributes: super::attributes::EntityAttributes, + }]); + + // Get the default values of the attributes. + let mut attribute_default_values = TokenStream::new(); + + if let Some(attributes) = &entity.attributes { + for attribute in attributes { + let name = ident(attribute.name.to_pascal_case()); + let base_value = attribute.base_value; + attribute_default_values.extend([quote! { + .with_attribute_and_value( + super::EntityAttribute::#name, + #base_value, + ) + }]); + } } - } - bundle_init_fields.extend([quote! { - living_attributes: super::attributes::EntityAttributes::new() #attribute_default_values, - }]); - bundle_fields.extend([quote! { - pub living_attributes_tracker: super::attributes::TrackedEntityAttributes, - }]); + bundle_init_fields.extend([quote! { + living_attributes: super::attributes::EntityAttributes::new() #attribute_default_values, + }]); + bundle_fields.extend([quote! { + pub living_attributes_tracker: super::attributes::TrackedEntityAttributes, + }]); - bundle_init_fields.extend([quote! { - living_attributes_tracker: Default::default(), - }]); + bundle_init_fields.extend([quote! { + living_attributes_tracker: Default::default(), + }]); + } + "PlayerEntity" => { + bundle_fields.extend([quote! { + pub player_food: super::player::Food, + pub player_saturation: super::player::Saturation, + }]); + + bundle_init_fields.extend([quote! { + player_food: Default::default(), + player_saturation: Default::default(), + }]); + } + _ => {} } } MarkerOrField::Field { entity_name, field } => { @@ -546,12 +560,32 @@ fn build() -> anyhow::Result { pub struct #entity_name_ident; }]); - if entity_name == "LivingEntity" { - module_body.extend([quote! { - #[doc = "Special untracked component for `LivingEntity` entities."] - #[derive(bevy_ecs::component::Component, Copy, Clone, Default, Debug)] - pub struct Absorption(pub f32); - }]); + match entity_name.as_str() { + "LivingEntity" => { + module_body.extend([quote! { + #[doc = "Special untracked component for `LivingEntity` entities."] + #[derive(bevy_ecs::component::Component, Copy, Clone, Default, Debug)] + pub struct Absorption(pub f32); + }]); + } + "PlayerEntity" => { + module_body.extend([quote! { + #[doc = "Special untracked component for `PlayerEntity` entities."] + #[derive(bevy_ecs::component::Component, Copy, Clone, Debug)] + pub struct Food(pub i32); + + impl Default for Food { + fn default() -> Self { + Self(20) + } + } + + #[doc = "Special untracked component for `PlayerEntity` entities."] + #[derive(bevy_ecs::component::Component, Copy, Clone, Default, Debug)] + pub struct Saturation(pub f32); + }]); + } + _ => {} } modules.extend([quote! { diff --git a/crates/valence_server/src/client.rs b/crates/valence_server/src/client.rs index 62adcd2d9..f51bd08cd 100644 --- a/crates/valence_server/src/client.rs +++ b/crates/valence_server/src/client.rs @@ -14,7 +14,8 @@ use derive_more::{Deref, DerefMut, From, Into}; use tracing::warn; use uuid::Uuid; use valence_entity::attributes::{EntityAttributes, TrackedEntityAttributes}; -use valence_entity::player::PlayerEntityBundle; +use valence_entity::living::Health; +use valence_entity::player::{Food, PlayerEntityBundle, Saturation}; use valence_entity::query::EntityInitQuery; use valence_entity::tracked_data::TrackedData; use valence_entity::{ @@ -28,8 +29,8 @@ use valence_protocol::packets::play::particle_s2c::Particle; use valence_protocol::packets::play::{ ChunkBiomeDataS2c, ChunkLoadDistanceS2c, ChunkRenderDistanceCenterS2c, DeathMessageS2c, DisconnectS2c, EntitiesDestroyS2c, EntityAttributesS2c, EntityStatusS2c, - EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, GameStateChangeS2c, ParticleS2c, PlaySoundS2c, - UnloadChunkS2c, + EntityTrackerUpdateS2c, EntityVelocityUpdateS2c, GameStateChangeS2c, HealthUpdateS2c, + ParticleS2c, PlaySoundS2c, UnloadChunkS2c, }; use valence_protocol::profile::Property; use valence_protocol::sound::{Sound, SoundCategory, SoundId}; @@ -78,6 +79,7 @@ impl Plugin for ClientPlugin { crate::spawn::respawn.after(crate::spawn::update_respawn_position), update_old_view_dist.after(update_view_and_layers), update_game_mode, + update_food_saturation_health, update_tracked_data, init_tracked_data, update_tracked_attributes, @@ -1109,6 +1111,21 @@ pub(crate) fn update_game_mode(mut clients: Query<(&mut Client, &GameMode), Chan } } +fn update_food_saturation_health( + mut clients: Query< + (&mut Client, &Food, &Saturation, &Health), + Or<(Changed, Changed, Changed)>, + >, +) { + for (mut client, food, saturation, health) in &mut clients { + client.write_packet(&HealthUpdateS2c { + health: health.0, + food: VarInt(food.0), + food_saturation: saturation.0, + }); + } +} + fn update_old_view_dist( mut clients: Query<(&mut OldViewDistance, &ViewDistance), Changed>, ) { diff --git a/src/tests.rs b/src/tests.rs index 8fa089e69..4534f8f42 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,6 +1,7 @@ mod boss_bar; mod client; mod example; +mod hunger; mod inventory; mod layer; mod player_list; diff --git a/src/tests/hunger.rs b/src/tests/hunger.rs new file mode 100644 index 000000000..37f655509 --- /dev/null +++ b/src/tests/hunger.rs @@ -0,0 +1,38 @@ +use valence_server::entity::living::Health; +use valence_server::entity::player::{Food, Saturation}; +use valence_server::protocol::packets::play::HealthUpdateS2c; +use valence_server::protocol::VarInt; + +use crate::testing::ScenarioSingleClient; + +#[test] +fn test_hunger() { + let ScenarioSingleClient { + mut app, + client, + mut helper, + layer: _, + } = ScenarioSingleClient::new(); + + app.update(); + helper.clear_received(); + + let og_saturation = app.world.get::(client).unwrap().0; + let og_health = app.world.get::(client).unwrap().0; + + // set food level to 5 + app.world.get_mut::(client).unwrap().0 = 5; + + app.update(); + + // make sure the packet was sent + let sent_packets = helper.collect_received(); + + sent_packets.assert_count::(1); + + let packet = sent_packets.first::(); + + assert_eq!(packet.health, og_health); + assert_eq!(packet.food, VarInt(5)); + assert_eq!(packet.food_saturation, og_saturation); +}