public void UpdateContextMenu()
 {
     PartModuleUtils.SetupEvent(
         this, TogglePlayStateEvent,
         x => x.guiName =
             sndMainTune == null || !sndMainTune.isPlaying ? PlayMenuTxt : StopMenuTxt);
 }
Exemple #2
0
 /// <inheritdoc/>
 public virtual void UpdateContextMenu()
 {
     PartModuleUtils.SetupEvent(this, ToggleVesselsDockModeEvent, e => {
         if (linkJoint != null)
         {
             e.guiName = linkJoint.coupleOnLinkMode ? DockedModeMenuTxt : UndockedModeMenuTxt;
             if (coupleMode == CoupleMode.SetViaGUI)
             {
                 e.active = coupleNode != null && (linkTarget == null || linkTarget.coupleNode != null);
             }
             else if (isLinked)
             {
                 // Just in case show GUI if the link is established, and its couple mode contradicts the
                 // joint setting. GUI will allow fixing it manually.
                 e.active = coupleMode == CoupleMode.NeverCouple && linkJoint.coupleOnLinkMode ||
                            coupleMode == CoupleMode.AlwaysCoupled && !linkJoint.coupleOnLinkMode;
             }
             else
             {
                 e.active = false;
             }
         }
         else
         {
             e.active = false;
         }
     });
 }
Exemple #3
0
        /// <inheritdoc/>
        public override void UpdateContextMenu()
        {
            base.UpdateContextMenu();
            deployedCableLengthMenuInfo = DistanceType.Format(
                cableJoint != null ? cableJoint.deployedCableLength : 0);

            PartModuleUtils.SetupEvent(this, ToggleExtendCableEvent, e => {
                e.active  = linkState != LinkState.NodeIsBlocked;
                e.guiName = motorTargetSpeed > float.Epsilon
          ? StopExtendingMenuTxt
          : ExtendCableMenuTxt;
            });
            PartModuleUtils.SetupEvent(this, ToggleRetractCableEvent, e => {
                e.active  = linkState != LinkState.NodeIsBlocked;
                e.guiName = motorTargetSpeed < -float.Epsilon
          ? StopRetractingMenuTxt
          : RetractCableMenuTxt;
            });
            PartModuleUtils.SetupEvent(this, InstantStretchEvent, e => {
                e.active = !isConnectorLocked && linkState != LinkState.NodeIsBlocked;
            });
            PartModuleUtils.SetupEvent(this, ReleaseCableEvent, e => {
                e.active = linkState != LinkState.NodeIsBlocked;
            });
        }
Exemple #4
0
        /// <inheritdoc/>
        public override void UpdateContextMenu()
        {
            base.UpdateContextMenu();

            connectorStateMenuInfo = ConnectorStatesMsgLookup.Lookup(connectorState);
            PartModuleUtils.SetupEvent(this, ToggleVesselsDockModeEvent, e => {
                e.active &= !isConnectorLocked && linkState != LinkState.NodeIsBlocked;
            });
            PartModuleUtils.SetupEvent(this, GrabConnectorEvent, e => {
                e.active = connectorState == ConnectorState.Locked && linkState != LinkState.NodeIsBlocked;
                if (_grabConnectorEventInject != null)
                {
                    _grabConnectorEventInject.guiName = e.guiName;
                }
            });
            PartModuleUtils.SetupEvent(this, ReturnConnectorEvent, e => {
                e.active = IsActiveEvaHoldingConnector();
            });
            PartModuleUtils.SetupEvent(this, DetachConnectorEvent, e => {
                e.active = isLinked;
            });
            PartModuleUtils.SetupEvent(this, InstantLockConnectorEvent, e => {
                e.active = connectorState == ConnectorState.Deployed;
            });
            if (_grabConnectorEventInject != null)
            {
                _grabConnectorEventInject.active = linkTarget != null &&
                                                   connectorState == ConnectorState.Plugged &&
                                                   FlightGlobals.ActiveVessel != linkTarget.part.vessel;
            }
        }
