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

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.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.registry.RegistryWrapper;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.sound.SoundCategory;
import net.minecraft.state.property.Properties;
import net.minecraft.text.Text;
import net.minecraft.util.Pair;
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 org.joml.Vector2i;
import rearth.oritech.Oritech;
import rearth.oritech.api.energy.EnergyApi;
import rearth.oritech.api.energy.containers.SimpleEnergyStorage;
import rearth.oritech.block.blocks.reactor.*;
import rearth.oritech.client.init.ParticleContent;
import rearth.oritech.client.ui.ReactorScreenHandler;
import rearth.oritech.init.BlockContent;
import rearth.oritech.init.BlockEntitiesContent;
import rearth.oritech.init.SoundContent;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.Geometry;

import java.util.*;

public class ReactorControllerBlockEntity extends BlockEntity implements BlockEntityTicker<ReactorControllerBlockEntity>, EnergyApi.BlockProvider, ExtendedMenuProvider {
    
    public static final int MAX_SIZE = Oritech.CONFIG.maxSize();
    public static final int RF_PER_PULSE = Oritech.CONFIG.rfPerPulse();
    public static final int ABSORBER_RATE = Oritech.CONFIG.absorberRate();
    public static final int VENT_BASE_RATE = Oritech.CONFIG.ventBaseRate();
    public static final int VENT_RELATIVE_RATE = Oritech.CONFIG.ventRelativeRate();
    public static final int MAX_HEAT = Oritech.CONFIG.maxHeat();
    public static final int MAX_UNSTABLE_TICKS = Oritech.CONFIG.maxUnstableTicks();
    
    private final HashMap<Vector2i, BaseReactorBlock> activeComponents = new HashMap<>();   // 2d local position on the first layer containing the reactor blocks
    private final HashMap<Vector2i, ReactorFuelPortEntity> fuelPorts = new HashMap<>();     // same grid, but contains a reference to the port at the ceiling
    private final HashMap<Vector2i, ReactorAbsorberPortEntity> absorberPorts = new HashMap<>(); // same
    private final HashMap<Vector2i, Integer> componentHeats = new HashMap<>();              // same grid, contains the current heat of the component
    private final HashMap<Vector2i, ComponentStatistics> componentStats = new HashMap<>(); // mainly for client displays, same grid
    private final HashSet<Pair<BlockPos, Direction>> energyPorts = new HashSet<>();   // list of all energy port outputs (e.g. the targets to output to)
    private final HashSet<BlockPos> redstonePorts = new HashSet<>();   // list of all redstone ports
    
    public SimpleEnergyStorage energyStorage = new SimpleEnergyStorage(0, Oritech.CONFIG.reactorMaxEnergyStored(), Oritech.CONFIG.reactorMaxEnergyStored(), this::markDirty);
    public boolean active = false;
    private int reactorStackHeight;
    private BlockPos areaMin;
    private BlockPos areaMax;
    private boolean disabledViaRedstone = false;
    private int unstableTicks = 0;
    public long disabledUntil = 0;
    
    private boolean doAutoInit = false; // used to auto-init when save is being loaded
    
    // client only
    public NetworkContent.ReactorUIDataPacket uiData;
    public NetworkContent.ReactorUISyncPacket uiSyncData;
    
    public ReactorControllerBlockEntity(BlockPos pos, BlockState state) {
        super(BlockEntitiesContent.REACTOR_CONTROLLER_BLOCK_ENTITY, pos, state);
    }
    
    // heat is only used for reactor rods and heat pipes
    // rods generate heat. Multi-cores and reflectors (expensive) change this
    // heat pipes move heat to themselves
    // vents remove heat from the hottest neighbor component
    // absorbers remove fixed heat amount from all neighboring blocks
    
