diff --git a/NewHorizons/Assets/textures/CloudEntry_GD_Island_d.png b/NewHorizons/Assets/textures/CloudEntry_GD_Island_d.png
new file mode 100644
index 00000000..5ba6654b
Binary files /dev/null and b/NewHorizons/Assets/textures/CloudEntry_GD_Island_d.png differ
diff --git a/NewHorizons/Assets/textures/CloudExit__PlayerShip_d.png b/NewHorizons/Assets/textures/CloudExit__PlayerShip_d.png
new file mode 100644
index 00000000..5e83973d
Binary files /dev/null and b/NewHorizons/Assets/textures/CloudExit__PlayerShip_d.png differ
diff --git a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png b/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png
index ece877d5..fc670c87 100644
Binary files a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png and b/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d.png differ
diff --git a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d_greyscale.png b/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d_greyscale.png
deleted file mode 100644
index e05049b1..00000000
Binary files a/NewHorizons/Assets/textures/OceanEntry_PlayerShip_d_greyscale.png and /dev/null differ
diff --git a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png b/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png
index d8e33ac1..7acfe355 100644
Binary files a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png and b/NewHorizons/Assets/textures/OceanExit_PlayerShip_d.png differ
diff --git a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d_greyscale.png b/NewHorizons/Assets/textures/OceanExit_PlayerShip_d_greyscale.png
deleted file mode 100644
index bba05cc4..00000000
Binary files a/NewHorizons/Assets/textures/OceanExit_PlayerShip_d_greyscale.png and /dev/null differ
diff --git a/NewHorizons/Components/SplashColourizer.cs b/NewHorizons/Components/SplashColourizer.cs
new file mode 100644
index 00000000..140704b4
--- /dev/null
+++ b/NewHorizons/Components/SplashColourizer.cs
@@ -0,0 +1,265 @@
+using NewHorizons.External.Configs;
+using NewHorizons.External.SerializableData;
+using NewHorizons.Utility;
+using NewHorizons.Utility.Files;
+using NewHorizons.Utility.OuterWilds;
+using NewHorizons.Utility.OWML;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace NewHorizons.Components;
+
+///
+/// When a Fluid Detector enters this volume, it's splash effects will get colourized to match whats on this planet
+///
+public class SplashColourizer : MonoBehaviour
+{
+ public float _radius;
+
+ private SphereShape _sphereShape;
+
+ private Dictionary _cachedOriginalPrefabs = new();
+ private Dictionary _cachedModifiedPrefabs = new();
+
+ private FluidDetector _playerDetector, _shipDetector, _probeDetector;
+
+ private MColor _waterColour, _cloudColour, _plasmaColour, _sandColour;
+
+ private GameObject _prefabHolder;
+
+ private bool _probeInsideVolume;
+
+ private List _customTextures = new();
+
+ public void Awake()
+ {
+ var volume = new GameObject("Volume");
+ volume.transform.parent = transform;
+ volume.transform.localPosition = Vector3.zero;
+ volume.layer = Layer.BasicEffectVolume;
+
+ _sphereShape = gameObject.AddComponent();
+ _sphereShape.radius = _radius;
+
+ volume.AddComponent();
+
+ _prefabHolder = new GameObject("Prefabs");
+ _prefabHolder.SetActive(false);
+ }
+
+ public static void Make(GameObject planet, PlanetConfig config, float soi)
+ {
+ var water = config.Water?.tint;
+ var cloud = config.Atmosphere?.clouds?.tint;
+ var plasma = config.Lava?.tint ?? config.Star?.tint;
+ var sand = config.Sand?.tint;
+
+ if (water != null || cloud != null || plasma != null || sand != null)
+ {
+ var size = Mathf.Max(
+ soi / 1.5f,
+ config.Water?.size ?? 0f,
+ config.Atmosphere?.clouds?.outerCloudRadius ?? 0f,
+ config.Lava?.size ?? 0f,
+ config.Star?.size ?? 0f,
+ config.Sand?.size ?? 0f
+ ) * 1.5f;
+
+ var colourizer = planet.AddComponent();
+
+ colourizer._radius = size;
+ if (colourizer._sphereShape != null) colourizer._sphereShape.radius = size;
+
+ colourizer._waterColour = water;
+ colourizer._cloudColour = cloud;
+ colourizer._plasmaColour = plasma;
+ colourizer._sandColour = sand;
+ }
+ }
+
+ public void Start()
+ {
+ // Cache all prefabs
+ CachePrefabs(_playerDetector = Locator.GetPlayerDetector().GetComponent());
+ CachePrefabs(_shipDetector = Locator.GetShipDetector().GetComponent());
+ CachePrefabs(_probeDetector = Locator.GetProbe().GetDetectorObject().GetComponent());
+
+ GlobalMessenger.AddListener("RetrieveProbe", OnRetrieveProbe);
+
+ // Check if player/ship are already inside
+ if ((_playerDetector.transform.position - transform.position).magnitude < _radius)
+ {
+ SetSplashEffects(_playerDetector, true);
+ }
+ if ((_shipDetector.transform.position - transform.position).magnitude < _radius)
+ {
+ SetSplashEffects(_shipDetector, true);
+ }
+ }
+
+ public void OnDestroy()
+ {
+ GlobalMessenger.RemoveListener("RetrieveProbe", OnRetrieveProbe);
+ }
+
+ private void OnRetrieveProbe(SurveyorProbe probe)
+ {
+ if (_probeInsideVolume)
+ {
+ // Else it never leaves the volume
+ SetProbeSplashEffects(false);
+ }
+ }
+
+ public void OnTriggerEnter(Collider hitCollider) => OnEnterExit(hitCollider, true);
+
+ public void OnTriggerExit(Collider hitCollider) => OnEnterExit(hitCollider, false);
+
+ ///
+ /// The probe keeps being null idgi
+ ///
+ ///
+ private bool IsProbeLaunched()
+ {
+ return Locator.GetProbe()?.IsLaunched() ?? false;
+ }
+
+ private void OnEnterExit(Collider hitCollider, bool entering)
+ {
+ if (!enabled) return;
+
+ if (hitCollider.attachedRigidbody != null)
+ {
+ var isPlayer = hitCollider.attachedRigidbody.CompareTag("Player");
+ var isShip = hitCollider.attachedRigidbody.CompareTag("Ship");
+ var isProbe = hitCollider.attachedRigidbody.CompareTag("Probe");
+
+ if (isPlayer)
+ {
+ SetSplashEffects(_playerDetector, entering);
+ if (IsProbeLaunched())
+ {
+ SetProbeSplashEffects(entering);
+ }
+ }
+ else if (isShip)
+ {
+ SetSplashEffects(_shipDetector, entering);
+ if (PlayerState.IsInsideShip())
+ {
+ SetSplashEffects(_playerDetector, entering);
+ }
+ if (IsProbeLaunched())
+ {
+ SetProbeSplashEffects(entering);
+ }
+ }
+ else if (isProbe)
+ {
+ SetProbeSplashEffects(entering);
+ }
+
+ // If the probe isn't launched we always consider it as being inside the volume
+ if (isProbe || !IsProbeLaunched())
+ {
+ _probeInsideVolume = entering;
+ }
+ }
+ }
+
+ public void CachePrefabs(FluidDetector detector)
+ {
+ foreach (var splashEffect in detector._splashEffects)
+ {
+ if (!_cachedOriginalPrefabs.ContainsKey(splashEffect))
+ {
+ _cachedOriginalPrefabs[splashEffect] = splashEffect.splashPrefab;
+ }
+ if (!_cachedModifiedPrefabs.ContainsKey(splashEffect))
+ {
+ Color? colour = null;
+ if (splashEffect.fluidType == FluidVolume.Type.CLOUD)
+ {
+ colour = _cloudColour?.ToColor();
+ }
+ switch(splashEffect.fluidType)
+ {
+ case FluidVolume.Type.CLOUD:
+ colour = _cloudColour?.ToColor();
+ break;
+ case FluidVolume.Type.WATER:
+ colour = _waterColour?.ToColor();
+ break;
+ case FluidVolume.Type.PLASMA:
+ colour = _plasmaColour?.ToColor();
+ break;
+ case FluidVolume.Type.SAND:
+ colour = _sandColour?.ToColor();
+ break;
+ }
+
+ if (colour is Color tint)
+ {
+ var flagError = false;
+ var prefab = splashEffect.splashPrefab.InstantiateInactive();
+ var meshRenderers = prefab.GetComponentsInChildren(true);
+ foreach (var meshRenderer in meshRenderers)
+ {
+ if (_customTextures.Contains(meshRenderer.material.mainTexture))
+ {
+ // Might be some shared material stuff? This image is already tinted, so skip it
+ continue;
+ }
+
+ // Can't access the textures in memory so we need to have our own copies
+ var texture = ImageUtilities.GetTexture(Main.Instance, $"Assets/textures/{meshRenderer.material.mainTexture.name}.png");
+ if (texture == null)
+ {
+ NHLogger.LogError($"Go tell an NH dev to add this image texture to the mod because I can't be bothered to until somebody complains: {meshRenderer.material.mainTexture.name}");
+ GameObject.Destroy(prefab);
+ flagError = true;
+ }
+
+ _customTextures.Add(texture);
+
+ meshRenderer.material = new(meshRenderer.material)
+ {
+ color = Color.white,
+ mainTexture = ImageUtilities.TintImage(texture, tint)
+ };
+ }
+
+ if (flagError) continue;
+
+ // Have to be active when being used by the base game classes but can't be seen
+ // Keep them active as children of an inactive game object
+ prefab.transform.parent = _prefabHolder.transform;
+ prefab.SetActive(true);
+
+ _cachedModifiedPrefabs[splashEffect] = prefab;
+ }
+ }
+ }
+ }
+
+ public void SetSplashEffects(FluidDetector detector, bool entering)
+ {
+ NHLogger.LogVerbose($"Body {detector.name} {(entering ? "entered" : "left")} colourizing volume on {name}");
+
+ foreach (var splashEffect in detector._splashEffects)
+ {
+ var prefabs = entering ? _cachedModifiedPrefabs : _cachedOriginalPrefabs;
+ if (prefabs.TryGetValue(splashEffect, out var prefab))
+ {
+ splashEffect.splashPrefab = prefab;
+ }
+ }
+ }
+
+ public void SetProbeSplashEffects(bool entering)
+ {
+ _probeInsideVolume = entering;
+
+ SetSplashEffects(_probeDetector, entering);
+ }
+}
diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs
index aaeec71e..8fe48397 100644
--- a/NewHorizons/Handlers/PlanetCreationHandler.cs
+++ b/NewHorizons/Handlers/PlanetCreationHandler.cs
@@ -20,6 +20,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
+using NewHorizons.Streaming;
+using Newtonsoft.Json;
+using NewHorizons.External.Modules.VariableSize;
+using NewHorizons.Components;
namespace NewHorizons.Handlers
{
@@ -764,6 +768,8 @@ namespace NewHorizons.Handlers
SpawnPointBuilder.Make(go, body.Config.Spawn, rb);
}
+ SplashColourizer.Make(go, body.Config, sphereOfInfluence);
+
// We allow removing children afterwards so you can also take bits off of the modules you used
if (body.Config.removeChildren != null) RemoveChildren(go, body);
diff --git a/NewHorizons/Utility/Files/ImageUtilities.cs b/NewHorizons/Utility/Files/ImageUtilities.cs
index 09ff831f..e124fe89 100644
--- a/NewHorizons/Utility/Files/ImageUtilities.cs
+++ b/NewHorizons/Utility/Files/ImageUtilities.cs
@@ -43,7 +43,17 @@ namespace NewHorizons.Utility.Files
return null;
}
// Copied from OWML but without the print statement lol
- var path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename);
+ string path;
+ try
+ {
+ path = Path.Combine(mod.ModHelper.Manifest.ModFolderPath, filename);
+ }
+ catch (Exception e)
+ {
+ NHLogger.LogError($"Invalid path: Couldn't combine {mod.ModHelper.Manifest.ModFolderPath} {filename} - {e}");
+ return null;
+ }
+
var key = GetKey(path);
if (_textureCache.TryGetValue(key, out var existingTexture))
{