Exemple #5
0
 /// <inheritdoc/>
 public override void OnUpdate()
 {
     base.OnUpdate();
     if (showSetup)
     {
         var distToPart = Vector3.Distance(
             FlightGlobals.ActiveVessel.transform.position, part.transform.position);
         var setupEvent = PartModuleUtils.GetEvent(this, SetupEvent);
         if (setupEvent == null || distToPart > setupEvent.unfocusedRange)
         {
             showSetup = false;
         }
     }
     if (activated)
     {
         delay -= TimeWarp.deltaTime;
         if (delay < 1 && !sndTimeEnd.isPlaying)
         {
             sndTimeEnd.Play();
         }
         if (delay < 0)
         {
             sndTimeStart.Stop();
             sndTimeLoop.Stop();
             Explode(part.transform.position);
         }
     }
 }
Exemple #6
0
 /// <inheritdoc/>
 public void UpdateContextMenu()
 {
     PartModuleUtils.SetupEvent(
         this, PickupConnectorEvent,
         x => x.guiActive = FlightGlobals.fetch != null && FlightGlobals.ActiveVessel == vessel &&
                            !isLinked && closestConnector != null);
 }
        /// <inheritdoc/>
        public override void UpdateContextMenu()
        {
            base.UpdateContextMenu();

            PartModuleUtils.SetupEvent(this, OpenGUIEvent, e => {
                e.active = linkTarget != null && linkTarget.part != null && !linkTarget.part.vessel.isEVA;
            });
        }
Exemple #8
0
        /// <summary>Cleans up the internal cache of the injected menu items.</summary>
        /// <param name="menuOwnerPart">The part for which the UI is being destroyed.</param>
        /// <seealso cref="OnPartGUIStart"/>
        void OnPartGUIStop(Part menuOwnerPart)
        {
            List <InjectedEvent> injects;

            if (_targetCandidates.TryGetValue(menuOwnerPart.flightID, out injects))
            {
                injects
                .Where(ie => ie.baseEvent != null).ToList()
                .ForEach(ie => PartModuleUtils.DropEvent(ie.module as PartModule, ie.baseEvent));
                _targetCandidates.Remove(menuOwnerPart.flightID);
            }
        }
 /// <inheritdoc/>
 public override void UpdateContextMenu()
 {
     base.UpdateContextMenu();
     PartModuleUtils.SetupEvent(this, StartLinkContextMenuAction, e => {
         e.guiName = startLinkMenu;
         e.active  = linkState == LinkState.Available;
     });
     PartModuleUtils.SetupEvent(this, BreakLinkContextMenuAction, e => {
         e.guiName = breakLinkMenu;
         e.active  = linkState == LinkState.Linked;
     });
 }
Exemple #10
0
 public void ActivateEvent()
 {
     if (!activated)
     {
         activated = true;
         sndTimeStart.Play();
         sndTimeLoop.Play();
         PartModuleUtils.SetupEvent(this, ActivateEvent, x => x.active = false);
         PartModuleUtils.SetupEvent(this, SetupEvent, x => x.active    = false);
         ScreenMessaging.ShowPriorityScreenMessage(TimeToEscapeMsg.Format((int)delay));
     }
 }
