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

import com.mojang.authlib.GameProfile;
import dev.architectury.registry.menu.ExtendedMenuProvider;
import net.minecraft.block.Block;
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.component.DataComponentTypes;
import net.minecraft.component.type.UnbreakableComponent;
import net.minecraft.enchantment.Enchantments;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.Monster;
import net.minecraft.entity.mob.WaterCreatureEntity;
import net.minecraft.entity.passive.AnimalEntity;
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.item.Items;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.recipe.RecipeEntry;
import net.minecraft.registry.RegistryKeys;
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.Property;
import net.minecraft.text.Text;
import net.minecraft.util.Pair;
import net.minecraft.util.math.*;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;
import rearth.oritech.Oritech;
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.block.behavior.LaserArmBlockBehavior;
import rearth.oritech.block.blocks.interaction.LaserArmBlock;
import rearth.oritech.block.blocks.processing.MachineCoreBlock;
import rearth.oritech.block.entity.MachineCoreEntity;
import rearth.oritech.block.entity.addons.RedstoneAddonBlockEntity;
import rearth.oritech.client.init.ModScreens;
import rearth.oritech.client.init.ParticleContent;
import rearth.oritech.client.ui.UpgradableMachineScreenHandler;
import rearth.oritech.init.BlockContent;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.init.TagContent;
import rearth.oritech.init.recipes.OritechRecipe;
import rearth.oritech.init.recipes.RecipeContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.*;
import software.bernie.geckolib.animatable.GeoBlockEntity;
import software.bernie.geckolib.animatable.instance.AnimatableInstanceCache;
import software.bernie.geckolib.animation.AnimatableManager;
import software.bernie.geckolib.animation.AnimationController;
import software.bernie.geckolib.util.GeckoLibUtil;

import java.util.*;
import java.util.stream.Collectors;

import static rearth.oritech.block.base.block.MultiblockMachine.ASSEMBLED;

