Pumpkin/pumpkin/src/world/scoreboard.rs
Alexander Medvedev 3629f43dcc fix: disable cave carver for now
fixes some issues
2026-05-23 16:36:08 +02:00

379 lines
11 KiB
Rust

use std::collections::HashMap;
use pumpkin_data::scoreboard::ScoreboardDisplaySlot;
use pumpkin_protocol::{
BClientPacket, ClientPacket, NumberFormat,
bedrock::client::scoreboard::{
CRemoveObjective as BRemoveObjective, CSetDisplayObjective as BSetDisplayObjective,
CSetScore as BSetScore, ScoreEntry as BScoreEntry,
},
codec::var_int::VarInt,
java::client::play::{
CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, Mode, RenderType,
TeamMethod, TeamParameters,
},
};
use pumpkin_util::text::{TextComponent, color::NamedColor};
use tracing::warn;
use super::World;
#[derive(Default)]
pub struct Scoreboard {
objectives: HashMap<String, ScoreboardObjective<'static>>,
teams: HashMap<String, Team>,
scores: HashMap<String, HashMap<String, ScoreboardScore<'static>>>,
}
impl Scoreboard {
async fn broadcast_editioned<J: ClientPacket, B: BClientPacket>(
world: &World,
je_packet: &J,
be_packet: &B,
) {
world.broadcast_editioned(je_packet, be_packet).await;
}
pub async fn add_objective(&mut self, world: &World, objective: ScoreboardObjective<'static>) {
if self.objectives.contains_key(objective.name) {
warn!(
"Tried to create an objective which already exists: {}",
&objective.name
);
return;
}
let je_update = CUpdateObjectives::new(
objective.name.to_string(),
Mode::Add,
objective.display_name.clone(),
objective.render_type,
objective.number_format.clone(),
);
let be_update = BSetDisplayObjective {
display_slot: "sidebar".to_string(), // Default to sidebar
objective_name: objective.name.to_string(),
display_name: objective.display_name.clone().get_text(),
criteria_name: "dummy".to_string(),
sort_order: VarInt(0),
};
Self::broadcast_editioned(world, &je_update, &be_update).await;
let je_display =
CDisplayObjective::new(ScoreboardDisplaySlot::Sidebar, objective.name.to_string());
// Bedrock's SetDisplayObjective already sets the slot.
world.broadcast_packet_all(&je_display);
self.objectives
.insert(objective.name.to_string(), objective);
}
pub async fn remove_objective(&mut self, world: &World, name: &str) {
if !self.objectives.contains_key(name) {
warn!(
"Tried to remove an objective which does not exist: {}",
name
);
return;
}
let je_packet = CUpdateObjectives::new(
name.to_string(),
Mode::Remove,
TextComponent::empty(),
RenderType::Integer,
None,
);
let be_packet = BRemoveObjective {
objective_name: name.to_string(),
};
Self::broadcast_editioned(world, &je_packet, &be_packet).await;
self.objectives.remove(name);
self.scores.remove(name);
}
pub async fn update_score(&mut self, world: &World, score: ScoreboardScore<'static>) {
if !self.objectives.contains_key(score.objective_name) {
warn!(
"Tried to place a score into an objective which does not exist: {}",
&score.objective_name
);
return;
}
let je_packet = CUpdateScore::new(
score.entity_name.to_string(),
score.objective_name.to_string(),
score.value,
score.display_name.clone(),
score.number_format.clone(),
);
let be_packet = BSetScore {
action: VarInt(0), // Change
entries: vec![BScoreEntry {
scoreboard_id: score.entity_name.as_ptr() as i64, // Hacky ID
objective_name: score.objective_name.to_string(),
score: score.value,
entry_type: VarInt(3), // Fake player/Literal
entity_unique_id: 0,
custom_name: score.entity_name.to_string(),
}],
};
Self::broadcast_editioned(world, &je_packet, &be_packet).await;
self.scores
.entry(score.objective_name.to_string())
.or_default()
.insert(score.entity_name.to_string(), score);
}
pub async fn remove_score(&mut self, world: &World, entity_name: &str, objective_name: &str) {
let je_packet =
CUpdateScore::new_remove(entity_name.to_string(), objective_name.to_string());
let be_packet = BSetScore {
action: VarInt(1), // Remove
entries: vec![BScoreEntry {
scoreboard_id: entity_name.as_ptr() as i64,
objective_name: objective_name.to_string(),
score: VarInt(0),
entry_type: VarInt(3),
entity_unique_id: 0,
custom_name: entity_name.to_string(),
}],
};
Self::broadcast_editioned(world, &je_packet, &be_packet).await;
if let Some(objective_scores) = self.scores.get_mut(objective_name) {
objective_scores.remove(entity_name);
}
}
pub fn add_team(&mut self, world: &World, team: Team) {
if self.teams.contains_key(&team.name) {
warn!(
"Tried to create Team which does already exist, {}",
team.name
);
return;
}
let parameters = TeamParameters {
display_name: &team.display_name,
options: team.options,
nametag_visibility: team.nametag_visibility.to_str(),
collision_rule: team.collision_rule.to_str(),
color: team.color as i32,
player_prefix: &team.player_prefix,
player_suffix: &team.player_suffix,
};
world.broadcast_packet_all(&CSetPlayerTeam {
team_name: team.name.clone(),
method: TeamMethod::Create,
parameters: Some(parameters),
players: team.players.clone().into(),
});
self.teams.insert(team.name.clone(), team);
}
pub fn update_team(&mut self, world: &World, team: Team) {
if !self.teams.contains_key(&team.name) {
warn!("Tried to update Team which does not exist, {}", team.name);
return;
}
let parameters = TeamParameters {
display_name: &team.display_name,
options: team.options,
nametag_visibility: team.nametag_visibility.to_str(),
collision_rule: team.collision_rule.to_str(),
color: team.color as i32,
player_prefix: &team.player_prefix,
player_suffix: &team.player_suffix,
};
world.broadcast_packet_all(&CSetPlayerTeam {
team_name: team.name.clone(),
method: TeamMethod::Update,
parameters: Some(parameters),
players: Box::new([]),
});
self.teams.insert(team.name.clone(), team);
}
pub fn remove_team(&mut self, world: &World, name: &str) {
if !self.teams.contains_key(name) {
warn!("Tried to remove Team which does not exist, {}", name);
return;
}
world.broadcast_packet_all(&CSetPlayerTeam {
team_name: name.to_string(),
method: TeamMethod::Remove,
parameters: None,
players: Box::new([]),
});
self.teams.remove(name);
}
pub fn add_player_to_team(&mut self, world: &World, team_name: &str, player: String) {
let Some(team) = self.teams.get_mut(team_name) else {
warn!(
"Tried to add player to Team which does not exist, {}",
team_name
);
return;
};
if team.players.contains(&player) {
return;
}
world.broadcast_packet_all(&CSetPlayerTeam {
team_name: team_name.to_string(),
method: TeamMethod::AddPlayers,
parameters: None,
players: vec![player.clone()].into(),
});
team.players.push(player);
}
pub fn remove_player_from_team(&mut self, world: &World, team_name: &str, player: &str) {
let Some(team) = self.teams.get_mut(team_name) else {
warn!(
"Tried to remove player from Team which does not exist, {}",
team_name
);
return;
};
if !team.players.contains(&player.to_string()) {
return;
}
world.broadcast_packet_all(&CSetPlayerTeam {
team_name: team_name.to_string(),
method: TeamMethod::RemovePlayers,
parameters: None,
players: vec![player.to_string()].into(),
});
team.players.retain(|p| p != player);
}
}
pub struct ScoreboardObjective<'a> {
pub name: &'a str,
pub display_name: TextComponent,
pub render_type: RenderType,
pub number_format: Option<NumberFormat>,
}
impl<'a> ScoreboardObjective<'a> {
#[must_use]
pub const fn new(
name: &'a str,
display_name: TextComponent,
render_type: RenderType,
number_format: Option<NumberFormat>,
) -> Self {
Self {
name,
display_name,
render_type,
number_format,
}
}
}
pub struct ScoreboardScore<'a> {
pub entity_name: &'a str,
pub objective_name: &'a str,
pub value: VarInt,
pub display_name: Option<TextComponent>,
pub number_format: Option<NumberFormat>,
}
impl<'a> ScoreboardScore<'a> {
#[must_use]
pub const fn new(
entity_name: &'a str,
objective_name: &'a str,
value: VarInt,
display_name: Option<TextComponent>,
number_format: Option<NumberFormat>,
) -> Self {
Self {
entity_name,
objective_name,
value,
display_name,
number_format,
}
}
}
pub enum NameTagVisibility {
Always,
Never,
HideForOtherTeams,
HideForOwnTeam,
}
impl NameTagVisibility {
#[must_use]
pub const fn to_str(&self) -> &'static str {
match self {
Self::Always => "always",
Self::Never => "never",
Self::HideForOtherTeams => "hideForOtherTeams",
Self::HideForOwnTeam => "hideForOwnTeam",
}
}
}
pub enum CollisionRule {
Always,
Never,
PushOtherTeams,
PushOwnTeam,
}
impl CollisionRule {
#[must_use]
pub const fn to_str(&self) -> &'static str {
match self {
Self::Always => "always",
Self::Never => "never",
Self::PushOtherTeams => "pushOtherTeams",
Self::PushOwnTeam => "pushOwnTeam",
}
}
}
pub struct Team {
pub name: String,
pub display_name: TextComponent,
pub options: i8,
pub nametag_visibility: NameTagVisibility,
pub collision_rule: CollisionRule,
pub color: NamedColor,
pub player_prefix: TextComponent,
pub player_suffix: TextComponent,
pub players: Vec<String>,
}