RyanHub – file viewer
filename: common/src/main/java/rearth/oritech/block/entity/interaction/TreefellerBlockEntity.java
branch: 1.21
back to repo
package rearth.oritech.block.entity.interaction;

import dev.architectury.registry.menu.ExtendedMenuProvider;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.block.entity.BlockEntity;
import net.minecraft.block.entity.BlockEntityTicker;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.inventory.Inventories;
import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.sound.SoundCategory;
import net.minecraft.state.property.Properties;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;
import net.minecraft.util.math.Vec3i;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.api.energy.EnergyApi;
import rearth.oritech.api.energy.containers.DynamicEnergyStorage;
import rearth.oritech.api.item.ItemApi;
import rearth.oritech.api.item.containers.SimpleInventoryStorage;
import rearth.oritech.block.base.entity.MachineBlockEntity;
import rearth.oritech.client.init.ModScreens;
import rearth.oritech.client.ui.BasicMachineScreenHandler;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.init.TagContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.AutoPlayingSoundKeyframeHandler;
import rearth.oritech.util.Geometry;
import rearth.oritech.util.InventoryInputMode;
import rearth.oritech.util.ScreenProvider;
import software.bernie.geckolib.animatable.GeoBlockEntity;
import software.bernie.geckolib.animatable.SingletonGeoAnimatable;
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.animation.AnimatableManager;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.animation.PlayState;
import software.bernie.geckolib.util.GeckoLibUtil;

import java.util.*;

public class TreefellerBlockEntity extends BlockEntity implements BlockEntityTicker<TreefellerBlockEntity>, GeoBlockEntity, EnergyApi.BlockProvider, ItemApi.BlockProvider, ExtendedMenuProvider, ScreenProvider {
    
    private static final int LOG_COST = 100;
    private static final int LEAF_COST = 10;
    
    private final Deque<BlockPos> pendingBlocks = new ArrayDeque<>();
    protected final AnimatableInstanceCache animatableInstanceCache = GeckoLibUtil.createInstanceCache(this);
    
    private long lastWorkedAt = 0;
    private boolean networkDirty = false;
    
    protected final DynamicEnergyStorage energyStorage = new DynamicEnergyStorage(50000, 4000, 0, this::markDirty);
    
    public final SimpleInventoryStorage inventory = new SimpleInventoryStorage(6, this::markDirty) {
        
        @Override
        public boolean supportsInsertion() {
            return false;
        }
    };
    
    public TreefellerBlockEntity(BlockPos pos, BlockState state) {
        super(BlockEntitiesContent.TREEFELLER_BLOCK_ENTITY, pos, state);
        SingletonGeoAnimatable.registerSyncedAnimatable(this);
    }
    
    @Override
    public void tick(World world, BlockPos pos, BlockState state, TreefellerBlockEntity blockEntity) {
        if (world.isClient) return;
        
        if (energyStorage.amount >= LOG_COST) {
            if (pendingBlocks.isEmpty() && world.getTime() % 20 == 0) {
                findTarget();
            }
            
            for (int i = 0; i < 6 && !pendingBlocks.isEmpty(); i++) {
                var candidate = pendingBlocks.peekLast();
                var candidateState = world.getBlockState(candidate);
                var isLog = candidateState.isIn(TagContent.CUTTER_LOGS_MINEABLE);

                var energyCost = isLog ? LOG_COST : LEAF_COST;
                if (energyCost > energyStorage.amount) break;
                
                var actionResult = breakTreeBlock(candidateState, candidate);
                if (actionResult == ActionResult.FAIL) break;
                pendingBlocks.pollLast();
                if (actionResult == ActionResult.PASS) continue;
                lastWorkedAt = world.getTime();

                energyStorage.amount -= energyCost;
                this.markDirty();
                
                if (isLog) break; // only harvest 1 log, but multiple leaves
            }
        }
        
        if (world.getTime() % 10 == 0) {
            var idleTicks = world.getTime() - lastWorkedAt;
            var isWorking = idleTicks < 20;
            var animName = isWorking ? "work" : "idle";
            playWorkAnimation(animName);
        }
        
        if (networkDirty && world.getTime() % 4 == 0) {
            networkDirty = false;
            sendNetworkEntry();
        }
    }
    
