From cb51228adb3338c7873ee0c8a56cc5571ecb1156 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sun, 11 May 2025 20:08:32 +0200 Subject: [PATCH] Add Noteblock support --- pumpkin-data/build/block.rs | 4 +- pumpkin-data/src/block_state.rs | 5 +- .../src/client/play/block_event.rs | 4 +- pumpkin/src/block/blocks/mod.rs | 1 + pumpkin/src/block/blocks/note.rs | 249 ++++++++++++++++++ pumpkin/src/block/mod.rs | 8 + pumpkin/src/block/pumpkin_block.rs | 26 +- pumpkin/src/block/registry.rs | 17 ++ pumpkin/src/world/mod.rs | 35 ++- 9 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 pumpkin/src/block/blocks/note.rs diff --git a/pumpkin-data/build/block.rs b/pumpkin-data/build/block.rs index bf048d579..874bf032e 100644 --- a/pumpkin-data/build/block.rs +++ b/pumpkin-data/build/block.rs @@ -355,12 +355,12 @@ impl ToTokens for CollisionShape { pub struct BlockState { pub id: u16, pub state_flags: u8, + pub instrument: String, // TODO: make this an enum pub luminance: u8, pub hardness: f32, pub collision_shapes: Vec, pub opacity: Option, pub block_entity_type: Option, - // pub instrument: String, // TODO: make this an enum } #[derive(Deserialize, Clone, Debug)] @@ -374,6 +374,7 @@ impl BlockState { let mut tokens = TokenStream::new(); let id = LitInt::new(&self.id.to_string(), Span::call_site()); let state_flags = LitInt::new(&self.state_flags.to_string(), Span::call_site()); + let instrument = self.instrument.clone(); let luminance = LitInt::new(&self.luminance.to_string(), Span::call_site()); let hardness = self.hardness; let opacity = match self.opacity { @@ -401,6 +402,7 @@ impl BlockState { BlockState { id: #id, state_flags: #state_flags, + instrument: #instrument, luminance: #luminance, hardness: #hardness, collision_shapes: &[#(#collision_shapes),*], diff --git a/pumpkin-data/src/block_state.rs b/pumpkin-data/src/block_state.rs index 65a54cac9..02982a8c0 100644 --- a/pumpkin-data/src/block_state.rs +++ b/pumpkin-data/src/block_state.rs @@ -2,12 +2,13 @@ pub struct BlockState { pub id: u16, pub state_flags: u8, + pub instrument: &'static str, pub luminance: u8, pub hardness: f32, pub collision_shapes: &'static [u16], - //u8::MAX is used as None + /// u8::MAX is used as None pub opacity: u8, - //u16::MAX is used as None + /// u16::MAX is used as None pub block_entity_type: u16, } diff --git a/pumpkin-protocol/src/client/play/block_event.rs b/pumpkin-protocol/src/client/play/block_event.rs index c1e9799b4..d392116c0 100644 --- a/pumpkin-protocol/src/client/play/block_event.rs +++ b/pumpkin-protocol/src/client/play/block_event.rs @@ -8,14 +8,14 @@ use crate::VarInt; #[derive(Serialize)] #[packet(PLAY_BLOCK_EVENT)] -pub struct CBlockAction { +pub struct CBlockEvent { location: BlockPos, action_id: u8, action_parameter: u8, block_type: VarInt, } -impl CBlockAction { +impl CBlockEvent { pub fn new( location: BlockPos, action_id: u8, diff --git a/pumpkin/src/block/blocks/mod.rs b/pumpkin/src/block/blocks/mod.rs index 470dbaa74..046db3547 100644 --- a/pumpkin/src/block/blocks/mod.rs +++ b/pumpkin/src/block/blocks/mod.rs @@ -15,6 +15,7 @@ pub mod iron_bars; pub mod jukebox; pub mod logs; pub mod nether_portal; +pub mod note; pub mod redstone; pub mod signs; pub mod slabs; diff --git a/pumpkin/src/block/blocks/note.rs b/pumpkin/src/block/blocks/note.rs new file mode 100644 index 000000000..33f9a295b --- /dev/null +++ b/pumpkin/src/block/blocks/note.rs @@ -0,0 +1,249 @@ +use std::sync::Arc; + +use crate::block::BlockIsReplacing; +use crate::server::Server; +use crate::{block::registry::BlockActionResult, entity::player::Player}; +use async_trait::async_trait; +use pumpkin_data::block_properties::Axis; +use pumpkin_data::item::Item; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{ + Block, + block_properties::{ + BlockProperties, EnumVariants, Instrument, Integer0To24, NoteBlockLikeProperties, + }, +}; +use pumpkin_macros::pumpkin_block; +use pumpkin_protocol::server::play::SUseItemOn; +use pumpkin_util::math::position::BlockPos; +use pumpkin_world::BlockStateId; +use pumpkin_world::block::BlockDirection; + +use crate::{ + block::pumpkin_block::PumpkinBlock, + world::{BlockFlags, World}, +}; + +use super::redstone::block_receives_redstone_power; + +#[pumpkin_block("minecraft:note_block")] +pub struct NoteBlock; + +impl NoteBlock { + pub async fn play_note(props: &NoteBlockLikeProperties, world: &World, pos: &BlockPos) { + if !is_base_block(props.instrument) + || world.get_block_state(&pos.up()).await.unwrap().is_air() + { + world.add_synced_block_event(*pos, 0, 0).await; + } + } + fn get_note_pitch(note: u16) -> f32 { + 2.0f64.powf((f64::from(note) - 12.0) / 12.0) as f32 + } + + async fn get_state_with_instrument( + world: &World, + pos: &BlockPos, + state: BlockStateId, + block: &Block, + ) -> BlockStateId { + let upper_instrument = Instrument::from_value( + &world + .get_block_state(&pos.up()) + .await + .unwrap() + .instrument + .to_lowercase(), + ); + let mut note_props = NoteBlockLikeProperties::from_state_id(state, block); + if !is_base_block(upper_instrument) { + note_props.instrument = upper_instrument; + return note_props.to_state_id(block); + } + let below_instrument = Instrument::from_value( + &world + .get_block_state(&pos.down()) + .await + .unwrap() + .instrument + .to_lowercase(), + ); + let below_instrument = if is_base_block(below_instrument) { + below_instrument + } else { + Instrument::Harp + }; + note_props.instrument = below_instrument; + note_props.to_state_id(block) + } +} + +#[async_trait] +impl PumpkinBlock for NoteBlock { + async fn on_neighbor_update( + &self, + world: &Arc, + block: &Block, + pos: &BlockPos, + _source_block: &Block, + _notify: bool, + ) { + let block_state = world.get_block_state(pos).await.unwrap(); + let mut note_props = NoteBlockLikeProperties::from_state_id(block_state.id, block); + let powered = block_receives_redstone_power(world, pos).await; + // check if powered state changed + if note_props.powered != powered { + if powered { + Self::play_note(¬e_props, world, pos).await; + } + note_props.powered = powered; + world + .set_block_state(pos, note_props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + } + } + + async fn normal_use( + &self, + block: &Block, + _player: &Player, + pos: BlockPos, + _server: &Server, + world: &Arc, + ) { + let block_state = world.get_block_state(&pos).await.unwrap(); + let mut note_props = NoteBlockLikeProperties::from_state_id(block_state.id, block); + let next_index = note_props.note.to_index() + 1; + // Increment and check if max + note_props.note = if next_index >= Integer0To24::variant_count() { + Integer0To24::from_index(0) + } else { + Integer0To24::from_index(next_index) + }; + world + .set_block_state(&pos, note_props.to_state_id(block), BlockFlags::NOTIFY_ALL) + .await; + Self::play_note(¬e_props, world, &pos).await; + } + + async fn use_with_item( + &self, + _block: &Block, + _player: &Player, + _location: BlockPos, + _item: &Item, + _server: &Server, + _world: &Arc, + ) -> BlockActionResult { + BlockActionResult::Continue + } + + async fn on_synced_block_event( + &self, + block: &Block, + world: &Arc, + pos: &BlockPos, + _type: u8, + _data: u8, + ) -> bool { + let block_state = world.get_block_state(pos).await.unwrap(); + let note_props = NoteBlockLikeProperties::from_state_id(block_state.id, block); + let instrument = note_props.instrument; + let pitch = if is_base_block(instrument) { + // checks if can be pitched + Self::get_note_pitch(note_props.note.to_index()) + } else { + 1.0 // default pitch + }; + // check hasCustomSound + world + .play_sound_raw( + convert_instrument_to_sound(instrument) as u16, + SoundCategory::Records, + &pos.to_f64(), + 3.0, + pitch, + ) + .await; + true + } + + async fn on_place( + &self, + _server: &Server, + world: &World, + _player: &Player, + block: &Block, + pos: &BlockPos, + _face: BlockDirection, + _replacing: BlockIsReplacing, + _use_item_on: &SUseItemOn, + ) -> BlockStateId { + Self::get_state_with_instrument(world, pos, Block::NOTE_BLOCK.default_state_id, block).await + } + + async fn get_state_for_neighbor_update( + &self, + world: &World, + block: &Block, + state: BlockStateId, + pos: &BlockPos, + direction: BlockDirection, + _neighbor_pos: &BlockPos, + _neighbor_state: BlockStateId, + ) -> BlockStateId { + if direction.to_axis() == Axis::Y { + return Self::get_state_with_instrument(world, pos, state, block).await; + } + state + } +} + +fn convert_instrument_to_sound(instrument: Instrument) -> Sound { + match instrument { + Instrument::Harp => Sound::BlockNoteBlockHarp, + Instrument::Basedrum => Sound::BlockNoteBlockBasedrum, + Instrument::Snare => Sound::BlockNoteBlockSnare, + Instrument::Hat => Sound::BlockNoteBlockHat, + Instrument::Bass => Sound::BlockNoteBlockBass, + Instrument::Flute => Sound::BlockNoteBlockFlute, + Instrument::Bell => Sound::BlockNoteBlockBell, + Instrument::Guitar => Sound::BlockNoteBlockGuitar, + Instrument::Chime => Sound::BlockNoteBlockChime, + Instrument::Xylophone => Sound::BlockNoteBlockXylophone, + Instrument::IronXylophone => Sound::BlockNoteBlockIronXylophone, + Instrument::CowBell => Sound::BlockNoteBlockCowBell, + Instrument::Didgeridoo => Sound::BlockNoteBlockDidgeridoo, + Instrument::Bit => Sound::BlockNoteBlockBit, + Instrument::Banjo => Sound::BlockNoteBlockBanjo, + Instrument::Pling => Sound::BlockNoteBlockPling, + Instrument::Zombie => Sound::BlockNoteBlockImitateZombie, + Instrument::Skeleton => Sound::BlockNoteBlockImitateSkeleton, + Instrument::Creeper => Sound::BlockNoteBlockImitateCreeper, + Instrument::Dragon => Sound::BlockNoteBlockImitateEnderDragon, + Instrument::WitherSkeleton => Sound::BlockNoteBlockImitateWitherSkeleton, + Instrument::Piglin => Sound::BlockNoteBlockImitatePiglin, + Instrument::CustomHead => Sound::UiButtonClick, + } +} + +fn is_base_block(instrument: Instrument) -> bool { + matches!( + instrument, + Instrument::Harp + | Instrument::Basedrum + | Instrument::Snare + | Instrument::Hat + | Instrument::Bass + | Instrument::Flute + | Instrument::Bell + | Instrument::Guitar + | Instrument::Chime + | Instrument::Xylophone + | Instrument::IronXylophone + | Instrument::CowBell + | Instrument::Didgeridoo + | Instrument::Bit + | Instrument::Banjo + ) +} diff --git a/pumpkin/src/block/mod.rs b/pumpkin/src/block/mod.rs index 0e04e3ffa..14755d56a 100644 --- a/pumpkin/src/block/mod.rs +++ b/pumpkin/src/block/mod.rs @@ -12,6 +12,7 @@ use blocks::glass_panes::GlassPaneBlock; use blocks::iron_bars::IronBarsBlock; use blocks::logs::LogBlock; use blocks::nether_portal::NetherPortalBlock; +use blocks::note::NoteBlock; use blocks::redstone::buttons::ButtonBlock; use blocks::redstone::observer::ObserverBlock; use blocks::redstone::piston::PistonBlock; @@ -88,6 +89,7 @@ pub fn default_registry() -> Arc { manager.register(TorchBlock); manager.register(WallBlock); manager.register(NetherPortalBlock); + manager.register(NoteBlock); // Fire manager.register(SoulFireBlock); @@ -118,6 +120,12 @@ pub fn default_registry() -> Arc { Arc::new(manager) } +pub struct BlockEvent { + pub pos: BlockPos, + pub r#type: u8, + pub data: u8, +} + pub async fn drop_loot( world: &Arc, block: &Block, diff --git a/pumpkin/src/block/pumpkin_block.rs b/pumpkin/src/block/pumpkin_block.rs index 61323ca2c..3de64c085 100644 --- a/pumpkin/src/block/pumpkin_block.rs +++ b/pumpkin/src/block/pumpkin_block.rs @@ -36,12 +36,6 @@ pub trait PumpkinBlock: Send + Sync { ) { } - fn should_drop_items_on_explosion(&self) -> bool { - true - } - - async fn explode(&self, _block: &Block, _world: &Arc, _location: BlockPos) {} - async fn use_with_item( &self, _block: &Block, @@ -54,6 +48,26 @@ pub trait PumpkinBlock: Send + Sync { BlockActionResult::Continue } + fn should_drop_items_on_explosion(&self) -> bool { + true + } + + async fn explode(&self, _block: &Block, _world: &Arc, _location: BlockPos) {} + + /// Handles the block event, which is an event specific to a block with an integer ID and data. + /// + /// returns whether the event was handled successfully + async fn on_synced_block_event( + &self, + _block: &Block, + _world: &Arc, + _pos: &BlockPos, + _type: u8, + _data: u8, + ) -> bool { + false + } + #[allow(clippy::too_many_arguments)] /// getPlacementState in source code async fn on_place( diff --git a/pumpkin/src/block/registry.rs b/pumpkin/src/block/registry.rs index 11dfe63b6..5451bda44 100644 --- a/pumpkin/src/block/registry.rs +++ b/pumpkin/src/block/registry.rs @@ -37,6 +37,23 @@ impl BlockRegistry { self.fluids.insert(fluid.names(), Arc::new(fluid)); } + pub async fn on_synced_block_event( + &self, + block: &Block, + world: &Arc, + pos: &BlockPos, + r#type: u8, + data: u8, + ) -> bool { + let pumpkin_block = self.get_pumpkin_block(block); + if let Some(pumpkin_block) = pumpkin_block { + return pumpkin_block + .on_synced_block_event(block, world, pos, r#type, data) + .await; + } + false + } + pub async fn on_use( &self, block: &Block, diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index bc78470b6..f0baf3362 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -9,6 +9,7 @@ pub mod explosion; pub mod portal; pub mod time; +use crate::block::BlockEvent; use crate::{ PLUGIN_MANAGER, block::{self, registry::BlockRegistry}, @@ -43,7 +44,7 @@ use pumpkin_data::{ use pumpkin_macros::send_cancellable; use pumpkin_nbt::to_bytes_unnamed; use pumpkin_protocol::client::play::{ - CRemoveMobEffect, CSetEntityMetadata, MetaDataType, Metadata, + CBlockEvent, CRemoveMobEffect, CSetEntityMetadata, MetaDataType, Metadata, }; use pumpkin_protocol::codec::identifier::Identifier; use pumpkin_protocol::ser::serializer::Serializer; @@ -166,6 +167,7 @@ pub struct World { pub weather: Mutex, /// Block Behaviour pub block_registry: Arc, + synced_block_event_queue: Mutex>, /// A map of unsent block changes, keyed by block position. unsent_block_changes: Mutex>, // TODO: entities @@ -193,6 +195,7 @@ impl World { weather: Mutex::new(Weather::new()), block_registry, sea_level: generation_settings.sea_level, + synced_block_event_queue: Mutex::new(Vec::new()), unsent_block_changes: Mutex::new(HashMap::new()), } } @@ -216,6 +219,35 @@ impl World { .await; } + pub async fn add_synced_block_event(&self, pos: BlockPos, r#type: u8, data: u8) { + let mut queue = self.synced_block_event_queue.lock().await; + queue.push(BlockEvent { pos, r#type, data }); + } + + async fn flush_synced_block_events(self: &Arc) { + let mut queue = self.synced_block_event_queue.lock().await; + let events: Vec = queue.drain(..).collect(); + // lets drop it here, allows us to add new things + drop(queue); + for event in events { + let block = self.get_block(&event.pos).await.unwrap(); // TODO + if !self + .block_registry + .on_synced_block_event(&block, self, &event.pos, event.r#type, event.data) + .await + { + continue; + } + self.broadcast_packet_all(&CBlockEvent::new( + event.pos, + event.r#type, + event.data, + VarInt(i32::from(block.id)), + )) + .await; + } + } + /// Broadcasts a packet to all connected players within the world. /// /// Sends the specified packet to every player currently logged in to the world. @@ -364,6 +396,7 @@ impl World { pub async fn tick(self: &Arc, server: &Server) { self.flush_block_updates().await; + self.flush_synced_block_events().await; // world ticks {