Skip to main content

steel_core/behavior/blocks/redstone/
note_block.rs

1//! Vanilla note-block behavior.
2
3use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_protocol::packets::game::SoundSource;
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::block_state_ext::BlockStateExt as _;
9use steel_registry::blocks::properties::{
10    BlockStateProperties, BoolProperty, Direction, EnumProperty, IntProperty, NoteBlockInstrument,
11};
12use steel_registry::sound_event::SoundEventRef;
13use steel_registry::vanilla_item_tags::ItemTag;
14use steel_registry::{sound_events, vanilla_custom_stats, vanilla_game_events};
15use steel_utils::types::{InteractionHand, UpdateFlags};
16use steel_utils::{BlockPos, BlockStateId};
17
18use crate::behavior::{
19    BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
20};
21use crate::entity::Entity;
22use crate::player::Player;
23use crate::world::game_event::GameEventContext;
24use crate::world::{LevelReader, ScheduledTickAccess, SignalGetter as _, World};
25
26const NOTE_VOLUME: f32 = 3.0;
27
28/// Vanilla `NoteBlock`, including tuning, redstone playback, and instrument selection.
29#[block_behavior]
30pub struct NoteBlock {
31    block: BlockRef,
32}
33
34const NOTE: &IntProperty = &BlockStateProperties::NOTE;
35const NOTEBLOCK_INSTRUMENT: &EnumProperty<NoteBlockInstrument> =
36    &BlockStateProperties::NOTEBLOCK_INSTRUMENT;
37const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
38
39impl NoteBlock {
40    /// Creates note-block behavior for `block`.
41    #[must_use]
42    pub const fn new(block: BlockRef) -> Self {
43        Self { block }
44    }
45
46    fn block_instrument(state: BlockStateId) -> NoteBlockInstrument {
47        state.get_block().config.instrument
48    }
49
50    fn set_instrument(
51        level: &(impl LevelReader + ?Sized),
52        pos: BlockPos,
53        state: BlockStateId,
54    ) -> BlockStateId {
55        let instrument_above = Self::block_instrument(level.get_block_state(pos.above()));
56        if instrument_above.works_above_note_block() {
57            return state.set_value(NOTEBLOCK_INSTRUMENT, instrument_above);
58        }
59
60        let instrument_below = Self::block_instrument(level.get_block_state(pos.below()));
61        let instrument = if instrument_below.works_above_note_block() {
62            NoteBlockInstrument::Harp
63        } else {
64            instrument_below
65        };
66        state.set_value(NOTEBLOCK_INSTRUMENT, instrument)
67    }
68
69    fn cycle_note(state: BlockStateId) -> BlockStateId {
70        let note = state.get_value(NOTE);
71        let next = if note == NOTE.max { NOTE.min } else { note + 1 };
72        state.set_value(NOTE, next)
73    }
74
75    fn play_note(
76        &self,
77        source: Option<&dyn Entity>,
78        state: BlockStateId,
79        world: &Arc<World>,
80        pos: BlockPos,
81    ) {
82        let instrument = state.get_value(NOTEBLOCK_INSTRUMENT);
83        if !instrument.works_above_note_block() && !world.get_block_state(pos.above()).is_air() {
84            return;
85        }
86
87        world.block_event(pos, self.block, 0, 0);
88        world.game_event(
89            &vanilla_game_events::NOTE_BLOCK_PLAY,
90            pos,
91            &GameEventContext::new(source, None),
92        );
93    }
94
95    /// Calculates the pitch for the given note index.
96    #[must_use]
97    pub fn pitch_from_note(note: u8) -> f32 {
98        2.0_f64.powf((f64::from(note) - 12.0) / 12.0) as f32
99    }
100
101    fn sound_event(instrument: NoteBlockInstrument) -> Option<SoundEventRef> {
102        Some(match instrument {
103            NoteBlockInstrument::Harp => &sound_events::BLOCK_NOTE_BLOCK_HARP,
104            NoteBlockInstrument::Basedrum => &sound_events::BLOCK_NOTE_BLOCK_BASEDRUM,
105            NoteBlockInstrument::Snare => &sound_events::BLOCK_NOTE_BLOCK_SNARE,
106            NoteBlockInstrument::Hat => &sound_events::BLOCK_NOTE_BLOCK_HAT,
107            NoteBlockInstrument::Bass => &sound_events::BLOCK_NOTE_BLOCK_BASS,
108            NoteBlockInstrument::Flute => &sound_events::BLOCK_NOTE_BLOCK_FLUTE,
109            NoteBlockInstrument::Bell => &sound_events::BLOCK_NOTE_BLOCK_BELL,
110            NoteBlockInstrument::Guitar => &sound_events::BLOCK_NOTE_BLOCK_GUITAR,
111            NoteBlockInstrument::Chime => &sound_events::BLOCK_NOTE_BLOCK_CHIME,
112            NoteBlockInstrument::Xylophone => &sound_events::BLOCK_NOTE_BLOCK_XYLOPHONE,
113            NoteBlockInstrument::IronXylophone => &sound_events::BLOCK_NOTE_BLOCK_IRON_XYLOPHONE,
114            NoteBlockInstrument::CowBell => &sound_events::BLOCK_NOTE_BLOCK_COW_BELL,
115            NoteBlockInstrument::Didgeridoo => &sound_events::BLOCK_NOTE_BLOCK_DIDGERIDOO,
116            NoteBlockInstrument::Bit => &sound_events::BLOCK_NOTE_BLOCK_BIT,
117            NoteBlockInstrument::Banjo => &sound_events::BLOCK_NOTE_BLOCK_BANJO,
118            NoteBlockInstrument::Pling => &sound_events::BLOCK_NOTE_BLOCK_PLING,
119            NoteBlockInstrument::Trumpet => &sound_events::BLOCK_NOTE_BLOCK_TRUMPET,
120            NoteBlockInstrument::TrumpetExposed => &sound_events::BLOCK_NOTE_BLOCK_TRUMPET_EXPOSED,
121            NoteBlockInstrument::TrumpetOxidized => {
122                &sound_events::BLOCK_NOTE_BLOCK_TRUMPET_OXIDIZED
123            }
124            NoteBlockInstrument::TrumpetWeathered => {
125                &sound_events::BLOCK_NOTE_BLOCK_TRUMPET_WEATHERED
126            }
127            NoteBlockInstrument::Zombie => &sound_events::BLOCK_NOTE_BLOCK_IMITATE_ZOMBIE,
128            NoteBlockInstrument::Skeleton => &sound_events::BLOCK_NOTE_BLOCK_IMITATE_SKELETON,
129            NoteBlockInstrument::Creeper => &sound_events::BLOCK_NOTE_BLOCK_IMITATE_CREEPER,
130            NoteBlockInstrument::Dragon => &sound_events::BLOCK_NOTE_BLOCK_IMITATE_ENDER_DRAGON,
131            NoteBlockInstrument::WitherSkeleton => {
132                &sound_events::BLOCK_NOTE_BLOCK_IMITATE_WITHER_SKELETON
133            }
134            NoteBlockInstrument::Piglin => &sound_events::BLOCK_NOTE_BLOCK_IMITATE_PIGLIN,
135            NoteBlockInstrument::CustomHead => return None,
136        })
137    }
138}
139
140impl BlockBehavior for NoteBlock {
141    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
142        Some(Self::set_instrument(
143            context.world,
144            context.place_pos(),
145            self.block.default_state(),
146        ))
147    }
148
149    fn update_shape(
150        &self,
151        state: BlockStateId,
152        world: &dyn ScheduledTickAccess,
153        pos: BlockPos,
154        direction: Direction,
155        _neighbor_pos: BlockPos,
156        _neighbor_state: BlockStateId,
157    ) -> BlockStateId {
158        if direction.axis().is_vertical() {
159            Self::set_instrument(world, pos, state)
160        } else {
161            state
162        }
163    }
164
165    fn handle_neighbor_changed(
166        &self,
167        state: BlockStateId,
168        world: &Arc<World>,
169        pos: BlockPos,
170        _source_block: BlockRef,
171        _moved_by_piston: bool,
172    ) {
173        let signal = world.has_neighbor_signal(pos);
174        if signal == state.get_value(POWERED) {
175            return;
176        }
177
178        if signal {
179            self.play_note(None, state, world, pos);
180        }
181        world.set_block(
182            pos,
183            state.set_value(POWERED, signal),
184            UpdateFlags::UPDATE_ALL,
185        );
186    }
187
188    fn use_item_on(
189        &self,
190        _state: BlockStateId,
191        _world: &Arc<World>,
192        _pos: BlockPos,
193        _player: &Player,
194        _hand: InteractionHand,
195        hit_result: &BlockHitResult,
196        inv: &mut InventoryAccess,
197    ) -> InteractionResult {
198        if hit_result.direction == Direction::Up
199            && inv.with_item(|item| item.item().has_tag(&ItemTag::NOTEBLOCK_TOP_INSTRUMENTS))
200        {
201            InteractionResult::Pass
202        } else {
203            InteractionResult::TryEmptyHandInteraction
204        }
205    }
206
207    fn use_without_item(
208        &self,
209        state: BlockStateId,
210        world: &Arc<World>,
211        pos: BlockPos,
212        player: &Player,
213        _hit_result: &BlockHitResult,
214        _inv: &mut InventoryAccess,
215    ) -> InteractionResult {
216        let tuned_state = Self::cycle_note(state);
217        world.set_block(pos, tuned_state, UpdateFlags::UPDATE_ALL);
218        self.play_note(Some(player), tuned_state, world, pos);
219        player.award_custom_stat(&vanilla_custom_stats::TUNE_NOTEBLOCK);
220        InteractionResult::Success
221    }
222
223    fn attack(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, player: &Player) {
224        self.play_note(Some(player), state, world, pos);
225        player.award_custom_stat(&vanilla_custom_stats::PLAY_NOTEBLOCK);
226    }
227
228    fn trigger_event(
229        &self,
230        state: BlockStateId,
231        world: &Arc<World>,
232        pos: BlockPos,
233        _param_a: i32,
234        _param_b: i32,
235    ) -> bool {
236        let instrument = state.get_value(NOTEBLOCK_INSTRUMENT);
237        let Some(sound) = Self::sound_event(instrument) else {
238            // Custom player-head sounds require the skull block entity's note-block sound.
239            return false;
240        };
241        let pitch = if instrument.is_tunable() {
242            Self::pitch_from_note(state.get_value(NOTE))
243        } else {
244            1.0
245        };
246
247        world.play_sound(sound, SoundSource::Records, pos, NOTE_VOLUME, pitch, None);
248        // The block-event packet makes the client create Vanilla's local note particle.
249        true
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use steel_registry::init_vanilla_registry;
256    use steel_registry::vanilla_blocks;
257    use steel_utils::ChunkPos;
258
259    use super::*;
260    use crate::behavior::init_behaviors;
261    use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
262
263    #[test]
264    fn vertical_blocks_select_instruments_with_vanilla_priority() {
265        init_vanilla_registry();
266        let pos = BlockPos::new(2, 64, 3);
267        let note_state = vanilla_blocks::NOTE_BLOCK.default_state();
268        let level = TestLevel::default()
269            .with_block(pos.above(), vanilla_blocks::ZOMBIE_HEAD.default_state())
270            .with_block(pos.below(), vanilla_blocks::CLAY.default_state());
271
272        let selected = NoteBlock::set_instrument(&level, pos, note_state);
273        assert_eq!(
274            selected.get_value(NOTEBLOCK_INSTRUMENT),
275            NoteBlockInstrument::Zombie
276        );
277
278        let below_head = TestLevel::default()
279            .with_block(pos.below(), vanilla_blocks::ZOMBIE_HEAD.default_state());
280        let selected = NoteBlock::set_instrument(&below_head, pos, note_state);
281        assert_eq!(
282            selected.get_value(NOTEBLOCK_INSTRUMENT),
283            NoteBlockInstrument::Harp
284        );
285    }
286
287    #[test]
288    fn tuning_wraps_after_the_top_note() {
289        init_vanilla_registry();
290        let highest = vanilla_blocks::NOTE_BLOCK
291            .default_state()
292            .set_value(NOTE, NOTE.max);
293
294        assert_eq!(NoteBlock::cycle_note(highest).get_value(NOTE), NOTE.min);
295    }
296
297    #[test]
298    fn redstone_updates_powered_state_on_both_edges() {
299        init_vanilla_registry();
300        init_behaviors();
301        let world_fixture = fresh_test_world("note_block_redstone_edges");
302        let world = &world_fixture.world;
303        let pos = BlockPos::new(8, 64, 8);
304        let power_pos = pos.west();
305        insert_ready_full_chunk(world, ChunkPos::from_block_pos(pos));
306        assert!(world.set_block(
307            pos,
308            vanilla_blocks::NOTE_BLOCK.default_state(),
309            UpdateFlags::UPDATE_ALL,
310        ));
311
312        assert!(world.set_block(
313            power_pos,
314            vanilla_blocks::REDSTONE_BLOCK.default_state(),
315            UpdateFlags::UPDATE_ALL,
316        ));
317        assert!(world.get_block_state(pos).get_value(POWERED));
318        world.run_block_events();
319
320        assert!(world.remove_block(power_pos, false));
321        assert!(!world.get_block_state(pos).get_value(POWERED));
322    }
323}