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