diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b96b5c1..da578c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,8 @@ name: Upload release +env: + SPACEDOCK_MOD_ID: 3534 + on: release: types: [ "published" ] @@ -11,18 +14,29 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + with: + lfs: true + + - name: Download NuGet + id: download-nuget + run: sudo curl -o /usr/local/bin/nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - name: Install jq uses: dcarbone/install-jq-action@v2.1.0 - name: Build the solution + run: dotnet build "KerbalLifeHacks.sln" -c Release + + - name: Extract current version + id: get-version run: | version=$(jq -r '.version' plugin_template/swinfo.json) echo "Version is $version" - dotnet build "KerbalLifeHacks.sln" -c Release + echo "version=$version" >> $GITHUB_ENV echo "release_filename=KerbalLifeHacks-$version.zip" >> $GITHUB_ENV echo "zip=$(ls -1 dist/KerbalLifeHacks-*.zip | head -n 1)" >> $GITHUB_ENV echo "upload_url=$(wget -qO- https://api.github.com/repos/$GITHUB_REPOSITORY/releases | jq '.[0].upload_url' | tr -d \")" >> $GITHUB_ENV + wget -qO- https://api.github.com/repos/$GITHUB_REPOSITORY/releases | jq -r '.[0].body' > ./changelog.md - name: Upload zip to release uses: shogo82148/actions-upload-release-asset@v1.7.2 @@ -34,3 +48,16 @@ jobs: asset_name: ${{ env.release_filename }} asset_content_type: application/zip + - name: Add Mask + run: echo "::add-mask::${{ secrets.SPACEDOCK_PASSWORD }}" + + - name: Update mod on SpaceDock + uses: KSP2Community/spacedock-upload@v1.0.0 + with: + username: ${{ secrets.SPACEDOCK_USER }} + password: ${{ secrets.SPACEDOCK_PASSWORD }} + game_id: 22407 + mod_id: ${{ env.SPACEDOCK_MOD_ID }} + version: ${{ env.version }} + zipball: ${{ env.zip }} + changelog: ./changelog.md \ No newline at end of file diff --git a/LICENSE b/LICENSE index 09d05da..5a5063c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 KSP2 Community +Copyright (c) 2023-2024 KSP2 Community Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,5 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index a07488f..68ba758 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,36 @@ changes, or _life hacks_, that don't fit into the scope of Community Fixes. - Requires **[SpaceWarp 1.7.0+](https://github.com/SpaceWarpDev/SpaceWarp/releases/)** ## Features Currently, this mod includes the following life hacks: -- **VAB Mission Tracker** by [munix](https://github.com/jan-bures) - Adds the Mission Tracker button to the VAB app tray. +### UI tweaks +#### Flight view +- **IVA Portraits Toggler** by [WiS3](https://github.com/WiS3) - Adds a button to the app bar to toggle the Kerbal + portraits panel. +#### Map +- **Warp To Orbital Points** by [munix](https://github.com/jan-bures) - Adds buttons to warp to apoapsis, periapsis and + SOI change. +- **Orbital Line Colors** by [munix](https://github.com/jan-bures) - Colors the various orbital lines of the current + vessel to make them easier to distinguish. +#### VAB +- **VAB Mission Tracker** by [munix](https://github.com/jan-bures) - Adds the Mission Tracker button to the VAB app + tray. +### Gameplay - **Disable Contrails** by [munix](https://github.com/jan-bures) - Disables contrails and wingtip vortices. Disabled by default, see the [Configuration](#Configuration) section for details. - **Better Experiments** by [dmarcuse](https://github.com/dmarcuse) - Automatically resumes paused experiments when re-entering the correct region, and ignores animation state. + ## Installation ### Recommended 1. Use [CKAN](https://github.com/KSP-CKAN/CKAN/releases/latest) to download and install Kerbal Life Hacks. ### Manual -1. Download and extract [UITK for KSP 2](https://github.com/UitkForKsp2/UitkForKsp2/releases) into your game folder (this is a dependency of SpaceWarp). +1. Download and extract [UITK for KSP 2](https://github.com/UitkForKsp2/UitkForKsp2/releases) into your game folder + (this is a dependency of SpaceWarp). 2. Download and extract [SpaceWarp](https://github.com/SpaceWarpDev/SpaceWarp/releases) into your game folder. -3. Download and extract this mod into the game folder. If done correctly, you should have the following folder structure: `/BepInEx/plugins/KerbalLifeHacks`. +3. Download and extract this mod into the game folder. If done correctly, you should have the following folder + structure: `/BepInEx/plugins/KerbalLifeHacks`. ## Configuration All life hacks can be toggled on or off in `Main menu` -> `Settings` -> `Mods` -> `Kerbal Life Hacks`. The changes will -take effect after the game is restarted. Life hacks are enabled by default unless otherwise noted. \ No newline at end of file +take effect after the game is restarted. Life hacks are enabled by default unless otherwise noted. +## Contributing +If you'd like to contribute to this project, please take a look at +[our wiki](https://github.com/KSP2Community/KerbalLifeHacks/wiki/Adding-your-hack). \ No newline at end of file diff --git a/plugin_template/assets/images/IVAPortraitsToggler-icon.png b/plugin_template/assets/images/IVAPortraitsToggler-icon.png new file mode 100644 index 0000000..dbb1ce5 Binary files /dev/null and b/plugin_template/assets/images/IVAPortraitsToggler-icon.png differ diff --git a/plugin_template/localizations/warp_to_orbital_point.csv b/plugin_template/localizations/warp_to_orbital_point.csv new file mode 100644 index 0000000..4613188 --- /dev/null +++ b/plugin_template/localizations/warp_to_orbital_point.csv @@ -0,0 +1,4 @@ +Key,Type,Desc,English +KerbalLifeHacks/Map/WarpToAp,text,,Warp to Apoapsis +KerbalLifeHacks/Map/WarpToPe,text,,Warp to Periapsis +KerbalLifeHacks/Map/WarpToSOI,text,,Warp to SOI Change \ No newline at end of file diff --git a/plugin_template/swinfo.json b/plugin_template/swinfo.json index cb95414..0bc15df 100644 --- a/plugin_template/swinfo.json +++ b/plugin_template/swinfo.json @@ -5,7 +5,7 @@ "name": "Kerbal Life Hacks", "description": "A counterpart to Community Fixes which contains quality of life improvements and tuning tweaks.", "source": "https://github.com/KSP2Community/KerbalLifeHacks", - "version": "1.1.0", + "version": "1.2.0", "version_check": "https://raw.githubusercontent.com/KSP2Community/KerbalLifeHacks/main/plugin_template/swinfo.json", "ksp2_version": { "min": "0.2.0", @@ -20,4 +20,4 @@ } } ] -} +} \ No newline at end of file diff --git a/src/KerbalLifeHacks/Hacks/IVAPortraitsToggler/AppBarButton.cs b/src/KerbalLifeHacks/Hacks/IVAPortraitsToggler/AppBarButton.cs new file mode 100644 index 0000000..7a80014 --- /dev/null +++ b/src/KerbalLifeHacks/Hacks/IVAPortraitsToggler/AppBarButton.cs @@ -0,0 +1,75 @@ +using KSP.Api.CoreTypes; +using KSP.UI; +using KSP.UI.Binding; +using SpaceWarp.API.UI.Appbar; +using UnityEngine; +using UnityEngine.UI; +using UnityObject = UnityEngine.Object; + +namespace KerbalLifeHacks.Hacks.IVAPortraitsToggler; + +internal class AppBarButton +{ + private readonly UIValue_WriteBool_Toggle _buttonState; + + public AppBarButton( + string buttonTooltip, + string buttonId, + Texture2D buttonIcon, + Action function, + int siblingIndex = -1 + ) + { + // Find 'ButtonBar' game object + var list = GameObject.Find( + "GameManager/Default Game Instance(Clone)/UI Manager(Clone)/Scaled Popup Canvas/Container/ButtonBar" + ); + + // Get 'NonStageable-Resources' button + var nonStageableResources = list != null ? list.GetChild("BTN-NonStageable-Resources") : null; + + if (list == null || nonStageableResources == null) + { + return; + } + + // Clone 'NonStageable-Resources' button + + var barButton = UnityObject.Instantiate(nonStageableResources, list.transform); + if (siblingIndex >= 0 && siblingIndex < list.transform.childCount - 1) + { + barButton.transform.SetSiblingIndex(siblingIndex); + } + + barButton.name = buttonId; + + // Change the tooltip + barButton.GetComponent()._tooltipTitleKey = buttonTooltip; + + // Change the icon + var sprite = Appbar.GetAppBarIconFromTexture(buttonIcon); + var icon = barButton.GetChild("Content").GetChild("GRP-icon"); + var image = icon.GetChild("ICO-asset").GetComponent(); + image.sprite = sprite; + + // Add our function call to the toggle + var toggle = barButton.GetComponent(); + toggle.onValueChanged.AddListener(state => function(state)); + toggle.onValueChanged.AddListener(SetButtonState); + + // Set the initial state of the button + _buttonState = barButton.GetComponent(); + _buttonState.valueKey = $"Is{buttonId}Visible"; + _buttonState.BindValue(new Property(false)); + } + + public void SetButtonState(bool state) + { + if (_buttonState == null) + { + return; + } + + _buttonState.SetValue(state); + } +} \ No newline at end of file diff --git a/src/KerbalLifeHacks/Hacks/IVAPortraitsToggler/IVAPortraitsToggler.cs b/src/KerbalLifeHacks/Hacks/IVAPortraitsToggler/IVAPortraitsToggler.cs new file mode 100644 index 0000000..533dc15 --- /dev/null +++ b/src/KerbalLifeHacks/Hacks/IVAPortraitsToggler/IVAPortraitsToggler.cs @@ -0,0 +1,69 @@ +using KSP.Game; +using KSP.Messages; +using SpaceWarp.API.Assets; +using UnityEngine; + +namespace KerbalLifeHacks.Hacks.IVAPortraitsToggler; + +[Hack("IVA Portraits Toggler")] +public class IVAPortraitsToggler : BaseHack +{ + private AppBarButton _buttonBar; + + // ReSharper disable once InconsistentNaming, IdentifierTypo + private Canvas _ivaportraits_canvas; + + public override void OnInitialized() + { + Messages.PersistentSubscribe(OnFlightViewEnteredMessage); + Messages.PersistentSubscribe(OnVesselChangedMessage); + } + + private void OnFlightViewEnteredMessage(MessageCenterMessage msg) + { + if (_ivaportraits_canvas == null) + { + var instruments = GameManager.Instance.Game.UI.FlightHud._instruments; + // ReSharper disable once StringLiteralTypo + instruments.TryGetValue("group_ivaportraits", out var ivaPortraits); + if (ivaPortraits != null) + { + _ivaportraits_canvas = ivaPortraits._parentCanvas; + } + } + + if (_buttonBar != null) + { + return; + } + + _buttonBar = new AppBarButton( + "IVA Portraits", + "BTN-IVA-Portraits", + AssetManager.GetAsset($"KerbalLifeHacks/images/IVAPortraitsToggler-icon.png"), + ToggleIVAPortraitsCanvas, + 0 + ); + } + + private void OnVesselChangedMessage(MessageCenterMessage msg) + { + if (msg is not VesselChangedMessage { Vessel: { } vessel }) + { + return; + } + + var vesselGuid = vessel.GetControlOwner().GlobalId; + var allKerbalsInSimObject = GameManager.Instance.Game.KerbalManager._kerbalRosterManager + ?.GetAllKerbalsInSimObject(vesselGuid); + var state = allKerbalsInSimObject?.Count > 0; + + ToggleIVAPortraitsCanvas(state); + _buttonBar.SetButtonState(state); + } + + public void ToggleIVAPortraitsCanvas(bool state) + { + _ivaportraits_canvas.enabled = state; + } +} \ No newline at end of file diff --git a/src/KerbalLifeHacks/Hacks/OrbitalLineColors/OrbitalLineColors.cs b/src/KerbalLifeHacks/Hacks/OrbitalLineColors/OrbitalLineColors.cs new file mode 100644 index 0000000..8594d64 --- /dev/null +++ b/src/KerbalLifeHacks/Hacks/OrbitalLineColors/OrbitalLineColors.cs @@ -0,0 +1,603 @@ +using HarmonyLib; +using KSP.Map; +using KSP.Sim; +using SpaceWarp.API.Game; +using UnityEngine; + +namespace KerbalLifeHacks.Hacks.OrbitalLineColors; + +[Hack("Use various colors for orbital lines")] +public class OrbitalLineColors : BaseHack +{ + private static readonly List TrajectoryColors = + [ + new Color(0, 0.5f, 1), // Light Blue + new Color(0.5f, 1, 1), // Light Cyan + new Color(0.5f, 0.5f, 1), // Light Blue + new Color(0.5f, 1, 0.5f), // Light Green + new Color(0.75f, 1, 0), // Lime Green + new Color(0.25f, 1, 0.75f), // Aqua + new Color(0, 0.75f, 1), // Sky Blue + new Color(0, 0, 1), // Blue + ]; + + private static readonly List ManeuverColors = + [ + new Color(1, 0, 0), // Red - first patch is always the burn, so this is not used + new Color(1, 0.5f, 0), // Orange + new Color(1, 0.5f, 0.5f), // Light Red + new Color(1, 0.75f, 0), // Light Orange + new Color(1, 0.5f, 1), // Light Magenta + new Color(1, 0, 0.5f), // Pink + new Color(1, 0.5f, 0.75f), // Light Pink + new Color(1, 1, 0.5f), // Light Yellow + new Color(1, 0, 1), // Magenta + ]; + + public override void OnInitialized() + { + HarmonyInstance.PatchAll(typeof(OrbitalLineColors)); + } + + [HarmonyPatch(typeof(OrbitRenderer), nameof(OrbitRenderer.UpdateOrbitStyling))] + [HarmonyPrefix] + // ReSharper disable once InconsistentNaming + private static bool UpdateOrbitStylingPrefix(ref OrbitRenderer __instance) + { + foreach (var value in __instance._orbitRenderData.Values) + { + var vessel = Vehicle.ActiveSimVessel; + var isTarget = vessel?.TargetObjectId == value.Orbiter.SimulationObject.GlobalId; + var isActiveVessel = vessel?.SimulationObject.GlobalId == value.Orbiter.SimulationObject.GlobalId; + + if (value.Segments == null) + { + continue; + } + + if (value.IsCelestialBody) + { + UpdateCelestialBodyStyling(value, isTarget); + } + else if (value.IsManeuver) + { + UpdateManeuverStyling(value, isTarget); + } + else + { + UpdateTrajectoryStyling(value, isTarget, isActiveVessel); + } + } + + return false; + } + + #region Celestial body orbit colors + + private static void UpdateCelestialBodyStyling(OrbitRenderer.OrbitRenderData data, bool isTarget) + { + foreach (var segment in data.Segments) + { + var startOrbitColor = isTarget ? MapMagicValues.TargetOrbitStartColor : data.DefaultOrbitColor; + var endOrbitColor = isTarget + ? MapMagicValues.TargetOrbitEndColor + : data.DefaultOrbitColor * MapMagicValues.CelestialBodyOrbitEndColorBrightness; + segment.SetColors(startOrbitColor, endOrbitColor); + segment.OrbitRenderStyle = MapMagicValues.CelestialBodyRenderStyle; + segment.DashStyling.size = MapMagicValues.CelestialBodyOrbitDashLength; + segment.DashStyling.spacing = MapMagicValues.CelestialBodyOrbitDashGap; + } + + data.OrbitThickness = MapMagicValues.CelestialBodyOrbitThickness; + data.IsClosedLoop = data.Orbiter.PatchedConicsOrbit.eccentricity < 1.0; + } + + #endregion + + #region Maneuver orbit colors + + private static void UpdateManeuverStyling(OrbitRenderer.OrbitRenderData data, bool isTarget) + { + var (startUT, endUT) = GetTrajectoryUT(data.Orbiter.ManeuverPlanSolver.ManeuverTrajectory); + + for (var index = 0; index < data.Orbiter.ManeuverPlanSolver.ManeuverTrajectory.Count; index++) + { + var patchedOrbit = data.Orbiter.ManeuverPlanSolver.ManeuverTrajectory[index]; + var segment = data.Segments[index]; + + if (segment.IsHighlighted) + { + SetManeuverHighlightedColors(data, index, startUT, endUT); + } + else if (patchedOrbit.PatchStartTransition == PatchTransitionType.Maneuver || + patchedOrbit.PatchEndTransition == PatchTransitionType.EndThrust || + patchedOrbit.PatchEndTransition == PatchTransitionType.PartialOutOfFuel || + patchedOrbit.PatchEndTransition == PatchTransitionType.CompletelyOutOfFuel) + { + SetManeuverBurnColors(data, index, startUT, endUT, isTarget); + } + else + { + SetManeuverGeneralColors(data, index, startUT, endUT, isTarget); + } + + segment.NeedRenderConnector = patchedOrbit.PatchEndTransition == PatchTransitionType.Escape; + if (!segment.NeedRenderConnector) + { + continue; + } + + segment.ConnectorDashStyling.size = MapMagicValues.TrajectoryConnectorDashLength; + segment.ConnectorDashStyling.spacing = MapMagicValues.TrajectoryConnectorDashGap; + } + + data.OrbitThickness = MapMagicValues.ManeuverOrbitThickness; + data.IsClosedLoop = false; + } + + private static void SetManeuverHighlightedColors( + OrbitRenderer.OrbitRenderData data, + int index, + double startUT, + double endUT + ) + { + Color startOrbitColor; + Color endOrbitColor; + + var (initialStartColor, initialEndColor) = GetColorsForSegmentIndex(index, SegmentType.Maneuver, true); + + var time = endUT - startUT; + var patchedOrbit = data.Orbiter.ManeuverPlanSolver.ManeuverTrajectory[index]; + var segment = data.Segments[index]; + + if (!MapMagicValues.PerPatchGradients) + { + startOrbitColor = Color.Lerp( + initialStartColor, + initialEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp( + initialStartColor, + initialEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + else + { + startOrbitColor = initialStartColor; + endOrbitColor = initialEndColor; + } + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + private static void SetManeuverBurnColors( + OrbitRenderer.OrbitRenderData data, + int index, + double startUT, + double endUT, + bool isTarget + ) + { + Color startOrbitColor; + Color endOrbitColor; + + var time = endUT - startUT; + var patchedOrbit = data.Orbiter.ManeuverPlanSolver.ManeuverTrajectory[index]; + var segment = data.Segments[index]; + + if (isTarget) + { + if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.TargetOrbitStartColor; + endOrbitColor = MapMagicValues.TargetOrbitEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.TargetOrbitStartColor, + MapMagicValues.TargetOrbitEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp( + MapMagicValues.TargetOrbitStartColor, + MapMagicValues.TargetOrbitEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + } + else if (data.Orbiter.IsLocallyOwned) + { + if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.ManeuverNonImpulseStartColor; + endOrbitColor = MapMagicValues.ManeuverNonImpulseEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.ManeuverNonImpulseStartColor, + MapMagicValues.ManeuverNonImpulseEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp( + MapMagicValues.ManeuverNonImpulseStartColor, + MapMagicValues.ManeuverNonImpulseEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + } + else if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.NonLocallyOwnedOrbitStartColor; + endOrbitColor = MapMagicValues.NonLocallyOwnedOrbitEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.NonLocallyOwnedOrbitStartColor, + MapMagicValues.NonLocallyOwnedOrbitEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp( + MapMagicValues.NonLocallyOwnedOrbitStartColor, + MapMagicValues.NonLocallyOwnedOrbitEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + + segment.OrbitRenderStyle = MapMagicValues.ManeuverNonImpulseRenderStyle; + segment.DashStyling.size = MapMagicValues.ManeuverNonImpulseDashLength; + segment.DashStyling.spacing = MapMagicValues.ManeuverNonImpulseDashGap; + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + private static void SetManeuverGeneralColors( + OrbitRenderer.OrbitRenderData data, + int index, + double startUT, + double endUT, + bool isTarget + ) + { + Color startOrbitColor; + Color endOrbitColor; + + var time = endUT - startUT; + var patchedOrbit = data.Orbiter.ManeuverPlanSolver.ManeuverTrajectory[index]; + var segment = data.Segments[index]; + + if (isTarget) + { + if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.TargetOrbitStartColor; + endOrbitColor = MapMagicValues.TargetOrbitEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.TargetOrbitStartColor, + MapMagicValues.TargetOrbitEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp(MapMagicValues.TargetOrbitStartColor, + MapMagicValues.TargetOrbitEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + } + else if (data.Orbiter.IsLocallyOwned) + { + var (initialStartColor, initialEndColor) = GetColorsForSegmentIndex(index, SegmentType.Maneuver); + + if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = initialStartColor; + endOrbitColor = initialEndColor; + } + else + { + startOrbitColor = Color.Lerp( + initialStartColor, + initialEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp( + initialStartColor, + initialEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + } + else if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.NonLocallyOwnedOrbitStartColor; + endOrbitColor = MapMagicValues.NonLocallyOwnedOrbitEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.NonLocallyOwnedOrbitStartColor, + MapMagicValues.NonLocallyOwnedOrbitEndColor, + (float)((patchedOrbit.StartUT - startUT) / time) + ); + endOrbitColor = Color.Lerp( + MapMagicValues.NonLocallyOwnedOrbitStartColor, + MapMagicValues.NonLocallyOwnedOrbitEndColor, + (float)((patchedOrbit.EndUT - startUT) / time) + ); + } + + segment.OrbitRenderStyle = MapMagicValues.ManeuverOrbitRenderStyle; + segment.DashStyling.size = MapMagicValues.ManeuverOrbitDashLength; + segment.DashStyling.spacing = MapMagicValues.ManeuverOrbitDashGap; + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + #endregion + + #region Trajectory orbit colors + + private static void UpdateTrajectoryStyling(OrbitRenderer.OrbitRenderData data, bool isTarget, bool isActiveVessel) + { + var (startUt, endUt) = GetTrajectoryUT(data.Orbiter.PatchedConicSolver.CurrentTrajectory); + + for (var index = 0; index < data.Orbiter.PatchedConicSolver.CurrentTrajectory.Count; index++) + { + var patchedConicsOrbit = data.Orbiter.PatchedConicSolver.CurrentTrajectory[index]; + var segment = data.Segments[index]; + + if (segment.IsHighlighted) + { + SetVesselHighlightedColors(data, index, startUt, endUt, isActiveVessel); + } + else if (isTarget) + { + SetVesselTargetColors(data, index, startUt, endUt); + } + else if (data.Orbiter.IsLocallyOwned) + { + SetVesselLocalPlayerColors(data, index, startUt, endUt, isActiveVessel); + } + else if (MapMagicValues.PerPatchGradients) + { + var startOrbitColor = MapMagicValues.NonLocallyOwnedOrbitStartColor; + var endOrbitColor = MapMagicValues.NonLocallyOwnedOrbitEndColor; + segment.SetColors(startOrbitColor, endOrbitColor); + } + else + { + var time = endUt - startUt; + + var startOrbitColor = Color.Lerp( + MapMagicValues.NonLocallyOwnedOrbitStartColor, + MapMagicValues.NonLocallyOwnedOrbitEndColor, + (float)((patchedConicsOrbit.StartUT - startUt) / time) + ); + var endOrbitColor = Color.Lerp( + MapMagicValues.NonLocallyOwnedOrbitStartColor, + MapMagicValues.NonLocallyOwnedOrbitEndColor, + (float)((patchedConicsOrbit.EndUT - startUt) / time) + ); + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + segment.OrbitRenderStyle = MapMagicValues.TrajectoryOrbitRenderStyle; + segment.DashStyling.size = MapMagicValues.TrajectoryOrbitDashLength; + segment.DashStyling.spacing = MapMagicValues.TrajectoryOrbitDashGap; + segment.NeedRenderConnector = patchedConicsOrbit.PatchEndTransition == PatchTransitionType.Escape; + + if (segment.NeedRenderConnector) + { + segment.ConnectorDashStyling.size = MapMagicValues.TrajectoryConnectorDashLength; + segment.ConnectorDashStyling.spacing = MapMagicValues.TrajectoryConnectorDashGap; + } + } + + data.OrbitThickness = MapMagicValues.TrajectoryOrbitThickness; + data.IsClosedLoop = data.Orbiter.PatchedConicsOrbit.eccentricity < 1.0; + } + + private static void SetVesselHighlightedColors( + OrbitRenderer.OrbitRenderData data, + int index, + double startUt, + double endUt, + bool isActiveVessel + ) + { + Color startOrbitColor; + Color endOrbitColor; + + var initialStartColor = MapMagicValues.HighlightedOrbitStartColor; + var initialEndColor = MapMagicValues.HighlightedOrbitEndColor; + + var time = endUt - startUt; + var patchedConicsOrbit = data.Orbiter.PatchedConicSolver.CurrentTrajectory[index]; + var segment = data.Segments[index]; + + if (data.Orbiter.IsLocallyOwned && isActiveVessel) + { + (initialStartColor, initialEndColor) = GetColorsForSegmentIndex(index, SegmentType.Trajectory, true); + } + + if (!MapMagicValues.PerPatchGradients) + { + startOrbitColor = Color.Lerp( + initialStartColor, + initialEndColor, + (float)((patchedConicsOrbit.StartUT - startUt) / time) + ); + endOrbitColor = Color.Lerp( + initialStartColor, + initialEndColor, + (float)((patchedConicsOrbit.EndUT - startUt) / time) + ); + } + else + { + startOrbitColor = initialStartColor; + endOrbitColor = initialEndColor; + } + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + private static void SetVesselTargetColors( + OrbitRenderer.OrbitRenderData data, + int index, + double startUt, + double endUt + ) + { + Color startOrbitColor; + Color endOrbitColor; + + var time = endUt - startUt; + var patchedConicsOrbit = data.Orbiter.PatchedConicSolver.CurrentTrajectory[index]; + var segment = data.Segments[index]; + + if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.TargetOrbitStartColor; + endOrbitColor = MapMagicValues.TargetOrbitEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.TargetOrbitStartColor, + MapMagicValues.TargetOrbitEndColor, + (float)((patchedConicsOrbit.StartUT - startUt) / time) + ); + endOrbitColor = Color.Lerp( + MapMagicValues.TargetOrbitStartColor, + MapMagicValues.TargetOrbitEndColor, + (float)((patchedConicsOrbit.EndUT - startUt) / time) + ); + } + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + private static void SetVesselLocalPlayerColors(OrbitRenderer.OrbitRenderData data, + int index, + double startUt, + double endUt, + bool isActiveVessel) + { + Color startOrbitColor; + Color endOrbitColor; + + var time = endUt - startUt; + var patchedConicsOrbit = data.Orbiter.PatchedConicSolver.CurrentTrajectory[index]; + var segment = data.Segments[index]; + + if (isActiveVessel) + { + var (startColor, endColor) = GetColorsForSegmentIndex(index, SegmentType.Trajectory); + + if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = startColor; + endOrbitColor = endColor; + } + else + { + startOrbitColor = Color.Lerp( + startColor, + endColor, + (float)((patchedConicsOrbit.StartUT - startUt) / time) + ); + endOrbitColor = Color.Lerp( + startColor, + endColor, + (float)((patchedConicsOrbit.EndUT - startUt) / time) + ); + } + } + else if (MapMagicValues.PerPatchGradients) + { + startOrbitColor = MapMagicValues.NonActiveVesselOrbitStartColor; + endOrbitColor = MapMagicValues.NonActiveVesselOrbitEndColor; + } + else + { + startOrbitColor = Color.Lerp( + MapMagicValues.NonActiveVesselOrbitStartColor, + MapMagicValues.NonActiveVesselOrbitEndColor, + (float)((patchedConicsOrbit.StartUT - startUt) / time) + ); + endOrbitColor = Color.Lerp( + MapMagicValues.NonActiveVesselOrbitStartColor, + MapMagicValues.NonActiveVesselOrbitEndColor, + (float)((patchedConicsOrbit.EndUT - startUt) / time) + ); + } + + segment.SetColors(startOrbitColor, endOrbitColor); + } + + #endregion + + #region Helper methods + + private static (double startUT, double endUT) GetTrajectoryUT(List trajectory) where T : IPatchedOrbit + { + var startUT = trajectory[0].StartUT; + var endUT = trajectory[0].EndUT; + if (MapMagicValues.PerPatchGradients) + { + return (startUT, endUT); + } + + foreach (var patchedOrbit in trajectory) + { + if (patchedOrbit.PatchEndTransition != PatchTransitionType.Final) + { + continue; + } + + endUT = patchedOrbit.EndUT; + break; + } + + return (startUT, endUT); + } + + private static (Color startColor, Color endColor) GetColorsForSegmentIndex( + int segmentIndex, + SegmentType type, + bool isHighlighted = false + ) + { + var alphaFactor = isHighlighted ? 0.5f : 1.0f; + + var colorList = GetSegmentColors(type); + var startColor = colorList[segmentIndex % colorList.Count] with {a = alphaFactor}; + var endColor = startColor * 0.75f; + + return (startColor, endColor); + } + + private static List GetSegmentColors(SegmentType type) => type switch + { + SegmentType.Trajectory => TrajectoryColors, + SegmentType.Maneuver => ManeuverColors, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) + }; + + #endregion + + private enum SegmentType + { + Trajectory, + Maneuver, + } +} \ No newline at end of file diff --git a/src/KerbalLifeHacks/Hacks/SkipOrientation/SkipOrientation.cs b/src/KerbalLifeHacks/Hacks/SkipOrientation/SkipOrientation.cs new file mode 100644 index 0000000..d34179e --- /dev/null +++ b/src/KerbalLifeHacks/Hacks/SkipOrientation/SkipOrientation.cs @@ -0,0 +1,26 @@ +using HarmonyLib; +using KSP.Game; +using KSP.Messages; +using KSP.VFX; + +namespace KerbalLifeHacks.Hacks.SkipOrientation; + +[Hack("Skip Orientation", false)] +public class SkipOrientation: BaseHack +{ + public override void OnInitialized() + { + HarmonyInstance.PatchAll(typeof(SkipOrientation)); + } + + /// + /// By default KSP.Game.CreateCampaignMenu._isFTUEEnabled is set to true + /// This patch will set it to false always, after the new campaign menu is opened. + /// + [HarmonyPatch(typeof(CreateCampaignMenu), nameof(CreateCampaignMenu.OnEnable), MethodType.Normal)] + [HarmonyPostfix] + public static void OrientationStartDisabled(CreateCampaignMenu __instance) + { + __instance._isFTUEEnabled.SetValue(false); + } +} \ No newline at end of file diff --git a/src/KerbalLifeHacks/Hacks/WarpToOrbitalPoint/WarpToOrbitalPoint.cs b/src/KerbalLifeHacks/Hacks/WarpToOrbitalPoint/WarpToOrbitalPoint.cs new file mode 100644 index 0000000..807b709 --- /dev/null +++ b/src/KerbalLifeHacks/Hacks/WarpToOrbitalPoint/WarpToOrbitalPoint.cs @@ -0,0 +1,148 @@ +using HarmonyLib; +using I2.Loc; +using KSP.Api.CoreTypes; +using KSP.Audio; +using KSP.Map; +using UnityEngine; + +namespace KerbalLifeHacks.Hacks.WarpToOrbitalPoint; + +[Hack("Add buttons in map view to warp to orbital points")] +public class WarpToOrbitalPoint : BaseHack +{ + private const string ContainerName = "Container"; + private const string WarpToButtonName = "BTN-WarpTo-ContextPanelButton"; + + private const string WarpToApButtonName = "BTN-WarpToAp-ContextPanelButton"; + private const string WarpToPeButtonName = "BTN-WarpToPe-ContextPanelButton"; + private const string WarpToSOIButtonName = "BTN-WarpToSOI-ContextPanelButton"; + + private const string WarpToApButtonKey = "KerbalLifeHacks/Map/WarpToAp"; + private const string WarpToPeButtonKey = "KerbalLifeHacks/Map/WarpToPe"; + private const string WarpToSOIButtonKey = "KerbalLifeHacks/Map/WarpToSOI"; + + public override void OnInitialized() + { + HarmonyInstance.PatchAll(typeof(WarpToOrbitalPoint)); + } + + [HarmonyPatch(typeof(Map3DManeuvers), nameof(Map3DManeuvers.Configure))] + [HarmonyPrefix] + private static void ConfigurePrefix( + // ReSharper disable once InconsistentNaming + Map3DManeuvers __instance, + // ReSharper disable once InconsistentNaming + ref bool __state + ) + { + // Save the state of the maneuver popup before the original method is called + __state = __instance._maneuverPopupInstance != null; + } + + private static int _nextSiblingIndex; + + [HarmonyPatch(typeof(Map3DManeuvers), nameof(Map3DManeuvers.Configure))] + [HarmonyPostfix] + private static void ConfigurePostfix( + // ReSharper disable once InconsistentNaming + Map3DManeuvers __instance, + // ReSharper disable once InconsistentNaming + bool __state + ) + { + // If the maneuver popup was already configured before the method was called, we don't need to do anything + if (__state) + { + return; + } + + _nextSiblingIndex = 4; + + var popupContainer = __instance._maneuverPopupInstance.gameObject.GetChild(ContainerName); + var warpToButton = popupContainer.GetChild(WarpToButtonName); + + CreateButton( + warpToButton, + popupContainer.transform, + WarpToApButtonName, + WarpToApButtonKey, + () => OnWarpToAp(__instance) + ); + CreateButton( + warpToButton, + popupContainer.transform, + WarpToPeButtonName, + WarpToPeButtonKey, + () => OnWarpToPe(__instance) + ); + CreateButton(warpToButton, + popupContainer.transform, + WarpToSOIButtonName, + WarpToSOIButtonKey, + () => OnWarpToSOI(__instance) + ); + } + + [HarmonyPatch(typeof(Map3DManeuvers), nameof(Map3DManeuvers.ShowManeuverPopup))] + [HarmonyPostfix] + // ReSharper disable once InconsistentNaming + private static void ShowManeuverPopupPostfix(Map3DManeuvers __instance) + { + var popupContainer = __instance._maneuverPopupInstance.gameObject.GetChild(ContainerName); + var warpToApButton = popupContainer.GetChild(WarpToApButtonName); + var warpToPeButton = popupContainer.GetChild(WarpToPeButtonName); + var warpToSOIButton = popupContainer.GetChild(WarpToSOIButtonName); + + var currentUT = __instance._game.UniverseModel.UniverseTime; + var orbitPatch = __instance._representedRenderData.Segment.OrbitPatch; + var timeAtAp = orbitPatch.StartUT + orbitPatch.TimeToAp; + var timeAtPe = orbitPatch.StartUT + orbitPatch.TimeToPe; + var timeAtSOI = orbitPatch.UniversalTimeAtSoiEncounter; + + warpToSOIButton.SetActive(timeAtSOI >= 0); + warpToApButton.SetActive(timeAtAp < orbitPatch.EndUT); + warpToPeButton.SetActive(timeAtPe < orbitPatch.EndUT && (timeAtPe > currentUT || timeAtSOI < 0)); + } + + private static void WarpTo(Map3DManeuvers instance, double time) + { + instance._game.ViewController.TimeWarp.WarpTo(time - 30); + KSPAudioEventManager.OnMapModeWarpTo(); + instance.HideManeuverPopup(); + } + + private static void OnWarpToAp(Map3DManeuvers instance) + { + var orbitPatch = instance._representedRenderData.Segment.OrbitPatch; + var apTime = orbitPatch.StartUT + orbitPatch.TimeToAp; + WarpTo(instance, apTime); + } + + private static void OnWarpToPe(Map3DManeuvers instance) + { + var orbitPatch = instance._representedRenderData.Segment.OrbitPatch; + var peTime = orbitPatch.StartUT + orbitPatch.TimeToPe; + WarpTo(instance, peTime); + } + + private static void OnWarpToSOI(Map3DManeuvers instance) + { + var soiTime = instance._representedRenderData.Segment.OrbitPatch.UniversalTimeAtSoiEncounter; + WarpTo(instance, soiTime); + } + + private static void CreateButton( + GameObject original, + Transform parent, + string name, + string locKey, + Action onClick + ) + { + var button = Instantiate(original, parent); + button.name = name; + button.transform.SetSiblingIndex(_nextSiblingIndex++); + button.GetChild("Text (TMP)").GetComponent().SetTerm(locKey); + button.GetComponent().action = new DelegateAction(onClick); + } +} \ No newline at end of file