diff --git a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs index 81c3c6ce..4f1c0ecb 100644 --- a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs @@ -1,5 +1,6 @@ using NewHorizons.Components.Volumes; using NewHorizons.External.Modules.Volumes.VolumeInfos; +using OWML.Common; using OWML.Utils; using UnityEngine; @@ -7,12 +8,13 @@ namespace NewHorizons.Builder.Volumes { internal static class CreditsVolumeBuilder { - public static LoadCreditsVolume Make(GameObject planetGO, Sector sector, LoadCreditsVolumeInfo info) + public static LoadCreditsVolume Make(GameObject planetGO, Sector sector, LoadCreditsVolumeInfo info, IModBehaviour mod) { var volume = VolumeBuilder.Make(planetGO, sector, info); volume.gameOver = info.gameOver; volume.deathType = info.deathType == null ? null : EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); + volume.mod = mod; return volume; } diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs index bdaa4a5a..128717f6 100644 --- a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -209,7 +209,7 @@ namespace NewHorizons.Builder.Volumes { foreach (var creditsVolume in config.Volumes.creditsVolume) { - CreditsVolumeBuilder.Make(go, sector, creditsVolume); + CreditsVolumeBuilder.Make(go, sector, creditsVolume, mod); } } } diff --git a/NewHorizons/Components/NHGameOverManager.cs b/NewHorizons/Components/NHGameOverManager.cs index 93a7339a..0fe3dc08 100644 --- a/NewHorizons/Components/NHGameOverManager.cs +++ b/NewHorizons/Components/NHGameOverManager.cs @@ -1,7 +1,11 @@ using NewHorizons.External.Modules; using NewHorizons.External.SerializableEnums; using NewHorizons.Handlers; +using NewHorizons.Patches.CreditsScenePatches; +using NewHorizons.Utility.Files; using NewHorizons.Utility.OWML; +using OWML.Common; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -15,14 +19,14 @@ namespace NewHorizons.Components /// Mod unique id to game over module list /// Done as a dictionary so that Reload Configs can overwrite entries per mod /// - public static Dictionary gameOvers = new(); + public static Dictionary gameOvers = new(); public static NHGameOverManager Instance { get; private set; } private GameOverController _gameOverController; private PlayerCameraEffectController _playerCameraEffectController; - private GameOverModule[] _gameOvers; + private (IModBehaviour mod, GameOverModule gameOver)[] _gameOvers; private bool _gameOverSequenceStarted; @@ -36,25 +40,35 @@ namespace NewHorizons.Components _gameOverController = FindObjectOfType(); _playerCameraEffectController = FindObjectOfType(); - _gameOvers = gameOvers.SelectMany(x => x.Value).ToArray(); + var gameOverList = new List<(IModBehaviour, GameOverModule)>(); + foreach (var gameOverPair in gameOvers) + { + var mod = gameOverPair.Key; + foreach (var gameOver in gameOverPair.Value) + { + gameOverList.Add((mod, gameOver)); + } + } + _gameOvers = gameOverList.ToArray(); } public void TryHijackDeathSequence() { - var gameOver = _gameOvers.FirstOrDefault(x => !string.IsNullOrEmpty(x.condition) && DialogueConditionManager.SharedInstance.GetConditionState(x.condition)); - if (!_gameOverSequenceStarted && gameOver != null && !Locator.GetDeathManager()._finishedDLC) + var gameOver = _gameOvers.FirstOrDefault(x => !string.IsNullOrEmpty(x.gameOver.condition) + && DialogueConditionManager.SharedInstance.GetConditionState(x.gameOver.condition)); + if (!_gameOverSequenceStarted && gameOver != default && !Locator.GetDeathManager()._finishedDLC) { - StartGameOverSequence(gameOver, null); + StartGameOverSequence(gameOver.gameOver, null, gameOver.mod); } } - public void StartGameOverSequence(GameOverModule gameOver, DeathType? deathType) + public void StartGameOverSequence(GameOverModule gameOver, DeathType? deathType, IModBehaviour mod) { _gameOverSequenceStarted = true; - Delay.StartCoroutine(GameOver(gameOver, deathType)); + Delay.StartCoroutine(GameOver(gameOver, deathType, mod)); } - private IEnumerator GameOver(GameOverModule gameOver, DeathType? deathType) + private IEnumerator GameOver(GameOverModule gameOver, DeathType? deathType, IModBehaviour mod) { OWInput.ChangeInputMode(InputMode.None); ReticleController.Hide(); @@ -104,12 +118,12 @@ namespace NewHorizons.Components yield return new WaitUntil(ReadytoLoadCreditsScene); } - LoadCreditsScene(gameOver); + LoadCreditsScene(gameOver, mod); } private bool ReadytoLoadCreditsScene() => _gameOverController._fadedOutText && _gameOverController._textAnimator.IsComplete(); - private void LoadCreditsScene(GameOverModule gameOver) + private void LoadCreditsScene(GameOverModule gameOver, IModBehaviour mod) { NHLogger.LogVerbose($"Load credits {gameOver.creditsType}"); @@ -125,6 +139,9 @@ namespace NewHorizons.Components TimelineObliterationController.s_hasRealityEnded = true; LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); break; + case NHCreditsType.Custom: + LoadCustomCreditsScene(gameOver, mod); + break; default: // GameOverController disables post processing _gameOverController._flashbackCamera.postProcessing.enabled = true; @@ -134,5 +151,35 @@ namespace NewHorizons.Components break; } } + + private void LoadCustomCreditsScene(GameOverModule gameOver, IModBehaviour mod) + { + LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); + + // Unfortunately we can't make this a private method, as EventArgs/EventHandler enforces the (sender, e) parameters, which prevents us from passing in gameOver and mod, which we need. + EventHandler onCreditsBuilt = null; // needs to be done so we can unsubscribe from within the lambda. + onCreditsBuilt = (sender, e) => + { + // Unsubscribe first, playing it safe in case it NREs + CreditsPatches.CreditsBuilt -= onCreditsBuilt; + + // Patch new music clip + var musicSource = Locator.FindObjectsOfType().Where(x => x.name == "AudioSource").Single(); // AudioSource that plays the credits music is literally called "AudioSource", luckily it's the only one called that. Lazy OW devs do be lazy. + AudioUtilities.SetAudioClip(musicSource, gameOver.audio, mod); + + musicSource.loop = gameOver.audioLooping; + musicSource._maxSourceVolume = gameOver.audioVolume; + + // Override fade in + musicSource.Stop(); + musicSource.Play(); + + // Patch scroll duration + var creditsScroll = Locator.FindObjectOfType(); + creditsScroll._scrollDuration = gameOver.length; + }; + + CreditsPatches.CreditsBuilt += onCreditsBuilt; + } } } diff --git a/NewHorizons/Components/Volumes/LoadCreditsVolume.cs b/NewHorizons/Components/Volumes/LoadCreditsVolume.cs index 26f75831..4f2dbfeb 100644 --- a/NewHorizons/Components/Volumes/LoadCreditsVolume.cs +++ b/NewHorizons/Components/Volumes/LoadCreditsVolume.cs @@ -1,4 +1,5 @@ using NewHorizons.External.Modules; +using OWML.Common; using UnityEngine; @@ -8,12 +9,13 @@ namespace NewHorizons.Components.Volumes { public GameOverModule gameOver; public DeathType? deathType; + public IModBehaviour mod; public override void OnTriggerVolumeEntry(GameObject hitObj) { if (hitObj.CompareTag("PlayerDetector") && enabled && (string.IsNullOrEmpty(gameOver.condition) || DialogueConditionManager.SharedInstance.GetConditionState(gameOver.condition))) { - NHGameOverManager.Instance.StartGameOverSequence(gameOver, deathType); + NHGameOverManager.Instance.StartGameOverSequence(gameOver, deathType, mod); } } diff --git a/NewHorizons/External/Modules/GameOverModule.cs b/NewHorizons/External/Modules/GameOverModule.cs index 00d029bd..7bb884e1 100644 --- a/NewHorizons/External/Modules/GameOverModule.cs +++ b/NewHorizons/External/Modules/GameOverModule.cs @@ -22,7 +22,34 @@ namespace NewHorizons.External.Modules /// Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over. /// Note this is a regular dialogue condition, not a persistent condition. /// - public string condition; + public string condition; + + /// + /// Path to the audio file to use as custom music for the credits. + /// Note: only applies when creditsType is set to "custom". + /// + public string audio; + + /// + /// The length of the fade in and out for the credits music. + /// Note: only applies when creditsType is set to "custom". + /// + [DefaultValue(1f)] + public float audioVolume; + + /// + /// Determines if the credits music should loop. + /// Note: only applies when creditsType is set to "custom". + /// + [DefaultValue(false)] + public bool audioLooping; + + /// + /// Duration of the credits scroll in seconds. + /// Note: only applies when creditsType is set to "custom". + /// + [DefaultValue(120f)] + public float length; /// /// The type of credits that will run after the game over message is shown diff --git a/NewHorizons/External/SerializableEnums/NHCreditsType.cs b/NewHorizons/External/SerializableEnums/NHCreditsType.cs index 83c1dc51..f4d0f6c4 100644 --- a/NewHorizons/External/SerializableEnums/NHCreditsType.cs +++ b/NewHorizons/External/SerializableEnums/NHCreditsType.cs @@ -13,6 +13,8 @@ namespace NewHorizons.External.SerializableEnums [EnumMember(Value = @"kazoo")] Kazoo = 2, - [EnumMember(Value = @"none")] None = 3 + [EnumMember(Value = @"custom")] Custom = 3, + + [EnumMember(Value = @"none")] None = 4 } } diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index f81400a9..113be63b 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -849,7 +849,7 @@ namespace NewHorizons } if (addonConfig.gameOver != null) { - NHGameOverManager.gameOvers[mod.ModHelper.Manifest.UniqueName] = addonConfig.gameOver; + NHGameOverManager.gameOvers[mod] = addonConfig.gameOver; } AddonConfigs[mod] = addonConfig; diff --git a/NewHorizons/NewHorizons.csproj.user b/NewHorizons/NewHorizons.csproj.user index 5e39a9dd..44729020 100644 --- a/NewHorizons/NewHorizons.csproj.user +++ b/NewHorizons/NewHorizons.csproj.user @@ -1,4 +1,4 @@ - + $(AppData)\OuterWildsModManager\OWML\Mods\xen.NewHorizons diff --git a/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs b/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs index 8892f334..037b30bf 100644 --- a/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs +++ b/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs @@ -1,17 +1,30 @@ using HarmonyLib; using NewHorizons.Handlers; +using System; namespace NewHorizons.Patches.CreditsScenePatches { [HarmonyPatch(typeof(Credits))] public static class CreditsPatches { + public static event EventHandler CreditsBuilt; // Used in NHGameOverManager to patch credits music and scroll speed + [HarmonyPrefix] [HarmonyPatch(nameof(Credits.Start))] public static void Credits_Start(Credits __instance) { CreditsHandler.AddCredits(__instance); } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Credits.BuildCredits))] + public static void Credits_BuildCredits_Post(Credits __instance) + { + // Do things BuildCredits() normally does + + // Fire event once finished + CreditsBuilt?.Invoke(__instance, new EventArgs()); + } } } diff --git a/NewHorizons/Utility/Files/AudioUtilities.cs b/NewHorizons/Utility/Files/AudioUtilities.cs index fe09d46e..f3c2924d 100644 --- a/NewHorizons/Utility/Files/AudioUtilities.cs +++ b/NewHorizons/Utility/Files/AudioUtilities.cs @@ -27,23 +27,34 @@ namespace NewHorizons.Utility.Files source._clipArrayLength = 0; source._clipSelectionOnPlay = OWAudioSource.ClipSelectionOnPlay.MANUAL; source.clip = clip; + NHLogger.LogVerbose($"[{nameof(AudioUtilities)}] : Audio {audio} was loaded from a file"); return; } catch { - NHLogger.LogError($"Could not load file {audio}"); + NHLogger.LogError($"[{nameof(AudioUtilities)}] : Could not load file {audio}"); } } if (EnumUtils.TryParse(audio, out AudioType type)) { source._audioLibraryClip = type; + NHLogger.LogVerbose($"[{nameof(AudioUtilities)}] : Audio {audio} was an AudioType enum"); } else { var audioClip = SearchUtilities.FindResourceOfTypeAndName(audio); - if (audioClip == null) NHLogger.Log($"Couldn't find audio clip {audio}"); - else source.clip = audioClip; + if (audioClip == null) + { + NHLogger.LogError($"[{nameof(AudioUtilities)}] : Couldn't find audio clip {audio}"); + } + else + { + NHLogger.LogVerbose($"[{nameof(AudioUtilities)}] : Audio {audio} was an AudioClip resource"); + // Else if this is set it will try to change the clip back when it starts playing + source._audioLibraryClip = AudioType.None; + source.clip = audioClip; + } } }