From f749c6ea23e04f62ae61140c49f5dbeebab01d9e Mon Sep 17 00:00:00 2001 From: Spartwo Date: Sun, 13 Sep 2026 21:35:25 +0100 Subject: [PATCH 1/5] MiniMods --- .../Localization/en-us.cfg | 6 + Source/Modules/ModuleAttachmentVisuals.cs | 278 ++++++++++++++++++ .../ModuleExclusiveResourceConverter.cs | 38 +++ Source/Modules/ModuleToggleTracking.cs | 98 ++++++ 4 files changed, 420 insertions(+) create mode 100644 Source/Modules/ModuleAttachmentVisuals.cs create mode 100644 Source/Modules/ModuleExclusiveResourceConverter.cs create mode 100644 Source/Modules/ModuleToggleTracking.cs diff --git a/GameData/KSPCommunityPartModules/Localization/en-us.cfg b/GameData/KSPCommunityPartModules/Localization/en-us.cfg index d0f4d05..42c0ac6 100644 --- a/GameData/KSPCommunityPartModules/Localization/en-us.cfg +++ b/GameData/KSPCommunityPartModules/Localization/en-us.cfg @@ -4,5 +4,11 @@ Localization { // ModuleAutoCutDrogue #KSPCPM_CutDrogues = Auto-Cut Drogue Chute(s) + + // ModuleToggleTracking + #KSPCPM_Tracking = Tracking + #KSPCPM_ToggleTracking = Toggle Tracking + #KSPCPM_DisableTracking = Disable Tracking + #KSPCPM_EnableTracking = Enable Tracking } } diff --git a/Source/Modules/ModuleAttachmentVisuals.cs b/Source/Modules/ModuleAttachmentVisuals.cs new file mode 100644 index 0000000..850b2db --- /dev/null +++ b/Source/Modules/ModuleAttachmentVisuals.cs @@ -0,0 +1,278 @@ +/* + Usecase: Part Gameobject Visibility based on Stack Attachment Node Occupancy with support for multiple nodes per part. + Originally By: Spartwo + Originally For: Kerbal Powers + License: GNU General Public License v3.0, see https://www.gnu.org/licenses/gpl-3.0.html +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace KSPCommunityPartModules.Modules +{ + public class ModuleAttachmentVisuals : PartModule + { + [KSPField] + public string requiredNodes; + + //visible transforms when node occupied + [KSPField] + public string showAttached; + + //visible transforms when node unoccupied + [KSPField] + public string showFree; + + //"Enable/Disable " in the editor + [KSPField] + public string objectDisplayName; + + [KSPEvent( + guiActive = false, + guiActiveEditor = true, + guiName = "#KSPCPM_Capping" + )] + public void EventToggleVisual() => ToggleVisual(); + + [KSPField(isPersistant = true)] + public bool transformEnabled = true; + + // Nodes used as conditions + private List nodes = new List(); + + // Transforms shown when nodes are occupied + private List attachedTransforms = new List(); + + // Transforms shown when nodes are free + private List freeTransforms = new List(); + + private HashSet directChildren = new HashSet(); + + public override void OnStart(StartState state) + { + base.OnStart(state); + + if (HighLogic.LoadedSceneIsEditor) + { + GameEvents.onEditorPartEvent.Add(OnEditorEvent); + } + + CacheInitialChildren(); + ParseConfig(); + UpdateVisuals(); + } + + public void OnDestroy() + { + if (HighLogic.LoadedSceneIsEditor) + { + GameEvents.onEditorPartEvent.Remove(OnEditorEvent); + } + } + + private void ParseConfig() + { + nodes.Clear(); + attachedTransforms.Clear(); + freeTransforms.Clear(); + + // Parse attachment nodes + if (!string.IsNullOrWhiteSpace(requiredNodes)) + { + foreach (string nodeName in requiredNodes.Split(',')) + { + string nodeId = nodeName.Trim(); + + if (string.IsNullOrEmpty(nodeId)) + continue; + + AttachNode node = part.FindAttachNode(nodeId); + + if (node != null) + { + nodes.Add(node); + + Debug.Log( + $"[ModuleAttachmentVisuals] Found node '{nodeId}' " + + $"for part '{part.name}'" + ); + } + else + { + Debug.LogWarning( + $"[ModuleAttachmentVisuals] Node '{nodeId}' " + + $"not found on part '{part.name}'" + ); + } + } + } + + // Parse transforms shown when condition is true + if (!string.IsNullOrWhiteSpace(showAttached)) + { + foreach (string transformName in showAttached.Split(',')) + { + string name = transformName.Trim(); + + if (string.IsNullOrEmpty(name)) + continue; + + Transform transform = part.FindModelTransform(name); + + if (transform != null) + { + attachedTransforms.Add(transform); + } + else + { + Debug.LogWarning( + $"[ModuleAttachmentVisuals] Could not find attached transform " + + $"'{name}' on '{part.name}'" + ); + } + } + } + + // Parse transforms shown when condition is false + if (!string.IsNullOrWhiteSpace(showFree)) + { + foreach (string transformName in showFree.Split(',')) + { + string name = transformName.Trim(); + + if (string.IsNullOrEmpty(name)) + continue; + + Transform transform = part.FindModelTransform(name); + + if (transform != null) + { + freeTransforms.Add(transform); + } + else + { + Debug.LogWarning( + $"[ModuleAttachmentVisuals] Could not find free transform " + + $"'{name}' on '{part.name}'" + ); + } + } + } + } + + private void UpdateVisuals() + { + // The objects only show when: + // - transform is enabled + // - all config nodes are occupied + + bool allNodesAttached = + nodes.Count > 0 && + nodes.All(node => node != null && node.attachedPart != null); + + bool visualActive = transformEnabled && allNodesAttached; + + SetTransforms(attachedTransforms, visualActive); + SetTransforms(freeTransforms, !visualActive); + + UpdateToggleEventUI(allNodesAttached); + } + + private void UpdateToggleEventUI(bool allNodesAttached) + { + BaseEvent toggleEvent = Events["EventToggleVisual"]; + + // Only offer the toggle when there's actually something capped to toggle + toggleEvent.active = allNodesAttached; + + string verb = transformEnabled ? "Disable" : "Enable"; + + string displayName = string.IsNullOrWhiteSpace(objectDisplayName) + ? $"{verb} Capping" + : $"{verb} {objectDisplayName}"; + + toggleEvent.guiName = displayName; + } + + private void SetTransforms(List transforms, bool active) + { + foreach (Transform transform in transforms) + { + if (transform == null) + { + Debug.LogWarning( + $"[ModuleAttachmentVisuals] Transform is null on part '{part.name}'" + ); + + continue; + } + + transform.gameObject.SetActive(active); + } + } + + private void ToggleVisual() + { + transformEnabled = !transformEnabled; + + UpdateVisuals(); + } + + private void OnEditorEvent(ConstructionEventType evt, Part p) + { + if ( + evt != ConstructionEventType.PartAttached && + evt != ConstructionEventType.PartDetached + ) + { + return; + } + + // Event directly involving this part + if (part == p) + { + CacheInitialChildren(); + UpdateVisuals(); + return; + } + + bool wasDirectChild = directChildren.Contains(p); + bool isDirectChildNow = p.parent == part; + + switch (evt) + { + case ConstructionEventType.PartAttached: + + if (isDirectChildNow) + { + directChildren.Add(p); + UpdateVisuals(); + } + + break; + + case ConstructionEventType.PartDetached: + + if (wasDirectChild) + { + directChildren.Remove(p); + UpdateVisuals(); + } + + break; + } + } + + private void CacheInitialChildren() + { + directChildren.Clear(); + + foreach (Part child in part.children) + { + directChildren.Add(child); + } + } + } +} \ No newline at end of file diff --git a/Source/Modules/ModuleExclusiveResourceConverter.cs b/Source/Modules/ModuleExclusiveResourceConverter.cs new file mode 100644 index 0000000..ab9ac45 --- /dev/null +++ b/Source/Modules/ModuleExclusiveResourceConverter.cs @@ -0,0 +1,38 @@ +/* + Usecase: Extends the stock resource converter so that only one type can run at a time. + Originally By: Spartwo + Originally For: Kerbal Powers + License: GNU General Public License v3.0, see https://www.gnu.org/licenses/gpl-3.0.html +*/ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using KSP.IO; +using KSP.UI.Screens; + +namespace KSPCommunityPartModules.Modules +{ + public class ModuleExclusiveResourceConverter : ModuleResourceConverter + { + public override void StartResourceConverter() + { + StopOtherConverters(); + base.StartResourceConverter(); + } + + private void StopOtherConverters () + { + ModuleExclusiveResourceConverter[] otherConverters = part.GetComponents(); + foreach (ModuleExclusiveResourceConverter e in otherConverters) + { + e.StopResourceConverter(); + } + } + + } +} diff --git a/Source/Modules/ModuleToggleTracking.cs b/Source/Modules/ModuleToggleTracking.cs new file mode 100644 index 0000000..b3925e2 --- /dev/null +++ b/Source/Modules/ModuleToggleTracking.cs @@ -0,0 +1,98 @@ +/* + Usecase: This module is applied to parts such as extendable solar panels and radiators to toggle their ability to track the sun. + Originally By: Spartwo + Originally For: Kerbal Powers + License: GNU General Public License v3.0, see https://www.gnu.org/licenses/gpl-3.0.html +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using KSP.IO; +using KSP.UI.Screens; +using UnityEngine.SceneManagement; + +namespace KSPCommunityPartModules.Modules +{ + public class ModuleToggleTracking : PartModule + { + + [KSPEvent(guiActive = true, + guiActiveEditor = true, + guiName = "#KSPCPM_Tracking")] + public void EventToggleTracking() => ToggleTracking(); + + + [KSPAction("#KSPCPM_ToggleTracking")] + public void AGToggleTracking(KSPActionParam param) => ToggleTracking(); + + [KSPAction("#KSPCPM_DisableTracking")] + public void AGDisableTracking(KSPActionParam param) => SetTracking(false); + + [KSPAction("#KSPCPM_EnableTracking")] + public void AGEnableTracking(KSPActionParam param) => SetTracking(true); + + [KSPField(isPersistant = true)] + public bool trackingEnabled; + + ModuleDeployablePart tracker; + + public override void OnStart(StartState state) + { + base.OnStart(state); + + try + { + tracker = part.GetComponent(); + SetToggleName(); + if (state != StartState.Editor) + { + SetTracking(trackingEnabled); + } + } + catch(Exception e) + { + Debug.Log($"Setup Error: {e}"); + } + + } + + public override void OnLoad(ConfigNode node) + { + try + { + tracker = part.GetComponent(); + SetToggleName(); + SetTracking(trackingEnabled); + } + catch (Exception e) + { + Debug.Log($"Load Error: {e}"); + } + } + + private void SetTracking(bool newState) + { + trackingEnabled = newState; + tracker.isTracking = newState; + SetToggleName(); + } + + private void ToggleTracking() + { + bool newState = !trackingEnabled; + trackingEnabled = newState; + tracker.isTracking = newState; + SetToggleName(); + } + + private void SetToggleName() + { + Events["EventToggleTracking"].guiName = trackingEnabled ? "#KSPCPM_DisableTracking" : "#KSPCPM_EnableTracking"; + } + } +} From b09f14534cf8f1b397cb9c30200df77d103634d6 Mon Sep 17 00:00:00 2001 From: Spartwo Date: Mon, 14 Sep 2026 21:38:56 +0100 Subject: [PATCH 2/5] Update ModuleAttachmentVisuals.cs --- Source/Modules/ModuleAttachmentVisuals.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Modules/ModuleAttachmentVisuals.cs b/Source/Modules/ModuleAttachmentVisuals.cs index 850b2db..6dbf1e5 100644 --- a/Source/Modules/ModuleAttachmentVisuals.cs +++ b/Source/Modules/ModuleAttachmentVisuals.cs @@ -81,7 +81,7 @@ private void ParseConfig() // Parse attachment nodes if (!string.IsNullOrWhiteSpace(requiredNodes)) { - foreach (string nodeName in requiredNodes.Split(',')) + foreach (string nodeName in requiredNodes.Split(';')) { string nodeId = nodeName.Trim(); From ae7504d909f4a408f9c1a5dba85cf16fff2261a0 Mon Sep 17 00:00:00 2001 From: Spartwo Date: Mon, 14 Sep 2026 21:49:44 +0100 Subject: [PATCH 3/5] Update loop optimisation Module is event driven and doesn't run inflight. disabled the frame behaviour --- Source/Modules/ModuleAttachmentVisuals.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Modules/ModuleAttachmentVisuals.cs b/Source/Modules/ModuleAttachmentVisuals.cs index 6dbf1e5..35d78c5 100644 --- a/Source/Modules/ModuleAttachmentVisuals.cs +++ b/Source/Modules/ModuleAttachmentVisuals.cs @@ -62,6 +62,10 @@ public override void OnStart(StartState state) CacheInitialChildren(); ParseConfig(); UpdateVisuals(); + + // make this module cheaper in update loops + isEnabled = false; + enabled = false; } public void OnDestroy() From cda8f3589040728607638e26e9b0da89b53adbe2 Mon Sep 17 00:00:00 2001 From: Spartwo Date: Sun, 20 Sep 2026 18:51:03 +0100 Subject: [PATCH 4/5] Cap Localisation The display name for the attachment visuals didn't have loc reference support isEnabled had turned off the capping toggle added general loc references using squad cfgs --- .../Localization/en-us.cfg | 3 +++ Source/Modules/ModuleAttachmentVisuals.cs | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/GameData/KSPCommunityPartModules/Localization/en-us.cfg b/GameData/KSPCommunityPartModules/Localization/en-us.cfg index 42c0ac6..3fe2888 100644 --- a/GameData/KSPCommunityPartModules/Localization/en-us.cfg +++ b/GameData/KSPCommunityPartModules/Localization/en-us.cfg @@ -10,5 +10,8 @@ Localization #KSPCPM_ToggleTracking = Toggle Tracking #KSPCPM_DisableTracking = Disable Tracking #KSPCPM_EnableTracking = Enable Tracking + + // ModuleAttachmentVisuals + #KSPCPM_AttachmentVisual = Capping } } diff --git a/Source/Modules/ModuleAttachmentVisuals.cs b/Source/Modules/ModuleAttachmentVisuals.cs index 35d78c5..8732640 100644 --- a/Source/Modules/ModuleAttachmentVisuals.cs +++ b/Source/Modules/ModuleAttachmentVisuals.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; +using KSP.Localization; namespace KSPCommunityPartModules.Modules { @@ -62,9 +63,8 @@ public override void OnStart(StartState state) CacheInitialChildren(); ParseConfig(); UpdateVisuals(); - + // make this module cheaper in update loops - isEnabled = false; enabled = false; } @@ -116,7 +116,7 @@ private void ParseConfig() // Parse transforms shown when condition is true if (!string.IsNullOrWhiteSpace(showAttached)) { - foreach (string transformName in showAttached.Split(',')) + foreach (string transformName in showAttached.Split(';')) { string name = transformName.Trim(); @@ -142,7 +142,7 @@ private void ParseConfig() // Parse transforms shown when condition is false if (!string.IsNullOrWhiteSpace(showFree)) { - foreach (string transformName in showFree.Split(',')) + foreach (string transformName in showFree.Split(';')) { string name = transformName.Trim(); @@ -191,13 +191,15 @@ private void UpdateToggleEventUI(bool allNodesAttached) // Only offer the toggle when there's actually something capped to toggle toggleEvent.active = allNodesAttached; - string verb = transformEnabled ? "Disable" : "Enable"; + string adjective = transformEnabled + ? Localizer.Format("#autoLOC_900889") + : Localizer.Format("#autoLOC_247995"); string displayName = string.IsNullOrWhiteSpace(objectDisplayName) - ? $"{verb} Capping" - : $"{verb} {objectDisplayName}"; + ? Localizer.Format("#KSPCPM_AttachmentVisual") + : Localizer.Format(objectDisplayName); - toggleEvent.guiName = displayName; + toggleEvent.guiName = $"{displayName} {adjective}"; } private void SetTransforms(List transforms, bool active) From b8815154b85ef14d23c032ca450adce6877a47af Mon Sep 17 00:00:00 2001 From: Spartwo <38593593+Spartwo@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:30:27 +0100 Subject: [PATCH 5/5] Update README with new module descriptions Added new modules with descriptions for ModuleAttachmentVisuals, ModuleExclusiveResourceConverter, and ModuleToggleTracking. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d9135bd..94cb152 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ Compatible with **KSP 1.12.3** and up - Available on [CKAN] - **ModuleDepthMask**
This module allows for parts to have hollow insets that dont clip into other parts, ideal for engine nozzles, landing gear, air intakes, solar panel bays, and more. - **ModuleNameTag**
This module adds a user-editable name tag to a part, set through an in-game window. Shared by kOS (part:TAG) and kRPC (Part.Tag) so a tag assigned by one is visible to the other. Consuming mods add the module to parts with their own ModuleManager patch; legacy KOSNameTag tags from older kOS/kRPC saves are migrated automatically. + +- **ModuleAttachmentVisuals**
This module adds configurable visibility for part objects similar to `ModuleJettison` but with improved configurability and the ability to support multiple instances on the same part. + +- **ModuleExclusiveResourceConverter**
This module is a variant of the stock resource converter which will only allow one process to run at a time (for example cannot convert LFO and Monoprop at the same time). + +- **ModuleToggleTracking**
This module allows the sun-tracking behaviour of solar/radiator panels or any `ModuelDeployablePart` instance to be enabled or disabled on the fly. [CKAN]: https://forum.kerbalspaceprogram.com/topic/197082-ckan-the-comprehensive-kerbal-archive-network-v1332-laplace-ksp-2-support/