feat(bedrock): add bossbar & scoreboard

This commit is contained in:
Alexander Medvedev 2026-05-19 21:55:23 +02:00
parent 330a68054f
commit 7fe103db67
8 changed files with 380 additions and 55 deletions

View File

@ -0,0 +1,75 @@
use std::io::{Error, Write};
use crate::{
codec::{var_int::VarInt, var_long::VarLong},
serial::PacketWrite,
};
use pumpkin_macros::packet;
#[packet(74)]
pub struct CBossEvent {
pub boss_entity_id: VarLong,
pub action: BossEventAction,
}
pub enum BossEventAction {
Add {
title: String,
health_percent: f32,
screen_darken: u16,
color: VarInt,
overlay: VarInt,
},
Remove,
UpdateHealth(f32),
UpdateTitle(String),
UpdateProperties {
screen_darken: u16,
color: VarInt,
overlay: VarInt,
},
}
impl PacketWrite for CBossEvent {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.boss_entity_id.write(writer)?;
match &self.action {
BossEventAction::Add {
title,
health_percent,
screen_darken,
color,
overlay,
} => {
VarInt(0).write(writer)?;
title.write(writer)?;
health_percent.write(writer)?;
screen_darken.write(writer)?;
color.write(writer)?;
overlay.write(writer)?;
}
BossEventAction::Remove => {
VarInt(2).write(writer)?;
}
BossEventAction::UpdateHealth(health) => {
VarInt(3).write(writer)?;
health.write(writer)?;
}
BossEventAction::UpdateTitle(title) => {
VarInt(4).write(writer)?;
title.write(writer)?;
}
BossEventAction::UpdateProperties {
screen_darken,
color,
overlay,
} => {
VarInt(5).write(writer)?;
screen_darken.write(writer)?;
color.write(writer)?;
overlay.write(writer)?;
}
}
Ok(())
}
}

View File

@ -1,4 +1,5 @@
pub mod available_commands; pub mod available_commands;
pub mod boss_event;
pub mod chunk_radius_update; pub mod chunk_radius_update;
pub mod container_open; pub mod container_open;
pub mod correct_player_move; pub mod correct_player_move;
@ -18,6 +19,7 @@ pub mod player_hotbar;
pub mod raknet; pub mod raknet;
pub mod resource_pack_stack; pub mod resource_pack_stack;
pub mod resource_packs_info; pub mod resource_packs_info;
pub mod scoreboard;
pub mod set_actor_data; pub mod set_actor_data;
pub mod set_actor_motion; pub mod set_actor_motion;
pub mod set_health; pub mod set_health;
@ -29,6 +31,7 @@ pub mod update_abilities;
pub mod update_attributes; pub mod update_attributes;
pub use available_commands::*; pub use available_commands::*;
pub use boss_event::*;
pub use chunk_radius_update::*; pub use chunk_radius_update::*;
pub use container_open::*; pub use container_open::*;
pub use correct_player_move::*; pub use correct_player_move::*;
@ -48,6 +51,7 @@ pub use player_hotbar::*;
pub use raknet::*; pub use raknet::*;
pub use resource_pack_stack::*; pub use resource_pack_stack::*;
pub use resource_packs_info::*; pub use resource_packs_info::*;
pub use scoreboard::*;
pub use set_actor_data::*; pub use set_actor_data::*;
pub use set_actor_motion::*; pub use set_actor_motion::*;
pub use set_health::*; pub use set_health::*;

View File

@ -0,0 +1,55 @@
use std::io::{Error, Write};
use crate::{codec::var_int::VarInt, serial::PacketWrite};
use pumpkin_macros::packet;
#[derive(PacketWrite)]
#[packet(107)]
pub struct CSetDisplayObjective {
pub display_slot: String,
pub objective_name: String,
pub display_name: String,
pub criteria_name: String,
pub sort_order: VarInt,
}
#[derive(PacketWrite)]
#[packet(108)]
pub struct CSetScore {
pub action: VarInt, // 0 = change, 1 = remove
pub entries: Vec<ScoreEntry>,
}
pub struct ScoreEntry {
pub scoreboard_id: i64,
pub objective_name: String,
pub score: VarInt,
pub entry_type: VarInt, // 1 = player, 2 = entity, 3 = fake player
pub entity_unique_id: i64,
pub custom_name: String,
}
impl PacketWrite for ScoreEntry {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.scoreboard_id.write(writer)?;
self.objective_name.write(writer)?;
self.score.write(writer)?;
self.entry_type.write(writer)?;
match self.entry_type.0 {
1 | 2 => {
self.entity_unique_id.write(writer)?;
}
3 => {
self.custom_name.write(writer)?;
}
_ => {}
}
Ok(())
}
}
#[derive(PacketWrite)]
#[packet(106)]
pub struct CRemoveObjective {
pub objective_name: String,
}

