/// <inheritdoc/> public virtual bool CreateJoint(ILinkSource source, ILinkTarget target) { if (isLinked) { HostedDebugLog.Error( this, "Cannot link the joint which is already linked to: {0}", linkTarget); return(false); } if (!CheckCoupled(source, target)) { var errors = CheckConstraints(source, target); if (errors.Length > 0) { HostedDebugLog.Error(this, "Cannot create joint:\n{0}", DbgFormatter.C2S(errors)); return(false); } } else { HostedDebugLog.Fine(this, "The parts are coupled. Skip the constraints check"); } linkSource = source; linkTarget = target; if (!originalLength.HasValue) { SetOriginalLength(Vector3.Distance( GetSourcePhysicalAnchor(source), GetTargetPhysicalAnchor(source, target))); } isLinked = true; // If the parts are already coupled at this moment, then the mode must be set as such. coupleOnLinkMode |= isCoupled; // Ensure the coupling can be done. coupleOnLinkMode &= linkSource.coupleNode != null && linkTarget.coupleNode != null; if (coupleOnLinkMode) { CoupleParts(); } else { AttachParts(); } return(true); }
/// <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); } }); }
/// <inheritdoc/> public string[] CheckColliderHits(Transform source, Transform target) { if (!pipeColliderIsPhysical) { return(new string[0]); // No need to check, the meshes will never collide. } // HACK: Start the renderer before getting the pipes. var oldStartState = isStarted; var oldPhysicalState = isPhysicalCollider; if (!isStarted) { isPhysicalCollider = false; StartRenderer(source, target); } else if (sourceTransform != source || targetTransform != target) { HostedDebugLog.Error(this, "Cannot verify hits on a started renderer"); } var points = GetPipePath(source, target); if (!oldStartState) { StopRenderer(); isPhysicalCollider = oldPhysicalState; } var hitParts = new HashSet <Part>(); for (var i = 0; i < points.Length - 1; i++) { CheckHitsForCapsule(points[i + 1], points[i], pipeDiameter, target, hitParts); } var hitMessages = new List <string>(); foreach (var hitPart in hitParts) { hitMessages.Add(hitPart != null ? LinkCollidesWithObjectMsg.Format(hitPart) : LinkCollidesWithSurfaceMsg.Format()); } return(hitMessages.ToArray()); }
/// <inheritdoc/> public virtual bool LinkToTarget(ILinkTarget target) { if (!linkStateMachine.CheckCanSwitchTo(LinkState.Linked)) { if (linkActor == LinkActorType.Player) { ShowStatusMessage(SourceIsNotAvailableForLinkMsg, isError: true); } HostedDebugLog.Error(this, "Cannot link in state: {0}", linkState); return(false); } if (!CheckCanLinkTo(target, reportToGUI: linkActor == LinkActorType.Player)) { return(false); } LogicalLink(target); PhysicaLink(); return(true); }
/// <inheritdoc/> protected override void CheckSettingsConsistency() { base.CheckSettingsConsistency(); if (!allowCoupling && coupleMode == CoupleMode.AlwaysCoupled) { allowCoupling = true; HostedDebugLog.Warning( this, "Inconsistent setting fixed: allowCoupling => true, due to coupleMode={0}", coupleMode); } if (!allowCoupling && linkJoint != null && linkJoint.coupleOnLinkMode) { // This check is needed for debug only. linkJoint.SetCoupleOnLinkMode(false); HostedDebugLog.Warning( this, "Inconsistent setting fixed: coupleOnLinkMode => false, due to allowCoupling={0}", allowCoupling); } }
/// <inheritdoc/> public virtual void StopRenderer() { // Stop meshes updates. if (_linkUpdateCoroutine != null) { HostedDebugLog.Fine(this, "Stopping renderer updates..."); StopCoroutine(_linkUpdateCoroutine); _linkUpdateCoroutine = null; } // Sync the renderers settings to the source part to handle the highlights. if (isStarted) { sourceTransform.GetComponentsInChildren <Renderer>().ToList() .ForEach(r => r.SetPropertyBlock(part.mpb)); targetTransform.GetComponentsInChildren <Renderer>().ToList() .ForEach(r => r.SetPropertyBlock(part.mpb)); } DestroyPipeMesh(); PartModel.UpdateHighlighters(part); // Update the target vessel relations (if any). if (targetPart != null) { PartModel.UpdateHighlighters(targetPart); if (targetPart.vessel != vessel) { targetPart.vessel.parts .Where(p => p != null) // It's a cleanup method. .ToList() .ForEach(p => SetCollisionIgnores(p, false)); } } targetPart = null; sourceTransform = null; targetTransform = null; GameEvents.onPartCoupleComplete.Remove(OnPartCoupleCompleteEvent); GameEvents.onPartDeCouple.Remove(OnPartDeCoupleEvent); GameEvents.onPartDeCoupleComplete.Remove(OnPartDeCoupleCompleteEvent); }
/// <inheritdoc/> protected override void SetupPhysXJoints() { if (isHeadStarted) { HostedDebugLog.Warning(this, "A physical head is running. Stop it before the link!"); StopPhysicalHead(); } var needStockJoint = isCoupled && isLockedWhenCoupled; if (needStockJoint && partJoint == null) { if (linkTarget.part.parent == linkSource.part) { HostedDebugLog.Fine(this, "Create a stock joint: from={0}, to={1}", linkTarget, linkSource); linkTarget.part.CreateAttachJoint(AttachModes.STACK); } else if (linkSource.part.parent == linkTarget.part) { HostedDebugLog.Fine(this, "Create a stock joint: from={0}, to={1}", linkSource, linkTarget); linkSource.part.CreateAttachJoint(AttachModes.STACK); } else { HostedDebugLog.Error( this, "Cannot create stock joint: {0} <=> {1}", linkSource, linkTarget); needStockJoint = false; } } else if (!needStockJoint && partJoint != null) { HostedDebugLog.Fine( this, "Drop stock joint: to={0}, isLockedWhenDocked={1}, isCoupled={2}", partJoint.Child, isLockedWhenCoupled, isCoupled); partJoint.DestroyJoint(); partJoint.Child.attachJoint = null; } if (!needStockJoint) { CreateDistanceJoint( linkSource, linkTarget.part.Rigidbody, GetTargetPhysicalAnchor(linkSource, linkTarget)); } }
/// <inheritdoc/> protected override void SetupStateMachine() { base.SetupStateMachine(); linkStateMachine.onAfterTransition += (start, end) => HostedDebugLog.Fine( this, "Target state changed at {0}: {1} => {2}", attachNodeName, start, end); linkStateMachine.SetTransitionConstraint( LinkState.Available, new[] { LinkState.AcceptingLinks, LinkState.RejectingLinks, LinkState.NodeIsBlocked }); linkStateMachine.SetTransitionConstraint( LinkState.NodeIsBlocked, new[] { LinkState.Available }); linkStateMachine.SetTransitionConstraint( LinkState.AcceptingLinks, new[] { LinkState.Available, LinkState.Linked, LinkState.Locked }); 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(OnStartConnecting), leaveHandler: x => KASAPI.KasEvents.OnStartLinking.Remove(OnStartConnecting)); linkStateMachine.AddStateHandlers( LinkState.AcceptingLinks, enterHandler: x => KASAPI.KasEvents.OnStopLinking.Add(OnStopConnecting), leaveHandler: x => KASAPI.KasEvents.OnStopLinking.Remove(OnStopConnecting)); linkStateMachine.AddStateHandlers( LinkState.RejectingLinks, enterHandler: x => KASAPI.KasEvents.OnStopLinking.Add(OnStopConnecting), leaveHandler: x => KASAPI.KasEvents.OnStopLinking.Remove(OnStopConnecting)); linkStateMachine.AddStateHandlers( LinkState.AcceptingLinks, enterHandler: x => SetEligiblePartHighlighting(true), leaveHandler: x => SetEligiblePartHighlighting(false), callOnShutdown: false); }
/// <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++; } }
/// <inheritdoc/> public AttachNode ParseNodeFromString(Part ownerPart, string def, string nodeId) { ArgumentGuard.NotNull(ownerPart, "ownerPart"); ArgumentGuard.NotNullOrEmpty(def, "def", context: ownerPart); ArgumentGuard.NotNullOrEmpty(nodeId, "nodeId", context: ownerPart); var array = def.Split(','); ArgumentGuard.InRange(array.Length, "def", 6, 10, message: "Unexpected number of components", context: ownerPart); try { // The logic is borrowed from PartLoader.ParsePart. var attachNode = new AttachNode(); attachNode.owner = ownerPart; attachNode.id = nodeId; var factor = ownerPart.rescaleFactor; attachNode.position = new Vector3( float.Parse(array[0]), float.Parse(array[1]), float.Parse(array[2])) * factor; attachNode.orientation = new Vector3( float.Parse(array[3]), float.Parse(array[4]), float.Parse(array[5])) * factor; attachNode.originalPosition = attachNode.position; attachNode.originalOrientation = attachNode.orientation; attachNode.size = array.Length >= 7 ? int.Parse(array[6]) : 1; attachNode.attachMethod = array.Length >= 8 ? (AttachNodeMethod)int.Parse(array[7]) : AttachNodeMethod.FIXED_JOINT; if (array.Length >= 9) { attachNode.ResourceXFeed = int.Parse(array[8]) > 0; } if (array.Length >= 10) { attachNode.rigid = int.Parse(array[9]) > 0; } attachNode.nodeType = AttachNode.NodeType.Stack; return(attachNode); } catch (Exception ex) { HostedDebugLog.Error(ownerPart, "Cannot parse node '{0}' from: {1}\nError: {2}", nodeId, def, ex.Message); return(null); } }
/// <summary>Gets the texture from either a KSP gamebase or the internal cache.</summary> /// <remarks> /// It's OK to call this method in the performance demanding code since once the texture is /// successfully returned it's cached internally. The subsequent calls won't issue expensive game /// database requests. /// </remarks> /// <param name="textureFileName"> /// Filename of the texture file. The path is realtive to <c>GameData</c> folder. The name must /// not have the file extension. /// </param> /// <param name="asNormalMap">If <c>true</c> then the texture will be loaded as a bumpmap.</param> /// <returns> /// The texture. Note that it's a shared object. Don't execute actions on it which you don't want /// to affect the other meshes in the game. /// </returns> /// <seealso href="https://docs.unity3d.com/ScriptReference/Texture2D.html"> /// Unity3D: Texture2D</seealso> protected Texture2D GetTexture(string textureFileName, bool asNormalMap = false) { Texture2D texture; if (!textures.TryGetValue(textureFileName, out texture)) { texture = GameDatabase.Instance.GetTexture(textureFileName, asNormalMap); if (texture == null) { // Use a "red" texture if no file found. HostedDebugLog.Warning(this, "Cannot load texture: {0}", textureFileName); texture = new Texture2D(1, 1, TextureFormat.ARGB32, false); texture.SetPixels(new[] { Color.red }); texture.Apply(); texture.Compress(highQuality: false); } textures[textureFileName] = texture; } return(texture); }
/// <inheritdoc/> public Transform GetTransformForNode(Part ownerPart, AttachNode an) { ArgumentGuard.NotNull(ownerPart, "ownerPart"); ArgumentGuard.NotNull(an, "an", context: ownerPart); if (an.owner != ownerPart) { HostedDebugLog.Warning( ownerPart, "Attach node doesn't belong to the part: {0}", NodeId(an)); } var partModel = Hierarchy.GetPartModelTransform(ownerPart); var objectName = "attachNode-" + an.id; var nodeTransform = partModel.Find(objectName) ?? new GameObject(objectName).transform; Hierarchy.MoveToParent( nodeTransform, partModel, newPosition: an.position / ownerPart.rescaleFactor, newRotation: Quaternion.LookRotation(an.orientation)); return(nodeTransform); }
/// <summary>Reacts on a part coupling and adjusts its colliders as needed.</summary> /// <remarks> /// The pipe meshes should not collide with the target vessel. So track the part changes on the /// target vessel and disable collisions on the newly appeared parts. /// </remarks> /// <param name="action">The callback action.</param> void OnPartCoupleCompleteEvent(GameEvents.FromToAction <Part, Part> action) { if (targetPart != null && targetPart.vessel != vessel && (action.from.vessel == targetPart.vessel || action.to.vessel == targetPart.vessel)) { if (action.from == targetPart) { // The target part has couple to a new vessel. HostedDebugLog.Fine(this, "Set collision ignores on: {0}", action.to.vessel); action.to.vessel.parts .ForEach(p => SetCollisionIgnores(p, true)); } else { // A part has joined the target vessel. HostedDebugLog.Fine(this, "Set collision ignores on: {0}", action.from); SetCollisionIgnores(action.from, true); } } }
/// <summary> /// Ensures that max setting of the FloatRange UI control is not less than the provided value. /// </summary> void SetupFloatUiControlMax(BaseField field, UI_Control control, float refValue) { if (control != null) { var uiFloat = control as UI_FloatRange; if (uiFloat == null) { HostedDebugLog.Error( this, "Field is not of a FloatRange type: {0}", field.MemberInfo.Name); return; } if (uiFloat.maxValue < refValue) { HostedDebugLog.Fine( this, "Adjust field max value: field={0}, oldMax={1}, newMax={2}", field.MemberInfo.Name, uiFloat.maxValue, refValue); uiFloat.maxValue = refValue; } } }
/// <summary>Reacts on the vessel name change and updates the vessel infos.</summary> void OnVesselRename(GameEvents.HostedFromToAction <Vessel, string> action) { if (!isLinked || action.host != vessel) { return; // Nothing to do. } if (persistedSrcVesselInfo.rootPartUId == action.host.rootPart.flightID) { persistedSrcVesselInfo.name = action.host.vesselName; persistedSrcVesselInfo.vesselType = action.host.vesselType; HostedDebugLog.Fine(this, "Update source vessel info to: name={0}, type={1}", persistedSrcVesselInfo.name, persistedSrcVesselInfo.vesselType); } if (persistedTgtVesselInfo.rootPartUId == action.host.rootPart.flightID) { persistedTgtVesselInfo.name = action.host.vesselName; persistedTgtVesselInfo.vesselType = action.host.vesselType; HostedDebugLog.Fine(this, "Update target vessel info to: name={0}, type={1}", persistedTgtVesselInfo.name, persistedTgtVesselInfo.vesselType); } }
/// <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; } }
/// <summary>Loads the state that should be processed after all the modules are created.</summary> /// <remarks> /// This method can be called by the debug tool, so add some extra checks to not critically fail /// if the settings are not correct. /// </remarks> void InitStartState() { var oldLinkJoint = linkJoint; linkJoint = part.Modules.OfType <ILinkJoint>() .FirstOrDefault(x => x.cfgJointName == jointName); if (linkJoint == null) { HostedDebugLog.Error(this, "Cannot find joint module: {0}", jointName); } linkJoint = linkJoint ?? oldLinkJoint; var oldLinkRenderer = linkRenderer; linkRenderer = part.Modules.OfType <ILinkRenderer>() .FirstOrDefault(x => x.cfgRendererName == linkRendererName); if (linkRenderer == null) { HostedDebugLog.Error(this, "Cannot find renderer module: {0}", linkRendererName); } linkRenderer = linkRenderer ?? oldLinkRenderer; }
/// <summary>Reacts on a part de-coupling and adjusts the docking mode.</summary> /// <remarks> /// This is a cleanup method that verifies that all links in the DOCKED mode remained coupled /// after decoupling of the part. If it's not the case, the DOCKED mode is reset to ATTACHED. In /// the normal case the joint module takes care of restoring the affected couplings, and this /// method becomes NO-OP. /// </remarks> /// <param name="originator">The part that has decoupled.</param> void OnPartDeCoupleCompleteEvent(Part originator) { if (!isLinked || !linkJoint.coupleOnLinkMode || linkTarget.part.vessel == vessel) { return; // Not interested. } // Wait for one frame to allow joint logic to restore the coupling, and then check. AsyncCall.CallOnEndOfFrame( this, () => { if (isLinked && linkJoint.coupleOnLinkMode && linkTarget.part.vessel != vessel) { HostedDebugLog.Fine( this, "Coupling has not been restored, resetting the docking mode: {0} <=> {1}", part, linkTarget.part); linkJoint.SetCoupleOnLinkMode(false); UpdateContextMenu(); } }, skipFrames: 1); }
/// <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); } }); }
/// <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); }
/// <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/> protected override void InitModuleSettings() { base.InitModuleSettings(); if (isAutoAttachNode && parsedAttachNode != null) { KASAPI.AttachNodesUtils.DropNode(part, parsedAttachNode); } parsedAttachNode = part.FindAttachNode(attachNodeName); isAutoAttachNode = parsedAttachNode == null; if (isAutoAttachNode) { parsedAttachNode = KASAPI.AttachNodesUtils.ParseNodeFromString( part, attachNodeDef, attachNodeName); if (parsedAttachNode != null) { HostedDebugLog.Fine( this, "Created auto node: {0}", KASAPI.AttachNodesUtils.NodeId(parsedAttachNode)); if (coupleNode != null && (HighLogic.LoadedSceneIsFlight || HighLogic.LoadedSceneIsEditor)) { // Only pre-add the node in the scenes that assume restoring a vessel state. // We'll drop it in the OnStartFinished if not used. KASAPI.AttachNodesUtils.AddNode(part, coupleNode); } } else { HostedDebugLog.Error(this, "Cannot create auto node from: {0}", attachNodeDef); } } if (parsedAttachNode != null) { // HACK: Handle a KIS issue which causes the nodes to be owned by the prefab part. parsedAttachNode.owner = part; nodeTransform = KASAPI.AttachNodesUtils.GetTransformForNode(part, parsedAttachNode); } }
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); System.Diagnostics.Debug.Assert(closestSource != null, nameof(closestSource) + " != null"); if (closestSource.LinkToTarget(LinkActorType.Player, this)) { // By default, the cable joints set the length limit to the actual distance. var cableJoint = closestSource.linkJoint as ILinkCableJoint; cableJoint?.SetCableLength(float.PositiveInfinity); // Let the module know that we've changed the values. var updateableMenu = closestSource as IHasContextMenu; updateableMenu?.UpdateContextMenu(); } else { UISoundPlayer.instance.Play(KASAPI.CommonConfig.sndPathBipWrong); } } }
/// <inheritdoc/> public override void OnDebugAdjustablesUpdated() { base.OnDebugAdjustablesUpdated(); AsyncCall.CallOnEndOfFrame( this, () => { HostedDebugLog.Warning(this, "Reloading settings..."); InitModuleSettings(); InitStartState(); UpdateContextMenu(); if (_dbgOldTarget != null) { HostedDebugLog.Warning(this, "Relinking to target: {0}", _dbgOldTarget); LinkToTarget(LinkActorType.Player, _dbgOldTarget); var cableJoint = linkJoint as ILinkCableJoint; if (cableJoint != null) { HostedDebugLog.Warning(this, "Restoring cable length: {0}", _dbgOldCableLength); cableJoint.SetCableLength(_dbgOldCableLength); } } }, skipFrames: 1); // The link's logic is asynchronous. }
/// <inheritdoc/> public override void OnStart(PartModule.StartState state) { // The EVA parts don't get the load method called. So, to complete the initalization, pretend // the method was called with no config provided. This is needed to make the parent working. Load(new ConfigNode()); base.OnStart(state); if (equipMeshName != "") { attachBoneTransform = Hierarchy.FindTransformByPath(part.transform, equipBoneName); if (attachBoneTransform != null) { boneAttachNodePosition = attachBoneTransform.InverseTransformPoint(nodeTransform.position); boneAttachNodeRotation = Quaternion.Inverse(attachBoneTransform.rotation) * nodeTransform.rotation; } else { HostedDebugLog.Error(this, "Cannot find bone for: mesh name={0}, bone name={1}", equipMeshName, equipBoneName); } } }
/// <summary> /// Makes the list of all fuels and mixtures that can be moved between the linked vessels. /// </summary> /// <remarks>This is a very expensive operation.</remarks> void MaybeUpdateResourceOptionList() { if (!resourceListNeedsUpdate) { return; // Nothing to do. } resourceListNeedsUpdate = false; HostedDebugLog.Fine(this, "Refreshing resources..."); // Gather all the resources that *both* vessel have. var leftResources = new HashSet <int>( vessel.parts .SelectMany(p => p.Resources) .Select(r => r.info.id)); var rightResources = new HashSet <int>( linkTarget.part.vessel.parts .SelectMany(p => p.Resources) .Select(r => r.info.id)); var availableResources = leftResources .Union(rightResources) .Distinct() .ToList(); // Find the predefined resources that the part can pump between the vessels. var allowedResourceIds = allowedResource .Select(x => StockResourceNames.GetId(x)) .ToArray(); if (allowedResourceIds.Length == 0) { // If no specific resources set, then allow all the vessel resources that are material and // not restricted for pumping. Allow overriding to include/exclude a specific resource. var overrideEnabled = resourceOverride .Where(x => x.Length > 0 && x[0] == '+') .Select(x => StockResourceNames.GetId(x.Substring(1))) .ToArray(); var overrideDisabled = resourceOverride .Where(x => x.Length > 0 && x[0] == '-') .Select(x => StockResourceNames.GetId(x.Substring(1))); var nonMovableIds = PartResourceLibrary.Instance.resourceDefinitions .Cast <PartResourceDefinition>() .Where(d => overrideEnabled.IndexOf(d.id) == -1 && (d.unitCost < float.Epsilon || d.volume < float.Epsilon || d.resourceTransferMode == ResourceTransferMode.NONE)) .Select(d => d.id) .Union(overrideDisabled) .ToArray(); allowedResourceIds = availableResources .Where(x => nonMovableIds.IndexOf(x) == -1) .ToArray(); } var movableResources = availableResources .Where(id => allowedResourceIds.IndexOf(id) != -1) // The GUI function will render the list in the reversed order. .OrderByDescending(id => id) .Select(id => new ResourceTransferOption(new[] { id }, new[] { 1.0 })) .ToList(); // Add the mixtures. var availableMixtures = fuelMixtures .Where(m => m.components.All(c => availableResources.Contains(StockResourceNames.GetId(c.name)))); foreach (var mixture in availableMixtures) { movableResources.Insert(0, new ResourceTransferOption( mixture.components.Select(x => StockResourceNames.GetId(x.name)).ToArray(), mixture.components.Select(x => x.ratio).ToArray())); } resourceRows = movableResources .Select(resource => resourceRowsHash.ContainsKey(resource.GetHashCode()) ? resourceRowsHash[resource.GetHashCode()] : resource) .ToArray(); resourceRowsHash = resourceRows.ToDictionary(r => r.GetHashCode()); }
/// <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); }
/// <summary>Creates the joins to make a physical link.</summary> protected override void SetupPhysXJoints() { // The stock joint is rigid, drop it. if (partJoint != null) { HostedDebugLog.Fine(this, "Dropping the stock joint to: {0}", partJoint.Child); partJoint.DestroyJoint(); partJoint.Child.attachJoint = null; } HostedDebugLog.Fine(this, "Creating a 2-joints assembly"); var srcAnchorPos = GetSourcePhysicalAnchor(linkSource); var tgtAnchorPos = GetTargetPhysicalAnchor(linkSource, linkTarget); var pipeHalfLength = (tgtAnchorPos - srcAnchorPos).magnitude / 2; middleObj = new GameObject("ConnectorObj"); middleObj.AddComponent <KASInternalBrokenJointListener>().hostPart = part; var middleRb = middleObj.AddComponent <Rigidbody>(); // PhysX behaves weird if the linked rigidbodies are too different in mass, so make the // connector object "somewhat" the same in mass as the both ends of the link. middleRb.mass = (linkSource.part.rb.mass + linkTarget.part.rb.mass) / 2; middleRb.useGravity = false; middleRb.velocity = linkSource.part.rb.velocity; middleRb.angularVelocity = linkSource.part.rb.angularVelocity; // Build all joints aligned to the source node direction to have the angle limits set correctly. middleObj.transform.position = srcAnchorPos + linkSource.nodeTransform.forward * pipeHalfLength; middleObj.transform.rotation = Quaternion.LookRotation(linkSource.nodeTransform.forward, linkSource.nodeTransform.up); srcJoint = middleObj.AddComponent <ConfigurableJoint>(); KASAPI.JointUtils.ResetJoint(srcJoint); KASAPI.JointUtils.SetupSphericalJoint(srcJoint, angleLimit: sourceLinkAngleLimit); srcJoint.autoConfigureConnectedAnchor = false; srcJoint.anchor = middleRb.transform.InverseTransformPoint(srcAnchorPos); srcJoint.connectedBody = linkSource.part.rb; srcJoint.connectedAnchor = srcJoint.connectedBody.transform.InverseTransformPoint(srcAnchorPos); SetBreakForces(srcJoint, linkBreakForce, linkBreakTorque); middleObj.transform.position = tgtAnchorPos + linkTarget.nodeTransform.forward * pipeHalfLength; middleObj.transform.rotation = Quaternion.LookRotation(-linkTarget.nodeTransform.forward, linkTarget.nodeTransform.up); trgJoint = middleObj.AddComponent <ConfigurableJoint>(); KASAPI.JointUtils.ResetJoint(trgJoint); KASAPI.JointUtils.SetupSphericalJoint(trgJoint, angleLimit: targetLinkAngleLimit); trgJoint.autoConfigureConnectedAnchor = false; trgJoint.anchor = middleRb.transform.InverseTransformPoint(tgtAnchorPos); trgJoint.connectedBody = linkTarget.part.rb; trgJoint.connectedAnchor = trgJoint.connectedBody.transform.InverseTransformPoint(tgtAnchorPos); SetBreakForces(trgJoint, linkBreakForce, linkBreakTorque); middleObj.transform.position = (tgtAnchorPos + srcAnchorPos) / 2; middleObj.transform.rotation = Quaternion.LookRotation( tgtAnchorPos - middleObj.transform.position, linkSource.nodeTransform.up); // This "joint" is only needed to disable the collisions between the parts. var collisionJoint = linkSource.part.gameObject.AddComponent <ConfigurableJoint>(); KASAPI.JointUtils.ResetJoint(collisionJoint); KASAPI.JointUtils.SetupDistanceJoint(collisionJoint); collisionJoint.xMotion = ConfigurableJointMotion.Free; collisionJoint.yMotion = ConfigurableJointMotion.Free; collisionJoint.zMotion = ConfigurableJointMotion.Free; collisionJoint.connectedBody = linkTarget.part.rb; SetCustomJoints(new[] { srcJoint, trgJoint, collisionJoint }, extraObjects: new[] { middleObj }); }
/// <summary>Intializes the connector model object and its anchors.</summary> /// <remarks> /// <para> /// If the connector model is not found then a stub object will be created. There will be no visual /// representation but the overall functionality of the winch should keep working. /// </para> /// <para> /// If the connector doesn't have the anchors then the missed ones will be created basing on the /// provided position/rotation. If the config file doesn't provide anything then the anchors will /// have a zero position and a random rotation. /// </para> /// </remarks> void LoadOrCreateConnectorModel() { var ConnectorModelName = "ConnectorModel" + part.Modules.IndexOf(this); var ConnectorParkAnchorName = "ConnectorParkAnchor" + part.Modules.IndexOf(this); const string CableAnchorName = "CableAnchor"; const string PartAnchorName = "PartAnchor"; if (!PartLoader.Instance.IsReady()) { // Make the missing models and set the proper hierarchy. connectorModelObj = Hierarchy.FindPartModelByPath(part, connectorModel); connectorCableAnchor = connectorCableAttachAt != "" ? Hierarchy.FindPartModelByPath(part, connectorCableAttachAt) : null; connectorPartAnchor = connectorPartAttachAt != "" ? Hierarchy.FindPartModelByPath(part, connectorPartAttachAt) : null; if (connectorModelObj == null) { HostedDebugLog.Error(this, "Cannot find a connector model: {0}", connectorModel); // Fallback to not have the whole code to crash. connectorModelObj = new GameObject().transform; } connectorModelObj.name = ConnectorModelName; connectorModelObj.parent = nodeTransform; if (connectorCableAnchor == null) { if (connectorCableAttachAt != "") { HostedDebugLog.Error( this, "Cannot find cable anchor transform: {0}", connectorCableAttachAt); } connectorCableAnchor = new GameObject().transform; var posAndRot = PosAndRot.FromString(connectorCableAttachAtPosAndRot); Hierarchy.MoveToParent(connectorCableAnchor, connectorModelObj, newPosition: posAndRot.pos, newRotation: posAndRot.rot); } connectorCableAnchor.name = CableAnchorName; connectorCableAnchor.parent = connectorModelObj; if (connectorPartAnchor == null) { if (connectorPartAttachAt != "") { HostedDebugLog.Error( this, "Cannot find part anchor transform: {0}", connectorPartAttachAt); } connectorPartAnchor = new GameObject().transform; var posAndRot = PosAndRot.FromString(connectorPartAttachAtPosAndRot); Hierarchy.MoveToParent(connectorPartAnchor, connectorModelObj, newPosition: posAndRot.pos, newRotation: posAndRot.rot); } connectorPartAnchor.name = PartAnchorName; connectorPartAnchor.parent = connectorModelObj; partCableAnchor = new GameObject(ConnectorParkAnchorName).transform; Hierarchy.MoveToParent( partCableAnchor, nodeTransform, newPosition: connectorParkPositionOffset); } else { connectorModelObj = nodeTransform.Find(ConnectorModelName); connectorCableAnchor = connectorModelObj.Find(CableAnchorName); connectorPartAnchor = connectorModelObj.Find(PartAnchorName); partCableAnchor = nodeTransform.Find(ConnectorParkAnchorName); } AlignTransforms.SnapAlign(connectorModelObj, connectorCableAnchor, partCableAnchor); }