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
+ }
+}