From 9d8910f0ccd1915b39cd73e9989512a3b3bcca72 Mon Sep 17 00:00:00 2001
From: siimav <1120038+siimav@users.noreply.github.com>
Date: Fri, 24 Apr 2026 02:08:41 +0300
Subject: [PATCH 1/4] Run boiloff in the background through Kerbalism
---
RealFuels/Resources/RealTankTypes.cfg | 63 -----
Source/RealFuels.csproj | 3 +
Source/Tanks/FuelTank.cs | 7 -
Source/Tanks/ModuleFuelTanksRF.cs | 306 ++++++++++++++++++++++---
Source/Utilities/KerbalismInterface.cs | 74 ++++++
Source/Utilities/ReflectionHelpers.cs | 82 +++++++
6 files changed, 438 insertions(+), 97 deletions(-)
create mode 100644 Source/Utilities/KerbalismInterface.cs
create mode 100644 Source/Utilities/ReflectionHelpers.cs
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 47a1f248..bbe38625 100644
--- a/Source/RealFuels.csproj
+++ b/Source/RealFuels.csproj
@@ -9,6 +9,7 @@
RealFuels
v4.8
+ 7.3
10.8.0
..\Build\RealFuels\obj
@@ -120,6 +121,7 @@
+
@@ -134,6 +136,7 @@
+
diff --git a/Source/Tanks/FuelTank.cs b/Source/Tanks/FuelTank.cs
index 3450b31e..d6423741 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,10 +34,6 @@ 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 resourceConductivity = 10;
diff --git a/Source/Tanks/ModuleFuelTanksRF.cs b/Source/Tanks/ModuleFuelTanksRF.cs
index ffa9f6c9..0d867b38 100644
--- a/Source/Tanks/ModuleFuelTanksRF.cs
+++ b/Source/Tanks/ModuleFuelTanksRF.cs
@@ -1,6 +1,7 @@
using KSP.Localization;
using System;
using System.Collections.Generic;
+using System.Globalization;
using UnityEngine;
using UnityEngine.Profiling;
@@ -20,7 +21,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;
@@ -39,6 +41,19 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr
private double cooling = 0;
+ // Thermal data captured while loaded, used for processing boiloff BackgroundUpdate on unloaded vessels.
+ // Format per entry: "resourceName,coldTempK,conductInternalWPerK,dewarAreaM2"
+ // coldTempK — boiling point of the propellant (K)
+ // conductInternalWPerK — part-interior→liquid thermal conductance in W/K (0 for Dewar tanks)
+ // dewarAreaM2 — tank area for Stefan-Boltzmann Dewar formula (−1 for non-Dewar tanks)
+ [KSPField(isPersistant = true)]
+ public string bgBoiloffData = "";
+
+ // 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;
+
[KSPField]
public int maxMLILayers = 10;
@@ -96,6 +111,40 @@ partial void OnAwakeRF()
}
}
+ partial void OnSaveRF(ConfigNode _)
+ {
+ if (!HighLogic.LoadedSceneIsFlight || cryoTanks.Count == 0)
+ return;
+
+ var bgEntries = new Dictionary();
+ foreach (var tank in cryoTanks)
+ {
+ if (tank.amount <= 0) continue;
+
+ if (tank.vsp > 0)
+ {
+ double conductInternalWPerK, dewarAreaM2;
+ if (tank.isDewar)
+ {
+ conductInternalWPerK = 0;
+ dewarAreaM2 = tank.totalArea;
+ }
+ else
+ {
+ 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;
+ conductInternalWPerK = tank.totalArea / Math.Max(double.Epsilon, wallF + insulF + resF);
+ dewarAreaM2 = -1;
+ }
+ bgEntries[tank.name] = string.Format(CultureInfo.InvariantCulture, "{0},{1:R},{2:R},{3:R}",
+ tank.name, tank.temperature, conductInternalWPerK, dewarAreaM2);
+ }
+ }
+
+ bgBoiloffData = bgEntries.Count > 0 ? string.Join(";", bgEntries.Values) : "";
+ }
+
partial void OnStartRF(StartState _)
{
if (HighLogic.LoadedSceneIsFlight)
@@ -103,7 +152,7 @@ partial void OnStartRF(StartState _)
foreach (var tank in tanksDict.Values)
{
- if (tank.maxAmount > 0 && (tank.vsp > 0 || tank.loss_rate > 0))
+ if (tank.maxAmount > 0 && tank.vsp > 0)
cryoTanks.Add(tank);
}
CalculateTankArea();
@@ -112,9 +161,11 @@ 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();
};
@@ -353,17 +404,6 @@ private void CalculateTankBoiloff(double deltaTime, bool analyticalMode = false,
}
}
}
- else if (tankAmount > 0 && tank.loss_rate > 0)
- {
- double deltaTemp = part.temperature - tank.temperature;
- if (deltaTemp > 0)
- {
- double lossAmount = tank.maxAmount * tank.loss_rate * deltaTemp * deltaTime;
- lossAmount = Math.Min(lossAmount, tankAmount);
- tank.resource.amount -= lossAmount;
- boiloffMassT += lossAmount * tank.density;
- }
- }
}
}
Profiler.EndSample();
@@ -385,6 +425,7 @@ partial void UpdateTankTypeRF(TankDefinition def)
_numberOfAddedMLILayers = Mathf.Clamp(_numberOfAddedMLILayers, 0, maxMLILayers);
((UI_FloatRange)Fields[nameof(_numberOfAddedMLILayers)].uiControlEditor).maxValue = maxMLILayers;
}
+ totalMLILayers = numberOfMLILayers + numberOfAddedMLILayers;
InitUtilization();
@@ -482,7 +523,22 @@ public void SetAnalyticTemperature(FlightIntegrator fi, double analyticTemp, dou
// Alternatively, just adjust the analytic output using the boiloff calculation anyway.
if (fi.timeSinceLastUpdate < double.MaxValue)
- CalculateTankBoiloff(fi.timeSinceLastUpdate, fi.isAnalytical, intScalar, skinScalar);
+ {
+ double remainingTime;
+ if (bgBoiloffLastUpdate > 0d)
+ {
+ remainingTime = Math.Max(0d, Planetarium.GetUniversalTime() - bgBoiloffLastUpdate);
+ analyticInternalTemp = ComputeMLIEquilibriumTemp(analyticSkinTemp);
+ bgBoiloffLastUpdate = 0d;
+ }
+ else
+ {
+ remainingTime = fi.timeSinceLastUpdate;
+ }
+
+ if (remainingTime > 0d)
+ CalculateTankBoiloff(remainingTime, fi.isAnalytical, intScalar, skinScalar);
+ }
else if (CalculateLowestTankTemperature())
{
// Vessel is freshly spawned and has cryogenic tanks, set temperatures appropriately
@@ -554,36 +610,231 @@ void DebugLog(string msg)
/// Default hot and cold values of 300 / 70. 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)
+ => 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
///
- 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
return PhysicsGlobals.StefanBoltzmanConstant * emissivity * area * (Math.Pow(hot,4) - Math.Pow(cold,4));
}
+ ///
+ /// Returns the steady-state interior temperature for MLI-insulated cryo tanks at the given skin temperature,
+ /// i.e. the point where heat conducted inward through the MLI blanket equals heat absorbed by boiloff.
+ ///
+ ///
+ ///
+ private double ComputeMLIEquilibriumTemp(double skinTemp)
+ {
+ if (totalMLILayers <= 0 || totalTankArea <= 0)
+ return skinTemp;
+
+ var tankParams = new List<(double conductW, double coldK)>(cryoTanks.Count);
+ var dewarParams = new List<(double area, double coldK)>();
+ foreach (var tank in cryoTanks)
+ {
+ if (tank.vsp <= 0) continue;
+
+ if (tank.isDewar)
+ {
+ dewarParams.Add((tank.totalArea, tank.temperature));
+ }
+ else
+ {
+ 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;
+ double conductW = tank.totalArea / Math.Max(double.Epsilon, wallF + insulF + resF);
+ tankParams.Add((conductW, tank.temperature));
+ }
+ }
+
+ return tankParams.Count > 0 || dewarParams.Count > 0
+ ? SolveMLIEquilibrium(skinTemp, totalTankArea, totalMLILayers, tankParams, dewarParams)
+ : skinTemp;
+ }
+
+ ///
+ /// Finds the shared part-interior equilibrium temperature given skin temperature and all cryo heat sinks.
+ /// Solves: GetMLITransferRate(skinTemp, T) × tankAreaM2 = Σ conductW_i × max(0, T − coldK_i) via bisection.
+ /// Left side is monotonically decreasing in T; right side increasing → unique root.
+ ///
+ private static double SolveMLIEquilibrium(
+ double skinTemp, double tankAreaM2, int mliLayers,
+ List<(double conductW, double coldK)> tanks,
+ List<(double area, double coldK)> dewarTanks)
+ {
+ double lo = double.MaxValue;
+ foreach (var t in tanks)
+ {
+ if (t.coldK < lo) lo = t.coldK;
+ }
+
+ foreach (var d in dewarTanks)
+ {
+ if (d.coldK < lo) lo = d.coldK;
+ }
+
+ double hi = skinTemp;
+ if (lo >= hi) return hi;
+
+ const double tolerance = 1e-3; // 1 mK — well below any physical significance
+ while (hi - lo > tolerance)
+ {
+ double mid = (lo + hi) * 0.5;
+ double mliFlux = GetMLITransferRate(skinTemp, mid, mliLayers) * tankAreaM2; // TODO: currently assumes pressureKPa is always 0 for unloaded vessels (space vacuum, no convective contribution).
+ double internalFlux = 0;
+ foreach (var t in tanks)
+ {
+ internalFlux += t.conductW * Math.Max(0, mid - t.coldK);
+ }
+
+ foreach (var d in dewarTanks)
+ {
+ if (mid > d.coldK)
+ internalFlux += GetDewarTransferRate(mid, d.coldK, d.area);
+ }
+
+ if (mliFlux > internalFlux) lo = mid;
+ else hi = mid;
+ }
+ return (lo + hi) * 0.5;
+ }
+
#endregion
#region Kerbalism
+
+ ///
+ /// Called by Kerbalism for unloaded (background) vessels via reflection.
+ /// Solves the MLI thermal equilibrium jointly for all non-Dewar cryo propellants in the part,
+ /// using Kerbalism's geometry+orientation-corrected VesselTemperature as the skin hot-side.
+ /// Solving jointly is essential: in a LH2+LOX tank, LH2's heat sink keeps T_interior below
+ /// LOX's boiling point, correctly suppressing LOX boiloff without any special-casing.
+ /// Falls back to the stored rate per-tank when geometry data is unavailable.
+ ///
+ public static string BackgroundUpdate(
+ Vessel vessel,
+ ProtoPartSnapshot proto_part,
+ ProtoPartModuleSnapshot proto_module,
+ PartModule partModule,
+ Part part,
+ Dictionary availableResources,
+ List> resourceChangeRequest,
+ double elapsed_s)
+ {
+ string data = proto_module.moduleValues.GetValue(nameof(bgBoiloffData));
+ if (string.IsNullOrEmpty(data)) return string.Empty;
+
+ bool hasGeometry = KerbalismInterface.TryGetThermalData(vessel, out double vesselTemp, out _);
+
+ // Read MLI geometry needed for the equilibrium solver.
+ int.TryParse(proto_module.moduleValues.GetValue(nameof(totalMLILayers)), out int mliLayers);
+ double.TryParse(proto_module.moduleValues.GetValue(nameof(totalTankArea)),
+ NumberStyles.Float, CultureInfo.InvariantCulture, out double tankAreaM2);
+
+ // Parse all entries. Separate Dewar tanks (handled individually) from vsp tanks
+ // (handled via joint MLI equilibrium).
+ var vspTanks = new List<(string name, double coldK, double conductW, PartResourceDefinition resDef)>();
+ var dewarTanks = new List<(string name, double coldK, double dewarArea, PartResourceDefinition resDef)>();
+
+ foreach (string entry in data.Split(';'))
+ {
+ if (string.IsNullOrEmpty(entry)) continue;
+ string[] f = entry.Split(',');
+ if (f.Length != 4) continue;
+ string resourceName = f[0];
+ if (!double.TryParse(f[1], NumberStyles.Float, CultureInfo.InvariantCulture, out double coldK)) continue;
+ if (!double.TryParse(f[2], NumberStyles.Float, CultureInfo.InvariantCulture, out double conductW)) continue;
+ if (!double.TryParse(f[3], NumberStyles.Float, CultureInfo.InvariantCulture, out double dewarArea)) continue;
+
+ PartResourceDefinition resDef = PartResourceLibrary.Instance.GetDefinition(resourceName);
+ if (resDef == null || resDef.density <= 0d) continue;
+
+ if (dewarArea >= 0)
+ dewarTanks.Add((resourceName, coldK, dewarArea, resDef));
+ else if (conductW > 0 && MFSSettings.resourceVsps.ContainsKey(resourceName))
+ vspTanks.Add((resourceName, coldK, conductW, resDef));
+ }
+
+ bool anyRequest = false;
+
+ // Solve the shared part-interior temperature, accounting for MLI and all cryo heat sinks.
+ // Requires Kerbalism VesselTemperature; boiloff is skipped entirely if geometry data is unavailable.
+ if (hasGeometry)
+ {
+ double interiorTemp;
+ if (mliLayers > 0 && tankAreaM2 > 0 && (vspTanks.Count > 0 || dewarTanks.Count > 0))
+ {
+ var tankParams = new List<(double conductW, double coldK)>(vspTanks.Count);
+ foreach (var t in vspTanks)
+ {
+ tankParams.Add((t.conductW, t.coldK));
+ }
+
+ var dewarParams = new List<(double area, double coldK)>(dewarTanks.Count);
+ foreach (var d in dewarTanks)
+ {
+ dewarParams.Add((d.dewarArea, d.coldK));
+ }
+
+ interiorTemp = SolveMLIEquilibrium(vesselTemp, tankAreaM2, mliLayers, tankParams, dewarParams);
+ }
+ else
+ {
+ interiorTemp = vesselTemp; // no MLI: interior equilibrates to skin temperature
+ }
+
+ foreach (var (name, coldK, conductW, resDef) in vspTanks)
+ {
+ if (!MFSSettings.resourceVsps.TryGetValue(name, out double vsp) || vsp <= 0) continue;
+ double deltaTemp = interiorTemp - coldK;
+ if (deltaTemp <= 0) continue;
+ double rateKgS = conductW * deltaTemp * 0.001 / vsp;
+ if (rateKgS <= 0) continue;
+ resourceChangeRequest.Add(new KeyValuePair(name, -rateKgS / resDef.density));
+ anyRequest = true;
+ }
+
+ foreach (var (name, coldK, dewarArea, resDef) in dewarTanks)
+ {
+ if (interiorTemp <= coldK || !MFSSettings.resourceVsps.TryGetValue(name, out double vsp) || vsp <= 0) continue;
+ double Q_kW = GetDewarTransferRate(interiorTemp, coldK, dewarArea) * 0.001;
+ double rateKgS = Math.Max(0, Q_kW / vsp);
+ if (rateKgS <= 0) continue;
+ resourceChangeRequest.Add(new KeyValuePair(name, -rateKgS / resDef.density));
+ anyRequest = true;
+ }
+ }
+
+ proto_module.moduleValues.SetValue("bgBoiloffLastUpdate",
+ Planetarium.GetUniversalTime().ToString(CultureInfo.InvariantCulture));
+
+ return anyRequest ? Localizer.GetStringByTag("#RF_FuelTankRF_kerbalismtips") : string.Empty;
+ }
+
///
- /// Called by Kerbalism every frame. Uses their resource system when Kerbalism is installed.
+ /// Called by Kerbalism every frame for loaded vessels. Uses their resource system when Kerbalism is installed.
///
public virtual string ResourceUpdate(Dictionary availableResources, List> resourceChangeRequest)
{
@@ -601,6 +852,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