diff --git a/crates/pumpkin-data/src/generated/scoreboard_slot.rs b/crates/pumpkin-data/src/generated/scoreboard_slot.rs index 7a1222eae..d934b5cad 100644 --- a/crates/pumpkin-data/src/generated/scoreboard_slot.rs +++ b/crates/pumpkin-data/src/generated/scoreboard_slot.rs @@ -1,4 +1,4 @@ -/* This file is generated. Do not edit manually. */ +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ScoreboardDisplaySlot { List, Sidebar, diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index 160251fb0..8e006930b 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit 160251fb0b15ad529bf7c4939140bb5fa0c07035 +Subproject commit 8e006930be6fa6b5e60201c92d1c0393448ca606 diff --git a/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs b/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs index 3b8721ce0..c497bee31 100644 --- a/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs +++ b/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs @@ -75,7 +75,7 @@ pub enum Mode { Update, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum RenderType { Integer, Hearts, diff --git a/crates/pumpkin-protocol/src/lib.rs b/crates/pumpkin-protocol/src/lib.rs index 237116486..90a633317 100644 --- a/crates/pumpkin-protocol/src/lib.rs +++ b/crates/pumpkin-protocol/src/lib.rs @@ -385,7 +385,7 @@ impl KnownPack<'_> { } } -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum NumberFormat { /// Show nothing. Blank, diff --git a/crates/pumpkin/src/command/commands/scoreboard.rs b/crates/pumpkin/src/command/commands/scoreboard.rs index de5b91ae9..d51a678eb 100644 --- a/crates/pumpkin/src/command/commands/scoreboard.rs +++ b/crates/pumpkin/src/command/commands/scoreboard.rs @@ -60,15 +60,12 @@ impl CommandExecutor for ObjectivesAddExecutor { return Err(DUPLICATE_OBJECTIVE_ERROR.create_without_context()); } - let obj_name_static = Box::leak(objective_name.to_string().into_boxed_str()); - let criterion_static = Box::leak(criterion.to_string().into_boxed_str()); - let new_objective = ScoreboardObjective::new( - obj_name_static, + objective_name, display_name.clone(), RenderType::Integer, None, - criterion_static, + criterion, ); scoreboard.add_objective(world, new_objective).await; @@ -128,8 +125,8 @@ impl CommandExecutor for PlayersEnableExecutor { let number_format = current_score.and_then(|s| s.number_format.clone()); let updated_score = ScoreboardScore { - entity_name: Box::leak(player_name.clone().into_boxed_str()), - objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + entity_name: player_name.clone(), + objective_name: objective_name.to_string(), value: VarInt(value), display_name, number_format, diff --git a/crates/pumpkin/src/command/commands/team.rs b/crates/pumpkin/src/command/commands/team.rs index 313b4accd..51499f4d5 100644 --- a/crates/pumpkin/src/command/commands/team.rs +++ b/crates/pumpkin/src/command/commands/team.rs @@ -122,7 +122,7 @@ impl CommandExecutor for TeamAddExecutor { players: vec![], }; - scoreboard.add_team(world, new_team); + scoreboard.add_team(world, new_team).await; context .source @@ -158,7 +158,7 @@ impl CommandExecutor for TeamRemoveExecutor { let team_display_name = team.display_name.clone(); - scoreboard.remove_team(world, team_name); + scoreboard.remove_team(world, team_name).await; context .source @@ -200,7 +200,9 @@ impl CommandExecutor for TeamEmptyExecutor { } for player in &players_to_remove { - scoreboard.remove_player_from_team(world, team_name, player); + scoreboard + .remove_player_from_team(world, team_name, player) + .await; } context @@ -262,7 +264,9 @@ impl CommandExecutor for TeamJoinExecutor { let count = entity_names.len(); for name in &entity_names { - scoreboard.add_player_to_team(world, team_name, name.clone()); + scoreboard + .add_player_to_team(world, team_name, name.clone()) + .await; } let msg = if count == 1 { @@ -327,7 +331,9 @@ impl CommandExecutor for TeamLeaveExecutor { } } if let Some(team_name) = found_team { - scoreboard.remove_player_from_team(world, &team_name, name); + scoreboard + .remove_player_from_team(world, &team_name, name) + .await; removed_count += 1; } } @@ -474,7 +480,7 @@ impl CommandExecutor for TeamModifyColorResetExecutor { team.color = NamedColor::White; let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source @@ -519,7 +525,7 @@ impl CommandExecutor for TeamModifyColorExecutor { team.color = new_color; let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source @@ -565,7 +571,7 @@ impl CommandExecutor for TeamModifyDisplayNameExecutor { team.display_name = new_display_name.clone(); let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source @@ -607,7 +613,7 @@ impl CommandExecutor for TeamModifyPrefixExecutor { let new_prefix = TextComponent::text(new_prefix_str.to_string()); team.player_prefix = new_prefix.clone(); let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source @@ -649,7 +655,7 @@ impl CommandExecutor for TeamModifySuffixExecutor { let new_suffix = TextComponent::text(new_suffix_str.to_string()); team.player_suffix = new_suffix.clone(); let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source @@ -703,7 +709,7 @@ impl CommandExecutor for TeamModifyFriendlyFireExecutor { } let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; let key = if value { translation::java::COMMANDS_TEAM_OPTION_FRIENDLYFIRE_ENABLED @@ -761,7 +767,7 @@ impl CommandExecutor for TeamModifySeeFriendlyInvisiblesExecutor { } let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; let key = if value { translation::java::COMMANDS_TEAM_OPTION_SEEFRIENDLYINVISIBLES_ENABLED @@ -829,7 +835,7 @@ impl CommandExecutor for TeamModifyNametagVisibilityExecutor { }; let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source @@ -931,7 +937,7 @@ impl CommandExecutor for TeamModifyCollisionRuleExecutor { }; let team_display_name = team.display_name.clone(); - scoreboard.update_team(world, team); + scoreboard.update_team(world, team).await; context .source diff --git a/crates/pumpkin/src/command/commands/trigger.rs b/crates/pumpkin/src/command/commands/trigger.rs index 34bf84876..e2e008918 100644 --- a/crates/pumpkin/src/command/commands/trigger.rs +++ b/crates/pumpkin/src/command/commands/trigger.rs @@ -69,8 +69,8 @@ impl CommandExecutor for SimpleTriggerExecutor { let new_value = current_value + 1; let updated_score = ScoreboardScore { - entity_name: Box::leak(player_name.clone().into_boxed_str()), - objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + entity_name: player_name.clone(), + objective_name: objective_name.to_string(), value: VarInt(new_value), display_name: None, number_format: None, @@ -139,8 +139,8 @@ impl CommandExecutor for AddTriggerExecutor { let new_value = current_value + value; let updated_score = ScoreboardScore { - entity_name: Box::leak(player_name.clone().into_boxed_str()), - objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + entity_name: player_name.clone(), + objective_name: objective_name.to_string(), value: VarInt(new_value), display_name: None, number_format: None, @@ -204,8 +204,8 @@ impl CommandExecutor for SetTriggerExecutor { } let updated_score = ScoreboardScore { - entity_name: Box::leak(player_name.clone().into_boxed_str()), - objective_name: Box::leak(objective_name.to_string().into_boxed_str()), + entity_name: player_name.clone(), + objective_name: objective_name.to_string(), value: VarInt(value), display_name: None, number_format: None, diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 5e0f1f55a..347768a22 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -10,6 +10,8 @@ use std::sync::atomic::{AtomicBool, AtomicI8, AtomicI32, AtomicU8, AtomicU32, Or use std::sync::{Arc, Weak}; use std::time::{Duration, Instant}; +use crate::world::scoreboard::{BedrockScoreboard, Scoreboard}; +use advancement::PlayerAdvancement; use arc_swap::ArcSwap; use crossbeam::atomic::AtomicCell; use crossbeam::channel::Receiver; @@ -37,7 +39,162 @@ use tokio::task::JoinHandle; use tracing::{debug, warn}; use uuid::Uuid; -use advancement::PlayerAdvancement; +#[derive(Clone, Debug)] +pub enum CustomScoreboard { + Java(Scoreboard), + Bedrock(BedrockScoreboard), +} + +impl From for CustomScoreboard { + fn from(sb: Scoreboard) -> Self { + Self::Java(sb) + } +} + +impl From for CustomScoreboard { + fn from(sb: BedrockScoreboard) -> Self { + Self::Bedrock(sb) + } +} + +#[derive(Copy, Clone)] +pub struct JavaPlayer<'a>(pub &'a Player); + +impl JavaPlayer<'_> { + pub async fn send_packet(&self, packet: &C) { + if let ClientPlatform::Java(client) = self.0.client.as_ref() { + client.enqueue_packet(packet).await; + } + } + + pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) { + if let ClientPlatform::Java(java) = self.0.client.as_ref() { + java.enqueue_packet(&CCustomPayload::new(channel, data)) + .await; + } + } + + pub async fn send_stats(&self) { + self.0.send_stats().await; + } + + pub async fn set_scoreboard(&self, scoreboard: Option) { + *self.0.custom_scoreboard.lock().await = scoreboard.map(CustomScoreboard::Java); + self.0.send_scoreboard().await; + } + + pub async fn reset_scoreboard(&self) { + self.set_scoreboard(None).await; + } + + pub async fn get_scoreboard(&self) -> Option { + let guard = self.0.custom_scoreboard.lock().await; + if let Some(CustomScoreboard::Java(sb)) = guard.as_ref() { + Some(sb.clone()) + } else { + None + } + } +} + +#[derive(Copy, Clone)] +pub struct BedrockPlayer<'a>(pub &'a Player); + +impl BedrockPlayer<'_> { + pub async fn send_packet(&self, packet: &P) { + if let ClientPlatform::Bedrock(client) = self.0.client.as_ref() { + client.send_game_packet(packet).await; + } + } + + pub async fn set_scoreboard(&self, scoreboard: Option) { + *self.0.custom_scoreboard.lock().await = scoreboard.map(CustomScoreboard::Bedrock); + self.0.send_scoreboard().await; + } + + pub async fn reset_scoreboard(&self) { + self.set_scoreboard(None).await; + } + + pub async fn get_scoreboard(&self) -> Option { + let guard = self.0.custom_scoreboard.lock().await; + if let Some(CustomScoreboard::Bedrock(sb)) = guard.as_ref() { + Some(sb.clone()) + } else { + None + } + } + + #[must_use] + pub fn client_data(&self) -> Option> { + if let ClientPlatform::Bedrock(client) = self.0.client.as_ref() { + let data = client.client_data.load(); + (**data).clone() + } else { + None + } + } + + #[must_use] + pub fn device_os(&self) -> Option { + self.client_data().map(|d| d.device_os) + } + + #[must_use] + pub fn device_id(&self) -> Option { + self.client_data().map(|d| d.device_id.clone()) + } + + #[must_use] + pub fn device_model(&self) -> Option { + self.client_data().map(|d| d.device_model.clone()) + } + + #[must_use] + pub fn game_version(&self) -> Option { + self.client_data().map(|d| d.game_version.clone()) + } + + #[must_use] + pub fn language_code(&self) -> Option { + self.client_data().map(|d| d.language_code.clone()) + } + + #[must_use] + pub fn current_input_mode(&self) -> Option { + self.client_data().map(|d| d.current_input_mode) + } + + #[must_use] + pub fn default_input_mode(&self) -> Option { + self.client_data().map(|d| d.default_input_mode) + } + + #[must_use] + pub fn ui_profile(&self) -> Option { + self.client_data().map(|d| d.ui_profile) + } + + #[must_use] + pub fn gui_scale(&self) -> Option { + self.client_data().map(|d| d.gui_scale) + } + + #[must_use] + pub fn max_view_distance(&self) -> Option { + self.client_data().map(|d| d.max_view_distance) + } + + #[must_use] + pub fn memory_tier(&self) -> Option { + self.client_data().map(|d| d.memory_tier) + } + + #[must_use] + pub fn graphics_mode(&self) -> Option { + self.client_data().map(|d| d.graphics_mode) + } +} use pumpkin_data::attributes::Attributes; use pumpkin_data::block_properties::{BlockProperties, HorizontalFacing}; use pumpkin_data::damage::DamageType; @@ -594,6 +751,7 @@ pub struct Player { pub tab_list_listed: AtomicBool, pub per_player_time: AtomicCell>, pub per_player_weather: AtomicCell>, + pub custom_scoreboard: Mutex>, pub compass_target: AtomicCell>, pub respawn_location: AtomicCell>, pub hidden_players: Mutex>, @@ -853,6 +1011,7 @@ impl Player { tab_list_listed: AtomicBool::new(true), per_player_time: AtomicCell::new(None), per_player_weather: AtomicCell::new(None), + custom_scoreboard: Mutex::new(None), compass_target: AtomicCell::new(None), respawn_location: AtomicCell::new(None), hidden_players: Mutex::new(std::collections::HashSet::new()), @@ -2760,14 +2919,16 @@ impl Player { .await; } - pub async fn set_player_time(&self, world: &World, time: u64, relative: bool) { + pub async fn set_player_time(&self, time: u64, relative: bool) { + let world = self.world(); self.per_player_time.store(Some((time, relative))); - self.send_time(world).await; + self.send_time(&world).await; } - pub async fn reset_player_time(&self, world: &World) { + pub async fn reset_player_time(&self) { + let world = self.world(); self.per_player_time.store(None); - self.send_time(world).await; + self.send_time(&world).await; } pub fn get_player_time(&self) -> Option { @@ -2790,6 +2951,66 @@ impl Player { self.per_player_weather.load() } + pub async fn send_editioned< + J: pumpkin_protocol::ClientPacket + Sync, + B: pumpkin_protocol::BClientPacket + Sync, + >( + &self, + je_packet: &J, + be_packet: &B, + ) { + match self.client.as_ref() { + ClientPlatform::Java(client) => client.enqueue_packet(je_packet).await, + ClientPlatform::Bedrock(client) => client.enqueue_packet(be_packet).await, + } + } + + pub async fn send_client_packet(&self, packet: &C) { + if let ClientPlatform::Java(client) = self.client.as_ref() { + client.enqueue_packet(packet).await; + } + } + + #[must_use] + pub fn as_java(&self) -> Option> { + matches!(self.client.as_ref(), ClientPlatform::Java(_)).then(|| JavaPlayer(self)) + } + + #[must_use] + pub fn as_bedrock(&self) -> Option> { + matches!(self.client.as_ref(), ClientPlatform::Bedrock(_)).then(|| BedrockPlayer(self)) + } + + pub async fn reset_scoreboard(&self) { + *self.custom_scoreboard.lock().await = None; + self.send_scoreboard().await; + } + + pub async fn send_scoreboard(&self) { + let guard = self.custom_scoreboard.lock().await; + match guard.as_ref() { + Some(CustomScoreboard::Java(custom)) + if matches!(self.client.as_ref(), ClientPlatform::Java(_)) => + { + custom.send_to_player(self).await; + } + Some(CustomScoreboard::Bedrock(custom)) + if matches!(self.client.as_ref(), ClientPlatform::Bedrock(_)) => + { + custom.send_to_player(self).await; + } + _ => { + drop(guard); + self.world() + .scoreboard + .lock() + .await + .send_to_player(self) + .await; + } + } + } + pub async fn set_compass_target(&self, pos: pumpkin_util::math::position::BlockPos) { use pumpkin_protocol::java::client::play::CPlayerSpawnPosition; self.compass_target.store(Some(pos)); @@ -2822,6 +3043,85 @@ impl Player { !self.hidden_players.lock().await.contains(other_id) } + pub async fn can_see_player(&self, other_id: &uuid::Uuid) -> bool { + self.can_see(other_id).await + } + + // --- Experience & Leveling API --- + pub async fn add_experience(self: &Arc, points: i32) { + self.add_experience_points(points).await; + } + + pub async fn add_levels(&self, levels: i32) { + self.add_experience_levels(levels).await; + } + + pub fn get_experience_level(&self) -> i32 { + self.experience_level.load(Ordering::Relaxed) + } + + pub fn get_experience_progress(&self) -> f32 { + self.experience_progress.load() + } + + pub fn get_total_experience(&self) -> i32 { + self.experience_points.load(Ordering::Relaxed) + } + + pub async fn set_experience_progress(&self, progress: f32) { + let level = self.get_experience_level(); + let max_points = experience::points_in_level(level); + let points = (progress.clamp(0.0, 1.0) * max_points as f32) as i32; + self.set_experience(level, progress, points).await; + } + + pub async fn set_total_experience(&self, points: i32) { + self.set_experience_points(points).await; + } + + // --- Item Cooldown System --- + pub async fn set_item_cooldown(&self, item_id: &str, ticks: i32) { + self.start_cooldown(item_id.to_string(), ticks).await; + } + + pub async fn get_item_cooldown(&self, item_id: &str) -> Option { + let cooldowns = self.item_cooldowns.lock().await; + if let Some(cooldown) = cooldowns.get(item_id) { + let current_tick = self.tick_counter.load(Ordering::Relaxed); + let elapsed = current_tick - cooldown.start_tick; + if elapsed < cooldown.duration { + return Some(cooldown.duration - elapsed); + } + } + None + } + + pub async fn has_item_cooldown(&self, item_id: &str) -> bool { + self.is_on_cooldown(item_id).await + } + + // --- Tab List & Display Names --- + pub fn set_tab_list_ping(&self, latency_ms: i32) { + self.set_tab_list_latency(latency_ms); + } + + // --- Hunger & Saturation Aliases --- + pub fn get_food_saturation(&self) -> f32 { + self.get_saturation() + } + + pub async fn set_food_saturation(&self, saturation: f32) { + self.set_saturation(saturation).await; + } + + pub fn get_food_exhaustion(&self) -> f32 { + self.get_exhaustion() + } + + pub async fn set_food_exhaustion(&self, exhaustion: f32) { + self.set_exhaustion(exhaustion).await; + } + pub async fn get_target_block( &self, world: &Arc, @@ -3653,14 +3953,6 @@ impl Player { .await; } - /// Sends a custom payload packet to this player (Java edition only). - pub async fn send_custom_payload(&self, channel: &str, data: &[u8]) { - if let ClientPlatform::Java(java) = self.client.as_ref() { - java.enqueue_packet(&CCustomPayload::new(channel, data)) - .await; - } - } - pub async fn drop_item(&self, item_stack: ItemStack) { self.increment_stat( statistics::StatisticCategory::Dropped, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs index efb236a6c..0b67fbf26 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/state.rs @@ -45,7 +45,15 @@ pub type EntityResource = WasmResource>; pub type WorldResource = WasmResource>; pub type ChunkResource = WasmResource<(Arc, Weak)>; pub type WorldBorderResource = WasmResource>; -pub type ScoreboardResource = WasmResource>; + +#[derive(Clone)] +pub enum ScoreboardProvider { + World(Arc), + Player(Arc), +} + +pub type ScoreboardResource = WasmResource; +pub type BedrockScoreboardResource = WasmResource>; pub type GuiResource = WasmResource>>; pub type BossBarResource = WasmResource< Arc>, @@ -176,12 +184,22 @@ impl PluginHostState { pub fn add_scoreboard( &mut self, - provider: Arc, + provider: ScoreboardProvider, ) -> wasmtime::Result> { let resource = self.resource_table.push(ScoreboardResource { provider })?; Ok(wasmtime::component::Resource::new_own(resource.rep())) } + pub fn add_bedrock_scoreboard( + &mut self, + provider: Arc, + ) -> wasmtime::Result> { + let resource = self + .resource_table + .push(BedrockScoreboardResource { provider })?; + Ok(wasmtime::component::Resource::new_own(resource.rep())) + } + pub fn add_gui( &mut self, provider: Arc>, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs index 66b012b01..53797150c 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/player.rs @@ -1059,21 +1059,13 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { relative: bool, ) -> wasmtime::Result<()> { let player = player_from_resource(self, &player)?; - player - .set_player_time( - &player.living_entity.entity.world.load_full(), - time, - relative, - ) - .await; + player.set_player_time(time, relative).await; Ok(()) } async fn reset_player_time(&mut self, player: Resource) -> wasmtime::Result<()> { let player = player_from_resource(self, &player)?; - player - .reset_player_time(&player.living_entity.entity.world.load_full()) - .await; + player.reset_player_time().await; Ok(()) } @@ -1222,6 +1214,55 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState { Ok(player.can_see(&other.gameprofile.id).await) } + async fn can_see_player( + &mut self, + player: Resource, + other: Resource, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + let other = player_from_resource(self, &other)?; + Ok(player.can_see(&other.gameprofile.id).await) + } + + async fn set_tab_list_ping( + &mut self, + player: Resource, + latency_ms: i32, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.set_tab_list_ping(latency_ms); + Ok(()) + } + + async fn set_item_cooldown( + &mut self, + player: Resource, + item_id: String, + ticks: i32, + ) -> wasmtime::Result<()> { + let player = player_from_resource(self, &player)?; + player.set_item_cooldown(&item_id, ticks).await; + Ok(()) + } + + async fn get_item_cooldown( + &mut self, + player: Resource, + item_id: String, + ) -> wasmtime::Result> { + let player = player_from_resource(self, &player)?; + Ok(player.get_item_cooldown(&item_id).await) + } + + async fn has_item_cooldown( + &mut self, + player: Resource, + item_id: String, + ) -> wasmtime::Result { + let player = player_from_resource(self, &player)?; + Ok(player.has_item_cooldown(&item_id).await) + } + async fn get_target_block( &mut self, player: Resource, @@ -2198,6 +2239,39 @@ impl pumpkin::plugin::player::HostJavaPlayer for PluginHostState { Ok(()) } + async fn get_scoreboard( + &mut self, + player_res: Resource, + ) -> wasmtime::Result> { + let player = self + .resource_table + .get::( + &Resource::new_own(player_res.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid java-player resource handle"))? + .provider + .clone(); + self.add_scoreboard( + crate::plugin::loader::wasm::wasm_host::state::ScoreboardProvider::Player(player), + ) + } + + async fn reset_scoreboard( + &mut self, + player_res: Resource, + ) -> wasmtime::Result<()> { + let player = self + .resource_table + .get::( + &Resource::new_own(player_res.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid java-player resource handle"))? + .provider + .clone(); + player.reset_scoreboard().await; + Ok(()) + } + async fn drop( &mut self, rep: Resource, @@ -2543,6 +2617,37 @@ impl pumpkin::plugin::player::HostBedrockPlayer for PluginHostState { } } + async fn get_scoreboard( + &mut self, + player_res: Resource, + ) -> wasmtime::Result> { + let player = self + .resource_table + .get::( + &Resource::new_own(player_res.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid bedrock-player resource handle"))? + .provider + .clone(); + self.add_bedrock_scoreboard(player) + } + + async fn reset_scoreboard( + &mut self, + player_res: Resource, + ) -> wasmtime::Result<()> { + let player = self + .resource_table + .get::( + &Resource::new_own(player_res.rep()), + ) + .map_err(|_| wasmtime::Error::msg("invalid bedrock-player resource handle"))? + .provider + .clone(); + player.reset_scoreboard().await; + Ok(()) + } + async fn drop( &mut self, rep: Resource, diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs index 0f1eaa243..604dcb5c4 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/scoreboard.rs @@ -1,11 +1,12 @@ use wasmtime::component::Resource; use crate::plugin::loader::wasm::wasm_host::{ - state::{PluginHostState, ScoreboardResource}, + state::{BedrockScoreboardResource, PluginHostState, ScoreboardProvider, ScoreboardResource}, wit::v0_1::pumpkin::{ self, plugin::scoreboard::{ - self, CollisionRule, DisplaySlot, NametagVisibility, RenderType, TeamSettings, + self, CollisionRule, DisplaySlot, HostBedrockScoreboard, NametagVisibility, RenderType, + TeamSettings, }, }, }; @@ -21,6 +22,15 @@ impl PluginHostState { .get::(&Resource::new_own(res.rep())) .map_err(wasmtime::Error::from) } + + fn get_bedrock_scoreboard_res( + &self, + res: &Resource, + ) -> wasmtime::Result<&BedrockScoreboardResource> { + self.resource_table + .get::(&Resource::new_own(res.rep())) + .map_err(wasmtime::Error::from) + } } impl scoreboard::Host for PluginHostState {} @@ -33,7 +43,7 @@ impl scoreboard::HostScoreboard for PluginHostState { display_name: Resource, render_type: RenderType, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); + let provider = self.get_scoreboard_res(&res)?.provider.clone(); let display_name = self.get_text_provider(&display_name)?; let rt = match render_type { @@ -41,19 +51,80 @@ impl scoreboard::HostScoreboard for PluginHostState { RenderType::Hearts => pumpkin_protocol::java::client::play::RenderType::Hearts, }; - let objective = ScoreboardObjective::new( - Box::leak(name.into_boxed_str()), - display_name, - rt, - None, - "dummy", - ); - world - .scoreboard - .lock() - .await - .add_objective(&world, objective) - .await; + let objective = ScoreboardObjective::new(name, display_name, rt, None, "dummy"); + + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .add_objective(world.as_ref(), objective) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Java(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Java( + crate::world::scoreboard::Scoreboard::default(), + )); + } + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.add_objective(player.as_ref(), objective).await; + } + } + } + Ok(()) + } + + async fn update_objective( + &mut self, + res: Resource, + name: String, + display_name: Resource, + render_type: RenderType, + ) -> wasmtime::Result<()> { + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + let display_name = self.get_text_provider(&display_name)?; + + let rt = match render_type { + RenderType::Integer => pumpkin_protocol::java::client::play::RenderType::Integer, + RenderType::Hearts => pumpkin_protocol::java::client::play::RenderType::Hearts, + }; + + let objective = ScoreboardObjective::new(name, display_name, rt, None, "dummy"); + + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .update_objective(world.as_ref(), objective) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Java(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Java( + crate::world::scoreboard::Scoreboard::default(), + )); + } + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.update_objective(player.as_ref(), objective).await; + } + } + } Ok(()) } @@ -62,13 +133,25 @@ impl scoreboard::HostScoreboard for PluginHostState { res: Resource, name: String, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - world - .scoreboard - .lock() - .await - .remove_objective(&world, &name) - .await; + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .remove_objective(world.as_ref(), &name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.remove_objective(player.as_ref(), &name).await; + } + } + } Ok(()) } @@ -78,65 +161,65 @@ impl scoreboard::HostScoreboard for PluginHostState { slot: DisplaySlot, objective_name: String, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - let slot = match slot { - DisplaySlot::PlayerList => pumpkin_data::scoreboard::ScoreboardDisplaySlot::List, - DisplaySlot::Sidebar => pumpkin_data::scoreboard::ScoreboardDisplaySlot::Sidebar, - DisplaySlot::BelowName => pumpkin_data::scoreboard::ScoreboardDisplaySlot::BelowName, - DisplaySlot::SidebarTeamBlack => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamBlack - } - DisplaySlot::SidebarTeamDarkBlue => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkBlue - } - DisplaySlot::SidebarTeamDarkGreen => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkGreen - } - DisplaySlot::SidebarTeamDarkAqua => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkAqua - } - DisplaySlot::SidebarTeamDarkRed => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkRed - } - DisplaySlot::SidebarTeamDarkPurple => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkPurple - } - DisplaySlot::SidebarTeamGold => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGold - } - DisplaySlot::SidebarTeamGray => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGray - } - DisplaySlot::SidebarTeamDarkGray => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkGray - } - DisplaySlot::SidebarTeamBlue => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamBlue - } - DisplaySlot::SidebarTeamGreen => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGreen - } - DisplaySlot::SidebarTeamAqua => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamAqua - } - DisplaySlot::SidebarTeamRed => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamRed, - DisplaySlot::SidebarTeamLightPurple => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamLightPurple - } - DisplaySlot::SidebarTeamYellow => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamYellow - } - DisplaySlot::SidebarTeamWhite => { - pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamWhite - } - }; + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + let slot = map_display_slot(slot); - world - .scoreboard - .lock() - .await - .set_display_objective(&world, slot, &objective_name) - .await; + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .set_display_objective(world.as_ref(), slot, Some(&objective_name)) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Java(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Java( + crate::world::scoreboard::Scoreboard::default(), + )); + } + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.set_display_objective(player.as_ref(), slot, Some(&objective_name)) + .await; + } + } + } + Ok(()) + } + + async fn clear_display_slot( + &mut self, + res: Resource, + slot: DisplaySlot, + ) -> wasmtime::Result<()> { + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + let slot = map_display_slot(slot); + + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .clear_display_objective(world.as_ref(), slot) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.clear_display_objective(player.as_ref(), slot).await; + } + } + } Ok(()) } @@ -147,36 +230,129 @@ impl scoreboard::HostScoreboard for PluginHostState { objective_name: String, value: i32, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - let score = ScoreboardScore::new( - Box::leak(entity_name.into_boxed_str()), - Box::leak(objective_name.into_boxed_str()), - VarInt(value), - None, - None, - ); - world - .scoreboard - .lock() - .await - .update_score(&world, score) - .await; + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + let score = ScoreboardScore::new(entity_name, objective_name, VarInt(value), None, None); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .update_score(world.as_ref(), score) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Java(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Java( + crate::world::scoreboard::Scoreboard::default(), + )); + } + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.update_score(player.as_ref(), score).await; + } + } + } Ok(()) } + async fn add_score( + &mut self, + res: Resource, + entity_name: String, + objective_name: String, + delta: i32, + ) -> wasmtime::Result { + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + let new_val = match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .add_score(world.as_ref(), entity_name, objective_name, delta) + .await + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Java(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Java( + crate::world::scoreboard::Scoreboard::default(), + )); + } + let Some(crate::entity::player::CustomScoreboard::Java(sb)) = custom_guard.as_mut() + else { + return Err(wasmtime::Error::msg("Invalid scoreboard state")); + }; + sb.add_score(player.as_ref(), entity_name, objective_name, delta) + .await + } + }; + Ok(new_val) + } + async fn remove_score( &mut self, res: Resource, entity_name: String, objective_name: String, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - world - .scoreboard - .lock() - .await - .remove_score(&world, &entity_name, &objective_name) - .await; + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .remove_score(world.as_ref(), &entity_name, &objective_name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.remove_score(player.as_ref(), &entity_name, &objective_name) + .await; + } + } + } + Ok(()) + } + + async fn reset_entity_scores( + &mut self, + res: Resource, + entity_name: String, + ) -> wasmtime::Result<()> { + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .reset_scores_for_entity(world.as_ref(), &entity_name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.reset_scores_for_entity(player.as_ref(), &entity_name) + .await; + } + } + } Ok(()) } @@ -186,9 +362,34 @@ impl scoreboard::HostScoreboard for PluginHostState { name: String, settings: TeamSettings, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); + let provider = self.get_scoreboard_res(&res)?.provider.clone(); let team = map_team_settings(name, &settings, self)?; - world.scoreboard.lock().await.add_team(&world, team); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .add_team(world.as_ref(), team) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Java(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Java( + crate::world::scoreboard::Scoreboard::default(), + )); + } + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.add_team(player.as_ref(), team).await; + } + } + } Ok(()) } @@ -197,8 +398,25 @@ impl scoreboard::HostScoreboard for PluginHostState { res: Resource, name: String, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - world.scoreboard.lock().await.remove_team(&world, &name); + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .remove_team(world.as_ref(), &name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.remove_team(player.as_ref(), &name).await; + } + } + } Ok(()) } @@ -208,9 +426,26 @@ impl scoreboard::HostScoreboard for PluginHostState { name: String, settings: TeamSettings, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); + let provider = self.get_scoreboard_res(&res)?.provider.clone(); let team = map_team_settings(name, &settings, self)?; - world.scoreboard.lock().await.update_team(&world, team); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .update_team(world.as_ref(), team) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.update_team(player.as_ref(), team).await; + } + } + } Ok(()) } @@ -220,12 +455,26 @@ impl scoreboard::HostScoreboard for PluginHostState { team_name: String, player_name: String, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - world - .scoreboard - .lock() - .await - .add_player_to_team(&world, &team_name, player_name); + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .add_player_to_team(world.as_ref(), &team_name, player_name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.add_player_to_team(player.as_ref(), &team_name, player_name) + .await; + } + } + } Ok(()) } @@ -235,12 +484,53 @@ impl scoreboard::HostScoreboard for PluginHostState { team_name: String, player_name: String, ) -> wasmtime::Result<()> { - let world = self.get_scoreboard_res(&res)?.provider.clone(); - world - .scoreboard - .lock() - .await - .remove_player_from_team(&world, &team_name, &player_name); + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .remove_player_from_team(world.as_ref(), &team_name, &player_name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.remove_player_from_team(player.as_ref(), &team_name, &player_name) + .await; + } + } + } + Ok(()) + } + + async fn clear_team_players( + &mut self, + res: Resource, + team_name: String, + ) -> wasmtime::Result<()> { + let provider = self.get_scoreboard_res(&res)?.provider.clone(); + match provider { + ScoreboardProvider::World(world) => { + world + .scoreboard + .lock() + .await + .clear_team_players(world.as_ref(), &team_name) + .await; + } + ScoreboardProvider::Player(player) => { + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Java(sb)) = + custom_guard.as_mut() + { + sb.clear_team_players(player.as_ref(), &team_name).await; + } + } + } Ok(()) } @@ -252,6 +542,46 @@ impl scoreboard::HostScoreboard for PluginHostState { } } +const fn map_display_slot(slot: DisplaySlot) -> pumpkin_data::scoreboard::ScoreboardDisplaySlot { + match slot { + DisplaySlot::PlayerList => pumpkin_data::scoreboard::ScoreboardDisplaySlot::List, + DisplaySlot::Sidebar => pumpkin_data::scoreboard::ScoreboardDisplaySlot::Sidebar, + DisplaySlot::BelowName => pumpkin_data::scoreboard::ScoreboardDisplaySlot::BelowName, + DisplaySlot::SidebarTeamBlack => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamBlack, + DisplaySlot::SidebarTeamDarkBlue => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkBlue + } + DisplaySlot::SidebarTeamDarkGreen => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkGreen + } + DisplaySlot::SidebarTeamDarkAqua => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkAqua + } + DisplaySlot::SidebarTeamDarkRed => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkRed + } + DisplaySlot::SidebarTeamDarkPurple => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkPurple + } + DisplaySlot::SidebarTeamGold => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGold, + DisplaySlot::SidebarTeamGray => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGray, + DisplaySlot::SidebarTeamDarkGray => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamDarkGray + } + DisplaySlot::SidebarTeamBlue => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamBlue, + DisplaySlot::SidebarTeamGreen => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamGreen, + DisplaySlot::SidebarTeamAqua => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamAqua, + DisplaySlot::SidebarTeamRed => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamRed, + DisplaySlot::SidebarTeamLightPurple => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamLightPurple + } + DisplaySlot::SidebarTeamYellow => { + pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamYellow + } + DisplaySlot::SidebarTeamWhite => pumpkin_data::scoreboard::ScoreboardDisplaySlot::TeamWhite, + } +} + fn map_team_settings( name: String, settings: &TeamSettings, @@ -336,3 +666,247 @@ const fn map_named_color( pumpkin::plugin::common::NamedColor::White => pumpkin_util::text::color::NamedColor::White, } } + +impl HostBedrockScoreboard for PluginHostState { + async fn add_objective( + &mut self, + res: Resource, + name: String, + display_name: String, + sort_order: scoreboard::BedrockSortOrder, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Bedrock(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Bedrock( + crate::world::scoreboard::BedrockScoreboard::default(), + )); + } + let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() + else { + return Err(wasmtime::Error::msg("Invalid scoreboard state")); + }; + sb.add_objective( + player.as_ref(), + crate::world::scoreboard::BedrockObjective { + name, + display_name, + sort_order: match sort_order { + scoreboard::BedrockSortOrder::Ascending => { + crate::world::scoreboard::BedrockSortOrder::Ascending + } + scoreboard::BedrockSortOrder::Descending => { + crate::world::scoreboard::BedrockSortOrder::Descending + } + }, + }, + ) + .await; + Ok(()) + } + + async fn update_objective( + &mut self, + res: Resource, + name: String, + display_name: String, + sort_order: scoreboard::BedrockSortOrder, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Bedrock(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Bedrock( + crate::world::scoreboard::BedrockScoreboard::default(), + )); + } + let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() + else { + return Err(wasmtime::Error::msg("Invalid scoreboard state")); + }; + sb.update_objective( + player.as_ref(), + crate::world::scoreboard::BedrockObjective { + name, + display_name, + sort_order: match sort_order { + scoreboard::BedrockSortOrder::Ascending => { + crate::world::scoreboard::BedrockSortOrder::Ascending + } + scoreboard::BedrockSortOrder::Descending => { + crate::world::scoreboard::BedrockSortOrder::Descending + } + }, + }, + ) + .await; + Ok(()) + } + + async fn remove_objective( + &mut self, + res: Resource, + name: String, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() { + sb.remove_objective(player.as_ref(), &name).await; + } + Ok(()) + } + + async fn set_display_slot( + &mut self, + res: Resource, + slot: scoreboard::BedrockDisplaySlot, + objective_name: String, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Bedrock(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Bedrock( + crate::world::scoreboard::BedrockScoreboard::default(), + )); + } + let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() + else { + return Err(wasmtime::Error::msg("Invalid scoreboard state")); + }; + let b_slot = match slot { + scoreboard::BedrockDisplaySlot::PlayerList => { + crate::world::scoreboard::BedrockDisplaySlot::PlayerList + } + scoreboard::BedrockDisplaySlot::Sidebar => { + crate::world::scoreboard::BedrockDisplaySlot::Sidebar + } + scoreboard::BedrockDisplaySlot::BelowName => { + crate::world::scoreboard::BedrockDisplaySlot::BelowName + } + }; + sb.set_display_objective(player.as_ref(), b_slot, Some(&objective_name)) + .await; + Ok(()) + } + + async fn clear_display_slot( + &mut self, + res: Resource, + slot: scoreboard::BedrockDisplaySlot, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() { + let b_slot = match slot { + scoreboard::BedrockDisplaySlot::PlayerList => { + crate::world::scoreboard::BedrockDisplaySlot::PlayerList + } + scoreboard::BedrockDisplaySlot::Sidebar => { + crate::world::scoreboard::BedrockDisplaySlot::Sidebar + } + scoreboard::BedrockDisplaySlot::BelowName => { + crate::world::scoreboard::BedrockDisplaySlot::BelowName + } + }; + sb.clear_display_objective(player.as_ref(), b_slot).await; + } + Ok(()) + } + + async fn update_score( + &mut self, + res: Resource, + entity_name: String, + objective_name: String, + value: i32, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Bedrock(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Bedrock( + crate::world::scoreboard::BedrockScoreboard::default(), + )); + } + let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() + else { + return Err(wasmtime::Error::msg("Invalid scoreboard state")); + }; + sb.update_score(player.as_ref(), &entity_name, &objective_name, value) + .await; + Ok(()) + } + + async fn add_score( + &mut self, + res: Resource, + entity_name: String, + objective_name: String, + delta: i32, + ) -> wasmtime::Result { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if !matches!( + *custom_guard, + Some(crate::entity::player::CustomScoreboard::Bedrock(_)) + ) { + *custom_guard = Some(crate::entity::player::CustomScoreboard::Bedrock( + crate::world::scoreboard::BedrockScoreboard::default(), + )); + } + let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() + else { + return Err(wasmtime::Error::msg("Invalid scoreboard state")); + }; + let new_val = sb + .add_score(player.as_ref(), entity_name, objective_name, delta) + .await; + Ok(new_val) + } + + async fn remove_score( + &mut self, + res: Resource, + entity_name: String, + objective_name: String, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() { + sb.remove_score(player.as_ref(), &entity_name, &objective_name) + .await; + } + Ok(()) + } + + async fn reset_entity_scores( + &mut self, + res: Resource, + entity_name: String, + ) -> wasmtime::Result<()> { + let player = self.get_bedrock_scoreboard_res(&res)?.provider.clone(); + let mut custom_guard = player.custom_scoreboard.lock().await; + if let Some(crate::entity::player::CustomScoreboard::Bedrock(sb)) = custom_guard.as_mut() { + sb.reset_scores_for_entity(player.as_ref(), &entity_name) + .await; + } + Ok(()) + } + + async fn drop( + &mut self, + _res: Resource, + ) -> wasmtime::Result<()> { + Ok(()) + } +} diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs index 974634d8d..d94e86617 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/world.rs @@ -495,7 +495,11 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState { world: Resource, ) -> wasmtime::Result> { let world_provider = self.get_world_res(&world)?.provider.clone(); - self.add_scoreboard(world_provider) + self.add_scoreboard( + crate::plugin::loader::wasm::wasm_host::state::ScoreboardProvider::World( + world_provider, + ), + ) } async fn play_sound( diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index eb5161c10..6de031657 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -3407,6 +3407,9 @@ impl World { // Sends initial time player.send_time(self).await; + // Sends initial scoreboard state + player.send_scoreboard().await; + let (spawn_block_pos, yaw, pitch) = { let level_info_lock = self.level_info.load(); ( diff --git a/crates/pumpkin/src/world/scoreboard.rs b/crates/pumpkin/src/world/scoreboard.rs index a276fdff2..9b401d442 100644 --- a/crates/pumpkin/src/world/scoreboard.rs +++ b/crates/pumpkin/src/world/scoreboard.rs @@ -9,48 +9,209 @@ use pumpkin_protocol::{ }, codec::var_int::VarInt, java::client::play::{ - CDisplayObjective, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, Mode, RenderType, - TeamMethod, TeamParameters, + CDisplayObjective, CResetScore, CSetPlayerTeam, CUpdateObjectives, CUpdateScore, Mode, + RenderType, TeamMethod, TeamParameters, }, }; use pumpkin_util::text::{TextComponent, color::NamedColor}; use tracing::warn; use super::World; +use crate::entity::player::Player; -#[derive(Default)] +#[allow(async_fn_in_trait)] +pub trait ScoreboardTarget: Send + Sync { + async fn send_editioned( + &self, + je_packet: &J, + be_packet: &B, + ); + async fn send_je(&self, je_packet: &J); +} + +impl ScoreboardTarget for World { + async fn send_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + self.broadcast_editioned(je_packet, be_packet).await; + } + + async fn send_je(&self, je_packet: &J) { + self.broadcast_packet_all(je_packet); + } +} + +impl ScoreboardTarget for &T { + async fn send_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + (*self).send_editioned(je_packet, be_packet).await; + } + + async fn send_je(&self, je_packet: &J) { + (*self).send_je(je_packet).await; + } +} + +impl ScoreboardTarget for std::sync::Arc { + async fn send_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + self.broadcast_editioned(je_packet, be_packet).await; + } + + async fn send_je(&self, je_packet: &J) { + self.broadcast_packet_all(je_packet); + } +} + +impl ScoreboardTarget for Player { + async fn send_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + Self::send_editioned(self, je_packet, be_packet).await; + } + + async fn send_je(&self, je_packet: &J) { + Self::send_client_packet(self, je_packet).await; + } +} + +impl ScoreboardTarget for std::sync::Arc { + async fn send_editioned( + &self, + je_packet: &J, + be_packet: &B, + ) { + Player::send_editioned(self, je_packet, be_packet).await; + } + + async fn send_je(&self, je_packet: &J) { + Player::send_client_packet(self, je_packet).await; + } +} + +pub struct NoTarget; + +impl ScoreboardTarget for NoTarget { + async fn send_editioned( + &self, + _je_packet: &J, + _be_packet: &B, + ) { + } + + async fn send_je(&self, _je_packet: &J) {} +} + +#[derive(Clone, Debug, Default)] pub struct Scoreboard { - objectives: HashMap>, + objectives: HashMap, + display_slots: HashMap, + scores: HashMap>, teams: HashMap, - scores: HashMap>>, } impl Scoreboard { #[must_use] - pub const fn get_objectives(&self) -> &HashMap> { + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub const fn get_objectives(&self) -> &HashMap { &self.objectives } #[must_use] - pub const fn get_scores(&self) -> &HashMap>> { + pub fn get_objective(&self, name: &str) -> Option<&ScoreboardObjective> { + self.objectives.get(name) + } + + pub fn get_objective_mut(&mut self, name: &str) -> Option<&mut ScoreboardObjective> { + self.objectives.get_mut(name) + } + + #[must_use] + pub const fn get_display_slots(&self) -> &HashMap { + &self.display_slots + } + + #[must_use] + pub fn get_display_objective(&self, slot: ScoreboardDisplaySlot) -> Option<&str> { + self.display_slots.get(&slot).map(String::as_str) + } + + #[must_use] + pub const fn get_scores(&self) -> &HashMap> { &self.scores } + #[must_use] + pub fn get_score(&self, entity_name: &str, objective_name: &str) -> Option<&ScoreboardScore> { + self.scores.get(objective_name)?.get(entity_name) + } + + #[must_use] + pub fn get_score_value(&self, entity_name: &str, objective_name: &str) -> Option { + self.get_score(entity_name, objective_name) + .map(|s| s.value.0) + } + + #[must_use] + pub fn get_scores_for_objective( + &self, + objective_name: &str, + ) -> Option<&HashMap> { + self.scores.get(objective_name) + } + + #[must_use] + pub fn get_scores_for_entity(&self, entity_name: &str) -> HashMap { + let mut entity_scores = HashMap::new(); + for (obj_name, obj_scores) in &self.scores { + if let Some(score) = obj_scores.get(entity_name) { + entity_scores.insert(obj_name.clone(), score); + } + } + entity_scores + } + #[must_use] pub const fn get_teams(&self) -> &HashMap { &self.teams } - async fn broadcast_editioned( - world: &World, - je_packet: &J, - be_packet: &B, - ) { - world.broadcast_editioned(je_packet, be_packet).await; + #[must_use] + pub fn get_team(&self, name: &str) -> Option<&Team> { + self.teams.get(name) } - pub async fn add_objective(&mut self, world: &World, objective: ScoreboardObjective<'static>) { - if self.objectives.contains_key(objective.name) { + pub fn get_team_mut(&mut self, name: &str) -> Option<&mut Team> { + self.teams.get_mut(name) + } + + #[must_use] + pub fn get_entity_team(&self, entity_name: &str) -> Option<&Team> { + self.teams + .values() + .find(|team| team.players.iter().any(|p| p == entity_name)) + } + + pub async fn add_objective( + &mut self, + target: &impl ScoreboardTarget, + objective: ScoreboardObjective, + ) { + if self.objectives.contains_key(&objective.name) { warn!( "Tried to create an objective which already exists: {}", &objective.name @@ -59,7 +220,7 @@ impl Scoreboard { } let je_update = CUpdateObjectives::new( - objective.name.to_string(), + objective.name.clone(), Mode::Add, objective.display_name.clone(), objective.render_type, @@ -68,29 +229,56 @@ impl Scoreboard { let be_update = BSetDisplayObjective { display_slot: "sidebar".to_string(), // Default to sidebar - objective_name: objective.name.to_string(), + objective_name: objective.name.clone(), 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; + target.send_editioned(&je_update, &be_update).await; - let je_display = - CDisplayObjective::new(ScoreboardDisplaySlot::Sidebar, objective.name.to_string()); - // Bedrock's SetDisplayObjective already sets the slot. + self.objectives.insert(objective.name.clone(), objective); + } - world.broadcast_packet_all(&je_display); + pub async fn update_objective( + &mut self, + target: &impl ScoreboardTarget, + objective: ScoreboardObjective, + ) { + if !self.objectives.contains_key(&objective.name) { + warn!( + "Tried to update an objective which does not exist: {}", + &objective.name + ); + return; + } - self.objectives - .insert(objective.name.to_string(), objective); + let je_update = CUpdateObjectives::new( + objective.name.clone(), + Mode::Update, + objective.display_name.clone(), + objective.render_type, + objective.number_format.clone(), + ); + + let be_update = BSetDisplayObjective { + display_slot: "sidebar".to_string(), + objective_name: objective.name.clone(), + display_name: objective.display_name.clone().get_text(), + criteria_name: "dummy".to_string(), + sort_order: VarInt(0), + }; + + target.send_editioned(&je_update, &be_update).await; + + self.objectives.insert(objective.name.clone(), objective); } pub async fn set_display_objective( &mut self, - world: &World, + target: &impl ScoreboardTarget, slot: ScoreboardDisplaySlot, - objective_name: &str, + objective_name: Option<&str>, ) { let slot_str = match slot { ScoreboardDisplaySlot::List => "list", @@ -98,24 +286,42 @@ impl Scoreboard { _ => "sidebar", }; - let display_name = self.objectives.get(objective_name).map_or_else( - || objective_name.to_string(), - |o| o.display_name.clone().get_text(), - ); + let obj_name_str = objective_name.unwrap_or(""); - let je_display = CDisplayObjective::new(slot, objective_name.to_string()); + let display_name = objective_name + .and_then(|name| self.objectives.get(name)) + .map_or_else( + || obj_name_str.to_string(), + |o| o.display_name.clone().get_text(), + ); + + let je_display = CDisplayObjective::new(slot, obj_name_str.to_string()); let be_display = BSetDisplayObjective { display_slot: slot_str.to_string(), - objective_name: objective_name.to_string(), + objective_name: obj_name_str.to_string(), display_name, criteria_name: "dummy".to_string(), sort_order: VarInt(0), }; - Self::broadcast_editioned(world, &je_display, &be_display).await; + target.send_editioned(&je_display, &be_display).await; + + if let Some(name) = objective_name { + self.display_slots.insert(slot, name.to_string()); + } else { + self.display_slots.remove(&slot); + } } - pub async fn remove_objective(&mut self, world: &World, name: &str) { + pub async fn clear_display_objective( + &mut self, + target: &impl ScoreboardTarget, + slot: ScoreboardDisplaySlot, + ) { + self.set_display_objective(target, slot, None).await; + } + + pub async fn remove_objective(&mut self, target: &impl ScoreboardTarget, name: &str) { if !self.objectives.contains_key(name) { warn!( "Tried to remove an objective which does not exist: {}", @@ -136,14 +342,15 @@ impl Scoreboard { objective_name: name.to_string(), }; - Self::broadcast_editioned(world, &je_packet, &be_packet).await; + target.send_editioned(&je_packet, &be_packet).await; self.objectives.remove(name); self.scores.remove(name); + self.display_slots.retain(|_, obj| obj != name); } - pub async fn update_score(&mut self, world: &World, score: ScoreboardScore<'static>) { - if !self.objectives.contains_key(score.objective_name) { + pub async fn update_score(&mut self, target: &impl ScoreboardTarget, score: ScoreboardScore) { + if !self.objectives.contains_key(&score.objective_name) { warn!( "Tried to place a score into an objective which does not exist: {}", &score.objective_name @@ -152,8 +359,8 @@ impl Scoreboard { } let je_packet = CUpdateScore::new( - score.entity_name.to_string(), - score.objective_name.to_string(), + score.entity_name.clone(), + score.objective_name.clone(), score.value, score.display_name.clone(), score.number_format.clone(), @@ -162,26 +369,66 @@ impl Scoreboard { 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(), + scoreboard_id: score.entity_name.as_ptr() as i64, // Internal ID + objective_name: score.objective_name.clone(), score: score.value, - entry_type: VarInt(3), // Fake player/Literal + entry_type: VarInt(3), // Fake player / Literal entity_unique_id: 0, - custom_name: score.entity_name.to_string(), + custom_name: score.entity_name.clone(), }], }; - Self::broadcast_editioned(world, &je_packet, &be_packet).await; + target.send_editioned(&je_packet, &be_packet).await; self.scores - .entry(score.objective_name.to_string()) + .entry(score.objective_name.clone()) .or_default() - .insert(score.entity_name.to_string(), score); + .insert(score.entity_name.clone(), 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()); + pub async fn set_score_value( + &mut self, + target: &impl ScoreboardTarget, + entity_name: impl Into, + objective_name: impl Into, + value: i32, + ) { + let entity_s = entity_name.into(); + let obj_s = objective_name.into(); + let existing = self.get_score(&entity_s, &obj_s).cloned(); + let score = ScoreboardScore { + entity_name: entity_s, + objective_name: obj_s, + value: VarInt(value), + display_name: existing.as_ref().and_then(|s| s.display_name.clone()), + number_format: existing.as_ref().and_then(|s| s.number_format.clone()), + locked: existing.as_ref().is_none_or(|s| s.locked), + }; + self.update_score(target, score).await; + } + + pub async fn add_score( + &mut self, + target: &impl ScoreboardTarget, + entity_name: impl Into, + objective_name: impl Into, + delta: i32, + ) -> i32 { + let entity_s = entity_name.into(); + let obj_s = objective_name.into(); + let current_val = self.get_score_value(&entity_s, &obj_s).unwrap_or(0); + let new_val = current_val + delta; + self.set_score_value(target, entity_s, obj_s, new_val).await; + new_val + } + + pub async fn remove_score( + &mut self, + target: &impl ScoreboardTarget, + entity_name: &str, + objective_name: &str, + ) { + let je_packet = CResetScore::new(entity_name.to_string(), Some(objective_name.to_string())); let be_packet = BSetScore { action: VarInt(1), // Remove @@ -195,19 +442,49 @@ impl Scoreboard { }], }; - Self::broadcast_editioned(world, &je_packet, &be_packet).await; + target.send_editioned(&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) { + pub async fn reset_scores_for_entity( + &mut self, + target: &impl ScoreboardTarget, + entity_name: &str, + ) { + let je_packet = CResetScore::new(entity_name.to_string(), None); + + let mut be_entries = Vec::new(); + for (obj_name, obj_scores) in &self.scores { + if obj_scores.contains_key(entity_name) { + be_entries.push(BScoreEntry { + scoreboard_id: entity_name.as_ptr() as i64, + objective_name: obj_name.clone(), + score: VarInt(0), + entry_type: VarInt(3), + entity_unique_id: 0, + custom_name: entity_name.to_string(), + }); + } + } + + let be_packet = BSetScore { + action: VarInt(1), // Remove + entries: be_entries, + }; + + target.send_editioned(&je_packet, &be_packet).await; + + for obj_scores in self.scores.values_mut() { + obj_scores.remove(entity_name); + } + } + + pub async fn add_team(&mut self, target: &impl ScoreboardTarget, team: Team) { if self.teams.contains_key(&team.name) { - warn!( - "Tried to create Team which does already exist, {}", - team.name - ); + warn!("Tried to create Team which already exists, {}", team.name); return; } @@ -221,17 +498,23 @@ impl Scoreboard { 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(), - }); + target + .send_je(&CSetPlayerTeam { + team_name: team.name.clone(), + method: TeamMethod::Create, + parameters: Some(parameters), + players: team.players.clone().into(), + }) + .await; self.teams.insert(team.name.clone(), team); } - pub fn update_team(&mut self, world: &World, team: Team) { + pub async fn create_team(&mut self, target: &impl ScoreboardTarget, team: Team) { + self.add_team(target, team).await; + } + + pub async fn update_team(&mut self, target: &impl ScoreboardTarget, team: Team) { if !self.teams.contains_key(&team.name) { warn!("Tried to update Team which does not exist, {}", team.name); return; @@ -247,33 +530,42 @@ impl Scoreboard { player_suffix: &team.player_suffix, }; - world.broadcast_packet_all(&CSetPlayerTeam { - team_name: team.name.clone(), - method: TeamMethod::Update, - parameters: Some(parameters), - players: Box::new([]), - }); + target + .send_je(&CSetPlayerTeam { + team_name: team.name.clone(), + method: TeamMethod::Update, + parameters: Some(parameters), + players: Box::new([]), + }) + .await; self.teams.insert(team.name.clone(), team); } - pub fn remove_team(&mut self, world: &World, name: &str) { + pub async fn remove_team(&mut self, target: &impl ScoreboardTarget, 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([]), - }); + target + .send_je(&CSetPlayerTeam { + team_name: name.to_string(), + method: TeamMethod::Remove, + parameters: None, + players: Box::new([]), + }) + .await; self.teams.remove(name); } - pub fn add_player_to_team(&mut self, world: &World, team_name: &str, player: String) { + pub async fn add_player_to_team( + &mut self, + target: &impl ScoreboardTarget, + 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, {}", @@ -286,17 +578,24 @@ impl Scoreboard { return; } - world.broadcast_packet_all(&CSetPlayerTeam { - team_name: team_name.to_string(), - method: TeamMethod::AddPlayers, - parameters: None, - players: vec![player.clone()].into(), - }); + target + .send_je(&CSetPlayerTeam { + team_name: team_name.to_string(), + method: TeamMethod::AddPlayers, + parameters: None, + players: vec![player.clone()].into(), + }) + .await; team.players.push(player); } - pub fn remove_player_from_team(&mut self, world: &World, team_name: &str, player: &str) { + pub async fn remove_player_from_team( + &mut self, + target: &impl ScoreboardTarget, + 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, {}", @@ -309,65 +608,179 @@ impl Scoreboard { return; } - world.broadcast_packet_all(&CSetPlayerTeam { - team_name: team_name.to_string(), - method: TeamMethod::RemovePlayers, - parameters: None, - players: vec![player.to_string()].into(), - }); + target + .send_je(&CSetPlayerTeam { + team_name: team_name.to_string(), + method: TeamMethod::RemovePlayers, + parameters: None, + players: vec![player.to_string()].into(), + }) + .await; 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, - pub criterion: &'a str, -} + pub async fn clear_team_players(&mut self, target: &impl ScoreboardTarget, team_name: &str) { + let Some(team) = self.teams.get_mut(team_name) else { + warn!( + "Tried to clear players from Team which does not exist, {}", + team_name + ); + return; + }; -impl<'a> ScoreboardObjective<'a> { - #[must_use] - pub const fn new( - name: &'a str, - display_name: TextComponent, - render_type: RenderType, - number_format: Option, - criterion: &'a str, - ) -> Self { - Self { - name, - display_name, - render_type, - number_format, - criterion, + if team.players.is_empty() { + return; + } + + let players_to_remove = team.players.clone(); + target + .send_je(&CSetPlayerTeam { + team_name: team_name.to_string(), + method: TeamMethod::RemovePlayers, + parameters: None, + players: players_to_remove.into(), + }) + .await; + + team.players.clear(); + } + + pub async fn send_to_player(&self, player: &Player) { + for objective in self.objectives.values() { + let je_update = CUpdateObjectives::new( + objective.name.clone(), + Mode::Add, + objective.display_name.clone(), + objective.render_type, + objective.number_format.clone(), + ); + let be_update = BSetDisplayObjective { + display_slot: "sidebar".to_string(), + objective_name: objective.name.clone(), + display_name: objective.display_name.clone().get_text(), + criteria_name: "dummy".to_string(), + sort_order: VarInt(0), + }; + player.send_editioned(&je_update, &be_update).await; + } + + for (slot, objective_name) in &self.display_slots { + let slot_str = match slot { + ScoreboardDisplaySlot::List => "list", + ScoreboardDisplaySlot::BelowName => "belowname", + _ => "sidebar", + }; + let display_name = self.objectives.get(objective_name).map_or_else( + || objective_name.clone(), + |o| o.display_name.clone().get_text(), + ); + let je_display = CDisplayObjective::new(*slot, objective_name.clone()); + let be_display = BSetDisplayObjective { + display_slot: slot_str.to_string(), + objective_name: objective_name.clone(), + display_name, + criteria_name: "dummy".to_string(), + sort_order: VarInt(0), + }; + player.send_editioned(&je_display, &be_display).await; + } + + for objective_scores in self.scores.values() { + for score in objective_scores.values() { + let je_packet = CUpdateScore::new( + score.entity_name.clone(), + score.objective_name.clone(), + score.value, + score.display_name.clone(), + score.number_format.clone(), + ); + let be_packet = BSetScore { + action: VarInt(0), + entries: vec![BScoreEntry { + scoreboard_id: score.entity_name.as_ptr() as i64, + objective_name: score.objective_name.clone(), + score: score.value, + entry_type: VarInt(3), + entity_unique_id: 0, + custom_name: score.entity_name.clone(), + }], + }; + player.send_editioned(&je_packet, &be_packet).await; + } + } + + for team in self.teams.values() { + 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, + }; + let je_packet = CSetPlayerTeam { + team_name: team.name.clone(), + method: TeamMethod::Create, + parameters: Some(parameters), + players: team.players.clone().into(), + }; + player.send_client_packet(&je_packet).await; } } } -pub struct ScoreboardScore<'a> { - pub entity_name: &'a str, - pub objective_name: &'a str, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ScoreboardObjective { + pub name: String, + pub display_name: TextComponent, + pub render_type: RenderType, + pub number_format: Option, + pub criterion: String, +} + +impl ScoreboardObjective { + #[must_use] + pub fn new( + name: impl Into, + display_name: TextComponent, + render_type: RenderType, + number_format: Option, + criterion: impl Into, + ) -> Self { + Self { + name: name.into(), + display_name, + render_type, + number_format, + criterion: criterion.into(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ScoreboardScore { + pub entity_name: String, + pub objective_name: String, pub value: VarInt, pub display_name: Option, pub number_format: Option, pub locked: bool, } -impl<'a> ScoreboardScore<'a> { +impl ScoreboardScore { #[must_use] - pub const fn new( - entity_name: &'a str, - objective_name: &'a str, + pub fn new( + entity_name: impl Into, + objective_name: impl Into, value: VarInt, display_name: Option, number_format: Option, ) -> Self { Self { - entity_name, - objective_name, + entity_name: entity_name.into(), + objective_name: objective_name.into(), value, display_name, number_format, @@ -416,7 +829,7 @@ impl CollisionRule { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Team { pub name: String, pub display_name: TextComponent, @@ -428,3 +841,468 @@ pub struct Team { pub player_suffix: TextComponent, pub players: Vec, } + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BedrockSortOrder { + Ascending, + Descending, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum BedrockDisplaySlot { + PlayerList, + Sidebar, + BelowName, +} + +impl BedrockDisplaySlot { + #[must_use] + pub const fn to_str(&self) -> &'static str { + match self { + Self::PlayerList => "list", + Self::Sidebar => "sidebar", + Self::BelowName => "belowname", + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BedrockObjective { + pub name: String, + pub display_name: String, + pub sort_order: BedrockSortOrder, +} + +#[derive(Clone, Debug, Default)] +pub struct BedrockScoreboard { + pub objectives: HashMap, + pub display_slots: HashMap, + pub scores: HashMap<(String, String), i32>, +} + +impl BedrockScoreboard { + pub async fn add_objective(&mut self, player: &Player, objective: BedrockObjective) { + let be_update = BSetDisplayObjective { + display_slot: "sidebar".to_string(), + objective_name: objective.name.clone(), + display_name: objective.display_name.clone(), + criteria_name: "dummy".to_string(), + sort_order: VarInt(match objective.sort_order { + BedrockSortOrder::Ascending => 0, + BedrockSortOrder::Descending => 1, + }), + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CUpdateObjectives::new( + objective.name.clone(), + pumpkin_protocol::java::client::play::Mode::Add, + TextComponent::text(objective.display_name.clone()), + pumpkin_protocol::java::client::play::RenderType::Integer, + None, + ), + &be_update, + ) + .await; + + self.objectives.insert(objective.name.clone(), objective); + } + + pub async fn update_objective(&mut self, player: &Player, objective: BedrockObjective) { + let be_update = BSetDisplayObjective { + display_slot: "sidebar".to_string(), + objective_name: objective.name.clone(), + display_name: objective.display_name.clone(), + criteria_name: "dummy".to_string(), + sort_order: VarInt(match objective.sort_order { + BedrockSortOrder::Ascending => 0, + BedrockSortOrder::Descending => 1, + }), + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CUpdateObjectives::new( + objective.name.clone(), + pumpkin_protocol::java::client::play::Mode::Update, + TextComponent::text(objective.display_name.clone()), + pumpkin_protocol::java::client::play::RenderType::Integer, + None, + ), + &be_update, + ) + .await; + + self.objectives.insert(objective.name.clone(), objective); + } + + pub async fn remove_objective(&mut self, player: &Player, name: &str) { + let be_remove = BRemoveObjective { + objective_name: name.to_string(), + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CUpdateObjectives::new( + name.to_string(), + pumpkin_protocol::java::client::play::Mode::Remove, + TextComponent::text(""), + pumpkin_protocol::java::client::play::RenderType::Integer, + None, + ), + &be_remove, + ) + .await; + self.objectives.remove(name); + self.display_slots.retain(|_, v| v != name); + self.scores.retain(|(_, obj), _| obj != name); + } + + pub async fn set_display_objective( + &mut self, + player: &Player, + slot: BedrockDisplaySlot, + objective_name: Option<&str>, + ) { + let obj_name_str = objective_name.unwrap_or(""); + let display_name = objective_name + .and_then(|name| self.objectives.get(name)) + .map_or_else(|| obj_name_str.to_string(), |o| o.display_name.clone()); + + let be_display = BSetDisplayObjective { + display_slot: slot.to_str().to_string(), + objective_name: obj_name_str.to_string(), + display_name, + criteria_name: "dummy".to_string(), + sort_order: VarInt(0), + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CDisplayObjective::new( + match slot { + BedrockDisplaySlot::PlayerList => ScoreboardDisplaySlot::List, + BedrockDisplaySlot::Sidebar => ScoreboardDisplaySlot::Sidebar, + BedrockDisplaySlot::BelowName => ScoreboardDisplaySlot::BelowName, + }, + obj_name_str.to_string(), + ), + &be_display, + ) + .await; + + if let Some(name) = objective_name { + self.display_slots.insert(slot, name.to_string()); + } else { + self.display_slots.remove(&slot); + } + } + + pub async fn clear_display_objective(&mut self, player: &Player, slot: BedrockDisplaySlot) { + self.set_display_objective(player, slot, None).await; + } + + pub async fn update_score( + &mut self, + player: &Player, + entity_name: &str, + objective_name: &str, + value: i32, + ) { + let score = ScoreboardScore::new( + entity_name.to_string(), + objective_name.to_string(), + VarInt(value), + None, + None, + ); + let be_score = BSetScore { + action: VarInt(0), + entries: vec![], + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CUpdateScore::new( + score.entity_name.clone(), + score.objective_name.clone(), + score.value, + score.display_name.clone(), + score.number_format.clone(), + ), + &be_score, + ) + .await; + + self.scores + .insert((entity_name.to_string(), objective_name.to_string()), value); + } + + pub async fn add_score( + &mut self, + player: &Player, + entity_name: impl Into, + objective_name: impl Into, + delta: i32, + ) -> i32 { + let entity_s = entity_name.into(); + let obj_s = objective_name.into(); + let current = self + .scores + .get(&(entity_s.clone(), obj_s.clone())) + .copied() + .unwrap_or(0); + let new_val = current + delta; + self.update_score(player, &entity_s, &obj_s, new_val).await; + new_val + } + + pub async fn remove_score(&mut self, player: &Player, entity_name: &str, objective_name: &str) { + let be_score = BSetScore { + action: VarInt(1), + entries: vec![], + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CResetScore::new( + entity_name.to_string(), + Some(objective_name.to_string()), + ), + &be_score, + ) + .await; + self.scores + .remove(&(entity_name.to_string(), objective_name.to_string())); + } + + pub async fn reset_scores_for_entity(&mut self, player: &Player, entity_name: &str) { + let be_score = BSetScore { + action: VarInt(1), + entries: vec![], + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CResetScore::new( + entity_name.to_string(), + None, + ), + &be_score, + ) + .await; + self.scores.retain(|(entity, _), _| entity != entity_name); + } + + pub async fn send_to_player(&self, player: &Player) { + for objective in self.objectives.values() { + let be_update = BSetDisplayObjective { + display_slot: "sidebar".to_string(), + objective_name: objective.name.clone(), + display_name: objective.display_name.clone(), + criteria_name: "dummy".to_string(), + sort_order: VarInt(match objective.sort_order { + BedrockSortOrder::Ascending => 0, + BedrockSortOrder::Descending => 1, + }), + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CUpdateObjectives::new( + objective.name.clone(), + pumpkin_protocol::java::client::play::Mode::Add, + TextComponent::text(objective.display_name.clone()), + pumpkin_protocol::java::client::play::RenderType::Integer, + None, + ), + &be_update, + ) + .await; + } + + for (slot, objective_name) in &self.display_slots { + let display_name = self + .objectives + .get(objective_name) + .map_or_else(|| objective_name.clone(), |o| o.display_name.clone()); + let be_display = BSetDisplayObjective { + display_slot: slot.to_str().to_string(), + objective_name: objective_name.clone(), + display_name, + criteria_name: "dummy".to_string(), + sort_order: VarInt(0), + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CDisplayObjective::new( + match slot { + BedrockDisplaySlot::PlayerList => ScoreboardDisplaySlot::List, + BedrockDisplaySlot::Sidebar => ScoreboardDisplaySlot::Sidebar, + BedrockDisplaySlot::BelowName => ScoreboardDisplaySlot::BelowName, + }, + objective_name.clone(), + ), + &be_display, + ) + .await; + } + + for ((entity_name, objective_name), value) in &self.scores { + let score = ScoreboardScore::new( + entity_name.clone(), + objective_name.clone(), + VarInt(*value), + None, + None, + ); + let be_score = BSetScore { + action: VarInt(0), + entries: vec![], + }; + player + .send_editioned( + &pumpkin_protocol::java::client::play::CUpdateScore::new( + score.entity_name, + score.objective_name, + score.value, + score.display_name, + score.number_format, + ), + &be_score, + ) + .await; + } + } +} + +#[derive(Default, Debug)] +pub struct ScoreboardBuilder { + scoreboard: Scoreboard, +} + +impl ScoreboardBuilder { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn objective(mut self, name: impl Into, display_name: TextComponent) -> Self { + let obj = ScoreboardObjective::new( + name.into(), + display_name, + RenderType::Integer, + None, + "dummy", + ); + self.scoreboard.objectives.insert(obj.name.clone(), obj); + self + } + + #[must_use] + pub fn objective_with_render( + mut self, + name: impl Into, + display_name: TextComponent, + render_type: RenderType, + ) -> Self { + let obj = ScoreboardObjective::new(name.into(), display_name, render_type, None, "dummy"); + self.scoreboard.objectives.insert(obj.name.clone(), obj); + self + } + + #[must_use] + pub fn display_slot( + mut self, + slot: ScoreboardDisplaySlot, + objective_name: impl Into, + ) -> Self { + self.scoreboard + .display_slots + .insert(slot, objective_name.into()); + self + } + + #[must_use] + pub fn score( + mut self, + entity_name: impl Into, + objective_name: impl Into, + value: i32, + ) -> Self { + let entity_s = entity_name.into(); + let obj_s = objective_name.into(); + let score = + ScoreboardScore::new(entity_s.clone(), obj_s.clone(), VarInt(value), None, None); + self.scoreboard + .scores + .entry(entity_s) + .or_default() + .insert(obj_s, score); + self + } + + #[must_use] + pub fn team(mut self, team: Team) -> Self { + self.scoreboard.teams.insert(team.name.clone(), team); + self + } + + #[must_use] + pub fn build(self) -> Scoreboard { + self.scoreboard + } +} + +#[derive(Default, Debug)] +pub struct BedrockScoreboardBuilder { + scoreboard: BedrockScoreboard, +} + +impl BedrockScoreboardBuilder { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn objective( + mut self, + name: impl Into, + display_name: impl Into, + sort_order: BedrockSortOrder, + ) -> Self { + let name = name.into(); + let obj = BedrockObjective { + name: name.clone(), + display_name: display_name.into(), + sort_order, + }; + self.scoreboard.objectives.insert(name, obj); + self + } + + #[must_use] + pub fn display_slot( + mut self, + slot: BedrockDisplaySlot, + objective_name: impl Into, + ) -> Self { + self.scoreboard + .display_slots + .insert(slot, objective_name.into()); + self + } + + #[must_use] + pub fn score( + mut self, + entity_name: impl Into, + objective_name: impl Into, + value: i32, + ) -> Self { + self.scoreboard + .scores + .insert((entity_name.into(), objective_name.into()), value); + self + } + + #[must_use] + pub fn build(self) -> BedrockScoreboard { + self.scoreboard + } +} diff --git a/tools/pumpkin-codegen/src/scoreboard_slot.rs b/tools/pumpkin-codegen/src/scoreboard_slot.rs index 52fd91552..128f6ce75 100644 --- a/tools/pumpkin-codegen/src/scoreboard_slot.rs +++ b/tools/pumpkin-codegen/src/scoreboard_slot.rs @@ -14,6 +14,7 @@ pub fn build() -> TokenStream { let variants = array_to_tokenstream(&sound_categories); quote! { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ScoreboardDisplaySlot { #variants }