Network energy cables (#2493)
This commit is contained in:
parent
9d6890946d
commit
086ad25a47
4 changed files with 297 additions and 133 deletions
|
@ -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<CableBlockEntity>, 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<Direction> sendingFace = new ArrayList<>();
|
||||
private BlockState cover = null;
|
||||
long lastTick = 0;
|
||||
// null means that it needs to be re-queried
|
||||
List<CableTarget> 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<EnergyStorage> acceptors = new ArrayList<>();
|
||||
ArrayList<CableBlockEntity> 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<OfferedEnergyStorage> 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<EnergyStorage, Direction> cache;
|
||||
|
||||
CableTarget(Direction directionTo, BlockApiCache<EnergyStorage, Direction> cache) {
|
||||
this.directionTo = directionTo;
|
||||
this.cache = cache;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
EnergyStorage find() {
|
||||
return cache.find(directionTo.getOpposite());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
164
src/main/java/techreborn/blockentity/cable/CableTickManager.java
Normal file
164
src/main/java/techreborn/blockentity/cable/CableTickManager.java
Normal file
|
@ -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<CableBlockEntity> cableList = new ArrayList<>();
|
||||
private static final List<OfferedEnergyStorage> targetStorages = new ArrayList<>();
|
||||
private static final Deque<CableBlockEntity> 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<SortableStorage> 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++);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue