1use 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#[block_behavior]
32pub struct WallBlock {
33 block: BlockRef,
34}
35
36const UP: &BoolProperty = &BlockStateProperties::UP;
38const NORTH: &EnumProperty<WallSide> = &BlockStateProperties::NORTH_WALL;
40const EAST: &EnumProperty<WallSide> = &BlockStateProperties::EAST_WALL;
42const SOUTH: &EnumProperty<WallSide> = &BlockStateProperties::SOUTH_WALL;
44const WEST: &EnumProperty<WallSide> = &BlockStateProperties::WEST_WALL;
46const WATERLOGGED: &BoolProperty = &BlockStateProperties::WATERLOGGED;
48
49const 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
55const 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 #[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 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 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
147fn is_connected(state: BlockStateId, side: &EnumProperty<WallSide>) -> bool {
149 state.get_value(side) != WallSide::None
150}
151
152fn 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
168fn 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
177fn 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#[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#[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
298fn 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
317fn 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
358fn 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}