Exemple #11
0
        /// <inheritdoc/>
        protected override void SetupStateMachine()
        {
            base.SetupStateMachine();
            linkStateMachine.onAfterTransition += (start, end) => HostedDebugLog.Fine(
                this, "Source state changed at {0}: {1} => {2}", attachNodeName, start, end);
            linkStateMachine.SetTransitionConstraint(
                LinkState.Available,
                new[] { LinkState.Linking, LinkState.RejectingLinks, LinkState.NodeIsBlocked });
            linkStateMachine.SetTransitionConstraint(
                LinkState.NodeIsBlocked,
                new[] { LinkState.Available });
            linkStateMachine.SetTransitionConstraint(
                LinkState.Linking,
                new[] { LinkState.Available, LinkState.Linked });
            linkStateMachine.SetTransitionConstraint(
                LinkState.Linked,
                new[] { LinkState.Available });
            linkStateMachine.SetTransitionConstraint(
                LinkState.Locked,
                new[] { LinkState.Available });
            linkStateMachine.SetTransitionConstraint(
                LinkState.RejectingLinks,
                new[] { LinkState.Available, LinkState.Locked });

            linkStateMachine.AddStateHandlers(
                LinkState.Available,
                enterHandler: x => KASAPI.KasEvents.OnStartLinking.Add(OnStartLinkingKASEvent),
                leaveHandler: x => KASAPI.KasEvents.OnStartLinking.Remove(OnStartLinkingKASEvent));
            linkStateMachine.AddStateHandlers(
                LinkState.RejectingLinks,
                enterHandler: x => KASAPI.KasEvents.OnStopLinking.Add(OnStopLinkingKASEvent),
                leaveHandler: x => KASAPI.KasEvents.OnStopLinking.Remove(OnStopLinkingKASEvent));
            linkStateMachine.AddStateHandlers(
                LinkState.Linked,
                enterHandler: x => {
                GameEvents.onVesselWillDestroy.Add(OnVesselWillDestroyGameEvent);
                var module = linkTarget as PartModule;
                PartModuleUtils.InjectEvent(this, ToggleVesselsDockModeEvent, module);
            },
                leaveHandler: x => {
                GameEvents.onVesselWillDestroy.Remove(OnVesselWillDestroyGameEvent);
                var module = linkTarget as PartModule;
                PartModuleUtils.WithdrawEvent(this, ToggleVesselsDockModeEvent, module);
            });
            linkStateMachine.AddStateHandlers(
                LinkState.Linking,
                enterHandler: x => KASAPI.KasEvents.OnStartLinking.Fire(this),
                leaveHandler: x => KASAPI.KasEvents.OnStopLinking.Fire(this));
        }
 /// <inheritdoc/>
 protected override void SetupStateMachine()
 {
     base.SetupStateMachine();
     linkStateMachine.onAfterTransition += (start, end) => UpdateContextMenu();
     linkStateMachine.AddStateHandlers(
         LinkState.Linking,
         enterHandler: x => {
         InputLockManager.SetControlLock(
             ControlTypes.All & ~ControlTypes.CAMERACONTROLS, TotalControlLock);
         canAutoSaveState = HighLogic.CurrentGame.Parameters.Flight.CanAutoSave;
         HighLogic.CurrentGame.Parameters.Flight.CanAutoSave = false;
         linkRenderer.shaderNameOverride = InteractiveShaderName;
         linkRenderer.colorOverride      = BadLinkColor;
         linkRenderer.isPhysicalCollider = false;
     },
         leaveHandler: x => {
         linkRenderer.StopRenderer(); // This resets the pipe state.
         linkRenderer.shaderNameOverride = null;
         linkRenderer.colorOverride      = null;
         linkRenderer.isPhysicalCollider = true;
         ScreenMessages.RemoveMessage(statusScreenMessage);
         InputLockManager.RemoveControlLock(TotalControlLock);
         HighLogic.CurrentGame.Parameters.Flight.CanAutoSave = canAutoSaveState;
         lastHoveredPart = null;
     });
     linkStateMachine.AddStateHandlers(
         LinkState.Linked,
         enterHandler: x => {
         if (linkActor == LinkActorType.Player || linkActor == LinkActorType.Physics)
         {
             UISoundPlayer.instance.Play(linkJoint.coupleOnLinkMode ? sndPathDock : sndPathPlug);
         }
         var module = linkTarget as PartModule;
         PartModuleUtils.InjectEvent(this, BreakLinkContextMenuAction, module);
     },
         leaveHandler: x => {
         if (linkActor == LinkActorType.Player)
         {
             UISoundPlayer.instance.Play(linkJoint.coupleOnLinkMode ? sndPathUndock : sndPathUnplug);
         }
         else if (linkActor == LinkActorType.Physics)
         {
             UISoundPlayer.instance.Play(sndPathBroke);
         }
         var module = linkTarget as PartModule;
         PartModuleUtils.WithdrawEvent(this, BreakLinkContextMenuAction, module);
     });
 }