    @Override
    public void markDirty() {
        super.markDirty();
        networkDirty = true;
    }
    
    private ActionResult breakTreeBlock(BlockState candidateState, BlockPos candidate) {
        if (!candidateState.isIn(TagContent.CUTTER_LOGS_MINEABLE) && !candidateState.isIn(TagContent.CUTTER_LEAVES_MINEABLE)) return ActionResult.PASS;
        
        var dropped = net.minecraft.block.Block.getDroppedStacks(candidateState, (ServerWorld) world, candidate, null);
        if (dropped.stream().anyMatch((itemStack) -> !(itemStack.isEmpty() || canInsert(itemStack)))) return ActionResult.FAIL;

        world.addBlockBreakParticles(candidate, candidateState);
        if (world.getTime() % 2 == 0)
            world.playSound(null, candidate, candidateState.getSoundGroup().getBreakSound(), SoundCategory.BLOCKS, 0.5f, 1f);
        world.setBlockState(candidate, Blocks.AIR.getDefaultState());
        
        dropped.forEach(stack -> inventory.insert(stack, false));
        return ActionResult.SUCCESS;
    }

    private boolean canInsert(ItemStack stack) {
        return inventory.heldStacks.stream().anyMatch((itemStack) -> 
            itemStack.isEmpty() || (ItemStack.areItemsAndComponentsEqual(itemStack, stack) && itemStack.getCount() + stack.getCount() <= itemStack.getMaxCount())
        );
     }
    
    public void findTarget() {
        
        var state = getCachedState();
        var facing = state.get(Properties.HORIZONTAL_FACING);
        var offset = Geometry.rotatePosition(new Vec3i(1, 0, 0), facing);
        var frontBlock = pos.add(offset);
        
        var res = getTreeBlocks(frontBlock, world);
        pendingBlocks.addAll(res);
        
    }
    
    public static Deque<BlockPos> getTreeBlocks(BlockPos startPos, World world) {
        
        var startState = world.getBlockState(startPos);
        if (!startState.isIn(TagContent.CUTTER_LOGS_MINEABLE)) return new ArrayDeque<>();
        
        var checkedPositions = new HashSet<BlockPos>();
        var foundPositions = new ArrayDeque<BlockPos>();
        var foundLogs = new HashSet<BlockPos>();
        var pendingPositions = new ArrayDeque<BlockPos>();
        
        checkedPositions.add(startPos);
        foundPositions.add(startPos);
        pendingPositions.addAll(getNeighbors(startPos));
        foundLogs.add(startPos);
        
        while (!pendingPositions.isEmpty() && checkedPositions.size() < 8000) {
            // do logs first, if none available then leaves
            var candidate = pendingPositions.pollFirst();
            if (candidate.getY() < startPos.getY()) continue;
            
            if (checkedPositions.contains(candidate)) continue;
            
            var candidateState = world.getBlockState(candidate);
            checkedPositions.add(candidate);
            
            var isLog = candidateState.isIn(TagContent.CUTTER_LOGS_MINEABLE);
            var isValidLeaf = candidateState.isIn(TagContent.CUTTER_LEAVES_MINEABLE) && !candidateState.getOrEmpty(Properties.PERSISTENT).orElse(false);
            
            if (!isLog && !isValidLeaf) continue;
            
            var isValid = false;
            if (isLog) {
                isValid = isInLogRange(candidate, foundLogs, 3);
            } else {
                // Give a default of 1 for "leaf" blocks without a DISTANCE_1_7 property (like shroomlights)
                var range = candidateState.getOrEmpty(Properties.DISTANCE_1_7).orElse(1);
                isValid = isInLogRange(candidate, foundLogs, range + 2);
            }
            
            if (!isValid) continue;
            
            if (isLog) {
                foundLogs.add(candidate);
            }
            
            foundPositions.add(candidate);
            pendingPositions.addAll(getNeighbors(candidate));
            
        }
        
        // when no leaves are found, return nothing to prevent accidentally destroying buildings
        if (foundLogs.size() == foundPositions.size()) return new ArrayDeque<>();
        
        return foundPositions;
    }
    
