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 boss_event;
pub mod chunk_radius_update;
pub mod container_open;
pub mod correct_player_move;
@ -18,6 +19,7 @@ pub mod player_hotbar;
pub mod raknet;
pub mod resource_pack_stack;
pub mod resource_packs_info;
pub mod scoreboard;
pub mod set_actor_data;
pub mod set_actor_motion;
pub mod set_health;
@ -29,6 +31,7 @@ pub mod update_abilities;
pub mod update_attributes;
pub use available_commands::*;
pub use boss_event::*;
pub use chunk_radius_update::*;
pub use container_open::*;
pub use correct_player_move::*;
@ -48,6 +51,7 @@ pub use player_hotbar::*;
pub use raknet::*;
pub use resource_pack_stack::*;
pub use resource_packs_info::*;
pub use scoreboard::*;
pub use set_actor_data::*;
pub use set_actor_motion::*;
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.color,
pbb.bossbar.division,
pbb.bossbar.flags,
)
.await;
}
@ -243,6 +244,7 @@ impl boss_bar::HostBossBar for PluginHostState {
&pbb.bossbar.uuid,
pbb.bossbar.color,
pbb.bossbar.division,
pbb.bossbar.flags,
)
.await;
}

View File

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

View File

@ -1,5 +1,9 @@
use crate::entity::player::Player;
use crate::net::ClientPlatform;
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_util::text::TextComponent;
use uuid::Uuid;
@ -15,6 +19,21 @@ pub enum BossbarColor {
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)]
pub enum BossbarDivisions {
NoDivision,
@ -24,6 +43,19 @@ pub enum BossbarDivisions {
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! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BossbarFlags: u8 {
@ -62,6 +94,8 @@ impl Bossbar {
/// Extra methods for [`Player`] to send and manage the bossbar.
impl Player {
pub async fn send_bossbar(&self, bossbar: &Bossbar) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::Add {
title: bossbar.title.clone(),
health: bossbar.health,
@ -71,28 +105,76 @@ impl Player {
};
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) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::Remove;
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) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateHealth(health);
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) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateTile(title);
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(
@ -100,20 +182,56 @@ impl Player {
uuid: &Uuid,
color: BossbarColor,
dividers: BossbarDivisions,
flags: BossbarFlags,
) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateStyle {
color: (color as u8).into(),
dividers: (dividers as u8).into(),
};
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) {
match &self.client {
ClientPlatform::Java(java) => {
let boss_action = BosseventAction::UpdateFlags(flags.bits());
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.color,
bossbar.bossbar_data.division,
bossbar.bossbar_data.flags,
)
.await;
}
@ -333,6 +334,7 @@ impl CustomBossbars {
&bossbar.bossbar_data.uuid,
bossbar.bossbar_data.color,
bossbar.bossbar_data.division,
bossbar.bossbar_data.flags,
)
.await;
}

View File

@ -2,11 +2,15 @@ use std::collections::HashMap;
use pumpkin_data::scoreboard::ScoreboardDisplaySlot;
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,
java::client::play::{
CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, RenderType, TeamMethod,
TeamParameters,
CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, Mode, RenderType,
TeamMethod, TeamParameters,
},
};
use pumpkin_util::text::{TextComponent, color::NamedColor};
@ -22,7 +26,15 @@ pub struct 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) {
warn!(
"Tried to create an objective which already exists: {}",
@ -30,22 +42,36 @@ impl Scoreboard {
);
return;
}
world.broadcast_packet_all(&CUpdateObjectives::new(
let je_update = CUpdateObjectives::new(
objective.name.to_string(),
pumpkin_protocol::java::client::play::Mode::Add,
Mode::Add,
objective.display_name.clone(),
objective.render_type,
objective.number_format.clone(),
));
world.broadcast_packet_all(&CDisplayObjective::new(
ScoreboardDisplaySlot::Sidebar,
objective.name.to_string(),
));
);
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 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) {
warn!(
"Tried to remove an objective which does not exist: {}",
@ -53,18 +79,26 @@ impl Scoreboard {
);
return;
}
world.broadcast_packet_all(&CUpdateObjectives::new(
let je_packet = CUpdateObjectives::new(
name.to_string(),
pumpkin_protocol::java::client::play::Mode::Remove,
Mode::Remove,
TextComponent::empty(),
pumpkin_protocol::java::client::play::RenderType::Integer,
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 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) {
warn!(
"Tried to place a score into an objective which does not exist: {}",
@ -72,13 +106,28 @@ impl Scoreboard {
);
return;
}
world.broadcast_packet_all(&CUpdateScore::new(
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())
@ -86,11 +135,23 @@ impl Scoreboard {
.insert(score.entity_name.to_string(), score);
}
pub fn remove_score(&mut self, world: &World, entity_name: &str, objective_name: &str) {
world.broadcast_packet_all(&CUpdateScore::new_remove(
entity_name.to_string(),
objective_name.to_string(),
));
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);