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 fn sea_level(&self) -> i32 {
302 self.level.sea_level()
303 }
304 }
305
306 fn powered_wire(power: u8) -> BlockStateId {
307 vanilla_blocks::REDSTONE_WIRE
308 .default_state()
309 .set_value(&BlockStateProperties::POWER, power)
310 }
311
312 fn calculate_target_strength_reference(level: &dyn LevelReader, pos: BlockPos) -> i32 {
313 let block_signal =
314 get_best_neighbor_signal(level, pos, SignalQueryContext::without_wire_signals());
315 if block_signal == 15 {
316 return 15;
317 }
318
319 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
320 block_signal.max(get_incoming_wire_signal_reference(&evaluator, level, pos))
321 }
322
323 fn get_incoming_wire_signal_reference(
324 evaluator: &DefaultRedstoneWireEvaluator,
325 level: &dyn LevelReader,
326 pos: BlockPos,
327 ) -> i32 {
328 let mut signal = 0;
329 let above_pos = pos.above();
330 let above_is_conductor =
331 is_redstone_conductor(level, level.get_block_state(above_pos), above_pos);
332
333 for direction in Direction::HORIZONTAL {
334 let neighbor_pos = pos.relative(direction);
335 let neighbor_state = level.get_block_state(neighbor_pos);
336 signal = signal.max(evaluator.get_wire_signal(neighbor_state));
337
338 if is_redstone_conductor(level, neighbor_state, neighbor_pos) && !above_is_conductor {
339 signal = signal
340 .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.above())));
341 } else if !is_redstone_conductor(level, neighbor_state, neighbor_pos) {
342 signal = signal
343 .max(evaluator.get_wire_signal(level.get_block_state(neighbor_pos.below())));
344 }
345 }
346
347 signal.saturating_sub(1)
348 }
349
350 fn assert_matches_reference(level: &dyn LevelReader, pos: BlockPos) {
351 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
352 assert_eq!(
353 evaluator.calculate_target_strength(level, pos),
354 calculate_target_strength_reference(level, pos),
355 );
356 }
357
358 const POWER: &IntProperty = &BlockStateProperties::POWER;
359
360 fn expected_positions(pos: BlockPos, labels: [&str; 7]) -> [BlockPos; 7] {
361 labels.map(|label| match label {
362 "center" => pos,
363 "down" => pos.below(),
364 "up" => pos.above(),
365 "north" => pos.north(),
366 "south" => pos.south(),
367 "west" => pos.west(),
368 "east" => pos.east(),
369 _ => panic!("invalid test direction label"),
370 })
371 }
372
373 #[test]
374 fn seven_position_order_matches_target_jdk_hash_set_fixtures() {
375 let fixtures = [
376 (
377 BlockPos::new(0, 64, 0),
378 ["center", "down", "south", "east", "up", "north", "west"],
379 ),
380 (
381 BlockPos::new(1, 64, 0),
382 ["up", "north", "west", "center", "down", "south", "east"],
383 ),
384 (
385 BlockPos::new(15, 64, 0),
386 ["down", "south", "east", "up", "north", "west", "center"],
387 ),
388 (
389 BlockPos::new(16, 64, 0),
390 ["center", "down", "south", "east", "up", "north", "west"],
391 ),
392 (
393 BlockPos::new(-16, -64, 31),
394 ["down", "south", "east", "up", "north", "west", "center"],
395 ),
396 (
397 BlockPos::new(30_000_000, 319, -30_000_000),
398 ["down", "south", "east", "center", "up", "north", "west"],
399 ),
400 ];
401
402 for (pos, labels) in fixtures {
403 assert_eq!(
404 java_hash_set_update_order(pos),
405 expected_positions(pos, labels)
406 );
407 }
408 }
409
410 #[test]
411 fn incoming_wire_power_does_not_feed_back_through_signal_queries() {
412 init_vanilla_registry();
413 init_behaviors();
414 let pos = BlockPos::new(0, 64, 0);
415 let powered_neighbor = vanilla_blocks::REDSTONE_WIRE
416 .default_state()
417 .set_value(POWER, 15);
418 let level = TestLevel::default().with_block(pos.east(), powered_neighbor);
419 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
420
421 assert_eq!(evaluator.calculate_target_strength(&level, pos), 14);
422 }
423
424 #[test]
425 fn target_strength_matches_vanilla() {
426 init_vanilla_registry();
427 init_behaviors();
428 let pos = BlockPos::new(0, 64, 0);
429 let stone = vanilla_blocks::STONE.default_state();
430 let redstone_block = vanilla_blocks::REDSTONE_BLOCK.default_state();
431
432 let cases = [
433 TestLevel::default(),
434 TestLevel::default().with_block(pos.east(), redstone_block),
435 TestLevel::default().with_block(pos.east(), powered_wire(15)),
436 TestLevel::default()
437 .with_block(pos.east(), stone)
438 .with_block(pos.east().above(), powered_wire(9)),
439 TestLevel::default()
440 .with_block(pos.east(), vanilla_blocks::GLASS.default_state())
441 .with_block(pos.east().below(), powered_wire(9)),
442 TestLevel::default()
443 .with_block(pos.above(), stone)
444 .with_block(pos.east(), stone)
445 .with_block(pos.east().above(), powered_wire(15)),
446 TestLevel::default()
447 .with_block(pos.east(), stone)
448 .with_block(pos.east().east(), redstone_block),
449 TestLevel::default()
450 .with_block(pos.north(), redstone_block)
451 .with_block(pos.east(), powered_wire(12)),
452 ];
453
454 for level in &cases {
455 assert_matches_reference(level, pos);
456 }
457 }
458
459 #[test]
460 fn target_strength_uses_fewer_block_state_reads() {
461 init_vanilla_registry();
462 init_behaviors();
463 let pos = BlockPos::new(0, 64, 0);
464 let level = TestLevel::default()
465 .with_block(pos.east(), vanilla_blocks::STONE.default_state())
466 .with_block(pos.east().above(), powered_wire(12))
467 .with_block(pos.north(), powered_wire(10));
468 let reference_level = CountingLevel::new(level);
469 let optimized_level = CountingLevel::new(
470 TestLevel::default()
471 .with_block(pos.east(), vanilla_blocks::STONE.default_state())
472 .with_block(pos.east().above(), powered_wire(12))
473 .with_block(pos.north(), powered_wire(10)),
474 );
475 let evaluator = DefaultRedstoneWireEvaluator::new(&vanilla_blocks::REDSTONE_WIRE);
476
477 let reference_signal = calculate_target_strength_reference(&reference_level, pos);
478 let optimized_signal = evaluator.calculate_target_strength(&optimized_level, pos);
479
480 assert_eq!(optimized_signal, reference_signal);
481 assert!(
482 optimized_level.block_state_reads() < reference_level.block_state_reads(),
483 "optimized reads: {}, reference reads: {}",
484 optimized_level.block_state_reads(),
485 reference_level.block_state_reads(),
486 );
487 }
488
489 #[test]
490 fn live_world_wire_line_settles_after_source_toggle() {
491 init_vanilla_registry();
492 init_behaviors();
493 let world_fixture = fresh_test_world("wire_evaluator_source_toggle");
494 let world = &world_fixture.world;
495 let wire_start = BlockPos::new(8, 64, 8);
496 insert_ready_full_chunk(world, ChunkPos::from_block_pos(wire_start));
497
498 let source_pos = wire_start.west();
499 assert!(world.set_block(
500 source_pos.below(),
501 vanilla_blocks::STONE.default_state(),
502 UpdateFlags::UPDATE_NONE,
503 ));
504 for offset in 0..4 {
505 assert!(world.set_block(
506 wire_start.offset(offset, -1, 0),
507 vanilla_blocks::STONE.default_state(),
508 UpdateFlags::UPDATE_NONE,
509 ));
510 assert!(world.set_block(
511 wire_start.offset(offset, 0, 0),
512 vanilla_blocks::REDSTONE_WIRE.default_state(),
513 UpdateFlags::UPDATE_NONE,
514 ));
515 }
516
517 assert!(world.set_block(
518 source_pos,
519 vanilla_blocks::REDSTONE_BLOCK.default_state(),
520 UpdateFlags::UPDATE_ALL,
521 ));
522 for (offset, power) in [15_u8, 14, 13, 12].into_iter().enumerate() {
523 assert_eq!(
524 world
525 .get_block_state(wire_start.offset(offset as i32, 0, 0))
526 .get_value(&BlockStateProperties::POWER),
527 power,
528 );
529 }
530
531 assert!(world.remove_block(source_pos, false));
532 for offset in 0..4 {
533 assert_eq!(
534 world
535 .get_block_state(wire_start.offset(offset, 0, 0))
536 .get_value(&BlockStateProperties::POWER),
537 0,
538 );
539 }
540 }
541}