diff --git a/NewHorizons/External/Configs/PlanetConfig.cs b/NewHorizons/External/Configs/PlanetConfig.cs index 4a353011..2b071244 100644 --- a/NewHorizons/External/Configs/PlanetConfig.cs +++ b/NewHorizons/External/Configs/PlanetConfig.cs @@ -169,6 +169,11 @@ namespace NewHorizons.External.Configs /// public WaterModule Water; + /// + /// Extra data that may be used by extension mods + /// + public object extras; + public PlanetConfig() { // Always have to have a base module diff --git a/NewHorizons/External/Configs/StarSystemConfig.cs b/NewHorizons/External/Configs/StarSystemConfig.cs index 2df0dc8c..ce311174 100644 --- a/NewHorizons/External/Configs/StarSystemConfig.cs +++ b/NewHorizons/External/Configs/StarSystemConfig.cs @@ -102,6 +102,11 @@ namespace NewHorizons.External.Configs /// public CuriosityColorInfo[] curiosities; + /// + /// Extra data that may be used by extension mods + /// + public object extras; + public class NomaiCoordinates { [MinLength(2)] diff --git a/NewHorizons/Handlers/PlanetCreationHandler.cs b/NewHorizons/Handlers/PlanetCreationHandler.cs index 73df7daa..ba0ea8f8 100644 --- a/NewHorizons/Handlers/PlanetCreationHandler.cs +++ b/NewHorizons/Handlers/PlanetCreationHandler.cs @@ -248,6 +248,16 @@ namespace NewHorizons.Handlers } } } + + try + { + Main.Instance.OnPlanetLoaded?.Invoke(body.Config.name); + } + catch (Exception e) + { + Logger.LogError($"Error in event handler for OnPlanetLoaded on body {body.Config.name}: {e}"); + } + return true; } diff --git a/NewHorizons/INewHorizons.cs b/NewHorizons/INewHorizons.cs index 7c647003..8f132ee7 100644 --- a/NewHorizons/INewHorizons.cs +++ b/NewHorizons/INewHorizons.cs @@ -43,6 +43,22 @@ namespace NewHorizons /// UnityEvent GetStarSystemLoadedEvent(); + /// + /// An event invoked when NH has finished a planet for a star system. + /// Gives the name of the planet that was just loaded. + /// + UnityEvent GetBodyLoadedEvent(); + + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); + + /// + /// Uses JSONPath to query a system + /// + object QuerySystem(Type outType, string path); + /// /// Allows you to overwrite the default system. This is where the player is respawned after dying. /// diff --git a/NewHorizons/Main.cs b/NewHorizons/Main.cs index 09d20227..80f5dc53 100644 --- a/NewHorizons/Main.cs +++ b/NewHorizons/Main.cs @@ -68,6 +68,7 @@ namespace NewHorizons public class StarSystemEvent : UnityEvent { } public StarSystemEvent OnChangeStarSystem; public StarSystemEvent OnStarSystemLoaded; + public StarSystemEvent OnPlanetLoaded; // For warping to the eye system private GameObject _ship; @@ -126,7 +127,7 @@ namespace NewHorizons BodyDict["SolarSystem"] = new List(); BodyDict["EyeOfTheUniverse"] = new List(); // Keep this empty tho fr - SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), Instance) + SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), "", Instance) { Config = { @@ -142,7 +143,7 @@ namespace NewHorizons } } }; - SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), Instance) + SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), "", Instance) { Config = { @@ -170,6 +171,7 @@ namespace NewHorizons OnChangeStarSystem = new StarSystemEvent(); OnStarSystemLoaded = new StarSystemEvent(); + OnPlanetLoaded = new StarSystemEvent(); SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; @@ -515,7 +517,7 @@ namespace NewHorizons } else { - SystemDict[name] = new NewHorizonsSystem(name, starSystemConfig, mod); + SystemDict[name] = new NewHorizonsSystem(name, starSystemConfig, relativePath, mod); } } } @@ -616,7 +618,7 @@ namespace NewHorizons starSystemConfig.Migrate(); starSystemConfig.FixCoordinates(); - var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, mod); + var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, $"", mod); SystemDict.Add(config.starSystem, system); diff --git a/NewHorizons/NewHorizonsApi.cs b/NewHorizons/NewHorizonsApi.cs index 0ddb9f14..4924834f 100644 --- a/NewHorizons/NewHorizonsApi.cs +++ b/NewHorizons/NewHorizonsApi.cs @@ -7,12 +7,15 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using UnityEngine; using UnityEngine.Events; using Logger = NewHorizons.Utility.Logger; namespace NewHorizons { + public class NewHorizonsApi : INewHorizons { [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] @@ -64,20 +67,10 @@ namespace NewHorizons return Main.BodyDict.Values.SelectMany(x => x)?.ToList()?.FirstOrDefault(x => x.Config.name == name)?.Object; } - public string GetCurrentStarSystem() - { - return Main.Instance.CurrentStarSystem; - } - - public UnityEvent GetChangeStarSystemEvent() - { - return Main.Instance.OnChangeStarSystem; - } - - public UnityEvent GetStarSystemLoadedEvent() - { - return Main.Instance.OnStarSystemLoaded; - } + public string GetCurrentStarSystem() => Main.Instance.CurrentStarSystem; + public UnityEvent GetChangeStarSystemEvent() => Main.Instance.OnChangeStarSystem; + public UnityEvent GetStarSystemLoadedEvent() => Main.Instance.OnStarSystemLoaded; + public UnityEvent GetBodyLoadedEvent() => Main.Instance.OnPlanetLoaded; public bool SetDefaultSystem(string name) { @@ -108,6 +101,42 @@ namespace NewHorizons } } + private static object QueryJson(Type outType, string filePath, string jsonPath) + { + if (filePath == "") return null; + try + { + var jsonText = File.ReadAllText(filePath); + var jsonData = JObject.Parse(jsonText); + return jsonData.SelectToken(jsonPath)?.ToObject(outType); + } + catch (FileNotFoundException) + { + return null; + } + catch (JsonException e) + { + Logger.LogError(e.ToString()); + return null; + } + } + + public object QueryBody(Type outType, string bodyName, string jsonPath) + { + var planet = Main.BodyDict[Main.Instance.CurrentStarSystem].Find((b) => b.Config.name == bodyName); + return planet == null + ? null + : QueryJson(outType, planet.Mod.ModHelper.Manifest.ModFolderPath + planet.RelativePath, jsonPath); + } + + public object QuerySystem(Type outType, string jsonPath) + { + var system = Main.SystemDict[Main.Instance.CurrentStarSystem]; + return system == null + ? null + : QueryJson(outType, system.Mod.ModHelper.Manifest.ModFolderPath + system.RelativePath, jsonPath); + } + public GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal) { diff --git a/NewHorizons/Schemas/body_schema.json b/NewHorizons/Schemas/body_schema.json index bc88b9f7..3783d7e7 100644 --- a/NewHorizons/Schemas/body_schema.json +++ b/NewHorizons/Schemas/body_schema.json @@ -128,6 +128,13 @@ "description": "Add water to this planet", "$ref": "#/definitions/WaterModule" }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" diff --git a/NewHorizons/Schemas/star_system_schema.json b/NewHorizons/Schemas/star_system_schema.json index b1f5e2cc..032fb8f1 100644 --- a/NewHorizons/Schemas/star_system_schema.json +++ b/NewHorizons/Schemas/star_system_schema.json @@ -71,6 +71,13 @@ "$ref": "#/definitions/CuriosityColorInfo" } }, + "extras": { + "type": "object", + "description": "Extra data that may be used by extension mods", + "additionalProperties": { + "type": "object" + } + }, "$schema": { "type": "string", "description": "The schema to validate with" diff --git a/NewHorizons/Utility/NewHorizonsSystem.cs b/NewHorizons/Utility/NewHorizonsSystem.cs index ae6bafa3..be9a539f 100644 --- a/NewHorizons/Utility/NewHorizonsSystem.cs +++ b/NewHorizons/Utility/NewHorizonsSystem.cs @@ -11,15 +11,17 @@ namespace NewHorizons.Utility public class NewHorizonsSystem { public string UniqueID; + public string RelativePath; public SpawnModule Spawn = null; public SpawnPoint SpawnPoint = null; public StarSystemConfig Config; public IModBehaviour Mod; - public NewHorizonsSystem(string uniqueID, StarSystemConfig config, IModBehaviour mod) + public NewHorizonsSystem(string uniqueID, StarSystemConfig config, string relativePath, IModBehaviour mod) { UniqueID = uniqueID; Config = config; + RelativePath = relativePath; Mod = mod; } } diff --git a/SchemaExporter/SchemaExporter.cs b/SchemaExporter/SchemaExporter.cs index f0c802f5..c2e94e25 100644 --- a/SchemaExporter/SchemaExporter.cs +++ b/SchemaExporter/SchemaExporter.cs @@ -78,16 +78,29 @@ public static class SchemaExporter {"description", _description} }); - if (_title == "Celestial Body Schema") + switch (_title) { - schema.Definitions["OrbitModule"].Properties["semiMajorAxis"].Default = 5000f; + case "Celestial Body Schema": + schema.Definitions["OrbitModule"].Properties["semiMajorAxis"].Default = 5000f; + break; + case "Star System Schema": + schema.Definitions["NomaiCoordinates"].Properties["x"].UniqueItems = true; + schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; + schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; + break; } - if (_title == "Star System Schema") + if (_title is "Star System Schema" or "Celestial Body Schema") { - schema.Definitions["NomaiCoordinates"].Properties["x"].UniqueItems = true; - schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; - schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; + schema.Properties["extras"] = new JsonSchemaProperty { + Type = JsonObjectType.Object, + Description = "Extra data that may be used by extension mods", + AllowAdditionalProperties = true, + AdditionalPropertiesSchema = new JsonSchema + { + Type = JsonObjectType.Object + } + }; } return schema; diff --git a/docs/content/pages/tutorials/api.md b/docs/content/pages/tutorials/api.md index 8689943c..5c7a8996 100644 --- a/docs/content/pages/tutorials/api.md +++ b/docs/content/pages/tutorials/api.md @@ -9,21 +9,99 @@ First create the following interface in your mod: ```cs public interface INewHorizons -{ - void LoadConfigs(IModBehaviour mod); + { + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config); - GameObject GetPlanet(string name); - - string GetCurrentStarSystem(); + [Obsolete("Create(Dictionary config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] + void Create(Dictionary config, IModBehaviour mod); - UnityEvent GetChangeStarSystemEvent(); + /// + /// Will load all configs in the regular folders (planets, systems, translations, etc) for this mod. + /// The NH addon config template is just a single call to this API method. + /// + void LoadConfigs(IModBehaviour mod); - UnityEvent GetStarSystemLoadedEvent(); - - GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); + /// + /// Retrieve the root GameObject of a custom planet made by creating configs. + /// Will only work if the planet has been created (see GetStarSystemLoadedEvent) + /// + GameObject GetPlanet(string name); - string[] GetInstalledAddons(); -} + /// + /// The name of the current star system loaded. + /// + string GetCurrentStarSystem(); + + /// + /// An event invoked when the player begins loading the new star system, before the scene starts to load. + /// Gives the name of the star system being switched to. + /// + UnityEvent GetChangeStarSystemEvent(); + + /// + /// An event invoked when NH has finished generating all planets for a new star system. + /// Gives the name of the star system that was just loaded. + /// + UnityEvent GetStarSystemLoadedEvent(); + + /// + /// An event invoked when NH has finished a planet for a star system. + /// Gives the name of the planet that was just loaded. + /// + UnityEvent GetBodyLoadedEvent(); + + /// + /// Uses JSONPath to query a body + /// + object QueryBody(Type outType, string bodyName, string path); + + /// + /// Uses JSONPath to query a system + /// + object QuerySystem(Type outType, string path); + + /// + /// Allows you to overwrite the default system. This is where the player is respawned after dying. + /// + bool SetDefaultSystem(string name); + + /// + /// Allows you to instantly begin a warp to a new star system. + /// Will return false if that system does not exist (cannot be warped to). + /// + bool ChangeCurrentStarSystem(string name); + + /// + /// Returns the uniqueIDs of each installed NH addon. + /// + string[] GetInstalledAddons(); + + /// + /// Allows you to spawn a copy of a prop by specifying its path. + /// This is the same as using Props->details in a config, but also returns the spawned gameObject to you. + /// + GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, + float scale, bool alignWithNormal); + + /// + /// Allows you to spawn an AudioSignal on a planet. + /// This is the same as using Props->signals in a config, but also returns the spawned AudioSignal to you. + /// This method will not set its position. You will have to do that with the returned object. + /// + AudioSignal SpawnSignal(IModBehaviour mod, GameObject root, string audio, string name, string frequency, + float sourceRadius = 1f, float detectionRadius = 20f, float identificationRadius = 10f, bool insideCloak = false, + bool onlyAudibleToScope = true, string reveals = ""); + + /// + /// Allows you to spawn character dialogue on a planet. Also returns the RemoteDialogueTrigger if remoteTriggerRadius is specified. + /// This is the same as using Props->dialogue in a config, but also returns the spawned game objects to you. + /// This method will not set the position of the dialogue or remote trigger. You will have to do that with the returned objects. + /// + (CharacterDialogueTree, RemoteDialogueTrigger) SpawnDialogue(IModBehaviour mod, GameObject root, string xmlFile, float radius = 1f, + float range = 1f, string blockAfterPersistentCondition = null, float lookAtRadius = 1f, string pathToAnimController = null, + float remoteTriggerRadius = 0f); + } ``` In your main `ModBehaviour` class you can get the NewHorizons API like so: @@ -33,7 +111,7 @@ public class MyMod : ModBehaviour { void Start() { - INewHorizons NewHorizonsAPI = ModHelper.Interaction.GetModApi("xen.NewHorizons"); + INewHorizons NewHorizonsAPI = ModHelper.Interaction.TryGetModApi("xen.NewHorizons"); } } ``` diff --git a/docs/content/pages/tutorials/extending.md b/docs/content/pages/tutorials/extending.md new file mode 100644 index 00000000..0e9b30bc --- /dev/null +++ b/docs/content/pages/tutorials/extending.md @@ -0,0 +1,74 @@ +--- +Title: Extending Configs +Description: A guide on extending config files with the New Horizons API +Sort_Priority: 5 +--- + +# Extending Configs + +This guide will explain how to use the API to add new features to New Horizons. + +## How Extending Works + +Addon developers will add a key to the `extras` object in the root of the config + +```json +{ + "name": "Wetrock", + "extras": { + "myCoolExtensionData": { + "myCoolExtensionProperty": 2 + } + } +} +``` + +Your mod will then use the API's `QueryBody` method to obtain the `myCoolExtensionData` object. + +**It's up to the addon dev to list your mod as a dependency!** + +## Extending Planets + +You can extend all planets by hooking into the `OnBodyLoaded` event of the API: + +```csharp +var api = ModHelper.Interactions.TryGetModApi("xen.NewHorizons"); +api.GetBodyLoadedEvent().AddListener((name) => { + ModHelper.Console.WriteLine($"Body: {name} Loaded!"); +}); +``` + +In order to get your extra module, first define the module as a class: + +```csharp +public class MyCoolExtensionData { + int myCoolExtensionProperty; +} +``` + +Then, use the `QueryBody` method: + +```csharp +var api = ModHelper.Interactions.TryGetModApi("xen.NewHorizons"); +api.GetBodyLoadedEvent().AddListener((name) => { + ModHelper.Console.WriteLine($"Body: {name} Loaded!"); + var potentialData = api.QueryBody(typeof(MyCoolExtensionData), "$.extras.myCoolExtensionData", name); + // Makes sure the module is valid and not null + if (potentialData is MyCoolExtensionData data) { + ModHelper.Console.WriteLine($"myCoolExtensionProperty for {name} is {data.myCoolExtensionProperty}!"); + } +}); +``` + +## Extending Systems + +Extending systems is the exact same as extending planets, except you use the `QuerySystem` method instead. + +## Accessing Other Values + +You can also use the `QueryBody` method to get values of the config outside of your extension object + +```csharp +var primaryBody = api.QueryBody(typeof(string), "Wetrock", "$.Orbit.primaryBody"); + ModHelper.Console.WriteLine($"Primary of {bodyName} is {primaryBody ?? "NULL"}!"); +```