summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2018-08-01 11:07:29 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2018-08-01 11:07:29 -0400
commit60b41195778af33fd609eab66d9ae3f1d1165e8f (patch)
tree7128b906d40e94c56c34ed6058f27bc31c31a08b /src/SMAPI.Web
parentb9bc1a6d17cafa0a97b46ffecda432cfc2f23b51 (diff)
parent52cf953f685c65b2b6814e375ec9a5ffa03c440a (diff)
downloadSMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.tar.gz
SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.tar.bz2
SMAPI-60b41195778af33fd609eab66d9ae3f1d1165e8f.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/SMAPI.Web')
-rw-r--r--src/SMAPI.Web/Controllers/IndexController.cs180
-rw-r--r--src/SMAPI.Web/Controllers/LogParserController.cs48
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs248
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs48
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs4
-rw-r--r--src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs50
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs11
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs3
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs (renamed from src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs)7
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs11
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs17
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs2
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs2
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs18
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs1
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs56
-rw-r--r--src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs4
-rw-r--r--src/SMAPI.Web/Framework/VersionConstraint.cs4
-rw-r--r--src/SMAPI.Web/StardewModdingAPI.Web.csproj26
-rw-r--r--src/SMAPI.Web/Startup.cs26
-rw-r--r--src/SMAPI.Web/ViewModels/LogParserModel.cs57
-rw-r--r--src/SMAPI.Web/Views/Index/Index.cshtml46
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml227
-rw-r--r--src/SMAPI.Web/Views/Shared/_Layout.cshtml7
-rw-r--r--src/SMAPI.Web/appsettings.Development.json7
-rw-r--r--src/SMAPI.Web/appsettings.json17
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/index.css59
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/log-parser.css121
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/main.css38
-rw-r--r--src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.pngbin0 -> 250 bytes
-rw-r--r--src/SMAPI.Web/wwwroot/Content/images/nexus-icon.pngbin0 -> 927 bytes
-rw-r--r--src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.pngbin0 -> 1099 bytes
-rw-r--r--src/SMAPI.Web/wwwroot/Content/images/pufferchick.pngbin0 -> 831 bytes
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/index.js34
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/log-parser.js136
-rw-r--r--src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json1676
36 files changed, 2658 insertions, 533 deletions
diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs
index 0464e50a..8c4a0332 100644
--- a/src/SMAPI.Web/Controllers/IndexController.cs
+++ b/src/SMAPI.Web/Controllers/IndexController.cs
@@ -1,10 +1,15 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using HtmlAgilityPack;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
-using StardewModdingAPI.Common;
+using Microsoft.Extensions.Options;
+using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
+using StardewModdingAPI.Web.Framework.ConfigModels;
using StardewModdingAPI.Web.ViewModels;
namespace StardewModdingAPI.Web.Controllers
@@ -17,6 +22,9 @@ namespace StardewModdingAPI.Web.Controllers
/*********
** Properties
*********/
+ /// <summary>The site config settings.</summary>
+ private readonly SiteConfig SiteConfig;
+
/// <summary>The cache in which to store release data.</summary>
private readonly IMemoryCache Cache;
@@ -24,7 +32,7 @@ namespace StardewModdingAPI.Web.Controllers
private readonly IGitHubClient GitHub;
/// <summary>The cache time for release info.</summary>
- private readonly TimeSpan CacheTime = TimeSpan.FromSeconds(1);
+ private readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10);
/// <summary>The GitHub repository name to check for update.</summary>
private readonly string RepositoryName = "Pathoschild/SMAPI";
@@ -36,34 +44,35 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Construct an instance.</summary>
/// <param name="cache">The cache in which to store release data.</param>
/// <param name="github">The GitHub API client.</param>
- public IndexController(IMemoryCache cache, IGitHubClient github)
+ /// <param name="siteConfig">The context config settings.</param>
+ public IndexController(IMemoryCache cache, IGitHubClient github, IOptions<SiteConfig> siteConfig)
{
this.Cache = cache;
this.GitHub = github;
+ this.SiteConfig = siteConfig.Value;
}
/// <summary>Display the index page.</summary>
[HttpGet]
public async Task<ViewResult> Index()
{
- // fetch SMAPI releases
- IndexVersionModel stableVersion = await this.Cache.GetOrCreateAsync("stable-version", async entry =>
- {
- entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
- GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);
- return new IndexVersionModel(release.Name, release.Body, this.GetMainDownloadUrl(release), this.GetDevDownloadUrl(release));
- });
- IndexVersionModel betaVersion = await this.Cache.GetOrCreateAsync("beta-version", async entry =>
- {
- entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
- GitRelease release = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true);
- return release.IsPrerelease
- ? this.GetBetaDownload(release)
- : null;
- });
+ // choose versions
+ ReleaseVersion[] versions = await this.GetReleaseVersionsAsync();
+ ReleaseVersion stableVersion = versions.LastOrDefault(version => !version.IsBeta && !version.IsForDevs);
+ ReleaseVersion stableVersionForDevs = versions.LastOrDefault(version => !version.IsBeta && version.IsForDevs);
+ ReleaseVersion betaVersion = versions.LastOrDefault(version => version.IsBeta && !version.IsForDevs);
+ ReleaseVersion betaVersionForDevs = versions.LastOrDefault(version => version.IsBeta && version.IsForDevs);
// render view
- var model = new IndexModel(stableVersion, betaVersion);
+ IndexVersionModel stableVersionModel = stableVersion != null
+ ? new IndexVersionModel(stableVersion.Version.ToString(), stableVersion.Release.Body, stableVersion.Asset.DownloadUrl, stableVersionForDevs?.Asset.DownloadUrl)
+ : new IndexVersionModel("unknown", "", "https://github.com/Pathoschild/SMAPI/releases", null); // just in case something goes wrong)
+ IndexVersionModel betaVersionModel = betaVersion != null && this.SiteConfig.EnableSmapiBeta
+ ? new IndexVersionModel(betaVersion.Version.ToString(), betaVersion.Release.Body, betaVersion.Asset.DownloadUrl, betaVersionForDevs?.Asset.DownloadUrl)
+ : null;
+
+ // render view
+ var model = new IndexModel(stableVersionModel, betaVersionModel);
return this.View(model);
}
@@ -71,62 +80,109 @@ namespace StardewModdingAPI.Web.Controllers
/*********
** Private methods
*********/
- /// <summary>Get the main download URL for a SMAPI release.</summary>
- /// <param name="release">The SMAPI release.</param>
- private string GetMainDownloadUrl(GitRelease release)
+ /// <summary>Get a sorted, parsed list of SMAPI downloads for the latest releases.</summary>
+ private async Task<ReleaseVersion[]> GetReleaseVersionsAsync()
{
- // get main download URL
- foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
+ return await this.Cache.GetOrCreateAsync("available-versions", async entry =>
{
- if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer.zip"))
- return asset.DownloadUrl;
- }
+ entry.AbsoluteExpiration = DateTimeOffset.UtcNow.Add(this.CacheTime);
- // fallback just in case
- return "https://github.com/pathoschild/SMAPI/releases";
- }
+ // get latest release (whether preview or stable)
+ GitRelease stableRelease = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: true);
- /// <summary>Get the for-developers download URL for a SMAPI release.</summary>
- /// <param name="release">The SMAPI release.</param>
- private string GetDevDownloadUrl(GitRelease release)
- {
- // get dev download URL
- foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
- {
- if (Regex.IsMatch(asset.FileName, @"SMAPI-[\d\.]+-installer-for-developers.zip"))
- return asset.DownloadUrl;
- }
+ // split stable/prerelease if applicable
+ GitRelease betaRelease = null;
+ if (stableRelease.IsPrerelease)
+ {
+ GitRelease result = await this.GitHub.GetLatestReleaseAsync(this.RepositoryName, includePrerelease: false);
+ if (result != null)
+ {
+ betaRelease = stableRelease;
+ stableRelease = result;
+ }
+ }
+
+ // strip 'noinclude' blocks from release descriptions
+ foreach (GitRelease release in new[] { stableRelease, betaRelease })
+ {
+ if (release == null)
+ continue;
+
+ HtmlDocument doc = new HtmlDocument();
+ doc.LoadHtml(release.Body);
+ foreach (HtmlNode node in doc.DocumentNode.SelectNodes("//*[@class='noinclude']")?.ToArray() ?? new HtmlNode[0])
+ node.Remove();
+ release.Body = doc.DocumentNode.InnerHtml.Trim();
+ }
- // fallback just in case
- return "https://github.com/pathoschild/SMAPI/releases";
+ // get versions
+ ReleaseVersion[] stableVersions = this.ParseReleaseVersions(stableRelease).ToArray();
+ ReleaseVersion[] betaVersions = this.ParseReleaseVersions(betaRelease).ToArray();
+ return stableVersions
+ .Concat(betaVersions)
+ .OrderBy(p => p.Version)
+ .ToArray();
+ });
}
- /// <summary>Get the latest beta download for a SMAPI release.</summary>
- /// <param name="release">The SMAPI release.</param>
- private IndexVersionModel GetBetaDownload(GitRelease release)
+ /// <summary>Get a parsed list of SMAPI downloads for a release.</summary>
+ /// <param name="release">The GitHub release.</param>
+ private IEnumerable<ReleaseVersion> ParseReleaseVersions(GitRelease release)
{
- // get download with the latest version
- SemanticVersionImpl latestVersion = null;
- string latestUrl = null;
- foreach (GitAsset asset in release.Assets ?? new GitAsset[0])
+ if (release?.Assets == null)
+ yield break;
+
+ foreach (GitAsset asset in release.Assets)
{
- // parse version
- Match versionMatch = Regex.Match(asset.FileName, @"SMAPI-([\d\.]+(?:-.+)?)-installer.zip");
- if (!versionMatch.Success || !SemanticVersionImpl.TryParse(versionMatch.Groups[1].Value, out SemanticVersionImpl version))
+ Match match = Regex.Match(asset.FileName, @"SMAPI-(?<version>[\d\.]+(?:-.+)?)-installer(?<forDevs>-for-developers)?.zip");
+ if (!match.Success || !SemanticVersion.TryParse(match.Groups["version"].Value, out ISemanticVersion version))
continue;
+ bool isBeta = version.IsPrerelease();
+ bool isForDevs = match.Groups["forDevs"].Success;
- // save latest version
- if (latestVersion == null || latestVersion.CompareTo(version) < 0)
- {
- latestVersion = version;
- latestUrl = asset.DownloadUrl;
- }
+ yield return new ReleaseVersion(release, asset, version, isBeta, isForDevs);
}
+ }
- // return if prerelease
- return latestVersion?.Tag != null
- ? new IndexVersionModel(latestVersion.ToString(), release.Body, latestUrl, null)
- : null;
+ /// <summary>A parsed release download.</summary>
+ private class ReleaseVersion
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The underlying GitHub release.</summary>
+ public GitRelease Release { get; }
+
+ /// <summary>The underlying download asset.</summary>
+ public GitAsset Asset { get; }
+
+ /// <summary>The SMAPI version.</summary>
+ public ISemanticVersion Version { get; }
+
+ /// <summary>Whether this is a beta download.</summary>
+ public bool IsBeta { get; }
+
+ /// <summary>Whether this is a 'for developers' download.</summary>
+ public bool IsForDevs { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="release">The underlying GitHub release.</param>
+ /// <param name="asset">The underlying download asset.</param>
+ /// <param name="version">The SMAPI version.</param>
+ /// <param name="isBeta">Whether this is a beta download.</param>
+ /// <param name="isForDevs">Whether this is a 'for developers' download.</param>
+ public ReleaseVersion(GitRelease release, GitAsset asset, ISemanticVersion version, bool isBeta, bool isForDevs)
+ {
+ this.Release = release;
+ this.Asset = asset;
+ this.Version = version;
+ this.IsBeta = isBeta;
+ this.IsForDevs = isForDevs;
+ }
}
}
}
diff --git a/src/SMAPI.Web/Controllers/LogParserController.cs b/src/SMAPI.Web/Controllers/LogParserController.cs
index 62547deb..17f8d3aa 100644
--- a/src/SMAPI.Web/Controllers/LogParserController.cs
+++ b/src/SMAPI.Web/Controllers/LogParserController.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.IO.Compression;
+using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
@@ -20,8 +21,8 @@ namespace StardewModdingAPI.Web.Controllers
/*********
** Properties
*********/
- /// <summary>The log parser config settings.</summary>
- private readonly ContextConfig Config;
+ /// <summary>The site config settings.</summary>
+ private readonly SiteConfig Config;
/// <summary>The underlying Pastebin client.</summary>
private readonly IPastebinClient Pastebin;
@@ -38,11 +39,11 @@ namespace StardewModdingAPI.Web.Controllers
** Constructor
***/
/// <summary>Construct an instance.</summary>
- /// <param name="contextProvider">The context config settings.</param>
+ /// <param name="siteConfig">The context config settings.</param>
/// <param name="pastebin">The Pastebin API client.</param>
- public LogParserController(IOptions<ContextConfig> contextProvider, IPastebinClient pastebin)
+ public LogParserController(IOptions<SiteConfig> siteConfig, IPastebinClient pastebin)
{
- this.Config = contextProvider.Value;
+ this.Config = siteConfig.Value;
this.Pastebin = pastebin;
}
@@ -51,34 +52,49 @@ namespace StardewModdingAPI.Web.Controllers
***/
/// <summary>Render the log parser UI.</summary>
/// <param name="id">The paste ID.</param>
+ /// <param name="raw">Whether to display the raw unparsed log.</param>
[HttpGet]
[Route("log")]
[Route("log/{id}")]
- public async Task<ViewResult> Index(string id = null)
+ public async Task<ViewResult> Index(string id = null, bool raw = false)
{
// fresh page
if (string.IsNullOrWhiteSpace(id))
- return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, null));
+ return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id));
// log page
PasteInfo paste = await this.GetAsync(id);
ParsedLog log = paste.Success
? new LogParser().Parse(paste.Content)
: new ParsedLog { IsValid = false, Error = "Pastebin error: " + paste.Error };
- return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log));
+ return this.View("Index", new LogParserModel(this.Config.LogParserUrl, id, log, raw));
}
/***
** JSON
***/
/// <summary>Save raw log data.</summary>
- /// <param name="content">The log content to save.</param>
- [HttpPost, Produces("application/json"), AllowLargePosts]
- [Route("log/save")]
- public async Task<SavePasteResult> PostAsync([FromBody] string content)
+ [HttpPost, AllowLargePosts]
+ [Route("log")]
+ public async Task<ActionResult> PostAsync()
{
- content = this.CompressString(content);
- return await this.Pastebin.PostAsync(content);
+ // get raw log text
+ string input = this.Request.Form["input"].FirstOrDefault();
+ if (string.IsNullOrWhiteSpace(input))
+ return this.View("Index", new LogParserModel(this.Config.LogParserUrl, null) { UploadError = "The log file seems to be empty." });
+
+ // upload log
+ input = this.CompressString(input);
+ SavePasteResult result = await this.Pastebin.PostAsync(input);
+
+ // handle errors
+ if (!result.Success)
+ return this.View("Index", new LogParserModel(this.Config.LogParserUrl, result.ID) { UploadError = $"Pastebin error: {result.Error ?? "unknown error"}" });
+
+ // redirect to view
+ UriBuilder uri = new UriBuilder(new Uri(this.Config.LogParserUrl));
+ uri.Path = uri.Path.TrimEnd('/') + '/' + result.ID;
+ return this.Redirect(uri.Uri.ToString());
}
@@ -115,7 +131,7 @@ namespace StardewModdingAPI.Web.Controllers
}
// prefix length
- var zipBuffer = new byte[compressedData.Length + 4];
+ byte[] zipBuffer = new byte[compressedData.Length + 4];
Buffer.BlockCopy(compressedData, 0, zipBuffer, 4, compressedData.Length);
Buffer.BlockCopy(BitConverter.GetBytes(buffer.Length), 0, zipBuffer, 0, 4);
@@ -151,7 +167,7 @@ namespace StardewModdingAPI.Web.Controllers
memoryStream.Write(zipBuffer, 4, zipBuffer.Length - 4);
// read data
- var buffer = new byte[dataLength];
+ byte[] buffer = new byte[dataLength];
memoryStream.Position = 0;
using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
gZipStream.Read(buffer, 0, buffer.Length);
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 24517263..b500e19d 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -1,12 +1,17 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
-using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
@@ -38,19 +43,28 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>A regex which matches SMAPI-style semantic version.</summary>
private readonly string VersionRegex;
+ /// <summary>The internal mod metadata list.</summary>
+ private readonly ModDatabase ModDatabase;
+
+ /// <summary>The web URL for the wiki compatibility list.</summary>
+ private readonly string WikiCompatibilityPageUrl;
+
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ /// <param name="environment">The web hosting environment.</param>
/// <param name="cache">The cache in which to store mod metadata.</param>
/// <param name="configProvider">The config settings for mod update checks.</param>
/// <param name="chucklefish">The Chucklefish API client.</param>
/// <param name="github">The GitHub API client.</param>
/// <param name="nexus">The Nexus API client.</param>
- public ModsApiController(IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
+ public ModsApiController(IHostingEnvironment environment, IMemoryCache cache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, IGitHubClient github, INexusClient nexus)
{
+ this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "StardewModdingAPI.metadata.json"));
ModUpdateCheckConfig config = configProvider.Value;
+ this.WikiCompatibilityPageUrl = config.WikiCompatibilityPageUrl;
this.Cache = cache;
this.SuccessCacheMinutes = config.SuccessCacheMinutes;
@@ -67,76 +81,126 @@ namespace StardewModdingAPI.Web.Controllers
}
/// <summary>Fetch version metadata for the given mods.</summary>
- /// <param name="modKeys">The namespaced mod keys to search as a comma-delimited array.</param>
- /// <param name="allowInvalidVersions">Whether to allow non-semantic versions, instead of returning an error for those.</param>
- [HttpGet]
- public async Task<IDictionary<string, ModInfoModel>> GetAsync(string modKeys, bool allowInvalidVersions = false)
+ /// <param name="model">The mod search criteria.</param>
+ [HttpPost]
+ public async Task<object> PostAsync([FromBody] ModSearchModel model)
{
- string[] modKeysArray = modKeys?.Split(',').ToArray();
- if (modKeysArray == null || !modKeysArray.Any())
- return new Dictionary<string, ModInfoModel>();
+ // parse request data
+ ISemanticVersion apiVersion = this.GetApiVersion();
+ ModSearchEntryModel[] searchMods = this.GetSearchMods(model, apiVersion).ToArray();
+
+ // fetch wiki data
+ WikiCompatibilityEntry[] wikiData = await this.GetWikiDataAsync();
+
+ // fetch data
+ IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
+ foreach (ModSearchEntryModel mod in searchMods)
+ {
+ if (string.IsNullOrWhiteSpace(mod.ID))
+ continue;
+
+ ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata);
+ result.SetBackwardsCompatibility(apiVersion);
+ mods[mod.ID] = result;
+ }
- return await this.PostAsync(new ModSearchModel(modKeysArray, allowInvalidVersions));
+ // return in expected structure
+ return apiVersion.IsNewerThan("2.6-beta.18")
+ ? mods.Values
+ : (object)mods;
}
- /// <summary>Fetch version metadata for the given mods.</summary>
- /// <param name="search">The mod search criteria.</param>
- [HttpPost]
- public async Task<IDictionary<string, ModInfoModel>> PostAsync([FromBody] ModSearchModel search)
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the metadata for a mod.</summary>
+ /// <param name="search">The mod data to match.</param>
+ /// <param name="wikiData">The wiki data.</param>
+ /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param>
+ /// <returns>Returns the mod data if found, else <c>null</c>.</returns>
+ private async Task<ModEntryModel> GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata)
{
- // parse model
- bool allowInvalidVersions = search?.AllowInvalidVersions ?? false;
- string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0])
- .Distinct(StringComparer.CurrentCultureIgnoreCase)
- .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase)
- .ToArray();
+ // resolve update keys
+ var updateKeys = new HashSet<string>(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase);
+ ModDataRecord record = this.ModDatabase.Get(search.ID);
+ if (record?.Fields != null)
+ {
+ string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value;
+ if (!string.IsNullOrWhiteSpace(defaultUpdateKey))
+ updateKeys.Add(defaultUpdateKey);
+ }
- // fetch mod info
- IDictionary<string, ModInfoModel> result = new Dictionary<string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase);
- foreach (string modKey in modKeys)
+ // get latest versions
+ ModEntryModel result = new ModEntryModel { ID = search.ID };
+ IList<string> errors = new List<string>();
+ foreach (string updateKey in updateKeys)
{
- // parse mod key
- if (!this.TryParseModKey(modKey, out string vendorKey, out string modID))
+ // fetch data
+ ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);
+ if (data.Error != null)
{
- result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
+ errors.Add(data.Error);
continue;
}
- // get matching repository
- if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
+ // handle main version
+ if (data.Version != null)
{
- result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
- continue;
+ if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version))
+ {
+ errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
+ continue;
+ }
+
+ if (this.IsNewer(version, result.Main?.Version))
+ result.Main = new ModEntryVersionModel(version, data.Url);
}
- // fetch mod info
- result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
+ // handle optional version
+ if (data.PreviewVersion != null)
{
- // fetch info
- ModInfoModel info = await repository.GetModInfoAsync(modID);
-
- // validate
- if (info.Error == null)
+ if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version))
{
- if (info.Version == null)
- info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number.");
- if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
- info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'.");
+ errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
+ continue;
}
- // cache & return
- entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes);
- return info;
- });
+ if (this.IsNewer(version, result.Optional?.Version))
+ result.Optional = new ModEntryVersionModel(version, data.Url);
+ }
}
+ // get unofficial version
+ WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
+ if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version))
+ result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl);
+
+ // fallback to preview if latest is invalid
+ if (result.Main == null && result.Optional != null)
+ {
+ result.Main = result.Optional;
+ result.Optional = null;
+ }
+
+ // special cases
+ if (result.ID == "Pathoschild.SMAPI")
+ {
+ if (result.Main != null)
+ result.Main.Url = "https://smapi.io/";
+ if (result.Optional != null)
+ result.Optional.Url = "https://smapi.io/";
+ }
+
+ // add extended metadata
+ if (includeExtendedMetadata && (wikiEntry != null || record != null))
+ result.Metadata = new ModExtendedMetadataModel(wikiEntry, record);
+
+ // add result
+ result.Errors = errors.ToArray();
return result;
}
-
- /*********
- ** Private methods
- *********/
/// <summary>Parse a namespaced mod ID.</summary>
/// <param name="raw">The raw mod ID to parse.</param>
/// <param name="vendorKey">The parsed vendor key.</param>
@@ -158,5 +222,91 @@ namespace StardewModdingAPI.Web.Controllers
modID = parts[1].Trim();
return true;
}
+
+ /// <summary>Get whether a <paramref name="current"/> version is newer than an <paramref name="other"/> version.</summary>
+ /// <param name="current">The current version.</param>
+ /// <param name="other">The other version.</param>
+ private bool IsNewer(ISemanticVersion current, ISemanticVersion other)
+ {
+ return current != null && (other == null || other.IsOlderThan(current));
+ }
+
+ /// <summary>Get the mods for which the API should return data.</summary>
+ /// <param name="model">The search model.</param>
+ /// <param name="apiVersion">The requested API version.</param>
+ private IEnumerable<ModSearchEntryModel> GetSearchMods(ModSearchModel model, ISemanticVersion apiVersion)
+ {
+ if (model == null)
+ yield break;
+
+ // yield standard entries
+ if (model.Mods != null)
+ {
+ foreach (ModSearchEntryModel mod in model.Mods)
+ yield return mod;
+ }
+
+ // yield mod update keys if backwards compatible
+ if (model.ModKeys != null && model.ModKeys.Any() && !apiVersion.IsNewerThan("2.6-beta.17"))
+ {
+ foreach (string updateKey in model.ModKeys.Distinct())
+ yield return new ModSearchEntryModel(updateKey, new[] { updateKey });
+ }
+ }
+
+ /// <summary>Get mod data from the wiki compatibility list.</summary>
+ private async Task<WikiCompatibilityEntry[]> GetWikiDataAsync()
+ {
+ ModToolkit toolkit = new ModToolkit();
+ return await this.Cache.GetOrCreateAsync($"_wiki", async entry =>
+ {
+ try
+ {
+ WikiCompatibilityEntry[] entries = await toolkit.GetWikiCompatibilityListAsync();
+ entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.SuccessCacheMinutes);
+ return entries;
+ }
+ catch
+ {
+ entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.ErrorCacheMinutes);
+ return new WikiCompatibilityEntry[0];
+ }
+ });
+ }
+
+ /// <summary>Get the mod info for an update key.</summary>
+ /// <param name="updateKey">The namespaced update key.</param>
+ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(string updateKey)
+ {
+ // parse update key
+ if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID))
+ return new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
+
+ // get matching repository
+ if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository))
+ return new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
+
+ // fetch mod info
+ return await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry =>
+ {
+ ModInfoModel result = await repository.GetModInfoAsync(modID);
+ if (result.Error != null)
+ {
+ if (result.Version == null)
+ result.Error = $"The update key '{updateKey}' matches a mod with no version number.";
+ else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
+ result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.";
+ }
+ entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes);
+ return result;
+ });
+ }
+
+ /// <summary>Get the requested API version.</summary>
+ private ISemanticVersion GetApiVersion()
+ {
+ string actualVersion = (string)this.RouteData.Values["version"];
+ return new SemanticVersion(actualVersion);
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
deleted file mode 100644
index adec41be..00000000
--- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Threading.Tasks;
-using Pathoschild.Http.Client;
-
-namespace StardewModdingAPI.Web.Framework.Clients.Nexus
-{
- /// <summary>An HTTP client for fetching mod metadata from the Nexus Mods API.</summary>
- internal class NexusClient : INexusClient
- {
- /*********
- ** Properties
- *********/
- /// <summary>The URL for a Nexus Mods API query excluding the base URL, where {0} is the mod ID.</summary>
- private readonly string ModUrlFormat;
-
- /// <summary>The underlying HTTP client.</summary>
- private readonly IClient Client;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
- /// <param name="baseUrl">The base URL for the Nexus Mods API.</param>
- /// <param name="modUrlFormat">The URL for a Nexus Mods API query excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
- public NexusClient(string userAgent, string baseUrl, string modUrlFormat)
- {
- this.ModUrlFormat = modUrlFormat;
- this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
- }
-
- /// <summary>Get metadata about a mod.</summary>
- /// <param name="id">The Nexus mod ID.</param>
- /// <returns>Returns the mod info if found, else <c>null</c>.</returns>
- public async Task<NexusMod> GetModAsync(uint id)
- {
- return await this.Client
- .GetAsync(string.Format(this.ModUrlFormat, id))
- .As<NexusMod>();
- }
-
- /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
- public void Dispose()
- {
- this.Client?.Dispose();
- }
- }
-}
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
index cd52c72b..4ecf2f76 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs
@@ -1,4 +1,5 @@
using Newtonsoft.Json;
+using StardewModdingAPI.Toolkit;
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
{
@@ -14,6 +15,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/// <summary>The mod's semantic version number.</summary>
public string Version { get; set; }
+ /// <summary>The latest file version.</summary>
+ public ISemanticVersion LatestFileVersion { get; set; }
+
/// <summary>The mod's web URL.</summary>
[JsonProperty("mod_page_uri")]
public string Url { get; set; }
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs
index d0597965..1b3fa195 100644
--- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusWebScrapeClient.cs
@@ -1,8 +1,11 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Net;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Pathoschild.Http.Client;
+using StardewModdingAPI.Toolkit;
namespace StardewModdingAPI.Web.Framework.Clients.Nexus
{
@@ -12,9 +15,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/*********
** Properties
*********/
- /// <summary>The URL for a Nexus web page excluding the base URL, where {0} is the mod ID.</summary>
+ /// <summary>The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID.</summary>
private readonly string ModUrlFormat;
+ /// <summary>The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</summary>
+ public string ModScrapeUrlFormat { get; set; }
+
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
@@ -25,10 +31,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
/// <summary>Construct an instance.</summary>
/// <param name="userAgent">The user agent for the Nexus Mods API client.</param>
/// <param name="baseUrl">The base URL for the Nexus Mods site.</param>
- /// <param name="modUrlFormat">The URL for a Nexus Mods web page excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
- public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat)
+ /// <param name="modUrlFormat">The URL for a Nexus Mods mod page for the user, excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param>
+ /// <param name="modScrapeUrlFormat">The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</param>
+ public NexusWebScrapeClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat)
{
this.ModUrlFormat = modUrlFormat;
+ this.ModScrapeUrlFormat = modScrapeUrlFormat;
this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent);
}
@@ -42,7 +50,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
try
{
html = await this.Client
- .GetAsync(string.Format(this.ModUrlFormat, id))
+ .GetAsync(string.Format(this.ModScrapeUrlFormat, id))
.AsString();
}
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
@@ -75,11 +83,43 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus
string url = this.GetModUrl(id);
string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim();
string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim();
+ SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion);
+
+ // extract file versions
+ List<string> rawVersions = new List<string>();
+ foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]"))
+ {
+ string sectionName = fileSection.Descendants("h2").First().InnerText;
+ if (sectionName != "Main files" && sectionName != "Optional files")
+ continue;
+
+ rawVersions.AddRange(
+ from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version"))
+ from versionStat in statBox.Descendants().Where(p => p.HasClass("stat"))
+ select versionStat.InnerText.Trim()
+ );
+ }
+
+ // choose latest file version
+ ISemanticVersion latestFileVersion = null;
+ foreach (string rawVersion in rawVersions)
+ {
+ if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur))
+ continue;
+ if (parsedVersion != null && !cur.IsNewerThan(parsedVersion))
+ continue;
+ if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion))
+ continue;
+
+ latestFileVersion = cur;
+ }
+ // yield info
return new NexusMod
{
Name = name,
- Version = version,
+ Version = parsedVersion?.ToString() ?? version,
+ LatestFileVersion = latestFileVersion,
Url = url
};
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
index de6c024a..ae8f18d2 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs
@@ -47,24 +47,21 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/****
** Nexus Mods
****/
- /// <summary>The user agent for the Nexus Mods API client.</summary>
- public string NexusUserAgent { get; set; }
-
/// <summary>The base URL for the Nexus Mods API.</summary>
public string NexusBaseUrl { get; set; }
- /// <summary>The URL for a Nexus Mods API query excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
+ /// <summary>The URL for a Nexus mod page for the user, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
public string NexusModUrlFormat { get; set; }
+ /// <summary>The URL for a Nexus mod page to scrape for versions, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary>
+ public string NexusModScrapeUrlFormat { get; set; }
+
/****
** Pastebin
****/
/// <summary>The base URL for the Pastebin API.</summary>
public string PastebinBaseUrl { get; set; }
- /// <summary>The user agent for the Pastebin API client, where {0} is the SMAPI version.</summary>
- public string PastebinUserAgent { get; set; }
-
/// <summary>The user key used to authenticate with the Pastebin API.</summary>
public string PastebinUserKey { get; set; }
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
index fc3b7dc2..ce4f3cb5 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
@@ -24,5 +24,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The repository key for Nexus Mods.</summary>
public string NexusKey { get; set; }
+
+ /// <summary>The web URL for the wiki compatibility list.</summary>
+ public string WikiCompatibilityPageUrl { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
index 117462f4..3d428015 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ContextConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/SiteConfig.cs
@@ -1,7 +1,7 @@
namespace StardewModdingAPI.Web.Framework.ConfigModels
{
- /// <summary>The config settings for the app context.</summary>
- public class ContextConfig // must be public to pass into views
+ /// <summary>The site config settings.</summary>
+ public class SiteConfig // must be public to pass into views
{
/*********
** Accessors
@@ -11,5 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The root URL for the log parser.</summary>
public string LogParserUrl { get; set; }
+
+ /// <summary>Whether to show SMAPI beta versions on the main page, if any.</summary>
+ public bool EnableSmapiBeta { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index f49fb05c..013c6c47 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
-using StardewModdingAPI.Common;
+using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Web.Framework.LogParsing.Models;
namespace StardewModdingAPI.Web.Framework.LogParsing
@@ -31,13 +31,13 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
/// <summary>A regex pattern matching an entry in SMAPI's mod list.</summary>
/// <remarks>The author name and description are optional.</remarks>
- private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>" + SemanticVersionImpl.UnboundedVersionPattern + @")(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ModListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?<author>[^\|]+))?(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching the start of SMAPI's content pack list.</summary>
private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching an entry in SMAPI's content pack list.</summary>
- private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| for (?<for>.+?) \| (?<description>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?<name>.+) (?<version>.+) by (?<author>.+) \| for (?<for>.+?)(?: \| (?<description>.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/*********
@@ -135,7 +135,10 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
{
Match match = this.ModPathPattern.Match(message.Text);
log.ModPath = match.Groups["path"].Value;
- log.GamePath = new FileInfo(log.ModPath).Directory.FullName;
+ int lastDelimiterPos = log.ModPath.LastIndexOfAny(new char[] { '/', '\\' });
+ log.GamePath = lastDelimiterPos >= 0
+ ? log.ModPath.Substring(0, lastDelimiterPos)
+ : log.ModPath;
}
// log UTC timestamp line
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs
index 40d21bf8..759f15db 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogLevel.cs
@@ -1,24 +1,29 @@
+using StardewModdingAPI.Internal.ConsoleWriting;
+
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
{
/// <summary>The log severity levels.</summary>
public enum LogLevel
{
/// <summary>Tracing info intended for developers.</summary>
- Trace,
+ Trace = ConsoleLogLevel.Trace,
/// <summary>Troubleshooting info that may be relevant to the player.</summary>
- Debug,
+ Debug = ConsoleLogLevel.Debug,
/// <summary>Info relevant to the player. This should be used judiciously.</summary>
- Info,
+ Info = ConsoleLogLevel.Info,
/// <summary>An issue the player should be aware of. This should be used rarely.</summary>
- Warn,
+ Warn = ConsoleLogLevel.Warn,
/// <summary>A message indicating something went wrong.</summary>
- Error,
+ Error = ConsoleLogLevel.Error,
/// <summary>Important information to highlight for the player when player action is needed (e.g. new version available). This should be used rarely to avoid alert fatigue.</summary>
- Alert
+ Alert = ConsoleLogLevel.Alert,
+
+ /// <summary>A critical issue that generally signals an immediate end to the application.</summary>
+ Critical = ConsoleLogLevel.Critical
}
}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs
index edb00454..4a4a40cd 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/BaseRepository.cs
@@ -1,6 +1,6 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
index 3e5a4272..e6074a60 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/ChucklefishRepository.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
namespace StardewModdingAPI.Web.Framework.ModRepositories
diff --git a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
index 59eb8cd1..1d7e4fff 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/GitHubRepository.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
namespace StardewModdingAPI.Web.Framework.ModRepositories
@@ -38,21 +38,25 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
// fetch info
try
{
- // get latest release
+ // get latest release (whether preview or stable)
GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true);
- GitRelease preview = null;
if (latest == null)
return new ModInfoModel("Found no mod with this ID.");
- // get latest stable release (if not latest)
+ // split stable/prerelease if applicable
+ GitRelease preview = null;
if (latest.IsPrerelease)
{
- preview = latest;
- latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
+ GitRelease result = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
+ if (result != null)
+ {
+ preview = latest;
+ latest = result;
+ }
}
// return data
- return new ModInfoModel(name: id, version: this.NormaliseVersion(latest?.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
+ return new ModInfoModel(name: id, version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
}
catch (Exception ex)
{
diff --git a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs
index 4496400c..09c59a86 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/IModRepository.cs
@@ -1,6 +1,5 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Common.Models;
namespace StardewModdingAPI.Web.Framework.ModRepositories
{
diff --git a/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs
new file mode 100644
index 00000000..18252298
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ModRepositories/ModInfoModel.cs
@@ -0,0 +1,56 @@
+namespace StardewModdingAPI.Web.Framework.ModRepositories
+{
+ /// <summary>Generic metadata about a mod.</summary>
+ internal class ModInfoModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod name.</summary>
+ public string Name { get; set; }
+
+ /// <summary>The mod's latest release number.</summary>
+ public string Version { get; set; }
+
+ /// <summary>The mod's latest optional release, if newer than <see cref="Version"/>.</summary>
+ public string PreviewVersion { get; set; }
+
+ /// <summary>The mod's web URL.</summary>
+ public string Url { get; set; }
+
+ /// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
+ public string Error { get; set; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an empty instance.</summary>
+ public ModInfoModel()
+ {
+ // needed for JSON deserialising
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="name">The mod name.</param>
+ /// <param name="version">The semantic version for the mod's latest release.</param>
+ /// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
+ /// <param name="url">The mod's web URL.</param>
+ /// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
+ public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null)
+ {
+ this.Name = name;
+ this.Version = version;
+ this.PreviewVersion = previewVersion;
+ this.Url = url;
+ this.Error = error;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="error">The error message indicating why the mod is invalid.</param>
+ public ModInfoModel(string error)
+ {
+ this.Error = error;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
index 6411ad4c..4afcda10 100644
--- a/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
+++ b/src/SMAPI.Web/Framework/ModRepositories/NexusRepository.cs
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
-using StardewModdingAPI.Common.Models;
+using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
namespace StardewModdingAPI.Web.Framework.ModRepositories
@@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
return new ModInfoModel("Found no mod with this ID.");
if (mod.Error != null)
return new ModInfoModel(mod.Error);
- return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url);
+ return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url);
}
catch (Exception ex)
{
diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs
index cffb1092..2d6ec603 100644
--- a/src/SMAPI.Web/Framework/VersionConstraint.cs
+++ b/src/SMAPI.Web/Framework/VersionConstraint.cs
@@ -1,5 +1,5 @@
using Microsoft.AspNetCore.Routing.Constraints;
-using StardewModdingAPI.Common;
+using StardewModdingAPI.Toolkit;
namespace StardewModdingAPI.Web.Framework
{
@@ -11,6 +11,6 @@ namespace StardewModdingAPI.Web.Framework
*********/
/// <summary>Construct an instance.</summary>
public VersionConstraint()
- : base(SemanticVersionImpl.Regex) { }
+ : base(SemanticVersion.Regex) { }
}
}
diff --git a/src/SMAPI.Web/StardewModdingAPI.Web.csproj b/src/SMAPI.Web/StardewModdingAPI.Web.csproj
index e2eee8a8..6761c7ad 100644
--- a/src/SMAPI.Web/StardewModdingAPI.Web.csproj
+++ b/src/SMAPI.Web/StardewModdingAPI.Web.csproj
@@ -10,18 +10,26 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="HtmlAgilityPack" Version="1.7.2" />
- <PackageReference Include="Markdig" Version="0.14.9" />
- <PackageReference Include="Microsoft.AspNetCore" Version="2.0.2" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.3" />
- <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.0.2" />
- <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.2" />
- <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.0.1" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.1.0" />
+ <PackageReference Include="HtmlAgilityPack" Version="1.8.4" />
+ <PackageReference Include="Markdig" Version="0.15.0" />
+ <PackageReference Include="Microsoft.AspNetCore" Version="2.1.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.1" />
+ <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.1.1" />
+ <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.2.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
</ItemGroup>
- <Import Project="..\SMAPI.Common\StardewModdingAPI.Common.projitems" Label="Shared" />
+ <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" />
+ <ItemGroup>
+ <ProjectReference Include="..\StardewModdingAPI.Toolkit\StardewModdingAPI.Toolkit.csproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <Content Update="wwwroot\StardewModdingAPI.metadata.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
+ </ItemGroup>
</Project>
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 6c7ccecd..bf3ec9a1 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -7,6 +7,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
+using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Web.Framework;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
@@ -49,13 +50,16 @@ namespace StardewModdingAPI.Web
// init configuration
services
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
- .Configure<ContextConfig>(this.Configuration.GetSection("Context"))
+ .Configure<SiteConfig>(this.Configuration.GetSection("Site"))
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
.AddMemoryCache()
.AddMvc()
.ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()))
.AddJsonOptions(options =>
{
+ foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
+ options.SerializerSettings.Converters.Add(converter);
+
options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
});
@@ -82,15 +86,11 @@ namespace StardewModdingAPI.Web
password: api.GitHubPassword
));
- //services.AddSingleton<INexusClient>(new NexusClient(
- // userAgent: api.NexusUserAgent,
- // baseUrl: api.NexusBaseUrl,
- // modUrlFormat: api.NexusModUrlFormat
- //));
services.AddSingleton<INexusClient>(new NexusWebScrapeClient(
userAgent: userAgent,
baseUrl: api.NexusBaseUrl,
- modUrlFormat: api.NexusModUrlFormat
+ modUrlFormat: api.NexusModUrlFormat,
+ modScrapeUrlFormat: api.NexusModScrapeUrlFormat
));
services.AddSingleton<IPastebinClient>(new PastebinClient(
@@ -153,23 +153,23 @@ namespace StardewModdingAPI.Web
));
// shortcut redirects
+ redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1"));
redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://stardewvalleywiki.com/Modding:SMAPI_compatibility"));
redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
- redirects.Add(new RedirectToUrlRule(@"^/buildmsg(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/mod-build-config.md#$1"));
+ redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
// redirect legacy canimod.com URLs
var wikiRedirects = new Dictionary<string, string[]>
{
- ["Modding:Creating_a_SMAPI_mod"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod" },
+ ["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
+ ["Modding:Modder_Guide"] = new[] { "^/for-devs/creating-a-smapi-mod", "^/guides/creating-a-smapi-mod", "^/for-devs/creating-a-smapi-mod-advanced-config" },
+ ["Modding:Player_Guide"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods", "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },
+
["Modding:Editing_XNB_files"] = new[] { "^/for-devs/creating-an-xnb-mod", "^/guides/creating-an-xnb-mod" },
["Modding:Event_data"] = new[] { "^/for-devs/events", "^/guides/events" },
["Modding:Gift_taste_data"] = new[] { "^/for-devs/npc-gift-tastes", "^/guides/npc-gift-tastes" },
["Modding:IDE_reference"] = new[] { "^/for-devs/creating-a-smapi-mod-ide-primer" },
- ["Modding:Installing_SMAPI"] = new[] { "^/for-players/install-smapi", "^/guides/using-mods" },
["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
- ["Modding:Player_FAQs"] = new[] { "^/for-players/faqs", "^/for-players/intro", "^/for-players/use-mods", "^/guides/asking-for-help", "^/guides/smapi-faq" },
- ["Modding:SMAPI_APIs"] = new[] { "^/for-devs/creating-a-smapi-mod-advanced-config" },
- ["Modding:Updating_deprecated_SMAPI_code"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
};
foreach (KeyValuePair<string, string[]> pair in wikiRedirects)
diff --git a/src/SMAPI.Web/ViewModels/LogParserModel.cs b/src/SMAPI.Web/ViewModels/LogParserModel.cs
index 8c026536..df36ca73 100644
--- a/src/SMAPI.Web/ViewModels/LogParserModel.cs
+++ b/src/SMAPI.Web/ViewModels/LogParserModel.cs
@@ -1,3 +1,6 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
using StardewModdingAPI.Web.Framework.LogParsing.Models;
namespace StardewModdingAPI.Web.ViewModels
@@ -6,6 +9,13 @@ namespace StardewModdingAPI.Web.ViewModels
public class LogParserModel
{
/*********
+ ** Properties
+ *********/
+ /// <summary>A regex pattern matching characters to remove from a mod name to create the slug ID.</summary>
+ private readonly Regex SlugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+
+ /*********
** Accessors
*********/
/// <summary>The root URL for the log parser controller.</summary>
@@ -17,6 +27,15 @@ namespace StardewModdingAPI.Web.ViewModels
/// <summary>The parsed log info.</summary>
public ParsedLog ParsedLog { get; set; }
+ /// <summary>Whether to show the raw unparsed log.</summary>
+ public bool ShowRaw { get; set; }
+
+ /// <summary>An error which occurred while uploading the log to Pastebin.</summary>
+ public string UploadError { get; set; }
+
+ /// <summary>An error which occurred while parsing the log file.</summary>
+ public string ParseError => this.ParsedLog?.Error;
+
/*********
** Public methods
@@ -27,12 +46,46 @@ namespace StardewModdingAPI.Web.ViewModels
/// <summary>Construct an instance.</summary>
/// <param name="sectionUrl">The root URL for the log parser controller.</param>
/// <param name="pasteID">The paste ID.</param>
- /// <param name="parsedLog">The parsed log info.</param>
- public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog)
+ public LogParserModel(string sectionUrl, string pasteID)
{
this.SectionUrl = sectionUrl;
this.PasteID = pasteID;
+ this.ParsedLog = null;
+ this.ShowRaw = false;
+ }
+
+ /// <summary>Construct an instance.</summary>
+ /// <param name="sectionUrl">The root URL for the log parser controller.</param>
+ /// <param name="pasteID">The paste ID.</param>
+ /// <param name="parsedLog">The parsed log info.</param>
+ /// <param name="showRaw">Whether to show the raw unparsed log.</param>
+ public LogParserModel(string sectionUrl, string pasteID, ParsedLog parsedLog, bool showRaw)
+ : this(sectionUrl, pasteID)
+ {
this.ParsedLog = parsedLog;
+ this.ShowRaw = showRaw;
+ }
+
+ /// <summary>Get all content packs in the log grouped by the mod they're for.</summary>
+ public IDictionary<string, LogModInfo[]> GetContentPacksByMod()
+ {
+ // get all mods & content packs
+ LogModInfo[] mods = this.ParsedLog?.Mods;
+ if (mods == null || !mods.Any())
+ return new Dictionary<string, LogModInfo[]>();
+
+ // group by mod
+ return mods
+ .Where(mod => mod.ContentPackFor != null)
+ .GroupBy(mod => mod.ContentPackFor)
+ .ToDictionary(group => group.Key, group => group.ToArray());
+ }
+
+ /// <summary>Get a sanitised mod name that's safe to use in anchors, attributes, and URLs.</summary>
+ /// <param name="modName">The mod name.</param>
+ public string GetSlug(string modName)
+ {
+ return this.SlugInvalidCharPattern.Replace(modName, "");
}
}
}
diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index 4efb9f8a..361d01de 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -3,7 +3,9 @@
}
@model StardewModdingAPI.Web.ViewModels.IndexModel
@section Head {
- <link rel="stylesheet" href="~/Content/css/index.css" />
+ <link rel="stylesheet" href="~/Content/css/index.css?r=20180615" />
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
+ <script src="~/Content/js/index.js?r=20180615"></script>
}
<p id="blurb">
@@ -13,17 +15,29 @@
</p>
<div id="call-to-action">
- <a href="@Model.StableVersion.DownloadUrl" class="main-cta">Download SMAPI @Model.StableVersion.Version</a><br />
+ <div class="cta-dropdown">
+ <a href="@Model.StableVersion.DownloadUrl" class="main-cta download">Download SMAPI @Model.StableVersion.Version</a><br/>
+ <div class="dropdown-content">
+ <a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
+ <a href="@Model.StableVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
+ </div>
+ </div><br />
+
@if (Model.BetaVersion != null)
{
- <a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta">Download SMAPI @Model.BetaVersion.Version<br /><small>for Stardew Valley 1.3 beta</small></a><br />
+ <div class="cta-dropdown secondary-cta-dropdown">
+ <a href="@Model.BetaVersion.DownloadUrl" class="secondary-cta download">Download SMAPI @Model.BetaVersion.Version<br/><small>for Stardew Valley 1.3 beta</small></a><br/>
+ <div class="dropdown-content">
+ <a href="https://www.nexusmods.com/stardewvalley/mods/2400"><img src="Content/images/nexus-icon.png" /> Download from Nexus</a>
+ <a href="@Model.BetaVersion.DownloadUrl"><img src="Content/images/direct-download-icon.png" /> Direct download</a>
+ </div>
+ </div><br />
}
- <a href="https://stardewvalleywiki.com/Modding:Installing_SMAPI" class="secondary-cta">Install guide</a><br />
- <a href="https://stardewvalleywiki.com/Modding:Player_FAQs" class="secondary-cta">FAQs</a><br />
- <img src="favicon.ico" />
+ <a href="https://stardewvalleywiki.com/Modding:Player_Guide" class="secondary-cta">Player guide</a><br />
+ <img id="pufferchick" src="Content/images/pufferchick.png" />
</div>
-<h2>Get help</h2>
+<h2 id="help">Get help</h2>
<ul>
<li><a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">Mod compatibility list</a></li>
<li>Get help <a href="https://stardewvalleywiki.com/Modding:Community#Discord">on Discord</a> or <a href="https://community.playstarbound.com/threads/smapi-stardew-modding-api.108375/">in the forums</a></li>
@@ -31,7 +45,7 @@
@if (Model.BetaVersion == null)
{
- <h2>What's new in SMAPI @Model.StableVersion.Version?</h2>
+ <h2 id="whatsnew">What's new in SMAPI @Model.StableVersion.Version?</h2>
<div class="github-description">
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
</div>
@@ -39,7 +53,7 @@
}
else
{
- <h2>What's new in...</h2>
+ <h2 id="whatsnew">What's new in...</h2>
<h3>SMAPI @Model.StableVersion.Version?</h3>
<div class="github-description">
@Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description))
@@ -53,7 +67,7 @@ else
<p>See the <a href="https://github.com/Pathoschild/SMAPI/blob/develop/docs/release-notes.md#release-notes">release notes</a> and <a href="https://stardewvalleywiki.com/Modding:SMAPI_compatibility">mod compatibility list</a> for more info.</p>
}
-<h2>Donate to support SMAPI ♥</h2>
+<h2 id="donate">Donate to support SMAPI ♥</h2>
<p>
SMAPI is an open-source project by Pathoschild. It will always be free, but donations
are much appreciated to help pay for development, server hosting, domain fees, coffee, etc.
@@ -75,15 +89,23 @@ else
Special thanks to
acerbicon,
<a href="https://www.nexusmods.com/stardewvalley/users/31393530">ChefRude</a>,
+ cheesysteak,
+ hawkfalcon,
jwdred,
- OfficialPiAddict,
+ KNakamura,
+ Kono Tyran,
+ Pucklynn,
Robby LaFarge,
and a few anonymous users for their ongoing support; you're awesome! 🏅
</p>
-<h2>For mod creators</h2>
+<h2 id="modcreators">For mod creators</h2>
<ul>
<li><a href="@Model.StableVersion.DevDownloadUrl">SMAPI @Model.StableVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li>
+ @if (Model.BetaVersion != null)
+ {
+ <li><a href="@Model.BetaVersion.DevDownloadUrl">SMAPI @Model.BetaVersion.Version for developers</a> (includes <a href="https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense">intellisense</a> and full console output)</li>
+ }
<li><a href="https://stardewvalleywiki.com/Modding:Index">Modding documentation</a></li>
<li>Need help? Come <a href="https://stardewvalleywiki.com/Modding:Community#Discord">chat on Discord</a>.</li>
</ul>
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 2d1c1b44..e735e8f3 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -1,76 +1,120 @@
+@using Newtonsoft.Json
+@using StardewModdingAPI.Web.Framework.LogParsing.Models
+@model StardewModdingAPI.Web.ViewModels.LogParserModel
+
@{
ViewData["Title"] = "SMAPI log parser";
+ IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod();
+ IDictionary<string, bool> defaultFilters = Enum
+ .GetValues(typeof(LogLevel))
+ .Cast<LogLevel>()
+ .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
+ JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None };
+}
- IDictionary<string, LogModInfo[]> contentPacks = Model.ParsedLog?.Mods
- ?.GroupBy(mod => mod.ContentPackFor)
- .Where(group => group.Key != null)
- .ToDictionary(group => group.Key, group => group.ToArray());
-
- Regex slugInvalidCharPattern = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
- string GetSlug(string modName)
+@section Head {
+ @if (Model.PasteID != null)
{
- return slugInvalidCharPattern.Replace(modName, "");
+ <meta name="robots" content="noindex" />
}
-}
-@using System.Text.RegularExpressions
-@using Newtonsoft.Json
-@using StardewModdingAPI.Web.Framework.LogParsing.Models
-@model StardewModdingAPI.Web.ViewModels.LogParserModel
-@section Head {
- <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180225" />
+ <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20180627" />
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js" crossorigin="anonymous"></script>
- <script src="~/Content/js/log-parser.js?r=20180225"></script>
+ <script src="~/Content/js/log-parser.js?r=20180627"></script>
<script>
$(function() {
smapi.logParser({
logStarted: new Date(@Json.Serialize(Model.ParsedLog?.Timestamp)),
showPopup: @Json.Serialize(Model.ParsedLog == null),
- showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), new JsonSerializerSettings { Formatting = Formatting.None }),
- showLevels: {
- trace: false,
- debug: false,
- info: true,
- alert: true,
- warn: true,
- error: true
- }
+ showMods: @Json.Serialize(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting),
+ showLevels: @Json.Serialize(defaultFilters, noFormatting),
+ enableFilters: @Json.Serialize(!Model.ShowRaw)
}, '@Model.SectionUrl');
});
</script>
}
-@*********
-** Intro
-*********@
-<p id="blurb">This page lets you upload, view, and share a SMAPI log to help troubleshoot mod issues.</p>
-
-@if (Model.ParsedLog?.IsValid == true)
+@* upload result banner *@
+@if (Model.UploadError != null)
{
- <div class="banner success" v-pre>
- <strong>The log was uploaded successfully!</strong><br/>
- Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br/>
- (Or <a id="upload-button" href="#">upload a new log</a>.)
+ <div class="banner error" v-pre>
+ <strong>Oops, the server ran into trouble saving that file.</strong><br />
+ <small v-pre>Error details: @Model.UploadError</small>
</div>
}
-else if (Model.ParsedLog?.IsValid == false)
+else if (Model.ParseError != null)
{
<div class="banner error" v-pre>
- <strong>Oops, couldn't parse that file. (Make sure you upload the log file, not the console text.)</strong><br />
+ <strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br />
Share this URL when asking for help: <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
- (Or <a id="upload-button" href="#">upload a new log</a>.)<br />
+ (Or <a href="@Model.SectionUrl">upload a new log</a>.)<br />
<br />
- <small v-pre>Error details: @Model.ParsedLog.Error</small>
+ <small v-pre>Error details: @Model.ParseError</small>
</div>
}
-else
+else if (Model.ParsedLog?.IsValid == true)
{
- <input type="button" id="upload-button" value="Share a new log" />
+ <div class="banner success" v-pre>
+ <strong>Share this link to let someone else see the log:</strong> <code>@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))</code><br />
+ (Or <a href="@Model.SectionUrl">upload a new log</a>.)
+ </div>
}
-@*********
-** Parsed log
-*********@
+@* upload new log *@
+@if (Model.ParsedLog == null)
+{
+ <h2>Where do I find my SMAPI log?</h2>
+ <div>What system do you use?</div>
+ <ul id="os-list">
+ <li><input type="radio" name="os" value="linux" id="os-linux" /> <label for="os-linux">Linux</label></li>
+ <li><input type="radio" name="os" value="mac" id="os-mac" /> <label for="os-mac">Mac</label></li>
+ <li><input type="radio" name="os" value="windows" id="os-windows" /> <label for="os-windows">Windows</label></li>
+ </ul>
+ <div data-os="linux">
+ On Linux:
+ <ol>
+ <li>Open the Files app.</li>
+ <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li>
+ <li>Choose <em>Enter Location</em>.</li>
+ <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ <div data-os="mac">
+ On Mac:
+ <ol>
+ <li>Open the Finder app.</li>
+ <li>Click <em>Go</em> at the top, then <em>Enter Location</em>.</li>
+ <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+ <div data-os="windows">
+ On Windows:
+ <ol>
+ <li>Press the <code>Windows</code> and <code>R</code> buttons at the same time.</li>
+ <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li>
+ <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
+ </ol>
+ </div>
+
+ <h2>How do I share my log?</h2>
+ <form action="@Model.SectionUrl" method="post">
+ <ol>
+ <li>
+ Drag the file onto this textbox (or paste the text in):<br />
+ <textarea id="input" name="input" placeholder="paste log here"></textarea>
+ </li>
+ <li>
+ Click this button:<br />
+ <input type="submit" id="submit" value="save log" />
+ </li>
+ <li>On the new page, copy the URL and send it to the person helping you.</li>
+ </ol>
+ </form>
+}
+
+@* parsed log *@
@if (Model.ParsedLog?.IsValid == true)
{
<h2>Log info</h2>
@@ -95,17 +139,20 @@ else
</tr>
</table>
<br />
- <table id="mods">
+ <table id="mods" class="@(Model.ShowRaw ? "filters-disabled" : null)">
<caption>
Installed mods:
- <span class="notice txt"><i>click any mod to filter</i></span>
- <span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span>
- <span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
+ @if (!Model.ShowRaw)
+ {
+ <span class="notice txt"><i>click any mod to filter</i></span>
+ <span class="notice btn txt" v-on:click="showAllMods" v-show="stats.modsHidden > 0">show all</span>
+ <span class="notice btn txt" v-on:click="hideAllMods" v-show="stats.modsShown > 0 && stats.modsHidden > 0">hide all</span>
+ }
</caption>
@foreach (var mod in Model.ParsedLog.Mods.Where(p => p.ContentPackFor == null))
{
- <tr v-on:click="toggleMod('@GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@GetSlug(mod.Name)'] }">
- <td><input type="checkbox" v-bind:checked="showMods['@GetSlug(mod.Name)']" v-show="anyModsHidden" /></td>
+ <tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }">
+ <td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-show="anyModsHidden" /></td>
<td v-pre>
<strong>@mod.Name</strong> @mod.Version
@if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[] contentPackList))
@@ -134,36 +181,47 @@ else
</tr>
}
</table>
- <div id="filters">
- Filter messages:
- <span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
- <span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
- <span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
- <span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
- <span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
- <span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
- </div>
- <table id="log">
- @foreach (var message in Model.ParsedLog.Messages)
- {
- string levelStr = message.Level.ToString().ToLower();
+ @if (!Model.ShowRaw)
+ {
+ <div id="filters">
+ Filter messages:
+ <span v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
+ <span v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
+ <span v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
+ <span v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
+ <span v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
+ <span v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
+ </div>
- <tr class="@levelStr mod" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')">
- <td v-pre>@message.Time</td>
- <td v-pre>@message.Level.ToString().ToUpper()</td>
- <td v-pre data-title="@message.Mod">@message.Mod</td>
- <td v-pre>@message.Text</td>
- </tr>
- if (message.Repeated > 0)
+ <table id="log">
+ @foreach (var message in Model.ParsedLog.Messages)
{
- <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@GetSlug(message.Mod)', '@levelStr')">
- <td colspan="3"></td>
- <td v-pre><i>repeats [@message.Repeated] times.</i></td>
+ string levelStr = message.Level.ToString().ToLower();
+
+ <tr class="@levelStr mod" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr')">
+ <td v-pre>@message.Time</td>
+ <td v-pre>@message.Level.ToString().ToUpper()</td>
+ <td v-pre data-title="@message.Mod">@message.Mod</td>
+ <td v-pre>@message.Text</td>
</tr>
+ if (message.Repeated > 0)
+ {
+ <tr class="@levelStr mod mod-repeat" v-show="filtersAllow('@Model.GetSlug(message.Mod)', '@levelStr')">
+ <td colspan="3"></td>
+ <td v-pre><i>repeats [@message.Repeated] times.</i></td>
+ </tr>
+ }
}
- }
- </table>
+ </table>
+
+ <small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))?raw=true">view raw log</a></small>
+ }
+ else
+ {
+ <pre v-pre>@Model.ParsedLog.RawText</pre>
+ <small><a href="@(new Uri(new Uri(Model.SectionUrl), Model.PasteID))">view parsed log</a></small>
+ }
</div>
}
else if (Model.ParsedLog?.IsValid == false)
@@ -171,22 +229,3 @@ else if (Model.ParsedLog?.IsValid == false)
<h3>Raw log</h3>
<pre v-pre>@Model.ParsedLog.RawText</pre>
}
-
-<div id="upload-area">
- <div id="popup-upload" class="popup">
- <h1>Upload log file</h1>
- <div class="frame">
- <ol>
- <li><a href="https://stardewvalleywiki.com/Modding:Player_FAQs#SMAPI_log" target="_blank">Find your SMAPI log file</a> (not the console text).</li>
- <li>Drag the file onto the textbox below (or paste the text in).</li>
- <li>Click <em>Parse</em>.</li>
- </ol>
- <textarea id="input" placeholder="Paste or drag the log here"></textarea>
- <div class="buttons">
- <input type="button" id="submit" value="Parse" />
- <input type="button" id="cancel" value="Cancel" />
- </div>
- </div>
- </div>
- <div id="uploader"></div>
-</div>
diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
index ac98c71b..29da9100 100644
--- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml
+++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
@@ -1,11 +1,12 @@
@using Microsoft.Extensions.Options
@using StardewModdingAPI.Web.Framework.ConfigModels
-@inject IOptions<ContextConfig> ContextConfig
+@inject IOptions<SiteConfig> SiteConfig
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1">
<title>@ViewData["Title"] - SMAPI.io</title>
<link rel="stylesheet" href="~/Content/css/main.css" />
@RenderSection("Head", required: false)
@@ -14,8 +15,8 @@
<div id="sidebar">
<h4>SMAPI</h4>
<ul>
- <li><a href="@ContextConfig.Value.RootUrl">About SMAPI</a></li>
- <li><a href="@ContextConfig.Value.LogParserUrl">Log parser</a></li>
+ <li><a href="@SiteConfig.Value.RootUrl">About SMAPI</a></li>
+ <li><a href="@SiteConfig.Value.LogParserUrl">Log parser</a></li>
<li><a href="https://stardewvalleywiki.com/Modding:Index">Docs</a></li>
</ul>
</div>
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index 495af120..67bb7748 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -16,10 +16,13 @@
"Microsoft": "Information"
}
},
- "Context": {
+
+ "Site": {
"RootUrl": "http://localhost:59482/",
- "LogParserUrl": "http://localhost:59482/log/"
+ "LogParserUrl": "http://localhost:59482/log/",
+ "EnableSmapiBeta": false
},
+
"ApiClients": {
"GitHubUsername": null,
"GitHubPassword": null,
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 095707a8..9e3270ae 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -13,10 +13,13 @@
"Default": "Warning"
}
},
- "Context": {
- "RootUrl": null, // see top note
- "LogParserUrl": null // see top note
+
+ "Site": {
+ "RootUrl": null, // see top note
+ "LogParserUrl": null, // see top note
+ "EnableSmapiBeta": null // see top note
},
+
"ApiClients": {
"UserAgent": "SMAPI/{0} (+https://smapi.io)",
@@ -30,9 +33,9 @@
"GitHubUsername": null, // see top note
"GitHubPassword": null, // see top note
- "NexusUserAgent": "Nexus Client v0.63.15",
- "NexusBaseUrl": "http://www.nexusmods.com/stardewvalley",
+ "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/",
"NexusModUrlFormat": "mods/{0}",
+ "NexusModScrapeUrlFormat": "mods/{0}?tab=files",
"PastebinBaseUrl": "https://pastebin.com/",
"PastebinUserKey": null, // see top note
@@ -46,6 +49,8 @@
"ChucklefishKey": "Chucklefish",
"GitHubKey": "GitHub",
- "NexusKey": "Nexus"
+ "NexusKey": "Nexus",
+
+ "WikiCompatibilityPageUrl": "https://smapi.io/compat"
}
}
diff --git a/src/SMAPI.Web/wwwroot/Content/css/index.css b/src/SMAPI.Web/wwwroot/Content/css/index.css
index 06cd6fb4..514e1a5c 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/index.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/index.css
@@ -18,7 +18,8 @@ h1 {
text-align: center;
}
-#call-to-action a {
+#call-to-action a.main-cta,
+#call-to-action a.secondary-cta {
box-shadow: #caefab 0 1px 0 0 inset;
background: linear-gradient(#77d42a 5%, #5cb811 100%) #77d42a;
border-radius: 6px;
@@ -40,6 +41,58 @@ h1 {
text-shadow: #2b665e 0 1px 0;
}
+.cta-dropdown {
+ position: relative;
+ display: inline-block;
+ margin-bottom: 1em;
+}
+
+.cta-dropdown a.download {
+ margin-bottom: 0 !important;
+}
+
+.cta-dropdown .dropdown-content {
+ display: none;
+ position: absolute;
+ text-align: left;
+ box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
+ border: 1px solid #566963;
+ background: #5cb811;
+ border-top: 0;
+ border-radius: 0 0 6px 6px;
+ margin-top: -6px;
+ z-index: 1;
+}
+
+.cta-dropdown .dropdown-content a:hover {
+ background-color: #ddd;
+}
+
+.cta-dropdown .dropdown-content img {
+ width: 0.85em;
+ height: 0.85em;
+}
+
+.cta-dropdown.secondary-cta-dropdown .dropdown-content a:hover {
+ background-color: #566963;
+}
+
+.cta-dropdown.secondary-cta-dropdown .dropdown-content {
+ background-color: #768d87;
+ border-color: #566963;
+}
+
+.cta-dropdown.secondary-cta-dropdown .dropdown-content a {
+ color: #fff;
+ text-shadow: #2b665e 0 1px 0;
+}
+
+.cta-dropdown .dropdown-content a {
+ padding: 0.75em 1em;
+ text-decoration: none;
+ display: block;
+}
+
/*********
** Subsections
*********/
@@ -48,10 +101,6 @@ h1 {
padding-left: 1em;
}
-.github-description .noinclude {
- display: none;
-}
-
#support-links li small {
display: block;
width: 50em;
diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
index 789274e2..1fcd1bff 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
@@ -1,12 +1,8 @@
/*********
** Main layout
*********/
-input[type="button"] {
- font-size: 20px;
- border-radius: 5px;
- outline: none;
- box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2);
- cursor: pointer;
+#content {
+ max-width: 100%;
}
caption {
@@ -20,15 +16,6 @@ caption {
font-family: monospace;
}
-input#upload-button {
- background: #ccf;
- border: 1px solid #000088;
-}
-
-input#upload-button {
- background: #eef;
-}
-
table caption {
font-weight: bold;
}
@@ -39,6 +26,7 @@ table caption {
.banner {
border: 2px solid gray;
border-radius: 5px;
+ margin-top: 1em;
padding: 1em;
}
@@ -91,6 +79,10 @@ table#metadata, table#mods {
cursor: pointer;
}
+#mods.filters-disabled tr {
+ cursor: default;
+}
+
#metadata tr,
#mods tr {
background: #eee
@@ -200,6 +192,12 @@ table#metadata, table#mods {
color: #f00;
}
+#log .critical {
+ background-color: #c00;
+ color: #fff;
+ font-weight: bold;
+}
+
#log {
border-spacing: 0;
}
@@ -256,88 +254,19 @@ table#metadata, table#mods {
/*********
-** Upload popup
+** Upload form
*********/
-#upload-area .popup,
-#upload-area #uploader {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(0, 0, 0, .33);
- z-index: 2;
- display: none;
- padding: 5px;
-}
-
-#upload-area #uploader:after {
- content: attr(data-text);
- display: block;
- width: 100px;
- height: 24px;
- line-height: 25px;
- border: 1px solid #000;
- background: #fff;
- position: absolute;
- top: 50%;
- left: 50%;
- margin: -12px -50px 0 0;
- font-size: 18px;
- font-weight: bold;
- text-align: center;
- border-radius: 5px;
-}
-
-#upload-area .popup h1 {
- position: absolute;
- top: 10%;
- left: 50%;
- margin-left: -150px;
- text-align: center;
- width: 300px;
- border: 1px solid #008;
- border-radius: 5px;
- background: #fff;
- font-family: sans-serif;
- font-size: 40px;
- margin-top: -25px;
- z-index: 10;
- border-bottom: 0;
+#os-list {
+ list-style: none;
}
-#upload-area .frame {
- margin: auto;
- margin-top: 25px;
- padding: 2em;
- position: absolute;
- top: 10%;
- left: 10%;
- right: 10%;
- bottom: 10%;
- padding-bottom: 30px;
- background: #FFF;
- border-radius: 5px;
- border: 1px solid #008;
-}
-
-#upload-area #cancel {
- border: 1px solid #880000;
- background-color: #fcc;
-}
-
-#upload-area #submit {
- border: 1px solid #008800;
- background-color: #cfc;
-}
-
-#upload-area #submit:hover {
- background-color: #efe;
+div[data-os] {
+ display: none;
}
-#upload-area #input {
+#input {
width: 100%;
- height: 30em;
+ height: 20em;
max-height: 70%;
margin: auto;
box-sizing: border-box;
@@ -346,3 +275,13 @@ table#metadata, table#mods {
outline: none;
box-shadow: inset 0px 0px 1px 1px rgba(0, 0, 192, .2);
}
+
+#submit {
+ font-size: 1.5em;
+ border-radius: 5px;
+ outline: none;
+ box-shadow: inset 0 0 1px 1px rgba(0, 0, 0, .2);
+ cursor: pointer;
+ border: 1px solid #008800;
+ background-color: #cfc;
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/css/main.css b/src/SMAPI.Web/wwwroot/Content/css/main.css
index d1fa49e0..57eeee88 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/main.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/main.css
@@ -28,7 +28,8 @@ h2 {
h3 {
font-size: 1.2em;
border-bottom: 1px solid #AAA;
- width: 50%;
+ width: 55em;
+ max-width: 100%;
}
a {
@@ -44,6 +45,8 @@ a {
#content {
min-height: 140px;
+ width: calc(100% - 2em);
+ max-width: 60em;
padding: 0 1em 1em 1em;
border-left: 1px solid #CCC;
background: #FFF;
@@ -51,7 +54,7 @@ a {
}
#content p {
- max-width: 55em;
+ max-width: 100%;
}
.section {
@@ -105,3 +108,34 @@ a {
#footer a {
color: #669;
}
+
+/* mobile fixes */
+@media (min-width: 1020px) and (max-width: 1199px) {
+ #sidebar {
+ width: 7em;
+ background: none;
+ }
+
+ #content-column {
+ left: 7em;
+ }
+}
+
+@media (max-width: 1019px) {
+ h1 {
+ margin-top: 0;
+ }
+
+ #sidebar {
+ margin-top: 0;
+ width: auto;
+ min-height: 0;
+ background: none;
+ }
+
+ #content-column {
+ position: inherit;
+ top: inherit;
+ left: inherit;
+ }
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png b/src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png
new file mode 100644
index 00000000..6c30ca36
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/images/direct-download-icon.png
Binary files differ
diff --git a/src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png b/src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png
new file mode 100644
index 00000000..10c66712
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/images/nexus-icon.png
Binary files differ
diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png
new file mode 100644
index 00000000..f359146c
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/images/pufferchick-cool.png
Binary files differ
diff --git a/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png
new file mode 100644
index 00000000..1de9cf47
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/images/pufferchick.png
Binary files differ
diff --git a/src/SMAPI.Web/wwwroot/Content/js/index.js b/src/SMAPI.Web/wwwroot/Content/js/index.js
new file mode 100644
index 00000000..d0734b02
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/Content/js/index.js
@@ -0,0 +1,34 @@
+$(document).ready(function () {
+ /* enable pufferchick */
+ var pufferchick = $("#pufferchick");
+ $(".cta-dropdown").hover(
+ function () {
+ pufferchick.attr("src", "Content/images/pufferchick-cool.png");
+ },
+ function () {
+ pufferchick.attr("src", "Content/images/pufferchick.png");
+ }
+ );
+
+ /* enable download dropdowns */
+ $(".cta-dropdown a.download").each(function(i, button) {
+ button = $(button);
+ var wrapper = button.parent(".cta-dropdown");
+ var button = wrapper.find(".download");
+ var dropdownContent = wrapper.find(".dropdown-content");
+
+ $(window).on("click", function(e) {
+ var target = $(e.target);
+
+ // toggle dropdown on button click
+ if (target.is(button) || $.contains(button.get(0), target.get(0))) {
+ e.preventDefault();
+ dropdownContent.toggle();
+ }
+
+ // else hide dropdown
+ else
+ dropdownContent.hide();
+ });
+ });
+});
diff --git a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
index c4a35e96..0c654205 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/log-parser.js
@@ -39,11 +39,17 @@ smapi.logParser = function (data, sectionUrl) {
}
},
methods: {
- toggleLevel: function(id) {
+ toggleLevel: function (id) {
+ if (!data.enableFilters)
+ return;
+
this.showLevels[id] = !this.showLevels[id];
},
toggleMod: function (id) {
+ if (!data.enableFilters)
+ return;
+
var curShown = this.showMods[id];
// first filter: only show this by default
@@ -64,6 +70,9 @@ smapi.logParser = function (data, sectionUrl) {
},
showAllMods: function () {
+ if (!data.enableFilters)
+ return;
+
for (var key in this.showMods) {
if (this.showMods.hasOwnProperty(key)) {
this.showMods[key] = true;
@@ -73,6 +82,9 @@ smapi.logParser = function (data, sectionUrl) {
},
hideAllMods: function () {
+ if (!data.enableFilters)
+ return;
+
for (var key in this.showMods) {
if (this.showMods.hasOwnProperty(key)) {
this.showMods[key] = false;
@@ -90,88 +102,50 @@ smapi.logParser = function (data, sectionUrl) {
/**********
** Upload form
*********/
- var error = $("#error");
-
- $("#upload-button").on("click", function(e) {
- e.preventDefault();
-
- $("#input").val("");
- $("#popup-upload").fadeIn();
- });
-
- var closeUploadPopUp = function() {
- $("#popup-upload").fadeOut(400);
- };
-
- $("#popup-upload").on({
- 'dragover dragenter': function(e) {
- e.preventDefault();
- e.stopPropagation();
- },
- 'drop': function(e) {
- $("#uploader").attr("data-text", "Reading...");
- $("#uploader").show();
- var dataTransfer = e.originalEvent.dataTransfer;
- if (dataTransfer && dataTransfer.files.length) {
- e.preventDefault();
- e.stopPropagation();
- var file = dataTransfer.files[0];
- var reader = new FileReader();
- reader.onload = $.proxy(function(file, $input, event) {
- $input.val(event.target.result);
- $("#uploader").fadeOut();
- $("#submit").click();
- }, this, file, $("#input"));
- reader.readAsText(file);
- }
- },
- 'click': function(e) {
- if (e.target.id === "popup-upload")
- closeUploadPopUp();
+ var input = $("#input");
+ if (input.length) {
+ // get elements
+ var systemOptions = $("input[name='os']");
+ var systemInstructions = $("div[data-os]");
+ var submit = $("#submit");
+
+ // instruction OS chooser
+ var chooseSystem = function() {
+ systemInstructions.hide();
+ systemInstructions.filter("[data-os='" + $("input[name='os']:checked").val() + "']").show();
}
- });
-
- $("#submit").on("click", function() {
- $("#popup-upload").fadeOut();
- var paste = $("#input").val();
- if (paste) {
- //memory = "";
- $("#uploader").attr("data-text", "Saving...");
- $("#uploader").fadeIn();
- $
- .ajax({
- type: "POST",
- url: sectionUrl + "/save",
- data: JSON.stringify(paste),
- contentType: "application/json" // sent to API
- })
- .fail(function(xhr, textStatus) {
- $("#uploader").fadeOut();
- error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: Upload</p>Error: ' + textStatus + ': ' + xhr.responseText + "<hr /><pre>" + $("#input").val() + "</pre>");
- })
- .then(function(data) {
- $("#uploader").fadeOut();
- if (!data.success)
- error.html('<h1>Parsing failed!</h1>Parsing of the log failed, details follow.<br />&nbsp;<p>Stage: Upload</p>Error: ' + data.error + "<hr /><pre>" + $("#input").val() + "</pre>");
- else
- location.href = (sectionUrl.replace(/\/$/, "") + "/" + data.id);
- });
- } else {
- alert("Unable to parse log, the input is empty!");
- $("#uploader").fadeOut();
+ systemOptions.on("click", chooseSystem);
+ chooseSystem();
+
+ // disable submit if it's empty
+ var toggleSubmit = function()
+ {
+ var hasText = !!input.val().trim();
+ submit.prop("disabled", !hasText);
}
- });
+ input.on("input", toggleSubmit);
+ toggleSubmit();
- $(document).on("keydown", function(e) {
- if (e.which === 27) {
- if ($("#popup-upload").css("display") !== "none" && $("#popup-upload").css("opacity") === 1) {
- closeUploadPopUp();
+ // drag & drop file
+ input.on({
+ 'dragover dragenter': function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ 'drop': function(e) {
+ var dataTransfer = e.originalEvent.dataTransfer;
+ if (dataTransfer && dataTransfer.files.length) {
+ e.preventDefault();
+ e.stopPropagation();
+ var file = dataTransfer.files[0];
+ var reader = new FileReader();
+ reader.onload = $.proxy(function(file, $input, event) {
+ $input.val(event.target.result);
+ toggleSubmit();
+ }, this, file, $("#input"));
+ reader.readAsText(file);
+ }
}
- }
- });
- $("#cancel").on("click", closeUploadPopUp);
-
- if (data.showPopup)
- $("#popup-upload").fadeIn();
-
+ });
+ }
};
diff --git a/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
new file mode 100644
index 00000000..e72efb39
--- /dev/null
+++ b/src/SMAPI.Web/wwwroot/StardewModdingAPI.metadata.json
@@ -0,0 +1,1676 @@
+{
+ /**
+ * Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This
+ * field shouldn't be edited by players in most cases.
+ *
+ * Standard fields
+ * ===============
+ * The predefined fields are documented below (only 'ID' is required). Each entry's key is the
+ * default display name for the mod if one isn't available (e.g. in dependency checks).
+ *
+ * - ID: the mod's latest unique ID (if any).
+ *
+ * - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching
+ * other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple
+ * variants can be separated with '|'.
+ *
+ * - MapLocalVersions and MapRemoteVersions correct local manifest versions and remote versions
+ * during update checks. For example, if the API returns version '1.1-1078' where '1078' is
+ * intended to be a build number, MapRemoteVersions can map it to '1.1' when comparing to the
+ * mod's current version. This is only meant to support legacy mods with injected update keys.
+ *
+ * Versioned metadata
+ * ==================
+ * Each record can also specify extra metadata using the field keys below.
+ *
+ * Each key consists of a field name prefixed with any combination of version range and 'Default',
+ * separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override,
+ * 'Default | UpdateKey' will only override if the mod has no update keys, and
+ * '~1.1 | Default | Name' will do the same up to version 1.1.
+ *
+ * The version format is 'min~max' (where either side can be blank for unbounded), or a single
+ * version number.
+ *
+ * These are the valid field names:
+ *
+ * - UpdateKey: the update key to set in the mod's manifest. This is used to enable update
+ * checks for older mods that haven't been updated to use it yet.
+ *
+ * - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load
+ * it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because
+ * the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it
+ * even if it detects incompatible code).
+ *
+ * Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility
+ * automatically, since that hides the details from trace logs.
+ *
+ * - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded
+ * (if applicable). If blank, will default to a generic not-compatible message.
+ *
+ * - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the
+ * mod is no longer compatible.
+ */
+ "ModData": {
+ "AccessChestAnywhere": {
+ "ID": "AccessChestAnywhere",
+ "MapLocalVersions": { "1.1-1078": "1.1" },
+ "Default | UpdateKey": "Nexus:257",
+ "~1.1 | Status": "AssumeBroken"
+ },
+
+ "Adjust Artisan Prices": {
+ "ID": "ThatNorthernMonkey.AdjustArtisanPrices",
+ "FormerIDs": "1e36d4ca-c7ef-4dfb-9927-d27a6c3c8bdc", // changed in 0.0.2-pathoschild-update
+ "MapRemoteVersions": { "0.01": "0.0.1" },
+ "Default | UpdateKey": "Chucklefish:3532"
+ },
+
+ "Adjust Monster": {
+ "ID": "mmanlapat.AdjustMonster",
+ "Default | UpdateKey": "Nexus:1161"
+ },
+
+ "Advanced Location Loader": {
+ "ID": "Entoarox.AdvancedLocationLoader",
+ "~1.3.7 | UpdateKey": "Chucklefish:3619" // only enable update checks up to 1.3.7 by request (has its own update-check feature)
+ },
+
+ "Adventure Shop Inventory": {
+ "ID": "HammurabiAdventureShopInventory",
+ "Default | UpdateKey": "Chucklefish:4608"
+ },
+
+ "AgingMod": {
+ "ID": "skn.AgingMod",
+ "Default | UpdateKey": "Nexus:1129",
+ "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "All Crops All Seasons": {
+ "ID": "cantorsdust.AllCropsAllSeasons",
+ "FormerIDs": "29ee8246-d67b-4242-a340-35a9ae0d5dd7 | community.AllCropsAllSeasons", // changed in 1.3 and 1.5
+ "Default | UpdateKey": "Nexus:170"
+ },
+
+ "All Professions": {
+ "ID": "cantorsdust.AllProfessions",
+ "FormerIDs": "8c37b1a7-4bfb-4916-9d8a-9533e6363ea3 | community.AllProfessions", // changed in 1.2 and 1.3.1
+ "Default | UpdateKey": "Nexus:174"
+ },
+
+ "Almighty Farming Tool": {
+ "ID": "439",
+ "MapRemoteVersions": {
+ "1.21": "1.2.1",
+ "1.22-unofficial.3.mizzion": "1.2.2-unofficial.3.mizzion"
+ },
+ "Default | UpdateKey": "Nexus:439"
+ },
+
+ "Animal Husbandry": {
+ "ID": "DIGUS.ANIMALHUSBANDRYMOD",
+ "FormerIDs": "DIGUS.BUTCHER", // changed in 2.0.1
+ "Default | UpdateKey": "Nexus:1538"
+ },
+
+ "Animal Mood Fix": {
+ "ID": "GPeters-AnimalMoodFix",
+ "~ | Status": "Obsolete",
+ "~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2."
+ },
+
+ "Animal Sitter": {
+ "ID": "jwdred.AnimalSitter",
+ "Default | UpdateKey": "Nexus:581",
+ "~1.0.8 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Arcade Pong": {
+ "ID": "Platonymous.ArcadePong",
+ "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals
+ },
+
+ "A Tapper's Dream": {
+ "ID": "ddde5195-8f85-4061-90cc-0d4fd5459358",
+ "Default | UpdateKey": "Nexus:260"
+ },
+
+ "Auto Animal Doors": {
+ "ID": "AaronTaggart.AutoAnimalDoors",
+ "Default | UpdateKey": "Nexus:1019"
+ },
+
+ "Auto-Eat": {
+ "ID": "Permamiss.AutoEat",
+ "FormerIDs": "BALANCEMOD_AutoEat", // changed in 1.1.1
+ "Default | UpdateKey": "Nexus:643"
+ },
+
+ "AutoFish": {
+ "ID": "WhiteMind.AF",
+ "Default | UpdateKey": "Nexus:1895"
+ },
+
+ "AutoGate": {
+ "ID": "AutoGate",
+ "Default | UpdateKey": "Nexus:820"
+ },
+
+ "Automate": {
+ "ID": "Pathoschild.Automate",
+ "Default | UpdateKey": "Nexus:1063",
+ "~1.10-beta.7 | Status": "AssumeBroken" // broke in SDV 1.3.20
+ },
+
+ "Automated Doors": {
+ "ID": "azah.automated-doors",
+ "FormerIDs": "1abcfa07-2cf4-4dc3-a6e9-6068b642112b", // changed in 1.4.1
+ "Default | UpdateKey": "GitHub:azah/AutomatedDoors" // added in 1.4.2
+ },
+
+ "AutoSpeed": {
+ "ID": "Omegasis.AutoSpeed",
+ "Default | UpdateKey": "Nexus:443" // added in 1.4.1
+ },
+
+ "Basic Sprinklers Improved": {
+ "ID": "lrsk_sdvm_bsi.0117171308",
+ "MapRemoteVersions": { "1.0.2": "1.0.1-release" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:833"
+ },
+
+ "Better Hay": {
+ "ID": "cat.betterhay",
+ "Default | UpdateKey": "Nexus:1430"
+ },
+
+ "Better Quality More Seasons": {
+ "ID": "SB_BQMS",
+ "Default | UpdateKey": "Nexus:935"
+ },
+
+ "Better Quarry": {
+ "ID": "BetterQuarry",
+ "Default | UpdateKey": "Nexus:771"
+ },
+
+ "Better Ranching": {
+ "ID": "BetterRanching",
+ "Default | UpdateKey": "Nexus:859"
+ },
+
+ "Better Shipping Box": {
+ "ID": "Kithio:BetterShippingBox",
+ "MapLocalVersions": { "1.0.1": "1.0.2" },
+ "Default | UpdateKey": "Chucklefish:4302"
+ },
+
+ "Better Sprinklers": {
+ "ID": "Speeder.BetterSprinklers",
+ "FormerIDs": "SPDSprinklersMod", // changed in 2.3
+ "Default | UpdateKey": "Nexus:41"
+ },
+
+ "Billboard Anywhere": {
+ "ID": "Omegasis.BillboardAnywhere",
+ "Default | UpdateKey": "Nexus:492" // added in 1.4.1
+ },
+
+ "Birthday Mail": {
+ "ID": "KathrynHazuka.BirthdayMail",
+ "FormerIDs": "005e02dc-d900-425c-9c68-1ff55c5a295d", // changed in 1.2.3-pathoschild-update
+ "Default | UpdateKey": "Nexus:276",
+ "MapRemoteVersions": { "1.3.1": "1.3" } // manifest not updated
+ },
+
+ "Breed Like Rabbits": {
+ "ID": "dycedarger.breedlikerabbits",
+ "Default | UpdateKey": "Nexus:948"
+ },
+
+ "Build Endurance": {
+ "ID": "Omegasis.BuildEndurance",
+ "Default | UpdateKey": "Nexus:445" // added in 1.4.1
+ },
+
+ "Build Health": {
+ "ID": "Omegasis.BuildHealth",
+ "Default | UpdateKey": "Nexus:446" // added in 1.4.1
+ },
+
+ "Buy Cooking Recipes": {
+ "ID": "Denifia.BuyRecipes",
+ "Default | UpdateKey": "Nexus:1126" // added in 1.0.1 (2017-10-04)
+ },
+
+ "Buy Back Collectables": {
+ "ID": "Omegasis.BuyBackCollectables",
+ "FormerIDs": "BuyBackCollectables", // changed in 1.4
+ "Default | UpdateKey": "Nexus:507" // added in 1.4.1
+ },
+
+ "Carry Chest": {
+ "ID": "spacechase0.CarryChest",
+ "Default | UpdateKey": "Nexus:1333"
+ },
+
+ "Casks Anywhere": {
+ "ID": "CasksAnywhere",
+ "MapLocalVersions": { "1.1-alpha": "1.1" },
+ "Default | UpdateKey": "Nexus:878"
+ },
+
+ "Categorize Chests": {
+ "ID": "CategorizeChests",
+ "Default | UpdateKey": "Nexus:1300",
+ "~1.4.3-unofficial.2.mizzion | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors)
+ },
+
+ "Chefs Closet": {
+ "ID": "Duder.ChefsCloset",
+ "MapLocalVersions": { "1.3-1": "1.3" },
+ "Default | UpdateKey": "Nexus:1030"
+ },
+
+ "Chest Label System": {
+ "ID": "Speeder.ChestLabel",
+ "FormerIDs": "SPDChestLabel", // changed in 1.5.1-pathoschild-update
+ "Default | UpdateKey": "Nexus:242"
+ },
+
+ "Chest Pooling": {
+ "ID": "mralbobo.ChestPooling",
+ "Default | UpdateKey": "GitHub:mralbobo/stardew-chest-pooling"
+ },
+
+ "Chests Anywhere": {
+ "ID": "Pathoschild.ChestsAnywhere",
+ "FormerIDs": "ChestsAnywhere", // changed in 1.9
+ "Default | UpdateKey": "Nexus:518",
+ "~1.12.4 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "CJB Automation": {
+ "ID": "CJBAutomation",
+ "Default | UpdateKey": "Nexus:211",
+ "~1.4 | Status": "AssumeBroken", // broke in SDV 1.2
+ "~1.4 | AlternativeUrl": "http://www.nexusmods.com/stardewvalley/mods/1063"
+ },
+
+ "CJB Cheats Menu": {
+ "ID": "CJBok.CheatsMenu",
+ "FormerIDs": "CJBCheatsMenu", // changed in 1.14
+ "Default | UpdateKey": "Nexus:4",
+ "~1.18-beta | Status": "AssumeBroken" // broke in SDV 1.3, first beta causes significant friendship bugs
+ },
+
+ "CJB Item Spawner": {
+ "ID": "CJBok.ItemSpawner",
+ "FormerIDs": "CJBItemSpawner", // changed in 1.7
+ "Default | UpdateKey": "Nexus:93",
+ "~1.10 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "CJB Show Item Sell Price": {
+ "ID": "CJBok.ShowItemSellPrice",
+ "FormerIDs": "CJBShowItemSellPrice", // changed in 1.7
+ "Default | UpdateKey": "Nexus:5",
+ "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Clean Farm": {
+ "ID": "tstaples.CleanFarm",
+ "Default | UpdateKey": "Nexus:794"
+ },
+
+ "Climates of Ferngill": {
+ "ID": "KoihimeNakamura.ClimatesOfFerngill",
+ "Default | UpdateKey": "Nexus:604"
+ },
+
+ "Coal Regen": {
+ "ID": "Blucifer.CoalRegen",
+ "Default | UpdateKey": "Nexus:1664"
+ },
+
+ "Cobalt": {
+ "ID": "spacechase0.Cobalt",
+ "MapRemoteVersions": { "1.1.3": "1.1.2" } // not updated in manifest
+ },
+
+ "Cold Weather Haley": {
+ "ID": "LordXamon.ColdWeatherHaleyPRO",
+ "Default | UpdateKey": "Nexus:1169",
+ "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Colored Chests": {
+ "ID": "4befde5c-731c-4853-8e4b-c5cdf946805f",
+ "~ | Status": "Obsolete",
+ "~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1."
+ },
+
+ "Combat with Farm Implements": {
+ "ID": "SPDFarmingImplementsInCombat",
+ "Default | UpdateKey": "Nexus:313"
+ },
+
+ "Community Bundle Item Tooltip": {
+ "ID": "musbah.bundleTooltip",
+ "Default | UpdateKey": "Nexus:1329"
+ },
+
+ "Concentration on Farming": {
+ "ID": "punyo.ConcentrationOnFarming",
+ "Default | UpdateKey": "Nexus:1445"
+ },
+
+ "Configurable Machines": {
+ "ID": "21da6619-dc03-4660-9794-8e5b498f5b97",
+ "MapLocalVersions": { "1.2-beta": "1.2" },
+ "Default | UpdateKey": "Nexus:280"
+ },
+
+ "Configurable Shipping Dates": {
+ "ID": "ConfigurableShippingDates",
+ "Default | UpdateKey": "Nexus:675",
+ "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Content Patcher": {
+ "ID": "Pathoschild.ContentPatcher",
+ "Default | UpdateKey": "Nexus:1915",
+ "~1.4-beta.5 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.18 (in-game errors)
+ },
+
+ "Cooking Skill": {
+ "ID": "spacechase0.CookingSkill",
+ "FormerIDs": "CookingSkill", // changed in 1.0.4–6
+ "Default | UpdateKey": "Nexus:522"
+ },
+
+ "CrabNet": {
+ "ID": "jwdred.CrabNet",
+ "Default | UpdateKey": "Nexus:584"
+ },
+
+ "Crafting Counter": {
+ "ID": "lolpcgaming.CraftingCounter",
+ "Default | UpdateKey": "Nexus:1585",
+ "MapRemoteVersions": { "1.1": "1.0" } // not updated in manifest
+ },
+
+ "Current Location": {
+ "ID": "CurrentLocation102120161203",
+ "Default | UpdateKey": "Nexus:638"
+ },
+
+ "Custom Asset Modifier": {
+ "ID": "Omegasis.CustomAssetModifier",
+ "Default | UpdateKey": "1836"
+ },
+
+ "Custom Critters": {
+ "ID": "spacechase0.CustomCritters",
+ "Default | UpdateKey": "Nexus:1255"
+ },
+
+ "Custom Crops": {
+ "ID": "spacechase0.CustomCrops",
+ "Default | UpdateKey": "Nexus:1592"
+ },
+
+ "Custom Element Handler": {
+ "ID": "Platonymous.CustomElementHandler",
+ "Default | UpdateKey": "Nexus:1068" // added in 1.3.1
+ },
+
+ "Custom Farming Redux": {
+ "ID": "Platonymous.CustomFarming",
+ "Default | UpdateKey": "Nexus:991" // added in 0.6.1
+ },
+
+ "Custom Farming Automate Bridge": {
+ "ID": "Platonymous.CFAutomate",
+ "~1.0.1 | Status": "AssumeBroken", // no longer compatible with Automate
+ "~1.0.1 | AlternativeUrl": "https://www.nexusmods.com/stardewvalley/mods/991"
+ },
+
+ "Custom Farm Types": {
+ "ID": "spacechase0.CustomFarmTypes",
+ "Default | UpdateKey": "Nexus:1140"
+ },
+
+ "Custom Furniture": {
+ "ID": "Platonymous.CustomFurniture",
+ "Default | UpdateKey": "Nexus:1254" // added in 0.4.1
+ },
+
+ "Customize Exterior": {
+ "ID": "spacechase0.CustomizeExterior",
+ "FormerIDs": "CustomizeExterior", // changed in 1.0.3
+ "Default | UpdateKey": "Nexus:1099",
+ "~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Customizable Cart Redux": {
+ "ID": "KoihimeNakamura.CCR",
+ "MapLocalVersions": { "1.1-20170917": "1.1" },
+ "Default | UpdateKey": "Nexus:1402"
+ },
+
+ "Customizable Traveling Cart Days": {
+ "ID": "TravelingCartYyeahdude",
+ "Default | UpdateKey": "Nexus:567"
+ },
+
+ "Custom Linens": {
+ "ID": "Mevima.CustomLinens",
+ "MapRemoteVersions": { "1.1": "1.0" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:1027"
+ },
+
+ "Custom NPC": {
+ "ID": "Platonymous.CustomNPC",
+ "Default | UpdateKey": "Nexus:1607"
+ },
+
+ "Custom Shops Redux": {
+ "ID": "Omegasis.CustomShopReduxGui",
+ "Default | UpdateKey": "Nexus:1378" // added in 1.4.1
+ },
+
+ "Custom TV": {
+ "ID": "Platonymous.CustomTV",
+ "Default | UpdateKey": "Nexus:1139" // added in 1.0.6
+ },
+
+ "Daily Luck Message": {
+ "ID": "Schematix.DailyLuckMessage",
+ "Default | UpdateKey": "Nexus:1327"
+ },
+
+ "Daily News": {
+ "ID": "bashNinja.DailyNews",
+ "Default | UpdateKey": "Nexus:1141",
+ "~1.2 | Status": "AssumeBroken" // broke in Stardew Valley 1.3 (or depends on CustomTV which broke)
+ },
+
+ "Daily Quest Anywhere": {
+ "ID": "Omegasis.DailyQuestAnywhere",
+ "FormerIDs": "DailyQuest", // changed in 1.4
+ "Default | UpdateKey": "Nexus:513" // added in 1.4.1
+ },
+
+ "Data Maps": {
+ "ID": "Pathoschild.DataMaps",
+ "Default | UpdateKey": "Nexus:1691",
+ "~1.4 | Status": "AssumeBroken" // replaced by Data Layers
+ },
+
+ "Debug Mode": {
+ "ID": "Pathoschild.DebugMode",
+ "FormerIDs": "Pathoschild.Stardew.DebugMode", // changed in 1.4
+ "Default | UpdateKey": "Nexus:679",
+ "~1.8 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Did You Water Your Crops?": {
+ "ID": "Nishtra.DidYouWaterYourCrops",
+ "Default | UpdateKey": "Nexus:1583"
+ },
+
+ "Dynamic Checklist": {
+ "ID": "gunnargolf.DynamicChecklist",
+ "Default | UpdateKey": "Nexus:1145" // added in 1.0.1-pathoschild-update
+ },
+
+ "Dynamic Horses": {
+ "ID": "Bpendragon-DynamicHorses",
+ "MapRemoteVersions": { "1.2": "1.1-release" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:874"
+ },
+
+ "Dynamic Machines": {
+ "ID": "DynamicMachines",
+ "MapLocalVersions": { "1.1": "1.1.1" },
+ "Default | UpdateKey": "Nexus:374",
+ "~1.1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Dynamic NPC Sprites": {
+ "ID": "BashNinja.DynamicNPCSprites",
+ "Default | UpdateKey": "Nexus:1183"
+ },
+
+ "Easier Farming": {
+ "ID": "cautiouswafffle.EasierFarming",
+ "Default | UpdateKey": "Nexus:1426"
+ },
+
+ "Empty Hands": {
+ "ID": "QuicksilverFox.EmptyHands",
+ "Default | UpdateKey": "Nexus:1176" // added in 1.0.1-pathoschild-update
+ },
+
+ "Enemy Health Bars": {
+ "ID": "Speeder.HealthBars",
+ "FormerIDs": "SPDHealthBar", // changed in 1.7.1-pathoschild-update
+ "Default | UpdateKey": "Nexus:193"
+ },
+
+ "Entoarox Framework": {
+ "ID": "Entoarox.EntoaroxFramework",
+ "FormerIDs": "eacdb74b-4080-4452-b16b-93773cda5cf9", // changed in ???
+ "~2.0.6 | UpdateKey": "Chucklefish:4228", // only enable update checks up to 2.0.6 by request (has its own update-check feature)
+ "~2.0.6 | Status": "AssumeBroken" // broke in SMAPI 2.5 (error reflecting into SMAPI internals)
+ },
+
+ "Expanded Fridge": {
+ "ID": "Uwazouri.ExpandedFridge",
+ "Default | UpdateKey": "Nexus:1191"
+ },
+
+ "Experience Bars": {
+ "ID": "spacechase0.ExperienceBars",
+ "FormerIDs": "ExperienceBars", // changed in 1.0.2
+ "Default | UpdateKey": "Nexus:509"
+ },
+
+ "Extended Bus System": {
+ "ID": "ExtendedBusSystem",
+ "Default | UpdateKey": "Chucklefish:4373"
+ },
+
+ "Extended Fridge": {
+ "ID": "Crystalmir.ExtendedFridge",
+ "FormerIDs": "Mystra007ExtendedFridge", // changed in 1.0.1
+ "Default | UpdateKey": "Nexus:485"
+ },
+
+ "Extended Greenhouse": {
+ "ID": "ExtendedGreenhouse",
+ "Default | UpdateKey": "Chucklefish:4303",
+ "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Extended Minecart": {
+ "ID": "Entoarox.ExtendedMinecart",
+ "~1.7.1 | UpdateKey": "Chucklefish:4359" // only enable update checks up to 1.7.1 by request (has its own update-check feature)
+ },
+
+ "Extended Reach": {
+ "ID": "spacechase0.ExtendedReach",
+ "Default | UpdateKey": "Nexus:1493"
+ },
+
+ "Fall 28 Snow Day": {
+ "ID": "Omegasis.Fall28SnowDay",
+ "Default | UpdateKey": "Nexus:486", // added in 1.4.1
+ "~1.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0, and update for SMAPI 2.0 doesn't do anything
+ },
+
+ "Farm Automation Unofficial: Item Collector": {
+ "ID": "Maddy99.FarmAutomation.ItemCollector",
+ "~0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Farm Expansion": {
+ "ID": "Advize.FarmExpansion",
+ "FormerIDs": "3888bdfd-73f6-4776-8bb7-8ad45aea1915 | AdvizeFarmExpansionMod-2-0 | AdvizeFarmExpansionMod-2-0-5", // changed in 2.0, 2.0.5, and 3.0
+ "Default | UpdateKey": "Nexus:130"
+ },
+
+ "Fast Animations": {
+ "ID": "Pathoschild.FastAnimations",
+ "Default | UpdateKey": "Nexus:1089",
+ "~1.5 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Faster Grass": {
+ "ID": "IceGladiador.FasterGrass",
+ "Default | UpdateKey": "Nexus:1772"
+ },
+
+ "Faster Paths": {
+ "ID": "Entoarox.FasterPaths",
+ "FormerIDs": "615f85f8-5c89-44ee-aecc-c328f172e413", // changed in 1.3
+ "~1.3.3 | UpdateKey": "Chucklefish:3641" // only enable update checks up to 1.3.3 by request (has its own update-check feature)
+ },
+
+ "Fishing Adjust": {
+ "ID": "shuaiz.FishingAdjustMod",
+ "Default | UpdateKey": "Nexus:1350",
+ "~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)'
+ },
+
+ "Fishing Tuner Redux": {
+ "ID": "HammurabiFishingTunerRedux",
+ "Default | UpdateKey": "Chucklefish:4578"
+ },
+
+ "Fixed Secret Woods Debris": {
+ "ID": "f4iTh.WoodsDebrisFix",
+ "Default | UpdateKey": "Nexus:1941"
+ },
+
+ "Fix Scythe Exp": {
+ "ID": "bcmpinc.FixScytheExp",
+ "~0.2 | Status": "AssumeBroken" // Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator.
+ },
+
+ "Flower Color Picker": {
+ "ID": "spacechase0.FlowerColorPicker",
+ "Default | UpdateKey": "Nexus:1229"
+ },
+
+ "Forage at the Farm": {
+ "ID": "Nishtra.ForageAtTheFarm",
+ "FormerIDs": "ForageAtTheFarm", // changed in <=1.6
+ "Default | UpdateKey": "Nexus:673"
+ },
+
+ "Furniture Anywhere": {
+ "ID": "Entoarox.FurnitureAnywhere",
+ "~1.1.5 | UpdateKey": "Chucklefish:4324" // only enable update checks up to 1.1.5 by request (has its own update-check feature)
+ },
+
+ "Game Reminder": {
+ "ID": "mmanlapat.GameReminder",
+ "Default | UpdateKey": "Nexus:1153"
+ },
+
+ "Gate Opener": {
+ "ID": "mralbobo.GateOpener",
+ "Default | UpdateKey": "GitHub:mralbobo/stardew-gate-opener"
+ },
+
+ "GenericShopExtender": {
+ "ID": "GenericShopExtender",
+ "Default | UpdateKey": "Nexus:814" // added in 0.1.3
+ },
+
+ "Geode Info Menu": {
+ "ID": "cat.geodeinfomenu",
+ "Default | UpdateKey": "Nexus:1448"
+ },
+
+ "Get Dressed": {
+ "ID": "Advize.GetDressed",
+ "Default | UpdateKey": "Nexus:331"
+ },
+
+ "Giant Crop Ring": {
+ "ID": "cat.giantcropring",
+ "Default | UpdateKey": "Nexus:1182"
+ },
+
+ "Gift Taste Helper": {
+ "ID": "tstaples.GiftTasteHelper",
+ "FormerIDs": "8008db57-fa67-4730-978e-34b37ef191d6", // changed in 2.5
+ "Default | UpdateKey": "Nexus:229"
+ },
+
+ "Grandfather's Gift": {
+ "ID": "ShadowDragon.GrandfathersGift",
+ "Default | UpdateKey": "Nexus:985"
+ },
+
+ "Happy Animals": {
+ "ID": "HappyAnimals",
+ "~1.0.3 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Happy Birthday (Omegasis)": {
+ "ID": "Omegasis.HappyBirthday",
+ "Default | UpdateKey": "Nexus:520" // added in 1.4.1
+ },
+
+ "Hardcore Mines": {
+ "ID": "kibbe.hardcore_mines",
+ "Default | UpdateKey": "Nexus:1674"
+ },
+
+ "Harp of Yoba Redux": {
+ "ID": "Platonymous.HarpOfYobaRedux",
+ "Default | UpdateKey": "Nexus:914" // added in 2.0.3
+ },
+
+ "Harvest Moon Witch Princess": {
+ "ID": "Sasara.WitchPrincess",
+ "Default | UpdateKey": "Nexus:1157"
+ },
+
+ "Harvest With Scythe": {
+ "ID": "965169fd-e1ed-47d0-9f12-b104535fb4bc",
+ "Default | UpdateKey": "Nexus:236"
+ },
+
+ "Horse Whistle (icepuente)": {
+ "ID": "icepuente.HorseWhistle",
+ "Default | UpdateKey": "Nexus:1131",
+ "~1.1.2-unofficial.1-pathoschild | Status": "AssumeBroken" // causes significant lag, fixed in unofficial.2
+ },
+
+ "Hunger (Yyeadude)": {
+ "ID": "HungerYyeadude",
+ "Default | UpdateKey": "Nexus:613"
+ },
+
+ "Hunger for Food (Tigerle)": {
+ "ID": "HungerForFoodByTigerle",
+ "Default | UpdateKey": "Nexus:810",
+ "~0.1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Hunger Mod (skn)": {
+ "ID": "skn.HungerMod",
+ "MapRemoteVersions": { "1.2.1": "1.0" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:1127"
+ },
+
+ "Idle Pause": {
+ "ID": "Veleek.IdlePause",
+ "MapRemoteVersions": { "1.2": "1.1" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:1092"
+ },
+
+ "Improved Quality of Life": {
+ "ID": "Demiacle.ImprovedQualityOfLife",
+ "Default | UpdateKey": "Nexus:1025",
+ "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Instant Geode": {
+ "ID": "InstantGeode",
+ "~1.12 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Instant Grow Trees": {
+ "ID": "cantorsdust.InstantGrowTrees",
+ "FormerIDs": "dc50c58b-c7d8-4e60-86cc-e27b5d95ee59 | community.InstantGrowTrees", // changed in 1.2 and 1.3.1
+ "Default | UpdateKey": "Nexus:173"
+ },
+
+ "Interaction Helper": {
+ "ID": "HammurabiInteractionHelper",
+ "Default | UpdateKey": "Chucklefish:4640" // added in 1.0.4-pathoschild-update
+ },
+
+ "Item Auto Stacker": {
+ "ID": "cat.autostacker",
+ "MapRemoteVersions": { "1.0.1": "1.0" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:1184"
+ },
+
+ "Json Assets": {
+ "ID": "spacechase0.JsonAssets",
+ "Default | UpdateKey": "Nexus:1720"
+ },
+
+ "Junimo Farm": {
+ "ID": "Platonymous.JunimoFarm",
+ "MapRemoteVersions": { "1.1.2": "1.1.1" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:984" // added in 1.1.3
+ },
+
+ "Less Strict Over-Exertion (AntiExhaustion)": {
+ "ID": "BALANCEMOD_AntiExhaustion",
+ "MapLocalVersions": { "0.0": "1.1" },
+ "Default | UpdateKey": "Nexus:637",
+ "~1.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Level Extender": {
+ "ID": "DevinLematty.LevelExtender",
+ "FormerIDs": "Devin Lematty.Level Extender", // changed in 1.3
+ "Default | UpdateKey": "Nexus:1471"
+ },
+
+ "Level Up Notifications": {
+ "ID": "Level Up Notifications",
+ "MapRemoteVersions": { "0.0.1a": "0.0.1" },
+ "Default | UpdateKey": "Nexus:855"
+ },
+
+ "Location and Music Logging": {
+ "ID": "Brandy Lover.LMlog",
+ "Default | UpdateKey": "Nexus:1366"
+ },
+
+ "Longevity": {
+ "ID": "RTGOAT.Longevity",
+ "MapRemoteVersions": { "1.6.8h": "1.6.8" },
+ "Default | UpdateKey": "Nexus:649"
+ },
+
+ "Lookup Anything": {
+ "ID": "Pathoschild.LookupAnything",
+ "FormerIDs": "LookupAnything", // changed in 1.10.1
+ "Default | UpdateKey": "Nexus:541",
+ "~1.18.1 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Love Bubbles": {
+ "ID": "LoveBubbles",
+ "Default | UpdateKey": "Nexus:1318"
+ },
+
+ "Loved Labels": {
+ "ID": "Advize.LovedLabels",
+ "Default | UpdateKey": "Nexus:279"
+ },
+
+ "Luck Skill": {
+ "ID": "spacechase0.LuckSkill",
+ "FormerIDs": "LuckSkill", // changed in 0.1.4
+ "Default | UpdateKey": "Nexus:521"
+ },
+
+ "Magic": {
+ "ID": "spacechase0.Magic",
+ "MapRemoteVersions": { "0.1.2": "0.1.1" } // not updated in manifest
+ },
+
+ "Mail Framework": {
+ "ID": "DIGUS.MailFrameworkMod",
+ "Default | UpdateKey": "Nexus:1536"
+ },
+
+ "MailOrderPigs": {
+ "ID": "jwdred.MailOrderPigs",
+ "Default | UpdateKey": "Nexus:632"
+ },
+
+ "Makeshift Multiplayer": {
+ "ID": "spacechase0.StardewValleyMP",
+ "FormerIDs": "StardewValleyMP", // changed in 0.3
+ "Default | UpdateKey": "Nexus:501"
+ },
+
+ "Map Image Exporter": {
+ "ID": "spacechase0.MapImageExporter",
+ "FormerIDs": "MapImageExporter", // changed in 1.0.2
+ "Default | UpdateKey": "Nexus:1073"
+ },
+
+ "Message Box [API]? (ChatMod)": {
+ "ID": "Kithio:ChatMod",
+ "Default | UpdateKey": "Chucklefish:4296",
+ "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Mining at the Farm": {
+ "ID": "Nishtra.MiningAtTheFarm",
+ "FormerIDs": "MiningAtTheFarm", // changed in <=1.7
+ "Default | UpdateKey": "Nexus:674"
+ },
+
+ "Mining With Explosives": {
+ "ID": "Nishtra.MiningWithExplosives",
+ "FormerIDs": "MiningWithExplosives", // changed in 1.1
+ "Default | UpdateKey": "Nexus:770"
+ },
+
+ "Modder Serialization Utility": {
+ "ID": "SerializerUtils-0-1",
+ "~ | Status": "Obsolete",
+ "~ | StatusReasonPhrase": "it's no longer maintained or used."
+ },
+
+ "Monster Level Tip": {
+ "ID": "WhiteMind.MonsterLT",
+ "Default | UpdateKey": "Nexus:1896"
+ },
+
+ "More Animals": {
+ "ID": "Entoarox.MoreAnimals",
+ "FormerIDs": "821ce8f6-e629-41ad-9fde-03b54f68b0b6MOREPETS | Entoarox.MorePets", // changed in 1.3 and 2.0
+ "~2.0.2 | UpdateKey": "Chucklefish:4288" // only enable update checks up to 2.0.2 by request (has its own update-check feature)
+ },
+
+ "More Artifact Spots": {
+ "ID": "451",
+ "Default | UpdateKey": "Nexus:451"
+ },
+
+ "More Map Layers": {
+ "ID": "Platonymous.MoreMapLayers",
+ "Default | UpdateKey": "Nexus:1134" // added in 1.1.1
+ },
+
+ "More Rain": {
+ "ID": "Omegasis.MoreRain",
+ "Default | UpdateKey": "Nexus:441", // added in 1.5.1
+ "~1.4 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "More Weapons": {
+ "ID": "Joco80.MoreWeapons",
+ "Default | UpdateKey": "Nexus:1168"
+ },
+
+ "Move Faster": {
+ "ID": "shuaiz.MoveFasterMod",
+ "Default | UpdateKey": "Nexus:1351",
+ "1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?)
+ },
+
+ "Multiple Sprites and Portraits On Rotation (File Loading)": {
+ "ID": "FileLoading",
+ "MapLocalVersions": { "1.1": "1.12" },
+ "Default | UpdateKey": "Nexus:1094",
+ "~1.12 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Museum Rearranger": {
+ "ID": "Omegasis.MuseumRearranger",
+ "Default | UpdateKey": "Nexus:428" // added in 1.4.1
+ },
+
+ "Mushroom Level Tip": {
+ "ID": "WhiteMind.MLT",
+ "Default | UpdateKey": "Nexus:1894"
+ },
+
+ "New Machines": {
+ "ID": "F70D4FAB-0AB2-4B78-9F1B-AF2CA2236A59",
+ "Default | UpdateKey": "Chucklefish:3683",
+ "~4.2.1343 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Night Owl": {
+ "ID": "Omegasis.NightOwl",
+ "MapLocalVersions": { "2.1": "1.3" }, // 1.3 had wrong version in manifest
+ "Default | UpdateKey": "Nexus:433" // added in 1.4.1
+ },
+
+ "No Crows": {
+ "ID": "cat.nocrows",
+ "Default | UpdateKey": "Nexus:1682"
+ },
+
+ "No Kids Ever": {
+ "ID": "Hangy.NoKidsEver",
+ "Default | UpdateKey": "Nexus:1464"
+ },
+
+ "No Debug Mode": {
+ "ID": "NoDebugMode",
+ "~ | Status": "Obsolete",
+ "~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0."
+ },
+
+ "No Fence Decay": {
+ "ID": "cat.nofencedecay",
+ "Default | UpdateKey": "Nexus:1180"
+ },
+
+ "No More Pets": {
+ "ID": "Omegasis.NoMorePets",
+ "FormerIDs": "NoMorePets", // changed in 1.4
+ "Default | UpdateKey": "Nexus:506" // added in 1.4.1
+ },
+
+ "No Rumble Horse": {
+ "ID": "Xangria.NoRumbleHorse",
+ "Default | UpdateKey": "Nexus:1779"
+ },
+
+ "No Soil Decay": {
+ "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610",
+ "Default | UpdateKey": "Nexus:237",
+ "~0.5 | Status": "AssumeBroken" // broke in SDV 1.2 and uses Assembly.GetExecutingAssembly().Location
+ },
+
+ "No Soil Decay Redux": {
+ "ID": "Platonymous.NoSoilDecayRedux",
+ "Default | UpdateKey": "Nexus:1084" // added in 1.1.9
+ },
+
+ "NPC Map Locations": {
+ "ID": "Bouhm.NPCMapLocations",
+ "FormerIDs": "NPCMapLocationsMod", // changed in 2.0
+ "Default | UpdateKey": "Nexus:239"
+ },
+
+ "Object Time Left": {
+ "ID": "spacechase0.ObjectTimeLeft",
+ "Default | UpdateKey": "Nexus:1315"
+ },
+
+ "OmniFarm": {
+ "ID": "PhthaloBlue.OmniFarm",
+ "FormerIDs": "BlueMod_OmniFarm", // changed in 2.0.2-pathoschild-update
+ "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_OmniFarm"
+ },
+
+ "One Click Shed": {
+ "ID": "BitwiseJonMods.OneClickShedReloader",
+ "Default | UpdateKey": "Nexus:2052"
+ },
+
+ "Out of Season Bonuses (Seasonal Items)": {
+ "ID": "midoriarmstrong.seasonalitems",
+ "Default | UpdateKey": "Nexus:1452"
+ },
+
+ "Part of the Community": {
+ "ID": "SB_PotC",
+ "Default | UpdateKey": "Nexus:923"
+ },
+
+ "PelicanFiber": {
+ "ID": "jwdred.PelicanFiber",
+ "Default | UpdateKey": "Nexus:631"
+ },
+
+ "PelicanTTS": {
+ "ID": "Platonymous.PelicanTTS",
+ "Default | UpdateKey": "Nexus:1079" // added in 1.6.1
+ },
+
+ "Persia the Mermaid - Standalone Custom NPC": {
+ "ID": "63b9f419-7449-42db-ab2e-440b4d05c073",
+ "Default | UpdateKey": "Nexus:1419"
+ },
+
+ "Persistent Game Options": {
+ "ID": "Xangria.PersistentGameOptions",
+ "Default | UpdateKey": "Nexus:1778"
+ },
+
+ "Plant on Grass": {
+ "ID": "Demiacle.PlantOnGrass",
+ "Default | UpdateKey": "Nexus:1026"
+ },
+
+ "PyTK - Platonymous Toolkit": {
+ "ID": "Platonymous.Toolkit",
+ "Default | UpdateKey": "Nexus:1726"
+ },
+
+ "Point-and-Plant": {
+ "ID": "jwdred.PointAndPlant",
+ "Default | UpdateKey": "Nexus:572",
+ "MapRemoteVersions": { "1.0.3": "1.0.2" } // manifest not updated
+ },
+
+ "Pony Weight Loss Program": {
+ "ID": "BadNetCode.PonyWeightLossProgram",
+ "Default | UpdateKey": "Nexus:1232"
+ },
+
+ "Portraiture": {
+ "ID": "Platonymous.Portraiture",
+ "Default | UpdateKey": "Nexus:999" // added in 1.3.1
+ },
+
+ "Prairie King Made Easy": {
+ "ID": "Mucchan.PrairieKingMadeEasy",
+ "Default | UpdateKey": "Chucklefish:3594",
+ "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Purchasable Recipes": {
+ "ID": "Paracosm.PurchasableRecipes",
+ "Default | UpdateKey": "Nexus:1722"
+ },
+
+ "Quest Delay": {
+ "ID": "BadNetCode.QuestDelay",
+ "Default | UpdateKey": "Nexus:1239"
+ },
+
+ "Recatch Legendary Fish": {
+ "ID": "cantorsdust.RecatchLegendaryFish",
+ "FormerIDs": "b3af8c31-48f0-43cf-8343-3eb08bcfa1f9 | community.RecatchLegendaryFish", // changed in 1.3 and 1.5.1
+ "Default | UpdateKey": "Nexus:172"
+ },
+
+ "Regeneration": {
+ "ID": "HammurabiRegeneration",
+ "Default | UpdateKey": "Chucklefish:4584"
+ },
+
+ "Relationship Bar UI": {
+ "ID": "RelationshipBar",
+ "Default | UpdateKey": "Nexus:1009"
+ },
+
+ "RelationshipsEnhanced": {
+ "ID": "relationshipsenhanced",
+ "Default | UpdateKey": "Chucklefish:4435",
+ "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Relationship Status": {
+ "ID": "relationshipstatus",
+ "MapRemoteVersions": { "1.0.5": "1.0.4" }, // not updated in manifest
+ "Default | UpdateKey": "Nexus:751",
+ "~1.0.5 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Rented Tools": {
+ "ID": "JarvieK.RentedTools",
+ "Default | UpdateKey": "Nexus:1307"
+ },
+
+ "Replanter": {
+ "ID": "jwdred.Replanter",
+ "Default | UpdateKey": "Nexus:589"
+ },
+
+ "ReRegeneration": {
+ "ID": "lrsk_sdvm_rerg.0925160827",
+ "MapLocalVersions": { "1.1.2-release": "1.1.2" },
+ "Default | UpdateKey": "Chucklefish:4465"
+ },
+
+ "Reseed": {
+ "ID": "Roc.Reseed",
+ "Default | UpdateKey": "Nexus:887"
+ },
+
+ "Reusable Wallpapers and Floors (Wallpaper Retain)": {
+ "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b",
+ "Default | UpdateKey": "Nexus:356",
+ "~1.5 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Ring of Fire": {
+ "ID": "Platonymous.RingOfFire",
+ "Default | UpdateKey": "Nexus:1166" // added in 1.0.1
+ },
+
+ "Rope Bridge": {
+ "ID": "RopeBridge",
+ "Default | UpdateKey": "Nexus:824"
+ },
+
+ "Rotate Toolbar": {
+ "ID": "Pathoschild.RotateToolbar",
+ "Default | UpdateKey": "Nexus:1100",
+ "~1.2.1 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Rush Orders": {
+ "ID": "spacechase0.RushOrders",
+ "FormerIDs": "RushOrders", // changed in 1.1
+ "Default | UpdateKey": "Nexus:605"
+ },
+
+ "Save Anywhere": {
+ "ID": "Omegasis.SaveAnywhere",
+ "Default | UpdateKey": "Nexus:444", // added in 2.6.1
+ "MapRemoteVersions": { "2.6.2": "2.6.1" } // not updated in manifest
+ },
+
+ "Save Backup": {
+ "ID": "Omegasis.SaveBackup",
+ "Default | UpdateKey": "Nexus:435", // added in 1.3.1
+ "~1.2 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Scroll to Blank": {
+ "ID": "caraxian.scroll.to.blank",
+ "Default | UpdateKey": "Chucklefish:4405"
+ },
+
+ "Scythe Harvesting": {
+ "ID": "mmanlapat.ScytheHarvesting",
+ "FormerIDs": "ScytheHarvesting", // changed in 1.6
+ "Default | UpdateKey": "Nexus:1106"
+ },
+
+ "SDV Twitch": {
+ "ID": "MTD.SDVTwitch",
+ "Default | UpdateKey": "Nexus:1760"
+ },
+
+ "Seasonal Immersion": {
+ "ID": "Entoarox.SeasonalImmersion",
+ "FormerIDs": "EntoaroxSeasonalHouse | EntoaroxSeasonalBuildings | EntoaroxSeasonalImmersion", // changed in 1.1, 1.6 or earlier, and 1.7
+ "~1.11 | UpdateKey": "Chucklefish:4262" // only enable update checks up to 1.11 by request (has its own update-check feature)
+ },
+
+ "Seed Bag": {
+ "ID": "Platonymous.SeedBag",
+ "Default | UpdateKey": "Nexus:1133" // added in 1.1.2
+ },
+
+ "Seed Catalogue": {
+ "ID": "spacechase0.SeedCatalogue",
+ "Default | UpdateKey": "Nexus:1640"
+ },
+
+ "Self Service": {
+ "ID": "JarvieK.SelfService",
+ "MapRemoteVersions": { "0.2.1": "0.2" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:1304"
+ },
+
+ "Send Items": {
+ "ID": "Denifia.SendItems",
+ "Default | UpdateKey": "Nexus:1087" // added in 1.0.3 (2017-10-04)
+ },
+
+ "Shed Notifications (BuildingsNotifications)": {
+ "ID": "TheCroak.BuildingsNotifications",
+ "Default | UpdateKey": "Nexus:620",
+ "~0.4.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Shenandoah Project": {
+ "ID": "Nishtra.ShenandoahProject",
+ "FormerIDs": "Shenandoah Project", // changed in 1.2
+ "MapRemoteVersions": { "1.1.1": "1.1" }, // not updated in manifest
+ "Default | UpdateKey": "Nexus:756"
+ },
+
+ "Ship Anywhere": {
+ "ID": "spacechase0.ShipAnywhere",
+ "Default | UpdateKey": "Nexus:1379"
+ },
+
+ "Shipment Tracker": {
+ "ID": "7e474181-e1a0-40f9-9c11-d08a3dcefaf3",
+ "Default | UpdateKey": "Nexus:321"
+ },
+
+ "Shop Expander": {
+ "ID": "Entoarox.ShopExpander",
+ "FormerIDs": "EntoaroxShopExpander", // changed in 1.5 and 1.5.2; disambiguate from Faster Paths
+ "MapRemoteVersions": { "1.6.0b": "1.6.0" },
+ "~1.6 | UpdateKey": "Chucklefish:4381" // only enable update checks up to 1.6 by request (has its own update-check feature)
+ },
+
+ "Showcase Mod": {
+ "ID": "Igorious.Showcase",
+ "MapLocalVersions": { "0.9-500": "0.9" },
+ "Default | UpdateKey": "Chucklefish:4487",
+ "~0.9 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Shroom Spotter": {
+ "ID": "TehPers.ShroomSpotter",
+ "Default | UpdateKey": "Nexus:908"
+ },
+
+ "Simple Crop Label": {
+ "ID": "SimpleCropLabel",
+ "Default | UpdateKey": "Nexus:314"
+ },
+
+ "Simple Sound Manager": {
+ "ID": "Omegasis.SimpleSoundManager",
+ "Default | UpdateKey": "Nexus:1410" // added in 1.0.1
+ },
+
+ "Simple Sprinklers": {
+ "ID": "tZed.SimpleSprinkler",
+ "Default | UpdateKey": "Nexus:76"
+ },
+
+ "Siv's Marriage Mod": {
+ "ID": "6266959802", // official version
+ "FormerIDs": "Siv.MarriageMod | medoli900.Siv's Marriage Mod", // 1.2.3-unofficial versions
+ "MapLocalVersions": { "0.0": "1.4" },
+ "Default | UpdateKey": "Nexus:366"
+ },
+
+ "Skill Prestige": {
+ "ID": "alphablackwolf.skillPrestige",
+ "FormerIDs": "6b843e60-c8fc-4a25-a67b-4a38ac8dcf9b", // changed circa 1.2.3
+ "Default | UpdateKey": "Nexus:569"
+ },
+
+ "Skill Prestige: Cooking Adapter": {
+ "ID": "Alphablackwolf.CookingSkillPrestigeAdapter",
+ "FormerIDs": "20d6b8a3-b6e7-460b-a6e4-07c2b0cb6c63", // changed circa 1.1
+ "MapRemoteVersions": { "1.2.3": "1.1" }, // manifest not updated
+ "Default | UpdateKey": "Nexus:569"
+ },
+
+ "Skip Intro": {
+ "ID": "Pathoschild.SkipIntro",
+ "FormerIDs": "SkipIntro", // changed in 1.4
+ "Default | UpdateKey": "Nexus:533",
+ "~1.7.2 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Skull Cavern Elevator": {
+ "ID": "SkullCavernElevator",
+ "Default | UpdateKey": "Nexus:963"
+ },
+
+ "Skull Cave Saver": {
+ "ID": "cantorsdust.SkullCaveSaver",
+ "FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2
+ "Default | UpdateKey": "Nexus:175",
+ "1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained
+ },
+
+ "Sleepy Eye": {
+ "ID": "spacechase0.SleepyEye",
+ "Default | UpdateKey": "Nexus:1152"
+ },
+
+ "Slower Fence Decay": {
+ "ID": "Speeder.SlowerFenceDecay",
+ "FormerIDs": "SPDSlowFenceDecay", // changed in 0.5.2-pathoschild-update
+ "Default | UpdateKey": "Nexus:252",
+ "~0.5.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Smart Mod": {
+ "ID": "KuroBear.SmartMod",
+ "~2.2 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Solar Eclipse Event": {
+ "ID": "KoihimeNakamura.SolarEclipseEvent",
+ "Default | UpdateKey": "Nexus:897",
+ "MapLocalVersions": { "1.3.1-20180131": "1.3.1" }
+ },
+
+ "SpaceCore": {
+ "ID": "spacechase0.SpaceCore",
+ "Default | UpdateKey": "Nexus:1348"
+ },
+
+ "Speedster": {
+ "ID": "Platonymous.Speedster",
+ "Default | UpdateKey": "Nexus:1102" // added in 1.3.1
+ },
+
+ "Split Screen": {
+ "ID": "Ilyaki.SplitScreen",
+ "~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals
+ },
+
+ "Sprinkler Range": {
+ "ID": "cat.sprinklerrange",
+ "Default | UpdateKey": "Nexus:1179"
+ },
+
+ "Sprinkles": {
+ "ID": "Platonymous.Sprinkles",
+ "Default | UpdateKey": "Chucklefish:4592"
+ },
+
+ "Sprint and Dash": {
+ "ID": "SPDSprintAndDash",
+ "Default | UpdateKey": "Nexus:235",
+ "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Sprint and Dash Redux": {
+ "ID": "littleraskol.SprintAndDashRedux",
+ "FormerIDs": "lrsk_sdvm_sndr.0921161059", // changed in 1.3
+ "Default | UpdateKey": "Chucklefish:4201"
+ },
+
+ "StackSplitX": {
+ "ID": "tstaples.StackSplitX",
+ "Default | UpdateKey": "Nexus:798"
+ },
+
+ "Stardew Config Menu": {
+ "ID": "Juice805.StardewConfigMenu",
+ "Default | UpdateKey": "Nexus:1312"
+ },
+
+ "Stardew Content Compatibility Layer (SCCL)": {
+ "ID": "SCCL",
+ "Default | UpdateKey": "Nexus:889",
+ "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Stardew Editor Game Integration": {
+ "ID": "spacechase0.StardewEditor.GameIntegration",
+ "Default | UpdateKey": "Nexus:1298"
+ },
+
+ "Stardew Notification": {
+ "ID": "stardewnotification",
+ "Default | UpdateKey": "GitHub:monopandora/StardewNotification"
+ },
+
+ "Stardew Symphony": {
+ "ID": "Omegasis.StardewSymphony",
+ "Default | UpdateKey": "Nexus:425" // added in 1.4.1
+ },
+
+ "StarDustCore": {
+ "ID": "StarDustCore",
+ "~ | Status": "Obsolete",
+ "~ | StatusReasonPhrase": "it was only used by earlier versions of Save Anywhere, and is no longer used or maintained."
+ },
+
+ "Starting Money": {
+ "ID": "mmanlapat.StartingMoney",
+ "FormerIDs": "StartingMoney", // changed in 1.1
+ "Default | UpdateKey": "Nexus:1138"
+ },
+
+ "StashItemsToChest": {
+ "ID": "BlueMod_StashItemsToChest",
+ "Default | UpdateKey": "GitHub:lambui/StardewValleyMod_StashItemsToChest",
+ "~1.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Stephan's Lots of Crops": {
+ "ID": "stephansstardewcrops",
+ "MapRemoteVersions": { "1.41": "1.1" }, // manifest not updated
+ "Default | UpdateKey": "Chucklefish:4314"
+ },
+
+ "Stumps to Hardwood Stumps": {
+ "ID": "StumpsToHardwoodStumps",
+ "Default | UpdateKey": "Nexus:691"
+ },
+
+ "Summit Reborn": {
+ "ID": "KoihimeNakamura.summitreborn",
+ "FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2
+ "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors)
+ },
+
+ "Super Greenhouse Warp Modifier": {
+ "ID": "SuperGreenhouse",
+ "Default | UpdateKey": "Chucklefish:4334",
+ "~1.0 | Status": "AssumeBroken" // broke in SMAPI 2.0
+ },
+
+ "Swim Almost Anywhere / Swim Suit": {
+ "ID": "Platonymous.SwimSuit",
+ "Default | UpdateKey": "Nexus:1215" // added in 0.5.1
+ },
+
+ "Tapper Ready": {
+ "ID": "skunkkk.TapperReady",
+ "Default | UpdateKey": "Nexus:1219"
+ },
+
+ "Teh's Fishing Overhaul": {
+ "ID": "TehPers.FishingOverhaul",
+ "Default | UpdateKey": "Nexus:866"
+ },
+
+ "Teleporter": {
+ "ID": "Teleporter",
+ "Default | UpdateKey": "Chucklefish:4374",
+ "~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "The Long Night": {
+ "ID": "Pathoschild.TheLongNight",
+ "Default | UpdateKey": "Nexus:1369",
+ "~1.1.1 | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "Three-heart Dance Partner": {
+ "ID": "ThreeHeartDancePartner",
+ "Default | UpdateKey": "Nexus:500"
+ },
+
+ "TimeFreeze": {
+ "ID": "Omegasis.TimeFreeze",
+ "FormerIDs": "4108e859-333c-4fec-a1a7-d2e18c1019fe", // changed in 1.2
+ "Default | UpdateKey": "Nexus:973" // added in 1.2.1
+ },
+
+ "Time Reminder": {
+ "ID": "KoihimeNakamura.TimeReminder",
+ "MapLocalVersions": { "1.0-20170314": "1.0.2" },
+ "Default | UpdateKey": "Nexus:1000"
+ },
+
+ "TimeSpeed": {
+ "ID": "cantorsdust.TimeSpeed",
+ "FormerIDs": "community.TimeSpeed", // changed in 2.3.3
+ "Default | UpdateKey": "Nexus:169"
+ },
+
+ "To Do List": {
+ "ID": "eleanor.todolist",
+ "Default | UpdateKey": "Nexus:1630"
+ },
+
+ "Tool Charging": {
+ "ID": "mralbobo.ToolCharging",
+ "Default | UpdateKey": "GitHub:mralbobo/stardew-tool-charging"
+ },
+
+ "TractorMod": {
+ "ID": "Pathoschild.TractorMod",
+ "FormerIDs": "BlueMod_TractorMod | PhthaloBlue.TractorMod | community.TractorMod", // changed in 3.2, 4.0 beta, and 4.0
+ "Default | UpdateKey": "Nexus:1401",
+ "~4.5-beta | Status": "AssumeBroken" // broke in SDV 1.3
+ },
+
+ "TrainerMod": {
+ "ID": "SMAPI.TrainerMod",
+ "~ | Status": "Obsolete",
+ "~ | StatusReasonPhrase": "replaced by ConsoleCommands, which is added by the SMAPI installer."
+ },
+
+ "Tree Transplant": {
+ "ID": "TreeTransplant",
+ "Default | UpdateKey": "Nexus:1342"
+ },
+
+ "UI Info Suite": {
+ "ID": "Cdaragorn.UiInfoSuite",
+ "Default | UpdateKey": "Nexus:1150"
+ },
+
+ "UiModSuite": {
+ "ID": "Demiacle.UiModSuite",
+ "MapLocalVersions": { "0.5": "1.0" }, // not updated in manifest
+ "Default | UpdateKey": "Nexus:1023",
+ "~1.0 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Variable Grass": {
+ "ID": "dantheman999.VariableGrass",
+ "Default | UpdateKey": "GitHub:dantheman999301/StardewMods"
+ },
+
+ "Vertical Toolbar": {
+ "ID": "SB_VerticalToolMenu",
+ "Default | UpdateKey": "Nexus:943"
+ },
+
+ "WarpAnimals": {
+ "ID": "Symen.WarpAnimals",
+ "Default | UpdateKey": "Nexus:1400"
+ },
+
+ "What Farm Cave / WhatAMush": {
+ "ID": "WhatAMush",
+ "Default | UpdateKey": "Nexus:1097"
+ },
+
+ "WHats Up": {
+ "ID": "wHatsUp",
+ "Default | UpdateKey": "Nexus:1082"
+ },
+
+ "Winter Grass": {
+ "ID": "cat.wintergrass",
+ "Default | UpdateKey": "Nexus:1601"
+ },
+
+ "Xnb Loader": {
+ "ID": "Entoarox.XnbLoader",
+ "~1.1.10 | UpdateKey": "Chucklefish:4506" // only enable update checks up to 1.1.10 by request (has its own update-check feature)
+ },
+
+ "zDailyIncrease": {
+ "ID": "zdailyincrease",
+ "MapRemoteVersions": { "1.3.5": "1.3.4" }, // not updated in manifest
+ "Default | UpdateKey": "Chucklefish:4247"
+ },
+
+ "Zoom Out Extreme": {
+ "ID": "RockinMods.ZoomMod",
+ "FormerIDs": "ZoomMod", // changed circa 1.2.1
+ "Default | UpdateKey": "Nexus:1326",
+ "~0.1 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Zoryn's Better RNG": {
+ "ID": "Zoryn.BetterRNG",
+ "FormerIDs": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods",
+ "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Zoryn's Calendar Anywhere": {
+ "ID": "Zoryn.CalendarAnywhere",
+ "FormerIDs": "a41c01cd-0437-43eb-944f-78cb5a53002a", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods",
+ "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Zoryn's Durable Fences": {
+ "ID": "Zoryn.DurableFences",
+ "FormerIDs": "56d3439c-7b9b-497e-9496-0c4890e8a00e", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods"
+ },
+
+ "Zoryn's Health Bars": {
+ "ID": "Zoryn.HealthBars",
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods",
+ "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Zoryn's Fishing Mod": {
+ "ID": "Zoryn.FishingMod",
+ "FormerIDs": "fa277b1f-265e-47c3-a84f-cd320cc74949", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods"
+ },
+
+ "Zoryn's Junimo Deposit Anywhere": {
+ "ID": "Zoryn.JunimoDepositAnywhere",
+ "FormerIDs": "f93a4fe8-cade-4146-9335-b5f82fbbf7bc", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods",
+ "~1.7 | Status": "AssumeBroken" // broke in SDV 1.2
+ },
+
+ "Zoryn's Movement Mod": {
+ "ID": "Zoryn.MovementModifier",
+ "FormerIDs": "8a632929-8335-484f-87dd-c29d2ba3215d", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods"
+ },
+
+ "Zoryn's Regen Mod": {
+ "ID": "Zoryn.RegenMod",
+ "FormerIDs": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", // changed in 1.6
+ "Default | UpdateKey": "GitHub:Zoryn4163/SMAPI-Mods",
+ "~1.6 | Status": "AssumeBroken" // broke in SDV 1.2
+ }
+ }
+}