From 125bcbee56bf40cf82abc7fdb502f8cbc18546cf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 13 Sep 2019 17:22:45 -0400 Subject: migrate to new project file format --- .../StardewModdingAPI.Installer.csproj | 75 ++-- .../StardewModdingAPI.ModBuildConfig.csproj | 79 ++--- .../StardewModdingAPI.Mods.ConsoleCommands.csproj | 92 +---- .../StardewModdingAPI.Mods.SaveBackup.csproj | 63 +--- src/SMAPI.Tests/StardewModdingAPI.Tests.csproj | 74 +--- src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs | 44 +++ .../IManifestContentPackFor.cs | 12 + .../IManifestDependency.cs | 18 + .../ISemanticVersion.cs | 68 ++++ .../Properties/AssemblyInfo.cs | 4 + ...StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 18 + .../Framework/Clients/WebApi/ModEntryModel.cs | 33 ++ .../Clients/WebApi/ModEntryVersionModel.cs | 31 ++ .../Clients/WebApi/ModExtendedMetadataModel.cs | 123 +++++++ .../Framework/Clients/WebApi/ModSeachModel.cs | 36 ++ .../Clients/WebApi/ModSearchEntryModel.cs | 34 ++ .../Framework/Clients/WebApi/WebApiClient.cs | 73 ++++ .../Framework/Clients/Wiki/WikiClient.cs | 237 +++++++++++++ .../Clients/Wiki/WikiCompatibilityInfo.cs | 24 ++ .../Clients/Wiki/WikiCompatibilityStatus.cs | 27 ++ .../Framework/Clients/Wiki/WikiModEntry.cs | 54 +++ .../Framework/Clients/Wiki/WikiModList.cs | 18 + .../Framework/ModData/MetadataModel.cs | 14 + .../Framework/ModData/ModDataField.cs | 82 +++++ .../Framework/ModData/ModDataFieldKey.cs | 18 + .../Framework/ModData/ModDataModel.cs | 127 +++++++ .../Framework/ModData/ModDataRecord.cs | 147 ++++++++ .../ModData/ModDataRecordVersionedFields.cs | 54 +++ src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs | 65 ++++ src/SMAPI.Toolkit/Framework/ModData/ModStatus.cs | 18 + src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs | 36 ++ .../Framework/ModScanning/ModFolder.cs | 64 ++++ .../Framework/ModScanning/ModScanner.cs | 183 ++++++++++ .../Framework/UpdateData/ModRepositoryKey.cs | 21 ++ .../Framework/UpdateData/UpdateKey.cs | 73 ++++ src/SMAPI.Toolkit/ModToolkit.cs | 89 +++++ src/SMAPI.Toolkit/Properties/AssemblyInfo.cs | 7 + src/SMAPI.Toolkit/SemanticVersion.cs | 312 +++++++++++++++++ .../Converters/ManifestContentPackForConverter.cs | 50 +++ .../Converters/ManifestDependencyArrayConverter.cs | 60 ++++ .../Converters/SemanticVersionConverter.cs | 98 ++++++ .../Converters/SimpleReadOnlyConverter.cs | 76 ++++ .../Serialisation/InternalExtensions.cs | 21 ++ src/SMAPI.Toolkit/Serialisation/JsonHelper.cs | 136 ++++++++ src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs | 74 ++++ .../Serialisation/Models/ManifestContentPackFor.cs | 15 + .../Serialisation/Models/ManifestDependency.cs | 35 ++ src/SMAPI.Toolkit/Serialisation/SParseException.cs | 17 + src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj | 28 ++ src/SMAPI.Toolkit/Utilities/FileUtilities.cs | 46 +++ src/SMAPI.Toolkit/Utilities/PathUtilities.cs | 85 +++++ src/SMAPI.Web/StardewModdingAPI.Web.csproj | 2 +- src/SMAPI.sln | 87 +++-- src/SMAPI/StardewModdingAPI.csproj | 381 ++------------------- .../IManifest.cs | 44 --- .../IManifestContentPackFor.cs | 12 - .../IManifestDependency.cs | 18 - .../ISemanticVersion.cs | 68 ---- .../Properties/AssemblyInfo.cs | 4 - ...StardewModdingAPI.Toolkit.CoreInterfaces.csproj | 18 - .../Framework/Clients/WebApi/ModEntryModel.cs | 33 -- .../Clients/WebApi/ModEntryVersionModel.cs | 31 -- .../Clients/WebApi/ModExtendedMetadataModel.cs | 123 ------- .../Framework/Clients/WebApi/ModSeachModel.cs | 36 -- .../Clients/WebApi/ModSearchEntryModel.cs | 34 -- .../Framework/Clients/WebApi/WebApiClient.cs | 73 ---- .../Framework/Clients/Wiki/WikiClient.cs | 237 ------------- .../Clients/Wiki/WikiCompatibilityInfo.cs | 24 -- .../Clients/Wiki/WikiCompatibilityStatus.cs | 27 -- .../Framework/Clients/Wiki/WikiModEntry.cs | 54 --- .../Framework/Clients/Wiki/WikiModList.cs | 18 - .../Framework/ModData/MetadataModel.cs | 14 - .../Framework/ModData/ModDataField.cs | 82 ----- .../Framework/ModData/ModDataFieldKey.cs | 18 - .../Framework/ModData/ModDataModel.cs | 127 ------- .../Framework/ModData/ModDataRecord.cs | 147 -------- .../ModData/ModDataRecordVersionedFields.cs | 54 --- .../Framework/ModData/ModDatabase.cs | 65 ---- .../Framework/ModData/ModStatus.cs | 18 - .../Framework/ModData/ModWarning.cs | 36 -- .../Framework/ModScanning/ModFolder.cs | 64 ---- .../Framework/ModScanning/ModScanner.cs | 183 ---------- .../Framework/UpdateData/ModRepositoryKey.cs | 21 -- .../Framework/UpdateData/UpdateKey.cs | 73 ---- src/StardewModdingAPI.Toolkit/ModToolkit.cs | 89 ----- .../Properties/AssemblyInfo.cs | 7 - src/StardewModdingAPI.Toolkit/SemanticVersion.cs | 312 ----------------- .../Converters/ManifestContentPackForConverter.cs | 50 --- .../Converters/ManifestDependencyArrayConverter.cs | 60 ---- .../Converters/SemanticVersionConverter.cs | 98 ------ .../Converters/SimpleReadOnlyConverter.cs | 76 ---- .../Serialisation/InternalExtensions.cs | 21 -- .../Serialisation/JsonHelper.cs | 136 -------- .../Serialisation/Models/Manifest.cs | 74 ---- .../Serialisation/Models/ManifestContentPackFor.cs | 15 - .../Serialisation/Models/ManifestDependency.cs | 35 -- .../Serialisation/SParseException.cs | 17 - .../StardewModdingAPI.Toolkit.csproj | 27 -- .../Utilities/FileUtilities.cs | 46 --- .../Utilities/PathUtilities.cs | 85 ----- 100 files changed, 3061 insertions(+), 3601 deletions(-) create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/IManifestDependency.cs create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs create mode 100644 src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModStatus.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs create mode 100644 src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs create mode 100644 src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs create mode 100644 src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs create mode 100644 src/SMAPI.Toolkit/ModToolkit.cs create mode 100644 src/SMAPI.Toolkit/Properties/AssemblyInfo.cs create mode 100644 src/SMAPI.Toolkit/SemanticVersion.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/JsonHelper.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs create mode 100644 src/SMAPI.Toolkit/Serialisation/SParseException.cs create mode 100644 src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj create mode 100644 src/SMAPI.Toolkit/Utilities/FileUtilities.cs create mode 100644 src/SMAPI.Toolkit/Utilities/PathUtilities.cs delete mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs delete mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs delete mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs delete mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs delete mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModData/ModWarning.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs delete mode 100644 src/StardewModdingAPI.Toolkit/ModToolkit.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs delete mode 100644 src/StardewModdingAPI.Toolkit/SemanticVersion.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs delete mode 100644 src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj delete mode 100644 src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs delete mode 100644 src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs (limited to 'src') diff --git a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj index 083044fb..ac64a774 100644 --- a/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/SMAPI.Installer/StardewModdingAPI.Installer.csproj @@ -1,62 +1,32 @@ - - - + + - Debug - x86 - {443DDF81-6AAF-420A-A610-3459F37E5575} - Exe - Properties StardewModdingAPI.Installer StardewModdingAPI.Installer - v4.5 - 512 - true + net45 + false latest - - - x86 - true - full - false - $(SolutionDir)\..\bin\Debug\Installer - DEBUG;TRACE - prompt - 4 - - + Exe x86 - pdbonly - true - $(SolutionDir)\..\bin\Release\Installer - TRACE - prompt - 4 + $(SolutionDir)\..\bin\$(Configuration)\Installer + false + - - - - - - - Properties\GlobalAssemblyInfo.cs - - - - - - + + - - Always - + + - + + PreserveNewest + + PreserveNewest - + PreserveNewest @@ -67,14 +37,9 @@ PreserveNewest - - - {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} - StardewModdingAPI.Toolkit - - + - - \ No newline at end of file + + diff --git a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj index 9118b043..f6cb4734 100644 --- a/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/StardewModdingAPI.ModBuildConfig.csproj @@ -1,73 +1,34 @@ - - - + + - Debug - x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A} - Library - Properties StardewModdingAPI.ModBuildConfig StardewModdingAPI.ModBuildConfig - v4.5 - 512 + net45 + false latest + x86 + false - - true - full - false - bin\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\ - TRACE - prompt - 4 - - - - - - - - - + - - - - + + - - - Designer + + mod-build-config.md - - Designer - - - - + - - {d5cfd923-37f1-4bc3-9be8-e506e202ac28} - StardewModdingAPI.Toolkit.CoreInterfaces - - - {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} - StardewModdingAPI.Toolkit - + + + + + + - - - \ No newline at end of file + + diff --git a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj index 2c958dbc..719a80e3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/StardewModdingAPI.Mods.ConsoleCommands.csproj @@ -1,93 +1,35 @@ - - - + + - Debug - x86 - {28480467-1A48-46A7-99F8-236D95225359} - Library - Properties StardewModdingAPI.Mods.ConsoleCommands ConsoleCommands - v4.5 - 512 + net45 + false latest - - - true - full - false - $(SolutionDir)\..\bin\Debug\Mods\ConsoleCommands\ - DEBUG;TRACE - prompt - 4 - x86 - false - true - - - pdbonly - true - $(SolutionDir)\..\bin\Release\Mods\ConsoleCommands\ - TRACE - prompt - 4 - false - true + $(SolutionDir)\..\bin\$(Configuration)\Mods\ConsoleCommands + false x86 + - - + + False + + Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} - StardewModdingAPI - False - + - + PreserveNewest - + + - \ No newline at end of file + + diff --git a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj index 56b6b7f4..3d378ca6 100644 --- a/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/StardewModdingAPI.Mods.SaveBackup.csproj @@ -1,62 +1,35 @@ - - - + + - Debug - x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4} - Library - Properties StardewModdingAPI.Mods.SaveBackup SaveBackup - v4.5 - 512 + net45 + false latest + $(SolutionDir)\..\bin\$(Configuration)\Mods\SaveBackup + false + x86 - - true - full - false - $(SolutionDir)\..\bin\Debug\Mods\SaveBackup\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - $(SolutionDir)\..\bin\Release\Mods\SaveBackup\ - TRACE - prompt - 4 - + - + + False + + Properties\GlobalAssemblyInfo.cs - - + - + PreserveNewest - - - {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} - StardewModdingAPI - False - - - {d5cfd923-37f1-4bc3-9be8-e506e202ac28} - StardewModdingAPI.Toolkit.CoreInterfaces - False - - - + + - \ No newline at end of file + + diff --git a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj index 83bd92af..1cb2d1e6 100644 --- a/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/SMAPI.Tests/StardewModdingAPI.Tests.csproj @@ -1,35 +1,20 @@ - - - + + - Debug - x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B} - Library - Properties StardewModdingAPI.Tests StardewModdingAPI.Tests - v4.5 - 512 + net45 + false latest + x86 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - + + + + + + + @@ -39,42 +24,17 @@ - - - - + Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - {f1a573b0-f436-472c-ae29-0b91ea6b9f8f} - StardewModdingAPI - - - {d5cfd923-37f1-4bc3-9be8-e506e202ac28} - StardewModdingAPI.Toolkit.CoreInterfaces - - - {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} - StardewModdingAPI.Toolkit - + - + - \ No newline at end of file + + diff --git a/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs new file mode 100644 index 00000000..7375f005 --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI +{ + /// A manifest which describes a mod for SMAPI. + public interface IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + string Name { get; } + + /// A brief description of the mod. + string Description { get; } + + /// The mod author's name. + string Author { get; } + + /// The mod version. + ISemanticVersion Version { get; } + + /// The minimum SMAPI version required by this mod, if any. + ISemanticVersion MinimumApiVersion { get; } + + /// The unique mod ID. + string UniqueID { get; } + + /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . + string EntryDll { get; } + + /// The mod which will read this as a content pack. Mutually exclusive with . + IManifestContentPackFor ContentPackFor { get; } + + /// The other mods that must be loaded before this mod. + IManifestDependency[] Dependencies { get; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + string[] UpdateKeys { get; } + + /// Any manifest fields which didn't match a valid field. + IDictionary ExtraFields { get; } + } +} diff --git a/src/SMAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs b/src/SMAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs new file mode 100644 index 00000000..f05a3873 --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public interface IManifestContentPackFor + { + /// The unique ID of the mod which can read this content pack. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + } +} diff --git a/src/SMAPI.Toolkit.CoreInterfaces/IManifestDependency.cs b/src/SMAPI.Toolkit.CoreInterfaces/IManifestDependency.cs new file mode 100644 index 00000000..e86cd1f4 --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/IManifestDependency.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI +{ + /// A mod dependency listed in a mod manifest. + public interface IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + string UniqueID { get; } + + /// The minimum required version (if any). + ISemanticVersion MinimumVersion { get; } + + /// Whether the dependency must be installed to use the mod. + bool IsRequired { get; } + } +} diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs new file mode 100644 index 00000000..0a6e5758 --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -0,0 +1,68 @@ +using System; + +namespace StardewModdingAPI +{ + /// A semantic version with an optional release tag. + public interface ISemanticVersion : IComparable, IEquatable + { + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + int PatchVersion { get; } + +#if !SMAPI_3_0_STRICT + /// An optional build tag. + [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] + string Build { get; } +#endif + + /// An optional prerelease tag. + string PrereleaseTag { get; } + + + /********* + ** Accessors + *********/ + /// Whether this is a pre-release version. + bool IsPrerelease(); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + bool IsOlderThan(ISemanticVersion other); + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsOlderThan(string other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + bool IsNewerThan(ISemanticVersion other); + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + bool IsNewerThan(string other); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + bool IsBetween(ISemanticVersion min, ISemanticVersion max); + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + bool IsBetween(string min, string max); + + /// Get a string representation of the version. + string ToString(); + } +} diff --git a/src/SMAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a29ba6cf --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Reflection; + +[assembly: AssemblyTitle("SMAPI.Toolkit.CoreInterfaces")] +[assembly: AssemblyDescription("Provides toolkit interfaces which are available to SMAPI mods.")] diff --git a/src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj new file mode 100644 index 00000000..67adbd67 --- /dev/null +++ b/src/SMAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj @@ -0,0 +1,18 @@ + + + + net4.5;netstandard2.0 + StardewModdingAPI + false + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces + ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml + latest + + + + + + + + + diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs new file mode 100644 index 00000000..8a9c0a25 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -0,0 +1,33 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a mod. + public class ModEntryModel + { + /********* + ** Accessors + *********/ + /// The mod's unique ID (if known). + public string ID { get; set; } + + /// The main version. + public ModEntryVersionModel Main { get; set; } + + /// The latest optional version, if newer than . + public ModEntryVersionModel Optional { get; set; } + + /// The latest unofficial version, if newer than and . + public ModEntryVersionModel Unofficial { get; set; } + + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + public ModEntryVersionModel UnofficialForBeta { get; set; } + + /// Optional extended data which isn't needed for update checks. + public ModExtendedMetadataModel Metadata { get; set; } + + /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . + public bool HasBetaInfo { get; set; } + + /// The errors that occurred while fetching update data. + public string[] Errors { get; set; } = new string[0]; + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs new file mode 100644 index 00000000..dadb8c10 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs @@ -0,0 +1,31 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Metadata about a version. + public class ModEntryVersionModel + { + /********* + ** Accessors + *********/ + /// The version number. + public ISemanticVersion Version { get; set; } + + /// The mod page URL. + public string Url { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModEntryVersionModel() { } + + /// Construct an instance. + /// The version number. + /// The mod page URL. + public ModEntryVersionModel(ISemanticVersion version, string url) + { + this.Version = version; + this.Url = url; + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs new file mode 100644 index 00000000..989c18b0 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Extended metadata about a mod. + public class ModExtendedMetadataModel + { + /********* + ** Accessors + *********/ + /**** + ** Mod info + ****/ + /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). + public string[] ID { get; set; } = new string[0]; + + /// The mod's display name. + public string Name { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The mod ID in the ModDrop mod repo. + public int? ModDropID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + + /**** + ** Stable compatibility + ****/ + /// The compatibility status. + [JsonConverter(typeof(StringEnumConverter))] + public WikiCompatibilityStatus? CompatibilityStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. + public string CompatibilitySummary { get; set; } + + /// The game or SMAPI version which broke this mod, if applicable. + public string BrokeIn { get; set; } + + + /**** + ** Beta compatibility + ****/ + /// The compatibility status for the Stardew Valley beta (if any). + [JsonConverter(typeof(StringEnumConverter))] + public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } + + /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatitng. + public string BetaCompatibilitySummary { get; set; } + + /// The beta game or SMAPI version which broke this mod, if applicable. + public string BetaBrokeIn { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModExtendedMetadataModel() { } + + /// Construct an instance. + /// The mod metadata from the wiki (if available). + /// The mod metadata from SMAPI's internal DB (if available). + public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) + { + // wiki data + if (wiki != null) + { + this.ID = wiki.ID; + this.Name = wiki.Name.FirstOrDefault(); + this.NexusID = wiki.NexusID; + this.ChucklefishID = wiki.ChucklefishID; + this.ModDropID = wiki.ModDropID; + this.GitHubRepo = wiki.GitHubRepo; + this.CustomSourceUrl = wiki.CustomSourceUrl; + this.CustomUrl = wiki.CustomUrl; + + this.CompatibilityStatus = wiki.Compatibility.Status; + this.CompatibilitySummary = wiki.Compatibility.Summary; + this.BrokeIn = wiki.Compatibility.BrokeIn; + + this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status; + this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary; + this.BetaBrokeIn = wiki.BetaCompatibility?.BrokeIn; + } + + // internal DB data + if (db != null) + { + this.ID = this.ID.Union(db.FormerIDs).ToArray(); + this.Name = this.Name ?? db.DisplayName; + } + } + + /// Get update keys based on the metadata. + public IEnumerable GetUpdateKeys() + { + if (this.NexusID.HasValue) + yield return $"Nexus:{this.NexusID}"; + if (this.ChucklefishID.HasValue) + yield return $"Chucklefish:{this.ChucklefishID}"; + if (this.GitHubRepo != null) + yield return $"GitHub:{this.GitHubRepo}"; + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs new file mode 100644 index 00000000..e352e1cc --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs @@ -0,0 +1,36 @@ +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies mods whose update-check info to fetch. + public class ModSearchModel + { + /********* + ** Accessors + *********/ + /// The mods for which to find data. + public ModSearchEntryModel[] Mods { get; set; } + + /// Whether to include extended metadata for each mod. + public bool IncludeExtendedMetadata { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The mods to search. + /// Whether to include extended metadata for each mod. + 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 new file mode 100644 index 00000000..bca47647 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Specifies the identifiers for a mod to match. + public class ModSearchEntryModel + { + /********* + ** Accessors + *********/ + /// The unique mod ID. + public string ID { get; set; } + + /// The namespaced mod update keys (if available). + public string[] UpdateKeys { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModSearchEntryModel() + { + // needed for JSON deserialising + } + + /// Construct an instance. + /// The unique mod ID. + /// The namespaced mod update keys (if available). + public ModSearchEntryModel(string id, string[] updateKeys) + { + this.ID = id; + this.UpdateKeys = updateKeys ?? new string[0]; + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs new file mode 100644 index 00000000..7c3df384 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi +{ + /// Provides methods for interacting with the SMAPI web API. + public class WebApiClient + { + /********* + ** Fields + *********/ + /// The base URL for the web API. + private readonly Uri BaseUrl; + + /// The API version number. + private readonly ISemanticVersion Version; + + /// The JSON serializer settings to use. + private readonly JsonSerializerSettings JsonSettings = new JsonHelper().JsonSettings; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The base URL for the web API. + /// The web API version. + public WebApiClient(string baseUrl, ISemanticVersion version) + { + this.BaseUrl = new Uri(baseUrl); + this.Version = version; + } + + /// Get metadata about a set of mods from the web API. + /// The mod keys for which to fetch the latest version. + /// Whether to include extended metadata for each mod. + public IDictionary GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) + { + return this.Post( + $"v{this.Version}/mods", + new ModSearchModel(mods, includeExtendedMetadata) + ).ToDictionary(p => p.ID); + } + + + /********* + ** Private methods + *********/ + /// Fetch the response from the backend API. + /// The body content type. + /// The expected response type. + /// The request URL, optionally excluding the base URL. + /// The body content to post. + private TResult Post(string url, TBody content) + { + // note: avoid HttpClient for Mac compatibility + using (WebClient client = new WebClient()) + { + Uri fullUrl = new Uri(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(response, this.JsonSettings); + } + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs new file mode 100644 index 00000000..3e9b8ea6 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using HtmlAgilityPack; +using Pathoschild.Http.Client; + +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// An HTTP client for fetching mod metadata from the wiki. + public class WikiClient : IDisposable + { + /********* + ** Fields + *********/ + /// The underlying HTTP client. + private readonly IClient Client; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The user agent for the wiki API. + /// The base URL for the wiki API. + public WikiClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") + { + this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + } + + /// Fetch mods from the compatibility list. + public async Task FetchModsAsync() + { + // fetch HTML + ResponseModel response = await this.Client + .GetAsync("") + .WithArguments(new + { + action = "parse", + page = "Modding:Mod_compatibility", + format = "json" + }) + .As(); + string html = response.Parse.Text["*"]; + + // parse HTML + var doc = new HtmlDocument(); + 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; + if (betaVersion == stableVersion) + betaVersion = null; + + // find mod entries + HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); + if (modNodes == null) + throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); + + // parse + WikiModEntry[] mods = this.ParseEntries(modNodes).ToArray(); + return new WikiModList + { + StableVersion = stableVersion, + BetaVersion = betaVersion, + Mods = mods + }; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Client?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// Parse valid mod compatibility entries. + /// The HTML compatibility entries. + private IEnumerable ParseEntries(IEnumerable nodes) + { + foreach (HtmlNode node in nodes) + { + // extract fields + string[] names = this.GetAttributeAsCsv(node, "data-name"); + string[] authors = this.GetAttributeAsCsv(node, "data-author"); + string[] ids = this.GetAttributeAsCsv(node, "data-id"); + string[] warnings = this.GetAttributeAsCsv(node, "data-warnings"); + int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); + int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); + 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"); + + // parse stable compatibility + WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo + { + Status = this.GetAttributeAsEnum(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; + { + WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum(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") + }; + } + } + + // yield model + yield return new WikiModEntry + { + ID = ids, + Name = names, + Author = authors, + NexusID = nexusID, + ChucklefishID = chucklefishID, + ModDropID = modDropID, + GitHubRepo = githubRepo, + CustomSourceUrl = customSourceUrl, + CustomUrl = customUrl, + ContentPackFor = contentPackFor, + Compatibility = compatibility, + BetaCompatibility = betaCompatibility, + Warnings = warnings, + Anchor = anchor + }; + } + } + + /// Get an attribute value. + /// The element whose attributes to read. + /// The attribute name. + private string GetAttribute(HtmlNode element, string name) + { + string value = element.GetAttributeValue(name, null); + if (string.IsNullOrWhiteSpace(value)) + return null; + + return WebUtility.HtmlDecode(value); + } + + /// Get an attribute value and parse it as a comma-delimited list of strings. + /// The element whose attributes to read. + /// The attribute name. + private string[] GetAttributeAsCsv(HtmlNode element, string name) + { + string raw = this.GetAttribute(element, name); + return !string.IsNullOrWhiteSpace(raw) + ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() + : new string[0]; + } + + /// Get an attribute value and parse it as an enum value. + /// The enum type. + /// The element whose attributes to read. + /// The attribute name. + private TEnum? GetAttributeAsEnum(HtmlNode element, string name) where TEnum : struct + { + string raw = this.GetAttribute(element, name); + if (raw == null) + return null; + if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value)) + throw new InvalidOperationException($"Unknown {typeof(TEnum).Name} value '{raw}' when parsing compatibility list."); + return value; + } + + /// Get an attribute value and parse it as a semantic version. + /// The element whose attributes to read. + /// The attribute name. + private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name) + { + string raw = this.GetAttribute(element, name); + return SemanticVersion.TryParse(raw, out ISemanticVersion version) + ? version + : null; + } + + /// Get an attribute value and parse it as a nullable int. + /// The element whose attributes to read. + /// The attribute name. + private int? GetAttributeAsNullableInt(HtmlNode element, string name) + { + string raw = this.GetAttribute(element, name); + if (raw != null && int.TryParse(raw, out int value)) + return value; + return null; + } + + /// Get the text of an element with the given class name. + /// The metadata container. + /// The field name. + private string GetInnerHtml(HtmlNode container, string className) + { + return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml; + } + + /// The response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseModel + { + /// The parse API results. + public ResponseParseModel Parse { get; set; } + } + + /// The inner response model for the MediaWiki parse API. + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] + [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] + private class ResponseParseModel + { + /// The parsed text. + public IDictionary Text { get; set; } + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs new file mode 100644 index 00000000..204acd2b --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs @@ -0,0 +1,24 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// Compatibility info for a mod. + public class WikiCompatibilityInfo + { + /********* + ** Accessors + *********/ + /// The compatibility status. + public WikiCompatibilityStatus Status { get; set; } + + /// The human-readable summary of the compatibility status or workaround, without HTML formatting. + public string Summary { get; set; } + + /// The game or SMAPI version which broke this mod (if applicable). + public string BrokeIn { get; set; } + + /// The version of the latest unofficial update, if applicable. + public ISemanticVersion UnofficialVersion { get; set; } + + /// The URL to the latest unofficial update, if applicable. + public string UnofficialUrl { get; set; } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs new file mode 100644 index 00000000..a1d2dfae --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs @@ -0,0 +1,27 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// The compatibility status for a mod. + public enum WikiCompatibilityStatus + { + /// The mod is compatible. + Ok = 0, + + /// The mod is compatible if you use an optional official download. + Optional = 1, + + /// The mod is compatible if you use an unofficial update. + Unofficial = 2, + + /// The mod isn't compatible, but the player can fix it or there's a good alternative. + Workaround = 3, + + /// The mod isn't compatible. + Broken = 4, + + /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. + Abandoned = 5, + + /// The mod is no longer needed and should be removed. + Obsolete = 6 + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs new file mode 100644 index 00000000..cf416cc6 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -0,0 +1,54 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// A mod entry in the wiki list. + public class WikiModEntry + { + /********* + ** Accessors + *********/ + /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. + public string[] ID { get; set; } + + /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. + public string[] Name { get; set; } + + /// The mod's author name. If the author has multiple names, the first one is the most canonical name. + public string[] Author { get; set; } + + /// The mod ID on Nexus. + public int? NexusID { get; set; } + + /// The mod ID in the Chucklefish mod repo. + public int? ChucklefishID { get; set; } + + /// The mod ID in the ModDrop mod repo. + public int? ModDropID { get; set; } + + /// The GitHub repository in the form 'owner/repo'. + public string GitHubRepo { get; set; } + + /// The URL to a non-GitHub source repo. + public string CustomSourceUrl { get; set; } + + /// The custom mod page URL (if applicable). + public string CustomUrl { get; set; } + + /// The name of the mod which loads this content pack, if applicable. + public string ContentPackFor { get; set; } + + /// The mod's compatibility with the latest stable version of the game. + public WikiCompatibilityInfo Compatibility { get; set; } + + /// The mod's compatibility with the latest beta version of the game (if any). + public WikiCompatibilityInfo BetaCompatibility { get; set; } + + /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . + public bool HasBetaInfo => this.BetaCompatibility != null; + + /// The human-readable warnings for players about this mod. + public string[] Warnings { get; set; } + + /// The link anchor for the mod entry in the wiki compatibility list. + public string Anchor { get; set; } + } +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs new file mode 100644 index 00000000..0d614f28 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki +{ + /// Metadata from the wiki's mod compatibility list. + public class WikiModList + { + /********* + ** Accessors + *********/ + /// The stable game version. + public string StableVersion { get; set; } + + /// The beta game version (if any). + public string BetaVersion { get; set; } + + /// The mods on the wiki. + public WikiModEntry[] Mods { get; set; } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs new file mode 100644 index 00000000..ef6d4dd9 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/MetadataModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The SMAPI predefined metadata. + internal class MetadataModel + { + /******** + ** Accessors + ********/ + /// Extra metadata about mods. + public IDictionary ModData { get; set; } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs new file mode 100644 index 00000000..b3954693 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataField.cs @@ -0,0 +1,82 @@ +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// A versioned mod metadata field. + public class ModDataField + { + /********* + ** Accessors + *********/ + /// The field key. + public ModDataFieldKey Key { get; } + + /// The field value. + public string Value { get; } + + /// Whether this field should only be applied if it's not already set. + public bool IsDefault { get; } + + /// The lowest version in the range, or null for all past versions. + public ISemanticVersion LowerVersion { get; } + + /// The highest version in the range, or null for all future versions. + public ISemanticVersion UpperVersion { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The field key. + /// The field value. + /// Whether this field should only be applied if it's not already set. + /// The lowest version in the range, or null for all past versions. + /// The highest version in the range, or null for all future versions. + public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) + { + this.Key = key; + this.Value = value; + this.IsDefault = isDefault; + this.LowerVersion = lowerVersion; + this.UpperVersion = upperVersion; + } + + /// Get whether this data field applies for the given manifest. + /// The mod manifest. + public bool IsMatch(IManifest manifest) + { + return + manifest?.Version != null // ignore invalid manifest + && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) + && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) + && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); + } + + + /********* + ** Private methods + *********/ + /// Get whether a manifest field has a meaningful value for the purposes of enforcing . + /// The mod manifest. + /// The field key matching . + private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) + { + switch (key) + { + // update key + case ModDataFieldKey.UpdateKey: + return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); + + // non-manifest fields + case ModDataFieldKey.AlternativeUrl: + case ModDataFieldKey.StatusReasonPhrase: + case ModDataFieldKey.Status: + return false; + + default: + return false; + } + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs new file mode 100644 index 00000000..09dd0cc5 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The valid field keys. + public enum ModDataFieldKey + { + /// A manifest update key. + UpdateKey, + + /// An alternative URL the player can check for an updated version. + AlternativeUrl, + + /// The mod's predefined compatibility status. + Status, + + /// A reason phrase for the , or null to use the default reason. + StatusReasonPhrase + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs new file mode 100644 index 00000000..18039762 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataModel.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The raw mod metadata from SMAPI's internal mod list. + internal class ModDataModel + { + /********* + ** Accessors + *********/ + /// The mod's current unique ID. + public string ID { get; set; } + + /// The former mod IDs (if any). + /// + /// This uses a custom format which uniquely identifies a mod across multiple versions and + /// supports matching other fields if no ID was specified. This doesn't include the latest + /// ID, if any. If the mod's ID changed over time, multiple variants can be separated by the + /// | character. + /// + public string FormerIDs { get; set; } + + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } = new Dictionary(); + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); + + /// The mod warnings to suppress, even if they'd normally be shown. + public ModWarning SuppressWarnings { get; set; } + + /// This field stores properties that aren't mapped to another field before they're parsed into . + [JsonExtensionData] + public IDictionary ExtensionData { get; set; } + + /// The versioned field data. + /// + /// This maps field names to values. This should be accessed via . + /// Format notes: + /// - Each key consists of a field name prefixed with any combination of version range + /// and Default, separated by pipes (whitespace trimmed). For example, Name + /// will always override the name, Default | Name will only override a blank + /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. + /// - The version format is min~max (where either side can be blank for unbounded), or + /// a single version number. + /// - The field name itself corresponds to a value. + /// + public IDictionary Fields { get; set; } = new Dictionary(); + + + /********* + ** Public methods + *********/ + /// Get a parsed representation of the . + public IEnumerable GetFields() + { + foreach (KeyValuePair pair in this.Fields) + { + // init fields + string packedKey = pair.Key; + string value = pair.Value; + bool isDefault = false; + ISemanticVersion lowerVersion = null; + ISemanticVersion upperVersion = null; + + // parse + string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); + ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); + foreach (string part in parts.Take(parts.Length - 1)) + { + // 'default' + if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) + { + isDefault = true; + continue; + } + + // version range + if (part.Contains("~")) + { + string[] versionParts = part.Split(new[] { '~' }, 2); + lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; + upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; + continue; + } + + // single version + lowerVersion = new SemanticVersion(part); + upperVersion = new SemanticVersion(part); + } + + yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); + } + } + + /// Get the former mod IDs. + public IEnumerable GetFormerIDs() + { + if (this.FormerIDs != null) + { + foreach (string id in this.FormerIDs.Split('|')) + yield return id.Trim(); + } + } + + + /********* + ** Private methods + *********/ + /// The method invoked after JSON deserialisation. + /// The deserialisation context. + [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; + } + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs new file mode 100644 index 00000000..794ad2e4 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecord.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The parsed mod metadata from SMAPI's internal mod list. + public class ModDataRecord + { + /********* + ** Accessors + *********/ + /// The mod's default display name. + public string DisplayName { get; } + + /// The mod's current unique ID. + public string ID { get; } + + /// The former mod IDs (if any). + public string[] FormerIDs { get; } + + /// The mod warnings to suppress, even if they'd normally be shown. + public ModWarning SuppressWarnings { get; set; } + + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; } + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; } + + /// The versioned field data. + public ModDataField[] Fields { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod's default display name. + /// The raw data model. + internal ModDataRecord(string displayName, ModDataModel model) + { + this.DisplayName = displayName; + this.ID = model.ID; + this.FormerIDs = model.GetFormerIDs().ToArray(); + this.SuppressWarnings = model.SuppressWarnings; + this.MapLocalVersions = new Dictionary(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); + this.MapRemoteVersions = new Dictionary(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); + this.Fields = model.GetFields().ToArray(); + } + + /// Get whether the mod has (or previously had) the given ID. + /// The mod ID. + public bool HasID(string id) + { + // try main ID + if (this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // try former IDs + foreach (string formerID in this.FormerIDs) + { + if (formerID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + + return false; + } + + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) + ? new SemanticVersion(newVersion) + : version; + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + 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; + } + + /// Get the possible mod IDs. + public IEnumerable GetIDs() + { + return this.FormerIDs + .Concat(new[] { this.ID }) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.Trim()) + .Distinct(); + } + + /// Get the default update key for this mod, if any. + public string GetDefaultUpdateKey() + { + string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; + return !string.IsNullOrWhiteSpace(updateKey) + ? updateKey + : null; + } + + /// Get a parsed representation of the which match a given manifest. + /// The manifest to match. + public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) + { + ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; + foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) + { + switch (field.Key) + { + // update key + case ModDataFieldKey.UpdateKey: + parsed.UpdateKey = field.Value; + break; + + // alternative URL + case ModDataFieldKey.AlternativeUrl: + parsed.AlternativeUrl = field.Value; + break; + + // status + case ModDataFieldKey.Status: + parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); + parsed.StatusUpperVersion = field.UpperVersion; + break; + + // status reason phrase + case ModDataFieldKey.StatusReasonPhrase: + parsed.StatusReasonPhrase = field.Value; + break; + } + } + + return parsed; + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs new file mode 100644 index 00000000..237f2c66 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs @@ -0,0 +1,54 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// The versioned fields from a for a specific manifest. + public class ModDataRecordVersionedFields + { + /********* + ** Accessors + *********/ + /// The underlying data record. + public ModDataRecord DataRecord { get; set; } + + /// The default mod name to display when the name isn't available (e.g. during dependency checks). + public string DisplayName { get; set; } + + /// The update key to apply. + public string UpdateKey { get; set; } + + /// The alternative URL the player can check for an updated version. + public string AlternativeUrl { get; set; } + + /// The predefined compatibility status. + public ModStatus Status { get; set; } = ModStatus.None; + + /// A reason phrase for the , or null to use the default reason. + public string StatusReasonPhrase { get; set; } + + /// The upper version for which the applies (if any). + public ISemanticVersion StatusUpperVersion { get; set; } + + + /********* + ** Public methods + *********/ + /// Get a semantic local version for update checks. + /// The remote version to normalise. + public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) + { + return this.DataRecord.GetLocalVersionForUpdateChecks(version); + } + + /// Get a semantic remote version for update checks. + /// The remote version to normalise. + 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/ModDatabase.cs b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs new file mode 100644 index 00000000..a9da884a --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModDatabase.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Handles access to SMAPI's internal mod metadata list. + public class ModDatabase + { + /********* + ** Fields + *********/ + /// The underlying mod data records indexed by default display name. + private readonly ModDataRecord[] Records; + + /// Get an update URL for an update key (if valid). + private readonly Func GetUpdateUrl; + + + /********* + ** Public methods + *********/ + /// Construct an empty instance. + public ModDatabase() + : this(new ModDataRecord[0], key => null) { } + + /// Construct an instance. + /// The underlying mod data records indexed by default display name. + /// Get an update URL for an update key (if valid). + public ModDatabase(IEnumerable records, Func getUpdateUrl) + { + this.Records = records.ToArray(); + this.GetUpdateUrl = getUpdateUrl; + } + + /// Get all mod data records. + public IEnumerable GetAll() + { + return this.Records; + } + + /// Get a mod data record. + /// The unique mod ID. + public ModDataRecord Get(string modID) + { + return !string.IsNullOrWhiteSpace(modID) + ? this.Records.FirstOrDefault(p => p.HasID(modID)) + : null; + } + + /// Get the mod page URL for a mod (if available). + /// The unique mod ID. + public string GetModPageUrlFor(string id) + { + // get update key + ModDataRecord record = this.Get(id); + ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); + if (updateKeyField == null) + return null; + + // get update URL + return this.GetUpdateUrl(updateKeyField.Value); + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModStatus.cs b/src/SMAPI.Toolkit/Framework/ModData/ModStatus.cs new file mode 100644 index 00000000..09da74bf --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModStatus.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Indicates how SMAPI should treat a mod. + public enum ModStatus + { + /// Don't override the status. + None, + + /// The mod is obsolete and shouldn't be used, regardless of version. + Obsolete, + + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs new file mode 100644 index 00000000..d61c427f --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs @@ -0,0 +1,36 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Framework.ModData +{ + /// Indicates a detected non-error mod issue. + [Flags] + public enum ModWarning + { + /// No issues detected. + None = 0, + + /// SMAPI detected incompatible code in the mod, but was configured to load it anyway. + BrokenCodeLoaded = 1, + + /// The mod affects the save serializer in a way that may make saves unloadable without the mod. + ChangesSaveSerialiser = 2, + + /// The mod patches the game in a way that may impact stability. + PatchesGame = 4, + + /// The mod uses the dynamic keyword which won't work on Linux/Mac. + UsesDynamic = 8, + + /// The mod references specialised 'unvalided update tick' events which may impact stability. + UsesUnvalidatedUpdateTick = 16, + + /// The mod has no update keys set. + NoUpdateKeys = 32, + + /// Uses .NET APIs for filesystem access. + AccessesFilesystem = 64, + + /// Uses .NET APIs for shell or process access. + AccessesShell = 128 + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs new file mode 100644 index 00000000..bb467b36 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; +using StardewModdingAPI.Toolkit.Utilities; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// The info about a mod read from its folder. + public class ModFolder + { + /********* + ** Accessors + *********/ + /// A suggested display name for the mod folder. + public string DisplayName { get; } + + /// The folder containing the mod's manifest.json. + public DirectoryInfo Directory { get; } + + /// The mod manifest. + public Manifest Manifest { get; } + + /// The error which occurred parsing the manifest, if any. + public string ManifestParseError { get; } + + /// Whether the mod should be loaded by default. This is false if it was found within a folder whose name starts with a dot. + public bool ShouldBeLoaded { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The root folder containing mods. + /// The folder containing the mod's manifest.json. + /// The mod manifest. + /// The error which occurred parsing the manifest, if any. + /// Whether the mod should be loaded by default. This should be false if it was found within a folder whose name starts with a dot. + public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) + { + // save info + this.Directory = directory; + this.Manifest = manifest; + this.ManifestParseError = manifestParseError; + this.ShouldBeLoaded = shouldBeLoaded; + + // set display name + this.DisplayName = manifest?.Name; + if (string.IsNullOrWhiteSpace(this.DisplayName)) + this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); + } + + /// Get the update keys for a mod. + /// The mod manifest. + public IEnumerable GetUpdateKeys(Manifest manifest) + { + return + (manifest.UpdateKeys ?? new string[0]) + .Where(p => !string.IsNullOrWhiteSpace(p)) + .ToArray(); + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs new file mode 100644 index 00000000..0ab73d56 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using StardewModdingAPI.Toolkit.Serialisation; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Framework.ModScanning +{ + /// Scans folders for mod data. + public class ModScanner + { + /********* + ** Fields + *********/ + /// The JSON helper with which to read manifests. + private readonly JsonHelper JsonHelper; + + /// A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod. + private readonly HashSet IgnoreFilesystemEntries = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ".DS_Store", + "mcs", + "Thumbs.db" + }; + + /// 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. + private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.InvariantCultureIgnoreCase) + { + ".md", + ".png", + ".txt", + ".xnb" + }; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The JSON helper with which to read manifests. + public ModScanner(JsonHelper jsonHelper) + { + this.JsonHelper = jsonHelper; + } + + /// Extract information about all mods in the given folder. + /// The root folder containing mods. + public IEnumerable GetModFolders(string rootPath) + { + DirectoryInfo root = new DirectoryInfo(rootPath); + return this.GetModFolders(root, root); + } + + /// Extract information from a mod folder. + /// The root folder containing mods. + /// The folder to search for a mod. + public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) + { + // find manifest.json + FileInfo manifestFile = this.FindManifest(searchFolder); + + // set appropriate invalid-mod error + if (manifestFile == null) + { + FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).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."); + } + + // read mod info + Manifest manifest = null; + string manifestError = null; + { + try + { + if (!this.JsonHelper.ReadJsonFileIfExists(manifestFile.FullName, out manifest) || manifest == null) + manifestError = "its manifest is invalid."; + } + catch (SParseException ex) + { + manifestError = $"parsing its manifest failed: {ex.Message}"; + } + catch (Exception ex) + { + manifestError = $"parsing its manifest failed:\n{ex}"; + } + } + + // normalise display fields + if (manifest != null) + { + manifest.Name = this.StripNewlines(manifest.Name); + manifest.Description = this.StripNewlines(manifest.Description); + manifest.Author = this.StripNewlines(manifest.Author); + } + + return new ModFolder(root, manifestFile.Directory, manifest, manifestError); + } + + + /********* + ** Private methods + *********/ + /// Recursively extract information about all mods in the given folder. + /// The root mod folder. + /// The folder to search for mods. + public IEnumerable 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); + + // recurse into subfolders + else if (this.IsModSearchFolder(root, folder)) + { + foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) + { + foreach (ModFolder match in this.GetModFolders(root, subfolder)) + yield return match; + } + } + + // treat as mod folder + else + yield return this.ReadFolder(root, folder); + } + + /// Find the manifest for a mod folder. + /// The folder to search. + 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; + } + + // not found + return null; + } + } + + /// Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods). + /// The root mod folder. + /// The folder to search for mods. + private bool IsModSearchFolder(DirectoryInfo root, DirectoryInfo folder) + { + if (root.FullName == folder.FullName) + return true; + + DirectoryInfo[] subfolders = folder.GetDirectories().Where(this.IsRelevant).ToArray(); + FileInfo[] files = folder.GetFiles().Where(this.IsRelevant).ToArray(); + return subfolders.Any() && !files.Any(); + } + + /// Get whether a file or folder is relevant when deciding how to process a mod folder. + /// The file or folder. + private bool IsRelevant(FileSystemInfo entry) + { + return !this.IgnoreFilesystemEntries.Contains(entry.Name); + } + + /// Strip newlines from a string. + /// The input to strip. + private string StripNewlines(string input) + { + return input?.Replace("\r", "").Replace("\n", ""); + } + } +} diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs new file mode 100644 index 00000000..f6c402d5 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs @@ -0,0 +1,21 @@ +namespace StardewModdingAPI.Toolkit.Framework.UpdateData +{ + /// A mod repository which SMAPI can check for updates. + public enum ModRepositoryKey + { + /// An unknown or invalid mod repository. + Unknown, + + /// The Chucklefish mod repository. + Chucklefish, + + /// A GitHub project containing releases. + GitHub, + + /// The ModDrop mod repository. + ModDrop, + + /// The Nexus Mods mod repository. + Nexus + } +} diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs new file mode 100644 index 00000000..865ebcf7 --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -0,0 +1,73 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Framework.UpdateData +{ + /// A namespaced mod ID which uniquely identifies a mod within a mod repository. + public class UpdateKey + { + /********* + ** Accessors + *********/ + /// The raw update key text. + public string RawText { get; } + + /// The mod repository containing the mod. + public ModRepositoryKey Repository { get; } + + /// The mod ID within the repository. + public string ID { get; } + + /// Whether the update key seems to be valid. + public bool LooksValid { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The raw update key text. + /// The mod repository containing the mod. + /// The mod ID within the repository. + public UpdateKey(string rawText, ModRepositoryKey repository, string id) + { + this.RawText = rawText; + this.Repository = repository; + this.ID = id; + this.LooksValid = + repository != ModRepositoryKey.Unknown + && !string.IsNullOrWhiteSpace(id); + } + + /// Parse a raw update key. + /// The raw update key to parse. + public static UpdateKey Parse(string raw) + { + // split parts + string[] parts = raw?.Split(':'); + if (parts == null || parts.Length != 2) + return new UpdateKey(raw, ModRepositoryKey.Unknown, null); + + // extract parts + string repositoryKey = parts[0].Trim(); + string id = parts[1].Trim(); + if (string.IsNullOrWhiteSpace(id)) + id = null; + + // parse + if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository)) + return new UpdateKey(raw, ModRepositoryKey.Unknown, id); + if (id == null) + return new UpdateKey(raw, repository, null); + + return new UpdateKey(raw, repository, id); + } + + /// Get a string that represents the current object. + public override string ToString() + { + return this.LooksValid + ? $"{this.Repository}:{this.ID}" + : this.RawText; + } + } +} diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs new file mode 100644 index 00000000..1b53e59e --- /dev/null +++ b/src/SMAPI.Toolkit/ModToolkit.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; +using StardewModdingAPI.Toolkit.Framework.ModData; +using StardewModdingAPI.Toolkit.Framework.ModScanning; +using StardewModdingAPI.Toolkit.Serialisation; + +namespace StardewModdingAPI.Toolkit +{ + /// A convenience wrapper for the various tools. + public class ModToolkit + { + /********* + ** Fields + *********/ + /// The default HTTP user agent for the toolkit. + private readonly string UserAgent; + + /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. + private readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) + { + ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", + ["GitHub"] = "https://github.com/{0}/releases", + ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" + }; + + + /********* + ** Accessors + *********/ + /// Encapsulates SMAPI's JSON parsing. + public JsonHelper JsonHelper { get; } = new JsonHelper(); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ModToolkit() + { + ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version); + this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; + } + + /// Extract mod metadata from the wiki compatibility list. + public async Task GetWikiCompatibilityListAsync() + { + var client = new WikiClient(this.UserAgent); + return await client.FetchModsAsync(); + } + + /// Get SMAPI's internal mod database. + /// The file path for the SMAPI metadata file. + public ModDatabase GetModDatabase(string metadataPath) + { + MetadataModel metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); + ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); + return new ModDatabase(records, this.GetUpdateUrl); + } + + /// Extract information about all mods in the given folder. + /// The root folder containing mods. + public IEnumerable GetModFolders(string rootPath) + { + return new ModScanner(this.JsonHelper).GetModFolders(rootPath); + } + + /// Get an update URL for an update key (if valid). + /// The update key. + public string GetUpdateUrl(string updateKey) + { + string[] parts = updateKey.Split(new[] { ':' }, 2); + if (parts.Length != 2) + return null; + + string vendorKey = parts[0].Trim(); + string modID = parts[1].Trim(); + + if (this.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) + return string.Format(urlTemplate, modID); + + return null; + } + } +} diff --git a/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..1bb19e8c --- /dev/null +++ b/src/SMAPI.Toolkit/Properties/AssemblyInfo.cs @@ -0,0 +1,7 @@ +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/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs new file mode 100644 index 00000000..ba9ca6c6 --- /dev/null +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -0,0 +1,312 @@ +using System; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Toolkit +{ + /// A semantic version with an optional release tag. + /// + /// The implementation is defined by Semantic Version 2.0 (https://semver.org/), with a few deviations: + /// - short-form "x.y" versions are supported (equivalent to "x.y.0"); + /// - hyphens are synonymous with dots in prerelease tags (like "-unofficial.3-pathoschild"); + /// - +build suffixes are not supported; + /// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial"). + /// + public class SemanticVersion : ISemanticVersion + { + /********* + ** Fields + *********/ + /// A regex pattern matching a valid prerelease tag. + internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; + + /// A regex pattern matching a version within a larger string. + internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?" + SemanticVersion.TagPattern + "))?"; + + /// A regular expression matching a semantic version string. + /// This pattern is derived from the BNF documentation in the semver repo, with deviations to support the Stardew Valley mod conventions (see remarks on ). + internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + + + /********* + ** Accessors + *********/ + /// The major version incremented for major API changes. + public int MajorVersion { get; } + + /// The minor version incremented for backwards-compatible changes. + public int MinorVersion { get; } + + /// The patch version for backwards-compatible bug fixes. + public int PatchVersion { get; } + + /// An optional prerelease tag. + public string PrereleaseTag { get; } + +#if !SMAPI_3_0_STRICT + /// An optional prerelease tag. + [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] + public string Build => this.PrereleaseTag; + + /// Whether the version was parsed from the legacy object format. + public bool IsLegacyFormat { get; } +#endif + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible fixes. + /// An optional prerelease tag. + /// Whether the version was parsed from the legacy object format. + public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null +#if !SMAPI_3_0_STRICT + , bool isLegacyFormat = false +#endif + ) + { + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.PrereleaseTag = this.GetNormalisedTag(prereleaseTag); +#if !SMAPI_3_0_STRICT + this.IsLegacyFormat = isLegacyFormat; +#endif + + this.AssertValid(); + } + + /// Construct an instance. + /// The assembly version. + /// The is null. + public SemanticVersion(Version version) + { + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version can't be null."); + + this.MajorVersion = version.Major; + this.MinorVersion = version.Minor; + this.PatchVersion = version.Build; + + this.AssertValid(); + } + + /// Construct an instance. + /// The semantic version string. + /// The is null. + /// The is not a valid semantic version. + public SemanticVersion(string version) + { + // parse + if (version == null) + throw new ArgumentNullException(nameof(version), "The input version string can't be null."); + var match = SemanticVersion.Regex.Match(version.Trim()); + if (!match.Success) + throw new FormatException($"The input '{version}' isn't a valid semantic version."); + + // initialise + 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.AssertValid(); + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The version to compare with this instance. + /// The value is null. + public int CompareTo(ISemanticVersion other) + { + if (other == null) + throw new ArgumentNullException(nameof(other)); + return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.PrereleaseTag); + } + + /// Indicates whether the current object is equal to another object of the same type. + /// true if the current object is equal to the parameter; otherwise, false. + /// An object to compare with this object. + public bool Equals(ISemanticVersion other) + { + return other != null && this.CompareTo(other) == 0; + } + + /// Whether this is a pre-release version. + public bool IsPrerelease() + { + return !string.IsNullOrWhiteSpace(this.PrereleaseTag); + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + public bool IsOlderThan(ISemanticVersion other) + { + return this.CompareTo(other) < 0; + } + + /// Get whether this version is older than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsOlderThan(string other) + { + return this.IsOlderThan(new SemanticVersion(other)); + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + public bool IsNewerThan(ISemanticVersion other) + { + return this.CompareTo(other) > 0; + } + + /// Get whether this version is newer than the specified version. + /// The version to compare with this instance. + /// The specified version is not a valid semantic version. + public bool IsNewerThan(string other) + { + return this.IsNewerThan(new SemanticVersion(other)); + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + public bool IsBetween(ISemanticVersion min, ISemanticVersion max) + { + return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; + } + + /// Get whether this version is between two specified versions (inclusively). + /// The minimum version. + /// The maximum version. + /// One of the specified versions is not a valid semantic version. + public bool IsBetween(string min, string max) + { + return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + } + + /// Get a string representation of the version. + 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; + } + + /// Parse a version string without throwing an exception if it fails. + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + public static bool TryParse(string version, out ISemanticVersion parsed) + { + try + { + parsed = new SemanticVersion(version); + return true; + } + catch + { + parsed = null; + return false; + } + } + + + /********* + ** Private methods + *********/ + /// Get a normalised build tag. + /// The tag to normalise. + private string GetNormalisedTag(string tag) + { + tag = tag?.Trim(); + return !string.IsNullOrWhiteSpace(tag) ? tag : null; + } + + /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. + /// The major version to compare with this instance. + /// The minor version to compare with this instance. + /// The patch version to compare with this instance. + /// The prerelease tag to compare with this instance. + private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + { + const int same = 0; + const int curNewer = 1; + const int curOlder = -1; + + // compare stable versions + if (this.MajorVersion != otherMajor) + return this.MajorVersion.CompareTo(otherMajor); + if (this.MinorVersion != otherMinor) + return this.MinorVersion.CompareTo(otherMinor); + if (this.PatchVersion != otherPatch) + return this.PatchVersion.CompareTo(otherPatch); + if (this.PrereleaseTag == otherTag) + return same; + + // stable supercedes pre-release + bool curIsStable = string.IsNullOrWhiteSpace(this.PrereleaseTag); + bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); + if (curIsStable) + return curNewer; + if (otherIsStable) + return curOlder; + + // compare two pre-release 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 + if (otherParts.Length <= i) + return curNewer; + + // compare if different + if (curParts[i] != otherParts[i]) + { + // unofficial is always lower-precedence + if (otherParts[i].Equals("unofficial", StringComparison.InvariantCultureIgnoreCase)) + return curNewer; + if (curParts[i].Equals("unofficial", StringComparison.InvariantCultureIgnoreCase)) + return curOlder; + + // compare numerically if possible + { + if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) + return curNum.CompareTo(otherNum); + } + + // else compare lexically + return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); + } + } + + // fallback (this should never happen) + return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + /// Assert that the current version is valid. + private void AssertValid() + { + if (this.MajorVersion < 0 || this.MinorVersion < 0 || this.PatchVersion < 0) + throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); + if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0) + throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); + if (this.PrereleaseTag != null) + { + if (this.PrereleaseTag.Trim() == "") + throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); + if (!Regex.IsMatch(this.PrereleaseTag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) + throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); + } + } + } +} diff --git a/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs new file mode 100644 index 00000000..232c22a7 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs @@ -0,0 +1,50 @@ +using System; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of arrays. + public class ManifestContentPackForConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ManifestContentPackFor[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return serializer.Deserialize(reader); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling 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/Serialisation/Converters/ManifestDependencyArrayConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs new file mode 100644 index 00000000..0a304ee3 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialisation.Models; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of arrays. + internal class ManifestDependencyArrayConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(ManifestDependency[]); + } + + + /********* + ** Protected methods + *********/ + /// Read the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling serializer. + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + List result = new List(); + foreach (JObject obj in JArray.Load(reader).Children()) + { + string uniqueID = obj.ValueIgnoreCase(nameof(ManifestDependency.UniqueID)); + string minVersion = obj.ValueIgnoreCase(nameof(ManifestDependency.MinimumVersion)); + bool required = obj.ValueIgnoreCase(nameof(ManifestDependency.IsRequired)) ?? true; + result.Add(new ManifestDependency(uniqueID, minVersion, required)); + } + return result.ToArray(); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling 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/Serialisation/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs new file mode 100644 index 00000000..aca06849 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs @@ -0,0 +1,98 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// Handles deserialisation of . + internal class SemanticVersionConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Get whether this converter can read JSON. + public override bool CanRead => true; + + /// Get whether this converter can write JSON. + public override bool CanWrite => true; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return typeof(ISemanticVersion).IsAssignableFrom(objectType); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling 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(), path); + default: + throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString()); + } + + + /********* + ** Private methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + private ISemanticVersion ReadObject(JObject obj) + { + int major = obj.ValueIgnoreCase(nameof(ISemanticVersion.MajorVersion)); + int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); + int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); + string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); +#if !SMAPI_3_0_STRICT + if (string.IsNullOrWhiteSpace(prereleaseTag)) + { + prereleaseTag = obj.ValueIgnoreCase("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 + ); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + private ISemanticVersion ReadString(string str, string path) + { + if (string.IsNullOrWhiteSpace(str)) + return null; + if (!SemanticVersion.TryParse(str, 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/Serialisation/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs new file mode 100644 index 00000000..5e0b0f4a --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs @@ -0,0 +1,76 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation.Converters +{ + /// The base implementation for simplified converters which deserialise without overriding serialisation. + /// The type to deserialise. + internal abstract class SimpleReadOnlyConverter : JsonConverter + { + /********* + ** Accessors + *********/ + /// Whether this converter can write JSON. + public override bool CanWrite => false; + + + /********* + ** Public methods + *********/ + /// Get whether this instance can convert the specified object type. + /// The object type. + public override bool CanConvert(Type objectType) + { + return objectType == typeof(T); + } + + /// Writes the JSON representation of the object. + /// The JSON writer. + /// The value. + /// The calling serializer. + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new InvalidOperationException("This converter does not write JSON."); + } + + /// Reads the JSON representation of the object. + /// The JSON reader. + /// The object type. + /// The object being read. + /// The calling 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), path); + case JsonToken.String: + return this.ReadString(JToken.Load(reader).Value(), path); + default: + throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); + } + } + + + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected virtual T ReadObject(JObject obj, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected virtual T ReadString(string str, string path) + { + throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs b/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs new file mode 100644 index 00000000..12b2c933 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/InternalExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// Provides extension methods for parsing JSON. + public static class JsonExtensions + { + /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. + /// The value type. + /// The JSON object to search. + /// The field name. + public static T ValueIgnoreCase(this JObject obj, string fieldName) + { + JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); + return token != null + ? token.Value() + : default(T); + } + } +} diff --git a/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs b/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs new file mode 100644 index 00000000..cf2ce0d1 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/JsonHelper.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// Encapsulates SMAPI's JSON file parsing. + public class JsonHelper + { + /********* + ** Accessors + *********/ + /// The JSON settings to use when serialising and deserialising files. + public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded + Converters = new List + { + new SemanticVersionConverter(), + new StringEnumConverter() + } + }; + + + /********* + ** Public methods + *********/ + /// Read a JSON file. + /// The model type. + /// The absolete file path. + /// The parsed content model. + /// Returns false if the file doesn't exist, else true. + /// The given is empty or invalid. + /// The file contains invalid JSON. + public bool ReadJsonFileIfExists(string fullPath, out TModel result) + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // read file + string json; + try + { + json = File.ReadAllText(fullPath); + } + catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + { + result = default(TModel); + return false; + } + + // deserialise model + try + { + result = this.Deserialise(json); + return true; + } + catch (Exception ex) + { + string error = $"Can't parse JSON file at {fullPath}."; + + if (ex is JsonReaderException) + { + error += " This doesn't seem to be valid JSON."; + if (json.Contains("“") || json.Contains("”")) + error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; + } + error += $"\nTechnical details: {ex.Message}"; + throw new JsonReaderException(error); + } + } + + /// Save to a JSON file. + /// The model type. + /// The absolete file path. + /// The model to save. + /// The given path is empty or invalid. + public void WriteJsonFile(string fullPath, TModel model) + where TModel : class + { + // validate + if (string.IsNullOrWhiteSpace(fullPath)) + throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); + + // create directory if needed + string dir = Path.GetDirectoryName(fullPath); + if (dir == null) + throw new ArgumentException("The file path is invalid.", nameof(fullPath)); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + // write file + string json = this.Serialise(model); + File.WriteAllText(fullPath, json); + } + + /// Deserialize JSON text if possible. + /// The model type. + /// The raw JSON text. + public TModel Deserialise(string json) + { + try + { + return JsonConvert.DeserializeObject(json, this.JsonSettings); + } + catch (JsonReaderException) + { + // try replacing curly quotes + if (json.Contains("“") || json.Contains("”")) + { + try + { + return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); + } + catch { /* rethrow original error */ } + } + + throw; + } + } + + /// Serialize a model to JSON text. + /// The model type. + /// The model to serialise. + /// The formatting to apply. + public string Serialise(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/Serialisation/Models/Manifest.cs new file mode 100644 index 00000000..6cb9496b --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Models/Manifest.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using StardewModdingAPI.Toolkit.Serialisation.Converters; + +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// A manifest which describes a mod for SMAPI. + public class Manifest : IManifest + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// A brief description of the mod. + public string Description { get; set; } + + /// The mod author's name. + public string Author { get; set; } + + /// The mod version. + public ISemanticVersion Version { get; set; } + + /// The minimum SMAPI version required by this mod, if any. + public ISemanticVersion MinimumApiVersion { get; set; } + + /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . + public string EntryDll { get; set; } + + /// The mod which will read this as a content pack. Mutually exclusive with . + [JsonConverter(typeof(ManifestContentPackForConverter))] + public IManifestContentPackFor ContentPackFor { get; set; } + + /// The other mods that must be loaded before this mod. + [JsonConverter(typeof(ManifestDependencyArrayConverter))] + public IManifestDependency[] Dependencies { get; set; } + + /// The namespaced mod IDs to query for updates (like Nexus:541). + public string[] UpdateKeys { get; set; } + + /// The unique mod ID. + public string UniqueID { get; set; } + + /// Any manifest fields which didn't match a valid field. + [JsonExtensionData] + public IDictionary ExtraFields { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public Manifest() { } + + /// Construct an instance for a transitional content pack. + /// The unique mod ID. + /// The mod name. + /// The mod author's name. + /// A brief description of the mod. + /// The mod version. + /// The modID which will read this as a content pack. + public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) + { + this.Name = name; + this.Author = author; + this.Description = description; + this.Version = version; + this.UniqueID = uniqueID; + this.UpdateKeys = new string[0]; + this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs new file mode 100644 index 00000000..d0e42216 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs @@ -0,0 +1,15 @@ +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// Indicates which mod can read the content pack represented by the containing manifest. + public class ManifestContentPackFor : IManifestContentPackFor + { + /********* + ** Accessors + *********/ + /// The unique ID of the mod which can read this content pack. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + } +} diff --git a/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs new file mode 100644 index 00000000..8db58d5d --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/Models/ManifestDependency.cs @@ -0,0 +1,35 @@ +namespace StardewModdingAPI.Toolkit.Serialisation.Models +{ + /// A mod dependency listed in a mod manifest. + public class ManifestDependency : IManifestDependency + { + /********* + ** Accessors + *********/ + /// The unique mod ID to require. + public string UniqueID { get; set; } + + /// The minimum required version (if any). + public ISemanticVersion MinimumVersion { get; set; } + + /// Whether the dependency must be installed to use the mod. + public bool IsRequired { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The unique mod ID to require. + /// The minimum required version (if any). + /// Whether the dependency must be installed to use the mod. + public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) + { + this.UniqueID = uniqueID; + this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) + ? new SemanticVersion(minimumVersion) + : null; + this.IsRequired = required; + } + } +} diff --git a/src/SMAPI.Toolkit/Serialisation/SParseException.cs b/src/SMAPI.Toolkit/Serialisation/SParseException.cs new file mode 100644 index 00000000..61a7b305 --- /dev/null +++ b/src/SMAPI.Toolkit/Serialisation/SParseException.cs @@ -0,0 +1,17 @@ +using System; + +namespace StardewModdingAPI.Toolkit.Serialisation +{ + /// A format exception which provides a user-facing error message. + internal class SParseException : FormatException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SParseException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj new file mode 100644 index 00000000..46d38f17 --- /dev/null +++ b/src/SMAPI.Toolkit/StardewModdingAPI.Toolkit.csproj @@ -0,0 +1,28 @@ + + + + net4.5;netstandard2.0 + false + ..\..\bin\$(Configuration)\SMAPI.Toolkit + ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml + latest + x86 + + + + + + + + + + + + + + + + + + + diff --git a/src/SMAPI.Toolkit/Utilities/FileUtilities.cs b/src/SMAPI.Toolkit/Utilities/FileUtilities.cs new file mode 100644 index 00000000..7856fdb1 --- /dev/null +++ b/src/SMAPI.Toolkit/Utilities/FileUtilities.cs @@ -0,0 +1,46 @@ +using System.IO; +using System.Threading; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides utilities for dealing with files. + public static class FileUtilities + { + /********* + ** Public methods + *********/ + /// Delete a file or folder regardless of file permissions, and block until deletion completes. + /// The file or folder to reset. + public static void ForceDelete(FileSystemInfo entry) + { + // ignore if already deleted + entry.Refresh(); + if (!entry.Exists) + return; + + // delete children + if (entry is DirectoryInfo folder) + { + foreach (FileSystemInfo child in folder.GetFileSystemInfos()) + FileUtilities.ForceDelete(child); + } + + // reset permissions & delete + entry.Attributes = FileAttributes.Normal; + entry.Delete(); + + // wait for deletion to finish + for (int i = 0; i < 10; i++) + { + entry.Refresh(); + if (entry.Exists) + Thread.Sleep(500); + } + + // throw exception if deletion didn't happen before timeout + entry.Refresh(); + if (entry.Exists) + throw new IOException($"Timed out trying to delete {entry.FullName}"); + } + } +} diff --git a/src/SMAPI.Toolkit/Utilities/PathUtilities.cs b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs new file mode 100644 index 00000000..8a3c2b03 --- /dev/null +++ b/src/SMAPI.Toolkit/Utilities/PathUtilities.cs @@ -0,0 +1,85 @@ +using System; +using System.Diagnostics.Contracts; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI.Toolkit.Utilities +{ + /// Provides utilities for normalising file paths. + public static class PathUtilities + { + /********* + ** Fields + *********/ + /// The possible directory separator characters in a file path. + private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); + + /// The preferred directory separator chaeacter in an asset key. + private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); + + + /********* + ** Public methods + *********/ + /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). + /// The path to split. + /// The number of segments to match. Any additional segments will be merged into the last returned part. + public static string[] GetSegments(string path, int? limit = null) + { + return limit.HasValue + ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) + : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); + } + + /// Normalise path separators in a file path. + /// The file path to normalise. + [Pure] + public static string NormalisePathSeparators(string path) + { + string[] parts = PathUtilities.GetSegments(path); + string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); + if (path.StartsWith(PathUtilities.PreferredPathSeparator)) + normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash + return normalised; + } + + /// Get a directory or file path relative to a given source path. + /// The source folder path. + /// The target folder or file path. + [Pure] + public static string GetRelativePath(string sourceDir, string targetPath) + { + // convert to URIs + Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); + if (from.Scheme != to.Scheme) + 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())); + if (relative == "") + relative = "./"; + return relative; + } + + /// Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain ../). + /// The path to check. + public static bool IsSafeRelativePath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return true; + + return + !Path.IsPathRooted(path) + && PathUtilities.GetSegments(path).All(segment => segment.Trim() != ".."); + } + + /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). + /// The string to check. + public static bool IsSlug(string str) + { + return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); + } + } +} diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj index 73476a8e..52ef1f1b 100644 --- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj +++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/SMAPI.sln b/src/SMAPI.sln index e01a69ba..ffd50455 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -1,14 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2036 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28729.10 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\StardewModdingAPI.Mods.ConsoleCommands.csproj", "{28480467-1A48-46A7-99F8-236D95225359}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI", "SMAPI\StardewModdingAPI.csproj", "{F1A573B0-F436-472C-AE29-0B91EA6B9F8F}" - ProjectSection(ProjectDependencies) = postProject - {80AD8528-AA49-4731-B4A6-C691845815A1} = {80AD8528-AA49-4731-B4A6-C691845815A1} - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI", "SMAPI\StardewModdingAPI.csproj", "{1298F2B2-57BD-4647-AF70-1FCBBEE500B6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" ProjectSection(SolutionItems) = preProject @@ -18,14 +13,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE ..\LICENSE.txt = ..\LICENSE.txt EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{443DDF81-6AAF-420A-A610-3459F37E5575}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Installer", "SMAPI.Installer\StardewModdingAPI.Installer.csproj", "{0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}" ProjectSection(ProjectDependencies) = postProject - {E272EB5D-8C57-417E-8E60-C1079D3F53C4} = {E272EB5D-8C57-417E-8E60-C1079D3F53C4} - {28480467-1A48-46A7-99F8-236D95225359} = {28480467-1A48-46A7-99F8-236D95225359} - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} = {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} + {E4113F3E-3CAA-4288-9378-39A77BA625DB} = {E4113F3E-3CAA-4288-9378-39A77BA625DB} + {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7} = {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7} + {1298F2B2-57BD-4647-AF70-1FCBBEE500B6} = {1298F2B2-57BD-4647-AF70-1FCBBEE500B6} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{36CCB19E-92EB-48C7-9615-98EEFD45109B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Tests", "SMAPI.Tests\StardewModdingAPI.Tests.csproj", "{E023DA12-5960-4101-80B9-A7DCE955725C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Web", "SMAPI.Web\StardewModdingAPI.Web.csproj", "{A308F679-51A3-4006-92D5-BAEC7EBD01A1}" EndProject @@ -47,10 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5 ..\build\prepare-nuget-package.targets = ..\build\prepare-nuget-package.targets EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{EA4F1E80-743F-4A1D-9757-AE66904A196A}" - ProjectSection(ProjectDependencies) = postProject - {80AD8528-AA49-4731-B4A6-C691845815A1} = {80AD8528-AA49-4731-B4A6-C691845815A1} - EndProjectSection +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildConfig", "SMAPI.ModBuildConfig\StardewModdingAPI.ModBuildConfig.csproj", "{C11D0AFB-2893-41A9-AD55-D002F032D6AD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.ModBuildConfig.Analyzer", "SMAPI.ModBuildConfig.Analyzer\StardewModdingAPI.ModBuildConfig.Analyzer.csproj", "{80AD8528-AA49-4731-B4A6-C691845815A1}" EndProject @@ -58,11 +50,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyz EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "StardewModdingAPI.Internal", "SMAPI.Internal\StardewModdingAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StardewModdingAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\StardewModdingAPI.Mods.SaveBackup.csproj", "{E272EB5D-8C57-417E-8E60-C1079D3F53C4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\StardewModdingAPI.Mods.ConsoleCommands.csproj", "{8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\StardewModdingAPI.Mods.SaveBackup.csproj", "{E4113F3E-3CAA-4288-9378-39A77BA625DB}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit", "SMAPI.Toolkit\StardewModdingAPI.Toolkit.csproj", "{EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit.CoreInterfaces", "StardewModdingAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj", "{D5CFD923-37F1-4BC3-9BE8-E506E202AC28}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StardewModdingAPI.Toolkit.CoreInterfaces", "SMAPI.Toolkit.CoreInterfaces\StardewModdingAPI.Toolkit.CoreInterfaces.csproj", "{D5CFD923-37F1-4BC3-9BE8-E506E202AC28}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{4B1CEB70-F756-4A57-AAE8-8CD78C475F25}" ProjectSection(SolutionItems) = preProject @@ -79,40 +73,33 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEM EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution - SMAPI.Internal\SMAPI.Internal.projitems*{443ddf81-6aaf-420a-a610-3459f37e5575}*SharedItemsImports = 4 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 - SMAPI.Internal\SMAPI.Internal.projitems*{ea4f1e80-743f-4a1d-9757-ae66904a196a}*SharedItemsImports = 4 - SMAPI.Internal\SMAPI.Internal.projitems*{f1a573b0-f436-472c-ae29-0b91ea6b9f8f}*SharedItemsImports = 4 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.ActiveCfg = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Debug|Any CPU.Build.0 = Debug|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.ActiveCfg = Release|x86 - {28480467-1A48-46A7-99F8-236D95225359}.Release|Any CPU.Build.0 = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.ActiveCfg = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Debug|Any CPU.Build.0 = Debug|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.ActiveCfg = Release|x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F}.Release|Any CPU.Build.0 = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.ActiveCfg = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Debug|Any CPU.Build.0 = Debug|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.ActiveCfg = Release|x86 - {443DDF81-6AAF-420A-A610-3459F37E5575}.Release|Any CPU.Build.0 = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.ActiveCfg = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Debug|Any CPU.Build.0 = Debug|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.ActiveCfg = Release|x86 - {36CCB19E-92EB-48C7-9615-98EEFD45109B}.Release|Any CPU.Build.0 = Release|x86 + {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1298F2B2-57BD-4647-AF70-1FCBBEE500B6}.Release|Any CPU.Build.0 = Release|Any CPU + {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0ED5EAD8-5D85-420D-8101-6D8CCCE29C9B}.Release|Any CPU.Build.0 = Release|Any CPU + {E023DA12-5960-4101-80B9-A7DCE955725C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E023DA12-5960-4101-80B9-A7DCE955725C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E023DA12-5960-4101-80B9-A7DCE955725C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E023DA12-5960-4101-80B9-A7DCE955725C}.Release|Any CPU.Build.0 = Release|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {A308F679-51A3-4006-92D5-BAEC7EBD01A1}.Release|Any CPU.Build.0 = Release|Any CPU - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.ActiveCfg = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Debug|Any CPU.Build.0 = Debug|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.ActiveCfg = Release|x86 - {EA4F1E80-743F-4A1D-9757-AE66904A196A}.Release|Any CPU.Build.0 = Release|x86 + {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C11D0AFB-2893-41A9-AD55-D002F032D6AD}.Release|Any CPU.Build.0 = Release|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {80AD8528-AA49-4731-B4A6-C691845815A1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -121,10 +108,6 @@ Global {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {0CF97929-B0D0-4D73-B7BF-4FF7191035F9}.Release|Any CPU.Build.0 = Release|Any CPU - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.ActiveCfg = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Debug|Any CPU.Build.0 = Debug|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.ActiveCfg = Release|x86 - {E272EB5D-8C57-417E-8E60-C1079D3F53C4}.Release|Any CPU.Build.0 = Release|x86 {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA5CFD2E-9453-4D29-B80F-8E0EA23F4AC6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -133,12 +116,20 @@ Global {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5CFD923-37F1-4BC3-9BE8-E506E202AC28}.Release|Any CPU.Build.0 = Release|Any CPU + {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C2CA4AB-BA8A-446A-B59E-9D6502E145F7}.Release|Any CPU.Build.0 = Release|Any CPU + {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4113F3E-3CAA-4288-9378-39A77BA625DB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {36CCB19E-92EB-48C7-9615-98EEFD45109B} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} + {E023DA12-5960-4101-80B9-A7DCE955725C} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} {0CF97929-B0D0-4D73-B7BF-4FF7191035F9} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 673deab0..eda53025 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -1,69 +1,28 @@ - - - + + - Debug - x86 - {F1A573B0-F436-472C-AE29-0B91EA6B9F8F} - Exe - Properties StardewModdingAPI StardewModdingAPI - v4.5 - 512 - false - - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true + net45 + false latest - true - - - x86 - false - DEBUG;TRACE - true - false - $(SolutionDir)\..\bin\Debug\SMAPI - $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml - true - - x86 - false - $(SolutionDir)\..\bin\Release\SMAPI - $(SolutionDir)\..\bin\Release\SMAPI\StardewModdingAPI.xml - TRACE - true - true - pdbonly - true - - + Exe + $(SolutionDir)\..\bin\$(Configuration)\SMAPI + $(SolutionDir)\..\bin\$(Configuration)\SMAPI\StardewModdingAPI.xml + false + true icon.ico + + - - - True @@ -71,282 +30,17 @@ True - + + + + + + - - Properties\GlobalAssemblyInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + PreserveNewest @@ -355,39 +49,12 @@ StardewModdingAPI.metadata.json PreserveNewest - - - - + PreserveNewest - - - - - False - Microsoft .NET Framework 4.5 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - - - - - {d5cfd923-37f1-4bc3-9be8-e506e202ac28} - StardewModdingAPI.Toolkit.CoreInterfaces - - - {ea5cfd2e-9453-4d29-b80f-8e0ea23f4ac6} - StardewModdingAPI.Toolkit - + + - - \ No newline at end of file + + diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs deleted file mode 100644 index 7375f005..00000000 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI -{ - /// A manifest which describes a mod for SMAPI. - public interface IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - string Name { get; } - - /// A brief description of the mod. - string Description { get; } - - /// The mod author's name. - string Author { get; } - - /// The mod version. - ISemanticVersion Version { get; } - - /// The minimum SMAPI version required by this mod, if any. - ISemanticVersion MinimumApiVersion { get; } - - /// The unique mod ID. - string UniqueID { get; } - - /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . - string EntryDll { get; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - IManifestContentPackFor ContentPackFor { get; } - - /// The other mods that must be loaded before this mod. - IManifestDependency[] Dependencies { get; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - string[] UpdateKeys { get; } - - /// Any manifest fields which didn't match a valid field. - IDictionary ExtraFields { get; } - } -} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs deleted file mode 100644 index f05a3873..00000000 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestContentPackFor.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace StardewModdingAPI -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - public interface IManifestContentPackFor - { - /// The unique ID of the mod which can read this content pack. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - } -} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs deleted file mode 100644 index e86cd1f4..00000000 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/IManifestDependency.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI -{ - /// A mod dependency listed in a mod manifest. - public interface IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - string UniqueID { get; } - - /// The minimum required version (if any). - ISemanticVersion MinimumVersion { get; } - - /// Whether the dependency must be installed to use the mod. - bool IsRequired { get; } - } -} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs deleted file mode 100644 index 0a6e5758..00000000 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; - -namespace StardewModdingAPI -{ - /// A semantic version with an optional release tag. - public interface ISemanticVersion : IComparable, IEquatable - { - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - int PatchVersion { get; } - -#if !SMAPI_3_0_STRICT - /// An optional build tag. - [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] - string Build { get; } -#endif - - /// An optional prerelease tag. - string PrereleaseTag { get; } - - - /********* - ** Accessors - *********/ - /// Whether this is a pre-release version. - bool IsPrerelease(); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - bool IsOlderThan(ISemanticVersion other); - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsOlderThan(string other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - bool IsNewerThan(ISemanticVersion other); - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - bool IsNewerThan(string other); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - bool IsBetween(ISemanticVersion min, ISemanticVersion max); - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - bool IsBetween(string min, string max); - - /// Get a string representation of the version. - string ToString(); - } -} diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs deleted file mode 100644 index a29ba6cf..00000000 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,4 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyTitle("SMAPI.Toolkit.CoreInterfaces")] -[assembly: AssemblyDescription("Provides toolkit interfaces which are available to SMAPI mods.")] diff --git a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj b/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj deleted file mode 100644 index 67adbd67..00000000 --- a/src/StardewModdingAPI.Toolkit.CoreInterfaces/StardewModdingAPI.Toolkit.CoreInterfaces.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - net4.5;netstandard2.0 - StardewModdingAPI - false - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\StardewModdingAPI.Toolkit.CoreInterfaces.xml - latest - - - - - - - - - diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs deleted file mode 100644 index 8a9c0a25..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Metadata about a mod. - public class ModEntryModel - { - /********* - ** Accessors - *********/ - /// The mod's unique ID (if known). - public string ID { get; set; } - - /// The main version. - public ModEntryVersionModel Main { get; set; } - - /// The latest optional version, if newer than . - public ModEntryVersionModel Optional { get; set; } - - /// The latest unofficial version, if newer than and . - public ModEntryVersionModel Unofficial { get; set; } - - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). - public ModEntryVersionModel UnofficialForBeta { get; set; } - - /// Optional extended data which isn't needed for update checks. - public ModExtendedMetadataModel Metadata { get; set; } - - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - public bool HasBetaInfo { get; set; } - - /// The errors that occurred while fetching update data. - public string[] Errors { get; set; } = new string[0]; - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs deleted file mode 100644 index dadb8c10..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Metadata about a version. - public class ModEntryVersionModel - { - /********* - ** Accessors - *********/ - /// The version number. - public ISemanticVersion Version { get; set; } - - /// The mod page URL. - public string Url { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ModEntryVersionModel() { } - - /// Construct an instance. - /// The version number. - /// The mod page URL. - public ModEntryVersionModel(ISemanticVersion version, string url) - { - this.Version = version; - this.Url = url; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs deleted file mode 100644 index 989c18b0..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; -using StardewModdingAPI.Toolkit.Framework.ModData; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Extended metadata about a mod. - public class ModExtendedMetadataModel - { - /********* - ** Accessors - *********/ - /**** - ** Mod info - ****/ - /// The mod's unique ID. A mod may have multiple current IDs in rare cases (e.g. due to parallel releases or unofficial updates). - public string[] ID { get; set; } = new string[0]; - - /// The mod's display name. - public string Name { get; set; } - - /// The mod ID on Nexus. - public int? NexusID { get; set; } - - /// The mod ID in the Chucklefish mod repo. - public int? ChucklefishID { get; set; } - - /// The mod ID in the ModDrop mod repo. - public int? ModDropID { get; set; } - - /// The GitHub repository in the form 'owner/repo'. - public string GitHubRepo { get; set; } - - /// The URL to a non-GitHub source repo. - public string CustomSourceUrl { get; set; } - - /// The custom mod page URL (if applicable). - public string CustomUrl { get; set; } - - - /**** - ** Stable compatibility - ****/ - /// The compatibility status. - [JsonConverter(typeof(StringEnumConverter))] - public WikiCompatibilityStatus? CompatibilityStatus { get; set; } - - /// The human-readable summary of the compatibility status or workaround, without HTML formatitng. - public string CompatibilitySummary { get; set; } - - /// The game or SMAPI version which broke this mod, if applicable. - public string BrokeIn { get; set; } - - - /**** - ** Beta compatibility - ****/ - /// The compatibility status for the Stardew Valley beta (if any). - [JsonConverter(typeof(StringEnumConverter))] - public WikiCompatibilityStatus? BetaCompatibilityStatus { get; set; } - - /// The human-readable summary of the compatibility status or workaround for the Stardew Valley beta (if any), without HTML formatitng. - public string BetaCompatibilitySummary { get; set; } - - /// The beta game or SMAPI version which broke this mod, if applicable. - public string BetaBrokeIn { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ModExtendedMetadataModel() { } - - /// Construct an instance. - /// The mod metadata from the wiki (if available). - /// The mod metadata from SMAPI's internal DB (if available). - public ModExtendedMetadataModel(WikiModEntry wiki, ModDataRecord db) - { - // wiki data - if (wiki != null) - { - this.ID = wiki.ID; - this.Name = wiki.Name.FirstOrDefault(); - this.NexusID = wiki.NexusID; - this.ChucklefishID = wiki.ChucklefishID; - this.ModDropID = wiki.ModDropID; - this.GitHubRepo = wiki.GitHubRepo; - this.CustomSourceUrl = wiki.CustomSourceUrl; - this.CustomUrl = wiki.CustomUrl; - - this.CompatibilityStatus = wiki.Compatibility.Status; - this.CompatibilitySummary = wiki.Compatibility.Summary; - this.BrokeIn = wiki.Compatibility.BrokeIn; - - this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status; - this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary; - this.BetaBrokeIn = wiki.BetaCompatibility?.BrokeIn; - } - - // internal DB data - if (db != null) - { - this.ID = this.ID.Union(db.FormerIDs).ToArray(); - this.Name = this.Name ?? db.DisplayName; - } - } - - /// Get update keys based on the metadata. - public IEnumerable GetUpdateKeys() - { - if (this.NexusID.HasValue) - yield return $"Nexus:{this.NexusID}"; - if (this.ChucklefishID.HasValue) - yield return $"Chucklefish:{this.ChucklefishID}"; - if (this.GitHubRepo != null) - yield return $"GitHub:{this.GitHubRepo}"; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs deleted file mode 100644 index e352e1cc..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSeachModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Specifies mods whose update-check info to fetch. - public class ModSearchModel - { - /********* - ** Accessors - *********/ - /// The mods for which to find data. - public ModSearchEntryModel[] Mods { get; set; } - - /// Whether to include extended metadata for each mod. - public bool IncludeExtendedMetadata { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The mods to search. - /// Whether to include extended metadata for each mod. - public ModSearchModel(ModSearchEntryModel[] mods, bool includeExtendedMetadata) - { - this.Mods = mods.ToArray(); - this.IncludeExtendedMetadata = includeExtendedMetadata; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs deleted file mode 100644 index bca47647..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/ModSearchEntryModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Specifies the identifiers for a mod to match. - public class ModSearchEntryModel - { - /********* - ** Accessors - *********/ - /// The unique mod ID. - public string ID { get; set; } - - /// The namespaced mod update keys (if available). - public string[] UpdateKeys { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModSearchEntryModel() - { - // needed for JSON deserialising - } - - /// Construct an instance. - /// The unique mod ID. - /// The namespaced mod update keys (if available). - public ModSearchEntryModel(string id, string[] updateKeys) - { - this.ID = id; - this.UpdateKeys = updateKeys ?? new string[0]; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs deleted file mode 100644 index 7c3df384..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi -{ - /// Provides methods for interacting with the SMAPI web API. - public class WebApiClient - { - /********* - ** Fields - *********/ - /// The base URL for the web API. - private readonly Uri BaseUrl; - - /// The API version number. - private readonly ISemanticVersion Version; - - /// The JSON serializer settings to use. - private readonly JsonSerializerSettings JsonSettings = new JsonHelper().JsonSettings; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The base URL for the web API. - /// The web API version. - public WebApiClient(string baseUrl, ISemanticVersion version) - { - this.BaseUrl = new Uri(baseUrl); - this.Version = version; - } - - /// Get metadata about a set of mods from the web API. - /// The mod keys for which to fetch the latest version. - /// Whether to include extended metadata for each mod. - public IDictionary GetModInfo(ModSearchEntryModel[] mods, bool includeExtendedMetadata = false) - { - return this.Post( - $"v{this.Version}/mods", - new ModSearchModel(mods, includeExtendedMetadata) - ).ToDictionary(p => p.ID); - } - - - /********* - ** Private methods - *********/ - /// Fetch the response from the backend API. - /// The body content type. - /// The expected response type. - /// The request URL, optionally excluding the base URL. - /// The body content to post. - private TResult Post(string url, TBody content) - { - // note: avoid HttpClient for Mac compatibility - using (WebClient client = new WebClient()) - { - Uri fullUrl = new Uri(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(response, this.JsonSettings); - } - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs deleted file mode 100644 index 3e9b8ea6..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using HtmlAgilityPack; -using Pathoschild.Http.Client; - -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// An HTTP client for fetching mod metadata from the wiki. - public class WikiClient : IDisposable - { - /********* - ** Fields - *********/ - /// The underlying HTTP client. - private readonly IClient Client; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The user agent for the wiki API. - /// The base URL for the wiki API. - public WikiClient(string userAgent, string baseUrl = "https://stardewvalleywiki.com/mediawiki/api.php") - { - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); - } - - /// Fetch mods from the compatibility list. - public async Task FetchModsAsync() - { - // fetch HTML - ResponseModel response = await this.Client - .GetAsync("") - .WithArguments(new - { - action = "parse", - page = "Modding:Mod_compatibility", - format = "json" - }) - .As(); - string html = response.Parse.Text["*"]; - - // parse HTML - var doc = new HtmlDocument(); - 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; - if (betaVersion == stableVersion) - betaVersion = null; - - // find mod entries - HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']"); - if (modNodes == null) - throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found."); - - // parse - WikiModEntry[] mods = this.ParseEntries(modNodes).ToArray(); - return new WikiModList - { - StableVersion = stableVersion, - BetaVersion = betaVersion, - Mods = mods - }; - } - - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() - { - this.Client?.Dispose(); - } - - - /********* - ** Private methods - *********/ - /// Parse valid mod compatibility entries. - /// The HTML compatibility entries. - private IEnumerable ParseEntries(IEnumerable nodes) - { - foreach (HtmlNode node in nodes) - { - // extract fields - string[] names = this.GetAttributeAsCsv(node, "data-name"); - string[] authors = this.GetAttributeAsCsv(node, "data-author"); - string[] ids = this.GetAttributeAsCsv(node, "data-id"); - string[] warnings = this.GetAttributeAsCsv(node, "data-warnings"); - int? nexusID = this.GetAttributeAsNullableInt(node, "data-nexus-id"); - int? chucklefishID = this.GetAttributeAsNullableInt(node, "data-cf-id"); - 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"); - - // parse stable compatibility - WikiCompatibilityInfo compatibility = new WikiCompatibilityInfo - { - Status = this.GetAttributeAsEnum(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; - { - WikiCompatibilityStatus? betaStatus = this.GetAttributeAsEnum(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") - }; - } - } - - // yield model - yield return new WikiModEntry - { - ID = ids, - Name = names, - Author = authors, - NexusID = nexusID, - ChucklefishID = chucklefishID, - ModDropID = modDropID, - GitHubRepo = githubRepo, - CustomSourceUrl = customSourceUrl, - CustomUrl = customUrl, - ContentPackFor = contentPackFor, - Compatibility = compatibility, - BetaCompatibility = betaCompatibility, - Warnings = warnings, - Anchor = anchor - }; - } - } - - /// Get an attribute value. - /// The element whose attributes to read. - /// The attribute name. - private string GetAttribute(HtmlNode element, string name) - { - string value = element.GetAttributeValue(name, null); - if (string.IsNullOrWhiteSpace(value)) - return null; - - return WebUtility.HtmlDecode(value); - } - - /// Get an attribute value and parse it as a comma-delimited list of strings. - /// The element whose attributes to read. - /// The attribute name. - private string[] GetAttributeAsCsv(HtmlNode element, string name) - { - string raw = this.GetAttribute(element, name); - return !string.IsNullOrWhiteSpace(raw) - ? raw.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(p => p.Trim()).ToArray() - : new string[0]; - } - - /// Get an attribute value and parse it as an enum value. - /// The enum type. - /// The element whose attributes to read. - /// The attribute name. - private TEnum? GetAttributeAsEnum(HtmlNode element, string name) where TEnum : struct - { - string raw = this.GetAttribute(element, name); - if (raw == null) - return null; - if (!Enum.TryParse(raw, true, out TEnum value) && Enum.IsDefined(typeof(TEnum), value)) - throw new InvalidOperationException($"Unknown {typeof(TEnum).Name} value '{raw}' when parsing compatibility list."); - return value; - } - - /// Get an attribute value and parse it as a semantic version. - /// The element whose attributes to read. - /// The attribute name. - private ISemanticVersion GetAttributeAsSemanticVersion(HtmlNode element, string name) - { - string raw = this.GetAttribute(element, name); - return SemanticVersion.TryParse(raw, out ISemanticVersion version) - ? version - : null; - } - - /// Get an attribute value and parse it as a nullable int. - /// The element whose attributes to read. - /// The attribute name. - private int? GetAttributeAsNullableInt(HtmlNode element, string name) - { - string raw = this.GetAttribute(element, name); - if (raw != null && int.TryParse(raw, out int value)) - return value; - return null; - } - - /// Get the text of an element with the given class name. - /// The metadata container. - /// The field name. - private string GetInnerHtml(HtmlNode container, string className) - { - return container.Descendants().FirstOrDefault(p => p.HasClass(className))?.InnerHtml; - } - - /// The response model for the MediaWiki parse API. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] - private class ResponseModel - { - /// The parse API results. - public ResponseParseModel Parse { get; set; } - } - - /// The inner response model for the MediaWiki parse API. - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")] - [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")] - private class ResponseParseModel - { - /// The parsed text. - public IDictionary Text { get; set; } - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs deleted file mode 100644 index 204acd2b..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// Compatibility info for a mod. - public class WikiCompatibilityInfo - { - /********* - ** Accessors - *********/ - /// The compatibility status. - public WikiCompatibilityStatus Status { get; set; } - - /// The human-readable summary of the compatibility status or workaround, without HTML formatting. - public string Summary { get; set; } - - /// The game or SMAPI version which broke this mod (if applicable). - public string BrokeIn { get; set; } - - /// The version of the latest unofficial update, if applicable. - public ISemanticVersion UnofficialVersion { get; set; } - - /// The URL to the latest unofficial update, if applicable. - public string UnofficialUrl { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs deleted file mode 100644 index a1d2dfae..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// The compatibility status for a mod. - public enum WikiCompatibilityStatus - { - /// The mod is compatible. - Ok = 0, - - /// The mod is compatible if you use an optional official download. - Optional = 1, - - /// The mod is compatible if you use an unofficial update. - Unofficial = 2, - - /// The mod isn't compatible, but the player can fix it or there's a good alternative. - Workaround = 3, - - /// The mod isn't compatible. - Broken = 4, - - /// The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely. - Abandoned = 5, - - /// The mod is no longer needed and should be removed. - Obsolete = 6 - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs deleted file mode 100644 index cf416cc6..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// A mod entry in the wiki list. - public class WikiModEntry - { - /********* - ** Accessors - *********/ - /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to newest order. - public string[] ID { get; set; } - - /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. - public string[] Name { get; set; } - - /// The mod's author name. If the author has multiple names, the first one is the most canonical name. - public string[] Author { get; set; } - - /// The mod ID on Nexus. - public int? NexusID { get; set; } - - /// The mod ID in the Chucklefish mod repo. - public int? ChucklefishID { get; set; } - - /// The mod ID in the ModDrop mod repo. - public int? ModDropID { get; set; } - - /// The GitHub repository in the form 'owner/repo'. - public string GitHubRepo { get; set; } - - /// The URL to a non-GitHub source repo. - public string CustomSourceUrl { get; set; } - - /// The custom mod page URL (if applicable). - public string CustomUrl { get; set; } - - /// The name of the mod which loads this content pack, if applicable. - public string ContentPackFor { get; set; } - - /// The mod's compatibility with the latest stable version of the game. - public WikiCompatibilityInfo Compatibility { get; set; } - - /// The mod's compatibility with the latest beta version of the game (if any). - public WikiCompatibilityInfo BetaCompatibility { get; set; } - - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - public bool HasBetaInfo => this.BetaCompatibility != null; - - /// The human-readable warnings for players about this mod. - public string[] Warnings { get; set; } - - /// The link anchor for the mod entry in the wiki compatibility list. - public string Anchor { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs b/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs deleted file mode 100644 index 0d614f28..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/Clients/Wiki/WikiModList.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki -{ - /// Metadata from the wiki's mod compatibility list. - public class WikiModList - { - /********* - ** Accessors - *********/ - /// The stable game version. - public string StableVersion { get; set; } - - /// The beta game version (if any). - public string BetaVersion { get; set; } - - /// The mods on the wiki. - public WikiModEntry[] Mods { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs deleted file mode 100644 index ef6d4dd9..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/MetadataModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The SMAPI predefined metadata. - internal class MetadataModel - { - /******** - ** Accessors - ********/ - /// Extra metadata about mods. - public IDictionary ModData { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs deleted file mode 100644 index b3954693..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataField.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// A versioned mod metadata field. - public class ModDataField - { - /********* - ** Accessors - *********/ - /// The field key. - public ModDataFieldKey Key { get; } - - /// The field value. - public string Value { get; } - - /// Whether this field should only be applied if it's not already set. - public bool IsDefault { get; } - - /// The lowest version in the range, or null for all past versions. - public ISemanticVersion LowerVersion { get; } - - /// The highest version in the range, or null for all future versions. - public ISemanticVersion UpperVersion { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The field key. - /// The field value. - /// Whether this field should only be applied if it's not already set. - /// The lowest version in the range, or null for all past versions. - /// The highest version in the range, or null for all future versions. - public ModDataField(ModDataFieldKey key, string value, bool isDefault, ISemanticVersion lowerVersion, ISemanticVersion upperVersion) - { - this.Key = key; - this.Value = value; - this.IsDefault = isDefault; - this.LowerVersion = lowerVersion; - this.UpperVersion = upperVersion; - } - - /// Get whether this data field applies for the given manifest. - /// The mod manifest. - public bool IsMatch(IManifest manifest) - { - return - manifest?.Version != null // ignore invalid manifest - && (!this.IsDefault || !this.HasFieldValue(manifest, this.Key)) - && (this.LowerVersion == null || !manifest.Version.IsOlderThan(this.LowerVersion)) - && (this.UpperVersion == null || !manifest.Version.IsNewerThan(this.UpperVersion)); - } - - - /********* - ** Private methods - *********/ - /// Get whether a manifest field has a meaningful value for the purposes of enforcing . - /// The mod manifest. - /// The field key matching . - private bool HasFieldValue(IManifest manifest, ModDataFieldKey key) - { - switch (key) - { - // update key - case ModDataFieldKey.UpdateKey: - return manifest.UpdateKeys != null && manifest.UpdateKeys.Any(p => !string.IsNullOrWhiteSpace(p)); - - // non-manifest fields - case ModDataFieldKey.AlternativeUrl: - case ModDataFieldKey.StatusReasonPhrase: - case ModDataFieldKey.Status: - return false; - - default: - return false; - } - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs deleted file mode 100644 index 09dd0cc5..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataFieldKey.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The valid field keys. - public enum ModDataFieldKey - { - /// A manifest update key. - UpdateKey, - - /// An alternative URL the player can check for an updated version. - AlternativeUrl, - - /// The mod's predefined compatibility status. - Status, - - /// A reason phrase for the , or null to use the default reason. - StatusReasonPhrase - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs deleted file mode 100644 index 18039762..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataModel.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The raw mod metadata from SMAPI's internal mod list. - internal class ModDataModel - { - /********* - ** Accessors - *********/ - /// The mod's current unique ID. - public string ID { get; set; } - - /// The former mod IDs (if any). - /// - /// This uses a custom format which uniquely identifies a mod across multiple versions and - /// supports matching other fields if no ID was specified. This doesn't include the latest - /// ID, if any. If the mod's ID changed over time, multiple variants can be separated by the - /// | character. - /// - public string FormerIDs { get; set; } - - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; set; } = new Dictionary(); - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; set; } = new Dictionary(); - - /// The mod warnings to suppress, even if they'd normally be shown. - public ModWarning SuppressWarnings { get; set; } - - /// This field stores properties that aren't mapped to another field before they're parsed into . - [JsonExtensionData] - public IDictionary ExtensionData { get; set; } - - /// The versioned field data. - /// - /// This maps field names to values. This should be accessed via . - /// Format notes: - /// - Each key consists of a field name prefixed with any combination of version range - /// and Default, separated by pipes (whitespace trimmed). For example, Name - /// will always override the name, Default | Name will only override a blank - /// name, and ~1.1 | Default | Name will override blank names up to version 1.1. - /// - The version format is min~max (where either side can be blank for unbounded), or - /// a single version number. - /// - The field name itself corresponds to a value. - /// - public IDictionary Fields { get; set; } = new Dictionary(); - - - /********* - ** Public methods - *********/ - /// Get a parsed representation of the . - public IEnumerable GetFields() - { - foreach (KeyValuePair pair in this.Fields) - { - // init fields - string packedKey = pair.Key; - string value = pair.Value; - bool isDefault = false; - ISemanticVersion lowerVersion = null; - ISemanticVersion upperVersion = null; - - // parse - string[] parts = packedKey.Split('|').Select(p => p.Trim()).ToArray(); - ModDataFieldKey fieldKey = (ModDataFieldKey)Enum.Parse(typeof(ModDataFieldKey), parts.Last(), ignoreCase: true); - foreach (string part in parts.Take(parts.Length - 1)) - { - // 'default' - if (part.Equals("Default", StringComparison.InvariantCultureIgnoreCase)) - { - isDefault = true; - continue; - } - - // version range - if (part.Contains("~")) - { - string[] versionParts = part.Split(new[] { '~' }, 2); - lowerVersion = versionParts[0] != "" ? new SemanticVersion(versionParts[0]) : null; - upperVersion = versionParts[1] != "" ? new SemanticVersion(versionParts[1]) : null; - continue; - } - - // single version - lowerVersion = new SemanticVersion(part); - upperVersion = new SemanticVersion(part); - } - - yield return new ModDataField(fieldKey, value, isDefault, lowerVersion, upperVersion); - } - } - - /// Get the former mod IDs. - public IEnumerable GetFormerIDs() - { - if (this.FormerIDs != null) - { - foreach (string id in this.FormerIDs.Split('|')) - yield return id.Trim(); - } - } - - - /********* - ** Private methods - *********/ - /// The method invoked after JSON deserialisation. - /// The deserialisation context. - [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; - } - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs deleted file mode 100644 index 794ad2e4..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecord.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The parsed mod metadata from SMAPI's internal mod list. - public class ModDataRecord - { - /********* - ** Accessors - *********/ - /// The mod's default display name. - public string DisplayName { get; } - - /// The mod's current unique ID. - public string ID { get; } - - /// The former mod IDs (if any). - public string[] FormerIDs { get; } - - /// The mod warnings to suppress, even if they'd normally be shown. - public ModWarning SuppressWarnings { get; set; } - - /// Maps local versions to a semantic version for update checks. - public IDictionary MapLocalVersions { get; } - - /// Maps remote versions to a semantic version for update checks. - public IDictionary MapRemoteVersions { get; } - - /// The versioned field data. - public ModDataField[] Fields { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod's default display name. - /// The raw data model. - internal ModDataRecord(string displayName, ModDataModel model) - { - this.DisplayName = displayName; - this.ID = model.ID; - this.FormerIDs = model.GetFormerIDs().ToArray(); - this.SuppressWarnings = model.SuppressWarnings; - this.MapLocalVersions = new Dictionary(model.MapLocalVersions, StringComparer.InvariantCultureIgnoreCase); - this.MapRemoteVersions = new Dictionary(model.MapRemoteVersions, StringComparer.InvariantCultureIgnoreCase); - this.Fields = model.GetFields().ToArray(); - } - - /// Get whether the mod has (or previously had) the given ID. - /// The mod ID. - public bool HasID(string id) - { - // try main ID - if (this.ID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - - // try former IDs - foreach (string formerID in this.FormerIDs) - { - if (formerID.Equals(id, StringComparison.InvariantCultureIgnoreCase)) - return true; - } - - return false; - } - - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.MapLocalVersions != null && this.MapLocalVersions.TryGetValue(version.ToString(), out string newVersion) - ? new SemanticVersion(newVersion) - : version; - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - 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; - } - - /// Get the possible mod IDs. - public IEnumerable GetIDs() - { - return this.FormerIDs - .Concat(new[] { this.ID }) - .Where(p => !string.IsNullOrWhiteSpace(p)) - .Select(p => p.Trim()) - .Distinct(); - } - - /// Get the default update key for this mod, if any. - public string GetDefaultUpdateKey() - { - string updateKey = this.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; - return !string.IsNullOrWhiteSpace(updateKey) - ? updateKey - : null; - } - - /// Get a parsed representation of the which match a given manifest. - /// The manifest to match. - public ModDataRecordVersionedFields GetVersionedFields(IManifest manifest) - { - ModDataRecordVersionedFields parsed = new ModDataRecordVersionedFields { DisplayName = this.DisplayName, DataRecord = this }; - foreach (ModDataField field in this.Fields.Where(field => field.IsMatch(manifest))) - { - switch (field.Key) - { - // update key - case ModDataFieldKey.UpdateKey: - parsed.UpdateKey = field.Value; - break; - - // alternative URL - case ModDataFieldKey.AlternativeUrl: - parsed.AlternativeUrl = field.Value; - break; - - // status - case ModDataFieldKey.Status: - parsed.Status = (ModStatus)Enum.Parse(typeof(ModStatus), field.Value, ignoreCase: true); - parsed.StatusUpperVersion = field.UpperVersion; - break; - - // status reason phrase - case ModDataFieldKey.StatusReasonPhrase: - parsed.StatusReasonPhrase = field.Value; - break; - } - } - - return parsed; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs deleted file mode 100644 index 237f2c66..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDataRecordVersionedFields.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// The versioned fields from a for a specific manifest. - public class ModDataRecordVersionedFields - { - /********* - ** Accessors - *********/ - /// The underlying data record. - public ModDataRecord DataRecord { get; set; } - - /// The default mod name to display when the name isn't available (e.g. during dependency checks). - public string DisplayName { get; set; } - - /// The update key to apply. - public string UpdateKey { get; set; } - - /// The alternative URL the player can check for an updated version. - public string AlternativeUrl { get; set; } - - /// The predefined compatibility status. - public ModStatus Status { get; set; } = ModStatus.None; - - /// A reason phrase for the , or null to use the default reason. - public string StatusReasonPhrase { get; set; } - - /// The upper version for which the applies (if any). - public ISemanticVersion StatusUpperVersion { get; set; } - - - /********* - ** Public methods - *********/ - /// Get a semantic local version for update checks. - /// The remote version to normalise. - public ISemanticVersion GetLocalVersionForUpdateChecks(ISemanticVersion version) - { - return this.DataRecord.GetLocalVersionForUpdateChecks(version); - } - - /// Get a semantic remote version for update checks. - /// The remote version to normalise. - 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/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs deleted file mode 100644 index a9da884a..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModDatabase.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// Handles access to SMAPI's internal mod metadata list. - public class ModDatabase - { - /********* - ** Fields - *********/ - /// The underlying mod data records indexed by default display name. - private readonly ModDataRecord[] Records; - - /// Get an update URL for an update key (if valid). - private readonly Func GetUpdateUrl; - - - /********* - ** Public methods - *********/ - /// Construct an empty instance. - public ModDatabase() - : this(new ModDataRecord[0], key => null) { } - - /// Construct an instance. - /// The underlying mod data records indexed by default display name. - /// Get an update URL for an update key (if valid). - public ModDatabase(IEnumerable records, Func getUpdateUrl) - { - this.Records = records.ToArray(); - this.GetUpdateUrl = getUpdateUrl; - } - - /// Get all mod data records. - public IEnumerable GetAll() - { - return this.Records; - } - - /// Get a mod data record. - /// The unique mod ID. - public ModDataRecord Get(string modID) - { - return !string.IsNullOrWhiteSpace(modID) - ? this.Records.FirstOrDefault(p => p.HasID(modID)) - : null; - } - - /// Get the mod page URL for a mod (if available). - /// The unique mod ID. - public string GetModPageUrlFor(string id) - { - // get update key - ModDataRecord record = this.Get(id); - ModDataField updateKeyField = record?.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey); - if (updateKeyField == null) - return null; - - // get update URL - return this.GetUpdateUrl(updateKeyField.Value); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs deleted file mode 100644 index 09da74bf..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModStatus.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// Indicates how SMAPI should treat a mod. - public enum ModStatus - { - /// Don't override the status. - None, - - /// The mod is obsolete and shouldn't be used, regardless of version. - Obsolete, - - /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. - AssumeBroken, - - /// Assume the mod is compatible, even if SMAPI detects incompatible code. - AssumeCompatible - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/StardewModdingAPI.Toolkit/Framework/ModData/ModWarning.cs deleted file mode 100644 index d61c427f..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModData/ModWarning.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit.Framework.ModData -{ - /// Indicates a detected non-error mod issue. - [Flags] - public enum ModWarning - { - /// No issues detected. - None = 0, - - /// SMAPI detected incompatible code in the mod, but was configured to load it anyway. - BrokenCodeLoaded = 1, - - /// The mod affects the save serializer in a way that may make saves unloadable without the mod. - ChangesSaveSerialiser = 2, - - /// The mod patches the game in a way that may impact stability. - PatchesGame = 4, - - /// The mod uses the dynamic keyword which won't work on Linux/Mac. - UsesDynamic = 8, - - /// The mod references specialised 'unvalided update tick' events which may impact stability. - UsesUnvalidatedUpdateTick = 16, - - /// The mod has no update keys set. - NoUpdateKeys = 32, - - /// Uses .NET APIs for filesystem access. - AccessesFilesystem = 64, - - /// Uses .NET APIs for shell or process access. - AccessesShell = 128 - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs deleted file mode 100644 index bb467b36..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; -using StardewModdingAPI.Toolkit.Utilities; - -namespace StardewModdingAPI.Toolkit.Framework.ModScanning -{ - /// The info about a mod read from its folder. - public class ModFolder - { - /********* - ** Accessors - *********/ - /// A suggested display name for the mod folder. - public string DisplayName { get; } - - /// The folder containing the mod's manifest.json. - public DirectoryInfo Directory { get; } - - /// The mod manifest. - public Manifest Manifest { get; } - - /// The error which occurred parsing the manifest, if any. - public string ManifestParseError { get; } - - /// Whether the mod should be loaded by default. This is false if it was found within a folder whose name starts with a dot. - public bool ShouldBeLoaded { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The root folder containing mods. - /// The folder containing the mod's manifest.json. - /// The mod manifest. - /// The error which occurred parsing the manifest, if any. - /// Whether the mod should be loaded by default. This should be false if it was found within a folder whose name starts with a dot. - public ModFolder(DirectoryInfo root, DirectoryInfo directory, Manifest manifest, string manifestParseError = null, bool shouldBeLoaded = true) - { - // save info - this.Directory = directory; - this.Manifest = manifest; - this.ManifestParseError = manifestParseError; - this.ShouldBeLoaded = shouldBeLoaded; - - // set display name - this.DisplayName = manifest?.Name; - if (string.IsNullOrWhiteSpace(this.DisplayName)) - this.DisplayName = PathUtilities.GetRelativePath(root.FullName, directory.FullName); - } - - /// Get the update keys for a mod. - /// The mod manifest. - public IEnumerable GetUpdateKeys(Manifest manifest) - { - return - (manifest.UpdateKeys ?? new string[0]) - .Where(p => !string.IsNullOrWhiteSpace(p)) - .ToArray(); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs deleted file mode 100644 index 0ab73d56..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/ModScanning/ModScanner.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using StardewModdingAPI.Toolkit.Serialisation; -using StardewModdingAPI.Toolkit.Serialisation.Models; - -namespace StardewModdingAPI.Toolkit.Framework.ModScanning -{ - /// Scans folders for mod data. - public class ModScanner - { - /********* - ** Fields - *********/ - /// The JSON helper with which to read manifests. - private readonly JsonHelper JsonHelper; - - /// A list of filesystem entry names to ignore when checking whether a folder should be treated as a mod. - private readonly HashSet IgnoreFilesystemEntries = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - ".DS_Store", - "mcs", - "Thumbs.db" - }; - - /// 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. - private readonly HashSet PotentialXnbModExtensions = new HashSet(StringComparer.InvariantCultureIgnoreCase) - { - ".md", - ".png", - ".txt", - ".xnb" - }; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The JSON helper with which to read manifests. - public ModScanner(JsonHelper jsonHelper) - { - this.JsonHelper = jsonHelper; - } - - /// Extract information about all mods in the given folder. - /// The root folder containing mods. - public IEnumerable GetModFolders(string rootPath) - { - DirectoryInfo root = new DirectoryInfo(rootPath); - return this.GetModFolders(root, root); - } - - /// Extract information from a mod folder. - /// The root folder containing mods. - /// The folder to search for a mod. - public ModFolder ReadFolder(DirectoryInfo root, DirectoryInfo searchFolder) - { - // find manifest.json - FileInfo manifestFile = this.FindManifest(searchFolder); - - // set appropriate invalid-mod error - if (manifestFile == null) - { - FileInfo[] files = searchFolder.GetFiles("*", SearchOption.AllDirectories).Where(this.IsRelevant).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."); - } - - // read mod info - Manifest manifest = null; - string manifestError = null; - { - try - { - if (!this.JsonHelper.ReadJsonFileIfExists(manifestFile.FullName, out manifest) || manifest == null) - manifestError = "its manifest is invalid."; - } - catch (SParseException ex) - { - manifestError = $"parsing its manifest failed: {ex.Message}"; - } - catch (Exception ex) - { - manifestError = $"parsing its manifest failed:\n{ex}"; - } - } - - // normalise display fields - if (manifest != null) - { - manifest.Name = this.StripNewlines(manifest.Name); - manifest.Description = this.StripNewlines(manifest.Description); - manifest.Author = this.StripNewlines(manifest.Author); - } - - return new ModFolder(root, manifestFile.Directory, manifest, manifestError); - } - - - /********* - ** Private methods - *********/ - /// Recursively extract information about all mods in the given folder. - /// The root mod folder. - /// The folder to search for mods. - public IEnumerable 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); - - // recurse into subfolders - else if (this.IsModSearchFolder(root, folder)) - { - foreach (DirectoryInfo subfolder in folder.EnumerateDirectories()) - { - foreach (ModFolder match in this.GetModFolders(root, subfolder)) - yield return match; - } - } - - // treat as mod folder - else - yield return this.ReadFolder(root, folder); - } - - /// Find the manifest for a mod folder. - /// The folder to search. - 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; - } - - // not found - return null; - } - } - - /// Get whether a given folder should be treated as a search folder (i.e. look for subfolders containing mods). - /// The root mod folder. - /// The folder to search for mods. - private bool IsModSearchFolder(DirectoryInfo root, DirectoryInfo folder) - { - if (root.FullName == folder.FullName) - return true; - - DirectoryInfo[] subfolders = folder.GetDirectories().Where(this.IsRelevant).ToArray(); - FileInfo[] files = folder.GetFiles().Where(this.IsRelevant).ToArray(); - return subfolders.Any() && !files.Any(); - } - - /// Get whether a file or folder is relevant when deciding how to process a mod folder. - /// The file or folder. - private bool IsRelevant(FileSystemInfo entry) - { - return !this.IgnoreFilesystemEntries.Contains(entry.Name); - } - - /// Strip newlines from a string. - /// The input to strip. - private string StripNewlines(string input) - { - return input?.Replace("\r", "").Replace("\n", ""); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs deleted file mode 100644 index f6c402d5..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/ModRepositoryKey.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Framework.UpdateData -{ - /// A mod repository which SMAPI can check for updates. - public enum ModRepositoryKey - { - /// An unknown or invalid mod repository. - Unknown, - - /// The Chucklefish mod repository. - Chucklefish, - - /// A GitHub project containing releases. - GitHub, - - /// The ModDrop mod repository. - ModDrop, - - /// The Nexus Mods mod repository. - Nexus - } -} diff --git a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs deleted file mode 100644 index 865ebcf7..00000000 --- a/src/StardewModdingAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit.Framework.UpdateData -{ - /// A namespaced mod ID which uniquely identifies a mod within a mod repository. - public class UpdateKey - { - /********* - ** Accessors - *********/ - /// The raw update key text. - public string RawText { get; } - - /// The mod repository containing the mod. - public ModRepositoryKey Repository { get; } - - /// The mod ID within the repository. - public string ID { get; } - - /// Whether the update key seems to be valid. - public bool LooksValid { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The raw update key text. - /// The mod repository containing the mod. - /// The mod ID within the repository. - public UpdateKey(string rawText, ModRepositoryKey repository, string id) - { - this.RawText = rawText; - this.Repository = repository; - this.ID = id; - this.LooksValid = - repository != ModRepositoryKey.Unknown - && !string.IsNullOrWhiteSpace(id); - } - - /// Parse a raw update key. - /// The raw update key to parse. - public static UpdateKey Parse(string raw) - { - // split parts - string[] parts = raw?.Split(':'); - if (parts == null || parts.Length != 2) - return new UpdateKey(raw, ModRepositoryKey.Unknown, null); - - // extract parts - string repositoryKey = parts[0].Trim(); - string id = parts[1].Trim(); - if (string.IsNullOrWhiteSpace(id)) - id = null; - - // parse - if (!Enum.TryParse(repositoryKey, true, out ModRepositoryKey repository)) - return new UpdateKey(raw, ModRepositoryKey.Unknown, id); - if (id == null) - return new UpdateKey(raw, repository, null); - - return new UpdateKey(raw, repository, id); - } - - /// Get a string that represents the current object. - public override string ToString() - { - return this.LooksValid - ? $"{this.Repository}:{this.ID}" - : this.RawText; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/ModToolkit.cs b/src/StardewModdingAPI.Toolkit/ModToolkit.cs deleted file mode 100644 index 1b53e59e..00000000 --- a/src/StardewModdingAPI.Toolkit/ModToolkit.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; -using StardewModdingAPI.Toolkit.Framework.ModData; -using StardewModdingAPI.Toolkit.Framework.ModScanning; -using StardewModdingAPI.Toolkit.Serialisation; - -namespace StardewModdingAPI.Toolkit -{ - /// A convenience wrapper for the various tools. - public class ModToolkit - { - /********* - ** Fields - *********/ - /// The default HTTP user agent for the toolkit. - private readonly string UserAgent; - - /// Maps vendor keys (like Nexus) to their mod URL template (where {0} is the mod ID). This doesn't affect update checks, which defer to the remote web API. - private readonly IDictionary VendorModUrls = new Dictionary(StringComparer.InvariantCultureIgnoreCase) - { - ["Chucklefish"] = "https://community.playstarbound.com/resources/{0}", - ["GitHub"] = "https://github.com/{0}/releases", - ["Nexus"] = "https://www.nexusmods.com/stardewvalley/mods/{0}" - }; - - - /********* - ** Accessors - *********/ - /// Encapsulates SMAPI's JSON parsing. - public JsonHelper JsonHelper { get; } = new JsonHelper(); - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public ModToolkit() - { - ISemanticVersion version = new SemanticVersion(this.GetType().Assembly.GetName().Version); - this.UserAgent = $"SMAPI Mod Handler Toolkit/{version}"; - } - - /// Extract mod metadata from the wiki compatibility list. - public async Task GetWikiCompatibilityListAsync() - { - var client = new WikiClient(this.UserAgent); - return await client.FetchModsAsync(); - } - - /// Get SMAPI's internal mod database. - /// The file path for the SMAPI metadata file. - public ModDatabase GetModDatabase(string metadataPath) - { - MetadataModel metadata = JsonConvert.DeserializeObject(File.ReadAllText(metadataPath)); - ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray(); - return new ModDatabase(records, this.GetUpdateUrl); - } - - /// Extract information about all mods in the given folder. - /// The root folder containing mods. - public IEnumerable GetModFolders(string rootPath) - { - return new ModScanner(this.JsonHelper).GetModFolders(rootPath); - } - - /// Get an update URL for an update key (if valid). - /// The update key. - public string GetUpdateUrl(string updateKey) - { - string[] parts = updateKey.Split(new[] { ':' }, 2); - if (parts.Length != 2) - return null; - - string vendorKey = parts[0].Trim(); - string modID = parts[1].Trim(); - - if (this.VendorModUrls.TryGetValue(vendorKey, out string urlTemplate)) - return string.Format(urlTemplate, modID); - - return null; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs b/src/StardewModdingAPI.Toolkit/Properties/AssemblyInfo.cs deleted file mode 100644 index 1bb19e8c..00000000 --- a/src/StardewModdingAPI.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/StardewModdingAPI.Toolkit/SemanticVersion.cs b/src/StardewModdingAPI.Toolkit/SemanticVersion.cs deleted file mode 100644 index ba9ca6c6..00000000 --- a/src/StardewModdingAPI.Toolkit/SemanticVersion.cs +++ /dev/null @@ -1,312 +0,0 @@ -using System; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI.Toolkit -{ - /// A semantic version with an optional release tag. - /// - /// The implementation is defined by Semantic Version 2.0 (https://semver.org/), with a few deviations: - /// - short-form "x.y" versions are supported (equivalent to "x.y.0"); - /// - hyphens are synonymous with dots in prerelease tags (like "-unofficial.3-pathoschild"); - /// - +build suffixes are not supported; - /// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial"). - /// - public class SemanticVersion : ISemanticVersion - { - /********* - ** Fields - *********/ - /// A regex pattern matching a valid prerelease tag. - internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; - - /// A regex pattern matching a version within a larger string. - internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?" + SemanticVersion.TagPattern + "))?"; - - /// A regular expression matching a semantic version string. - /// This pattern is derived from the BNF documentation in the semver repo, with deviations to support the Stardew Valley mod conventions (see remarks on ). - internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); - - - /********* - ** Accessors - *********/ - /// The major version incremented for major API changes. - public int MajorVersion { get; } - - /// The minor version incremented for backwards-compatible changes. - public int MinorVersion { get; } - - /// The patch version for backwards-compatible bug fixes. - public int PatchVersion { get; } - - /// An optional prerelease tag. - public string PrereleaseTag { get; } - -#if !SMAPI_3_0_STRICT - /// An optional prerelease tag. - [Obsolete("Use " + nameof(ISemanticVersion.PrereleaseTag) + " instead")] - public string Build => this.PrereleaseTag; - - /// Whether the version was parsed from the legacy object format. - public bool IsLegacyFormat { get; } -#endif - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The major version incremented for major API changes. - /// The minor version incremented for backwards-compatible changes. - /// The patch version for backwards-compatible fixes. - /// An optional prerelease tag. - /// Whether the version was parsed from the legacy object format. - public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null -#if !SMAPI_3_0_STRICT - , bool isLegacyFormat = false -#endif - ) - { - this.MajorVersion = major; - this.MinorVersion = minor; - this.PatchVersion = patch; - this.PrereleaseTag = this.GetNormalisedTag(prereleaseTag); -#if !SMAPI_3_0_STRICT - this.IsLegacyFormat = isLegacyFormat; -#endif - - this.AssertValid(); - } - - /// Construct an instance. - /// The assembly version. - /// The is null. - public SemanticVersion(Version version) - { - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version can't be null."); - - this.MajorVersion = version.Major; - this.MinorVersion = version.Minor; - this.PatchVersion = version.Build; - - this.AssertValid(); - } - - /// Construct an instance. - /// The semantic version string. - /// The is null. - /// The is not a valid semantic version. - public SemanticVersion(string version) - { - // parse - if (version == null) - throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersion.Regex.Match(version.Trim()); - if (!match.Success) - throw new FormatException($"The input '{version}' isn't a valid semantic version."); - - // initialise - 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.AssertValid(); - } - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The version to compare with this instance. - /// The value is null. - public int CompareTo(ISemanticVersion other) - { - if (other == null) - throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.PrereleaseTag); - } - - /// Indicates whether the current object is equal to another object of the same type. - /// true if the current object is equal to the parameter; otherwise, false. - /// An object to compare with this object. - public bool Equals(ISemanticVersion other) - { - return other != null && this.CompareTo(other) == 0; - } - - /// Whether this is a pre-release version. - public bool IsPrerelease() - { - return !string.IsNullOrWhiteSpace(this.PrereleaseTag); - } - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - public bool IsOlderThan(ISemanticVersion other) - { - return this.CompareTo(other) < 0; - } - - /// Get whether this version is older than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - public bool IsOlderThan(string other) - { - return this.IsOlderThan(new SemanticVersion(other)); - } - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - public bool IsNewerThan(ISemanticVersion other) - { - return this.CompareTo(other) > 0; - } - - /// Get whether this version is newer than the specified version. - /// The version to compare with this instance. - /// The specified version is not a valid semantic version. - public bool IsNewerThan(string other) - { - return this.IsNewerThan(new SemanticVersion(other)); - } - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - public bool IsBetween(ISemanticVersion min, ISemanticVersion max) - { - return this.CompareTo(min) >= 0 && this.CompareTo(max) <= 0; - } - - /// Get whether this version is between two specified versions (inclusively). - /// The minimum version. - /// The maximum version. - /// One of the specified versions is not a valid semantic version. - public bool IsBetween(string min, string max) - { - return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); - } - - /// Get a string representation of the version. - 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; - } - - /// Parse a version string without throwing an exception if it fails. - /// The version string. - /// The parsed representation. - /// Returns whether parsing the version succeeded. - public static bool TryParse(string version, out ISemanticVersion parsed) - { - try - { - parsed = new SemanticVersion(version); - return true; - } - catch - { - parsed = null; - return false; - } - } - - - /********* - ** Private methods - *********/ - /// Get a normalised build tag. - /// The tag to normalise. - private string GetNormalisedTag(string tag) - { - tag = tag?.Trim(); - return !string.IsNullOrWhiteSpace(tag) ? tag : null; - } - - /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. - /// The major version to compare with this instance. - /// The minor version to compare with this instance. - /// The patch version to compare with this instance. - /// The prerelease tag to compare with this instance. - private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) - { - const int same = 0; - const int curNewer = 1; - const int curOlder = -1; - - // compare stable versions - if (this.MajorVersion != otherMajor) - return this.MajorVersion.CompareTo(otherMajor); - if (this.MinorVersion != otherMinor) - return this.MinorVersion.CompareTo(otherMinor); - if (this.PatchVersion != otherPatch) - return this.PatchVersion.CompareTo(otherPatch); - if (this.PrereleaseTag == otherTag) - return same; - - // stable supercedes pre-release - bool curIsStable = string.IsNullOrWhiteSpace(this.PrereleaseTag); - bool otherIsStable = string.IsNullOrWhiteSpace(otherTag); - if (curIsStable) - return curNewer; - if (otherIsStable) - return curOlder; - - // compare two pre-release 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 - if (otherParts.Length <= i) - return curNewer; - - // compare if different - if (curParts[i] != otherParts[i]) - { - // unofficial is always lower-precedence - if (otherParts[i].Equals("unofficial", StringComparison.InvariantCultureIgnoreCase)) - return curNewer; - if (curParts[i].Equals("unofficial", StringComparison.InvariantCultureIgnoreCase)) - return curOlder; - - // compare numerically if possible - { - if (int.TryParse(curParts[i], out int curNum) && int.TryParse(otherParts[i], out int otherNum)) - return curNum.CompareTo(otherNum); - } - - // else compare lexically - return string.Compare(curParts[i], otherParts[i], StringComparison.OrdinalIgnoreCase); - } - } - - // fallback (this should never happen) - return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); - } - - /// Assert that the current version is valid. - private void AssertValid() - { - if (this.MajorVersion < 0 || this.MinorVersion < 0 || this.PatchVersion < 0) - throw new FormatException($"{this} isn't a valid semantic version. The major, minor, and patch numbers can't be negative."); - if (this.MajorVersion == 0 && this.MinorVersion == 0 && this.PatchVersion == 0) - throw new FormatException($"{this} isn't a valid semantic version. At least one of the major, minor, and patch numbers must be more than zero."); - if (this.PrereleaseTag != null) - { - if (this.PrereleaseTag.Trim() == "") - throw new FormatException($"{this} isn't a valid semantic version. The tag cannot be a blank string (but may be omitted)."); - if (!Regex.IsMatch(this.PrereleaseTag, $"^{SemanticVersion.TagPattern}$", RegexOptions.IgnoreCase)) - throw new FormatException($"{this} isn't a valid semantic version. The tag is invalid."); - } - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs deleted file mode 100644 index 232c22a7..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestContentPackForConverter.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Models; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// Handles deserialisation of arrays. - public class ManifestContentPackForConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ManifestContentPackFor[]); - } - - - /********* - ** Protected methods - *********/ - /// Read the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - return serializer.Deserialize(reader); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs deleted file mode 100644 index 0a304ee3..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/ManifestDependencyArrayConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialisation.Models; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// Handles deserialisation of arrays. - internal class ManifestDependencyArrayConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ManifestDependency[]); - } - - - /********* - ** Protected methods - *********/ - /// Read the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling serializer. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - List result = new List(); - foreach (JObject obj in JArray.Load(reader).Children()) - { - string uniqueID = obj.ValueIgnoreCase(nameof(ManifestDependency.UniqueID)); - string minVersion = obj.ValueIgnoreCase(nameof(ManifestDependency.MinimumVersion)); - bool required = obj.ValueIgnoreCase(nameof(ManifestDependency.IsRequired)) ?? true; - result.Add(new ManifestDependency(uniqueID, minVersion, required)); - } - return result.ToArray(); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs deleted file mode 100644 index aca06849..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SemanticVersionConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// Handles deserialisation of . - internal class SemanticVersionConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Get whether this converter can read JSON. - public override bool CanRead => true; - - /// Get whether this converter can write JSON. - public override bool CanWrite => true; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return typeof(ISemanticVersion).IsAssignableFrom(objectType); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling 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(), path); - default: - throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path})."); - } - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(value?.ToString()); - } - - - /********* - ** Private methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - private ISemanticVersion ReadObject(JObject obj) - { - int major = obj.ValueIgnoreCase(nameof(ISemanticVersion.MajorVersion)); - int minor = obj.ValueIgnoreCase(nameof(ISemanticVersion.MinorVersion)); - int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); - string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); -#if !SMAPI_3_0_STRICT - if (string.IsNullOrWhiteSpace(prereleaseTag)) - { - prereleaseTag = obj.ValueIgnoreCase("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 - ); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - private ISemanticVersion ReadString(string str, string path) - { - if (string.IsNullOrWhiteSpace(str)) - return null; - if (!SemanticVersion.TryParse(str, 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/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs deleted file mode 100644 index 5e0b0f4a..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Converters/SimpleReadOnlyConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Serialisation.Converters -{ - /// The base implementation for simplified converters which deserialise without overriding serialisation. - /// The type to deserialise. - internal abstract class SimpleReadOnlyConverter : JsonConverter - { - /********* - ** Accessors - *********/ - /// Whether this converter can write JSON. - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// Get whether this instance can convert the specified object type. - /// The object type. - public override bool CanConvert(Type objectType) - { - return objectType == typeof(T); - } - - /// Writes the JSON representation of the object. - /// The JSON writer. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } - - /// Reads the JSON representation of the object. - /// The JSON reader. - /// The object type. - /// The object being read. - /// The calling 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), path); - case JsonToken.String: - return this.ReadString(JToken.Load(reader).Value(), path); - default: - throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path})."); - } - } - - - /********* - ** Protected methods - *********/ - /// Read a JSON object. - /// The JSON object to read. - /// The path to the current JSON node. - protected virtual T ReadObject(JObject obj, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from object node (path: {path})."); - } - - /// Read a JSON string. - /// The JSON string value. - /// The path to the current JSON node. - protected virtual T ReadString(string str, string path) - { - throw new SParseException($"Can't parse {typeof(T).Name} from string node (path: {path})."); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs b/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs deleted file mode 100644 index 12b2c933..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/InternalExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Newtonsoft.Json.Linq; - -namespace StardewModdingAPI.Toolkit.Serialisation -{ - /// Provides extension methods for parsing JSON. - public static class JsonExtensions - { - /// Get a JSON field value from a case-insensitive field name. This will check for an exact match first, then search without case sensitivity. - /// The value type. - /// The JSON object to search. - /// The field name. - public static T ValueIgnoreCase(this JObject obj, string fieldName) - { - JToken token = obj.GetValue(fieldName, StringComparison.InvariantCultureIgnoreCase); - return token != null - ? token.Value() - : default(T); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs b/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs deleted file mode 100644 index cf2ce0d1..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/JsonHelper.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Toolkit.Serialisation -{ - /// Encapsulates SMAPI's JSON file parsing. - public class JsonHelper - { - /********* - ** Accessors - *********/ - /// The JSON settings to use when serialising and deserialising files. - public JsonSerializerSettings JsonSettings { get; } = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection values are duplicated each time the config is loaded - Converters = new List - { - new SemanticVersionConverter(), - new StringEnumConverter() - } - }; - - - /********* - ** Public methods - *********/ - /// Read a JSON file. - /// The model type. - /// The absolete file path. - /// The parsed content model. - /// Returns false if the file doesn't exist, else true. - /// The given is empty or invalid. - /// The file contains invalid JSON. - public bool ReadJsonFileIfExists(string fullPath, out TModel result) - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // read file - string json; - try - { - json = File.ReadAllText(fullPath); - } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) - { - result = default(TModel); - return false; - } - - // deserialise model - try - { - result = this.Deserialise(json); - return true; - } - catch (Exception ex) - { - string error = $"Can't parse JSON file at {fullPath}."; - - if (ex is JsonReaderException) - { - error += " This doesn't seem to be valid JSON."; - if (json.Contains("“") || json.Contains("”")) - error += " Found curly quotes in the text; note that only straight quotes are allowed in JSON."; - } - error += $"\nTechnical details: {ex.Message}"; - throw new JsonReaderException(error); - } - } - - /// Save to a JSON file. - /// The model type. - /// The absolete file path. - /// The model to save. - /// The given path is empty or invalid. - public void WriteJsonFile(string fullPath, TModel model) - where TModel : class - { - // validate - if (string.IsNullOrWhiteSpace(fullPath)) - throw new ArgumentException("The file path is empty or invalid.", nameof(fullPath)); - - // create directory if needed - string dir = Path.GetDirectoryName(fullPath); - if (dir == null) - throw new ArgumentException("The file path is invalid.", nameof(fullPath)); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - // write file - string json = this.Serialise(model); - File.WriteAllText(fullPath, json); - } - - /// Deserialize JSON text if possible. - /// The model type. - /// The raw JSON text. - public TModel Deserialise(string json) - { - try - { - return JsonConvert.DeserializeObject(json, this.JsonSettings); - } - catch (JsonReaderException) - { - // try replacing curly quotes - if (json.Contains("“") || json.Contains("”")) - { - try - { - return JsonConvert.DeserializeObject(json.Replace('“', '"').Replace('”', '"'), this.JsonSettings); - } - catch { /* rethrow original error */ } - } - - throw; - } - } - - /// Serialize a model to JSON text. - /// The model type. - /// The model to serialise. - /// The formatting to apply. - public string Serialise(TModel model, Formatting formatting = Formatting.Indented) - { - return JsonConvert.SerializeObject(model, formatting, this.JsonSettings); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs deleted file mode 100644 index 6cb9496b..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/Manifest.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using StardewModdingAPI.Toolkit.Serialisation.Converters; - -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// A manifest which describes a mod for SMAPI. - public class Manifest : IManifest - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// A brief description of the mod. - public string Description { get; set; } - - /// The mod author's name. - public string Author { get; set; } - - /// The mod version. - public ISemanticVersion Version { get; set; } - - /// The minimum SMAPI version required by this mod, if any. - public ISemanticVersion MinimumApiVersion { get; set; } - - /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . - public string EntryDll { get; set; } - - /// The mod which will read this as a content pack. Mutually exclusive with . - [JsonConverter(typeof(ManifestContentPackForConverter))] - public IManifestContentPackFor ContentPackFor { get; set; } - - /// The other mods that must be loaded before this mod. - [JsonConverter(typeof(ManifestDependencyArrayConverter))] - public IManifestDependency[] Dependencies { get; set; } - - /// The namespaced mod IDs to query for updates (like Nexus:541). - public string[] UpdateKeys { get; set; } - - /// The unique mod ID. - public string UniqueID { get; set; } - - /// Any manifest fields which didn't match a valid field. - [JsonExtensionData] - public IDictionary ExtraFields { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public Manifest() { } - - /// Construct an instance for a transitional content pack. - /// The unique mod ID. - /// The mod name. - /// The mod author's name. - /// A brief description of the mod. - /// The mod version. - /// The modID which will read this as a content pack. - public Manifest(string uniqueID, string name, string author, string description, ISemanticVersion version, string contentPackFor = null) - { - this.Name = name; - this.Author = author; - this.Description = description; - this.Version = version; - this.UniqueID = uniqueID; - this.UpdateKeys = new string[0]; - this.ContentPackFor = new ManifestContentPackFor { UniqueID = contentPackFor }; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs deleted file mode 100644 index d0e42216..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestContentPackFor.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// Indicates which mod can read the content pack represented by the containing manifest. - public class ManifestContentPackFor : IManifestContentPackFor - { - /********* - ** Accessors - *********/ - /// The unique ID of the mod which can read this content pack. - public string UniqueID { get; set; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs b/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs deleted file mode 100644 index 8db58d5d..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/Models/ManifestDependency.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Serialisation.Models -{ - /// A mod dependency listed in a mod manifest. - public class ManifestDependency : IManifestDependency - { - /********* - ** Accessors - *********/ - /// The unique mod ID to require. - public string UniqueID { get; set; } - - /// The minimum required version (if any). - public ISemanticVersion MinimumVersion { get; set; } - - /// Whether the dependency must be installed to use the mod. - public bool IsRequired { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The unique mod ID to require. - /// The minimum required version (if any). - /// Whether the dependency must be installed to use the mod. - public ManifestDependency(string uniqueID, string minimumVersion, bool required = true) - { - this.UniqueID = uniqueID; - this.MinimumVersion = !string.IsNullOrWhiteSpace(minimumVersion) - ? new SemanticVersion(minimumVersion) - : null; - this.IsRequired = required; - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs b/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs deleted file mode 100644 index 61a7b305..00000000 --- a/src/StardewModdingAPI.Toolkit/Serialisation/SParseException.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace StardewModdingAPI.Toolkit.Serialisation -{ - /// A format exception which provides a user-facing error message. - internal class SParseException : FormatException - { - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The error message. - /// The underlying exception, if any. - public SParseException(string message, Exception ex = null) - : base(message, ex) { } - } -} diff --git a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj b/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj deleted file mode 100644 index 351b36b6..00000000 --- a/src/StardewModdingAPI.Toolkit/StardewModdingAPI.Toolkit.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - net4.5;netstandard2.0 - false - ..\..\bin\$(Configuration)\SMAPI.Toolkit - ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\StardewModdingAPI.Toolkit.xml - latest - - - - - - - - - - - - - - - - - - - diff --git a/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs deleted file mode 100644 index 7856fdb1..00000000 --- a/src/StardewModdingAPI.Toolkit/Utilities/FileUtilities.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.IO; -using System.Threading; - -namespace StardewModdingAPI.Toolkit.Utilities -{ - /// Provides utilities for dealing with files. - public static class FileUtilities - { - /********* - ** Public methods - *********/ - /// Delete a file or folder regardless of file permissions, and block until deletion completes. - /// The file or folder to reset. - public static void ForceDelete(FileSystemInfo entry) - { - // ignore if already deleted - entry.Refresh(); - if (!entry.Exists) - return; - - // delete children - if (entry is DirectoryInfo folder) - { - foreach (FileSystemInfo child in folder.GetFileSystemInfos()) - FileUtilities.ForceDelete(child); - } - - // reset permissions & delete - entry.Attributes = FileAttributes.Normal; - entry.Delete(); - - // wait for deletion to finish - for (int i = 0; i < 10; i++) - { - entry.Refresh(); - if (entry.Exists) - Thread.Sleep(500); - } - - // throw exception if deletion didn't happen before timeout - entry.Refresh(); - if (entry.Exists) - throw new IOException($"Timed out trying to delete {entry.FullName}"); - } - } -} diff --git a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs b/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs deleted file mode 100644 index 8a3c2b03..00000000 --- a/src/StardewModdingAPI.Toolkit/Utilities/PathUtilities.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Diagnostics.Contracts; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; - -namespace StardewModdingAPI.Toolkit.Utilities -{ - /// Provides utilities for normalising file paths. - public static class PathUtilities - { - /********* - ** Fields - *********/ - /// The possible directory separator characters in a file path. - private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); - - /// The preferred directory separator chaeacter in an asset key. - private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString(); - - - /********* - ** Public methods - *********/ - /// Get the segments from a path (e.g. /usr/bin/boop => usr, bin, and boop). - /// The path to split. - /// The number of segments to match. Any additional segments will be merged into the last returned part. - public static string[] GetSegments(string path, int? limit = null) - { - return limit.HasValue - ? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries) - : path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); - } - - /// Normalise path separators in a file path. - /// The file path to normalise. - [Pure] - public static string NormalisePathSeparators(string path) - { - string[] parts = PathUtilities.GetSegments(path); - string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts); - if (path.StartsWith(PathUtilities.PreferredPathSeparator)) - normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash - return normalised; - } - - /// Get a directory or file path relative to a given source path. - /// The source folder path. - /// The target folder or file path. - [Pure] - public static string GetRelativePath(string sourceDir, string targetPath) - { - // convert to URIs - Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/"); - if (from.Scheme != to.Scheme) - 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())); - if (relative == "") - relative = "./"; - return relative; - } - - /// Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain ../). - /// The path to check. - public static bool IsSafeRelativePath(string path) - { - if (string.IsNullOrWhiteSpace(path)) - return true; - - return - !Path.IsPathRooted(path) - && PathUtilities.GetSegments(path).All(segment => segment.Trim() != ".."); - } - - /// Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc). - /// The string to check. - public static bool IsSlug(string str) - { - return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase); - } - } -} -- cgit