diff options
Diffstat (limited to 'src')
41 files changed, 733 insertions, 142 deletions
diff --git a/src/SMAPI.Installer/assets/install on Linux.sh b/src/SMAPI.Installer/assets/install on Linux.sh index 3b7eae9c..70b21521 100644 --- a/src/SMAPI.Installer/assets/install on Linux.sh +++ b/src/SMAPI.Installer/assets/install on Linux.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd "`dirname "$0"`" internal/linux/SMAPI.Installer diff --git a/src/SMAPI.Installer/assets/install on Windows.bat b/src/SMAPI.Installer/assets/install on Windows.bat index b0d9ae81..c61a801e 100644 --- a/src/SMAPI.Installer/assets/install on Windows.bat +++ b/src/SMAPI.Installer/assets/install on Windows.bat @@ -4,7 +4,9 @@ setlocal enabledelayedexpansion SET installerDir="%~dp0" REM make sure we're not running within a zip folder -echo %installerDir% | findstr /C:"%TEMP%" 1>nul +REM The error level is usually 0 (install dir contains temp path), 1 (it doesn't), or 9009 (findstr doesn't exist due to a Windows issue). +REM If the command doesn't exist, just skip this check. +echo %installerDir% | findstr /C:"%TEMP%" 1>nul 2>null if %ERRORLEVEL% EQU 0 ( echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first. echo. diff --git a/src/SMAPI.Installer/assets/install on macOS.command b/src/SMAPI.Installer/assets/install on macOS.command index abd21dc8..e85230ed 100644 --- a/src/SMAPI.Installer/assets/install on macOS.command +++ b/src/SMAPI.Installer/assets/install on macOS.command @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash cd "`dirname "$0"`" diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index 3be9c225..1719d39b 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -6,9 +6,9 @@ <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.10.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> <PackageReference Include="NUnit" Version="3.13.3" /> - <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj index cded6f65..badabfc7 100644 --- a/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj +++ b/src/SMAPI.ModBuildConfig/SMAPI.ModBuildConfig.csproj @@ -10,7 +10,7 @@ <!--NuGet package--> <PackageId>Pathoschild.Stardew.ModBuildConfig</PackageId> <Title>Build package for SMAPI mods</Title> - <Version>4.0.2</Version> + <Version>4.1.0</Version> <Authors>Pathoschild</Authors> <Description>Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.13.0 or later.</Description> <PackageLicenseExpression>MIT</PackageLicenseExpression> @@ -24,7 +24,7 @@ <ItemGroup> <PackageReference Include="Microsoft.Build.Utilities.Core" Version="16.10" /> - <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <!-- This is imported through Microsoft.Build.Utilities.Core. When installed by a mod, NuGet diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 0afb5837..2447c5c3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.18.1", + "Version": "3.18.3", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.18.1" + "MinimumApiVersion": "3.18.3" } diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json index fe802d88..306c92fc 100644 --- a/src/SMAPI.Mods.ErrorHandler/manifest.json +++ b/src/SMAPI.Mods.ErrorHandler/manifest.json @@ -1,9 +1,9 @@ { "Name": "Error Handler", "Author": "SMAPI", - "Version": "3.18.1", + "Version": "3.18.3", "Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.", "UniqueID": "SMAPI.ErrorHandler", "EntryDll": "ErrorHandler.dll", - "MinimumApiVersion": "3.18.1" + "MinimumApiVersion": "3.18.3" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 9a587a2b..c5075c57 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.18.1", + "Version": "3.18.3", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.18.1" + "MinimumApiVersion": "3.18.3" } diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 597cd7dd..0b1fb638 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -14,12 +14,12 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.7.0" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.2.0" /> - <PackageReference Include="Moq" Version="4.18.1" /> - <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="FluentAssertions" Version="6.8.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" /> + <PackageReference Include="Moq" Version="4.18.4" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> <PackageReference Include="NUnit" Version="3.13.3" /> - <PackageReference Include="NUnit3TestAdapter" Version="4.2.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.3.1" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs index 47cd3f7e..195b0367 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs @@ -19,6 +19,9 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData ModDrop, /// <summary>The Nexus Mods mod repository.</summary> - Nexus + Nexus, + + /// <summary>An arbitrary URL to a JSON file containing update data.</summary> + UpdateManifest } } diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs index 960caf96..3e8064fd 100644 --- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs +++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs @@ -58,31 +58,17 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData /// <param name="raw">The raw update key to parse.</param> public static UpdateKey Parse(string? raw) { + if (raw is null) + return new UpdateKey(raw, ModSiteKey.Unknown, null, null); // extract site + ID - string? rawSite; - string? id; - { - string[]? parts = raw?.Trim().Split(':'); - if (parts?.Length != 2) - return new UpdateKey(raw, ModSiteKey.Unknown, null, null); - - rawSite = parts[0].Trim(); - id = parts[1].Trim(); - } - if (string.IsNullOrWhiteSpace(id)) + (string rawSite, string? id) = UpdateKey.SplitTwoParts(raw, ':'); + if (string.IsNullOrEmpty(id)) id = null; // extract subkey string? subkey = null; if (id != null) - { - string[] parts = id.Split('@'); - if (parts.Length == 2) - { - id = parts[0].Trim(); - subkey = $"@{parts[1]}".Trim(); - } - } + (id, subkey) = UpdateKey.SplitTwoParts(id, '@', true); // parse if (!Enum.TryParse(rawSite, true, out ModSiteKey site)) @@ -151,5 +137,23 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData { return $"{site}:{id}{subkey}".Trim(); } + + + /********* + ** Private methods + *********/ + /// <summary>Split a string into two parts at a delimiter and trim whitespace.</summary> + /// <param name="str">The string to split.</param> + /// <param name="delimiter">The character on which to split.</param> + /// <param name="keepDelimiter">Whether to include the delimiter in the second string.</param> + /// <returns>Returns a tuple containing the two strings, with the second value <c>null</c> if the delimiter wasn't found.</returns> + private static (string, string?) SplitTwoParts(string str, char delimiter, bool keepDelimiter = false) + { + int splitIndex = str.IndexOf(delimiter); + + return splitIndex >= 0 + ? (str.Substring(0, splitIndex).Trim(), str.Substring(splitIndex + (keepDelimiter ? 0 : 1)).Trim()) + : (str.Trim(), null); + } } } diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 10f1df70..2a9a8294 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -9,9 +9,9 @@ <Import Project="..\..\build\common.targets" /> <ItemGroup> - <PackageReference Include="HtmlAgilityPack" Version="1.11.43" /> - <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> - <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.2.0" /> + <PackageReference Include="HtmlAgilityPack" Version="1.11.46" /> + <PackageReference Include="Newtonsoft.Json" Version="13.0.2" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.3.0" /> <PackageReference Include="System.Management" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" Condition="'$(OS)' == 'Windows_NT'" /> <PackageReference Include="VdfConverter" Version="1.0.3" Condition="'$(OS)' == 'Windows_NT'" Private="False" /> diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 71fb42c2..f687c7dd 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -22,6 +22,7 @@ using StardewModdingAPI.Web.Framework.Clients.CurseForge; using StardewModdingAPI.Web.Framework.Clients.GitHub; using StardewModdingAPI.Web.Framework.Clients.ModDrop; using StardewModdingAPI.Web.Framework.Clients.Nexus; +using StardewModdingAPI.Web.Framework.Clients.UpdateManifest; using StardewModdingAPI.Web.Framework.ConfigModels; namespace StardewModdingAPI.Web.Controllers @@ -63,14 +64,15 @@ namespace StardewModdingAPI.Web.Controllers /// <param name="github">The GitHub API client.</param> /// <param name="modDrop">The ModDrop API client.</param> /// <param name="nexus">The Nexus API client.</param> - public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus) + /// <param name="updateManifest">The API client for arbitrary update manifest URLs.</param> + public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest) { this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json")); this.WikiCache = wikiCache; this.ModCache = modCache; this.Config = config; - this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus }); + this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus, updateManifest }); } /// <summary>Fetch version metadata for the given mods.</summary> @@ -145,7 +147,12 @@ namespace StardewModdingAPI.Web.Controllers foreach (UpdateKey updateKey in updateKeys) { // validate update key - if (!updateKey.LooksValid) + if ( + !updateKey.LooksValid +#if SMAPI_DEPRECATED + || (updateKey.Site == ModSiteKey.UpdateManifest && apiVersion?.IsNewerThan("4.0.0-alpha") != true) // 4.0-alpha feature, don't make available to released mods in case it changes before release +#endif + ) { errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'."); continue; @@ -162,17 +169,21 @@ namespace StardewModdingAPI.Web.Controllers // if there's only a prerelease version (e.g. from GitHub), don't override the main version ISemanticVersion? curMain = data.Version; ISemanticVersion? curPreview = data.PreviewVersion; + string? curMainUrl = data.MainModPageUrl; + string? curPreviewUrl = data.PreviewModPageUrl; if (curPreview == null && curMain?.IsPrerelease() == true) { curPreview = curMain; + curPreviewUrl = curMainUrl; curMain = null; + curMainUrl = null; } // handle versions if (this.IsNewer(curMain, main?.Version)) - main = new ModEntryVersionModel(curMain, data.Url!); + main = new ModEntryVersionModel(curMain, curMainUrl ?? data.Url!); if (this.IsNewer(curPreview, optional?.Version)) - optional = new ModEntryVersionModel(curPreview, data.Url!); + optional = new ModEntryVersionModel(curPreview, curPreviewUrl ?? data.Url!); } // get unofficial version @@ -295,7 +306,7 @@ namespace StardewModdingAPI.Web.Controllers } // get version info - return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions); + return this.ModSites.GetPageVersions(page, updateKey, allowNonStandardVersions, mapRemoteVersions); } /// <summary>Get update keys based on the available mod metadata, while maintaining the precedence order.</summary> diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs index 548f17c3..6c9c08ef 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs @@ -1,3 +1,5 @@ +using System; + namespace StardewModdingAPI.Web.Framework.Clients { /// <summary>Generic metadata about a file download on a mod page.</summary> @@ -15,6 +17,9 @@ namespace StardewModdingAPI.Web.Framework.Clients /// <summary>The download's file version.</summary> public string? Version { get; } + /// <summary>The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</summary> + public string? ModPageUrl { get; } + /********* ** Public methods @@ -23,11 +28,22 @@ namespace StardewModdingAPI.Web.Framework.Clients /// <param name="name">The download's display name.</param> /// <param name="description">The download's description.</param> /// <param name="version">The download's file version.</param> - public GenericModDownload(string name, string? description, string? version) + /// <param name="modPageUrl">The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</param> + public GenericModDownload(string name, string? description, string? version, string? modPageUrl = null) { this.Name = name; this.Description = description; this.Version = version; + this.ModPageUrl = modPageUrl; + } + + /// <summary>Get whether the subkey matches this download.</summary> + /// <param name="subkey">The update subkey to check.</param> + public virtual bool MatchesSubkey(string subkey) + { + return + this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) + || this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true; } } } diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs index 5353c7e1..63ca5a95 100644 --- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs +++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs @@ -40,6 +40,9 @@ namespace StardewModdingAPI.Web.Framework.Clients [MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))] public bool IsValid => this.Status == RemoteModStatus.Ok; + /// <summary>Whether this mod page requires update subkeys and does not allow matching downloads without them.</summary> + public bool RequireSubkey { get; set; } = false; + /********* ** Public methods @@ -79,5 +82,19 @@ namespace StardewModdingAPI.Web.Framework.Clients return this; } + + /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary> + /// <param name="subkey">The update subkey.</param> + public virtual string? GetName(string? subkey) + { + return this.Name; + } + + /// <summary>Get the mod page URL for an update subkey, if different from the mod page it was fetched from.</summary> + /// <param name="subkey">The update subkey.</param> + public virtual string? GetUrl(string? subkey) + { + return this.Url; + } } } diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs new file mode 100644 index 00000000..bf1edd3f --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs @@ -0,0 +1,7 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest +{ + /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary> + internal interface IUpdateManifestClient : IModSiteClient, IDisposable { } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs new file mode 100644 index 00000000..ead5c229 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs @@ -0,0 +1,35 @@ +using System; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels +{ + /// <summary>The data model for a mod in an update manifest file.</summary> + internal class UpdateManifestModModel + { + /********* + ** Accessors + *********/ + /// <summary>The mod's name.</summary> + public string? Name { get; } + + /// <summary>The mod page URL from which to download updates.</summary> + public string? ModPageUrl { get; } + + /// <summary>The available versions for this mod.</summary> + public UpdateManifestVersionModel[] Versions { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="name">The mod's name.</param> + /// <param name="modPageUrl">The mod page URL from which to download updates.</param> + /// <param name="versions">The available versions for this mod.</param> + public UpdateManifestModModel(string? name, string? modPageUrl, UpdateManifestVersionModel[]? versions) + { + this.Name = name; + this.ModPageUrl = modPageUrl; + this.Versions = versions ?? Array.Empty<UpdateManifestVersionModel>(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs new file mode 100644 index 00000000..5ccd31b0 --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels +{ + /// <summary>The data model for an update manifest file.</summary> + internal class UpdateManifestModel + { + /********* + ** Accessors + *********/ + /// <summary>The manifest format version. This is equivalent to the SMAPI version, and is used to parse older manifests correctly if later versions of SMAPI change the expected format.</summary> + public string Format { get; } + + /// <summary>The mod info in this update manifest.</summary> + public IDictionary<string, UpdateManifestModModel> Mods { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="format">The manifest format version.</param> + /// <param name="mods">The mod info in this update manifest.</param> + public UpdateManifestModel(string format, IDictionary<string, UpdateManifestModModel>? mods) + { + this.Format = format; + this.Mods = mods ?? new Dictionary<string, UpdateManifestModModel>(); + } + } +} diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs new file mode 100644 index 00000000..6678f5eb --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs @@ -0,0 +1,28 @@ +namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels +{ + /// <summary>Data model for a Version in an update manifest.</summary> + internal class UpdateManifestVersionModel + { + /********* + ** Accessors + *********/ + /// <summary>The mod's semantic version.</summary> + public string? Version { get; } + + /// <summary>The mod page URL from which to download updates, if different from <see cref="UpdateManifestModModel.ModPag |
