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