From 56cf9e9df791d7217539e9a735411429e8e501cf Mon Sep 17 00:00:00 2001 From: djungelorm Date: Fri, 17 Jul 2026 17:26:55 +0100 Subject: [PATCH 1/9] Import kOS name-tag part module verbatim Copies kOS's KOSNameTag part module and its KOSNameTagWindow editor UI unchanged from the kOS repository (src/kOS/Module/KOSNameTag.cs and src/kOS/Screen/KOSNameTagWindow.cs) as the starting point for a shared name-tag module. This commit is a faithful byte-for-byte snapshot; it is not yet wired into the build. Subsequent commits adapt it for KSPCommunityPartModules. Provenance: adapted from the kOS mod (https://github.com/KSP-KOS/KOS), which is licensed under the GPLv3. See issue krpc/krpc#829. --- Source/Modules/KOSNameTag.cs | 119 +++++++++++++++++ Source/Modules/KOSNameTagWindow.cs | 197 +++++++++++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 Source/Modules/KOSNameTag.cs create mode 100644 Source/Modules/KOSNameTagWindow.cs diff --git a/Source/Modules/KOSNameTag.cs b/Source/Modules/KOSNameTag.cs new file mode 100644 index 0000000..3723407 --- /dev/null +++ b/Source/Modules/KOSNameTag.cs @@ -0,0 +1,119 @@ +using kOS.Screen; +using kOS.Suffixed; +using kOS.Safe.Utilities; +using UnityEngine; +using System; + +namespace kOS.Module +{ + public class KOSNameTag : PartModule + { + private const string PAWGroup = "kOS"; + + private KOSNameTagWindow typingWindow; + + [KSPField(isPersistant = true, + guiActive = true, + guiActiveEditor = true, + guiName = "name tag", + groupName = PAWGroup, + groupDisplayName = PAWGroup)] + public string nameTag = ""; + + [KSPEvent(guiActive = true, + guiActiveEditor = true, + guiName = "Change Name Tag", + groupName = PAWGroup, + groupDisplayName = PAWGroup)] + public void PopupNameTagChanger() + { + if (typingWindow != null) + typingWindow.Close(); + if (HighLogic.LoadedSceneIsEditor) + { + EditorFacility whichEditor = EditorLogic.fetch.ship.shipFacility; + if (!(Career.CanTagInEditor(whichEditor))) + { + var formattedString = string.Format("The {0} requires an upgrade to assign name tags", whichEditor); + ScreenMessages.PostScreenMessage(formattedString, 6, ScreenMessageStyle.UPPER_CENTER); + return; + } + } + // Make a new instance of typingWindow, replacing the existing one if there was one: + KOSNameTagWindow oldTypingWindow = gameObject.GetComponent(); + if (oldTypingWindow != null) + Destroy(oldTypingWindow); + typingWindow = gameObject.AddComponent(); + typingWindow.Invoke(this, nameTag); + } + + // For issue #2764, this enforces a rule that says regardless of what ModuleManager + // rules end up doing, there shall only ever be one KosNameTag per part: + public override void OnAwake() + { + // If other instances of me exist in this part, remove them. I am replacing them: + for (int i = part.Modules.Count - 1; i >= 0; --i) + { + PartModule pm = part.Modules[i]; + if (pm != this && pm is KOSNameTag) + { + SafeHouse.Logger.Log(string.Format( + "Removing duplicate KOSNameTag PartModule from {0}. KOS cannot deal with more than one tag per part.", part.name)); + part.RemoveModule(pm); + } + } + + // make this module cheaper in update loops + isEnabled = false; + enabled = false; + } + + public void TypingDone(string newValue) + { + nameTag = newValue; + TypingCancel(); + } + + public void TypingCancel() + { + typingWindow.Close(); + Destroy(typingWindow); + typingWindow = null; + } + } + + // setting isEnabled to false prevents the nametag from showing up in the PAW...work around that. + [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] + class KOSNameTagActivationManager : MonoBehaviour + { + void Awake() + { + GameEvents.onPartActionUICreate.Add(OnPartActionUICreate); + GameEvents.onPartActionUIShown.Add(OnPartActionUIShown); + } + + void OnDestroy() + { + GameEvents.onPartActionUICreate.Remove(OnPartActionUICreate); + GameEvents.onPartActionUIShown.Remove(OnPartActionUIShown); + } + + private void OnPartActionUICreate(Part part) + { + var nameTagModule = part.FindModuleImplementing(); + if (nameTagModule != null) + { + nameTagModule.isEnabled = true; + } + } + + private void OnPartActionUIShown(UIPartActionWindow paw, Part part) + { + var nameTagModule = part.FindModuleImplementing(); + if (nameTagModule != null) + { + nameTagModule.isEnabled = false; + } + } + } +} \ No newline at end of file diff --git a/Source/Modules/KOSNameTagWindow.cs b/Source/Modules/KOSNameTagWindow.cs new file mode 100644 index 0000000..2a6b462 --- /dev/null +++ b/Source/Modules/KOSNameTagWindow.cs @@ -0,0 +1,197 @@ +using kOS.Utilities; +using UnityEngine; +using kOS.Module; +using System; + +namespace kOS.Screen +{ + public class KOSNameTagWindow : MonoBehaviour + { + private KOSNameTag attachedModule; + private Rect windowRect; + private string tagValue; + // ReSharper disable RedundantDefaultFieldInitializer + private bool wasFocusedOnce = false; // "explicit", not "redundant". + private int numberOfRepaints = 0; // "explicit", not "redundant". + private bool gameEventHooksExist = false; // "explicit", not "redundant". + private int myWindowId; // must be unique for Unity to not mash two nametag windows togehter. + + // ReSharper enable RedundantDefaultFieldInitializer + + public void Invoke(KOSNameTag module, string oldValue) + { + attachedModule = module; + tagValue = oldValue; + myWindowId = GetInstanceID(); // Use the Id of this MonoBehaviour to guarantee unique window ID. + + Vector3 screenPos = GetViewportPosFor(attachedModule.part.transform.position); + + // screenPos is in coords from 0 to 1, 0 to 1, not screen pixel coords. + // Transform it to pixel coords: + float xPixelPoint = screenPos.x * UnityEngine.Screen.width; + float yPixelPoint = (1-screenPos.y) * UnityEngine.Screen.height; + const float WINDOW_WIDTH = 200; + + // windowRect = new Rect(xPixelWindow, yPixelPoint, windowWidth, 130); + windowRect = new Rect(xPixelPoint, yPixelPoint, WINDOW_WIDTH, 130); + + // Please don't delete these. They're not being used, but that's because we haven't + // finished prettying up the interface with the tag line and so the coords aren't + // being made use of yet. But keep this in the code so I can remember how I did the math: + // -------------------------------------------------------------------------------------- + // bool drawOnLeft = (screenPos.x > 0.5f); + // float xPixelWindow = (drawOnLeft ? screenPos.x - 0.3f : screenPos.x + 0.2f) * UnityEngine.Screen.width; + // float tagLineWidth = (drawOnLeft) ? (xPixelPoint - xPixelWindow) : (xPixelWindow - xPixelPoint - windowWidth); + // tagLineRect = new Rect(xPixelPoint, yPixelPoint, tagLineWidth, 3); + // SafeHouse.Logger.Log("tagLineRect = " + tagLineRect ); + + SetEnabled(true); + + if (HighLogic.LoadedSceneIsEditor) + attachedModule.part.SetHighlight(false, false); + + } + + /// + /// Catch the event of the part disappearing, from crashing or + /// from unloading from distance or scene change, and ensure + /// the window closes if it was open when that happens: + /// + /// The callback is called for EVERY part + /// that ever goes away, so we have to check if it's the right one + public void GoAwayEventCallback(Part whichPartWentAway) + { + if (whichPartWentAway != attachedModule.part) + return; + + Close(); + } + + /// + /// If you try to set a Unity.Behaviour.enabled to false when it already IS false, + /// and Unity hasn't fully finished configuring the MonoBehaviour yet, the Property's + /// "set" code throws a null ref error. How lame is that? + /// That's why I wrapped every attempt to set enabled's value with this check, because KSP + /// tries running my hooks in this class before Unity's ready for them. + /// + private void SetEnabled(bool newVal) + { + // ReSharper disable once RedundantCheckBeforeAssignment + if (newVal != enabled) + enabled = newVal; + + if (enabled) + { + if (! gameEventHooksExist) + { + GameEvents.onPartDestroyed.Add(GoAwayEventCallback); + GameEvents.onPartDie.Add(GoAwayEventCallback); + gameEventHooksExist = true; + } + } + else + { + if (gameEventHooksExist) + { + GameEvents.onPartDestroyed.Remove(GoAwayEventCallback); + GameEvents.onPartDie.Remove(GoAwayEventCallback); + gameEventHooksExist = false; + } + } + } + + /// + /// Get the position in screen coordinates that the 3d world coordinates + /// project onto, abstracting the two different ways KSP has to access + /// the camera depending on view mode. + /// Returned coords are in a system where the screen viewport goes from + /// (0,0) to (1,1) and the Z coord is how far from the screen it is + /// (-Z means behind you). + /// + private Vector3 GetViewportPosFor( Vector3 v ) + { + return Utils.GetCurrentCamera().WorldToViewportPoint(v); + } + + public void OnGUI() + { + if (Event.current.type != EventType.Repaint) + ++numberOfRepaints; + + if (! enabled) + return; + if (HighLogic.LoadedSceneIsEditor) + EditorLogic.fetch.Lock(false, false, false, "KOSNameTagLock"); + + GUI.skin = HighLogic.Skin; + GUILayout.Window(myWindowId, windowRect, DrawWindow,"KOS nametag"); + + // Ensure that the first time the window is made, it gets keybaord focus, + // but allow the focus to leave the window after that: + // The reason for the "number of repaints" check is that OnGUI has to run + // through several initial passes before all the components are present, + // and if you call FocusControl on the first few passes, it has no effect. + if (numberOfRepaints >= 2 && ! wasFocusedOnce) + { + GUI.FocusControl("NameTagField"); + wasFocusedOnce = true; + } + } + + public void DrawWindow( int windowID ) + { + if (! enabled) + return; + + Event e = Event.current; + if (e.type == EventType.KeyDown) + { + if (e.keyCode == KeyCode.Return || + e.keyCode == KeyCode.KeypadEnter) + { + e.Use(); + attachedModule.TypingDone(tagValue); + Close(); + } + } + GUILayout.Label(attachedModule.part.name); + GUI.SetNextControlName("NameTagField"); + tagValue = GUILayout.TextField( tagValue, GUILayout.MinWidth(160f)); + + GUILayout.BeginHorizontal(); + if (GUILayout.Button("Cancel")) + { + e.Use(); + Close(); + } + if (GUILayout.Button("Accept")) + { + e.Use(); + attachedModule.TypingDone(tagValue); + Close(); + } + GUILayout.EndHorizontal(); + + // Before going any further, suppress any remaining unprocessed clicks + // so they don't end up causing the editor to detach parts: + if (e.type == EventType.MouseDown || e.type == EventType.MouseUp || e.type == EventType.MouseDrag) + e.Use(); + } + + public void Close() + { + if (HighLogic.LoadedSceneIsEditor) + EditorLogic.fetch.Unlock("KOSNameTagLock"); + + SetEnabled(false); + + if (HighLogic.LoadedSceneIsEditor) + attachedModule.part.SetHighlight(false, false); + } + + public void OnDestroy() + { + SetEnabled(false); + } + } +} \ No newline at end of file From b219c4ec6b0efef8ef61b1ffc17ff4f116a43643 Mon Sep 17 00:00:00 2001 From: djungelorm Date: Fri, 17 Jul 2026 17:31:38 +0100 Subject: [PATCH 2/9] Adapt the imported name-tag module to build in KSPCommunityPartModules Moves both files into the KSPCommunityPartModules.Modules namespace, removes kOS-only dependencies (SafeHouse.Logger becomes Debug.Log with a [MODULENAME] prefix; the camera helper and editor-building career gate are inlined so kOS's Utils/Career/Safe assemblies are no longer needed), and wires the two files into the (non-globbed) project file. The class name KOSNameTag and the persistent nameTag field are unchanged for now; a later commit renames the module. --- Source/KSPCommunityPartModules.csproj | 2 + Source/Modules/KOSNameTag.cs | 55 +++++++++++++++++------ Source/Modules/KOSNameTagWindow.cs | 63 +++++++++++++-------------- 3 files changed, 74 insertions(+), 46 deletions(-) diff --git a/Source/KSPCommunityPartModules.csproj b/Source/KSPCommunityPartModules.csproj index 134a31c..80dc00d 100644 --- a/Source/KSPCommunityPartModules.csproj +++ b/Source/KSPCommunityPartModules.csproj @@ -35,6 +35,8 @@ + + diff --git a/Source/Modules/KOSNameTag.cs b/Source/Modules/KOSNameTag.cs index 3723407..91868b1 100644 --- a/Source/Modules/KOSNameTag.cs +++ b/Source/Modules/KOSNameTag.cs @@ -1,15 +1,19 @@ -using kOS.Screen; -using kOS.Suffixed; -using kOS.Safe.Utilities; +/* + Usecase: A user-editable name tag on any part, readable and writable by other mods. + Example: kOS's part:TAG suffix and kRPC's Part.Tag both read and write this module's value. + Originally By: kOS contributors (Dunbaratu et al.) + Originally For: kOS +*/ using UnityEngine; -using System; -namespace kOS.Module +namespace KSPCommunityPartModules.Modules { public class KOSNameTag : PartModule { + public const string MODULENAME = nameof(KOSNameTag); + private const string PAWGroup = "kOS"; - + private KOSNameTagWindow typingWindow; [KSPField(isPersistant = true, @@ -32,7 +36,7 @@ public void PopupNameTagChanger() if (HighLogic.LoadedSceneIsEditor) { EditorFacility whichEditor = EditorLogic.fetch.ship.shipFacility; - if (!(Career.CanTagInEditor(whichEditor))) + if (!CanTagInEditor(whichEditor)) { var formattedString = string.Format("The {0} requires an upgrade to assign name tags", whichEditor); ScreenMessages.PostScreenMessage(formattedString, 6, ScreenMessageStyle.UPPER_CENTER); @@ -42,13 +46,13 @@ public void PopupNameTagChanger() // Make a new instance of typingWindow, replacing the existing one if there was one: KOSNameTagWindow oldTypingWindow = gameObject.GetComponent(); if (oldTypingWindow != null) - Destroy(oldTypingWindow); + Destroy(oldTypingWindow); typingWindow = gameObject.AddComponent(); typingWindow.Invoke(this, nameTag); } - // For issue #2764, this enforces a rule that says regardless of what ModuleManager - // rules end up doing, there shall only ever be one KosNameTag per part: + // For kOS issue #2764, this enforces a rule that says regardless of what ModuleManager + // rules end up doing, there shall only ever be one name tag module per part: public override void OnAwake() { // If other instances of me exist in this part, remove them. I am replacing them: @@ -57,8 +61,9 @@ public override void OnAwake() PartModule pm = part.Modules[i]; if (pm != this && pm is KOSNameTag) { - SafeHouse.Logger.Log(string.Format( - "Removing duplicate KOSNameTag PartModule from {0}. KOS cannot deal with more than one tag per part.", part.name)); + Debug.Log(string.Format( + "[{0}] Removing duplicate name tag PartModule from {1}. Only one tag per part is supported.", + MODULENAME, part.name)); part.RemoveModule(pm); } } @@ -80,6 +85,28 @@ public void TypingCancel() Destroy(typingWindow); typingWindow = null; } + + /// + /// Whether the current editor building is upgraded enough to allow assigning a name tag. + /// In flight there is no such restriction. Tagging unlocks at the same point the game starts + /// unlocking basic action groups. + /// + private static bool CanTagInEditor(EditorFacility whichEditor) + { + float buildingLevel; + switch (whichEditor) + { + case EditorFacility.VAB: + buildingLevel = ScenarioUpgradeableFacilities.GetFacilityLevel(SpaceCenterFacility.VehicleAssemblyBuilding); + break; + case EditorFacility.SPH: + buildingLevel = ScenarioUpgradeableFacilities.GetFacilityLevel(SpaceCenterFacility.SpaceplaneHangar); + break; + default: + return false; + } + return GameVariables.Instance.UnlockedActionGroupsStock(buildingLevel, false); + } } // setting isEnabled to false prevents the nametag from showing up in the PAW...work around that. @@ -97,7 +124,7 @@ void OnDestroy() GameEvents.onPartActionUICreate.Remove(OnPartActionUICreate); GameEvents.onPartActionUIShown.Remove(OnPartActionUIShown); } - + private void OnPartActionUICreate(Part part) { var nameTagModule = part.FindModuleImplementing(); @@ -116,4 +143,4 @@ private void OnPartActionUIShown(UIPartActionWindow paw, Part part) } } } -} \ No newline at end of file +} diff --git a/Source/Modules/KOSNameTagWindow.cs b/Source/Modules/KOSNameTagWindow.cs index 2a6b462..dbce580 100644 --- a/Source/Modules/KOSNameTagWindow.cs +++ b/Source/Modules/KOSNameTagWindow.cs @@ -1,10 +1,12 @@ -using kOS.Utilities; +/* + Usecase: The in-editor / in-flight IMGUI window used to edit a part's name tag. + Originally By: kOS contributors (Dunbaratu et al.) + Originally For: kOS +*/ using UnityEngine; -using kOS.Module; -using System; -namespace kOS.Screen -{ +namespace KSPCommunityPartModules.Modules +{ public class KOSNameTagWindow : MonoBehaviour { private KOSNameTag attachedModule; @@ -15,7 +17,7 @@ public class KOSNameTagWindow : MonoBehaviour private int numberOfRepaints = 0; // "explicit", not "redundant". private bool gameEventHooksExist = false; // "explicit", not "redundant". private int myWindowId; // must be unique for Unity to not mash two nametag windows togehter. - + // ReSharper enable RedundantDefaultFieldInitializer public void Invoke(KOSNameTag module, string oldValue) @@ -23,7 +25,7 @@ public void Invoke(KOSNameTag module, string oldValue) attachedModule = module; tagValue = oldValue; myWindowId = GetInstanceID(); // Use the Id of this MonoBehaviour to guarantee unique window ID. - + Vector3 screenPos = GetViewportPosFor(attachedModule.part.transform.position); // screenPos is in coords from 0 to 1, 0 to 1, not screen pixel coords. @@ -32,26 +34,15 @@ public void Invoke(KOSNameTag module, string oldValue) float yPixelPoint = (1-screenPos.y) * UnityEngine.Screen.height; const float WINDOW_WIDTH = 200; - // windowRect = new Rect(xPixelWindow, yPixelPoint, windowWidth, 130); windowRect = new Rect(xPixelPoint, yPixelPoint, WINDOW_WIDTH, 130); - // Please don't delete these. They're not being used, but that's because we haven't - // finished prettying up the interface with the tag line and so the coords aren't - // being made use of yet. But keep this in the code so I can remember how I did the math: - // -------------------------------------------------------------------------------------- - // bool drawOnLeft = (screenPos.x > 0.5f); - // float xPixelWindow = (drawOnLeft ? screenPos.x - 0.3f : screenPos.x + 0.2f) * UnityEngine.Screen.width; - // float tagLineWidth = (drawOnLeft) ? (xPixelPoint - xPixelWindow) : (xPixelWindow - xPixelPoint - windowWidth); - // tagLineRect = new Rect(xPixelPoint, yPixelPoint, tagLineWidth, 3); - // SafeHouse.Logger.Log("tagLineRect = " + tagLineRect ); - SetEnabled(true); if (HighLogic.LoadedSceneIsEditor) attachedModule.part.SetHighlight(false, false); - + } - + /// /// Catch the event of the part disappearing, from crashing or /// from unloading from distance or scene change, and ensure @@ -63,7 +54,7 @@ public void GoAwayEventCallback(Part whichPartWentAway) { if (whichPartWentAway != attachedModule.part) return; - + Close(); } @@ -94,8 +85,8 @@ private void SetEnabled(bool newVal) if (gameEventHooksExist) { GameEvents.onPartDestroyed.Remove(GoAwayEventCallback); - GameEvents.onPartDie.Remove(GoAwayEventCallback); - gameEventHooksExist = false; + GameEvents.onPartDie.Remove(GoAwayEventCallback); + gameEventHooksExist = false; } } } @@ -110,14 +101,22 @@ private void SetEnabled(bool newVal) /// private Vector3 GetViewportPosFor( Vector3 v ) { - return Utils.GetCurrentCamera().WorldToViewportPoint(v); + return GetCurrentCamera().WorldToViewportPoint(v); + } + + private static Camera GetCurrentCamera() + { + return HighLogic.LoadedSceneIsEditor ? + EditorLogic.fetch.editorCamera : + (MapView.MapIsEnabled ? + PlanetariumCamera.Camera : FlightCamera.fetch.mainCamera); } - + public void OnGUI() { if (Event.current.type != EventType.Repaint) ++numberOfRepaints; - + if (! enabled) return; if (HighLogic.LoadedSceneIsEditor) @@ -125,7 +124,7 @@ public void OnGUI() GUI.skin = HighLogic.Skin; GUILayout.Window(myWindowId, windowRect, DrawWindow,"KOS nametag"); - + // Ensure that the first time the window is made, it gets keybaord focus, // but allow the focus to leave the window after that: // The reason for the "number of repaints" check is that OnGUI has to run @@ -142,7 +141,7 @@ public void DrawWindow( int windowID ) { if (! enabled) return; - + Event e = Event.current; if (e.type == EventType.KeyDown) { @@ -177,21 +176,21 @@ public void DrawWindow( int windowID ) if (e.type == EventType.MouseDown || e.type == EventType.MouseUp || e.type == EventType.MouseDrag) e.Use(); } - + public void Close() { if (HighLogic.LoadedSceneIsEditor) EditorLogic.fetch.Unlock("KOSNameTagLock"); - + SetEnabled(false); if (HighLogic.LoadedSceneIsEditor) attachedModule.part.SetHighlight(false, false); } - + public void OnDestroy() { SetEnabled(false); } } -} \ No newline at end of file +} From 6afaacd5abe9f20ab25b773ae2a6f3cfa1535fa8 Mon Sep 17 00:00:00 2001 From: djungelorm Date: Fri, 17 Jul 2026 17:33:31 +0100 Subject: [PATCH 3/9] Rename the module to ModuleNameTag Renames the part module class KOSNameTag to ModuleNameTag (and its files and the window/activation-manager helper types) to follow the KSPCommunityPartModules Module* naming convention and to drop the kOS-specific name. The persistent nameTag field is deliberately unchanged so it remains the save key; a Harmony shim in a later commit migrates old KOSNameTag save nodes to the new module name. User-facing strings are neutralized (window title and PAW group). --- Source/KSPCommunityPartModules.csproj | 4 ++-- .../{KOSNameTag.cs => ModuleNameTag.cs} | 20 +++++++++---------- .../{KOSNameTagWindow.cs => NameTagWindow.cs} | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) rename Source/Modules/{KOSNameTag.cs => ModuleNameTag.cs} (88%) rename Source/Modules/{KOSNameTagWindow.cs => NameTagWindow.cs} (97%) diff --git a/Source/KSPCommunityPartModules.csproj b/Source/KSPCommunityPartModules.csproj index 80dc00d..ea4f378 100644 --- a/Source/KSPCommunityPartModules.csproj +++ b/Source/KSPCommunityPartModules.csproj @@ -35,8 +35,8 @@ - - + + diff --git a/Source/Modules/KOSNameTag.cs b/Source/Modules/ModuleNameTag.cs similarity index 88% rename from Source/Modules/KOSNameTag.cs rename to Source/Modules/ModuleNameTag.cs index 91868b1..2a3cb04 100644 --- a/Source/Modules/KOSNameTag.cs +++ b/Source/Modules/ModuleNameTag.cs @@ -8,13 +8,13 @@ namespace KSPCommunityPartModules.Modules { - public class KOSNameTag : PartModule + public class ModuleNameTag : PartModule { - public const string MODULENAME = nameof(KOSNameTag); + public const string MODULENAME = nameof(ModuleNameTag); - private const string PAWGroup = "kOS"; + private const string PAWGroup = "Name Tag"; - private KOSNameTagWindow typingWindow; + private NameTagWindow typingWindow; [KSPField(isPersistant = true, guiActive = true, @@ -44,10 +44,10 @@ public void PopupNameTagChanger() } } // Make a new instance of typingWindow, replacing the existing one if there was one: - KOSNameTagWindow oldTypingWindow = gameObject.GetComponent(); + NameTagWindow oldTypingWindow = gameObject.GetComponent(); if (oldTypingWindow != null) Destroy(oldTypingWindow); - typingWindow = gameObject.AddComponent(); + typingWindow = gameObject.AddComponent(); typingWindow.Invoke(this, nameTag); } @@ -59,7 +59,7 @@ public override void OnAwake() for (int i = part.Modules.Count - 1; i >= 0; --i) { PartModule pm = part.Modules[i]; - if (pm != this && pm is KOSNameTag) + if (pm != this && pm is ModuleNameTag) { Debug.Log(string.Format( "[{0}] Removing duplicate name tag PartModule from {1}. Only one tag per part is supported.", @@ -111,7 +111,7 @@ private static bool CanTagInEditor(EditorFacility whichEditor) // setting isEnabled to false prevents the nametag from showing up in the PAW...work around that. [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] - class KOSNameTagActivationManager : MonoBehaviour + class NameTagActivationManager : MonoBehaviour { void Awake() { @@ -127,7 +127,7 @@ void OnDestroy() private void OnPartActionUICreate(Part part) { - var nameTagModule = part.FindModuleImplementing(); + var nameTagModule = part.FindModuleImplementing(); if (nameTagModule != null) { nameTagModule.isEnabled = true; @@ -136,7 +136,7 @@ private void OnPartActionUICreate(Part part) private void OnPartActionUIShown(UIPartActionWindow paw, Part part) { - var nameTagModule = part.FindModuleImplementing(); + var nameTagModule = part.FindModuleImplementing(); if (nameTagModule != null) { nameTagModule.isEnabled = false; diff --git a/Source/Modules/KOSNameTagWindow.cs b/Source/Modules/NameTagWindow.cs similarity index 97% rename from Source/Modules/KOSNameTagWindow.cs rename to Source/Modules/NameTagWindow.cs index dbce580..44edb11 100644 --- a/Source/Modules/KOSNameTagWindow.cs +++ b/Source/Modules/NameTagWindow.cs @@ -7,9 +7,9 @@ namespace KSPCommunityPartModules.Modules { - public class KOSNameTagWindow : MonoBehaviour + public class NameTagWindow : MonoBehaviour { - private KOSNameTag attachedModule; + private ModuleNameTag attachedModule; private Rect windowRect; private string tagValue; // ReSharper disable RedundantDefaultFieldInitializer @@ -20,7 +20,7 @@ public class KOSNameTagWindow : MonoBehaviour // ReSharper enable RedundantDefaultFieldInitializer - public void Invoke(KOSNameTag module, string oldValue) + public void Invoke(ModuleNameTag module, string oldValue) { attachedModule = module; tagValue = oldValue; @@ -123,7 +123,7 @@ public void OnGUI() EditorLogic.fetch.Lock(false, false, false, "KOSNameTagLock"); GUI.skin = HighLogic.Skin; - GUILayout.Window(myWindowId, windowRect, DrawWindow,"KOS nametag"); + GUILayout.Window(myWindowId, windowRect, DrawWindow,"Name tag"); // Ensure that the first time the window is made, it gets keybaord focus, // but allow the focus to leave the window after that: From e2d4b650e1cf073ce1997343f0c02a6e4ebbb2b0 Mon Sep 17 00:00:00 2001 From: djungelorm Date: Fri, 17 Jul 2026 17:34:31 +0100 Subject: [PATCH 4/9] Fix name-tag editor window click-through and lock-id collision The window locked editor input with EditorLogic.fetch.Lock(false, false, false, ...), whose all-false flags never lock part pick/place, so clicking Accept/Cancel dragged whatever part sat behind the button. Use an InputLockManager EDITOR_LOCK instead, which includes pick/place, and pair a matching RemoveControlLock in Close(). The lock ID was also a fixed string shared by every window, so closing one open name-tag window released the lock for all of them. Make the lock ID per-window (keyed on the instance id). --- Source/Modules/NameTagWindow.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Modules/NameTagWindow.cs b/Source/Modules/NameTagWindow.cs index 44edb11..d726899 100644 --- a/Source/Modules/NameTagWindow.cs +++ b/Source/Modules/NameTagWindow.cs @@ -17,6 +17,7 @@ public class NameTagWindow : MonoBehaviour private int numberOfRepaints = 0; // "explicit", not "redundant". private bool gameEventHooksExist = false; // "explicit", not "redundant". private int myWindowId; // must be unique for Unity to not mash two nametag windows togehter. + private string lockId; // per-window editor input lock, so two open windows don't unlock each other. // ReSharper enable RedundantDefaultFieldInitializer @@ -25,6 +26,7 @@ public void Invoke(ModuleNameTag module, string oldValue) attachedModule = module; tagValue = oldValue; myWindowId = GetInstanceID(); // Use the Id of this MonoBehaviour to guarantee unique window ID. + lockId = "NameTagLock" + myWindowId; // unique per window, so closing one doesn't unlock another. Vector3 screenPos = GetViewportPosFor(attachedModule.part.transform.position); @@ -120,7 +122,11 @@ public void OnGUI() if (! enabled) return; if (HighLogic.LoadedSceneIsEditor) - EditorLogic.fetch.Lock(false, false, false, "KOSNameTagLock"); + // Lock the whole editor (including part pick/place) while the window is up. The old + // EditorLogic.fetch.Lock(false, false, false, ...) passed all-false flags, so it never + // locked pick/place and clicks on Accept/Cancel fell through and dragged the part + // behind the window. SetControlLock is keyed by lockId and is safe to call every frame. + InputLockManager.SetControlLock(ControlTypes.EDITOR_LOCK, lockId); GUI.skin = HighLogic.Skin; GUILayout.Window(myWindowId, windowRect, DrawWindow,"Name tag"); @@ -180,7 +186,7 @@ public void DrawWindow( int windowID ) public void Close() { if (HighLogic.LoadedSceneIsEditor) - EditorLogic.fetch.Unlock("KOSNameTagLock"); + InputLockManager.RemoveControlLock(lockId); SetEnabled(false); From bfa63386aa80f76173339003d1a8c217ce1f7500 Mon Sep 17 00:00:00 2001 From: djungelorm Date: Fri, 17 Jul 2026 17:35:09 +0100 Subject: [PATCH 5/9] Simplify the name-tag module for shared use Removes the isEnabled=false optimization and the NameTagActivationManager KSPAddon that existed only to re-show the field in the part action window after that optimization hid it. The field is guiActive already, so it shows in the PAW directly. The OnAwake duplicate-removal guard (kOS #2764, for parts that get the module added twice) is kept. --- Source/Modules/ModuleNameTag.cs | 39 --------------------------------- 1 file changed, 39 deletions(-) diff --git a/Source/Modules/ModuleNameTag.cs b/Source/Modules/ModuleNameTag.cs index 2a3cb04..a704068 100644 --- a/Source/Modules/ModuleNameTag.cs +++ b/Source/Modules/ModuleNameTag.cs @@ -67,10 +67,6 @@ public override void OnAwake() part.RemoveModule(pm); } } - - // make this module cheaper in update loops - isEnabled = false; - enabled = false; } public void TypingDone(string newValue) @@ -108,39 +104,4 @@ private static bool CanTagInEditor(EditorFacility whichEditor) return GameVariables.Instance.UnlockedActionGroupsStock(buildingLevel, false); } } - - // setting isEnabled to false prevents the nametag from showing up in the PAW...work around that. - [KSPAddon(KSPAddon.Startup.FlightAndEditor, false)] - class NameTagActivationManager : MonoBehaviour - { - void Awake() - { - GameEvents.onPartActionUICreate.Add(OnPartActionUICreate); - GameEvents.onPartActionUIShown.Add(OnPartActionUIShown); - } - - void OnDestroy() - { - GameEvents.onPartActionUICreate.Remove(OnPartActionUICreate); - GameEvents.onPartActionUIShown.Remove(OnPartActionUIShown); - } - - private void OnPartActionUICreate(Part part) - { - var nameTagModule = part.FindModuleImplementing(); - if (nameTagModule != null) - { - nameTagModule.isEnabled = true; - } - } - - private void OnPartActionUIShown(UIPartActionWindow paw, Part part) - { - var nameTagModule = part.FindModuleImplementing(); - if (nameTagModule != null) - { - nameTagModule.isEnabled = false; - } - } - } } From 41b374ba8b6fa6869f3e0c79a5839c955a675f4b Mon Sep 17 00:00:00 2001 From: djungelorm Date: Fri, 17 Jul 2026 17:40:01 +0100 Subject: [PATCH 6/9] Migrate legacy KOSNameTag save data to ModuleNameTag Adds a Harmony prefix on Part.LoadModule that rewrites a saved MODULE { name = KOSNameTag } node to name = ModuleNameTag before KSP binds it to a prefab module. Both the editor craft path (ShipConstruct.LoadShip) and the flight path (ProtoPartModuleSnapshot.Load) go through Part.LoadModule, so this single hook preserves tags from craft and saves made with the old kOS/kRPC modules. The nameTag value key is unchanged, so it binds directly once the module name matches. The rewrite is skipped if a real KOSNameTag PartModule type is still loaded, so an old kOS/kRPC or the retired standalone NameTag mod is not misrouted. --- .../HarmonyPatches/NameTagMigrationPatch.cs | 41 +++++++++++++++++++ Source/KSPCommunityPartModules.csproj | 1 + 2 files changed, 42 insertions(+) create mode 100644 Source/HarmonyPatches/NameTagMigrationPatch.cs diff --git a/Source/HarmonyPatches/NameTagMigrationPatch.cs b/Source/HarmonyPatches/NameTagMigrationPatch.cs new file mode 100644 index 0000000..2491437 --- /dev/null +++ b/Source/HarmonyPatches/NameTagMigrationPatch.cs @@ -0,0 +1,41 @@ +using HarmonyLib; +using KSPCommunityPartModules.Modules; + +namespace KSPCommunityPartModules.HarmonyPatches +{ + /// + /// Migrates legacy name-tag save data onto the renamed . + /// + /// kOS and kRPC each used to ship a part module named "KOSNameTag"; that module now lives here as + /// ModuleNameTag. Craft and saves made with the old mods store their tag in a + /// MODULE { name = KOSNameTag, nameTag = "..." } node. KSP binds a saved MODULE node to a prefab + /// module by matching the node's "name", so without this patch an old node would not match the + /// ModuleNameTag prefab module and the tag would be lost. Both the editor craft path + /// (ShipConstruct.LoadShip) and the flight/persistence path (ProtoPartModuleSnapshot.Load) funnel + /// through Part.LoadModule, so rewriting the node's module name there covers both. The nameTag + /// value key is unchanged, so it binds directly once the module name matches. + /// + /// This patch is applied by the assembly-wide Harmony.PatchAll() bootstrap in + /// ModuleParachuteEvents.ModuleManagerPostLoad; it deliberately has no bootstrap of its own so the + /// parachute patches are not applied twice. + /// + [HarmonyPatch(typeof(Part), nameof(Part.LoadModule))] + public static class NameTagMigrationPatch + { + private const string LegacyModuleName = "KOSNameTag"; + + [HarmonyPrefix] + static void Prefix(ConfigNode node) + { + if (node == null || node.GetValue("name") != LegacyModuleName) + return; + + // If a real KOSNameTag PartModule type is still loaded (an old kOS/kRPC, or the retired + // standalone NameTag mod), leave the node alone so we don't misroute it to ModuleNameTag. + if (AssemblyLoader.GetClassByName(typeof(PartModule), LegacyModuleName) != null) + return; + + node.SetValue("name", ModuleNameTag.MODULENAME); + } + } +} diff --git a/Source/KSPCommunityPartModules.csproj b/Source/KSPCommunityPartModules.csproj index ea4f378..319b8db 100644 --- a/Source/KSPCommunityPartModules.csproj +++ b/Source/KSPCommunityPartModules.csproj @@ -35,6 +35,7 @@ + From e5ae53781f74d0f608068a32b15a1fc762a1a610 Mon Sep 17 00:00:00 2001 From: djungelorm Date: Sat, 18 Jul 2026 07:25:02 +0100 Subject: [PATCH 7/9] Bump KSPBuildTools to 0.0.4 The 0.0.2-alpha.7 package did not provide the .NET Framework 4.8 reference assemblies, so the Linux CI build failed with MSB3644. Version 0.0.4 supplies them, matching the build workflow which already targets KSPBuildTools 0.0.4. --- Source/KSPCommunityPartModules.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/KSPCommunityPartModules.csproj b/Source/KSPCommunityPartModules.csproj index 319b8db..00c6e67 100644 --- a/Source/KSPCommunityPartModules.csproj +++ b/Source/KSPCommunityPartModules.csproj @@ -31,7 +31,7 @@ 4 - + From 0644617d4e6962bc0d494f77036b910496d688f6 Mon Sep 17 00:00:00 2001 From: Sofie <63377159+SofieBrink@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:38:27 +0200 Subject: [PATCH 8/9] Update licensing information --- .../KSPCommunityPartModules/KSPCommunityPartModules.ckan | 2 +- LICENSE.md | 9 +++++++++ Source/Modules/ModuleNameTag.cs | 1 + Source/Modules/NameTagWindow.cs | 1 + 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/GameData/KSPCommunityPartModules/KSPCommunityPartModules.ckan b/GameData/KSPCommunityPartModules/KSPCommunityPartModules.ckan index 6ae6250..2089ac0 100644 --- a/GameData/KSPCommunityPartModules/KSPCommunityPartModules.ckan +++ b/GameData/KSPCommunityPartModules/KSPCommunityPartModules.ckan @@ -1,6 +1,6 @@ identifier: KSPCommunityPartModules name: KSP Community Part Modules -license: MIT +license: GPLv3 abstract: A collection of part modules that are frequently used, Exists as a dependency for other mods. description: >- This mod is a collection of part modules that are frequently used by multiple mods. diff --git a/LICENSE.md b/LICENSE.md index cb582fd..32bd985 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -19,3 +19,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +**Exceptions:** +The following files are derived from GPLv3 sources and are licensed under the GNU General Public License v3.0: +- Source/Modules/NameTagWindow.cs +- Source/Modules/ModuleNameTag.cs + +See the license headers in those files for additional details. diff --git a/Source/Modules/ModuleNameTag.cs b/Source/Modules/ModuleNameTag.cs index a704068..7965669 100644 --- a/Source/Modules/ModuleNameTag.cs +++ b/Source/Modules/ModuleNameTag.cs @@ -3,6 +3,7 @@ Example: kOS's part:TAG suffix and kRPC's Part.Tag both read and write this module's value. Originally By: kOS contributors (Dunbaratu et al.) Originally For: kOS + License: GNU General Public License v3.0, see https://www.gnu.org/licenses/gpl-3.0.html */ using UnityEngine; diff --git a/Source/Modules/NameTagWindow.cs b/Source/Modules/NameTagWindow.cs index d726899..fa22d88 100644 --- a/Source/Modules/NameTagWindow.cs +++ b/Source/Modules/NameTagWindow.cs @@ -2,6 +2,7 @@ Usecase: The in-editor / in-flight IMGUI window used to edit a part's name tag. Originally By: kOS contributors (Dunbaratu et al.) Originally For: kOS + License: GNU General Public License v3.0, see https://www.gnu.org/licenses/gpl-3.0.html */ using UnityEngine; From 0b7131abf4cdda89e037a4bba53a700434256083 Mon Sep 17 00:00:00 2001 From: Sofie <63377159+SofieBrink@users.noreply.github.com> Date: Sat, 18 Jul 2026 11:44:47 +0200 Subject: [PATCH 9/9] Update Readme.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5f9590b..d9135bd 100644 --- a/README.md +++ b/README.md @@ -29,5 +29,7 @@ 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. + [CKAN]: https://forum.kerbalspaceprogram.com/topic/197082-ckan-the-comprehensive-kerbal-archive-network-v1332-laplace-ksp-2-support/