diff --git a/pumpkin-protocol/src/bedrock/client/boss_event.rs b/pumpkin-protocol/src/bedrock/client/boss_event.rs new file mode 100644 index 000000000..99e001dbd --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/boss_event.rs @@ -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(&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(()) + } +} diff --git a/pumpkin-protocol/src/bedrock/client/mod.rs b/pumpkin-protocol/src/bedrock/client/mod.rs index 6090fde18..1bc245c39 100644 --- a/pumpkin-protocol/src/bedrock/client/mod.rs +++ b/pumpkin-protocol/src/bedrock/client/mod.rs @@ -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::*; diff --git a/pumpkin-protocol/src/bedrock/client/scoreboard.rs b/pumpkin-protocol/src/bedrock/client/scoreboard.rs new file mode 100644 index 000000000..d58cbc4dc --- /dev/null +++ b/pumpkin-protocol/src/bedrock/client/scoreboard.rs @@ -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, +} + +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(&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, +} diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/boss_bar.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/boss_bar.rs index 7133ab9f3..5de22c6cb 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/boss_bar.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/boss_bar.rs @@ -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; } diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs index 82b34e661..5a0d42bbe 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs @@ -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(()) } diff --git a/pumpkin/src/world/bossbar.rs b/pumpkin/src/world/bossbar.rs index 442a44fa6..57944195f 100644 --- a/pumpkin/src/world/bossbar.rs +++ b/pumpkin/src/world/bossbar.rs @@ -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,37 +94,87 @@ impl Bossbar { /// Extra methods for [`Player`] to send and manage the bossbar. impl Player { pub async fn send_bossbar(&self, bossbar: &Bossbar) { - let boss_action = BosseventAction::Add { - title: bossbar.title.clone(), - health: bossbar.health, - color: (bossbar.color as u8).into(), - division: (bossbar.division as u8).into(), - flags: bossbar.flags.bits(), - }; + match &self.client { + ClientPlatform::Java(java) => { + let boss_action = BosseventAction::Add { + title: bossbar.title.clone(), + health: bossbar.health, + color: (bossbar.color as u8).into(), + division: (bossbar.division as u8).into(), + flags: bossbar.flags.bits(), + }; - let packet = CBossEvent::new(&bossbar.uuid, boss_action); - self.client.enqueue_packet(&packet).await; + let packet = CBossEvent::new(&bossbar.uuid, boss_action); + 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) { - let boss_action = BosseventAction::Remove; + match &self.client { + ClientPlatform::Java(java) => { + let boss_action = BosseventAction::Remove; - let packet = CBossEvent::new(&uuid, boss_action); - self.client.enqueue_packet(&packet).await; + let packet = CBossEvent::new(&uuid, boss_action); + 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) { - let boss_action = BosseventAction::UpdateHealth(health); + 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; + let packet = CBossEvent::new(uuid, boss_action); + 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) { - let boss_action = BosseventAction::UpdateTile(title); + 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; + let packet = CBossEvent::new(uuid, boss_action); + 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, ) { - let boss_action = BosseventAction::UpdateStyle { - color: (color as u8).into(), - dividers: (dividers as u8).into(), - }; + 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; + let packet = CBossEvent::new(uuid, boss_action); + 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) { - let boss_action = BosseventAction::UpdateFlags(flags.bits()); + 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; + let packet = CBossEvent::new(uuid, boss_action); + 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; + } + } } } diff --git a/pumpkin/src/world/custom_bossbar.rs b/pumpkin/src/world/custom_bossbar.rs index 2394088c1..30191fe26 100644 --- a/pumpkin/src/world/custom_bossbar.rs +++ b/pumpkin/src/world/custom_bossbar.rs @@ -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; } diff --git a/pumpkin/src/world/scoreboard.rs b/pumpkin/src/world/scoreboard.rs index 682ee216c..8287b924f 100644 --- a/pumpkin/src/world/scoreboard.rs +++ b/pumpkin/src/world/scoreboard.rs @@ -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( + 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);