Skip to content

Commit

Permalink
Added food and saturation (valence-rs#568)
Browse files Browse the repository at this point in the history
# Objective

- Food and saturation currently don't exist for the player in valence
- Closes valence-rs#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
  • Loading branch information
SelfMadeSystem authored Oct 18, 2023
1 parent 4c6af33 commit 7fab7fe
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 45 deletions.
118 changes: 76 additions & 42 deletions crates/valence_entity/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,45 +366,59 @@ fn build() -> anyhow::Result<TokenStream> {
#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 } => {
Expand Down Expand Up @@ -546,12 +560,32 @@ fn build() -> anyhow::Result<TokenStream> {
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! {
Expand Down
23 changes: 20 additions & 3 deletions crates/valence_server/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Food>, Changed<Saturation>, Changed<Health>)>,
>,
) {
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<ViewDistance>>,
) {
Expand Down
1 change: 1 addition & 0 deletions src/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod boss_bar;
mod client;
mod example;
mod hunger;
mod inventory;
mod layer;
mod player_list;
Expand Down
38 changes: 38 additions & 0 deletions src/tests/hunger.rs
Original file line number Diff line number Diff line change
@@ -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::<Saturation>(client).unwrap().0;
let og_health = app.world.get::<Health>(client).unwrap().0;

// set food level to 5
app.world.get_mut::<Food>(client).unwrap().0 = 5;

app.update();

// make sure the packet was sent
let sent_packets = helper.collect_received();

sent_packets.assert_count::<HealthUpdateS2c>(1);

let packet = sent_packets.first::<HealthUpdateS2c>();

assert_eq!(packet.health, og_health);
assert_eq!(packet.food, VarInt(5));
assert_eq!(packet.food_saturation, og_saturation);
}

0 comments on commit 7fab7fe

Please sign in to comment.