Exemple #13
0
        /// <summary>Updates the GUI items when a part's context menu is opened.</summary>
        /// <remarks>
        /// <para>
        /// The goal of this method is to intercept the action of opening a context menu on the other
        /// part. The method checks if the target part can be a target for the link of the connector that
        /// is being carried by the kerbal. If this is the case, then a special menu item is injected to
        /// allow player to complete the link.
        /// </para>
        /// <para>
        /// This event is called once for every part with an opened menu in every frame. For this reason
        /// it must be very efficient, or else the performance will suffer. To not impact the performance,
        /// this method caches all the opened menus.
        /// </para>
        /// </remarks>
        /// <param name="menuOwnerPart">The part for which the UI is created.</param>
        /// <seealso cref="OnPartGUIStop"/>
        void OnPartGUIStart(Part menuOwnerPart)
        {
            if (FlightGlobals.ActiveVessel != vessel)
            {
                // If the EVA part has lost the focus, then cleanup all the caches.
                if (_targetCandidates.Count > 0)
                {
                    _targetCandidates
                    .SelectMany(t => t.Value)
                    .Where(ie => ie.baseEvent != null)
                    .ToList()
                    .ForEach(ie => PartModuleUtils.DropEvent(ie.module as PartModule, ie.baseEvent));
                    _targetCandidates.Clear();
                }
                return;
            }

            // Check if the menu injects need to be added/removed on the monitored parts.
            List <InjectedEvent> injects;

            if (!_targetCandidates.TryGetValue(menuOwnerPart.flightID, out injects))
            {
                injects = menuOwnerPart.Modules.OfType <ILinkTarget>()
                          .Where(t => t.cfgLinkType == cfgLinkType)
                          .Select(t => new InjectedEvent()
                {
                    module = t, baseEvent = null
                })
                          .ToList();
                _targetCandidates.Add(menuOwnerPart.flightID, injects);
            }
            foreach (var inject in injects)
            {
                var target  = inject.module;
                var canLink = inject.module.linkState == LinkState.Available && isLinked;
                if (!canLink && inject.baseEvent != null)
                {
                    PartModuleUtils.DropEvent(target as PartModule, inject.baseEvent);
                    inject.baseEvent = null;
                }
                else if (canLink && inject.baseEvent == null)
                {
                    inject.baseEvent = MakeEvent(target, AttachConnectorMenu, LinkCarriedConnector);
                    PartModuleUtils.AddEvent(target as PartModule, inject.baseEvent);
                }
            }
        }
Exemple #14
0
        /// <inheritdoc/>
        public override void UpdateContextMenu()
        {
            base.UpdateContextMenu();
            deployedCableLengthMenuInfo = DistanceType.Format(
                cableJoint != null ? cableJoint.deployedCableLength : 0);

            PartModuleUtils.SetupEvent(this, ToggleExtendCableEvent, e => {
                e.guiName = motorTargetSpeed > float.Epsilon
          ? StopExtendingMenuTxt
          : ExtendCableMenuTxt;
            });
            PartModuleUtils.SetupEvent(this, ToggleRetractCableEvent, e => {
                e.guiName = motorTargetSpeed < -float.Epsilon
          ? StopRetractingMenuTxt
          : RetractCableMenuTxt;
            });
        }
 /// <summary>Creates the context menu events from the orientation descriptions.</summary>
 /// <seealso cref="parkedOrientations"/>
 void InjectOrientationMenuItems()
 {
     foreach (var orientation in parkedOrientations)
     {
         var eventInject = new BaseEvent(
             Events,
             "autoEventOrientation" + part.Modules.IndexOf(this),
             () => {
             persistedParkedOrientation = orientation.direction;
             UpdateLinkLengthAndOrientation();
         },
             new KSPEvent());
         eventInject.guiName            = orientation.title;
         eventInject.guiActive          = false;
         eventInject.guiActiveEditor    = true;
         eventInject.guiActiveUnfocused = true;
         PartModuleUtils.AddEvent(this, eventInject);
         injectedOrientationEvents.Add(eventInject);
     }
 }
Exemple #16
0
 /// <inheritdoc/>
 public virtual void UpdateContextMenu()
 {
     PartModuleUtils.SetupEvent(this, ToggleVesselsDockModeEvent, e => {
         if (linkJoint != null)
         {
             if (linkJoint.coupleOnLinkMode)
             {
                 e.active  = true;
                 e.guiName = DockedModeMenuTxt;
             }
             else
             {
                 e.active = showCouplingUi && allowCoupling &&
                            (linkTarget == null || linkTarget.coupleNode != null);
                 e.guiName = UndockedModeMenuTxt;
             }
         }
         else
         {
             e.active = false;
         }
     });
 }
