From 01866cc6becabd39131d6ff3f3499a749f537f22 Mon Sep 17 00:00:00 2001 From: xen-42 Date: Sat, 28 Jun 2025 20:31:00 -0400 Subject: [PATCH 01/18] Update README.md From 3fbe75239541ce23e1c31a6a22bdd2c9a27b166b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter-Mikha=C3=ABl=20Richard?= Date: Mon, 30 Jun 2025 18:47:18 +0200 Subject: [PATCH 02/18] Added security to ScatterBuilder.cs Prevent prop positioning loop going infinite when there are too much constraint on height --- NewHorizons/Builder/Props/ScatterBuilder.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index 862f0629..9f08d403 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -80,6 +80,15 @@ namespace NewHorizons.Builder.Props }; var scatterPrefab = DetailBuilder.Make(go, sector, mod, prefab, detailInfo); + bool reasonableHeightConstraints = true; + if (!propInfo.preventOverlap && (heightMapTexture != null) && (propInfo.minHeight != null || propInfo.maxHeight != null)) // If caution is relevant + { + var maxHeight = (propInfo.minHeight != null ? Math.Min(propInfo.maxHeight, heightMap.maxHeight) : heightMap.maxHeight); + var minHeight = (propInfo.minHeight != null ? Math.Max(propInfo.minHeight, heightMap.minHeight) : heightMap.minHeight); + if ((maxHeight - minHeight) / (heightMap.maxHeight - heightMap.minHeight) < 0.001) // If height roll has less than 0.1% chance of being valid + reasonableHeightConstraints = false; // Ignore propInfo.min/maxHeight to prevent infinite rerolls + // That way, even if often not valid, it still won't loop much more than propInfo.count * 1000 per prop + } for (int i = 0; i < propInfo.count; i++) { Vector3 point; @@ -113,7 +122,7 @@ namespace NewHorizons.Builder.Props float relativeHeight = heightMapTexture.GetPixel((int)sampleX, (int)sampleY).r; height = (relativeHeight * (heightMap.maxHeight - heightMap.minHeight) + heightMap.minHeight); - if ((propInfo.minHeight != null && height < propInfo.minHeight) || (propInfo.maxHeight != null && height > propInfo.maxHeight)) + if (reasonableHeightConstraints && ((propInfo.minHeight != null && height < propInfo.minHeight) || (propInfo.maxHeight != null && height > propInfo.maxHeight))) { // Try this point again i--; From b84d94a404b8dd046731e7f1e49c125929cd4e75 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Tue, 1 Jul 2025 12:28:12 -0500 Subject: [PATCH 03/18] Condition trigger volume --- .../Volumes/ConditionTriggerVolumeBuilder.cs | 25 +++++++ .../Builder/Volumes/VolumesBuildManager.cs | 7 ++ .../Volumes/ConditionTriggerVolume.cs | 66 +++++++++++++++++++ .../VolumeInfos/ConditionTriggerVolumeInfo.cs | 39 +++++++++++ .../External/Modules/Volumes/VolumesModule.cs | 5 ++ 5 files changed, 142 insertions(+) create mode 100644 NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs create mode 100644 NewHorizons/Components/Volumes/ConditionTriggerVolume.cs create mode 100644 NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs diff --git a/NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs new file mode 100644 index 00000000..2f9f270a --- /dev/null +++ b/NewHorizons/Builder/Volumes/ConditionTriggerVolumeBuilder.cs @@ -0,0 +1,25 @@ +using NewHorizons.Components.Volumes; +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using UnityEngine; + +namespace NewHorizons.Builder.Volumes +{ + internal static class ConditionTriggerVolumeBuilder + { + public static ConditionTriggerVolume Make(GameObject planetGO, Sector sector, ConditionTriggerVolumeInfo info) + { + var volume = VolumeBuilder.Make(planetGO, sector, info); + + volume.Condition = info.condition; + volume.Persistent = info.persistent; + volume.Reversible = info.reversible; + volume.Player = info.player; + volume.Probe = info.probe; + volume.Ship = info.ship; + + volume.gameObject.SetActive(true); + + return volume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs index 161f1d6a..58ff8fdb 100644 --- a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -35,6 +35,13 @@ namespace NewHorizons.Builder.Volumes AudioVolumeBuilder.Make(go, sector, audioVolume, mod); } } + if (config.Volumes.conditionTriggerVolumes != null) + { + foreach (var conditionTriggerVolume in config.Volumes.conditionTriggerVolumes) + { + ConditionTriggerVolumeBuilder.Make(go, sector, conditionTriggerVolume); + } + } if (config.Volumes.dayNightAudioVolumes != null) { foreach (var dayNightAudioVolume in config.Volumes.dayNightAudioVolumes) diff --git a/NewHorizons/Components/Volumes/ConditionTriggerVolume.cs b/NewHorizons/Components/Volumes/ConditionTriggerVolume.cs new file mode 100644 index 00000000..047f8fb2 --- /dev/null +++ b/NewHorizons/Components/Volumes/ConditionTriggerVolume.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + public class ConditionTriggerVolume : BaseVolume + { + public string Condition { get; set; } + public bool Persistent { get; set; } + public bool Reversible { get; set; } + public bool Player { get; set; } = true; + public bool Probe { get; set; } + public bool Ship { get; set; } + + public override void OnTriggerVolumeEntry(GameObject hitObj) + { + if (TestHitObject(hitObj)) + { + if (Persistent) + { + PlayerData.SetPersistentCondition(Condition, true); + } + else + { + DialogueConditionManager.SharedInstance.SetConditionState(Condition, true); + } + } + } + + public override void OnTriggerVolumeExit(GameObject hitObj) + { + if (Reversible && TestHitObject(hitObj)) + { + if (Persistent) + { + PlayerData.SetPersistentCondition(Condition, false); + } + else + { + DialogueConditionManager.SharedInstance.SetConditionState(Condition, false); + } + } + } + + bool TestHitObject(GameObject hitObj) + { + if (Player && hitObj.CompareTag("PlayerDetector")) + { + return true; + } + if (Probe && hitObj.CompareTag("ProbeDetector")) + { + return true; + } + if (Ship && hitObj.CompareTag("ShipDetector")) + { + return true; + } + return false; + } + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs new file mode 100644 index 00000000..c364d151 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/ConditionTriggerVolumeInfo.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using System.ComponentModel; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class ConditionTriggerVolumeInfo : VolumeInfo + { + /// + /// The name of the dialogue condition or persistent condition to set when entering the volume. + /// + public string condition; + + /// + /// If true, the condition will persist across all future loops until unset. + /// + public bool persistent; + + /// + /// Whether to unset the condition when existing the volume. + /// + public bool reversible; + + /// + /// Whether to set the condition when the player enters this volume. Defaults to true. + /// + [DefaultValue(true)] public bool player = true; + + /// + /// Whether to set the condition when the scout probe enters this volume. + /// + public bool probe; + + /// + /// Whether to set the condition when the ship enters this volume. + /// + public bool ship; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumesModule.cs b/NewHorizons/External/Modules/Volumes/VolumesModule.cs index 34795d15..7f44b911 100644 --- a/NewHorizons/External/Modules/Volumes/VolumesModule.cs +++ b/NewHorizons/External/Modules/Volumes/VolumesModule.cs @@ -11,6 +11,11 @@ namespace NewHorizons.External.Modules.Volumes /// public AudioVolumeInfo[] audioVolumes; + /// + /// Add condition trigger volumes to this planet. Sets a condition when the player, scout, or ship enters this volume. + /// + public ConditionTriggerVolumeInfo[] conditionTriggerVolumes; + /// /// Add day night audio volumes to this planet. These volumes play a different clip depending on the time of day. /// From 330564538a3e996b2f4a90fa4a85fcaef65795d8 Mon Sep 17 00:00:00 2001 From: Ben C Date: Tue, 1 Jul 2025 17:36:52 +0000 Subject: [PATCH 04/18] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 75 ++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 27da29db..7b35e000 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -5365,6 +5365,13 @@ "$ref": "#/definitions/AudioVolumeInfo" } }, + "conditionTriggerVolumes": { + "type": "array", + "description": "Add condition trigger volumes to this planet. Sets a condition when the player, scout, or ship enters this volume.", + "items": { + "$ref": "#/definitions/ConditionTriggerVolumeInfo" + } + }, "dayNightAudioVolumes": { "type": "array", "description": "Add day night audio volumes to this planet. These volumes play a different clip depending on the time of day.", @@ -5717,6 +5724,74 @@ "manual" ] }, + "ConditionTriggerVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "radius": { + "type": "number", + "description": "The radius of this volume, if a shape is not specified.", + "format": "float", + "default": 1.0 + }, + "shape": { + "description": "The shape of this volume. Defaults to a sphere with a radius of `radius` if not specified.", + "$ref": "#/definitions/ShapeInfo" + }, + "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" + }, + "condition": { + "type": "string", + "description": "The name of the dialogue condition or persistent condition to set when entering the volume." + }, + "persistent": { + "type": "boolean", + "description": "If true, the condition will persist across all future loops until unset." + }, + "reversible": { + "type": "boolean", + "description": "Whether to unset the condition when existing the volume." + }, + "player": { + "type": "boolean", + "description": "Whether to set the condition when the player enters this volume. Defaults to true.", + "default": true + }, + "probe": { + "type": "boolean", + "description": "Whether to set the condition when the scout probe enters this volume." + }, + "ship": { + "type": "boolean", + "description": "Whether to set the condition when the ship enters this volume." + } + } + }, "DayNightAudioVolumeInfo": { "type": "object", "additionalProperties": false, From d072a74b5d9f9656a96fd348a3dd6afb75c6a68d Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Tue, 1 Jul 2025 17:08:13 -0500 Subject: [PATCH 05/18] Dream candle and projection totem conditions --- .../EchoesOfTheEye/DreamCandleBuilder.cs | 8 ++ .../EchoesOfTheEye/ProjectionTotemBuilder.cs | 8 ++ .../EOTE/DreamLightConditionController.cs | 85 +++++++++++++++++++ .../Props/EchoesOfTheEye/DreamCandleInfo.cs | 5 ++ .../EchoesOfTheEye/DreamLightConditionInfo.cs | 31 +++++++ .../EchoesOfTheEye/ProjectionTotemInfo.cs | 5 ++ 6 files changed, 142 insertions(+) create mode 100644 NewHorizons/Components/EOTE/DreamLightConditionController.cs create mode 100644 NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs index 38387bf8..be0b2184 100644 --- a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs @@ -1,3 +1,4 @@ +using NewHorizons.Components.EOTE; using NewHorizons.External.Modules.Props; using NewHorizons.External.Modules.Props.EchoesOfTheEye; using NewHorizons.Handlers; @@ -9,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using TerrainBaker; using UnityEngine; namespace NewHorizons.Builder.Props.EchoesOfTheEye @@ -70,6 +72,12 @@ namespace NewHorizons.Builder.Props.EchoesOfTheEye dreamCandle._startLit = info.startLit; dreamCandle.SetLit(info.startLit, false, true); + if (info.condition != null) + { + var conditionController = dreamCandle.gameObject.AddComponent(); + conditionController.SetFromInfo(info.condition); + } + return candleObj; } } diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs index 69155147..6b70b567 100644 --- a/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/ProjectionTotemBuilder.cs @@ -1,3 +1,4 @@ +using NewHorizons.Components.EOTE; using NewHorizons.External.Modules.Props; using NewHorizons.External.Modules.Props.EchoesOfTheEye; using NewHorizons.Handlers; @@ -116,6 +117,13 @@ namespace NewHorizons.Builder.Props.EchoesOfTheEye projector._lit = info.startLit; projector._startLit = info.startLit; projector._extinguishOnly = info.extinguishOnly; + + if (info.condition != null) + { + var conditionController = projector.gameObject.AddComponent(); + conditionController.SetFromInfo(info.condition); + } + /* Delay.FireOnNextUpdate(() => { diff --git a/NewHorizons/Components/EOTE/DreamLightConditionController.cs b/NewHorizons/Components/EOTE/DreamLightConditionController.cs new file mode 100644 index 00000000..7800f236 --- /dev/null +++ b/NewHorizons/Components/EOTE/DreamLightConditionController.cs @@ -0,0 +1,85 @@ +using NewHorizons.External.Modules.Props.EchoesOfTheEye; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.EOTE +{ + public class DreamLightConditionController : MonoBehaviour + { + public string Condition { get; set; } + public bool Persistent { get; set; } + public bool Reversible { get; set; } + public bool OnExtinguish { get; set; } + + DreamObjectProjector _projector; + DreamCandle _dreamCandle; + + public void SetFromInfo(DreamLightConditionInfo info) + { + Condition = info.condition; + Persistent = info.persistent; + Reversible = info.reversible; + OnExtinguish = info.onExtinguish; + } + + protected void Awake() + { + _projector = GetComponent(); + _projector.OnProjectorLit.AddListener(OnProjectorLit); + _projector.OnProjectorExtinguished.AddListener(OnProjectorExtinguished); + + _dreamCandle = GetComponent(); + if (_dreamCandle != null) + { + _dreamCandle.OnLitStateChanged.AddListener(OnCandleLitStateChanged); + } + } + + protected void OnDestroy() + { + if (_projector != null) + { + _projector.OnProjectorLit.RemoveListener(OnProjectorLit); + _projector.OnProjectorExtinguished.RemoveListener(OnProjectorExtinguished); + } + if (_dreamCandle != null) + { + _dreamCandle.OnLitStateChanged.RemoveListener(OnCandleLitStateChanged); + } + } + + private void OnProjectorLit() + { + HandleCondition(!OnExtinguish); + } + + private void OnProjectorExtinguished() + { + HandleCondition(OnExtinguish); + } + + private void OnCandleLitStateChanged() + { + HandleCondition(OnExtinguish ? !_dreamCandle._lit : _dreamCandle._lit); + } + + private void HandleCondition(bool shouldSet) + { + if (shouldSet || Reversible) + { + if (Persistent) + { + PlayerData.SetPersistentCondition(Condition, shouldSet); + } + else + { + DialogueConditionManager.SharedInstance.SetConditionState(Condition, shouldSet); + } + } + } + } +} diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs index 7f4efb24..95017236 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs @@ -20,5 +20,10 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye /// Whether the candle should start lit or extinguished. /// public bool startLit; + + /// + /// A condition to set when the candle is lit. + /// + public DreamLightConditionInfo condition; } } diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs new file mode 100644 index 00000000..b1b00583 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamLightConditionInfo.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Props.EchoesOfTheEye +{ + public class DreamLightConditionInfo + { + /// + /// The name of the dialogue condition or persistent condition to set when the light is lit. + /// + public string condition; + + /// + /// If true, the condition will persist across all future loops until unset. + /// + public bool persistent; + + /// + /// Whether to unset the condition when the light is extinguished again. + /// + public bool reversible; + + /// + /// Whether to set the condition when the light is extinguished instead. If `reversible` is true, the condition will be unset when the light is lit again. + /// + public bool onExtinguish; + } +} diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs index 0852b7e9..31953579 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionTotemInfo.cs @@ -45,5 +45,10 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye /// If set, projected objects will be set to fully active or fully disabled instantly instead of smoothly fading lights/renderers/colliders. Use this if the normal behavior is insufficient for the objects you're using. /// public bool toggleProjectedObjectsActive; + + /// + /// A condition to set when the totem is lit. + /// + public DreamLightConditionInfo condition; } } From b71aa9f84537fbc28582116980aa86553ec9c2fb Mon Sep 17 00:00:00 2001 From: Ben C Date: Tue, 1 Jul 2025 22:10:15 +0000 Subject: [PATCH 06/18] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 7b35e000..d8f7036c 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -4571,6 +4571,10 @@ "startLit": { "type": "boolean", "description": "Whether the candle should start lit or extinguished." + }, + "condition": { + "description": "A condition to set when the candle is lit.", + "$ref": "#/definitions/DreamLightConditionInfo" } } }, @@ -4600,6 +4604,28 @@ "pile" ] }, + "DreamLightConditionInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "condition": { + "type": "string", + "description": "The name of the dialogue condition or persistent condition to set when the light is lit." + }, + "persistent": { + "type": "boolean", + "description": "If true, the condition will persist across all future loops until unset." + }, + "reversible": { + "type": "boolean", + "description": "Whether to unset the condition when the light is extinguished again." + }, + "onExtinguish": { + "type": "boolean", + "description": "Whether to set the condition when the light is extinguished instead. If `reversible` is true, the condition will be unset when the light is lit again." + } + } + }, "ProjectionTotemInfo": { "type": "object", "additionalProperties": false, @@ -4667,6 +4693,10 @@ "toggleProjectedObjectsActive": { "type": "boolean", "description": "If set, projected objects will be set to fully active or fully disabled instantly instead of smoothly fading lights/renderers/colliders. Use this if the normal behavior is insufficient for the objects you're using." + }, + "condition": { + "description": "A condition to set when the totem is lit.", + "$ref": "#/definitions/DreamLightConditionInfo" } } }, From 1330df64b48c70e848f49ae138a37d08f8146f46 Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Fri, 4 Jul 2025 14:21:14 -0500 Subject: [PATCH 07/18] Interaction volumes --- .../Volumes/InteractionVolumeBuilder.cs | 87 +++++++++++++++++++ .../Builder/Volumes/VolumesBuildManager.cs | 7 ++ .../Components/Volumes/NHInteractionVolume.cs | 69 +++++++++++++++ .../VolumeInfos/InteractionVolumeInfo.cs | 65 ++++++++++++++ .../External/Modules/Volumes/VolumesModule.cs | 6 ++ 5 files changed, 234 insertions(+) create mode 100644 NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs create mode 100644 NewHorizons/Components/Volumes/NHInteractionVolume.cs create mode 100644 NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs diff --git a/NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs b/NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs new file mode 100644 index 00000000..2f5c0122 --- /dev/null +++ b/NewHorizons/Builder/Volumes/InteractionVolumeBuilder.cs @@ -0,0 +1,87 @@ +using NewHorizons.Components.Volumes; +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using NewHorizons.Utility.Files; +using NewHorizons.Utility.OuterWilds; +using NewHorizons.Utility.OWML; +using OWML.Common; +using UnityEngine; + +namespace NewHorizons.Builder.Volumes +{ + internal static class InteractionVolumeBuilder + { + public static InteractReceiver Make(GameObject planetGO, Sector sector, InteractionVolumeInfo info, IModBehaviour mod) + { + // Interaction volumes must use colliders because the first-person interaction system uses raycasting + if (info.shape != null) + { + info.shape.useShape = false; + } + + var receiver = VolumeBuilder.Make(planetGO, sector, info); + receiver.gameObject.layer = Layer.Interactible; + + receiver._interactRange = info.range; + receiver._checkViewAngle = info.maxViewAngle.HasValue; + receiver._maxViewAngle = info.maxViewAngle ?? 180f; + receiver._usableInShip = info.usableInShip; + + var volume = receiver.gameObject.AddComponent(); + + volume.Reusable = info.reusable; + volume.Condition = info.condition; + volume.Persistent = info.persistent; + + if (!string.IsNullOrEmpty(info.audio)) + { + var audioSource = receiver.gameObject.AddComponent(); + + // This could be more configurable but this should cover the most common use cases without bloating the info object + var owAudioSource = receiver.gameObject.AddComponent(); + owAudioSource._audioSource = audioSource; + owAudioSource.playOnAwake = false; + owAudioSource.loop = false; + owAudioSource.SetMaxVolume(1f); + owAudioSource.SetClipSelectionType(OWAudioSource.ClipSelectionOnPlay.RANDOM); + owAudioSource.SetTrack(OWAudioMixer.TrackName.Environment); + AudioUtilities.SetAudioClip(owAudioSource, info.audio, mod); + } + + if (!string.IsNullOrEmpty(info.pathToAnimator)) + { + var animObj = planetGO.transform.Find(info.pathToAnimator); + + if (animObj == null) + { + NHLogger.LogError($"Couldn't find child of {planetGO.transform.GetPath()} at {info.pathToAnimator}"); + } + else + { + var animator = animObj.GetComponent(); + if (animator == null) + { + NHLogger.LogError($"Couldn't find Animator on {animObj.name} at {info.pathToAnimator}"); + } + else + { + volume.TargetAnimator = animator; + volume.AnimationTrigger = info.animationTrigger; + } + } + } + + receiver.gameObject.SetActive(true); + + var text = TranslationHandler.GetTranslation(info.prompt, TranslationHandler.TextType.UI); + Delay.FireOnNextUpdate(() => + { + // This NREs if set immediately + receiver.ChangePrompt(text); + }); + + return receiver; + } + } +} diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs index 58ff8fdb..1b098f3f 100644 --- a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -70,6 +70,13 @@ namespace NewHorizons.Builder.Volumes VolumeBuilder.MakeAndEnable(go, sector, mapRestrictionVolume); } } + if (config.Volumes.interactionVolumes != null) + { + foreach (var interactionVolume in config.Volumes.interactionVolumes) + { + InteractionVolumeBuilder.Make(go, sector, interactionVolume, mod); + } + } if (config.Volumes.interferenceVolumes != null) { foreach (var interferenceVolume in config.Volumes.interferenceVolumes) diff --git a/NewHorizons/Components/Volumes/NHInteractionVolume.cs b/NewHorizons/Components/Volumes/NHInteractionVolume.cs new file mode 100644 index 00000000..4e258208 --- /dev/null +++ b/NewHorizons/Components/Volumes/NHInteractionVolume.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + public class NHInteractionVolume : MonoBehaviour + { + public bool Reusable { get; set; } + public string Condition { get; set; } + public bool Persistent { get; set; } + public Animator TargetAnimator { get; set; } + public string AnimationTrigger { get; set; } + + InteractReceiver _interactReceiver; + OWAudioSource _audioSource; + + protected void Awake() + { + _interactReceiver = GetComponent(); + _audioSource = GetComponent(); + + _interactReceiver.OnPressInteract += OnInteract; + } + + protected void OnDestroy() + { + _interactReceiver.OnPressInteract -= OnInteract; + } + + protected void OnInteract() + { + if (!string.IsNullOrEmpty(Condition)) + { + if (Persistent) + { + PlayerData.SetPersistentCondition(Condition, true); + } + else + { + DialogueConditionManager.SharedInstance.SetConditionState(Condition, true); + } + } + + if (_audioSource != null) + { + _audioSource.Play(); + } + + if (TargetAnimator) + { + TargetAnimator.SetTrigger(AnimationTrigger); + } + + if (Reusable) + { + _interactReceiver.ResetInteraction(); + _interactReceiver.EnableInteraction(); + } + else + { + _interactReceiver.DisableInteraction(); + } + } + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs new file mode 100644 index 00000000..0a9e7744 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/InteractionVolumeInfo.cs @@ -0,0 +1,65 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class InteractionVolumeInfo : VolumeInfo + { + /// + /// The prompt to display when the volume is interacted with. + /// + public string prompt; + + /// + /// The range at which the volume can be interacted with. + /// + [DefaultValue(2f)] public float range = 2f; + + /// + /// The max view angle (in degrees) the player can see the volume with to interact with it. This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation. + /// If not specified, no view angle restriction will be applied. + /// + public float? maxViewAngle; + + /// + /// Whether the volume can be interacted with while in the ship. + /// + public bool usableInShip; + + /// + /// Whether the volume can be interacted with multiple times. + /// + public bool reusable; + + /// + /// The name of the dialogue condition or persistent condition to set when the volume is interacted with. + /// + public string condition; + + /// + /// If true, the condition will persist across all future loops until unset. + /// + public bool persistent; + + /// + /// A sound to play when the volume is interacted with. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string audio; + + /// + /// A path to an animator component where an animation will be triggered when the volume is interacted with. + /// + public string pathToAnimator; + + /// + /// The name of an animation trigger to set on the animator when the volume is interacted with. + /// + public string animationTrigger; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumesModule.cs b/NewHorizons/External/Modules/Volumes/VolumesModule.cs index 7f44b911..959d8004 100644 --- a/NewHorizons/External/Modules/Volumes/VolumesModule.cs +++ b/NewHorizons/External/Modules/Volumes/VolumesModule.cs @@ -43,6 +43,12 @@ namespace NewHorizons.External.Modules.Volumes /// public HazardVolumeInfo[] hazardVolumes; + /// + /// Add interaction volumes to this planet. + /// They can be interacted with by the player to trigger various effects. + /// + public InteractionVolumeInfo[] interactionVolumes; + /// /// Add interference volumes to this planet. /// Hides HUD markers of ship scout/probe and prevents scout photos if you are not inside the volume together with ship or scout probe. From 2e06de70c272928bb78ed2cc0dc523a6f87012e9 Mon Sep 17 00:00:00 2001 From: Ben C Date: Fri, 4 Jul 2025 19:22:54 +0000 Subject: [PATCH 08/18] Updated Schemas --- NewHorizons/Schemas/body_schema.json | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index d8f7036c..265cef6b 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -5434,6 +5434,13 @@ "$ref": "#/definitions/HazardVolumeInfo" } }, + "interactionVolumes": { + "type": "array", + "description": "Add interaction volumes to this planet.\nThey can be interacted with by the player to trigger various effects.", + "items": { + "$ref": "#/definitions/InteractionVolumeInfo" + } + }, "interferenceVolumes": { "type": "array", "description": "Add interference volumes to this planet.\nHides HUD markers of ship scout/probe and prevents scout photos if you are not inside the volume together with ship or scout probe.", @@ -6626,6 +6633,95 @@ } } }, + "InteractionVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "radius": { + "type": "number", + "description": "The radius of this volume, if a shape is not specified.", + "format": "float", + "default": 1.0 + }, + "shape": { + "description": "The shape of this volume. Defaults to a sphere with a radius of `radius` if not specified.", + "$ref": "#/definitions/ShapeInfo" + }, + "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" + }, + "prompt": { + "type": "string", + "description": "The prompt to display when the volume is interacted with." + }, + "range": { + "type": "number", + "description": "The range at which the volume can be interacted with.", + "format": "float", + "default": 2.0 + }, + "maxViewAngle": { + "type": [ + "null", + "number" + ], + "description": "The max view angle (in degrees) the player can see the volume with to interact with it. This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation.\nIf not specified, no view angle restriction will be applied.", + "format": "float" + }, + "usableInShip": { + "type": "boolean", + "description": "Whether the volume can be interacted with while in the ship." + }, + "reusable": { + "type": "boolean", + "description": "Whether the volume can be interacted with multiple times." + }, + "condition": { + "type": "string", + "description": "The name of the dialogue condition or persistent condition to set when the volume is interacted with." + }, + "persistent": { + "type": "boolean", + "description": "If true, the condition will persist across all future loops until unset." + }, + "audio": { + "type": "string", + "description": "A sound to play when the volume is interacted with. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "pathToAnimator": { + "type": "string", + "description": "A path to an animator component where an animation will be triggered when the volume is interacted with. " + }, + "animationTrigger": { + "type": "string", + "description": "The name of an animation trigger to set on the animator when the volume is interacted with." + } + } + }, "VolumeInfo": { "type": "object", "additionalProperties": false, From e4f89be4e9978ed1090b1503e6dd197edcd6bfcd Mon Sep 17 00:00:00 2001 From: Joshua Thome Date: Fri, 4 Jul 2025 14:27:19 -0500 Subject: [PATCH 09/18] Remove accidental import --- NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs index be0b2184..5b648899 100644 --- a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -using TerrainBaker; using UnityEngine; namespace NewHorizons.Builder.Props.EchoesOfTheEye From ea85e3bab0f48686085eba384dc0cd29ed841ef8 Mon Sep 17 00:00:00 2001 From: Noah Pilarski Date: Sun, 8 Jun 2025 00:35:06 -0400 Subject: [PATCH 10/18] Fix items not having sound when you make socket first --- NewHorizons/Builder/Props/ItemBuilder.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/NewHorizons/Builder/Props/ItemBuilder.cs b/NewHorizons/Builder/Props/ItemBuilder.cs index 05620329..15b3bc27 100644 --- a/NewHorizons/Builder/Props/ItemBuilder.cs +++ b/NewHorizons/Builder/Props/ItemBuilder.cs @@ -16,13 +16,6 @@ namespace NewHorizons.Builder.Props internal static void Init() { - if (_itemTypes != null) - { - foreach (var value in _itemTypes.Values) - { - EnumUtils.Remove(value); - } - } _itemTypes = new Dictionary(); } @@ -141,11 +134,7 @@ namespace NewHorizons.Builder.Props { go.layer = Layer.Interactible; - var itemType = EnumUtils.TryParse(info.itemType, true, out ItemType result) ? result : ItemType.Invalid; - if (itemType == ItemType.Invalid && !string.IsNullOrEmpty(info.itemType)) - { - itemType = EnumUtilities.Create(info.itemType); - } + var itemType = GetOrCreateItemType(info.itemType); var socket = go.GetAddComponent(); socket._sector = sector; @@ -205,7 +194,7 @@ namespace NewHorizons.Builder.Props } else if (!string.IsNullOrEmpty(name)) { - itemType = EnumUtils.Create(name); + itemType = EnumUtilities.Create(name); _itemTypes.Add(name, itemType); } return itemType; From 20126dfe91d4e4150fd173a5f6620900a33fddf6 Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Sat, 19 Jul 2025 00:59:47 -0700 Subject: [PATCH 11/18] unload streaming in custom systems --- NewHorizons/Handlers/PlanetCreationHandler.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 1528151d..4730e58e 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -42,6 +42,15 @@ namespace NewHorizons.Handlers public static void Init(List bodies) { + // TH gets preloaded in title screen. custom systems dont need this + if (Main.Instance.CurrentStarSystem is not ("SolarSystem" or "EyeOfTheUniverse")) + { + foreach (var bundle in StreamingManager.s_activeBundles) + { + StreamingManager.UnloadStreamingAssets(bundle.assetBundleName); + } + } + // Start by destroying all planets if need be if (Main.SystemDict[Main.Instance.CurrentStarSystem].Config.destroyStockPlanets) { From d38d9fa1c86323d48376f79ea55efd71e1a50fab Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Sat, 19 Jul 2025 01:07:04 -0700 Subject: [PATCH 12/18] unload immediate --- NewHorizons/Handlers/PlanetCreationHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 4730e58e..e1c4864e 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -47,7 +47,8 @@ namespace NewHorizons.Handlers { foreach (var bundle in StreamingManager.s_activeBundles) { - StreamingManager.UnloadStreamingAssets(bundle.assetBundleName); + // save memory NOW instead of next frame when other stuff has loaded and taken memory + bundle.UnloadImmediate(); } } From b0f445e415f9e0b19131ee99c0d67f16630235d4 Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Sat, 19 Jul 2025 01:11:15 -0700 Subject: [PATCH 13/18] Revert "unload immediate" This reverts commit d38d9fa1c86323d48376f79ea55efd71e1a50fab. --- NewHorizons/Handlers/PlanetCreationHandler.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index e1c4864e..4730e58e 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -47,8 +47,7 @@ namespace NewHorizons.Handlers { foreach (var bundle in StreamingManager.s_activeBundles) { - // save memory NOW instead of next frame when other stuff has loaded and taken memory - bundle.UnloadImmediate(); + StreamingManager.UnloadStreamingAssets(bundle.assetBundleName); } } From 361fc2aa57688c1ca3629cc32448018941b5f55f Mon Sep 17 00:00:00 2001 From: JohnCorby Date: Sat, 19 Jul 2025 01:14:38 -0700 Subject: [PATCH 14/18] Reapply "unload immediate" This reverts commit b0f445e415f9e0b19131ee99c0d67f16630235d4. --- NewHorizons/Handlers/PlanetCreationHandler.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 4730e58e..e1c4864e 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -47,7 +47,8 @@ namespace NewHorizons.Handlers { foreach (var bundle in StreamingManager.s_activeBundles) { - StreamingManager.UnloadStreamingAssets(bundle.assetBundleName); + // save memory NOW instead of next frame when other stuff has loaded and taken memory + bundle.UnloadImmediate(); } } From 5391e4a5555c451c27ce94182fdce82c13d4f85b Mon Sep 17 00:00:00 2001 From: coderCleric <56094451+coderCleric@users.noreply.github.com> Date: Tue, 29 Jul 2025 18:15:36 -0600 Subject: [PATCH 15/18] Add Nomai Text Printer to the docs --- docs/src/content/docs/guides/nomai-text.md | 4 +++- docs/src/content/docs/start-here/helpful-resources.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/guides/nomai-text.md b/docs/src/content/docs/guides/nomai-text.md index 959aeeb4..df6fb82b 100644 --- a/docs/src/content/docs/guides/nomai-text.md +++ b/docs/src/content/docs/guides/nomai-text.md @@ -23,4 +23,6 @@ To unlock ship logs after reading each text block, add a `` n In your planet config, you must define where the Nomai text is positioned. See [the translator text json schema](/schemas/body-schema/defs/translatortextinfo/) for more info. -You can input a `seed` for a wall of text which will randomly generate the position of each arc. To test out different combinations, just keep incrementing the number and then hit "Reload Configs" from the pause menu with debug mode on. This seed ensures the same positioning each time the mod is played. Alternatively, you can use `arcInfo` to set the position and rotation of all text arcs, as well as determining their types (adult, teenager, child, or Stranger). The various age stages make the text look messier, while Stranger allows you to make a translatable version of the DLC text. \ No newline at end of file +You can input a `seed` for a wall of text which will randomly generate the position of each arc. To test out different combinations, just keep incrementing the number and then hit "Reload Configs" from the pause menu with debug mode on. This seed ensures the same positioning each time the mod is played. Alternatively, you can use `arcInfo` to set the position and rotation of all text arcs, as well as determining their types (adult, teenager, child, or Stranger). The various age stages make the text look messier, while Stranger allows you to make a translatable version of the DLC text. + +If you decide to arrange text manually in your mod, the [Unity Explorer](https://outerwildsmods.com/mods/unityexplorer/) and [Nomai Text Printer](https://github.com/coderCleric/NomaiTextPrinter) mods can make that much more convenient. \ No newline at end of file diff --git a/docs/src/content/docs/start-here/helpful-resources.md b/docs/src/content/docs/start-here/helpful-resources.md index 0ca13791..4ba3f2c8 100644 --- a/docs/src/content/docs/start-here/helpful-resources.md +++ b/docs/src/content/docs/start-here/helpful-resources.md @@ -56,6 +56,7 @@ These mods are useful when developing your addon - [Save Editor](https://outerwildsmods.com/mods/saveeditor) - Useful when creating a custom [ship log](/ship-log), can be used to reveal all custom facts so you can see them in the ship's computer. - [Time Saver](https://outerwildsmods.com/mods/timesaver/) - Lets you skip some repeated cutscenes and get into the game faster. - [The Examples Mod](https://github.com/Outer-Wilds-New-Horizons/nh-examples) - A mod that contains examples of how to use New Horizons features. +- [Nomai Text Printer](https://github.com/coderCleric/NomaiTextPrinter) - Useful for saving text changes (such as moving arcs with Unity Explorer) to your json files. ## Helpful Tools From f7c9685457840db9f809bd8ba16ba9963ece2d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter-Mikha=C3=ABl=20Richard?= Date: Thu, 31 Jul 2025 06:21:48 +0200 Subject: [PATCH 16/18] Update ScatterBuilder.cs --- NewHorizons/Builder/Props/ScatterBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index 9f08d403..b47d6ab0 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -83,7 +83,7 @@ namespace NewHorizons.Builder.Props bool reasonableHeightConstraints = true; if (!propInfo.preventOverlap && (heightMapTexture != null) && (propInfo.minHeight != null || propInfo.maxHeight != null)) // If caution is relevant { - var maxHeight = (propInfo.minHeight != null ? Math.Min(propInfo.maxHeight, heightMap.maxHeight) : heightMap.maxHeight); + var maxHeight = (propInfo.maxHeight != null ? Math.Min(propInfo.maxHeight, heightMap.maxHeight) : heightMap.maxHeight); var minHeight = (propInfo.minHeight != null ? Math.Max(propInfo.minHeight, heightMap.minHeight) : heightMap.minHeight); if ((maxHeight - minHeight) / (heightMap.maxHeight - heightMap.minHeight) < 0.001) // If height roll has less than 0.1% chance of being valid reasonableHeightConstraints = false; // Ignore propInfo.min/maxHeight to prevent infinite rerolls From 38ae757f58ddc5562ef9dc9782b36994995ff918 Mon Sep 17 00:00:00 2001 From: Noah Pilarski Date: Mon, 4 Aug 2025 19:00:57 -0400 Subject: [PATCH 17/18] add log --- NewHorizons/Builder/Props/ScatterBuilder.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index b47d6ab0..ee33e2f9 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -86,7 +86,10 @@ namespace NewHorizons.Builder.Props var maxHeight = (propInfo.maxHeight != null ? Math.Min(propInfo.maxHeight, heightMap.maxHeight) : heightMap.maxHeight); var minHeight = (propInfo.minHeight != null ? Math.Max(propInfo.minHeight, heightMap.minHeight) : heightMap.minHeight); if ((maxHeight - minHeight) / (heightMap.maxHeight - heightMap.minHeight) < 0.001) // If height roll has less than 0.1% chance of being valid + { + NHLogger.LogError($"Ignoring minHeight/maxHeight for scatter of [{scatterPrefab.name}] to prevent infinite rerolls."); reasonableHeightConstraints = false; // Ignore propInfo.min/maxHeight to prevent infinite rerolls + } // That way, even if often not valid, it still won't loop much more than propInfo.count * 1000 per prop } for (int i = 0; i < propInfo.count; i++) From 201a310e2bd6291bb735e95e6a0b2a1626b42670 Mon Sep 17 00:00:00 2001 From: Noah Pilarski Date: Mon, 4 Aug 2025 19:02:27 -0400 Subject: [PATCH 18/18] extend a little --- NewHorizons/Builder/Props/ScatterBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index ee33e2f9..0df720d6 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -87,7 +87,7 @@ namespace NewHorizons.Builder.Props var minHeight = (propInfo.minHeight != null ? Math.Max(propInfo.minHeight, heightMap.minHeight) : heightMap.minHeight); if ((maxHeight - minHeight) / (heightMap.maxHeight - heightMap.minHeight) < 0.001) // If height roll has less than 0.1% chance of being valid { - NHLogger.LogError($"Ignoring minHeight/maxHeight for scatter of [{scatterPrefab.name}] to prevent infinite rerolls."); + NHLogger.LogError($"Ignoring minHeight/maxHeight for scatter of [{scatterPrefab.name}] to prevent infinite rerolls from too much constraint on height."); reasonableHeightConstraints = false; // Ignore propInfo.min/maxHeight to prevent infinite rerolls } // That way, even if often not valid, it still won't loop much more than propInfo.count * 1000 per prop