Pumpkin/pumpkin/src/block/blocks/jukebox.rs
2026-05-14 12:28:43 +02:00

248 lines
9.9 KiB
Rust

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<World>,
) {
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<World>) {
if let Some(block_entity) = world.get_block_entity(position)
&& let Some(jukebox_entity) = block_entity.as_any().downcast_ref::<JukeboxBlockEntity>()
{
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<World>) {
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<World>, 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::<JukeboxPlayableImpl>()
.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::<JukeboxBlockEntity>()
{
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::<JukeboxBlockEntity>()
&& 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<u8>> {
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::<JukeboxBlockEntity>()
{
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::<JukeboxPlayableImpl>()
&& let Some(song_name) = playable.song.split(':').nth(1)
&& let Some(song) = JukeboxSong::from_name(song_name)
{
return Some(song.comparator_output());
}
}
Some(0)
})
}
}