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);
}
}