Network energy cables (#2493)

This commit is contained in:
Technici4n 2021-09-16 22:23:41 +02:00 committed by GitHub
parent 9d6890946d
commit 086ad25a47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 297 additions and 133 deletions

View file

@ -24,7 +24,7 @@
package techreborn.blockentity.cable; 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.Block;
import net.minecraft.block.BlockState; import net.minecraft.block.BlockState;
import net.minecraft.block.entity.BlockEntity; import net.minecraft.block.entity.BlockEntity;
@ -34,6 +34,7 @@ import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtHelper; import net.minecraft.nbt.NbtHelper;
import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.LiteralText; import net.minecraft.text.LiteralText;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.text.TranslatableText; import net.minecraft.text.TranslatableText;
@ -49,45 +50,49 @@ import reborncore.common.network.NetworkManager;
import reborncore.common.powerSystem.PowerSystem; import reborncore.common.powerSystem.PowerSystem;
import reborncore.common.util.StringUtils; import reborncore.common.util.StringUtils;
import team.reborn.energy.api.EnergyStorage; import team.reborn.energy.api.EnergyStorage;
import team.reborn.energy.api.EnergyStorageUtil;
import team.reborn.energy.api.base.SimpleSidedEnergyContainer; import team.reborn.energy.api.base.SimpleSidedEnergyContainer;
import techreborn.blocks.cable.CableBlock; import techreborn.blocks.cable.CableBlock;
import techreborn.init.TRBlockEntities; import techreborn.init.TRBlockEntities;
import techreborn.init.TRContent; import techreborn.init.TRContent;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/**
* Created by modmuss50 on 19/05/2017.
*/
public class CableBlockEntity extends BlockEntity public class CableBlockEntity extends BlockEntity
implements BlockEntityTicker<CableBlockEntity>, IListInfoProvider, IToolDrop { 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 @Override
public long getCapacity() { public long getCapacity() {
return getCableType().transferRate * 4; return getCableType().transferRate * 4L;
} }
@Override @Override
public long getMaxInsert(@Nullable Direction side) { public long getMaxInsert(Direction side) {
if (!canAcceptEnergy(side)) { if (allowTransfer(side)) return getCableType().transferRate;
return 0; else return 0;
}
return getCableType().transferRate;
} }
@Override @Override
public long getMaxExtract(@Nullable Direction side) { public long getMaxExtract(Direction side) {
return getCableType().transferRate; if (allowTransfer(side)) return getCableType().transferRate;
else return 0;
} }
}; };
private double energy = 0;
private TRContent.Cables cableType = null; private TRContent.Cables cableType = null;
private ArrayList<Direction> sendingFace = new ArrayList<>();
private BlockState cover = null; 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) { public CableBlockEntity(BlockPos pos, BlockState state) {
super(TRBlockEntities.CABLE, pos, state); super(TRBlockEntities.CABLE, pos, state);
@ -98,7 +103,7 @@ public class CableBlockEntity extends BlockEntity
this.cableType = type; this.cableType = type;
} }
private TRContent.Cables getCableType() { TRContent.Cables getCableType() {
if (cableType != null) { if (cableType != null) {
return cableType; return cableType;
} }
@ -113,6 +118,10 @@ public class CableBlockEntity extends BlockEntity
return TRContent.Cables.COPPER; return TRContent.Cables.COPPER;
} }
private boolean allowTransfer(Direction side) {
return !ioBlocked && (blockedSides & (1 << side.ordinal())) == 0;
}
public EnergyStorage getSideEnergyStorage(@Nullable Direction side) { public EnergyStorage getSideEnergyStorage(@Nullable Direction side) {
return energyContainer.getSideStorage(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() { public long getEnergy() {
return energyContainer.amount; return energyContainer.amount;
} }
@ -160,7 +162,7 @@ public class CableBlockEntity extends BlockEntity
public void readNbt(NbtCompound compound) { public void readNbt(NbtCompound compound) {
super.readNbt(compound); super.readNbt(compound);
if (compound.contains("energy")) { if (compound.contains("energy")) {
energy = compound.getDouble("energy"); energyContainer.amount = compound.getLong("energy");
} }
if (compound.contains("cover")) { if (compound.contains("cover")) {
cover = NbtHelper.toBlockState(compound.getCompound("cover")); cover = NbtHelper.toBlockState(compound.getCompound("cover"));
@ -172,13 +174,17 @@ public class CableBlockEntity extends BlockEntity
@Override @Override
public NbtCompound writeNbt(NbtCompound compound) { public NbtCompound writeNbt(NbtCompound compound) {
super.writeNbt(compound); super.writeNbt(compound);
compound.putDouble("energy", energy); compound.putLong("energy", energyContainer.amount);
if (cover != null) { if (cover != null) {
compound.put("cover", NbtHelper.fromBlockState(cover)); compound.put("cover", NbtHelper.fromBlockState(cover));
} }
return compound; return compound;
} }
public void neighborUpdate() {
targets = null;
}
// Tickable // Tickable
@Override @Override
public void tick(World world, BlockPos pos, BlockState state, CableBlockEntity blockEntity2) { public void tick(World world, BlockPos pos, BlockState state, CableBlockEntity blockEntity2) {
@ -186,72 +192,7 @@ public class CableBlockEntity extends BlockEntity
return; return;
} }
sendingFace.clear(); CableTickManager.handleCableTick(this);
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--;
}
}
} }
// IListInfoProvider // IListInfoProvider
@ -286,4 +227,67 @@ public class CableBlockEntity extends BlockEntity
public ItemStack getToolDrop(PlayerEntity playerIn) { public ItemStack getToolDrop(PlayerEntity playerIn) {
return new ItemStack(getCableType().block); 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());
}
}
} }

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

View file

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

View file

@ -105,42 +105,6 @@ public class CableBlock extends BlockWithEntity implements Waterloggable {
cableShapeUtil = new CableShapeUtil(this); 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 // BlockContainer
@Override @Override
public BlockRenderType getRenderType(BlockState state) { public BlockRenderType getRenderType(BlockState state) {
@ -210,7 +174,7 @@ public class CableBlock extends BlockWithEntity implements Waterloggable {
@Override @Override
public BlockState getPlacementState(ItemPlacementContext context) { public BlockState getPlacementState(ItemPlacementContext context) {
return makeConnections(context.getWorld(), context.getBlockPos()) return getDefaultState()
.with(WATERLOGGED, context.getWorld().getFluidState(context.getBlockPos()).getFluid() == Fluids.WATER); .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)) { if (ourState.get(WATERLOGGED)) {
worldIn.getFluidTickScheduler().schedule(ourPos, Fluids.WATER, Fluids.WATER.getTickRate(worldIn)); worldIn.getFluidTickScheduler().schedule(ourPos, Fluids.WATER, Fluids.WATER.getTickRate(worldIn));
} }
Boolean value = canConnectTo(worldIn, otherPos, direction); return ourState;
return ourState.with(getProperty(direction), value); }
@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") @SuppressWarnings("deprecation")