use std::sync::Arc; use pumpkin_data::sound::SoundCategory; use pumpkin_data::translation; use pumpkin_util::text::TextComponent; use rand::{RngExt, rng}; use crate::entity::EntityBase; use crate::{ command::{ CommandExecutor, CommandResult, CommandSender, args::{ Arg, ConsumedArgs, FindArg, bounded_num::BoundedNumArgumentConsumer, players::PlayersArgumentConsumer, position_3d::Position3DArgumentConsumer, sound::SoundArgumentConsumer, sound_category::SoundCategoryArgumentConsumer, }, dispatcher::CommandError, tree::{CommandTree, builder::argument}, }, entity::player::Player, }; /// Command: playsound [] [] [] [] [] [] /// /// Plays a sound at specified position for target players. /// - sound: The sound resource location to play /// - source: Sound category (master, music, record, etc.) /// - targets: Players who will hear the sound /// - pos: Position to play the sound from /// - volume: Sound volume (>=0, default: 1.0) /// - pitch: Sound pitch (0.5-2.0, default: 1.0) /// - minVolume: Minimum volume for distant players (0.0-1.0, default: 0.0) const NAMES: [&str; 1] = ["playsound"]; const DESCRIPTION: &str = "Plays a sound at a position."; const ARG_SOUND: &str = "sound"; const ARG_SOURCE: &str = "source"; const ARG_TARGETS: &str = "targets"; const ARG_POS: &str = "pos"; const ARG_VOLUME: &str = "volume"; const ARG_PITCH: &str = "pitch"; const ARG_MIN_VOLUME: &str = "minVolume"; // Volume must be >= 0, no upper limit const fn volume_consumer() -> BoundedNumArgumentConsumer { BoundedNumArgumentConsumer::new().name(ARG_VOLUME).min(0.0) } // Pitch must be between 0.0 and 2.0 // Values below 0.5 are treated as 0.5 const fn pitch_consumer() -> BoundedNumArgumentConsumer { BoundedNumArgumentConsumer::new() .name(ARG_PITCH) .min(0.0) .max(2.0) } // Minimum volume must be between 0.0 and 1.0 // Controls the volume for players outside normal hearing range const fn min_volume_consumer() -> BoundedNumArgumentConsumer { BoundedNumArgumentConsumer::new() .name(ARG_MIN_VOLUME) .min(0.0) .max(1.0) } struct Executor; impl CommandExecutor for Executor { fn execute<'a>( &'a self, sender: &'a CommandSender, _server: &'a crate::server::Server, args: &'a ConsumedArgs<'a>, ) -> CommandResult<'a> { Box::pin(async move { // Get required sound argument let sound = SoundArgumentConsumer::find_arg(args, ARG_SOUND)?; // Get optional sound category, defaults to Master let source = args .get(ARG_SOURCE) .map_or(SoundCategory::Master, |arg| match arg { Arg::SoundCategory(category) => *category, _ => SoundCategory::Master, }); // Get target players, defaults to sender if not specified let targets: &[Arc] = match ( PlayersArgumentConsumer::find_arg(args, ARG_TARGETS), sender.as_player(), ) { (Ok(players), _) => players, (_, Some(player)) => &[player], (_, _) => &[], }; // Get optional position, defaults to target's position let position = Position3DArgumentConsumer::find_arg(args, ARG_POS).ok(); // Get optional volume parameter let volume = match BoundedNumArgumentConsumer::::find_arg(args, ARG_VOLUME) { Ok(Ok(v)) => v, _ => 1.0, // Default volume }; // Get optional pitch parameter let pitch = match BoundedNumArgumentConsumer::::find_arg(args, ARG_PITCH) { Ok(Ok(p)) => p.max(0.5), // Values below 0.5 are clamped _ => 1.0, // Default pitch }; // Get optional minimum volume (currently unused in implementation) let min_volume = match BoundedNumArgumentConsumer::::find_arg(args, ARG_MIN_VOLUME) { Ok(Ok(v)) => v, _ => 0.0, // Default minimum volume }; // Use same random seed for all targets to ensure sound synchronization let seed = rng().random::(); // Track how many players actually received the sound let mut players_who_heard = 0; // Play sound for each target player for target in targets { let pos = position.unwrap_or(target.living_entity.entity.pos.load()); // Check if player can hear the sound based on volume and distance let player_pos = target.living_entity.entity.pos.load(); let distance = player_pos.squared_distance_to_vec(&pos); let max_distance: f64 = (16.0 * volume).into(); // 16 blocks is base distance at volume 1.0 if distance <= max_distance || min_volume > 0.0 { target .play_sound(sound as u16, source, &pos, volume, pitch, seed) .await; players_who_heard += 1; } } // Send appropriate message based on results if players_who_heard == 0 { Err(CommandError::CommandFailed(TextComponent::translate_cross( "commands.playsound.failed", "commands.playsound.failed", [], ))) } else { let sound_name = sound.to_name(); if players_who_heard == 1 { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, translation::java::COMMANDS_PLAYSOUND_SUCCESS_SINGLE, [ TextComponent::text(sound_name), targets[0].get_display_name().await, ], )) .await; } else { sender .send_message(TextComponent::translate_cross( translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, translation::java::COMMANDS_PLAYSOUND_SUCCESS_MULTIPLE, [ TextComponent::text(sound_name), TextComponent::text(players_who_heard.to_string()), ], )) .await; } Ok(players_who_heard) } }) } } pub fn init_command_tree() -> CommandTree { CommandTree::new(NAMES, DESCRIPTION).then( argument(ARG_SOUND, SoundArgumentConsumer) .then( argument(ARG_SOURCE, SoundCategoryArgumentConsumer) .then( argument(ARG_TARGETS, PlayersArgumentConsumer) .then( argument(ARG_POS, Position3DArgumentConsumer) .then( argument(ARG_VOLUME, volume_consumer()) .then( argument(ARG_PITCH, pitch_consumer()) .then( argument( ARG_MIN_VOLUME, min_volume_consumer(), ) .execute(Executor), ) .execute(Executor), ) .execute(Executor), ) .execute(Executor), ) .execute(Executor), ) .execute(Executor), ) .execute(Executor), ) }