View File

@ -215,6 +215,7 @@ impl boss_bar::HostBossBar for PluginHostState {
&pbb.bossbar.uuid, &pbb.bossbar.uuid,
pbb.bossbar.color, pbb.bossbar.color,
pbb.bossbar.division, pbb.bossbar.division,
pbb.bossbar.flags,
) )
.await; .await;
} }
@ -243,6 +244,7 @@ impl boss_bar::HostBossBar for PluginHostState {
&pbb.bossbar.uuid, &pbb.bossbar.uuid,
pbb.bossbar.color, pbb.bossbar.color,
pbb.bossbar.division, pbb.bossbar.division,
pbb.bossbar.flags,
) )
.await; .await;
} }

View File

@ -47,7 +47,8 @@ impl scoreboard::HostScoreboard for PluginHostState {
.scoreboard .scoreboard
.lock() .lock()
.await .await
.add_objective(&world, objective); .add_objective(&world, objective)
.await;
Ok(()) Ok(())
} }
@ -61,7 +62,8 @@ impl scoreboard::HostScoreboard for PluginHostState {
.scoreboard .scoreboard
.lock() .lock()
.await .await
.remove_objective(&world, &name); .remove_objective(&world, &name)
.await;
Ok(()) Ok(())
} }
@ -145,7 +147,12 @@ impl scoreboard::HostScoreboard for PluginHostState {
None, None,
None, None,
); );
world.scoreboard.lock().await.update_score(&world, score); world
.scoreboard
.lock()
.await
.update_score(&world, score)
.await;
Ok(()) Ok(())
} }
@ -160,7 +167,8 @@ impl scoreboard::HostScoreboard for PluginHostState {
.scoreboard .scoreboard
.lock() .lock()
.await .await
.remove_score(&world, &entity_name, &objective_name); .remove_score(&world, &entity_name, &objective_name)
.await;
Ok(()) Ok(())
} }

View File

