summaryrefslogtreecommitdiff
path: root/src/SMAPI.Toolkit
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Toolkit')
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs18
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs36
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs36
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs13
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs52
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs10
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs42
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs21
-rw-r--r--src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs169
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs10
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs31
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs24
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs4
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs27
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs24
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs162
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs21
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs3
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs32
-rw-r--r--src/SMAPI.Toolkit/ModToolkit.cs21
-rw-r--r--src/SMAPI.Toolkit/Properties/AssemblyInfo.cs7
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj29
-rw-r--r--src/SMAPI.Toolkit/SemanticVersion.cs57
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs (renamed from src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs)6
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs (renamed from src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs)6
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs (renamed from src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs)18
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs (renamed from src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs)6
-rw-r--r--src/SMAPI.Toolkit/Serialization/InternalExtensions.cs (renamed from src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs)2
-rw-r--r--src/SMAPI.Toolkit/Serialization/JsonHelper.cs (renamed from src/SMAPI.Toolkit/Serialisation/JsonHelper.cs)22
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/Manifest.cs (renamed from src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs)4
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs (renamed from src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs)2
-rw-r--r--src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs (renamed from src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs)2
-rw-r--r--src/SMAPI.Toolkit/Serialization/SParseException.cs (renamed from src/SMAPI.Toolkit/Serialisation/SParseException.cs)2
-rw-r--r--src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj28
-rw-r--r--src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs157
-rw-r--r--src/SMAPI.Toolkit/Utilities/PathUtilities.cs20
-rw-r--r--src/SMAPI.Toolkit/Utilities/Platform.cs18
37 files changed, 861 insertions, 281 deletions
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs
index 8a9c0a25..f1bcfccc 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>
@@ -9,23 +11,31 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The mod's unique ID (if known).</summary>
public string ID { get; set; }
+ /// <summary>The update version recommended by the web API based on its version update and mapping rules.</summary>
+ public ModEntryVersionModel SuggestedUpdate { get; set; }
+
+ /// <summary>Optional extended data which isn't needed for update checks.</summary>
+ public ModExtendedMetadataModel Metadata { get; set; }
+
/// <summary>The main version.</summary>
+ [Obsolete]
public ModEntryVersionModel Main { get; set; }
/// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
+ [Obsolete]
public ModEntryVersionModel Optional { get; set; }
/// <summary>The latest unofficial version, if newer than <see cref="Main"/> and <see cref="Optional"/>.</summary>
+ [Obsolete]
public ModEntryVersionModel Unofficial { get; set; }
/// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
+ [Obsolete]
public ModEntryVersionModel UnofficialForBeta { get; set; }
- /// <summary>Optional extended data which isn't needed for update checks.</summary>
- public ModExtendedMetadataModel Metadata { get; set; }
-
/// <summary>Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, <see cref="UnofficialForBeta"/> should be used for beta versions of SMAPI instead of <see cref="Unofficial"/>.</summary>
- public bool HasBetaInfo { get; set; }
+ [Obsolete]
+ public bool? HasBetaInfo { get; set; }
/// <summary>The errors that occurred while fetching update data.</summary>
public string[] Errors { get; set; } = new string[0];
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
index 989c18b0..4a697585 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs
@@ -28,6 +28,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The mod ID in the Chucklefish mod repo.</summary>
public int? ChucklefishID { get; set; }
+ /// <summary>The mod ID in the CurseForge mod repo.</summary>
+ 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; }
+
/// <summary>The mod ID in the ModDrop mod repo.</summary>
public int? ModDropID { get; set; }
@@ -40,6 +46,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The custom mod page URL (if applicable).</summary>
public string CustomUrl { get; set; }
+ /// <summary>The main version.</summary>
+ public ModEntryVersionModel Main { get; set; }
+
+ /// <summary>The latest optional version, if newer than <see cref="Main"/>.</summary>
+ 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; }
+
+ /// <summary>The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see <see cref="HasBetaInfo"/>).</summary>
+ public ModEntryVersionModel UnofficialForBeta { get; set; }
/****
** Stable compatibility
@@ -48,13 +65,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
[JsonConverter(typeof(StringEnumConverter))]
public WikiCompatibilityStatus? CompatibilityStatus { get; set; }
- /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatitng.</summary>
+ /// <summary>The human-readable summary of the compatibility status or workaround, without HTML formatting.</summary>
public string CompatibilitySummary { get; set; }
/// <summary>The game or SMAPI version which broke this mod, if applicable.</summary>
public string BrokeIn { get; set; }
-
/****
** Beta compatibility
****/
@@ -62,7 +78,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
[JsonConverter(typeof(StringEnumConverter))]
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 formatitng.</summary>
+ /// <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; }
/// <summary>The beta game or SMAPI version which broke this mod, if applicable.</summary>
@@ -78,8 +94,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an instance.</summary>
/// <param name="wiki">The mod metadata from the wiki (if available).</param>
/// <param name="db">The mod metadata from SMAPI's internal DB (if available).</param>
- public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db)
+ /// <param name="main">The main version.</param>
+ /// <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)
{
+ // versions
+ this.Main = main;
+ this.Optional = optional;
+ this.Unofficial = unofficial;
+ this.UnofficialForBeta = unofficialForBeta;
+
// wiki data
if (wiki != null)
{
@@ -87,6 +113,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
this.Name = wiki.Name.FirstOrDefault();
this.NexusID = wiki.NexusID;
this.ChucklefishID = wiki.ChucklefishID;
+ this.CurseForgeID = wiki.CurseForgeID;
+ this.CurseForgeKey = wiki.CurseForgeKey;
this.ModDropID = wiki.ModDropID;
this.GitHubRepo = wiki.GitHubRepo;
this.CustomSourceUrl = wiki.CustomSourceUrl;
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs
deleted file mode 100644
index e352e1cc..00000000
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Linq;
-
-namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
-{
- /// <summary>Specifies mods whose update-check info to fetch.</summary>
- public class ModSearchModel
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The mods for which to find data.</summary>
- public ModSearchEntryModel[] Mods { get; set; }
-
- /// <summary>Whether to include extended metadata for each mod.</summary>
- public bool IncludeExtendedMetadata { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an empty instance.</summary>
- public ModSearchModel()
- {
- // needed for JSON deserialising
- }
-
- /// <summary>Construct an instance.</summary>
- /// <param name="mods">The mods to search.</param>
- /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
- public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata)
- {
- this.Mods = mods.ToArray();
- this.IncludeExtendedMetadata = includeExtendedMetadata;
- }
- }
-}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
index bca47647..bf81e102 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs
@@ -12,6 +12,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>The namespaced mod update keys (if available).</summary>
public string[] UpdateKeys { get; 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; }
+
+ /// <summary>Whether the installed version is broken or could not be loaded.</summary>
+ public bool IsBroken { get; set; }
+
/*********
** Public methods
@@ -19,15 +25,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Construct an empty instance.</summary>
public ModSearchEntryModel()
{
- // needed for JSON deserialising
+ // 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>
- public ModSearchEntryModel(string id, string[] updateKeys)
+ /// <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)
{
this.ID = id;
+ this.InstalledVersion = installedVersion;
this.UpdateKeys = updateKeys ?? new string[0];
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
new file mode 100644
index 00000000..73698173
--- /dev/null
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchModel.cs
@@ -0,0 +1,52 @@
+using System.Linq;
+using StardewModdingAPI.Toolkit.Utilities;
+
+namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
+{
+ /// <summary>Specifies mods whose update-check info to fetch.</summary>
+ public class ModSearchModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mods for which to find data.</summary>
+ public ModSearchEntryModel[] Mods { get; set; }
+
+ /// <summary>Whether to include extended metadata for each mod.</summary>
+ public bool IncludeExtendedMetadata { get; set; }
+
+ /// <summary>The SMAPI version installed by the player. This is used for version mapping in some cases.</summary>
+ public ISemanticVersion ApiVersion { get; set; }
+
+ /// <summary>The Stardew Valley version installed by the player.</summary>
+ public ISemanticVersion GameVersion { get; set; }
+
+ /// <summary>The OS on which the player plays.</summary>
+ public Platform? Platform { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an empty instance.</summary>
+ public ModSearchModel()
+ {
+ // needed for JSON deserializing
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="mods">The mods to search.</param>
+ /// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
+ /// <param name="gameVersion">The Stardew Valley version installed by the player.</param>
+ /// <param name="platform">The OS on which the player plays.</param>
+ /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
+ public ModSearchModel(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata)
+ {
+ this.Mods = mods.ToArray();
+ this.ApiVersion = apiVersion;
+ this.GameVersion = gameVersion;
+ this.Platform = platform;
+ this.IncludeExtendedMetadata = includeExtendedMetadata;
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
index 7c3df384..f0a7c82a 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
@@ -3,7 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using Newtonsoft.Json;
-using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialization;
+using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
@@ -37,12 +38,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
/// <summary>Get metadata about a set of mods from the web API.</summary>
/// <param name="mods">The mod keys for which to fetch the latest version.</param>
+ /// <param name="apiVersion">The SMAPI version installed by the player. If this is null, the API won't provide a recommended update.</param>
+ /// <param name="gameVersion">The Stardew Valley version installed by the player.</param>
+ /// <param name="platform">The OS on which the player plays.</param>
/// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
- public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false)
+ public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false)
{
return this.Post<ModSearchModel, ModEntryModel[]>(
$"v{this.Version}/mods",
- new ModSearchModel(mods, includeExtendedMetadata)
+ new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata)
).ToDictionary(p => p.ID);
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
index 3e9b8ea6..384f23fc 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
@@ -93,12 +93,17 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
string[] warnings = this.GetAttributeAsCsv(node, "data-warnings");
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");
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");
+ IDictionary<string, string> mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions");
+ IDictionary<string, string> mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions");
// parse stable compatibility
WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo
@@ -127,6 +132,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
}
}
+ // parse links
+ List<Tuple<Uri, string>> metadataLinks = new List<Tuple<Uri, string>>();
+ foreach (HtmlNode linkElement in node.Descendants("td").Last().Descendants("a").Skip(1)) // skip anchor link
+ {
+ string text = linkElement.InnerText.Trim();
+ Uri url = new Uri(linkElement.GetAttributeValue("href", ""));
+ metadataLinks.Add(Tuple.Create(url, text));
+ }
+
// yield model
yield return new WikiModEntry
{
@@ -135,6 +149,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
Author = authors,
NexusID = nexusID,
ChucklefishID = chucklefishID,
+ CurseForgeID = curseForgeID,
+ CurseForgeKey = curseForgeKey,
ModDropID = modDropID,
GitHubRepo = githubRepo,
CustomSourceUrl = customSourceUrl,
@@ -143,6 +159,10 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
Compatibility = compatibility,
BetaCompatibility = betaCompatibility,
Warnings = warnings,
+ MetadataLinks = metadataLinks.ToArray(),
+ DevNote = devNote,
+ MapLocalVersions = mapLocalVersions,
+ MapRemoteVersions = mapRemoteVersions,
Anchor = anchor
};
}
@@ -207,6 +227,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
return null;
}
+ /// <summary>Get an attribute value and parse it as a version mapping.</summary>
+ /// <param name="element">The element whose attributes to read.</param>
+ /// <param name="name">The attribute name.</param>
+ private IDictionary<string, string> GetAttributeAsVersionMapping(HtmlNode element, string name)
+ {
+ // get raw value
+ string raw = this.GetAttribute(element, name);
+ if (raw?.Contains("→") != true)
+ return null;
+
+ // parse
+ // Specified on the wiki in the form "remote version → mapped version; another remote version → mapped version"
+ IDictionary<string, string> map = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
+ foreach (string pair in raw.Split(';'))
+ {
+ string[] versions = pair.Split('→');
+ if (versions.Length == 2 && !string.IsNullOrWhiteSpace(versions[0]) && !string.IsNullOrWhiteSpace(versions[1]))
+ map[versions[0].Trim()] = versions[1].Trim();
+ }
+ return map;
+ }
+
/// <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>
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
index cf416cc6..931dcd43 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs
@@ -1,3 +1,6 @@
+using System;
+using System.Collections.Generic;
+
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
{
/// <summary>A mod entry in the wiki list.</summary>
@@ -21,6 +24,12 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>The mod ID in the Chucklefish mod repo.</summary>
public int? ChucklefishID { get; set; }
+ /// <summary>The mod ID in the CurseForge mod repo.</summary>
+ 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; }
+
/// <summary>The mod ID in the ModDrop mod repo.</summary>
public int? ModDropID { get; set; }
@@ -48,6 +57,18 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>The human-readable warnings for players about this mod.</summary>
public string[] Warnings { get; set; }
+ /// <summary>Extra metadata links (usually for open pull requests).</summary>
+ public Tuple<Uri, string>[] MetadataLinks { get; set; }
+
+ /// <summary>Special notes intended for developers who maintain unofficial updates or submit pull requests. </summary>
+ public string DevNote { get; set; }
+
+ /// <summary>Maps local versions to a semantic version for update checks.</summary>
+ public IDictionary<string, string> MapLocalVersions { get; set; }
+
+ /// <summary>Maps remote versions to a semantic version for update checks.</summary>
+ public IDictionary<string, string> MapRemoteVersions { get; set; }
+
/// <summary>The link anchor for the mod entry in the wiki compatibility list.</summary>
public string Anchor { get; set; }
}
diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
new file mode 100644
index 00000000..212c70ef
--- /dev/null
+++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using StardewModdingAPI.Toolkit.Utilities;
+#if SMAPI_FOR_WINDOWS
+using Microsoft.Win32;
+#endif
+
+namespace StardewModdingAPI.Toolkit.Framework.GameScanning
+{
+ /// <summary>Finds installed game folders.</summary>
+ public class GameScanner
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Find all valid Stardew Valley install folders.</summary>
+ /// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS.</remarks>
+ public IEnumerable<DirectoryInfo> Scan()
+ {
+ // get OS info
+ Platform platform = EnvironmentUtility.DetectPlatform();
+ string executableFilename = EnvironmentUtility.GetExecutableName(platform);
+
+ // get install paths
+ IEnumerable<string> paths = this
+ .GetCustomInstallPaths(platform)
+ .Concat(this.GetDefaultInstallPaths(platform))
+ .Select(PathUtilities.NormalizePathSeparators)
+ .Distinct(StringComparer.InvariantCultureIgnoreCase);
+
+ // yield valid folders
+ foreach (string path in paths)
+ {
+ DirectoryInfo folder = new DirectoryInfo(path);
+ if (folder.Exists && folder.EnumerateFiles(executableFilename).Any())
+ yield return folder;
+ }
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The default file paths where Stardew Valley can be installed.</summary>
+ /// <param name="platform">The target platform.</param>
+ /// <remarks>Derived from the crossplatform mod config: https://github.com/Pathoschild/Stardew.ModBuildConfig. </remarks>
+ private IEnumerable<string> GetDefaultInstallPaths(Platform platform)
+ {
+ switch (platform)
+ {
+ case Platform.Linux:
+ case Platform.Mac:
+ {
+ string home = Environment.GetEnvironmentVariable("HOME");
+
+ // Linux
+ yield return $"{home}/GOG Games/Stardew Valley/game";
+ yield return Directory.Exists($"{home}/.steam/steam/steamapps/common/Stardew Valley")
+ ? $"{home}/.steam/steam/steamapps/common/Stardew Valley"
+ : $"{home}/.local/share/Steam/steamapps/common/Stardew Valley";
+
+ // Mac
+ yield return "/Applications/Stardew Valley.app/Contents/MacOS";
+ yield return $"{home}/Library/Application Support/Steam/steamapps/common/Stardew Valley/Contents/MacOS";
+ }
+ break;
+
+ case Platform.Windows:
+ {
+ // Windows
+ foreach (string programFiles in new[] { @"C:\Program Files", @"C:\Program Files (x86)" })
+ {
+ yield return $@"{programFiles}\GalaxyClient\Games\Stardew Valley";
+ yield return $@"{programFiles}\GOG Galaxy\Games\Stardew Valley";
+ yield return $@"{programFiles}\Steam\steamapps\common\Stardew Valley";
+ }
+
+ // Windows registry
+#if SMAPI_FOR_WINDOWS
+ IDictionary<string, string> registryKeys = new Dictionary<string, string>
+ {
+ [@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Steam App 413150"] = "InstallLocation", // Steam
+ [@"SOFTWARE\WOW6432Node\GOG.com\Games\1453375253"] = "PATH", // GOG on 64-bit Windows
+ };
+ foreach (var pair in registryKeys)
+ {
+ 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");
+ if (steampath != null)
+ yield return Path.Combine(steampath.Replace('/', '\\'), @"steamapps\common\Stardew Valley");
+#endif
+ }
+ break;
+
+ default:
+ throw new InvalidOperationException($"Unknown platform '{platform}'.");
+ }
+ }
+
+ /// <summary>Get the custom install path from the <c>stardewvalley.targets</c> file in the home directory, if any.</summary>
+ /// <param name="platform">The target platform.</param>
+ private IEnumerable<string> GetCustomInstallPaths(Platform platform)
+ {
+ // get home path
+ string homePath = Environment.GetEnvironmentVariable(platform == Platform.Windows ? "USERPROFILE" : "HOME");
+ if (string.IsNullOrWhiteSpace(homePath))
+ yield break;
+
+ // get targets file
+ FileInfo file = new FileInfo(Path.Combine(homePath, "stardewvalley.targets"));
+ if (!file.Exists)
+ yield break;
+
+ // parse file
+ XElement root;
+ try
+ {
+ using (FileStream stream = file.OpenRead())
+ root = XElement.Load(stream);
+ }
+ catch
+ {
+ yield break;
+ }
+
+ // get install path
+ 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();
+ }
+
+#if SMAPI_FOR_WINDOWS
+ /// <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)
+ {
+ RegistryKey localMachine = Environment.Is64BitOperatingSystem ? RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64) : Registry.LocalMachine;
+ RegistryKey openKey = localMachine.OpenSubKey(key);
+ if (openKey == null)
+ return null;
+ using (openKey)
+ 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)
+ {
+ 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);
+ }
+#endif
+ }
+}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
index 18039762..8b40c301 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs
@@ -25,12 +25,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// </remarks>
public string FormerIDs { get; set; }
- /// <summary>Maps local versions to a semantic version for update checks.</summary>
- public IDictionary<string, string> MapLocalVersions { get; set; } = new Dictionary<string, string>();
-
- /// <summary>Maps remote versions to a semantic version for update checks.</summary>
- public IDictionary<string, string> MapRemoteVersions { get; set; } = new Dictionary<string, string>();
-
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
public ModWarning SuppressWarnings { get; set; }
@@ -112,8 +106,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/*********
** Private methods
*********/
- /// <summary>The method invoked after JSON deserialisation.</summary>
- /// <param name="context">The deserialisation context.</param>
+ /// <summary>The method invoked after JSON deserialization.</summary>
+ /// <param name="context">The deserialization context.</param>
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
index 794ad2e4..c892d820 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs
@@ -22,12 +22,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The mod warnings to suppress, even if they'd normally be shown.</summary>
public ModWarning SuppressWarnings { get; set; }
- /// <summary>Maps local versions to a semantic version for update checks.</summary>
- public IDictionary<string, string> MapLocalVersions { get; }
-
- /// <summary>Maps remote versions to a semantic version for update checks.</summary>
- public IDictionary<string, string> MapRemoteVersions { get; }
-
/// <summary>The versioned field data.</summary>
public ModDataField[] Fields { get; }
@@ -44,8 +38,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
this.ID = model.ID;
this.FormerIDs = model.GetFormerIDs().ToArray();
this.SuppressWarnings = model.SuppressWarnings;
- this.MapLocalVersions = new Dictionary<string, string>(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase);
- this.MapRemoteVersions = new Dictionary<string, string>(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase);
this.Fields = model.GetFields().ToArray();
}
@@ -67,29 +59,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
return false;
}
- /// <summary>Get a semantic local version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
- {
- return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion)
- ? new SemanticVersion(newVersion)
- : version;
- }
-
- /// <summary>Get a semantic remote version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public string GetRemoteVersionForUpdateChecks(string version)
- {
- // normalise version if possible
- if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
- version = parsed.ToString();
-
- // fetch remote version
- return this.MapRemoteVersions != null && this.MapRemoteVersions.TryGetValue(version, out string newVersion)
- ? newVersion
- : version;
- }
-
/// <summary>Get the possible mod IDs.</summary>
public IEnumerable<string> GetIDs()
{
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
index 237f2c66..598da66a 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs
@@ -26,29 +26,5 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The upper version for which the <see cref="Status"/> applies (if any).</summary>
public ISemanticVersion StatusUpperVersion { get; set; }
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Get a semantic local version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version)
- {
- return this.DataRecord.GetLocalVersionForUpdateChecks(version);
- }
-
- /// <summary>Get a semantic remote version for update checks.</summary>
- /// <param name="version">The remote version to normalise.</param>
- public ISemanticVersion GetRemoteVersionForUpdateChecks(ISemanticVersion version)
- {
- if (version == null)
- return null;
-
- string rawVersion = this.DataRecord.GetRemoteVersionForUpdateChecks(version.ToString());
- return rawVersion != null
- ? new SemanticVersion(rawVersion)
- : version;
- }
}
}
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
index d61c427f..e67616d0 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
@@ -13,7 +13,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
BrokenCodeLoaded = 1,
/// <summary>The mod affects the save serializer in a way that may make saves unloadable without the mod.</summary>
- ChangesSaveSerialiser = 2,
+ ChangesSaveSerializer = 2,
/// <summary>The mod patches the game in a way that may impact stability.</summary>
PatchesGame = 4,
@@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/Mac.</summary>
UsesDynamic = 8,
- /// <summary>The mod references specialised 'unvalided update tick' events which may impact stability.</summary>
+ /// <summary>The mod references specialized 'unvalidated update tick' events which may impact stability.</summary>
UsesUnvalidatedUpdateTick = 16,
/// <summary>The mod has no update keys set.</summary>
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs
index bb467b36..d0df09a1 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using StardewModdingAPI.Toolkit.Serialisation.Models;
+using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Toolkit.Framework.ModScanning
@@ -18,14 +18,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>The folder containing the mod's manifest.json.</summary>
public DirectoryInfo Directory { get; }
+ /// <summary>The mod type.</summary>
+ public ModType Type { get; }
+
/// <summary>The mod manifest.</summary>
public Manifest Manifest { get; }
/// <summary>The error which occurred parsing the manifest, if any.</summary>
- public string ManifestParseError { get; }
+ public ModParseError ManifestParseError { get; set; }
- /// <summary>Whether the mod should be loaded by default. This is <c>false</c> if it was found within a folder whose name starts with a dot.</summary>
- public bool ShouldBeLoaded { get; }
+ /// <summary>A human-readable message for the <see cref="ManifestParseError"/>, if any.</summary>
+ public string ManifestParseErrorText { get; set; }
/*********
@@ -34,16 +37,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Construct an instance.</summary>
/// <param name="root">The root folder containing mods.</param>
/// <param name="directory">The folder containing the mod's manifest.json.</param>
+ /// <param name="type">The mod type.</param>
+ /// <param name="manifest">The mod manifest.</param>
+ public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest manifest)
+ : this(root, directory, type, manifest, ModParseError.None, null) { }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="root">The root folder containing mods.</param>
+ /// <param name="directory">The folder containing the mod's manifest.json.</param>
+ /// <param name="type">The mod type.</param>
/// <param name="manifest">The mod manifest.</param>
/// <param name="manifestParseError">The error which occurred parsing the manifest, if any.</param>
- /// <param name="shouldBeLoaded">Whether the mod should be loaded by default. This should be <c>false</c> if it was found within a folder whose name starts with a dot.</param>
- public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true)
+ /// <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)
{
// save info
this.Directory = directory;
+ this.Type = type;
this.Manifest = manifest;
this.ManifestParseError = manifestParseError;
- this.ShouldBeLoaded = shouldBeLoaded;
+ this.ManifestParseErrorText = manifestParseErrorText;
// set display name
this.DisplayName = manifest?.Name;
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs
new file mode 100644
index 00000000..b10510ff
--- /dev/null
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModParseError.cs
@@ -0,0 +1,24 @@
+namespace StardewModdingAPI.Toolkit.Framework.ModScanning
+{
+ /// <summary>Indicates why a mod could not be parsed.</summary>
+ public enum ModParseError
+ {
+ /// <summary>No parse error.</summary>
+ None,
+
+ /// <summary>The folder is empty or contains only ignored files.</summary>
+ EmptyFolder,
+
+ /// <summary>The folder is ignored by convention.</summary>
+ IgnoredFolder,
+
+ /// <summary>The mod's <c>manifest.json</c> could not be parsed.</summary>
+ ManifestInvalid,
+
+ /// <summary>The folder contains non-ignored and non-XNB files, but none of them are <c>manifest.json</c>.</summary>
+ ManifestMissing,
+
+ /// <summary>The folder is an XNB mod, which can't be loaded through SMAPI.</summary>
+ XnbMod
+ }
+}
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index 0ab73d56..f11cc1a7 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -2,8 +2,9 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using StardewModdingAPI.Toolkit.Serialisation;
-using StardewModdingAPI.Toolkit.Serialisation.Models;
+using System.Text.RegularExpressions;
+using StardewModdingAPI.Toolkit.Serialization;
+using StardewModdingAPI.Toolkit.Serialization.Models;
namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{
@@ -17,20 +18,32 @@ 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<string> IgnoreFilesystemEntries = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
+ private readonly HashSet<Regex> IgnoreFilesystemEntries = new HashSet<Regex>
{
- ".DS_Store",
- "mcs",
- "Thumbs.db"
+ // OS metadata files
+ new Regex(@"^__folder_managed_by_vortex$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Vortex mod manager
+ new Regex(@"^(?:__MACOSX|\._\.DS_Store|\.DS_Store|mcs)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // MacOS
+ new Regex(@"^(?:desktop\.ini|Thumbs\.db)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows
+ new Regex(@"\.(?:url|lnk)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // Windows shortcut files
+
+ // other
+ new Regex(@"\.(?:bmp|gif|jpeg|jpg|png|psd|tif)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // image files
+ new Regex(@"\.(?:md|rtf|txt)$", RegexOptions.Compiled | RegexOptions.IgnoreCase), // text files
+ new Regex(@"\.(?:backup|bak|old)$", RegexOptions.Compiled | RegexOptions.IgnoreCase) // backup file
};
- /// <summary>The extensions for files which an XNB mod may contain. If a mod contains *only* these file extensions, it should be considered an XNB mod.</summary>
+ /// <summary>The extensions for files which an XNB mod may contain. If a mod doesn't have a <c>manifest.json</c> and contains *only* these file extensions, it should be considered an XNB mod.</summary>
private readonly HashSet<string> PotentialXnbModExtensions = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
- ".md",
- ".png",
- ".txt",
- ".xnb"
+ // XNB files
+ ".xgs",
+ ".xnb",
+ ".xsb",
+ ".xwb",
+
+ // unpacking artifacts
+ ".json",
+ ".yaml"
};
@@ -52,6 +65,15 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return this.GetModFolders(root, root);
}
+ /// <summary>Extract information about all mods in the given folder.</summary>
+ /// <param name="rootPath">The root folder containing mods. Only the <paramref name="modPath"/> will be searched, but this field allows it to be treated as a potential mod folder of its own.</param>
+ /// <param name="modPath">The mod path to search.</param>
+ // /// <param name="tryConsolidateMod">If the folder contains multiple XNB mods, treat them as subfolders of a single mod. This is useful when reading a single mod archive, as opposed to a mods folder.</param>
+ public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath)
+ {
+ return this.GetModFolders(root: new DirectoryInfo(rootPath), folder: new DirectoryInfo(modPath));
+ }
+
/// <summary>Extract information from a mod folder.</summary>
/// <param name="root">The root folder containing mods.</param>
/// <param name="searchFolder">The folder to search for a mod.</param>
@@ -63,34 +85,40 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
// set appropriate invalid-mod error
if (manifestFile == null)
{
- FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).ToArray();
+ FileInfo[] files = this.RecursivelyGetRelevantFiles(searchFolder).ToArray();
if (!files.Any())
- return new ModFolder(root, searchFolder, null, "it's an empty folder.");
- if (files.All(file => this.PotentialXnbModExtensions.Contains(file.Extension)))
- return new ModFolder(root, searchFolder, null, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
- return new ModFolder(root, searchFolder, null, "it contains files, but none of them are manifest.json.");
+ return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.EmptyFolder, "it's an empty folder.");
+ if (files.All(this.IsPotentialXnbFile))
+ return new ModFolder(root, searchFolder, ModType.Xnb, null, ModParseError.XnbMod, "it's not a SMAPI mod (see https://smapi.io/xnb for info).");
+ return new ModFolder(root, searchFolder, ModType.Invalid, null, ModParseError.ManifestMissing, "it contains files, but none of them are manifest.json.");
}
// read mod info
Manifest manifest = null;
- string manifestError = null;
+ ModParseError error = ModParseError.None;
+ string errorText = null;
{
try
{
if (!this.JsonHelper.ReadJsonFileIfExists<Manifest>(manifestFile.FullName, out manifest) || manifest == null)
- manifestError = "its manifest is invalid.";
+ {
+ error = ModParseError.ManifestInvalid;
+ errorText = "its manifest is invalid.";
+ }
}
catch (SParseException ex)
{
- manifestError = $"parsing its manifest failed: {ex.Message}";
+ error = ModParseError.ManifestInvalid;
+ errorText = $"parsing its manifest failed: {ex.Message}";
}
catch (Exception ex)
{
- manifestError = $"parsing its manifest failed:\n{ex}";
+ error = ModParseError.ManifestInvalid;
+ errorText = $"parsing its manifest failed:\n{ex}";
}
}
- // normalise display fields
+ // normalize display fields
if (manifest != null)
{
manifest.Name = this.StripNewlines(manifest.Name);
@@ -98,7 +126,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
manifest.Author = this.StripNewlines(manifest.Author);
}
- return new ModFolder(root, manifestFile.Directory, manifest, manifestError);
+ // get mod type
+ ModType type = ModType.Invalid;
+ if (manifest != null)
+ {
+ type = !string.IsNullOrWhiteSpace(manifest.ContentPackFor?.UniqueID)
+ ? ModType.ContentPack
+ : ModType.Smapi;
+ }
+
+ // build result
+ return new ModFolder(root, manifestFile.Directory, type, manifest, error, errorText);
}
@@ -108,20 +146,30 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Recursively extract information about all mods in the given folder.</summary>
/// <param name="root">The root mod folder.</param>
/// <param name="folder">The folder to search for mods.</param>
- public IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder)
+ private IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder)
{
- // skip
- if (folder.FullName != root.FullName && folder.Name.StartsWith("."))
- yield return new ModFolder(root, folder, null, "ignored folder because its name starts with a dot.", shouldBeLoaded: false);
+ bool isRoot = folder.FullName == root.FullName;
- // recurse into subfolders
- else if (this.IsModSearchFolder(root, folder))
+ // skip
+ if (!isRoot)
{
- foreach (DirectoryInfo subfolder in folder.EnumerateDirectories())
+ if (folder.Name.StartsWith("."))
{
- foreach (ModFolder match in this.GetModFolders(root, subfolder))
- yield return match;
+ yield return new ModFolder(root, folder, ModType.Ignored, null, ModParseError.IgnoredFolder, "ignored folder because its name starts with a dot.");
+ yield break;
}
+ if (!this.IsRelevant(folder))
+ yield break;
+ }
+
+ // find mods in subfolders
+ if (this.IsModSearchFolder(root, folder))
+ {
+ IEnumerable<ModFolder> subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub));
+ if (!isRoot)
+ subfolders = this.TryConsolidate(root, folder, subfolders.ToArray());
+ foreach (ModFolder subfolder in subfolders)
+ yield return subfolder;
}
// treat as mod folder
@@ -129,6 +177,26 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
yield return this.ReadFolder(root, folder);
}
+ /// <summary>Consolidate adjacent folders into one mod folder, if possible.</summary>
+ /// <param name="root">The folder containing both parent and subfolders.</param>
+ /// <param name="parentFolder">The parent folder to consolidate, if possible.</param>
+ /// <param name="subfolders">The subfolders to consolidate, if possible.</param>
+ private IEnumerable<ModFolder> TryConsolidate(DirectoryInfo root, DirectoryInfo parentFolder, ModFolder[] subfolders)
+ {
+ if (subfolders.Length > 1)
+ {
+ // a collection of empty folders
+ if (subfolders.All(p => p.ManifestParseError == ModParseError.EmptyFolder))
+ return new[] { new ModFolder(root, parentFolder, ModType.Invalid, null, ModParseError.EmptyFolder, subfolders[0].ManifestParseErrorText) };
+
+ // an XNB mod
+ if (subfolders.All(p => p.Type == ModType.Xnb || p.ManifestParseError == ModParseError.EmptyFolder))
+ return new[] { new ModFolder(root, parentFolder, ModType.Xnb, null, ModParseError.XnbMod, subfolders[0].ManifestParseErrorText) };
+ }
+
+ return subfolders;
+ }
+
/// <summary>Find the manifest for a mod folder.</summary>
/// <param name="folder">The folder to search.</param>
private FileInfo FindManifest(DirectoryInfo folder)
@@ -166,11 +234,41 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
return subfolders.Any() && !files.Any();
}
+ /// <summary>Recursively get all relevant files in a folder based on the result of <see cref="IsRelevant"/>.</summary>
+ /// <param name="folder">The root folder to search.</param>
+ private IEnumerable<FileInfo> RecursivelyGetRelevantFiles(DirectoryInfo folder)
+ {
+ foreach (FileSystemInfo entry in folder.GetFileSystemInfos())
+ {
+ if (!this.IsRelevant(entry))
+ continue;
+
+ if (entry is FileInfo file)
+ yield return file;
+
+ if (entry is DirectoryInfo subfolder)
+ {
+ foreach (FileInfo subfolderFile in this.RecursivelyGetRelevantFiles(subfolder))
+ yield return subfolderFile;
+ }
+ }
+ }
+
/// <summary>Get whether a file or folder is relevant when deciding how to process a mod folder.</summary>
/// <param name="entry">The file or folder.</param>
private bool IsRelevant(FileSystemInfo entry)
{
- return !this.IgnoreFilesystemEntries.Contains(entry.Name);
+ return !this.IgnoreFilesystemEntries.Any(p => p.IsMatch(entry.Name));
+ }
+
+ /// <summary>Get whether a file is potentially part of an XNB mod.</summary>
+ /// <param name="entry">The file.</param>
+ private bool IsPotentialXnbFile(FileInfo entry)
+ {
+ if (!this.IsRelevant(entry))
+ return true;
+
+ return this.PotentialXnbModExtensions.Contains(entry.Extension); // use EndsWith to handle cases like image..png
}
/// <summary>Strip newlines from a string.</summary>
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs
new file mode 100644
index 00000000..bc86edb6
--- /dev/null
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModType.cs
@@ -0,0 +1,21 @@
+namespace StardewModdingAPI.Toolkit.Framework.ModScanning
+{
+ /// <summary>A general mod type.</summary>
+ public enum ModType
+ {
+ /// <summary>The mod is invalid and its type could not be determined.</summary>
+ Invalid,
+
+ /// <summary>The folder is ignored by convention.</summary>
+ Ignored,
+
+ /// <summary>A mod which uses SMAPI directly.</summary>
+ Smapi,
+
+ /// <summary>A mod which contains files loaded by a SMAPI mod.</summary>
+ ContentPack,
+
+ /// <summary>A legacy mod which replaces game files directly.</summary>
+ Xnb
+ }
+}
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs
index f6c402d5..765ca334 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs
@@ -9,6 +9,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <summary>The Chucklefish mod repository.</summary>
Chucklefish,
+ /// <summary>The CurseForge mod repository.</summary>
+ CurseForge,
+
/// <summary>A GitHub project containing releases.</summary>
GitHub,
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
index 865ebcf7..3fc1759e 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
@@ -3,7 +3,7 @@ using System;
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
{
/// <summary>A namespaced mod ID which uniquely identifies a mod within a mod repository.</summary>
- public class UpdateKey
+ public class UpdateKey : IEquatable<UpdateKey>
{
/*********
** Accessors
@@ -38,6 +38,12 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
&& !string.IsNullOrWhiteSpace(id);
}
+ /// <summary>Construct an instance.</summary>
+ /// <param name="repository">The mod repository containing the mod.</param>
+ /// <param name="id">The mod ID within the repository.</param>
+ public UpdateKey(ModRepositoryKey repository, string id)
+ : this($"{repository}:{id}", repository, id) { }
+
/// <summary>Parse a raw update key.</summary>
/// <param name="raw">The raw update key to parse.</param>
public static UpdateKey Parse(string raw)
@@ -69,5 +75,29 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
? $"{this.Repository}:{this.ID}"
: this.RawText;
}
+
+ /// <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)
+ {
+ return
+ other != null
+ && this.Repository == other.Repository
+ && string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
+ }
+
+ /// <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)
+ {
+ return obj is UpdateKey other && this.Equals(other);
+ }
+
+ /// <summary>Serves as the default hash function. </summary>
+ /// <returns>A hash code for the current object.</returns>
+ public override int GetHashCode()
+ {
+ return $"{this.Repository}:{this.ID}".ToLower().GetHashCode();
+ }
}
}
diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs
index 1b53e59e..80b14659 100644
--- a/src/SMAPI.Toolkit/ModToolkit.cs
+++ b/src/SMAPI.Toolkit/ModToolkit.cs
@@ -2,13 +2,17 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+using StardewModdingAPI.Toolkit.Framework.GameScanning;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.ModScanning;
-using StardewModdingAPI.Toolkit.Serialisation;
+using StardewModdingAPI.Toolkit.Serialization;
+[assembly: InternalsVisibleTo("StardewModdingAPI")]
+[assembly: InternalsVisibleTo("SMAPI.Web")]
namespace StardewModdingAPI.Toolkit
{
/// <summary>A convenience wrapper for the various tools.</summary>
@@ -46,6 +50,13 @@ namespace StardewModdingAPI.Toolkit
this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}";
}
+ /// <summary>Find valid Stardew Valley install folders.</summary>
+ /// <remarks>This checks default game locations, and on Windows checks the Windows registry for GOG/Steam install data. A folder is considered 'valid' if it contains the Stardew Valley executable for the current OS.</remarks>
+ public IEnumerable<DirectoryInfo> GetGameFolders()
+ {
+ return new GameScanner().Scan();
+ }
+
/// <summary>Extract mod metadata from the wiki compatibility list.</summary>
public async Task<WikiModList> GetWikiCompatibilityListAsync()
{
@@ -69,6 +80,14 @@ namespace StardewModdingAPI.Toolkit
return new ModScanner(this.JsonHelper).GetModFolders(rootPath);
}
+ /// <summary>Extract information about all mods in the given folder.</summary>
+ /// <param name="rootPath">The root folder containing mods. Only the <paramref name="modPath"/> will be searched, but this field allows it to be treated as a potential mod folder of its own.</param>
+ /// <param name="modPath">The mod path to search.</param>
+ public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath)
+ {
+ return new ModScanner(this.JsonHelper).GetModFolders(rootPath, modPath);
+ }
+
/// <summary>Get an update URL for an update key (if valid).</summary>
/// <param name="updateKey">The update key.</param>
public string GetUpdateUrl(string updateKey)
diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
deleted file mode 100644
index 1bb19e8c..00000000
--- a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-
-[assembly: AssemblyTitle("SMAPI.Toolkit")]
-[assembly: AssemblyDescription("A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.")]
-[assembly: InternalsVisibleTo("StardewModdingAPI")]
-[assembly: InternalsVisibleTo("StardewModdingAPI.Web")]
diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
new file mode 100644
index 00000000..3bb7e313
--- /dev/null
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <AssemblyName>SMAPI.Toolkit</AssemblyName>
+ <RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
+ <Description>A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods.</Description>
+ <TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
+ <OutputPath>..\..\bin\$(Configuration)\SMAPI.Toolkit</OutputPath>
+ <DocumentationFile>..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\SMAPI.Toolkit.xml</DocumentationFile>
+ <LangVersion>latest</LangVersion>
+ <PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
+ <RootNamespace>StardewModdingAPI.Toolkit</RootNamespace>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="HtmlAgilityPack" Version="1.11.16" />
+ <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
+ <PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />
+ <PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
+ </ItemGroup>
+
+ <Import Project="..\..\build\common.targets" />
+
+</Project>
diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs
index ba9ca6c6..2ce3578e 100644
--- a/src/SMAPI.Toolkit/SemanticVersion.cs
+++ b/src/SMAPI.Toolkit/SemanticVersion.cs
@@ -42,15 +42,6 @@ namespace StardewModdingAPI.Toolkit
/// <summary>An optional prerelease tag.</summary>
public string PrereleaseTag { get; }
-#if !SMAPI_3_0_STRICT
- /// <summary>An optional prerelease tag.</summary>
- [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")]
- public string Build => this.PrereleaseTag;
-
- /// <summary>Whether the version was parsed from the legacy object format.</summary>
- public bool IsLegacyFormat { get; }
-#endif
-
/*********
** Public methods
@@ -60,20 +51,12 @@ namespace StardewModdingAPI.Toolkit
/// <param name="minor">The minor version incremented for backwards-compatible changes.</param>
/// <param name="patch">The patch version for backwards-compatible fixes.</param>
/// <param name="prereleaseTag">An optional prerelease tag.</param>
- /// <param name="isLegacyFormat">Whether the version was parsed from the legacy object format.</param>
- public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null
-#if !SMAPI_3_0_STRICT
- , bool isLegacyFormat = false
-#endif
- )
+ public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null)
{
this.MajorVersion = major;
this.MinorVersion = minor;
this.PatchVersion = patch;
- this.PrereleaseTag = this.GetNormalisedTag(prereleaseTag);
-#if !SMAPI_3_0_STRICT
- this.IsLegacyFormat = isLegacyFormat;
-#endif
+ this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag);
this.AssertValid();
}
@@ -106,16 +89,16 @@ namespace StardewModdingAPI.Toolkit
if (!match.Success)
throw new FormatException($"The input '{version}' isn't a valid semantic version.");
- // initialise
+ // initialize
this.MajorVersion = int.Parse(match.Groups["major"].Value);
this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0;
this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0;
- this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalisedTag(match.Groups["prerelease"].Value) : null;
+ this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null;
this.AssertValid();
}
- /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
+ /// <summary>Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="other">The version to compare with this instance.</param>
/// <exception cref="ArgumentNullException">The <paramref name="other"/> value is null.</exception>
public int CompareTo(ISemanticVersion other)
@@ -133,7 +116,7 @@ namespace StardewModdingAPI.Toolkit
return other != null && this.CompareTo(other) == 0;
}
- /// <summary>Whether this is a pre-release version.</summary>
+ /// <summary>Whether this is a prerelease version.</summary>
public bool IsPrerelease()
{
return !string.IsNullOrWhiteSpace(this.PrereleaseTag);
@@ -189,16 +172,10 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Get a string representation of the version.</summary>
public override string ToString()
{
- // version
- string result = this.PatchVersion != 0
- ? $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}"
- : $"{this.MajorVersion}.{this.MinorVersion}";
-
- // tag
- string tag = this.PrereleaseTag;
- if (tag != null)
- result += $"-{tag}";
- return result;
+ string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}";
+ if (this.PrereleaseTag != null)
+ version += $"-{this.PrereleaseTag}";
+ return version;
}
/// <summary>Parse a version string without throwing an exception if it fails.</summary>
@@ -223,15 +200,15 @@ namespace StardewModdingAPI.Toolkit
/*********
** Private methods
*********/
- /// <summary>Get a normalised build tag.</summary>
- /// <param name="tag">The tag to normalise.</param>
- private string GetNormalisedTag(string tag)
+ /// <summary>Get a normalized build tag.</summary>
+ /// <param name="tag">The tag to normalize.</param>
+ private string GetNormalizedTag(string tag)
{
tag = tag?.Trim();
return !string.IsNullOrWhiteSpace(tag) ? tag : null;
}
- /// <summary>Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version.</summary>
+ /// <summary>Get an integer indicating whether this version precedes (less than 0), supersedes (more than 0), or is equivalent to (0) the specified version.</summary>
/// <param name="otherMajor">The major version to compare with this instance.</param>
/// <param name="otherMinor">The minor version to compare with this instance.</param>
/// <param name="otherPatch">The patch version to compare with this instance.</param>
@@ -252,7 +229,7 @@ namespace StardewModdingAPI.Toolkit
if (this.PrereleaseTag == otherTag)
return same;
- // stable supercedes pre-release
+ // stable supersedes prerelease
bool curIsStable = string.IsNullOrWhiteSpace(this.PrereleaseTag);
bool otherIsStable = string.IsNullOrWhiteSpace(otherTag);
if (curIsStable)
@@ -260,12 +237,12 @@ namespace StardewModdingAPI.Toolkit
if (otherIsStable)
return curOlder;
- // compare two pre-release tag values
+ // compare two prerelease tag values
string[] curParts = this.PrereleaseTag.Split('.', '-');
string[] otherParts = otherTag.Split('.', '-');
for (int i = 0; i < curParts.Length; i++)
{
- // longer prerelease tag supercedes if otherwise equal
+ // longer prerelease tag supersedes if otherwise equal
if (otherParts.Length <= i)
return curNewer;
diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs
index 232c22a7..5cabe9d8 100644
--- a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestContentPackForConverter.cs
@@ -1,10 +1,10 @@
using System;
using Newtonsoft.Json;
-using StardewModdingAPI.Toolkit.Serialisation.Models;
+using StardewModdingAPI.Toolkit.Serialization.Models;
-namespace StardewModdingAPI.Toolkit.Serialisation.Converters
+namespace StardewModdingAPI.Toolkit.Serialization.Converters
{
- /// <summary>Handles deserialisation of <see cref="ManifestContentPackFor"/> arrays.</summary>
+ /// <summary>Handles deserialization of <see cref="ManifestContentPackFor"/> arrays.</summary>
public class ManifestContentPackForConverter : JsonConverter
{
/*********
diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs
index 0a304ee3..7b88d6b7 100644
--- a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/ManifestDependencyArrayConverter.cs
@@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-using StardewModdingAPI.Toolkit.Serialisation.Models;
+using StardewModdingAPI.Toolkit.Serialization.Models;
-namespace StardewModdingAPI.Toolkit.Serialisation.Converters
+namespace StardewModdingAPI.Toolkit.Serialization.Converters
{
- /// <summary>Handles deserialisation of <see cref="ManifestDependency"/> arrays.</summary>
+ /// <summary>Handles deserialization of <see cref="ManifestDependency"/> arrays.</summary>
internal class ManifestDependencyArrayConverter : JsonConverter
{
/*********
diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
index aca06849..ece4a72e 100644
--- a/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
@@ -2,9 +2,9 @@ using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-namespace StardewModdingAPI.Toolkit.Serialisation.Converters
+namespace StardewModdingAPI.Toolkit.Serialization.Converters
{
- /// <summary>Handles deserialisation of <see cref="ISemanticVersion"/>.</summary>
+ /// <summary>Handles deserialization of <see cref="ISemanticVersion"/>.</summary>
internal class SemanticVersionConverter : JsonConverter
{
/*********
@@ -67,20 +67,8 @@ namespace StardewModdingAPI.Toolkit.Serialisation.Converters
int minor = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.MinorVersion));
int patch = obj.ValueIgnoreCase<int>(nameof(ISemanticVersion.PatchVersion));
string prereleaseTag = obj.ValueIgnoreCase<string>(nameof(ISemanticVersion.PrereleaseTag));
-#if !SMAPI_3_0_STRICT
- if (string.IsNullOrWhiteSpace(prereleaseTag))
- {
- prereleaseTag = obj.ValueIgnoreCase<string>("Build");
- if (prereleaseTag == "0")
- prereleaseTag = null; // '0' from incorrect examples in old SMAPI documentation
- }
-#endif
- return new SemanticVersion(major, minor, patch, prereleaseTag
-#if !SMAPI_3_0_STRICT
- , isLegacyFormat: true
-#endif
- );
+ return new SemanticVersion(major, minor, patch, prereleaseTag);
}
/// <summary>Read a JSON string.</summary>
diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
index 5e0b0f4a..549f0c18 100644
--- a/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
@@ -2,10 +2,10 @@ using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-namespace StardewModdingAPI.Toolkit.Serialisation.Converters
+namespace StardewModdingAPI.Toolkit.Serialization.Converters
{
- /// <summary>The base implementation for simplified converters which deserialise <typeparamref name="T"/> without overriding serialisation.</summary>
- /// <typeparam name="T">The type to deserialise.</typeparam>
+ /// <summary>The base implementation for simplified converters which deserialize <typeparamref name="T"/> without overriding serialization.</summary>
+ /// <typeparam name="T">The type to deserialize.</typeparam>
internal abstract class SimpleReadOnlyConverter<T> : JsonConverter
{
/*********
diff --git a/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs
index 12b2c933..9aba53bf 100644
--- a/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs
+++ b/src/SMAPI.Toolkit/Serialization/InternalExtensions.cs
@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json.Linq;
-namespace StardewModdingAPI.Toolkit.Serialisation
+namespace StardewModdingAPI.Toolkit.Serialization
{
/// <summary>Provides extension methods for parsing JSON.</summary>
public static class JsonExtensions
diff --git a/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
index cf2ce0d1..031afbb0 100644
--- a/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs
+++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
@@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
-using StardewModdingAPI.Toolkit.Serialisation.Converters;
+using StardewModdingAPI.Toolkit.Serialization.Converters;
-namespace StardewModdingAPI.Toolkit.Serialisation
+namespace StardewModdingAPI.Toolkit.Serialization
{
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
public class JsonHelper
@@ -13,7 +13,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation
/*********
** Accessors
*********/
- /// <summary>The JSON settings to use when serialising and deserialising files.</summary>
+ /// <summary>The JSON settings to use when serializing and deserializing files.</summary>
public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
@@ -31,7 +31,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation
*********/
/// <summary>Read a JSON file.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
- /// <param name="fullPath">The absolete file path.</param>
+ /// <param name="fullPath">The absolute file path.</param>
/// <param name="result">The parsed content model.</param>
/// <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>
@@ -54,10 +54,10 @@ namespace StardewModdingAPI.Toolkit.Serialisation
return false;
}
- // deserialise model
+ // deserialize model
try
{
- result = this.Deserialise<TModel>(json);
+ result = this.Deserialize<TModel>(json);
return true;
}
catch (Exception ex)
@@ -77,7 +77,7 @@ namespace StardewModdingAPI.Toolkit.Serialisation
/// <summary>Save to a JSON file.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
- /// <param name="fullPath">The absolete file path.</param>
+ /// <param name="fullPath">The absolute file path.</param>
/// <param name="model">The model to save.</param>
/// <exception cref="InvalidOperationException">The given path is empty or invalid.</exception>
public void WriteJsonFile<TModel>(string fullPath, TModel model)
@@ -95,14 +95,14 @@ namespace StardewModdingAPI.Toolkit.Serialisation
Directory.CreateDirectory(dir);
// write file
- string json = this.Serialise(model);
+ string json = this.Serialize(model);
File.WriteAllText(fullPath, json);
}
/// <summary>Deserialize JSON text if possible.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="json">The raw JSON text.</param>
- public TModel Deserialise<TModel>(string json)
+ public TModel Deserialize<TModel>(string json)
{
try
{
@@ -126,9 +126,9 @@ namespace StardewModdingAPI.Toolkit.Serialisation
/// <summary>Serialize a model to JSON text.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
- /// <param name="model">The model to serialise.</param>
+ /// <param name="model">The model to serialize.</param>
/// <param name="formatting">The formatting to apply.</param>
- public string Serialise<TModel>(TModel model, Formatting formatting = Formatting.Indented)
+ public string Serialize<TModel>(TModel model, Formatting formatting = Formatting.Indented)
{
return JsonConvert.SerializeObject(model, formatting, this.JsonSettings);
}
diff --git a/src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
index 6cb9496b..99e85cbd 100644
--- a/src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using Newtonsoft.Json;
-using StardewModdingAPI.Toolkit.Serialisation.Converters;
+using StardewModdingAPI.Toolkit.Serialization.Converters;
-namespace StardewModdingAPI.Toolkit.Serialisation.Models
+namespace StardewModdingAPI.Toolkit.Serialization.Models
{
/// <summary>A manifest which describes a mod for SMAPI.</summary>
public class Manifest : IManifest
diff --git a/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs
index d0e42216..1eb80889 100644
--- a/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestContentPackFor.cs
@@ -1,4 +1,4 @@
-namespace StardewModdingAPI.Toolkit.Serialisation.Models
+namespace StardewModdingAPI.Toolkit.Serialization.Models
{
/// <summary>Indicates which mod can read the content pack represented by the containing manifest.</summary>
public class ManifestContentPackFor : IManifestContentPackFor
diff --git a/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs
index 8db58d5d..00f168f4 100644
--- a/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs
+++ b/src/SMAPI.Toolkit/Serialization/Models/ManifestDependency.cs
@@ -1,4 +1,4 @@
-namespace StardewModdingAPI.Toolkit.Serialisation.Models
+namespace StardewModdingAPI.Toolkit.Serialization.Models
{
/// <summary>A mod dependency listed in a mod manifest.</summary>
public class ManifestDependency : IManifestDependency
diff --git a/src/SMAPI.Toolkit/Serialisation/SParseException.cs b/src/SMAPI.Toolkit/Serialization/SParseException.cs
index 61a7b305..5f58b5b8 100644
--- a/src/SMAPI.Toolkit/Serialisation/SParseException.cs
+++ b/src/SMAPI.Toolkit/Serialization/SParseException.cs
@@ -1,6 +1,6 @@
using System;
-namespace StardewModdingAPI.Toolkit.Serialisation
+namespace StardewModdingAPI.Toolkit.Serialization
{
/// <summary>A format exception which provides a user-facing error message.</summary>
internal class SParseException : FormatException
diff --git a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj
deleted file mode 100644
index 46d38f17..00000000
--- a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
- <PropertyGroup>
- <TargetFrameworks>net4.5;netstandard2.0</TargetFrameworks>
- <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
- <OutputPath>..\..\bin\$(Configuration)\SMAPI.Toolkit</OutputPath>
- <DocumentationFile>..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml</DocumentationFile>
- <LangVersion>latest</LangVersion>
- <PlatformTarget Condition="'$(TargetFramework)' == 'net4.5'">x86</PlatformTarget>
- </PropertyGroup>
-
- <ItemGroup>
- <Compile Include="..\..\build\GlobalAssemblyInfo.cs" Link="Properties\GlobalAssemblyInfo.cs" />
- </ItemGroup>
-
- <ItemGroup>
- <PackageReference Include="HtmlAgilityPack" Version="1.8.9" />
- <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.2.0" />
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj" />
- </ItemGroup>
-
- <Import Project="..\..\build\common.targets" />
-
-</Project>
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
new file mode 100644
index 00000000..6dce5da5
--- /dev/null
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+#if SMAPI_FOR_WINDOWS
+using System.Management;
+#endif
+using System.Runtime.InteropServices;
+
+namespace StardewModdingAPI.Toolkit.Utilities
+{
+ /// <summary>Provides methods for fetching environment information.</summary>
+ public static class EnvironmentUtility
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The cached platform.</summary>
+ private static Platform? CachedPlatform;
+
+ /// <summary>Get the OS name from the system uname command.</summary>
+ /// <param name="buffer">The buffer to fill with the resulting string.</param>
+ [DllImport("libc")]
+ static extern int uname(IntPtr buffer);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Detect the current OS.</summary>
+ public static Platform DetectPlatform()
+ {
+ if (EnvironmentUtility.CachedPlatform == null)
+ EnvironmentUtility.CachedPlatform = EnvironmentUtility.DetectPlatformImpl();
+
+ return EnvironmentUtility.CachedPlatform.Value;
+ }
+
+
+ /// <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)
+ {
+#if SMAPI_FOR_WINDOWS
+ try
+ {
+ return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem")
+ .Get()
+ .Cast<ManagementObject>()
+ .Select(entry => entry.GetPropertyValue("Caption").ToString())
+ .FirstOrDefault();
+ }
+ catch { }
+#endif
+ return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion;
+ }
+
+ /// <summary>Get the name of the Stardew Valley executable.</summary>
+ /// <param name="platform">The current platform.</param>
+ public static string GetExecutableName(Platform platform)
+ {
+ return platform == Platform.Windows
+ ? "Stardew Valley.exe"
+ : "StardewValley.exe";
+ }
+
+ /// <summary>Get whether the platform uses Mono.</summary>
+ /// <param name="platform">The current platform.</param>
+ public static bool IsMono(this Platform platform)
+ {
+ return platform == Platform.Linux || platform == Platform.Mac;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Detect the current OS.</summary>
+ private static Platform DetectPlatformImpl()
+ {
+ switch (Environment.OSVersion.Platform)
+ {
+ case PlatformID.MacOSX:
+ return Platform.Mac;
+
+ case PlatformID.Unix when EnvironmentUtility.IsRunningAndroid():
+ return Platform.Android;
+
+ case PlatformID.Unix when EnvironmentUtility.IsRunningMac():
+ return Platform.Mac;
+
+ case PlatformID.Unix:
+ return Platform.Linux;
+
+ default:
+ return Platform.Windows;
+ }
+ }
+
+ /// <summary>Detect whether the code is running on Android.</summary>
+ /// <remarks>
+ /// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the
+ /// <c>getprop</c> system command to check for an Android-specific property.
+ /// </remarks>
+ private static bool IsRunningAndroid()
+ {
+ using (Process process = new Process())
+ {
+ process.StartInfo.FileName = "getprop";
+ process.StartInfo.Arguments = "ro.build.user";
+ process.StartInfo.RedirectStandardOutput = true;
+ process.StartInfo.UseShellExecute = false;
+ process.StartInfo.CreateNoWindow = true;
+ try
+ {
+ process.Start();
+ string output = process.StandardOutput.ReadToEnd();
+ return !string.IsNullOrEmpty(output);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+
+ /// <summary>Detect whether the code is running on Mac.</summary>
+ /// <remarks>
+ /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects Mac by calling the
+ /// <c>uname</c> system command and checking the response, which is always 'Darwin' for MacOS.
+ /// </remarks>
+ private static bool IsRunningMac()
+ {
+ IntPtr buffer = IntPtr.Zero;
+ try
+ {
+ buffer = Marshal.AllocHGlobal(8192);
+ if (EnvironmentUtility.uname(buffer) == 0)
+ {
+ string os = Marshal.PtrToStringAnsi(buffer);
+ return os == "Darwin";
+ }
+ return false;
+ }
+ catch
+ {
+ return false; // default to Linux
+ }
+ finally
+ {
+ if (buffer != IntPtr.Zero)
+ Marshal.FreeHGlobal(buffer);
+ }
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs
index 8a3c2b03..40a59d87 100644
--- a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs
+++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs
@@ -6,7 +6,7 @@ using System.Text.RegularExpressions;
namespace StardewModdingAPI.Toolkit.Utilities
{
- /// <summary>Provides utilities for normalising file paths.</summary>
+ /// <summary>Provides utilities for normalizing file paths.</summary>
public static class PathUtilities
{
/*********
@@ -15,14 +15,14 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>The possible directory separator characters in a file path.</summary>
private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
- /// <summary>The preferred directory separator chaeacter in an asset key.</summary>
+ /// <summary>The preferred directory separator character in an asset key.</summary>
private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
/*********
** Public methods
*********/
- /// <summary>Get the segments from a path (e.g. <c>/usr/bin/boop</c> => <c>usr</c>, <c>bin</c>, and <c>boop</c>).</summary>
+ /// <summary>Get the segments from a path (e.g. <c>/usr/bin/example</c> => <c>usr</c>, <c>bin</c>, and <c>example</c>).</summary>
/// <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>
public static string[] GetSegments(string path, int? limit = null)
@@ -32,16 +32,16 @@ namespace StardewModdingAPI.Toolkit.Utilities
: path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
}
- /// <summary>Normalise path separators in a file path.</summary>
- /// <param name="path">The file path to normalise.</param>
+ /// <summary>Normalize path separators in a file path.</summary>
+ /// <param name="path">The file path to normalize.</param>
[Pure]
- public static string NormalisePathSeparators(string path)
+ public static string NormalizePathSeparators(string path)
{
string[] parts = PathUtilities.GetSegments(path);
- string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts);
+ string normalized = string.Join(PathUtilities.PreferredPathSeparator, parts);
if (path.StartsWith(PathUtilities.PreferredPathSeparator))
- normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash
- return normalised;
+ normalized = PathUtilities.PreferredPathSeparator + normalized; // keep root slash
+ return normalized;
}
/// <summary>Get a directory or file path relative to a given source path.</summary>
@@ -57,7 +57,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'.");
// get relative path
- string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()));
+ string relative = PathUtilities.NormalizePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()));
if (relative == "")
relative = "./";
return relative;
diff --git a/src/SMAPI.Toolkit/Utilities/Platform.cs b/src/SMAPI.Toolkit/Utilities/Platform.cs
new file mode 100644
index 00000000..f780e812
--- /dev/null
+++ b/src/SMAPI.Toolkit/Utilities/Platform.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Toolkit.Utilities
+{
+ /// <summary>The game's platform version.</summary>
+ public enum Platform
+ {
+ /// <summary>The Android version of the game.</summary>
+ Android,
+
+ /// <summary>The Linux version of the game.</summary>
+ Linux,
+
+ /// <summary>The Mac version of the game.</summary>
+ Mac,
+
+ /// <summary>The Windows version of the game.</summary>
+ Windows
+ }
+}