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