@ -1,5 +1,9 @@
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::net::ClientPlatform;
use bitflags::bitflags; use bitflags::bitflags;
use pumpkin_protocol::bedrock::client::boss_event::{BossEventAction, CBossEvent as BBossEvent};
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::codec::var_long::VarLong;
use pumpkin_protocol::java::client::play::{BosseventAction, CBossEvent}; use pumpkin_protocol::java::client::play::{BosseventAction, CBossEvent};
use pumpkin_util::text::TextComponent; use pumpkin_util::text::TextComponent;
use uuid::Uuid; use uuid::Uuid;
@ -15,6 +19,21 @@ pub enum BossbarColor {
White, White,
} }
impl BossbarColor {
#[must_use]
pub const fn to_bedrock(self) -> VarInt {
match self {
Self::Pink => VarInt(0),
Self::Blue => VarInt(1),
Self::Red => VarInt(2),
Self::Green => VarInt(3),
Self::Yellow => VarInt(4),
Self::Purple => VarInt(5),
Self::White => VarInt(6),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)] #[derive(Clone, Copy, PartialEq, Eq)]
pub enum BossbarDivisions { pub enum BossbarDivisions {
NoDivision, NoDivision,
@ -24,6 +43,19 @@ pub enum BossbarDivisions {
Notches20, Notches20,
} }
impl BossbarDivisions {
#[must_use]
pub const fn to_bedrock(self) -> VarInt {
match self {
Self::NoDivision => VarInt(0),
Self::Notches6 => VarInt(1),
Self::Notches10 => VarInt(2),
Self::Notches12 => VarInt(3),
Self::Notches20 => VarInt(4),
}
}
}
bitflags! { bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BossbarFlags: u8 { pub struct BossbarFlags: u8 {
@ -62,6 +94,8 @@ impl Bossbar {
/// Extra methods for [`Player`] to send and manage the bossbar. /// Extra methods for [`Player`] to send and manage the bossbar.
impl Player { impl Player {
pub async fn send_bossbar(&self, bossbar: &Bossbar) { pub async fn send_bossbar(&self, bossbar: &Bossbar) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::Add { let boss_action = BosseventAction::Add {
title: bossbar.title.clone(), title: bossbar.title.clone(),
health: bossbar.health, health: bossbar.health,
@ -71,28 +105,76 @@ impl Player {
}; };
let packet = CBossEvent::new(&bossbar.uuid, boss_action); let packet = CBossEvent::new(&bossbar.uuid, boss_action);
self.client.enqueue_packet(&packet).await; java.enqueue_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(bossbar.uuid.as_u128() as i64),
action: BossEventAction::Add {
title: bossbar.title.clone().get_text(),
health_percent: bossbar.health,
screen_darken: u16::from(bossbar.flags.contains(BossbarFlags::DARKEN_SKY)),
color: bossbar.color.to_bedrock(),
overlay: bossbar.division.to_bedrock(),
},
};
bedrock.send_game_packet(&packet).await;
}
}
} }
pub async fn remove_bossbar(&self, uuid: Uuid) { pub async fn remove_bossbar(&self, uuid: Uuid) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::Remove; let boss_action = BosseventAction::Remove;
let packet = CBossEvent::new(&uuid, boss_action); let packet = CBossEvent::new(&uuid, boss_action);
self.client.enqueue_packet(&packet).await; java.enqueue_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::Remove,
};
bedrock.send_game_packet(&packet).await;
}
}
} }
pub async fn update_bossbar_health(&self, uuid: &Uuid, health: f32) { pub async fn update_bossbar_health(&self, uuid: &Uuid, health: f32) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateHealth(health); let boss_action = BosseventAction::UpdateHealth(health);
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
self.client.enqueue_packet(&packet).await; java.enqueue_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateHealth(health),
};
bedrock.send_game_packet(&packet).await;
}
}
} }
pub async fn update_bossbar_title(&self, uuid: &Uuid, title: TextComponent) { pub async fn update_bossbar_title(&self, uuid: &Uuid, title: TextComponent) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateTile(title); let boss_action = BosseventAction::UpdateTile(title);
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
self.client.enqueue_packet(&packet).await; java.enqueue_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateTitle(title.get_text()),
};
bedrock.send_game_packet(&packet).await;
}
}
} }
pub async fn update_bossbar_style( pub async fn update_bossbar_style(
@ -100,20 +182,56 @@ impl Player {
uuid: &Uuid, uuid: &Uuid,
color: BossbarColor, color: BossbarColor,
dividers: BossbarDivisions, dividers: BossbarDivisions,
flags: BossbarFlags,
) { ) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateStyle { let boss_action = BosseventAction::UpdateStyle {
color: (color as u8).into(), color: (color as u8).into(),
dividers: (dividers as u8).into(), dividers: (dividers as u8).into(),
}; };
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
self.client.enqueue_packet(&packet).await; java.enqueue_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateProperties {
screen_darken: u16::from(flags.contains(BossbarFlags::DARKEN_SKY)),
color: color.to_bedrock(),
overlay: dividers.to_bedrock(),
},
};
bedrock.send_game_packet(&packet).await;
}
}
} }
pub async fn update_bossbar_flags(&self, uuid: &Uuid, flags: BossbarFlags) { pub async fn update_bossbar_flags(&self, uuid: &Uuid, flags: BossbarFlags) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateFlags(flags.bits()); let boss_action = BosseventAction::UpdateFlags(flags.bits());
let packet = CBossEvent::new(uuid, boss_action); let packet = CBossEvent::new(uuid, boss_action);
self.client.enqueue_packet(&packet).await; java.enqueue_packet(&packet).await;
}
ClientPlatform::Bedrock(bedrock) => {
// For Bedrock, flags are part of properties (screen darken)
// We don't have color and dividers here, so we might need more info or just skip if not critical
// Actually, properties includes screen_darken, color, and overlay.
// Since this method only has flags, we might need to store the current color/division on the player
// or retrieve them from the bossbar if we had a reference to it.
let packet = BBossEvent {
boss_entity_id: VarLong(uuid.as_u128() as i64),
action: BossEventAction::UpdateProperties {
screen_darken: u16::from(flags.contains(BossbarFlags::DARKEN_SKY)),
color: VarInt(0),
overlay: VarInt(0),
},
};
bedrock.send_game_packet(&packet).await;
}
}
} }
} }

View File

@ -294,6 +294,7 @@ impl CustomBossbars {
&bossbar.bossbar_data.uuid, &bossbar.bossbar_data.uuid,
bossbar.bossbar_data.color, bossbar.bossbar_data.color,
bossbar.bossbar_data.division, bossbar.bossbar_data.division,
bossbar.bossbar_data.flags,
) )
.await; .await;
} }
@ -333,6 +334,7 @@ impl CustomBossbars {
&bossbar.bossbar_data.uuid, &bossbar.bossbar_data.uuid,
bossbar.bossbar_data.color, bossbar.bossbar_data.color,
bossbar.bossbar_data.division, bossbar.bossbar_data.division,
bossbar.bossbar_data.flags,
) )
.await; .await;
} }

View File