Exemple #17
0
        /// <inheritdoc/>
        public void UpdateContextMenu()
        {
            Fields["lockStatus"].guiActive          = isLinked;
            Fields["steeringStatus"].guiActive      = isLinked;
            Fields["steeringInvert"].guiActive      = isLinked && persistedActiveSteeringEnabled;
            Fields["steeringSensitivity"].guiActive = isLinked && persistedActiveSteeringEnabled;

            PartModuleUtils.SetupEvent(
                this, StartLockLockingAction,
                e => e.active = isLinked && persistedLockingMode == LockMode.Disabled);
            PartModuleUtils.SetupEvent(
                this, UnlockAction,
                e => e.active = isLinked && persistedLockingMode != LockMode.Disabled);
            PartModuleUtils.SetupEvent(
                this, DeactivateSteeringAction,
                e => e.active = isLinked && persistedActiveSteeringEnabled);
            PartModuleUtils.SetupEvent(
                this, ActiveSteeringAction,
                e => e.active = isLinked && !persistedActiveSteeringEnabled);

            lockStatus     = LockStatusMsgLookup.Lookup(persistedLockingMode);
            steeringStatus = SteeringStatusMsgLookup.Lookup(
                persistedActiveSteeringEnabled ? SteeringStatus.Active : SteeringStatus.Disabled);
        }
Exemple #18
0
 /// <inheritdoc/>
 public void UpdateContextMenu()
 {
     // FIXME: Consider the mounted state.
     PartModuleUtils.SetupEvent(this, ReleaseEvent, x => x.active        = allowRelease);
     PartModuleUtils.SetupAction(this, ActionGroupRelease, x => x.active = allowRelease);
 }
Exemple #19
0
 /// <inheritdoc/>
 public void UpdateContextMenu()
 {
     _injectedOrientationEvents.ForEach(e => e.active = !isLinked);
     PartModuleUtils.SetupEvent(this, ExtendAtMaxMenuAction, x => x.active  = !isLinked);
     PartModuleUtils.SetupEvent(this, RetractToMinMenuAction, x => x.active = !isLinked);
 }
Exemple #20
0
 void SetupEvents()
 {
     // This call will activate the event.
     PartModuleUtils.SetupEvent(this, TestEvent, x => x.active = true);
 }
Exemple #21
0
        void SetupEvents()
        {
            var e = PartModuleUtils.GetEvent(this, TestEvent);

            e.active = true; // Activates the event.
        }
