summaryrefslogtreecommitdiff
path: root/src/SMAPI.Toolkit
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-05-01 18:16:09 -0400
committerJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2022-05-01 18:16:09 -0400
commitc8ad50dad1d706a1901798f9396f6becfea36c0e (patch)
tree28bd818a5db39ec5ece1bd141a28de955950463b /src/SMAPI.Toolkit
parent451b70953ff4c0b1b27ae0de203ad99379b45b2a (diff)
parentf78093bdb58d477b400cde3f19b70ffd6ddf833d (diff)
downloadSMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.gz
SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.tar.bz2
SMAPI-c8ad50dad1d706a1901798f9396f6becfea36c0e.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Toolkit')
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs21
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs7
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs37
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs29
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs13
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs7
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs23
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs161
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs31
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs4
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs96
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs21
-rw-r--r--src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs36
-rw-r--r--src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs8
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs2
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs8
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs30
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs10
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs26
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs14
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs12
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs76
-rw-r--r--src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs11
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs32
-rw-r--r--src/SMAPI.Toolkit/ModToolkit.cs12
-rw-r--r--src/SMAPI.Toolkit/Properties/AssemblyInfo.cs1
-rw-r--r--src/SMAPI.Toolkit/SemanticVersion.cs83
-rw-r--r--src/SMAPI.Toolkit/SemanticVersionComparer.cs6
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs4
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs8
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs12
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs20
-rw-r--r--src/SMAPI.Toolkit/Serialization/InternalExtensions.cs6
-rw-r--r--src/SMAPI.Toolkit/Serialization/JsonHelper.cs24
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/Manifest.cs101
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs33
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs46
-rw-r--r--src/SMAPI.Toolkit/Serialization/SParseException.cs2
-rw-r--r--src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs139
-rw-r--r--src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs2
-rw-r--r--src/SMAPI.Toolkit/Utilities/PathUtilities.cs33
41 files changed, 852 insertions, 395 deletions
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
index 2f58a3f1..4fc4ea54 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
/// <summary>Metadata about a mod.</summary>
@@ -7,15 +9,26 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Accessors
*********/
/// <summary>The mod's unique ID (if known).</summary>
- public string ID { get; set; }
+ public string ID { get; }
/// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary>
- public ModEntryVersionModel SuggestedUpdate { get; set; }
+ public ModEntryVersionModel? SuggestedUpdate { get; set; }
/// <summary>Optional extended data which isn't needed for update checks.</summary>
- public ModExtendedMetadataModel Metadata { get; set; }
+ public ModExtendedMetadataModel? Metadata { get; set; }
/// <summary>The errors that occurred while fetching update data.</summary>
- public string[] Errors { get; set; } = new string[0];
+ public string[] Errors { get; set; } = Array.Empty<string>();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID (if known).</param>
+ public ModEntryModel(string id)
+ {
+ this.ID = id;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
index 188db31d..a1e78986 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
@@ -11,19 +11,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
*********/
/// <summary>The version number.</summary>
[JsonConverter(typeof(NonStandardSemanticVersionConverter))]
- public ISemanticVersion Version { get; set; }
+ public ISemanticVersion Version { get; }
/// <summary>The mod page URL.</summary>
- public string Url { get; set; }
+ public string Url { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
- public ModEntryVersionModel() { }
-
- /// <summary>Construct an instance.</summary>
/// <param name="version">The version number.</param>
/// <param name="url">The mod page URL.</param>
public ModEntryVersionModel(ISemanticVersion version, string url)
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
index 5c2ce366..272a2063 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
@@ -17,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Mod info
****/
/// <summary>The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates).</summary>
- public string[] ID { get; set; } = new string[0];
+ public string[] ID { get; set; } = Array.Empty<string>();
/// <summary>The mod's display name.</summary>
- public string Name { get; set; }
+ public string? Name { get; set; }
/// <summary>The mod ID on Nexus.</summary>
public int? NexusID { get; set; }
@@ -32,31 +33,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public int? CurseForgeID { get; set; }
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
- public string CurseForgeKey { get; set; }
+ public string? CurseForgeKey { get; set; }
/// <summary>The mod ID in the ModDrop mod repo.</summary>
public int? ModDropID { get; set; }
/// <summary>The GitHub repository in the form 'owner/repo'.</summary>
- public string GitHubRepo { get; set; }
+ public string? GitHubRepo { get; set; }
/// <summary>The URL to a non-GitHub source repo.</summary>
- public string CustomSourceUrl { get; set; }
+ public string? CustomSourceUrl { get; set; }
/// <summary>The custom mod page URL (if applicable).</summary>
- public string CustomUrl { get; set; }
+ public string? CustomUrl { get; set; }
/// <summary>The main version.</summary>
- public ModEntryVersionModel Main { get; set; }
+ public ModEntryVersionModel? Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
- public ModEntryVersionModel Optional { get; set; }
+ public ModEntryVersionModel? Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
- public ModEntryVersionModel Unofficial { get; set; }
+ public ModEntryVersionModel? Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</summary>
- public ModEntryVersionModel UnofficialForBeta { get; set; }
+ public ModEntryVersionModel? UnofficialForBeta { get; set; }
/****
** Stable compatibility
@@ -66,10 +67,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public WikiCompatibilityStatus? CompatibilityStatus { get; set; }
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
- public string CompatibilitySummary { get; set; }
+ public string? CompatibilitySummary { get; set; }
/// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
- public string BrokeIn { get; set; }
+ public string? BrokeIn { get; set; }
/****
** Beta compatibility
@@ -79,22 +80,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; }
/// <summary>The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatting.</summary>
- public string BetaCompatibilitySummary { get; set; }
+ public string? BetaCompatibilitySummary { get; set; }
/// <summary>The beta game or SMAPI version which broke this mod, if applicable.</summary>
- public string BetaBrokeIn { get; set; }
+ public string? BetaBrokeIn { get; set; }
/****
** Version mappings
****/
/// <summary>A serialized change descriptor to apply to the local version during update checks (see <see cref="ChangeDescriptor"/>).</summary>
- public string ChangeLocalVersions { get; set; }
+ public string? ChangeLocalVersions { get; set; }
/// <summary>A serialized change descriptor to apply to the remote version during update checks (see <see cref="ChangeDescriptor"/>).</summary>
- public string ChangeRemoteVersions { get; set; }
+ public string? ChangeRemoteVersions { get; set; }
/// <summary>A serialized change descriptor to apply to the update keys during update checks (see <see cref="ChangeDescriptor"/>).</summary>
- public string ChangeUpdateKeys { get; set; }
+ public string? ChangeUpdateKeys { get; set; }
/*********
@@ -110,7 +111,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <param name="optional">The latest optional version, if newer than <paramref name="main"/>.</param>
/// <param name="unofficial">The latest unofficial version, if newer than <paramref name="main"/> and <paramref name="optional"/>.</param>
/// <param name="unofficialForBeta">The latest unofficial version for the current Stardew Valley or SMAPI beta, if any.</param>
- public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db, ModEntryVersionModel main, ModEntryVersionModel optional, ModEntryVersionModel unofficial, ModEntryVersionModel unofficialForBeta)
+ public ModExtendedMetadataModel(WikiModEntry? wiki, ModDataRecord? db, ModEntryVersionModel? main, ModEntryVersionModel? optional, ModEntryVersionModel? unofficial, ModEntryVersionModel? unofficialForBeta)
{
// versions
this.Main = main;
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
index bf81e102..9c11e1db 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Linq;
+
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
/// <summary>Specifies the identifiers for a mod to match.</summary>
@@ -7,37 +10,39 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Accessors
*********/
/// <summary>The unique mod ID.</summary>
- public string ID { get; set; }
+ public string ID { get; }
/// <summary>The namespaced mod update keys (if available).</summary>
- public string[] UpdateKeys { get; set; }
+ public string[] UpdateKeys { get; private set; }
/// <summary>The mod version installed by the local player. This is used for version mapping in some cases.</summary>
- public ISemanticVersion InstalledVersion { get; set; }
+ public ISemanticVersion? InstalledVersion { get; }
/// <summary>Whether the installed version is broken or could not be loaded.</summary>
- public bool IsBroken { get; set; }
+ public bool IsBroken { get; }
/*********
** Public methods
*********/
- /// <summary>Construct an empty instance.</summary>
- public ModSearchEntryModel()
- {
- // needed for JSON deserializing
- }
-
/// <summary>Construct an instance.</summary>
/// <param name="id">The unique mod ID.</param>
/// <param name="installedVersion">The version installed by the local player. This is used for version mapping in some cases.</param>
/// <param name="updateKeys">The namespaced mod update keys (if available).</param>
/// <param name="isBroken">Whether the installed version is broken or could not be loaded.</param>
- public ModSearchEntryModel(string id, ISemanticVersion installedVersion, string[] updateKeys, bool isBroken = false)
+ public ModSearchEntryModel(string id, ISemanticVersion? installedVersion, string[]? updateKeys, bool isBroken = false)
{
this.ID = id;
this.InstalledVersion = installedVersion;
- this.UpdateKeys = updateKeys ?? new string[0];
+ this.UpdateKeys = updateKeys ?? Array.Empty<string>();
+ this.IsBroken = isBroken;
+ }
+
+ /// <summary>Add update keys for the mod.</summary>
+ /// <param name="updateKeys">The update keys to add.</param>
+ public void AddUpdateKeys(params string[] updateKeys)
+ {
+ this.UpdateKeys = this.UpdateKeys.Concat(updateKeys).ToArray();
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
index 73698173..3c74bab0 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
using StardewModdingAPI.Toolkit.Utilities;
@@ -22,16 +24,23 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
public ISemanticVersion GameVersion { get; set; }
/// <summary>The OS on which the player plays.</summary>
- public Platform? Platform { get; set; }
+ public Platform Platform { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an empty instance.</summary>
+ [Obsolete("This constructor only exists to support ASP.NET model binding, and shouldn't be used directly.")]
+ [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used by ASP.NET model binding.")]
public ModSearchModel()
{
- // needed for JSON deserializing
+ // ASP.NET Web API needs a public empty constructor for top-level request models, and
+ // it'll fail if the other constructor is marked with [JsonConstructor]. Apparently
+ // it's fine with non-empty constructors in nested models like ModSearchEntryModel.
+ this.Mods = Array.Empty<ModSearchEntryModel>();
+ this.ApiVersion = null!;
+ this.GameVersion = null!;
}
/// <summary>Construct an instance.</summary>
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
index c2d906a0..d4282617 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
@@ -62,15 +62,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
private TResult Post<TBody, TResult>(string url, TBody content)
{
// note: avoid HttpClient for macOS compatibility
- using WebClient client = new WebClient();
+ using WebClient client = new();
- Uri fullUrl = new Uri(this.BaseUrl, url);
+ Uri fullUrl = new(this.BaseUrl, url);
string data = JsonConvert.SerializeObject(content);
client.Headers["Content-Type"] = "application/json";
client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
string response = client.UploadString(fullUrl, data);
- return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
+ return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings)
+ ?? throw new InvalidOperationException($"Could not parse the response from POST {url}.");
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs
index f1feb44b..a2497dea 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/ChangeDescriptor.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
@@ -47,11 +48,19 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Apply the change descriptors to a comma-delimited field.</summary>
/// <param name="rawField">The raw field text.</param>
/// <returns>Returns the modified field.</returns>
- public string ApplyToCopy(string rawField)
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("rawField")]
+#endif
+ public string? ApplyToCopy(string? rawField)
{
// get list
List<string> values = !string.IsNullOrWhiteSpace(rawField)
- ? new List<string>(rawField.Split(','))
+ ? new List<string>(
+ from field in rawField.Split(',')
+ let value = field.Trim()
+ where value.Length > 0
+ select value
+ )
: new List<string>();
// apply changes
@@ -73,12 +82,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
for (int i = values.Count - 1; i >= 0; i--)
{
- string value = this.FormatValue(values[i]?.Trim() ?? string.Empty);
+ string value = this.FormatValue(values[i].Trim());
if (this.Remove.Contains(value))
values.RemoveAt(i);
- else if (this.Replace.TryGetValue(value, out string newValue))
+ else if (this.Replace.TryGetValue(value, out string? newValue))
values[i] = newValue;
}
}
@@ -86,7 +95,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
// add values
if (this.Add.Any())
{
- HashSet<string> curValues = new HashSet<string>(values.Select(p => p?.Trim() ?? string.Empty), StringComparer.OrdinalIgnoreCase);
+ HashSet<string> curValues = new HashSet<string>(values.Select(p => p.Trim()), StringComparer.OrdinalIgnoreCase);
foreach (string add in this.Add)
{
if (!curValues.Contains(add))
@@ -119,7 +128,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="descriptor">The raw change descriptor.</param>
/// <param name="errors">The human-readable error message describing any invalid values that were ignored.</param>
/// <param name="formatValue">Format a raw value into a normalized form if needed.</param>
- public static ChangeDescriptor Parse(string descriptor, out string[] errors, Func<string, string> formatValue = null)
+ public static ChangeDescriptor Parse(string? descriptor, out string[] errors, Func<string, string>? formatValue = null)
{
// init
formatValue ??= p => p;
@@ -179,7 +188,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
errors = rawErrors.ToArray();
}
else
- errors = new string[0];
+ errors = Array.Empty<string>();
// build model
return new ChangeDescriptor(
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
index f85e82e1..7f06d170 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
@@ -51,8 +51,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
doc.LoadHtml(html);
// fetch game versions
- string stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText;
- string betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText;
+ string? stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText;
+ string? betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText;
if (betaVersion == stableVersion)
betaVersion = null;
@@ -63,9 +63,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
if (modNodes == null)
throw new InvalidOperationException("Can't parse wiki compatibility list, no mod data overrides section found.");
- foreach (var entry in this.ParseOverrideEntries(modNodes))
+ foreach (WikiDataOverrideEntry entry in this.ParseOverrideEntries(modNodes))
{
- if (entry.Ids?.Any() != true || !entry.HasChanges)
+ if (entry.Ids.Any() != true || !entry.HasChanges)
continue;
foreach (string id in entry.Ids)
@@ -83,18 +83,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
}
// build model
- return new WikiModList
- {
- StableVersion = stableVersion,
- BetaVersion = betaVersion,
- Mods = mods
- };
+ return new WikiModList(
+ stableVersion: stableVersion,
+ betaVersion: betaVersion,
+ mods: mods
+ );
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
- this.Client?.Dispose();
+ this.Client.Dispose();
}
@@ -116,71 +115,68 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id");
int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id");
int? curseForgeID = this.GetAttributeAsNullableInt(node, "data-curseforge-id");
- string curseForgeKey = this.GetAttribute(node, "data-curseforge-key");
+ string? curseForgeKey = this.GetAttribute(node, "data-curseforge-key");
int? modDropID = this.GetAttributeAsNullableInt(node, "data-moddrop-id");
- string githubRepo = this.GetAttribute(node, "data-github");
- string customSourceUrl = this.GetAttribute(node, "data-custom-source");
- string customUrl = this.GetAttribute(node, "data-url");
- string anchor = this.GetAttribute(node, "id");
- string contentPackFor = this.GetAttribute(node, "data-content-pack-for");
- string devNote = this.GetAttribute(node, "data-dev-note");
- string pullRequestUrl = this.GetAttribute(node, "data-pr");
+ string? githubRepo = this.GetAttribute(node, "data-github");
+ string? customSourceUrl = this.GetAttribute(node, "data-custom-source");
+ string? customUrl = this.GetAttribute(node, "data-url");
+ string? anchor = this.GetAttribute(node, "id");
+ string? contentPackFor = this.GetAttribute(node, "data-content-pack-for");
+ string? devNote = this.GetAttribute(node, "data-dev-note");
+ string? pullRequestUrl = this.GetAttribute(node, "data-pr");
// parse stable compatibility
- WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
- {
- Status = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok,
- BrokeIn = this.GetAttribute(node, "data-broke-in"),
- UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"),
- UnofficialUrl = this.GetAttribute(node, "data-unofficial-url"),
- Summary = this.GetInnerHtml(node, "mod-summary")?.Trim()
- };
+ WikiCompatibilityInfo compatibility = new(
+ status: this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-status") ?? WikiCompatibilityStatus.Ok,
+ brokeIn: this.GetAttribute(node, "data-broke-in"),
+ unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-unofficial-version"),
+ unofficialUrl: this.GetAttribute(node, "data-unofficial-url"),
+ summary: this.GetInnerHtml(node, "mod-summary")?.Trim()
+ );
// parse beta compatibility
- WikiCompatibilityInfo betaCompatibility = null;
+ WikiCompatibilityInfo? betaCompatibility = null;
{
WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum<WikiCompatibilityStatus>(node, "data-beta-status");
if (betaStatus.HasValue)
{
- betaCompatibility = new WikiCompatibilityInfo
- {
- Status = betaStatus.Value,
- BrokeIn = this.GetAttribute(node, "data-beta-broke-in"),
- UnofficialVersion = this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"),
- UnofficialUrl = this.GetAttribute(node, "data-beta-unofficial-url"),
- Summary = this.GetInnerHtml(node, "mod-beta-summary")
- };
+ betaCompatibility = new WikiCompatibilityInfo(
+ status: betaStatus.Value,
+ brokeIn: this.GetAttribute(node, "data-beta-broke-in"),
+ unofficialVersion: this.GetAttributeAsSemanticVersion(node, "data-beta-unofficial-version"),
+ unofficialUrl: this.GetAttribute(node, "data-beta-unofficial-url"),
+ summary: this.GetInnerHtml(node, "mod-beta-summary")
+ );
}
}
// find data overrides
- WikiDataOverrideEntry overrides = ids
+ WikiDataOverrideEntry? overrides = ids
.Select(id => overridesById.TryGetValue(id, out overrides) ? overrides : null)
.FirstOrDefault(p => p != null);
// yield model
- yield return new WikiModEntry
- {
- ID = ids,
- Name = names,
- Author = authors,
- NexusID = nexusID,
- ChucklefishID = chucklefishID,
- CurseForgeID = curseForgeID,
- CurseForgeKey = curseForgeKey,
- ModDropID = modDropID,
- GitHubRepo = githubRepo,
- CustomSourceUrl = customSourceUrl,
- CustomUrl = customUrl,
- ContentPackFor = contentPackFor,
- Compatibility = compatibility,
- BetaCompatibility = betaCompatibility,
- Warnings = warnings,
- PullRequestUrl = pullRequestUrl,
- DevNote = devNote,
- Overrides = overrides,
- Anchor = anchor
- };
+ yield return new WikiModEntry(
+ id: ids,
+ name: names,
+ author: authors,
+ nexusId: nexusID,
+ chucklefishId: chucklefishID,
+ curseForgeId: curseForgeID,
+ curseForgeKey: curseForgeKey,
+ modDropId: modDropID,
+ githubRepo: githubRepo,
+ customSourceUrl: customSourceUrl,
+ customUrl: customUrl,
+ contentPackFor: contentPackFor,
+ compatibility: compatibility,
+ betaCompatibility: betaCompatibility,
+ warnings: warnings,
+ pullRequestUrl: pullRequestUrl,
+ devNote: devNote,
+ overrides: overrides,
+ anchor: anchor
+ );
}
}
@@ -194,10 +190,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
Ids = this.GetAttributeAsCsv(node, "data-id"),
ChangeLocalVersions = this.GetAttributeAsChangeDescriptor(node, "data-local-version",
- raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw
+ raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw
),
ChangeRemoteVersions = this.GetAttributeAsChangeDescriptor(node, "data-remote-version",
- raw => SemanticVersion.TryParse(raw, out ISemanticVersion version) ? version.ToString() : raw
+ raw => SemanticVersion.TryParse(raw, out ISemanticVersion? version) ? version.ToString() : raw
),
ChangeUpdateKeys = this.GetAttributeAsChangeDescriptor(node, "data-update-keys",
@@ -210,7 +206,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Get an attribute value.</summary>
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
- private string GetAttribute(HtmlNode element, string name)
+ private string? GetAttribute(HtmlNode element, string name)
{
string value = element.GetAttributeValue(name, null);
if (string.IsNullOrWhiteSpace(value))
@@ -223,9 +219,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
/// <param name="formatValue">Format an raw entry value when applying changes.</param>
- private ChangeDescriptor GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func<string, string> formatValue)
+ private ChangeDescriptor? GetAttributeAsChangeDescriptor(HtmlNode element, string name, Func<string, string> formatValue)
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
return raw != null
? ChangeDescriptor.Parse(raw, out _, formatValue)
: null;
@@ -236,10 +232,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="name">The attribute name.</param>
private string[] GetAttributeAsCsv(HtmlNode element, string name)
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
return !string.IsNullOrWhiteSpace(raw)
? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray()
- : new string[0];
+ : Array.Empty<string>();
}
/// <summary>Get an attribute value and parse it as an enum value.</summary>
@@ -248,7 +244,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="name">The attribute name.</param>
private TEnum? GetAttributeAsEnum<TEnum>(HtmlNode element, string name) where TEnum : struct
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
if (raw == null)
return null;
if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value))
@@ -259,10 +255,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Get an attribute value and parse it as a semantic version.</summary>
/// <param name="element">The element whose attributes to read.</param>
/// <param name="name">The attribute name.</param>
- private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name)
+ private ISemanticVersion? GetAttributeAsSemanticVersion(HtmlNode element, string name)
{
- string raw = this.GetAttribute(element, name);
- return SemanticVersion.TryParse(raw, out ISemanticVersion version)
+ string? raw = this.GetAttribute(element, name);
+ return SemanticVersion.TryParse(raw, out ISemanticVersion? version)
? version
: null;
}
@@ -272,7 +268,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <param name="name">The attribute name.</param>
private int? GetAttributeAsNullableInt(HtmlNode element, string name)
{
- string raw = this.GetAttribute(element, name);
+ string? raw = this.GetAttribute(element, name);
if (raw != null && int.TryParse(raw, out int value))
return value;
return null;
@@ -281,7 +277,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>Get the text of an element with the given class name.</summary>
/// <param name="container">The metadata container.</param>
/// <param name="className">The field name.</param>
- private string GetInnerHtml(HtmlNode container, string className)
+ private string? GetInnerHtml(HtmlNode container, string className)
{
return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml;
}
@@ -291,8 +287,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
private class ResponseModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The parse API results.</summary>
- public ResponseParseModel Parse { get; set; }
+ public ResponseParseModel Parse { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="parse">The parse API results.</param>
+ public ResponseModel(ResponseParseModel parse)
+ {
+ this.Parse = parse;
+ }
}
/// <summary>The inner response model for the MediaWiki parse API.</summary>
@@ -301,8 +311,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
private class ResponseParseModel
{
+ /*********
+ ** Accessors
+ *********/
/// <summary>The parsed text.</summary>
- public IDictionary<string, string> Text { get; set; }
+ public IDictionary<string, string> Text { get; } = new Dictionary<string, string>();
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
index 204acd2b..71c90d0c 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs
@@ -7,18 +7,37 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
** Accessors
*********/
/// <summary>The compatibility status.</summary>
- public WikiCompatibilityStatus Status { get; set; }
+ public WikiCompatibilityStatus Status { get; }
/// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
- public string Summary { get; set; }
+ public string? Summary { get; }
- /// <summary>The game or SMAPI version which broke this mod (if applicable).</summary>
- public string BrokeIn { get; set; }
+ /// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
+ public string? BrokeIn { get; }
/// <summary>The version of the latest unofficial update, if applicable.</summary>
- public ISemanticVersion UnofficialVersion { get; set; }
+ public ISemanticVersion? UnofficialVersion { get; }
/// <summary>The URL to the latest unofficial update, if applicable.</summary>
- public string UnofficialUrl { get; set; }
+ public string? UnofficialUrl { get; }
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="status">The compatibility status.</param>
+ /// <param name="summary">The human-readable summary of the compatibility status or workaround, without HTML formatting.</param>
+ /// <param name="brokeIn">The game or SMAPI version which broke this mod, if applicable.</param>
+ /// <param name="unofficialVersion">The version of the latest unofficial update, if applicable.</param>
+ /// <param name="unofficialUrl">The URL to the latest unofficial update, if applicable.</param>
+ public WikiCompatibilityInfo(WikiCompatibilityStatus status, string? summary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl)
+ {
+ this.Status = status;
+ this.Summary = summary;
+ this.BrokeIn = brokeIn;
+ this.UnofficialVersion = unofficialVersion;
+ this.UnofficialUrl = unofficialUrl;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs
index 0587e09d..a6f5a88f 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiDataOverrideEntry.cs
@@ -1,4 +1,4 @@
-#nullable enable
+using System;
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
@@ -9,7 +9,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
** Accessors
*********/
/// <summary>The unique mod IDs for the mods to override.</summary>
- public string[] Ids { get; set; } = new string[0];
+ public string[] Ids { get; set; } = Array.Empty<string>();
/// <summary>Maps local versions to a semantic version for update checks.</summary>
public ChangeDescriptor? ChangeLocalVersions { get; set; }
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
index 4e0104da..fc50125f 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
/// <summary>A mod entry in the wiki list.</summary>
@@ -6,64 +8,114 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/*********
** Accessors
*********/
- /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order.</summary>
- public string[] ID { get; set; }
+ /// <summary>The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order.</summary>
+ public string[] ID { get; }
/// <summary>The mod's display name. If the mod has multiple names, the first one is the most canonical name.</summary>
- public string[] Name { get; set; }
+ public string[] Name { get; }
- /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
- public string[] Author { get; set; }
+ /// <summary>The mod's author name. If the author has multiple names, the first one is the most canonical name.</summary>
+ public string[] Author { get; }
/// <summary>The mod ID on Nexus.</summary>
- public int? NexusID { get; set; }
+ public int? NexusID { get; }
/// <summary>The mod ID in the Chucklefish mod repo.</summary>
- public int? ChucklefishID { get; set; }
+ public int? ChucklefishID { get; }
/// <summary>The mod ID in the CurseForge mod repo.</summary>
- public int? CurseForgeID { get; set; }
+ public int? CurseForgeID { get; }
/// <summary>The mod key in the CurseForge mod repo (used in mod page URLs).</summary>
- public string CurseForgeKey { get; set; }
+ public string? CurseForgeKey { get; }
/// <summary>The mod ID in the ModDrop mod repo.</summary>
- public int? ModDropID { get; set; }
+ public int? ModDropID { get; }
/// <summary>The GitHub repository in the form 'owner/repo'.</summary>
- public string GitHubRepo { get; set; }
+ public string? GitHubRepo { get; }
/// <summary>The URL to a non-GitHub source repo.</summary>
- public string CustomSourceUrl { get; set; }
+ public string? CustomSourceUrl { get; }
/// <summary>The custom mod page URL (if applicable).</summary>
- public string CustomUrl { get; set; }
+ public string? CustomUrl { get; }
/// <summary>The name of the mod which loads this content pack, if applicable.</summary>
- public string ContentPackFor { get; set; }
+ public string? ContentPackFor { get; }
/// <summary>The mod's compatibility with the latest stable version of the game.</summary>
- public WikiCompatibilityInfo Compatibility { get; set; }
+ public WikiCompatibilityInfo Compatibility { get; }
/// <summary>The mod's compatibility with the latest beta version of the game (if any).</summary>
- public WikiCompatibilityInfo BetaCompatibility { get; set; }
+ public WikiCompatibilityInfo? BetaCompatibility { get; }
/// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="BetaCompatibility"/> should be used for beta versions of SMAPI instead of <see cref="Compatibility"/>.</summary>
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(WikiModEntry.BetaCompatibility))]
+#endif
public bool HasBetaInfo => this.BetaCompatibility != null;
/// <summary>The human-readable warnings for players about this mod.</summary>
- public string[] Warnings { get; set; }
+ public string[] Warnings { get; }
/// <summary>The URL of the pull request which submits changes for an unofficial update to the author, if any.</summary>
- public string PullRequestUrl { get; set; }
+ public string? PullRequestUrl { get; }
- /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
- public string DevNote { get; set; }
+ /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests.</summary>
+ public string? DevNote { get; }
/// <summary>The data overrides to apply to the mod's manifest or remote mod page data, if any.</summary>
- public WikiDataOverrideEntry Overrides { get; set; }
+ public WikiDataOverrideEntry? Overrides { get; }
/// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
- public string Anchor { get; set; }
+ public string? Anchor { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order.</param>
+ /// <param name="name">The mod's display name. If the mod has multiple names, the first one is the most canonical name.</param>
+ /// <param name="author">The mod's author name. If the author has multiple names, the first one is the most canonical name.</param>
+ /// <param name="nexusId">The mod ID on Nexus.</param>
+ /// <param name="chucklefishId">The mod ID in the Chucklefish mod repo.</param>
+ /// <param name="curseForgeId">The mod ID in the CurseForge mod repo.</param>
+ /// <param name="curseForgeKey">The mod ID in the CurseForge mod repo.</param>
+ /// <param name="modDropId">The mod ID in the ModDrop mod repo.</param>
+ /// <param name="githubRepo">The GitHub repository in the form 'owner/repo'.</param>
+ /// <param name="customSourceUrl">The URL to a non-GitHub source repo.</param>
+ /// <param name="customUrl">The custom mod page URL (if applicable).</param>
+ /// <param name="contentPackFor">The name of the mod which loads this content pack, if applicable.</param>
+ /// <param name="compatibility">The mod's compatibility with the latest stable version of the game.</param>
+ /// <param name="betaCompatibility">The mod's compatibility with the latest beta version of the game (if any).</param>
+ /// <param name="warnings">The human-readable warnings for players about this mod.</param>
+ /// <param name="pullRequestUrl">The URL of the pull request which submits changes for an unofficial update to the author, if any.</param>
+ /// <param name="devNote">Special notes intended for developers who maintain unofficial updates or submit pull requests.</param>
+ /// <param name="overrides">The data overrides to apply to the mod's manifest or remote mod page data, if any.</param>
+ /// <param name="anchor">The link anchor for the mod entry in the wiki compatibility list.</param>
+ public WikiModEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, string? curseForgeKey, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, WikiCompatibilityInfo compatibility, WikiCompatibilityInfo? betaCompatibility, string[] warnings, string? pullRequestUrl, string? devNote, WikiDataOverrideEntry? overrides, string? anchor)
+ {
+ this.ID = id;
+ this.Name = name;
+ this.Author = author;
+ this.NexusID = nexusId;
+ this.ChucklefishID = chucklefishId;
+ this.CurseForgeID = curseForgeId;
+ this.CurseForgeKey = curseForgeKey;
+ this.ModDropID = modDropId;
+ this.GitHubRepo = githubRepo;
+ this.CustomSourceUrl = customSourceUrl;
+ this.CustomUrl = customUrl;
+ this.ContentPackFor = contentPackFor;
+ this.Compatibility = compatibility;
+ this.BetaCompatibility = betaCompatibility;
+ this.Warnings = warnings;
+ this.PullRequestUrl = pullRequestUrl;
+ this.DevNote = devNote;
+ this.Overrides = overrides;
+ this.Anchor = anchor;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
index 0d614f28..24548078 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs
@@ -7,12 +7,27 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
** Accessors
*********/
/// <summary>The stable game version.</summary>
- public string StableVersion { get; set; }
+ public string? StableVersion { get; }
/// <summary>The beta game version (if any).</summary>
- public string BetaVersion { get; set; }
+ public string? BetaVersion { get; }
/// <summary>The mods on the wiki.</summary>
- public WikiModEntry[] Mods { get; set; }
+ public WikiModEntry[] Mods { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="stableVersion">The stable game version.</param>
+ /// <param name="betaVersion">The beta game version (if any).</param>
+ /// <param name="mods">The mods on the wiki.</param>
+ public WikiModList(string? stableVersion, string? betaVersion, WikiModEntry[] mods)
+ {
+ this.StableVersion = stableVersion;
+ this.BetaVersion = betaVersion;
+ this.Mods = mods;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
index 8d4198de..8e1538a5 100644
--- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Xml.Linq;
@@ -13,6 +14,7 @@ using Microsoft.Win32;
namespace StardewModdingAPI.Toolkit.Framework.GameScanning
{
/// <summary>Finds installed game folders.</summary>
+ [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These are valid game install paths.")]
public class GameScanner
{
/*********
@@ -39,13 +41,13 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
IEnumerable<string> paths = this
.GetCustomInstallPaths()
.Concat(this.GetDefaultInstallPaths())
- .Select(PathUtilities.NormalizePath)
+ .Select(path => PathUtilities.NormalizePath(path))
.Distinct(StringComparer.OrdinalIgnoreCase);
// yield valid folders
foreach (string path in paths)
{
- DirectoryInfo folder = new DirectoryInfo(path);
+ DirectoryInfo folder = new(path);
if (this.LooksLikeGameFolder(folder))
yield return folder;
}
@@ -78,10 +80,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
return GameFolderType.NoGameFound;
// get assembly version
- Version version;
+ Version? version;
try
{
version = AssemblyName.GetAssemblyName(executable.FullName).Version;
+ if (version == null)
+ return GameFolderType.InvalidUnknown;
}
catch
{
@@ -121,7 +125,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
case Platform.Linux:
case Platform.Mac:
{
- string home = Environment.GetEnvironmentVariable("HOME");
+ string home = Environment.GetEnvironmentVariable("HOME")!;
// Linux
yield return $"{home}/GOG Games/Stardew Valley/game";
@@ -146,13 +150,13 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
};
foreach (var pair in registryKeys)
{
- string path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value);
+ string? path = this.GetLocalMachineRegistryValue(pair.Key, pair.Value);
if (!string.IsNullOrWhiteSpace(path))
yield return path;
}
// via Steam library path
- string steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
+ string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath");
if (steamPath != null)
yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
#endif
@@ -186,12 +190,12 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
private IEnumerable<string> GetCustomInstallPaths()
{
// get home path
- string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME");
+ string homePath = Environment.GetEnvironmentVariable(this.Platform == Platform.Windows ? "USERPROFILE" : "HOME")!;
if (string.IsNullOrWhiteSpace(homePath))
yield break;
// get targets file
- FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets"));
+ FileInfo file = new(Path.Combine(homePath, "stardewvalley.targets"));
if (!file.Exists)
yield break;
@@ -208,7 +212,7 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
}
// get install path
- XElement element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace
+ XElement? element = root.XPathSelectElement("//*[local-name() = 'GamePath']"); // can't use '//GamePath' due to the default namespace
if (!string.IsNullOrWhiteSpace(element?.Value))
yield return element.Value.Trim();
}
@@ -217,27 +221,27 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
/// <summary>Get the value of a key in the Windows HKLM registry.</summary>
/// <param name="key">The full path of the registry key relative to HKLM.</param>
/// <param name="name">The name of the value.</param>
- private string GetLocalMachineRegistryValue(string key, string name)
+ private string? GetLocalMachineRegistryValue(string key, string name)
{
RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine;
- RegistryKey openKey = localMachine.OpenSubKey(key);
+ RegistryKey? openKey = localMachine.OpenSubKey(key);
if (openKey == null)
return null;
using (openKey)
- return (string)openKey.GetValue(name);
+ return (string?)openKey.GetValue(name);
}
/// <summary>Get the value of a key in the Windows HKCU registry.</summary>
/// <param name="key">The full path of the registry key relative to HKCU.</param>
/// <param name="name">The name of the value.</param>
- private string GetCurrentUserRegistryValue(string key, string name)
+ private string? GetCurrentUserRegistryValue(string key, string name)
{
- RegistryKey currentuser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser;
- RegistryKey openKey = currentuser.OpenSubKey(key);
+ RegistryKey currentUser = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64) : Registry.CurrentUser;
+ RegistryKey? openKey = currentUser.OpenSubKey(key);
if (openKey == null)
return null;
using (openKey)
- return (string)openKey.GetValue(name);
+ return (string?)openKey.GetValue(name);
}
#endif
}
diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
index 8b6eb5fb..6978567e 100644
--- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
@@ -57,11 +57,13 @@ namespace StardewModdingAPI.Toolkit.Framework
#if SMAPI_FOR_WINDOWS
try
{
- return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
+ string? result = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
.Get()
.Cast<ManagementObject>()
.Select(entry => entry.GetPropertyValue("Caption").ToString())
.FirstOrDefault();
+
+ return result ?? "Windows";
}
catch { }
#endif
@@ -98,7 +100,7 @@ namespace StardewModdingAPI.Toolkit.Framework
/// </remarks>
private static bool IsRunningAndroid()
{
- using Process process = new Process
+ using Process process = new()
{
StartInfo =
{
@@ -135,7 +137,7 @@ namespace StardewModdingAPI.Toolkit.Framework
buffer = Marshal.AllocHGlobal(8192);
if (LowLevelEnvironmentUtility.uname(buffer) == 0)
{
- string os = Marshal.PtrToStringAnsi(buffer);
+ string? os = Marshal.PtrToStringAnsi(buffer);
return os == "Darwin";
}
return false;
diff --git a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs
index ef6d4dd9..da678ac9 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs
@@ -9,6 +9,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
** Accessors
********/
/// <summary>Extra metadata about mods.</summary>
- public IDictionary<string, ModDataModel> ModData { get; set; }
+ public IDictionary<string, ModDataModel> ModData { get; } = new Dictionary<string, ModDataModel>();
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs
index b02be3e4..9674d283 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs
@@ -18,10 +18,10 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
public bool IsDefault { get; }
/// <summary>The lowest version in the range, or <c>null</c> for all past versions.</summary>
- public ISemanticVersion LowerVersion { get; }
+ public ISemanticVersion? LowerVersion { get; }
/// <summary>The highest version in the range, or <c>null</c> for all future versions.</summary>
- public ISemanticVersion UpperVersion { get; }
+ public ISemanticVersion? UpperVersion { get; }
/*********
@@ -33,7 +33,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <param name="isDefault">Whether this field should only be applied if it's not already set.</param>
/// <param name="lowerVersion">The lowest version in the range, or <c>null</c> for all past versions.</param>
/// <param name="upperVersion">The highest version in the range, or <c>null</c> for all future versions.</param>
- public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion)
+ public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion? lowerVersion, ISemanticVersion? upperVersion)
{
this.Key = key;
this.Value = value;
@@ -44,7 +44,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get whether this data field applies for the given manifest.</summary>
/// <param name="manifest">The mod manifest.</param>
- public bool IsMatch(IManifest manifest)
+ public bool IsMatch(IManifest? manifest)
{
return
manifest?.Version != null // ignore invalid manifest
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
index 2167d3e5..5912fb87 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
@@ -14,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
** Accessors
*********/
/// <summary>The mod's current unique ID.</summary>
- public string ID { get; set; }
+ public string ID { get; }
/// <summary>The former mod IDs (if any).</summary>
/// <remarks>
@@ -23,14 +23,14 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// ID, if any. If the mod's ID changed over time, multiple variants can be separated by the
/// <c>|</c> character.
/// </remarks>
- public string FormerIDs { get; set; }
+ public string? FormerIDs { get; }
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
- public ModWarning SuppressWarnings { get; set; }
+ public ModWarning SuppressWarnings { get; }
/// <summary>This field stores properties that aren't mapped to another field before they're parsed into <see cref="Fields"/>.</summary>
[JsonExtensionData]
- public IDictionary<string, JToken> ExtensionData { get; set; }
+ public IDictionary<string, JToken> ExtensionData { get; } = new Dictionary<string, JToken>();
/// <summary>The versioned field data.</summary>
/// <remarks>
@@ -50,6 +50,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/*********
** Public methods
*********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="id">The mod's current unique ID.</param>
+ /// <param name="formerIds">The former mod IDs (if any).</param>
+ /// <param name="suppressWarnings">The mod warnings to suppress, even if they'd normally be shown.</param>
+ public ModDataModel(string id, string? formerIds, ModWarning suppressWarnings)
+ {
+ this.ID = id;
+ this.FormerIDs = formerIds;
+ this.SuppressWarnings = suppressWarnings;
+ }
+
/// <summary>Get a parsed representation of the <see cref="Fields"/>.</summary>
public IEnumerable<ModDataField> GetFields()
{
@@ -59,8 +70,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
string packedKey = pair.Key;
string value = pair.Value;
bool isDefault = false;
- ISemanticVersion lowerVersion = null;
- ISemanticVersion upperVersion = null;
+ ISemanticVersion? lowerVersion = null;
+ ISemanticVersion? upperVersion = null;
// parse
string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray();
@@ -111,11 +122,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
- if (this.ExtensionData != null)
- {
- this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString());
- this.ExtensionData = null;
- }
+ this.Fields = this.ExtensionData.ToDictionary(p => p.Key, p => p.Value.ToString());
+ this.ExtensionData.Clear();
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
index 5dd32acf..ab0e4377 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
public string[] FormerIDs { get; }
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
- public ModWarning SuppressWarnings { get; set; }
+ public ModWarning SuppressWarnings { get; }
/// <summary>The versioned field data.</summary>
public ModDataField[] Fields { get; }
@@ -70,9 +70,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
}
/// <summary>Get the default update key for this mod, if any.</summary>
- public string GetDefaultUpdateKey()
+ public string? GetDefaultUpdateKey()
{
- string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
+ string? updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
return !string.IsNullOrWhiteSpace(updateKey)
? updateKey
: null;
@@ -80,9 +80,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get a parsed representation of the <see cref="ModDataRecord.Fields"/> which match a given manifest.</summary>
/// <param name="manifest">The manifest to match.</param>
- public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest)
+ public ModDataRecordVersionedFields GetVersionedFields(IManifest? manifest)
{
- ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this };
+ ModDataRecordVersionedFields parsed = new(this);
foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest)))
{
switch (field.Key)
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
index 5aaabd51..65fa424e 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
@@ -7,24 +7,32 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
** Accessors
*********/
/// <summary>The underlying data record.</summary>
- public ModDataRecord DataRecord { get; set; }
+ public ModDataRecord DataRecord { get; }
- /// <summary>The default mod name to display when the name isn't available (e.g. during dependency checks).</summary>
- public string DisplayName { get; set; }
-
- /// <summary>The update key to apply.</summary>
- public string UpdateKey { get; set; }
+ /// <summary>The update key to apply (if any).</summary>
+ public string? UpdateKey { get; set; }
/// <summary>The predefined compatibility status.</summary>
public ModStatus Status { get; set; } = ModStatus.None;
/// <summary>A reason phrase for the <see cref="Status"/>, or <c>null</c> to use the default reason.</summary>
- public string StatusReasonPhrase { get; set; }
+ public string? StatusReasonPhrase { get; set; }
/// <summary>Technical details shown in TRACE logs for the <see cref="Status"/>, or <c>null</c> to omit it.</summary>
- public string StatusReasonDetails { get; set; }
+ public string? StatusReasonDetails { get; set; }
/// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary>
- public ISemanticVersion StatusUpperVersion { get; set; }
+ public ISemanticVersion? StatusUpperVersion { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="dataRecord">The underlying data record.</param>
+ public ModDataRecordVersionedFields(ModDataRecord dataRecord)
+ {
+ this.DataRecord = dataRecord;
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs
index a9da884a..168b8aac 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs
@@ -14,7 +14,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
private readonly ModDataRecord[] Records;
/// <summary>Get an update URL for an update key (if valid).</summary>
- private readonly Func<string, string> GetUpdateUrl;
+ private readonly Func<string, string?> GetUpdateUrl;
/*********
@@ -22,12 +22,12 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
*********/
/// <summary>Construct an empty instance.</summary>
public ModDatabase()
- : this(new ModDataRecord[0], key => null) { }
+ : this(Array.Empty<ModDataRecord>(), _ => null) { }
/// <summary>Construct an instance.</summary>
/// <param name="records">The underlying mod data records indexed by default display name.</param>
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
- public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string> getUpdateUrl)
+ public ModDatabase(IEnumerable<ModDataRecord> records, Func<string, string?> getUpdateUrl)
{
this.Records = records.ToArray();
this.GetUpdateUrl = getUpdateUrl;
@@ -41,7 +41,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get a mod data record.</summary>
/// <param name="modID">The unique mod ID.</param>
- public ModDataRecord Get(string modID)
+ public ModDataRecord? Get(string? modID)
{
return !string.IsNullOrWhiteSpace(modID)
? this.Records.FirstOrDefault(p => p.HasID(modID))
@@ -50,11 +50,11 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>Get the mod page URL for a mod (if available).</summary>
/// <param name="id">The unique mod ID.</param>
- public string GetModPageUrlFor(string id)
+ public string? GetModPageUrlFor(string? id)
{
// get update key
- ModDataRecord record = this.Get(id);
- ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey);
+ ModDataRecord? record = this.Get(id);
+ ModDataField? updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey);
if (updateKeyField == null)
return null;
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs
index 825b98e5..da2a3c85 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs
@@ -22,13 +22,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
public ModType Type { get; }
/// <summary>The mod manifest.</summary>
- public Manifest Manifest { get; }
+ public Manifest? Manifest { get; }
/// <summary>The error which occurred parsing the manifest, if any.</summary>
public ModParseError ManifestParseError { get; set; }
/// <summary>A human-readable message for the <see cref="ManifestParseError"/>, if any.</summary>
- public string ManifestParseErrorText { get; set; }
+ public string? ManifestParseErrorText { get; set; }
/*********
@@ -49,7 +49,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <param name="manifest">The mod manifest.</param>
/// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param>
/// <param name="manifestParseErrorText">A human-readable message for the <paramref name="manifestParseError"/>, if any.</param>
- public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest, ModParseError manifestParseError, string manifestParseErrorText)
+ public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest? manifest, ModParseError manifestParseError, string? manifestParseErrorText)
{
// save info
this.Directory = directory;
@@ -59,9 +59,9 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
this.ManifestParseErrorText = manifestParseErrorText;
// set display name
- this.DisplayName = manifest?.Name;
- if (string.IsNullOrWhiteSpace(this.DisplayName))
- this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName);
+ this.DisplayName = !string.IsNullOrWhiteSpace(manifest?.Name)
+ ? manifest.Name
+ : PathUtilities.GetRelativePath(root.FullName, directory.FullName);
}
/// <summary>Get the update keys for a mod.</summary>
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index e6105f9c..24485620 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialization.Models;
+using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{
@@ -18,7 +19,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
private readonly JsonHelper JsonHelper;
/// <summary>A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod.</summary>
- private readonly HashSet<Regex> IgnoreFilesystemNames = new HashSet<Regex>
+ private readonly HashSet<Regex> IgnoreFilesystemNames = new()
{
new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager
new Regex(@"(?:^\._|^\.DS_Store$|^__MACOSX$|^mcs$)", RegexOptions.Compiled | RegexOptions.IgnoreCase), // macOS
@@ -26,7 +27,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
};
/// <summary>A list of file extensions to ignore when searching for mod files.</summary>
- private readonly HashSet<string> IgnoreFileExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private readonly HashSet<string> IgnoreFileExtensions = new(StringComparer.OrdinalIgnoreCase)
{
// text
".doc",
@@ -60,7 +61,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
};
/// <summary>The extensions for packed content files.</summary>
- private readonly HashSet<string> StrictXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private readonly HashSet<string> StrictXnbModExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".xgs",
".xnb",
@@ -69,7 +70,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
};
/// <summary>The extensions for files which an XNB mod may contain, in addition to <see cref="StrictXnbModExtensions"/>.</summary>
- private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private readonly HashSet<string> PotentialXnbModExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".json",
".yaml"
@@ -96,7 +97,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <param name="rootPath">The root folder containing mods.</param>
public IEnumerable<ModFolder> GetModFolders(string rootPath)
{
- DirectoryInfo root = new DirectoryInfo(rootPath);
+ DirectoryInfo root = new(rootPath);
return this.GetModFolders(root, root);
}
@@ -115,7 +116,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder)
{
// find manifest.json
- FileInfo manifestFile = this.FindManifest(searchFolder);
+ FileInfo? manifestFile = this.FindManifest(searchFolder);
// set appropriate invalid-mod error
if (manifestFile == null)
@@ -137,7 +138,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
// SMAPI installer
- if (relevantFiles.Any(p => p.Name == "install on Linux.sh" || p.Name == "install on macOS.command" || p.Name == "install on Windows.bat"))
+ if (relevantFiles.Any(p => p.Name is "install on Linux.sh" or "install on macOS.command" or "install on Windows.bat"))
return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "the SMAPI installer isn't a mod (you can delete this folder after running the installer file).");
// not a mod?
@@ -145,13 +146,13 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
}
// read mod info
- Manifest manifest = null;
+ Manifest? manifest = null;
ModParseError error = ModParseError.None;
- string errorText = null;
+ string? errorText = null;
{
try
{
- if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null)
+ if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest))
{
error = ModParseError.ManifestInvalid;
errorText = "its manifest is invalid.";
@@ -169,14 +170,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
}
}
- // normalize display fields
- if (manifest != null)
- {
- manifest.Name = this.StripNewlines(manifest.Name);
- manifest.Description = this.StripNewlines(manifest.Description);
- manifest.Author = this.StripNewlines(manifest.Author);
- }
-
// get mod type
ModType type;
{
@@ -192,7 +185,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
}
// build result
- return new ModFolder(root, manifestFile.Directory, type, manifest, error, errorText);
+ return new ModFolder(root, manifestFile.Directory!, type, manifest, error, errorText);
}
@@ -255,26 +248,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Find the manifest for a mod folder.</summary>
/// <param name="folder">The folder to search.</param>
- private FileInfo FindManifest(DirectoryInfo folder)
+ private FileInfo? FindManifest(DirectoryInfo folder)
{
- while (true)
- {
- // check for manifest in current folder
- FileInfo file = new FileInfo(Path.Combine(folder.FullName, "manifest.json"));
- if (file.Exists)
- return file;
-
- // check for single subfolder
- FileSystemInfo[] entries = folder.EnumerateFileSystemInfos().Take(2).ToArray();
- if (entries.Length == 1 && entries[0] is DirectoryInfo subfolder)
- {
- folder = subfolder;
- continue;
- }
+ // check for conventional manifest in current folder
+ const string defaultName = "manifest.json";
+ FileInfo file = new(Path.Combine(folder.FullName, defaultName));
+ if (file.Exists)
+ return file;
- // not found
- return null;
+ // check for manifest with incorrect capitalization
+ {
+ CaseInsensitivePathLookup pathLookup = new(folder.FullName, SearchOption.TopDirectoryOnly); // don't use GetCachedFor, since we only need it temporarily
+ string realName = pathLookup.GetFilePath(defaultName);
+ if (realName != defaultName)
+ file = new(Path.Combine(folder.FullName, realName));
}
+ if (file.Exists)
+ return file;
+
+ // not found
+ return null;
}
/// <summary>Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods).</summary>
@@ -314,8 +307,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <param name="entry">The file or folder.</param>
private bool IsRelevant(FileSystemInfo entry)
{
- // ignored file extension
- if (entry is FileInfo file && this.IgnoreFileExtensions.Contains(file.Extension))
+ // ignored file extensions and any files starting with "."
+ if ((entry is FileInfo file) && (this.IgnoreFileExtensions.Contains(file.Extension) || file.Name.StartsWith(".")))
return false;
// ignored entry name
@@ -363,12 +356,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return hasVortexMarker;
}
-
- /// <summary>Strip newlines from a string.</summary>
- /// <param name="input">The input to strip.</param>
- private string StripNewlines(string input)
- {
- return input?.Replace("\r", "").Replace("\n", "");
- }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs
index 489e1c4d..939be771 100644
--- a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs
+++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Toolkit.Framework
{
/// <summary>Reads strings into a semantic version.</summary>
@@ -16,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework
/// <param name="prereleaseTag">An optional prerelease tag.</param>
/// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
/// <returns>Returns whether the version was successfully parsed.</returns>
- public static bool TryParse(string versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata)
+ public static bool TryParse(string? versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string? prereleaseTag, out string? buildMetadata)
{
// init
major = 0;
@@ -103,7 +105,12 @@ namespace StardewModdingAPI.Toolkit.Framework
/// <param name="raw">The raw characters to parse.</param>
/// <param name="index">The index of the next character to read.</param>
/// <param name="tag">The parsed tag.</param>
- private static bool TryParseTag(char[] raw, ref int index, out string tag)
+ private static bool TryParseTag(char[] raw, ref int index,
+#if NET5_0_OR_GREATER
+ [NotNullWhen(true)]
+#endif
+ out string? tag
+ )
{
// read tag length
int length = 0;
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
index 077c0361..960caf96 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
{
@@ -15,12 +16,15 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
public ModSiteKey Site { get; }
/// <summary>The mod ID within the repository.</summary>
- public string ID { get; }
+ public string? ID { get; }
/// <summary>If specified, a substring in download names/descriptions to match.</summary>
- public string Subkey { get; }
+ public string? Subkey { get; }
/// <summary>Whether the update key seems to be valid.</summary>
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(UpdateKey.ID))]
+#endif
public bool LooksValid { get; }
@@ -32,9 +36,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <param name="site">The mod site containing the mod.</param>
/// <param name="id">The mod ID within the site.</param>
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
- public UpdateKey(string rawText, ModSiteKey site, string id, string subkey)
+ public UpdateKey(string? rawText, ModSiteKey site, string? id, string? subkey)
{
- this.RawText = rawText?.Trim();
+ this.RawText = rawText?.Trim() ?? string.Empty;
this.Site = site;
this.ID = id?.Trim();
this.Subkey = subkey?.Trim();
@@ -47,19 +51,19 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <param name="site">The mod site containing the mod.</param>
/// <param name="id">The mod ID within the site.</param>
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
- public UpdateKey(ModSiteKey site, string id, string subkey)
+ public UpdateKey(ModSiteKey site, string? id, string? subkey)
: this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { }
/// <summary>Parse a raw update key.</summary>
/// <param name="raw">The raw update key to parse.</param>
- public static UpdateKey Parse(string raw)
+ public static UpdateKey Parse(string? raw)
{
// extract site + ID
- string rawSite;
- string id;
+ string? rawSite;
+ string? id;
{
- string[] parts = raw?.Trim().Split(':');
- if (parts == null || parts.Length != 2)
+ string[]? parts = raw?.Trim().Split(':');
+ if (parts?.Length != 2)
return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
rawSite = parts[0].Trim();
@@ -69,7 +73,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
id = null;
// extract subkey
- string subkey = null;
+ string? subkey = null;
if (id != null)
{
string[] parts = id.Split('@');
@@ -109,7 +113,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
- public bool Equals(UpdateKey other)
+ public bool Equals(UpdateKey? other)
{
if (!this.LooksValid)
{
@@ -127,7 +131,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object.</param>
- public override bool Equals(object obj)
+ public override bool Equals(object? obj)
{
return obj is UpdateKey other && this.Equals(other);
}
@@ -143,7 +147,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <param name="site">The mod site containing the mod.</param>
/// <param name="id">The mod ID within the repository.</param>
/// <param name="subkey">If specified, a substring in download names/descriptions to match.</param>
- public static string GetString(ModSiteKey site, string id, string subkey = null)
+ public static string GetString(ModSiteKey site, string? id, string? subkey = null)
{
return $"{site}:{id}{subkey}".Trim();
}
diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs
index 38a67ae5..51f6fa24 100644
--- a/src/SMAPI.Toolkit/ModToolkit.cs
+++ b/src/SMAPI.Toolkit/ModToolkit.cs
@@ -22,7 +22,7 @@ namespace StardewModdingAPI.Toolkit
private readonly string UserAgent;
/// <summary>Maps vendor keys (like <c>Nexus</c>) to their mod URL template (where <c>{0}</c> is the mod ID). This doesn't affect update checks, which defer to the remote web API.</summary>
- private readonly IDictionary<ModSiteKey, string> VendorModUrls = new Dictionary<ModSiteKey, string>()
+ private readonly Dictionary<ModSiteKey, string> VendorModUrls = new()
{
[ModSiteKey.Chucklefish] = "https://community.playstarbound.com/resources/{0}",
[ModSiteKey.GitHub] = "https://github.com/{0}/releases",
@@ -34,7 +34,7 @@ namespace StardewModdingAPI.Toolkit
** Accessors
*********/
/// <summary>Encapsulates SMAPI's JSON parsing.</summary>
- public JsonHelper JsonHelper { get; } = new JsonHelper();
+ public JsonHelper JsonHelper { get; } = new();
/*********
@@ -43,7 +43,7 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Construct an instance.</summary>
public ModToolkit()
{
- ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version);
+ ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version!);
this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}";
}
@@ -57,7 +57,7 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Extract mod metadata from the wiki compatibility list.</summary>
public async Task<WikiModList> GetWikiCompatibilityListAsync()
{
- var client = new WikiClient(this.UserAgent);
+ WikiClient client = new(this.UserAgent);
return await client.FetchModsAsync();
}
@@ -87,13 +87,13 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Get an update URL for an update key (if valid).</summary>
/// <param name="updateKey">The update key.</param>
- public string GetUpdateUrl(string updateKey)
+ public string? GetUpdateUrl(string updateKey)
{
UpdateKey parsed = UpdateKey.Parse(updateKey);
if (!parsed.LooksValid)
return null;
- if (this.VendorModUrls.TryGetValue(parsed.Site, out string urlTemplate))
+ if (this.VendorModUrls.TryGetValue(parsed.Site, out string? urlTemplate))
return string.Format(urlTemplate, parsed.ID);
return null;
diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
index eede4562..6f5dffbe 100644
--- a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
+++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
@@ -2,4 +2,5 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("StardewModdingAPI")]
[assembly: InternalsVisibleTo("SMAPI.Installer")]
+[assembly: InternalsVisibleTo("SMAPI.Tests")]
[assembly: InternalsVisibleTo("SMAPI.Web")]
diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs
index 2f3e282b..3713758f 100644
--- a/src/SMAPI.Toolkit/SemanticVersion.cs
+++ b/src/SMAPI.Toolkit/SemanticVersion.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Framework;
@@ -38,10 +39,10 @@ namespace StardewModdingAPI.Toolkit
public int PlatformRelease { get; }
/// <inheritdoc />
- public string PrereleaseTag { get; }
+ public string? PrereleaseTag { get; }
/// <inheritdoc />
- public string BuildMetadata { get; }
+ public string? BuildMetadata { get; }
/*********
@@ -54,7 +55,7 @@ namespace StardewModdingAPI.Toolkit
/// <param name="platformRelease">The platform-specific version (if applicable).</param>
/// <param name="prereleaseTag">An optional prerelease tag.</param>
/// <param name="buildMetadata">Optional build metadata. This is ignored when determining version precedence.</param>
- public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string prereleaseTag = null, string buildMetadata = null)
+ public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string? prereleaseTag = null, string? buildMetadata = null)
{
this.MajorVersion = major;
this.MinorVersion = minor;
@@ -90,7 +91,7 @@ namespace StardewModdingAPI.Toolkit
{
if (version == null)
throw new ArgumentNullException(nameof(version), "The input version string can't be null.");
- if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) || (!allowNonStandard && platformRelease != 0))
+ if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string? prereleaseTag, out string? buildMetadata) || (!allowNonStandard && platformRelease != 0))
throw new FormatException($"The input '{version}' isn't a valid semantic version.");
this.MajorVersion = major;
@@ -104,59 +105,77 @@ namespace StardewModdingAPI.Toolkit
}
/// <inheritdoc />
- public int CompareTo(ISemanticVersion other)
+ public int CompareTo(ISemanticVersion? other)
{
- if (other == null)
- throw new ArgumentNullException(nameof(other));
- return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag);
+ return other == null
+ ? 1
+ : this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag);
}
/// <inheritdoc />
- public bool Equals(ISemanticVersion other)
+ public bool Equals(ISemanticVersion? other)
{
return other != null && this.CompareTo(other) == 0;
}
/// <inheritdoc />
+#if NET5_0_OR_GREATER
+ [MemberNotNullWhen(true, nameof(SemanticVersion.PrereleaseTag))]
+#endif
public bool IsPrerelease()
{
return !string.IsNullOrWhiteSpace(this.PrereleaseTag);
}
/// <inheritdoc />
- public bool IsOlderThan(ISemanticVersion other)
+ public bool IsOlderThan(ISemanticVersion? other)
{
return this.CompareTo(other) < 0;
}
/// <inheritdoc />
- public bool IsOlderThan(string other)
+ public bool IsOlderThan(string? other)
{
- return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true));
+ ISemanticVersion? otherVersion = other != null
+ ? new SemanticVersion(other, allowNonStandard: true)
+ : null;
+
+ return this.IsOlderThan(otherVersion);
}
/// <inheritdoc />
- public bool IsNewerThan(ISemanticVersion other)
+ public bool IsNewerThan(ISemanticVersion? other)
{
return this.CompareTo(other) > 0;
}
/// <inheritdoc />
- public bool IsNewerThan(string other)
+ public bool IsNewerThan(string? other)
{
- return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true));
+ ISemanticVersion? otherVersion = other != null
+ ? new SemanticVersion(other, allowNonStandard: true)
+ : null;
+
+ return this.IsNewerThan(otherVersion);
}
/// <inheritdoc />
- public bool IsBetween(ISemanticVersion min, ISemanticVersion max)
+ public bool IsBetween(ISemanticVersion? min, ISemanticVersion? max)
{
return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0;
}
/// <inheritdoc />
- public bool IsBetween(string min, string max)
+ public bool IsBetween(string? min, string? max)
{
- return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true));
+ ISemanticVersion? minVersion = min != null
+ ? new SemanticVersion(min, allowNonStandard: true)
+ : null;
+ ISemanticVersion? maxVersion = max != null
+ ? new SemanticVersion(max, allowNonStandard: true)
+ : null;
+
+ return this.IsBetween(minVersion, maxVersion);
}
/// <inheritdoc cref="ISemanticVersion.ToString" />
@@ -182,7 +201,12 @@ namespace StardewModdingAPI.Toolkit
/// <param name="version">The version string.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
- public static bool TryParse(string version, out ISemanticVersion parsed)
+ public static bool TryParse(string? version,
+#if NET5_0_OR_GREATER
+ [NotNullWhen(true)]
+#endif
+ out ISemanticVersion? parsed
+ )
{
return SemanticVersion.TryParse(version, allowNonStandard: false, out parsed);
}
@@ -192,8 +216,19 @@ namespace StardewModdingAPI.Toolkit
/// <param name="allowNonStandard">Whether to allow non-standard extensions to semantic versioning.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
- public static bool TryParse(string version, bool allowNonStandard, out ISemanticVersion parsed)
+ public static bool TryParse(string? version, bool allowNonStandard,
+#if NET5_0_OR_GREATER
+ [NotNullWhen(true)]
+#endif
+ out ISemanticVersion? parsed
+ )
{
+ if (version == null)
+ {
+ parsed = null;
+ return false;
+ }
+
try
{
parsed = new SemanticVersion(version, allowNonStandard);
@@ -212,7 +247,7 @@ namespace StardewModdingAPI.Toolkit
*********/
/// <summary>Get a normalized prerelease or build tag.</summary>
/// <param name="tag">The tag to normalize.</param>
- private string GetNormalizedTag(string tag)
+ private string? GetNormalizedTag(string? tag)
{
tag = tag?.Trim();
return !string.IsNullOrWhiteSpace(tag) ? tag : null;
@@ -224,7 +259,7 @@ namespace StardewModdingAPI.Toolkit
/// <param name="otherPatch">The patch version to compare with this instance.</param>
/// <param name="otherPlatformRelease">The non-standard platform release to compare with this instance.</param>
/// <param name="otherTag">The prerelease tag to compare with this instance.</param>
- private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string otherTag)
+ private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string? otherTag)
{
const int same = 0;
const int curNewer = 1;
@@ -253,8 +288,8 @@ namespace StardewModdingAPI.Toolkit
return curOlder;
// compare two prerelease tag values
- string[] curParts = this.PrereleaseTag.Split('.', '-');
- string[] otherParts = otherTag.Split('.', '-');
+ string[] curParts = this.PrereleaseTag?.Split('.', '-') ?? Array.Empty<string>();
+ string[] otherParts = otherTag?.Split('.', '-') ?? Array.Empty<string>();
int length = Math.Max(curParts.Length, otherParts.Length);
for (int i = 0; i < length; i++)
{
diff --git a/src/SMAPI.Toolkit/SemanticVersionComparer.cs b/src/SMAPI.Toolkit/SemanticVersionComparer.cs
index 9f6b57a2..2eca30df 100644
--- a/src/SMAPI.Toolkit/SemanticVersionComparer.cs
+++ b/src/SMAPI.Toolkit/SemanticVersionComparer.cs
@@ -3,20 +3,20 @@ using System.Collections.Generic;
namespace StardewModdingAPI.Toolkit
{
/// <summary>A comparer for semantic versions based on the <see cref="SemanticVersion.CompareTo(ISemanticVersion)"/> field.</summary>
- public class SemanticVersionComparer : IComparer<ISemanticVersion>
+ public class SemanticVersionComparer : IComparer<ISemanticVersion?>
{
/*********
** Accessors
*********/
/// <summary>A singleton instance of the comparer.</summary>
- public static SemanticVersionComparer Instance { get; } = new SemanticVersionComparer();
+ public static SemanticVersionComparer Instance { get; } = new();
/*********
** Public methods
*********/
/// <inheritdoc />
- public int Compare(ISemanticVersion x, ISemanticVersion y)
+ public int Compare(ISemanticVersion? x, ISemanticVersion? y)
{
if (object.ReferenceEquals(x, y))
return 0;
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs
index 5cabe9d8..faaeedea 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs
@@ -33,7 +33,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <param name="objectType">The object type.</param>
/// <param name="existingValue">The object being read.</param>
/// <param name="serializer">The calling serializer.</param>
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
return serializer.Deserialize<ManifestContentPackFor>(reader);
}
@@ -42,7 +42,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <param name="writer">The JSON writer.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new InvalidOperationException("This converter does not write JSON.");
}
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs
index 7b88d6b7..c499a2c6 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs
@@ -35,13 +35,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <param name="objectType">The object type.</param>
/// <param name="existingValue">The object being read.</param>
/// <param name="serializer">The calling serializer.</param>
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
List<ManifestDependency> result = new List<ManifestDependency>();
foreach (JObject obj in JArray.Load(reader).Children<JObject>())
{
- string uniqueID = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.UniqueID));
- string minVersion = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.MinimumVersion));
+ string uniqueID = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.UniqueID))!; // will be validated separately if null
+ string? minVersion = obj.ValueIgnoreCase<string>(nameof(ManifestDependency.MinimumVersion));
bool required = obj.ValueIgnoreCase<bool?>(nameof(ManifestDependency.IsRequired)) ?? true;
result.Add(new ManifestDependency(uniqueID, minVersion, required));
}
@@ -52,7 +52,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <param name="writer">The JSON writer.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
throw new InvalidOperationException("This converter does not write JSON.");
}
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
index cf69104d..c32c3185 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
@@ -39,15 +39,17 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <param name="objectType">The object type.</param>
/// <param name="existingValue">The object being read.</param>
/// <param name="serializer">The calling serializer.</param>
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
string path = reader.Path;
switch (reader.TokenType)
{
case JsonToken.StartObject:
return this.ReadObject(JObject.Load(reader));
+
case JsonToken.String:
return this.ReadString(JToken.Load(reader).Value<string>(), path);
+
default:
throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path}).");
}
@@ -57,7 +59,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <param name="writer">The JSON writer.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
writer.WriteValue(value?.ToString());
}
@@ -73,7 +75,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
int major = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MajorVersion));
int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion));
int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion));
- string prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag));
+ string? prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag));
return new SemanticVersion(major, minor, patch, prereleaseTag: prereleaseTag);
}
@@ -81,11 +83,11 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <summary>Read a JSON string.</summary>
/// <param name="str">The JSON string value.</param>
/// <param name="path">The path to the current JSON node.</param>
- private ISemanticVersion ReadString(string str, string path)
+ private ISemanticVersion? ReadString(string str, string path)
{
if (string.IsNullOrWhiteSpace(str))
return null;
- if (!SemanticVersion.TryParse(str, allowNonStandard: this.AllowNonStandard, out ISemanticVersion version))
+ if (!SemanticVersion.TryParse(str, allowNonStandard: this.AllowNonStandard, out ISemanticVersion? version))
throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path}).");
return version;
}
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
index ccc5158b..1c59f5e7 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
@@ -25,21 +25,12 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
}
- /// <summary>Writes the JSON representation of the object.</summary>
- /// <param name="writer">The JSON writer.</param>
- /// <param name="value">The value.</param>
- /// <param name="serializer">The calling serializer.</param>
- public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
- {
- throw new InvalidOperationException("This converter does not write JSON.");
- }
-
/// <summary>Reads the JSON representation of the object.</summary>
/// <param name="reader">The JSON reader.</param>
/// <param name="objectType">The object type.</param>
/// <param name="existingValue">The object being read.</param>
/// <param name="serializer">The calling serializer.</param>
- public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
string path = reader.Path;
switch (reader.TokenType)
@@ -58,6 +49,15 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
}
}
+ /// <summary>Writes the JSON representation of the object.</summary>
+ /// <param name="writer">The JSON writer.</param>
+ /// <param name="value">The value.</param>
+ /// <param name="serializer">The calling serializer.</param>
+ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
+ {
+ throw new InvalidOperationException("This converter does not write JSON.");
+ }
+
/*********
** Protected methods
diff --git a/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs
index 10f88dde..78297035 100644
--- a/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs
+++ b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs
@@ -10,12 +10,12 @@ namespace StardewModdingAPI.Toolkit.Serialization
/// <typeparam name="T">The value type.</typeparam>
/// <param name="obj">The JSON object to search.</param>
/// <param name="fieldName">The field name.</param>
- public static T ValueIgnoreCase<T>(this JObject obj, string fieldName)
+ public static T? ValueIgnoreCase<T>(this JObject obj, string fieldName)
{
- JToken token = obj.GetValue(fieldName, StringComparison.OrdinalIgnoreCase);
+ JToken? token = obj.GetValue(fieldName, StringComparison.OrdinalIgnoreCase);
return token != null
? token.Value<T>()
- : default(T);
+ : default;
}
}
}
diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
index 00db9903..3c9308f2 100644
--- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
+++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@@ -14,7 +15,7 @@ namespace StardewModdingAPI.Toolkit.Serialization
** Accessors
*********/
/// <summary>The JSON settings to use when serializing and deserializing files.</summary>
- public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings
+ public JsonSerializerSettings JsonSettings { get; } = new()
{
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded
@@ -36,7 +37,12 @@ namespace StardewModdingAPI.Toolkit.Serialization
/// <returns>Returns false if the file doesn't exist, else true.</returns>
/// <exception cref="ArgumentException">The given <paramref name="fullPath"/> is empty or invalid.</exception>
/// <exception cref="JsonReaderException">The file contains invalid JSON.</exception>
- public bool ReadJsonFileIfExists<TModel>(string fullPath, out TModel result)
+ public bool ReadJsonFileIfExists<TModel>(string fullPath,
+#if NET5_0_OR_GREATER
+ [NotNullWhen(true)]
+#endif
+ out TModel? result
+ )
{
// validate
if (string.IsNullOrWhiteSpace(fullPath))
@@ -48,9 +54,9 @@ namespace StardewModdingAPI.Toolkit.Serialization
{
json = File.ReadAllText(fullPath);
}
- catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException)
+ catch (Exception ex) when (ex is DirectoryNotFoundException or FileNotFoundException)
{
- result = default(TModel);
+ result = default;
return false;
}
@@ -58,7 +64,7 @@ namespace StardewModdingAPI.Toolkit.Serialization
try
{
result = this.Deserialize<TModel>(json);
- return true;
+ return result != null;
}
catch (Exception ex)
{
@@ -88,7 +94,7 @@ namespace StardewModdingAPI.Toolkit.Serialization
throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath));
// create directory if needed
- string dir = Path.GetDirectoryName(fullPath);
+ string dir = Path.GetDirectoryName(fullPath)!;
if (dir == null)
throw new ArgumentException("The file path is invalid.", nameof(fullPath));
if (!Directory.Exists(dir))
@@ -106,7 +112,8 @@ namespace StardewModdingAPI.Toolkit.Serialization
{
try
{
- return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings);
+ return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings)
+ ?? throw new InvalidOperationException($"Couldn't deserialize model type '{typeof(TModel)}' from empty or null JSON.");
}
catch (JsonReaderException)
{
@@ -115,7 +122,8 @@ namespace StardewModdingAPI.Toolkit.Serialization
{
try
{
- return JsonConvert.DeserializeObject<TModel>(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings);
+ return JsonConvert.DeserializeObject<TModel>(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings)
+ ?? throw new InvalidOperationException($"Couldn't deserialize model type '{typeof(TModel)}' from empty or null JSON.");
}
catch { /* rethrow original error */ }
}
diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index 46b654a5..da3ad608 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -1,5 +1,6 @@
+using System;
using System.Collections.Generic;
-using System.Runtime.Serialization;
+using System.Diagnostics.CodeAnalysis;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization.Converters;
@@ -12,48 +13,45 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
** Accessors
*********/
/// <summary>The mod name.</summary>
- public string Name { get; set; }
+ public string Name { get; }
/// <summary>A brief description of the mod.</summary>
- public string Description { get; set; }
+ public string Description { get; }
/// <summary>The mod author's name.</summary>
- public string Author { get; set; }
+ public string Author { get; }
/// <summary>The mod version.</summary>
- public ISemanticVersion Version { get; set; }
+ public ISemanticVersion Version { get; }
/// <summary>The minimum SMAPI version required by this mod, if any.</summary>
- public ISemanticVersion MinimumApiVersion { get; set; }
+ public ISemanticVersion? MinimumApiVersion { get; }
/// <summary>The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</summary>
- public string EntryDll { get; set; }
+ public string? EntryDll { get; }
/// <summary>The mod which will read this as a content pack. Mutually exclusive with <see cref="Manifest.EntryDll"/>.</summary>
[JsonConverter(typeof(ManifestContentPackForConverter))]
- public IManifestContentPackFor ContentPackFor { get; set; }
+ public IManifestContentPackFor? ContentPackFor { get; }
/// <summary>The other mods that must be loaded before this mod.</summary>
[JsonConverter(typeof(ManifestDependencyArrayConverter))]
- public IManifestDependency[] Dependencies { get; set; }
+ public IManifestDependency[] Dependencies { get; }
/// <summary>The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</summary>
- public string[] UpdateKeys { get; set; }
+ public string[] UpdateKeys { get; private set; }
/// <summary>The unique mod ID.</summary>
- public string UniqueID { get; set; }
+ public string UniqueID { get; }
/// <summary>Any manifest fields which didn't match a valid field.</summary>
[JsonExtensionData]
- public IDictionary<string, object> ExtraFields { get; set; }
+ public IDictionary<string, object> ExtraFields { get; } = new Dictionary<string, object>();
/*********
** Public methods
*********/
- /// <summary>Construct an instance.</summary>
- public Manifest() { }
-
/// <summary>Construct an instance for a transitional content pack.</summary>
/// <param name="uniqueID">The unique mod ID.</param>
/// <param name="name">The mod name.</param>
@@ -61,24 +59,71 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
/// <param name="description">A brief description of the mod.</param>
/// <param name="version">The mod version.</param>
/// <param name="contentPackFor">The modID which will read this as a content pack.</param>
- public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null)
+ public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string? contentPackFor = null)
+ : this(
+ uniqueId: uniqueID,
+ name: name,
+ author: author,
+ description: description,
+ version: version,
+ minimumApiVersion: null,
+ entryDll: null,
+ contentPackFor: contentPackFor != null
+ ? new ManifestContentPackFor(contentPackFor, null)
+ : null,
+ dependencies: null,
+ updateKeys: null
+ )
+ { }
+
+ /// <summary>Construct an instance for a transitional content pack.</summary>
+ /// <param name="uniqueId">The unique mod ID.</param>
+ /// <param name="name">The mod name.</param>
+ /// <param name="author">The mod author's name.</param>
+ /// <param name="description">A brief description of the mod.</param>
+ /// <param name="version">The mod version.</param>
+ /// <param name="minimumApiVersion">The minimum SMAPI version required by this mod, if any.</param>
+ /// <param name="entryDll">The name of the DLL in the directory that has the <c>Entry</c> method. Mutually exclusive with <see cref="ContentPackFor"/>.</param>
+ /// <param name="contentPackFor">The modID which will read this as a content pack.</param>
+ /// <param name="dependencies">The other mods that must be loaded before this mod.</param>
+ /// <param name="updateKeys">The namespaced mod IDs to query for updates (like <c>Nexus:541</c>).</param>
+ [JsonConstructor]
+ public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys)
{
- this.Name = name;
- this.Author = author;
- this.Description = description;
+ this.UniqueID = this.NormalizeWhitespace(uniqueId);
+ this.Name = this.NormalizeWhitespace(name);
+ this.Author = this.NormalizeWhitespace(author);
+ this.Description = this.NormalizeWhitespace(description);
this.Version = version;
- this.UniqueID = uniqueID;
- this.UpdateKeys = new string[0];
- this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor };
+ this.MinimumApiVersion = minimumApiVersion;
+ this.EntryDll = this.NormalizeWhitespace(entryDll);
+ this.ContentPackFor = contentPackFor;
+ this.Dependencies = dependencies ?? Array.Empty<IManifestDependency>();
+ this.UpdateKeys = updateKeys ?? Array.Empty<string>();
+ }
+
+ /// <summary>Override the update keys loaded from the mod info.</summary>
+ /// <param name="updateKeys">The new update keys to set.</param>
+ internal void OverrideUpdateKeys(params string[] updateKeys)
+ {
+ this.UpdateKeys = updateKeys;
}
- /// <summary>Normalize the model after it's deserialized.</summary>
- /// <param name="context">The deserialization context.</param>
- [OnDeserialized]
- public void OnDeserialized(StreamingContext context)
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Normalize whitespace in a raw string.</summary>
+ /// <param name="input">The input to strip.</param>
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("input")]
+#endif
+ private string? NormalizeWhitespace(string? input)
{
- this.Dependencies ??= new IManifestDependency[0];
- this.UpdateKeys ??= new string[0];
+ return input
+ ?.Trim()
+ .Replace("\r", "")
+ .Replace("\n", "");
}
}
}
diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs
index 1eb80889..f7dc8aa8 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace StardewModdingAPI.Toolkit.Serialization.Models
{
/// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary>
@@ -7,9 +9,36 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
** Accessors
*********/
/// <summary>The unique ID of the mod which can read this content pack.</summary>
- public string UniqueID { get; set; }
+ public string UniqueID { get; }
/// <summary>The minimum required version (if any).</summary>
- public ISemanticVersion MinimumVersion { get; set; }
+ public ISemanticVersion? MinimumVersion { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="uniqueId">The unique ID of the mod which can read this content pack.</param>
+ /// <param name="minimumVersion">The minimum required version (if any).</param>
+ public ManifestContentPackFor(string uniqueId, ISemanticVersion? minimumVersion)
+ {
+ this.UniqueID = this.NormalizeWhitespace(uniqueId);
+ this.MinimumVersion = minimumVersion;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Normalize whitespace in a raw string.</summary>
+ /// <param name="input">The input to strip.</param>
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("input")]
+#endif
+ private string? NormalizeWhitespace(string? input)
+ {
+ return input?.Trim();
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs
index 00f168f4..fa254ea7 100644
--- a/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs
@@ -1,3 +1,6 @@
+using System.Diagnostics.CodeAnalysis;
+using Newtonsoft.Json;
+
namespace StardewModdingAPI.Toolkit.Serialization.Models
{
/// <summary>A mod dependency listed in a mod manifest.</summary>
@@ -7,13 +10,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
** Accessors
*********/
/// <summary>The unique mod ID to require.</summary>
- public string UniqueID { get; set; }
+ public string UniqueID { get; }
/// <summary>The minimum required version (if any).</summary>
- public ISemanticVersion MinimumVersion { get; set; }
+ public ISemanticVersion? MinimumVersion { get; }
/// <summary>Whether the dependency must be installed to use the mod.</summary>
- public bool IsRequired { get; set; }
+ public bool IsRequired { get; }
/*********
@@ -23,13 +26,40 @@ namespace StardewModdingAPI.Toolkit.Serialization.Models
/// <param name="uniqueID">The unique mod ID to require.</param>
/// <param name="minimumVersion">The minimum required version (if any).</param>
/// <param name="required">Whether the dependency must be installed to use the mod.</param>
- public ManifestDependency(string uniqueID, string minimumVersion, bool required = true)
+ public ManifestDependency(string uniqueID, string? minimumVersion, bool required = true)
+ : this(
+ uniqueID: uniqueID,
+ minimumVersion: !string.IsNullOrWhiteSpace(minimumVersion)
+ ? new SemanticVersion(minimumVersion)
+ : null,
+ required: required
+ )
+ { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="uniqueID">The unique mod ID to require.</param>
+ /// <param name="minimumVersion">The minimum required version (if any).</param>
+ /// <param name="required">Whether the dependency must be installed to use the mod.</param>
+ [JsonConstructor]
+ public ManifestDependency(string uniqueID, ISemanticVersion? minimumVersion, bool required = true)
{
- this.UniqueID = uniqueID;
- this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion)
- ? new SemanticVersion(minimumVersion)
- : null;
+ this.UniqueID = this.NormalizeWhitespace(uniqueID);
+ this.MinimumVersion = minimumVersion;
this.IsRequired = required;
}
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Normalize whitespace in a raw string.</summary>
+ /// <param name="input">The input to strip.</param>
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("input")]
+#endif
+ private string? NormalizeWhitespace(string? input)
+ {
+ return input?.Trim();
+ }
}
}
diff --git a/src/SMAPI.Toolkit/Serialization/SParseException.cs b/src/SMAPI.Toolkit/Serialization/SParseException.cs
index 5f58b5b8..c2b3f68e 100644
--- a/src/SMAPI.Toolkit/Serialization/SParseException.cs
+++ b/src/SMAPI.Toolkit/Serialization/SParseException.cs
@@ -11,7 +11,7 @@ namespace StardewModdingAPI.Toolkit.Serialization
/// <summary>Construct an instance.</summary>
/// <param name="message">The error message.</param>
/// <param name="ex">The underlying exception, if any.</param>
- public SParseException(string message, Exception ex = null)
+ public SParseException(string message, Exception? ex = null)
: base(message, ex) { }
}
}
diff --git a/src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs b/src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs
new file mode 100644
index 00000000..12fad008
--- /dev/null
+++ b/src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace StardewModdingAPI.Toolkit.Utilities
+{
+ /// <summary>Provides an API for case-insensitive relative path lookups within a root directory.</summary>
+ internal class CaseInsensitivePathLookup
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The root directory path for relative paths.</summary>
+ private readonly string RootPath;
+
+ /// <summary>A case-insensitive lookup of file paths within the <see cref="RootPath"/>. Each path is listed in both file path and asset name format, so it's usable in both contexts without needing to re-parse paths.</summary>
+ private readonly Lazy<Dictionary<string, string>> RelativePathCache;
+
+ /// <summary>The case-insensitive path caches by root path.</summary>
+ private static readonly Dictionary<string, CaseInsensitivePathLookup> CachedRoots = new(StringComparer.OrdinalIgnoreCase);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="rootPath">The root directory path for relative paths.</param>
+ /// <param name="searchOption">Which directories to scan from the root.</param>
+ public CaseInsensitivePathLookup(string rootPath, SearchOption searchOption = SearchOption.AllDirectories)
+ {
+ this.RootPath = rootPath;
+ this.RelativePathCache = new(() => this.GetRelativePathCache(searchOption));
+ }
+
+ /// <summary>Get the exact capitalization for a given relative file path.</summary>
+ /// <param name="relativePath">The relative path.</param>
+ /// <remarks>Returns the resolved path in file path format, else the normalized <paramref name="relativePath"/>.</remarks>
+ public string GetFilePath(string relativePath)
+ {
+ return this.GetImpl(PathUtilities.NormalizePath(relativePath));
+ }
+
+ /// <summary>Get the exact capitalization for a given asset name.</summary>
+ /// <param name="relativePath">The relative path.</param>
+ /// <remarks>Returns the resolved path in asset name format, else the normalized <paramref name="relativePath"/>.</remarks>
+ public string GetAssetName(string relativePath)
+ {
+ return this.GetImpl(PathUtilities.NormalizeAssetName(relativePath));
+ }
+
+ /// <summary>Add a relative path that was just created by a SMAPI API.</summary>
+ /// <param name="relativePath">The relative path. This must already be normalized in asset name or file path format.</param>
+ public void Add(string relativePath)
+ {
+ // skip if cache isn't created yet (no need to add files manually in that case)
+ if (!this.RelativePathCache.IsValueCreated)
+ return;
+
+ // skip if already cached
+ if (this.RelativePathCache.Value.ContainsKey(relativePath))
+ return;
+
+ // make sure path exists
+ relativePath = PathUtilities.NormalizePath(relativePath);
+ if (!File.Exists(Path.Combine(this.RootPath, relativePath)))
+ throw new InvalidOperationException($"Can't add relative path '{relativePath}' to the case-insensitive cache for '{this.RootPath}' because that file doesn't exist.");
+
+ // cache path
+ this.CacheRawPath(this.RelativePathCache.Value, relativePath);
+ }
+
+ /// <summary>Get a cached dictionary of relative paths within a root path, for case-insensitive file lookups.</summary>
+ /// <param name="rootPath">The root path to scan.</param>
+ public static CaseInsensitivePathLookup GetCachedFor(string rootPath)
+ {
+ rootPath = PathUtilities.NormalizePath(rootPath);
+
+ if (!CaseInsensitivePathLookup.CachedRoots.TryGetValue(rootPath, out CaseInsensitivePathLookup? cache))
+ CaseInsensitivePathLookup.CachedRoots[rootPath] = cache = new CaseInsensitivePathLookup(rootPath);
+
+ return cache;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the exact capitalization for a given relative path.</summary>
+ /// <param name="relativePath">The relative path. This must already be normalized into asset name or file path format (i.e. using <see cref="PathUtilities.NormalizeAssetName"/> or <see cref="PathUtilities.NormalizePath"/> respectively).</param>
+ /// <remarks>Returns the resolved path in the same format if found, else returns the path as-is.</remarks>
+ private string GetImpl(string relativePath)
+ {
+ // invalid path
+ if (string.IsNullOrWhiteSpace(relativePath))
+ return relativePath;
+
+ // already cached
+ if (this.RelativePathCache.Value.TryGetValue(relativePath, out string? resolved))
+ return resolved;
+
+ // keep capitalization as-is
+ if (File.Exists(Path.Combine(this.RootPath, relativePath)))
+ {
+ // file exists but isn't cached for some reason
+ // cache it now so any later references to it are case-insensitive
+ this.CacheRawPath(this.RelativePathCache.Value, relativePath);
+ }
+ return relativePath;
+ }
+
+ /// <summary>Get a case-insensitive lookup of file paths (see <see cref="RelativePathCache"/>).</summary>
+ /// <param name="searchOption">Which directories to scan from the root.</param>
+ private Dictionary<string, string> GetRelativePathCache(SearchOption searchOption)
+ {
+ Dictionary<string, string> cache = new(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string path in Directory.EnumerateFiles(this.RootPath, "*", searchOption))
+ {
+ string relativePath = path.Substring(this.RootPath.Length + 1);
+
+ this.CacheRawPath(cache, relativePath);
+ }
+
+ return cache;
+ }
+
+ /// <summary>Add a raw relative path to the cache.</summary>
+ /// <param name="cache">The cache to update.</param>
+ /// <param name="relativePath">The relative path to cache, with its exact filesystem capitalization.</param>
+ private void CacheRawPath(IDictionary<string, string> cache, string relativePath)
+ {
+ string filePath = PathUtilities.NormalizePath(relativePath);
+ string assetName = PathUtilities.NormalizeAssetName(relativePath);
+
+ cache[filePath] = filePath;
+ cache[assetName] = assetName;
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
index 7536337a..1791c5b3 100644
--- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using StardewModdingAPI.Toolkit.Framework;
namespace StardewModdingAPI.Toolkit.Utilities
@@ -34,7 +33,6 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>Get the human-readable OS name and version.</summary>
/// <param name="platform">The current platform.</param>
- [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
public static string GetFriendlyPlatformName(Platform platform)
{
return LowLevelEnvironmentUtility.GetFriendlyPlatformName(platform.ToString());
diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs
index 2e9e5eac..136279f2 100644
--- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs
+++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
@@ -36,8 +37,11 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <param name="path">The path to split.</param>
/// <param name="limit">The number of segments to match. Any additional segments will be merged into the last returned part.</param>
[Pure]
- public static string[] GetSegments(string path, int? limit = null)
+ public static string[] GetSegments(string? path, int? limit = null)
{
+ if (path == null)
+ return Array.Empty<string>();
+
return limit.HasValue
? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries)
: path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
@@ -45,8 +49,16 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>Normalize an asset name to match how MonoGame's content APIs would normalize and cache it.</summary>
/// <param name="assetName">The asset name to normalize.</param>
- public static string NormalizeAssetName(string assetName)
+ [Pure]
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("assetName")]
+#endif
+ public static string? NormalizeAssetName(string? assetName)
{
+ assetName = assetName?.Trim();
+ if (string.IsNullOrEmpty(assetName))
+ return assetName;
+
return string.Join(PathUtilities.PreferredAssetSeparator.ToString(), PathUtilities.GetSegments(assetName)); // based on MonoGame's ContentManager.Load<T> logic
}
@@ -54,7 +66,10 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <param name="path">The file path to normalize.</param>
/// <remarks>This should only be used for file paths. For asset names, use <see cref="NormalizeAssetName"/> instead.</remarks>
[Pure]
- public static string NormalizePath(string path)
+#if NET5_0_OR_GREATER
+ [return: NotNullIfNotNull("path")]
+#endif
+ public static string? NormalizePath(string? path)
{
path = path?.Trim();
if (string.IsNullOrEmpty(path))
@@ -100,8 +115,8 @@ namespace StardewModdingAPI.Toolkit.Utilities
// though, this is only for compatibility with the mod build package.
// convert to URIs
- Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
- Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
+ Uri from = new(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
+ Uri to = new(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
if (from.Scheme != to.Scheme)
throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'.");
@@ -132,7 +147,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary>
/// <param name="path">The path to check.</param>
[Pure]
- public static bool IsSafeRelativePath(string path)
+ public static bool IsSafeRelativePath(string? path)
{
if (string.IsNullOrWhiteSpace(path))
return true;
@@ -145,9 +160,11 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).</summary>
/// <param name="str">The string to check.</param>
[Pure]
- public static bool IsSlug(string str)
+ public static bool IsSlug(string? str)
{
- return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase);
+ return
+ string.IsNullOrWhiteSpace(str)
+ || !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase);
}
}
}