diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5a040682..c56ab4f4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -66,4 +66,4 @@ jobs: uses: tj-actions/verify-changed-files@v20 id: changed_files with: - files: NewHorizons/Schemas/** + files: NewHorizons/Schemas/** \ No newline at end of file diff --git a/NewHorizons/Assets/translations/chinese_simple.json b/NewHorizons/Assets/translations/chinese_simple.json index f541e484..9d7d0560 100644 --- a/NewHorizons/Assets/translations/chinese_simple.json +++ b/NewHorizons/Assets/translations/chinese_simple.json @@ -6,6 +6,7 @@ "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_3": "之后只需要系好安全带并启动自动驾驶即可进行跃迁!" }, "UIDictionary": { + "INSTALL_OUTER_WILDS_CHINESE_FONT_FIX": "在享受故事之前,建议安装Outer Wilds Chinese Fix这个Mod来解决缺字问题。", "INTERSTELLAR_MODE": "恒星际模式", "FREQ_STATUE": "挪麦雕像", "FREQ_WARP_CORE": "反引力推进", diff --git a/NewHorizons/Assets/translations/english.json b/NewHorizons/Assets/translations/english.json index 6eed88c3..30857f54 100644 --- a/NewHorizons/Assets/translations/english.json +++ b/NewHorizons/Assets/translations/english.json @@ -6,6 +6,7 @@ "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_3": "Then just buckle up and engage the autopilot to warp there!" }, "UIDictionary": { + "INSTALL_OUTER_WILDS_CHINESE_FONT_FIX": "What why is this message being shown when the language is not set to Chinese go report this bug!", "INTERSTELLAR_MODE": "Interstellar Mode", "FREQ_STATUE": "Nomai Statue", "FREQ_WARP_CORE": "Anti-Graviton Flux", diff --git a/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs b/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs index 29854385..ac38cd76 100644 --- a/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs @@ -100,6 +100,15 @@ namespace NewHorizons.Builder.Atmosphere atmoGO.transform.position = planetGO.transform.TransformPoint(Vector3.zero); atmoGO.SetActive(true); + // CullGroups have already set up their renderers when this is done so we need to add ourself to it + // TODO: There are probably other builders where this is relevant + // This in particular was a bug affecting hazy dreams + if (sector != null && sector.gameObject.GetComponent() is CullGroup cullGroup) + { + cullGroup.RecursivelyAddRenderers(atmoGO.transform, true); + cullGroup.SetVisible(cullGroup.IsVisible()); + } + return atmoGO; } } diff --git a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs index e1c6d41e..c0319e79 100644 --- a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs +++ b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs @@ -5,6 +5,7 @@ using NewHorizons.External; using NewHorizons.External.Modules; using NewHorizons.External.Modules.Props; using NewHorizons.Utility; +using NewHorizons.Utility.DebugTools; using NewHorizons.Utility.Files; using NewHorizons.Utility.OWML; using OWML.Common; @@ -189,6 +190,7 @@ namespace NewHorizons.Builder.Body outerFogWarpVolume._linkedInnerWarpVolume = null; outerFogWarpVolume._name = OuterFogWarpVolume.Name.None; outerFogWarpVolume._sector = sector; + exitWarps.GetAddComponent().fogWarpVolume = outerFogWarpVolume; PairExit(config.linksTo, outerFogWarpVolume); diff --git a/NewHorizons/Builder/Body/CometTailBuilder.cs b/NewHorizons/Builder/Body/CometTailBuilder.cs index 9633b316..2af94def 100644 --- a/NewHorizons/Builder/Body/CometTailBuilder.cs +++ b/NewHorizons/Builder/Body/CometTailBuilder.cs @@ -60,14 +60,22 @@ namespace NewHorizons.Builder.Body } } - public static void Make(GameObject planetGO, Sector sector, CometTailModule cometTailModule, PlanetConfig config) + public static void Make(GameObject planetGO, Sector sector, CometTailModule cometTailModule, PlanetConfig config, AstroObject ao) { - if (config.Orbit.primaryBody == null) + var primaryBody = ao.GetPrimaryBody(); + + if (!string.IsNullOrEmpty(config.Orbit.primaryBody)) primaryBody = AstroObjectLocator.GetAstroObject(config.Orbit.primaryBody); + + if (primaryBody == null) { NHLogger.LogError($"Comet {planetGO.name} does not orbit anything. That makes no sense"); return; } + if (string.IsNullOrEmpty(cometTailModule.primaryBody)) + cometTailModule.primaryBody = !string.IsNullOrEmpty(config.Orbit.primaryBody) ? config.Orbit.primaryBody + : primaryBody.GetKey(); + var rootObj = new GameObject("CometRoot"); rootObj.SetActive(false); rootObj.transform.parent = sector?.transform ?? planetGO.transform; @@ -79,13 +87,11 @@ namespace NewHorizons.Builder.Body if (cometTailModule.rotationOverride != null) controller.SetRotationOverride(cometTailModule.rotationOverride); - if (string.IsNullOrEmpty(cometTailModule.primaryBody)) cometTailModule.primaryBody = config.Orbit.primaryBody; - Delay.FireOnNextUpdate(() => { controller.SetPrimaryBody( - AstroObjectLocator.GetAstroObject(cometTailModule.primaryBody).transform, - AstroObjectLocator.GetAstroObject(config.Orbit.primaryBody).GetAttachedOWRigidbody() + AstroObjectLocator.GetAstroObject(cometTailModule.primaryBody).transform, + primaryBody.GetAttachedOWRigidbody() ); }); diff --git a/NewHorizons/Builder/Body/ProxyBuilder.cs b/NewHorizons/Builder/Body/ProxyBuilder.cs index 0b4cafc3..943726eb 100644 --- a/NewHorizons/Builder/Body/ProxyBuilder.cs +++ b/NewHorizons/Builder/Body/ProxyBuilder.cs @@ -203,7 +203,7 @@ namespace NewHorizons.Builder.Body if (body.Config.CometTail != null) { - CometTailBuilder.Make(proxy, null, body.Config.CometTail, body.Config); + CometTailBuilder.Make(proxy, null, body.Config.CometTail, body.Config, planetGO.GetComponent()); } if (body.Config.Props?.proxyDetails != null) diff --git a/NewHorizons/Builder/Body/SingularityBuilder.cs b/NewHorizons/Builder/Body/SingularityBuilder.cs index eb8863c8..f7d303ef 100644 --- a/NewHorizons/Builder/Body/SingularityBuilder.cs +++ b/NewHorizons/Builder/Body/SingularityBuilder.cs @@ -150,6 +150,7 @@ namespace NewHorizons.Builder.Body streamingVolume.streamingGroup = streamingGroup; streamingVolume.transform.parent = blackHoleVolume.transform; streamingVolume.transform.localPosition = Vector3.zero; + streamingVolume.gameObject.SetActive(true); } } catch (Exception e) diff --git a/NewHorizons/Builder/General/GroupsBuilder.cs b/NewHorizons/Builder/General/GroupsBuilder.cs index b2252f5e..2998c02f 100644 --- a/NewHorizons/Builder/General/GroupsBuilder.cs +++ b/NewHorizons/Builder/General/GroupsBuilder.cs @@ -25,5 +25,8 @@ public static class GroupsBuilder go.GetAddComponent()._sector = sector; go.GetAddComponent()._sector = sector; go.GetAddComponent()._sector = sector; + + // SectorCollisionGroup is unique among the sector groups because it only attaches its event listener on Start() instead of Awake(), so if the detail gets immediately deactivated then it never gets attached. To avoid this, we'll attach the event listener manually (even if it means getting attached twice). + sector.OnSectorOccupantsUpdated += go.GetComponent().OnSectorOccupantsUpdated; } } \ No newline at end of file diff --git a/NewHorizons/Builder/General/MarkerBuilder.cs b/NewHorizons/Builder/General/MarkerBuilder.cs index ad17b191..a228f20a 100644 --- a/NewHorizons/Builder/General/MarkerBuilder.cs +++ b/NewHorizons/Builder/General/MarkerBuilder.cs @@ -15,7 +15,7 @@ namespace NewHorizons.Builder.General { var module = config.MapMarker; NHMapMarker mapMarker = body.AddComponent(); - mapMarker._labelID = (UITextType)TranslationHandler.AddUI(config.name); + mapMarker._labelID = (UITextType)TranslationHandler.AddUI(config.name, true); var markerType = MapMarker.MarkerType.Planet; diff --git a/NewHorizons/Builder/General/SpawnPointBuilder.cs b/NewHorizons/Builder/General/SpawnPointBuilder.cs index a38802b6..a5e8700d 100644 --- a/NewHorizons/Builder/General/SpawnPointBuilder.cs +++ b/NewHorizons/Builder/General/SpawnPointBuilder.cs @@ -69,6 +69,8 @@ namespace NewHorizons.Builder.General PlayerSpawn = playerSpawn; PlayerSpawnInfo = point; } + + spawnGO.SetActive(true); } } @@ -77,7 +79,6 @@ namespace NewHorizons.Builder.General foreach (var point in module.shipSpawnPoints) { var spawnGO = GeneralPropBuilder.MakeNew("ShipSpawnPoint", planetGO, null, point); - spawnGO.SetActive(false); spawnGO.layer = Layer.PlayerSafetyCollider; var shipSpawn = spawnGO.AddComponent(); diff --git a/NewHorizons/Builder/Props/BrambleNodeBuilder.cs b/NewHorizons/Builder/Props/BrambleNodeBuilder.cs index 810b43d8..5633ff06 100644 --- a/NewHorizons/Builder/Props/BrambleNodeBuilder.cs +++ b/NewHorizons/Builder/Props/BrambleNodeBuilder.cs @@ -4,6 +4,7 @@ using NewHorizons.External.Configs; using NewHorizons.External.Modules.Props.Audio; using NewHorizons.Handlers; using NewHorizons.Utility; +using NewHorizons.Utility.DebugTools; using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OWML; using Newtonsoft.Json; @@ -12,7 +13,6 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; using static NewHorizons.External.Modules.BrambleModule; -using static NewHorizons.External.Modules.SignalModule; namespace NewHorizons.Builder.Props { @@ -204,6 +204,10 @@ namespace NewHorizons.Builder.Props foreach (var fogWarpVolume in brambleNode.GetComponentsInChildren(true).Append(brambleNode.GetComponent())) { _nhFogWarpVolumes.Add(fogWarpVolume); + if (fogWarpVolume is SphericalFogWarpVolume sphericalFogWarpVolume) + { + fogWarpVolume.gameObject.GetAddComponent().fogWarpVolume = sphericalFogWarpVolume; + } } var innerFogWarpVolume = brambleNode.GetComponent(); diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index 022acd74..82a22d05 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -356,7 +356,10 @@ namespace NewHorizons.Builder.Props singleLightSensor._sector.OnSectorOccupantsUpdated -= singleLightSensor.OnSectorOccupantsUpdated; } singleLightSensor._sector = sector; - singleLightSensor._sector.OnSectorOccupantsUpdated += singleLightSensor.OnSectorOccupantsUpdated; + if (singleLightSensor._sector != null) + { + singleLightSensor._sector.OnSectorOccupantsUpdated += singleLightSensor.OnSectorOccupantsUpdated; + } } } @@ -470,10 +473,14 @@ namespace NewHorizons.Builder.Props { // These flood toggles are to disable flooded docks on the Stranger // Presumably the user isn't making one of those - foreach (var toggle in dock.GetComponents()) + foreach (var toggle in dock.GetComponentsInChildren()) { Component.DestroyImmediate(toggle); } + foreach (var floodSensor in dock.GetComponentsInChildren()) + { + Component.DestroyImmediate(floodSensor); + } } } diff --git a/NewHorizons/Builder/Props/DialogueBuilder.cs b/NewHorizons/Builder/Props/DialogueBuilder.cs index 8bd12c86..0878cd87 100644 --- a/NewHorizons/Builder/Props/DialogueBuilder.cs +++ b/NewHorizons/Builder/Props/DialogueBuilder.cs @@ -290,6 +290,10 @@ namespace NewHorizons.Builder.Props interact._interactRange = info.range; + // If a dialogue is on the ship, make sure its usable + // Assumes these are inside the ship and not outside, not sure if thats an issue + interact._usableInShip = planetGO.name == "Ship_Body"; + if (info.radius <= 0) { sphere.enabled = false; diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/DreamCandleBuilder.cs index 38387bf8..5b648899 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; @@ -70,6 +71,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/Builder/Props/EchoesOfTheEye/RaftBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/RaftBuilder.cs new file mode 100644 index 00000000..2ae3435f --- /dev/null +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/RaftBuilder.cs @@ -0,0 +1,180 @@ +using NewHorizons.Components.Props; +using NewHorizons.External.Modules.Props.EchoesOfTheEye; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using NewHorizons.Utility.OWML; +using System.Collections.Generic; +using UnityEngine; + +namespace NewHorizons.Builder.Props.EchoesOfTheEye +{ + public static class RaftBuilder + { + private static GameObject _prefab; + private static GameObject _cleanPrefab; + + internal static void InitPrefab() + { + if (_prefab == null) + { + _prefab = Object.FindObjectOfType()?.gameObject?.InstantiateInactive()?.Rename("Raft_Body_Prefab")?.DontDestroyOnLoad(); + if (_prefab == null) + { + NHLogger.LogWarning($"Tried to make a raft but couldn't. Do you have the DLC installed?"); + return; + } + else + { + _prefab.AddComponent()._destroyOnDLCNotOwned = true; + var raftController = _prefab.GetComponent(); + if (raftController._sector != null) + { + // Since awake already ran we have to unhook these events + raftController._sector.OnOccupantEnterSector -= raftController.OnOccupantEnterSector; + raftController._sector.OnOccupantExitSector -= raftController.OnOccupantExitSector; + raftController._sector = null; + } + raftController._riverFluid = null; + foreach (var lightSensor in _prefab.GetComponentsInChildren()) + { + if (lightSensor._sector != null) + { + lightSensor._sector.OnSectorOccupantsUpdated -= lightSensor.OnSectorOccupantsUpdated; + lightSensor._sector = null; + } + lightSensor._detectDreamLanterns = true; + lightSensor._lanternFocusThreshold = 0.9f; + lightSensor._illuminatingDreamLanternList = new List(); + lightSensor._lightSourceMask |= LightSourceType.DREAM_LANTERN; + } + + // TODO: Change to one mesh + var twRaftRoot = new GameObject("Effects_IP_SIM_Raft"); + twRaftRoot.transform.SetParent(_prefab.transform, false); + var twRaft = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Interactibles_Dreamworld/DreamRaft_Body/Effects_IP_SIM_Raft_1Way") + .Instantiate(Vector3.zero, Quaternion.identity, twRaftRoot.transform).Rename("Effects_IP_SIM_Raft_1Way"); + twRaft.Instantiate(Vector3.zero, Quaternion.Euler(0, 180, 0), twRaftRoot.transform).Rename(twRaft.name); + twRaft.Instantiate(Vector3.zero, Quaternion.Euler(0, 90, 0), twRaftRoot.transform).Rename(twRaft.name); + twRaft.Instantiate(Vector3.zero, Quaternion.Euler(0, -90, 0), twRaftRoot.transform).Rename(twRaft.name); + } + } + if (_cleanPrefab == null && _prefab != null) + { + _cleanPrefab = _prefab?.InstantiateInactive()?.Rename("Raft_Body_Prefab_Clean")?.DontDestroyOnLoad(); + if (_cleanPrefab == null) + { + NHLogger.LogWarning($"Tried to make a raft but couldn't. Do you have the DLC installed?"); + return; + } + else + { + var raftController = _cleanPrefab.GetComponent(); + var rwRaft = _cleanPrefab.FindChild("Structure_IP_Raft"); + var rwRaftAnimator = rwRaft.GetComponent(); + var rac = rwRaftAnimator.runtimeAnimatorController; + Object.DestroyImmediate(rwRaft); + + var dwRaft = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Interactibles_Dreamworld/DreamRaft_Body/Structure_IP_Raft") + .Instantiate(Vector3.zero, Quaternion.identity, _cleanPrefab.transform).Rename("Structure_IP_DreamRaft"); + dwRaft.transform.SetSiblingIndex(3); + foreach (var child in dwRaft.GetAllChildren()) + { + child.SetActive(true); + } + + var dwRaftAnimator = dwRaft.AddComponent(); + dwRaftAnimator.runtimeAnimatorController = rac; + raftController._railingAnimator = dwRaftAnimator; + + var dwLightSensorForward = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Interactibles_Dreamworld/DreamRaft_Body/LightSensor_Forward"); + var dwLightSensorOrigMaterial = dwLightSensorForward.GetComponent()._origMaterial; + var dwLightSensor = dwLightSensorForward.FindChild("Structure_IP_Raft_Sensor"); + ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Forward"), dwLightSensorOrigMaterial, dwLightSensor); + ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Right"), dwLightSensorOrigMaterial, dwLightSensor); + ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Rear"), dwLightSensorOrigMaterial, dwLightSensor, true); + ChangeSensor(_cleanPrefab.FindChild("LightSensorRoot/LightSensor_Left"), dwLightSensorOrigMaterial, dwLightSensor); + } + } + } + + private static void ChangeSensor(GameObject lightSensor, Material origMaterial, GameObject newLightSensor, bool reverse = false) + { + var singleLightSensor = lightSensor.GetComponent(); + var lightSensorEffects = lightSensor.GetComponent(); + lightSensorEffects._lightSensor = singleLightSensor; + Object.DestroyImmediate(lightSensor.FindChild("Structure_IP_Raft_Sensor")); + lightSensorEffects._origMaterial = origMaterial; + var copiedLightSensor = newLightSensor + .Instantiate(reverse ? new Vector3(0, -1.5f, -0.1303f) : new Vector3(0, -1.5f, -0.0297f), + reverse ? Quaternion.identity : Quaternion.Euler(0, 180, 0), + lightSensor.transform).Rename("Structure_IP_DreamRaft_Sensor"); + var bulb = copiedLightSensor.FindChild("Props_IP_Raft_Lamp_geoBulb"); + lightSensorEffects._renderer = bulb.GetComponent(); + lightSensorEffects._lightRenderer = bulb.GetComponent(); + } + + public static GameObject Make(GameObject planetGO, Sector sector, RaftInfo info, OWRigidbody planetBody) + { + InitPrefab(); + + if (_prefab == null || _cleanPrefab == null || sector == null) return null; + + GameObject raftObject = GeneralPropBuilder.MakeFromPrefab(info.pristine ? _cleanPrefab : _prefab, "Raft_Body", planetGO, sector, info); + + StreamingHandler.SetUpStreaming(raftObject, sector); + + var raftController = raftObject.GetComponent(); + raftController._sector = sector; + raftController._acceleration = info.acceleration; + sector.OnOccupantEnterSector += raftController.OnOccupantEnterSector; + sector.OnOccupantExitSector += raftController.OnOccupantExitSector; + + // Detectors + var fluidDetector = raftObject.transform.Find("Detector_Raft").GetComponent(); + var waterVolume = planetGO.GetComponentInChildren(); + fluidDetector._alignmentFluid = waterVolume; + fluidDetector._buoyancy.checkAgainstWaves = true; + // Rafts were unable to trigger docks because these were disabled for some reason + fluidDetector.GetComponent().enabled = true; + fluidDetector.GetComponent().enabled = true; + + // Light sensors + foreach (var lightSensor in raftObject.GetComponentsInChildren()) + { + lightSensor._sector = sector; + sector.OnSectorOccupantsUpdated += lightSensor.OnSectorOccupantsUpdated; + } + + var nhRaftController = raftObject.AddComponent(); + + var achievementObject = new GameObject("AchievementVolume"); + achievementObject.transform.SetParent(raftObject.transform, false); + + var shape = achievementObject.AddComponent(); + shape.radius = 3; + shape.SetCollisionMode(Shape.CollisionMode.Volume); + + achievementObject.AddComponent()._shape = shape; + achievementObject.AddComponent(); + + raftObject.SetActive(true); + + if (planetGO != null && !string.IsNullOrEmpty(info.dockPath)) + { + var dockTransform = planetGO.transform.Find(info.dockPath); + if (dockTransform != null && dockTransform.TryGetComponent(out RaftDock raftDock)) + { + raftController.SkipSuspendOnStart(); + raftDock._startRaft = raftController; + raftDock._raft = raftController; + } + else + { + NHLogger.LogError($"Cannot find raft dock object at path: {planetGO.name}/{info.dockPath}"); + } + } + + return raftObject; + } + } +} diff --git a/NewHorizons/Builder/Props/EchoesOfTheEye/RaftDockBuilder.cs b/NewHorizons/Builder/Props/EchoesOfTheEye/RaftDockBuilder.cs new file mode 100644 index 00000000..b8cc22fa --- /dev/null +++ b/NewHorizons/Builder/Props/EchoesOfTheEye/RaftDockBuilder.cs @@ -0,0 +1,55 @@ +using NewHorizons.Components.Props; +using NewHorizons.External.Modules.Props; +using NewHorizons.External.Modules.Props.EchoesOfTheEye; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using NewHorizons.Utility.OWML; +using OWML.Common; +using UnityEngine; + +namespace NewHorizons.Builder.Props.EchoesOfTheEye +{ + public static class RaftDockBuilder + { + private static GameObject _prefab; + + internal static void InitPrefab() + { + if (_prefab == null) + { + _prefab = SearchUtilities.Find("RingWorld_Body/Sector_RingInterior/Sector_Zone1/Structures_Zone1/RaftHouse_ArrivalPatio_Zone1/Interactables_RaftHouse_ArrivalPatio_Zone1/Prefab_IP_RaftDock").InstantiateInactive().Rename("Prefab_RaftDock").DontDestroyOnLoad(); + if (_prefab == null) + { + NHLogger.LogWarning($"Tried to make a raft dock but couldn't. Do you have the DLC installed?"); + return; + } + else + { + _prefab.AddComponent()._destroyOnDLCNotOwned = true; + var raftDock = _prefab.GetComponent(); + raftDock._startRaft = null; + raftDock._floodSensor = null; + foreach (var floodToggle in _prefab.GetComponents()) + { + Component.DestroyImmediate(floodToggle); + } + Object.DestroyImmediate(_prefab.FindChild("FloodSensor")); + Object.DestroyImmediate(_prefab.FindChild("FloodSensor_RaftHouseArrivalPatio_NoDelay")); + } + } + } + + public static GameObject Make(GameObject planetGO, Sector sector, RaftDockInfo info, IModBehaviour mod) + { + InitPrefab(); + + if (_prefab == null || sector == null) return null; + + var dockObject = DetailBuilder.Make(planetGO, sector, mod, _prefab, new DetailInfo(info)); + + //var raftDock = dockObject.GetComponent(); + + return dockObject; + } + } +} diff --git a/NewHorizons/Builder/Props/ItemBuilder.cs b/NewHorizons/Builder/Props/ItemBuilder.cs index f4992e8a..15b3bc27 100644 --- a/NewHorizons/Builder/Props/ItemBuilder.cs +++ b/NewHorizons/Builder/Props/ItemBuilder.cs @@ -1,6 +1,7 @@ using NewHorizons.Components.Props; using NewHorizons.External.Modules.Props.Item; using NewHorizons.Handlers; +using NewHorizons.Utility.OuterWilds; using NewHorizons.Utility.OWML; using OWML.Common; using OWML.Utils; @@ -15,18 +16,13 @@ namespace NewHorizons.Builder.Props internal static void Init() { - if (_itemTypes != null) - { - foreach (var value in _itemTypes.Values) - { - EnumUtils.Remove(value); - } - } _itemTypes = new Dictionary(); } public static NHItem MakeItem(GameObject go, GameObject planetGO, Sector sector, ItemInfo info, IModBehaviour mod) { + go.layer = Layer.Interactible; + var itemName = info.name; if (string.IsNullOrEmpty(itemName)) { @@ -93,10 +89,13 @@ namespace NewHorizons.Builder.Props if (info.colliderRadius > 0f) { - go.AddComponent().radius = info.colliderRadius; + var col = go.AddComponent(); + col.radius = info.colliderRadius; + col.isTrigger = info.colliderIsTrigger; go.GetAddComponent(); } + // Wait until next frame when all objects are built before trying to socket the item if it has an initial socket Delay.FireOnNextUpdate(() => { if (item != null && !string.IsNullOrEmpty(info.pathToInitialSocket)) @@ -133,11 +132,9 @@ namespace NewHorizons.Builder.Props public static NHItemSocket MakeSocket(GameObject go, GameObject planetGO, Sector sector, ItemSocketInfo info) { - 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); - } + go.layer = Layer.Interactible; + + var itemType = GetOrCreateItemType(info.itemType); var socket = go.GetAddComponent(); socket._sector = sector; @@ -151,15 +148,18 @@ namespace NewHorizons.Builder.Props if (socket._socketTransform == null) { var socketGO = GeneralPropBuilder.MakeNew("Socket", planetGO, sector, info, defaultParent: go.transform); - if (info.colliderRadius > 0f) - { - go.AddComponent().radius = info.colliderRadius; - go.GetAddComponent(); - } socketGO.SetActive(true); socket._socketTransform = socketGO.transform; } + if (info.colliderRadius > 0f) + { + var col = go.AddComponent(); + col.radius = info.colliderRadius; + col.isTrigger = info.colliderIsTrigger; + go.GetAddComponent(); + } + socket.ItemType = itemType; socket.UseGiveTakePrompts = info.useGiveTakePrompts; socket.InsertCondition = info.insertCondition; @@ -169,6 +169,7 @@ namespace NewHorizons.Builder.Props socket.ClearRemovalConditionOnInsert = info.clearRemovalConditionOnInsert; socket.RemovalFact = info.removalFact; + // Wait until initial item socketing is done before considering the socket empty Delay.FireInNUpdates(() => { if (socket != null && !socket._socketedItem) @@ -193,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; diff --git a/NewHorizons/Builder/Props/NomaiTextBuilder.cs b/NewHorizons/Builder/Props/NomaiTextBuilder.cs index ce4ed43d..d71f3d49 100644 --- a/NewHorizons/Builder/Props/NomaiTextBuilder.cs +++ b/NewHorizons/Builder/Props/NomaiTextBuilder.cs @@ -312,7 +312,7 @@ namespace NewHorizons.Builder.Props scrollItem._nomaiWallText = nomaiWallText; scrollItem.SetSector(sector); customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Geo").GetComponent().enabled = true; - customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Collider").gameObject.SetActive(true); + customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Collider").gameObject.SetActive(false); nomaiWallText.gameObject.GetComponent().enabled = false; customScroll.GetComponent().enabled = true; } diff --git a/NewHorizons/Builder/Props/ProjectionBuilder.cs b/NewHorizons/Builder/Props/ProjectionBuilder.cs index 2ebf64fc..8639d3c7 100644 --- a/NewHorizons/Builder/Props/ProjectionBuilder.cs +++ b/NewHorizons/Builder/Props/ProjectionBuilder.cs @@ -43,6 +43,7 @@ namespace NewHorizons.Builder.Props private static GameObject _autoPrefab; private static GameObject _visionTorchDetectorPrefab; private static GameObject _standingVisionTorchPrefab; + private static GameObject _standingVisionTorchCleanPrefab; private static readonly int EmissionMap = Shader.PropertyToID("_EmissionMap"); private static bool _isInit; @@ -90,13 +91,28 @@ namespace NewHorizons.Builder.Props _visionTorchDetectorPrefab.AddComponent()._destroyOnDLCNotOwned = true; } + if (_standingVisionTorchCleanPrefab == null) + { + _standingVisionTorchCleanPrefab = SearchUtilities.Find("DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_4/Interactibles_DreamZone_4_Upper/Prefab_IP_VisionTorchProjector")?.gameObject?.InstantiateInactive()?.Rename("Prefab_DW_VisionTorchProjector")?.DontDestroyOnLoad(); + if (_standingVisionTorchCleanPrefab == null) + NHLogger.LogWarning($"Tried to make standing vision torch prefab but couldn't. Do you have the DLC installed?"); + else + { + _standingVisionTorchCleanPrefab.AddComponent()._destroyOnDLCNotOwned = true; + GameObject.DestroyImmediate(_standingVisionTorchCleanPrefab.FindChild("Prefab_IP_Reel_PrisonPeephole_Vision")); + } + } + if (_standingVisionTorchPrefab == null) { _standingVisionTorchPrefab = SearchUtilities.Find("RingWorld_Body/Sector_RingWorld/Sector_SecretEntrance/Interactibles_SecretEntrance/Experiment_1/VisionTorchApparatus/VisionTorchRoot/Prefab_IP_VisionTorchProjector")?.gameObject?.InstantiateInactive()?.Rename("Prefab_IP_VisionTorchProjector")?.DontDestroyOnLoad(); if (_standingVisionTorchPrefab == null) NHLogger.LogWarning($"Tried to make standing vision torch prefab but couldn't. Do you have the DLC installed?"); else + { _standingVisionTorchPrefab.AddComponent()._destroyOnDLCNotOwned = true; + GameObject.Instantiate(_standingVisionTorchCleanPrefab.FindChild("Effects_IP_SIM_VisionTorch"), _standingVisionTorchPrefab.transform, false).Rename("Effects_IP_SIM_VisionTorch"); + } } } @@ -446,10 +462,11 @@ namespace NewHorizons.Builder.Props { InitPrefabs(); - if (_standingVisionTorchPrefab == null) return null; + if (_standingVisionTorchPrefab == null || _standingVisionTorchCleanPrefab == null) return null; // Spawn the torch itself - var standingTorch = DetailBuilder.Make(planetGO, sector, mod, _standingVisionTorchPrefab, new DetailInfo(info)); + var prefab = info.reelCondition == ProjectionInfo.SlideReelCondition.Pristine ? _standingVisionTorchCleanPrefab : _standingVisionTorchPrefab; + var standingTorch = DetailBuilder.Make(planetGO, sector, mod, prefab, new DetailInfo(info)); if (standingTorch == null) { diff --git a/NewHorizons/Builder/Props/PropBuildManager.cs b/NewHorizons/Builder/Props/PropBuildManager.cs index 579d5e21..fb8d21c7 100644 --- a/NewHorizons/Builder/Props/PropBuildManager.cs +++ b/NewHorizons/Builder/Props/PropBuildManager.cs @@ -114,6 +114,7 @@ namespace NewHorizons.Builder.Props MakeGeneralProps(go, config.Props.grappleTotems, (totem) => GrappleTotemBuilder.Make(go, sector, totem, mod)); MakeGeneralProps(go, config.Props.dreamCampfires, (campfire) => DreamCampfireBuilder.Make(go, sector, campfire, mod), (campfire) => campfire.id); MakeGeneralProps(go, config.Props.dreamArrivalPoints, (point) => DreamArrivalPointBuilder.Make(go, sector, point, mod), (point) => point.id); + MakeGeneralProps(go, config.Props.raftDocks, (dock) => RaftDockBuilder.Make(go, sector, dock, mod)); MakeGeneralProps(go, config.Props.rafts, (raft) => RaftBuilder.Make(go, sector, raft, planetBody)); } MakeGeneralProps(go, config.Props.tornados, (tornado) => TornadoBuilder.Make(go, sector, tornado, config.Atmosphere?.clouds != null)); diff --git a/NewHorizons/Builder/Props/QuantumBuilder.cs b/NewHorizons/Builder/Props/QuantumBuilder.cs index 302270e8..1a5bc8ab 100644 --- a/NewHorizons/Builder/Props/QuantumBuilder.cs +++ b/NewHorizons/Builder/Props/QuantumBuilder.cs @@ -69,13 +69,17 @@ namespace NewHorizons.Builder.Props { (GameObject go, QuantumDetailInfo detail)[] propsInGroup = quantumGroup.details.Select(x => (DetailBuilder.GetGameObjectFromDetailInfo(x), x)).ToArray(); - GameObject specialProp = null; QuantumDetailInfo specialInfo = null; if (propsInGroup.Length == quantumGroup.sockets.Length) { // Special case! - specialProp = propsInGroup.Last().go; + propsInGroup.Last().go.SetActive(false); + + // Will be manually positioned on the sockets anyway specialInfo = propsInGroup.Last().detail; + specialInfo.parentPath = string.Empty; + specialInfo.isRelativeToParent = false; + var propsInGroupList = propsInGroup.ToList(); propsInGroupList.RemoveAt(propsInGroup.Length - 1); propsInGroup = propsInGroupList.ToArray(); @@ -117,13 +121,13 @@ namespace NewHorizons.Builder.Props prop.go.SetActive(true); } - if (specialProp != null) + if (specialInfo != null) { // Can't have 4 objects in 4 slots // Instead we have a duplicate of the final object for each slot, which appears when that slot is "empty" for (int i = 0; i < sockets.Length; i++) - { - var emptySocketObject = DetailBuilder.Make(planetGO, sector, mod, specialProp, new DetailInfo()); + { + var emptySocketObject = DetailBuilder.Make(planetGO, sector, mod, new DetailInfo(specialInfo)); var socket = sockets[i]; socket._emptySocketObject = emptySocketObject; emptySocketObject.SetActive(socket._quantumObject == null); diff --git a/NewHorizons/Builder/Props/RaftBuilder.cs b/NewHorizons/Builder/Props/RaftBuilder.cs deleted file mode 100644 index 59f734bf..00000000 --- a/NewHorizons/Builder/Props/RaftBuilder.cs +++ /dev/null @@ -1,97 +0,0 @@ -using NewHorizons.Components.Props; -using NewHorizons.External.Modules.Props.EchoesOfTheEye; -using NewHorizons.Handlers; -using NewHorizons.Utility; -using NewHorizons.Utility.OWML; -using UnityEngine; - -namespace NewHorizons.Builder.Props -{ - public static class RaftBuilder - { - private static GameObject _prefab; - - internal static void InitPrefab() - { - if (_prefab == null) - { - _prefab = Object.FindObjectOfType()?.gameObject?.InstantiateInactive()?.Rename("Raft_Body_Prefab")?.DontDestroyOnLoad(); - if (_prefab == null) - { - NHLogger.LogWarning($"Tried to make a raft but couldn't. Do you have the DLC installed?"); - return; - } - else - { - _prefab.AddComponent()._destroyOnDLCNotOwned = true; - var raftController = _prefab.GetComponent(); - if (raftController._sector != null) - { - // Since awake already ran we have to unhook these events - raftController._sector.OnOccupantEnterSector -= raftController.OnOccupantEnterSector; - raftController._sector.OnOccupantExitSector -= raftController.OnOccupantExitSector; - raftController._sector = null; - } - raftController._riverFluid = null; - foreach (var lightSensor in _prefab.GetComponentsInChildren()) - { - if (lightSensor._sector != null) - { - lightSensor._sector.OnSectorOccupantsUpdated -= lightSensor.OnSectorOccupantsUpdated; - lightSensor._sector = null; - } - } - } - } - } - - public static GameObject Make(GameObject planetGO, Sector sector, RaftInfo info, OWRigidbody planetBody) - { - InitPrefab(); - - if (_prefab == null || sector == null) return null; - - GameObject raftObject = GeneralPropBuilder.MakeFromPrefab(_prefab, "Raft_Body", planetGO, sector, info); - - StreamingHandler.SetUpStreaming(raftObject, sector); - - var raftController = raftObject.GetComponent(); - raftController._sector = sector; - raftController._acceleration = info.acceleration; - sector.OnOccupantEnterSector += raftController.OnOccupantEnterSector; - sector.OnOccupantExitSector += raftController.OnOccupantExitSector; - - // Detectors - var fluidDetector = raftObject.transform.Find("Detector_Raft").GetComponent(); - var waterVolume = planetGO.GetComponentInChildren(); - fluidDetector._alignmentFluid = waterVolume; - fluidDetector._buoyancy.checkAgainstWaves = true; - // Rafts were unable to trigger docks because these were disabled for some reason - fluidDetector.GetComponent().enabled = true; - fluidDetector.GetComponent().enabled = true; - - // Light sensors - foreach (var lightSensor in raftObject.GetComponentsInChildren()) - { - lightSensor._sector = sector; - sector.OnSectorOccupantsUpdated += lightSensor.OnSectorOccupantsUpdated; - } - - var nhRaftController = raftObject.AddComponent(); - - var achievementObject = new GameObject("AchievementVolume"); - achievementObject.transform.SetParent(raftObject.transform, false); - - var shape = achievementObject.AddComponent(); - shape.radius = 3; - shape.SetCollisionMode(Shape.CollisionMode.Volume); - - achievementObject.AddComponent()._shape = shape; - achievementObject.AddComponent(); - - raftObject.SetActive(true); - - return raftObject; - } - } -} diff --git a/NewHorizons/Builder/Props/ShapeBuilder.cs b/NewHorizons/Builder/Props/ShapeBuilder.cs new file mode 100644 index 00000000..2563cd88 --- /dev/null +++ b/NewHorizons/Builder/Props/ShapeBuilder.cs @@ -0,0 +1,183 @@ +using NewHorizons.Components; +using NewHorizons.External.Modules.Props; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Builder.Props +{ + public static class ShapeBuilder + { + public static OWTriggerVolume AddTriggerVolume(GameObject go, ShapeInfo info, float defaultRadius) + { + var owTriggerVolume = go.AddComponent(); + + if (info != null) + { + var shapeOrCol = AddShapeOrCollider(go, info); + if (shapeOrCol is Shape shape) + { + owTriggerVolume._shape = shape; + } + else if (shapeOrCol is Collider col) + { + owTriggerVolume._owCollider = col.GetComponent(); + } + } + else + { + var shape = go.AddComponent(); + shape.radius = defaultRadius; + owTriggerVolume._shape = shape; + } + + return owTriggerVolume; + } + + public static Component AddShapeOrCollider(GameObject go, ShapeInfo info) + { + if (info.useShape.HasValue) + { + // Explicitly add either a shape or collider if specified + if (info.useShape.Value) + { + return AddShape(go, info); + } + else + { + return AddCollider(go, info); + } + } + else + { + // Prefer shapes over colliders if no preference is specified and not using collision + // This is required for backwards compat (previously it defaulted to shapes) + // A common-ish puzzle is to put an insulating volume on a held item. Held items disabled all colliders when held, but don't disable shapes. + // Changing the default from shapes to colliders broke these puzzles + // The reason OWItem disables all colliders (even those that are just triggers) is presumably for scrolls which have nomai text as a trigger collider + if (info.hasCollision) + { + return AddCollider(go, info); + } + else + { + return AddShape(go, info); + } + } + } + + public static Shape AddShape(GameObject go, ShapeInfo info) + { + if (info.hasCollision) + { + throw new NotSupportedException($"Shapes do not support collision; set {nameof(info.hasCollision)} to false or use a supported collider type (sphere, box, or capsule)."); + } + if (info.useShape.HasValue && !info.useShape.Value) + { + throw new NotSupportedException($"{info.useShape} was explicitly set to false but a shape is required here."); + } + switch (info.type) + { + case ShapeType.Sphere: + var sphereShape = go.AddComponent(); + sphereShape._radius = info.radius; + sphereShape._center = info.offset ?? Vector3.zero; + return sphereShape; + case ShapeType.Box: + var boxShape = go.AddComponent(); + boxShape._size = info.size ?? Vector3.one; + boxShape._center = info.offset ?? Vector3.zero; + return boxShape; + case ShapeType.Capsule: + var capsuleShape = go.AddComponent(); + capsuleShape._radius = info.radius; + capsuleShape._direction = (int)info.direction; + capsuleShape._height = info.height; + capsuleShape._center = info.offset ?? Vector3.zero; + return capsuleShape; + case ShapeType.Cylinder: + var cylinderShape = go.AddComponent(); + cylinderShape._radius = info.radius; + cylinderShape._height = info.height; + cylinderShape._center = info.offset ?? Vector3.zero; + cylinderShape._pointChecksOnly = true; + return cylinderShape; + case ShapeType.Cone: + var coneShape = go.AddComponent(); + coneShape._topRadius = info.innerRadius; + coneShape._bottomRadius = info.outerRadius; + coneShape._direction = (int)info.direction; + coneShape._height = info.height; + coneShape._center = info.offset ?? Vector3.zero; + coneShape._pointChecksOnly = true; + return coneShape; + case ShapeType.Hemisphere: + var hemisphereShape = go.AddComponent(); + hemisphereShape._radius = info.radius; + hemisphereShape._direction = (int)info.direction; + hemisphereShape._cap = info.cap; + hemisphereShape._center = info.offset ?? Vector3.zero; + hemisphereShape._pointChecksOnly = true; + return hemisphereShape; + case ShapeType.Hemicapsule: + var hemicapsuleShape = go.AddComponent(); + hemicapsuleShape._radius = info.radius; + hemicapsuleShape._direction = (int)info.direction; + hemicapsuleShape._height = info.height; + hemicapsuleShape._cap = info.cap; + hemicapsuleShape._center = info.offset ?? Vector3.zero; + hemicapsuleShape._pointChecksOnly = true; + return hemicapsuleShape; + case ShapeType.Ring: + var ringShape = go.AddComponent(); + ringShape.innerRadius = info.innerRadius; + ringShape.outerRadius = info.outerRadius; + ringShape.height = info.height; + ringShape.center = info.offset ?? Vector3.zero; + ringShape._pointChecksOnly = true; + return ringShape; + default: + throw new ArgumentOutOfRangeException(nameof(info.type), info.type, $"Unsupported shape type"); + } + } + + public static Collider AddCollider(GameObject go, ShapeInfo info) + { + if (info.useShape.HasValue && info.useShape.Value) + { + throw new NotSupportedException($"{info.useShape} was explicitly set to true but a non-shape collider is required here."); + } + switch (info.type) + { + case ShapeType.Sphere: + var sphereCollider = go.AddComponent(); + sphereCollider.radius = info.radius; + sphereCollider.center = info.offset ?? Vector3.zero; + sphereCollider.isTrigger = !info.hasCollision; + go.GetAddComponent(); + return sphereCollider; + case ShapeType.Box: + var boxCollider = go.AddComponent(); + boxCollider.size = info.size ?? Vector3.one; + boxCollider.center = info.offset ?? Vector3.zero; + boxCollider.isTrigger = !info.hasCollision; + go.GetAddComponent(); + return boxCollider; + case ShapeType.Capsule: + var capsuleCollider = go.AddComponent(); + capsuleCollider.radius = info.radius; + capsuleCollider.direction = (int)info.direction; + capsuleCollider.height = info.height; + capsuleCollider.center = info.offset ?? Vector3.zero; + capsuleCollider.isTrigger = !info.hasCollision; + go.GetAddComponent(); + return capsuleCollider; + default: + throw new ArgumentOutOfRangeException(nameof(info.type), info.type, $"Unsupported collider type"); + } + } + } +} diff --git a/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs b/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs index 18a09f87..f5cf3b2b 100644 --- a/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs +++ b/NewHorizons/Builder/Props/TranslatorText/TranslatorTextBuilder.cs @@ -206,7 +206,7 @@ namespace NewHorizons.Builder.Props.TranslatorText scrollItem._nomaiWallText = nomaiWallText; scrollItem.SetSector(sector); customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Geo").GetComponent().enabled = true; - customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Collider").gameObject.SetActive(true); + customScroll.transform.Find("Props_NOM_Scroll/Props_NOM_Scroll_Collider").gameObject.SetActive(false); nomaiWallText.gameObject.GetComponent().enabled = false; customScroll.GetComponent().enabled = true; scrollItem._nomaiWallText.HideImmediate(); @@ -471,7 +471,7 @@ namespace NewHorizons.Builder.Props.TranslatorText if (info.arcInfo != null && info.arcInfo.Count() != dict.Values.Count()) { - NHLogger.LogError($"Can't make NomaiWallText, arcInfo length [{info.arcInfo.Count()}] doesn't equal number of TextBlocks [{dict.Values.Count()}] in the xml"); + NHLogger.LogError($"Can't make NomaiWallText [{info.xmlFile}], arcInfo length [{info.arcInfo.Count()}] doesn't equal number of TextBlocks [{dict.Values.Count()}] in the xml"); return; } diff --git a/NewHorizons/Builder/ShipLog/MapModeBuilder.cs b/NewHorizons/Builder/ShipLog/MapModeBuilder.cs index de4116aa..1be8cc7c 100644 --- a/NewHorizons/Builder/ShipLog/MapModeBuilder.cs +++ b/NewHorizons/Builder/ShipLog/MapModeBuilder.cs @@ -459,11 +459,6 @@ namespace NewHorizons.Builder.ShipLog private static MapModeObject ConstructPrimaryNode(List bodies) { - float DistanceFromPrimary(NewHorizonsBody body) - { - return Mathf.Max(body.Config.Orbit.semiMajorAxis, body.Config.Orbit.staticPosition?.Length() ?? 0f); - } - foreach (NewHorizonsBody body in bodies.Where(b => b.Config.Base.centerOfSolarSystem)) { bodies.Sort((b, o) => b.Config.Orbit.semiMajorAxis.CompareTo(o.Config.Orbit.semiMajorAxis)); diff --git a/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs index 6b961a79..0c209154 100644 --- a/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs @@ -25,7 +25,8 @@ namespace NewHorizons.Builder.Volumes owAudioSource.SetTrack(info.track.ConvertToOW()); AudioUtilities.SetAudioClip(owAudioSource, info.audio, mod); - var audioVolume = go.AddComponent(); + var audioVolume = PriorityVolumeBuilder.MakeExisting(go, planetGO, sector, info); + audioVolume._layer = info.layer; audioVolume.SetPriority(info.priority); audioVolume._fadeSeconds = info.fadeSeconds; @@ -33,11 +34,7 @@ namespace NewHorizons.Builder.Volumes audioVolume._randomizePlayhead = info.randomizePlayhead; audioVolume._pauseOnFadeOut = info.pauseOnFadeOut; - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; + var owTriggerVolume = go.GetComponent(); audioVolume._triggerVolumeOverride = owTriggerVolume; go.SetActive(true); diff --git a/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs index 5557da98..54eddc3d 100644 --- a/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/ChangeStarSystemVolumeBuilder.cs @@ -13,6 +13,8 @@ namespace NewHorizons.Builder.Volumes volume.TargetSolarSystem = info.targetStarSystem; volume.TargetSpawnID = info.spawnPointID; + volume.gameObject.SetActive(true); + return volume; } } 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/CreditsVolumeBuilder.cs b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs index 81c3c6ce..cd55f9da 100644 --- a/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/CreditsVolumeBuilder.cs @@ -1,5 +1,6 @@ using NewHorizons.Components.Volumes; using NewHorizons.External.Modules.Volumes.VolumeInfos; +using OWML.Common; using OWML.Utils; using UnityEngine; @@ -7,12 +8,15 @@ namespace NewHorizons.Builder.Volumes { internal static class CreditsVolumeBuilder { - public static LoadCreditsVolume Make(GameObject planetGO, Sector sector, LoadCreditsVolumeInfo info) + public static LoadCreditsVolume Make(GameObject planetGO, Sector sector, LoadCreditsVolumeInfo info, IModBehaviour mod) { var volume = VolumeBuilder.Make(planetGO, sector, info); volume.gameOver = info.gameOver; volume.deathType = info.deathType == null ? null : EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); + volume.mod = mod; + + volume.gameObject.SetActive(true); return volume; } diff --git a/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs b/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs index b2ed1ce1..875bba75 100644 --- a/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/DayNightAudioVolumeBuilder.cs @@ -15,7 +15,8 @@ namespace NewHorizons.Builder.Volumes var go = GeneralPropBuilder.MakeNew("DayNightAudioVolume", planetGO, sector, info); go.layer = Layer.AdvancedEffectVolume; - var audioVolume = go.AddComponent(); + var audioVolume = PriorityVolumeBuilder.MakeExisting(go, planetGO, sector, info); + audioVolume.sunName = info.sun; audioVolume.dayWindow = info.dayWindow; audioVolume.dayAudio = info.dayAudio; @@ -24,13 +25,7 @@ namespace NewHorizons.Builder.Volumes audioVolume.volume = info.volume; audioVolume.SetTrack(info.track.ConvertToOW()); - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; - - go.SetActive(true); + audioVolume.gameObject.SetActive(true); return audioVolume; } diff --git a/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs b/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs index ad93ad7f..d633d27c 100644 --- a/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/DestructionVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes volume._deathType = EnumUtils.Parse(info.deathType.ToString(), DeathType.Default); + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs b/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs index e2839bd3..70b6d399 100644 --- a/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/FluidVolumeBuilder.cs @@ -35,6 +35,8 @@ namespace NewHorizons.Builder.Volumes volume._allowShipAutoroll = info.allowShipAutoroll; volume._disableOnStart = info.disableOnStart; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/ForceVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ForceVolumeBuilder.cs new file mode 100644 index 00000000..ebc5c360 --- /dev/null +++ b/NewHorizons/Builder/Volumes/ForceVolumeBuilder.cs @@ -0,0 +1,111 @@ +using NewHorizons.Builder.Props; +using NewHorizons.External; +using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using NewHorizons.Utility.OuterWilds; +using OWML.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Builder.Volumes +{ + public static class ForceVolumeBuilder + { + public static CylindricalForceVolume Make(GameObject planetGO, Sector sector, CylindricalForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._acceleration = info.force; + forceVolume._localAxis = info.normal ?? Vector3.up; + forceVolume._playGravityCrystalAudio = info.playGravityCrystalAudio; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static DirectionalForceVolume Make(GameObject planetGO, Sector sector, DirectionalForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._fieldDirection = info.normal ?? Vector3.up; + forceVolume._fieldMagnitude = info.force; + forceVolume._affectsAlignment = info.affectsAlignment; + forceVolume._offsetCentripetalForce = info.offsetCentripetalForce; + forceVolume._playGravityCrystalAudio = info.playGravityCrystalAudio; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static GravityVolume Make(GameObject planetGO, Sector sector, GravityVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._isPlanetGravityVolume = false; + forceVolume._setMass = false; + forceVolume._surfaceAcceleration = info.force; + forceVolume._upperSurfaceRadius = info.upperRadius; + forceVolume._lowerSurfaceRadius = info.lowerRadius; + forceVolume._cutoffAcceleration = info.minForce; + forceVolume._cutoffRadius = info.minRadius; + forceVolume._alignmentRadius = info.alignmentRadius ?? info.upperRadius * 1.5f; + forceVolume._falloffType = info.fallOff switch + { + GravityFallOff.Linear => GravityVolume.FalloffType.linear, + GravityFallOff.InverseSquared => GravityVolume.FalloffType.inverseSquared, + _ => throw new NotImplementedException(), + }; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static PolarForceVolume Make(GameObject planetGO, Sector sector, PolarForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._acceleration = info.force; + forceVolume._localAxis = info.normal ?? Vector3.up; + forceVolume._fieldMode = info.tangential ? PolarForceVolume.ForceMode.Tangential : PolarForceVolume.ForceMode.Polar; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static RadialForceVolume Make(GameObject planetGO, Sector sector, RadialForceVolumeInfo info) + { + var forceVolume = Make(planetGO, sector, info); + + forceVolume._acceleration = info.force; + forceVolume._falloff = info.fallOff switch + { + RadialForceVolumeInfo.FallOff.Constant => RadialForceVolume.Falloff.Constant, + RadialForceVolumeInfo.FallOff.Linear => RadialForceVolume.Falloff.Linear, + RadialForceVolumeInfo.FallOff.InverseSquared => RadialForceVolume.Falloff.InvSqr, + _ => throw new NotImplementedException(), + }; + + forceVolume.gameObject.SetActive(true); + + return forceVolume; + } + + public static TVolume Make(GameObject planetGO, Sector sector, ForceVolumeInfo info) where TVolume : ForceVolume + { + var forceVolume = PriorityVolumeBuilder.Make(planetGO, sector, info); + + forceVolume._alignmentPriority = info.alignmentPriority; + forceVolume._inheritable = info.inheritable; + + return forceVolume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs index f26f0c2e..1d5fe013 100644 --- a/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs @@ -3,6 +3,7 @@ using NewHorizons.External.Modules.Volumes.VolumeInfos; using NewHorizons.Utility.OuterWilds; using OWML.Common; using OWML.Utils; +using System; using System.Linq; using UnityEngine; @@ -13,35 +14,28 @@ namespace NewHorizons.Builder.Volumes public static HazardVolume Make(GameObject planetGO, Sector sector, OWRigidbody owrb, HazardVolumeInfo info, IModBehaviour mod) { var go = GeneralPropBuilder.MakeNew("HazardVolume", planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; - - var shape = go.AddComponent(); - shape.radius = info.radius; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; - - var volume = AddHazardVolume(go, sector, owrb, info.type, info.firstContactDamageType, info.firstContactDamage, info.damagePerSecond); + + var volume = MakeExisting(go, planetGO, sector, owrb, info); go.SetActive(true); return volume; } - public static HazardVolume AddHazardVolume(GameObject go, Sector sector, OWRigidbody owrb, HazardVolumeInfo.HazardType? type, HazardVolumeInfo.InstantDamageType? firstContactDamageType, float firstContactDamage, float damagePerSecond) + public static HazardVolume MakeExisting(GameObject go, GameObject planetGO, Sector sector, OWRigidbody owrb, HazardVolumeInfo info) { HazardVolume hazardVolume = null; - if (type == HazardVolumeInfo.HazardType.RIVERHEAT) + if (info.type == HazardVolumeInfo.HazardType.RIVERHEAT) { - hazardVolume = go.AddComponent(); + hazardVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); } - else if (type == HazardVolumeInfo.HazardType.HEAT) + else if (info.type == HazardVolumeInfo.HazardType.HEAT) { - hazardVolume = go.AddComponent(); + hazardVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); } - else if (type == HazardVolumeInfo.HazardType.DARKMATTER) + else if (info.type == HazardVolumeInfo.HazardType.DARKMATTER) { - hazardVolume = go.AddComponent(); + hazardVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); var visorFrostEffectVolume = go.AddComponent(); visorFrostEffectVolume._frostRate = 0.5f; visorFrostEffectVolume._maxFrost = 0.91f; @@ -67,28 +61,38 @@ namespace NewHorizons.Builder.Volumes submerge._fluidDetector = detector; } } - else if (type == HazardVolumeInfo.HazardType.ELECTRICITY) + else if (info.type == HazardVolumeInfo.HazardType.ELECTRICITY) { - var electricityVolume = go.AddComponent(); + var electricityVolume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); electricityVolume._shockAudioPool = new OWAudioSource[0]; hazardVolume = electricityVolume; } else { var simpleHazardVolume = go.AddComponent(); - simpleHazardVolume._type = EnumUtils.Parse(type.ToString(), HazardVolume.HazardType.GENERAL); + simpleHazardVolume._type = EnumUtils.Parse(info.type.ToString(), HazardVolume.HazardType.GENERAL); hazardVolume = simpleHazardVolume; } hazardVolume._attachedBody = owrb; - hazardVolume._damagePerSecond = type == null ? 0f : damagePerSecond; + hazardVolume._damagePerSecond = info.type == HazardVolumeInfo.HazardType.NONE ? 0f : info.damagePerSecond; - if (firstContactDamageType != null) - { - hazardVolume._firstContactDamageType = EnumUtils.Parse(firstContactDamageType.ToString(), InstantDamageType.Impact); - hazardVolume._firstContactDamage = firstContactDamage; - } + hazardVolume._firstContactDamageType = EnumUtils.Parse(info.firstContactDamageType.ToString(), InstantDamageType.Impact); + hazardVolume._firstContactDamage = info.firstContactDamage; return hazardVolume; } + + public static HazardVolume AddHazardVolume(GameObject go, Sector sector, OWRigidbody owrb, HazardVolumeInfo.HazardType? type, HazardVolumeInfo.InstantDamageType? firstContactDamageType, float firstContactDamage, float damagePerSecond) + { + var planetGO = sector.transform.root.gameObject; + return MakeExisting(go, planetGO, sector, owrb, new HazardVolumeInfo + { + radius = 0f, // Volume builder should skip creating an extra trigger volume and collider if radius is 0 + type = type ?? HazardVolumeInfo.HazardType.NONE, + firstContactDamageType = firstContactDamageType ?? HazardVolumeInfo.InstantDamageType.Impact, + firstContactDamage = firstContactDamage, + damagePerSecond = damagePerSecond + }); + } } } 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/NotificationVolumeBuilder.cs b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs index 18f51e69..bf4bbe3f 100644 --- a/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs @@ -11,21 +11,16 @@ namespace NewHorizons.Builder.Volumes { public static NHNotificationVolume Make(GameObject planetGO, Sector sector, NotificationVolumeInfo info, IModBehaviour mod) { - var go = GeneralPropBuilder.MakeNew("NotificationVolume", planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; + var notificationVolume = VolumeBuilder.Make(planetGO, sector, info); - var shape = go.AddComponent(); - shape.radius = info.radius; + // Preserving name for backwards compatibility + notificationVolume.gameObject.name = string.IsNullOrEmpty(info.rename) ? "NotificationVolume" : info.rename; - 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); + notificationVolume.gameObject.SetActive(true); return notificationVolume; } diff --git a/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs b/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs index 3fa34dfe..bcd1aa77 100644 --- a/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/OxygenVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes volume._treeVolume = info.treeVolume; volume._playRefillAudio = info.playRefillAudio; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs b/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs index f3cce567..fc76910f 100644 --- a/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/PriorityVolumeBuilder.cs @@ -5,6 +5,17 @@ namespace NewHorizons.Builder.Volumes { public static class PriorityVolumeBuilder { + public static TVolume MakeExisting(GameObject go, GameObject planetGO, Sector sector, PriorityVolumeInfo info) where TVolume : PriorityVolume + { + var volume = VolumeBuilder.MakeExisting(go, planetGO, sector, info); + + volume._layer = info.layer; + volume.SetPriority(info.priority); + + return volume; + } + + public static TVolume Make(GameObject planetGO, Sector sector, PriorityVolumeInfo info) where TVolume : PriorityVolume { var volume = VolumeBuilder.Make(planetGO, sector, info); diff --git a/NewHorizons/Builder/Volumes/RepairVolumeBuilder.cs b/NewHorizons/Builder/Volumes/RepairVolumeBuilder.cs new file mode 100644 index 00000000..7f51dc2c --- /dev/null +++ b/NewHorizons/Builder/Volumes/RepairVolumeBuilder.cs @@ -0,0 +1,52 @@ +using NewHorizons.Builder.Props; +using NewHorizons.Components.Volumes; +using NewHorizons.External.Modules.Props; +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Builder.Volumes +{ + public static class RepairVolumeBuilder + { + public static NHRepairReceiver Make(GameObject planetGO, Sector sector, RepairVolumeInfo info) + { + // Repair receivers aren't technically volumes (no OWTriggerVolume) so we don't use the VolumeBuilder + + var go = GeneralPropBuilder.MakeNew("RepairVolume", planetGO, sector, info); + + if (info.shape != null) + { + ShapeBuilder.AddCollider(go, info.shape); + } + else + { + var shapeInfo = new ShapeInfo() + { + type = ShapeType.Sphere, + useShape = false, + hasCollision = true, + radius = info.radius, + }; + ShapeBuilder.AddCollider(go, shapeInfo); + } + + var repairReceiver = go.AddComponent(); + repairReceiver.displayName = info.name ?? info.rename ?? go.name; + repairReceiver.repairFraction = info.repairFraction; + repairReceiver.repairTime = info.repairTime; + repairReceiver.repairDistance = info.repairDistance; + repairReceiver.damagedCondition = info.damagedCondition; + repairReceiver.repairedCondition = info.repairedCondition; + repairReceiver.revealFact = info.revealFact; + + go.SetActive(true); + + return repairReceiver; + } + } +} diff --git a/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs b/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs index f892ea07..988e9da8 100644 --- a/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs +++ b/NewHorizons/Builder/Volumes/Rulesets/PlayerImpactRulesetBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes.Rulesets volume.minImpactSpeed = info.minImpactSpeed; volume.maxImpactSpeed = info.maxImpactSpeed; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs b/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs index 98823ffa..b0768324 100644 --- a/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs +++ b/NewHorizons/Builder/Volumes/Rulesets/ProbeRulesetBuilder.cs @@ -15,6 +15,8 @@ namespace NewHorizons.Builder.Volumes.Rulesets volume._lanternRange = info.lanternRange; volume._ignoreAnchor = info.ignoreAnchor; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs b/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs index 8a49808d..5e158ecd 100644 --- a/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs +++ b/NewHorizons/Builder/Volumes/Rulesets/ThrustRulesetBuilder.cs @@ -13,6 +13,8 @@ namespace NewHorizons.Builder.Volumes.Rulesets volume._nerfJetpackBooster = info.nerfJetpackBooster; volume._nerfDuration = info.nerfDuration; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs b/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs new file mode 100644 index 00000000..0bd94b87 --- /dev/null +++ b/NewHorizons/Builder/Volumes/SpeedLimiterVolumeBuilder.cs @@ -0,0 +1,22 @@ +using NewHorizons.Components.Volumes; +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using UnityEngine; + +namespace NewHorizons.Builder.Volumes +{ + public static class SpeedLimiterVolumeBuilder + { + public static SpeedLimiterVolume Make(GameObject planetGO, Sector sector, SpeedLimiterVolumeInfo info) + { + var volume = VolumeBuilder.Make(planetGO, sector, info); + + volume.maxSpeed = info.maxSpeed; + volume.stoppingDistance = info.stoppingDistance; + volume.maxEntryAngle = info.maxEntryAngle; + + volume.gameObject.SetActive(true); + + return volume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs b/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs index fa1ed9bc..1b9319ed 100644 --- a/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/SpeedTrapVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes volume._speedLimit = info.speedLimit; volume._acceleration = info.acceleration; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs b/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs index 9bc2a514..cca70c4a 100644 --- a/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VanishVolumeBuilder.cs @@ -1,6 +1,5 @@ -using NewHorizons.Builder.Props; using NewHorizons.External.Modules.Volumes.VolumeInfos; -using NewHorizons.Utility.OuterWilds; +using NewHorizons.Utility.OWML; using UnityEngine; namespace NewHorizons.Builder.Volumes @@ -9,27 +8,23 @@ namespace NewHorizons.Builder.Volumes { public static TVolume Make(GameObject planetGO, Sector sector, VanishVolumeInfo info) where TVolume : VanishVolume { - var go = GeneralPropBuilder.MakeNew(typeof(TVolume).Name, planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; + if (info.shape != null && info.shape?.useShape == false) + { + NHLogger.LogError($"Destruction/VanishVolumes only support colliders. Affects planet [{planetGO.name}]. Set useShape to false."); + } - var collider = go.AddComponent(); - collider.isTrigger = true; - collider.radius = info.radius; + // VanishVolume is only compatible with sphere colliders + // If info.shape was null, it will still default to using a sphere with info.radius, just make sure it does so with a collider + info.shape ??= new(); + info.shape.useShape = false; - var owCollider = go.AddComponent(); - owCollider._collider = collider; - - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._owCollider = owCollider; - - var volume = go.AddComponent(); + var volume = VolumeBuilder.Make(planetGO, sector, info); + var collider = volume.gameObject.GetComponent(); volume._collider = collider; volume._shrinkBodies = info.shrinkBodies; volume._onlyAffectsPlayerAndShip = info.onlyAffectsPlayerRelatedBodies; - go.SetActive(true); - return volume; } } diff --git a/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs b/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs index 793d72b4..c33886e2 100644 --- a/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VisorEffects/VisorFrostEffectVolumeBuilder.cs @@ -12,6 +12,8 @@ namespace NewHorizons.Builder.Volumes.VisorEffects volume._frostRate = info.frostRate; volume._maxFrost = info.maxFrost; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs b/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs index e07c95e6..6f7c9c01 100644 --- a/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VisorEffects/VisorRainEffectVolumeBuilder.cs @@ -13,6 +13,8 @@ namespace NewHorizons.Builder.Volumes.VisorEffects volume._dropletRate = info.dropletRate; volume._streakRate = info.streakRate; + volume.gameObject.SetActive(true); + return volume; } } diff --git a/NewHorizons/Builder/Volumes/VolumeBuilder.cs b/NewHorizons/Builder/Volumes/VolumeBuilder.cs index 0ee5d0b7..d9d28607 100644 --- a/NewHorizons/Builder/Volumes/VolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/VolumeBuilder.cs @@ -1,27 +1,60 @@ using NewHorizons.Builder.Props; using NewHorizons.External.Modules.Volumes.VolumeInfos; using NewHorizons.Utility.OuterWilds; +using NewHorizons.Utility.OWML; using UnityEngine; namespace NewHorizons.Builder.Volumes { public static class VolumeBuilder { - public static TVolume Make(GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour //Could be BaseVolume but I need to create vanilla volumes too. + public static TVolume MakeExisting(GameObject go, GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour { - var go = GeneralPropBuilder.MakeNew(typeof(TVolume).Name, planetGO, sector, info); - go.layer = Layer.BasicEffectVolume; + // Backwards compat for the two possible radii settings + // Both radii default to 1 + if (info.shape != null && info.shape.type == External.Modules.Props.ShapeType.Sphere && info.shape.radius != info.radius) + { + // If the info shape radius if the default but the info radius is not the default, use it + if (info.shape.radius == 1f && info.radius != 1f) + { + info.shape.radius = info.radius; + } + } - var shape = go.AddComponent(); - shape.radius = info.radius; + // Warning if you set the radius to not be one but are using a non sphere shape + if (info.radius != 1f && (info.shape != null && info.shape.type != External.Modules.Props.ShapeType.Sphere)) + { + NHLogger.LogError($"Volume [{typeof(TVolume).Name}] on [{go.name}] has a radius value set but it's shape is [{info.shape.type}]"); + } - var owTriggerVolume = go.AddComponent(); - owTriggerVolume._shape = shape; + // Respect existing layer if set to a valid volume layer + if (go.layer != Layer.AdvancedEffectVolume) + { + go.layer = Layer.BasicEffectVolume; + } + + // Skip creating a trigger volume if one already exists and has a shape set and we aren't overriding it + var trigger = go.GetComponent(); + if (trigger == null || (trigger._shape == null && trigger._owCollider == null) || info.shape != null || info.radius > 0f) + { + ShapeBuilder.AddTriggerVolume(go, info.shape, info.radius); + } var volume = go.AddComponent(); + + return volume; + } - go.SetActive(true); + public static TVolume Make(GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour // Could be BaseVolume but I need to create vanilla volumes too. + { + var go = GeneralPropBuilder.MakeNew(typeof(TVolume).Name, planetGO, sector, info); + return MakeExisting(go, planetGO, sector, info); + } + public static TVolume MakeAndEnable(GameObject planetGO, Sector sector, VolumeInfo info) where TVolume : MonoBehaviour + { + var volume = Make(planetGO, sector, info); + volume.gameObject.SetActive(true); return volume; } } diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs index bdaa4a5a..3acb65cf 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) @@ -60,28 +67,35 @@ namespace NewHorizons.Builder.Volumes { foreach (var mapRestrictionVolume in config.Volumes.mapRestrictionVolumes) { - VolumeBuilder.Make(go, sector, mapRestrictionVolume); + 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) { - VolumeBuilder.Make(go, sector, interferenceVolume); + VolumeBuilder.MakeAndEnable(go, sector, interferenceVolume); } } if (config.Volumes.reverbVolumes != null) { foreach (var reverbVolume in config.Volumes.reverbVolumes) { - VolumeBuilder.Make(go, sector, reverbVolume); + VolumeBuilder.MakeAndEnable(go, sector, reverbVolume); } } if (config.Volumes.insulatingVolumes != null) { foreach (var insulatingVolume in config.Volumes.insulatingVolumes) { - VolumeBuilder.Make(go, sector, insulatingVolume); + VolumeBuilder.MakeAndEnable(go, sector, insulatingVolume); } } if (config.Volumes.zeroGravityVolumes != null) @@ -112,20 +126,58 @@ namespace NewHorizons.Builder.Volumes FluidVolumeBuilder.Make(go, sector, fluidVolume); } } + if (config.Volumes.forces != null) + { + if (config.Volumes.forces.cylindricalVolumes != null) + { + foreach (var cylindricalVolume in config.Volumes.forces.cylindricalVolumes) + { + ForceVolumeBuilder.Make(go, sector, cylindricalVolume); + } + } + if (config.Volumes.forces.directionalVolumes != null) + { + foreach (var directionalVolume in config.Volumes.forces.directionalVolumes) + { + ForceVolumeBuilder.Make(go, sector, directionalVolume); + } + } + if (config.Volumes.forces.gravityVolumes != null) + { + foreach (var gravityVolume in config.Volumes.forces.gravityVolumes) + { + ForceVolumeBuilder.Make(go, sector, gravityVolume); + } + } + if (config.Volumes.forces.polarVolumes != null) + { + foreach (var polarVolume in config.Volumes.forces.polarVolumes) + { + ForceVolumeBuilder.Make(go, sector, polarVolume); + } + } + if (config.Volumes.forces.radialVolumes != null) + { + foreach (var radialVolume in config.Volumes.forces.radialVolumes) + { + ForceVolumeBuilder.Make(go, sector, radialVolume); + } + } + } if (config.Volumes.probe != null) { if (config.Volumes.probe.destructionVolumes != null) { foreach (var destructionVolume in config.Volumes.probe.destructionVolumes) { - VolumeBuilder.Make(go, sector, destructionVolume); + VolumeBuilder.MakeAndEnable(go, sector, destructionVolume); } } if (config.Volumes.probe.safetyVolumes != null) { foreach (var safetyVolume in config.Volumes.probe.safetyVolumes) { - VolumeBuilder.Make(go, sector, safetyVolume); + VolumeBuilder.MakeAndEnable(go, sector, safetyVolume); } } } @@ -152,7 +204,7 @@ namespace NewHorizons.Builder.Volumes { foreach (var antiTravelMusicRuleset in config.Volumes.rulesets.antiTravelMusicRulesets) { - VolumeBuilder.Make(go, sector, antiTravelMusicRuleset); + VolumeBuilder.MakeAndEnable(go, sector, antiTravelMusicRuleset); } } if (config.Volumes.rulesets.playerImpactRulesets != null) @@ -181,7 +233,14 @@ namespace NewHorizons.Builder.Volumes { foreach (var referenceFrameBlockerVolume in config.Volumes.referenceFrameBlockerVolumes) { - VolumeBuilder.Make(go, sector, referenceFrameBlockerVolume); + VolumeBuilder.MakeAndEnable(go, sector, referenceFrameBlockerVolume); + } + } + if (config.Volumes.repairVolumes != null) + { + foreach (var repairVolume in config.Volumes.repairVolumes) + { + RepairVolumeBuilder.Make(go, sector, repairVolume); } } if (config.Volumes.speedTrapVolumes != null) @@ -191,11 +250,18 @@ namespace NewHorizons.Builder.Volumes SpeedTrapVolumeBuilder.Make(go, sector, speedTrapVolume); } } + if (config.Volumes.speedLimiterVolumes != null) + { + foreach (var speedLimiterVolume in config.Volumes.speedLimiterVolumes) + { + SpeedLimiterVolumeBuilder.Make(go, sector, speedLimiterVolume); + } + } if (config.Volumes.lightSourceVolumes != null) { foreach (var lightSourceVolume in config.Volumes.lightSourceVolumes) { - VolumeBuilder.Make(go, sector, lightSourceVolume); + VolumeBuilder.MakeAndEnable(go, sector, lightSourceVolume); } } if (config.Volumes.solarSystemVolume != null) @@ -209,7 +275,7 @@ namespace NewHorizons.Builder.Volumes { foreach (var creditsVolume in config.Volumes.creditsVolume) { - CreditsVolumeBuilder.Make(go, sector, creditsVolume); + CreditsVolumeBuilder.Make(go, sector, creditsVolume, mod); } } } diff --git a/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs b/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs index afdde0b8..70ecc7e1 100644 --- a/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/ZeroGVolumeBuilder.cs @@ -11,6 +11,8 @@ namespace NewHorizons.Builder.Volumes volume._inheritable = true; + volume.gameObject.SetActive(true); + return volume; } } 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/Components/NHGameOverManager.cs b/NewHorizons/Components/NHGameOverManager.cs index 93a7339a..37661562 100644 --- a/NewHorizons/Components/NHGameOverManager.cs +++ b/NewHorizons/Components/NHGameOverManager.cs @@ -1,7 +1,11 @@ using NewHorizons.External.Modules; using NewHorizons.External.SerializableEnums; using NewHorizons.Handlers; +using NewHorizons.Patches.CreditsScenePatches; +using NewHorizons.Utility.Files; using NewHorizons.Utility.OWML; +using OWML.Common; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; @@ -15,14 +19,14 @@ namespace NewHorizons.Components /// Mod unique id to game over module list /// Done as a dictionary so that Reload Configs can overwrite entries per mod /// - public static Dictionary gameOvers = new(); + public static Dictionary gameOvers = new(); public static NHGameOverManager Instance { get; private set; } private GameOverController _gameOverController; private PlayerCameraEffectController _playerCameraEffectController; - private GameOverModule[] _gameOvers; + private (IModBehaviour mod, GameOverModule gameOver)[] _gameOvers; private bool _gameOverSequenceStarted; @@ -36,25 +40,35 @@ namespace NewHorizons.Components _gameOverController = FindObjectOfType(); _playerCameraEffectController = FindObjectOfType(); - _gameOvers = gameOvers.SelectMany(x => x.Value).ToArray(); + var gameOverList = new List<(IModBehaviour, GameOverModule)>(); + foreach (var gameOverPair in gameOvers) + { + var mod = gameOverPair.Key; + foreach (var gameOver in gameOverPair.Value) + { + gameOverList.Add((mod, gameOver)); + } + } + _gameOvers = gameOverList.ToArray(); } public void TryHijackDeathSequence() { - var gameOver = _gameOvers.FirstOrDefault(x => !string.IsNullOrEmpty(x.condition) && DialogueConditionManager.SharedInstance.GetConditionState(x.condition)); - if (!_gameOverSequenceStarted && gameOver != null && !Locator.GetDeathManager()._finishedDLC) + var gameOver = _gameOvers.FirstOrDefault(x => !string.IsNullOrEmpty(x.gameOver.condition) + && DialogueConditionManager.SharedInstance.GetConditionState(x.gameOver.condition)); + if (!_gameOverSequenceStarted && gameOver != default && !Locator.GetDeathManager()._finishedDLC) { - StartGameOverSequence(gameOver, null); + StartGameOverSequence(gameOver.gameOver, null, gameOver.mod); } } - public void StartGameOverSequence(GameOverModule gameOver, DeathType? deathType) + public void StartGameOverSequence(GameOverModule gameOver, DeathType? deathType, IModBehaviour mod) { _gameOverSequenceStarted = true; - Delay.StartCoroutine(GameOver(gameOver, deathType)); + Delay.StartCoroutine(GameOver(gameOver, deathType, mod)); } - private IEnumerator GameOver(GameOverModule gameOver, DeathType? deathType) + private IEnumerator GameOver(GameOverModule gameOver, DeathType? deathType, IModBehaviour mod) { OWInput.ChangeInputMode(InputMode.None); ReticleController.Hide(); @@ -104,12 +118,12 @@ namespace NewHorizons.Components yield return new WaitUntil(ReadytoLoadCreditsScene); } - LoadCreditsScene(gameOver); + LoadCreditsScene(gameOver, mod); } private bool ReadytoLoadCreditsScene() => _gameOverController._fadedOutText && _gameOverController._textAnimator.IsComplete(); - private void LoadCreditsScene(GameOverModule gameOver) + private void LoadCreditsScene(GameOverModule gameOver, IModBehaviour mod) { NHLogger.LogVerbose($"Load credits {gameOver.creditsType}"); @@ -125,6 +139,9 @@ namespace NewHorizons.Components TimelineObliterationController.s_hasRealityEnded = true; LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); break; + case NHCreditsType.Custom: + LoadCustomCreditsScene(gameOver, mod); + break; default: // GameOverController disables post processing _gameOverController._flashbackCamera.postProcessing.enabled = true; @@ -134,5 +151,42 @@ namespace NewHorizons.Components break; } } + + private void LoadCustomCreditsScene(GameOverModule gameOver, IModBehaviour mod) + { + LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack); + + // Unfortunately we can't make this a private method, as EventArgs/EventHandler enforces the (sender, e) parameters, which prevents us from passing in gameOver and mod, which we need. + EventHandler onCreditsBuilt = null; // needs to be done so we can unsubscribe from within the lambda. + onCreditsBuilt = (sender, e) => + { + // Unsubscribe first, playing it safe in case it NREs + CreditsPatches.CreditsBuilt -= onCreditsBuilt; + + // Patch new music clip + var musicSource = Locator.FindObjectsOfType().Where(x => x.name == "AudioSource").Single(); // AudioSource that plays the credits music is literally called "AudioSource", luckily it's the only one called that. Lazy OW devs do be lazy. + if (!string.IsNullOrEmpty(gameOver.audio)) // string.Empty is default value for "audio" in GameOverModule, means no audio is specified. + { + AudioUtilities.SetAudioClip(musicSource, gameOver.audio, mod); // Load audio if specified + } + else + { + musicSource.AssignAudioLibraryClip(AudioType.PLACEHOLDER); // Otherwise default custom credits are silent - AudioType.PLACEHOLDER is silence (apparently) + } + + musicSource.loop = gameOver.audioLooping; + musicSource._maxSourceVolume = gameOver.audioVolume; + + // Override fade in + musicSource.Stop(); + musicSource.Play(); + + // Patch scroll duration + var creditsScroll = Locator.FindObjectOfType(); + creditsScroll._scrollDuration = gameOver.length; + }; + + CreditsPatches.CreditsBuilt += onCreditsBuilt; + } } } diff --git a/NewHorizons/Components/Volumes/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/Components/Volumes/LoadCreditsVolume.cs b/NewHorizons/Components/Volumes/LoadCreditsVolume.cs index 26f75831..4f2dbfeb 100644 --- a/NewHorizons/Components/Volumes/LoadCreditsVolume.cs +++ b/NewHorizons/Components/Volumes/LoadCreditsVolume.cs @@ -1,4 +1,5 @@ using NewHorizons.External.Modules; +using OWML.Common; using UnityEngine; @@ -8,12 +9,13 @@ namespace NewHorizons.Components.Volumes { public GameOverModule gameOver; public DeathType? deathType; + public IModBehaviour mod; public override void OnTriggerVolumeEntry(GameObject hitObj) { if (hitObj.CompareTag("PlayerDetector") && enabled && (string.IsNullOrEmpty(gameOver.condition) || DialogueConditionManager.SharedInstance.GetConditionState(gameOver.condition))) { - NHGameOverManager.Instance.StartGameOverSequence(gameOver, deathType); + NHGameOverManager.Instance.StartGameOverSequence(gameOver, deathType, mod); } } diff --git a/NewHorizons/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/Components/Volumes/NHRepairReceiver.cs b/NewHorizons/Components/Volumes/NHRepairReceiver.cs new file mode 100644 index 00000000..130d8486 --- /dev/null +++ b/NewHorizons/Components/Volumes/NHRepairReceiver.cs @@ -0,0 +1,103 @@ +using NewHorizons.Handlers; +using OWML.Utils; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Events; + +namespace NewHorizons.Components.Volumes +{ + // RepairReceiver isn't set up for proper subclassing but a subclass is necessary for the first-person manipulator to detect it. + public class NHRepairReceiver : RepairReceiver + { + public static Type RepairReceiverType = EnumUtils.Create("NewHorizons"); + + public class RepairEvent : UnityEvent { } + + public RepairEvent OnRepaired = new(); + public RepairEvent OnDamaged = new(); + + public string displayName; + public float repairTime; + public string damagedCondition; + public string repairedCondition; + public string revealFact; + + float _repairFraction = 0f; + UITextType _uiTextType = UITextType.None; + + public float repairFraction + { + get => _repairFraction; + set + { + var prevValue = _repairFraction; + _repairFraction = Mathf.Clamp01(value); + if (prevValue < 1f && _repairFraction >= 1f) + { + Repair(); + } + else if (prevValue >= 1f && _repairFraction < 1f) + { + Damage(); + } + } + } + + public new virtual bool IsRepairable() => IsDamaged(); + public new virtual bool IsDamaged() => _repairFraction < 1f; + public new virtual float GetRepairFraction() => _repairFraction; + + protected new void Awake() + { + base.Awake(); + _type = RepairReceiverType; + if (IsDamaged()) Damage(); + else Repair(); + } + + public new virtual void RepairTick() + { + if (!IsRepairable()) return; + repairFraction += Time.deltaTime / repairTime; + } + + public new virtual UITextType GetRepairableName() + { + if (_uiTextType != UITextType.None) return _uiTextType; + var value = TranslationHandler.GetTranslation(displayName, TranslationHandler.TextType.UI); + _uiTextType = (UITextType)TranslationHandler.AddUI(value, false); + return _uiTextType; + } + + void Damage() + { + if (!string.IsNullOrEmpty(damagedCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(damagedCondition, true); + } + if (!string.IsNullOrEmpty(repairedCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(repairedCondition, false); + } + OnDamaged.Invoke(this); + } + + void Repair() + { + if (!string.IsNullOrEmpty(damagedCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(damagedCondition, false); + } + if (!string.IsNullOrEmpty(repairedCondition)) + { + DialogueConditionManager.SharedInstance.SetConditionState(repairedCondition, true); + } + if (!string.IsNullOrEmpty(revealFact)) + { + Locator.GetShipLogManager().RevealFact(revealFact); + } + OnRepaired.Invoke(this); + } + } +} diff --git a/NewHorizons/Components/Volumes/SpeedLimiterVolume.cs b/NewHorizons/Components/Volumes/SpeedLimiterVolume.cs new file mode 100644 index 00000000..478589ff --- /dev/null +++ b/NewHorizons/Components/Volumes/SpeedLimiterVolume.cs @@ -0,0 +1,164 @@ +using System.Collections.Generic; +using System; +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + public class SpeedLimiterVolume : BaseVolume + { + public float maxSpeed = 10f; + public float stoppingDistance = 100f; + public float maxEntryAngle = 60f; + + private OWRigidbody _parentBody; + private List _trackedBodies = new List(); + private bool _playerJustExitedDream; + + public override void Awake() + { + _parentBody = GetComponentInParent(); + base.Awake(); + GlobalMessenger.AddListener("ExitDreamWorld", OnExitDreamWorld); + } + + public void Start() + { + enabled = false; + } + + public override void OnDestroy() + { + base.OnDestroy(); + GlobalMessenger.RemoveListener("ExitDreamWorld", OnExitDreamWorld); + } + + public void FixedUpdate() + { + foreach (var trackedBody in _trackedBodies) + { + bool slowed = false; + Vector3 velocity = trackedBody.body.GetVelocity() - _parentBody.GetVelocity(); + float magnitude = velocity.magnitude; + if (magnitude <= maxSpeed) + { + slowed = true; + } + else + { + bool needsSlowing = true; + float velocityReduction = trackedBody.deceleration * Time.deltaTime; + float requiredReduction = maxSpeed - magnitude; + if (requiredReduction > velocityReduction) + { + velocityReduction = requiredReduction; + slowed = true; + } + if (trackedBody.name == Detector.Name.Ship) + { + Autopilot component = Locator.GetShipTransform().GetComponent(); + if (component != null && component.IsFlyingToDestination()) + { + needsSlowing = false; + } + } + if (needsSlowing) + { + Vector3 velocityChange = velocityReduction * velocity.normalized; + trackedBody.body.AddVelocityChange(velocityChange); + if (trackedBody.name == Detector.Name.Ship && PlayerState.IsInsideShip()) + { + Locator.GetPlayerBody().AddVelocityChange(velocityChange); + } + } + } + if (slowed) + { + if (trackedBody.name == Detector.Name.Ship) + GlobalMessenger.FireEvent("ShipExitSpeedLimiter"); + _trackedBodies.Remove(trackedBody); + if (_trackedBodies.Count == 0) + enabled = false; + } + } + } + + private void OnExitDreamWorld() + { + _playerJustExitedDream = true; + } + + public override void OnTriggerVolumeEntry(GameObject hitObj) + { + DynamicForceDetector component = hitObj.GetComponent(); + if (component == null || !component.CompareNameMask(Detector.Name.Player | Detector.Name.Probe | Detector.Name.Ship)) return; + + if (component.GetName() == Detector.Name.Player && (PlayerState.IsInsideShip() || _playerJustExitedDream)) + { + _playerJustExitedDream = false; + } + else + { + OWRigidbody attachedOWRigidbody = component.GetAttachedOWRigidbody(); + Vector3 from = transform.position - attachedOWRigidbody.GetPosition(); + Vector3 to = attachedOWRigidbody.GetVelocity() - _parentBody.GetVelocity(); + float magnitude = to.magnitude; + if (magnitude > maxSpeed && Vector3.Angle(from, to) < maxEntryAngle) + { + float deceleration = (maxSpeed * maxSpeed - magnitude * magnitude) / (2f * stoppingDistance); + TrackedBody trackedBody = new TrackedBody(attachedOWRigidbody, component.GetName(), deceleration); + _trackedBodies.Add(trackedBody); + if (component.GetName() == Detector.Name.Ship) + GlobalMessenger.FireEvent("ShipEnterSpeedLimiter"); + enabled = true; + } + } + } + + public override void OnTriggerVolumeExit(GameObject hitObj) + { + DynamicForceDetector component = hitObj.GetComponent(); + if (component == null) return; + + OWRigidbody body = component.GetAttachedOWRigidbody(); + TrackedBody trackedBody = _trackedBodies.Find((TrackedBody i) => i.body == body); + if (trackedBody != null) + { + if (trackedBody.name == Detector.Name.Ship) + GlobalMessenger.FireEvent("ShipExitSpeedLimiter"); + + _trackedBodies.Remove(trackedBody); + + if (_trackedBodies.Count == 0) + enabled = false; + } + } + + [Serializable] + protected class TrackedBody + { + public OWRigidbody body; + + public Detector.Name name; + + public float deceleration; + + public TrackedBody(OWRigidbody body, Detector.Name name, float deceleration) + { + this.body = body; + this.name = name; + this.deceleration = deceleration; + } + + public override bool Equals(object obj) + { + if (obj is TrackedBody trackedBody) + { + return trackedBody.body == body && trackedBody.name == name; + } + return base.Equals(obj); + } + + public override int GetHashCode() => body.GetHashCode(); + } + } +} diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index 596ca54f..c4965f6a 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -669,14 +669,12 @@ namespace NewHorizons.External.Configs { if (!string.IsNullOrEmpty(volume.gameOverText)) { - if (volume.gameOver == null) - { - volume.gameOver = new(); - } + volume.gameOver ??= new(); volume.gameOver.text = volume.gameOverText; } if (volume.creditsType != null) { + volume.gameOver ??= new(); volume.gameOver.creditsType = (SerializableEnums.NHCreditsType)volume.creditsType; } } diff --git a/NewHorizons/External/Configs/StarSystemConfig.cs b/NewHorizons/External/Configs/StarSystemConfig.cs index d2cab099..a59105ef 100644 --- a/NewHorizons/External/Configs/StarSystemConfig.cs +++ b/NewHorizons/External/Configs/StarSystemConfig.cs @@ -304,12 +304,18 @@ namespace NewHorizons.External.Configs // True by default so if one is false go false canEnterViaWarpDrive = canEnterViaWarpDrive && otherConfig.canEnterViaWarpDrive; + canExitViaWarpDrive = canExitViaWarpDrive && otherConfig.canExitViaWarpDrive; destroyStockPlanets = destroyStockPlanets && otherConfig.destroyStockPlanets; enableTimeLoop = enableTimeLoop && otherConfig.enableTimeLoop; + + // False by default + returnToSolarSystemWhenTooFar = returnToSolarSystemWhenTooFar || otherConfig.returnToSolarSystemWhenTooFar; + loopDuration = loopDuration == 22f ? otherConfig.loopDuration : loopDuration; // If current one is null take the other factRequiredForWarp = string.IsNullOrEmpty(factRequiredForWarp) ? otherConfig.factRequiredForWarp : factRequiredForWarp; + factRequiredToExitViaWarpDrive = string.IsNullOrEmpty(factRequiredToExitViaWarpDrive) ? otherConfig.factRequiredToExitViaWarpDrive : factRequiredToExitViaWarpDrive; Skybox = Skybox == null ? otherConfig.Skybox : Skybox; // False by default so if one is true go true diff --git a/NewHorizons/External/Configs/TitleScreenConfig.cs b/NewHorizons/External/Configs/TitleScreenConfig.cs index e5a73046..b7c18a12 100644 --- a/NewHorizons/External/Configs/TitleScreenConfig.cs +++ b/NewHorizons/External/Configs/TitleScreenConfig.cs @@ -6,15 +6,24 @@ using Newtonsoft.Json; namespace NewHorizons.External.Configs { + /// + /// Allows you to configure the title screen with custom music, skyboxes, and loading props from asset bundles. + /// You can define a list of title screen configurations, with different persistent condition/ship log facts required to display them. + /// [JsonObject] public class TitleScreenConfig { /// - /// Create title screens + /// Create title screens. + /// The last title screen in the list with its display conditions (persistent condition and/or ship log) met will be displayed if this mod + /// is chosen to be shown on the main menu. /// public TitleScreenInfo[] titleScreens = new TitleScreenInfo[0]; } + /// + /// A single title screen configuration + /// [JsonObject] public class TitleScreenInfo { diff --git a/NewHorizons/External/Modules/GameOverModule.cs b/NewHorizons/External/Modules/GameOverModule.cs index 00d029bd..5d73f4a3 100644 --- a/NewHorizons/External/Modules/GameOverModule.cs +++ b/NewHorizons/External/Modules/GameOverModule.cs @@ -24,6 +24,31 @@ namespace NewHorizons.External.Modules /// public string condition; + /// + /// The audio to use for the credits music. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// Credits will be silent unless this attribute is specified. + /// Note: only applies when creditsType is set to "custom". + /// + public string audio; + + /// + /// The length of the fade in and out for the credits music. + /// Note: only applies when creditsType is set to "custom". + /// + [DefaultValue(1f)] public float audioVolume = 1f; + + /// + /// Determines if the credits music should loop. + /// Note: only applies when creditsType is set to "custom". + /// + [DefaultValue(false)] public bool audioLooping = false; + + /// + /// Duration of the credits scroll in seconds. + /// Note: only applies when creditsType is set to "custom". + /// + [DefaultValue(120f)] public float length = 120f; + /// /// The type of credits that will run after the game over message is shown /// diff --git a/NewHorizons/External/Modules/PropModule.cs b/NewHorizons/External/Modules/PropModule.cs index 4a162bd8..1f7806ef 100644 --- a/NewHorizons/External/Modules/PropModule.cs +++ b/NewHorizons/External/Modules/PropModule.cs @@ -58,6 +58,11 @@ namespace NewHorizons.External.Modules /// public RaftInfo[] rafts; + /// + /// Add raft docks to this planet (requires Echoes of the Eye DLC) + /// + public RaftDockInfo[] raftDocks; + /// /// Scatter props around this planet's surface /// 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/ProjectionInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionInfo.cs index e059c310..43424ec0 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/ProjectionInfo.cs @@ -82,7 +82,7 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye [DefaultValue("sevenSlides")] public SlideReelType reelModel = SlideReelType.SevenSlides; /// - /// Exclusive to the slide reel type. Condition/material of the reel. Antique is the Stranger, Pristine is the Dreamworld, Rusted is a burned reel. + /// Exclusive to the slide reel and standing vision torch type. Condition/material of the reel. Antique is the Stranger, Pristine is the Dreamworld, Rusted (exclusive to slide reels) is a burned reel. /// [DefaultValue("antique")] public SlideReelCondition reelCondition = SlideReelCondition.Antique; 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; } } diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftDockInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftDockInfo.cs new file mode 100644 index 00000000..37730991 --- /dev/null +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftDockInfo.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; +using System.ComponentModel; + +namespace NewHorizons.External.Modules.Props.EchoesOfTheEye +{ + [JsonObject] + public class RaftDockInfo : GeneralPropInfo + { + } +} diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftInfo.cs index d248679e..af35da12 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/RaftInfo.cs @@ -10,6 +10,16 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye /// Acceleration of the raft. Default acceleration is 5. /// [DefaultValue(5f)] public float acceleration = 5f; + + /// + /// Path to the dock this raft will start attached to. + /// + public string dockPath; + + /// + /// Uses the raft model from the dreamworld + /// + public bool pristine; } } diff --git a/NewHorizons/External/Modules/Props/Item/ItemInfo.cs b/NewHorizons/External/Modules/Props/Item/ItemInfo.cs index f837443a..4d5751ae 100644 --- a/NewHorizons/External/Modules/Props/Item/ItemInfo.cs +++ b/NewHorizons/External/Modules/Props/Item/ItemInfo.cs @@ -32,6 +32,10 @@ namespace NewHorizons.External.Modules.Props.Item /// [DefaultValue(0.5f)] public float colliderRadius = 0.5f; /// + /// Whether the added sphere collider will be a trigger (interactible but does not collide). Defaults to true. + /// + [DefaultValue(true)] public bool colliderIsTrigger = true; + /// /// Whether the item can be dropped. Defaults to true. /// [DefaultValue(true)] public bool droppable = true; diff --git a/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs b/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs index 7294fbb0..d2ff2336 100644 --- a/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs +++ b/NewHorizons/External/Modules/Props/Item/ItemSocketInfo.cs @@ -19,6 +19,15 @@ namespace NewHorizons.External.Modules.Props.Item /// [DefaultValue(2f)] public float interactRange = 2f; /// + /// Default collider radius when interacting with the socket + /// + [DefaultValue(0f)] + public float colliderRadius = 0f; + /// + /// Whether the added sphere collider will be a trigger (interactible but does not collide). Defaults to true. + /// + [DefaultValue(true)] public bool colliderIsTrigger = true; + /// /// Whether to use "Give Item" / "Take Item" prompts instead of "Insert Item" / "Remove Item". /// public bool useGiveTakePrompts; @@ -46,10 +55,5 @@ namespace NewHorizons.External.Modules.Props.Item /// A ship log fact to reveal when removing an item from this socket, or when the socket is empty. /// public string removalFact; - /// - /// Default collider radius when interacting with the socket - /// - [DefaultValue(0f)] - public float colliderRadius = 0f; } } diff --git a/NewHorizons/External/Modules/Props/ShapeInfo.cs b/NewHorizons/External/Modules/Props/ShapeInfo.cs new file mode 100644 index 00000000..e7a9ae20 --- /dev/null +++ b/NewHorizons/External/Modules/Props/ShapeInfo.cs @@ -0,0 +1,94 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Props +{ + [JsonObject] + public class ShapeInfo + { + /// + /// The type of shape or collider to add. Sphere, box, and capsule colliders are more performant and support collision. Defaults to sphere. + /// + public ShapeType type = ShapeType.Sphere; + + /// + /// The radius of the shape or collider. Defaults to 1 meter. Only used by spheres, capsules, cylinders, hemispheres, hemicapsules, and rings. + /// + public float radius = 1f; + + /// + /// The height of the shape or collider. Defaults to 1 meter. Only used by capsules, cylinders, cones, hemicapsules, and rings. + /// + public float height = 1f; + + /// + /// The axis that the shape or collider is aligned with. Defaults to the Y axis (up). The flat bottom of the shape will be pointing towards the negative axis. Only used by capsules, cones, hemispheres, and hemicapsules. + /// + public ColliderAxis direction = ColliderAxis.Y; + + /// + /// The inner radius of the shape. Defaults to 0 meters. Only used by cones and rings. + /// + public float innerRadius = 0f; + + /// + /// The outer radius of the shape. Defaults to 0.5 meters. Only used by cones and rings. + /// + public float outerRadius = 0.5f; + + /// + /// Whether the shape has an end cap. Defaults to true. Only used by hemispheres and hemicapsules. + /// + public bool cap = true; + + /// + /// The size of the shape or collider. Defaults to (1,1,1). Only used by boxes. + /// + public MVector3 size; + + /// + /// The offset of the shape or collider from the object's origin. Defaults to (0,0,0). Supported by all collider and shape types. + /// + public MVector3 offset; + + /// + /// Whether the collider should have collision enabled. If false, the collider will be a trigger. Defaults to false. Only supported for spheres, boxes, and capsules. + /// + public bool hasCollision = false; + + /// + /// Setting this to false will force it to use a collider, and setting to true will force it to use a shape. + /// Shapes do not support collision and are less performant, but support a wider set of shapes and are required by some components. + /// If left empty it will defaults to using a shape, unless hasCollision is true in which case it defaults to using a collider. + /// + public bool? useShape; + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ShapeType + { + [EnumMember(Value = @"sphere")] Sphere, + [EnumMember(Value = @"box")] Box, + [EnumMember(Value = @"capsule")] Capsule, + [EnumMember(Value = @"cylinder")] Cylinder, + [EnumMember(Value = @"cone")] Cone, + [EnumMember(Value = @"hemisphere")] Hemisphere, + [EnumMember(Value = @"hemicapsule")] Hemicapsule, + [EnumMember(Value = @"ring")] Ring, + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum ColliderAxis + { + [EnumMember(Value = @"x")] X = 0, + [EnumMember(Value = @"y")] Y = 1, + [EnumMember(Value = @"z")] Z = 2, + } +} diff --git a/NewHorizons/External/Modules/Volumes/ForceModule.cs b/NewHorizons/External/Modules/Volumes/ForceModule.cs new file mode 100644 index 00000000..8250d3e1 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/ForceModule.cs @@ -0,0 +1,40 @@ +using NewHorizons.External.Modules.Volumes.VolumeInfos; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes +{ + [JsonObject] + public class ForceModule + { + /// + /// Applies a constant force along the volume's XZ plane towards the volume's center. Affects alignment. + /// + public CylindricalForceVolumeInfo[] cylindricalVolumes; + + /// + /// Applies a constant force in the direction of the volume's Y axis. May affect alignment. + /// + public DirectionalForceVolumeInfo[] directionalVolumes; + + /// + /// Applies planet-like gravity towards the volume's center with falloff by distance. May affect alignment. + /// For actual planetary body gravity, use the properties in the Base module. + /// + public GravityVolumeInfo[] gravityVolumes; + + /// + /// Applies a constant force towards the volume's center. Affects alignment. + /// + public PolarForceVolumeInfo[] polarVolumes; + + /// + /// Applies a force towards the volume's center with falloff by distance. Affects alignment. + /// + public RadialForceVolumeInfo[] radialVolumes; + } +} 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/VolumeInfos/CylindricalForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/CylindricalForceVolumeInfo.cs new file mode 100644 index 00000000..5926056b --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/CylindricalForceVolumeInfo.cs @@ -0,0 +1,24 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class CylindricalForceVolumeInfo : ForceVolumeInfo + { + /// + /// The direction that the force applied by this volume will be perpendicular to. Defaults to up (0, 1, 0). + /// + public MVector3 normal; + + /// + /// Whether to play the gravity crystal audio when the player is in this volume. + /// + public bool playGravityCrystalAudio; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/DirectionalForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/DirectionalForceVolumeInfo.cs new file mode 100644 index 00000000..82af81d2 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/DirectionalForceVolumeInfo.cs @@ -0,0 +1,35 @@ +using NewHorizons.External.SerializableData; +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 DirectionalForceVolumeInfo : ForceVolumeInfo + { + /// + /// The direction of the force applied by this volume. Defaults to up (0, 1, 0). + /// + public MVector3 normal; + + /// + /// Whether this force volume affects alignment. Defaults to true. + /// + [DefaultValue(true)] public bool affectsAlignment = true; + + /// + /// Whether the force applied by this volume takes the centripetal force of the volume's parent body into account. Defaults to false. + /// + public bool offsetCentripetalForce; + + /// + /// Whether to play the gravity crystal audio when the player is in this volume. + /// + public bool playGravityCrystalAudio; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/ForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/ForceVolumeInfo.cs new file mode 100644 index 00000000..e56a3ca5 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/ForceVolumeInfo.cs @@ -0,0 +1,35 @@ +using NewHorizons.External.Modules.Props; +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 ForceVolumeInfo : PriorityVolumeInfo + { + /// + /// The force applied by this volume. Can be negative to reverse the direction. + /// + public float force; + + /// + /// The priority of this force volume for the purposes of alignment. + /// + /// Volumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal. + /// Ex: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity. + /// + /// Default value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. + /// + [DefaultValue(1)] public int alignmentPriority = 1; + + /// + /// Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ. + /// + public bool inheritable; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/GravityVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/GravityVolumeInfo.cs new file mode 100644 index 00000000..44b226fc --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/GravityVolumeInfo.cs @@ -0,0 +1,44 @@ +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 GravityVolumeInfo : ForceVolumeInfo + { + /// + /// The upper bounds of the volume's "surface". Above this radius, the force applied by this volume will have falloff applied. + /// + public float upperRadius; + + /// + /// The lower bounds of the volume's "surface". Above this radius and below the `upperRadius`, the force applied by this volume will be constant. Defaults to 0. + /// + [DefaultValue(0f)] public float lowerRadius; + + /// + /// The volume's force will decrease linearly from `force` to `minForce` as distance decreases from `lowerRadius` to `minRadius`. Defaults to 0. + /// + [DefaultValue(0f)] public float minRadius; + + /// + /// The minimum force applied by this volume between `lowerRadius` and `minRadius`. Defaults to 0. + /// + [DefaultValue(0f)] public float minForce; + + /// + /// How the force falls off with distance. Most planets use linear but the sun and some moons use inverseSquared. + /// + [DefaultValue("linear")] public GravityFallOff fallOff = GravityFallOff.Linear; + + /// + /// The radius where objects will be aligned to the volume's force. Defaults to 1.5x the `upperRadius`. Set to 0 to disable alignment. + /// + public float? alignmentRadius; + } +} 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/VolumeInfos/PolarForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/PolarForceVolumeInfo.cs new file mode 100644 index 00000000..bdf1ee0d --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/PolarForceVolumeInfo.cs @@ -0,0 +1,23 @@ +using NewHorizons.External.SerializableData; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class PolarForceVolumeInfo : ForceVolumeInfo + { + /// + /// Tangential mode only. The force applied by this volume will be perpendicular to this direction and the direction to the other body. Defaults to up (0, 1, 0). + /// + public MVector3 normal; + /// + /// Enables tangential mode. The force applied by this volume will be perpendicular to the normal and the direction to the other body. Defaults to false. + /// + public bool tangential; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/RadialForceVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/RadialForceVolumeInfo.cs new file mode 100644 index 00000000..eefe58e4 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/RadialForceVolumeInfo.cs @@ -0,0 +1,32 @@ +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.Volumes.VolumeInfos +{ + [JsonObject] + public class RadialForceVolumeInfo : ForceVolumeInfo + { + /// + /// How the force falls off with distance. Defaults to linear. + /// + [DefaultValue("linear")] public FallOff fallOff = FallOff.Linear; + + [JsonConverter(typeof(StringEnumConverter))] + public enum FallOff + { + [EnumMember(Value = @"constant")] Constant = 0, + + [EnumMember(Value = @"linear")] Linear = 1, + + [EnumMember(Value = @"inverseSquared")] + InverseSquared = 2 + } + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/RepairVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/RepairVolumeInfo.cs new file mode 100644 index 00000000..550e2a3d --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/RepairVolumeInfo.cs @@ -0,0 +1,49 @@ +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 RepairVolumeInfo : VolumeInfo + { + /// + /// The name displayed in the UI when the player is repairing this object. If not set, the name of the object will be used. + /// + public string name; + + /// + /// How much of the object is initially repaired. 0 = not repaired, 1 = fully repaired. + /// + [DefaultValue(0f)] public float repairFraction = 0f; + + /// + /// The time it takes to repair the object. Defaults to 3 seconds. + /// + [DefaultValue(3f)] public float repairTime = 3f; + + /// + /// The distance from the object that the player can be to repair it. Defaults to 3 meters. + /// + [DefaultValue(3f)] public float repairDistance = 3f; + + /// + /// A dialogue condition that will be set while the object is damaged. It will be unset when the object is repaired. + /// + public string damagedCondition; + + /// + /// A dialogue condition that will be set when the object is repaired. It will be unset if the object is damaged again. + /// + public string repairedCondition; + + /// + /// A ship log fact that will be revealed when the object is repaired. + /// + public string revealFact; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs index 2f577918..49a917c7 100644 --- a/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/RevealVolumeInfo.cs @@ -29,7 +29,7 @@ namespace NewHorizons.External.Modules.Volumes.VolumeInfos } /// - /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only) + /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only). This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation. /// [DefaultValue(180f)] public float maxAngle = 180f; // Observe Only diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/SpeedLimiterVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/SpeedLimiterVolumeInfo.cs new file mode 100644 index 00000000..ef341d57 --- /dev/null +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/SpeedLimiterVolumeInfo.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; +using System.ComponentModel; +using UnityEngine; + +namespace NewHorizons.External.Modules.Volumes.VolumeInfos +{ + [JsonObject] + public class SpeedLimiterVolumeInfo : VolumeInfo + { + /// + /// The speed the volume will slow you down to when you enter it. + /// + [DefaultValue(10f)] + public float maxSpeed = 10f; + + /// + /// The distance from the outside of the volume that the limiter slows you down to max speed at. + /// + [DefaultValue(100f)] + public float stoppingDistance = 100f; + + /// + /// The maximum angle (in degrees) between the direction the incoming object is moving relative to the volume's center and the line from the object toward the center of the volume, within which the speed limiter will activate. + /// + [DefaultValue(60f)] + public float maxEntryAngle = 60f; + } +} diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/VanishVolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/VanishVolumeInfo.cs index bb66b0a8..95c85078 100644 --- a/NewHorizons/External/Modules/Volumes/VolumeInfos/VanishVolumeInfo.cs +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/VanishVolumeInfo.cs @@ -4,6 +4,9 @@ using System.ComponentModel; namespace NewHorizons.External.Modules.Volumes.VolumeInfos { + /// + /// Note: Only colliders work on vanish volumes! + /// [JsonObject] public class VanishVolumeInfo : VolumeInfo { diff --git a/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs b/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs index 7b106614..94f7a41b 100644 --- a/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs +++ b/NewHorizons/External/Modules/Volumes/VolumeInfos/VolumeInfo.cs @@ -1,14 +1,20 @@ +using NewHorizons.External.Modules.Props; using Newtonsoft.Json; using System.ComponentModel; namespace NewHorizons.External.Modules.Volumes.VolumeInfos { [JsonObject] - public class VolumeInfo : GeneralPointPropInfo + public class VolumeInfo : GeneralPropInfo { /// - /// The radius of this volume. + /// The radius of this volume, if a shape is not specified. /// [DefaultValue(1f)] public float radius = 1f; + + /// + /// The shape of this volume. Defaults to a sphere with a radius of `radius` if not specified. + /// + public ShapeInfo shape; } } diff --git a/NewHorizons/External/Modules/Volumes/VolumesModule.cs b/NewHorizons/External/Modules/Volumes/VolumesModule.cs index 6e3afe19..0320df0b 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. /// @@ -27,12 +32,23 @@ namespace NewHorizons.External.Modules.Volumes /// public FluidVolumeInfo[] fluidVolumes; + /// + /// Add force volumes to this planet. + /// + public ForceModule forces; + /// /// Add hazard volumes to this planet. /// Causes damage to player when inside this volume. /// 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. @@ -80,6 +96,11 @@ namespace NewHorizons.External.Modules.Volumes /// public VolumeInfo[] referenceFrameBlockerVolumes; + /// + /// Add repair volumes to this planet. + /// + public RepairVolumeInfo[] repairVolumes; + /// /// Add triggers that reveal parts of the ship log on this planet. /// @@ -101,6 +122,13 @@ namespace NewHorizons.External.Modules.Volumes /// public SpeedTrapVolumeInfo[] speedTrapVolumes; + /// + /// Add speed limiter volumes to this planet. + /// Slows down the player, ship, and probe when they enter this volume. + /// Used on the Stranger in DLC. + /// + public SpeedLimiterVolumeInfo[] speedLimiterVolumes; + /// /// Add visor effect volumes to this planet. /// diff --git a/NewHorizons/External/NewHorizonsData.cs b/NewHorizons/External/NewHorizonsData.cs index cedb624a..199ec3ec 100644 --- a/NewHorizons/External/NewHorizonsData.cs +++ b/NewHorizons/External/NewHorizonsData.cs @@ -191,6 +191,7 @@ namespace NewHorizons.External if (_activeProfile != null && !_activeProfile.PopupsRead.Contains(id)) { _activeProfile.PopupsRead.Add(id); + Save(); } } diff --git a/NewHorizons/External/SerializableEnums/NHCreditsType.cs b/NewHorizons/External/SerializableEnums/NHCreditsType.cs index 83c1dc51..f4d0f6c4 100644 --- a/NewHorizons/External/SerializableEnums/NHCreditsType.cs +++ b/NewHorizons/External/SerializableEnums/NHCreditsType.cs @@ -13,6 +13,8 @@ namespace NewHorizons.External.SerializableEnums [EnumMember(Value = @"kazoo")] Kazoo = 2, - [EnumMember(Value = @"none")] None = 3 + [EnumMember(Value = @"custom")] Custom = 3, + + [EnumMember(Value = @"none")] None = 4 } } diff --git a/NewHorizons/Handlers/EyeSceneHandler.cs b/NewHorizons/Handlers/EyeSceneHandler.cs index 233086e6..e0e3bccc 100644 --- a/NewHorizons/Handlers/EyeSceneHandler.cs +++ b/NewHorizons/Handlers/EyeSceneHandler.cs @@ -96,7 +96,7 @@ namespace NewHorizons.Handlers vesselAO._type = AstroObject.Type.SpaceStation; vesselAO.Register(); vesselMapMarker._markerType = MapMarker.MarkerType.Moon; - vesselMapMarker._labelID = (UITextType)TranslationHandler.AddUI("Vessel"); + vesselMapMarker._labelID = (UITextType)TranslationHandler.AddUI("Vessel", true); RFVolumeBuilder.Make(vessel, vesselBody, 600, new External.Modules.ReferenceFrameModule { localPosition = new MVector3(0, 0, -207.375f) }); // Resize vessel sector so that the vessel is fully collidable. diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 858131e9..e1c4864e 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -5,6 +5,7 @@ using NewHorizons.Builder.Orbital; using NewHorizons.Builder.Props; using NewHorizons.Builder.ShipLog; using NewHorizons.Builder.Volumes; +using NewHorizons.Components; using NewHorizons.Components.Orbital; using NewHorizons.Components.Quantum; using NewHorizons.Components.Stars; @@ -20,10 +21,6 @@ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; -using NewHorizons.Streaming; -using Newtonsoft.Json; -using NewHorizons.External.Modules.VariableSize; -using NewHorizons.Components; namespace NewHorizons.Handlers { @@ -45,6 +42,16 @@ 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) + { + // save memory NOW instead of next frame when other stuff has loaded and taken memory + bundle.UnloadImmediate(); + } + } + // Start by destroying all planets if need be if (Main.SystemDict[Main.Instance.CurrentStarSystem].Config.destroyStockPlanets) { @@ -657,7 +664,7 @@ namespace NewHorizons.Handlers if (body.Config.CometTail != null) { - CometTailBuilder.Make(go, sector, body.Config.CometTail, body.Config); + CometTailBuilder.Make(go, sector, body.Config.CometTail, body.Config, go.GetComponent()); } if (body.Config.Lava != null) diff --git a/NewHorizons/Handlers/TranslationHandler.cs b/NewHorizons/Handlers/TranslationHandler.cs index 43f787a0..753e13d6 100644 --- a/NewHorizons/Handlers/TranslationHandler.cs +++ b/NewHorizons/Handlers/TranslationHandler.cs @@ -205,11 +205,14 @@ namespace NewHorizons.Handlers TextTranslation.Get().m_table.theShipLogTable[key] = value; } - public static int AddUI(string rawText) + public static int AddUI(string rawText) => AddUI(rawText, false); + + public static int AddUI(string rawText, bool upper) { var uiTable = TextTranslation.Get().m_table.theUITable; - var text = GetTranslation(rawText, TextType.UI).ToUpperFixed(); + var text = GetTranslation(rawText, TextType.UI); + if (upper) text = text.ToUpperFixed(); var key = uiTable.Keys.Max() + 1; try diff --git a/NewHorizons/Handlers/VesselWarpHandler.cs b/NewHorizons/Handlers/VesselWarpHandler.cs index c5e622eb..87f8d1ad 100644 --- a/NewHorizons/Handlers/VesselWarpHandler.cs +++ b/NewHorizons/Handlers/VesselWarpHandler.cs @@ -210,7 +210,7 @@ namespace NewHorizons.Handlers vesselWarpController._whiteHoleOneShot = vesselWarpController._whiteHole.transform.parent.Find("WhiteHoleAudio_OneShot").GetComponent(); vesselWarpController._whiteHole._startActive = true; - vesselObject.GetComponent()._labelID = (UITextType)TranslationHandler.AddUI("Vessel"); + vesselObject.GetComponent()._labelID = (UITextType)TranslationHandler.AddUI("Vessel", true); var hasParentBody = !string.IsNullOrEmpty(system.Config.Vessel?.vesselSpawn?.parentBody); var hasPhysics = system.Config.Vessel?.hasPhysics ?? !hasParentBody; diff --git a/NewHorizons/INewHorizons.cs b/NewHorizons/INewHorizons.cs index 07512ad6..deecd0cf 100644 --- a/NewHorizons/INewHorizons.cs +++ b/NewHorizons/INewHorizons.cs @@ -255,5 +255,12 @@ namespace NewHorizons /// Persistent condition required for this title screen to appear. /// Ship log fact required for this title screen to appear. void RegisterTitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets = true, bool shareTitleScreen = false, string persistentConditionRequired = null, string factRequired = null); + + /// + /// Clears all loaded configs for the given system. + /// This exists solely for Nomai Sky to use :bleh: + /// + /// + void ClearSystem(string name); } } diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 01e1f089..260db13a 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -46,6 +46,7 @@ namespace NewHorizons // Settings public static bool Debug { get; private set; } public static bool VisualizeQuantumObjects { get; private set; } + public static bool VisualizeBrambleVolumeNames { get; private set; } public static bool VerboseLogs { get; private set; } public static bool SequentialPreCaching { get; private set; } public static bool CustomTitleScreen { get; private set; } @@ -138,6 +139,7 @@ namespace NewHorizons Debug = config.GetSettingsValue(nameof(Debug)); VisualizeQuantumObjects = config.GetSettingsValue(nameof(VisualizeQuantumObjects)); + VisualizeBrambleVolumeNames = config.GetSettingsValue(nameof(VisualizeBrambleVolumeNames)); VerboseLogs = config.GetSettingsValue(nameof(VerboseLogs)); SequentialPreCaching = config.GetSettingsValue(nameof(SequentialPreCaching)); @@ -380,6 +382,7 @@ namespace NewHorizons ProjectionBuilder.InitPrefabs(); CloakBuilder.InitPrefab(); RaftBuilder.InitPrefab(); + RaftDockBuilder.InitPrefab(); DreamCampfireBuilder.InitPrefab(); DreamArrivalPointBuilder.InitPrefab(); } @@ -436,6 +439,11 @@ namespace NewHorizons TitleSceneHandler.Init(); } + if (isTitleScreen) + { + MenuHandler.TitleScreen(); + } + // EOTU fixes if (isEyeOfTheUniverse) { @@ -847,7 +855,7 @@ namespace NewHorizons } if (addonConfig.gameOver != null) { - NHGameOverManager.gameOvers[mod.ModHelper.Manifest.UniqueName] = addonConfig.gameOver; + NHGameOverManager.gameOvers[mod] = addonConfig.gameOver; } AddonConfigs[mod] = addonConfig; diff --git a/NewHorizons/NewHorizons.csproj.user b/NewHorizons/NewHorizons.csproj.user index 5e39a9dd..44729020 100644 --- a/NewHorizons/NewHorizons.csproj.user +++ b/NewHorizons/NewHorizons.csproj.user @@ -1,4 +1,4 @@ - + $(AppData)\OuterWildsModManager\OWML\Mods\xen.NewHorizons diff --git a/NewHorizons/NewHorizonsApi.cs b/NewHorizons/NewHorizonsApi.cs index 14a4fdcf..e62d0bdb 100644 --- a/NewHorizons/NewHorizonsApi.cs +++ b/NewHorizons/NewHorizonsApi.cs @@ -367,5 +367,17 @@ namespace NewHorizons public void RegisterTitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets = true, bool shareTitleScreen = false, string persistentConditionRequired = null, string factRequired = null) => TitleSceneHandler.RegisterBuilder(mod, builder, disableNHPlanets, shareTitleScreen, persistentConditionRequired, factRequired); + + public void ClearSystem(string name) + { + if (Main.SystemDict.ContainsKey(name)) + { + Main.SystemDict.Remove(name); + } + if (Main.BodyDict.ContainsKey(name)) + { + Main.BodyDict.Remove(name); + } + } } } diff --git a/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs b/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs index 1a54fc46..4a3228ff 100644 --- a/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs +++ b/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs @@ -3,9 +3,11 @@ using NewHorizons.Handlers; using NewHorizons.Utility; using NewHorizons.Utility.OWML; using OWML.Common; +using OWML.ModHelper; using System.Collections.Generic; using System.Linq; using UnityEngine; +using static TextTranslation; namespace NewHorizons.OtherMods.MenuFramework { @@ -61,5 +63,18 @@ namespace NewHorizons.OtherMods.MenuFramework public static void RegisterFailedConfig(string filename) => _failedFiles.Add(filename); public static void RegisterOneTimePopup(IModBehaviour mod, string message, bool repeat) => _registeredPopups.Add((mod, message, repeat)); + + public static void TitleScreen() + { + // Custom popup for recommending the Chinese Outer Wilds Font Fix mod if they are playing in chinese + // Only shows once per profile + if (TextTranslation.Get().m_language == Language.CHINESE_SIMPLE + && !Main.Instance.ModHelper.Interaction.ModExists("nice2cu1.OuterWildFixFont") + && !NewHorizonsData.HasReadOneTimePopup("INSTALL_OUTER_WILDS_CHINESE_FONT_FIX")) + { + Main.Instance.ModHelper.MenuHelper.PopupMenuManager.RegisterStartupPopup(TranslationHandler.GetTranslation("INSTALL_OUTER_WILDS_CHINESE_FONT_FIX", TranslationHandler.TextType.UI)); + NewHorizonsData.ReadOneTimePopup("INSTALL_OUTER_WILDS_CHINESE_FONT_FIX"); + } + } } } diff --git a/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs b/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs index 8892f334..037b30bf 100644 --- a/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs +++ b/NewHorizons/Patches/CreditsScenePatches/CreditsPatches.cs @@ -1,17 +1,30 @@ using HarmonyLib; using NewHorizons.Handlers; +using System; namespace NewHorizons.Patches.CreditsScenePatches { [HarmonyPatch(typeof(Credits))] public static class CreditsPatches { + public static event EventHandler CreditsBuilt; // Used in NHGameOverManager to patch credits music and scroll speed + [HarmonyPrefix] [HarmonyPatch(nameof(Credits.Start))] public static void Credits_Start(Credits __instance) { CreditsHandler.AddCredits(__instance); } + + [HarmonyPostfix] + [HarmonyPatch(nameof(Credits.BuildCredits))] + public static void Credits_BuildCredits_Post(Credits __instance) + { + // Do things BuildCredits() normally does + + // Fire event once finished + CreditsBuilt?.Invoke(__instance, new EventArgs()); + } } } diff --git a/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs b/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs index 48a08f08..f437e4a8 100644 --- a/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs +++ b/NewHorizons/Patches/EchoesOfTheEyePatches/RaftControllerPatches.cs @@ -75,30 +75,41 @@ namespace NewHorizons.Patches.EchoesOfTheEyePatches return false; } - [HarmonyPostfix] + [HarmonyPrefix] [HarmonyPatch(nameof(RaftController.UpdateMoveToTarget))] - public static void UpdateMoveToTarget(RaftController __instance) + public static bool UpdateMoveToTarget(RaftController __instance) { + OWRigidbody raftBody = __instance._raftBody; + OWRigidbody origParentBody = raftBody.GetOrigParentBody(); + Transform transform = origParentBody.transform; + Vector3 position = transform.TransformPoint(__instance._targetLocalPosition); + Vector3 distance = position - raftBody.GetPosition(); + float speed = Mathf.Min(__instance._targetSpeed, distance.magnitude / Time.deltaTime); + Vector3 pointVelocity = raftBody.GetOrigParentBody().GetPointVelocity(raftBody.GetPosition()); + raftBody.SetVelocity(pointVelocity + (distance.normalized * speed)); + float t = (__instance.currentDistanceLerp = Mathf.InverseLerp(__instance._startDistance, 0.001f, distance.magnitude)); + var st = Mathf.SmoothStep(0f, 1f, t); // If it has a riverFluid volume then its a regular stranger one - if (__instance._movingToTarget && __instance._riverFluid == null) + var isStranger = __instance._riverFluid != null; + // Base game threshold has this at 1f (after doing smoothstep on it) + // For whatever reason it never hits that for NH planets (probably since they're moving so much compared to the steady velocity of the Stranger) + // Might break for somebody with a wacky spinning planet in which case we can adjust this or add some kind of fallback (i.e., wait x seconds and then just say its there) + // Fixes #1005 + if (isStranger ? (st >= 1f) : (t > 0.999f)) { - OWRigidbody raftBody = __instance._raftBody; - OWRigidbody origParentBody = __instance._raftBody.GetOrigParentBody(); - Transform transform = origParentBody.transform; - Vector3 vector = transform.TransformPoint(__instance._targetLocalPosition); - - // Base game threshold has this at 1f (after doing smoothstep on it) - // For whatever reason it never hits that for NH planets (probably since they're moving so much compared to the steady velocity of the Stranger) - // Might break for somebody with a wacky spinning planet in which case we can adjust this or add some kind of fallback (i.e., wait x seconds and then just say its there) - // Fixes #1005 - if (__instance.currentDistanceLerp > 0.999f) - { - raftBody.SetPosition(vector); - raftBody.SetRotation(transform.rotation * __instance._targetLocalRotation); - __instance.StopMovingToTarget(); - __instance.OnArriveAtTarget.Invoke(); - } + raftBody.SetPosition(position); + raftBody.SetRotation(transform.rotation * __instance._targetLocalRotation); + __instance.StopMovingToTarget(); + __instance.OnArriveAtTarget.Invoke(); } + else + { + Quaternion quaternion = Quaternion.Slerp(__instance._startLocalRotation, __instance._targetLocalRotation, st); + Quaternion toRotation = transform.rotation * quaternion; + Vector3 vector4 = OWPhysics.FromToAngularVelocity(raftBody.GetRotation(), toRotation); + raftBody.SetAngularVelocity(origParentBody.GetAngularVelocity() + vector4); + } + return false; } } } diff --git a/NewHorizons/Patches/EchoesOfTheEyePatches/SingleLightSensorPatches.cs b/NewHorizons/Patches/EchoesOfTheEyePatches/SingleLightSensorPatches.cs new file mode 100644 index 00000000..39665dfc --- /dev/null +++ b/NewHorizons/Patches/EchoesOfTheEyePatches/SingleLightSensorPatches.cs @@ -0,0 +1,24 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.Patches.EchoesOfTheEyePatches +{ + [HarmonyPatch(typeof(SingleLightSensor))] + public static class SingleLightSensorPatches + { + [HarmonyPostfix] + [HarmonyPatch(nameof(SingleLightSensor.Start))] + public static void Start(SingleLightSensor __instance) + { + // SingleLightSensor assumes that the sector will be empty when it starts and disables itself, but this may not be true if it starts disabled and is activated later, or spawned via the API + if (__instance._sector && __instance._sector.ContainsAnyOccupants(DynamicOccupant.Player | DynamicOccupant.Probe)) + { + __instance.OnSectorOccupantsUpdated(); + } + } + } +} diff --git a/NewHorizons/Patches/VolumePatches/RepairReceiverPatches.cs b/NewHorizons/Patches/VolumePatches/RepairReceiverPatches.cs new file mode 100644 index 00000000..a21e6b79 --- /dev/null +++ b/NewHorizons/Patches/VolumePatches/RepairReceiverPatches.cs @@ -0,0 +1,62 @@ +using HarmonyLib; +using NewHorizons.Components.Volumes; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.Patches.VolumePatches +{ + + [HarmonyPatch(typeof(RepairReceiver))] + public static class RepairReceiverPatches + { + // We can't actually override these methods so we patch the base class methods to invoke the subclass methods dynamically + + [HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.IsRepairable))] + public static void IsRepairable(RepairReceiver __instance, ref bool __result) + { + if (__instance is NHRepairReceiver r) + { + __result = r.IsRepairable(); + } + } + + [HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.RepairTick))] + public static void RepairTick(RepairReceiver __instance) + { + if (__instance is NHRepairReceiver r) + { + r.RepairTick(); + } + } + + [HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.IsDamaged))] + public static void IsDamaged(RepairReceiver __instance, ref bool __result) + { + if (__instance is NHRepairReceiver r) + { + __result = r.IsDamaged(); + } + } + + [HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.GetRepairableName))] + public static void GetRepairableName(RepairReceiver __instance, ref UITextType __result) + { + if (__instance is NHRepairReceiver r) + { + __result = r.GetRepairableName(); + } + } + + [HarmonyPostfix, HarmonyPatch(nameof(RepairReceiver.GetRepairFraction))] + public static void GetRepairFraction(RepairReceiver __instance, ref float __result) + { + if (__instance is NHRepairReceiver r) + { + __result = r.GetRepairFraction(); + } + } + } +} diff --git a/NewHorizons/Schemas/addon_manifest_schema.json b/NewHorizons/Schemas/addon_manifest_schema.json index d68f545c..401db544 100644 --- a/NewHorizons/Schemas/addon_manifest_schema.json +++ b/NewHorizons/Schemas/addon_manifest_schema.json @@ -103,6 +103,27 @@ "type": "string", "description": "Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.\nNote this is a regular dialogue condition, not a persistent condition." }, + "audio": { + "type": "string", + "description": "The audio to use for the credits music. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nCredits will be silent unless this attribute is specified.\nNote: only applies when creditsType is set to \"custom\"." + }, + "audioVolume": { + "type": "number", + "description": "The length of the fade in and out for the credits music.\nNote: only applies when creditsType is set to \"custom\".", + "format": "float", + "default": 1.0 + }, + "audioLooping": { + "type": "boolean", + "description": "Determines if the credits music should loop.\nNote: only applies when creditsType is set to \"custom\".", + "default": false + }, + "length": { + "type": "number", + "description": "Duration of the credits scroll in seconds.\nNote: only applies when creditsType is set to \"custom\".", + "format": "float", + "default": 120.0 + }, "creditsType": { "description": "The type of credits that will run after the game over message is shown", "default": "fast", @@ -152,12 +173,14 @@ "Fast", "Final", "Kazoo", + "Custom", "None" ], "enum": [ "fast", "final", "kazoo", + "custom", "none" ] } diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index f8c63685..a15baa45 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -1121,6 +1121,11 @@ "format": "float", "default": 0.5 }, + "colliderIsTrigger": { + "type": "boolean", + "description": "Whether the added sphere collider will be a trigger (interactible but does not collide). Defaults to true.", + "default": true + }, "droppable": { "type": "boolean", "description": "Whether the item can be dropped. Defaults to true.", @@ -1230,6 +1235,17 @@ "format": "float", "default": 2.0 }, + "colliderRadius": { + "type": "number", + "description": "Default collider radius when interacting with the socket", + "format": "float", + "default": 0.0 + }, + "colliderIsTrigger": { + "type": "boolean", + "description": "Whether the added sphere collider will be a trigger (interactible but does not collide). Defaults to true.", + "default": true + }, "useGiveTakePrompts": { "type": "boolean", "description": "Whether to use \"Give Item\" / \"Take Item\" prompts instead of \"Insert Item\" / \"Remove Item\"." @@ -1259,12 +1275,6 @@ "removalFact": { "type": "string", "description": "A ship log fact to reveal when removing an item from this socket, or when the socket is empty." - }, - "colliderRadius": { - "type": "number", - "description": "Default collider radius when interacting with the socket", - "format": "float", - "default": 0.0 } } }, @@ -2221,6 +2231,13 @@ "$ref": "#/definitions/RaftInfo" } }, + "raftDocks": { + "type": "array", + "description": "Add raft docks to this planet (requires Echoes of the Eye DLC)", + "items": { + "$ref": "#/definitions/RaftDockInfo" + } + }, "scatter": { "type": "array", "description": "Scatter props around this planet's surface", @@ -2803,6 +2820,47 @@ "description": "Acceleration of the raft. Default acceleration is 5.", "format": "float", "default": 5.0 + }, + "dockPath": { + "type": "string", + "description": "Path to the dock this raft will start attached to." + }, + "pristine": { + "type": "boolean", + "description": "Uses the raft model from the dreamworld" + } + } + }, + "RaftDockInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" } } }, @@ -2954,7 +3012,7 @@ "$ref": "#/definitions/SlideReelType" }, "reelCondition": { - "description": "Exclusive to the slide reel type. Condition/material of the reel. Antique is the Stranger, Pristine is the Dreamworld, Rusted is a burned reel.", + "description": "Exclusive to the slide reel and standing vision torch type. Condition/material of the reel. Antique is the Stranger, Pristine is the Dreamworld, Rusted (exclusive to slide reels) is a burned reel.", "default": "antique", "$ref": "#/definitions/SlideReelCondition" }, @@ -4513,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" } } }, @@ -4542,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, @@ -4609,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" } } }, @@ -5307,6 +5395,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.", @@ -5328,6 +5423,10 @@ "$ref": "#/definitions/FluidVolumeInfo" } }, + "forces": { + "description": "Add force volumes to this planet.", + "$ref": "#/definitions/ForceModule" + }, "hazardVolumes": { "type": "array", "description": "Add hazard volumes to this planet.\nCauses damage to player when inside this volume.", @@ -5335,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.", @@ -5388,6 +5494,13 @@ "$ref": "#/definitions/VolumeInfo" } }, + "repairVolumes": { + "type": "array", + "description": "Add repair volumes to this planet.", + "items": { + "$ref": "#/definitions/RepairVolumeInfo" + } + }, "revealVolumes": { "type": "array", "description": "Add triggers that reveal parts of the ship log on this planet.", @@ -5413,6 +5526,13 @@ "$ref": "#/definitions/SpeedTrapVolumeInfo" } }, + "speedLimiterVolumes": { + "type": "array", + "description": "Add speed limiter volumes to this planet.\nSlows down the player, ship, and probe when they enter this volume.\nUsed on the Stranger in DLC.", + "items": { + "$ref": "#/definitions/SpeedLimiterVolumeInfo" + } + }, "visorEffects": { "description": "Add visor effect volumes to this planet.", "$ref": "#/definitions/VisorEffectModule" @@ -5458,10 +5578,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5524,6 +5659,101 @@ } } }, + "ShapeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "description": "The type of shape or collider to add. Sphere, box, and capsule colliders are more performant and support collision. Defaults to sphere.", + "$ref": "#/definitions/ShapeType" + }, + "radius": { + "type": "number", + "description": "The radius of the shape or collider. Defaults to 1 meter. Only used by spheres, capsules, cylinders, hemispheres, hemicapsules, and rings.", + "format": "float" + }, + "height": { + "type": "number", + "description": "The height of the shape or collider. Defaults to 1 meter. Only used by capsules, cylinders, cones, hemicapsules, and rings.", + "format": "float" + }, + "direction": { + "description": "The axis that the shape or collider is aligned with. Defaults to the Y axis (up). The flat bottom of the shape will be pointing towards the negative axis. Only used by capsules, cones, hemispheres, and hemicapsules.", + "$ref": "#/definitions/ColliderAxis" + }, + "innerRadius": { + "type": "number", + "description": "The inner radius of the shape. Defaults to 0 meters. Only used by cones and rings.", + "format": "float" + }, + "outerRadius": { + "type": "number", + "description": "The outer radius of the shape. Defaults to 0.5 meters. Only used by cones and rings.", + "format": "float" + }, + "cap": { + "type": "boolean", + "description": "Whether the shape has an end cap. Defaults to true. Only used by hemispheres and hemicapsules." + }, + "size": { + "description": "The size of the shape or collider. Defaults to (1,1,1). Only used by boxes.", + "$ref": "#/definitions/MVector3" + }, + "offset": { + "description": "The offset of the shape or collider from the object's origin. Defaults to (0,0,0). Supported by all collider and shape types.", + "$ref": "#/definitions/MVector3" + }, + "hasCollision": { + "type": "boolean", + "description": "Whether the collider should have collision enabled. If false, the collider will be a trigger. Defaults to false. Only supported for spheres, boxes, and capsules." + }, + "useShape": { + "type": [ + "boolean", + "null" + ], + "description": "Setting this to false will force it to use a collider, and setting to true will force it to use a shape.\nShapes do not support collision and are less performant, but support a wider set of shapes and are required by some components. \nIf left empty it will defaults to using a shape, unless hasCollision is true in which case it defaults to using a collider." + } + } + }, + "ShapeType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Sphere", + "Box", + "Capsule", + "Cylinder", + "Cone", + "Hemisphere", + "Hemicapsule", + "Ring" + ], + "enum": [ + "sphere", + "box", + "capsule", + "cylinder", + "cone", + "hemisphere", + "hemicapsule", + "ring" + ] + }, + "ColliderAxis": { + "type": "string", + "description": "", + "x-enumNames": [ + "X", + "Y", + "Z" + ], + "enum": [ + "x", + "y", + "z" + ] + }, "NHClipSelectionType": { "type": "string", "description": "", @@ -5538,6 +5768,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, @@ -5556,10 +5854,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5626,10 +5939,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5709,10 +6037,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5754,16 +6097,510 @@ } } }, + "ForceModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "cylindricalVolumes": { + "type": "array", + "description": "Applies a constant force along the volume's XZ plane towards the volume's center. Affects alignment.", + "items": { + "$ref": "#/definitions/CylindricalForceVolumeInfo" + } + }, + "directionalVolumes": { + "type": "array", + "description": "Applies a constant force in the direction of the volume's Y axis. May affect alignment.", + "items": { + "$ref": "#/definitions/DirectionalForceVolumeInfo" + } + }, + "gravityVolumes": { + "type": "array", + "description": "Applies planet-like gravity towards the volume's center with falloff by distance. May affect alignment.\nFor actual planetary body gravity, use the properties in the Base module.", + "items": { + "$ref": "#/definitions/GravityVolumeInfo" + } + }, + "polarVolumes": { + "type": "array", + "description": "Applies a constant force towards the volume's center. Affects alignment.", + "items": { + "$ref": "#/definitions/PolarForceVolumeInfo" + } + }, + "radialVolumes": { + "type": "array", + "description": "Applies a force towards the volume's center with falloff by distance. Affects alignment.", + "items": { + "$ref": "#/definitions/RadialForceVolumeInfo" + } + } + } + }, + "CylindricalForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "normal": { + "description": "The direction that the force applied by this volume will be perpendicular to. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "playGravityCrystalAudio": { + "type": "boolean", + "description": "Whether to play the gravity crystal audio when the player is in this volume." + } + } + }, + "DirectionalForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "normal": { + "description": "The direction of the force applied by this volume. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "affectsAlignment": { + "type": "boolean", + "description": "Whether this force volume affects alignment. Defaults to true.", + "default": true + }, + "offsetCentripetalForce": { + "type": "boolean", + "description": "Whether the force applied by this volume takes the centripetal force of the volume's parent body into account. Defaults to false." + }, + "playGravityCrystalAudio": { + "type": "boolean", + "description": "Whether to play the gravity crystal audio when the player is in this volume." + } + } + }, + "GravityVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "upperRadius": { + "type": "number", + "description": "The upper bounds of the volume's \"surface\". Above this radius, the force applied by this volume will have falloff applied.", + "format": "float" + }, + "lowerRadius": { + "type": "number", + "description": "The lower bounds of the volume's \"surface\". Above this radius and below the `upperRadius`, the force applied by this volume will be constant. Defaults to 0.", + "format": "float", + "default": 0.0 + }, + "minRadius": { + "type": "number", + "description": "The volume's force will decrease linearly from `force` to `minForce` as distance decreases from `lowerRadius` to `minRadius`. Defaults to 0.", + "format": "float", + "default": 0.0 + }, + "minForce": { + "type": "number", + "description": "The minimum force applied by this volume between `lowerRadius` and `minRadius`. Defaults to 0.", + "format": "float", + "default": 0.0 + }, + "fallOff": { + "description": "How the force falls off with distance. Most planets use linear but the sun and some moons use inverseSquared.", + "default": "linear", + "$ref": "#/definitions/GravityFallOff" + }, + "alignmentRadius": { + "type": [ + "null", + "number" + ], + "description": "The radius where objects will be aligned to the volume's force. Defaults to 1.5x the `upperRadius`. Set to 0 to disable alignment.", + "format": "float" + } + } + }, + "PolarForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "normal": { + "description": "Tangential mode only. The force applied by this volume will be perpendicular to this direction and the direction to the other body. Defaults to up (0, 1, 0).", + "$ref": "#/definitions/MVector3" + }, + "tangential": { + "type": "boolean", + "description": "Enables tangential mode. The force applied by this volume will be perpendicular to the normal and the direction to the other body. Defaults to false." + } + } + }, + "RadialForceVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "force": { + "type": "number", + "description": "The force applied by this volume. Can be negative to reverse the direction.", + "format": "float" + }, + "alignmentPriority": { + "type": "integer", + "description": "The priority of this force volume for the purposes of alignment.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "inheritable": { + "type": "boolean", + "description": "Whether this force volume is inheritable. The most recently activated inheritable force volume will stack with other force volumes even if their priorities differ." + }, + "layer": { + "type": "integer", + "description": "The layer of this volume.\n\nLayers separate the priority system. The priority of volumes in one layer will not affect or override volumes in another. The highest priority volume in each layer will stack like normal.\nThe exception is layer 0. A higher-priority volume in layer 0 will override lower-priority volumes in ALL other layers. A lower-priority volume in layer 0 will stack with other layers like normal.\n \nEx: A player could be affected by the sun on layer 9 priority 0 and planet gravity on layer 3 priority 2. They would experience the gravity of both volumes since they are on different layers.\nIf there was a zero-g volume on layer 0 priority 1, since it is on layer 0 it will override the gravity from the sun (priority 0 which is less than 1) but they will still feel the \ngravity of the planet (priority 2 is greater than 1). The zero-g volume will also still be applied because it is on a different layer.\n \nDefault value here is 0 which means this volume's priority will be evaluated against all other priority volumes regardless of their layer.", + "format": "int32", + "default": 0 + }, + "priority": { + "type": "integer", + "description": "The priority of this volume.\n\nVolumes of higher priority will override volumes of lower priority. Volumes of the same priority will stack like normal.\nEx: A player in a gravity volume with priority 0, and zero-gravity volume with priority 1, will feel zero gravity.\n \nDefault value here is 1 instead of 0 so it automatically overrides planet gravity, which is 0 by default. ", + "format": "int32", + "default": 1 + }, + "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" + }, + "fallOff": { + "description": "How the force falls off with distance. Defaults to linear.", + "default": "linear", + "$ref": "#/definitions/FallOff" + } + } + }, + "FallOff": { + "type": "string", + "description": "", + "x-enumNames": [ + "Constant", + "Linear", + "InverseSquared" + ], + "enum": [ + "constant", + "linear", + "inverseSquared" + ] + }, "HazardVolumeInfo": { "type": "object", "additionalProperties": false, "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5803,10 +6640,110 @@ } } }, + "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, "properties": { + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, "position": { "description": "Position of the object", "$ref": "#/definitions/MVector3" @@ -5825,9 +6762,13 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" } } }, @@ -5837,10 +6778,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5908,10 +6864,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5959,16 +6930,108 @@ } } }, + "RepairVolumeInfo": { + "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" + }, + "name": { + "type": "string", + "description": "The name displayed in the UI when the player is repairing this object. If not set, the name of the object will be used." + }, + "repairFraction": { + "type": "number", + "description": "How much of the object is initially repaired. 0 = not repaired, 1 = fully repaired.", + "format": "float", + "default": 0.0 + }, + "repairTime": { + "type": "number", + "description": "The time it takes to repair the object. Defaults to 3 seconds.", + "format": "float", + "default": 3.0 + }, + "repairDistance": { + "type": "number", + "description": "The distance from the object that the player can be to repair it. Defaults to 3 meters.", + "format": "float", + "default": 3.0 + }, + "damagedCondition": { + "type": "string", + "description": "A dialogue condition that will be set while the object is damaged. It will be unset when the object is repaired." + }, + "repairedCondition": { + "type": "string", + "description": "A dialogue condition that will be set when the object is repaired. It will be unset if the object is damaged again." + }, + "revealFact": { + "type": "string", + "description": "A ship log fact that will be revealed when the object is repaired." + } + } + }, "RevealVolumeInfo": { "type": "object", "additionalProperties": false, "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -5987,7 +7050,7 @@ }, "maxAngle": { "type": "number", - "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only)", + "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only). This will effectively be a cone extending from the volume's center forwards (along the Z axis) based on the volume's rotation.", "format": "float", "default": 180.0 }, @@ -6088,10 +7151,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6128,10 +7206,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6178,10 +7271,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6222,10 +7330,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6256,6 +7379,67 @@ } } }, + "SpeedLimiterVolumeInfo": { + "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" + }, + "maxSpeed": { + "type": "number", + "description": "The speed the volume will slow you down to when you enter it.", + "format": "float", + "default": 10.0 + }, + "stoppingDistance": { + "type": "number", + "description": "The distance from the outside of the volume that the limiter slows you down to max speed at.", + "format": "float", + "default": 100.0 + }, + "maxEntryAngle": { + "type": "number", + "description": "The maximum angle (in degrees) between the direction the incoming object is moving relative to the volume's center and the line from the object toward the center of the volume, within which the speed limiter will activate.", + "format": "float", + "default": 60.0 + } + } + }, "VisorEffectModule": { "type": "object", "additionalProperties": false, @@ -6294,10 +7478,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6348,10 +7547,25 @@ }, "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6388,10 +7602,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6428,10 +7657,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6465,10 +7709,25 @@ "properties": { "radius": { "type": "number", - "description": "The radius of this volume.", + "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" @@ -6519,6 +7778,27 @@ "type": "string", "description": "Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.\nNote this is a regular dialogue condition, not a persistent condition." }, + "audio": { + "type": "string", + "description": "The audio to use for the credits music. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list.\nCredits will be silent unless this attribute is specified.\nNote: only applies when creditsType is set to \"custom\"." + }, + "audioVolume": { + "type": "number", + "description": "The length of the fade in and out for the credits music.\nNote: only applies when creditsType is set to \"custom\".", + "format": "float", + "default": 1.0 + }, + "audioLooping": { + "type": "boolean", + "description": "Determines if the credits music should loop.\nNote: only applies when creditsType is set to \"custom\".", + "default": false + }, + "length": { + "type": "number", + "description": "Duration of the credits scroll in seconds.\nNote: only applies when creditsType is set to \"custom\".", + "format": "float", + "default": 120.0 + }, "creditsType": { "description": "The type of credits that will run after the game over message is shown", "default": "fast", @@ -6533,12 +7813,14 @@ "Fast", "Final", "Kazoo", + "Custom", "None" ], "enum": [ "fast", "final", "kazoo", + "custom", "none" ] }, diff --git a/NewHorizons/Schemas/title_screen_schema.json b/NewHorizons/Schemas/title_screen_schema.json index f1c45631..8eb61bda 100644 --- a/NewHorizons/Schemas/title_screen_schema.json +++ b/NewHorizons/Schemas/title_screen_schema.json @@ -2,11 +2,12 @@ "$schema": "http://json-schema.org/draft-04/schema#", "title": "Title Screen Schema", "type": "object", + "description": "Allows you to configure the title screen with custom music, skyboxes, and loading props from asset bundles.\nYou can define a list of title screen configurations, with different persistent condition/ship log facts required to display them.", "additionalProperties": false, "properties": { "titleScreens": { "type": "array", - "description": "Create title screens", + "description": "Create title screens.\nThe last title screen in the list with its display conditions (persistent condition and/or ship log) met will be displayed if this mod\nis chosen to be shown on the main menu.", "items": { "$ref": "#/definitions/TitleScreenInfo" } @@ -19,6 +20,7 @@ "definitions": { "TitleScreenInfo": { "type": "object", + "description": "A single title screen configuration", "additionalProperties": false, "properties": { "menuTextTint": { diff --git a/NewHorizons/Utility/DebugTools/DebugFogWarp.cs b/NewHorizons/Utility/DebugTools/DebugFogWarp.cs new file mode 100644 index 00000000..c541e29f --- /dev/null +++ b/NewHorizons/Utility/DebugTools/DebugFogWarp.cs @@ -0,0 +1,52 @@ +using UnityEngine; + +namespace NewHorizons.Utility.DebugTools +{ + /// + /// Adapted from Survivors https://github.com/Hawkbat/ow-mod-jam-2/blob/main/EscapePodFour.cs#L197 + /// + [RequireComponent(typeof(SphericalFogWarpVolume))] + public class DebugFogWarp : MonoBehaviour + { + public SphericalFogWarpVolume fogWarpVolume; + public void OnGUI() + { + if (Main.Debug && Main.VisualizeBrambleVolumeNames && fogWarpVolume != null) + { + DrawWorldLabel(fogWarpVolume, fogWarpVolume.name); + if (fogWarpVolume._exits != null) + { + foreach (var e in fogWarpVolume._exits) + { + if (e != null) + { + DrawWorldLabel(fogWarpVolume.GetExitPosition(e), e.name); + } + } + } + } + } + + public void DrawWorldLabel(Component component, string text) + { + DrawWorldLabel(component.transform.position, text); + } + + public void DrawWorldLabel(Vector3 worldPos, string text) + { + var c = Locator.GetPlayerCamera(); + var d = Vector3.Distance(c.transform.position, worldPos); + if (d > 1000f) return; + GUI.Label(new Rect(WorldToGui(worldPos), new Vector2(500f, 20f)), text); + } + + public Vector2 WorldToGui(Vector3 wp) + { + var c = Locator.GetPlayerCamera(); + var sp = c.WorldToScreenPoint(wp); + if (sp.z < 0) return new Vector2(Screen.width, Screen.height); + var gp = new Vector2(sp.x, Screen.height - sp.y); + return gp; + } + } +} diff --git a/NewHorizons/Utility/Files/AudioUtilities.cs b/NewHorizons/Utility/Files/AudioUtilities.cs index fe09d46e..f3c2924d 100644 --- a/NewHorizons/Utility/Files/AudioUtilities.cs +++ b/NewHorizons/Utility/Files/AudioUtilities.cs @@ -27,23 +27,34 @@ namespace NewHorizons.Utility.Files source._clipArrayLength = 0; source._clipSelectionOnPlay = OWAudioSource.ClipSelectionOnPlay.MANUAL; source.clip = clip; + NHLogger.LogVerbose($"[{nameof(AudioUtilities)}] : Audio {audio} was loaded from a file"); return; } catch { - NHLogger.LogError($"Could not load file {audio}"); + NHLogger.LogError($"[{nameof(AudioUtilities)}] : Could not load file {audio}"); } } if (EnumUtils.TryParse(audio, out AudioType type)) { source._audioLibraryClip = type; + NHLogger.LogVerbose($"[{nameof(AudioUtilities)}] : Audio {audio} was an AudioType enum"); } else { var audioClip = SearchUtilities.FindResourceOfTypeAndName(audio); - if (audioClip == null) NHLogger.Log($"Couldn't find audio clip {audio}"); - else source.clip = audioClip; + if (audioClip == null) + { + NHLogger.LogError($"[{nameof(AudioUtilities)}] : Couldn't find audio clip {audio}"); + } + else + { + NHLogger.LogVerbose($"[{nameof(AudioUtilities)}] : Audio {audio} was an AudioClip resource"); + // Else if this is set it will try to change the clip back when it starts playing + source._audioLibraryClip = AudioType.None; + source.clip = audioClip; + } } } diff --git a/NewHorizons/Utility/NewHorizonExtensions.cs b/NewHorizons/Utility/NewHorizonExtensions.cs index 9f4da941..10ca7570 100644 --- a/NewHorizons/Utility/NewHorizonExtensions.cs +++ b/NewHorizons/Utility/NewHorizonExtensions.cs @@ -274,6 +274,14 @@ namespace NewHorizons.Utility return UnityEngine.Object.Instantiate(original); } + public static GameObject Instantiate(this GameObject original, Vector3 localPosition, Quaternion localRotation, Transform parent) + { + var copy = UnityEngine.Object.Instantiate(original, parent, false); + copy.transform.localPosition = localPosition; + copy.transform.localRotation = localRotation; + return copy; + } + public static T DontDestroyOnLoad(this T target) where T : UnityEngine.Object { UnityEngine.Object.DontDestroyOnLoad(target); @@ -445,6 +453,11 @@ namespace NewHorizons.Utility return globalMusicController._endTimesSource.clip.length; } + public static string GetKey(this AstroObject ao) + { + return ao._name == AstroObject.Name.CustomString ? ao.GetCustomName() : ao._name.ToString(); + } + public static CodeMatcher LogInstructions(this CodeMatcher matcher, string prefix) { matcher.InstructionEnumeration().LogInstructions(prefix); diff --git a/NewHorizons/Utility/OuterWilds/AstroObjectLocator.cs b/NewHorizons/Utility/OuterWilds/AstroObjectLocator.cs index 9e3a8a01..dd3c9564 100644 --- a/NewHorizons/Utility/OuterWilds/AstroObjectLocator.cs +++ b/NewHorizons/Utility/OuterWilds/AstroObjectLocator.cs @@ -65,7 +65,7 @@ namespace NewHorizons.Utility.OuterWilds public static void RegisterCustomAstroObject(AstroObject ao) { - var key = ao._name == AstroObject.Name.CustomString ? ao.GetCustomName() : ao._name.ToString(); + var key = ao.GetKey(); if (_customAstroObjectDictionary.ContainsKey(key)) { @@ -81,7 +81,7 @@ namespace NewHorizons.Utility.OuterWilds public static void DeregisterCustomAstroObject(AstroObject ao) { - var key = ao._name == AstroObject.Name.CustomString ? ao.GetCustomName() : ao._name.ToString(); + var key = ao.GetKey(); _customAstroObjectDictionary.Remove(key); } diff --git a/NewHorizons/default-config.json b/NewHorizons/default-config.json index 4f3239c2..7502ba9a 100644 --- a/NewHorizons/default-config.json +++ b/NewHorizons/default-config.json @@ -22,6 +22,12 @@ "value": false, "tooltip": "Draws boundaries around quantum objects when Debug mode is on." }, + "VisualizeBrambleVolumeNames": { + "title": "Visualize Bramble Volume Names", + "type": "toggle", + "value": false, + "tooltip": "Adds a label to all custom spherical fog warp volumes and entrances/exits when Debug mode is on." + }, "VerboseLogs": { "title": "Verbose Logs", "type": "toggle", diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 8d7ca082..75cd8f6b 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -1,10 +1,10 @@ { "$schema": "https://raw.githubusercontent.com/amazingalek/owml/master/schemas/manifest_schema.json", "filename": "NewHorizons.dll", - "author": "xen, Bwc9876, JohnCorby, MegaPiggy, Trifid, and friends", + "author": "xen, Bwc9876, JohnCorby, MegaPiggy, and friends", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.27.0", + "version": "1.28.5", "owmlVersion": "2.12.1", "dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ], "conflicts": [ "PacificEngine.OW_CommonResources" ], diff --git a/README.md b/README.md index 75307d4f..1842c1f9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ ![Latest release date](https://img.shields.io/github/release-date/xen-42/outer-wilds-new-horizons) [![Build](https://github.com/xen-42/outer-wilds-new-horizons/actions/workflows/build.yaml/badge.svg)](https://github.com/xen-42/outer-wilds-new-horizons/actions/workflows/build.yaml) -_Do you want to create planets using New Horizons?_ Then check out our [website](https://nh.outerwildsmods.com/) for all our documentation! +_Do you want to create your own story mod using New Horizons?_ Then check out our [website](https://nh.outerwildsmods.com/) for all our documentation! If you want to see examples of what NH can do check out the [examples add-on](https://github.com/xen-42/ow-new-horizons-examples) or [real solar system add-on](https://github.com/xen-42/outer-wilds-real-solar-system). @@ -26,7 +26,7 @@ Check the ship's log for how to use your warp drive to travel between star syste ## Incompatible mods -New Horizons conflicts with the mod Common Resources. This mod is a requirement for other mods such as Cheats Mod (we recommend you use the [Cheat and Debug Menu](https://outerwildsmods.com/mods/cheatanddebugmenu/) mod instead) and OW Randomizer. +New Horizons conflicts with the mod Common Resources. This mod is a requirement for other mods such as OW Randomizer (not to be confused with Archipelago Randomizer) and Difficulty Mod. Why do these two mods conflict? Common Resources is a mod which reimplements many of the game's features underneath the hood, for one reason or another. For instance, it completely overhauls how the orbits of planets work, as this is a requirement for it to support OW Randomizer. It does this even when you are only using Cheats Mod. In particular, having CR installed seems to, for whatever reason, break character dialogue introduced by New Horizons. As CR is no longer actively maintained, it is unlikely this issue will be resolved any time soon. @@ -40,25 +40,27 @@ New Horizons has optional support for a few other mods: ## Features -- Load planet meshes or details from asset bundles -- Use our [template Unity project](https://github.com/ow-mods/outer-wilds-unity-template) to create assets for use in NH, including all game scripts recovered using UtinyRipper -- Separate solar system scenes accessible via wormhole OR via the ship's new warp drive feature found in the ship's log -- Remove or edit existing planets, including their orbits -- Create custom planets from heightmaps/texturemaps with support for triplanar mapping -- Create stars (and supernovae), comets, asteroid belts, satellites, quantum planets/moons, and custom Dark Bramble dimensions. -- Add stock planet features to custom ones, such as geysers, cloaking fields, meteor-launching volcanoes, rafts, tornados, and Dark Bramble seeds/nodes. -- Binary orbits -- Signalscope signals and custom frequencies -- Surface scatter: rocks, trees, etc, using in-game models, or custom ones -- Black hole / white hole pairs -- Custom dialogue, slide-reel projections, translatable text, and custom ship log entries for rumour mode and map mode -- Funnels and variable surface height (can be made of sand/water/lava/star) +NH allows you to create planets: +- Load planet meshes or details from asset bundles made in Unity +- Create custom planets from heightmaps/texturemaps with support for triplanar mapping +- Add stock planet features: dialogue, Nomai text, geysers, cloaking fields, meteor-launching volcanoes, rafts, tornados, Dark Bramble seeds/nodes, black/white holes, funnels, lava/oceans/sand, signalscope signals/frequencies and others. +- Edit existing planets: Remove them, alter their orbits, add/delete objects from them. +- All these features work in both the main solar system and the Eye of the Universe scene + +Even if the majority of your story mod is going to be done in Unity, NH still offers useful features which will improve mod-compatibility and stop you reinventing the wheel: +- NH allows you to separate your story mod into its own solar system scene accessible via wormhole OR via the ship's new warp drive feature found in the ship's log. +- NH allows you to create custom ship log entries. +- NH allows you to create Inhabitant slide reels with asynchronous loading optimization. +- NH has a localization system to translate ship logs, dialogue, Nomai text, and custom UI elements. +- NH allows you to add new characters to the cosmic-jam-session at the Eye of the Universe while ensuring mod compatibility + +Use our [template Unity project](https://github.com/ow-mods/outer-wilds-unity-template) to create assets for use in NH, including all game scripts recovered using UtinyRipper ## Development -If you want to help (please dear god help us) then check out the [contact](#contact) info below or the [contributing](https://github.com/xen-42/outer-wilds-new-horizons/blob/master/CONTRIBUTING.md) page. +If you want to help (please dear god help us) then check out the [contact](#contact) info below or the [contributing](https://github.com/Outer-Wilds-New-Horizons/new-horizons/blob/master/CONTRIBUTING.md) page. -The Unity project we use to make asset bundles for this mod is [here](https://github.com/xen-42/new-horizons-unity). +The Unity project we use to make asset bundles for this mod is [here](https://github.com/Outer-Wilds-New-Horizons/nh-unity). ## Contact @@ -70,11 +72,10 @@ Main authors: - [xen](https://github.com/xen-42) - [Bwc9876](https://github.com/Bwc9876) (New Horizons v0.9.0 onwards) - -New Horizons was made with help from: - - [JohnCorby](https://github.com/JohnCorby) - [MegaPiggy](https://github.com/MegaPiggy) + +New Horizons was made with help from: - [FreezeDriedMangos](https://github.com/FreezeDriedMangos) - [Trifid](https://github.com/TerrificTrifid) - [Hawkbar](https://github.com/Hawkbat) @@ -89,7 +90,7 @@ Translation credits: - Japanese: TRSasasusu - Portuguese: avengerx, loco-choco -New Horizons was based off [Marshmallow](https://github.com/misternebula/Marshmallow) was made by: +New Horizons was based off [Marshmallow](https://github.com/misternebula/Marshmallow) made by: - [\_nebula](https://github.com/misternebula) diff --git a/docs/src/content/docs/guides/details.md b/docs/src/content/docs/guides/details.md index faff0389..bb0ee020 100644 --- a/docs/src/content/docs/guides/details.md +++ b/docs/src/content/docs/guides/details.md @@ -20,12 +20,12 @@ You can use [Unity Explorer](https://outerwildsmods.com/mods/unityexplorer) to t ## Asset Bundles -There is an [old unity template](https://github.com/xen-42/outer-wilds-unity-template) and a [new one](https://github.com/ow-mods/outer-wilds-unity-wiki/wiki#outer-wilds-unity-assets) +There is an [old unity template](https://github.com/xen-42/outer-wilds-unity-template) and a [new one](https://github.com/ow-mods/outer-wilds-unity-wiki/wiki/Tools-%E2%80%90-Outer-Wilds-Unity-Assets-repository) The project contains ripped versions of all the game scripts, meaning you can put things like DirectionalForceVolumes in your Unity project to have artificial gravity volumes loaded right into the game.\ Either one works, but the new one has more tools and better versions of the scripts (in exchange for being invite-only). -Read [this guide](https://github.com/ow-mods/outer-wilds-unity-wiki/wiki/Tutorials-%E2%80%90-Using-asset-bundles) on how to work with asset bundles in editor. +Read [this guide](https://github.com/ow-mods/outer-wilds-unity-wiki/wiki/Tutorials-%E2%80%90-Using-AssetBundles) on how to work with asset bundles in editor. ## Importing a planet's surface from Unity diff --git a/docs/src/content/docs/guides/troubleshooting.md b/docs/src/content/docs/guides/troubleshooting.md index d4650d40..8f6b4592 100644 --- a/docs/src/content/docs/guides/troubleshooting.md +++ b/docs/src/content/docs/guides/troubleshooting.md @@ -19,5 +19,5 @@ which interact poorly with the fluid detector and can mess up the movement of th Either clear the .nhcache files or enable Debug mode to always regenerate the text cache. ## Prop placer is gone! -This is not a bug, actually. We removed prop placer because it was inconsistent and buggy, and no one in years cared enough to fix it. -Use the debug raycast button and Unity Explorer to place your props, or otherwise work in unity editor. +It has been moved to a [separate mod](https://outerwildsmods.com/mods/propplacer/). +Use it in addition to the debug raycast button and Unity Explorer to place your props, or otherwise work in unity editor. diff --git a/docs/src/content/docs/guides/volumes.md b/docs/src/content/docs/guides/volumes.md new file mode 100644 index 00000000..192b2180 --- /dev/null +++ b/docs/src/content/docs/guides/volumes.md @@ -0,0 +1,72 @@ +--- +title: Volumes +description: Guide to making volumes in New Horizons +--- + +Volumes are invisible 3D "zones" or "triggers" that cause various effects when objects enter or leave them. For example, `oxygenVolumes` refill the player's oxygen when they enter (used for the various oxygen-generating trees in the game), `forces.directionalVolumes` push players and other physics objects in a specific direction (used by both Nomai artificial gravity surfaces and tractor beams), `revealVolumes` unlock ship log facts when the player enters or observes them (used everywhere in the game), and more. + +New Horizons makes adding volumes to your planets easy; just specify them like you would [for a prop](/guides/details/) but under `Volumes` instead of `Props`. For example, to add an oxygen volume at certain location: + +```json title="planets/My Cool Planet.json" +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name" : "My Cool Planet", + "Volumes": { + "oxygenVolumes": [ + { + "position": {"x": 399.4909, "y": -1.562098, "z": 20.11444}, + "radius": 30, + "treeVolume": true, + "playRefillAudio": true + } + ] + } +} +``` + +Listing out every type of volume is outside the scope of this guide, but you can see every supported type of volume and the properties they need in [the VolumesModule schema](/schemas/body-schema/defs/volumesmodule/). + +## Volume Shapes + +By default, volumes are spherical, and you can specify the radius of that sphere with the `radius` property. If you want to use a different shape for your volume, such as a box or capsule, you can specify your volume's `shape` like so: + +```json title="planets/My Cool Planet.json" +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", + "name" : "My Cool Planet", + "Volumes": { + "forces": { + "directionalVolumes": [ + { + "rename": "ArtificialGravitySurface", + "force": 8, + "playGravityCrystalAudio": true, + "shape": { + "type": "box", + "size": { + "x": 15.0, + "y": 10.0, + "z": 5.0 + }, + "offset": { + "x": 0, + "y": 5.0, + "z": 0 + } + }, + "position": { "x": 0, "y": -110, "z": 0 }, + "rotation": { "x": 180, "y": 0, "z": 0 } + } + ] + } + } +} +``` + +The supported shape types are: `sphere`, `box`, `capsule`, `cylinder`, `cone`, `hemisphere`, `hemicapsule`, and `ring`. See [the ShapeInfo schema](/schemas/body-schema/defs/shapeinfo/) for the full list of properties available to define each shape. + +Note that `sphere`, `box`, and `capsule` shapes are more reliable and efficient than other shapes, so prefer using them whenever possible. + +### Debugging + +To visualize the shapes of your volumes in-game, use the [Collider Visualizer mod](https://outerwildsmods.com/mods/collidervisualizer/). It will display a wireframe of the shapes around you so you can see precisely where they are and reposition or resize them as needed. diff --git a/docs/src/content/docs/reference/api.md b/docs/src/content/docs/reference/api.md index 9accd91c..8022026e 100644 --- a/docs/src/content/docs/reference/api.md +++ b/docs/src/content/docs/reference/api.md @@ -10,109 +10,264 @@ Put this in a C# file somewhere in your mod: ```cs -public interface INewHorizons +using OWML.Common; +using System; +using System.Collections.Generic; +using System.Xml.Linq; +using UnityEngine; +using UnityEngine.Events; + +namespace NewHorizons { - [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] - void Create(Dictionary config); + public interface INewHorizons + { + #region Obsolete + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config); - [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] - void Create(Dictionary config, IModBehaviour mod); + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config, IModBehaviour mod); - /// - /// 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); + [Obsolete("SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) is deprecated, please use SpawnObject(IModBehaviour mod, GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) instead")] + GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); + #endregion - /// - /// 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); + /// + /// 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); - /// - /// The name of the current star system loaded. - /// - string GetCurrentStarSystem(); + /// + /// 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); - /// - /// 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(); + /// + /// Returns the uniqueIDs of each installed NH addon. + /// + string[] GetInstalledAddons(); - /// - /// 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(); + #region Get/set/change star system + /// + /// The name of the current star system loaded. + /// + string GetCurrentStarSystem(); - /// - /// 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(); + /// + /// Allows you to overwrite the default system. This is where the player is respawned after dying. + /// + bool SetDefaultSystem(string name); - /// - /// Uses JSONPath to query a body - /// - object QueryBody(Type outType, string bodyName, string path); + /// + /// 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); + #endregion - /// - /// Uses JSONPath to query a body - /// - T QueryBody(string bodyName, string path); + #region events + /// + /// 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(); - /// - /// Uses JSONPath to query the current star system - /// - object QuerySystem(Type outType, string path); + /// + /// 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(); - /// - /// Uses JSONPath to query the current star system - /// - T QuerySystem(string path); + /// + /// 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(); - /// - /// Allows you to overwrite the default system. This is where the player is respawned after dying. - /// - bool SetDefaultSystem(string name); + /// + /// An event invoked when NH has finished building a title screen. + /// Gives the unique name of the mod the title screen builder was from and the index for when you have multiple title screens. + /// + UnityEvent GetTitleScreenLoadedEvent(); - /// - /// 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); + /// + /// An event invoked when NH has finished building the title screen. + /// + UnityEvent GetAllTitleScreensLoadedEvent(); + #endregion - /// - /// Returns the uniqueIDs of each installed NH addon. - /// - string[] GetInstalledAddons(); + #region Querying configs + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); - /// - /// 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); + /// + /// Uses JSONPath to query a body + /// + T QueryBody(string bodyName, string path); - /// - /// 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 = ""); + /// + /// Uses JSONPath to query the current star system + /// + object QuerySystem(Type outType, string path); - /// - /// 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); + /// + /// Uses JSONPath to query the current star system + /// + T QuerySystem(string path); + + /// + /// Uses JSONPath to query a title screen config + /// + object QueryTitleScreen(Type outType, IModBehaviour mod, string path); + + /// + /// Uses JSONPath to query a title screen config + /// + T QueryTitleScreen(IModBehaviour mod, string path); + + /// + /// Register your own builder that will act on the given GameObject by reading the json string of its "extras" module + /// + void RegisterCustomBuilder(Action builder); + #endregion + + #region Spawn props + /// + /// 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(IModBehaviour mod, 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); + #endregion + + #region Load json/xml directly + /// + /// Allows creation of a planet by directly passing the json contents as a string. + /// + /// + /// + void CreatePlanet(string config, IModBehaviour mod); + + /// + /// Allows defining details of a star system by directly passing the json contents as a string. + /// + /// + /// + /// + void DefineStarSystem(string name, string config, IModBehaviour mod); + + /// + /// Allows creation of dialogue by directly passing the xml and dialogueInfo json contents as strings. + /// Must be called at least 2 frames before entering dialogue if you're using ReuseDialogueOptionsFrom + /// + /// TextAsset name used for compatibility with voice mod. Just has to be a unique identifier. + /// The contents of the dialogue xml file as a string + /// The json dialogue info as a string. See the documentation/schema for what this can contain. + /// The root planet rigidbody that this dialogue is attached to. Any paths in the dialogueInfo are relative to this body. + /// + (CharacterDialogueTree, RemoteDialogueTrigger) CreateDialogueFromXML(string textAssetID, string xml, string dialogueInfo, GameObject planetGO); + + /// + /// Allows the creation of Nomai text by directly passing the xml and translatorTextInfo json contents as strings + /// + /// The contents of the translator text file as a string + /// The json translator text info as a string. See the documentation/schema for what this can contain. + /// The root planet rigidbody that this text is attached to. Any paths in the translatorTextInfo are relative to this body. + /// + GameObject CreateNomaiText(string xml, string textInfo, GameObject planetGO); + + /// + /// Directly add ship logs from XML. Call this method right before ShipLogManager awake. + /// + /// The mod this method is being called from. This is required for loading files. + /// The ship log xml contents + /// The planet that these ship logs correspond to in the map mode + /// The relative path from your mod manifest.json where the ship log images are located. The filename must be the same as the fact id. Optional. + /// A dictionary of each fact id to its 2D position in the ship log. Optional. + /// A dictionary of each curiousity ID to its colour and highlight colour in the ship log. Optional. + void AddShipLogXML(IModBehaviour mod, XElement xml, string planetName, string imageFolder = null, Dictionary entryPositions = null, Dictionary curiousityColours = null); + #endregion + + #region Translations + /// + /// Look up shiplog-related translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForShipLog(string text); + /// + /// Look up dialogue-related translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForDialogue(string text); + /// + /// Look up UI-related translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForUI(string text); + /// + /// Look up miscellaneous translated text for the given text key. + /// Defaults to English if no translation in the current language is available, and just the key if no English translation is available. + /// + /// The text key to look up. + /// + string GetTranslationForOtherText(string text); + #endregion + + /// + /// Registers a subtitle for the main menu. + /// Call this once before the main menu finishes loading + /// + /// + /// + void AddSubtitle(IModBehaviour mod, string filePath); + + /// + /// Whatever system the player is warping to next, they will spawn at the spawn point with this ID + /// Gets reset after warping. Is also overriden by entering a system-changing black hole or warp volume by their `spawnPointID` + /// + /// + void SetNextSpawnID(string id); + + /// + /// Registers a builder for the main menu. + /// Call this once before the main menu finishes loading + /// + /// + /// Builder to run when this title screen is selected. The GameObject passed through it is the main scene object containing both the background and menu planet. + /// If set to true, NH generated planets will not show on the title screen. If false, this title screen has the same chance as other NH planet title screens to show. + /// If set to true, this custom title screen will merge with all other custom title screens with shareTitleScreen set to true. If false, NH will randomly select between this and other valid title screens that are loaded. + /// Persistent condition required for this title screen to appear. + /// Ship log fact required for this title screen to appear. + void RegisterTitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets = true, bool shareTitleScreen = false, string persistentConditionRequired = null, string factRequired = null); + } } ```