Add Noteblock support

This commit is contained in:
Alexander Medvedev 2025-05-11 20:08:32 +02:00
parent 575f17948f
commit cb51228adb
9 changed files with 337 additions and 12 deletions

View File

@ -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<u16>,
pub opacity: Option<u8>,
pub block_entity_type: Option<u16>,
// 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),*],

View File

@ -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,
}

View File

@ -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,

View File

@ -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;

View File

@ -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<World>,
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(&note_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<World>,
) {
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(&note_props, world, &pos).await;
}
async fn use_with_item(
&self,
_block: &Block,
_player: &Player,
_location: BlockPos,
_item: &Item,
_server: &Server,
_world: &Arc<World>,
) -> BlockActionResult {
BlockActionResult::Continue
}
async fn on_synced_block_event(
&self,
block: &Block,
world: &Arc<World>,
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
)
}

View File

@ -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<BlockRegistry> {
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<BlockRegistry> {
Arc::new(manager)
}
pub struct BlockEvent {
pub pos: BlockPos,
pub r#type: u8,
pub data: u8,
}
pub async fn drop_loot(
world: &Arc<World>,
block: &Block,

View File

@ -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<World>, _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<World>, _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<World>,
_pos: &BlockPos,
_type: u8,
_data: u8,
) -> bool {
false
}
#[allow(clippy::too_many_arguments)]
/// getPlacementState in source code
async fn on_place(

View File

@ -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<World>,
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,

View File

@ -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<Weather>,
/// Block Behaviour
pub block_registry: Arc<BlockRegistry>,
synced_block_event_queue: Mutex<Vec<BlockEvent>>,
/// A map of unsent block changes, keyed by block position.
unsent_block_changes: Mutex<HashMap<BlockPos, u16>>,
// 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<Self>) {
let mut queue = self.synced_block_event_queue.lock().await;
let events: Vec<BlockEvent> = 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<Self>, server: &Server) {
self.flush_block_updates().await;
self.flush_synced_block_events().await;
// world ticks
{