Added Stop Command

This commit is contained in:
Alexander Medvedev
2025-03-05 23:06:33 +01:00
parent 7a4b46f51b
commit 081bdbbbaa
7 changed files with 191 additions and 5 deletions

View File

@@ -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::<TokenStream>();
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::<TokenStream>();
quote! {
#[derive(Clone, Copy)]
pub enum SoundCategory {
#variants
}
impl SoundCategory {
pub fn from_name(name: &str) -> Option<Self> {
match name {
#type_from_name
_ => None
}
}
pub const fn to_name(&self) -> &'static str {
match self {
#type_to_name
}
}
}
}
}

View File

@@ -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::*;

View File

@@ -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<Identifier>,
category: Option<SoundCategory>,
}
impl CStopSound {
pub fn new(sound_id: Option<Identifier>, category: Option<SoundCategory>) -> 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);
}
}
}
}

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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)),
),
)
}

View File

@@ -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<Identifier>, category: Option<SoundCategory>) {
self.client
.send_packet(&CStopSound::new(sound_id, category))
.await;
}
pub async fn await_cancel(&self) {
self.cancel_tasks.notified().await;
}