Exemple #22
0
        /// <inheritdoc/>
        protected override void SetupStateMachine()
        {
            base.SetupStateMachine();
            linkStateMachine.onAfterTransition += (start, end) => UpdateContextMenu();
            linkStateMachine.AddStateHandlers(
                LinkState.Linked,
                enterHandler: oldState => {
                var module = linkTarget as PartModule;
                PartModuleUtils.InjectEvent(this, DetachConnectorEvent, module);
                PartModuleUtils.AddEvent(module, _grabConnectorEventInject);
            },
                leaveHandler: newState => {
                var module = linkTarget as PartModule;
                PartModuleUtils.WithdrawEvent(this, DetachConnectorEvent, module);
                PartModuleUtils.DropEvent(module, _grabConnectorEventInject);
            });
            linkStateMachine.AddStateHandlers(
                LinkState.NodeIsBlocked,
                enterHandler: oldState => {
                if (decoupleIncompatibleTargets &&
                    coupleNode != null && coupleNode.attachedPart != null)
                {
                    HostedDebugLog.Warning(this, "Decouple incompatible part from the node: {0}",
                                           coupleNode.FindOpposingNode().attachedPart);
                    UISoundPlayer.instance.Play(KASAPI.CommonConfig.sndPathBipWrong);
                    ShowStatusMessage(
                        CannotLinkToPreAttached.Format(coupleNode.attachedPart), isError: true);
                    KASAPI.LinkUtils.DecoupleParts(part, coupleNode.attachedPart);
                }
            },
                callOnShutdown: false);

            // The default state is "Locked". All the enter state handlers rely on it, and all the exit
            // state handlers reset the state back to the default.
            connectorStateMachine = new SimpleStateMachine <ConnectorState>();
            connectorStateMachine.onAfterTransition += (start, end) => {
                if (end != null) // Do nothing on state machine shutdown.
                {
                    persistedIsConnectorLocked = isConnectorLocked;
                    if (end == ConnectorState.Locked)
                    {
                        KASAPI.AttachNodesUtils.AddNode(part, coupleNode);
                    }
                    else if (coupleNode.attachedPart == null)
                    {
                        KASAPI.AttachNodesUtils.DropNode(part, coupleNode);
                    }
                    UpdateContextMenu();
                }
                HostedDebugLog.Info(this, "Connector state changed: {0} => {1}", start, end);
            };
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Docked,
                new[] {
                ConnectorState.Plugged,
                ConnectorState.Locked, // External detach.
            });
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Locked,
                new[] {
                ConnectorState.Deployed,
                ConnectorState.Plugged,
                ConnectorState.Docked, // External attach.
            });
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Deployed,
                new[] {
                ConnectorState.Locked,
                ConnectorState.Plugged,
            });
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Plugged,
                new[] {
                ConnectorState.Deployed,
                ConnectorState.Docked,
            });
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Locked,
                enterHandler: oldState => {
                SaveConnectorModelPosAndRot();
                if (oldState.HasValue) // Skip when restoring state.
                {
                    UISoundPlayer.instance.Play(sndPathLockConnector);
                }
            },
                leaveHandler: newState =>
                SaveConnectorModelPosAndRot(saveNonPhysical: newState == ConnectorState.Deployed),
                callOnShutdown: false);
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Docked,
                enterHandler: oldState => {
                SaveConnectorModelPosAndRot();
                cableJoint.SetLockedOnCouple(true);

                // Align the docking part to the nodes if it's a separate vessel.
                if (oldState.HasValue && linkTarget.part.vessel != vessel)
                {
                    AlignTransforms.SnapAlignNodes(linkTarget.coupleNode, coupleNode);
                    linkJoint.SetCoupleOnLinkMode(true);
                    UISoundPlayer.instance.Play(sndPathDockConnector);
                }
            },
                leaveHandler: newState => {
                cableJoint.SetLockedOnCouple(false);
                SaveConnectorModelPosAndRot(saveNonPhysical: newState == ConnectorState.Deployed);
                linkJoint.SetCoupleOnLinkMode(false);
            },
                callOnShutdown: false);
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Plugged,
                enterHandler: oldState => SaveConnectorModelPosAndRot(),
                leaveHandler: newState =>
                SaveConnectorModelPosAndRot(saveNonPhysical: newState == ConnectorState.Deployed),
                callOnShutdown: false);
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Deployed,
                enterHandler: oldState => StartPhysicsOnConnector(),
                leaveHandler: newState => StopPhysicsOnConnector(),
                callOnShutdown: false);
        }
