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::{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#[block_behavior]
28pub struct NoteBlock {
29 block: BlockRef,
30}
31
32impl NoteBlock {
33 #[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 #[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 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 }
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 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 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}