diff --git a/NewHorizons/Assets/textures/MENU_OuterWildsLogo_d.png b/NewHorizons/Assets/textures/MENU_OuterWildsLogo_d.png new file mode 100644 index 00000000..c960405b Binary files /dev/null and b/NewHorizons/Assets/textures/MENU_OuterWildsLogo_d.png differ diff --git a/NewHorizons/Builder/Body/ProxyBuilder.cs b/NewHorizons/Builder/Body/ProxyBuilder.cs index 3520008d..0b4cafc3 100644 --- a/NewHorizons/Builder/Body/ProxyBuilder.cs +++ b/NewHorizons/Builder/Body/ProxyBuilder.cs @@ -210,6 +210,7 @@ namespace NewHorizons.Builder.Body { foreach (var detailInfo in body.Config.Props.proxyDetails) { + // Thought about switching these to SimplifiedDetailInfo but we use AlignRadial with these so we can't DetailBuilder.Make(proxy, null, body.Mod, detailInfo); } } diff --git a/NewHorizons/Builder/StarSystem/SkyboxBuilder.cs b/NewHorizons/Builder/StarSystem/SkyboxBuilder.cs index 5d4b8548..d531c977 100644 --- a/NewHorizons/Builder/StarSystem/SkyboxBuilder.cs +++ b/NewHorizons/Builder/StarSystem/SkyboxBuilder.cs @@ -1,4 +1,4 @@ -using NewHorizons.External.Configs; +using NewHorizons.External.Modules; using NewHorizons.Utility; using NewHorizons.Utility.Files; using NewHorizons.Utility.OWML; @@ -13,13 +13,13 @@ namespace NewHorizons.Builder.StarSystem { private static readonly Shader _unlitShader = Shader.Find("Unlit/Texture"); - public static void Make(StarSystemConfig.SkyboxModule module, IModBehaviour mod) + public static void Make(SkyboxModule module, IModBehaviour mod) { NHLogger.Log("Building Skybox"); BuildSkySphere(module, mod); } - public static GameObject BuildSkySphere(StarSystemConfig.SkyboxModule module, IModBehaviour mod) + public static GameObject BuildSkySphere(SkyboxModule module, IModBehaviour mod) { var skybox = SearchUtilities.Find("Skybox"); diff --git a/NewHorizons/External/Configs/StarSystemConfig.cs b/NewHorizons/External/Configs/StarSystemConfig.cs index 257d98e3..d2cab099 100644 --- a/NewHorizons/External/Configs/StarSystemConfig.cs +++ b/NewHorizons/External/Configs/StarSystemConfig.cs @@ -181,51 +181,6 @@ namespace NewHorizons.External.Configs public int[] z; } - [JsonObject] - public class SkyboxModule - { - - /// - /// Whether to destroy the star field around the player - /// - public bool destroyStarField; - - /// - /// Whether to use a cube for the skybox instead of a smooth sphere - /// - public bool useCube; - - /// - /// Relative filepath to the texture to use for the skybox's positive X direction - /// - public string rightPath; - - /// - /// Relative filepath to the texture to use for the skybox's negative X direction - /// - public string leftPath; - - /// - /// Relative filepath to the texture to use for the skybox's positive Y direction - /// - public string topPath; - - /// - /// Relative filepath to the texture to use for the skybox's negative Y direction - /// - public string bottomPath; - - /// - /// Relative filepath to the texture to use for the skybox's positive Z direction - /// - public string frontPath; - - /// - /// Relative filepath to the texture to use for the skybox's negative Z direction - /// - public string backPath; - } - [JsonObject] public class GlobalMusicModule { diff --git a/NewHorizons/External/Configs/TitleScreenConfig.cs b/NewHorizons/External/Configs/TitleScreenConfig.cs new file mode 100644 index 00000000..e5a73046 --- /dev/null +++ b/NewHorizons/External/Configs/TitleScreenConfig.cs @@ -0,0 +1,119 @@ +using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Props; +using NewHorizons.External.SerializableData; +using NewHorizons.Handlers; +using Newtonsoft.Json; + +namespace NewHorizons.External.Configs +{ + [JsonObject] + public class TitleScreenConfig + { + /// + /// Create title screens + /// + public TitleScreenInfo[] titleScreens = new TitleScreenInfo[0]; + } + + [JsonObject] + public class TitleScreenInfo + { + /// + /// Colour of the text on the main menu + /// + public MColor menuTextTint; + + /// + /// Ship log fact required for this title screen to appear. + /// + public string factRequiredForTitle; + + /// + /// Persistent condition required for this title screen to appear. + /// + public string persistentConditionRequiredForTitle; + + /// + /// If set to true, NH generated planets will not show on the title screen. If false, this title screen has the same chance as other NH planet title screens to show. + /// + public bool disableNHPlanets = true; + + /// + /// If set to true, this custom title screen will merge with all other custom title screens with shareTitleScreen set to true. If false, NH will randomly select between this and other valid title screens that are loaded. + /// + public bool shareTitleScreen = true; + + /// + /// Customize the skybox for this title screen + /// + public SkyboxModule Skybox; + + /// + /// The music audio that will play on the title screen. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string music; + + /// + /// The ambience audio that will play on the title screen. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list. + /// + public string ambience; + + /// + /// Edit properties of the background + /// + public BackgroundModule Background; + + /// + /// Edit properties of the main menu planet + /// + public MenuPlanetModule MenuPlanet; + + [JsonObject] + public class BackgroundModule + { + /// + /// Changes the speed the background rotates (and by extension the main menu planet). This is in degrees per second. + /// + public float rotationSpeed = 1; + + /// + /// Disables the renderers of objects at the provided paths + /// + public string[] removeChildren; + + /// + /// A list of DetailInfos to populate the background with. + /// + public SimplifiedDetailInfo[] details; + } + + [JsonObject] + public class MenuPlanetModule + { + /// + /// Disables the renderers of the main menu planet and all objects on it (this is to improve compatibility with other mods that don't use the NH title screen json). + /// + public bool destroyMenuPlanet = false; + + /// + /// Disables the renderers of objects at the provided paths + /// + public string[] removeChildren; + + /// + /// A list of DetailInfos to populate the main menu planet with. + /// + public SimplifiedDetailInfo[] details; + + /// + /// Changes the speed the main menu planet. This is in degrees per second. + /// + public float rotationSpeed = 2; + } + + /// + /// Extra data that may be used by extension mods + /// + public object extras; + } +} diff --git a/NewHorizons/External/Modules/Props/DetailInfo.cs b/NewHorizons/External/Modules/Props/DetailInfo.cs index c16ff6b2..f7399e2b 100644 --- a/NewHorizons/External/Modules/Props/DetailInfo.cs +++ b/NewHorizons/External/Modules/Props/DetailInfo.cs @@ -6,12 +6,15 @@ using System.ComponentModel; namespace NewHorizons.External.Modules.Props { + /// + /// A lesser form of DetailInfo used for the title screen since that supports fewer features + /// [JsonObject] - public class DetailInfo : GeneralPropInfo + public class SimplifiedDetailInfo : GeneralPropInfo { - public DetailInfo() { } + public SimplifiedDetailInfo() { } - public DetailInfo(GeneralPointPropInfo info) + public SimplifiedDetailInfo(GeneralPointPropInfo info) { JsonConvert.PopulateObject(JsonConvert.SerializeObject(info), this); } @@ -47,6 +50,23 @@ namespace NewHorizons.External.Modules.Props /// Scale each axis of the prop. Overrides `scale`. /// public MVector3 stretch; + } + + [JsonObject] + public class DetailInfo : SimplifiedDetailInfo + { + public DetailInfo() { } + + public DetailInfo(GeneralPointPropInfo info) + { + JsonConvert.PopulateObject(JsonConvert.SerializeObject(info), this); + } + + public DetailInfo(SimplifiedDetailInfo info) + { + keepLoaded = true; + JsonConvert.PopulateObject(JsonConvert.SerializeObject(info), this); + } [Obsolete("Use QuantumDetailInfo")] public string quantumGroupID; diff --git a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs index 10106fb8..7f4efb24 100644 --- a/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs +++ b/NewHorizons/External/Modules/Props/EchoesOfTheEye/DreamCandleInfo.cs @@ -14,7 +14,7 @@ namespace NewHorizons.External.Modules.Props.EchoesOfTheEye /// /// The type of dream candle this is. /// - [DefaultValue(DreamCandleType.Ground)] public DreamCandleType type = DreamCandleType.Ground; + [DefaultValue("ground")] public DreamCandleType type = DreamCandleType.Ground; /// /// Whether the candle should start lit or extinguished. diff --git a/NewHorizons/External/Modules/SkyboxModule.cs b/NewHorizons/External/Modules/SkyboxModule.cs new file mode 100644 index 00000000..b9f27f13 --- /dev/null +++ b/NewHorizons/External/Modules/SkyboxModule.cs @@ -0,0 +1,48 @@ +using Newtonsoft.Json; + +namespace NewHorizons.External.Modules +{ + [JsonObject] + public class SkyboxModule + { + /// + /// Whether to destroy the star field around the player + /// + public bool destroyStarField; + + /// + /// Whether to use a cube for the skybox instead of a smooth sphere + /// + public bool useCube; + + /// + /// Relative filepath to the texture to use for the skybox's positive X direction + /// + public string rightPath; + + /// + /// Relative filepath to the texture to use for the skybox's negative X direction + /// + public string leftPath; + + /// + /// Relative filepath to the texture to use for the skybox's positive Y direction + /// + public string topPath; + + /// + /// Relative filepath to the texture to use for the skybox's negative Y direction + /// + public string bottomPath; + + /// + /// Relative filepath to the texture to use for the skybox's positive Z direction + /// + public string frontPath; + + /// + /// Relative filepath to the texture to use for the skybox's negative Z direction + /// + public string backPath; + } +} diff --git a/NewHorizons/Handlers/AudioTypeHandler.cs b/NewHorizons/Handlers/AudioTypeHandler.cs index 9d25bb7e..ef6e71f3 100644 --- a/NewHorizons/Handlers/AudioTypeHandler.cs +++ b/NewHorizons/Handlers/AudioTypeHandler.cs @@ -15,14 +15,15 @@ namespace NewHorizons.Handlers { private static Dictionary _customAudioTypes; private static List _audioEntries; + private static bool _postInitialized = false; public static void Init() { _customAudioTypes = new Dictionary(); _audioEntries = new List(); + _postInitialized = false; - Delay.RunWhen( - () => Locator.GetAudioManager()?._libraryAsset != null, + Delay.RunWhen(() => Locator.GetAudioManager()?._libraryAsset != null && Locator.GetAudioManager()?._audioLibraryDict != null, PostInit ); } @@ -30,7 +31,12 @@ namespace NewHorizons.Handlers private static void PostInit() { NHLogger.LogVerbose($"Adding all custom AudioTypes to the library"); + _postInitialized = true; + ModifyAudioLibrary(); + } + private static void ModifyAudioLibrary() + { var library = Locator.GetAudioManager()._libraryAsset; var audioEntries = library.audioEntries; // store previous array library.audioEntries = library.audioEntries.Concat(_audioEntries).ToArray(); // concat custom entries @@ -88,6 +94,8 @@ namespace NewHorizons.Handlers _audioEntries.Add(new AudioLibrary.AudioEntry(audioType, audioClips)); _customAudioTypes.Add(id, audioType); + if (_postInitialized) ModifyAudioLibrary(); + return audioType; } } diff --git a/NewHorizons/Handlers/TitleSceneHandler.cs b/NewHorizons/Handlers/TitleSceneHandler.cs index c0c3924c..7183675f 100644 --- a/NewHorizons/Handlers/TitleSceneHandler.cs +++ b/NewHorizons/Handlers/TitleSceneHandler.cs @@ -1,16 +1,125 @@ using NewHorizons.Builder.Body; +using NewHorizons.Builder.Props; +using NewHorizons.Builder.StarSystem; using NewHorizons.External; +using NewHorizons.External.Configs; using NewHorizons.External.Modules; +using NewHorizons.External.Modules.Props; +using NewHorizons.Handlers.TitleScreen; using NewHorizons.Utility; using NewHorizons.Utility.OWML; +using OWML.Common; +using System; using System.Collections.Generic; using System.Linq; using UnityEngine; +using UnityEngine.SceneManagement; +using Color = UnityEngine.Color; namespace NewHorizons.Handlers { public static class TitleSceneHandler { + internal static Dictionary TitleScreenBuilders = new(); + internal static NewHorizonsBody[] eligibleBodies => Main.BodyDict.Values.ToList().SelectMany(x => x).ToList() + .Where(b => (b.Config.HeightMap != null || b.Config.Atmosphere?.clouds != null) && b.Config.Star == null && b.Config.canShowOnTitle).ToArray(); + internal static int eligibleCount => eligibleBodies.Count(); + internal static bool reloaded = false; + internal static bool reopenProfile = false; + + public static void Init() + { + var scene = SearchUtilities.Find("Scene"); + var background = SearchUtilities.Find("Scene/Background"); + var planetPivot = SearchUtilities.Find("Scene/Background/PlanetPivot"); + + // Add fake sectors for ocean water component support + scene.AddComponent(); + background.AddComponent(); + planetPivot.AddComponent(); + + // parent ambient light and campfire to the root (idk why they aren't parented in the first place mobius) + var planetRoot = SearchUtilities.Find("Scene/Background/PlanetPivot/PlanetRoot"); + var campfire = SearchUtilities.Find("Scene/Background/PlanetPivot/Prefab_HEA_Campfire"); + campfire.transform.SetParent(planetRoot.transform, true); + var ambientLight = SearchUtilities.Find("Scene/Background/PlanetPivot/AmbientLight_CaveTwin"); + ambientLight.transform.SetParent(planetRoot.transform, true); + + InitSubtitles(); + TitleScreenColourHandler.ResetColour(); // reset color at the start + AudioTypeHandler.Init(); // init audio for custom music + + // Load player data for fact and persistent condition checking + var profileManager = StandaloneProfileManager.SharedInstance; + profileManager.OnProfileSignInComplete += OnProfileSignInComplete; + profileManager.PreInitialize(); + profileManager.Initialize(); + if (profileManager.currentProfile != null) // check if there is even a profile made yet + PlayerData.Init(profileManager.currentProfileGameSave, + profileManager.currentProfileGameSettings, + profileManager.currentProfileGraphicsSettings, + profileManager.currentProfileInputJSON); + + // Grab configs and handlers and merge them into one list + var validBuilders = TitleScreenBuilders.Values + .Where(list => list.IsValid) + .Select(list => list.GetRelevantBuilder()).ToList(); + + var hasNHPlanets = eligibleCount != 0; + + // Get random index for the main builder + var index = UnityEngine.Random.Range(0, validBuilders.Count()); + var randomBuilder = validBuilders.ElementAtOrDefault(index); + if (randomBuilder != null) + { + validBuilders.RemoveAt(index); + + // display nh planets if not disabled + if (!randomBuilder.DisableNHPlanets) + { + DisplayBodiesOnTitleScreen(); + } + + // if it can share build extras + if (randomBuilder.CanShare) + { + // only build the ones that also can share and have the same value for disabling nh planet (if there is any nh planets) + foreach (var builder in validBuilders.Where(builder => builder.CanShare && (hasNHPlanets ? builder.DisableNHPlanets == randomBuilder.DisableNHPlanets : true))) + { + builder.Build(); + } + } + + // Build main one last so it overrides the extras + randomBuilder.Build(); + } + // default to displaying nh planets if no title screen builders + else + { + DisplayBodiesOnTitleScreen(); + } + + try + { + Main.Instance.OnAllTitleScreensLoaded?.Invoke(); + } + catch (Exception e) + { + NHLogger.LogError($"Error in event handler for OnAllTitleScreensLoaded: {e}"); + } + } + + private static void OnProfileSignInComplete(ProfileManagerSignInResult result) + { + NHLogger.LogVerbose($"OnProfileSignInComplete {result}: {StandaloneProfileManager.SharedInstance.currentProfile.profileName}"); + reloaded = true; + reopenProfile = true; + + // Taken and modified from SubmitActionLoadScene.ConfirmSubmit + LoadManager.LoadScene(OWScene.TitleScreen); + Locator.GetMenuInputModule().DisableInputs(); + } + public static void InitSubtitles() { GameObject subtitleContainer = SearchUtilities.Find("TitleMenu/TitleCanvas/TitleLayoutGroup/Logo_EchoesOfTheEye"); @@ -25,73 +134,184 @@ namespace NewHorizons.Handlers subtitleContainer.AddComponent(); } - public static void DisplayBodyOnTitleScreen(List bodies) + public static void BuildConfig(IModBehaviour mod, TitleScreenInfo config) { - // Try loading one planet why not - // var eligible = BodyDict.Values.ToList().SelectMany(x => x).ToList().Where(b => (b.Config.HeightMap != null || b.Config.Atmosphere?.Cloud != null) && b.Config.Star == null).ToArray(); - var eligible = bodies.Where(b => (b.Config.HeightMap != null || b.Config.Atmosphere?.clouds != null) && b.Config.Star == null && b.Config.canShowOnTitle).ToArray(); - var eligibleCount = eligible.Count(); - if (eligibleCount == 0) return; - - var selectionCount = Mathf.Min(eligibleCount, 3); - var indices = RandomUtility.GetUniqueRandomArray(0, eligible.Count(), selectionCount); - - NHLogger.LogVerbose($"Displaying {selectionCount} bodies on the title screen"); - - var planetSizes = new List<(GameObject planet, float size)>(); - - var bodyInfo = LoadTitleScreenBody(eligible[indices[0]]); - bodyInfo.planet.transform.localRotation = Quaternion.Euler(15, 0, 0); - planetSizes.Add(bodyInfo); - - if (selectionCount > 1) + if (config.menuTextTint != null) { - bodyInfo.planet.transform.localPosition = new Vector3(0, -15, 0); - bodyInfo.planet.transform.localRotation = Quaternion.Euler(10f, 0f, 0f); - - var bodyInfo2 = LoadTitleScreenBody(eligible[indices[1]]); - bodyInfo2.planet.transform.localPosition = new Vector3(7, 30, 0); - bodyInfo2.planet.transform.localRotation = Quaternion.Euler(10f, 0f, 0f); - planetSizes.Add(bodyInfo2); + TitleScreenColourHandler.SetColour(config.menuTextTint.ToColor()); } - if (selectionCount > 2) + if (config.Skybox?.destroyStarField ?? false) { - var bodyInfo3 = LoadTitleScreenBody(eligible[indices[2]]); - bodyInfo3.planet.transform.localPosition = new Vector3(-5, 10, 0); - bodyInfo3.planet.transform.localRotation = Quaternion.Euler(10f, 0f, 0f); - planetSizes.Add(bodyInfo3); + UnityEngine.Object.Destroy(SearchUtilities.Find("Skybox/Starfield")); } - SearchUtilities.Find("Scene/Background/PlanetPivot/Prefab_HEA_Campfire").SetActive(false); - SearchUtilities.Find("Scene/Background/PlanetPivot/PlanetRoot").SetActive(false); - - var lightGO = new GameObject("Light"); - lightGO.transform.parent = SearchUtilities.Find("Scene/Background").transform; - lightGO.transform.localPosition = new Vector3(-47.9203f, 145.7596f, 43.1802f); - lightGO.transform.localRotation = Quaternion.Euler(13.1412f, 122.8785f, 169.4302f); - var light = lightGO.AddComponent(); - light.type = LightType.Directional; - light.color = Color.white; - light.range = float.PositiveInfinity; - light.intensity = 0.8f; - - // Resize planets relative to each other - // If there are multiple planets shrink them down to 30% of the size - var maxSize = planetSizes.Select(x => x.size).Max(); - var minSize = planetSizes.Select(x => x.size).Min(); - var multiplePlanets = planetSizes.Count > 1; - foreach (var (planet, size) in planetSizes) + if (config.Skybox?.rightPath != null || + config.Skybox?.leftPath != null || + config.Skybox?.topPath != null || + config.Skybox?.bottomPath != null || + config.Skybox?.frontPath != null || + config.Skybox?.bottomPath != null) { - var adjustedSize = size / maxSize; - // If some planets would be too small we'll do a funny thing - if (minSize / maxSize < 0.3f) + SkyboxBuilder.Make(config.Skybox, mod); + } + + if (!string.IsNullOrEmpty(config.music)) + { + var musicSource = SearchUtilities.Find("Scene/AudioSource_Music").GetComponent(); + var audioType = AudioTypeHandler.GetAudioType(config.music, mod); + Delay.FireOnNextUpdate(() => musicSource.AssignAudioLibraryClip(audioType)); + } + + if (!string.IsNullOrEmpty(config.ambience)) + { + var ambienceSource = SearchUtilities.Find("Scene/AudioSource_Ambience").GetComponent(); + var audioType = AudioTypeHandler.GetAudioType(config.ambience, mod); + Delay.FireOnNextUpdate(() => ambienceSource.AssignAudioLibraryClip(audioType)); + } + + var background = SearchUtilities.Find("Scene/Background"); + var menuPlanet = SearchUtilities.Find("Scene/Background/PlanetPivot"); + + if (config.Background != null) + { + if (config.Background.removeChildren != null) { - var t = Mathf.InverseLerp(minSize, maxSize, size); - adjustedSize = Mathf.Lerp(0.3f, 1f, t * t); + RemoveChildren(background, config.Background.removeChildren); } - planet.transform.localScale *= adjustedSize * (multiplePlanets ? 0.3f : 1f); + if (config.Background.details != null) + { + foreach (var simplifiedDetail in config.Background.details) + { + DetailBuilder.Make(background, background.GetComponentInParent(), mod, new DetailInfo(simplifiedDetail)); + } + } + + var rotator = background.GetComponent(); + rotator._degreesPerSecond = config.Background.rotationSpeed; + } + + if (config.MenuPlanet != null) + { + if (config.MenuPlanet.removeChildren != null) + { + RemoveChildren(menuPlanet, config.MenuPlanet.removeChildren); + } + + if (config.MenuPlanet.details != null) + { + foreach (var simplifiedDetail in config.MenuPlanet.details) + { + DetailBuilder.Make(menuPlanet, menuPlanet.GetComponentInParent(), mod, new DetailInfo(simplifiedDetail)); + } + } + + var rotator = menuPlanet.GetComponent(); + rotator._localAxis = Vector3.up; // fix axis (because there is no reason for it to be negative when degrees were also negative) + rotator._degreesPerSecond = config.MenuPlanet.rotationSpeed; + + if (config.MenuPlanet.destroyMenuPlanet) + { + SearchUtilities.Find("Scene/Background/PlanetPivot/PlanetRoot").SetActive(false); + } + } + } + + private static void RemoveChildren(GameObject go, string[] paths) + { + foreach (var childPath in paths) + { + var flag = true; + foreach (var childObj in go.transform.FindAll(childPath)) + { + flag = false; + // idk why we wait here but we do + Delay.FireInNUpdates(() => + { + if (childObj != null && childObj.gameObject != null) + { + childObj.gameObject.SetActive(false); + } + }, 2); + } + + if (flag) NHLogger.LogWarning($"Couldn't find \"{childPath}\"."); + } + } + + public static void DisplayBodiesOnTitleScreen() + { + try + { + // Try loading one planet why not + var eligible = eligibleBodies; + var eligibleCount = eligible.Count(); + if (eligibleCount == 0) return; + + var selectionCount = Mathf.Min(eligibleCount, 3); + var indices = RandomUtility.GetUniqueRandomArray(0, eligible.Count(), selectionCount); + + NHLogger.LogVerbose($"Displaying {selectionCount} bodies on the title screen"); + + var planetSizes = new List<(GameObject planet, float size)>(); + + var bodyInfo = LoadTitleScreenBody(eligible[indices[0]]); + bodyInfo.planet.transform.localRotation = Quaternion.Euler(15, 0, 0); + planetSizes.Add(bodyInfo); + + if (selectionCount > 1) + { + bodyInfo.planet.transform.localPosition = new Vector3(0, -15, 0); + bodyInfo.planet.transform.localRotation = Quaternion.Euler(10f, 0f, 0f); + + var bodyInfo2 = LoadTitleScreenBody(eligible[indices[1]]); + bodyInfo2.planet.transform.localPosition = new Vector3(7, 30, 0); + bodyInfo2.planet.transform.localRotation = Quaternion.Euler(10f, 0f, 0f); + planetSizes.Add(bodyInfo2); + } + + if (selectionCount > 2) + { + var bodyInfo3 = LoadTitleScreenBody(eligible[indices[2]]); + bodyInfo3.planet.transform.localPosition = new Vector3(-5, 10, 0); + bodyInfo3.planet.transform.localRotation = Quaternion.Euler(10f, 0f, 0f); + planetSizes.Add(bodyInfo3); + } + + SearchUtilities.Find("Scene/Background/PlanetPivot/PlanetRoot").SetActive(false); + + var lightGO = new GameObject("Light"); + lightGO.transform.parent = SearchUtilities.Find("Scene/Background").transform; + lightGO.transform.localPosition = new Vector3(-47.9203f, 145.7596f, 43.1802f); + lightGO.transform.localRotation = Quaternion.Euler(13.1412f, 122.8785f, 169.4302f); + var light = lightGO.AddComponent(); + light.type = LightType.Directional; + light.color = Color.white; + light.range = float.PositiveInfinity; + light.intensity = 0.8f; + + // Resize planets relative to each other + // If there are multiple planets shrink them down to 30% of the size + var maxSize = planetSizes.Select(x => x.size).Max(); + var minSize = planetSizes.Select(x => x.size).Min(); + var multiplePlanets = planetSizes.Count > 1; + foreach (var (planet, size) in planetSizes) + { + var adjustedSize = size / maxSize; + // If some planets would be too small we'll do a funny thing + if (minSize / maxSize < 0.3f) + { + var t = Mathf.InverseLerp(minSize, maxSize, size); + adjustedSize = Mathf.Lerp(0.3f, 1f, t * t); + } + + planet.transform.localScale *= adjustedSize * (multiplePlanets ? 0.3f : 1f); + } + } + catch (Exception e) + { + NHLogger.LogError($"Failed to make title screen bodies: {e}"); } } @@ -153,11 +373,11 @@ namespace NewHorizons.Handlers } } - var pivot = Object.Instantiate(SearchUtilities.Find("Scene/Background/PlanetPivot"), SearchUtilities.Find("Scene/Background").transform); + var pivot = UnityEngine.Object.Instantiate(SearchUtilities.Find("Scene/Background/PlanetPivot"), SearchUtilities.Find("Scene/Background").transform); pivot.GetComponent()._degreesPerSecond = 10f; foreach (Transform child in pivot.transform) { - Object.Destroy(child.gameObject); + UnityEngine.Object.Destroy(child.gameObject); } pivot.name = "Pivot"; @@ -222,5 +442,195 @@ namespace NewHorizons.Handlers return meshRenderer; } + + internal static void RegisterBuilder(IModBehaviour mod, ITitleScreenBuilder builder) + { + if (!TitleScreenBuilders.ContainsKey(mod)) + TitleScreenBuilders.Add(mod, new TitleScreenBuilderList()); + + TitleScreenBuilders[mod].Add(builder); + } + + public static void RegisterBuilder(IModBehaviour mod, TitleScreenInfo config) + => RegisterBuilder(mod, + new TitleScreenConfigBuilder(mod, config)); + + public static void RegisterBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets, bool shareTitleScreen, string persistentConditionRequired, string factRequired) + => RegisterBuilder(mod, + new TitleScreenBuilder(mod, builder, + disableNHPlanets, shareTitleScreen, + persistentConditionRequired, factRequired)); + + internal static void ResetConfigs() + { + foreach (var builderList in TitleScreenBuilders.Values) + { + builderList.list.RemoveAll(builder => builder is TitleScreenConfigBuilder); + } + } + + internal class TitleScreenBuilderList + { + public List list = new List(); + + public void Add(ITitleScreenBuilder builder) + { + list.Add(builder); + builder.Index = list.IndexOf(builder); + } + + public bool IsValid => GetRelevantBuilder() != null; + + public ITitleScreenBuilder GetRelevantBuilder() + { + return list.LastOrDefault(builder => builder.KnowsFact() && builder.HasCondition()); + } + } + + internal class TitleScreenBuilder : ITitleScreenBuilder + { + public IModBehaviour mod; + public Action builder; + public bool disableNHPlanets; + public bool shareTitleScreen; + public string persistentConditionRequired; + public string factRequired; + + public TitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets, bool shareTitleScreen, string persistentConditionRequired, string factRequired) + { + this.mod = mod; + this.builder = builder; + this.disableNHPlanets = disableNHPlanets; + this.shareTitleScreen = shareTitleScreen; + this.persistentConditionRequired = persistentConditionRequired; + this.factRequired = factRequired; + } + + public void Build() + { + NHLogger.LogVerbose($"Building handler {mod.ModHelper.Manifest.UniqueName} #{index}"); + try + { + builder.Invoke(SearchUtilities.Find("Scene")); + } + catch (Exception e) + { + NHLogger.LogError($"Error while building title screen handler {mod.ModHelper.Manifest.UniqueName} #{index}: {e}"); + } + + try + { + Main.Instance.OnTitleScreenLoaded?.Invoke(mod.ModHelper.Manifest.UniqueName, index); + } + catch (Exception e) + { + NHLogger.LogError($"Error in event handler for OnTitleScreenLoaded on title screen {mod.ModHelper.Manifest.UniqueName} #{index}: {e}"); + } + } + + public IModBehaviour Mod => mod; + + public bool DisableNHPlanets => disableNHPlanets; + + public bool CanShare => shareTitleScreen; + + public bool KnowsFact() => string.IsNullOrEmpty(factRequired) || StandaloneProfileManager.SharedInstance.currentProfile != null && ShipLogHandler.KnowsFact(factRequired); + + public bool HasCondition() => string.IsNullOrEmpty(persistentConditionRequired) || StandaloneProfileManager.SharedInstance.currentProfile != null && PlayerData.GetPersistentCondition(persistentConditionRequired); + + private int index = -1; + public int Index { get => index; set => index = value; } + + public override string ToString() => Mod.ModHelper.Manifest.UniqueName + " #" + Index; + } + + internal class TitleScreenConfigBuilder : ITitleScreenBuilder + { + public IModBehaviour mod; + public TitleScreenInfo config; + + public TitleScreenConfigBuilder(IModBehaviour mod, TitleScreenInfo config) + { + this.mod = mod; + this.config = config; + } + + public void Build() + { + NHLogger.LogVerbose($"Building config {mod.ModHelper.Manifest.UniqueName} #{index}"); + try + { + BuildConfig(mod, config); + } + catch (Exception e) + { + NHLogger.LogError($"Error while building title screen config {mod.ModHelper.Manifest.UniqueName} #{index}: {e}"); + } + + try + { + Main.Instance.OnTitleScreenLoaded?.Invoke(mod.ModHelper.Manifest.UniqueName, index); + } + catch (Exception e) + { + NHLogger.LogError($"Error in event handler for OnTitleScreenLoaded on title screen {mod.ModHelper.Manifest.UniqueName} #{index}: {e}"); + } + } + + public IModBehaviour Mod => mod; + + public bool DisableNHPlanets => config.disableNHPlanets; + + public bool CanShare => config.shareTitleScreen; + + public bool KnowsFact() => string.IsNullOrEmpty(config.factRequiredForTitle) || StandaloneProfileManager.SharedInstance.currentProfile != null && ShipLogHandler.KnowsFact(config.factRequiredForTitle); + + public bool HasCondition() => string.IsNullOrEmpty(config.persistentConditionRequiredForTitle) || StandaloneProfileManager.SharedInstance.currentProfile != null && PlayerData.GetPersistentCondition(config.persistentConditionRequiredForTitle); + + private int index = -1; + public int Index { get => index; set => index = value; } + + public override string ToString() => Mod.ModHelper.Manifest.UniqueName + " #" + Index; + } + + internal interface ITitleScreenBuilder + { + IModBehaviour Mod { get; } + + bool DisableNHPlanets { get; } + + bool CanShare { get; } + + void Build(); + bool KnowsFact(); + bool HasCondition(); + + int Index { get; set; } + } + + /// + /// For water and etc (they require a sector or else they will get deleted by detail builder) + /// + private class FakeSector : Sector + { + public override void Awake() + { + _triggerRoot = gameObject; + _subsectors = new List(); + _occupantMask = DynamicOccupant.Player; + var parentSector = GetComponentsInParent().FirstOrDefault(parentSector => parentSector != this); + if (parentSector != null) + { + _parentSector = parentSector; + _parentSector.AddSubsector(this); + } + SectorManager.RegisterSector(this); + } + + public void Start() + { + OnSectorOccupantsUpdated.Invoke(); + } + } } } diff --git a/NewHorizons/Handlers/TitleScreen/TitleScreenColourHandler.cs b/NewHorizons/Handlers/TitleScreen/TitleScreenColourHandler.cs new file mode 100644 index 00000000..ffb30f3f --- /dev/null +++ b/NewHorizons/Handlers/TitleScreen/TitleScreenColourHandler.cs @@ -0,0 +1,79 @@ +using HarmonyLib; +using NewHorizons.Utility; +using NewHorizons.Utility.Files; +using NewHorizons.Utility.OWML; +using System.Linq; +using UnityEngine; +using UnityEngine.SceneManagement; +using UnityEngine.UI; + +namespace NewHorizons.Handlers.TitleScreen +{ + [HarmonyPatch] + public class TitleScreenColourHandler + { + public static void SetColour(Color colour) + { + NHLogger.LogVerbose("Setting title screen colour to " + colour.ToString()); + colour.a = 1; + var buttons = GameObject.FindObjectOfType()._mainMenu.GetComponentsInChildren(); + var footer = GameObject.Find("TitleMenu/TitleCanvas/FooterBlock").GetComponentsInChildren(); + foreach (var button in buttons.Concat(footer)) + { + button.color = colour; + } + _mainMenuColour = colour; + var logo = ImageUtilities.TintImage(ImageUtilities.GetTexture(Main.Instance, "Assets\\textures\\MENU_OuterWildsLogo_d.png"), (Color)_mainMenuColour); + var animRenderer = GameObject.FindObjectOfType(); + var colouredLogoMaterial = GameObject.Instantiate(animRenderer._logoMaterial).Rename("MENU_OuterWildsLogoANIM_mat_Coloured"); + colouredLogoMaterial.mainTexture = logo; + animRenderer._logoMaterial = colouredLogoMaterial; + animRenderer.Awake(); + } + + public static void ResetColour() + { + _mainMenuColour = null; + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(UIStyleApplier), nameof(UIStyleApplier.ChangeColors))] + public static bool UIStyleApplier_ChangeColors(UIStyleApplier __instance, UIElementState state) + { + if (SceneManager.GetActiveScene().name == "TitleScreen" && _mainMenuColour is Color colour && __instance.transform.parent.name == "MainMenuLayoutGroup") + { + // Wyrm didn't say to account for any of these states I win! + switch (state) + { + case UIElementState.INTERMEDIATELY_HIGHLIGHTED: + case UIElementState.HIGHLIGHTED: + case UIElementState.PRESSED: + case UIElementState.ROLLOVER_HIGHLIGHT: + Color.RGBToHSV(colour, out var h, out var s, out var v); + colour = Color.HSVToRGB(h, s * 0.2f, v * 1.2f); + break; + case UIElementState.DISABLED: + return true; + default: + break; + } + + for (int i = 0; i < __instance._foregroundGraphics.Length; i++) + { + __instance._foregroundGraphics[i].color = colour; + } + for (int j = 0; j < __instance._backgroundGraphics.Length; j++) + { + __instance._backgroundGraphics[j].color = colour; + } + return false; + } + else + { + return true; + } + } + + private static Color? _mainMenuColour; + } +} diff --git a/NewHorizons/INewHorizons.cs b/NewHorizons/INewHorizons.cs index 350d0e2b..07512ad6 100644 --- a/NewHorizons/INewHorizons.cs +++ b/NewHorizons/INewHorizons.cs @@ -73,6 +73,17 @@ namespace NewHorizons /// Gives the name of the planet that was just loaded. /// UnityEvent GetBodyLoadedEvent(); + + /// + /// An event invoked when NH has finished building a title screen. + /// Gives the unique name of the mod the title screen builder was from and the index for when you have multiple title screens. + /// + UnityEvent GetTitleScreenLoadedEvent(); + + /// + /// An event invoked when NH has finished building the title screen. + /// + UnityEvent GetAllTitleScreensLoadedEvent(); #endregion #region Querying configs @@ -96,6 +107,16 @@ namespace NewHorizons /// T QuerySystem(string path); + /// + /// Uses JSONPath to query a title screen config + /// + object QueryTitleScreen(Type outType, IModBehaviour mod, string path); + + /// + /// Uses JSONPath to query a title screen config + /// + T QueryTitleScreen(IModBehaviour mod, string path); + /// /// Register your own builder that will act on the given GameObject by reading the json string of its "extras" module /// @@ -222,5 +243,17 @@ namespace NewHorizons /// /// void SetNextSpawnID(string id); + + /// + /// Registers a builder for the main menu. + /// Call this once before the main menu finishes loading + /// + /// + /// Builder to run when this title screen is selected. The GameObject passed through it is the main scene object containing both the background and menu planet. + /// If set to true, NH generated planets will not show on the title screen. If false, this title screen has the same chance as other NH planet title screens to show. + /// If set to true, this custom title screen will merge with all other custom title screens with shareTitleScreen set to true. If false, NH will randomly select between this and other valid title screens that are loaded. + /// Persistent condition required for this title screen to appear. + /// Ship log fact required for this title screen to appear. + void RegisterTitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets = true, bool shareTitleScreen = false, string persistentConditionRequired = null, string factRequired = null); } } diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 42a6a9ca..01e1f089 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -56,6 +56,7 @@ namespace NewHorizons public static Dictionary> BodyDict = new(); public static List MountedAddons = new(); public static Dictionary AddonConfigs = new(); + public static Dictionary TitleScreenConfigs = new(); public static float SecondsElapsedInLoop = -1; @@ -107,10 +108,13 @@ namespace NewHorizons public ShipWarpController ShipWarpController { get; private set; } // API events - public class StarSystemEvent : UnityEvent { } - public StarSystemEvent OnChangeStarSystem = new(); - public StarSystemEvent OnStarSystemLoaded = new(); - public StarSystemEvent OnPlanetLoaded = new(); + public class StringEvent : UnityEvent { } + public StringEvent OnChangeStarSystem = new(); + public StringEvent OnStarSystemLoaded = new(); + public StringEvent OnPlanetLoaded = new(); + public class StringIndexEvent : UnityEvent { } + public StringIndexEvent OnTitleScreenLoaded = new(); + public UnityEvent OnAllTitleScreensLoaded = new(); /// /// Depending on platform, the AsyncOwnershipStatus might not be ready by the time we go to check it. @@ -161,7 +165,10 @@ namespace NewHorizons if (wasUsingCustomTitleScreen != CustomTitleScreen && SceneManager.GetActiveScene().name == "TitleScreen" && _wasConfigured) { NHLogger.LogVerbose("Reloading"); - SceneManager.LoadScene("TitleScreen", LoadSceneMode.Single); + TitleSceneHandler.reloaded = true; + // Taken and modified from SubmitActionLoadScene.ConfirmSubmit + LoadManager.LoadScene(OWScene.TitleScreen); + Locator.GetMenuInputModule().DisableInputs(); } _wasConfigured = true; @@ -171,6 +178,9 @@ namespace NewHorizons { BodyDict.Clear(); SystemDict.Clear(); + TitleScreenConfigs.Clear(); + + TitleSceneHandler.ResetConfigs(); BodyDict["SolarSystem"] = new List(); BodyDict["EyeOfTheUniverse"] = new List(); // Keep this empty tho fr @@ -423,15 +433,7 @@ namespace NewHorizons if (isTitleScreen && CustomTitleScreen) { - try - { - TitleSceneHandler.DisplayBodyOnTitleScreen(BodyDict.Values.ToList().SelectMany(x => x).ToList()); - } - catch (Exception e) - { - NHLogger.LogError($"Failed to make title screen bodies: {e}"); - } - TitleSceneHandler.InitSubtitles(); + TitleSceneHandler.Init(); } // EOTU fixes @@ -795,6 +797,10 @@ namespace NewHorizons { LoadAddonManifest("addon-manifest.json", mod); } + if (File.Exists(Path.Combine(folder, "title-screen.json"))) + { + LoadTitleScreenConfig("title-screen.json", mod); + } if (Directory.Exists(Path.Combine(folder, "translations"))) { LoadTranslations(folder, mod); @@ -847,6 +853,25 @@ namespace NewHorizons AddonConfigs[mod] = addonConfig; } + private void LoadTitleScreenConfig(string file, IModBehaviour mod) + { + NHLogger.LogVerbose($"Loading title screen config for {mod.ModHelper.Manifest.Name}"); + + var titleScreenConfig = mod.ModHelper.Storage.Load(file, false); + + if (titleScreenConfig == null) + { + NHLogger.LogError($"Title screen config for {mod.ModHelper.Manifest.Name} could not load, check your JSON"); + return; + } + + TitleScreenConfigs[mod] = titleScreenConfig; + foreach (var info in titleScreenConfig.titleScreens) + { + TitleSceneHandler.RegisterBuilder(mod, info); + } + } + private void LoadTranslations(string folder, IModBehaviour mod) { var foundFile = false; diff --git a/NewHorizons/NewHorizonsApi.cs b/NewHorizons/NewHorizonsApi.cs index 348991c0..14a4fdcf 100644 --- a/NewHorizons/NewHorizonsApi.cs +++ b/NewHorizons/NewHorizonsApi.cs @@ -90,6 +90,8 @@ namespace NewHorizons public UnityEvent GetChangeStarSystemEvent() => Main.Instance.OnChangeStarSystem; public UnityEvent GetStarSystemLoadedEvent() => Main.Instance.OnStarSystemLoaded; public UnityEvent GetBodyLoadedEvent() => Main.Instance.OnPlanetLoaded; + public UnityEvent GetTitleScreenLoadedEvent() => Main.Instance.OnTitleScreenLoaded; + public UnityEvent GetAllTitleScreensLoadedEvent() => Main.Instance.OnAllTitleScreensLoaded; public bool SetDefaultSystem(string name) { @@ -178,6 +180,24 @@ namespace NewHorizons return default; } + public object QueryTitleScreen(Type outType, IModBehaviour mod, string jsonPath) + { + var titleScreenConfig = Main.TitleScreenConfigs[mod]; + return titleScreenConfig == null + ? null + : QueryJson(outType, Path.Combine(mod.ModHelper.Manifest.ModFolderPath, "title-screen.json"), jsonPath); + } + + public T QueryTitleScreen(IModBehaviour mod, string jsonPath) + { + var data = QueryTitleScreen(typeof(T), mod, jsonPath); + if (data is T result) + { + return result; + } + return default; + } + public GameObject SpawnObject(IModBehaviour mod, GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignRadial) { @@ -344,5 +364,8 @@ namespace NewHorizons public void AddSubtitle(IModBehaviour mod, string filePath) => SubtitlesHandler.RegisterAdditionalSubtitle(mod, filePath); public void SetNextSpawnID(string id) => PlayerSpawnHandler.TargetSpawnID = id; + + public void RegisterTitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets = true, bool shareTitleScreen = false, string persistentConditionRequired = null, string factRequired = null) + => TitleSceneHandler.RegisterBuilder(mod, builder, disableNHPlanets, shareTitleScreen, persistentConditionRequired, factRequired); } } diff --git a/NewHorizons/Patches/TitleScenePatches.cs b/NewHorizons/Patches/TitleScenePatches.cs new file mode 100644 index 00000000..a0705c53 --- /dev/null +++ b/NewHorizons/Patches/TitleScenePatches.cs @@ -0,0 +1,44 @@ +using HarmonyLib; +using NewHorizons.Handlers; +using NewHorizons.Utility; +using NewHorizons.Utility.OWML; +using OWML.Utils; +using UnityEngine; + +namespace NewHorizons.Patches; + +[HarmonyPatch] +internal static class TitleScenePatches +{ + [HarmonyPrefix, HarmonyPatch(typeof(TitleScreenAnimation), nameof(TitleScreenAnimation.Awake))] + public static void TitleScreenAnimation_Awake(TitleScreenAnimation __instance) + { + if (TitleSceneHandler.reloaded) + { + TitleSceneHandler.reloaded = false; + + // Skip Splash on title screen reload + TitleScreenAnimation titleScreenAnimation = __instance; + titleScreenAnimation._fadeDuration = 0; + titleScreenAnimation._gamepadSplash = false; + titleScreenAnimation._introPan = false; + + TitleAnimationController titleAnimationController = GameObject.FindObjectOfType(); + titleAnimationController._logoFadeDelay = 0.001f; + titleAnimationController._logoFadeDuration = 0.001f; + titleAnimationController._optionsFadeDelay = 0.001f; + titleAnimationController._optionsFadeDuration = 0.001f; + titleAnimationController._optionsFadeSpacing = 0.001f; + titleAnimationController.FadeInTitleLogo(); + + // Reopen profile + if (TitleSceneHandler.reopenProfile) + { + TitleSceneHandler.reopenProfile = false; + Delay.FireOnNextUpdate(() => + SearchUtilities.Find("TitleMenu/TitleCanvas/TitleLayoutGroup/MainMenuBlock/MainMenuLayoutGroup/Button-Profile") + .GetComponent().Submit()); + } + } + } +} diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index 82ee3dda..f8c63685 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -946,35 +946,6 @@ "type": "object", "additionalProperties": false, "properties": { - "assetBundle": { - "type": "string", - "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" - }, - "path": { - "type": "string", - "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." - }, - "removeChildren": { - "type": "array", - "description": "A list of children to remove from this detail", - "items": { - "type": "string" - } - }, - "removeComponents": { - "type": "boolean", - "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." - }, - "scale": { - "type": "number", - "description": "Scale the prop", - "format": "float", - "default": 1.0 - }, - "stretch": { - "description": "Scale each axis of the prop. Overrides `scale`.", - "$ref": "#/definitions/MVector3" - }, "keepLoaded": { "type": "boolean", "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." @@ -1025,6 +996,35 @@ "description": "Should this detail be treated as a socket for an interactible item", "$ref": "#/definitions/ItemSocketInfo" }, + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, "rotation": { "description": "Rotation of the object", "$ref": "#/definitions/MVector3" @@ -1564,35 +1564,6 @@ "type": "object", "additionalProperties": false, "properties": { - "assetBundle": { - "type": "string", - "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" - }, - "path": { - "type": "string", - "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." - }, - "removeChildren": { - "type": "array", - "description": "A list of children to remove from this detail", - "items": { - "type": "string" - } - }, - "removeComponents": { - "type": "boolean", - "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." - }, - "scale": { - "type": "number", - "description": "Scale the prop", - "format": "float", - "default": 1.0 - }, - "stretch": { - "description": "Scale each axis of the prop. Overrides `scale`.", - "$ref": "#/definitions/MVector3" - }, "keepLoaded": { "type": "boolean", "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." @@ -1643,6 +1614,35 @@ "description": "Should this detail be treated as a socket for an interactible item", "$ref": "#/definitions/ItemSocketInfo" }, + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, "rotation": { "description": "Rotation of the object", "$ref": "#/definitions/MVector3" @@ -1680,35 +1680,6 @@ "type": "object", "additionalProperties": false, "properties": { - "assetBundle": { - "type": "string", - "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" - }, - "path": { - "type": "string", - "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." - }, - "removeChildren": { - "type": "array", - "description": "A list of children to remove from this detail", - "items": { - "type": "string" - } - }, - "removeComponents": { - "type": "boolean", - "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." - }, - "scale": { - "type": "number", - "description": "Scale the prop", - "format": "float", - "default": 1.0 - }, - "stretch": { - "description": "Scale each axis of the prop. Overrides `scale`.", - "$ref": "#/definitions/MVector3" - }, "keepLoaded": { "type": "boolean", "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." @@ -1759,6 +1730,35 @@ "description": "Should this detail be treated as a socket for an interactible item", "$ref": "#/definitions/ItemSocketInfo" }, + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, "rotation": { "description": "Rotation of the object", "$ref": "#/definitions/MVector3" @@ -2381,33 +2381,6 @@ "type": "object", "additionalProperties": false, "properties": { - "rotation": { - "description": "Rotation of the object", - "$ref": "#/definitions/MVector3" - }, - "alignRadial": { - "type": [ - "boolean", - "null" - ], - "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." - }, - "position": { - "description": "Position of the object", - "$ref": "#/definitions/MVector3" - }, - "isRelativeToParent": { - "type": "boolean", - "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." - }, - "parentPath": { - "type": "string", - "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." - }, - "rename": { - "type": "string", - "description": "An optional rename of this object" - }, "assetBundle": { "type": "string", "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" @@ -2437,6 +2410,33 @@ "description": "Scale each axis of the prop. Overrides `scale`.", "$ref": "#/definitions/MVector3" }, + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, "keepLoaded": { "type": "boolean", "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." @@ -3976,35 +3976,6 @@ "type": "object", "additionalProperties": false, "properties": { - "assetBundle": { - "type": "string", - "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" - }, - "path": { - "type": "string", - "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." - }, - "removeChildren": { - "type": "array", - "description": "A list of children to remove from this detail", - "items": { - "type": "string" - } - }, - "removeComponents": { - "type": "boolean", - "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." - }, - "scale": { - "type": "number", - "description": "Scale the prop", - "format": "float", - "default": 1.0 - }, - "stretch": { - "description": "Scale each axis of the prop. Overrides `scale`.", - "$ref": "#/definitions/MVector3" - }, "keepLoaded": { "type": "boolean", "description": "Should this detail stay loaded (visible and collideable) even if you're outside the sector (good for very large props)?\nAlso makes this detail visible on the map.\nKeeping many props loaded is bad for performance so use this only when it's actually relevant\nMost logic/behavior scripts will still only work inside the sector, as most of those scripts break if a sector is not provided." @@ -4055,6 +4026,35 @@ "description": "Should this detail be treated as a socket for an interactible item", "$ref": "#/definitions/ItemSocketInfo" }, + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + }, "rotation": { "description": "Rotation of the object", "$ref": "#/definitions/MVector3" @@ -4507,7 +4507,7 @@ }, "type": { "description": "The type of dream candle this is.", - "default": "Ground", + "default": "ground", "$ref": "#/definitions/DreamCandleType" }, "startLit": { diff --git a/NewHorizons/Schemas/title_screen_schema.json b/NewHorizons/Schemas/title_screen_schema.json new file mode 100644 index 00000000..f1c45631 --- /dev/null +++ b/NewHorizons/Schemas/title_screen_schema.json @@ -0,0 +1,286 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Title Screen Schema", + "type": "object", + "additionalProperties": false, + "properties": { + "titleScreens": { + "type": "array", + "description": "Create title screens", + "items": { + "$ref": "#/definitions/TitleScreenInfo" + } + }, + "$schema": { + "type": "string", + "description": "The schema to validate with" + } + }, + "definitions": { + "TitleScreenInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "menuTextTint": { + "description": "Colour of the text on the main menu", + "$ref": "#/definitions/MColor" + }, + "factRequiredForTitle": { + "type": "string", + "description": "Ship log fact required for this title screen to appear." + }, + "persistentConditionRequiredForTitle": { + "type": "string", + "description": "Persistent condition required for this title screen to appear." + }, + "disableNHPlanets": { + "type": "boolean", + "description": "If set to true, NH generated planets will not show on the title screen. If false, this title screen has the same chance as other NH planet title screens to show." + }, + "shareTitleScreen": { + "type": "boolean", + "description": "If set to true, this custom title screen will merge with all other custom title screens with shareTitleScreen set to true. If false, NH will randomly select between this and other valid title screens that are loaded." + }, + "Skybox": { + "description": "Customize the skybox for this title screen", + "$ref": "#/definitions/SkyboxModule" + }, + "music": { + "type": "string", + "description": "The music audio that will play on the title screen. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "ambience": { + "type": "string", + "description": "The ambience audio that will play on the title screen. Can be a path to a .wav/.ogg/.mp3 file, or taken from the AudioClip list." + }, + "Background": { + "description": "Edit properties of the background", + "$ref": "#/definitions/BackgroundModule" + }, + "MenuPlanet": { + "description": "Edit properties of the main menu planet", + "$ref": "#/definitions/MenuPlanetModule" + }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + } + } + }, + "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 + } + } + }, + "SkyboxModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "destroyStarField": { + "type": "boolean", + "description": "Whether to destroy the star field around the player" + }, + "useCube": { + "type": "boolean", + "description": "Whether to use a cube for the skybox instead of a smooth sphere" + }, + "rightPath": { + "type": "string", + "description": "Relative filepath to the texture to use for the skybox's positive X direction" + }, + "leftPath": { + "type": "string", + "description": "Relative filepath to the texture to use for the skybox's negative X direction" + }, + "topPath": { + "type": "string", + "description": "Relative filepath to the texture to use for the skybox's positive Y direction" + }, + "bottomPath": { + "type": "string", + "description": "Relative filepath to the texture to use for the skybox's negative Y direction" + }, + "frontPath": { + "type": "string", + "description": "Relative filepath to the texture to use for the skybox's positive Z direction" + }, + "backPath": { + "type": "string", + "description": "Relative filepath to the texture to use for the skybox's negative Z direction" + } + } + }, + "BackgroundModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "rotationSpeed": { + "type": "number", + "description": "Changes the speed the background rotates (and by extension the main menu planet). This is in degrees per second.", + "format": "float" + }, + "removeChildren": { + "type": "array", + "description": "Disables the renderers of objects at the provided paths", + "items": { + "type": "string" + } + }, + "details": { + "type": "array", + "description": "A list of DetailInfos to populate the background with.", + "items": { + "$ref": "#/definitions/SimplifiedDetailInfo" + } + } + } + }, + "SimplifiedDetailInfo": { + "type": "object", + "description": "A lesser form of DetailInfo used for the title screen since that supports fewer features", + "additionalProperties": false, + "properties": { + "rotation": { + "description": "Rotation of the object", + "$ref": "#/definitions/MVector3" + }, + "alignRadial": { + "type": [ + "boolean", + "null" + ], + "description": "Do we try to automatically align this object to stand upright relative to the body's center? Stacks with rotation.\nDefaults to true for geysers, tornados, and volcanoes, and false for everything else." + }, + "position": { + "description": "Position of the object", + "$ref": "#/definitions/MVector3" + }, + "isRelativeToParent": { + "type": "boolean", + "description": "Whether the positional and rotational coordinates are relative to parent instead of the root planet object." + }, + "parentPath": { + "type": "string", + "description": "The relative path from the planet to the parent of this object. Optional (will default to the root sector)." + }, + "rename": { + "type": "string", + "description": "An optional rename of this object" + }, + "assetBundle": { + "type": "string", + "description": "Relative filepath to an asset-bundle to load the prefab defined in `path` from" + }, + "path": { + "type": "string", + "description": "Either the path in the scene hierarchy of the item to copy or the path to the object in the supplied asset bundle. \nIf empty, will make an empty game object. This can be useful for adding other props to it as its children." + }, + "removeChildren": { + "type": "array", + "description": "A list of children to remove from this detail", + "items": { + "type": "string" + } + }, + "removeComponents": { + "type": "boolean", + "description": "Do we reset all the components on this object? Useful for certain props that have dialogue components attached to\nthem." + }, + "scale": { + "type": "number", + "description": "Scale the prop", + "format": "float", + "default": 1.0 + }, + "stretch": { + "description": "Scale each axis of the prop. Overrides `scale`.", + "$ref": "#/definitions/MVector3" + } + } + }, + "MVector3": { + "type": "object", + "additionalProperties": false, + "properties": { + "x": { + "type": "number", + "format": "float" + }, + "y": { + "type": "number", + "format": "float" + }, + "z": { + "type": "number", + "format": "float" + } + } + }, + "MenuPlanetModule": { + "type": "object", + "additionalProperties": false, + "properties": { + "destroyMenuPlanet": { + "type": "boolean", + "description": "Disables the renderers of the main menu planet and all objects on it (this is to improve compatibility with other mods that don't use the NH title screen json)." + }, + "removeChildren": { + "type": "array", + "description": "Disables the renderers of objects at the provided paths", + "items": { + "type": "string" + } + }, + "details": { + "type": "array", + "description": "A list of DetailInfos to populate the main menu planet with.", + "items": { + "$ref": "#/definitions/SimplifiedDetailInfo" + } + }, + "rotationSpeed": { + "type": "number", + "description": "Changes the speed the main menu planet. This is in degrees per second.", + "format": "float" + } + } + } + }, + "$docs": { + "title": "Title Screen Schema", + "description": "Schema for the title screen config in New Horizons" + } +} \ No newline at end of file diff --git a/SchemaExporter/SchemaExporter.cs b/SchemaExporter/SchemaExporter.cs index 00e37f02..4b15e469 100644 --- a/SchemaExporter/SchemaExporter.cs +++ b/SchemaExporter/SchemaExporter.cs @@ -33,6 +33,9 @@ public static class SchemaExporter var translationSchema = new Schema("Translation Schema", "Schema for a translation file in New Horizons", $"{folderName}/translation_schema", settings); translationSchema.Output(); + var titleScreenSchema = new Schema("Title Screen Schema", + "Schema for the title screen config in New Horizons", $"{folderName}/title_screen_schema", settings); + titleScreenSchema.Output(); Console.WriteLine("Done!"); } @@ -108,6 +111,19 @@ public static class SchemaExporter }; } + if (_title is "Title Screen Schema") + { + schema.Definitions["TitleScreenInfo"].Properties["extras"] = new JsonSchemaProperty { + Type = JsonObjectType.Object, + Description = "Extra data that may be used by extension mods", + AllowAdditionalProperties = true, + AdditionalPropertiesSchema = new JsonSchema + { + Type = JsonObjectType.Object + } + }; + } + return schema; } } diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 61ba83dc..a99b16dc 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -14,6 +14,7 @@ const schemas = [ "addon_manifest_schema.json", "dialogue_schema.xsd", "text_schema.xsd", + "title_screen_schema.json", "shiplog_schema.xsd" ]; @@ -90,6 +91,7 @@ export default defineConfig({ { label: "Addon Manifest Schema", link: "schemas/addon-manifest-schema" }, { label: "Dialogue Schema", link: "schemas/dialogue-schema" }, { label: "Text Schema", link: "schemas/text-schema" }, + { label: "Title Screen Schema", link: "schemas/title-screen-schema" }, { label: "Ship Log Schema", link: "schemas/shiplog-schema" } ] }, diff --git a/docs/src/content/docs/guides/star-systems.md b/docs/src/content/docs/guides/star-systems.md index a7d8f671..8ca4e695 100644 --- a/docs/src/content/docs/guides/star-systems.md +++ b/docs/src/content/docs/guides/star-systems.md @@ -16,7 +16,12 @@ A star system config file will look something like this: ```json title="my_star_system.json" { "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/star_system_schema.json", - "travelAudio": "assets/Travel.mp3", + "canEnterViaWarpDrive": true, + "startHere": false, + "respawnHere": true, + "GlobalMusic": { + "travelAudio": "planets/assets/Travel Audio.mp3" + }, "Vessel": { "coords": { "x": [4, 0, 3, 1], diff --git a/docs/src/content/docs/guides/title-screens.md b/docs/src/content/docs/guides/title-screens.md new file mode 100644 index 00000000..285a4713 --- /dev/null +++ b/docs/src/content/docs/guides/title-screens.md @@ -0,0 +1,194 @@ +--- +title: Title Screens +description: A guide to creating a custom title screens in New Horizons +--- + +Welcome! This page outlines how to make a custom title screen. + +## Getting Started + +Your mod's title screen config is a JSON file named `title-screen.json` that should be placed within your mod folder. + +A title screen config file will look something like this: + +```json title="title-screen.json" +{ + "$schema": "https://raw.githubusercontent.com/Outer-Wilds-New-Horizons/new-horizons/main/NewHorizons/Schemas/title_screen_schema.json", + "titleScreens": [ + { + "disableNHPlanets": false, + "shareTitleScreen": true, + "music": "planets/assets/TitleScreenMusic.mp3" + }, + { + "disableNHPlanets": true, + "shareTitleScreen": true, + "factRequiredForTitle": "EXAMPLES_ARTIFICIAL_GRAVITY", + "menuTextTint": { + "r": 128, + "g": 128, + "b": 255 + }, + "music": "planets/assets/TitleScreenMusic.mp3", + "ambience": "planets/assets/TitleScreenAmbience.mp3", + "Skybox": { + "destroyStarField": true, + "rightPath": "systems/New System Assets/Skybox/Right_Large.png", + "leftPath": "systems/New System Assets/Skybox/Left_Large.png", + "topPath": "systems/New System Assets/Skybox/Up_Large.png", + "bottomPath": "systems/New System Assets/Skybox/Down_Large.png", + "frontPath": "systems/New System Assets/Skybox/Front_Large.png", + "backPath": "systems/New System Assets/Skybox/Back_Large.png" + }, + "Background": { + "details": [ + { + "assetBundle": "assetbundles/test", + "path": "Assets/Prefabs/Background.prefab", + "position": {"x": 200, "y": 280, "z": -50}, + "rotation": {"x": 310, "y": 0, "z": 310}, + "scale": 0.05 + } + ], + "rotationSpeed": 10 + }, + "MenuPlanet": { + "destroyMenuPlanet": false, + "removeChildren": ["PlanetRoot/Props"], + "details": [ + { + "assetBundle": "assetbundles/test", + "path": "Assets/Prefabs/ArtificialGravity.prefab", + "removeChildren": ["Gravity"], + "parentPath": "PlanetRoot", + "position": {"x": 0, "y": 32, "z": 0}, + "rotation": {"x": 90, "y": 0, "z": 0}, + "scale": 10 + } + ], + "rotationSpeed": 20 + } + } + ] +} +``` + +You can have multiple title screens but only one will be selected from the list. The last title screen in the list, that is unlocked, will always be selected. + +## Configs + +Title screens from configs are always put first into the list. + +### `disableNHPlanets` + +If set to `true`, prevents NH-generated planets from appearing on the title screen. Defaults to true. + +### `shareTitleScreen` + +If set to `true`, this title screen will merge with others that have the same setting enabled. For more info head to the [sharing section](#sharing) of this page. Defaults to false. + +### `menuTextTint` + +Defines the color of the menu text and logo. Uses RGB values, where `r`, `g`, and `b` range from `0` to `255`. + +### `factRequiredForTitle` + +Specifies a ship log fact that must be discovered for this title screen to appear. + +### `conditionRequiredForTitle` + +Specifies a persistent condition required for this title screen to appear. + +### `music` and `ambience` + +The audio for background music and ambience. Can be a path to a .wav/.ogg/.mp3 file, or taken from the [AudioClip list](/reference/audio-enum). + +### `Background` and `MenuPlanet` + +A module for the background and main menu planet that include object additions, removal, and rotation speed. + +##### `details` + +You can add objects to both the background and menu planet. The menu planet objects spin while the background objects are stationary. +These simplified details are just like the details in planet configs except that they only have the basic features. + +## Schema + +To see all the different things you can put into a config file check out the [Title Screen Schema](/schemas/title-screen-schema). + +## API + +New Horizons provides an API method to register and build custom title screens dynamically. + +These will be put at the end of the list for the selection of all your mod's title screens. + +You cannot combine configs with API unfortunately as only the API will be selected. + +```csharp title="INewHorizons.cs" +/// +/// Registers a builder for the main menu. +/// Call this once before the main menu finishes loading +/// +void RegisterTitleScreenBuilder(IModBehaviour mod, Action builder, bool disableNHPlanets = true, bool shareTitleScreen = false, string conditionRequired = null, string factRequired = null); +``` + +It shares a few values with the configs but also has an exclusive one. + +`builder`: Builder to run when this title screen is selected. The GameObject passed through it is the main scene object containing both the background and menu planet. + +### Example API usage + +You can run `RegisterTitleScreenBuilder` more than once to add multiple title screen builders. + +```csharp title="YourModBehaviour.cs" +NewHorizons = ModHelper.Interaction.TryGetModApi("xen.NewHorizons"); +NewHorizons.RegisterTitleScreenBuilder(this, BuildTitleScreen, disableNHPlanets: true, shareTitleScreen: true); +``` + +```csharp title="YourModBehaviour.cs" +public void BuildTitleScreen(GameObject scene) +{ + ModHelper.Console.WriteLine($"Building title screen", MessageType.Success); + //Add an object to the title screen or do whatever else you want +} +``` + +## Events + +Additionally, New Horizons provides events in the API for tracking title screen loading: + +```csharp title="INewHorizons.cs" +/// +/// An event invoked when NH has finished building a title screen. +/// Gives the unique name of the mod the title screen builder was from and the index for when you have multiple title screens. +/// +UnityEvent GetTitleScreenLoadedEvent(); + +/// +/// An event invoked when NH has finished building the title screen. +/// +UnityEvent GetAllTitleScreensLoadedEvent(); +``` + +### Example event usage + +```csharp title="YourModBehaviour.cs" +NewHorizons = ModHelper.Interaction.TryGetModApi("xen.NewHorizons"); +NewHorizons.GetTitleScreenLoadedEvent().AddListener(OnTitleScreenLoaded); +NewHorizons.GetAllTitleScreensLoadedEvent().AddListener(OnAllTitleScreensLoaded); +``` + +```csharp title="YourModBehaviour.cs" +public void OnTitleScreenLoaded(string modUniqueName, int index) +{ + ModHelper.Console.WriteLine($"Title screen loaded: {modUniqueName} #{index}", MessageType.Success); +} +public void OnAllTitleScreensLoaded() +{ + ModHelper.Console.WriteLine("All title screens loaded", MessageType.Success); +} +``` + +## Sharing + +New Horizons will randomly select a valid title screen each time the user enters the main menu and then if `shareTitleScreen` is set to `true` it will build all the other shareable title screens (that also have matching `disableNHPlanets` values). If it doesn't have share set to true then it will only show the randomly selected.