Exemple #23
0
        /// <inheritdoc/>
        protected override void SetupStateMachine()
        {
            base.SetupStateMachine();
            linkStateMachine.onAfterTransition += (start, end) => UpdateContextMenu();
            linkStateMachine.AddStateHandlers(
                LinkState.Linked,
                enterHandler: oldState => {
                var module = linkTarget as PartModule;
                PartModuleUtils.InjectEvent(this, DetachConnectorEvent, module);
                PartModuleUtils.AddEvent(module, GrabConnectorEventInject);
            },
                leaveHandler: newState => {
                var module = linkTarget as PartModule;
                PartModuleUtils.WithdrawEvent(this, DetachConnectorEvent, module);
                PartModuleUtils.DropEvent(module, GrabConnectorEventInject);
            });

            // The default state is "Locked". All the enter state handlers rely on it, and all the exit
            // state handlers reset the state back to the default.
            connectorStateMachine = new SimpleStateMachine <ConnectorState>(strict: true);
            connectorStateMachine.onAfterTransition += (start, end) => {
                if (end != null) // Do nothing on state machine shutdown.
                {
                    persistedIsConnectorLocked = isConnectorLocked;
                    if (end == ConnectorState.Locked)
                    {
                        KASAPI.AttachNodesUtils.AddNode(part, coupleNode);
                    }
                    else if (coupleNode.attachedPart == null)
                    {
                        KASAPI.AttachNodesUtils.DropNode(part, coupleNode);
                    }
                    UpdateContextMenu();
                }
                HostedDebugLog.Info(this, "Connector state changed: {0} => {1}", start, end);
            };
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Docked,
                new[] {
                ConnectorState.Plugged,
                ConnectorState.Locked, // External detach.
            });
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Locked,
                new[] {
                ConnectorState.Deployed,
                ConnectorState.Plugged,
                ConnectorState.Docked, // External attach.
            });
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Deployed,
                new[] {
                ConnectorState.Locked,
                ConnectorState.Plugged,
            });
            connectorStateMachine.SetTransitionConstraint(
                ConnectorState.Plugged,
                new[] {
                ConnectorState.Deployed,
                ConnectorState.Docked,
            });
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Locked,
                enterHandler: oldState => {
                connectorModelObj.parent = nodeTransform;
                PartModel.UpdateHighlighters(part);
                connectorModelObj.GetComponentsInChildren <Renderer>().ToList()
                .ForEach(r => r.SetPropertyBlock(part.mpb));
                AlignTransforms.SnapAlign(connectorModelObj, connectorCableAnchor, partCableAnchor);
                SetCableLength(0);
                if (oldState.HasValue) // Skip when restoring state.
                {
                    UISoundPlayer.instance.Play(sndPathLockConnector);
                }
            },
                callOnShutdown: false);
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Docked,
                enterHandler: oldState => {
                connectorModelObj.parent = nodeTransform;
                AlignTransforms.SnapAlign(connectorModelObj, connectorCableAnchor, partCableAnchor);
                SetCableLength(0);

                // Align the docking part to the nodes if it's a separate vessel.
                if (oldState.HasValue && linkTarget.part.vessel != vessel)
                {
                    AlignTransforms.SnapAlignNodes(linkTarget.coupleNode, coupleNode);
                    linkJoint.SetCoupleOnLinkMode(true);
                    UISoundPlayer.instance.Play(sndPathDockConnector);
                }
            },
                leaveHandler: newState => linkJoint.SetCoupleOnLinkMode(false),
                callOnShutdown: false);
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Deployed,
                enterHandler: oldState => {
                TurnConnectorPhysics(true);
                connectorModelObj.parent = connectorModelObj;
                PartModel.UpdateHighlighters(part);
                linkRenderer.StartRenderer(partCableAnchor, connectorCableAnchor);
            },
                leaveHandler: newState => {
                TurnConnectorPhysics(false);
                linkRenderer.StopRenderer();
            },
                callOnShutdown: false);
            connectorStateMachine.AddStateHandlers(
                ConnectorState.Plugged,
                enterHandler: oldState => {
                // Destroy the previous highlighter if any, since it would interfere with the new owner.
                DestroyImmediate(connectorModelObj.GetComponent <Highlighter>());
                connectorModelObj.parent = linkTarget.nodeTransform;
                PartModel.UpdateHighlighters(part);
                PartModel.UpdateHighlighters(linkTarget.part);
                connectorModelObj.GetComponentsInChildren <Renderer>().ToList()
                .ForEach(r => r.SetPropertyBlock(linkTarget.part.mpb));
                AlignTransforms.SnapAlign(
                    connectorModelObj, connectorPartAnchor, linkTarget.nodeTransform);
                linkRenderer.StartRenderer(partCableAnchor, connectorCableAnchor);
            },
                leaveHandler: newState => {
                var oldParent = connectorModelObj.GetComponentInParent <Part>();
                var oldHigh   = oldParent.HighlightActive;
                if (oldHigh)
                {
                    // Disable the part highlight to restore the connector's renderer materials.
                    oldParent.SetHighlight(false, false);
                }
                connectorModelObj.parent = nodeTransform; // Back to the model.
                PartModel.UpdateHighlighters(part);
                PartModel.UpdateHighlighters(oldParent);
                if (oldHigh)
                {
                    oldParent.SetHighlight(true, false);
                }
                linkRenderer.StopRenderer();
            },
                callOnShutdown: false);
        }