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/// Vanilla `WallBlock.connectsTo`.
153///
154/// `face_solid` is whether the neighbor has a sturdy face pointing back at
155/// the wall; `direction` is that same (opposite-to-neighbor) direction.
156fn connects_to(neighbor_state: BlockStateId, face_solid: bool, direction: Direction) -> bool {
157    let block = neighbor_state.get_block();
158    let connected_fence_gate = block.has_tag(&BlockTag::FENCE_GATES)
159        && FenceGateBlock::connects_to_direction(neighbor_state, direction);
160
161    block.has_tag(&BlockTag::WALLS)
162        || (!is_excluded_for_connection(block) && face_solid)
163        || block.has_tag(&BlockTag::BARS)
164        || block.has_tag(&BlockTag::C_GLASS_PANES)
165        || connected_fence_gate
166}
167
168/// Vanilla `WallBlock.topUpdate`.
169fn top_update(state: BlockStateId, top_pos: BlockPos, top_neighbor: BlockStateId) -> BlockStateId {
170    let north = is_connected(state, NORTH);
171    let east = is_connected(state, EAST);
172    let south = is_connected(state, SOUTH);
173    let west = is_connected(state, WEST);
174    update_wall_state(state, top_pos, top_neighbor, north, east, south, west)
175}
176
177/// Vanilla `WallBlock.sideUpdate`.
178fn side_update(
179    world: &dyn ScheduledTickAccess,
180    pos: BlockPos,
181    state: BlockStateId,
182    neighbor_pos: BlockPos,
183    neighbor: BlockStateId,
184    direction: Direction,
185) -> BlockStateId {
186    let opposite = direction.opposite();
187    let connected = connects_to(
188        neighbor,
189        world.is_face_sturdy(neighbor, neighbor_pos, opposite),
190        opposite,
191    );
192
193    let north = if direction == Direction::North {
194        connected
195    } else {
196        is_connected(state, NORTH)
197    };
198    let east = if direction == Direction::East {
199        connected
200    } else {
201        is_connected(state, EAST)
202    };
203    let south = if direction == Direction::South {
204        connected
205    } else {
206        is_connected(state, SOUTH)
207    };
208    let west = if direction == Direction::West {
209        connected
210    } else {
211        is_connected(state, WEST)
212    };
213
214    let above = Direction::Up.relative(pos);
215    let above_state = world.get_block_state(above);
216    update_wall_state(state, above, above_state, north, east, south, west)
217}
218
219/// Vanilla `WallBlock.updateShape` (private side/post helper).
220#[expect(
221    clippy::fn_params_excessive_bools,
222    reason = "mirrors vanilla WallBlock north/east/south/west signature"
223)]
224fn update_wall_state(
225    state: BlockStateId,
226    top_pos: BlockPos,
227    top_neighbor: BlockStateId,
228    north: bool,
229    east: bool,
230    south: bool,
231    west: bool,
232) -> BlockStateId {
233    let above_shape = top_neighbor.get_collision_shape_at(top_pos);
234    let sides = update_sides(state, above_shape, north, east, south, west);
235    sides.set_value(UP, should_raise_post(sides, top_neighbor, above_shape))
236}
237
238/// Vanilla `WallBlock.updateSides`.
239#[expect(
240    clippy::fn_params_excessive_bools,
241    reason = "mirrors vanilla WallBlock north/east/south/west signature"
242)]
243fn update_sides(
244    state: BlockStateId,
245    above_shape: OffsetVoxelShape,
246    north: bool,
247    east: bool,
248    south: bool,
249    west: bool,
250) -> BlockStateId {
251    state
252        .set_value(
253            NORTH,
254            make_wall_state(
255                north,
256                above_shape,
257                WALL_ARM_MIN,
258                WALL_ARM_MAX,
259                0.0,
260                WALL_ARM_EXTENT,
261            ),
262        )
263        .set_value(
264            EAST,
265            make_wall_state(
266                east,
267                above_shape,
268                WALL_ARM_MIN,
269                1.0,
270                WALL_ARM_MIN,
271                WALL_ARM_MAX,
272            ),
273        )
274        .set_value(
275            SOUTH,
276            make_wall_state(
277                south,
278                above_shape,
279                WALL_ARM_MIN,
280                WALL_ARM_MAX,
281                WALL_ARM_MIN,
282                1.0,
283            ),
284        )
285        .set_value(
286            WEST,
287            make_wall_state(
288                west,
289                above_shape,
290                0.0,
291                WALL_ARM_EXTENT,
292                WALL_ARM_MIN,
293                WALL_ARM_MAX,
294            ),
295        )
296}
297
298/// Vanilla `WallBlock.makeWallState`.
299fn make_wall_state(
300    connects_to_side: bool,
301    above_shape: OffsetVoxelShape,
302    x_min: f64,
303    x_max: f64,
304    z_min: f64,
305    z_max: f64,
306) -> WallSide {
307    if !connects_to_side {
308        return WallSide::None;
309    }
310    if is_covered(above_shape, x_min, x_max, z_min, z_max) {
311        WallSide::Tall
312    } else {
313        WallSide::Low
314    }
315}
316
317/// Vanilla `WallBlock.shouldRaisePost`.
318fn should_raise_post(
319    state: BlockStateId,
320    top_neighbor: BlockStateId,
321    above_shape: OffsetVoxelShape,
322) -> bool {
323    let top_neighbor_has_post = top_neighbor.get_block().has_tag(&BlockTag::WALLS)
324        && top_neighbor.try_get_value(UP).unwrap_or(false);
325    if top_neighbor_has_post {
326        return true;
327    }
328
329    let north_wall = state.get_value(NORTH);
330    let south_wall = state.get_value(SOUTH);
331    let east_wall = state.get_value(EAST);
332    let west_wall = state.get_value(WEST);
333
334    let north_none = north_wall == WallSide::None;
335    let south_none = south_wall == WallSide::None;
336    let east_none = east_wall == WallSide::None;
337    let west_none = west_wall == WallSide::None;
338
339    let has_corner = (north_none && south_none && west_none && east_none)
340        || (north_none != south_none)
341        || (west_none != east_none);
342    if has_corner {
343        return true;
344    }
345
346    let has_high_wall = (north_wall == WallSide::Tall && south_wall == WallSide::Tall)
347        || (east_wall == WallSide::Tall && west_wall == WallSide::Tall);
348    if has_high_wall {
349        return false;
350    }
351
352    top_neighbor
353        .get_block()
354        .has_tag(&BlockTag::WALL_POST_OVERRIDE)
355        || is_covered(above_shape, POST_X_MIN, POST_X_MAX, POST_Z_MIN, POST_Z_MAX)
356}
357
358/// Vanilla `WallBlock.isCovered`.
359///
360/// Checks whether the block above's collision shape fully covers a test
361/// rectangle on its DOWN face.
362fn is_covered(
363    above_shape: OffsetVoxelShape,
364    x_min: f64,
365    x_max: f64,
366    z_min: f64,
367    z_max: f64,
368) -> bool {
369    offset_face_rectangles_cover(above_shape, Direction::Down, x_min, x_max, z_min, z_max)
370}