summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj2
-rw-r--r--src/SMAPI.ModBuildConfig/DeployModTask.cs27
-rw-r--r--src/SMAPI.Tests/SMAPI.Tests.csproj2
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs17
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs17
-rw-r--r--src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs4
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj2
-rw-r--r--src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs5
-rw-r--r--src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs33
-rw-r--r--src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs37
-rw-r--r--src/SMAPI.Web.LegacyRedirects/Program.cs23
-rw-r--r--src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json29
-rw-r--r--src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj21
-rw-r--r--src/SMAPI.Web/BackgroundService.cs5
-rw-r--r--src/SMAPI.Web/Controllers/JsonValidatorController.cs32
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs6
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs2
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs89
-rw-r--r--src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs (renamed from src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs)19
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs2
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs54
-rw-r--r--src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs (renamed from src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs)30
-rw-r--r--src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs3
-rw-r--r--src/SMAPI.Web/Framework/Compression/GzipHelper.cs2
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs25
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs18
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs15
-rw-r--r--src/SMAPI.Web/Framework/Extensions.cs22
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs6
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs54
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs58
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs54
-rw-r--r--src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs47
-rw-r--r--src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs62
-rw-r--r--src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs57
-rw-r--r--src/SMAPI.Web/Program.cs14
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj23
-rw-r--r--src/SMAPI.Web/Startup.cs220
-rw-r--r--src/SMAPI.Web/ViewModels/ModListModel.cs2
-rw-r--r--src/SMAPI.Web/ViewModels/ModModel.cs4
-rw-r--r--src/SMAPI.Web/Views/Index/Index.cshtml2
-rw-r--r--src/SMAPI.Web/Views/JsonValidator/Index.cshtml4
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml18
-rw-r--r--src/SMAPI.Web/Views/Mods/Index.cshtml34
-rw-r--r--src/SMAPI.Web/Views/Shared/_Layout.cshtml2
-rw-r--r--src/SMAPI.Web/appsettings.Development.json3
-rw-r--r--src/SMAPI.Web/appsettings.json3
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/mods.css4
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/mods.js2
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json4
-rw-r--r--src/SMAPI.sln6
-rw-r--r--src/SMAPI/Framework/IModMetadata.cs6
-rw-r--r--src/SMAPI/Framework/Input/GamePadStateBuilder.cs17
-rw-r--r--src/SMAPI/Framework/Input/MouseStateBuilder.cs23
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs1
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs11
-rw-r--r--src/SMAPI/Framework/Networking/SGalaxyNetServer.cs29
-rw-r--r--src/SMAPI/Framework/Networking/SLidgrenServer.cs31
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs2
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs2
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs2
-rw-r--r--src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs2
-rw-r--r--src/SMAPI/Framework/Rendering/SDisplayDevice.cs13
-rw-r--r--src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs130
-rw-r--r--src/SMAPI/Framework/SGame.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/ColorConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/PointConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/RectangleConverter.cs2
-rw-r--r--src/SMAPI/Framework/Serialization/Vector2Converter.cs2
70 files changed, 870 insertions, 637 deletions
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
index e2be66d9..5ae6574d 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
<PrivateAssets>all</PrivateAssets>
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
index 96d95e06..ced05a28 100644
--- a/src/SMAPI.ModBuildConfig/DeployModTask.cs
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -153,23 +153,22 @@ namespace StardewModdingAPI.ModBuildConfig
// create zip file
Directory.CreateDirectory(outputFolderPath);
- using (Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write))
- using (ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create))
+ using Stream zipStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write);
+ using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Create);
+
+ foreach (var fileEntry in files)
{
- foreach (var fileEntry in files)
- {
- string relativePath = fileEntry.Key;
- FileInfo file = fileEntry.Value;
+ string relativePath = fileEntry.Key;
+ FileInfo file = fileEntry.Value;
- // get file info
- string filePath = file.FullName;
- string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
+ // get file info
+ string filePath = file.FullName;
+ string entryName = folderName + '/' + relativePath.Replace(Path.DirectorySeparatorChar, '/');
- // add to zip
- using (Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
- using (Stream fileStreamInZip = archive.CreateEntry(entryName).Open())
- fileStream.CopyTo(fileStreamInZip);
- }
+ // add to zip
+ using Stream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ using Stream fileStreamInZip = archive.CreateEntry(entryName).Open();
+ fileStream.CopyTo(fileStreamInZip);
}
}
diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj
index 639c22a4..b1548e3a 100644
--- a/src/SMAPI.Tests/SMAPI.Tests.csproj
+++ b/src/SMAPI.Tests/SMAPI.Tests.csproj
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Moq" Version="4.13.1" />
+ <PackageReference Include="Moq" Version="4.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NUnit" Version="3.12.0" />
</ItemGroup>
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
index f0a7c82a..2fb6ed20 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs
@@ -62,16 +62,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
private TResult Post<TBody, TResult>(string url, TBody content)
{
// note: avoid HttpClient for Mac compatibility
- using (WebClient client = new WebClient())
- {
- Uri fullUrl = new Uri(this.BaseUrl, url);
- string data = JsonConvert.SerializeObject(content);
+ using WebClient client = new WebClient();
- client.Headers["Content-Type"] = "application/json";
- client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
- string response = client.UploadString(fullUrl, data);
- return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
- }
+ Uri fullUrl = new Uri(this.BaseUrl, url);
+ string data = JsonConvert.SerializeObject(content);
+
+ client.Headers["Content-Type"] = "application/json";
+ client.Headers["User-Agent"] = $"SMAPI/{this.Version}";
+ string response = client.UploadString(fullUrl, data);
+ return JsonConvert.DeserializeObject<TResult>(response, this.JsonSettings);
}
}
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
index a1d2dfae..5cdf489f 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiCompatibilityStatus.cs
@@ -3,25 +3,28 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
/// <summary>The compatibility status for a mod.</summary>
public enum WikiCompatibilityStatus
{
+ /// <summary>The status is unknown.</summary>
+ Unknown,
+
/// <summary>The mod is compatible.</summary>
- Ok = 0,
+ Ok,
/// <summary>The mod is compatible if you use an optional official download.</summary>
- Optional = 1,
+ Optional,
/// <summary>The mod is compatible if you use an unofficial update.</summary>
- Unofficial = 2,
+ Unofficial,
/// <summary>The mod isn't compatible, but the player can fix it or there's a good alternative.</summary>
- Workaround = 3,
+ Workaround,
/// <summary>The mod isn't compatible.</summary>
- Broken = 4,
+ Broken,
/// <summary>The mod is no longer maintained by the author, and an unofficial update or continuation is unlikely.</summary>
- Abandoned = 5,
+ Abandoned,
/// <summary>The mod is no longer needed and should be removed.</summary>
- Obsolete = 6
+ Obsolete
}
}
diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
index 212c70ef..4eec3424 100644
--- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs
@@ -124,8 +124,8 @@ namespace StardewModdingAPI.Toolkit.Framework.GameScanning
XElement root;
try
{
- using (FileStream stream = file.OpenRead())
- root = XElement.Load(stream);
+ using FileStream stream = file.OpenRead();
+ root = XElement.Load(stream);
}
catch
{
diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
index edb1d612..4e6918ad 100644
--- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -14,7 +14,7 @@
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" />
<PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT' AND '$(TargetFramework)' == 'netstandard2.0'" />
</ItemGroup>
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
index c45448f3..1e490448 100644
--- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -30,10 +30,7 @@ namespace StardewModdingAPI.Toolkit.Utilities
/// <summary>Detect the current OS.</summary>
public static Platform DetectPlatform()
{
- if (EnvironmentUtility.CachedPlatform == null)
- EnvironmentUtility.CachedPlatform = EnvironmentUtility.DetectPlatformImpl();
-
- return EnvironmentUtility.CachedPlatform.Value;
+ return EnvironmentUtility.CachedPlatform ??= EnvironmentUtility.DetectPlatformImpl();
}
diff --git a/src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs b/src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs
deleted file mode 100644
index 44ed0b6b..00000000
--- a/src/SMAPI.Web.LegacyRedirects/Controllers/ModsApiController.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-using Pathoschild.Http.Client;
-using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
-
-namespace SMAPI.Web.LegacyRedirects.Controllers
-{
- /// <summary>Provides an API to perform mod update checks.</summary>
- [ApiController]
- [Produces("application/json")]
- [Route("api/v{version}/mods")]
- public class ModsApiController : Controller
- {
- /*********
- ** Public methods
- *********/
- /// <summary>Fetch version metadata for the given mods.</summary>
- /// <param name="model">The mod search criteria.</param>
- [HttpPost]
- public async Task<IEnumerable<ModEntryModel>> PostAsync([FromBody] ModSearchModel model)
- {
- using IClient client = new FluentClient("https://smapi.io/api");
-
- Startup.ConfigureJsonNet(client.Formatters.JsonFormatter.SerializerSettings);
-
- return await client
- .PostAsync(this.Request.Path)
- .WithBody(model)
- .AsArray<ModEntryModel>();
- }
- }
-}
diff --git a/src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs b/src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs
deleted file mode 100644
index e5138e5c..00000000
--- a/src/SMAPI.Web.LegacyRedirects/Framework/LambdaRewriteRule.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Rewrite;
-
-namespace SMAPI.Web.LegacyRedirects.Framework
-{
- /// <summary>Rewrite requests to prepend the subdomain portion (if any) to the path.</summary>
- /// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" />.</remarks>
- internal class LambdaRewriteRule : IRule
- {
- /*********
- ** Accessors
- *********/
- /// <summary>Rewrite an HTTP request if needed.</summary>
- private readonly Action<RewriteContext, HttpRequest, HttpResponse> Rewrite;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="rewrite">Rewrite an HTTP request if needed.</param>
- public LambdaRewriteRule(Action<RewriteContext, HttpRequest, HttpResponse> rewrite)
- {
- this.Rewrite = rewrite ?? throw new ArgumentNullException(nameof(rewrite));
- }
-
- /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
- /// <param name="context">The rewrite context.</param>
- public void ApplyRule(RewriteContext context)
- {
- HttpRequest request = context.HttpContext.Request;
- HttpResponse response = context.HttpContext.Response;
- this.Rewrite(context, request, response);
- }
- }
-}
diff --git a/src/SMAPI.Web.LegacyRedirects/Program.cs b/src/SMAPI.Web.LegacyRedirects/Program.cs
deleted file mode 100644
index 6adee877..00000000
--- a/src/SMAPI.Web.LegacyRedirects/Program.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-
-namespace SMAPI.Web.LegacyRedirects
-{
- /// <summary>The main app entry point.</summary>
- public class Program
- {
- /*********
- ** Public methods
- *********/
- /// <summary>The main app entry point.</summary>
- /// <param name="args">The command-line arguments.</param>
- public static void Main(string[] args)
- {
- Host
- .CreateDefaultBuilder(args)
- .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
- .Build()
- .Run();
- }
- }
-}
diff --git a/src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json b/src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json
deleted file mode 100644
index e9a1b210..00000000
--- a/src/SMAPI.Web.LegacyRedirects/Properties/launchSettings.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:52756",
- "sslPort": 0
- }
- },
- "$schema": "http://json.schemastore.org/launchsettings.json",
- "profiles": {
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "SMAPI.Web.LegacyRedirects": {
- "commandName": "Project",
- "launchBrowser": true,
- "launchUrl": "/",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- },
- "applicationUrl": "https://localhost:5001;http://localhost:5000"
- }
- }
-} \ No newline at end of file
diff --git a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj b/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj
deleted file mode 100644
index 36831961..00000000
--- a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj
+++ /dev/null
@@ -1,21 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk.Web">
-
- <PropertyGroup>
- <TargetFramework>netcoreapp3.0</TargetFramework>
- </PropertyGroup>
-
- <ItemGroup>
- <Content Remove="aws-beanstalk-tools-defaults.json" />
- </ItemGroup>
-
- <ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
- </ItemGroup>
-
- <ItemGroup>
- <ProjectReference Include="..\SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj" />
- <ProjectReference Include="..\SMAPI.Toolkit\SMAPI.Toolkit.csproj" />
- </ItemGroup>
-
-</Project>
diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs
index ee7a60f3..275622fe 100644
--- a/src/SMAPI.Web/BackgroundService.cs
+++ b/src/SMAPI.Web/BackgroundService.cs
@@ -1,4 +1,5 @@
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Hangfire;
@@ -36,7 +37,9 @@ namespace StardewModdingAPI.Web
/// <summary>Construct an instance.</summary>
/// <param name="wikiCache">The cache in which to store wiki metadata.</param>
/// <param name="modCache">The cache in which to store mod data.</param>
- public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache)
+ /// <param name="hangfireStorage">The Hangfire storage implementation.</param>
+ [SuppressMessage("ReSharper", "UnusedParameter.Local", Justification = "The Hangfire reference forces it to initialize first, since it's needed by the background service.")]
+ public BackgroundService(IWikiCacheRepository wikiCache, IModCacheRepository modCache, JobStorage hangfireStorage)
{
BackgroundService.WikiCache = wikiCache;
BackgroundService.ModCache = modCache;
diff --git a/src/SMAPI.Web/Controllers/JsonValidatorController.cs b/src/SMAPI.Web/Controllers/JsonValidatorController.cs
index 2ade3e3d..c43fb929 100644
--- a/src/SMAPI.Web/Controllers/JsonValidatorController.cs
+++ b/src/SMAPI.Web/Controllers/JsonValidatorController.cs
@@ -275,21 +275,20 @@ namespace StardewModdingAPI.Web.Controllers
errors = new Dictionary<string, string>(errors, StringComparer.InvariantCultureIgnoreCase);
// match error by type and message
- foreach (var pair in errors)
+ foreach ((string target, string errorMessage) in errors)
{
- if (!pair.Key.Contains(":"))
+ if (!target.Contains(":"))
continue;
- string[] parts = pair.Key.Split(':', 2);
+ string[] parts = target.Split(':', 2);
if (parts[0].Equals(error.ErrorType.ToString(), StringComparison.InvariantCultureIgnoreCase) && Regex.IsMatch(error.Message, parts[1]))
- return pair.Value?.Trim();
+ return errorMessage?.Trim();
}
// match by type
- if (errors.TryGetValue(error.ErrorType.ToString(), out string message))
- return message?.Trim();
-
- return null;
+ return errors.TryGetValue(error.ErrorType.ToString(), out string message)
+ ? message?.Trim()
+ : null;
}
return GetRawOverrideError()
@@ -304,10 +303,10 @@ namespace StardewModdingAPI.Web.Controllers
{
if (schema.ExtensionData != null)
{
- foreach (var pair in schema.ExtensionData)
+ foreach ((string curKey, JToken value) in schema.ExtensionData)
{
- if (pair.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase))
- return pair.Value.ToObject<T>();
+ if (curKey.Equals(key, StringComparison.InvariantCultureIgnoreCase))
+ return value.ToObject<T>();
}
}
@@ -318,14 +317,11 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="value">The value to format.</param>
private string FormatValue(object value)
{
- switch (value)
+ return value switch
{
- case List<string> list:
- return string.Join(", ", list);
-
- default:
- return value?.ToString() ?? "null";
- }
+ List<string> list => string.Join(", ", list),
+ _ => value?.ToString() ?? "null"
+ };
}
}
}
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 06768f03..6032186f 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -61,7 +61,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="github">The GitHub API client.</param>
/// <param name="modDrop">The ModDrop API client.</param>
/// <param name="nexus">The Nexus API client.</param>
- public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
+ public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
{
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
@@ -388,9 +388,9 @@ namespace StardewModdingAPI.Web.Controllers
if (map.ContainsKey(parsed.ToString()))
return map[parsed.ToString()];
- foreach (var pair in map)
+ foreach ((string fromRaw, string toRaw) in map)
{
- if (SemanticVersion.TryParse(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, allowNonStandard, out ISemanticVersion newVersion))
+ if (SemanticVersion.TryParse(fromRaw, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(toRaw, allowNonStandard, out ISemanticVersion newVersion))
return newVersion.ToString();
}
}
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
index bcec8b36..08749f3b 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/IModCacheRepository.cs
@@ -4,7 +4,7 @@ using StardewModdingAPI.Web.Framework.ModRepositories;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
{
- /// <summary>Encapsulates logic for accessing the mod data cache.</summary>
+ /// <summary>Manages cached mod data.</summary>
internal interface IModCacheRepository : ICacheRepository
{
/*********
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
new file mode 100644
index 00000000..9c5a217e
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMemoryRepository.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.ModRepositories;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Mods
+{
+ /// <summary>Manages cached mod data in-memory.</summary>
+ internal class ModCacheMemoryRepository : BaseCacheRepository, IModCacheRepository
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The cached mod data indexed by <c>{site key}:{ID}</c>.</summary>
+ private readonly IDictionary<string, CachedMod> Mods = new Dictionary<string, CachedMod>(StringComparer.InvariantCultureIgnoreCase);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get the cached mod data.</summary>
+ /// <param name="site">The mod site to search.</param>
+ /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
+ /// <param name="mod">The fetched mod.</param>
+ /// <param name="markRequested">Whether to update the mod's 'last requested' date.</param>
+ public bool TryGetMod(ModRepositoryKey site, string id, out CachedMod mod, bool markRequested = true)
+ {
+ // get mod
+ if (!this.Mods.TryGetValue(this.GetKey(site, id), out mod))
+ return false;
+
+ // bump 'last requested'
+ if (markRequested)
+ {
+ mod.LastRequested = DateTimeOffset.UtcNow;
+ mod = this.SaveMod(mod);
+ }
+
+ return true;
+ }
+
+ /// <summary>Save data fetched for a mod.</summary>
+ /// <param name="site">The mod site on which the mod is found.</param>
+ /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param>
+ /// <param name="mod">The mod data.</param>
+ /// <param name="cachedMod">The stored mod record.</param>
+ public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod)
+ {
+ string key = this.GetKey(site, id);
+ cachedMod = this.SaveMod(new CachedMod(site, id, mod));
+ }
+
+ /// <summary>Delete data for mods which haven't been requested within a given time limit.</summary>
+ /// <param name="age">The minimum age for which to remove mods.</param>
+ public void RemoveStaleMods(TimeSpan age)
+ {
+ DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age);
+
+ string[] staleKeys = this.Mods
+ .Where(p => p.Value.LastRequested < minDate)
+ .Select(p => p.Key)
+ .ToArray();
+
+ foreach (string key in staleKeys)
+ this.Mods.Remove(key);
+ }
+
+ /// <summary>Save data fetched for a mod.</summary>
+ /// <param name="mod">The mod data.</param>
+ public CachedMod SaveMod(CachedMod mod)
+ {
+ string key = this.GetKey(mod.Site, mod.ID);
+ return this.Mods[key] = mod;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a cache key.</summary>
+ /// <param name="site">The mod site.</param>
+ /// <param name="id">The mod ID.</param>
+ public string GetKey(ModRepositoryKey site, string id)
+ {
+ return $"{site}:{id.Trim()}".ToLower();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs
index 2e7804a7..f105baab 100644
--- a/src/SMAPI.Web/Framework/Caching/Mods/ModCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Mods/ModCacheMongoRepository.cs
@@ -5,8 +5,8 @@ using StardewModdingAPI.Web.Framework.ModRepositories;
namespace StardewModdingAPI.Web.Framework.Caching.Mods
{
- /// <summary>Encapsulates logic for accessing the mod data cache.</summary>
- internal class ModCacheRepository : BaseCacheRepository, IModCacheRepository
+ /// <summary>Manages cached mod data in MongoDB.</summary>
+ internal class ModCacheMongoRepository : BaseCacheRepository, IModCacheRepository
{
/*********
** Fields
@@ -20,7 +20,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="database">The authenticated MongoDB database.</param>
- public ModCacheRepository(IMongoDatabase database)
+ public ModCacheMongoRepository(IMongoDatabase database)
{
// get collections
this.Mods = database.GetCollection<CachedMod>("mods");
@@ -29,6 +29,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
this.Mods.Indexes.CreateOne(new CreateIndexModel<CachedMod>(Builders<CachedMod>.IndexKeys.Ascending(p => p.ID).Ascending(p => p.Site)));
}
+
/*********
** Public methods
*********/
@@ -72,13 +73,9 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
public void RemoveStaleMods(TimeSpan age)
{
DateTimeOffset minDate = DateTimeOffset.UtcNow.Subtract(age);
- var result = this.Mods.DeleteMany(p => p.LastRequested < minDate);
+ this.Mods.DeleteMany(p => p.LastRequested < minDate);
}
-
- /*********
- ** Private methods
- *********/
/// <summary>Save data fetched for a mod.</summary>
/// <param name="mod">The mod data.</param>
public CachedMod SaveMod(CachedMod mod)
@@ -88,12 +85,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
this.Mods.ReplaceOne(
entry => entry.ID == id && entry.Site == mod.Site,
mod,
- new UpdateOptions { IsUpsert = true }
+ new ReplaceOptions { IsUpsert = true }
);
return mod;
}
+
+ /*********
+ ** Private methods
+ *********/
/// <summary>Normalize a mod ID for case-insensitive search.</summary>
/// <param name="id">The mod ID.</param>
public string NormalizeId(string id)
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
index b54c8a2f..02097f52 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/IWikiCacheRepository.cs
@@ -5,7 +5,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
{
- /// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
+ /// <summary>Manages cached wiki data.</summary>
internal interface IWikiCacheRepository : ICacheRepository
{
/*********
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
new file mode 100644
index 00000000..4621f5e3
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMemoryRepository.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
+
+namespace StardewModdingAPI.Web.Framework.Caching.Wiki
+{
+ /// <summary>Manages cached wiki data in-memory.</summary>
+ internal class WikiCacheMemoryRepository : BaseCacheRepository, IWikiCacheRepository
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The saved wiki metadata.</summary>
+ private CachedWikiMetadata Metadata;
+
+ /// <summary>The cached wiki data.</summary>
+ private CachedWikiMod[] Mods = new CachedWikiMod[0];
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Get the cached wiki metadata.</summary>
+ /// <param name="metadata">The fetched metadata.</param>
+ public bool TryGetWikiMetadata(out CachedWikiMetadata metadata)
+ {
+ metadata = this.Metadata;
+ return metadata != null;
+ }
+
+ /// <summary>Get the cached wiki mods.</summary>
+ /// <param name="filter">A filter to apply, if any.</param>
+ public IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null)
+ {
+ return filter != null
+ ? this.Mods.Where(filter.Compile())
+ : this.Mods.ToArray();
+ }
+
+ /// <summary>Save data fetched from the wiki compatibility list.</summary>
+ /// <param name="stableVersion">The current stable Stardew Valley version.</param>
+ /// <param name="betaVersion">The current beta Stardew Valley version.</param>
+ /// <param name="mods">The mod data.</param>
+ /// <param name="cachedMetadata">The stored metadata record.</param>
+ /// <param name="cachedMods">The stored mod records.</param>
+ public void SaveWikiData(string stableVersion, string betaVersion, IEnumerable<WikiModEntry> mods, out CachedWikiMetadata cachedMetadata, out CachedWikiMod[] cachedMods)
+ {
+ this.Metadata = cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion);
+ this.Mods = cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs
index 1ae9d38f..07e7c721 100644
--- a/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheRepository.cs
+++ b/src/SMAPI.Web/Framework/Caching/Wiki/WikiCacheMongoRepository.cs
@@ -7,17 +7,17 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
namespace StardewModdingAPI.Web.Framework.Caching.Wiki
{
- /// <summary>Encapsulates logic for accessing the wiki data cache.</summary>
- internal class WikiCacheRepository : BaseCacheRepository, IWikiCacheRepository
+ /// <summary>Manages cached wiki data in MongoDB.</summary>
+ internal class WikiCacheMongoRepository : BaseCacheRepository, IWikiCacheRepository
{
/*********
** Fields
*********/
/// <summary>The collection for wiki metadata.</summary>
- private readonly IMongoCollection<CachedWikiMetadata> WikiMetadata;
+ private readonly IMongoCollection<CachedWikiMetadata> Metadata;
/// <summary>The collection for wiki mod data.</summary>
- private readonly IMongoCollection<CachedWikiMod> WikiMods;
+ private readonly IMongoCollection<CachedWikiMod> Mods;
/*********
@@ -25,21 +25,21 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
*********/
/// <summary>Construct an instance.</summary>
/// <param name="database">The authenticated MongoDB database.</param>
- public WikiCacheRepository(IMongoDatabase database)
+ public WikiCacheMongoRepository(IMongoDatabase database)
{
// get collections
- this.WikiMetadata = database.GetCollection<CachedWikiMetadata>("wiki-metadata");
- this.WikiMods = database.GetCollection<CachedWikiMod>("wiki-mods");
+ this.Metadata = database.GetCollection<CachedWikiMetadata>("wiki-metadata");
+ this.Mods = database.GetCollection<CachedWikiMod>("wiki-mods");
// add indexes if needed
- this.WikiMods.Indexes.CreateOne(new CreateIndexModel<CachedWikiMod>(Builders<CachedWikiMod>.IndexKeys.Ascending(p => p.ID)));
+ this.Mods.Indexes.CreateOne(new CreateIndexModel<CachedWikiMod>(Builders<CachedWikiMod>.IndexKeys.Ascending(p => p.ID)));
}
/// <summary>Get the cached wiki metadata.</summary>
/// <param name="metadata">The fetched metadata.</param>
public bool TryGetWikiMetadata(out CachedWikiMetadata metadata)
{
- metadata = this.WikiMetadata.Find("{}").FirstOrDefault();
+ metadata = this.Metadata.Find("{}").FirstOrDefault();
return metadata != null;
}
@@ -48,8 +48,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
public IEnumerable<CachedWikiMod> GetWikiMods(Expression<Func<CachedWikiMod, bool>> filter = null)
{
return filter != null
- ? this.WikiMods.Find(filter).ToList()
- : this.WikiMods.Find("{}").ToList();
+ ? this.Mods.Find(filter).ToList()
+ : this.Mods.Find("{}").ToList();
}
/// <summary>Save data fetched from the wiki compatibility list.</summary>
@@ -63,11 +63,11 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki
cachedMetadata = new CachedWikiMetadata(stableVersion, betaVersion);
cachedMods = mods.Select(mod => new CachedWikiMod(mod)).ToArray();
- this.WikiMods.DeleteMany("{}");
- this.WikiMods.InsertMany(cachedMods);
+ this.Mods.DeleteMany("{}");
+ this.Mods.InsertMany(cachedMods);
- this.WikiMetadata.DeleteMany("{}");
- this.WikiMetadata.InsertOne(cachedMetadata);
+ this.Metadata.DeleteMany("{}");
+ this.Metadata.InsertOne(cachedMetadata);
}
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
index 140b854e..a6fd21fd 100644
--- a/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/CurseForge/CurseForgeClient.cs
@@ -57,8 +57,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.CurseForge
if (!SemanticVersion.TryParse(raw, out version))
{
- if (invalidVersion == null)
- invalidVersion = raw;
+ invalidVersion ??= raw;
continue;
}
}
diff --git a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
index cc8f4737..676d660d 100644
--- a/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
+++ b/src/SMAPI.Web/Framework/Compression/GzipHelper.cs
@@ -69,7 +69,7 @@ namespace StardewModdingAPI.Web.Framework.Compression
return rawText;
// decompress
- using (MemoryStream memoryStream = new MemoryStream())
+ using MemoryStream memoryStream = new MemoryStream();
{
// read length prefix
int dataLength = BitConverter.ToInt32(zipBuffer, 0);
diff --git a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs
deleted file mode 100644
index c7b6cb00..00000000
--- a/src/SMAPI.Web/Framework/ConfigModels/MongoDbConfig.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace StardewModdingAPI.Web.Framework.ConfigModels
-{
- /// <summary>The config settings for mod compatibility list.</summary>
- internal class MongoDbConfig
- {
- /*********
- ** Accessors
- *********/
- /// <summary>The MongoDB connection string.</summary>
- public string ConnectionString { get; set; }
-
- /// <summary>The database name.</summary>
- public string Database { get; set; }
-
-
- /*********
- ** Public method
- *********/
- /// <summary>Get whether a MongoDB instance is configured.</summary>
- public bool IsConfigured()
- {
- return !string.IsNullOrWhiteSpace(this.ConnectionString);
- }
- }
-}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs
new file mode 100644
index 00000000..61cc4855
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/StorageConfig.cs
@@ -0,0 +1,18 @@
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>The config settings for cache storage.</summary>
+ internal class StorageConfig
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The storage mechanism to use.</summary>
+ public StorageMode Mode { get; set; }
+
+ /// <summary>The connection string for the storage mechanism, if applicable.</summary>
+ public string ConnectionString { get; set; }
+
+ /// <summary>The database name for the storage mechanism, if applicable.</summary>
+ public string Database { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs
new file mode 100644
index 00000000..4c2ea801
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/StorageMode.cs
@@ -0,0 +1,15 @@
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>Indicates a storage mechanism to use.</summary>
+ internal enum StorageMode
+ {
+ /// <summary>Store data in a hosted MongoDB instance.</summary>
+ Mongo,
+
+ /// <summary>Store data in an in-memory MongoDB instance. This is useful for testing MongoDB storage locally, but will likely fail when deployed since it needs permission to open a local port.</summary>
+ MongoInMemory,
+
+ /// <summary>Store data in-memory. This is suitable for local testing or single-instance servers, but will cause issues when distributed across multiple servers.</summary>
+ InMemory
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Extensions.cs b/src/SMAPI.Web/Framework/Extensions.cs
index e0da1424..ad7e645a 100644
--- a/src/SMAPI.Web/Framework/Extensions.cs
+++ b/src/SMAPI.Web/Framework/Extensions.cs
@@ -1,8 +1,12 @@
using System;
using JetBrains.Annotations;
+using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Razor;
+using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
+using Newtonsoft.Json;
namespace StardewModdingAPI.Web.Framework
{
@@ -18,6 +22,7 @@ namespace StardewModdingAPI.Web.Framework
/// <returns>The generated URL.</returns>
public static string PlainAction(this IUrlHelper helper, [AspMvcAction] string action, [AspMvcController] string controller, object values = null, bool absoluteUrl = false)
{
+ // get route values
RouteValueDictionary valuesDict = new RouteValueDictionary(values);
foreach (var value in helper.ActionContext.RouteData.Values)
{
@@ -25,14 +30,31 @@ namespace StardewModdingAPI.Web.Framework
valuesDict[value.Key] = null; // explicitly remove it from the URL
}
+ // get relative URL
string url = helper.Action(action, controller, valuesDict);
+ if (url == null && action.EndsWith("Async"))
+ url = helper.Action(action[..^"Async".Length], controller, valuesDict);
+
+ // get absolute URL
if (absoluteUrl)
{
HttpRequest request = helper.ActionContext.HttpContext.Request;
Uri baseUri = new Uri($"{request.Scheme}://{request.Host}");
url = new Uri(baseUri, url).ToString();
}
+
return url;
}
+
+ /// <summary>Get a serialized JSON representation of the value.</summary>
+ /// <param name="page">The page to extend.</param>
+ /// <param name="value">The value to serialize.</param>
+ /// <returns>The serialized JSON.</returns>
+ /// <remarks>This bypasses unnecessary validation (e.g. not allowing null values) in <see cref="IJsonHelper.Serialize"/>.</remarks>
+ public static IHtmlContent ForJson(this RazorPageBase page, object value)
+ {
+ string json = JsonConvert.SerializeObject(value);
+ return new HtmlString(json);
+ }
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index cce80816..227dcd89 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -45,7 +45,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?<name>.+?) (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>A regex pattern matching SMAPI's update line.</summary>
- private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly Regex SmapiUpdatePattern = new Regex(@"^You can update SMAPI to (?<version>[^\s]+): (?<link>.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/*********
@@ -181,9 +181,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
message.Section = LogSection.ModUpdateList;
}
- else if (message.Level == LogLevel.Alert && this.SMAPIUpdatePattern.IsMatch(message.Text))
+ else if (message.Level == LogLevel.Alert && this.SmapiUpdatePattern.IsMatch(message.Text))
{
- Match match = this.SMAPIUpdatePattern.Match(message.Text);
+ Match match = this.SmapiUpdatePattern.Match(message.Text);
string version = match.Groups["version"].Value;
string link = match.Groups["link"].Value;
smapiMod.UpdateVersion = version;
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
new file mode 100644
index 00000000..d75ee791
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectHostsToUrlsRule.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect hostnames to a URL if they match a condition.</summary>
+ internal class RedirectHostsToUrlsRule : RedirectMatchRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Maps a lowercase hostname to the resulting redirect URL.</summary>
+ private readonly Func<string, string> Map;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="statusCode">The status code to use for redirects.</param>
+ /// <param name="map">Hostnames mapped to the resulting redirect URL.</param>
+ public RedirectHostsToUrlsRule(HttpStatusCode statusCode, Func<string, string> map)
+ {
+ this.StatusCode = statusCode;
+ this.Map = map ?? throw new ArgumentNullException(nameof(map));
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected override string GetNewUrl(RewriteContext context)
+ {
+ // get requested host
+ string host = context.HttpContext.Request.Host.Host;
+ if (host == null)
+ return null;
+
+ // get new host
+ host = this.Map(host);
+ if (host == null)
+ return null;
+
+ // rewrite URL
+ UriBuilder uri = this.GetUrl(context.HttpContext.Request);
+ uri.Host = host;
+ return uri.ToString();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
new file mode 100644
index 00000000..6e81c4ca
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectMatchRule.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect matching requests to a URL.</summary>
+ internal abstract class RedirectMatchRule : IRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The status code to use for redirects.</summary>
+ protected HttpStatusCode StatusCode { get; set; } = HttpStatusCode.Redirect;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
+ /// <param name="context">The rewrite context.</param>
+ public void ApplyRule(RewriteContext context)
+ {
+ string newUrl = this.GetNewUrl(context);
+ if (newUrl == null)
+ return;
+
+ HttpResponse response = context.HttpContext.Response;
+ response.StatusCode = (int)HttpStatusCode.Redirect;
+ response.Headers["Location"] = newUrl;
+ context.Result = RuleResult.EndResponse;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected abstract string GetNewUrl(RewriteContext context);
+
+ /// <summary>Get the full request URL.</summary>
+ /// <param name="request">The request.</param>
+ protected UriBuilder GetUrl(HttpRequest request)
+ {
+ return new UriBuilder
+ {
+ Scheme = request.Scheme,
+ Host = request.Host.Host,
+ Port = request.Host.Port ?? -1,
+ Path = request.PathBase + request.Path,
+ Query = request.QueryString.Value
+ };
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
new file mode 100644
index 00000000..16397c1e
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectPathsToUrlsRule.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect paths to URLs if they match a condition.</summary>
+ internal class RedirectPathsToUrlsRule : RedirectMatchRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Regex patterns matching the current URL mapped to the resulting redirect URL.</summary>
+ private readonly IDictionary<Regex, string> Map;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="map">Regex patterns matching the current URL mapped to the resulting redirect URL.</param>
+ public RedirectPathsToUrlsRule(IDictionary<string, string> map)
+ {
+ this.Map = map.ToDictionary(
+ p => new Regex(p.Key, RegexOptions.IgnoreCase | RegexOptions.Compiled),
+ p => p.Value
+ );
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected override string GetNewUrl(RewriteContext context)
+ {
+ string path = context.HttpContext.Request.Path.Value;
+
+ if (!string.IsNullOrWhiteSpace(path))
+ {
+ foreach ((Regex pattern, string url) in this.Map)
+ {
+ if (pattern.IsMatch(path))
+ return pattern.Replace(path, url);
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
new file mode 100644
index 00000000..2a503ae3
--- /dev/null
+++ b/src/SMAPI.Web/Framework/RedirectRules/RedirectToHttpsRule.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Net;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Rewrite;
+
+namespace StardewModdingAPI.Web.Framework.RedirectRules
+{
+ /// <summary>Redirect requests to HTTPS.</summary>
+ internal class RedirectToHttpsRule : RedirectMatchRule
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Matches requests which should be ignored.</summary>
+ private readonly Func<HttpRequest, bool> Except;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="except">Matches requests which should be ignored.</param>
+ public RedirectToHttpsRule(Func<HttpRequest, bool> except = null)
+ {
+ this.Except = except ?? (req => false);
+ this.StatusCode = HttpStatusCode.RedirectKeepVerb;
+ }
+
+
+ /*********
+ ** Protected methods
+ *********/
+ /// <summary>Get the new redirect URL.</summary>
+ /// <param name="context">The rewrite context.</param>
+ /// <returns>Returns the redirect URL, or <c>null</c> if the redirect doesn't apply.</returns>
+ protected override string GetNewUrl(RewriteContext context)
+ {
+ HttpRequest request = context.HttpContext.Request;
+ if (request.IsHttps || this.Except(request))
+ return null;
+
+ UriBuilder uri = this.GetUrl(request);
+ uri.Scheme = "https";
+ return uri.ToString();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs b/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs
deleted file mode 100644
index 36effd82..00000000
--- a/src/SMAPI.Web/Framework/RewriteRules/ConditionalRedirectToHttpsRule.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System;
-using System.Net;
-using System.Text;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Rewrite;
-
-namespace StardewModdingAPI.Web.Framework.RewriteRules
-{
- /// <summary>Redirect requests to HTTPS.</summary>
- /// <remarks>Derived from <a href="https://stackoverflow.com/a/44526747/262123" /> and <see cref="Microsoft.AspNetCore.Rewrite.Internal.RedirectToHttpsRule"/>.</remarks>
- internal class ConditionalRedirectToHttpsRule : IRule
- {
- /*********
- ** Fields
- *********/
- /// <summary>A predicate which indicates when the rule should be applied.</summary>
- private readonly Func<HttpRequest, bool> ShouldRewrite;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param>
- public ConditionalRedirectToHttpsRule(Func<HttpRequest, bool> shouldRewrite = null)
- {
- this.ShouldRewrite = shouldRewrite ?? (req => true);
- }
-
- /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
- /// <param name="context">The rewrite context.</param>
- public void ApplyRule(RewriteContext context)
- {
- HttpRequest request = context.HttpContext.Request;
-
- // check condition
- if (this.IsSecure(request) || !this.ShouldRewrite(request))
- return;
-
- // redirect request
- HttpResponse response = context.HttpContext.Response;
- response.StatusCode = (int)HttpStatusCode.RedirectKeepVerb;
- response.Headers["Location"] = new StringBuilder()
- .Append("https://")
- .Append(request.Host.Host)
- .Append(request.PathBase)
- .Append(request.Path)
- .Append(request.QueryString)
- .ToString();
- context.Result = RuleResult.EndResponse;
- }
-
- /// <summary>Get whether the request was received over HTTPS.</summary>
- /// <param name="request">The request to check.</param>
- public bool IsSecure(HttpRequest request)
- {
- return
- request.IsHttps // HTTPS to server
- || string.Equals(request.Headers["x-forwarded-proto"], "HTTPS", StringComparison.OrdinalIgnoreCase); // HTTPS to AWS load balancer
- }
- }
-}
diff --git a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs b/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs
deleted file mode 100644
index ab9e019c..00000000
--- a/src/SMAPI.Web/Framework/RewriteRules/RedirectToUrlRule.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using System;
-using System.Net;
-using System.Text.RegularExpressions;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Rewrite;
-
-namespace StardewModdingAPI.Web.Framework.RewriteRules
-{
- /// <summary>Redirect requests to an external URL if they match a condition.</summary>
- internal class RedirectToUrlRule : IRule
- {
- /*********
- ** Fields
- *********/
- /// <summary>Get the new URL to which to redirect (or <c>null</c> to skip).</summary>
- private readonly Func<HttpRequest, string> NewUrl;
-
-
- /*********
- ** Public methods
- *********/
- /// <summary>Construct an instance.</summary>
- /// <param name="shouldRewrite">A predicate which indicates when the rule should be applied.</param>
- /// <param name="url">The new URL to which to redirect.</param>
- public RedirectToUrlRule(Func<HttpRequest, bool> shouldRewrite, string url)
- {
- this.NewUrl = req => shouldRewrite(req) ? url : null;
- }
-
- /// <summary>Construct an instance.</summary>
- /// <param name="pathRegex">A case-insensitive regex to match against the path.</param>
- /// <param name="url">The external URL.</param>
- public RedirectToUrlRule(string pathRegex, string url)
- {
- Regex regex = new Regex(pathRegex, RegexOptions.IgnoreCase | RegexOptions.Compiled);
- this.NewUrl = req => req.Path.HasValue ? regex.Replace(req.Path.Value, url) : null;
- }
-
- /// <summary>Applies the rule. Implementations of ApplyRule should set the value for <see cref="RewriteContext.Result" /> (defaults to RuleResult.ContinueRules).</summary>
- /// <param name="context">The rewrite context.</param>
- public void ApplyRule(RewriteContext context)
- {
- HttpRequest request = context.HttpContext.Request;
-
- // check rewrite
- string newUrl = this.NewUrl(request);
- if (newUrl == null || newUrl == request.Path.Value)
- return;
-
- // redirect request
- HttpResponse response = context.HttpContext.Response;
- response.StatusCode = (int)HttpStatusCode.Redirect;
- response.Headers["Location"] = newUrl;
- context.Result = RuleResult.EndResponse;
- }
- }
-}
diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs
index 70082160..1fdd3185 100644
--- a/src/SMAPI.Web/Program.cs
+++ b/src/SMAPI.Web/Program.cs
@@ -1,5 +1,5 @@
-using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
namespace StardewModdingAPI.Web
{
@@ -13,13 +13,13 @@ namespace StardewModdingAPI.Web
/// <param name="args">The command-line arguments.</param>
public static void Main(string[] args)
{
- // configure web server
- WebHost
+ Host
.CreateDefaultBuilder(args)
- .CaptureStartupErrors(true)
- .UseSetting("detailedErrors", "true")
- .UseKestrel().UseIISIntegration() // must be used together; fixes intermittent errors on Azure: https://stackoverflow.com/a/38312175/262123
- .UseStartup<Startup>()
+ .ConfigureWebHostDefaults(builder => builder
+ .CaptureStartupErrors(true)
+ .UseSetting("detailedErrors", "true")
+ .UseStartup<Startup>()
+ )
.Build()
.Run();
}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index 0a978b30..7ed79ea3 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -3,7 +3,7 @@
<PropertyGroup>
<AssemblyName>SMAPI.Web</AssemblyName>
<RootNamespace>StardewModdingAPI.Web</RootNamespace>
- <TargetFramework>netcoreapp2.0</TargetFramework>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
</PropertyGroup>
@@ -12,23 +12,20 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Azure.Storage.Blobs" Version="12.4.0" />
- <PackageReference Include="Hangfire.AspNetCore" Version="1.7.9" />
+ <PackageReference Include="Azure.Storage.Blobs" Version="12.4.2" />
+ <PackageReference Include="Hangfire.AspNetCore" Version="1.7.11" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.7.0" />
<PackageReference Include="Hangfire.Mongo" Version="0.6.7" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
- <PackageReference Include="Humanizer.Core" Version="2.7.9" />
- <PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
- <PackageReference Include="Markdig" Version="0.18.3" />
- <PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" />
- <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
+ <PackageReference Include="Humanizer.Core" Version="2.8.11" />
+ <PackageReference Include="JetBrains.Annotations" Version="2020.1.0" />
+ <PackageReference Include="Markdig" Version="0.20.0" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.2" />
<PackageReference Include="Mongo2Go" Version="2.2.12" />
- <PackageReference Include="MongoDB.Driver" Version="2.10.2" />
+ <PackageReference Include="MongoDB.Driver" Version="2.10.4" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
- <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.0" />
- <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
+ <PackageReference Include="Pathoschild.FluentNexus" Version="1.0.1" />
+ <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs
index 56ef9a79..dee2edc2 100644
--- a/src/SMAPI.Web/Startup.cs
+++ b/src/SMAPI.Web/Startup.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Net;
using Hangfire;
using Hangfire.MemoryStorage;
using Hangfire.Mongo;
@@ -27,7 +28,7 @@ using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.Clients.Pastebin;
using StardewModdingAPI.Web.Framework.Compression;
using StardewModdingAPI.Web.Framework.ConfigModels;
-using StardewModdingAPI.Web.Framework.RewriteRules;
+using StardewModdingAPI.Web.Framework.RedirectRules;
using StardewModdingAPI.Web.Framework.Storage;
namespace StardewModdingAPI.Web
@@ -47,7 +48,7 @@ namespace StardewModdingAPI.Web
*********/
/// <summary>Construct an instance.</summary>
/// <param name="env">The hosting environment.</param>
- public Startup(IHostingEnvironment env)
+ public Startup(IWebHostEnvironment env)
{
this.Configuration = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
@@ -67,70 +68,91 @@ namespace StardewModdingAPI.Web
.Configure<BackgroundServicesConfig>(this.Configuration.GetSection("BackgroundServices"))
.Configure<ModCompatibilityListConfig>(this.Configuration.GetSection("ModCompatibilityList"))
.Configure<ModUpdateCheckConfig>(this.Configuration.GetSection("ModUpdateCheck"))
- .Configure<MongoDbConfig>(this.Configuration.GetSection("MongoDB"))
+ .Configure<StorageConfig>(this.Configuration.GetSection("Storage"))
.Configure<SiteConfig>(this.Configuration.GetSection("Site"))
.Configure<RouteOptions>(options => options.ConstraintMap.Add("semanticVersion", typeof(VersionConstraint)))
.AddLogging()
- .AddMemoryCache()
- .AddMvc()
- .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()))
- .AddJsonOptions(options =>
- {
- foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
- options.SerializerSettings.Converters.Add(converter);
+ .AddMemoryCache();
+ StorageConfig storageConfig = this.Configuration.GetSection("Storage").Get<StorageConfig>();
+ StorageMode storageMode = storageConfig.Mode;
- options.SerializerSettings.Formatting = Formatting.Indented;
- options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
- });
- MongoDbConfig mongoConfig = this.Configuration.GetSection("MongoDB").Get<MongoDbConfig>();
+ // init MVC
+ services
+ .AddControllers()
+ .AddNewtonsoftJson(options => this.ConfigureJsonNet(options.SerializerSettings))
+ .ConfigureApplicationPartManager(manager => manager.FeatureProviders.Add(new InternalControllerFeatureProvider()));
+ services
+ .AddRazorPages();
- // init background service
+ // init storage
+ switch (storageMode)
{
- BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get<BackgroundServicesConfig>();
- if (config.Enabled)
- services.AddHostedService<BackgroundService>();
- }
+ case StorageMode.InMemory:
+ services.AddSingleton<IModCacheRepository>(new ModCacheMemoryRepository());
+ services.AddSingleton<IWikiCacheRepository>(new WikiCacheMemoryRepository());
+ break;
- // init MongoDB
- services.AddSingleton<MongoDbRunner>(serv => !mongoConfig.IsConfigured()
- ? MongoDbRunner.Start()
- : throw new InvalidOperationException("The MongoDB connection is configured, so the local development version should not be used.")
- );
- services.AddSingleton<IMongoDatabase>(serv =>
- {
- // get connection string
- string connectionString = mongoConfig.IsConfigured()
- ? mongoConfig.ConnectionString
- : serv.GetRequiredService<MongoDbRunner>().ConnectionString;
-
- // get client
- BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer());
- return new MongoClient(connectionString).GetDatabase(mongoConfig.Database);
- });
- services.AddSingleton<IModCacheRepository>(serv => new ModCacheRepository(serv.GetRequiredService<IMongoDatabase>()));
- services.AddSingleton<IWikiCacheRepository>(serv => new WikiCacheRepository(serv.GetRequiredService<IMongoDatabase>()));
+ case StorageMode.Mongo:
+ case StorageMode.MongoInMemory:
+ {
+ // local MongoDB instance
+ services.AddSingleton<MongoDbRunner>(_ => storageMode == StorageMode.MongoInMemory
+ ? MongoDbRunner.Start()
+ : throw new NotSupportedException($"The in-memory MongoDB runner isn't available in storage mode {storageMode}.")
+ );
+
+ // MongoDB
+ services.AddSingleton<IMongoDatabase>(serv =>
+ {
+ BsonSerializer.RegisterSerializer(new UtcDateTimeOffsetSerializer());
+ return new MongoClient(this.GetMongoDbConnectionString(serv, storageConfig))
+ .GetDatabase(storageConfig.Database);
+ });
+
+ // repositories
+ services.AddSingleton<IModCacheRepository>(serv => new ModCacheMongoRepository(serv.GetRequiredService<IMongoDatabase>()));
+ services.AddSingleton<IWikiCacheRepository>(serv => new WikiCacheMongoRepository(serv.GetRequiredService<IMongoDatabase>()));
+ }
+ break;
+
+ default:
+ throw new NotSupportedException($"Unhandled storage mode '{storageMode}'.");
+ }
// init Hangfire
services
- .AddHangfire(config =>
+ .AddHangfire((serv, config) =>
{
config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_170)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings();
- if (mongoConfig.IsConfigured())
+ switch (storageMode)
{
- config.UseMongoStorage(mongoConfig.ConnectionString, $"{mongoConfig.Database}-hangfire", new MongoStorageOptions
- {
- MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop),
- CheckConnection = false // error on startup takes down entire process
- });
+ case StorageMode.InMemory:
+ config.UseMemoryStorage();
+ break;
+
+ case StorageMode.MongoInMemory:
+ case StorageMode.Mongo:
+ string connectionString = this.GetMongoDbConnectionString(serv, storageConfig);
+ config.UseMongoStorage(MongoClientSettings.FromConnectionString(connectionString), $"{storageConfig.Database}-hangfire", new MongoStorageOptions
+ {
+ MigrationOptions = new MongoMigrationOptions(MongoMigrationStrategy.Drop),
+ CheckConnection = false // error on startup takes down entire process
+ });
+ break;
}
- else
- config.UseMemoryStorage();
});
+ // init background service
+ {
+ BackgroundServicesConfig config = this.Configuration.GetSection("BackgroundServices").Get<BackgroundServicesConfig>();
+ if (config.Enabled)
+ services.AddHostedService<BackgroundService>();
+ }
+
// init API clients
{
ApiClientsConfig api = this.Configuration.GetSection("ApiClients").Get<ApiClientsConfig>();
@@ -142,6 +164,7 @@ namespace StardewModdingAPI.Web
baseUrl: api.ChucklefishBaseUrl,
modPageUrlFormat: api.ChucklefishModPageUrlFormat
));
+
services.AddSingleton<ICurseForgeClient>(new CurseForgeClient(
userAgent: userAgent,
apiUrl: api.CurseForgeBaseUrl
@@ -188,8 +211,7 @@ namespace StardewModdingAPI.Web
/// <summary>The method called by the runtime to configure the HTTP request pipeline.</summary>
/// <param name="app">The application builder.</param>
- /// <param name="env">The hosting environment.</param>
- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+ public void Configure(IApplicationBuilder app)
{
// basic config
app.UseDeveloperExceptionPage();
@@ -201,7 +223,13 @@ namespace StardewModdingAPI.Web
)
.UseRewriter(this.GetRedirectRules())
.UseStaticFiles() // wwwroot folder
- .UseMvc();
+ .UseRouting()
+ .UseAuthorization()
+ .UseEndpoints(p =>
+ {
+ p.MapControllers();
+ p.MapRazorPages();
+ });
// enable Hangfire dashboard
app.UseHangfireDashboard("/tasks", new DashboardOptions
@@ -215,29 +243,77 @@ namespace StardewModdingAPI.Web
/*********
** Private methods
*********/
+ /// <summary>Configure a Json.NET serializer.</summary>
+ /// <param name="settings">The serializer settings to edit.</param>
+ private void ConfigureJsonNet(JsonSerializerSettings settings)
+ {
+ foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters)
+ settings.Converters.Add(converter);
+
+ settings.Formatting = Formatting.Indented;
+ settings.NullValueHandling = NullValueHandling.Ignore;
+ }
+
+ /// <summary>Get the MongoDB connection string for the given storage configuration.</summary>
+ /// <param name="services">The service provider.</param>
+ /// <param name="storageConfig">The storage configuration</param>
+ /// <exception cref="NotSupportedException">There's no MongoDB instance in the given storage mode.</exception>
+ private string GetMongoDbConnectionString(IServiceProvider services, StorageConfig storageConfig)
+ {
+ return storageConfig.Mode switch
+ {
+ StorageMode.Mongo => storageConfig.ConnectionString,
+ StorageMode.MongoInMemory => services.GetRequiredService<MongoDbRunner>().ConnectionString,
+ _ => throw new NotSupportedException($"There's no MongoDB instance in storage mode {storageConfig.Mode}.")
+ };
+ }
+
/// <summary>Get the redirect rules to apply.</summary>
private RewriteOptions GetRedirectRules()
{
- var redirects = new RewriteOptions();
-
- // redirect to HTTPS (except API for Linux/Mac Mono compatibility)
- redirects.Add(new ConditionalRedirectToHttpsRule(
- shouldRewrite: req =>
- req.Host.Host != "localhost"
- && !req.Path.StartsWithSegments("/api")
- ));
-
- // shortcut redirects
- redirects.Add(new RedirectToUrlRule(@"^/3\.0\.?$", "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0"));
- redirects.Add(new RedirectToUrlRule(@"^/(?:buildmsg|package)(?:/?(.*))$", "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1")); // buildmsg deprecated, remove when SDV 1.4 is released
- redirects.Add(new RedirectToUrlRule(@"^/community\.?$", "https://stardewvalleywiki.com/Modding:Community"));
- redirects.Add(new RedirectToUrlRule(@"^/compat\.?$", "https://smapi.io/mods"));
- redirects.Add(new RedirectToUrlRule(@"^/docs\.?$", "https://stardewvalleywiki.com/Modding:Index"));
- redirects.Add(new RedirectToUrlRule(@"^/install\.?$", "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI"));
- redirects.Add(new RedirectToUrlRule(@"^/troubleshoot(.*)$", "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1"));
- redirects.Add(new RedirectToUrlRule(@"^/xnb\.?$", "https://stardewvalleywiki.com/Modding:Using_XNB_mods"));
-
- // redirect legacy canimod.com URLs
+ var redirects = new RewriteOptions()
+ // shortcut paths
+ .Add(new RedirectPathsToUrlsRule(new Dictionary<string, string>
+ {
+ [@"^/3\.0\.?$"] = "https://stardewvalleywiki.com/Modding:Migrate_to_SMAPI_3.0",
+ [@"^/(?:buildmsg|package)(?:/?(.*))$"] = "https://github.com/Pathoschild/SMAPI/blob/develop/docs/technical/mod-package.md#$1", // buildmsg deprecated, remove when SDV 1.4 is released
+ [@"^/community\.?$"] = "https://stardewvalleywiki.com/Modding:Community",
+ [@"^/compat\.?$"] = "https://smapi.io/mods",
+ [@"^/docs\.?$"] = "https://stardewvalleywiki.com/Modding:Index",
+ [@"^/install\.?$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Getting_Started#Install_SMAPI",
+ [@"^/troubleshoot(.*)$"] = "https://stardewvalleywiki.com/Modding:Player_Guide/Troubleshooting$1",
+ [@"^/xnb\.?$"] = "https://stardewvalleywiki.com/Modding:Using_XNB_mods"
+ }))
+
+ // legacy paths
+ .Add(new RedirectPathsToUrlsRule(this.GetLegacyPathRedirects()))
+
+ // subdomains
+ .Add(new RedirectHostsToUrlsRule(HttpStatusCode.PermanentRedirect, host => host switch
+ {
+ "api.smapi.io" => "smapi.io/api",
+ "json.smapi.io" => "smapi.io/json",
+ "log.smapi.io" => "smapi.io/log",
+ "mods.smapi.io" => "smapi.io/mods",
+ _ => host.EndsWith(".smapi.io")
+ ? "smapi.io"
+ : null
+ }))
+
+ // redirect to HTTPS (except API for Linux/Mac Mono compatibility)
+ .Add(
+ new RedirectToHttpsRule(except: req => req.Host.Host == "localhost" || req.Path.StartsWithSegments("/api"))
+ );
+
+ return redirects;
+ }
+
+ /// <summary>Get the redirects for legacy paths that have been moved elsewhere.</summary>
+ private IDictionary<string, string> GetLegacyPathRedirects()
+ {
+ var redirects = new Dictionary<string, string>();
+
+ // canimod.com => wiki
var wikiRedirects = new Dictionary<string, string[]>
{
["Modding:Index#Migration_guides"] = new[] { "^/for-devs/updating-a-smapi-mod", "^/guides/updating-a-smapi-mod" },
@@ -251,10 +327,10 @@ namespace StardewModdingAPI.Web
["Modding:Object_data"] = new[] { "^/for-devs/object-data", "^/guides/object-data" },
["Modding:Weather_data"] = new[] { "^/for-devs/weather", "^/guides/weather" }
};
- foreach (KeyValuePair<string, string[]> pair in wikiRedirects)
+ foreach ((string page, string[] patterns) in wikiRedirects)
{
- foreach (string pattern in pair.Value)
- redirects.Add(new RedirectToUrlRule(pattern, "https://stardewvalleywiki.com/" + pair.Key));
+ foreach (string pattern in patterns)
+ redirects.Add(pattern, "https://stardewvalleywiki.com/" + page);
}
return redirects;
diff --git a/src/SMAPI.Web/ViewModels/ModListModel.cs b/src/SMAPI.Web/ViewModels/ModListModel.cs
index ff7513bc..6b8279c1 100644
--- a/src/SMAPI.Web/ViewModels/ModListModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModListModel.cs
@@ -26,7 +26,7 @@ namespace StardewModdingAPI.Web.ViewModels
public bool IsStale { get; set; }
/// <summary>Whether the mod metadata is available.</summary>
- public bool HasData => this.Mods != null;
+ public bool HasData => this.Mods?.Any() == true;
/*********
diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs
index 56316ab7..45b12397 100644
--- a/src/SMAPI.Web/ViewModels/ModModel.cs
+++ b/src/SMAPI.Web/ViewModels/ModModel.cs
@@ -22,6 +22,9 @@ namespace StardewModdingAPI.Web.ViewModels
/// <summary>The mod author's alternative names, if any.</summary>
public string AlternateAuthors { get; set; }
+ /// <summary>The GitHub repo, if any.</summary>
+ public string GitHubRepo { get; set; }
+
/// <summary>The URL to the mod's source code, if any.</summary>
public string SourceUrl { get; set; }
@@ -62,6 +65,7 @@ namespace StardewModdingAPI.Web.ViewModels
this.AlternateNames = string.Join(", ", entry.Name.Skip(1).ToArray());
this.Author = entry.Author.FirstOrDefault();
this.AlternateAuthors = string.Join(", ", entry.Author.Skip(1).ToArray());
+ this.GitHubRepo = entry.GitHubRepo;
this.SourceUrl = this.GetSourceUrl(entry);
this.Compatibility = new ModCompatibilityModel(entry.Compatibility);
this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null;
diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml
index eded9df3..d78a155e 100644
--- a/src/SMAPI.Web/Views/Index/Index.cshtml
+++ b/src/SMAPI.Web/Views/Index/Index.cshtml
@@ -9,7 +9,7 @@
}
@section Head {
<link rel="stylesheet" href="~/Content/css/index.css?r=20200105" />
- <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
<script src="~/Content/js/index.js?r=20200105"></script>
}
diff --git a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
index 7287e00b..f23bd150 100644
--- a/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
+++ b/src/SMAPI.Web/Views/JsonValidator/Index.cshtml
@@ -32,7 +32,7 @@
<link rel="stylesheet" href="~/Content/css/json-validator.css?r=202002" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/themes/sunlight.default.min.css" />
- <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/sunlight.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/plugins/sunlight-plugin.linenumbers.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/tmont/sunlight@1.22.0/src/lang/sunlight.javascript.min.js" crossorigin="anonymous"></script>
@@ -40,7 +40,7 @@
<script src="~/Content/js/json-validator.js?r=202002"></script>
<script>
$(function() {
- smapi.jsonValidator(@Json.Serialize(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = "$schemaName", id = "$id" })), @Json.Serialize(Model.PasteID));
+ smapi.jsonValidator(@this.ForJson(this.Url.PlainAction("Index", "JsonValidator", new { schemaName = "$schemaName", id = "$id" })), @this.ForJson(Model.PasteID));
});
</script>
}
diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 2183992b..71e12d47 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -1,5 +1,4 @@
@using Humanizer
-@using Newtonsoft.Json
@using StardewModdingAPI.Toolkit.Utilities
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.Framework.LogParsing.Models
@@ -12,7 +11,6 @@
.GetValues(typeof(LogLevel))
.Cast<LogLevel>()
.ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
- JsonSerializerSettings noFormatting = new JsonSerializerSettings { Formatting = Formatting.None };
string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true);
}
@@ -25,19 +23,19 @@
<link rel="stylesheet" href="~/Content/css/file-upload.css?r=202002" />
<link rel="stylesheet" href="~/Content/css/log-parser.css?r=202002" />
- <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
- <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
<script src="~/Content/js/file-upload.js?r=202002"></script>
<script src="~/Content/js/log-parser.js?r=202002"></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 => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true), noFormatting),
- showSections: @Json.Serialize(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false), noFormatting),
- showLevels: @Json.Serialize(defaultFilters, noFormatting),
- enableFilters: @Json.Serialize(!Model.ShowRaw)
+ logStarted: new Date(@this.ForJson(Model.ParsedLog?.Timestamp)),
+ showPopup: @this.ForJson(Model.ParsedLog == null),
+ showMods: @this.ForJson(Model.ParsedLog?.Mods?.Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, slug => true)),
+ showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, section => false)),
+ showLevels: @this.ForJson(defaultFilters),
+ enableFilters: @this.ForJson(!Model.ShowRaw)
}, '@this.Url.PlainAction("Index", "LogParser", values: null)');
});
</script>
diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml
index b1d9ae2c..cda2923d 100644
--- a/src/SMAPI.Web/Views/Mods/Index.cshtml
+++ b/src/SMAPI.Web/Views/Mods/Index.cshtml
@@ -1,22 +1,26 @@
@using Humanizer
@using Humanizer.Localisation
-@using Newtonsoft.Json
+@using StardewModdingAPI.Web.Framework
+@using StardewModdingAPI.Web.ViewModels
@model StardewModdingAPI.Web.ViewModels.ModListModel
@{
ViewData["Title"] = "Mod compatibility";
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
+
+ bool hasBeta = true; // Model.BetaVersion != null;
+ string betaLabel = "SMAPI 3.6 only"; //"SDV @Model.BetaVersion only";
}
@section Head {
<link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" />
- <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
- <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
- <script src="https://cdn.jsdelivr.net/npm/tablesorter@2.31.0/dist/js/jquery.tablesorter.combined.min.js" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
+ <script src="https://cdn.jsdelivr.net/npm/tablesorter@2.31.3" crossorigin="anonymous"></script>
<script src="~/Content/js/mods.js?r=20200218"></script>
<script>
$(function() {
- var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None });
- var enableBeta = @Json.Serialize(Model.BetaVersion != null);
+ var data = @this.ForJson(Model.Mods ?? new ModModel[0]);
+ var enableBeta = @this.ForJson(hasBeta);
smapi.modList(data, enableBeta);
});
</script>
@@ -39,9 +43,9 @@ else
<p>The list is updated every few days (you can help <a href="https://stardewvalleywiki.com/Modding:Mod_compatibility">update it</a>!). It doesn't include XNB mods (see <a href="https://stardewvalleywiki.com/Modding:Using_XNB_mods"><em>using XNB mods</em> on the wiki</a> instead) or compatible content packs.</p>
- @if (Model.BetaVersion != null)
+ @if (hasBeta)
{
- <p id="beta-blurb" v-show="showAdvanced"><strong>Note:</strong> "SDV @Model.BetaVersion only" lines are for an unreleased version of the game, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions of the game.</p>
+ <p id="beta-blurb" v-show="showAdvanced"><strong>Note:</strong> "@betaLabel" lines are for an unreleased version of SMAPI, not the stable version most players have. If a mod doesn't have that line, the info applies to both versions of SMAPI.</p>
}
</div>
@@ -79,14 +83,14 @@ else
</tr>
</thead>
<tbody>
- <tr v-for="mod in mods" :key="mod.Name" v-bind:id="mod.Slug" :key="mod.Slug" v-bind:data-status="mod.Compatibility.Status" v-show="mod.Visible">
+ <tr v-for="mod in mods" :key="mod.Slug" v-bind:id="mod.Slug" v-bind:data-status="mod.Compatibility.Status" v-show="mod.Visible">
<td>
{{mod.Name}}
<small class="mod-alt-names" v-if="mod.AlternateNames">(aka {{mod.AlternateNames}})</small>
</td>
<td class="mod-page-links">
<span v-for="(link, i) in mod.ModPages">
- <a v-bind:href="link.Url">{{link.Text}}</a>{{i < mod.ModPages.length - 1 ? ', ' : ''}}
+ <a v-bind:href="link.Url">{{link.Text}}</a>{{i &lt; mod.ModPages.length - 1 ? ', ' : ''}}
</span>
</td>
<td>
@@ -96,14 +100,20 @@ else
<td>
<div v-html="mod.Compatibility.Summary"></div>
<div v-if="mod.BetaCompatibility" v-show="showAdvanced">
- <strong v-if="mod.BetaCompatibility">SDV @Model.BetaVersion only:</strong>
+ <strong v-if="mod.BetaCompatibility">@betaLabel:</strong>
<span v-html="mod.BetaCompatibility.Summary"></span>
</div>
<div v-for="(warning, i) in mod.Warnings">⚠ {{warning}}</div>
</td>
<td class="mod-broke-in" v-html="mod.LatestCompatibility.BrokeIn" v-show="showAdvanced"></td>
<td v-show="showAdvanced">
- <span v-if="mod.SourceUrl"><a v-bind:href="mod.SourceUrl">source</a></span>
+ <span v-if="mod.SourceUrl">
+ <a v-bind:href="mod.SourceUrl">source</a>
+ <span v-if="mod.GitHubRepo">
+ @* see https://shields.io/category/license *@
+ (<img v-bind:src="'https://img.shields.io/github/license/' + mod.GitHubRepo + '?style=flat-square.png&label='" class="license-badge" alt="source" />)
+ </span>
+ </span>
<span v-else class="mod-closed-source">no source</span>
</td>
<td>
diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
index 2d06ceb1..67dcd3b3 100644
--- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml
+++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml
@@ -29,7 +29,7 @@
</div>
<div id="content-column">
<div id="content">
- @if (ViewData["ViewTitle"] != string.Empty)
+ @if (ViewData["ViewTitle"] as string != string.Empty)
{
<h1>@(ViewData["ViewTitle"] ?? ViewData["Title"])</h1>
}
diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json
index 54460c46..41c00e79 100644
--- a/src/SMAPI.Web/appsettings.Development.json
+++ b/src/SMAPI.Web/appsettings.Development.json
@@ -17,7 +17,8 @@
"NexusApiKey": null
},
- "MongoDB": {
+ "Storage": {
+ "Mode": "MongoInMemory",
"ConnectionString": null,
"Database": "smapi-edge"
},
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index 9cd1efc8..b1d39a6f 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -49,7 +49,8 @@
"PastebinBaseUrl": "https://pastebin.com/"
},
- "MongoDB": {
+ "Storage": {
+ "Mode": "InMemory",
"ConnectionString": null,
"Database": "smapi"
},
diff --git a/src/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css
index 697ba514..4f6578cb 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/mods.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css
@@ -153,3 +153,7 @@ table.wikitable > caption {
#mod-list td.smapi-3-col span {
border-bottom: 1px dashed gray;
}
+
+#mod-list .license-badge {
+ vertical-align: middle;
+}
diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js
index 35098b60..ac2754a4 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/mods.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js
@@ -1,5 +1,3 @@
-/* globals $ */
-
var smapi = smapi || {};
var app;
smapi.modList = function (mods, enableBeta) {
diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
index f627ab95..726b50be 100644
--- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
+++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
@@ -11,9 +11,9 @@
"title": "Format version",
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
"type": "string",
- "const": "1.13.0",
+ "const": "1.14.0",
"@errorMessages": {
- "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.13.0'."
+ "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.14.0'."
}
},
"ConfigSchema": {
diff --git a/src/SMAPI.sln b/src/SMAPI.sln
index f9c537c4..92b0cd2c 100644
--- a/src/SMAPI.sln
+++ b/src/SMAPI.sln
@@ -77,8 +77,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit.CoreInterface
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web", "SMAPI.Web\SMAPI.Web.csproj", "{80EFD92F-728F-41E0-8A5B-9F6F49A91899}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web.LegacyRedirects", "SMAPI.Web.LegacyRedirects\SMAPI.Web.LegacyRedirects.csproj", "{159AA5A5-35C2-488C-B23F-1613C80594AE}"
-EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5
@@ -138,10 +136,6 @@ Global
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.Build.0 = Release|Any CPU
- {159AA5A5-35C2-488C-B23F-1613C80594AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {159AA5A5-35C2-488C-B23F-1613C80594AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {159AA5A5-35C2-488C-B23F-1613C80594AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {159AA5A5-35C2-488C-B23F-1613C80594AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/SMAPI/Framework/IModMetadata.cs b/src/SMAPI/Framework/IModMetadata.cs
index 37927482..1231b494 100644
--- a/src/SMAPI/Framework/IModMetadata.cs
+++ b/src/SMAPI/Framework/IModMetadata.cs
@@ -112,9 +112,9 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the mod has at least one valid update key set.</summary>
bool HasValidUpdateKeys();
- /// <summary>Get whether the mod has a given warning and it hasn't been suppressed in the <see cref="DataRecord"/>.</summary>
- /// <param name="warning">The warning to check.</param>
- bool HasUnsuppressWarning(ModWarning warning);
+ /// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="DataRecord"/>.</summary>
+ /// <param name="warnings">The warnings to check.</param>
+ bool HasUnsuppressedWarnings(params ModWarning[] warnings);
/// <summary>Get a relative path which includes the root folder name.</summary>
string GetRelativePathWithRoot();
diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
index 36622066..2657fd12 100644
--- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
+++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs
@@ -205,16 +205,13 @@ namespace StardewModdingAPI.Framework.Input
/// <summary>Get the equivalent state.</summary>
public GamePadState GetState()
{
- if (this.State == null)
- {
- this.State = new GamePadState(
- leftThumbStick: this.LeftStickPos,
- rightThumbStick: this.RightStickPos,
- leftTrigger: this.LeftTrigger,
- rightTrigger: this.RightTrigger,
- buttons: this.GetButtonBitmask() // MonoGame requires one bitmask here; don't specify multiple values
- );
- }
+ this.State ??= new GamePadState(
+ leftThumbStick: this.LeftStickPos,
+ rightThumbStick: this.RightStickPos,
+ leftTrigger: this.LeftTrigger,
+ rightTrigger: this.RightTrigger,
+ buttons: this.GetButtonBitmask() // MonoGame requires one bitmask here; don't specify multiple values
+ );
return this.State.Value;
}
diff --git a/src/SMAPI/Framework/Input/MouseStateBuilder.cs b/src/SMAPI/Framework/Input/MouseStateBuilder.cs
index 59956feb..1cc16ca9 100644
--- a/src/SMAPI/Framework/Input/MouseStateBuilder.cs
+++ b/src/SMAPI/Framework/Input/MouseStateBuilder.cs
@@ -89,19 +89,16 @@ namespace StardewModdingAPI.Framework.Input
/// <summary>Get the equivalent state.</summary>
public MouseState GetState()
{
- if (this.State == null)
- {
- this.State = new MouseState(
- x: this.X,
- y: this.Y,
- scrollWheel: this.ScrollWheelValue,
- leftButton: this.ButtonStates[SButton.MouseLeft],
- middleButton: this.ButtonStates[SButton.MouseMiddle],
- rightButton: this.ButtonStates[SButton.MouseRight],
- xButton1: this.ButtonStates[SButton.MouseX1],
- xButton2: this.ButtonStates[SButton.MouseX2]
- );
- }
+ this.State ??= new MouseState(
+ x: this.X,
+ y: this.Y,
+ scrollWheel: this.ScrollWheelValue,
+ leftButton: this.ButtonStates[SButton.MouseLeft],
+ middleButton: this.ButtonStates[SButton.MouseMiddle],
+ rightButton: this.ButtonStates[SButton.MouseRight],
+ xButton1: this.ButtonStates[SButton.MouseX1],
+ xButton2: this.ButtonStates[SButton.MouseX2]
+ );
return this.State.Value;
}
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index d9b4af1b..5218938f 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -127,7 +127,6 @@ namespace StardewModdingAPI.Framework.ModLoading
{
if (!oneAssembly)
this.Monitor.Log($" Loading {assembly.File.Name} (rewritten)...", LogLevel.Trace);
-
using MemoryStream outStream = new MemoryStream();
assembly.Definition.Write(outStream);
byte[] bytes = outStream.ToArray();
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index 0e90362e..30701552 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -215,13 +215,14 @@ namespace StardewModdingAPI.Framework.ModLoading
return this.GetUpdateKeys(validOnly: true).Any();
}
- /// <summary>Get whether the mod has a given warning and it hasn't been suppressed in the <see cref="DataRecord"/>.</summary>
- /// <param name="warning">The warning to check.</param>
- public bool HasUnsuppressWarning(ModWarning warning)
+ /// <summary>Get whether the mod has any of the given warnings which haven't been suppressed in the <see cref="IModMetadata.DataRecord"/>.</summary>
+ /// <param name="warnings">The warnings to check.</param>
+ public bool HasUnsuppressedWarnings(params ModWarning[] warnings)
{
- return
+ return warnings.Any(warning =>
this.Warnings.HasFlag(warning)
- && (this.DataRecord?.DataRecord == null || !this.DataRecord.DataRecord.SuppressWarnings.HasFlag(warning));
+ && (this.DataRecord?.DataRecord == null || !this.DataRecord.DataRecord.SuppressWarnings.HasFlag(warning))
+ );
}
/// <summary>Get a relative path which includes the root folder name.</summary>
diff --git a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs
index 7dbfa767..ac9cf313 100644
--- a/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs
+++ b/src/SMAPI/Framework/Networking/SGalaxyNetServer.cs
@@ -45,23 +45,22 @@ namespace StardewModdingAPI.Framework.Networking
[SuppressMessage("ReSharper", "AccessToDisposedClosure", Justification = "The callback is invoked synchronously.")]
protected override void onReceiveMessage(GalaxyID peer, Stream messageStream)
{
- using (IncomingMessage message = new IncomingMessage())
- using (BinaryReader reader = new BinaryReader(messageStream))
+ using IncomingMessage message = new IncomingMessage();
+ using BinaryReader reader = new BinaryReader(messageStream);
+
+ message.Read(reader);
+ ulong peerID = peer.ToUint64(); // note: GalaxyID instances get reused, so need to store the underlying ID instead
+ this.OnProcessingMessage(message, outgoing => this.SendMessageToPeerID(peerID, outgoing), () =>
{
- message.Read(reader);
- ulong peerID = peer.ToUint64(); // note: GalaxyID instances get reused, so need to store the underlying ID instead
- this.OnProcessingMessage(message, outgoing => this.SendMessageToPeerID(peerID, outgoing), () =>
+ if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peerID)
+ this.gameServer.processIncomingMessage(message);
+ else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction)
{
- if (this.peers.ContainsLeft(message.FarmerID) && (long)this.peers[message.FarmerID] == (long)peerID)
- this.gameServer.processIncomingMessage(message);
- else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction)
- {
- NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader);
- GalaxyID capturedPeer = new GalaxyID(peerID);
- this.gameServer.checkFarmhandRequest(Convert.ToString(peerID), this.getConnectionId(peer), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64());
- }
- });
- }
+ NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader);
+ GalaxyID capturedPeer = new GalaxyID(peerID);
+ this.gameServer.checkFarmhandRequest(Convert.ToString(peerID), this.getConnectionId(peer), farmer, msg => this.sendMessage(capturedPeer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = capturedPeer.ToUint64());
+ }
+ });
}
/// <summary>Send a message to a remote peer.</summary>
diff --git a/src/SMAPI/Framework/Networking/SLidgrenServer.cs b/src/SMAPI/Framework/Networking/SLidgrenServer.cs
index f2c61917..05c8b872 100644
--- a/src/SMAPI/Framework/Networking/SLidgrenServer.cs
+++ b/src/SMAPI/Framework/Networking/SLidgrenServer.cs
@@ -44,25 +44,24 @@ namespace StardewModdingAPI.Framework.Networking
{
// add hook to call multiplayer core
NetConnection peer = rawMessage.SenderConnection;
- using (IncomingMessage message = new IncomingMessage())
- using (Stream readStream = new NetBufferReadStream(rawMessage))
- using (BinaryReader reader = new BinaryReader(readStream))
+ using IncomingMessage message = new IncomingMessage();
+ using Stream readStream = new NetBufferReadStream(rawMessage);
+ using BinaryReader reader = new BinaryReader(readStream);
+
+ while (rawMessage.LengthBits - rawMessage.Position >= 8)
{
- while (rawMessage.LengthBits - rawMessage.Position >= 8)
+ message.Read(reader);
+ NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused
+ this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () =>
{
- message.Read(reader);
- NetConnection connection = rawMessage.SenderConnection; // don't pass rawMessage into context because it gets reused
- this.OnProcessingMessage(message, outgoing => this.sendMessage(connection, outgoing), () =>
+ if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer)
+ this.gameServer.processIncomingMessage(message);
+ else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction)
{
- if (this.peers.ContainsLeft(message.FarmerID) && this.peers[message.FarmerID] == peer)
- this.gameServer.processIncomingMessage(message);
- else if (message.MessageType == StardewValley.Multiplayer.playerIntroduction)
- {
- NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader);
- this.gameServer.checkFarmhandRequest("", this.getConnectionId(rawMessage.SenderConnection), farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer);
- }
- });
- }
+ NetFarmerRoot farmer = this.Multiplayer.readFarmer(message.Reader);
+ this.gameServer.checkFarmhandRequest("", this.getConnectionId(rawMessage.SenderConnection), farmer, msg => this.sendMessage(peer, msg), () => this.peers[farmer.Value.UniqueMultiplayerID] = peer);
+ }
+ });
}
}
}
diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs
index 01197f74..af630055 100644
--- a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs
+++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs
@@ -1,7 +1,7 @@
namespace StardewModdingAPI.Framework.PerformanceMonitoring
{
/// <summary>The context for an alert.</summary>
- internal struct AlertContext
+ internal readonly struct AlertContext
{
/*********
** Accessors
diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs
index f5b80189..d5a0b343 100644
--- a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs
+++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs
@@ -1,7 +1,7 @@
namespace StardewModdingAPI.Framework.PerformanceMonitoring
{
/// <summary>A single alert entry.</summary>
- internal struct AlertEntry
+ internal readonly struct AlertEntry
{
/*********
** Accessors
diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs
index cff502ad..1746e358 100644
--- a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs
+++ b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs
@@ -3,7 +3,7 @@ using System;
namespace StardewModdingAPI.Framework.PerformanceMonitoring
{
/// <summary>A peak invocation time.</summary>
- internal struct PeakEntry
+ internal readonly struct PeakEntry
{
/*********
** Accessors
diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs
index 8adbd88d..18cca628 100644
--- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs
+++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs
@@ -3,7 +3,7 @@ using System;
namespace StardewModdingAPI.Framework.PerformanceMonitoring
{
/// <summary>A single performance counter entry.</summary>
- internal struct PerformanceCounterEntry
+ internal readonly struct PerformanceCounterEntry
{
/*********
** Accessors
diff --git a/src/SMAPI/Framework/Rendering/SDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs
index 382949bf..85e69ae6 100644
--- a/src/SMAPI/Framework/Rendering/SDisplayDevice.cs
+++ b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs
@@ -2,7 +2,6 @@ using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
-using StardewValley;
using xTile.Dimensions;
using xTile.Layers;
using xTile.ObjectModel;
@@ -14,23 +13,13 @@ namespace StardewModdingAPI.Framework.Rendering
internal class SDisplayDevice : SXnaDisplayDevice
{
/*********
- ** Fields
- *********/
- /// <summary>The origin to use when rotating tiles.</summary>
- private readonly Vector2 RotationOrigin;
-
-
- /*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="contentManager">The content manager through which to load tiles.</param>
/// <param name="graphicsDevice">The graphics device with which to render tiles.</param>
public SDisplayDevice(ContentManager contentManager, GraphicsDevice graphicsDevice)
- : base(contentManager, graphicsDevice)
- {
- this.RotationOrigin = new Vector2((Game1.tileSize * Game1.pixelZoom) / 2f);
- }
+ : base(contentManager, graphicsDevice) { }
/// <summary>Draw a tile to the screen.</summary>
/// <param name="tile">The tile to draw.</param>
diff --git a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
index d4f62b4f..121e53bc 100644
--- a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
+++ b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
@@ -10,7 +10,7 @@ using xTile.Layers;
using xTile.Tiles;
using Rectangle = xTile.Dimensions.Rectangle;
-namespace StardewModdingAPI.Framework
+namespace StardewModdingAPI.Framework.Rendering
{
/// <summary>A map display device which reimplements the default logic.</summary>
/// <remarks>This is an exact copy of <see cref="XnaDisplayDevice"/>, except that private fields are protected and all methods are virtual.</remarks>
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index de9c955d..cd292bfc 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -1129,67 +1129,115 @@ namespace StardewModdingAPI.Framework
// log warnings
if (modsWithWarnings.Any())
{
- // issue block format logic
- void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb)
- {
- IModMetadata[] matches = modsWithWarnings
- .Where(mod => mod.HasUnsuppressWarning(warning))
- .ToArray();
- if (!matches.Any())
- return;
-
- this.Monitor.Log(" " + heading, logLevel);
- this.Monitor.Log(" " + "".PadRight(50, '-'), logLevel);
- foreach (string line in blurb)
- this.Monitor.Log(" " + line, logLevel);
- this.Monitor.Newline();
- foreach (IModMetadata match in matches)
- this.Monitor.Log($" - {match.DisplayName}", logLevel);
- this.Monitor.Newline();
- }
-
- // supported issues
- LogWarningGroup(ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods",
+ // broken code
+ this.LogModWarningGroup(modsWithWarnings, ModWarning.BrokenCodeLoaded, LogLevel.Error, "Broken mods",
"These mods have broken code, but you configured SMAPI to load them anyway. This may cause bugs,",
"errors, or crashes in-game."
);
- LogWarningGroup(ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer",
+
+ // changes serializer
+ this.LogModWarningGroup(modsWithWarnings, ModWarning.ChangesSaveSerializer, LogLevel.Warn, "Changed save serializer",
"These mods change the save serializer. They may corrupt your save files, or make them unusable if",
"you uninstall these mods."
);
- if (this.Settings.ParanoidWarnings)
- {
- LogWarningGroup(ModWarning.AccessesConsole, LogLevel.Warn, "Accesses the console directly",
- "These mods directly access the SMAPI console, and you enabled paranoid warnings. (Note that this may be",
- "legitimate and innocent usage; this warning is meaningless without further investigation.)"
- );
- LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly",
- "These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be",
- "legitimate and innocent usage; this warning is meaningless without further investigation.)"
- );
- LogWarningGroup(ModWarning.AccessesShell, LogLevel.Warn, "Accesses shell/process directly",
- "These mods directly access the OS shell or processes, and you enabled paranoid warnings. (Note that",
- "this may be legitimate and innocent usage; this warning is meaningless without further investigation.)"
- );
- }
- LogWarningGroup(ModWarning.PatchesGame, LogLevel.Info, "Patched game code",
+
+ // patched game code
+ this.LogModWarningGroup(modsWithWarnings, ModWarning.PatchesGame, LogLevel.Info, "Patched game code",
"These mods directly change the game code. They're more likely to cause errors or bugs in-game; if",
"your game has issues, try removing these first. Otherwise you can ignore this warning."
);
- LogWarningGroup(ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks",
+
+ // unvalidated update tick
+ this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesUnvalidatedUpdateTick, LogLevel.Info, "Bypassed safety checks",
"These mods bypass SMAPI's normal safety checks, so they're more likely to cause errors or save",
"corruption. If your game has issues, try removing these first."
);
- LogWarningGroup(ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys",
+
+ // paranoid warnings
+ if (this.Settings.ParanoidWarnings)
+ {
+ this.LogModWarningGroup(
+ modsWithWarnings,
+ match: mod => mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole, ModWarning.AccessesFilesystem, ModWarning.AccessesShell),
+ level: LogLevel.Debug,
+ heading: "Direct system access",
+ blurb: new[]
+ {
+ "You enabled paranoid warnings and these mods directly access the filesystem, shells/processes, or",
+ "SMAPI console. (This is usually legitimate and innocent usage; this warning is only useful for",
+ "further investigation.)"
+ },
+ modLabel: mod =>
+ {
+ List<string> labels = new List<string>();
+ if (mod.HasUnsuppressedWarnings(ModWarning.AccessesConsole))
+ labels.Add("console");
+ if (mod.HasUnsuppressedWarnings(ModWarning.AccessesFilesystem))
+ labels.Add("files");
+ if (mod.HasUnsuppressedWarnings(ModWarning.AccessesShell))
+ labels.Add("shells/processes");
+
+ return $"{mod.DisplayName} ({string.Join(", ", labels)})";
+ }
+ );
+ }
+
+ // no update keys
+ this.LogModWarningGroup(modsWithWarnings, ModWarning.NoUpdateKeys, LogLevel.Debug, "No update keys",
"These mods have no update keys in their manifest. SMAPI may not notify you about updates for these",
"mods. Consider notifying the mod authors about this problem."
);
- LogWarningGroup(ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform",
+
+ // not crossplatform
+ this.LogModWarningGroup(modsWithWarnings, ModWarning.UsesDynamic, LogLevel.Debug, "Not crossplatform",
"These mods use the 'dynamic' keyword, and won't work on Linux/Mac."
);
}
}
+ /// <summary>Write a mod warning group to the console and log.</summary>
+ /// <param name="mods">The mods to search.</param>
+ /// <param name="match">Matches mods to include in the warning group.</param>
+ /// <param name="level">The log level for the logged messages.</param>
+ /// <param name="heading">A brief heading label for the group.</param>
+ /// <param name="blurb">A detailed explanation of the warning, split into lines.</param>
+ /// <param name="modLabel">Formats the mod label, or <c>null</c> to use the <see cref="IModMetadata.DisplayName"/>.</param>
+ private void LogModWarningGroup(IModMetadata[] mods, Func<IModMetadata, bool> match, LogLevel level, string heading, string[] blurb, Func<IModMetadata, string> modLabel = null)
+ {
+ // get matching mods
+ string[] modLabels = mods
+ .Where(match)
+ .Select(mod => modLabel?.Invoke(mod) ?? mod.DisplayName)
+ .OrderBy(p => p)
+ .ToArray();
+ if (!modLabels.Any())
+ return;
+
+ // log header/blurb
+ this.Monitor.Log(" " + heading, level);
+ this.Monitor.Log(" " + "".PadRight(50, '-'), level);
+ foreach (string line in blurb)
+ this.Monitor.Log(" " + line, level);
+ this.Monitor.Newline();
+
+ // log mod list
+ foreach (string label in modLabels)
+ this.Monitor.Log($" - {label}", level);
+
+ this.Monitor.Newline();
+ }
+
+ /// <summary>Write a mod warning group to the console and log.</summary>
+ /// <param name="mods">The mods to search.</param>
+ /// <param name="warning">The mod warning to match.</param>
+ /// <param name="level">The log level for the logged messages.</param>
+ /// <param name="heading">A brief heading label for the group.</param>
+ /// <param name="blurb">A detailed explanation of the warning, split into lines.</param>
+ void LogModWarningGroup(IModMetadata[] mods, ModWarning warning, LogLevel level, string heading, params string[] blurb)
+ {
+ this.LogModWarningGroup(mods, mod => mod.HasUnsuppressedWarnings(warning), level, heading, blurb);
+ }
+
/// <summary>Load a mod's entry class.</summary>
/// <param name="modAssembly">The mod assembly.</param>
/// <param name="mod">The loaded instance.</param>
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 2a30b595..82db5857 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -1310,7 +1310,7 @@ namespace StardewModdingAPI.Framework
}
Game1.drawPlayerHeldObject(Game1.player);
}
- label_139:
+ label_139:
if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)))
Game1.drawTool(Game1.player);
if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null)
diff --git a/src/SMAPI/Framework/Serialization/ColorConverter.cs b/src/SMAPI/Framework/Serialization/ColorConverter.cs
index 19979981..7315f1a5 100644
--- a/src/SMAPI/Framework/Serialization/ColorConverter.cs
+++ b/src/SMAPI/Framework/Serialization/ColorConverter.cs
@@ -35,7 +35,7 @@ namespace StardewModdingAPI.Framework.Serialization
{
string[] parts = str.Split(',');
if (parts.Length != 4)
- throw new SParseException($"Can't parse {typeof(Color).Name} from invalid value '{str}' (path: {path}).");
+ throw new SParseException($"Can't parse {nameof(Color)} from invalid value '{str}' (path: {path}).");
int r = Convert.ToInt32(parts[0]);
int g = Convert.ToInt32(parts[1]);
diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs
index 3481c9b2..6cf795dc 100644
--- a/src/SMAPI/Framework/Serialization/PointConverter.cs
+++ b/src/SMAPI/Framework/Serialization/PointConverter.cs
@@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.Serialization
{
string[] parts = str.Split(',');
if (parts.Length != 2)
- throw new SParseException($"Can't parse {typeof(Point).Name} from invalid value '{str}' (path: {path}).");
+ throw new SParseException($"Can't parse {nameof(Point)} from invalid value '{str}' (path: {path}).");
int x = Convert.ToInt32(parts[0]);
int y = Convert.ToInt32(parts[1]);
diff --git a/src/SMAPI/Framework/Serialization/RectangleConverter.cs b/src/SMAPI/Framework/Serialization/RectangleConverter.cs
index fbb2e253..a5780d8a 100644
--- a/src/SMAPI/Framework/Serialization/RectangleConverter.cs
+++ b/src/SMAPI/Framework/Serialization/RectangleConverter.cs
@@ -39,7 +39,7 @@ namespace StardewModdingAPI.Framework.Serialization
var match = Regex.Match(str, @"^\{X:(?<x>\d+) Y:(?<y>\d+) Width:(?<width>\d+) Height:(?<height>\d+)\}$", RegexOptions.IgnoreCase);
if (!match.Success)
- throw new SParseException($"Can't parse {typeof(Rectangle).Name} from invalid value '{str}' (path: {path}).");
+ throw new SParseException($"Can't parse {nameof(Rectangle)} from invalid value '{str}' (path: {path}).");
int x = Convert.ToInt32(match.Groups["x"].Value);
int y = Convert.ToInt32(match.Groups["y"].Value);
diff --git a/src/SMAPI/Framework/Serialization/Vector2Converter.cs b/src/SMAPI/Framework/Serialization/Vector2Converter.cs
index 1d9b08e0..3e2ab776 100644
--- a/src/SMAPI/Framework/Serialization/Vector2Converter.cs
+++ b/src/SMAPI/Framework/Serialization/Vector2Converter.cs
@@ -33,7 +33,7 @@ namespace StardewModdingAPI.Framework.Serialization
{
string[] parts = str.Split(',');
if (parts.Length != 2)
- throw new SParseException($"Can't parse {typeof(Vector2).Name} from invalid value '{str}' (path: {path}).");
+ throw new SParseException($"Can't parse {nameof(Vector2)} from invalid value '{str}' (path: {path}).");
float x = Convert.ToSingle(parts[0]);
float y = Convert.ToSingle(parts[1]);