    @Override
    public void tick(World world, BlockPos pos, BlockState state, ReactorControllerBlockEntity blockEntity) {
        if (world.isClient) return;
        
        if (!active && doAutoInit) {
            doAutoInit = false;
            init(null);
        }
        
        
        if (!active || activeComponents.isEmpty()) return;
        
        var activeRods = 0;
        var hottestHeat = 0;
        
        for (var localPos : activeComponents.keySet()) {
            var component = activeComponents.get(localPos);
            var componentHeat = componentHeats.get(localPos);
            
            if (component instanceof ReactorRodBlock rodBlock) {
                
                var ownRodCount = rodBlock.getRodCount();
                var receivedPulses = rodBlock.getInternalPulseCount();
                
                var portEntity = fuelPorts.get(localPos);
                if (portEntity == null || portEntity.isRemoved()) {
                    continue;
                }
                
                var hasFuel = portEntity.tryConsumeFuel(ownRodCount * reactorStackHeight, isDisabled() || disabledViaRedstone);
                var heatCreated = 0;
                
                setRodBlockState(localPos, hasFuel);
                
                if (hasFuel) {
                    // check how many pulses are received from neighbors / reflectors
                    for (var neighborPos : getNeighborsInBounds(localPos, activeComponents.keySet())) {
                        
                        var neighbor = activeComponents.get(neighborPos);
                        if (neighbor instanceof ReactorRodBlock neighborRod) {
                            receivedPulses += neighborRod.getRodCount();
                        } else if (neighbor instanceof ReactorReflectorBlock reflectorBlock) {
                            receivedPulses += rodBlock.getRodCount();
                        }
                    }
                    
                    if (!isDisabled()) {
                        activeRods++;
                        energyStorage.insertIgnoringLimit(RF_PER_PULSE * receivedPulses * reactorStackHeight, false);
                    }
                    
                    // generate heat per pulse
                    heatCreated = (receivedPulses / 2 * receivedPulses + 4);
                    componentHeat += heatCreated;
                    
                    if (componentHeat > MAX_HEAT * 0.85) {
                        playMeltdownAnimation(portEntity.getPos());
                    }
                    
                } else {
                    receivedPulses = 0;
                }
                
                componentStats.put(localPos, new ComponentStatistics((short) receivedPulses, componentHeat, (short) heatCreated));
                
            } else if (component instanceof ReactorHeatPipeBlock heatPipeBlock) {
                
                var sumGainedHeat = 0;
                
                // take heat in from neighbors
                for (var neighbor : getNeighborsInBounds(localPos, activeComponents.keySet())) {
                    var neighborHeat = componentHeats.get(neighbor);
                    if (neighborHeat <= componentHeat) continue;
                    var diff = neighborHeat - componentHeat;
                    var gainedHeat = Math.min(diff / 4 + 10, diff);
                    neighborHeat -= gainedHeat;
                    componentHeats.put(neighbor, neighborHeat);
                    componentHeat += gainedHeat;
                    sumGainedHeat += gainedHeat;
                }
                
                componentStats.put(localPos, new ComponentStatistics((short) 0, componentHeat, (short) sumGainedHeat));
                
            } else if (component instanceof ReactorAbsorberBlock absorberBlock) {
                
                var sumRemovedHeat = 0;
                var portEntity = absorberPorts.get(localPos);
                if (portEntity == null || portEntity.isRemoved()) {
                    continue;
                }
                var fuelAvailable = portEntity.getAvailableFuel();
                
                if (fuelAvailable >= reactorStackHeight) {
                    // take heat in from neighbors and remove it
                    for (var neighbor : getNeighborsInBounds(localPos, activeComponents.keySet())) {
                        var neighborHeat = componentHeats.get(neighbor);
                        if (neighborHeat <= 0) continue;
                        neighborHeat -= ABSORBER_RATE;
                        sumRemovedHeat += ABSORBER_RATE;
                        componentHeats.put(neighbor, neighborHeat);
                    }
                } else if (fuelAvailable > 0) {
                    // remove last small unusable part
                    portEntity.consumeFuel(fuelAvailable);
                }
                
                if (sumRemovedHeat > 0) {
                    portEntity.consumeFuel(reactorStackHeight);
                }
                
                componentStats.put(localPos, new ComponentStatistics((short) 0, 0, (short) sumRemovedHeat));
            } else if (component instanceof ReactorHeatVentBlock ventBlock) {
                
                // remove heat from hottest neighbor
                
                var hottestPos = localPos;
                var max = 0;
                for (var neighbor : getNeighborsInBounds(localPos, activeComponents.keySet())) {
                    var neighborHeat = componentHeats.get(neighbor);
                    if (neighborHeat <= max) continue;
                    hottestPos = neighbor;
                    max = neighborHeat;
                }
                
                var removed = 0;
                if (max != 0) {
                    var neighborHeat = max;
                    removed = Math.min(neighborHeat / VENT_RELATIVE_RATE + VENT_BASE_RATE, neighborHeat);
                    neighborHeat -= removed;
                    componentHeats.put(hottestPos, neighborHeat);
                }
                
                componentStats.put(localPos, new ComponentStatistics((short) 0, 0, (short) removed));
                
            }
            
            componentHeats.put(localPos, componentHeat);
            
            if (componentHeat > hottestHeat)
                hottestHeat = componentHeat;
            
        }
        
        outputEnergy();
        updateRedstonePorts(hottestHeat, activeRods);
        
        if (activeRods > 0)
            playAmbientSound();
        
        if (activeRods > 0 && hottestHeat > MAX_HEAT * 0.8f) {
            playWarningSound();
        }
        
        if (hottestHeat > MAX_HEAT && activeRods > 0) {
            unstableTicks++;
            if (unstableTicks > MAX_UNSTABLE_TICKS)
                doReactorExplosion(activeRods * reactorStackHeight);
        } else {
            unstableTicks = 0;
        }
        
        if (world.getTime() % 2 == 0)
            sendUINetworkData();
        
    }
    
