Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scoreboards #436

Merged
merged 24 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ default = [
"log",
"network",
"player_list",
"scoreboard",
"world_border",
"weather",
]
Expand All @@ -29,6 +30,7 @@ inventory = ["dep:valence_inventory"]
log = ["dep:bevy_log"]
network = ["dep:valence_network"]
player_list = ["dep:valence_player_list"]
scoreboard = ["dep:valence_scoreboard"]
world_border = ["dep:valence_world_border"]
weather = ["dep:valence_weather"]

Expand Down Expand Up @@ -56,6 +58,7 @@ valence_nbt.workspace = true
valence_network = { workspace = true, optional = true }
valence_player_list = { workspace = true, optional = true }
valence_registry.workspace = true
valence_scoreboard = { workspace = true, optional = true }
valence_world_border = { workspace = true, optional = true }
valence_packet.workspace = true
valence_weather = { workspace = true, optional = true }
Expand Down Expand Up @@ -181,6 +184,7 @@ valence_nbt = { path = "crates/valence_nbt", features = ["uuid"] }
valence_network.path = "crates/valence_network"
valence_player_list.path = "crates/valence_player_list"
valence_registry.path = "crates/valence_registry"
valence_scoreboard.path = "crates/valence_scoreboard"
valence_world_border.path = "crates/valence_world_border"
valence_boss_bar.path = "crates/valence_boss_bar"
valence_packet.path = "crates/valence_packet"
Expand Down
2 changes: 1 addition & 1 deletion crates/valence_core/src/uuid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use uuid::Uuid;
///
/// This component is expected to remain _unique_ and _constant_ during the
/// lifetime of the entity. The [`Default`] impl generates a new random UUID.
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Component, Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash)]
pub struct UniqueId(pub Uuid);

/// Generates a new random UUID.
Expand Down
11 changes: 10 additions & 1 deletion crates/valence_packet/src/packets/play/scoreboard_display_s2c.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use bevy_ecs::prelude::Component;

use super::team_s2c::TeamColor;
use super::*;

Expand All @@ -8,11 +10,18 @@ pub struct ScoreboardDisplayS2c<'a> {
pub score_name: &'a str,
}

#[derive(Copy, Clone, PartialEq, Debug)]
/// Defines where a scoreboard is displayed.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Component, Default)]
pub enum ScoreboardPosition {
/// Display the scoreboard in the player list (the one you see when you
/// press tab), as a yellow number next to players' names.
List,
/// Display the scoreboard on the sidebar.
#[default]
Sidebar,
/// Display the scoreboard below players' name tags in the world.
BelowName,
/// Display the scoreboard on the sidebar, visible only to one team.
SidebarTeam(TeamColor),
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
use bevy_ecs::prelude::*;

use super::*;

#[derive(Clone, Debug, Encode, Decode, Packet)]
#[packet(id = packet_id::SCOREBOARD_OBJECTIVE_UPDATE_S2C)]
pub struct ScoreboardObjectiveUpdateS2c<'a> {
pub objective_name: &'a str,
pub mode: ObjectiveMode,
pub mode: ObjectiveMode<'a>,
}

#[derive(Clone, PartialEq, Debug, Encode, Decode)]
pub enum ObjectiveMode {
pub enum ObjectiveMode<'a> {
Create {
objective_display_name: Text,
objective_display_name: Cow<'a, Text>,
render_type: ObjectiveRenderType,
},
Remove,
Update {
objective_display_name: Text,
objective_display_name: Cow<'a, Text>,
render_type: ObjectiveRenderType,
},
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode)]
#[derive(Copy, Clone, PartialEq, Eq, Debug, Encode, Decode, Component, Default)]
pub enum ObjectiveRenderType {
/// Display the value as a number.
#[default]
Integer,
/// Display the value as hearts.
Hearts,
}
14 changes: 14 additions & 0 deletions crates/valence_scoreboard/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[package]
name = "valence_scoreboard"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy_app.workspace = true
bevy_ecs.workspace = true
valence_client.workspace = true
valence_core.workspace = true
valence_entity.workspace = true
valence_layer.workspace = true
valence_packet.workspace = true
tracing.workspace = true
23 changes: 23 additions & 0 deletions crates/valence_scoreboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# valence_scoreboard

