Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion RealFuels/Localization/en-us.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Localization
#RF_Engine_GUIHide = Hide GUI
#RF_Engine_GUIShow = Show GUI
#RF_Engine_WindowTitle = Configure <<1>>
#RF_Engine_CurrentConfig = Current config
#RF_Engine_CurrentConfig = Current
#RF_Engine_ConfigSwitch = Switch to
#RF_Engine_LacksTech = Lacks tech for <<1>>
#RF_Engine_Purchase = Purchase
Expand Down
23 changes: 23 additions & 0 deletions RealFuels/RF_TestFlight_UISupport.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// RealFuels TestFlight UI Support
// This patch copies TestFlight reliability values from TESTFLIGHT nodes into CONFIG nodes
// so the RealFuels engine configuration UI can display them.
// This runs after RealismOverhaul but before TESTFLIGHT nodes are deleted.

@PART[*]:HAS[@MODULE[Module*EngineConfigs]]:BEFORE[zTestFlight]

{
@MODULE[Module*EngineConfigs]
{
// Copy reliability values for RealFuels UI display
@CONFIG:HAS[@TESTFLIGHT:HAS[#ignitionReliabilityStart]] { &ignitionReliabilityStart = #$TESTFLIGHT/ignitionReliabilityStart$ }
@CONFIG:HAS[@TESTFLIGHT:HAS[#ignitionReliabilityEnd]] { &ignitionReliabilityEnd = #$TESTFLIGHT/ignitionReliabilityEnd$ }
@CONFIG:HAS[@TESTFLIGHT:HAS[#cycleReliabilityStart]] { &cycleReliabilityStart = #$TESTFLIGHT/cycleReliabilityStart$ }
@CONFIG:HAS[@TESTFLIGHT:HAS[#cycleReliabilityEnd]] { &cycleReliabilityEnd = #$TESTFLIGHT/cycleReliabilityEnd$ }

// Copy burn time values for failure probability chart
@CONFIG:HAS[@TESTFLIGHT:HAS[#ratedBurnTime]] { &ratedBurnTime = #$TESTFLIGHT/ratedBurnTime$ }
@CONFIG:HAS[@TESTFLIGHT:HAS[#ratedContinuousBurnTime]] { &ratedContinuousBurnTime = #$TESTFLIGHT/ratedContinuousBurnTime$ }
@CONFIG:HAS[@TESTFLIGHT:HAS[#testedBurnTime]] { &testedBurnTime = #$TESTFLIGHT/testedBurnTime$ }
@CONFIG:HAS[@TESTFLIGHT:HAS[#overburnPenalty]] { &overburnPenalty = #$TESTFLIGHT/overburnPenalty$ }
}
}
182 changes: 182 additions & 0 deletions Source/Engines/EngineConfigIntegrations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;

namespace RealFuels
{
/// <summary>
/// Handles integration with B9PartSwitch and TestFlight mods.
/// Manages B9PS module linking and TestFlight interop values.
/// </summary>
public class EngineConfigIntegrations
{
private readonly ModuleEngineConfigsBase _module;

// B9PartSwitch reflection fields
protected static bool _b9psReflectionInitialized = false;
protected static FieldInfo B9PS_moduleID;
protected static MethodInfo B9PS_SwitchSubtype;
protected static FieldInfo B9PS_switchInFlight;

public Dictionary<string, PartModule> B9PSModules;
protected Dictionary<string, string> RequestedB9PSVariants = new Dictionary<string, string>();

public EngineConfigIntegrations(ModuleEngineConfigsBase module)
{
_module = module;
InitializeB9PSReflection();
}

#region TestFlight Integration

/// <summary>
/// Updates TestFlight interop values with current configuration.
/// </summary>
public void UpdateTFInterops()
{
TestFlightWrapper.AddInteropValue(_module.part, _module.isMaster ? "engineConfig" : "vernierConfig", _module.configuration, "RealFuels");
}

#endregion

#region B9PartSwitch Integration

/// <summary>
/// Initializes reflection for B9PartSwitch if available.
/// </summary>
private void InitializeB9PSReflection()
{
if (_b9psReflectionInitialized || !Utilities.B9PSFound) return;
B9PS_moduleID = Type.GetType("B9PartSwitch.CustomPartModule, B9PartSwitch")?.GetField("moduleID");
B9PS_SwitchSubtype = Type.GetType("B9PartSwitch.ModuleB9PartSwitch, B9PartSwitch")?.GetMethod("SwitchSubtype");
B9PS_switchInFlight = Type.GetType("B9PartSwitch.ModuleB9PartSwitch, B9PartSwitch")?.GetField("switchInFlight");
_b9psReflectionInitialized = true;
}

/// <summary>
/// Loads all B9PartSwitch modules that are linked to configs.
/// </summary>
public void LoadB9PSModules()
{
IEnumerable<string> b9psModuleIDs = _module.configs
.Where(cfg => cfg.HasNode("LinkB9PSModule"))
.SelectMany(cfg => cfg.GetNodes("LinkB9PSModule"))
.Select(link => link?.GetValue("name"))
.Where(moduleID => moduleID != null)
.Distinct();

B9PSModules = new Dictionary<string, PartModule>(b9psModuleIDs.Count());

foreach (string moduleID in b9psModuleIDs)
{
var module = ModuleEngineConfigsBase.GetSpecifiedModules(_module.part, string.Empty, -1, "ModuleB9PartSwitch", false)
.FirstOrDefault(m => (string)B9PS_moduleID?.GetValue(m) == moduleID);
if (module == null)
Debug.LogError($"*RFMEC* B9PartSwitch module with ID {moduleID} was not found for {_module.part}!");
else
B9PSModules[moduleID] = module;
}
}

/// <summary>
/// Hide the GUI for all `ModuleB9PartSwitch`s managed by RF.
/// This is somewhat of a hack-ish approach...
/// </summary>
public void HideB9PSVariantSelectors()
{
if (B9PSModules == null) return;
foreach (var module in B9PSModules.Values)
{
module.Fields["currentSubtypeTitle"].guiActive = false;
module.Fields["currentSubtypeTitle"].guiActiveEditor = false;
module.Fields["currentSubtypeIndex"].guiActive = false;
module.Fields["currentSubtypeIndex"].guiActiveEditor = false;
module.Events["ShowSubtypesWindow"].guiActive = false;
module.Events["ShowSubtypesWindow"].guiActiveEditor = false;
}
}

/// <summary>
/// Coroutine to hide B9PS in-flight selector after a frame delay.
/// </summary>
private IEnumerator HideB9PSInFlightSelector_Coroutine(PartModule module)
{
yield return null;
module.Events["ShowSubtypesWindow"].guiActive = false;
}

/// <summary>
/// Requests B9PS variant changes for the given config.
/// </summary>
public void RequestB9PSVariantsForConfig(ConfigNode node)
{
if (B9PSModules == null || B9PSModules.Count == 0) return;
RequestedB9PSVariants.Clear();
if (node.GetNodes("LinkB9PSModule") is ConfigNode[] links)
{
foreach (ConfigNode link in links)
{
string moduleID = null, subtype = null;
if (!link.TryGetValue("name", ref moduleID))
Debug.LogError($"*RFMEC* Config `{_module.configurationDisplay}` of {_module.part} has a LinkB9PSModule specification without a name key!");
if (!link.TryGetValue("subtype", ref subtype))
Debug.LogError($"*RFMEC* Config `{_module.configurationDisplay}` of {_module.part} has a LinkB9PSModule specification without a subtype key!");
if (moduleID != null && subtype != null)
RequestedB9PSVariants[moduleID] = subtype;
}
}
_module.StartCoroutine(ApplyRequestedB9PSVariants_Coroutine());
}

/// <summary>
/// Coroutine that applies requested B9PS variant changes after a frame delay.
/// </summary>
protected IEnumerator ApplyRequestedB9PSVariants_Coroutine()
{
yield return new WaitForEndOfFrame();

if (RequestedB9PSVariants.Count == 0) yield break;

foreach (var entry in B9PSModules)
{
string moduleID = entry.Key;
PartModule module = entry.Value;

if (HighLogic.LoadedSceneIsFlight
&& B9PS_switchInFlight != null
&& !(bool)B9PS_switchInFlight.GetValue(module)) continue;

if (!RequestedB9PSVariants.TryGetValue(moduleID, out string subtypeName))
{
Debug.LogError($"*RFMEC* Config {_module.configurationDisplay} of {_module.part} does not specify a subtype for linked B9PS module with ID {moduleID}; defaulting to `{_module.configuration}`.");
subtypeName = _module.configuration;
}

B9PS_SwitchSubtype?.Invoke(module, new object[] { subtypeName });
if (HighLogic.LoadedSceneIsFlight) _module.StartCoroutine(HideB9PSInFlightSelector_Coroutine(module));
}

RequestedB9PSVariants.Clear();
// Clear symmetry counterparts' queues since B9PS already handles symmetry.
_module.DoForEachSymmetryCounterpart(mec => mec.Integrations.ClearRequestedB9PSVariants());
}

/// <summary>
/// Clears the requested B9PS variants queue.
/// </summary>
public void ClearRequestedB9PSVariants()
{
RequestedB9PSVariants.Clear();
}

/// <summary>
/// Updates B9PS variants based on current config.
/// </summary>
public void UpdateB9PSVariants() => RequestB9PSVariantsForConfig(_module.config);

#endregion
}
}
91 changes: 91 additions & 0 deletions Source/Engines/EngineConfigPropellants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using KSP.UI.Screens;

namespace RealFuels
{
/// <summary>
/// Static utility class for propellant and resource operations.
/// Contains pure functions for clearing float curves, propellant gauges, and RCS propellants.
/// </summary>
public static class EngineConfigPropellants
{
private static readonly FieldInfo MRCSConsumedResources = typeof(ModuleRCS).GetField("consumedResources", BindingFlags.NonPublic | BindingFlags.Instance);

/// <summary>
/// Clears all FloatCurves that need to be cleared based on config node or tech level.
/// </summary>
public static void ClearFloatCurves(Type mType, PartModule pm, ConfigNode cfg, int techLevel)
{
// clear all FloatCurves we need to clear (i.e. if our config has one, or techlevels are enabled)
bool delAtmo = cfg.HasNode("atmosphereCurve") || techLevel >= 0;
bool delDens = cfg.HasNode("atmCurve") || techLevel >= 0;
bool delVel = cfg.HasNode("velCurve") || techLevel >= 0;
foreach (FieldInfo field in mType.GetFields())
{
if (field.FieldType == typeof(FloatCurve) &&
((field.Name.Equals("atmosphereCurve") && delAtmo)
|| (field.Name.Equals("atmCurve") && delDens)
|| (field.Name.Equals("velCurve") && delVel)))
{
field.SetValue(pm, new FloatCurve());
}
}
}

/// <summary>
/// Clears propellant gauges from the staging icon.
/// </summary>
public static void ClearPropellantGauges(Type mType, PartModule pm)
{
foreach (FieldInfo field in mType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
{
if (field.FieldType == typeof(Dictionary<Propellant, ProtoStageIconInfo>) &&
field.GetValue(pm) is Dictionary<Propellant, ProtoStageIconInfo> boxes)
{
foreach (ProtoStageIconInfo v in boxes.Values)
{
try
{
if (v is ProtoStageIconInfo)
pm.part.stackIcon.RemoveInfo(v);
}
catch (Exception e)
{
Debug.LogError("*RFMEC* Trying to remove info box: " + e.Message);
}
}
boxes.Clear();
}
}
}

/// <summary>
/// Clears RCS propellants and reloads them from config.
/// </summary>
/// <param name="part">The part containing RCS modules</param>
/// <param name="cfg">The configuration node to load propellants from</param>
/// <param name="doConfigAction">Action to execute DoConfig on the configuration</param>
public static void ClearRCSPropellants(Part part, ConfigNode cfg, Action<ConfigNode> doConfigAction)
{
List<ModuleRCS> RCSModules = part.Modules.OfType<ModuleRCS>().ToList();
if (RCSModules.Count > 0)
{
doConfigAction(cfg);
foreach (var rcsModule in RCSModules)
{
if (cfg.HasNode("PROPELLANT"))
rcsModule.propellants.Clear();
rcsModule.Load(cfg);
List<PartResourceDefinition> res = MRCSConsumedResources.GetValue(rcsModule) as List<PartResourceDefinition>;
res.Clear();
foreach (Propellant p in rcsModule.propellants)
res.Add(p.resourceDef);
}
}
}
}
}
Loading