package buildcraft.transport;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.collect.EnumMultiset;
import com.google.common.collect.Multiset;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraftforge.common.util.ForgeDirection;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidEvent;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.FluidTankInfo;
import net.minecraftforge.fluids.IFluidHandler;
import buildcraft.BuildCraftCore;
import buildcraft.BuildCraftTransport;
import buildcraft.api.core.SafeTimeTracker;
import buildcraft.api.tiles.IDebuggable;
import buildcraft.api.transport.IPipeTile;
import buildcraft.core.DefaultProps;
import buildcraft.core.lib.utils.MathUtils;
import buildcraft.transport.network.PacketFluidUpdate;
import buildcraft.transport.pipes.PipeFluidsClay;
import buildcraft.transport.pipes.PipeFluidsCobblestone;
import buildcraft.transport.pipes.PipeFluidsDiamond;
import buildcraft.transport.pipes.PipeFluidsEmerald;
import buildcraft.transport.pipes.PipeFluidsGold;
import buildcraft.transport.pipes.PipeFluidsIron;
import buildcraft.transport.pipes.PipeFluidsQuartz;
import buildcraft.transport.pipes.PipeFluidsSandstone;
import buildcraft.transport.pipes.PipeFluidsStone;
import buildcraft.transport.pipes.PipeFluidsVoid;
import buildcraft.transport.pipes.PipeFluidsWood;
import buildcraft.transport.pipes.events.PipeEventFluid;
import buildcraft.transport.utils.FluidRenderData;
public class PipeTransportFluids extends PipeTransport implements IFluidHandler, IDebuggable {
public static final Map<Class<? extends Pipe<?>>, Integer> fluidCapacities = new HashMap<Class<? extends Pipe<?>>, Integer>();
/**
* The amount of liquid contained by a pipe section. For simplicity, all
* pipe sections are assumed to be of the same volume.
*/
public static int MAX_TRAVEL_DELAY = 12;
public static short INPUT_TTL = 60; // 100
public static short OUTPUT_TTL = 80; // 80
public static short OUTPUT_COOLDOWN = 30; // 30
private static int NETWORK_SYNC_TICKS = BuildCraftCore.updateFactor / 2;
private static final ForgeDirection[] directions = ForgeDirection.VALID_DIRECTIONS;
private static final ForgeDirection[] orientations = ForgeDirection.values();
public class PipeSection {
public int amount;
private short currentTime = 0;
private short[] incoming = new short[MAX_TRAVEL_DELAY];
public int fill(int maxFill, boolean doFill) {
int amountToFill = Math.min(getMaxFillRate(), maxFill);
if (amountToFill <= 0) {
return 0;
}
if (doFill) {
incoming[currentTime] += amountToFill;
amount += amountToFill;
}
return amountToFill;
}
public int drain(int maxDrain, boolean doDrain) {
int maxToDrain = getAvailable();
if (maxToDrain > maxDrain) {
maxToDrain = maxDrain;
}
if (maxToDrain > flowRate) {
maxToDrain = flowRate;
}
if (maxToDrain <= 0) {
return 0;
} else {
if (doDrain) {
amount -= maxToDrain;
}
return maxToDrain;
}
}
public void moveFluids() {
incoming[currentTime] = 0;
}
public void setTime(short newTime) {
currentTime = newTime;
}
public void reset() {
this.amount = 0;
incoming = new short[MAX_TRAVEL_DELAY];
}
/**
* Get the amount of fluid available to move. This nicely takes care
* of the travel delay mechanic.
*
* @return
*/
public int getAvailable() {
int all = amount;
for (short slot : incoming) {
all -= slot;
}
return all;
}
public int getMaxFillRate() {
return Math.min(getCapacity() - amount, flowRate - incoming[currentTime]);
}
public void readFromNBT(NBTTagCompound compoundTag) {
this.amount = compoundTag.getShort("capacity");
for (int i = 0; i < travelDelay; ++i) {
incoming[i] = compoundTag.getShort("in[" + i + "]");
}
}
public void writeToNBT(NBTTagCompound subTag) {
subTag.setShort("capacity", (short) amount);
for (int i = 0; i < travelDelay; ++i) {
subTag.setShort("in[" + i + "]", incoming[i]);
}
}
}
public PipeSection[] sections = new PipeSection[7];
public FluidStack fluidType;
public FluidRenderData renderCache = new FluidRenderData();
private final SafeTimeTracker networkSyncTracker = new SafeTimeTracker(NETWORK_SYNC_TICKS);
private final TransferState[] transferState = new TransferState[directions.length];
private final int[] inputPerTick = new int[directions.length];
private final short[] inputTTL = new short[]{0, 0, 0, 0, 0, 0};
private final short[] outputTTL = new short[]{OUTPUT_TTL, OUTPUT_TTL, OUTPUT_TTL, OUTPUT_TTL, OUTPUT_TTL, OUTPUT_TTL};
private final short[] outputCooldown = new short[]{0, 0, 0, 0, 0, 0};
private final boolean[] canReceiveCache = new boolean[6];
private int clientSyncCounter = 0;
private int capacity, flowRate;
private int travelDelay = MAX_TRAVEL_DELAY;
public enum TransferState {
None, Input, Output
}
public PipeTransportFluids() {
for (ForgeDirection direction : directions) {
sections[direction.ordinal()] = new PipeSection();
transferState[direction.ordinal()] = TransferState.None;
}
sections[6] = new PipeSection();
}
/**
* This value has to be the same on client and server!
*
* @return
*/
public int getCapacity() {
return capacity;
}
public int getFlowRate() {
return flowRate;
}
public void initFromPipe(Class<? extends Pipe<?>> pipeClass) {
capacity = 25 * Math.min(1000, BuildCraftTransport.pipeFluidsBaseFlowRate);
flowRate = fluidCapacities.get(pipeClass);
travelDelay = MathUtils.clamp(Math.round(16F / (flowRate / BuildCraftTransport.pipeFluidsBaseFlowRate)), 1, MAX_TRAVEL_DELAY);
}
@Override
public void initialize() {
super.initialize();
for (ForgeDirection d : directions) {
canReceiveCache[d.ordinal()] = canReceiveFluid(d);
}
}
@Override
public IPipeTile.PipeType getPipeType() {
return IPipeTile.PipeType.FLUID;
}
private boolean canReceiveFluid(ForgeDirection o) {
TileEntity tile = container.getTile(o);
if (!container.isPipeConnected(o)) {
return false;
}
if (tile instanceof IPipeTile) {
Pipe<?> pipe = (Pipe<?>) ((IPipeTile) tile).getPipe();
if (pipe == null || !inputOpen(o.getOpposite())) {
return false;
}
}
if (tile instanceof IFluidHandler) {
return true;
}
return false;
}
@Override
public void updateEntity() {
if (container.getWorldObj().isRemote) {
return;
}
moveFluids();
if (networkSyncTracker.markTimeIfDelay(container.getWorldObj())) {
boolean init = false;
if (++clientSyncCounter > BuildCraftCore.longUpdateFactor * 2) {
clientSyncCounter = 0;
init = true;
}
PacketFluidUpdate packet = computeFluidUpdate(init, true);
if (packet != null) {
BuildCraftTransport.instance.sendToPlayers(packet, container.getWorldObj(), container.xCoord, container.yCoord, container.zCoord, DefaultProps.PIPE_CONTENTS_RENDER_DIST);
}
}
}
private void moveFluids() {
if (fluidType != null) {
short newTimeSlot = (short) (container.getWorldObj().getTotalWorldTime() % travelDelay);
int outputCount = computeCurrentConnectionStatesAndTickFlows(newTimeSlot > 0 && newTimeSlot < travelDelay ? newTimeSlot : 0);
if (fluidType != null) {
moveFromPipe(outputCount);
moveFromCenter();
moveToCenter();
}
} else {
computeTTLs();
}
}
private void moveFromPipe(int outputCount) {
// Move liquid from the non-center to the connected output blocks
if (outputCount > 0) {
for (ForgeDirection o : directions) {
if (transferState[o.ordinal()] == TransferState.Output) {
TileEntity target = this.container.getTile(o);
if (!(target instanceof IFluidHandler)) {
continue;
}
PipeSection section = sections[o.ordinal()];
FluidStack liquidToPush = new FluidStack(fluidType, section.drain(flowRate, false));
if (liquidToPush.amount > 0) {
int filled = ((IFluidHandler) target).fill(o.getOpposite(), liquidToPush, true);
if (filled <= 0) {
outputTTL[o.ordinal()]--;
} else {
section.drain(filled, true);
}
}
}
}
}
}
private void moveFromCenter() {
// Split liquids moving to output equally based on flowrate, how much each side can accept and available liquid
int pushAmount = sections[6].amount;
int totalAvailable = sections[6].getAvailable();
if (totalAvailable < 1 || pushAmount < 1) {
return;
}
int testAmount = flowRate;
// Move liquid from the center to the output sides
Multiset<ForgeDirection> realDirections = EnumMultiset.create(ForgeDirection.class);
for (ForgeDirection direction : directions) {
if (transferState[direction.ordinal()] == TransferState.Output) {
realDirections.add(direction);
}
}
if (realDirections.size() > 0) {
container.pipe.eventBus.handleEvent(PipeEventFluid.FindDest.class, new PipeEventFluid.FindDest(container.pipe, new FluidStack(fluidType, pushAmount), realDirections));
float min = Math.min(flowRate * realDirections.size(), totalAvailable) / (float) flowRate / realDirections.size();
for (ForgeDirection direction : realDirections.elementSet()) {
int available = sections[direction.ordinal()].fill(testAmount, false);
int amountToPush = (int) (available * min * realDirections.count(direction));
if (amountToPush < 1) {
amountToPush++;
}
amountToPush = sections[6].drain(amountToPush, false);
if (amountToPush > 0) {
int filled = sections[direction.ordinal()].fill(amountToPush, true);
sections[6].drain(filled, true);
}
}
}
}
private void moveToCenter() {
int transferInCount = 0;
int spaceAvailable = getCapacity() - sections[6].amount;
for (ForgeDirection dir : directions) {
inputPerTick[dir.ordinal()] = 0;
if (transferState[dir.ordinal()] != TransferState.Output) {
inputPerTick[dir.ordinal()] = sections[dir.ordinal()].drain(flowRate, false);
if (inputPerTick[dir.ordinal()] > 0) {
transferInCount++;
}
}
}
float min = Math.min(flowRate * transferInCount, spaceAvailable) / (float) flowRate / transferInCount;
for (ForgeDirection dir : directions) {
// Move liquid from input sides to the center
if (inputPerTick[dir.ordinal()] > 0) {
int amountToDrain = (int) (inputPerTick[dir.ordinal()] * min);
if (amountToDrain < 1) {
amountToDrain++;
}
int amountToPush = sections[dir.ordinal()].drain(amountToDrain, false);
if (amountToPush > 0) {
int filled = sections[6].fill(amountToPush, true);
sections[dir.ordinal()].drain(filled, true);
}
}
}
}
private void computeTTLs() {
for (int i = 0; i < 6; i++) {
if (transferState[i] == TransferState.Input) {
if (inputTTL[i] > 0) {
inputTTL[i]--;
} else {
transferState[i] = TransferState.None;
}
}
if (outputCooldown[i] > 0) {
outputCooldown[i]--;
}
}
}
private int computeCurrentConnectionStatesAndTickFlows(short newTimeSlot) {
int outputCount = 0;
int fluidAmount = 0;
// Processes all internal tanks
for (ForgeDirection direction : orientations) {
int dirI = direction.ordinal();
PipeSection section = sections[dirI];
fluidAmount += section.amount;
section.setTime(newTimeSlot);
section.moveFluids();
// Input processing
if (direction == ForgeDirection.UNKNOWN) {
continue;
}
if (transferState[dirI] == TransferState.Input) {
inputTTL[dirI]--;
if (inputTTL[dirI] <= 0) {
transferState[dirI] = TransferState.None;
}
continue;
}
if (!container.pipe.outputOpen(direction)) {
transferState[dirI] = TransferState.None;
continue;
}
if (outputCooldown[dirI] > 0) {
outputCooldown[dirI]--;
continue;
}
if (outputTTL[dirI] <= 0) {
transferState[dirI] = TransferState.None;
outputCooldown[dirI] = OUTPUT_COOLDOWN;
outputTTL[dirI] = OUTPUT_TTL;
continue;
}
if (canReceiveCache[dirI] && container.pipe.outputOpen(direction)) {
transferState[dirI] = TransferState.Output;
outputCount++;
}
}
if (fluidAmount == 0) {
setFluidType(null);
}
return outputCount;
}
/**
* Computes the PacketFluidUpdate packet for transmission to a client
*
* @param initPacket everything is sent, no delta stuff ( first packet )
* @param persistChange The render cache change is persisted
* @return PacketFluidUpdate liquid update packet
*/
private PacketFluidUpdate computeFluidUpdate(boolean initPacket, boolean persistChange) {
boolean changed = false;
BitSet delta = new BitSet(8);
FluidRenderData renderCacheCopy = this.renderCache;
if (initPacket || (fluidType == null && renderCacheCopy.fluidID != 0)
|| (fluidType != null && renderCacheCopy.fluidID != fluidType.getFluid().getID())) {
changed = true;
renderCache.fluidID = fluidType != null ? fluidType.getFluid().getID() : 0;
renderCache.color = fluidType != null ? fluidType.getFluid().getColor(fluidType) : 0;
renderCache.flags = FluidRenderData.getFlags(fluidType);
delta.set(0);
}
for (ForgeDirection dir : orientations) {
int pamount = renderCache.amount[dir.ordinal()];
int camount = sections[dir.ordinal()].amount;
int displayQty = (pamount * 4 + camount) / 5;
if (displayQty == 0 && camount > 0 || initPacket) {
displayQty = camount;
}
displayQty = Math.min(getCapacity(), displayQty);
if (pamount != displayQty || initPacket) {
changed = true;
renderCache.amount[dir.ordinal()] = displayQty;
delta.set(dir.ordinal() + 1);
}
}
if (persistChange) {
this.renderCache = renderCacheCopy;
}
if (changed || initPacket) {
PacketFluidUpdate packet = new PacketFluidUpdate(container.xCoord, container.yCoord, container.zCoord, initPacket, getCapacity() > 255);
packet.renderCache = renderCacheCopy;
packet.delta = delta;
return packet;
}
return null;
}
private void setFluidType(FluidStack type) {
fluidType = type;
}
/**
* Initializes client
*/
@Override
public void sendDescriptionPacket() {
super.sendDescriptionPacket();
PacketFluidUpdate update = computeFluidUpdate(true, true);
BuildCraftTransport.instance.sendToPlayers(update, container.getWorldObj(), container.xCoord, container.yCoord, container.zCoord, DefaultProps.PIPE_CONTENTS_RENDER_DIST);
}
public FluidStack getStack(ForgeDirection direction) {
if (fluidType == null) {
return null;
} else {
return new FluidStack(fluidType, sections[direction.ordinal()].amount);
}
}
@Override
public void dropContents() {
if (fluidType != null) {
int totalAmount = 0;
for (int i = 0; i < 7; i++) {
totalAmount += sections[i].amount;
}
if (totalAmount > 0) {
FluidEvent.fireEvent(new FluidEvent.FluidSpilledEvent(
new FluidStack(fluidType, totalAmount),
getWorld(), container.xCoord, container.yCoord, container.zCoord
));
}
}
}
@Override
public void readFromNBT(NBTTagCompound nbttagcompound) {
super.readFromNBT(nbttagcompound);
if (nbttagcompound.hasKey("fluid")) {
setFluidType(FluidStack.loadFluidStackFromNBT(nbttagcompound.getCompoundTag("fluid")));
} else {
setFluidType(null);
}
for (ForgeDirection direction : orientations) {
if (nbttagcompound.hasKey("tank[" + direction.ordinal() + "]")) {
NBTTagCompound compound = nbttagcompound.getCompoundTag("tank[" + direction.ordinal() + "]");
if (compound.hasKey("FluidType")) {
FluidStack stack = FluidStack.loadFluidStackFromNBT(compound);
if (fluidType == null) {
setFluidType(stack);
}
if (stack.isFluidEqual(fluidType)) {
sections[direction.ordinal()].readFromNBT(compound);
}
} else {
sections[direction.ordinal()].readFromNBT(compound);
}
}
if (direction != ForgeDirection.UNKNOWN) {
transferState[direction.ordinal()] = TransferState.values()[nbttagcompound.getShort("transferState[" + direction.ordinal() + "]")];
}
}
}
@Override
public void writeToNBT(NBTTagCompound nbttagcompound) {
super.writeToNBT(nbttagcompound);
if (fluidType != null) {
NBTTagCompound fluidTag = new NBTTagCompound();
fluidType.writeToNBT(fluidTag);
nbttagcompound.setTag("fluid", fluidTag);
for (ForgeDirection direction : orientations) {
NBTTagCompound subTag = new NBTTagCompound();
sections[direction.ordinal()].writeToNBT(subTag);
nbttagcompound.setTag("tank[" + direction.ordinal() + "]", subTag);
if (direction != ForgeDirection.UNKNOWN) {
nbttagcompound.setShort("transferState[" + direction.ordinal() + "]", (short) transferState[direction.ordinal()].ordinal());
}
}
}
}
@Override
public int fill(ForgeDirection from, FluidStack resource, boolean doFill) {
if (from != ForgeDirection.UNKNOWN && !inputOpen(from)) {
return 0;
}
if (resource == null || (fluidType != null && !resource.isFluidEqual(fluidType))) {
return 0;
}
int filled;
if (this.container.pipe instanceof IPipeTransportFluidsHook) {
filled = ((IPipeTransportFluidsHook) this.container.pipe).fill(from, resource, doFill);
} else {
filled = sections[from.ordinal()].fill(resource.amount, doFill);
}
if (doFill && filled > 0) {
if (fluidType == null) {
setFluidType(new FluidStack(resource, 0));
}
if (from != ForgeDirection.UNKNOWN) {
transferState[from.ordinal()] = TransferState.Input;
inputTTL[from.ordinal()] = INPUT_TTL;
}
}
return filled;
}
@Override
public FluidStack drain(ForgeDirection from, FluidStack resource, boolean doDrain) {
return null;
}
@Override
public FluidStack drain(ForgeDirection from, int maxDrain, boolean doDrain) {
return null;
}
@Override
public boolean canFill(ForgeDirection from, Fluid fluid) {
return inputOpen(from);
}
@Override
public boolean canDrain(ForgeDirection from, Fluid fluid) {
return false;
}
@Override
public FluidTankInfo[] getTankInfo(ForgeDirection from) {
return new FluidTankInfo[]{new FluidTankInfo(fluidType, sections[from.ordinal()].amount)};
}
@Override
public void onNeighborChange(ForgeDirection direction) {
super.onNeighborChange(direction);
if (!container.isPipeConnected(direction)) {
sections[direction.ordinal()].reset();
transferState[direction.ordinal()] = TransferState.None;
renderCache.amount[direction.ordinal()] = 0;
canReceiveCache[direction.ordinal()] = false;
} else {
canReceiveCache[direction.ordinal()] = canReceiveFluid(direction);
}
}
@Override
public boolean canPipeConnect(TileEntity tile, ForgeDirection side) {
if (tile instanceof IPipeTile) {
Pipe<?> pipe2 = (Pipe<?>) ((IPipeTile) tile).getPipe();
if (BlockGenericPipe.isValid(pipe2) && !(pipe2.transport instanceof PipeTransportFluids)) {
return false;
}
}
if (tile instanceof IFluidHandler) {
return true;
}
return tile instanceof IPipeTile;
}
@Override
public void getDebugInfo(List<String> info, ForgeDirection side, ItemStack debugger, EntityPlayer player) {
int[] amount = new int[7];
for (int i = 0; i < 7; i++) {
if (sections[i] != null) {
amount[i] = sections[i].amount;
}
}
info.add(String.format("PipeTransportFluids (%s, %d mB, %d mB/t)", fluidType != null ? fluidType.getLocalizedName() : "Empty", capacity, flowRate));
info.add("- Stored: " + Arrays.toString(amount));
}
static {
fluidCapacities.put(PipeFluidsVoid.class, 1 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsWood.class, 1 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsCobblestone.class, 1 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsSandstone.class, 2 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsStone.class, 2 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsClay.class, 4 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsEmerald.class, 4 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsIron.class, 4 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsQuartz.class, 4 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsDiamond.class, 8 * BuildCraftTransport.pipeFluidsBaseFlowRate);
fluidCapacities.put(PipeFluidsGold.class, 8 * BuildCraftTransport.pipeFluidsBaseFlowRate);
}
}