From 086ad25a4705acb81ee90661c84c218a577e1e9f Mon Sep 17 00:00:00 2001 From: Technici4n <13494793+Technici4n@users.noreply.github.com> Date: Thu, 16 Sep 2021 22:23:41 +0200 Subject: [PATCH] Network energy cables (#2493) --- .../blockentity/cable/CableBlockEntity.java | 192 +++++++++--------- .../blockentity/cable/CableTickManager.java | 164 +++++++++++++++ .../cable/OfferedEnergyStorage.java | 24 +++ .../techreborn/blocks/cable/CableBlock.java | 50 +---- 4 files changed, 297 insertions(+), 133 deletions(-) create mode 100644 src/main/java/techreborn/blockentity/cable/CableTickManager.java create mode 100644 src/main/java/techreborn/blockentity/cable/OfferedEnergyStorage.java diff --git a/src/main/java/techreborn/blockentity/cable/CableBlockEntity.java b/src/main/java/techreborn/blockentity/cable/CableBlockEntity.java index 04f72a3f2..c8b7a8996 100644 --- a/src/main/java/techreborn/blockentity/cable/CableBlockEntity.java +++ b/src/main/java/techreborn/blockentity/cable/CableBlockEntity.java @@ -24,7 +24,7 @@ package techreborn.blockentity.cable; -import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiCache; import net.minecraft.block.Block; import net.minecraft.block.BlockState; import net.minecraft.block.entity.BlockEntity; @@ -34,6 +34,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtHelper; import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.server.world.ServerWorld; import net.minecraft.text.LiteralText; import net.minecraft.text.Text; import net.minecraft.text.TranslatableText; @@ -49,45 +50,49 @@ import reborncore.common.network.NetworkManager; import reborncore.common.powerSystem.PowerSystem; import reborncore.common.util.StringUtils; import team.reborn.energy.api.EnergyStorage; -import team.reborn.energy.api.EnergyStorageUtil; import team.reborn.energy.api.base.SimpleSidedEnergyContainer; import techreborn.blocks.cable.CableBlock; import techreborn.init.TRBlockEntities; import techreborn.init.TRContent; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -/** - * Created by modmuss50 on 19/05/2017. - */ - public class CableBlockEntity extends BlockEntity implements BlockEntityTicker, IListInfoProvider, IToolDrop { - private final SimpleSidedEnergyContainer energyContainer = new SimpleSidedEnergyContainer() { + // Can't use SimpleEnergyStorage because the cable type is not available when the BE is constructed. + final SimpleSidedEnergyContainer energyContainer = new SimpleSidedEnergyContainer() { @Override public long getCapacity() { - return getCableType().transferRate * 4; + return getCableType().transferRate * 4L; } @Override - public long getMaxInsert(@Nullable Direction side) { - if (!canAcceptEnergy(side)) { - return 0; - } - return getCableType().transferRate; + public long getMaxInsert(Direction side) { + if (allowTransfer(side)) return getCableType().transferRate; + else return 0; } @Override - public long getMaxExtract(@Nullable Direction side) { - return getCableType().transferRate; + public long getMaxExtract(Direction side) { + if (allowTransfer(side)) return getCableType().transferRate; + else return 0; } }; - private double energy = 0; private TRContent.Cables cableType = null; - private ArrayList sendingFace = new ArrayList<>(); private BlockState cover = null; + long lastTick = 0; + // null means that it needs to be re-queried + List targets = null; + /** + * Bitmask to prevent input or output into/from the cable when the cable already transferred in the target direction. + * This prevents double transfer rates, and back and forth between two cables. + */ + int blockedSides = 0; + /** + * This is only used during the cable tick, whereas blockedOperations is used between ticks. + */ + boolean ioBlocked = false; public CableBlockEntity(BlockPos pos, BlockState state) { super(TRBlockEntities.CABLE, pos, state); @@ -98,7 +103,7 @@ public class CableBlockEntity extends BlockEntity this.cableType = type; } - private TRContent.Cables getCableType() { + TRContent.Cables getCableType() { if (cableType != null) { return cableType; } @@ -113,6 +118,10 @@ public class CableBlockEntity extends BlockEntity return TRContent.Cables.COPPER; } + private boolean allowTransfer(Direction side) { + return !ioBlocked && (blockedSides & (1 << side.ordinal())) == 0; + } + public EnergyStorage getSideEnergyStorage(@Nullable Direction side) { return energyContainer.getSideStorage(side); } @@ -128,13 +137,6 @@ public class CableBlockEntity extends BlockEntity } } - public boolean canAcceptEnergy(@Nullable Direction direction) { - if (sendingFace.contains(direction)) { - return false; - } - return energyContainer.getCapacity() != getEnergy(); - } - public long getEnergy() { return energyContainer.amount; } @@ -160,7 +162,7 @@ public class CableBlockEntity extends BlockEntity public void readNbt(NbtCompound compound) { super.readNbt(compound); if (compound.contains("energy")) { - energy = compound.getDouble("energy"); + energyContainer.amount = compound.getLong("energy"); } if (compound.contains("cover")) { cover = NbtHelper.toBlockState(compound.getCompound("cover")); @@ -172,13 +174,17 @@ public class CableBlockEntity extends BlockEntity @Override public NbtCompound writeNbt(NbtCompound compound) { super.writeNbt(compound); - compound.putDouble("energy", energy); + compound.putLong("energy", energyContainer.amount); if (cover != null) { compound.put("cover", NbtHelper.fromBlockState(cover)); } return compound; } + public void neighborUpdate() { + targets = null; + } + // Tickable @Override public void tick(World world, BlockPos pos, BlockState state, CableBlockEntity blockEntity2) { @@ -186,72 +192,7 @@ public class CableBlockEntity extends BlockEntity return; } - sendingFace.clear(); - - if (getEnergy() == 0) { - return; - } - - ArrayList acceptors = new ArrayList<>(); - ArrayList cables = new ArrayList<>(); - - for (Direction face : Direction.values()) { - BlockPos adjPos = pos.offset(face); - BlockEntity blockEntity = world.getBlockEntity(adjPos); - EnergyStorage target = EnergyStorage.SIDED.find(world, adjPos, null, blockEntity, face.getOpposite()); - if (target == null) { - continue; - } - - if (blockEntity instanceof CableBlockEntity cableBlockEntity) { - if (cableBlockEntity.cableType != this.cableType) { - continue; - } - // Only need ones with energy stores less than ours - if (cableBlockEntity.getEnergy() < this.getEnergy()) { - cables.add(cableBlockEntity); - } - - - } else { - try (Transaction transaction = Transaction.openOuter()) { - if (target.insert(Long.MAX_VALUE, transaction) > 0) { - acceptors.add(target); - if (!sendingFace.contains(face)) { - sendingFace.add(face); - } - } - } - } - } - - if (!acceptors.isEmpty()) { - Collections.shuffle(acceptors); - - acceptors.forEach(t -> EnergyStorageUtil.move( - energyContainer.getSideStorage(null), - t, - Long.MAX_VALUE, - null - )); - } - - - // Distribute energy between cables (TODO DIRTY FIX, until we game network cables) - - if (!cables.isEmpty()) { - - cables.add(this); - long energyTotal = cables.stream().mapToLong(CableBlockEntity::getEnergy).sum(); - long remainingCount = cables.size(); - - for (CableBlockEntity cable : cables) { - long newEnergy = energyTotal / remainingCount; - cable.setEnergy(newEnergy); - energyTotal -= newEnergy; - remainingCount--; - } - } + CableTickManager.handleCableTick(this); } // IListInfoProvider @@ -286,4 +227,67 @@ public class CableBlockEntity extends BlockEntity public ItemStack getToolDrop(PlayerEntity playerIn) { return new ItemStack(getCableType().block); } + + void appendTargets(List targetStorages) { + ServerWorld serverWorld = (ServerWorld) world; + + // Update our targets if necessary. + if (targets == null) { + BlockState newBlockState = getCachedState(); + + targets = new ArrayList<>(); + for (Direction direction : Direction.values()) { + boolean foundSomething = false; + + BlockPos adjPos = getPos().offset(direction); + BlockEntity adjBe = serverWorld.getBlockEntity(adjPos); + + if (adjBe instanceof CableBlockEntity adjCable && adjCable.getCableType() == getCableType()) { + // Make sure cables are not used as regular targets. + foundSomething = true; + } else if (EnergyStorage.SIDED.find(serverWorld, adjPos, null, adjBe, direction.getOpposite()) != null) { + foundSomething = true; + targets.add(new CableTarget( + direction, + BlockApiCache.create(EnergyStorage.SIDED, serverWorld, adjPos) + )); + } + + newBlockState = newBlockState.with(CableBlock.PROPERTY_MAP.get(direction), foundSomething); + } + + serverWorld.setBlockState(getPos(), newBlockState); + } + + // Fill the list. + for (CableTarget target : targets) { + EnergyStorage storage = target.find(); + + if (storage == null) { + // Schedule a rebuild next tick. + // This is just a reference change, the iterator remains valid. + targets = null; + } else { + targetStorages.add(new OfferedEnergyStorage(this, target.directionTo, storage)); + } + } + + // Reset blocked sides. + blockedSides = 0; + } + + private static final class CableTarget { + private final Direction directionTo; + private final BlockApiCache cache; + + CableTarget(Direction directionTo, BlockApiCache cache) { + this.directionTo = directionTo; + this.cache = cache; + } + + @Nullable + EnergyStorage find() { + return cache.find(directionTo.getOpposite()); + } + } } diff --git a/src/main/java/techreborn/blockentity/cable/CableTickManager.java b/src/main/java/techreborn/blockentity/cable/CableTickManager.java new file mode 100644 index 000000000..81b4b9c55 --- /dev/null +++ b/src/main/java/techreborn/blockentity/cable/CableTickManager.java @@ -0,0 +1,164 @@ +package techreborn.blockentity.cable; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import team.reborn.energy.api.EnergyStorage; +import techreborn.init.TRContent; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.List; + +@SuppressWarnings("UnstableApiUsage") +class CableTickManager { + private static long tickCounter = 0; + private static final List cableList = new ArrayList<>(); + private static final List targetStorages = new ArrayList<>(); + private static final Deque bfsQueue = new ArrayDeque<>(); + + static void handleCableTick(CableBlockEntity startingCable) { + if (!(startingCable.getWorld() instanceof ServerWorld)) throw new IllegalStateException(); + + try { + gatherCables(startingCable); + if (cableList.size() == 0) return; + + // Group all energy into the network. + long networkCapacity = 0; + long networkAmount = 0; + + for (CableBlockEntity cable : cableList) { + networkAmount += cable.energyContainer.amount; + networkCapacity += cable.energyContainer.getCapacity(); + + // Update cable connections. + cable.appendTargets(targetStorages); + // Block any cable I/O while we access the network amount directly. + // Some things might try to access cables, for example a p2p tunnel pointing back at a cable. + // If the cables and the network go out of sync, we risk duping or voiding energy. + cable.ioBlocked = true; + } + + // Just in case. + if (networkAmount > networkCapacity) { + networkAmount = networkCapacity; + } + + // Pull energy from storages. + networkAmount += dispatchTransfer(startingCable.getCableType(), EnergyStorage::extract, networkCapacity - networkAmount); + // Push energy into storages. + networkAmount -= dispatchTransfer(startingCable.getCableType(), EnergyStorage::insert, networkAmount); + + // Split energy evenly across cables. + int cableCount = cableList.size(); + for (CableBlockEntity cable : cableList) { + cable.energyContainer.amount = networkAmount / cableCount; + networkAmount -= cable.energyContainer.amount; + cableCount--; + cable.markDirty(); + cable.ioBlocked = false; + } + } finally { + cableList.clear(); + targetStorages.clear(); + bfsQueue.clear(); + } + } + + private static boolean shouldTickCable(CableBlockEntity current) { + // Make sure we only gather and tick each cable once per tick. + if (current.lastTick == tickCounter) return false; + // Make sure we ignore cables in non-ticking chunks. + if (!(current.getWorld() instanceof ServerWorld sw) || !sw.method_37117(current.getPos())) return false; + + return true; + } + + /** + * Perform a BFS to gather all connected ticking cables. + */ + private static void gatherCables(CableBlockEntity start) { + if (!shouldTickCable(start)) return; + + bfsQueue.add(start); + start.lastTick = tickCounter; + cableList.add(start); + + while (!bfsQueue.isEmpty()) { + CableBlockEntity current = bfsQueue.removeFirst(); + + for (Direction direction : Direction.values()) { + BlockPos adjPos = current.getPos().offset(direction); + + if (current.getWorld().getBlockEntity(adjPos) instanceof CableBlockEntity adjCable && current.getCableType() == adjCable.getCableType()) { + if (shouldTickCable(adjCable)) { + bfsQueue.add(adjCable); + adjCable.lastTick = tickCounter; + cableList.add(adjCable); + } + } + } + } + } + + /** + * Perform a transfer operation across a list of targets. + */ + private static long dispatchTransfer(TRContent.Cables cableType, TransferOperation operation, long maxAmount) { + // Build target list. + List sortedTargets = new ArrayList<>(); + for (var storage : targetStorages) { + sortedTargets.add(new SortableStorage(operation, storage)); + } + // Shuffle for better average transfer. + Collections.shuffle(sortedTargets); + // Sort by lowest simulation target. + sortedTargets.sort(Comparator.comparingLong(sortableStorage -> sortableStorage.simulationResult)); + // Actually perform the transfer. + try (Transaction transaction = Transaction.openOuter()) { + long transferredAmount = 0; + for (int i = 0; i < sortedTargets.size(); ++i) { + SortableStorage target = sortedTargets.get(i); + int remainingTargets = sortedTargets.size() - i; + long remainingAmount = maxAmount - transferredAmount; + // Limit max amount to the cable transfer rate. + long targetMaxAmount = Math.min(remainingAmount / remainingTargets, cableType.transferRate); + + long localTransferred = operation.transfer(target.storage.storage, targetMaxAmount, transaction); + if (localTransferred > 0) { + transferredAmount += localTransferred; + // Block duplicate operations. + target.storage.afterTransfer(); + } + } + transaction.commit(); + return transferredAmount; + } + } + + private interface TransferOperation { + long transfer(EnergyStorage storage, long maxAmount, Transaction transaction); + } + + private static class SortableStorage { + private final OfferedEnergyStorage storage; + private final long simulationResult; + + SortableStorage(TransferOperation operation, OfferedEnergyStorage storage) { + this.storage = storage; + try (Transaction tx = Transaction.openOuter()) { + this.simulationResult = operation.transfer(storage.storage, Long.MAX_VALUE, tx); + } + } + } + + static { + ServerTickEvents.START_SERVER_TICK.register(server -> tickCounter++); + } +} diff --git a/src/main/java/techreborn/blockentity/cable/OfferedEnergyStorage.java b/src/main/java/techreborn/blockentity/cable/OfferedEnergyStorage.java new file mode 100644 index 000000000..bf55bef5c --- /dev/null +++ b/src/main/java/techreborn/blockentity/cable/OfferedEnergyStorage.java @@ -0,0 +1,24 @@ +package techreborn.blockentity.cable; + +import net.minecraft.util.math.Direction; +import team.reborn.energy.api.EnergyStorage; + +/** + * EnergyStorage adjacent to an energy cable, with some additional info. + */ +class OfferedEnergyStorage { + final CableBlockEntity sourceCable; + final Direction direction; + final EnergyStorage storage; + + OfferedEnergyStorage(CableBlockEntity sourceCable, Direction direction, EnergyStorage storage) { + this.sourceCable = sourceCable; + this.direction = direction; + this.storage = storage; + } + + void afterTransfer() { + // Block insertions from this side. + sourceCable.blockedSides |= 1 << direction.ordinal(); + } +} diff --git a/src/main/java/techreborn/blocks/cable/CableBlock.java b/src/main/java/techreborn/blocks/cable/CableBlock.java index ac98293d2..92e6aa450 100644 --- a/src/main/java/techreborn/blocks/cable/CableBlock.java +++ b/src/main/java/techreborn/blocks/cable/CableBlock.java @@ -105,42 +105,6 @@ public class CableBlock extends BlockWithEntity implements Waterloggable { cableShapeUtil = new CableShapeUtil(this); } - public BooleanProperty getProperty(Direction facing) { - return switch (facing) { - case WEST -> WEST; - case NORTH -> NORTH; - case SOUTH -> SOUTH; - case UP -> UP; - case DOWN -> DOWN; - default -> EAST; - }; - } - - private BlockState makeConnections(World world, BlockPos pos) { - Boolean down = canConnectTo(world, pos, Direction.DOWN); - Boolean up = canConnectTo(world, pos, Direction.UP); - Boolean north = canConnectTo(world, pos, Direction.NORTH); - Boolean east = canConnectTo(world, pos, Direction.EAST); - Boolean south = canConnectTo(world, pos, Direction.SOUTH); - Boolean west = canConnectTo(world, pos, Direction.WEST); - - return this.getDefaultState().with(DOWN, down).with(UP, up).with(NORTH, north).with(EAST, east) - .with(SOUTH, south).with(WEST, west); - } - - private Boolean canConnectTo(WorldAccess world, BlockPos currentPos, Direction side) { - BlockPos adjPos = currentPos.offset(side); - BlockEntity adjBe = world.getBlockEntity(adjPos); - if (adjBe instanceof CableBlockEntity) { - return Boolean.TRUE; - } - // TODO: this doesn't work at all, fix this - else if (world instanceof ServerWorld sw && EnergyStorage.SIDED.find(sw, adjPos, null, adjBe, side.getOpposite()) != null) { - return Boolean.TRUE; - } - return Boolean.FALSE; - } - // BlockContainer @Override public BlockRenderType getRenderType(BlockState state) { @@ -210,7 +174,7 @@ public class CableBlock extends BlockWithEntity implements Waterloggable { @Override public BlockState getPlacementState(ItemPlacementContext context) { - return makeConnections(context.getWorld(), context.getBlockPos()) + return getDefaultState() .with(WATERLOGGED, context.getWorld().getFluidState(context.getBlockPos()).getFluid() == Fluids.WATER); } @@ -221,8 +185,16 @@ public class CableBlock extends BlockWithEntity implements Waterloggable { if (ourState.get(WATERLOGGED)) { worldIn.getFluidTickScheduler().schedule(ourPos, Fluids.WATER, Fluids.WATER.getTickRate(worldIn)); } - Boolean value = canConnectTo(worldIn, otherPos, direction); - return ourState.with(getProperty(direction), value); + return ourState; + } + + @SuppressWarnings("deprecation") + @Override + public void neighborUpdate(BlockState state, World world, BlockPos pos, Block block, BlockPos fromPos, boolean notify) { + if (world.getBlockEntity(pos) instanceof CableBlockEntity cable) { + cable.neighborUpdate(); + } + super.neighborUpdate(state, world, pos, block, fromPos, notify); } @SuppressWarnings("deprecation")