    private boolean isDisabled() {
        return world.getTime() < disabledUntil;
    }
    
    @Override
    protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.writeNbt(nbt, registryLookup);
        
        nbt.putLong("energy_stored", energyStorage.getAmount());
        nbt.putBoolean("was_active", active);
        nbt.putBoolean("redstone_disabled", disabledViaRedstone);
        
    }
    
    @Override
    protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) {
        super.readNbt(nbt, registryLookup);
        
        energyStorage.setAmount(nbt.getLong("energy_stored"));
        doAutoInit = nbt.getBoolean("was_active");
        disabledViaRedstone = nbt.getBoolean("redstone_disabled");
    }
    
    private void playMeltdownAnimation(BlockPos port) {
        ParticleContent.MELTDOWN_IMMINENT.spawn(world, port.toCenterPos().add(0, 0.3, 0), 5);
    }
    
    private void playAmbientSound() {
        var soundDuration = 250;
        
        if (world.getTime() % soundDuration == 0)
            world.playSound(null, pos, SoundContent.REACTOR, SoundCategory.BLOCKS, 0.7f, 0.8f);
    }
    
    
    private void playWarningSound() {
        var soundDuration = 50;
        
        if (world.getTime() % soundDuration == 0)
            world.playSound(null, pos, SoundContent.REACTOR_WARNING, SoundCategory.BLOCKS, 4f, 0.8f);
    }
    
    // strength is the amount of total active rods (e.g. activeRods * stackHeight)
    private void doReactorExplosion(int strength) {
        
        if (Oritech.CONFIG.safeMode()) {
            disableReactor();
            return;
        }
        
        var spawnedBlock = BlockContent.REACTOR_EXPLOSION_SMALL;
        if (strength > 8 && strength <= 25) {
            spawnedBlock = BlockContent.REACTOR_EXPLOSION_MEDIUM;
        } else if (strength > 25) {
            spawnedBlock = BlockContent.REACTOR_EXPLOSION_LARGE;
        }
        
        world.setBlockState(pos, spawnedBlock.getDefaultState());
    }
    
    private void disableReactor() {
        this.disabledUntil = world.getTime() + Oritech.CONFIG.safeModeCooldown();
    }
    
    public void init(@Nullable PlayerEntity player) {
        
        active = false;
        
        // find low and high corners of reactor
        var cornerA = pos;
        cornerA = expandWall(cornerA, new Vec3i(0, -1, 0), true);   // first go down through other wall blocks
        cornerA = expandWall(cornerA, new Vec3i(0, 0, -1));
        cornerA = expandWall(cornerA, new Vec3i(-1, 0, 0));
        cornerA = expandWall(cornerA, new Vec3i(0, 0, -1)); // expand z again to support all rotations
        
        var cornerB = cornerA;
        cornerB = expandWall(cornerB, new Vec3i(0, 1, 0));
        cornerB = expandWall(cornerB, new Vec3i(0, 0, 1));
        cornerB = expandWall(cornerB, new Vec3i(1, 0, 0));
        
        if (cornerA == pos || cornerB == pos || cornerA == cornerB || onSameAxis(cornerA, cornerB)) {
            if (player != null)
                player.sendMessage(Text.translatable("message.oritech.reactor_edge_invalid"));
            return;
        }
        
        // verify and load all blocks in reactor area
        var finalCornerA = cornerA;
        var finalCornerB = cornerB;
        
        // these get loaded in the next step
        energyPorts.clear();
        redstonePorts.clear();
        
        // verify edges
        var wallsValid = BlockPos.stream(cornerA, cornerB).allMatch(pos -> {
            if (isAtEdgeOfBox(pos, finalCornerA, finalCornerB)) {
                var block = world.getBlockState(pos).getBlock();
                return block instanceof ReactorWallBlock;
            } else if (isOnWall(pos, finalCornerA, finalCornerB)) {
                var state = world.getBlockState(pos);
                var block = state.getBlock();
                
                // load wall energy ports
                if (block instanceof ReactorEnergyPortBlock) {
                    var facing = state.get(Properties.FACING);
                    var blockInFront = pos.add(Geometry.getForward(facing));
                    energyPorts.add(new Pair<>(blockInFront, Direction.fromVector(Geometry.getBackward(facing).getX(), Geometry.getBackward(facing).getY(), Geometry.getBackward(facing).getZ())));
                } else if (block instanceof ReactorRedstonePortBlock) {
                    redstonePorts.add(pos.toImmutable());
                }
                
                return !(block instanceof BaseReactorBlock reactorBlock) || reactorBlock.validForWalls();
            }
            
            return true;
        });
        
        if (!wallsValid) {
            if (player != null)
                player.sendMessage(Text.translatable("message.oritech.reactor_wall_invalid"));
            return;
        }
        
        // verify interior is identical in all layers
        var interiorHeight = cornerB.getY() - cornerA.getY() - 1;
        var cornerAFlat = cornerA.add(1, 1, 1);
        var cornerBFlat = new BlockPos(cornerB.getX() - 1, cornerA.getY() + 1, cornerB.getZ() - 1);
        
        // these get loaded in the next step
        fuelPorts.clear();
        absorberPorts.clear();
        reactorStackHeight = interiorHeight;
        
        var interiorStackedRight = BlockPos.stream(cornerAFlat, cornerBFlat).allMatch(pos -> {
            
            var offset = pos.subtract(cornerAFlat);
            var localPos = new Vector2i(offset.getX(), offset.getZ());
            
            var block = world.getBlockState(pos).getBlock();
            if (!(block instanceof BaseReactorBlock reactorBlock)) return true;
            
            for (int i = 1; i < interiorHeight; i++) {
                var candidatePos = pos.add(0, i, 0);
                var candidate = world.getBlockState(candidatePos);
                if (!candidate.getBlock().equals(block))
                    return false;
            }
            
            var requiredCeiling = reactorBlock.requiredStackCeiling();
            if (requiredCeiling != Blocks.AIR) {
                var ceilingPos = pos.add(0, interiorHeight, 0);
                var ceilingBlock = world.getBlockState(ceilingPos).getBlock();
                if (!requiredCeiling.equals(ceilingBlock)) return false;
                
                if (block instanceof ReactorRodBlock) {
                    fuelPorts.put(localPos, (ReactorFuelPortEntity) world.getBlockEntity(ceilingPos));
                } else if (block instanceof ReactorAbsorberBlock) {
                    absorberPorts.put(localPos, (ReactorAbsorberPortEntity) world.getBlockEntity(ceilingPos));
                }
                
            }
            activeComponents.put(localPos, reactorBlock);
            componentHeats.putIfAbsent(localPos, 0);
            
            return true;
        });
        
        if (!interiorStackedRight) {
            if (player != null)
                player.sendMessage(Text.translatable("message.oritech.reactor_interior_issues"));
            return;
        }
        
        areaMin = finalCornerA;
        areaMax = finalCornerB;
        active = true;
        
    }
    
    private void setRodBlockState(Vector2i localPos, boolean on) {
        if (world.getTime() % 10 != 0) return;
        var stackTop = fuelPorts.get(localPos).getPos();
        
        for (int i = 1; i <= reactorStackHeight; i++) {
            var candidatePos = stackTop.down(i);
            var candidateState = world.getBlockState(candidatePos);
            if (!(candidateState.getBlock() instanceof ReactorRodBlock)) continue;
            var oldLit = candidateState.get(Properties.LIT);
            if (oldLit != on) {
                // update only when changed
                world.setBlockState(candidatePos, candidateState.with(Properties.LIT, on), Block.NOTIFY_LISTENERS, 0);
            }
        }
    }
    
    private static Set<Vector2i> getNeighborsInBounds(Vector2i pos, Set<Vector2i> keys) {
        
        var res = new HashSet<Vector2i>(4);
        
        var a = new Vector2i(pos).add(-1, 0);
        if (keys.contains(a)) res.add(a);
        var b = new Vector2i(pos).add(0, 1);
        if (keys.contains(b)) res.add(b);
        var c = new Vector2i(pos).add(1, 0);
        if (keys.contains(c)) res.add(c);
        var d = new Vector2i(pos).add(0, -1);
        if (keys.contains(d)) res.add(d);
        
        return res;
    }
    
    private static boolean onSameAxis(BlockPos A, BlockPos B) {
        return A.getX() == B.getX() || A.getY() == B.getY() || A.getZ() == B.getZ();
    }
    
    private static boolean isOnWall(BlockPos pos, BlockPos min, BlockPos max) {
        return onSameAxis(pos, min) || onSameAxis(pos, max);
    }
    
    private static boolean isAtEdgeOfBox(BlockPos pos, BlockPos min, BlockPos max) {
        int planesAligned = 0;
        
        if (pos.getX() == min.getX() || pos.getX() == max.getX()) planesAligned++;
        if (pos.getY() == min.getY() || pos.getY() == max.getY()) planesAligned++;
        if (pos.getZ() == min.getZ() || pos.getZ() == max.getZ()) planesAligned++;
        
        return planesAligned >= 2;
    }
    
    private BlockPos expandWall(BlockPos from, Vec3i direction) {
        return expandWall(from, direction, false);
    }
    
    private BlockPos expandWall(BlockPos from, Vec3i direction, boolean allReactorBlocks) {
        
        var result = from;
        for (int i = 1; i < MAX_SIZE; i++) {
            var candidate = from.add(direction.multiply(i));
            var candidateBlock = world.getBlockState(candidate).getBlock();
            
            if (!allReactorBlocks && !(candidateBlock instanceof ReactorWallBlock)) return result;
            if (allReactorBlocks && !(candidateBlock instanceof BaseReactorBlock)) return result;
            
            result = candidate;
        }
        
        return result;
        
    }
    
    private void updateRedstonePorts(int hottestTemp, int filledRods) {
        
        disabledViaRedstone = false;
        
        for (var pos : redstonePorts) {
            var state = world.getBlockState(pos);
            if (!state.getBlock().equals(BlockContent.REACTOR_REDSTONE_PORT)) continue;
            
            var resOutput = 0;
            
            var mode = state.get(ReactorRedstonePortBlock.PORT_MODE);
            if (mode == 0 && hottestTemp > 0) {    // temp of hottest component
                resOutput = (int) ((hottestTemp / (float) MAX_HEAT) * 15);
                resOutput = Math.max(resOutput, 1);  // ensure at least level 1 if any component has heat
            } else if (mode == 1) { // amount of rods with fuel
                resOutput = Math.min(filledRods, 15);
            } else if (mode == 2 && energyStorage.getAmount() > 0) { // amount of energy stored
                var fillPercentage = energyStorage.getAmount() / (float) energyStorage.getCapacity();
                resOutput = (int) (1 + fillPercentage * 14);
            }
            
            resOutput = Math.min(resOutput, 15);
            
            var lastLevel = state.get(Properties.POWER);
            if (lastLevel != resOutput) {
                world.setBlockState(pos, state.with(Properties.POWER, resOutput));
                world.markDirty(pos);
            }
            
            if (world.isReceivingRedstonePower(pos)) {
                disabledViaRedstone = true;
            }
            
        }
        
    }
    
    private void outputEnergy() {
        
        var totalMoved = 0;
        var maxRatePerSlot = Oritech.CONFIG.reactorMaxEnergyOutput();
        
        var randomOrderedList = new ArrayList<>(energyPorts);
        Collections.shuffle(randomOrderedList);
        
        for (var candidateData : randomOrderedList) {
            var candidate = EnergyApi.BLOCK.find(world, candidateData.getLeft(), candidateData.getRight());
            if (candidate == null) continue;
            var moved = EnergyApi.transfer(energyStorage, candidate, maxRatePerSlot, false);
            
            if (moved > 0)
                candidate.update();
            
            totalMoved += moved;
        }
        
        if (totalMoved > 0)
            energyStorage.update();
    }
    
    @Override
    public EnergyApi.EnergyStorage getEnergyStorage(Direction direction) {
        return energyStorage;
    }
    
    private void sendUINetworkData() {
        
        if (!active || activeComponents.isEmpty() || !isActivelyViewed()) return;
        
        for (var port : fuelPorts.values()) port.updateNetwork();
        for (var port : absorberPorts.values()) port.updateNetwork();
        
        var positionsFlat = activeComponents.keySet();
        var positions = positionsFlat.stream().map(pos -> areaMin.add(pos.x + 1, 1, pos.y + 1)).toList();
        var heats = positionsFlat.stream().map(pos -> componentStats.getOrDefault(pos, ComponentStatistics.EMPTY)).toList();
        
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.ReactorUISyncPacket(pos, positions, heats, energyStorage.getAmount()));
    }
    
    private boolean isActivelyViewed() {
        var closestPlayer = Objects.requireNonNull(world).getClosestPlayer(pos.getX(), pos.getY(), pos.getZ(), 5, false);
        return closestPlayer != null && closestPlayer.currentScreenHandler instanceof ReactorScreenHandler handler && getPos().equals(handler.reactorEntity.pos);
    }
    
    @Override
    public Text getDisplayName() {
        return Text.of("");
    }
    
    @Nullable
    @Override
    public ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
        return new ReactorScreenHandler(syncId, playerInventory, this);
    }
    
    @Override
    public void saveExtraData(PacketByteBuf buf) {
        var previewMax = new BlockPos(areaMax.getX(), areaMin.getY() + 1, areaMax.getZ());
        NetworkContent.MACHINE_CHANNEL.serverHandle(this).send(new NetworkContent.ReactorUIDataPacket(pos, areaMin, areaMax, previewMax));
        buf.writeBlockPos(pos);
    }
    
    public record ComponentStatistics(short receivedPulses, int storedHeat, short heatChanged) {
        public static final ComponentStatistics EMPTY = new ComponentStatistics((short) 0, -1, (short) 0);
    }
}