diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs index 93efe304..e5967672 100644 --- a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -18,6 +18,11 @@ namespace NewHorizons.Builder.Props { var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + if (string.IsNullOrEmpty(info.name)) + { + info.name = go.name; + } + var travelerController = go.GetAddComponent(); if (!string.IsNullOrEmpty(info.startPlayingCondition)) { @@ -34,6 +39,8 @@ namespace NewHorizons.Builder.Props if (info.dialogue != null) { var (dialogueTree, remoteTrigger) = DialogueBuilder.Make(planetGO, sector, info.dialogue, nhBody.Mod); + dialogueTree.transform.SetParent(travelerController.transform, false); + dialogueTree.transform.localPosition = Vector3.zero; if (travelerController._dialogueTree != null) { travelerController._dialogueTree.OnStartConversation -= travelerController.OnStartConversation; @@ -54,17 +61,20 @@ namespace NewHorizons.Builder.Props { var signalInfo = new SignalInfo() { + name = info.name, audio = info.loopAudio, - detectionRadius = 0, + detectionRadius = 10f, identificationRadius = 10f, + onlyAudibleToScope = false, frequency = string.IsNullOrEmpty(info.frequency) ? "Traveler" : info.frequency, - parentPath = go.transform.GetPath(), - isRelativeToParent = true, - position = Vector3.up * 0.5f, }; var signalGO = SignalBuilder.Make(planetGO, sector, signalInfo, nhBody.Mod); + signalGO.transform.SetParent(travelerController.transform, false); + signalGO.transform.localPosition = Vector3.zero; + var signal = signalGO.GetComponent(); travelerController._signal = signal; + signal.SetSignalActivation(false); loopAudioSource = signal.GetOWAudioSource(); } else if (travelerController._signal == null) @@ -80,11 +90,14 @@ namespace NewHorizons.Builder.Props { audio = info.finaleAudio, track = External.SerializableEnums.NHAudioMixerTrackName.Music, + volume = 1f, }; finaleAudioSource = GeneralAudioBuilder.Make(planetGO, sector, finaleAudioInfo, nhBody.Mod); finaleAudioSource.SetTrack(finaleAudioInfo.track.ConvertToOW()); finaleAudioSource.loop = false; finaleAudioSource.spatialBlend = 0f; + finaleAudioSource.playOnAwake = false; + finaleAudioSource.gameObject.SetActive(true); } var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); @@ -111,6 +124,7 @@ namespace NewHorizons.Builder.Props go.GetAddComponent(); var quantumInstrument = go.GetAddComponent(); quantumInstrument._gatherWithScope = info.gatherWithScope; + ArrayHelpers.Append(ref quantumInstrument._deactivateObjects, go); var trigger = go.AddComponent(); trigger.gatherCondition = info.gatherCondition; @@ -124,15 +138,15 @@ namespace NewHorizons.Builder.Props { var signalInfo = new SignalInfo() { + name = travelerData.info.name, audio = travelerData.info.loopAudio, detectionRadius = 0, identificationRadius = 0, frequency = string.IsNullOrEmpty(travelerData.info.frequency) ? "Traveler" : travelerData.info.frequency, - parentPath = go.transform.GetPath(), - isRelativeToParent = true, - position = Vector3.zero, }; var signalGO = SignalBuilder.Make(planetGO, sector, signalInfo, nhBody.Mod); + signalGO.transform.SetParent(quantumInstrument.transform, false); + signalGO.transform.localPosition = Vector3.zero; } else { diff --git a/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs index 3a93cbda..46bfc5a0 100644 --- a/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs +++ b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs @@ -1,3 +1,4 @@ +using NewHorizons.Utility.OWML; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -16,6 +17,7 @@ namespace NewHorizons.Components.EyeOfTheUniverse { src.loop = false; src.SetLocalVolume(1f); + src.Stop(); _loopSources.Add(src); } @@ -23,6 +25,7 @@ namespace NewHorizons.Components.EyeOfTheUniverse { src.loop = false; src.SetLocalVolume(1f); + src.Stop(); _finaleSources.Add(src); } @@ -36,61 +39,85 @@ namespace NewHorizons.Components.EyeOfTheUniverse public void TransitionToFinale() { _transitionToFinale = true; - } - private IEnumerator DoLoop() - { - // Initial delay to ensure audio system has time to schedule all loops for the same tick - double timeBufferWindow = 0.5; + var cosmicInflationController = FindObjectOfType(); - // Track when the next audio (loop or finale) should play, in audio system time - double nextAudioEventTime = AudioSettings.dspTime + timeBufferWindow; + // Schedule finale for as soon as the current segment loop ends + double finaleAudioTime = _segmentEndAudioTime; + float finaleGameTime = _segmentEndGameTime; - // Determine timing using the first loop audio clip (should be Riebeck's banjo loop) - var referenceLoopClip = _loopSources.First().clip; - double loopDuration = referenceLoopClip.samples / (double)referenceLoopClip.frequency; - double segmentDuration = loopDuration / 4.0; - - while (!_transitionToFinale) + // Cancel loop audio + foreach (var loopSrc in _loopSources) { - // Play loops in sync - var loopStartTime = nextAudioEventTime; - foreach (var loopSrc in _loopSources) - { - loopSrc._audioSource.PlayScheduled(loopStartTime); - } - - nextAudioEventTime += loopDuration; - - // Handle loop segments (the current musical measure will always finish playing before transitioning to the finale) - for (int i = 0; i < 4; i++) - { - // Interrupting the upcoming segment for the finale - if (_transitionToFinale) - { - // End the loop at the start time of the upcoming segment - var loopStopTime = loopStartTime + segmentDuration * (i + 1); - - // Cancel scheduled upcoming loop - foreach (var loopSrc in _loopSources) - { - loopSrc._audioSource.SetScheduledEndTime(loopStopTime); - } - - // Schedule finale for as soon as the loop ends - nextAudioEventTime = loopStopTime; - break; - } - - // Wait until shortly before the next segment (`nextAudioEventTime` will be ahead of current time by `timeBufferWindow`) - yield return new WaitForSecondsRealtime((float)segmentDuration); - } + loopSrc._audioSource.SetScheduledEndTime(finaleAudioTime); } + // Set quantum sphere inflation timer + var finaleDuration = cosmicInflationController._travelerFinaleSource.clip.length; + cosmicInflationController._startFormationTime = Time.time; + cosmicInflationController._finishFormationTime = finaleGameTime + finaleDuration - 4f; + // Play finale in sync foreach (var finaleSrc in _finaleSources) { - finaleSrc._audioSource.PlayScheduled(nextAudioEventTime); + finaleSrc._audioSource.PlayScheduled(finaleAudioTime); + } + } + + // Delay between game logic and audio to ensure audio system has time to schedule all loops for the same tick + const double TIME_BUFFER_WINDOW = 0.5; + + private double _segmentEndAudioTime; + private float _segmentEndGameTime; + + private IEnumerator DoLoop() + { + // Determine timing using the first loop audio clip (should be Riebeck's banjo loop) + var referenceLoopClip = _loopSources.First().clip; + double loopDuration = referenceLoopClip.samples / (double)referenceLoopClip.frequency; + + // Vanilla audio divides the loop into 4 segments, but that actually causes weird key shifting during the crossfade + int segmentCount = 2; + double segmentDuration = loopDuration / segmentCount; + + // Track when the next loop will play, in both audio system time and game time + double nextLoopAudioTime = AudioSettings.dspTime + TIME_BUFFER_WINDOW; + float nextLoopGameTime = Time.time + (float)TIME_BUFFER_WINDOW; + + while (!_transitionToFinale) + { + // Play loops in sync + double loopStartAudioTime = nextLoopAudioTime; + float loopStartGameTime = nextLoopGameTime; + + foreach (var loopSrc in _loopSources) + { + if (!loopSrc.gameObject.activeInHierarchy) continue; + if (loopSrc.loop) continue; + // We only need to schedule once and then Unity will loop it for us + loopSrc._audioSource.PlayScheduled(loopStartAudioTime); + loopSrc.loop = true; + } + + // Schedule next loop + nextLoopAudioTime += loopDuration; + nextLoopGameTime += (float)loopDuration; + + // Track loop segment timing (the current musical verse should always finish playing before the finale) + for (int i = 0; i < segmentCount; i++) + { + _segmentEndAudioTime = loopStartAudioTime + segmentDuration * (i + 1); + _segmentEndGameTime = loopStartGameTime + (float)(segmentDuration * (i + 1)); + + // Wait until the next segment + while (Time.time < _segmentEndGameTime && !_transitionToFinale) + { + yield return null; + } + + // Interrupt the remaining segments for the finale + if (_transitionToFinale) break; + } } } } diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs index 8f4ee0c2..41655e3a 100644 --- a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs @@ -11,6 +11,11 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse /// public string id; + /// + /// The name to display for this traveler's signals. Defaults to the name of the detail. + /// + public string name; + /// /// The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler. /// diff --git a/NewHorizons/Handlers/EyeDetailCacher.cs b/NewHorizons/Handlers/EyeDetailCacher.cs index 62dfb460..f4ff2ed0 100644 --- a/NewHorizons/Handlers/EyeDetailCacher.cs +++ b/NewHorizons/Handlers/EyeDetailCacher.cs @@ -19,6 +19,36 @@ public static class EyeDetailCacher foreach (var body in Main.BodyDict["EyeOfTheUniverse"]) { NHLogger.LogVerbose($"{nameof(EyeDetailCacher)}: {body.Config.name}"); + if (body.Config?.EyeOfTheUniverse?.eyeTravelers != null) + { + foreach (var detail in body.Config.EyeOfTheUniverse.eyeTravelers) + { + if (!string.IsNullOrEmpty(detail.assetBundle)) continue; + + AddPathToCache(detail.path); + } + } + + if (body.Config?.EyeOfTheUniverse?.instrumentZones != null) + { + foreach (var detail in body.Config.EyeOfTheUniverse.instrumentZones) + { + if (!string.IsNullOrEmpty(detail.assetBundle)) continue; + + AddPathToCache(detail.path); + } + } + + if (body.Config?.EyeOfTheUniverse?.quantumInstruments != null) + { + foreach (var detail in body.Config.EyeOfTheUniverse.quantumInstruments) + { + if (!string.IsNullOrEmpty(detail.assetBundle)) continue; + + AddPathToCache(detail.path); + } + } + if (body.Config?.Props?.details != null) { foreach (var detail in body.Config.Props.details) diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index 16fd0194..dc2f3e2e 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -180,6 +180,8 @@ namespace NewHorizons.Handlers public static void SetUpEyeCampfireSequence() { + if (!GetCustomEyeTravelers().Any()) return; + _eyeMusicController = new GameObject("EyeMusicController").AddComponent(); var quantumCampsiteController = Object.FindObjectOfType(); @@ -196,6 +198,8 @@ namespace NewHorizons.Handlers { if (eyeTraveler.controller != null) { + eyeTraveler.controller.gameObject.SetActive(false); + ArrayHelpers.Append(ref quantumCampsiteController._travelerControllers, eyeTraveler.controller); eyeTraveler.controller.OnStartPlaying += quantumCampsiteController.OnTravelerStartPlaying; @@ -211,6 +215,7 @@ namespace NewHorizons.Handlers if (eyeTraveler.loopAudioSource != null) { + eyeTraveler.loopAudioSource.GetComponent().SetSignalActivation(false); _eyeMusicController.RegisterLoopSource(eyeTraveler.loopAudioSource); } if (eyeTraveler.finaleAudioSource != null) @@ -227,6 +232,7 @@ namespace NewHorizons.Handlers if (ancestorInstrumentZone == null) { // Quantum instrument is not a child of an instrument zone, so treat it like its own zone + quantumInstrument.gameObject.SetActive(false); ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, quantumInstrument.gameObject); } } @@ -237,11 +243,13 @@ namespace NewHorizons.Handlers ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, instrumentZone.gameObject); } } + + UpdateTravelerPositions(); } public static void UpdateTravelerPositions() { - //if (!GetCustomEyeTravelers().Any()) return; + if (!GetCustomEyeTravelers().Any()) return; var quantumCampsiteController = Object.FindObjectOfType(); diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 64f9a47e..e679e79a 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -438,7 +438,6 @@ namespace NewHorizons _playerAwake = true; EyeSceneHandler.Init(); EyeSceneHandler.OnSceneLoad(); - EyeSceneHandler.SetUpEyeCampfireSequence(); } if (isSolarSystem || isEyeOfTheUniverse) @@ -537,6 +536,8 @@ namespace NewHorizons IsWarpingFromVessel = false; DidWarpFromVessel = false; DidWarpFromShip = false; + + EyeSceneHandler.SetUpEyeCampfireSequence(); } //Stop starfield from disappearing when there is no lights diff --git a/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs index e6e4ce99..57c12544 100644 --- a/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs @@ -1,6 +1,7 @@ using HarmonyLib; using NewHorizons.Handlers; using NewHorizons.Utility.OWML; +using System.Linq; namespace NewHorizons.Patches.EyeScenePatches { @@ -11,7 +12,7 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(CosmicInflationController.UpdateFormation))] public static void CosmicInflationController_UpdateFormation(CosmicInflationController __instance) { - if (__instance._waitForCrossfade) + if (__instance._waitForCrossfade && EyeSceneHandler.GetCustomEyeTravelers().Any()) { NHLogger.Log($"Hijacking finale cross-fade, NH will handle it"); __instance._waitForCrossfade = false; diff --git a/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs index 93c1b410..d02453f5 100644 --- a/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs @@ -30,10 +30,13 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(QuantumCampsiteController.AreAllTravelersGathered))] public static bool QuantumCampsiteController_AreAllTravelersGathered(QuantumCampsiteController __instance, ref bool __result) { + if (!EyeSceneHandler.GetCustomEyeTravelers().Any()) + { + return true; + } bool gatheredAllHearthianTravelers = __instance._travelerControllers.Take(4).All(t => t.gameObject.activeInHierarchy); if (!gatheredAllHearthianTravelers) { - NHLogger.LogVerbose(""); __result = false; return false; } @@ -73,7 +76,7 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(QuantumCampsiteController.OnTravelerStartPlaying))] public static void OnTravelerStartPlaying(QuantumCampsiteController __instance) { - if (!__instance._hasJamSessionStarted) + if (!__instance._hasJamSessionStarted && EyeSceneHandler.GetCustomEyeTravelers().Any()) { NHLogger.Log($"NH is handling Eye sequence music"); // Jam session is starting, start our custom music handler diff --git a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs index 8548c244..346dfd92 100644 --- a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs @@ -1,4 +1,6 @@ using HarmonyLib; +using NewHorizons.Handlers; +using System.Linq; namespace NewHorizons.Patches.EyeScenePatches { @@ -9,6 +11,10 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(TravelerEyeController.OnStartCosmicJamSession))] public static bool TravelerEyeController_OnStartCosmicJamSession(TravelerEyeController __instance) { + if (!EyeSceneHandler.GetCustomEyeTravelers().Any()) + { + return true; + } // Not starting the loop audio here; EyeMusicController will handle that __instance._signal.GetOWAudioSource().SetLocalVolume(0f); return false;