Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config schema generation #930

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build/deploy-local-smapi.targets
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ This assumes `find-game-folder.targets` has already been imported and validated.
<Copy SourceFiles="$(TargetDir)\$(TargetName).xml" DestinationFolder="$(GamePath)" />
<Copy SourceFiles="$(TargetDir)\SMAPI.config.json" DestinationFiles="$(GamePath)\smapi-internal\config.json" />
<Copy SourceFiles="$(TargetDir)\SMAPI.metadata.json" DestinationFiles="$(GamePath)\smapi-internal\metadata.json" />
<Copy SourceFiles="$(TargetDir)\Namotion.Reflection.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Newtonsoft.Json.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\NJsonSchema.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\NJsonSchema.Annotations.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\TMXTile.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="$(TargetDir)\Pintail.dll" DestinationFolder="$(GamePath)\smapi-internal" />
<Copy SourceFiles="@(TranslationFiles)" DestinationFolder="$(GamePath)\smapi-internal\i18n" />
Expand Down
2 changes: 1 addition & 1 deletion build/unix/prepare-install-package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ for folder in ${folders[@]}; do
cp -r "$smapiBin/i18n" "$bundlePath/smapi-internal"

# bundle smapi-internal
for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pathoschild.Http.Client.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.xml" "System.Net.Http.Formatting.dll"; do
for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Namotion.Reflection.dll" "Newtonsoft.Json.dll" "NJsonSchema.dll" "NJsonSchema.Annotations.dll" "Pathoschild.Http.Client.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.xml" "System.Net.Http.Formatting.dll"; do
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
done

Expand Down
2 changes: 1 addition & 1 deletion build/windows/prepare-install-package.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ foreach ($folder in $folders) {
cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal"

# bundle smapi-internal
foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) {
foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Namotion.Reflection.dll", "Newtonsoft.Json.dll", "NJsonSchema.dll", "NJsonSchema.Annotations.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) {
cp "$smapiBin/$name" "$bundlePath/smapi-internal"
}

Expand Down
1 change: 1 addition & 0 deletions src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.61" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NJsonSchema" Version="11.0.1" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="4.4.0" />
<PackageReference Include="System.Management" Version="8.0.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" />
Expand Down
30 changes: 25 additions & 5 deletions src/SMAPI.Toolkit/Serialization/JsonHelper.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using NJsonSchema;
using StardewModdingAPI.Toolkit.Serialization.Converters;

namespace StardewModdingAPI.Toolkit.Serialization
Expand Down Expand Up @@ -45,7 +45,7 @@ public static JsonSerializerSettings CreateDefaultSettings()
/// <exception cref="JsonReaderException">The file contains invalid JSON.</exception>
public bool ReadJsonFileIfExists<TModel>(string fullPath,
#if NET6_0_OR_GREATER
[NotNullWhen(true)]
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
#endif
out TModel? result
)
Expand Down Expand Up @@ -100,9 +100,7 @@ public void WriteJsonFile<TModel>(string fullPath, TModel model)
throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath));

// create directory if needed
string dir = Path.GetDirectoryName(fullPath)!;
if (dir == null)
throw new ArgumentException("The file path is invalid.", nameof(fullPath));
string dir = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("The file path is invalid.", nameof(fullPath));
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);

Expand All @@ -111,6 +109,28 @@ public void WriteJsonFile<TModel>(string fullPath, TModel model)
File.WriteAllText(fullPath, json);
}

/// <summary>Save a data model schema to a JSON file.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="fullPath">The absolute file path.</param>
/// <exception cref="InvalidOperationException">The given path is empty or invalid.</exception>
public void WriteJsonSchemaFile<TModel>(string fullPath)
where TModel : class
{
// validate
if (string.IsNullOrWhiteSpace(fullPath))
throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath));

// create directory if needed
string dir = Path.GetDirectoryName(fullPath) ?? throw new ArgumentException("The file path is invalid.", nameof(fullPath));
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);

// write file
JsonSchema schema = JsonSchema.FromType<TModel>();
string json = schema.ToJson();
File.WriteAllText(fullPath, json);
}

