From ed4e43a04484d1ce5b1724522385358e5942c996 Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Mon, 13 Jan 2025 16:49:28 -0800 Subject: [PATCH 01/39] delete duplicate box shape fixer --- NewHorizons/Builder/Props/QuantumBuilder.cs | 35 ------------------- .../Utility/Geometry/BoxShapeFinder.cs | 34 ++++++++---------- 2 files changed, 15 insertions(+), 54 deletions(-) diff --git a/NewHorizons/Builder/Props/QuantumBuilder.cs b/NewHorizons/Builder/Props/QuantumBuilder.cs index 90227e11..a6a475cf 100644 --- a/NewHorizons/Builder/Props/QuantumBuilder.cs +++ b/NewHorizons/Builder/Props/QuantumBuilder.cs @@ -295,39 +295,4 @@ namespace NewHorizons.Builder.Props return globalCorners.Select(globalCorner => relativeTo.transform.InverseTransformPoint(globalCorner)).ToArray(); } } - - /// - /// for some reason mesh bounds are wrong unless we wait a bit - /// so this script contiously checks everything until it is correct - /// - /// this actually only seems to be a problem with skinned renderers. normal ones work fine - /// TODO: at some point narrow this down to just skinned, instead of doing everything and checking every frame - /// - public class BoxShapeFixer : MonoBehaviour - { - public BoxShape shape; - public MeshFilter meshFilter; - public SkinnedMeshRenderer skinnedMeshRenderer; - - public void Update() - { - if (meshFilter == null && skinnedMeshRenderer == null) - { - NHLogger.LogVerbose("Useless BoxShapeFixer, destroying"); - DestroyImmediate(this); - } - - Mesh sharedMesh = null; - if (meshFilter != null) sharedMesh = meshFilter.sharedMesh; - if (skinnedMeshRenderer != null) sharedMesh = skinnedMeshRenderer.sharedMesh; - - if (sharedMesh == null) return; - if (sharedMesh.bounds.size == Vector3.zero) return; - - shape.size = sharedMesh.bounds.size; - shape.center = sharedMesh.bounds.center; - - DestroyImmediate(this); - } - } } diff --git a/NewHorizons/Utility/Geometry/BoxShapeFinder.cs b/NewHorizons/Utility/Geometry/BoxShapeFinder.cs index ebdfd9e4..40e5c37a 100644 --- a/NewHorizons/Utility/Geometry/BoxShapeFinder.cs +++ b/NewHorizons/Utility/Geometry/BoxShapeFinder.cs @@ -3,7 +3,14 @@ using UnityEngine; namespace NewHorizons.Utility.Geometry; -internal class BoxShapeFixer : MonoBehaviour +/// +/// for some reason mesh bounds are wrong unless we wait a bit +/// so this script contiously checks everything until it is correct +/// +/// this actually only seems to be a problem with skinned renderers. normal ones work fine +/// TODO: at some point narrow this down to just skinned, instead of doing everything and checking every frame +/// +public class BoxShapeFixer : MonoBehaviour { public BoxShape shape; public MeshFilter meshFilter; @@ -13,31 +20,20 @@ internal class BoxShapeFixer : MonoBehaviour { if (meshFilter == null && skinnedMeshRenderer == null) { - NHLogger.LogVerbose("Useless BoxShapeFixer, destroying"); DestroyImmediate(this); + NHLogger.LogVerbose("Useless BoxShapeFixer, destroying"); + DestroyImmediate(this); } Mesh sharedMesh = null; - if (meshFilter != null) - { - sharedMesh = meshFilter.sharedMesh; - } - if (skinnedMeshRenderer != null) - { - sharedMesh = skinnedMeshRenderer.sharedMesh; - } + if (meshFilter != null) sharedMesh = meshFilter.sharedMesh; + if (skinnedMeshRenderer != null) sharedMesh = skinnedMeshRenderer.sharedMesh; - if (sharedMesh == null) - { - return; - } - if (sharedMesh.bounds.size == Vector3.zero) - { - return; - } + if (sharedMesh == null) return; + if (sharedMesh.bounds.size == Vector3.zero) return; shape.size = sharedMesh.bounds.size; shape.center = sharedMesh.bounds.center; DestroyImmediate(this); } -} +} \ No newline at end of file From 06023b77544d13dde7961978edbe611bbccc94f2 Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Mon, 13 Jan 2025 16:51:46 -0800 Subject: [PATCH 02/39] delete duplicate BoundsUtilities stuff --- NewHorizons/Builder/Props/QuantumBuilder.cs | 70 +------------------ .../Utility/Geometry/BoundsUtilities.cs | 17 ++--- 2 files changed, 10 insertions(+), 77 deletions(-) diff --git a/NewHorizons/Builder/Props/QuantumBuilder.cs b/NewHorizons/Builder/Props/QuantumBuilder.cs index a6a475cf..302270e8 100644 --- a/NewHorizons/Builder/Props/QuantumBuilder.cs +++ b/NewHorizons/Builder/Props/QuantumBuilder.cs @@ -223,76 +223,8 @@ namespace NewHorizons.Builder.Props shuffle._shuffledObjects = propsInGroup.Select(p => p.transform).ToArray(); shuffle.Awake(); // this doesn't get called on its own for some reason. what? how? - AddBoundsVisibility(shuffleParent); + BoundsUtilities.AddBoundsVisibility(shuffleParent); shuffleParent.SetActive(true); } - - - struct BoxShapeReciever - { - public MeshFilter f; - public SkinnedMeshRenderer s; - public GameObject gameObject; - } - - public static void AddBoundsVisibility(GameObject g) - { - var meshFilters = g.GetComponentsInChildren(); - var skinnedMeshRenderers = g.GetComponentsInChildren(); - - var boxShapeRecievers = meshFilters - .Select(f => new BoxShapeReciever() { f = f, gameObject = f.gameObject }) - .Concat( - skinnedMeshRenderers.Select(s => new BoxShapeReciever() { s = s, gameObject = s.gameObject }) - ) - .ToList(); - - foreach (var boxshapeReciever in boxShapeRecievers) - { - var box = boxshapeReciever.gameObject.AddComponent(); - boxshapeReciever.gameObject.AddComponent(); - boxshapeReciever.gameObject.AddComponent(); - - var fixer = boxshapeReciever.gameObject.AddComponent(); - fixer.shape = box; - fixer.meshFilter = boxshapeReciever.f; - fixer.skinnedMeshRenderer = boxshapeReciever.s; - } - } - - // BUG: ignores skinned guys. this coincidentally makes it work without BoxShapeFixer - public static Bounds GetBoundsOfSelfAndChildMeshes(GameObject g) - { - var meshFilters = g.GetComponentsInChildren(); - var corners = meshFilters.SelectMany(m => GetMeshCorners(m, g)).ToList(); - - Bounds b = new Bounds(corners[0], Vector3.zero); - corners.ForEach(corner => b.Encapsulate(corner)); - - return b; - } - - public static Vector3[] GetMeshCorners(MeshFilter m, GameObject relativeTo = null) - { - var bounds = m.mesh.bounds; - - var localCorners = new Vector3[] - { - bounds.min, - bounds.max, - new Vector3(bounds.min.x, bounds.min.y, bounds.max.z), - new Vector3(bounds.min.x, bounds.max.y, bounds.min.z), - new Vector3(bounds.max.x, bounds.min.y, bounds.min.z), - new Vector3(bounds.min.x, bounds.max.y, bounds.max.z), - new Vector3(bounds.max.x, bounds.min.y, bounds.max.z), - new Vector3(bounds.max.x, bounds.max.y, bounds.min.z), - }; - - var globalCorners = localCorners.Select(localCorner => m.transform.TransformPoint(localCorner)).ToArray(); - - if (relativeTo == null) return globalCorners; - - return globalCorners.Select(globalCorner => relativeTo.transform.InverseTransformPoint(globalCorner)).ToArray(); - } } } diff --git a/NewHorizons/Utility/Geometry/BoundsUtilities.cs b/NewHorizons/Utility/Geometry/BoundsUtilities.cs index b251a6d7..9f60ce0d 100644 --- a/NewHorizons/Utility/Geometry/BoundsUtilities.cs +++ b/NewHorizons/Utility/Geometry/BoundsUtilities.cs @@ -40,6 +40,7 @@ public static class BoundsUtilities } } + // BUG: ignores skinned guys. this coincidentally makes it work without BoxShapeFixer public static Bounds GetBoundsOfSelfAndChildMeshes(GameObject gameObject) { var meshFilters = gameObject.GetComponentsInChildren(); @@ -57,14 +58,14 @@ public static class BoundsUtilities var localCorners = new Vector3[] { - bounds.min, - bounds.max, - new Vector3(bounds.min.x, bounds.min.y, bounds.max.z), - new Vector3(bounds.min.x, bounds.max.y, bounds.min.z), - new Vector3(bounds.max.x, bounds.min.y, bounds.min.z), - new Vector3(bounds.min.x, bounds.max.y, bounds.max.z), - new Vector3(bounds.max.x, bounds.min.y, bounds.max.z), - new Vector3(bounds.max.x, bounds.max.y, bounds.min.z), + bounds.min, + bounds.max, + new Vector3(bounds.min.x, bounds.min.y, bounds.max.z), + new Vector3(bounds.min.x, bounds.max.y, bounds.min.z), + new Vector3(bounds.max.x, bounds.min.y, bounds.min.z), + new Vector3(bounds.min.x, bounds.max.y, bounds.max.z), + new Vector3(bounds.max.x, bounds.min.y, bounds.max.z), + new Vector3(bounds.max.x, bounds.max.y, bounds.min.z), }; var globalCorners = localCorners.Select(meshFilter.transform.TransformPoint).ToArray(); From 649883ab4e464fc6f9e3d139a07972e532a0fc91 Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Mon, 13 Jan 2025 16:53:59 -0800 Subject: [PATCH 03/39] j --- NewHorizons/Utility/Geometry/BoxShapeFinder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/Utility/Geometry/BoxShapeFinder.cs b/NewHorizons/Utility/Geometry/BoxShapeFinder.cs index 40e5c37a..1c0173a8 100644 --- a/NewHorizons/Utility/Geometry/BoxShapeFinder.cs +++ b/NewHorizons/Utility/Geometry/BoxShapeFinder.cs @@ -36,4 +36,4 @@ public class BoxShapeFixer : MonoBehaviour DestroyImmediate(this); } -} \ No newline at end of file +} From 63fbc8afeb45133569938dd21bc192b694401b90 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Sat, 18 Jan 2025 10:09:15 -0600 Subject: [PATCH 04/39] Eye sequence props first pass --- .../Builder/Props/EyeOfTheUniverseBuilder.cs | 187 ++++++++++++++++++ .../EyeOfTheUniverse/EyeMusicController.cs | 97 +++++++++ .../EyeOfTheUniverse/InstrumentZone.cs | 9 + .../QuantumInstrumentTrigger.cs | 30 +++ NewHorizons/External/Configs/PlanetConfig.cs | 5 + .../Modules/EyeOfTheUniverseModule.cs | 24 +++ .../Props/EyeOfTheUniverse/EyeTravelerInfo.cs | 45 +++++ .../EyeOfTheUniverse/InstrumentZoneInfo.cs | 13 ++ .../EyeOfTheUniverse/QuantumInstrumentInfo.cs | 34 ++++ NewHorizons/Handlers/EyeSceneHandler.cs | 167 ++++++++++++++++ NewHorizons/Handlers/PlanetCreationHandler.cs | 5 + NewHorizons/Main.cs | 2 + .../CosmicInflationControllerPatches.cs | 23 +++ .../QuantumCampsiteControllerPatches.cs | 85 ++++++++ .../TravelerEyeControllerPatches.cs | 17 ++ 15 files changed, 743 insertions(+) create mode 100644 NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs create mode 100644 NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs create mode 100644 NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs create mode 100644 NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs create mode 100644 NewHorizons/External/Modules/EyeOfTheUniverseModule.cs create mode 100644 NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs create mode 100644 NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs create mode 100644 NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs create mode 100644 NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs create mode 100644 NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs create mode 100644 NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs new file mode 100644 index 00000000..93efe304 --- /dev/null +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -0,0 +1,187 @@ +using NewHorizons.Builder.Props.Audio; +using NewHorizons.Components.EyeOfTheUniverse; +using NewHorizons.External; +using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Props.Audio; +using NewHorizons.External.Modules.Props.EyeOfTheUniverse; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using NewHorizons.Utility.OuterWilds; +using NewHorizons.Utility.OWML; +using UnityEngine; + +namespace NewHorizons.Builder.Props +{ + public static class EyeOfTheUniverseBuilder + { + public static TravelerEyeController MakeEyeTraveler(GameObject planetGO, Sector sector, EyeTravelerInfo info, NewHorizonsBody nhBody) + { + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + + var travelerController = go.GetAddComponent(); + if (!string.IsNullOrEmpty(info.startPlayingCondition)) + { + travelerController._startPlayingCondition = info.startPlayingCondition; + } + else if (string.IsNullOrEmpty(travelerController._startPlayingCondition)) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have a Start Playing condition set"); + } + if (travelerController._animator == null) + { + travelerController._animator = go.GetComponentInChildren(); + } + if (info.dialogue != null) + { + var (dialogueTree, remoteTrigger) = DialogueBuilder.Make(planetGO, sector, info.dialogue, nhBody.Mod); + if (travelerController._dialogueTree != null) + { + travelerController._dialogueTree.OnStartConversation -= travelerController.OnStartConversation; + travelerController._dialogueTree.OnEndConversation -= travelerController.OnEndConversation; + } + travelerController._dialogueTree = dialogueTree; + travelerController._dialogueTree.OnStartConversation += travelerController.OnStartConversation; + travelerController._dialogueTree.OnEndConversation += travelerController.OnEndConversation; + } + else if (travelerController._dialogueTree == null) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any dialogue set"); + } + + OWAudioSource loopAudioSource = null; + + if (!string.IsNullOrEmpty(info.loopAudio)) + { + var signalInfo = new SignalInfo() + { + audio = info.loopAudio, + detectionRadius = 0, + identificationRadius = 10f, + 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); + var signal = signalGO.GetComponent(); + travelerController._signal = signal; + loopAudioSource = signal.GetOWAudioSource(); + } + else if (travelerController._signal == null) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); + } + + OWAudioSource finaleAudioSource = null; + + if (!string.IsNullOrEmpty(info.finaleAudio)) + { + var finaleAudioInfo = new AudioSourceInfo() + { + audio = info.finaleAudio, + track = External.SerializableEnums.NHAudioMixerTrackName.Music, + }; + finaleAudioSource = GeneralAudioBuilder.Make(planetGO, sector, finaleAudioInfo, nhBody.Mod); + finaleAudioSource.SetTrack(finaleAudioInfo.track.ConvertToOW()); + finaleAudioSource.loop = false; + finaleAudioSource.spatialBlend = 0f; + } + + var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); + travelerData.info = info; + travelerData.controller = travelerController; + travelerData.loopAudioSource = loopAudioSource; + travelerData.finaleAudioSource = finaleAudioSource; + + return travelerController; + } + + public static QuantumInstrument MakeQuantumInstrument(GameObject planetGO, Sector sector, QuantumInstrumentInfo info, NewHorizonsBody nhBody) + { + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + go.layer = Layer.Interactible; + if (info.interactRadius > 0f) + { + var collider = go.AddComponent(); + collider.radius = info.interactRadius; + collider.isTrigger = true; + go.GetAddComponent(); + } + + go.GetAddComponent(); + var quantumInstrument = go.GetAddComponent(); + quantumInstrument._gatherWithScope = info.gatherWithScope; + + var trigger = go.AddComponent(); + trigger.gatherCondition = info.gatherCondition; + + var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); + travelerData.quantumInstruments.Add(quantumInstrument); + + if (travelerData.info != null) + { + if (!string.IsNullOrEmpty(travelerData.info.loopAudio)) + { + var signalInfo = new SignalInfo() + { + 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); + } + else + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); + } + } + else + { + NHLogger.LogError($"Quantum instrument with ID \"{info.id}\" has no matching eye traveler"); + } + + return quantumInstrument; + } + + public static InstrumentZone MakeInstrumentZone(GameObject planetGO, Sector sector, InstrumentZoneInfo info, NewHorizonsBody nhBody) + { + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); + + var instrumentZone = go.AddComponent(); + + var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); + travelerData.instrumentZones.Add(instrumentZone); + + return instrumentZone; + } + + public static void Make(GameObject go, Sector sector, EyeOfTheUniverseModule module, NewHorizonsBody nhBody) + { + if (module.eyeTravelers != null) + { + foreach (var info in module.eyeTravelers) + { + MakeEyeTraveler(go, sector, info, nhBody); + } + } + if (module.instrumentZones != null) + { + foreach (var info in module.instrumentZones) + { + MakeInstrumentZone(go, sector, info, nhBody); + } + } + if (module.quantumInstruments != null) + { + foreach (var info in module.quantumInstruments) + { + MakeQuantumInstrument(go, sector, info, nhBody); + } + } + } + } +} diff --git a/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs new file mode 100644 index 00000000..3a93cbda --- /dev/null +++ b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs @@ -0,0 +1,97 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components.EyeOfTheUniverse +{ + public class EyeMusicController : MonoBehaviour + { + private List _loopSources = new(); + private List _finaleSources = new(); + private bool _transitionToFinale; + private bool _isPlaying; + + public void RegisterLoopSource(OWAudioSource src) + { + src.loop = false; + src.SetLocalVolume(1f); + _loopSources.Add(src); + } + + public void RegisterFinaleSource(OWAudioSource src) + { + src.loop = false; + src.SetLocalVolume(1f); + _finaleSources.Add(src); + } + + public void StartPlaying() + { + if (_isPlaying) return; + _isPlaying = true; + StartCoroutine(DoLoop()); + } + + 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; + + // Track when the next audio (loop or finale) should play, in audio system time + double nextAudioEventTime = AudioSettings.dspTime + timeBufferWindow; + + // 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) + { + // 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); + } + } + + // Play finale in sync + foreach (var finaleSrc in _finaleSources) + { + finaleSrc._audioSource.PlayScheduled(nextAudioEventTime); + } + } + } +} diff --git a/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs b/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs new file mode 100644 index 00000000..39b0d529 --- /dev/null +++ b/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs @@ -0,0 +1,9 @@ +using UnityEngine; + +namespace NewHorizons.Components.EyeOfTheUniverse +{ + public class InstrumentZone : MonoBehaviour + { + + } +} diff --git a/NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs b/NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs new file mode 100644 index 00000000..c927c36c --- /dev/null +++ b/NewHorizons/Components/EyeOfTheUniverse/QuantumInstrumentTrigger.cs @@ -0,0 +1,30 @@ +using UnityEngine; + +namespace NewHorizons.Components.EyeOfTheUniverse +{ + public class QuantumInstrumentTrigger : MonoBehaviour + { + public string gatherCondition; + + private QuantumInstrument _quantumInstrument; + + private void Awake() + { + _quantumInstrument = GetComponent(); + _quantumInstrument.OnFinishGather += OnFinishGather; + } + + private void OnDestroy() + { + _quantumInstrument.OnFinishGather -= OnFinishGather; + } + + private void OnFinishGather() + { + if (!string.IsNullOrEmpty(gatherCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(gatherCondition, true); + } + } + } +} diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index c5ef092c..6184ed36 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -102,6 +102,11 @@ namespace NewHorizons.External.Configs /// public DreamModule Dream; + /// + /// Add features exclusive to the Eye of the Universe scene + /// + public EyeOfTheUniverseModule EyeOfTheUniverse; + /// /// Make this body into a focal point (barycenter) /// diff --git a/NewHorizons/External/Modules/EyeOfTheUniverseModule.cs b/NewHorizons/External/Modules/EyeOfTheUniverseModule.cs new file mode 100644 index 00000000..58e878c1 --- /dev/null +++ b/NewHorizons/External/Modules/EyeOfTheUniverseModule.cs @@ -0,0 +1,24 @@ +using NewHorizons.External.Modules.Props.EyeOfTheUniverse; +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules +{ + [JsonObject] + public class EyeOfTheUniverseModule + { + /// + /// Add custom travelers to the campfire sequence + /// + public EyeTravelerInfo[] eyeTravelers; + + /// + /// Add instrument zones which contain puzzles to gather a quantum instrument. You can parent other props to these with `parentPath` + /// + public InstrumentZoneInfo[] instrumentZones; + + /// + /// Add quantum instruments which cause their associated eye traveler to appear and instrument zones to disappear + /// + public QuantumInstrumentInfo[] quantumInstruments; + } +} diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs new file mode 100644 index 00000000..8f4ee0c2 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs @@ -0,0 +1,45 @@ +using NewHorizons.External.Modules.Props.Dialogue; +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse +{ + [JsonObject] + public class EyeTravelerInfo : DetailInfo + { + /// + /// A unique ID to associate this traveler with their corresponding quantum instruments and instrument zones. Must be unique for each traveler. + /// + public string id; + + /// + /// The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler. + /// + public string startPlayingCondition; + + /// + /// If specified, this dialogue condition must be set for the traveler to participate in the campfire song. Otherwise, the song will be able to start without them. + /// + public string participatingCondition; + + /// + /// The audio to use for the traveler while playing around the campfire (and also for their paired quantum instrument). It should be 16 measures at 92 BPM (approximately 42 seconds long). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string loopAudio; + + /// + /// The audio to use for the traveler during the finale of the campfire song. It should be 8 measures of the main loop at 92 BPM followed by 2 measures of fade-out (approximately 26 seconds long in total). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string finaleAudio; + + /// + /// The frequency ID of the signal emitted by the traveler. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`, + /// `Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. Defaults to `Traveler`. You can also put a custom value. + /// + public string frequency; + + /// + /// The dialogue to use for this traveler. Omit this or set it to null if your traveler already has valid dialogue. + /// + public DialogueInfo dialogue; + } +} diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs new file mode 100644 index 00000000..047babb9 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/InstrumentZoneInfo.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse +{ + [JsonObject] + public class InstrumentZoneInfo : DetailInfo + { + /// + /// The unique ID of the Eye Traveler associated with this instrument zone. + /// + public string id; + } +} diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs new file mode 100644 index 00000000..4e03728f --- /dev/null +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System.ComponentModel; + +namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse +{ + [JsonObject] + public class QuantumInstrumentInfo : DetailInfo + { + /// + /// The unique ID of the Eye Traveler associated with this quantum instrument. + /// + public string id; + + /// + /// A dialogue condition to set when gathering this quantum instrument. Use it in conjunction with `activationCondition` or `deactivationCondition` on other details. + /// + public string gatherCondition; + + /// + /// Allows gathering this quantum instrument using the zoomed-in signalscope, like Chert's bongos. + /// + public bool gatherWithScope; + + /// + /// The radius of the added sphere collider that will be used for interaction. + /// + [DefaultValue(0.5f)] public float interactRadius = 0.5f; + + /// + /// The furthest distance where the player can interact with this quantum instrument. + /// + [DefaultValue(2f)] public float interactRange = 2f; + } +} diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index a0c925a9..16fd0194 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -1,14 +1,57 @@ using NewHorizons.Builder.General; using NewHorizons.Components.EyeOfTheUniverse; using NewHorizons.Components.Stars; +using NewHorizons.External.Modules.Props.EyeOfTheUniverse; using NewHorizons.External.SerializableData; using NewHorizons.Utility; +using NewHorizons.Utility.OWML; +using System.Collections.Generic; +using System.Linq; using UnityEngine; namespace NewHorizons.Handlers { public static class EyeSceneHandler { + private static Dictionary _eyeTravelers = new(); + private static EyeMusicController _eyeMusicController; + + public static void Init() + { + _eyeTravelers.Clear(); + _eyeMusicController = null; + } + + public static EyeMusicController GetMusicController() + { + return _eyeMusicController; + } + + public static EyeTravelerData GetOrCreateEyeTravelerData(string id) + { + if (_eyeTravelers.TryGetValue(id, out EyeTravelerData traveler)) + { + return traveler; + } + traveler = new EyeTravelerData() + { + id = id, + info = null, + controller = null, + loopAudioSource = null, + finaleAudioSource = null, + instrumentZones = new(), + quantumInstruments = new(), + }; + _eyeTravelers[traveler.id] = traveler; + return traveler; + } + + public static List GetCustomEyeTravelers() + { + return _eyeTravelers.Values.ToList(); + } + public static void OnSceneLoad() { // Create astro objects for eye and vessel because they didn't have them somehow. @@ -134,5 +177,129 @@ namespace NewHorizons.Handlers SunLightEffectsController.AddStar(starController); SunLightEffectsController.AddStarLight(sunLight); } + + public static void SetUpEyeCampfireSequence() + { + _eyeMusicController = new GameObject("EyeMusicController").AddComponent(); + + var quantumCampsiteController = Object.FindObjectOfType(); + var cosmicInflationController = Object.FindObjectOfType(); + + _eyeMusicController.RegisterFinaleSource(cosmicInflationController._travelerFinaleSource); + + foreach (var controller in quantumCampsiteController._travelerControllers) + { + _eyeMusicController.RegisterLoopSource(controller._signal.GetOWAudioSource()); + } + + foreach (var eyeTraveler in _eyeTravelers.Values) + { + if (eyeTraveler.controller != null) + { + ArrayHelpers.Append(ref quantumCampsiteController._travelerControllers, eyeTraveler.controller); + eyeTraveler.controller.OnStartPlaying += quantumCampsiteController.OnTravelerStartPlaying; + + ArrayHelpers.Append(ref cosmicInflationController._travelers, eyeTraveler.controller); + eyeTraveler.controller.OnStartPlaying += cosmicInflationController.OnTravelerStartPlaying; + + ArrayHelpers.Append(ref cosmicInflationController._inflationObjects, eyeTraveler.controller.transform); + } + else + { + NHLogger.LogError($"Missing Eye Traveler for ID \"{eyeTraveler.id}\""); + } + + if (eyeTraveler.loopAudioSource != null) + { + _eyeMusicController.RegisterLoopSource(eyeTraveler.loopAudioSource); + } + if (eyeTraveler.finaleAudioSource != null) + { + _eyeMusicController.RegisterFinaleSource(eyeTraveler.finaleAudioSource); + } + + foreach (var quantumInstrument in eyeTraveler.quantumInstruments) + { + ArrayHelpers.Append(ref quantumInstrument._activateObjects, eyeTraveler.controller.gameObject); + ArrayHelpers.Append(ref quantumInstrument._deactivateObjects, eyeTraveler.instrumentZones.Select(z => z.gameObject)); + + var ancestorInstrumentZone = quantumInstrument.GetComponentInParent(); + if (ancestorInstrumentZone == null) + { + // Quantum instrument is not a child of an instrument zone, so treat it like its own zone + ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, quantumInstrument.gameObject); + } + } + + foreach (var instrumentZone in eyeTraveler.instrumentZones) + { + instrumentZone.gameObject.SetActive(false); + ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, instrumentZone.gameObject); + } + } + } + + public static void UpdateTravelerPositions() + { + //if (!GetCustomEyeTravelers().Any()) return; + + var quantumCampsiteController = Object.FindObjectOfType(); + + var travelers = new List() + { + quantumCampsiteController._travelerControllers[0].transform, // Riebeck + quantumCampsiteController._travelerControllers[2].transform, // Chert + quantumCampsiteController._travelerControllers[6].transform, // Esker + quantumCampsiteController._travelerControllers[1].transform, // Felspar + quantumCampsiteController._travelerControllers[3].transform, // Gabbro + }; + + if (quantumCampsiteController._hasMetSolanum) + { + travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum + } + if (quantumCampsiteController._hasMetPrisoner) + { + travelers.Add(quantumCampsiteController._travelerControllers[5].transform); // Prisoner + } + + // Custom travelers (starting at index 7) + for (int i = 7; i < quantumCampsiteController._travelerControllers.Length; i++) + { + travelers.Add(quantumCampsiteController._travelerControllers[i].transform); + } + + var radius = 2f + 0.2f * travelers.Count; + var angle = Mathf.PI * 2f / travelers.Count; + var index = 0; + + foreach (var traveler in travelers) + { + // Esker isn't at height 0 so we have to do all this + var initialY = traveler.transform.position.y; + var newPos = quantumCampsiteController.transform.TransformPoint(new Vector3( + Mathf.Cos(angle * index) * radius, + 0f, + -Mathf.Sin(angle * index) * radius + )); + newPos.y = initialY; + traveler.transform.position = newPos; + var lookTarget = quantumCampsiteController.transform.position; + lookTarget.y = newPos.y; + traveler.transform.LookAt(lookTarget, traveler.transform.up); + index++; + } + } + + public class EyeTravelerData + { + public string id; + public EyeTravelerInfo info; + public TravelerEyeController controller; + public OWAudioSource loopAudioSource; + public OWAudioSource finaleAudioSource; + public List quantumInstruments = new(); + public List instrumentZones = new(); + } } } diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index fc8b8ae7..27b67234 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -691,6 +691,11 @@ namespace NewHorizons.Handlers atmosphere = AtmosphereBuilder.Make(go, sector, body.Config.Atmosphere, surfaceSize).GetComponentInChildren(); } + if (body.Config.EyeOfTheUniverse != null) + { + EyeOfTheUniverseBuilder.Make(go, sector, body.Config.EyeOfTheUniverse, body); + } + if (body.Config.ParticleFields != null) { EffectsBuilder.Make(go, sector, body.Config); diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index c964744f..64f9a47e 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -436,7 +436,9 @@ namespace NewHorizons if (isEyeOfTheUniverse) { _playerAwake = true; + EyeSceneHandler.Init(); EyeSceneHandler.OnSceneLoad(); + EyeSceneHandler.SetUpEyeCampfireSequence(); } if (isSolarSystem || isEyeOfTheUniverse) diff --git a/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs new file mode 100644 index 00000000..e6e4ce99 --- /dev/null +++ b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs @@ -0,0 +1,23 @@ +using HarmonyLib; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; + +namespace NewHorizons.Patches.EyeScenePatches +{ + [HarmonyPatch(typeof(CosmicInflationController))] + public static class CosmicInflationControllerPatches + { + [HarmonyPostfix] + [HarmonyPatch(nameof(CosmicInflationController.UpdateFormation))] + public static void CosmicInflationController_UpdateFormation(CosmicInflationController __instance) + { + if (__instance._waitForCrossfade) + { + NHLogger.Log($"Hijacking finale cross-fade, NH will handle it"); + __instance._waitForCrossfade = false; + __instance._waitForMusicEnd = true; + EyeSceneHandler.GetMusicController().TransitionToFinale(); + } + } + } +} diff --git a/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs new file mode 100644 index 00000000..93c1b410 --- /dev/null +++ b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs @@ -0,0 +1,85 @@ +using HarmonyLib; +using NewHorizons.Handlers; +using NewHorizons.Utility.OWML; +using System.Linq; + +namespace NewHorizons.Patches.EyeScenePatches +{ + [HarmonyPatch(typeof(QuantumCampsiteController))] + public static class QuantumCampsiteControllerPatches + { + [HarmonyPostfix] + [HarmonyPatch(nameof(QuantumCampsiteController.Start))] + public static void QuantumCampsiteController_Start() + { + EyeSceneHandler.UpdateTravelerPositions(); + } + + [HarmonyPostfix] + [HarmonyPatch(nameof(QuantumCampsiteController.ActivateRemainingInstrumentZones))] + public static void QuantumCampsiteController_ActivateRemainingInstrumentZones(QuantumCampsiteController __instance) + { + // We modify this array when registering a custom instrument zone but the vanilla method only activates the first 6 + for (int i = 6; i < __instance._instrumentZones.Length; i++) + { + __instance._instrumentZones[i].SetActive(true); + } + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(QuantumCampsiteController.AreAllTravelersGathered))] + public static bool QuantumCampsiteController_AreAllTravelersGathered(QuantumCampsiteController __instance, ref bool __result) + { + bool gatheredAllHearthianTravelers = __instance._travelerControllers.Take(4).All(t => t.gameObject.activeInHierarchy); + if (!gatheredAllHearthianTravelers) + { + NHLogger.LogVerbose(""); + __result = false; + return false; + } + bool needsSolanum = __instance._hasMetSolanum; + bool gatheredSolanum = __instance._travelerControllers[QuantumCampsiteController.SOLANUM_INDEX].gameObject.activeInHierarchy; + if (needsSolanum && !gatheredSolanum) + { + __result = false; + return false; + } + bool needsPrisoner = __instance._hasMetPrisoner && !__instance._hasErasedPrisoner; + bool gatheredPrisoner = __instance._travelerControllers[QuantumCampsiteController.PRISONER_INDEX].gameObject.activeInHierarchy; + if (needsPrisoner && !gatheredPrisoner) + { + __result = false; + return false; + } + foreach (var traveler in EyeSceneHandler.GetCustomEyeTravelers()) + { + bool needsTraveler = true; + if (!string.IsNullOrEmpty(traveler.info.participatingCondition)) + { + needsTraveler = DialogueConditionManager.SharedInstance.GetConditionState(traveler.info.participatingCondition); + } + bool gatheredTraveler = traveler.controller.gameObject.activeInHierarchy; + if (needsTraveler && !gatheredTraveler) + { + __result = false; + return false; + } + } + __result = true; + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(nameof(QuantumCampsiteController.OnTravelerStartPlaying))] + public static void OnTravelerStartPlaying(QuantumCampsiteController __instance) + { + if (!__instance._hasJamSessionStarted) + { + NHLogger.Log($"NH is handling Eye sequence music"); + // Jam session is starting, start our custom music handler + EyeSceneHandler.GetMusicController().StartPlaying(); + } + // Letting the original method run in case mods have patched TravelerEyeController.OnStartCosmicJamSession() + } + } +} diff --git a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs new file mode 100644 index 00000000..8548c244 --- /dev/null +++ b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs @@ -0,0 +1,17 @@ +using HarmonyLib; + +namespace NewHorizons.Patches.EyeScenePatches +{ + [HarmonyPatch(typeof(TravelerEyeController))] + public static class TravelerEyeControllerPatches + { + [HarmonyPrefix] + [HarmonyPatch(nameof(TravelerEyeController.OnStartCosmicJamSession))] + public static bool TravelerEyeController_OnStartCosmicJamSession(TravelerEyeController __instance) + { + // Not starting the loop audio here; EyeMusicController will handle that + __instance._signal.GetOWAudioSource().SetLocalVolume(0f); + return false; + } + } +} From 13a35bb5a1c7f80451150cd7bd4fe33d5410c5af Mon Sep 17 00:00:00 2001 From: Ben C Date: Sat, 18 Jan 2025 16:11:36 +0000 Subject: [PATCH 05/39] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 1137 ++++++++++++++++++-------- 1 file changed, 780 insertions(+), 357 deletions(-) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 7ed834cb..86af5942 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -74,6 +74,10 @@ "description": "Make this planet part of the dream world", "$ref": "#/definitions/DreamModule" }, + "EyeOfTheUniverse": { + "description": "Add features exclusive to the Eye of the Universe scene", + "$ref": "#/definitions/EyeOfTheUniverseModule" + }, "FocalPoint": { "description": "Make this body into a focal point (barycenter)", "$ref": "#/definitions/FocalPointModule" @@ -850,6 +854,782 @@ } } }, + "EyeOfTheUniverseModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "eyeTravelers": { + "type": "array", + "description": "Add custom travelers to the campfire sequence", + "items": { + "$ref": "#/definitions/EyeTravelerInfo" + } + }, + "instrumentZones": { + "type": "array", + "description": "Add instrument zones which contain puzzles to gather a quantum instrument. You can parent other props to these with `parentPath` ", + "items": { + "$ref": "#/definitions/InstrumentZoneInfo" + } + }, + "quantumInstruments": { + "type": "array", + "description": "Add quantum instruments which cause their associated eye traveler to appear and instrument zones to disappear", + "items": { + "$ref": "#/definitions/QuantumInstrumentInfo" + } + } + } + }, + "EyeTravelerInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, + "keepLoaded": { + "type": "boolean", + "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." + }, + "hasPhysics": { + "type": "boolean", + "description": "Should this object dynamically move around?\nThis tries to make all mesh colliders convex, as well as adding a sphere collider in case the detail has no others." + }, + "physicsMass": { + "type": "number", + "description": "The mass of the physics object.\nMost pushable props use the default value, which matches the player mass.", + "format": "float", + "default": 0.001 + }, + "physicsRadius": { + "type": "number", + "description": "The radius that the added sphere collider will use for physics collision.\nIf there's already good colliders on the detail, you can make this 0.", + "format": "float", + "default": 1.0 + }, + "physicsSuspendUntilImpact": { + "type": "boolean", + "description": "If true, this detail will stay still until it touches something.\nGood for zero-g props.", + "default": false + }, + "ignoreSun": { + "type": "boolean", + "description": "Set to true if this object's lighting should ignore the effects of sunlight" + }, + "activationCondition": { + "type": "string", + "description": "Activates this game object when the dialogue condition is met" + }, + "deactivationCondition": { + "type": "string", + "description": "Deactivates this game object when the dialogue condition is met" + }, + "blinkWhenActiveChanged": { + "type": "boolean", + "description": "Should the player close their eyes while the activation state changes. Only relevant if activationCondition or deactivationCondition are set.", + "default": true + }, + "item": { + "description": "Should this detail be treated as an interactible item", + "$ref": "#/definitions/ItemInfo" + }, + "itemSocket": { + "description": "Should this detail be treated as a socket for an interactible item", + "$ref": "#/definitions/ItemSocketInfo" + }, + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "id": { + "type": "string", + "description": "A unique ID to associate this traveler with their corresponding quantum instruments and instrument zones. Must be unique for each traveler." + }, + "startPlayingCondition": { + "type": "string", + "description": "The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler." + }, + "participatingCondition": { + "type": "string", + "description": "If specified, this dialogue condition must be set for the traveler to participate in the campfire song. Otherwise, the song will be able to start without them." + }, + "loopAudio": { + "type": "string", + "description": "The audio to use for the traveler while playing around the campfire (and also for their paired quantum instrument). It should be 16 measures at 92 BPM (approximately 42 seconds long). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "finaleAudio": { + "type": "string", + "description": "The audio to use for the traveler during the finale of the campfire song. It should be 8 measures of the main loop at 92 BPM followed by 2 measures of fade-out (approximately 26 seconds long in total). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "frequency": { + "type": "string", + "description": "The frequency ID of the signal emitted by the traveler. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`,\n`Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. Defaults to `Traveler`. You can also put a custom value." + }, + "dialogue": { + "description": "The dialogue to use for this traveler. Omit this or set it to null if your traveler already has valid dialogue.", + "$ref": "#/definitions/DialogueInfo" + } + } + }, + "ItemInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "The name of the item to be displayed in the UI. Defaults to the name of the detail object." + }, + "itemType": { + "type": "string", + "description": "The type of the item, which determines its orientation when held and what sockets it fits into. This can be a custom string, or a vanilla ItemType (Scroll, WarpCore, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch). Defaults to the item name." + }, + "interactRange": { + "type": "number", + "description": "The furthest distance where the player can interact with this item. Defaults to two meters, same as most vanilla items. Set this to zero to disable all interaction by default.", + "format": "float", + "default": 2.0 + }, + "colliderRadius": { + "type": "number", + "description": "The radius that the added sphere collider will use for collision and hover detection.\nIf there's already a collider on the detail, you can make this 0.", + "format": "float", + "default": 0.5 + }, + "droppable": { + "type": "boolean", + "description": "Whether the item can be dropped. Defaults to true.", + "default": true + }, + "dropOffset": { + "description": "A relative offset to apply to the item's position when dropping it on the ground.", + "$ref": "#/definitions/MVector3" + }, + "dropNormal": { + "description": "The direction the item will be oriented when dropping it on the ground. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "holdOffset": { + "description": "A relative offset to apply to the item's position when holding it. The initial position varies for vanilla item types.", + "$ref": "#/definitions/MVector3" + }, + "holdRotation": { + "description": "A relative offset to apply to the item's rotation when holding it.", + "$ref": "#/definitions/MVector3" + }, + "socketOffset": { + "description": "A relative offset to apply to the item's position when placing it into a socket.", + "$ref": "#/definitions/MVector3" + }, + "socketRotation": { + "description": "A relative offset to apply to the item's rotation when placing it into a socket.", + "$ref": "#/definitions/MVector3" + }, + "pickupAudio": { + "type": "string", + "description": "The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "dropAudio": { + "type": "string", + "description": "The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "socketAudio": { + "type": "string", + "description": "The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "unsocketAudio": { + "type": "string", + "description": "The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "pickupCondition": { + "type": "string", + "description": "A dialogue condition to set when picking up this item." + }, + "clearPickupConditionOnDrop": { + "type": "boolean", + "description": "Whether the pickup condition should be cleared when dropping the item. Defaults to true.", + "default": true + }, + "pickupFact": { + "type": "string", + "description": "A ship log fact to reveal when picking up this item." + }, + "pathToInitialSocket": { + "type": "string", + "description": "A relative path from the planet to a socket that this item will be automatically inserted into." + } + } + }, + "ItemSocketInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "socketPath": { + "type": "string", + "description": "The relative path to a child game object of this detail that will act as the socket point for the item. Will be used instead of this socket's positioning info if set." + }, + "itemType": { + "type": "string", + "description": "The type of item allowed in this socket. This can be a custom string, or a vanilla ItemType (Scroll, WarpCode, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch)." + }, + "interactRange": { + "type": "number", + "description": "The furthest distance where the player can interact with this item socket. Defaults to two meters, same as most vanilla item sockets. Set this to zero to disable all interaction by default.", + "format": "float", + "default": 2.0 + }, + "useGiveTakePrompts": { + "type": "boolean", + "description": "Whether to use \"Give Item\" / \"Take Item\" prompts instead of \"Insert Item\" / \"Remove Item\"." + }, + "insertCondition": { + "type": "string", + "description": "A dialogue condition to set when inserting an item into this socket." + }, + "clearInsertConditionOnRemoval": { + "type": "boolean", + "description": "Whether the insert condition should be cleared when removing the socketed item. Defaults to true.", + "default": true + }, + "insertFact": { + "type": "string", + "description": "A ship log fact to reveal when inserting an item into this socket." + }, + "removalCondition": { + "type": "string", + "description": "A dialogue condition to set when removing an item from this socket, or when the socket is empty." + }, + "clearRemovalConditionOnInsert": { + "type": "boolean", + "description": "Whether the removal condition should be cleared when inserting a socketed item. Defaults to true.", + "default": true + }, + "removalFact": { + "type": "string", + "description": "A ship log fact to reveal when removing an item from this socket, or when the socket is empty." + }, + "colliderRadius": { + "type": "number", + "description": "Default collider radius when interacting with the socket", + "format": "float", + "default": 0.0 + } + } + }, + "DialogueInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "blockAfterPersistentCondition": { + "type": "string", + "description": "Prevents the dialogue from being created after a specific persistent condition is set. Useful for remote dialogue\ntriggers that you want to have happen only once." + }, + "lookAtRadius": { + "type": "number", + "description": "If a pathToAnimController is supplied, if you are within this distance the character will look at you. If it is set\nto 0, they will only look at you when spoken to.", + "format": "float" + }, + "pathToAnimController": { + "type": "string", + "description": "If this dialogue is meant for a character, this is the relative path from the planet to that character's\nCharacterAnimController, TravelerController, TravelerEyeController (eye of the universe), FacePlayerWhenTalking, \nHearthianRecorderEffects or SolanumAnimController.\n\nIf it's a Recorder this will also delete the existing dialogue already attached to that prop.\n\nIf none of those components are present it will add a FacePlayerWhenTalking component.\n\n`pathToAnimController` also makes the dialogue into a child of the anim controller. This can be used with `isRelativeToParent`\nto position the dialogue on relative to the speaker. If you also provide `parentPath`, that will instead override which object \nis the parent, but the anim controller will otherwise function as expected." + }, + "pathToExistingDialogue": { + "type": "string", + "description": "If this dialogue is adding to existing character dialogue, put a path to the game object with the dialogue on it here" + }, + "radius": { + "type": "number", + "description": "Radius of the spherical collision volume where you get the \"talk to\" prompt when looking at. If you use a\nremoteTrigger, you can set this to 0 to make the dialogue only trigger remotely.", + "format": "float" + }, + "range": { + "type": "number", + "description": "Distance from radius the prompt appears", + "format": "float", + "default": 2.0 + }, + "attentionPoint": { + "description": "The point that the camera looks at when dialogue advances.", + "$ref": "#/definitions/AttentionPointInfo" + }, + "swappedAttentionPoints": { + "type": "array", + "description": "Additional points that the camera looks at when dialogue advances through specific dialogue nodes and pages.", + "items": { + "$ref": "#/definitions/SwappedAttentionPointInfo" + } + }, + "remoteTrigger": { + "description": "Allows you to trigger dialogue from a distance when you walk into an area.", + "$ref": "#/definitions/RemoteTriggerInfo" + }, + "xmlFile": { + "type": "string", + "description": "Relative path to the xml file defining the dialogue." + }, + "flashlightToggle": { + "description": "What type of flashlight toggle to do when dialogue is interacted with", + "default": "none", + "$ref": "#/definitions/FlashlightToggle" + } + } + }, + "AttentionPointInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "offset": { + "description": "An additional offset to apply to apply when the camera looks at this attention point.", + "$ref": "#/definitions/MVector3" + } + } + }, + "SwappedAttentionPointInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "offset": { + "description": "An additional offset to apply to apply when the camera looks at this attention point.", + "$ref": "#/definitions/MVector3" + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "dialogueNode": { + "type": "string", + "description": "The name of the dialogue node to activate this attention point for. If null or blank, activates for every node." + }, + "dialoguePage": { + "type": "integer", + "description": "The index of the page in the current dialogue node to activate this attention point for, if the node has multiple pages.", + "format": "int32" + }, + "lookEasing": { + "type": "number", + "description": "The easing factor which determines how 'snappy' the camera is when looking at the attention point.", + "format": "float", + "default": 1 + } + } + }, + "RemoteTriggerInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "radius": { + "type": "number", + "description": "The radius of the remote trigger volume.", + "format": "float" + }, + "prereqCondition": { + "type": "string", + "description": "This condition must be met for the remote trigger volume to trigger." + } + } + }, + "FlashlightToggle": { + "type": "string", + "description": "", + "x-enumNames": [ + "TurnOff", + "TurnOffThenOn", + "None" + ], + "enum": [ + "turnOff", + "turnOffThenOn", + "none" + ] + }, + "InstrumentZoneInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, + "keepLoaded": { + "type": "boolean", + "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." + }, + "hasPhysics": { + "type": "boolean", + "description": "Should this object dynamically move around?\nThis tries to make all mesh colliders convex, as well as adding a sphere collider in case the detail has no others." + }, + "physicsMass": { + "type": "number", + "description": "The mass of the physics object.\nMost pushable props use the default value, which matches the player mass.", + "format": "float", + "default": 0.001 + }, + "physicsRadius": { + "type": "number", + "description": "The radius that the added sphere collider will use for physics collision.\nIf there's already good colliders on the detail, you can make this 0.", + "format": "float", + "default": 1.0 + }, + "physicsSuspendUntilImpact": { + "type": "boolean", + "description": "If true, this detail will stay still until it touches something.\nGood for zero-g props.", + "default": false + }, + "ignoreSun": { + "type": "boolean", + "description": "Set to true if this object's lighting should ignore the effects of sunlight" + }, + "activationCondition": { + "type": "string", + "description": "Activates this game object when the dialogue condition is met" + }, + "deactivationCondition": { + "type": "string", + "description": "Deactivates this game object when the dialogue condition is met" + }, + "blinkWhenActiveChanged": { + "type": "boolean", + "description": "Should the player close their eyes while the activation state changes. Only relevant if activationCondition or deactivationCondition are set.", + "default": true + }, + "item": { + "description": "Should this detail be treated as an interactible item", + "$ref": "#/definitions/ItemInfo" + }, + "itemSocket": { + "description": "Should this detail be treated as a socket for an interactible item", + "$ref": "#/definitions/ItemSocketInfo" + }, + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "id": { + "type": "string", + "description": "The unique ID of the Eye Traveler associated with this instrument zone." + } + } + }, + "QuantumInstrumentInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, + "keepLoaded": { + "type": "boolean", + "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." + }, + "hasPhysics": { + "type": "boolean", + "description": "Should this object dynamically move around?\nThis tries to make all mesh colliders convex, as well as adding a sphere collider in case the detail has no others." + }, + "physicsMass": { + "type": "number", + "description": "The mass of the physics object.\nMost pushable props use the default value, which matches the player mass.", + "format": "float", + "default": 0.001 + }, + "physicsRadius": { + "type": "number", + "description": "The radius that the added sphere collider will use for physics collision.\nIf there's already good colliders on the detail, you can make this 0.", + "format": "float", + "default": 1.0 + }, + "physicsSuspendUntilImpact": { + "type": "boolean", + "description": "If true, this detail will stay still until it touches something.\nGood for zero-g props.", + "default": false + }, + "ignoreSun": { + "type": "boolean", + "description": "Set to true if this object's lighting should ignore the effects of sunlight" + }, + "activationCondition": { + "type": "string", + "description": "Activates this game object when the dialogue condition is met" + }, + "deactivationCondition": { + "type": "string", + "description": "Deactivates this game object when the dialogue condition is met" + }, + "blinkWhenActiveChanged": { + "type": "boolean", + "description": "Should the player close their eyes while the activation state changes. Only relevant if activationCondition or deactivationCondition are set.", + "default": true + }, + "item": { + "description": "Should this detail be treated as an interactible item", + "$ref": "#/definitions/ItemInfo" + }, + "itemSocket": { + "description": "Should this detail be treated as a socket for an interactible item", + "$ref": "#/definitions/ItemSocketInfo" + }, + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "id": { + "type": "string", + "description": "The unique ID of the Eye Traveler associated with this quantum instrument." + }, + "gatherCondition": { + "type": "string", + "description": "A dialogue condition to set when gathering this quantum instrument. Use it in conjunction with `activationCondition` or `deactivationCondition` on other details." + }, + "gatherWithScope": { + "type": "boolean", + "description": "Allows gathering this quantum instrument using the zoomed-in signalscope, like Chert's bongos." + }, + "interactRadius": { + "type": "number", + "description": "The radius of the added sphere collider that will be used for interaction.", + "format": "float", + "default": 0.5 + }, + "interactRange": { + "type": "number", + "description": "The furthest distance where the player can interact with this quantum instrument.", + "format": "float", + "default": 2.0 + } + } + }, "FocalPointModule": { "type": "object", "additionalProperties": false, @@ -1523,363 +2303,6 @@ } } }, - "ItemInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "name": { - "type": "string", - "description": "The name of the item to be displayed in the UI. Defaults to the name of the detail object." - }, - "itemType": { - "type": "string", - "description": "The type of the item, which determines its orientation when held and what sockets it fits into. This can be a custom string, or a vanilla ItemType (Scroll, WarpCore, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch). Defaults to the item name." - }, - "interactRange": { - "type": "number", - "description": "The furthest distance where the player can interact with this item. Defaults to two meters, same as most vanilla items. Set this to zero to disable all interaction by default.", - "format": "float", - "default": 2.0 - }, - "colliderRadius": { - "type": "number", - "description": "The radius that the added sphere collider will use for collision and hover detection.\nIf there's already a collider on the detail, you can make this 0.", - "format": "float", - "default": 0.5 - }, - "droppable": { - "type": "boolean", - "description": "Whether the item can be dropped. Defaults to true.", - "default": true - }, - "dropOffset": { - "description": "A relative offset to apply to the item's position when dropping it on the ground.", - "$ref": "#/definitions/MVector3" - }, - "dropNormal": { - "description": "The direction the item will be oriented when dropping it on the ground. Defaults to up (0, 1, 0).", - "$ref": "#/definitions/MVector3" - }, - "holdOffset": { - "description": "A relative offset to apply to the item's position when holding it. The initial position varies for vanilla item types.", - "$ref": "#/definitions/MVector3" - }, - "holdRotation": { - "description": "A relative offset to apply to the item's rotation when holding it.", - "$ref": "#/definitions/MVector3" - }, - "socketOffset": { - "description": "A relative offset to apply to the item's position when placing it into a socket.", - "$ref": "#/definitions/MVector3" - }, - "socketRotation": { - "description": "A relative offset to apply to the item's rotation when placing it into a socket.", - "$ref": "#/definitions/MVector3" - }, - "pickupAudio": { - "type": "string", - "description": "The audio to play when this item is picked up. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "dropAudio": { - "type": "string", - "description": "The audio to play when this item is dropped. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "socketAudio": { - "type": "string", - "description": "The audio to play when this item is inserted into a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "unsocketAudio": { - "type": "string", - "description": "The audio to play when this item is removed from a socket. Only applies to custom/non-vanilla item types.\nCan be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "pickupCondition": { - "type": "string", - "description": "A dialogue condition to set when picking up this item." - }, - "clearPickupConditionOnDrop": { - "type": "boolean", - "description": "Whether the pickup condition should be cleared when dropping the item. Defaults to true.", - "default": true - }, - "pickupFact": { - "type": "string", - "description": "A ship log fact to reveal when picking up this item." - }, - "pathToInitialSocket": { - "type": "string", - "description": "A relative path from the planet to a socket that this item will be automatically inserted into." - } - } - }, - "ItemSocketInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "rotation": { - "description": "Rotation of the object", - "$ref": "#/definitions/MVector3" - }, - "alignRadial": { - "type": [ - "boolean", - "null" - ], - "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." - }, - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, - "socketPath": { - "type": "string", - "description": "The relative path to a child game object of this detail that will act as the socket point for the item. Will be used instead of this socket's positioning info if set." - }, - "itemType": { - "type": "string", - "description": "The type of item allowed in this socket. This can be a custom string, or a vanilla ItemType (Scroll, WarpCode, SharedStone, ConversationStone, Lantern, SlideReel, DreamLantern, or VisionTorch)." - }, - "interactRange": { - "type": "number", - "description": "The furthest distance where the player can interact with this item socket. Defaults to two meters, same as most vanilla item sockets. Set this to zero to disable all interaction by default.", - "format": "float", - "default": 2.0 - }, - "useGiveTakePrompts": { - "type": "boolean", - "description": "Whether to use \"Give Item\" / \"Take Item\" prompts instead of \"Insert Item\" / \"Remove Item\"." - }, - "insertCondition": { - "type": "string", - "description": "A dialogue condition to set when inserting an item into this socket." - }, - "clearInsertConditionOnRemoval": { - "type": "boolean", - "description": "Whether the insert condition should be cleared when removing the socketed item. Defaults to true.", - "default": true - }, - "insertFact": { - "type": "string", - "description": "A ship log fact to reveal when inserting an item into this socket." - }, - "removalCondition": { - "type": "string", - "description": "A dialogue condition to set when removing an item from this socket, or when the socket is empty." - }, - "clearRemovalConditionOnInsert": { - "type": "boolean", - "description": "Whether the removal condition should be cleared when inserting a socketed item. Defaults to true.", - "default": true - }, - "removalFact": { - "type": "string", - "description": "A ship log fact to reveal when removing an item from this socket, or when the socket is empty." - }, - "colliderRadius": { - "type": "number", - "description": "Default collider radius when interacting with the socket", - "format": "float", - "default": 0.0 - } - } - }, - "DialogueInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, - "blockAfterPersistentCondition": { - "type": "string", - "description": "Prevents the dialogue from being created after a specific persistent condition is set. Useful for remote dialogue\ntriggers that you want to have happen only once." - }, - "lookAtRadius": { - "type": "number", - "description": "If a pathToAnimController is supplied, if you are within this distance the character will look at you. If it is set\nto 0, they will only look at you when spoken to.", - "format": "float" - }, - "pathToAnimController": { - "type": "string", - "description": "If this dialogue is meant for a character, this is the relative path from the planet to that character's\nCharacterAnimController, TravelerController, TravelerEyeController (eye of the universe), FacePlayerWhenTalking, \nHearthianRecorderEffects or SolanumAnimController.\n\nIf it's a Recorder this will also delete the existing dialogue already attached to that prop.\n\nIf none of those components are present it will add a FacePlayerWhenTalking component.\n\n`pathToAnimController` also makes the dialogue into a child of the anim controller. This can be used with `isRelativeToParent`\nto position the dialogue on relative to the speaker. If you also provide `parentPath`, that will instead override which object \nis the parent, but the anim controller will otherwise function as expected." - }, - "pathToExistingDialogue": { - "type": "string", - "description": "If this dialogue is adding to existing character dialogue, put a path to the game object with the dialogue on it here" - }, - "radius": { - "type": "number", - "description": "Radius of the spherical collision volume where you get the \"talk to\" prompt when looking at. If you use a\nremoteTrigger, you can set this to 0 to make the dialogue only trigger remotely.", - "format": "float" - }, - "range": { - "type": "number", - "description": "Distance from radius the prompt appears", - "format": "float", - "default": 2.0 - }, - "attentionPoint": { - "description": "The point that the camera looks at when dialogue advances.", - "$ref": "#/definitions/AttentionPointInfo" - }, - "swappedAttentionPoints": { - "type": "array", - "description": "Additional points that the camera looks at when dialogue advances through specific dialogue nodes and pages.", - "items": { - "$ref": "#/definitions/SwappedAttentionPointInfo" - } - }, - "remoteTrigger": { - "description": "Allows you to trigger dialogue from a distance when you walk into an area.", - "$ref": "#/definitions/RemoteTriggerInfo" - }, - "xmlFile": { - "type": "string", - "description": "Relative path to the xml file defining the dialogue." - }, - "flashlightToggle": { - "description": "What type of flashlight toggle to do when dialogue is interacted with", - "default": "none", - "$ref": "#/definitions/FlashlightToggle" - } - } - }, - "AttentionPointInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, - "offset": { - "description": "An additional offset to apply to apply when the camera looks at this attention point.", - "$ref": "#/definitions/MVector3" - } - } - }, - "SwappedAttentionPointInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "offset": { - "description": "An additional offset to apply to apply when the camera looks at this attention point.", - "$ref": "#/definitions/MVector3" - }, - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, - "dialogueNode": { - "type": "string", - "description": "The name of the dialogue node to activate this attention point for. If null or blank, activates for every node." - }, - "dialoguePage": { - "type": "integer", - "description": "The index of the page in the current dialogue node to activate this attention point for, if the node has multiple pages.", - "format": "int32" - }, - "lookEasing": { - "type": "number", - "description": "The easing factor which determines how 'snappy' the camera is when looking at the attention point.", - "format": "float", - "default": 1 - } - } - }, - "RemoteTriggerInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, - "radius": { - "type": "number", - "description": "The radius of the remote trigger volume.", - "format": "float" - }, - "prereqCondition": { - "type": "string", - "description": "This condition must be met for the remote trigger volume to trigger." - } - } - }, - "FlashlightToggle": { - "type": "string", - "description": "", - "x-enumNames": [ - "TurnOff", - "TurnOffThenOn", - "None" - ], - "enum": [ - "turnOff", - "turnOffThenOn", - "none" - ] - }, "EntryLocationInfo": { "type": "object", "additionalProperties": false, From e0f79ae3ea45f5aa89cb6bc9d2978591154b7df0 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Sat, 18 Jan 2025 17:26:02 -0600 Subject: [PATCH 06/39] Eye sequence fixes --- .../Builder/Props/EyeOfTheUniverseBuilder.cs | 28 +++-- .../EyeOfTheUniverse/EyeMusicController.cs | 119 +++++++++++------- .../Props/EyeOfTheUniverse/EyeTravelerInfo.cs | 5 + NewHorizons/Handlers/EyeDetailCacher.cs | 30 +++++ NewHorizons/Handlers/EyeSceneHandler.cs | 10 +- NewHorizons/Main.cs | 3 +- .../CosmicInflationControllerPatches.cs | 3 +- .../QuantumCampsiteControllerPatches.cs | 7 +- .../TravelerEyeControllerPatches.cs | 6 + 9 files changed, 153 insertions(+), 58 deletions(-) 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; From bba0a1a42b56009de0b9a76638c8e7e70bdf6bbf Mon Sep 17 00:00:00 2001 From: Ben C Date: Sat, 18 Jan 2025 23:28:04 +0000 Subject: [PATCH 07/39] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 86af5942..73c73cf9 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -995,6 +995,10 @@ "type": "string", "description": "A unique ID to associate this traveler with their corresponding quantum instruments and instrument zones. Must be unique for each traveler." }, + "name": { + "type": "string", + "description": "The name to display for this traveler's signals. Defaults to the name of the detail." + }, "startPlayingCondition": { "type": "string", "description": "The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler." From 06870db051b4bc6f1c355d74af1f8e5d1e6c4c9e Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 28 Jan 2025 23:02:44 -0500 Subject: [PATCH 08/39] Fix raft docks not working on NH rafts --- NewHorizons/Builder/Props/DetailBuilder.cs | 9 +++++++ NewHorizons/Builder/Props/RaftBuilder.cs | 3 +++ .../RaftControllerPatches.cs | 27 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 2b7dbfde..ffdefcb5 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -459,6 +459,15 @@ namespace NewHorizons.Builder.Props { component.gameObject.AddComponent(); } + else if (component is RaftDock dock) + { + // These flood toggles are to disable flooded docks on the Stranger + // Presumably the user isn't making one of those + foreach (var toggle in dock.GetComponents()) + { + Component.DestroyImmediate(toggle); + } + } } /// diff --git a/NewHorizons/Builder/Props/RaftBuilder.cs b/NewHorizons/Builder/Props/RaftBuilder.cs index 98196885..59f734bf 100644 --- a/NewHorizons/Builder/Props/RaftBuilder.cs +++ b/NewHorizons/Builder/Props/RaftBuilder.cs @@ -66,6 +66,9 @@ namespace NewHorizons.Builder.Props var waterVolume = planetGO.GetComponentInChildren(); fluidDetector._alignmentFluid = waterVolume; fluidDetector._buoyancy.checkAgainstWaves = true; + // Rafts were unable to trigger docks because these were disabled for some reason + fluidDetector.GetComponent().enabled = true; + fluidDetector.GetComponent().enabled = true; // Light sensors foreach (var lightSensor in raftObject.GetComponentsInChildren()) diff --git a/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs b/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs index e615c201..48a08f08 100644 --- a/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs +++ b/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using NewHorizons.Components.Props; using UnityEngine; namespace NewHorizons.Patches.EchoesOfTheEyePatches @@ -73,5 +74,31 @@ namespace NewHorizons.Patches.EchoesOfTheEyePatches return false; } + + [HarmonyPostfix] + [HarmonyPatch(nameof(RaftController.UpdateMoveToTarget))] + public static void UpdateMoveToTarget(RaftController __instance) + { + // If it has a riverFluid volume then its a regular stranger one + if (__instance._movingToTarget && __instance._riverFluid == null) + { + OWRigidbody raftBody = __instance._raftBody; + OWRigidbody origParentBody = __instance._raftBody.GetOrigParentBody(); + Transform transform = origParentBody.transform; + Vector3 vector = transform.TransformPoint(__instance._targetLocalPosition); + + // Base game threshold has this at 1f (after doing smoothstep on it) + // For whatever reason it never hits that for NH planets (probably since they're moving so much compared to the steady velocity of the Stranger) + // Might break for somebody with a wacky spinning planet in which case we can adjust this or add some kind of fallback (i.e., wait x seconds and then just say its there) + // Fixes #1005 + if (__instance.currentDistanceLerp > 0.999f) + { + raftBody.SetPosition(vector); + raftBody.SetRotation(transform.rotation * __instance._targetLocalRotation); + __instance.StopMovingToTarget(); + __instance.OnArriveAtTarget.Invoke(); + } + } + } } } From f9b517d7b197db0dd63fa44b272e6af13aa9fce0 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 28 Jan 2025 23:03:11 -0500 Subject: [PATCH 09/39] Update manifest.json --- NewHorizons/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index b1487c71..4454f483 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, JohnCorby, MegaPiggy, Trifid, and friends", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.25.1", + "version": "1.25.2", "owmlVersion": "2.12.1", "dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "PacificEngine.OW_CommonResources" ], From 3746e794f60477ef7e56f923bbc2dfec4517b2d6 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Wed, 29 Jan 2025 19:35:13 -0600 Subject: [PATCH 10/39] Make eye traveler and quantum instrument signals full SignalInfos --- .../Builder/Props/EyeOfTheUniverseBuilder.cs | 148 +++++++++++++----- .../Props/EyeOfTheUniverse/EyeTravelerInfo.cs | 23 +-- .../EyeOfTheUniverse/QuantumInstrumentInfo.cs | 6 + NewHorizons/Handlers/EyeSceneHandler.cs | 22 ++- .../CosmicInflationControllerPatches.cs | 2 +- .../QuantumCampsiteControllerPatches.cs | 6 +- .../TravelerEyeControllerPatches.cs | 2 +- 7 files changed, 146 insertions(+), 63 deletions(-) diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs index e5967672..1e7b3e16 100644 --- a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -16,6 +16,25 @@ namespace NewHorizons.Builder.Props { public static TravelerEyeController MakeEyeTraveler(GameObject planetGO, Sector sector, EyeTravelerInfo info, NewHorizonsBody nhBody) { + var travelerData = EyeSceneHandler.CreateEyeTravelerData(info.id); + travelerData.info = info; + travelerData.requirementsMet = true; + + if (!string.IsNullOrEmpty(info.requiredFact) && !ShipLogHandler.KnowsFact(info.requiredFact)) + { + travelerData.requirementsMet = false; + } + + if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && DialogueConditionManager.SharedInstance.GetConditionState(info.requiredPersistentCondition)) + { + travelerData.requirementsMet = false; + } + + if (!travelerData.requirementsMet) + { + return null; + } + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); if (string.IsNullOrEmpty(info.name)) @@ -39,8 +58,12 @@ 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 (info.dialogue.position == null && info.dialogue.parentPath == null) + { + info.dialogue.isRelativeToParent = true; + } + GeneralPropBuilder.MakeFromExisting(dialogueTree.gameObject, planetGO, sector, info.dialogue, defaultParent: go.transform); + if (travelerController._dialogueTree != null) { travelerController._dialogueTree.OnStartConversation -= travelerController.OnStartConversation; @@ -52,36 +75,47 @@ namespace NewHorizons.Builder.Props } else if (travelerController._dialogueTree == null) { - NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any dialogue set"); + travelerController._dialogueTree = go.GetComponentInChildren(); + if (travelerController._dialogueTree == null) + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any dialogue set"); + } } + travelerData.controller = travelerController; + OWAudioSource loopAudioSource = null; - if (!string.IsNullOrEmpty(info.loopAudio)) + if (info.signal != null) { - var signalInfo = new SignalInfo() + if (string.IsNullOrEmpty(info.signal.name)) { - name = info.name, - audio = info.loopAudio, - detectionRadius = 10f, - identificationRadius = 10f, - onlyAudibleToScope = false, - frequency = string.IsNullOrEmpty(info.frequency) ? "Traveler" : info.frequency, - }; - var signalGO = SignalBuilder.Make(planetGO, sector, signalInfo, nhBody.Mod); - signalGO.transform.SetParent(travelerController.transform, false); - signalGO.transform.localPosition = Vector3.zero; + info.signal.name = info.name; + } + if (string.IsNullOrEmpty(info.signal.frequency)) + { + info.signal.frequency = "Traveler"; + } + var signalGO = SignalBuilder.Make(planetGO, sector, info.signal, nhBody.Mod); + if (info.signal.position == null && info.signal.parentPath == null) + { + info.signal.isRelativeToParent = true; + } + GeneralPropBuilder.MakeFromExisting(signalGO, planetGO, sector, info.signal, defaultParent: go.transform); var signal = signalGO.GetComponent(); travelerController._signal = signal; signal.SetSignalActivation(false); loopAudioSource = signal.GetOWAudioSource(); + } else if (travelerController._signal == null) { NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); } + travelerData.loopAudioSource = loopAudioSource; + OWAudioSource finaleAudioSource = null; if (!string.IsNullOrEmpty(info.finaleAudio)) @@ -100,10 +134,6 @@ namespace NewHorizons.Builder.Props finaleAudioSource.gameObject.SetActive(true); } - var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); - travelerData.info = info; - travelerData.controller = travelerController; - travelerData.loopAudioSource = loopAudioSource; travelerData.finaleAudioSource = finaleAudioSource; return travelerController; @@ -111,6 +141,13 @@ namespace NewHorizons.Builder.Props public static QuantumInstrument MakeQuantumInstrument(GameObject planetGO, Sector sector, QuantumInstrumentInfo info, NewHorizonsBody nhBody) { + var travelerData = EyeSceneHandler.GetEyeTravelerData(info.id); + + if (travelerData != null && !travelerData.requirementsMet) + { + return null; + } + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); go.layer = Layer.Interactible; if (info.interactRadius > 0f) @@ -129,46 +166,71 @@ namespace NewHorizons.Builder.Props var trigger = go.AddComponent(); trigger.gatherCondition = info.gatherCondition; - var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); - travelerData.quantumInstruments.Add(quantumInstrument); - - if (travelerData.info != null) + if (travelerData != null) { - if (!string.IsNullOrEmpty(travelerData.info.loopAudio)) - { - 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, - }; - var signalGO = SignalBuilder.Make(planetGO, sector, signalInfo, nhBody.Mod); - signalGO.transform.SetParent(quantumInstrument.transform, false); - signalGO.transform.localPosition = Vector3.zero; - } - else - { - NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); - } + travelerData.quantumInstruments.Add(quantumInstrument); } else { NHLogger.LogError($"Quantum instrument with ID \"{info.id}\" has no matching eye traveler"); } + info.signal ??= new SignalInfo(); + + if (travelerData?.info != null && travelerData.info.signal != null) + { + if (string.IsNullOrEmpty(info.signal.name)) + { + info.signal.name = travelerData.info.name; + } + if (string.IsNullOrEmpty(info.signal.audio)) + { + info.signal.audio = travelerData.info.signal.audio; + } + if (string.IsNullOrEmpty(info.signal.frequency)) + { + info.signal.frequency = travelerData.info.signal.frequency; + } + } + + if (!string.IsNullOrEmpty(info.signal.audio)) + { + var signalGO = SignalBuilder.Make(planetGO, sector, info.signal, nhBody.Mod); + if (info.signal.position == null && info.signal.parentPath == null) + { + info.signal.isRelativeToParent = true; + } + GeneralPropBuilder.MakeFromExisting(signalGO, planetGO, sector, info.signal, defaultParent: go.transform); + } + else + { + NHLogger.LogError($"Eye Traveler with ID \"{info.id}\" does not have any loop audio set"); + } + return quantumInstrument; } public static InstrumentZone MakeInstrumentZone(GameObject planetGO, Sector sector, InstrumentZoneInfo info, NewHorizonsBody nhBody) { + var travelerData = EyeSceneHandler.GetEyeTravelerData(info.id); + + if (travelerData != null && !travelerData.requirementsMet) + { + return null; + } + var go = DetailBuilder.Make(planetGO, sector, nhBody.Mod, info); var instrumentZone = go.AddComponent(); - var travelerData = EyeSceneHandler.GetOrCreateEyeTravelerData(info.id); - travelerData.instrumentZones.Add(instrumentZone); + if (travelerData != null) + { + travelerData.instrumentZones.Add(instrumentZone); + } + else + { + NHLogger.LogError($"Instrument zone with ID \"{info.id}\" has no matching eye traveler"); + } return instrumentZone; } diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs index 41655e3a..e917f839 100644 --- a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs @@ -1,3 +1,4 @@ +using NewHorizons.External.Modules.Props.Audio; using NewHorizons.External.Modules.Props.Dialogue; using Newtonsoft.Json; @@ -16,6 +17,16 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse /// public string name; + /// + /// If set, the player must know this ship log fact for this traveler (and their instrument zones and quantum instruments) to appear. The fact does not need to exist in the current star system; the player's save data will be checked directly. + /// + public string requiredFact; + + /// + /// If set, the player must have this persistent dialogue condition set for this traveler (and their instrument zones and quantum instruments) to appear. + /// + public string requiredPersistentCondition; + /// /// The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler. /// @@ -27,9 +38,9 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse public string participatingCondition; /// - /// The audio to use for the traveler while playing around the campfire (and also for their paired quantum instrument). It should be 16 measures at 92 BPM (approximately 42 seconds long). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// The audio signal to use for the traveler while playing around the campfire (and also for their paired quantum instrument if another is not specified). The audio clip should be 16 measures at 92 BPM (approximately 42 seconds long). /// - public string loopAudio; + public SignalInfo signal; /// /// The audio to use for the traveler during the finale of the campfire song. It should be 8 measures of the main loop at 92 BPM followed by 2 measures of fade-out (approximately 26 seconds long in total). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. @@ -37,13 +48,7 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse public string finaleAudio; /// - /// The frequency ID of the signal emitted by the traveler. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`, - /// `Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. Defaults to `Traveler`. You can also put a custom value. - /// - public string frequency; - - /// - /// The dialogue to use for this traveler. Omit this or set it to null if your traveler already has valid dialogue. + /// The dialogue to use for this traveler. If omitted, the first CharacterDialogueTree in the object will be used. /// public DialogueInfo dialogue; } diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs index 4e03728f..1a686b9c 100644 --- a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/QuantumInstrumentInfo.cs @@ -1,3 +1,4 @@ +using NewHorizons.External.Modules.Props.Audio; using Newtonsoft.Json; using System.ComponentModel; @@ -21,6 +22,11 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse /// public bool gatherWithScope; + /// + /// The audio signal emitted by this quantum instrument. The fields `name`, `audio`, and `frequency` will be copied from the corresponding Eye Traveler's signal if not specified here. + /// + public SignalInfo signal; + /// /// The radius of the added sphere collider that will be used for interaction. /// diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index dc2f3e2e..48281347 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -27,7 +27,16 @@ namespace NewHorizons.Handlers return _eyeMusicController; } - public static EyeTravelerData GetOrCreateEyeTravelerData(string id) + public static EyeTravelerData GetEyeTravelerData(string id) + { + if (_eyeTravelers.TryGetValue(id, out EyeTravelerData traveler)) + { + return traveler; + } + return traveler; + } + + public static EyeTravelerData CreateEyeTravelerData(string id) { if (_eyeTravelers.TryGetValue(id, out EyeTravelerData traveler)) { @@ -47,9 +56,9 @@ namespace NewHorizons.Handlers return traveler; } - public static List GetCustomEyeTravelers() + public static List GetActiveCustomEyeTravelers() { - return _eyeTravelers.Values.ToList(); + return _eyeTravelers.Values.Where(t => t.requirementsMet).ToList(); } public static void OnSceneLoad() @@ -180,7 +189,7 @@ namespace NewHorizons.Handlers public static void SetUpEyeCampfireSequence() { - if (!GetCustomEyeTravelers().Any()) return; + if (!GetActiveCustomEyeTravelers().Any()) return; _eyeMusicController = new GameObject("EyeMusicController").AddComponent(); @@ -194,7 +203,7 @@ namespace NewHorizons.Handlers _eyeMusicController.RegisterLoopSource(controller._signal.GetOWAudioSource()); } - foreach (var eyeTraveler in _eyeTravelers.Values) + foreach (var eyeTraveler in GetActiveCustomEyeTravelers()) { if (eyeTraveler.controller != null) { @@ -249,7 +258,7 @@ namespace NewHorizons.Handlers public static void UpdateTravelerPositions() { - if (!GetCustomEyeTravelers().Any()) return; + if (!GetActiveCustomEyeTravelers().Any()) return; var quantumCampsiteController = Object.FindObjectOfType(); @@ -308,6 +317,7 @@ namespace NewHorizons.Handlers public OWAudioSource finaleAudioSource; public List quantumInstruments = new(); public List instrumentZones = new(); + public bool requirementsMet; } } } diff --git a/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs index 57c12544..2363140c 100644 --- a/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/CosmicInflationControllerPatches.cs @@ -12,7 +12,7 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(CosmicInflationController.UpdateFormation))] public static void CosmicInflationController_UpdateFormation(CosmicInflationController __instance) { - if (__instance._waitForCrossfade && EyeSceneHandler.GetCustomEyeTravelers().Any()) + if (__instance._waitForCrossfade && EyeSceneHandler.GetActiveCustomEyeTravelers().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 d02453f5..542c310d 100644 --- a/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/QuantumCampsiteControllerPatches.cs @@ -30,7 +30,7 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(QuantumCampsiteController.AreAllTravelersGathered))] public static bool QuantumCampsiteController_AreAllTravelersGathered(QuantumCampsiteController __instance, ref bool __result) { - if (!EyeSceneHandler.GetCustomEyeTravelers().Any()) + if (!EyeSceneHandler.GetActiveCustomEyeTravelers().Any()) { return true; } @@ -54,7 +54,7 @@ namespace NewHorizons.Patches.EyeScenePatches __result = false; return false; } - foreach (var traveler in EyeSceneHandler.GetCustomEyeTravelers()) + foreach (var traveler in EyeSceneHandler.GetActiveCustomEyeTravelers()) { bool needsTraveler = true; if (!string.IsNullOrEmpty(traveler.info.participatingCondition)) @@ -76,7 +76,7 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(QuantumCampsiteController.OnTravelerStartPlaying))] public static void OnTravelerStartPlaying(QuantumCampsiteController __instance) { - if (!__instance._hasJamSessionStarted && EyeSceneHandler.GetCustomEyeTravelers().Any()) + if (!__instance._hasJamSessionStarted && EyeSceneHandler.GetActiveCustomEyeTravelers().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 346dfd92..4f43d925 100644 --- a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs @@ -11,7 +11,7 @@ namespace NewHorizons.Patches.EyeScenePatches [HarmonyPatch(nameof(TravelerEyeController.OnStartCosmicJamSession))] public static bool TravelerEyeController_OnStartCosmicJamSession(TravelerEyeController __instance) { - if (!EyeSceneHandler.GetCustomEyeTravelers().Any()) + if (!EyeSceneHandler.GetActiveCustomEyeTravelers().Any()) { return true; } From 3999db71a2af6e00fecc1f92705dea0e8c128e54 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Wed, 29 Jan 2025 19:36:19 -0600 Subject: [PATCH 11/39] Fix signals playing on awake and not disabling after finale --- .../EyeOfTheUniverse/EyeMusicController.cs | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs index 46bfc5a0..12a4a386 100644 --- a/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs +++ b/NewHorizons/Components/EyeOfTheUniverse/EyeMusicController.cs @@ -8,16 +8,23 @@ namespace NewHorizons.Components.EyeOfTheUniverse { public class EyeMusicController : MonoBehaviour { + // 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 List _loopSources = new(); private List _finaleSources = new(); private bool _transitionToFinale; private bool _isPlaying; + private double _segmentEndAudioTime; + private float _segmentEndGameTime; + private CosmicInflationController _cosmicInflationController; public void RegisterLoopSource(OWAudioSource src) { src.loop = false; src.SetLocalVolume(1f); src.Stop(); + src.playOnAwake = false; _loopSources.Add(src); } @@ -26,6 +33,7 @@ namespace NewHorizons.Components.EyeOfTheUniverse src.loop = false; src.SetLocalVolume(1f); src.Stop(); + src.playOnAwake = false; _finaleSources.Add(src); } @@ -40,8 +48,6 @@ namespace NewHorizons.Components.EyeOfTheUniverse { _transitionToFinale = true; - var cosmicInflationController = FindObjectOfType(); - // Schedule finale for as soon as the current segment loop ends double finaleAudioTime = _segmentEndAudioTime; float finaleGameTime = _segmentEndGameTime; @@ -53,9 +59,9 @@ namespace NewHorizons.Components.EyeOfTheUniverse } // Set quantum sphere inflation timer - var finaleDuration = cosmicInflationController._travelerFinaleSource.clip.length; - cosmicInflationController._startFormationTime = Time.time; - cosmicInflationController._finishFormationTime = finaleGameTime + finaleDuration - 4f; + var finaleDuration = _cosmicInflationController._travelerFinaleSource.clip.length; + _cosmicInflationController._startFormationTime = Time.time; + _cosmicInflationController._finishFormationTime = finaleGameTime + finaleDuration - 4f; // Play finale in sync foreach (var finaleSrc in _finaleSources) @@ -64,18 +70,17 @@ namespace NewHorizons.Components.EyeOfTheUniverse } } - // 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 void Awake() + { + _cosmicInflationController = FindObjectOfType(); + } 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; @@ -119,6 +124,22 @@ namespace NewHorizons.Components.EyeOfTheUniverse if (_transitionToFinale) break; } } + + // Wait until the bubble has finished expanding + while (Time.time < _cosmicInflationController._finishFormationTime) + { + yield return null; + } + + // Disable audio signals + foreach (var loopSrc in _loopSources) + { + var signal = loopSrc.GetComponent(); + if (signal != null) + { + signal.SetSignalActivation(false, 0f); + } + } } } } From b77ef5a554fe427a8be616e26770cecf6d625bff Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 30 Jan 2025 01:38:48 +0000 Subject: [PATCH 12/39] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 192 ++++++++++++++------------- 1 file changed, 100 insertions(+), 92 deletions(-) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 73c73cf9..1dc9c2ba 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -999,6 +999,14 @@ "type": "string", "description": "The name to display for this traveler's signals. Defaults to the name of the detail." }, + "requiredFact": { + "type": "string", + "description": "If set, the player must know this ship log fact for this traveler (and their instrument zones and quantum instruments) to appear. The fact does not need to exist in the current star system; the player's save data will be checked directly." + }, + "requiredPersistentCondition": { + "type": "string", + "description": "If set, the player must have this persistent dialogue condition set for this traveler (and their instrument zones and quantum instruments) to appear." + }, "startPlayingCondition": { "type": "string", "description": "The dialogue condition that will trigger the traveler to start playing their instrument. Must be unique for each traveler." @@ -1007,20 +1015,16 @@ "type": "string", "description": "If specified, this dialogue condition must be set for the traveler to participate in the campfire song. Otherwise, the song will be able to start without them." }, - "loopAudio": { - "type": "string", - "description": "The audio to use for the traveler while playing around the campfire (and also for their paired quantum instrument). It should be 16 measures at 92 BPM (approximately 42 seconds long). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + "signal": { + "description": "The audio signal to use for the traveler while playing around the campfire (and also for their paired quantum instrument if another is not specified). The audio clip should be 16 measures at 92 BPM (approximately 42 seconds long).", + "$ref": "#/definitions/SignalInfo" }, "finaleAudio": { "type": "string", "description": "The audio to use for the traveler during the finale of the campfire song. It should be 8 measures of the main loop at 92 BPM followed by 2 measures of fade-out (approximately 26 seconds long in total). Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." }, - "frequency": { - "type": "string", - "description": "The frequency ID of the signal emitted by the traveler. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`,\n`Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. Defaults to `Traveler`. You can also put a custom value." - }, "dialogue": { - "description": "The dialogue to use for this traveler. Omit this or set it to null if your traveler already has valid dialogue.", + "description": "The dialogue to use for this traveler. If omitted, the first CharacterDialogueTree in the object will be used.", "$ref": "#/definitions/DialogueInfo" } } @@ -1196,6 +1200,90 @@ } } }, + "SignalInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "audio": { + "type": "string", + "description": "The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "minDistance": { + "type": "number", + "description": "At this distance the sound is at its loudest.", + "format": "float" + }, + "maxDistance": { + "type": "number", + "description": "The sound will drop off by this distance (Note: for signals, this only effects when it is heard aloud and not via the signalscope).", + "format": "float", + "default": 5.0 + }, + "volume": { + "type": "number", + "description": "How loud the sound will play", + "format": "float", + "default": 0.5 + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "detectionRadius": { + "type": "number", + "description": "How close the player must get to the signal to detect it. This is when you get the \"Unknown Signal Detected\"\nnotification.", + "format": "float", + "minimum": 0.0 + }, + "frequency": { + "type": "string", + "description": "The frequency ID of the signal. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`,\n`Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. You can also put a custom value." + }, + "identificationRadius": { + "type": "number", + "description": "How close the player must get to the signal to identify it. This is when you learn its name.", + "format": "float", + "default": 10.0, + "minimum": 0.0 + }, + "insideCloak": { + "type": "boolean", + "description": "Only set to `true` if you are putting this signal inside a cloaking field." + }, + "name": { + "type": "string", + "description": "The unique ID of the signal." + }, + "onlyAudibleToScope": { + "type": "boolean", + "description": "`false` if the player can hear the signal without equipping the signal-scope.", + "default": true + }, + "reveals": { + "type": "string", + "description": "A ship log fact to reveal when the signal is identified.", + "default": "" + }, + "sourceRadius": { + "type": "number", + "description": "Radius of the sphere giving off the signal.", + "format": "float", + "default": 1.0 + } + } + }, "DialogueInfo": { "type": "object", "additionalProperties": false, @@ -1620,6 +1708,10 @@ "type": "boolean", "description": "Allows gathering this quantum instrument using the zoomed-in signalscope, like Chert's bongos." }, + "signal": { + "description": "The audio signal emitted by this quantum instrument. The fields `name`, `audio`, and `frequency` will be copied from the corresponding Eye Traveler's signal if not specified here.", + "$ref": "#/definitions/SignalInfo" + }, "interactRadius": { "type": "number", "description": "The radius of the added sphere collider that will be used for interaction.", @@ -3227,90 +3319,6 @@ "whiteHole" ] }, - "SignalInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "audio": { - "type": "string", - "description": "The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "minDistance": { - "type": "number", - "description": "At this distance the sound is at its loudest.", - "format": "float" - }, - "maxDistance": { - "type": "number", - "description": "The sound will drop off by this distance (Note: for signals, this only effects when it is heard aloud and not via the signalscope).", - "format": "float", - "default": 5.0 - }, - "volume": { - "type": "number", - "description": "How loud the sound will play", - "format": "float", - "default": 0.5 - }, - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, - "detectionRadius": { - "type": "number", - "description": "How close the player must get to the signal to detect it. This is when you get the \"Unknown Signal Detected\"\nnotification.", - "format": "float", - "minimum": 0.0 - }, - "frequency": { - "type": "string", - "description": "The frequency ID of the signal. The built-in game values are `Default`, `Traveler`, `Quantum`, `EscapePod`,\n`Statue`, `WarpCore`, `HideAndSeek`, and `Radio`. You can also put a custom value." - }, - "identificationRadius": { - "type": "number", - "description": "How close the player must get to the signal to identify it. This is when you learn its name.", - "format": "float", - "default": 10.0, - "minimum": 0.0 - }, - "insideCloak": { - "type": "boolean", - "description": "Only set to `true` if you are putting this signal inside a cloaking field." - }, - "name": { - "type": "string", - "description": "The unique ID of the signal." - }, - "onlyAudibleToScope": { - "type": "boolean", - "description": "`false` if the player can hear the signal without equipping the signal-scope.", - "default": true - }, - "reveals": { - "type": "string", - "description": "A ship log fact to reveal when the signal is identified.", - "default": "" - }, - "sourceRadius": { - "type": "number", - "description": "Radius of the sphere giving off the signal.", - "format": "float", - "default": 1.0 - } - } - }, "RemoteInfo": { "type": "object", "additionalProperties": false, From b5538f7627a788f337eb41e9398a6b3f92595cb1 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Wed, 29 Jan 2025 19:56:55 -0600 Subject: [PATCH 13/39] Remove vestigial name property from traveler info --- NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs | 9 ++------- .../Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs | 5 ----- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs index 1e7b3e16..743c149f 100644 --- a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -37,11 +37,6 @@ 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)) { @@ -90,7 +85,7 @@ namespace NewHorizons.Builder.Props { if (string.IsNullOrEmpty(info.signal.name)) { - info.signal.name = info.name; + info.signal.name = go.name; } if (string.IsNullOrEmpty(info.signal.frequency)) { @@ -181,7 +176,7 @@ namespace NewHorizons.Builder.Props { if (string.IsNullOrEmpty(info.signal.name)) { - info.signal.name = travelerData.info.name; + info.signal.name = travelerData.info.signal.name; } if (string.IsNullOrEmpty(info.signal.audio)) { diff --git a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs index e917f839..82f5abe8 100644 --- a/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs +++ b/NewHorizons/External/Modules/Props/EyeOfTheUniverse/EyeTravelerInfo.cs @@ -12,11 +12,6 @@ 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; - /// /// If set, the player must know this ship log fact for this traveler (and their instrument zones and quantum instruments) to appear. The fact does not need to exist in the current star system; the player's save data will be checked directly. /// From c9f0689116f01a122b6a4c6f22f41b07e77498fd Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Wed, 29 Jan 2025 20:04:48 -0600 Subject: [PATCH 14/39] Add instrument zones and standalone quantum instruments to the inflation object list --- NewHorizons/Handlers/EyeSceneHandler.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index 48281347..7a3a4c1a 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -243,6 +243,8 @@ namespace NewHorizons.Handlers // 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); + + ArrayHelpers.Append(ref cosmicInflationController._inflationObjects, quantumInstrument.transform); } } @@ -250,6 +252,8 @@ namespace NewHorizons.Handlers { instrumentZone.gameObject.SetActive(false); ArrayHelpers.Append(ref quantumCampsiteController._instrumentZones, instrumentZone.gameObject); + + ArrayHelpers.Append(ref cosmicInflationController._inflationObjects, instrumentZone.transform); } } From b78c27f4a2f827a6f25c82d3a77463f68a8dfa47 Mon Sep 17 00:00:00 2001 From: Ben C Date: Thu, 30 Jan 2025 02:07:44 +0000 Subject: [PATCH 15/39] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 1dc9c2ba..298e2300 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -995,10 +995,6 @@ "type": "string", "description": "A unique ID to associate this traveler with their corresponding quantum instruments and instrument zones. Must be unique for each traveler." }, - "name": { - "type": "string", - "description": "The name to display for this traveler's signals. Defaults to the name of the detail." - }, "requiredFact": { "type": "string", "description": "If set, the player must know this ship log fact for this traveler (and their instrument zones and quantum instruments) to appear. The fact does not need to exist in the current star system; the player's save data will be checked directly." From 23c7efd36a0e74cabb7df04a824dea76e38608de Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Sat, 1 Feb 2025 16:04:17 -0600 Subject: [PATCH 16/39] Add a null check for potentially missing custom traveler audio --- .../Patches/EyeScenePatches/TravelerEyeControllerPatches.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs index 4f43d925..d31fca50 100644 --- a/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs +++ b/NewHorizons/Patches/EyeScenePatches/TravelerEyeControllerPatches.cs @@ -16,7 +16,10 @@ namespace NewHorizons.Patches.EyeScenePatches return true; } // Not starting the loop audio here; EyeMusicController will handle that - __instance._signal.GetOWAudioSource().SetLocalVolume(0f); + if (__instance._signal != null) + { + __instance._signal.GetOWAudioSource().SetLocalVolume(0f); + } return false; } } From 8ad30fefe0ab97a5c1693f2abb845c43bede2c3f Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Sat, 1 Feb 2025 16:04:31 -0600 Subject: [PATCH 17/39] Eye of the Universe documentation page --- .../eye_of_the_universe/animController.webp | Bin 0 -> 29328 bytes .../docs/guides/eye-of-the-universe.md | 341 ++++++++++++++++++ 2 files changed, 341 insertions(+) create mode 100644 docs/src/assets/docs-images/eye_of_the_universe/animController.webp create mode 100644 docs/src/content/docs/guides/eye-of-the-universe.md diff --git a/docs/src/assets/docs-images/eye_of_the_universe/animController.webp b/docs/src/assets/docs-images/eye_of_the_universe/animController.webp new file mode 100644 index 0000000000000000000000000000000000000000..5e1e2f8c19371dcd9169b8c986df92283797d8fd GIT binary patch literal 29328 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 literal 0 HcmV?d00001 diff --git a/docs/src/content/docs/guides/eye-of-the-universe.md b/docs/src/content/docs/guides/eye-of-the-universe.md new file mode 100644 index 00000000..09a02504 --- /dev/null +++ b/docs/src/content/docs/guides/eye-of-the-universe.md @@ -0,0 +1,341 @@ +--- +title: Eye of the Universe +description: A guide to adding additional content to the Eye of the Universe with New Horizons +--- + +This guide covers some 'gotchas' and features unique to the Eye of the Universe. + +## Extending the Eye of the Universe + +### Star System + +To define a Star System config for the Eye of the Universe, name your star system config file `EyeOfTheUniverse.json` or specify the `"name"` as `"EyeOfTheUniverse"`. Note that many of the star system features have no effect at the Eye compared to a regular custom star system. + +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/star_system_schema.json", + "name": "EyeOfTheUniverse", + // etc. +} +``` + +### Existing Planet + +The existing areas of the Eye of the Universe, such as the "sixth planet" and funnel, the museum/observatory, the forest of galaxies, and the campfire, are all contained within one static "planet" (despite visually being distinct locations). To add to these areas, you'll need to specify a planet config file with a `"name"` of `"EyeOfTheUniverse"` and *also* a `"starSystem"` of `"EyeOfTheUniverse"`, as the star system and "planet" share the same name. + +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name": "EyeOfTheUniverse", + "starSystem": "EyeOfTheUniverse", + "Props": { + "details": [ + // etc. + ] + }, + // etc. +} +``` + +### The Vessel + +You can also add props to the Vessel at the Eye: + +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name": "Vessel", + "starSystem": "EyeOfTheUniverse", + "Props": { + "details": [ + // etc. + ] + }, + // etc. +} +``` + +## Eye-Specific Features + +### Eye Travelers + +Eye Travelers are a special kind of detail prop (see [Detailing](/guides/details/)) that get added in as additional characters around the campfire at the end of the Eye sequence, similar to Solanum and the Prisoner in the vanilla game. + +At minimum, you will need a character object to act as the traveler, an audio file for the looping part of the new instrument track, an audio file to layer over the finale, a dialogue XML file with certain dialogue conditions set up, and a [quantum instrument](#quantum-instruments). + +The traveler will only appear once their quantum instrument is gathered. After that, they will appear in the circle around the campfire, and they can be interacted with through dialogue to start playing their instrument. The instrument audio is handled via a signal on the traveler that only becomes audible after talking to them. + +Custom travelers will automatically be included in the inflation animation that pushes everyone away from the campfire at the end of the sequence. + +[Eye Travelers](#eye-travelers), [Quantum Instruments](#quantum-instruments), and [Instrument Zones](#instrument-zones) are all linked by their `"id"` properties. Ensure that your ID matches between those details and is unique enough to not conflict with other mods. + +Here's an example config: +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name": "EyeOfTheUniverse", + "starSystem": "EyeOfTheUniverse", + "EyeOfTheUniverse": { + "eyeTravelers": [ + { + "id": "Slate", + "signal": { + "name": "Slate", + "audio": "planets/MetronomeLoop.wav", + "detectionRadius": 0, + "identificationRadius": 10, + "onlyAudibleToScope": false, + "position": {"x": 0, "y": 0.75, "z": 0}, + "isRelativeToParent": true + }, + "finaleAudio": "planets/MetronomeFinale.wav", + "startPlayingCondition": "EyeSlatePlaying", + "participatingCondition": "EyeSlateParticipating", + "dialogue": { + "xmlFile": "planets/EyeSlate.xml", + "position": {"x": 0.0, "y": 1.5, "z": 0.0}, + "isRelativeToParent": true + }, + "path": "TimberHearth_Body/Sector_TH/Sector_Village/Sector_StartingCamp/Characters_StartingCamp/Villager_HEA_Slate" + } + ] + } +} +``` + +To see the full list of eye traveler properties and descriptions of what each property does, check [the EyeTravelerInfo schema](/schemas/body-schema/defs/eyetravelerinfo/). + +On compatibility: +> New Horizons changes the campfire sequence in minor ways to make it easier for mods which add additional travelers to be compatible with each other. The audio handling is changed so that instrument audio will be synchronized and layered with each other automatically. All travelers will be automatically repositioned in a circle around the campfire based on the total number of travelers, both vanilla and modded. As long as other details (such as quantum instruments and instrument zones) are not placed in the same locations as other existing mods, the campfire sequence changes will be compatible. + +#### Audio + +The looping audio clip is used for the signals emitted by the traveler and their quantum instrument, and is the audio that gets played when they play their instrument. It should be a WAV file with 16 measures of music at 92 BPM (exactly 2,003,478 samples at 48,000 Hz, or approximately 42 seconds long). It is highly recommended that you use Audacity or another audio editor to trim your audio clip to exactly the same length as one of the vanilla traveler audio clips, or else it will fall gradually out of sync with the other instrument loops. + +The finale audio clip is only played once, after all travelers have started playing. It should be 8 measures of the main loop at 92 BPM followed by 2 measures of fade-out (approximately 26 seconds long in total). Unlike the looping audio clip, it does not need to precisely match the length of the vanilla finale clip; it can end early or continue playing after the other ends. + +The game plays all of the looping audio clips (including your custom one) simultaneously once you tell the first traveler to start playing, and then fades them in one by one as you talk to the others. After all travelers are playing, the game selects a finale audio clip that contains all Hearthian and Nomai/Owlk instruments mixed into one file, and then your custom finale audio clip will be layered over whichever vanilla clip plays. Only include your own instrument in the clip, and ensure it sounds okay alongside Solanum, the Prisoner, and both/neither. + +#### Dialogue + +The dialogue XML for your traveler works like other dialogue (see the [Dialogue](/guides/dialogue/) guide) but there are specially named conditions you will need to use for the functionality to work as expected: +- Use `AnyTravelersGathered` to check if any traveler has been gathered yet. This includes Riebeck and Esker, so it should always be true, unless you forcibly enable your traveler to be enabled early. +- Use `AllTravelersGathered` to check if all of the travelers have been gathered and are ready to start playing. +- Use `JamSessionIsOver` to check if the travelers have stopped playing the song and the sphere of possibilities has appeared. +- Use a `` with the condition defined in your eye traveler config's `"startPlayingCondition"` on the node or dialogue option that should make your traveler start playing their instrument. This condition name must be unique and not conflict with other mods. +- If you want your traveler to be present but have an option to not participate in the campfire song (like the Prisoner), use a `` with the condition defined in your eye traveler config's `"participatingCondition"` on the node or dialogue option where your traveler agrees to join in. This condition name must be unique and not conflict with other mods. + +```xml + + Slate, Probably + + WAITING_FOR_OTHERS + DEFAULT + + It's me, definitely Slate and not a dreamstalker in disguise. + + + + ANY_GATHERED + AnyTravelersGathered + + You still have other travelers to gather. + + + + You're going to join in, right? + PARTICIPATING + + + Okay then... + + + + + PARTICIPATING + + Sure. + + EyeSlateParticipating + + + READY_TO_PLAY + AllTravelersGathered + + We're all here. Time to start the music. + + + + Ready to go. + START_PLAYING + + + Not yet. + NOT_YET + + + + + START_PLAYING + + Let's begin. + + EyeSlatePlaying + + + NOT_YET + + Whenever you're ready. + + + + FAREWELL + JamSessionIsOver + + It's rewind time. + + + +``` + +#### Custom Animation + +To add custom animations to your Eye Traveler, there is some setup work that has to be done in Unity. You will need to set up your character in Unity and load them via asset bundle, like you would any other detail: + +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name": "EyeOfTheUniverse", + "starSystem": "EyeOfTheUniverse", + "EyeOfTheUniverse": { + "eyeTravelers": [ + { + "id": "MyCoolEyeGuy", + "assetBundle": "planets/bundles/eyeoftheuniverse", + "path": "Assets/EyeOfTheUniverse/Traveler/MyCoolEyeGuy.prefab" + } + ] + } +} +``` + +Next, create an Animator Controller asset in Unity with at least two states, named "Idle" and "PlayingInstrument". You can assign whatever animation clip you like to these states, but the names of the states must match exactly. The default Idle state will play when the traveler is first spawned in, and will transition to the PlayingInstrument state when the right conditions are met to start playing the instrument. Ensure that both animation clips are set to loop in their import settings. + +Add a boolean Parameter in the left panel named "Playing". This will be set to true when the traveler starts playing their instrument. + +Add a transition in both directions between the Idle and PlayingInstrument states. Uncheck "Has Exit Time" for both transitions and adjust the other timing settings as desired. + +Add a Condition on the `Idle -> PlayingInstrument` transition to check for `Playing` = `true`, and the inverse for `PlayingInstrument -> Idle`. + +![animController](@/assets/docs-images/eye_of_the_universe/animController.webp) + +In your character object, find the `Animator` component and set its `Controller` property to the Animator Controller asset you created. If you have a `TravelerEyeController` component in your object, set its `_animator` property to your Animator component. + +If everything was set up correctly, your character should play their animations in-game. + +### Quantum Instruments + +Quantum instruments are the interactible instruments, typically hidden by a short 'puzzle', that cause their corresponding traveler to appear around the campfire. They are just like any other detail prop (see [Detailing](/guides/details/)) but they have additional handling to only activate after gathering and speaking to Riebeck, like the other instrument 'puzzles' in the Eye sequence. + +If not specified, the quantum instrument will inherit some of the properties for its `"signal"` from the corresponding eye traveler config. + +If you want other objects besides the traveler to appear or disappear in response to gathering the instrument, specify a custom dialogue condition name for `"gatherCondition"` and use that same condition as the `"activationCondition"` or `"deactivationCondition"` for the details you want to toggle. + +Quantum instruments will automatically be included in the inflation animation that pushes everyone away from the campfire at the end of the sequence. + +[Eye Travelers](#eye-travelers), [Quantum Instruments](#quantum-instruments), and [Instrument Zones](#instrument-zones) are all linked by their `"id"` properties. Ensure that your ID matches between those details and is unique enough to not conflict with other mods. + +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name": "EyeOfTheUniverse", + "starSystem": "EyeOfTheUniverse", + "EyeOfTheUniverse": { + "eyeTravelers": [ + { + "id": "Slate", + "signal": { + "name": "Slate", + "audio": "planets/MetronomeLoop.wav" + // etc. + }, + // etc. + } + ], + "quantumInstruments": [ + { + "id": "Slate", + "gatherWithScope": false, + "gatherCondition": "EyeSlateGather", + "path": "TimberHearth_Body/Sector_TH/Sector_Village/Sector_StartingCamp/Props_StartingCamp/OtherComponentsGroup/Props_HEA_CampsiteLogAssets/Props_HEA_MarshmallowCanOpened", + "position": {"x": -43.94369, "y": 0, "z": 7506.436}, + "signal": { + "detectionRadius": 0, + "identificationRadius": 10, + "position": {"x": 0, "y": 0.1, "z": 0}, + "isRelativeToParent": true + } + } + ] + } +} +``` + +To see the full list of quantum instrument properties and descriptions of what each property does, check [the QuantumInstrumentInfo schema](/schemas/body-schema/defs/quantuminstrumentinfo/). + +### Instrument Zones + +Instrument zones are just like any other detail prop (see [Detailing](/guides/details/)) but they have additional handling to only activate after gathering and speaking to Riebeck, like the other instrument 'puzzles' in the Eye sequence. + +Custom instrument zones will automatically be included in the inflation animation that pushes everyone away from the campfire at the end of the sequence. + +[Eye Travelers](#eye-travelers), [Quantum Instruments](#quantum-instruments), and [Instrument Zones](#instrument-zones) are all linked by their `"id"` properties. Ensure that your ID matches between those details and is unique enough to not conflict with other mods. + +```json +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name": "EyeOfTheUniverse", + "starSystem": "EyeOfTheUniverse", + "EyeOfTheUniverse": { + "eyeTravelers": [ + { + "id": "Slate", + // etc. + } + ], + "instrumentZones": [ + { + "id": "Slate", + "deactivationCondition": "EyeSlateGather", + "path": "TimberHearth_Body/Sector_TH/Sector_Village/Sector_StartingCamp/Props_StartingCamp/OtherComponentsGroup/Props_HEA_CampsiteLogAssets", + "removeChildren": [ + "Props_HEA_MarshmallowCanOpened" + ], + "position": {"x": -43.30302, "y": 0, "z": 7507.822} + } + ] + } +} +``` + +To see the full list of instrument zone properties and descriptions of what each property does, check [the InstrumentZoneInfo schema](/schemas/body-schema/defs/instrumentzoneinfo/). + +## Eye-Specific Considerations + +### Cross-System Details + +Specifying details with a `"path"` pointing to an object in the regular solar system normally wouldn't work, as the Eye of the Universe lives in a completely separate scene from the rest of the game and those objects don't exist at the Eye. New Horizons works around this by force-loading the regular solar system, grabbing any objects referenced in Eye of the Universe config files, and then attempting to preserve these objects when loading the Eye of the Universe scene. This can cause issues with many different kinds of props, especially interactive ones that depend on some other part of the solar system existing. + +Because the objects are not available outside of this workaround, objects from the regular solar system cannot be spawned in via the New Horizons API. + +### Custom Planets + +While you *can* define completely custom planets the same as you would in a regular custom solar system, they may exhibit weird orbital behaviors or pass through the existing static Eye of the Universe objects. Prefer adding onto the existing bodies or setting a `"staticPosition"` on your planet configs to lock them in place. + +### The Player Ship and Ship Logs + +The player's ship does not exist in the Eye of the Universe scene. In addition to the obvious issues this causes (no access to the ship's warp functionality, ship spawn points being non-functional, etc.), the ship log computer not existing causes some methods of checking and learning ship log facts to not function at all while at the Eye. If you need to track whether the player has met certain conditions elsewhere in the game (for example, if they've previously met a character that you now want to appear at the campfire), consider using a Persistent Dialogue Condition, which does not have these issues. + +### Mod Compatibility + +Other existing and future story mods will want to add additional content to the Eye of the Universe, and unlike entirely custom planets and star systems, there is a high probability that objects placed at the Eye may overlap with those placed by other mods. When testing, try installing as many of these other mods as possible and seeing if the objects they add overlap with yours. If so, consider moving your objects to a different position. When possible, use New Horizons features that preserve compatibility between mods. \ No newline at end of file From 04e94a952c382e59e6100dcef7f51581b5de1cff Mon Sep 17 00:00:00 2001 From: Heavenly Avenger Date: Tue, 4 Feb 2025 11:50:04 -0300 Subject: [PATCH 18/39] Updates the portuguese_br translation file. The file only had 'vessel' translated. Translation made taking into account game original translation, keeping where possible translation choices of the game, like: - Vessel => Hospedeiro - Dark Bramble => Abrolho Sombrio (translation of names) - warp => translocation A special case was made for the "NOMAI_SHUTTLE_COMPUTER" to split the location with colon and avoid a gender combination ("no", "na") with a common "em". As locations might have male or female "genders" to combine with in the Portuguese language. "Raycast", the name of the mod (New Horizon) and "Dieclone" weren't translated at all for being either the very name of the mod, or being considered keywords or neologism words (as much as "Nomai" is not translated). --- .../Assets/translations/portuguese_br.json | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/NewHorizons/Assets/translations/portuguese_br.json b/NewHorizons/Assets/translations/portuguese_br.json index cac86858..100d293b 100644 --- a/NewHorizons/Assets/translations/portuguese_br.json +++ b/NewHorizons/Assets/translations/portuguese_br.json @@ -1,6 +1,69 @@ { - "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "$schema": "https://raw.githubusercontent.com/outer-wilds-new-horizons/new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "DialogueDictionary": { + "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_1": "Sua nave agora está equipada com uma unidade de translocação!", + "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_2": "Você pode usar a nova aba \"Modo Interestelar\" no diário de bordo de sua nave para ajustar o piloto automático rumo a outro sistema solar.", + "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_3": "Então é só acionar o piloto automático e relaxar!" + }, "UIDictionary": { - "Vessel": "Hospedeiro" + "INTERSTELLAR_MODE": "Modo Interestelar", + "FREQ_STATUE": "Estátua Nomai", + "FREQ_WARP_CORE": "Fluxo Anti-Gravitacional", + "FREQ_UNKNOWN": "???", + "ENGAGE_WARP_PROMPT": "Acionar translocação a {0}", + "WARP_LOCKED": "PILOTO AUTOMÁTICO AJUSTADO PARA:\n{0}", + "LOCK_AUTOPILOT_WARP": "Ajustar Piloto Automático para sistema solar", + "RICH_PRESENCE_EXPLORING": "Explorando {0}.", + "RICH_PRESENCE_WARPING": "Translocando para {0}.", + "OUTDATED_VERSION_WARNING": "AVISO\n\nO mod New Horizons funciona apenas na versão {0} ou superior. Você está usando a versão {1}.\n\nAtualize o Outer Wilds ou desinstale NH.", + "JSON_FAILED_TO_LOAD": "Arquivo(s) inválido(s): {0}", + "DEBUG_RAYCAST": "Raycast", + "DEBUG_PLACE": "Posicionar objeto", + "DEBUG_PLACE_TEXT": "Colocar texto Nomai", + "DEBUG_UNDO": "Desfazer", + "DEBUG_REDO": "Refazer", + "Vessel": "Hospedeiro", + "DLC_REQUIRED": "AVISO\n\nVocê tem addons (ex. {0}) instalados que requerem a DLC, mas a mesma não está habilitada.\n\nSuas mods podem não funcionar como esperado." + }, + "OtherDictionary": { + "NOMAI_SHUTTLE_COMPUTER": "A exploradora]]> está repousando por hora em: {0}]]>." + }, + "AchievementTranslations": { + "NH_EATEN_OUTSIDE_BRAMBLE": { + "Name": "Fenda de Contenção", + "Description": "Foi devorado fora das premisas de Abrolho Sombrio" + }, + "NH_MULTIPLE_SYSTEM": { + "Name": "Viajante", + "Description": "Visitou 5 sistemas solares seguidos." + }, + "NH_NEW_FREQ": { + "Name": "Frequências Anômalas", + "Description": "Descobriu uma nova frequência." + }, + "NH_PROBE_LOST": { + "Name": "Conexão Perdida", + "Description": "Perdeu seu pequeno batedor." + }, + "NH_WARP_DRIVE": { + "Name": "História Mal Contada", + "Description": "Usou a unidade de translocação da nave." + }, + "NH_VESSEL_WARP": { + "Name": "História Acertada", + "Description": "Translocou para um sistema estelar usando o Hospedeiro." + }, + "NH_RAFTING": { + "Name": "A Jangada e os Furiosos", + "Description": "Deu um rolé de jangada." + }, + "NH_SUCKED_INTO_LAVA_BY_TORNADO": { + "Name": "Dieclone", + "Description": "Foi sugado na lava por um tornado." + }, + "NH_TALK_TO_FIVE_CHARACTERS": { + "Name": "Social", + "Description": "Conversou com 5 personagens." + } } -} \ No newline at end of file +} From 80de0f1cb79b59487f4c4ce4285659554c5db78a Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 4 Feb 2025 18:46:40 -0500 Subject: [PATCH 19/39] Revert changes by MegaPiggy --- NewHorizons/Builder/General/GravityBuilder.cs | 7 +++---- NewHorizons/Builder/Orbital/FocalPointBuilder.cs | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/NewHorizons/Builder/General/GravityBuilder.cs b/NewHorizons/Builder/General/GravityBuilder.cs index 7d202e27..68a2be33 100644 --- a/NewHorizons/Builder/General/GravityBuilder.cs +++ b/NewHorizons/Builder/General/GravityBuilder.cs @@ -15,11 +15,10 @@ namespace NewHorizons.Builder.General var gravityRadius = GM / 0.1f; if (exponent == 2f) gravityRadius = Mathf.Sqrt(gravityRadius); - if (config.FocalPoint != null) gravityRadius = 0; // keep it at the lowest possible - else if (config.Base.soiOverride != 0f) gravityRadius = config.Base.soiOverride; - else if (config.Star != null) gravityRadius = Mathf.Min(gravityRadius, 15 * config.Base.surfaceSize); // To let you actually orbit things the way you would expect we cap this at 4x the diameter if its not a star (this is what giants deep has) - else gravityRadius = Mathf.Min(gravityRadius, 4 * config.Base.surfaceSize); + if (config.Star == null) gravityRadius = Mathf.Min(gravityRadius, 4 * config.Base.surfaceSize); + else gravityRadius = Mathf.Min(gravityRadius, 15 * config.Base.surfaceSize); + if (config.Base.soiOverride != 0f) gravityRadius = config.Base.soiOverride; var gravityGO = new GameObject("GravityWell"); gravityGO.transform.parent = planetGO.transform; diff --git a/NewHorizons/Builder/Orbital/FocalPointBuilder.cs b/NewHorizons/Builder/Orbital/FocalPointBuilder.cs index b3e63391..6183b6c8 100644 --- a/NewHorizons/Builder/Orbital/FocalPointBuilder.cs +++ b/NewHorizons/Builder/Orbital/FocalPointBuilder.cs @@ -57,11 +57,11 @@ namespace NewHorizons.Builder.Orbital config.Base.surfaceGravity = gravitationalMass * GravityVolume.GRAVITATIONAL_CONSTANT; config.Base.gravityFallOff = primary.Config.Base.gravityFallOff; + // Other stuff to make the barycenter not interact with anything in any way + config.Base.soiOverride = 0; var separation = primary.Config.Orbit.semiMajorAxis + secondary.Config.Orbit.semiMajorAxis; - var separationRadius = (separation / 2); - config.Base.soiOverride = separationRadius * 1.5f; - config.ReferenceFrame.bracketRadius = separationRadius; - config.ReferenceFrame.targetColliderRadius = separationRadius; + config.ReferenceFrame.bracketRadius = separation; + config.ReferenceFrame.targetColliderRadius = separation; config.Base.showMinimap = false; } From 786f1751dc5656c1b96957b328504a0838c9d81b Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 4 Feb 2025 18:46:49 -0500 Subject: [PATCH 20/39] Revert another change --- NewHorizons/Builder/General/GravityBuilder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Builder/General/GravityBuilder.cs b/NewHorizons/Builder/General/GravityBuilder.cs index 68a2be33..140a9742 100644 --- a/NewHorizons/Builder/General/GravityBuilder.cs +++ b/NewHorizons/Builder/General/GravityBuilder.cs @@ -48,7 +48,8 @@ namespace NewHorizons.Builder.General if (config.Base.surfaceGravity == 0) alignmentRadius = 0; gravityVolume._alignmentRadius = config.Base.gravityAlignmentRadiusOverride ?? alignmentRadius; - gravityVolume._upperSurfaceRadius = config.FocalPoint != null ? 0 : config.Base.surfaceSize; + // Nobody write any FocalPoint overriding here, those work as intended gravitationally so deal with it! + gravityVolume._upperSurfaceRadius = config.Base.surfaceSize; gravityVolume._lowerSurfaceRadius = 0; gravityVolume._layer = 3; gravityVolume._priority = config.Base.gravityVolumePriority; From 458c202fa45ec6d2368c719f591a5b31c8e0bc9a Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 4 Feb 2025 18:47:10 -0500 Subject: [PATCH 21/39] Revert "Revert changes by MegaPiggy" This reverts commit 80de0f1cb79b59487f4c4ce4285659554c5db78a. --- NewHorizons/Builder/General/GravityBuilder.cs | 7 ++++--- NewHorizons/Builder/Orbital/FocalPointBuilder.cs | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/NewHorizons/Builder/General/GravityBuilder.cs b/NewHorizons/Builder/General/GravityBuilder.cs index 140a9742..8b366f62 100644 --- a/NewHorizons/Builder/General/GravityBuilder.cs +++ b/NewHorizons/Builder/General/GravityBuilder.cs @@ -15,10 +15,11 @@ namespace NewHorizons.Builder.General var gravityRadius = GM / 0.1f; if (exponent == 2f) gravityRadius = Mathf.Sqrt(gravityRadius); + if (config.FocalPoint != null) gravityRadius = 0; // keep it at the lowest possible + else if (config.Base.soiOverride != 0f) gravityRadius = config.Base.soiOverride; + else if (config.Star != null) gravityRadius = Mathf.Min(gravityRadius, 15 * config.Base.surfaceSize); // To let you actually orbit things the way you would expect we cap this at 4x the diameter if its not a star (this is what giants deep has) - if (config.Star == null) gravityRadius = Mathf.Min(gravityRadius, 4 * config.Base.surfaceSize); - else gravityRadius = Mathf.Min(gravityRadius, 15 * config.Base.surfaceSize); - if (config.Base.soiOverride != 0f) gravityRadius = config.Base.soiOverride; + else gravityRadius = Mathf.Min(gravityRadius, 4 * config.Base.surfaceSize); var gravityGO = new GameObject("GravityWell"); gravityGO.transform.parent = planetGO.transform; diff --git a/NewHorizons/Builder/Orbital/FocalPointBuilder.cs b/NewHorizons/Builder/Orbital/FocalPointBuilder.cs index 6183b6c8..b3e63391 100644 --- a/NewHorizons/Builder/Orbital/FocalPointBuilder.cs +++ b/NewHorizons/Builder/Orbital/FocalPointBuilder.cs @@ -57,11 +57,11 @@ namespace NewHorizons.Builder.Orbital config.Base.surfaceGravity = gravitationalMass * GravityVolume.GRAVITATIONAL_CONSTANT; config.Base.gravityFallOff = primary.Config.Base.gravityFallOff; - // Other stuff to make the barycenter not interact with anything in any way - config.Base.soiOverride = 0; var separation = primary.Config.Orbit.semiMajorAxis + secondary.Config.Orbit.semiMajorAxis; - config.ReferenceFrame.bracketRadius = separation; - config.ReferenceFrame.targetColliderRadius = separation; + var separationRadius = (separation / 2); + config.Base.soiOverride = separationRadius * 1.5f; + config.ReferenceFrame.bracketRadius = separationRadius; + config.ReferenceFrame.targetColliderRadius = separationRadius; config.Base.showMinimap = false; } From 35c0b934c7faa7e5cfc7a121a65dd304cd2d26c8 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 4 Feb 2025 19:03:25 -0500 Subject: [PATCH 22/39] Try really hard to not have the gravity well for a focal point affect you --- NewHorizons/Builder/General/GravityBuilder.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/NewHorizons/Builder/General/GravityBuilder.cs b/NewHorizons/Builder/General/GravityBuilder.cs index 8b366f62..515ec4c3 100644 --- a/NewHorizons/Builder/General/GravityBuilder.cs +++ b/NewHorizons/Builder/General/GravityBuilder.cs @@ -27,15 +27,25 @@ namespace NewHorizons.Builder.General gravityGO.layer = Layer.BasicEffectVolume; gravityGO.SetActive(false); - var SC = gravityGO.AddComponent(); - SC.isTrigger = true; - SC.radius = gravityRadius; + var sphereCollider = gravityGO.AddComponent(); + sphereCollider.isTrigger = true; + sphereCollider.radius = gravityRadius; var owCollider = gravityGO.AddComponent(); owCollider.SetLODActivationMask(DynamicOccupant.Player); var owTriggerVolume = gravityGO.AddComponent(); + // If it's a focal point dont add collision stuff + if (config.FocalPoint != null) + { + owCollider.enabled = false; + owTriggerVolume.enabled = false; + sphereCollider.radius = 0; + sphereCollider.enabled = false; + sphereCollider.isTrigger = false; + } + // copied from th and qm var gravityVolume = gravityGO.AddComponent(); gravityVolume._cutoffAcceleration = 0f; From a8b9c3648ea1611d0b9603f3a251586eb17818d8 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Tue, 4 Feb 2025 19:11:07 -0500 Subject: [PATCH 23/39] Update GravityBuilder.cs --- NewHorizons/Builder/General/GravityBuilder.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/NewHorizons/Builder/General/GravityBuilder.cs b/NewHorizons/Builder/General/GravityBuilder.cs index 515ec4c3..e358d1a8 100644 --- a/NewHorizons/Builder/General/GravityBuilder.cs +++ b/NewHorizons/Builder/General/GravityBuilder.cs @@ -36,16 +36,6 @@ namespace NewHorizons.Builder.General var owTriggerVolume = gravityGO.AddComponent(); - // If it's a focal point dont add collision stuff - if (config.FocalPoint != null) - { - owCollider.enabled = false; - owTriggerVolume.enabled = false; - sphereCollider.radius = 0; - sphereCollider.enabled = false; - sphereCollider.isTrigger = false; - } - // copied from th and qm var gravityVolume = gravityGO.AddComponent(); gravityVolume._cutoffAcceleration = 0f; @@ -70,6 +60,21 @@ namespace NewHorizons.Builder.General gravityVolume._isPlanetGravityVolume = true; gravityVolume._cutoffRadius = 0f; + // If it's a focal point dont add collision stuff + // This is overkill + if (config.FocalPoint != null) + { + owCollider.enabled = false; + owTriggerVolume.enabled = false; + sphereCollider.radius = 0; + sphereCollider.enabled = false; + sphereCollider.isTrigger = false; + // This should ensure that even if the player enters the volume, it counts them as being inside the zero-gee cave equivalent + gravityVolume._cutoffRadius = gravityVolume._upperSurfaceRadius; + gravityVolume._lowerSurfaceRadius = gravityVolume._upperSurfaceRadius; + gravityVolume._cutoffAcceleration = 0; + } + gravityGO.SetActive(true); ao._gravityVolume = gravityVolume; From 95ff46cc1729f19370bd8335e02576574ae454cb Mon Sep 17 00:00:00 2001 From: AnonymousStrangerOW <169866456+AnonymousStrangerOW@users.noreply.github.com> Date: Fri, 7 Feb 2025 13:56:07 -0500 Subject: [PATCH 24/39] stopped orbit line from generating at the eye the eye of the universe does not have a map viewer instance in its scene, so why even generate orbit lines to begin with? this also solved a graphical glitch containing a missing texture seen in new horizons examples. --- NewHorizons/Handlers/PlanetCreationHandler.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 201def01..3d30788a 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -519,7 +519,10 @@ namespace NewHorizons.Handlers if (body.Config.Orbit.showOrbitLine && !body.Config.Orbit.isStatic) { - OrbitlineBuilder.Make(body.Object, body.Config.Orbit.isMoon, body.Config); + if (LoadManager.GetCurrentScene() != OWScene.EyeOfTheUniverse) + { + OrbitlineBuilder.Make(body.Object, body.Config.Orbit.isMoon, body.Config); + } } DetectorBuilder.Make(go, owRigidBody, primaryBody, ao, body.Config); @@ -841,7 +844,10 @@ namespace NewHorizons.Handlers var isMoon = newAO.GetAstroObjectType() is AstroObject.Type.Moon or AstroObject.Type.Satellite or AstroObject.Type.SpaceStation; if (body.Config.Orbit.showOrbitLine) { - OrbitlineBuilder.Make(go, isMoon, body.Config); + if (LoadManager.GetCurrentScene() != OWScene.EyeOfTheUniverse) + { + OrbitlineBuilder.Make(go, isMoon, body.Config); + } } DetectorBuilder.SetDetector(primary, newAO, go.GetComponentInChildren()); From 40d57b139e5a5b50ccb1a6de5239279a0806da8c Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Fri, 7 Feb 2025 13:48:09 -0600 Subject: [PATCH 25/39] Fix persistent condition check --- NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs index 743c149f..b8c84b62 100644 --- a/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs +++ b/NewHorizons/Builder/Props/EyeOfTheUniverseBuilder.cs @@ -25,7 +25,7 @@ namespace NewHorizons.Builder.Props travelerData.requirementsMet = false; } - if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && DialogueConditionManager.SharedInstance.GetConditionState(info.requiredPersistentCondition)) + if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && !DialogueConditionManager.SharedInstance.GetConditionState(info.requiredPersistentCondition)) { travelerData.requirementsMet = false; } From fc0dd05cf3c63888eb12ae6cb77f313e68a18643 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Fri, 7 Feb 2025 14:24:33 -0600 Subject: [PATCH 26/39] Improve eye docs formatting --- ...he-universe.md => eye-of-the-universe.mdx} | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) rename docs/src/content/docs/guides/{eye-of-the-universe.md => eye-of-the-universe.mdx} (93%) diff --git a/docs/src/content/docs/guides/eye-of-the-universe.md b/docs/src/content/docs/guides/eye-of-the-universe.mdx similarity index 93% rename from docs/src/content/docs/guides/eye-of-the-universe.md rename to docs/src/content/docs/guides/eye-of-the-universe.mdx index 09a02504..0a02d614 100644 --- a/docs/src/content/docs/guides/eye-of-the-universe.md +++ b/docs/src/content/docs/guides/eye-of-the-universe.mdx @@ -3,6 +3,8 @@ title: Eye of the Universe description: A guide to adding additional content to the Eye of the Universe with New Horizons --- +import { Aside } from "@astrojs/starlight/components"; + This guide covers some 'gotchas' and features unique to the Eye of the Universe. ## Extending the Eye of the Universe @@ -11,7 +13,7 @@ This guide covers some 'gotchas' and features unique to the Eye of the Universe. To define a Star System config for the Eye of the Universe, name your star system config file `EyeOfTheUniverse.json` or specify the `"name"` as `"EyeOfTheUniverse"`. Note that many of the star system features have no effect at the Eye compared to a regular custom star system. -```json +```json title="systems/EyeOfTheUniverse.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/star_system_schema.json", "name": "EyeOfTheUniverse", @@ -23,7 +25,7 @@ To define a Star System config for the Eye of the Universe, name your star syste The existing areas of the Eye of the Universe, such as the "sixth planet" and funnel, the museum/observatory, the forest of galaxies, and the campfire, are all contained within one static "planet" (despite visually being distinct locations). To add to these areas, you'll need to specify a planet config file with a `"name"` of `"EyeOfTheUniverse"` and *also* a `"starSystem"` of `"EyeOfTheUniverse"`, as the star system and "planet" share the same name. -```json +```json title="planets/EyeOfTheUniverse.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", "name": "EyeOfTheUniverse", @@ -41,7 +43,7 @@ The existing areas of the Eye of the Universe, such as the "sixth planet" and fu You can also add props to the Vessel at the Eye: -```json +```json title="planets/Vessel.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", "name": "Vessel", @@ -55,9 +57,7 @@ You can also add props to the Vessel at the Eye: } ``` -## Eye-Specific Features - -### Eye Travelers +## Eye Travelers Eye Travelers are a special kind of detail prop (see [Detailing](/guides/details/)) that get added in as additional characters around the campfire at the end of the Eye sequence, similar to Solanum and the Prisoner in the vanilla game. @@ -70,7 +70,7 @@ Custom travelers will automatically be included in the inflation animation that [Eye Travelers](#eye-travelers), [Quantum Instruments](#quantum-instruments), and [Instrument Zones](#instrument-zones) are all linked by their `"id"` properties. Ensure that your ID matches between those details and is unique enough to not conflict with other mods. Here's an example config: -```json +```json title="planets/EyeOfTheUniverse.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", "name": "EyeOfTheUniverse", @@ -105,10 +105,11 @@ Here's an example config: To see the full list of eye traveler properties and descriptions of what each property does, check [the EyeTravelerInfo schema](/schemas/body-schema/defs/eyetravelerinfo/). -On compatibility: -> New Horizons changes the campfire sequence in minor ways to make it easier for mods which add additional travelers to be compatible with each other. The audio handling is changed so that instrument audio will be synchronized and layered with each other automatically. All travelers will be automatically repositioned in a circle around the campfire based on the total number of travelers, both vanilla and modded. As long as other details (such as quantum instruments and instrument zones) are not placed in the same locations as other existing mods, the campfire sequence changes will be compatible. + -#### Audio +### Audio The looping audio clip is used for the signals emitted by the traveler and their quantum instrument, and is the audio that gets played when they play their instrument. It should be a WAV file with 16 measures of music at 92 BPM (exactly 2,003,478 samples at 48,000 Hz, or approximately 42 seconds long). It is highly recommended that you use Audacity or another audio editor to trim your audio clip to exactly the same length as one of the vanilla traveler audio clips, or else it will fall gradually out of sync with the other instrument loops. @@ -116,7 +117,7 @@ The finale audio clip is only played once, after all travelers have started play The game plays all of the looping audio clips (including your custom one) simultaneously once you tell the first traveler to start playing, and then fades them in one by one as you talk to the others. After all travelers are playing, the game selects a finale audio clip that contains all Hearthian and Nomai/Owlk instruments mixed into one file, and then your custom finale audio clip will be layered over whichever vanilla clip plays. Only include your own instrument in the clip, and ensure it sounds okay alongside Solanum, the Prisoner, and both/neither. -#### Dialogue +### Dialogue The dialogue XML for your traveler works like other dialogue (see the [Dialogue](/guides/dialogue/) guide) but there are specially named conditions you will need to use for the functionality to work as expected: - Use `AnyTravelersGathered` to check if any traveler has been gathered yet. This includes Riebeck and Esker, so it should always be true, unless you forcibly enable your traveler to be enabled early. @@ -125,7 +126,7 @@ The dialogue XML for your traveler works like other dialogue (see the [Dialogue] - Use a `` with the condition defined in your eye traveler config's `"startPlayingCondition"` on the node or dialogue option that should make your traveler start playing their instrument. This condition name must be unique and not conflict with other mods. - If you want your traveler to be present but have an option to not participate in the campfire song (like the Prisoner), use a `` with the condition defined in your eye traveler config's `"participatingCondition"` on the node or dialogue option where your traveler agrees to join in. This condition name must be unique and not conflict with other mods. -```xml +```xml title="planets/dialogue/SlateEyeTraveler.xml" Slate, Probably @@ -198,11 +199,11 @@ The dialogue XML for your traveler works like other dialogue (see the [Dialogue] ``` -#### Custom Animation +### Custom Animation To add custom animations to your Eye Traveler, there is some setup work that has to be done in Unity. You will need to set up your character in Unity and load them via asset bundle, like you would any other detail: -```json +```json title="planets/EyeOfTheUniverse.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", "name": "EyeOfTheUniverse", @@ -233,7 +234,7 @@ In your character object, find the `Animator` component and set its `Controller` If everything was set up correctly, your character should play their animations in-game. -### Quantum Instruments +## Quantum Instruments Quantum instruments are the interactible instruments, typically hidden by a short 'puzzle', that cause their corresponding traveler to appear around the campfire. They are just like any other detail prop (see [Detailing](/guides/details/)) but they have additional handling to only activate after gathering and speaking to Riebeck, like the other instrument 'puzzles' in the Eye sequence. @@ -245,7 +246,7 @@ Quantum instruments will automatically be included in the inflation animation th [Eye Travelers](#eye-travelers), [Quantum Instruments](#quantum-instruments), and [Instrument Zones](#instrument-zones) are all linked by their `"id"` properties. Ensure that your ID matches between those details and is unique enough to not conflict with other mods. -```json +```json title="planets/EyeOfTheUniverse.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", "name": "EyeOfTheUniverse", @@ -283,7 +284,7 @@ Quantum instruments will automatically be included in the inflation animation th To see the full list of quantum instrument properties and descriptions of what each property does, check [the QuantumInstrumentInfo schema](/schemas/body-schema/defs/quantuminstrumentinfo/). -### Instrument Zones +## Instrument Zones Instrument zones are just like any other detail prop (see [Detailing](/guides/details/)) but they have additional handling to only activate after gathering and speaking to Riebeck, like the other instrument 'puzzles' in the Eye sequence. @@ -291,7 +292,7 @@ Custom instrument zones will automatically be included in the inflation animatio [Eye Travelers](#eye-travelers), [Quantum Instruments](#quantum-instruments), and [Instrument Zones](#instrument-zones) are all linked by their `"id"` properties. Ensure that your ID matches between those details and is unique enough to not conflict with other mods. -```json +```json title="planets/EyeOfTheUniverse.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", "name": "EyeOfTheUniverse", @@ -322,20 +323,20 @@ To see the full list of instrument zone properties and descriptions of what each ## Eye-Specific Considerations -### Cross-System Details +#### Cross-System Details Specifying details with a `"path"` pointing to an object in the regular solar system normally wouldn't work, as the Eye of the Universe lives in a completely separate scene from the rest of the game and those objects don't exist at the Eye. New Horizons works around this by force-loading the regular solar system, grabbing any objects referenced in Eye of the Universe config files, and then attempting to preserve these objects when loading the Eye of the Universe scene. This can cause issues with many different kinds of props, especially interactive ones that depend on some other part of the solar system existing. Because the objects are not available outside of this workaround, objects from the regular solar system cannot be spawned in via the New Horizons API. -### Custom Planets +#### Custom Planets While you *can* define completely custom planets the same as you would in a regular custom solar system, they may exhibit weird orbital behaviors or pass through the existing static Eye of the Universe objects. Prefer adding onto the existing bodies or setting a `"staticPosition"` on your planet configs to lock them in place. -### The Player Ship and Ship Logs +#### The Player Ship and Ship Logs The player's ship does not exist in the Eye of the Universe scene. In addition to the obvious issues this causes (no access to the ship's warp functionality, ship spawn points being non-functional, etc.), the ship log computer not existing causes some methods of checking and learning ship log facts to not function at all while at the Eye. If you need to track whether the player has met certain conditions elsewhere in the game (for example, if they've previously met a character that you now want to appear at the campfire), consider using a Persistent Dialogue Condition, which does not have these issues. -### Mod Compatibility +#### Mod Compatibility Other existing and future story mods will want to add additional content to the Eye of the Universe, and unlike entirely custom planets and star systems, there is a high probability that objects placed at the Eye may overlap with those placed by other mods. When testing, try installing as many of these other mods as possible and seeing if the objects they add overlap with yours. If so, consider moving your objects to a different position. When possible, use New Horizons features that preserve compatibility between mods. \ No newline at end of file From 547e9ae8b628752690026af30452eafc047d4339 Mon Sep 17 00:00:00 2001 From: Heavenly Avenger Date: Sat, 8 Feb 2025 12:53:48 -0300 Subject: [PATCH 27/39] Applies revisions suggested by loco-choco. Basically steam achievements from "Indicative" to "Imperative" verbal mode (Brazillian Porutuguese gramatics) to be more consistent with steam achievements, and remove some unnecessary bits. --- .../Assets/translations/portuguese_br.json | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/NewHorizons/Assets/translations/portuguese_br.json b/NewHorizons/Assets/translations/portuguese_br.json index 100d293b..299a6037 100644 --- a/NewHorizons/Assets/translations/portuguese_br.json +++ b/NewHorizons/Assets/translations/portuguese_br.json @@ -15,7 +15,7 @@ "LOCK_AUTOPILOT_WARP": "Ajustar Piloto Automático para sistema solar", "RICH_PRESENCE_EXPLORING": "Explorando {0}.", "RICH_PRESENCE_WARPING": "Translocando para {0}.", - "OUTDATED_VERSION_WARNING": "AVISO\n\nO mod New Horizons funciona apenas na versão {0} ou superior. Você está usando a versão {1}.\n\nAtualize o Outer Wilds ou desinstale NH.", + "OUTDATED_VERSION_WARNING": "AVISO\n\nO mod New Horizons funciona apenas na versão {0} ou superior. Você está usando a versão {1}.\n\nAtualize Outer Wilds ou desinstale NH.", "JSON_FAILED_TO_LOAD": "Arquivo(s) inválido(s): {0}", "DEBUG_RAYCAST": "Raycast", "DEBUG_PLACE": "Posicionar objeto", @@ -31,39 +31,39 @@ "AchievementTranslations": { "NH_EATEN_OUTSIDE_BRAMBLE": { "Name": "Fenda de Contenção", - "Description": "Foi devorado fora das premisas de Abrolho Sombrio" + "Description": "Seja devorado fora das premisas de Abrolho Sombrio" }, "NH_MULTIPLE_SYSTEM": { "Name": "Viajante", - "Description": "Visitou 5 sistemas solares seguidos." + "Description": "Visite 5 sistemas solares seguidos." }, "NH_NEW_FREQ": { "Name": "Frequências Anômalas", - "Description": "Descobriu uma nova frequência." + "Description": "Descubra uma nova frequência." }, "NH_PROBE_LOST": { "Name": "Conexão Perdida", - "Description": "Perdeu seu pequeno batedor." + "Description": "Perca seu pequeno batedor." }, "NH_WARP_DRIVE": { "Name": "História Mal Contada", - "Description": "Usou a unidade de translocação da nave." + "Description": "Use a unidade de translocação da nave." }, "NH_VESSEL_WARP": { "Name": "História Acertada", - "Description": "Translocou para um sistema estelar usando o Hospedeiro." + "Description": "Transloque para um sistema estelar usando o Hospedeiro." }, "NH_RAFTING": { "Name": "A Jangada e os Furiosos", - "Description": "Deu um rolé de jangada." + "Description": "Dê um rolé de jangada." }, "NH_SUCKED_INTO_LAVA_BY_TORNADO": { "Name": "Dieclone", - "Description": "Foi sugado na lava por um tornado." + "Description": "Seja sugado na lava por um tornado." }, "NH_TALK_TO_FIVE_CHARACTERS": { "Name": "Social", - "Description": "Conversou com 5 personagens." + "Description": "Converse com 5 personagens." } } } From 7f6a25fd49c2600ac7cb729696fa4859260fa6e9 Mon Sep 17 00:00:00 2001 From: Heavenly Avenger Date: Sat, 8 Feb 2025 12:54:19 -0300 Subject: [PATCH 28/39] Corrects "premises" translation to pt_BR. Closest to "premises", "premisas" used is wrong. Closest correct would be "premissas" which means something different (presumptions perhaps). Translated then to "limits" which would translate literally to "boundaries" but reflects better the meaning of the sentence. --- NewHorizons/Assets/translations/portuguese_br.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/Assets/translations/portuguese_br.json b/NewHorizons/Assets/translations/portuguese_br.json index 299a6037..97f1d9cf 100644 --- a/NewHorizons/Assets/translations/portuguese_br.json +++ b/NewHorizons/Assets/translations/portuguese_br.json @@ -31,7 +31,7 @@ "AchievementTranslations": { "NH_EATEN_OUTSIDE_BRAMBLE": { "Name": "Fenda de Contenção", - "Description": "Seja devorado fora das premisas de Abrolho Sombrio" + "Description": "Seja devorado fora dos limites de Abrolho Sombrio" }, "NH_MULTIPLE_SYSTEM": { "Name": "Viajante", From 71050a21600550db37f99ac7a123aeb5412c586b Mon Sep 17 00:00:00 2001 From: Heavenly Avenger Date: Sat, 8 Feb 2025 12:56:02 -0300 Subject: [PATCH 29/39] Coesion in pt_BR "gender" matching to "mod". MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Elsewhere used as female gender, "a mod", from the "modificação" noun, uses also the female gender to this message. The "modificação" noun is a feminine noun, in Brazillian Portuguese. The article (a/o) here is necessary. --- NewHorizons/Assets/translations/portuguese_br.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/Assets/translations/portuguese_br.json b/NewHorizons/Assets/translations/portuguese_br.json index 97f1d9cf..279aa104 100644 --- a/NewHorizons/Assets/translations/portuguese_br.json +++ b/NewHorizons/Assets/translations/portuguese_br.json @@ -15,7 +15,7 @@ "LOCK_AUTOPILOT_WARP": "Ajustar Piloto Automático para sistema solar", "RICH_PRESENCE_EXPLORING": "Explorando {0}.", "RICH_PRESENCE_WARPING": "Translocando para {0}.", - "OUTDATED_VERSION_WARNING": "AVISO\n\nO mod New Horizons funciona apenas na versão {0} ou superior. Você está usando a versão {1}.\n\nAtualize Outer Wilds ou desinstale NH.", + "OUTDATED_VERSION_WARNING": "AVISO\n\nA mod New Horizons funciona apenas na versão {0} ou superior. Você está usando a versão {1}.\n\nAtualize Outer Wilds ou desinstale NH.", "JSON_FAILED_TO_LOAD": "Arquivo(s) inválido(s): {0}", "DEBUG_RAYCAST": "Raycast", "DEBUG_PLACE": "Posicionar objeto", From 037261ed2695efcd0860335113138b1da92868be Mon Sep 17 00:00:00 2001 From: xen-42 Date: Sat, 8 Feb 2025 22:47:53 -0500 Subject: [PATCH 30/39] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cab6d6ce..75307d4f 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Translation credits: - Spanish: Ciborgm9, Ink, GayCoffee - French: xen - Japanese: TRSasasusu +- Portuguese: avengerx, loco-choco New Horizons was based off [Marshmallow](https://github.com/misternebula/Marshmallow) was made by: From 187bf415dc37dcbfb4f9c7747d7510018f6a3077 Mon Sep 17 00:00:00 2001 From: AnonymousStrangerOW <169866456+AnonymousStrangerOW@users.noreply.github.com> Date: Sun, 9 Feb 2025 21:08:51 -0500 Subject: [PATCH 31/39] fixes warping to other systems death bug - the player no longer violently gets hurled out of the vessel when warping to custom-made system - possible issues involving violent hurling also happening with the ship when warping into other nh systems may have been fixed as well --- .../Handlers/InvulnerabilityHandler.cs | 24 +++++++++++++++---- NewHorizons/Handlers/PlayerSpawnHandler.cs | 12 ++++++++++ NewHorizons/Handlers/VesselWarpHandler.cs | 23 ++++++++++++++++++ NewHorizons/Main.cs | 2 +- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index 9aa0e793..6a9e8f1b 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -1,36 +1,50 @@ +using HarmonyLib; using NewHorizons.Utility.OWML; using UnityEngine; +using UnityEngine.SceneManagement; namespace NewHorizons.Handlers { + [HarmonyPatch] internal class InvulnerabilityHandler { private static float _defaultImpactDeathSpeed = -1f; + private static bool _invulnerable; public static void MakeInvulnerable(bool invulnerable) { NHLogger.Log($"Toggling immortality: {invulnerable}"); + _invulnerable = invulnerable; var deathManager = GetDeathManager(); var resources = GetPlayerResouces(); if (invulnerable) { - if (_defaultImpactDeathSpeed == -1f) - _defaultImpactDeathSpeed = deathManager._impactDeathSpeed; - - deathManager._impactDeathSpeed = Mathf.Infinity; deathManager._invincible = true; } else { - deathManager._impactDeathSpeed = _defaultImpactDeathSpeed; resources._currentHealth = 100f; deathManager._invincible = false; } } + [HarmonyPrefix] + [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] + [HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.ApplyInstantDamage))] + [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] + public static bool DeathManager_KillPlayer_Prefix() + { + return !_invulnerable; + } + private static DeathManager GetDeathManager() => GameObject.FindObjectOfType(); private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType(); + + static InvulnerabilityHandler() + { + SceneManager.sceneUnloaded += (_) => _invulnerable = false; + } } } diff --git a/NewHorizons/Handlers/PlayerSpawnHandler.cs b/NewHorizons/Handlers/PlayerSpawnHandler.cs index 352b4eff..cca2a620 100644 --- a/NewHorizons/Handlers/PlayerSpawnHandler.cs +++ b/NewHorizons/Handlers/PlayerSpawnHandler.cs @@ -70,6 +70,18 @@ namespace NewHorizons.Handlers // Spawn ship Delay.FireInNUpdates(SpawnShip, 30); + Delay.FireInNUpdates(() => + { + var spawnOWRigidBody = GetDefaultSpawn().GetAttachedOWRigidbody(); + if (shouldWarpInFromVessel) spawnOWRigidBody = VesselWarpHandler.VesselSpawnPoint.GetAttachedOWRigidbody(); + if (shouldWarpInFromShip) spawnOWRigidBody = Locator.GetShipBody(); + + var spawnVelocity = spawnOWRigidBody.GetVelocity(); + var spawnAngularVelocity = spawnOWRigidBody.GetPointTangentialVelocity(Locator.GetPlayerBody().GetPosition()); + var velocity = spawnVelocity + spawnAngularVelocity; + + Locator.GetPlayerBody().SetVelocity(velocity); + }, 31); } public static void SpawnShip() diff --git a/NewHorizons/Handlers/VesselWarpHandler.cs b/NewHorizons/Handlers/VesselWarpHandler.cs index bac8fdf7..ba7d4404 100644 --- a/NewHorizons/Handlers/VesselWarpHandler.cs +++ b/NewHorizons/Handlers/VesselWarpHandler.cs @@ -6,6 +6,7 @@ using NewHorizons.Utility; using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OWML; using UnityEngine; +using System.Collections; using static NewHorizons.Main; namespace NewHorizons.Handlers @@ -84,6 +85,8 @@ namespace NewHorizons.Handlers { NHLogger.LogVerbose("Relative warping into vessel"); vesselSpawnPoint.WarpPlayer();//Delay.FireOnNextUpdate(vesselSpawnPoint.WarpPlayer); + //Delay.FireInNUpdates(() => InvulnerabilityHandler.MakeInvulnerable(false), 25); + Delay.StartCoroutine(RunEveryNFrames(25, vesselSpawnPoint)); } else { @@ -95,6 +98,26 @@ namespace NewHorizons.Handlers LoadDB(); } + private static IEnumerator RunEveryNFrames(int frameInterval, VesselSpawnPoint vesselSpawn) + { + int frameCount = 0; + InvulnerabilityHandler.MakeInvulnerable(true); + + while (frameCount <= frameInterval) + { + vesselSpawn.WarpPlayer(); + if (frameCount == frameInterval) + { + InvulnerabilityHandler.MakeInvulnerable(false); + var playerBody = SearchUtilities.Find("Player_Body").GetAttachedOWRigidbody(); + var resources = playerBody.GetComponent(); + resources._currentHealth = 100f; + } + frameCount++; + yield return null; // Wait for the next frame + } + } + public static void LoadDB() { if (Instance.CurrentStarSystem == "SolarSystem") diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 41827310..8de703be 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -614,7 +614,7 @@ namespace NewHorizons IsSystemReady = true; // ShipWarpController will handle the invulnerability otherwise - if (!shouldWarpInFromShip) + if (!shouldWarpInFromShip && !shouldWarpInFromVessel) { Delay.FireOnNextUpdate(() => InvulnerabilityHandler.MakeInvulnerable(false)); } From 0f38b022f320c5e28633779f3af9c52a71a5b5de Mon Sep 17 00:00:00 2001 From: xen-42 Date: Sun, 9 Feb 2025 21:14:35 -0500 Subject: [PATCH 32/39] Changes I requested --- .../Components/EyeOfTheUniverse/InstrumentZone.cs | 3 +++ NewHorizons/Handlers/PlanetCreationHandler.cs | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs b/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs index 39b0d529..37d0948f 100644 --- a/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs +++ b/NewHorizons/Components/EyeOfTheUniverse/InstrumentZone.cs @@ -2,6 +2,9 @@ using UnityEngine; namespace NewHorizons.Components.EyeOfTheUniverse { + /// + /// Class does nothing but is used with GetComponent to find instrument zones in the hierarchy + /// public class InstrumentZone : MonoBehaviour { diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 27b67234..fe6abb8d 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -693,7 +693,14 @@ namespace NewHorizons.Handlers if (body.Config.EyeOfTheUniverse != null) { - EyeOfTheUniverseBuilder.Make(go, sector, body.Config.EyeOfTheUniverse, body); + if (Main.Instance.CurrentStarSystem == "EyeOfTheUniverse") + { + EyeOfTheUniverseBuilder.Make(go, sector, body.Config.EyeOfTheUniverse, body); + } + else + { + NHLogger.LogWarning($"A mod creator (you?) has defined Eye of the Universe specific settings on a body [{body.Config.name}] that is not in the eye of the universe"); + } } if (body.Config.ParticleFields != null) From 599b5ff8081d7dd7c831856ec69a23efb7a4de1d Mon Sep 17 00:00:00 2001 From: xen-42 Date: Sun, 9 Feb 2025 23:17:54 -0500 Subject: [PATCH 33/39] Fix anglerfish at eye --- NewHorizons/Builder/Props/DetailBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 2b7dbfde..72f97c6c 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -486,7 +486,10 @@ namespace NewHorizons.Builder.Props // Disable the angler anim controller because we don't want Update or LateUpdate to run, just need it to set the initial Animator state angler.enabled = false; angler.OnChangeAnglerState(AnglerfishController.AnglerState.Lurking); - + + angler._animator.SetFloat("MoveSpeed", angler._moveCurrent); + angler._animator.SetFloat("Jaw", angler._jawCurrent); + Destroy(this); } } From ddb1d50816d9fe19501ce441a402225c33ed55cc Mon Sep 17 00:00:00 2001 From: xen-42 Date: Sun, 9 Feb 2025 23:26:16 -0500 Subject: [PATCH 34/39] Reorganize a bit and add more comments --- .../Handlers/InvulnerabilityHandler.cs | 2 ++ NewHorizons/Handlers/PlayerSpawnHandler.cs | 26 +++++++++++------- NewHorizons/Handlers/VesselWarpHandler.cs | 27 ++++++++++--------- NewHorizons/Main.cs | 2 +- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index 6a9e8f1b..e6f3777a 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -36,6 +36,8 @@ namespace NewHorizons.Handlers [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] public static bool DeathManager_KillPlayer_Prefix() { + // Base game _invincible is still overriden by high speed impacts + // We also are avoiding playing impact related effects by just skipping these methods return !_invulnerable; } diff --git a/NewHorizons/Handlers/PlayerSpawnHandler.cs b/NewHorizons/Handlers/PlayerSpawnHandler.cs index cca2a620..24b6d73c 100644 --- a/NewHorizons/Handlers/PlayerSpawnHandler.cs +++ b/NewHorizons/Handlers/PlayerSpawnHandler.cs @@ -70,18 +70,24 @@ namespace NewHorizons.Handlers // Spawn ship Delay.FireInNUpdates(SpawnShip, 30); - Delay.FireInNUpdates(() => - { - var spawnOWRigidBody = GetDefaultSpawn().GetAttachedOWRigidbody(); - if (shouldWarpInFromVessel) spawnOWRigidBody = VesselWarpHandler.VesselSpawnPoint.GetAttachedOWRigidbody(); - if (shouldWarpInFromShip) spawnOWRigidBody = Locator.GetShipBody(); - var spawnVelocity = spawnOWRigidBody.GetVelocity(); - var spawnAngularVelocity = spawnOWRigidBody.GetPointTangentialVelocity(Locator.GetPlayerBody().GetPosition()); - var velocity = spawnVelocity + spawnAngularVelocity; + // Have had bug reports (#1034, #975) where sometimes after spawning via vessel warp or ship warp you die from impact velocity after being flung + // Something weird must be happening with velocity. + // Try to correct it here after the ship is done spawning + Delay.FireInNUpdates(() => FixVelocity(shouldWarpInFromVessel, shouldWarpInFromShip), 31); + } - Locator.GetPlayerBody().SetVelocity(velocity); - }, 31); + private static void FixVelocity(bool shouldWarpInFromVessel, bool shouldWarpInFromShip) + { + var spawnOWRigidBody = GetDefaultSpawn().GetAttachedOWRigidbody(); + if (shouldWarpInFromVessel) spawnOWRigidBody = VesselWarpHandler.VesselSpawnPoint.GetAttachedOWRigidbody(); + if (shouldWarpInFromShip) spawnOWRigidBody = Locator.GetShipBody(); + + var spawnVelocity = spawnOWRigidBody.GetVelocity(); + var spawnAngularVelocity = spawnOWRigidBody.GetPointTangentialVelocity(Locator.GetPlayerBody().GetPosition()); + var velocity = spawnVelocity + spawnAngularVelocity; + + Locator.GetPlayerBody().SetVelocity(velocity); } public static void SpawnShip() diff --git a/NewHorizons/Handlers/VesselWarpHandler.cs b/NewHorizons/Handlers/VesselWarpHandler.cs index ba7d4404..25aac76b 100644 --- a/NewHorizons/Handlers/VesselWarpHandler.cs +++ b/NewHorizons/Handlers/VesselWarpHandler.cs @@ -56,7 +56,9 @@ namespace NewHorizons.Handlers } if (IsVesselPresentAndActive()) + { _vesselSpawnPoint = Instance.CurrentStarSystem == "SolarSystem" ? UpdateVessel() : CreateVessel(); + } else { var vesselDimension = SearchUtilities.Find("DB_VesselDimension_Body/Sector_VesselDimension"); @@ -84,9 +86,12 @@ namespace NewHorizons.Handlers if (_vesselSpawnPoint is VesselSpawnPoint vesselSpawnPoint) { NHLogger.LogVerbose("Relative warping into vessel"); - vesselSpawnPoint.WarpPlayer();//Delay.FireOnNextUpdate(vesselSpawnPoint.WarpPlayer); - //Delay.FireInNUpdates(() => InvulnerabilityHandler.MakeInvulnerable(false), 25); - Delay.StartCoroutine(RunEveryNFrames(25, vesselSpawnPoint)); + vesselSpawnPoint.WarpPlayer(); + + // #1034 Vessel warp sometimes has the player get flung away into space and die + // We do what we do with regular spawns where we keep resetting their position to the right one while invincible until we're relatively certain + // that the spawning sequence is done + Delay.StartCoroutine(FixPlayerSpawning(25, vesselSpawnPoint)); } else { @@ -98,24 +103,22 @@ namespace NewHorizons.Handlers LoadDB(); } - private static IEnumerator RunEveryNFrames(int frameInterval, VesselSpawnPoint vesselSpawn) + private static IEnumerator FixPlayerSpawning(int frameInterval, VesselSpawnPoint vesselSpawn) { - int frameCount = 0; InvulnerabilityHandler.MakeInvulnerable(true); + var frameCount = 0; while (frameCount <= frameInterval) { vesselSpawn.WarpPlayer(); - if (frameCount == frameInterval) - { - InvulnerabilityHandler.MakeInvulnerable(false); - var playerBody = SearchUtilities.Find("Player_Body").GetAttachedOWRigidbody(); - var resources = playerBody.GetComponent(); - resources._currentHealth = 100f; - } frameCount++; yield return null; // Wait for the next frame } + + InvulnerabilityHandler.MakeInvulnerable(false); + var playerBody = SearchUtilities.Find("Player_Body").GetAttachedOWRigidbody(); + var resources = playerBody.GetComponent(); + resources._currentHealth = 100f; } public static void LoadDB() diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 8de703be..995f46c5 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -613,7 +613,7 @@ namespace NewHorizons { IsSystemReady = true; - // ShipWarpController will handle the invulnerability otherwise + // ShipWarpController or VesselWarpHandler will handle the invulnerability otherwise if (!shouldWarpInFromShip && !shouldWarpInFromVessel) { Delay.FireOnNextUpdate(() => InvulnerabilityHandler.MakeInvulnerable(false)); From 71d6f5732dcc496eef4bf221b2c556f4fd6aa501 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Sun, 9 Feb 2025 23:28:36 -0500 Subject: [PATCH 35/39] Bump version --- NewHorizons/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 4454f483..3503b410 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, JohnCorby, MegaPiggy, Trifid, and friends", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.25.2", + "version": "1.26.0", "owmlVersion": "2.12.1", "dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "PacificEngine.OW_CommonResources" ], From 90cf5880edafb2c885819a2f17a7f07cc7085437 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Mon, 10 Feb 2025 10:01:51 -0500 Subject: [PATCH 36/39] Address review - use _invinicible in its own file, comment orbitlines --- .../Handlers/InvulnerabilityHandler.cs | 23 ------------------- NewHorizons/Handlers/PlanetCreationHandler.cs | 2 ++ NewHorizons/Patches/InvincibilityPatches.cs | 19 +++++++++++++++ 3 files changed, 21 insertions(+), 23 deletions(-) create mode 100644 NewHorizons/Patches/InvincibilityPatches.cs diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index e6f3777a..00df816e 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -1,21 +1,14 @@ -using HarmonyLib; using NewHorizons.Utility.OWML; using UnityEngine; -using UnityEngine.SceneManagement; namespace NewHorizons.Handlers { - [HarmonyPatch] internal class InvulnerabilityHandler { - private static float _defaultImpactDeathSpeed = -1f; - private static bool _invulnerable; - public static void MakeInvulnerable(bool invulnerable) { NHLogger.Log($"Toggling immortality: {invulnerable}"); - _invulnerable = invulnerable; var deathManager = GetDeathManager(); var resources = GetPlayerResouces(); @@ -30,23 +23,7 @@ namespace NewHorizons.Handlers } } - [HarmonyPrefix] - [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] - [HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.ApplyInstantDamage))] - [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] - public static bool DeathManager_KillPlayer_Prefix() - { - // Base game _invincible is still overriden by high speed impacts - // We also are avoiding playing impact related effects by just skipping these methods - return !_invulnerable; - } - private static DeathManager GetDeathManager() => GameObject.FindObjectOfType(); private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType(); - - static InvulnerabilityHandler() - { - SceneManager.sceneUnloaded += (_) => _invulnerable = false; - } } } diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index e254abef..6898382f 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -514,6 +514,7 @@ namespace NewHorizons.Handlers if (body.Config.Orbit.showOrbitLine && !body.Config.Orbit.isStatic) { + // No map mode at eye if (LoadManager.GetCurrentScene() != OWScene.EyeOfTheUniverse) { OrbitlineBuilder.Make(body.Object, body.Config.Orbit.isMoon, body.Config); @@ -858,6 +859,7 @@ namespace NewHorizons.Handlers var isMoon = newAO.GetAstroObjectType() is AstroObject.Type.Moon or AstroObject.Type.Satellite or AstroObject.Type.SpaceStation; if (body.Config.Orbit.showOrbitLine) { + // No map mode at eye if (LoadManager.GetCurrentScene() != OWScene.EyeOfTheUniverse) { OrbitlineBuilder.Make(go, isMoon, body.Config); diff --git a/NewHorizons/Patches/InvincibilityPatches.cs b/NewHorizons/Patches/InvincibilityPatches.cs new file mode 100644 index 00000000..0a7f58e9 --- /dev/null +++ b/NewHorizons/Patches/InvincibilityPatches.cs @@ -0,0 +1,19 @@ +using HarmonyLib; + +namespace NewHorizons.Patches +{ + [HarmonyPatch] + public static class InvincibilityPatches + { + [HarmonyPrefix] + [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] + [HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.ApplyInstantDamage))] + [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] + public static bool DeathManager_KillPlayer_Prefix() + { + // Base game _invincible is still overriden by high speed impacts + // We also are avoiding playing impact related effects by just skipping these methods + return !(Locator.GetDeathManager()?._invincible ?? false); + } + } +} From d1c8d132aebab9de1165b4dc2bc346973efc739c Mon Sep 17 00:00:00 2001 From: xen-42 Date: Mon, 10 Feb 2025 10:09:02 -0500 Subject: [PATCH 37/39] Revert changes to InvulnerabilityHandler --- .../Handlers/InvulnerabilityHandler.cs | 24 +++++++++++++++++++ NewHorizons/Patches/InvincibilityPatches.cs | 19 --------------- 2 files changed, 24 insertions(+), 19 deletions(-) delete mode 100644 NewHorizons/Patches/InvincibilityPatches.cs diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index 00df816e..cb48fe22 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -1,14 +1,21 @@ +using HarmonyLib; using NewHorizons.Utility.OWML; using UnityEngine; +using UnityEngine.SceneManagement; namespace NewHorizons.Handlers { + [HarmonyPatch] internal class InvulnerabilityHandler { + private static bool _invulnerableOverride; + public static void MakeInvulnerable(bool invulnerable) { NHLogger.Log($"Toggling immortality: {invulnerable}"); + // We're setting our own override because we want to ensure that no other mod that can set _invincible can break this for us + _invulnerableOverride = invulnerable; var deathManager = GetDeathManager(); var resources = GetPlayerResouces(); @@ -23,7 +30,24 @@ namespace NewHorizons.Handlers } } + [HarmonyPrefix] + [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] + [HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.ApplyInstantDamage))] + [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] + public static bool DeathManager_KillPlayer_Prefix() + { + // Base game _invincible is still overriden by high speed impacts + // We also are avoiding playing impact related effects by just skipping these methods + return !_invulnerableOverride; + } + private static DeathManager GetDeathManager() => GameObject.FindObjectOfType(); private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType(); + + static InvulnerabilityHandler() + { + // If the scene unloads when _invulnerableOverride is on it might not get turned off + SceneManager.sceneUnloaded += (_) => _invulnerableOverride = false; + } } } diff --git a/NewHorizons/Patches/InvincibilityPatches.cs b/NewHorizons/Patches/InvincibilityPatches.cs deleted file mode 100644 index 0a7f58e9..00000000 --- a/NewHorizons/Patches/InvincibilityPatches.cs +++ /dev/null @@ -1,19 +0,0 @@ -using HarmonyLib; - -namespace NewHorizons.Patches -{ - [HarmonyPatch] - public static class InvincibilityPatches - { - [HarmonyPrefix] - [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] - [HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.ApplyInstantDamage))] - [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] - public static bool DeathManager_KillPlayer_Prefix() - { - // Base game _invincible is still overriden by high speed impacts - // We also are avoiding playing impact related effects by just skipping these methods - return !(Locator.GetDeathManager()?._invincible ?? false); - } - } -} From d5d9200331c14e19a2944ad59a8bf89bd5312f2b Mon Sep 17 00:00:00 2001 From: xen-42 Date: Mon, 10 Feb 2025 16:07:02 -0500 Subject: [PATCH 38/39] Don't use patch, use invincible field on player resources --- .../Handlers/InvulnerabilityHandler.cs | 24 ++----------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index cb48fe22..3649cf70 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -5,49 +5,29 @@ using UnityEngine.SceneManagement; namespace NewHorizons.Handlers { - [HarmonyPatch] internal class InvulnerabilityHandler { - private static bool _invulnerableOverride; - public static void MakeInvulnerable(bool invulnerable) { NHLogger.Log($"Toggling immortality: {invulnerable}"); - // We're setting our own override because we want to ensure that no other mod that can set _invincible can break this for us - _invulnerableOverride = invulnerable; var deathManager = GetDeathManager(); var resources = GetPlayerResouces(); if (invulnerable) { deathManager._invincible = true; + resources._invincible = true; } else { resources._currentHealth = 100f; deathManager._invincible = false; + resources._invincible = false; } } - [HarmonyPrefix] - [HarmonyPatch(typeof(DeathManager), nameof(DeathManager.KillPlayer))] - [HarmonyPatch(typeof(PlayerResources), nameof(PlayerResources.ApplyInstantDamage))] - [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] - public static bool DeathManager_KillPlayer_Prefix() - { - // Base game _invincible is still overriden by high speed impacts - // We also are avoiding playing impact related effects by just skipping these methods - return !_invulnerableOverride; - } - private static DeathManager GetDeathManager() => GameObject.FindObjectOfType(); private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType(); - - static InvulnerabilityHandler() - { - // If the scene unloads when _invulnerableOverride is on it might not get turned off - SceneManager.sceneUnloaded += (_) => _invulnerableOverride = false; - } } } From 8a1fa3de979c694448a7a69858ab3589778b8ca4 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Mon, 10 Feb 2025 16:12:02 -0500 Subject: [PATCH 39/39] Bring back patch to impact audio --- .../Handlers/InvulnerabilityHandler.cs | 13 +++++++++++- .../Patches/PlayerImpactAudioPatches.cs | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 NewHorizons/Patches/PlayerImpactAudioPatches.cs diff --git a/NewHorizons/Handlers/InvulnerabilityHandler.cs b/NewHorizons/Handlers/InvulnerabilityHandler.cs index 3649cf70..cacc9e19 100644 --- a/NewHorizons/Handlers/InvulnerabilityHandler.cs +++ b/NewHorizons/Handlers/InvulnerabilityHandler.cs @@ -1,4 +1,3 @@ -using HarmonyLib; using NewHorizons.Utility.OWML; using UnityEngine; using UnityEngine.SceneManagement; @@ -7,10 +6,16 @@ namespace NewHorizons.Handlers { internal class InvulnerabilityHandler { + /// + /// Used in patches + /// + public static bool Invincible { get; private set; } + public static void MakeInvulnerable(bool invulnerable) { NHLogger.Log($"Toggling immortality: {invulnerable}"); + Invincible = invulnerable; var deathManager = GetDeathManager(); var resources = GetPlayerResouces(); @@ -29,5 +34,11 @@ namespace NewHorizons.Handlers private static DeathManager GetDeathManager() => GameObject.FindObjectOfType(); private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType(); + + static InvulnerabilityHandler() + { + // If the scene unloads when Invincible is on it might not get turned off + SceneManager.sceneUnloaded += (_) => Invincible = false; + } } } diff --git a/NewHorizons/Patches/PlayerImpactAudioPatches.cs b/NewHorizons/Patches/PlayerImpactAudioPatches.cs new file mode 100644 index 00000000..bf583229 --- /dev/null +++ b/NewHorizons/Patches/PlayerImpactAudioPatches.cs @@ -0,0 +1,21 @@ +using HarmonyLib; +using NewHorizons.Handlers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.Patches; + +[HarmonyPatch] +public static class PlayerImpactAudioPatches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(PlayerImpactAudio), nameof(PlayerImpactAudio.OnImpact))] + public static bool PlayerImpactAudio_OnImpact() + { + // DeathManager and PlayerResources _invincible stops player dying but you still hear the impact sounds which is annoying so we disable them + return !InvulnerabilityHandler.Invincible; + } +}