diff --git a/.gitignore b/.gitignore index 94675367..4e3a3cec 100644 --- a/.gitignore +++ b/.gitignore @@ -417,4 +417,9 @@ docs/.m_cache/** # Unity project new-horizons-unity new-horizons-unity/** +nh-unity +nh-unity/** +NewHorizons/Assets/Assets +NewHorizons/Assets/Assets.manifest + docs/.env diff --git a/NewHorizons/Assets/translations/english.json b/NewHorizons/Assets/translations/english.json index b7aae651..b6fa95be 100644 --- a/NewHorizons/Assets/translations/english.json +++ b/NewHorizons/Assets/translations/english.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "$schema": "https://raw.githubusercontent.com/outer-wilds-new-horizons/new-horizons/main/NewHorizons/Schemas/translation_schema.json", "DialogueDictionary": { "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_1": "Your ship is now equiped with a warp drive!", "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_2": "You can use the new \"Interstellar Mode\" page in the ship log to lock-on your autopilot to another star system.", @@ -14,7 +14,14 @@ "WARP_LOCKED": "AUTOPILOT LOCKED TO:\n{0}", "LOCK_AUTOPILOT_WARP": "Lock Autopilot to Star System", "RICH_PRESENCE_EXPLORING": "Exploring {0}.", - "RICH_PRESENCE_WARPING": "Warping to {0}." + "RICH_PRESENCE_WARPING": "Warping to {0}.", + "OUTDATED_VERSION_WARNING": "WARNING\n\nNew Horizons only works on version {0} or higher. You're on version {1}.\n\nPlease update your game or uninstall NH.", + "JSON_FAILED_TO_LOAD": "Invalid file(s): {0}", + "DEBUG_RAYCAST": "Raycast", + "DEBUG_PLACE": "Place Object", + "DEBUG_PLACE_TEXT": "Place Nomai Text", + "DEBUG_UNDO": "Undo", + "DEBUG_REDO": "Redo" }, "AchievementTranslations": { "NH_EATEN_OUTSIDE_BRAMBLE": { diff --git a/NewHorizons/Assets/translations/french.json b/NewHorizons/Assets/translations/french.json index 37adf8ac..5c6c1d19 100644 --- a/NewHorizons/Assets/translations/french.json +++ b/NewHorizons/Assets/translations/french.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "$schema": "https://raw.githubusercontent.com/outer-wilds-new-horizons/new-horizons/main/NewHorizons/Schemas/translation_schema.json", "DialogueDictionary": { "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_1": "Votre fusée est maintenant équipé d'un moteur de distorsion!", "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_2": "Vous pouvez utiliser la nouvelle page \"Mode Interstellaire\" dans le journal de bord pour diriger votre pilote automatique vers un autre système solaire", @@ -14,7 +14,9 @@ "WARP_LOCKED": "PILOTE AUTOMATIQUE VISÉ SUR:\n{0}", "LOCK_AUTOPILOT_WARP": "Visez le pilote automatique", "RICH_PRESENCE_EXPLORING": "En explorant {0}.", - "RICH_PRESENCE_WARPING": "En route vers {0}." + "RICH_PRESENCE_WARPING": "En route vers {0}.", + "OUTDATED_VERSION_WARNING": "AVERTISSEMENT\n\nNew Horizons fonctionne seulement sur la version {0} ou plus récente. Vous êtes sur la version {1}.\n\nVeuillez mettre à jour votre jeu ou désinstaller NH.", + "JSON_FAILED_TO_LOAD": "Fichier(s) invalide(s): {0}" }, "AchievementTranslations": { "NH_EATEN_OUTSIDE_BRAMBLE": { diff --git a/NewHorizons/Assets/translations/german.json b/NewHorizons/Assets/translations/german.json index 460adc9f..3762146c 100644 --- a/NewHorizons/Assets/translations/german.json +++ b/NewHorizons/Assets/translations/german.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "$schema": "https://raw.githubusercontent.com/outer-wilds-new-horizons/new-horizons/main/NewHorizons/Schemas/translation_schema.json", "DialogueDictionary": { "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_1": "Dein Schiff ist nun mit einem Warpantrieb ausgestattet!", "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_2": "Du kannst den neuen \"Interstellar Modus\" aus dem Schiffslogbuch auswählen um mit dem Autopilot in andere Sternensysteme zu reisen.", diff --git a/NewHorizons/Assets/translations/russian.json b/NewHorizons/Assets/translations/russian.json index 7063f448..a33d7f56 100644 --- a/NewHorizons/Assets/translations/russian.json +++ b/NewHorizons/Assets/translations/russian.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "$schema": "https://raw.githubusercontent.com/outer-wilds-new-horizons/new-horizons/main/NewHorizons/Schemas/translation_schema.json", "DialogueDictionary": { "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_1": "Твой корабль теперь экипирован варп-двигателем!", "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_2": "Ты можешь использовать новый \"Режим Interstellar\" на своем корабле, что-бы закрепить автопилот на выбранную звёздную систему.", diff --git a/NewHorizons/Assets/translations/spanish_la.json b/NewHorizons/Assets/translations/spanish_la.json index 787342ba..d79d5a12 100644 --- a/NewHorizons/Assets/translations/spanish_la.json +++ b/NewHorizons/Assets/translations/spanish_la.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/translation_schema.json", + "$schema": "https://raw.githubusercontent.com/outer-wilds-new-horizons/new-horizons/main/NewHorizons/Schemas/translation_schema.json", "DialogueDictionary": { "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_1": "¡Tu nave ahora tiene un motor de curvatura!", "NEW_HORIZONS_WARP_DRIVE_DIALOGUE_2": "Puedes usar la nueva página de \"modo interestelar\" en el registro de la nave para fijar el piloto automático a otro sistema solar.", diff --git a/NewHorizons/Assets/xen.newhorizons b/NewHorizons/Assets/xen.newhorizons index 84e681f3..de0815e9 100644 Binary files a/NewHorizons/Assets/xen.newhorizons and b/NewHorizons/Assets/xen.newhorizons differ diff --git a/NewHorizons/Assets/xen.newhorizons.manifest b/NewHorizons/Assets/xen.newhorizons.manifest new file mode 100644 index 00000000..8902bd7f --- /dev/null +++ b/NewHorizons/Assets/xen.newhorizons.manifest @@ -0,0 +1,26 @@ +ManifestFileVersion: 0 +CRC: 2022446871 +Hashes: + AssetFileHash: + serializedVersion: 2 + Hash: 083882699617744b8fc49234bb8cb795 + TypeTreeHash: + serializedVersion: 2 + Hash: 10a6a558690295dadb3dd990eda0821a +HashAppended: 0 +ClassTypes: +- Class: 21 + Script: {instanceID: 0} +- Class: 48 + Script: {instanceID: 0} +SerializeReferenceClassIdentifiers: [] +Assets: +- Assets/Shaders/SphereTextureWrapper.shader +- Assets/Shaders/Ring.shader +- Assets/Shaders/SphereTextureWrapperNormal.shader +- Assets/Shaders/UnlitRing1Pixel.shader +- Assets/Shaders/UnlitTransparent.shader +- Assets/Resources/TransparentCloud.mat +- Assets/Shaders/StandardCullOFF.shader +- Assets/Shaders/Ring1Pixel.shader +Dependencies: [] diff --git a/NewHorizons/Builder/Atmosphere/AirBuilder.cs b/NewHorizons/Builder/Atmosphere/AirBuilder.cs index 7b5ab84d..209a2b26 100644 --- a/NewHorizons/Builder/Atmosphere/AirBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/AirBuilder.cs @@ -23,28 +23,31 @@ namespace NewHorizons.Builder.Atmosphere sfv._allowShipAutoroll = true; sfv._disableOnStart = false; - // Try to parent it to the same as other rulesets to match vanilla but if its null put it on the root object - var ruleSetGO = planetGO.GetComponentInChildren()?.gameObject; - if (ruleSetGO == null) ruleSetGO = planetGO; - - var shockLayerRuleset = ruleSetGO.AddComponent(); - shockLayerRuleset._type = ShockLayerRuleset.ShockType.Atmospheric; - shockLayerRuleset._radialCenter = airGO.transform; - shockLayerRuleset._minShockSpeed = config.Atmosphere.minShockSpeed; - shockLayerRuleset._maxShockSpeed = config.Atmosphere.maxShockSpeed; - - if (config.Atmosphere.clouds != null) + if (config.Atmosphere.hasShockLayer) { - shockLayerRuleset._innerRadius = config.Atmosphere.clouds.innerCloudRadius; - shockLayerRuleset._outerRadius = config.Atmosphere.clouds.outerCloudRadius; - } - else - { - var bottom = config.Base.surfaceSize; - var top = config.Atmosphere.size; + // Try to parent it to the same as other rulesets to match vanilla but if its null put it on the root object + var ruleSetGO = planetGO.GetComponentInChildren()?.gameObject; + if (ruleSetGO == null) ruleSetGO = planetGO; - shockLayerRuleset._innerRadius = (bottom + top) / 2f; - shockLayerRuleset._outerRadius = top; + var shockLayerRuleset = ruleSetGO.AddComponent(); + shockLayerRuleset._type = ShockLayerRuleset.ShockType.Atmospheric; + shockLayerRuleset._radialCenter = airGO.transform; + shockLayerRuleset._minShockSpeed = config.Atmosphere.minShockSpeed; + shockLayerRuleset._maxShockSpeed = config.Atmosphere.maxShockSpeed; + + if (config.Atmosphere.clouds != null) + { + shockLayerRuleset._innerRadius = config.Atmosphere.clouds.innerCloudRadius; + shockLayerRuleset._outerRadius = config.Atmosphere.clouds.outerCloudRadius; + } + else + { + var bottom = config.Base.surfaceSize; + var top = config.Atmosphere.size; + + shockLayerRuleset._innerRadius = (bottom + top) / 2f; + shockLayerRuleset._outerRadius = top; + } } if (config.Atmosphere.hasOxygen) diff --git a/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs b/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs index 7646a205..9dd1e8a8 100644 --- a/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/AtmosphereBuilder.cs @@ -59,7 +59,7 @@ namespace NewHorizons.Builder.Atmosphere } } - material.SetFloat(InnerRadius, atmosphereModule.clouds != null ? atmosphereModule.size : surfaceSize); + material.SetFloat(InnerRadius, (atmosphereModule.clouds != null && atmosphereModule.clouds.cloudsPrefab != CloudPrefabType.Transparent) ? atmosphereModule.size : surfaceSize); material.SetFloat(OuterRadius, atmosphereModule.size * 1.2f); if (atmosphereModule.atmosphereTint != null) material.SetColor(SkyColor, atmosphereModule.atmosphereTint.ToColor()); diff --git a/NewHorizons/Builder/Atmosphere/CloudsBuilder.cs b/NewHorizons/Builder/Atmosphere/CloudsBuilder.cs index 65039e3a..d5b15f89 100644 --- a/NewHorizons/Builder/Atmosphere/CloudsBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/CloudsBuilder.cs @@ -9,9 +9,9 @@ namespace NewHorizons.Builder.Atmosphere { public static class CloudsBuilder { - private static Shader _sphereShader = null; private static Material[] _gdCloudMaterials; private static Material[] _qmCloudMaterials; + private static Material _transparentCloud; private static GameObject _lightningPrefab; private static Texture2D _colorRamp; private static readonly int Color = Shader.PropertyToID("_Color"); @@ -19,6 +19,7 @@ namespace NewHorizons.Builder.Atmosphere private static readonly int MainTex = Shader.PropertyToID("_MainTex"); private static readonly int RampTex = Shader.PropertyToID("_RampTex"); private static readonly int CapTex = Shader.PropertyToID("_CapTex"); + private static readonly int Smoothness = Shader.PropertyToID("_Glossiness"); public static void Make(GameObject planetGO, Sector sector, AtmosphereModule atmo, bool cloaked, IModBehaviour mod) { @@ -29,7 +30,15 @@ namespace NewHorizons.Builder.Atmosphere cloudsMainGO.SetActive(false); cloudsMainGO.transform.parent = sector?.transform ?? planetGO.transform; - MakeTopClouds(cloudsMainGO, atmo, mod); + if (atmo.clouds.cloudsPrefab != CloudPrefabType.Transparent) MakeTopClouds(cloudsMainGO, atmo, mod); + else + { + MakeTransparentClouds(cloudsMainGO, atmo, mod); + if (atmo.clouds.hasLightning) MakeLightning(cloudsMainGO, sector, atmo); + cloudsMainGO.transform.position = planetGO.transform.TransformPoint(Vector3.zero); + cloudsMainGO.SetActive(true); + return; + } GameObject cloudsBottomGO = new GameObject("BottomClouds"); cloudsBottomGO.SetActive(false); @@ -91,8 +100,6 @@ namespace NewHorizons.Builder.Atmosphere // Fix the rotations once the rest is done cloudsMainGO.transform.rotation = planetGO.transform.TransformRotation(Quaternion.Euler(0, 0, 0)); - // For the base shader it has to be rotated idk - if (atmo.clouds.cloudsPrefab == CloudPrefabType.Basic) cloudsMainGO.transform.rotation = planetGO.transform.TransformRotation(Quaternion.Euler(90, 0, 0)); // Lightning if (atmo.clouds.hasLightning) @@ -117,7 +124,7 @@ namespace NewHorizons.Builder.Atmosphere lightning.transform.localPosition = Vector3.zero; var lightningGenerator = lightning.GetComponent(); - lightningGenerator._altitude = (atmo.clouds.outerCloudRadius + atmo.clouds.innerCloudRadius) / 2f; + lightningGenerator._altitude = atmo.clouds.cloudsPrefab != CloudPrefabType.Transparent ? (atmo.clouds.outerCloudRadius + atmo.clouds.innerCloudRadius) / 2f : atmo.clouds.outerCloudRadius; if (noAudio) { lightningGenerator._audioPrefab = null; @@ -170,7 +177,6 @@ namespace NewHorizons.Builder.Atmosphere MeshRenderer topMR = cloudsTopGO.AddComponent(); - if (_sphereShader == null) _sphereShader = Main.NHAssetBundle.LoadAsset("Assets/Shaders/SphereTextureWrapper.shader"); if (_gdCloudMaterials == null) _gdCloudMaterials = SearchUtilities.Find("CloudsTopLayer_GD").GetComponent().sharedMaterials; if (_qmCloudMaterials == null) _qmCloudMaterials = SearchUtilities.Find("CloudsTopLayer_QM").GetComponent().sharedMaterials; Material[] prefabMaterials = atmo.clouds.cloudsPrefab == CloudPrefabType.GiantsDeep ? _gdCloudMaterials : _qmCloudMaterials; @@ -178,10 +184,10 @@ namespace NewHorizons.Builder.Atmosphere if (atmo.clouds.cloudsPrefab == CloudPrefabType.Basic) { - var material = new Material(_sphereShader); + var material = new Material(Shader.Find("Standard")); if (atmo.clouds.unlit) material.renderQueue = 3000; material.name = atmo.clouds.unlit ? "BasicCloud" : "BasicShadowCloud"; - + material.SetFloat(Smoothness, 0f); tempArray[0] = material; } else @@ -211,8 +217,7 @@ namespace NewHorizons.Builder.Atmosphere if (atmo.clouds.rotationSpeed != 0f) { var topRT = cloudsTopGO.AddComponent(); - // Idk why but the axis is weird - topRT._localAxis = atmo.clouds.cloudsPrefab == CloudPrefabType.Basic ? Vector3.forward : Vector3.up; + topRT._localAxis = Vector3.up; topRT._degreesPerSecond = atmo.clouds.rotationSpeed; topRT._randomizeRotationRate = false; } @@ -223,5 +228,65 @@ namespace NewHorizons.Builder.Atmosphere return cloudsTopGO; } + + public static GameObject MakeTransparentClouds(GameObject rootObject, AtmosphereModule atmo, IModBehaviour mod, bool isProxy = false) + { + Texture2D image; + + try + { + image = ImageUtilities.GetTexture(mod, atmo.clouds.texturePath); + } + catch (Exception e) + { + Logger.LogError($"Couldn't load Cloud texture for [{atmo.clouds.texturePath}]:\n{e}"); + return null; + } + + GameObject cloudsTransparentGO = new GameObject("TransparentClouds"); + cloudsTransparentGO.SetActive(false); + cloudsTransparentGO.transform.parent = rootObject.transform; + cloudsTransparentGO.transform.localScale = Vector3.one * atmo.clouds.outerCloudRadius; + + MeshFilter filter = cloudsTransparentGO.AddComponent(); + filter.mesh = SearchUtilities.Find("CloudsTopLayer_GD").GetComponent().mesh; + + MeshRenderer renderer = cloudsTransparentGO.AddComponent(); + if (_transparentCloud == null) _transparentCloud = Main.NHAssetBundle.LoadAsset("Assets/Resources/TransparentCloud.mat"); + var material = new Material(_transparentCloud); + material.name = "TransparentClouds_" + image.name; + material.SetTexture(MainTex, image); + renderer.sharedMaterial = material; + + if (!isProxy) + { + GameObject tcrqcGO = new GameObject("TransparentCloudRenderQueueController"); + tcrqcGO.transform.SetParent(cloudsTransparentGO.transform, false); + tcrqcGO.layer = LayerMask.NameToLayer("BasicEffectVolume"); + + var shape = tcrqcGO.AddComponent(); + shape.radius = 1; + + var owTriggerVolume = tcrqcGO.AddComponent(); + owTriggerVolume._shape = shape; + + TransparentCloudRenderQueueController tcrqc = tcrqcGO.AddComponent(); + tcrqc.renderer = renderer; + } + + if (atmo.clouds.rotationSpeed != 0f) + { + var rt = cloudsTransparentGO.AddComponent(); + rt._localAxis = Vector3.up; + rt._degreesPerSecond = atmo.clouds.rotationSpeed; + rt._randomizeRotationRate = false; + } + + cloudsTransparentGO.transform.localPosition = Vector3.zero; + + cloudsTransparentGO.SetActive(true); + + return cloudsTransparentGO; + } } } diff --git a/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs b/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs index fb405705..6394e02f 100644 --- a/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/EffectsBuilder.cs @@ -20,6 +20,18 @@ namespace NewHorizons.Builder.Atmosphere SCG._dynamicCullingBounds = false; SCG._waitForStreaming = false; + var minHeight = surfaceSize; + if (config.HeightMap?.minHeight != null) + { + if (config.Water?.size >= config.HeightMap.minHeight) minHeight = config.Water.size; // use sea level if its higher + else minHeight = config.HeightMap.minHeight; + } + else if (config.Water?.size != null) minHeight = config.Water.size; + else if (config.Lava?.size != null) minHeight = config.Lava.size; + + var maxHeight = config.Atmosphere.size; + if (config.Atmosphere.clouds?.outerCloudRadius != null) maxHeight = config.Atmosphere.clouds.outerCloudRadius; + if (config.Atmosphere.hasRain) { var rainGO = GameObject.Instantiate(SearchUtilities.Find("GiantsDeep_Body/Sector_GD/Sector_GDInterior/Effects_GDInterior/Effects_GD_Rain"), effectsGO.transform); @@ -29,9 +41,9 @@ namespace NewHorizons.Builder.Atmosphere var pvc = rainGO.GetComponent(); pvc._densityByHeight = new AnimationCurve(new Keyframe[] { - new Keyframe(surfaceSize - 0.5f, 0), - new Keyframe(surfaceSize, 10f), - new Keyframe(config.Atmosphere.size, 0f) + new Keyframe(minHeight - 0.5f, 0), + new Keyframe(minHeight, 10f), + new Keyframe(maxHeight, 0f) }); rainGO.GetComponent()._activeInSector = sector; @@ -53,9 +65,9 @@ namespace NewHorizons.Builder.Atmosphere var pvc = snowEmitter.GetComponent(); pvc._densityByHeight = new AnimationCurve(new Keyframe[] { - new Keyframe(surfaceSize - 0.5f, 0), - new Keyframe(surfaceSize, 10f), - new Keyframe(config.Atmosphere.size, 0f) + new Keyframe(minHeight - 0.5f, 0), + new Keyframe(minHeight, 10f), + new Keyframe(maxHeight, 0f) }); snowEmitter.GetComponent()._activeInSector = sector; diff --git a/NewHorizons/Builder/Atmosphere/VolumesBuilder.cs b/NewHorizons/Builder/Atmosphere/VolumesBuilder.cs index cba3845d..ceb9b758 100644 --- a/NewHorizons/Builder/Atmosphere/VolumesBuilder.cs +++ b/NewHorizons/Builder/Atmosphere/VolumesBuilder.cs @@ -31,6 +31,7 @@ namespace NewHorizons.Builder.Atmosphere PlanetoidRuleset PR = rulesetGO.AddComponent(); PR._altitudeFloor = innerRadius; PR._altitudeCeiling = sphereOfInfluence; + PR._shuttleLandingRadius = sphereOfInfluence; PR._useMinimap = config.Base.showMinimap; PR._useAltimeter = config.Base.showMinimap; diff --git a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs index 65fa1fcf..18927c1f 100644 --- a/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs +++ b/NewHorizons/Builder/Body/BrambleDimensionBuilder.cs @@ -192,8 +192,12 @@ namespace NewHorizons.Builder.Body cloak.GetComponent().enabled = true; // Cull stuff - var cullController = go.AddComponent(); - cullController.SetSector(sector); + // Do next update so other nodes can be built first + Delay.FireOnNextUpdate(() => + { + var cullController = go.AddComponent(); + cullController.SetSector(sector); + }); // finalize atmo.SetActive(true); diff --git a/NewHorizons/Builder/Body/ProxyBuilder.cs b/NewHorizons/Builder/Body/ProxyBuilder.cs index 6c823c20..89820072 100644 --- a/NewHorizons/Builder/Body/ProxyBuilder.cs +++ b/NewHorizons/Builder/Body/ProxyBuilder.cs @@ -54,7 +54,7 @@ namespace NewHorizons.Builder.Body remnantGO.transform.parent = proxy.transform; remnantGO.transform.localPosition = Vector3.zero; - SharedMake(planetGO, remnantGO, proxyController, remnant); + SharedMake(planetGO, remnantGO, null, remnant); proxyController.stellarRemnantGO = remnantGO; } @@ -111,7 +111,8 @@ namespace NewHorizons.Builder.Body if (body.Config.Atmosphere.clouds != null) { - topClouds = CloudsBuilder.MakeTopClouds(proxy, body.Config.Atmosphere, body.Mod).GetComponent(); + if (body.Config.Atmosphere.clouds.cloudsPrefab != External.Modules.CloudPrefabType.Transparent) topClouds = CloudsBuilder.MakeTopClouds(proxy, body.Config.Atmosphere, body.Mod).GetComponent(); + else topClouds = CloudsBuilder.MakeTransparentClouds(proxy, body.Config.Atmosphere, body.Mod, true).GetAddComponent(); if (body.Config.Atmosphere.clouds.hasLightning) lightningGenerator = CloudsBuilder.MakeLightning(proxy, null, body.Config.Atmosphere, true); @@ -119,15 +120,20 @@ namespace NewHorizons.Builder.Body } } - if (body.Config.Ring != null) + if (body.Config.Rings != null) { - RingBuilder.MakeRingGraphics(proxy, null, body.Config.Ring, body.Mod); - if (realSize < body.Config.Ring.outerRadius) realSize = body.Config.Ring.outerRadius; + foreach (var ring in body.Config.Rings) + { + RingBuilder.MakeRingGraphics(proxy, null, ring, body.Mod); + if (realSize < ring.outerRadius) realSize = ring.outerRadius; + } } + Renderer starAtmosphere = null; + Renderer starFog = null; if (body.Config.Star != null) { - StarBuilder.MakeStarProxy(planetGO, proxy, body.Config.Star, body.Mod, body.Config.isStellarRemnant); + (_, starAtmosphere, starFog) = StarBuilder.MakeStarProxy(planetGO, proxy, body.Config.Star, body.Mod, body.Config.isStellarRemnant); if (realSize < body.Config.Star.size) realSize = body.Config.Star.size; } @@ -217,9 +223,17 @@ namespace NewHorizons.Builder.Body if (proxyController != null) { - proxyController._atmosphere = atmosphere; - proxyController._fog = fog; - proxyController._fogCurveMaxVal = fogCurveMaxVal; + proxyController._atmosphere = atmosphere ?? starAtmosphere; + if (fog != null) + { + proxyController._fog = fog; + proxyController._fogCurveMaxVal = fogCurveMaxVal; + } + else if (starFog != null) + { + proxyController._fog = starFog; + proxyController._fogCurveMaxVal = 0.05f; + } proxyController.topClouds = topClouds; proxyController.lightningGenerator = lightningGenerator; proxyController.supernovaPlanetEffectController = supernovaPlanetEffect; diff --git a/NewHorizons/Builder/Body/RingBuilder.cs b/NewHorizons/Builder/Body/RingBuilder.cs index 9d2e6b05..40d5953f 100644 --- a/NewHorizons/Builder/Body/RingBuilder.cs +++ b/NewHorizons/Builder/Body/RingBuilder.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using NewHorizons.External.Modules; using UnityEngine; using Logger = NewHorizons.Utility.Logger; +using NewHorizons.Components.Volumes; namespace NewHorizons.Builder.Body { @@ -73,7 +74,7 @@ namespace NewHorizons.Builder.Body return null; } - var ringGO = new GameObject("Ring"); + var ringGO = new GameObject(!string.IsNullOrEmpty(ring.rename) ? ring.rename : "Ring"); ringGO.transform.parent = sector?.transform ?? rootObject.transform; ringGO.transform.position = rootObject.transform.position; ringGO.transform.rotation = rootObject.transform.rotation; diff --git a/NewHorizons/Builder/Body/SingularityBuilder.cs b/NewHorizons/Builder/Body/SingularityBuilder.cs index 4b9d4cdf..c9cd4d4d 100644 --- a/NewHorizons/Builder/Body/SingularityBuilder.cs +++ b/NewHorizons/Builder/Body/SingularityBuilder.cs @@ -1,4 +1,3 @@ -using NewHorizons.Components; using NewHorizons.External.Configs; using NewHorizons.Utility; using System; @@ -10,6 +9,7 @@ using System.Linq; using NewHorizons.Components.SizeControllers; using System.Drawing; using Color = UnityEngine.Color; +using NewHorizons.Components.Volumes; namespace NewHorizons.Builder.Body { diff --git a/NewHorizons/Builder/Body/StarBuilder.cs b/NewHorizons/Builder/Body/StarBuilder.cs index e976f800..eb9cac25 100644 --- a/NewHorizons/Builder/Body/StarBuilder.cs +++ b/NewHorizons/Builder/Body/StarBuilder.cs @@ -1,4 +1,3 @@ -using NewHorizons.Components; using NewHorizons.Components.SizeControllers; using NewHorizons.Utility; using OWML.Utils; @@ -9,6 +8,7 @@ using OWML.ModHelper; using OWML.Common; using UnityEngine.InputSystem.XR; using System.Linq; +using NewHorizons.Components.Stars; namespace NewHorizons.Builder.Body { @@ -46,23 +46,29 @@ namespace NewHorizons.Builder.Body sunAtmosphere.transform.position = planetGO.transform.position; sunAtmosphere.transform.localScale = Vector3.one * OuterRadiusRatio; sunAtmosphere.name = "Atmosphere_Star"; + + var atmospheres = sunAtmosphere.transform.Find("AtmoSphere"); + atmospheres.transform.localScale = Vector3.one; + var lods = atmospheres.GetComponentsInChildren(); + foreach (var lod in lods) + { + lod.material.SetFloat(InnerRadius, starModule.size); + lod.material.SetFloat(OuterRadius, starModule.size * OuterRadiusRatio); + } + var fog = sunAtmosphere.transform.Find("FogSphere").GetComponent(); + fog.transform.localScale = Vector3.one; + fog.fogRadius = starModule.size * OuterRadiusRatio; + fog.lodFadeDistance = fog.fogRadius * (StarBuilder.OuterRadiusRatio - 1f); + + fog.fogImpostor.material.SetFloat(Radius, starModule.size * OuterRadiusRatio); if (starModule.tint != null) { fog.fogTint = starModule.tint.ToColor(); fog.fogImpostor.material.SetColor(Tint, starModule.tint.ToColor()); - sunAtmosphere.transform.Find("AtmoSphere").transform.localScale = Vector3.one; - foreach (var lod in sunAtmosphere.transform.Find("AtmoSphere").GetComponentsInChildren()) - { + foreach (var lod in lods) lod.material.SetColor(SkyColor, starModule.tint.ToColor()); - lod.material.SetFloat(InnerRadius, starModule.size); - lod.material.SetFloat(OuterRadius, starModule.size * OuterRadiusRatio); - } } - fog.transform.localScale = Vector3.one; - fog.fogRadius = starModule.size * OuterRadiusRatio; - fog.lodFadeDistance = fog.fogRadius * (StarBuilder.OuterRadiusRatio - 1f); - fog.fogImpostor.material.SetFloat(Radius, starModule.size * OuterRadiusRatio); } var ambientLightGO = Object.Instantiate(SearchUtilities.Find("Sun_Body/AmbientLight_SUN"), starGO.transform); @@ -120,7 +126,6 @@ namespace NewHorizons.Builder.Body light.CopyPropertiesFrom(SearchUtilities.Find("Sun_Body/Sector_SUN/Effects_SUN/SunLight").GetComponent()); light.intensity *= starModule.solarLuminosity; light.range = starModule.lightRadius; - light.range *= Mathf.Sqrt(starModule.solarLuminosity); Color lightColour = light.color; if (starModule.lightTint != null) lightColour = starModule.lightTint.ToColor(); @@ -179,10 +184,37 @@ namespace NewHorizons.Builder.Body return (starGO, starController, starEvolutionController); } - public static GameObject MakeStarProxy(GameObject planet, GameObject proxyGO, StarModule starModule, IModBehaviour mod, bool isStellarRemnant) + public static (GameObject, Renderer, Renderer) MakeStarProxy(GameObject planet, GameObject proxyGO, StarModule starModule, IModBehaviour mod, bool isStellarRemnant) { var (starGO, controller, supernova) = SharedStarGeneration(proxyGO, null, mod, starModule, isStellarRemnant); + Renderer atmosphere = null; + Renderer fog = null; + if (starModule.hasAtmosphere) + { + GameObject sunAtmosphere = Object.Instantiate(SearchUtilities.Find("SunProxy/Sun_Proxy_Body/Atmosphere_SUN", false) ?? SearchUtilities.Find("SunProxy(Clone)/Sun_Proxy_Body/Atmosphere_SUN"), starGO.transform); + sunAtmosphere.transform.position = proxyGO.transform.position; + sunAtmosphere.transform.localScale = Vector3.one * OuterRadiusRatio; + sunAtmosphere.name = "Atmosphere_Star"; + + atmosphere = sunAtmosphere.transform.Find("Atmosphere_LOD2").GetComponent(); + atmosphere.transform.localScale = Vector3.one; + atmosphere.material.SetFloat(InnerRadius, starModule.size); + atmosphere.material.SetFloat(OuterRadius, starModule.size * OuterRadiusRatio); + + fog = sunAtmosphere.transform.Find("FogSphere").GetComponent(); + fog.transform.localScale = Vector3.one; + fog.material.SetFloat(Radius, starModule.size * OuterRadiusRatio); + + if (starModule.tint != null) + { + fog.material.SetColor(Tint, starModule.tint.ToColor()); + atmosphere.material.SetColor(SkyColor, starModule.tint.ToColor()); + } + + controller.atmosphere = sunAtmosphere; + } + controller.isProxy = true; // Planet can have multiple stars on them, so find the one that is also a remnant / not a remnant @@ -198,7 +230,7 @@ namespace NewHorizons.Builder.Body supernova.mainStellerDeathController = mainController.supernova; } - return starGO; + return (starGO, atmosphere, fog); } private static (GameObject, StarEvolutionController, StellarDeathController) SharedStarGeneration(GameObject planetGO, Sector sector, IModBehaviour mod, StarModule starModule, bool isStellarRemnant) diff --git a/NewHorizons/Builder/Body/StellarRemnantBuilder.cs b/NewHorizons/Builder/Body/StellarRemnantBuilder.cs index 31ea505a..6e06c5dc 100644 --- a/NewHorizons/Builder/Body/StellarRemnantBuilder.cs +++ b/NewHorizons/Builder/Body/StellarRemnantBuilder.cs @@ -88,7 +88,7 @@ namespace NewHorizons.Builder.Body lightRadius = 10000, solarLuminosity = 0.5f }; - if (proxy != null) return StarBuilder.MakeStarProxy(planetGO, proxy, whiteDwarfModule, mod, true); + if (proxy != null) return StarBuilder.MakeStarProxy(planetGO, proxy, whiteDwarfModule, mod, true).Item1; else return StarBuilder.Make(planetGO, sector, whiteDwarfModule, mod, true).Item1; } @@ -107,7 +107,7 @@ namespace NewHorizons.Builder.Body // Instead of showing the typical star surface we use a tinted singularity GameObject neutronStar; - if (proxy != null) neutronStar = StarBuilder.MakeStarProxy(planetGO, proxy, neutronStarModule, mod, true); + if (proxy != null) neutronStar = StarBuilder.MakeStarProxy(planetGO, proxy, neutronStarModule, mod, true).Item1; else (neutronStar, _, _) = StarBuilder.Make(planetGO, sector, neutronStarModule, mod, true); neutronStar.FindChild("Surface").SetActive(false); diff --git a/NewHorizons/Builder/Body/WaterBuilder.cs b/NewHorizons/Builder/Body/WaterBuilder.cs index 5cbc5d70..a05dd63e 100644 --- a/NewHorizons/Builder/Body/WaterBuilder.cs +++ b/NewHorizons/Builder/Body/WaterBuilder.cs @@ -1,9 +1,9 @@ -using NewHorizons.Components; using NewHorizons.Components.SizeControllers; using NewHorizons.Utility; using UnityEngine; using NewHorizons.External.Modules.VariableSize; using Tessellation; +using NewHorizons.Components.Volumes; namespace NewHorizons.Builder.Body { diff --git a/NewHorizons/Builder/General/GroupsBuilder.cs b/NewHorizons/Builder/General/GroupsBuilder.cs new file mode 100644 index 00000000..e37e7eab --- /dev/null +++ b/NewHorizons/Builder/General/GroupsBuilder.cs @@ -0,0 +1,29 @@ +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.Builder.General; + +public static class GroupsBuilder +{ + /// + /// puts groups on an object, activated by sector. + /// run this before the gameobject is active. + /// + public static void Make(GameObject go, Sector sector) + { + if (!sector) + { + Logger.LogWarning($"tried to put groups on {go.name} when sector is null"); + return; + } + if (go.activeInHierarchy) + { + Logger.LogWarning($"tried to put groups on an active gameobject {go.name}"); + return; + } + + go.GetAddComponent()._sector = sector; + go.GetAddComponent()._sector = sector; + go.GetAddComponent()._sector = sector; + } +} \ No newline at end of file diff --git a/NewHorizons/Builder/General/SpawnPointBuilder.cs b/NewHorizons/Builder/General/SpawnPointBuilder.cs index f094a57a..a224333d 100644 --- a/NewHorizons/Builder/General/SpawnPointBuilder.cs +++ b/NewHorizons/Builder/General/SpawnPointBuilder.cs @@ -1,5 +1,7 @@ using NewHorizons.External.Modules; using NewHorizons.Utility; +using System; +using System.Reflection; using UnityEngine; using Logger = NewHorizons.Utility.Logger; namespace NewHorizons.Builder.General @@ -89,38 +91,28 @@ namespace NewHorizons.Builder.General public static void SuitUp() { suitUpQueued = false; - if (Locator.GetPlayerController()._isWearingSuit) return; - - Locator.GetPlayerTransform().GetComponent().SuitUp(false, true, true); - - // Make the ship act as if the player took the suit - var spv = SearchUtilities.Find("Ship_Body/Module_Supplies/Systems_Supplies/ExpeditionGear")?.GetComponent(); - - if (spv == null) return; - - spv._containsSuit = false; - - if (spv._allowSuitReturn) + if (!Locator.GetPlayerController()._isWearingSuit) { - spv._interactVolume.ChangePrompt(UITextType.ReturnSuitPrompt, spv._pickupSuitCommandIndex); + var spv = SearchUtilities.Find("Ship_Body/Module_Supplies/Systems_Supplies/ExpeditionGear")?.GetComponent(); + if (spv != null) + { + var command = spv._interactVolume.GetInteractionAt(spv._pickupSuitCommandIndex).inputCommand; + + // Make the ship act as if the player took the suit + var eventDelegate = (MulticastDelegate)typeof(MultipleInteractionVolume).GetField( + nameof(MultipleInteractionVolume.OnPressInteract), + BindingFlags.Instance | BindingFlags.NonPublic) + .GetValue(spv._interactVolume); + foreach (var handler in eventDelegate.GetInvocationList()) + { + handler.Method.Invoke(handler.Target, new object[] { command }); + } + } + else + { + Locator.GetPlayerTransform().GetComponent().SuitUp(false, true, true); + } } - else - { - spv._interactVolume.EnableSingleInteraction(false, spv._pickupSuitCommandIndex); - } - - spv._timer = 0f; - spv._index = 0; - - spv.OnSuitUp(); - - GameObject suitGeometry = spv._suitGeometry; - if (suitGeometry != null) suitGeometry.SetActive(false); - - OWCollider suitOWCollider = spv._suitOWCollider; - if (suitOWCollider != null) suitOWCollider.SetActivation(false); - - spv.enabled = true; } } } diff --git a/NewHorizons/Builder/Props/DetailBuilder.cs b/NewHorizons/Builder/Props/DetailBuilder.cs index ae7a63f7..660956b6 100644 --- a/NewHorizons/Builder/Props/DetailBuilder.cs +++ b/NewHorizons/Builder/Props/DetailBuilder.cs @@ -79,7 +79,7 @@ namespace NewHorizons.Builder.Props } else FixSectoredComponent(component, sector, isTorch); - FixComponent(component, go, prefab.name); + FixComponent(component, go); } prop.transform.position = detail.position == null ? go.transform.position : go.transform.TransformPoint(detail.position); @@ -222,11 +222,11 @@ namespace NewHorizons.Builder.Props return false; } - private static void FixComponent(Component component, GameObject planetGO, string prefab) + private static void FixComponent(Component component, GameObject planetGO) { // Fix other components // I forget why this is here - if (component is GhostIK || component is GhostEffects) + if (component is GhostIK or GhostEffects) { Component.DestroyImmediate(component); return; @@ -278,43 +278,52 @@ namespace NewHorizons.Builder.Props torchItem.mindSlideProjector._mindProjectorImageEffect = SearchUtilities.Find("Player_Body/PlayerCamera").GetComponent(); } - // Fix a bunch of stuff when done loading - Delay.RunWhen(() => Main.IsSystemReady, () => + if (component is Animator animator) animator.enabled = true; + if (component is Collider collider) collider.enabled = true; + if (component is Renderer renderer) renderer.enabled = true; + if (component is Shape shape) shape.enabled = true; + + // fixes sector cull group deactivating renderers on map view enter and fast foward + // TODO: does this actually work? what? how? + if (component is SectorCullGroup sectorCullGroup) { - try + sectorCullGroup._inMapView = false; + sectorCullGroup._isFastForwarding = false; + sectorCullGroup.SetVisible(sectorCullGroup.ShouldBeVisible(), true, false); + } + + // If it's not a moving anglerfish make sure the anim controller is regular + if (component is AnglerfishAnimController && component.GetComponentInParent() == null) + component.gameObject.AddComponent(); + } + + /// + /// Has to happen after AnglerfishAnimController awake to remove the events it has set up. + /// Otherwise results in the anglerfish 1) having its animations controlled by an actual fish 2) randomly having different animations on solarsystem load + /// Can't do delay because it needs to work with scatter (copies a prefab made using MakeDetail). + /// + [RequireComponent(typeof(AnglerfishAnimController))] + private class AnglerAnimFixer : MonoBehaviour + { + private void Start() + { + var angler = GetComponent(); + + Logger.LogVerbose("Fixing anglerfish animation"); + + // Remove any event reference to its angler + if (angler._anglerfishController) { - if (component == null) return; - if (component is Animator animator) animator.enabled = true; - else if (component is Collider collider) collider.enabled = true; - else if (component is Renderer renderer) renderer.enabled = true; - else if (component is Shape shape) shape.enabled = true; - else if (component is SectorCullGroup sectorCullGroup) - { - sectorCullGroup._inMapView = false; - sectorCullGroup._isFastForwarding = false; - sectorCullGroup.SetVisible(sectorCullGroup.ShouldBeVisible(), true, false); - } - // If it's not a moving anglerfish make sure the anim controller is regular - else if (component is AnglerfishAnimController angler && angler.GetComponentInParent() == null) - { - Logger.LogVerbose("Enabling anglerfish animation"); - // Remove any reference to its angler - if (angler._anglerfishController) - { - angler._anglerfishController.OnChangeAnglerState -= angler.OnChangeAnglerState; - angler._anglerfishController.OnAnglerTurn -= angler.OnAnglerTurn; - angler._anglerfishController.OnAnglerSuspended -= angler.OnAnglerSuspended; - angler._anglerfishController.OnAnglerUnsuspended -= angler.OnAnglerUnsuspended; - } - angler.enabled = true; - angler.OnChangeAnglerState(AnglerfishController.AnglerState.Lurking); - } + angler._anglerfishController.OnChangeAnglerState -= angler.OnChangeAnglerState; + angler._anglerfishController.OnAnglerTurn -= angler.OnAnglerTurn; + angler._anglerfishController.OnAnglerSuspended -= angler.OnAnglerSuspended; + angler._anglerfishController.OnAnglerUnsuspended -= angler.OnAnglerUnsuspended; } - catch (Exception e) - { - Logger.LogWarning($"Exception when modifying component [{component.GetType().Name}] on [{planetGO.name}] for prop [{prefab}]:\n{e}"); - } - }); + angler.enabled = true; + angler.OnChangeAnglerState(AnglerfishController.AnglerState.Lurking); + + Destroy(this); + } } } } \ No newline at end of file diff --git a/NewHorizons/Builder/Props/DialogueBuilder.cs b/NewHorizons/Builder/Props/DialogueBuilder.cs index f917c973..b20a57d5 100644 --- a/NewHorizons/Builder/Props/DialogueBuilder.cs +++ b/NewHorizons/Builder/Props/DialogueBuilder.cs @@ -87,7 +87,7 @@ namespace NewHorizons.Builder.Props var dialogueTree = conversationZone.AddComponent(); - var xml = File.ReadAllText(mod.Manifest.ModFolderPath + info.xmlFile); + var xml = File.ReadAllText(Path.Combine(mod.Manifest.ModFolderPath, info.xmlFile)); var text = new TextAsset(xml); // Text assets need a name to be used with VoiceMod @@ -207,19 +207,21 @@ namespace NewHorizons.Builder.Props var name = xmlNode2.SelectSingleNode("Name").InnerText; XmlNodeList xmlText = xmlNode2.SelectNodes("Dialogue/Page"); - foreach (object Page in xmlText) + foreach (object page in xmlText) { - XmlNode pageData = (XmlNode)Page; + XmlNode pageData = (XmlNode)page; var text = pageData.InnerText; - TranslationHandler.AddDialogue(text, name); + // The text is trimmed in DialogueText constructor (_listTextBlocks), so we also need to trim it for the key + TranslationHandler.AddDialogue(text, true, name); } xmlText = xmlNode2.SelectNodes("DialogueOptionsList/DialogueOption/Text"); - foreach (object Page in xmlText) + foreach (object option in xmlText) { - XmlNode pageData = (XmlNode)Page; - var text = pageData.InnerText; - TranslationHandler.AddDialogue(text, characterName, name); + XmlNode optionData = (XmlNode)option; + var text = optionData.InnerText; + // The text is trimmed in CharacterDialogueTree.LoadXml, so we also need to trim it for the key + TranslationHandler.AddDialogue(text, true, characterName, name); } } } diff --git a/NewHorizons/Builder/Props/NomaiTextBuilder.cs b/NewHorizons/Builder/Props/NomaiTextBuilder.cs index 7743d754..981f447d 100644 --- a/NewHorizons/Builder/Props/NomaiTextBuilder.cs +++ b/NewHorizons/Builder/Props/NomaiTextBuilder.cs @@ -2,12 +2,12 @@ using NewHorizons.External.Modules; using NewHorizons.Handlers; using NewHorizons.Utility; using OWML.Common; -using Enum = System.Enum; using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; using UnityEngine; +using Enum = System.Enum; using Logger = NewHorizons.Utility.Logger; using Random = UnityEngine.Random; namespace NewHorizons.Builder.Props @@ -99,7 +99,7 @@ namespace NewHorizons.Builder.Props _preCrashRecorderPrefab.name = "Prefab_NOM_Recorder_Vessel"; _preCrashRecorderPrefab.transform.rotation = Quaternion.identity; - _trailmarkerPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/Sector_NorthPole/Sector_HangingCity/Sector_HangingCity_District2/Interactables_HangingCity_District2/Prefab_NOM_Sign"); + _trailmarkerPrefab = SearchUtilities.Find("BrittleHollow_Body/Sector_BH/Sector_NorthHemisphere/Sector_NorthPole/Sector_HangingCity/Sector_HangingCity_District2/Interactables_HangingCity_District2/Prefab_NOM_Sign").InstantiateInactive(); _trailmarkerPrefab.name = "Prefab_NOM_Trailmarker"; _trailmarkerPrefab.transform.rotation = Quaternion.identity; } @@ -108,7 +108,7 @@ namespace NewHorizons.Builder.Props { if (_scrollPrefab == null) InitPrefabs(); - var xmlPath = File.ReadAllText(mod.ModHelper.Manifest.ModFolderPath + info.xmlFile); + var xmlPath = File.ReadAllText(Path.Combine(mod.ModHelper.Manifest.ModFolderPath, info.xmlFile)); switch (info.type) { @@ -492,6 +492,9 @@ namespace NewHorizons.Builder.Props trailmarkerObject.transform.position = planetGO.transform.TransformPoint(info?.position ?? Vector3.zero); + // shrink because that is what mobius does on all trailmarkers or else they are the size of the player + trailmarkerObject.transform.localScale = Vector3.one * 0.75f; + if (info.rotation != null) { trailmarkerObject.transform.rotation = planetGO.transform.TransformRotation(Quaternion.Euler(info.rotation)); diff --git a/NewHorizons/Builder/Props/ProjectionBuilder.cs b/NewHorizons/Builder/Props/ProjectionBuilder.cs index f6cb399d..32876489 100644 --- a/NewHorizons/Builder/Props/ProjectionBuilder.cs +++ b/NewHorizons/Builder/Props/ProjectionBuilder.cs @@ -4,9 +4,12 @@ using NewHorizons.Utility; using OWML.Common; using System; using System.Collections.Generic; +using System.IO; +using System.Threading; using UnityEngine; using static NewHorizons.External.Modules.PropModule; using Logger = NewHorizons.Utility.Logger; + namespace NewHorizons.Builder.Props { public static class ProjectionBuilder @@ -89,19 +92,8 @@ namespace NewHorizons.Builder.Props // The base game ones only have 15 slides max var textures = new Texture2D[slidesCount >= 15 ? 15 : slidesCount]; - var imageLoader = slideReelObj.AddComponent(); - for (int i = 0; i < slidesCount; i++) - { - var slide = new Slide(); - var slideInfo = info.slides[i]; + var imageLoader = AddAsyncLoader(slideReelObj, mod, info.slides, ref slideCollection); - imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath); - - AddModules(slideInfo, ref slide, mod); - - slideCollection.slides[i] = slide; - } - // this variable just lets us track how many of the first 15 slides have been loaded. // this way as soon as the last one is loaded (due to async loading, this may be // slide 7, or slide 3, or whatever), we can build the slide reel texture. This allows us @@ -113,24 +105,22 @@ namespace NewHorizons.Builder.Props slideCollection.slides[index]._image = ImageUtilities.Invert(tex); // Track the first 15 to put on the slide reel object - if (index < 15) + if (index < textures.Length) { textures[index] = tex; - displaySlidesLoaded++; // threading moment - } + if (Interlocked.Increment(ref displaySlidesLoaded) == textures.Length) + { + // all textures required to build the reel's textures have been loaded + var slidesBack = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Back").GetComponent(); + var slidesFront = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Front").GetComponent(); - if (displaySlidesLoaded >= textures.Length) - { - // all textures required to build the reel's textures have been loaded - var slidesBack = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Back").GetComponent(); - var slidesFront = slideReelObj.transform.Find("Props_IP_SlideReel_7/Slides_Front").GetComponent(); - - // Now put together the textures into a 4x4 thing for the materials - var reelTexture = ImageUtilities.MakeReelTexture(textures); - slidesBack.material.mainTexture = reelTexture; - slidesBack.material.SetTexture(EmissionMap, reelTexture); - slidesFront.material.mainTexture = reelTexture; - slidesFront.material.SetTexture(EmissionMap, reelTexture); + // Now put together the textures into a 4x4 thing for the materials + var reelTexture = ImageUtilities.MakeReelTexture(textures); + slidesBack.material.mainTexture = reelTexture; + slidesBack.material.SetTexture(EmissionMap, reelTexture); + slidesFront.material.mainTexture = reelTexture; + slidesFront.material.SetTexture(EmissionMap, reelTexture); + } } } ); @@ -191,18 +181,7 @@ namespace NewHorizons.Builder.Props int slidesCount = info.slides.Length; var slideCollection = new SlideCollection(slidesCount); - var imageLoader = projectorObj.AddComponent(); - for (int i = 0; i < slidesCount; i++) - { - var slide = new Slide(); - var slideInfo = info.slides[i]; - - imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath); - - AddModules(slideInfo, ref slide, mod); - - slideCollection.slides[i] = slide; - } + var imageLoader = AddAsyncLoader(projectorObj, mod, info.slides, ref slideCollection); imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index]._image = ImageUtilities.Invert(tex); }); slideCollectionContainer.slideCollection = slideCollection; @@ -256,19 +235,7 @@ namespace NewHorizons.Builder.Props var slidesCount = slides.Length; var slideCollection = new SlideCollection(slidesCount); - - var imageLoader = g.AddComponent(); - for (int i = 0; i < slidesCount; i++) - { - var slide = new Slide(); - var slideInfo = slides[i]; - - imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath); - - AddModules(slideInfo, ref slide, mod); - - slideCollection.slides[i] = slide; - } + var imageLoader = AddAsyncLoader(g, mod, info.slides, ref slideCollection); imageLoader.imageLoadedEvent.AddListener((Texture2D tex, int index) => { slideCollection.slides[index]._image = tex; }); // attach a component to store all the data for the slides that play when a vision torch scans this target @@ -330,19 +297,8 @@ namespace NewHorizons.Builder.Props var slidesCount = slides.Length; var slideCollection = new SlideCollection(slidesCount); - var imageLoader = standingTorch.AddComponent(); - for (int i = 0; i < slidesCount; i++) - { - var slide = new Slide(); - var slideInfo = slides[i]; + var imageLoader = AddAsyncLoader(standingTorch, mod, slides, ref slideCollection); - imageLoader.pathsToLoad.Add(mod.ModHelper.Manifest.ModFolderPath + slideInfo.imagePath); - - AddModules(slideInfo, ref slide, mod); - - slideCollection.slides[i] = slide; - } - // This variable just lets us track how many of the slides have been loaded. // This way as soon as the last one is loaded (due to async loading, this may be // slide 7, or slide 3, or whatever), we can enable the vision torch. This allows us @@ -352,9 +308,8 @@ namespace NewHorizons.Builder.Props (Texture2D tex, int index) => { slideCollection.slides[index]._image = tex; - displaySlidesLoaded++; // threading moment - if (displaySlidesLoaded >= slides.Length) + if (Interlocked.Increment(ref displaySlidesLoaded) == slides.Length) { mindSlideProjector.enabled = true; visionBeamEffect.SetActive(true); @@ -378,6 +333,31 @@ namespace NewHorizons.Builder.Props return standingTorch; } + private static ImageUtilities.AsyncImageLoader AddAsyncLoader(GameObject gameObject, IModBehaviour mod, SlideInfo[] slides, ref SlideCollection slideCollection) + { + var imageLoader = gameObject.AddComponent(); + for (int i = 0; i < slides.Length; i++) + { + var slide = new Slide(); + var slideInfo = slides[i]; + + if (string.IsNullOrEmpty(slideInfo.imagePath)) + { + imageLoader.imageLoadedEvent?.Invoke(Texture2D.blackTexture, i); + } + else + { + imageLoader.PathsToLoad.Add((i, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, slideInfo.imagePath))); + } + + AddModules(slideInfo, ref slide, mod); + + slideCollection.slides[i] = slide; + } + + return imageLoader; + } + private static void AddModules(PropModule.SlideInfo slideInfo, ref Slide slide, IModBehaviour mod) { var modules = new List(); diff --git a/NewHorizons/Builder/Props/PropBuildManager.cs b/NewHorizons/Builder/Props/PropBuildManager.cs index 8f442041..bda9684d 100644 --- a/NewHorizons/Builder/Props/PropBuildManager.cs +++ b/NewHorizons/Builder/Props/PropBuildManager.cs @@ -109,20 +109,6 @@ namespace NewHorizons.Builder.Props } } } - if (config.Props.reveal != null) - { - foreach (var revealInfo in config.Props.reveal) - { - try - { - RevealBuilder.Make(go, sector, revealInfo, mod); - } - catch (Exception ex) - { - Logger.LogError($"Couldn't make reveal location [{revealInfo.reveals}] for [{go.name}]:\n{ex}"); - } - } - } if (config.Props.entryLocation != null) { foreach (var entryLocationInfo in config.Props.entryLocation) @@ -207,13 +193,6 @@ namespace NewHorizons.Builder.Props } } } - if (config.Props.audioVolumes != null) - { - foreach (var audioVolume in config.Props.audioVolumes) - { - AudioVolumeBuilder.Make(go, sector, audioVolume, mod); - } - } if (config.Props.signals != null) { foreach (var signal in config.Props.signals) diff --git a/NewHorizons/Builder/Props/QuantumBuilder.cs b/NewHorizons/Builder/Props/QuantumBuilder.cs index 16484b8f..3dd81c3b 100644 --- a/NewHorizons/Builder/Props/QuantumBuilder.cs +++ b/NewHorizons/Builder/Props/QuantumBuilder.cs @@ -1,5 +1,5 @@ using HarmonyLib; -using NewHorizons.Components; +using NewHorizons.Components.Quantum; using NewHorizons.External.Configs; using NewHorizons.External.Modules; using NewHorizons.Utility; diff --git a/NewHorizons/Builder/Props/RaftBuilder.cs b/NewHorizons/Builder/Props/RaftBuilder.cs index 6ad6277f..2e8c6a15 100644 --- a/NewHorizons/Builder/Props/RaftBuilder.cs +++ b/NewHorizons/Builder/Props/RaftBuilder.cs @@ -1,4 +1,4 @@ -using NewHorizons.Components; +using NewHorizons.Components.Volumes; using NewHorizons.External.Modules; using NewHorizons.Handlers; using NewHorizons.Utility; diff --git a/NewHorizons/Builder/Props/ScatterBuilder.cs b/NewHorizons/Builder/Props/ScatterBuilder.cs index 26af9d33..a92ebd9b 100644 --- a/NewHorizons/Builder/Props/ScatterBuilder.cs +++ b/NewHorizons/Builder/Props/ScatterBuilder.cs @@ -3,6 +3,8 @@ using NewHorizons.External.Modules; using NewHorizons.Utility; using OWML.Common; using System; +using System.Collections.Generic; +using System.Linq; using UnityEngine; using Object = UnityEngine.Object; using Random = UnityEngine.Random; @@ -19,13 +21,20 @@ namespace NewHorizons.Builder.Props { var heightMap = config.HeightMap; - var area = 4f * Mathf.PI * radius * radius; + var makeFibonacciSphere = scatterInfo.Any(x => x.preventOverlap); - // To not use more than 0.5GB of RAM while doing this - // Works up to planets with 575 radius before capping - var numPoints = Math.Min((int)(area * 10), 41666666); + List points = new(); - var points = RandomUtility.FibonacciSphere(numPoints); + if (makeFibonacciSphere) + { + var area = 4f * Mathf.PI * radius * radius; + + // To not use more than 0.5GB of RAM while doing this + // Works up to planets with 575 radius before capping + var numPoints = Math.Min((int)(area * 10), 41666666); + + points = RandomUtility.FibonacciSphere(numPoints); + } Texture2D heightMapTexture = null; if (heightMap != null) @@ -55,13 +64,29 @@ namespace NewHorizons.Builder.Props GameObject prefab; if (propInfo.assetBundle != null) prefab = AssetBundleUtilities.LoadPrefab(propInfo.assetBundle, propInfo.path, mod); else prefab = SearchUtilities.Find(propInfo.path); + + // Run all the make detail stuff on it early and just copy it over and over instead + var detailInfo = new PropModule.DetailInfo() + { + scale = propInfo.scale, + keepLoaded = propInfo.keepLoaded + }; + var scatterPrefab = DetailBuilder.Make(go, sector, prefab, detailInfo); + for (int i = 0; i < propInfo.count; i++) { - // Failsafe - if (points.Count == 0) break; - - var randomInd = (int)Random.Range(0, points.Count - 1); - var point = points[randomInd]; + Vector3 point; + if (propInfo.preventOverlap) + { + if (points.Count == 0) break; + var randomInd = Random.Range(0, points.Count - 1); + point = points[randomInd]; + points.QuickRemoveAt(randomInd); + } + else + { + point = Random.onUnitSphere; + } var height = radius; if (heightMapTexture != null) @@ -92,13 +117,11 @@ namespace NewHorizons.Builder.Props point = Quaternion.Euler(90, 0, 0) * point; } - var detailInfo = new PropModule.DetailInfo() - { - position = point.normalized * height, - scale = propInfo.scale, - alignToNormal = true - }; - var prop = DetailBuilder.Make(go, sector, prefab, detailInfo); + var prop = scatterPrefab.InstantiateInactive(); + prop.transform.SetParent(sector?.transform ?? go.transform); + prop.transform.localPosition = go.transform.TransformPoint(point * height); + var up = go.transform.InverseTransformPoint(prop.transform.position).normalized; + prop.transform.rotation = Quaternion.FromToRotation(Vector3.up, up); if (propInfo.offset != null) prop.transform.localPosition += prop.transform.TransformVector(propInfo.offset); if (propInfo.rotation != null) prop.transform.rotation *= Quaternion.Euler(propInfo.rotation); @@ -106,9 +129,10 @@ namespace NewHorizons.Builder.Props // Rotate around normal prop.transform.localRotation *= Quaternion.AngleAxis(Random.Range(0, 360), Vector3.up); - points.QuickRemoveAt(randomInd); - if (points.Count == 0) return; + prop.SetActive(true); } + + GameObject.Destroy(scatterPrefab); } } } diff --git a/NewHorizons/Builder/ShipLog/EntryLocationBuilder.cs b/NewHorizons/Builder/ShipLog/EntryLocationBuilder.cs index 4efd3ca1..b35ec450 100644 --- a/NewHorizons/Builder/ShipLog/EntryLocationBuilder.cs +++ b/NewHorizons/Builder/ShipLog/EntryLocationBuilder.cs @@ -1,4 +1,4 @@ -using NewHorizons.External.Modules; +using NewHorizons.External.Modules; using OWML.Common; using System.Collections.Generic; using UnityEngine; @@ -15,6 +15,7 @@ namespace NewHorizons.Builder.ShipLog entryLocationGameObject.transform.position = go.transform.TransformPoint(info.position ?? Vector3.zero); ShipLogEntryLocation newLocation = entryLocationGameObject.AddComponent(); newLocation._entryID = info.id; + newLocation._outerFogWarpVolume = go.GetComponentInChildren(); newLocation._isWithinCloakField = info.cloaked; _locationsToInitialize.Add(newLocation); entryLocationGameObject.SetActive(true); diff --git a/NewHorizons/Builder/ShipLog/MapModeBuilder.cs b/NewHorizons/Builder/ShipLog/MapModeBuilder.cs index b377bf51..0eaf91aa 100644 --- a/NewHorizons/Builder/ShipLog/MapModeBuilder.cs +++ b/NewHorizons/Builder/ShipLog/MapModeBuilder.cs @@ -1,4 +1,3 @@ -using NewHorizons.Components; using NewHorizons.External.Modules; using NewHorizons.Handlers; using NewHorizons.Utility; @@ -9,6 +8,8 @@ using NewHorizons.External.Modules.VariableSize; using UnityEngine; using UnityEngine.UI; using Logger = NewHorizons.Utility.Logger; +using NewHorizons.Components.ShipLog; + namespace NewHorizons.Builder.ShipLog { public static class MapModeBuilder @@ -219,7 +220,7 @@ namespace NewHorizons.Builder.ShipLog foreach (NewHorizonsBody body in bodies) { - if (body.Config.ShipLog?.mapMode?.manualNavigationPosition == null) continue; + if (body.Config.ShipLog?.mapMode?.manualNavigationPosition == null && body.Config.ShipLog?.mapMode?.details == null) continue; // Sometimes they got other names idk var name = body.Config.name.Replace(" ", ""); @@ -283,6 +284,7 @@ namespace NewHorizons.Builder.ShipLog { gameObject.transform.localScale = Vector3.one * body.Config.ShipLog.mapMode.scale; } + MakeDetails(body, gameObject.transform, greyScaleMaterial); } } } diff --git a/NewHorizons/Builder/ShipLog/RevealBuilder.cs b/NewHorizons/Builder/ShipLog/RevealBuilder.cs index 43133771..3bfcdd69 100644 --- a/NewHorizons/Builder/ShipLog/RevealBuilder.cs +++ b/NewHorizons/Builder/ShipLog/RevealBuilder.cs @@ -7,18 +7,18 @@ namespace NewHorizons.Builder.ShipLog { public static class RevealBuilder { - public static void Make(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + public static void Make(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { var newRevealGO = MakeGameObject(go, sector, info, mod); switch (info.revealOn) { - case PropModule.RevealInfo.RevealVolumeType.Enter: + case VolumesModule.RevealVolumeInfo.RevealVolumeType.Enter: MakeTrigger(newRevealGO, sector, info, mod); break; - case PropModule.RevealInfo.RevealVolumeType.Observe: + case VolumesModule.RevealVolumeInfo.RevealVolumeType.Observe: MakeObservable(newRevealGO, sector, info, mod); break; - case PropModule.RevealInfo.RevealVolumeType.Snapshot: + case VolumesModule.RevealVolumeInfo.RevealVolumeType.Snapshot: MakeSnapshot(newRevealGO, sector, info, mod); break; default: @@ -28,7 +28,7 @@ namespace NewHorizons.Builder.ShipLog newRevealGO.SetActive(true); } - private static SphereShape MakeShape(GameObject go, PropModule.RevealInfo info, Shape.CollisionMode collisionMode) + private static SphereShape MakeShape(GameObject go, VolumesModule.RevealVolumeInfo info, Shape.CollisionMode collisionMode) { SphereShape newShape = go.AddComponent(); newShape.radius = info.radius; @@ -36,16 +36,36 @@ namespace NewHorizons.Builder.ShipLog return newShape; } - private static GameObject MakeGameObject(GameObject planetGO, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static GameObject MakeGameObject(GameObject planetGO, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { GameObject revealTriggerVolume = new GameObject("Reveal Volume (" + info.revealOn + ")"); revealTriggerVolume.SetActive(false); revealTriggerVolume.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.rename)) + { + revealTriggerVolume.name = info.rename; + } + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + revealTriggerVolume.transform.parent = newParent; + } + else + { + Logger.LogWarning($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + revealTriggerVolume.transform.position = planetGO.transform.TransformPoint(info.position ?? Vector3.zero); + return revealTriggerVolume; } - private static void MakeTrigger(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static void MakeTrigger(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { var shape = MakeShape(go, info, Shape.CollisionMode.Volume); @@ -65,7 +85,7 @@ namespace NewHorizons.Builder.ShipLog } } - private static void MakeObservable(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static void MakeObservable(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { go.layer = LayerMask.NameToLayer("Interactible"); @@ -96,7 +116,7 @@ namespace NewHorizons.Builder.ShipLog } } - private static void MakeSnapshot(GameObject go, Sector sector, PropModule.RevealInfo info, IModBehaviour mod) + private static void MakeSnapshot(GameObject go, Sector sector, VolumesModule.RevealVolumeInfo info, IModBehaviour mod) { var shape = MakeShape(go, info, Shape.CollisionMode.Manual); diff --git a/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs b/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs index a5c18d78..9e82ae8b 100644 --- a/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs +++ b/NewHorizons/Builder/ShipLog/RumorModeBuilder.cs @@ -4,6 +4,7 @@ using NewHorizons.Handlers; using NewHorizons.Utility; using System; using System.Collections.Generic; +using System.IO; using System.Xml.Linq; using UnityEngine; using Logger = NewHorizons.Utility.Logger; @@ -53,7 +54,7 @@ namespace NewHorizons.Builder.ShipLog public static void AddBodyToShipLog(ShipLogManager manager, NewHorizonsBody body) { string systemName = body.Config.starSystem; - XElement astroBodyFile = XElement.Load(body.Mod.ModHelper.Manifest.ModFolderPath + "/" + body.Config.ShipLog.xmlFile); + XElement astroBodyFile = XElement.Load(Path.Combine(body.Mod.ModHelper.Manifest.ModFolderPath, body.Config.ShipLog.xmlFile)); XElement astroBodyId = astroBodyFile.Element("ID"); if (astroBodyId == null) { diff --git a/NewHorizons/Builder/Props/AudioVolumeBuilder.cs b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs similarity index 68% rename from NewHorizons/Builder/Props/AudioVolumeBuilder.cs rename to NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs index 8e45efc8..bbb201d8 100644 --- a/NewHorizons/Builder/Props/AudioVolumeBuilder.cs +++ b/NewHorizons/Builder/Volumes/AudioVolumeBuilder.cs @@ -9,16 +9,35 @@ using System.Threading.Tasks; using UnityEngine; using Logger = NewHorizons.Utility.Logger; -namespace NewHorizons.Builder.Props +namespace NewHorizons.Builder.Volumes { public static class AudioVolumeBuilder { - public static AudioVolume Make(GameObject planetGO, Sector sector, PropModule.AudioVolumeInfo info, IModBehaviour mod) + public static AudioVolume Make(GameObject planetGO, Sector sector, VolumesModule.AudioVolumeInfo info, IModBehaviour mod) { var go = new GameObject("AudioVolume"); go.SetActive(false); go.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.rename)) + { + go.name = info.rename; + } + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + go.transform.parent = newParent; + } + else + { + Logger.LogWarning($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + go.transform.position = planetGO.transform.TransformPoint(info.position != null ? (Vector3)info.position : Vector3.zero); go.layer = LayerMask.NameToLayer("AdvancedEffectVolume"); @@ -26,7 +45,7 @@ namespace NewHorizons.Builder.Props var owAudioSource = go.AddComponent(); owAudioSource._audioSource = audioSource; - owAudioSource.loop = true; + owAudioSource.loop = info.loop; owAudioSource.SetTrack((OWAudioMixer.TrackName)Enum.Parse(typeof(OWAudioMixer.TrackName), Enum.GetName(typeof(AudioMixerTrackName), info.track))); AudioUtilities.SetAudioClip(owAudioSource, info.audio, mod); diff --git a/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs new file mode 100644 index 00000000..705570d5 --- /dev/null +++ b/NewHorizons/Builder/Volumes/HazardVolumeBuilder.cs @@ -0,0 +1,60 @@ +using NewHorizons.External.Modules; +using OWML.Common; +using OWML.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.Builder.Volumes +{ + public static class HazardVolumeBuilder + { + public static HazardVolume Make(GameObject planetGO, Sector sector, OWRigidbody owrb, VolumesModule.HazardVolumeInfo info, IModBehaviour mod) + { + var go = new GameObject("HazardVolume"); + go.SetActive(false); + + go.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.rename)) + { + go.name = info.rename; + } + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + go.transform.parent = newParent; + } + else + { + Logger.LogWarning($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + go.transform.position = planetGO.transform.TransformPoint(info.position != null ? (Vector3)info.position : Vector3.zero); + go.layer = LayerMask.NameToLayer("BasicEffectVolume"); + + var shape = go.AddComponent(); + shape.radius = info.radius; + + var owTriggerVolume = go.AddComponent(); + owTriggerVolume._shape = shape; + + var hazardVolume = go.AddComponent(); + hazardVolume._attachedBody = owrb; + hazardVolume._type = EnumUtils.Parse(info.type.ToString(), HazardVolume.HazardType.GENERAL); + hazardVolume._damagePerSecond = info.damagePerSecond; + hazardVolume._firstContactDamageType = EnumUtils.Parse(info.firstContactDamageType.ToString(), InstantDamageType.Impact); + hazardVolume._firstContactDamage = info.firstContactDamage; + + go.SetActive(true); + + return hazardVolume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs new file mode 100644 index 00000000..dab95143 --- /dev/null +++ b/NewHorizons/Builder/Volumes/NotificationVolumeBuilder.cs @@ -0,0 +1,61 @@ +using NewHorizons.External.Modules; +using NewHorizons.Utility; +using OWML.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; +using NHNotificationVolume = NewHorizons.Components.Volumes.NotificationVolume; + +namespace NewHorizons.Builder.Volumes +{ + public static class NotificationVolumeBuilder + { + public static NHNotificationVolume Make(GameObject planetGO, Sector sector, VolumesModule.NotificationVolumeInfo info, IModBehaviour mod) + { + var go = new GameObject("NotificationVolume"); + go.SetActive(false); + + go.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.rename)) + { + go.name = info.rename; + } + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + go.transform.parent = newParent; + } + else + { + Logger.LogWarning($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + go.transform.position = planetGO.transform.TransformPoint(info.position != null ? (Vector3)info.position : Vector3.zero); + go.layer = LayerMask.NameToLayer("BasicEffectVolume"); + + var shape = go.AddComponent(); + shape.radius = info.radius; + + var owTriggerVolume = go.AddComponent(); + owTriggerVolume._shape = shape; + + var notificationVolume = go.AddComponent(); + notificationVolume.SetTarget(info.target); + if (info.entryNotification != null) notificationVolume.SetEntryNotification(info.entryNotification.displayMessage, info.entryNotification.duration); + if (info.exitNotification != null) notificationVolume.SetExitNotification(info.exitNotification.displayMessage, info.exitNotification.duration); + + go.SetActive(true); + + return notificationVolume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/VolumeBuilder.cs b/NewHorizons/Builder/Volumes/VolumeBuilder.cs new file mode 100644 index 00000000..966c3c25 --- /dev/null +++ b/NewHorizons/Builder/Volumes/VolumeBuilder.cs @@ -0,0 +1,51 @@ +using NewHorizons.Components; +using NewHorizons.External.Modules; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.Builder.Volumes +{ + public static class VolumeBuilder + { + public static TVolume Make(GameObject planetGO, Sector sector, VolumesModule.VolumeInfo info) where TVolume : MonoBehaviour //Could be BaseVolume but I need to create vanilla volumes too. + { + var go = new GameObject(typeof(TVolume).Name); + go.SetActive(false); + + go.transform.parent = sector?.transform ?? planetGO.transform; + + if (!string.IsNullOrEmpty(info.rename)) + { + go.name = info.rename; + } + + if (!string.IsNullOrEmpty(info.parentPath)) + { + var newParent = planetGO.transform.Find(info.parentPath); + if (newParent != null) + { + go.transform.parent = newParent; + } + else + { + Logger.LogWarning($"Cannot find parent object at path: {planetGO.name}/{info.parentPath}"); + } + } + + go.transform.position = planetGO.transform.TransformPoint(info.position != null ? (Vector3)info.position : Vector3.zero); + go.layer = LayerMask.NameToLayer("BasicEffectVolume"); + + var shape = go.AddComponent(); + shape.radius = info.radius; + + var owTriggerVolume = go.AddComponent(); + owTriggerVolume._shape = shape; + + var volume = go.AddComponent(); + + go.SetActive(true); + + return volume; + } + } +} diff --git a/NewHorizons/Builder/Volumes/VolumesBuildManager.cs b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs new file mode 100644 index 00000000..9b13db4f --- /dev/null +++ b/NewHorizons/Builder/Volumes/VolumesBuildManager.cs @@ -0,0 +1,83 @@ +using NewHorizons.Builder.Body; +using NewHorizons.Builder.ShipLog; +using NewHorizons.Builder.Volumes; +using NewHorizons.Components.Volumes; +using NewHorizons.External.Configs; +using OWML.Common; +using System; +using System.Collections.Generic; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.Builder.Volumes +{ + public static class VolumesBuildManager + { + public static void Make(GameObject go, Sector sector, OWRigidbody planetBody, PlanetConfig config, IModBehaviour mod) + { + if (config.Volumes.revealVolumes != null) + { + foreach (var revealInfo in config.Volumes.revealVolumes) + { + try + { + RevealBuilder.Make(go, sector, revealInfo, mod); + } + catch (Exception ex) + { + Logger.LogError($"Couldn't make reveal location [{revealInfo.reveals}] for [{go.name}]:\n{ex}"); + } + } + } + if (config.Volumes.audioVolumes != null) + { + foreach (var audioVolume in config.Volumes.audioVolumes) + { + AudioVolumeBuilder.Make(go, sector, audioVolume, mod); + } + } + if (config.Volumes.notificationVolumes != null) + { + foreach (var notificationVolume in config.Volumes.notificationVolumes) + { + NotificationVolumeBuilder.Make(go, sector, notificationVolume, mod); + } + } + if (config.Volumes.hazardVolumes != null) + { + foreach (var hazardVolume in config.Volumes.hazardVolumes) + { + HazardVolumeBuilder.Make(go, sector, planetBody, hazardVolume, mod); + } + } + if (config.Volumes.mapRestrictionVolumes != null) + { + foreach (var mapRestrictionVolume in config.Volumes.mapRestrictionVolumes) + { + VolumeBuilder.Make(go, sector, mapRestrictionVolume); + } + } + if (config.Volumes.interferenceVolumes != null) + { + foreach (var interferenceVolume in config.Volumes.interferenceVolumes) + { + VolumeBuilder.Make(go, sector, interferenceVolume); + } + } + if (config.Volumes.reverbVolumes != null) + { + foreach (var reverbVolume in config.Volumes.reverbVolumes) + { + VolumeBuilder.Make(go, sector, reverbVolume); + } + } + if (config.Volumes.insulatingVolumes != null) + { + foreach (var insulatingVolume in config.Volumes.insulatingVolumes) + { + VolumeBuilder.Make(go, sector, insulatingVolume); + } + } + } + } +} diff --git a/NewHorizons/Components/MapSatelliteOrbitFix.cs b/NewHorizons/Components/Fixers/MapSatelliteOrbitFix.cs similarity index 94% rename from NewHorizons/Components/MapSatelliteOrbitFix.cs rename to NewHorizons/Components/Fixers/MapSatelliteOrbitFix.cs index 1e8e0778..4b1a9b30 100644 --- a/NewHorizons/Components/MapSatelliteOrbitFix.cs +++ b/NewHorizons/Components/Fixers/MapSatelliteOrbitFix.cs @@ -1,7 +1,7 @@ using NewHorizons.Builder.General; using NewHorizons.External.Configs; using UnityEngine; -namespace NewHorizons.Components +namespace NewHorizons.Components.Fixers { public class MapSatelliteOrbitFix : MonoBehaviour { diff --git a/NewHorizons/Components/NHMultiStateQuantumObject.cs b/NewHorizons/Components/Quantum/NHMultiStateQuantumObject.cs similarity index 54% rename from NewHorizons/Components/NHMultiStateQuantumObject.cs rename to NewHorizons/Components/Quantum/NHMultiStateQuantumObject.cs index 6c73cef5..4abd57b1 100644 --- a/NewHorizons/Components/NHMultiStateQuantumObject.cs +++ b/NewHorizons/Components/Quantum/NHMultiStateQuantumObject.cs @@ -7,60 +7,60 @@ using UnityEngine; using Logger = NewHorizons.Utility.Logger; -namespace NewHorizons.Components +namespace NewHorizons.Components.Quantum { public class NHMultiStateQuantumObject : MultiStateQuantumObject { - - public override bool ChangeQuantumState(bool skipInstantVisibilityCheck) - { - for (int i = 0; i < _prerequisiteObjects.Length; i++) - { - if (!_prerequisiteObjects[i].HasCollapsed()) - { - return false; - } - } - int stateIndex = _stateIndex; - if (_stateIndex == -1 && _initialState != -1) - { - _stateIndex = _initialState; - } - else if (_sequential) - { - _stateIndex = (_reverse ? (_stateIndex - 1) : (_stateIndex + 1)); - if (_loop) - { - if (_stateIndex < 0) - { - _stateIndex = _states.Length - 1; - } - else if (_stateIndex > _states.Length - 1) - { - _stateIndex = 0; - } - } - else - { - _stateIndex = Mathf.Clamp(_stateIndex, 0, _states.Length - 1); - } - } - else - { + + public override bool ChangeQuantumState(bool skipInstantVisibilityCheck) + { + for (int i = 0; i < _prerequisiteObjects.Length; i++) + { + if (!_prerequisiteObjects[i].HasCollapsed()) + { + return false; + } + } + int stateIndex = _stateIndex; + if (_stateIndex == -1 && _initialState != -1) + { + _stateIndex = _initialState; + } + else if (_sequential) + { + _stateIndex = _reverse ? _stateIndex - 1 : _stateIndex + 1; + if (_loop) + { + if (_stateIndex < 0) + { + _stateIndex = _states.Length - 1; + } + else if (_stateIndex > _states.Length - 1) + { + _stateIndex = 0; + } + } + else + { + _stateIndex = Mathf.Clamp(_stateIndex, 0, _states.Length - 1); + } + } + else + { // TODO: perform this roll for number of states, each time adding the selected state to the end of a list and removing it from the source list // this gets us a randomly ordered list that respects states' probability // then we can sequentially attempt collapsing to them, checking at each state whether the new state is invalid due to the player being able to see it, according to this: // // if (!((!IsPlayerEntangled()) ? (CheckIllumination() ? CheckVisibilityInstantly() : CheckPointInside(Locator.GetPlayerCamera().transform.position)) : CheckIllumination())) - // { - // return true; // this is a valid state - // } + // { + // return true; // this is a valid state + // } // List indices = new List(); for (var i = 0; i < _states.Length; i++) if (i != stateIndex) indices.Add(i); - + var previousIndex = stateIndex; do @@ -69,31 +69,31 @@ namespace NewHorizons.Components _stateIndex = RollState(stateIndex, indices); if (previousIndex >= 0 && previousIndex < _states.Length) _states[previousIndex].SetVisible(visible: false); _states[_stateIndex].SetVisible(visible: true); - + Logger.LogVerbose($"MultiStateQuantumObject - Trying to change state {_stateIndex}"); indices.Remove(_stateIndex); } while (!CurrentStateIsValid() && indices.Count > 0); - } + } var stateIndexIsValid = stateIndex >= 0 && stateIndex < _states.Length; - if (stateIndexIsValid) _states[stateIndex].SetVisible(visible: false); + if (stateIndexIsValid) _states[stateIndex].SetVisible(visible: false); - _states[_stateIndex].SetVisible(visible: true); + _states[_stateIndex].SetVisible(visible: true); if (!CurrentStateIsValid() && stateIndexIsValid) { - _states[_stateIndex].SetVisible(visible: false); - _states[stateIndex] .SetVisible(visible: true); - _stateIndex = stateIndex; + _states[_stateIndex].SetVisible(visible: false); + _states[stateIndex].SetVisible(visible: true); + _stateIndex = stateIndex; return false; } - if (_sequential && !_loop && _stateIndex == _states.Length - 1) - { - SetActivation(active: false); - } - return true; - } + if (_sequential && !_loop && _stateIndex == _states.Length - 1) + { + SetActivation(active: false); + } + return true; + } public bool CurrentStateIsValid() { @@ -102,20 +102,20 @@ namespace NewHorizons.Components var visibility = CheckVisibilityInstantly(); var playerInside = CheckPointInside(Locator.GetPlayerCamera().transform.position); - var isVisible = + var isVisible = isPlayerEntangled ? illumination - : ( - illumination + : + illumination ? visibility : playerInside - ); + ; return !isVisible; } public int RollState(int excludeIndex, List indices) - { + { var stateIndex = excludeIndex; // this function constructs a sort of segmented range: @@ -132,32 +132,32 @@ namespace NewHorizons.Components // // the second for looop uses num3 and num4 to figure out which segment num2 landed in - int num = 0; - foreach (int j in indices) - { - if (j != stateIndex) - { - _probabilities[j] = _states[j].GetProbability(); - num += _probabilities[j]; - } - } - int num2 = UnityEngine.Random.Range(0, num); - int num3 = 0; - int num4 = 0; - foreach (int k in indices) - { - if (k != stateIndex) - { - num3 = num4; - num4 += _probabilities[k]; - if (_probabilities[k] > 0 && num2 >= num3 && num2 < num4) - { - return k; - } - } - } + int num = 0; + foreach (int j in indices) + { + if (j != stateIndex) + { + _probabilities[j] = _states[j].GetProbability(); + num += _probabilities[j]; + } + } + int num2 = UnityEngine.Random.Range(0, num); + int num3 = 0; + int num4 = 0; + foreach (int k in indices) + { + if (k != stateIndex) + { + num3 = num4; + num4 += _probabilities[k]; + if (_probabilities[k] > 0 && num2 >= num3 && num2 < num4) + { + return k; + } + } + } - return indices[indices.Count-1]; + return indices[indices.Count - 1]; } } } diff --git a/NewHorizons/Components/QuantumPlanet.cs b/NewHorizons/Components/Quantum/QuantumPlanet.cs similarity index 98% rename from NewHorizons/Components/QuantumPlanet.cs rename to NewHorizons/Components/Quantum/QuantumPlanet.cs index 2b853b76..568a364b 100644 --- a/NewHorizons/Components/QuantumPlanet.cs +++ b/NewHorizons/Components/Quantum/QuantumPlanet.cs @@ -9,7 +9,7 @@ using System.Linq; using UnityEngine; using Logger = NewHorizons.Utility.Logger; using Random = UnityEngine.Random; -namespace NewHorizons.Components +namespace NewHorizons.Components.Quantum { public class QuantumPlanet : QuantumObject { @@ -162,9 +162,9 @@ namespace NewHorizons.Components private void OnPlayerBlink() { - if (base.IsVisible()) + if (IsVisible()) { - base.Collapse(true); + Collapse(true); } } diff --git a/NewHorizons/Components/RingOpacityController.cs b/NewHorizons/Components/RingOpacityController.cs index fde75452..df707a33 100644 --- a/NewHorizons/Components/RingOpacityController.cs +++ b/NewHorizons/Components/RingOpacityController.cs @@ -1,3 +1,4 @@ +using NewHorizons.Components.Volumes; using NewHorizons.External.Modules.VariableSize; using NewHorizons.Utility; using UnityEngine; diff --git a/NewHorizons/Components/ShipLogDetail.cs b/NewHorizons/Components/ShipLog/ShipLogDetail.cs similarity index 92% rename from NewHorizons/Components/ShipLogDetail.cs rename to NewHorizons/Components/ShipLog/ShipLogDetail.cs index a2ce809d..3852ff88 100644 --- a/NewHorizons/Components/ShipLogDetail.cs +++ b/NewHorizons/Components/ShipLog/ShipLogDetail.cs @@ -1,8 +1,8 @@ -using NewHorizons.External.Modules; +using NewHorizons.External.Modules; using UnityEngine; using UnityEngine.UI; using Logger = NewHorizons.Utility.Logger; -namespace NewHorizons.Components +namespace NewHorizons.Components.ShipLog { public class ShipLogDetail : MonoBehaviour { @@ -51,7 +51,7 @@ namespace NewHorizons.Components private void SetGreyScale(bool greyScale) { - _revealedImage.material = (greyScale ? _greyScaleMaterial : null); + _revealedImage.material = greyScale ? _greyScaleMaterial : null; } } } \ No newline at end of file diff --git a/NewHorizons/Components/ShipLogStarChartMode.cs b/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs similarity index 95% rename from NewHorizons/Components/ShipLogStarChartMode.cs rename to NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs index a5d9dad0..a7b972ed 100644 --- a/NewHorizons/Components/ShipLogStarChartMode.cs +++ b/NewHorizons/Components/ShipLog/ShipLogStarChartMode.cs @@ -6,7 +6,7 @@ using System.Linq; using UnityEngine; using UnityEngine.UI; using Logger = NewHorizons.Utility.Logger; -namespace NewHorizons.Components +namespace NewHorizons.Components.ShipLog { public class ShipLogStarChartMode : ShipLogMode { @@ -41,7 +41,7 @@ namespace NewHorizons.Components public override void Initialize(ScreenPromptList centerPromptList, ScreenPromptList upperRightPromptList, OWAudioSource oneShotSource) { - root = base.transform.Find("ScaleRoot/PanRoot"); + root = transform.Find("ScaleRoot/PanRoot"); _oneShotSource = oneShotSource; _centerPromptList = centerPromptList; @@ -101,11 +101,11 @@ namespace NewHorizons.Components if (_cardTemplate == null) { var panRoot = SearchUtilities.Find("Ship_Body/Module_Cabin/Systems_Cabin/ShipLogPivot/ShipLog/ShipLogPivot/ShipLogCanvas/DetectiveMode/ScaleRoot/PanRoot"); - _cardTemplate = GameObject.Instantiate(panRoot.GetComponentInChildren().gameObject); + _cardTemplate = Instantiate(panRoot.GetComponentInChildren().gameObject); _cardTemplate.SetActive(false); } - var newCard = GameObject.Instantiate(_cardTemplate, parent); + var newCard = Instantiate(_cardTemplate, parent); var textComponent = newCard.transform.Find("EntryCardRoot/NameBackground/Name").GetComponent(); var name = UniqueIDToName(uniqueID); @@ -164,7 +164,7 @@ namespace NewHorizons.Components public override void EnterMode(string entryID = "", List revealQueue = null) { - base.gameObject.SetActive(true); + gameObject.SetActive(true); Locator.GetPromptManager().AddScreenPrompt(_detectiveModePrompt, _upperRightPromptList, TextAnchor.MiddleRight, -1, true); Locator.GetPromptManager().AddScreenPrompt(_targetSystemPrompt, _centerPromptList, TextAnchor.MiddleCenter, -1, true); @@ -172,7 +172,7 @@ namespace NewHorizons.Components public override void ExitMode() { - base.gameObject.SetActive(false); + gameObject.SetActive(false); Locator.GetPromptManager().RemoveScreenPrompt(_detectiveModePrompt); Locator.GetPromptManager().RemoveScreenPrompt(_targetSystemPrompt); @@ -223,7 +223,7 @@ namespace NewHorizons.Components if (oldIndex != _cardIndex) { - _oneShotSource.PlayOneShot(global::AudioType.ShipLogMoveBetweenPlanets, 1f); + _oneShotSource.PlayOneShot(AudioType.ShipLogMoveBetweenPlanets, 1f); _startPanTime = Time.unscaledTime; _startPanPos = _panRootPos; _panDuration = 0.25f; @@ -297,7 +297,7 @@ namespace NewHorizons.Components { if (_warpNotificationData != null) NotificationManager.SharedInstance.UnpinNotification(_warpNotificationData); if (_target == null) return; - if (playSound) _oneShotSource.PlayOneShot(global::AudioType.ShipLogMarkLocation, 1f); + if (playSound) _oneShotSource.PlayOneShot(AudioType.ShipLogMarkLocation, 1f); _target.SetMarkedOnHUD(false); _target = null; } diff --git a/NewHorizons/Components/SizeControllers/StarEvolutionController.cs b/NewHorizons/Components/SizeControllers/StarEvolutionController.cs index ce904cca..5e8a1b2e 100644 --- a/NewHorizons/Components/SizeControllers/StarEvolutionController.cs +++ b/NewHorizons/Components/SizeControllers/StarEvolutionController.cs @@ -1,5 +1,6 @@ using NewHorizons.Builder.Body; using NewHorizons.Components.Orbital; +using NewHorizons.Components.Stars; using NewHorizons.External.Modules.VariableSize; using NewHorizons.Handlers; using NewHorizons.Utility; @@ -286,7 +287,9 @@ namespace NewHorizons.Components.SizeControllers { _stellarRemnant.SetActive(true); var remnantStarController = _stellarRemnant.GetComponentInChildren(); - if (remnantStarController != null) StarLightController.AddStar(remnantStarController); + if (remnantStarController != null) SunLightEffectsController.AddStar(remnantStarController); + var remnantStarLight = _stellarRemnant.FindChild("SunLight"); + if (remnantStarLight != null) SunLightEffectsController.AddStarLight(remnantStarLight.GetComponent()); } if (Time.time > _supernovaStartTime + supernovaTime) @@ -299,7 +302,8 @@ namespace NewHorizons.Components.SizeControllers private void DisableStar(bool start = false) { - if (controller != null) StarLightController.RemoveStar(controller); + if (controller != null) SunLightEffectsController.RemoveStar(controller); + if (!isProxy) SunLightEffectsController.RemoveStarLight(gameObject.FindChild("SunLight").GetComponent()); if (_stellarRemnant != null) { diff --git a/NewHorizons/Components/StarLightController.cs b/NewHorizons/Components/StarLightController.cs deleted file mode 100644 index 55a79074..00000000 --- a/NewHorizons/Components/StarLightController.cs +++ /dev/null @@ -1,129 +0,0 @@ -using NewHorizons.Builder.Atmosphere; -using System.Collections.Generic; -using UnityEngine; -using Logger = NewHorizons.Utility.Logger; -namespace NewHorizons.Components -{ - [RequireComponent(typeof(SunLightController))] - [RequireComponent(typeof(SunLightParamUpdater))] - public class StarLightController : MonoBehaviour - { - private static readonly int SunIntensity = Shader.PropertyToID("_SunIntensity"); - private static readonly float hearthSunDistanceSqr = 8593 * 8593; - - public static StarLightController Instance { get; private set; } - - private List _stars = new List(); - private StarController _activeStar; - - private SunLightController _sunLightController; - private SunLightParamUpdater _sunLightParamUpdater; - - public void Awake() - { - Instance = this; - _sunLightController = GetComponent(); - _sunLightController.enabled = true; - _sunLightParamUpdater = GetComponent(); - _sunLightParamUpdater._sunLightController = _sunLightController; - } - - public static void AddStar(StarController star) - { - if (star == null) return; - - Logger.LogVerbose($"Adding new star to list: {star.gameObject.name}"); - Instance._stars.Add(star); - } - - public static void RemoveStar(StarController star) - { - Logger.LogVerbose($"Removing star from list: {star?.gameObject?.name}"); - if (Instance._stars.Contains(star)) - { - if (Instance._activeStar != null && Instance._activeStar.Equals(star)) - { - Instance._stars.Remove(star); - if (Instance._stars.Count > 0) Instance.ChangeActiveStar(Instance._stars[0]); - } - else - { - Instance._stars.Remove(star); - } - } - } - - public void Update() - { - if (_activeStar == null || !_activeStar.gameObject.activeInHierarchy) - { - if (_stars.Contains(_activeStar)) _stars.Remove(_activeStar); - if (_stars.Count > 0) ChangeActiveStar(_stars[0]); - else gameObject.SetActive(false); - - foreach (var (_, material) in AtmosphereBuilder.Skys) - { - material.SetFloat(SunIntensity, 0); - } - - return; - } - - // Update atmo shaders - foreach (var (planet, material) in AtmosphereBuilder.Skys) - { - var sqrDist = (planet.transform.position - _activeStar.transform.position).sqrMagnitude; - var intensity = Mathf.Min(_activeStar.Intensity / (sqrDist / hearthSunDistanceSqr), 1f); - - material.SetFloat(SunIntensity, intensity); - } - - foreach (var star in _stars) - { - if (star == null) continue; - if (!(star.gameObject.activeSelf && star.gameObject.activeInHierarchy)) continue; - - // Player is always at 0,0,0 more or less so if they arent using the map camera then wtv - var origin = Vector3.zero; - if (PlayerState.InMapView()) - { - origin = Locator.GetActiveCamera().transform.position; - } - - if (star.Intensity * (star.transform.position - origin).sqrMagnitude < _activeStar.Intensity * (_activeStar.transform.position - origin).sqrMagnitude) - { - ChangeActiveStar(star); - break; - } - } - } - - private void ChangeActiveStar(StarController star) - { - if (_sunLightController == null || _sunLightParamUpdater == null) return; - - if (_activeStar != null) _activeStar.Disable(); - - Logger.LogVerbose($"Switching active star: {star.gameObject.name}"); - - _activeStar = star; - - star.Enable(); - - _sunLightController._sunBaseColor = star.SunColor; - _sunLightController._sunBaseIntensity = star.Intensity; - _sunLightController._sunLight = star.Light; - _sunLightController._ambientLight = star.AmbientLight; - - _sunLightParamUpdater.sunLight = star.Light; - _sunLightParamUpdater._sunController = star.transform.GetComponent(); - _sunLightParamUpdater._propID_SunPosition = Shader.PropertyToID("_SunPosition"); - _sunLightParamUpdater._propID_OWSunPositionRange = Shader.PropertyToID("_OWSunPositionRange"); - _sunLightParamUpdater._propID_OWSunColorIntensity = Shader.PropertyToID("_OWSunColorIntensity"); - - // For the param thing to work it wants this to be on the star idk - this.transform.parent = star.transform; - this.transform.localPosition = Vector3.zero; - } - } -} diff --git a/NewHorizons/Components/StarController.cs b/NewHorizons/Components/Stars/StarController.cs similarity index 94% rename from NewHorizons/Components/StarController.cs rename to NewHorizons/Components/Stars/StarController.cs index 8f69f085..374d4437 100644 --- a/NewHorizons/Components/StarController.cs +++ b/NewHorizons/Components/Stars/StarController.cs @@ -1,5 +1,5 @@ -using UnityEngine; -namespace NewHorizons.Components +using UnityEngine; +namespace NewHorizons.Components.Stars { public class StarController : MonoBehaviour { diff --git a/NewHorizons/Components/StarDestructionVolume.cs b/NewHorizons/Components/Stars/StarDestructionVolume.cs similarity index 98% rename from NewHorizons/Components/StarDestructionVolume.cs rename to NewHorizons/Components/Stars/StarDestructionVolume.cs index e0068d24..5354821a 100644 --- a/NewHorizons/Components/StarDestructionVolume.cs +++ b/NewHorizons/Components/Stars/StarDestructionVolume.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; using UnityEngine; -namespace NewHorizons.Components +namespace NewHorizons.Components.Stars { public class StarDestructionVolume : DestructionVolume { diff --git a/NewHorizons/Components/StarFluidVolume.cs b/NewHorizons/Components/Stars/StarFluidVolume.cs similarity index 96% rename from NewHorizons/Components/StarFluidVolume.cs rename to NewHorizons/Components/Stars/StarFluidVolume.cs index 150d39f4..bd91f025 100644 --- a/NewHorizons/Components/StarFluidVolume.cs +++ b/NewHorizons/Components/Stars/StarFluidVolume.cs @@ -1,6 +1,6 @@ using NewHorizons.Components.SizeControllers; using UnityEngine; -namespace NewHorizons.Components +namespace NewHorizons.Components.Stars { public class StarFluidVolume : SimpleFluidVolume { diff --git a/NewHorizons/Components/StarSurfaceAudioController.cs b/NewHorizons/Components/Stars/StarSurfaceAudioController.cs similarity index 88% rename from NewHorizons/Components/StarSurfaceAudioController.cs rename to NewHorizons/Components/Stars/StarSurfaceAudioController.cs index aab8d3ce..71bdf525 100644 --- a/NewHorizons/Components/StarSurfaceAudioController.cs +++ b/NewHorizons/Components/Stars/StarSurfaceAudioController.cs @@ -1,7 +1,7 @@ using UnityEngine; using NewHorizons.Components.SizeControllers; -namespace NewHorizons.Components +namespace NewHorizons.Components.Stars { [RequireComponent(typeof(OWAudioSource))] public class StarSurfaceAudioController : SectoredMonoBehaviour @@ -34,7 +34,7 @@ namespace NewHorizons.Components public void Update() { _fade = Mathf.MoveTowards(_fade, 1, Time.deltaTime * 0.2f); - float value = Mathf.Max(0.0f, Vector3.Distance(Locator.GetPlayerCamera().transform.position, this.transform.position) - (_starEvolutionController != null ? _starEvolutionController.CurrentScale : _size)); + float value = Mathf.Max(0.0f, Vector3.Distance(Locator.GetPlayerCamera().transform.position, transform.position) - (_starEvolutionController != null ? _starEvolutionController.CurrentScale : _size)); float num = Mathf.InverseLerp(1600f, 100f, value); _audioSource.SetLocalVolume(num * num * _fade); } diff --git a/NewHorizons/Components/StellarDeathController.cs b/NewHorizons/Components/Stars/StellarDeathController.cs similarity index 88% rename from NewHorizons/Components/StellarDeathController.cs rename to NewHorizons/Components/Stars/StellarDeathController.cs index fe5dadb3..400297b3 100644 --- a/NewHorizons/Components/StellarDeathController.cs +++ b/NewHorizons/Components/Stars/StellarDeathController.cs @@ -1,6 +1,6 @@ using UnityEngine; -namespace NewHorizons.Components +namespace NewHorizons.Components.Stars { public class StellarDeathController : MonoBehaviour { @@ -22,6 +22,7 @@ namespace NewHorizons.Components private float _currentSupernovaScale; private Material _localSupernovaMat; private bool _isProxy; + private bool _renderingEnabled = true; private ParticleSystemRenderer[] _cachedParticleRenderers; public void Awake() @@ -34,8 +35,12 @@ namespace NewHorizons.Components public void Activate() { enabled = true; - shockwave.enabled = true; - foreach (var particle in explosionParticles) particle.Play(); + shockwave.enabled = _renderingEnabled; + for (int i = 0; i < explosionParticles.Length; i++) + { + explosionParticles[i].Play(); + _cachedParticleRenderers[i].enabled = _renderingEnabled; + } _time = 0.0f; _currentSupernovaScale = supernovaScale.Evaluate(0.0f); _localSupernovaMat = new Material(supernovaMaterial); @@ -71,7 +76,7 @@ namespace NewHorizons.Components surface.transform.localScale = Vector3.one * _currentSupernovaScale; _localSupernovaMat.color = Color.Lerp(Color.black, supernovaMaterial.color, supernovaAlpha.Evaluate(_time)); - float distanceToPlayer = PlayerState.InDreamWorld() ? 20000f : (Vector3.Distance(transform.position, Locator.GetPlayerCamera().transform.position) - GetSupernovaRadius()); + float distanceToPlayer = PlayerState.InDreamWorld() ? 20000f : Vector3.Distance(transform.position, Locator.GetPlayerCamera().transform.position) - GetSupernovaRadius(); if (_isProxy) return; @@ -97,6 +102,7 @@ namespace NewHorizons.Components public void SetRenderingEnabled(bool renderingEnabled) { + _renderingEnabled = renderingEnabled; if (!enabled) return; shockwave.enabled = renderingEnabled; SetParticlesVisibility(renderingEnabled); diff --git a/NewHorizons/Components/Stars/SunLightEffectsController.cs b/NewHorizons/Components/Stars/SunLightEffectsController.cs new file mode 100644 index 00000000..c5260e54 --- /dev/null +++ b/NewHorizons/Components/Stars/SunLightEffectsController.cs @@ -0,0 +1,191 @@ +using NewHorizons.Builder.Atmosphere; +using NewHorizons.Utility; +using System.Collections.Generic; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; +namespace NewHorizons.Components.Stars +{ + [RequireComponent(typeof(SunLightController))] + [RequireComponent(typeof(SunLightParamUpdater))] + public class SunLightEffectsController : MonoBehaviour + { + private static readonly int SunIntensity = Shader.PropertyToID("_SunIntensity"); + private static readonly float hearthSunDistanceSqr = 8593 * 8593; + + public static SunLightEffectsController Instance { get; private set; } + + private readonly List _stars = new(); + private readonly List _lights = new(); + + private StarController _activeStar; + private SunLightController _sunLightController; + private SunLightParamUpdater _sunLightParamUpdater; + + public void Awake() + { + Instance = this; + + _sunLightController = GetComponent(); + _sunLightController.enabled = true; + + _sunLightParamUpdater = GetComponent(); + _sunLightParamUpdater._sunLightController = _sunLightController; + } + + public void Start() + { + // Using GameObject.Find here so that if its null we just dont find it + var sunlight = GameObject.Find("Sun_Body/Sector_SUN/Effects_SUN/SunLight").GetComponent(); + if (sunlight != null) AddStarLight(sunlight); + } + + public static void AddStar(StarController star) + { + if (star == null) return; + + Logger.LogVerbose($"Adding new star to list: {star.gameObject.name}"); + Instance._stars.Add(star); + } + + public static void RemoveStar(StarController star) + { + Logger.LogVerbose($"Removing star from list: {star?.gameObject?.name}"); + if (Instance._stars.Contains(star)) + { + if (Instance._activeStar != null && Instance._activeStar.Equals(star)) + { + Instance._stars.Remove(star); + if (Instance._stars.Count > 0) Instance.ChangeActiveStar(Instance._stars[0]); + } + else + { + Instance._stars.Remove(star); + } + } + } + + public static void AddStarLight(Light light) + { + if (light != null) + { + Instance._lights.SafeAdd(light); + } + } + + public static void RemoveStarLight(Light light) + { + if (light != null && Instance._lights.Contains(light)) + { + Instance._lights.Remove(light); + } + } + + public void Update() + { + // Player is always at 0,0,0 more or less so if they arent using the map camera then wtv + var origin = Vector3.zero; + + if (PlayerState.InMapView()) + { + origin = Locator.GetActiveCamera().transform.position; + + // Keep all star lights on in map + foreach (var light in _lights) + { + light.enabled = true; + } + } + else + { + // Outside map, only show lights within 50km range or light.range + // For some reason outside of the actual range of the lights they still show reflection effects on water and glass + foreach (var light in _lights) + { + // Minimum 50km range so it's not badly noticeable for dim stars + if ((light.transform.position - origin).sqrMagnitude <= Mathf.Max(light.range * light.range, 2500000000)) + { + light.enabled = true; + } + else + { + light.enabled = false; + } + } + } + + if (_stars.Count > 0) + { + if (_activeStar == null || !_activeStar.gameObject.activeInHierarchy) + { + if (_stars.Contains(_activeStar)) + { + _stars.Remove(_activeStar); + } + + if (_stars.Count > 0) + { + ChangeActiveStar(_stars[0]); + } + else + { + foreach (var (_, material) in AtmosphereBuilder.Skys) + { + material.SetFloat(SunIntensity, 0); + } + } + } + else + { + // Update atmo shaders + foreach (var (planet, material) in AtmosphereBuilder.Skys) + { + var sqrDist = (planet.transform.position - _activeStar.transform.position).sqrMagnitude; + var intensity = Mathf.Min(_activeStar.Intensity / (sqrDist / hearthSunDistanceSqr), 1f); + + material.SetFloat(SunIntensity, intensity); + } + + foreach (var star in _stars) + { + if (star == null) continue; + if (!(star.gameObject.activeSelf && star.gameObject.activeInHierarchy)) continue; + + if (star.Intensity * (star.transform.position - origin).sqrMagnitude < _activeStar.Intensity * (_activeStar.transform.position - origin).sqrMagnitude) + { + ChangeActiveStar(star); + break; + } + } + } + } + } + + private void ChangeActiveStar(StarController star) + { + if (_sunLightController == null || _sunLightParamUpdater == null) return; + + if (_activeStar != null) _activeStar.Disable(); + + Logger.LogVerbose($"Switching active star: {star.gameObject.name}"); + + _activeStar = star; + + star.Enable(); + + _sunLightController._sunBaseColor = star.SunColor; + _sunLightController._sunBaseIntensity = star.Intensity; + _sunLightController._sunLight = star.Light; + _sunLightController._ambientLight = star.AmbientLight; + + _sunLightParamUpdater.sunLight = star.Light; + _sunLightParamUpdater._sunController = star.transform.GetComponent(); + _sunLightParamUpdater._propID_SunPosition = Shader.PropertyToID("_SunPosition"); + _sunLightParamUpdater._propID_OWSunPositionRange = Shader.PropertyToID("_OWSunPositionRange"); + _sunLightParamUpdater._propID_OWSunColorIntensity = Shader.PropertyToID("_OWSunColorIntensity"); + + // For the param thing to work it wants this to be on the star idk + transform.parent = star.transform; + transform.localPosition = Vector3.zero; + } + } +} diff --git a/NewHorizons/Components/TransparentCloudRenderQueueController.cs b/NewHorizons/Components/TransparentCloudRenderQueueController.cs new file mode 100644 index 00000000..d78259b1 --- /dev/null +++ b/NewHorizons/Components/TransparentCloudRenderQueueController.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components +{ + [RequireComponent(typeof(OWTriggerVolume))] + public class TransparentCloudRenderQueueController : MonoBehaviour + { + public int insideQueue = 3001; + public int outsideQueue = 2999; + + private OWTriggerVolume _triggerVolume; + public Renderer renderer; + + public void Awake() + { + _triggerVolume = this.GetRequiredComponent(); + if (_triggerVolume == null) return; + _triggerVolume.OnEntry += OnTriggerVolumeEntry; + _triggerVolume.OnExit += OnTriggerVolumeExit; + } + + public void OnDestroy() + { + if (_triggerVolume == null) return; + _triggerVolume.OnEntry -= OnTriggerVolumeEntry; + _triggerVolume.OnExit -= OnTriggerVolumeExit; + } + + public void OnTriggerVolumeEntry(GameObject hitObj) + { + if (hitObj.CompareTag("PlayerDetector")) SetQueueToInside(); + } + + public void OnTriggerVolumeExit(GameObject hitObj) + { + if (hitObj.CompareTag("PlayerDetector")) SetQueueToOutside(); + } + + public void SetQueueToInside() + { + if (renderer == null) return; + renderer.sharedMaterial.renderQueue = insideQueue; + } + + public void SetQueueToOutside() + { + if (renderer == null) return; + renderer.sharedMaterial.renderQueue = outsideQueue; + } + } +} diff --git a/NewHorizons/Components/Volumes/BaseVolume.cs b/NewHorizons/Components/Volumes/BaseVolume.cs new file mode 100644 index 00000000..f313310c --- /dev/null +++ b/NewHorizons/Components/Volumes/BaseVolume.cs @@ -0,0 +1,28 @@ +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + [RequireComponent(typeof(OWTriggerVolume))] + public abstract class BaseVolume : MonoBehaviour + { + private OWTriggerVolume _triggerVolume; + + public virtual void Awake() + { + _triggerVolume = this.GetRequiredComponent(); + _triggerVolume.OnEntry += OnTriggerVolumeEntry; + _triggerVolume.OnExit += OnTriggerVolumeExit; + } + + public virtual void OnDestroy() + { + if (_triggerVolume == null) return; + _triggerVolume.OnEntry -= OnTriggerVolumeEntry; + _triggerVolume.OnExit -= OnTriggerVolumeExit; + } + + public abstract void OnTriggerVolumeEntry(GameObject hitObj); + + public abstract void OnTriggerVolumeExit(GameObject hitObj); + } +} diff --git a/NewHorizons/Components/BlackHoleDestructionVolume.cs b/NewHorizons/Components/Volumes/BlackHoleDestructionVolume.cs similarity index 84% rename from NewHorizons/Components/BlackHoleDestructionVolume.cs rename to NewHorizons/Components/Volumes/BlackHoleDestructionVolume.cs index 145a0c5c..e5489760 100644 --- a/NewHorizons/Components/BlackHoleDestructionVolume.cs +++ b/NewHorizons/Components/Volumes/BlackHoleDestructionVolume.cs @@ -1,6 +1,6 @@ using NewHorizons.OtherMods.AchievementsPlus.NH; -namespace NewHorizons.Components +namespace NewHorizons.Components.Volumes { public class BlackHoleDestructionVolume : DestructionVolume { @@ -15,7 +15,7 @@ namespace NewHorizons.Components SurveyorProbe requiredComponent = probeBody.GetRequiredComponent(); if (requiredComponent.IsLaunched()) { - UnityEngine.Object.Destroy(requiredComponent.gameObject); + Destroy(requiredComponent.gameObject); ProbeLostAchievement.Earn(); } } diff --git a/NewHorizons/Components/ChangeStarSystemVolume.cs b/NewHorizons/Components/Volumes/ChangeStarSystemVolume.cs similarity index 94% rename from NewHorizons/Components/ChangeStarSystemVolume.cs rename to NewHorizons/Components/Volumes/ChangeStarSystemVolume.cs index ea57c53f..bdf43aae 100644 --- a/NewHorizons/Components/ChangeStarSystemVolume.cs +++ b/NewHorizons/Components/Volumes/ChangeStarSystemVolume.cs @@ -1,4 +1,4 @@ -namespace NewHorizons.Components +namespace NewHorizons.Components.Volumes { public class ChangeStarSystemVolume : BlackHoleDestructionVolume { diff --git a/NewHorizons/Components/Volumes/InterferenceVolume.cs b/NewHorizons/Components/Volumes/InterferenceVolume.cs new file mode 100644 index 00000000..acb3582f --- /dev/null +++ b/NewHorizons/Components/Volumes/InterferenceVolume.cs @@ -0,0 +1,54 @@ +using NewHorizons.Handlers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + public class InterferenceVolume : BaseVolume + { + public override void OnTriggerVolumeEntry(GameObject hitObj) + { + if (hitObj.CompareTag("PlayerDetector")) + { + OnPlayerEnter(); + } + else if (hitObj.CompareTag("ProbeDetector")) + { + OnProbeEnter(); + } + else if (hitObj.CompareTag("ShipDetector")) + { + OnShipEnter(); + } + } + + public override void OnTriggerVolumeExit(GameObject hitObj) + { + if (hitObj.CompareTag("PlayerDetector")) + { + OnPlayerExit(); + } + else if (hitObj.CompareTag("ProbeDetector")) + { + OnProbeExit(); + } + else if (hitObj.CompareTag("ShipDetector")) + { + OnShipExit(); + } + } + + public void OnPlayerEnter() => InterferenceHandler.OnPlayerEnterInterferenceVolume(this); + public void OnPlayerExit() => InterferenceHandler.OnPlayerExitInterferenceVolume(this); + + public void OnProbeEnter() => InterferenceHandler.OnProbeEnterInterferenceVolume(this); + public void OnProbeExit() => InterferenceHandler.OnProbeExitInterferenceVolume(this); + + public void OnShipEnter() => InterferenceHandler.OnShipEnterInterferenceVolume(this); + public void OnShipExit() => InterferenceHandler.OnShipExitInterferenceVolume(this); + } +} diff --git a/NewHorizons/Components/Volumes/MapRestrictionVolume.cs b/NewHorizons/Components/Volumes/MapRestrictionVolume.cs new file mode 100644 index 00000000..ee31a2a2 --- /dev/null +++ b/NewHorizons/Components/Volumes/MapRestrictionVolume.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + public class MapRestrictionVolume : BaseVolume + { + public override void OnTriggerVolumeEntry(GameObject hitObj) + { + if (hitObj.CompareTag("PlayerDetector")) + { + Locator.GetMapController()?.OnPlayerEnterMapRestriction(); + } + } + + public override void OnTriggerVolumeExit(GameObject hitObj) + { + if (hitObj.CompareTag("PlayerDetector")) + { + Locator.GetMapController()?.OnPlayerExitMapRestriction(); + } + } + } +} diff --git a/NewHorizons/Components/NHFluidVolume.cs b/NewHorizons/Components/Volumes/NHFluidVolume.cs similarity index 86% rename from NewHorizons/Components/NHFluidVolume.cs rename to NewHorizons/Components/Volumes/NHFluidVolume.cs index 3b19f04d..3b5a4a00 100644 --- a/NewHorizons/Components/NHFluidVolume.cs +++ b/NewHorizons/Components/Volumes/NHFluidVolume.cs @@ -1,5 +1,5 @@ -using UnityEngine; -namespace NewHorizons.Components +using UnityEngine; +namespace NewHorizons.Components.Volumes { public class NHFluidVolume : RadialFluidVolume { diff --git a/NewHorizons/Components/NHInnerFogWarpVolume.cs b/NewHorizons/Components/Volumes/NHInnerFogWarpVolume.cs similarity index 83% rename from NewHorizons/Components/NHInnerFogWarpVolume.cs rename to NewHorizons/Components/Volumes/NHInnerFogWarpVolume.cs index 61c5e3ea..70d74979 100644 --- a/NewHorizons/Components/NHInnerFogWarpVolume.cs +++ b/NewHorizons/Components/Volumes/NHInnerFogWarpVolume.cs @@ -1,4 +1,4 @@ -namespace NewHorizons.Components +namespace NewHorizons.Components.Volumes { public class NHInnerFogWarpVolume : InnerFogWarpVolume { diff --git a/NewHorizons/Components/Volumes/NotificationVolume.cs b/NewHorizons/Components/Volumes/NotificationVolume.cs new file mode 100644 index 00000000..70543449 --- /dev/null +++ b/NewHorizons/Components/Volumes/NotificationVolume.cs @@ -0,0 +1,113 @@ +using NewHorizons.Handlers; +using OWML.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Components.Volumes +{ + public class NotificationVolume : BaseVolume + { + private NotificationTarget _target = NotificationTarget.All; + private bool _pin = false; + private NotificationData _entryNotification; + private NotificationData _exitNotification; + + public void SetPinned(bool pin) => _pin = pin; + + public void SetTarget(External.Modules.VolumesModule.NotificationVolumeInfo.NotificationTarget target) => SetTarget(EnumUtils.Parse(target.ToString(), NotificationTarget.All)); + + public void SetTarget(NotificationTarget target) => _target = target; + + public void SetEntryNotification(string displayMessage, float duration = 5) + { + _entryNotification = new NotificationData(_target, TranslationHandler.GetTranslation(displayMessage, TranslationHandler.TextType.UI), duration); + } + + public void SetExitNotification(string displayMessage, float duration = 5) + { + _exitNotification = new NotificationData(_target, TranslationHandler.GetTranslation(displayMessage, TranslationHandler.TextType.UI), duration); + } + + public override void OnTriggerVolumeEntry(GameObject hitObj) + { + if (_target == NotificationTarget.All) + { + if (hitObj.CompareTag("PlayerDetector") || hitObj.CompareTag("ShipDetector")) + { + PostEntryNotification(); + } + } + else if (_target == NotificationTarget.Player) + { + if (hitObj.CompareTag("PlayerDetector")) + { + PostEntryNotification(); + } + } + else if (_target == NotificationTarget.Ship) + { + if (hitObj.CompareTag("ShipDetector")) + { + PostEntryNotification(); + } + } + } + + public override void OnTriggerVolumeExit(GameObject hitObj) + { + if (_target == NotificationTarget.All) + { + if (hitObj.CompareTag("PlayerDetector") || hitObj.CompareTag("ShipDetector")) + { + PostExitNotification(); + } + } + else if (_target == NotificationTarget.Player) + { + if (hitObj.CompareTag("PlayerDetector")) + { + PostExitNotification(); + } + } + else if (_target == NotificationTarget.Ship) + { + if (hitObj.CompareTag("ShipDetector")) + { + PostExitNotification(); + } + } + } + + public void PostEntryNotification() + { + if (_entryNotification == null) return; + NotificationManager.SharedInstance.PostNotification(_entryNotification, _pin); + } + + public void PostExitNotification() + { + if (_exitNotification == null) return; + NotificationManager.SharedInstance.PostNotification(_exitNotification, _pin); + } + + public void UnpinEntryNotification() + { + if (_entryNotification == null) return; + if (NotificationManager.SharedInstance.IsPinnedNotification(_entryNotification)) + { + NotificationManager.SharedInstance.UnpinNotification(_entryNotification); + } + } + + public void UnpinExitNotification() + { + if (_exitNotification == null) return; + if (NotificationManager.SharedInstance.IsPinnedNotification(_exitNotification)) + { + NotificationManager.SharedInstance.UnpinNotification(_exitNotification); + } + } + } +} diff --git a/NewHorizons/Components/RingFluidVolume.cs b/NewHorizons/Components/Volumes/RingFluidVolume.cs similarity index 95% rename from NewHorizons/Components/RingFluidVolume.cs rename to NewHorizons/Components/Volumes/RingFluidVolume.cs index 83b377a7..66982bdc 100644 --- a/NewHorizons/Components/RingFluidVolume.cs +++ b/NewHorizons/Components/Volumes/RingFluidVolume.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using UnityEngine; -namespace NewHorizons.Components +namespace NewHorizons.Components.Volumes { public class RingFluidVolume : SimpleFluidVolume { @@ -14,7 +14,7 @@ namespace NewHorizons.Components ForceDetector forceDetector = hitObj.GetComponent(); if (forceDetector != null && forceDetector._activeVolumes != null && forceDetector._activeVolumes.Count > 0 && forceDetector._activeVolumes.Where(activeVolume => activeVolume is ForceVolume).Select(activeVolume => activeVolume as ForceVolume).Any(activeVolume => activeVolume.GetAffectsAlignment(forceDetector._attachedBody))) return; - + fluidDetector.AddVolume(this); } diff --git a/NewHorizons/External/Configs/AddonConfig.cs b/NewHorizons/External/Configs/AddonConfig.cs index 08069e00..00257080 100644 --- a/NewHorizons/External/Configs/AddonConfig.cs +++ b/NewHorizons/External/Configs/AddonConfig.cs @@ -28,5 +28,10 @@ namespace NewHorizons.External.Configs /// Credits info for this mod. A list of contributors and their roles separated by #. For example: xen#New Horizons dev. /// public string[] credits; + + /// + /// A pop-up message for the first time a user runs the add-on + /// + public string popupMessage; } } diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index 4a353011..55c59231 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -67,6 +67,10 @@ namespace NewHorizons.External.Configs [Obsolete("Signal is deprecated, please use Props->signals")] public SignalModule Signal; + + [Obsolete("Ring is deprecated, please use Rings")] + public RingModule Ring; + #endregion Obsolete /// @@ -135,9 +139,9 @@ namespace NewHorizons.External.Configs public string[] removeChildren; /// - /// Creates a ring around the planet + /// Create rings around the planet /// - public RingModule Ring; + public RingModule[] Rings; /// /// Add sand to this planet @@ -169,6 +173,16 @@ namespace NewHorizons.External.Configs /// public WaterModule Water; + /// + /// Add various volumes on this body + /// + public VolumesModule Volumes; + + /// + /// Extra data that may be used by extension mods + /// + public object extras; + public PlanetConfig() { // Always have to have a base module @@ -312,6 +326,20 @@ namespace NewHorizons.External.Configs if (tornado.downwards) tornado.type = PropModule.TornadoInfo.TornadoType.Downwards; + if (Props?.audioVolumes != null) + { + if (Volumes == null) Volumes = new VolumesModule(); + if (Volumes.audioVolumes == null) Volumes.audioVolumes = new VolumesModule.AudioVolumeInfo[0]; + Volumes.audioVolumes = Volumes.audioVolumes.Concat(Props.audioVolumes).ToArray(); + } + + if (Props?.reveal != null) + { + if (Volumes == null) Volumes = new VolumesModule(); + if (Volumes.revealVolumes == null) Volumes.revealVolumes = new VolumesModule.RevealVolumeInfo[0]; + Volumes.revealVolumes = Volumes.revealVolumes.Concat(Props.reveal).ToArray(); + } + if (Base.sphereOfInfluence != 0f) Base.soiOverride = Base.sphereOfInfluence; // Moved a bunch of stuff off of shiplog module to star system module because it didnt exist when we made this @@ -377,10 +405,20 @@ namespace NewHorizons.External.Configs if (!string.IsNullOrEmpty(Cloak.audioFilePath)) Cloak.audio = Cloak.audioFilePath; } - // Rings are no longer variable size module + // Ring is now a list so you can have many per planet if (Ring != null) { - if (Ring.curve != null) Ring.scaleCurve = Ring.curve; + if (Rings == null) Rings = new RingModule[0]; + Rings = Rings.Append(Ring).ToArray(); + } + + // Rings are no longer variable size module + if (Rings != null) + { + foreach (var ring in Rings) + { + if (ring.curve != null) ring.scaleCurve = ring.curve; + } } } } diff --git a/NewHorizons/External/Configs/StarSystemConfig.cs b/NewHorizons/External/Configs/StarSystemConfig.cs index 2df0dc8c..5d3963aa 100644 --- a/NewHorizons/External/Configs/StarSystemConfig.cs +++ b/NewHorizons/External/Configs/StarSystemConfig.cs @@ -14,6 +14,11 @@ namespace NewHorizons.External.Configs [JsonObject] public class StarSystemConfig { + /// + /// An override value for the far clip plane. Allows you to see farther. + /// + public float farClipPlaneOverride; + /// /// Whether this system can be warped to via the warp drive. If you set factRequiredForWarp, this will be true. /// @@ -102,6 +107,11 @@ namespace NewHorizons.External.Configs /// public CuriosityColorInfo[] curiosities; + /// + /// Extra data that may be used by extension mods + /// + public object extras; + public class NomaiCoordinates { [MinLength(2)] diff --git a/NewHorizons/External/Modules/AtmosphereModule.cs b/NewHorizons/External/Modules/AtmosphereModule.cs index f03f3ceb..33c0ccef 100644 --- a/NewHorizons/External/Modules/AtmosphereModule.cs +++ b/NewHorizons/External/Modules/AtmosphereModule.cs @@ -30,6 +30,8 @@ namespace NewHorizons.External.Modules [EnumMember(Value = @"quantumMoon")] QuantumMoon = 1, [EnumMember(Value = @"basic")] Basic = 2, + + [EnumMember(Value = @"transparent")] Transparent = 3, } [JsonObject] @@ -100,6 +102,11 @@ namespace NewHorizons.External.Modules /// public bool useAtmosphereShader; + /// + /// Whether this atmosphere will have flames appear when your ship goes a certain speed. + /// + [DefaultValue(true)] public bool hasShockLayer = true; + /// /// Minimum speed that your ship can go in the atmosphere where flames will appear. /// diff --git a/NewHorizons/External/Modules/PropModule.cs b/NewHorizons/External/Modules/PropModule.cs index 6ed70204..1644192d 100644 --- a/NewHorizons/External/Modules/PropModule.cs +++ b/NewHorizons/External/Modules/PropModule.cs @@ -48,11 +48,6 @@ namespace NewHorizons.External.Modules /// public RaftInfo[] rafts; - /// - /// Add triggers that reveal parts of the ship log on this planet - /// - public RevealInfo[] reveal; - /// /// Scatter props around this planet's surface /// @@ -83,11 +78,6 @@ namespace NewHorizons.External.Modules /// public SingularityModule[] singularities; - /// - /// Add audio volumes to this planet - /// - public AudioVolumeInfo[] audioVolumes; - /// /// Add signalscope signals to this planet /// @@ -98,6 +88,10 @@ namespace NewHorizons.External.Modules /// public RemoteInfo[] remotes; + [Obsolete("reveal is deprecated. Use Volumes->revealVolumes instead.")] public VolumesModule.RevealVolumeInfo[] reveal; + + [Obsolete("audioVolumes is deprecated. Use Volumes->audioVolumes instead.")] public VolumesModule.AudioVolumeInfo[] audioVolumes; + [JsonObject] public class ScatterInfo { @@ -145,6 +139,16 @@ namespace NewHorizons.External.Modules /// The highest height that these objects will be placed at (only relevant if there's a heightmap) /// public float? maxHeight; + + /// + /// Should we try to prevent overlap between the scattered details? True by default. If it's affecting load times turn it off. + /// + [DefaultValue(true)] public bool preventOverlap = true; + + /// + /// Should this detail stay loaded even if you're outside the sector (good for very large props) + /// + public bool keepLoaded; } [JsonObject] @@ -433,55 +437,6 @@ namespace NewHorizons.External.Modules public string xmlFile; } - [JsonObject] - public class RevealInfo - { - [JsonConverter(typeof(StringEnumConverter))] - public enum RevealVolumeType - { - [EnumMember(Value = @"enter")] Enter = 0, - - [EnumMember(Value = @"observe")] Observe = 1, - - [EnumMember(Value = @"snapshot")] Snapshot = 2 - } - - /// - /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only) - /// - public float maxAngle = 180f; // Observe Only - - /// - /// The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only) - /// - public float maxDistance = -1f; // Snapshot & Observe Only - - /// - /// The position to place this volume at - /// - public MVector3 position; - - /// - /// The radius of this reveal volume - /// - public float radius = 1f; - - /// - /// What needs to be done to the volume to unlock the facts - /// - [DefaultValue("enter")] public RevealVolumeType revealOn = RevealVolumeType.Enter; - - /// - /// A list of facts to reveal - /// - public string[] reveals; - - /// - /// An achievement to unlock. Optional. - /// - public string achievementID; - } - [JsonObject] public class EntryLocationInfo { @@ -818,30 +773,6 @@ namespace NewHorizons.External.Modules [DefaultValue(1f)] public float probability = 1f; } - [JsonObject] - public class AudioVolumeInfo - { - /// - /// The location of this audio volume. Optional (will default to 0,0,0). - /// - public MVector3 position; - - /// - /// The radius of this audio volume - /// - public float radius; - - /// - /// The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. - /// - public string audio; - - /// - /// The audio track of this audio volume - /// - [DefaultValue("environment")] public AudioMixerTrackName track = AudioMixerTrackName.Environment; - } - [JsonObject] public class RemoteInfo { @@ -1002,25 +933,4 @@ namespace NewHorizons.External.Modules } } } - - [JsonConverter(typeof(StringEnumConverter))] - public enum AudioMixerTrackName - { - [EnumMember(Value = @"undefined")] Undefined = 0, - [EnumMember(Value = @"menu")] Menu = 1, - [EnumMember(Value = @"music")] Music = 2, - [EnumMember(Value = @"environment")] Environment = 4, - [EnumMember(Value = @"environmentUnfiltered")] Environment_Unfiltered = 5, - [EnumMember(Value = @"endTimesSfx")] EndTimes_SFX = 8, - [EnumMember(Value = @"signal")] Signal = 16, - [EnumMember(Value = @"death")] Death = 32, - [EnumMember(Value = @"player")] Player = 64, - [EnumMember(Value = @"playerExternal")] Player_External = 65, - [EnumMember(Value = @"ship")] Ship = 128, - [EnumMember(Value = @"map")] Map = 256, - [EnumMember(Value = @"endTimesMusic")] EndTimes_Music = 512, - [EnumMember(Value = @"muffleWhileRafting")] MuffleWhileRafting = 1024, - [EnumMember(Value = @"muffleIndoors")] MuffleIndoors = 2048, - [EnumMember(Value = @"slideReelMusic")] SlideReelMusic = 4096, - } } \ No newline at end of file diff --git a/NewHorizons/External/Modules/RingModule.cs b/NewHorizons/External/Modules/RingModule.cs index 5f21a8a6..a5c93c4c 100644 --- a/NewHorizons/External/Modules/RingModule.cs +++ b/NewHorizons/External/Modules/RingModule.cs @@ -62,5 +62,10 @@ namespace NewHorizons.External.Modules /// Fade rings in/out over time. Optional. Value between 0-1, time is in minutes. /// public TimeValuePair[] opacityCurve; + + /// + /// An optional rename of this object + /// + public string rename; } } \ No newline at end of file diff --git a/NewHorizons/External/Modules/VolumesModule.cs b/NewHorizons/External/Modules/VolumesModule.cs new file mode 100644 index 00000000..49bbc542 --- /dev/null +++ b/NewHorizons/External/Modules/VolumesModule.cs @@ -0,0 +1,247 @@ +using NewHorizons.Utility; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.External.Modules +{ + [JsonObject] + public class VolumesModule + { + /// + /// Add audio volumes to this planet. + /// + public AudioVolumeInfo[] audioVolumes; + + /// + /// Add hazard volumes to this planet. + /// + public HazardVolumeInfo[] hazardVolumes; + + /// + /// Add interference volumes to this planet. + /// + public VolumeInfo[] interferenceVolumes; + + /// + /// Add insulating volumes to this planet. These will stop electricty hazard volumes from affecting you (just like the jellyfish). + /// + public VolumeInfo[] insulatingVolumes; + + /// + /// Add map restriction volumes to this planet. + /// + public VolumeInfo[] mapRestrictionVolumes; + + /// + /// Add notification volumes to this planet. + /// + public NotificationVolumeInfo[] notificationVolumes; + + /// + /// Add triggers that reveal parts of the ship log on this planet. + /// + public RevealVolumeInfo[] revealVolumes; + + /// + /// Add reverb volumes to this planet. Great for echoes in caves. + /// + public VolumeInfo[] reverbVolumes; + + [JsonObject] + public class VolumeInfo + { + /// + /// The location of this volume. Optional (will default to 0,0,0). + /// + public MVector3 position; + + /// + /// The radius of this volume. + /// + public float radius = 1f; + + /// + /// The relative path from the planet to the parent of this object. Optional (will default to the root sector). + /// + public string parentPath; + + /// + /// An optional rename of this volume. + /// + public string rename; + } + + [JsonObject] + public class RevealVolumeInfo : VolumeInfo + { + [JsonConverter(typeof(StringEnumConverter))] + public enum RevealVolumeType + { + [EnumMember(Value = @"enter")] Enter = 0, + + [EnumMember(Value = @"observe")] Observe = 1, + + [EnumMember(Value = @"snapshot")] Snapshot = 2 + } + + /// + /// The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only) + /// + public float maxAngle = 180f; // Observe Only + + /// + /// The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only) + /// + public float maxDistance = -1f; // Snapshot & Observe Only + + /// + /// What needs to be done to the volume to unlock the facts + /// + [DefaultValue("enter")] public RevealVolumeType revealOn = RevealVolumeType.Enter; + + /// + /// A list of facts to reveal + /// + public string[] reveals; + + /// + /// An achievement to unlock. Optional. + /// + public string achievementID; + } + + [JsonObject] + public class AudioVolumeInfo : VolumeInfo + { + /// + /// The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string audio; + + /// + /// The audio track of this audio volume + /// + [DefaultValue("environment")] public AudioMixerTrackName track = AudioMixerTrackName.Environment; + + /// + /// Whether to loop this audio while in this audio volume or just play it once + /// + [DefaultValue(true)] public bool loop = true; + } + + [JsonObject] + public class NotificationVolumeInfo : VolumeInfo + { + /// + /// What the notification will show for. + /// + [DefaultValue("all")] public NotificationTarget target = NotificationTarget.All; + + /// + /// The notification that will play when you enter this volume. + /// + public NotificationInfo entryNotification; + + /// + /// The notification that will play when you exit this volume. + /// + public NotificationInfo exitNotification; + + + [JsonObject] + public class NotificationInfo + { + /// + /// The message that will be displayed. + /// + public string displayMessage; + + /// + /// The duration this notification will be displayed. + /// + [DefaultValue(5f)] public float duration = 5f; + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum NotificationTarget + { + [EnumMember(Value = @"all")] All = 0, + [EnumMember(Value = @"ship")] Ship = 1, + [EnumMember(Value = @"player")] Player = 2, + } + } + + [JsonObject] + public class HazardVolumeInfo : VolumeInfo + { + /// + /// The type of hazard for this volume. + /// + [DefaultValue("general")] public HazardType type = HazardType.GENERAL; + + /// + /// The amount of damage you will take per second while inside this volume. + /// + [DefaultValue(10f)] public float damagePerSecond = 10f; + + /// + /// The type of damage you will take when you first touch this volume. + /// + [DefaultValue("impact")] public InstantDamageType firstContactDamageType = InstantDamageType.Impact; + + /// + /// The amount of damage you will take when you first touch this volume. + /// + public float firstContactDamage; + + [JsonConverter(typeof(StringEnumConverter))] + public enum HazardType + { + [EnumMember(Value = @"none")] NONE = 0, + [EnumMember(Value = @"general")] GENERAL = 1, + [EnumMember(Value = @"ghostMatter")] DARKMATTER = 2, + [EnumMember(Value = @"heat")] HEAT = 4, + [EnumMember(Value = @"fire")] FIRE = 8, + [EnumMember(Value = @"sandfall")] SANDFALL = 16, + [EnumMember(Value = @"electricity")] ELECTRICITY = 32, + [EnumMember(Value = @"rapids")] RAPIDS = 64 + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum InstantDamageType + { + [EnumMember(Value = @"impact")] Impact, + [EnumMember(Value = @"puncture")] Puncture, + [EnumMember(Value = @"electrical")] Electrical + } + } + } + + [JsonConverter(typeof(StringEnumConverter))] + public enum AudioMixerTrackName + { + [EnumMember(Value = @"undefined")] Undefined = 0, + [EnumMember(Value = @"menu")] Menu = 1, + [EnumMember(Value = @"music")] Music = 2, + [EnumMember(Value = @"environment")] Environment = 4, + [EnumMember(Value = @"environmentUnfiltered")] Environment_Unfiltered = 5, + [EnumMember(Value = @"endTimesSfx")] EndTimes_SFX = 8, + [EnumMember(Value = @"signal")] Signal = 16, + [EnumMember(Value = @"death")] Death = 32, + [EnumMember(Value = @"player")] Player = 64, + [EnumMember(Value = @"playerExternal")] Player_External = 65, + [EnumMember(Value = @"ship")] Ship = 128, + [EnumMember(Value = @"map")] Map = 256, + [EnumMember(Value = @"endTimesMusic")] EndTimes_Music = 512, + [EnumMember(Value = @"muffleWhileRafting")] MuffleWhileRafting = 1024, + [EnumMember(Value = @"muffleIndoors")] MuffleIndoors = 2048, + [EnumMember(Value = @"slideReelMusic")] SlideReelMusic = 4096, + } +} diff --git a/NewHorizons/External/NewHorizonsData.cs b/NewHorizons/External/NewHorizonsData.cs index a6273dd7..52a8cbf1 100644 --- a/NewHorizons/External/NewHorizonsData.cs +++ b/NewHorizons/External/NewHorizonsData.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NewHorizons.Utility; @@ -11,12 +11,15 @@ namespace NewHorizons.External private static string _activeProfileName; private static readonly string FileName = "save.json"; + // This is its own method so it can be patched by NH-QSB compat + public static string GetProfileName() => StandaloneProfileManager.SharedInstance?.currentProfile?.profileName; + public static void Load() { - _activeProfileName = StandaloneProfileManager.SharedInstance?.currentProfile?.profileName; + _activeProfileName = GetProfileName(); if (_activeProfileName == null) { - Logger.LogError("Couldn't find active profile, are you on Gamepass?"); + Logger.LogWarning("Couldn't find active profile, are you on Gamepass?"); _activeProfileName = "XboxGamepassDefaultProfile"; } @@ -79,12 +82,13 @@ namespace NewHorizons.External KnownFrequencies = new List(); KnownSignals = new List(); NewlyRevealedFactIDs = new List(); + PopupsRead = new List(); } public List KnownFrequencies { get; } public List KnownSignals { get; } - public List NewlyRevealedFactIDs { get; } + public List PopupsRead { get; } } #region Frequencies @@ -152,5 +156,21 @@ namespace NewHorizons.External } #endregion + + #region Read popups + + public static void ReadOneTimePopup(string id) + { + _activeProfile?.PopupsRead.Add(id); + Save(); + } + + public static bool HasReadOneTimePopup(string id) + { + // To avoid spam, we'll just say the popup has been read if we can't load the profile + return _activeProfile?.PopupsRead.Contains(id) ?? true; + } + + #endregion } } \ No newline at end of file diff --git a/NewHorizons/Handlers/AudioTypeHandler.cs b/NewHorizons/Handlers/AudioTypeHandler.cs index 698514c8..70878ae3 100644 --- a/NewHorizons/Handlers/AudioTypeHandler.cs +++ b/NewHorizons/Handlers/AudioTypeHandler.cs @@ -2,6 +2,7 @@ using NewHorizons.Utility; using OWML.Common; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using UnityEngine; using Logger = NewHorizons.Utility.Logger; @@ -65,7 +66,7 @@ namespace NewHorizons.Handlers var id = mod.ModHelper.Manifest.UniqueName + "_" + audioPath; if (_customAudioTypes.TryGetValue(id, out audioType)) return audioType; - var audioClip = AudioUtilities.LoadAudio(mod.ModHelper.Manifest.ModFolderPath + "/" + audioPath); + var audioClip = AudioUtilities.LoadAudio(Path.Combine(mod.ModHelper.Manifest.ModFolderPath, audioPath)); if (audioClip == null) { diff --git a/NewHorizons/Handlers/CreditsHandler.cs b/NewHorizons/Handlers/CreditsHandler.cs index 9d7e7405..4c45b07a 100644 --- a/NewHorizons/Handlers/CreditsHandler.cs +++ b/NewHorizons/Handlers/CreditsHandler.cs @@ -39,7 +39,7 @@ namespace NewHorizons.Handlers private static void AddCreditsSection(string sectionName, string[] entries, ref XmlDocument xml) { - var finalCredits = xml.SelectSingleNode("Credits/section"); + var finalCredits = xml.SelectSingleNode("Credits/section[@name='CreditsFinal']"); /* * Looks bad, would need more customization, complicated, messes up music timing, wont do for now @@ -134,11 +134,8 @@ namespace NewHorizons.Handlers { var rootSection = MakeNode(doc, "section", new Dictionary() { - { "platform", "All" }, - { "type", "Scroll" }, - { "scrollDuration", "214" }, - { "spacing", "12" }, - { "width", "1590" } + { "name", title }, + { "credits-type", "Final Fast Krazy" } }); var titleLayout = MakeNode(doc, "layout", new Dictionary() diff --git a/NewHorizons/Handlers/InterferenceHandler.cs b/NewHorizons/Handlers/InterferenceHandler.cs new file mode 100644 index 00000000..73912289 --- /dev/null +++ b/NewHorizons/Handlers/InterferenceHandler.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NewHorizons.Handlers +{ + using InterferenceVolume = Components.Volumes.InterferenceVolume; + + public static class InterferenceHandler + { + private static List _playerInterference; + private static List _probeInterference; + private static List _shipInterference; + + public static void Init() + { + _playerInterference = new List(); + _probeInterference = new List(); + _shipInterference = new List(); + } + + public static bool PlayerHasInterference() => _playerInterference.Any(volume => volume != null); + public static bool ProbeHasInterference() => _probeInterference.Any(volume => volume != null); + public static bool ShipHasInterference() => _shipInterference.Any(volume => volume != null); + + public static bool IsPlayerSameAsProbe() + { + _playerInterference.RemoveAll(volume => volume == null); + return _playerInterference.All(_probeInterference.Contains) && _playerInterference.Count == _probeInterference.Count; + } + + public static bool IsPlayerSameAsShip() + { + _playerInterference.RemoveAll(volume => volume == null); + return _playerInterference.All(_shipInterference.Contains) && _playerInterference.Count == _shipInterference.Count; + } + + public static void OnPlayerEnterInterferenceVolume(InterferenceVolume interferenceVolume) + { + _playerInterference.SafeAdd(interferenceVolume); + GlobalMessenger.FireEvent("RefreshHUDVisibility"); + } + + public static void OnPlayerExitInterferenceVolume(InterferenceVolume interferenceVolume) + { + _playerInterference.Remove(interferenceVolume); + GlobalMessenger.FireEvent("RefreshHUDVisibility"); + } + + public static void OnProbeEnterInterferenceVolume(InterferenceVolume interferenceVolume) + { + _probeInterference.SafeAdd(interferenceVolume); + GlobalMessenger.FireEvent("RefreshHUDVisibility"); + } + + public static void OnProbeExitInterferenceVolume(InterferenceVolume interferenceVolume) + { + _probeInterference.Remove(interferenceVolume); + GlobalMessenger.FireEvent("RefreshHUDVisibility"); + } + + public static void OnShipEnterInterferenceVolume(InterferenceVolume interferenceVolume) + { + _shipInterference.SafeAdd(interferenceVolume); + GlobalMessenger.FireEvent("RefreshHUDVisibility"); + } + public static void OnShipExitInterferenceVolume(InterferenceVolume interferenceVolume) + { + _shipInterference.Remove(interferenceVolume); + GlobalMessenger.FireEvent("RefreshHUDVisibility"); + } + } +} diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 73df7daa..ffc14739 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -3,8 +3,10 @@ using NewHorizons.Builder.Body; using NewHorizons.Builder.General; using NewHorizons.Builder.Orbital; using NewHorizons.Builder.Props; -using NewHorizons.Components; +using NewHorizons.Builder.Volumes; using NewHorizons.Components.Orbital; +using NewHorizons.Components.Quantum; +using NewHorizons.Components.Stars; using NewHorizons.OtherMods.OWRichPresence; using NewHorizons.Utility; using System; @@ -56,8 +58,9 @@ namespace NewHorizons.Handlers GameObject.Destroy(starLightGO.GetComponent()); starLightGO.name = "StarLightController"; - starLightGO.AddComponent(); - StarLightController.AddStar(starController); + starLightGO.AddComponent(); + SunLightEffectsController.AddStar(starController); + SunLightEffectsController.AddStarLight(starController.Light); starLightGO.SetActive(true); @@ -248,6 +251,16 @@ namespace NewHorizons.Handlers } } } + + try + { + Main.Instance.OnPlanetLoaded?.Invoke(body.Config.name); + } + catch (Exception e) + { + Logger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}"); + } + return true; } @@ -487,7 +500,10 @@ namespace NewHorizons.Handlers { var (star, starController, starEvolutionController) = StarBuilder.Make(go, sector, body.Config.Star, body.Mod, body.Config.isStellarRemnant); - if (starController != null) StarLightController.AddStar(starController); + if (starController != null) SunLightEffectsController.AddStar(starController); + + var starLight = star.FindChild("SunLight"); + if (starLight != null) SunLightEffectsController.AddStarLight(starLight.GetComponent()); // If it has an evolution controller that means it will die -> we make a remnant (unless its a remnant) if (starEvolutionController != null && !body.Config.isStellarRemnant) @@ -536,9 +552,12 @@ namespace NewHorizons.Handlers } } - if (body.Config.Ring != null) + if (body.Config.Rings != null) { - RingBuilder.Make(go, sector, body.Config.Ring, body.Mod); + foreach (var ring in body.Config.Rings) + { + RingBuilder.Make(go, sector, ring, body.Mod); + } } if (body.Config.AsteroidBelt != null) @@ -579,7 +598,7 @@ namespace NewHorizons.Handlers if (!string.IsNullOrEmpty(body.Config.Atmosphere?.clouds?.texturePath)) { CloudsBuilder.Make(go, sector, body.Config.Atmosphere, willHaveCloak, body.Mod); - SunOverrideBuilder.Make(go, sector, body.Config.Atmosphere, body.Config.Water, surfaceSize); + if (body.Config.Atmosphere.clouds.cloudsPrefab != External.Modules.CloudPrefabType.Transparent) SunOverrideBuilder.Make(go, sector, body.Config.Atmosphere, body.Config.Water, surfaceSize); } if (body.Config.Atmosphere.hasRain || body.Config.Atmosphere.hasSnow) @@ -598,6 +617,11 @@ namespace NewHorizons.Handlers PropBuildManager.Make(go, sector, rb, body.Config, body.Mod); } + if (body.Config.Volumes != null) + { + VolumesBuildManager.Make(go, sector, rb, body.Config, body.Mod); + } + if (body.Config.Funnel != null) { FunnelBuilder.Make(go, go.GetComponentInChildren(), rb, body.Config.Funnel); diff --git a/NewHorizons/Handlers/PlanetDestructionHandler.cs b/NewHorizons/Handlers/PlanetDestructionHandler.cs index 4d60a271..a170ff62 100644 --- a/NewHorizons/Handlers/PlanetDestructionHandler.cs +++ b/NewHorizons/Handlers/PlanetDestructionHandler.cs @@ -1,4 +1,4 @@ -using NewHorizons.Components; +using NewHorizons.Components.Stars; using NewHorizons.Utility; using OWML.Utils; using System; @@ -117,7 +117,8 @@ namespace NewHorizons.Handlers break; case AstroObject.Name.Sun: var starController = ao.gameObject.GetComponent(); - StarLightController.RemoveStar(starController); + SunLightEffectsController.RemoveStar(starController); + SunLightEffectsController.RemoveStarLight(ao.transform.Find("Sector_SUN/Effects_SUN/SunLight").GetComponent()); GameObject.Destroy(starController); var audio = ao.GetComponentInChildren(); diff --git a/NewHorizons/Handlers/StarChartHandler.cs b/NewHorizons/Handlers/StarChartHandler.cs index 69e542c7..4ddec030 100644 --- a/NewHorizons/Handlers/StarChartHandler.cs +++ b/NewHorizons/Handlers/StarChartHandler.cs @@ -1,4 +1,4 @@ -using NewHorizons.Components; +using NewHorizons.Components.ShipLog; using NewHorizons.Utility; using System.Collections.Generic; using UnityEngine; diff --git a/NewHorizons/Handlers/TitleSceneHandler.cs b/NewHorizons/Handlers/TitleSceneHandler.cs index b4fb22dc..9faee29c 100644 --- a/NewHorizons/Handlers/TitleSceneHandler.cs +++ b/NewHorizons/Handlers/TitleSceneHandler.cs @@ -89,7 +89,7 @@ namespace NewHorizons.Handlers heightMap.minHeight = body.Config.HeightMap.minHeight * size / body.Config.HeightMap.maxHeight; heightMap.stretch = body.Config.HeightMap.stretch; } - if (body.Config.Atmosphere?.clouds?.texturePath != null) + if (body.Config.Atmosphere?.clouds?.texturePath != null && body.Config.Atmosphere?.clouds?.cloudsPrefab != CloudPrefabType.Transparent) { // Hacky but whatever I just want a sphere size = Mathf.Clamp(body.Config.Atmosphere.size / 10, minSize, maxSize); @@ -107,13 +107,16 @@ namespace NewHorizons.Handlers } pivot.name = "Pivot"; - if (body.Config.Ring != null) + if (body.Config.Rings != null && body.Config.Rings.Length > 0) { - RingModule newRing = new RingModule(); - newRing.innerRadius = size * 1.2f; - newRing.outerRadius = size * 2f; - newRing.texture = body.Config.Ring.texture; - var ring = RingBuilder.Make(titleScreenGO, null, newRing, body.Mod); + foreach (var ring in body.Config.Rings) + { + RingModule newRing = new RingModule(); + newRing.innerRadius = size * 1.2f; + newRing.outerRadius = size * 2f; + newRing.texture = ring.texture; + RingBuilder.Make(titleScreenGO, null, newRing, body.Mod); + } titleScreenGO.transform.localScale = Vector3.one * 0.8f; } diff --git a/NewHorizons/Handlers/TranslationHandler.cs b/NewHorizons/Handlers/TranslationHandler.cs index aa253c3e..38c6e76e 100644 --- a/NewHorizons/Handlers/TranslationHandler.cs +++ b/NewHorizons/Handlers/TranslationHandler.cs @@ -66,9 +66,10 @@ namespace NewHorizons.Handlers foreach (var originalKey in config.ShipLogDictionary.Keys) { var key = originalKey.Replace("<", "<").Replace(">", ">").Replace("", ""); + var value = config.ShipLogDictionary[originalKey].Replace("<", "<").Replace(">", ">").Replace("", ""); - if (!_shipLogTranslationDictionary[language].ContainsKey(key)) _shipLogTranslationDictionary[language].Add(key, config.ShipLogDictionary[originalKey]); - else _shipLogTranslationDictionary[language][key] = config.ShipLogDictionary[originalKey]; + if (!_shipLogTranslationDictionary[language].ContainsKey(key)) _shipLogTranslationDictionary[language].Add(key, value); + else _shipLogTranslationDictionary[language][key] = value; } } @@ -78,9 +79,10 @@ namespace NewHorizons.Handlers foreach (var originalKey in config.DialogueDictionary.Keys) { var key = originalKey.Replace("<", "<").Replace(">", ">").Replace("", ""); + var value = config.DialogueDictionary[originalKey].Replace("<", "<").Replace(">", ">").Replace("", ""); - if (!_dialogueTranslationDictionary[language].ContainsKey(key)) _dialogueTranslationDictionary[language].Add(key, config.DialogueDictionary[originalKey]); - else _dialogueTranslationDictionary[language][key] = config.DialogueDictionary[originalKey]; + if (!_dialogueTranslationDictionary[language].ContainsKey(key)) _dialogueTranslationDictionary[language].Add(key, value); + else _dialogueTranslationDictionary[language][key] = value; } } @@ -90,16 +92,17 @@ namespace NewHorizons.Handlers foreach (var originalKey in config.UIDictionary.Keys) { var key = originalKey.Replace("<", "<").Replace(">", ">").Replace("", ""); + var value = config.UIDictionary[originalKey].Replace("<", "<").Replace(">", ">").Replace("", ""); - if (!_uiTranslationDictionary[language].ContainsKey(key)) _uiTranslationDictionary[language].Add(key, config.UIDictionary[originalKey]); - else _uiTranslationDictionary[language][key] = config.UIDictionary[originalKey]; + if (!_uiTranslationDictionary[language].ContainsKey(key)) _uiTranslationDictionary[language].Add(key, value); + else _uiTranslationDictionary[language][key] = value; } } } - public static void AddDialogue(string rawText, params string[] rawPreText) + public static void AddDialogue(string rawText, bool trimRawTextForKey = false, params string[] rawPreText) { - var key = string.Join(string.Empty, rawPreText) + rawText; + var key = string.Join(string.Empty, rawPreText) + (trimRawTextForKey? rawText.Trim() : rawText); var text = GetTranslation(rawText, TextType.DIALOGUE); diff --git a/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs b/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs index f3578bda..f1e547c2 100644 --- a/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs +++ b/NewHorizons/Handlers/VesselCoordinatePromptHandler.cs @@ -1,4 +1,4 @@ -using NewHorizons.Components; +using NewHorizons.Components.ShipLog; using NewHorizons.Utility; using System; using System.Collections.Generic; diff --git a/NewHorizons/INewHorizons.cs b/NewHorizons/INewHorizons.cs index 7c647003..8f132ee7 100644 --- a/NewHorizons/INewHorizons.cs +++ b/NewHorizons/INewHorizons.cs @@ -43,6 +43,22 @@ namespace NewHorizons /// UnityEvent GetStarSystemLoadedEvent(); + /// + /// An event invoked when NH has finished a planet for a star system. + /// Gives the name of the planet that was just loaded. + /// + UnityEvent GetBodyLoadedEvent(); + + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); + + /// + /// Uses JSONPath to query a system + /// + object QuerySystem(Type outType, string path); + /// /// Allows you to overwrite the default system. This is where the player is respawned after dying. /// diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index cb64f092..96e670d7 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -1,16 +1,20 @@ using HarmonyLib; -using NewHorizons.OtherMods.AchievementsPlus; using NewHorizons.Builder.Atmosphere; using NewHorizons.Builder.Body; using NewHorizons.Builder.Props; using NewHorizons.Components; +using NewHorizons.Components.Fixers; +using NewHorizons.Components.SizeControllers; using NewHorizons.External; using NewHorizons.External.Configs; using NewHorizons.Handlers; +using NewHorizons.OtherMods.AchievementsPlus; +using NewHorizons.OtherMods.MenuFramework; +using NewHorizons.OtherMods.OWRichPresence; +using NewHorizons.OtherMods.VoiceActing; using NewHorizons.Utility; using NewHorizons.Utility.DebugMenu; using NewHorizons.Utility.DebugUtilities; -using NewHorizons.OtherMods.VoiceActing; using OWML.Common; using OWML.ModHelper; using System; @@ -22,8 +26,6 @@ using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; using Logger = NewHorizons.Utility.Logger; -using NewHorizons.OtherMods.OWRichPresence; -using NewHorizons.Components.SizeControllers; namespace NewHorizons { @@ -68,6 +70,7 @@ namespace NewHorizons public class StarSystemEvent : UnityEvent { } public StarSystemEvent OnChangeStarSystem; public StarSystemEvent OnStarSystemLoaded; + public StarSystemEvent OnPlanetLoaded; // For warping to the eye system private GameObject _ship; @@ -94,9 +97,9 @@ namespace NewHorizons DebugMenu.UpdatePauseMenuButton(); } - if (VerboseLogs) Logger.UpdateLogLevel(Logger.LogType.Verbose); - else if (Debug) Logger.UpdateLogLevel(Logger.LogType.Log); - else Logger.UpdateLogLevel(Logger.LogType.Error); + if (VerboseLogs) Logger.UpdateLogLevel(Logger.LogType.Verbose); + else if (Debug) Logger.UpdateLogLevel(Logger.LogType.Log); + else Logger.UpdateLogLevel(Logger.LogType.Error); _defaultSystemOverride = config.GetSettingsValue("Default System Override"); @@ -119,14 +122,14 @@ namespace NewHorizons _wasConfigured = true; } - public static void ResetConfigs(bool resetTranslation = true) + public void ResetConfigs(bool resetTranslation = true) { BodyDict.Clear(); SystemDict.Clear(); BodyDict["SolarSystem"] = new List(); BodyDict["EyeOfTheUniverse"] = new List(); // Keep this empty tho fr - SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), Instance) + SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), "", Instance) { Config = { @@ -142,7 +145,7 @@ namespace NewHorizons } } }; - SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), Instance) + SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), "", Instance) { Config = { @@ -158,9 +161,18 @@ namespace NewHorizons } }; - if (!resetTranslation) return; - TranslationHandler.ClearTables(); - TextTranslation.Get().SetLanguage(TextTranslation.Get().GetLanguage()); + if (resetTranslation) + { + TranslationHandler.ClearTables(); + TextTranslation.Get().SetLanguage(TextTranslation.Get().GetLanguage()); + } + + LoadTranslations(Path.Combine(Instance.ModHelper.Manifest.ModFolderPath, "Assets/"), this); + } + + public void Awake() + { + Instance = this; } public void Start() @@ -170,11 +182,11 @@ namespace NewHorizons OnChangeStarSystem = new StarSystemEvent(); OnStarSystemLoaded = new StarSystemEvent(); + OnPlanetLoaded = new StarSystemEvent(); SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; - Instance = this; GlobalMessenger.AddListener("PlayerDeath", OnDeath); GlobalMessenger.AddListener("WakeUp", OnWakeUp); @@ -198,6 +210,7 @@ namespace NewHorizons Instance.ModHelper.Events.Unity.FireOnNextUpdate(() => _firstLoad = false); Instance.ModHelper.Menus.PauseMenu.OnInit += DebugReload.InitializePauseMenu; + MenuHandler.Init(); AchievementHandler.Init(); VoiceHandler.Init(); RichPresenceHandler.Init(); @@ -303,14 +316,12 @@ namespace NewHorizons AstroObjectLocator.Init(); StreamingHandler.Init(); AudioTypeHandler.Init(); + InterferenceHandler.Init(); RemoteHandler.Init(); AtmosphereBuilder.Init(); BrambleNodeBuilder.Init(BodyDict[CurrentStarSystem].Select(x => x.Config).Where(x => x.Bramble?.dimension != null).ToArray()); StarEvolutionController.Init(); - // Has to go before loading planets else the Discord Rich Presence mod won't show the right text - LoadTranslations(ModHelper.Manifest.ModFolderPath + "Assets/", this); - if (isSolarSystem) { foreach (var supernovaPlanetEffectController in GameObject.FindObjectsOfType()) @@ -343,6 +354,7 @@ namespace NewHorizons var map = GameObject.FindObjectOfType(); if (map != null) map._maxPanDistance = FurthestOrbit * 1.5f; + // Fix the map satellite SearchUtilities.Find("HearthianMapSatellite_Body", false).AddComponent(); @@ -409,7 +421,7 @@ namespace NewHorizons var ssrLight = solarSystemRoot.AddComponent(); ssrLight.innerSpotAngle = 0; ssrLight.spotAngle = 179; - ssrLight.range = Main.FurthestOrbit * (4f/3f); + ssrLight.range = Main.FurthestOrbit * (4f / 3f); ssrLight.intensity = 0.001f; var fluid = playerBody.FindChild("PlayerDetector").GetComponent(); @@ -483,10 +495,22 @@ namespace NewHorizons } var folder = mod.ModHelper.Manifest.ModFolderPath; + var systemsFolder = Path.Combine(folder, "systems"); + var planetsFolder = Path.Combine(folder, "planets"); + // Load systems first so that when we load bodies later we can check for missing ones - if (Directory.Exists(folder + @"systems\")) + if (Directory.Exists(systemsFolder)) { - foreach (var file in Directory.GetFiles(folder + @"systems\", "*.json?", SearchOption.AllDirectories)) + var systemFiles = Directory.GetFiles(systemsFolder, "*.json", SearchOption.AllDirectories) + .Concat(Directory.GetFiles(systemsFolder, "*.jsonc", SearchOption.AllDirectories)) + .ToArray(); + + if(systemFiles.Length == 0) + { + Logger.LogVerbose($"Found no JSON files in systems folder: {systemsFolder}"); + } + + foreach (var file in systemFiles) { var name = Path.GetFileNameWithoutExtension(file); @@ -500,7 +524,7 @@ namespace NewHorizons if (starSystemConfig.startHere) { // We always want to allow mods to overwrite setting the main SolarSystem as default but not the other way around - if (name != "SolarSystem") + if (name != "SolarSystem") { SetDefaultSystem(name); _currentStarSystem = name; @@ -515,13 +539,22 @@ namespace NewHorizons } else { - SystemDict[name] = new NewHorizonsSystem(name, starSystemConfig, mod); + SystemDict[name] = new NewHorizonsSystem(name, starSystemConfig, relativePath, mod); } } } - if (Directory.Exists(folder + "planets")) + if (Directory.Exists(planetsFolder)) { - foreach (var file in Directory.GetFiles(folder + @"planets\", "*.json?", SearchOption.AllDirectories)) + var planetFiles = Directory.GetFiles(planetsFolder, "*.json", SearchOption.AllDirectories) + .Concat(Directory.GetFiles(planetsFolder, "*.jsonc", SearchOption.AllDirectories)) + .ToArray(); + + if(planetFiles.Length == 0) + { + Logger.LogVerbose($"Found no JSON files in planets folder: {planetsFolder}"); + } + + foreach (var file in planetFiles) { var relativeDirectory = file.Replace(folder, ""); var body = LoadConfig(mod, relativeDirectory); @@ -562,6 +595,7 @@ namespace NewHorizons if (addonConfig.achievements != null) AchievementHandler.RegisterAddon(addonConfig, mod as ModBehaviour); if (addonConfig.credits != null) CreditsHandler.RegisterCredits(mod.ModHelper.Manifest.Name, addonConfig.credits); + if (!string.IsNullOrEmpty(addonConfig.popupMessage)) MenuHandler.RegisterOneTimePopup(mod, addonConfig.popupMessage); } private void LoadTranslations(string folder, IModBehaviour mod) @@ -601,6 +635,7 @@ namespace NewHorizons if (config == null) { Logger.LogError($"Couldn't load {relativePath}. Is your Json formatted correctly?"); + MenuHandler.RegisterFailedConfig(Path.GetFileName(relativePath)); return null; } @@ -616,7 +651,7 @@ namespace NewHorizons starSystemConfig.Migrate(); starSystemConfig.FixCoordinates(); - var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, mod); + var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, $"", mod); SystemDict.Add(config.starSystem, system); @@ -632,6 +667,7 @@ namespace NewHorizons catch (Exception e) { Logger.LogError($"Error encounter when loading {relativePath}:\n{e}"); + MenuHandler.RegisterFailedConfig(Path.GetFileName(relativePath)); } return body; @@ -647,6 +683,13 @@ namespace NewHorizons #region Change star system public void ChangeCurrentStarSystem(string newStarSystem, bool warp = false, bool vessel = false) { + // If we're just on the title screen set the system for later + if (LoadManager.GetCurrentScene() == OWScene.TitleScreen) + { + _currentStarSystem = newStarSystem; + return; + } + if (IsChangingStarSystem) return; IsWarpingFromShip = warp; @@ -658,9 +701,6 @@ namespace NewHorizons IsChangingStarSystem = true; WearingSuit = PlayerState.IsWearingSuit(); - // We kill them so they don't move as much - Locator.GetDeathManager().KillPlayer(DeathType.Meditation); - OWScene sceneToLoad; if (newStarSystem == "EyeOfTheUniverse") @@ -678,12 +718,15 @@ namespace NewHorizons _currentStarSystem = newStarSystem; + // Freeze player inputs + OWInput.ChangeInputMode(InputMode.None); + LoadManager.LoadSceneAsync(sceneToLoad, !vessel, LoadManager.FadeType.ToBlack, 0.1f, true); } void OnDeath(DeathType _) { - // We reset the solar system on death (unless we just killed the player) + // We reset the solar system on death if (!IsChangingStarSystem) { // If the override is a valid system then we go there diff --git a/NewHorizons/NewHorizons.csproj b/NewHorizons/NewHorizons.csproj index bb43cdbf..a4e7f6e4 100644 --- a/NewHorizons/NewHorizons.csproj +++ b/NewHorizons/NewHorizons.csproj @@ -17,7 +17,7 @@ - + diff --git a/NewHorizons/NewHorizonsApi.cs b/NewHorizons/NewHorizonsApi.cs index 0ddb9f14..a4da089f 100644 --- a/NewHorizons/NewHorizonsApi.cs +++ b/NewHorizons/NewHorizonsApi.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using UnityEngine; using UnityEngine.Events; using Logger = NewHorizons.Utility.Logger; namespace NewHorizons { + public class NewHorizonsApi : INewHorizons { [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] @@ -33,10 +36,10 @@ namespace NewHorizons if (name == null) return; var relativePath = $"temp/{name}.json"; - var fullPath = Main.Instance.ModHelper.Manifest.ModFolderPath + relativePath; - if (!Directory.Exists(Main.Instance.ModHelper.Manifest.ModFolderPath + "temp")) + var fullPath = Path.Combine(Main.Instance.ModHelper.Manifest.ModFolderPath, relativePath); + if (!Directory.Exists(Path.Combine(Main.Instance.ModHelper.Manifest.ModFolderPath, "temp"))) { - Directory.CreateDirectory(Main.Instance.ModHelper.Manifest.ModFolderPath + "temp"); + Directory.CreateDirectory(Path.Combine(Main.Instance.ModHelper.Manifest.ModFolderPath, "temp")); } JsonHelper.SaveJsonObject(fullPath, config); var body = Main.Instance.LoadConfig(Main.Instance, relativePath); @@ -64,20 +67,10 @@ namespace NewHorizons return Main.BodyDict.Values.SelectMany(x => x)?.ToList()?.FirstOrDefault(x => x.Config.name == name)?.Object; } - public string GetCurrentStarSystem() - { - return Main.Instance.CurrentStarSystem; - } - - public UnityEvent GetChangeStarSystemEvent() - { - return Main.Instance.OnChangeStarSystem; - } - - public UnityEvent GetStarSystemLoadedEvent() - { - return Main.Instance.OnStarSystemLoaded; - } + public string GetCurrentStarSystem() => Main.Instance.CurrentStarSystem; + public UnityEvent GetChangeStarSystemEvent() => Main.Instance.OnChangeStarSystem; + public UnityEvent GetStarSystemLoadedEvent() => Main.Instance.OnStarSystemLoaded; + public UnityEvent GetBodyLoadedEvent() => Main.Instance.OnPlanetLoaded; public bool SetDefaultSystem(string name) { @@ -108,6 +101,42 @@ namespace NewHorizons } } + private static object QueryJson(Type outType, string filePath, string jsonPath) + { + if (filePath == "") return null; + try + { + var jsonText = File.ReadAllText(filePath); + var jsonData = JObject.Parse(jsonText); + return jsonData.SelectToken(jsonPath)?.ToObject(outType); + } + catch (FileNotFoundException) + { + return null; + } + catch (JsonException e) + { + Logger.LogError(e.ToString()); + return null; + } + } + + public object QueryBody(Type outType, string bodyName, string jsonPath) + { + var planet = Main.BodyDict[Main.Instance.CurrentStarSystem].Find((b) => b.Config.name == bodyName); + return planet == null + ? null + : QueryJson(outType, Path.Combine(planet.Mod.ModHelper.Manifest.ModFolderPath, planet.RelativePath), jsonPath); + } + + public object QuerySystem(Type outType, string jsonPath) + { + var system = Main.SystemDict[Main.Instance.CurrentStarSystem]; + return system == null + ? null + : QueryJson(outType, Path.Combine(system.Mod.ModHelper.Manifest.ModFolderPath, system.RelativePath), jsonPath); + } + public GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal) { diff --git a/NewHorizons/OtherMods/CommonCameraUtility/CommonCameraHandler.cs b/NewHorizons/OtherMods/CommonCameraUtility/CommonCameraHandler.cs new file mode 100644 index 00000000..e344a57f --- /dev/null +++ b/NewHorizons/OtherMods/CommonCameraUtility/CommonCameraHandler.cs @@ -0,0 +1,27 @@ +using NewHorizons.OtherMods.MenuFramework; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.OtherMods.CommonCameraUtility +{ + public static class CommonCameraHandler + { + private static ICommonCameraAPI _cameraAPI; + + static CommonCameraHandler() + { + _cameraAPI = Main.Instance.ModHelper.Interaction.TryGetModApi("xen.CommonCameraUtility"); + } + + public static void RegisterCustomCamera(OWCamera camera) + { + if (_cameraAPI != null) + { + _cameraAPI.RegisterCustomCamera(camera); + } + else + { + Logger.LogError("Tried to register custom camera but Common Camera Utility was missing."); + } + } + } +} diff --git a/NewHorizons/OtherMods/CommonCameraUtility/ICommonCameraAPI.cs b/NewHorizons/OtherMods/CommonCameraUtility/ICommonCameraAPI.cs new file mode 100644 index 00000000..bd49375b --- /dev/null +++ b/NewHorizons/OtherMods/CommonCameraUtility/ICommonCameraAPI.cs @@ -0,0 +1,13 @@ +using UnityEngine; +using UnityEngine.Events; + +namespace NewHorizons.OtherMods.CommonCameraUtility +{ + public interface ICommonCameraAPI + { + void RegisterCustomCamera(OWCamera OWCamera); + (OWCamera, Camera) CreateCustomCamera(string name); + UnityEvent EquipTool(); + UnityEvent UnequipTool(); + } +} diff --git a/NewHorizons/OtherMods/MenuFramework/IMenuAPI.cs b/NewHorizons/OtherMods/MenuFramework/IMenuAPI.cs new file mode 100644 index 00000000..f44aecdf --- /dev/null +++ b/NewHorizons/OtherMods/MenuFramework/IMenuAPI.cs @@ -0,0 +1,20 @@ +using UnityEngine; +using UnityEngine.UI; + +namespace NewHorizons.OtherMods.MenuFramework +{ + public interface IMenuAPI + { + GameObject TitleScreen_MakeMenuOpenButton(string name, int index, Menu menuToOpen); + GameObject TitleScreen_MakeSceneLoadButton(string name, int index, SubmitActionLoadScene.LoadableScenes sceneToLoad, PopupMenu confirmPopup = null); + Button TitleScreen_MakeSimpleButton(string name, int index); + GameObject PauseMenu_MakeMenuOpenButton(string name, Menu menuToOpen, Menu customMenu = null); + GameObject PauseMenu_MakeSceneLoadButton(string name, SubmitActionLoadScene.LoadableScenes sceneToLoad, PopupMenu confirmPopup = null, Menu customMenu = null); + Button PauseMenu_MakeSimpleButton(string name, Menu customMenu = null); + Menu PauseMenu_MakePauseListMenu(string title); + PopupMenu MakeTwoChoicePopup(string message, string confirmText, string cancelText); + PopupInputMenu MakeInputFieldPopup(string message, string placeholderMessage, string confirmText, string cancelText); + PopupMenu MakeInfoPopup(string message, string continueButtonText); + void RegisterStartupPopup(string message); + } +} diff --git a/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs b/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs new file mode 100644 index 00000000..bb88e7b4 --- /dev/null +++ b/NewHorizons/OtherMods/MenuFramework/MenuHandler.cs @@ -0,0 +1,69 @@ +using NewHorizons.External; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using OWML.Common; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using Logger = NewHorizons.Utility.Logger; + +namespace NewHorizons.OtherMods.MenuFramework +{ + public static class MenuHandler + { + private static IMenuAPI _menuApi; + + private static List<(IModBehaviour mod, string message)> _registeredPopups = new(); + private static List _failedFiles = new(); + + public static void Init() + { + _menuApi = Main.Instance.ModHelper.Interaction.TryGetModApi("_nebula.MenuFramework"); + + TextTranslation.Get().OnLanguageChanged += OnLanguageChanged; + } + + public static void OnLanguageChanged() + { + // Have to load save data before doing popups + NewHorizonsData.Load(); + + if (!VersionUtility.CheckUpToDate()) + { + var warning = string.Format(TranslationHandler.GetTranslation("OUTDATED_VERSION_WARNING", TranslationHandler.TextType.UI), + VersionUtility.RequiredVersionString, + Application.version); + + Logger.LogError(warning); + _menuApi.RegisterStartupPopup(warning); + } + + foreach(var (mod, message) in _registeredPopups) + { + if (!NewHorizonsData.HasReadOneTimePopup(mod.ModHelper.Manifest.UniqueName)) + { + _menuApi.RegisterStartupPopup(TranslationHandler.GetTranslation(message, TranslationHandler.TextType.UI)); + NewHorizonsData.ReadOneTimePopup(mod.ModHelper.Manifest.UniqueName); + } + } + + if (_failedFiles.Count > 0) + { + var message = TranslationHandler.GetTranslation("JSON_FAILED_TO_LOAD", TranslationHandler.TextType.UI); + var mods = string.Join(",", _failedFiles.Take(10)); + if (_failedFiles.Count > 10) mods += "..."; + _menuApi.RegisterStartupPopup(string.Format(message, mods)); + } + + _registeredPopups.Clear(); + _failedFiles.Clear(); + + // Just wanted to do this when the language is loaded in initially + TextTranslation.Get().OnLanguageChanged -= OnLanguageChanged; + } + + public static void RegisterFailedConfig(string filename) => _failedFiles.Add(filename); + + public static void RegisterOneTimePopup(IModBehaviour mod, string message) => _registeredPopups.Add((mod, message)); + } +} diff --git a/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs b/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs index a1e4da27..727bc082 100644 --- a/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs +++ b/NewHorizons/OtherMods/OWRichPresence/RichPresenceHandler.cs @@ -1,4 +1,4 @@ -using NewHorizons.Components; +using NewHorizons.Components.ShipLog; using NewHorizons.Handlers; using NewHorizons.Utility; using System; @@ -47,7 +47,7 @@ namespace NewHorizons.OtherMods.OWRichPresence var localizedName = TranslationHandler.GetTranslation(name, TranslationHandler.TextType.UI); var message = TranslationHandler.GetTranslation("RICH_PRESENCE_EXPLORING", TranslationHandler.TextType.UI).Replace("{0}", localizedName); - API.CreateTrigger(go, sector, message, name.Replace(" ", "").Replace("'", "").ToLowerInvariant()); + API.CreateTrigger(go, sector, message, name.Replace(" ", "").Replace("'", "").Replace("-", "").ToLowerInvariant()); } public static void OnStarSystemLoaded(string name) diff --git a/NewHorizons/OtherMods/VoiceActing/VoiceHandler.cs b/NewHorizons/OtherMods/VoiceActing/VoiceHandler.cs index 65f66b79..ed7142de 100644 --- a/NewHorizons/OtherMods/VoiceActing/VoiceHandler.cs +++ b/NewHorizons/OtherMods/VoiceActing/VoiceHandler.cs @@ -38,10 +38,10 @@ namespace NewHorizons.OtherMods.VoiceActing { foreach (var mod in Main.Instance.GetDependants().Append(Main.Instance)) { - var folder = $"{mod.ModHelper.Manifest.ModFolderPath}voicemod"; + var folder = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, "voicemod"); if (!Directory.Exists(folder)) { // Fallback to PascalCase bc it used to be like that - folder = $"{mod.ModHelper.Manifest.ModFolderPath}VoiceMod"; + folder = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, "VoiceMod"); } if (Directory.Exists(folder)) { diff --git a/NewHorizons/Patches/CameraPatches/NomaiRemoteCameraPatches.cs b/NewHorizons/Patches/CameraPatches/NomaiRemoteCameraPatches.cs new file mode 100644 index 00000000..29719c0e --- /dev/null +++ b/NewHorizons/Patches/CameraPatches/NomaiRemoteCameraPatches.cs @@ -0,0 +1,24 @@ +using HarmonyLib; +using NewHorizons.OtherMods.CommonCameraUtility; +using UnityEngine; + +namespace NewHorizons.Patches.CameraPatches +{ + [HarmonyPatch] + public static class NomaiRemoteCameraPatches + { + [HarmonyPostfix] + [HarmonyPatch(typeof(NomaiRemoteCamera), nameof(NomaiRemoteCamera.Awake))] + public static void NomaiRemoteCamera_Awake(NomaiRemoteCamera __instance) + { + // Ensures that if the player is visible from the remote camera they look normal + CommonCameraHandler.RegisterCustomCamera(__instance._camera); + + // These layers were left on because it doesnt come up in base game (Dreamworld is inactive, player is far away) + __instance._camera.mainCamera.cullingMask &= ~(1 << LayerMask.NameToLayer("DreamSimulation")); + __instance._camera.mainCamera.cullingMask &= ~(1 <.AddListener("AttachPlayerToPoint", __instance.OnAttachPlayerToPoint); + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(CharacterDialogueTree), nameof(CharacterDialogueTree.OnDestroy))] + private static void CharacterDialogueTree_OnDestroy(CharacterDialogueTree __instance) + { + GlobalMessenger.RemoveListener("AttachPlayerToPoint", __instance.OnAttachPlayerToPoint); + } + + private static void OnAttachPlayerToPoint(this CharacterDialogueTree characterDialogueTree, OWRigidbody rigidbody) + { + characterDialogueTree.EndConversation(); + } +} diff --git a/NewHorizons/Patches/DestructionVolumePatches.cs b/NewHorizons/Patches/DestructionVolumePatches.cs index 23221daa..e29e3d02 100644 --- a/NewHorizons/Patches/DestructionVolumePatches.cs +++ b/NewHorizons/Patches/DestructionVolumePatches.cs @@ -1,5 +1,5 @@ using HarmonyLib; -using NewHorizons.Components; +using NewHorizons.Components.Quantum; using System; using System.Collections.Generic; using System.Linq; diff --git a/NewHorizons/Patches/HUDPatches.cs b/NewHorizons/Patches/HUDPatches.cs index bbb0061a..f58fa342 100644 --- a/NewHorizons/Patches/HUDPatches.cs +++ b/NewHorizons/Patches/HUDPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using NewHorizons.Handlers; namespace NewHorizons.Patches { @@ -9,6 +10,8 @@ namespace NewHorizons.Patches [HarmonyPatch(typeof(HUDMarker), nameof(HUDMarker.Awake))] public static void HUDMarker_Awake(HUDMarker __instance) { + GlobalMessenger.AddListener("RefreshHUDVisibility", __instance.RefreshOwnVisibility); + GlobalMessenger.AddListener("RefreshHUDVisibility", __instance.RefreshOwnVisibility); GlobalMessenger.AddListener("PlayerEnterCloakField", __instance.OnPlayerEnterCloakField); GlobalMessenger.AddListener("PlayerExitCloakField", __instance.OnPlayerExitCloakField); } @@ -17,6 +20,8 @@ namespace NewHorizons.Patches [HarmonyPatch(typeof(HUDMarker), nameof(HUDMarker.OnDestroy))] public static void HUDMarker_OnDestroy(HUDMarker __instance) { + GlobalMessenger.RemoveListener("RefreshHUDVisibility", __instance.RefreshOwnVisibility); + GlobalMessenger.RemoveListener("RefreshHUDVisibility", __instance.RefreshOwnVisibility); GlobalMessenger.RemoveListener("PlayerEnterCloakField", __instance.OnPlayerEnterCloakField); GlobalMessenger.RemoveListener("PlayerExitCloakField", __instance.OnPlayerExitCloakField); } @@ -53,11 +58,67 @@ namespace NewHorizons.Patches GlobalMessenger.RemoveListener("ShipExitCloakField", __instance.RefreshOwnVisibility); } + [HarmonyPrefix] + [HarmonyPatch(typeof(ProbeHUDMarker), nameof(ProbeHUDMarker.RefreshOwnVisibility))] + public static bool ProbeHUDMarker_RefreshOwnVisibility(ProbeHUDMarker __instance) + { + bool insideEYE = Locator.GetEyeStateManager() != null && Locator.GetEyeStateManager().IsInsideTheEye(); + bool insideQM = __instance._quantumMoon != null && (__instance._quantumMoon.IsPlayerInside() || __instance._quantumMoon.IsProbeInside()); + bool insideRW = Locator.GetRingWorldController() != null && Locator.GetRingWorldController().isPlayerInside == Locator.GetRingWorldController().isProbeInside; + bool insideIP = Locator.GetCloakFieldController() != null && Locator.GetCloakFieldController().isPlayerInsideCloak == Locator.GetCloakFieldController().isProbeInsideCloak; + bool insideCloak = Components.CloakSectorController.isPlayerInside == Components.CloakSectorController.isProbeInside; + bool sameInterference = InterferenceHandler.IsPlayerSameAsProbe(); + bool isActive = __instance.gameObject.activeInHierarchy || __instance._isTLCDuplicate; + + __instance._isVisible = isActive && !insideEYE && !insideQM && !__instance._translatorEquipped && !__instance._inConversation && __instance._launched && (__instance._isWearingHelmet || __instance._atFlightConsole) && insideRW && insideIP && insideCloak && sameInterference; + + if (__instance._canvasMarker != null) __instance._canvasMarker.SetVisibility(__instance._isVisible); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(ShipHUDMarker), nameof(ShipHUDMarker.RefreshOwnVisibility))] + public static bool ShipHUDMarker_RefreshOwnVisibility(ShipHUDMarker __instance) + { + bool insideEYE = Locator.GetEyeStateManager() != null && Locator.GetEyeStateManager().IsInsideTheEye(); + bool insideQM = __instance._quantumMoon != null && (__instance._quantumMoon.IsPlayerInside() || __instance._quantumMoon.IsShipInside()); + bool insideRW = Locator.GetRingWorldController() != null && Locator.GetRingWorldController().isPlayerInside; + bool insideIP = Locator.GetCloakFieldController() != null ? Locator.GetCloakFieldController().isPlayerInsideCloak == Locator.GetCloakFieldController().isShipInsideCloak : true; + bool insideCloak = Components.CloakSectorController.isPlayerInside == Components.CloakSectorController.isShipInside; + bool sameInterference = InterferenceHandler.IsPlayerSameAsShip(); + + __instance._isVisible = !insideEYE && !insideQM && !insideRW && !__instance._translatorEquipped && !__instance._inConversation && !__instance._shipDestroyed && !__instance._playerInShip && PlayerState.HasPlayerEnteredShip() && __instance._isWearingHelmet && insideIP && insideCloak && sameInterference; + + if (__instance._canvasMarker != null) __instance._canvasMarker.SetVisibility(__instance._isVisible); + + return false; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(ShipLogEntryHUDMarker), nameof(ShipLogEntryHUDMarker.RefreshOwnVisibility))] + public static bool ShipLogEntryHUDMarker_RefreshOwnVisibility(ShipLogEntryHUDMarker __instance) + { + bool hasEntryLocation = ShipLogEntryHUDMarker.s_entryLocation != null; + bool insideEYE = Locator.GetEyeStateManager() != null && Locator.GetEyeStateManager().IsInsideTheEye(); + bool insideQM = __instance._quantumMoon != null && __instance._quantumMoon.IsPlayerInside(); + bool insideRW = Locator.GetRingWorldController() != null && Locator.GetRingWorldController().isPlayerInside && ShipLogEntryHUDMarker.s_entryLocationID == "IP_RING_WORLD"; + bool insideIP = (hasEntryLocation && ShipLogEntryHUDMarker.s_entryLocation.IsWithinCloakField()) || !(Locator.GetCloakFieldController() != null && Locator.GetCloakFieldController().isPlayerInsideCloak); + bool insideCloak = (hasEntryLocation && ShipLogEntryHUDMarker.s_entryLocation.IsWithinCloakField()) || !Components.CloakSectorController.isPlayerInside; + + __instance._isVisible = (!insideEYE && !insideQM && !insideRW && !__instance._translatorEquipped && !__instance._inConversation && hasEntryLocation && (__instance._isWearingHelmet || __instance._atFlightConsole) && insideIP && insideCloak); + + if (__instance._canvasMarker != null) __instance._canvasMarker.SetVisibility(__instance._isVisible); + + return false; + } + + [HarmonyPostfix] [HarmonyPatch(typeof(ProbeCamera), nameof(ProbeCamera.HasInterference))] public static void ProbeCamera_HasInterference(ProbeCamera __instance, ref bool __result) { - __result = __result || Components.CloakSectorController.isPlayerInside != Components.CloakSectorController.isProbeInside; + __result = __result || (__instance._id != ProbeCamera.ID.PreLaunch && (Components.CloakSectorController.isPlayerInside != Components.CloakSectorController.isProbeInside || !InterferenceHandler.IsPlayerSameAsProbe())); } } } diff --git a/NewHorizons/Patches/MapControllerPatches.cs b/NewHorizons/Patches/MapControllerPatches.cs index dd53f603..3c13673e 100644 --- a/NewHorizons/Patches/MapControllerPatches.cs +++ b/NewHorizons/Patches/MapControllerPatches.cs @@ -1,4 +1,5 @@ using HarmonyLib; +using UnityEngine; using UnityEngine.SceneManagement; namespace NewHorizons.Patches @@ -10,11 +11,11 @@ namespace NewHorizons.Patches [HarmonyPatch(typeof(MapController), nameof(MapController.Awake))] public static void MapController_Awake(MapController __instance) { - __instance._maxPanDistance = Main.FurthestOrbit * 1.5f; + __instance._maxPanDistance = Mathf.Max(__instance._maxPanDistance, Main.FurthestOrbit * 1.5f); __instance._maxZoomDistance *= 6f; __instance._minPitchAngle = -90f; __instance._zoomSpeed *= 4f; - __instance._mapCamera.farClipPlane = Main.FurthestOrbit * 10f; + __instance._mapCamera.farClipPlane = Mathf.Max(__instance._mapCamera.farClipPlane, Main.FurthestOrbit * 10f); } [HarmonyPostfix] diff --git a/NewHorizons/Patches/OWCameraPatch.cs b/NewHorizons/Patches/OWCameraPatch.cs deleted file mode 100644 index 863b56bd..00000000 --- a/NewHorizons/Patches/OWCameraPatch.cs +++ /dev/null @@ -1,32 +0,0 @@ -using HarmonyLib; -namespace NewHorizons.Patches -{ - [HarmonyPatch] - public static class OWCameraPatch - { - [HarmonyPostfix] - [HarmonyPatch(typeof(OWCamera), nameof(OWCamera.Awake))] - public static void OnOWCameraAwake(OWCamera __instance) - { - // var oldDist = __instance.farClipPlane; - // var newDist = __instance.farClipPlane * 10f; - // if (__instance.useFarCamera) Mathf.Clamp(newDist, oldDist, 50000f); - // else newDist = Mathf.Clamp(newDist, oldDist, 10000000f); - // __instance.farClipPlane = newDist; - // __instance.farCameraDistance = newDist; - // __instance.mainCamera.farClipPlane = newDist; - } - - // [HarmonyPrefix] - // [HarmonyPatch(typeof(OWCamera), nameof(OWCamera.RebuildSkybox))] - // public static bool OnOWCameraRebuildSkybox(OWCamera __instance) - // { - // __instance._skyboxCommandBuffer = new CommandBuffer(); - // __instance._skyboxCommandBuffer.name = "Skybox"; - // var camera = __instance._useFarCamera && !SystemInfo.usesReversedZBuffer ? __instance._farCamera : __instance._mainCamera; - // CameraEvent evt = CameraEvent.BeforeSkybox; - // camera.AddCommandBuffer(evt, __instance._skyboxCommandBuffer); - // return false; - // } - } -} diff --git a/NewHorizons/Patches/ProxyBodyPatches.cs b/NewHorizons/Patches/ProxyBodyPatches.cs index 107967ca..23a710df 100644 --- a/NewHorizons/Patches/ProxyBodyPatches.cs +++ b/NewHorizons/Patches/ProxyBodyPatches.cs @@ -25,6 +25,7 @@ namespace NewHorizons.Patches ProxyPlanet_Initialize(__instance); __instance._moon.SetOriginalBodies(Locator.GetAstroObject(AstroObject.Name.VolcanicMoon).transform, Locator.GetAstroObject(AstroObject.Name.BrittleHollow).transform); if (!__instance._fragmentsResolved) __instance.ResolveFragments(); + __instance.AssignBrittleHollowReference(); __instance._blackHoleMaterial = new Material(__instance._blackHoleRenderer.sharedMaterial); __instance._blackHoleRenderer.sharedMaterial = __instance._blackHoleMaterial; } diff --git a/NewHorizons/Patches/RaftPatches.cs b/NewHorizons/Patches/RaftPatches.cs index b6f214ac..fea55497 100644 --- a/NewHorizons/Patches/RaftPatches.cs +++ b/NewHorizons/Patches/RaftPatches.cs @@ -1,5 +1,5 @@ using HarmonyLib; -using NewHorizons.Components; +using NewHorizons.Components.Volumes; using UnityEngine; namespace NewHorizons.Patches { diff --git a/NewHorizons/Patches/ShapePatches.cs b/NewHorizons/Patches/ShapePatches.cs new file mode 100644 index 00000000..cf4d19d0 --- /dev/null +++ b/NewHorizons/Patches/ShapePatches.cs @@ -0,0 +1,29 @@ +using HarmonyLib; +using System.Collections.Generic; + +namespace NewHorizons.Patches +{ + [HarmonyPatch] + public class ShapePatches + { + [HarmonyPrefix] + [HarmonyPatch(typeof(ShapeManager), nameof(ShapeManager.Initialize))] + public static bool ShapeManager_Initialize() + { + ShapeManager._exists = true; + + ShapeManager._detectors = new ShapeManager.Layer(256); + for (int index = 0; index < 256; ++index) + ShapeManager._detectors[index].contacts = new List(64); + + ShapeManager._volumes = new ShapeManager.Layer[4]; + for (int index = 0; index < 4; ++index) + ShapeManager._volumes[index] = new ShapeManager.Layer(2048); + + ShapeManager._locked = false; + ShapeManager._frameFlag = false; + + return false; + } + } +} diff --git a/NewHorizons/Patches/ShipLogPatches.cs b/NewHorizons/Patches/ShipLogPatches.cs index ba588f12..52ce5a74 100644 --- a/NewHorizons/Patches/ShipLogPatches.cs +++ b/NewHorizons/Patches/ShipLogPatches.cs @@ -1,7 +1,6 @@ using HarmonyLib; using NewHorizons.OtherMods.AchievementsPlus; using NewHorizons.Builder.ShipLog; -using NewHorizons.Components; using NewHorizons.Handlers; using NewHorizons.Utility; using System; @@ -10,6 +9,8 @@ using System.Linq; using UnityEngine; using Logger = NewHorizons.Utility.Logger; using Object = UnityEngine.Object; +using NewHorizons.Components.ShipLog; + namespace NewHorizons.Patches { [HarmonyPatch] @@ -230,5 +231,20 @@ namespace NewHorizons.Patches AchievementHandler.OnRevealFact(); } + + [HarmonyPrefix] + [HarmonyPatch(typeof(ShipLogFact), nameof(ShipLogFact.GetText))] + public static bool ShipLogFact_GetText(ShipLogFact __instance, ref string __result) + { + if (ShipLogHandler.IsModdedFact(__instance.GetID())) + { + __result = TranslationHandler.GetTranslation(__instance._text, TranslationHandler.TextType.SHIPLOG); + return false; + } + else + { + return true; + } + } } } \ No newline at end of file diff --git a/NewHorizons/Schemas/addon_manifest_schema.json b/NewHorizons/Schemas/addon_manifest_schema.json index bfe88b86..8d3c0da7 100644 --- a/NewHorizons/Schemas/addon_manifest_schema.json +++ b/NewHorizons/Schemas/addon_manifest_schema.json @@ -19,6 +19,10 @@ "type": "string" } }, + "popupMessage": { + "type": "string", + "description": "A pop-up message for the first time a user runs the add-on" + }, "$schema": { "type": "string", "description": "The schema to validate with" diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index bc88b9f7..a3d243d1 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -100,9 +100,12 @@ "type": "string" } }, - "Ring": { - "description": "Creates a ring around the planet", - "$ref": "#/definitions/RingModule" + "Rings": { + "type": "array", + "description": "Create rings around the planet", + "items": { + "$ref": "#/definitions/RingModule" + } }, "Sand": { "description": "Add sand to this planet", @@ -128,6 +131,17 @@ "description": "Add water to this planet", "$ref": "#/definitions/WaterModule" }, + "Volumes": { + "description": "Add various volumes on this body", + "$ref": "#/definitions/VolumesModule" + }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" @@ -302,6 +316,11 @@ "type": "boolean", "description": "Whether we use an atmospheric shader on the planet. Doesn't affect clouds, fog, rain, snow, oxygen, etc. Purely\nvisual." }, + "hasShockLayer": { + "type": "boolean", + "description": "Whether this atmosphere will have flames appear when your ship goes a certain speed.", + "default": true + }, "minShockSpeed": { "type": "number", "description": "Minimum speed that your ship can go in the atmosphere where flames will appear.", @@ -384,12 +403,14 @@ "x-enumNames": [ "GiantsDeep", "QuantumMoon", - "Basic" + "Basic", + "Transparent" ], "enum": [ "giantsDeep", "quantumMoon", - "basic" + "basic", + "transparent" ] }, "FluidType": { @@ -925,13 +946,6 @@ "$ref": "#/definitions/RaftInfo" } }, - "reveal": { - "type": "array", - "description": "Add triggers that reveal parts of the ship log on this planet", - "items": { - "$ref": "#/definitions/RevealInfo" - } - }, "scatter": { "type": "array", "description": "Scatter props around this planet's surface", @@ -974,13 +988,6 @@ "$ref": "#/definitions/SingularityModule" } }, - "audioVolumes": { - "type": "array", - "description": "Add audio volumes to this planet", - "items": { - "$ref": "#/definitions/AudioVolumeInfo" - } - }, "signals": { "type": "array", "description": "Add signalscope signals to this planet", @@ -1337,61 +1344,6 @@ } } }, - "RevealInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "maxAngle": { - "type": "number", - "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only)", - "format": "float" - }, - "maxDistance": { - "type": "number", - "description": "The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only)", - "format": "float" - }, - "position": { - "description": "The position to place this volume at", - "$ref": "#/definitions/MVector3" - }, - "radius": { - "type": "number", - "description": "The radius of this reveal volume", - "format": "float" - }, - "revealOn": { - "description": "What needs to be done to the volume to unlock the facts", - "default": "enter", - "$ref": "#/definitions/RevealVolumeType" - }, - "reveals": { - "type": "array", - "description": "A list of facts to reveal", - "items": { - "type": "string" - } - }, - "achievementID": { - "type": "string", - "description": "An achievement to unlock. Optional." - } - } - }, - "RevealVolumeType": { - "type": "string", - "description": "", - "x-enumNames": [ - "Enter", - "Observe", - "Snapshot" - ], - "enum": [ - "enter", - "observe", - "snapshot" - ] - }, "ScatterInfo": { "type": "object", "additionalProperties": false, @@ -1442,6 +1394,15 @@ ], "description": "The highest height that these objects will be placed at (only relevant if there's a heightmap)", "format": "float" + }, + "preventOverlap": { + "type": "boolean", + "description": "Should we try to prevent overlap between the scattered details? True by default. If it's affecting load times turn it off.", + "default": true + }, + "keepLoaded": { + "type": "boolean", + "description": "Should this detail stay loaded even if you're outside the sector (good for very large props)" } } }, @@ -1801,70 +1762,6 @@ "whiteHole" ] }, - "AudioVolumeInfo": { - "type": "object", - "additionalProperties": false, - "properties": { - "position": { - "description": "The location of this audio volume. Optional (will default to 0,0,0).", - "$ref": "#/definitions/MVector3" - }, - "radius": { - "type": "number", - "description": "The radius of this audio volume", - "format": "float" - }, - "audio": { - "type": "string", - "description": "The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." - }, - "track": { - "description": "The audio track of this audio volume", - "default": "environment", - "$ref": "#/definitions/AudioMixerTrackName" - } - } - }, - "AudioMixerTrackName": { - "type": "string", - "description": "", - "x-enumNames": [ - "Undefined", - "Menu", - "Music", - "Environment", - "Environment_Unfiltered", - "EndTimes_SFX", - "Signal", - "Death", - "Player", - "Player_External", - "Ship", - "Map", - "EndTimes_Music", - "MuffleWhileRafting", - "MuffleIndoors", - "SlideReelMusic" - ], - "enum": [ - "undefined", - "menu", - "music", - "environment", - "environmentUnfiltered", - "endTimesSfx", - "signal", - "death", - "player", - "playerExternal", - "ship", - "map", - "endTimesMusic", - "muffleWhileRafting", - "muffleIndoors", - "slideReelMusic" - ] - }, "SignalInfo": { "type": "object", "additionalProperties": false, @@ -2173,6 +2070,10 @@ "items": { "$ref": "#/definitions/TimeValuePair" } + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" } } }, @@ -2503,6 +2404,379 @@ "$ref": "#/definitions/MColor" } } + }, + "VolumesModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "audioVolumes": { + "type": "array", + "description": "Add audio volumes to this planet.", + "items": { + "$ref": "#/definitions/AudioVolumeInfo" + } + }, + "hazardVolumes": { + "type": "array", + "description": "Add hazard volumes to this planet.", + "items": { + "$ref": "#/definitions/HazardVolumeInfo" + } + }, + "interferenceVolumes": { + "type": "array", + "description": "Add interference volumes to this planet.", + "items": { + "$ref": "#/definitions/VolumeInfo" + } + }, + "insulatingVolumes": { + "type": "array", + "description": "Add insulating volumes to this planet. These will stop electricty hazard volumes from affecting you (just like the jellyfish).", + "items": { + "$ref": "#/definitions/VolumeInfo" + } + }, + "mapRestrictionVolumes": { + "type": "array", + "description": "Add map restriction volumes to this planet.", + "items": { + "$ref": "#/definitions/VolumeInfo" + } + }, + "notificationVolumes": { + "type": "array", + "description": "Add notification volumes to this planet.", + "items": { + "$ref": "#/definitions/NotificationVolumeInfo" + } + }, + "revealVolumes": { + "type": "array", + "description": "Add triggers that reveal parts of the ship log on this planet.", + "items": { + "$ref": "#/definitions/RevealVolumeInfo" + } + }, + "reverbVolumes": { + "type": "array", + "description": "Add reverb volumes to this planet. Great for echoes in caves.", + "items": { + "$ref": "#/definitions/VolumeInfo" + } + } + } + }, + "AudioVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this volume.", + "format": "float" + }, + "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 volume." + }, + "audio": { + "type": "string", + "description": "The audio to use. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "track": { + "description": "The audio track of this audio volume", + "default": "environment", + "$ref": "#/definitions/AudioMixerTrackName" + }, + "loop": { + "type": "boolean", + "description": "Whether to loop this audio while in this audio volume or just play it once", + "default": true + } + } + }, + "AudioMixerTrackName": { + "type": "string", + "description": "", + "x-enumNames": [ + "Undefined", + "Menu", + "Music", + "Environment", + "Environment_Unfiltered", + "EndTimes_SFX", + "Signal", + "Death", + "Player", + "Player_External", + "Ship", + "Map", + "EndTimes_Music", + "MuffleWhileRafting", + "MuffleIndoors", + "SlideReelMusic" + ], + "enum": [ + "undefined", + "menu", + "music", + "environment", + "environmentUnfiltered", + "endTimesSfx", + "signal", + "death", + "player", + "playerExternal", + "ship", + "map", + "endTimesMusic", + "muffleWhileRafting", + "muffleIndoors", + "slideReelMusic" + ] + }, + "HazardVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this volume.", + "format": "float" + }, + "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 volume." + }, + "type": { + "description": "The type of hazard for this volume.", + "default": "general", + "$ref": "#/definitions/HazardType" + }, + "damagePerSecond": { + "type": "number", + "description": "The amount of damage you will take per second while inside this volume.", + "format": "float", + "default": 10.0 + }, + "firstContactDamageType": { + "description": "The type of damage you will take when you first touch this volume.", + "default": "impact", + "$ref": "#/definitions/InstantDamageType" + }, + "firstContactDamage": { + "type": "number", + "description": "The amount of damage you will take when you first touch this volume.", + "format": "float" + } + } + }, + "HazardType": { + "type": "string", + "description": "", + "x-enumNames": [ + "NONE", + "GENERAL", + "DARKMATTER", + "HEAT", + "FIRE", + "SANDFALL", + "ELECTRICITY", + "RAPIDS" + ], + "enum": [ + "none", + "general", + "ghostMatter", + "heat", + "fire", + "sandfall", + "electricity", + "rapids" + ] + }, + "InstantDamageType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Impact", + "Puncture", + "Electrical" + ], + "enum": [ + "impact", + "puncture", + "electrical" + ] + }, + "VolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this volume.", + "format": "float" + }, + "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 volume." + } + } + }, + "NotificationVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this volume.", + "format": "float" + }, + "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 volume." + }, + "target": { + "description": "What the notification will show for.", + "default": "all", + "$ref": "#/definitions/NotificationTarget" + }, + "entryNotification": { + "description": "The notification that will play when you enter this volume.", + "$ref": "#/definitions/NotificationInfo" + }, + "exitNotification": { + "description": "The notification that will play when you exit this volume.", + "$ref": "#/definitions/NotificationInfo" + } + } + }, + "NotificationTarget": { + "type": "string", + "description": "", + "x-enumNames": [ + "All", + "Ship", + "Player" + ], + "enum": [ + "all", + "ship", + "player" + ] + }, + "NotificationInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "displayMessage": { + "type": "string", + "description": "The message that will be displayed." + }, + "duration": { + "type": "number", + "description": "The duration this notification will be displayed.", + "format": "float", + "default": 5.0 + } + } + }, + "RevealVolumeInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "position": { + "description": "The location of this volume. Optional (will default to 0,0,0).", + "$ref": "#/definitions/MVector3" + }, + "radius": { + "type": "number", + "description": "The radius of this volume.", + "format": "float" + }, + "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 volume." + }, + "maxAngle": { + "type": "number", + "description": "The max view angle (in degrees) the player can see the volume with to unlock the fact (`observe` only)", + "format": "float" + }, + "maxDistance": { + "type": "number", + "description": "The max distance the user can be away from the volume to reveal the fact (`snapshot` and `observe` only)", + "format": "float" + }, + "revealOn": { + "description": "What needs to be done to the volume to unlock the facts", + "default": "enter", + "$ref": "#/definitions/RevealVolumeType" + }, + "reveals": { + "type": "array", + "description": "A list of facts to reveal", + "items": { + "type": "string" + } + }, + "achievementID": { + "type": "string", + "description": "An achievement to unlock. Optional." + } + } + }, + "RevealVolumeType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Enter", + "Observe", + "Snapshot" + ], + "enum": [ + "enter", + "observe", + "snapshot" + ] } }, "$docs": { diff --git a/NewHorizons/Schemas/star_system_schema.json b/NewHorizons/Schemas/star_system_schema.json index b1f5e2cc..f1f8785e 100644 --- a/NewHorizons/Schemas/star_system_schema.json +++ b/NewHorizons/Schemas/star_system_schema.json @@ -5,6 +5,11 @@ "description": "Configuration for a specific star system", "additionalProperties": false, "properties": { + "farClipPlaneOverride": { + "type": "number", + "description": "An override value for the far clip plane. Allows you to see farther.", + "format": "float" + }, "canEnterViaWarpDrive": { "type": "boolean", "description": "Whether this system can be warped to via the warp drive. If you set factRequiredForWarp, this will be true.", @@ -71,6 +76,13 @@ "$ref": "#/definitions/CuriosityColorInfo" } }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" diff --git a/NewHorizons/Utility/AssetBundleUtilities.cs b/NewHorizons/Utility/AssetBundleUtilities.cs index 29841acd..263e08bb 100644 --- a/NewHorizons/Utility/AssetBundleUtilities.cs +++ b/NewHorizons/Utility/AssetBundleUtilities.cs @@ -1,4 +1,4 @@ -using OWML.Common; +using OWML.Common; using System; using System.Collections.Generic; using System.IO; @@ -34,7 +34,7 @@ namespace NewHorizons.Utility } else { - var completePath = mod.ModHelper.Manifest.ModFolderPath + assetBundleRelativeDir; + var completePath = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, assetBundleRelativeDir); bundle = AssetBundle.LoadFromFile(completePath); if (bundle == null) { diff --git a/NewHorizons/Utility/AudioUtilities.cs b/NewHorizons/Utility/AudioUtilities.cs index d78e5b53..4a6801b7 100644 --- a/NewHorizons/Utility/AudioUtilities.cs +++ b/NewHorizons/Utility/AudioUtilities.cs @@ -1,6 +1,7 @@ using OWML.Common; using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using UnityEngine; @@ -19,7 +20,7 @@ namespace NewHorizons.Utility { try { - var clip = LoadAudio(mod.ModHelper.Manifest.ModFolderPath + "/" + audio); + var clip = LoadAudio(Path.Combine(mod.ModHelper.Manifest.ModFolderPath, audio)); source._audioLibraryClip = AudioType.None; source._clipArrayIndex = 0; source._clipArrayLength = 0; diff --git a/NewHorizons/Utility/DebugMenu/DebugMenu.cs b/NewHorizons/Utility/DebugMenu/DebugMenu.cs index 0d25ebfe..ca54f738 100644 --- a/NewHorizons/Utility/DebugMenu/DebugMenu.cs +++ b/NewHorizons/Utility/DebugMenu/DebugMenu.cs @@ -69,7 +69,13 @@ namespace NewHorizons.Utility.DebugMenu PauseMenuInitHook(); - Main.Instance.OnChangeStarSystem.AddListener((string s) => SaveLoadedConfigsForRecentSystem()); + Main.Instance.OnChangeStarSystem.AddListener((string s) => { + if (saveButtonUnlocked) + { + SaveLoadedConfigsForRecentSystem(); + saveButtonUnlocked = false; + } + }); } else { @@ -204,7 +210,7 @@ namespace NewHorizons.Utility.DebugMenu continue; } - loadedConfigFiles[folder + body.RelativePath] = body.Config; + loadedConfigFiles[Path.Combine(folder, body.RelativePath)] = body.Config; submenus.ForEach(submenu => submenu.LoadConfigFile(this, body.Config)); } } @@ -227,10 +233,28 @@ namespace NewHorizons.Utility.DebugMenu var json = loadedConfigFiles[filePath].ToSerializedJson(); + try + { + var path = Path.Combine(loadedMod.ModHelper.Manifest.ModFolderPath, backupFolderName, relativePath); + Logger.LogVerbose($"Backing up... {relativePath} to {path}"); + var oldPath = Path.Combine(loadedMod.ModHelper.Manifest.ModFolderPath, relativePath); + var directoryName = Path.GetDirectoryName(path); + Directory.CreateDirectory(directoryName); + + if (File.Exists(oldPath)) + File.WriteAllBytes(path, File.ReadAllBytes(oldPath)); + else + File.WriteAllText(path, json); + } + catch (Exception e) + { + Logger.LogError($"Failed to save backup file {backupFolderName}{relativePath}:\n{e}"); + } + try { Logger.Log($"Saving... {relativePath} to {filePath}"); - var path = loadedMod.ModHelper.Manifest.ModFolderPath + relativePath; + var path = Path.Combine(loadedMod.ModHelper.Manifest.ModFolderPath, relativePath); var directoryName = Path.GetDirectoryName(path); Directory.CreateDirectory(directoryName); @@ -240,19 +264,6 @@ namespace NewHorizons.Utility.DebugMenu { Logger.LogError($"Failed to save file {relativePath}:\n{e}"); } - - try - { - var path = Main.Instance.ModHelper.Manifest.ModFolderPath + backupFolderName + relativePath; - var directoryName = Path.GetDirectoryName(path); - Directory.CreateDirectory(directoryName); - - File.WriteAllText(path, json); - } - catch (Exception e) - { - Logger.LogError($"Failed to save backup file {backupFolderName}{relativePath}:\n{e}"); - } } } diff --git a/NewHorizons/Utility/DebugUtilities/DebugNomaiTextPlacer.cs b/NewHorizons/Utility/DebugUtilities/DebugNomaiTextPlacer.cs index d26815e8..13bf0523 100644 --- a/NewHorizons/Utility/DebugUtilities/DebugNomaiTextPlacer.cs +++ b/NewHorizons/Utility/DebugUtilities/DebugNomaiTextPlacer.cs @@ -1,3 +1,4 @@ +using NewHorizons.Handlers; using System; using System.Collections.Generic; using System.Linq; @@ -15,13 +16,24 @@ namespace NewHorizons.Utility.DebugUtilities private DebugRaycaster _rc; + private ScreenPrompt _placePrompt; + private void Awake() { _rc = this.GetComponent(); + + _placePrompt = new ScreenPrompt(TranslationHandler.GetTranslation("DEBUG_PLACE_TEXT", TranslationHandler.TextType.UI) + " ", ImageUtilities.GetButtonSprite(KeyCode.G)); + Locator.GetPromptManager().AddScreenPrompt(_placePrompt, PromptPosition.UpperRight, false); } - void Update() + private void OnDestroy() { + Locator.GetPromptManager()?.RemoveScreenPrompt(_placePrompt, PromptPosition.UpperRight); + } + + private void Update() + { + UpdatePromptVisibility(); if (!Main.Debug) return; if (!active) return; @@ -31,5 +43,10 @@ namespace NewHorizons.Utility.DebugUtilities if (onRaycast != null) onRaycast.Invoke(data); } } + + public void UpdatePromptVisibility() + { + _placePrompt.SetVisibility(!OWTime.IsPaused() && Main.Debug && active); + } } } diff --git a/NewHorizons/Utility/DebugUtilities/DebugPropPlacer.cs b/NewHorizons/Utility/DebugUtilities/DebugPropPlacer.cs index c3bb644c..70e85fcc 100644 --- a/NewHorizons/Utility/DebugUtilities/DebugPropPlacer.cs +++ b/NewHorizons/Utility/DebugUtilities/DebugPropPlacer.cs @@ -1,6 +1,7 @@ using NewHorizons.Builder.Props; using NewHorizons.External.Configs; using NewHorizons.External.Modules; +using NewHorizons.Handlers; using System.Collections.Generic; using System.Linq; using UnityEngine; @@ -39,14 +40,36 @@ namespace NewHorizons.Utility.DebugUtilities public GameObject mostRecentlyPlacedPropGO { get { return props.Count() <= 0 ? null : props[props.Count() - 1].gameObject; } } public string mostRecentlyPlacedPropPath { get { return props.Count() <= 0 ? "" : props[props.Count() - 1].detailInfo.path; } } + private ScreenPrompt _placePrompt; + private ScreenPrompt _undoPrompt; + private ScreenPrompt _redoPrompt; + private void Awake() { _rc = this.GetRequiredComponent(); currentObject = DEFAULT_OBJECT; + + _placePrompt = new ScreenPrompt(TranslationHandler.GetTranslation("DEBUG_PLACE", TranslationHandler.TextType.UI) + " ", ImageUtilities.GetButtonSprite(KeyCode.G)); + _undoPrompt = new ScreenPrompt(TranslationHandler.GetTranslation("DEBUG_UNDO", TranslationHandler.TextType.UI) + " ", ImageUtilities.GetButtonSprite(KeyCode.Minus)); + _redoPrompt = new ScreenPrompt(TranslationHandler.GetTranslation("DEBUG_REDO", TranslationHandler.TextType.UI) + " ", ImageUtilities.GetButtonSprite(KeyCode.Equals)); + + Locator.GetPromptManager().AddScreenPrompt(_placePrompt, PromptPosition.UpperRight, false); + Locator.GetPromptManager().AddScreenPrompt(_undoPrompt, PromptPosition.UpperRight, false); + Locator.GetPromptManager().AddScreenPrompt(_redoPrompt, PromptPosition.UpperRight, false); + } + + private void OnDestroy() + { + var promptManager = Locator.GetPromptManager(); + if (promptManager == null) return; + promptManager.RemoveScreenPrompt(_placePrompt, PromptPosition.UpperRight); + promptManager.RemoveScreenPrompt(_undoPrompt, PromptPosition.UpperRight); + promptManager.RemoveScreenPrompt(_redoPrompt, PromptPosition.UpperRight); } private void Update() { + UpdatePromptVisibility(); if (!Main.Debug) return; if (!active) return; @@ -66,6 +89,14 @@ namespace NewHorizons.Utility.DebugUtilities } } + public void UpdatePromptVisibility() + { + var visible = !OWTime.IsPaused() && Main.Debug && active; + _placePrompt.SetVisibility(visible); + _undoPrompt.SetVisibility(visible && props.Count > 0); + _redoPrompt.SetVisibility(visible && deletedProps.Count > 0); + } + public void SetCurrentObject(string s) { currentObject = s; diff --git a/NewHorizons/Utility/DebugUtilities/DebugRaycaster.cs b/NewHorizons/Utility/DebugUtilities/DebugRaycaster.cs index b01e68e5..16f502e8 100644 --- a/NewHorizons/Utility/DebugUtilities/DebugRaycaster.cs +++ b/NewHorizons/Utility/DebugUtilities/DebugRaycaster.cs @@ -1,4 +1,4 @@ -using NewHorizons.Components.Orbital; +using NewHorizons.Handlers; using UnityEngine; using UnityEngine.InputSystem; @@ -17,15 +17,25 @@ namespace NewHorizons.Utility.DebugUtilities private GameObject _planeDownRightSphere; private GameObject _planeDownLeftSphere; + private ScreenPrompt _raycastPrompt; private void Awake() { _rb = this.GetRequiredComponent(); + + _raycastPrompt = new ScreenPrompt(TranslationHandler.GetTranslation("DEBUG_RAYCAST", TranslationHandler.TextType.UI) + " ", ImageUtilities.GetButtonSprite(KeyCode.P)); + + Locator.GetPromptManager().AddScreenPrompt(_raycastPrompt, PromptPosition.UpperRight, false); } + private void OnDestroy() + { + Locator.GetPromptManager()?.RemoveScreenPrompt(_raycastPrompt, PromptPosition.UpperRight); + } private void Update() { + UpdatePromptVisibility(); if (!Main.Debug) return; if (Keyboard.current == null) return; @@ -35,7 +45,12 @@ namespace NewHorizons.Utility.DebugUtilities } } - + + public void UpdatePromptVisibility() + { + _raycastPrompt.SetVisibility(!OWTime.IsPaused() && Main.Debug); + } + internal void PrintRaycast() { diff --git a/NewHorizons/Utility/DebugUtilities/DebugReload.cs b/NewHorizons/Utility/DebugUtilities/DebugReload.cs index b8a513c9..1bb6e639 100644 --- a/NewHorizons/Utility/DebugUtilities/DebugReload.cs +++ b/NewHorizons/Utility/DebugUtilities/DebugReload.cs @@ -31,7 +31,7 @@ namespace NewHorizons.Utility.DebugUtilities { Logger.Log("Begin reload of config files..."); - Main.ResetConfigs(); + Main.Instance.ResetConfigs(); try { diff --git a/NewHorizons/Utility/ImageUtilities.cs b/NewHorizons/Utility/ImageUtilities.cs index 37bc267b..db33fd99 100644 --- a/NewHorizons/Utility/ImageUtilities.cs +++ b/NewHorizons/Utility/ImageUtilities.cs @@ -3,10 +3,10 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using UnityEngine; using UnityEngine.Events; using UnityEngine.Networking; -using UnityEngine.UIElements; namespace NewHorizons.Utility { @@ -17,14 +17,14 @@ namespace NewHorizons.Utility public static bool IsTextureLoaded(IModBehaviour mod, string filename) { - var path = mod.ModHelper.Manifest.ModFolderPath + filename; + var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename); return _loadedTextures.ContainsKey(path); } public static Texture2D GetTexture(IModBehaviour mod, string filename, bool useMipmaps = true, bool wrap = false) { // Copied from OWML but without the print statement lol - var path = mod.ModHelper.Manifest.ModFolderPath + filename; + var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename); if (_loadedTextures.ContainsKey(path)) { Logger.LogVerbose($"Already loaded image at path: {path}"); @@ -53,7 +53,7 @@ namespace NewHorizons.Utility public static void DeleteTexture(IModBehaviour mod, string filename, Texture2D texture) { - var path = mod.ModHelper.Manifest.ModFolderPath + filename; + var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename); if (_loadedTextures.ContainsKey(path)) { if (_loadedTextures[path] == texture) @@ -128,7 +128,7 @@ namespace NewHorizons.Utility var texture = (new Texture2D(size * 4, size * 4, TextureFormat.ARGB32, false)); texture.name = "SlideReelAtlas"; - Color[] fillPixels = new Color[size * size * 4 * 4]; + var fillPixels = new Color[size * size * 4 * 4]; for (int xIndex = 0; xIndex < 4; xIndex++) { for (int yIndex = 0; yIndex < 4; yIndex++) @@ -276,8 +276,8 @@ namespace NewHorizons.Utility { var tex = (new Texture2D(1, 1, TextureFormat.ARGB32, false)); tex.name = "Clear"; - Color fillColor = Color.clear; - Color[] fillPixels = new Color[tex.width * tex.height]; + var fillColor = Color.clear; + var fillPixels = new Color[tex.width * tex.height]; for (int i = 0; i < fillPixels.Length; i++) { fillPixels[i] = fillColor; @@ -296,7 +296,7 @@ namespace NewHorizons.Utility { var tex = (new Texture2D(width, height, TextureFormat.ARGB32, false)); tex.name = src.name + "CanvasScaled"; - Color[] fillPixels = new Color[tex.width * tex.height]; + var fillPixels = new Color[tex.width * tex.height]; for (int i = 0; i < tex.width; i++) { for (int j = 0; j < tex.height; j++) @@ -339,26 +339,40 @@ namespace NewHorizons.Utility } public static Texture2D MakeSolidColorTexture(int width, int height, Color color) { - Color[] pixels = new Color[width*height]; + var pixels = new Color[width*height]; for(int i = 0; i < pixels.Length; i++) { pixels[i] = color; } - Texture2D newTexture = new Texture2D(width, height); + var newTexture = new Texture2D(width, height); newTexture.SetPixels(pixels); newTexture.Apply(); return newTexture; } - + + public static Sprite GetButtonSprite(JoystickButton button) => GetButtonSprite(ButtonPromptLibrary.SharedInstance.GetButtonTexture(button)); + public static Sprite GetButtonSprite(KeyCode key) => GetButtonSprite(ButtonPromptLibrary.SharedInstance.GetButtonTexture(key)); + private static Sprite GetButtonSprite(Texture2D texture) + { + var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100, 0, SpriteMeshType.FullRect, Vector4.zero, false); + sprite.name = texture.name; + return sprite; + } + // Modified from https://stackoverflow.com/a/69141085/9643841 public class AsyncImageLoader : MonoBehaviour { - public List pathsToLoad = new List(); + public List<(int index, string path)> PathsToLoad { get; private set; } = new (); public class ImageLoadedEvent : UnityEvent { } - public ImageLoadedEvent imageLoadedEvent = new ImageLoadedEvent(); + public ImageLoadedEvent imageLoadedEvent = new (); + + private readonly object _lockObj = new(); + + public bool FinishedLoading { get; private set; } + private int _loadedCount = 0; // TODO: set up an optional “StartLoading” and “StartUnloading” condition on AsyncTextureLoader, // and make use of that for at least for projector stuff (require player to be in the same sector as the slides @@ -366,39 +380,59 @@ namespace NewHorizons.Utility void Start() { - for (int i = 0; i < pathsToLoad.Count; i++) + imageLoadedEvent.AddListener(OnImageLoaded); + foreach (var (index, path) in PathsToLoad) { - StartCoroutine(DownloadTexture(pathsToLoad[i], i)); + StartCoroutine(DownloadTexture(path, index)); + } + } + + private void OnImageLoaded(Texture texture, int index) + { + lock (_lockObj) + { + _loadedCount++; + + if (_loadedCount >= PathsToLoad.Count) + { + Logger.LogVerbose($"Finished loading all textures for {gameObject.name} (one was {PathsToLoad.FirstOrDefault()}"); + FinishedLoading = true; + } } } IEnumerator DownloadTexture(string url, int index) { - if (_loadedTextures.ContainsKey(url)) + lock(_loadedTextures) { - Logger.LogVerbose($"Already loaded image at path: {url}"); - var texture = _loadedTextures[url]; - imageLoadedEvent.Invoke(texture, index); - yield break; + if (_loadedTextures.ContainsKey(url)) + { + Logger.LogVerbose($"Already loaded image {index}:{url}"); + var texture = _loadedTextures[url]; + imageLoadedEvent?.Invoke(texture, index); + yield break; + } } - using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(url)) + using UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(url); + + yield return uwr.SendWebRequest(); + + var hasError = uwr.error != null && uwr.error != ""; + + if (hasError) { - yield return uwr.SendWebRequest(); + Logger.LogError($"Failed to load {index}:{url} - {uwr.error}"); + } + else + { + var texture = DownloadHandlerTexture.GetContent(uwr); - var hasError = uwr.error != null && uwr.error != ""; - - if (hasError) // (uwr.result != UnityWebRequest.Result.Success) + lock(_loadedTextures) { - Debug.Log(uwr.error); - } - else - { - var texture = DownloadHandlerTexture.GetContent(uwr); - if (_loadedTextures.ContainsKey(url)) { - Logger.LogVerbose($"Already loaded image at path: {url}"); + Logger.LogVerbose($"Already loaded image {index}:{url}"); Destroy(texture); texture = _loadedTextures[url]; } @@ -407,7 +441,7 @@ namespace NewHorizons.Utility _loadedTextures.Add(url, texture); } - imageLoadedEvent.Invoke(texture, index); + imageLoadedEvent?.Invoke(texture, index); } } } diff --git a/NewHorizons/Utility/NewHorizonsSystem.cs b/NewHorizons/Utility/NewHorizonsSystem.cs index ae6bafa3..be9a539f 100644 --- a/NewHorizons/Utility/NewHorizonsSystem.cs +++ b/NewHorizons/Utility/NewHorizonsSystem.cs @@ -11,15 +11,17 @@ namespace NewHorizons.Utility public class NewHorizonsSystem { public string UniqueID; + public string RelativePath; public SpawnModule Spawn = null; public SpawnPoint SpawnPoint = null; public StarSystemConfig Config; public IModBehaviour Mod; - public NewHorizonsSystem(string uniqueID, StarSystemConfig config, IModBehaviour mod) + public NewHorizonsSystem(string uniqueID, StarSystemConfig config, string relativePath, IModBehaviour mod) { UniqueID = uniqueID; Config = config; + RelativePath = relativePath; Mod = mod; } } diff --git a/NewHorizons/Utility/SearchUtilities.cs b/NewHorizons/Utility/SearchUtilities.cs index 970c33d4..97cb4a5e 100644 --- a/NewHorizons/Utility/SearchUtilities.cs +++ b/NewHorizons/Utility/SearchUtilities.cs @@ -95,38 +95,41 @@ namespace NewHorizons.Utility { if (CachedGameObjects.TryGetValue(path, out var go)) return go; + // 1: normal find go = GameObject.Find(path); - if (go == null) + if (go) { - // find inactive use root + transform.find - var names = path.Split('/'); - var rootName = names[0]; - var root = SceneManager.GetActiveScene().GetRootGameObjects().FirstOrDefault(x => x.name == rootName); - if (root == null) - { - if (warn) Logger.LogWarning($"Couldn't find root object in path {path}"); - return null; - } - - var childPath = string.Join("/", names.Skip(1)); - go = root.FindChild(childPath); - if (go == null) - { - var name = names.Last(); - if (warn) Logger.LogWarning($"Couldn't find object in path {path}, will look for potential matches for name {name}"); - // find resource to include inactive objects - // also includes prefabs but hopefully thats okay - go = FindResourceOfTypeAndName(name); - if (go == null) - { - if (warn) Logger.LogWarning($"Couldn't find object with name {name}"); - return null; - } - } + CachedGameObjects.Add(path, go); + return go; } - CachedGameObjects.Add(path, go); - return go; + // 2: find inactive using root + transform.find + var names = path.Split('/'); + + var rootName = names[0]; + var root = SceneManager.GetActiveScene().GetRootGameObjects().FirstOrDefault(x => x.name == rootName); + + var childPath = string.Join("/", names.Skip(1)); + go = root ? root.FindChild(childPath) : null; + if (go) + { + CachedGameObjects.Add(path, go); + return go; + } + + var name = names.Last(); + if (warn) Logger.LogWarning($"Couldn't find object in path {path}, will look for potential matches for name {name}"); + // 3: find resource to include inactive objects (but skip prefabs + go = Resources.FindObjectsOfTypeAll() + .FirstOrDefault(x => x.name == name && x.scene.name != null); + if (go) + { + CachedGameObjects.Add(path, go); + return go; + } + + if (warn) Logger.LogWarning($"Couldn't find object with name {name}"); + return null; } public static List GetAllChildren(this GameObject parent) diff --git a/NewHorizons/Utility/VersionUtility.cs b/NewHorizons/Utility/VersionUtility.cs new file mode 100644 index 00000000..e7c0ea23 --- /dev/null +++ b/NewHorizons/Utility/VersionUtility.cs @@ -0,0 +1,25 @@ +using System.Linq; +using UnityEngine; + +namespace NewHorizons.Utility +{ + internal static class VersionUtility + { + public static int[] RequiredVersion => new int[] {1, 1, 12}; + public static string RequiredVersionString => string.Join(".", RequiredVersion); + + public static bool CheckUpToDate() + { + // If they're using an outdated game version we create an error popup here + var version = Application.version.Split('.').Select(x => int.Parse(x)).ToArray(); + var major = version[0]; + var minor = version[1]; + var patch = version[2]; + + // Must be at least 1.1.12 + return major > RequiredVersion[0] || + (major == RequiredVersion[0] && minor > RequiredVersion[1]) || + (major == RequiredVersion[0] && minor == RequiredVersion[1] && patch >= RequiredVersion[2]); + } + } +} diff --git a/NewHorizons/manifest.json b/NewHorizons/manifest.json index 5696ae38..015d57e4 100644 --- a/NewHorizons/manifest.json +++ b/NewHorizons/manifest.json @@ -4,9 +4,9 @@ "author": "xen, Bwc9876, clay, MegaPiggy, John, Hawkbar, Trifid, Book", "name": "New Horizons", "uniqueName": "xen.NewHorizons", - "version": "1.5.1", + "version": "1.6.0", "owmlVersion": "2.6.0", - "dependencies": [ "JohnCorby.VanillaFix" ], + "dependencies": [ "JohnCorby.VanillaFix", "_nebula.MenuFramework", "xen.CommonCameraUtility" ], "conflicts": [ "Raicuparta.QuantumSpaceBuddies", "PacificEngine.OW_Randomizer" ], "pathsToPreserve": [ "planets", "systems", "translations" ] } diff --git a/SchemaExporter/SchemaExporter.cs b/SchemaExporter/SchemaExporter.cs index f0c802f5..c2e94e25 100644 --- a/SchemaExporter/SchemaExporter.cs +++ b/SchemaExporter/SchemaExporter.cs @@ -78,16 +78,29 @@ public static class SchemaExporter {"description", _description} }); - if (_title == "Celestial Body Schema") + switch (_title) { - schema.Definitions["OrbitModule"].Properties["semiMajorAxis"].Default = 5000f; + case "Celestial Body Schema": + schema.Definitions["OrbitModule"].Properties["semiMajorAxis"].Default = 5000f; + break; + case "Star System Schema": + schema.Definitions["NomaiCoordinates"].Properties["x"].UniqueItems = true; + schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; + schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; + break; } - if (_title == "Star System Schema") + if (_title is "Star System Schema" or "Celestial Body Schema") { - schema.Definitions["NomaiCoordinates"].Properties["x"].UniqueItems = true; - schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; - schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; + schema.Properties["extras"] = new JsonSchemaProperty { + Type = JsonObjectType.Object, + Description = "Extra data that may be used by extension mods", + AllowAdditionalProperties = true, + AdditionalPropertiesSchema = new JsonSchema + { + Type = JsonObjectType.Object + } + }; } return schema; diff --git a/SchemaExporter/SchemaExporter.csproj b/SchemaExporter/SchemaExporter.csproj index 2c55f3ad..40ec8c5e 100644 --- a/SchemaExporter/SchemaExporter.csproj +++ b/SchemaExporter/SchemaExporter.csproj @@ -20,7 +20,7 @@ - + diff --git a/docs/content/pages/editor.md b/docs/content/pages/editor.md new file mode 100644 index 00000000..fd314789 --- /dev/null +++ b/docs/content/pages/editor.md @@ -0,0 +1,65 @@ +--- +Title: Config Editor +Sort_Priority: 50 +--- + +# Config Editor + +Are you tired of manually editing JSON? Do you want richer validation than a JSON schema? Well then the config editor may be for you! + +This page outlines how to install and use the config editor. + +## Installation + +To get started, head over to the [releases page for the editor](https://github.com/Outer-Wilds-New-Horizons/nh-config-editor/releases/latest) and install the file for your OS: + +- Windows: The .msi file (not the .msi.zip and .msi.zip.sig file) +- MacOS: The .AppImage file (not the .AppImage.tar.gz or the .AppImage.tar.gz.sig file) + +Follow the installer instructions to complete setup + +## Creating a New Project + +Creating a new project is as simple as clicking the button. +Fill out the form with thr info for your mod and a new project will be made at the specified path. + +## Editing Files + +To edit a file, navigate to it in the left panel and click on it. + +### JSON files + +JSON files (planets, systems, etc) have a graphical interface for editing, **this will clear comments!** + +If you don't want comments to be cleared, use the text editor + +#### Using the Text Editor + +Already familiar with JSON and prefer text-based editing? Simply open up settings (File -> Settings) and turn on the "Always use Text Editor" option. + +### Image and Audio Files + +You can view images and play audio files with this editor. + +### XML Files + +Right now, XML support is limited. You'll get syntax highlighting but no error checking or autofill. + + +## Running the Game + +You can start the game from the editor by selecting Project -> Run Project this will open a new window where you can run the game + +### Log Port + +If you're using the mod manager and would like logs to appear there, you need to get the log port from the console, it's always the first entry in the logs. Keep in mind this port changes whenever you restart the manager. + +![Get the log port]({{ "images/editor/log_port.webp"|static }}) + + +## Building + +The editor also provides a system for building your mod to a zip file, which can then be uploaded to GitHub in a release. To do this, press Project -> Build (Release) + + + diff --git a/docs/content/pages/faq.jinja2 b/docs/content/pages/faq.jinja2 index c5b95209..717f0d4a 100644 --- a/docs/content/pages/faq.jinja2 +++ b/docs/content/pages/faq.jinja2 @@ -46,14 +46,14 @@ faq( "ui-program", "Will you make a UI program to generate json files in the future?", - "Maybe! Will have to wait until New Horizons gets to version 1.0.0." + "Yes! It's available [on GitHub](https://github.com/Outer-Wilds-New-Horizons/nh-config-editor){ target='_blank' }." ) }} {{ faq( "when-version-1", "When will New Horizons get to version 1.0.0.", - "Soon/eventually/never/yesterday." + "It already did **BOZO**!!!!!" ) }} {{ diff --git a/docs/content/pages/home.md b/docs/content/pages/home.md index 06196020..6fc7c70e 100644 --- a/docs/content/pages/home.md +++ b/docs/content/pages/home.md @@ -8,118 +8,11 @@ Sort_Priority: 100 # Outer Wilds New Horizons -This is the official documentation for [New Horizons](https://github.com/xen-42/outer-wilds-new-horizons), a framework for creating custom planets in the game [Outer Wilds](https://www.mobiusdigitalgames.com/outer-wilds.html) by Mobius Digital. Planets are created using simple JSON and XML files and are loaded at runtime. An [API]({{ "API"|route }}) is also provided for more advanced use-cases. +This is the official documentation for [New Horizons](https://github.com/xen-42/outer-wilds-new-horizons), a framework for creating custom planets in the game [Outer Wilds](https://www.mobiusdigitalgames.com/outer-wilds.html) by Mobius Digital. Planets are created using simple JSON and XML files and are loaded at runtime. An [API]({{ "API"|route }}) is also provided for more advanced use-cases. ## Getting Started -Before starting, go into your in-game mod settings for New Horizons and switch Debug mode on. This allows you to: - -- Use the [Prop Placer tool]({{ "detailing"|route }}#using-the-prop-placer). This convienence tool allows you to place details in game and save your work to your config files. -- Print the position of what you are looking at to the logs by pressing "P". This is useful for determining locations to place props the Prop Placer is unable to, such as signal scope points or dialogue triggers. -- Use the "Reload Configs" button in the pause menu. This will restart the current solar system and update all the planets. Much faster than quitting and relaunching the game. - -!!! alert-danger "Get VSCode" - Please get [VSCode](https://code.visualstudio.com/){ target="_blank" } or some other advanced text editor, as it will help highlight common errors. - -Planets are created using a JSON file format structure, and placed in a folder called planets (or in any subdirectory of it) in the location where New Horizons is installed (by default this folder doesn't exist, you have to create it within the xen.NewHorizons directory). You can learn how the configs work by picking apart the [Real Solar System](https://github.com/xen-42/outer-wilds-real-solar-system){ target="_blank" } mod or the [New Horizons Examples](https://github.com/xen-42/ow-new-horizons-examples){ target="_blank" } mod. - -To locate this directory, click the "⋮" symbol next to "New Horizons" in the Outer Wilds Mod Manager and then click " -show in explorer" in the pop-up. - -![Click the three dots in the mod manager]({{ "images/home/mod_manager_dots.webp"|static }}) - -![Create a new folder named "planets"]({{ "images/home/create_planets.webp"|static }}) - -Planets can also be placed in a folder called planets within a separate mod, if you plan on releasing your planets on the mod database. The [Config Template](https://github.com/xen-42/ow-new-horizons-config-template){ target="_blank" } is available if you want to release your own planet mod using configs. - -Now that you have created your planets folder, this is where you will put your planet config files. A config file will -look something like this: - -```json -{ - "name": "Wetrock", - "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/body_schema.json", - "starSystem": "SolarSystem", - "Base": { - "groundSize": 100, - "surfaceSize": 101, - "surfaceGravity": 12, - "hasMapMarker": true - }, - "Orbit": { - "semiMajorAxis": 1300, - "inclination": 0, - "primaryBody": "TIMBER_HEARTH", - "isMoon": true, - "isTidallyLocked": true, - "longitudeOfAscendingNode": 0, - "eccentricity": 0, - "argumentOfPeriapsis": 0 - }, - "Atmosphere": { - "size": 150, - "fogTint": { - "r": 200, - "g": 255, - "b": 255, - "a": 255 - }, - "fogSize": 150, - "fogDensity": 0.2, - "hasRain": true - }, - "Props": { - "scatter": [ - { - "path": "DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_1/Props_DreamZone_1/OtherComponentsGroup/Trees_Z1/DreamHouseIsland/Tree_DW_M_Var", - "count": 12 - } - ] - } -} -``` - -The first field you should have in any config file is the `name`. This should be unique in the solar system. If it -isn't, the mod will instead try to modify the planet that already has that name. - -After `name` is `starSystem`. You can use this to place the planet in a different system accessible using a black-hole or via the ship's warp drive (accessible from the ship log computer). To ensure compatibility with other mods this name should be unique. After setting a value for this, the changes in the config will only affect that body in that star system. By default, it is "SolarSystem", which is the scene from the stock game. - -Including the "$schema" line is optional, but will allow your text editor to highlight errors and auto-suggest words in your config. I recommend using VSCode as a text editor, but anything that supports Json files will work. Something as basic as notepad will work but will not highlight any of your errors. - -The config file is then split into modules, each one with its own fields that define how that part of the planet will be generated. In the example above I've used the `Base`, `Orbit`, `Atmosphere`, and `Props` modules. A config file must have a `Base` and `Orbit` module, the rest are optional. - -Each `{` must match up with a closing `}` to denote its section. If you don't know how JSONs work then check Wikipedia. - -Modules look like this: - -```json -{ - "Star": { - "size": 3000, - "tint": { - "r": 201, - "g": 87, - "b": 55, - "a": 255 - } - } -} -``` - -In this example the `Star` module has a `size` field and a `tint` field. Since the colour is a complex object it needs -another set of `{` and `}` around it, and then it has its own fields inside it : `r`, `g`, `b`, and `a`. Don't forget to put -commas after each field. - -Most fields are either true/false, a decimal number, and integer number, or a string (word with quotation marks around -it). - -To see all the different things you can put into a config file check out the [Celestial Body schema]({{ 'Celestial Body Schema'|route}}). - -Check out the rest of the site for how to format [star system]({{ 'Star System Schema'|route}}), [dialogue]({{ 'Dialogue Schema'|route}}), [ship log]({{ 'Shiplog Schema'|route}}), and [translation]({{ 'Translation Schema'|route}}) files! - -## Publishing Your Mod - -Once your mod is complete, you can use the [planet creation template](https://github.com/xen-42/ow-new-horizons-config-template#readme){ target="_blank" } GitHub template. +For a guide on getting started with New Horizons, go to the [Getting Started]({{ "Getting Started"|route }}) page. ## Helpful Resources diff --git a/docs/content/pages/tutorials/api.md b/docs/content/pages/tutorials/api.md index 8689943c..2afd6ffe 100644 --- a/docs/content/pages/tutorials/api.md +++ b/docs/content/pages/tutorials/api.md @@ -1,6 +1,6 @@ --- Title: API -Sort_Priority: 40 +Sort_Priority: 20 --- ## How to use the API @@ -9,21 +9,99 @@ First create the following interface in your mod: ```cs public interface INewHorizons -{ - void LoadConfigs(IModBehaviour mod); + { + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config); - GameObject GetPlanet(string name); - - string GetCurrentStarSystem(); + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config, IModBehaviour mod); - UnityEvent GetChangeStarSystemEvent(); + /// + /// Will load all configs in the regular folders (planets, systems, translations, etc) for this mod. + /// The NH addon config template is just a single call to this API method. + /// + void LoadConfigs(IModBehaviour mod); - UnityEvent GetStarSystemLoadedEvent(); - - GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); + /// + /// Retrieve the root GameObject of a custom planet made by creating configs. + /// Will only work if the planet has been created (see GetStarSystemLoadedEvent) + /// + GameObject GetPlanet(string name); - string[] GetInstalledAddons(); -} + /// + /// The name of the current star system loaded. + /// + string GetCurrentStarSystem(); + + /// + /// An event invoked when the player begins loading the new star system, before the scene starts to load. + /// Gives the name of the star system being switched to. + /// + UnityEvent GetChangeStarSystemEvent(); + + /// + /// An event invoked when NH has finished generating all planets for a new star system. + /// Gives the name of the star system that was just loaded. + /// + UnityEvent GetStarSystemLoadedEvent(); + + /// + /// An event invoked when NH has finished a planet for a star system. + /// Gives the name of the planet that was just loaded. + /// + UnityEvent GetBodyLoadedEvent(); + + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); + + /// + /// Uses JSONPath to query a system + /// + object QuerySystem(Type outType, string path); + + /// + /// Allows you to overwrite the default system. This is where the player is respawned after dying. + /// + bool SetDefaultSystem(string name); + + /// + /// Allows you to instantly begin a warp to a new star system. + /// Will return false if that system does not exist (cannot be warped to). + /// + bool ChangeCurrentStarSystem(string name); + + /// + /// Returns the uniqueIDs of each installed NH addon. + /// + string[] GetInstalledAddons(); + + /// + /// Allows you to spawn a copy of a prop by specifying its path. + /// This is the same as using Props->details in a config, but also returns the spawned gameObject to you. + /// + GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, + float scale, bool alignWithNormal); + + /// + /// Allows you to spawn an AudioSignal on a planet. + /// This is the same as using Props->signals in a config, but also returns the spawned AudioSignal to you. + /// This method will not set its position. You will have to do that with the returned object. + /// + AudioSignal SpawnSignal(IModBehaviour mod, GameObject root, string audio, string name, string frequency, + float sourceRadius = 1f, float detectionRadius = 20f, float identificationRadius = 10f, bool insideCloak = false, + bool onlyAudibleToScope = true, string reveals = ""); + + /// + /// Allows you to spawn character dialogue on a planet. Also returns the RemoteDialogueTrigger if remoteTriggerRadius is specified. + /// This is the same as using Props->dialogue in a config, but also returns the spawned game objects to you. + /// This method will not set the position of the dialogue or remote trigger. You will have to do that with the returned objects. + /// + (CharacterDialogueTree, RemoteDialogueTrigger) SpawnDialogue(IModBehaviour mod, GameObject root, string xmlFile, float radius = 1f, + float range = 1f, string blockAfterPersistentCondition = null, float lookAtRadius = 1f, string pathToAnimController = null, + float remoteTriggerRadius = 0f); + } ``` In your main `ModBehaviour` class you can get the NewHorizons API like so: @@ -33,7 +111,7 @@ public class MyMod : ModBehaviour { void Start() { - INewHorizons NewHorizonsAPI = ModHelper.Interaction.GetModApi("xen.NewHorizons"); + INewHorizons NewHorizonsAPI = ModHelper.Interaction.TryGetModApi("xen.NewHorizons"); } } ``` diff --git a/docs/content/pages/tutorials/creating_addon.md b/docs/content/pages/tutorials/creating_addon.md new file mode 100644 index 00000000..fc88919f --- /dev/null +++ b/docs/content/pages/tutorials/creating_addon.md @@ -0,0 +1,83 @@ +--- +Title: Creating An Addon +Sort_Priority: 85 +--- + +# Creating An Addon + +Up until now, you've been using the sandbox feature of New Horizons (simply placing your files in the `xen.NewHorizons` folder). +While this is the easiest way to get started, you won't be able to publish your work like this. In this tutorial we will: + +- Create a new GitHub repository from a template +- Use GitHub Desktop to clone this repository to our computer +- Edit the files in this repository to make our addon + +## Making a GitHub Repository + +To get started, we need a place to store our code. GitHub is one of the most popular websites to store source code, and it's also what the mod database uses to let people access our mod. +First you're going to want to [create a GitHub account](https://github.com/signup){ target="_blank" }, and then head to [this repository](https://github.com/xen-42/ow-new-horizons-config-template){ target="_blank" }. +Now, click the green "Use This Template" button. + +- Set the Name to your username followed by a dot (`.`), followed by your mod's name in PascalCase (no spaces, new words have capital letters). So for example if my username was "Test" and my mod's name was "Really Cool Addon", I would name the repo `Test.ReallyCoolAddon`. +- The description is what will appear in the mod manager under the mod's name, you can always edit it later +- You can set the visibility to what you want; But when you go to publish your mod, it will need to be public + +## Cloning the Repository + +Now that we've created our GitHub repository (or "repo"), we need to clone (or download) it onto our computer. +To do this we recommend using the [GitHub Desktop App](https://desktop.github.com/){ target="_blank" }, as it's much easier to use than having to fight with the command line. + +Once we open GitHub desktop we're going to log in, select File -> Options -> Accounts and sign in to your newly created GitHub account. +Now we're ready to clone the repo, select File -> Clone Repository. Your repository should appear in the list. +Before you click "Clone", we need to select where to store the repo, open up the mod manager and go to "Settings", then copy the value located in the "OWML path" field and paste it in the "Local path" field on GitHub desktop. +This *will* show an error, and this is going to sound extremely stupid, but just click the "Choose..." button, and press "Select Folder" and it will be fixed. + +Our repository is now cloned to our computer! + +## Editing Files + +Now that our repo is cloned, we're going to need to edit the files in it. +To get started editing the files, simply click "Open in Visual Studio Code" in GitHub Desktop. + +### Files Explanation + +- .github: This folder contains special files for use on GitHub, they aren't useful right now but will be when we go to publish the mod +- planets: This folder contains a single example config file that destroys the Quantum Moon, we'll keep it for now so we can test our addon later. +- .gitattributes: This is another file that will be useful when publishing +- default-config.json: This file is used in C#-based mods to allow a custom options menu, New Horizons doesn't support a custom options menu, but we still need the file here in order for the addon to work. +- manifest.json: This is the first file we're going to edit, we need to fill it out with information about our mod + - First you're going to set `author` to your author name, this should be the same name that you used when creating the GitHub repo. + - Next, set `name` to the name you want to appear in the mod manager and website. + - Now set `uniqueName` to the name of your GitHub Repo. + - You can leave `version`, `owmlVersion`, and `dependencies` alone +- NewHorizonsConfig.dll: This is the heart of your addon, make sure to never move or rename it. +- README.md: This file is displayed on the mod website when you go to a specific mod's page, you can delete the current contents. + - This file is a [markdown](https://www.markdowntutorial.com/){ target="_blank" } file, if you're not comfortable writing an entire README right now, just write a small description of your mod. + +### Committing The Changes + +Now that we have our files set up, switch back to GitHub desktop, you'll notice that the files you've changed have appeared in a list on the left. +What GitHub Desktop does is keep track of changes you make to your files over time. +Then, once you're ready, you commit these changes to your repo by filling out the "Summary" field with a small description of your changes, and then pressing the blue button that says "commit to main". + +Think of committing like taking a snapshot of your project at this moment in time. If you ever mess up your project, you can always revert to another commit to get back to a working version. It is highly recommended to commit often, there is no downside to committing too much. + +### Pushing The Changes + +OK, so we've committed our new changes, but these commits still only exist on our computer, to get these changes onto GitHub we can click the "Push Origin" button on the top right. + +## Testing The Addon + +Now that we have our manifest filled out, go take a look at the "Mods" tab in the manager and scroll to the bottom of the "Enabled Mods" list. + +You should see your mod there with the downloads counter set as a dash and the version set to "0.0.0". + +### Checking In-Game + +Now when you click "Start Game" and load into the solar system, you should be able to notice that the quantum moon is gone entirely, this means that your addon and its configs were successfully loaded. + +## Going Forward + +Now instead of using the New Horizons mod folder, you can use your own mod's folder instead. + +**Next Up: [Planet Generation]({{ "Planet Generation"|route }})** diff --git a/docs/content/pages/tutorials/details.md b/docs/content/pages/tutorials/details.md index c8483065..1c4c0cae 100644 --- a/docs/content/pages/tutorials/details.md +++ b/docs/content/pages/tutorials/details.md @@ -1,6 +1,6 @@ --- Title: Detailing -Sort_Priority: 85 +Sort_Priority: 80 --- # Details/Scatterer @@ -20,7 +20,7 @@ The Prop Placer is a convenience tool that lets you manually place details from 1. Pause the game. You will see an extra menu option titled "Toggle Prop Placer Menu". Click it 2. The prop placer menu should now be open. At the bottom of the menu, you will see a list of mods. Click yours. 1. This menu scrolls. If you do not see your mod, it may be further down the list. -3. The Prop Placer is now active! Unpause the game and you can now place Nomai vases using "G" +3. The Prop Placer is now active! Unpause the game, and you can now place Nomai vases using "G" ### How to Save @@ -39,7 +39,7 @@ What's that? You want to place something other than just vases? Well I can't say ### How to Select Props 1. Pause the game again. The prop placer menu should still be visible. -2. At the top of the menu, you'll see a text box contianing the path for the vase. Replace this with the path for the prop you want to place. For example: `DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_1/Props_DreamZone_1/OtherComponentsGroup/Trees_Z1/DreamHouseIsland/Tree_DW_M_Var` +2. At the top of the menu, you'll see a text box containing the path for the vase. Replace this with the path for the prop you want to place. For example: `DreamWorld_Body/Sector_DreamWorld/Sector_DreamZone_1/Props_DreamZone_1/OtherComponentsGroup/Trees_Z1/DreamHouseIsland/Tree_DW_M_Var` 3. Tip: use the Unity Explorer mod to find the path for the object you want to place. You only have to do this once. 4. Unpause the game and press "G". Say hello to your new tree! 5. Pause the game again. You will now see the prop you just placed on the list of recently placed props just below the "path" text box. diff --git a/docs/content/pages/tutorials/dialogue.md b/docs/content/pages/tutorials/dialogue.md index 88647927..bd44c160 100644 --- a/docs/content/pages/tutorials/dialogue.md +++ b/docs/content/pages/tutorials/dialogue.md @@ -1,36 +1,38 @@ --- Title: Dialogue Description: Guide to making dialogue in New Horizons -Sort_Priority: 50 +Sort_Priority: 30 --- # Dialogue This page goes over how to use dialogue in New Horizons. -# Understanding Dialogue +You may want to view [Understanding XML]({{ "Understanding XML"|route }}) if you haven't already. -## Dialogue Tree +## Understanding Dialogue + +### Dialogue Tree A dialogue tree is an entire conversation, it's made up of dialogue nodes. -## Dialogue Node +### Dialogue Node A node is a set of pages shown to the player followed by options the player can choose from to change the flow of the conversation. -## Condition +### Condition A condition is a yes/no value stored **for this loop and this loop only**. It can be used to show new dialogue options, stop someone from talking to you (looking at you Slate), and more. -## Persistent Condition +### Persistent Condition -A persistent condition is similar to a condition, except it *persists* through loops, and is saved on the player's save file. +A persistent condition is similar to a condition, except it *persists* through loops, and is saved on the players save file. -## Remote Trigger +### Remote Trigger A remote trigger is used to have an NPC talk to you from a distance; ex: Slate stopping you for the umpteenth time to tell you information you already knew. -# Example XML +## Example XML Here's an example dialogue XML: @@ -113,7 +115,7 @@ Here's an example dialogue XML: ``` -# Using the XML +## Using the XML To use the dialogue XML you have created, you simply need to reference it in the `dialogue` prop @@ -130,11 +132,11 @@ To use the dialogue XML you have created, you simply need to reference it in the } ``` -# Dialogue Config +## Dialogue Config To view the options for the dialogue prop, check [the schema]({{ "Celestial Body Schema"|route }}#Props_dialogue) -# Controlling Conditions +## Controlling Conditions You can set condition in dialogue with the `` and `` tags @@ -147,18 +149,18 @@ You can set condition in dialogue with the `` and ` ``` -# Dialogue Options +## Dialogue Options There are many control structures for dialogue options to hide/reveal them if conditions are met. Take a look at [the DialogueOption schema]({{ "Dialogue Schema"|route }}#DialogueTree-DialogueNode-DialogueOptionsList-DialogueOption-DialogueTarget) for more info. -# Controlling Flow +## Controlling Flow In addition to ``, there are other ways to control the flow of the conversation. -## DialogueTarget +### DialogueTarget Defining `` in the `` tag instead of a `` will make the conversation go directly to that target after the character is done talking. -## DialogueTargetShipLogCondition +### DialogueTargetShipLogCondition -Used in tandum with `DialogueTarget`, makes it so you must have a [ship log fact]({{ "Ship Log"|route }}#explore-facts) to go to the next node. +Used in tandem with `DialogueTarget`, makes it so you must have a [ship log fact]({{ "Ship Log"|route }}#explore-facts) to go to the next node. diff --git a/docs/content/pages/tutorials/extending.md b/docs/content/pages/tutorials/extending.md new file mode 100644 index 00000000..754140d9 --- /dev/null +++ b/docs/content/pages/tutorials/extending.md @@ -0,0 +1,74 @@ +--- +Title: Extending Configs +Description: A guide on extending config files with the New Horizons API +Sort_Priority: 5 +--- + +# Extending Configs + +This guide will explain how to use the API to add new features to New Horizons. + +## How Extending Works + +Addon developers will add a key to the `extras` object in the root of the config + +```json +{ + "name": "Wetrock", + "extras": { + "myCoolExtensionData": { + "myCoolExtensionProperty": 2 + } + } +} +``` + +Your mod will then use the APIs `QueryBody` method to obtain the `myCoolExtensionData` object. + +**It's up to the addon dev to list your mod as a dependency!** + +## Extending Planets + +You can extend all planets by hooking into the `OnBodyLoaded` event of the API: + +```csharp +var api = ModHelper.Interactions.TryGetModApi("xen.NewHorizons"); +api.GetBodyLoadedEvent().AddListener((name) => { + ModHelper.Console.WriteLine($"Body: {name} Loaded!"); +}); +``` + +In order to get your extra module, first define the module as a class: + +```csharp +public class MyCoolExtensionData { + int myCoolExtensionProperty; +} +``` + +Then, use the `QueryBody` method: + +```csharp +var api = ModHelper.Interactions.TryGetModApi("xen.NewHorizons"); +api.GetBodyLoadedEvent().AddListener((name) => { + ModHelper.Console.WriteLine($"Body: {name} Loaded!"); + var potentialData = api.QueryBody(typeof(MyCoolExtensionData), "$.extras.myCoolExtensionData", name); + // Makes sure the module is valid and not null + if (potentialData is MyCoolExtensionData data) { + ModHelper.Console.WriteLine($"myCoolExtensionProperty for {name} is {data.myCoolExtensionProperty}!"); + } +}); +``` + +## Extending Systems + +Extending systems is the exact same as extending planets, except you use the `QuerySystem` method instead. + +## Accessing Other Values + +You can also use the `QueryBody` method to get values of the config outside your extension object + +```csharp +var primaryBody = api.QueryBody(typeof(string), "Wetrock", "$.Orbit.primaryBody"); + ModHelper.Console.WriteLine($"Primary of {bodyName} is {primaryBody ?? "NULL"}!"); +``` diff --git a/docs/content/pages/tutorials/getting_started.md b/docs/content/pages/tutorials/getting_started.md new file mode 100644 index 00000000..1b2da307 --- /dev/null +++ b/docs/content/pages/tutorials/getting_started.md @@ -0,0 +1,259 @@ +--- +Title: Getting Started +Sort_Priority: 100 +--- + +# Getting Started + +Congrats on taking the first step to becoming an addon developer! +This tutorial will outline how to begin learning to use new horizons. + +## Recommended Tools + +It's strongly recommended you get [VSCode](https://code.visualstudio.com/){ target="_blank" } to edit your files, as it can provide syntax and error highlighting. + +## Using The Sandbox + +Making an entirely separate addon can get a little complicated, so New Horizons provides a way to play around without the need to set up a full addon. +To get started, navigate to your mod manager and click the ⋮ symbol, then select "Show In Explorer". + +![Select "Show in explorer"]({{ "images/getting_started/mod_manager_show_in_explorer.webp"|static }}) + +Now, create a new folder named "planets". As the name suggests, New Horizons will search the files in this folder for planets to generate. + +## Making Your First Planet + +To get started, create a new file in this folder called `wetrock.json`, we'll explain what that .json at the end means soon. +Open this file in VSCode (you can do so by right-clicking the file and clicking "Open with Code") +Once in VSCode, paste this code into the file: + +```json +{ + "name": "Wetrock", + "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/body_schema.json", + "starSystem": "SolarSystem", + "Base": { + "groundSize": 100, + "surfaceSize": 101, + "surfaceGravity": 12, + "hasMapMarker": true + }, + "Orbit": { + "semiMajorAxis": 1300, + "primaryBody": "TIMBER_HEARTH", + "isMoon": true, + "isTidallyLocked": true + }, + "Atmosphere": { + "size": 150, + "fogTint": { + "r": 200, + "g": 255, + "b": 255, + "a": 255 + }, + "fogSize": 150, + "fogDensity": 0.2, + "hasRain": true + } +} +``` + +This language is **J**ava**S**cript **O**bject **N**otation, or JSON. +It's a common way to convey data in many programs. + +## Understanding JSON + +All JSON files start out with an `object`, or a set of key value mappings, for example if we represent a person as JSON it might look like: + +```json +{ + "name": "Jim" +} +``` + +Those braces (`{}`) denote an object, and by doing `"name": "Jim"` we're saying that the name of this person is Jim; +`"name"` is the key, and `"Jim"` is the value. + +Objects can have multiple keys as well, as long as you separate them by commas: + +```json +{ + "name": "Jim", + "age": 23 +} +``` + +But wait! why is `Jim` in quotation marks while `23` isn't? that's because of something called data types. +Each value has a datatype, in this case `"Jim"` is a `string`, because it represents a *string* of characters. +Age is a `number`, it represents a numerical value. If we put 23 in quotation marks, its data type switches from a number to a string. +And if we remove the quotation marks from `"Jim"` we get a syntax error (a red underline). Datatypes are a common source of errors, which is why we recommend using an editor like VSCode. + +### JSON Data Types + +Here's a list of data types you'll use when making your addons: + +#### String + +A set of characters surrounded in quotation marks + +```json +"Im a string!" +``` + +If you need to use quotation marks within your string, place a backslash (`\`) before them + +```json +"\"Im a string!\" - Mr. String Stringerton" +``` + +#### Number + +A numerical value, can be negative and have decimals, **not** surrounded in quotation marks + +```json +-25.3 +``` + +#### Boolean + +A `true` or `false` value, think of it like an on or off switch, also not surrounded in quotation marks + +```json +true +``` + +```json +false +``` + +#### Array + +A set of values, values can be of any data type. Items are seperated by commas. + +```json +[23, 45, 56] +``` + +```json +["Bob", "Suzy", "Mark"] +``` + +And they can be empty like so: + +```json +[] +``` + +#### Object + +A set of key value pairs, where each key is a string and each value can be of any data type (even other objects!) + +```json +{ + "name": "Jim", + "age": 23, + "isMarried": false, + "clothes": { + "shirtColor": "red", + "pantsColor": "blue" + }, + "friends": ["Bob", "Wade"], + "enemies": [] +} +``` + +## Back to Wetrock + +Now that we understand JSON better, let's look at that config file again: + +```json +{ + "name": "Wetrock", + "$schema": "https://raw.githubusercontent.com/xen-42/outer-wilds-new-horizons/main/NewHorizons/Schemas/body_schema.json", + "starSystem": "SolarSystem", + "Base": { + "groundSize": 100, + "surfaceSize": 101, + "surfaceGravity": 12, + "hasMapMarker": true + }, + "Orbit": { + "semiMajorAxis": 1300, + "primaryBody": "TIMBER_HEARTH", + "isMoon": true, + "isTidallyLocked": true + }, + "Atmosphere": { + "size": 150, + "fogTint": { + "r": 200, + "g": 255, + "b": 255, + "a": 255 + }, + "fogSize": 150, + "fogDensity": 0.2, + "hasRain": true + } +} +``` + +Here we can see we have a planet object, which name is "Wetrock", and is in the "SolarSystem" (Base-game) star system. +It has an object called Base, which has a groundSize of 100, and a surfaceSize of 101, and the list continues on. + +Alright so now that we understand how the file is structures, let's look into what each value actually does: + +- `name` simply sets the name of the planet +- `$schema` we'll get to in a second +- `starSystem` specifies what star system this planet is located in, in this case we're using the base game star system, so we put "SolarSystem" + - Then it has an object called `Base` + - Base has a `groundSize` of 100, this generates a perfect sphere that is 100 units in radius as the ground of our planet + - It also has a `surfaceSize` of 101, surface size is used in many calculations, it's generally good to set it to a bit bigger than ground size. + - `surfaceGravity` describes the strength of gravity on this planet, in this case it's 12 which is the same as Timber Hearth + - `hasMapMarker` tells new horizons that we want this planet to have a marker on the map screen + - Next it has another object called `Orbit` + - `semiMajorAxis` specifies the radius of the orbit (how far away the body is from its parent) + - `primaryBody` is set to TIMBER_HEARTH, this makes our planet orbit timber hearth + - `isMoon` simply tells the game how close you have to be to the planet in map mode before its name appears + - `isTidallyLocked` makes sure that one side of our planet is always facing timber hearth (the primary body) + - Finally, we have `Atmosphere` + - Its `size` is 150, this simply sets how far away from the planet our atmosphere stretches + - Its `fogTint` is set to a color which is an object with r, g, b, and a properties (properties is another word for keys) + - `fogSize` determines how far away the fog stretches from the planet + - `fogDensity` is simply how dense the fog is + - `hasRain` makes rainfall on the planet + +### What's a Schema? + +That `$schema` property is a bit special, it instructs VSCode to use a pre-made schema to provide a better editing experience. +With the schema you get: + +- Automatic descriptions for properties when hovering over keys +- Automatic error detection for incorrect data types or values +- Autocomplete, also called IntelliSense + +## Testing The Planet + +With the new planet created (*and saved!*), launch the game through the mod manager and click resume expedition. If all went well you should be able to open your map and see wetrock orbiting Timber Hearth. + +If you run into issues please make sure: + +- You placed the JSON file in a folder called `planets` in the New Horizons mod folder +- There are no red or yellow squiggly lines in your file + +## Experiment! + +With that, try tweaking some value like groundSize and semiMajorAxis, get a feel for how editing JSON works. + +## Reloading Configs + +It can get annoying when you have to keep closing and opening the game over and over again to test changes, that's why New Horizons has a "Reload Configs" feature. +To enable it, head to your Mods menu and select New Horizons and check the box that says Debug, this will cause a "Reload Configs" option to appear in your pause menu which will reload changes from your filesystem. +You may also notice blue and yellow logs start appearing in your console, this is New Horizons providing additional info on what it's currently doing, it can be helpful when you're trying to track down an issue. + +## Modules + +Base, Atmosphere, and Orbit are all modules, which define the different aspects of your planet, modules are represented by JSON objects + +**Next Up: [Reading Schemas]({{ "Reading Schemas"|route }})** diff --git a/docs/content/pages/tutorials/planet_gen.md b/docs/content/pages/tutorials/planet_gen.md new file mode 100644 index 00000000..0dc589f0 --- /dev/null +++ b/docs/content/pages/tutorials/planet_gen.md @@ -0,0 +1,137 @@ +--- +Title: Planet Generation +Sort_Priority: 85 +--- + +# Planet Generation + +This guide covers some aspects of generating your planet, a lot of stuff is already explained in [the celestial body schema]({{ "Celestial Body Schema"|route }}). + +## Orbits + +First thing you should specify about your planet is its orbit. `primaryBody` will specify what planet this body will orbit. If you're in a new solar system and want this planet to be the center, set `centerOfSolarSystem` to `true` (keep in mind `centerOfSolarSystem` is in the `Base` module, not `Orbit`). Next up you'll need to specify the [orbital parameters](https://en.wikipedia.org/wiki/Orbital_elements). + +## Heightmaps + +Heightmaps are a way to generate unique terrain on your planet. First you specify a maximum and minimum height, and then specify a [heightMap]({{ "Celestial Body Schema"|route }}#HeightMap_heightMap) image. The more white a section of that image is, the closer to `maxHeight` that part of the terrain will be. Finally, you specify a `textureMap` which is an image that gets applied to the terrain. + +Here's an example heightmap of earth from the Real Solar System addon. + +![Earth's Heightmap]({{ "images/planet_gen/earth_heightmap.webp"|static }}) + +```json +{ + "name": "My Cool Planet", + "HeightMap": { + "minHeight": 5, + "maxHeight": 100, + "heightMap": "planets/assets/my_cool_heightmap.png", + "textureMap": "planets/assets/my_cool_texturemap.png" + } +} +``` + +There are also tools to help generate these images for you such as [Textures For Planets](https://www.texturesforplanets.com/){ target="_blank" }. + +## Variable Size Modules + +The following modules support variable sizing, meaning they can change scale over the course of the loop. + +- Water +- Lava +- Star +- Sand +- Funnel +- Ring + +To do this, simply specify a `curve` property on the module + +```json +{ + "name": "My Cool Planet", + "Water": { + "curve": [ + { + "time": 0, + "value": 100 + }, + { + "time": 22, + "value": 0 + } + ] + } +} +``` + +This makes the water on this planet shrink over the course of 22 minutes. + +## Quantum Planets + +In order to create a quantum planet, first create a normal planet. Then, create a second planet config with the same `name` as the first and `isQuantumState` set to `true`. +This makes the second planet a quantum state of the first, anything you specify here will only apply when the planet is in this state. + +```json +{ + "name": "MyPlanet", + "Orbit": { + "semiMajorAxis": 5000, + "primaryBody": "Sun" + } +} +``` + +```json +{ + "name": "MyPlanet", + "isQuantumState": true, + "Orbit": { + "semiMajorAxis": 1300, + "primaryBody": "TIMBER_HEARTH" + } +} +``` + +## Barycenters (Focal Points) + +To create a binary system of planets (like ash twin and ember twin), first create a config with `FocalPoint` set + +```json +{ + "name": "My Focal Point", + "Orbit": { + "semiMajorAxis": 22000, + "primaryBody": "Sun" + }, + "FocalPoint": { + "primary": "Planet A", + "secondary": "Planet B" + } +} +``` + +Now in each config set the `primaryBody` to the focal point + +```json +{ + "name": "Planet A", + "Orbit": { + "primaryBody": "My Focal Point", + "semiMajorAxis": 0, + "isTidallyLocked": true, + "isMoon": true + } +} +``` + +```json +{ + "name": "Planet B", + "Orbit": { + "primaryBody": "My Focal Point", + "semiMajorAxis": 440, + "isTidallyLocked": true, + "isMoon": true + } +} +``` diff --git a/docs/content/pages/tutorials/publishing.md b/docs/content/pages/tutorials/publishing.md new file mode 100644 index 00000000..661ab3b8 --- /dev/null +++ b/docs/content/pages/tutorials/publishing.md @@ -0,0 +1,56 @@ +--- +Title: Publishing Addons +Sort_Priority: 1 +--- + +# Publishing Your Addon + +This page goes over how to publish a release for your mod and submit your mod to the [outer wilds mod database](https://github.com/ow-mods/ow-mod-db) for review. + +This guide assumes you've created your addon by following [the addon creation guide]({{ "Creating An Addon"|route }}). + +## Housekeeping + +Before you release anything, you'll want to make sure: + +- Your mod has a descriptive `README.md`. (This will be shown on the website) +- Your repo has the description field (click the cog in the right column on the "Code" tab) set. (this will be shown in the manager) +- There's no `config.json` in your addon. (Not super important, but good practice) +- Your manifest has a valid name, author, and unique name. + + +## Releasing + +First things first we're going to create a release on GitHub. To do this, first make sure all your changes are committed and pushed in GitHub desktop. + +Then, edit your `manifest.json` and set the version number to `0.1.0` (or any version number that's higher than `0.0.0`). + +Finally, push your changes to GitHub, head to the "Actions" tab of your repository, and you should see an action running. + +Once the action finishes head back to the "Code" tab, you should see a Version 0.1.0 (or whatever version you put) in the column on the right. + +Double-check the release, it should have a zip file in the assets with your mod's unique name. + +## Submitting + +The hard part is over now, all that's left is to submit your mod to the database. + +[Head to the mod database and make a new "Add/Update Existing Mod" issue](https://github.com/ow-mods/ow-mod-db/issues/new?assignees=&labels=add-mod&template=add-mod.yml&title=%5BYour+mod+name+here%5D) + +Fill this out with your mod's info and make sure to put `xen.NewHorizons` in the parent mod field. + +Once you're done filling out the form, an admin will review your mod, make sure it works, and approve it into the database. + +Congrats! You just published your addon! + +## Updating + +If you want to update your mod, you can simply bump the version number in `manifest.json` again. + +To edit the release notes displayed in discord, enter them in the "Description" field before you commit in GitHub desktop. The most recent commits description is used for the release notes. + +**You don't need to create a new issue on the database to update your mod, it will be updated automatically after a few minutes** + + + + diff --git a/docs/content/pages/tutorials/reading_schemas.md b/docs/content/pages/tutorials/reading_schemas.md new file mode 100644 index 00000000..db162888 --- /dev/null +++ b/docs/content/pages/tutorials/reading_schemas.md @@ -0,0 +1,84 @@ +--- +Title: Reading Schemas +Sort_Priority: 90 +--- + +# Reading Schema Pages + +Reading and understanding the schema pages are key to knowing how to create planets. While these tutorials may be helpful, they won't cover everything, and new features may be added before the tutorial on them can be written. + +## Celestial Body Schema + +The [celestial body schema]({{ "Celestial Body Schema"|route }}) is the schema for making planets, there are other schemas which will be explained later but for now let's focus on this one. + +![The Celestial Body Schema Page]({{ "images/reading_schemas/body_schema_1.webp"|static }}) + +As you can see the type of this is `object`, which we talked about in the previous section. +We can also observe a blue badge that says "No Additional Properties", this signifies that you can't add keys to the object that aren't in the schema, for example: + +```json +{ + "name": "Wetrock", + "coolKey": "Look at my cool key!" +} +``` + +Will result in a warning in VSCode. Now, this will *not* prevent the planet from being loaded, however you should still avoid doing it. + +## Simple Properties + +![The name property on the celestial body schema]({{ "images/reading_schemas/body_schema_2.webp"|static }}) + +Next up let's look at `name`, this field is required, meaning you *have* to have it for a planet to load. +When we click on name we first see a breadcrumb, this is essentially a guide of where you are in the schema, right now we're in the name property of the root (topmost) object. +We can also see it's description, its type is `string`, and that it requires at least one character (so you can't just put `""`). + +Badges can also show stuff such as the default value, the minimum and maximum values, and more. + +## Object Properties + +![The Base object on the celestial body schema]({{ "images/reading_schemas/body_schema_3.webp"|static }}) + +Next let's look at an `object` within our root `object`, let's use `Base` as the example. + +Here we can see it's similar to our root object, in that it doesn't allow additional properties. +We can also see all of its properties listed out. + +## Array Properties + +Now let's take a look over at [removeChildren]({{ "Celestial Body Schema"|route }}#removeChildren) to see how arrays work (if you're wondering how you can get the page to scroll to a specific property, simply click on the property and copy the URL in your URL bar) + +![The curve property on a star in the celestial body schema]({{ "images/reading_schemas/body_schema_4.webp"|static }}) + +Here we can see that the type is an `array`, and each item in this array must be a `string` + +## Enum Properties + +Enum properties simply mean that they must be of one of the values shown, for example [Ring fluid type]({{ "Celestial Body Schema"|route }}#Ring_fluidType) has to be one of these values. + +![The enum values of fluidType]({{ "images/reading_schemas/body_schema_5.webp"|static }}) + +## Some Vocabulary + +- GameObject: Essentially just any object in, well, the game. You can view these object in a tree-like structure with the [Unity Explorer](https://outerwildsmods.com/mods/unityexplorer) mod. Every GameObject has a path, which is sort of like a file path in that it's a list of parent GameObjects seperated by forward slashes followed by the GameObject's name. +- Component: By themselves, a GameObject doesn't actually *do* anything, components provide stuff like collision, rendering, and logistics to GameObjects +- Config: Just another name for a JSON file "planet config" simply means a json file that describes a planet +- Module: A specific section of the config (e.g. Base, Atmosphere, etc), these usually start with capital letters + +## Note About File Paths + +Whenever a description refers to the "relative path" of a file, it means relative to the mod's directory, this means you **must** include the `planets` folder in the path: + +```json +"planets/assets/images/MyCoolImage.png" +``` + +## Other Schemas + +There are other schemas available, some are for JSON, and some are for XML. + +## Moving Forward + +Now that you know how to read the schema pages, you can understand the rest of this site. A lot of the other tutorials here will often tell you to take a look at schemas to explain what certain properties do. + +**Next Up: [Creating An Addon]({{ "Creating An Addon"|route }})** diff --git a/docs/content/pages/tutorials/ship_log.md b/docs/content/pages/tutorials/ship_log.md index 11e7a25d..62deeac7 100644 --- a/docs/content/pages/tutorials/ship_log.md +++ b/docs/content/pages/tutorials/ship_log.md @@ -1,13 +1,15 @@ --- Title: Ship Log Description: A guide to editing the ship log in New Horizons -Sort_Priority: 70 +Sort_Priority: 40 --- # Intro Welcome! this page outlines how to create a custom ship log. +If you haven't already, you may want to take a look at [Understanding XML]({{ "Understanding XML"|route }}) to get a better idea of how XML works. + # Understanding Ship Logs First thing's first, I'll define some terminology regarding ship logs in the game, and how ship logs are structured. diff --git a/docs/content/pages/tutorials/star_system.md b/docs/content/pages/tutorials/star_system.md index 3fbb5cf5..aa3218de 100644 --- a/docs/content/pages/tutorials/star_system.md +++ b/docs/content/pages/tutorials/star_system.md @@ -1,7 +1,7 @@ --- -Title: Star System +Title: Star Systems Description: A guide to editing a custom star system in New Horizons -Sort_Priority: 90 +Sort_Priority: 65 --- # Intro diff --git a/docs/content/pages/tutorials/translation.md b/docs/content/pages/tutorials/translation.md index bfec8d09..095a9f01 100644 --- a/docs/content/pages/tutorials/translation.md +++ b/docs/content/pages/tutorials/translation.md @@ -3,7 +3,7 @@ Title: Translations Sort_Priority: 60 --- -## Translations +# Translations There are 12 supported languages in Outer Wilds: english, spanish_la, german, french, italian, polish, portuguese_br, japanese, russian, chinese_simple, korean, and turkish. diff --git a/docs/content/pages/tutorials/update_existing.md b/docs/content/pages/tutorials/update_existing.md index 7e67ea0a..76d127db 100644 --- a/docs/content/pages/tutorials/update_existing.md +++ b/docs/content/pages/tutorials/update_existing.md @@ -1,13 +1,13 @@ --- Title: Update Planets -Sort_Priority: 80 +Sort_Priority: 85 --- -## Update Existing Planets +# Update Existing Planets Similar to above, make a config where "Name" is the name of the planet. The name should be able to just match their in-game english names, however if you encounter any issues with that here are the in-code names for planets that are guaranteed to work: `SUN`, `CAVE_TWIN` (Ember Twin), `TOWER_TWIN` (Ash Twin), `TIMBER_HEARTH`, `BRITTLE_HOLLOW`, `GIANTS_DEEP`, `DARK_BRAMBLE`, `COMET` (Interloper), `WHITE_HOLE`, `WHITE_HOLE_TARGET` (Whitehole station I believe), `QUANTUM_MOON`, `ORBITAL_PROBE_CANNON`, `TIMBER_MOON` (Attlerock), `VOLCANIC_MOON` (Hollow's Lantern), `DREAMWORLD`, `MapSatellite`, `RINGWORLD` (the Stranger). -Only some of the above modules are supported (currently) for existing planets. Things you cannot modify for existing planets include: heightmaps, procedural generation, gravity, or their orbits. You also can't make them into stars or binary focal points (but why would you want to, just delete them and replace them entirely). However this still means there are many things you can do: completely change their atmospheres, give them rings, asteroid belts, comet tails, lava, water, prop details, or signals. +Only some of the above modules are supported (currently) for existing planets. Things you cannot modify for existing planets include: heightmaps, procedural generation, gravity, or their orbits. You also can't make them into stars or binary focal points (but why would you want to, just delete them and replace them entirely). However, this still means there are many things you can do: completely change their atmospheres, give them rings, asteroid belts, comet tails, lava, water, prop details, or signals. You can also delete parts of an existing planet. Here's part of an example config which would delete the rising sand from Ember Twin: ```json @@ -31,4 +31,4 @@ You do this (but with the appropriate name) as its own config. } ``` -Remember that if you destroy Timber Hearth you better put a `Spawn` module on another planet. If you want to entirely replace the solar system you can destroy everything, including the sun. Also, deleting a planet destroys anything orbiting it, so if you want to replace the solar system you can just destroy the sun. If you're making a brand new star system, you don't have to worry about deleting any existing planets; they won't be there. \ No newline at end of file +Remember that if you destroy Timber Hearth you better put a `Spawn` module on another planet. If you want to entirely replace the solar system you can destroy everything, including the sun. Also, deleting a planet destroys anything orbiting it, so if you want to replace the solar system you can just destroy the sun. If you're making a brand new star system, you don't have to worry about deleting any existing planets; they won't be there. diff --git a/docs/content/pages/tutorials/xml.md b/docs/content/pages/tutorials/xml.md new file mode 100644 index 00000000..5c4db19e --- /dev/null +++ b/docs/content/pages/tutorials/xml.md @@ -0,0 +1,63 @@ +--- +Title: Understanding XML +Sort_Priority: 50 +--- + +# Understanding XML + +XML is the other language New Horizons uses for content. +XML files are usually passed straight to the game's code instead of going through New Horizons. + +## Syntax + +XML is composed of tags, a tag can represent a section or attribute + +```xml + + Jim + 32 + + +``` + +Notice how each tag is closed by an identical tag with a slash at the front (i.e. `` is closed by ``). + +If the tag has no content you can use the self-closing tag shorthand (i.e. `` doesn't need a closing tag because of the `/` at the end). + +This XML could be written in JSON as: + +```json +{ + "name": "Jim", + "age": 32, + "isMarried": true +} +``` + +XML is a lot more descriptive, you can actually tell that the object is supposed to be a person by the name of the tag. + +## Structure + +All XML files must have **one** top-level tag, this varies depending on what you're using it for (like how ship logs use a `` tag). + +## Schemas + +XML files can also have schemas, you specify them by adding attributes to the top-level tag: + +```xml + + +``` + +In order to get schema validation and autofill you'll need the [Redhat XML VSCode extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-xml){ target="_blank" }. + +## Uses + +XML is used for the following: + +- [Ship log Entries]({{ "Ship Log"|route }}) +- [Dialogue]({{ "Dialogue"|route }}) +- [Translatable Text](#) + + + diff --git a/docs/content/static/images/editor/log_port.webp b/docs/content/static/images/editor/log_port.webp new file mode 100644 index 00000000..0382c847 Binary files /dev/null and b/docs/content/static/images/editor/log_port.webp differ diff --git a/docs/content/static/images/getting_started/mod_manager_show_in_explorer.webp b/docs/content/static/images/getting_started/mod_manager_show_in_explorer.webp new file mode 100644 index 00000000..a54601f7 Binary files /dev/null and b/docs/content/static/images/getting_started/mod_manager_show_in_explorer.webp differ diff --git a/docs/content/static/images/home/create_planets.webp b/docs/content/static/images/home/create_planets.webp deleted file mode 100644 index 7629e85d..00000000 Binary files a/docs/content/static/images/home/create_planets.webp and /dev/null differ diff --git a/docs/content/static/images/home/mod_manager_dots.webp b/docs/content/static/images/home/mod_manager_dots.webp deleted file mode 100644 index 8320b2ea..00000000 Binary files a/docs/content/static/images/home/mod_manager_dots.webp and /dev/null differ diff --git a/docs/content/static/images/planet_gen/earth_heightmap.webp b/docs/content/static/images/planet_gen/earth_heightmap.webp new file mode 100644 index 00000000..3f822c92 Binary files /dev/null and b/docs/content/static/images/planet_gen/earth_heightmap.webp differ diff --git a/docs/content/static/images/reading_schemas/body_schema_1.webp b/docs/content/static/images/reading_schemas/body_schema_1.webp new file mode 100644 index 00000000..066440da Binary files /dev/null and b/docs/content/static/images/reading_schemas/body_schema_1.webp differ diff --git a/docs/content/static/images/reading_schemas/body_schema_2.webp b/docs/content/static/images/reading_schemas/body_schema_2.webp new file mode 100644 index 00000000..eb101a75 Binary files /dev/null and b/docs/content/static/images/reading_schemas/body_schema_2.webp differ diff --git a/docs/content/static/images/reading_schemas/body_schema_3.webp b/docs/content/static/images/reading_schemas/body_schema_3.webp new file mode 100644 index 00000000..3ca75b14 Binary files /dev/null and b/docs/content/static/images/reading_schemas/body_schema_3.webp differ diff --git a/docs/content/static/images/reading_schemas/body_schema_4.webp b/docs/content/static/images/reading_schemas/body_schema_4.webp new file mode 100644 index 00000000..5e218ec0 Binary files /dev/null and b/docs/content/static/images/reading_schemas/body_schema_4.webp differ diff --git a/docs/content/static/images/reading_schemas/body_schema_5.webp b/docs/content/static/images/reading_schemas/body_schema_5.webp new file mode 100644 index 00000000..9c88e15c Binary files /dev/null and b/docs/content/static/images/reading_schemas/body_schema_5.webp differ