Skip to content

Commit

Permalink
Boss Bar API (#371)
Browse files Browse the repository at this point in the history
## Description

Hey, I started working on this issue
#337.

### So far:
- I created a crate "**valence_boss_bar**" and I moved this packet there
https://github.com/valence-rs/valence/blob/2ed5a8840da244d8f1f91f1df43750664952be46/crates/valence_core/src/protocol/packet.rs#L612
- I initialized the plugin and added a system that removes the players
when a boss bar is despawned.
- I created/moved the boss bar components (BossBarTitle, BossBarHealth,
BossBarStyle, BossBarFlags and BossBarViewers)
- I added systems that handle the update of boss bar attributes and
player disconnection
- I documented the code
- I made unit tests

### Playground
I've tested the API in playground and it works fine.


https://github.com/valence-rs/valence/assets/29759371/5661873b-c3a7-4a5d-b9e6-6c739bef4c2f

---------

Co-authored-by: pepperoni21 <[email protected]>
  • Loading branch information
pepperoni21 and pepperoni21 authored Jun 20, 2023
1 parent 26d082c commit 0f3e6f2
Show file tree
Hide file tree
Showing 13 changed files with 761 additions and 59 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ default = [
"anvil",
"advancement",
"world_border",
"boss_bar",
]
network = ["dep:valence_network"]
player_list = ["dep:valence_player_list"]
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
Expand All @@ -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
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions crates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ graph TD
entity --> block
advancement --> client
world_border --> client
boss_bar --> client
```
18 changes: 18 additions & 0 deletions crates/valence_boss_bar/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions crates/valence_boss_bar/README.md
Original file line number Diff line number Diff line change
@@ -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.
96 changes: 96 additions & 0 deletions crates/valence_boss_bar/src/components.rs
Original file line number Diff line number Diff line change
@@ -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<Entity>,
/// The viewers of the last tick in order to determine which viewers have
/// been added and removed.
pub(crate) old_viewers: BTreeSet<Entity>,
}
209 changes: 209 additions & 0 deletions crates/valence_boss_bar/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<BossBarTitle>>,
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<BossBarHealth>>,
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<BossBarStyle>>,
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<BossBarFlags>>,
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<BossBarViewers>,
>,
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<Despawned>>,
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<Entity, (With<Client>, Added<Despawned>)>,
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);
}
}
}
30 changes: 30 additions & 0 deletions crates/valence_boss_bar/src/packet.rs
Original file line number Diff line number Diff line change
@@ -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),
}
Loading

0 comments on commit 0f3e6f2

Please sign in to comment.