From 8676144bfebb12cbd357ded25034e84073e0961a Mon Sep 17 00:00:00 2001 From: Halban <30965946+Halbann@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:35:54 +0000 Subject: [PATCH 1/3] Made extensible, had to remove manual controls (for now) --- Source/CapMod/EndCapper/EndCapper.csproj | 9 +- .../EndCapper/ModuleAttachmentVisuals.cs | 210 +++++++++++++++++ Source/CapMod/EndCapper/ModuleEndCap.cs | 220 ------------------ 3 files changed, 215 insertions(+), 224 deletions(-) create mode 100644 Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs delete mode 100644 Source/CapMod/EndCapper/ModuleEndCap.cs diff --git a/Source/CapMod/EndCapper/EndCapper.csproj b/Source/CapMod/EndCapper/EndCapper.csproj index 107c328..737b33c 100644 --- a/Source/CapMod/EndCapper/EndCapper.csproj +++ b/Source/CapMod/EndCapper/EndCapper.csproj @@ -1,4 +1,4 @@ - + @@ -15,7 +15,7 @@ true - full + portable false bin\Debug\ DEBUG;TRACE @@ -65,11 +65,12 @@ ..\..\Kerbal Space Program - RP\KSP_x64_Data\Managed\UnityEngine.UI.dll - ..\..\Kerbal Space Program - RP\KSP_x64_Data\Managed\UnityEngine.UIModule.dll + G:\Games\KSP_win64\KSP_x64_Data\Managed\UnityEngine.UIModule.dll + False - + diff --git a/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs b/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs new file mode 100644 index 0000000..69d4d9e --- /dev/null +++ b/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace AttachmentVisuals +{ + public class ModuleAttachmentVisuals : PartModule + { + // todo: dynamically add buttons for manual control of each registered + // attachment node (only when advanced tweakables is enabled). Will + // need implement IConfigNode on NodeData so state can be saved rather than inferred. + + public class NodeVisual + { + public AttachNode attachNode; + + public List showWhenAttached; + public List showWhenFree; + + public bool Load(Part part, ConfigNode configNode) + { + string name = ""; + if (!configNode.TryGetValue("name", ref name)) + { + Debug.LogError("[ModuleAttachmentVisuals]: NodeVisuals is missing an attachment node name."); + return false; + } + + attachNode = part.FindAttachNode(name); + if (attachNode == null) + { + Debug.LogError($"[ModuleAttachmentVisuals]: Node '{name}' not found on part '{part.name}'"); + return false; + } + + bool valid = false; + valid = valid || TryLoadlist(ref showWhenAttached, part, configNode, "showWhenAttached"); + valid = valid || TryLoadlist(ref showWhenFree, part, configNode, "showWhenFree"); + + return valid; + } + + private bool TryLoadlist(ref List list, Part part, ConfigNode configNode, string listName) + { + string listString = null; + if (!configNode.TryGetValue(listName, ref listString)) + return false; + + if (string.IsNullOrWhiteSpace(listString)) + return false; + + string[] transformList = listString.Split(','); + + if (transformList.Length < 1) + return false; + + foreach (string transformName in transformList) + { + Transform t = part.FindModelTransform(transformName.Trim()); + + if (t != null) + (list ?? (list = new List())).Add(t); + else + Debug.LogError($"[ModuleAttachmentVisuals]: Could not find transform '{transformName}' on {part.name}"); + } + + return true; + } + + public void ApplyVisiblity(bool attached) + { + ApplyList(showWhenAttached, attached); + ApplyList(showWhenFree, !attached); + } + + private void ApplyList(List list, bool show) + { + if (list == null) + return; + + foreach (var t in list) + { + if (t == null) + continue; + + t.gameObject.SetActive(show); + } + } + } + + [SerializeField] + private string[] nodeVisualConfigs; + + [NonSerialized] + public List nodeVisuals; + + private HashSet directChildren; + + // --- Lifecycle --- + + public override void OnLoad(ConfigNode node) + { + ConfigNode[] nodesCfg = node.GetNodes("NODEVISUAL"); + if (nodesCfg.Length > 0) + { + nodeVisualConfigs = new string[nodesCfg.Length]; + for (int i = 0; i < nodesCfg.Length; i++) + nodeVisualConfigs[i] = nodesCfg[i].ToString(); + } + } + + public override void OnStart(StartState state) + { + if (nodeVisualConfigs == null) + return; + + for (int i = 0; i < nodeVisualConfigs.Length; i++) + { + try + { + NodeVisual newNode = new NodeVisual(); + + if (newNode.Load(part, ConfigNode.Parse(nodeVisualConfigs[i]).GetNode("NODEVISUAL"))) + (nodeVisuals ?? (nodeVisuals = new List())).Add(newNode); + } + catch { } + } + + // Module doesn't need to do anything from here if no valid node visuals were found. + if (nodeVisuals == null) + return; + + if (HighLogic.LoadedSceneIsEditor) + { + GameEvents.onEditorPartEvent.Add(OnEditorEvent); + CacheInitialChildren(); + } + + UpdateVisuals(); + } + + public void OnDestroy() + { + if (nodeVisuals == null) + return; + + if (HighLogic.LoadedSceneIsEditor) + GameEvents.onEditorPartEvent.Remove(OnEditorEvent); + } + + // --- Functions --- + + private void UpdateVisuals() + { + if (nodeVisuals == null) + return; + + foreach (var nodeData in nodeVisuals) + { + if (nodeData.attachNode == null) + continue; + + nodeData.ApplyVisiblity(nodeData.attachNode.attachedPart != null); + } + } + + private void OnEditorEvent(ConstructionEventType evt, Part p) + { + // Only care about attach/detach + if (evt != ConstructionEventType.PartAttached && evt != ConstructionEventType.PartDetached) + return; + + // If the event is on this part + if (part == p) + { + UpdateVisuals(); + return; + } + + // Only care about events involving this parts direct relatives + switch (evt) + { + case ConstructionEventType.PartAttached: + if (p.parent == part) + { + directChildren.Add(p); + UpdateVisuals(); + } + break; + + case ConstructionEventType.PartDetached: + if (directChildren.Contains(p)) + { + directChildren.Remove(p); + UpdateVisuals(); + } + break; + } + } + + private void CacheInitialChildren() + { + // Populate children at start to track later + directChildren = new HashSet(); + + foreach (var child in part.children) + directChildren.Add(child); + } + } +} diff --git a/Source/CapMod/EndCapper/ModuleEndCap.cs b/Source/CapMod/EndCapper/ModuleEndCap.cs deleted file mode 100644 index d084a33..0000000 --- a/Source/CapMod/EndCapper/ModuleEndCap.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -namespace EndCapper -{ - // This module is applied to parts with multiple possible jettison shrouds to allow automatic triggers - public class ModuleEndCap : PartModule - { - [KSPField] - public string nodeNames; // "node_stack_top,node_stack_bottom" - - [KSPField] - public string showAttached; // "TopCap1;TopCap2,BottomCap1;BottomCap2" - - [KSPField] - public string showFree; // "TopFree1;TopFree2,BottomFree1" - - [KSPEvent(guiActive = true, - guiActiveEditor = true, - guiName = "#LOC_KPDynamics_Capping")] - public void EventToggleTracking() => ToggleCapping(); - - [KSPField(isPersistant = true)] - public bool cappingEnabled = true; - - // Internally handle as a nodeData object //TODO: Config structure change to defined nodes - private class NodeData - { - public AttachNode node; - public List showWhenAttached = new List(); - public List showWhenFree = new List(); - - public NodeData(AttachNode node) - { - this.node = node; - } - } - - private List nodes = new List(); - - public override void OnStart(StartState state) - { - base.OnStart(state); - - if (HighLogic.LoadedSceneIsEditor) - { - GameEvents.onEditorPartEvent.Add(OnEditorEvent); - } - - CacheInitialChildren(); - ParseConfig(); - UpdateVisuals(); - - //Set starting value - Events["EventToggleTracking"].guiName = cappingEnabled ? "#LOC_KPDynamics_DisableCapping" : "#LOC_KPDynamics_EnableCapping"; - } - - public void OnDestroy() - { - if (HighLogic.LoadedSceneIsEditor) - { - GameEvents.onEditorPartEvent.Remove(OnEditorEvent); - } - } - - // Read the config file to find the node associations - private void ParseConfig() - { - nodes.Clear(); - List attachNodes = part.attachNodes; - - String test = attachNodes[0].id; - - var nodeList = nodeNames.Split(','); - var attachedList = showAttached.Split(','); - var freeList = showFree.Split(','); - - for (int i = 0; i < nodeList.Length; i++) - { - string nodeId = nodeList[i].Trim(); - AttachNode node = part.FindAttachNode(nodeId); - Debug.Log(node != null - ? $"[ModuleEndCap] Found node '{nodeId}' for part '{part.name}'" - : $"[ModuleEndCap] WARNING: Node '{nodeId}' not found on part '{part.name}'"); - if (node == null) continue; - - // Create node data - NodeData nodeData = new NodeData(node); - - // Attached transforms - if (i < attachedList.Length && !string.IsNullOrWhiteSpace(attachedList[i])) - { - foreach (var tName in attachedList[i].Split(';')) - { - Transform t = part.FindModelTransform(tName.Trim()); - if (t != null) nodeData.showWhenAttached.Add(t); - else Debug.LogWarning($"[ModuleEndCap] Could not find attached transform '{tName}' on {part.name}"); - } - } - - // Free transforms - if (i < freeList.Length && !string.IsNullOrWhiteSpace(freeList[i])) - { - foreach (var tName in freeList[i].Split(';')) - { - Transform t = part.FindModelTransform(tName.Trim()); - if (t != null) nodeData.showWhenFree.Add(t); - else Debug.LogWarning($"[ModuleEndCap] Could not find free transform '{tName}' on {part.name}"); - } - } - - nodes.Add(nodeData); - } - } - - private void UpdateVisuals() - { - foreach (var nodeData in nodes) - { - if (nodeData.node == null) - { - Debug.LogWarning("[ModuleEndCap] NodeData has null node!"); - continue; - } - - bool cappingActive = cappingEnabled && nodeData.node.attachedPart != null; - //Debug.Log($"[ModuleEndCap] Node '{nodeData.node.id}' attached? {attached}"); - - // Show model transforms - SetAttachedTransforms(!cappingActive, nodeData); - SetFreeTransforms(cappingActive, nodeData); - } - } - - private void SetFreeTransforms(bool s, NodeData n) - { - // Show attached transforms - foreach (var t in n.showWhenAttached) - { - if (t == null) - { - Debug.LogWarning($"[ModuleEndCap] showWhenAttached transform null for node '{n.node.id}'"); - continue; - } - t.gameObject.SetActive(s); - } - } - - private void SetAttachedTransforms(bool s, NodeData n) - { - // Show free transforms - foreach (var t in n.showWhenFree) - { - if (t == null) - { - Debug.LogWarning($"[ModuleEndCap] showWhenFree transform null for node '{n.node.id}'"); - continue; - } - t.gameObject.SetActive(s); - } - } - - private void ToggleCapping() - { - cappingEnabled = !cappingEnabled; - Events["EventToggleTracking"].guiName = cappingEnabled ? "#LOC_KPDynamics_DisableCapping" : "#LOC_KPDynamics_EnableCapping"; - UpdateVisuals(); - } - - private HashSet directChildren = new HashSet(); - private void OnEditorEvent(ConstructionEventType evt, Part p) - { - // Only care about attach/detach - if (evt != ConstructionEventType.PartAttached && evt != ConstructionEventType.PartDetached) - return; - - // If the event is on this part - if (part == p) - { - UpdateVisuals(); - return; - } - - bool wasDirectChild = directChildren.Contains(p); - bool isDirectChildNow = p.parent == part; - - // Only care about events involving this parts direct relatives - 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() - { - // Populate children at start to track later - directChildren.Clear(); - foreach (var child in part.children) - { - directChildren.Add(child); - } - } - } -} From 5d199f4f1b5ff7cad02c464dcd661b5d9b932766 Mon Sep 17 00:00:00 2001 From: Halban <30965946+Halbann@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:41:34 +0000 Subject: [PATCH 2/3] Reverse change to csproj --- Source/CapMod/EndCapper/EndCapper.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/CapMod/EndCapper/EndCapper.csproj b/Source/CapMod/EndCapper/EndCapper.csproj index 737b33c..5fbac35 100644 --- a/Source/CapMod/EndCapper/EndCapper.csproj +++ b/Source/CapMod/EndCapper/EndCapper.csproj @@ -65,8 +65,7 @@ ..\..\Kerbal Space Program - RP\KSP_x64_Data\Managed\UnityEngine.UI.dll - G:\Games\KSP_win64\KSP_x64_Data\Managed\UnityEngine.UIModule.dll - False + ..\..\Kerbal Space Program - RP\KSP_x64_Data\Managed\UnityEngine.UIModule.dll From 4a6391b508f8a676e47f0eed54a0c9b3a1088d8b Mon Sep 17 00:00:00 2001 From: Halban <30965946+Halbann@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:55:22 +0000 Subject: [PATCH 3/3] Code cleanup --- Source/CapMod/EndCapper/EndCapper.csproj | 1 + .../EndCapper/ModuleAttachmentVisuals.cs | 125 ++++-------------- 2 files changed, 30 insertions(+), 96 deletions(-) diff --git a/Source/CapMod/EndCapper/EndCapper.csproj b/Source/CapMod/EndCapper/EndCapper.csproj index 5fbac35..b39b04b 100644 --- a/Source/CapMod/EndCapper/EndCapper.csproj +++ b/Source/CapMod/EndCapper/EndCapper.csproj @@ -12,6 +12,7 @@ v4.8.1 512 true + 11.0 true diff --git a/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs b/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs index 69d4d9e..4b957f6 100644 --- a/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs +++ b/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace AttachmentVisuals @@ -10,10 +11,11 @@ public class ModuleAttachmentVisuals : PartModule // attachment node (only when advanced tweakables is enabled). Will // need implement IConfigNode on NodeData so state can be saved rather than inferred. + // todo: serialise per-node state so that they never change during flight. + public class NodeVisual { public AttachNode attachNode; - public List showWhenAttached; public List showWhenFree; @@ -33,33 +35,22 @@ public bool Load(Part part, ConfigNode configNode) return false; } - bool valid = false; - valid = valid || TryLoadlist(ref showWhenAttached, part, configNode, "showWhenAttached"); - valid = valid || TryLoadlist(ref showWhenFree, part, configNode, "showWhenFree"); - - return valid; + return TryLoadList(ref showWhenAttached, part, configNode, "showWhenAttached") + | TryLoadList(ref showWhenFree, part, configNode, "showWhenFree"); } - private bool TryLoadlist(ref List list, Part part, ConfigNode configNode, string listName) + private bool TryLoadList(ref List list, Part part, ConfigNode configNode, string listName) { string listString = null; - if (!configNode.TryGetValue(listName, ref listString)) - return false; - - if (string.IsNullOrWhiteSpace(listString)) + if (!configNode.TryGetValue(listName, ref listString) || string.IsNullOrWhiteSpace(listString)) return false; - string[] transformList = listString.Split(','); - - if (transformList.Length < 1) - return false; - - foreach (string transformName in transformList) + foreach (string transformName in listString.Split(',')) { Transform t = part.FindModelTransform(transformName.Trim()); if (t != null) - (list ?? (list = new List())).Add(t); + (list ??= new List()).Add(t); else Debug.LogError($"[ModuleAttachmentVisuals]: Could not find transform '{transformName}' on {part.name}"); } @@ -67,46 +58,33 @@ private bool TryLoadlist(ref List list, Part part, ConfigNode configN return true; } - public void ApplyVisiblity(bool attached) + public void UpdateVisibility() { - ApplyList(showWhenAttached, attached); - ApplyList(showWhenFree, !attached); + if (attachNode != null) + ApplyVisibility(attachNode.attachedPart != null); } - private void ApplyList(List list, bool show) + public void ApplyVisibility(bool attached) { - if (list == null) - return; - - foreach (var t in list) - { - if (t == null) - continue; - - t.gameObject.SetActive(show); - } + showWhenAttached?.ForEach(t => t?.gameObject.SetActive(attached)); + showWhenFree?.ForEach(t => t?.gameObject.SetActive(!attached)); } } - [SerializeField] - private string[] nodeVisualConfigs; - - [NonSerialized] - public List nodeVisuals; - + [NonSerialized] public List nodeVisuals; + [SerializeField] private string[] nodeVisualConfigs; private HashSet directChildren; // --- Lifecycle --- public override void OnLoad(ConfigNode node) { + if (nodeVisualConfigs != null) + return; + ConfigNode[] nodesCfg = node.GetNodes("NODEVISUAL"); if (nodesCfg.Length > 0) - { - nodeVisualConfigs = new string[nodesCfg.Length]; - for (int i = 0; i < nodesCfg.Length; i++) - nodeVisualConfigs[i] = nodesCfg[i].ToString(); - } + nodeVisualConfigs = nodesCfg.Select(n => n.ToString()).ToArray(); } public override void OnStart(StartState state) @@ -118,10 +96,10 @@ public override void OnStart(StartState state) { try { - NodeVisual newNode = new NodeVisual(); + var newNode = new NodeVisual(); if (newNode.Load(part, ConfigNode.Parse(nodeVisualConfigs[i]).GetNode("NODEVISUAL"))) - (nodeVisuals ?? (nodeVisuals = new List())).Add(newNode); + (nodeVisuals ??= new List()).Add(newNode); } catch { } } @@ -133,7 +111,7 @@ public override void OnStart(StartState state) if (HighLogic.LoadedSceneIsEditor) { GameEvents.onEditorPartEvent.Add(OnEditorEvent); - CacheInitialChildren(); + directChildren = part.children.ToHashSet(); } UpdateVisuals(); @@ -141,70 +119,25 @@ public override void OnStart(StartState state) public void OnDestroy() { - if (nodeVisuals == null) - return; - - if (HighLogic.LoadedSceneIsEditor) + if (nodeVisuals != null && HighLogic.LoadedSceneIsEditor) GameEvents.onEditorPartEvent.Remove(OnEditorEvent); } // --- Functions --- - private void UpdateVisuals() - { - if (nodeVisuals == null) - return; - - foreach (var nodeData in nodeVisuals) - { - if (nodeData.attachNode == null) - continue; - - nodeData.ApplyVisiblity(nodeData.attachNode.attachedPart != null); - } - } + private void UpdateVisuals() => nodeVisuals?.ForEach(n => n.UpdateVisibility()); private void OnEditorEvent(ConstructionEventType evt, Part p) { - // Only care about attach/detach if (evt != ConstructionEventType.PartAttached && evt != ConstructionEventType.PartDetached) return; - // If the event is on this part - if (part == p) + if (p == part + || (evt == ConstructionEventType.PartAttached && p.parent == part && directChildren.Add(p)) + || (evt == ConstructionEventType.PartDetached && directChildren.Remove(p))) { UpdateVisuals(); - return; - } - - // Only care about events involving this parts direct relatives - switch (evt) - { - case ConstructionEventType.PartAttached: - if (p.parent == part) - { - directChildren.Add(p); - UpdateVisuals(); - } - break; - - case ConstructionEventType.PartDetached: - if (directChildren.Contains(p)) - { - directChildren.Remove(p); - UpdateVisuals(); - } - break; } } - - private void CacheInitialChildren() - { - // Populate children at start to track later - directChildren = new HashSet(); - - foreach (var child in part.children) - directChildren.Add(child); - } } }