void AddTank(FuelTank tank) { GUILayout.Label(addLabelCache[tank.name], GUILayout.Width(150)); if (GUILayout.Button("Add", GUILayout.Width(120))) { tank.maxAmount = tank_module.AvailableVolume * tank.utilization; tank.amount = tank.fillable ? tank.maxAmount : 0; tank.maxAmountExpression = tank.maxAmount.ToString(); GameEvents.onEditorShipModified.Fire(EditorLogic.fetch.ship); tank_module.MarkWindowDirty(); log.dbg("Adding tank {0} maxAmount: {1}", tank.name, tank.maxAmountExpression); } }
protected void ChangeResources(double volumeRatio, bool propagate = false) { // The used volume will rescale automatically when setting maxAmount for (int i = 0; i < tankList.Count; i++) { FuelTank tank = tankList[i]; bool save_propagate = tank.propagate; tank.propagate = propagate; tank.maxAmount *= volumeRatio; tank.propagate = save_propagate; } }
private bool CalculateLowestTankTemperature() { bool result = false; lowestTankTemperature = 300; for (int i = tankList.Count - 1; i >= 0; --i) { FuelTank tank = tankList[i]; if (tank.maxAmount > 0d && (tank.vsp > 0.0 || tank.loss_rate > 0d)) { lowestTankTemperature = Math.Min(lowestTankTemperature, tank.temperature); result = true; } } return(result); }
void RecordTankTypeResources(HashSet <string> resources, string type) { TankDefinition def; if (!MFSSettings.tankDefinitions.Contains(type)) { return; } def = MFSSettings.tankDefinitions[type]; for (int i = 0; i < def.tankList.Count; i++) { FuelTank tank = def.tankList[i]; resources.Add(tank.name); } }
internal FuelTank CreateCopy(ModuleFuelTanks toModule, ConfigNode overNode, bool initializeAmounts) { FuelTank clone = (FuelTank)MemberwiseClone(); clone.module = toModule; if (overNode != null) { clone.Load(overNode); } if (initializeAmounts) { clone.InitializeAmounts(); } else { clone.amountExpression = clone.maxAmountExpression = null; } clone.GetDensity(); return(clone); }
void EditTank(FuelTank tank) { GUILayout.Label(" ", GUILayout.Width(5)); GUIStyle style = unchanged; if (tank.maxAmountExpression == null) { tank.maxAmountExpression = tank.maxAmount.ToString(); //log.warn("Adding tank from API " + tank.name + " amount: " + tank.maxAmountExpression ?? "null"); } else if (tank.maxAmountExpression.Length > 0 && tank.maxAmountExpression != tank.maxAmount.ToString()) { style = changed; } tank.maxAmountExpression = GUILayout.TextField(tank.maxAmountExpression, style, GUILayout.Width(127)); UpdateTank(tank); RemoveTank(tank); }
public override string GetInfo() { if (!compatible) { return(""); } UpdateTankType(); StringBuilder info = new StringBuilder(); info.AppendLine("Modular Fuel Tank:"); info.Append(" Max Volume: ").AppendLine(KSPUtil.PrintSI(volume, MFSSettings.unitLabel)); info.AppendLine(" Tank can hold:"); for (int i = 0; i < tankList.Count; i++) { FuelTank tank = tankList[i]; info.Append(" ").Append(tank).Append(" ").AppendLine(tank.note); } return(info.ToString()); }
partial void OnStartRF(StartState state) { GameEvents.onVesselWasModified.Add(OnVesselWasModified); GameEvents.onEditorShipModified.Add(OnEditorShipModified); GameEvents.onPartDestroyed.Add(OnPartDestroyed); if (HighLogic.LoadedSceneIsFlight) { for (int i = 0; i < vessel.vesselModules.Count; i++) { if (vessel.vesselModules[i] is FlightIntegrator) { _flightIntegrator = vessel.vesselModules[i] as FlightIntegrator; } } } // Wait to calculate tank area because it depends on drag cubes // MLI depends on tank area so mass will also be recalculated IEnumerator WaitAndRecalculateMass() { yield return(null); yield return(null); yield return(null); CalculateTankArea(); massDirty = true; CalculateMass(); } if (HighLogic.LoadedSceneIsFlight) { StartCoroutine(WaitAndRecalculateMass()); } for (int i = tankList.Count - 1; i >= 0; --i) { FuelTank tank = tankList[i]; if (tank.maxAmount > 0.0 && (tank.vsp > 0.0 || tank.loss_rate > 0.0)) { supportsBoiloff = true; break; } } if (state == StartState.Editor) { if (maxMLILayers > 0) { ((UI_FloatRange)Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor).maxValue = maxMLILayers; } else { Fields[nameof(_numberOfAddedMLILayers)].guiActiveEditor = false; } Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor.onFieldChanged = delegate(BaseField field, object value) { massDirty = true; CalculateMass(); }; } Fields[nameof(debug0Display)].guiActive = this.supportsBoiloff && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW); Fields[nameof(debug1Display)].guiActive = this.supportsBoiloff && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW); Fields[nameof(debug2Display)].guiActive = this.supportsBoiloff && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW); //numberOfAddedMLILayers = Mathf.Round(numberOfAddedMLILayers); //CalculateInsulation(); }
public void CalculateTankArea() { // TODO: Codify a more accurate tank area calculator. // Thought: cube YN/YP can be used to find the part diameter / circumference... X or Z finds the length // Also should try to determine if tank has a common bulkhead - and adjust heat flux into individual tanks accordingly #if DEBUG log.dbg("CalculateTankArea() running"); #endif if (HighLogic.LoadedSceneIsEditor) { if (!this.part.DragCubes.None && this.oldTotalVolume != this.totalVolume) { if (this.IsProcedural()) { bool origProceduralValue = this.part.DragCubes.Procedural; this.part.DragCubes.Procedural = true; this.part.DragCubes.ForceUpdate(true, true, true); this.part.DragCubes.SetDragWeights(); this.part.DragCubes.RequestOcclusionUpdate(); this.part.DragCubes.SetPartOcclusion(); this.part.DragCubes.Procedural = origProceduralValue; this.oldTotalVolume = this.totalVolume; } } } totalTankArea = 0f; for (int i = 0; i < 6; ++i) { totalTankArea += part.DragCubes.WeightedArea[i]; } #if DEBUG log.info("Part WeightedArea: {0} = {1:0.00}", part.name, totalTankArea); log.info("Part Area: {0} = {1:0.00}", part.name, part.DragCubes.Area); #endif // This allows a rough guess as to individual tank surface area based on ratio of tank volume to total volume but it breaks down at very small fractions // So use greater of spherical calculation and tank ratio of total area. // if for any reason our totalTankArea is still 0 (no drag cubes available yet or analytic temp routines executed first) // then we're going to be defaulting to spherical calculation double tankMaxAmount; double tempTotal = 0; if (RFSettings.Instance.debugBoilOff) { log.info("Initializing {0}.totalTankArea as {1}", part.name, totalTankArea); } for (int i = tankList.Count - 1; i >= 0; --i) { FuelTank tank = tankList[i]; if (tank.maxAmount > 0.0) { tankMaxAmount = tank.maxAmount; if (tank.utilization > 1.0) { tankMaxAmount /= tank.utilization; } tank.tankRatio = tankMaxAmount / volume; tank.totalArea = Math.Max(Math.Pow(Math.PI, 1.0 / 3.0) * Math.Pow((tankMaxAmount / 1000.0) * 6, 2.0 / 3.0), totalTankArea * tank.tankRatio); tempTotal += tank.totalArea; if (RFSettings.Instance.debugBoilOff) { log.detail("{0}.tankRatio = {1}", tank.name, tank.tankRatio); log.detail("{0}.maxAmount = {1}", tank.name, tankMaxAmount); log.detail("{0}.totalTankArea = {1}", part.name, totalTankArea); log.detail("Tank surface area = {0}", tank.totalArea); log.dbg("tank Dewar status = {0}", tank.isDewar); } } } if (!(totalTankArea > 0) || tempTotal > totalTankArea) { totalTankArea = tempTotal; } if (RFSettings.Instance.debugBoilOff) { log.info("{0}.totalTankArea = {1}", part.name, totalTankArea); log.info("{0}.GetModuleSize() = {1:0.00}" + part.name, part.GetModuleSize(Vector3.zero)); } }
private IEnumerator CalculateTankBoiloff(double deltaTime, bool analyticalMode = false) { // Need to ensure that all heat compensation (radiators, heat pumps, etc) run first. if (totalTankArea <= 0) { CalculateTankArea(); } if (!analyticalMode) { yield return(new WaitForFixedUpdate()); } boiloffMass = 0d; previewInternalFluxAdjust = 0; bool hasCryoFuels = CalculateLowestTankTemperature(); if (tankList.Count > 0 && lowestTankTemperature < 300d && MFSSettings.radiatorMinTempMult >= 0d) { part.radiatorMax = (lowestTankTemperature * MFSSettings.radiatorMinTempMult) / part.maxTemp; } if (fueledByLaunchClamp) { if (hasCryoFuels) { part.temperature = lowestTankTemperature; part.skinTemperature = lowestTankTemperature; } fueledByLaunchClamp = false; yield break; } if (!double.IsNaN(part.temperature)) { partPrevTemperature = part.temperature; } else { part.temperature = partPrevTemperature; } if (deltaTime > 0) { double deltaTimeRecip = 1d / deltaTime; log.detail("internalFlux = {0}, thermalInternalFluxPrevious = {1}, analytic internal flux = {2}", part.thermalInternalFlux, part.thermalInternalFluxPrevious, previewInternalFluxAdjust); if (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) { string MLIText = totalMLILayers > 0 ? GetMLITransferRate(part.skinTemperature, part.temperature).ToString("F4") : "No MLI"; debug0Display = part.temperature.ToString("F4") + "(" + MLIText + " * " + (part.radiativeArea * part.skinExposedAreaFrac).ToString("F2") + "m2)"; } double cooling = 0; if (analyticalMode) { if (part.thermalInternalFlux < 0) { cooling = part.thermalInternalFlux; } else if (part.thermalInternalFluxPrevious < 0) { cooling = part.thermalInternalFluxPrevious; } if (cooling < 0) { // in analytic mode, MFTRF interprets this as an attempt to cool the tanks analyticInternalTemp += cooling * part.thermalMassReciprocal * deltaTime; // because of what we're doing in CalculateAnalyticInsulationFactor(), it will take too much time to reach that temperature so part.temperature += cooling * part.thermalMassReciprocal * deltaTime; } } debug3Display = Utilities.FormatFlux(cooling); for (int i = tankList.Count - 1; i >= 0; --i) { FuelTank tank = tankList[i]; if (tank.amount <= 0) { continue; } if (tank.vsp > 0.0 && tank.totalArea > 0) { // Opposite of original boil off code. Finds massLost first. double massLost = 0.0; double deltaTemp; double hotTemp = part.temperature; double tankRatio = tank.maxAmount / volume; if (RFSettings.Instance.ferociousBoilOff) { hotTemp = Math.Max(((hotTemp * part.thermalMass) - (tank.temperature * part.resourceThermalMass)) / (part.thermalMass - part.resourceThermalMass), part.temperature); } deltaTemp = hotTemp - tank.temperature; if (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) { if (debug2Display != "") { debug2Display += " / "; } if (debug1Display != "") { debug1Display += " / "; } } double Q = 0; if (deltaTemp > 0) { #if DEBUG if (analyticalMode) { log.dbg("Tank {0} surface area = {1}", tank.name, tank.totalArea); } #endif double wettedArea = tank.totalArea; // disabled until proper wetted vs ullage conduction can be done (tank.amount / tank.maxAmount); if (tank.isDewar) { Q = GetDewarTransferRate(hotTemp, tank.temperature, tank.totalArea); } else { Q = deltaTemp / ((tank.wallThickness / (tank.wallConduction * wettedArea)) + (tank.insulationThickness / (tank.insulationConduction * wettedArea)) + (tank.resourceConductivity > 0 ? (0.01 / (tank.resourceConductivity * wettedArea)) : 0)); } Q *= 0.001d; // convert to kilowatts if (!double.IsNaN(Q)) { massLost = Q / tank.vsp; } else { log.dbg("Q = NaN! W - T - F!!!"); } if (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) { // Only do debugging displays if debugging enabled in RFSettings debug1Display += Utilities.FormatFlux(Q); debug2Display += (massLost * 1000 * 3600).ToString("F4") + "kg/hr"; } massLost *= deltaTime; // Frame scaling } double lossAmount = massLost / tank.density; if (double.IsNaN(lossAmount)) { log.dbg("{0} lossAmount is NaN!", tank.name); } else { if (lossAmount > tank.amount) { if (!CheatOptions.InfinitePropellant) { tank.amount = 0d; } } else { if (!CheatOptions.InfinitePropellant) { tank.amount -= lossAmount; } // See if there is boiloff byproduct and see if any other parts want to accept it. if (tank.boiloffProductResource != null) { double boiloffProductAmount = -(massLost / tank.boiloffProductResource.density); double retainedAmount = part.RequestResource(tank.boiloffProductResource.id, boiloffProductAmount, ResourceFlowMode.STAGE_PRIORITY_FLOW); massLost -= retainedAmount * tank.boiloffProductResource.density; } boiloffMass += massLost; } // subtract heat from boiloff // subtracting heat in analytic mode is tricky: Analytic flux handling is 'cheaty' and tricky to predict. // scratch sheet: example // [ModularFuelSystem.ModuleFuelTankRF] proceduralTankRealFuels Analytic Temp = 256.679360297684, Analytic Internal = 256.679360297684, Analytic Skin = 256.679360297684 // [ModularFuelSystem.ModuleFuelTankRF] proceduralTankRealFuels deltaTime = 17306955.5092776, heat lost = 6638604.21227684, thermalMassReciprocal = 0.00444787360733243 if (!double.IsNaN(Q)) { double heatLost = -Q; if (!analyticalMode) { part.AddThermalFlux(heatLost); } else { analyticInternalTemp = analyticInternalTemp + (heatLost * part.thermalMassReciprocal * deltaTime); // Don't try to adjust flux if significant time has passed; it never works out. // Analytic mode flux only gets applied if timewarping AND analytic mode was set. if (TimeWarp.CurrentRate > 1) { previewInternalFluxAdjust += heatLost; } else { log.dbg("boiloff function running with delta time of {0} (vessel.lastUT = {1} seconds ago)", deltaTime, (Planetarium.GetUniversalTime() - vessel.lastUT).ToString("F4")); } #if DEBUG if (deltaTime > 0) { log.dbg("{0} deltaTime = {1}, heat lost = {2}, thermalMassReciprocal = {3}", part.name, deltaTime, heatLost, part.thermalMassReciprocal); } #endif } } else { log.dbg("WHO WOULD WIN? Some Well Written Code or One Misplaced NaN?"); log.dbg("heatLost = {0}", Q); log.dbg("deltaTime = {0}", deltaTime); log.dbg("deltaTimeRecip = {0}", deltaTimeRecip); log.dbg("massLost = {0}" + massLost); log.dbg("tank.vsp = {0}" + tank.vsp); } } } else if (tank.loss_rate > 0 && tank.amount > 0) { double deltaTemp = part.temperature - tank.temperature; if (deltaTemp > 0) { double lossAmount = tank.maxAmount * tank.loss_rate * deltaTemp * deltaTime; if (lossAmount > tank.amount) { lossAmount = -tank.amount; tank.amount = 0d; } else { lossAmount = -lossAmount; tank.amount += lossAmount; } double massLost = tank.density * lossAmount; boiloffMass += massLost; } } } } }
private void FillAttachedTanks(double deltaTime) { // sanity check if (deltaTime <= 0) { return; } // now, let's look at what we're connected to. foreach (Part p in vessel.parts) // look through all parts { Tanks.ModuleFuelTanks m = p.FindModuleImplementing <Tanks.ModuleFuelTanks>(); if (m != null) { m.fueledByLaunchClamp = true; // look through all tanks inside this part for (int j = m.tankList.Count - 1; j >= 0; --j) { Tanks.FuelTank tank = m.tankList[j]; // if a tank isn't full, start filling it. if (tank.maxAmount <= 0) { continue; } PartResource r = tank.resource; if (r == null) { continue; } PartResourceDefinition d = PartResourceLibrary.Instance.GetDefinition(r.resourceName); if (d == null) { continue; } if (tank.amount < tank.maxAmount && tank.fillable && r.flowMode != PartResource.FlowMode.None && d.resourceTransferMode == ResourceTransferMode.PUMP && r.flowState) { double amount = Math.Min(deltaTime * pump_rate * tank.utilization, tank.maxAmount - tank.amount); Game game = HighLogic.CurrentGame; if (d.unitCost > 0 && game.Mode == Game.Modes.CAREER && Funding.Instance != null) { double funds = Funding.Instance.Funds; double cost = amount * d.unitCost; if (cost > funds) { amount = funds / d.unitCost; cost = funds; } Funding.Instance.AddFunds(-cost, TransactionReasons.VesselRollout); } //tank.amount = tank.amount + amount; p.TransferResource(r, amount, this.part); } } } else { for (int j = p.Resources.Count - 1; j >= 0; --j) { PartResource partResource = p.Resources[j]; if (partResource.info.name == "ElectricCharge") { if (partResource.flowMode != PartResource.FlowMode.None && partResource.info.resourceTransferMode == ResourceTransferMode.PUMP && partResource.flowState) { double amount = deltaTime * pump_rate; amount = Math.Min(amount, partResource.maxAmount - partResource.amount); p.TransferResource(partResource, amount, this.part); } } } } } }
public void CalculateMass() { if (tankList == null || !massDirty) { return; } massDirty = false; double basemass = basemassConst + basemassPV * (MFSSettings.basemassUseTotalVolume ? totalVolume : volume); CalculateMassRF(ref basemass); if (basemass >= 0) { double tankDryMass = 0; for (int i = 0; i < tankList.Count; i++) { FuelTank tank = tankList[i]; tankDryMass += tank.maxAmount * tank.mass / tank.utilization; } mass = (float)((basemass + tankDryMass) * MassMult); // compute massDelta based on prefab, if available. if (part.partInfo == null || part.partInfo.partPrefab == null) { part.mass = mass; massDelta = 0; } else { massDelta = mass - part.partInfo.partPrefab.mass; } } else { mass = part.mass; // display dry mass even in this case. massDelta = 0f; } if (isEditor) { UsedVolume = tankList .Where(fuel => fuel.maxAmount > 0 && fuel.utilization > 0) .Sum(fuel => fuel.maxAmount / fuel.utilization); double availRounded = AvailableVolume; if (Math.Abs(availRounded) < 0.001d) { availRounded = 0d; } string availVolStr = KSPUtil.PrintSI(availRounded, MFSSettings.unitLabel); string volStr = KSPUtil.PrintSI(volume, MFSSettings.unitLabel); volumeDisplay = "Avail: " + availVolStr + " / Tot: " + volStr; double resourceMass = part.Resources.Cast <PartResource> ().Sum(partResource => partResource.maxAmount * partResource.info.density); double wetMass = mass + resourceMass; massDisplay = "Dry: " + FormatMass(mass) + " / Wet: " + FormatMass((float)wetMass); UpdateTweakableMenu(); } }
private void UpdateTankType(bool initializeAmounts = true) { if (oldType == type || type == null) { return; } // Copy the tank list from the tank definitiion TankDefinition def; if (!MFSSettings.tankDefinitions.Contains(type)) { log.error("Unable to find tank definition for type \"{0}\" reverting.", type); type = oldType; return; } def = MFSSettings.tankDefinitions[type]; if (!def.canHave) { type = oldType; if (!string.IsNullOrEmpty(oldType)) // we have an old type { def = MFSSettings.tankDefinitions[type]; if (def.canHave) { return; // go back to old type } } // else find one that does work if (typesAvailable != null) { for (int i = 0; i < typesAvailable.Count(); i++) { string tn = typesAvailable[i]; TankDefinition newDef = MFSSettings.tankDefinitions.Contains(tn) ? MFSSettings.tankDefinitions[tn] : null; if (newDef != null && newDef.canHave) { def = newDef; type = newDef.name; break; } } } if (type == oldType) // if we didn't find a new one { log.error("Unable to find a type that is tech-available for part {0}", part.name); return; } } oldType = type; // Build the new tank list. tankList = new FuelTankList(); for (int i = 0; i < def.tankList.Count; i++) { FuelTank tank = def.tankList[i]; // Pull the override from the list of overrides ConfigNode overNode = MFSSettings.GetOverrideList(part).FirstOrDefault(n => n.GetValue("name") == tank.name); tankList.Add(tank.CreateCopy(this, overNode, initializeAmounts)); } tankList.TechAmounts(); // update for current techs // Destroy any managed resources that are not in the new type. HashSet <string> managed = MFSSettings.managedResources[part.name]; // if this throws, we have some big fish to fry bool needsMesage = false; for (int i = part.Resources.Count - 1; i >= 0; --i) { PartResource partResource = part.Resources[i]; string resname = partResource.resourceName; if (!managed.Contains(resname) || tankList.Contains(resname) || unmanagedResources.ContainsKey(resname)) { continue; } part.Resources.Remove(partResource.info.id); #if KSP150 part.SimulationResources.Remove(partResource.info.id); #endif needsMesage = true; } if (needsMesage) { RaiseResourceListChanged(); } if (!basemassOverride) { ParseBaseMass(def.basemass); } if (!baseCostOverride) { ParseBaseCost(def.baseCost); } if (!isDatabaseLoad) { // being called in the SpaceCenter scene is assumed to be a database reload //FIXME is this really needed? massDirty = true; } UpdateTankTypeRF(def); UpdateTestFlight(); }