From 4acbd02572cf600ef5dd9af06ae170d9b72f335c Mon Sep 17 00:00:00 2001 From: maddavo <1432875+maddavo@users.noreply.github.com> Date: Wed, 2 Sep 2026 14:48:16 +1000 Subject: [PATCH 1/3] Add compatible docking target port selector --- Localization/en-us.cfg | 6 + Localization/es-es.cfg | 6 + Localization/fr-fr.cfg | 6 + Localization/ru.cfg | 6 + Localization/zh-cn.cfg | 6 + MechJeb2/MechJebModuleDockingGuidance.cs | 141 +++++++++++++++++++++++ 6 files changed, 171 insertions(+) diff --git a/Localization/en-us.cfg b/Localization/en-us.cfg index 15e99f9ef..d1257fb56 100644 --- a/Localization/en-us.cfg +++ b/Localization/en-us.cfg @@ -435,6 +435,12 @@ Localization #MechJeb_Docking_label13 = Distance Dock: <<1>> #MechJeb_Docking_label14 = Distance Dock Axis: <<1>> + #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) + #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) + #MechJeb_Docking_targetPortNone = No compatible undocked ports + #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_button = Dump Bounding Box Info #MechJeb_Docking_checkbox1 = Autopilot enabled diff --git a/Localization/es-es.cfg b/Localization/es-es.cfg index 2a9480b1e..82d9c1edb 100644 --- a/Localization/es-es.cfg +++ b/Localization/es-es.cfg @@ -427,6 +427,12 @@ Localization #MechJeb_Docking_label13 = Distancia Muelle: <<1>> #MechJeb_Docking_label14 = Distancia eje del muelle: <<1>> + #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) + #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) + #MechJeb_Docking_targetPortNone = No compatible undocked ports + #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_button = Información de la caja de volcado #MechJeb_Docking_checkbox1 = Piloto automático habilitado diff --git a/Localization/fr-fr.cfg b/Localization/fr-fr.cfg index c31e6e14d..0fde5149e 100644 --- a/Localization/fr-fr.cfg +++ b/Localization/fr-fr.cfg @@ -431,6 +431,12 @@ Localization #MechJeb_Docking_label13 = Distance d'amarrage : <<1>> #MechJeb_Docking_label14 = Distance de l'axe d'amarrage : <<1>> + #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) + #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) + #MechJeb_Docking_targetPortNone = No compatible undocked ports + #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_button = Enlever les infos de la boîte de collision #MechJeb_Docking_checkbox1 = Pilote automatique activé diff --git a/Localization/ru.cfg b/Localization/ru.cfg index 06f409e77..27d7aa574 100644 --- a/Localization/ru.cfg +++ b/Localization/ru.cfg @@ -435,6 +435,12 @@ Localization #MechJeb_Docking_label13 = Расстояние до стыковочного порта: <<1>> #MechJeb_Docking_label14 = Расстояние до оси стыковочного порта: <<1>> + #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) + #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) + #MechJeb_Docking_targetPortNone = No compatible undocked ports + #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_button = Получить данные о размерах #MechJeb_Docking_checkbox1 = Активировать автопилот diff --git a/Localization/zh-cn.cfg b/Localization/zh-cn.cfg index 22b410f9f..d347c47c4 100644 --- a/Localization/zh-cn.cfg +++ b/Localization/zh-cn.cfg @@ -435,6 +435,12 @@ Localization #MechJeb_Docking_label13 = 距离对接口: <<1>> #MechJeb_Docking_label14 = 距离对接轴: <<1>> + #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) + #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) + #MechJeb_Docking_targetPortNone = No compatible undocked ports + #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_button = 转储飞船边界框信息 #MechJeb_Docking_checkbox1 = 启动自动驾驶 diff --git a/MechJeb2/MechJebModuleDockingGuidance.cs b/MechJeb2/MechJebModuleDockingGuidance.cs index b61007d0a..6a047052a 100644 --- a/MechJeb2/MechJebModuleDockingGuidance.cs +++ b/MechJeb2/MechJebModuleDockingGuidance.cs @@ -1,4 +1,6 @@ extern alias JetBrainsAnnotations; +using System; +using System.Collections.Generic; using System.Linq; using KSP.Localization; using UnityEngine; @@ -11,6 +13,8 @@ public MechJebModuleDockingGuidance(MechJebCore core) : base(core) { } private MechJebModuleDockingAutopilot autopilot; + private static readonly char[] DockingNodeTypeSeparator = { ',' }; + public override void OnStart(PartModule.StartState state) => autopilot = Core.GetComputerModule(); protected override void WindowGUI(int windowID) @@ -37,6 +41,8 @@ protected override void WindowGUI(int windowID) GuiUtils.YellowLabel); //Warning: target is not a docking port. Right click the target docking port and select "Set as target" } + DrawTargetPortSelector(); + bool onAxisNodeExists = false; foreach (ITargetable node in Vessel.GetTargetables() .Where(t => t.GetTargetingMode() == VesselTargetModes.DirectionVelocityAndOrientation)) @@ -138,6 +144,141 @@ protected override void WindowGUI(int windowID) base.WindowGUI(windowID); } + private void DrawTargetPortSelector() + { + Vessel targetVessel = GetTargetVessel(); + if (targetVessel == null || targetVessel == Vessel) + return; + + GUILayout.BeginHorizontal(); + GUILayout.Label(Localizer.Format("#MechJeb_Docking_targetPort"), GuiUtils.LayoutNoExpandWidth); + + ModuleDockingNode referencePort = GetReferenceDockingPort(); + if (!targetVessel.loaded || targetVessel.packed) + { + GUILayout.Label(Localizer.Format("#MechJeb_Docking_targetPortUnavailable"), GuiUtils.ArrowSelectorStyeGuiStyleExpand); + GUILayout.EndHorizontal(); + return; + } + + List targetPorts = targetVessel.GetModules() + .Where(port => IsAvailableTargetPort(port) && ArePortsCompatible(referencePort, port)) + .ToList(); + + ModuleDockingNode currentPort = Core.Target.Target as ModuleDockingNode; + int currentIndex = targetPorts.IndexOf(currentPort); + string portLabel; + if (currentIndex >= 0) + { + portLabel = Localizer.Format("#MechJeb_Docking_targetPortSelected", GetPortName(currentPort), currentIndex + 1, + targetPorts.Count); + } + else if (targetPorts.Count > 0) + { + portLabel = Localizer.Format("#MechJeb_Docking_targetPortSelect", targetPorts.Count); + } + else + { + portLabel = Localizer.Format("#MechJeb_Docking_targetPortNone"); + } + + bool guiEnabled = GUI.enabled; + GUI.enabled = guiEnabled && targetPorts.Count > 0 && !autopilot.Enabled; + if (GUILayout.Button("<", GUILayout.ExpandWidth(false))) + SelectTargetPort(targetPorts, currentIndex, -1); + GUILayout.Label(portLabel, GuiUtils.ArrowSelectorStyeGuiStyleExpand); + if (GUILayout.Button(">", GUILayout.ExpandWidth(false))) + SelectTargetPort(targetPorts, currentIndex, 1); + GUI.enabled = guiEnabled; + GUILayout.EndHorizontal(); + } + + private Vessel GetTargetVessel() + { + if (Core.Target.Target is Vessel targetVessel) + return targetVessel; + + return Core.Target.Target?.GetVessel(); + } + + private ModuleDockingNode GetReferenceDockingPort() + { + Part referencePart = Vessel.GetReferenceTransformPart(); + if (referencePart == null || Vessel.ReferenceTransform == null) + return null; + + ModuleDockingNode referencePort = null; + double smallestAngle = 2; + foreach (ModuleDockingNode port in referencePart.Modules.OfType()) + { + Transform portTransform = port.GetTransform(); + if (portTransform == null) + continue; + + double angle = Vector3d.Angle(portTransform.forward, Vessel.ReferenceTransform.up); + if (angle < smallestAngle) + { + smallestAngle = angle; + referencePort = port; + } + } + + return referencePort; + } + + private static bool IsAvailableTargetPort(ModuleDockingNode port) => + port != null && !IsDockedState(port.state); + + private static bool IsDockedState(string state) => + state != null && (state.StartsWith("Docked", StringComparison.Ordinal) || state == "PreAttached"); + + private static bool ArePortsCompatible(ModuleDockingNode sourcePort, ModuleDockingNode targetPort) + { + if (sourcePort == null || targetPort == null) + return false; + + return ArePortsCompatible(sourcePort.nodeType, sourcePort.gendered, sourcePort.genderFemale, targetPort.nodeType, + targetPort.gendered, targetPort.genderFemale); + } + + private static bool ArePortsCompatible(string sourceNodeType, bool sourceGendered, bool sourceFemale, string targetNodeType, + bool targetGendered, bool targetFemale) + { + if (sourceGendered != targetGendered) + return false; + + if (sourceGendered && sourceFemale == targetFemale) + return false; + + if (string.IsNullOrEmpty(sourceNodeType) || string.IsNullOrEmpty(targetNodeType)) + return false; + + string[] sourceNodeTypes = sourceNodeType.Split(DockingNodeTypeSeparator, StringSplitOptions.RemoveEmptyEntries); + string[] targetNodeTypes = targetNodeType.Split(DockingNodeTypeSeparator, StringSplitOptions.RemoveEmptyEntries); + return sourceNodeTypes.Intersect(targetNodeTypes).Any(); + } + + private void SelectTargetPort(IReadOnlyList targetPorts, int currentIndex, int direction) + { + int nextIndex = currentIndex < 0 + ? direction > 0 ? 0 : targetPorts.Count - 1 + : (currentIndex + direction + targetPorts.Count) % targetPorts.Count; + + ModuleDockingNode targetPort = targetPorts[nextIndex]; + Core.Target.Set(targetPort); + // Keep KSP's global target in sync so other docking aids observe the port change immediately. + FlightGlobals.fetch.SetVesselTarget(targetPort); + } + + private static string GetPortName(ModuleDockingNode port) + { + string portName = port.GetName(); + if (!string.IsNullOrEmpty(portName)) + return portName; + + return port.part?.partInfo?.title ?? port.part?.name ?? "?"; + } + protected override GUILayoutOption[] WindowOptions() => new[] { GuiUtils.LayoutWidth(300), GUILayout.Height(50) }; protected override void OnModuleDisabled() From 2011c40fff80699d65479a8763711d11ef9b58f0 Mon Sep 17 00:00:00 2001 From: maddavo <1432875+maddavo@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:22:34 +1000 Subject: [PATCH 2/3] Keep docking selector state when KSP retargets vessel --- MechJeb2/MechJebModuleDockingGuidance.cs | 34 +++++++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/MechJeb2/MechJebModuleDockingGuidance.cs b/MechJeb2/MechJebModuleDockingGuidance.cs index 6a047052a..2b3699f71 100644 --- a/MechJeb2/MechJebModuleDockingGuidance.cs +++ b/MechJeb2/MechJebModuleDockingGuidance.cs @@ -15,6 +15,12 @@ public MechJebModuleDockingGuidance(MechJebCore core) : base(core) { } private static readonly char[] DockingNodeTypeSeparator = { ',' }; + // KSP changes a docking-port target back to its parent vessel while the + // target is farther than the part-target range. Keep the user's port + // choice locally so that cycling does not jump back to the first port. + private ModuleDockingNode selectedTargetPort; + private Vessel selectedTargetVessel; + public override void OnStart(PartModule.StartState state) => autopilot = Core.GetComputerModule(); protected override void WindowGUI(int windowID) @@ -35,14 +41,14 @@ protected override void WindowGUI(int windowID) GuiUtils.YellowLabel); //Warning: You need to control the vessel from a docking port. Right click a docking port and select "Control from here" } - if (!(Core.Target.Target is ModuleDockingNode)) + DrawTargetPortSelector(); + + if (!(Core.Target.Target is ModuleDockingNode) && selectedTargetPort == null) { GUILayout.Label(Localizer.Format("#MechJeb_Docking_label3"), GuiUtils.YellowLabel); //Warning: target is not a docking port. Right click the target docking port and select "Set as target" } - DrawTargetPortSelector(); - bool onAxisNodeExists = false; foreach (ITargetable node in Vessel.GetTargetables() .Where(t => t.GetTargetingMode() == VesselTargetModes.DirectionVelocityAndOrientation)) @@ -148,7 +154,17 @@ private void DrawTargetPortSelector() { Vessel targetVessel = GetTargetVessel(); if (targetVessel == null || targetVessel == Vessel) + { + selectedTargetPort = null; + selectedTargetVessel = null; return; + } + + if (selectedTargetVessel != targetVessel) + { + selectedTargetPort = null; + selectedTargetVessel = targetVessel; + } GUILayout.BeginHorizontal(); GUILayout.Label(Localizer.Format("#MechJeb_Docking_targetPort"), GuiUtils.LayoutNoExpandWidth); @@ -162,10 +178,19 @@ private void DrawTargetPortSelector() } List targetPorts = targetVessel.GetModules() - .Where(port => IsAvailableTargetPort(port) && ArePortsCompatible(referencePort, port)) + .Where(port => IsAvailableTargetPort(port) && port.GetTransform() != null && + ArePortsCompatible(referencePort, port)) .ToList(); ModuleDockingNode currentPort = Core.Target.Target as ModuleDockingNode; + if (currentPort != null && targetPorts.Contains(currentPort)) + selectedTargetPort = currentPort; + else if (selectedTargetPort != null && !targetPorts.Contains(selectedTargetPort)) + selectedTargetPort = null; + + if (currentPort == null || !targetPorts.Contains(currentPort)) + currentPort = selectedTargetPort; + int currentIndex = targetPorts.IndexOf(currentPort); string portLabel; if (currentIndex >= 0) @@ -265,6 +290,7 @@ private void SelectTargetPort(IReadOnlyList targetPorts, int : (currentIndex + direction + targetPorts.Count) % targetPorts.Count; ModuleDockingNode targetPort = targetPorts[nextIndex]; + selectedTargetPort = targetPort; Core.Target.Set(targetPort); // Keep KSP's global target in sync so other docking aids observe the port change immediately. FlightGlobals.fetch.SetVesselTarget(targetPort); From aeee32212801b987e0fffcad800e0cb565584abb Mon Sep 17 00:00:00 2001 From: maddavo <1432875+maddavo@users.noreply.github.com> Date: Wed, 2 Sep 2026 23:36:39 +1000 Subject: [PATCH 3/3] Translate docking port selector strings --- Localization/es-es.cfg | 8 ++++---- Localization/fr-fr.cfg | 8 ++++---- Localization/ru.cfg | 8 ++++---- Localization/zh-cn.cfg | 8 ++++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Localization/es-es.cfg b/Localization/es-es.cfg index 82d9c1edb..ec5c47d41 100644 --- a/Localization/es-es.cfg +++ b/Localization/es-es.cfg @@ -427,11 +427,11 @@ Localization #MechJeb_Docking_label13 = Distancia Muelle: <<1>> #MechJeb_Docking_label14 = Distancia eje del muelle: <<1>> - #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPort = Puerto objetivo #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) - #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) - #MechJeb_Docking_targetPortNone = No compatible undocked ports - #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_targetPortSelect = Seleccionar un puerto (<<1>> disponibles) + #MechJeb_Docking_targetPortNone = No hay puertos compatibles sin acoplar + #MechJeb_Docking_targetPortUnavailable = Disponible cuando la nave objetivo esté cerca #MechJeb_Docking_button = Información de la caja de volcado diff --git a/Localization/fr-fr.cfg b/Localization/fr-fr.cfg index 0fde5149e..6960afdfd 100644 --- a/Localization/fr-fr.cfg +++ b/Localization/fr-fr.cfg @@ -431,11 +431,11 @@ Localization #MechJeb_Docking_label13 = Distance d'amarrage : <<1>> #MechJeb_Docking_label14 = Distance de l'axe d'amarrage : <<1>> - #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPort = Port cible #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) - #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) - #MechJeb_Docking_targetPortNone = No compatible undocked ports - #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_targetPortSelect = Sélectionnez un port (<<1>> disponible(s)) + #MechJeb_Docking_targetPortNone = Aucun port compatible non amarré + #MechJeb_Docking_targetPortUnavailable = Disponible lorsque le vaisseau cible est à proximité #MechJeb_Docking_button = Enlever les infos de la boîte de collision diff --git a/Localization/ru.cfg b/Localization/ru.cfg index 27d7aa574..0d3cb16d8 100644 --- a/Localization/ru.cfg +++ b/Localization/ru.cfg @@ -435,11 +435,11 @@ Localization #MechJeb_Docking_label13 = Расстояние до стыковочного порта: <<1>> #MechJeb_Docking_label14 = Расстояние до оси стыковочного порта: <<1>> - #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPort = Целевой порт #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) - #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) - #MechJeb_Docking_targetPortNone = No compatible undocked ports - #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_targetPortSelect = Выберите порт (доступно: <<1>>) + #MechJeb_Docking_targetPortNone = Нет совместимых свободных стыковочных портов + #MechJeb_Docking_targetPortUnavailable = Доступно, когда целевой корабль находится поблизости #MechJeb_Docking_button = Получить данные о размерах diff --git a/Localization/zh-cn.cfg b/Localization/zh-cn.cfg index d347c47c4..2596f0026 100644 --- a/Localization/zh-cn.cfg +++ b/Localization/zh-cn.cfg @@ -435,11 +435,11 @@ Localization #MechJeb_Docking_label13 = 距离对接口: <<1>> #MechJeb_Docking_label14 = 距离对接轴: <<1>> - #MechJeb_Docking_targetPort = Target port + #MechJeb_Docking_targetPort = 目标对接口 #MechJeb_Docking_targetPortSelected = <<1>> (<<2>>/<<3>>) - #MechJeb_Docking_targetPortSelect = Select a port (<<1>> available) - #MechJeb_Docking_targetPortNone = No compatible undocked ports - #MechJeb_Docking_targetPortUnavailable = Available when the target vessel is nearby + #MechJeb_Docking_targetPortSelect = 选择对接口(可用:<<1>>) + #MechJeb_Docking_targetPortNone = 没有兼容的未对接接口 + #MechJeb_Docking_targetPortUnavailable = 目标飞船靠近时可用 #MechJeb_Docking_button = 转储飞船边界框信息