Skip to main content

steel_core/behavior/blocks/redstone/piston/
structure_resolver.rs

1//! Vanilla piston push-structure discovery.
2
3use 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}