diff --git a/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs b/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs index fb405705..c177a9ed 100644 --- a/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs @@ -20,6 +20,16 @@ namespace NewHorizons.Builder.Atmosphere SCG._dynamicCullingBounds = false; SCG._waitForStreaming = false; + var minHeight = surfaceSize; + var maxHeight = config.Atmosphere.size; + if (config.HeightMap?.minHeight != null) + { + if (config.Water?.size >= config.HeightMap.minHeight) minHeight = config.Water.size; // use sea level if its higher + else minHeight = config.HeightMap.minHeight; + } + else if (config.Water?.size != null) minHeight = config.Water.size; + else if (config.Lava?.size != null) minHeight = config.Lava.size; + if (config.Atmosphere.hasRain) { var rainGO = GameObject.Instantiate(SearchUtilities.Find("GiantsDeep_Body/Sector_GD/Sector_GDInterior/Effects_GDInterior/Effects_GD_Rain"), effectsGO.transform); @@ -29,9 +39,9 @@ namespace NewHorizons.Builder.Atmosphere var pvc = rainGO.GetComponent(); pvc._densityByHeight = new AnimationCurve(new Keyframe[] { - new Keyframe(surfaceSize - 0.5f, 0), - new Keyframe(surfaceSize, 10f), - new Keyframe(config.Atmosphere.size, 0f) + new Keyframe(minHeight - 0.5f, 0), + new Keyframe(minHeight, 10f), + new Keyframe(maxHeight, 0f) }); rainGO.GetComponent()._activeInSector = sector; @@ -53,9 +63,9 @@ namespace NewHorizons.Builder.Atmosphere var pvc = snowEmitter.GetComponent(); pvc._densityByHeight = new AnimationCurve(new Keyframe[] { - new Keyframe(surfaceSize - 0.5f, 0), - new Keyframe(surfaceSize, 10f), - new Keyframe(config.Atmosphere.size, 0f) + new Keyframe(minHeight - 0.5f, 0), + new Keyframe(minHeight, 10f), + new Keyframe(maxHeight, 0f) }); snowEmitter.GetComponent()._activeInSector = sector; diff --git a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs index 65fa1fcf..18927c1f 100644 --- a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs +++ b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs @@ -192,8 +192,12 @@ namespace NewHorizons.Builder.Body cloak.GetComponent().enabled = true; // Cull stuff - var cullController = go.AddComponent(); - cullController.SetSector(sector); + // Do next update so other nodes can be built first + Delay.FireOnNextUpdate(() => + { + var cullController = go.AddComponent(); + cullController.SetSector(sector); + }); // finalize atmo.SetActive(true); diff --git a/NewHorizons/Builder/Props/PropBuildManager.cs b/NewHorizons/Builder/Props/PropBuildManager.cs index 8f442041..bda9684d 100644 --- a/NewHorizons/Builder/Props/PropBuildManager.cs +++ b/NewHorizons/Builder/Props/PropBuildManager.cs @@ -109,20 +109,6 @@ namespace NewHorizons.Builder.Props } } } - if (config.Props.reveal != null) - { - foreach (var revealInfo in config.Props.reveal) - { - try - { - RevealBuilder.Make(go, sector, revealInfo, mod); - } - catch (Exception ex) - { - Logger.LogError($"Couldn't make reveal location [{revealInfo.reveals}] for [{go.name}]:\n{ex}"); - } - } - } if (config.Props.entryLocation != null) { foreach (var entryLocationInfo in config.Props.entryLocation) @@ -207,13 +193,6 @@ namespace NewHorizons.Builder.Props } } } - if (config.Props.audioVolumes != null) - { - foreach (var audioVolume in config.Props.audioVolumes) - { - AudioVolumeBuilder.Make(go, sector, audioVolume, mod); - } - } if (config.Props.signals != null) { foreach (var signal in config.Props.signals) diff --git a/NewHorizons/Builder/ShipLog/RevealBuilder.cs b/NewHorizons/Builder/ShipLog/RevealBuilder.cs index 43133771..95f118b6 100644 --- a/NewHorizons/Builder/ShipLog/RevealBuilder.cs +++ b/NewHorizons/Builder/ShipLog/RevealBuilder.cs @@ -7,18 +7,18 @@ namespace NewHorizons.Builder.ShipLog { public static class RevealBuilder { - public static void Make(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + public static void Make(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { var newRevealGO = MakeGameObject(go, sector, info, mod); switch (info.revealOn) { - case PropModule.RevealInfo.RevealVolumeType.Enter: + case VolumesModule.RevealVolumeInfo.RevealVolumeType.Enter: MakeTrigger(newRevealGO, sector, info, mod); break; - case PropModule.RevealInfo.RevealVolumeType.Observe: + case VolumesModule.RevealVolumeInfo.RevealVolumeType.Observe: MakeObservable(newRevealGO, sector, info, mod); break; - case PropModule.RevealInfo.RevealVolumeType.Snapshot: + case VolumesModule.RevealVolumeInfo.RevealVolumeType.Snapshot: MakeSnapshot(newRevealGO, sector, info, mod); break; default: @@ -28,7 +28,7 @@ namespace NewHorizons.Builder.ShipLog newRevealGO.SetActive(true); } - private static SphereShape MakeShape(GameObject go, PropModule.RevealInfo info, Shape.CollisionMode collisionMode) + private static SphereShape MakeShape(GameObject go, VolumesModule.RevealVolumeInfo info, Shape.CollisionMode collisionMode) { SphereShape newShape = go.AddComponent(); newShape.radius = info.radius; @@ -36,7 +36,7 @@ namespace NewHorizons.Builder.ShipLog return newShape; } - private static GameObject MakeGameObject(GameObject planetGO, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static GameObject MakeGameObject(GameObject planetGO, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { GameObject revealTriggerVolume = new GameObject("Reveal Volume (" + info.revealOn + ")"); revealTriggerVolume.SetActive(false); @@ -45,7 +45,7 @@ namespace NewHorizons.Builder.ShipLog return revealTriggerVolume; } - private static void MakeTrigger(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static void MakeTrigger(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { var shape = MakeShape(go, info, Shape.CollisionMode.Volume); @@ -65,7 +65,7 @@ namespace NewHorizons.Builder.ShipLog } } - private static void MakeObservable(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static void MakeObservable(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { go.layer = LayerMask.NameToLayer("Interactible"); @@ -96,7 +96,7 @@ namespace NewHorizons.Builder.ShipLog } } - private static void MakeSnapshot(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static void MakeSnapshot(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { var shape = MakeShape(go, info, Shape.CollisionMode.Manual); diff --git a/NewHorizons/Builder/Props/AudioVolumeBuilder.cs b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs similarity index 91% rename from NewHorizons/Builder/Props/AudioVolumeBuilder.cs rename to NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs index 6e62d9cb..9946d31e 100644 --- a/NewHorizons/Builder/Props/AudioVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs @@ -10,11 +10,11 @@ using System.Threading.Tasks; using UnityEngine; using Logger = NewHorizons.Utility.Logger; -namespace NewHorizons.Builder.Props +namespace NewHorizons.Builder.Volumes { public static class AudioVolumeBuilder { - public static AudioVolume Make(GameObject planetGO, Sector sector, PropModule.AudioVolumeInfo info, IModBehaviour mod) + public static AudioVolume Make(GameObject planetGO, Sector sector, VolumesModule.AudioVolumeInfo info, IModBehaviour mod) { var go = new GameObject("AudioVolume"); go.SetActive(false); @@ -27,7 +27,7 @@ namespace NewHorizons.Builder.Props var owAudioSource = go.AddComponent(); owAudioSource._audioSource = audioSource; - owAudioSource.loop = true; + owAudioSource.loop = info.loop; owAudioSource.SetTrack(EnumUtils.Parse(info.track.ToString())); AudioUtilities.SetAudioClip(owAudioSource, info.audio, mod); diff --git a/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs new file mode 100644 index 00000000..67c6f22c --- /dev/null +++ b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs @@ -0,0 +1,40 @@ +using NewHorizons.External.Modules; +using OWML.Common; +using OWML.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Builder.Volumes +{ + public static class HazardVolumeBuilder + { + public static HazardVolume Make(GameObject planetGO, Sector sector, OWRigidbody owrb, VolumesModule.HazardVolumeInfo info, IModBehaviour mod) + { + var go = new GameObject("HazardVolume"); + go.SetActive(false); + + go.transform.parent = sector?.transform ?? planetGO.transform; + go.transform.position = planetGO.transform.TransformPoint(info.position != null ? (Vector3)info.position : Vector3.zero); + go.layer = LayerMask.NameToLayer("BasicEffectVolume"); + + var shape = go.AddComponent(); + shape.radius = info.radius; + + var owTriggerVolume = go.AddComponent(); + owTriggerVolume._shape = shape; + + var hazardVolume = go.AddComponent(); + hazardVolume._attachedBody = owrb; + hazardVolume._type = EnumUtils.Parse(info.type.ToString(), HazardVolume.HazardType.GENERAL); + hazardVolume._damagePerSecond = info.damagePerSecond; + hazardVolume._firstContactDamageType = EnumUtils.Parse(info.firstContactDamageType.ToString(), InstantDamageType.Impact); + hazardVolume._firstContactDamage = info.firstContactDamage; + + go.SetActive(true); + + return hazardVolume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs new file mode 100644 index 00000000..c91e4161 --- /dev/null +++ b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs @@ -0,0 +1,43 @@ +using NewHorizons.Components; +using NewHorizons.External.Modules; +using NewHorizons.Utility; +using OWML.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; +using NHNotificationVolume = NewHorizons.Components.NotificationVolume; + +namespace NewHorizons.Builder.Volumes +{ + public static class NotificationVolumeBuilder + { + public static NHNotificationVolume Make(GameObject planetGO, Sector sector, VolumesModule.NotificationVolumeInfo info, IModBehaviour mod) + { + var go = new GameObject("NotificationVolume"); + go.SetActive(false); + + go.transform.parent = sector?.transform ?? planetGO.transform; + go.transform.position = planetGO.transform.TransformPoint(info.position != null ? (Vector3)info.position : Vector3.zero); + go.layer = LayerMask.NameToLayer("BasicEffectVolume"); + + var shape = go.AddComponent(); + shape.radius = info.radius; + + var owTriggerVolume = go.AddComponent(); + owTriggerVolume._shape = shape; + + var notificationVolume = go.AddComponent(); + notificationVolume.SetTarget(info.target); + if (info.entryNotification != null) notificationVolume.SetEntryNotification(info.entryNotification.displayMessage, info.entryNotification.duration); + if (info.exitNotification != null) notificationVolume.SetExitNotification(info.exitNotification.displayMessage, info.exitNotification.duration); + + go.SetActive(true); + + return notificationVolume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs new file mode 100644 index 00000000..f7a664b9 --- /dev/null +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -0,0 +1,54 @@ +using NewHorizons.Builder.Body; +using NewHorizons.Builder.ShipLog; +using NewHorizons.Builder.Volumes; +using NewHorizons.External.Configs; +using OWML.Common; +using System; +using System.Collections.Generic; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.Builder.Volumes +{ + public static class VolumesBuildManager + { + public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, PlanetConfig config, IModBehaviour mod) + { + if (config.Volumes.revealVolumes != null) + { + foreach (var revealInfo in config.Volumes.revealVolumes) + { + try + { + RevealBuilder.Make(go, sector, revealInfo, mod); + } + catch (Exception ex) + { + Logger.LogError($"Couldn't make reveal location [{revealInfo.reveals}] for [{go.name}]:\n{ex}"); + } + } + } + if (config.Volumes.audioVolumes != null) + { + foreach (var audioVolume in config.Volumes.audioVolumes) + { + AudioVolumeBuilder.Make(go, sector, audioVolume, mod); + } + } + if (config.Volumes.notificationVolumes != null) + { + foreach (var notificationVolume in config.Volumes.notificationVolumes) + { + NotificationVolumeBuilder.Make(go, sector, notificationVolume, mod); + } + } + if (config.Volumes.hazardVolumes != null) + { + foreach (var hazardVolume in config.Volumes.hazardVolumes) + { + HazardVolumeBuilder.Make(go, sector, planetBody, hazardVolume, mod); + } + } + } + } +} diff --git a/NewHorizons/Components/NotificationVolume.cs b/NewHorizons/Components/NotificationVolume.cs new file mode 100644 index 00000000..2dc81ed1 --- /dev/null +++ b/NewHorizons/Components/NotificationVolume.cs @@ -0,0 +1,129 @@ +using NewHorizons.Handlers; +using OWML.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components +{ + [RequireComponent(typeof(OWTriggerVolume))] + public class NotificationVolume : MonoBehaviour + { + private NotificationTarget _target = NotificationTarget.All; + private bool _pin = false; + private OWTriggerVolume _triggerVolume; + private NotificationData _entryNotification; + private NotificationData _exitNotification; + + public void Awake() + { + _triggerVolume = this.GetRequiredComponent(); + _triggerVolume.OnEntry += OnTriggerVolumeEntry; + _triggerVolume.OnExit += OnTriggerVolumeExit; + } + + public void OnDestroy() + { + if (_triggerVolume == null) return; + _triggerVolume.OnEntry -= OnTriggerVolumeEntry; + _triggerVolume.OnExit -= OnTriggerVolumeExit; + } + + public void SetPinned(bool pin) => _pin = pin; + + public void SetTarget(External.Modules.VolumesModule.NotificationVolumeInfo.NotificationTarget target) => SetTarget(EnumUtils.Parse(target.ToString(), NotificationTarget.All)); + + public void SetTarget(NotificationTarget target) => _target = target; + + public void SetEntryNotification(string displayMessage, float duration = 5) + { + _entryNotification = new NotificationData(_target, TranslationHandler.GetTranslation(displayMessage, TranslationHandler.TextType.UI), duration); + } + + public void SetExitNotification(string displayMessage, float duration = 5) + { + _exitNotification = new NotificationData(_target, TranslationHandler.GetTranslation(displayMessage, TranslationHandler.TextType.UI), duration); + } + + public void OnTriggerVolumeEntry(GameObject hitObj) + { + if (_target == NotificationTarget.All) + { + if (hitObj.CompareTag("PlayerDetector") || hitObj.CompareTag("ShipDetector")) + { + PostEntryNotification(); + } + } + else if (_target == NotificationTarget.Player) + { + if (hitObj.CompareTag("PlayerDetector")) + { + PostEntryNotification(); + } + } + else if (_target == NotificationTarget.Ship) + { + if (hitObj.CompareTag("ShipDetector")) + { + PostEntryNotification(); + } + } + } + + public void OnTriggerVolumeExit(GameObject hitObj) + { + if (_target == NotificationTarget.All) + { + if (hitObj.CompareTag("PlayerDetector") || hitObj.CompareTag("ShipDetector")) + { + PostExitNotification(); + } + } + else if (_target == NotificationTarget.Player) + { + if (hitObj.CompareTag("PlayerDetector")) + { + PostExitNotification(); + } + } + else if (_target == NotificationTarget.Ship) + { + if (hitObj.CompareTag("ShipDetector")) + { + PostExitNotification(); + } + } + } + + public void PostEntryNotification() + { + if (_entryNotification == null) return; + NotificationManager.SharedInstance.PostNotification(_entryNotification, _pin); + } + + public void PostExitNotification() + { + if (_exitNotification == null) return; + NotificationManager.SharedInstance.PostNotification(_exitNotification, _pin); + } + + public void UnpinEntryNotification() + { + if (_entryNotification == null) return; + if (NotificationManager.SharedInstance.IsPinnedNotification(_entryNotification)) + { + NotificationManager.SharedInstance.UnpinNotification(_entryNotification); + } + } + + public void UnpinExitNotification() + { + if (_exitNotification == null) return; + if (NotificationManager.SharedInstance.IsPinnedNotification(_exitNotification)) + { + NotificationManager.SharedInstance.UnpinNotification(_exitNotification); + } + } + } +} diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index 4a353011..4ea8a886 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -169,6 +169,16 @@ namespace NewHorizons.External.Configs /// public WaterModule Water; + /// + /// Add various volumes on this body + /// + public VolumesModule Volumes; + + /// + /// Extra data that may be used by extension mods + /// + public object extras; + public PlanetConfig() { // Always have to have a base module @@ -312,6 +322,20 @@ namespace NewHorizons.External.Configs if (tornado.downwards) tornado.type = PropModule.TornadoInfo.TornadoType.Downwards; + if (Props?.audioVolumes != null) + { + if (Volumes == null) Volumes = new VolumesModule(); + if (Volumes.audioVolumes == null) Volumes.audioVolumes = new VolumesModule.AudioVolumeInfo[0]; + Volumes.audioVolumes = Volumes.audioVolumes.Concat(Props.audioVolumes).ToArray(); + } + + if (Props?.reveal != null) + { + if (Volumes == null) Volumes = new VolumesModule(); + if (Volumes.revealVolumes == null) Volumes.revealVolumes = new VolumesModule.RevealVolumeInfo[0]; + Volumes.revealVolumes = Volumes.revealVolumes.Concat(Props.reveal).ToArray(); + } + if (Base.sphereOfInfluence != 0f) Base.soiOverride = Base.sphereOfInfluence; // Moved a bunch of stuff off of shiplog module to star system module because it didnt exist when we made this diff --git a/NewHorizons/External/Configs/StarSystemConfig.cs b/NewHorizons/External/Configs/StarSystemConfig.cs index 2df0dc8c..ce311174 100644 --- a/NewHorizons/External/Configs/StarSystemConfig.cs +++ b/NewHorizons/External/Configs/StarSystemConfig.cs @@ -102,6 +102,11 @@ namespace NewHorizons.External.Configs /// public CuriosityColorInfo[] curiosities; + /// + /// Extra data that may be used by extension mods + /// + public object extras; + public class NomaiCoordinates { [MinLength(2)] diff --git a/NewHorizons/External/Modules/PropModule.cs b/NewHorizons/External/Modules/PropModule.cs index 6ed70204..932b1bc0 100644 --- a/NewHorizons/External/Modules/PropModule.cs +++ b/NewHorizons/External/Modules/PropModule.cs @@ -48,11 +48,6 @@ namespace NewHorizons.External.Modules /// public RaftInfo[] rafts; - /// - /// Add triggers that reveal parts of the ship log on this planet - /// - public RevealInfo[] reveal; - /// /// Scatter props around this planet's surface /// @@ -83,11 +78,6 @@ namespace NewHorizons.External.Modules /// public SingularityModule[] singularities; - /// - /// Add audio volumes to this planet - /// - public AudioVolumeInfo[] audioVolumes; - /// /// Add signalscope signals to this planet /// @@ -98,6 +88,10 @@ namespace NewHorizons.External.Modules /// public RemoteInfo[] remotes; + [Obsolete("reveal is deprecated. Use Volumes->revealVolumes instead.")] public VolumesModule.RevealVolumeInfo[] reveal; + + [Obsolete("audioVolumes is deprecated. Use Volumes->audioVolumes instead.")] public VolumesModule.AudioVolumeInfo[] audioVolumes; + [JsonObject] public class ScatterInfo { @@ -433,55 +427,6 @@ namespace NewHorizons.External.Modules public string xmlFile; } - [JsonObject] - public class RevealInfo - { - [JsonConverter(typeof(StringEnumConverter))] - public enum RevealVolumeType - { - [EnumMember(Value = @"enter")] Enter = 0, - - [EnumMember(Value = @"observe")] Observe = 1, - - [EnumMember(Value = @"snapshot")] Snapshot = 2 - } - - /// - /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only) - /// - public float maxAngle = 180f; // Observe Only - - /// - /// The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only) - /// - public float maxDistance = -1f; // Snapshot & Observe Only - - /// - /// The position to place this volume at - /// - public MVector3 position; - - /// - /// The radius of this reveal volume - /// - public float radius = 1f; - - /// - /// What needs to be done to the volume to unlock the facts - /// - [DefaultValue("enter")] public RevealVolumeType revealOn = RevealVolumeType.Enter; - - /// - /// A list of facts to reveal - /// - public string[] reveals; - - /// - /// An achievement to unlock. Optional. - /// - public string achievementID; - } - [JsonObject] public class EntryLocationInfo { @@ -818,30 +763,6 @@ namespace NewHorizons.External.Modules [DefaultValue(1f)] public float probability = 1f; } - [JsonObject] - public class AudioVolumeInfo - { - /// - /// The location of this audio volume. Optional (will default to 0,0,0). - /// - public MVector3 position; - - /// - /// The radius of this audio volume - /// - public float radius; - - /// - /// The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. - /// - public string audio; - - /// - /// The audio track of this audio volume - /// - [DefaultValue("environment")] public AudioMixerTrackName track = AudioMixerTrackName.Environment; - } - [JsonObject] public class RemoteInfo { @@ -1002,25 +923,4 @@ namespace NewHorizons.External.Modules } } } - - [JsonConverter(typeof(StringEnumConverter))] - public enum AudioMixerTrackName - { - [EnumMember(Value = @"undefined")] Undefined = 0, - [EnumMember(Value = @"menu")] Menu = 1, - [EnumMember(Value = @"music")] Music = 2, - [EnumMember(Value = @"environment")] Environment = 4, - [EnumMember(Value = @"environmentUnfiltered")] Environment_Unfiltered = 5, - [EnumMember(Value = @"endTimesSfx")] EndTimes_SFX = 8, - [EnumMember(Value = @"signal")] Signal = 16, - [EnumMember(Value = @"death")] Death = 32, - [EnumMember(Value = @"player")] Player = 64, - [EnumMember(Value = @"playerExternal")] Player_External = 65, - [EnumMember(Value = @"ship")] Ship = 128, - [EnumMember(Value = @"map")] Map = 256, - [EnumMember(Value = @"endTimesMusic")] EndTimes_Music = 512, - [EnumMember(Value = @"muffleWhileRafting")] MuffleWhileRafting = 1024, - [EnumMember(Value = @"muffleIndoors")] MuffleIndoors = 2048, - [EnumMember(Value = @"slideReelMusic")] SlideReelMusic = 4096, - } } \ No newline at end of file diff --git a/NewHorizons/External/Modules/VolumesModule.cs b/NewHorizons/External/Modules/VolumesModule.cs new file mode 100644 index 00000000..43059fbf --- /dev/null +++ b/NewHorizons/External/Modules/VolumesModule.cs @@ -0,0 +1,243 @@ +using NewHorizons.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules +{ + [JsonObject] + public class VolumesModule + { + /// + /// Add audio volumes to this planet + /// + public AudioVolumeInfo[] audioVolumes; + + /// + /// Add hazard volumes to this planet + /// + public HazardVolumeInfo[] hazardVolumes; + + /// + /// Add notification volumes to this planet + /// + public NotificationVolumeInfo[] notificationVolumes; + + /// + /// Add triggers that reveal parts of the ship log on this planet + /// + public RevealVolumeInfo[] revealVolumes; + + [JsonObject] + public class RevealVolumeInfo + { + [JsonConverter(typeof(StringEnumConverter))] + public enum RevealVolumeType + { + [EnumMember(Value = @"enter")] Enter = 0, + + [EnumMember(Value = @"observe")] Observe = 1, + + [EnumMember(Value = @"snapshot")] Snapshot = 2 + } + + /// + /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only) + /// + public float maxAngle = 180f; // Observe Only + + /// + /// The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only) + /// + public float maxDistance = -1f; // Snapshot & Observe Only + + /// + /// The position to place this volume at + /// + public MVector3 position; + + /// + /// The radius of this reveal volume + /// + public float radius = 1f; + + /// + /// What needs to be done to the volume to unlock the facts + /// + [DefaultValue("enter")] public RevealVolumeType revealOn = RevealVolumeType.Enter; + + /// + /// A list of facts to reveal + /// + public string[] reveals; + + /// + /// An achievement to unlock. Optional. + /// + public string achievementID; + } + + [JsonObject] + public class AudioVolumeInfo + { + /// + /// The location of this audio volume. Optional (will default to 0,0,0). + /// + public MVector3 position; + + /// + /// The radius of this audio volume + /// + public float radius; + + /// + /// The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string audio; + + /// + /// The audio track of this audio volume + /// + [DefaultValue("environment")] public AudioMixerTrackName track = AudioMixerTrackName.Environment; + + /// + /// Whether to loop this audio while in this audio volume or just play it once + /// + [DefaultValue(true)] public bool loop = true; + } + + [JsonObject] + public class NotificationVolumeInfo + { + /// + /// What the notification will show for. + /// + [DefaultValue("all")] public NotificationTarget target = NotificationTarget.All; + + /// + /// The location of this notification volume. Optional (will default to 0,0,0). + /// + public MVector3 position; + + /// + /// The radius of this notification volume. + /// + public float radius; + + /// + /// The notification that will play when you enter this volume. + /// + public NotificationInfo entryNotification; + + /// + /// The notification that will play when you exit this volume. + /// + public NotificationInfo exitNotification; + + + [JsonObject] + public class NotificationInfo + { + /// + /// The message that will be displayed. + /// + public string displayMessage; + + /// + /// The duration this notification will be displayed. + /// + [DefaultValue(5f)] public float duration = 5f; + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum NotificationTarget + { + [EnumMember(Value = @"all")] All = 0, + [EnumMember(Value = @"ship")] Ship = 1, + [EnumMember(Value = @"player")] Player = 2, + } + } + + [JsonObject] + public class HazardVolumeInfo + { + /// + /// The location of this hazard volume. Optional (will default to 0,0,0). + /// + public MVector3 position; + + /// + /// The radius of this hazard volume. + /// + public float radius; + + /// + /// The type of hazard for this volume. + /// + [DefaultValue("general")] public HazardType type = HazardType.GENERAL; + + /// + /// The amount of damage you will take per second while inside this volume. + /// + [DefaultValue(10f)] public float damagePerSecond = 10f; + + /// + /// The type of damage you will take when you first touch this volume. + /// + [DefaultValue("impact")] public InstantDamageType firstContactDamageType = InstantDamageType.Impact; + + /// + /// The amount of damage you will take when you first touch this volume. + /// + public float firstContactDamage; + + [JsonConverter(typeof(StringEnumConverter))] + public enum HazardType + { + [EnumMember(Value = @"none")] NONE = 0, + [EnumMember(Value = @"general")] GENERAL = 1, + [EnumMember(Value = @"darkMatter")] DARKMATTER = 2, + [EnumMember(Value = @"heat")] HEAT = 4, + [EnumMember(Value = @"fire")] FIRE = 8, + [EnumMember(Value = @"sandfall")] SANDFALL = 16, + [EnumMember(Value = @"electricity")] ELECTRICITY = 32, + [EnumMember(Value = @"rapids")] RAPIDS = 64 + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum InstantDamageType + { + [EnumMember(Value = @"impact")] Impact, + [EnumMember(Value = @"puncture")] Puncture, + [EnumMember(Value = @"electrical")] Electrical + } + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum AudioMixerTrackName + { + [EnumMember(Value = @"undefined")] Undefined = 0, + [EnumMember(Value = @"menu")] Menu = 1, + [EnumMember(Value = @"music")] Music = 2, + [EnumMember(Value = @"environment")] Environment = 4, + [EnumMember(Value = @"environmentUnfiltered")] Environment_Unfiltered = 5, + [EnumMember(Value = @"endTimesSfx")] EndTimes_SFX = 8, + [EnumMember(Value = @"signal")] Signal = 16, + [EnumMember(Value = @"death")] Death = 32, + [EnumMember(Value = @"player")] Player = 64, + [EnumMember(Value = @"playerExternal")] Player_External = 65, + [EnumMember(Value = @"ship")] Ship = 128, + [EnumMember(Value = @"map")] Map = 256, + [EnumMember(Value = @"endTimesMusic")] EndTimes_Music = 512, + [EnumMember(Value = @"muffleWhileRafting")] MuffleWhileRafting = 1024, + [EnumMember(Value = @"muffleIndoors")] MuffleIndoors = 2048, + [EnumMember(Value = @"slideReelMusic")] SlideReelMusic = 4096, + } +} diff --git a/NewHorizons/External/NewHorizonsData.cs b/NewHorizons/External/NewHorizonsData.cs index a6273dd7..f90cb479 100644 --- a/NewHorizons/External/NewHorizonsData.cs +++ b/NewHorizons/External/NewHorizonsData.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NewHorizons.Utility; @@ -11,9 +11,12 @@ namespace NewHorizons.External private static string _activeProfileName; private static readonly string FileName = "save.json"; + // This is its own method so it can be patched by NH-QSB compat + public static string GetProfileName() => StandaloneProfileManager.SharedInstance?.currentProfile?.profileName; + public static void Load() { - _activeProfileName = StandaloneProfileManager.SharedInstance?.currentProfile?.profileName; + _activeProfileName = GetProfileName(); if (_activeProfileName == null) { Logger.LogError("Couldn't find active profile, are you on Gamepass?"); diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 73df7daa..67700c36 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -3,6 +3,7 @@ using NewHorizons.Builder.Body; using NewHorizons.Builder.General; using NewHorizons.Builder.Orbital; using NewHorizons.Builder.Props; +using NewHorizons.Builder.Volumes; using NewHorizons.Components; using NewHorizons.Components.Orbital; using NewHorizons.OtherMods.OWRichPresence; @@ -248,6 +249,16 @@ namespace NewHorizons.Handlers } } } + + try + { + Main.Instance.OnPlanetLoaded?.Invoke(body.Config.name); + } + catch (Exception e) + { + Logger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}"); + } + return true; } @@ -598,6 +609,11 @@ namespace NewHorizons.Handlers PropBuildManager.Make(go, sector, rb, body.Config, body.Mod); } + if (body.Config.Volumes != null) + { + VolumesBuildManager.Make(go, sector, rb, body.Config, body.Mod); + } + if (body.Config.Funnel != null) { FunnelBuilder.Make(go, go.GetComponentInChildren(), rb, body.Config.Funnel); diff --git a/NewHorizons/INewHorizons.cs b/NewHorizons/INewHorizons.cs index 7c647003..8f132ee7 100644 --- a/NewHorizons/INewHorizons.cs +++ b/NewHorizons/INewHorizons.cs @@ -43,6 +43,22 @@ namespace NewHorizons /// UnityEvent GetStarSystemLoadedEvent(); + /// + /// An event invoked when NH has finished a planet for a star system. + /// Gives the name of the planet that was just loaded. + /// + UnityEvent GetBodyLoadedEvent(); + + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); + + /// + /// Uses JSONPath to query a system + /// + object QuerySystem(Type outType, string path); + /// /// Allows you to overwrite the default system. This is where the player is respawned after dying. /// diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 9015bd32..be01cd1a 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -69,6 +69,7 @@ namespace NewHorizons public class StarSystemEvent : UnityEvent { } public StarSystemEvent OnChangeStarSystem; public StarSystemEvent OnStarSystemLoaded; + public StarSystemEvent OnPlanetLoaded; // For warping to the eye system private GameObject _ship; @@ -127,7 +128,7 @@ namespace NewHorizons BodyDict["SolarSystem"] = new List(); BodyDict["EyeOfTheUniverse"] = new List(); // Keep this empty tho fr - SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), Instance) + SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), "", Instance) { Config = { @@ -143,7 +144,7 @@ namespace NewHorizons } } }; - SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), Instance) + SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), "", Instance) { Config = { @@ -171,6 +172,7 @@ namespace NewHorizons OnChangeStarSystem = new StarSystemEvent(); OnStarSystemLoaded = new StarSystemEvent(); + OnPlanetLoaded = new StarSystemEvent(); SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; @@ -517,7 +519,7 @@ namespace NewHorizons } else { - SystemDict[name] = new NewHorizonsSystem(name, starSystemConfig, mod); + SystemDict[name] = new NewHorizonsSystem(name, starSystemConfig, relativePath, mod); } } } @@ -618,7 +620,7 @@ namespace NewHorizons starSystemConfig.Migrate(); starSystemConfig.FixCoordinates(); - var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, mod); + var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, $"", mod); SystemDict.Add(config.starSystem, system); @@ -649,6 +651,13 @@ namespace NewHorizons #region Change star system public void ChangeCurrentStarSystem(string newStarSystem, bool warp = false, bool vessel = false) { + // If we're just on the title screen set the system for later + if (LoadManager.GetCurrentScene() == OWScene.TitleScreen) + { + _currentStarSystem = newStarSystem; + return; + } + if (IsChangingStarSystem) return; IsWarpingFromShip = warp; @@ -660,9 +669,6 @@ namespace NewHorizons IsChangingStarSystem = true; WearingSuit = PlayerState.IsWearingSuit(); - // We kill them so they don't move as much - Locator.GetDeathManager().KillPlayer(DeathType.Meditation); - OWScene sceneToLoad; if (newStarSystem == "EyeOfTheUniverse") @@ -680,12 +686,15 @@ namespace NewHorizons _currentStarSystem = newStarSystem; + // Freeze player inputs + OWInput.ChangeInputMode(InputMode.None); + LoadManager.LoadSceneAsync(sceneToLoad, !vessel, LoadManager.FadeType.ToBlack, 0.1f, true); } void OnDeath(DeathType _) { - // We reset the solar system on death (unless we just killed the player) + // We reset the solar system on death if (!IsChangingStarSystem) { // If the override is a valid system then we go there diff --git a/NewHorizons/NewHorizonsApi.cs b/NewHorizons/NewHorizonsApi.cs index 0ddb9f14..4924834f 100644 --- a/NewHorizons/NewHorizonsApi.cs +++ b/NewHorizons/NewHorizonsApi.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using UnityEngine; using UnityEngine.Events; using Logger = NewHorizons.Utility.Logger; namespace NewHorizons { + public class NewHorizonsApi : INewHorizons { [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] @@ -64,20 +67,10 @@ namespace NewHorizons return Main.BodyDict.Values.SelectMany(x => x)?.ToList()?.FirstOrDefault(x => x.Config.name == name)?.Object; } - public string GetCurrentStarSystem() - { - return Main.Instance.CurrentStarSystem; - } - - public UnityEvent GetChangeStarSystemEvent() - { - return Main.Instance.OnChangeStarSystem; - } - - public UnityEvent GetStarSystemLoadedEvent() - { - return Main.Instance.OnStarSystemLoaded; - } + public string GetCurrentStarSystem() => Main.Instance.CurrentStarSystem; + public UnityEvent GetChangeStarSystemEvent() => Main.Instance.OnChangeStarSystem; + public UnityEvent GetStarSystemLoadedEvent() => Main.Instance.OnStarSystemLoaded; + public UnityEvent GetBodyLoadedEvent() => Main.Instance.OnPlanetLoaded; public bool SetDefaultSystem(string name) { @@ -108,6 +101,42 @@ namespace NewHorizons } } + private static object QueryJson(Type outType, string filePath, string jsonPath) + { + if (filePath == "") return null; + try + { + var jsonText = File.ReadAllText(filePath); + var jsonData = JObject.Parse(jsonText); + return jsonData.SelectToken(jsonPath)?.ToObject(outType); + } + catch (FileNotFoundException) + { + return null; + } + catch (JsonException e) + { + Logger.LogError(e.ToString()); + return null; + } + } + + public object QueryBody(Type outType, string bodyName, string jsonPath) + { + var planet = Main.BodyDict[Main.Instance.CurrentStarSystem].Find((b) => b.Config.name == bodyName); + return planet == null + ? null + : QueryJson(outType, planet.Mod.ModHelper.Manifest.ModFolderPath + planet.RelativePath, jsonPath); + } + + public object QuerySystem(Type outType, string jsonPath) + { + var system = Main.SystemDict[Main.Instance.CurrentStarSystem]; + return system == null + ? null + : QueryJson(outType, system.Mod.ModHelper.Manifest.ModFolderPath + system.RelativePath, jsonPath); + } + public GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal) { diff --git a/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs b/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs index a1e4da27..7b94ca6c 100644 --- a/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs +++ b/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs @@ -47,7 +47,7 @@ namespace NewHorizons.OtherMods.OWRichPresence var localizedName = TranslationHandler.GetTranslation(name, TranslationHandler.TextType.UI); var message = TranslationHandler.GetTranslation("RICH_PRESENCE_EXPLORING", TranslationHandler.TextType.UI).Replace("{0}", localizedName); - API.CreateTrigger(go, sector, message, name.Replace(" ", "").Replace("'", "").ToLowerInvariant()); + API.CreateTrigger(go, sector, message, name.Replace(" ", "").Replace("'", "").Replace("-", "").ToLowerInvariant()); } public static void OnStarSystemLoaded(string name) diff --git a/NewHorizons/Patches/ShapePatches.cs b/NewHorizons/Patches/ShapePatches.cs new file mode 100644 index 00000000..cf4d19d0 --- /dev/null +++ b/NewHorizons/Patches/ShapePatches.cs @@ -0,0 +1,29 @@ +using HarmonyLib; +using System.Collections.Generic; + +namespace NewHorizons.Patches +{ + [HarmonyPatch] + public class ShapePatches + { + [HarmonyPrefix] + [HarmonyPatch(typeof(ShapeManager), nameof(ShapeManager.Initialize))] + public static bool ShapeManager_Initialize() + { + ShapeManager._exists = true; + + ShapeManager._detectors = new ShapeManager.Layer(256); + for (int index = 0; index < 256; ++index) + ShapeManager._detectors[index].contacts = new List(64); + + ShapeManager._volumes = new ShapeManager.Layer[4]; + for (int index = 0; index < 4; ++index) + ShapeManager._volumes[index] = new ShapeManager.Layer(2048); + + ShapeManager._locked = false; + ShapeManager._frameFlag = false; + + return false; + } + } +} diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index bc88b9f7..85fabcd8 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -128,6 +128,17 @@ "description": "Add water to this planet", "$ref": "#/definitions/WaterModule" }, + "Volumes": { + "description": "Add various volumes on this body", + "$ref": "#/definitions/VolumesModule" + }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" @@ -925,13 +936,6 @@ "$ref": "#/definitions/RaftInfo" } }, - "reveal": { - "type": "array", - "description": "Add triggers that reveal parts of the ship log on this planet", - "items": { - "$ref": "#/definitions/RevealInfo" - } - }, "scatter": { "type": "array", "description": "Scatter props around this planet's surface", @@ -974,13 +978,6 @@ "$ref": "#/definitions/SingularityModule" } }, - "audioVolumes": { - "type": "array", - "description": "Add audio volumes to this planet", - "items": { - "$ref": "#/definitions/AudioVolumeInfo" - } - }, "signals": { "type": "array", "description": "Add signalscope signals to this planet", @@ -1337,61 +1334,6 @@ } } }, - "RevealInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "maxAngle": { - "type": "number", - "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only)", - "format": "float" - }, - "maxDistance": { - "type": "number", - "description": "The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only)", - "format": "float" - }, - "position": { - "description": "The position to place this volume at", - "$ref": "#/definitions/MVector3" - }, - "radius": { - "type": "number", - "description": "The radius of this reveal volume", - "format": "float" - }, - "revealOn": { - "description": "What needs to be done to the volume to unlock the facts", - "default": "enter", - "$ref": "#/definitions/RevealVolumeType" - }, - "reveals": { - "type": "array", - "description": "A list of facts to reveal", - "items": { - "type": "string" - } - }, - "achievementID": { - "type": "string", - "description": "An achievement to unlock. Optional." - } - } - }, - "RevealVolumeType": { - "type": "string", - "description": "", - "x-enumNames": [ - "Enter", - "Observe", - "Snapshot" - ], - "enum": [ - "enter", - "observe", - "snapshot" - ] - }, "ScatterInfo": { "type": "object", "additionalProperties": false, @@ -1801,70 +1743,6 @@ "whiteHole" ] }, - "AudioVolumeInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "position": { - "description": "The location of this audio volume. Optional (will default to 0,0,0).", - "$ref": "#/definitions/MVector3" - }, - "radius": { - "type": "number", - "description": "The radius of this audio volume", - "format": "float" - }, - "audio": { - "type": "string", - "description": "The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "track": { - "description": "The audio track of this audio volume", - "default": "environment", - "$ref": "#/definitions/AudioMixerTrackName" - } - } - }, - "AudioMixerTrackName": { - "type": "string", - "description": "", - "x-enumNames": [ - "Undefined", - "Menu", - "Music", - "Environment", - "Environment_Unfiltered", - "EndTimes_SFX", - "Signal", - "Death", - "Player", - "Player_External", - "Ship", - "Map", - "EndTimes_Music", - "MuffleWhileRafting", - "MuffleIndoors", - "SlideReelMusic" - ], - "enum": [ - "undefined", - "menu", - "music", - "environment", - "environmentUnfiltered", - "endTimesSfx", - "signal", - "death", - "player", - "playerExternal", - "ship", - "map", - "endTimesMusic", - "muffleWhileRafting", - "muffleIndoors", - "slideReelMusic" - ] - }, "SignalInfo": { "type": "object", "additionalProperties": false, @@ -2503,6 +2381,296 @@ "$ref": "#/definitions/MColor" } } + }, + "VolumesModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "audioVolumes": { + "type": "array", + "description": "Add audio volumes to this planet", + "items": { + "$ref": "#/definitions/AudioVolumeInfo" + } + }, + "hazardVolumes": { + "type": "array", + "description": "Add hazard volumes to this planet", + "items": { + "$ref": "#/definitions/HazardVolumeInfo" + } + }, + "notificationVolumes": { + "type": "array", + "description": "Add notification volumes to this planet", + "items": { + "$ref": "#/definitions/NotificationVolumeInfo" + } + }, + "revealVolumes": { + "type": "array", + "description": "Add triggers that reveal parts of the ship log on this planet", + "items": { + "$ref": "#/definitions/RevealVolumeInfo" + } + } + } + }, + "AudioVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this audio volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this audio volume", + "format": "float" + }, + "audio": { + "type": "string", + "description": "The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "track": { + "description": "The audio track of this audio volume", + "default": "environment", + "$ref": "#/definitions/AudioMixerTrackName" + }, + "loop": { + "type": "boolean", + "description": "Whether to loop this audio while in this audio volume or just play it once", + "default": true + } + } + }, + "AudioMixerTrackName": { + "type": "string", + "description": "", + "x-enumNames": [ + "Undefined", + "Menu", + "Music", + "Environment", + "Environment_Unfiltered", + "EndTimes_SFX", + "Signal", + "Death", + "Player", + "Player_External", + "Ship", + "Map", + "EndTimes_Music", + "MuffleWhileRafting", + "MuffleIndoors", + "SlideReelMusic" + ], + "enum": [ + "undefined", + "menu", + "music", + "environment", + "environmentUnfiltered", + "endTimesSfx", + "signal", + "death", + "player", + "playerExternal", + "ship", + "map", + "endTimesMusic", + "muffleWhileRafting", + "muffleIndoors", + "slideReelMusic" + ] + }, + "HazardVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this hazard volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this hazard volume.", + "format": "float" + }, + "type": { + "description": "The type of hazard for this volume.", + "default": "general", + "$ref": "#/definitions/HazardType" + }, + "damagePerSecond": { + "type": "number", + "description": "The amount of damage you will take per second while inside this volume.", + "format": "float", + "default": 10.0 + }, + "firstContactDamageType": { + "description": "The type of damage you will take when you first touch this volume.", + "default": "impact", + "$ref": "#/definitions/InstantDamageType" + }, + "firstContactDamage": { + "type": "number", + "description": "The amount of damage you will take when you first touch this volume.", + "format": "float" + } + } + }, + "HazardType": { + "type": "string", + "description": "", + "x-enumNames": [ + "NONE", + "GENERAL", + "DARKMATTER", + "HEAT", + "FIRE", + "SANDFALL", + "ELECTRICITY", + "RAPIDS" + ], + "enum": [ + "none", + "general", + "darkMatter", + "heat", + "fire", + "sandfall", + "electricity", + "rapids" + ] + }, + "InstantDamageType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Impact", + "Puncture", + "Electrical" + ], + "enum": [ + "impact", + "puncture", + "electrical" + ] + }, + "NotificationVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "target": { + "description": "What the notification will show for.", + "default": "all", + "$ref": "#/definitions/NotificationTarget" + }, + "position": { + "description": "The location of this notification volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this notification volume.", + "format": "float" + }, + "entryNotification": { + "description": "The notification that will play when you enter this volume.", + "$ref": "#/definitions/NotificationInfo" + }, + "exitNotification": { + "description": "The notification that will play when you exit this volume.", + "$ref": "#/definitions/NotificationInfo" + } + } + }, + "NotificationTarget": { + "type": "string", + "description": "", + "x-enumNames": [ + "All", + "Ship", + "Player" + ], + "enum": [ + "all", + "ship", + "player" + ] + }, + "NotificationInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "displayMessage": { + "type": "string", + "description": "The message that will be displayed." + }, + "duration": { + "type": "number", + "description": "The duration this notification will be displayed.", + "format": "float", + "default": 5.0 + } + } + }, + "RevealVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "maxAngle": { + "type": "number", + "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only)", + "format": "float" + }, + "maxDistance": { + "type": "number", + "description": "The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only)", + "format": "float" + }, + "position": { + "description": "The position to place this volume at", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this reveal volume", + "format": "float" + }, + "revealOn": { + "description": "What needs to be done to the volume to unlock the facts", + "default": "enter", + "$ref": "#/definitions/RevealVolumeType" + }, + "reveals": { + "type": "array", + "description": "A list of facts to reveal", + "items": { + "type": "string" + } + }, + "achievementID": { + "type": "string", + "description": "An achievement to unlock. Optional." + } + } + }, + "RevealVolumeType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Enter", + "Observe", + "Snapshot" + ], + "enum": [ + "enter", + "observe", + "snapshot" + ] } }, "$docs": { diff --git a/NewHorizons/Schemas/star_system_schema.json b/NewHorizons/Schemas/star_system_schema.json index b1f5e2cc..032fb8f1 100644 --- a/NewHorizons/Schemas/star_system_schema.json +++ b/NewHorizons/Schemas/star_system_schema.json @@ -71,6 +71,13 @@ "$ref": "#/definitions/CuriosityColorInfo" } }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" diff --git a/NewHorizons/Utility/DebugMenu/DebugMenu.cs b/NewHorizons/Utility/DebugMenu/DebugMenu.cs index 0d25ebfe..be19c867 100644 --- a/NewHorizons/Utility/DebugMenu/DebugMenu.cs +++ b/NewHorizons/Utility/DebugMenu/DebugMenu.cs @@ -69,7 +69,13 @@ namespace NewHorizons.Utility.DebugMenu PauseMenuInitHook(); - Main.Instance.OnChangeStarSystem.AddListener((string s) => SaveLoadedConfigsForRecentSystem()); + Main.Instance.OnChangeStarSystem.AddListener((string s) => { + if (saveButtonUnlocked) + { + SaveLoadedConfigsForRecentSystem(); + saveButtonUnlocked = false; + } + }); } else { @@ -227,6 +233,24 @@ namespace NewHorizons.Utility.DebugMenu var json = loadedConfigFiles[filePath].ToSerializedJson(); + try + { + var path = loadedMod.ModHelper.Manifest.ModFolderPath + backupFolderName + relativePath; + Logger.LogVerbose($"Backing up... {relativePath} to {path}"); + var oldPath = loadedMod.ModHelper.Manifest.ModFolderPath + relativePath; + var directoryName = Path.GetDirectoryName(path); + Directory.CreateDirectory(directoryName); + + if (File.Exists(oldPath)) + File.WriteAllBytes(path, File.ReadAllBytes(oldPath)); + else + File.WriteAllText(path, json); + } + catch (Exception e) + { + Logger.LogError($"Failed to save backup file {backupFolderName}{relativePath}:\n{e}"); + } + try { Logger.Log($"Saving... {relativePath} to {filePath}"); @@ -240,19 +264,6 @@ namespace NewHorizons.Utility.DebugMenu { Logger.LogError($"Failed to save file {relativePath}:\n{e}"); } - - try - { - var path = Main.Instance.ModHelper.Manifest.ModFolderPath + backupFolderName + relativePath; - var directoryName = Path.GetDirectoryName(path); - Directory.CreateDirectory(directoryName); - - File.WriteAllText(path, json); - } - catch (Exception e) - { - Logger.LogError($"Failed to save backup file {backupFolderName}{relativePath}:\n{e}"); - } } } diff --git a/NewHorizons/Utility/NewHorizonsSystem.cs b/NewHorizons/Utility/NewHorizonsSystem.cs index ae6bafa3..be9a539f 100644 --- a/NewHorizons/Utility/NewHorizonsSystem.cs +++ b/NewHorizons/Utility/NewHorizonsSystem.cs @@ -11,15 +11,17 @@ namespace NewHorizons.Utility public class NewHorizonsSystem { public string UniqueID; + public string RelativePath; public SpawnModule Spawn = null; public SpawnPoint SpawnPoint = null; public StarSystemConfig Config; public IModBehaviour Mod; - public NewHorizonsSystem(string uniqueID, StarSystemConfig config, IModBehaviour mod) + public NewHorizonsSystem(string uniqueID, StarSystemConfig config, string relativePath, IModBehaviour mod) { UniqueID = uniqueID; Config = config; + RelativePath = relativePath; Mod = mod; } } diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 38fd21ab..5696ae38 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,7 +4,7 @@ "author": "xen, Bwc9876, clay, MegaPiggy, John, Hawkbar, Trifid, Book", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.5.0", + "version": "1.5.1", "owmlVersion": "2.6.0", "dependencies": [ "JohnCorby.VanillaFix" ], "conflicts": [ "Raicuparta.QuantumSpaceBuddies", "PacificEngine.OW_Randomizer" ], diff --git a/SchemaExporter/SchemaExporter.cs b/SchemaExporter/SchemaExporter.cs index f0c802f5..c2e94e25 100644 --- a/SchemaExporter/SchemaExporter.cs +++ b/SchemaExporter/SchemaExporter.cs @@ -78,16 +78,29 @@ public static class SchemaExporter {"description", _description} }); - if (_title == "Celestial Body Schema") + switch (_title) { - schema.Definitions["OrbitModule"].Properties["semiMajorAxis"].Default = 5000f; + case "Celestial Body Schema": + schema.Definitions["OrbitModule"].Properties["semiMajorAxis"].Default = 5000f; + break; + case "Star System Schema": + schema.Definitions["NomaiCoordinates"].Properties["x"].UniqueItems = true; + schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; + schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; + break; } - if (_title == "Star System Schema") + if (_title is "Star System Schema" or "Celestial Body Schema") { - schema.Definitions["NomaiCoordinates"].Properties["x"].UniqueItems = true; - schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; - schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; + schema.Properties["extras"] = new JsonSchemaProperty { + Type = JsonObjectType.Object, + Description = "Extra data that may be used by extension mods", + AllowAdditionalProperties = true, + AdditionalPropertiesSchema = new JsonSchema + { + Type = JsonObjectType.Object + } + }; } return schema; diff --git a/docs/content/pages/tutorials/api.md b/docs/content/pages/tutorials/api.md index 8689943c..5c7a8996 100644 --- a/docs/content/pages/tutorials/api.md +++ b/docs/content/pages/tutorials/api.md @@ -9,21 +9,99 @@ First create the following interface in your mod: ```cs public interface INewHorizons -{ - void LoadConfigs(IModBehaviour mod); + { + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config); - GameObject GetPlanet(string name); - - string GetCurrentStarSystem(); + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config, IModBehaviour mod); - UnityEvent GetChangeStarSystemEvent(); + /// + /// Will load all configs in the regular folders (planets, systems, translations, etc) for this mod. + /// The NH addon config template is just a single call to this API method. + /// + void LoadConfigs(IModBehaviour mod); - UnityEvent GetStarSystemLoadedEvent(); - - GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); + /// + /// Retrieve the root GameObject of a custom planet made by creating configs. + /// Will only work if the planet has been created (see GetStarSystemLoadedEvent) + /// + GameObject GetPlanet(string name); - string[] GetInstalledAddons(); -} + /// + /// The name of the current star system loaded. + /// + string GetCurrentStarSystem(); + + /// + /// An event invoked when the player begins loading the new star system, before the scene starts to load. + /// Gives the name of the star system being switched to. + /// + UnityEvent GetChangeStarSystemEvent(); + + /// + /// An event invoked when NH has finished generating all planets for a new star system. + /// Gives the name of the star system that was just loaded. + /// + UnityEvent GetStarSystemLoadedEvent(); + + /// + /// An event invoked when NH has finished a planet for a star system. + /// Gives the name of the planet that was just loaded. + /// + UnityEvent GetBodyLoadedEvent(); + + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); + + /// + /// Uses JSONPath to query a system + /// + object QuerySystem(Type outType, string path); + + /// + /// Allows you to overwrite the default system. This is where the player is respawned after dying. + /// + bool SetDefaultSystem(string name); + + /// + /// Allows you to instantly begin a warp to a new star system. + /// Will return false if that system does not exist (cannot be warped to). + /// + bool ChangeCurrentStarSystem(string name); + + /// + /// Returns the uniqueIDs of each installed NH addon. + /// + string[] GetInstalledAddons(); + + /// + /// Allows you to spawn a copy of a prop by specifying its path. + /// This is the same as using Props->details in a config, but also returns the spawned gameObject to you. + /// + GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, + float scale, bool alignWithNormal); + + /// + /// Allows you to spawn an AudioSignal on a planet. + /// This is the same as using Props->signals in a config, but also returns the spawned AudioSignal to you. + /// This method will not set its position. You will have to do that with the returned object. + /// + AudioSignal SpawnSignal(IModBehaviour mod, GameObject root, string audio, string name, string frequency, + float sourceRadius = 1f, float detectionRadius = 20f, float identificationRadius = 10f, bool insideCloak = false, + bool onlyAudibleToScope = true, string reveals = ""); + + /// + /// Allows you to spawn character dialogue on a planet. Also returns the RemoteDialogueTrigger if remoteTriggerRadius is specified. + /// This is the same as using Props->dialogue in a config, but also returns the spawned game objects to you. + /// This method will not set the position of the dialogue or remote trigger. You will have to do that with the returned objects. + /// + (CharacterDialogueTree, RemoteDialogueTrigger) SpawnDialogue(IModBehaviour mod, GameObject root, string xmlFile, float radius = 1f, + float range = 1f, string blockAfterPersistentCondition = null, float lookAtRadius = 1f, string pathToAnimController = null, + float remoteTriggerRadius = 0f); + } ``` In your main `ModBehaviour` class you can get the NewHorizons API like so: @@ -33,7 +111,7 @@ public class MyMod : ModBehaviour { void Start() { - INewHorizons NewHorizonsAPI = ModHelper.Interaction.GetModApi("xen.NewHorizons"); + INewHorizons NewHorizonsAPI = ModHelper.Interaction.TryGetModApi("xen.NewHorizons"); } } ``` diff --git a/docs/content/pages/tutorials/extending.md b/docs/content/pages/tutorials/extending.md new file mode 100644 index 00000000..0e9b30bc --- /dev/null +++ b/docs/content/pages/tutorials/extending.md @@ -0,0 +1,74 @@ +--- +Title: Extending Configs +Description: A guide on extending config files with the New Horizons API +Sort_Priority: 5 +--- + +# Extending Configs + +This guide will explain how to use the API to add new features to New Horizons. + +## How Extending Works + +Addon developers will add a key to the `extras` object in the root of the config + +```json +{ + "name": "Wetrock", + "extras": { + "myCoolExtensionData": { + "myCoolExtensionProperty": 2 + } + } +} +``` + +Your mod will then use the API's `QueryBody` method to obtain the `myCoolExtensionData` object. + +**It's up to the addon dev to list your mod as a dependency!** + +## Extending Planets + +You can extend all planets by hooking into the `OnBodyLoaded` event of the API: + +```csharp +var api = ModHelper.Interactions.TryGetModApi("xen.NewHorizons"); +api.GetBodyLoadedEvent().AddListener((name) => { + ModHelper.Console.WriteLine($"Body: {name} Loaded!"); +}); +``` + +In order to get your extra module, first define the module as a class: + +```csharp +public class MyCoolExtensionData { + int myCoolExtensionProperty; +} +``` + +Then, use the `QueryBody` method: + +```csharp +var api = ModHelper.Interactions.TryGetModApi("xen.NewHorizons"); +api.GetBodyLoadedEvent().AddListener((name) => { + ModHelper.Console.WriteLine($"Body: {name} Loaded!"); + var potentialData = api.QueryBody(typeof(MyCoolExtensionData), "$.extras.myCoolExtensionData", name); + // Makes sure the module is valid and not null + if (potentialData is MyCoolExtensionData data) { + ModHelper.Console.WriteLine($"myCoolExtensionProperty for {name} is {data.myCoolExtensionProperty}!"); + } +}); +``` + +## Extending Systems + +Extending systems is the exact same as extending planets, except you use the `QuerySystem` method instead. + +## Accessing Other Values + +You can also use the `QueryBody` method to get values of the config outside of your extension object + +```csharp +var primaryBody = api.QueryBody(typeof(string), "Wetrock", "$.Orbit.primaryBody"); + ModHelper.Console.WriteLine($"Primary of {bodyName} is {primaryBody ?? "NULL"}!"); +```