diff --git a/Source/CapMod/EndCapper/EndCapper.csproj b/Source/CapMod/EndCapper/EndCapper.csproj index 107c328..b39b04b 100644 --- a/Source/CapMod/EndCapper/EndCapper.csproj +++ b/Source/CapMod/EndCapper/EndCapper.csproj @@ -1,4 +1,4 @@ - + @@ -12,10 +12,11 @@ v4.8.1 512 true + 11.0 true - full + portable false bin\Debug\ DEBUG;TRACE @@ -69,7 +70,7 @@ - + diff --git a/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs b/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs new file mode 100644 index 0000000..4b957f6 --- /dev/null +++ b/Source/CapMod/EndCapper/ModuleAttachmentVisuals.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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. + + // todo: serialise per-node state so that they never change during flight. + + 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; + } + + 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) + { + string listString = null; + if (!configNode.TryGetValue(listName, ref listString) || string.IsNullOrWhiteSpace(listString)) + return false; + + foreach (string transformName in listString.Split(',')) + { + Transform t = part.FindModelTransform(transformName.Trim()); + + if (t != null) + (list ??= new List()).Add(t); + else + Debug.LogError($"[ModuleAttachmentVisuals]: Could not find transform '{transformName}' on {part.name}"); + } + + return true; + } + + public void UpdateVisibility() + { + if (attachNode != null) + ApplyVisibility(attachNode.attachedPart != null); + } + + public void ApplyVisibility(bool attached) + { + showWhenAttached?.ForEach(t => t?.gameObject.SetActive(attached)); + showWhenFree?.ForEach(t => t?.gameObject.SetActive(!attached)); + } + } + + [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 = nodesCfg.Select(n => n.ToString()).ToArray(); + } + + public override void OnStart(StartState state) + { + if (nodeVisualConfigs == null) + return; + + for (int i = 0; i < nodeVisualConfigs.Length; i++) + { + try + { + var newNode = new NodeVisual(); + + if (newNode.Load(part, ConfigNode.Parse(nodeVisualConfigs[i]).GetNode("NODEVISUAL"))) + (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); + directChildren = part.children.ToHashSet(); + } + + UpdateVisuals(); + } + + public void OnDestroy() + { + if (nodeVisuals != null && HighLogic.LoadedSceneIsEditor) + GameEvents.onEditorPartEvent.Remove(OnEditorEvent); + } + + // --- Functions --- + + private void UpdateVisuals() => nodeVisuals?.ForEach(n => n.UpdateVisibility()); + + private void OnEditorEvent(ConstructionEventType evt, Part p) + { + if (evt != ConstructionEventType.PartAttached && evt != ConstructionEventType.PartDetached) + return; + + if (p == part + || (evt == ConstructionEventType.PartAttached && p.parent == part && directChildren.Add(p)) + || (evt == ConstructionEventType.PartDetached && directChildren.Remove(p))) + { + UpdateVisuals(); + } + } + } +} 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); - } - } - } -}