From 1409a13d5ac910feceb39afdd1fb04ecc550ea22 Mon Sep 17 00:00:00 2001 From: Sofie <63377159+SofieBrink@users.noreply.github.com> Date: Mon, 14 Oct 2024 07:09:53 +0200 Subject: [PATCH] BCS Custom Modules (#30) - Added ModuleDecoupleAtAltitude. - Added ModuleBCSAirbags, a module to automatically deploy airbags if enabled. --- .../BoringCrewServices.ckan | 1 + .../BoringCrewServices/Localization/en-us.cfg | 17 + .../Starliner/BCS_Centauri_CentreBag.cfg | 22 +- .../Starliner/BCS_Centauri_CrewCapsule.cfg | 13 +- .../Starliner/BCS_Centauri_HeatShield.cfg | 5 +- Source/BoringCrewServices.csproj | 13 +- Source/Modules/ModuleBCSAirbags.cs | 322 ++++++++++++++++++ Source/Modules/ModuleDecoupleAtAltitude.cs | 120 +++++++ 8 files changed, 488 insertions(+), 25 deletions(-) create mode 100644 GameData/BoringCrewServices/Localization/en-us.cfg create mode 100644 Source/Modules/ModuleBCSAirbags.cs create mode 100644 Source/Modules/ModuleDecoupleAtAltitude.cs diff --git a/GameData/BoringCrewServices/BoringCrewServices.ckan b/GameData/BoringCrewServices/BoringCrewServices.ckan index d412694..67d0d34 100644 --- a/GameData/BoringCrewServices/BoringCrewServices.ckan +++ b/GameData/BoringCrewServices/BoringCrewServices.ckan @@ -6,6 +6,7 @@ ksp_version_min: '1.12.3' author: - Zorg - DylanSemrau + - SofieBrink depends: - name: ModuleManager - name: B9PartSwitch diff --git a/GameData/BoringCrewServices/Localization/en-us.cfg b/GameData/BoringCrewServices/Localization/en-us.cfg new file mode 100644 index 0000000..3da42a0 --- /dev/null +++ b/GameData/BoringCrewServices/Localization/en-us.cfg @@ -0,0 +1,17 @@ +Localization +{ + en-us + { + // ModuleDecoupleAtAltitude + #BCS_ArmJettison = Arm Auto Jettison + #BCS_DisarmJettison = Disarm Auto Jettison + #BCS_JettisonAltitude = Jettison Altitude + + // ModuleBCSAirbag + #BCS_DeployAirbags = Deploy Airbags + #BCS_DeflateAirbags = Deflate Airbags + #BCS_ArmDeploy = Arm Auto Deploy + #BCS_DisarmDeploy = Disarm Auto Deploy + #BCS_DeployAltitude = Deploy Altitude + } +} diff --git a/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CentreBag.cfg b/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CentreBag.cfg index 4f52674..451eaf2 100644 --- a/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CentreBag.cfg +++ b/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CentreBag.cfg @@ -45,16 +45,14 @@ PART tags = Airbag Cushion Starliner ?BCS Boring - MODULE - { - name = ModuleAnimateGeneric - animationName = deploy - startEventGUIName = Deploy Airbag - endEventGUIName = Retract Airbag - actionGUIName = Toggle Airbag - eventAvailableEditor = true - eventAvailableFlight = true - eventAvailableEVA = true - } - + MODULE + { + name = ModuleBCSAirbags + deployAnimationName = deploy + deflateAnimationEnd = 0.65 + referenceNodeName = bottom + referenceParentNode = true + deployAboveLand = false + autoDeflateOnLand = false + } } diff --git a/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CrewCapsule.cfg b/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CrewCapsule.cfg index 09ecdfb..6177b16 100644 --- a/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CrewCapsule.cfg +++ b/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_CrewCapsule.cfg @@ -142,15 +142,10 @@ PART } MODULE { - name = ModuleAnimateGeneric - animationName = deploy - startEventGUIName = Deploy Airbags - endEventGUIName = Retract Airbags - actionGUIName = Toggle Airbags - restrictedNode = bottom - eventAvailableEditor = true - eventAvailableFlight = true - eventAvailableEVA = true + name = ModuleBCSAirbags + deployAnimationName = deploy + deflateAnimationEnd = 0.40 + referenceNodeName = bottom } MODULE { diff --git a/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_HeatShield.cfg b/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_HeatShield.cfg index c6f30e3..785c6d7 100644 --- a/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_HeatShield.cfg +++ b/GameData/BoringCrewServices/Parts/Starliner/BCS_Centauri_HeatShield.cfg @@ -54,10 +54,11 @@ PART MODULE { - name = ModuleDecouple + name = ModuleDecoupleAtAltitude ejectionForce = 250 explosiveNodeID = top - stagingEnabled = False + stagingEnabled = True + menuName = #autoLOC_502004 //Jettison Heatshield } MODULE { diff --git a/Source/BoringCrewServices.csproj b/Source/BoringCrewServices.csproj index 95f2a53..5fc8885 100644 --- a/Source/BoringCrewServices.csproj +++ b/Source/BoringCrewServices.csproj @@ -31,16 +31,25 @@ 4 + + 1.0.3 + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + - + + - + + \ No newline at end of file diff --git a/Source/Modules/ModuleBCSAirbags.cs b/Source/Modules/ModuleBCSAirbags.cs new file mode 100644 index 0000000..00702bb --- /dev/null +++ b/Source/Modules/ModuleBCSAirbags.cs @@ -0,0 +1,322 @@ +using System.Collections; +using UnityEngine; + +namespace BoringCrewServices.Modules +{ + public class ModuleBCSAirbags : PartModule + { + // Fields + [KSPField] + public string deployAnimationName; + + [KSPField] + public string deflateAnimationName; + + [KSPField] + public bool reverseDeployAnimation = false; + + [KSPField] + public float deployAnimationEnd = 1f; + + [KSPField] + public float deflateAnimationEnd = 0f; + + [KSPField] + public bool reverseDeflateAnimation = true; + + [KSPField] + public bool deployAboveLand = true; + + [KSPField] + public bool deployAboveWater = true; + + [KSPField] + public bool autoDeflateOnLand = true; + + [KSPField] + public bool autoDeflateOnWater = false; + + [KSPField] + public string referenceNodeName; + + [KSPField] + public bool referenceParentNode = false; + + [KSPField(isPersistant = true)] + public AirbagState airbagState = AirbagState.Disarmed; + + private AttachNode referenceNode; + + private Animation deployAnimation; + + private Animation deflateAnimation; + + private string deflateAnimationNameGet => string.IsNullOrEmpty(deflateAnimationName) ? deployAnimationName : deflateAnimationName; + + private Coroutine altitudeCoroutine; + + private Coroutine nodeCoroutine; + + // UI + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#BCS_DeployAltitude", guiUnits = "#autoLOC_289929")] + [UI_FloatRange(stepIncrement = 50f, maxValue = 1500f, minValue = 50f)] + public float deployAltitude = 650f; + + [KSPAction(guiName = "#BCS_ArmDeploy", activeEditor = true)] + public void ArmAction(KSPActionParam param) => Arm(); + + [KSPEvent(active = true, guiActiveUnfocused = true, externalToEVAOnly = true, guiActive = true, unfocusedRange = 4f, guiName = "#BCS_ArmDeploy")] + public void Arm() + { + if (airbagState == AirbagState.Disarmed) + { + airbagState = AirbagState.Armed; + part.stackIcon.SetIconColor(XKCDColors.LightCyan); + Events["Arm"].active = false; + Events["Disarm"].active = true; + if (altitudeCoroutine == null) altitudeCoroutine = StartCoroutine(AltitudeCoroutine()); + } + } + + [KSPAction(guiName = "#BCS_DisarmDeploy", activeEditor = true)] + public void DisarmAction(KSPActionParam param) => Disarm(); + + [KSPEvent(active = false, guiActiveUnfocused = true, externalToEVAOnly = true, guiActive = true, unfocusedRange = 4f, guiName = "#BCS_DisarmDeploy")] + public void Disarm() + { + if (airbagState == AirbagState.Armed) + { + airbagState = AirbagState.Disarmed; + part.stackIcon.SetIconColor(XKCDColors.White); + Events["Arm"].active = true; + Events["Disarm"].active = false; + StopAltitudeCoroutine(); + StopNodeCoroutine(); + } + } + + [KSPAction(guiName = "#BCS_DeployAirbags", activeEditor = true)] + public void DeployAction(KSPActionParam param) => Deploy(); + + [KSPEvent(active = true, guiActiveUnfocused = true, externalToEVAOnly = true, guiActive = true, unfocusedRange = 4f, guiName = "#BCS_DeployAirbags")] + public void Deploy() + { + if (airbagState == AirbagState.Disarmed || airbagState == AirbagState.Armed) + { + airbagState = AirbagState.Deployed; + part.stackIcon.SetIconColor(XKCDColors.RadioactiveGreen); + Events["Arm"].active = false; + Events["Disarm"].active = false; + Events["Deploy"].active = false; + Events["Deflate"].active = true; + + PlayAnimation(deployAnimation, deployAnimationName, reverseDeployAnimation ? 1 : 0, deployAnimationEnd); + } + } + + [KSPAction(guiName = "#BCS_DeflateAirbags", activeEditor = true)] + public void DeflateAction(KSPActionParam param) => Deflate(); + + [KSPEvent(active = false, guiActiveUnfocused = true, externalToEVAOnly = true, guiActive = true, unfocusedRange = 4f, guiName = "#BCS_DeflateAirbags")] + public void Deflate() + { + if (airbagState == AirbagState.Deployed) + { + airbagState = AirbagState.Deflated; + part.stackIcon.SetIconColor(XKCDColors.Red); + Events["Arm"].active = false; + Events["Disarm"].active = false; + Events["Deploy"].active = false; + Events["Deflate"].active = false; + + PlayAnimation(deflateAnimation, deflateAnimationNameGet, reverseDeflateAnimation ? 1 : 0, deflateAnimationEnd); + + } + } + + // KSP Methods + public override void OnStart(StartState state) + { + base.OnStart(state); + SetupPartIcon(); + + GameEvents.onVesselSituationChange.Add(OnStatus); + + if (string.IsNullOrEmpty(deployAnimationName)) Debug.LogError($"[{nameof(ModuleBCSAirbags)}] deployAnimationName was not set!"); + else deployAnimation = part.FindModelAnimator(deployAnimationName); + if (deployAnimation == null) Debug.LogError($"[{nameof(ModuleBCSAirbags)}] Part: {part.partInfo?.name} does not have an animation named: {deployAnimationName}"); + + if (string.IsNullOrEmpty(deflateAnimationName)) + { + #if DEBUG + Debug.Log($"[{nameof(ModuleBCSAirbags)}] deflateAnimationName was not set, using deploy animation"); + #endif + deflateAnimation = deployAnimation; + } + else deflateAnimation = part.FindModelAnimator(deflateAnimationName); + // this will print something empty in the case that its supposed to use the deploy animation and that doesn't exist, but there will already be a correct error above. + if (deflateAnimation == null) Debug.LogError($"[{nameof(ModuleBCSAirbags)}] Part: {part.partInfo?.name} does not have an animation named: {deflateAnimationName}"); + + // possibly needs to be a KSPField if there's ever multiple anims playing simultaneously? + deployAnimation[deployAnimationName].layer = 0; + deflateAnimation[deflateAnimationNameGet].layer = 0; + + deployAnimation[deployAnimationName].speed = 0; + deflateAnimation[deflateAnimationNameGet].speed = 0; + + if (airbagState == AirbagState.Deployed) + { + deployAnimation[deployAnimationName].normalizedTime = deployAnimationEnd; + } + else if (airbagState == AirbagState.Deflated) { + deflateAnimation[deflateAnimationNameGet].normalizedTime = deflateAnimationEnd; + } + } + + public override void OnActive() + { + base.OnActive(); + Arm(); + } + + public void OnDestroy() + { + StopAltitudeCoroutine(); + StopNodeCoroutine(); + GameEvents.onVesselSituationChange.Remove(OnStatus); + referenceNode = null; + } + + public void OnStatus(GameEvents.HostedFromToAction data) + { + if (airbagState == AirbagState.Deployed && vessel == data.host) + { + if ((data.to == Vessel.Situations.LANDED && autoDeflateOnLand) || (data.to == Vessel.Situations.SPLASHED && autoDeflateOnWater)) Deflate(); + } + } + + // Module Methods + private void SetupPartIcon() + { + part.stagingIcon = "CUSTOM"; + part.stackIcon.iconType = DefaultIcons.CUSTOM; + part.stackIcon.customIconFilename = "BoringCrewServices/Icons/BCS_AirbagIcon"; + switch (airbagState) + { + case AirbagState.Deployed: + part.stackIcon.SetIconColor(XKCDColors.RadioactiveGreen); + break; + case AirbagState.Deflated: + part.stackIcon.SetIconColor(XKCDColors.Red); + break; + case AirbagState.Armed: + part.stackIcon.SetIconColor(XKCDColors.LightCyan); + if (altitudeCoroutine == null) altitudeCoroutine = StartCoroutine(AltitudeCoroutine()); + break; + default: + part.stackIcon.SetIconColor(XKCDColors.White); + break; + } + } + + private void StopAltitudeCoroutine() + { + if (altitudeCoroutine != null) + { + StopCoroutine(altitudeCoroutine); + altitudeCoroutine = null; + } + } + private void StopNodeCoroutine() + { + if (nodeCoroutine != null) + { + StopCoroutine(nodeCoroutine); + nodeCoroutine = null; + } + } + + private bool ShouldDeploy() + { + var altitude = FlightGlobals.getAltitudeAtPos(base.part.transform.position, base.vessel.mainBody); + return altitude < deployAltitude || Physics.Raycast(base.part.transform.position, -base.vessel.upAxis, deployAltitude, 32768, QueryTriggerInteraction.Ignore); + } + + private bool NodeDetached() + { + if (string.IsNullOrEmpty(referenceNodeName)) return true; + + var refPart = referenceParentNode ? part.parent : part; + if (refPart == null) + { + Debug.LogError($"[{nameof(ModuleBCSAirbags)}] Part: {part.partInfo?.name} does not have a parent part!"); + StopNodeCoroutine(); + return false; + } + if (referenceNode == null || referenceNode.id != referenceNodeName) + { + referenceNode = refPart.FindAttachNode(referenceNodeName); + if (referenceNode == null) + { + Debug.LogError($"[{nameof(ModuleBCSAirbags)}] Part: {refPart.partInfo?.name} does not have a node with id: {referenceNodeName}"); + StopNodeCoroutine(); + return false; + } + } + + return referenceNode.attachedPart == null; + } + + private void PlayAnimation(Animation anim, string animationName, float start, float end) + { + float direction = start > end ? -1f : 1f; + + anim[animationName].normalizedTime = start; + anim[animationName].speed = direction; + + anim.Play(animationName); + StartCoroutine(AnimationCoroutine(anim, animationName, end)); + } + + // Coroutines + public IEnumerator AltitudeCoroutine() + { + yield return new WaitUntil(ShouldDeploy); + part.stackIcon.SetIconColor(XKCDColors.Yellow); + if (nodeCoroutine == null) nodeCoroutine = StartCoroutine(NodeCoroutine()); + } + + public IEnumerator NodeCoroutine() + { + StopAltitudeCoroutine(); + yield return new WaitUntil(NodeDetached); + + bool isAboveWater = vessel.terrainAltitude <= 0; + if ((isAboveWater && !deployAboveWater) || (!isAboveWater && !deployAboveLand)) Disarm(); + else Deploy(); + } + + public IEnumerator AnimationCoroutine(Animation anim, string animationName, float end) + { + var isForward = anim[animationName].speed > 0f; + + while (((isForward && anim[animationName].normalizedTime < end) || (!isForward && anim[animationName].normalizedTime > end)) && anim.isPlaying) + { + yield return null; + } + + anim.Stop(animationName); + #if DEBUG + Debug.Log($"[{nameof(ModuleBCSAirbags)}] Animation Coroutine finished"); + #endif + } + } + + public enum AirbagState + { + Disarmed + , Armed + , Deployed + , Deflated + } +} diff --git a/Source/Modules/ModuleDecoupleAtAltitude.cs b/Source/Modules/ModuleDecoupleAtAltitude.cs new file mode 100644 index 0000000..e23eaff --- /dev/null +++ b/Source/Modules/ModuleDecoupleAtAltitude.cs @@ -0,0 +1,120 @@ +using System.Collections; +using UnityEngine; + +namespace BoringCrewServices.Modules +{ + public class ModuleDecoupleAtAltitude : ModuleDecouple + { + [KSPField(isPersistant = true, guiActive = true, guiActiveEditor = true, guiName = "#BCS_JettisonAltitude", guiUnits = "#autoLOC_289929")] + [UI_FloatRange(stepIncrement = 50f, maxValue = 1500f, minValue = 50f)] + public float jettisonAltitude = 650f; + + [KSPAction(guiName = "#BCS_DisarmJettison", activeEditor = true)] + public void DisarmAction(KSPActionParam param) => Disarm(); + + [KSPEvent(active = false, guiActiveUnfocused = true, externalToEVAOnly = true, guiActive = true, unfocusedRange = 4f, guiName = "#BCS_DisarmJettison")] + public void Disarm() + { + if (heatshieldState == HeatshieldState.Armed) + { + heatshieldState = HeatshieldState.Disarmed; + part.stackIcon.SetIconColor(XKCDColors.White); + ToggleEvents(false); + StopAltitudeCoroutine(); + } + } + + [KSPAction(guiName = "#BCS_ArmJettison", activeEditor = true)] + public void ArmAction(KSPActionParam param) => Arm(); + + [KSPEvent(active = true, guiActiveUnfocused = true, externalToEVAOnly = true, guiActive = true, unfocusedRange = 4f, guiName = "#BCS_ArmJettison")] + public void Arm() => OnActive(); + + public new void Decouple() + { + base.OnActive(); + heatshieldState = HeatshieldState.Deployed; + ToggleEvents(false); + Fields["jettisonAltitude"].guiActive = false; + StopAltitudeCoroutine(); + } + + [KSPField(isPersistant = true)] + public HeatshieldState heatshieldState = HeatshieldState.Disarmed; + + private Coroutine altitudeCoroutine; + + public override void OnStart(StartState state) + { + base.OnStart(state); + SetupPartIcon(); + } + + public void OnDestroy() + { + StopAltitudeCoroutine(); + } + + public override void OnActive() + { + if (heatshieldState == HeatshieldState.Disarmed) + { + heatshieldState = HeatshieldState.Armed; + part.stackIcon.SetIconColor(XKCDColors.LightCyan); + ToggleEvents(true); + if (altitudeCoroutine == null) altitudeCoroutine = StartCoroutine(AltitudeDecouple()); + } + } + + private void ToggleEvents(bool armedState) + { + Events["Disarm"].active = armedState; + Events["Arm"].active = !armedState; + } + + private void SetupPartIcon() + { + part.stagingIcon = "CUSTOM"; + part.stackIcon.iconType = DefaultIcons.CUSTOM; + part.stackIcon.customIconFilename = "BoringCrewServices/Icons/BCS_JettisionIcon"; + switch (heatshieldState) + { + case HeatshieldState.Armed: + part.stackIcon.SetIconColor(XKCDColors.LightCyan); + if (altitudeCoroutine == null) altitudeCoroutine = StartCoroutine(AltitudeDecouple()); + break; + default: + part.stackIcon.SetIconColor(XKCDColors.White); + break; + } + } + + private void StopAltitudeCoroutine() + { + if (altitudeCoroutine != null) + { + StopCoroutine(altitudeCoroutine); + altitudeCoroutine = null; + } + } + + protected bool ShouldJetison() + { + var altitude = FlightGlobals.getAltitudeAtPos(base.part.transform.position, base.vessel.mainBody); + return altitude < jettisonAltitude || Physics.Raycast(base.part.transform.position, -base.vessel.upAxis, jettisonAltitude, 32768, QueryTriggerInteraction.Ignore); + } + + public IEnumerator AltitudeDecouple() + { + yield return new WaitUntil(ShouldJetison); + Decouple(); + } + } + + public enum HeatshieldState + { + Disarmed + , Armed + , Deployed + } +}