public class LaserArmBlockEntity extends BlockEntity implements
  GeoBlockEntity, BlockEntityTicker<LaserArmBlockEntity>, EnergyApi.BlockProvider, ScreenProvider, ExtendedMenuProvider,
    MultiblockMachineController, MachineAddonController, ItemApi.BlockProvider, RedstoneAddonBlockEntity.RedstoneControllable {
    
    public static final String LASER_PLAYER_NAME = "oritech_laser";
    private static final int BLOCK_BREAK_ENERGY = Oritech.CONFIG.laserArmConfig.blockBreakEnergyBase();
    
    // storage
    protected final DynamicEnergyStorage energyStorage = new DynamicEnergyStorage(getDefaultCapacity(), getDefaultInsertRate(), 0, this::markDirty);
    
    public final SimpleInventoryStorage inventory = new SimpleInventoryStorage(3, this::markDirty);
    
    // animation
    protected final AnimatableInstanceCache animatableInstanceCache = GeckoLibUtil.createInstanceCache(this);
    private final AnimationController<LaserArmBlockEntity> animationController = getAnimationController();
    
    // multiblock
    private final ArrayList<BlockPos> coreBlocksConnected = new ArrayList<>();
    
    // addons
    private final List<BlockPos> connectedAddons = new ArrayList<>();
    private final List<BlockPos> openSlots = new ArrayList<>();
    private float coreQuality = 1f;
    private BaseAddonData addonData = MachineAddonController.DEFAULT_ADDON_DATA;
    public int areaSize = 1;
    public int yieldAddons = 0;
    public int hunterAddons = 0;
    public boolean hasCropFilterAddon = false;
    
    // config
    private final int range = Oritech.CONFIG.laserArmConfig.range();
    
    private Vec3d laserHead;
    
    // working data
    private BlockPos targetDirection;
    private BlockPos currentTarget;
    public HunterTargetMode hunterTargetMode = HunterTargetMode.HOSTILE_ONLY;
    private LivingEntity currentLivingTarget;
    private long lastFiredAt;
    private int progress;
    private int targetBlockEnergyNeeded = BLOCK_BREAK_ENERGY;
    private boolean networkDirty;
    private boolean redstonePowered;
    private ArrayDeque<BlockPos> pendingArea;
    private final ArrayDeque<LivingEntity> pendingLivingTargets = new ArrayDeque<>();
    
    // needed only on client
    public Vec3d lastRenderPosition;
    private PlayerEntity laserPlayerEntity = null;
    
    public LaserArmBlockEntity(BlockPos pos, BlockState state) {
        super(BlockEntitiesContent.LASER_ARM_ENTITY, pos, state);
        laserHead = Vec3d.of(pos.up()).add(0.5, 0.55, 0.5);
    }
    
    @Override
    public void tick(World world, BlockPos pos, BlockState state, LaserArmBlockEntity blockEntity) {
        if (world.isClient() || !isActive(state))
            return;
        
        if (!redstonePowered && energyStorage.getAmount() >= energyRequiredToFire()) {
            if (hunterAddons > 0) {
                fireAtLivingEntities(world, pos, state, blockEntity);
            } else if (currentTarget != null && !currentTarget.equals(BlockPos.ZERO)) {
                fireAtBlocks(world, pos, state, blockEntity);
            } else if (targetDirection != null && !targetDirection.equals(BlockPos.ORIGIN) && (world.getTime() + pos.getZ()) % 40 == 0) {
                // target pos is set, but no target is found (e.g. all blocks already mined). Periodically scan again for new blocks.
                findNextBlockBreakTarget();
            }
        }
        
        if (networkDirty)
            updateNetwork();
    }
    
    private void fireAtBlocks(World world, BlockPos pos, BlockState state, LaserArmBlockEntity blockEntity) {
        var targetBlockPos = currentTarget;
        var targetBlockState = world.getBlockState(targetBlockPos);
        var targetBlock = targetBlockState.getBlock();
        var targetBlockEntity = world.getBlockEntity(targetBlockPos);
        
        LaserArmBlockBehavior behavior = LaserArmBlock.getBehaviorForBlock(targetBlock);
        boolean fired = false;
        if (behavior.fireAtBlock(world, this, targetBlock, targetBlockPos, targetBlockState, targetBlockEntity)) {
            energyStorage.amount -= energyRequiredToFire();
            lastFiredAt = world.getTime();
            networkDirty = true;
        } else {
            findNextBlockBreakTarget();
        }
    }
    
    private void fireAtLivingEntities(World world, BlockPos pos, BlockState state, LaserArmBlockEntity blockEntity) {
        // check that there is a target, that is still alive and still in range
        if (currentLivingTarget != null && validTarget(currentLivingTarget)) {
            
            var behavior = LaserArmBlock.getBehaviorForEntity(currentLivingTarget.getType());
            if (behavior.fireAtEntity(world, this, currentLivingTarget)) {
                energyStorage.amount -= energyRequiredToFire();
                this.targetDirection = currentLivingTarget.getBlockPos();
                lastFiredAt = world.getTime();
                networkDirty = true;
            } else {
                pendingLivingTargets.remove(currentLivingTarget);
                currentLivingTarget = null;
                currentTarget = null;
                networkDirty = true;
            }
            ;
        } else {
            loadNextLivingTarget();
        }
        
    }
    
    public void setRedstonePowered(boolean redstonePowered) {
        this.redstonePowered = redstonePowered;
    }
    
    public void addBlockBreakProgress(int progress) {
        this.progress += progress;
    }
    
    public int getBlockBreakProgress() {
        return this.progress;
    }
    
    public int getTargetBlockEnergyNeeded() {
        return targetBlockEnergyNeeded;
    }
    
    public void finishBlockBreaking(BlockPos targetPos, BlockState targetBlockState) {
        progress -= targetBlockEnergyNeeded;
        
        var targetEntity = world.getBlockEntity(targetPos);
        List<ItemStack> dropped;
        // added getLaserPlayerEntity() to make ae2 certus quartz drop from certus
        // quartz clusters because it's expecting an entity in
        // LootContextParameters.THIS_ENTITY
        if (yieldAddons > 0) {
            dropped = DestroyerBlockEntity.getLootDrops(targetBlockState, (ServerWorld) world, targetPos, targetEntity, yieldAddons, getLaserPlayerEntity());
        } else {
            dropped = net.minecraft.block.Block.getDroppedStacks(targetBlockState, (ServerWorld) world, targetPos, targetEntity, getLaserPlayerEntity(), ItemStack.EMPTY);
        }
        
        var blockRecipe = tryGetRecipeOfBlock(targetBlockState);
        if (blockRecipe != null) {
            var recipe = blockRecipe.value();
            var farmedCount = 1 + yieldAddons;
            dropped = List.of(new ItemStack(recipe.getResults().get(0).getItem(), farmedCount));
            ParticleContent.CHARGING.spawn(world, Vec3d.of(targetPos), 1);
        }
        
        // yes, this will discard items that wont fit anymore
        for (var stack : dropped) {
            this.inventory.insert(stack, false);
        }
        
        try {
            targetBlockState.getBlock().onBreak(world, targetPos, targetBlockState, getLaserPlayerEntity());
        } catch (Exception exception) {
            Oritech.LOGGER.warn("Laser arm block break event failure when breaking " + targetBlockState + " at " + targetPos + ": " + exception.getLocalizedMessage());
        }
        world.addBlockBreakParticles(targetPos, world.getBlockState(targetPos));
        world.playSound(null, targetPos, targetBlockState.getSoundGroup().getBreakSound(), SoundCategory.BLOCKS, 1f, 1f);
        world.breakBlock(targetPos, false);
        
        findNextBlockBreakTarget();
    }
    
    private RecipeEntry<OritechRecipe> tryGetRecipeOfBlock(BlockState destroyed) {
        var inputItem = destroyed.getBlock().asItem();
        var inputInv = new SimpleCraftingInventory(new ItemStack(inputItem));
        var candidate = world.getRecipeManager().getFirstMatch(RecipeContent.LASER, inputInv, world);
        return candidate.orElse(null);
    }
    
    public PlayerEntity getLaserPlayerEntity() {
        if (!(world instanceof ServerWorld))
            return null;
        
        if (laserPlayerEntity == null) {
            laserPlayerEntity = FakeMachinePlayer.create((ServerWorld) world, new GameProfile(UUID.randomUUID(), LASER_PLAYER_NAME), inventory);
        }
        
        if (hunterAddons > 0 && yieldAddons > 0) {
            var lootingSword = new ItemStack(Items.NETHERITE_SWORD);
            lootingSword.set(DataComponentTypes.UNBREAKABLE, new UnbreakableComponent(false));
            var lootingEntry = world.getRegistryManager().get(RegistryKeys.ENCHANTMENT).getEntry(Enchantments.LOOTING).get();
            lootingSword.addEnchantment(lootingEntry, Math.min(yieldAddons, 3));
            laserPlayerEntity.getInventory().main.set(laserPlayerEntity.getInventory().selectedSlot, lootingSword);
        }
        
        return laserPlayerEntity;
    }
    
    private void findNextBlockBreakTarget() {
        
        while (pendingArea != null && !pendingArea.isEmpty()) {
            if (trySetNewTarget(pendingArea.pop(), false)) {
                if (pendingArea.isEmpty()) pendingArea = null;
                return;
            }
        }
        
        var direction = Vec3d.of(targetDirection.subtract(pos.up())).normalize();
        var from = laserHead.add(direction.multiply(1.5));
        
        var nextBlock = basicRaycast(from, direction, range, 0.45F);
        if (nextBlock == null) {
            currentTarget = null;
            return;
        }
        
        var maxSize = (int) from.distanceTo(nextBlock.toCenterPos()) - 1;
        var scanDist = Math.min(areaSize, maxSize);
        if (scanDist > 1)
            pendingArea = findNextAreaBlockTarget(nextBlock, scanDist);
        
        
        if (!trySetNewTarget(nextBlock, false)) {
            currentTarget = null;   // out of range or invalid for another reason
        }
        
    }
    
    private double hunterRange() {
        // hunter range is 2^hunterAddons, with max 3 hunterAddons
        // range should be calculated near the center of the laser head's cube, so add 0.5 to start counting range from side of cube
        return Math.pow(4, Math.min(hunterAddons, 3)) + 0.5;
    }
    
    private boolean canSee(LivingEntity entity) {
        if (entity.getWorld() != this.getWorld() || entity.isInvisible()) {
            return false;
        } else {
            var target = entity.getEyePos();
            var direction = target.subtract(laserHead).normalize();
            if (laserHead.distanceTo(target) > 128.0) {
                return false;
            } else {
                // can see if basicRaycast() doesn't find anything it can't pass through between laser and target
                return basicRaycast(laserHead.add(direction.multiply(1.5)), direction, (int) (laserHead.distanceTo(target) - 1), 0.2f) == null;
            }
        }
    }
    
    private boolean validTarget(LivingEntity entity) {
        return entity.isAlive() && canSee(entity) && huntedTarget(entity) && entity.getPos().isInRange(pos.up().toCenterPos(), hunterRange());
    }
    
    private boolean huntedTarget(LivingEntity entity) {
        // Not including Allay, Villagers, Trader, Iron Golem, Snow Golem
        // Also not including pets
        return switch (hunterTargetMode) {
            // Regardless of mode, laser will always target player to charge energy storing chestplate
            case HunterTargetMode.HOSTILE_ONLY -> entity instanceof Monster;
            case HunterTargetMode.HOSTILE_NEUTRAL -> {
                if ((entity instanceof AnimalEntity animal && animal.getLovingPlayer() == null) || entity instanceof WaterCreatureEntity)
                    yield true;
                yield entity instanceof Monster;
            }
            case HunterTargetMode.ALL -> true;
        };
    }
    
    // this only gets called if we don't have a target (e.g. null or not valid)
    private void loadNextLivingTarget() {
        
        // load targets if we don't have any (only every 10 ticks to save performance
        if (pendingLivingTargets.isEmpty() && (world.getTime() + pos.asLong()) % 10 == 0) {
            updateEntityTargets();
        }
        
        // assign first target from cached, distance sorted target list
        while (!pendingLivingTargets.isEmpty()) {
            var candidate = pendingLivingTargets.pop();
            if (validTarget(candidate)) {
                currentLivingTarget = candidate;
                currentTarget = candidate.getBlockPos();
                return;
            }
        }
    }
    
    private void updateEntityTargets() {
        var entityRange = hunterRange();
        // Only sort the list when getting a new list of entities in range.
        // The entities can move around so the sort order isn't guaranteed to be correct, but it should be good enough.
        // There's no need to spend the time re-sorting the list every time the laser needs to pick a new target from the cached list.
        var targets = world.getEntitiesByClass(LivingEntity.class, new Box(laserHead.x - entityRange, laserHead.y - entityRange, laserHead.z - entityRange, laserHead.x + entityRange, laserHead.y + entityRange, laserHead.z + entityRange), EntityPredicates.VALID_LIVING_ENTITY.and(EntityPredicates.EXCEPT_CREATIVE_OR_SPECTATOR));
        targets.sort(Comparator.comparingDouble((entity) -> entity.squaredDistanceTo(laserHead)));
        pendingLivingTargets.addAll(targets);
    }
    
    // returns the first block in an X*X*X cube, from the outside in
    private ArrayDeque<BlockPos> findNextAreaBlockTarget(BlockPos center, int scanDist) {
        
        var targets = new ArrayList<BlockPos>();
        
        for (int x = -scanDist; x < scanDist; x++) {
            for (int y = -scanDist; y < scanDist; y++) {
                for (int z = -scanDist; z < scanDist; z++) {
                    var pos = center.add(x, y, z);
                    if (!canPassThrough(world.getBlockState(pos), pos) && !center.equals(pos))
                        targets.add(pos);
                }
            }
        }
        
        targets.sort(Comparator.comparingInt(pos::getManhattanDistance));
        return new ArrayDeque<>(targets);
    }
    
    private BlockPos basicRaycast(Vec3d from, Vec3d direction, int range, float searchOffset) {
        
        for (float i = 0; i < range; i += 0.3f) {
            var to = from.add(direction.multiply(i));
            var targetBlockPos = BlockPos.ofFloored(to.add(0, searchOffset, 0));
            var targetState = world.getBlockState(targetBlockPos);
            if (isSearchTerminatorBlock(targetState)) return null;
            if (!canPassThrough(targetState, targetBlockPos)) return targetBlockPos;
            
            if (searchOffset == 0.0F)
                return null;
            
            var offsetTop = to.add(0, -searchOffset, 0);
            targetBlockPos = BlockPos.ofFloored(offsetTop);
            targetState = world.getBlockState(targetBlockPos);
            if (isSearchTerminatorBlock(targetState)) return null;
            if (!canPassThrough(targetState, targetBlockPos)) return targetBlockPos;
            
            var offsetLeft = to.add(-searchOffset, 0, 0);
            targetBlockPos = BlockPos.ofFloored(offsetLeft);
            targetState = world.getBlockState(targetBlockPos);
            if (isSearchTerminatorBlock(targetState)) return null;
            if (!canPassThrough(targetState, targetBlockPos)) return targetBlockPos;
            
            var offsetRight = to.add(searchOffset, 0, 0);
            targetBlockPos = BlockPos.ofFloored(offsetRight);
            targetState = world.getBlockState(targetBlockPos);
            if (isSearchTerminatorBlock(targetState)) return null;
            if (!canPassThrough(targetState, targetBlockPos)) return targetBlockPos;
            
            var offsetFront = to.add(0, 0, searchOffset);
            targetBlockPos = BlockPos.ofFloored(offsetFront);
            targetState = world.getBlockState(targetBlockPos);
            if (isSearchTerminatorBlock(targetState)) return null;
            if (!canPassThrough(targetState, targetBlockPos)) return targetBlockPos;
            
            var offsetBack = to.add(0, 0, -searchOffset);
            targetBlockPos = BlockPos.ofFloored(offsetBack);
            targetState = world.getBlockState(targetBlockPos);
            if (isSearchTerminatorBlock(targetState)) return null;
            if (!canPassThrough(targetState, targetBlockPos)) return targetBlockPos;
        }
        
        return null;
    }
    
    private boolean isSearchTerminatorBlock(BlockState state) {
        return state.getBlock().equals(Blocks.TARGET);
    }
    
    public boolean canPassThrough(BlockState state, BlockPos blockPos) {
        // When targetting entities, don't let grass, vines, small mushrooms, pressure plates, etc. get in the way of the laser
        return state.isAir() || state.getFluidState().isStill() || state.isIn(TagContent.LASER_PASSTHROUGH) || (hunterAddons > 0 && !state.isSolidBlock(world, blockPos));
    }
    
    @Override
    public void gatherAddonStats(List<AddonBlock> addons) {
        
        areaSize = 1;
        yieldAddons = 0;
        hunterAddons = 0;
        hasCropFilterAddon = false;
        
        MachineAddonController.super.gatherAddonStats(addons);
    }
    
    @Override
    public void getAdditionalStatFromAddon(AddonBlock addonBlock) {
        MachineAddonController.super.getAdditionalStatFromAddon(addonBlock);
        
        if (addonBlock.state().getBlock().equals(BlockContent.QUARRY_ADDON))
            areaSize++;
        if (addonBlock.state().getBlock().equals(BlockContent.MACHINE_HUNTER_ADDON))
            hunterAddons++;
        if (addonBlock.state().getBlock().equals(BlockContent.MACHINE_YIELD_ADDON))
            yieldAddons++;
        if (addonBlock.state().getBlock().equals(BlockContent.CROP_FILTER_ADDON))
            hasCropFilterAddon = true;
        
    }
    
    public int energyRequiredToFire() {
        return (int) (Oritech.CONFIG.laserArmConfig.energyPerTick() * (1 / addonData.speed()));
    }
    
    public float getDamageTick() {
        return (Oritech.CONFIG.laserArmConfig.damageTickBase() * (1 / addonData.speed()));
    }
    
    private void updateNetwork() {
        var entityId = currentLivingTarget != null ? currentLivingTarget.getId() : -1;
        var sendTarget = currentTarget != null ? currentTarget : BlockPos.ORIGIN;
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.LaserArmSyncPacket(pos, sendTarget, lastFiredAt, areaSize, yieldAddons, hunterAddons, hunterTargetMode.value, hasCropFilterAddon, entityId, redstonePowered));
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.GenericEnergySyncPacket(pos, energyStorage.amount, energyStorage.capacity));
        networkDirty = false;
    }
    
    public boolean setTargetFromDesignator(BlockPos targetPos) {
        var success = trySetNewTarget(targetPos, true);
        findNextBlockBreakTarget();
        
        return success;
    }
    
    public void cycleHunterTargetMode() {
        hunterTargetMode = hunterTargetMode.next();
    }
    
    private boolean trySetNewTarget(BlockPos targetPos, boolean alsoSetDirection) {
        
        // if target is coreblock, adjust it to point to controller if connected
        var targetState = Objects.requireNonNull(world).getBlockState(targetPos);
        if (targetState.getBlock() instanceof MachineCoreBlock && targetState.get(MachineCoreBlock.USED)) {
            var coreEntity = (MachineCoreEntity) world.getBlockEntity(targetPos);
            var controllerPos = Objects.requireNonNull(coreEntity).getControllerPos();
            if (controllerPos != null) targetPos = controllerPos;
        }
        
        var distance = targetPos.getManhattanDistance(pos);
        var blockHardness = targetState.getBlock().getHardness();
        if (distance > range || blockHardness < 0.0 || targetState.getBlock().equals(Blocks.AIR)) {
            return false;
        }
        
        this.targetBlockEnergyNeeded = (int) (BLOCK_BREAK_ENERGY * Math.sqrt(blockHardness) * addonData.efficiency());
        this.currentTarget = targetPos;
        
        if (alsoSetDirection) {
            this.targetDirection = targetPos;
            pendingArea = null;
            networkDirty = true;
        }
        this.markDirty();
        
        return true;
    }
    
    @Override
    protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.writeNbt(nbt, registryLookup);
        Inventories.writeNbt(nbt, inventory.heldStacks, false, registryLookup);
        addMultiblockToNbt(nbt);
        writeAddonToNbt(nbt);
        nbt.putLong("energy_stored", energyStorage.amount);
        nbt.putBoolean("redstone", redstonePowered);
        nbt.putInt("areaSize", areaSize);
        nbt.putInt("yieldAddons", yieldAddons);
        nbt.putInt("hunterAddons", hunterAddons);
        nbt.putBoolean("cropAddon", hasCropFilterAddon);
        nbt.putInt("hunterTargetMode", hunterTargetMode.value);
        
        if (targetDirection != null && currentTarget != null) {
            nbt.putLong("target_position", currentTarget.asLong());
            nbt.putLong("target_direction", targetDirection.asLong());
        }
        
        if (pendingArea != null && !pendingArea.isEmpty()) {
            var positions = pendingArea.stream().mapToLong(BlockPos::asLong).toArray();
            nbt.putLongArray("pendingPositions", positions);
        } else {
            nbt.remove("pendingPositions");
        }
    }
    
    @Override
    protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.readNbt(nbt, registryLookup);
        Inventories.readNbt(nbt, inventory.heldStacks, registryLookup);
        loadMultiblockNbtData(nbt);
        loadAddonNbtData(nbt);
        
        updateEnergyContainer();
        
        redstonePowered = nbt.getBoolean("redstone");
        energyStorage.amount = nbt.getLong("energy_stored");
        targetDirection = BlockPos.fromLong(nbt.getLong("target_direction"));
        currentTarget = BlockPos.fromLong(nbt.getLong("target_position"));
        areaSize = nbt.getInt("areaSize");
        yieldAddons = nbt.getInt("yieldAddons");
        hunterAddons = nbt.getInt("hunterAddons");
        hunterTargetMode = HunterTargetMode.fromValue(nbt.getInt("hunterTargetMode"));
        hasCropFilterAddon = nbt.getBoolean("cropAddon");
        
        if (nbt.contains("pendingPositions")) {
            pendingArea = Arrays.stream(nbt.getLongArray("pendingPositions")).mapToObj(BlockPos::fromLong).collect(Collectors.toCollection(ArrayDeque::new));
        }
    }
    
    @Override
    public void markDirty() {
        // basically the same as the parent method, but without the comparator update for a slight speed increase
        if (this.world != null)
            world.markDirty(pos);
        
        networkDirty = true;
    }
    
    //region multiblock
    @Override
    public ArrayList<BlockPos> getConnectedCores() {
        return coreBlocksConnected;
    }
    
    @Override
    public Direction getFacingForMultiblock() {
        return Direction.NORTH;
    }
    
    @Override
    public BlockPos getPosForMultiblock() {
        return pos;
    }
    
    @Override
    public World getWorldForMultiblock() {
        return world;
    }
    
    @Override
    public float getCoreQuality() {
        return this.coreQuality;
    }
    
    @Override
    public void setCoreQuality(float quality) {
        this.coreQuality = quality;
    }
    
    @Override
    public ItemApi.InventoryStorage getInventoryForMultiblock() {
        return inventory;
    }
    
    @Override
    public EnergyApi.EnergyStorage getEnergyStorageForMultiblock(Direction direction) {
        return energyStorage;
    }
    
    @Override
    public List<Vec3i> getCorePositions() {
        return List.of(
          new Vec3i(0, 1, 0)
        );
    }
    //endregion
    
    // energyprovider
    @Override
    public EnergyApi.EnergyStorage getEnergyStorage(Direction direction) {
        return energyStorage;
    }
    
    //region addons
    @Override
    public List<BlockPos> getConnectedAddons() {
        return connectedAddons;
    }
    
    @Override
    public List<BlockPos> getOpenAddonSlots() {
        return openSlots;
    }
    
    @Override
    public Direction getFacingForAddon() {
        return Direction.NORTH;
    }
    
    @Override
    public DynamicEnergyStorage getStorageForAddon() {
        return energyStorage;
    }
    
    @Override
    public ItemApi.InventoryStorage getInventoryForAddon() {
        return inventory;
    }
    
    @Override
    public ScreenProvider getScreenProvider() {
        return null;
    }
    
    @Override
    public List<Vec3i> getAddonSlots() {
        return List.of(
          new Vec3i(0, -1, 0)
        );
    }
    
    @Override
    public BaseAddonData getBaseAddonData() {
        return addonData;
    }
    
    @Override
    public void setBaseAddonData(BaseAddonData data) {
        this.addonData = data;
    }
    
    @Override
    public long getDefaultCapacity() {
        return Oritech.CONFIG.laserArmConfig.energyCapacity();
    }
    
    @Override
    public long getDefaultInsertRate() {
        return Oritech.CONFIG.laserArmConfig.maxEnergyInsertion();
    }
    //endregion
    
    // region animation
    @Override
    public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
        controllers.add(animationController);
    }
    
    @Override
    public AnimatableInstanceCache getAnimatableInstanceCache() {
        return animatableInstanceCache;
    }
    
    private AnimationController<LaserArmBlockEntity> getAnimationController() {
        return new AnimationController<>(this, state -> {
            
            if (state.isCurrentAnimation(MachineBlockEntity.SETUP)) {
                if (state.getController().hasAnimationFinished()) {
                    state.setAndContinue(MachineBlockEntity.IDLE);
                } else {
                    return state.setAndContinue(MachineBlockEntity.SETUP);
                }
            }
            
            if (isActive(getCachedState())) {
                if (isFiring()) {
                    return state.setAndContinue(MachineBlockEntity.WORKING);
                } else {
                    return state.setAndContinue(MachineBlockEntity.IDLE);
                }
            } else {
                return state.setAndContinue(MachineBlockEntity.PACKAGED);
            }
        }).setSoundKeyframeHandler(new AutoPlayingSoundKeyframeHandler<>());
    }
    
    @Override
    public void playSetupAnimation() {
        animationController.setAnimation(MachineBlockEntity.SETUP);
        animationController.forceAnimationReset();
    }
    
    public boolean isActive(BlockState state) {
        return state.get(ASSEMBLED);
    }
    
    @Override
    public ItemApi.InventoryStorage getInventoryStorage(Direction direction) {
        return inventory;
    }
    
    //endregion
    
    
    public BlockPos getCurrentTarget() {
        return currentTarget;
    }
    
    public Vec3d getVisualTarget() {
        if (hunterAddons > 0 && currentLivingTarget != null) {
            return currentLivingTarget.getEyePos().subtract(0.5f, 0, 0.5f);
        } else {
            return Vec3d.of(getCurrentTarget()).add(0, 0.5, 0);
        }
    }
    
    public void setCurrentTarget(BlockPos currentTarget) {
        this.currentTarget = currentTarget;
    }
    
    public void setLivingTargetFromNetwork(int id) {
        if (id == -1) {
            currentLivingTarget = null;
        } else {
            var candidate = world.getEntityById(id);
            if (candidate instanceof LivingEntity livingEntity) {
                currentLivingTarget = livingEntity;
            } else {
                currentLivingTarget = null;
            }
        }
    }
    
    public long getLastFiredAt() {
        return lastFiredAt;
    }
    
    
    @Override
    public BlockPos getPosForAddon() {
        return getPos();
    }
    
    @Override
    public World getWorldForAddon() {
        return getWorld();
    }
    
    public void setLastFiredAt(long lastFiredAt) {
        this.lastFiredAt = lastFiredAt;
    }
    
    public boolean isFiring() {
        var idleTime = world.getTime() - lastFiredAt;
        return idleTime < 3;
    }
    
    public boolean isTargetingAtomicForge(Block block) {
        return block.equals(BlockContent.ATOMIC_FORGE_BLOCK);
    }
    
    public boolean isTargetingDeepdrill(Block block) {
        return block.equals(BlockContent.DEEP_DRILL_BLOCK);
    }
    
    public boolean isTargetingCatalyst(Block block) {
        return block.equals(BlockContent.ENCHANTMENT_CATALYST_BLOCK);
    }
    
    public boolean isTargetingUnstableContainer(Block block) {
        return block.equals(BlockContent.UNSTABLE_CONTAINER);
    }
    
    public boolean isTargetingEnergyContainer() {
        var storageCandidate = EnergyApi.BLOCK.find(world, currentTarget, null);
        var block = world.getBlockState(currentTarget).getBlock();
        return storageCandidate != null || isTargetingAtomicForge(block) || isTargetingDeepdrill(block) || isTargetingCatalyst(block) || isTargetingUnstableContainer(block);
    }
    
    public boolean isTargetingBuddingAmethyst() {
        return world.getBlockState(currentTarget).isIn(TagContent.LASER_ACCELERATED);
    }
    
    @Override
    public List<Pair<Text, Text>> getExtraExtensionLabels() {
        if (areaSize == 1 && yieldAddons == 0 && hunterAddons == 0)
            return ScreenProvider.super.getExtraExtensionLabels();
        if (hunterAddons > 0)
            return List.of(
              new Pair<>(Text.translatable("title.oritech.machine.addon_range", (int) hunterRange()), Text.translatable("tooltip.oritech.laser_arm.addon_hunter_range")),
              new Pair<>(Text.translatable("title.oritech.laser_arm.addon_hunter_damage", String.format("%.2f", getDamageTick())), Text.translatable("tooltip.oritech.laser_arm.addon_hunter_damage")),
              new Pair<>(Text.translatable("title.oritech.machine.addon_looting", yieldAddons), Text.translatable("tooltip.oritech.machine.addon_looting")));
        return List.of(
          new Pair<>(Text.translatable("title.oritech.machine.addon_range", areaSize), Text.translatable("tooltip.oritech.laser_arm.addon_range")),
          new Pair<>(Text.translatable("title.oritech.machine.addon_fortune", yieldAddons), Text.translatable("tooltip.oritech.machine.addon_fortune")));
    }
    
    @Override
    public List<GuiSlot> getGuiSlots() {
        return List.of(
          new GuiSlot(0, 117, 20, true),
          new GuiSlot(1, 117, 38, true),
          new GuiSlot(2, 117, 56, true));
    }
    
    @Override
    public float getDisplayedEnergyUsage() {
        return energyRequiredToFire();
    }
    
    @Override
    public float getProgress() {
        return 0;
    }
    
    @Override
    public boolean showProgress() {
        return false;
    }
    
    @Override
    public InventoryInputMode getInventoryInputMode() {
        return InventoryInputMode.FILL_LEFT_TO_RIGHT;
    }
    
    @Override
    public boolean inputOptionsEnabled() {
        return false;
    }
    
    @Override
    public Inventory getDisplayedInventory() {
        return inventory;
    }
    
    @Override
    public float getDisplayedEnergyTransfer() {
        return energyStorage.maxInsert;
    }
    
    @Override
    public ScreenHandlerType<?> getScreenHandlerType() {
        return ModScreens.LASER_SCREEN;
    }
    
    @Override
    public Property<Direction> getBlockFacingProperty() {
        return ScreenProvider.super.getBlockFacingProperty();
    }
    
    @Nullable
    @Override
    public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.FullEnergySyncPacket(pos, energyStorage.amount, energyStorage.capacity, energyStorage.maxInsert, energyStorage.maxExtract));
        return new UpgradableMachineScreenHandler(syncId, playerInventory, this, getUiData(), getCoreQuality());
    }
    
    @Override
    public void saveExtraData(PacketByteBuf buf) {
        updateNetwork();
        var data = new ModScreens.UpgradableData(pos, getUiData(), getCoreQuality());
        ModScreens.UpgradableData.PACKET_CODEC.encode(buf, data);
        
    }
    
    @Override
    public Text getDisplayName() {
        return Text.literal("");
    }
    
    @Override
    public int getComparatorEnergyAmount() {
        return (int) ((energyStorage.amount / (float) energyStorage.capacity) * 15);
    }
    
    @Override
    public int getComparatorSlotAmount(int slot) {
        if (inventory.heldStacks.size() <= slot) return 0;
        
        var stack = inventory.getStack(slot);
        if (stack.isEmpty()) return 0;
        
        return (int) ((stack.getCount() / (float) stack.getMaxCount()) * 15);
    }
    
    @Override
    public int getComparatorProgress() {
        if (currentTarget == null || currentTarget.equals(BlockPos.ORIGIN)) return 0;
        
        return (int) (currentTarget.getSquaredDistance(pos) / range) * 15;
    }
    
    @Override
    public int getComparatorActiveState() {
        var idleTicks = world.getTime() - lastFiredAt;
        return idleTicks > 3 ? 15 : 0;
    }
    
    @Override
    public boolean hasRedstoneControlAvailable() {
        return true;
    }
    
    @Override
    public int receivedRedstoneSignal() {
        if (redstonePowered) return 15;
        return world.getReceivedRedstonePower(pos);
    }
    
    @Override
    public String currentRedstoneEffect() {
        if (redstonePowered) return "tooltip.oritech.redstone_disabled";
        return "tooltip.oritech.redstone_enabled_direct";
    }
    
    @Override
    public void onRedstoneEvent(boolean isPowered) {
        this.redstonePowered = isPowered;
    }
    
    public enum HunterTargetMode {
        HOSTILE_ONLY(1, "message.oritech.target_designator.hunter_hostile"),
        HOSTILE_NEUTRAL(2, "message.oritech.target_designator.hunter_neutral"),
        ALL(3, "message.oritech.target_designator.hunter_all");
        
        public final int value;
        public final String message;
        
        HunterTargetMode(int value, String message) {
            this.value = value;
            this.message = message;
        }
        
        private static final Map<Integer, HunterTargetMode> map = new HashMap<Integer, HunterTargetMode>();
        
        static {
            for (HunterTargetMode targetMode : HunterTargetMode.values())
                map.put(targetMode.value, targetMode);
        }
        
        public static HunterTargetMode fromValue(int i) {
            return map.getOrDefault(i, HOSTILE_ONLY);
        }
        
        public HunterTargetMode next() {
            return values()[(ordinal() + 1) % values().length];
        }
    }
}