This crate provides functionality for creating and managing scoreboards. In Minecraft, a scoreboard references an [`Objective`], which is a mapping from strings to scores. Typically, the string is a player name, and the score is a number of points, but the string can be any arbitrary string <= 40 chars, and the score can be any integer.

In Valence, scoreboards obey the rules implied by [`EntityLayer`]s, meaning that every Objective must have an [`EntityLayerId`] associated with it. Scoreboards are only transmitted to clients if the [`EntityLayer`] is visible to the client.

To create a scoreboard, spawn an [`ObjectiveBundle`]. The [`Objective`] component represents the identifier that the client uses to reference the scoreboard.

Example:

```rust
# use bevy_ecs::prelude::*;
use valence_scoreboard::*;
use valence_core::text::IntoText;

fn spawn_scoreboard(mut commands: Commands) {
commands.spawn(ObjectiveBundle {
name: Objective::new("foo"),
display: ObjectiveDisplay("Foo".bold()),
..Default::default()
});
}
```
119 changes: 119 additions & 0 deletions crates/valence_scoreboard/src/components.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use std::collections::HashMap;

use bevy_ecs::prelude::*;
use valence_core::text::{IntoText, Text};
use valence_entity::EntityLayerId;
use valence_packet::packets::play::scoreboard_display_s2c::ScoreboardPosition;
use valence_packet::packets::play::scoreboard_objective_update_s2c::ObjectiveRenderType;

/// A string that identifies an objective. There is one scoreboard per
/// objective.It's generally not safe to modify this after it's been created.
/// Limited to 16 characters.
///
/// Directly analogous to an Objective's Name.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Component)]
pub struct Objective(pub(crate) String);

impl Objective {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
debug_assert!(
name.len() <= 16,
"Objective name {} is too long ({} > 16)",
name,
name.len()
);
Self(name)
}

pub fn name(&self) -> &str {
&self.0
}
}

/// Optional display name for an objective. If not present, the objective's name
/// is used.
#[derive(Debug, Clone, PartialEq, Component)]
pub struct ObjectiveDisplay(pub Text);

/// A mapping of keys to their scores.
#[derive(Debug, Clone, Component, Default)]
pub struct ObjectiveScores(pub(crate) HashMap<String, i32>);

impl ObjectiveScores {
pub fn new() -> Self {
Default::default()
}

pub fn with_map(map: impl Into<HashMap<String, i32>>) -> Self {
Self(map.into())
}

pub fn get(&self, key: &str) -> Option<&i32> {
self.0.get(key)
}

pub fn get_mut(&mut self, key: &str) -> Option<&mut i32> {
self.0.get_mut(key)
}

pub fn insert(&mut self, key: impl Into<String>, value: i32) -> Option<i32> {
self.0.insert(key.into(), value)
}
}

#[derive(Debug, Clone, Default, PartialEq, Component)]
pub struct OldObjectiveScores(pub(crate) HashMap<String, i32>);

impl OldObjectiveScores {
pub fn diff<'a>(&'a self, scores: &'a ObjectiveScores) -> Vec<&'a str> {
let mut diff = Vec::new();

for (key, value) in &self.0 {
if scores.0.get(key) != Some(value) {
diff.push(key.as_str());
}
}

let new_keys = scores
.0
.keys()
.filter(|key| !self.0.contains_key(key.as_str()))
.map(|key| key.as_str());

let removed_keys = self
.0
.keys()
.filter(|key| !scores.0.contains_key(key.as_str()))
.map(|key| key.as_str());

diff.extend(new_keys);
diff.extend(removed_keys);
diff
}
}

#[derive(Bundle)]
pub struct ObjectiveBundle {
pub name: Objective,
pub display: ObjectiveDisplay,
pub render_type: ObjectiveRenderType,
pub scores: ObjectiveScores,
pub old_scores: OldObjectiveScores,
pub position: ScoreboardPosition,
pub layer: EntityLayerId,
}

impl Default for ObjectiveBundle {
fn default() -> Self {
Self {
name: Objective::new(""),
display: ObjectiveDisplay("".into_text()),
render_type: Default::default(),
scores: Default::default(),
old_scores: Default::default(),
position: Default::default(),
layer: Default::default(),
}
}
}
Loading
Loading