/// <summary> /// Goes thru the parts on the source and target vessels, and tries to restore the coupling /// between the parts. /// </summary> /// <remarks> /// Any linking module on the source or the target vessel, which is linked and in the docking /// mode, will be attempted to use to restore the vessels coupling. This work will be done at the /// end of frame to let the other logic to cleanup. /// </remarks> /// <param name="tgtPart"> /// The former target part that was holding the coupling with this part. /// </param> void DelegateCouplingRole(Part tgtPart) { AsyncCall.CallOnEndOfFrame(this, () => { var candidates = new List <ILinkJoint>() .Concat(vessel.parts .SelectMany(p => p.Modules.OfType <ILinkJoint>()) .Where(j => !ReferenceEquals(j, this) && j.coupleOnLinkMode && j.isLinked && j.linkTarget.part.vessel == tgtPart.vessel)) .Concat(tgtPart.vessel.parts .SelectMany(p => p.Modules.OfType <ILinkJoint>()) .Where(j => j.coupleOnLinkMode && j.isLinked && j.linkTarget.part.vessel == vessel)); foreach (var joint in candidates) { HostedDebugLog.Fine(this, "Trying to couple via: {0}", joint); if (joint.SetCoupleOnLinkMode(true)) { HostedDebugLog.Info(this, "The coupling role is delegated to: {0}", joint); return; } } if (candidates.Any()) { HostedDebugLog.Warning(this, "None of the found candidates took the coupling role"); } }); }
public void PickupConnectorEvent() { var connector = closestConnector; if (connector != null) { var closestSource = connector.ownerModule as ILinkSource; HostedDebugLog.Info(this, "Try picking up a physical connector of: {0}...", closestSource); if (closestSource.LinkToTarget(LinkActorType.Player, this)) { var cableJoint = closestSource.linkJoint as ILinkCableJoint; if (cableJoint != null) { // By default, the cable joints set the length limit to the actual distance. cableJoint.SetCableLength(float.PositiveInfinity); } var updatableMenu = closestSource as IHasContextMenu; if (updatableMenu != null) { // Let the module know that we've changed the values. updatableMenu.UpdateContextMenu(); } } else { UISoundPlayer.instance.Play(KASAPI.CommonConfig.sndPathBipWrong); } } }
/// <inheritdoc/> public void OnJointBreak(float breakForce) { HostedDebugLog.Info(gameObject.transform, "Joint is broken with force {0}. Notifying part {1}", breakForce, hostPart); hostPart.FindModulesImplementing <IKasJointEventsListener>() .ForEach(x => x.OnKASJointBreak(gameObject, breakForce)); }
/// <inheritdoc/> protected override void CreatePartModel() { CreateLeverModels(); CreatePistonModels(); UpdateValuesFromModel(); // Log basic part values to help part's designers. HostedDebugLog.Info(this, "Procedural model: minLinkLength={0}, maxLinkLength={1}, attachNodePosition.Y={2}," + " pistonLength={3}, outerPistonDiameter={4}", minLinkLength, maxLinkLength, Hierarchy.FindTransformInChildren(srcStrutJoint, PivotAxleTransformName).position.y, pistonLength, outerPistonDiameter); // Init parked state. It must go after all the models are created. if (parkedOrientations.Count > 0) { persistedParkedOrientation = parkedOrientations[0].direction; } if (Mathf.Approximately(persistedParkedLength, 0)) { persistedParkedLength = minLinkLength; } UpdateLinkLengthAndOrientation(); }
/// <summary>Localizes the modules in the part and in all of its children parts.</summary> /// <param name="rootPart">The root part to start from.</param> static void UpdateLocalizationInPartHierarchy(Part rootPart) { HostedDebugLog.Info(rootPart, "EDITOR: Load localizations for the existing part from {0}", LibraryLoader.assemblyVersionStr); UpdateLocalizationInPartModules(rootPart); rootPart.children.ForEach(UpdateLocalizationInPartHierarchy); }
/// <inheritdoc/> public virtual void OnPartDie() { if (isLinked) { HostedDebugLog.Info(this, "Part has died. Drop the link to: {0}", linkSource); linkSource.BreakCurrentLink(LinkActorType.Physics); } }
public override void OnAwake() { base.OnAwake(); // The logging below will identify the owning part instance. HostedDebugLog.Info(part, "Part is being created"); // The logging below will identify the part instance and the specific module in it. HostedDebugLog.Info(this, "Module created"); }
/// <summary>Reacts on the vessel destruction and break the link if needed.</summary> /// <remarks>This event can get called from the physics callbacks.</remarks> /// <param name="targetVessel">The vessel that is being destroyed.</param> void OnVesselWillDestroyGameEvent(Vessel targetVessel) { if (isLinked && vessel != linkTarget.part.vessel && (targetVessel == vessel || targetVessel == linkTarget.part.vessel)) { HostedDebugLog.Info( this, "Drop the link due to the peer vessel destruction: {0}", targetVessel); BreakCurrentLink(LinkActorType.Physics); } }
public override void OnAwake() { base.OnAwake(); // This will load the localized string and print it into the log. HostedDebugLog.Info(this, msg1.Format("Blah", 123)); // The next example will only work if there is a localizable string defined. Otherwise, it will // print the tag. HostedDebugLog.Info(this, msg2.Format("Blah", 123)); }
public virtual void ReturnConnectorEvent() { if (FlightGlobals.ActiveVessel.isEVA && linkTarget != null && linkTarget.part.vessel == FlightGlobals.ActiveVessel) { BreakCurrentLink(LinkActorType.Player); SetConnectorState(ConnectorState.Locked); HostedDebugLog.Info( this, "{0} has returned the winch connector", FlightGlobals.ActiveVessel.vesselName); } }
/// <summary>Replaces stock light dimming animation to properly adjust emissive color.</summary> /// <param name="animation">Animation object to fix.</param> void ReplaceLightOnOffAnimation(Animation animation) { HostedDebugLog.Info(this, "Replacing animation clip with {0}", animation.clip.name); var clip = animation.clip; clip.ClearCurves(); clip.SetCurve(EmissiveLensModelPath, typeof(Material), "_EmissiveColor.a", AnimationCurve.EaseInOut(0, 0, 1.0f, 1.0f)); clip.SetCurve(lightName, typeof(Light), "m_Intensity", AnimationCurve.EaseInOut(0, 0, 1.0f, 1.0f)); }
public virtual void ReturnConnectorEvent() { if (FlightGlobals.ActiveVessel.isEVA && linkTarget != null && linkTarget.part.vessel == FlightGlobals.ActiveVessel) { var kerbalTarget = FlightGlobals.ActiveVessel.rootPart.Modules.OfType <ILinkTarget>() .FirstOrDefault(t => ReferenceEquals(t.linkSource, this)); BreakCurrentLink(LinkActorType.Player); SetConnectorState(ConnectorState.Locked); HostedDebugLog.Info( this, "{0} has returned the winch connector", FlightGlobals.ActiveVessel.vesselName); } }
/// <summary>Logically links the source and the target, and starts the renderer.</summary> /// <remarks>It's always called <i>before</i> the physical link updates.</remarks> /// <param name="target">The target to link with.</param> protected virtual void LogicalLink(ILinkTarget target) { HostedDebugLog.Info(this, "Linking to target: {0}, actor={1}", target, linkActor); var linkInfo = new KasLinkEventImpl(this, target, linkActor); otherPeer = target; linkTarget.linkSource = this; linkState = LinkState.Linked; linkRenderer.StartRenderer(nodeTransform, linkTarget.nodeTransform); KASAPI.KasEvents.OnLinkCreated.Fire(linkInfo); part.Modules.OfType <ILinkStateEventListener>().ToList() .ForEach(x => x.OnKASLinkedState(linkInfo, isLinked: true)); }
public override void OnAwake() { base.OnAwake(); // This will load the localized string and print it into the log. HostedDebugLog.Info(this, msg1.Format()); // The next example will only work if there is a localizable string defined. Otherwise, it will // print the tag. HostedDebugLog.Info(this, msg2.Format()); // A simple message can be just casted to string to get the localized content. PrintString(msg1); }
/// <inheritdoc cref="IPartModule.OnLoad" /> public override void OnLoad(ConfigNode node) { ConfigAccessor.ReadPartConfig(this, cfgNode: node); ConfigAccessor.ReadFieldsFromNode(node, GetType(), this, StdPersistentGroups.PartPersistant); base.OnLoad(node); if (vessel == null && PartLoader.Instance.IsReady()) { HostedDebugLog.Info(this, "EVA construction part loaded"); OnEvaPartLoaded(); } if (!_moduleSettingsLoaded) { _moduleSettingsLoaded = true; InitModuleSettings(); } }
/// <summary>Converts a physical connector back into the physicsless model.</summary> /// <remarks>It's a cleanup method that must always succeed.</remarks> void StopPhysicsOnConnector() { if (connectorObj == null || !linkRenderer.isStarted) { return; // Nothing to do. } HostedDebugLog.Info(this, "Make the cable connector non-physical"); linkRenderer.StopRenderer(); cableJoint.StopPhysicalHead(); KASInternalPhysicalConnector.Demote(connectorObj.gameObject, false); Destroy(connectorObj.gameObject); connectorObj = null; part.mass += connectorMass; part.rb.mass += connectorMass; SaveConnectorModelPosAndRot(); }
/// <summary> /// Checks if the cable connector can be locked without triggering significant physical forces. /// </summary> /// <param name="logCheckResult"> /// If <c>true</c> then the result of the check will be logged. /// </param> /// <returns> /// <c>true</c> if projection of the position and direction of the connector, and whatever is /// attached to it, won't deal a significant disturbance to the system. /// </returns> bool CheckIsConnectorAligned(bool logCheckResult) { // Check the pre-conditions. if (cableJoint.deployedCableLength > Mathf.Epsilon || // Cable is not fully retracted. cableJoint.realCableLength > connectorLockMaxErrorDist) // Not close enough. { if (logCheckResult) { HostedDebugLog.Info(this, "Connector cannot lock, the preconditions failed:" + " maxLength={0}, realLength={1}, isLinked={2}", cableJoint.deployedCableLength, cableJoint.realCableLength, isLinked); } return(false); } // The alignment doesn't matter if the connector is not attached to anything. if (!isLinked) { if (logCheckResult) { HostedDebugLog.Info(this, "Unplugged connector is allowed to lock"); } return(true); } // Check if the alignment error is small enough to not awake Kraken on dock. var fwdAngleErr = 180 - Vector3.Angle(GetConnectorModelPipeAnchor().forward, nodeTransform.forward); if (fwdAngleErr > connectorLockMaxErrorDir) { if (logCheckResult) { HostedDebugLog.Info( this, "Plugged connector align error: yaw/pitch={0}", fwdAngleErr); } return(false); } if (logCheckResult) { HostedDebugLog.Info(this, "Plugged connector is allowed to lock"); } return(true); }
public void OnSave(ConfigNode node) { node.AddValue("partName", availablePart.name); node.AddValue("slot", slot); ConfigAccessor.WriteFieldsIntoNode( node, GetType(), this, group: StdPersistentGroups.PartPersistant); // Items in pod and container may have equipped status True but they are not actually equipped, // so there is no equipped part. if (equipped && equippedPart != null && (equipMode == EquipMode.Part || equipMode == EquipMode.Physic)) { HostedDebugLog.Info( inventory, "Update config node of equipped part: {0}", availablePart.name); partNode = KISAPI.PartNodeUtils.PartSnapshot(equippedPart); } partNode.CopyTo(node.AddNode("PART")); }
/// <inheritdoc/> public override void OnLoad(ConfigNode node) { base.OnLoad(node); if (part.Resources.Count == 0) { HostedDebugLog.Error(this, "No resources on the canister! This won't work."); return; } if (part.Resources.Count > 1) { HostedDebugLog.Error(this, "Too many resources on the canister! The first one will be used."); } _mainResourceName = part.Resources[0].resourceName; if (_mainResourceName != StockResourceNames.EvaPropellant) { HostedDebugLog.Info(this, "Using a customized resource type: {0}", _mainResourceName); } }
/// <summary> /// Logically un-links the source and the current target, and stops the renderer. /// </summary> /// <remarks>It's always called <i>after</i> the physical link updates.</remarks> /// <param name="actorType">The actor which has initiated the un-linking.</param> protected virtual void LogicalUnlink(LinkActorType actorType) { HostedDebugLog.Info(this, "Un-linking from target: {0}, actor={1}", linkTarget, actorType); linkActor = actorType; var linkInfo = new KasLinkEventImpl(this, linkTarget, actorType); linkRenderer.StopRenderer(); SetLinkState(LinkState.Available); if (linkTarget != null) { linkTarget.linkSource = null; SetOtherPeer(null); } linkActor = LinkActorType.None; KASAPI.KasEvents.OnLinkBroken.Fire(linkInfo); part.Modules.OfType <ILinkStateEventListener>().ToList() .ForEach(x => x.OnKASLinkedState(linkInfo, isLinked: false)); }
public void Unequip(ActorType actorType = ActorType.API) { if (!prefabModule) { return; } // This must be the first thing to happen to prevent the other handlers to trigger. equipped = false; if (equipMode == EquipMode.Model) { UnityEngine.Object.Destroy(equippedGameObj); } if (equipMode == EquipMode.Part || equipMode == EquipMode.Physic) { HostedDebugLog.Info(inventory, "Update config node of the equipped part: {0}", equippedPart); partNode = KISAPI.PartNodeUtils.PartSnapshot(equippedPart); equippedPart.Die(); } evaTransform = null; equippedPart = null; equippedGameObj = null; if (actorType == ActorType.Player) { UISoundPlayer.instance.Play(prefabModule.moveSndPath); } prefabModule.OnUnEquip(this); // Return back the stock meshes if the custom helmet is unequipped. if (equipSlot == HelmetSlotName) { var kerbalModule = inventory.part.FindModuleImplementing <KerbalEVA>(); if (kerbalModule.helmetTransform != null) { for (var i = 0; i < kerbalModule.helmetTransform.childCount; i++) { kerbalModule.helmetTransform.GetChild(i).gameObject.SetActive(true); } } else { DebugEx.Warning("Kerbal model doesn't have helmet transform: {0}", inventory.part); } } }
/// <inheritdoc/> public virtual void OnJointBreak(float breakForce) { HostedDebugLog.Fine(this, "Joint is broken with force: {0}", breakForce); Part parentPart = null; Vector3 relPos = Vector3.zero; Quaternion relRot = Quaternion.identity; if (isLinked && part.parent != linkTarget.part) { // Calculate relative position and rotation of the part to properly restore the coupling. parentPart = part.parent; var root = vessel.rootPart.transform; var rootRotation = root.rotation; var thisPartPos = root.TransformPoint(part.orgPos); var thisPartRot = rootRotation * part.orgRot; var parentPartPos = root.TransformPoint(parentPart.orgPos); var parentPartRot = rootRotation * parentPart.orgRot; relPos = parentPartRot.Inverse() * (thisPartPos - parentPartPos); relRot = parentPartRot.Inverse() * thisPartRot; } // The break event is sent for *any* joint on the game object that got broken. Even though it // may be KAS joint broken, the owner part will decouple from the vessel due to the KSP core // doesn't validate which joint has actually broke. AsyncCall.CallOnFixedUpdate(this, () => { if (isLinked && customJoints.Any(x => x == null)) { // It was KAS joint that broke. Restore the part attachment and break KAS link. if (parentPart != null) { HostedDebugLog.Fine(this, "Restore coupling with: {0}", parentPart); var parentPartTransform = parentPart.transform; var parentPartRotation = parentPartTransform.rotation; var partTransform = part.transform; partTransform.position = parentPartTransform.position + parentPartRotation * relPos; partTransform.rotation = parentPartRotation * relRot; part.Couple(parentPart); } HostedDebugLog.Info(this, "KAS joint is broken, unlink the parts"); linkSource.BreakCurrentLink(LinkActorType.Physics); } }); }
/// <summary>Adds seat inventories to cover the maximum pod occupancy.</summary> /// <remarks> /// If the part already has seat inventories, they will be adjusted to have the unique seat /// indexes. This is usefull if the part's config provides the needed number of modules. If number /// of the existing modules is not enough to cover <c>CrewCapacity</c>, extra modules are added. /// </remarks> /// <param name="part">The part to add seat inventorties for.</param> public static void AddPodInventories(Part part) { // Check the fields that once had unexpected values. if (part.partInfo == null) { HostedDebugLog.Error(part, "Unexpected part configuration: partInfo=<NULL>"); return; } if (part.partInfo.partConfig == null) { HostedDebugLog.Error(part, "Unexpected part configuration: partConfig=<NULL>"); return; } var checkInventories = part.Modules.OfType <ModuleKISInventory>() .Where(m => m.invType == ModuleKISInventory.InventoryType.Pod) .ToArray(); var seatIndex = 0; foreach (var inventory in checkInventories) { HostedDebugLog.Info( inventory, "Assing seat to a pre-configured pod inventory: {0}", seatIndex); evaInventory.TryGetValue("slotsX", ref inventory.slotsX); evaInventory.TryGetValue("slotsY", ref inventory.slotsY); evaInventory.TryGetValue("maxVolume", ref inventory.maxVolume); inventory.podSeat = seatIndex++; } while (seatIndex < part.CrewCapacity) { var moduleNode = new ConfigNode("MODULE", "Dynamically created by KIS."); evaInventory.CopyTo(moduleNode); moduleNode.SetValue("name", typeof(ModuleKISInventory).Name, createIfNotFound: true); moduleNode.SetValue( "invType", ModuleKISInventory.InventoryType.Pod.ToString(), createIfNotFound: true); moduleNode.SetValue("podSeat", seatIndex, createIfNotFound: true); part.partInfo.partConfig.AddNode(moduleNode); var inventory = part.AddModule(moduleNode, forceAwake: true); HostedDebugLog.Info(inventory, "Dynamically create pod inventory at seat: {0}", seatIndex); seatIndex++; } }
/// <summary> /// Makes the winch connector an idependent physcal onbject or returns it into a part's model as /// a physicsless object. /// </summary> /// <remarks> /// Note, that physics objects on the connector don't die in this method call. They will be /// cleaned up at the frame end. The caller must consider it when dealing with the connector. /// </remarks> /// <param name="state">The physical state of the connector: <c>true</c> means "physical".</param> void TurnConnectorPhysics(bool state) { if (state && cableJoint.headRb == null) { HostedDebugLog.Info(this, "Make the cable connector physical"); var connector = KASInternalPhysicalConnector.Promote( this, connectorModelObj.gameObject, connectorInteractDistance); cableJoint.StartPhysicalHead(this, connectorCableAnchor); connector.connectorRb.mass = connectorMass; part.mass -= connectorMass; part.rb.mass -= connectorMass; } else if (!state && cableJoint.headRb != null) { HostedDebugLog.Info(this, "Make the cable connector non-physical"); cableJoint.StopPhysicalHead(); KASInternalPhysicalConnector.Demote(connectorModelObj.gameObject); part.mass += connectorMass; part.rb.mass += connectorMass; } }
/// <inheritdoc/> public virtual void OnJointBreak(float breakForce) { HostedDebugLog.Fine(this, "Joint is broken with force: {0}", breakForce); Part parentPart = null; Vector3 relPos = Vector3.zero; Quaternion relRot = Quaternion.identity; if (part.parent != linkTarget.part) { // Calculate relative position and rotation of the part to properly restore the coupling. parentPart = part.parent; var root = vessel.rootPart.transform; var thisPartPos = root.TransformPoint(part.orgPos); var thisPartRot = root.rotation * part.orgRot; var parentPartPos = root.TransformPoint(parentPart.orgPos); var parentPartRot = root.rotation * parentPart.orgRot; relPos = parentPartRot.Inverse() * (thisPartPos - parentPartPos); relRot = parentPartRot.Inverse() * thisPartRot; } // The break event is sent for *any* joint on the game object that got broken. However, it may // not be our link's joint. To figure it out, wait till the engine has cleared the object. AsyncCall.CallOnFixedUpdate(this, () => { if (isLinked && customJoints.Any(x => x == null)) { if (parentPart != null) { HostedDebugLog.Fine(this, "Restore coupling with: {0}", parentPart); part.transform.position = parentPart.transform.position + parentPart.transform.rotation * relPos; part.transform.rotation = parentPart.transform.rotation * relRot; part.Couple(parentPart); } HostedDebugLog.Info(this, "KAS joint is broken, unlink the parts"); linkSource.BreakCurrentLink(LinkActorType.Physics); } }); }
/// <summary>Converts a physicsless connector model into a physical object.</summary> void StartPhysicsOnConnector() { HostedDebugLog.Info(this, "Make the cable connector physical"); var connectorPosAndRot = gameObject.transform.TransformPosAndRot(persistedConnectorPosAndRot); var connectorModel = GetConnectorModel(); var pipeAttach = GetConnectorModelPipeAnchor(); var partAttach = GetConnectorModelPartAnchor(); // Make a physical object and attach renderer to it. This will make connector following physics. // Adjust pipe and part transforms the same way as in the connector. connectorObj = new GameObject( "physicalConnectorObj" + part.launchID + "-" + linkRendererName).transform; connectorObj.SetPositionAndRotation(connectorModel.position, connectorModel.rotation); var physPartAttach = new GameObject(partAttach.name + "-reverseAnchor").transform; physPartAttach.SetPositionAndRotation( partAttach.position, Quaternion.LookRotation(-partAttach.forward, -partAttach.up)); physPartAttach.parent = connectorObj; var physPipeAttachObj = new GameObject(pipeAttach.name + "-Anchor").transform; physPipeAttachObj.parent = connectorObj; physPipeAttachObj.SetPositionAndRotation(pipeAttach.position, pipeAttach.rotation); connectorObj.SetPositionAndRotation(connectorPosAndRot.pos, connectorPosAndRot.rot); var connector = KASInternalPhysicalConnector.Promote( this, connectorObj.gameObject, connectorInteractDistance); connector.connectorRb.mass = connectorMass; part.mass -= connectorMass; part.rb.mass -= connectorMass; linkRenderer.StartRenderer(nodeTransform, physPartAttach); Colliders.UpdateColliders(connectorModel.gameObject); cableJoint.StartPhysicalHead(this, physPipeAttachObj); SaveConnectorModelPosAndRot(); }
/// <inheritdoc/> public virtual bool SetCoupleOnLinkMode(bool isCoupleOnLink) { if (!isLinked) { coupleOnLinkMode = isCoupleOnLink; HostedDebugLog.Fine( this, "Coupling mode updated in a non-linked module: {0}", isCoupleOnLink); return(true); } if (isCoupleOnLink && (linkSource.coupleNode == null || linkTarget.coupleNode == null)) { HostedDebugLog.Error(this, "Cannot couple due to source or target doesn't support it"); coupleOnLinkMode = false; return(false); } if (isCoupleOnLink && linkSource.part.vessel != linkTarget.part.vessel) { // Couple the parts, and drop the other link(s). HostedDebugLog.Info(this, "Change coupling mode: ATTACHED => COUPLED"); DetachParts(); coupleOnLinkMode = isCoupleOnLink; CoupleParts(); } else if (!isCoupleOnLink && isCoupled) { // Decouple the parts, and make the non-coupling link(s). HostedDebugLog.Info(this, "Change coupling mode: COUPLED => ATTACHED"); DecoupleParts(); coupleOnLinkMode = isCoupleOnLink; AttachParts(); } else { coupleOnLinkMode = isCoupleOnLink; // Simply change the mode. } return(true); }
/// <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 CheckCoupleNode() { base.CheckCoupleNode(); if (coupleNode == null) { // If the part doesn't want to couple, then we should obey. if (parsedAttachNode.attachedPart != null) { HostedDebugLog.Error( this, "Cannot maintain coupling with: {0}", parsedAttachNode.attachedPart); if (linkState == LinkState.Available) { AsyncCall.CallOnEndOfFrame(this, () => { if (parsedAttachNode.attachedPart) { HostedDebugLog.Info( this, "Decoupling incompatible part: {0}", parsedAttachNode.attachedPart); parsedAttachNode.attachedPart.decouple(); } }); } else if (linkState == LinkState.Linked && linkTarget != null) { HostedDebugLog.Warning(this, "Breaking the link to: {0}", linkTarget); AsyncCall.CallOnEndOfFrame(this, () => BreakCurrentLink(LinkActorType.API)); } else { AsyncCall.CallOnEndOfFrame(this, () => { if (parsedAttachNode.attachedPart) { HostedDebugLog.Error( this, "Cannot pickup coupling in unexpected link state: {0}", linkState); parsedAttachNode.attachedPart.decouple(); } }); } } return; } if (linkState == LinkState.Available && coupleNode != null && coupleNode.attachedPart != null) { var target = coupleNode.attachedPart.Modules .OfType <ILinkTarget>() .FirstOrDefault(t => t.cfgLinkType == cfgLinkType && t.linkState == LinkState.Available && t.coupleNode != null && t.coupleNode.attachedPart == part && CheckCanLinkTo(t)); if (target != null) { HostedDebugLog.Fine(this, "Linking with the pre-attached part: {0}", target); LinkToTarget(LinkActorType.API, target); } if (!isLinked) { // Let the other part a chance to couple, and block if it didn't succeed. HostedDebugLog.Fine(this, "Cannot link, wait for the other part: target={0}", coupleNode.attachedPart); AsyncCall.CallOnEndOfFrame(this, () => { if (linkState == LinkState.Available && coupleNode.attachedPart != null) { HostedDebugLog.Warning(this, "Cannot link to the pre-attached part: from={0}, to={1}", KASAPI.AttachNodesUtils.NodeId(coupleNode), KASAPI.AttachNodesUtils.NodeId(coupleNode.FindOpposingNode())); SetLinkState(LinkState.NodeIsBlocked); } }); } } else if (linkState == LinkState.NodeIsBlocked && parsedAttachNode.attachedPart == null) { SetLinkState(LinkState.Available); } // Restore the link state if not yet done. if (isLinked && !linkJoint.isLinked) { linkJoint.CreateJoint(this, linkTarget); } UpdateContextMenu(); // To update the dock/undock menu. }
/// <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); }