    private static boolean isInLogRange(BlockPos pos, Set<BlockPos> logs, int maxDist) {
        return logs.stream().anyMatch(elem -> elem.getManhattanDistance(pos) <= maxDist);
    }
    
    private static List<BlockPos> getNeighbors(BlockPos input) {
        List<BlockPos> neighbors = new ArrayList<>();
        for (BlockPos pos : BlockPos.iterateOutwards(input, 1, 1, 1)) {
            // Without toImmutable, all of the elements in the collected list end up being the same BlockPos
            neighbors.add(pos.toImmutable());
        }
        return neighbors;
    }
    
    @Override
    protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.writeNbt(nbt, registryLookup);
        Inventories.writeNbt(nbt, inventory.heldStacks, false, registryLookup);
        nbt.putLong("energy_stored", energyStorage.amount);
    }
    
    @Override
    protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.readNbt(nbt, registryLookup);
        Inventories.readNbt(nbt, inventory.heldStacks, registryLookup);
        energyStorage.amount = nbt.getLong("energy_stored");
    }
    
    @Override
    public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
        controllers.add(new AnimationController<>(this, "machine", 5, state -> PlayState.CONTINUE)
                          .triggerableAnim("work", MachineBlockEntity.WORKING)
                          .triggerableAnim("idle", MachineBlockEntity.IDLE)
                          .setSoundKeyframeHandler(new AutoPlayingSoundKeyframeHandler<>()));
    }
    
    @Override
    public AnimatableInstanceCache getAnimatableInstanceCache() {
        return animatableInstanceCache;
    }
    
    @Override
    public EnergyApi.EnergyStorage getEnergyStorage(Direction direction) {
        return energyStorage;
    }
    
    @Override
    public ItemApi.InventoryStorage getInventoryStorage(Direction direction) {
        return inventory;
    }
    
    @Override
    public List<GuiSlot> getGuiSlots() {
        var list = new ArrayList<GuiSlot>();
        for (int i = 0; i < inventory.size(); i++) {
            list.add(new GuiSlot(i, 40 + i * 19, 25, true));
        }
        return list;
    }
    
    @Override
    public float getDisplayedEnergyUsage() {
        return LOG_COST;
    }
    
    @Override
    public float getProgress() {
        return 0;
    }
    
    @Override
    public InventoryInputMode getInventoryInputMode() {
        return InventoryInputMode.FILL_LEFT_TO_RIGHT;
    }
    
    @Override
    public boolean inputOptionsEnabled() {
        return false;
    }
    
    @Override
    public boolean showProgress() {
        return false;
    }
    
    @Override
    public Inventory getDisplayedInventory() {
        return inventory;
    }
    
    @Override
    public ScreenHandlerType<?> getScreenHandlerType() {
        return ModScreens.TREEFELLER_SCREEN;
    }
    
    @Override
    public Text getDisplayName() {
        return Text.of("");
    }
    
    @Nullable
    @Override
    public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
        sendNetworkEntry();
        return new BasicMachineScreenHandler(syncId, playerInventory, this);
    }
    
    public void playWorkAnimation(String animName) {
        triggerAnim("machine", animName);
    }
    
    private void sendNetworkEntry() {
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.GenericEnergySyncPacket(pos, energyStorage.amount, energyStorage.capacity));
    }
    
    @Override
    public void saveExtraData(PacketByteBuf buf) {
        buf.writeBlockPos(pos);
    }
}