1use std::sync::Arc;
4
5use steel_macros::block_behavior;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{
9 BlockStateProperties, BoolProperty, Direction, EnumProperty,
10};
11use steel_registry::blocks::shapes::SupportType;
12use steel_registry::{REGISTRY, level_events, vanilla_blocks};
13use steel_utils::types::UpdateFlags;
14use steel_utils::{BlockPos, BlockStateId};
15
16use crate::behavior::blocks::redstone::{MAX_REDSTONE_SIGNAL, MIN_REDSTONE_SIGNAL};
17use crate::behavior::{BlockBehavior, BlockPlaceContext};
18use crate::world::{
19 LevelReader, ScheduledTickAccess, SignalQueryContext, World, get_signal as get_redstone_signal,
20};
21
22const TOGGLE_DELAY: i32 = 2;
23const RESTART_DELAY: i32 = 160;
24
25fn notify_neighbors(block: BlockRef, world: &Arc<World>, pos: BlockPos) {
26 for direction in Direction::ALL {
27 world.update_neighbors_at(pos.relative(direction), block);
28 }
29}
30
31fn on_place(block: BlockRef, world: &Arc<World>, pos: BlockPos) {
32 notify_neighbors(block, world, pos);
33}
34
35fn affect_neighbors_after_removal(
36 block: BlockRef,
37 world: &Arc<World>,
38 pos: BlockPos,
39 moved_by_piston: bool,
40) {
41 if !moved_by_piston {
42 notify_neighbors(block, world, pos);
43 }
44}
45
46fn handle_neighbor_changed(
47 block: BlockRef,
48 state: BlockStateId,
49 world: &Arc<World>,
50 pos: BlockPos,
51 has_neighbor_signal: bool,
52) {
53 if state.get_value(LIT) == has_neighbor_signal && !world.will_tick_block_this_tick(pos, block) {
54 world.schedule_block_tick_default(pos, block, TOGGLE_DELAY);
55 }
56}
57
58fn tick_torch(state: BlockStateId, world: &Arc<World>, pos: BlockPos, has_neighbor_signal: bool) {
59 world.prune_recent_redstone_torch_toggles();
60
61 if state.get_value(LIT) {
62 if !has_neighbor_signal {
63 return;
64 }
65
66 world.set_block(pos, state.set_value(LIT, false), UpdateFlags::UPDATE_ALL);
67 if world.redstone_torch_toggled_too_frequently(pos, true) {
68 world.level_event(level_events::REDSTONE_TORCH_BURNOUT, pos, 0, None);
69 let current_block = world.get_block_state(pos).get_block();
70 world.schedule_block_tick_default(pos, current_block, RESTART_DELAY);
71 }
72 return;
73 }
74
75 if !has_neighbor_signal && !world.redstone_torch_toggled_too_frequently(pos, false) {
76 world.set_block(pos, state.set_value(LIT, true), UpdateFlags::UPDATE_ALL);
77 }
78}
79
80fn own_signal(state: BlockStateId) -> i32 {
81 if state.get_value(LIT) {
82 MAX_REDSTONE_SIGNAL
83 } else {
84 MIN_REDSTONE_SIGNAL
85 }
86}
87
88#[block_behavior]
90pub struct RedstoneTorchBlock {
91 block: BlockRef,
92}
93
94const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
95const LIT: &BoolProperty = &BlockStateProperties::LIT;
96
97impl RedstoneTorchBlock {
98 #[must_use]
100 pub const fn new(block: BlockRef) -> Self {
101 Self { block }
102 }
103
104 fn has_neighbor_signal(world: &dyn LevelReader, pos: BlockPos) -> bool {
105 get_redstone_signal(
106 world,
107 pos.below(),
108 Direction::Down,
109 SignalQueryContext::DEFAULT,
110 ) > MIN_REDSTONE_SIGNAL
111 }
112}
113
114impl BlockBehavior for RedstoneTorchBlock {
115 fn can_survive(&self, _state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
116 let below_pos = pos.below();
117 world.is_face_sturdy_for(
118 world.get_block_state(below_pos),
119 below_pos,
120 Direction::Up,
121 SupportType::Center,
122 )
123 }
124
125 fn update_shape(
126 &self,
127 state: BlockStateId,
128 world: &dyn ScheduledTickAccess,
129 pos: BlockPos,
130 direction: Direction,
131 _neighbor_pos: BlockPos,
132 _neighbor_state: BlockStateId,
133 ) -> BlockStateId {
134 if direction == Direction::Down && !self.can_survive(state, world, pos) {
135 REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
136 } else {
137 state
138 }
139 }
140
141 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
142 let state = self.block.default_state();
143 self.can_survive(state, context.world.as_ref(), context.place_pos())
144 .then_some(state)
145 }
146
147 fn on_place(
148 &self,
149 _state: BlockStateId,
150 world: &Arc<World>,
151 pos: BlockPos,
152 _old_state: BlockStateId,
153 _moved_by_piston: bool,
154 ) {
155 on_place(self.block, world, pos);
156 }
157
158 fn affect_neighbors_after_removal(
159 &self,
160 _state: BlockStateId,
161 world: &Arc<World>,
162 pos: BlockPos,
163 moved_by_piston: bool,
164 ) {
165 affect_neighbors_after_removal(self.block, world, pos, moved_by_piston);
166 }
167
168 fn handle_neighbor_changed(
169 &self,
170 state: BlockStateId,
171 world: &Arc<World>,
172 pos: BlockPos,
173 _source_block: BlockRef,
174 _moved_by_piston: bool,
175 ) {
176 handle_neighbor_changed(
177 self.block,
178 state,
179 world,
180 pos,
181 Self::has_neighbor_signal(world.as_ref(), pos),
182 );
183 }
184
185 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
186 tick_torch(
187 state,
188 world,
189 pos,
190 Self::has_neighbor_signal(world.as_ref(), pos),
191 );
192 }
193
194 fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
195 true
196 }
197
198 fn get_own_signal(
199 &self,
200 state: BlockStateId,
201 _world: &dyn LevelReader,
202 _pos: BlockPos,
203 _context: SignalQueryContext,
204 ) -> i32 {
205 own_signal(state)
206 }
207
208 fn get_signal(
209 &self,
210 state: BlockStateId,
211 _world: &dyn LevelReader,
212 _pos: BlockPos,
213 direction: Direction,
214 _context: SignalQueryContext,
215 ) -> i32 {
216 if direction == Direction::Up {
217 MIN_REDSTONE_SIGNAL
218 } else {
219 own_signal(state)
220 }
221 }
222
223 fn get_direct_signal(
224 &self,
225 state: BlockStateId,
226 world: &dyn LevelReader,
227 pos: BlockPos,
228 direction: Direction,
229 context: SignalQueryContext,
230 ) -> i32 {
231 if direction == Direction::Down {
232 self.get_signal(state, world, pos, direction, context)
233 } else {
234 MIN_REDSTONE_SIGNAL
235 }
236 }
237
238 }
240
241#[block_behavior]
243pub struct RedstoneWallTorchBlock {
244 block: BlockRef,
245}
246
247impl RedstoneWallTorchBlock {
248 #[must_use]
250 pub const fn new(block: BlockRef) -> Self {
251 Self { block }
252 }
253
254 fn has_neighbor_signal(state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
255 let opposite = state.get_value(HORIZONTAL_FACING).opposite();
256 get_redstone_signal(
257 world,
258 pos.relative(opposite),
259 opposite,
260 SignalQueryContext::DEFAULT,
261 ) > MIN_REDSTONE_SIGNAL
262 }
263}
264
265impl BlockBehavior for RedstoneWallTorchBlock {
266 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
267 let facing = state.get_value(HORIZONTAL_FACING);
268 let support_pos = pos.relative(facing.opposite());
269 world.is_face_sturdy(world.get_block_state(support_pos), support_pos, facing)
270 }
271
272 fn update_shape(
273 &self,
274 state: BlockStateId,
275 world: &dyn ScheduledTickAccess,
276 pos: BlockPos,
277 direction: Direction,
278 _neighbor_pos: BlockPos,
279 _neighbor_state: BlockStateId,
280 ) -> BlockStateId {
281 let facing = state.get_value(HORIZONTAL_FACING);
282 if direction.opposite() == facing && !self.can_survive(state, world, pos) {
283 REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
284 } else {
285 state
286 }
287 }
288
289 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
290 for direction in context.get_nearest_looking_directions() {
291 if !direction.is_horizontal() {
292 continue;
293 }
294 let state = self
295 .block
296 .default_state()
297 .set_value(HORIZONTAL_FACING, direction.opposite());
298 if self.can_survive(state, context.world.as_ref(), context.place_pos()) {
299 return Some(state);
300 }
301 }
302 None
303 }
304
305 fn on_place(
306 &self,
307 _state: BlockStateId,
308 world: &Arc<World>,
309 pos: BlockPos,
310 _old_state: BlockStateId,
311 _moved_by_piston: bool,
312 ) {
313 on_place(self.block, world, pos);
314 }
315
316 fn affect_neighbors_after_removal(
317 &self,
318 _state: BlockStateId,
319 world: &Arc<World>,
320 pos: BlockPos,
321 moved_by_piston: bool,
322 ) {
323 affect_neighbors_after_removal(self.block, world, pos, moved_by_piston);
324 }
325
326 fn handle_neighbor_changed(
327 &self,
328 state: BlockStateId,
329 world: &Arc<World>,
330 pos: BlockPos,
331 _source_block: BlockRef,
332 _moved_by_piston: bool,
333 ) {
334 handle_neighbor_changed(
335 self.block,
336 state,
337 world,
338 pos,
339 Self::has_neighbor_signal(state, world.as_ref(), pos),
340 );
341 }
342
343 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
344 tick_torch(
345 state,
346 world,
347 pos,
348 Self::has_neighbor_signal(state, world.as_ref(), pos),
349 );
350 }
351
352 fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
353 true
354 }
355
356 fn get_own_signal(
357 &self,
358 state: BlockStateId,
359 _world: &dyn LevelReader,
360 _pos: BlockPos,
361 _context: SignalQueryContext,
362 ) -> i32 {
363 own_signal(state)
364 }
365
366 fn get_signal(
367 &self,
368 state: BlockStateId,
369 _world: &dyn LevelReader,
370 _pos: BlockPos,
371 direction: Direction,
372 _context: SignalQueryContext,
373 ) -> i32 {
374 if state.get_value(HORIZONTAL_FACING) == direction {
375 MIN_REDSTONE_SIGNAL
376 } else {
377 own_signal(state)
378 }
379 }
380
381 fn get_direct_signal(
382 &self,
383 state: BlockStateId,
384 world: &dyn LevelReader,
385 pos: BlockPos,
386 direction: Direction,
387 context: SignalQueryContext,
388 ) -> i32 {
389 if direction == Direction::Down {
390 self.get_signal(state, world, pos, direction, context)
391 } else {
392 MIN_REDSTONE_SIGNAL
393 }
394 }
395
396 }
398
399#[cfg(test)]
400mod tests {
401 use steel_registry::init_vanilla_registry;
402
403 use super::*;
404 use crate::behavior::init_behaviors;
405 use crate::test_support::TestLevel;
406
407 #[test]
408 fn standing_torch_reads_power_from_its_support() {
409 init_vanilla_registry();
410 init_behaviors();
411 let pos = BlockPos::new(0, 64, 0);
412 let powered = TestLevel::default()
413 .with_block(pos.below(), vanilla_blocks::REDSTONE_BLOCK.default_state());
414 let unpowered =
415 TestLevel::default().with_block(pos.below(), vanilla_blocks::STONE.default_state());
416
417 assert!(RedstoneTorchBlock::has_neighbor_signal(&powered, pos));
418 assert!(!RedstoneTorchBlock::has_neighbor_signal(&unpowered, pos));
419 }
420
421 #[test]
422 fn wall_torch_omits_weak_signal_toward_its_support() {
423 init_vanilla_registry();
424 init_behaviors();
425 let behavior = RedstoneWallTorchBlock::new(&vanilla_blocks::REDSTONE_WALL_TORCH);
426 let state = vanilla_blocks::REDSTONE_WALL_TORCH
427 .default_state()
428 .set_value(HORIZONTAL_FACING, Direction::East)
429 .set_value(LIT, true);
430 let level = TestLevel::default();
431 let pos = BlockPos::new(0, 64, 0);
432
433 assert_eq!(
434 behavior.get_signal(
435 state,
436 &level,
437 pos,
438 Direction::East,
439 SignalQueryContext::DEFAULT,
440 ),
441 0
442 );
443 assert_eq!(
444 behavior.get_signal(
445 state,
446 &level,
447 pos,
448 Direction::West,
449 SignalQueryContext::DEFAULT,
450 ),
451 15
452 );
453 assert_eq!(
454 behavior.get_direct_signal(
455 state,
456 &level,
457 pos,
458 Direction::Down,
459 SignalQueryContext::DEFAULT,
460 ),
461 15
462 );
463 }
464}