Merge branch 'dev' into title-screen-config

This commit is contained in:
xen-42 2025-02-16 17:32:56 -05:00
commit 731bd64bdf
57 changed files with 1463 additions and 304 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

After

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 564 KiB

View File

@ -87,6 +87,9 @@ namespace NewHorizons.Builder.Atmosphere
MR.material.SetFloat(Radius, atmo.fogSize);
MR.material.SetFloat(Density, atmo.fogDensity);
MR.material.SetFloat(DensityExponent, 1);
// We apply fogTint to the material and tint the fog ramp, which means the ramp and tint get multiplied together in the shader, so tint is applied twice
// However nobody has visually complained about this, so we don't want to change it until maybe somebody does
// Was previously documented by issue #747.
MR.material.SetTexture(ColorRampTexture, colorRampTexture);
fogGO.transform.position = planetGO.transform.position;

View File

@ -39,7 +39,8 @@ namespace NewHorizons.Builder.Body
{
surfaceGravity = belt.gravity,
surfaceSize = size,
gravityFallOff = GravityFallOff.InverseSquared
gravityFallOff = GravityFallOff.InverseSquared,
hasFluidDetector = false
};
config.Orbit = new OrbitModule()
@ -95,6 +96,7 @@ namespace NewHorizons.Builder.Body
color = new MColor(126, 94, 73)
};
}
config.AmbientLights = new[] { new AmbientLightModule() { outerRadius = size * 1.2f, intensity = 0.1f } };
var asteroid = new NewHorizonsBody(config, mod);
PlanetCreationHandler.GenerateBody(asteroid);

View File

@ -100,6 +100,11 @@ namespace NewHorizons.Builder.Body.Geometry
mesh.normals = normals;
mesh.uv = uvs;
mesh.RecalculateBounds();
// Unity recalculate normals does not support smooth normals
//mesh.RecalculateNormals();
mesh.RecalculateTangents();
return mesh;
}

View File

