619 lines
20 KiB
Rust
619 lines
20 KiB
Rust
use std::sync::Arc;
|
|
|
|
use futures::future::join;
|
|
use pumpkin_data::block_properties::{
|
|
BlockProperties, ChestLikeProperties, ChestType, HorizontalFacing,
|
|
};
|
|
use pumpkin_data::entity::EntityPose;
|
|
use pumpkin_data::{Block, BlockDirection, translation};
|
|
use pumpkin_inventory::double::DoubleInventory;
|
|
use pumpkin_inventory::generic_container_screen_handler::{create_generic_9x3, create_generic_9x6};
|
|
use pumpkin_inventory::player::player_inventory::PlayerInventory;
|
|
use pumpkin_inventory::screen_handler::{
|
|
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
|
|
};
|
|
use pumpkin_macros::{pumpkin_block, pumpkin_block_from_tag};
|
|
use pumpkin_util::math::position::BlockPos;
|
|
use pumpkin_util::text::TextComponent;
|
|
use pumpkin_world::BlockStateId;
|
|
use pumpkin_world::block::entities::BlockEntity;
|
|
use pumpkin_world::block::entities::chest::ChestBlockEntity;
|
|
use pumpkin_world::inventory::Inventory;
|
|
use pumpkin_world::world::BlockFlags;
|
|
use tokio::sync::Mutex;
|
|
|
|
use crate::block::{
|
|
BlockFuture, BrokenArgs, EmitsRedstonePowerArgs, GetRedstonePowerArgs, NormalUseArgs,
|
|
OnPlaceArgs, OnSyncedBlockEventArgs, PlacedArgs, RandomTickArgs,
|
|
};
|
|
use crate::entity::EntityBase;
|
|
use crate::world::World;
|
|
use crate::{
|
|
block::{BlockBehaviour, registry::BlockActionResult},
|
|
entity::player::Player,
|
|
};
|
|
|
|
struct ChestScreenFactory(Arc<dyn Inventory>);
|
|
|
|
impl ScreenHandlerFactory for ChestScreenFactory {
|
|
fn create_screen_handler<'a>(
|
|
&'a self,
|
|
sync_id: u8,
|
|
player_inventory: &'a Arc<PlayerInventory>,
|
|
_player: &'a dyn InventoryPlayer,
|
|
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
|
Box::pin(async move {
|
|
let concrete_handler = if self.0.size() > 27 {
|
|
create_generic_9x6(sync_id, player_inventory, self.0.clone()).await
|
|
} else {
|
|
create_generic_9x3(sync_id, player_inventory, self.0.clone()).await
|
|
};
|
|
|
|
let concrete_arc = Arc::new(Mutex::new(concrete_handler));
|
|
|
|
Some(concrete_arc as SharedScreenHandler)
|
|
})
|
|
}
|
|
|
|
fn get_display_name(&self) -> TextComponent {
|
|
if self.0.size() > 27 {
|
|
TextComponent::translate_cross(
|
|
translation::java::CONTAINER_CHESTDOUBLE,
|
|
translation::bedrock::CONTAINER_CHESTDOUBLE,
|
|
&[],
|
|
)
|
|
} else {
|
|
TextComponent::translate_cross(
|
|
translation::java::CONTAINER_CHEST,
|
|
translation::bedrock::CONTAINER_CHEST,
|
|
&[],
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared chest behavior implementations
|
|
const LID_ANIMATION_EVENT_TYPE: u8 = 1;
|
|
|
|
async fn on_place_chest_impl(args: OnPlaceArgs<'_>) -> BlockStateId {
|
|
let mut chest_props = ChestLikeProperties::default(args.block);
|
|
chest_props.waterlogged = args.replacing.water_source();
|
|
|
|
let (r#type, facing) = compute_chest_props(
|
|
args.world,
|
|
args.player,
|
|
args.block,
|
|
args.position,
|
|
args.direction,
|
|
)
|
|
.await;
|
|
chest_props.facing = facing;
|
|
chest_props.r#type = r#type;
|
|
|
|
chest_props.to_state_id(args.block)
|
|
}
|
|
|
|
async fn placed_chest_impl<E: BlockEntity + 'static>(
|
|
args: PlacedArgs<'_>,
|
|
create_entity: impl FnOnce(BlockPos) -> E,
|
|
) {
|
|
let chest = create_entity(*args.position);
|
|
args.world.add_block_entity(Arc::new(chest)).await;
|
|
|
|
let chest_props = ChestLikeProperties::from_state_id(args.state_id, args.block);
|
|
let connected_towards = match chest_props.r#type {
|
|
ChestType::Single => return,
|
|
ChestType::Left => chest_props.facing.rotate_clockwise(),
|
|
ChestType::Right => chest_props.facing.rotate_counter_clockwise(),
|
|
};
|
|
|
|
if let Some(mut neighbor_props) = get_chest_properties_if_can_connect(
|
|
args.world,
|
|
args.block,
|
|
args.position,
|
|
chest_props.facing,
|
|
connected_towards,
|
|
ChestType::Single,
|
|
)
|
|
.await
|
|
{
|
|
neighbor_props.r#type = chest_props.r#type.opposite();
|
|
|
|
args.world
|
|
.set_block_state(
|
|
&args.position.offset(connected_towards.to_offset()),
|
|
neighbor_props.to_state_id(args.block),
|
|
BlockFlags::NOTIFY_LISTENERS,
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
async fn normal_use_chest_impl(args: NormalUseArgs<'_>) -> BlockActionResult {
|
|
let (state, first_chest) = join(
|
|
args.world.get_block_state_id(args.position),
|
|
args.world.get_block_entity(args.position),
|
|
)
|
|
.await;
|
|
|
|
let Some(first_inventory) = first_chest.and_then(BlockEntity::get_inventory) else {
|
|
return BlockActionResult::Fail;
|
|
};
|
|
|
|
let chest_props = ChestLikeProperties::from_state_id(state, args.block);
|
|
let connected_towards = match chest_props.r#type {
|
|
ChestType::Single => None,
|
|
ChestType::Left => Some(chest_props.facing.rotate_clockwise()),
|
|
ChestType::Right => Some(chest_props.facing.rotate_counter_clockwise()),
|
|
};
|
|
|
|
if is_chest_blocked(args.world, args.position).await {
|
|
return BlockActionResult::Success;
|
|
}
|
|
|
|
if let Some(direction) = connected_towards {
|
|
let neighbor_pos = args.position.offset(direction.to_offset());
|
|
if is_chest_blocked(args.world, &neighbor_pos).await {
|
|
return BlockActionResult::Success;
|
|
}
|
|
}
|
|
|
|
let inventory = if let Some(direction) = connected_towards
|
|
&& let Some(second_inventory) = args
|
|
.world
|
|
.get_block_entity(&args.position.offset(direction.to_offset()))
|
|
.await
|
|
.and_then(BlockEntity::get_inventory)
|
|
{
|
|
// Vanilla: chestType == ChestType.RIGHT ? DoubleBlockProperties.Type.FIRST : DoubleBlockProperties.Type.SECOND;
|
|
if matches!(chest_props.r#type, ChestType::Right) {
|
|
DoubleInventory::new(first_inventory, second_inventory)
|
|
} else {
|
|
DoubleInventory::new(second_inventory, first_inventory)
|
|
}
|
|
} else {
|
|
first_inventory
|
|
};
|
|
|
|
args.player
|
|
.open_handled_screen(&ChestScreenFactory(inventory), Some(*args.position))
|
|
.await;
|
|
|
|
BlockActionResult::Success
|
|
}
|
|
|
|
async fn broken_chest_impl(args: BrokenArgs<'_>) {
|
|
let chest_props = ChestLikeProperties::from_state_id(args.state.id, args.block);
|
|
let connected_towards = match chest_props.r#type {
|
|
ChestType::Single => return,
|
|
ChestType::Left => chest_props.facing.rotate_clockwise(),
|
|
ChestType::Right => chest_props.facing.rotate_counter_clockwise(),
|
|
};
|
|
|
|
if let Some(mut neighbor_props) = get_chest_properties_if_can_connect(
|
|
args.world,
|
|
args.block,
|
|
args.position,
|
|
chest_props.facing,
|
|
connected_towards,
|
|
chest_props.r#type.opposite(),
|
|
)
|
|
.await
|
|
{
|
|
neighbor_props.r#type = ChestType::Single;
|
|
|
|
args.world
|
|
.set_block_state(
|
|
&args.position.offset(connected_towards.to_offset()),
|
|
neighbor_props.to_state_id(args.block),
|
|
BlockFlags::NOTIFY_LISTENERS,
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
|
|
#[pumpkin_block_from_tag("c:chests/wooden")]
|
|
pub struct ChestBlock;
|
|
|
|
impl BlockBehaviour for ChestBlock {
|
|
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
|
|
Box::pin(on_place_chest_impl(args))
|
|
}
|
|
|
|
fn on_synced_block_event<'a>(
|
|
&'a self,
|
|
args: OnSyncedBlockEventArgs<'a>,
|
|
) -> BlockFuture<'a, bool> {
|
|
Box::pin(async move { args.r#type == LID_ANIMATION_EVENT_TYPE })
|
|
}
|
|
|
|
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
|
Box::pin(placed_chest_impl(args, ChestBlockEntity::new))
|
|
}
|
|
|
|
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
|
Box::pin(normal_use_chest_impl(args))
|
|
}
|
|
|
|
fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> {
|
|
Box::pin(broken_chest_impl(args))
|
|
}
|
|
}
|
|
|
|
/// Copper chests have the same behavior as wooden chests but also oxidize over time.
|
|
#[pumpkin_block_from_tag("minecraft:copper_chests")]
|
|
pub struct CopperChestBlock;
|
|
|
|
impl BlockBehaviour for CopperChestBlock {
|
|
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
|
|
Box::pin(on_place_chest_impl(args))
|
|
}
|
|
|
|
fn on_synced_block_event<'a>(
|
|
&'a self,
|
|
args: OnSyncedBlockEventArgs<'a>,
|
|
) -> BlockFuture<'a, bool> {
|
|
Box::pin(async move { args.r#type == LID_ANIMATION_EVENT_TYPE })
|
|
}
|
|
|
|
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
|
Box::pin(placed_chest_impl(args, ChestBlockEntity::new))
|
|
}
|
|
|
|
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
|
Box::pin(normal_use_chest_impl(args))
|
|
}
|
|
|
|
fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> {
|
|
Box::pin(broken_chest_impl(args))
|
|
}
|
|
|
|
fn random_tick<'a>(&'a self, args: RandomTickArgs<'a>) -> BlockFuture<'a, ()> {
|
|
Box::pin(async move {
|
|
let current_state_id = args.world.get_block_state_id(args.position).await;
|
|
let chest_props = ChestLikeProperties::from_state_id(current_state_id, args.block);
|
|
|
|
// Only oxidize LEFT or SINGLE chests (not RIGHT) to prevent double oxidation
|
|
if chest_props.r#type == ChestType::Right {
|
|
return;
|
|
}
|
|
|
|
// Only oxidize if no players are viewing the chest
|
|
if let Some(block_entity) = args.world.get_block_entity(args.position).await
|
|
&& let Some(chest_entity) = block_entity.as_any().downcast_ref::<ChestBlockEntity>()
|
|
&& chest_entity.get_viewer_count() > 0
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Try to oxidize the copper chest
|
|
try_oxidize_copper_chest(args.world, args.position, args.block, chest_props).await;
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Copper oxidation levels with their ordinal values
|
|
const COPPER_CHEST_OXIDATION: &[(&Block, &Block, u8)] = &[
|
|
(&Block::COPPER_CHEST, &Block::EXPOSED_COPPER_CHEST, 0),
|
|
(
|
|
&Block::EXPOSED_COPPER_CHEST,
|
|
&Block::WEATHERED_COPPER_CHEST,
|
|
1,
|
|
),
|
|
(
|
|
&Block::WEATHERED_COPPER_CHEST,
|
|
&Block::OXIDIZED_COPPER_CHEST,
|
|
2,
|
|
),
|
|
];
|
|
|
|
/// Get the oxidation level ordinal for a block (None if not oxidizable copper chest)
|
|
fn get_oxidation_level(block: &Block) -> Option<u8> {
|
|
// Check non-waxed variants
|
|
if block == &Block::COPPER_CHEST {
|
|
return Some(0);
|
|
}
|
|
if block == &Block::EXPOSED_COPPER_CHEST {
|
|
return Some(1);
|
|
}
|
|
if block == &Block::WEATHERED_COPPER_CHEST {
|
|
return Some(2);
|
|
}
|
|
if block == &Block::OXIDIZED_COPPER_CHEST {
|
|
return Some(3);
|
|
}
|
|
// Waxed variants don't oxidize
|
|
None
|
|
}
|
|
|
|
/// Try to oxidize a copper chest to its next oxidation level.
|
|
/// Uses vanilla's degradation algorithm with neighbor checking.
|
|
async fn try_oxidize_copper_chest(
|
|
world: &Arc<World>,
|
|
position: &BlockPos,
|
|
current_block: &Block,
|
|
chest_props: ChestLikeProperties,
|
|
) {
|
|
use rand::RngExt;
|
|
|
|
// Base chance per random tick: ~5.69%
|
|
const BASE_DEGRADATION_CHANCE: f32 = 0.056_888_89;
|
|
|
|
// First roll: only ~5.69% chance to even attempt oxidation
|
|
if rand::rng().random::<f32>() >= BASE_DEGRADATION_CHANCE {
|
|
return;
|
|
}
|
|
|
|
// Find the next oxidation level
|
|
let (next_block, current_level) = match COPPER_CHEST_OXIDATION
|
|
.iter()
|
|
.find(|(from, _, _)| *from == current_block)
|
|
{
|
|
Some((_, to, level)) => (*to, *level),
|
|
None => return, // Already fully oxidized or waxed
|
|
};
|
|
|
|
// Scan neighbors in 4-block Manhattan distance to calculate oxidation chance
|
|
let (same_level_count, higher_level_count) =
|
|
count_neighbor_oxidation_levels(world, position, current_level).await;
|
|
|
|
// If we found any neighbors at a LOWER level, oxidation is blocked
|
|
// (This is handled in count_neighbor_oxidation_levels by returning early)
|
|
|
|
// Calculate weighted probability: ((higher + 1) / (higher + same + 1))^2 * multiplier
|
|
let ratio =
|
|
(higher_level_count + 1) as f32 / (higher_level_count + same_level_count + 1) as f32;
|
|
// Multiplier is 0.75 for UNAFFECTED (level 0), 1.0 for others
|
|
let multiplier = if current_level == 0 { 0.75 } else { 1.0 };
|
|
let final_chance = ratio * ratio * multiplier;
|
|
|
|
if rand::rng().random::<f32>() >= final_chance {
|
|
return;
|
|
}
|
|
|
|
// Apply oxidation with same properties
|
|
let new_state_id = chest_props.to_state_id(next_block);
|
|
world
|
|
.set_block_state(position, new_state_id, BlockFlags::NOTIFY_LISTENERS)
|
|
.await;
|
|
}
|
|
|
|
/// Count copper blocks at same and higher oxidation levels within 4-block Manhattan distance.
|
|
/// Returns (same, higher) counts, or (0, 0) if a lower-level neighbor was found (blocking oxidation).
|
|
async fn count_neighbor_oxidation_levels(
|
|
world: &Arc<World>,
|
|
center: &BlockPos,
|
|
current_level: u8,
|
|
) -> (i32, i32) {
|
|
use std::cmp::Ordering;
|
|
|
|
let mut same_level_count = 0i32;
|
|
let mut higher_level_count = 0i32;
|
|
|
|
// Iterate in a 4-block Manhattan distance (9x9x9 cube checked with distance filter)
|
|
for dx in -4i32..=4 {
|
|
for dy in -4i32..=4 {
|
|
for dz in -4i32..=4 {
|
|
let manhattan_dist = dx.abs() + dy.abs() + dz.abs();
|
|
if manhattan_dist > 4 || manhattan_dist == 0 {
|
|
continue;
|
|
}
|
|
|
|
let neighbor_pos = BlockPos(pumpkin_util::math::vector3::Vector3::new(
|
|
center.0.x + dx,
|
|
center.0.y + dy,
|
|
center.0.z + dz,
|
|
));
|
|
|
|
let neighbor_block = world.get_block(&neighbor_pos).await;
|
|
|
|
if let Some(neighbor_level) = get_oxidation_level(neighbor_block) {
|
|
match neighbor_level.cmp(¤t_level) {
|
|
Ordering::Less => {
|
|
// Found a neighbor at lower oxidation level - block oxidation entirely
|
|
return (0, 0);
|
|
}
|
|
Ordering::Greater => {
|
|
higher_level_count += 1;
|
|
}
|
|
Ordering::Equal => {
|
|
same_level_count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
(same_level_count, higher_level_count)
|
|
}
|
|
|
|
/// Trapped chests have the same behavior as wooden chests but also emit redstone power based on viewer count.
|
|
#[pumpkin_block("minecraft:trapped_chest")]
|
|
pub struct TrappedChestBlock;
|
|
|
|
impl BlockBehaviour for TrappedChestBlock {
|
|
fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
|
|
Box::pin(on_place_chest_impl(args))
|
|
}
|
|
|
|
fn on_synced_block_event<'a>(
|
|
&'a self,
|
|
args: OnSyncedBlockEventArgs<'a>,
|
|
) -> BlockFuture<'a, bool> {
|
|
Box::pin(async move { args.r#type == LID_ANIMATION_EVENT_TYPE })
|
|
}
|
|
|
|
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
|
use pumpkin_world::block::entities::trapped_chest::TrappedChestBlockEntity;
|
|
Box::pin(placed_chest_impl(args, TrappedChestBlockEntity::new))
|
|
}
|
|
|
|
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
|
Box::pin(normal_use_chest_impl(args))
|
|
}
|
|
|
|
fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> {
|
|
Box::pin(broken_chest_impl(args))
|
|
}
|
|
|
|
fn emits_redstone_power<'a>(
|
|
&'a self,
|
|
_args: EmitsRedstonePowerArgs<'a>,
|
|
) -> BlockFuture<'a, bool> {
|
|
Box::pin(async move { true })
|
|
}
|
|
|
|
fn get_weak_redstone_power<'a>(
|
|
&'a self,
|
|
args: GetRedstonePowerArgs<'a>,
|
|
) -> BlockFuture<'a, u8> {
|
|
Box::pin(async move {
|
|
use pumpkin_world::block::entities::trapped_chest::TrappedChestBlockEntity;
|
|
|
|
// Get viewer count from this chest
|
|
let viewer_count = if let Some(block_entity) =
|
|
args.world.get_block_entity(args.position).await
|
|
&& let Some(trapped_chest) = block_entity
|
|
.as_any()
|
|
.downcast_ref::<TrappedChestBlockEntity>()
|
|
{
|
|
trapped_chest.get_viewer_count()
|
|
} else {
|
|
0
|
|
};
|
|
|
|
viewer_count.min(15) as u8
|
|
})
|
|
}
|
|
|
|
fn get_strong_redstone_power<'a>(
|
|
&'a self,
|
|
args: GetRedstonePowerArgs<'a>,
|
|
) -> BlockFuture<'a, u8> {
|
|
Box::pin(async move {
|
|
// Strong power emitted to the block beneath the trapped chest
|
|
// The block below queries with direction Up (from below looking up at the chest)
|
|
if args.direction == BlockDirection::Up {
|
|
self.get_weak_redstone_power(args).await
|
|
} else {
|
|
0
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn compute_chest_props(
|
|
world: &World,
|
|
player: &Player,
|
|
block: &Block,
|
|
block_pos: &BlockPos,
|
|
face: BlockDirection,
|
|
) -> (ChestType, HorizontalFacing) {
|
|
let player_facing = player.get_entity().get_horizontal_facing();
|
|
let chest_facing = player_facing.opposite();
|
|
|
|
if player.get_entity().pose.load() == EntityPose::Crouching {
|
|
let Some(face) = face.to_horizontal_facing() else {
|
|
return (ChestType::Single, chest_facing);
|
|
};
|
|
|
|
let (clicked_block, clicked_block_state) = world
|
|
.get_block_and_state_id(&block_pos.offset(face.to_offset()))
|
|
.await;
|
|
|
|
if clicked_block == block {
|
|
let clicked_props =
|
|
ChestLikeProperties::from_state_id(clicked_block_state, clicked_block);
|
|
|
|
if clicked_props.r#type != ChestType::Single {
|
|
return (ChestType::Single, chest_facing);
|
|
}
|
|
|
|
if clicked_props.facing.rotate_clockwise() == face {
|
|
return (ChestType::Left, clicked_props.facing);
|
|
} else if clicked_props.facing.rotate_counter_clockwise() == face {
|
|
return (ChestType::Right, clicked_props.facing);
|
|
}
|
|
}
|
|
|
|
return (ChestType::Single, chest_facing);
|
|
}
|
|
|
|
if get_chest_properties_if_can_connect(
|
|
world,
|
|
block,
|
|
block_pos,
|
|
chest_facing,
|
|
chest_facing.rotate_clockwise(),
|
|
ChestType::Single,
|
|
)
|
|
.await
|
|
.is_some()
|
|
{
|
|
(ChestType::Left, chest_facing)
|
|
} else if get_chest_properties_if_can_connect(
|
|
world,
|
|
block,
|
|
block_pos,
|
|
chest_facing,
|
|
chest_facing.rotate_counter_clockwise(),
|
|
ChestType::Single,
|
|
)
|
|
.await
|
|
.is_some()
|
|
{
|
|
(ChestType::Right, chest_facing)
|
|
} else {
|
|
(ChestType::Single, chest_facing)
|
|
}
|
|
}
|
|
|
|
async fn get_chest_properties_if_can_connect(
|
|
world: &World,
|
|
block: &Block,
|
|
block_pos: &BlockPos,
|
|
facing: HorizontalFacing,
|
|
direction: HorizontalFacing,
|
|
wanted_type: ChestType,
|
|
) -> Option<ChestLikeProperties> {
|
|
let (neighbor_block, neighbor_block_state) = world
|
|
.get_block_and_state_id(&block_pos.offset(direction.to_offset()))
|
|
.await;
|
|
|
|
if neighbor_block != block {
|
|
return None;
|
|
}
|
|
|
|
let neighbor_props = ChestLikeProperties::from_state_id(neighbor_block_state, neighbor_block);
|
|
if neighbor_props.facing == facing && neighbor_props.r#type == wanted_type {
|
|
return Some(neighbor_props);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
async fn is_chest_blocked(world: &World, block_pos: &BlockPos) -> bool {
|
|
// TODO: Block opening when a cat is sitting on top.
|
|
has_block_on_top(world, block_pos).await
|
|
}
|
|
async fn has_block_on_top(world: &World, block_pos: &BlockPos) -> bool {
|
|
let above_pos = block_pos.up();
|
|
let above_state = world.get_block_state(&above_pos).await;
|
|
above_state.is_solid_block()
|
|
}
|
|
|
|
trait ChestTypeExt {
|
|
fn opposite(&self) -> ChestType;
|
|
}
|
|
|
|
impl ChestTypeExt for ChestType {
|
|
fn opposite(&self) -> Self {
|
|
match self {
|
|
Self::Single => Self::Single,
|
|
Self::Left => Self::Right,
|
|
Self::Right => Self::Left,
|
|
}
|
|
}
|
|
}
|