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
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}