@ -11,7 +11,7 @@ namespace NewHorizons.Builder.Body
{
public static class HeightMapBuilder
{
public static Shader PlanetShader;
private static Shader _planetShader;
// I hate nested functions okay
private static IModBehaviour _currentMod;
@ -62,7 +62,7 @@ namespace NewHorizons.Builder.Body
cubeSphere.SetActive(false);
cubeSphere.transform.SetParent(sector?.transform ?? planetGO.transform, false);
if (PlanetShader == null) PlanetShader = AssetBundleUtilities.NHAssetBundle.LoadAsset<Shader>("Assets/Shaders/SphereTextureWrapperTriplanar.shader");
if (_planetShader == null) _planetShader = AssetBundleUtilities.NHAssetBundle.LoadAsset<Shader>("Assets/Shaders/SphereTextureWrapperTriplanar.shader");
var stretch = module.stretch != null ? (Vector3)module.stretch : Vector3.one;
@ -115,7 +115,7 @@ namespace NewHorizons.Builder.Body
LODCubeSphere.AddComponent<MeshFilter>().mesh = CubeSphere.Build(resolution, heightMap, module.minHeight, module.maxHeight, stretch);
var cubeSphereMR = LODCubeSphere.AddComponent<MeshRenderer>();
var material = new Material(PlanetShader);
var material = new Material(_planetShader);
cubeSphereMR.material = material;
material.name = textureMap.name;

View File

@ -1,21 +1,50 @@
using NewHorizons.Builder.Body.Geometry;
using NewHorizons.External.Modules;
using NewHorizons.Utility;
using NewHorizons.Utility.Files;
using OWML.Common;
using System.Collections.Generic;
using UnityEngine;
namespace NewHorizons.Builder.Body
{
public static class ProcGenBuilder
{
private static Material quantumMaterial;
private static Material iceMaterial;
private static Material _material;
private static Shader _planetShader;
public static GameObject Make(GameObject planetGO, Sector sector, ProcGenModule module)
private static Dictionary<ProcGenModule, Material> _materialCache = new();
public static void ClearCache()
{
if (quantumMaterial == null) quantumMaterial = SearchUtilities.FindResourceOfTypeAndName<Material>("Rock_QM_EyeRock_mat");
if (iceMaterial == null) iceMaterial = SearchUtilities.FindResourceOfTypeAndName<Material>("Rock_BH_IceSpike_mat");
foreach (var material in _materialCache.Values)
{
Object.Destroy(material);
}
_materialCache.Clear();
}
private static Material MakeMaterial()
{
var material = new Material(_planetShader);
GameObject icosphere = new GameObject("Icosphere");
var keyword = "BASE_TILE";
var prefix = "_BaseTile";
material.SetFloat(prefix, 1);
material.EnableKeyword(keyword);
material.SetTexture("_BlendMap", ImageUtilities.MakeSolidColorTexture(1, 1, Color.white));
return material;
}
public static GameObject Make(IModBehaviour mod, GameObject planetGO, Sector sector, ProcGenModule module)
{
if (_planetShader == null) _planetShader = AssetBundleUtilities.NHAssetBundle.LoadAsset<Shader>("Assets/Shaders/SphereTextureWrapperTriplanar.shader");
if (_material == null) _material = MakeMaterial();
var icosphere = new GameObject("Icosphere");
icosphere.SetActive(false);
icosphere.transform.parent = sector?.transform ?? planetGO.transform;
icosphere.transform.rotation = Quaternion.Euler(90, 0, 0);
@ -26,8 +55,61 @@ namespace NewHorizons.Builder.Body
icosphere.AddComponent<MeshFilter>().mesh = mesh;
var cubeSphereMR = icosphere.AddComponent<MeshRenderer>();
cubeSphereMR.material = new Material(Shader.Find("Standard"));
cubeSphereMR.material.color = module.color != null ? module.color.ToColor() : Color.white;
if (!_materialCache.TryGetValue(module, out var material))
{
material = new Material(_material);
material.name = planetGO.name;
if (module.material == ProcGenModule.Material.Default)
{
if (!string.IsNullOrEmpty(module.texture))
{
material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(mod, module.texture, wrap: true));
}
else
{
material.mainTexture = ImageUtilities.MakeSolidColorTexture(1, 1, module.color?.ToColor() ?? Color.white);
}
if (!string.IsNullOrEmpty(module.smoothnessMap))
{
material.SetTexture($"_BaseTileSmoothnessMap", ImageUtilities.GetTexture(mod, module.smoothnessMap, wrap: true));
}
if (!string.IsNullOrEmpty(module.normalMap))
{
material.SetFloat($"_BaseTileBumpStrength", module.normalStrength);
material.SetTexture($"_BaseTileBumpMap", ImageUtilities.GetTexture(mod, module.normalMap, wrap: true));
}
}
else
{
switch (module.material)
{
case ProcGenModule.Material.Ice:
material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(Main.Instance, "Assets/textures/Ice.png", wrap: true));
break;
case ProcGenModule.Material.Quantum:
material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(Main.Instance, "Assets/textures/Quantum.png", wrap: true));
break;
case ProcGenModule.Material.Rock:
material.SetTexture($"_BaseTileAlbedo", ImageUtilities.GetTexture(Main.Instance, "Assets/textures/Rocks.png", wrap: true));
break;
default:
break;
}
material.SetFloat($"_BaseTileScale", 5 / module.scale);
if (module.color != null)
{
material.color = module.color.ToColor();
}
}
material.SetFloat("_Smoothness", module.smoothness);
material.SetFloat("_Metallic", module.metallic);
_materialCache[module] = material;
}
cubeSphereMR.sharedMaterial = material;
var cubeSphereMC = icosphere.AddComponent<MeshCollider>();
cubeSphereMC.sharedMesh = mesh;

View File

@ -162,7 +162,7 @@ namespace NewHorizons.Builder.Body
GameObject procGen = null;
if (body.Config.ProcGen != null)
{
procGen = ProcGenBuilder.Make(proxy, null, body.Config.ProcGen);
procGen = ProcGenBuilder.Make(body.Mod, proxy, null, body.Config.ProcGen);
if (realSize < body.Config.ProcGen.scale) realSize = body.Config.ProcGen.scale;
}

View File

@ -56,6 +56,7 @@ namespace NewHorizons.Builder.Props
private static void SceneManager_sceneUnloaded(Scene scene)
{
// would be nice to only clear when system changes, but fixed prefabs rely on stuff in the scene
foreach (var prefab in _fixedPrefabCache.Values)
{
UnityEngine.Object.Destroy(prefab.prefab);
@ -155,6 +156,17 @@ namespace NewHorizons.Builder.Props
continue;
}
/* We used to set SectorCullGroup._controllingProxy to null. Now we do not.
* This may break things on copied details because it prevents SetSector from doing anything,
* so that part of the detail might be culled by wrong sector.
* So if you copy something from e.g. Giants Deep it might turn off the detail if you arent in
* the sector of the thing you copied from (since it's still pointing to the original proxy,
* which has the original sector at giants deep there)
*
* Anyway nobody has complained about this for the year it's been like that so closing issue #831 until
* this affects somebody
*/
FixSectoredComponent(component, sector, existingSectors);
}
@ -219,15 +231,10 @@ namespace NewHorizons.Builder.Props
if (detail.removeChildren != null)
{
var detailPath = prop.transform.GetPath();
var transforms = prop.GetComponentsInChildren<Transform>(true);
foreach (var childPath in detail.removeChildren)
{
// Multiple children can have the same path so we delete all that match
var path = $"{detailPath}/{childPath}";
var flag = true;
foreach (var childObj in transforms.Where(x => x.GetPath() == path))
foreach (var childObj in prop.transform.FindAll(childPath))
{
flag = false;
childObj.gameObject.SetActive(false);
@ -263,7 +270,7 @@ namespace NewHorizons.Builder.Props
UnityEngine.Object.DestroyImmediate(prop);
prop = newDetailGO;
}
if (isItem)
{
// Else when you put them down you can't pick them back up
@ -275,7 +282,7 @@ namespace NewHorizons.Builder.Props
// For DLC related props
// Make sure to do this before its set active
if (!string.IsNullOrEmpty(detail?.path) &&
if (!string.IsNullOrEmpty(detail?.path) &&
(detail.path.ToLowerInvariant().StartsWith("ringworld") || detail.path.ToLowerInvariant().StartsWith("dreamworld")))
{
prop.AddComponent<DestroyOnDLC>()._destroyOnDLCNotOwned = true;
@ -294,7 +301,7 @@ namespace NewHorizons.Builder.Props
if (!string.IsNullOrEmpty(detail.activationCondition))
{
ConditionalObjectActivation.SetUp(prop, detail.activationCondition, detail.blinkWhenActiveChanged, true);
ConditionalObjectActivation.SetUp(prop, detail.activationCondition, detail.blinkWhenActiveChanged, true);
}
if (!string.IsNullOrEmpty(detail.deactivationCondition))
{
@ -568,22 +575,22 @@ namespace NewHorizons.Builder.Props
// Manually copied these values from a artifact lantern so that we don't have to find it (works in Eye)
lantern._origLensFlareBrightness = 0f;
lantern._focuserPetalsBaseEulerAngles = new Vector3[]
{
new Vector3(0.7f, 270.0f, 357.5f),
new Vector3(288.7f, 270.1f, 357.4f),
lantern._focuserPetalsBaseEulerAngles = new Vector3[]
{
new Vector3(0.7f, 270.0f, 357.5f),
new Vector3(288.7f, 270.1f, 357.4f),
new Vector3(323.3f, 90.0f, 177.5f),
new Vector3(35.3f, 90.0f, 177.5f),
new Vector3(72.7f, 270.1f, 357.5f)
new Vector3(35.3f, 90.0f, 177.5f),
new Vector3(72.7f, 270.1f, 357.5f)
};
lantern._dirtyFlag_focus = true;
lantern._concealerRootsBaseScale = new Vector3[]
lantern._concealerRootsBaseScale = new Vector3[]
{
Vector3.one,
Vector3.one,
Vector3.one
};
lantern._concealerCoversStartPos = new Vector3[]
lantern._concealerCoversStartPos = new Vector3[]
{
new Vector3(0.0f, 0.0f, 0.0f),
new Vector3(0.0f, -0.1f, 0.0f),
@ -594,7 +601,7 @@ namespace NewHorizons.Builder.Props
};
lantern._dirtyFlag_concealment = true;
lantern.UpdateVisuals();
Destroy(this);
}
}

View File

@ -25,7 +25,7 @@ namespace NewHorizons.Builder.Props
travelerData.requirementsMet = false;
}
if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && !DialogueConditionManager.SharedInstance.GetConditionState(info.requiredPersistentCondition))
if (!string.IsNullOrEmpty(info.requiredPersistentCondition) && !PlayerData.GetPersistentCondition(info.requiredPersistentCondition))
{
travelerData.requirementsMet = false;
}

View File

@ -1,14 +1,12 @@
using NewHorizons.Components.ShipLog;
using NewHorizons.External;
using NewHorizons.External.Modules;
using NewHorizons.External.Modules.VariableSize;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using NewHorizons.Utility.Files;
using NewHorizons.Utility.OuterWilds;
using NewHorizons.Utility.OWML;
using OWML.ModHelper;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
@ -28,7 +26,7 @@ namespace NewHorizons.Builder.ShipLog
if (_astroObjectToMapModeInfo.TryGetValue(slao, out var mapModeInfo))
{
return mapModeInfo;
}
}
else
{
return null;
@ -149,7 +147,7 @@ namespace NewHorizons.Builder.ShipLog
Rect rect = new Rect(0, 0, texture.width, texture.height);
Vector2 pivot = new Vector2(texture.width / 2, texture.height / 2);
newImage.sprite = Sprite.Create(texture, rect, pivot);
newImage.sprite = Sprite.Create(texture, rect, pivot, 100, 0, SpriteMeshType.FullRect, Vector4.zero, false);
return newImageGO;
}
@ -190,15 +188,15 @@ namespace NewHorizons.Builder.ShipLog
Texture2D image = null;
Texture2D outline = null;
string imagePath = body.Config.ShipLog?.mapMode?.revealedSprite;
string outlinePath = body.Config.ShipLog?.mapMode?.outlineSprite;
if (imagePath != null) image = ImageUtilities.GetTexture(body.Mod, imagePath);
if (image == null) image = AutoGenerateMapModePicture(body);
if (image == null) image = ImageUtilities.AutoGenerateMapModePicture(body);
if (outlinePath != null) outline = ImageUtilities.GetTexture(body.Mod, outlinePath);
if (outline == null) outline = ImageUtilities.MakeOutline(image, Color.white, 10);
if (outline == null) outline = ImageUtilities.GetCachedOutlineOrCreate(body, image, imagePath);
astroObject._imageObj = CreateImage(gameObject, image, body.Config.name + " Revealed", layer);
astroObject._outlineObj = CreateImage(gameObject, outline, body.Config.name + " Outline", layer);
@ -246,10 +244,10 @@ namespace NewHorizons.Builder.ShipLog
string outlinePath = info.outlineSprite;
if (imagePath != null) image = ImageUtilities.GetTexture(body.Mod, imagePath);
else image = AutoGenerateMapModePicture(body);
else image = ImageUtilities.AutoGenerateMapModePicture(body);
if (outlinePath != null) outline = ImageUtilities.GetTexture(body.Mod, outlinePath);
else outline = ImageUtilities.MakeOutline(image, Color.white, 10);
else outline = ImageUtilities.GetCachedOutlineOrCreate(body, image, imagePath);
Image revealedImage = CreateImage(detailGameObject, image, "Detail Revealed", parent.gameObject.layer).GetComponent<Image>();
Image outlineImage = CreateImage(detailGameObject, outline, "Detail Outline", parent.gameObject.layer).GetComponent<Image>();
@ -591,7 +589,7 @@ namespace NewHorizons.Builder.ShipLog
GameObject newNodeGO = CreateMapModeGameObject(node.mainBody, parent, layer, position);
ShipLogAstroObject astroObject = AddShipLogAstroObject(newNodeGO, node.mainBody, greyScaleMaterial, layer);
if (node.mainBody.Config.FocalPoint != null)
{
{
astroObject._imageObj.GetComponent<Image>().enabled = false;
astroObject._outlineObj.GetComponent<Image>().enabled = false;
astroObject._unviewedObj.GetComponent<Image>().enabled = false;
@ -605,68 +603,6 @@ namespace NewHorizons.Builder.ShipLog
}
#endregion
private static Texture2D AutoGenerateMapModePicture(NewHorizonsBody body)
{
Texture2D texture;
if (body.Config.Star != null) texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModeStar.png");
else if (body.Config.Atmosphere != null) texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModNoAtmo.png");
else texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModePlanet.png");
var color = GetDominantPlanetColor(body);
var darkColor = new Color(color.r / 3f, color.g / 3f, color.b / 3f);
texture = ImageUtilities.LerpGreyscaleImage(texture, color, darkColor);
return texture;
}
private static Color GetDominantPlanetColor(NewHorizonsBody body)
{
try
{
var starColor = body.Config?.Star?.tint;
if (starColor != null) return starColor.ToColor();
var atmoColor = body.Config.Atmosphere?.atmosphereTint;
if (body.Config.Atmosphere?.clouds != null && atmoColor != null) return atmoColor.ToColor();
if (body.Config?.HeightMap?.textureMap != null)
{
try
{
var texture = ImageUtilities.GetTexture(body.Mod, body.Config.HeightMap.textureMap);
var landColor = ImageUtilities.GetAverageColor(texture);
if (landColor != null) return landColor;
}
catch (Exception) { }
}
var waterColor = body.Config.Water?.tint;
if (waterColor != null) return waterColor.ToColor();
var lavaColor = body.Config.Lava?.tint;
if (lavaColor != null) return lavaColor.ToColor();
var sandColor = body.Config.Sand?.tint;
if (sandColor != null) return sandColor.ToColor();
switch (body.Config?.Props?.singularities?.FirstOrDefault()?.type)
{
case SingularityModule.SingularityType.BlackHole:
return Color.black;
case SingularityModule.SingularityType.WhiteHole:
return Color.white;
}
}
catch (Exception)
{
NHLogger.LogWarning($"Something went wrong trying to pick the colour for {body.Config.name} but I'm too lazy to fix it.");
}
return Color.white;
}
#region Replacement
private static List<(NewHorizonsBody, ModBehaviour, MapModeInfo)> _mapModIconsToUpdate = new();
public static void TryReplaceExistingMapModeIcon(NewHorizonsBody body, ModBehaviour mod, MapModeInfo info)
@ -687,7 +623,7 @@ namespace NewHorizons.Builder.ShipLog
}
private static void ReplaceExistingMapModeIcon(NewHorizonsBody body, ModBehaviour mod, MapModeInfo info)
{
{
var astroObject = _astroObjectToShipLog[body.Object];
var gameObject = astroObject.gameObject;
var layer = gameObject.layer;

View File

@ -246,7 +246,7 @@ namespace NewHorizons.Builder.ShipLog
Texture2D newTexture = ImageUtilities.GetTexture(body.Mod, relativePath);
Rect rect = new Rect(0, 0, newTexture.width, newTexture.height);
Vector2 pivot = new Vector2(newTexture.width / 2, newTexture.height / 2);
return Sprite.Create(newTexture, rect, pivot);
return Sprite.Create(newTexture, rect, pivot, 100, 0, SpriteMeshType.FullRect, Vector4.zero, false);
}
catch (Exception)
{

View File

@ -11,9 +11,8 @@ namespace NewHorizons.Builder.Volumes
{
var volume = VolumeBuilder.Make<LoadCreditsVolume>(planetGO, sector, info);
volume.creditsType = info.creditsType;
volume.gameOverText = info.gameOverText;
volume.deathType = EnumUtils.Parse(info.deathType.ToString(), DeathType.Default);
volume.gameOver = info.gameOver;
volume.deathType = info.deathType == null ? null : EnumUtils.Parse(info.deathType.ToString(), DeathType.Default);
return volume;
}

View File

@ -0,0 +1,138 @@
using NewHorizons.External.Modules;
using NewHorizons.External.SerializableEnums;
using NewHorizons.Handlers;
using NewHorizons.Utility.OWML;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace NewHorizons.Components
{
public class NHGameOverManager : MonoBehaviour
{
/// <summary>
/// Mod unique id to game over module list
/// Done as a dictionary so that Reload Configs can overwrite entries per mod
/// </summary>
public static Dictionary<string, GameOverModule[]> gameOvers = new();
public static NHGameOverManager Instance { get; private set; }
private GameOverController _gameOverController;
private PlayerCameraEffectController _playerCameraEffectController;
private GameOverModule[] _gameOvers;
private bool _gameOverSequenceStarted;
public void Awake()
{
Instance = this;
}
public void Start()
{
_gameOverController = FindObjectOfType<GameOverController>();
_playerCameraEffectController = FindObjectOfType<PlayerCameraEffectController>();
_gameOvers = gameOvers.SelectMany(x => x.Value).ToArray();
}
public void TryHijackDeathSequence()
{
var gameOver = _gameOvers.FirstOrDefault(x => !string.IsNullOrEmpty(x.condition) && DialogueConditionManager.SharedInstance.GetConditionState(x.condition));
if (!_gameOverSequenceStarted && gameOver != null && !Locator.GetDeathManager()._finishedDLC)
{
StartGameOverSequence(gameOver, null);
}
}
public void StartGameOverSequence(GameOverModule gameOver, DeathType? deathType)
{
_gameOverSequenceStarted = true;
Delay.StartCoroutine(GameOver(gameOver, deathType));
}
private IEnumerator GameOver(GameOverModule gameOver, DeathType? deathType)
{
OWInput.ChangeInputMode(InputMode.None);
ReticleController.Hide();
Locator.GetPromptManager().SetPromptsVisible(false);
Locator.GetPauseCommandListener().AddPauseCommandLock();
// The PlayerCameraEffectController is what actually kills us, so convince it we're already dead
Locator.GetDeathManager()._isDead = true;
var fadeLength = 2f;
if (Locator.GetDeathManager()._isDying)
{
// Player already died at this point, so don't fade
fadeLength = 0f;
}
else if (deathType is DeathType nonNullDeathType)
{
_playerCameraEffectController.OnPlayerDeath(nonNullDeathType);
fadeLength = _playerCameraEffectController._deathFadeLength;
}
else
{
// Wake up relaxed next loop
PlayerData.SetLastDeathType(DeathType.Meditation);
FadeHandler.FadeOut(fadeLength);
}
yield return new WaitForSeconds(fadeLength);
if (!string.IsNullOrEmpty(gameOver.text) && _gameOverController != null)
{
_gameOverController._deathText.text = TranslationHandler.GetTranslation(gameOver.text, TranslationHandler.TextType.UI);
_gameOverController.SetupGameOverScreen(5f);
if (gameOver.colour != null)
{
_gameOverController._deathText.color = gameOver.colour.ToColor();
}
// Make sure the fade handler is off now
FadeHandler.FadeIn(0f);
// We set this to true to stop it from loading the credits scene, so we can do it ourselves
_gameOverController._loading = true;
yield return new WaitUntil(ReadytoLoadCreditsScene);
}
LoadCreditsScene(gameOver);
}
private bool ReadytoLoadCreditsScene() => _gameOverController._fadedOutText && _gameOverController._textAnimator.IsComplete();
private void LoadCreditsScene(GameOverModule gameOver)
{
NHLogger.LogVerbose($"Load credits {gameOver.creditsType}");
switch (gameOver.creditsType)
{
case NHCreditsType.Fast:
LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack);
break;
case NHCreditsType.Final:
LoadManager.LoadScene(OWScene.Credits_Final, LoadManager.FadeType.ToBlack);
break;
case NHCreditsType.Kazoo:
TimelineObliterationController.s_hasRealityEnded = true;
LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack);
break;
default:
// GameOverController disables post processing
_gameOverController._flashbackCamera.postProcessing.enabled = true;
// For some reason this isn't getting set sometimes
AudioListener.volume = 1;
GlobalMessenger.FireEvent("TriggerFlashback");
break;
}
}
}
}

View File

@ -13,7 +13,7 @@ namespace NewHorizons.Components.Props
public bool CloseEyes;
public bool SetActiveWithCondition;
private PlayerCameraEffectController _playerCameraEffectController;
private static PlayerCameraEffectController _playerCameraEffectController;
private bool _changeConditionOnExitConversation;
private bool _inConversation;
@ -45,7 +45,7 @@ namespace NewHorizons.Components.Props
public void Awake()
{
_playerCameraEffectController = GameObject.FindObjectOfType<PlayerCameraEffectController>();
if (_playerCameraEffectController == null) _playerCameraEffectController = GameObject.FindObjectOfType<PlayerCameraEffectController>();
GlobalMessenger<string, bool>.AddListener("DialogueConditionChanged", OnDialogueConditionChanged);
GlobalMessenger.AddListener("ExitConversation", OnExitConversation);
GlobalMessenger.AddListener("EnterConversation", OnEnterConversation);

View File

@ -63,7 +63,7 @@ namespace NewHorizons.Components.ShipLog
}
}
/*
/*
if(VesselCoordinatePromptHandler.KnowsEyeCoordinates())
{
AddSystemCard("EyeOfTheUniverse");
@ -279,7 +279,7 @@ namespace NewHorizons.Components.ShipLog
{
var rect = new Rect(0, 0, texture.width, texture.height);
var pivot = new Vector2(texture.width / 2, texture.height / 2);
return Sprite.Create(texture, rect, pivot);
return Sprite.Create(texture, rect, pivot, 100, 0, SpriteMeshType.FullRect, Vector4.zero, false);
}
private void OnTargetReferenceFrame(ReferenceFrame referenceFrame)

View File

@ -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;
/// <summary>
/// When a Fluid Detector enters this volume, it's splash effects will get colourized to match whats on this planet
/// </summary>
public class SplashColourizer : MonoBehaviour
{
public float _radius;
private SphereShape _sphereShape;
private Dictionary<SplashEffect, GameObject> _cachedOriginalPrefabs = new();
private Dictionary<SplashEffect, GameObject> _cachedModifiedPrefabs = new();
private FluidDetector _playerDetector, _shipDetector, _probeDetector;
private MColor _waterColour, _cloudColour, _plasmaColour, _sandColour;
private GameObject _prefabHolder;
private bool _probeInsideVolume;
private List<Texture> _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>();
_sphereShape.radius = _radius;
volume.AddComponent<OWTriggerVolume>();
_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<SplashColourizer>();
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<DynamicFluidDetector>());
CachePrefabs(_shipDetector = Locator.GetShipDetector().GetComponent<ShipFluidDetector>());
CachePrefabs(_probeDetector = Locator.GetProbe().GetDetectorObject().GetComponent<ProbeFluidDetector>());
GlobalMessenger<SurveyorProbe>.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<SurveyorProbe>.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);
/// <summary>
/// The probe keeps being null idgi
/// </summary>
/// <returns></returns>
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<MeshRenderer>(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);
}
}

View File

@ -21,6 +21,7 @@ namespace NewHorizons.Components.Volumes
public override void VanishPlayer(OWRigidbody playerBody, RelativeLocationData entryLocation)
{
Locator.GetPlayerAudioController().PlayOneShotInternal(AudioType.BH_BlackHoleEmission);
FadeHandler.FadeOut(0.2f, false);
Main.Instance.ChangeCurrentStarSystem(TargetSolarSystem, PlayerState.AtFlightConsole());
PlayerSpawnHandler.TargetSpawnID = TargetSpawnID;
}

View File

@ -1,8 +1,4 @@
using NewHorizons.External.SerializableEnums;
using NewHorizons.Handlers;
using NewHorizons.Utility;
using NewHorizons.Utility.OWML;
using System.Collections;
using NewHorizons.External.Modules;
using UnityEngine;
@ -10,78 +6,17 @@ namespace NewHorizons.Components.Volumes
{
internal class LoadCreditsVolume : BaseVolume
{
public NHCreditsType creditsType = NHCreditsType.Fast;
public string gameOverText;
public DeathType deathType = DeathType.Default;
private GameOverController _gameOverController;
private PlayerCameraEffectController _playerCameraEffectController;
public void Start()
{
_gameOverController = FindObjectOfType<GameOverController>();
_playerCameraEffectController = FindObjectOfType<PlayerCameraEffectController>();
}
public GameOverModule gameOver;
public DeathType? deathType;
public override void OnTriggerVolumeEntry(GameObject hitObj)
{
if (hitObj.CompareTag("PlayerDetector") && enabled)
if (hitObj.CompareTag("PlayerDetector") && enabled && (string.IsNullOrEmpty(gameOver.condition) || DialogueConditionManager.SharedInstance.GetConditionState(gameOver.condition)))
{
// Have to run it off the mod behaviour since the game over controller disables everything
Delay.StartCoroutine(GameOver());
NHGameOverManager.Instance.StartGameOverSequence(gameOver, deathType);
}
}
private IEnumerator GameOver()
{
OWInput.ChangeInputMode(InputMode.None);
ReticleController.Hide();
Locator.GetPromptManager().SetPromptsVisible(false);
Locator.GetPauseCommandListener().AddPauseCommandLock();
// The PlayerCameraEffectController is what actually kills us, so convince it we're already dead
Locator.GetDeathManager()._isDead = true;
_playerCameraEffectController.OnPlayerDeath(deathType);
yield return new WaitForSeconds(_playerCameraEffectController._deathFadeLength);
if (!string.IsNullOrEmpty(gameOverText) && _gameOverController != null)
{
_gameOverController._deathText.text = TranslationHandler.GetTranslation(gameOverText, TranslationHandler.TextType.UI);
_gameOverController.SetupGameOverScreen(5f);
// We set this to true to stop it from loading the credits scene, so we can do it ourselves
_gameOverController._loading = true;
yield return new WaitUntil(ReadytoLoadCreditsScene);
}
LoadCreditsScene();
}
private bool ReadytoLoadCreditsScene() => _gameOverController._fadedOutText && _gameOverController._textAnimator.IsComplete();
public override void OnTriggerVolumeExit(GameObject hitObj) { }
private void LoadCreditsScene()
{
NHLogger.LogVerbose($"Load credits {creditsType}");
switch (creditsType)
{
case NHCreditsType.Fast:
LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack);
break;
case NHCreditsType.Final:
LoadManager.LoadScene(OWScene.Credits_Final, LoadManager.FadeType.ToBlack);
break;
case NHCreditsType.Kazoo:
TimelineObliterationController.s_hasRealityEnded = true;
LoadManager.LoadScene(OWScene.Credits_Fast, LoadManager.FadeType.ToBlack);
break;
}
}
}
}

View File

@ -1,3 +1,4 @@
using NewHorizons.External.Modules;
using NewHorizons.OtherMods.AchievementsPlus;
using Newtonsoft.Json;
@ -44,5 +45,11 @@ namespace NewHorizons.External.Configs
/// The dimensions of the Echoes of the Eye subtitle is 669 x 67, so aim for that size
/// </summary>
public string subtitlePath = "subtitle.png";
/// <summary>
/// Custom game over messages for this mod. This can either display a title card before looping like in EOTE, or show a message and roll credits like the various time loop escape endings.
/// You must set a dialogue condition for the game over sequence to run.
/// </summary>
public GameOverModule[] gameOver;
}
}

View File

@ -64,6 +64,11 @@ namespace NewHorizons.External.Configs
/// </summary>
public string[] removeChildren;
/// <summary>
/// optimization. turn this off if you know you're generating a new body and aren't worried about other addons editing it.
/// </summary>
[DefaultValue(true)] public bool checkForExisting = true;
#endregion
#region Modules
@ -530,7 +535,7 @@ namespace NewHorizons.External.Configs
Spawn.shipSpawnPoints = new SpawnModule.ShipSpawnPoint[] { Spawn.shipSpawn };
}
// Because these guys put TWO spawn points
// Because these guys put TWO spawn points
if (starSystem == "2walker2.OogaBooga" && name == "The Campground")
{
Spawn.playerSpawnPoints[0].isDefault = true;
@ -658,6 +663,25 @@ namespace NewHorizons.External.Configs
}
}
if (Volumes?.creditsVolume != null)
{
foreach (var volume in Volumes.creditsVolume)
{
if (!string.IsNullOrEmpty(volume.gameOverText))
{
if (volume.gameOver == null)
{
volume.gameOver = new();
}
volume.gameOver.text = volume.gameOverText;
}
if (volume.creditsType != null)
{
volume.gameOver.creditsType = (SerializableEnums.NHCreditsType)volume.creditsType;
}
}
}
if (Base.invulnerableToSun)
{
Base.hasFluidDetector = false;
@ -742,4 +766,4 @@ namespace NewHorizons.External.Configs
}
#endregion
}
}
}

View File

@ -0,0 +1,32 @@
using NewHorizons.External.SerializableData;
using NewHorizons.External.SerializableEnums;
using Newtonsoft.Json;
using System.ComponentModel;
namespace NewHorizons.External.Modules
{
[JsonObject]
public class GameOverModule
{
/// <summary>
/// Text displayed in orange on game over. For localization, put translations under UI.
/// </summary>
public string text;
/// <summary>
/// Change the colour of the game over text. Leave empty to use the default orange.
/// </summary>
public MColor colour;
/// <summary>
/// Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.
/// Note this is a regular dialogue condition, not a persistent condition.
/// </summary>
public string condition;
/// <summary>
/// The type of credits that will run after the game over message is shown
/// </summary>
[DefaultValue("fast")] public NHCreditsType creditsType = NHCreditsType.Fast;
}
}

View File

@ -1,14 +1,78 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using NewHorizons.External.SerializableData;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace NewHorizons.External.Modules
{
[JsonObject]
public class ProcGenModule
{
/// <summary>
/// Scale height of the proc gen.
/// </summary>
[Range(0, double.MaxValue)] public float scale;
/// <summary>
/// Ground color, only applied if no texture or material is chosen.
/// </summary>
public MColor color;
[Range(0, double.MaxValue)] public float scale;
/// <summary>
/// Can pick a preset material with a texture from the base game. Does not work with color or any textures.
/// </summary>
public Material material;
/// <summary>
/// Can use a custom texture. Does not work with material or color.
/// </summary>
public string texture;
/// <summary>
/// Relative filepath to the texture used for the terrain's smoothness and metallic, which are controlled by the texture's alpha and red channels respectively. Optional.
/// Typically black with variable transparency, when metallic isn't wanted.
/// </summary>
public string smoothnessMap;
/// <summary>
/// How "glossy" the surface is, where 0 is diffuse, and 1 is like a mirror.
/// Multiplies with the alpha of the smoothness map if using one.
/// </summary>
[Range(0f, 1f)]
[DefaultValue(0f)]
public float smoothness = 0f;
/// <summary>
/// How metallic the surface is, from 0 to 1.
/// Multiplies with the red of the smoothness map if using one.
/// </summary>
[Range(0f, 1f)]
[DefaultValue(0f)]
public float metallic = 0f;
/// <summary>
/// Relative filepath to the texture used for the normal (aka bump) map. Optional.
/// </summary>
public string normalMap;
/// <summary>
/// Strength of the normal map. Usually 0-1, but can go above, or negative to invert the map.
/// </summary>
[DefaultValue(1f)]
public float normalStrength = 1f;
[JsonConverter(typeof(StringEnumConverter))]
public enum Material
{
[EnumMember(Value = @"default")] Default = 0,
[EnumMember(Value = @"ice")] Ice = 1,
[EnumMember(Value = @"quantum")] Quantum = 2,
[EnumMember(Value = @"rock")] Rock = 3
}
}
}

View File

@ -1,6 +1,7 @@
using NewHorizons.External.Modules.Props.Audio;
using NewHorizons.External.Modules.Props.Dialogue;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse
{
@ -46,5 +47,23 @@ namespace NewHorizons.External.Modules.Props.EyeOfTheUniverse
/// The dialogue to use for this traveler. If omitted, the first CharacterDialogueTree in the object will be used.
/// </summary>
public DialogueInfo dialogue;
/// <summary>
/// The name of the base game traveler to position this traveler after at the campfire, starting clockwise from Riebeck. Defaults to the end of the list (right before Riebeck).
/// </summary>
public TravelerName? afterTraveler;
[JsonConverter(typeof(StringEnumConverter))]
public enum TravelerName
{
Riebeck,
Chert,
Esker,
Felspar,
Gabbro,
Solanum,
Prisoner,
}
}
}

View File

@ -61,7 +61,7 @@ namespace NewHorizons.External.Modules
public float offset;
/// <summary>
/// The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode.
/// The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode. If empty, a texture will be created and cached based on the revealed sprite.
/// </summary>
public string outlineSprite;

View File

@ -1,5 +1,6 @@
using NewHorizons.External.SerializableEnums;
using Newtonsoft.Json;
using System;
using System.ComponentModel;
namespace NewHorizons.External.Modules.Volumes.VolumeInfos
@ -7,16 +8,20 @@ namespace NewHorizons.External.Modules.Volumes.VolumeInfos
[JsonObject]
public class LoadCreditsVolumeInfo : VolumeInfo
{
[DefaultValue("fast")] public NHCreditsType creditsType = NHCreditsType.Fast;
[Obsolete("Use gameOver.creditsType")]
public NHCreditsType? creditsType;
/// <summary>
/// Text displayed in orange on game over. For localization, put translations under UI.
/// </summary>
[Obsolete("Use gameOver.text")]
public string gameOverText;
/// <summary>
/// The type of death the player will have if they enter this volume.
/// The type of death the player will have if they enter this volume. Don't set to have the camera just fade out.
/// </summary>
[DefaultValue("default")] public NHDeathType deathType = NHDeathType.Default;
[DefaultValue("default")] public NHDeathType? deathType = null;
/// <summary>
/// The game over message to display. Leave empty to go straight to credits.
/// </summary>
public GameOverModule gameOver;
}
}

View File

@ -11,6 +11,8 @@ namespace NewHorizons.External.SerializableEnums
[EnumMember(Value = @"final")] Final = 1,
[EnumMember(Value = @"kazoo")] Kazoo = 2
[EnumMember(Value = @"kazoo")] Kazoo = 2,
[EnumMember(Value = @"none")] None = 3
}
}

View File

@ -266,28 +266,48 @@ namespace NewHorizons.Handlers
var quantumCampsiteController = Object.FindObjectOfType<QuantumCampsiteController>();
var travelers = new List<Transform>()
{
quantumCampsiteController._travelerControllers[0].transform, // Riebeck
quantumCampsiteController._travelerControllers[2].transform, // Chert
quantumCampsiteController._travelerControllers[6].transform, // Esker
quantumCampsiteController._travelerControllers[1].transform, // Felspar
quantumCampsiteController._travelerControllers[3].transform, // Gabbro
};
var travelers = new List<Transform>();
if (quantumCampsiteController._hasMetSolanum)
{
travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum
}
if (quantumCampsiteController._hasMetPrisoner)
var hasMetSolanum = quantumCampsiteController._hasMetSolanum;
var hasMetPrisoner = quantumCampsiteController._hasMetPrisoner;
// The order of the travelers in the base game differs depending on if the player has met both Solanum and the Prisoner or not.
if (hasMetPrisoner && hasMetSolanum)
{
travelers.Add(quantumCampsiteController._travelerControllers[0].transform); // Riebeck
travelers.Add(quantumCampsiteController._travelerControllers[5].transform); // Prisoner
travelers.Add(quantumCampsiteController._travelerControllers[6].transform); // Esker
travelers.Add(quantumCampsiteController._travelerControllers[1].transform); // Felspar
travelers.Add(quantumCampsiteController._travelerControllers[3].transform); // Gabbro
travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum
travelers.Add(quantumCampsiteController._travelerControllers[2].transform); // Chert
}
else
{
travelers.Add(quantumCampsiteController._travelerControllers[0].transform); // Riebeck
travelers.Add(quantumCampsiteController._travelerControllers[2].transform); // Chert
travelers.Add(quantumCampsiteController._travelerControllers[6].transform); // Esker
travelers.Add(quantumCampsiteController._travelerControllers[1].transform); // Felspar
travelers.Add(quantumCampsiteController._travelerControllers[3].transform); // Gabbro
if (hasMetSolanum)
travelers.Add(quantumCampsiteController._travelerControllers[4].transform); // Solanum
if (hasMetPrisoner)
travelers.Add(quantumCampsiteController._travelerControllers[5].transform); // Prisoner
}
// Custom travelers (starting at index 7)
// Custom travelers (starting at index 7, after Esker). We loop through the array instead of the list of custom travelers in case a non-NH mod added their own.
for (int i = 7; i < quantumCampsiteController._travelerControllers.Length; i++)
{
travelers.Add(quantumCampsiteController._travelerControllers[i].transform);
var travelerInfo = GetActiveCustomEyeTravelers().FirstOrDefault(t => t.controller == quantumCampsiteController._travelerControllers[i]);
var travelerName = travelerInfo?.info?.afterTraveler;
if (travelerName.HasValue)
{
InsertTravelerAfter(quantumCampsiteController, travelers, travelerInfo.info.afterTraveler.ToString(), quantumCampsiteController._travelerControllers[i].transform);
}
else
{
travelers.Add(quantumCampsiteController._travelerControllers[i].transform);
}
}
var radius = 2f + 0.2f * travelers.Count;
@ -312,6 +332,22 @@ namespace NewHorizons.Handlers
}
}
private static void InsertTravelerAfter(QuantumCampsiteController campsite, List<Transform> travelers, string travelerName, Transform newTraveler)
{
if (travelerName == "Prisoner")
travelerName = "Prisoner_Campfire";
var existingTraveler = campsite._travelerControllers.FirstOrDefault(c => c.name == travelerName);
if (existingTraveler != null)
{
var index = travelers.IndexOf(existingTraveler.transform);
travelers.Insert(index + 1, newTraveler);
}
else
{
travelers.Add(newTraveler);
}
}
public class EyeTravelerData
{
public string id;

View File

@ -11,24 +11,62 @@ namespace NewHorizons.Handlers
/// </summary>
public static class FadeHandler
{
public static void FadeOut(float length) => Delay.StartCoroutine(FadeOutCoroutine(length));
public static void FadeOut(float length) => Delay.StartCoroutine(FadeOutCoroutine(length, true));
private static IEnumerator FadeOutCoroutine(float length)
public static void FadeOut(float length, bool fadeSound) => Delay.StartCoroutine(FadeOutCoroutine(length, fadeSound));
public static void FadeIn(float length) => Delay.StartCoroutine(FadeInCoroutine(length));
private static IEnumerator FadeOutCoroutine(float length, bool fadeSound)
{
// Make sure its not already faded
if (!LoadManager.s_instance._fadeCanvas.enabled)
{
LoadManager.s_instance._fadeCanvas.enabled = true;
float startTime = Time.unscaledTime;
float endTime = Time.unscaledTime + length;
while (Time.unscaledTime < endTime)
{
var t = Mathf.Clamp01((Time.unscaledTime - startTime) / length);
LoadManager.s_instance._fadeImage.color = Color.Lerp(Color.clear, Color.black, t);
if (fadeSound)
{
AudioListener.volume = 1f - t;
}
yield return new WaitForEndOfFrame();
}
LoadManager.s_instance._fadeImage.color = Color.black;
if (fadeSound)
{
AudioListener.volume = 0;
}
yield return new WaitForEndOfFrame();
}
else
{
yield return new WaitForSeconds(length);
}
}
private static IEnumerator FadeInCoroutine(float length)
{
LoadManager.s_instance._fadeCanvas.enabled = true;
float startTime = Time.unscaledTime;
float endTime = Time.unscaledTime + length;
while (Time.unscaledTime < endTime)
{
var t = Mathf.Clamp01((Time.unscaledTime - startTime) / length);
LoadManager.s_instance._fadeImage.color = Color.Lerp(Color.clear, Color.black, t);
AudioListener.volume = 1f - t;
LoadManager.s_instance._fadeImage.color = Color.Lerp(Color.black, Color.clear, t);
AudioListener.volume = t;
yield return new WaitForEndOfFrame();
}
LoadManager.s_instance._fadeImage.color = Color.black;
AudioListener.volume = 0;
AudioListener.volume = 1;
LoadManager.s_instance._fadeCanvas.enabled = false;
LoadManager.s_instance._fadeImage.color = Color.clear;
yield return new WaitForEndOfFrame();
}
@ -36,7 +74,7 @@ namespace NewHorizons.Handlers
private static IEnumerator FadeThenCoroutine(float length, Action action)
{
yield return FadeOutCoroutine(length);
yield return FadeOutCoroutine(length, true);
action?.Invoke();
}

View File

@ -4,7 +4,7 @@ using UnityEngine.SceneManagement;
namespace NewHorizons.Handlers
{
internal class InvulnerabilityHandler
public static class InvulnerabilityHandler
{
/// <summary>
/// Used in patches
@ -32,8 +32,25 @@ namespace NewHorizons.Handlers
}
}
private static DeathManager GetDeathManager() => GameObject.FindObjectOfType<DeathManager>();
private static PlayerResources GetPlayerResouces() => GameObject.FindObjectOfType<PlayerResources>();
private static DeathManager _deathManager;
private static DeathManager GetDeathManager()
{
if (_deathManager == null)
{
_deathManager = GameObject.FindObjectOfType<DeathManager>();
}
return _deathManager;
}
private static PlayerResources _playerResources;
private static PlayerResources GetPlayerResouces()
{
if (_playerResources == null)
{
_playerResources = GameObject.FindObjectOfType<PlayerResources>();
}
return _playerResources;
}
static InvulnerabilityHandler()
{

View File

@ -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
{
@ -168,26 +172,29 @@ namespace NewHorizons.Handlers
// I don't remember doing this why is it exceptions what am I doing
GameObject existingPlanet = null;
try
if (body.Config.checkForExisting) // TODO: remove this when we cache name->fullpath in Find
{
existingPlanet = AstroObjectLocator.GetAstroObject(body.Config.name).gameObject;
}
catch (Exception)
{
if (body?.Config?.name == null)
try
{
NHLogger.LogError($"How is there no name for {body}");
existingPlanet = AstroObjectLocator.GetAstroObject(body.Config.name).gameObject;
}
else
catch (Exception)
{
existingPlanet = SearchUtilities.Find(body.Config.name.Replace(" ", "") + "_Body", false);
if (body?.Config?.name == null)
{
NHLogger.LogError($"How is there no name for {body}");
}
else
{
existingPlanet = SearchUtilities.Find(body.Config.name.Replace(" ", "") + "_Body", false);
}
}
}
if (existingPlanet == null && body.Config.destroy)
{
NHLogger.LogError($"{body.Config.name} was meant to be destroyed, but was not found");
return false;
if (existingPlanet == null && body.Config.destroy)
{
NHLogger.LogError($"{body.Config.name} was meant to be destroyed, but was not found");
return false;
}
}
if (existingPlanet != null)
@ -287,9 +294,9 @@ namespace NewHorizons.Handlers
try
{
NHLogger.Log($"Creating [{body.Config.name}]");
var planetObject = GenerateBody(body, defaultPrimaryToSun)
var planetObject = GenerateBody(body, defaultPrimaryToSun)
?? throw new NullReferenceException("Something went wrong when generating the body but no errors were logged.");
planetObject.SetActive(true);
var ao = planetObject.GetComponent<NHAstroObject>();
@ -316,7 +323,7 @@ namespace NewHorizons.Handlers
{
NHLogger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}");
}
body.UnloadCache(true);
_loadedBodies.Add(body);
return true;
@ -390,7 +397,7 @@ namespace NewHorizons.Handlers
body.Config.MapMarker.enabled = false;
const float sphereOfInfluence = 2000f;
var owRigidBody = RigidBodyBuilder.Make(go, sphereOfInfluence, body.Config);
var ao = AstroObjectBuilder.Make(go, null, body, false);
@ -402,7 +409,7 @@ namespace NewHorizons.Handlers
BrambleDimensionBuilder.Make(body, go, ao, sector, body.Mod, owRigidBody);
go = SharedGenerateBody(body, go, sector, owRigidBody);
// Not included in SharedGenerate to not mess up gravity on base game planets
if (body.Config.Base.surfaceGravity != 0)
{
@ -467,7 +474,7 @@ namespace NewHorizons.Handlers
}
var sphereOfInfluence = GetSphereOfInfluence(body);
var owRigidBody = RigidBodyBuilder.Make(go, sphereOfInfluence, body.Config);
var ao = AstroObjectBuilder.Make(go, primaryBody, body, false);
@ -581,7 +588,7 @@ namespace NewHorizons.Handlers
GameObject procGen = null;
if (body.Config.ProcGen != null)
{
procGen = ProcGenBuilder.Make(go, sector, body.Config.ProcGen);
procGen = ProcGenBuilder.Make(body.Mod, go, sector, body.Config.ProcGen);
}
if (body.Config.Star != null)
@ -686,7 +693,7 @@ namespace NewHorizons.Handlers
SunOverrideBuilder.Make(go, sector, body.Config.Atmosphere, body.Config.Water, surfaceSize);
}
}
if (body.Config.Atmosphere.fogSize != 0)
{
fog = FogBuilder.Make(go, sector, body.Config.Atmosphere, body.Mod);
@ -764,6 +771,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);
@ -997,15 +1006,10 @@ namespace NewHorizons.Handlers
private static void RemoveChildren(GameObject go, NewHorizonsBody body)
{
var goPath = go.transform.GetPath();
var transforms = go.GetComponentsInChildren<Transform>(true);
foreach (var childPath in body.Config.removeChildren)
{
// Multiple children can have the same path so we delete all that match
var path = $"{goPath}/{childPath}";
var flag = true;
foreach (var childObj in transforms.Where(x => x.GetPath() == path))
foreach (var childObj in go.transform.FindAll(childPath))
{
flag = false;
// idk why we wait here but we do

View File

@ -2,6 +2,7 @@ using NewHorizons.Utility;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Profiling;
namespace NewHorizons.Handlers
{
@ -51,6 +52,9 @@ namespace NewHorizons.Handlers
/// </summary>
public static void SetUpStreaming(GameObject obj, Sector sector)
{
// TODO: used OFTEN by detail builder. 20-40ms adds up to seconds. speed up!
Profiler.BeginSample("get bundles");
// find the asset bundles to load
// tries the cache first, then builds
if (!_objectCache.TryGetValue(obj, out var assetBundles))
@ -94,7 +98,9 @@ namespace NewHorizons.Handlers
assetBundles = assetBundlesList.ToArray();
_objectCache[obj] = assetBundles;
}
Profiler.EndSample();
Profiler.BeginSample("get sectors");
foreach (var assetBundle in assetBundles)
{
// Track the sector even if its null. null means stay loaded forever
@ -105,7 +111,9 @@ namespace NewHorizons.Handlers
}
sectors.SafeAdd(sector);
}
Profiler.EndSample();
Profiler.BeginSample("load assets");
if (sector)
{
sector.OnOccupantEnterSector += _ =>
@ -128,6 +136,7 @@ namespace NewHorizons.Handlers
foreach (var assetBundle in assetBundles)
StreamingManager.LoadStreamingAssets(assetBundle);
}
Profiler.EndSample();
}
public static bool IsBundleInUse(string assetBundle)
@ -152,4 +161,4 @@ namespace NewHorizons.Handlers
}
}
}
}
}

View File

@ -109,7 +109,7 @@ namespace NewHorizons.Handlers
var tex = ImageUtilities.GetTexture(mod, filepath, false);
if (tex == null) return;
var sprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100.0f);
var sprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100, 0, SpriteMeshType.FullRect, Vector4.zero, false);
AddSubtitle(sprite);
}

View File

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using static NewHorizons.External.Configs.StarSystemConfig;
namespace NewHorizons.Handlers
@ -47,7 +48,7 @@ namespace NewHorizons.Handlers
if (_textureCache == null) _textureCache = new List<Texture2D>();
_textureCache.Add(texture);
var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(texture.width / 2f, texture.height / 2f));
var sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(texture.width / 2f, texture.height / 2f), 100, 0, SpriteMeshType.FullRect, Vector4.zero, false);
var name = ShipLogStarChartMode.UniqueIDToName(systemID);

View File

@ -6,6 +6,7 @@ using NewHorizons.Builder.Props;
using NewHorizons.Builder.Props.Audio;
using NewHorizons.Builder.Props.EchoesOfTheEye;
using NewHorizons.Builder.Props.TranslatorText;
using NewHorizons.Components;
using NewHorizons.Components.EOTE;
using NewHorizons.Components.Fixers;
using NewHorizons.Components.Ship;
@ -311,6 +312,7 @@ namespace NewHorizons
ImageUtilities.ClearCache();
AudioUtilities.ClearCache();
AssetBundleUtilities.ClearCache();
ProcGenBuilder.ClearCache();
}
IsSystemReady = false;
@ -478,6 +480,8 @@ namespace NewHorizons
// Fix spawn point
PlayerSpawnHandler.SetUpPlayerSpawn();
new GameObject(nameof(NHGameOverManager)).AddComponent<NHGameOverManager>();
if (isSolarSystem)
{
// Warp drive
@ -834,6 +838,10 @@ namespace NewHorizons
AssetBundleUtilities.PreloadBundle(bundle, mod);
}
}
if (addonConfig.gameOver != null)
{
NHGameOverManager.gameOvers[mod.ModHelper.Manifest.UniqueName] = addonConfig.gameOver;
}
AddonConfigs[mod] = addonConfig;
}

View File

@ -10,6 +10,8 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<NoWarn>1701;1702;1591</NoWarn>
<!-- <DefineConstants>ENABLE_PROFILER</DefineConstants>-->
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DebugType>none</DebugType>
@ -39,4 +41,4 @@
<ItemGroup>
<Content Include="NewHorizons.csproj.user" />
</ItemGroup>
</Project>
</Project>

View File

@ -0,0 +1,15 @@
using HarmonyLib;
using NewHorizons.Components;
namespace NewHorizons.Patches;
[HarmonyPatch]
public static class DeathManagerPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(DeathManager), nameof(DeathManager.FinishDeathSequence))]
public static void DeathManager_FinishDeathSequence()
{
NHGameOverManager.Instance.TryHijackDeathSequence();
}
}

View File

@ -0,0 +1,73 @@
#if ENABLE_PROFILER
using HarmonyLib;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.Profiling;
namespace NewHorizons.Patches;
/// <summary>
/// attach profiler markers to important methods
/// </summary>
[HarmonyPatch]
public static class ProfilerPatch
{
private static string FriendlyName(this MethodBase @this) => $"{@this.DeclaringType.Name}.{@this.Name}";
[HarmonyTargetMethods]
public static IEnumerable<MethodBase> TargetMethods()
{
foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
{
if (!(
type.Name == "Main" ||
type.Name.EndsWith("Builder") ||
type.Name.EndsWith("Handler") ||
type.Name.EndsWith("Utilities")
)) continue;
foreach (var method in type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
if (method.ContainsGenericParameters) continue;
// Main.Instance.ModHelper.Console.WriteLine($"[profiler] profiling {method.FriendlyName()}");
yield return method;
}
}
}
[HarmonyPrefix]
public static void Prefix(MethodBase __originalMethod /*, out Stopwatch __state*/)
{
Profiler.BeginSample(__originalMethod.FriendlyName());
// __state = new Stopwatch();
// __state.Start();
}
[HarmonyPostfix]
public static void Postfix( /*MethodBase __originalMethod, Stopwatch __state*/)
{
Profiler.EndSample();
// __state.Stop();
// Main.Instance.ModHelper.Console.WriteLine($"[profiler] {__originalMethod.MethodName()} took {__state.Elapsed.TotalMilliseconds:f1} ms");
}
}
/// <summary>
/// bundle loading causes log spam that slows loading, but only in unity dev profiler mode.
/// patch it out so it doesnt do false-positive slowness.
/// </summary>
[HarmonyPatch]
public static class DisableShaderLogSpamPatch
{
[HarmonyPrefix]
[HarmonyPatch(typeof(StackTraceUtility), "ExtractStackTrace")]
[HarmonyPatch(typeof(Application), "CallLogCallback")]
private static bool DisableShaderLogSpam() => false;
}
#endif

View File

@ -38,6 +38,13 @@
"type": "string",
"description": "The path to the addons subtitle for the main menu.\nDefaults to \"subtitle.png\".\nThe dimensions of the Echoes of the Eye subtitle is 669 x 67, so aim for that size"
},
"gameOver": {
"type": "array",
"description": "Custom game over messages for this mod. This can either display a title card before looping like in EOTE, or show a message and roll credits like the various time loop escape endings.\nYou must set a dialogue condition for the game over sequence to run.",
"items": {
"$ref": "#/definitions/GameOverModule"
}
},
"$schema": {
"type": "string",
"description": "The schema to validate with"
@ -79,6 +86,80 @@
}
}
}
},
"GameOverModule": {
"type": "object",
"additionalProperties": false,
"properties": {
"text": {
"type": "string",
"description": "Text displayed in orange on game over. For localization, put translations under UI."
},
"colour": {
"description": "Change the colour of the game over text. Leave empty to use the default orange.",
"$ref": "#/definitions/MColor"
},
"condition": {
"type": "string",
"description": "Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.\nNote this is a regular dialogue condition, not a persistent condition."
},
"creditsType": {
"description": "The type of credits that will run after the game over message is shown",
"default": "fast",
"$ref": "#/definitions/NHCreditsType"
}
}
},
"MColor": {
"type": "object",
"additionalProperties": false,
"properties": {
"r": {
"type": "integer",
"description": "The red component of this colour from 0-255, higher values will make the colour glow if applicable.",
"format": "int32",
"maximum": 2147483647.0,
"minimum": 0.0
},
"g": {
"type": "integer",
"description": "The green component of this colour from 0-255, higher values will make the colour glow if applicable.",
"format": "int32",
"maximum": 2147483647.0,
"minimum": 0.0
},
"b": {
"type": "integer",
"description": "The blue component of this colour from 0-255, higher values will make the colour glow if applicable.",
"format": "int32",
"maximum": 2147483647.0,
"minimum": 0.0
},
"a": {
"type": "integer",
"description": "The alpha (opacity) component of this colour",
"format": "int32",
"default": 255,
"maximum": 255.0,
"minimum": 0.0
}
}
},
"NHCreditsType": {
"type": "string",
"description": "",
"x-enumNames": [
"Fast",
"Final",
"Kazoo",
"None"
],
"enum": [
"fast",
"final",
"kazoo",
"none"
]
}
},
"$docs": {

View File

@ -43,6 +43,11 @@
"type": "string"
}
},
"checkForExisting": {
"type": "boolean",
"description": "optimization. turn this off if you know you're generating a new body and aren't worried about other addons editing it.",
"default": true
},
"AmbientLights": {
"type": "array",
"description": "Add ambient lights to this body",
@ -353,16 +358,72 @@
"type": "object",
"additionalProperties": false,
"properties": {
"color": {
"$ref": "#/definitions/MColor"
},
"scale": {
"type": "number",
"description": "Scale height of the proc gen.",
"format": "float",
"minimum": 0.0
},
"color": {
"description": "Ground color, only applied if no texture or material is chosen.",
"$ref": "#/definitions/MColor"
},
"material": {
"description": "Can pick a preset material with a texture from the base game. Does not work with color or any textures.",
"$ref": "#/definitions/Material"
},
"texture": {
"type": "string",
"description": "Can use a custom texture. Does not work with material or color."
},
"smoothnessMap": {
"type": "string",
"description": "Relative filepath to the texture used for the terrain's smoothness and metallic, which are controlled by the texture's alpha and red channels respectively. Optional.\nTypically black with variable transparency, when metallic isn't wanted."
},
"smoothness": {
"type": "number",
"description": "How \"glossy\" the surface is, where 0 is diffuse, and 1 is like a mirror.\nMultiplies with the alpha of the smoothness map if using one.",
"format": "float",
"default": 0.0,
"maximum": 1.0,
"minimum": 0.0
},
"metallic": {
"type": "number",
"description": "How metallic the surface is, from 0 to 1.\nMultiplies with the red of the smoothness map if using one.",
"format": "float",
"default": 0.0,
"maximum": 1.0,
"minimum": 0.0
},
"normalMap": {
"type": "string",
"description": "Relative filepath to the texture used for the normal (aka bump) map. Optional."
},
"normalStrength": {
"type": "number",
"description": "Strength of the normal map. Usually 0-1, but can go above, or negative to invert the map.",
"format": "float",
"default": 1.0
}
}
},
"Material": {
"type": "string",
"description": "",
"x-enumNames": [
"Default",
"Ice",
"Quantum",
"Rock"
],
"enum": [
"default",
"ice",
"quantum",
"rock"
]
},
"AtmosphereModule": {
"type": "object",
"additionalProperties": false,
@ -1022,6 +1083,17 @@
"dialogue": {
"description": "The dialogue to use for this traveler. If omitted, the first CharacterDialogueTree in the object will be used.",
"$ref": "#/definitions/DialogueInfo"
},
"afterTraveler": {
"description": "The name of the base game traveler to position this traveler after at the campfire, starting clockwise from Riebeck. Defaults to the end of the list (right before Riebeck).",
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/TravelerName"
}
]
}
}
},
@ -1466,6 +1538,28 @@
"none"
]
},
"TravelerName": {
"type": "string",
"description": "",
"x-enumNames": [
"Riebeck",
"Chert",
"Esker",
"Felspar",
"Gabbro",
"Solanum",
"Prisoner"
],
"enum": [
"Riebeck",
"Chert",
"Esker",
"Felspar",
"Gabbro",
"Solanum",
"Prisoner"
]
},
"InstrumentZoneInfo": {
"type": "object",
"additionalProperties": false,
@ -4690,7 +4784,7 @@
},
"outlineSprite": {
"type": "string",
"description": "The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode."
"description": "The path to the sprite (.png/.jpg/.exr) to show when the planet is unexplored in map mode. If empty, a texture will be created and cached based on the revealed sprite."
},
"remove": {
"type": "boolean",
@ -6391,18 +6485,44 @@
"type": "string",
"description": "An optional rename of this object"
},
"creditsType": {
"default": "fast",
"$ref": "#/definitions/NHCreditsType"
"deathType": {
"description": "The type of death the player will have if they enter this volume. Don't set to have the camera just fade out.",
"default": "default",
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/NHDeathType"
}
]
},
"gameOverText": {
"gameOver": {
"description": "The game over message to display. Leave empty to go straight to credits.",
"$ref": "#/definitions/GameOverModule"
}
}
},
"GameOverModule": {
"type": "object",
"additionalProperties": false,
"properties": {
"text": {
"type": "string",
"description": "Text displayed in orange on game over. For localization, put translations under UI."
},
"deathType": {
"description": "The type of death the player will have if they enter this volume.",
"default": "default",
"$ref": "#/definitions/NHDeathType"
"colour": {
"description": "Change the colour of the game over text. Leave empty to use the default orange.",
"$ref": "#/definitions/MColor"
},
"condition": {
"type": "string",
"description": "Condition that must be true for this game over to trigger. If this is on a LoadCreditsVolume, leave empty to always trigger this game over.\nNote this is a regular dialogue condition, not a persistent condition."
},
"creditsType": {
"description": "The type of credits that will run after the game over message is shown",
"default": "fast",
"$ref": "#/definitions/NHCreditsType"
}
}
},
@ -6412,12 +6532,14 @@
"x-enumNames": [
"Fast",
"Final",
"Kazoo"
"Kazoo",
"None"
],
"enum": [
"fast",
"final",
"kazoo"
"kazoo",
"none"
]
},
"CometTailModule": {

View File

@ -12,6 +12,7 @@ namespace NewHorizons.Utility.Files
public static class AssetBundleUtilities
{
public static Dictionary<string, (AssetBundle bundle, bool keepLoaded)> AssetBundles = new();
private static Dictionary<string, GameObject> _prefabCache = new();
private static readonly List<AssetBundleCreateRequest> _loadingBundles = new();
@ -52,6 +53,7 @@ namespace NewHorizons.Utility.Files
}
AssetBundles = AssetBundles.Where(x => x.Value.keepLoaded).ToDictionary(x => x.Key, x => x.Value);
_prefabCache.Clear();
}
public static void PreloadBundle(string assetBundleRelativeDir, IModBehaviour mod)
@ -113,11 +115,17 @@ namespace NewHorizons.Utility.Files
public static GameObject LoadPrefab(string assetBundleRelativeDir, string pathInBundle, IModBehaviour mod)
{
var prefab = Load<GameObject>(assetBundleRelativeDir, pathInBundle, mod);
if (_prefabCache.TryGetValue(assetBundleRelativeDir + pathInBundle, out var prefab))
return prefab;
prefab = Load<GameObject>(assetBundleRelativeDir, pathInBundle, mod);
prefab.SetActive(false);
ReplaceShaders(prefab);
// replacing shaders is expensive, so cache it
_prefabCache.Add(assetBundleRelativeDir + pathInBundle, prefab);
return prefab;
}