/// <summary>Deserialize JSON text if possible.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="json">The raw JSON text.</param>
Expand Down
14 changes: 13 additions & 1 deletion src/SMAPI/Framework/ModHelpers/DataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal class DataHelper : BaseHelper, IDataHelper
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod using this instance.</param>
/// <param name="modFolderPath">The absolute path to the mod folder.</param>
/// <param name="jsonHelper">The absolute path to the mod folder.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
public DataHelper(IModMetadata mod, string modFolderPath, JsonHelper jsonHelper)
: base(mod)
{
Expand Down Expand Up @@ -67,6 +67,18 @@ public void WriteJsonFile<TModel>(string path, TModel? data)
File.Delete(path);
}

/// <inheritdoc />
public void WriteJsonSchemaFile<TModel>(string path)
where TModel : class
{
if (!PathUtilities.IsSafeRelativePath(path))
throw new InvalidOperationException($"You must call {nameof(IMod.Helper)}.{nameof(IModHelper.Data)}.{nameof(this.WriteJsonSchemaFile)} with a relative path (without directory climbing).");

path = Path.Combine(this.ModFolderPath, PathUtilities.NormalizePath(path));

this.JsonHelper.WriteJsonSchemaFile<TModel>(path);
}

/****
** Save file
****/
Expand Down
14 changes: 13 additions & 1 deletion src/SMAPI/Framework/ModHelpers/ModHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>Provides simplified APIs for writing mods.</summary>
internal class ModHelper : BaseHelper, IModHelper, IDisposable
{
/*********
** Fields
*********/
/// <summary>Whether to generate a <c>config.schema.json</c> file based on the mod's config model when it's loaded or saved.</summary>
private readonly bool GenerateConfigSchema;


/*********
** Accessors
*********/
Expand Down Expand Up @@ -65,9 +72,10 @@ internal class ModHelper : BaseHelper, IModHelper, IDisposable
/// <param name="reflectionHelper">An API for accessing private game code.</param>
/// <param name="multiplayer">Provides multiplayer utilities.</param>
/// <param name="translationHelper">An API for reading translations stored in the mod's <c>i18n</c> folder.</param>
/// <param name="generateConfigSchema">Whether to generate a <c>config.schema.json</c> file based on the mod's config model when it's loaded or saved.</param>
/// <exception cref="ArgumentNullException">An argument is null or empty.</exception>
/// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception>
public ModHelper(IModMetadata mod, string modDirectory, Func<SInputState> currentInputState, IModEvents events, IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper)
public ModHelper(IModMetadata mod, string modDirectory, Func<SInputState> currentInputState, IModEvents events, IGameContentHelper gameContentHelper, IModContentHelper modContentHelper, IContentPackHelper contentPackHelper, ICommandHelper commandHelper, IDataHelper dataHelper, IModRegistry modRegistry, IReflectionHelper reflectionHelper, IMultiplayerHelper multiplayer, ITranslationHelper translationHelper, bool generateConfigSchema)
: base(mod)
{
// validate directory
Expand All @@ -89,6 +97,7 @@ public ModHelper(IModMetadata mod, string modDirectory, Func<SInputState> curren
this.Multiplayer = multiplayer ?? throw new ArgumentNullException(nameof(multiplayer));
this.Translation = translationHelper ?? throw new ArgumentNullException(nameof(translationHelper));
this.Events = events;
this.GenerateConfigSchema = generateConfigSchema;
}

/****
Expand All @@ -108,6 +117,9 @@ public void WriteConfig<TConfig>(TConfig config)
where TConfig : class, new()
{
this.Data.WriteJsonFile("config.json", config);

if (this.GenerateConfigSchema)
this.Data.WriteJsonSchemaFile<TConfig>("config.schema.json");
}

/****
Expand Down
10 changes: 8 additions & 2 deletions src/SMAPI/Framework/Models/SConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ internal class SConfig
[nameof(RewriteMods)] = true,
[nameof(FixHarmony)] = true,
[nameof(UseCaseInsensitivePaths)] = Constants.Platform is Platform.Android or Platform.Linux,
[nameof(SuppressHarmonyDebugMode)] = true
[nameof(SuppressHarmonyDebugMode)] = true,
[nameof(GenerateConfigSchemas)] = false
};

/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
Expand Down Expand Up @@ -99,6 +100,9 @@ internal class SConfig
/// <summary>The mod IDs SMAPI should load after any other mods.</summary>
public HashSet<string> ModsToLoadLate { get; set; }

/// <summary>Whether to generate <c>config.schema.json</c> files for external tools like mod managers.</summary>
public bool GenerateConfigSchemas { get; set; }


/********
** Public methods
Expand All @@ -122,7 +126,8 @@ internal class SConfig
/// <param name="suppressUpdateChecks"><inheritdoc cref="SuppressUpdateChecks" path="/summary" /></param>
/// <param name="modsToLoadEarly"><inheritdoc cref="ModsToLoadEarly" path="/summary" /></param>
/// <param name="modsToLoadLate"><inheritdoc cref="ModsToLoadLate" path="/summary" /></param>
public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? fixHarmony, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, bool? logTechnicalDetailsForBrokenMods, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate)
/// <param name="generateConfigSchemas"><inheritdoc cref="GenerateConfigSchemas" path="/summary" /></param>
public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsoleInput, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? fixHarmony, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, bool? logTechnicalDetailsForBrokenMods, ColorSchemeConfig consoleColors, bool? suppressHarmonyDebugMode, string[]? suppressUpdateChecks, string[]? modsToLoadEarly, string[]? modsToLoadLate, bool? generateConfigSchemas)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
Expand All @@ -142,6 +147,7 @@ public SConfig(bool developerMode, bool? checkForUpdates, bool? listenForConsole
this.SuppressUpdateChecks = new HashSet<string>(suppressUpdateChecks ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
this.ModsToLoadEarly = new HashSet<string>(modsToLoadEarly ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
this.ModsToLoadLate = new HashSet<string>(modsToLoadLate ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
this.GenerateConfigSchemas = generateConfigSchemas ?? (bool)SConfig.DefaultValues[nameof(this.GenerateConfigSchemas)];
}

/// <summary>Override the value of <see cref="DeveloperMode"/>.</summary>
Expand Down
2 changes: 1 addition & 1 deletion src/SMAPI/Framework/SCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2050,7 +2050,7 @@ IContentPack[] GetContentPacks()
IModRegistry modRegistryHelper = new ModRegistryHelper(mod, this.ModRegistry, proxyFactory, monitor);
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(mod, this.Multiplayer);

modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper);
modHelper = new ModHelper(mod, mod.DirectoryPath, () => this.GetCurrentGameInstance().Input, events, gameContentHelper, modContentHelper, contentPackHelper, commandHelper, dataHelper, modRegistryHelper, reflectionHelper, multiplayerHelper, translationHelper, this.Settings.GenerateConfigSchemas);
}

// init mod
Expand Down
7 changes: 7 additions & 0 deletions src/SMAPI/IDataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ public interface IDataHelper
void WriteJsonFile<TModel>(string path, TModel? data)
where TModel : class;

/// <summary>Save a data model schema to a JSON file in the mod's folder.</summary>
/// <typeparam name="TModel">The model type. This should be a plain class that has public properties for the data you want. The properties can be complex types.</typeparam>
/// <param name="path">The file path relative to the mod folder.</param>
/// <exception cref="InvalidOperationException">The <paramref name="path"/> is not relative or contains directory climbing (../).</exception>
void WriteJsonSchemaFile<TModel>(string path)
where TModel : class;

/****
** Save file
****/
Expand Down
9 changes: 8 additions & 1 deletion src/SMAPI/SMAPI.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,5 +172,12 @@ in future SMAPI versions.
* the mod author.
*/
"ModsToLoadEarly": [],
"ModsToLoadLate": []
"ModsToLoadLate": [],

/**
* Whether to generate a `config.schema.json` file next to each mod's `config.json` file.
*
* This can be used by separate tools like mod managers to enable config editing features.
*/
"GenerateConfigSchemas": false
}