steel_core/behavior/blocks/redstone/
note_block.rs1use 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#[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 #[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 #[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 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 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}