22using System ;
33using System . Collections . Generic ;
44using System . Globalization ;
5+ using System . Runtime . CompilerServices ;
56using UnityEngine ;
67using UnityEngine . Profiling ;
78
@@ -80,6 +81,15 @@ public partial class ModuleFuelTanks : IAnalyticTemperatureModifier, IAnalyticPr
8081 private readonly List < double > lossInfo = new List < double > ( ) ;
8182 private readonly List < double > fluxInfo = new List < double > ( ) ;
8283
84+ // Cached solver params for ComputeMLIEquilibriumTemp (values are fixed for a given tank definition).
85+ private ( double conductW , double coldK ) [ ] _mliTankParamsCache ;
86+ private ( double dewarArea , double coldK ) [ ] _mliDewarParamsCache ;
87+
88+ // Pre-parsed tank boiloff data for background processing.
89+ // Keys naturally expire when the vessel loads and KSP releases the snapshot, so no manual cleanup is needed.
90+ private static readonly ConditionalWeakTable < ProtoPartModuleSnapshot , BgBoiloffCache > _bgCache
91+ = new ConditionalWeakTable < ProtoPartModuleSnapshot , BgBoiloffCache > ( ) ;
92+
8393 private FlightIntegrator _flightIntegrator ;
8494
8595 double lowestTankTemperature = 300d ;
@@ -626,8 +636,18 @@ private double ComputeMLIEquilibriumTemp(double skinTemp)
626636 if ( totalMLILayers <= 0 || totalTankArea <= 0 )
627637 return skinTemp ;
628638
639+ if ( _mliTankParamsCache == null )
640+ BuildMLISolverParams ( ) ;
641+
642+ return _mliTankParamsCache . Length > 0 || _mliDewarParamsCache . Length > 0
643+ ? SolveMLIEquilibrium ( skinTemp , totalTankArea , totalMLILayers , _mliTankParamsCache , _mliDewarParamsCache )
644+ : skinTemp ;
645+ }
646+
647+ private void BuildMLISolverParams ( )
648+ {
629649 var tankParams = new List < ( double conductW , double coldK ) > ( cryoTanks . Count ) ;
630- var dewarParams = new List < ( double area , double coldK ) > ( ) ;
650+ var dewarParams = new List < ( double dewarArea , double coldK ) > ( ) ;
631651 foreach ( var tank in cryoTanks )
632652 {
633653 if ( tank . vsp <= 0 ) continue ;
@@ -641,14 +661,11 @@ private double ComputeMLIEquilibriumTemp(double skinTemp)
641661 double wallF = tank . wallConduction > 0 ? tank . wallThickness / tank . wallConduction : 0 ;
642662 double insulF = tank . insulationConduction > 0 ? tank . insulationThickness / tank . insulationConduction : 0 ;
643663 double resF = tank . resourceConductivity > 0 ? 0.01 / tank . resourceConductivity : 0 ;
644- double conductW = tank . totalArea / Math . Max ( double . Epsilon , wallF + insulF + resF ) ;
645- tankParams . Add ( ( conductW , tank . temperature ) ) ;
664+ tankParams . Add ( ( tank . totalArea / Math . Max ( double . Epsilon , wallF + insulF + resF ) , tank . temperature ) ) ;
646665 }
647666 }
648-
649- return tankParams . Count > 0 || dewarParams . Count > 0
650- ? SolveMLIEquilibrium ( skinTemp , totalTankArea , totalMLILayers , tankParams , dewarParams )
651- : skinTemp ;
667+ _mliTankParamsCache = tankParams . ToArray ( ) ;
668+ _mliDewarParamsCache = dewarParams . ToArray ( ) ;
652669 }
653670
654671 /// <summary>
@@ -658,8 +675,8 @@ private double ComputeMLIEquilibriumTemp(double skinTemp)
658675 /// </summary>
659676 private static double SolveMLIEquilibrium (
660677 double skinTemp , double tankAreaM2 , int mliLayers ,
661- List < ( double conductW , double coldK ) > tanks ,
662- List < ( double area , double coldK ) > dewarTanks )
678+ IReadOnlyList < ( double conductW , double coldK ) > tanks ,
679+ IReadOnlyList < ( double area , double coldK ) > dewarTanks )
663680 {
664681 double lo = double . MaxValue ;
665682 foreach ( var t in tanks )
@@ -725,81 +742,48 @@ public static string BackgroundUpdate(
725742
726743 bool hasGeometry = KerbalismInterface . TryGetThermalData ( vessel , out double vesselTemp , out _ ) ;
727744
728- // Read MLI geometry needed for the equilibrium solver.
729- int . TryParse ( proto_module . moduleValues . GetValue ( nameof ( totalMLILayers ) ) , out int mliLayers ) ;
730- double . TryParse ( proto_module . moduleValues . GetValue ( nameof ( totalTankArea ) ) ,
731- NumberStyles . Float , CultureInfo . InvariantCulture , out double tankAreaM2 ) ;
732-
733- // Parse all entries. Separate Dewar tanks (handled individually) from vsp tanks
734- // (handled via joint MLI equilibrium).
735- var vspTanks = new List < ( string name , double coldK , double conductW , PartResourceDefinition resDef ) > ( ) ;
736- var dewarTanks = new List < ( string name , double coldK , double dewarArea , PartResourceDefinition resDef ) > ( ) ;
737-
738- foreach ( string entry in data . Split ( ';' ) )
745+ // bgBoiloffData, totalMLILayers, and totalTankArea are all static while a vessel is unloaded,
746+ // so parse them once and cache on the ProtoPartModuleSnapshot (automatically GC'd when the
747+ // vessel loads and the snapshot is released).
748+ if ( ! _bgCache . TryGetValue ( proto_module , out BgBoiloffCache cache ) || cache . DataVersion != data )
739749 {
740- if ( string . IsNullOrEmpty ( entry ) ) continue ;
741- string [ ] f = entry . Split ( ',' ) ;
742- if ( f . Length != 4 ) continue ;
743- string resourceName = f [ 0 ] ;
744- if ( ! double . TryParse ( f [ 1 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out double coldK ) ) continue ;
745- if ( ! double . TryParse ( f [ 2 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out double conductW ) ) continue ;
746- if ( ! double . TryParse ( f [ 3 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out double dewarArea ) ) continue ;
747-
748- PartResourceDefinition resDef = PartResourceLibrary . Instance . GetDefinition ( resourceName ) ;
749- if ( resDef == null || resDef . density <= 0d ) continue ;
750-
751- if ( dewarArea >= 0 )
752- dewarTanks . Add ( ( resourceName , coldK , dewarArea , resDef ) ) ;
753- else if ( conductW > 0 && MFSSettings . resourceVsps . ContainsKey ( resourceName ) )
754- vspTanks . Add ( ( resourceName , coldK , conductW , resDef ) ) ;
750+ int . TryParse ( proto_module . moduleValues . GetValue ( nameof ( totalMLILayers ) ) , out int mliLayers ) ;
751+ double . TryParse ( proto_module . moduleValues . GetValue ( nameof ( totalTankArea ) ) ,
752+ NumberStyles . Float , CultureInfo . InvariantCulture , out double tankAreaM2 ) ;
753+ cache = BuildBgBoiloffCache ( data , mliLayers , tankAreaM2 ) ;
754+ _bgCache . Remove ( proto_module ) ;
755+ _bgCache . Add ( proto_module , cache ) ;
755756 }
756757
757758 bool anyRequest = false ;
758759
759- // Solve the shared part-interior temperature, accounting for MLI and all cryo heat sinks.
760- // Requires Kerbalism VesselTemperature; boiloff is skipped entirely if geometry data is unavailable.
761- if ( hasGeometry )
760+ if ( hasGeometry && ( cache . VspParams . Length > 0 || cache . DewarParams . Length > 0 ) )
762761 {
763- double interiorTemp ;
764- if ( mliLayers > 0 && tankAreaM2 > 0 && ( vspTanks . Count > 0 || dewarTanks . Count > 0 ) )
765- {
766- var tankParams = new List < ( double conductW , double coldK ) > ( vspTanks . Count ) ;
767- foreach ( var t in vspTanks )
768- {
769- tankParams . Add ( ( t . conductW , t . coldK ) ) ;
770- }
771-
772- var dewarParams = new List < ( double area , double coldK ) > ( dewarTanks . Count ) ;
773- foreach ( var d in dewarTanks )
774- {
775- dewarParams . Add ( ( d . dewarArea , d . coldK ) ) ;
776- }
762+ double interiorTemp = cache . MliLayers > 0 && cache . TankAreaM2 > 0
763+ ? SolveMLIEquilibrium ( vesselTemp , cache . TankAreaM2 , cache . MliLayers , cache . VspParams , cache . DewarParams )
764+ : vesselTemp ;
777765
778- interiorTemp = SolveMLIEquilibrium ( vesselTemp , tankAreaM2 , mliLayers , tankParams , dewarParams ) ;
779- }
780- else
781- {
782- interiorTemp = vesselTemp ; // no MLI: interior equilibrates to skin temperature
783- }
784-
785- foreach ( var ( name , coldK , conductW , resDef ) in vspTanks )
766+ for ( int i = 0 ; i < cache . VspParams . Length ; i ++ )
786767 {
787- if ( ! MFSSettings . resourceVsps . TryGetValue ( name , out double vsp ) || vsp <= 0 ) continue ;
768+ var ( conductW , coldK ) = cache . VspParams [ i ] ;
769+ var ( name , vsp , density ) = cache . VspInfo [ i ] ;
788770 double deltaTemp = interiorTemp - coldK ;
789771 if ( deltaTemp <= 0 ) continue ;
790772 double rateKgS = conductW * deltaTemp * 0.001 / vsp ;
791773 if ( rateKgS <= 0 ) continue ;
792- resourceChangeRequest . Add ( new KeyValuePair < string , double > ( name , - rateKgS / resDef . density ) ) ;
774+ resourceChangeRequest . Add ( new KeyValuePair < string , double > ( name , - rateKgS / density ) ) ;
793775 anyRequest = true ;
794776 }
795777
796- foreach ( var ( name , coldK , dewarArea , resDef ) in dewarTanks )
778+ for ( int i = 0 ; i < cache . DewarParams . Length ; i ++ )
797779 {
798- if ( interiorTemp <= coldK || ! MFSSettings . resourceVsps . TryGetValue ( name , out double vsp ) || vsp <= 0 ) continue ;
780+ var ( dewarArea , coldK ) = cache . DewarParams [ i ] ;
781+ var ( name , vsp , density ) = cache . DewarInfo [ i ] ;
782+ if ( interiorTemp <= coldK ) continue ;
799783 double Q_kW = GetDewarTransferRate ( interiorTemp , coldK , dewarArea ) * 0.001 ;
800784 double rateKgS = Math . Max ( 0 , Q_kW / vsp ) ;
801785 if ( rateKgS <= 0 ) continue ;
802- resourceChangeRequest . Add ( new KeyValuePair < string , double > ( name , - rateKgS / resDef . density ) ) ;
786+ resourceChangeRequest . Add ( new KeyValuePair < string , double > ( name , - rateKgS / density ) ) ;
803787 anyRequest = true ;
804788 }
805789 }
@@ -810,6 +794,44 @@ public static string BackgroundUpdate(
810794 return anyRequest ? Localizer . GetStringByTag ( "#RF_FuelTankRF_kerbalismtips" ) : string . Empty ;
811795 }
812796
797+ private static BgBoiloffCache BuildBgBoiloffCache ( string data , int mliLayers , double tankAreaM2 )
798+ {
799+ var vspParams = new List < ( double conductW , double coldK ) > ( ) ;
800+ var vspInfo = new List < ( string name , double vsp , double density ) > ( ) ;
801+ var dewarParams = new List < ( double dewarArea , double coldK ) > ( ) ;
802+ var dewarInfo = new List < ( string name , double vsp , double density ) > ( ) ;
803+
804+ foreach ( string entry in data . Split ( ';' ) )
805+ {
806+ if ( string . IsNullOrEmpty ( entry ) ) continue ;
807+ string [ ] split = entry . Split ( ',' ) ;
808+ if ( split . Length != 4 ) continue ;
809+ string resourceName = split [ 0 ] ;
810+ if ( ! double . TryParse ( split [ 1 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out double coldK ) ) continue ;
811+ if ( ! double . TryParse ( split [ 2 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out double conductW ) ) continue ;
812+ if ( ! double . TryParse ( split [ 3 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out double dewarArea ) ) continue ;
813+
814+ PartResourceDefinition resDef = PartResourceLibrary . Instance . GetDefinition ( resourceName ) ;
815+ if ( resDef == null || resDef . density <= 0d ) continue ;
816+ if ( ! MFSSettings . resourceVsps . TryGetValue ( resourceName , out double vsp ) || vsp <= 0 ) continue ;
817+
818+ if ( dewarArea >= 0 )
819+ {
820+ dewarParams . Add ( ( dewarArea , coldK ) ) ;
821+ dewarInfo . Add ( ( resourceName , vsp , resDef . density ) ) ;
822+ }
823+ else if ( conductW > 0 )
824+ {
825+ vspParams . Add ( ( conductW , coldK ) ) ;
826+ vspInfo . Add ( ( resourceName , vsp , resDef . density ) ) ;
827+ }
828+ }
829+
830+ return new BgBoiloffCache ( data , mliLayers , tankAreaM2 ,
831+ vspParams . ToArray ( ) , vspInfo . ToArray ( ) ,
832+ dewarParams . ToArray ( ) , dewarInfo . ToArray ( ) ) ;
833+ }
834+
813835 /// <summary>
814836 /// Called by Kerbalism every frame for loaded vessels. Uses their resource system when Kerbalism is installed.
815837 /// </summary>
0 commit comments