diff --git a/Cargo.toml b/Cargo.toml index 0da3447d7..b5ac683dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ default = [ "anvil", "advancement", "world_border", + "boss_bar", ] network = ["dep:valence_network"] player_list = ["dep:valence_player_list"] @@ -25,6 +26,7 @@ inventory = ["dep:valence_inventory"] anvil = ["dep:valence_anvil"] advancement = ["dep:valence_advancement"] world_border = ["dep:valence_world_border"] +boss_bar = ["dep:valence_boss_bar"] [dependencies] bevy_app.workspace = true @@ -46,6 +48,7 @@ valence_inventory = { workspace = true, optional = true } valence_anvil = { workspace = true, optional = true } valence_advancement = { workspace = true, optional = true } valence_world_border = { workspace = true, optional = true } +valence_boss_bar = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true @@ -170,5 +173,6 @@ valence_network.path = "crates/valence_network" valence_player_list.path = "crates/valence_player_list" valence_registry.path = "crates/valence_registry" valence_world_border.path = "crates/valence_world_border" +valence_boss_bar.path = "crates/valence_boss_bar" valence.path = "." zip = "0.6.3" diff --git a/crates/README.md b/crates/README.md index 7a63625c3..5937cbc32 100644 --- a/crates/README.md +++ b/crates/README.md @@ -23,4 +23,5 @@ graph TD entity --> block advancement --> client world_border --> client + boss_bar --> client ``` diff --git a/crates/valence_boss_bar/Cargo.toml b/crates/valence_boss_bar/Cargo.toml new file mode 100644 index 000000000..efe5fdfee --- /dev/null +++ b/crates/valence_boss_bar/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "valence_boss_bar" +description = "Boss bar API for Valence" +readme = "README.md" +keywords = ["minecraft", "bossbar", "api"] +documentation.workspace = true +version.workspace = true +edition.workspace = true + +[dependencies] +valence_core.workspace = true +valence_network.workspace = true +valence_entity.workspace = true +valence_client.workspace = true +uuid.workspace = true +bitfield-struct.workspace = true +bevy_app.workspace = true +bevy_ecs.workspace = true \ No newline at end of file diff --git a/crates/valence_boss_bar/README.md b/crates/valence_boss_bar/README.md new file mode 100644 index 000000000..f5ce42d7b --- /dev/null +++ b/crates/valence_boss_bar/README.md @@ -0,0 +1,3 @@ +# valence_boss_bar + +Manages Minecraft's boss bar which is the bar seen at the top of the screen when a boss mob is present. \ No newline at end of file diff --git a/crates/valence_boss_bar/src/components.rs b/crates/valence_boss_bar/src/components.rs new file mode 100644 index 000000000..86f422e45 --- /dev/null +++ b/crates/valence_boss_bar/src/components.rs @@ -0,0 +1,96 @@ +use std::collections::BTreeSet; + +use bevy_ecs::prelude::{Bundle, Component, Entity}; +use bitfield_struct::bitfield; +use valence_core::protocol::{Decode, Encode}; +use valence_core::text::Text; +use valence_core::uuid::UniqueId; + +/// The bundle of components that make up a boss bar. +#[derive(Bundle)] +pub struct BossBarBundle { + pub id: UniqueId, + pub title: BossBarTitle, + pub health: BossBarHealth, + pub style: BossBarStyle, + pub flags: BossBarFlags, + pub viewers: BossBarViewers, +} + +impl BossBarBundle { + pub fn new( + title: Text, + color: BossBarColor, + division: BossBarDivision, + flags: BossBarFlags, + ) -> BossBarBundle { + BossBarBundle { + id: UniqueId::default(), + title: BossBarTitle(title), + health: BossBarHealth(1.0), + style: BossBarStyle { color, division }, + flags, + viewers: BossBarViewers::default(), + } + } +} + +/// The title of a boss bar. +#[derive(Component, Clone)] +pub struct BossBarTitle(pub Text); + +/// The health of a boss bar. +#[derive(Component)] +pub struct BossBarHealth(pub f32); + +/// The style of a boss bar. This includes the color and division of the boss +/// bar. +#[derive(Component)] +pub struct BossBarStyle { + pub color: BossBarColor, + pub division: BossBarDivision, +} + +/// The color of a boss bar. +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub enum BossBarColor { + Pink, + Blue, + Red, + Green, + Yellow, + Purple, + White, +} + +/// The division of a boss bar. +#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub enum BossBarDivision { + NoDivision, + SixNotches, + TenNotches, + TwelveNotches, + TwentyNotches, +} + +/// The flags of a boss bar (darken sky, dragon bar, create fog). +#[bitfield(u8)] +#[derive(Component, PartialEq, Eq, Encode, Decode)] +pub struct BossBarFlags { + pub darken_sky: bool, + pub dragon_bar: bool, + pub create_fog: bool, + #[bits(5)] + _pad: u8, +} + +/// The viewers of a boss bar. +#[derive(Component, Default)] +pub struct BossBarViewers { + /// The current viewers of the boss bar. It is the list that should be + /// updated. + pub viewers: BTreeSet, + /// The viewers of the last tick in order to determine which viewers have + /// been added and removed. + pub(crate) old_viewers: BTreeSet, +} diff --git a/crates/valence_boss_bar/src/lib.rs b/crates/valence_boss_bar/src/lib.rs new file mode 100644 index 000000000..8d63f24a3 --- /dev/null +++ b/crates/valence_boss_bar/src/lib.rs @@ -0,0 +1,209 @@ +#![doc = include_str!("../README.md")] +#![allow(clippy::type_complexity)] +#![deny( + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + rustdoc::invalid_html_tags +)] +#![warn( + trivial_casts, + trivial_numeric_casts, + unused_lifetimes, + unused_import_braces, + unreachable_pub, + clippy::dbg_macro +)] + +use std::borrow::Cow; + +use bevy_app::CoreSet::PostUpdate; +use bevy_app::Plugin; +use bevy_ecs::prelude::Entity; +use bevy_ecs::query::{Added, Changed, With}; +use bevy_ecs::schedule::{IntoSystemConfig, IntoSystemConfigs}; +use bevy_ecs::system::Query; +use packet::{BossBarAction, BossBarS2c}; +use valence_client::{Client, FlushPacketsSet}; +use valence_core::despawn::Despawned; +use valence_core::protocol::encode::WritePacket; +use valence_core::uuid::UniqueId; + +mod components; +pub use components::*; + +pub mod packet; + +pub struct BossBarPlugin; + +impl Plugin for BossBarPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.add_systems( + ( + boss_bar_title_update, + boss_bar_health_update, + boss_bar_style_update, + boss_bar_flags_update, + boss_bar_viewers_update, + boss_bar_despawn, + client_disconnection.before(boss_bar_viewers_update), + ) + .before(FlushPacketsSet) + .in_base_set(PostUpdate), + ); + } +} + +/// System that sends a bossbar update title packet to all viewers of a boss bar +/// that has had its title updated. +fn boss_bar_title_update( + boss_bars: Query<(&UniqueId, &BossBarTitle, &BossBarViewers), Changed>, + mut clients: Query<&mut Client>, +) { + for (id, title, boss_bar_viewers) in boss_bars.iter() { + for viewer in boss_bar_viewers.viewers.iter() { + if let Ok(mut client) = clients.get_mut(*viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::UpdateTitle(Cow::Borrowed(&title.0)), + }); + } + } + } +} + +/// System that sends a bossbar update health packet to all viewers of a boss +/// bar that has had its health updated. +fn boss_bar_health_update( + boss_bars: Query<(&UniqueId, &BossBarHealth, &BossBarViewers), Changed>, + mut clients: Query<&mut Client>, +) { + for (id, health, boss_bar_viewers) in boss_bars.iter() { + for viewer in boss_bar_viewers.viewers.iter() { + if let Ok(mut client) = clients.get_mut(*viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::UpdateHealth(health.0), + }); + } + } + } +} + +/// System that sends a bossbar update style packet to all viewers of a boss bar +/// that has had its style updated. +fn boss_bar_style_update( + boss_bars: Query<(&UniqueId, &BossBarStyle, &BossBarViewers), Changed>, + mut clients: Query<&mut Client>, +) { + for (id, style, boss_bar_viewers) in boss_bars.iter() { + for viewer in boss_bar_viewers.viewers.iter() { + if let Ok(mut client) = clients.get_mut(*viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::UpdateStyle(style.color, style.division), + }); + } + } + } +} + +/// System that sends a bossbar update flags packet to all viewers of a boss bar +/// that has had its flags updated. +fn boss_bar_flags_update( + boss_bars: Query<(&UniqueId, &BossBarFlags, &BossBarViewers), Changed>, + mut clients: Query<&mut Client>, +) { + for (id, flags, boss_bar_viewers) in boss_bars.iter() { + for viewer in boss_bar_viewers.viewers.iter() { + if let Ok(mut client) = clients.get_mut(*viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::UpdateFlags(*flags), + }); + } + } + } +} + +/// System that sends a bossbar add/remove packet to all viewers of a boss bar +/// that just have been added/removed. +fn boss_bar_viewers_update( + mut boss_bars: Query< + ( + &UniqueId, + &BossBarTitle, + &BossBarHealth, + &BossBarStyle, + &BossBarFlags, + &mut BossBarViewers, + ), + Changed, + >, + mut clients: Query<&mut Client>, +) { + for (id, title, health, style, flags, mut boss_bar_viewers) in boss_bars.iter_mut() { + let old_viewers = &boss_bar_viewers.old_viewers; + let current_viewers = &boss_bar_viewers.viewers; + + for &added_viewer in current_viewers.difference(old_viewers) { + if let Ok(mut client) = clients.get_mut(added_viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::Add { + title: Cow::Borrowed(&title.0), + health: health.0, + color: style.color, + division: style.division, + flags: *flags, + }, + }); + } + } + + for &removed_viewer in old_viewers.difference(current_viewers) { + if let Ok(mut client) = clients.get_mut(removed_viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::Remove, + }); + } + } + + boss_bar_viewers.old_viewers = boss_bar_viewers.viewers.clone(); + } +} + +/// System that sends a bossbar remove packet to all viewers of a boss bar that +/// has been despawned. +fn boss_bar_despawn( + mut boss_bars: Query<(&UniqueId, &BossBarViewers), Added>, + mut clients: Query<&mut Client>, +) { + for (id, viewers) in boss_bars.iter_mut() { + for viewer in viewers.viewers.iter() { + if let Ok(mut client) = clients.get_mut(*viewer) { + client.write_packet(&BossBarS2c { + id: id.0, + action: BossBarAction::Remove, + }); + } + } + } +} + +/// System that removes a client from the viewers of its boss bars when it +/// disconnects. +fn client_disconnection( + disconnected_clients: Query, Added)>, + mut boss_bars_viewers: Query<&mut BossBarViewers>, +) { + for entity in disconnected_clients.iter() { + for mut boss_bar_viewers in boss_bars_viewers.iter_mut() { + boss_bar_viewers.viewers.remove(&entity); + } + } +} diff --git a/crates/valence_boss_bar/src/packet.rs b/crates/valence_boss_bar/src/packet.rs new file mode 100644 index 000000000..ef87b82ef --- /dev/null +++ b/crates/valence_boss_bar/src/packet.rs @@ -0,0 +1,30 @@ +use std::borrow::Cow; + +use uuid::Uuid; +use valence_core::protocol::{packet_id, Decode, Encode, Packet}; +use valence_core::text::Text; + +use crate::components::{BossBarColor, BossBarDivision, BossBarFlags}; + +#[derive(Clone, Debug, Encode, Decode, Packet)] +#[packet(id = packet_id::BOSS_BAR_S2C)] +pub struct BossBarS2c<'a> { + pub id: Uuid, + pub action: BossBarAction<'a>, +} + +#[derive(Clone, PartialEq, Debug, Encode, Decode)] +pub enum BossBarAction<'a> { + Add { + title: Cow<'a, Text>, + health: f32, + color: BossBarColor, + division: BossBarDivision, + flags: BossBarFlags, + }, + Remove, + UpdateHealth(f32), + UpdateTitle(Cow<'a, Text>), + UpdateStyle(BossBarColor, BossBarDivision), + UpdateFlags(BossBarFlags), +} diff --git a/crates/valence_core/src/protocol/packet.rs b/crates/valence_core/src/protocol/packet.rs index 655ff28e6..d11b6ded1 100644 --- a/crates/valence_core/src/protocol/packet.rs +++ b/crates/valence_core/src/protocol/packet.rs @@ -608,64 +608,6 @@ pub mod scoreboard { } } -// TODO: move to valence_boss_bar? -pub mod boss_bar { - use super::*; - - #[derive(Clone, Debug, Encode, Decode, Packet)] - #[packet(id = packet_id::BOSS_BAR_S2C)] - pub struct BossBarS2c { - pub id: Uuid, - pub action: BossBarAction, - } - - #[derive(Clone, PartialEq, Debug, Encode, Decode)] - pub enum BossBarAction { - Add { - title: Text, - health: f32, - color: BossBarColor, - division: BossBarDivision, - flags: BossBarFlags, - }, - Remove, - UpdateHealth(f32), - UpdateTitle(Text), - UpdateStyle(BossBarColor, BossBarDivision), - UpdateFlags(BossBarFlags), - } - - #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] - pub enum BossBarColor { - Pink, - Blue, - Red, - Green, - Yellow, - Purple, - White, - } - - #[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)] - pub enum BossBarDivision { - NoDivision, - SixNotches, - TenNotches, - TwelveNotches, - TwentyNotches, - } - - #[bitfield(u8)] - #[derive(PartialEq, Eq, Encode, Decode)] - pub struct BossBarFlags { - pub darken_sky: bool, - pub dragon_bar: bool, - pub create_fog: bool, - #[bits(5)] - _pad: u8, - } -} - // TODO: move to valence_sound? pub mod sound { use super::*; diff --git a/examples/boss_bar.rs b/examples/boss_bar.rs new file mode 100644 index 000000000..c851f98a0 --- /dev/null +++ b/examples/boss_bar.rs @@ -0,0 +1,183 @@ +use rand::seq::SliceRandom; +use valence::prelude::*; +use valence_boss_bar::{ + BossBarBundle, BossBarColor, BossBarDivision, BossBarFlags, BossBarHealth, BossBarStyle, + BossBarTitle, BossBarViewers, +}; +use valence_client::message::{ChatMessageEvent, SendMessage}; + +const SPAWN_Y: i32 = 64; + +pub fn main() { + tracing_subscriber::fmt().init(); + + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(init_clients) + .add_system(despawn_disconnected_clients) + .add_system(listen_messages) + .run(); +} + +fn setup( + mut commands: Commands, + server: Res, + dimensions: Res, + biomes: Res, +) { + let mut instance = Instance::new(ident!("overworld"), &dimensions, &biomes, &server); + + for z in -5..5 { + for x in -5..5 { + instance.insert_chunk([x, z], Chunk::default()); + } + } + + for z in -25..25 { + for x in -25..25 { + instance.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK); + } + } + + commands.spawn(BossBarBundle::new( + Text::text("Boss bar"), + BossBarColor::Blue, + BossBarDivision::TenNotches, + BossBarFlags::new(), + )); + + commands.spawn(instance); +} + +fn init_clients( + mut clients: Query< + ( + Entity, + &mut Client, + &mut Location, + &mut Position, + &mut GameMode, + ), + Added, + >, + mut boss_bar_viewers: Query<&mut BossBarViewers>, + instances: Query>, +) { + let mut boss_bar_viewers = boss_bar_viewers.single_mut(); + for (entity, mut client, mut loc, mut pos, mut game_mode) in &mut clients { + loc.0 = instances.single(); + pos.set([0.5, SPAWN_Y as f64 + 1.0, 0.5]); + *game_mode = GameMode::Creative; + + client.send_chat_message( + "Type 'view' to toggle bar display" + .on_click_suggest_command("view") + .on_hover_show_text("Type 'view'"), + ); + client.send_chat_message( + "Type 'color' to set a random color" + .on_click_suggest_command("color") + .on_hover_show_text("Type 'color'"), + ); + client.send_chat_message( + "Type 'division' to set a random division" + .on_click_suggest_command("division") + .on_hover_show_text("Type 'division'"), + ); + client.send_chat_message( + "Type 'flags' to set random flags" + .on_click_suggest_command("flags") + .on_hover_show_text("Type 'flags'"), + ); + client.send_chat_message( + "Type any string to set the title".on_click_suggest_command("title"), + ); + client.send_chat_message( + "Type any number between 0 and 1 to set the health".on_click_suggest_command("health"), + ); + + boss_bar_viewers.viewers.insert(entity); + } +} + +fn listen_messages( + mut message_events: EventReader, + mut boss_bar: Query<( + &mut BossBarViewers, + &mut BossBarStyle, + &mut BossBarFlags, + &mut BossBarHealth, + &mut BossBarTitle, + )>, +) { + let ( + mut boss_bar_viewers, + mut boss_bar_style, + mut boss_bar_flags, + mut boss_bar_health, + mut boss_bar_title, + ) = boss_bar.single_mut(); + + let events: Vec = message_events.iter().cloned().collect(); + for ChatMessageEvent { + client, message, .. + } in events.iter() + { + match message.as_ref() { + "view" => { + if boss_bar_viewers.viewers.contains(client) { + boss_bar_viewers.viewers.remove(client); + } else { + boss_bar_viewers.viewers.insert(*client); + } + } + "color" => { + let mut colors = vec![ + BossBarColor::Pink, + BossBarColor::Blue, + BossBarColor::Red, + BossBarColor::Green, + BossBarColor::Yellow, + ]; + colors.retain(|c| *c != boss_bar_style.color); + + let random_color = colors.choose(&mut rand::thread_rng()).unwrap(); + + boss_bar_style.color = *random_color; + } + "division" => { + let mut divisions = vec![ + BossBarDivision::NoDivision, + BossBarDivision::SixNotches, + BossBarDivision::TenNotches, + BossBarDivision::TwelveNotches, + BossBarDivision::TwentyNotches, + ]; + divisions.retain(|d| *d != boss_bar_style.division); + + let random_division = divisions.choose(&mut rand::thread_rng()).unwrap(); + + boss_bar_style.division = *random_division; + } + "flags" => { + let darken_sky: bool = rand::random(); + let dragon_bar: bool = rand::random(); + let create_fog: bool = rand::random(); + + boss_bar_flags.set_darken_sky(darken_sky); + boss_bar_flags.set_dragon_bar(dragon_bar); + boss_bar_flags.set_create_fog(create_fog); + } + _ => { + if let Ok(health) = message.parse::() { + if (0.0..=1.0).contains(&health) { + boss_bar_health.0 = health; + } + } else { + boss_bar_title.0 = message.to_string().into(); + } + } + }; + } +} diff --git a/src/lib.rs b/src/lib.rs index 9ee2077cd..35b596622 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,6 +30,8 @@ mod tests; pub use valence_advancement as advancement; #[cfg(feature = "anvil")] pub use valence_anvil as anvil; +#[cfg(feature = "boss_bar")] +pub use valence_boss_bar as boss_bar; pub use valence_core::*; #[cfg(feature = "inventory")] pub use valence_inventory as inventory; @@ -171,6 +173,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(valence_world_border::WorldBorderPlugin); } + #[cfg(feature = "boss_bar")] + { + group = group.add(valence_boss_bar::BossBarPlugin); + } + group } } diff --git a/src/tests.rs b/src/tests.rs index 9f88b6bce..17e299175 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -280,6 +280,7 @@ impl_packet_list!(A, B, C, D, E, F, G, H, I); impl_packet_list!(A, B, C, D, E, F, G, H, I, J); impl_packet_list!(A, B, C, D, E, F, G, H, I, J, K); +mod boss_bar; mod client; mod example; mod inventory; diff --git a/src/tests/boss_bar.rs b/src/tests/boss_bar.rs new file mode 100644 index 000000000..4d75fe52c --- /dev/null +++ b/src/tests/boss_bar.rs @@ -0,0 +1,208 @@ +use bevy_app::App; +use bevy_ecs::entity::Entity; +use valence_boss_bar::packet::BossBarS2c; +use valence_boss_bar::{ + BossBarBundle, BossBarColor, BossBarDivision, BossBarFlags, BossBarHealth, BossBarStyle, + BossBarTitle, BossBarViewers, +}; +use valence_core::despawn::Despawned; +use valence_core::text::Text; + +use super::{scenario_single_client, MockClientHelper}; + +#[test] +fn test_intialize_on_join() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Check if a boss bar packet was sent + let frames = client_helper.collect_sent(); + frames.assert_count::(1); +} + +#[test] +fn test_despawn() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Despawn the boss bar + app.world.entity_mut(instance_ent).insert(Despawned); + + app.update(); + + // Check if a boss bar packet was sent in addition to the ADD packet, which + // should be a Remove packet + let frames = client_helper.collect_sent(); + frames.assert_count::(2); +} + +#[test] +fn test_title_update() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Update the title + app.world + .entity_mut(instance_ent) + .insert(BossBarTitle(Text::text("Test 2"))); + + app.update(); + + // Check if a boss bar packet was sent in addition to the ADD packet, which + // should be an UpdateTitle packet + let frames = client_helper.collect_sent(); + frames.assert_count::(2); +} + +#[test] +fn test_health_update() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Update the health + app.world + .entity_mut(instance_ent) + .insert(BossBarHealth(0.5)); + + app.update(); + + // Check if a boss bar packet was sent in addition to the ADD packet, which + // should be an UpdateHealth packet + let frames = client_helper.collect_sent(); + frames.assert_count::(2); +} + +#[test] +fn test_style_update() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Update the style + app.world.entity_mut(instance_ent).insert(BossBarStyle { + color: BossBarColor::Red, + division: BossBarDivision::TenNotches, + }); + + app.update(); + + // Check if a boss bar packet was sent in addition to the ADD packet, which + // should be an UpdateStyle packet + let frames = client_helper.collect_sent(); + frames.assert_count::(2); +} + +#[test] +fn test_flags_update() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Update the flags + let mut new_flags = BossBarFlags::new(); + new_flags.set_create_fog(true); + app.world.entity_mut(instance_ent).insert(new_flags); + + app.update(); + + // Check if a boss bar packet was sent in addition to the ADD packet, which + // should be an UpdateFlags packet + let frames = client_helper.collect_sent(); + frames.assert_count::(2); +} + +#[test] +fn test_client_disconnection() { + let mut app = App::new(); + let (client_ent, mut client_helper, instance_ent) = prepare(&mut app); + + // Fetch the boss bar component + let mut boss_bar = app.world.get_mut::(instance_ent).unwrap(); + // Add our mock client to the viewers list + assert!(boss_bar.viewers.insert(client_ent)); + + app.update(); + + // Remove the client from the world + app.world.entity_mut(client_ent).insert(Despawned); + + app.update(); + + assert!(app + .world + .get_mut::(instance_ent) + .unwrap() + .viewers + .is_empty()); + + // Check if a boss bar packet was sent in addition to the ADD packet, which + // should be a Remove packet + let frames = client_helper.collect_sent(); + frames.assert_count::(2); +} + +fn prepare(app: &mut App) -> (Entity, MockClientHelper, Entity) { + let (client_ent, mut client_helper) = scenario_single_client(app); + + // Process a tick to get past the "on join" logic. + app.update(); + client_helper.clear_sent(); + + // Insert a boss bar into the world + let boss_bar = app + .world + .spawn(BossBarBundle::new( + Text::text("Test"), + BossBarColor::Blue, + BossBarDivision::SixNotches, + BossBarFlags::new(), + )) + .id(); + + for _ in 0..2 { + app.update(); + } + + client_helper.clear_sent(); + (client_ent, client_helper, boss_bar) +} diff --git a/tools/playground/Cargo.toml b/tools/playground/Cargo.toml index 417982710..656de011a 100644 --- a/tools/playground/Cargo.toml +++ b/tools/playground/Cargo.toml @@ -10,4 +10,4 @@ glam.workspace = true tracing-subscriber.workspace = true tracing.workspace = true valence_core.workspace = true -valence.workspace = true +valence.workspace = true \ No newline at end of file