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