diff --git a/RealFuels/CryoCooler.cfg b/RealFuels/CryoCooler.cfg new file mode 100644 index 00000000..e73e48e3 --- /dev/null +++ b/RealFuels/CryoCooler.cfg @@ -0,0 +1,63 @@ +// Cryocooler defaults for RealFuels tanks. +// Refer to \RealismOverhaul\RO_RealFuels_Crycoolers.cfg for references and formulas. + +@PART:HAS[@MODULE[ModuleFuelTanks]:HAS[#type[Cryogenic]]]:AFTER[RealFuels] +{ + &rfAddCryocooler = true +} + +@PART:HAS[@MODULE[ModuleFuelTanks]:HAS[#type[BalloonCryo]]]:AFTER[RealFuels] +{ + &rfAddCryocooler = true +} + +@PART:HAS[@MODULE[ModuleFuelTanks]:HAS[#type[ServiceModule]]]:AFTER[RealFuels] +{ + &rfAddCryocooler = true +} + +@PART:HAS[#rfAddCryocooler[?rue]]:NEEDS[!RealismOverhaul]:AFTER[RealFuels] +{ + @MODULE[ModuleFuelTanks] + { + maxCoolerInputKW = 10 + coolerBaseMass = 0.01 + coolerMassPerKWInput + { + key = 4 0.120 + key = 20 0.060 + key = 40 0.030 + key = 65 0.025 + key = 90 0.020 + key = 150 0.015 + key = 250 0.010 + } + coolerBaseCost = 150 + coolerCostPerKWInput + { + key = 4 9000 + key = 20 4500 + key = 40 2250 + key = 65 1875 + key = 90 1500 + key = 150 1125 + key = 250 750 + } + cryoCoolerEfficiency + { + key = 0 0 + key = 4 0.01 + key = 20 0.08 + key = 65 0.20 + key = 90 0.25 + key = 150 0.40 + key = 250 0.50 + } + } +} + +// cleanup +@PART:HAS[#rfAddCryocooler]:AFTER[RealFuels] +{ + !rfAddCryocooler = delete +} diff --git a/RealFuels/Localization/en-us.cfg b/RealFuels/Localization/en-us.cfg index edc44872..31fb8a73 100644 --- a/RealFuels/Localization/en-us.cfg +++ b/RealFuels/Localization/en-us.cfg @@ -178,10 +178,13 @@ Localization #RF_FuelTankRF_WallTemp = Wall Temp #RF_FuelTankRF_HeatPenetration = Heat Penetration #RF_FuelTankRF_BoiloffLoss = Boil-off Loss - #RF_FuelTankRF_AnalyticCooling = Analytic Cooling #RF_FuelTankRF_NoMLI = No MLI #RF_FuelTankRF_Boiloffunit = kg/hr #RF_FuelTankRF_kerbalismtips = boiloff product + #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input + #RF_FuelTankRF_CryoCoolerLift = Cooling Lift + #RF_FuelTankRF_CryoCoolerDraw = Power Draw + #RF_FuelTankRF_CryoCoolerCOP = Avg COP #RF_TankDefineSelection_HighlyPressurized = Highly Pressurized #RF_TankDefineSelection_NotHighlyPressurized = Not Highly Pressurized diff --git a/RealFuels/Localization/pt-br.cfg b/RealFuels/Localization/pt-br.cfg index e8f1b1f6..ef34580a 100644 --- a/RealFuels/Localization/pt-br.cfg +++ b/RealFuels/Localization/pt-br.cfg @@ -174,10 +174,13 @@ Localization #RF_FuelTankRF_WallTemp = Temp. da Parede #RF_FuelTankRF_HeatPenetration = Penetração de Calor #RF_FuelTankRF_BoiloffLoss = Perda por Evaporação - #RF_FuelTankRF_AnalyticCooling = Resfriamento Analítico #RF_FuelTankRF_NoMLI = Sem MLI #RF_FuelTankRF_Boiloffunit = kg/h #RF_FuelTankRF_kerbalismtips = produto de evaporação + #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input + #RF_FuelTankRF_CryoCoolerLift = Cooling Lift + #RF_FuelTankRF_CryoCoolerDraw = Power Draw + #RF_FuelTankRF_CryoCoolerCOP = Avg COP #RF_TankDefineSelection_HighlyPressurized = Altamente Pressurizado #RF_TankDefineSelection_NotHighlyPressurized = Não Altamente Pressurizado diff --git a/RealFuels/Localization/ru.cfg b/RealFuels/Localization/ru.cfg index 0aaa0f07..4ab2f291 100644 --- a/RealFuels/Localization/ru.cfg +++ b/RealFuels/Localization/ru.cfg @@ -174,10 +174,13 @@ Localization #RF_FuelTankRF_WallTemp = Температура стенок #RF_FuelTankRF_HeatPenetration = Проникащее тепло #RF_FuelTankRF_BoiloffLoss = Потери на испарение - #RF_FuelTankRF_AnalyticCooling = Аналитическое охлаждение #RF_FuelTankRF_NoMLI = Без ЭВТИ #RF_FuelTankRF_Boiloffunit = кг/ч #RF_FuelTankRF_kerbalismtips = резальтат испарения + #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input + #RF_FuelTankRF_CryoCoolerLift = Cooling Lift + #RF_FuelTankRF_CryoCoolerDraw = Power Draw + #RF_FuelTankRF_CryoCoolerCOP = Avg COP #RF_TankDefineSelection_HighlyPressurized = Бак высокого давления #RF_TankDefineSelection_NotHighlyPressurized = Бак нормальн. давления diff --git a/RealFuels/Localization/zh-cn.cfg b/RealFuels/Localization/zh-cn.cfg index c05e4a4c..13e32b3e 100644 --- a/RealFuels/Localization/zh-cn.cfg +++ b/RealFuels/Localization/zh-cn.cfg @@ -174,10 +174,13 @@ Localization #RF_FuelTankRF_WallTemp = 壁面温度 #RF_FuelTankRF_HeatPenetration = 热渗透 #RF_FuelTankRF_BoiloffLoss = 蒸发损耗 - #RF_FuelTankRF_AnalyticCooling = 冷却分析 #RF_FuelTankRF_NoMLI = 无隔热层 #RF_FuelTankRF_Boiloffunit = 千克/时 #RF_FuelTankRF_kerbalismtips = 蒸发产出 + #RF_FuelTankRF_CryoCoolerInputPower = Cooler Input + #RF_FuelTankRF_CryoCoolerLift = Cooling Lift + #RF_FuelTankRF_CryoCoolerDraw = Power Draw + #RF_FuelTankRF_CryoCoolerCOP = Avg COP #RF_TankDefineSelection_HighlyPressurized = 高压 #RF_TankDefineSelection_NotHighlyPressurized = 非高压 diff --git a/RealFuels/MFSSettings.cfg b/RealFuels/MFSSettings.cfg index 74b112d0..ac865d4f 100644 --- a/RealFuels/MFSSettings.cfg +++ b/RealFuels/MFSSettings.cfg @@ -5,9 +5,7 @@ MFSSETTINGS BatteryMultiplier = 1 basemassUseTotalVolume = True - - radiatorMinTempMult = 0.99 - + IgnoreFuelsForFill { IntakeAir = True diff --git a/RealFuels/Resources/RealTankTypes.cfg b/RealFuels/Resources/RealTankTypes.cfg index 41d3b480..6c295480 100644 --- a/RealFuels/Resources/RealTankTypes.cfg +++ b/RealFuels/Resources/RealTankTypes.cfg @@ -205,7 +205,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0.00000000023 note = (lacks insulation) } TANK @@ -226,7 +225,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0.00000000025 note = (lacks insulation) } TANK @@ -238,7 +236,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0.00000000035 note = (lacks insulation) } TANK @@ -250,7 +247,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000001 note = (lacks insulation) } TANK @@ -262,7 +258,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.0000000055 note = (lacks insulation) } TANK @@ -274,7 +269,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0.00000000017 note = (lacks insulation) } TANK @@ -331,7 +325,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -343,7 +336,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -355,7 +347,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -706,7 +697,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0 note = (has insulation) } TANK @@ -727,7 +717,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0 note = (has insulation) } TANK @@ -739,7 +728,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0 note = (has insulation) } TANK @@ -751,7 +739,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000000016 note = (has insulation) } TANK @@ -763,7 +750,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.000000000088 note = (has insulation) } TANK @@ -775,7 +761,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0 note = (has insulation) } TANK @@ -832,7 +817,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation) } TANK @@ -844,7 +828,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation) } TANK @@ -856,7 +839,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation) } TANK @@ -1250,7 +1232,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1272,7 +1253,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1284,7 +1264,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1296,7 +1275,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000000016 note = (has insulation, pressurized) } TANK @@ -1308,7 +1286,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.000000000088 note = (has insulation, pressurized) } TANK @@ -1320,7 +1297,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1382,7 +1358,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1394,7 +1369,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1406,7 +1380,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -1927,7 +1900,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0.00000000023 note = (lacks insulation) } TANK @@ -1948,7 +1920,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0.00000000025 note = (lacks insulation) } TANK @@ -1960,7 +1931,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0.00000000035 note = (lacks insulation) } TANK @@ -1972,7 +1942,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000001 note = (lacks insulation) } TANK @@ -1984,7 +1953,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.0000000055 note = (lacks insulation) } TANK @@ -1996,7 +1964,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0.00000000017 note = (lacks insulation) } TANK @@ -2053,7 +2020,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -2065,7 +2031,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -2077,7 +2042,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -2478,7 +2442,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -2500,7 +2463,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -2512,7 +2474,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -2524,7 +2485,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000000016 note = (has insulation, pressurized) } TANK @@ -2536,7 +2496,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.000000000088 note = (has insulation, pressurized) isDewar = true } @@ -2549,7 +2508,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -2611,7 +2569,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -2623,7 +2580,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -2635,7 +2591,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation, pressurized) } TANK @@ -3156,7 +3111,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0.00000000023 note = (lacks insulation) } TANK @@ -3177,7 +3131,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0.00000000025 note = (lacks insulation) } TANK @@ -3189,7 +3142,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0.00000000035 note = (lacks insulation) } TANK @@ -3201,7 +3153,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000001 note = (lacks insulation) } TANK @@ -3213,7 +3164,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.0000000055 note = (lacks insulation) } TANK @@ -3225,7 +3175,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0.00000000017 note = (lacks insulation) } TANK @@ -3282,7 +3231,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -3294,7 +3242,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -3306,7 +3253,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0.0000000001 note = (lacks insulation) } TANK @@ -3657,7 +3603,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 180.65 - loss_rate = 0 note = (has insulation) } TANK @@ -3678,7 +3623,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 184.65 - loss_rate = 0 note = (has insulation) } TANK @@ -3690,7 +3634,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 169.45 - loss_rate = 0 note = (has insulation) } TANK @@ -3702,7 +3645,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 128.4 - loss_rate = 0.000000000016 note = (has insulation) } TANK @@ -3714,7 +3656,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 85.04 - loss_rate = 0.000000000088 note = (has insulation) } TANK @@ -3726,7 +3667,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 200.15 - loss_rate = 0 note = (has insulation) } TANK @@ -3783,7 +3723,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation) } TANK @@ -3795,7 +3734,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation) } TANK @@ -3807,7 +3745,6 @@ TANK_DEFINITION amount = 0.0 maxAmount = 0.0 temperature = 216.15 - loss_rate = 0 note = (has insulation) } TANK diff --git a/Source/RealFuels.csproj b/Source/RealFuels.csproj index b3e002cf..4f09880e 100644 --- a/Source/RealFuels.csproj +++ b/Source/RealFuels.csproj @@ -112,11 +112,13 @@ + + @@ -131,6 +133,7 @@ + diff --git a/Source/Tanks/BgBoiloffCache.cs b/Source/Tanks/BgBoiloffCache.cs new file mode 100644 index 00000000..5c2f20b5 --- /dev/null +++ b/Source/Tanks/BgBoiloffCache.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace RealFuels.Tanks +{ + internal sealed class BgBoiloffCache + { + internal readonly int MliLayers; + internal readonly BgTankEntry[] Tanks; + internal readonly double[] InternalTemps; // mutable, parallel to Tanks + internal readonly double[] FluxScratch; // mutable per-tick scratch for Q_kW pre-pass + + internal readonly double CoolerInputKW; // (0 if no cooler installed) + internal readonly double CoolerFrac; // fraction-of-Carnot at CoolerLowestTempK + internal readonly double CoolerLowestTempK; // cold-side T of the cooler + + internal BgBoiloffCache(int mliLayers, BgTankEntry[] tanks, + double coolerInputKW, double coolerFrac, double coolerLowestTempK) + { + MliLayers = mliLayers; + Tanks = tanks; + InternalTemps = new double[tanks.Length]; + FluxScratch = new double[tanks.Length]; + CoolerInputKW = coolerInputKW; + CoolerFrac = coolerFrac; + CoolerLowestTempK = coolerLowestTempK; + } + + internal static BgBoiloffCache Build(string data, string coolerData, int mliLayers) + { + var tanks = new List(); + + foreach (string entry in data.Split(';')) + { + if (string.IsNullOrEmpty(entry)) continue; + string[] split = entry.Split(','); + if (split.Length != 7) continue; + + string resourceName = split[0]; + if (!double.TryParse(split[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double boilingPointK)) continue; + if (!double.TryParse(split[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double tankAreaM2)) continue; + if (!double.TryParse(split[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double conductWPerK)) continue; + if (!int.TryParse(split[4], out int isDewarInt)) continue; + if (!double.TryParse(split[5], NumberStyles.Float, CultureInfo.InvariantCulture, out double hsp)) continue; + if (!double.TryParse(split[6], NumberStyles.Float, CultureInfo.InvariantCulture, out double structThermalMassKJ)) continue; + + PartResourceDefinition resDef = PartResourceLibrary.Instance.GetDefinition(resourceName); + if (resDef == null || resDef.density <= 0d) continue; + if (!MFSSettings.resourceVsps.TryGetValue(resourceName, out double vsp) || vsp <= 0) continue; + + tanks.Add(new BgTankEntry + { + Name = resourceName, + Vsp = vsp, + Density = resDef.density, + BoilingPointK = boilingPointK, + TankAreaM2 = tankAreaM2, + ConductWPerK = conductWPerK, + IsDewar = isDewarInt != 0, + Hsp = hsp, + StructThermalMassKJ = structThermalMassKJ, + }); + } + + double coolerInputKW = 0d, coolerFrac = 0d, coolerLowestTempK = 0d; + if (!string.IsNullOrEmpty(coolerData)) + { + string[] cSplit = coolerData.Split(','); + if (cSplit.Length == 3 + && double.TryParse(cSplit[0], NumberStyles.Float, CultureInfo.InvariantCulture, out coolerInputKW) + && double.TryParse(cSplit[1], NumberStyles.Float, CultureInfo.InvariantCulture, out coolerFrac) + && double.TryParse(cSplit[2], NumberStyles.Float, CultureInfo.InvariantCulture, out coolerLowestTempK)) + { + // ok + } + else + { + coolerInputKW = 0d; coolerFrac = 0d; coolerLowestTempK = 0d; + } + } + + return new BgBoiloffCache(mliLayers, tanks.ToArray(), coolerInputKW, coolerFrac, coolerLowestTempK); + } + + internal void InitTemps(ProtoPartModuleSnapshot proto_module) + { + // Seed InternalTemps from the persisted TANK nodes + var tempLookup = new Dictionary(StringComparer.Ordinal); + foreach (ConfigNode tankNode in proto_module.moduleValues.GetNodes("TANK")) + { + string tName = tankNode.GetValue("name"); + string sVal = tankNode.GetValue("internalTemp"); + if (tName != null && double.TryParse(sVal, NumberStyles.Float, CultureInfo.InvariantCulture, out double t)) + tempLookup[tName] = t; + } + + for (int i = 0; i < Tanks.Length; i++) + { + InternalTemps[i] = tempLookup.TryGetValue(Tanks[i].Name, out double v) && v > 0 + ? v : Tanks[i].BoilingPointK; + } + } + } + + internal struct BgTankEntry + { + internal string Name; + internal double Vsp; // kJ/t + internal double Density; // t/unit + internal double BoilingPointK; + internal double TankAreaM2; // per-tank surface area; used by MLI and Dewar formulas + internal double ConductWPerK; // wall conductance for non-MLI tanks; 0 for MLI/Dewar + internal bool IsDewar; + internal double Hsp; // specific heat capacity, kJ/(t·K) + internal double StructThermalMassKJ; // structural thermal mass contribution for this tank, kJ/K + } +} diff --git a/Source/Tanks/FuelTank.cs b/Source/Tanks/FuelTank.cs index 3450b31e..ec16fe31 100644 --- a/Source/Tanks/FuelTank.cs +++ b/Source/Tanks/FuelTank.cs @@ -16,9 +16,6 @@ namespace RealFuels.Tanks // of tank installed for this resource type. Tons per // volume unit. // temperature the part temperature at which this tank's contents start boiling - // loss_rate How quickly this resource type bleeds out of the tank. - // (TODO: instead of this unrealistic static loss_rate, all - // resources should have vsp (heat of vaporization) added and optionally conduction) // public class FuelTank : IConfigNode @@ -37,11 +34,8 @@ public class FuelTank : IConfigNode public float mass = 0.0f; [Persistent] public float cost = 0.0f; - // TODO Retaining for fallback purposes but should be deprecated - [Persistent] - public double loss_rate = 0.0; - - public double vsp; + public double hsp = 1000d; // specific heat capacity, kJ/(t·K), loaded from RESOURCE_DEFINITION + public double vsp; // heat of vapourization, kJ/t, loaded from RESOURCE_DEFINITION public double resourceConductivity = 10; @@ -62,6 +56,9 @@ public class FuelTank : IConfigNode [Persistent] public float temperature = 300.0f; + [Persistent] + public double internalTemp = -1d; // -1 = uninitialized; set on first OnStart + [Persistent] public bool fillable = true; [Persistent] @@ -308,7 +305,7 @@ public void Load(ConfigNode node) if (node.TryGetValue("boiloffProduct", ref boiloffRes)) boiloffProductResource = PartResourceLibrary.Instance.GetDefinition(boiloffRes); - GetDensity(); + Init(); } public void Save(ConfigNode node) @@ -384,14 +381,17 @@ internal FuelTank CreateCopy(ModuleFuelTanks toModule, ConfigNode overNode, bool else clone.amountExpression = clone.maxAmountExpression = null; - clone.GetDensity(); + clone.Init(); return clone; } - internal void GetDensity() + internal void Init() { PartResourceDefinition d = PartResourceLibrary.Instance.GetDefinition(name); density = (d != null) ? d.density : 0; + + if (!MFSSettings.resourceHsps.TryGetValue(name, out hsp) || hsp <= 0) + hsp = 1000d; } } } diff --git a/Source/Tanks/MFSSettings.cs b/Source/Tanks/MFSSettings.cs index defc4f4c..3bad20c4 100644 --- a/Source/Tanks/MFSSettings.cs +++ b/Source/Tanks/MFSSettings.cs @@ -14,7 +14,6 @@ public class MFSSettings public static bool partUtilizationTweakable = false; public static string unitLabel = "u"; public static bool basemassUseTotalVolume = false; - public static double radiatorMinTempMult = 0.99d; // Move all possible tank upgrades into the preview list in OnStart // It requires an external mod to be responsible for calling the Validate() method. @@ -25,6 +24,7 @@ public class MFSSettings public static readonly Dictionary resourceVsps = new Dictionary(); public static readonly Dictionary resourceConductivities = new Dictionary(); + public static readonly Dictionary resourceHsps = new Dictionary(); private static readonly Dictionary overrides = new Dictionary(); @@ -72,8 +72,9 @@ public static void ModuleManagerPostLoad() { resourceVsps.Clear(); resourceConductivities.Clear(); + resourceHsps.Clear(); - // fill vsps & conductivities + // fill vsps, conductivities & heat capacities foreach (ConfigNode n in GameDatabase.Instance.GetConfigNodes("RESOURCE_DEFINITION")) { string nm = n.GetValue("name"); @@ -82,6 +83,8 @@ public static void ModuleManagerPostLoad() resourceVsps[nm] = dtmp; if (n.TryGetValue("conductivity", ref dtmp)) resourceConductivities[nm] = dtmp; + if (n.TryGetValue("hsp", ref dtmp)) + resourceHsps[nm] = dtmp; } ConfigNode node = GameDatabase.Instance.GetConfigNodes("MFSSETTINGS").LastOrDefault(); @@ -95,7 +98,6 @@ public static void ModuleManagerPostLoad() node.TryGetValue("partUtilizationTweakable", ref partUtilizationTweakable); node.TryGetValue("unitLabel", ref unitLabel); node.TryGetValue("basemassUseTotalVolume", ref basemassUseTotalVolume); - node.TryGetValue("radiatorMinTempMult", ref radiatorMinTempMult); node.TryGetValue("previewAllLockedTypes", ref previewAllLockedTypes); ignoreFuelsForFill.Clear(); diff --git a/Source/Tanks/ModuleFuelTanksRF.cs b/Source/Tanks/ModuleFuelTanksRF.cs index ffa9f6c9..a9af9cec 100644 --- a/Source/Tanks/ModuleFuelTanksRF.cs +++ b/Source/Tanks/ModuleFuelTanksRF.cs @@ -1,6 +1,9 @@ -using KSP.Localization; +using KSP.Localization; +using ROUtils; using System; using System.Collections.Generic; +using System.Globalization; +using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.Profiling; @@ -20,7 +23,8 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr public float _numberOfAddedMLILayers = 0; // This is the number of layers added by the player. public int numberOfAddedMLILayers => (int)_numberOfAddedMLILayers; - public int totalMLILayers => numberOfMLILayers + numberOfAddedMLILayers; + [KSPField(isPersistant = true)] + public int totalMLILayers = 0; [KSPField(isPersistant = true)] protected double totalTankArea; @@ -34,10 +38,24 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr [KSPField(guiName = "#RF_FuelTankRF_BoiloffLoss", groupName = CryogenicGroupName)] // Boil-off Loss public string sBoiloffLoss; - [KSPField(guiName = "#RF_FuelTankRF_AnalyticCooling", groupName = CryogenicGroupName)] // Analytic Cooling - public string sAnalyticCooling; + // Thermal data captured while loaded, used for processing boiloff BackgroundUpdate on unloaded vessels. + // Format per entry: "resourceName,boilingPointK,tankAreaM2,conductWPerK,isDewar" + // boilingPointK — boiling point of the propellant (K) + // tankAreaM2 — per-tank surface area for MLI/Dewar formulas + // conductWPerK — wall conductance W/K for non-MLI tanks (0 for MLI and Dewar) + // isDewar — 1 for Dewar tanks, 0 otherwise + [KSPField(isPersistant = true)] + public string bgBoiloffData = ""; - private double cooling = 0; + // UT of the last Kerbalism BackgroundUpdate tick; used to avoid double-applying boiloff + // when the vessel loads and SetAnalyticTemperature catches up for the unloaded period. + [KSPField(isPersistant = true)] + public double bgBoiloffLastUpdate = 0d; + + // Cryocooler params captured at save time for unloaded BackgroundUpdate use. + // Format: "coolerInputKW,coolerFracAtLowestTemp,lowestTempK" + [KSPField(isPersistant = true)] + public string bgCoolerData = ""; [KSPField] public int maxMLILayers = 10; @@ -48,18 +66,61 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr [KSPField] public float MLIArealDensity = 0.000015f; + [KSPField] + public HermiteCurve cryoCoolerEfficiency = new HermiteCurve(); + + [KSPField] + public float maxCoolerInputKW = 0f; + + [KSPField] + public float coolerBaseMass = 0f; // tonnes, fixed overhead (electronics, housing) + + [KSPField] + public HermiteCurve coolerMassPerKWInput = new HermiteCurve(); // tonnes/kW, keyed to T_cold (K) + + [KSPField] + public float coolerBaseCost = 0f; + + [KSPField] + public HermiteCurve coolerCostPerKWInput = new HermiteCurve(); // funds/kW, keyed to T_cold (K) + + [KSPField(isPersistant = true, guiActiveEditor = true, + guiName = "#RF_FuelTankRF_CryoCoolerInputPower", guiUnits = " kW", guiFormat = "F2", + groupName = CryogenicGroupName, groupDisplayName = CryogenicGroupName), + UI_FloatEdit(minValue = 0f, maxValue = 0f, incrementLarge = 1f, incrementSmall = 0.1f, incrementSlide = 0.01f, sigFigs = 2, + unit = " kW", scene = UI_Scene.Editor)] + public float coolerInputKW = 0f; + + [KSPField(guiName = "#RF_FuelTankRF_CryoCoolerLift", groupName = CryogenicGroupName)] + public string sCoolerLift; + + [KSPField(guiName = "#RF_FuelTankRF_CryoCoolerDraw", groupName = CryogenicGroupName)] + public string sCoolerDraw; + + [KSPField(guiName = "#RF_FuelTankRF_CryoCoolerCOP", groupName = CryogenicGroupName)] + public string sCoolerCOP; // Coefficient of Performance, ratio of the useful cooling provided to the work (energy) required + private double analyticSkinTemp; - private double analyticInternalTemp; private readonly Dictionary boiloffProducts = new Dictionary(); public int numberOfMLILayers = 0; // base number of layers taken from TANK_DEFINITION configs + private double currentCoolerLiftKW; + private double currentCoolerDrawKW; + private double currentCoolerCOP; + private double boiloffMassT = 0d; public double BoiloffMassRate => boiloffMassT; private readonly List cryoTanks = new List(); // anything with maxAmount > 0 && vsp > 0 private readonly List lossInfo = new List(); private readonly List fluxInfo = new List(); + private readonly Dictionary _perTankFlux = new Dictionary(); + private readonly Dictionary _perTankLift = new Dictionary(); + + // Pre-parsed tank boiloff data for background processing. + private static readonly ConditionalWeakTable _bgCache + = new ConditionalWeakTable(); // for EngineIgnitor integration: store a public dictionary of all pressurized propellants [NonSerialized] @@ -69,8 +130,9 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr double lowestTankTemperature = 300d; - private static double ConductionFactors => RFSettings.Instance.globalConductionCompensation ? Math.Max(1.0d, PhysicsGlobals.ConductionFactor) : 1d; public bool SupportsBoiloff => cryoTanks.Count > 0; + public bool SupportCryoCooler => maxCoolerInputKW > 0f; + public bool HasCryoCooler => coolerInputKW > 0f; private bool IsProcedural => part.Modules.Contains("SSTUModularPart") || part.Modules.Contains("WingProcedural"); partial void OnLoadRF(ConfigNode _) { } @@ -96,6 +158,52 @@ partial void OnAwakeRF() } } + partial void OnSaveRF(ConfigNode _) + { + if (!HighLogic.LoadedSceneIsFlight || !SupportsBoiloff) + return; + + double structuralThermalMass = ComputeStructuralThermalMass(); + var entries = new List(cryoTanks.Count); + foreach (var tank in cryoTanks) + { + if (tank.amount <= 0 || tank.vsp <= 0) continue; + + double tankAreaM2 = tank.totalArea; + double conductWPerK = 0; + int isDewar = tank.isDewar ? 1 : 0; + + if (!tank.isDewar && totalMLILayers == 0) + { + double wallF = tank.wallConduction > 0 ? tank.wallThickness / tank.wallConduction : 0; + double insulF = tank.insulationConduction > 0 ? tank.insulationThickness / tank.insulationConduction : 0; + double resF = tank.resourceConductivity > 0 ? 0.01 / tank.resourceConductivity : 0; + conductWPerK = tank.totalArea / Math.Max(double.Epsilon, wallF + insulF + resF); + } + + entries.Add(string.Format(CultureInfo.InvariantCulture, "{0},{1:R},{2:R},{3:R},{4},{5:R},{6:R}", + tank.name, tank.temperature, tankAreaM2, conductWPerK, isDewar, + tank.hsp, structuralThermalMass * tank.tankRatio)); + } + + bgBoiloffData = entries.Count > 0 ? string.Join(";", entries) : ""; + + bgCoolerData = ""; + if (HasCryoCooler) + { + CalculateLowestTankTemperature(); + if (lowestTankTemperature > 0d && lowestTankTemperature < 300d) + { + double frac = cryoCoolerEfficiency.Evaluate(lowestTankTemperature); + if (frac > 0d) + { + bgCoolerData = string.Format(CultureInfo.InvariantCulture, "{0:R},{1:R},{2:R}", + coolerInputKW, frac, lowestTankTemperature); + } + } + } + } + partial void OnStartRF(StartState _) { if (HighLogic.LoadedSceneIsFlight) @@ -103,7 +211,10 @@ partial void OnStartRF(StartState _) foreach (var tank in tanksDict.Values) { - if (tank.maxAmount > 0 && (tank.vsp > 0 || tank.loss_rate > 0)) + if (tank.internalTemp < 0) + tank.internalTemp = tank.vsp > 0 ? tank.temperature : (double.IsNaN(part.temperature) ? 300d : part.temperature); + + if (tank.maxAmount > 0 && tank.vsp > 0) cryoTanks.Add(tank); } CalculateTankArea(); @@ -112,55 +223,47 @@ partial void OnStartRF(StartState _) { Fields[nameof(_numberOfAddedMLILayers)].guiActiveEditor = maxMLILayers > 0; _numberOfAddedMLILayers = Mathf.Clamp(_numberOfAddedMLILayers, 0, maxMLILayers); + totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers; ((UI_FloatRange)Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor).maxValue = maxMLILayers; Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor.onFieldChanged = delegate (BaseField field, object value) { + totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers; massDirty = true; CalculateMass(); }; + + Fields[nameof(coolerInputKW)].guiActiveEditor = SupportCryoCooler; + if (SupportCryoCooler) + { + coolerInputKW = Mathf.Clamp(coolerInputKW, 0f, maxCoolerInputKW); + ((UI_FloatEdit)Fields[nameof(coolerInputKW)].uiControlEditor).maxValue = maxCoolerInputKW; + Fields[nameof(coolerInputKW)].uiControlEditor.onFieldChanged = delegate (BaseField field, object value) + { + massDirty = true; + CalculateMass(); + }; + } } bool debugBoilActive = SupportsBoiloff && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW); Fields[nameof(sWallTemp)].guiActive = debugBoilActive; Fields[nameof(sHeatPenetration)].guiActive = debugBoilActive; Fields[nameof(sBoiloffLoss)].guiActive = debugBoilActive; - Fields[nameof(sAnalyticCooling)].guiActive = debugBoilActive; + + bool coolerFlightActive = HighLogic.LoadedSceneIsFlight && HasCryoCooler; + Fields[nameof(sCoolerLift)].guiActive = coolerFlightActive; + Fields[nameof(sCoolerDraw)].guiActive = coolerFlightActive; + Fields[nameof(sCoolerCOP)].guiActive = coolerFlightActive; GameEvents.onPartResourceListChange.Add(OnPartResourceListChange); GameEvents.onPartDestroyed.Add(OnPartDestroyed); } - private void CalculateInsulation() - { - Profiler.BeginSample("CalculateInsulation"); - // TODO tie this into insulation configuration GUI! Also, we should handle MLI separately and as part skin-internal conduction. (DONE) - // Dewars and SOFI should be handled separately as part of the boiloff code on a per-tank basis (DONE) - // Current SOFI configuration system should be left in place with players able to add to tanks that don't have it. - if (totalMLILayers > 0 && totalVolume > 0 && !(double.IsNaN(part.temperature) || double.IsNaN(part.skinTemperature))) - { - double normalizationFactor = 1 / (PhysicsGlobals.SkinInternalConductionFactor * PhysicsGlobals.ConductionFactor * PhysicsGlobals.ThermalConvergenceFactor * 10 * 0.5); - double tDelta = part.skinTemperature - part.temperature; - if (tDelta == 0d) - tDelta = 0.00000000001d; - double insulationFactor = Math.Abs(GetMLITransferRate(part.skinTemperature, part.temperature) / tDelta) * 0.001; - double condRecip = part.partInfo.partPrefab.skinInternalConductionMult == 0d ? double.MaxValue : (1d / part.partInfo.partPrefab.skinInternalConductionMult); - part.heatConductivity = normalizationFactor * 1 / ((1 / insulationFactor) + condRecip); - CalculateAnalyticInsulationFactor(insulationFactor); - } - Profiler.EndSample(); - } - - private void CalculateAnalyticInsulationFactor(double insulationFactor) - { - double tMassRecip = part.thermalMass == 0d ? 1d : 1d / part.thermalMass; - part.analyticInternalInsulationFactor = _flightIntegrator is FlightIntegrator - ? (1d / PhysicsGlobals.AnalyticLerpRateInternal) * (insulationFactor * totalTankArea * tMassRecip) * RFSettings.Instance.analyticInsulationMultiplier * part.partInfo.partPrefab.analyticInternalInsulationFactor - : 0; - } - partial void CalculateMassRF(ref double mass) { mass += MLIArealDensity * totalTankArea * totalMLILayers; + if (HasCryoCooler) + mass += coolerBaseMass + coolerMassPerKWInput.Evaluate(GetCoolerTargetTemp()) * coolerInputKW; } partial void GetModuleCostRF(ref double cost) @@ -168,16 +271,24 @@ partial void GetModuleCostRF(ref double cost) // Estimate material cost at 0.10764/m2 treating as Fund = $1000 (for RO purposes) // Plus another 0.1 for installation cost += MLIArealCost * totalTankArea * totalMLILayers; + if (HasCryoCooler) + cost += coolerBaseCost + coolerCostPerKWInput.Evaluate(GetCoolerTargetTemp()) * coolerInputKW; } partial void UpdateRF() { - if (HighLogic.LoadedSceneIsFlight && (RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) && SupportsBoiloff && - UIPartActionController.Instance.GetItem(part) != null) + if (!HighLogic.LoadedSceneIsFlight) return; + if (UIPartActionController.Instance.GetItem(part) == null) return; + + if ((RFSettings.Instance.debugBoilOff || RFSettings.Instance.debugBoilOffPAW) && SupportsBoiloff) { - string MLIText = totalMLILayers > 0 ? $"{GetMLITransferRate(part.skinTemperature, part.temperature):F2} W/m²" : Localizer.GetStringByTag("#RF_FuelTankRF_NoMLI"); // "No MLI" - sWallTemp = $"{part.temperature:F2} ({MLIText} * {part.radiativeArea:F2} m²)"; // - sAnalyticCooling = Utilities.FormatFlux(cooling); + sWallTemp = ""; + foreach (var tank in cryoTanks) + sWallTemp += $"{tank.internalTemp:F2} | "; + if (!string.IsNullOrEmpty(sWallTemp)) + sWallTemp = sWallTemp.Remove(sWallTemp.Length - 3); + string MLIText = totalMLILayers > 0 ? $"{GetMLITransferRate(part.skinTemperature, lowestTankTemperature):F2} W/m²" : Localizer.GetStringByTag("#RF_FuelTankRF_NoMLI"); // "No MLI" + sWallTemp += $" ({MLIText} * {part.radiativeArea:F2} m²)"; sHeatPenetration = ""; sBoiloffLoss = ""; @@ -191,43 +302,67 @@ partial void UpdateRF() if (!string.IsNullOrEmpty(sHeatPenetration)) sHeatPenetration = sHeatPenetration.Remove(sHeatPenetration.Length - 3); } + + if (HasCryoCooler) + { + sCoolerLift = Utilities.FormatFlux(currentCoolerLiftKW); + sCoolerDraw = Utilities.FormatFlux(currentCoolerDrawKW); + sCoolerCOP = currentCoolerCOP > 0d ? currentCoolerCOP.ToString("F3") : "—"; + } } public void FixedUpdate() { - //print ("[Real Fuels]" + Time.time.ToString ()); if (HighLogic.LoadedSceneIsFlight && FlightGlobals.ready) { - // MLI performance varies by temperature delta - CalculateInsulation(); - - if(!_flightIntegrator.isAnalytical && SupportsBoiloff) - CalculateTankBoiloff(_flightIntegrator.timeSinceLastUpdate, _flightIntegrator.isAnalytical); + // For analytic case boiloff will run though SetAnalyticTemperature() + if (!_flightIntegrator.isAnalytical && SupportsBoiloff) + ProcessBoiloff(_flightIntegrator.timeSinceLastUpdate); } } - private void HandleCooling(ref double cooling, double deltaTime, bool analyticalMode) + private double GetCoolerTargetTemp() { - cooling = 0; - if (analyticalMode) + double lowest = 300d; + foreach (FuelTank tank in tanksDict.Values) { - 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 - // Questionable since the thermalInternalFlux is already tracking it?? - if (part.thermalMassReciprocal > 0d) - analyticInternalTemp += cooling * part.thermalMassReciprocal * deltaTime; - } + if (tank.amount > 0 && tank.vsp > 0 && tank.temperature < lowest) + lowest = tank.temperature; } + + return lowest; } - private double GetBoiloffTransferRate(double deltaTemp, double wettedArea, in FuelTank tank) + /// + /// Returns incoming heat flux in W for a single tank from the part skin, + /// based on tank type and whether the part has MLI. + /// + private double GetIncomingFlux(double skinTemp, FuelTank tank) { + if (tank.isDewar) + return GetDewarTransferRate(skinTemp, tank.internalTemp, tank.totalArea); + + if (totalMLILayers > 0) + return GetMLITransferRate(skinTemp, tank.internalTemp) * tank.totalArea; + + return GetBoiloffTransferRate(skinTemp, tank.internalTemp, tank.totalArea, tank); + } + + /// + /// Returns the structural thermal mass of the part (kJ/K), i.e. part.thermalMass + /// minus the thermal contribution of all resources using KSP's standard specific heat. + /// + private double ComputeStructuralThermalMass() + { + double resourceThermalMass = 0; + foreach (PartResource res in part.Resources) + resourceThermalMass += res.amount * res.info.density * PhysicsGlobals.StandardSpecificHeatCapacity; + return Math.Max(0, part.thermalMass - resourceThermalMass); + } + + private double GetBoiloffTransferRate(double outerTemperature, double innerTemperature, double wettedArea, in FuelTank tank) + { + double deltaTemp = outerTemperature - innerTemperature; double wallFactor = tank.wallConduction > 0 ? tank.wallThickness / tank.wallConduction : 0; double insulationFactor = tank.insulationConduction > 0 ? tank.insulationThickness / tank.insulationConduction : 0; double resourceFactor = tank.resourceConductivity > 0 ? 0.01 / tank.resourceConductivity : 0; @@ -236,7 +371,7 @@ private double GetBoiloffTransferRate(double deltaTemp, double wettedArea, in Fu return deltaTemp * wettedArea / divisor; } - private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false, double unclampedIntScalar = 0, double unclampedSkinScalar = 0) + private void ProcessBoiloff(double deltaTime, bool analyticalMode = false) { Profiler.BeginSample("CalculateTankBoiloff"); if (totalTankArea <= 0) @@ -245,9 +380,11 @@ private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false, CalculateTankArea(); } - if (double.IsNaN(part.temperature)) + double skinTemp = analyticalMode ? analyticSkinTemp : part.skinTemperature; + + if (double.IsNaN(skinTemp)) { - Debug.LogError($"RF: CalculateTankBoiloff found NaN part.temperature on {part}"); + Debug.LogError($"RF: CalculateTankBoiloff found NaN skinTemperature on {part}"); Profiler.EndSample(); return; } @@ -255,118 +392,195 @@ private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false, boiloffMassT = 0d; lossInfo.Clear(); fluxInfo.Clear(); + _perTankFlux.Clear(); + _perTankLift.Clear(); bool hasCryoFuels = CalculateLowestTankTemperature(); - if (hasCryoFuels && MFSSettings.radiatorMinTempMult >= 0d) - part.radiatorMax = lowestTankTemperature * MFSSettings.radiatorMinTempMult / part.maxTemp; if (fueledByLaunchClamp) { if (hasCryoFuels) { - if (analyticalMode) - analyticInternalTemp = lowestTankTemperature; - else - part.temperature = lowestTankTemperature; - // part.skinTemperature or analyticSkinTemp ? Nah. + foreach (var tank in cryoTanks) + tank.internalTemp = tank.temperature; } fueledByLaunchClamp = false; + currentCoolerLiftKW = 0d; + currentCoolerDrawKW = 0d; + currentCoolerCOP = 0d; Profiler.EndSample(); return; } - if (deltaTime > 0 && !CheatOptions.InfinitePropellant) + if (deltaTime <= 0 || CheatOptions.InfinitePropellant) { - //Debug.Log($"internalFlux = {part.thermalInternalFlux}, thermalInternalFluxPrevious = {part.thermalInternalFluxPrevious}, analytic internal flux = {previewInternalFluxAdjust}"); - HandleCooling(ref cooling, deltaTime, analyticalMode); + Profiler.EndSample(); + return; + } - foreach (var tank in cryoTanks) - { - double tankAmount = tank.amount; - if (tankAmount > 0 && tank.vsp > 0) - { - double massLost = 0; - double hotTemp = part.temperature; + // TODO: structuralThermalMass should be split up per-tank + // TODO2: KSP will internally still assign a part.thermalMass value that includes resources + double structuralThermalMass = ComputeStructuralThermalMass(); - // We might be in analytic mode, and have a target temperature = analyticInternalTemp/analyticSkinTemp, and "progress" towards it reprsented by the scalar params - if (analyticalMode) - { - hotTemp = UtilMath.Lerp(part.temperature, analyticInternalTemp, Math.Min(1, unclampedIntScalar / 2)); - DebugLog($"[MFTRF] CalculateBoiloff.Analytic using adjusted temp {hotTemp:F1} from {part.temperature:F1} towards {analyticInternalTemp:F1} based on scalar {unclampedIntScalar:F2}"); - } + // Pre-pass: compute per-tank incoming flux and tally what the cryocooler could usefully lift. + // Only at-boiling cryo tanks are coolable; sub-boiling tanks are left to warm normally + // (no subcooling), and non-cryo tanks aren't cooled. + double totalCoolableKW = 0d; + foreach (FuelTank tank in tanksDict.Values) + { + double q = GetIncomingFlux(skinTemp, tank) * 0.001d; + _perTankFlux[tank] = q; + if (tank.vsp > 0 && tank.amount > 0 && q > 0 && tank.internalTemp >= tank.temperature) + totalCoolableKW += q; + } - double deltaTemp = hotTemp - tank.temperature; - double Q = 0; - if (deltaTemp > 0) - { - double wettedArea = tank.totalArea; // disabled until proper wetted vs ullage conduction can be done (tank.amount / tank.maxAmount); - Q = tank.isDewar ? GetDewarTransferRate(hotTemp, tank.temperature, tank.totalArea) - : GetBoiloffTransferRate(deltaTemp, wettedArea, tank); + double totalLiftKW = 0d, totalInputKW = 0d; + if (hasCryoFuels) + ApplyCryocooling(deltaTime, skinTemp, totalCoolableKW, out totalLiftKW, out totalInputKW); - Q *= 0.001d; // convert to kilowatts - massLost = Q / tank.vsp; + double totalAbsorbedQ_kW = 0d; + foreach (FuelTank tank in tanksDict.Values) + { + double qIn = _perTankFlux.TryGetValue(tank, out double qv) ? qv : 0d; + double lift = _perTankLift.TryGetValue(tank, out double lv) ? lv : 0d; + totalAbsorbedQ_kW += CalculateBoiloffForTank(tank, qIn - lift, deltaTime, structuralThermalMass); + } - lossInfo.Add(massLost * 1000 * 3600); - fluxInfo.Add(Q); - massLost *= deltaTime; // Frame scaling - } + currentCoolerLiftKW = totalLiftKW; + currentCoolerDrawKW = totalInputKW; + currentCoolerCOP = totalInputKW > 0d ? totalLiftKW / totalInputKW : 0d; + + // Skin energy balance: + // skin → tanks: -totalAbsorbedQ_kW (net flux absorbed by tanks, after any cooling) + // tanks → skin via cooler: +totalLiftKW (heat pumped out of tanks) + // EC → skin via cooler: +totalInputKW (electrical work turns into heat in the warm end) + // AddSkinThermalFlux takes kW and multiplies by TimeWarp.fixedDeltaTime internally, + // so passing the rate is correct here. Skipped in analytic mode. + if (!analyticalMode) + { + double skinFlux = -totalAbsorbedQ_kW + totalLiftKW + totalInputKW; + if (skinFlux != 0d) + part.AddSkinThermalFlux(skinFlux); + } - double d = tank.density > 0 ? tank.density : 1; - double lossAmount = massLost / d; - lossAmount = Math.Min(lossAmount, tankAmount); + Profiler.EndSample(); + } - if (lossAmount > 0) + private void ApplyCryocooling(double deltaTime, double skinTemp, double totalCoolableKW, out double totalLiftKW, out double totalInputKW) + { + // Cryocooler allocation: COP = (T_cold / (T_hot - T_cold)) * fraction-of-Carnot curve + totalLiftKW = 0d; + totalInputKW = 0d; + if (HasCryoCooler && totalCoolableKW > 0d && skinTemp > lowestTankTemperature) + { + double carnot = lowestTankTemperature / (skinTemp - lowestTankTemperature); + double frac = cryoCoolerEfficiency.Evaluate(lowestTankTemperature); + double cop = Math.Max(0d, carnot * frac); + if (cop > 0d) + { + double maxLiftKW = coolerInputKW * cop; + double wantedLiftKW = Math.Min(maxLiftKW, totalCoolableKW); + double wantedInputKW = wantedLiftKW / cop; + double requestedEC = wantedInputKW * deltaTime; + double receivedEC = requestedEC > 0d + ? part.RequestResource("ElectricCharge", requestedEC, ResourceFlowMode.ALL_VESSEL) + : 0d; + + double scale = requestedEC > 0d ? receivedEC / requestedEC : 0d; + if (scale < 0d) scale = 0d; + else if (scale > 1d) scale = 1d; + + totalLiftKW = wantedLiftKW * scale; + totalInputKW = wantedInputKW * scale; + + if (totalLiftKW > 0d) + { + double liftFrac = totalLiftKW / totalCoolableKW; + foreach (FuelTank tank in cryoTanks) { - // operate directly with the PartResource because FuelTank.amount isn't really meant for in-flight resource consumption - tank.resource.amount -= lossAmount; - - // See if there is boiloff byproduct and see if any other parts want to accept it. - if (tank.boiloffProductResource != null) + if (tank.amount > 0 && tank.vsp > 0 + && tank.internalTemp >= tank.temperature + && _perTankFlux.TryGetValue(tank, out double q) && q > 0) { - double boiloffProductAmount = -(massLost / tank.boiloffProductResource.density); - double retainedAmount = part.RequestResource(tank.boiloffProductResource.id, boiloffProductAmount, ResourceFlowMode.STAGE_PRIORITY_FLOW, Utilities.KerbalismFound); - massLost -= retainedAmount * tank.boiloffProductResource.density; - - if (Utilities.KerbalismFound) - { - string rName = tank.boiloffProductResource.name; - retainedAmount /= deltaTime; - boiloffProducts[rName] = boiloffProducts.TryGetValue(rName, out double v) ? v + retainedAmount : retainedAmount; - } + _perTankLift[tank] = q * liftFrac; } } + } + } + } + } - boiloffMassT += massLost; + private double CalculateBoiloffForTank(FuelTank tank, double Q_kW, double deltaTime, double structuralThermalMass) + { + if (tank.totalArea <= 0 || tank.tankRatio <= 0) + return 0d; - // subtract heat from boiloff - // subtracting heat in analytic mode is tricky: Analytic flux handling is 'cheaty' and tricky to predict. + double thermalMass = structuralThermalMass * tank.tankRatio + + tank.amount * tank.density * tank.hsp; + thermalMass = Math.Max(thermalMass, 1.0); - if (Q > 0) - { - double heatLost = -Q; - if (!analyticalMode) - part.AddThermalFlux(heatLost); - else - { - analyticInternalTemp += heatLost * part.thermalMassReciprocal * deltaTime; - DebugLog($"{part.name} deltaTime = {deltaTime:F2}s, heat lost = {heatLost:F4}, thermalMassReciprocal = {part.thermalMassReciprocal:F6}"); - } - } - } - else if (tankAmount > 0 && tank.loss_rate > 0) + if (tank.vsp <= 0 || tank.internalTemp < tank.temperature) + { + if (tank.vsp <= 0) + { + // Non-cryo: clamp internalTemp at skinTemp to prevent overshoot. + // At high warp the large deltaTime can push internalTemp past skinTemp in a + // single step; the resulting oscillation with asymmetric flux handling bleeds + // energy from the skin each cycle, eventually driving it to 0 K (or infinity). + double skinTemp = part.skinTemperature; + double prevTemp = tank.internalTemp; + double newTemp = prevTemp + Q_kW * deltaTime / thermalMass; + newTemp = Q_kW >= 0 ? Math.Min(newTemp, skinTemp) : Math.Max(newTemp, skinTemp); + tank.internalTemp = newTemp; + return (newTemp - prevTemp) * thermalMass / deltaTime; + } + else + { + // Sub-boiling cryo: heat toward boiling point + tank.internalTemp += Q_kW * deltaTime / thermalMass; + if (tank.internalTemp > tank.temperature) + tank.internalTemp = tank.temperature; + return Q_kW > 0 ? Q_kW : 0d; + } + } + else + { + // At or above boiling point: phase transition holds temperature, all flux → boiloff + tank.internalTemp = tank.temperature; + + double tankAmount = tank.amount; + if (tankAmount <= 0 || Q_kW <= 0) return 0d; + + double massLost = Q_kW / tank.vsp * deltaTime; + + lossInfo.Add(Q_kW / tank.vsp * 1000d * 3600d); // kg/hr for display + fluxInfo.Add(Q_kW); + + double d = tank.density > 0 ? tank.density : 1; + double lossAmount = Math.Min(massLost / d, tankAmount); + + if (lossAmount > 0) + { + tank.resource.amount -= lossAmount; + + if (tank.boiloffProductResource != null) { - double deltaTemp = part.temperature - tank.temperature; - if (deltaTemp > 0) + double boiloffProductAmount = -(massLost / tank.boiloffProductResource.density); + double retainedAmount = part.RequestResource(tank.boiloffProductResource.id, boiloffProductAmount, ResourceFlowMode.STAGE_PRIORITY_FLOW, Utilities.KerbalismFound); + massLost -= retainedAmount * tank.boiloffProductResource.density; + + if (Utilities.KerbalismFound) { - double lossAmount = tank.maxAmount * tank.loss_rate * deltaTemp * deltaTime; - lossAmount = Math.Min(lossAmount, tankAmount); - tank.resource.amount -= lossAmount; - boiloffMassT += lossAmount * tank.density; + string rName = tank.boiloffProductResource.name; + retainedAmount /= deltaTime; + boiloffProducts[rName] = boiloffProducts.TryGetValue(rName, out double v) ? v + retainedAmount : retainedAmount; } } } + + boiloffMassT += massLost; + return Q_kW; } - Profiler.EndSample(); } partial void UpdateTankTypeRF(TankDefinition def) @@ -385,6 +599,7 @@ partial void UpdateTankTypeRF(TankDefinition def) _numberOfAddedMLILayers = Mathf.Clamp(_numberOfAddedMLILayers, 0, maxMLILayers); ((UI_FloatRange)Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor).maxValue = maxMLILayers; } + totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers; InitUtilization(); @@ -407,11 +622,10 @@ private void UpdateEngineIgnitor(TankDefinition def) public void OnResourceMaxChanged(BaseEventDetails _) => CalculateTankArea(); private void OnPartResourceListChange(Part p) { - if (p == part) + if (p == part) CalculateTankArea(); } - // This is how you update drag cubes, we shouldn't be the service for this, but left-over code. private void UpdateDragCubes() { DragCube dragCube = DragCubeSystem.Instance.RenderProceduralDragCube(part); @@ -423,15 +637,8 @@ private void UpdateDragCubes() 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 SetTankAreaInfo(volume); - // 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 areaSpherical = SphericalAreaFromVolume(totalVolume); double areaPartsSpherical = CalculateTankAreaFromSphericalSubTanks(); @@ -464,30 +671,35 @@ private bool CalculateLowestTankTemperature() } #region IAnalyticTemperatureModifier - // Analytic Interface public void SetAnalyticTemperature(FlightIntegrator fi, double analyticTemp, double predictedInternalTemp, double predictedSkinTemp) { analyticSkinTemp = predictedSkinTemp; - analyticInternalTemp = predictedInternalTemp; if (SupportsBoiloff) { - DebugLog($"{part.name} Analytic Temp = {analyticTemp:F2}, Analytic Internal = {predictedInternalTemp:F2}, Analytic Skin = {predictedSkinTemp:F2}"); - double lerpScalarInt = PhysicsGlobals.AnalyticLerpRateInternal * fi.timeSinceLastUpdate; - double lerpScalarSkin = PhysicsGlobals.AnalyticLerpRateSkin * fi.timeSinceLastUpdate; - double skinScalar = lerpScalarSkin * part.analyticSkinInsulationFactor; - double intScalar = lerpScalarInt * part.analyticInternalInsulationFactor; - // A value of 1.0 (unclamped) indicates the time that has passed == the expected time to equalize temperatures - // For values <= 1-ish, we may consider trying to scale the temp progress down by accounting for boiloff. - // Alternatively, just adjust the analytic output using the boiloff calculation anyway. + DebugLog($"{part.name} Analytic Temp = {analyticTemp:F2}, Analytic Skin = {predictedSkinTemp:F2}"); if (fi.timeSinceLastUpdate < double.MaxValue) - CalculateTankBoiloff(fi.timeSinceLastUpdate, fi.isAnalytical, intScalar, skinScalar); + { + double remainingTime; + if (bgBoiloffLastUpdate > 0d) + { + remainingTime = Math.Max(0d, Planetarium.GetUniversalTime() - bgBoiloffLastUpdate); + bgBoiloffLastUpdate = 0d; + } + else + { + remainingTime = fi.timeSinceLastUpdate; + } + + if (remainingTime > 0d) + ProcessBoiloff(remainingTime, fi.isAnalytical); + } else if (CalculateLowestTankTemperature()) { - // Vessel is freshly spawned and has cryogenic tanks, set temperatures appropriately - analyticSkinTemp = lowestTankTemperature; - analyticInternalTemp = lowestTankTemperature; + // Vessel is freshly spawned with cryogenic tanks — initialise per-tank state only + foreach (var tank in cryoTanks) + tank.internalTemp = tank.temperature; } } } @@ -498,42 +710,21 @@ public double GetSkinTemperature(out bool lerp) return Math.Max(analyticSkinTemp, PhysicsGlobals.SpaceTemperature); } + // Return skin temp as the internal temp: the part structure has no meaningful insulation + // from the skin surface, so it equilibrates with it. Propellant thermal state is managed + // independently via per-tank internalTemp and must not inflate part.thermalMass here. public double GetInternalTemperature(out bool lerp) { lerp = false; - return Math.Max(analyticInternalTemp, PhysicsGlobals.SpaceTemperature); + return Math.Max(analyticSkinTemp, PhysicsGlobals.SpaceTemperature); } #endregion #region Analytic Preview Interface - - // We don't really implement this interface anymore. - // Boiloff should not be a significant portion of the flux generation that it will actively keep - // the vessel cool and will change the steady-state temperature that is being calculated/previewed here. - // Diff-Eq problem: this calculates the steady-state temp, of which boiloff result is an input. - // However, boiloff as a resource can be consumed, so the amount of time to target this steady state changes. - // - // Normally called every FixedUpdate by FlightIntegrator in Analytic Mode - // May be called outside of Analytic Mode if part.temp/part.skinTemp were out of bounds public void AnalyticInfo(FlightIntegrator fi, double sunAndBodyIn, double backgroundRadiation, double radArea, double absEmissRatio, double internalFlux, double convCoeff, double ambientTemp, double maxPartTemp) { if (!fi.isAnalytical) Debug.Log($"[MFTRF] AnalyticInfo called in non-analytic mode for {vessel}. dT: {fi.timeSinceLastUpdate:F2}s"); - /* - if (TimeWarp.CurrentRate == 1) - DebugLog("AnalyticInfo being called with: sunAndBodyIn = " + sunAndBodyIn.ToString() - + ", backgroundRadiation = " + backgroundRadiation.ToString() - + ", radArea = "+ radArea.ToString() - + ", absEmissRatio = " + absEmissRatio.ToString() - + ", internalFlux = " + internalFlux.ToString() - + ", convCoeff = " + convCoeff.ToString() - + ", ambientTemp = " + ambientTemp.ToString() - + ", maxPartTemp = " + maxPartTemp.ToString() - ); - //float deltaTime = (float)(Planetarium.GetUniversalTime() - vessel.lastUT); - //if (this.supportsBoiloff) - // CalculateTankBoiloff(TimeWarp.fixedDeltaTime, true); - */ } public double InternalFluxAdjust() => 0; @@ -551,28 +742,30 @@ void DebugLog(string msg) /// /// Transfer rate through multilayer insulation in watts/m2 via radiation, conduction and convection (conduction through gas in the layers). - /// Default hot and cold values of 300 / 70. Can be called in real time substituting skin temp and internal temp for hot and cold. + /// Can be called in real time substituting skin temp and internal temp for hot and cold. /// - private double GetMLITransferRate(double outerTemperature = 300, double innerTemperature = 70) - { - // - double QrCoefficient = 0.0000000004944; // typical MLI radiation flux coefficient - double QcCoefficient = 0.0000000895; // typical MLI conductive flux coefficient. Possible tech upgrade target based on spacing mechanism between layers? - //double QvCoefficient = 3.65; // 14.600; // 14600; // not even sure how this is right: convective contribution will be MURDEROUS. - double emissivity = 0.03; // typical reflective mylar emissivity...? - double layerDensity = 10.055; //14.99813f; // 8.51f; // layer density (layers/cm) + private double GetMLITransferRate(double outerTemperature, double innerTemperature) + => GetMLITransferRate(outerTemperature, innerTemperature, totalMLILayers, vessel.staticPressurekPa); - double radiation = (QrCoefficient * emissivity * (Math.Pow(outerTemperature, 4.67) - Math.Pow(innerTemperature, 4.67))) / totalMLILayers; - double conduction = ((QcCoefficient * Math.Pow(layerDensity, 2.63) * ((outerTemperature + innerTemperature) / 2)) / (totalMLILayers + 1)) * (outerTemperature - innerTemperature); - double convection = RFSettings.Instance.QvCoefficient * ((vessel.staticPressurekPa * 7.500616851) * (Math.Pow(outerTemperature, 0.52) - Math.Pow(innerTemperature, 0.52))) / totalMLILayers; - return radiation + conduction + convection; + private static double GetMLITransferRate(double outerTemp, double innerTemp, int mliLayers, double pressureKPa = 0) + { + const double QrCoefficient = 0.0000000004944; // typical MLI radiation flux coefficient + const double QcCoefficient = 0.0000000895; // typical MLI conductive flux coefficient + const double emissivity = 0.03; // typical reflective mylar emissivity + const double layerDensity = 10.055; // layer density (layers/cm) + + double radiation = QrCoefficient * emissivity * (Math.Pow(outerTemp, 4.67) - Math.Pow(innerTemp, 4.67)) / mliLayers; + double conduction = QcCoefficient * Math.Pow(layerDensity, 2.63) * ((outerTemp + innerTemp) / 2) / (mliLayers + 1) * (outerTemp - innerTemp); + double result = radiation + conduction; + if (pressureKPa > 0) + result += RFSettings.Instance.QvCoefficient * (pressureKPa * 7.500616851) * (Math.Pow(outerTemp, 0.52) - Math.Pow(innerTemp, 0.52)) / mliLayers; + return result; } /// - /// Transfer rate through Dewar walls - /// This is simplified down to basic radiation formula using corrected emissivity values for concentric walls for sake of performance + /// Transfer rate through Dewar walls via radiation across the vacuum gap. /// - private double GetDewarTransferRate(double hot, double cold, double area) + private static double GetDewarTransferRate(double hot, double cold, double area) { // TODO Just radiation now; need to calculate conduction through piping/lid, etc double emissivity = 0.005074871897; // corrected and rounded value for concentric surfaces, actual emissivity of each surface is assumed to be 0.01 for silvered or aluminized coating @@ -582,13 +775,168 @@ private double GetDewarTransferRate(double hot, double cold, double area) #endregion #region Kerbalism + /// - /// Called by Kerbalism every frame. Uses their resource system when Kerbalism is installed. + /// Called by Kerbalism for unloaded (background) vessels via reflection. + /// For each cryo propellant, computes boiloff directly from the Kerbalism vessel temperature + /// using the appropriate heat-transfer formula (MLI, conduction, or Dewar radiation). /// - public virtual string ResourceUpdate(Dictionary availableResources, List> resourceChangeRequest) + public static string BackgroundUpdate( + Vessel vessel, + ProtoPartSnapshot proto_part, + ProtoPartModuleSnapshot proto_module, + PartModule partModule, + Part part, + Dictionary availableResources, + List> resourceChangeRequest, + double elapsed_s) { - //resourceChangeRequest.Clear(); + bool hasGeometry = KerbalismInterface.TryGetThermalData(vessel, out double vesselTemp, out _); + if (!hasGeometry) return string.Empty; + + if (!_bgCache.TryGetValue(proto_module, out BgBoiloffCache cache)) + { + string data = proto_module.moduleValues.GetValue(nameof(bgBoiloffData)); + if (string.IsNullOrEmpty(data)) return string.Empty; + + string coolerData = proto_module.moduleValues.GetValue(nameof(bgCoolerData)) ?? ""; + int.TryParse(proto_module.moduleValues.GetValue(nameof(totalMLILayers)), out int mliLayers); + cache = BgBoiloffCache.Build(data, coolerData, mliLayers); + cache.InitTemps(proto_module); + + _bgCache.Remove(proto_module); + _bgCache.Add(proto_module, cache); + } + + double totalCoolableKW = ProcessBackgroundHeatLeakage(vesselTemp, cache); + double liftFrac = ProcessBackgroundCryocooling(availableResources, resourceChangeRequest, elapsed_s, vesselTemp, cache, totalCoolableKW); + bool anyBoiloff = ApplyBackgroundTankFlux(availableResources, resourceChangeRequest, elapsed_s, cache, liftFrac); + PersistBackgroundTankTemps(proto_module, cache); + + proto_module.moduleValues.SetValue("bgBoiloffLastUpdate", + Planetarium.GetUniversalTime().ToString(CultureInfo.InvariantCulture)); + + return anyBoiloff ? Localizer.GetStringByTag("#RF_FuelTankRF_kerbalismtips") : string.Empty; + } + + /// + /// compute per-tank Q_kW and tally coolable demand + /// + /// + /// + /// + private static double ProcessBackgroundHeatLeakage(double vesselTemp, BgBoiloffCache cache) + { + double totalCoolableKW = 0d; + for (int i = 0; i < cache.Tanks.Length; i++) + { + BgTankEntry entry = cache.Tanks[i]; + double internalTemp = cache.InternalTemps[i]; + double q; + if (entry.IsDewar) + q = GetDewarTransferRate(vesselTemp, internalTemp, entry.TankAreaM2) * 0.001; + else if (cache.MliLayers > 0 && entry.TankAreaM2 > 0) + q = GetMLITransferRate(vesselTemp, internalTemp, cache.MliLayers) * entry.TankAreaM2 * 0.001; + else if (entry.ConductWPerK > 0) + q = entry.ConductWPerK * (vesselTemp - internalTemp) * 0.001; + else + q = 0d; + + cache.FluxScratch[i] = q; + if (q > 0 && internalTemp >= entry.BoilingPointK) + totalCoolableKW += q; + } + + return totalCoolableKW; + } + + private static double ProcessBackgroundCryocooling(Dictionary availableResources, List> resourceChangeRequest, double elapsed_s, double vesselTemp, BgBoiloffCache cache, double totalCoolableKW) + { + double liftFrac = 0d; + if (cache.CoolerInputKW > 0d && cache.CoolerFrac > 0d && totalCoolableKW > 0d + && vesselTemp > cache.CoolerLowestTempK) + { + double carnot = cache.CoolerLowestTempK / (vesselTemp - cache.CoolerLowestTempK); + double cop = carnot * cache.CoolerFrac; + if (cop > 0d) + { + double maxLiftKW = cache.CoolerInputKW * cop; + double wantedLiftKW = Math.Min(maxLiftKW, totalCoolableKW); + double wantedInputKW = wantedLiftKW / cop; + double ecAvail = availableResources.TryGetValue("ElectricCharge", out double ev) ? ev : 0d; + double requestedEC = wantedInputKW * elapsed_s; + double scale = requestedEC > 0d ? Math.Min(1d, ecAvail / requestedEC) : 0d; + if (scale < 0d) scale = 0d; + double actualInputKW = wantedInputKW * scale; + double actualLiftKW = wantedLiftKW * scale; + if (actualInputKW > 0d) + resourceChangeRequest.Add(new KeyValuePair("ElectricCharge", -actualInputKW)); + if (actualLiftKW > 0d) + liftFrac = actualLiftKW / totalCoolableKW; + } + } + + return liftFrac; + } + + private static bool ApplyBackgroundTankFlux(Dictionary availableResources, List> resourceChangeRequest, double elapsed_s, BgBoiloffCache cache, double liftFrac) + { + bool anyRequest = false; + for (int i = 0; i < cache.Tanks.Length; i++) + { + BgTankEntry entry = cache.Tanks[i]; + double internalTemp = cache.InternalTemps[i]; + double Q_kW = cache.FluxScratch[i]; + + if (Q_kW > 0d && internalTemp >= entry.BoilingPointK && liftFrac > 0d) + Q_kW -= Q_kW * liftFrac; + + if (Q_kW == 0d) continue; + + if (internalTemp < entry.BoilingPointK) + { + // Sub-boiling: advance internalTemp through thermal mass + double amount = availableResources.TryGetValue(entry.Name, out double a) ? a : 0d; + double thermalMass = entry.StructThermalMassKJ + amount * entry.Density * entry.Hsp; + thermalMass = Math.Max(thermalMass, 1.0); + cache.InternalTemps[i] = Math.Min(internalTemp + Q_kW * elapsed_s / thermalMass, entry.BoilingPointK); + } + else if (Q_kW > 0) + { + // At boiling point: residual flux drives boiloff + double rateKgS = Q_kW / entry.Vsp; + resourceChangeRequest.Add(new KeyValuePair(entry.Name, -rateKgS / entry.Density)); + anyRequest = true; + } + } + + return anyRequest; + } + + private static void PersistBackgroundTankTemps(ProtoPartModuleSnapshot proto_module, BgBoiloffCache cache) + { + foreach (ConfigNode tankNode in proto_module.moduleValues.GetNodes("TANK")) + { + string tName = tankNode.GetValue("name"); + if (tName == null) continue; + for (int i = 0; i < cache.Tanks.Length; i++) + { + if (cache.Tanks[i].Name == tName) + { + var sTemp = cache.InternalTemps[i].ToString("R", CultureInfo.InvariantCulture); + tankNode.SetValue("internalTemp", sTemp, true); + break; + } + } + } + } + + /// + /// Called by Kerbalism every frame for loaded vessels. Uses their resource system when Kerbalism is installed. + /// + public virtual string ResourceUpdate(Dictionary availableResources, List> resourceChangeRequest) + { foreach (var resourceRequest in boiloffProducts) { var definition = PartResourceLibrary.Instance.GetDefinition(resourceRequest.Key); @@ -601,6 +949,7 @@ public virtual string ResourceUpdate(Dictionary availableResourc return Localizer.GetStringByTag("#RF_FuelTankRF_kerbalismtips"); // "boiloff product" } + #endregion #region Tank Dimensions diff --git a/Source/Utilities/KerbalismInterface.cs b/Source/Utilities/KerbalismInterface.cs new file mode 100644 index 00000000..a2f8d1f3 --- /dev/null +++ b/Source/Utilities/KerbalismInterface.cs @@ -0,0 +1,74 @@ +using System; +using System.Reflection; +using UnityEngine; + +namespace RealFuels +{ + public static class KerbalismInterface + { + private static bool _initialized; + private static Func _getVesselData; + private static Func _getVesselTemperature; + private static Func _getVesselSurfaceArea; + + public static bool TryGetThermalData(Vessel vessel, out double vesselTemp, out double surfaceArea) + { + vesselTemp = 0; + surfaceArea = -1; + + if (!_initialized) + Initialize(); + + if (_getVesselData == null || _getVesselTemperature == null || _getVesselSurfaceArea == null) + return false; + + try + { + object vd = _getVesselData(vessel); + if (vd == null) return false; + vesselTemp = _getVesselTemperature(vd); + surfaceArea = _getVesselSurfaceArea(vd); + return surfaceArea > 0; + } + catch + { + return false; + } + } + + private static void Initialize() + { + _initialized = true; + try + { + Assembly kbAsm = null; + foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) + { + if (string.Equals(new AssemblyName(a.FullName).Name, "Kerbalism", StringComparison.OrdinalIgnoreCase)) + { + kbAsm = a; + break; + } + } + if (kbAsm == null) return; + + Type dbType = kbAsm.GetType("KERBALISM.DB"); + MethodInfo vesselDataMethod = dbType?.GetMethod("KerbalismData", BindingFlags.Public | BindingFlags.Static, null, new[] { typeof(Vessel) }, null); + Type vdType = kbAsm.GetType("KERBALISM.VesselData"); + PropertyInfo tempProp = vdType?.GetProperty("VesselTemperature"); + PropertyInfo areaProp = vdType?.GetProperty("VesselSurfaceArea"); + + if (vesselDataMethod != null) + _getVesselData = ReflectionHelpers.BuildStaticMethodDelegate(vesselDataMethod); + if (tempProp != null) + _getVesselTemperature = ReflectionHelpers.BuildPropertyGetter(tempProp); + if (areaProp != null) + _getVesselSurfaceArea = ReflectionHelpers.BuildPropertyGetter(areaProp); + } + catch (Exception ex) + { + Debug.LogWarning($"[RF] KerbalismInterface reflection init failed: {ex}"); + } + } + } +} diff --git a/Source/Utilities/ReflectionHelpers.cs b/Source/Utilities/ReflectionHelpers.cs new file mode 100644 index 00000000..8cdacabd --- /dev/null +++ b/Source/Utilities/ReflectionHelpers.cs @@ -0,0 +1,82 @@ +using System; +using System.Reflection; +using System.Reflection.Emit; + +namespace RealFuels +{ + public static class ReflectionHelpers + { + /// + /// Create a getter for an instance or static field. Only works with fields declared in a class (won't work for struct fields).
+ ///
+ /// The field type + /// The field info + /// An func delegate where the argument is the class instance (or null for a static field) and the return value is the field value + public static Func CreateFieldGetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".get_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(T), new Type[1] { typeof(object) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + if (field.IsStatic) + { + gen.Emit(OpCodes.Ldsfld, field); + } + else + { + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldfld, field); + } + gen.Emit(OpCodes.Ret); + return (Func)setterMethod.CreateDelegate(typeof(Func)); + } + + /// + /// Create a setter for an instance or static field. Only works with fields declared in a class (won't work for struct fields). + /// + /// The field type + /// The field info + /// An action delegate where the first argument is the class instance (or null for a static field) and the second argument is the new value + public static Action CreateFieldSetter(FieldInfo field) + { + string methodName = field.ReflectedType.FullName + ".set_" + field.Name; + DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2] { typeof(object), typeof(T) }, true); + ILGenerator gen = setterMethod.GetILGenerator(); + if (field.IsStatic) + { + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stsfld, field); + } + else + { + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Ldarg_1); + gen.Emit(OpCodes.Stfld, field); + } + gen.Emit(OpCodes.Ret); + return (Action)setterMethod.CreateDelegate(typeof(Action)); + } + + public static Func BuildPropertyGetter(PropertyInfo prop) + { + MethodInfo getter = prop.GetGetMethod(nonPublic: true); + var dm = new DynamicMethod(prop.DeclaringType.FullName + ".get_" + prop.Name, + typeof(T), new[] { typeof(object) }, true); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Callvirt, getter); + il.Emit(OpCodes.Ret); + return (Func)dm.CreateDelegate(typeof(Func)); + } + + public static Func BuildStaticMethodDelegate(MethodInfo method) + { + var dm = new DynamicMethod(method.DeclaringType.FullName + "." + method.Name, + typeof(object), new[] { typeof(Vessel) }, true); + ILGenerator il = dm.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Call, method); + il.Emit(OpCodes.Ret); + return (Func)dm.CreateDelegate(typeof(Func)); + } + } +}