diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-06-28 18:17:27 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2022-06-28 18:17:27 -0400 |
commit | 6d11c41facb2e1397a25110517cc281f87be2caf (patch) | |
tree | 5a1ee7b4f8414b359cf3e7cff5c1d77433f2029d | |
parent | 5e1212e99aa7cb8c0333a57fa60236df79b0ac6d (diff) | |
download | SMAPI-6d11c41facb2e1397a25110517cc281f87be2caf.tar.gz SMAPI-6d11c41facb2e1397a25110517cc281f87be2caf.tar.bz2 SMAPI-6d11c41facb2e1397a25110517cc281f87be2caf.zip |
migrate update checks to FluentHttpClient
WebClient isn't needed for compatibility with macOS after the .NET 5 update in Stardew Valley 1.5.5, and causes noticeable lag for some players even when running on a background thread.
-rw-r--r-- | build/deploy-local-smapi.targets | 4 | ||||
-rwxr-xr-x | build/unix/prepare-install-package.sh | 2 | ||||
-rw-r--r-- | build/windows/prepare-install-package.ps1 | 2 | ||||
-rw-r--r-- | docs/release-notes.md | 5 | ||||
-rw-r--r-- | src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs | 56 | ||||
-rw-r--r-- | src/SMAPI.Toolkit/Serialization/JsonHelper.cs | 26 | ||||
-rw-r--r-- | src/SMAPI.Web/Startup.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 44 | ||||
-rw-r--r-- | src/SMAPI/SMAPI.csproj | 1 |
9 files changed, 78 insertions, 64 deletions
diff --git a/build/deploy-local-smapi.targets b/build/deploy-local-smapi.targets index cb330e24..6ea5f0a2 100644 --- a/build/deploy-local-smapi.targets +++ b/build/deploy-local-smapi.targets @@ -35,6 +35,10 @@ This assumes `find-game-folder.targets` has already been imported and validated. <Copy SourceFiles="$(TargetDir)\Mono.Cecil.Pdb.dll" DestinationFolder="$(GamePath)\smapi-internal" /> <Copy SourceFiles="$(TargetDir)\MonoMod.Common.dll" DestinationFolder="$(GamePath)\smapi-internal" /> + <!-- FluentHttpClient + dependencies --> + <Copy SourceFiles="$(TargetDir)\Pathoschild.Http.Client.dll" DestinationFolder="$(GamePath)\smapi-internal" /> + <Copy SourceFiles="$(TargetDir)\System.Net.Http.Formatting.dll" DestinationFolder="$(GamePath)\smapi-internal" /> + <!-- .NET dependencies --> <Copy SourceFiles="$(TargetDir)\System.Management.dll" DestinationFolder="$(GamePath)\smapi-internal" Condition="$(OS) == 'Windows_NT'" /> diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 01cd2080..1d805e00 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -134,7 +134,7 @@ for folder in ${folders[@]}; do cp -r "$smapiBin/i18n" "$bundlePath/smapi-internal" # bundle smapi-internal - for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.pdb" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.pdb" "SMAPI.Toolkit.CoreInterfaces.xml"; do + for name in "0Harmony.dll" "0Harmony.xml" "Mono.Cecil.dll" "Mono.Cecil.Mdb.dll" "Mono.Cecil.Pdb.dll" "MonoMod.Common.dll" "Newtonsoft.Json.dll" "Pathoschild.Http.Client.dll" "Pintail.dll" "TMXTile.dll" "SMAPI.Toolkit.dll" "SMAPI.Toolkit.pdb" "SMAPI.Toolkit.xml" "SMAPI.Toolkit.CoreInterfaces.dll" "SMAPI.Toolkit.CoreInterfaces.pdb" "SMAPI.Toolkit.CoreInterfaces.xml" "System.Net.Http.Formatting.dll"; do cp "$smapiBin/$name" "$bundlePath/smapi-internal" done diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index 7e3c6c86..87a4fe01 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -154,7 +154,7 @@ foreach ($folder in $folders) { cp -Recurse "$smapiBin/i18n" "$bundlePath/smapi-internal" # bundle smapi-internal - foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml")) { + foreach ($name in @("0Harmony.dll", "0Harmony.xml", "Mono.Cecil.dll", "Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll", "MonoMod.Common.dll", "Newtonsoft.Json.dll", "Pathoschild.Http.Client.dll", "Pintail.dll", "TMXTile.dll", "SMAPI.Toolkit.dll", "SMAPI.Toolkit.pdb", "SMAPI.Toolkit.xml", "SMAPI.Toolkit.CoreInterfaces.dll", "SMAPI.Toolkit.CoreInterfaces.pdb", "SMAPI.Toolkit.CoreInterfaces.xml", "System.Net.Http.Formatting.dll")) { cp "$smapiBin/$name" "$bundlePath/smapi-internal" } diff --git a/docs/release-notes.md b/docs/release-notes.md index 4c504150..414396f4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,9 +9,12 @@ ## Upcoming release * For players: - * Minor optimizations. + * Fixed lag which occurred for some players since Stardew Valley 1.5.5. * Fixed `smapi-internal/config.user.json` overrides not applied after SMAPI 3.14.0. +* For mod authors: + * The [FluentHttpClient package](https://github.com/Pathoschild/FluentHttpClient#readme) is now loaded by SMAPI. + * For the web UI: * Updated the JSON validator/schema for Content Patcher 1.27.0. * Fixed the mod count in the log parser metadata. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs index d4282617..ef1904d4 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs @@ -1,27 +1,24 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; -using Newtonsoft.Json; +using System.Threading.Tasks; +using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// <summary>Provides methods for interacting with the SMAPI web API.</summary> - public class WebApiClient + public class WebApiClient : IDisposable { /********* ** Fields *********/ - /// <summary>The base URL for the web API.</summary> - private readonly Uri BaseUrl; - /// <summary>The API version number.</summary> private readonly ISemanticVersion Version; - /// <summary>The JSON serializer settings to use.</summary> - private readonly JsonSerializerSettings JsonSettings = new JsonHelper().JsonSettings; + /// <summary>The underlying HTTP client.</summary> + private readonly IClient Client; /********* @@ -32,8 +29,11 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <param name="version">The web API version.</param> public WebApiClient(string baseUrl, ISemanticVersion version) { - this.BaseUrl = new Uri(baseUrl); this.Version = version; + this.Client = new FluentClient(baseUrl) + .SetUserAgent($"SMAPI/{version}"); + + this.Client.Formatters.JsonFormatter.SerializerSettings = JsonHelper.CreateDefaultSettings(); } /// <summary>Get metadata about a set of mods from the web API.</summary> @@ -42,36 +42,22 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// <param name="gameVersion">The Stardew Valley version installed by the player.</param> /// <param name="platform">The OS on which the player plays.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> - public IDictionary<string, ModEntryModel> GetModInfo(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false) + public async Task<IDictionary<string, ModEntryModel>> GetModInfoAsync(ModSearchEntryModel[] mods, ISemanticVersion apiVersion, ISemanticVersion gameVersion, Platform platform, bool includeExtendedMetadata = false) { - return this.Post<ModSearchModel, ModEntryModel[]>( - $"v{this.Version}/mods", - new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata) - ).ToDictionary(p => p.ID); + ModEntryModel[] result = await this.Client + .PostAsync( + $"v{this.Version}/mods", + new ModSearchModel(mods, apiVersion, gameVersion, platform, includeExtendedMetadata) + ) + .As<ModEntryModel[]>(); + + return result.ToDictionary(p => p.ID); } - - /********* - ** Private methods - *********/ - /// <summary>Fetch the response from the backend API.</summary> - /// <typeparam name="TBody">The body content type.</typeparam> - /// <typeparam name="TResult">The expected response type.</typeparam> - /// <param name="url">The request URL, optionally excluding the base URL.</param> - /// <param name="content">The body content to post.</param> - private TResult Post<TBody, TResult>(string url, TBody content) + /// <inheritdoc /> + public void Dispose() { - // note: avoid HttpClient for macOS compatibility - using WebClient client = new(); - - Uri fullUrl = new(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) - ?? throw new InvalidOperationException($"Could not parse the response from POST {url}."); + this.Client.Dispose(); } } } diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs index 1a003c51..a5d7e2e8 100644 --- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs +++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs @@ -15,21 +15,27 @@ namespace StardewModdingAPI.Toolkit.Serialization ** Accessors *********/ /// <summary>The JSON settings to use when serializing and deserializing files.</summary> - public JsonSerializerSettings JsonSettings { get; } = new() - { - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded - Converters = new List<JsonConverter> - { - new SemanticVersionConverter(), - new StringEnumConverter() - } - }; + public JsonSerializerSettings JsonSettings { get; } = JsonHelper.CreateDefaultSettings(); /********* ** Public methods *********/ + /// <summary>Create an instance of the default JSON serializer settings.</summary> + public static JsonSerializerSettings CreateDefaultSettings() + { + return new() + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace, // avoid issue where default ICollection<T> values are duplicated each time the config is loaded + Converters = new List<JsonConverter> + { + new SemanticVersionConverter(), + new StringEnumConverter() + } + }; + } + /// <summary>Read a JSON file.</summary> /// <typeparam name="TModel">The model type.</typeparam> /// <param name="fullPath">The absolute file path.</param> diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 9980d00c..54c25979 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -199,7 +199,7 @@ namespace StardewModdingAPI.Web /// <param name="settings">The serializer settings to edit.</param> private void ConfigureJsonNet(JsonSerializerSettings settings) { - foreach (JsonConverter converter in new JsonHelper().JsonSettings.Converters) + foreach (JsonConverter converter in JsonHelper.CreateDefaultSettings().Converters) settings.Converters.Add(converter); settings.Formatting = Formatting.Indented; diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 16c168a0..fdfe70fc 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -10,6 +10,7 @@ using System.Runtime.ExceptionServices; using System.Security; using System.Text; using System.Threading; +using System.Threading.Tasks; using Microsoft.Xna.Framework; #if SMAPI_FOR_WINDOWS using Microsoft.Win32; @@ -406,7 +407,7 @@ namespace StardewModdingAPI.Framework this.CheckForSoftwareConflicts(); // check for updates - this.CheckForUpdatesAsync(mods); + _ = this.CheckForUpdatesAsync(mods); // ignore task since the main thread doesn't need to wait for it } // update window titles @@ -1450,16 +1451,15 @@ namespace StardewModdingAPI.Framework /// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> /// <param name="mods">The mods to include in the update check (if eligible).</param> - private void CheckForUpdatesAsync(IModMetadata[] mods) + private async Task CheckForUpdatesAsync(IModMetadata[] mods) { - if (!this.Settings.CheckForUpdates) - return; - - new Thread(() => + try { + if (!this.Settings.CheckForUpdates) + return; + // create client - string url = this.Settings.WebApiBaseUrl; - WebApiClient client = new(url, Constants.ApiVersion); + using WebApiClient client = new(this.Settings.WebApiBaseUrl, Constants.ApiVersion); this.Monitor.Log("Checking for updates..."); // check SMAPI version @@ -1469,9 +1469,15 @@ namespace StardewModdingAPI.Framework try { // fetch update check - ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }, apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform).Single().Value; - updateFound = response.SuggestedUpdate?.Version; - updateUrl = response.SuggestedUpdate?.Url; + IDictionary<string, ModEntryModel> response = await client.GetModInfoAsync( + mods: new[] { new ModSearchEntryModel("Pathoschild.SMAPI", Constants.ApiVersion, new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }, + apiVersion: Constants.ApiVersion, + gameVersion: Constants.GameVersion, + platform: Constants.Platform + ); + ModEntryModel updateInfo = response.Single().Value; + updateFound = updateInfo.SuggestedUpdate?.Version; + updateUrl = updateInfo.SuggestedUpdate?.Url; // log message if (updateFound != null) @@ -1480,10 +1486,10 @@ namespace StardewModdingAPI.Framework this.Monitor.Log(" SMAPI okay."); // show errors - if (response.Errors.Any()) + if (updateInfo.Errors.Any()) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); - this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}"); + this.Monitor.Log($"Error: {string.Join("\n", updateInfo.Errors)}"); } } catch (Exception ex) @@ -1523,7 +1529,7 @@ namespace StardewModdingAPI.Framework // fetch results this.Monitor.Log($" Checking for updates to {searchMods.Count} mods..."); - IDictionary<string, ModEntryModel> results = client.GetModInfo(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform); + IDictionary<string, ModEntryModel> results = await client.GetModInfoAsync(searchMods.ToArray(), apiVersion: Constants.ApiVersion, gameVersion: Constants.GameVersion, platform: Constants.Platform); // extract update alerts & errors var updates = new List<Tuple<IModMetadata, ISemanticVersion, string>>(); @@ -1573,7 +1579,15 @@ namespace StardewModdingAPI.Framework ); } } - }).Start(); + } + catch (Exception ex) + { + this.Monitor.Log("Couldn't check for updates. This won't affect your game, but you won't be notified of SMAPI or mod updates if this keeps happening.", LogLevel.Warn); + this.Monitor.Log(ex is WebException && ex.InnerException == null + ? ex.Message + : ex.ToString() + ); + } } /// <summary>Create a directory path if it doesn't exist.</summary> diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 3abefeab..c05512e9 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -25,6 +25,7 @@ <PackageReference Include="Mono.Cecil" Version="0.11.4" /> <PackageReference Include="MonoMod.Common" Version="22.3.5.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> + <PackageReference Include="Pathoschild.Http.FluentClient" Version="4.1.1" /> <PackageReference Include="Pintail" Version="2.2.0" /> <PackageReference Include="Platonymous.TMXTile" Version="1.5.9" /> <PackageReference Include="System.Reflection.Emit" Version="4.7.0" /> |