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