Add Support For Extra Modules (#320)

Other mods can now add their own modules by using the API's `QueryBody` and `QuerySystem` methods.
This commit is contained in:
Noah 2022-08-30 03:51:27 -04:00 committed by GitHub
commit 5ba58f2642
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 285 additions and 37 deletions

View File

@ -169,6 +169,11 @@ namespace NewHorizons.External.Configs
/// </summary> /// </summary>
public WaterModule Water; public WaterModule Water;
/// <summary>
/// Extra data that may be used by extension mods
/// </summary>
public object extras;
public PlanetConfig() public PlanetConfig()
{ {
// Always have to have a base module // Always have to have a base module

View File

@ -102,6 +102,11 @@ namespace NewHorizons.External.Configs
/// </summary> /// </summary>
public CuriosityColorInfo[] curiosities; public CuriosityColorInfo[] curiosities;
/// <summary>
/// Extra data that may be used by extension mods
/// </summary>
public object extras;
public class NomaiCoordinates public class NomaiCoordinates
{ {
[MinLength(2)] [MinLength(2)]

View File

@ -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; return true;
} }

View File

@ -43,6 +43,22 @@ namespace NewHorizons
/// </summary> /// </summary>
UnityEvent<string> GetStarSystemLoadedEvent(); UnityEvent<string> GetStarSystemLoadedEvent();
/// <summary>
/// An event invoked when NH has finished a planet for a star system.
/// Gives the name of the planet that was just loaded.
/// </summary>
UnityEvent<string> GetBodyLoadedEvent();
/// <summary>
/// Uses JSONPath to query a body
/// </summary>
object QueryBody(Type outType, string bodyName, string path);
/// <summary>
/// Uses JSONPath to query a system
/// </summary>
object QuerySystem(Type outType, string path);
/// <summary> /// <summary>
/// Allows you to overwrite the default system. This is where the player is respawned after dying. /// Allows you to overwrite the default system. This is where the player is respawned after dying.
/// </summary> /// </summary>

View File

@ -68,6 +68,7 @@ namespace NewHorizons
public class StarSystemEvent : UnityEvent<string> { } public class StarSystemEvent : UnityEvent<string> { }
public StarSystemEvent OnChangeStarSystem; public StarSystemEvent OnChangeStarSystem;
public StarSystemEvent OnStarSystemLoaded; public StarSystemEvent OnStarSystemLoaded;
public StarSystemEvent OnPlanetLoaded;
// For warping to the eye system // For warping to the eye system
private GameObject _ship; private GameObject _ship;
@ -126,7 +127,7 @@ namespace NewHorizons
BodyDict["SolarSystem"] = new List<NewHorizonsBody>(); BodyDict["SolarSystem"] = new List<NewHorizonsBody>();
BodyDict["EyeOfTheUniverse"] = new List<NewHorizonsBody>(); // Keep this empty tho fr BodyDict["EyeOfTheUniverse"] = new List<NewHorizonsBody>(); // Keep this empty tho fr
SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), Instance) SystemDict["SolarSystem"] = new NewHorizonsSystem("SolarSystem", new StarSystemConfig(), "", Instance)
{ {
Config = Config =
{ {
@ -142,7 +143,7 @@ namespace NewHorizons
} }
} }
}; };
SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), Instance) SystemDict["EyeOfTheUniverse"] = new NewHorizonsSystem("EyeOfTheUniverse", new StarSystemConfig(), "", Instance)
{ {
Config = Config =
{ {
@ -170,6 +171,7 @@ namespace NewHorizons
OnChangeStarSystem = new StarSystemEvent(); OnChangeStarSystem = new StarSystemEvent();
OnStarSystemLoaded = new StarSystemEvent(); OnStarSystemLoaded = new StarSystemEvent();
OnPlanetLoaded = new StarSystemEvent();
SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneLoaded += OnSceneLoaded;
SceneManager.sceneUnloaded += OnSceneUnloaded; SceneManager.sceneUnloaded += OnSceneUnloaded;
@ -515,7 +517,7 @@ namespace NewHorizons
} }
else 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.Migrate();
starSystemConfig.FixCoordinates(); starSystemConfig.FixCoordinates();
var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, mod); var system = new NewHorizonsSystem(config.starSystem, starSystemConfig, $"", mod);
SystemDict.Add(config.starSystem, system); SystemDict.Add(config.starSystem, system);

View File

@ -7,12 +7,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine; using UnityEngine;
using UnityEngine.Events; using UnityEngine.Events;
using Logger = NewHorizons.Utility.Logger; using Logger = NewHorizons.Utility.Logger;
namespace NewHorizons namespace NewHorizons
{ {
public class NewHorizonsApi : INewHorizons public class NewHorizonsApi : INewHorizons
{ {
[Obsolete("Create(Dictionary<string, object> config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")] [Obsolete("Create(Dictionary<string, object> 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; return Main.BodyDict.Values.SelectMany(x => x)?.ToList()?.FirstOrDefault(x => x.Config.name == name)?.Object;
} }
public string GetCurrentStarSystem() public string GetCurrentStarSystem() => Main.Instance.CurrentStarSystem;
{ public UnityEvent<string> GetChangeStarSystemEvent() => Main.Instance.OnChangeStarSystem;
return Main.Instance.CurrentStarSystem; public UnityEvent<string> GetStarSystemLoadedEvent() => Main.Instance.OnStarSystemLoaded;
} public UnityEvent<string> GetBodyLoadedEvent() => Main.Instance.OnPlanetLoaded;
public UnityEvent<string> GetChangeStarSystemEvent()
{
return Main.Instance.OnChangeStarSystem;
}
public UnityEvent<string> GetStarSystemLoadedEvent()
{
return Main.Instance.OnStarSystemLoaded;
}
public bool SetDefaultSystem(string name) 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, public GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles,
float scale, bool alignWithNormal) float scale, bool alignWithNormal)
{ {

View File

@ -128,6 +128,13 @@
"description": "Add water to this planet", "description": "Add water to this planet",
"$ref": "#/definitions/WaterModule" "$ref": "#/definitions/WaterModule"
}, },
"extras": {
"type": "object",
"description": "Extra data that may be used by extension mods",
"additionalProperties": {
"type": "object"
}
},
"$schema": { "$schema": {
"type": "string", "type": "string",
"description": "The schema to validate with" "description": "The schema to validate with"

View File

@ -71,6 +71,13 @@
"$ref": "#/definitions/CuriosityColorInfo" "$ref": "#/definitions/CuriosityColorInfo"
} }
}, },
"extras": {
"type": "object",
"description": "Extra data that may be used by extension mods",
"additionalProperties": {
"type": "object"
}
},
"$schema": { "$schema": {
"type": "string", "type": "string",
"description": "The schema to validate with" "description": "The schema to validate with"

View File

@ -11,15 +11,17 @@ namespace NewHorizons.Utility
public class NewHorizonsSystem public class NewHorizonsSystem
{ {
public string UniqueID; public string UniqueID;
public string RelativePath;
public SpawnModule Spawn = null; public SpawnModule Spawn = null;
public SpawnPoint SpawnPoint = null; public SpawnPoint SpawnPoint = null;
public StarSystemConfig Config; public StarSystemConfig Config;
public IModBehaviour Mod; public IModBehaviour Mod;
public NewHorizonsSystem(string uniqueID, StarSystemConfig config, IModBehaviour mod) public NewHorizonsSystem(string uniqueID, StarSystemConfig config, string relativePath, IModBehaviour mod)
{ {
UniqueID = uniqueID; UniqueID = uniqueID;
Config = config; Config = config;
RelativePath = relativePath;
Mod = mod; Mod = mod;
} }
} }

View File

@ -78,16 +78,29 @@ public static class SchemaExporter
{"description", _description} {"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.Properties["extras"] = new JsonSchemaProperty {
schema.Definitions["NomaiCoordinates"].Properties["y"].UniqueItems = true; Type = JsonObjectType.Object,
schema.Definitions["NomaiCoordinates"].Properties["z"].UniqueItems = true; Description = "Extra data that may be used by extension mods",
AllowAdditionalProperties = true,
AdditionalPropertiesSchema = new JsonSchema
{
Type = JsonObjectType.Object
}
};
} }
return schema; return schema;

View File

@ -9,21 +9,99 @@ First create the following interface in your mod:
```cs ```cs
public interface INewHorizons public interface INewHorizons
{ {
void LoadConfigs(IModBehaviour mod); [Obsolete("Create(Dictionary<string, object> config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")]
void Create(Dictionary<string, object> config);
GameObject GetPlanet(string name); [Obsolete("Create(Dictionary<string, object> config) is deprecated, please use LoadConfigs(IModBehaviour mod) instead")]
void Create(Dictionary<string, object> config, IModBehaviour mod);
string GetCurrentStarSystem();
UnityEvent<string> GetChangeStarSystemEvent(); /// <summary>
/// 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.
/// </summary>
void LoadConfigs(IModBehaviour mod);
UnityEvent<string> GetStarSystemLoadedEvent(); /// <summary>
/// Retrieve the root GameObject of a custom planet made by creating configs.
GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles, float scale, bool alignWithNormal); /// Will only work if the planet has been created (see GetStarSystemLoadedEvent)
/// </summary>
GameObject GetPlanet(string name);
string[] GetInstalledAddons(); /// <summary>
} /// The name of the current star system loaded.
/// </summary>
string GetCurrentStarSystem();
/// <summary>
/// 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.
/// </summary>
UnityEvent<string> GetChangeStarSystemEvent();
/// <summary>
/// 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.
/// </summary>
UnityEvent<string> GetStarSystemLoadedEvent();
/// <summary>
/// An event invoked when NH has finished a planet for a star system.
/// Gives the name of the planet that was just loaded.
/// </summary>
UnityEvent<string> GetBodyLoadedEvent();
/// <summary>
/// Uses JSONPath to query a body
/// </summary>
object QueryBody(Type outType, string bodyName, string path);
/// <summary>
/// Uses JSONPath to query a system
/// </summary>
object QuerySystem(Type outType, string path);
/// <summary>
/// Allows you to overwrite the default system. This is where the player is respawned after dying.
/// </summary>
bool SetDefaultSystem(string name);
/// <summary>
/// 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).
/// </summary>
bool ChangeCurrentStarSystem(string name);
/// <summary>
/// Returns the uniqueIDs of each installed NH addon.
/// </summary>
string[] GetInstalledAddons();
/// <summary>
/// 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.
/// </summary>
GameObject SpawnObject(GameObject planet, Sector sector, string propToCopyPath, Vector3 position, Vector3 eulerAngles,
float scale, bool alignWithNormal);
/// <summary>
/// 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.
/// </summary>
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 = "");
/// <summary>
/// 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.
/// </summary>
(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: In your main `ModBehaviour` class you can get the NewHorizons API like so:
@ -33,7 +111,7 @@ public class MyMod : ModBehaviour
{ {
void Start() void Start()
{ {
INewHorizons NewHorizonsAPI = ModHelper.Interaction.GetModApi<INewHorizons>("xen.NewHorizons"); INewHorizons NewHorizonsAPI = ModHelper.Interaction.TryGetModApi<INewHorizons>("xen.NewHorizons");
} }
} }
``` ```

View File

@ -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<INewHorizons>("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<INewHorizons>("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"}!");
```