package erogenousbeef.bigreactors.common.multiblock;
import io.netty.buffer.ByteBuf;
import java.util.HashSet;
import java.util.Set;
import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import net.minecraftforge.fluids.Fluid;
import net.minecraftforge.fluids.FluidRegistry;
import net.minecraftforge.fluids.FluidStack;
import net.minecraftforge.fluids.FluidTank;
import net.minecraftforge.fluids.FluidTankInfo;
import cofh.api.energy.IEnergyHandler;
import cofh.lib.util.helpers.ItemHelper;
import cpw.mods.fml.common.network.simpleimpl.IMessage;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import erogenousbeef.bigreactors.api.data.CoilPartData;
import erogenousbeef.bigreactors.api.registry.TurbineCoil;
import erogenousbeef.bigreactors.common.BRLog;
import erogenousbeef.bigreactors.common.BigReactors;
import erogenousbeef.bigreactors.common.block.BlockBRMetal;
import erogenousbeef.bigreactors.common.interfaces.IMultipleFluidHandler;
import erogenousbeef.bigreactors.common.multiblock.block.BlockTurbineRotorPart;
import erogenousbeef.bigreactors.common.multiblock.helpers.FloatUpdateTracker;
import erogenousbeef.bigreactors.common.multiblock.interfaces.IActivateable;
import erogenousbeef.bigreactors.common.multiblock.interfaces.ITickableMultiblockPart;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbinePartBase;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbinePartGlass;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbinePowerTap;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbineRotorBearing;
import erogenousbeef.bigreactors.common.multiblock.tileentity.TileEntityTurbineRotorPart;
import erogenousbeef.bigreactors.gui.container.ISlotlessUpdater;
import erogenousbeef.bigreactors.net.CommonPacketHandler;
import erogenousbeef.bigreactors.net.message.multiblock.TurbineUpdateMessage;
import erogenousbeef.bigreactors.utils.StaticUtils;
import erogenousbeef.core.common.CoordTriplet;
import erogenousbeef.core.multiblock.IMultiblockPart;
import erogenousbeef.core.multiblock.MultiblockControllerBase;
import erogenousbeef.core.multiblock.MultiblockValidationException;
import erogenousbeef.core.multiblock.rectangular.RectangularMultiblockControllerBase;
public class MultiblockTurbine extends RectangularMultiblockControllerBase implements IEnergyHandler, IMultipleFluidHandler, ISlotlessUpdater, IActivateable {
public enum VentStatus {
VentOverflow,
VentAll,
DoNotVent
}
public static final VentStatus[] s_VentStatuses = VentStatus.values();
// UI updates
private Set<EntityPlayer> updatePlayers;
private int ticksSinceLastUpdate;
private static final int ticksBetweenUpdates = 3;
// Fluid tanks. Input = Steam, Output = Water.
public static final int TANK_INPUT = 0;
public static final int TANK_OUTPUT = 1;
public static final int NUM_TANKS = 2;
public static final int FLUID_NONE = -1;
public static final int TANK_SIZE = 4000;
public static final int MAX_PERMITTED_FLOW = 2000;
private FluidTank[] tanks;
static final float maxEnergyStored = 1000000f; // 1 MegaRF
// Persistent game data
float energyStored;
boolean active;
float rotorEnergy;
boolean inductorEngaged;
// Player settings
VentStatus ventStatus;
int maxIntakeRate;
// Derivable game data
int bladeSurfaceArea; // # of blocks that are blades
int rotorMass; // 10 = 1 standard block-weight
int coilSize; // number of blocks in the coils
// Inductor dynamic constants - get from a table on assembly
float inductorDragCoefficient = inductorBaseDragCoefficient;
float inductionEfficiency = 0.5f; // Final energy rectification efficiency. Averaged based on coil material and shape. 0.25-0.5 = iron, 0.75-0.9 = diamond, 1 = perfect.
float inductionEnergyExponentBonus = 1f; // Exponential bonus to energy generation. Use this for very rare materials or special constructs.
// Rotor dynamic constants - calculate on assembly
float rotorDragCoefficient = 0.01f; // RF/t lost to friction per unit of mass in the rotor.
float bladeDrag = 0.00025f; // RF/t lost to friction, multiplied by rotor speed squared.
float frictionalDrag = 0f;
// Penalize suboptimal shapes with worse drag (i.e. increased drag without increasing lift)
// Suboptimal is defined as "not a christmas-tree shape". At worst, drag is increased 4x.
// Game balance constants - some of these are modified by configs at startup
public static int inputFluidPerBlade = 25; // mB
public static float inductorBaseDragCoefficient = 0.1f; // RF/t extracted per coil block, multiplied by rotor speed squared.
public static final float baseBladeDragCoefficient = 0.00025f; // RF/t base lost to aero drag per blade block. Includes a 50% reduction to factor in constant parts of the drag equation
float energyGeneratedLastTick;
int fluidConsumedLastTick;
float rotorEfficiencyLastTick;
private Set<IMultiblockPart> attachedControllers;
private Set<TileEntityTurbineRotorBearing> attachedRotorBearings;
private Set<TileEntityTurbinePowerTap> attachedPowerTaps;
private Set<ITickableMultiblockPart> attachedTickables;
private Set<TileEntityTurbineRotorPart> attachedRotorShafts;
private Set<TileEntityTurbineRotorPart> attachedRotorBlades;
private Set<TileEntityTurbinePartGlass> attachedGlass;
// Data caches for validation
private Set<CoordTriplet> foundCoils;
private FloatUpdateTracker rpmUpdateTracker;
private static final ForgeDirection[] RotorXBladeDirections = new ForgeDirection[] { ForgeDirection.UP, ForgeDirection.SOUTH, ForgeDirection.DOWN, ForgeDirection.NORTH };
private static final ForgeDirection[] RotorZBladeDirections = new ForgeDirection[] { ForgeDirection.UP, ForgeDirection.EAST, ForgeDirection.DOWN, ForgeDirection.WEST };
public MultiblockTurbine(World world) {
super(world);
updatePlayers = new HashSet<EntityPlayer>();
ticksSinceLastUpdate = 0;
tanks = new FluidTank[NUM_TANKS];
for(int i = 0; i < NUM_TANKS; i++)
tanks[i] = new FluidTank(TANK_SIZE);
attachedControllers = new HashSet<IMultiblockPart>();
attachedRotorBearings = new HashSet<TileEntityTurbineRotorBearing>();
attachedPowerTaps = new HashSet<TileEntityTurbinePowerTap>();
attachedTickables = new HashSet<ITickableMultiblockPart>();
attachedRotorShafts = new HashSet<TileEntityTurbineRotorPart>();
attachedRotorBlades = new HashSet<TileEntityTurbineRotorPart>();
attachedGlass = new HashSet<TileEntityTurbinePartGlass>();
energyStored = 0f;
active = false;
inductorEngaged = true;
ventStatus = VentStatus.VentOverflow;
rotorEnergy = 0f;
maxIntakeRate = MAX_PERMITTED_FLOW;
bladeSurfaceArea = 0;
rotorMass = 0;
coilSize = 0;
energyGeneratedLastTick = 0f;
fluidConsumedLastTick = 0;
rotorEfficiencyLastTick = 1f;
foundCoils = new HashSet<CoordTriplet>();
rpmUpdateTracker = new FloatUpdateTracker(100, 5, 10f, 100f); // Minimum 10RPM difference for slow updates, if change > 100 RPM, update every 5 ticks
}
/**
* Sends a full state update to a player.
*/
protected void sendIndividualUpdate(EntityPlayer player) {
if(this.worldObj.isRemote) { return; }
CommonPacketHandler.INSTANCE.sendTo(getUpdatePacket(), (EntityPlayerMP)player);
}
protected IMessage getUpdatePacket() {
return new TurbineUpdateMessage(this);
}
/**
* Send an update to any clients with GUIs open
*/
protected void sendTickUpdate() {
if(this.updatePlayers.size() <= 0) { return; }
for(EntityPlayer player : updatePlayers) {
CommonPacketHandler.INSTANCE.sendTo(getUpdatePacket(), (EntityPlayerMP)player);
}
}
// MultiblockControllerBase overrides
@Override
public void onAttachedPartWithMultiblockData(IMultiblockPart part, NBTTagCompound data) {
readFromNBT(data);
}
@Override
protected void onBlockAdded(IMultiblockPart newPart) {
if(newPart instanceof TileEntityTurbineRotorBearing) {
this.attachedRotorBearings.add((TileEntityTurbineRotorBearing)newPart);
}
if(newPart instanceof TileEntityTurbinePowerTap) {
attachedPowerTaps.add((TileEntityTurbinePowerTap)newPart);
}
if(newPart instanceof ITickableMultiblockPart) {
attachedTickables.add((ITickableMultiblockPart)newPart);
}
if(newPart instanceof TileEntityTurbineRotorPart) {
TileEntityTurbineRotorPart turbinePart = (TileEntityTurbineRotorPart)newPart;
if(turbinePart.isRotorShaft()) {
attachedRotorShafts.add(turbinePart);
}
if(turbinePart.isRotorBlade()) {
attachedRotorBlades.add(turbinePart);
}
}
if(newPart instanceof TileEntityTurbinePartGlass) {
attachedGlass.add((TileEntityTurbinePartGlass)newPart);
}
}
@Override
protected void onBlockRemoved(IMultiblockPart oldPart) {
if(oldPart instanceof TileEntityTurbineRotorBearing) {
this.attachedRotorBearings.remove(oldPart);
}
if(oldPart instanceof TileEntityTurbinePowerTap) {
attachedPowerTaps.remove((TileEntityTurbinePowerTap)oldPart);
}
if(oldPart instanceof ITickableMultiblockPart) {
attachedTickables.remove((ITickableMultiblockPart)oldPart);
}
if(oldPart instanceof TileEntityTurbineRotorPart) {
TileEntityTurbineRotorPart turbinePart = (TileEntityTurbineRotorPart)oldPart;
if(turbinePart.isRotorShaft()) {
attachedRotorShafts.remove(turbinePart);
}
if(turbinePart.isRotorBlade()) {
attachedRotorBlades.remove(turbinePart);
}
}
if(oldPart instanceof TileEntityTurbinePartGlass) {
attachedGlass.remove((TileEntityTurbinePartGlass)oldPart);
}
}
@Override
protected void onMachineAssembled() {
recalculateDerivedStatistics();
}
@Override
protected void onMachineRestored() {
recalculateDerivedStatistics();
}
@Override
protected void onMachinePaused() {
}
@Override
protected void onMachineDisassembled() {
rotorMass = 0;
bladeSurfaceArea = 0;
coilSize = 0;
rotorEnergy = 0f; // Kill energy when machines get broken by players/explosions
rpmUpdateTracker.setValue(0f);
}
// Validation code
@Override
protected void isMachineWhole() throws MultiblockValidationException {
if(attachedRotorBearings.size() != 1) {
throw new MultiblockValidationException("Turbines require exactly 1 rotor bearing");
}
// Set up validation caches
foundCoils.clear();
super.isMachineWhole();
// Now do additional validation based on the coils/blades/rotors that were found
// Check that we have a rotor that goes all the way up the bearing
TileEntityTurbinePartBase rotorPart = attachedRotorBearings.iterator().next();
// Rotor bearing must calculate outwards dir, as this is normally only calculated in onMachineAssembled().
rotorPart.recalculateOutwardsDirection(getMinimumCoord(), getMaximumCoord());
// Find out which way the rotor runs. Obv, this is inwards from the bearing.
ForgeDirection rotorDir = rotorPart.getOutwardsDir().getOpposite();
CoordTriplet rotorCoord = rotorPart.getWorldLocation();
CoordTriplet minRotorCoord = getMinimumCoord();
CoordTriplet maxRotorCoord = getMaximumCoord();
// Constrain min/max rotor coords to where the rotor bearing is and the block opposite it
if(rotorDir.offsetX == 0) {
minRotorCoord.x = maxRotorCoord.x = rotorCoord.x;
}
if(rotorDir.offsetY == 0) {
minRotorCoord.y = maxRotorCoord.y = rotorCoord.y;
}
if(rotorDir.offsetZ == 0) {
minRotorCoord.z = maxRotorCoord.z = rotorCoord.z;
}
// Figure out where the rotor ends and which directions are normal to the rotor's 4 faces (this is where blades emit from)
CoordTriplet endRotorCoord = rotorCoord.equals(minRotorCoord) ? maxRotorCoord : minRotorCoord;
endRotorCoord.translate(rotorDir.getOpposite());
ForgeDirection[] bladeDirections;
if(rotorDir.offsetY != 0) {
bladeDirections = StaticUtils.CardinalDirections;
}
else if(rotorDir.offsetX != 0) {
bladeDirections = RotorXBladeDirections;
}
else {
bladeDirections = RotorZBladeDirections;
}
Set<CoordTriplet> rotorShafts = new HashSet<CoordTriplet>(attachedRotorShafts.size());
Set<CoordTriplet> rotorBlades = new HashSet<CoordTriplet>(attachedRotorBlades.size());
for(TileEntityTurbineRotorPart part : attachedRotorShafts) {
rotorShafts.add(part.getWorldLocation());
}
for(TileEntityTurbineRotorPart part : attachedRotorBlades) {
rotorBlades.add(part.getWorldLocation());
}
// Move along the length of the rotor, 1 block at a time
boolean encounteredCoils = false;
while(!rotorShafts.isEmpty() && !rotorCoord.equals(endRotorCoord)) {
rotorCoord.translate(rotorDir);
// Ensure we find a rotor block along the length of the entire rotor
if(!rotorShafts.remove(rotorCoord)) {
throw new MultiblockValidationException(String.format("%s - This block must contain a rotor. The rotor must begin at the bearing and run the entire length of the turbine", rotorCoord));
}
// Now move out in the 4 rotor normals, looking for blades and coils
CoordTriplet checkCoord = rotorCoord.copy();
boolean encounteredBlades = false;
for(ForgeDirection bladeDir : bladeDirections) {
checkCoord.copy(rotorCoord);
boolean foundABlade = false;
checkCoord.translate(bladeDir);
// If we find 1 blade, we can keep moving along the normal to find more blades
while(rotorBlades.remove(checkCoord)) {
// We found a coil already?! NOT ALLOWED.
if(encounteredCoils) {
throw new MultiblockValidationException(String.format("%s - Rotor blades must be placed closer to the rotor bearing than all other parts inside a turbine", checkCoord));
}
foundABlade = encounteredBlades = true;
checkCoord.translate(bladeDir);
}
// If this block wasn't a blade, check to see if it was a coil
if(!foundABlade) {
if(foundCoils.remove(checkCoord)) {
encounteredCoils = true;
// We cannot have blades and coils intermix. This prevents intermixing, depending on eval order.
if(encounteredBlades) {
throw new MultiblockValidationException(String.format("%s - Metal blocks must by placed further from the rotor bearing than all rotor blades", checkCoord));
}
// Check the two coil spots in the 'corners', which are permitted if they're connected to the main rotor coil somehow
CoordTriplet coilCheck = checkCoord.copy();
coilCheck.translate(bladeDir.getRotation(rotorDir));
foundCoils.remove(coilCheck);
coilCheck.copy(checkCoord);
coilCheck.translate(bladeDir.getRotation(rotorDir.getOpposite()));
foundCoils.remove(coilCheck);
}
// Else: It must have been air.
}
}
}
if(!rotorCoord.equals(endRotorCoord)) {
throw new MultiblockValidationException("The rotor shaft must extend the entire length of the turbine interior.");
}
// Ensure that we encountered all the rotor, blade and coil blocks. If not, there's loose stuff inside the turbine.
if(!rotorShafts.isEmpty()) {
throw new MultiblockValidationException(String.format("Found %d rotor blocks that are not attached to the main rotor. All rotor blocks must be in a column extending the entire length of the turbine, starting from the bearing.", rotorShafts.size()));
}
if(!rotorBlades.isEmpty()) {
throw new MultiblockValidationException(String.format("Found %d rotor blades that are not attached to the rotor. All rotor blades must extend continuously from the rotor's shaft.", rotorBlades.size()));
}
if(!foundCoils.isEmpty()) {
throw new MultiblockValidationException(String.format("Found %d metal blocks which were not in a ring around the rotor. All metal blocks must be in rings, or partial rings, around the rotor.", foundCoils.size()));
}
// A-OK!
}
@Override
protected void isBlockGoodForInterior(World world, int x, int y, int z) throws MultiblockValidationException {
// We only allow air and functional parts in turbines.
// Air is ok
if(world.isAirBlock(x, y, z)) { return; }
Block block = world.getBlock(x, y, z);
int metadata = world.getBlockMetadata(x,y,z);
// Coil windings below here:
if(getCoilPartData(x, y, z, block, metadata) != null) {
foundCoils.add(new CoordTriplet(x,y,z));
return;
}
// Everything else, gtfo
throw new MultiblockValidationException(String.format("%d, %d, %d is invalid for a turbine interior. Only rotor parts, metal blocks and empty space are allowed.", x, y, z));
}
@Override
protected int getMinimumNumberOfBlocksForAssembledMachine() {
// Hollow 5x5x4 cube (100 - 18), interior minimum is 3x3x2
return 82;
}
@Override
protected int getMaximumXSize() {
return BigReactors.maximumTurbineSize;
}
@Override
protected int getMaximumZSize() {
return BigReactors.maximumTurbineSize;
}
@Override
protected int getMaximumYSize() {
return BigReactors.maximumTurbineHeight;
}
@Override
protected int getMinimumXSize() { return 5; }
@Override
protected int getMinimumYSize() { return 4; }
@Override
protected int getMinimumZSize() { return 5; }
@Override
protected void onAssimilate(MultiblockControllerBase otherMachine) {
if(!(otherMachine instanceof MultiblockTurbine)) {
BRLog.warning("[%s] Turbine @ %s is attempting to assimilate a non-Turbine machine! That machine's data will be lost!", worldObj.isRemote?"CLIENT":"SERVER", getReferenceCoord());
return;
}
MultiblockTurbine otherTurbine = (MultiblockTurbine)otherMachine;
setRotorEnergy(Math.max(rotorEnergy, otherTurbine.rotorEnergy));
}
@Override
protected void onAssimilated(MultiblockControllerBase assimilator) {
attachedControllers.clear();
attachedRotorBearings.clear();
attachedTickables.clear();
attachedPowerTaps.clear();
}
@Override
protected boolean updateServer() {
energyGeneratedLastTick = 0f;
fluidConsumedLastTick = 0;
rotorEfficiencyLastTick = 1f;
// Generate energy based on steam
int steamIn = 0; // mB. Based on water, actually. Probably higher for steam. Measure it.
if(getActive()) {
// Spin up via steam inputs, convert some steam back into water.
// Use at most the user-configured max, or the amount in the tank, whichever is less.
steamIn = Math.min(maxIntakeRate, tanks[TANK_INPUT].getFluidAmount());
if(ventStatus == VentStatus.DoNotVent) {
// Cap steam used to available space, if not venting
int availableSpace = tanks[TANK_OUTPUT].getCapacity() - tanks[TANK_OUTPUT].getFluidAmount();
steamIn = Math.min(steamIn, availableSpace);
}
}
if(steamIn > 0 || rotorEnergy > 0) {
float rotorSpeed = getRotorSpeed();
// RFs lost to aerodynamic drag.
float aerodynamicDragTorque = (float)rotorSpeed * bladeDrag;
float liftTorque = 0f;
if(steamIn > 0) {
// TODO: Lookup fluid parameters from a table
float fluidEnergyDensity = 10f; // RF per mB
// Cap amount of steam we can fully extract energy from based on blade size
int steamToProcess = bladeSurfaceArea * inputFluidPerBlade;
steamToProcess = Math.min(steamToProcess, steamIn);
liftTorque = steamToProcess * fluidEnergyDensity;
// Did we have excess steam for our blade size?
if(steamToProcess < steamIn) {
// Extract some percentage of the remaining steam's energy, based on how many blades are missing
steamToProcess = steamIn - steamToProcess;
float bladeEfficiency = 1f;
int neededBlades = steamIn / inputFluidPerBlade; // round in the player's favor
int missingBlades = neededBlades - bladeSurfaceArea;
bladeEfficiency = 1f - (float)missingBlades / (float)neededBlades;
liftTorque += steamToProcess * fluidEnergyDensity * bladeEfficiency;
rotorEfficiencyLastTick = liftTorque / (steamIn * fluidEnergyDensity);
}
}
// Yay for derivation. We're assuming delta-Time is always 1, as we're always calculating for 1 tick.
// RFs available to coils
float inductionTorque = inductorEngaged ? rotorSpeed * inductorDragCoefficient * coilSize : 0f;
float energyToGenerate = (float)Math.pow(inductionTorque, inductionEnergyExponentBonus) * inductionEfficiency;
if(energyToGenerate > 0f) {
// Efficiency curve. Rotors are 50% less efficient when not near 900/1800 RPMs.
float efficiency = (float)(0.25*Math.cos(rotorSpeed/(45.5*Math.PI))) + 0.75f;
if(rotorSpeed < 500) {
efficiency = Math.min(0.5f, efficiency);
}
generateEnergy(energyToGenerate * efficiency);
}
rotorEnergy += liftTorque + -1f*inductionTorque + -1f*aerodynamicDragTorque + -1f*frictionalDrag;
if(rotorEnergy < 0f) { rotorEnergy = 0f; }
// And create some water
if(steamIn > 0) {
fluidConsumedLastTick = steamIn;
drain(TANK_INPUT, steamIn, true);
if(ventStatus != VentStatus.VentAll) {
Fluid effluent = FluidRegistry.WATER;
FluidStack effluentStack = new FluidStack(effluent, steamIn);
fill(TANK_OUTPUT, effluentStack, true);
}
}
}
int energyAvailable = (int)getEnergyStored();
int energyRemaining = energyAvailable;
if(energyStored > 0 && attachedPowerTaps.size() > 0) {
// First, try to distribute fairly
int splitEnergy = energyRemaining / attachedPowerTaps.size();
for(TileEntityTurbinePowerTap powerTap : attachedPowerTaps) {
if(energyRemaining <= 0) { break; }
if(powerTap == null || !powerTap.isConnected()) { continue; }
energyRemaining -= splitEnergy - powerTap.onProvidePower(splitEnergy);
}
// Next, just hose out whatever we can, if we have any left
if(energyRemaining > 0) {
for(TileEntityTurbinePowerTap powerTap : attachedPowerTaps) {
if(energyRemaining <= 0) { break; }
if(powerTap == null || !powerTap.isConnected()) { continue; }
energyRemaining = powerTap.onProvidePower(energyRemaining);
}
}
}
if(energyAvailable != energyRemaining) {
reduceStoredEnergy((energyAvailable - energyRemaining));
}
for(ITickableMultiblockPart part : attachedTickables) {
part.onMultiblockServerTick();
}
ticksSinceLastUpdate++;
if(ticksSinceLastUpdate >= ticksBetweenUpdates) {
sendTickUpdate();
ticksSinceLastUpdate = 0;
}
if(rpmUpdateTracker.shouldUpdate(getRotorSpeed())) {
markReferenceCoordDirty();
}
return energyGeneratedLastTick > 0 || fluidConsumedLastTick > 0;
}
@Override
protected void updateClient() {
}
@Override
public void writeToNBT(NBTTagCompound data) {
data.setTag("inputTank", tanks[TANK_INPUT].writeToNBT(new NBTTagCompound()));
data.setTag("outputTank", tanks[TANK_OUTPUT].writeToNBT(new NBTTagCompound()));
data.setBoolean("active", active);
data.setFloat("energy", energyStored);
data.setInteger("ventStatus", ventStatus.ordinal());
data.setFloat("rotorEnergy", rotorEnergy);
data.setInteger("maxIntakeRate", maxIntakeRate);
data.setBoolean("inductorEngaged", inductorEngaged);
}
@Override
public void readFromNBT(NBTTagCompound data) {
if(data.hasKey("inputTank")) {
tanks[TANK_INPUT].readFromNBT(data.getCompoundTag("inputTank"));
}
if(data.hasKey("outputTank")) {
tanks[TANK_OUTPUT].readFromNBT(data.getCompoundTag("outputTank"));
}
if(data.hasKey("active")) {
setActive(data.getBoolean("active"));
}
if(data.hasKey("energy")) {
setEnergyStored(data.getFloat("energy"));
}
if(data.hasKey("ventStatus")) {
setVentStatus(VentStatus.values()[data.getInteger("ventStatus")], false);
}
if(data.hasKey("rotorEnergy")) {
setRotorEnergy(data.getFloat("rotorEnergy"));
if(!worldObj.isRemote) {
rpmUpdateTracker.setValue(getRotorSpeed());
}
}
if(data.hasKey("maxIntakeRate")) {
maxIntakeRate = data.getInteger("maxIntakeRate");
}
if(data.hasKey("inductorEngaged")) {
setInductorEngaged(data.getBoolean("inductorEngaged"), false);
}
}
@Override
public void formatDescriptionPacket(NBTTagCompound data) {
writeToNBT(data);
}
@Override
public void decodeDescriptionPacket(NBTTagCompound data) {
readFromNBT(data);
}
// Network Serialization
/**
* Used when dispatching update packets from the server.
* @param buf ByteBuf into which the turbine's full status should be written
*/
public void serialize(ByteBuf buf) {
// Capture compacted fluid data first
int inputFluidID, inputFluidAmt, outputFluidID, outputFluidAmt;
{
FluidStack inputFluid, outputFluid;
inputFluid = tanks[TANK_INPUT].getFluid();
outputFluid = tanks[TANK_OUTPUT].getFluid();
if(inputFluid == null || inputFluid.amount <= 0) {
inputFluidID = FLUID_NONE;
inputFluidAmt = 0;
}
else {
inputFluidID = inputFluid.getFluid().getID();
inputFluidAmt = inputFluid.amount;
}
if(outputFluid == null || outputFluid.amount <= 0) {
outputFluidID = FLUID_NONE;
outputFluidAmt = 0;
}
else {
outputFluidID = outputFluid.getFluid().getID();
outputFluidAmt = outputFluid.amount;
}
}
// User settings
buf.writeBoolean(active);
buf.writeBoolean(inductorEngaged);
buf.writeInt(ventStatus.ordinal());
buf.writeInt(maxIntakeRate);
// Basic stats
buf.writeFloat(energyStored);
buf.writeFloat(rotorEnergy);
// Reportage statistics
buf.writeFloat(energyGeneratedLastTick);
buf.writeInt(fluidConsumedLastTick);
buf.writeFloat(rotorEfficiencyLastTick);
// Fluid data
buf.writeInt(inputFluidID);
buf.writeInt(inputFluidAmt);
buf.writeInt(outputFluidID);
buf.writeInt(outputFluidAmt);
}
/**
* Used when a status packet arrives on the client.
* @param buf ByteBuf containing serialized turbine data
*/
public void deserialize(ByteBuf buf) {
// User settings
setActive(buf.readBoolean());
setInductorEngaged(buf.readBoolean(), false);
setVentStatus(s_VentStatuses[buf.readInt()], false);
setMaxIntakeRate(buf.readInt());
// Basic data
setEnergyStored(buf.readFloat());
setRotorEnergy(buf.readFloat());
// Reportage
energyGeneratedLastTick = buf.readFloat();
fluidConsumedLastTick = buf.readInt();
rotorEfficiencyLastTick = buf.readFloat();
// Fluid data
int inputFluidID = buf.readInt();
int inputFluidAmt = buf.readInt();
int outputFluidID = buf.readInt();
int outputFluidAmt = buf.readInt();
if(inputFluidID == FLUID_NONE || inputFluidAmt <= 0) {
tanks[TANK_INPUT].setFluid(null);
}
else {
Fluid fluid = FluidRegistry.getFluid(inputFluidID);
if(fluid == null) {
BRLog.warning("[CLIENT] Multiblock Turbine received an unknown fluid of type %d, setting input tank to empty", inputFluidID);
tanks[TANK_INPUT].setFluid(null);
}
else {
tanks[TANK_INPUT].setFluid(new FluidStack(fluid, inputFluidAmt));
}
}
if(outputFluidID == FLUID_NONE || outputFluidAmt <= 0) {
tanks[TANK_OUTPUT].setFluid(null);
}
else {
Fluid fluid = FluidRegistry.getFluid(outputFluidID);
if(fluid == null) {
BRLog.warning("[CLIENT] Multiblock Turbine received an unknown fluid of type %d, setting output tank to empty", outputFluidID);
tanks[TANK_OUTPUT].setFluid(null);
}
else {
tanks[TANK_OUTPUT].setFluid(new FluidStack(fluid, outputFluidAmt));
}
}
}
// Nondirectional FluidHandler implementation, similar to IFluidHandler
public int fill(int tank, FluidStack resource, boolean doFill) {
if(!canFill(tank, resource.getFluid())) {
return 0;
}
return tanks[tank].fill(resource, doFill);
}
public FluidStack drain(int tank, FluidStack resource, boolean doDrain) {
if(canDrain(tank, resource.getFluid())) {
return tanks[tank].drain(resource.amount, doDrain);
}
return null;
}
public FluidStack drain(int tank, int maxDrain, boolean doDrain) {
if(tank < 0 || tank >= NUM_TANKS) { return null; }
return tanks[tank].drain(maxDrain, doDrain);
}
public boolean canFill(int tank, Fluid fluid) {
if(tank < 0 || tank >= NUM_TANKS) { return false; }
FluidStack fluidStack = tanks[tank].getFluid();
if(fluidStack != null) {
return fluidStack.getFluid().getID() == fluid.getID();
}
else if(tank == TANK_INPUT) {
// TODO: Input tank can only be filled with compatible fluids from a registry
return fluid.getName().equals("steam");
}
else {
// Output tank can be filled with anything. Don't be a dumb.
return true;
}
}
public boolean canDrain(int tank, Fluid fluid) {
if(tank < 0 || tank >= NUM_TANKS) { return false; }
FluidStack fluidStack = tanks[tank].getFluid();
if(fluidStack == null) {
return false;
}
return fluidStack.getFluid().getID() == fluid.getID();
}
public FluidTankInfo[] getTankInfo() {
FluidTankInfo[] infos = new FluidTankInfo[NUM_TANKS];
for(int i = 0; i < NUM_TANKS; i++) {
infos[i] = tanks[i].getInfo();
}
return infos;
}
public FluidTankInfo getTankInfo(int tankIdx) {
return tanks[tankIdx].getInfo();
}
// IEnergyHandler
@Override
public int receiveEnergy(ForgeDirection from, int maxReceive, boolean simulate) {
// haha no
return 0;
}
@Override
public int extractEnergy(ForgeDirection from, int maxExtract, boolean simulate) {
int energyExtracted = Math.min((int)energyStored, maxExtract);
if(!simulate) {
energyStored -= energyExtracted;
}
return energyExtracted;
}
@Override
public boolean canConnectEnergy(ForgeDirection from) {
return true;
}
@Override
public int getEnergyStored(ForgeDirection from) {
return (int)energyStored;
}
@Override
public int getMaxEnergyStored(ForgeDirection from) {
return (int)maxEnergyStored;
}
private void setEnergyStored(float newEnergy) {
if(Float.isInfinite(newEnergy) || Float.isNaN(newEnergy)) { return; }
energyStored = Math.max(0f, Math.min(maxEnergyStored, newEnergy));
}
// Energy Helpers
public float getEnergyStored() {
return energyStored;
}
/**
* Remove some energy from the internal storage buffer.
* Will not reduce the buffer below 0.
* @param energy Amount by which the buffer should be reduced.
*/
protected void reduceStoredEnergy(float energy) {
addStoredEnergy(-1f * energy);
}
/**
* Add some energy to the internal storage buffer.
* Will not increase the buffer above the maximum or reduce it below 0.
* @param newEnergy
*/
protected void addStoredEnergy(float newEnergy) {
if(Float.isNaN(newEnergy)) { return; }
energyStored += newEnergy;
if(energyStored > maxEnergyStored) {
energyStored = maxEnergyStored;
}
if(-0.00001f < energyStored && energyStored < 0.00001f) {
// Clamp to zero
energyStored = 0f;
}
}
public void setStoredEnergy(float oldEnergy) {
energyStored = oldEnergy;
if(energyStored < 0.0 || Float.isNaN(energyStored)) {
energyStored = 0.0f;
}
else if(energyStored > maxEnergyStored) {
energyStored = maxEnergyStored;
}
}
/**
* Generate energy, internally. Will be multiplied by the BR Setting powerProductionMultiplier
* @param newEnergy Base, unmultiplied energy to generate
*/
protected void generateEnergy(float newEnergy) {
energyGeneratedLastTick += newEnergy * BigReactors.powerProductionMultiplier;
addStoredEnergy(newEnergy * BigReactors.powerProductionMultiplier);
}
// Activity state
public boolean getActive() {
return active;
}
public void setActive(boolean newValue) {
if(newValue != active) {
this.active = newValue;
for(IMultiblockPart part : connectedParts) {
if(this.active) { part.onMachineActivated(); }
else { part.onMachineDeactivated(); }
}
CoordTriplet referenceCoord = getReferenceCoord();
worldObj.markBlockForUpdate(referenceCoord.x, referenceCoord.y, referenceCoord.z);
markReferenceCoordDirty();
}
if(worldObj.isRemote) {
// Force controllers to re-render on client
for(IMultiblockPart part : attachedControllers) {
worldObj.markBlockForUpdate(part.xCoord, part.yCoord, part.zCoord);
}
for(TileEntityTurbineRotorPart part : attachedRotorBlades) {
worldObj.markBlockForUpdate(part.xCoord, part.yCoord, part.zCoord);
}
for(TileEntityTurbineRotorPart part : attachedRotorShafts) {
worldObj.markBlockForUpdate(part.xCoord, part.yCoord, part.zCoord);
}
}
}
// Governor
public int getMaxIntakeRate() { return maxIntakeRate; }
public void setMaxIntakeRate(int newRate) {
maxIntakeRate = Math.min(MAX_PERMITTED_FLOW, Math.max(0, newRate));
markReferenceCoordDirty();
}
// for GUI use
public int getMaxIntakeRateMax() { return MAX_PERMITTED_FLOW; }
// ISlotlessUpdater
@Override
public void beginUpdatingPlayer(EntityPlayer playerToUpdate) {
updatePlayers.add(playerToUpdate);
sendIndividualUpdate(playerToUpdate);
}
@Override
public void stopUpdatingPlayer(EntityPlayer playerToRemove) {
updatePlayers.remove(playerToRemove);
}
@Override
public boolean isUseableByPlayer(EntityPlayer player) {
return true;
}
private CoilPartData getCoilPartData(int x, int y, int z, Block block, int metadata) {
// Allow vanilla iron and gold blocks
if(block == Blocks.iron_block) { return TurbineCoil.getBlockData("blockIron"); }
if(block == Blocks.gold_block) { return TurbineCoil.getBlockData("blockGold"); }
if(block == BigReactors.blockMetal && metadata == BlockBRMetal.METADATA_LUDICRITE) { return TurbineCoil.getBlockData("blockLudicrite"); }
// Check the oredict to see if it's copper, or a funky kind of gold/iron block
String oreName = ItemHelper.oreProxy.getOreName(new ItemStack(block, 1, metadata));
return TurbineCoil.getBlockData(oreName);
}
/**
* Recalculate rotor and coil parameters
*/
private void recalculateDerivedStatistics() {
CoordTriplet minInterior, maxInterior;
minInterior = getMinimumCoord();
maxInterior = getMaximumCoord();
minInterior.x++; minInterior.y++; minInterior.z++;
maxInterior.x++; maxInterior.y++; maxInterior.z++;
rotorMass = 0;
bladeSurfaceArea = 0;
coilSize = 0;
float coilEfficiency = 0f;
float coilBonus = 0f;
float coilDragCoefficient = 0f;
// Loop over interior space. Calculate mass and blade area of rotor and size of coils
for(int x = minInterior.x; x <= maxInterior.x; x++) {
for(int y = minInterior.y; y <= maxInterior.y; y++) {
for(int z = minInterior.z; z <= maxInterior.z; z++) {
Block block = worldObj.getBlock(x, y, z);
int metadata = worldObj.getBlockMetadata(x, y, z);
CoilPartData coilData = null;
if(block == BigReactors.blockTurbineRotorPart) {
rotorMass += BigReactors.blockTurbineRotorPart.getRotorMass(block, metadata);
if(BlockTurbineRotorPart.isRotorBlade(metadata)) {
bladeSurfaceArea += 1;
}
}
coilData = getCoilPartData(x, y, z, block, metadata);
if(coilData != null) {
coilEfficiency += coilData.efficiency;
coilBonus += coilData.bonus;
coilDragCoefficient += coilData.energyExtractionRate;
coilSize += 1;
}
} // end z
} // end y
} // end x loop - looping over interior
// Precalculate some stuff now that we know how big the rotor and blades are
frictionalDrag = rotorMass * rotorDragCoefficient * BigReactors.turbineMassDragMultiplier;
bladeDrag = baseBladeDragCoefficient * bladeSurfaceArea * BigReactors.turbineAeroDragMultiplier;
if(coilSize <= 0)
{
// Uh. No coil? Fine.
inductionEfficiency = 0f;
inductionEnergyExponentBonus = 1f;
inductorDragCoefficient = 0f;
}
else
{
inductionEfficiency = (coilEfficiency * 0.33f) / coilSize;
inductionEnergyExponentBonus = Math.max(1f, (coilBonus / coilSize));
inductorDragCoefficient = (coilDragCoefficient / coilSize) * inductorBaseDragCoefficient;
}
}
public float getRotorSpeed() {
if(attachedRotorBlades.size() <= 0 || rotorMass <= 0) { return 0f; }
return rotorEnergy / (attachedRotorBlades.size() * rotorMass);
}
public float getEnergyGeneratedLastTick() { return energyGeneratedLastTick; }
public int getFluidConsumedLastTick() { return fluidConsumedLastTick; }
public int getNumRotorBlades() { return attachedRotorBlades.size(); }
public float getRotorEfficiencyLastTick() { return rotorEfficiencyLastTick; }
public float getMaxRotorSpeed() {
return 2000f;
}
public int getRotorMass() {
return rotorMass;
}
public VentStatus getVentSetting() {
return ventStatus;
}
public void setVentStatus(VentStatus newStatus, boolean markReferenceCoordDirty) {
ventStatus = newStatus;
if(markReferenceCoordDirty)
markReferenceCoordDirty();
}
public boolean getInductorEngaged() {
return inductorEngaged;
}
public void setInductorEngaged(boolean engaged, boolean markReferenceCoordDirty) {
inductorEngaged = engaged;
if(markReferenceCoordDirty)
markReferenceCoordDirty();
}
private void setRotorEnergy(float newEnergy) {
if(Float.isNaN(newEnergy) || Float.isInfinite(newEnergy)) { return; }
rotorEnergy = Math.max(0f, newEnergy);
}
protected void markReferenceCoordDirty() {
if(worldObj == null || worldObj.isRemote) { return; }
CoordTriplet referenceCoord = getReferenceCoord();
if(referenceCoord == null) { return; }
rpmUpdateTracker.onExternalUpdate();
TileEntity saveTe = worldObj.getTileEntity(referenceCoord.x, referenceCoord.y, referenceCoord.z);
worldObj.markTileEntityChunkModified(referenceCoord.x, referenceCoord.y, referenceCoord.z, saveTe);
worldObj.markBlockForUpdate(referenceCoord.x, referenceCoord.y, referenceCoord.z);
}
// For client usage only
public ForgeDirection getRotorDirection() {
if(attachedRotorBearings.size() < 1) {
return ForgeDirection.UNKNOWN;
}
if(!this.isAssembled()) {
return ForgeDirection.UNKNOWN;
}
TileEntityTurbineRotorBearing rotorBearing = attachedRotorBearings.iterator().next();
return rotorBearing.getOutwardsDir().getOpposite();
}
public boolean hasGlass() { return attachedGlass.size() > 0; }
public String getDebugInfo() {
StringBuilder sb = new StringBuilder();
sb.append("Assembled: ").append(Boolean.toString(isAssembled())).append("\n");
sb.append("Attached Blocks: ").append(Integer.toString(connectedParts.size())).append("\n");
if(getLastValidationException() != null) {
sb.append("Validation Exception:\n").append(getLastValidationException().getMessage()).append("\n");
}
if(isAssembled()) {
sb.append("\nActive: ").append(Boolean.toString(getActive()));
sb.append("\nStored Energy: ").append(Float.toString(getEnergyStored()));
sb.append("\nRotor Energy: ").append(Float.toString(rotorEnergy));
sb.append("\nRotor Speed: ").append(Float.toString(getRotorSpeed())).append(" rpm");
sb.append("\nInductor Engaged: ").append(Boolean.toString(inductorEngaged));
sb.append("\nVent Status: ").append(ventStatus.toString());
sb.append("\nMax Intake Rate: ").append(Integer.toString(maxIntakeRate));
sb.append("\nCoil Size: ").append(Integer.toString(coilSize));
sb.append("\nRotor Mass: ").append(Integer.toString(rotorMass));
sb.append("\nBlade SurfArea: ").append(Integer.toString(bladeSurfaceArea));
sb.append("\n# Blades: ").append(Integer.toString(attachedRotorBlades.size()));
sb.append("\n# Shafts: ").append(Integer.toString(attachedRotorShafts.size()));
sb.append("\nRotor Drag CoEff: ").append(Float.toString(rotorDragCoefficient));
sb.append("\nBlade Drag: ").append(Float.toString(bladeDrag));
sb.append("\nFrict Drag: ").append(Float.toString(frictionalDrag));
sb.append("\n\nFluid Tanks:\n");
for(int i = 0; i < tanks.length; i++) {
sb.append(String.format("[%d] %s ", i, i == TANK_OUTPUT ? "outlet":"inlet"));
if(tanks[i] == null || tanks[i].getFluid() == null) {
sb.append("empty");
}
else {
FluidStack stack = tanks[i].getFluid();
sb.append(String.format("%s, %d mB", stack.getFluid().getName(), stack.amount));
}
sb.append("\n");
}
}
return sb.toString();
}
@SideOnly(Side.CLIENT)
public void resetCachedRotors() {
for(TileEntityTurbineRotorBearing bearing: attachedRotorBearings) {
bearing.clearDisplayList();
}
}
}