@ -2,11 +2,15 @@ use std::collections::HashMap;
use pumpkin_data::scoreboard::ScoreboardDisplaySlot; use pumpkin_data::scoreboard::ScoreboardDisplaySlot;
use pumpkin_protocol::{ use pumpkin_protocol::{
NumberFormat, BClientPacket, ClientPacket, NumberFormat,
bedrock::client::scoreboard::{
CRemoveObjective as BRemoveObjective, CSetDisplayObjective as BSetDisplayObjective,
CSetScore as BSetScore, ScoreEntry as BScoreEntry,
},
codec::var_int::VarInt, codec::var_int::VarInt,
java::client::play::{ java::client::play::{
CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, RenderType, TeamMethod, CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, Mode, RenderType,
TeamParameters, TeamMethod, TeamParameters,
}, },
}; };
use pumpkin_util::text::{TextComponent, color::NamedColor}; use pumpkin_util::text::{TextComponent, color::NamedColor};
@ -22,7 +26,15 @@ pub struct Scoreboard {
} }
impl Scoreboard { impl Scoreboard {
pub fn add_objective(&mut self, world: &World, objective: ScoreboardObjective<'static>) { 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) { if self.objectives.contains_key(objective.name) {
warn!( warn!(
"Tried to create an objective which already exists: {}", "Tried to create an objective which already exists: {}",
@ -30,22 +42,36 @@ impl Scoreboard {
); );
return; return;
} }
world.broadcast_packet_all(&CUpdateObjectives::new(
let je_update = CUpdateObjectives::new(
objective.name.to_string(), objective.name.to_string(),
pumpkin_protocol::java::client::play::Mode::Add, Mode::Add,
objective.display_name.clone(), objective.display_name.clone(),
objective.render_type, objective.render_type,
objective.number_format.clone(), objective.number_format.clone(),
)); );
world.broadcast_packet_all(&CDisplayObjective::new(
ScoreboardDisplaySlot::Sidebar, let be_update = BSetDisplayObjective {
objective.name.to_string(), 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 self.objectives
.insert(objective.name.to_string(), objective); .insert(objective.name.to_string(), objective);
} }
pub fn remove_objective(&mut self, world: &World, name: &str) { pub async fn remove_objective(&mut self, world: &World, name: &str) {
if !self.objectives.contains_key(name) { if !self.objectives.contains_key(name) {
warn!( warn!(
"Tried to remove an objective which does not exist: {}", "Tried to remove an objective which does not exist: {}",
@ -53,18 +79,26 @@ impl Scoreboard {
); );
return; return;
} }
world.broadcast_packet_all(&CUpdateObjectives::new(
let je_packet = CUpdateObjectives::new(
name.to_string(), name.to_string(),
pumpkin_protocol::java::client::play::Mode::Remove, Mode::Remove,
TextComponent::empty(), TextComponent::empty(),
pumpkin_protocol::java::client::play::RenderType::Integer, RenderType::Integer,
None, None,
)); );
let be_packet = BRemoveObjective {
objective_name: name.to_string(),
};
Self::broadcast_editioned(world, &je_packet, &be_packet).await;
self.objectives.remove(name); self.objectives.remove(name);
self.scores.remove(name); self.scores.remove(name);
} }
pub fn update_score(&mut self, world: &World, score: ScoreboardScore<'static>) { pub async fn update_score(&mut self, world: &World, score: ScoreboardScore<'static>) {
if !self.objectives.contains_key(score.objective_name) { if !self.objectives.contains_key(score.objective_name) {
warn!( warn!(
"Tried to place a score into an objective which does not exist: {}", "Tried to place a score into an objective which does not exist: {}",
@ -72,13 +106,28 @@ impl Scoreboard {
); );
return; return;
} }
world.broadcast_packet_all(&CUpdateScore::new(
let je_packet = CUpdateScore::new(
score.entity_name.to_string(), score.entity_name.to_string(),
score.objective_name.to_string(), score.objective_name.to_string(),
score.value, score.value,
score.display_name.clone(), score.display_name.clone(),
score.number_format.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 self.scores
.entry(score.objective_name.to_string()) .entry(score.objective_name.to_string())
@ -86,11 +135,23 @@ impl Scoreboard {
.insert(score.entity_name.to_string(), score); .insert(score.entity_name.to_string(), score);
} }
pub fn remove_score(&mut self, world: &World, entity_name: &str, objective_name: &str) { pub async fn remove_score(&mut self, world: &World, entity_name: &str, objective_name: &str) {
world.broadcast_packet_all(&CUpdateScore::new_remove( let je_packet =
entity_name.to_string(), CUpdateScore::new_remove(entity_name.to_string(), objective_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) { if let Some(objective_scores) = self.scores.get_mut(objective_name) {
objective_scores.remove(entity_name); objective_scores.remove(entity_name);