1use std::sync::Arc;
7
8use steel_macros::block_behavior;
9use steel_registry::{
10 blocks::{
11 BlockRef,
12 block_state_ext::BlockStateExt as _,
13 properties::{
14 BlockStateProperties, BoolProperty, Direction, DoorHingeSide, DoubleBlockHalf,
15 EnumProperty,
16 },
17 shapes,
18 },
19 sound_event::SoundEventRef,
20 vanilla_blocks, vanilla_game_events,
21};
22use steel_utils::{
23 BlockPos, BlockStateId,
24 axis::Axis,
25 types::{InteractionHand, UpdateFlags},
26};
27
28use super::weathering_block::{WeatherState, WeatheringCopper};
29use crate::{
30 behavior::{
31 BlockBehavior, BlockHitResult, BlockPlaceContext, InteractionResult, InventoryAccess,
32 PlacementSource,
33 },
34 entity::Entity,
35 entity::ai::path::PathComputationType,
36 fluid::fluid_state_to_block,
37 player::Player,
38 world::{
39 LevelReader, ScheduledTickAccess, SignalGetter as _, World, game_event::GameEventContext,
40 },
41};
42
43#[block_behavior]
45pub struct DoorBlock {
46 block: BlockRef,
47 #[json_arg(value, json = "type_can_open_by_hand")]
48 can_open_by_hand: bool,
49 #[json_arg(sound_events, json = "type_door_open")]
50 sound_open: SoundEventRef,
51 #[json_arg(sound_events, json = "type_door_close")]
52 sound_close: SoundEventRef,
53}
54
55const DOOR_HINGE: &EnumProperty<DoorHingeSide> = &BlockStateProperties::DOOR_HINGE;
56const DOUBLE_BLOCK_HALF: &EnumProperty<DoubleBlockHalf> = &BlockStateProperties::DOUBLE_BLOCK_HALF;
57const HORIZONTAL_FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
58const OPEN: &BoolProperty = &BlockStateProperties::OPEN;
59const POWERED: &BoolProperty = &BlockStateProperties::POWERED;
60
61impl DoorBlock {
62 const USE_UPDATE_FLAGS: UpdateFlags =
63 UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_IMMEDIATE);
64
65 #[must_use]
67 pub const fn new(
68 block: BlockRef,
69 can_open_by_hand: bool,
70 sound_open: SoundEventRef,
71 sound_close: SoundEventRef,
72 ) -> Self {
73 Self {
74 block,
75 can_open_by_hand,
76 sound_open,
77 sound_close,
78 }
79 }
80
81 fn is_door(state: BlockStateId) -> bool {
82 state.try_get_value(DOOR_HINGE).is_some()
83 && state.try_get_value(DOUBLE_BLOCK_HALF).is_some()
84 }
85
86 fn is_lower_door(state: BlockStateId) -> bool {
87 Self::is_door(state) && state.get_value(DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower
88 }
89
90 fn hinge_for_placement(context: &BlockPlaceContext<'_>) -> DoorHingeSide {
91 let pos = context.place_pos();
92 let above_pos = pos.above();
93 let place_direction = context.horizontal_direction();
94
95 let left_direction = place_direction.rotate_y_counter_clockwise();
96 let left_pos = left_direction.relative(pos);
97 let left_state = context.world.get_block_state(left_pos);
98 let left_above_pos = left_direction.relative(above_pos);
99 let left_above_state = context.world.get_block_state(left_above_pos);
100
101 let right_direction = place_direction.rotate_y_clockwise();
102 let right_pos = right_direction.relative(pos);
103 let right_state = context.world.get_block_state(right_pos);
104 let right_above_pos = right_direction.relative(above_pos);
105 let right_above_state = context.world.get_block_state(right_above_pos);
106
107 let solid_block_balance = i32::from(shapes::is_offset_shape_full_block(
108 right_state.get_collision_shape_at(right_pos),
109 )) + i32::from(shapes::is_offset_shape_full_block(
110 right_above_state.get_collision_shape_at(right_above_pos),
111 )) - i32::from(shapes::is_offset_shape_full_block(
112 left_state.get_collision_shape_at(left_pos),
113 )) - i32::from(shapes::is_offset_shape_full_block(
114 left_above_state.get_collision_shape_at(left_above_pos),
115 ));
116
117 let door_left = Self::is_lower_door(left_state);
118 let door_right = Self::is_lower_door(right_state);
119
120 if (!door_left || door_right) && solid_block_balance <= 0 {
121 if (!door_right || door_left) && solid_block_balance >= 0 {
122 let (step_x, step_z) = place_direction.offset_xz();
123 let click_x = context.click_location().x - f64::from(pos.x());
124 let click_z = context.click_location().z - f64::from(pos.z());
125
126 if (step_x >= 0 || click_z >= 0.5)
127 && (step_x <= 0 || click_z <= 0.5)
128 && (step_z >= 0 || click_x <= 0.5)
129 && (step_z <= 0 || click_x >= 0.5)
130 {
131 DoorHingeSide::Left
132 } else {
133 DoorHingeSide::Right
134 }
135 } else {
136 DoorHingeSide::Left
137 }
138 } else {
139 DoorHingeSide::Right
140 }
141 }
142
143 fn has_correct_tool_for_drops(player: &Player, state: BlockStateId) -> bool {
144 let inv = player.inventory.lock();
145 let main_hand = inv.get_item_in_hand(InteractionHand::MainHand);
146 main_hand.is_correct_tool_for_drops(state)
147 || !state.get_block().config.requires_correct_tool_for_drops
148 }
149
150 fn prevent_drop_from_bottom_part(
151 world: &Arc<World>,
152 pos: BlockPos,
153 state: BlockStateId,
154 player: &Player,
155 ) {
156 if state.get_value(DOUBLE_BLOCK_HALF) != DoubleBlockHalf::Upper {
157 return;
158 }
159
160 let bottom_pos = pos.below();
161 let bottom_state = world.get_block_state(bottom_pos);
162 if bottom_state.get_block() != state.get_block()
163 || bottom_state.get_value(DOUBLE_BLOCK_HALF) != DoubleBlockHalf::Lower
164 {
165 return;
166 }
167
168 let replacement = fluid_state_to_block(bottom_state.get_fluid_state());
169 world.set_block(
170 bottom_pos,
171 replacement,
172 UpdateFlags::UPDATE_ALL | UpdateFlags::UPDATE_SUPPRESS_DROPS,
173 );
174 world.destroy_block_effect(bottom_pos, u32::from(bottom_state.0), Some(player.id()));
175 }
176
177 fn play_sound(&self, world: &Arc<World>, pos: BlockPos, open: bool, exclude: Option<i32>) {
178 let sound = if open {
179 self.sound_open
180 } else {
181 self.sound_close
182 };
183 world.play_block_sound(sound, pos, 1.0, 1.0, exclude);
184 }
185}
186
187impl BlockBehavior for DoorBlock {
188 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
189 let pos = context.place_pos();
190 if pos.y() >= context.world.max_y_exclusive() - 1 {
191 return None;
192 }
193 if !context.world.get_block_state(pos.above()).is_replaceable() {
194 return None;
195 }
196
197 let powered = context.world.has_neighbor_signal(pos)
198 || context.world.has_neighbor_signal(pos.above());
199 Some(
200 self.block
201 .default_state()
202 .set_value(HORIZONTAL_FACING, context.horizontal_direction())
203 .set_value(DOOR_HINGE, Self::hinge_for_placement(context))
204 .set_value(POWERED, powered)
205 .set_value(OPEN, powered)
206 .set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Lower),
207 )
208 }
209
210 fn update_shape(
211 &self,
212 state: BlockStateId,
213 world: &dyn ScheduledTickAccess,
214 pos: BlockPos,
215 direction: Direction,
216 _neighbor_pos: BlockPos,
217 neighbor_state: BlockStateId,
218 ) -> BlockStateId {
219 let half = state.get_value(DOUBLE_BLOCK_HALF);
220 if direction.get_axis() == Axis::Y
221 && (half == DoubleBlockHalf::Lower) == (direction == Direction::Up)
222 {
223 if Self::is_door(neighbor_state) && neighbor_state.get_value(DOUBLE_BLOCK_HALF) != half
224 {
225 return neighbor_state.set_value(DOUBLE_BLOCK_HALF, half);
226 }
227 return vanilla_blocks::AIR.default_state();
228 }
229
230 if half == DoubleBlockHalf::Lower
231 && direction == Direction::Down
232 && !self.can_survive(state, world, pos)
233 {
234 return vanilla_blocks::AIR.default_state();
235 }
236
237 state
238 }
239
240 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
241 let below_pos = pos.below();
242 let below_state = world.get_block_state(below_pos);
243 if state.get_value(DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower {
244 world.is_face_sturdy(below_state, below_pos, Direction::Up)
245 } else {
246 below_state.get_block() == self.block
247 }
248 }
249
250 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
251 match computation_type {
252 PathComputationType::Land | PathComputationType::Air => state.get_value(OPEN),
253 PathComputationType::Water => false,
254 }
255 }
256
257 fn is_wooden_door(&self, state: BlockStateId) -> bool {
258 self.can_open_by_hand && Self::is_door(state)
259 }
260
261 fn set_door_open(
262 &self,
263 state: BlockStateId,
264 world: &Arc<World>,
265 pos: BlockPos,
266 source_entity: Option<&dyn Entity>,
267 open: bool,
268 ) -> bool {
269 if !Self::is_door(state) || state.get_value(OPEN) == open {
270 return false;
271 }
272
273 let new_state = state.set_value(OPEN, open);
274 if !world.set_block(pos, new_state, Self::USE_UPDATE_FLAGS) {
275 return false;
276 }
277
278 self.play_sound(world, pos, open, source_entity.map(Entity::id));
279 let event = if open {
280 &vanilla_game_events::BLOCK_OPEN
281 } else {
282 &vanilla_game_events::BLOCK_CLOSE
283 };
284 world.game_event(event, pos, &GameEventContext::new(source_entity, None));
285 true
286 }
287
288 fn set_placed_by(
289 &self,
290 state: BlockStateId,
291 world: &Arc<World>,
292 pos: BlockPos,
293 _source: &PlacementSource<'_>,
294 ) {
295 world.set_block(
296 pos.above(),
297 state.set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Upper),
298 UpdateFlags::UPDATE_ALL,
299 );
300 }
301
302 fn player_will_destroy(
303 &self,
304 state: BlockStateId,
305 world: &Arc<World>,
306 pos: BlockPos,
307 player: &Player,
308 ) -> BlockStateId {
309 if player.has_infinite_materials() || !Self::has_correct_tool_for_drops(player, state) {
310 Self::prevent_drop_from_bottom_part(world, pos, state, player);
311 }
312 state
313 }
314
315 fn use_without_item(
316 &self,
317 state: BlockStateId,
318 world: &Arc<World>,
319 pos: BlockPos,
320 player: &Player,
321 _hit_result: &BlockHitResult,
322 _inv: &mut InventoryAccess,
323 ) -> InteractionResult {
324 if !self.can_open_by_hand {
325 return InteractionResult::Pass;
326 }
327
328 let open = !state.get_value(OPEN);
329 let new_state = state.set_value(OPEN, open);
330 world.set_block(pos, new_state, Self::USE_UPDATE_FLAGS);
331 self.play_sound(world, pos, open, Some(player.id()));
332 let event = if open {
333 &vanilla_game_events::BLOCK_OPEN
334 } else {
335 &vanilla_game_events::BLOCK_CLOSE
336 };
337 world.game_event(event, pos, &GameEventContext::new(Some(player), None));
338 InteractionResult::Success
339 }
340
341 fn handle_neighbor_changed(
342 &self,
343 state: BlockStateId,
344 world: &Arc<World>,
345 pos: BlockPos,
346 source_block: BlockRef,
347 _moved_by_piston: bool,
348 ) {
349 if source_block == self.block {
350 return;
351 }
352
353 let half = state.get_value(DOUBLE_BLOCK_HALF);
354 let other_half_pos = if half == DoubleBlockHalf::Lower {
355 pos.above()
356 } else {
357 pos.below()
358 };
359 let signal = world.has_neighbor_signal(pos) || world.has_neighbor_signal(other_half_pos);
360 if signal == state.get_value(POWERED) {
361 return;
362 }
363
364 if signal != state.get_value(OPEN) {
365 self.play_sound(world, pos, signal, None);
366 let event = if signal {
367 &vanilla_game_events::BLOCK_OPEN
368 } else {
369 &vanilla_game_events::BLOCK_CLOSE
370 };
371 world.game_event(event, pos, &GameEventContext::default());
372 }
373
374 let new_state = state.set_value(POWERED, signal).set_value(OPEN, signal);
375 world.set_block(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
376 }
377}
378
379#[block_behavior]
381pub struct WeatheringCopperDoorBlock {
382 block: BlockRef,
383 #[json_arg(r#enum = "WeatherState", json = "weather_state")]
384 weathering: WeatheringCopper,
385 #[json_arg(value, json = "type_can_open_by_hand")]
386 can_open_by_hand: bool,
387 #[json_arg(sound_events, json = "type_door_open")]
388 sound_open: SoundEventRef,
389 #[json_arg(sound_events, json = "type_door_close")]
390 sound_close: SoundEventRef,
391}
392
393impl WeatheringCopperDoorBlock {
394 #[must_use]
396 pub const fn new(
397 block: BlockRef,
398 weather_state: WeatherState,
399 can_open_by_hand: bool,
400 sound_open: SoundEventRef,
401 sound_close: SoundEventRef,
402 ) -> Self {
403 Self {
404 block,
405 weathering: WeatheringCopper::new(weather_state),
406 can_open_by_hand,
407 sound_open,
408 sound_close,
409 }
410 }
411
412 const fn door(&self) -> DoorBlock {
413 DoorBlock::new(
414 self.block,
415 self.can_open_by_hand,
416 self.sound_open,
417 self.sound_close,
418 )
419 }
420}
421
422impl BlockBehavior for WeatheringCopperDoorBlock {
423 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
424 self.door().get_state_for_placement(context)
425 }
426
427 fn update_shape(
428 &self,
429 state: BlockStateId,
430 world: &dyn ScheduledTickAccess,
431 pos: BlockPos,
432 direction: Direction,
433 neighbor_pos: BlockPos,
434 neighbor_state: BlockStateId,
435 ) -> BlockStateId {
436 self.door()
437 .update_shape(state, world, pos, direction, neighbor_pos, neighbor_state)
438 }
439
440 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
441 self.door().can_survive(state, world, pos)
442 }
443
444 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
445 self.door().is_pathfindable(state, computation_type)
446 }
447
448 fn is_wooden_door(&self, state: BlockStateId) -> bool {
449 self.door().is_wooden_door(state)
450 }
451
452 fn set_door_open(
453 &self,
454 state: BlockStateId,
455 world: &Arc<World>,
456 pos: BlockPos,
457 source_entity: Option<&dyn Entity>,
458 open: bool,
459 ) -> bool {
460 self.door()
461 .set_door_open(state, world, pos, source_entity, open)
462 }
463
464 fn set_placed_by(
465 &self,
466 state: BlockStateId,
467 world: &Arc<World>,
468 pos: BlockPos,
469 source: &PlacementSource<'_>,
470 ) {
471 self.door().set_placed_by(state, world, pos, source);
472 }
473
474 fn player_will_destroy(
475 &self,
476 state: BlockStateId,
477 world: &Arc<World>,
478 pos: BlockPos,
479 player: &Player,
480 ) -> BlockStateId {
481 self.door().player_will_destroy(state, world, pos, player)
482 }
483
484 fn use_without_item(
485 &self,
486 state: BlockStateId,
487 world: &Arc<World>,
488 pos: BlockPos,
489 player: &Player,
490 hit_result: &BlockHitResult,
491 inv: &mut InventoryAccess,
492 ) -> InteractionResult {
493 self.door()
494 .use_without_item(state, world, pos, player, hit_result, inv)
495 }
496
497 fn handle_neighbor_changed(
498 &self,
499 state: BlockStateId,
500 world: &Arc<World>,
501 pos: BlockPos,
502 source_block: BlockRef,
503 moved_by_piston: bool,
504 ) {
505 self.door()
506 .handle_neighbor_changed(state, world, pos, source_block, moved_by_piston);
507 }
508
509 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
510 if state.get_value(DOUBLE_BLOCK_HALF) == DoubleBlockHalf::Lower {
511 self.weathering.change_over_time(state, world, pos);
512 }
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use steel_registry::{init_vanilla_registry, sound_events, vanilla_blocks};
519 use steel_utils::BlockPos;
520
521 use crate::test_support::TestLevel;
522
523 use super::*;
524
525 #[test]
526 fn lower_half_copies_transformed_upper_half_state() {
527 init_vanilla_registry();
528 let behavior = DoorBlock::new(
529 &vanilla_blocks::SPRUCE_DOOR,
530 true,
531 &sound_events::BLOCK_WOODEN_DOOR_OPEN,
532 &sound_events::BLOCK_WOODEN_DOOR_CLOSE,
533 );
534 let lower = vanilla_blocks::SPRUCE_DOOR
535 .default_state()
536 .set_value(HORIZONTAL_FACING, Direction::West)
537 .set_value(DOOR_HINGE, DoorHingeSide::Right)
538 .set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Lower)
539 .set_value(OPEN, false)
540 .set_value(POWERED, false);
541 let upper = vanilla_blocks::SPRUCE_DOOR
542 .default_state()
543 .set_value(HORIZONTAL_FACING, Direction::South)
544 .set_value(DOOR_HINGE, DoorHingeSide::Left)
545 .set_value(DOUBLE_BLOCK_HALF, DoubleBlockHalf::Upper)
546 .set_value(OPEN, false)
547 .set_value(POWERED, false);
548 let level = TestLevel::default();
549
550 let updated = behavior.update_shape(
551 lower,
552 &level,
553 BlockPos::ZERO,
554 Direction::Up,
555 BlockPos::ZERO.above(),
556 upper,
557 );
558
559 assert_eq!(updated.get_value(DOUBLE_BLOCK_HALF), DoubleBlockHalf::Lower);
560 assert_eq!(updated.get_value(HORIZONTAL_FACING), Direction::South);
561 assert_eq!(updated.get_value(DOOR_HINGE), DoorHingeSide::Left);
562 }
563
564 #[test]
565 fn door_wooden_query_uses_can_open_by_hand_like_vanilla() {
566 init_vanilla_registry();
567 let oak = DoorBlock::new(
568 &vanilla_blocks::OAK_DOOR,
569 true,
570 &sound_events::BLOCK_WOODEN_DOOR_OPEN,
571 &sound_events::BLOCK_WOODEN_DOOR_CLOSE,
572 );
573 let iron = DoorBlock::new(
574 &vanilla_blocks::IRON_DOOR,
575 false,
576 &sound_events::BLOCK_IRON_DOOR_OPEN,
577 &sound_events::BLOCK_IRON_DOOR_CLOSE,
578 );
579
580 assert!(oak.is_wooden_door(vanilla_blocks::OAK_DOOR.default_state()));
581 assert!(!iron.is_wooden_door(vanilla_blocks::IRON_DOOR.default_state()));
582 assert!(!oak.is_wooden_door(vanilla_blocks::STONE.default_state()));
583 }
584}