mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-31 08:22:33 +00:00
fix: jukebox now properly handles music discs (#1438)
* fix: jukebox now properly handles music discs (#568) - Add JukeboxBlockEntity for disc storage and playback tracking - Disc is removed from player inventory when inserted - Disc drops when player right-clicks to retrieve it - Disc drops when jukebox is broken - Redstone power output (15 when playing, 0 otherwise) - Comparator output varies per song (vanilla values 1-15) - Playback auto-stops when song finishes * refactor: use synced registries for jukebox song data * replace indexmap with btreemap per clippy.toml policy * fix import ordering for cargo fmt
This commit is contained in:
@@ -1,18 +1,40 @@
|
||||
use heck::ToPascalCase;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JukeboxSongData {
|
||||
length_in_seconds: f32,
|
||||
comparator_output: u8,
|
||||
}
|
||||
|
||||
pub(crate) fn build() -> TokenStream {
|
||||
println!("cargo:rerun-if-changed=../assets/jukebox_song.json");
|
||||
println!("cargo:rerun-if-changed=../assets/registry/1_21_11_synced_registries.json");
|
||||
|
||||
let songs: BTreeMap<String, u32> = serde_json::from_str(
|
||||
&fs::read_to_string("../assets/jukebox_song.json").expect("Missing jukebox_song.json"),
|
||||
)
|
||||
.expect("Failed to parse jukebox_song.json");
|
||||
|
||||
// Helper to handle numeric keys like "11" -> "Id11"
|
||||
let registries: BTreeMap<String, Value> = serde_json::from_str(
|
||||
&fs::read_to_string("../assets/registry/1_21_11_synced_registries.json")
|
||||
.expect("Missing synced_registries.json"),
|
||||
)
|
||||
.expect("Failed to parse synced_registries.json");
|
||||
|
||||
let song_data: BTreeMap<String, JukeboxSongData> = serde_json::from_value(
|
||||
registries
|
||||
.get("jukebox_song")
|
||||
.expect("Missing jukebox_song in synced registries")
|
||||
.clone(),
|
||||
)
|
||||
.expect("Failed to parse jukebox_song data");
|
||||
|
||||
let make_variant_ident = |name: &str| {
|
||||
let pascal = name.to_pascal_case();
|
||||
if pascal.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
@@ -54,6 +76,30 @@ pub(crate) fn build() -> TokenStream {
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let type_to_length = songs
|
||||
.keys()
|
||||
.map(|name| {
|
||||
let variant_name = make_variant_ident(name);
|
||||
let length = song_data
|
||||
.get(name)
|
||||
.map(|d| d.length_in_seconds as u32)
|
||||
.unwrap_or(0);
|
||||
quote! { Self::#variant_name => #length, }
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
let type_to_comparator = songs
|
||||
.keys()
|
||||
.map(|name| {
|
||||
let variant_name = make_variant_ident(name);
|
||||
let output = song_data
|
||||
.get(name)
|
||||
.map(|d| d.comparator_output)
|
||||
.unwrap_or(0);
|
||||
quote! { Self::#variant_name => #output, }
|
||||
})
|
||||
.collect::<TokenStream>();
|
||||
|
||||
quote! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[repr(u32)]
|
||||
@@ -83,6 +129,25 @@ pub(crate) fn build() -> TokenStream {
|
||||
#type_to_id
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = r" Returns the comparator output value (0-15) for this song."]
|
||||
pub const fn comparator_output(&self) -> u8 {
|
||||
match self {
|
||||
#type_to_comparator
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = r" Returns the song length in seconds."]
|
||||
pub const fn length_in_seconds(&self) -> u32 {
|
||||
match self {
|
||||
#type_to_length
|
||||
}
|
||||
}
|
||||
|
||||
#[doc = r" Returns the song length in ticks (20 ticks per second)."]
|
||||
pub const fn length_in_ticks(&self) -> u64 {
|
||||
self.length_in_seconds() as u64 * 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
233
pumpkin-world/src/block/entities/jukebox.rs
Normal file
233
pumpkin-world/src/block/entities/jukebox.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use std::any::Any;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::inventory::{Clearable, Inventory, InventoryFuture};
|
||||
use crate::world::SimpleWorld;
|
||||
use crate::{block::entities::BlockEntity, item::ItemStack};
|
||||
|
||||
/// Matches vanilla's JukeboxBlockEntity
|
||||
pub struct JukeboxBlockEntity {
|
||||
position: BlockPos,
|
||||
/// The record item stored in the jukebox (RecordItem in NBT)
|
||||
record_stack: Arc<Mutex<ItemStack>>,
|
||||
/// Ticks since the current song started playing
|
||||
ticks_since_song_started: AtomicU64,
|
||||
/// Length of the current song in ticks (0 if not playing)
|
||||
song_length_ticks: AtomicU64,
|
||||
dirty: AtomicBool,
|
||||
}
|
||||
|
||||
const RECORD_ITEM_NBT_KEY: &str = "RecordItem";
|
||||
const TICKS_SINCE_SONG_STARTED_NBT_KEY: &str = "ticks_since_song_started";
|
||||
|
||||
impl BlockEntity for JukeboxBlockEntity {
|
||||
fn resource_location(&self) -> &'static str {
|
||||
Self::ID
|
||||
}
|
||||
|
||||
fn get_position(&self) -> BlockPos {
|
||||
self.position
|
||||
}
|
||||
|
||||
fn from_nbt(nbt: &NbtCompound, position: BlockPos) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let record_stack = nbt
|
||||
.get_compound(RECORD_ITEM_NBT_KEY)
|
||||
.and_then(ItemStack::read_item_stack)
|
||||
.unwrap_or_else(|| ItemStack::EMPTY.clone());
|
||||
|
||||
let ticks_since_song_started =
|
||||
nbt.get_long(TICKS_SINCE_SONG_STARTED_NBT_KEY).unwrap_or(0) as u64;
|
||||
|
||||
Self {
|
||||
position,
|
||||
record_stack: Arc::new(Mutex::new(record_stack)),
|
||||
ticks_since_song_started: AtomicU64::new(ticks_since_song_started),
|
||||
song_length_ticks: AtomicU64::new(0), // Will be set when playing starts
|
||||
dirty: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_nbt<'a>(
|
||||
&'a self,
|
||||
nbt: &'a mut NbtCompound,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
let record = self.record_stack.lock().await;
|
||||
if !record.is_empty() {
|
||||
let mut record_nbt = NbtCompound::new();
|
||||
record.write_item_stack(&mut record_nbt);
|
||||
nbt.put(RECORD_ITEM_NBT_KEY, record_nbt);
|
||||
}
|
||||
|
||||
let ticks = self.ticks_since_song_started.load(Ordering::Relaxed);
|
||||
if ticks > 0 {
|
||||
nbt.put_long(TICKS_SINCE_SONG_STARTED_NBT_KEY, ticks as i64);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn tick<'a>(
|
||||
&'a self,
|
||||
_world: &'a Arc<dyn SimpleWorld>,
|
||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||
Box::pin(async move {
|
||||
// Increment ticks if we're playing
|
||||
let song_length = self.song_length_ticks.load(Ordering::Relaxed);
|
||||
if song_length > 0 {
|
||||
let ticks = self
|
||||
.ticks_since_song_started
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
// Check if song has finished
|
||||
if ticks >= song_length {
|
||||
self.stop_playing();
|
||||
// TODO: Update block state to has_record = false? Or just stop redstone?
|
||||
// In vanilla, the disc stays but music stops and redstone turns off
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
self.dirty.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn get_inventory(self: Arc<Self>) -> Option<Arc<dyn Inventory>> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl JukeboxBlockEntity {
|
||||
pub const ID: &'static str = "minecraft:jukebox";
|
||||
|
||||
#[must_use]
|
||||
pub fn new(position: BlockPos) -> Self {
|
||||
Self {
|
||||
position,
|
||||
record_stack: Arc::new(Mutex::new(ItemStack::EMPTY.clone())),
|
||||
ticks_since_song_started: AtomicU64::new(0),
|
||||
song_length_ticks: AtomicU64::new(0),
|
||||
dirty: AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current record stack
|
||||
pub async fn get_record(&self) -> ItemStack {
|
||||
self.record_stack.lock().await.clone()
|
||||
}
|
||||
|
||||
/// Set the record stack - matches vanilla's setStack()
|
||||
/// Note: The caller is responsible for updating block state and playing music
|
||||
pub async fn set_record(&self, stack: ItemStack) {
|
||||
*self.record_stack.lock().await = stack;
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Clear the stack and return what was there - used for dropping
|
||||
pub async fn clear_record(&self) -> ItemStack {
|
||||
self.stop_playing();
|
||||
let mut record = self.record_stack.lock().await;
|
||||
let taken = record.clone();
|
||||
*record = ItemStack::EMPTY.clone();
|
||||
self.mark_dirty();
|
||||
taken
|
||||
}
|
||||
|
||||
/// Start playing a song with the given length in ticks
|
||||
pub fn start_playing(&self, length_in_ticks: u64) {
|
||||
self.ticks_since_song_started.store(0, Ordering::Relaxed);
|
||||
self.song_length_ticks
|
||||
.store(length_in_ticks, Ordering::Relaxed);
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Stop playing the current song
|
||||
pub fn stop_playing(&self) {
|
||||
self.ticks_since_song_started.store(0, Ordering::Relaxed);
|
||||
self.song_length_ticks.store(0, Ordering::Relaxed);
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Check if a song is currently playing
|
||||
pub fn is_playing(&self) -> bool {
|
||||
let song_length = self.song_length_ticks.load(Ordering::Relaxed);
|
||||
if song_length == 0 {
|
||||
return false;
|
||||
}
|
||||
let ticks = self.ticks_since_song_started.load(Ordering::Relaxed);
|
||||
ticks < song_length
|
||||
}
|
||||
|
||||
fn mark_dirty(&self) {
|
||||
self.dirty.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Implements single-slot inventory for jukebox (matches vanilla's SingleStackInventory)
|
||||
impl Inventory for JukeboxBlockEntity {
|
||||
fn size(&self) -> usize {
|
||||
1
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> InventoryFuture<'_, bool> {
|
||||
Box::pin(async move { self.record_stack.lock().await.is_empty() })
|
||||
}
|
||||
|
||||
fn get_stack(&self, _slot: usize) -> InventoryFuture<'_, Arc<Mutex<ItemStack>>> {
|
||||
Box::pin(async move { self.record_stack.clone() })
|
||||
}
|
||||
|
||||
fn remove_stack(&self, _slot: usize) -> InventoryFuture<'_, ItemStack> {
|
||||
Box::pin(async move {
|
||||
self.stop_playing();
|
||||
let mut record = self.record_stack.lock().await;
|
||||
let taken = record.clone();
|
||||
*record = ItemStack::EMPTY.clone();
|
||||
self.mark_dirty();
|
||||
taken
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_stack_specific(&self, _slot: usize, _amount: u8) -> InventoryFuture<'_, ItemStack> {
|
||||
// Jukebox only holds one item, so remove the whole stack
|
||||
self.remove_stack(0)
|
||||
}
|
||||
|
||||
fn set_stack(&self, _slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
*self.record_stack.lock().await = stack;
|
||||
self.mark_dirty();
|
||||
})
|
||||
}
|
||||
|
||||
fn mark_dirty(&self) {
|
||||
self.dirty.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Clearable for JukeboxBlockEntity {
|
||||
fn clear(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
||||
Box::pin(async move {
|
||||
self.stop_playing();
|
||||
*self.record_stack.lock().await = ItemStack::EMPTY.clone();
|
||||
self.mark_dirty();
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use crate::block::entities::blasting_furnace::BlastingFurnaceBlockEntity;
|
||||
use crate::block::entities::command_block::CommandBlockEntity;
|
||||
use crate::block::entities::ender_chest::EnderChestBlockEntity;
|
||||
use crate::block::entities::hopper::HopperBlockEntity;
|
||||
use crate::block::entities::jukebox::JukeboxBlockEntity;
|
||||
use crate::block::entities::mob_spawner::MobSpawnerBlockEntity;
|
||||
use crate::block::entities::shulker_box::ShulkerBoxBlockEntity;
|
||||
use crate::block::entities::smoker::SmokerBlockEntity;
|
||||
@@ -39,6 +40,7 @@ pub mod ender_chest;
|
||||
pub mod furnace;
|
||||
pub mod furnace_like_block_entity;
|
||||
pub mod hopper;
|
||||
pub mod jukebox;
|
||||
pub mod mob_spawner;
|
||||
pub mod piston;
|
||||
pub mod shulker_box;
|
||||
@@ -136,6 +138,7 @@ pub fn block_entity_from_nbt(nbt: &NbtCompound) -> Option<Arc<dyn BlockEntity>>
|
||||
EnderChestBlockEntity::ID => {
|
||||
Arc::new(block_entity_from_generic::<EnderChestBlockEntity>(nbt))
|
||||
}
|
||||
JukeboxBlockEntity::ID => Arc::new(block_entity_from_generic::<JukeboxBlockEntity>(nbt)),
|
||||
SignBlockEntity::ID => Arc::new(block_entity_from_generic::<SignBlockEntity>(nbt)),
|
||||
BedBlockEntity::ID => Arc::new(block_entity_from_generic::<BedBlockEntity>(nbt)),
|
||||
ComparatorBlockEntity::ID => {
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::block::registry::BlockActionResult;
|
||||
use crate::block::{BlockBehaviour, BlockFuture, BrokenArgs, UseWithItemArgs};
|
||||
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::{
|
||||
@@ -12,94 +18,233 @@ use pumpkin_data::{
|
||||
};
|
||||
use pumpkin_macros::pumpkin_block;
|
||||
use pumpkin_util::math::position::BlockPos;
|
||||
use pumpkin_util::math::vector3::Vector3;
|
||||
use pumpkin_world::block::entities::jukebox::JukeboxBlockEntity;
|
||||
use pumpkin_world::world::BlockFlags;
|
||||
use rand::{RngExt, rng};
|
||||
|
||||
#[pumpkin_block("minecraft:jukebox")]
|
||||
pub struct JukeboxBlock;
|
||||
|
||||
impl JukeboxBlock {
|
||||
async fn has_record(&self, block: &Block, location: &BlockPos, world: &World) -> bool {
|
||||
let state_id = world.get_block_state(location).await.id;
|
||||
fn has_record_state(block: &Block, state_id: u16) -> bool {
|
||||
JukeboxLikeProperties::from_state_id(state_id, block).has_record
|
||||
}
|
||||
|
||||
async fn set_record(
|
||||
&self,
|
||||
async fn set_record_state(
|
||||
has_record: bool,
|
||||
block: &Block,
|
||||
location: &BlockPos,
|
||||
position: &BlockPos,
|
||||
world: &Arc<World>,
|
||||
) {
|
||||
let new_state = JukeboxLikeProperties { has_record };
|
||||
world
|
||||
.set_block_state(location, new_state.to_state_id(block), BlockFlags::empty())
|
||||
.set_block_state(
|
||||
position,
|
||||
new_state.to_state_id(block),
|
||||
BlockFlags::NOTIFY_LISTENERS,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn stop_music(&self, block: &Block, position: &BlockPos, world: &Arc<World>) {
|
||||
self.set_record(false, block, position, world).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).await
|
||||
&& 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).await);
|
||||
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::JukeboxStopsPlaying, *position, 0)
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Starts playing music
|
||||
async fn start_playing(position: &BlockPos, world: &Arc<World>, song_id: u32) {
|
||||
world
|
||||
.sync_world_event(WorldEvent::JukeboxStartsPlaying, *position, song_id as i32)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
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).await.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.player.living_entity.entity.world.load_full();
|
||||
let world = args.world;
|
||||
let state_id = world.get_block_state(args.position).await.id;
|
||||
|
||||
// if the jukebox already has a record, stop playing
|
||||
if self.has_record(args.block, args.position, &world).await {
|
||||
self.stop_music(args.block, args.position, &world).await;
|
||||
return BlockActionResult::Success;
|
||||
// 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 jukebox_playable = args
|
||||
.item_stack
|
||||
.lock()
|
||||
.await
|
||||
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::Pass;
|
||||
return BlockActionResult::PassToDefaultBlockAction;
|
||||
};
|
||||
|
||||
let Some(song) = jukebox_playable.split(':').nth(1) else {
|
||||
return BlockActionResult::Pass;
|
||||
let Some(song_name) = jukebox_playable.split(':').nth(1) else {
|
||||
return BlockActionResult::PassToDefaultBlockAction;
|
||||
};
|
||||
|
||||
let Some(jukebox_song) = JukeboxSong::from_name(song) else {
|
||||
log::error!("Jukebox playable song not registered!");
|
||||
return BlockActionResult::Pass;
|
||||
let Some(jukebox_song) = JukeboxSong::from_name(song_name) else {
|
||||
log::error!("Jukebox playable song not registered: {song_name}");
|
||||
return BlockActionResult::PassToDefaultBlockAction;
|
||||
};
|
||||
|
||||
// TODO: Update block nbt
|
||||
// Vanilla: ItemStack lv3 = stack.splitUnlessCreative(1, player)
|
||||
let record = item_stack.split_unless_creative(args.player.gamemode.load(), 1);
|
||||
|
||||
self.set_record(true, args.block, args.position, &world)
|
||||
.await;
|
||||
world
|
||||
.sync_world_event(
|
||||
WorldEvent::JukeboxStartsPlaying,
|
||||
*args.position,
|
||||
jukebox_song.get_id() as i32,
|
||||
)
|
||||
.await;
|
||||
// Vanilla: lv4.setStack(lv3)
|
||||
if let Some(block_entity) = world.get_block_entity(args.position).await
|
||||
&& 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()).await;
|
||||
|
||||
// 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 {
|
||||
// For now just stop the music at this position
|
||||
// Drop the record if there is one
|
||||
Self::drop_record(args.position, args.world).await;
|
||||
// Stop the music
|
||||
args.world
|
||||
.sync_world_event(WorldEvent::JukeboxStopsPlaying, *args.position, 0)
|
||||
.await;
|
||||
})
|
||||
}
|
||||
|
||||
/// 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).await
|
||||
&& 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).await
|
||||
&& 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user