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(level: &dyn LevelReader, pos: BlockPos, state: BlockStateId) -> BlockStateId {
51 let instrument_above = Self::block_instrument(level.get_block_state(pos.above()));
52 if instrument_above.works_above_note_block() {
53 return state.set_value(NOTEBLOCK_INSTRUMENT, instrument_above);
54 }
55
56 let instrument_below = Self::block_instrument(level.get_block_state(pos.below()));
57 let instrument = if instrument_below.works_above_note_block() {
58 NoteBlockInstrument::Harp
59 } else {
60 instrument_below
61 };
62 state.set_value(NOTEBLOCK_INSTRUMENT, instrument)
63 }
64
65 fn cycle_note(state: BlockStateId) -> BlockStateId {
66 let note = state.get_value(NOTE);
67 let next = if note == NOTE.max { NOTE.min } else { note + 1 };
68 state.set_value(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(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(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(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 player.award_custom_stat(&vanilla_custom_stats::TUNE_NOTEBLOCK);
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 player.award_custom_stat(&vanilla_custom_stats::PLAY_NOTEBLOCK);
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(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(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(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(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(NOTE, NOTE.max);
289
290 assert_eq!(NoteBlock::cycle_note(highest).get_value(NOTE), NOTE.min);
291 }
292
293 #[test]
294 fn redstone_updates_powered_state_on_both_edges() {
295 init_vanilla_registry();
296 init_behaviors();
297 let world = fresh_test_world("note_block_redstone_edges");
298 let pos = BlockPos::new(8, 64, 8);
299 let power_pos = pos.west();
300 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(pos));
301 assert!(world.set_block(
302 pos,
303 vanilla_blocks::NOTE_BLOCK.default_state(),
304 UpdateFlags::UPDATE_ALL,
305 ));
306
307 assert!(world.set_block(
308 power_pos,
309 vanilla_blocks::REDSTONE_BLOCK.default_state(),
310 UpdateFlags::UPDATE_ALL,
311 ));
312 assert!(world.get_block_state(pos).get_value(POWERED));
313 world.run_block_events();
314
315 assert!(world.remove_block(power_pos, false));
316 assert!(!world.get_block_state(pos).get_value(POWERED));
317 }
318}