/// <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); } }
/// <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); } } }
/// <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); }
/// <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); }