346 lines
12 KiB
Rust
346 lines
12 KiB
Rust
use crate::block::entities::BlockEntity;
|
|
use crate::entity::experience_orb::ExperienceOrbEntity;
|
|
use crate::world::World;
|
|
use pumpkin_data::block_properties::{BlockProperties, FacingHopper, HopperLikeProperties};
|
|
use pumpkin_data::item_stack::ItemStack;
|
|
use pumpkin_data::tag::Taggable;
|
|
use pumpkin_data::{Block, tag};
|
|
use pumpkin_nbt::compound::NbtCompound;
|
|
use pumpkin_nbt::tag::NbtTag;
|
|
use pumpkin_util::math::position::BlockPos;
|
|
use pumpkin_util::math::vector3::Vector3;
|
|
use pumpkin_world::BlockStateId;
|
|
use pumpkin_world::inventory::{Clearable, Inventory, InventoryFuture, split_stack};
|
|
use std::any::Any;
|
|
use std::array::from_fn;
|
|
use std::pin::Pin;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::Ordering;
|
|
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64};
|
|
use tokio::sync::Mutex;
|
|
|
|
pub struct HopperBlockEntity {
|
|
pub position: BlockPos,
|
|
pub items: [Arc<Mutex<ItemStack>>; Self::INVENTORY_SIZE],
|
|
pub dirty: AtomicBool,
|
|
pub facing: FacingHopper,
|
|
pub cooldown_time: AtomicI32,
|
|
pub ticked_game_time: AtomicI64,
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn to_offset(facing: &FacingHopper) -> Vector3<i32> {
|
|
match facing {
|
|
FacingHopper::Down => (0, -1, 0),
|
|
FacingHopper::North => (0, 0, -1),
|
|
FacingHopper::South => (0, 0, 1),
|
|
FacingHopper::West => (-1, 0, 0),
|
|
FacingHopper::East => (1, 0, 0),
|
|
}
|
|
.into()
|
|
}
|
|
|
|
impl BlockEntity for HopperBlockEntity {
|
|
fn write_nbt<'a>(
|
|
&'a self,
|
|
nbt: &'a mut NbtCompound,
|
|
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
|
Box::pin(async move {
|
|
nbt.put(
|
|
"TransferCooldown",
|
|
NbtTag::Int(self.cooldown_time.load(Ordering::Relaxed)),
|
|
);
|
|
self.write_inventory_nbt(nbt, true).await;
|
|
})
|
|
}
|
|
|
|
fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self
|
|
where
|
|
Self: Sized,
|
|
{
|
|
let hopper = Self {
|
|
position,
|
|
items: from_fn(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone()))),
|
|
dirty: AtomicBool::new(false),
|
|
facing: FacingHopper::Down,
|
|
cooldown_time: AtomicI32::from(nbt.get_int("TransferCooldown").unwrap_or(-1)),
|
|
ticked_game_time: AtomicI64::new(0),
|
|
};
|
|
|
|
hopper.read_data(nbt, &hopper.items);
|
|
|
|
hopper
|
|
}
|
|
|
|
fn tick<'a>(&'a self, world: &'a Arc<World>) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
|
Box::pin(async move {
|
|
self.ticked_game_time
|
|
.store(world.get_world_age().await, Ordering::Relaxed);
|
|
if self.cooldown_time.fetch_sub(1, Ordering::Relaxed) <= 0 {
|
|
self.cooldown_time.store(0, Ordering::Relaxed);
|
|
let state = HopperLikeProperties::from_state_id(
|
|
world.get_block_state(&self.position).await.id,
|
|
&Block::HOPPER,
|
|
);
|
|
self.try_move_items(&state, world).await;
|
|
}
|
|
})
|
|
}
|
|
|
|
fn resource_location(&self) -> &'static str {
|
|
Self::ID
|
|
}
|
|
|
|
fn get_position(&self) -> BlockPos {
|
|
self.position
|
|
}
|
|
|
|
fn get_inventory(self: Arc<Self>) -> Option<Arc<dyn Inventory>> {
|
|
Some(self)
|
|
}
|
|
|
|
fn set_block_state(&mut self, block_state: BlockStateId) {
|
|
// TODO !!!IMPORTANT!!! set block state when loading the chunk
|
|
self.facing = HopperLikeProperties::from_state_id(block_state, &Block::HOPPER).facing;
|
|
}
|
|
|
|
fn is_dirty(&self) -> bool {
|
|
self.dirty.load(Ordering::Relaxed)
|
|
}
|
|
|
|
fn clear_dirty(&self) {
|
|
self.dirty.store(false, Ordering::Relaxed);
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl HopperBlockEntity {
|
|
pub const INVENTORY_SIZE: usize = 5;
|
|
pub const ID: &'static str = "minecraft:hopper";
|
|
|
|
#[must_use]
|
|
pub fn new(position: BlockPos, facing: FacingHopper) -> Self {
|
|
Self {
|
|
position,
|
|
items: from_fn(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone()))),
|
|
dirty: AtomicBool::new(false),
|
|
facing,
|
|
cooldown_time: AtomicI32::new(-1),
|
|
ticked_game_time: AtomicI64::new(0),
|
|
}
|
|
}
|
|
async fn try_move_items(&self, state: &HopperLikeProperties, world: &Arc<World>) {
|
|
if self.cooldown_time.load(Ordering::Relaxed) <= 0 && state.enabled {
|
|
let mut success = if self.is_empty().await {
|
|
false
|
|
} else {
|
|
self.eject_items(world).await
|
|
};
|
|
if !self.inventory_full().await {
|
|
success |= self.suck_in_items(world).await;
|
|
}
|
|
if success {
|
|
self.cooldown_time.store(8, Ordering::Relaxed);
|
|
self.mark_dirty();
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn inventory_full(&self) -> bool {
|
|
for i in &self.items {
|
|
let item = i.lock().await;
|
|
if item.is_empty() || item.item_count != item.get_max_stack_size() {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
async fn suck_in_items(&self, world: &Arc<World>) -> bool {
|
|
// TODO getEntityContainer
|
|
let pos_up = &self.position.up();
|
|
if let Some(entity) = world.get_block_entity(pos_up).await
|
|
&& let Some(container) = entity.clone().get_inventory()
|
|
{
|
|
// TODO check WorldlyContainer
|
|
for i in 0..container.size() {
|
|
let bind = container.get_stack(i).await;
|
|
let mut item = bind.lock().await;
|
|
if !item.is_empty() && container.can_transfer_to(self, i, &item) {
|
|
//TODO WorldlyContainer
|
|
let backup = item.clone();
|
|
let one_item = item.split(1);
|
|
if Self::add_one_item(container.as_ref(), self, one_item).await {
|
|
// If extracting from furnace output slot (index 2), drop XP as orbs
|
|
const FURNACE_OUTPUT_SLOT: usize = 2;
|
|
if i == FURNACE_OUTPUT_SLOT
|
|
&& let Some(experience_container) =
|
|
entity.clone().to_experience_container()
|
|
{
|
|
let xp = experience_container.extract_experience();
|
|
if xp > 0 {
|
|
let pos = self.position.to_f64();
|
|
ExperienceOrbEntity::spawn(world, pos, xp as u32).await;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
*item = backup;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
let (block, state) = world.get_block_and_state(pos_up).await;
|
|
if !(state.is_solid() && block.has_tag(&tag::Block::MINECRAFT_DOES_NOT_BLOCK_HOPPERS)) {
|
|
// TODO getItemsAtAndAbove(level, hopper)
|
|
return false;
|
|
}
|
|
false
|
|
}
|
|
|
|
async fn eject_items(&self, world: &Arc<World>) -> bool {
|
|
// TODO getEntityContainer
|
|
|
|
if let Some(entity) = world
|
|
.get_block_entity(&self.position.offset(to_offset(&self.facing)))
|
|
.await
|
|
&& let Some(container) = entity.get_inventory()
|
|
{
|
|
// TODO check WorldlyContainer
|
|
let mut is_full = true;
|
|
for i in 0..container.size() {
|
|
let bind = container.get_stack(i).await;
|
|
let item = bind.lock().await;
|
|
if item.item_count < item.get_max_stack_size() {
|
|
is_full = false;
|
|
break;
|
|
}
|
|
}
|
|
if is_full {
|
|
return false;
|
|
}
|
|
for i in &self.items {
|
|
let mut item = i.lock().await;
|
|
if !item.is_empty() {
|
|
//TODO WorldlyContainer
|
|
let backup = item.clone();
|
|
let one_item = item.split(1);
|
|
if Self::add_one_item(self, container.as_ref(), one_item).await {
|
|
return true;
|
|
}
|
|
*item = backup;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
pub async fn add_one_item(from: &dyn Inventory, to: &dyn Inventory, item: ItemStack) -> bool {
|
|
let mut success = false;
|
|
let to_empty = to.is_empty().await;
|
|
for j in 0..to.size() {
|
|
if to.is_valid_slot_for(j, &item) {
|
|
let bind = to.get_stack(j).await;
|
|
let mut dst = bind.lock().await;
|
|
if dst.is_empty() {
|
|
*dst = item.clone();
|
|
success = true;
|
|
} else if dst.item_count < dst.get_max_stack_size() && dst.item == item.item {
|
|
// TODO check Components equal
|
|
dst.item_count += 1;
|
|
success = true;
|
|
}
|
|
if success {
|
|
if to_empty
|
|
&& let Some(hopper) = to.as_any().downcast_ref::<Self>()
|
|
&& hopper.cooldown_time.load(Ordering::Relaxed) <= 8
|
|
{
|
|
if let Some(from_hopper) = from.as_any().downcast_ref::<Self>() {
|
|
if from_hopper.cooldown_time.load(Ordering::Relaxed)
|
|
>= hopper.cooldown_time.load(Ordering::Relaxed)
|
|
{
|
|
hopper.cooldown_time.store(7, Ordering::Relaxed);
|
|
} else {
|
|
hopper.cooldown_time.store(8, Ordering::Relaxed);
|
|
}
|
|
} else {
|
|
hopper.cooldown_time.store(8, Ordering::Relaxed);
|
|
}
|
|
}
|
|
to.mark_dirty();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
false
|
|
}
|
|
}
|
|
|
|
impl Inventory for HopperBlockEntity {
|
|
fn size(&self) -> usize {
|
|
self.items.len()
|
|
}
|
|
|
|
fn is_empty(&self) -> InventoryFuture<'_, bool> {
|
|
Box::pin(async move {
|
|
for slot in &self.items {
|
|
if !slot.lock().await.is_empty() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
})
|
|
}
|
|
|
|
fn get_stack(&self, slot: usize) -> InventoryFuture<'_, Arc<Mutex<ItemStack>>> {
|
|
Box::pin(async move { self.items[slot].clone() })
|
|
}
|
|
|
|
fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
|
|
Box::pin(async move {
|
|
let mut removed = ItemStack::EMPTY.clone();
|
|
let mut guard = self.items[slot].lock().await;
|
|
std::mem::swap(&mut removed, &mut *guard);
|
|
self.mark_dirty();
|
|
removed
|
|
})
|
|
}
|
|
|
|
fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> {
|
|
Box::pin(async move {
|
|
let res = split_stack(&self.items, slot, amount).await;
|
|
self.mark_dirty();
|
|
res
|
|
})
|
|
}
|
|
|
|
fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
|
|
Box::pin(async move {
|
|
*self.items[slot].lock().await = stack;
|
|
self.mark_dirty();
|
|
})
|
|
}
|
|
|
|
fn mark_dirty(&self) {
|
|
self.dirty.store(true, Ordering::Relaxed);
|
|
}
|
|
|
|
fn as_any(&self) -> &dyn Any {
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Clearable for HopperBlockEntity {
|
|
fn clear(&self) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
|
|
Box::pin(async move {
|
|
for slot in &self.items {
|
|
*slot.lock().await = ItemStack::EMPTY.clone();
|
|
}
|
|
self.mark_dirty();
|
|
})
|
|
}
|
|
}
|