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"}!");
+```