Skip to main content

steel_core/behavior/blocks/building/
wall_block.rs

1//! Wall block behavior implementation.
2//!
3//! Walls connect to adjacent walls, bars, fence gates and solid blocks. Each
4//! horizontal side has a [`WallSide`] (none/low/tall) and an `UP` post flag.
5
6use steel_macros::block_behavior;
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::block_state_ext::BlockStateExt;
9use steel_registry::blocks::properties::{
10    BlockStateProperties, BoolProperty, Direction, EnumProperty, WallSide,
11};
12use steel_registry::blocks::shapes::{OffsetVoxelShape, offset_face_rectangles_cover};
13use steel_registry::vanilla_block_tags::BlockTag;
14use steel_utils::{BlockPos, BlockStateId};
15
16use crate::behavior::block::{BlockBehavior, schedule_water_tick_if_waterlogged};
17use crate::behavior::blocks::building::FenceGateBlock;
18use crate::behavior::blocks::utils::is_excluded_for_connection;
19use crate::behavior::context::BlockPlaceContext;
20use crate::entity::ai::path::PathComputationType;
21use crate::world::{LevelReader as _, ScheduledTickAccess};
22
23/// Behavior for wall blocks.
24///
25/// Walls have four [`WallSide`] properties (north, east, south, west) plus an
26/// `UP` post flag and a `WATERLOGGED` flag. A wall connects to:
27/// - Other walls
28/// - Iron bars
29/// - Fence gates facing the appropriate direction
30/// - Blocks with a sturdy face on the connecting side
31#[block_behavior]
32pub struct WallBlock {
33    block: BlockRef,
34}
35
36/// Post (center column) property.
37const UP: &BoolProperty = &BlockStateProperties::UP;
38/// North connection property.
39const NORTH: &EnumProperty<WallSide> = &BlockStateProperties::NORTH_WALL;
40/// East connection property.
41const EAST: &EnumProperty<WallSide> = &BlockStateProperties::EAST_WALL;
42/// South connection property.
43const SOUTH: &EnumProperty<WallSide> = &BlockStateProperties::SOUTH_WALL;
44/// West connection property.
45const WEST: &EnumProperty<WallSide> = &BlockStateProperties::WEST_WALL;
46/// Waterlogged property.
47const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
48
49// Vanilla TEST_SHAPE_POST = Block.column(2.0, 0.0, 16.0), projected onto the DOWN face.
50const POST_X_MIN: f64 = 7.0 / 16.0;
51const POST_X_MAX: f64 = 9.0 / 16.0;
52const POST_Z_MIN: f64 = 7.0 / 16.0;
53const POST_Z_MAX: f64 = 9.0 / 16.0;
54
55// Vanilla TEST_SHAPES_WALL = Shapes.rotateHorizontal(Block.boxZ(2.0, 16.0, 0.0, 9.0)),
56// projected onto the DOWN face per direction.
57const WALL_ARM_MIN: f64 = 7.0 / 16.0;
58const WALL_ARM_MAX: f64 = 9.0 / 16.0;
59const WALL_ARM_EXTENT: f64 = 9.0 / 16.0;
60
61impl WallBlock {
62    /// Creates a new wall block behavior for the given block.
63    #[must_use]
64    pub const fn new(block: BlockRef) -> Self {
65        Self { block }
66    }
67}
68
69impl BlockBehavior for WallBlock {
70    fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
71        let world = context.world;
72        let pos = context.place_pos();
73
74        let north_pos = Direction::North.relative(pos);
75        let east_pos = Direction::East.relative(pos);
76        let south_pos = Direction::South.relative(pos);
77        let west_pos = Direction::West.relative(pos);
78        let top_pos = Direction::Up.relative(pos);
79
80        let north_state = world.get_block_state(north_pos);
81        let east_state = world.get_block_state(east_pos);
82        let south_state = world.get_block_state(south_pos);
83        let west_state = world.get_block_state(west_pos);
84        let top_state = world.get_block_state(top_pos);
85
86        // Vanilla checks the neighbor's face that points back at the wall,
87        // i.e. the opposite of the direction toward the neighbor.
88        let north = connects_to(
89            north_state,
90            world.is_face_sturdy(north_state, north_pos, Direction::South),
91            Direction::South,
92        );
93        let east = connects_to(
94            east_state,
95            world.is_face_sturdy(east_state, east_pos, Direction::West),
96            Direction::West,
97        );
98        let south = connects_to(
99            south_state,
100            world.is_face_sturdy(south_state, south_pos, Direction::North),
101            Direction::North,
102        );
103        let west = connects_to(
104            west_state,
105            world.is_face_sturdy(west_state, west_pos, Direction::East),
106            Direction::East,
107        );
108
109        let state = self
110            .block
111            .default_state()
112            .set_value(WATERLOGGED, context.is_water_source());
113
114        Some(update_wall_state(
115            state, top_pos, top_state, north, east, south, west,
116        ))
117    }
118
119    fn update_shape(
120        &self,
121        state: BlockStateId,
122        world: &dyn ScheduledTickAccess,
123        pos: BlockPos,
124        direction: Direction,
125        neighbor_pos: BlockPos,
126        neighbor_state: BlockStateId,
127    ) -> BlockStateId {
128        schedule_water_tick_if_waterlogged(state, world, pos);
129
130        match direction {
131            // Base behavior: nothing below changes a wall's shape.
132            Direction::Down => state,
133            Direction::Up => top_update(state, neighbor_pos, neighbor_state),
134            _ => side_update(world, pos, state, neighbor_pos, neighbor_state, direction),
135        }
136    }
137
138    fn is_pathfindable(
139        &self,
140        _state: BlockStateId,
141        _computation_type: PathComputationType,
142    ) -> bool {
143        false
144    }
145}
146
147/// Whether the wall is connected on the given side.
148fn is_connected(state: BlockStateId, side: &EnumProperty<WallSide>) -> bool {
149    state.get_value(side) != WallSide::None
150}
151
152/// `face_solid` is whether the neighbor has a sturdy face pointing back at
153/// the wall; `direction` is that same (opposite-to-neighbor) direction.
154fn connects_to(neighbor_state: BlockStateId, face_solid: bool, direction: Direction) -> bool {
155    let block = neighbor_state.get_block();
156    let connected_fence_gate = block.has_tag(&BlockTag::FENCE_GATES)
157        && FenceGateBlock::connects_to_direction(neighbor_state, direction);
158
159    block.has_tag(&BlockTag::WALLS)
160        || (!is_excluded_for_connection(block) && face_solid)
161        || block.has_tag(&BlockTag::BARS)
162        || block.has_tag(&BlockTag::C_GLASS_PANES)
163        || connected_fence_gate
164}
165
166fn top_update(state: BlockStateId, top_pos: BlockPos, top_neighbor: BlockStateId) -> BlockStateId {
167    let north = is_connected(state, NORTH);
168    let east = is_connected(state, EAST);
169    let south = is_connected(state, SOUTH);
170    let west = is_connected(state, WEST);
171    update_wall_state(state, top_pos, top_neighbor, north, east, south, west)
172}
173
174fn side_update(
175    world: &dyn ScheduledTickAccess,
176    pos: BlockPos,
177    state: BlockStateId,
178    neighbor_pos: BlockPos,
179    neighbor: BlockStateId,
180    direction: Direction,
181) -> BlockStateId {
182    let opposite = direction.opposite();
183    let connected = connects_to(
184        neighbor,
185        world.is_face_sturdy(neighbor, neighbor_pos, opposite),
186        opposite,
187    );
188
189    let north = if direction == Direction::North {
190        connected
191    } else {
192        is_connected(state, NORTH)
193    };
194    let east = if direction == Direction::East {
195        connected
196    } else {
197        is_connected(state, EAST)
198    };
199    let south = if direction == Direction::South {
200        connected
201    } else {
202        is_connected(state, SOUTH)
203    };
204    let west = if direction == Direction::West {
205        connected
206    } else {
207        is_connected(state, WEST)
208    };
209
210    let above = Direction::Up.relative(pos);
211    let above_state = world.get_block_state(above);
212    update_wall_state(state, above, above_state, north, east, south, west)
213}
214
215/// Vanilla `WallBlock.updateShape` (private side/post helper).
216#[expect(
217    clippy::fn_params_excessive_bools,
218    reason = "mirrors vanilla WallBlock north/east/south/west signature"
219)]
220fn update_wall_state(
221    state: BlockStateId,
222    top_pos: BlockPos,
223    top_neighbor: BlockStateId,
224    north: bool,
225    east: bool,
226    south: bool,
227    west: bool,
228) -> BlockStateId {
229    let above_shape = top_neighbor.get_collision_shape_at(top_pos);
230    let sides = update_sides(state, above_shape, north, east, south, west);
231    sides.set_value(UP, should_raise_post(sides, top_neighbor, above_shape))
232}
233
234#[expect(
235    clippy::fn_params_excessive_bools,
236    reason = "mirrors vanilla WallBlock north/east/south/west signature"
237)]
238fn update_sides(
239    state: BlockStateId,
240    above_shape: OffsetVoxelShape,
241    north: bool,
242    east: bool,
243    south: bool,
244    west: bool,
245) -> BlockStateId {
246    state
247        .set_value(
248            NORTH,
249            make_wall_state(
250                north,
251                above_shape,
252                WALL_ARM_MIN,
253                WALL_ARM_MAX,
254                0.0,
255                WALL_ARM_EXTENT,
256            ),
257        )
258        .set_value(
259            EAST,
260            make_wall_state(
261                east,
262                above_shape,
263                WALL_ARM_MIN,
264                1.0,
265                WALL_ARM_MIN,
266                WALL_ARM_MAX,
267            ),
268        )
269        .set_value(
270            SOUTH,
271            make_wall_state(
272                south,
273                above_shape,
274                WALL_ARM_MIN,
275                WALL_ARM_MAX,
276                WALL_ARM_MIN,
277                1.0,
278            ),
279        )
280        .set_value(
281            WEST,
282            make_wall_state(
283                west,
284                above_shape,
285                0.0,
286                WALL_ARM_EXTENT,
287                WALL_ARM_MIN,
288                WALL_ARM_MAX,
289            ),
290        )
291}
292
293fn make_wall_state(
294    connects_to_side: bool,
295    above_shape: OffsetVoxelShape,
296    x_min: f64,
297    x_max: f64,
298    z_min: f64,
299    z_max: f64,
300) -> WallSide {
301    if !connects_to_side {
302        return WallSide::None;
303    }
304    if is_covered(above_shape, x_min, x_max, z_min, z_max) {
305        WallSide::Tall
306    } else {
307        WallSide::Low
308    }
309}
310
311fn should_raise_post(
312    state: BlockStateId,
313    top_neighbor: BlockStateId,
314    above_shape: OffsetVoxelShape,
315) -> bool {
316    let top_neighbor_has_post = top_neighbor.get_block().has_tag(&BlockTag::WALLS)
317        && top_neighbor.try_get_value(UP).unwrap_or(false);
318    if top_neighbor_has_post {
319        return true;
320    }
321
322    let north_wall = state.get_value(NORTH);
323    let south_wall = state.get_value(SOUTH);
324    let east_wall = state.get_value(EAST);
325    let west_wall = state.get_value(WEST);
326
327    let north_none = north_wall == WallSide::None;
328    let south_none = south_wall == WallSide::None;
329    let east_none = east_wall == WallSide::None;
330    let west_none = west_wall == WallSide::None;
331
332    let has_corner = (north_none && south_none && west_none && east_none)
333        || (north_none != south_none)
334        || (west_none != east_none);
335    if has_corner {
336        return true;
337    }
338
339    let has_high_wall = (north_wall == WallSide::Tall && south_wall == WallSide::Tall)
340        || (east_wall == WallSide::Tall && west_wall == WallSide::Tall);
341    if has_high_wall {
342        return false;
343    }
344
345    top_neighbor
346        .get_block()
347        .has_tag(&BlockTag::WALL_POST_OVERRIDE)
348        || is_covered(above_shape, POST_X_MIN, POST_X_MAX, POST_Z_MIN, POST_Z_MAX)
349}
350
351/// Checks whether the block above's collision shape fully covers a test
352/// rectangle on its DOWN face.
353fn is_covered(
354    above_shape: OffsetVoxelShape,
355    x_min: f64,
356    x_max: f64,
357    z_min: f64,
358    z_max: f64,
359) -> bool {
360    offset_face_rectangles_cover(above_shape, Direction::Down, x_min, x_max, z_min, z_max)
361}