package org.sidoh.reactor_simulator.simulator; import java.util.HashSet; import java.util.Set; import cofh.api.energy.IEnergyHandler; 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.interfaces.IMultipleFluidHandler; 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.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; import io.netty.buffer.ByteBuf; import net.minecraft.block.Block; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; 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; public class MultiblockTurbineSimulator 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 MultiblockTurbineSimulator(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 null; } /** * 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() { } @Override protected void onMachineRestored() { } @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: // 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 MultiblockTurbineSimulator)) { 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; } MultiblockTurbineSimulator otherTurbine = (MultiblockTurbineSimulator)otherMachine; setRotorEnergy(Math.max(rotorEnergy, otherTurbine.rotorEnergy)); } @Override protected void onAssimilated(MultiblockControllerBase assimilator) { attachedControllers.clear(); attachedRotorBearings.clear(); attachedTickables.clear(); attachedPowerTaps.clear(); } protected boolean updateServer() { return false; } protected static TurbineResult simulateServer(int steamIn, int levelsOfBlades, int bladesPerLevel, String inductorName, int inductorLevels, float rotorSpeed, int extraShaft) { float energyGeneratedLastTick = 0f; float fluidConsumedLastTick = 0; float rotorEfficiencyLastTick = 1f; int bladeSurfaceArea = levelsOfBlades * bladesPerLevel; int inductorBlockAmount = inductorLevels * 8; int rotorMass = bladeSurfaceArea * 10 + levelsOfBlades * 10 + inductorLevels * 10 + extraShaft *10; float rotorDragCoefficient = 0.01f; int rotorMomentum = bladeSurfaceArea * rotorMass; float rotorEnergy = rotorSpeed * rotorMomentum; float frictionalDrag = rotorMass * rotorDragCoefficient * BigReactors.turbineMassDragMultiplier; float bladeDrag = baseBladeDragCoefficient * bladeSurfaceArea * BigReactors.turbineAeroDragMultiplier; CoilPartData coilData = getCoilPartData(inductorName); float coilEfficiency = 0; float coilBonus = 0; float coilDragCoefficient = 0; int coilSize = 0; if (coilData != null) { for (int i = 0; i < inductorBlockAmount; i++) { coilEfficiency += coilData.efficiency; coilBonus += coilData.bonus; coilDragCoefficient += coilData.energyExtractionRate; coilSize += 1; } } float inductionEfficiency = 0f; float inductionEnergyExponentBonus = 1f; float inductorDragCoefficient = 0f; 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; } // Generate energy based on steam if (steamIn > 0 || rotorEnergy > 0) { // 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 = rotorSpeed * inductorDragCoefficient * coilSize; 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); } float newEnergy = energyToGenerate * efficiency; energyGeneratedLastTick += newEnergy * BigReactors.powerProductionMultiplier; } rotorEnergy += liftTorque + -1f * inductionTorque + -1f * aerodynamicDragTorque + -1f * frictionalDrag; if (rotorEnergy < 0f) { rotorEnergy = 0f; } } return new TurbineResult(energyGeneratedLastTick, rotorEfficiencyLastTick, (rotorEnergy / (rotorMomentum) - rotorSpeed)); } public static class TurbineResult{ public float energyGenerated; public float efficiency; public float changeInRotorSpeed; public TurbineResult(float energyGenerated, float efficiency, float changeInRotorSpeed) { this.energyGenerated = energyGenerated; this.efficiency = efficiency; this.changeInRotorSpeed = changeInRotorSpeed; } @Override public String toString() { return "TurbineResult{" + "energyGenerated=" + energyGenerated + ", efficiency=" + efficiency + ", changeInRotorSpeed=" + changeInRotorSpeed + '}'; } } @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 static CoilPartData getCoilPartData(String name) { return TurbineCoil.getBlockData(name); } /** * 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(); } } }