diff --git a/pumpkin-data/build/sound_category.rs b/pumpkin-data/build/sound_category.rs index 837d9a052..6137e9aa4 100644 --- a/pumpkin-data/build/sound_category.rs +++ b/pumpkin-data/build/sound_category.rs @@ -1,5 +1,6 @@ +use heck::ToPascalCase; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use crate::array_to_tokenstream; @@ -10,11 +11,48 @@ pub(crate) fn build() -> TokenStream { serde_json::from_str(include_str!("../../assets/sound_category.json")) .expect("Failed to parse sound_category.json"); let variants = array_to_tokenstream(&sound_categories); + let type_from_name = &sound_categories + .iter() + .map(|sound| { + let id = &sound.to_lowercase(); + let name = format_ident!("{}", sound.to_pascal_case()); + quote! { + #id => Some(Self::#name), + } + }) + .collect::(); + + let type_to_name = &sound_categories + .iter() + .map(|sound| { + let id = &sound.to_lowercase(); + let name = format_ident!("{}", sound.to_pascal_case()); + + quote! { + Self::#name => #id, + } + }) + .collect::(); quote! { #[derive(Clone, Copy)] pub enum SoundCategory { #variants } + + impl SoundCategory { + pub fn from_name(name: &str) -> Option { + match name { + #type_from_name + _ => None + } + } + + pub const fn to_name(&self) -> &'static str { + match self { + #type_to_name + } + } + } } } diff --git a/pumpkin-protocol/src/client/play/mod.rs b/pumpkin-protocol/src/client/play/mod.rs index 55f5958fa..ec7a16c55 100644 --- a/pumpkin-protocol/src/client/play/mod.rs +++ b/pumpkin-protocol/src/client/play/mod.rs @@ -62,6 +62,7 @@ mod set_time; mod set_title; mod sound_effect; mod spawn_entity; +mod stop_sound; mod store_cookie; mod subtitle; mod system_chat_message; @@ -141,6 +142,7 @@ pub use set_time::*; pub use set_title::*; pub use sound_effect::*; pub use spawn_entity::*; +pub use stop_sound::*; pub use store_cookie::*; pub use subtitle::*; pub use system_chat_message::*; diff --git a/pumpkin-protocol/src/client/play/stop_sound.rs b/pumpkin-protocol/src/client/play/stop_sound.rs new file mode 100644 index 000000000..31bee8c79 --- /dev/null +++ b/pumpkin-protocol/src/client/play/stop_sound.rs @@ -0,0 +1,45 @@ +use crate::bytebuf::ByteBufMut; +use crate::codec::var_int::VarInt; +use crate::{ClientPacket, codec::identifier::Identifier}; +use pumpkin_data::{packet::clientbound::PLAY_STOP_SOUND, sound::SoundCategory}; +use pumpkin_macros::packet; + +#[packet(PLAY_STOP_SOUND)] +pub struct CStopSound { + sound_id: Option, + category: Option, +} + +impl CStopSound { + pub fn new(sound_id: Option, category: Option) -> Self { + Self { sound_id, category } + } +} + +impl ClientPacket for CStopSound { + fn write(&self, bytebuf: &mut impl bytes::BufMut) { + const NO_CATEGORY_NO_SOUND: u8 = 0; + const CATEGORY_ONLY: u8 = 1; + const SOUND_ONLY: u8 = 2; + const CATEGORY_AND_SOUND: u8 = 3; + + match (self.category, &self.sound_id) { + (Some(category), Some(sound_id)) => { + bytebuf.put_u8(CATEGORY_AND_SOUND); + bytebuf.put_var_int(&VarInt(category as i32)); + bytebuf.put_identifier(sound_id); + } + (Some(category), None) => { + bytebuf.put_u8(CATEGORY_ONLY); + bytebuf.put_var_int(&VarInt(category as i32)); + } + (None, Some(sound_id)) => { + bytebuf.put_u8(SOUND_ONLY); + bytebuf.put_identifier(sound_id); + } + (None, None) => { + bytebuf.put_u8(NO_CATEGORY_NO_SOUND); + } + } + } +} diff --git a/pumpkin/src/command/args/sound_category.rs b/pumpkin/src/command/args/sound_category.rs index 003f80c80..8ad570b5b 100644 --- a/pumpkin/src/command/args/sound_category.rs +++ b/pumpkin/src/command/args/sound_category.rs @@ -39,14 +39,15 @@ impl ArgumentConsumer for SoundCategoryArgumentConsumer { let category = match s.to_lowercase().as_str() { "master" => Some(SoundCategory::Master), // Default category, affects all sounds "music" => Some(SoundCategory::Music), // Background music + // i don't use SoundCategory::from_name because its is record and not records :c "record" => Some(SoundCategory::Records), // Music discs "weather" => Some(SoundCategory::Weather), // Rain, thunder - "block" => Some(SoundCategory::Blocks), // Block sounds + "block" => Some(SoundCategory::Blocks), // Block sounds "hostile" => Some(SoundCategory::Hostile), // Hostile mob sounds "neutral" => Some(SoundCategory::Neutral), // Neutral mob sounds "player" => Some(SoundCategory::Players), // Player sounds "ambient" => Some(SoundCategory::Ambient), // Ambient environment - "voice" => Some(SoundCategory::Voice), // Voice/speech + "voice" => Some(SoundCategory::Voice), // Voice/speech _ => None, }; diff --git a/pumpkin/src/command/commands/mod.rs b/pumpkin/src/command/commands/mod.rs index bee0e6959..0b7509de3 100644 --- a/pumpkin/src/command/commands/mod.rs +++ b/pumpkin/src/command/commands/mod.rs @@ -33,6 +33,7 @@ mod say; mod seed; mod setblock; mod stop; +mod stopsound; mod summon; mod teleport; mod time; @@ -73,6 +74,7 @@ pub fn default_dispatcher() -> CommandDispatcher { dispatcher.register(bossbar::init_command_tree(), PermissionLvl::Two); dispatcher.register(say::init_command_tree(), PermissionLvl::Two); dispatcher.register(gamemode::init_command_tree(), PermissionLvl::Two); + dispatcher.register(stopsound::init_command_tree(), PermissionLvl::Two); dispatcher.register(defaultgamemode::init_command_tree(), PermissionLvl::Two); // Three dispatcher.register(op::init_command_tree(), PermissionLvl::Three); diff --git a/pumpkin/src/command/commands/stopsound.rs b/pumpkin/src/command/commands/stopsound.rs new file mode 100644 index 000000000..ccf7757af --- /dev/null +++ b/pumpkin/src/command/commands/stopsound.rs @@ -0,0 +1,84 @@ +use crate::command::{ + CommandExecutor, CommandSender, + args::{ + ConsumedArgs, FindArg, players::PlayersArgumentConsumer, sound::SoundArgumentConsumer, + sound_category::SoundCategoryArgumentConsumer, + }, + dispatcher::CommandError, + tree::{CommandTree, builder::argument}, +}; +use async_trait::async_trait; +use pumpkin_protocol::codec::identifier::Identifier; +use pumpkin_util::text::TextComponent; + +const NAMES: [&str; 1] = ["stopsound"]; +const DESCRIPTION: &str = "Stops a currently playing sound."; + +const ARG_TARGETS: &str = "targets"; +const ARG_SOURCE: &str = "source"; +const ARG_SOUND: &str = "sound"; + +pub struct Executor; + +#[async_trait] +impl CommandExecutor for Executor { + async fn execute<'a>( + &self, + sender: &mut CommandSender<'a>, + _server: &crate::server::Server, + args: &ConsumedArgs<'a>, + ) -> Result<(), CommandError> { + let targets = PlayersArgumentConsumer::find_arg(args, ARG_TARGETS)?; + + let mut category = SoundCategoryArgumentConsumer::find_arg(args, ARG_SOURCE); + let mut sound = SoundArgumentConsumer::find_arg(args, ARG_SOUND); + + for target in targets { + target + .stop_sound( + sound + .as_mut() + .cloned() + .map(|s| Identifier::vanilla(s.to_name())) + .ok(), + category.as_mut().map(|s| **s).ok(), + ) + .await; + } + let text = match (category, sound) { + (Ok(c), Ok(s)) => TextComponent::translate( + "commands.stopsound.success.source.sound", + [ + TextComponent::text(s.to_name()), + TextComponent::text(c.to_name()), + ], + ), + (Ok(c), Err(_)) => TextComponent::translate( + "commands.stopsound.success.source.any", + [TextComponent::text(c.to_name())], + ), + (Err(_), Ok(s)) => TextComponent::translate( + "commands.stopsound.success.sourceless.sound", + [TextComponent::text(s.to_name())], + ), + (Err(_), Err(_)) => { + TextComponent::translate("commands.stopsound.success.sourceless.any", []) + } + }; + sender.send_message(text).await; + + Ok(()) + } +} + +pub fn init_command_tree() -> CommandTree { + CommandTree::new(NAMES, DESCRIPTION).then( + argument(ARG_TARGETS, PlayersArgumentConsumer) + .execute(Executor) + .then( + argument(ARG_SOURCE, SoundCategoryArgumentConsumer) + .execute(Executor) + .then(argument(ARG_SOUND, SoundArgumentConsumer).execute(Executor)), + ), + ) +} diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 995471777..8dea50149 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -26,9 +26,11 @@ use pumpkin_protocol::{ client::play::{ CAcknowledgeBlockChange, CActionBar, CCombatDeath, CDisguisedChatMessage, CGameEvent, CKeepAlive, CParticle, CPlayDisconnect, CPlayerAbilities, CPlayerInfoUpdate, - CPlayerPosition, CRespawn, CSetExperience, CSetHealth, CSubtitle, CSystemChatMessage, - CTitleText, CUnloadChunk, CUpdateMobEffect, GameEvent, MetaDataType, PlayerAction, + CPlayerPosition, CRespawn, CSetExperience, CSetHealth, CStopSound, CSubtitle, + CSystemChatMessage, CTitleText, CUnloadChunk, CUpdateMobEffect, GameEvent, MetaDataType, + PlayerAction, }, + codec::identifier::Identifier, server::play::{ SChatCommand, SChatMessage, SClientCommand, SClientInformationPlay, SClientTickEnd, SCommandSuggestion, SConfirmTeleport, SInteract, SPickItemFromBlock, SPlayerAbilities, @@ -416,6 +418,18 @@ impl Player { .await; } + /// Stops a sound playing on the client. + /// + /// # Arguments + /// + /// * `sound_id`: An optional `Identifier` specifying the sound to stop. If `None`, all sounds in the specified category (if any) will be stopped. + /// * `category`: An optional `SoundCategory` specifying the sound category to stop. If `None`, all sounds with the specified identifier (if any) will be stopped. + pub async fn stop_sound(&self, sound_id: Option, category: Option) { + self.client + .send_packet(&CStopSound::new(sound_id, category)) + .await; + } + pub async fn await_cancel(&self) { self.cancel_tasks.notified().await; }