steel_core/behavior/blocks/redstone/piston/
structure_resolver.rs1use steel_registry::blocks::behavior::PushReaction;
4use steel_registry::blocks::block_state_ext::BlockStateExt as _;
5use steel_registry::vanilla_blocks;
6use steel_utils::{BlockPos, Direction};
7
8use super::base::PistonBaseBlock;
9use crate::world::{LevelReader, World};
10
11const MAX_PUSH_DEPTH: usize = 12;
12
13pub(super) trait PistonLevel: LevelReader {
14 fn is_within_world_border(&self, pos: BlockPos) -> bool;
15}
16
17impl PistonLevel for World {
18 fn is_within_world_border(&self, pos: BlockPos) -> bool {
19 self.is_block_within_world_border(pos)
20 }
21}
22
23pub(super) struct PistonStructureResolver<'a> {
24 level: &'a dyn PistonLevel,
25 piston_pos: BlockPos,
26 extending: bool,
27 start_pos: BlockPos,
28 push_direction: Direction,
29 piston_direction: Direction,
30 to_push: Vec<BlockPos>,
31 to_destroy: Vec<BlockPos>,
32}
33
34impl<'a> PistonStructureResolver<'a> {
35 pub(super) fn new(
36 level: &'a dyn PistonLevel,
37 piston_pos: BlockPos,
38 direction: Direction,
39 extending: bool,
40 ) -> Self {
41 let (push_direction, start_pos) = if extending {
42 (direction, piston_pos.relative(direction))
43 } else {
44 (direction.opposite(), piston_pos.relative_n(direction, 2))
45 };
46 Self {
47 level,
48 piston_pos,
49 extending,
50 start_pos,
51 push_direction,
52 piston_direction: direction,
53 to_push: Vec::new(),
54 to_destroy: Vec::new(),
55 }
56 }
57
58 pub(super) fn resolve(&mut self) -> bool {
59 self.to_push.clear();
60 self.to_destroy.clear();
61 let next_state = self.level.get_block_state(self.start_pos);
62 if !PistonBaseBlock::is_pushable(
63 next_state,
64 self.level,
65 self.start_pos,
66 self.push_direction,
67 false,
68 self.piston_direction,
69 ) {
70 if self.extending
71 && next_state.get_block().config.push_reaction == PushReaction::Destroy
72 {
73 self.to_destroy.push(self.start_pos);
74 return true;
75 }
76 return false;
77 }
78
79 if !self.add_block_line(self.start_pos, self.push_direction) {
80 return false;
81 }
82
83 let mut index = 0;
84 while index < self.to_push.len() {
85 let pos = self.to_push[index];
86 if Self::is_sticky(self.level.get_block_state(pos)) && !self.add_branching_blocks(pos) {
87 return false;
88 }
89 index += 1;
90 }
91 true
92 }
93
94 fn is_sticky(state: steel_utils::BlockStateId) -> bool {
95 state.get_block() == &vanilla_blocks::SLIME_BLOCK
96 || state.get_block() == &vanilla_blocks::HONEY_BLOCK
97 }
98
99 fn can_stick_to_each_other(
100 first: steel_utils::BlockStateId,
101 second: steel_utils::BlockStateId,
102 ) -> bool {
103 let first_block = first.get_block();
104 let second_block = second.get_block();
105 if (first_block == &vanilla_blocks::HONEY_BLOCK
106 && second_block == &vanilla_blocks::SLIME_BLOCK)
107 || (first_block == &vanilla_blocks::SLIME_BLOCK
108 && second_block == &vanilla_blocks::HONEY_BLOCK)
109 {
110 return false;
111 }
112 Self::is_sticky(first) || Self::is_sticky(second)
113 }
114
115 fn add_block_line(&mut self, start: BlockPos, direction: Direction) -> bool {
116 let mut next_state = self.level.get_block_state(start);
117 if next_state.is_air() {
118 return true;
119 }
120 if !PistonBaseBlock::is_pushable(
121 next_state,
122 self.level,
123 start,
124 self.push_direction,
125 false,
126 direction,
127 ) || start == self.piston_pos
128 || self.to_push.contains(&start)
129 {
130 return true;
131 }
132
133 let mut block_count = 1;
134 if block_count + self.to_push.len() > MAX_PUSH_DEPTH {
135 return false;
136 }
137
138 while Self::is_sticky(next_state) {
139 let pos = start.relative_n(self.push_direction.opposite(), block_count as i32);
140 let previous_state = next_state;
141 next_state = self.level.get_block_state(pos);
142 if next_state.is_air()
143 || !Self::can_stick_to_each_other(previous_state, next_state)
144 || !PistonBaseBlock::is_pushable(
145 next_state,
146 self.level,
147 pos,
148 self.push_direction,
149 false,
150 self.push_direction.opposite(),
151 )
152 || pos == self.piston_pos
153 {
154 break;
155 }
156
157 block_count += 1;
158 if block_count + self.to_push.len() > MAX_PUSH_DEPTH {
159 return false;
160 }
161 }
162
163 let mut blocks_added = 0;
164 for offset in (0..block_count).rev() {
165 self.to_push
166 .push(start.relative_n(self.push_direction.opposite(), offset as i32));
167 blocks_added += 1;
168 }
169
170 let mut offset = 1;
171 loop {
172 let pos = start.relative_n(self.push_direction, offset);
173 if let Some(collision_pos) = self.to_push.iter().position(|p| *p == pos) {
174 self.reorder_list_at_collision(blocks_added, collision_pos);
175 let end = collision_pos + blocks_added;
176 for index in 0..=end {
177 let branch_pos = self.to_push[index];
178 if Self::is_sticky(self.level.get_block_state(branch_pos))
179 && !self.add_branching_blocks(branch_pos)
180 {
181 return false;
182 }
183 }
184 return true;
185 }
186
187 next_state = self.level.get_block_state(pos);
188 if next_state.is_air() {
189 return true;
190 }
191 if !PistonBaseBlock::is_pushable(
192 next_state,
193 self.level,
194 pos,
195 self.push_direction,
196 true,
197 self.push_direction,
198 ) || pos == self.piston_pos
199 {
200 return false;
201 }
202 if next_state.get_block().config.push_reaction == PushReaction::Destroy {
203 self.to_destroy.push(pos);
204 return true;
205 }
206 if self.to_push.len() >= MAX_PUSH_DEPTH {
207 return false;
208 }
209
210 self.to_push.push(pos);
211 blocks_added += 1;
212 offset += 1;
213 }
214 }
215
216 fn reorder_list_at_collision(&mut self, blocks_added: usize, collision_pos: usize) {
217 let last_line_start = self.to_push.len() - blocks_added;
218 let mut reordered = Vec::with_capacity(self.to_push.len());
219 reordered.extend_from_slice(&self.to_push[..collision_pos]);
220 reordered.extend_from_slice(&self.to_push[last_line_start..]);
221 reordered.extend_from_slice(&self.to_push[collision_pos..last_line_start]);
222 self.to_push = reordered;
223 }
224
225 fn add_branching_blocks(&mut self, from_pos: BlockPos) -> bool {
226 let from_state = self.level.get_block_state(from_pos);
227 for direction in Direction::ALL {
228 if direction.axis() == self.push_direction.axis() {
229 continue;
230 }
231 let neighbor_pos = from_pos.relative(direction);
232 let neighbor_state = self.level.get_block_state(neighbor_pos);
233 if Self::can_stick_to_each_other(neighbor_state, from_state)
234 && !self.add_block_line(neighbor_pos, direction)
235 {
236 return false;
237 }
238 }
239 true
240 }
241
242 pub(super) const fn push_direction(&self) -> Direction {
243 self.push_direction
244 }
245
246 pub(super) fn to_push(&self) -> &[BlockPos] {
247 &self.to_push
248 }
249
250 pub(super) fn to_destroy(&self) -> &[BlockPos] {
251 &self.to_destroy
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use steel_registry::init_vanilla_registry;
258 use steel_registry::vanilla_blocks;
259
260 use super::*;
261 use crate::behavior::init_behaviors;
262 use crate::test_support::TestLevel;
263
264 impl PistonLevel for TestLevel {
265 fn is_within_world_border(&self, _pos: BlockPos) -> bool {
266 true
267 }
268 }
269
270 #[test]
271 fn resolver_accepts_twelve_blocks_and_rejects_thirteen() {
272 init_vanilla_registry();
273 init_behaviors();
274 let piston_pos = BlockPos::new(0, 64, 0);
275 let twelve = TestLevel::default();
276 for offset in 1..=12 {
277 twelve.set_test_block(
278 piston_pos.relative_n(Direction::East, offset),
279 vanilla_blocks::STONE.default_state(),
280 );
281 }
282 let mut resolver = PistonStructureResolver::new(&twelve, piston_pos, Direction::East, true);
283 assert!(resolver.resolve());
284 assert_eq!(resolver.to_push().len(), 12);
285
286 let thirteen = TestLevel::default();
287 for offset in 1..=13 {
288 thirteen.set_test_block(
289 piston_pos.relative_n(Direction::East, offset),
290 vanilla_blocks::STONE.default_state(),
291 );
292 }
293 let mut resolver =
294 PistonStructureResolver::new(&thirteen, piston_pos, Direction::East, true);
295 assert!(!resolver.resolve());
296 }
297
298 #[test]
299 fn slime_branches_but_does_not_bind_honey() {
300 init_vanilla_registry();
301 init_behaviors();
302 let piston_pos = BlockPos::new(0, 64, 0);
303 let slime_pos = piston_pos.east();
304 let south_pos = slime_pos.south();
305 let honey_pos = slime_pos.north();
306 let level = TestLevel::default()
307 .with_block(slime_pos, vanilla_blocks::SLIME_BLOCK.default_state())
308 .with_block(south_pos, vanilla_blocks::STONE.default_state())
309 .with_block(honey_pos, vanilla_blocks::HONEY_BLOCK.default_state());
310
311 let mut resolver = PistonStructureResolver::new(&level, piston_pos, Direction::East, true);
312 assert!(resolver.resolve());
313 assert!(resolver.to_push().contains(&slime_pos));
314 assert!(resolver.to_push().contains(&south_pos));
315 assert!(!resolver.to_push().contains(&honey_pos));
316 }
317}