View File

@ -1,9 +1,12 @@
using NewHorizons.Builder.Props;
using NewHorizons.External;
using NewHorizons.External.Modules.VariableSize;
using NewHorizons.Utility.OWML;
using OWML.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
namespace NewHorizons.Utility.Files
@ -43,7 +46,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))
{
@ -91,8 +104,8 @@ namespace NewHorizons.Utility.Files
DeleteTexture(key, texture);
}
public static void DeleteTexture(string key, Texture2D texture)
{
public static void DeleteTexture(string key, Texture2D texture)
{
if (_textureCache.ContainsKey(key))
{
if (_textureCache[key] == texture)
@ -247,7 +260,7 @@ namespace NewHorizons.Utility.Files
return texture;
}
public static Texture2D MakeOutline(Texture2D texture, Color color, int thickness)
private static Texture2D MakeOutline(Texture2D texture, Color color, int thickness)
{
var key = $"{texture.name} > outline {color} {thickness}";
if (_textureCache.TryGetValue(key, out var existingTexture)) return (Texture2D)existingTexture;
@ -368,7 +381,7 @@ namespace NewHorizons.Utility.Files
var pixels = image.GetPixels();
for (int i = 0; i < pixels.Length; i++)
{
var amount = (i % image.width) / (float) image.width;
var amount = (i % image.width) / (float)image.width;
var lightTint = LerpColor(lightTintStart, lightTintEnd, amount);
var darkTint = LerpColor(darkTintStart, darkTintEnd, amount);
@ -489,5 +502,119 @@ namespace NewHorizons.Utility.Files
sprite.name = texture.name;
return sprite;
}
public static Texture2D GetCachedOutlineOrCreate(NewHorizonsBody body, Texture2D original, string originalPath)
{
if (string.IsNullOrEmpty(originalPath))
{
Texture2D defaultTexture = null;
if (body.Config.Star != null)
{
defaultTexture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModeStarOutline.png");
}
else if (body.Config.Atmosphere != null)
{
defaultTexture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModNoAtmoOutline.png");
}
else
{
defaultTexture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModePlanetOutline.png");
}
return defaultTexture;
}
else
{
var cachedPath = Path.Combine(body.Mod.ModHelper.Manifest.ModFolderPath, $"TextureCache_{Main.Instance.CurrentStarSystem}", originalPath);
var outlineTexture = ImageUtilities.GetTexture(body.Mod, cachedPath);
if (outlineTexture == null)
{
NHLogger.LogVerbose($"Caching outline to {cachedPath}");
var newTexture = ImageUtilities.MakeOutline(original, Color.white, 10);
Directory.CreateDirectory(Path.GetDirectoryName(cachedPath));
File.WriteAllBytes(cachedPath, newTexture.EncodeToPNG());
return newTexture;
}
else
{
return outlineTexture;
}
}
}
public static Texture2D AutoGenerateMapModePicture(NewHorizonsBody body)
{
Texture2D texture;
if (body.Config.Star != null)
{
texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModeStar.png");
}
else if (body.Config.Atmosphere != null)
{
texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModNoAtmo.png");
}
else
{
texture = ImageUtilities.GetTexture(Main.Instance, "Assets/DefaultMapModePlanet.png");
}
var color = GetDominantPlanetColor(body);
var darkColor = new Color(color.r / 3f, color.g / 3f, color.b / 3f);
texture = ImageUtilities.LerpGreyscaleImage(texture, color, darkColor);
return texture;
}
private static Color GetDominantPlanetColor(NewHorizonsBody body)
{
try
{
var starColor = body.Config?.Star?.tint;
if (starColor != null) return starColor.ToColor();
var atmoColor = body.Config.Atmosphere?.atmosphereTint;
if (body.Config.Atmosphere?.clouds != null && atmoColor != null) return atmoColor.ToColor();
if (body.Config?.HeightMap?.textureMap != null)
{
try
{
var texture = ImageUtilities.GetTexture(body.Mod, body.Config.HeightMap.textureMap);
var landColor = ImageUtilities.GetAverageColor(texture);
if (landColor != null) return landColor;
}
catch (Exception) { }
}
var waterColor = body.Config.Water?.tint;
if (waterColor != null) return waterColor.ToColor();
var lavaColor = body.Config.Lava?.tint;
if (lavaColor != null) return lavaColor.ToColor();
var sandColor = body.Config.Sand?.tint;
if (sandColor != null) return sandColor.ToColor();
switch (body.Config?.Props?.singularities?.FirstOrDefault()?.type)
{
case SingularityModule.SingularityType.BlackHole:
return Color.black;
case SingularityModule.SingularityType.WhiteHole:
return Color.white;
}
}
catch (Exception)
{
NHLogger.LogWarning($"Something went wrong trying to pick the colour for {body.Config.name} but I'm too lazy to fix it.");
}
return Color.white;
}
}
}

View File

@ -2,6 +2,7 @@ using NewHorizons.Utility.OWML;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.SceneManagement;
using Object = UnityEngine.Object;
@ -116,19 +117,23 @@ namespace NewHorizons.Utility
if (CachedGameObjects.TryGetValue(path, out var go)) return go;
// 1: normal find
Profiler.BeginSample("1");
go = GameObject.Find(path);
if (go)
{
CachedGameObjects.Add(path, go);
Profiler.EndSample();
return go;
}
Profiler.EndSample();
Profiler.BeginSample("2");
// 2: find inactive using root + transform.find
var names = path.Split('/');
// Cache the root objects so we don't loop through all of them each time
var rootName = names[0];
if (!CachedRootGameObjects.TryGetValue(rootName, out var root))
if (!CachedRootGameObjects.TryGetValue(rootName, out var root))
{
root = SceneManager.GetActiveScene().GetRootGameObjects().FirstOrDefault(x => x.name == rootName);
if (root != null)
@ -142,9 +147,12 @@ namespace NewHorizons.Utility
if (go)
{
CachedGameObjects.Add(path, go);
Profiler.EndSample();
return go;
}
Profiler.EndSample();
Profiler.BeginSample("3");
var name = names.Last();
if (warn) NHLogger.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)
@ -153,10 +161,12 @@ namespace NewHorizons.Utility
if (go)
{
CachedGameObjects.Add(path, go);
Profiler.EndSample();
return go;
}
if (warn) NHLogger.LogWarning($"Couldn't find object with name {name}");
Profiler.EndSample();
return null;
}
@ -169,5 +179,31 @@ namespace NewHorizons.Utility
}
return children;
}
/// <summary>
/// transform.find but works for gameobjects with same name
/// </summary>
public static List<Transform> FindAll(this Transform @this, string path)
{
var names = path.Split('/');
var currentTransforms = new List<Transform> { @this };
foreach (var name in names)
{
var newTransforms = new List<Transform>();
foreach (var currentTransform in currentTransforms)
{
foreach (Transform child in currentTransform)
{
if (child.name == name)
{
newTransforms.Add(child);
}
}
}
currentTransforms = newTransforms;
}
return currentTransforms;
}
}
}

