use std::sync::Arc; use crate::block::entities::jukebox::JukeboxBlockEntity; use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, BlockFuture, BrokenArgs, EmitsRedstonePowerArgs, GetComparatorOutputArgs, GetRedstonePowerArgs, NormalUseArgs, OnStateReplacedArgs, PlacedArgs, UseWithItemArgs, }; use crate::entity::Entity; use crate::entity::item::ItemEntity; use crate::world::World; use pumpkin_data::data_component_impl::JukeboxPlayableImpl; use pumpkin_data::entity::EntityType; use pumpkin_data::jukebox_song::JukeboxSong; use pumpkin_data::world::WorldEvent; use pumpkin_data::{ Block, block_properties::{BlockProperties, JukeboxLikeProperties}, }; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use pumpkin_world::world::BlockFlags; use rand::{RngExt, rng}; use tracing::error; #[pumpkin_block("minecraft:jukebox")] pub struct JukeboxBlock; impl JukeboxBlock { fn has_record_state(block: &Block, state_id: u16) -> bool { JukeboxLikeProperties::from_state_id(state_id, block).has_record } async fn set_record_state( has_record: bool, block: &Block, position: &BlockPos, world: &Arc, ) { let new_state = JukeboxLikeProperties { has_record }; world .set_block_state( position, new_state.to_state_id(block), BlockFlags::NOTIFY_LISTENERS, ) .await; } /// Drops the record from the jukebox - matches vanilla's `JukeboxBlockEntity.dropRecord()` /// Spawns item at (pos + 0.5, pos + 1.01, pos + 0.5) with horizontal random offset async fn drop_record(position: &BlockPos, world: &Arc) { if let Some(block_entity) = world.get_block_entity(position) && let Some(jukebox_entity) = block_entity.as_any().downcast_ref::() { let record = jukebox_entity.clear_record().await; if !record.is_empty() { // Vanilla: Vec3d.add(pos, 0.5, 1.01, 0.5).addHorizontalRandom(random, 0.7F) // addHorizontalRandom adds random in range [-0.35, 0.35] to x and z let spawn_pos = Vector3::new( f64::from(position.0.x) + 0.5 + rng().random_range(-0.35..0.35), f64::from(position.0.y) + 1.01, f64::from(position.0.z) + 0.5 + rng().random_range(-0.35..0.35), ); let entity = Entity::new(world.clone(), spawn_pos, &EntityType::ITEM); // Vanilla: setToDefaultPickupDelay() = 10 ticks let item_entity = Arc::new(ItemEntity::new(entity, record)); world.spawn_entity(item_entity).await; } } } /// Stops the music and updates block state async fn stop_playing(block: &Block, position: &BlockPos, world: &Arc) { Self::set_record_state(false, block, position, world).await; world.sync_world_event(WorldEvent::SoundStopJukeboxSong, *position, 0); } /// Starts playing music fn start_playing(position: &BlockPos, world: &Arc, song_id: u32) { world.sync_world_event(WorldEvent::SoundPlayJukeboxSong, *position, song_id as i32); } } impl BlockBehaviour for JukeboxBlock { /// Called when the jukebox is placed - creates the block entity fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { let block_entity = JukeboxBlockEntity::new(*args.position); args.world.add_block_entity(Arc::new(block_entity)).await; }) } /// Called when player right-clicks with empty hand or non-disc item /// Vanilla: `JukeboxBlock.onUse()` - drops record if present fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> { Box::pin(async move { let state_id = args.world.get_block_state(args.position).id; // Vanilla: if (state.get(HAS_RECORD) && world.getBlockEntity(pos) instanceof JukeboxBlockEntity lv) if Self::has_record_state(args.block, state_id) { // Drop the record Self::drop_record(args.position, args.world).await; // Stop the music and update block state Self::stop_playing(args.block, args.position, args.world).await; return BlockActionResult::Success; } BlockActionResult::Pass }) } /// Called when player right-clicks with an item /// Vanilla: `JukeboxBlock.onUseWithItem()` -> `JukeboxPlayableComponent.tryPlayStack()` fn use_with_item<'a>( &'a self, args: UseWithItemArgs<'a>, ) -> BlockFuture<'a, BlockActionResult> { Box::pin(async move { let world = args.world; let state_id = world.get_block_state(args.position).id; // Vanilla: if (state.get(HAS_RECORD)) return PASS_TO_DEFAULT_BLOCK_ACTION if Self::has_record_state(args.block, state_id) { return BlockActionResult::PassToDefaultBlockAction; } let mut item_stack = args.item_stack.lock().await; // Vanilla: JukeboxPlayableComponent lv = stack.get(DataComponentTypes.JUKEBOX_PLAYABLE) let jukebox_playable = item_stack .get_data_component::() .map(|i| i.song); // Vanilla: if (lv == null) return PASS_TO_DEFAULT_BLOCK_ACTION let Some(jukebox_playable) = jukebox_playable else { return BlockActionResult::PassToDefaultBlockAction; }; let Some(song_name) = jukebox_playable.split(':').nth(1) else { return BlockActionResult::PassToDefaultBlockAction; }; let Some(jukebox_song) = JukeboxSong::from_name(song_name) else { error!("Jukebox playable song not registered: {song_name}"); return BlockActionResult::PassToDefaultBlockAction; }; // Vanilla: ItemStack lv3 = stack.splitUnlessCreative(1, player) let record = item_stack.split_unless_creative(args.player.gamemode.load(), 1); // Vanilla: lv4.setStack(lv3) if let Some(block_entity) = world.get_block_entity(args.position) && let Some(jukebox_entity) = block_entity.as_any().downcast_ref::() { jukebox_entity.set_record(record).await; // Start tracking playback with song duration jukebox_entity.start_playing(jukebox_song.length_in_ticks()); } // Update block state to has_record = true Self::set_record_state(true, args.block, args.position, world).await; // Start playing the music (client-side audio) Self::start_playing(args.position, world, jukebox_song.get_id()); // TODO: world.emitGameEvent(GameEvent.BLOCK_CHANGE, pos, ...) // TODO: player.incrementStat(Stats.PLAY_RECORD) BlockActionResult::Success }) } /// Called when the jukebox is broken fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { // Drop the record if there is one Self::drop_record(args.position, args.world).await; // Stop the music args.world .sync_world_event(WorldEvent::SoundStopJukeboxSong, *args.position, 0); }) } /// Vanilla: `JukeboxBlock.onStateReplaced()` -> `ItemScatterer.onStateReplaced()` fn on_state_replaced<'a>(&'a self, _args: OnStateReplacedArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { // Vanilla calls ItemScatterer.onStateReplaced which updates comparators // TODO: world.updateComparators(pos, block) when implemented }) } /// Vanilla: `JukeboxBlock.emitsRedstonePower()` returns true fn emits_redstone_power<'a>( &'a self, _args: EmitsRedstonePowerArgs<'a>, ) -> BlockFuture<'a, bool> { Box::pin(async move { true }) } /// Vanilla: Returns 15 if playing, 0 otherwise fn get_weak_redstone_power<'a>( &'a self, args: GetRedstonePowerArgs<'a>, ) -> BlockFuture<'a, u8> { Box::pin(async move { // Vanilla: return world.getBlockEntity(pos) instanceof JukeboxBlockEntity lv && lv.getManager().isPlaying() ? 15 : 0 if let Some(block_entity) = args.world.get_block_entity(args.position) && let Some(jukebox_entity) = block_entity.as_any().downcast_ref::() && jukebox_entity.is_playing() { 15 } else { 0 } }) } /// Vanilla: Returns the song's comparator output (0-15) fn get_comparator_output<'a>( &'a self, args: GetComparatorOutputArgs<'a>, ) -> BlockFuture<'a, Option> { Box::pin(async move { // Vanilla: return world.getBlockEntity(pos) instanceof JukeboxBlockEntity lv ? lv.getComparatorOutput() : 0 if let Some(block_entity) = args.world.get_block_entity(args.position) && let Some(jukebox_entity) = block_entity.as_any().downcast_ref::() { let record = jukebox_entity.get_record().await; // Get the song from the record's jukebox_playable component if let Some(playable) = record.get_data_component::() && let Some(song_name) = playable.song.split(':').nth(1) && let Some(song) = JukeboxSong::from_name(song_name) { return Some(song.comparator_output()); } } Some(0) }) } }