1use std::sync::Arc;
4
5use steel_registry::blocks::BlockRef;
6use steel_registry::blocks::block_state_ext::BlockStateExt as _;
7use steel_registry::blocks::properties::{BlockStateProperties, IntProperty};
8use steel_utils::types::UpdateFlags;
9use steel_utils::{BlockPos, BlockStateId, Direction};
10
11use crate::behavior::BLOCK_BEHAVIORS;
12use crate::world::{LevelReader, SignalQueryContext, World};
13
14use crate::behavior::blocks::redstone::java_hash::sort_small_map_positions;
15
16pub(super) struct DefaultRedstoneWireEvaluator {
18 wire_block: BlockRef,
19}
20
21const POWER: &IntProperty = &BlockStateProperties::POWER;
22
23impl DefaultRedstoneWireEvaluator {
24 pub(super) const fn new(wire_block: BlockRef) -> Self {
25 Self { wire_block }
26 }
27
28 pub(super) fn update_power_strength(
29 &self,
30 world: &Arc<World>,
31 pos: BlockPos,
32 state: BlockStateId,
33 ) {
34 let target_strength = self.calculate_target_strength(world.as_ref(), pos);
35 if i32::from(state.get_value(POWER)) == target_strength {
36 return;
37 }
38
39 if world.get_block_state(pos) == state {
40 world.set_block(
41 pos,
42 state.set_value(POWER, target_strength as u8),
43 UpdateFlags::UPDATE_CLIENTS,
44 );
45 }
46
47 for update_pos in java_hash_set_update_order(pos) {
48 world.update_neighbors_at(update_pos, self.wire_block);
49 }
50 }
51
52 fn calculate_target_strength(&self, level: &dyn LevelReader, pos: BlockPos) -> i32 {
53 let context = SignalQueryContext::without_wire_signals();
56 let center_state = level.get_block_state(pos);
57 let ignore_center_signal =
58 center_state.is_air() || center_state.get_block() == self.wire_block;
59 let below_pos = pos.below();
60 let above_pos = pos.above();
61 let below_state = level.get_block_state(below_pos);
62 let above_state = level.get_block_state(above_pos);
63
64 let (below_signal, _) = self.get_signal_from_vertical(
65 level,
66 below_pos,
67 below_state,
68 Direction::Down,
69 context,
70 ignore_center_signal,
71 );
72 if below_signal == 15 {
73 return 15;
74 }
75
76 let (above_signal, above_is_conductor) = self.get_signal_from_vertical(
77 level,
78 above_pos,
79 above_state,
80 Direction::Up,
81 context,
82 ignore_center_signal,
83 );
84 let mut signal = below_signal.max(above_signal);
85 if signal == 15 {
86 return 15;
87 }
88
89 for direction in Direction::HORIZONTAL {
90 let neighbor_pos = pos.relative(direction);
91 let neighbor_state = level.get_block_state(neighbor_pos);
92 if neighbor_state.get_block() == self.wire_block {
93 signal = signal.max(self.get_wire_signal(neighbor_state) - 1);
94 } else {
95 let (neighbor_signal, neighbor_is_conductor) = self.get_non_wire_signal(
99 level,
100 neighbor_pos,
101 neighbor_state,
102 direction,
103 context,
104 ignore_center_signal,
105 );
106 signal = signal.max(neighbor_signal);
107 if signal == 15 {
108 return 15;
109 }
110
111 if signal < 14 {
112 if neighbor_is_conductor && !above_is_conductor {
113 let above_neighbor_pos = neighbor_pos.above();
114 signal = signal.max(
115 self.get_wire_signal(level.get_block_state(above_neighbor_pos)) - 1,
116 );
117 } else if !neighbor_is_conductor {
118 let below_neighbor_pos = neighbor_pos.below();
119 signal = signal.max(
120 self.get_wire_signal(level.get_block_state(below_neighbor_pos)) - 1,
121 );
122 }
123 }
124 }
125
126 if signal == 15 {
127 return 15;
128 }
129 }
130
131 signal
132 }
133
134 fn get_signal_from_vertical(
136 &self,
137 level: &dyn LevelReader,
138 pos: BlockPos,
139 state: BlockStateId,
140 direction: Direction,
141 context: SignalQueryContext,
142 ignore_center_signal: bool,
143 ) -> (i32, bool) {
144 if state.get_block() == self.wire_block {
145 return (0, false);
146 }
147
148 self.get_non_wire_signal(level, pos, state, direction, context, ignore_center_signal)
149 }
150
151 fn get_non_wire_signal(
154 &self,
155 level: &dyn LevelReader,
156 pos: BlockPos,
157 state: BlockStateId,
158 direction: Direction,
159 context: SignalQueryContext,
160 ignore_center_signal: bool,
161 ) -> (i32, bool) {
162 if state.is_air() {
163 return (0, false);
164 }
165
166 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
167 let mut signal = behavior.get_signal(state, level, pos, direction, context);
168 let is_conductor = behavior.is_redstone_conductor(state, level, pos);
169 if is_conductor && signal < 15 {
170 signal = signal.max(self.get_direct_signal_to(
171 level,
172 pos,
173 direction.opposite(),
174 context,
175 ignore_center_signal,
176 ));
177 }
178
179 (signal, is_conductor)
180 }
181
182 fn get_direct_signal_to(
188 &self,
189 level: &dyn LevelReader,
190 pos: BlockPos,
191 direction_to_center: Direction,
192 context: SignalQueryContext,
193 ignore_center_signal: bool,
194 ) -> i32 {
195 let mut signal = 0;
196 for direction in Direction::ALL {
197 if direction == direction_to_center && ignore_center_signal {
198 continue;
199 }
200
201 let neighbor_pos = pos.relative(direction);
202 let neighbor_state = level.get_block_state(neighbor_pos);
203 if neighbor_state.is_air() || neighbor_state.get_block() == self.wire_block {
204 continue;
205 }
206
207 signal = signal.max(
208 BLOCK_BEHAVIORS
209 .get_behavior(neighbor_state.get_block())
210 .get_direct_signal(neighbor_state, level, neighbor_pos, direction, context),
211 );
212 if signal == 15 {
213 return 15;
214 }
215 }
216
217 signal
218 }
219
220 fn get_wire_signal(&self, state: BlockStateId) -> i32 {
221 if state.get_block() == self.wire_block {
222 i32::from(state.get_value(POWER))
223 } else {
224 0
225 }
226 }
227}
228
229fn java_hash_set_update_order(pos: BlockPos) -> [BlockPos; 7] {
237 let mut positions = [
238 pos,
239 pos.below(),
240 pos.above(),
241 pos.north(),
242 pos.south(),
243 pos.west(),
244 pos.east(),
245 ];
246
247 sort_small_map_positions(&mut positions);
248
249 positions
250}
251
252#[cfg(test)]
253mod tests {
254 use std::cell::Cell;
255
256 use steel_registry::init_vanilla_registry;
257 use steel_registry::vanilla_blocks;
258 use steel_utils::ChunkPos;
259
260 use super::*;
261 use crate::behavior::init_behaviors;
262 use crate::test_support::{TestLevel, fresh_test_world, insert_ready_full_chunk};
263 use crate::world::{get_best_neighbor_signal, is_redstone_conductor};
264
265 struct CountingLevel {
266 level: TestLevel,
267 block_state_reads: Cell<usize>,
268 }
269
270 impl CountingLevel {
271 fn new(level: TestLevel) -> Self {
272 Self {
273 level,
274 block_state_reads: Cell::new(0),
275 }
276 }
277
278 fn block_state_reads(&self) -> usize {
279 self.block_state_reads.get()
280 }
281 }
282
283 impl LevelReader for CountingLevel {
284 fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
285 self.block_state_reads.set(self.block_state_reads.get() + 1);
286 self.level.get_block_state(pos)
287 }
288
289 fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
290 self.level.raw_brightness(pos, sky_darkening)
291 }
292
293 fn min_y(&self) -> i32 {
294 self.level.min_y()
295 }
296
297 fn height(&self) -> i32 {
298 self.level.height()
299 }
300 }
301
302 fn powered_wire(power: u8) -> BlockStateId {
303 vanilla_blocks::REDSTONE_WIRE
304 .default_state()
305 .set_value(&BlockStateProperties::POWER, power)
306 }
307
308 fn calculate_target_strength_reference(level: &dyn LevelReader, pos: BlockPos) -> i32 {
309 let block_signal =
310 get_best_neighbor_signal(level, pos, SignalQueryContext::without_wire_signals());
311 if block_signal == 15 {
312 return 15;
313 }
314
315 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
316 block_signal.max(get_incoming_wire_signal_reference(&evaluator, level, pos))
317 }
318
319 fn get_incoming_wire_signal_reference(
320 evaluator: &DefaultRedstoneWireEvaluator,
321 level: &dyn LevelReader,
322 pos: BlockPos,
323 ) -> i32 {
324 let mut signal = 0;
325 let above_pos = pos.above();
326 let above_is_conductor =
327 is_redstone_conductor(level, level.get_block_state(above_pos), above_pos);
328
329 for direction in Direction::HORIZONTAL {
330 let neighbor_pos = pos.relative(direction);
331 let neighbor_state = level.get_block_state(neighbor_pos);
332 signal = signal.max(evaluator.get_wire_signal(neighbor_state));
333
334 if is_redstone_conductor(level, neighbor_state, neighbor_pos) && !above_is_conductor {
335 signal = signal
336 .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.above())));
337 } else if !is_redstone_conductor(level, neighbor_state, neighbor_pos) {
338 signal = signal
339 .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.below())));
340 }
341 }
342
343 signal.saturating_sub(1)
344 }
345
346 fn assert_matches_reference(level: &dyn LevelReader, pos: BlockPos) {
347 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
348 assert_eq!(
349 evaluator.calculate_target_strength(level, pos),
350 calculate_target_strength_reference(level, pos),
351 );
352 }
353
354 const POWER: &IntProperty = &BlockStateProperties::POWER;
355
356 fn expected_positions(pos: BlockPos, labels: [&str; 7]) -> [BlockPos; 7] {
357 labels.map(|label| match label {
358 "center" => pos,
359 "down" => pos.below(),
360 "up" => pos.above(),
361 "north" => pos.north(),
362 "south" => pos.south(),
363 "west" => pos.west(),
364 "east" => pos.east(),
365 _ => panic!("invalid test direction label"),
366 })
367 }
368
369 #[test]
370 fn seven_position_order_matches_target_jdk_hash_set_fixtures() {
371 let fixtures = [
372 (
373 BlockPos::new(0, 64, 0),
374 ["center", "down", "south", "east", "up", "north", "west"],
375 ),
376 (
377 BlockPos::new(1, 64, 0),
378 ["up", "north", "west", "center", "down", "south", "east"],
379 ),
380 (
381 BlockPos::new(15, 64, 0),
382 ["down", "south", "east", "up", "north", "west", "center"],
383 ),
384 (
385 BlockPos::new(16, 64, 0),
386 ["center", "down", "south", "east", "up", "north", "west"],
387 ),
388 (
389 BlockPos::new(-16, -64, 31),
390 ["down", "south", "east", "up", "north", "west", "center"],
391 ),
392 (
393 BlockPos::new(30_000_000, 319, -30_000_000),
394 ["down", "south", "east", "center", "up", "north", "west"],
395 ),
396 ];
397
398 for (pos, labels) in fixtures {
399 assert_eq!(
400 java_hash_set_update_order(pos),
401 expected_positions(pos, labels)
402 );
403 }
404 }
405
406 #[test]
407 fn incoming_wire_power_does_not_feed_back_through_signal_queries() {
408 init_vanilla_registry();
409 init_behaviors();
410 let pos = BlockPos::new(0, 64, 0);
411 let powered_neighbor = vanilla_blocks::REDSTONE_WIRE
412 .default_state()
413 .set_value(POWER, 15);
414 let level = TestLevel::default().with_block(pos.east(), powered_neighbor);
415 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
416
417 assert_eq!(evaluator.calculate_target_strength(&level, pos), 14);
418 }
419
420 #[test]
421 fn target_strength_matches_vanilla() {
422 init_vanilla_registry();
423 init_behaviors();
424 let pos = BlockPos::new(0, 64, 0);
425 let stone = vanilla_blocks::STONE.default_state();
426 let redstone_block = vanilla_blocks::REDSTONE_BLOCK.default_state();
427
428 let cases = [
429 TestLevel::default(),
430 TestLevel::default().with_block(pos.east(), redstone_block),
431 TestLevel::default().with_block(pos.east(), powered_wire(15)),
432 TestLevel::default()
433 .with_block(pos.east(), stone)
434 .with_block(pos.east().above(), powered_wire(9)),
435 TestLevel::default()
436 .with_block(pos.east(), vanilla_blocks::GLASS.default_state())
437 .with_block(pos.east().below(), powered_wire(9)),
438 TestLevel::default()
439 .with_block(pos.above(), stone)
440 .with_block(pos.east(), stone)
441 .with_block(pos.east().above(), powered_wire(15)),
442 TestLevel::default()
443 .with_block(pos.east(), stone)
444 .with_block(pos.east().east(), redstone_block),
445 TestLevel::default()
446 .with_block(pos.north(), redstone_block)
447 .with_block(pos.east(), powered_wire(12)),
448 ];
449
450 for level in &cases {
451 assert_matches_reference(level, pos);
452 }
453 }
454
455 #[test]
456 fn target_strength_uses_fewer_block_state_reads() {
457 init_vanilla_registry();
458 init_behaviors();
459 let pos = BlockPos::new(0, 64, 0);
460 let level = TestLevel::default()
461 .with_block(pos.east(), vanilla_blocks::STONE.default_state())
462 .with_block(pos.east().above(), powered_wire(12))
463 .with_block(pos.north(), powered_wire(10));
464 let reference_level = CountingLevel::new(level);
465 let optimized_level = CountingLevel::new(
466 TestLevel::default()
467 .with_block(pos.east(), vanilla_blocks::STONE.default_state())
468 .with_block(pos.east().above(), powered_wire(12))
469 .with_block(pos.north(), powered_wire(10)),
470 );
471 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
472
473 let reference_signal = calculate_target_strength_reference(&reference_level, pos);
474 let optimized_signal = evaluator.calculate_target_strength(&optimized_level, pos);
475
476 assert_eq!(optimized_signal, reference_signal);
477 assert!(
478 optimized_level.block_state_reads() < reference_level.block_state_reads(),
479 "optimized reads: {}, reference reads: {}",
480 optimized_level.block_state_reads(),
481 reference_level.block_state_reads(),
482 );
483 }
484
485 #[test]
486 fn live_world_wire_line_settles_after_source_toggle() {
487 init_vanilla_registry();
488 init_behaviors();
489 let world = fresh_test_world("wire_evaluator_source_toggle");
490 let wire_start = BlockPos::new(8, 64, 8);
491 insert_ready_full_chunk(&world, ChunkPos::from_block_pos(wire_start));
492
493 let source_pos = wire_start.west();
494 assert!(world.set_block(
495 source_pos.below(),
496 vanilla_blocks::STONE.default_state(),
497 UpdateFlags::UPDATE_NONE,
498 ));
499 for offset in 0..4 {
500 assert!(world.set_block(
501 wire_start.offset(offset, -1, 0),
502 vanilla_blocks::STONE.default_state(),
503 UpdateFlags::UPDATE_NONE,
504 ));
505 assert!(world.set_block(
506 wire_start.offset(offset, 0, 0),
507 vanilla_blocks::REDSTONE_WIRE.default_state(),
508 UpdateFlags::UPDATE_NONE,
509 ));
510 }
511
512 assert!(world.set_block(
513 source_pos,
514 vanilla_blocks::REDSTONE_BLOCK.default_state(),
515 UpdateFlags::UPDATE_ALL,
516 ));
517 for (offset, power) in [15_u8, 14, 13, 12].into_iter().enumerate() {
518 assert_eq!(
519 world
520 .get_block_state(wire_start.offset(offset as i32, 0, 0))
521 .get_value(&BlockStateProperties::POWER),
522 power,
523 );
524 }
525
526 assert!(world.remove_block(source_pos, false));
527 for offset in 0..4 {
528 assert_eq!(
529 world
530 .get_block_state(wire_start.offset(offset, 0, 0))
531 .get_value(&BlockStateProperties::POWER),
532 0,
533 );
534 }
535 }
536}