View File

@ -4,7 +4,7 @@
"author": "xen, Bwc9876, JohnCorby, MegaPiggy, Trifid, and friends",
"name": "New Horizons",
"uniqueName": "xen.NewHorizons",
"version": "1.26.1",
"version": "1.27.0",
"owmlVersion": "2.12.1",
"dependencies": [ "JohnCorby.VanillaFix", "xen.CommonCameraUtility", "dgarro.CustomShipLogModes" ],
"conflicts": [ "PacificEngine.OW_CommonResources" ],

View File

@ -15,38 +15,14 @@ A dialogue tree is an entire conversation, it's made up of dialogue nodes.
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
### Conditions
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
A persistent condition is similar to a condition, except it _persists_ through loops, and is saved on the players save file.
In dialogue, the available conversation topics can be limited by what the player knows, defined using dialogue conditions, persistent conditions, and ship log facts. Dialogue can also set conditions to true or false, and reveal ship log facts to the player. This is covered in detail later on this page.
### 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.
### ReuseDialogueOptionsListFrom
This is a custom XML node introduced by New Horizons. Use it when adding new dialogue to existing characters, to repeat the dialogue options list from another node.
For example, Slate's first dialogue with options is named `Scientist5`. To make a custom DialogueNode using these dialogue options (meaning new dialogue said by Slate, but reusing the possible player responses) you can write:
```xml
<DialogueNode>
<Name>...</Name>
<Dialogue>
<Page>NEW DIALOGUE FOR SLATE HERE.</Page>
</Dialogue>
<DialogueOptionsList>
<ReuseDialogueOptionsListFrom>Scientist5</ReuseDialogueOptionsListFrom>
</DialogueOptionsList>
</DialogueNode>
```
Note: If you're loading dialogue in code, 2 frames must pass before entering the conversation in order for ReuseDialogueOptionsListFrom to take effect.
## Example XML
Here's an example dialogue XML:
@ -176,11 +152,39 @@ In addition to `<DialogueOptions>`, there are other ways to control the flow of
Defining `<DialogueTarget>` in the `<DialogueNode>` tag instead of a `<DialogueOption>` will make the conversation go directly to that target after the character is done talking.
### DialogueTargetShipLogCondition
### EntryCondition
Used in tandem with `DialogueTarget`, makes it so you must have a [ship log fact](/guides/ship-log#explore-facts) to go to the next node.
The first dialogue node that opens when a player starts talking to a character is chosen using this property. To mark a DialogueNode as beginning the dialogue by default, use the condition DEFAULT (a DialogueTree should always have a node with the DEFAULT entry condition to ensure there is a way to start dialogue).
### Adding to existing dialogue
The entry condition can be either a condition or a persistent 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.
Conditions can be set in dialogue using `<SetCondition>CONDITION_NAME</SetCondition>`. This can go in a DialogueNode in which case it will set the condition to true when that node is read. There is a similar version of this for DialogueOptions called `<ConditionToSet>CONDITION_NAME</ConditionToSet>` which will set it to true when that option is selected. Conditions can be disabled using `<ConditionToCancel>CONDITION_NAME</<ConditionToCancel>` in a DialogueOption, but cannot be disabled just by entering a DialogueNode.
You can lock a DialogueOption behind a condition using `<RequiredCondition>CONDITION_NAME</RequiredCondition>`, or remove a DialogueOption after the condition is set to true using `<CancelledCondition>CONDITION_NAME</CancelledCondition>`.
Dialogue conditions can also be set in code with `DialogueConditionManager.SharedInstance.SetConditionState("CONDITION_NAME", true/false)` or read with `DialogueConditionManager.SharedInstance.GetConditionState("CONDITION_NAME")`.
Note that `CONDITION_NAME` is a placeholder that you would replace with whatever you want to call your condition. Consider appending conditions with the name of your mod to make for better compatibility between mods, for example a condition name like `SPOKEN_TO` is very generic and might conflict with other mods whereas `NH_EXAMPLES_SPOKEN_TO_ERNESTO` is much less likely to conflict with another mod.
### Persistent Condition
A persistent condition is similar to a condition, except it _persists_ through loops, and is saved on the players save file.
Persistent conditions shared many similar traits with regular dialogue conditions. You can use `<SetPersistentCondition>`, `<DisablePersistentCondition>`. On dialogue options you can use `<RequiredPersistentCondition>`, `<CancelledPersistentCondition>`
Persistent conditions can also be set in code with `PlayerData.SetPersistentCondition("PERSISTENT_CONDITION_NAME", true/false)` and read using `PlayerData.GetPersistentCondition("PERSISTENT_CONDITION_NAME")`.
### Ship Logs
Dialogue can interact with ship logs, either granting them to the player (`<RevealFacts>` on a DialogueNode) or locking dialogue behind ship log completion (`<RequiredLogCondition>` on a DialogueOption).
You can also use `<DialogueTargetShipLogCondition>` in tandem with `DialogueTarget` to make it so you must have a [ship log fact](/guides/ship-log#explore-facts) to go to the next node.
## Adding to existing dialogue
Here's an example of how to add new dialogue to Slate, without overwriting their existing dialogue. This will also allow multiple mods to all add new dialogue to the same character.
@ -221,8 +225,33 @@ To use this additional dialogue you need to reference it in a planet config file
]
```
### ReuseDialogueOptionsListFrom
This is a custom XML node introduced by New Horizons. Use it when adding new dialogue to existing characters, to repeat the dialogue options list from another node.
For example, Slate's first dialogue with options is named `Scientist5`. To make a custom DialogueNode using these dialogue options (meaning new dialogue said by Slate, but reusing the possible player responses) you can write:
```xml
<DialogueNode>
<Name>...</Name>
<Dialogue>
<Page>NEW DIALOGUE FOR SLATE HERE.</Page>
</Dialogue>
<DialogueOptionsList>
<ReuseDialogueOptionsListFrom>Scientist5</ReuseDialogueOptionsListFrom>
</DialogueOptionsList>
</DialogueNode>
```
Note: If you're loading dialogue in code, 2 frames must pass before entering the conversation in order for ReuseDialogueOptionsListFrom to take effect.
## Dialogue FAQ
### How do I easily position my dialogue relative to a speaking character
Use `pathToAnimController` to specify the path to the speaking character (if they are a Nomai or Hearthian make sure this goes directly to whatever script controls their animations), then set `isRelativeToParent` to true (this is setting available on all NH props for easier positioning). Now when you set their `position`, it will be relative to the speaker. Since this position is normally where the character is standing, set the `y` position to match how tall the character is. Instead of `pathToAnimController` you can also use `parentPath`.
### How do I have the dialogue prompt say "Read" or "Play recording"
`<NameField>` sets the name of the character, which will then show in the prompt to start dialogue. You can alternatively use `<NameField>SIGN</NameField>` to have the prompt say "Read", and `<NameField>RECORDING</NameField>` to have it say "Play recording".

View File

@ -0,0 +1,26 @@
---
title: Nomai Text
description: Guide to making Nomai Text in New Horizons
---
This page goes over how to use Nomai text in New Horizons.
## Understanding Nomai Text
Nomai text is the backbone of many story mods. There are two parts to setting up Nomai text: The XML file and the planet config.
### XML
In your XML, you define the actual raw text which will be displayed, the ship logs it unlocks, and the way it branches. See [the Nomai text XML schema](/schemas/text-schema/) for more info.
Nomai text contains a root `<NomaiObject>` node, followed by `<TextBlock>` nodes and optionally a `<ShipLogConditions>` node.
Nomai text is made up of `TextBlock`s. Each text block has an `ID` which must be unique (you can just number them for simplicity). After the first defined text block, each must have a `ParentID`. For scrolls and regular wall text, the text block only gets revealed after its parent block. Multiple text blocks can have the same parent, allowing for branching paths. In recorders and computers, each text block must procede in order (the second parented to the first, the third to the second, etc). In cairns, there is only one text block.
To unlock ship logs after reading each text block, add a `<ShipLogConditions>` node. This can contains multiple `<RevealFact>` nodes, each one defining a `<FactID>`, `<Condition>`. The ship log conditions node can either have `<LocationA/>` or `<LocationB/>`, which means the logs will unlock only if you are at that location. The `<Condition>` lists the TextBlock ids which must be read to reveal the fact as a comma delimited list (e.g., `<Condition>1,2,4</Condition>`)..
### Json
In your planet config, you must define where the Nomai text is positioned. See [the translator text json schema](/schemas/body-schema/defs/translatortextinfo/) for more info.
You can input a `seed` for a wall of text which will randomly generate the position of each arc. To test out different combinations, just keep incrementing the number and then hit "Reload Configs" from the pause menu with debug mode on. This seed ensures the same positioning each time the mod is played. Alternatively, you can use `arcInfo` to set the position and rotation of all text arcs, as well as determining their types (adult, teenager, child, or Stranger). The various age stages make the text look messier, while Stranger allows you to make a translatable version of the DLC text.

View File

@ -14,6 +14,7 @@ Before you release anything, you'll want to make sure:
- 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.
- You have included any caches New Horizons has made (i.e., slide reel caches). Since these are made in the install location of the mod you will have to manually copy them into the mod repo and ensure they stay up to date. While these files are not required, they ensure that your players will have faster loading times and reduced memory usage on their first loop (after which the caches will generate for them locally).
## Releasing