Skip to content

Commit

Permalink
Fix fluid transfer rate (#468)
Browse files Browse the repository at this point in the history
* fix: limit fluid pipe throughput per second instead of per tick

* fix: display super tank / quantum tank amount correctly on fabric

* refactor: extract max fluid pipe channels into a constant

* fix: wrong cable colors

* refactor: cleanup FluidPipeNet

* fix: large boiler stops working after being unloaded

* chore: update changelog & bump version
  • Loading branch information
mikerooni authored Oct 16, 2023
1 parent 9a41904 commit 0591fc5
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 80 deletions.
10 changes: 4 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# ChangeLog

* add tooltip to item pipes
* fix small distillery recipes missing
* fix multi smelter being able to run centrifuge recipes
* fix machines not being silenced/muffled when right-clicking with a hammer
* fix machine controller using the redstone signal from adjacent block's wrong side
* fix onChanged() not being thread-safe
* fix fluid pipe throughput being too low
* fix super tank & quantum tank amount being displayed incorrectly on fabric
* fix wires not being colored
* fix large boiler not resuming work after being unloaded
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
import java.util.Objects;

public class FluidPipeProperties implements IMaterialProperty<FluidPipeProperties>, IPropertyFluidFilter {
/**
* The maximum number of channels any fluid pipe can have
*/
public static final int MAX_PIPE_CHANNELS = 9;

@Getter @Setter
private long throughput;
@Getter @Setter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public void renderTank(PoseStack poseStack, MultiBufferSource buffer, Direction
} else {
RenderUtils.rotateToFace(poseStack, frontFacing, null);
}
var amount = TextFormattingUtil.formatLongToCompactString(stored.getAmount(), 4);
var amount = TextFormattingUtil.formatLongToCompactString(stored.getAmount() / (FluidHelper.getBucket() / 1000), 4);
poseStack.scale(1f / 64, 1f / 64, 0);
poseStack.translate(-32, -32, 0);
new TextTexture(amount).draw(poseStack, 0, 0, 0, 24, 64, 28);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public int tinted(BlockState blockState, @Nullable BlockAndTintGetter blockAndTi
if (pipeType.isCable && index == 0) {
return 0x404040;
}
return index == 1 ? material.getMaterialRGB() : -1;
return index == 0 || index == 1 ? material.getMaterialRGB() : -1;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
import net.minecraft.network.chat.Style;
import net.minecraft.server.TickTask;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Mth;
import net.minecraft.world.level.material.Fluids;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -74,6 +76,16 @@ public ManagedFieldHolder getFieldHolder() {
//****** Recipe Logic ******//
//////////////////////////////////////


@Override
public void onLoad() {
super.onLoad();

if (getLevel() instanceof ServerLevel serverLevel) {
serverLevel.getServer().tell(new TickTask(0, this::updateSteamSubscription));
}
}

protected void updateSteamSubscription() {
if (currentTemperature > 0) {
temperatureSubs = subscribeServerTick(temperatureSubs, this::updateCurrentTemperature);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
import com.gregtechceu.gtceu.api.capability.IControllable;
import com.gregtechceu.gtceu.api.capability.recipe.IO;
import com.gregtechceu.gtceu.api.gui.GuiTextures;
import com.gregtechceu.gtceu.api.gui.UITemplate;
import com.gregtechceu.gtceu.api.gui.widget.ToggleButtonWidget;
import com.gregtechceu.gtceu.api.item.tool.GTToolType;
import com.gregtechceu.gtceu.api.machine.IMachineBlockEntity;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.TickableSubscription;
import com.gregtechceu.gtceu.api.machine.TieredMachine;
import com.gregtechceu.gtceu.api.machine.feature.*;
import com.gregtechceu.gtceu.api.machine.feature.IAutoOutputFluid;
import com.gregtechceu.gtceu.api.machine.feature.IDropSaveMachine;
import com.gregtechceu.gtceu.api.machine.feature.IFancyUIMachine;
import com.gregtechceu.gtceu.api.machine.feature.IInteractedMachine;
import com.gregtechceu.gtceu.api.machine.trait.NotifiableFluidTank;
import com.gregtechceu.gtceu.api.syncdata.RequireRerender;
import com.lowdragmc.lowdraglib.gui.editor.ColorPattern;
import com.lowdragmc.lowdraglib.gui.modular.ModularUI;
import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture;
import com.lowdragmc.lowdraglib.gui.widget.*;
import com.lowdragmc.lowdraglib.misc.FluidStorage;
Expand Down Expand Up @@ -292,7 +293,9 @@ public Widget createUIWidget() {
var group = new WidgetGroup(0, 0, 90, 63);
group.addWidget(new ImageWidget(4, 4, 82, 55, GuiTextures.DISPLAY))
.addWidget(new LabelWidget(8, 8, "gtceu.gui.fluid_amount"))
.addWidget(new LabelWidget(8, 18, () -> cache.getFluidInTank(0).getAmount() + "").setTextColor(-1).setDropShadow(true))
.addWidget(new LabelWidget(8, 18, () ->
String.valueOf(cache.getFluidInTank(0).getAmount() / (FluidHelper.getBucket() / 1000))
).setTextColor(-1).setDropShadow(true))
.addWidget(new TankWidget(cache.storages[0], 68, 23, true, true)
.setBackground(GuiTextures.FLUID_SLOT))
.addWidget(new PhantomFluidWidget(lockedFluid, 68, 41, 18, 18)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
import com.gregtechceu.gtceu.api.data.chemical.material.properties.FluidPipeProperties;
import com.lowdragmc.lowdraglib.pipelike.LevelPipeNet;
import com.lowdragmc.lowdraglib.pipelike.PipeNet;
import it.unimi.dsi.fastutil.longs.*;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.material.Fluid;
import org.apache.commons.lang3.mutable.MutableLong;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Stream;

/**
* @author KilaBash
Expand All @@ -19,6 +24,10 @@ public class FluidPipeNet extends PipeNet<FluidPipeData> {

private final Map<BlockPos, List<PipeNetRoutePath>> NET_DATA = new HashMap<>();

private final Long2ObjectMap<Fluid[]> channelFluidsByBlock = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<ThroughputUsage[]> throughputUsagesByBlock = new Long2ObjectOpenHashMap<>();
private long lastUpdate;

public FluidPipeNet(LevelPipeNet<FluidPipeData, ? extends PipeNet> world) {
super(world);
lastUpdate = world.getWorld().getGameTime();
Expand Down Expand Up @@ -48,6 +57,185 @@ public void onPipeConnectionsUpdate() {
NET_DATA.clear();
}

/////////////////////////////////////
//*********** API ***********//
/////////////////////////////////////

/**
* Get the throughput of the specified block and channel, summed over the last 20 ticks.
*
* @return The throughput over the last second
*/
public long getLastSecondTotalThroughput(BlockPos blockPos, int channel) {
MutableLong totalAmount = new MutableLong(0L);

withLastSecondUsages(blockPos.asLong(), (used) -> {
totalAmount.add(used.channelAmounts[channel]);
});

return totalAmount.longValue();
}

/**
* Get the channel index that is currently used for the specified block and fluid.
* <p>
* Note that this may be outside the pipe's max channel number, so you still need to check if the range is valid.
*
* @return The channel containing the fluid or -1 if the fluid is not currently being transferred.
*/
public int getChannel(BlockPos pos, Fluid fluid) {
updateTick();
var channelFluids = getChannelFluids(pos.asLong());

for (int channel = 0; channel < channelFluids.length; channel++) {
if (channelFluids[channel] != null && channelFluids[channel].equals(fluid))
return channel;
}

return -1;
}

/**
* Use the existing channel for the specified fluid or assign it to the best free channel if none is used
* for this fluid type yet.
*
* @return The channel to be used.
*/
public int useChannel(BlockPos pos, Fluid fluid) {
// Note that updateTick() is not called separately here because getChannel() already calls it.

var channel = getChannel(pos, fluid);
if (channel != -1)
return channel;

var newChannel = findBestFreeChannel(pos.asLong());
if (newChannel == -1)
return -1;

getChannelFluids(pos.asLong())[newChannel] = fluid;
return newChannel;
}

/**
* Add the specified amount to the current tick's used throughput.
*/
public void useThroughput(BlockPos pos, int channel, long amount) {
updateTick();
getCurrentTickUsage(pos.asLong()).channelAmounts[channel] += amount;
}

public Fluid getFluid(BlockPos pos, int channel) {
updateTick();
return getChannelFluids(pos.asLong())[channel];
}

//////////////////////////////////////
//******* Pipe Status *******//
//////////////////////////////////////


private void updateTick() {
var latestTime = getWorldData().getWorld().getGameTime();
if (lastUpdate == latestTime)
return;

channelFluidsByBlock.forEach((k, v) -> Arrays.fill(v, null));

lastUpdate = latestTime;
}


private static class ThroughputUsage {
public long tick = 0L;
public long[] channelAmounts = new long[FluidPipeProperties.MAX_PIPE_CHANNELS];

public ThroughputUsage() {
resetAmounts();
}

public ThroughputUsage(ThroughputUsage value) {
this.tick = value.tick;
this.channelAmounts = Arrays.copyOf(value.channelAmounts, value.channelAmounts.length);
}

public void resetAmounts() {
Arrays.fill(channelAmounts, 0L);
}
}

@NotNull
private ThroughputUsage[] getThroughputUsages(long blockPos) {
return throughputUsagesByBlock.computeIfAbsent(blockPos, bp ->
Stream.generate(ThroughputUsage::new).limit(20).toArray(ThroughputUsage[]::new)
);
}

private ThroughputUsage getCurrentTickUsage(long blockPos) {
var currentTick = getLevel().getGameTime();
var lastSecondThroughputs = getThroughputUsages(blockPos);
var currentThroughput = lastSecondThroughputs[(int) (currentTick % 20)];

if (currentThroughput.tick != currentTick) {
currentThroughput.tick = currentTick;
currentThroughput.resetAmounts();
}

return currentThroughput;
}

private void withLastSecondUsages(long blockPos, Consumer<ThroughputUsage> consumer) {
var minTick = getLevel().getGameTime() - 19;
var lastSecondThroughputs = getThroughputUsages(blockPos);

for (int i = 0; i < 20; i++) {
ThroughputUsage throughputUsage = lastSecondThroughputs[i];
if (throughputUsage.tick < minTick)
continue;

consumer.accept(throughputUsage);
}
}

private Fluid[] getChannelFluids(long blockPos) {
return channelFluidsByBlock.computeIfAbsent(blockPos, bp -> new Fluid[FluidPipeProperties.MAX_PIPE_CHANNELS]);
}

private long[] getLastSecondTotalUsagePerChannel(long blockPos) {
var totalAmounts = new long[FluidPipeProperties.MAX_PIPE_CHANNELS];

withLastSecondUsages(blockPos, (used) -> {
for (int channel = 0; channel < FluidPipeProperties.MAX_PIPE_CHANNELS; channel++) {
totalAmounts[channel] += used.channelAmounts[channel];
}
});

return totalAmounts;
}

private int findBestFreeChannel(long pos) {
Fluid[] channelFluids = getChannelFluids(pos);
long[] lastSecondUsagePerChannel = getLastSecondTotalUsagePerChannel(pos);

long leastUsedAmount = Long.MAX_VALUE;
int bestChannel = -1;

for (int channel = 0; channel < channelFluids.length; channel++) {
if (channelFluids[channel] != null)
continue;

if (lastSecondUsagePerChannel[channel] < leastUsedAmount) {
leastUsedAmount = lastSecondUsagePerChannel[channel];
bestChannel = channel;
}
}

return bestChannel;
}

/////////////////////////////////////
//*********** NBT ***********//
/////////////////////////////////////

@Override
protected void writeNodeData(FluidPipeData nodeData, CompoundTag tagCompound) {
tagCompound.putInt("max_temperature", nodeData.properties.getMaxFluidTemperature());
Expand All @@ -72,59 +260,28 @@ protected FluidPipeData readNodeData(CompoundTag tagCompound) {
return new FluidPipeData(new FluidPipeProperties(maxTemperature, throughput, gasProof, acidProof, cryoProof, plasmaProof, channels), tagCompound.getByte("connections"));
}

//////////////////////////////////////
//******* Pipe Status *******//
//////////////////////////////////////

private final Long2ObjectMap<Set<Fluid>> channelUsed = new Long2ObjectOpenHashMap<>();
private final Long2LongMap throughputUsed = new Long2LongOpenHashMap();
private long lastUpdate;

private void updateTick() {
var latestTime = getWorldData().getWorld().getGameTime();
if (lastUpdate != latestTime) {
channelUsed.clear();
throughputUsed.clear();
}
lastUpdate = latestTime;
}

public Set<Fluid> getChannelUsed(BlockPos pos) {
updateTick();
return channelUsed.getOrDefault(pos.asLong(), Collections.emptySet());
}

public long getThroughputUsed(BlockPos pos) {
updateTick();
return throughputUsed.getOrDefault(pos.asLong(), 0);
}

public void useChannel(BlockPos pos, Fluid fluid) {
updateTick();
channelUsed.computeIfAbsent(pos.asLong(), p -> new HashSet<>()).add(fluid);
}

public void useThroughput(BlockPos pos, long filled) {
updateTick();
throughputUsed.put(pos.asLong(), throughputUsed.getOrDefault(pos.asLong(), 0) + filled);
public record Snapshot(Long2ObjectMap<Fluid[]> channelFluids, Long2ObjectMap<ThroughputUsage[]> throughputUsage) {
}

public record Snapshot(Long2ObjectMap<Set<Fluid>> channelUsed, Long2LongMap throughputUsed) {
public Snapshot createSnapshot() {
Long2ObjectMap<Fluid[]> channelUsedCopied = new Long2ObjectOpenHashMap<>();
channelFluidsByBlock.forEach((k, v) -> channelUsedCopied.put(k.longValue(), Arrays.copyOf(v, v.length)));

}
Long2ObjectOpenHashMap<ThroughputUsage[]> throughputUsedCopied = new Long2ObjectOpenHashMap<>();
throughputUsagesByBlock.forEach((k, v) -> throughputUsedCopied.put(k.longValue(), Arrays.stream(v)
.map(ThroughputUsage::new)
.toArray(ThroughputUsage[]::new)
));

public Snapshot createSnapeShot() {
Long2ObjectMap<Set<Fluid>> channelUsedCopied = new Long2ObjectOpenHashMap<>();
channelUsed.forEach((k, v) -> channelUsedCopied.computeIfAbsent(k, key -> new HashSet<>(v)).addAll(v));
Long2LongMap throughputUsedCopied = new Long2LongOpenHashMap();
throughputUsedCopied.putAll(throughputUsed);
return new Snapshot(channelUsedCopied, throughputUsedCopied);
}

public void resetData(Snapshot snapshot) {
channelUsed.clear();
throughputUsed.clear();
snapshot.channelUsed.forEach((k, v) -> channelUsed.computeIfAbsent(k, key -> new HashSet<>(v)).addAll(v));
throughputUsed.putAll(snapshot.throughputUsed);
channelFluidsByBlock.clear();
channelFluidsByBlock.putAll(snapshot.channelFluids);

throughputUsagesByBlock.clear();
throughputUsagesByBlock.putAll(snapshot.throughputUsage);
}
}
Loading

0 comments on commit 0591fc5

Please sign in to comment.